reverse engineer POS895 discovery; add SPRT discovery probe

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
This commit is contained in:
EugeneTes
2026-09-03 10:45:35 +02:00
parent 8ca8370426
commit 17a6eb6b39
6 changed files with 269 additions and 111 deletions

4
.gitattributes vendored
View File

@@ -61,3 +61,7 @@
#*.PDF diff=astextplain #*.PDF diff=astextplain
#*.rtf diff=astextplain #*.rtf diff=astextplain
#*.RTF diff=astextplain #*.RTF diff=astextplain
# packet captures
*.pcapng binary
*.etl binary

View File

@@ -1,6 +1,8 @@
# POS895 / POS896 printer support: feasibility notes # POS895 / POS896 printer support: feasibility notes
Date: 2026-09-02. Status: study only, no code changes made. Date: 2026-09-02, updated 2026-09-03 after a hardware session. Status: several
of the open questions below are now answered, and the vendor discovery protocol
is reverse engineered and reimplemented. See `Pos895Test/`.
Source material reviewed: `D:\Inspectron\James\epson\POS895\POS895&896 SDK\` (Linux SDK, Windows POS_SDK, EasySet tool, POS895 and POS896 user manuals, POS_SDK Development Manual V1.4). Source material reviewed: `D:\Inspectron\James\epson\POS895\POS895&896 SDK\` (Linux SDK, Windows POS_SDK, EasySet tool, POS895 and POS896 user manuals, POS_SDK Development Manual V1.4).
@@ -93,9 +95,10 @@ Code pages: the SDK manual's code page table (TSPL section, but the same family)
| Command | Our use | Assessment | | Command | Our use | Assessment |
|---|---|---| |---|---|---|
| ESC @, ESC d, GS V, ESC 3, ESC 2 | init, feed, cut, line spacing | Fine, SDK emits them. | | ESC @, GS V, ESC 3, ESC 2 | init, cut, line spacing | Fine, SDK emits them. |
| DLE EOT 1..4 | status | Fine, identical layout. | | ESC d n | feed | **Appears to be ignored.** Feed with literal LF bytes. |
| GS ! n (`SetFontSizeAsync`) | magnification | Likely fine, Windows SDK exposes a width/height font-size call. | | 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 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 | Likely fine, see code pages above. |
| ESC $ (`SetAbsolutePrintPosition`) | logo offset 100 | Unverified. SDK uses GS L margins instead. | | ESC $ (`SetAbsolutePrintPosition`) | logo offset 100 | Unverified. SDK uses GS L margins instead. |
@@ -103,9 +106,42 @@ Code pages: the SDK manual's code page table (TSPL section, but the same family)
| GS I 1 (`GetPrinterIdAsync`) | model byte | **Unknown.** SDK uses GS I 66 for a maker string instead. | | GS I 1 (`GetPrinterIdAsync`) | model byte | **Unknown.** SDK uses GS I 66 for a maker string instead. |
| ESC r (`SetRedColor`) | two-colour | Not applicable, monochrome printer. The T30III adapter ignores `IsRed` anyway. | | ESC r (`SetRedColor`) | two-colour | Not applicable, monochrome printer. The T30III adapter ignores `IsRed` anyway. |
## SPRT discovery protocol
Captured with `pktmon` while running `EasySet Tool.exe` on the printer's segment,
then reimplemented in `Pos895Test` and confirmed to return byte-identical results
(same MAC, same serial) as the vendor tool.
**Query** - UDP broadcast to `255.255.255.255:3289`, sent **from source port 9100**, 13 bytes:
```
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 bytes of ASCII:
```
28-0e-8b-3d-ca-f1 2608120478
|<--- MAC, 17 --->||<- 12 sp ->||<- serial, 10 ->|
```
The printer's address is the reply's source IP. The payload carries MAC and
serial only, no model name, so model identification remains a separate TCP-side
step and cannot be folded into discovery.
- Port 3289 is the same UDP port Epson ENPC uses, so a composite
`IDiscoveryService` can reuse the socket it already has and dispatch on the
shape of the reply.
- The querying machine was `10.1.250.71` and the printer `10.1.252.219`:
different /24s on one flat segment. A /24 sweep would have missed this printer,
which is why the capture route beat the port-sweep route here.
- Unverified: whether the firmware replies to the sender's source port or
hard-codes 9100. `Pos895Test` binds 9100 to match EasySet.
- Unverified: reply framing when more than one printer answers.
## What breaks today if a POS895 is plugged into the network ## What breaks today if a POS895 is plugged into the network
1. **Discovery.** `PrintServerBootstrapper` binds `DiscoveryService`, which broadcasts Epson's proprietary `EPSONQ` packet on UDP 3289. The POS895 never answers. `PrintServer.RegisterPrinter` is never called, so `SubmitJob` logs "Printer not found" and reports the job Failed. `JamesDiscoveredPrintersReceiver` never tells the backend about the printer, so it cannot be assigned to a work area. 1. **Discovery.** `PrintServerBootstrapper` binds `DiscoveryService`, which broadcasts Epson's proprietary `EPSONQ` packet on UDP 3289. The POS895 never answers it (confirmed: an `EPSONQ` broadcast went unanswered in the same capture that the printer did answer SPRT discovery in). `PrintServer.RegisterPrinter` is never called, so `SubmitJob` logs "Printer not found" and reports the job Failed. `JamesDiscoveredPrintersReceiver` never tells the backend about the printer, so it cannot be assigned to a work area.
2. **Identification.** `EpsonPrinter.GetPrinterIdAsync` sends `1D 49 01` and reads one byte. Three places dispatch on that byte: `PrinterWrapperFactory.CreatePrinterFromId`, `ReceiptConverterFactory.Create` (48 vs 33 columns), and `DiscoveryService.GetModelNameFromId`. If the printer answers nothing, the read waits the full connection timeout (10 s), returns null, `printerId.Value` throws, the job fails with `PrintErrorType.Other`, and `PrinterQueue` retries three times. If it answers `0x01` it is silently treated as a TM-T30III and mostly works by accident, with a 384 px logo at offset 100 on a 576-dot line. 2. **Identification.** `EpsonPrinter.GetPrinterIdAsync` sends `1D 49 01` and reads one byte. Three places dispatch on that byte: `PrinterWrapperFactory.CreatePrinterFromId`, `ReceiptConverterFactory.Create` (48 vs 33 columns), and `DiscoveryService.GetModelNameFromId`. If the printer answers nothing, the read waits the full connection timeout (10 s), returns null, `printerId.Value` throws, the job fails with `PrintErrorType.Other`, and `PrinterQueue` retries three times. If it answers `0x01` it is silently treated as a TM-T30III and mostly works by accident, with a 384 px logo at offset 100 on a 576-dot line.
3. **Paper-out logic.** `EpsonPrintService.PrintAsync` maps `!PrinterStatus.IsOnline` (DLE EOT 1 bit 3) to `OutOfPaper`. Whether this firmware sets that bit on paper-out is unverified. DLE EOT 4 bit 6 is what the SDK uses. 3. **Paper-out logic.** `EpsonPrintService.PrintAsync` maps `!PrinterStatus.IsOnline` (DLE EOT 1 bit 3) to `OutOfPaper`. Whether this firmware sets that bit on paper-out is unverified. DLE EOT 4 bit 6 is what the SDK uses.
@@ -121,15 +157,33 @@ Code pages: the SDK manual's code page table (TSPL section, but the same family)
- `EpsonDiagnostics.RunFullDiagnosticsAsync` output for the record - `EpsonDiagnostics.RunFullDiagnosticsAsync` output for the record
2. **Adapter.** New `Inspectron.Epson/PrintServer/Printers/POS895.cs` implementing `IPrinter`, cloned from `TM_T30IIITranslated`: logo width 576, centred offset `(576 - width) / 2`, image via ESC * or GS v 0, delays tuned. One case each in `PrinterWrapperFactory`, `ReceiptConverterFactory` (48 columns), `DiscoveryService.GetModelNameFromId`. 2. **Adapter.** New `Inspectron.Epson/PrintServer/Printers/POS895.cs` implementing `IPrinter`, cloned from `TM_T30IIITranslated`: logo width 576, centred offset `(576 - width) / 2`, image via ESC * or GS v 0, delays tuned. One case each in `PrinterWrapperFactory`, `ReceiptConverterFactory` (48 columns), `DiscoveryService.GetModelNameFromId`.
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. 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.** Preferred: sweep the local /24 for open TCP 9100 and run the identification probe on responders. Alternatives: the vendor's UDP discovery protocol used by `EasySet Tool.exe` (unknown, needs a packet capture; the exe yields no readable strings), or manual static registration through `SimpleHttpServer`. Then a composite `IDiscoveryService` that merges ENPC results with the new strategy. `PrinterDiscoveryBackgroundTask` needs no change. This is the largest item, 1 to 2 days. 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. 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`, only if the probe shows GS ( L is rejected.
7. **CLAUDE.md updates** once the adapter exists: printer ID byte, image width, discovery method. 7. **CLAUDE.md updates** once the adapter exists: printer ID byte, image width, discovery method.
## Answered on hardware, 2026-09-03
Probed through an AnyDesk TCP tunnel to the printer's port 9100, plus the SPRT
discovery probe run on the printer's own segment. Tooling in `Pos895Test/`.
- **Does the printer answer any UDP discovery?** Yes, SPRT on UDP 3289, above.
- **Does DLE EOT work?** Yes. All four queries answered, exact Epson bit layout,
`0x12` fixed bits set on every reply. `EpsonPrinterStatus.cs` parses this
printer unmodified, so no custom status parser is needed.
- **Font A column count?** 48 on the 576-dot line, as the manuals claim. The
existing 48-column converters apply unchanged.
- **Does `GS !` magnification work?** Yes, verified at width 1x/2x/3x, height 2x,
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.
## Open questions that only hardware can answer ## Open questions that only hardware can answer
- What does `GS I 1` return, if anything? - What does `GS I 1` return, if anything?
- Does the firmware accept `GS ( L` raster graphics? - Does the firmware accept `GS ( L` raster graphics?
- Is code page 18 really CP852 in ESC/POS mode? - Is code page 18 really CP852 in ESC/POS mode?
- Does DLE EOT 1 bit 3 go high on paper-out? - Does DLE EOT 1 bit 3 go high on paper-out? The status queries answer, but
- Does the printer answer any UDP or mDNS discovery at all? 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?

View File

@@ -1,138 +1,138 @@
using System.Net;
using System.Net.Sockets;
using System.Text; using System.Text;
using Inspectron.Epson; using Inspectron.Epson;
// POS895 smoke test: Font A column count vs. GS ! width magnification. // POS895 discovery probe.
// //
// Prints one ruler row per width magnification, each at 48/width characters, so // The POS895 does not answer Epson's ENPC broadcast, but it does answer the
// every row should end at the same right margin with progressively larger type. // vendor's own SPRT discovery, which the EasySet tool uses. Protocol, taken from
// A row that stops short means that magnification was ignored. // a packet capture of EasySet on the printer's segment:
//
// 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\0\0\r"
//
// reply from <printer>:3289 back to <querier>:9100, 39 bytes of ASCII
// "28-0e-8b-3d-ca-f1 2608120478"
// |<-- MAC, 17 -->||<- 12 sp ->||<- serial, 10 ->|
//
// The printer's address is the reply's source IP; the payload carries only MAC
// and serial, no model name. Note 3289 is the same UDP port Epson ENPC uses, so
// a composite IDiscoveryService can reuse the socket it already has.
// //
// All settings are the constants below. Edit them here; there are no arguments. // All settings are the constants below. Edit them here; there are no arguments.
// An AnyDesk TCP tunnel forwards this local endpoint to the remote printer's const string ProbeMagic = "SPRT-Infor";
// port 9100, so the printer is reachable as though it sat on this machine. const int DiscoveryPort = 3289;
// A refused connection here usually means the AnyDesk session or its tunnel is const int ReplyPort = 9100;
// down, not that the printer is off. const int ListenSeconds = 3;
const string PrinterIp = "127.0.0.1";
const int PrinterPort = 8888;
const int ConnectTimeoutSeconds = 5;
// Font A is 12 dots wide on a 576-dot line. Width magnification divides this. // EasySet sends from source port 9100 and the reply comes back to 9100. Whether
const int FontAColumns = 48; // the firmware honours the sender's port or hard-codes 9100 is unknown from a
const int HeightMagnification = 2; // single capture. Set this false to bind an ephemeral port instead: if replies
int[] widths = { 1, 2, 3 }; // still arrive, the firmware replies to the sender; if they stop, 9100 is
// hard-coded and any implementation must bind it.
bool bindSourcePort9100 = true;
// Blank lines between the last row and the cut. Literal LF bytes rather than byte[] probe = BuildProbe(ProbeMagic);
// ESC d n: LF is the most universally implemented feed in ESC/POS, and this
// firmware appears to ignore ESC d.
const int BlankLinesAfterText = 10;
// Set true to drop the ESC t 18 (code page 18 / CP852) selection. The ruler is Console.WriteLine("POS895 / SPRT discovery probe");
// pure ASCII either way; this only isolates whether ESC t itself is honoured. Console.WriteLine($" probe -> 255.255.255.255:{DiscoveryPort} ({probe.Length} bytes)");
bool useRawAscii = false; Console.WriteLine(Describe(probe));
byte[] job = BuildJob(FontAColumns, widths, HeightMagnification, BlankLinesAfterText, useRawAscii);
Console.WriteLine($"POS895 smoke test -> {PrinterIp}:{PrinterPort}");
Console.WriteLine($"Font A, height {HeightMagnification}x, widths {string.Join("x / ", widths)}x");
Console.WriteLine();
foreach (int w in widths)
{
int cols = FontAColumns / w;
Console.WriteLine($" GS ! 0x{SizeByte(w, HeightMagnification):X2} w={w} h={HeightMagnification} {cols,2} cols {Ruler(cols)}");
}
Console.WriteLine();
Console.WriteLine("Byte sequence:");
Console.WriteLine(Describe(job));
Console.WriteLine(); Console.WriteLine();
using var udp = new UdpClient(AddressFamily.InterNetwork);
udp.EnableBroadcast = true;
try try
{ {
await using var printer = new EpsonPrinter(); udp.Client.Bind(new IPEndPoint(IPAddress.Any, bindSourcePort9100 ? ReplyPort : 0));
Console.Write("Connecting... ");
await printer.ConnectAsync(PrinterIp, PrinterPort, ConnectTimeoutSeconds);
Console.WriteLine("connected.");
Console.Write($"Sending {job.Length} bytes... ");
await printer.SendRawCommandAsync(job);
Console.WriteLine("sent.");
Console.WriteLine();
Console.WriteLine("[OK] Bytes sent. All three rows should end at the same right");
Console.WriteLine(" margin, each in larger type than the one above it.");
return 0;
} }
catch (EpsonConnectionException ex) catch (SocketException ex)
{ {
Console.Error.WriteLine(); Console.Error.WriteLine($"[FAIL] Could not bind UDP {ReplyPort}: {ex.Message}");
Console.Error.WriteLine($"[FAIL] Could not reach {PrinterIp}:{PrinterPort}."); Console.Error.WriteLine(" Something else holds it, or this needs elevation.");
Console.Error.WriteLine(" Is the port forward to the printer's 9100 up?"); Console.Error.WriteLine(" Set bindSourcePort9100 = false to try an ephemeral port.");
Console.Error.WriteLine($" {ex.Message}");
if (ex.InnerException is not null)
{
Console.Error.WriteLine($" {ex.InnerException.Message}");
}
return 1;
}
catch (Exception ex)
{
Console.Error.WriteLine();
Console.Error.WriteLine($"[FAIL] {ex.GetType().Name}: {ex.Message}");
return 1; return 1;
} }
// The whole job as one array, so what gets listed above is exactly what goes on int localPort = ((IPEndPoint)udp.Client.LocalEndPoint!).Port;
// the wire. Console.WriteLine($" listening on UDP {localPort} for {ListenSeconds}s");
static byte[] BuildJob(int fontACols, int[] widths, int heightMag, int blankLines, bool rawAscii) Console.WriteLine();
await udp.SendAsync(probe, probe.Length, new IPEndPoint(IPAddress.Broadcast, DiscoveryPort));
var found = new Dictionary<string, string>();
DateTime deadline = DateTime.UtcNow.AddSeconds(ListenSeconds);
while (DateTime.UtcNow < deadline)
{ {
var job = new List<byte> { 0x1B, 0x40 }; // ESC @ initialize using var cts = new CancellationTokenSource(deadline - DateTime.UtcNow);
UdpReceiveResult res;
if (!rawAscii) try
{ {
job.AddRange(new byte[] { 0x1B, 0x74, 0x12 }); // ESC t 18 code page 852 res = await udp.ReceiveAsync(cts.Token);
}
catch (OperationCanceledException)
{
break;
} }
job.AddRange(new byte[] { 0x1B, 0x21, 0x00 }); // ESC ! 0 Font A, no attributes string ip = res.RemoteEndPoint.Address.ToString();
var parsed = ParseReply(res.Buffer);
foreach (int w in widths) if (parsed is null)
{ {
job.AddRange(new byte[] { 0x1D, 0x21, SizeByte(w, heightMag) }); Console.WriteLine($" {ip}:{res.RemoteEndPoint.Port} unrecognised, {res.Buffer.Length} bytes");
job.AddRange(Encoding.ASCII.GetBytes(Ruler(fontACols / w))); Console.WriteLine(Describe(res.Buffer));
job.Add(0x0A); continue;
} }
job.AddRange(new byte[] { 0x1D, 0x21, 0x00 }); // GS ! 0 back to 1x1 (string mac, string serial) = parsed.Value;
found[ip] = mac;
for (int i = 0; i < blankLines; i++) Console.WriteLine($" FOUND {ip,-15} mac {mac} serial {serial}");
{
job.Add(0x0A); // blank line
}
job.AddRange(new byte[] { 0x1D, 0x56, 0x00 }); // GS V 0 full cut
return job.ToArray();
} }
// GS ! n: bits 4-6 are width magnification - 1, bits 0-2 height - 1. Same Console.WriteLine();
// formula as EpsonPrinter.SetFontSizeAsync. if (found.Count == 0)
static byte SizeByte(int width, int height) =>
(byte)(((height - 1) & 0x07) | (((width - 1) & 0x07) << 4));
// "|---" repeated and cut to an exact column count, so the pipes fall every
// 4 characters and the row ends precisely at the claimed right margin.
static string Ruler(int columns)
{ {
var sb = new StringBuilder(); Console.WriteLine("[--] No replies.");
while (sb.Length < columns) Console.WriteLine(" - is this machine on the printer's broadcast domain?");
{ Console.WriteLine(" - does the firewall allow inbound UDP on this port?");
sb.Append("|---"); Console.WriteLine($" - try flipping bindSourcePort9100 (currently {bindSourcePort9100}).");
} return 1;
return sb.ToString(0, columns);
} }
// Hex plus a printable-ASCII gutter, so the output can be compared against the Console.WriteLine($"[OK] {found.Count} printer(s) answered SPRT discovery.");
// printer's hex dump mode (open cover, FEED twice within 3 s, close cover). return 0;
// "SPRT-Infor" followed by 00 00 0D. The trailing 0x0D is also the total length
// of the datagram, which may or may not be a coincidence.
static byte[] BuildProbe(string magic)
{
var p = new List<byte>(Encoding.ASCII.GetBytes(magic));
p.AddRange(new byte[] { 0x00, 0x00, 0x0D });
return p.ToArray();
}
// 17 ASCII bytes of dashed lowercase MAC, space padding, then the serial.
static (string Mac, string Serial)? ParseReply(byte[] payload)
{
if (payload.Length < 17)
{
return null;
}
string text = Encoding.ASCII.GetString(payload);
string mac = text[..17];
bool looksLikeMac = mac.Split('-') is { Length: 6 } parts
&& parts.All(p => p.Length == 2 && p.All(Uri.IsHexDigit));
return looksLikeMac ? (mac, text[17..].Trim()) : null;
}
// Hex plus a printable-ASCII gutter.
static string Describe(byte[] bytes) static string Describe(byte[] bytes)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
@@ -158,3 +158,78 @@ static string Describe(byte[] bytes)
return sb.ToString().TrimEnd(); return sb.ToString().TrimEnd();
} }
/* =====================================================================
PRINT TEST -- disabled.
Confirmed working against the printer through the AnyDesk tunnel on
127.0.0.1:8888. Font A is 48 columns on the 576-dot line, and GS !
magnification works at width 1x/2x/3x with height 2x. ESC d n appears
to be ignored, hence the literal LF bytes before the cut.
To re-enable, move this code above the discovery section.
const string PrinterIp = "127.0.0.1";
const int PrinterPort = 8888;
const int ConnectTimeoutSeconds = 5;
const int FontAColumns = 48;
const int HeightMagnification = 2;
int[] widths = { 1, 2, 3 };
const int BlankLinesAfterText = 10;
bool useRawAscii = false;
byte[] job = BuildJob(FontAColumns, widths, HeightMagnification, BlankLinesAfterText, useRawAscii);
Console.WriteLine("Byte sequence:");
Console.WriteLine(Describe(job));
await using var printer = new EpsonPrinter();
await printer.ConnectAsync(PrinterIp, PrinterPort, ConnectTimeoutSeconds);
await printer.SendRawCommandAsync(job);
static byte[] BuildJob(int fontACols, int[] widths, int heightMag, int blankLines, bool rawAscii)
{
var job = new List<byte> { 0x1B, 0x40 }; // ESC @ initialize
if (!rawAscii)
{
job.AddRange(new byte[] { 0x1B, 0x74, 0x12 }); // ESC t 18 code page 852
}
job.AddRange(new byte[] { 0x1B, 0x21, 0x00 }); // ESC ! 0 Font A
foreach (int w in widths)
{
job.AddRange(new byte[] { 0x1D, 0x21, SizeByte(w, heightMag) });
job.AddRange(Encoding.ASCII.GetBytes(Ruler(fontACols / w)));
job.Add(0x0A);
}
job.AddRange(new byte[] { 0x1D, 0x21, 0x00 }); // GS ! 0 back to 1x1
for (int i = 0; i < blankLines; i++)
{
job.Add(0x0A);
}
job.AddRange(new byte[] { 0x1D, 0x56, 0x00 }); // GS V 0 full cut
return job.ToArray();
}
// GS ! n: bits 4-6 width magnification - 1, bits 0-2 height - 1.
static byte SizeByte(int width, int height) =>
(byte)(((height - 1) & 0x07) | (((width - 1) & 0x07) << 4));
static string Ruler(int columns)
{
var sb = new StringBuilder();
while (sb.Length < columns)
{
sb.Append("|---");
}
return sb.ToString(0, columns);
}
===================================================================== */

25
docs/captures/README.md Normal file
View File

@@ -0,0 +1,25 @@
# Packet captures
## easyset.pcapng / easyset.etl
`pktmon` capture taken on 2026-09-03 on a machine on the POS895's network
segment, while running the vendor's `EasySet Tool.exe` and clicking its scan.
Taken to reverse engineer the vendor discovery protocol, since the POS895 does
not answer Epson's ENPC broadcast and the EasySet binary yields no readable
strings. The resulting wire format is written up under "SPRT discovery protocol"
in `POS895_POS896_integration_notes.md` and implemented in `Pos895Test`.
`easyset.etl` is the raw pktmon output; `easyset.pcapng` is the converted form
and the one worth opening.
What is in it:
- `10.1.250.71:9100 -> 255.255.255.255:3289` the `SPRT-Infor` discovery query
- `10.1.252.219:3289 -> 10.1.250.71:9100` the printer's MAC + serial reply
- `10.1.252.148:58165 -> 255.255.255.255:3289` an Epson `EPSONQ` broadcast that
the POS895 does not answer, which is the point
- unrelated noise: Ubiquiti AP beacons on UDP 10001, SSDP, mDNS, NetBIOS
Note pktmon records each frame several times, once per stack layer, so every
packet appears about six times.

BIN
docs/captures/easyset.etl Normal file

Binary file not shown.

Binary file not shown.