Created
November 15, 2025 20:05
-
-
Save wd5gnr/6ebe8becf6cec4b83195a8b330c2abd8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <!-- hint: Put this in a folder, edit the IP address and port | |
| variable MOON and just above there, the webcam URL | |
| Then in the same folder run (if you want to use 8085 as the port #): | |
| python3 -m http.server 8085 | |
| Then in Orca, set your device tab to http://127.0.0.1:8085/mini-dash.html | |
| Easy to launch this with systemd, in your profile, whatever | |
| --> | |
| <meta charset="UTF-8"> | |
| <title>AD5X Enhanced Dashboard</title> | |
| <style> | |
| body { | |
| font-family: system-ui, sans-serif; | |
| background: #1d1d1d; | |
| color: #f0f0f0; | |
| margin: 0; | |
| padding: 12px; | |
| } | |
| h1 { margin: 0 0 12px 0; font-size: 1.3rem; } | |
| .grid { display: grid; grid-template-columns: 1.2fr 1fr; gap: 12px; } | |
| .card { | |
| background: #262626; | |
| border-radius: 8px; | |
| padding: 10px 12px; | |
| box-shadow: 0 0 6px rgba(0,0,0,0.5); | |
| } | |
| .card h2 { | |
| margin: 0 0 8px 0; | |
| font-size: 1.05rem; | |
| border-bottom: 1px solid #444; | |
| padding-bottom: 4px; | |
| } | |
| .row { display: flex; justify-content: space-between; padding: 2px 0; } | |
| .label { color: #aaa; } | |
| .value { font-weight: 600; } | |
| .small { font-size: 0.85rem; } | |
| #webcam { | |
| width: 100%; | |
| max-height: 100%; | |
| object-fit: contain; | |
| background: #000; | |
| border-radius: 6px; | |
| } | |
| .bar-container { | |
| background: #333; | |
| border-radius: 4px; | |
| height: 10px; | |
| margin-top: 4px; | |
| overflow: hidden; | |
| } | |
| .bar { | |
| height: 100%; | |
| background: #4aa3ff; | |
| width: 0%; | |
| } | |
| /* Graph styles */ | |
| .graph { | |
| width: 100%; | |
| height: 120px; | |
| background: #111; | |
| position: relative; | |
| border-radius: 6px; | |
| margin-top: 8px; | |
| } | |
| .graph svg { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .axis-label { | |
| fill: #aaa; | |
| font-size: 10px; | |
| } | |
| #console { | |
| background: #111; | |
| border: 1px solid #333; | |
| padding: 6px; | |
| height: 150px; | |
| overflow-y: auto; | |
| font-size: 0.85rem; | |
| white-space: pre-wrap; | |
| border-radius: 6px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>AD5X Enhanced Dashboard</h1> | |
| <div class="grid"> | |
| <div> | |
| <div class="card"> | |
| <h2>Printer State</h2> | |
| <div class="row"><span class="label">State</span><span class="value" id="state-text">—</span></div> | |
| <div class="row"><span class="label">Flags</span><span class="value small" id="state-flags">—</span></div> | |
| <div class="row"><span class="label">ZMod</span><span class="value small" id="state-zmod">—</span></div> | |
| </div> | |
| <div class="card"> | |
| <h2>Temperatures & System</h2> | |
| <div class="row"><span class="label">Nozzle</span><span class="value" id="temp-nozzle">—</span></div> | |
| <div id="noz-graph" class="graph"></div> | |
| <div class="row"><span class="label">Bed</span><span class="value" id="temp-bed">—</span></div> | |
| <div id="bed-graph" class="graph"></div> | |
| <div class="row"><span class="label">CPU Load</span><span class="value small" id="cpu-load-text">—</span></div> | |
| <div id="cpu-graph" class="graph"></div> | |
| <div class="row"><span class="label">Memory Free</span><span class="value small" id="mem-free">—</span></div> | |
| </div> | |
| <div class="card"> | |
| <h2>Job</h2> | |
| <div class="row"><span class="label">File</span><span class="value small" id="job-file">—</span></div> | |
| <div class="row"><span class="label">State</span><span class="value" id="job-state">—</span></div> | |
| <div class="row"><span class="label">Progress</span><span class="value" id="job-progress">—</span></div> | |
| <div class="bar-container"><div class="bar" id="job-bar"></div></div> | |
| <div class="row"><span class="label">Elapsed</span><span class="value small" id="job-printtime">—</span></div> | |
| <div class="row"><span class="label">Remaining</span><span class="value small" id="job-remaining">—</span></div> | |
| <div class="row"><span class="label">Layers</span><span class="value small" id="job-layers">—</span></div> | |
| <div class="bar-container"><div class="bar" id="layer-bar"></div></div> | |
| <div class="row"><span class="label">Filament</span><span class="value small" id="job-filament">—</span></div> | |
| <div class="row"><span class="label">Feedrate</span><span class="value small" id="job-feed">—</span></div> | |
| <div class="row"><span class="label">Speed Factor</span><span class="value small" id="job-speedfactor">—</span></div> | |
| </div> | |
| <div class="card"> | |
| <h2>Toolhead</h2> | |
| <div class="row"><span class="label">XYZ</span><span class="value small" id="toolhead-pos">—</span></div> | |
| <div class="row"><span class="label">Homed</span><span class="value" id="toolhead-homed">—</span></div> | |
| </div> | |
| <div class="card"> | |
| <h2>Messages</h2> | |
| <div id="console"></div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>Webcam</h2> | |
| <img id="webcam" src="http://192.168.1.70:8080/?action=stream"> | |
| </div> | |
| </div> | |
| <script> | |
| const MOON = "http://192.168.1.70:7125"; | |
| function $(id){return document.getElementById(id);} | |
| function fmtTime(sec){ | |
| if(!sec || sec<=0) return "—"; | |
| sec = Math.floor(sec); | |
| const h=Math.floor(sec/3600), m=Math.floor((sec%3600)/60), s=sec%60; | |
| return h>0 ? `${h}h ${m}m ${s}s` : `${m}m ${s}s`; | |
| } | |
| async function fetchJSON(url){ | |
| const r=await fetch(url,{cache:"no-store"}); | |
| if(!r.ok) throw new Error(); | |
| return r.json(); | |
| } | |
| /* -------------------- GRAPHS -------------------- */ | |
| let nozData=[], bedData=[], cpuData=[]; | |
| const graphLimit=60; | |
| function addGraphPoint(arr,v){ | |
| arr.push(v); | |
| if(arr.length>graphLimit) arr.shift(); | |
| } | |
| function drawGraph(arr, elem, labelMax="°C", labelMin="°C"){ | |
| const w = elem.clientWidth; | |
| const h = elem.clientHeight; | |
| if(arr.length<2){ elem.innerHTML=""; return; } | |
| const max = Math.max(...arr); | |
| const min = Math.min(...arr); | |
| const range = Math.max(max-min,1); | |
| const pts = arr.map((v,i)=>[ | |
| (i/(arr.length-1))*w, | |
| h - ((v-min)/range)*h | |
| ]); | |
| elem.innerHTML = ` | |
| <svg> | |
| <text x="4" y="12" class="axis-label">${max.toFixed(0)}${labelMax}</text> | |
| <text x="4" y="${h-4}" class="axis-label">${min.toFixed(0)}${labelMin}</text> | |
| <text x="${w-40}" y="${h-4}" class="axis-label">60s</text> | |
| <polyline fill="none" stroke="#4aa3ff" stroke-width="2" | |
| points="${pts.map(p=>p.join(',')).join(' ')}" /> | |
| </svg>`; | |
| } | |
| /* -------------------- STATE -------------------- */ | |
| async function updateState() { | |
| try { | |
| // Printer info (basic) | |
| const js = await fetchJSON(`${MOON}/printer/info`); | |
| const r = js.result || {}; | |
| // Print stats (from previous update) | |
| const ps = window._latestPrintStats || null; | |
| // STATE text | |
| if (ps && ps.state) { | |
| $("state-text").textContent = ps.state; | |
| } else { | |
| $("state-text").textContent = r.state || "—"; | |
| } | |
| // FLAGS | |
| const flags = []; | |
| if (ps) { | |
| if (ps.state === "printing") flags.push("printing"); | |
| if (ps.state === "paused") flags.push("paused"); | |
| if (ps.state === "ready") flags.push("ready"); | |
| } | |
| if (r.klippy_connected !== false) flags.push("connected"); | |
| $("state-flags").textContent = flags.length ? flags.join(", ") : "—"; | |
| // ZMod version | |
| const js1 = await fetchJSON(`${MOON}/machine/system_info`); | |
| const d1 = js1.result.system_info.distribution; | |
| $("state-zmod").textContent = | |
| d1 ? `${d1.name} (${d1.codename || "-"})` : "—"; | |
| } catch (e) { | |
| $("state-text").textContent = "-"; | |
| $("state-flags").textContent = "-"; | |
| $("state-zmod").textContent = "-"; | |
| } | |
| } | |
| /* -------------------- TEMPS & CPU -------------------- */ | |
| async function updateTemps(){ | |
| try{ | |
| const js = await fetchJSON(`${MOON}/printer/objects/query?extruder&heater_bed&system_stats`); | |
| const st = js.result.status; | |
| /* Temps */ | |
| const noz=st.extruder.temperature; | |
| const nozT=st.extruder.target; | |
| const bed=st.heater_bed.temperature; | |
| const bedT=st.heater_bed.target; | |
| $("temp-nozzle").textContent = `${noz.toFixed(1)}°C (${nozT}°)`; | |
| $("temp-bed").textContent = `${bed.toFixed(1)}°C (${bedT}°)`; | |
| addGraphPoint(nozData,noz); | |
| addGraphPoint(bedData,bed); | |
| drawGraph(nozData,$("noz-graph")); | |
| drawGraph(bedData,$("bed-graph")); | |
| /* CPU Load */ | |
| const load = st.system_stats.sysload || 0; | |
| $("cpu-load-text").textContent = load.toFixed(2); | |
| addGraphPoint(cpuData,load); | |
| drawGraph(cpuData,$("cpu-graph"),"", ""); | |
| /* Memory */ | |
| const avail = st.system_stats.memavail || 0; | |
| $("mem-free").textContent = (avail/1024).toFixed(0)+" MB"; | |
| }catch(e){} | |
| } | |
| /* -------------------- JOB -------------------- */ | |
| async function updateJob(){ | |
| try{ | |
| const js = await fetchJSON(`${MOON}/printer/objects/query?print_stats&display_status&virtual_sdcard&gcode_move&toolhead`); | |
| const st = js.result.status; | |
| const ps = st.print_stats; | |
| window._latestPrintStats=ps; | |
| const ds = st.display_status; | |
| const vsd = st.virtual_sdcard; | |
| const gm = st.gcode_move; | |
| const th = st.toolhead; | |
| const info = ps.info||{}; | |
| const fname = ps.filename || (vsd.file_path? vsd.file_path.split("/").pop():"—"); | |
| $("job-file").textContent = fname; | |
| $("job-state").textContent = ps.state||"—"; | |
| let pct = null; | |
| if(ds.progress!=null) pct = ds.progress*100; | |
| else if(vsd.progress!=null) pct = vsd.progress*100; | |
| $("job-progress").textContent = pct!=null? pct.toFixed(1)+"%":"—"; | |
| if(pct!=null) $("job-bar").style.width = pct+"%"; | |
| $("job-printtime").textContent = fmtTime(ps.print_duration); | |
| let remaining = null; | |
| if(pct>0) remaining = ps.print_duration*(100/pct - 1); | |
| $("job-remaining").textContent = remaining? fmtTime(remaining):"—"; | |
| const cl=info.current_layer, tl=info.total_layer; | |
| if(cl!=null && tl!=null){ | |
| $("job-layers").textContent = `${cl} / ${tl}`; | |
| $("layer-bar").style.width = (cl/tl)*100+"%"; | |
| }else $("job-layers").textContent="—"; | |
| $("job-filament").textContent = | |
| ps.filament_used? ps.filament_used.toFixed(1)+"mm":"—"; | |
| $("job-feed").textContent = | |
| gm.speed ? (gm.speed/60).toFixed(1)+" mm/s" : "—"; | |
| $("job-speedfactor").textContent = | |
| gm.speed_factor? gm.speed_factor.toFixed(2)+"×":"—"; | |
| const pos = th.position||[]; | |
| $("toolhead-pos").textContent = | |
| pos.length>=3?`${pos[0].toFixed(2)}, ${pos[1].toFixed(2)}, ${pos[2].toFixed(2)}`:"—"; | |
| $("toolhead-homed").textContent = th.homed_axes||"—"; | |
| }catch(e){} | |
| } | |
| /* -------------------- CONSOLE -------------------- */ | |
| let lastMessage=""; | |
| async function updateConsole(){ | |
| try{ | |
| const js = await fetchJSON(`${MOON}/printer/objects/query?display_status`); | |
| const msg = js.result.status.display_status.message; | |
| if(msg && msg!==lastMessage){ | |
| lastMessage=msg; | |
| const c=$("console"); | |
| c.textContent+=msg+"\n"; | |
| c.scrollTop=c.scrollHeight; | |
| } | |
| }catch(e){} | |
| } | |
| /* -------------------- LOOP -------------------- */ | |
| setInterval(updateState, 3000); | |
| setInterval(updateTemps, 1000); | |
| setInterval(updateJob, 1200); | |
| setInterval(updateConsole, 1500); | |
| </script> | |
| </body> | |
| </html> | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment