Skip to content

Cash drawer auto-open: harden across printer languages, connectors, and transports #601

Description

@kilbot

Background

Follow-up to #600 (fix: honor auto-open cash drawer for thermal templates and raster). That PR made the autoOpenDrawer profile toggle actually fire the drawer pulse on the two client-rendered paths that were silently ignoring it (custom thermal templates + full-receipt raster). It did not address the broader question the fix surfaced: do we open the cash drawer correctly across every printer language, connector wiring, and transport we support?

This issue tracks hardening the cash-drawer feature so it's reliable on real hardware. It also captures a researched reference of ESC/POS / Star / Epson drawer-command behaviour so support and future work have a single source of truth.

What already works (verified)

  • Language-correct command bytes. The drawer kick is encoder.pulse() from @point-of-sale/receipt-printer-encoder v3.0.3, built with the profile's resolved language. Verified in the installed dist:
    • esc-pos1B 70 m t1 t2 (ESC p, on/off ÷2ms, capped 500)
    • star-prnt / star-line1B 07 n1 n2 <07|1A> (ESC BEL + drive byte, on/off ÷10ms, capped 127)
      So Epson and Star printers get their own drawer command, not a blind ESC/POS sequence. Selecting the Star vendor sets language: 'star-line' (add-printer.tsx), so a correctly-configured Star profile already kicks.
  • Native adapters pass raw bytes through. EpsonNativeAdapter.printRawaddCommand(data) + sendData(); StarNativeAdapter.printRawprintRawData([...]). The pulse bytes reach the printer on Bluetooth/USB native.
  • Ordering fixed in fix(printer): honor auto-open cash drawer for thermal templates and raster #600. The auto-pulse is emitted before the trailing <cut/> (matching the built-in template). Some firmware ignores/defers a kick that arrives after GS V — see appendix.
  • Star CloudPRNT is client-rendered (cloud-adapter.ts octet-stream path), so fix(printer): honor auto-open cash drawer for thermal templates and raster #600 covers it.

Gaps / tasks

P1 — correctness on real hardware

  • Drawer connector / channel is not configurable. We always call encoder.pulse() with no args → device=0pin 2 (ESC/POS) / drawer 1 (Star). Drawers wired to pin 5 or a second drawer / connector 2 can never be opened. Add a per-profile drawerConnector (pin 2 / pin 5, i.e. pulse(0|1)) — default pin 2 (most drawers ship on pin 2). Thread it into renderEscpos's drawer emit and the <drawer> element. Buzzer-equipped Epson models steal pin 5 for the buzzer, so the default must stay pin 2.
  • Verify the native Star path actually kicks. printRawData sends raw Star ESC BEL bytes — confirm on a real Star (TSP650II / mC-Print) that this opens the drawer, vs needing the StarXpand DrawerBuilder().actionOpen(Channel.No1) SDK call. If raw bytes are unreliable through react-native-star-io10, route the drawer via the SDK's drawer builder for native Star.
  • Emulation/language mismatch guard (Star). ESC p sent to a Star printer in Star mode (or ESC BEL sent to a Star printer in ESC/POS-emulation mode) is a no-op — the drawer stays shut. Our vendor→language default helps, but if a user's Star printer is in ESC/POS emulation while the profile says star-line, the kick fails silently. Surface this in the printer dialog / test print, or detect+warn. (star-line vs star-prnt does not matter for the drawer — identical bytes.)

P2 — reliability

  • Connection-close tail-drop. On raw TCP 9100 / Bluetooth, the last bytes in the stream can be lost if we disconnect() before the printer drains its buffer. fix(printer): honor auto-open cash drawer for thermal templates and raster #600 helps the drawer (it's now before the cut, so the cut is the tail), but audit NetworkAdapter / device adapters to ensure we flush/drain (ideally gate on a DLE EOT status reply) before closing. Otherwise we just moved the tail-drop from the drawer to the cut.
  • Consider the real-time kick (DLE DC4 fn=1 = 10 14 01 m t). Fires immediately regardless of buffering and is the recommended path for guaranteed opens / "no-sale" opens. The encoder only emits buffered ESC p; evaluate whether a real-time option is worth adding (esp. for a dedicated "open drawer" button).
  • Optional: configurable pulse width. We use library defaults (ESC/POS 50ms on / Star 200ms on). Heavier solenoids occasionally need a wider pulse. Low priority — only if a user reports a weak/non-latching drawer.

P3 — coverage

  • "Open drawer" button in the Add/Edit Printer dialog footer (Option A — agreed, ready to build). Sends just the drawer kick so users can verify wiring / pin / emulation without ringing a sale, and doubles as a "no-sale" open. Decided design:
    • Footer order: Open drawer | Test print | Save — the new button is leftmost. No icon (text only).
    • New service method mirroring testPrint, e.g. in packages/printer/src/printer-service.ts:
      async openDrawer(profile: PrinterProfile): Promise<void> {
        return this.queue.add(async () => {
          const transport = await this.getTransport(profile);
          const bytes = encodeThermalTemplate('<receipt><drawer /></receipt>', {}, {
            language: profile.language,        // language-correct pulse (ESC p vs Star ESC BEL)
            columns: profile.columns,
            printerModel: profile.printerModel,
            emitEscPrintMode: profile.emitEscPrintMode ?? true,
          });
          await transport.printRaw(bytes);     // just the kick, no receipt
        });
      }
    • UI wiring mirrors the existing Test Print path: add the button to dialog/printer-dialog-footer.tsx (leftmost) + a handleOpenDrawer callback in dialog/use-printer-dialog-form.ts that builds the profile from current form values (works pre-save, exactly like handleTestPrint). Add a stable testID (e.g. add-printer-open-drawer-button) per the repo E2E selector policy — no localized-text selectors.
    • Availability: show only for raw-capable transports — network, bluetooth, usb, and Star CloudPRNT (client-rendered). Hide/disable for connectionType === 'system' and order-based cloud (isOrderBasedCloudProfile → Epson SDP / PrintNode), which reject raw client bytes.
    • Errors: surface failures like Test Print does (e.g. the existing TestPrintError pattern) — "No USB printer selected", connection refused, etc.
    • Reuses the connector default (pin 2 / drawer 1) until the P1 connector selector lands; once it does, the button should honor the selected connector.
  • Test-print drawer check (optional, separate). Also fire a drawer kick from the existing Test Print when autoOpenDrawer is on. Lower priority than the dedicated button above.
  • Cloud server-rendered providers don't honor the setting (client + server). Epson SDP and PrintNode are order-based (isOrderBasedCloudProfile); the client sends no rendered bytes, so autoOpenDrawer never reaches the renderer.
    • Client (this repo) — required, blocks the server work: order-based cloud jobs must SEND autoOpenDrawer (and drawerConnector) in the /print-jobs request. Today use-cloud-enqueue.ts POSTs only { printer_id, order_id, template_id } and printOrderViaCloud → enqueueOrder carries only { orderId, templateId } — thread the drawer fields through that chain. Until this lands, the server-side work below is inert (its tests pass but no drawer opens).
    • Server (woocommerce-pos): Epson <pulse> + PrintNode raw drawer-kick, tracked in Cloud print (Epson SDP / PrintNode): honor auto-open cash drawer in server-rendered jobs woocommerce-pos#1169 (plan reviewed). Note for parity: explicit <drawer/> stays pin 2 there to match this client; only the auto-injected pulse honors the connector. Automatic auto-print rules are not yet covered server-side.
  • All-in-one devices (Sunmi / iMin) open the drawer via a vendor SDK (openDrawer() / opencashBox()), not ESC/POS over a port. We don't integrate these today — out of scope, noted here so it isn't forgotten if we add such device support.

Appendix — drawer command reference (researched, citation-backed)

ESC/POS (Epson + ESC/POS-emulation):

  • ESC p m t1 t2 = 1B 70 m t1 t2 — buffered. m 0/48 = connector pin 2, 1/49 = pin 5. t1 ON, t2 OFF, unit ×2 ms (0–510 ms). If t2 < t1, OFF is clamped to ON. Industry-standard value: 1B 70 00 19 FA (pin 2, 50 ms on / 500 ms off).
  • DLE DC4 1 m t = 10 14 01 m treal-time (jumps the buffer). m 0=pin2/1=pin5, t 1–8, unit ×100 ms. Ignored while a pulse is already firing, in error state, or if disabled via GS ( D.
  • Drawer-after-cut: emit the kick before GS V; some firmware trims feed / drops the kick if it lands after the cut.
  • Status sense: DLE EOT 1 = 10 04 01, bit 2 = pin-3 level (open/closed depends on switch wiring).
  • Hardware: 24V / max 1A drive, solenoid ≥24Ω; wrong-voltage drawer (12V on 24V port or vice-versa) is a hardware mismatch no command fixes. Two drawers share the RJ12 (pins 4↔2 and 4↔5); can't fire both at once.

Star (Star Line Mode + StarPRNT — identical for drawers):

  • Set pulse: ESC BEL n1 n2 = 1B 07 n1 n2 (ON 10×n1 ms, OFF 10×n2 ms, default 20/20 = 200/200 ms).
  • Drive drawer 1: BEL = 07 (alias FS = 1C). Drive drawer 2: SUB = 1A (alias EM = 19, fixed 200/200 ms).
  • Our encoder emits 1B 07 n1 n2 07 (drawer 1) or …1A (drawer 2) — matches.
  • Do NOT confuse with the buzzer command ESC GS BEL = 1B 1D 07 … (Star warns it can damage a unit if used to drive a drawer).
  • TSP100 (futurePRNT) is raster/GDI — it does not interpret raw command bytes; the drawer only opens via the driver/SDK. "Works on TSP650, dead on TSP100."
  • ESC p (1B 70) to a Star printer not in ESC/POS emulation = no-op.

Cloud / server-rendered:

  • PrintNode: a PDF job cannot open the drawer. Needs a raw_base64 job carrying ESC/POS pulse bytes (no dedicated cash-drawer endpoint in the API), or a local OPOS/driver "open on print" setting we don't control.
  • Epson SDP: drawer is the <pulse> element in the ePOS-Print XML the server delivers.
  • Star CloudPRNT: cputil drawer-start / drawer-end option, or Star Document Markup [drawer] tag.

Citations: Epson ESC/POS Command Reference (ESC p, DLE DC4 fn=1, DLE EOT, GS ( D, GS V) via download4.epson.biz / Internet Archive; Epson TM-T88V Technical Reference Guide (drawer wiring, 24V/1A, DIP/buzzer); Star StarPRNT / Line Mode / Graphic Mode / ESC-POS Mode Command Specifications (starmicronics.com); Star Document Markup [drawer]; Star CloudPRNT cputil; StarXpand / react-native-star-io10 Cash Drawer; react-native-esc-pos-printer addPulse; PrintNode raw printing docs; Sunmi / iMin SDK drawer docs; @point-of-sale/receipt-printer-encoder pulse() source (NielsLeenheer/ReceiptPrinterEncoder).


Researched and filed via Claude Code. The appendix bytes were verified against primary vendor specs and our installed encoder dist; the gap list is grounded in the actual transport/adapter code in packages/printer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions