π¨οΈ This app emulates a networked receipt printer to test your ESC/POS commands against.
- Cross-platform application (Avalonia 12 + SkiaSharp + .NET 10), runs on Windows, macOS and Linux
- Listens for ESC/POS commands over TCP/IP and (optionally) a serial port
- Logs commands and visually represents the resulting receipt(s)
- Renders 1D barcodes and 2D codes (QR, PDF417, DataMatrix, Aztec), bit images, and page mode
- Answers status queries (
DLE EOT,GS r, Automatic Status Back) β simulate paper-out, cover, drawer, offline and error states from the Printer state panel - Signals buzzer / cash-drawer events with a sound and on-screen toast
- Configure the TCP listen address/port and serial port live from the UI
- Export rendered tickets to PNG β all in one image, or one file per cut
- It support different text formattings in the same line, although a few combinations were tested.
Cross-platform fork. This project began as a cross-platform port of roydejong/EscPosEmulator (originally a Windows/WPF app). It has been migrated to Avalonia + SkiaSharp + .NET 10 so it runs on Windows, macOS and Linux, and extended with barcode/QR rendering and a serial transport. All credit for the original emulator goes to the upstream author.
π· This is an unfinished experiment. Use at your own risk and keep your expectations low. :)
Pre-built, self-contained apps (no .NET install required) are published on the Releases page:
| Platform | Artifact |
|---|---|
| Windows (x64) | CrossEscPos-win-x64.zip |
| Linux (x64) | CrossEscPos-linux-x64.tar.gz |
| macOS (Intel) | CrossEscPos-osx-x64.zip (.app bundle) |
| macOS (Apple Silicon) | CrossEscPos-osx-arm64.zip (.app bundle) |
Releases are produced by the Release GitHub Actions workflow on
each v* tag.
macOS first launch. The
.appis ad-hoc signed but not notarized (no paid Apple Developer ID). macOS quarantines anything downloaded from the internet, so on first launch you may see "CrossEscPos is damaged and can't be opened" (especially on Apple Silicon). Clear the quarantine flag once, then open it:xattr -dr com.apple.quarantine /path/to/CrossEscPos.app open /path/to/CrossEscPos.app(Right-click β Open also works once the quarantine is cleared.)
- .NET 10 Β· Avalonia 12 Β· SkiaSharp (rendering) Β· CommunityToolkit.Mvvm (MVVM)
- ZXing.Net (1D barcodes) Β· QRCoder (QR codes) Β· Ardalis.SmartEnum (class-based enums) Β· System.IO.Ports (serial)
- ESC-POS-.NET (the Monitor test client) Β· LibUsbDotNet (direct USB printing)
- Raw Text
- LF: Line feed
- CR: Carriage return
- ESC Commands:
- Initialize printer (
ESC @) - Toggle italic (
ESC 4/ESC 5) [possibly deprecated?] - Select font (
ESC M) - Select charset (
ESC R) - Select character table (
ESC t) - Select justification (
ESC a) - Select line spacing (
ESC 2/ESC 3) - Toggle emphasis (
ESC E) - Toggle underline (
ESC -) - Set print text mode (
ESC !) - Full cut (
ESC m) - Partial cut (
ESC i) - Print and feed n lines (
ESC d) - Print and feed paper (
ESC J) - Generate pulse / kick cash drawer (
ESC p m t1 t2) - Select character code table (
ESC t) β PC437/850/852/858/860/863/865/866/1252 remapped to Unicode - Beeper (
ESC ( A) - Bit image (
ESC *) β 8-dot and 24-dot inline raster - Page mode: select page / standard mode (
ESC L/ESC S), print area (ESC W), direction (ESC T), absolute position (ESC $) - User-defined characters (
ESC &/ESC %/ESC ?) β parsed & stored
- Initialize printer (
- Control characters:
- Buzzer / beeper (
BEL, 0x07) - Form feed (
FF) β prints the page in page mode - Cancel (
CAN) β cancels page data
- Buzzer / beeper (
- DLE (real-time) Commands:
- Real-time status (
DLE EOT n, n=1-4) - Real-time request / recover (
DLE ENQ) - Real-time cash-drawer pulse (
DLE DC4 1 m t)
- Real-time status (
- FS Commands:
- Print stored logo (
FS p n m) - Auto cut (
FS } 0x60 n)
- Print stored logo (
- GS Commands:
- Select character size
- Select cut mode and cut paper
- Paper eject (
GS e n [m t]) - Print raster image (
GS v 0 [m xL xH yL yH ...pixels]) - Print 1D barcode (
GS k) β UPC-A/E, EAN-13/8, CODE39, CODE93, CODE128, ITF, CODABAR (both function A & B forms) - Set barcode height / module width (
GS h/GS w) - Select HRI text position / font (
GS H/GS f) - Print 2D symbols (
GS ( k) β QR Code (cn=49), PDF417 (cn=48), DataMatrix (cn=54), Aztec (cn=55) - Status / transmit-back: paper & drawer status (
GS r), printer ID (GS I), Automatic Status Back (GS a) - Download bit image: define (
GS *) and print (GS /) - Set motion units (
GS P), absolute/relative vertical position (GS $/GS \) - Config (accepted/ignored): user setup (
GS ( E), print control (GS ( K), response request (GS ( H)
The emulator is bidirectional: status commands (DLE EOT, GS r, GS I) and Automatic Status
Back reply to the host over the same TCP/serial connection, driven by the Printer state panel
(right side) where you can simulate paper-out/near-end, cover open, cash-drawer open/closed,
offline, and error conditions. Like a real device, the emulator refuses to print while it isn't
ready (out of paper, cover open, offline, or in an error state) and shows a notification instead.
Barcodes and QR codes render inline on the receipt, with optional HRI text:
A few things remain partial or unimplemented:
- Page-mode coordinate system β page mode buffers output and rasterizes it on
FF, but absolute/relative positioning (ESC $,GS $,GS \) and print direction (ESC T) are accepted as no-ops rather than fully positioned. - User-defined glyph substitution β
ESC &glyphs are parsed and stored, but the inline text renderer still draws the font glyph rather than the custom bitmap. - MaxiCode / GS1 DataBar / Composite 2D symbologies (
GS ( kcn=50/51/52). - Graphics commands
GS ( L/GS 8 L(NV/raster graphics store-and-print) and otherESC *-family densities. - Real-time
DLE DC4functions other than the cash-drawer pulse (power-off, recover-and-cancel, buzzer). - Katakana / CJK code pages render as missing glyphs since the bundled Latin font has no such glyphs.
Contributions welcome β new commands follow the simple BaseCommand pattern in
EscPos/Commands and are registered in
EscPosInterpreter.RegisterCommands.
The emulator accepts ESC/POS data over two transports. Both can be changed live from the UI (left panel): pick a TCP listen address and port and Start/Stop the listener, or select a serial port + baud and Open/Close it (β³ refreshes the port list). The environment variables below set the initial values at startup:
| Variable | Default | Meaning |
|---|---|---|
ESCPOS_LISTEN_ADDRESS |
0.0.0.0 |
Initial TCP bind address (0.0.0.0 = all interfaces, 127.0.0.1 = localhost). |
ESCPOS_TCP_PORT |
9100 |
Initial TCP listen port. Set to off / 0 to start with TCP stopped. |
ESCPOS_SERIAL_PORT |
(unset) | Serial device to auto-open (e.g. /dev/ttys004, COM3). Unset = serial closed. |
ESCPOS_SERIAL_BAUD |
9600 |
Serial baud rate. |
ESCPOS_DEBUG_DUMP |
(off) | Set to 1 to dump every received payload to last_* files. |
ESCPOS_RENDER_BACKEND |
skia |
Render backend: skia (default) or imagesharp (managed). The --backend arg overrides it. |
Examples:
# (run is shorthand for: dotnet run --project src/CrossEscPos.App.Desktop)
dotnet run --project src/CrossEscPos.App.Desktop # TCP only, port 9100
ESCPOS_TCP_PORT=9200 dotnet run --project src/CrossEscPos.App.Desktop # TCP on 9200
ESCPOS_SERIAL_PORT=/dev/ttys004 dotnet run --project src/CrossEscPos.App.Desktop # TCP 9100 + serial
ESCPOS_TCP_PORT=off ESCPOS_SERIAL_PORT=COM3 dotnet run --project src/CrossEscPos.App.Desktop # serial onlyPick the render backend (for A/B testing the two rendering libraries) with --backend skia
(default) or --backend imagesharp (the managed, no-native backend). The active backend is shown in
the window title bar.
dotnet run --project src/CrossEscPos.App.Desktop -- --backend imagesharp # managed ImageSharp backend
dotnet run --project src/CrossEscPos.App.Desktop -- --backend skia # default SkiaSharp backendThe status panel shows the active TCP endpoint and serial port.
You don't need a USB serial adapter. Create a virtual serial bridge β a pair of linked ports β then point the emulator at one end and your POS application (or a shell) at the other. Bytes written to one end appear on the other.
macOS / Linux β using socat (brew install socat /
apt install socat). A helper script is included:
./scripts/serial-bridge.sh
# It prints a linked pair, e.g.:
# PORT A (emulator): /dev/ttys004
# PORT B (your app): /dev/ttys005
# Leave it running.Then, in two more terminals:
# Terminal 2 β run the emulator on port A
ESCPOS_SERIAL_PORT=/dev/ttys004 dotnet run --project src/CrossEscPos.App.Desktop
# Terminal 3 β send a receipt from "another app" on port B
cat test_receipt.txt > /dev/ttys005
# β¦or from your own program, just open /dev/ttys005 like a normal serial port
# (9600 8N1) and write ESC/POS bytes to it.The receipt appears in the emulator window. Because the interpreter is stateful across reads, fragmented serial writes (commands split across packets) are handled correctly.
Windows β install com0com and create a linked pair
(e.g. COM3 β COM4). Run the emulator with ESCPOS_SERIAL_PORT=COM3 and have your application
write to COM4.
Sending test jobs is the monitor's job β the emulator is the device, the monitor is the POS-side client that drives it over the wire (just like a real application would). The Monitor is shared by both heads: click Open monitorβ¦ to launch it β a window on desktop, an in-page overlay in the browser. Its test jobs and status display are identical; only the transport differs:
- Desktop (built on ESC-POS-.NET) β pick a transport:
- TCP/IP β connect to the emulator's listener (or any networked printer).
- Serial β pick a port + baud; pairs with the emulator's serial transport via a virtual port bridge.
- USB β print directly to a real USB printer selected from the connected-device list (by
VID:PID), via libusb. This is send-only (no status), and needs native libusb installed
(macOS
brew install libusb, Debian/Ubuntuapt install libusb-1.0-0; bundled on Windows) and the OS not already holding the device.
- Browser β sends over the SignalR proxy (the
CrossEscPos.Hosthub) to the in-page emulator, for a full round-trip without any native transport.
It then lets you exercise the target without writing any code:
- Print a sample receipt, all 1D barcodes, or QR / PDF417 / DataMatrix / Aztec.
- Send the full feature test receipt, open the cash drawer, buzz, or cut.
- Watch the printer status the emulator reports back: toggle paper-out / cover / drawer / offline in the Printer state panel and the monitor's status display updates live (via Automatic Status Back), confirming the emulator's status responses are wire-correct. When the printer isn't ready, the emulator drops the job and shows a notification, just like real hardware.
Toggling the emulator's Printer state panel pushes status to the monitor in real time β here the printer reports paper low and a recoverable error, so the monitor shows Not ready:
Each cut (ESC i / ESC m / GS V) starts a new receipt β a "page". The Export buttons in the
left panel save the rendered tickets as PNG:
- Export all (single image) β stacks every receipt into one tall PNG (a save dialog).
- Export each cut (folder) β writes one
receipt_NNN.pngper cut into a chosen folder.
The emulator is split into layered, independently-publishable packages so the rendering backend is
swappable and the core runs headless (and in the browser). The namespace is unified under
CrossEscPos.* (organized by feature/directory, not by package), so types resolve across assemblies.
| Package | Namespace(s) | Role | Depends on |
|---|---|---|---|
CrossEscPos.Abstractions |
CrossEscPos, CrossEscPos.Graphics |
Backend-agnostic rendering + printer contracts (IReceiptCanvas, IReceiptImage, IReceiptImageFactory, ITypefaceProvider, IImageEncoder, IReceiptPrintable, IPrinterResponder) |
β |
CrossEscPos.Core |
CrossEscPos.Emulator, CrossEscPos.EscPos, β¦ |
Headless ESC/POS interpreter, printer state machine, receipt document model, barcode/QR generation. ESC/POS code maps (2D families, code tables, barcode systems, status requests) are behaviour-carrying SmartEnums, not magic-number switches |
Abstractions, QRCoder, ZXing.Net, Ardalis.SmartEnum |
CrossEscPos.Rendering.Skia |
CrossEscPos.Rendering.Skia |
The default render backend (SkiaSharp). Swap it for another IReceiptImageFactory/ITypefaceProvider/IImageEncoder |
Abstractions, SkiaSharp |
CrossEscPos.Rendering.ImageSharp |
CrossEscPos.Rendering.ImageSharp |
A 100% managed render backend (ImageSharp) β no native dependency, so it runs in Blazor WASM without a native relink. Same output as the Skia backend | Abstractions, SixLabors.ImageSharp.Drawing |
CrossEscPos.Transports |
CrossEscPos.Transports |
TCP / serial / USB transports (desktop only) | Core, System.IO.Ports, LibUsbDotNet, ESC-POS-.NET |
CrossEscPos.Controls |
CrossEscPos.Controls |
Reusable Avalonia controls (ReceiptView, PrinterStatePanel) β host apps consume these. Backend-agnostic (no SkiaSharp dependency) |
Core, Avalonia |
Core carries no UI and no graphics-backend dependency, so the library works headless or in WASM.
The host (desktop, browser, or your own app) is the composition root: it picks a backend and injects it.
var imageFactory = new SkiaImageFactory();
var typefaces = new SkiaTypefaceProvider();
var printer = new ReceiptPrinter(PaperConfiguration.Default, imageFactory, typefaces);
printer.FeedEscPos(escPosBytes); // byte[] β ESC/POS is binary
using var image = printer.CurrentReceipt.Render(); // IReceiptImage
new SkiaImageEncoder().EncodePng(image, outputStream);π¦ Package usage guides: docs/packages/ β getting started, the core
emulator, rendering & custom backends (Skia + managed ImageSharp), the Avalonia controls, and the
transports. The desktop and browser apps are one shared Avalonia app (src/CrossEscPos.App).
π Full reference & guides: the project wiki, including a step-by-step Adding a render backend guide.
Requires the .NET 10 SDK (and the wasm-tools workload for the browser head:
dotnet workload install wasm-tools).
# Desktop app (Windows / macOS / Linux)
dotnet run --project src/CrossEscPos.App.Desktop
# Headless: ESC/POS bytes -> PNG, no UI, no Avalonia
dotnet run --project samples/CrossEscPos.Headless -- test_receipt.txt out.png
# Browser: the SAME app in the browser (Avalonia WASM head) β full parity with desktop
dotnet run --project src/CrossEscPos.App.Browser
# Web host: serves the browser app AND the SignalR broker on one origin, giving it TCP reception
# (browsers can't open raw sockets). First run publishes the WASM app into wwwroot (cached after).
# Browse to the printed URL; the emulator opens the TCP port itself.
dotnet run --project samples/CrossEscPos.Host
# Tests (unit + headless UI)
dotnet test CrossEscPos.slnxOne app, two heads. The desktop and browser apps are the same Avalonia application
(src/CrossEscPos.App) β the same views, view models, receipts, printer-state
panel, PNG export, and the Monitor test-client. Only the platform edges differ, injected via
IPlatformServices:
| Desktop head | Browser head | |
|---|---|---|
| Transports | TCP + serial | Web Serial + WebUSB + SignalR TCP proxy |
| Export | native save dialog | browser download (same Avalonia storage API) |
| Monitor | test-client window | in-page overlay (SignalR round-trip) |
A browser can't open a raw TCP listen socket, so the browser head connects over SignalR to
samples/CrossEscPos.Host β one ASP.NET Core host that serves the WASM app
and runs the broker. The emulator asks the host to open a TCP listener on the address:port you choose;
POS software connects there and its jobs bridge to the in-page emulator, with status flowing back. The
same hub carries the browser Monitor's jobs, so it round-trips against the in-page emulator too.
- Windows / macOS: no extra setup β native rendering libraries ship with the Avalonia packages.
- Linux: install the usual font/render native deps if they are missing, e.g.
sudo apt install libfontconfig1 libfreetype6(Debian/Ubuntu).
Two test projects under tests/:
CrossEscPos.Core.Testsβ emulation coverage. ESC/POS sequences are fed through the interpreter and the observable results (rendered draws, printer state, host responses, events) asserted: text and control characters, styling (emphasis/italic/size/justification), cutting and feeding, 1D/2D barcodes and bit images, status/transmit-back (DLE EOT,GS r,GS I,GS a), cash drawer and buzzer, real-time commands, code pages, page mode and not-ready handling β plus exactStatusByteBuilderbit layouts and theSmartEnumcode maps. Rendering goes to a synthetic backend, so the suite runs with no SkiaSharp (proving the core is headless).CrossEscPos.Controls.Testsβ headless Avalonia UI tests for the controls (two-way state binding, receipt image rendering).
dotnet test CrossEscPos.slnxReceipt text is rendered with JetBrains Mono, embedded in the
CrossEscPos.Rendering.Skia package (under
src/CrossEscPos.Rendering.Skia/Assets/Fonts/) so output
is identical across platforms and works in the browser sandbox (no file IO). JetBrains Mono is licensed
under the SIL Open Font License 1.1; the full license text is included at
src/CrossEscPos.Rendering.Skia/Assets/Fonts/OFL.txt. Per the OFL, the font is redistributed here under its
original license and "JetBrains Mono" is a trademark of JetBrains s.r.o. To swap in a different
monospace font, replace the receipt-mono*.ttf files (and keep its license alongside).
This program emulates a printer with the following specifications:
- 80mm paper width
- 72mm printing width
- 180x180dpi
- ASCII Font A/B: 12x24 pixels
- Automatic line feed



