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
7.5 KiB
POS895 integration design
Date: 2026-09-03. Status: design complete, awaiting review.
Adds POS895 / POS896 support to the print service. Every decision here rests on
hardware probes run against a real POS895 through an AnyDesk TCP tunnel; the raw
findings live in POS895_POS896_integration_notes.md and the tooling in
Pos895Test/.
Goal
A POS895 on the network is discovered automatically, reported to the backend so it can be assigned to a work area, and prints every existing receipt type correctly at 48 columns with a logo.
What the hardware told us
These four results shape the whole design.
| Probe | Result | Consequence |
|---|---|---|
GS I 1 |
0x20, no collision with 0x01/0x0D/0x13 |
Model identity keeps working as a byte. No resolver, no discovery-seeded cache. |
| SPRT discovery | SPRT-Infor on UDP 3289 answers with MAC + serial |
Discovery is a new IDiscoveryService, not a TCP port sweep. |
GS ( L |
prints nothing; ESC * and GS v 0 both work |
The adapter must not use LoadImageAsync. |
ESC d n |
ignored | The adapter must feed with LF. |
The GS I 1 result is what keeps this small. 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 flow from discovery instead - a much larger
change to EpsonPrintService.
Components
1. Discovery
IDiscoveryService
├── DiscoveryService ENPC / EPSONQ unchanged
├── SprtDiscoveryService SPRT-Infor new
└── CompositeDiscoveryService runs both, merges new
SprtDiscoveryService - Inspectron.Epson/SprtDiscoveryService.cs.
Promotes the probe in Pos895Test into the SDK. Broadcasts
"SPRT-Infor" 00 00 0D to 255.255.255.255:3289 from UDP source port 9100,
listens for the timeout, and maps each 39-byte reply to a DiscoveredPrinter:
| Field | Value |
|---|---|
IPAddress |
reply's source IP |
MACAddress |
payload bytes 0..16, dashed lowercase |
Serial |
payload bytes 29..38, trimmed |
ModelName |
"POS895" |
Port |
9100 |
ServiceType |
"SPRT" |
RawResponse |
the 39 bytes |
Replies that do not parse as a 17-character dashed MAC are logged and skipped, never thrown.
Binding UDP 9100 can fail when another process holds it. On SocketException
the service logs a warning and returns an empty list, so ENPC discovery still
runs. Discovery degrades; it does not crash.
CompositeDiscoveryService - takes IEnumerable<IDiscoveryService>, runs
them concurrently on the shared timeout, merges results by IPAddress with
first-wins, and forwards each child's onPrinterDiscovered callback as it fires.
Wiring - PrintServerBootstrapper.cs:77 changes from
Bind<IDiscoveryService>().To<DiscoveryService>() to the composite over both
children. PrinterDiscoveryBackgroundTask is untouched: its 3s timeout and 30s
interval still hold because the children run concurrently.
2. DiscoveredPrinter.Serial
A new nullable string Serial property. SPRT discovery returns a serial, and
GS I 68 returns the same value over TCP, making it the one field that can
correlate a discovered printer with a connected one. ENPC discovery leaves it
null. JamesDiscoveredPrintersReceiver is unchanged - it does not send the
field upstream, and adding it there is out of scope.
3. Identification
No change to the flow in EpsonPrintService.PrintAsync; GS I 1 returns 0x20
and the existing code path works.
One defensive fix: printerId is a byte? dereferenced with .Value twice,
at EpsonPrintService.cs:48 and :91. A printer that does not answer GS I 1
throws a NullReferenceException into the generic catch, which reports
PrintErrorType.Other and burns three PrinterQueue retries with no usable log
line. Add one null check after the call at :38 that logs the IP and returns
PrintResult.Fail(PrintErrorType.Other), which covers both dereferences. This
printer came within one probe of triggering it.
4. POS895 printer adapter
Inspectron.Epson/PrintServer/Printers/POS895.cs, implementing IPrinter,
cloned from TM-T30IIITranslated.cs. Three differences, each backed by a probe:
| Concern | TM-T30III | POS895 | Why |
|---|---|---|---|
| Logo | LoadImageAsync(path, 384) (GS ( L) |
PrintImageBitModeAsync(path, DoubleDensity24Dot, 576) (ESC *) |
GS ( L prints nothing on this firmware |
| Logo position | SetAbsolutePrintPosition(100) |
none, print at 0 across the full width | avoids ESC $, which is still unverified |
| Feed before cut | FeedLinesAsync(10) (ESC d) |
10 × LF |
ESC d is ignored, so the receipt would be cut through its last line |
Everything else - line spacing, GS ! sizing, ESC E bold - carries over
unchanged. GS ! is confirmed working at width 1x/2x/3x and height 2x, so
SetFontSizeAsync needs no clamping.
The adapter needs a small FeedLinesAsync-equivalent that emits LF bytes,
since IEpsonPrinter.FeedLinesAsync is hardcoded to ESC d. Implement it in
the adapter using SendRawCommandAsync, not by changing EpsonPrinter - the
Epson models rely on ESC d working.
5. Three registrations
| File | Change |
|---|---|
PrinterWrapperFactory.CreatePrinterFromId |
case 0x20: return new POS895(epsonPrinter); |
DiscoveryService.GetModelNameFromId |
0x20 => "POS895" |
ReceiptConverterFactory.Create |
add 0x20 to the WorkareaTicket switch, 48/24 |
The third is the easy one to miss. That switch defaults to 33/20, the narrow
TM-U220 layout, so without it a POS895 prints kitchen tickets at 33 columns on a
48-column head - wrong, but not obviously broken. Every other receipt type
already hardcodes 48/24 and needs no change.
NextCourse stays at 33/20 for all printers. It looks like a pre-existing bug,
but changing it alters TM-T30III behaviour and is deliberately out of scope.
6. Emulation
Add a 576px HtmlPrinter entry under ID 0x20 to the emulation dictionary in
PrintServerBootstrapper, so receipt layouts can be checked without hardware.
Testing
Unit-testable, no hardware:
- SPRT reply parsing: valid 39-byte reply, short reply, wrong-shape MAC, trailing whitespace in the serial.
CompositeDiscoveryService: merge by IP, first-wins on duplicates, one child throwing does not suppress the other, callbacks forwarded.
Hardware, via Pos895Test: discovery against the real printer, and a receipt
of each type through the full PrintAsync path with a logo.
Emulation: existing EpsonTest receipt-layout cases extended to ID 0x20.
Risks
| Risk | Mitigation |
|---|---|
POS896 may not report 0x20 |
Untestable without the hardware. If it differs, it is one more case in the same three switches. |
| SPRT may hard-code reply port 9100 | We bind 9100, which is proven to work. Only matters if the port is ever contended. |
Logo rendering quality at 576 via ESC * |
The command is confirmed working at full width; only dithering quality is unverified. |
Explicitly out of scope
- The CP852 code page problem.
EpsonPrinter.PrintTextAsynchardcodes CP852 fleet-wide, and CP852 cannot encodee-grave,a-grave,u-grave,e-circumflex,i-grave oro-grave. French and Italian accents are dropped on every printer today, Epsons included. Now known fixable viaESC t 2(CP850, confirmed working), but it is a product-wide change with its own blast radius and deserves its own spec. NextCoursehardcoded to 33 columns.- USB-attached POS895 operation - the pipeline keys on an IP address throughout.
- Sending
Serialupstream fromJamesDiscoveredPrintersReceiver.