|
| 1 | +// ============================================================================ |
| 2 | +// idle_state.h — night-mode suspension, as pure deadline arithmetic |
| 3 | +// ============================================================================ |
| 4 | +// |
| 5 | +// Arduino-free for the same reason gate_logic.h and measurement_json.h are: the |
| 6 | +// interesting part of "stop counting until told otherwise" is entirely about |
| 7 | +// clock arithmetic that is awkward to reproduce on a bench and impossible to |
| 8 | +// reproduce on a hive. It is exercised by test/test_idle_state/ on a host |
| 9 | +// compiler; src/main.cpp owns the emitters and the poll loop and calls in here |
| 10 | +// to decide whether to run them. |
| 11 | +// |
| 12 | +// The model |
| 13 | +// --------- |
| 14 | +// The counter never learns what time it is. HiveHub writes SET_IDLE with a |
| 15 | +// DURATION, this header turns that into a millis() deadline, and every poll |
| 16 | +// asks whether the deadline has passed. Three properties fall out of that and |
| 17 | +// they are the whole design: |
| 18 | +// |
| 19 | +// 1. **It expires.** A HiveHub that crashes, loses power, or is carried away |
| 20 | +// cannot leave a counter suspended: the deadline runs out and sensing |
| 21 | +// resumes on its own. Compare a stored 20:00-06:00 schedule, which stays |
| 22 | +// wrong until someone walks to the hive. |
| 23 | +// 2. **It is bounded.** Requests are clamped to MAX_IDLE_SECONDS rather than |
| 24 | +// refused, so a malformed or over-eager duration costs a re-arm next |
| 25 | +// cycle, not a night of running emitters. |
| 26 | +// 3. **It is not persistent.** There is no NVS write and no RTC-memory copy. |
| 27 | +// Any reset — brownout, OTA, watchdog — comes back counting. |
| 28 | +// |
| 29 | +// millis() rollover |
| 30 | +// ----------------- |
| 31 | +// Deadlines are compared with signed differences, like the rest of this |
| 32 | +// firmware, so the ~49.7-day millis() wrap is a non-event: (int32_t)(now - |
| 33 | +// deadline) >= 0 stays correct across it as long as the interval itself is |
| 34 | +// shorter than half the counter's range, which MAX_IDLE_SECONDS (1 h) is by |
| 35 | +// four orders of magnitude. |
| 36 | +// ============================================================================ |
| 37 | + |
| 38 | +#pragma once |
| 39 | + |
| 40 | +#include <stdint.h> |
| 41 | + |
| 42 | +#include "counter_protocol.h" |
| 43 | + |
| 44 | +namespace idlestate { |
| 45 | + |
| 46 | +// Suspension state. Default-constructed is "sensing", which is what a freshly |
| 47 | +// booted counter must always be. |
| 48 | +struct State { |
| 49 | + bool active = false; |
| 50 | + uint32_t deadline_ms = 0; // only meaningful while active |
| 51 | +}; |
| 52 | + |
| 53 | +// Result of a SET_IDLE request, so the caller can log what it actually did |
| 54 | +// rather than what it was asked to do. |
| 55 | +struct Request { |
| 56 | + uint32_t granted_s = 0; // duration actually applied, after clamping |
| 57 | + bool clamped = false; |
| 58 | +}; |
| 59 | + |
| 60 | +// Arm (or re-arm) the suspension for `duration_s` seconds from `now_ms`. |
| 61 | +// |
| 62 | +// A zero duration resumes sensing immediately — that is the same thing |
| 63 | +// CTRL_OP_RESUME does, and accepting it here means HiveHub can express "not |
| 64 | +// tonight" by re-arming with 0 rather than needing a second opcode on a path |
| 65 | +// where it already has one. |
| 66 | +// |
| 67 | +// Re-arming while already idle is the normal case, not an edge case: HiveHub |
| 68 | +// pushes a fresh deadline every upload cycle for as long as the night window |
| 69 | +// lasts, so the deadline moves forward roughly every 10 minutes and the counter |
| 70 | +// stays suspended without any single request having to cover the whole night. |
| 71 | +inline Request request(State& s, uint32_t now_ms, uint32_t duration_s) { |
| 72 | + Request r; |
| 73 | + r.clamped = duration_s > beecounter_proto::MAX_IDLE_SECONDS; |
| 74 | + r.granted_s = r.clamped ? beecounter_proto::MAX_IDLE_SECONDS : duration_s; |
| 75 | + |
| 76 | + if (r.granted_s == 0) { |
| 77 | + s.active = false; |
| 78 | + s.deadline_ms = 0; |
| 79 | + return r; |
| 80 | + } |
| 81 | + s.active = true; |
| 82 | + s.deadline_ms = now_ms + r.granted_s * 1000UL; |
| 83 | + return r; |
| 84 | +} |
| 85 | + |
| 86 | +// Resume sensing now, discarding any deadline. |
| 87 | +inline void resume(State& s) { |
| 88 | + s.active = false; |
| 89 | + s.deadline_ms = 0; |
| 90 | +} |
| 91 | + |
| 92 | +// Has an armed suspension run out? False when not suspended at all. |
| 93 | +inline bool expired(const State& s, uint32_t now_ms) { |
| 94 | + if (!s.active) return false; |
| 95 | + return (int32_t)(now_ms - s.deadline_ms) >= 0; |
| 96 | +} |
| 97 | + |
| 98 | +// Clear the suspension if its deadline has passed. Returns true exactly on the |
| 99 | +// poll that ends it, so the caller can do its one-off resume work (resetting |
| 100 | +// the gate state machines) without tracking the edge itself. |
| 101 | +inline bool serviceExpiry(State& s, uint32_t now_ms) { |
| 102 | + if (!expired(s, now_ms)) return false; |
| 103 | + resume(s); |
| 104 | + return true; |
| 105 | +} |
| 106 | + |
| 107 | +// Whether sensing should run right now. |
| 108 | +inline bool sensing(const State& s) { |
| 109 | + return !s.active; |
| 110 | +} |
| 111 | + |
| 112 | +// Seconds left on the suspension, rounded UP so a live suspension never reports |
| 113 | +// zero (which reads as "sensing" to HiveHub and would make it re-arm a beat |
| 114 | +// early). Zero means not suspended. |
| 115 | +inline uint32_t remainingSeconds(const State& s, uint32_t now_ms) { |
| 116 | + if (!s.active) return 0; |
| 117 | + const int32_t left_ms = (int32_t)(s.deadline_ms - now_ms); |
| 118 | + if (left_ms <= 0) return 0; |
| 119 | + return ((uint32_t)left_ms + 999UL) / 1000UL; |
| 120 | +} |
| 121 | + |
| 122 | +} // namespace idlestate |
0 commit comments