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
This commit is contained in:
EugeneTes
2026-09-03 13:34:02 +02:00
parent b42d980877
commit ac72e9ac4e
4 changed files with 294 additions and 10 deletions

View File

@@ -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");
}
}

View File

@@ -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();
}
/// <summary>
/// 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.
/// </summary>
public static async Task PrintCommandsViaAdapterAsync(
EpsonPrinter printer,
IEnumerable<PrintCommand> 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<PrintCommand> commands)

173
EpsonTest/test.html Normal file
View File

@@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Epson Printer Output</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #f0f0f0;
padding: 20px;
font-family: 'Courier New', Courier, monospace;
}
.receipt {
width: 384px;
background-color: #fff;
padding: 10px;
margin: 0 auto;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.receipt span {
display: inline;
}
.receipt .image {
display: block;
margin: 5px 0;
}
.receipt .image img {
max-width: 100%;
height: auto;
display: block;
}
.cut {
position: relative;
border-top: 2px dashed #333;
margin: 20px 0;
height: 0;
}
.cut .scissors {
position: absolute;
left: -5px;
top: -12px;
font-size: 18px;
background: #fff;
padding: 0 3px;
}
/* Simulated paper texture */
.receipt::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 1px,
rgba(0,0,0,0.02) 1px,
rgba(0,0,0,0.02) 2px
);
pointer-events: none;
}
</style>
</head>
<body>
<div class="receipt">
<div class="image" style="margin-left: 42px;"><img src="test_images/image_0001.png" alt="Printed image"></div>
<span style="font-size: 12px; line-height: 13px;"> Klingler Gastro AG</span>
<span style="font-size: 12px; line-height: 13px;"> Münzplatz 3</span>
<span style="font-size: 12px; line-height: 13px;"> CH-8001 Zürich</span>
<span style="font-size: 12px; line-height: 13px;"> 043 321 22 22</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;">Debitorenrechnung</span>
<span style="font-size: 12px; font-weight: bold; line-height: 13px;"> 14:58 16.12.2025</span>
<span style="font-size: 12px; line-height: 13px;">Guests: 2</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;">1x San Pellegrino 50cl 6.50 6.50 A</span>
<span style="font-size: 12px; line-height: 13px;">1x Panna 50cl 6.50 6.50 A</span>
<span style="font-size: 12px; line-height: 13px;">1x An item with really long 5.50 5.50 A</span>
<span style="font-size: 12px; line-height: 13px;"> description that should wrap</span>
<span style="font-size: 12px; line-height: 13px;">2x Business Lunch Menu Seco 44.00 88.00 A</span>
<span style="font-size: 12px; line-height: 13px;"> - Tomato Soup</span>
<span style="font-size: 12px; line-height: 13px;"> - Grilled Salmon</span>
<span style="font-size: 12px; line-height: 13px;"> - Tiramisu</span>
<span style="font-size: 12px; line-height: 13px;">2x Brunello di Montalcino 1 16.00 32.00 A</span>
<span style="font-size: 12px; line-height: 13px;">2x Espresso 5.50 11.00 A</span>
<span style="font-size: 12px; line-height: 13px;">1x Tip 10.50 10.50 B</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;"> ---------</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; letter-spacing: 6px; font-weight: bold; line-height: 13px;"> Summe: 160.00 CHF</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;"> 172.80 EUR</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;">Fine Dine -50.00 CHF</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;"> Cash: 50.00 CHF</span>
<span style="font-size: 12px; line-height: 13px;"> Card: 110.00 CHF</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; font-weight: bold; line-height: 13px;">MASTER 160.00 CHF</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;">MwSt % Brutto Netto MwSt</span>
<span style="font-size: 12px; line-height: 13px;">A: 8.1% 149.50 CHF 138.30 CHF 11.20 CHF</span>
<span style="font-size: 12px; line-height: 13px;">B: 0% 10.50 CHF 10.50 CHF 0.00 CHF</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;"> Bedient von: Yves</span>
<span style="font-size: 12px; line-height: 13px;"> Terminal: Hauptkasse ZH</span>
<span style="font-size: 12px; line-height: 13px;"> Tisch: 12</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;"> CHE-449.635.880 MWST</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;"> *** Kundenbeleg ***</span>
<span style="font-size: 12px; line-height: 13px;"> Buchung</span>
<span style="font-size: 12px; line-height: 13px;"> TWINT</span>
<span style="font-size: 12px; line-height: 13px;">XXXXXXXXXXXXXXX1494</span>
<span style="font-size: 12px; line-height: 13px;">11.11.2025 12:34:49</span>
<span style="font-size: 12px; line-height: 13px;">Trm-Id: 31108834</span>
<span style="font-size: 12px; line-height: 13px;">AID: A0000015749E</span>
<span style="font-size: 12px; line-height: 13px;">Trx. Seq-Cnt: 6127</span>
<span style="font-size: 12px; line-height: 13px;">Trx. Ref-No: 99036644599</span>
<span style="font-size: 12px; line-height: 13px;">Auth. Code: 0d3396</span>
<span style="font-size: 12px; line-height: 13px;">Acq-Id: 2</span>
<span style="font-size: 12px; line-height: 13px;">EFT CHF: 67.00</span>
<span style="font-size: 12px; line-height: 13px;">Trinkgeld CHF: 6.70</span>
<span style="font-size: 12px; line-height: 13px;">Total-EFT CHF: 73.70</span>
<span style="font-size: 12px; line-height: 13px;">------------------------------------------------</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;"> *** Kundenbeleg ***</span>
<span style="font-size: 12px; line-height: 13px;"> Buchung</span>
<span style="font-size: 12px; line-height: 13px;"> MASTER</span>
<span style="font-size: 12px; line-height: 13px;">XXXXXXXXXXXXXXX2587</span>
<span style="font-size: 12px; line-height: 13px;">11.11.2025 12:36:12</span>
<span style="font-size: 12px; line-height: 13px;">Trm-Id: 31108834</span>
<span style="font-size: 12px; line-height: 13px;">AID: A0000000041010</span>
<span style="font-size: 12px; line-height: 13px;">Trx. Seq-Cnt: 6128</span>
<span style="font-size: 12px; line-height: 13px;">Trx. Ref-No: 99036644600</span>
<span style="font-size: 12px; line-height: 13px;">Auth. Code: 1a4b2c</span>
<span style="font-size: 12px; line-height: 13px;">Acq-Id: 1</span>
<span style="font-size: 12px; line-height: 13px;">EFT CHF: 93.00</span>
<span style="font-size: 12px; line-height: 13px;">Trinkgeld CHF: 0.00</span>
<span style="font-size: 12px; line-height: 13px;">Total-EFT CHF: 93.00</span>
<span style="font-size: 12px; line-height: 13px;">------------------------------------------------</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;"> Das Team bedankt sich herzlich für Ihren</span>
<span style="font-size: 12px; line-height: 13px;"> Besuch.</span>
<span style="font-size: 12px; line-height: 13px;"> Auf Wiedersehen.</span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;"></span>
<span style="font-size: 12px; line-height: 13px;">------------------------------------------------</span>
<span style="font-size: 12px; line-height: 13px;">Unterschrift</span>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB