nx_iot_anchor.nx
buildroot/runtime/nx_iot_anchor.nx
about
nx_iot_anchor.nx -- stable device-identity anchor for the hub.
Roadmap: NISHI_IOT_HUB_ROADMAP.md Epoch 3 (PAIR) keystone +
the cross-cutting identity substrate every later epoch writes into.
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 repair and then they duplicate in the app ... light bulbs
aren't wandering electronics, they should stay anchored like
sprinklers."
Incumbent apps (Smart Life / Kasa / etc.) key a device by a
CLOUD-ISSUED registration token. When a fixture drops WiFi,
reverts to pairing mode, and is re-paired, the cloud mints a NEW
token -> a NEW device row appears, the old one goes stale, and
scenes/automations silently point at the dead row. The fixture
never moved; its IDENTITY wandered. That is the duplication bug.
THE FIX (structural, not heuristic -- per
feedback-honest-perf-verdict-no-aspirational-claims):
Anchor identity to the device's STABLE HARDWARE id (its MAC, or a
vendor gwId) -- NOT to an IP, a session, or a cloud token. A
(re)discovery of a known hardware id RE-ADOPTS the existing
logical device: same logical_id, same friendly name, same history;
only the volatile fields (ip, last_seen, reachability) update. A
physical device therefore maps to EXACTLY ONE logical record for
its entire life, no matter how many times it blinks, drops, and
re-pairs. Duplication is eliminated BY CONSTRUCTION, not by a
de-dup pass that can be skipped.
HONEST SCOPE (what this primitive does NOT do):
- It does not, by itself, stop a fixture from dropping WiFi. That
is the LAN-session + watchdog + provision layers (queued:
nx_iot_watchdog.nx, nx_iot_provision_softap.nx).
- It does not talk to the network. It is pure-buffer: the caller
(discovery / provision / daemon) hands it a hardware id + the
volatile facts, and it returns the stable logical identity.
This is the IDENTITY spine those layers hang on.
VENDOR-AGNOSTIC BY DESIGN (per feedback-hub-primitive-thin-per-site-
dependencies 0 imports · 3 importers
imports: none
imported by: nx_iot_anchor_test.nxnx_iot_classify.nxnx_iot_discover.nx
structs
| 160 | struct IotAnchorReg |
| 173 | struct IotDeviceMeta |
consts
| 111 | const NX_IOT_VENDOR_UNKNOWN: i64 = 0 |
| 112 | const NX_IOT_VENDOR_TUYA: i64 = 1 |
| 113 | const NX_IOT_VENDOR_MAGICHOME: i64 = 2 |
| 114 | const NX_IOT_VENDOR_WIZ: i64 = 3 |
| 115 | const NX_IOT_VENDOR_YEELIGHT: i64 = 4 |
| 116 | const NX_IOT_VENDOR_KASA: i64 = 5 |
| 117 | const NX_IOT_VENDOR_N: i64 = 6 |
| 127 | const NX_IOT_ANCHOR_UNKNOWN: i64 = 0 |
| 128 | const NX_IOT_ANCHOR_NEW: i64 = 1 // first sighting -> new logical device |
| 129 | const NX_IOT_ANCHOR_READOPTED: i64 = 2 // known hwid -> SAME logical_id (no dup) |
| 130 | const NX_IOT_ANCHOR_UPDATED: i64 = 3 // metadata-only change (name / reachability) |
| 131 | const NX_IOT_ANCHOR_FULL: i64 = 4 // registry at capacity |
| 132 | const NX_IOT_ANCHOR_BAD_ARG: i64 = 5 |
| 133 | const NX_IOT_ANCHOR_NOT_FOUND: i64 = 6 |
| 134 | const NX_IOT_ANCHOR_N: i64 = 7 |
| 153 | const NX_IOT_HWID_CAP: i64 = 32 // fits a 17-char MAC string or a gwId |
| 154 | const NX_IOT_NAME_CAP: i64 = 32 // friendly name, e.g. "Kitchen Switch" |
| 155 | const NX_IOT_BLOB_STRIDE: i64 = 64 // HWID_CAP + NAME_CAP |
| 156 | const NX_IOT_DEV_META_BYTES: i64 = 64 // 8 i64 fields |
functions
| 119 | func nx_iot_vendor_is_valid(v: i64) -> i64 |
| 136 | func nx_iot_anchor_verdict_is_valid(v: i64) -> i64 called by 1: main |
| 186 | func nx_iot_anchor_meta_ptr(metas: *IotDeviceMeta, slot: i64) -> *IotDeviceMeta |
| 191 | func nx_iot_anchor_hwid_base(slot: i64) -> i64 |
| 195 | func nx_iot_anchor_name_base(slot: i64) -> i64 |
| 201 | func nx_iot_anchor_store_hwid(blob: *u8, slot: i64, |
| 215 | func nx_iot_anchor_hwid_eq(blob: *u8, slot: i64, |
| 240 | func nx_iot_anchor_find(reg: *IotAnchorReg, blob: *u8, |
| 254 | func nx_iot_anchor_slot_by_logical(reg: *IotAnchorReg, metas: *IotDeviceMeta, |
| 272 | func nx_iot_anchor_init(reg: *IotAnchorReg, cap: i64) -> i64 |
| 294 | func nx_iot_anchor_adopt(reg: *IotAnchorReg, metas: *IotDeviceMeta, |
| 367 | func nx_iot_anchor_set_name(reg: *IotAnchorReg, metas: *IotDeviceMeta, called by 1: main calls 3: nx_iot_anchor_slot_by_logicalnx_iot_anchor_name_basenx_iot_anchor_meta_ptr |
| 409 | func nx_iot_anchor_mark_unreachable(reg: *IotAnchorReg, metas: *IotDeviceMeta, |
| 425 | func nx_iot_anchor_count(reg: *IotAnchorReg) -> i64 |
| 430 | func nx_iot_anchor_naive_adopts(reg: *IotAnchorReg) -> i64 |
| 437 | func nx_iot_anchor_duplicates_prevented(reg: *IotAnchorReg) -> i64 |
| 442 | func nx_iot_anchor_count_current(reg: *IotAnchorReg, metas: *IotDeviceMeta) -> i64 |
| 456 | func nx_iot_anchor_logical_adopt_count(reg: *IotAnchorReg, metas: *IotDeviceMeta, |
| 464 | func nx_iot_anchor_logical_is_current(reg: *IotAnchorReg, metas: *IotDeviceMeta, |
| 472 | func nx_iot_anchor_logical_vendor(reg: *IotAnchorReg, metas: *IotDeviceMeta, |
| 480 | func nx_iot_anchor_logical_ip(reg: *IotAnchorReg, metas: *IotDeviceMeta, |