diff --git a/lib/printer.js b/lib/printer.js index 75aaea3..19965ae 100644 --- a/lib/printer.js +++ b/lib/printer.js @@ -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) => { diff --git a/server.js b/server.js index 4dcda4b..69f467b 100644 --- a/server.js +++ b/server.js @@ -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); } }); diff --git a/test/printer.test.js b/test/printer.test.js index 08fd165..f1f5fb2 100644 --- a/test/printer.test.js +++ b/test/printer.test.js @@ -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' }]), []);