Every Bambu spool reports an RFID identity — a spool id and a tag id, both unique — so a printer's report can now keep the Filament page in step: a new tagged spool is created, named and coded from the bundled filament list; one already known is moved to its current slot; one you added by hand before the printer was connected is adopted rather than duplicated; and one the printer no longer reports is marked as taken out. It runs on "Save & test connection" and every ten minutes in the background, still only reading from the printer. Untagged spools are listed, not stored: without a tag, one spool of a colour is indistinguishable from the next, so guessing would mean duplicates. The planning is a pure function, tested against the cases that would cause duplicates. The database side was exercised separately against a real Postgres: three successive reports created, moved and unloaded spools as intended, and a deliberate duplicate was refused by the partial unique index. The amount left isn't taken from the printer yet: an AMS lite only reports full or empty, so it's left blank until printers can say which kind of AMS they have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
84 lines
4.4 KiB
JavaScript
84 lines
4.4 KiB
JavaScript
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 }]);
|
||
});
|
||
|
||
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);
|
||
});
|