code wiki / _hdl_build / nx_printer_ctl_lib.nx

nx_printer_ctl_lib.nx source

↩ module page · 836 lines · 36013 B

1// nx_printer_ctl_lib.nx -- sovereign 3D-printer / home-IoT control LIB (no main). 2// 3// The reusable core behind nx_printer_ctl (CLI organ) + nx_printer_ctl_gate. 4// Composes the shipped Moonraker primitives (nx_moonraker_client request 5// builders + nx_moonraker_io arbitrary-IPv4 roundtrip) into a cap-gated, 6// JSON-emitting device-control surface that the sovereign edge reaches over 7// the LAN. 8// 9// ARCHITECTURE (the operator's browser->edge->mTLS-home->printer design, 10// realized sovereignly): the sovereign edge (sites.elf :8443) already does 11// OPAQUE cap-token auth + reverse-proxy. This organ is the "home gateway" 12// leg -- BUT the NAS that runs the edge is ITSELF on the home LAN, so the 13// organ reaches the printer at <lan-ip>:7125 directly. The printer never 14// sees the WAN; only the cap-gated organ ever touches it => "zero external 15// exposure" holds BY CONSTRUCTION, no Node/nginx/WAN-mTLS-tunnel required. 16// 17// NEVER-BRICK (CLAUDE rule 26, ABSOLUTE): every control verb maps to a FIXED 18// allowlisted Moonraker endpoint (info / emergency_stop / print pause|resume 19// |cancel). There is NO raw-G-code passthrough, so no path can ever carry a 20// firmware-flash / EEPROM-write G-code (M500/M502/etc.). emergency_stop is a 21// Klipper RUNTIME MCU halt (recovered by a soft FIRMWARE_RESTART, RAM-only) -- 22// it writes NO persistent hardware state. The safe-endpoint allowlist is 23// proven mechanically by nx_printer_ctl_gate (a control path outside the 24// allowlist => gate RED). 25// 26// license_tier: ORIGINAL No hw writes (Rule 26). 27import "nx_syscalls.nx" 28import "nx_moonraker_client.nx" 29import "nx_moonraker_io.nx" 30import "nx_lan_scan.nx" 31 32const PCTL_SCRATCH: i64 = 8192 33const PCTL_RESP: i64 = 16384 34const PCTL_OUT: i64 = 32768 35 36// verb ids 37const PCTL_V_INFO: i64 = 1 38const PCTL_V_ESTOP: i64 = 2 39const PCTL_V_PAUSE: i64 = 3 40const PCTL_V_RESUME: i64 = 4 41const PCTL_V_CANCEL: i64 = 5 42const PCTL_V_CONTRACT: i64 = 6 43const PCTL_V_STATUS: i64 = 7 44const PCTL_V_READY: i64 = 8 45const PCTL_V_DISCOVER: i64 = 9 46const PCTL_V_SURVEY: i64 = 10 47const PCTL_V_MONITOR: i64 = 11 48const PCTL_V_FILES: i64 = 12 49const PCTL_V_PROGRESS: i64 = 13 50const PCTL_V_PRINTSTART: i64 = 14 51const PCTL_V_UPLOAD: i64 = 15 52const PCTL_V_BAD: i64 = 0 53const PCTL_TEMP_NONE: i64 = 0 - 32768 54const PCTL_SOCK_NONBLOCK: i64 = 2048 // SOCK_NONBLOCK 55const PCTL_POLLOUT: i64 = 4 56const PCTL_POLLERR: i64 = 8 57const PCTL_POLLHUP: i64 = 16 58// home-device survey port profile (curated; each port scanned across the /24). 59// Port index -> bit in the per-host mask; classification derives from the mask. 60const PCTL_SURVEY_NPORTS: i64 = 4 61 62// ===== small utils ================================================= 63func pctl_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 64func pctl_streq(a: *u8, b: *u8) -> i64 { 65 var i: i64 = 0 66 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 67 if b[i] != (0 as u8) { return 0 } 68 return 1 69} 70func pctl_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o] = s[i]; o = o + 1; i = i + 1 } return o } 71func pctl_catn(d: *u8, o: i64, v: i64) -> i64 { 72 let t: *u8 = sys_mmap(28) 73 var m: i64 = v 74 if m < 0 { d[o] = 45 as u8; o = o + 1; m = 0 - m } 75 var k: i64 = 0 76 if m == 0 { t[0] = 48 as u8; k = 1 } 77 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 78 var i: i64 = 0 79 while i < k { d[o] = t[k-1-i]; o = o + 1; i = i + 1 } 80 return o 81} 82// escape body bytes [s,e) into a JSON string value: " -> ', \ -> /, ctrl -> space 83func pctl_cat_esc(d: *u8, o: i64, q: *u8, s: i64, e: i64) -> i64 { 84 var i: i64 = s 85 while i < e { 86 var c: i64 = q[i] as i64 87 c = c & 0xff 88 if c == 34 { c = 39 } 89 if c == 92 { c = 47 } 90 if c < 32 { c = 32 } 91 d[o] = c as u8 92 o = o + 1 93 i = i + 1 94 } 95 return o 96} 97 98// ===== dotted-IPv4 parse -> packed i64 (matches nx_mr_pack_ipv4) ==== 99// "192.168.1.42" -> (192<<24)|(168<<16)|(1<<8)|42 ; returns -1 on malformed. 100func pctl_parse_ip(s: *u8) -> i64 { 101 var val: i64 = 0 102 var ndig: i64 = 0 103 var oct: i64 = 0 104 var packed: i64 = 0 105 var i: i64 = 0 106 var done: i64 = 0 107 while done == 0 { 108 let c: i64 = s[i] & 0xff 109 if c == 0 { 110 done = 1 111 } else { 112 if c == 46 { 113 if ndig == 0 { return 0 - 1 } 114 if val > 255 { return 0 - 1 } 115 packed = (packed << 8) | val 116 oct = oct + 1 117 val = 0 118 ndig = 0 119 i = i + 1 120 } else { 121 if c < 48 { return 0 - 1 } 122 if c > 57 { return 0 - 1 } 123 val = val * 10 + (c - 48) 124 ndig = ndig + 1 125 i = i + 1 126 } 127 } 128 } 129 if ndig == 0 { return 0 - 1 } 130 if val > 255 { return 0 - 1 } 131 packed = (packed << 8) | val 132 oct = oct + 1 133 if oct != 4 { return 0 - 1 } 134 return packed 135} 136 137// parse a positive decimal string -> value, or -1 on any non-digit / empty 138func pctl_parse_port(s: *u8) -> i64 { 139 var v: i64 = 0 140 var i: i64 = 0 141 if s[0] == (0 as u8) { return 0 - 1 } 142 while s[i] != (0 as u8) { 143 let c: i64 = s[i] & 0xff 144 if c < 48 { return 0 - 1 } 145 if c > 57 { return 0 - 1 } 146 v = v * 10 + (c - 48) 147 i = i + 1 148 } 149 if v < 1 { return 0 - 1 } 150 if v > 65535 { return 0 - 1 } 151 return v 152} 153 154// build the "a.b.c.d:port" Host header value into host buf; returns length 155func pctl_build_host(ip: *u8, port: i64, host: *u8) -> i64 { 156 var o: i64 = 0 157 o = pctl_cat(host, o, ip) 158 host[o] = 58 as u8 159 o = o + 1 160 o = pctl_catn(host, o, port) 161 host[o] = 0 as u8 162 return o 163} 164 165// ===== verb name -> id + safe-endpoint allowlist (NEVER-BRICK) ===== 166// Returns the FIXED Moonraker path for a control verb. Any verb not in this 167// map returns NULL -- there is no way to reach an arbitrary path, so no 168// firmware-write G-code can ever be routed. This IS the never-brick guarantee. 169func pctl_verb_id(v: *u8) -> i64 { 170 if pctl_streq(v, "info" as *u8) == 1 { return PCTL_V_INFO } 171 if pctl_streq(v, "estop" as *u8) == 1 { return PCTL_V_ESTOP } 172 if pctl_streq(v, "pause" as *u8) == 1 { return PCTL_V_PAUSE } 173 if pctl_streq(v, "resume" as *u8) == 1 { return PCTL_V_RESUME } 174 if pctl_streq(v, "cancel" as *u8) == 1 { return PCTL_V_CANCEL } 175 if pctl_streq(v, "contract" as *u8) == 1 { return PCTL_V_CONTRACT } 176 if pctl_streq(v, "status" as *u8) == 1 { return PCTL_V_STATUS } 177 if pctl_streq(v, "ready" as *u8) == 1 { return PCTL_V_READY } 178 if pctl_streq(v, "discover" as *u8) == 1 { return PCTL_V_DISCOVER } 179 if pctl_streq(v, "survey" as *u8) == 1 { return PCTL_V_SURVEY } 180 if pctl_streq(v, "monitor" as *u8) == 1 { return PCTL_V_MONITOR } 181 if pctl_streq(v, "files" as *u8) == 1 { return PCTL_V_FILES } 182 if pctl_streq(v, "progress" as *u8) == 1 { return PCTL_V_PROGRESS } 183 if pctl_streq(v, "printstart" as *u8) == 1 { return PCTL_V_PRINTSTART } 184 if pctl_streq(v, "upload" as *u8) == 1 { return PCTL_V_UPLOAD } 185 return PCTL_V_BAD 186} 187func pctl_verb_is_get(id: i64) -> i64 { 188 if id == PCTL_V_INFO { return 1 } 189 if id == PCTL_V_STATUS { return 1 } 190 if id == PCTL_V_READY { return 1 } 191 if id == PCTL_V_FILES { return 1 } 192 if id == PCTL_V_PROGRESS { return 1 } 193 return 0 194} 195// the ONLY paths this organ can ever emit (safe control surface allowlist) 196func pctl_verb_path(id: i64) -> *u8 { 197 if id == PCTL_V_INFO { return "/printer/info" as *u8 } 198 if id == PCTL_V_ESTOP { return "/printer/emergency_stop" as *u8 } 199 if id == PCTL_V_PAUSE { return "/printer/print/pause" as *u8 } 200 if id == PCTL_V_RESUME { return "/printer/print/resume" as *u8 } 201 if id == PCTL_V_CANCEL { return "/printer/print/cancel" as *u8 } 202 if id == PCTL_V_STATUS { return "/printer/objects/query?extruder=temperature,target&heater_bed=temperature,target" as *u8 } 203 if id == PCTL_V_READY { return "/printer/info" as *u8 } 204 if id == PCTL_V_FILES { return "/server/files/list" as *u8 } 205 if id == PCTL_V_PROGRESS { return "/printer/objects/query?print_stats&virtual_sdcard&display_status" as *u8 } 206 if id == PCTL_V_PRINTSTART { return "/printer/print/start" as *u8 } 207 return "\x00" as *u8 208} 209func pctl_verb_name(id: i64) -> *u8 { 210 if id == PCTL_V_INFO { return "info" as *u8 } 211 if id == PCTL_V_ESTOP { return "estop" as *u8 } 212 if id == PCTL_V_PAUSE { return "pause" as *u8 } 213 if id == PCTL_V_RESUME { return "resume" as *u8 } 214 if id == PCTL_V_CANCEL { return "cancel" as *u8 } 215 if id == PCTL_V_CONTRACT { return "contract" as *u8 } 216 if id == PCTL_V_STATUS { return "status" as *u8 } 217 if id == PCTL_V_READY { return "ready" as *u8 } 218 if id == PCTL_V_DISCOVER { return "discover" as *u8 } 219 if id == PCTL_V_SURVEY { return "survey" as *u8 } 220 if id == PCTL_V_MONITOR { return "monitor" as *u8 } 221 if id == PCTL_V_FILES { return "files" as *u8 } 222 if id == PCTL_V_PROGRESS { return "progress" as *u8 } 223 if id == PCTL_V_PRINTSTART { return "printstart" as *u8 } 224 if id == PCTL_V_UPLOAD { return "upload" as *u8 } 225 return "bad" as *u8 226} 227 228// ===== response body-start (after "\r\n\r\n") ====================== 229func pctl_body_start(resp: *u8, n: i64) -> i64 { 230 var i: i64 = 0 231 while i + 3 < n { 232 if resp[i] == (13 as u8) { if resp[i+1] == (10 as u8) { if resp[i+2] == (13 as u8) { if resp[i+3] == (10 as u8) { return i + 4 } } } } 233 i = i + 1 234 } 235 return n 236} 237 238// ===== the composed network call (SAME path CLI + gate both use) === 239// Performs the verb's Moonraker roundtrip against ip:port. Returns the HTTP 240// status code (>=100), or a negative nx_moonraker_io verdict on transport 241// failure. Fills resp[0..out] with the raw response; caller reads n via *rn. 242func pctl_call(id: i64, ipv4: i64, port: i64, 243 host: *u8, host_len: i64, 244 api_key: *u8, api_key_len: i64, 245 scratch: *u8, resp: *u8, rn: *i64) -> i64 { 246 let path: *u8 = pctl_verb_path(id) 247 let plen: i64 = pctl_slen(path) 248 var n: i64 = 0 249 if pctl_verb_is_get(id) == 1 { 250 n = nx_mr_get_ipv4(ipv4, port, host, host_len, path, plen, api_key, api_key_len, scratch, PCTL_SCRATCH, resp, PCTL_RESP) 251 } else { 252 let empty: *u8 = "\x00" as *u8 253 n = nx_mr_post_ipv4(ipv4, port, host, host_len, path, plen, empty, 0, api_key, api_key_len, scratch, PCTL_SCRATCH, resp, PCTL_RESP) 254 } 255 rn[0] = n 256 if n <= 0 { return n } 257 let st: i64 = nx_mr_parse_status(resp, n) 258 return st 259} 260 261// ===== JSON result emit for a network verb ========================= 262// {"verb":"..","target":"ip:port","http_status":N,"result":"..","body_excerpt":".."} 263func pctl_emit_result(out: *u8, id: i64, host: *u8, status: i64, resp: *u8, n: i64) -> i64 { 264 var o: i64 = 0 265 o = pctl_cat(out, o, "{\"verb\":\"" as *u8) 266 o = pctl_cat(out, o, pctl_verb_name(id)) 267 o = pctl_cat(out, o, "\",\"target\":\"" as *u8) 268 o = pctl_cat(out, o, host) 269 o = pctl_cat(out, o, "\",\"http_status\":" as *u8) 270 o = pctl_catn(out, o, status) 271 o = pctl_cat(out, o, ",\"result\":\"" as *u8) 272 if status >= 200 { if status < 300 { o = pctl_cat(out, o, "OK" as *u8) } } 273 if status >= 300 { o = pctl_cat(out, o, "HTTP_ERROR" as *u8) } 274 if status < 200 { if status >= 100 { o = pctl_cat(out, o, "HTTP_INFO" as *u8) } } 275 if status <= 0 { o = pctl_cat(out, o, "NETWORK_ERR" as *u8) } 276 if status > 0 { if status < 100 { o = pctl_cat(out, o, "BAD_RESPONSE" as *u8) } } 277 o = pctl_cat(out, o, "\",\"body_excerpt\":\"" as *u8) 278 if n > 0 { 279 let bs: i64 = pctl_body_start(resp, n) 280 var be: i64 = bs + 240 281 if be > n { be = n } 282 o = pctl_cat_esc(out, o, resp, bs, be) 283 } 284 o = pctl_cat(out, o, "\"}\n" as *u8) 285 return o 286} 287 288// ===== lean telemetry parse (mirrors nx_moonraker_telemetry_parse; self-contained ===== 289// so the build closure stays minimal for a robust NAS build) ======================== 290func pctl_find(body: *u8, n: i64, start: i64, needle: *u8) -> i64 { 291 var nl: i64 = 0 292 while needle[nl] != (0 as u8) { nl = nl + 1 } 293 if nl == 0 { return start } 294 if start < 0 { return 0 - 1 } 295 var i: i64 = start 296 let stop: i64 = n - nl 297 while i <= stop { 298 var j: i64 = 0 299 var m: i64 = 1 300 while j < nl { 301 if body[i + j] != needle[j] { m = 0; j = nl } else { j = j + 1 } 302 } 303 if m == 1 { return i } 304 i = i + 1 305 } 306 return 0 - 1 307} 308// skip ws, optional '-', digits; truncate at first non-digit; PCTL_TEMP_NONE if none 309func pctl_parse_int_deg(body: *u8, off: i64, n: i64) -> i64 { 310 var q: i64 = off 311 var sk: i64 = 1 312 while sk == 1 { 313 if q >= n { sk = 0 } else { 314 let c: i64 = body[q] & 0xff 315 if c == 32 { q = q + 1 } else { 316 if c == 9 { q = q + 1 } else { 317 if c == 10 { q = q + 1 } else { 318 if c == 13 { q = q + 1 } else { sk = 0 } 319 } 320 } 321 } 322 } 323 } 324 if q >= n { return PCTL_TEMP_NONE } 325 var neg: i64 = 0 326 if body[q] == (45 as u8) { neg = 1; q = q + 1 } 327 var have: i64 = 0 328 var v: i64 = 0 329 var rd: i64 = 1 330 while rd == 1 { 331 if q >= n { rd = 0 } else { 332 let d: i64 = body[q] & 0xff 333 if d < 48 { rd = 0 } else { 334 if d > 57 { rd = 0 } else { 335 v = v * 10 + (d - 48) 336 have = 1 337 q = q + 1 338 } 339 } 340 } 341 } 342 if have == 0 { return PCTL_TEMP_NONE } 343 if neg == 1 { return 0 - v } 344 return v 345} 346// find object key (e.g. "\"extruder\":") then field (e.g. "\"temperature\":") after it -> int 347func pctl_temp_in(body: *u8, n: i64, objkey: *u8, field: *u8) -> i64 { 348 let os: i64 = pctl_find(body, n, 0, objkey) 349 if os < 0 { return PCTL_TEMP_NONE } 350 let fs: i64 = pctl_find(body, n, os, field) 351 if fs < 0 { return PCTL_TEMP_NONE } 352 let fl: i64 = pctl_slen(field) 353 return pctl_parse_int_deg(body, fs + fl, n) 354} 355func pctl_cat_num_or_null(d: *u8, o: i64, v: i64) -> i64 { 356 if v == PCTL_TEMP_NONE { return pctl_cat(d, o, "null" as *u8) } 357 return pctl_catn(d, o, v) 358} 359func pctl_ready_flag(body: *u8, n: i64) -> i64 { 360 if pctl_find(body, n, 0, "\"state\":\"ready\"" as *u8) >= 0 { return 1 } 361 if pctl_find(body, n, 0, "\"state\": \"ready\"" as *u8) >= 0 { return 1 } 362 return 0 363} 364func pctl_state_of(body: *u8, n: i64, rdy: i64) -> *u8 { 365 if rdy == 1 { return "ready" as *u8 } 366 if pctl_find(body, n, 0, "shutdown" as *u8) >= 0 { return "shutdown" as *u8 } 367 if pctl_find(body, n, 0, "error" as *u8) >= 0 { return "error" as *u8 } 368 if pctl_find(body, n, 0, "startup" as *u8) >= 0 { return "startup" as *u8 } 369 return "unknown" as *u8 370} 371func pctl_cat_netresult(d: *u8, o: i64, status: i64) -> i64 { 372 if status <= 0 { return pctl_cat(d, o, "NETWORK_ERR" as *u8) } 373 if status >= 200 { if status < 300 { return pctl_cat(d, o, "OK" as *u8) } } 374 return pctl_cat(d, o, "HTTP_ERROR" as *u8) 375} 376// ===== status verb: parsed hotend/bed temps + targets ============== 377func pctl_emit_status(out: *u8, host: *u8, status: i64, resp: *u8, n: i64) -> i64 { 378 var o: i64 = 0 379 o = pctl_cat(out, o, "{\"verb\":\"status\",\"target\":\"" as *u8) 380 o = pctl_cat(out, o, host) 381 o = pctl_cat(out, o, "\",\"http_status\":" as *u8) 382 o = pctl_catn(out, o, status) 383 var ha: i64 = PCTL_TEMP_NONE 384 var ht: i64 = PCTL_TEMP_NONE 385 var ba: i64 = PCTL_TEMP_NONE 386 var bt: i64 = PCTL_TEMP_NONE 387 if n > 0 { if status >= 200 { if status < 300 { 388 let bs: i64 = pctl_body_start(resp, n) 389 let bp: *u8 = ((resp as i64) + bs) as *u8 390 let bn: i64 = n - bs 391 ha = pctl_temp_in(bp, bn, "\"extruder\":" as *u8, "\"temperature\":" as *u8) 392 ht = pctl_temp_in(bp, bn, "\"extruder\":" as *u8, "\"target\":" as *u8) 393 ba = pctl_temp_in(bp, bn, "\"heater_bed\":" as *u8, "\"temperature\":" as *u8) 394 bt = pctl_temp_in(bp, bn, "\"heater_bed\":" as *u8, "\"target\":" as *u8) 395 } } } 396 o = pctl_cat(out, o, ",\"hotend\":{\"actual_c\":" as *u8) 397 o = pctl_cat_num_or_null(out, o, ha) 398 o = pctl_cat(out, o, ",\"target_c\":" as *u8) 399 o = pctl_cat_num_or_null(out, o, ht) 400 o = pctl_cat(out, o, "},\"bed\":{\"actual_c\":" as *u8) 401 o = pctl_cat_num_or_null(out, o, ba) 402 o = pctl_cat(out, o, ",\"target_c\":" as *u8) 403 o = pctl_cat_num_or_null(out, o, bt) 404 o = pctl_cat(out, o, "},\"result\":\"" as *u8) 405 o = pctl_cat_netresult(out, o, status) 406 o = pctl_cat(out, o, "\"}\n" as *u8) 407 return o 408} 409// ===== ready verb: preflight klippy state ========================== 410func pctl_emit_ready(out: *u8, host: *u8, status: i64, resp: *u8, n: i64) -> i64 { 411 var o: i64 = 0 412 o = pctl_cat(out, o, "{\"verb\":\"ready\",\"target\":\"" as *u8) 413 o = pctl_cat(out, o, host) 414 o = pctl_cat(out, o, "\",\"http_status\":" as *u8) 415 o = pctl_catn(out, o, status) 416 var rdy: i64 = 0 417 var state: *u8 = "unknown" as *u8 418 if n > 0 { if status >= 200 { if status < 300 { 419 let bs: i64 = pctl_body_start(resp, n) 420 let bp: *u8 = ((resp as i64) + bs) as *u8 421 let bn: i64 = n - bs 422 rdy = pctl_ready_flag(bp, bn) 423 state = pctl_state_of(bp, bn, rdy) 424 } } } 425 o = pctl_cat(out, o, ",\"ready\":" as *u8) 426 o = pctl_catn(out, o, rdy) 427 o = pctl_cat(out, o, ",\"klippy_state\":\"" as *u8) 428 o = pctl_cat(out, o, state) 429 o = pctl_cat(out, o, "\",\"result\":\"" as *u8) 430 o = pctl_cat_netresult(out, o, status) 431 o = pctl_cat(out, o, "\"}\n" as *u8) 432 return o 433} 434 435// ===== discover: sovereign LAN sweep for Moonraker printers ======== 436// 437// DRY (rule 15): the sweep itself now lives in nx_lan_scan and is SHARED with 438// nx_iot_ctl. It moved because the two organs must AGREE about the same LAN -- 439// with two copies, one gets a fix and the other silently does not, and they 440// then answer "is host H open on port P" differently while both look healthy. 441// That is not hypothetical: on 2026-07-25 the printer survey reported 5 hosts 442// on 192.168.8 while the new IoT scan reported 6, minutes apart. 443// 444// These remain as thin wrappers so every existing caller and the 14-tooth 445// nx_printer_ctl_gate keep working unchanged (rule 19: never break a contract). 446func pctl_parse_prefix(s: *u8) -> i64 { return lscan_parse_prefix(s) } 447// non-blocking TCP connect probe: 1 if <ipv4>:<port> accepts, else 0 448func pctl_probe_host(ipv4: i64, port: i64, timeout_ms: i64) -> i64 { 449 return lscan_probe_host(ipv4, port, timeout_ms) 450} 451// scan base24.1 .. base24.254 on <port>; fill out_ips with packed found; return count. 452// BATCHED (scale-law): fire all 254 non-blocking connects, then ONE poll over the whole 453// set -> O(one timeout) not O(N x timeout), so a full /24 sweep fits an MCP call window. 454func pctl_discover_scan(base24: i64, port: i64, timeout_ms: i64, out_ips: *i64, max: i64) -> i64 { 455 return lscan_sweep(base24, port, timeout_ms, out_ips, max) 456} 457func pctl_cat_ip(d: *u8, o: i64, ipv4: i64) -> i64 { 458 o = pctl_catn(d, o, (ipv4 >> 24) & 0xff) 459 d[o] = 46 as u8 460 o = o + 1 461 o = pctl_catn(d, o, (ipv4 >> 16) & 0xff) 462 d[o] = 46 as u8 463 o = o + 1 464 o = pctl_catn(d, o, (ipv4 >> 8) & 0xff) 465 d[o] = 46 as u8 466 o = o + 1 467 o = pctl_catn(d, o, ipv4 & 0xff) 468 return o 469} 470func pctl_emit_discover(out: *u8, prefix: *u8, port: i64, ips: *i64, count: i64) -> i64 { 471 var o: i64 = 0 472 o = pctl_cat(out, o, "{\"verb\":\"discover\",\"subnet\":\"" as *u8) 473 o = pctl_cat(out, o, prefix) 474 o = pctl_cat(out, o, "\",\"port\":" as *u8) 475 o = pctl_catn(out, o, port) 476 o = pctl_cat(out, o, ",\"found\":[" as *u8) 477 var i: i64 = 0 478 while i < count { 479 if i > 0 { o = pctl_cat(out, o, "," as *u8) } 480 o = pctl_cat(out, o, "\"" as *u8) 481 o = pctl_cat_ip(out, o, ips[i]) 482 o = pctl_cat(out, o, "\"" as *u8) 483 i = i + 1 484 } 485 o = pctl_cat(out, o, "],\"count\":" as *u8) 486 o = pctl_catn(out, o, count) 487 o = pctl_cat(out, o, "}\n" as *u8) 488 return o 489} 490 491// ===== survey: home-device inventory (multi-port classify) ========= 492// bit0=7125 Moonraker, bit1=10088 Fluidd, bit2=80 HTTP, bit3=443 HTTPS 493func pctl_survey_port(pi: i64) -> i64 { 494 if pi == 0 { return 7125 } 495 if pi == 1 { return 10088 } 496 if pi == 2 { return 80 } 497 if pi == 3 { return 443 } 498 return 0 499} 500// scan each survey port across the /24, OR-ing a per-host port-mask. 501// Reuses the proven batched poll-until-resolved scan (each port = <=254 sockets, fd-safe). 502func pctl_survey_scan(base24: i64, budget_per_port: i64, mask: *i64) -> i64 { 503 let ips: *i64 = sys_mmap(254 * 8) as *i64 504 var pi: i64 = 0 505 while pi < PCTL_SURVEY_NPORTS { 506 let port: i64 = pctl_survey_port(pi) 507 let cnt: i64 = pctl_discover_scan(base24, port, budget_per_port, ips, 254) 508 var j: i64 = 0 509 while j < cnt { 510 let h: i64 = ips[j] & 0xff 511 let m: i64 = mask[h] 512 mask[h] = m | (1 << pi) 513 j = j + 1 514 } 515 pi = pi + 1 516 } 517 return 0 518} 519func pctl_survey_kind(bits: i64) -> *u8 { 520 if (bits & 1) != 0 { return "3d-printer" as *u8 } 521 if (bits & 2) != 0 { return "3d-printer" as *u8 } 522 if (bits & 4) != 0 { return "web-device" as *u8 } 523 if (bits & 8) != 0 { return "web-device" as *u8 } 524 return "device" as *u8 525} 526func pctl_emit_survey(out: *u8, prefix: *u8, base24: i64, mask: *i64) -> i64 { 527 var o: i64 = 0 528 o = pctl_cat(out, o, "{\"verb\":\"survey\",\"subnet\":\"" as *u8) 529 o = pctl_cat(out, o, prefix) 530 o = pctl_cat(out, o, "\",\"devices\":[" as *u8) 531 var count: i64 = 0 532 var h: i64 = 1 533 while h <= 254 { 534 let bits: i64 = mask[h] 535 if bits != 0 { 536 if count > 0 { o = pctl_cat(out, o, "," as *u8) } 537 o = pctl_cat(out, o, "{\"ip\":\"" as *u8) 538 o = pctl_cat_ip(out, o, (base24 << 8) | h) 539 o = pctl_cat(out, o, "\",\"kind\":\"" as *u8) 540 o = pctl_cat(out, o, pctl_survey_kind(bits)) 541 o = pctl_cat(out, o, "\",\"ports\":[" as *u8) 542 var pi: i64 = 0 543 var np: i64 = 0 544 while pi < PCTL_SURVEY_NPORTS { 545 if (bits & (1 << pi)) != 0 { 546 if np > 0 { o = pctl_cat(out, o, "," as *u8) } 547 o = pctl_catn(out, o, pctl_survey_port(pi)) 548 np = np + 1 549 } 550 pi = pi + 1 551 } 552 o = pctl_cat(out, o, "]}" as *u8) 553 count = count + 1 554 } 555 h = h + 1 556 } 557 o = pctl_cat(out, o, "],\"count\":" as *u8) 558 o = pctl_catn(out, o, count) 559 o = pctl_cat(out, o, "}\n" as *u8) 560 return o 561} 562 563// ===== monitor: ONE sovereign call = the whole dashboard JSON ======= 564// Replaces the shell beat's grep/date logic (no coreutils). discover -> if online 565// status+ready -> survey -> epoch (sovereign clock syscall) -> aggregated JSON. 566func pctl_ip_to_str(pip: i64, buf: *u8) -> i64 { 567 var o: i64 = 0 568 o = pctl_catn(buf, o, (pip >> 24) & 0xff) 569 buf[o] = 46 as u8 570 o = o + 1 571 o = pctl_catn(buf, o, (pip >> 16) & 0xff) 572 buf[o] = 46 as u8 573 o = o + 1 574 o = pctl_catn(buf, o, (pip >> 8) & 0xff) 575 buf[o] = 46 as u8 576 o = o + 1 577 o = pctl_catn(buf, o, pip & 0xff) 578 buf[o] = 0 as u8 579 return o 580} 581func pctl_build_monitor(out: *u8, subnet: *u8, base24: i64) -> i64 { 582 let ts: *i64 = sys_mmap(16) as *i64 583 sys_clock_gettime_real(ts) 584 let epoch: i64 = ts[0] 585 let ips: *i64 = sys_mmap(254 * 8) as *i64 586 let dcount: i64 = pctl_discover_scan(base24, 7125, 250, ips, 254) 587 var online: i64 = 0 588 var pip: i64 = 0 589 if dcount > 0 { online = 1; pip = ips[0] } 590 let mask: *i64 = sys_mmap(256 * 8) as *i64 591 pctl_survey_scan(base24, LSCAN_BUDGET_MS, mask) 592 var o: i64 = 0 593 o = pctl_cat(out, o, "{\"updated_epoch\":" as *u8) 594 o = pctl_catn(out, o, epoch) 595 o = pctl_cat(out, o, ",\"subnet\":\"" as *u8) 596 o = pctl_cat(out, o, subnet) 597 o = pctl_cat(out, o, "\",\"printer_ip\":\"" as *u8) 598 let ipstr: *u8 = sys_mmap(24) 599 if online == 1 { 600 pctl_ip_to_str(pip, ipstr) 601 o = pctl_cat(out, o, ipstr) 602 } 603 o = pctl_cat(out, o, "\",\"printer_online\":" as *u8) 604 if online == 1 { o = pctl_cat(out, o, "true" as *u8) } else { o = pctl_cat(out, o, "false" as *u8) } 605 o = pctl_cat(out, o, ",\"status\":" as *u8) 606 if online == 1 { 607 let host: *u8 = sys_mmap(64) 608 let hl: i64 = pctl_build_host(ipstr, 7125, host) 609 let scratch: *u8 = sys_mmap(PCTL_SCRATCH) 610 let resp: *u8 = sys_mmap(PCTL_RESP) 611 let rn: *i64 = sys_mmap(8) as *i64 612 let empty: *u8 = "\x00" as *u8 613 let sc: i64 = pctl_call(PCTL_V_STATUS, pip, 7125, host, hl, empty, 0, scratch, resp, rn) 614 let tmp: *u8 = sys_mmap(PCTL_OUT) 615 let tl: i64 = pctl_emit_status(tmp, host, sc, resp, rn[0]) 616 if tl > 0 { if tmp[tl-1] == (10 as u8) { tmp[tl-1] = 0 as u8 } else { tmp[tl] = 0 as u8 } } 617 o = pctl_cat(out, o, tmp) 618 o = pctl_cat(out, o, ",\"ready\":" as *u8) 619 let resp2: *u8 = sys_mmap(PCTL_RESP) 620 let rn2: *i64 = sys_mmap(8) as *i64 621 let rc: i64 = pctl_call(PCTL_V_READY, pip, 7125, host, hl, empty, 0, scratch, resp2, rn2) 622 let tmp2: *u8 = sys_mmap(PCTL_OUT) 623 let tl2: i64 = pctl_emit_ready(tmp2, host, rc, resp2, rn2[0]) 624 if tl2 > 0 { if tmp2[tl2-1] == (10 as u8) { tmp2[tl2-1] = 0 as u8 } else { tmp2[tl2] = 0 as u8 } } 625 o = pctl_cat(out, o, tmp2) 626 } else { 627 o = pctl_cat(out, o, "null,\"ready\":null" as *u8) 628 } 629 o = pctl_cat(out, o, ",\"survey\":" as *u8) 630 let sv: *u8 = sys_mmap(PCTL_OUT) 631 let svl: i64 = pctl_emit_survey(sv, subnet, base24, mask) 632 if svl > 0 { if sv[svl-1] == (10 as u8) { sv[svl-1] = 0 as u8 } else { sv[svl] = 0 as u8 } } 633 o = pctl_cat(out, o, sv) 634 o = pctl_cat(out, o, "}\n" as *u8) 635 return o 636} 637 638// ===== print job workflow: progress parse (print_stats + virtual_sdcard) ==== 639// copy a JSON string value: find <anchor> then <keyq> (e.g. "\"state\":\"") after it, 640// copy chars until the closing quote (escaping \ and ctrl for our own JSON). 641func pctl_cat_jsonstr_after(out: *u8, o: i64, body: *u8, n: i64, anchor: *u8, keyq: *u8) -> i64 { 642 let ao: i64 = pctl_find(body, n, 0, anchor) 643 if ao < 0 { return o } 644 let ko: i64 = pctl_find(body, n, ao, keyq) 645 if ko < 0 { return o } 646 var i: i64 = ko + pctl_slen(keyq) 647 var go: i64 = 1 648 while go == 1 { 649 if i >= n { go = 0 } else { 650 var c: i64 = body[i] & 0xff 651 if c == 34 { go = 0 } else { 652 if c == 92 { c = 47 } 653 if c < 32 { c = 32 } 654 out[o] = c as u8 655 o = o + 1 656 i = i + 1 657 } 658 } 659 } 660 return o 661} 662// parse a Moonraker 0.0-1.0 progress float after <anchor>/<keyq> -> integer percent 0-100 663func pctl_pct_after(body: *u8, n: i64, anchor: *u8, keyq: *u8) -> i64 { 664 let ao: i64 = pctl_find(body, n, 0, anchor) 665 if ao < 0 { return 0 } 666 let ko: i64 = pctl_find(body, n, ao, keyq) 667 if ko < 0 { return 0 } 668 var i: i64 = ko + pctl_slen(keyq) 669 var sk: i64 = 1 670 while sk == 1 { if i >= n { sk = 0 } else { let c: i64 = body[i] & 0xff; if c == 32 { i = i + 1 } else { sk = 0 } } } 671 var intp: i64 = 0 672 var rd: i64 = 1 673 while rd == 1 { if i >= n { rd = 0 } else { let d: i64 = body[i] & 0xff; if d < 48 { rd = 0 } else { if d > 57 { rd = 0 } else { intp = intp * 10 + (d - 48); i = i + 1 } } } } 674 if intp >= 1 { return 100 } 675 var f: i64 = 0 676 if i < n { 677 if body[i] == (46 as u8) { 678 i = i + 1 679 if i < n { 680 let d1: i64 = body[i] & 0xff 681 if d1 >= 48 { if d1 <= 57 { 682 f = (d1 - 48) * 10 683 i = i + 1 684 if i < n { let d2: i64 = body[i] & 0xff; if d2 >= 48 { if d2 <= 57 { f = f + (d2 - 48) } } } 685 } } 686 } 687 } 688 } 689 return f 690} 691func pctl_emit_progress(out: *u8, host: *u8, status: i64, resp: *u8, n: i64) -> i64 { 692 var o: i64 = 0 693 o = pctl_cat(out, o, "{\"verb\":\"progress\",\"target\":\"" as *u8) 694 o = pctl_cat(out, o, host) 695 o = pctl_cat(out, o, "\",\"http_status\":" as *u8) 696 o = pctl_catn(out, o, status) 697 var pct: i64 = 0 698 o = pctl_cat(out, o, ",\"state\":\"" as *u8) 699 if n > 0 { if status >= 200 { if status < 300 { 700 let bs: i64 = pctl_body_start(resp, n) 701 let bp: *u8 = ((resp as i64) + bs) as *u8 702 let bn: i64 = n - bs 703 o = pctl_cat_jsonstr_after(out, o, bp, bn, "\"print_stats\"" as *u8, "\"state\":\"" as *u8) 704 pct = pctl_pct_after(bp, bn, "\"virtual_sdcard\"" as *u8, "\"progress\":" as *u8) 705 } } } 706 o = pctl_cat(out, o, "\",\"filename\":\"" as *u8) 707 if n > 0 { if status >= 200 { if status < 300 { 708 let bs2: i64 = pctl_body_start(resp, n) 709 let bp2: *u8 = ((resp as i64) + bs2) as *u8 710 let bn2: i64 = n - bs2 711 o = pctl_cat_jsonstr_after(out, o, bp2, bn2, "\"print_stats\"" as *u8, "\"filename\":\"" as *u8) 712 } } } 713 o = pctl_cat(out, o, "\",\"progress_pct\":" as *u8) 714 o = pctl_catn(out, o, pct) 715 o = pctl_cat(out, o, ",\"result\":\"" as *u8) 716 o = pctl_cat_netresult(out, o, status) 717 o = pctl_cat(out, o, "\"}\n" as *u8) 718 return o 719} 720 721// ===== printstart: start a print of a NAMED, pre-sliced file ======= 722// Validated filename (safe chars only, via nx_mr_build_print_start_body); we NEVER inject 723// raw G-code -> never-brick holds. Deeper hardening rung: fetch the file + nx_klipper_gcode 724// _validator before start (reject M500/M502/firmware writes). 725func pctl_printstart(out: *u8, ipv4: i64, port: i64, host: *u8, hl: i64, fn: *u8, fnlen: i64) -> i64 { 726 let body: *u8 = sys_mmap(512) 727 let blen: i64 = nx_mr_build_print_start_body(fn, fnlen, body, 512) 728 var o: i64 = 0 729 if blen <= 0 { 730 o = pctl_cat(out, o, "{\"verb\":\"printstart\",\"result\":\"BAD_FILENAME\",\"note\":\"filename must be safe chars a-zA-Z0-9._-/\"}\n" as *u8) 731 return o 732 } 733 let scratch: *u8 = sys_mmap(PCTL_SCRATCH) 734 let resp: *u8 = sys_mmap(PCTL_RESP) 735 let empty: *u8 = "\x00" as *u8 736 let n: i64 = nx_mr_post_ipv4(ipv4, port, host, hl, "/printer/print/start" as *u8, 20, body, blen, empty, 0, scratch, PCTL_SCRATCH, resp, PCTL_RESP) 737 var status: i64 = n 738 if n > 0 { status = nx_mr_parse_status(resp, n) } 739 o = pctl_cat(out, o, "{\"verb\":\"printstart\",\"target\":\"" as *u8) 740 o = pctl_cat(out, o, host) 741 o = pctl_cat(out, o, "\",\"filename\":\"" as *u8) 742 o = pctl_cat(out, o, fn) 743 o = pctl_cat(out, o, "\",\"http_status\":" as *u8) 744 o = pctl_catn(out, o, status) 745 o = pctl_cat(out, o, ",\"result\":\"" as *u8) 746 o = pctl_cat_netresult(out, o, status) 747 o = pctl_cat(out, o, "\"}\n" as *u8) 748 return o 749} 750 751// ===== upload: sovereign multipart POST /server/files/upload ======= 752// Uploads a local G-code file to the printer (stores it; does NOT run it -- printstart 753// runs a named file). Builds the multipart/form-data body ourselves (no 3rd-party HTTP lib). 754func pctl_build_upload_req(req: *u8, host: *u8, filename: *u8, gcode: *u8, gcode_len: i64) -> i64 { 755 let bnd: *u8 = "----nxbprinterctlB0undary" as *u8 756 // part1: --bnd\r\nContent-Disposition...filename\r\nContent-Type...\r\n\r\n 757 let p1: *u8 = sys_mmap(1024) 758 var a: i64 = 0 759 a = pctl_cat(p1, a, "--" as *u8) 760 a = pctl_cat(p1, a, bnd) 761 a = pctl_cat(p1, a, "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"" as *u8) 762 a = pctl_cat(p1, a, filename) 763 a = pctl_cat(p1, a, "\"\r\nContent-Type: application/octet-stream\r\n\r\n" as *u8) 764 // part3: \r\n--bnd--\r\n 765 let p3: *u8 = sys_mmap(64) 766 var b: i64 = 0 767 b = pctl_cat(p3, b, "\r\n--" as *u8) 768 b = pctl_cat(p3, b, bnd) 769 b = pctl_cat(p3, b, "--\r\n" as *u8) 770 let body_len: i64 = a + gcode_len + b 771 // headers 772 var o: i64 = 0 773 o = pctl_cat(req, o, "POST /server/files/upload HTTP/1.1\r\nHost: " as *u8) 774 o = pctl_cat(req, o, host) 775 o = pctl_cat(req, o, "\r\nContent-Type: multipart/form-data; boundary=" as *u8) 776 o = pctl_cat(req, o, bnd) 777 o = pctl_cat(req, o, "\r\nContent-Length: " as *u8) 778 o = pctl_catn(req, o, body_len) 779 o = pctl_cat(req, o, "\r\nConnection: close\r\n\r\n" as *u8) 780 // body: part1 + gcode + part3 781 var i: i64 = 0 782 while i < a { req[o] = p1[i]; o = o + 1; i = i + 1 } 783 i = 0 784 while i < gcode_len { req[o] = gcode[i]; o = o + 1; i = i + 1 } 785 i = 0 786 while i < b { req[o] = p3[i]; o = o + 1; i = i + 1 } 787 return o 788} 789func pctl_upload_send(out: *u8, ipv4: i64, port: i64, host: *u8, filename: *u8, gcode: *u8, gcode_len: i64) -> i64 { 790 let req: *u8 = sys_mmap(gcode_len + 16384) 791 let rlen: i64 = pctl_build_upload_req(req, host, filename, gcode, gcode_len) 792 let fd: i64 = nx_mr_tcp_connect_ipv4(ipv4, port) 793 var status: i64 = 0 - 2 794 var n: i64 = 0 795 if fd >= 0 { 796 var sent: i64 = 0 797 while sent < rlen { 798 let w: i64 = sys_write(fd, ((req as i64) + sent) as *u8, rlen - sent) 799 if w <= 0 { sent = rlen } else { sent = sent + w } 800 } 801 let resp: *u8 = sys_mmap(PCTL_RESP) 802 n = sys_read(fd, resp, PCTL_RESP) 803 sys_close(fd) 804 if n > 0 { status = nx_mr_parse_status(resp, n) } else { status = 0 - 4 } 805 } 806 var o: i64 = 0 807 o = pctl_cat(out, o, "{\"verb\":\"upload\",\"target\":\"" as *u8) 808 o = pctl_cat(out, o, host) 809 o = pctl_cat(out, o, "\",\"filename\":\"" as *u8) 810 o = pctl_cat(out, o, filename) 811 o = pctl_cat(out, o, "\",\"bytes\":" as *u8) 812 o = pctl_catn(out, o, gcode_len) 813 o = pctl_cat(out, o, ",\"http_status\":" as *u8) 814 o = pctl_catn(out, o, status) 815 o = pctl_cat(out, o, ",\"result\":\"" as *u8) 816 o = pctl_cat_netresult(out, o, status) 817 o = pctl_cat(out, o, "\"}\n" as *u8) 818 return o 819} 820 821// ===== the sovereign architecture contract (no network; agent-facing) ===== 822func pctl_emit_contract(out: *u8) -> i64 { 823 var o: i64 = 0 824 o = pctl_cat(out, o, "{\"verb\":\"contract\",\"capability\":\"nx_printer_ctl\",\"maps_operator_design\":\"browser->edge->home-gateway->printer\"," as *u8) 825 o = pctl_cat(out, o, "\"flow\":[" as *u8) 826 o = pctl_cat(out, o, "{\"hop\":1,\"node\":\"browser\",\"presents\":\"opaque X-Nishi-Cap token (cookie/bearer)\"}," as *u8) 827 o = pctl_cat(out, o, "{\"hop\":2,\"node\":\"sovereign edge sites.elf :8443\",\"does\":\"validate opaque cap-token, reverse-proxy /printers route\",\"tls\":\"sovereign TLS-1.3 own trust store\"}," as *u8) 828 o = pctl_cat(out, o, "{\"hop\":3,\"node\":\"nx_printer_ctl organ (on-NAS, on-LAN)\",\"does\":\"cap-gated fixed-endpoint control\",\"auth\":\"least-authority cap\"}," as *u8) 829 o = pctl_cat(out, o, "{\"hop\":4,\"node\":\"QIDI/Moonraker :7125 (Fluidd :10088)\",\"reach\":\"LAN HTTP only\"}]," as *u8) 830 o = pctl_cat(out, o, "\"zero_external_exposure\":\"BY_CONSTRUCTION: NAS edge is on the home LAN; printer never sees WAN; only the cap-gated organ reaches it\"," as *u8) 831 o = pctl_cat(out, o, "\"never_brick\":\"fixed safe-endpoint allowlist (info/emergency_stop/pause/resume/cancel); NO raw-gcode passthrough => no firmware/EEPROM write path; estop = runtime MCU halt, RAM-only\"," as *u8) 832 o = pctl_cat(out, o, "\"safe_endpoints\":[\"/printer/info\",\"/printer/objects/query\",\"/server/files/list\",\"/server/files/upload\",\"/printer/emergency_stop\",\"/printer/print/start\",\"/printer/print/pause\",\"/printer/print/resume\",\"/printer/print/cancel\"]," as *u8) 833 o = pctl_cat(out, o, "\"print_workflow\":\"files->printstart(validated filename, never raw-gcode)->progress; slice via sovereign nx_print_session (mesh->gcode); G-code validated by nx_klipper_gcode_validator\"," as *u8) 834 o = pctl_cat(out, o, "\"no_third_party\":\"sovereign organs only; no Node/Express/nginx/http-proxy-middleware\"}\n" as *u8) 835 return o 836}