code wiki / _hdl_build / nx_iot_ctl.nx

nx_iot_ctl.nx source

↩ module page · 117 lines · 4082 B

1// nx_iot_ctl.nx -- sovereign HOME-ELECTRONICS identification ORGAN (CLI + MCP tool). 2// 3// The "home electronics" half of the operator's iot ask, as a first-class organ 4// rather than a bolt-on to the printer tool (rule 9: one responsibility each -- 5// nx_printer_ctl owns the printer, this owns the house). 6// 7// Verbs (JSON on stdout = the MCP/API/agent interchange): 8// nx_iot_ctl services -- emit the loaded service manifest (no network) 9// nx_iot_ctl scan <a.b.c> -- IoT-aware /24 sweep: kind + vendor per host 10// nx_iot_ctl probe <ip> [port] -- READ-ONLY active identify (Kasa get_sysinfo) 11// nx_iot_ctl contract -- security / never-brick contract (no network) 12// 13// NEVER-BRICK (rule 26): there is NO write verb. The verb map above is the 14// entire reachable surface; no argument can steer this organ onto a path that 15// mutates a device. Proven mechanically by nx_iot_ctl_gate. 16// 17// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 18import "nx_syscalls.nx" 19import "nx_iot_ctl_lib.nx" 20 21const IC_STDERR: i64 = 2 22const IC_EXIT_USAGE: i64 = 2 23 24func ic_werr(s: *u8) -> i64 { 25 var n: i64 = 0 26 while s[n] != (0 as u8) { n = n + 1 } 27 sys_write(IC_STDERR, s, n) 28 return 0 29} 30func ic_usage() -> i64 { 31 ic_werr("usage: nx_iot_ctl services | scan <a.b.c> | probe <ip> [port] | contract\n" as *u8) 32 return 0 33} 34 35func main(argc: i64, argv: *i64) -> i64 { 36 if argc < 2 { 37 ic_usage() 38 sys_exit(IC_EXIT_USAGE) 39 return IC_EXIT_USAGE 40 } 41 let verb: *u8 = argv[1] as *u8 42 let id: i64 = iotc_verb_id(verb) 43 if id == IOTC_V_BAD { 44 ic_usage() 45 sys_exit(IC_EXIT_USAGE) 46 return IC_EXIT_USAGE 47 } 48 49 // The manifest is loaded for EVERY verb: data-driven config is not an 50 // optional path that only some code takes (rule 17 / rule 20). 51 let man: *u8 = iotm_new() 52 let from_conf: i64 = iotm_load(man, "iot_services.conf" as *u8) 53 let out: *u8 = sys_mmap(IOTC_OUT) 54 55 if id == IOTC_V_SERVICES { 56 let so: i64 = iotc_emit_services(out, man, from_conf) 57 sys_write(1, out, so) 58 sys_exit(0) 59 return 0 60 } 61 62 if id == IOTC_V_CONTRACT { 63 let co: i64 = iotc_emit_contract(out, man, from_conf) 64 sys_write(1, out, co) 65 sys_exit(0) 66 return 0 67 } 68 69 if id == IOTC_V_SCAN { 70 if argc < 3 { 71 ic_werr("usage: nx_iot_ctl scan <subnet-prefix a.b.c>\n" as *u8) 72 sys_exit(IC_EXIT_USAGE) 73 return IC_EXIT_USAGE 74 } 75 let prefix: *u8 = argv[2] as *u8 76 let base24: i64 = lscan_parse_prefix(prefix) 77 if base24 < 0 { 78 ic_werr("bad subnet prefix (expect a.b.c, e.g. 192.168.10)\n" as *u8) 79 sys_exit(IC_EXIT_USAGE) 80 return IC_EXIT_USAGE 81 } 82 // Budget comes from the SHARED constant, not a local number: host 83 // visibility depends on it, so a per-call-site value is how two organs 84 // end up disagreeing about the same LAN (measured, see nx_lan_scan). 85 let mask: *i64 = sys_mmap(256 * 8) as *i64 86 iotc_scan(man, base24, LSCAN_BUDGET_MS, mask) 87 let o: i64 = iotc_emit_scan(out, man, prefix, base24, mask, from_conf) 88 sys_write(1, out, o) 89 sys_exit(0) 90 return 0 91 } 92 93 // probe <ip> [port] 94 if argc < 3 { 95 ic_werr("usage: nx_iot_ctl probe <ip> [port]\n" as *u8) 96 sys_exit(IC_EXIT_USAGE) 97 return IC_EXIT_USAGE 98 } 99 let ip_s: *u8 = argv[2] as *u8 100 let ipv4: i64 = lscan_parse_ip(ip_s) 101 if ipv4 < 0 { 102 ic_werr("bad ip (expect dotted IPv4 a.b.c.d)\n" as *u8) 103 sys_exit(IC_EXIT_USAGE) 104 return IC_EXIT_USAGE 105 } 106 var port: i64 = NX_IOT_KASA_PORT 107 if argc > 3 { 108 let pp: i64 = lscan_parse_port(argv[3] as *u8) 109 if pp > 0 { port = pp } 110 } 111 let plain: *u8 = sys_mmap(IOTC_RESP) 112 let pn: i64 = iotc_kasa_probe(ipv4, port, plain, IOTC_RESP) 113 let po: i64 = iotc_emit_probe(out, ipv4, port, pn, plain) 114 sys_write(1, out, po) 115 sys_exit(0) 116 return 0 117}