diff --git a/POS895_POS896_integration_notes.md b/POS895_POS896_integration_notes.md index 4343fbf..f1ea350 100644 --- a/POS895_POS896_integration_notes.md +++ b/POS895_POS896_integration_notes.md @@ -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. diff --git a/docs/superpowers/specs/2026-09-03-pos895-integration-design.md b/docs/superpowers/specs/2026-09-03-pos895-integration-design.md new file mode 100644 index 0000000..fe33964 --- /dev/null +++ b/docs/superpowers/specs/2026-09-03-pos895-integration-design.md @@ -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`, 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().To()` 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`.