Closes every hardware question that was blocking the integration design, so the design now rests on measurements rather than assumptions. Probe results, all against the real printer: - GS I 1 returns 0x20, unique against 0x01/0x0D/0x13. This is what keeps the integration small: model identity stays a byte and the existing per-job identification flow works. Had it returned nothing or 0x01 the POS895 would have been indistinguishable from a TM-T30III by probe, and model identity would have had to come from discovery instead. - GS I 66 and 67 are blank, so the model-name fallback the work items proposed is useless here. GS I 68 returns the serial, and it matches the SPRT discovery serial exactly, which makes it a correlation key between a discovered printer and a connected one. - GS ( L prints nothing. ESC * 33 and GS v 0 both render correctly across the full 576 dots, so the adapter uses PrintImageBitModeAsync and the GS v 0 work item is dropped as redundant. - ESC t is honoured and index 18 really is CP852; 2 and 0 also render. That last one exposed a fleet-wide bug that is deliberately left out of scope: PrintTextAsync hardcodes CP852 for every printer, and CP852 cannot encode e-grave, a-grave, u-grave, e-circumflex, i-grave or o-grave, so French and Italian accents are dropped on the Epsons too. Now known to be fixable via ESC t 2, but it needs its own spec. The design spec covers discovery composition, the adapter, the three switch registrations, testing and risks. It calls out that the ReceiptConverterFactory WorkareaTicket switch defaults to 33 columns, so missing it yields a POS895 printing kitchen tickets at the narrow TM-U220 layout: wrong, but not obviously broken. Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
207 lines
14 KiB
Markdown
207 lines
14 KiB
Markdown
# 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 <skip> 16 <bytes> <inverted row data>`, 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 `<printer>:3289` back to `<querier>: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.
|