code wiki / (root) / nx_monitor_html.nx

nx_monitor_html.nx source

↩ module page · 261 lines · 14722 B

1// nx_monitor_html.nx -- sovereign substrate-emitted browser monitor. 2// 3// Emits a single static HTML+vanilla-JS page that runs in the 4// operator's existing browser (Firefox/Chrome) and connects DIRECTLY 5// to the Qidi's Moonraker WebSocket + camera service. No nginx, no 6// Mainsail, no Fluidd, no Vue/React/Tailwind, no npm dependency -- 7// every byte the browser receives is substrate-emitted text. 8// 9// Per NISHI_3D_PRINT_BULLETPROOF_PREFLIGHT_ROADMAP §2: the operator's 10// browser IS the rendering layer until nx_browser ships native; the 11// SUBSTRATE-side (this primitive, telemetry parsing logic, etc.) is 12// 100% sovereign Nishi. 13// 14// What the emitted page does: 15// 1. WebSocket: ws://<host>:<port>/websocket -> Moonraker 16// subscribe to print_stats + extruder + heater_bed + gcode_move + 17// virtual_sdcard 18// 2. MJPEG: <img src="http://<host>:<cam_port>/?action=stream"> 19// 3. 6 telemetry tiles: state, progress, hotend, bed, Z, ETA 20// 4. 4 buttons: pause / resume / cancel (confirm) / EMERGENCY STOP 21// (M112 via /printer/emergency_stop) 22// 5. Half-print detection: JS polls every 5s, alerts if Z hasn't 23// advanced in 2 min while printing 24// 25// Composes nx_moonraker_client's buffer write helpers (no duplication 26// per [[feedback-symbol-collision-prevention-pre-write-check]]). 27// 28// Reuses existing operator-facing browser (third-party until nx_browser 29// ships) for RENDERING; refuses every other third-party dependency per 30// "no 3rd party bullshit" directive. 31// 32// license_tier: ORIGINAL 33 34import "nx_syscalls.nx" 35import "nx_moonraker_client.nx" 36const NX_MAGIC_7125: i64 = 7125 37const NX_MAGIC_8080: i64 = 8080 38 39// ===== verdicts ==================================================== 40 41const NX_MONITOR_OK: i64 = 0 42const NX_MONITOR_ERR_OVERFLOW: i64 = 1 43const NX_MONITOR_ERR_NULL_INPUT: i64 = 2 44 45func nx_monitor_verdict_name(v: i64) -> *u8 { 46 if v == NX_MONITOR_OK { return "OK" } 47 if v == NX_MONITOR_ERR_OVERFLOW { return "OVERFLOW" } 48 if v == NX_MONITOR_ERR_NULL_INPUT { return "NULL_INPUT" } 49 return "UNKNOWN" 50} 51 52// ===== config struct =============================================== 53 54struct NxMonitorConfig { 55 printer_host: *u8, // e.g., "192.168.1.42" -- emitted into JS literal 56 printer_host_len: i64, 57 printer_port: i64, // NX_MAGIC_7125 typical (Moonraker) 58 camera_port: i64, // NX_MAGIC_8080 typical (mjpg-streamer) 59 api_key: *u8, // optional; 0 if no auth 60 api_key_len: i64, 61} 62 63const NX_MONITOR_CONFIG_BYTES: i64 = 48 64 65// ===== emit ======================================================= 66 67func nx_monitor_html_emit(cfg: *NxMonitorConfig, 68 out: *u8, cap: i64) -> i64 { 69 if (cfg as i64) == 0 { return 0 - NX_MONITOR_ERR_NULL_INPUT } 70 if cfg.printer_host_len <= 0 { return 0 - NX_MONITOR_ERR_NULL_INPUT } 71 var o: i64 = 0 72 73 // HTML head + CSS (single-line dark-mode monospace layout). 74 o = nx_mr_write_cstr(out, o, cap, 75 "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Nishi Print Monitor</title><style>") 76 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 77 o = nx_mr_write_cstr(out, o, cap, 78 "body{font-family:monospace;background:#111;color:#ddd;margin:0;padding:20px}") 79 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 80 o = nx_mr_write_cstr(out, o, cap, 81 ".tile{display:inline-block;background:#222;padding:15px;margin:10px;border-radius:4px;min-width:180px}") 82 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 83 o = nx_mr_write_cstr(out, o, cap, 84 ".tile h3{margin:0 0 10px 0;color:#5a5;font-size:13px}") 85 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 86 o = nx_mr_write_cstr(out, o, cap, 87 ".tile .v{font-size:22px;font-weight:bold}") 88 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 89 o = nx_mr_write_cstr(out, o, cap, 90 ".alert{background:#500;color:#fff;padding:15px;margin:10px;border-radius:4px;display:none}") 91 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 92 o = nx_mr_write_cstr(out, o, cap, 93 ".cam{max-width:640px;border:2px solid #444}") 94 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 95 o = nx_mr_write_cstr(out, o, cap, 96 "button{background:#555;color:#fff;border:0;padding:12px 24px;margin:5px;cursor:pointer;font-size:14px}") 97 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 98 o = nx_mr_write_cstr(out, o, cap, 99 "button.danger{background:#a00}</style></head><body>") 100 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 101 102 // Body: title + alert + 6 tiles 103 o = nx_mr_write_cstr(out, o, cap, 104 "<h1>Nishi Print Monitor</h1><div id=\"alert\" class=\"alert\"></div><div>") 105 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 106 o = nx_mr_write_cstr(out, o, cap, 107 "<div class=\"tile\"><h3>STATE</h3><div class=\"v\" id=\"state\">--</div></div>") 108 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 109 o = nx_mr_write_cstr(out, o, cap, 110 "<div class=\"tile\"><h3>PROGRESS</h3><div class=\"v\" id=\"progress\">--</div></div>") 111 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 112 o = nx_mr_write_cstr(out, o, cap, 113 "<div class=\"tile\"><h3>HOTEND</h3><div class=\"v\" id=\"hotend\">--</div></div>") 114 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 115 o = nx_mr_write_cstr(out, o, cap, 116 "<div class=\"tile\"><h3>BED</h3><div class=\"v\" id=\"bed\">--</div></div>") 117 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 118 o = nx_mr_write_cstr(out, o, cap, 119 "<div class=\"tile\"><h3>Z</h3><div class=\"v\" id=\"zpos\">--</div></div>") 120 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 121 o = nx_mr_write_cstr(out, o, cap, 122 "<div class=\"tile\"><h3>ETA</h3><div class=\"v\" id=\"eta\">--</div></div></div>") 123 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 124 125 // Camera <img>: src="http://<host>:<cam_port>/?action=stream" 126 o = nx_mr_write_cstr(out, o, cap, 127 "<div><img class=\"cam\" src=\"http://") 128 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 129 o = nx_mr_write_bytes(out, o, cap, cfg.printer_host, cfg.printer_host_len) 130 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 131 o = nx_mr_write_cstr(out, o, cap, ":") 132 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 133 o = nx_mr_write_int(out, o, cap, cfg.camera_port) 134 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 135 o = nx_mr_write_cstr(out, o, cap, 136 "/?action=stream\" alt=\"camera\"></div>") 137 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 138 139 // Buttons 140 o = nx_mr_write_cstr(out, o, cap, 141 "<div><button onclick=\"pause()\">Pause</button>") 142 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 143 o = nx_mr_write_cstr(out, o, cap, 144 "<button onclick=\"resume()\">Resume</button>") 145 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 146 o = nx_mr_write_cstr(out, o, cap, 147 "<button onclick=\"cancelPrint()\" class=\"danger\">Cancel</button>") 148 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 149 o = nx_mr_write_cstr(out, o, cap, 150 "<button onclick=\"estop()\" class=\"danger\">EMERGENCY STOP (M112)</button></div>") 151 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 152 153 // Script: HOST/PORT/API_KEY constants + WebSocket logic + buttons 154 o = nx_mr_write_cstr(out, o, cap, "<script>const HOST=\"") 155 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 156 o = nx_mr_write_bytes(out, o, cap, cfg.printer_host, cfg.printer_host_len) 157 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 158 o = nx_mr_write_cstr(out, o, cap, "\";const PORT=") 159 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 160 o = nx_mr_write_int(out, o, cap, cfg.printer_port) 161 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 162 o = nx_mr_write_cstr(out, o, cap, ";const API_KEY=\"") 163 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 164 if cfg.api_key_len > 0 { 165 o = nx_mr_write_bytes(out, o, cap, cfg.api_key, cfg.api_key_len) 166 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 167 } 168 o = nx_mr_write_cstr(out, o, cap, "\";") 169 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 170 171 // WebSocket connect + subscribe + extended detection state. 172 // Tracked: lastZ + lastZAt, hotend/bed drift onsets, 173 // progress stall, extruder E stall, current state. 174 o = nx_mr_write_cstr(out, o, cap, 175 "let ws=null,lastZ=0,lastZAt=Date.now(),lastHotendDriftAt=0,lastBedDriftAt=0,lastProgress=-1,lastProgressAt=Date.now(),lastE=0,lastEAt=Date.now(),curState=\"\";") 176 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 177 o = nx_mr_write_cstr(out, o, cap, 178 "function connect(){ws=new WebSocket(\"ws://\"+HOST+\":\"+PORT+\"/websocket\");") 179 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 180 o = nx_mr_write_cstr(out, o, cap, 181 "ws.onopen=()=>ws.send(JSON.stringify({jsonrpc:\"2.0\",method:\"printer.objects.subscribe\",params:{objects:{print_stats:null,extruder:null,heater_bed:null,gcode_move:null,virtual_sdcard:null}},id:1}));") 182 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 183 o = nx_mr_write_cstr(out, o, cap, 184 "ws.onmessage=(m)=>{const d=JSON.parse(m.data);if(d.method==\"notify_status_update\"&&d.params)handleStatus(d.params[0]);else if(d.result&&d.result.status)handleStatus(d.result.status);};") 185 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 186 o = nx_mr_write_cstr(out, o, cap, 187 "ws.onclose=()=>setTimeout(connect,2000);}") 188 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 189 190 // Telemetry handler with extended detection state tracking. 191 o = nx_mr_write_cstr(out, o, cap, 192 "function handleStatus(s){if(s.print_stats){document.getElementById(\"state\").textContent=s.print_stats.state||\"--\";curState=s.print_stats.state||curState;}") 193 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 194 o = nx_mr_write_cstr(out, o, cap, 195 "if(s.virtual_sdcard&&s.virtual_sdcard.progress!==undefined){const p=Math.round(s.virtual_sdcard.progress*1000)/10;document.getElementById(\"progress\").textContent=p+\"%\";if(p!=lastProgress){lastProgress=p;lastProgressAt=Date.now();}}") 196 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 197 // Hotend with drift tracking (>10°C off target = drift) 198 o = nx_mr_write_cstr(out, o, cap, 199 "if(s.extruder&&s.extruder.temperature!==undefined){const t=s.extruder.temperature,tg=s.extruder.target||0;document.getElementById(\"hotend\").textContent=Math.round(t)+\"/\"+Math.round(tg)+\"\\xB0C\";") 200 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 201 o = nx_mr_write_cstr(out, o, cap, 202 "if(tg>0&&Math.abs(t-tg)>10){if(lastHotendDriftAt==0)lastHotendDriftAt=Date.now();}else lastHotendDriftAt=0;}") 203 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 204 // Bed with drift tracking (>5°C off target = drift) 205 o = nx_mr_write_cstr(out, o, cap, 206 "if(s.heater_bed&&s.heater_bed.temperature!==undefined){const t=s.heater_bed.temperature,tg=s.heater_bed.target||0;document.getElementById(\"bed\").textContent=Math.round(t)+\"/\"+Math.round(tg)+\"\\xB0C\";") 207 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 208 o = nx_mr_write_cstr(out, o, cap, 209 "if(tg>0&&Math.abs(t-tg)>5){if(lastBedDriftAt==0)lastBedDriftAt=Date.now();}else lastBedDriftAt=0;}") 210 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 211 // Z + E position tracking from gcode_position [X, Y, Z, E] 212 o = nx_mr_write_cstr(out, o, cap, 213 "if(s.gcode_move&&s.gcode_move.gcode_position){const z=s.gcode_move.gcode_position[2];const e=s.gcode_move.gcode_position[3]||0;document.getElementById(\"zpos\").textContent=z.toFixed(3)+\"mm\";if(z!==lastZ){lastZ=z;lastZAt=Date.now();}if(e!==lastE){lastE=e;lastEAt=Date.now();}}") 214 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 215 o = nx_mr_write_cstr(out, o, cap, 216 "if(s.print_stats&&s.print_stats.print_duration!==undefined){const d=s.print_stats.print_duration;const m=Math.floor(d/60);document.getElementById(\"eta\").textContent=m+\"min\";}}") 217 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 218 219 // Control endpoints + half-print detection 220 o = nx_mr_write_cstr(out, o, cap, 221 "function api(path){const h=API_KEY?{\"X-Api-Key\":API_KEY}:{};fetch(\"http://\"+HOST+\":\"+PORT+path,{method:\"POST\",headers:h});}") 222 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 223 o = nx_mr_write_cstr(out, o, cap, 224 "function pause(){api(\"/printer/print/pause\");}function resume(){api(\"/printer/print/resume\");}") 225 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 226 o = nx_mr_write_cstr(out, o, cap, 227 "function cancelPrint(){if(confirm(\"Cancel print?\"))api(\"/printer/print/cancel\");}") 228 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 229 o = nx_mr_write_cstr(out, o, cap, 230 "function estop(){if(confirm(\"EMERGENCY STOP -- halt motors + heaters?\"))api(\"/printer/emergency_stop\");}") 231 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 232 // Multi-mode half-print detector (5 failure signatures). 233 o = nx_mr_write_cstr(out, o, cap, 234 "function showAlert(m){const a=document.getElementById(\"alert\");a.textContent=m;a.style.display=\"block\";}") 235 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 236 o = nx_mr_write_cstr(out, o, cap, 237 "setInterval(()=>{let msg=\"\";") 238 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 239 o = nx_mr_write_cstr(out, o, cap, 240 "if(Date.now()-lastZAt>120000&&lastZ>0)msg=\"Z position stalled 2+ min -- consider abort\";") 241 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 242 o = nx_mr_write_cstr(out, o, cap, 243 "else if(lastHotendDriftAt>0&&Date.now()-lastHotendDriftAt>60000)msg=\"Hotend drift >10\\xB0C for 60s+ -- check thermistor / flow\";") 244 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 245 o = nx_mr_write_cstr(out, o, cap, 246 "else if(lastBedDriftAt>0&&Date.now()-lastBedDriftAt>120000)msg=\"Bed drift >5\\xB0C for 2 min -- check power / draft\";") 247 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 248 o = nx_mr_write_cstr(out, o, cap, 249 "else if(curState==\"printing\"&&Date.now()-lastProgressAt>300000)msg=\"Progress stalled 5 min while printing -- check for stall\";") 250 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 251 o = nx_mr_write_cstr(out, o, cap, 252 "else if(curState==\"printing\"&&Date.now()-lastEAt>120000)msg=\"Extruder E stalled 2 min while printing -- possible clog / runout\";") 253 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 254 o = nx_mr_write_cstr(out, o, cap, 255 "if(msg)showAlert(msg);else document.getElementById(\"alert\").style.display=\"none\";},5000);") 256 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 257 o = nx_mr_write_cstr(out, o, cap, "connect();</script></body></html>\n") 258 if o < 0 { return 0 - NX_MONITOR_ERR_OVERFLOW } 259 260 return o 261}