nx_iot_classify.nx source
↩ module page · 181 lines · 7382 B
1// nx_iot_classify.nx -- universal device-type probe / vendor router.
2//
3// Roadmap: NISHI_IOT_HUB_ROADMAP.md Epoch 1 (DISCOVER). The operator's
4// ask (2026-06-18): "device-driver-type probing to find out and use
5// their driver, or build it autonomously from the ground up." This is
6// the probe: given whatever signals discovery captured about an unknown
7// device, decide which vendor driver to route it to -- or report
8// UNKNOWN so the hub can fall back to active probing / autonomous
9// driver synthesis.
10//
11// Four independent signal classifiers, fused by confidence priority:
12// 1. reply content (strongest -- the device literally told us)
13// 2. MAC OUI (hardware identity, survives firmware)
14// 3. service port (which port answered)
15// 4. SoftAP SSID (pairing-mode name -- works BEFORE the device
16// has joined the LAN, the "scan for unpaired" case)
17//
18// HONEST coverage note: the OUI + SSID tables are SEEDS, not exhaustive
19// (there are thousands of IoT OUIs). UNKNOWN is a first-class result,
20// not a failure -- it is the signal that triggers active probing /
21// autonomous driver work. The tables grow data-driven over time.
22//
23// S-class invariants: S6 (no cloud), S7 (sealed-enum verdict -- returns
24// the shared NX_IOT_VENDOR_* enum), S8-orchestrator (this is the
25// cross-vendor router by design; icl_override on kind_isolated like
26// nx_iot_announce).
27//
28// Vendor enum is the shared one from nx_iot_anchor.nx (single source).
29//
30// genealogy_id: NISHI_IOT_HUB_ROADMAP.md Epoch 1 + project-iot-hub-*
31// license_tier: ORIGINAL
32//
33// nx_capability_claims:
34// needs: [pointer_arithmetic]
35// provides: [classify_by_ssid, classify_by_oui, classify_by_port,
36// classify_by_reply, classify_fuse]
37// safety: [no_unchecked_deref, no_floating_point, no_syscall,
38// bounded_iteration, kind_isolated_orchestrator]
39// verdict: [sealed_enum_vendor, no_silent_failure]
40// license: ORIGINAL
41// kind: iot_runtime_primitive
42// sss: [S6, S7]
43//
44// icl_override: kind_isolated: this IS the cross-family router; it
45// deliberately reasons about every vendor. Same exception as
46// nx_iot_announce's classifier.
47
48import "nx_iot_anchor.nx" // NX_IOT_VENDOR_* shared enum
49
50// ---- string helpers ------------------------------------------------
51
52func nx_iot_cls_strlen(s: *u8) -> i64 {
53 var i: i64 = 0
54 var keep: i64 = 1
55 while keep == 1 {
56 if s[i] == (0 as u8) { keep = 0 }
57 else { i = i + 1 }
58 }
59 return i
60}
61
62// 1 if s[0..n) begins with the null-terminated prefix `pre`.
63func nx_iot_cls_starts_with(s: *u8, n: i64, pre: *u8) -> i64 {
64 let plen: i64 = nx_iot_cls_strlen(pre)
65 if plen > n { return 0 }
66 var i: i64 = 0
67 var ok: i64 = 1
68 var keep: i64 = 1
69 while keep == 1 {
70 if i >= plen { keep = 0 }
71 else {
72 if (s[i] as i64) != (pre[i] as i64) {
73 ok = 0
74 keep = 0
75 } else {
76 i = i + 1
77 }
78 }
79 }
80 return ok
81}
82
83// Offset of null-terminated pattern `pat` in buf[0..len), or -1.
84func nx_iot_cls_find(buf: *u8, len: i64, pat: *u8) -> i64 {
85 let pat_len: i64 = nx_iot_cls_strlen(pat)
86 if pat_len == 0 { return 0 }
87 if pat_len > len { return -1 }
88 var i: i64 = 0
89 let end: i64 = len - pat_len + 1
90 var hit: i64 = -1
91 while i < end {
92 if hit == -1 {
93 var j: i64 = 0
94 var ok: i64 = 1
95 while j < pat_len {
96 if (buf[i + j] as i64) != (pat[j] as i64) {
97 ok = 0
98 j = pat_len
99 } else {
100 j = j + 1
101 }
102 }
103 if ok == 1 { hit = i }
104 }
105 i = i + 1
106 }
107 return hit
108}
109
110// ---- Signal 4: SoftAP SSID (pairing-mode name) --------------------
111
112func nx_iot_classify_by_ssid(ssid: *u8, n: i64) -> i64 {
113 if nx_iot_cls_starts_with(ssid, n, "TP-LINK_") == 1 { return NX_IOT_VENDOR_KASA }
114 if nx_iot_cls_starts_with(ssid, n, "SmartLife") == 1 { return NX_IOT_VENDOR_TUYA }
115 if nx_iot_cls_starts_with(ssid, n, "HF-") == 1 { return NX_IOT_VENDOR_TUYA }
116 if nx_iot_cls_starts_with(ssid, n, "wiz_") == 1 { return NX_IOT_VENDOR_WIZ }
117 if nx_iot_cls_starts_with(ssid, n, "LEDnet") == 1 { return NX_IOT_VENDOR_MAGICHOME }
118 if nx_iot_cls_starts_with(ssid, n, "yeelink-") == 1 { return NX_IOT_VENDOR_YEELIGHT }
119 if nx_iot_cls_starts_with(ssid, n, "yeelight-") == 1 { return NX_IOT_VENDOR_YEELIGHT }
120 return NX_IOT_VENDOR_UNKNOWN
121}
122
123// ---- Signal 2: MAC OUI (first 3 bytes; seed table) ----------------
124
125func nx_iot_classify_by_oui(mac: *u8) -> i64 {
126 let b0: i64 = mac[0] as i64
127 let b1: i64 = mac[1] as i64
128 let b2: i64 = mac[2] as i64
129 // TP-Link / Kasa (d8:0d:17 = the operator's live HS210)
130 if b0 == 0xD8 { if b1 == 0x0D { if b2 == 0x17 { return NX_IOT_VENDOR_KASA } } }
131 if b0 == 0x50 { if b1 == 0xC7 { if b2 == 0xBF { return NX_IOT_VENDOR_KASA } } }
132 if b0 == 0x1C { if b1 == 0x3B { if b2 == 0xF3 { return NX_IOT_VENDOR_KASA } } }
133 // Tuya
134 if b0 == 0x68 { if b1 == 0x57 { if b2 == 0x2D { return NX_IOT_VENDOR_TUYA } } }
135 if b0 == 0xD8 { if b1 == 0x1F { if b2 == 0x12 { return NX_IOT_VENDOR_TUYA } } }
136 // WiZ (Signify)
137 if b0 == 0xA8 { if b1 == 0xBB { if b2 == 0x50 { return NX_IOT_VENDOR_WIZ } } }
138 // Yeelight
139 if b0 == 0x7C { if b1 == 0x49 { if b2 == 0xEB { return NX_IOT_VENDOR_YEELIGHT } } }
140 return NX_IOT_VENDOR_UNKNOWN
141}
142
143// ---- Signal 3: service port ---------------------------------------
144
145func nx_iot_classify_by_port(port: i64) -> i64 {
146 if port == 9999 { return NX_IOT_VENDOR_KASA }
147 if port == 6668 { return NX_IOT_VENDOR_TUYA }
148 if port == 6667 { return NX_IOT_VENDOR_TUYA }
149 if port == 6666 { return NX_IOT_VENDOR_TUYA }
150 if port == 38899 { return NX_IOT_VENDOR_WIZ }
151 if port == 55443 { return NX_IOT_VENDOR_YEELIGHT }
152 if port == 5577 { return NX_IOT_VENDOR_MAGICHOME }
153 return NX_IOT_VENDOR_UNKNOWN
154}
155
156// ---- Signal 1: decrypted/plain reply content ----------------------
157
158func nx_iot_classify_by_reply(plain: *u8, n: i64) -> i64 {
159 if nx_iot_cls_find(plain, n, "relay_state") >= 0 { return NX_IOT_VENDOR_KASA }
160 if nx_iot_cls_find(plain, n, "mic_type") >= 0 { return NX_IOT_VENDOR_KASA }
161 if nx_iot_cls_find(plain, n, "smartlife.iot") >= 0 { return NX_IOT_VENDOR_KASA }
162 if nx_iot_cls_find(plain, n, "devId") >= 0 { return NX_IOT_VENDOR_TUYA }
163 if nx_iot_cls_find(plain, n, "getPilot") >= 0 { return NX_IOT_VENDOR_WIZ }
164 if nx_iot_cls_find(plain, n, "setPilot") >= 0 { return NX_IOT_VENDOR_WIZ }
165 if nx_iot_cls_find(plain, n, "yeelight") >= 0 { return NX_IOT_VENDOR_YEELIGHT }
166 return NX_IOT_VENDOR_UNKNOWN
167}
168
169// ---- Fuse: reply > oui > port > ssid ------------------------------
170//
171// Pass NX_IOT_VENDOR_UNKNOWN for any signal not available. Returns the
172// highest-confidence non-UNKNOWN vote, or UNKNOWN if no signal fired.
173
174func nx_iot_classify_fuse(v_reply: i64, v_oui: i64,
175 v_port: i64, v_ssid: i64) -> i64 {
176 if v_reply != NX_IOT_VENDOR_UNKNOWN { return v_reply }
177 if v_oui != NX_IOT_VENDOR_UNKNOWN { return v_oui }
178 if v_port != NX_IOT_VENDOR_UNKNOWN { return v_port }
179 if v_ssid != NX_IOT_VENDOR_UNKNOWN { return v_ssid }
180 return NX_IOT_VENDOR_UNKNOWN
181}