# 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](https://www.npmjs.com/package/dotenv). ```bash 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--.bin` — the exact raw bytes (reprintable). - `job--.txt` — the same bytes decoded as `latin1` (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: ```bash # 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). ```bash # 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: ```js 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 ```