printhive/test/printerLive.test.js
dlawler489 13b5e550a3 Show live printer status over a standing read-only connection
Each printer now has one MQTT connection that stays open, merging the
partial reports printers send while printing. Cards poll every 3 seconds
(paused while the tab is hidden) and show state, file, progress with time
left and finish time, layer, nozzle/bed/chamber temperatures, speed
setting, and print error and HMS codes. A spool change syncs the Filament
page and the card's drawing within seconds; a full report is requested
every 10 minutes as a backstop, replacing the 10-minute probe loop.

"Test & sync" uses the open connection rather than making a second one
(?debug=1 still makes a short separate one). Connections are reconciled
when printers are added, edited or removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 21:30:13 +10:00

75 lines
3.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const test = require('node:test');
const assert = require('node:assert/strict');
const { EventEmitter } = require('node:events');
// A stand-in for the MQTT client, so no network is involved.
const clients = [];
const live = require('../lib/printerLive');
live.setConnector((url, options) => {
const c = new EventEmitter();
Object.assign(c, {
url, options, connected: false, reconnecting: false, published: [], ended: false,
subscribe: (_topic, cb) => cb(null),
publish: (topic, body) => c.published.push({ topic, body: JSON.parse(body) }),
end: () => { c.ended = true; },
reconnect: () => {},
});
clients.push(c);
return c;
});
const report = (print) => Buffer.from(JSON.stringify({ print }));
const printerRow = { id: 1, host: '10.0.0.5', serial: 'SER1', access_code: 'code1' };
const tray = (id, type, colour) => ({ id: String(id), tray_type: type, tray_color: colour, tray_uuid: type ? `U${id}` : '0' });
test('live status merges partial reports, and a tray change syncs spools once', async (t) => {
t.after(() => live.stopAll());
const synced = [];
live.setTraysHandler(async (id, trays) => { synced.push({ id, trays }); return { updated: trays.length }; });
live.reconcile([printerRow, { id: 2, host: '', serial: 'X', access_code: 'y' }]);
assert.equal(clients.length, 1, 'a printer missing its address isn’t connected');
const c = clients[0];
assert.equal(c.options.password, 'code1');
c.connected = true;
c.emit('connect');
assert.equal(c.published[0].topic, 'device/SER1/request');
assert.equal(c.published[0].body.pushing.command, 'pushall');
c.emit('message', 'device/SER1/report', report({
gcode_state: 'RUNNING', mc_percent: 10, nozzle_temper: 220,
ams: { ams: [{ id: '0', tray: [tray(0, 'PLA', 'FF0000FF'), tray(1, '', '')] }] },
}));
c.emit('message', 'device/SER1/report', report({ mc_percent: 11 }));
const snap = live.snapshot(1);
assert.equal(snap.connected, true);
assert.equal(snap.status.state, 'RUNNING');
assert.equal(snap.status.progress, 11);
assert.equal(snap.status.nozzle, 220, 'kept from the earlier report');
assert.deepEqual(snap.loaded.map((l) => [l.slot, l.empty, l.colour, l.tagged]),
[[0, false, '#FF0000', true], [1, true, null, false]]);
assert.equal(JSON.stringify(snap).includes('code1'), false, 'no access code in a snapshot');
// "Test & sync" asks again and syncs straight away.
const pending = live.refresh(1, 3000);
c.emit('message', 'device/SER1/report', report({ ams: { ams: [{ id: '0', tray: [tray(0, 'PLA', 'FF0000FF'), tray(1, '', '')] }] } }));
const result = await pending;
assert.equal(result.connected, true);
assert.equal(result.sync.updated, 2);
assert.equal(synced.length, 1);
assert.equal(c.published.at(-1).body.pushing.command, 'pushall');
});
test('changing a printer’s code reopens its connection; removing it closes it', () => {
clients.length = 0;
live.reconcile([printerRow]);
live.reconcile([printerRow]);
assert.equal(clients.length, 1, 'no change, no new connection');
live.reconcile([{ ...printerRow, access_code: 'code2' }]);
assert.equal(clients.length, 2);
assert.equal(clients[0].ended, true);
live.reconcile([]);
assert.equal(clients[1].ended, true);
assert.deepEqual(live.snapshot(1), { live: false });
});