Saltar al contenido principal

Desktop generic printing

How FlowPOS Desktop prints to generic ESC/POS printers, and the handful of rules that explain most of the code.

Feature 055-desktop-generic-printing. Spec: specs/055-desktop-generic-printing/spec.md. Builds on the desktop runtime (054).

Five connection kinds, one boundary each​

Every kind is a Rust state machine over a small OS-port trait, so the retry boundary is testable against a scripted fake with no printer in the room. The real port is the only part that talks to the operating system.

KindPortThe boundary — the moment the runtime can no longer prove nothing reached the printer
tcpstd::netthe first byte handed to the socket
blebtleplugthe first chunk handed to the link (chunks are MTU − 3, twenty bytes when nothing was negotiated)
serialserialport, opened exclusivelythe first byte handed to the port; done only after drain
windows_spoolerwinspool (Windows only)the first WritePrinter — the queue's bytes are the printer's
usbnusbthe first bulk transfer beginning

Before that moment a failure is Connection or Timeout and the executor retries on its schedule. At or after it, the failure is LostDuringWrite and the job goes to needs_attention for a person — never an automatic retry, because a retry after the first byte is how a kitchen gets two tickets. No message string decides which side of the line a failure fell on; the transport's own flag does.

tests/transport_contract.rs runs one case list over every kind's fake, and tests/boundary_planted.rs rebuilds those fixtures with --cfg planted_retry_after_write and asserts they fail for every kind. A gate nobody has seen fail is a gate nobody has tested.

The held Windows queue​

An installed Windows printer is written RAW through the print queue. The queue accepts a job whether or not the printer is there, so "accepted" is not "printed":

  • Before submitting, the queue's status is read. Paused, offline, in error, out of paper or door open refuses the job as Connection("printer <state>") — nothing is submitted, and the executor retries.
  • After submitting, the job is polled every 500 ms for up to 30 s. Gone, or PRINTED/DELETED, is done. An error state, or still present at the bound, is LostDuringWrite naming the Windows job id. The queue may still print it when the printer returns, and the runtime never cancels, restarts or deletes a job.
  • Crash during the wait: the job id is persisted in spooler_recovery before the wait begins. On restart the queue is asked once — printed or needs_attention, never a retry.

The mirror and the claim set​

The runtime keeps a copy of the saved printers (printer_mirror, beside the outbox) so routing, claims and readiness work with no web application present. The till feeds it from printer.configure; the agent feeds it from GET /printer-config on every heartbeat. It is never the source of truth.

Which stations a paired machine claims falls out of the mirror:

  • stations of printers bound to this installation (Bluetooth, serial, installed queue, direct USB) — claimed automatically, because nothing else can reach them;
  • stations of a shared network printer — claimed only when the merchant designated this machine as that printer's server. A machine never claims a station because it happens to be able to reach the printer.

The held set is reconciled to that every tick; a station another device holds is yielded, not fought over. A printer that stops being ready for a minute releases its stations, so no station is held by a machine that cannot honour it, and re-claims when the printer is back.

Server designation​

A shared network printer has at most one designated server. Setting it replaces any existing designation; a backup machine is a deliberate re-designation, not a standing second one. Designation is recorded against the device record and cleared when that device is unpaired, retired or deleted. A printer with no server is served by the location-wide client, as before.

Hardware verification and the gate​

Compiled connection kinds are advertised whether or not a counting record exists. Find printers offers every merchant-selectable kind this binary can drive:

OSAdvertised kinds
Windowstcp, ble, serial, windows_spooler, usb
macOStcp, ble, serial, usb

A missing record does not hide USB, a Windows queue, BLE, or serial. windows_spooler on macOS is unavailable_on_platform because it is not compiled there — an OS fact, not a verification lock.

Records under apps/desktop/hardware-verification/records/ are lab evidence written by flowpos-hwcheck. build.rs bakes counting records as FLOWPOS_VERIFIED_KINDS for crash-report honesty. The handshake transports list is Registry::advertised() (compiled kinds). unverified_transports is only non-empty on a non-production build that set FLOWPOS_UNVERIFIED_RELEASE.

tests/hardware_gate.rs prints OFFERED-WITHOUT-RECORD: when a compiled kind has no counting record (pass), STALE-OTHER: when a counting record is merely old or from another crate version (pass), and still fails a planted wrong fingerprint or failing check. Editing a transport makes its record stale by fingerprint. That is the point: the gate is honesty, not a lock on offering the kind.

FLOWPOS_UNVERIFIED_RELEASE is a leftover honesty flag. It panics the production channel (FLOWPOS_CHANNEL=production). On internal / staging it lists kinds in unverified_transports and release notes; it does not change Find printers. FLOWPOS_DEV_UNVERIFIED is debug-only leftover of the same shape.

Older bundles​

A web bundle that predates a connection kind keeps the printer visible with its name and assignments, labels it as needing a newer version, and offers it nothing this version cannot honour. It is never dropped and never guessed at.