Skip to content

Commit f80a891

Browse files
committed
v1.7.1 — live status pills in grid/table, cam improvements, grid partition fix
Printer grid & table — live status - All connected printers now show their live state (Idle/Printing/Paused/ Preparing/Complete/Error…) directly in the grid card and table row using the same snap-job-state pill classes as the sidecard (ISO visual style). Spinning ring animation on printing/preparing; colour-coded per state. - Online badge in grid cards now matches the sidecard pill: coloured background + border (green=online, amber=connecting, grey=offline). - Progress bar + pct + remaining time (BambuLab/Elegoo/Creality) + truncated filename shown for active jobs. - _getPrinterJob returns { state, pct, isActive, filename, remainSec } for all five brands; Creality numeric state normalised; remaining time unified to seconds from brand-specific units. - New i18n keys (9 locales): snapState_finished, snapState_preparing, snapState_failed, snapState_ready. Grid Online/Offline partition fix (all brands) - ctx.onPrinterGridChange referenced _printerSub (const scoped inside renderPrintersView), causing a silent ReferenceError in strict-mode ES modules that swallowed every re-partition RAF call. Fixed: state.viewMode. - RAF coalescing race: shared flag between statusChanged=true (re-partition) and statusChanged=false (surgical patch) blocked the grid update on fast LANs. Fixed with two independent flags per brand driver. Camera - Cam wall card click → opens sidecard for that printer. - FlashForge MJPEG multiplexer (cam_mux.js): single fetch() stream shared across cam wall + sidecard; respects 1-client limit; auto-stops when last consumer unregisters. - Creality camera persistence: _activeIp guard prevents WebRTC restart on WS reconnect; #creCamContainer persists in DOM via CSS toggle.
1 parent bcddde4 commit f80a891

46 files changed

Lines changed: 5063 additions & 314 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ Versions follow [Semantic Versioning](https://semver.org/).
55

66
---
77

8+
## v1.7.1 — 2026-05-17
9+
10+
### Printer grid & table — live status and progress
11+
12+
- **Status pills in grid cards and table** — every connected printer now shows its live state (Idle, Printing, Paused, Preparing, Complete, Error, …) directly in the grid card and table row without opening the sidecard. Offline printers show nothing; connected-but-idle printers show a muted grey pill; active jobs show the progress bar + `XX% · 1h 23m`.
13+
- **ISO visual style** — the state pills in cards and table use the exact same `snap-job-state snap-job-state--{state}` classes as the sidecard, scaled via `.snap-job-state--compact`. Spinning ring animation on `printing` and `preparing`, colour-coded per state (blue=printing, amber=paused/preparing, green=complete/finished, red=error/failed, grey=idle/standby/ready).
14+
- **Online badge pill** in grid cards now matches the sidecard pill: rounded background + coloured border (green for online, amber for connecting, grey for offline).
15+
- **Filename + remaining time** — when a job is active, the truncated filename appears below the progress bar and remaining time is shown alongside the percentage (`42% · 1h 23m`). BambuLab, Elegoo, and Creality expose remaining time; all brands expose the filename when printing.
16+
- **Cross-brand normalisation** — `_getPrinterJob` now returns a uniform `{ state, pct, isActive, filename, remainSec }` for all five brands. Creality's numeric `d.state` is normalised to `idle`/`printing`/`complete`; remaining time converted from brand-specific units (BambuLab minutes, Elegoo ms, Creality seconds).
17+
- **New i18n keys** across all 9 locales: `snapState_finished`, `snapState_preparing`, `snapState_failed`, `snapState_ready`.
18+
19+
### Printer grid — Online/Offline partition fix (all brands)
20+
21+
- **Root cause**: `ctx.onPrinterGridChange` referenced `_printerSub`, a `const` scoped inside `renderPrintersView()`. In strict mode (ES modules) this threw a silent `ReferenceError` on every RAF tick, swallowing the re-partition call — printers that connected after the initial render were stuck in the Offline section indefinitely. Fixed: `state.viewMode !== "printer-cam"`.
22+
- **RAF coalescing race** (all 4 brand drivers): the shared RAF flag for `statusChanged=true` (re-partition) and `statusChanged=false` (surgical job patch) could block the connected-status RAF on a fast LAN. Fixed by splitting into two independent flags (`_xxxStatusRaf` / `_xxxGridRaf`) per brand.
23+
24+
### Camera improvements
25+
26+
- **Cam wall card → click → sidecard** — clicking any camera wall card opens the sidecard for that printer. CSS `cursor: pointer` + `border-color` hover feedback on `.cam-wall-card`.
27+
- **FlashForge MJPEG multiplexer** (`cam_mux.js`) — a single `fetch()` reads the MJPEG stream and distributes JPEG frames to all registered `<img>` consumers (cam wall + sidecard simultaneously) with zero extra connections. Respects FlashForge's 1-client limit. Stream auto-stops when the last consumer unregisters.
28+
- **Creality camera persistence** — `_activeIp` tracking prevents redundant WebRTC restarts on WS reconnect. `#creCamContainer` persists in the DOM; `.cre-cam-hidden` toggled by CSS instead of DOM removal.
29+
30+
---
31+
832
## v1.7.0 — 2026-05-15
933

1034
### DB pipeline — unified reference data layer

‎ROADMAP.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ Grouped by domain. Versions in parentheses are the release that landed the featu
8080
- ✅ **Creality live integration** — WebSocket port 9999, heartbeat, live temps, CFS colour grid, WebRTC camera (v1.4.15)
8181
- ✅ **Elegoo live integration** — MQTT port 1883, UDP discovery port 52700; job card, temp card, mono + 4-slot Canvas filament card, control card (XY circle jog pad + Z pill + X/Y home pill + fans + LED + files button), filament edit sheet (colour + material + vendor pickers), Files/History sheet, camera; surgical DOM patch on control card to eliminate MQTT-tick flash (v1.6.0)
8282
- ✅ **Bambu Lab live integration** — MQTTS port 8883 TLS, LAN mode; job card, temp card, AMS filament grid (Ext. + module rows), camera widget, online badge (v1.6.0); filament edit bottom-sheet redesigned ISO with Snapmaker/Elegoo/FlashForge (2 rows, auto-close on color pick, "Edit filament" title) (v1.7.0)
83+
- ✅ **Printer grid & table — live status pills** — every connected printer shows its live state directly in the grid card and table row without opening the sidecard. ISO visual style: same `snap-job-state` pill classes as the sidecard (spinner on printing, colour-coded per state). Progress bar + `XX% · Nh Nm` for active jobs; filename truncated below the bar. Online badge in cards now matches the sidecard pill (coloured background + border). (v1.7.1)
84+
- ✅ **Grid Online/Offline partition — fixed for all brands** — `ctx.onPrinterGridChange` referenced an out-of-scope variable (`_printerSub`) causing a silent ES-module `ReferenceError` that swallowed every re-partition call. Also fixed: shared RAF coalescing flag across `statusChanged=true/false` paths blocked the grid re-partition on fast LANs. Both fixed in all 4 brand drivers. (v1.7.1)
85+
- ✅ **Cam wall card → click → sidecard** — clicking a camera wall card opens the sidecard for that printer; CSS hover feedback on `.cam-wall-card`. (v1.7.1)
86+
- ✅ **FlashForge MJPEG multiplexer** (`cam_mux.js`) — single `fetch()` stream shared across cam wall tile + sidecard simultaneously, respecting FlashForge's 1-client limit. Auto-stops when the last consumer unregisters. (v1.7.1)
87+
- ✅ **Creality camera persistence** — `_activeIp` guard prevents WebRTC restart on WS reconnect; `#creCamContainer` persists in DOM with CSS visibility toggle. (v1.7.1)
8388

8489
### Sensors & devices
8590
- ✅ **ACR122U NFC reader** (USB) via `nfc-pcsc` — `main.js` ↔ renderer IPC bridge
146 KB
Loading

‎assets/svg/icons/icon_rfid.svg‎

Lines changed: 13 additions & 0 deletions
Loading

‎assets/svg/icons/icon_youtube.svg‎

Lines changed: 3 additions & 0 deletions
Loading
42.6 KB
Loading

‎assets/svg/logos/logo_fluidd.svg‎

Lines changed: 1 addition & 0 deletions
Loading

‎main.js‎

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ let imgCacheDir;
8989

9090
let mainWindow;
9191

92+
// NFC state — replayed to renderer on every (re)load
93+
const _nfcReaders = new Map(); // name → reader object
94+
const _nfcCardPresent = new Map(); // name → { uid, rawUid }
95+
9296
// ── Convert hex UID (e.g. "1D895E7C004A80") to decimal string used by TigerTag
9397
function hexToDecimalUid(hex) {
9498
try {
@@ -98,6 +102,9 @@ function hexToDecimalUid(hex) {
98102
}
99103
}
100104

105+
// ── TigerTag binary parser (isolated module) ──────────────────────────────────
106+
const { parseTigerTag } = require('./renderer/rfid_protocol/tigertag/parser.js');
107+
101108
// ── Create main window
102109
function createWindow() {
103110
mainWindow = new BrowserWindow({
@@ -154,56 +161,72 @@ function createWindow() {
154161

155162
// ── NFC / RFID reader
156163
function initNFC() {
157-
let NFC, nfc;
164+
let nfc;
158165
try {
159-
NFC = require('nfc-pcsc');
166+
const { NFC } = require('nfc-pcsc'); // named export, not the module object
160167
nfc = new NFC();
161168
} catch (err) {
162169
console.warn('[NFC] not available:', err.message);
163170
return;
164171
}
165172

166173
nfc.on('reader', (reader) => {
167-
console.log(`[NFC] Reader connected: ${reader.name}`);
168-
mainWindow?.webContents.send('reader-status', {
169-
connected: true,
170-
name: reader.name,
171-
});
174+
const rName = reader.name;
175+
console.log(`[NFC] Reader connected: ${rName}`);
176+
_nfcReaders.set(rName, reader);
177+
mainWindow?.webContents.send('rfid-reader-update', { name: rName, connected: true });
172178

173179
reader.on('card', (card) => {
174-
const rawUid = card.uid; // hex string e.g. "1d895e7c004a80"
175-
const uid = hexToDecimalUid(rawUid); // decimal e.g. "8307741719072896"
176-
console.log(`[NFC] Card detected — raw: ${rawUid} → uid: ${uid}`);
180+
const rawUid = card.uid;
181+
const uid = hexToDecimalUid(rawUid);
182+
console.log(`[NFC] Card present on ${rName} — uid: ${uid}`);
183+
_nfcCardPresent.set(rName, { uid, rawUid });
184+
// Notify inventory for automatic spool lookup
177185
mainWindow?.webContents.send('rfid-uid', uid, rawUid);
186+
// Notify RFID tester modal — card present, not yet read
187+
mainWindow?.webContents.send('rfid-card-present', { readerName: rName, uid, rawUid });
178188
});
179189

180190
reader.on('card.off', () => {
181-
console.log('[NFC] Card removed');
191+
console.log(`[NFC] Card removed from ${rName}`);
192+
_nfcCardPresent.delete(rName);
193+
mainWindow?.webContents.send('rfid-card-present', { readerName: rName, uid: null, rawUid: null });
182194
});
183195

184196
reader.on('error', (err) => {
185-
console.error('[NFC] Reader error:', err.message);
197+
console.error(`[NFC] Reader error on ${rName}:`, err.message);
186198
});
187199

188200
reader.on('end', () => {
189-
console.log(`[NFC] Reader disconnected: ${reader.name}`);
190-
mainWindow?.webContents.send('reader-status', {
191-
connected: false,
192-
name: reader.name,
193-
});
201+
console.log(`[NFC] Reader disconnected: ${rName}`);
202+
_nfcReaders.delete(rName);
203+
_nfcCardPresent.delete(rName);
204+
mainWindow?.webContents.send('rfid-reader-update', { name: rName, connected: false });
194205
});
195206
});
196207

197208
nfc.on('error', (err) => {
198209
console.error('[NFC] NFC error:', err.message);
199-
mainWindow?.webContents.send('reader-status', {
200-
connected: false,
201-
name: null,
202-
error: err.message,
203-
});
204210
});
205211
}
206212

213+
// On-demand card read — called by renderer "Read" button in RFID tester
214+
ipcMain.handle('rfid:read-now', async (_evt, readerName) => {
215+
const reader = _nfcReaders.get(readerName);
216+
if (!reader) return { ok: false, error: 'Reader not connected' };
217+
const card = _nfcCardPresent.get(readerName);
218+
if (!card) return { ok: false, error: 'No card present' };
219+
try {
220+
const data = await reader.read(0, 180, 4); // 45 pages × 4 bytes
221+
const rawPagesHex = data.toString('hex');
222+
const tigerTag = parseTigerTag(data);
223+
console.log('[NFC] read-now result:', tigerTag ? tigerTag.version : 'unknown format');
224+
return { ok: true, uid: card.uid, rawUid: card.rawUid, rawPagesHex, tigerTag };
225+
} catch (err) {
226+
return { ok: false, error: err.message };
227+
}
228+
});
229+
207230

208231
// ── TD1S color sensor ────────────────────────────────────────────────────────
209232
function initTD1S() {
@@ -267,6 +290,13 @@ function initTD1S() {
267290
if (currentTd !== null) {
268291
mainWindow.webContents.send('td1s-data', { TD: currentTd, HEX: currentHex });
269292
}
293+
// Replay NFC reader + card state — needed when reader was already connected at startup
294+
for (const name of _nfcReaders.keys()) {
295+
mainWindow.webContents.send('rfid-reader-update', { name, connected: true });
296+
}
297+
for (const [name, card] of _nfcCardPresent.entries()) {
298+
mainWindow.webContents.send('rfid-card-present', { readerName: name, uid: card.uid, rawUid: card.rawUid });
299+
}
270300
});
271301

272302
function extractTdHex(rawLine) {
@@ -1127,6 +1157,38 @@ ipcMain.handle('db:isUpdateAvailable', () => db.isUpdateAvailabl
11271157
ipcMain.handle('db:updateIfNeeded', () => db.updateIfNeeded());
11281158
ipcMain.handle('db:downloadAndSaveLatestData', () => db.downloadAndSaveLatestData());
11291159

1160+
// ── Timelapse download ───────────────────────────────────────────────────────
1161+
// Shows a native Save dialog then streams the video from the printer over HTTP.
1162+
ipcMain.handle('timelapse:download', async (_evt, videoUrl, suggestedFilename) => {
1163+
const defaultName = suggestedFilename || 'timelapse.mp4';
1164+
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, {
1165+
title: 'Save Timelapse',
1166+
defaultPath: path.join(app.getPath('downloads'), defaultName),
1167+
filters: [{ name: 'MP4 Video', extensions: ['mp4'] }],
1168+
});
1169+
if (canceled || !filePath) return { ok: false, reason: 'cancelled' };
1170+
1171+
return new Promise((resolve) => {
1172+
const dest = fs.createWriteStream(filePath);
1173+
const cleanup = (err) => {
1174+
dest.destroy();
1175+
try { fs.unlinkSync(filePath); } catch (_) {}
1176+
resolve({ ok: false, reason: err?.message || 'error' });
1177+
};
1178+
// URL is: http://<ip>/download?X-Token=<pwd>&file_name=<encoded_path>
1179+
// Pass the full URL string directly — no path manipulation needed.
1180+
http.get(videoUrl, { timeout: 30000 }, (res) => {
1181+
if (res.statusCode !== 200) {
1182+
res.resume();
1183+
return cleanup(new Error(`HTTP ${res.statusCode}`));
1184+
}
1185+
res.pipe(dest);
1186+
dest.on('finish', () => { dest.close(); resolve({ ok: true, path: filePath }); });
1187+
dest.on('error', cleanup);
1188+
}).on('error', cleanup).on('timeout', function() { this.destroy(); cleanup(new Error('timeout')); });
1189+
});
1190+
});
1191+
11301192
// ── Elegoo MQTT bridge ───────────────────────────────────────────────────────
11311193
// Each connected printer gets its own mqtt.Client stored in _elegooClients.
11321194
// The renderer sends connect/disconnect/publish; this bridge forwards

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tigertag-inventory",
3-
"version": "1.7.0",
3+
"version": "1.7.1",
44
"description": "Tiger Studio Manager — desktop app",
55
"main": "main.js",
66
"engines": {

‎preload.js‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,22 @@ contextBridge.exposeInMainWorld('electronAPI', {
4040
onRfid: (callback) =>
4141
ipcRenderer.on('rfid-uid', (_, uid, rawHex) => callback(uid, rawHex)),
4242

43-
// Called when reader connects/disconnects — callback({ connected, name, error? })
43+
// Called when reader connects/disconnects — callback({ name, connected })
4444
onReaderStatus: (callback) =>
4545
ipcRenderer.on('reader-status', (_, status) => callback(status)),
4646

47+
// Called when a reader connects/disconnects (new unified event)
48+
onRfidReaderUpdate: (callback) =>
49+
ipcRenderer.on('rfid-reader-update', (_, data) => callback(data)),
50+
51+
// Called when a card appears/disappears on a reader — callback({ readerName, uid, rawUid })
52+
onRfidCardPresent: (callback) =>
53+
ipcRenderer.on('rfid-card-present', (_, data) => callback(data)),
54+
55+
// On-demand read: returns { ok, uid, rawUid, rawPagesHex, tigerTag } | { ok:false, error }
56+
readRfidNow: (readerName) =>
57+
ipcRenderer.invoke('rfid:read-now', readerName),
58+
4759
// Called when an app update is available or ready to install
4860
onUpdateStatus: (callback) =>
4961
ipcRenderer.on('update-status', (_, info) => callback(info)),
@@ -79,6 +91,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
7991
getAppInfo: () => ipcRenderer.invoke('app:info'),
8092
// Open a URL in the default system application (e.g. VLC for rtsp://).
8193
openExternal: (url) => ipcRenderer.send('shell:open-external', url),
94+
// Show native Save dialog and stream a timelapse video from the printer to disk.
95+
downloadTimelapse: (url, filename) => ipcRenderer.invoke('timelapse:download', url, filename),
8296

8397
// Absolute path to renderer/ dir — used to build file:// preload paths
8498
// for <webview> elements (e.g. Creality camera preload script).

0 commit comments

Comments
 (0)