Skip to content

Commit 5eb4b9b

Browse files
committed
docs: track FFUI drift and the TCP-probe discovery research
PORTING_TODO.md records what the 2026-07-10 SSH sync brought over from FlashForgeUI-Electron, the port-specific caveats (SecureStorage is base64-only here, passwords stay write-only over REST), and the known remaining drift -- notably the ifs-station -> material-station rename this repo has not taken yet. research-tcp-probe-discovery.md is the analysis behind 1fc1f1c: the UDP broadcast already carries the product ID and serial the probe was consulted for, so the probe is only required for genuine legacy printers. Both were written alongside shipped commits but never committed.
1 parent d6c6335 commit 5eb4b9b

2 files changed

Lines changed: 222 additions & 0 deletions

File tree

PORTING_TODO.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Porting TODO — drift from FlashForgeUI-Electron
2+
3+
Last sync: 2026-07-10 — the **SSH feature set** was ported from FlashForgeUI-Electron
4+
(`alpha` branch) in one pass. This file tracks what was ported and what has NOT
5+
yet been brought over, so future syncs know where the two codebases still diverge.
6+
7+
## Ported in the 2026-07-10 SSH sync
8+
9+
- **Types**: `src/types/{calibration,ssh-settings,file-manager,printer-power}.ts`
10+
- **Calibration engine**: `src/services/calibration/**` (engine, parsers, shaper,
11+
report, ssh) + its 8 Jest suites, `src/managers/CalibrationManager.ts`
12+
- **SSH stack**: `SSHSettingsService` (per-serial credential store,
13+
`ssh-settings.json` in the data dir), `SSHConnectionManager`/`SCPFileTransfer`
14+
(under `services/calibration/ssh/`), `FileManagerService` (SFTP listing /
15+
delete / rename / thumbnails), `PrinterRebootService` (reboot + reconnect
16+
monitor, REBOOT_STATUS WebSocket broadcasts)
17+
- **Routes** (registered in `api-routes.ts`): `calibration-routes`,
18+
`file-manager-routes`, `ssh-settings-routes`, `printer-power-routes`
19+
- **Static client**: `features/{file-manager,calibration,reboot,ssh}.ts` +
20+
`features/calibration/` (canvas visualizers + local types), topbar buttons
21+
(folder / gauge / power, hidden for unsupported models), the calibration and
22+
file-manager modals, the reboot confirm modal + progress overlay, the
23+
Settings → SSH section, `REBOOT_STATUS` handling in `core/Transport.ts`
24+
- **New deps**: `ssh2`, `pdf-lib`, `pngjs` (+ `@types/ssh2`, `@types/pngjs`)
25+
- **Utils**: `SecureStorage` (see caveat below), `isRebootSupportedModel` in
26+
`PrinterUtils`
27+
28+
### Port-specific caveats
29+
30+
- **`SecureStorage` is base64-only here.** The desktop app encrypts SSH
31+
passwords with Electron `safeStorage` (`enc:` prefix); this Node/pkg build has
32+
no OS keychain, so passwords are stored `plain:`-prefixed base64. An
33+
`ssh-settings.json` copied from the desktop app with `enc:` passwords resolves
34+
those to the easy-SSH default.
35+
- **Passwords are write-only over the REST surface** (`GET /api/ssh-settings`
36+
returns only a `passwordIsCustom` flag). Keep this invariant when touching the
37+
routes.
38+
- SSH features target **Adventurer 5M / 5M Pro / AD5X only** (flashforge-easyssh
39+
provisioning, default `root`/`flashforge`). Creator 5 / 5 Pro are intentionally
40+
unsupported for now.
41+
42+
## NOT yet ported (known drift from FlashForgeUI-Electron)
43+
44+
1. **`ifs-station``material-station` rename.** FFUI renamed the grid
45+
component (with saved-layout auto-migration) and made the filament palette
46+
per-printer-family (AD5X vs Creator 5) with CIEDE2000 color snapping in a
47+
shared `material-station` card. This repo still uses
48+
`features/ifs-station.ts`, `shared/ifs-palette.ts`, and the
49+
`ifs-station` component id.
50+
2. **Newer FFUI static client.** FFUI's WebUI has grown: theme *profiles*
51+
(save/load named themes) in settings, updated component registry entries
52+
(`creator5-temperature` behaviors), camera bootstrap fixes, icon-hydration
53+
fixes covered by its browser Playwright suite. Diff
54+
`src/main/webui/static/**` against `src/webui/static/**` when syncing.
55+
3. **Server route drift.** FFUI has additional/updated route modules (material
56+
station slot config, debug routes, updated spoolman/camera routes). Only the
57+
four SSH-feature route files were synced.
58+
4. **Shared-type drift.** FFUI's `@shared/types` have evolved (web-api types,
59+
material station, polling payloads). This repo duplicates them under
60+
`src/types/` and they are NOT auto-synced.
61+
5. **slicer-meta / ff-api versions.** Check dependency versions against FFUI
62+
when syncing job-upload/discovery behavior.
63+
6. **Pre-existing `npm audit` findings.** 16 vulnerabilities (3 critical)
64+
reported at install time, unrelated to the SSH port — needs its own pass.
65+
66+
## Validation commands
67+
68+
`npm run type-check``npm run build``npm run lint``npm test`
69+
`npm run docs:check` (all passing as of the 2026-07-10 sync).
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Research: TCP-probe usage in the automatic discovery/connection path
2+
3+
**Scope:** Standalone FlashForgeWebUI (web UI server + headless CLI). Research only — no code changed.
4+
**Question:** Can the TCP M115 probe be removed from the *automatic UDP-discovery → connect* path for modern printers, keeping it only for genuine legacy printers?
5+
6+
**Short answer:** **Yes, for modern printers.** The UDP broadcast already carries the two things the TCP probe is actually used for on modern printers — the USB product ID (authoritative model identity) and the serial number. The TCP probe is only *required* for genuine legacy printers (140-byte broadcast, no productId/serial), where the probe socket is also reused as the runtime control channel.
7+
8+
---
9+
10+
## 1. UDP broadcast vs TCP probe — what each provides
11+
12+
### UDP broadcast (`src/services/PrinterDiscoveryService.ts`, `parsePrinterResponse`)
13+
- **Modern packet (≥276 bytes)**`PrinterDiscoveryService.ts:257-273`:
14+
- `name``readNullTerminatedAscii(0x00, 132)` (L258)
15+
- `serialNumber``readNullTerminatedAscii(0x92, 130)` (L259)
16+
- `commandPort``readUInt16BE(0x84)` (L265)
17+
- `eventPort``readUInt16BE(0x8e)` (L266)
18+
- **`productId``readUInt16BE(0x88)`** (L269) — the USB product ID, authoritative for model identity
19+
- **Legacy packet (140 ≤ len < 276)**`PrinterDiscoveryService.ts:275-284`:
20+
- `name``readNullTerminatedAscii(0x00, 128)` (L275)
21+
- `commandPort``readUInt16BE(0x84)` (L281)
22+
- `serialNumber: ''` (hardcoded empty, L280) — **no serial, no productId, no eventPort**
23+
24+
### TCP M115 probe (`FlashForgeClient.getPrinterInfo()``PrinterInfo`)
25+
Source of truth: `node_modules/@ghosttypes/ff-api/dist/tcpapi/replays/PrinterInfo.d.ts` and `FlashForgeClient.d.ts:195`.
26+
Fields: `TypeName`, `Name`, `FirmwareVersion`, `SerialNumber`, `Dimensions`, `MacAddress`, `ToolCount`.
27+
28+
**Which of those the app actually consumes** (`createTemporaryConnection`, `ConnectionEstablishmentService.ts:129-308`): only `TypeName` (L228), `Name` (L233), `SerialNumber` (L234). `FirmwareVersion` is dead — it appears **only** in the type defs (`src/types/printer.ts:85,95`) and is never read for any branching decision (confirmed by grep). `Dimensions`/`MacAddress`/`ToolCount` are unused. The probe also returns a `_reuseableClient` (L248) — but **only in the legacy branch** (`!familyInfo.is5MFamily`, L241).
29+
30+
---
31+
32+
## 2. Table — TCP-probe outputs, consumers, alternatives, verdict
33+
34+
| TCP-probe output | Used by (file:line) | Non-TCP alternative | Verdict |
35+
|---|---|---|---|
36+
| **`TypeName`** (model identity) | detection route `detectPrinterFamily` (`printer-detection-routes.ts:88`); headless `detectPrinterModelType` (`ConnectionFlowManager.ts:597,1262`); backend selection via `printerModel` (`PrinterBackendManager.ts:185,269`) | UDP `productId`@0x88 → `NEW_API_PRODUCT_IDS` (`PrinterUtils.ts:132-138`); post-pairing `client.isPro`/`isAD5X`/`info.Pid` (per CLAUDE.md) | **Removable for modern printers** (productId covers 5M/5M Pro/AD5X/Creator 5/5 Pro). **Required for legacy** (no productId in legacy packet). |
37+
| **`SerialNumber`** | FiveMClient auth `serial+checkCode` (`ConnectionEstablishmentService.ts:383-386`); saved-printer keying (`ConnectionFlowManager.ts:600-604,1260`); `/detect``/connect` handoff (`printer-detection-routes.ts:77-81`) | UDP `serialNumber`@0x92 (modern 276-byte packet only) | **Removable for modern** (broadcast carries it). **Required fallback** when broadcast serial is empty. |
38+
| **`Name`** (user-assigned) | display name (`ConnectionFlowManager.ts:590-593`) | UDP `name`@0x00 | **Removable** (broadcast carries it). |
39+
| **`FirmwareVersion`** | nothing | HTTP `/detail` after pairing | **Already removable** (dead code — unused). |
40+
| `Dimensions` / `MacAddress` / `ToolCount` | nothing | n/a | **Already removable** (unused). |
41+
| **`_reuseableClient`** (reusable TCP control socket) | `establishLegacyConnection` (`ConnectionEstablishmentService.ts:474-499`) | none — TCP **is** the legacy transport | **REQUIRED for legacy printers only.** |
42+
43+
---
44+
45+
## 3. The HTTP-only short-circuit (quoted verbatim)
46+
47+
`src/services/ConnectionEstablishmentService.ts:136-153`:
48+
49+
```ts
50+
// HTTP-only models (Creator 5 / 5 Pro) run no legacy TCP server, so the usual
51+
// TCP probe can't work. When discovery's USB product ID identifies such a model,
52+
// synthesize the type info from the discovery packet and skip the TCP probe.
53+
const idModelType = detectPrinterModelTypeFromId(printer.productId, '');
54+
if (isHttpOnlyModel(idModelType)) {
55+
const typeName = getModelDisplayName(idModelType);
56+
console.log(`[Connection] HTTP-only model detected via product ID: ${typeName}`);
57+
this.emit('printer-type-detected', { typeName, familyInfo: detectPrinterFamily(typeName) });
58+
return {
59+
success: true,
60+
typeName,
61+
printerInfo: {
62+
Name: printer.name,
63+
SerialNumber: printer.serialNumber,
64+
TypeName: typeName,
65+
} as unknown as ExtendedPrinterInfo,
66+
};
67+
}
68+
```
69+
70+
**Logic:**
71+
1. Compute `idModelType = detectPrinterModelTypeFromId(printer.productId, '')` — keyed **only** on productId (typeName arg is `''`).
72+
2. If `isHttpOnlyModel(idModelType)` (i.e. `creator-5`/`creator-5-pro`, `PrinterUtils.ts:98-102`), skip the TCP probe.
73+
3. Return synthesized result: `typeName` = display name (e.g. `"Creator 5"`); `Name` and `SerialNumber` copied straight from the discovery packet (`printer.name` / `printer.serialNumber` — the latter can be `''`).
74+
75+
The dual-API product IDs (5M/5M Pro/AD5X = 35/36/38) deliberately do **not** short-circuit — proven by `src/services/ConnectionEstablishmentService.test.ts:38-64` ("does not short-circuit for dual-API product IDs", productId 35). **This test is the exact line that a removal change would flip.**
76+
77+
---
78+
79+
## 4. Caller graph of the TCP probe (`createTemporaryConnection`)
80+
81+
Grep-confirmed consumers:
82+
- `/api/printers/detect``printer-detection-routes.ts:68` (frontend detect step).
83+
- `connectHeadlessDirect``ConnectionFlowManager.ts:1241` (the `/connect` route + `--printers=` CLI).
84+
- `connectToPrinter``ConnectionFlowManager.ts:573` (interactive/Electron-era flow; **not invoked by any HTTP route or `src/index.ts`** — vestigial in the standalone WebUI).
85+
- `establishLegacyConnection``ConnectionEstablishmentService.ts:478` (reached only for genuine legacy printers; reuses the probe socket).
86+
87+
**Startup path (`src/index.ts:124,159,177`) uses only `connectHeadlessFromSaved` / `connectHeadlessDirect`.** `connectHeadlessFromSaved → connectWithSavedDetails` calls `establishFinalConnection` directly (L944) — **no probe** for saved-printer reconnects. So the probe is *not* run at startup for already-saved printers; it only runs for the live discover/manual-direct path.
88+
89+
---
90+
91+
## 5. Creator 5 serial flow, end-to-end
92+
93+
### Case A — discovered Creator 5 **with** a serial in the broadcast (276-byte packet, serial@0x92 populated)
94+
1. Discovery returns `DiscoveredPrinter{ productId:40|41, serialNumber:"<SN>", name, ports }`.
95+
2. Frontend `connectToDiscoveredPrinter(ip, serial, …)``printer-discovery.ts:336`.
96+
- `POST /api/printers/detect` body `{ ipAddress, commandPort, httpPort, productId }`**serial is NOT forwarded** (`printer-discovery.ts:361-366`).
97+
- detect route builds `mockPrinter.serialNumber = ''` (body has no serial, `printer-detection-routes.ts:60`); `createTemporaryConnection` short-circuits on productId; returns `SerialNumber: ''`.
98+
- detect route returns `serialNumber: ''` (`printer-detection-routes.ts:77-81,99`).
99+
- Frontend: `detectedSerial = serialNumber || serial` = `'' || "<SN>"` = `"<SN>"` (`printer-discovery.ts:375`). ✓
100+
- `POST /api/printers/connect` body includes `serialNumber:"<SN>"`, `productId:40` (`printer-discovery.ts:403-421`).
101+
3. connect route: `serialNumber` is truthy → the http-only serial guard at `printer-management-routes.ts:66-76` passes → `connectHeadlessDirect([spec])`.
102+
4. Headless: `createTemporaryConnection` (mock has productId+serial) short-circuits; `probedSerial = spec.serialNumber` (`ConnectionFlowManager.ts:1254-1260`); model refined from `primaryClient.model` (`ConnectionFlowManager.ts:1303-1310`); `establishDualAPIConnection(httpOnly=true)` creates the FiveMClient with serial+checkCode (`ConnectionEstablishmentService.ts:389,394`).
103+
- **→ One-click connect works today when the broadcast carries the serial.**
104+
105+
### Case B — discovered Creator 5 **without** a serial (broadcast serial@0x92 empty/missing)
106+
1. Discovery returns `serialNumber:''` (modern packet) or the legacy packet (no serial field at all).
107+
2. `connectToDiscoveredPrinter(ip, serial='', …)`:
108+
- `/detect` returns `serialNumber:''`; frontend `detectedSerial = '' || '' = ''`.
109+
- `/connect` body: `serialNumber:''` → in the route:
110+
```ts
111+
// printer-management-routes.ts:58-76
112+
const serialNumber =
113+
typeof body.serialNumber === 'string' && body.serialNumber.trim() !== ''
114+
? body.serialNumber.trim()
115+
: undefined;
116+
if (
117+
typeof productId === 'number' &&
118+
isHttpOnlyModel(detectPrinterModelTypeFromId(productId, '')) &&
119+
!serialNumber
120+
) {
121+
return sendErrorResponse(res, 400, 'Serial number is required for Creator 5 series printers');
122+
}
123+
```
124+
- **400 "Serial number is required for Creator 5 series printers".**
125+
126+
The manual-connect path can never hit this 400, because `connectManually` requires a serial for any modern type (`printer-discovery.ts:480-489`) and supplies the productId hint (`MANUAL_PRODUCT_ID_HINTS`, `printer-discovery.ts:27-30,476`).
127+
128+
---
129+
130+
## 6. Recommendation
131+
132+
**TCP probing can be removed from the automatic discovery path for modern printers.** The minimal, low-risk change is to **generalize the existing HTTP-only short-circuit to all new-API product IDs**, keeping the TCP probe as the fallback for genuine-legacy/unknown cases:
133+
134+
1. **`src/services/ConnectionEstablishmentService.ts:139-153`**widen the guard from `isHttpOnlyModel(idModelType)` to "productId present and resolves to a known new-API model", i.e. roughly:
135+
`if (printer.productId !== undefined && printer.productId in NEW_API_PRODUCT_IDS) { …synthesize from productId + broadcast name + broadcast serial… }`
136+
This single change removes the TCP probe from `/detect` and `connectHeadlessDirect` for 5M/5M Pro/AD5X, exactly as it already does for Creator 5/5 Pro.
137+
2. **`src/services/ConnectionEstablishmentService.test.ts:38-64`**the "does not short-circuit for dual-API product IDs" test encodes the *current* (probe-runs) behavior and must be rewritten to assert synthesis for productId 35/36/38.
138+
3. **Optional polish (not required for correctness):** have the frontend discover path forward the discovered serial to `/detect` (`printer-discovery.ts:361-366`) so the detect response is self-contained. Not strictly needed`printer-discovery.ts:375` (`serialNumber || serial`) already falls back to the discovered serial.
139+
4. **Keep the TCP probe as the fallback** in `createTemporaryConnection` for: `productId === undefined`, `productId === 0`, or any value not in `NEW_API_PRODUCT_IDS` (genuine legacy + unknown). Legacy printers *must* keep itit is their runtime control channel (`_reuseableClient`, `ConnectionEstablishmentService.ts:241-250,474-499`).
140+
141+
**Nothing else forces TCP to stay** for modern printers. The runtime dual-API TCP socket (secondary `FlashForgeClient` for G-code, `ConnectionEstablishmentService.ts:427-452`) is unrelated to the *probe* and would remain; only the redundant *type-detection* TCP connection goes away.
142+
143+
### CLAUDE.md rationale reconciliation
144+
CLAUDE.md states the TCP-first M115 bootstrap is "correct and intentional" because `/detail` requires auth before pairing. That was true when the only pre-auth sources of model identity were `/detail` (auth-gated) and the TCP M115. The codebase has since added the **UDP `productId`@0x88** as a pre-auth, authoritative model-identity source (`NEW_API_PRODUCT_IDS`, `PrinterUtils.ts:132-138`) — the HTTP-only short-circuit already relies on it. So for the **UDP-discovery** path specifically, broadcast `productId` + broadcast serial make the probe redundant. The CLAUDE.md note's general point still holds for the auth-gated `/detail` and for manual-IP connects where no productId is supplied.
145+
146+
---
147+
148+
## 7. Unknowns needing firmware/hardware confirmation
149+
150+
1. **productId@0x88 reliability for dual-API printers**is it always populated and non-zero on 5M / 5M Pro / AD5X across firmware versions? (Already trusted for Creator 5.) Safe by fallback: a `0`/absent value falls through to the TCP probe, so a misconfigured field degrades gracefully rather than breaking.
151+
2. **broadcast serial@0x92 reliability**is it reliably populated for modern dual-API printers? If empty, the synthesized path returns `serial:''`; the discover path then degrades to the `Unknown-${Date.now()}` fallback (`ConnectionFlowManager.ts:613-616,1260`), which breaks saved-printer keying. Creator 5 already depends on this field being usable (the 400 in Case B).
152+
3. **Whether the synthesized display-name TypeName (e.g. `"Adventurer 5M Pro"`) stored as `printerModel` is acceptable** vs. the firmware string `"FlashForge Adventurer 5M Pro"`. The headless path refines it from `primaryClient.model` (`ConnectionFlowManager.ts:1303-1310`); the `/detect`-driven path does not. Cosmetic/consistency only`detectPrinterFamily`/`detectPrinterModelType` use substring matching and accept either.
153+
4. **Vestigial interactive flow**`startConnectionFlow`/`tryAutoConnect`/`connectToPrinter`/`connectDirectlyToIP` are not wired to any WebUI route or `index.ts`. Confirm they are truly dead before relying on that claim; if kept, widening the guard removes TCP there too (harmless).

0 commit comments

Comments
 (0)