code wiki / _hdl_build / nx_home.nx
nx_home.nx source
↩ module page · 206 lines · 12709 B
1// nx_home.nx -- SOVEREIGN home-control server (H0 of the IoT workstream). The brain a control surface (Roku
2// "Home" tab / web / phone) POSTs to; it holds the device registry + live state and applies commands. H0 uses a
3// SIM driver (a command just updates state) -- real IP-hub drivers (Matter/Hue/MQTT) plug in behind this later.
4// Sovereign socket HTTP server (no python/shell). REST:
5// GET /home/state -> {"devices":[{id,name,room,type,on,brightness},...]}
6// POST /home/cmd?id=<id>&on=<0|1>&bri=<0-100> -> the device's new state (404 if unknown id, 400 if no id)
7// POST /home/shutdown -> {"ok":1} then the server exits (managed teardown; auth-gate in prod)
8// Idempotent (#10), structured errors (#6), graceful (#14). Devices embedded for H0; H1 makes them data-driven
9// (#11, TSV/seg_store) + persisted. license_tier: ORIGINAL
10// nx_home <port>
11import "nx_syscalls.nx"
12import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
13const K_MAGIC_65536: i64 = 65536
14const K_MAGIC_16384: i64 = 16384
15const K_MAGIC_1024: i64 = 1024
16const K_MAGIC_131072: i64 = 131072
17
18func h_p(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
19// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
20// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
21// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
22// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
23func h_n(v: i64) -> i64 { nxi_out(v); return 0 }
24func h_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
25func h_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){ dst[off+i]=s[i]; i=i+1 } return off+i }
26func h_catb(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { var i: i64=0; while i<n { dst[off+i]=src[i]; i=i+1 } return off+n }
27func h_catn(dst: *u8, off: i64, v: i64) -> i64 { var m: i64=v; let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48 as u8;k=1}; while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1}; var o: i64=off; var i: i64=0; while i<k{dst[o]=t[k-1-i];o=o+1;i=i+1} return o }
28func h_streq(a: *u8, b: *u8) -> i64 { var i: i64=0; while a[i]!=(0 as u8){ if a[i]!=b[i]{return 0} i=i+1 } if b[i]!=(0 as u8){return 0} return 1 }
29func h_atoi(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8){ let c: i64=s[i] as i64; if c>=48 { if c<=57 { v=v*10+(c-48) } } i=i+1 } return v }
30func h_find(hay: *u8, n: i64, needle: *u8) -> i64 {
31 let nl: i64=h_strlen(needle); if nl==0 { return 0-1 }
32 var i: i64=0
33 while i+nl<=n { var j: i64=0; var hit: i64=1; while j<nl { if hay[i+j]!=needle[j] { hit=0; j=nl } else { j=j+1 } } if hit==1 { return i+nl } i=i+1 }
34 return 0-1
35}
36func h_has(hay: *u8, n: i64, needle: *u8) -> i64 { if h_find(hay,n,needle)>=0 { return 1 } return 0 }
37func h_extract(hay: *u8, n: i64, key: *u8, out: *u8, cap: i64) -> i64 {
38 let start: i64=h_find(hay, n, key); if start<0 { return 0-1 }
39 var i: i64=start; var o: i64=0
40 while i<n {
41 let c: i64=hay[i] as i64; var stop: i64=0
42 if c==38 { stop=1 } if c==32 { stop=1 } if c==13 { stop=1 } if c==10 { stop=1 }
43 if stop==1 { i=n } else { if o<cap { out[o]=hay[i]; o=o+1 } i=i+1 }
44 }
45 out[o]=0 as u8; return o
46}
47func h_sockaddr(buf: *u8, port: i64, a: i64, b: i64, c: i64, d: i64) -> i64 {
48 var i: i64=0; while i<16 { buf[i]=0 as u8; i=i+1 }
49 buf[0]=2 as u8; buf[2]=((port>>8)&0xff) as u8; buf[3]=(port&0xff) as u8
50 buf[4]=a as u8; buf[5]=b as u8; buf[6]=c as u8; buf[7]=d as u8; return 0
51}
52func h_bind_listen(port: i64) -> i64 {
53 let lfd: i64=sys_socket(AF_INET, SOCK_STREAM, 0); if lfd<0 { return 0-1 }
54 let one: *i64=sys_mmap(8) as *i64; one[0]=1
55 sys_setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, one as *u8, 4)
56 let addr: *u8=sys_mmap(16); h_sockaddr(addr, port, 127, 0, 0, 1)
57 if sys_bind(lfd, addr, 16)!=0 { sys_close(lfd); return 0-2 }
58 if sys_listen(lfd, 16)!=0 { sys_close(lfd); return 0-3 }
59 return lfd
60}
61func h_recv(cfd: i64, buf: *u8, cap: i64) -> i64 {
62 var total: i64=0; var done: i64=0
63 while done==0 {
64 let r: i64=sys_read(cfd, ((buf as i64)+total) as *u8, cap-total)
65 if r<=0 { done=1 } else { total=total+r; if h_find(buf, total, "\r\n\r\n" as *u8)>=0 { done=1 } if total>=cap { done=1 } }
66 }
67 return total
68}
69func h_send(cfd: i64, status: *u8, body: *u8, blen: i64) -> i64 {
70 let b: *u8=sys_mmap(K_MAGIC_65536); var o: i64=0
71 o=h_cat(b,o,"HTTP/1.1 " as *u8); o=h_cat(b,o,status)
72 o=h_cat(b,o,"\r\nContent-Type: application/json\r\nContent-Length: " as *u8); o=h_catn(b,o,blen)
73 o=h_cat(b,o,"\r\nConnection: close\r\n\r\n" as *u8); o=h_catb(b,o,body,blen)
74 sys_write(cfd, b, o); return 0
75}
76func find_dev(ids: *i64, ndev: i64, id: *u8) -> i64 {
77 var i: i64=0; while i<ndev { if h_streq(ids[i] as *u8, id)==1 { return i } i=i+1 } return 0-1
78}
79func j_dev(buf: *u8, off: i64, ids: *i64, names: *i64, rooms: *i64, kinds: *i64, pwr: *i64, bri: *i64, i: i64) -> i64 {
80 var o: i64=off
81 o=h_cat(buf,o,"{\"id\":\"" as *u8); o=h_cat(buf,o,ids[i] as *u8)
82 o=h_cat(buf,o,"\",\"name\":\"" as *u8); o=h_cat(buf,o,names[i] as *u8)
83 o=h_cat(buf,o,"\",\"room\":\"" as *u8); o=h_cat(buf,o,rooms[i] as *u8)
84 o=h_cat(buf,o,"\",\"type\":\"" as *u8); o=h_cat(buf,o,kinds[i] as *u8)
85 o=h_cat(buf,o,"\",\"on\":" as *u8); o=h_catn(buf,o,pwr[i])
86 o=h_cat(buf,o,",\"brightness\":" as *u8); o=h_catn(buf,o,bri[i])
87 o=h_cat(buf,o,"}" as *u8); return o
88}
89func handle(cfd: i64, req: *u8, n: i64, ids: *i64, names: *i64, rooms: *i64, kinds: *i64, pwr: *i64, bri: *i64, ndev: i64) -> i64 {
90 if h_has(req,n,"POST /home/shutdown" as *u8)==1 {
91 let body: *u8="{\"ok\":1,\"msg\":\"shutting down\"}" as *u8
92 h_send(cfd, "200 OK" as *u8, body, h_strlen(body)); return 1
93 }
94 if h_has(req,n,"GET /home/state" as *u8)==1 {
95 let body: *u8=sys_mmap(K_MAGIC_16384); var o: i64=0
96 o=h_cat(body,o,"{\"devices\":[" as *u8)
97 var i: i64=0; while i<ndev { if i>0 { o=h_cat(body,o,"," as *u8) } o=j_dev(body,o, ids,names,rooms,kinds,pwr,bri, i); i=i+1 }
98 o=h_cat(body,o,"]}" as *u8)
99 h_send(cfd, "200 OK" as *u8, body, o); return 0
100 }
101 if h_has(req,n,"POST /home/cmd" as *u8)==1 {
102 let idbuf: *u8=sys_mmap(128); let idl: i64=h_extract(req,n,"id=" as *u8,idbuf,127)
103 if idl<0 { let e: *u8="{\"error\":\"missing id\"}" as *u8; h_send(cfd,"400 Bad Request" as *u8,e,h_strlen(e)); return 0 }
104 let idx: i64=find_dev(ids,ndev,idbuf)
105 if idx<0 {
106 let e: *u8=sys_mmap(256); var eo: i64=0; eo=h_cat(e,eo,"{\"error\":\"unknown device\",\"id\":\"" as *u8); eo=h_cat(e,eo,idbuf); eo=h_cat(e,eo,"\"}" as *u8)
107 h_send(cfd,"404 Not Found" as *u8, e, eo); return 0
108 }
109 let onbuf: *u8=sys_mmap(64); let onl: i64=h_extract(req,n,"on=" as *u8,onbuf,63)
110 if onl>=0 { var v: i64=h_atoi(onbuf); if v!=0 { pwr[idx]=1 } else { pwr[idx]=0 } }
111 let brbuf: *u8=sys_mmap(64); let brl: i64=h_extract(req,n,"bri=" as *u8,brbuf,63)
112 if brl>=0 { var v2: i64=h_atoi(brbuf); if v2<0 { v2=0 } if v2>100 { v2=100 } bri[idx]=v2 }
113 let body: *u8=sys_mmap(K_MAGIC_1024); let bl: i64=j_dev(body,0, ids,names,rooms,kinds,pwr,bri, idx)
114 h_send(cfd,"200 OK" as *u8, body, bl); return 0
115 }
116 let e: *u8="{\"error\":\"not found\"}" as *u8; h_send(cfd,"404 Not Found" as *u8, e, h_strlen(e)); return 0
117}
118
119// copy buf[s..e) (minus any \r) into pool as a NUL-terminated string; return its address; advance pool cursor
120func tsv_field(pool: *u8, pip: *i64, buf: *u8, s: i64, e: i64) -> i64 {
121 let base: i64=(pool as i64)+pip[0]; let p: *u8=base as *u8
122 var k: i64=0; var j: i64=s; while j<e { if buf[j]!=(13 as u8) { p[k]=buf[j]; k=k+1 } j=j+1 } p[k]=0 as u8
123 pip[0]=pip[0]+k+1; return base
124}
125// data-driven registry (#11): one device per line, TAB-separated id<TAB>name<TAB>room<TAB>type; return ndev (0 = none/missing -> caller uses embedded defaults)
126func load_tsv(path: *u8, ids: *i64, names: *i64, rooms: *i64, kinds: *i64, pwr: *i64, bri: *i64) -> i64 {
127 let szp: *i64=sys_mmap(16) as *i64
128 let buf: *u8=sys_read_file(path, szp)
129 if (buf as i64)==0 { return 0 }
130 let n: i64=szp[0]
131 let pool: *u8=sys_mmap(K_MAGIC_131072); let pip: *i64=sys_mmap(8) as *i64; pip[0]=0
132 let fp: *i64=sys_mmap(8*8) as *i64
133 var ndev: i64=0; var i: i64=0
134 while i<n {
135 if ndev>=64 { i=n } else {
136 var fnum: i64=0; var fs: i64=i; var done: i64=0
137 while done==0 {
138 if i>=n {
139 if i>fs { if fnum<4 { fp[fnum]=tsv_field(pool,pip,buf,fs,i) } fnum=fnum+1 }
140 done=1
141 } else {
142 let c: i64=buf[i] as i64
143 if c==9 { if fnum<4 { fp[fnum]=tsv_field(pool,pip,buf,fs,i) } fnum=fnum+1; i=i+1; fs=i }
144 else { if c==10 { if i>fs { if fnum<4 { fp[fnum]=tsv_field(pool,pip,buf,fs,i) } fnum=fnum+1 } i=i+1; done=1 }
145 else { i=i+1 } }
146 }
147 }
148 if fnum>=4 { ids[ndev]=fp[0]; names[ndev]=fp[1]; rooms[ndev]=fp[2]; kinds[ndev]=fp[3]; pwr[ndev]=0; bri[ndev]=0; ndev=ndev+1 }
149 }
150 }
151 return ndev
152}
153
154// operator-only auth: request must carry "Authorization: Bearer <token>" exactly matching the configured token
155func check_auth(req: *u8, n: i64, token: *u8) -> i64 {
156 let p: i64=h_find(req, n, "Authorization: Bearer " as *u8); if p<0 { return 0 }
157 var i: i64=0
158 while token[i]!=(0 as u8) { if p+i>=n { return 0 } if req[p+i]!=token[i] { return 0 } i=i+1 }
159 if p+i<n { let c: i64=req[p+i] as i64; if c!=13 { if c!=10 { if c!=32 { return 0 } } } } // exact match, no prefix
160 return 1
161}
162
163func main(argc: i64, argv: *i64) -> i64 {
164 if argc<2 { h_p("usage: nx_home <port> [registry.tsv] [tokenfile]\n" as *u8); sys_exit(2); return 2 }
165 let port: i64=h_atoi(argv[1] as *u8)
166 let ids: *i64=sys_mmap(8*64) as *i64; let names: *i64=sys_mmap(8*64) as *i64
167 let rooms: *i64=sys_mmap(8*64) as *i64; let kinds: *i64=sys_mmap(8*64) as *i64
168 let pwr: *i64=sys_mmap(8*64) as *i64; let bri: *i64=sys_mmap(8*64) as *i64
169 var ndev: i64=0
170 if argc>=3 { ndev=load_tsv(argv[2] as *u8, ids, names, rooms, kinds, pwr, bri) } // data-driven registry (#11)
171 if ndev==0 { // graceful fallback to embedded defaults (#17 config hierarchy: file > defaults)
172 ids[0]="living_room" as *u8 as i64; names[0]="Living Room" as *u8 as i64; rooms[0]="living" as *u8 as i64; kinds[0]="light" as *u8 as i64; pwr[0]=0; bri[0]=0
173 ids[1]="bedroom" as *u8 as i64; names[1]="Bedroom" as *u8 as i64; rooms[1]="bedroom" as *u8 as i64; kinds[1]="light" as *u8 as i64; pwr[1]=0; bri[1]=0
174 ids[2]="kitchen" as *u8 as i64; names[2]="Kitchen" as *u8 as i64; rooms[2]="kitchen" as *u8 as i64; kinds[2]="light" as *u8 as i64; pwr[2]=0; bri[2]=0
175 ndev=3
176 }
177 var tokenset: i64=0; let token: *u8=sys_mmap(256)
178 if argc>=4 { // operator-only auth when a token file is given (#12 defensive at boundary; #17 env/config-driven)
179 let tszp: *i64=sys_mmap(16) as *i64; let tb: *u8=sys_read_file(argv[3] as *u8, tszp)
180 if (tb as i64)!=0 {
181 var tn: i64=tszp[0]; var trimming: i64=1
182 while trimming==1 { if tn<=0 { trimming=0 } else { if tb[tn-1]==(10 as u8) { tn=tn-1 } else { if tb[tn-1]==(13 as u8) { tn=tn-1 } else { trimming=0 } } } }
183 var k: i64=0; while k<tn { token[k]=tb[k]; k=k+1 } token[tn]=0 as u8
184 if tn>0 { tokenset=1 }
185 }
186 }
187 let lfd: i64=h_bind_listen(port); if lfd<0 { h_p("nx_home BIND-FAIL rc=" as *u8); h_n(lfd); h_p("\n" as *u8); sys_exit(1); return 1 }
188 h_p("nx_home listening port=" as *u8); h_n(port); h_p(" devices=" as *u8); h_n(ndev); h_p(" (sim driver)\n" as *u8)
189 let req: *u8=sys_mmap(K_MAGIC_65536); var run: i64=1
190 while run==1 {
191 let cfd: i64=sys_accept(lfd)
192 if cfd<0 { run=0 } else {
193 let n: i64=h_recv(cfd, req, K_MAGIC_65536)
194 var authok: i64=1
195 if tokenset==1 { if check_auth(req, n, token)==0 { authok=0 } }
196 if authok==0 {
197 let e: *u8="{\"error\":\"unauthorized\"}" as *u8; h_send(cfd, "401 Unauthorized" as *u8, e, h_strlen(e)); sys_close(cfd)
198 } else {
199 let sd: i64=handle(cfd, req, n, ids,names,rooms,kinds,pwr,bri, ndev)
200 sys_close(cfd)
201 if sd==1 { run=0 }
202 }
203 }
204 }
205 sys_close(lfd); h_p("nx_home stopped\n" as *u8); sys_exit(0); return 0
206}