Saltar al contenido principal

Print Bridge — Bidirectional Printer Status

Overview

Print Bridge now reports real printer telemetry (online/offline, paper, cover, cash drawer, identity) from TCP printers over ESC/POS.

USB printers: status polling is disabled in this release (Phase 2, pending libusb integration). The bridge reports statusSource: "disabled" for USB printers and continues printing normally.


Architecture

PrinterStatusManager          StatusBus
DLE EOT poll (per 5s) ──► DiagnosticsStore ──► SSE (browser)
GS I identity (once) ──► Socket.IO ──► backend printer:status event
ESC p drawer kick
  • PrinterStatusManager polls each TCP printer every 5 s with DLE EOT commands. Identity is queried once per connection lifecycle via GS I.
  • StatusBus fans out the results to three sinks: DiagnosticsStore (triggers SSE), Socket.IO printer:status event to the FlowPOS backend.
  • Fresh-socket-per-poll: each poll opens its own TCP connection to avoid conflicting with print job sockets. Most receipt printers only allow one connection at a time on port 9100; the poll naturally yields to in-progress print jobs (which fail the poll gracefully, keeping the last known status).

Status sources

ValueMeaning
telemetryReal data from hardware (DLE EOT response)
inferredNo telemetry — status inferred from write-path success/failure
disabledPolling disabled (USB printer or statusPollingEnabled: false)
unknownAgent just started, no data yet

Socket.IO event: printer:status

Emitted on the /restaurant namespace by each PrinterAgent to the FlowPOS backend.

Event name

printer:status

Payload schema

{
printerId: string; // UUID — bridge-side printer instance
deviceId: string | null; // Bridge device UUID (from pairing)
businessId: string;
locationId: string;
stationIds: string[]; // Kitchen station IDs served by this printer
statusSource: "telemetry" | "inferred" | "disabled" | "unknown";

// Present when statusSource === "telemetry"
online?: boolean;
paperLoaded?: boolean; // false → out of paper
paperLow?: boolean; // true → near-end sensor triggered
coverOpened?: boolean;

// Present once after first successful connection (null until fetched)
identity?: {
manufacturer: string | null;
model: string | null;
serialNumber: string | null; // Redacted: only last 4 chars
firmwareVersion: string | null;
} | null;
}

Security notes

  • deviceToken is never included.
  • serialNumber is redacted to its last 4 characters before emission (e.g. "****1234").
  • Strings from the printer are sanitized (non-printable chars stripped, max 128 chars) before storage or emission.

REST endpoints

GET /api/status

Returns the full DiagnosticsSnapshot including new telemetry fields per printer:

{
statusSource: "telemetry" | "inferred" | "disabled" | "unknown";
paperLow: boolean | null;
coverOpened: boolean | null;
printerIdentity: {
manufacturer: string | null;
model: string | null;
serialNumber: string | null;
firmwareVersion: string | null;
} | null;
// ... existing fields unchanged
}

POST /api/printers/:id/drawer/open

Opens the cash drawer for a TCP printer.

  • Auth: Bearer token required (same as all other authenticated endpoints).
  • Rate limit: 1 kickout per second per printer (HTTP 429 on excess).
  • Conditions: Returns 409 if the printer is not a TCP printer or has no URL.
POST /api/printers/<printerId>/drawer/open
Authorization: Bearer <token>

→ 200 { "ok": true }
→ 404 { "error": "Printer not found" }
→ 409 { "error": "Cash drawer control not available for this printer" }
→ 429 { "error": "Too many requests" }

GET /api/devices/usb

Now includes usbDeviceClass on each device entry:

[
{
path: string; // e.g. "/dev/ttyUSB0"
stable: boolean;
usbDeviceClass: "printer" | "serial" | "unknown";
}
]

SSE events

The /events stream sends type: "status" on every DiagnosticsStore change. The payload now includes all new telemetry fields. No new event type was added — existing SSE consumers will receive the extended state transparently.


Per-printer configuration

Config fieldTypeDefaultDescription
statusPollingEnabledbooleantrueSet false to disable status polling for a misbehaving printer
statusPollIntervalMsnumber5000Poll interval in ms (range: 1000–60000)
usbDeviceClass"printer" | "serial" | "unknown"auto-detectedUSB device class; set at pair time

Graceful degradation

ConditionOutcome
5 consecutive poll failuresstatusSource: "disabled", polling stops
statusPollingEnabled: falsestatusSource: "disabled", no polling started
connectionType: "usb"statusSource: "disabled" (Phase 2 pending)
Single poll failureprinterStatus: "offline", retries next interval
Identity query failsprinterIdentity: null, printing unaffected
Drawer kickout on USBHTTP 409 (not supported)

Backend integration (out of scope for this release)

The backend does not yet consume printer:status. This page documents the payload contract so the backend listener can be implemented separately. A listener on the /restaurant Socket.IO namespace should:

  1. Validate businessId + locationId from the payload against the authenticated device session.
  2. Persist or forward status to any interested subscribers (e.g. FlowPOS dashboard).
  3. Optionally emit a printer:status:ack back to the bridge (currently ignored).