# POS895 / POS896 printer support: feasibility notes Date: 2026-09-02, updated 2026-09-03 after a hardware session. Status: several of the open questions below are now answered, and the vendor discovery protocol is reverse engineered and reimplemented. See `Pos895Test/`. Source material reviewed: `D:\Inspectron\James\epson\POS895\POS895&896 SDK\` (Linux SDK, Windows POS_SDK, EasySet tool, POS895 and POS896 user manuals, POS_SDK Development Manual V1.4). ## Verdict The POS895 and POS896 can be supported by this codebase, but **not by linking the vendor SDK**. The SDK is a thin C wrapper that builds ESC/POS byte strings and writes them to a raw TCP socket on port 9100. `Inspectron.Epson/EpsonPrinter.cs` already does the same thing in C#. The real work is: 1. a new `IPrinter` adapter for a 576-dot, 80 mm ESC/POS clone, 2. a discovery strategy that does not rely on Epson's ENPC broadcast, 3. a more robust printer identification than the single `GS I 1` byte, 4. a hardware probe session to close the unknowns listed below. Rough effort: 3 to 5 developer days once a printer is on the bench. No new deployment dependencies, no native code, no libusb. Restrict to the Ethernet models (POS895 UEd, POS896 UE); USB-attached operation is a separate project because the whole pipeline keys on an IP address. ## What the vendor SDK is Path: `Linux_SDK\Linux_SDK_20190110\Linux_SDK_64_group_ARM\Linux_SDK_64_group\` | File | What it is | |---|---| | `HWISPRTPrinter.c` / `.h` | ~5,500 lines of C. Transports: libusb, serial tty, TCP socket to port 9100. Every `POS_*` function fills a global 1 KB buffer with an ESC/POS sequence and writes it. | | `sprtGraphic.c` / `.h` | BMP loader and a proprietary line-bitmap encoder (see below). | | `libHWISPRTPrinter.so` | Prebuilt library. **32-bit Intel i386**, despite the folder name saying ARM. | | `libusb/` | Bundled libusb, **x86-64**. | | `testMain.c`, `CPCLtest.c`, `TSPLtest.c`, `CPCLDemo.c`, `GtestMain.c` | Demos. Only `testMain.c` is ESC/POS. | | `makefile` | Cross-compiles with `arm-linux-gcc`. The shipped binaries were not built with it. | | `README.txt` | Chinese, GBK encoded. Decode with `iconv -f GB18030 -t UTF-8`. The `.c` and `.h` files are UTF-8. | Reasons not to use it: - Global state only. One socket descriptor (`socketFd`), one command buffer (`gCmdBuf`), no printer handle. It cannot drive two printers concurrently; `PrintServer` does. - The shipped `.so` would not load on the Raspberry Pi. It would need cross-compiling from source, then P/Invoke from .NET, for zero functional gain over what `EpsonPrinter` already sends. - Roughly half the API is CPCL and TSPL label-printer commands. Irrelevant for receipts. - `pos-sdk\POS-SDK\POS-SDK_V1219\` is a Windows `POS_SDK.dll` with a WinForms P/Invoke demo (`Testing\C#\TestSDKCSharp\Form1.cs`). Irrelevant for the Linux service. Its manual (`POS_SDK Development Manual V1.4_Neutral.pdf`) is in English and is the best command reference in the package. - Development stopped around 2016 to 2019 for the Linux part. The firmware family is "SPRT" (the SDK is named `HWISPRTPrinter`, log prefix `qzf`). Treat the printer as a SPRT-derived ESC/POS clone. ## Printer facts from the manuals | | POS895 | POS896 | |---|---|---| | Paper | 80 mm (79.5 ±0.5), 72 mm printable | same | | Resolution | 203 dpi, 8 dots/mm, 576 dots/line | same | | Fonts | Font A 12x24 (48 columns), Font B 9x17, CJK 24x24 | same | | Interfaces | Ud: USB. UEd: USB + Ethernet | UBTDM/UBTMM: USB + Bluetooth + drawer. UE: USB + serial + Ethernet + drawer | | Command set | ESC/POS | ESC/POS | | Barcodes | UPC/EAN/CODE39/ITF/CODABAR/CODE93/CODE128, PDF417, QR, DataMatrix | same | | Speed | 160 mm/s | 260 mm/s | | DHCP | Off by default, auto-enables if the static IP cannot reach the router | same | | Factory reset | `1B 09 1B 92 FF 01` | | | Hex dump mode | Open cover, press FEED twice within 3 s, close cover. Exit: open cover, press FEED three times. | | | Self-test | Press FEED twice within 3 s. Prints model, firmware, interface, default code page. | | The POS896 buffers up to 45 KB of unprinted data across a paper-out and prints it after reloading. Worth knowing when reasoning about retries. ## ESC/POS commands the SDK emits (so the firmware certainly supports them) | Function | Bytes | |---|---| | Initialize | `1B 40` | | Feed n lines | `1B 64 n` | | Cut, full / partial | `1D 56 00` / `1D 56 01` (SDK always feeds 3 lines first) | | Line spacing | `1B 33 n` | | Alignment | `1B 61 n` | | Left margin | `1D 4C nL nH` | | Font mode (font B, bold, double width, double height, underline) | `1B 21 n` (bits 0, 3, 5, 4, 7) | | Bit image | `1B 2A m nL nH data`, m in {0, 1, 32, 33} | | Real-time status | `10 04 n`, n in 1..4, one-byte reply | | Printer ID | `1D 49 42` (GS I 66, returns `"_" + factory name` as a string) | | Cash drawer | `1B 70 m t1 t2` | | Inverse print | `1D 42 n` | | Rotate / upside-down | `1B 56 n` / `1B 7B n` | | 1D barcode | `1D 6B ...` | | 2D barcode | `1D 5A ...` (PDF417 / DataMatrix / QR selector) | | Proprietary line bitmap | `1D 44 01`, then per non-blank row `15 16 `, then `1D 44 00` | Status byte interpretation in the SDK (matches Epson's DLE EOT layout and our parsers in `EpsonPrinterStatus.cs`): | Query | Fixed bits | Meaning of set bit | |---|---|---| | `10 04 01` printer status | `0x12` must be set | `0x04` drawer high | | `10 04 02` offline status | `0x12` | `0x04` cover open | | `10 04 03` error status | `0x12` | `0x08` cutter error, `0x20` unrecoverable, `0x40` auto-recoverable | | `10 04 04` paper status | `0x12` | `0x40` paper end | Code pages: the SDK manual's code page table (TSPL section, but the same family) has index 18 = CP852, matching Epson numbering. Our `EpsonCommands.CP852Encoding` sends `1B 74 12`, so Latin-2 text is probably fine. Confirm on the self-test page. ## Compatibility of what `TM_T30IIITranslated` sends today | Command | Our use | Assessment | |---|---|---| | ESC @, GS V, ESC 3, ESC 2 | init, cut, line spacing | Fine, SDK emits them. | | ESC d n | feed | **Appears to be ignored.** Feed with literal LF bytes. | | DLE EOT 1..4 | status | **Confirmed.** Identical layout, `0x12` fixed bits set. | | GS ! n (`SetFontSizeAsync`) | magnification | **Confirmed** at width 1x/2x/3x, height 2x. | | ESC E (`SetEmphasized`) | bold | Standard, likely fine. SDK uses ESC ! bit 3 instead. | | ESC t 18 + CP852 | umlauts | **Confirmed.** ESC t honoured; 18=CP852, 2=CP850, 0=CP437 all render. | | ESC $ (`SetAbsolutePrintPosition`) | logo offset 100 | Unverified. SDK uses GS L margins instead. | | GS ( L fn 112 / fn 50 (`LoadImageAsync` + `PrintLoadedImage`) | logo | **Confirmed unsupported.** Prints nothing. | | ESC * 33 (`PrintImageBitModeAsync`) | logo | **Confirmed.** Full 576-dot width. Use this. | | GS v 0 | logo | **Confirmed** working too, but redundant. Do not implement. | | GS I 1 (`GetPrinterIdAsync`) | model byte | **Confirmed `0x20`.** No collision with 0x01/0x0D/0x13. | | ESC r (`SetRedColor`) | two-colour | Not applicable, monochrome printer. The T30III adapter ignores `IsRed` anyway. | ## SPRT discovery protocol Captured with `pktmon` while running `EasySet Tool.exe` on the printer's segment, then reimplemented in `Pos895Test` and confirmed to return byte-identical results (same MAC, same serial) as the vendor tool. **Query** - UDP broadcast to `255.255.255.255:3289`, sent **from source port 9100**, 13 bytes: ``` 53 50 52 54 2D 49 6E 66 6F 72 00 00 0D "SPRT-Infor" + 00 00 0D ``` **Reply** - from `:3289` back to `:9100`, 39 bytes of ASCII: ``` 28-0e-8b-3d-ca-f1 2608120478 |<--- MAC, 17 --->||<- 12 sp ->||<- serial, 10 ->| ``` The printer's address is the reply's source IP. The payload carries MAC and serial only, no model name, so model identification remains a separate TCP-side step and cannot be folded into discovery. - Port 3289 is the same UDP port Epson ENPC uses, so a composite `IDiscoveryService` can reuse the socket it already has and dispatch on the shape of the reply. - The querying machine was `10.1.250.71` and the printer `10.1.252.219`: different /24s on one flat segment. A /24 sweep would have missed this printer, which is why the capture route beat the port-sweep route here. - Unverified: whether the firmware replies to the sender's source port or hard-codes 9100. `Pos895Test` binds 9100 to match EasySet. - Unverified: reply framing when more than one printer answers. ## What breaks today if a POS895 is plugged into the network 1. **Discovery.** `PrintServerBootstrapper` binds `DiscoveryService`, which broadcasts Epson's proprietary `EPSONQ` packet on UDP 3289. The POS895 never answers it (confirmed: an `EPSONQ` broadcast went unanswered in the same capture that the printer did answer SPRT discovery in). `PrintServer.RegisterPrinter` is never called, so `SubmitJob` logs "Printer not found" and reports the job Failed. `JamesDiscoveredPrintersReceiver` never tells the backend about the printer, so it cannot be assigned to a work area. 2. **Identification.** `EpsonPrinter.GetPrinterIdAsync` sends `1D 49 01` and reads one byte. Three places dispatch on that byte: `PrinterWrapperFactory.CreatePrinterFromId`, `ReceiptConverterFactory.Create` (48 vs 33 columns), and `DiscoveryService.GetModelNameFromId`. If the printer answers nothing, the read waits the full connection timeout (10 s), returns null, `printerId.Value` throws, the job fails with `PrintErrorType.Other`, and `PrinterQueue` retries three times. If it answers `0x01` it is silently treated as a TM-T30III and mostly works by accident, with a 384 px logo at offset 100 on a 576-dot line. 3. **Paper-out logic.** `EpsonPrintService.PrintAsync` maps `!PrinterStatus.IsOnline` (DLE EOT 1 bit 3) to `OutOfPaper`. Whether this firmware sets that bit on paper-out is unverified. DLE EOT 4 bit 6 is what the SDK uses. ## Work items 1. **Hardware probe (do first, half a day).** With the printer in hex dump mode and then in normal mode, send and record replies for: - `1D 49 01`, `1D 49 41`, `1D 49 42`, `1D 49 43` (what identifies the model, and how many bytes come back) - `10 04 01..04` in normal, paper-out, cover-open states - `1B 74 12` followed by `ä ö ü ß č` bytes in CP852 - `1D 21 00 / 11 / 10 / 01` text sizes - `1B 24 nL nH` then text (does absolute positioning work) - a logo via GS ( L, via ESC * 33, and via `1D 76 30` - `EpsonDiagnostics.RunFullDiagnosticsAsync` output for the record 2. **Adapter.** New `Inspectron.Epson/PrintServer/Printers/POS895.cs` implementing `IPrinter`, cloned from `TM_T30IIITranslated`: logo width 576, centred offset `(576 - width) / 2`, image via ESC * or GS v 0, delays tuned. One case each in `PrinterWrapperFactory`, `ReceiptConverterFactory` (48 columns), `DiscoveryService.GetModelNameFromId`. 3. **Identification hardening in `EpsonPrinter`.** Shorter read timeout for GS I, fall back to the GS I 67 model-name string, or to a per-IP configured model, when the byte is missing or collides with an Epson ID. 4. **Discovery for non-Epson printers.** Solved: the vendor SPRT protocol is documented above and reimplemented in `Pos895Test`, confirmed against the printer. Remaining work is a `SprtDiscoveryService` implementing `IDiscoveryService`, and a composite service that merges its results with ENPC. Both probes go to UDP 3289, so they can share a socket. `PrinterDiscoveryBackgroundTask` needs no change. Now well under the 1 to 2 days originally estimated. 5. **Emulation and tests.** Add a 576 px `HtmlPrinter` entry with the new ID to the emulation dictionary in `PrintServerBootstrapper`, and EpsonTest cases, so receipt layouts can be checked without hardware. 6. ~~**GS v 0 raster** in `EpsonCommands`~~ Not needed: `GS ( L` is rejected but `ESC *` works and is already implemented. 7. **CLAUDE.md updates** once the adapter exists: printer ID byte, image width, discovery method. ## Answered on hardware, 2026-09-03 Probed through an AnyDesk TCP tunnel to the printer's port 9100, plus the SPRT discovery probe run on the printer's own segment. Tooling in `Pos895Test/`. - **Does the printer answer any UDP discovery?** Yes, SPRT on UDP 3289, above. - **Does DLE EOT work?** Yes. All four queries answered, exact Epson bit layout, `0x12` fixed bits set on every reply. `EpsonPrinterStatus.cs` parses this printer unmodified, so no custom status parser is needed. - **Font A column count?** 48 on the 576-dot line, as the manuals claim. The existing 48-column converters apply unchanged. - **Does `GS !` magnification work?** Yes, verified at width 1x/2x/3x, height 2x, so `SetFontSizeAsync` can pass magnification through without clamping. - **`ESC !`?** Selects Font A with `0x00`. - **`ESC d n`?** Appears to be ignored. Feed with literal LF bytes. - **`GS I` family?** `GS I 1` = `0x20`, unique. `GS I 2` = `0x02`, `GS I 3` = `0x64`, `GS I 65` = `"N-656 1.+0"`, `GS I 69` = `"_English"`. `GS I 66` (maker) and `GS I 67` (model name) are **blank**, so the model-name fallback the work items suggested is useless here. `GS I 68` returns the serial and it matches the SPRT discovery serial exactly, which makes it a usable correlation key between a discovered and a connected printer. - **Raster graphics?** `GS ( L` prints nothing. `ESC * 33` and `GS v 0` both render correctly across the full 576 dots. Use `PrintImageBitModeAsync`. - **Code pages?** `ESC t` is honoured and index 18 really is CP852. Index 2 (CP850) and 0 (CP437) also render correctly. Note CP852 has no `e`-grave, `a`-grave, `u`-grave, `e`-circumflex, `i`-grave or `o`-grave. `EpsonPrinter.PrintTextAsync` hardcodes CP852 for every printer, so French and Italian accents are already dropped fleet-wide, Epsons included. Now known to be fixable by selecting CP850, but that is a product-wide change with its own blast radius, not part of POS895 support. ## Open questions that only hardware can answer - Does DLE EOT 1 bit 3 go high on paper-out? The status queries answer, but paper-out was never induced. - Font B, and its claimed 64 columns. - Does the SPRT reply go to the sender's port, or is 9100 hard-coded? - Does the POS896 report the same `GS I 1` = `0x20`? Assumed yes, same spec sheet and same command set, but untested.