printhive/test/spoolSync.test.js
dlawler489 e98edaa9ed Add a Printers page for any number of printers, and show spools' RFID tags
Printers move out of Settings into their own table and page: each has an
optional nickname (the model is shown when blank), a model from the Bambu
lineup with a line drawing that follows the selection, an AMS type
suggested from the model, and its address, serial and access code. Access
codes are never returned to the browser. The printer set up in Settings is
migrated automatically and its settings rows removed.

Background sync covers every printer. Grams left are filled in only from an
AMS that measures (AMS, AMS 2 Pro, AMS HT), never from an AMS lite.

Spools now show their RFID tag ID, product code and filament code, and say
which printer they're loaded in.

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

98 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 { planSync, slotKey } = require('../lib/spoolSync');
const { paletteMatch } = require('../lib/filaments');
const palette = require('../db/bambu-filaments.json').filaments;
const SERIAL = 'PRINTER1';
const tray = (slot, hex, uuid, extra = {}) => ({
unit: 0, slot, empty: false, material: 'PLA', type: 'Basic', colour: hex, brand: 'PLA Basic',
uuid, tagUid: uuid ? `TAG${slot}` : null, idName: null, weight: 1000, ...extra,
});
test('a tagged spool the shelf has never seen is created and named from the filament list', () => {
const { actions, untracked } = planSync([], [tray(0, '#C12E1F', 'U-RED', { idName: 'A00-R0' })], SERIAL, palette);
assert.equal(untracked.length, 0);
assert.equal(actions.length, 1);
assert.equal(actions[0].op, 'create');
assert.equal(actions[0].slot, 'PRINTER1/0/0');
assert.equal(actions[0].spool.colour, 'Red');
assert.equal(actions[0].spool.code, '10200');
assert.equal(actions[0].spool.colour_hex, '#C12E1F');
assert.equal(actions[0].spool.product_code, 'A00-R0');
assert.equal(actions[0].spool.total_g, 1000);
assert.equal(actions[0].spool.remaining_g, null, 'an AMS lite can’t say how much is left');
});
test('a spool the shelf already knows is moved, not added again', () => {
const existing = [{ id: 7, material: 'PLA', colour_hex: '#000000', rfid_uuid: 'U-BLACK',
printer_serial: SERIAL, ams_slot: 'PRINTER1/0/3' }];
const { actions } = planSync(existing, [tray(1, '#000000', 'U-BLACK')], SERIAL, palette);
assert.deepEqual(actions, [{ op: 'update', id: 7, slot: 'PRINTER1/0/1', printerSerial: SERIAL, remaining: null }]);
});
test('a spool added by hand is adopted by the printer rather than duplicated', () => {
// The Jade White spool added before the printer was connected.
const existing = [{ id: 1, material: 'PLA', colour_hex: '#ffffff', rfid_uuid: null, printer_serial: null, ams_slot: null }];
const { actions } = planSync(existing, [tray(3, '#FFFFFF', 'U-WHITE', { idName: 'A00-W1' })], SERIAL, palette);
assert.equal(actions.length, 1);
assert.equal(actions[0].op, 'adopt');
assert.equal(actions[0].id, 1);
assert.equal(actions[0].uuid, 'U-WHITE');
assert.equal(actions[0].code, '10100');
});
test('one hand-added spool is adopted once, even with two matching trays', () => {
const existing = [{ id: 1, material: 'PLA', colour_hex: '#FFFFFF', rfid_uuid: null }];
const { actions } = planSync(existing, [tray(0, '#FFFFFF', 'U-W1'), tray(1, '#FFFFFF', 'U-W2')], SERIAL, palette);
assert.deepEqual(actions.map((a) => a.op), ['adopt', 'create']);
});
test('a hand-added spool of another colour or material is left alone', () => {
const existing = [
{ id: 1, material: 'PLA', colour_hex: '#FF0000', rfid_uuid: null }, // wrong colour
{ id: 2, material: 'PETG', colour_hex: '#FFFFFF', rfid_uuid: null }, // wrong material
];
const { actions } = planSync(existing, [tray(0, '#FFFFFF', 'U-W')], SERIAL, palette);
assert.deepEqual(actions.map((a) => a.op), ['create']);
});
test('an untagged spool is listed, not guessed at', () => {
const { actions, untracked } = planSync([], [tray(2, '#123456', null)], SERIAL, palette);
assert.deepEqual(actions, []);
assert.deepEqual(untracked, [{ slot: 'PRINTER1/0/2', material: 'PLA', colour: '#123456' }]);
});
test('a spool this printer no longer reports is unloaded; other printers’ spools are not', () => {
const existing = [
{ id: 5, material: 'PLA', colour_hex: '#000000', rfid_uuid: 'U-GONE', printer_serial: SERIAL, ams_slot: 'PRINTER1/0/0' },
{ id: 6, material: 'PLA', colour_hex: '#000000', rfid_uuid: 'U-ELSEWHERE', printer_serial: 'OTHER', ams_slot: 'OTHER/0/0' },
];
const { actions } = planSync(existing, [], SERIAL, palette);
assert.deepEqual(actions, [{ op: 'unload', id: 5 }]);
});
test('empty trays are ignored, and the external holder has its own slot key', () => {
assert.deepEqual(planSync([], [{ empty: true, unit: 0, slot: 0 }], SERIAL, palette).actions, []);
assert.equal(slotKey(SERIAL, { unit: null, slot: 254 }), 'PRINTER1/ext/254');
});
test('palette matching names a colour within its product line, and doesn’t guess', () => {
assert.deepEqual(paletteMatch(palette, 'PLA', 'Basic', '#FFFFFF'), { name: 'Jade White', code: '10100', type: 'Basic' });
assert.equal(paletteMatch(palette, 'PLA', 'Matte', '#FFFFFF').name, 'Ivory White');
assert.equal(paletteMatch(palette, 'PLA', 'Basic', '#123456'), null);
});
test('an AMS that measures fills in grams left; an AMS lite never does', () => {
const trays = [tray(0, '#C12E1F', 'U-RED', { remain: 42 })];
const lite = planSync([], trays, SERIAL, palette, { measures: false });
assert.equal(lite.actions[0].spool.remaining_g, null);
const pro = planSync([], trays, SERIAL, palette, { measures: true });
assert.equal(pro.actions[0].spool.remaining_g, 420);
const existing = [{ id: 7, material: 'PLA', colour_hex: '#C12E1F', rfid_uuid: 'U-RED', printer_serial: SERIAL, ams_slot: 'PRINTER1/0/0' }];
assert.equal(planSync(existing, trays, SERIAL, palette, { measures: true }).actions[0].remaining, 420);
// No reading at all leaves the stored amount alone.
const unknown = [tray(0, '#C12E1F', 'U-RED', { remain: null })];
assert.equal(planSync(existing, unknown, SERIAL, palette, { measures: true }).actions[0].remaining, null);
});