diff --git a/lib/printer.js b/lib/printer.js index ae2843c..79e943a 100644 --- a/lib/printer.js +++ b/lib/printer.js @@ -152,6 +152,7 @@ function probe({ host, serial, accessCode, waitMs = 8000, debug = false }) { amsKeys: Object.keys(p.ams), unitKeys: Object.keys(unit), unitInfo: { info: unit.info, humidity: unit.humidity, temp: unit.temp }, + trayNow: p.ams.tray_now, trayTar: p.ams.tray_tar, tray: (unit.tray || []).find((t) => t && t.tray_type) || null, }; } diff --git a/lib/printerLive.js b/lib/printerLive.js index 645409e..c533394 100644 --- a/lib/printerLive.js +++ b/lib/printerLive.js @@ -24,6 +24,19 @@ function allTrays(c) { return [...(c.ams || []), ...(c.external ? [c.external] : [])]; } +// Which tray is feeding the toolhead. The printer sends `tray_now` as a number: +// 255 for none, 254 for the external spool, otherwise unit × 4 + slot — where +// "unit" is the AMS's id on most printers, or its position among the units. +function activeTray(c) { + const n = c.trayNow; + if (n == null || Number.isNaN(n) || n === 255) return null; + if (n === 254) return { external: true }; + const ids = c.unitIds || []; + const byId = Math.floor(n / 4); + const unit = ids.includes(byId) ? byId : ids[byId]; + return unit === undefined ? null : { unit, slot: n % 4 }; +} + function requestFullReport(c) { if (!c.client.connected) return; c.client.publish(`device/${c.serial}/request`, @@ -58,7 +71,11 @@ function handleMessage(c, payload) { // AMS details come whole when they change; a report without them changes nothing. const units = p.ams && p.ams.ams; - if (Array.isArray(units) && units.every((u) => Array.isArray(u.tray))) c.ams = describeAms(units); + if (Array.isArray(units) && units.every((u) => Array.isArray(u.tray))) { + c.ams = describeAms(units); + c.unitIds = units.map((u) => Number(u.id)); + } + if (p.ams && p.ams.tray_now !== undefined) c.trayNow = Number(p.ams.tray_now); if (p.vt_tray) c.external = describeTray(p.vt_tray, null); // A printer mid-print reports every second or so, so only a change in what's @@ -88,6 +105,8 @@ function open(p) { external: null, syncedKey: null, pendingKey: null, + trayNow: null, + unitIds: [], lastSync: null, traysTimer: null, waiters: [], @@ -158,7 +177,7 @@ function snapshot(id) { connected: c.connected, error: c.connected ? null : c.error, updatedAt: c.updatedAt, - status: c.status, + status: { ...c.status, active: activeTray(c) }, loaded: c.ams || c.external ? loaded : null, }; } diff --git a/public/app.js b/public/app.js index d035f69..2fcb577 100644 --- a/public/app.js +++ b/public/app.js @@ -2696,27 +2696,53 @@ function drawnSlots(p) { return slots; } +// Which of the drawn things is feeding the toolhead, for the drawing: a slot of +// the first AMS unit, or the external spool with its colour. +function drawnUse(p) { + const a = p.active; + if (!a || !p.loaded) return null; + if (a.external) { + const ext = p.loaded.find((t) => t.unit == null); + return { external: true, colour: ext && !ext.empty ? ext.colour : null }; + } + const units = p.loaded.filter((t) => t.unit != null).map((t) => t.unit); + return units.length && a.unit === Math.min(...units) ? { slot: a.slot } : null; +} + +const isActiveSlot = (p, t) => Boolean(p.active && (p.active.external + ? t.unit == null + : t.unit === p.active.unit && t.slot === p.active.slot)); + +// A tagged spool is Bambu's, so its colour has a name: from the tracked spool, +// or the Bambu colour it matches. An untagged one could be any brand, so it +// gets the colour the printer reports and nothing more. +function slotColourName(p, t) { + if (!t.tagged) return t.colour || ''; + const s = spools.find((x) => x.ams_slot === `${p.serial}/${t.unit == null ? 'ext' : t.unit}/${t.slot == null ? 0 : t.slot}`); + return (s && s.colour) || (t.colour ? paletteName(t.colour, t.material) : '') || ''; +} + +const slotName = (t) => (t.unit == null ? 'External' : `${amsUnitLabel(t.unit)} · ${(t.slot ?? 0) + 1}`); + +function printerArt(p, model) { + const moving = Boolean(p.statusState && ['RUNNING', 'PREPARE'].includes(p.statusState)); + return printerModels().art(model, drawnSlots(p), drawnUse(p), moving); +} + // What's in each slot, as last reported, under the drawing. function loadedHtml(p) { if (!p.loaded) return p.id ? '
Nothing reported yet — “Test & sync” to read it.
' : ''; - // A tagged spool is Bambu's, so its colour has a name: from the tracked spool, - // or the Bambu colour it matches. An untagged one could be any brand, so it - // gets the colour the printer reports and nothing more. - const colourName = (t) => { - if (!t.tagged) return t.colour || ''; - const s = spools.find((x) => x.ams_slot === `${p.serial}/${t.unit == null ? 'ext' : t.unit}/${t.slot == null ? 0 : t.slot}`); - return (s && s.colour) || (t.colour ? paletteName(t.colour, t.material) : '') || ''; - }; const chip = (t, label) => { - const what = t.empty ? 'empty' : `${escapeHtml([t.material || '?', colourName(t)].filter(Boolean).join(' '))}${t.tagged ? '' : ' (untagged)'}`; + const what = t.empty ? 'empty' : `${escapeHtml([t.material || '?', slotColourName(p, t)].filter(Boolean).join(' '))}${t.tagged ? '' : ' (untagged)'}`; const dot = t.empty ? '' : ``; - return `Live from the printer
' : p.loaded_at ? `As of ${escapeHtml(new Date(p.loaded_at).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }))}
` : ''; return rows.length ? `Nothing loaded.
${when}`; } @@ -2732,7 +2758,7 @@ function printerCardHtml(p) { return `