Merge spool RFID identity and the raw-report diagnostic
All checks were successful
build-and-push / docker (push) Successful in 15s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dlawler489 2026-09-17 20:31:50 +10:00
commit 99d6bdf06c
3 changed files with 48 additions and 6 deletions

View file

@ -9,8 +9,12 @@ const mqtt = require('mqtt');
const PORT = 8883;
// An id the printer sends as all zeros means no RFID tag was read.
const realId = (v) => (v && !/^0+$/.test(String(v)) ? String(v) : null);
// One tray, as the printer describes it. `remain` is a percentage; the printer
// sends -1 when it can't tell, which is mostly the case for non-Bambu spools.
// sends -1 when it can't tell. An AMS lite only knows full or empty, so its
// figure isn't a measurement — callers decide whether to trust it.
function describeTray(tray, unit = null) {
const loaded = Boolean(tray && tray.tray_type);
return {
@ -22,6 +26,10 @@ function describeTray(tray, unit = null) {
profile: loaded ? tray.tray_info_idx || null : null, // Bambu's filament profile, e.g. GFA00
brand: loaded ? tray.tray_sub_brands || null : null,
remain: loaded && Number(tray.remain) >= 0 ? Number(tray.remain) : null,
// A Bambu spool's RFID tag: tray_uuid names the spool, tag_uid the tag.
uuid: loaded ? realId(tray.tray_uuid) : null,
tagUid: loaded ? realId(tray.tag_uid) : null,
idName: loaded ? tray.tray_id_name || null : null, // e.g. "A00-W1", on firmware that sends it
};
}
@ -45,10 +53,12 @@ function friendlyError(err) {
// Connects, asks for a full report, and gathers what arrives for a few seconds.
// Resolves with what was seen — never with the access code.
function probe({ host, serial, accessCode, waitMs = 8000 }) {
// With `debug`, also returns one loaded tray exactly as the printer sent it, to
// see which fields a given printer and firmware actually provide.
function probe({ host, serial, accessCode, waitMs = 8000, debug = false }) {
return new Promise((resolve) => {
const started = Date.now();
const seen = { connected: false, reports: 0, printer: null, ams: null, external: null, error: null };
const seen = { connected: false, reports: 0, printer: null, ams: null, external: null, error: null, raw: null };
let done = false;
let timer = null;
const client = mqtt.connect(`mqtts://${host}:${PORT}`, {
@ -85,7 +95,18 @@ function probe({ host, serial, accessCode, waitMs = 8000 }) {
if (!p) return;
if (p.gcode_state) seen.printer = { ...(seen.printer || {}), state: p.gcode_state };
if (p.mc_percent != null) seen.printer = { ...(seen.printer || {}), progress: Number(p.mc_percent) };
if (p.ams && Array.isArray(p.ams.ams)) seen.ams = describeAms(p.ams.ams);
if (p.ams && Array.isArray(p.ams.ams)) {
seen.ams = describeAms(p.ams.ams);
if (debug) {
const unit = p.ams.ams[0] || {};
seen.raw = {
amsKeys: Object.keys(p.ams),
unitKeys: Object.keys(unit),
unitInfo: { info: unit.info, humidity: unit.humidity, temp: unit.temp },
tray: (unit.tray || []).find((t) => t && t.tray_type) || null,
};
}
}
if (p.vt_tray) seen.external = describeTray(p.vt_tray, null);
});
client.on('error', (err) => {

View file

@ -337,7 +337,10 @@ app.post('/api/printer/test', async (req, res, next) => {
const accessCode = (await resolveSetting('printer_access_code')).value;
const missing = [!host && 'address', !serial && 'serial number', !accessCode && 'access code'].filter(Boolean);
if (missing.length) return res.status(400).json({ error: `Fill in the printer’s ${missing.join(', ')} first.` });
res.json(await printer.probe({ host: host.trim(), serial: serial.trim(), accessCode: accessCode.trim() }));
res.json(await printer.probe({
host: host.trim(), serial: serial.trim(), accessCode: accessCode.trim(),
debug: req.query.debug === '1', // raw fields for one tray — filament data, no credentials
}));
} catch (e) { next(e); }
});

View file

@ -10,7 +10,7 @@ test('AMS trays are read into material, colour and how much is left', () => {
] }]);
assert.deepEqual(trays[0], {
unit: 0, slot: 0, empty: false, material: 'PLA', colour: '#FFFFFF',
profile: 'GFA00', brand: 'PLA Basic', remain: 82,
profile: 'GFA00', brand: 'PLA Basic', remain: 82, uuid: null, tagUid: null, idName: null,
});
assert.equal(trays[1].empty, true);
assert.equal(trays[1].material, null);
@ -19,6 +19,24 @@ test('AMS trays are read into material, colour and how much is left', () => {
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' }]), []);