printhive/test/printerLive.test.js
dlawler489 1aca9a6c01 Show which filament is feeding each printer, with a tube from spool to toolhead
The live connection reads tray_now (unit × 4 + slot, 254 external, 255
none) and works out the unit by id or, failing that, by position — so the
A2L's AMS lite numbered 16 is handled. Cards say 'Using' (or 'Loaded' when
not printing) with the filament's name and slot, mark the slot in the list,
and draw a tube in its colour from that spool to the toolhead, with a
moving flow while printing. The external spool is drawn when it's in use.
Debug reports include tray_now and tray_tar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 00:27:57 +10:00

117 lines
5.3 KiB
JavaScript
Raw Permalink 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('a busy printer’s steady reports don’t hold back a spool sync', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
t.after(() => live.stopAll());
clients.length = 0;
const synced = [];
live.setTraysHandler(async (id) => { synced.push(id); return {}; });
live.reconcile([{ ...printerRow, id: 5, serial: 'SER5' }]);
const c = clients[0];
c.connected = true;
c.emit('connect');
c.emit('message', 't', report({ ams: { ams: [{ id: '0', tray: [tray(0, 'PLA', 'FF0000FF')] }] } }));
for (let i = 0; i < 10; i += 1) {
t.mock.timers.tick(1000);
c.emit('message', 't', report({ mc_percent: i }));
}
assert.equal(synced.length, 1, 'synced once, five seconds after the change');
// The same trays again change nothing.
c.emit('message', 't', report({ ams: { ams: [{ id: '0', tray: [tray(0, 'PLA', 'FF0000FF')] }] } }));
t.mock.timers.tick(6000);
assert.equal(synced.length, 1);
});
test('the tray feeding the toolhead is worked out from tray_now', (t) => {
t.after(() => live.stopAll());
clients.length = 0;
live.setTraysHandler(async () => ({}));
live.reconcile([{ ...printerRow, id: 7, serial: 'SER7' }, { ...printerRow, id: 8, serial: 'SER8' }]);
const [a, b] = clients;
const units = (ids) => ({ ams: ids.map((id) => ({ id: String(id), tray: [tray(0, 'PLA', 'FF0000FF')] })) });
// Two AMS units numbered 0 and 1: tray 6 is the second unit's third slot.
a.emit('message', 't', report({ ams: { ...units([0, 1]), tray_now: '6' } }));
assert.deepEqual(live.snapshot(7).status.active, { unit: 1, slot: 2 });
// A later report changing only tray_now still counts.
a.emit('message', 't', report({ ams: { tray_now: '254' } }));
assert.deepEqual(live.snapshot(7).status.active, { external: true });
a.emit('message', 't', report({ ams: { tray_now: '255' } }));
assert.equal(live.snapshot(7).status.active, null);
// An AMS lite numbered 16 (as on the A2L): tray 1 is its second slot.
b.emit('message', 't', report({ ams: { ...units([16]), tray_now: '1' } }));
assert.deepEqual(live.snapshot(8).status.active, { unit: 16, slot: 1 });
});
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 });
});