Files
Print_server/CLAUDE.md
EugeneTes a4e96f3041 Fix three pre-merge review findings on POS895 branch
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
2026-09-03 12:46:14 +02:00

15 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

This is a C# .NET 8.0 solution for Epson thermal receipt printer integration. The solution consists of a core SDK library (Inspectron.Epson) and a production print service application.

Solution Structure

Projects

  • Inspectron.Epson - Core SDK library providing printer communication, status monitoring, ESC/POS commands, print queue management, and receipt conversion
  • EpsonPrintService - Production console application with SignalR-based remote job source, printer discovery, and systemd service support
  • EpsonTest - Test/example project for SDK functionality and receipt conversion
  • Inspectron.Epson.Tests - xunit unit test project (POS895, CompositeDiscoveryService, SprtDiscoveryService, receipt converters, etc.)
  • Pos895Test - Scratch console app for probing POS895/POS896 hardware directly; not production code

Common Build Commands

# Build entire solution
dotnet build Inspectron.Epson.slnx

# Build specific project
dotnet build Inspectron.Epson/Inspectron.Epson.csproj
dotnet build EpsonPrintService/EpsonPrintService.csproj

# Run print service
dotnet run --project EpsonPrintService/EpsonPrintService.csproj

# Run tests/examples
dotnet run --project EpsonTest/EpsonTest.csproj

# Run the unit test suite
dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj

# Clean build artifacts
dotnet clean

Architecture

Three-Layer Architecture

  1. SDK Layer (Inspectron.Epson)

    • IEpsonPrinter / EpsonPrinter: Main interface and TCP/IP implementation for printer operations (async/await)
    • EpsonCommands: Static ESC/POS command definitions
    • EpsonImageConverter: Image processing with Floyd-Steinberg dithering
    • IDiscoveryService / DiscoveryService: Network printer discovery (SLP, ENPC)
    • PreconfiguredDiscoveryService: Preconfigured printer list (no network scan)
    • BonjourDiscovery: mDNS-based printer discovery
    • SprtDiscoveryService: UDP broadcast discovery for SPRT-family printers (POS895 / POS896), which do not answer Epson ENPC
    • CompositeDiscoveryService: Runs several IDiscoveryService implementations concurrently and merges results by IP; absorbs individual child failures but rethrows if every child fails, so a genuinely broken discovery pass is distinguishable from an empty network
    • HtmlPrinter: HTML output implementation for testing without hardware
    • Status classes: PrinterStatus, OfflineStatus, ErrorStatus, PaperSensorStatus, OverallStatus
    • Exception hierarchy: EpsonPrinterException -> EpsonConnectionException, EpsonCommandException
  2. Queue Layer (Inspectron.Epson.Queue namespace)

    • PrintServer: Manager for per-printer queues with read/write lock coordination between printing and discovery
    • PrinterQueue: Per-printer queue with retry logic (ConcurrentQueue + priority ConcurrentStack)
    • IPrintService: Print workflow interface (PrintAsync returns PrintResult)
    • PrintJob: Job model with IP, Document, QueuedAt, RetryCount
    • PrintResult: Success/Failure result with PrintErrorType
    • IJobStatusReporter: Interface for reporting job status updates
  3. PrintServer Layer (Inspectron.Epson.PrintServer namespace)

    • PrintLoop: Orchestrator connecting job sources to print server
    • JobSources/: IPrintJobSource strategy pattern (SignalRPrintJobSource, SingleJobSource)
    • PrintServices/: IPrintService implementation (EpsonPrintService), IAssignedPrinterRepository, IPrinterConfigurationSource
    • ConfigurationSources/: EpsonPrintServiceConfiguration, FixedConfigurationSource
    • Printers/: IPrinterFactory (creates IEpsonPrinter), IWrapperPrinterFactory / PrinterWrapperFactory (creates IPrinter adapter by model ID)
    • Printers/Utils/: PrintCommand model, IReceiptConverter / IReceiptConverterFactory with receipt type implementations
    • WorkAreaSources/: IWorkAreasSource, JamesWorkAreaSource
    • DiscoveredPrintersReceiver/: IDiscoveredPrintersReceiver, JamesDiscoveredPrintersReceiver
  4. Application Layer (EpsonPrintService)

    • Production deployment with Ninject DI, SignalR remote jobs, printer discovery, heartbeat, systemd service

Receipt Conversion System

Receipt content is converted from JSON to PrintCommand lists via IReceiptConverterFactory:

  • KitchenReceipt - Kitchen order tickets
  • FinalReceipt - Final customer receipts
  • InvoiceReceipt - Invoice receipts
  • BarReceipt - Bar order tickets
  • OrderItems - Order item lists

PrintCommand properties: Text, IsBig, IsTall, IsBold, IsRed, SetLineSpacing, IsCut

Printer Model Adapters

IPrinter implementations wrap IEpsonPrinter with model-specific behavior:

  • TM-T30III / TM-T30IIITranslated - Thermal receipt printer (384px image width)
  • TM-U220II / TM-U220IITranslated - Dot matrix receipt printer (narrower image width)
  • POS895 - SPRT-family ESC/POS clone (POS895 / POS896), 576-dot line, Font A at 48 columns. See "Non-Epson printers" below for the firmware quirks it works around.

Created via IWrapperPrinterFactory.CreatePrinterFromId(modelIdByte, epsonPrinter).

Dependency Injection (Ninject)

StandardKernel kernel = new();
kernel.Bind<IPrinterFactory>().To<EpsonPrinterFactory>();
kernel.Bind<IWrapperPrinterFactory>().To<PrinterWrapperFactory>().InSingletonScope();
kernel.Bind<IPrintJobSource, SignalRPrintJobSource>().To<SignalRPrintJobSource>().InSingletonScope();
kernel.Bind<IPrintService>().To<EpsonPrintService>();
kernel.Bind<IPrinterConfigurationSource>().To<FixedConfigurationSource>();
kernel.Bind<IReceiptConverterFactory>().To<ReceiptConverterFactory>().InSingletonScope();
kernel.Bind<IJobStatusReporter>().To<NullJobStatusReporter>();
kernel.Bind<IDiscoveryService>().ToConstant(discovery);
kernel.Bind<IDiscoveredPrintersReceiver>().To<JamesDiscoveredPrintersReceiver>().InSingletonScope();
kernel.Bind<PrintServer>().ToSelf().InSingletonScope();

Key Interfaces and Abstractions

  • IEpsonPrinter: Low-level printer operations (connect, status, ESC/POS commands, image printing)
  • IPrinter: Printer model adapter (InitAsync, PrintImageAsync, SetFontSizeAsync, PrintAsync, Cut)
  • IPrintJobSource: Pluggable job sources (async via Channel)
  • IPrintService: Print workflow execution (PrintAsync returns PrintResult)
  • IPrinterFactory: Creates IEpsonPrinter instances (EpsonPrinterFactory, HtmlPrinterFactory)
  • IWrapperPrinterFactory: Creates IPrinter adapter from model ID byte
  • IPrinterConfigurationSource: Provides printer configuration
  • IReceiptConverter / IReceiptConverterFactory: Converts JSON receipt content to PrintCommand list
  • IDiscoveryService: Printer network discovery
  • IDiscoveredPrintersReceiver: Receives and processes discovered printers
  • IJobStatusReporter: Reports job status updates

Print Job Flow

PrintLoop.StartAsync()
  -> IPrintJobSource.GetNextJobAsync() [blocks until job available]
  -> PrintServer.SubmitJob(job.IP, job)
    -> PrinterQueue.Enqueue(job) [per-printer queue]
      -> ProcessQueueAsync() [background loop]
        -> IPrintService.PrintAsync(printerIp, job)
          -> IPrinterFactory.CreatePrinter() [creates IEpsonPrinter]
          -> Connect, get printer ID, check status
          -> IWrapperPrinterFactory.CreatePrinterFromId() [creates IPrinter adapter]
          -> Initialize printer, download & cache logo (if URL provided)
          -> IReceiptConverterFactory.Create(receiptType, printerId)
          -> Convert JSON content to PrintCommand list
          -> printerAdapter.PrintAsync(commands), cut paper
          -> Return PrintResult.Ok() or PrintResult.Fail(errorType)
        -> On failure: retry up to 3 times via priority queue

Configuration System

Configuration is loaded from a base64-encoded config.txt file containing apiUrl;restaurantId;apiKey:

# config.txt contains a single base64-encoded line
# Decoded format: apiUrl;restaurantId;apiKey
# Example decoded: https://api.gastrojames.ch;63e37295bd6f26dbd36164c0;yourApiKeyHere

ConfigurationPaths resolves the config path:

  • Windows: local config.txt
  • Linux: /root/epsonprintservice/config.txt

Printers are registered via PreconfiguredDiscoveryService with hardcoded IPs in Program.cs, or dynamically via PrinterDiscoveryBackgroundTask.

PrintServer Read/Write Lock

PrintServer uses a readers-writer lock pattern to coordinate printing with discovery:

  • Print operations acquire a read lock (EnterPrintLock / ExitPrintLock) - multiple prints run concurrently
  • Discovery operations acquire a write lock (EnterDiscoveryLockAsync / ExitDiscoveryLock) - blocks until all prints complete, prevents new prints during discovery

Printer Communication

  • Protocol: ESC/POS over TCP/IP (port 9100 default)
  • Encoding: UTF-8 (ESC t 255) or CP852 (ESC t 18) with System.Text.Encoding.CodePages
  • Status Queries: DLE EOT real-time commands (0x10 0x04 + sub-command byte)
  • Image Printing: Two modes
    • Raster mode (GS ( L): 384px width for TM-T30III
    • Bit-image mode (ESC *): 8-dot or 24-dot with single/double density
  • Error Handling: All async operations throw EpsonConnectionException or EpsonCommandException on failure

Adding New Printer Models

  1. Get printer ID byte via EpsonPrinter.GetPrinterIdAsync() (GS I 1 command). Note: ID 0x20 is already taken by the POS895 - pick a different byte.
  2. Create new class implementing IPrinter in Inspectron.Epson/PrintServer/Printers/
  3. Implement required methods: InitAsync, PrintImageAsync, SetFontSizeAsync, PrintAsync, Cut
  4. Add case to PrinterWrapperFactory.CreatePrinterFromId() with the printer ID byte
  5. Add receipt converter support in ReceiptConverterFactory if needed
  6. Test with actual hardware - different models have different image widths and positioning

Non-Epson printers (POS895 / POS896)

POS895 / POS896 support has shipped: POS895 (Inspectron.Epson/PrintServer/Printers/POS895.cs) is the IPrinter adapter, SprtDiscoveryService is the discovery strategy, and PrinterWrapperFactory maps printer ID 0x20 to POS895. The full write-up, including the hardware checks that are still open, lives in POS895_POS896_integration_notes.md - read it before touching discovery, printer identification (GetPrinterIdAsync), or PrinterWrapperFactory for a non-Epson model.

Established hardware facts (confirmed on hardware, so treat as given rather than re-deriving them):

  • 576 dots wide; Font A is 48 columns.
  • GS I 1 (GetPrinterIdAsync) answers with 0x20.
  • The firmware ignores ESC d, so POS895 feeds with literal LF bytes via SendRawCommandAsync instead of IEpsonPrinter.FeedLinesAsync. Using ESC d here would cut the receipt through its last printed line.
  • GS ( L raster mode prints nothing, so logos go out via ESC * bit-image mode (PrintImageBitModeAsync, DoubleDensity24Dot) instead of LoadImageAsync.
  • Discovery is a UDP broadcast of "SPRT-Infor\0\0\r" to port 3289, sent from source port 9100 (SprtDiscoveryService); this does not go through Epson's ENPC protocol.

Adding New Receipt Types

  1. Create a new folder under Inspectron.Epson/PrintServer/Printers/Utils/
  2. Add Models.cs with the receipt data model
  3. Create a converter implementing IReceiptConverter (returns List<PrintCommand>)
  4. Register the new receipt type in ReceiptConverterFactory

Testing Printer Connectivity

// Basic connection test
await using var printer = new EpsonPrinter(logger);
await printer.ConnectAsync("192.168.1.100");
var status = await printer.GetOverallStatusAsync();
Console.WriteLine($"Status: {status.StatusText}, Ready: {status.IsReady}");

// Full diagnostics
var diagnostics = new EpsonDiagnostics(logger);
var report = await diagnostics.RunFullDiagnosticsAsync("192.168.1.100");
Console.WriteLine(report);

Deployment

Systemd Service (Linux)

EpsonPrintService includes systemd service files (epson.service, print_server.service):

# Copy files to deployment directory
cp -r EpsonPrintService/bin/Debug/net8.0/* /home/pi/epson_service/

# Install and start service
sudo cp epson.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable epson.service
sudo systemctl start epson.service

# Check status
sudo systemctl status epson.service

Deployment scripts included: start.sh, postinst, prem, uninstall.sh.

Important Implementation Notes

PrinterQueue Retry Logic

Failed print jobs are automatically retried up to 3 times via priority queue:

  • Failed jobs pushed to ConcurrentStack<PrintJob> (LIFO)
  • Priority stack checked before main queue in ProcessQueueAsync()
  • RetryCount incremented on each failure
  • Jobs with RetryCount >= 3 are discarded

Image Processing Pipeline

When printing images via EpsonPrinter.LoadImageAsync():

  1. Load image with SixLabors.ImageSharp
  2. Resize maintaining aspect ratio (max width: printer-specific)
  3. Convert to grayscale
  4. Apply Floyd-Steinberg dithering for 1-bit black/white conversion
  5. Pack pixels to byte-aligned format
  6. Generate column format for bit-image mode (ESC * 33)

Logo Caching

EpsonPrintService downloads logos from URLs provided in print jobs:

  • Logos are cached locally in a logos/ directory under the app base directory
  • Filename is base64-encoded to avoid path issues
  • Subsequent prints skip the download if the cached file exists

SignalR Job Source

SignalRPrintJobSource connects to {ApiUrl}/hubs/printer-servers:

  • Uses InfiniteRetryPolicy with exponential backoff (capped at 30s) for automatic reconnection
  • Retries initial connection in a loop on failure
  • Joins group by RestaurantId from configuration
  • Message: "PrintJob" with { PrinterIp, LogoUrl, ReceiptType, Content }
  • Uses Channel for async producer-consumer pattern

Font Sizing

Font magnification is 1-8x for both width and height:

  • ESC ! command: bits 4-7 control size
  • Formula: (widthMagnifier - 1) << 4 | (heightMagnifier - 1)
  • Example: Size 2 = 0x11 (2x width, 2x height)

Common Troubleshooting

"Printer not found" or connection timeout

  • Verify IP address with ping <printer-ip>
  • Check printer is on same network/VLAN
  • Ensure port 9100 is not blocked by firewall
  • Check PreconfiguredDiscoveryService entries in Program.cs

"Cover open" or "Paper end" errors

  • Check OverallStatus.Recommendations for specific issues
  • Common: Cover not fully closed, paper roll empty/misaligned

Image not printing or garbled

  • Verify image width matches printer specification (384px for TM-T30III)
  • Check image file path or URL is accessible
  • Ensure printer model supports raster graphics (TM-T30III does, older models may not)

SignalR connection failures

  • Check RestaurantId and ApiKey in config.txt (base64-encoded)
  • Verify network connectivity to the API URL
  • SignalR auto-reconnects with infinite retry; check logs for reconnection attempts