# 0016. Printing

- Status: Accepted
- Date: 2026-10-27

## Context

Shops use many brands of thermal printer, mostly cheap ESC/POS models from China plus Epson,
and Sunmi Android terminals with built-in printers. Printer vendors ship SDKs, but several are
closed binaries or carry "all rights reserved" licences that conflict with the AGPL. Printers also
disagree on less common commands, and they go offline, run out of paper or get unplugged during
service.

## Decision

- OpenKedai builds ESC/POS bytes itself (`packages/escpos`) from a small command subset that
  common printers share. Each job starts by resetting the printer and leaving Chinese
  double-byte mode (`ESC @`, `FS .`); text is ASCII, with accents and symbols transliterated.
- Network printers get raw bytes over TCP port 9100 from our own native module (Network.framework
  on iOS, `java.net.Socket` on Android). Bytes go in 4 KB chunks with short pauses, and every
  connect and send has a timeout.
- Sunmi built-in printers go through Sunmi's SDKs (both Apache-2.0): `com.sunmi:printerlibrary`
  for the classic print service, with `com.sunmi:printerx` as the fallback for newer devices
  such as the P-series. No other vendor SDK is used: the Epson ePOS SDK and closed binaries such as
  `libPrinterSDK.a` are not AGPL-compatible, and raw TCP works on those printers anyway.
- QR codes print as images (`GS v 0`) by default, because not every printer has a QR command;
  the printer's own QR command is an option.
- Every printer's profile (characters per line, cutter, QR mode, drawer) is a setting with
  defaults by paper width (ADR 0014).
- All printing goes through a stored queue that sends one job at a time per printer, in order,
  and retries with growing delays. Checkout and kitchen actions only add jobs to the queue, so a
  printer can never block a sale (ADR 0002).

## Consequences

- No printer app or driver to install, and nothing proprietary in the build except Sunmi's
  Apache-2.0 libraries (listed in `THIRD_PARTY_NOTICES.md`).
- Chinese text needs a raster fallback or a printer font (M8); until then it prints as "?".
- Printer status queries (`DLE EOT`) aren't used yet. A failed connection is the offline signal.
- Before targeting Android 17 (SDK 37), the app must request `ACCESS_LOCAL_NETWORK`. Without it,
  network printing fails with timeouts that look like an offline printer.
