pycentauri 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pycentauri-0.3.0 → pycentauri-0.3.1}/CHANGELOG.md +15 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/PKG-INFO +1 -1
- {pycentauri-0.3.0 → pycentauri-0.3.1}/pyproject.toml +1 -1
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/__init__.py +1 -1
- pycentauri-0.3.1/src/pycentauri/web/app.js +282 -0
- pycentauri-0.3.1/src/pycentauri/web/index.html +210 -0
- pycentauri-0.3.1/src/pycentauri/web/styles.css +744 -0
- pycentauri-0.3.0/src/pycentauri/web/app.js +0 -179
- pycentauri-0.3.0/src/pycentauri/web/index.html +0 -85
- pycentauri-0.3.0/src/pycentauri/web/styles.css +0 -257
- {pycentauri-0.3.0 → pycentauri-0.3.1}/.gitignore +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/LICENSE +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/README.md +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/camera.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/cli.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/client.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/discovery.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/mcp/__init__.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/mcp/__main__.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/mcp/server.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/models.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/py.typed +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/sdcp.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/server.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/src/pycentauri/web/__init__.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/tests/__init__.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/tests/integration/__init__.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/tests/test_client.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/tests/test_discovery.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/tests/test_sdcp.py +0 -0
- {pycentauri-0.3.0 → pycentauri-0.3.1}/tests/test_server.py +0 -0
|
@@ -6,6 +6,21 @@ Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.1] - 2026-04-22
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Web UI redesign.** Moved from the generic dark-dashboard look to an
|
|
13
|
+
industrial instrument-panel aesthetic — amber-on-near-black, viewfinder
|
|
14
|
+
corner brackets and a faint crosshair over the webcam, ruler-style
|
|
15
|
+
progress bar with diagonal-hatch fill, tabular-numeric temperature
|
|
16
|
+
gauges with per-channel setpoint bars, kinematics readout with X/Y/Z
|
|
17
|
+
cells and fan/Z-offset/mainboard telemetry, keyboard-key-style F1/F2/F3
|
|
18
|
+
control buttons (with real keyboard shortcuts). Typography: Space Mono
|
|
19
|
+
for display numerals, IBM Plex Mono for data, IBM Plex Sans Condensed
|
|
20
|
+
for SCADA-style labels.
|
|
21
|
+
- Webcam MJPEG element now auto-recovers if the stream stalls for 15s.
|
|
22
|
+
- Added a favicon so browsers stop 404-ing `/favicon.ico`.
|
|
23
|
+
|
|
9
24
|
## [0.3.0] - 2026-04-22
|
|
10
25
|
|
|
11
26
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycentauri
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Python client and MCP server for Elegoo Centauri Carbon 3D printers
|
|
5
5
|
Project-URL: Homepage, https://github.com/bjan/pycentauri
|
|
6
6
|
Project-URL: Repository, https://github.com/bjan/pycentauri
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// pycentauri web console
|
|
2
|
+
// Consumes /api/info, /status, /events/status (SSE), /attributes.
|
|
3
|
+
// Posts to /print/{pause,resume,stop} when --enable-control is on.
|
|
4
|
+
|
|
5
|
+
const $ = (id) => document.getElementById(id);
|
|
6
|
+
|
|
7
|
+
// PrintInfo.Status → display label + semantic class.
|
|
8
|
+
// Codes decoded from CentauriLink + official elegoo-link + live observation.
|
|
9
|
+
const PRINT_STATUS = {
|
|
10
|
+
0: { label: "IDLE", cls: "state-idle" },
|
|
11
|
+
1: { label: "HOMING", cls: "state-printing" },
|
|
12
|
+
2: { label: "DROPPING", cls: "state-printing" },
|
|
13
|
+
3: { label: "EXPOSING", cls: "state-printing" },
|
|
14
|
+
4: { label: "LIFTING", cls: "state-printing" },
|
|
15
|
+
5: { label: "PAUSING", cls: "state-paused" },
|
|
16
|
+
6: { label: "PAUSED", cls: "state-paused" },
|
|
17
|
+
7: { label: "STOPPING", cls: "state-stopped" },
|
|
18
|
+
8: { label: "STOPPED", cls: "state-stopped" },
|
|
19
|
+
9: { label: "COMPLETED", cls: "state-completed" },
|
|
20
|
+
10: { label: "CHECKING", cls: "state-printing" },
|
|
21
|
+
12: { label: "PREPARING", cls: "state-printing" },
|
|
22
|
+
13: { label: "PRINTING", cls: "state-printing" },
|
|
23
|
+
18: { label: "RESUMED", cls: "state-printing" },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function fmtSeconds(s) {
|
|
27
|
+
if (s == null || Number.isNaN(+s)) return "—";
|
|
28
|
+
s = Math.max(0, Math.round(+s));
|
|
29
|
+
const h = Math.floor(s / 3600);
|
|
30
|
+
const m = Math.floor((s % 3600) / 60);
|
|
31
|
+
const sec = s % 60;
|
|
32
|
+
if (h > 0) return `${h}h ${m.toString().padStart(2, "0")}m`;
|
|
33
|
+
if (m > 0) return `${m}m ${sec.toString().padStart(2, "0")}s`;
|
|
34
|
+
return `${sec}s`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fmtTemp(v, padTo = "---.-") {
|
|
38
|
+
if (v == null || Number.isNaN(+v)) return padTo;
|
|
39
|
+
return (+v).toFixed(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fmtCoord(v) {
|
|
43
|
+
if (v == null || Number.isNaN(+v)) return "---.--";
|
|
44
|
+
return (+v).toFixed(2).padStart(6, " ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function tempPct(actual, target) {
|
|
48
|
+
if (actual == null || target == null || target <= 0) return 0;
|
|
49
|
+
return Math.min(100, Math.max(0, (actual / target) * 100));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setStatusPill(kind, label) {
|
|
53
|
+
const pill = $("status-pill");
|
|
54
|
+
pill.classList.remove("warn", "err");
|
|
55
|
+
if (kind === "warn") pill.classList.add("warn");
|
|
56
|
+
if (kind === "err") pill.classList.add("err");
|
|
57
|
+
$("status-text").textContent = label;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
async function loadInfo() {
|
|
63
|
+
try {
|
|
64
|
+
const r = await fetch("/api/info");
|
|
65
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
66
|
+
const info = await r.json();
|
|
67
|
+
|
|
68
|
+
$("version").textContent = info.version || "—";
|
|
69
|
+
$("host").textContent = info.printer_host || "—";
|
|
70
|
+
$("mainboard").textContent = info.mainboard_id || "—";
|
|
71
|
+
$("mainboard").title = info.mainboard_id || "";
|
|
72
|
+
|
|
73
|
+
if (info.enable_control) $("controls").hidden = false;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const attrs = await (await fetch("/attributes")).json();
|
|
77
|
+
$("printer-name").textContent = (attrs.machine_name || attrs.name || "Centauri Carbon").toUpperCase();
|
|
78
|
+
$("firmware").textContent = attrs.firmware_version ? `FW ${attrs.firmware_version}` : "";
|
|
79
|
+
} catch (_) { /* firmware may not push in idle */ }
|
|
80
|
+
|
|
81
|
+
if (info.connected) {
|
|
82
|
+
setStatusPill("ok", "LINK OK");
|
|
83
|
+
} else {
|
|
84
|
+
setStatusPill("warn", "LINK DOWN");
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
setStatusPill("err", "SRV OFFLINE");
|
|
88
|
+
console.warn("loadInfo failed:", e);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderStatus(raw) {
|
|
93
|
+
const pi = raw.PrintInfo || {};
|
|
94
|
+
const pstatus = pi.Status;
|
|
95
|
+
const filename = pi.Filename;
|
|
96
|
+
|
|
97
|
+
// Job name
|
|
98
|
+
$("filename").textContent = filename || "— · AWAITING ·";
|
|
99
|
+
|
|
100
|
+
// Progress
|
|
101
|
+
const progress = pi.Progress;
|
|
102
|
+
const pct = (progress != null) ? Math.max(0, Math.min(100, +progress)) : 0;
|
|
103
|
+
$("progress-bar").style.width = pct + "%";
|
|
104
|
+
$("progress-label").textContent = (progress != null) ? `${pct}%` : "—";
|
|
105
|
+
|
|
106
|
+
// State word + code
|
|
107
|
+
const info = PRINT_STATUS[pstatus];
|
|
108
|
+
const sw = $("print-status");
|
|
109
|
+
const sc = $("print-status-code");
|
|
110
|
+
sw.classList.remove("state-idle", "state-printing", "state-paused", "state-stopped", "state-completed");
|
|
111
|
+
if (info) {
|
|
112
|
+
sw.textContent = info.label;
|
|
113
|
+
sw.classList.add(info.cls);
|
|
114
|
+
} else if (pstatus != null) {
|
|
115
|
+
sw.textContent = `CODE·${pstatus}`;
|
|
116
|
+
} else {
|
|
117
|
+
sw.textContent = "—";
|
|
118
|
+
}
|
|
119
|
+
sc.textContent = (pstatus != null) ? String(pstatus).padStart(2, "0") : "--";
|
|
120
|
+
|
|
121
|
+
// Layer
|
|
122
|
+
$("layer").textContent = (pi.CurrentLayer != null && pi.TotalLayer)
|
|
123
|
+
? `${pi.CurrentLayer} / ${pi.TotalLayer}`
|
|
124
|
+
: "—";
|
|
125
|
+
|
|
126
|
+
// Times (CurrentTicks / TotalTicks are seconds on CC firmware)
|
|
127
|
+
const elapsed = pi.CurrentTicks;
|
|
128
|
+
const total = pi.TotalTicks;
|
|
129
|
+
$("elapsed").textContent = fmtSeconds(elapsed);
|
|
130
|
+
$("remaining").textContent = (total != null && elapsed != null)
|
|
131
|
+
? fmtSeconds(total - elapsed) : "—";
|
|
132
|
+
|
|
133
|
+
// Speed %
|
|
134
|
+
$("speed").textContent = (pi.PrintSpeedPct != null) ? `${pi.PrintSpeedPct}%` : "—";
|
|
135
|
+
|
|
136
|
+
// Temperatures
|
|
137
|
+
const noz = raw.TempOfNozzle, nozT = raw.TempTargetNozzle;
|
|
138
|
+
const bed = raw.TempOfHotbed, bedT = raw.TempTargetHotbed;
|
|
139
|
+
const cham = raw.TempOfBox, chamT = raw.TempTargetBox;
|
|
140
|
+
$("t-nozzle").textContent = fmtTemp(noz);
|
|
141
|
+
$("t-nozzle-target").textContent = (nozT != null) ? String(Math.round(+nozT)) : "---";
|
|
142
|
+
$("t-nozzle-bar").style.width = tempPct(noz, nozT) + "%";
|
|
143
|
+
$("t-bed").textContent = fmtTemp(bed);
|
|
144
|
+
$("t-bed-target").textContent = (bedT != null) ? String(Math.round(+bedT)) : "---";
|
|
145
|
+
$("t-bed-bar").style.width = tempPct(bed, bedT) + "%";
|
|
146
|
+
$("t-chamber").textContent = fmtTemp(cham);
|
|
147
|
+
$("t-chamber-target").textContent = (chamT != null) ? String(Math.round(+chamT)) : "---";
|
|
148
|
+
// Chamber often targets 0; show the actual vs a reference of 50°C so the bar conveys warmth.
|
|
149
|
+
$("t-chamber-bar").style.width = Math.min(100, Math.max(0, (+(cham ?? 0) / 50) * 100)) + "%";
|
|
150
|
+
|
|
151
|
+
// Coords "x,y,z"
|
|
152
|
+
if (typeof raw.CurrenCoord === "string") {
|
|
153
|
+
const [x, y, z] = raw.CurrenCoord.split(",");
|
|
154
|
+
$("pos-x").textContent = fmtCoord(x);
|
|
155
|
+
$("pos-y").textContent = fmtCoord(y);
|
|
156
|
+
$("pos-z").textContent = fmtCoord(z);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Z offset
|
|
160
|
+
if (raw.ZOffset != null) $("z-offset").textContent = (+raw.ZOffset).toFixed(3);
|
|
161
|
+
|
|
162
|
+
// Fans
|
|
163
|
+
const fans = raw.CurrentFanSpeed || {};
|
|
164
|
+
const setFan = (id, key) => {
|
|
165
|
+
const v = fans[key];
|
|
166
|
+
$(id).textContent = (v != null) ? `${v}%` : "—";
|
|
167
|
+
};
|
|
168
|
+
setFan("fan-model", "ModelFan");
|
|
169
|
+
setFan("fan-aux", "AuxiliaryFan");
|
|
170
|
+
setFan("fan-box", "BoxFan");
|
|
171
|
+
|
|
172
|
+
setStatusPill("ok", "LINK OK");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function pollOnce() {
|
|
176
|
+
try {
|
|
177
|
+
const r = await fetch("/status");
|
|
178
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
179
|
+
const body = await r.json();
|
|
180
|
+
if (body.raw) renderStatus(body.raw);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
setStatusPill("warn", "LINK WAIT");
|
|
183
|
+
console.warn("poll failed:", e);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function connectSSE() {
|
|
188
|
+
let src;
|
|
189
|
+
try { src = new EventSource("/events/status"); }
|
|
190
|
+
catch (_) {
|
|
191
|
+
setStatusPill("warn", "SSE N/A");
|
|
192
|
+
setInterval(pollOnce, 3000);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
src.addEventListener("status", (ev) => {
|
|
196
|
+
try { renderStatus(JSON.parse(ev.data)); } catch (_) {}
|
|
197
|
+
});
|
|
198
|
+
src.onerror = () => {
|
|
199
|
+
setStatusPill("warn", "LINK WAIT");
|
|
200
|
+
setTimeout(pollOnce, 2000);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Controls
|
|
206
|
+
|
|
207
|
+
function setMsg(text, kind) {
|
|
208
|
+
const el = $("ctrl-msg");
|
|
209
|
+
el.classList.remove("ok", "warn", "err");
|
|
210
|
+
if (kind) el.classList.add(kind);
|
|
211
|
+
el.textContent = text;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function doAction(label, path, confirmMsg) {
|
|
215
|
+
if (confirmMsg && !confirm(confirmMsg)) return;
|
|
216
|
+
setMsg(`» ${label}…`);
|
|
217
|
+
const btns = document.querySelectorAll(".controls button");
|
|
218
|
+
for (const b of btns) b.disabled = true;
|
|
219
|
+
try {
|
|
220
|
+
const r = await fetch(path, { method: "POST" });
|
|
221
|
+
if (!r.ok) throw new Error(`HTTP ${r.status} — ${await r.text()}`);
|
|
222
|
+
setMsg(`✓ ${label} acknowledged`, "ok");
|
|
223
|
+
} catch (e) {
|
|
224
|
+
setMsg(`✗ ${label}: ${e.message}`, "err");
|
|
225
|
+
} finally {
|
|
226
|
+
setTimeout(() => {
|
|
227
|
+
for (const b of btns) b.disabled = false;
|
|
228
|
+
}, 800);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function wireControls() {
|
|
233
|
+
$("btn-pause")?.addEventListener("click", () =>
|
|
234
|
+
doAction("PAUSE", "/print/pause", "Pause the current print?")
|
|
235
|
+
);
|
|
236
|
+
$("btn-resume")?.addEventListener("click", () =>
|
|
237
|
+
doAction("RESUME", "/print/resume")
|
|
238
|
+
);
|
|
239
|
+
$("btn-stop")?.addEventListener("click", () =>
|
|
240
|
+
doAction("STOP", "/print/stop",
|
|
241
|
+
"Stop the current print?\n\nThis cannot be undone.")
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Keyboard shortcuts when controls are visible.
|
|
245
|
+
document.addEventListener("keydown", (ev) => {
|
|
246
|
+
if ($("controls").hidden) return;
|
|
247
|
+
if (ev.target && /^(INPUT|TEXTAREA|SELECT)$/.test(ev.target.tagName)) return;
|
|
248
|
+
if (ev.key === "F1") { ev.preventDefault(); $("btn-pause").click(); }
|
|
249
|
+
if (ev.key === "F2") { ev.preventDefault(); $("btn-resume").click(); }
|
|
250
|
+
if (ev.key === "F3") { ev.preventDefault(); $("btn-stop").click(); }
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Webcam resilience — if the MJPEG stream stalls, reload it.
|
|
256
|
+
|
|
257
|
+
function wireWebcamKeepalive() {
|
|
258
|
+
const img = $("webcam");
|
|
259
|
+
let lastChange = Date.now();
|
|
260
|
+
let loads = 0;
|
|
261
|
+
img.addEventListener("load", () => { lastChange = Date.now(); loads++; });
|
|
262
|
+
img.addEventListener("error", () => {
|
|
263
|
+
setTimeout(() => { img.src = `/stream?t=${Date.now()}`; }, 2000);
|
|
264
|
+
});
|
|
265
|
+
setInterval(() => {
|
|
266
|
+
if (Date.now() - lastChange > 15000) {
|
|
267
|
+
img.src = `/stream?t=${Date.now()}`;
|
|
268
|
+
lastChange = Date.now();
|
|
269
|
+
}
|
|
270
|
+
}, 5000);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
(async function main() {
|
|
276
|
+
wireControls();
|
|
277
|
+
wireWebcamKeepalive();
|
|
278
|
+
await loadInfo();
|
|
279
|
+
await pollOnce();
|
|
280
|
+
connectSSE();
|
|
281
|
+
setInterval(loadInfo, 60000);
|
|
282
|
+
})();
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<meta name="theme-color" content="#0b0b0d">
|
|
7
|
+
<title>pycentauri · console</title>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans+Condensed:wght@500;600;700&display=swap">
|
|
11
|
+
<link rel="stylesheet" href="styles.css">
|
|
12
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%230b0b0d'/%3E%3Ccircle cx='16' cy='16' r='6' fill='%23ffb347'/%3E%3Ccircle cx='16' cy='16' r='10' fill='none' stroke='%23ffb347' stroke-width='1.5' stroke-dasharray='2 3'/%3E%3C/svg%3E">
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div class="bg-grid" aria-hidden="true"></div>
|
|
16
|
+
<div class="bg-noise" aria-hidden="true"></div>
|
|
17
|
+
|
|
18
|
+
<!-- Upper rail -->
|
|
19
|
+
<nav class="rail rail-top">
|
|
20
|
+
<div class="rail-group">
|
|
21
|
+
<span class="logo">◉ PYCENTAURI</span>
|
|
22
|
+
<span class="rail-sep">/</span>
|
|
23
|
+
<span class="rail-ver">v<span id="version">—</span></span>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="rail-group">
|
|
26
|
+
<span class="rail-dim" id="printer-name">—</span>
|
|
27
|
+
<span class="rail-sep">·</span>
|
|
28
|
+
<span class="rail-dim"><span id="firmware">—</span></span>
|
|
29
|
+
<span class="status-pill" id="status-pill">
|
|
30
|
+
<span class="status-dot"></span>
|
|
31
|
+
<span id="status-text">LINK</span>
|
|
32
|
+
</span>
|
|
33
|
+
</div>
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<main class="console">
|
|
37
|
+
<!-- LEFT column — viewport + progress + controls -->
|
|
38
|
+
<section class="viewport-col">
|
|
39
|
+
|
|
40
|
+
<!-- Webcam viewport hero -->
|
|
41
|
+
<div class="viewport" role="img" aria-label="Live webcam feed">
|
|
42
|
+
<img id="webcam" src="/stream" alt="">
|
|
43
|
+
<div class="viewport-corners" aria-hidden="true">
|
|
44
|
+
<span class="corner tl"></span>
|
|
45
|
+
<span class="corner tr"></span>
|
|
46
|
+
<span class="corner bl"></span>
|
|
47
|
+
<span class="corner br"></span>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="viewport-crosshair" aria-hidden="true">
|
|
50
|
+
<span class="ch ch-h"></span>
|
|
51
|
+
<span class="ch ch-v"></span>
|
|
52
|
+
<span class="ch-dot"></span>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="viewport-scanline" aria-hidden="true"></div>
|
|
55
|
+
<div class="viewport-overlay top">
|
|
56
|
+
<span class="ovl">CAM/01</span>
|
|
57
|
+
<span class="ovl ovl-rec"><span class="rec-dot"></span>LIVE</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="viewport-overlay bot">
|
|
60
|
+
<span class="ovl">MJPEG · :3031</span>
|
|
61
|
+
<a class="snap-btn" href="/snapshot" download="snapshot.jpg">DL · JPG</a>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Job progress strip -->
|
|
66
|
+
<div class="panel job-panel">
|
|
67
|
+
<header class="panel-header">
|
|
68
|
+
<span class="panel-tag">JOB</span>
|
|
69
|
+
<span class="panel-rule"></span>
|
|
70
|
+
<span class="panel-idx">02</span>
|
|
71
|
+
</header>
|
|
72
|
+
<div class="job-name" id="filename">— · AWAITING ·</div>
|
|
73
|
+
<div class="job-track">
|
|
74
|
+
<div class="ruler" aria-hidden="true"></div>
|
|
75
|
+
<div class="bar">
|
|
76
|
+
<div class="bar-fill" id="progress-bar"></div>
|
|
77
|
+
<div class="bar-label">
|
|
78
|
+
<span id="progress-label">0%</span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="ruler" aria-hidden="true"></div>
|
|
82
|
+
</div>
|
|
83
|
+
<dl class="job-stats">
|
|
84
|
+
<div><dt>LAYER</dt><dd id="layer">—</dd></div>
|
|
85
|
+
<div><dt>ELAPSED</dt><dd id="elapsed">—</dd></div>
|
|
86
|
+
<div><dt>ETA</dt><dd id="remaining">—</dd></div>
|
|
87
|
+
<div><dt>SPEED</dt><dd id="speed">—</dd></div>
|
|
88
|
+
</dl>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- Exec / control -->
|
|
92
|
+
<div class="panel controls" id="controls" hidden>
|
|
93
|
+
<header class="panel-header">
|
|
94
|
+
<span class="panel-tag">EXEC</span>
|
|
95
|
+
<span class="panel-rule"></span>
|
|
96
|
+
<span class="panel-idx">03</span>
|
|
97
|
+
</header>
|
|
98
|
+
<div class="btn-grid">
|
|
99
|
+
<button id="btn-pause" class="kbd-btn warn" type="button">
|
|
100
|
+
<span class="kbd-key">F1</span>
|
|
101
|
+
<span class="kbd-label">PAUSE</span>
|
|
102
|
+
</button>
|
|
103
|
+
<button id="btn-resume" class="kbd-btn ok" type="button">
|
|
104
|
+
<span class="kbd-key">F2</span>
|
|
105
|
+
<span class="kbd-label">RESUME</span>
|
|
106
|
+
</button>
|
|
107
|
+
<button id="btn-stop" class="kbd-btn danger" type="button">
|
|
108
|
+
<span class="kbd-key">F3</span>
|
|
109
|
+
<span class="kbd-label">STOP</span>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="ctrl-msg" id="ctrl-msg" aria-live="polite"></div>
|
|
113
|
+
</div>
|
|
114
|
+
</section>
|
|
115
|
+
|
|
116
|
+
<!-- RIGHT column — instruments -->
|
|
117
|
+
<aside class="instruments-col">
|
|
118
|
+
|
|
119
|
+
<!-- State -->
|
|
120
|
+
<div class="panel state-panel">
|
|
121
|
+
<header class="panel-header">
|
|
122
|
+
<span class="panel-tag">STATE</span>
|
|
123
|
+
<span class="panel-rule"></span>
|
|
124
|
+
<span class="panel-idx">01</span>
|
|
125
|
+
</header>
|
|
126
|
+
<div class="state-row">
|
|
127
|
+
<span class="state-code" id="print-status-code">--</span>
|
|
128
|
+
<span class="state-word" id="print-status">IDLE</span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<!-- Thermals -->
|
|
133
|
+
<div class="panel thermal-panel">
|
|
134
|
+
<header class="panel-header">
|
|
135
|
+
<span class="panel-tag">THERMAL</span>
|
|
136
|
+
<span class="panel-rule"></span>
|
|
137
|
+
<span class="panel-idx">04</span>
|
|
138
|
+
</header>
|
|
139
|
+
<div class="gauges">
|
|
140
|
+
<div class="gauge" data-id="nozzle">
|
|
141
|
+
<div class="gauge-head">
|
|
142
|
+
<span class="gauge-label">NZL</span>
|
|
143
|
+
<span class="gauge-ref">T-01</span>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="gauge-val"><span id="t-nozzle">---.-</span><span class="unit">°C</span></div>
|
|
146
|
+
<div class="gauge-target">SETPOINT · <span id="t-nozzle-target">---</span>°</div>
|
|
147
|
+
<div class="gauge-bar"><div class="gauge-fill" id="t-nozzle-bar"></div></div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="gauge" data-id="bed">
|
|
150
|
+
<div class="gauge-head">
|
|
151
|
+
<span class="gauge-label">BED</span>
|
|
152
|
+
<span class="gauge-ref">T-02</span>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="gauge-val"><span id="t-bed">---.-</span><span class="unit">°C</span></div>
|
|
155
|
+
<div class="gauge-target">SETPOINT · <span id="t-bed-target">---</span>°</div>
|
|
156
|
+
<div class="gauge-bar"><div class="gauge-fill" id="t-bed-bar"></div></div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="gauge" data-id="chamber">
|
|
159
|
+
<div class="gauge-head">
|
|
160
|
+
<span class="gauge-label">BOX</span>
|
|
161
|
+
<span class="gauge-ref">T-03</span>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="gauge-val"><span id="t-chamber">---.-</span><span class="unit">°C</span></div>
|
|
164
|
+
<div class="gauge-target">AMBIENT · <span id="t-chamber-target">---</span>°</div>
|
|
165
|
+
<div class="gauge-bar"><div class="gauge-fill" id="t-chamber-bar"></div></div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Kinematics + fans -->
|
|
171
|
+
<div class="panel tele-panel">
|
|
172
|
+
<header class="panel-header">
|
|
173
|
+
<span class="panel-tag">KINEMATICS</span>
|
|
174
|
+
<span class="panel-rule"></span>
|
|
175
|
+
<span class="panel-idx">05</span>
|
|
176
|
+
</header>
|
|
177
|
+
<div class="coords">
|
|
178
|
+
<div class="axis"><span class="axis-lbl">X</span><span class="axis-val" id="pos-x">---.--</span></div>
|
|
179
|
+
<div class="axis"><span class="axis-lbl">Y</span><span class="axis-val" id="pos-y">---.--</span></div>
|
|
180
|
+
<div class="axis"><span class="axis-lbl">Z</span><span class="axis-val" id="pos-z">---.--</span></div>
|
|
181
|
+
</div>
|
|
182
|
+
<dl class="tele-dl">
|
|
183
|
+
<div><dt>Z · OFFSET</dt><dd id="z-offset">—</dd></div>
|
|
184
|
+
<div><dt>FAN · MODEL</dt><dd id="fan-model">—</dd></div>
|
|
185
|
+
<div><dt>FAN · AUX</dt><dd id="fan-aux">—</dd></div>
|
|
186
|
+
<div><dt>FAN · CHAMBER</dt><dd id="fan-box">—</dd></div>
|
|
187
|
+
<div><dt>MAINBOARD</dt><dd class="mono-xs" id="mainboard">—</dd></div>
|
|
188
|
+
</dl>
|
|
189
|
+
</div>
|
|
190
|
+
</aside>
|
|
191
|
+
</main>
|
|
192
|
+
|
|
193
|
+
<!-- Lower rail -->
|
|
194
|
+
<footer class="rail rail-bottom">
|
|
195
|
+
<div class="rail-group">
|
|
196
|
+
<span class="rail-dim">HOST</span>
|
|
197
|
+
<span id="host">—</span>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="rail-group">
|
|
200
|
+
<a href="/docs">API · OPENAPI</a>
|
|
201
|
+
<span class="rail-sep">/</span>
|
|
202
|
+
<a href="/redoc">REDOC</a>
|
|
203
|
+
<span class="rail-sep">/</span>
|
|
204
|
+
<a href="https://github.com/bjan/pycentauri" target="_blank" rel="noopener">SRC</a>
|
|
205
|
+
</div>
|
|
206
|
+
</footer>
|
|
207
|
+
|
|
208
|
+
<script src="app.js"></script>
|
|
209
|
+
</body>
|
|
210
|
+
</html>
|