probe remaining POS895 unknowns; add integration design spec
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
This commit is contained in:
@@ -100,10 +100,12 @@ Code pages: the SDK manual's code page table (TSPL section, but the same family)
|
||||
| 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 | Likely fine, see code pages above. |
|
||||
| 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 | Probably unsupported. SDK never uses it. Fallbacks: ESC * 24-dot double density (`PrintImageBitModeAsync`, already implemented) or GS v 0 (not implemented). |
|
||||
| GS I 1 (`GetPrinterIdAsync`) | model byte | **Unknown.** SDK uses GS I 66 for a maker string 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
|
||||
@@ -159,7 +161,7 @@ step and cannot be folded into discovery.
|
||||
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`, only if the probe shows GS ( L is rejected.
|
||||
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
|
||||
@@ -177,13 +179,28 @@ discovery probe run on the printer's own segment. Tooling in `Pos895Test/`.
|
||||
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
|
||||
|
||||
- What does `GS I 1` return, if anything?
|
||||
- Does the firmware accept `GS ( L` raster graphics?
|
||||
- Is code page 18 really CP852 in ESC/POS mode?
|
||||
- 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.
|
||||
|
||||
168
docs/superpowers/specs/2026-09-03-pos895-integration-design.md
Normal file
168
docs/superpowers/specs/2026-09-03-pos895-integration-design.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# 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.PrintTextAsync` hardcodes CP852
|
||||
fleet-wide, and CP852 cannot encode `e`-grave, `a`-grave, `u`-grave,
|
||||
`e`-circumflex, `i`-grave or `o`-grave. French and Italian accents are dropped
|
||||
on every printer today, Epsons included. Now known fixable via `ESC t 2`
|
||||
(CP850, confirmed working), but it is a product-wide change with its own blast
|
||||
radius and deserves its own spec.
|
||||
- `NextCourse` hardcoded to 33 columns.
|
||||
- USB-attached POS895 operation - the pipeline keys on an IP address throughout.
|
||||
- Sending `Serial` upstream from `JamesDiscoveredPrintersReceiver`.
|
||||
Reference in New Issue
Block a user