nx_iot_watchdog.nx
buildroot/runtime/nx_iot_watchdog.nx
about
nx_iot_watchdog.nx -- per-device link healer (closes the blink loop).
Roadmap: NISHI_IOT_HUB_ROADMAP.md Epoch 5 (HEAL). The rung that turns
"the light dropped + blinks + I re-pair it" into a fully automatic
detect -> re-provision -> re-adopt cycle, with a MEASURED count of the
manual re-pairs it spared the operator.
THE PROBLEM THIS CLOSES (operator-reported):
The anchor (WHO) gives a fixture one stable identity for life.
The provision state machine (nx_iot_provision_softap) auto-pairs a
fixture found in SoftAP mode with no manual step. This organ is the
LOOP that ties them together over TIME: it watches each device's
reachability, declares it down after a bounded run of missed
heartbeats, recognises when it has reverted to pairing mode (the
blink), kicks off re-provisioning, and marks it healthy again when
the anchor re-adopts it. The blink loop is closed without a human.
WHAT THIS ORGAN IS (and is NOT):
A pure, deterministic per-device HEALTH state machine. It takes a
reachability OBSERVATION each tick (seen / missed / found-in-pairing /
re-adopted) and returns the ACTION the daemon should take (probe /
mark-unreachable / re-provision / mark-current). It does NOT itself
touch the network or the devices -- the daemon wires its actions to
nx_iot_provision_softap (re-provision) and nx_iot_anchor (soft-delete
/ revive), keeping each primitive thin (feedback-hub-primitive-thin-
per-site-wiring) + single-responsibility (CLAUDE.md #9).
MEASURED-EXCEED HOOK (feedback-no-wave-measured-exceed; mirrors the
anchor's duplicates_prevented): recover_count is the number of times
the watchdog drove a fixture from RECOVERING back to HEALTHY by
re-provisioning it -- i.e. the exact count of MANUAL re-pairs the
operator did NOT have to do. The gate asserts it.
NEVER-BRICK (CLAUDE.md #26): every action this organ emits is brick-safe
-- probe is read-only, mark-unreachable/mark-current touch only the
anchor REGISTRY (soft-delete/revive, additive #13), and re-provision
delegates to nx_iot_provision_softap whose action alphabet is proven
firmware-write-free. No watchdog action writes device firmware.
genealogy_id: NISHI_IOT_HUB_ROADMAP.md Epoch 5 + project-iot-hub-multi-vendor-kickoff-2026-05-16
dependencies 0 imports · 1 importers
imports: none
imported by: nx_iot_watchdog_test.nx
structs
| 131 | struct IotWatchdog |
consts
| 63 | const WD_ST_HEALTHY: i64 = 0 // seen recently, reachable |
| 64 | const WD_ST_MISSED: i64 = 1 // missed >=1 heartbeat (transient) |
| 65 | const WD_ST_UNREACHABLE: i64 = 2 // missed >= threshold -> declared down |
| 66 | const WD_ST_RECOVERING: i64 = 3 // found in pairing mode -> re-provisioning |
| 67 | const WD_ST_N: i64 = 4 |
| 77 | const WD_OBS_NONE: i64 = 0 |
| 78 | const WD_OBS_SEEN: i64 = 1 // device replied / present on the LAN |
| 79 | const WD_OBS_MISS: i64 = 2 // no reply this tick |
| 80 | const WD_OBS_PAIRING: i64 = 3 // device found back in open pairing mode (the blink) |
| 81 | const WD_OBS_READOPTED: i64 = 4 // provision+anchor re-adopted it (healed) |
| 82 | const WD_OBS_N: i64 = 5 |
| 94 | const WD_ACT_NONE: i64 = 0 |
| 95 | const WD_ACT_PROBE: i64 = 1 // actively re-probe the device |
| 96 | const WD_ACT_MARK_UNREACHABLE: i64 = 2 // anchor soft-delete (additive) |
| 97 | const WD_ACT_REPROVISION: i64 = 3 // kick nx_iot_provision_softap |
| 98 | const WD_ACT_MARK_CURRENT: i64 = 4 // anchor revive (re-adopt healthy) |
| 99 | const WD_ACT_N: i64 = 5 |
| 127 | const NX_IOT_WD_MISS_THRESHOLD: i64 = 3 |
functions
| 69 | func nx_iot_wd_state_is_valid(v: i64) -> i64 called by 1: main |
| 84 | func nx_iot_wd_obs_is_valid(v: i64) -> i64 |
| 101 | func nx_iot_wd_action_is_valid(v: i64) -> i64 |
| 110 | func nx_iot_wd_action_writes_firmware(act: i64) -> i64 |
| 119 | func nx_iot_wd_action_is_brick_safe(act: i64) -> i64 |
| 147 | func nx_iot_wd_init(w: *IotWatchdog, logical_id: i64, called by 1: main |
| 170 | func nx_iot_wd_observe(w: *IotWatchdog, obs: i64, now_ms: i64) -> i64 |
| 275 | func nx_iot_wd_state(w: *IotWatchdog) -> i64 called by 1: main |
| 280 | func nx_iot_wd_action(w: *IotWatchdog) -> i64 |
| 285 | func nx_iot_wd_consec_misses(w: *IotWatchdog) -> i64 called by 1: main |
| 290 | func nx_iot_wd_recover_count(w: *IotWatchdog) -> i64 called by 1: main |
| 295 | func nx_iot_wd_tick_count(w: *IotWatchdog) -> i64 called by 1: main |
| 300 | func nx_iot_wd_logical_id(w: *IotWatchdog) -> i64 called by 1: main |
| 305 | func nx_iot_wd_is_healthy(w: *IotWatchdog) -> i64 called by 1: main |
| 311 | func nx_iot_wd_is_down(w: *IotWatchdog) -> i64 called by 1: main |