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>
75 lines
3.2 KiB
JavaScript
75 lines
3.2 KiB
JavaScript
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 });
|
||
});
|