nx_iot_provision_softap.nx
buildroot/runtime/nx_iot_provision_softap.nx
about
nx_iot_provision_softap.nx -- the auto-pair STATE MACHINE.
Roadmap: NISHI_IOT_HUB_ROADMAP.md Epoch 3 (PAIR), the rung that turns
the anchor (WHO) + the vendor drivers (HOW) + the classifier (PROBE)
into the operator's actual outcome: lights that re-join the house on
their own, with NO manual re-pair step.
THE PROBLEM THIS CLOSES (operator-reported, 2026-05-16 + 2026-06-18):
"they disconnect all the time and blink and then I MANUALLY have to
re-pair and then they duplicate in the app."
The anchor already killed the duplicate (re-adopt -> same logical id).
This kills the MANUAL step: when a fixture drops to its open pairing
SoftAP (e.g. TP-LINK_HS210_C209), the hub scans for it, joins it on
the free Wi-Fi 6E radio, pushes the house creds over the vendor's own
pairing path, waits for it to re-join the LAN, verifies it, and
anchors it -- no human in the loop, no new app row.
WHAT THIS ORGAN IS (and is NOT):
This is the pure, deterministic LOGIC -- a sealed-enum state machine.
It takes the RESULT of each radio/driver step as an event and returns
the NEXT action the runner must perform. The radio calls themselves
(scan / join SoftAP / socket-send creds / re-probe LAN) are the
Windows-native live step, injected by the caller. Keeping the logic
pure + buffer-only makes the whole auto-pair flow gate-provable today,
off the LAN, on every nxc2 backend -- and lets the live runner be a
thin shim that just performs actions and feeds back events.
NEVER-BRICK BY CONSTRUCTION (CLAUDE.md #26 -- ABSOLUTE, brand-critical):
Provisioning is the one IoT flow that writes persistent device state
(the Wi-Fi credential). The guarantee here is MECHANICAL, not an
asserted promise:
1. The action alphabet (PROV_ACT_*) contains NO firmware/OTA/flash
action. The machine is structurally incapable of emitting one.
2. The most device-touching action it CAN emit, PUSH_CREDS, carries
only a bounded Wi-Fi credential over the vendor's documented,
reversible pairing API; a rejected push leaves the device safe in
pairing mode (PROV_FAIL_PUSH, recoverable) -- never a half-write.
3. nx_iot_prov_action_writes_firmware is a FAIL-SAFE classifier:
it returns 0 only for the explicit allow-list of safe actions and
1 (= writes firmware = brick risk) for anything else, so a future
dependencies 1 imports · 1 importers
imports: nx_iot_classify.nx
imported by: nx_iot_provision_softap_test.nx
structs
| 233 | struct IotProvSession |
consts
| 106 | const PROV_ST_IDLE: i64 = 0 |
| 107 | const PROV_ST_SCANNING: i64 = 1 // waiting for a scan result |
| 108 | const PROV_ST_JOINING: i64 = 2 // joining the device SoftAP |
| 109 | const PROV_ST_PUSHING: i64 = 3 // pushing house creds to the device |
| 110 | const PROV_ST_AWAIT_REJOIN: i64 = 4 // device rebooting onto the home LAN |
| 111 | const PROV_ST_VERIFYING: i64 = 5 // confirming reachability on the LAN |
| 112 | const PROV_ST_ANCHORING: i64 = 6 // anchoring the stable identity |
| 113 | const PROV_ST_DONE: i64 = 7 // terminal: provisioned + anchored |
| 114 | const PROV_ST_FAILED: i64 = 8 // terminal: gave up (see fail_reason) |
| 115 | const PROV_ST_N: i64 = 9 |
| 131 | const PROV_ACT_NONE: i64 = 0 |
| 132 | const PROV_ACT_SCAN: i64 = 1 // scan for open pairing SoftAPs |
| 133 | const PROV_ACT_JOIN_AP: i64 = 2 // join the selected device SoftAP |
| 134 | const PROV_ACT_PUSH_CREDS: i64 = 3 // send house Wi-Fi creds (reversible) |
| 135 | const PROV_ACT_REJOIN_HOME: i64 = 4 // rejoin home LAN + probe for device |
| 136 | const PROV_ACT_VERIFY: i64 = 5 // confirm device reachable on LAN |
| 137 | const PROV_ACT_ANCHOR: i64 = 6 // anchor stable identity (no dup) |
| 138 | const PROV_ACT_REPORT_DONE: i64 = 7 // terminal success |
| 139 | const PROV_ACT_REPORT_FAIL: i64 = 8 // terminal failure |
| 140 | const PROV_ACT_N: i64 = 9 |
| 150 | const PROV_EV_NONE: i64 = 0 |
| 151 | const PROV_EV_START: i64 = 1 // begin provisioning a device |
| 152 | const PROV_EV_AP_FOUND: i64 = 2 // scan found a pairing AP (ssid given) |
| 153 | const PROV_EV_AP_NONE: i64 = 3 // scan found no pairing AP |
| 154 | const PROV_EV_JOIN_OK: i64 = 4 |
| 155 | const PROV_EV_JOIN_FAIL: i64 = 5 |
| 156 | const PROV_EV_PUSH_OK: i64 = 6 |
| 157 | const PROV_EV_PUSH_FAIL: i64 = 7 |
| 158 | const PROV_EV_REJOIN_OK: i64 = 8 // device seen on the home LAN |
| 159 | const PROV_EV_REJOIN_FAIL: i64 = 9 // device never came back (timeout) |
| 160 | const PROV_EV_VERIFY_OK: i64 = 10 |
| 161 | const PROV_EV_VERIFY_FAIL: i64 = 11 |
| 162 | const PROV_EV_ANCHOR_OK: i64 = 12 // anchor adopt succeeded (logical_id in arg) |
| 163 | const PROV_EV_ANCHOR_FAIL: i64 = 13 // anchor rejected (e.g. registry full) |
| 164 | const PROV_EV_N: i64 = 14 |
| 174 | const PROV_FAIL_NONE: i64 = 0 |
| 175 | const PROV_FAIL_NO_AP: i64 = 1 // scan found nothing to pair |
| 176 | const PROV_FAIL_UNKNOWN_VENDOR: i64 = 2 // pairing AP, but no driver for it |
| 177 | const PROV_FAIL_JOIN: i64 = 3 // could not join the SoftAP (retries gone) |
| 178 | const PROV_FAIL_PUSH: i64 = 4 // creds push rejected (device left safe) |
| 179 | const PROV_FAIL_REJOIN_TIMEOUT: i64 = 5 // device never re-joined the LAN |
| 180 | const PROV_FAIL_VERIFY: i64 = 6 // on LAN but not reachable |
| 181 | const PROV_FAIL_ANCHOR: i64 = 7 // anchor adopt failed |
| 182 | const PROV_FAIL_BAD_ARG: i64 = 8 // null session at init |
| 183 | const PROV_FAIL_N: i64 = 9 |
| 196 | const NX_IOT_PROV_MAX_ATTEMPTS: i64 = 3 |
functions
| 117 | func nx_iot_prov_state_is_valid(v: i64) -> i64 called by 1: main |
| 142 | func nx_iot_prov_action_is_valid(v: i64) -> i64 |
| 166 | func nx_iot_prov_event_is_valid(v: i64) -> i64 |
| 185 | func nx_iot_prov_fail_is_valid(v: i64) -> i64 called by 1: main |
| 206 | func nx_iot_prov_action_writes_firmware(act: i64) -> i64 |
| 222 | func nx_iot_prov_action_is_brick_safe(act: i64) -> i64 |
| 249 | func nx_iot_prov_init(s: *IotProvSession, max_attempts: i64) -> i64 called by 1: main |
| 270 | func nx_iot_prov_is_pairing_ssid(ssid: *u8, n: i64) -> i64 |
| 287 | func nx_iot_prov_step(s: *IotProvSession, ev: i64, |
| 444 | func nx_iot_prov_state(s: *IotProvSession) -> i64 called by 1: main |
| 449 | func nx_iot_prov_action(s: *IotProvSession) -> i64 called by 1: main |
| 454 | func nx_iot_prov_vendor(s: *IotProvSession) -> i64 called by 1: main |
| 459 | func nx_iot_prov_fail_reason(s: *IotProvSession) -> i64 called by 1: main |
| 464 | func nx_iot_prov_logical_id(s: *IotProvSession) -> i64 called by 1: main |
| 469 | func nx_iot_prov_attempts(s: *IotProvSession) -> i64 called by 1: main |
| 474 | func nx_iot_prov_ev_count(s: *IotProvSession) -> i64 called by 1: main |
| 479 | func nx_iot_prov_is_terminal(s: *IotProvSession) -> i64 called by 1: main |
| 486 | func nx_iot_prov_is_success(s: *IotProvSession) -> i64 called by 1: main |