printhive/test/printer.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

94 lines
4.6 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 printer = require('../lib/printer');
test('AMS trays are read into material, colour and how much is left', () => {
const trays = printer.describeAms([{ id: '0', tray: [
{ id: '0', tray_type: 'PLA', tray_color: 'FFFFFFFF', tray_info_idx: 'GFA00', tray_sub_brands: 'PLA Basic', remain: 82 },
{ id: '1', tray_type: '', tray_color: '00000000' }, // empty slot
{ id: '2', tray_type: 'PETG', tray_color: '000000FF', remain: -1 }, // not a Bambu spool
] }]);
assert.deepEqual(trays[0], {
unit: 0, slot: 0, empty: false, material: 'PLA', colour: '#FFFFFF',
profile: 'GFA00', brand: 'PLA Basic', remain: 82, uuid: null, tagUid: null, idName: null,
weight: null, type: 'Basic',
});
assert.equal(trays[1].empty, true);
assert.equal(trays[1].material, null);
assert.equal(trays[1].colour, null, 'an empty slot has no colour, whatever the printer says');
assert.equal(trays[2].remain, null, '-1 means the printer can’t tell');
assert.equal(trays[2].colour, '#000000', 'the alpha byte is dropped');
});
test('a Bambu spool’s RFID identity is kept, and an unread tag is not invented', () => {
const [tagged] = printer.describeAms([{ id: '0', tray: [{
id: '0', tray_type: 'PLA', tray_color: 'C12E1FFF',
tray_uuid: 'A1B2C3D4E5F60718293A4B5C6D7E8F90', tag_uid: '04AB12CD34EF5680', tray_id_name: 'A00-R0',
}] }]);
assert.equal(tagged.uuid, 'A1B2C3D4E5F60718293A4B5C6D7E8F90');
assert.equal(tagged.tagUid, '04AB12CD34EF5680');
assert.equal(tagged.idName, 'A00-R0');
const [plain] = printer.describeAms([{ id: '0', tray: [{
id: '0', tray_type: 'PLA', tray_color: 'FFFFFFFF',
tray_uuid: '00000000000000000000000000000000', tag_uid: '0000000000000000',
}] }]);
assert.equal(plain.uuid, null, 'all zeros means no tag was read');
assert.equal(plain.tagUid, null);
assert.equal(plain.idName, null);
});
test('a printer with no AMS reports nothing rather than failing', () => {
assert.deepEqual(printer.describeAms(undefined), []);
assert.deepEqual(printer.describeAms([{ id: '0' }]), []);
});
test('the external spool holder is read the same way as a tray', () => {
const ext = printer.describeTray({ id: '254', tray_type: 'PLA', tray_color: 'C12E1FFF', remain: 40 });
assert.equal(ext.unit, null);
assert.equal(ext.material, 'PLA');
assert.equal(ext.colour, '#C12E1F');
assert.equal(ext.remain, 40);
});
test('connection failures are explained in plain words', () => {
assert.equal(printer.friendlyError(new Error('Connection refused: Not authorized')),
'The printer refused the access code.');
assert.equal(printer.friendlyError(new Error('connect ECONNREFUSED 192.0.2.10:8883')),
'Nothing answered on port 8883 at that address.');
assert.equal(printer.friendlyError(new Error('connect EHOSTUNREACH 192.0.2.10:8883')),
'Couldn’t reach the printer from the server.');
assert.equal(printer.friendlyError(null), 'The connection failed.');
});
test('the product line is what follows the material in the brand', () => {
assert.equal(printer.productLine('PLA Basic', 'PLA'), 'Basic');
assert.equal(printer.productLine('PLA Silk+', 'PLA'), 'Silk+');
assert.equal(printer.productLine('PETG HF', 'PETG'), 'HF');
assert.equal(printer.productLine('Generic', 'PLA'), 'Generic', 'a brand without the material is kept whole');
assert.equal(printer.productLine('', 'PLA'), null);
});
test('a tray reports its weight when new', () => {
const t = printer.describeTray({ id: '0', tray_type: 'PLA', tray_color: 'FFFFFFFF', tray_weight: '1000' });
assert.equal(t.weight, 1000);
assert.equal(printer.describeTray({ id: '0', tray_type: 'PLA', tray_weight: '0' }).weight, null);
});
test('status is read from a report, and fields a report leaves out stay out', () => {
const { readStatus } = require('../lib/printer');
const full = readStatus({
gcode_state: 'RUNNING', mc_percent: 42, mc_remaining_time: 73, subtask_name: 'Benchy',
layer_num: 80, total_layer_num: 210, nozzle_temper: 219.6, nozzle_target_temper: 220,
bed_temper: 59.8, bed_target_temper: 60, spd_lvl: 2, print_error: 0,
hms: [{ attr: 0x07002000, code: 0x00020001 }],
});
assert.deepEqual(full, {
state: 'RUNNING', progress: 42, remainingMin: 73, file: 'Benchy', layer: 80, totalLayers: 210,
nozzle: 219.6, nozzleTarget: 220, bed: 59.8, bedTarget: 60, speed: 'Standard', printError: null,
hms: ['0700_2000_0002_0001'],
});
// A delta with only the nozzle temperature says nothing about the rest.
assert.deepEqual(readStatus({ nozzle_temper: 200 }), { nozzle: 200 });
assert.equal(readStatus({ print_error: 117473298 }).printError, '07008012');
});