17uint32_t stamp(uint32_t now) {
return now == 0 ? 1 : now; }
19bool reached(uint32_t now, uint32_t t) {
return static_cast<int32_t
>(now - t) > 0; }
24 t.last_remaining_s.reset();
26 t.predicted_end_read =
false;
31 tracking_[device_id].interval_ms = interval_ms;
35 auto it = tracking_.find(device_id);
36 return (it != tracking_.end()) ? it->second.interval_ms : 0;
40 state_unknown_.erase(device_id);
43 t.
next_update = (initial_delay_ms == 0) ? 0 : (now + initial_delay_ms);
49 drop_predicted_end(t);
53 state_unknown_.erase(device_id);
54 auto it = tracking_.find(device_id);
55 if (it == tracking_.end())
65 drop_predicted_end(t);
74 note_ended_(device_id);
86 auto it = tracking_.find(device_id);
87 if (it == tracking_.end())
91 if (!remaining_s.has_value()) {
92 drop_predicted_end(t);
106 const uint32_t aim = now + (*remaining_s * 1000U) + PREDICTED_END_MARGIN_MS;
114 auto it = tracking_.find(device_id);
115 if (it == tracking_.end() || it->second.stale_reports < PREDICTED_END_STALE_REPORTS)
117 switch (it->second.stale_reports - PREDICTED_END_STALE_REPORTS) {
119 return FROZEN_COUNT_BACKOFF_FIRST_MS;
121 return FROZEN_COUNT_BACKOFF_SECOND_MS;
123 return FROZEN_COUNT_BACKOFF_MAX_MS;
128 auto it = tracking_.find(device_id);
129 return (it != tracking_.end()) ? it->second.settle_update : 0;
133 tracking_[device_id].next_update = abs_time;
137 auto it = tracking_.find(device_id);
138 return (it != tracking_.end()) ? it->second.next_update : 0;
142 auto it = tracking_.find(device_id);
143 return (it != tracking_.end()) ? it->second.poll_deadline : 0;
147 auto it = tracking_.find(device_id);
148 if (it == tracking_.end())
155 bool device_moving) {
156 auto it = tracking_.find(device_id);
157 if (it == tracking_.end())
168 const uint32_t delay_ms =
196 note_ended_(device_id);
202 auto it = tracking_.find(device_id);
203 if (it == tracking_.end())
205 it->second.status_poll_failures = 0;
206 it->second.auth_poll_failures = 0;
207 it->second.moving_poll_misses = 0;
211 auto it = tracking_.find(device_id);
212 if (it != tracking_.end())
213 it->second.stop_settle =
true;
217 auto it = tracking_.find(device_id);
218 if (it == tracking_.end())
220 const bool marked = it->second.stop_settle;
221 it->second.stop_settle =
false;
226 auto it = tracking_.find(device_id);
227 if (it != tracking_.end())
228 it->second.confirm_stopped = needed;
232 auto it = tracking_.find(device_id);
233 if (it == tracking_.end())
235 const bool marked = it->second.confirm_stopped;
236 it->second.confirm_stopped =
false;
241 auto it = tracking_.find(device_id);
242 return it != tracking_.end() && it->second.predicted_end_read;
248 bool predicted_end =
false;
249 auto it = tracking_.find(device_id);
250 if (it != tracking_.end()) {
251 predicted_end = it->second.predicted_end_read;
252 it->second.predicted_end_read =
false;
254 return after_stop || confirms_stop || predicted_end;
258 auto it = tracking_.find(device_id);
259 return (it != tracking_.end()) ? it->second.status_poll_failures : 0;
263 auto it = tracking_.find(device_id);
264 return (it != tracking_.end()) ? it->second.auth_poll_failures : 0;
268 auto it = tracking_.find(device_id);
269 return (it != tracking_.end()) ? it->second.moving_poll_misses : 0;
274bool older_than(uint32_t now, uint32_t t, uint32_t span_ms) {
return t == 0 || now - t >= span_ms; }
278 auto it = foreign_.find(device_id);
279 if (it == foreign_.end() || it->second.pending_due_ms == 0)
283 const uint32_t since_due = now - it->second.pending_due_ms;
284 return since_due > INT32_MAX || since_due <= REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS;
288 auto it = foreign_.find(device_id);
289 if (it != foreign_.end())
290 it->second.pending_due_ms = 0;
294 const std::string &controller, uint8_t cmd, uint32_t now,
295 uint32_t last_status_ms) {
307 f.
pending_due_ms = stamp(now + REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS);
316 arm = older_than(now, last_status_ms, FOREIGN_READ_RESYNC_MS) &&
332 f.
pending_due_ms = stamp(now + REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS);
337 for (
auto &pair : tracking_) {
341 if (!predicted_end_due && !progress_due)
345 note_ended_(pair.first);
349 if (predicted_end_due) {
357 static_cast<int32_t
>(t.
settle_update - now) <=
static_cast<int32_t
>(PREDICTED_END_DROPS_PROGRESS_WITHIN_MS)) {
370 std::string
id = ended_.front();
371 ended_.erase(ended_.begin());
378 if (!catchup_burst_open_)
381 catchup_burst_open_ =
false;
385 catchup_due_ms_ = stamp(now + delay_ms);
394 if (!catchup_burst_open_ || catchup_due_ms_ == 0 || now - catchup_due_ms_ > INT32_MAX)
396 catchup_burst_open_ =
false;
401void StatusPollPolicy::note_ended_(
const std::string &device_id) {
402 if (std::find(ended_.begin(), ended_.end(), device_id) == ended_.end())
403 ended_.push_back(device_id);
409constexpr std::array<uint32_t, 5> SILENT_RETRY_LADDER_MS{
410 STATUS_RETRY_AFTER_FAIL_MS, STATUS_RETRY_AFTER_FAIL_STEP2_MS, STATUS_RETRY_AFTER_FAIL_STEP3_MS,
411 STATUS_RETRY_AFTER_FAIL_STEP4_MS, STATUS_RETRY_AFTER_FAIL_MAX_MS};
412constexpr std::array<uint32_t, 4> AUTH_RETRY_LADDER_MS{
413 STATUS_AUTH_RETRY_AFTER_FAIL_MS, STATUS_AUTH_RETRY_AFTER_FAIL_STEP2_MS, STATUS_AUTH_RETRY_AFTER_FAIL_STEP3_MS,
414 STATUS_AUTH_RETRY_AFTER_FAIL_MAX_MS};
417template<
size_t N> uint32_t ladder_step(
const std::array<uint32_t, N> &ladder, uint8_t consecutive_failures) {
418 const size_t index = consecutive_failures <= 1 ? 0 : consecutive_failures - 1;
419 return ladder[std::min(index, N - 1)];
424uint32_t StatusPollPolicy::retry_delay_ms(uint8_t consecutive_failures,
bool auth_like) {
425 return auth_like ? ladder_step(AUTH_RETRY_LADDER_MS, consecutive_failures)
426 : ladder_step(SILENT_RETRY_LADDER_MS, consecutive_failures);
void mark_stop_settle(const std::string &device_id)
Mark the device's next poll as the one reading where an accepted STOP left it, so it is a settling re...
uint32_t frozen_count_delay_ms(const std::string &device_id) const
How long the next progress read waits when the device's remaining time has stopped falling.
bool take_settling_read(const std::string &device_id)
Consume the marks (none short-circuits another) for the poll being dispatched.
ForeignAction on_foreign_frame(const std::string &device_id, ForeignFrameKind kind, const std::string &controller, uint8_t cmd, uint32_t now, uint32_t last_status_ms)
Decide what an overheard frame of another controller means for this device.
void on_exchange_ended(uint32_t now, uint32_t delay_ms)
An exchange ended at now.
bool take_state_unknown(const std::string &device_id)
Consume the mark set by mark_state_unknown(): true once, for the status read dispatched after it.
void set_interval(const std::string &device_id, uint32_t interval_ms)
Set the configured follow-up poll interval (ms; 0 = legacy one-shot settle only).
void set_confirm_stopped(const std::string &device_id, bool needed)
Mark (or unmark) the device's next poll as the one confirming a stop off target, so it is a settling ...
bool is_tracking_active(const std::string &device_id, uint32_t now) const
True when the device has an active bounded polling window (deadline set and not expired).
uint32_t get_next_update(const std::string &device_id) const
MovingReportAction on_moving_report(const std::string &device_id, uint32_t now, bool may_open)
Decide what a status report that reads as moving means for the follow-up window.
uint32_t get_interval(const std::string &device_id) const
Return the configured interval (0 when not set or unconfigured).
void clear_failure_streaks(const std::string &device_id)
Reset failure streaks (and skipped moving misses) after a successful exchange response.
void on_remote_poll_ran(const std::string &device_id)
The poll armed by foreign traffic ran (or was dropped): nothing is pending any more.
void on_linked_remote_activity()
A linked remote was heard: open a burst, or keep the open one.
void set_next_update(const std::string &device_id, uint32_t abs_time)
Schedule the next poll at an absolute millis() timestamp.
uint32_t on_exchange_failed(const std::string &device_id, bool auth_like, uint32_t now, bool device_moving=false)
Record a failed background poll; apply backoff or clear tracking if the window expired.
bool take_confirm_stopped(const std::string &device_id)
Consume the mark set by set_confirm_stopped(): true once, for the first poll dispatched after it.
uint8_t get_moving_poll_misses(const std::string &device_id) const
Consecutive failed polls of a moving device skipped so far.
void begin_tracking(const std::string &device_id, uint32_t initial_delay_ms, uint32_t now)
Begin bounded follow-up polling after a command or remote activity.
uint8_t get_status_poll_failures(const std::string &device_id) const
bool take_due_catchup(uint32_t now)
True once when the open burst's catch-up is due at now.
void on_trusted_report(const std::string &device_id, std::optional< uint16_t > remaining_s, uint32_t now)
Aim or re-aim the predicted-end read from a trusted status report of a moving device.
void mark_state_unknown(const std::string &device_id)
Mark the device's next status read as a catch-up read: a press the hub missed may have set it moving ...
uint8_t get_auth_poll_failures(const std::string &device_id) const
uint32_t get_poll_deadline(const std::string &device_id) const
bool take_stop_settle(const std::string &device_id)
Consume the mark set by mark_stop_settle(): true once, for the first poll dispatched after it.
std::optional< std::string > pop_ended_device()
Return and consume a device whose polling window ended without the device being reported at rest: the...
void clear(const std::string &device_id)
Stop all polling for a device and reset failure counts. Preserves interval_ms.
bool is_remote_poll_pending(const std::string &device_id, uint32_t now) const
True while a poll armed by foreign traffic is pending.
uint32_t get_settle_update(const std::string &device_id) const
The planned predicted-end read's timestamp (0 = none).
std::optional< std::string > pop_due_device(uint32_t now)
Return and consume the first device with a poll overdue.
bool is_predicted_end_read(const std::string &device_id) const
True when the next poll is the predicted-end read (the mark take_settling_read() consumes).
Pure transition helpers for hub-owned exchange and pairing frame decisions.
bool overheard_command_writes(uint8_t cmd)
Returns true for the commands that write to a device: an execute, a mode activation or a private regi...
ForeignFrameKind
What an overheard frame of another controller is.
@ REQUEST_TO_DEVICE
Another controller's command to a registered device.
uint32_t settle_delay_ms(uint32_t interval_ms, uint32_t remaining_delay_ms, bool cap_for_stop)
Resolve the follow-up settle-poll delay while a device may still be moving.
MovingReportAction
What the hub does with a status report that reads as moving.
@ FOLLOW
A window is open: schedule the next poll as usual.
@ OPEN
No window: one was opened now; schedule the next poll as usual.
@ END
The window ran out: it is closed and reported ended; no further poll.
@ NONE
No window and none may be opened: no poll.
ForeignAction
What the hub does about an overheard foreign frame.
@ PUSH_PENDING
A poll armed by foreign traffic is pending: move its timer out, change nothing else.
@ ARM
Schedule a poll REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS from now and begin tracking.
@ IGNORE
Nothing: no poll, no tracking change.
Per-device poll scheduling, failure backoff, and follow-up-poll state machine.
Per-device bookkeeping of overheard foreign traffic.
std::string last_request_from
Controller that sent it (frame source); empty = none heard.
uint32_t last_request_ms
When it was heard; 0 = never.
uint32_t pending_due_ms
When the armed poll is due; 0 = none pending.
uint8_t last_request_cmd
Command of the last request heard to this device.
uint32_t last_read_poll_ms
When a read last armed a poll; 0 = never.
Per-device poll scheduling state.
uint8_t auth_poll_failures
Consecutive background poll failures that saw a 0x3C challenge.
uint32_t settle_update
Absolute millis() timestamp of the predicted-end read; 0 = none planned.
bool confirm_stopped
The next poll confirms a stop off target (a settling read, see decisions::StoppedReport::SETTLING).
bool stop_settle
The next poll reads where an accepted STOP left the device (a settling read).
uint8_t status_poll_failures
Consecutive background poll failures (silent — no reply).
uint32_t poll_deadline
Hard stop for bounded follow-up polling.
uint8_t stale_reports
Reports in a row whose remaining time did not fall.
bool predicted_end_read
The next poll is the predicted-end read (a settling read).
std::optional< uint16_t > last_remaining_s
Remaining time of the last report that was aimed at (> 2 s).
uint32_t interval_ms
Configured follow-up poll interval (0 = no configured cadence; delays fall back to remaining time / d...
uint8_t moving_poll_misses
Consecutive failed polls of a moving device skipped so far (see MOVING_POLL_MISSES_SKIPPED); not coun...
uint32_t next_update
Absolute millis() timestamp for the next poll; 0 = idle.