code wiki / (root) / nx_iot_classify.nx

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}