Printer Vendor Port
Source-backed guide for manufacturer resolution in
packages/printer-transport/src/vendor-profile.ts.
Use this page when adding a printer brand or diagnosing why a saved printer stays generic. For merchant APIs and station claims, see Printer Config. For drawer opens, see Cash Drawer.
Intent
Feature 052-mobile-generic-printing treated every printer as anonymous ESC/POS
over TCP or BLE. Feature 053-mobile-vendor-printing adds a vendor profile
registry: one code-defined table that says what each manufacturer supports,
over which connections, on which platform.
Resolution is vendor first, connection second. A Star printer on Ethernet
must go through Star's adapter, not a raw socket, because only the vendor path
returns paper/cover state and a drawer kick that works on models where ESC p
does not.
The registry lives in @flowpos-workspace/printer-transport and ships no
adapters. Choosing a transport is the caller's job
(apps/mobile/src/printing/transport-registry.ts on mobile;
Print Bridge still implements PrinterTransport for TCP/USB).
Do not describe StarIO10 identifiers, USB permission dialogs, or HMAC hashing as domain behavior. Those are adapter or telemetry concerns.
Hexagonal map
| Layer | Codepaths | Responsibility |
|---|---|---|
| Domain (shared port) | packages/printer-transport/src/vendor-profile.ts, transport.types.ts | PrinterVendor, VendorEvidence, resolveVendor(), VENDOR_PROFILES |
| Application | apps/backend/src/printer-config/application/vendor-resolution.service.ts | Backfill saved rows from stored evidence; never from displayName |
| Infrastructure | apps/mobile/src/printing/transports/*, apps/mobile/modules/printer-* | StarIO10, Android SPP/USB, generic TCP/BLE |
| Interfaces | printer-config/manage DTOs, mobile handshake vendors field | Persist the client's resolution; declare which SDKs this build loaded |
Profiles in this release
PrinterVendor is "generic" \| "star" \| "epson" \| "bixolon" \| "sunmi".
generic is the fallthrough, never a match target (match: []).
| Vendor | Status reporting | Drawer (model) | iOS transports | Android transports |
|---|---|---|---|---|
generic | No | Yes (ESC p) | tcp, ble | tcp, ble, spp, usb |
star | Yes | Yes | tcp, ble, usb | tcp, ble, spp, usb |
epson | Yes | Yes | tcp, ble, usb | tcp, ble, spp, usb |
bixolon | Yes | Yes | tcp, ble | tcp, ble, spp, usb |
sunmi | Yes | Yes | — | sunmi only |
supportsCashDrawer on the profile means the model family can pulse a
drawer. Whether one is wired to this printer is
printer_config.supports_cash_drawer (default false).
What actually loads in the mobile container
The registry can identify all five vendors. Adapters registered at startup
(registerVendorTransports()) are narrower:
| Adapter key | When it exists |
|---|---|
generic:tcp, generic:ble | Always (feature 052) |
generic:spp, generic:usb | Android, when the Expo modules load |
star:{tcp,ble,usb,spp} | When react-native-star-io10 loads |
There is no Epson, Bixolon, or Sunmi transport file in
apps/mobile/src/printing/transports/ in this tree. Those printers still
resolve and print on any generic connection they support (FR-012). sunmi has
no generic fallback — transportFor({ connectionKind: "sunmi" }) throws until
a Sunmi adapter registers.
A missing SDK is a first-class state, not a crash: Star adapters are lazy-required so Jest, Expo Go, and a build shipped without Star degrade to generic.
The container declares the intersection of VENDOR_PROFILES and
loadedVendors() on the handshake (describeVendorIntegrations()). The
backend does not keep a version→vendor map.
Resolution
resolveVendor(evidence, source) is total and synchronous. Unmatched evidence
returns { vendor: "generic" }. Order: sunmi, star, epson, bixolon.
Evidence (passive first)
| Field | Typical source | Notes |
|---|---|---|
usbVendorId | USB scan | Assigned IDs: Star 0x0519, Epson 0x04b8, Bixolon 0x0419 / 0x1504 |
macOui | ARP, bonded device, Bonjour | First three octets; LAA bit 0x02 is cleared so Ethernet A4:D7:3C… and Bluetooth A6:D7:3C… match the same Epson |
advertisedName | BLE / Bonjour / accessory | Device-announced names only — never a merchant-typed displayName |
bonjourTxt | Bonjour usb_MFG, ty | Printer-set |
deviceIdResponse | Bounded probe (GS I 65/66/67) | Only after passive resolution stays generic |
hardwareManufacturer | Terminal Build.MANUFACTURER | Resolves sunmi with no address |
A probe may overwrite a passive result. Passive must never overwrite a probe
(vendor_evidence on the row).
Saved-row backfill
VendorResolutionService.evidenceFromRow rebuilds evidence from
connection_kind, stable_identity (MAC-shaped only), and
connection_target.vendorId. It skips vendor_locked_generic rows so a
merchant who forced generic is not undone by a later sweep.
NULL vendor means "never resolved". 'generic' means "resolved,
unrecognised". Collapsing those two would re-probe every no-name printer
forever.
Adding a manufacturer
Follow the existing extensibility test in
packages/printer-transport/__tests__/vendor-extensibility.spec.ts:
- Add the vendor to
PrinterVendorand a frozenVENDOR_PROFILESentry (match rules, transports, paper widths). - Add a lazy-loaded adapter under
apps/mobile/src/printing/transports/. - Register it from
registerVendorTransports()only when the native module is present. - Do not edit discovery merge, station claims, the outbox, or the setup screen — they consume the handshake declaration.
Printed bytes stay receipt-escpos. Vendor SDKs are passthroughs
(printRawData); they must not re-encode the slip.
Related
- Printer Config
- Cash Drawer
- Vendor printing troubleshooting
- Mobile printing limits
- Spec:
specs/053-mobile-vendor-printing/