// Keeps the Filament page in step with what a printer has loaded. // // planSync() works out, without touching the database, what a printer's report // means for your spools; applySync() carries that out. Only spools with an RFID // tag are handled automatically: an untagged spool looks identical to any other // of its colour, so there's no way to tell it from one you already have. const db = require('./db'); const { colourDistance, normalise, paletteMatch } = require('./filaments'); // How close a hand-added spool's colour must be for it to adopt a printer's tag. const ADOPT_TOLERANCE = 12; // Where a tray is, as stored on a spool: "//", with // "ext" as the unit for the external spool holder. const slotKey = (printerSerial, tray) => `${printerSerial}/${tray.unit == null ? 'ext' : tray.unit}/${tray.slot == null ? 0 : tray.slot}`; function planSync(existing, trays, printerSerial, palette, { measures = false } = {}) { const actions = []; // Grams left, only when this printer's AMS actually measures it. An AMS lite // reports full or empty, which is not a weight. const gramsLeft = (tray) => (measures && tray.remain != null && tray.weight ? Math.round((tray.weight * tray.remain) / 100) : null); const untracked = []; const byUuid = new Map(existing.filter((s) => s.rfid_uuid).map((s) => [s.rfid_uuid, s])); const adopted = new Set(); // hand-added spools already matched in this report const seen = new Set(); // existing spools this report accounts for for (const tray of trays || []) { if (!tray || tray.empty) continue; const slot = slotKey(printerSerial, tray); if (!tray.uuid) { untracked.push({ slot, material: tray.material, colour: tray.colour }); continue; } const known = byUuid.get(tray.uuid); if (known) { seen.add(known.id); actions.push({ op: 'update', id: known.id, slot, printerSerial, remaining: gramsLeft(tray) }); continue; } const named = paletteMatch(palette, tray.material, tray.type, tray.colour); // A spool you added by hand before the printer reported it: take it over // rather than adding a second copy. const adopt = existing.find((s) => { if (s.rfid_uuid || adopted.has(s.id)) return false; if (normalise(s.material) !== normalise(tray.material)) return false; const d = colourDistance(s.colour_hex, tray.colour); return d !== null && d <= ADOPT_TOLERANCE; }); if (adopt) { adopted.add(adopt.id); seen.add(adopt.id); actions.push({ op: 'adopt', id: adopt.id, slot, printerSerial, uuid: tray.uuid, tagUid: tray.tagUid, productCode: tray.idName, code: named ? named.code : null, totalG: tray.weight, remaining: gramsLeft(tray), }); continue; } actions.push({ op: 'create', slot, printerSerial, spool: { material: tray.material, colour: named ? named.name : (tray.idName || tray.colour || tray.material), colour_hex: tray.colour, brand: 'Bambu Lab', code: named ? named.code : null, total_g: tray.weight, remaining_g: gramsLeft(tray), // blank unless this printer's AMS measures it rfid_uuid: tray.uuid, tag_uid: tray.tagUid, product_code: tray.idName, }, }); } // Anything this printer used to hold that it no longer reports has come out. for (const s of existing) { if (s.printer_serial === printerSerial && s.ams_slot && !seen.has(s.id)) { actions.push({ op: 'unload', id: s.id }); } } return { actions, untracked }; } async function applySync({ actions }) { const summary = { created: 0, adopted: 0, updated: 0, unloaded: 0 }; for (const a of actions) { if (a.op === 'create') { const s = a.spool; const { rowCount } = await db.query( `INSERT INTO filament_spools (material, colour, colour_hex, brand, code, total_g, remaining_g, rfid_uuid, tag_uid, product_code, ams_slot, printer_serial, last_seen_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12, now()) ON CONFLICT (rfid_uuid) WHERE rfid_uuid IS NOT NULL DO NOTHING`, [s.material, s.colour, s.colour_hex, s.brand, s.code, s.total_g, s.remaining_g, s.rfid_uuid, s.tag_uid, s.product_code, a.slot, a.printerSerial]); summary.created += rowCount; } else if (a.op === 'adopt') { const { rowCount } = await db.query( `UPDATE filament_spools SET rfid_uuid = $2, tag_uid = $3, product_code = $4, code = COALESCE(code, $5), total_g = COALESCE(total_g, $6), ams_slot = $7, printer_serial = $8, remaining_g = COALESCE($9, remaining_g), last_seen_at = now(), date_modified = now() WHERE id = $1 AND rfid_uuid IS NULL`, [a.id, a.uuid, a.tagUid, a.productCode, a.code, a.totalG, a.slot, a.printerSerial, a.remaining ?? null]); summary.adopted += rowCount; } else if (a.op === 'update') { // A measured weight replaces the stored one; no reading leaves it alone. await db.query( `UPDATE filament_spools SET ams_slot = $2, printer_serial = $3, last_seen_at = now(), remaining_g = COALESCE($4, remaining_g) WHERE id = $1`, [a.id, a.slot, a.printerSerial, a.remaining ?? null]); summary.updated += 1; } else if (a.op === 'unload') { await db.query('UPDATE filament_spools SET ams_slot = NULL WHERE id = $1', [a.id]); summary.unloaded += 1; } } return summary; } // Reads the shelf, plans against one printer's trays, and applies it. async function syncPrinter(printerSerial, trays, palette, options = {}) { const { rows: existing } = await db.query('SELECT * FROM filament_spools'); const plan = planSync(existing, trays, printerSerial, palette, options); const summary = await applySync(plan); return { ...summary, untracked: plan.untracked }; } module.exports = { planSync, syncPrinter, slotKey };