From ac72e9ac4e156be52ebb2ec746771de75003f576 Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Thu, 3 Sep 2026 13:34:02 +0200 Subject: [PATCH] add a POS895 target to EpsonTest that prints via the IPrinter adapter EpsonTest predates the adapter layer: PrintCommandsToEpsonPrinterAsync takes an EpsonPrinter and drives it directly, so PrinterWrapperFactory, the model adapters and the 0x20 dispatch were never exercised. Against a POS895 that path feeds with ESC d, which the firmware ignores, so every cut slices through the last printed line, and it never sends a logo at all - the only logo branch is the HTML one, which uses GS ( L, the command that prints nothing on this hardware. PrintCommandsViaAdapterAsync instead does what EpsonPrintService does for a real job: query GS I 1, let PrinterWrapperFactory choose the adapter from the reported byte, then print the logo and commands through it and cut. It logs the reported id and the chosen adapter, so a wrong or missing model byte is visible rather than silent. Menu gains a third target and the non-interactive form accepts "pos895", so `dotnet run -- final pos895` prints a Final receipt with its logo through the production path. That covers the one part of the POS895 work nothing had executed end to end: the ESC * bit-image logo. Note EpsonTest must be run from its own directory; the logo paths are relative and `dotnet run --project` from the repo root cannot resolve them. That predates this change. Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref --- EpsonTest/Program.cs | 87 ++++++++++++-- EpsonTest/TestHelpers.cs | 44 +++++++ EpsonTest/test.html | 173 +++++++++++++++++++++++++++ EpsonTest/test_images/image_0001.png | Bin 0 -> 2874 bytes 4 files changed, 294 insertions(+), 10 deletions(-) create mode 100644 EpsonTest/test.html create mode 100644 EpsonTest/test_images/image_0001.png diff --git a/EpsonTest/Program.cs b/EpsonTest/Program.cs index d58c249..4db06c5 100644 --- a/EpsonTest/Program.cs +++ b/EpsonTest/Program.cs @@ -18,8 +18,8 @@ if (args.Length > 0) return; } - int pt = args.Length > 1 && args[1].Equals("epson", StringComparison.OrdinalIgnoreCase) ? 1 : 0; - Console.WriteLine($"Running {GetReceiptTypeName(rt)} on {(pt == 0 ? "HTML" : "Epson")} printer...\n"); + int pt = ParsePrinterTargetArg(args.Length > 1 ? args[1] : "html"); + Console.WriteLine($"Running {GetReceiptTypeName(rt)} on {GetPrinterTargetName(pt)} printer...\n"); await RunTest(rt, pt); Console.WriteLine("\nDone."); return; @@ -46,11 +46,12 @@ while (true) var printerTarget = ShowMenu("Select printer target:", new[] { "HTML Printer (local file)", - "Epson Printer (real hardware)" + "Epson Printer (real hardware)", + "POS895 (real hardware, via IPrinter adapter)" }); Console.Clear(); - Console.WriteLine($"Running {GetReceiptTypeName(receiptType)} on {(printerTarget == 0 ? "HTML" : "Epson")} printer...\n"); + Console.WriteLine($"Running {GetReceiptTypeName(receiptType)} on {GetPrinterTargetName(printerTarget)} printer...\n"); try { @@ -109,6 +110,21 @@ static int ShowMenu(string prompt, string[] options) } } +static string GetPrinterTargetName(int index) => index switch +{ + 0 => "HTML", + 1 => "Epson", + 2 => "POS895", + _ => "Unknown" +}; + +static int ParsePrinterTargetArg(string arg) => arg.ToLowerInvariant() switch +{ + "1" or "epson" => 1, + "2" or "pos895" or "pos" => 2, + _ => 0 +}; + static string GetReceiptTypeName(int index) => index switch { 0 => "KitchenReceipt", @@ -163,11 +179,19 @@ static async Task RunKitchenReceiptTest(int printerTarget) await TestHelpers.PrintKitchenCommandsToHtmlPrinterAsync(printer, printCommands); Console.WriteLine("Output written to: kitchen_test.html"); } - else + else if (printerTarget == 1) { var printer = await TestHelpers.CreateEpsonPrinterAsync(paperWidth: TestHelpers.KitchenPaperWidth); await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands, useDelay: true); } + else + { + // Routes through PrinterWrapperFactory and the model adapter, the way + // EpsonPrintService does for a real job. The Epson branch above drives + // EpsonPrinter directly and feeds with ESC d, which the POS895 ignores. + var printer = await TestHelpers.CreateEpsonPrinterAsync(); + await TestHelpers.PrintCommandsViaAdapterAsync(printer, printCommands); + } } static async Task RunFinalReceiptTest(int printerTarget) @@ -189,11 +213,20 @@ static async Task RunFinalReceiptTest(int printerTarget) logoPath: "ristorante-klinglers.ch-logo_white_bg.png"); Console.WriteLine("Output written to: test.html"); } - else + else if (printerTarget == 1) { var printer = await TestHelpers.CreateEpsonPrinterAsync(); await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands); } + else + { + // Routes through PrinterWrapperFactory and the model adapter, the way + // EpsonPrintService does for a real job. The Epson branch above drives + // EpsonPrinter directly and feeds with ESC d, which the POS895 ignores. + var printer = await TestHelpers.CreateEpsonPrinterAsync(); + await TestHelpers.PrintCommandsViaAdapterAsync(printer, printCommands, + logoPath: "ristorante-klinglers.ch-logo_white_bg.png"); + } } static async Task RunInvoiceReceiptTest(int printerTarget) @@ -215,11 +248,20 @@ static async Task RunInvoiceReceiptTest(int printerTarget) logoPath: "ristorante-klinglers.ch-logo_white_bg.png"); Console.WriteLine("Output written to: invoice_test.html"); } - else + else if (printerTarget == 1) { var printer = await TestHelpers.CreateEpsonPrinterAsync(); await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands); } + else + { + // Routes through PrinterWrapperFactory and the model adapter, the way + // EpsonPrintService does for a real job. The Epson branch above drives + // EpsonPrinter directly and feeds with ESC d, which the POS895 ignores. + var printer = await TestHelpers.CreateEpsonPrinterAsync(); + await TestHelpers.PrintCommandsViaAdapterAsync(printer, printCommands, + logoPath: "ristorante-klinglers.ch-logo_white_bg.png"); + } } static async Task RunBarReceiptTest(int printerTarget) @@ -239,11 +281,19 @@ static async Task RunBarReceiptTest(int printerTarget) await TestHelpers.PrintKitchenCommandsToHtmlPrinterAsync(printer, printCommands); Console.WriteLine("Output written to: bar_test.html"); } - else + else if (printerTarget == 1) { var printer = await TestHelpers.CreateEpsonPrinterAsync(paperWidth: TestHelpers.KitchenPaperWidth); await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands, useDelay: true); } + else + { + // Routes through PrinterWrapperFactory and the model adapter, the way + // EpsonPrintService does for a real job. The Epson branch above drives + // EpsonPrinter directly and feeds with ESC d, which the POS895 ignores. + var printer = await TestHelpers.CreateEpsonPrinterAsync(); + await TestHelpers.PrintCommandsViaAdapterAsync(printer, printCommands); + } } static async Task RunOrderItemsTest(int printerTarget) @@ -262,11 +312,19 @@ static async Task RunOrderItemsTest(int printerTarget) await TestHelpers.PrintCommandsToHtmlPrinterAsync(printer, printCommands); Console.WriteLine("Output written to: order_items.html"); } - else + else if (printerTarget == 1) { var printer = await TestHelpers.CreateEpsonPrinterAsync(); await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands); } + else + { + // Routes through PrinterWrapperFactory and the model adapter, the way + // EpsonPrintService does for a real job. The Epson branch above drives + // EpsonPrinter directly and feeds with ESC d, which the POS895 ignores. + var printer = await TestHelpers.CreateEpsonPrinterAsync(); + await TestHelpers.PrintCommandsViaAdapterAsync(printer, printCommands); + } } static async Task RunMoneyReturnReceiptTest(int printerTarget) @@ -288,9 +346,18 @@ static async Task RunMoneyReturnReceiptTest(int printerTarget) logoPath: "ristorante-klinglers.ch-logo_white_bg.png"); Console.WriteLine("Output written to: money_return_test.html"); } - else + else if (printerTarget == 1) { var printer = await TestHelpers.CreateEpsonPrinterAsync(); await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands); } + else + { + // Routes through PrinterWrapperFactory and the model adapter, the way + // EpsonPrintService does for a real job. The Epson branch above drives + // EpsonPrinter directly and feeds with ESC d, which the POS895 ignores. + var printer = await TestHelpers.CreateEpsonPrinterAsync(); + await TestHelpers.PrintCommandsViaAdapterAsync(printer, printCommands, + logoPath: "ristorante-klinglers.ch-logo_white_bg.png"); + } } diff --git a/EpsonTest/TestHelpers.cs b/EpsonTest/TestHelpers.cs index 0209d9d..c8010b0 100644 --- a/EpsonTest/TestHelpers.cs +++ b/EpsonTest/TestHelpers.cs @@ -1,5 +1,7 @@ using System.Diagnostics; using Inspectron.Epson; +using Inspectron.Epson.PrintServer.Printers; +using Inspectron.Epson.PrintServer.PrintServices; using Inspectron.Epson.PrintServer.Printers.Utils; using Inspectron.Epson.PrintServer.Printers.Utils.BarReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.FinalRecipe; @@ -577,6 +579,48 @@ public static class TestHelpers await printer.CutAsync(); } + /// + /// Prints through the production IPrinter adapter chain instead of driving + /// EpsonPrinter directly: query the model byte, let PrinterWrapperFactory pick + /// the adapter from it, then print the logo and commands through that adapter. + /// This is what EpsonPrintService does for a real job, so it exercises the + /// model dispatch and the adapter's own ESC/POS dialect. + /// + /// Use this for the POS895. PrintCommandsToEpsonPrinterAsync above predates the + /// adapter layer and feeds with ESC d, which that firmware ignores - every cut + /// would slice through the last printed line. + /// + public static async Task PrintCommandsViaAdapterAsync( + EpsonPrinter printer, + IEnumerable commands, + string? logoPath = null) + { + byte? printerId = await printer.GetPrinterIdAsync(); + if (printerId is null) + { + throw new InvalidOperationException( + "Printer did not answer GS I 1, so no adapter can be chosen for it."); + } + + Console.WriteLine($"Printer reported model id 0x{printerId.Value:X2}"); + + IPrinter adapter = new PrinterWrapperFactory() + .CreatePrinterFromId(printerId.Value, printer); + + Console.WriteLine($"Using adapter {adapter.GetType().Name}"); + + await adapter.InitAsync(); + + if (logoPath is not null) + { + Console.WriteLine($"Printing logo {logoPath}"); + await adapter.PrintImageAsync(logoPath); + } + + await adapter.PrintAsync(commands.ToList()); + await adapter.Cut(); + } + public static async Task PrintKitchenCommandsToHtmlPrinterAsync( HtmlPrinter printer, IEnumerable commands) diff --git a/EpsonTest/test.html b/EpsonTest/test.html new file mode 100644 index 0000000..4b2f947 --- /dev/null +++ b/EpsonTest/test.html @@ -0,0 +1,173 @@ + + + + + + Epson Printer Output + + + +
+
Printed image
+ Klingler Gastro AG + Münzplatz 3 + CH-8001 Zürich + 043 321 22 22 + + +Debitorenrechnung + 14:58 16.12.2025 +Guests: 2 + +1x San Pellegrino 50cl 6.50 6.50 A +1x Panna 50cl 6.50 6.50 A +1x An item with really long 5.50 5.50 A + description that should wrap +2x Business Lunch Menu Seco 44.00 88.00 A + - Tomato Soup + - Grilled Salmon + - Tiramisu +2x Brunello di Montalcino 1 16.00 32.00 A +2x Espresso 5.50 11.00 A +1x Tip 10.50 10.50 B + + --------- + + Summe: 160.00 CHF + + 172.80 EUR + +Fine Dine -50.00 CHF + + Cash: 50.00 CHF + Card: 110.00 CHF + +MASTER 160.00 CHF + +MwSt % Brutto Netto MwSt +A: 8.1% 149.50 CHF 138.30 CHF 11.20 CHF +B: 0% 10.50 CHF 10.50 CHF 0.00 CHF + + Bedient von: Yves + Terminal: Hauptkasse ZH + Tisch: 12 + + + CHE-449.635.880 MWST + + *** Kundenbeleg *** + Buchung + TWINT +XXXXXXXXXXXXXXX1494 +11.11.2025 12:34:49 +Trm-Id: 31108834 +AID: A0000015749E +Trx. Seq-Cnt: 6127 +Trx. Ref-No: 99036644599 +Auth. Code: 0d3396 +Acq-Id: 2 +EFT CHF: 67.00 +Trinkgeld CHF: 6.70 +Total-EFT CHF: 73.70 +------------------------------------------------ + + *** Kundenbeleg *** + Buchung + MASTER +XXXXXXXXXXXXXXX2587 +11.11.2025 12:36:12 +Trm-Id: 31108834 +AID: A0000000041010 +Trx. Seq-Cnt: 6128 +Trx. Ref-No: 99036644600 +Auth. Code: 1a4b2c +Acq-Id: 1 +EFT CHF: 93.00 +Trinkgeld CHF: 0.00 +Total-EFT CHF: 93.00 +------------------------------------------------ + + Das Team bedankt sich herzlich für Ihren + Besuch. + Auf Wiedersehen. + + + +------------------------------------------------ +Unterschrift + +
+ + \ No newline at end of file diff --git a/EpsonTest/test_images/image_0001.png b/EpsonTest/test_images/image_0001.png new file mode 100644 index 0000000000000000000000000000000000000000..ec4894788ab09a7a16f1420bc825c78f3ab30800 GIT binary patch literal 2874 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