You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
star-prnt / star-line → 1B 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.printRaw → addCommand(data) + sendData(); StarNativeAdapter.printRaw → printRawData([...]). The pulse bytes reach the printer on Bluetooth/USB native.
Drawer connector / channel is not configurable. We always call encoder.pulse() with no args → device=0 → pin 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:
asyncopenDrawer(profile: PrinterProfile): Promise<void>{returnthis.queue.add(async()=>{consttransport=awaitthis.getTransport(profile);constbytes=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,});awaittransport.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).
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.
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 t — real-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 beforeGS 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).
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: cputildrawer-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-encoderpulse() 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.
Background
Follow-up to #600 (fix: honor auto-open cash drawer for thermal templates and raster). That PR made the
autoOpenDrawerprofile 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)
encoder.pulse()from@point-of-sale/receipt-printer-encoderv3.0.3, built with the profile's resolved language. Verified in the installed dist:esc-pos→1B 70 m t1 t2(ESC p, on/off ÷2ms, capped 500)star-prnt/star-line→1B 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.EpsonNativeAdapter.printRaw→addCommand(data)+sendData();StarNativeAdapter.printRaw→printRawData([...]). The pulse bytes reach the printer on Bluetooth/USB native.<cut/>(matching the built-in template). Some firmware ignores/defers a kick that arrives afterGS V— see appendix.cloud-adapter.tsoctet-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
encoder.pulse()with no args →device=0→ pin 2 (ESC/POS) / drawer 1 (Star). Drawers wired to pin 5 or a second drawer / connector 2 can never be opened. Add a per-profiledrawerConnector(pin 2 / pin 5, i.e.pulse(0|1)) — default pin 2 (most drawers ship on pin 2). Thread it intorenderEscpos's drawer emit and the<drawer>element. Buzzer-equipped Epson models steal pin 5 for the buzzer, so the default must stay pin 2.printRawDatasends raw StarESC BELbytes — confirm on a real Star (TSP650II / mC-Print) that this opens the drawer, vs needing the StarXpandDrawerBuilder().actionOpen(Channel.No1)SDK call. If raw bytes are unreliable throughreact-native-star-io10, route the drawer via the SDK's drawer builder for native Star.ESC psent to a Star printer in Star mode (orESC BELsent 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 saysstar-line, the kick fails silently. Surface this in the printer dialog / test print, or detect+warn. (star-linevsstar-prntdoes not matter for the drawer — identical bytes.)P2 — reliability
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 auditNetworkAdapter/ device adapters to ensure we flush/drain (ideally gate on aDLE EOTstatus reply) before closing. Otherwise we just moved the tail-drop from the drawer to the cut.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 bufferedESC p; evaluate whether a real-time option is worth adding (esp. for a dedicated "open drawer" button).P3 — coverage
Open drawer | Test print | Save— the new button is leftmost. No icon (text only).testPrint, e.g. inpackages/printer/src/printer-service.ts:dialog/printer-dialog-footer.tsx(leftmost) + ahandleOpenDrawercallback indialog/use-printer-dialog-form.tsthat builds the profile from current form values (works pre-save, exactly likehandleTestPrint). Add a stabletestID(e.g.add-printer-open-drawer-button) per the repo E2E selector policy — no localized-text selectors.connectionType === 'system'and order-based cloud (isOrderBasedCloudProfile→ Epson SDP / PrintNode), which reject raw client bytes.TestPrintErrorpattern) — "No USB printer selected", connection refused, etc.autoOpenDraweris on. Lower priority than the dedicated button above.isOrderBasedCloudProfile); the client sends no rendered bytes, soautoOpenDrawernever reaches the renderer.autoOpenDrawer(anddrawerConnector) in the/print-jobsrequest. Todayuse-cloud-enqueue.tsPOSTs only{ printer_id, order_id, template_id }andprintOrderViaCloud → enqueueOrdercarries 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).<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.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.m0/48 = connector pin 2, 1/49 = pin 5.t1ON,t2OFF, unit ×2 ms (0–510 ms). Ift2 < 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 t— real-time (jumps the buffer).m0=pin2/1=pin5,t1–8, unit ×100 ms. Ignored while a pulse is already firing, in error state, or if disabled viaGS ( D.GS V; some firmware trims feed / drops the kick if it lands after the cut.DLE EOT 1=10 04 01, bit 2 = pin-3 level (open/closed depends on switch wiring).Star (Star Line Mode + StarPRNT — identical for drawers):
ESC BEL n1 n2=1B 07 n1 n2(ON 10×n1 ms, OFF 10×n2 ms, default 20/20 = 200/200 ms).BEL=07(aliasFS=1C). Drive drawer 2:SUB=1A(aliasEM=19, fixed 200/200 ms).1B 07 n1 n2 07(drawer 1) or…1A(drawer 2) — matches.ESC GS BEL=1B 1D 07 …(Star warns it can damage a unit if used to drive a drawer).ESC p(1B 70) to a Star printer not in ESC/POS emulation = no-op.Cloud / server-rendered:
raw_base64job 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.<pulse>element in the ePOS-Print XML the server delivers.cputildrawer-start/drawer-endoption, 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 CloudPRNTcputil; StarXpand / react-native-star-io10 Cash Drawer; react-native-esc-pos-printeraddPulse; PrintNode raw printing docs; Sunmi / iMin SDK drawer docs;@point-of-sale/receipt-printer-encoderpulse()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.