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}