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}