FIX 1 — UDP 9100 binding race: SprtDiscoveryServiceTests and
CompositeDiscoveryServiceTests both use exclusive UDP 9100 binds. xunit
runs test classes concurrently by default, causing the tests to fail
intermittently with AddressAlreadyInUse. Add [Collection("udp-9100")]
attribute to both classes so they run serially instead.
FIX 2 — Design doc: CompositeDiscoveryService does NOT forward the callback
down to children; it raises the caller's callback itself after all children
finish, once per unique IP. Rewrite the relevant sentence to describe the
real behavior.
Verify: 30 tests pass (x2), build clean, no production code touched.
Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
SprtDiscoveryService caught SocketException on the UDP 9100 bind and the
broadcast send and returned an empty list either way. That defeated the
CompositeDiscoveryService fix that rethrows only when every child discovery
service fails: on a real network fault the composite would see one child
throw (ENPC) and one child silently succeed empty (SPRT), conclude discovery
worked and found nothing, and let PrinterDiscoveryBackgroundTask expire and
deregister live Epson printers after the retention window - discarding their
queued jobs.
Move the policy decision to the composite, which already has it right: the
child now reports bind/broadcast failure honestly by rethrowing, and
CompositeDiscoveryService absorbs a single child's failure while still
rethrowing when every child fails. The per-datagram receive-loop catch is
unchanged - a single bad receive still shouldn't abort the pass.
Rewrote SprtDiscoveryServiceTests' bind-failure test to assert the throw, and
added an end-to-end CompositeDiscoveryServiceTests case with a real
SprtDiscoveryService failing to bind alongside a stub that succeeds, proving
the composite still degrades gracefully rather than losing the pass. Updated
the POS895 design doc to match the new contract.
Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
1. Strengthen POS895 IsCut test: add RecordingEpsonPrinter.CallOrder so
the test can assert feeds happen BEFORE the cut, plus a LineFeedBytes
count assertion. The prior test could not detect the FeedAsync call
being deleted from the IsCut branch (verified: test now fails when
that line is removed, passes when restored).
2. CompositeDiscoveryService: distinguish "some children failed" from
"every child failed". Per-child failures are still absorbed, but if
every discovery service throws, the composite now rethrows instead
of silently returning an empty list. Previously a total discovery
failure (e.g. a routeless interface) looked like "discovery
succeeded, found nothing" to PrinterDiscoveryBackgroundTask, which
would expire and unregister every known printer after the retention
window, discarding their queued jobs. Also corrected a doc comment
that overstated Task.WhenAll's timeout guarantee.
3. Update CLAUDE.md to describe the POS895/POS896 work as shipped
rather than future work: added the Inspectron.Epson.Tests and
Pos895Test projects, a dotnet test command, SprtDiscoveryService/
CompositeDiscoveryService/POS895 in the architecture sections, the
0x20 printer-ID note, and the established SPRT hardware facts.
Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
Epson printers answer ENPC and POS895/POS896 answer SPRT; only the
composite discovery service sees both. Wire CompositeDiscoveryService
(wrapping DiscoveryService + SprtDiscoveryService) into production DI,
and add a 576px emulated POS895 (127.0.0.4) alongside the existing
TM-U220II/TM-T30III emulation entries so receipt layouts can be
checked without hardware.
Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
- Replace weak deduplication test with one using distinguishable printers
(TM-T30III/ENPC vs POS895/SPRT on same IP) to prove first-wins semantics
- Add concurrency test using SlowStubDiscovery to catch sequential regressions
(3 * 300ms tasks must complete in < 700ms, not >= 900ms)
- Add StubDiscovery constructor accepting List<DiscoveredPrinter> for
fully-populated test data
Test suite now 13/13 passing with proven concurrent execution.
Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
Pre-flight scan found two things the plan mandated that a reviewer would
rightly flag: Task 1 committed a DiscoverPrintersAsync that threw
NotImplementedException until Task 2 replaced it, and a unit test asserted
an empty result from a real UDP broadcast, which passes on a laptop and
fails on any network with a POS895 on it.
Tasks 1 and 2 are now one task delivering the complete service, and the
network-dependent test is gone. The deterministic bind-failure test still
covers the degradation path. Six tasks now.
Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
Seven TDD tasks derived from the design spec, each ending in a commit:
test project and SPRT protocol parsing, the discovery network path,
CompositeDiscoveryService, the POS895 adapter, the three switch
registrations, the nullable printer-id guard, and DI wiring plus an
emulated POS895.
Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
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
The POS895 does not answer Epson's ENPC broadcast, so PrintServer never
registers it and every job for it fails with "Printer not found". The
feasibility notes listed the vendor's own discovery protocol as unknown
and expected a packet capture to recover it, which is what this does.
Captured pktmon traffic while running the vendor EasySet tool on the
printer's segment. The protocol is:
query UDP broadcast 255.255.255.255:3289, sent FROM source port 9100
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 ASCII bytes
"28-0e-8b-3d-ca-f1 2608120478"
MAC, 17 chars 12 spaces serial, 10 chars
The printer's address is the reply's source IP. Pos895Test now sends this
probe and parses the replies; run against the real printer it returns the
same MAC and serial the vendor tool got, so the reimplementation matches.
Two useful side effects. Port 3289 is the same UDP port ENPC uses, so a
composite IDiscoveryService can share one socket. And the querying machine
and the printer sat in different /24s of one flat segment, so the /24 TCP
sweep the notes originally preferred would have missed this printer.
The print test is commented out at the bottom of Program.cs rather than
deleted; it confirmed Font A is 48 columns and that GS ! magnification
works at width 1x/2x/3x with height 2x.
Notes updated: the discovery work item is no longer the largest one, the
compatibility table records DLE EOT and GS ! as confirmed and ESC d as
apparently ignored, and the answered questions are separated from the
ones still open.
Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
Pos895Test is a console app that drives a POS895 over TCP through the
existing EpsonPrinter class, to answer the first question in the
feasibility notes: does the printer accept our ESC/POS at all.
It builds the whole job as one byte array and sends that exact array, so
the hex listing it prints is the wire content by construction rather than
a reconstruction that can drift. Current job prints a "|---" column ruler
at 48/width characters per row for width magnifications 1x, 2x and 3x at
double height, so every row should end at the same right margin.
Confirmed against the hardware so far:
- DLE EOT 1..4 are answered and follow the Epson bit layout exactly, with
the 0x12 fixed bits set on every reply, so EpsonPrinterStatus parses
this printer unmodified.
- Font A is 48 columns on the 576-dot line, as the manuals claim.
- GS ! magnification works, verified at width 1x/2x/3x and height 2x.
- ESC ! 0x00 selects Font A.
Not yet confirmed: Font B and its claimed 64 columns, the GS I 1 model
byte, ESC d n feed (this firmware appears to ignore it, hence the literal
LF bytes before the cut), code page 18 being CP852, and raster graphics.
Deliberately does not call GS I 1: the model byte is still an open
unknown and a smoke test must not be able to fail on one.
All settings are constants at the top of Program.cs; there are no
arguments.
Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
Study-only writeup of what supporting the SPRT-derived POS895/POS896
ESC/POS clones would take: no vendor SDK linking, a 576-dot IPrinter
adapter, non-ENPC discovery, a sturdier printer-ID probe, and the
hardware checks still open.
Claude-Session: https://claude.ai/code/session_01WAG8xogHxq9hDJScu386Jf
Use fully-qualified KitchenReceipt namespace for Gang, Dish, and
DishModifications in BuildSampleKitchenReceipt and
BuildSampleKitchenReceiptNextDish to resolve CS0104 errors caused by
identical type names in BarReceipt and KitchenReceipt namespaces.
Add global.json to pin SDK to .NET 8.0 for compatibility with .slnx.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Replace commented-out code blocks with arrow-key navigation menu.
First menu selects receipt type (Kitchen/Final/Invoice/Bar/OrderItems),
second selects printer target (HTML or Epson hardware).
Co-Authored-By: Paperclip <noreply@paperclip.ing>