Skip to content

Commit 97bfbcc

Browse files
committed
v1.8.9 — Creality LAN discovery (scan + manual IP), optional Creality credentials, friend-view read-only actions hidden
- Creality auto-discovery in Add printer → Scan: main-process TCP :9999 filter (cre:tcp-probe IPC) + renderer WebSocket printerInfo handshake validated with isCrealityLike; one-click add, manual IP + Add-by-IP, persisted extra subnets. Common subnets 192.168.1/192.168.40 always scanned. Verified on Ender-3 V4. - Creality settings: Root account + password now optional (printer works without auth). - Friend (read-only) view hides the + Scan and Add product/device buttons.
1 parent 756c62c commit 97bfbcc

20 files changed

Lines changed: 1221 additions & 9 deletions

‎CHANGELOG.md‎

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

66
---
77

8+
## v1.8.9 — 2026-05-29
9+
10+
### Added
11+
12+
- **Creality printers now show up in the network scan** — *Add printer → Creality → Scan network* discovers Creality machines on your LAN (K-series, K2, and current-gen Enders running Klipper, e.g. the Ender-3 V4) and adds them in one click, just like Snapmaker and FlashForge. There's also a manual *Enter IP address* path and an inline *Add by IP* shortcut for printers the scan can't reach directly. The common Creality home subnets (192.168.1.x, 192.168.40.x) are always scanned, and any extra subnets you add now persist across a *Restart scan*. Verified live against an Ender-3 V4.
13+
14+
### Changed
15+
16+
- **Adding a Creality printer no longer requires a username/password** — the *Root* account and password fields are now optional. Most Creality printers expose their control channel without authentication, so you can add and connect to them without entering anything; only fill them in if your printer's firmware enforces a login.
17+
- **A friend's inventory is cleaner in read-only mode** — when viewing a friend's inventory, the *+ Scan* and *Add* buttons are now hidden, since those actions can't apply to someone else's collection.
18+
19+
---
20+
821
## v1.8.8 — 2026-05-29
922

1023
### Fixed

‎ROADMAP.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,35 @@ A possible 7th driver later: `drivers/elegoo-mqtt.js` for Centauri — **researc
367367
| **Generic Klipper / Wondermaker** | Moonraker WS (`:7125`) | mDNS varies; falls back to manual IP | `klipper-generic` | New driver — at this point we have 3 Klipper-class implementations (Snapmaker + Creality K + Generic) and the empirical common surface is clear, so this is the right moment to extract `_moonraker-base.js`. New brand entry "Klipper machine" with a manual-IP-only flow (auto-discovery is hit-or-miss across Klipper distros). |
368368
| (Future) Prusa MK4 / MINI | PrusaLink HTTP (`:80`) | mDNS `_prusalink._tcp.local.` | `prusa-http` | Out of scope for first ship; add to the model JSON files later. |
369369

370+
#### 🔎 Multi-brand LAN discovery — unified "Scan network" *(can ship before the live drivers)* · **Effort: M** · **Risk: low**
371+
372+
Mirrors the mobile Flutter scanners on the desktop. Today **+ Add printer → Scan** only finds **Snapmaker** (mDNS `_snapmaker._tcp` + port-scan). Generalize it so a single scan surfaces printers of **every** brand on the LAN, each ready for one-click add — independent of whether that brand has a live driver yet (discovery → add → read-only card is already useful, and the live block lights up later as F2-F5 land).
373+
374+
**Per-brand discovery probe** — all already documented in `renderer/printers/<brand>/PROTOCOL.md` (those specs were derived from the Flutter scanners, e.g. `creality_scan_printers.dart`), so each one is a straight Dart→Node translation:
375+
376+
| Brand | Probe | Spec |
377+
|---|---|---|
378+
| Snapmaker | mDNS `_snapmaker._tcp.local.` (✅ done) | — |
379+
| Bambu Lab | SSDP `M-SEARCH ssdp:discover` → UDP multicast `239.255.255.250:1900` + active TLS cert sniff on `:8883` (cert subject/issuer contains `bambu`/`bbl`) | bambulab PROTOCOL §12 |
380+
| Creality | TCP `:9999` open → WS handshake → confirm `isCrealityLike` JSON (drop unconfirmed hosts) | creality PROTOCOL §scan |
381+
| Elegoo | UDP datagram to `:52700` per host → parse reply | elegoo PROTOCOL §13 |
382+
| FlashForge | HTTP `POST :8898/detail` probe + UDP multicast `225.0.0.9:19000` payload `"Hello World!"` (older Adventurer-era models) | flashforge PROTOCOL §2.1 / §2.6 |
383+
384+
> ⚠️ Spec drift to resolve from the Flutter source: the per-brand status table above lists FlashForge discovery as "UDP broadcast `48899` magic byte", while flashforge PROTOCOL §2.6 says multicast `225.0.0.9:19000` "Hello World!". Confirm against the Flutter scanner before implementing.
385+
386+
**Where the code goes** — all probes run in the **main process** (the Chromium renderer can't open raw UDP/TCP sockets), exposed to the renderer over IPC, exactly like the Snapmaker scan + Bambu MQTT already do:
387+
- UDP (SSDP, Elegoo, FlashForge multicast): Node `dgram`.
388+
- TCP / TLS cert sniff (Creality, Bambu): Node `net` / `tls`.
389+
- mDNS: `bonjour-service` (already a dependency since v1.4.8).
390+
391+
♻️ **Reuses (existing, exploitable)**:
392+
- `inventory.js` L8030-8226 — subnet enumeration + batched port-scan engine. Already brand-agnostic; feed it the union of brand ports and dispatch the matching probe per open port.
393+
- `bonjour-service` mDNS in `main.js` — browse extra service types (`_bambu._tcp`, `_octoprint._tcp`, …) alongside `_snapmaker._tcp`.
394+
- `snapAddDiscoveredPrinter()` (L8241-8633) — the one-click "write printer doc to Firestore + open detail" pattern; generalize to set `brand` from the matched probe.
395+
- Scan UI panel (`openSnapmakerScan` & friends, L8410+) — rename to a brand-neutral "Scan network" panel grouping candidates by brand.
396+
397+
**Merge / dedupe**: key by serial number when the probe returns one (Bambu SSDP, FlashForge), else by IP. Run all probes in parallel; merge into one list with a per-candidate confidence score (mDNS/SSDP > raw port-open). Same logic as the Bambu §12 dedupe (serial-or-IP, keep highest score).
398+
370399
#### Sub-features — recommended ship order
371400

372401
##### F1 — Driver interface extraction *(refactor)* · **Effort: M** · **Risk: low**

‎WORKLOG.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
# Worklog — v1.8.8 (in progress)
1+
# Worklog — v1.8.9 (in progress)
22

33
## Added
4+
- Creality LAN discovery in the "Add printer → Scan" flow — mirrors the mobile Flutter scanner and the existing Snapmaker/FlashForge desktop flows. Choice modal (Scan vs Manual), live LAN scan panel with one-click add, and manual-IP probe. Two-stage probe: fast TCP `:9999` port-open filter in the main process, then a renderer WebSocket handshake (`get printerInfo`, heartbeat-aware frame accumulation, `isCrealityLike` validation) — unconfirmed hosts are dropped. Common Creality subnets (192.168.1, 192.168.40) always scanned; user extra subnets persisted in localStorage so they survive a Restart scan. Logs a clear error in the debug scan log when the IPC bridge isn't loaded (app needs a full relaunch). `cre:tcp-probe` IPC + `creTcpProbe` preload bridge — `main.js`, `preload.js`, `renderer/printers/creality/probe.js`, `renderer/printers/creality/add-flow.js`, `renderer/inventory.js`
45

56
## Changed
7+
- Creality printer settings: `Root` account + password are now optional (were required). Most Creality printers — incl. the Ender-3 V4 — expose the WebSocket without auth and the live driver connects fine with empty credentials — `renderer/printers/creality/settings.js`
8+
- Friend (read-only) view now hides the write-action buttons (`+ Scan` and `Add product` / `Add device`) — they can't act on a friend's docs — `renderer/inventory.js` (`renderFriendBanner`)
69

710
## Fixed
811

9-
- **Bambu RTSP camera failed with "Option tls_verify not found" (no stream on bundled ffmpeg)** — `main.js`
10-
- The RTSP launch passed `-tls_verify 0` before `-i`, applied to the rtsp demuxer. Older ffmpeg (the bundled ffmpeg-static 6.0) has no `tls_verify` on the rtsp demuxer → ffmpeg exits code 1 when it reaches the TLS stage of a reachable printer. Homebrew ffmpeg 8.x has it, which masked the bug in dev before ffmpeg-static was bundled. Removed the flag — the tls protocol defaults to `verify=0`, so the printer's self-signed cert is still accepted (works on ffmpeg 6.0 and 8.x). This was the real reason the P2S camera showed no stream on Windows (and on macOS once it switched to the bundled binary).
11-
1212
## Removed
1313

1414
## i18n
15+
- Added: `creAddChoiceTitle`, `creScanEmpty` — 9 locales (all other scan UI strings reuse the shared `snap*` keys)

‎main.js‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,38 @@ ipcMain.handle('ffg:tcp-probe', async (_evt, ip) => {
13771377
});
13781378
});
13791379

1380+
// ── Creality TCP probe — port 9999 open/closed fast filter ───────────────────
1381+
// Mirrors ffg:tcp-probe above but is a pure reachability check: it only
1382+
// reports whether the WebSocket control port (9999) accepts a TCP
1383+
// connection. The renderer then opens a real WebSocket on each open host to
1384+
// run the Creality JSON handshake (isCrealityLike) — the browser WebSocket
1385+
// API works cross-origin without CORS, so that part stays in the renderer.
1386+
// Returns { ok: true } when the port is open, { ok: false, error } otherwise.
1387+
ipcMain.handle('cre:tcp-probe', async (_evt, ip) => {
1388+
if (!ip || typeof ip !== 'string') return { ok: false, error: 'missing ip' };
1389+
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) return { ok: false, error: 'invalid ip' };
1390+
1391+
const net = require('net');
1392+
const TIMEOUT_MS = 650;
1393+
const CRE_WS_PORT = 9999;
1394+
1395+
return new Promise((resolve) => {
1396+
let resolved = false;
1397+
const finish = (result) => {
1398+
if (resolved) return; resolved = true;
1399+
try { socket.destroy(); } catch {}
1400+
resolve(result);
1401+
};
1402+
1403+
const socket = new net.Socket();
1404+
socket.setTimeout(TIMEOUT_MS);
1405+
socket.on('connect', () => finish({ ok: true }));
1406+
socket.on('timeout', () => finish({ ok: false, error: 'timeout' }));
1407+
socket.on('error', (e) => finish({ ok: false, error: e.message }));
1408+
socket.connect(CRE_WS_PORT, ip);
1409+
});
1410+
});
1411+
13801412
// ── Snapmaker HTTP GET — main-process bridge (CORS bypass) ───────────────────
13811413
// Mirrors the FlashForge bridge above. The renderer's Chromium engine treats
13821414
// requests from http://localhost:<port> to http://192.168.x.x:7125 as cross-

‎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.8.8",
3+
"version": "1.8.9",
44
"description": "Tiger Studio Manager — desktop app",
55
"main": "main.js",
66
"engines": {

‎preload.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
181181
// Returns { ok, fields: { machineModel, machineName, firmware, serialNumber, macAddress } }.
182182
ffgTcpProbe: (ip) => ipcRenderer.invoke('ffg:tcp-probe', ip),
183183

184+
// ── Creality TCP probe — port 9999 open/closed fast filter.
185+
// Connects to ip:9999 (650 ms timeout) and reports whether the port is
186+
// open. The renderer then runs the WebSocket JSON handshake on each open
187+
// host to confirm it's a Creality printer (isCrealityLike). Returns
188+
// { ok: true } when the port accepts a connection, { ok: false, error } otherwise.
189+
creTcpProbe: (ip) => ipcRenderer.invoke('cre:tcp-probe', ip),
190+
184191
// ── Image cache (main-process side) ─────────────────────────────────────
185192
imgGet: (url) => ipcRenderer.invoke('img:get', url),
186193

‎renderer/CODEMAP.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,4 +358,5 @@ Most common navigation tasks → start here:
358358
- `printers/creality/widget_camera.js` → `renderCreCamBanner`, `startCreCam`, `stopCreCam` — direct RTCPeerConnection + `<video>` (port 8000); `_activeIp` guard prevents restart on WS reconnect; CSS in `55-creality.css`
359359
- `printers/flashforge/widget_camera.js` → `renderFfgCamBanner`, `renderFfgCamWallBanner`, `ffgRefreshCamBanner`, `ffgCamBaseUrl` — MJPEG sidecard/cam-wall banners (no src on `<img>` — mux provides frames)
360360
- `printers/flashforge/cam_mux.js` → `ffgMuxStart`, `ffgMuxStop`, `ffgMuxStopAll`, `ffgMuxRestart`, `ffgMuxRegister`, `ffgMuxUnregister` — single `fetch()` MJPEG multiplexer; distributes JPEG blob URLs to all `<img>` consumers; auto-stops when last consumer unregisters
361+
- **Per-brand LAN discovery** (Add Printer → Scan): Snapmaker, FlashForge and Creality each have a `probe.js` (pure network/data layer — no DOM) + `add-flow.js` (Scan/Manual choice modal, slide-in scan panel, manual-IP probe). `inventory.js` imports `openSnapAddFlow` / `openFfgAddFlow` / `openCreAddFlow` and dispatches by brand in `openPrinterBrandPicker` (~L10585). All three reuse the shared `snap*` scan-UI i18n keys + CSS (`snap-scan-*`, `snap-add-ip-*`, `snap-extra-subnets-*` in `40-printers.css`); only the brand title + empty-state strings are brand-specific. Raw socket probes run in the **main process** over IPC (`ffg:tcp-probe`, `cre:tcp-probe`, `net:get-local-subnets`, `snap:http-get`, `mdns:browse-snapmaker`) — the renderer can open WebSockets (Creality `:9999`, Snapmaker `:7125`) but not raw TCP/UDP. Creality probe is two-stage: main-process TCP `:9999` open check, then a renderer WebSocket `printerInfo` handshake validated with `isCrealityLike()`.
361362
- **Line numbers will drift** as the file changes. If a range looks wrong, grep for the anchor function name rather than trusting the L-number.

‎renderer/inventory.js‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { renderSnapCamBanner } from './printers/snapmaker/widget_camera.js';
4848
import { openSnapAddFlow } from './printers/snapmaker/add-flow.js';
4949
import { snapFanPct, snapFanStep, renderSnapControlCard } from './printers/snapmaker/widget_control.js';
5050
import { openFfgAddFlow } from './printers/flashforge/add-flow.js';
51+
import { openCreAddFlow } from './printers/creality/add-flow.js';
5152
import { renderCreCamBanner, startCreCam, stopCreCam, reAttachCreCamConsumers, addCreCamConsumer, removeCreCamConsumer } from './printers/creality/widget_camera.js';
5253
import {
5354
snapKey, snapGetConn, snapIsOnline,
@@ -10586,12 +10587,14 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
1058610587
btn.addEventListener("click", () => {
1058710588
const brand = btn.dataset.brand;
1058810589
closePrinterBrandPicker();
10589-
// Snapmaker and FlashForge have dedicated LAN-discovery flows
10590-
// (scan + manual IP). Other brands jump straight to the add form.
10590+
// Snapmaker, FlashForge and Creality have dedicated LAN-discovery
10591+
// flows (scan + manual IP). Other brands jump straight to the add form.
1059110592
if (brand === "snapmaker") {
1059210593
openSnapAddFlow();
1059310594
} else if (brand === "flashforge") {
1059410595
openFfgAddFlow();
10596+
} else if (brand === "creality") {
10597+
openCreAddFlow();
1059510598
} else {
1059610599
openPrinterAddForm(brand);
1059710600
}
@@ -12775,6 +12778,10 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
1277512778
// click handler reads the same state to decide whether to act as a
1277612779
// dropdown trigger or as a one-click "return home" button.
1277712780
$("sbUser")?.classList.toggle("sb-user--viewing-friend", !!state.friendView);
12781+
// A friend's inventory is read-only — hide the write actions (+ Scan and
12782+
// Add product / Add device) since none of them can act on a friend's docs.
12783+
$("btnAddScan")?.classList.toggle("hidden", !!state.friendView);
12784+
$("btnAddProduct")?.classList.toggle("hidden", !!state.friendView);
1277812785
if (!banner) return;
1277912786
banner.classList.remove("fvb--own", "fvb--error");
1278012787
// ─── Friend view ───────────────────────────────────────────────

‎renderer/locales/de.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,9 @@
572572
"snapAddChoiceManual": "IP-Adresse eingeben",
573573
"snapAddChoiceManualHint": "Lokale IP des Druckers manuell eingeben",
574574
"ffgAddChoiceTitle": "FlashForge-Drucker hinzufügen",
575+
"creAddChoiceTitle": "Creality-Drucker hinzufügen",
575576
"ffgScanEmpty": "Keine FlashForge-Drucker im Netzwerk gefunden",
577+
"creScanEmpty": "Keine Creality-Drucker im LAN gefunden",
576578
"snapManualSub": "Gib die lokale IP des Druckers ein — wir fragen ihn ab, um den Rest vorauszufüllen.",
577579
"snapManualProbe": "Abfragen + weiter",
578580
"snapManualBadIp": "Ungültige IP-Adresse.",

‎renderer/locales/en.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,9 @@
572572
"snapAddChoiceManual": "Enter IP address",
573573
"snapAddChoiceManualHint": "Manually enter the printer's local IP",
574574
"ffgAddChoiceTitle": "Add FlashForge printer",
575+
"creAddChoiceTitle": "Add Creality printer",
575576
"ffgScanEmpty": "No FlashForge printers found on your LAN",
577+
"creScanEmpty": "No Creality printers found on your LAN",
576578
"snapManualSub": "Type the printer's local IP — we'll probe it to pre-fill the rest.",
577579
"snapManualProbe": "Probe + continue",
578580
"snapManualBadIp": "Invalid IP address.",

0 commit comments

Comments
 (0)