escpos-proxy
Minimal ESC/POS print proxy written as a Node.js ESM app.
It listens for incoming ESC/POS print jobs on a TCP port, forwards the raw
bytes straight to a target printer, and logs every job to disk as both a
raw .bin file and a decoded .txt sidecar.
A job is delimited by either:
- a cut command ("bladecut") in the stream — ESC/POS
GS V=0x1D 0x56, or - a connection drop (the client closes / resets the connection).
No database, websocket, or receipt re-rendering is involved.
Usage
Configuration lives in .env (copy .env.example to .env and edit). It is loaded via dotenv.
cp .env.example .env # first time only
npm start
Environment variables (.env)
| Variable | Default | Description |
|---|---|---|
LISTEN_HOST |
0.0.0.0 |
Host the proxy listens on |
LISTEN_PORT |
9100 |
Port the proxy listens on |
TARGET_HOST |
10.10.10.24 |
Printer to forward raw bytes to |
TARGET_PORT |
9100 |
Printer port |
LOG_DIR |
./logs |
Directory for job logs |
CUT_PREFIX |
1d 56 |
ESC/POS cut command prefix, hex space-separated (GS V) |
KEEP_AFTER_CUT |
true |
Treat bytes after a cut as the next job |
Real environment variables always take precedence over .env.
Logs
For each completed job, two files are written into LOG_DIR:
job-<timestamp>-<seq>.bin— the exact raw bytes (reprintable).job-<timestamp>-<seq>.txt— the same bytes decoded aslatin1(ASCII + control characters visible, ESC/POS faithful).
Testing
test-send.mjs sends a sample ESC/POS receipt (with a GS V cut) to the
listener so you can verify logging and forwarding:
# terminal 1
npm start
# terminal 2
npm run test:send
Parsing the logs
parser.js is a lossless ESC/POS parser. It decomposes the raw byte stream
into the ESC/POS command grammar: each control sequence is recognized by its
opcode with its parsed parameters, and text is kept as raw byte spans (decoded
via the selected code page). Nothing is interpreted into higher-level meaning
(no "this is bold", no line/style inference) — it is a faithful tokenization.
Every byte is preserved, and the original stream can be rebuilt via
serialize(parse(buf)).
Token types:
{ type: "text", bytes, text }— raw bytes + code-page decode{ type: "command", bytes, group, name, params }— control sequence + params{ type: "image", bytes, width, height, data }— raster bit image (GS v 0)
parse-log.mjs is a CLI wrapper. By default it prints the parsed token stream
(opcode + command name + parameters, text spans, images) and verifies the
round-trip serialize(parse(buf)) === buf (no bytes lost).
# parsed token stream (default) — a faithful decomposition
npm run parse -- logs/job-2026-07-08T21-08-02-523Z-0003.bin
# parse every .bin in a directory
node parse-log.mjs logs --all
# also show the raw bytes of each token
node parse-log.mjs logs/job-...bin --hex
# convenience: decoded receipt text only (drops structure — not lossless)
node parse-log.mjs logs/job-...bin --text
# also render raster bit images (e.g. QR codes) as ASCII art
node parse-log.mjs logs/job-...bin --image --max-width 60
As a module:
import { parse, serialize, renderText } from "./parser.js";
const buf = fs.readFileSync("logs/job-xxx.bin");
const events = parse(buf); // lossless token stream
console.log(serialize(events).equals(buf)); // true — nothing lost