Desktop Printing Troubleshooting
Operational runbook for generic ESC/POS printing on FlowPOS Desktop
(055-desktop-generic-printing), including follow-up advertisement and
bundle-update behavior from PR #746 /
PR #747.
Architecture: Desktop generic printing. Runtime: Desktop runtime. Local launch: Run locally.
Lab spikes, planted-defect procedures, and hardware-record writing stay in
specs/055-desktop-generic-printing/quickstart.md. This page is what to do
when a till misprints.
Scope
Primary codepaths:
apps/desktop/src-tauri/src/transport/— five kinds, retry boundaryapps/desktop/src-tauri/src/printers/mirror.rs—printer_mirrorapps/desktop/src-tauri/src/executor/— outbox state machineapps/desktop/src-tauri/src/discovery/— scan +printer_discovery_eventapps/desktop/src-tauri/src/verification/andbuild.rs— counting recordsapps/desktop/src-tauri/src/bridge/handlers/updates.rs— web bundle applyapps/backend/src/printer-config/— saved printers, claims, server designation
1) Symptom triage
| Symptom | Likely layer |
|---|---|
| Kind missing from Find printers on one OS | Not compiled for that OS (windows_spooler on macOS), or Registry did not load |
Kind present but handshake unverified_transports names it | Non-production build with FLOWPOS_UNVERIFIED_RELEASE; Find printers still offers it |
| "Requires newer version" after a shell update | Old web bundle; the native shell already knows the kind |
| Ticket printed twice | Retry after LostDuringWrite — transport boundary, never a merchant error |
| Station silently stops printing | Printer not ready for a minute → claims released; Windows queue paused |
| Serial "port in use" | Warm session of a held station owns the exclusive serial port |
| Direct USB fails on Windows | WinUSB not bound; setup screen driver-step text |
| Held Windows queue printed later, runtime also retried | Runtime must not cancel or reprint; look for a boundary defect |
About → Check for updates did nothing / status: error | NeedsNewerShell, unsigned/dev session, or webVersion older than this installer |
| Scan finds nothing | OS permission, radio off, USB driver — not a missing hardware-verification record |
2) Advertisement vs verification
Do not treat a missing hardware-verification/records/{kind}.{platform}.json as
"this till cannot print over that kind."
| Question | Answer in this build |
|---|---|
| What does Find printers offer? | Every compiled merchant-selectable kind (Registry::advertised()) |
What does the handshake transports list? | The same list |
| What do counting records do? | Honesty: FLOWPOS_VERIFIED_KINDS, crash reports, flowpos-hwcheck |
| What hides a kind? | It is not compiled for this OS, or an older web bundle does not know the kind name |
cargo test --test hardware_gate is still useful: OFFERED-WITHOUT-RECORD: and
NOT-COUNTING: explain lab evidence. They do not explain a merchant scan.
3) Retry boundary (duplicates)
Before the first byte reaches the printer, a failure is Connection or Timeout
and the executor retries. At or after that moment the failure is LostDuringWrite
and the job goes to needs_attention — never an automatic retry.
| Kind | The moment the runtime can no longer prove nothing printed |
|---|---|
tcp | first byte handed to the socket |
ble | first chunk handed to the link (MTU − 3, twenty bytes if nothing negotiated) |
serial | first byte handed to the exclusive port; done only after drain |
windows_spooler | first WritePrinter — the queue's bytes are the printer's |
usb | first bulk transfer beginning |
When a ticket printed twice:
- Status window / tray attention count, job
attempt_phase. outbox_counter.double_completionsif the counter exists on that build.- Transport mapping to
LostDuringWrite— a duplicate is a boundary defect.
Never advise "just retry" on needs_attention without the duplicate-risk
acknowledgement the runtime requires.
Windows queue extra rule: after submit, the job is polled every 500 ms for up to
30 s. Error or still present at the bound is LostDuringWrite naming the Windows
job id. The runtime never cancels, restarts, or deletes that job. Crash during
the wait uses spooler_recovery.
4) Mirror, claims, and silent stations
The runtime copies saved printers into printer_mirror (beside the outbox) so
routing works with no web app present. It is never the source of truth.
Stations claimed automatically: printers bound to this installation (Bluetooth, serial, installed queue, direct USB). Shared network printers are claimed only when this machine is the designated server.
A printer that is not ready for a minute releases its stations. Check:
- tray attention count
printer_mirror.last_ready_state- backend
kds_device_station_claimrows for thisinstallation_id
Paused / offline / error / out of paper / door open on a Windows queue refuses
before submit (Connection) so the executor can retry. After submit, do not
expect the runtime to print a second copy when the printer returns.
5) Serial and USB
- Serial ports are opened exclusively. "Port in use" usually means this machine already holds a station on that printer, or another program has the port. Release the station or close the other program.
- Direct USB on Windows needs WinUSB bound to the device. The setup screen
driver-step text is the merchant-facing instruction;
nusbwill not talk to the vendor driver.
6) Web bundle vs native shell
Two update paths, two keys — see Desktop runtime. Printing symptoms that look like "the kind disappeared" after an update are usually the web bundle.
| Path | Who | When it takes effect |
|---|---|---|
Unattended retrieve (spawn_unattended) | Background after the window is up | Stages pending.json; activates on next start |
About → Check for updates (updates.apply) | Merchant confirmed not mid-ticket | Activates now and reloads the WebView |
| Native installer | Tagged release | Next start of the new executable |
tauri dev reports development and does not fetch a signed bundle.
NeedsNewerShell: the bundle verified, was staged, and must not activate
because requires.min_bridge_contract or a required capability exceeds this
installation. Unattended logs
bundle: {version} needs a newer application. Merchant-initiated apply
returns status: "error" with the current webBuildVersion. The merchant
needs a newer desktop app, not another web refresh.
latest.json names a git SHA; the About chip is webVersion. Retrieve will
not activate a pointer whose webVersion is strictly older than this
installer, so Check for updates cannot walk 0.0.384 back to 0.0.383.
7) Telemetry (desktop scans)
After a scan, identifiers stay hashed; no name/host/MAC in findings.
SELECT
client_kind,
findings->'client' AS client,
findings->'unavailableKinds' AS unavailable_kinds
FROM printer_discovery_event
WHERE client_kind = 'desktop'
ORDER BY occurred_at DESC
LIMIT 1;
Expect client_kind = 'desktop', installation id / role / OS present, and
unavailableKinds only for kinds this OS cannot compile (for example
windows_spooler on macOS).
More survey queries: Installed-base view.
8) Verification checklist (merchant flows)
Use this when a till is in the room. Full lab procedure:
specs/055-desktop-generic-printing/quickstart.md.
- Settings → Printers → Scan: Ethernet, Windows queue (Windows), paired Bluetooth as serial or "found but unusable — no serial port", BLE, USB (Mac, or Windows with no queue for that device).
- Test print with the printer off: failure text must name the condition (busy port, paused queue, nothing answered).
- Complete a sale; pull the cable mid-receipt:
needs_attention, duplicate warning, no automatic second copy. - Spooler: power off after accept; within 30 s the job needs attention naming the Windows job id; power on prints the first copy only.
- Headless: pair, assign a bound printer, designate server for a shared network printer, close the POS window, restart. Tickets print once. Unplug bound printer → station released; plug back → re-claimed.