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>
117 lines
5.3 KiB
JavaScript
117 lines
5.3 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('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 });
|
||
});
|