// A read-only connection to a Bambu Lab printer on your network. // // The printer runs an MQTT broker on port 8883 — TLS with a self-signed // certificate, username "bblp", and the access code from its screen as the // password. Printhive only subscribes to the printer's reports and asks once for // a full one ("pushall"); it never sends anything that changes what the printer // is doing. 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. 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 { unit, slot: tray && tray.id != null ? Number(tray.id) : null, empty: !loaded, material: loaded ? tray.tray_type : null, colour: loaded && tray.tray_color ? `#${String(tray.tray_color).slice(0, 6).toUpperCase()}` : 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 }; } // Every tray in every AMS unit, in order. function describeAms(units) { const trays = []; for (const unit of Array.isArray(units) ? units : []) { for (const tray of unit.tray || []) trays.push(describeTray(tray, Number(unit.id))); } return trays; } // The messages the MQTT library gives are terse; say what they mean. function friendlyError(err) { const m = String((err && err.message) || err || ''); if (/not authori[sz]ed|bad user ?name or password/i.test(m)) return 'The printer refused the access code.'; if (/ECONNREFUSED/.test(m)) return 'Nothing answered on port 8883 at that address.'; if (/ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|timed? ?out/i.test(m)) return 'Couldn’t reach the printer from the server.'; return m.slice(0, 200) || 'The connection failed.'; } // Connects, asks for a full report, and gathers what arrives for a few seconds. // Resolves with what was seen — never with the access code. // 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, raw: null }; let done = false; let timer = null; const client = mqtt.connect(`mqtts://${host}:${PORT}`, { username: 'bblp', password: accessCode, rejectUnauthorized: false, // the printer's certificate is self-signed connectTimeout: 6000, reconnectPeriod: 0, // one attempt: this is a test, not a session clientId: `printhive-${Math.random().toString(16).slice(2, 10)}`, }); const finish = () => { if (done) return; done = true; clearTimeout(timer); client.end(true); resolve({ ...seen, ms: Date.now() - started }); }; timer = setTimeout(finish, waitMs); client.on('connect', () => { seen.connected = true; client.subscribe(`device/${serial}/report`, (err) => { if (err) { seen.error = `The printer wouldn’t let Printhive listen: ${err.message}`; return; } // The printer only sends everything when asked. client.publish(`device/${serial}/request`, JSON.stringify({ pushing: { sequence_id: '0', command: 'pushall' } })); }); }); client.on('message', (_topic, payload) => { seen.reports += 1; let msg; try { msg = JSON.parse(payload.toString()); } catch { return; } const p = msg && msg.print; 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 (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) => { seen.error = friendlyError(err); finish(); }); client.on('close', () => { if (!seen.connected) finish(); }); }); } module.exports = { probe, describeAms, describeTray, friendlyError };