using System.Net; using System.Net.Sockets; using System.Text; using Inspectron.Epson; // POS895 discovery probe. // // The POS895 does not answer Epson's ENPC broadcast, but it does answer the // vendor's own SPRT discovery, which the EasySet tool uses. Protocol, taken from // 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 :3289 back to :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. const string ProbeMagic = "SPRT-Infor"; const int DiscoveryPort = 3289; const int ReplyPort = 9100; const int ListenSeconds = 3; // EasySet sends from source port 9100 and the reply comes back to 9100. Whether // the firmware honours the sender's port or hard-codes 9100 is unknown from a // single capture. Set this false to bind an ephemeral port instead: if replies // still arrive, the firmware replies to the sender; if they stop, 9100 is // hard-coded and any implementation must bind it. bool bindSourcePort9100 = true; byte[] probe = BuildProbe(ProbeMagic); Console.WriteLine("POS895 / SPRT discovery probe"); Console.WriteLine($" probe -> 255.255.255.255:{DiscoveryPort} ({probe.Length} bytes)"); Console.WriteLine(Describe(probe)); Console.WriteLine(); using var udp = new UdpClient(AddressFamily.InterNetwork); udp.EnableBroadcast = true; try { udp.Client.Bind(new IPEndPoint(IPAddress.Any, bindSourcePort9100 ? ReplyPort : 0)); } catch (SocketException ex) { Console.Error.WriteLine($"[FAIL] Could not bind UDP {ReplyPort}: {ex.Message}"); Console.Error.WriteLine(" Something else holds it, or this needs elevation."); Console.Error.WriteLine(" Set bindSourcePort9100 = false to try an ephemeral port."); return 1; } int localPort = ((IPEndPoint)udp.Client.LocalEndPoint!).Port; Console.WriteLine($" listening on UDP {localPort} for {ListenSeconds}s"); Console.WriteLine(); await udp.SendAsync(probe, probe.Length, new IPEndPoint(IPAddress.Broadcast, DiscoveryPort)); var found = new Dictionary(); DateTime deadline = DateTime.UtcNow.AddSeconds(ListenSeconds); while (DateTime.UtcNow < deadline) { using var cts = new CancellationTokenSource(deadline - DateTime.UtcNow); UdpReceiveResult res; try { res = await udp.ReceiveAsync(cts.Token); } catch (OperationCanceledException) { break; } string ip = res.RemoteEndPoint.Address.ToString(); var parsed = ParseReply(res.Buffer); if (parsed is null) { Console.WriteLine($" {ip}:{res.RemoteEndPoint.Port} unrecognised, {res.Buffer.Length} bytes"); Console.WriteLine(Describe(res.Buffer)); continue; } (string mac, string serial) = parsed.Value; found[ip] = mac; Console.WriteLine($" FOUND {ip,-15} mac {mac} serial {serial}"); } Console.WriteLine(); if (found.Count == 0) { Console.WriteLine("[--] No replies."); Console.WriteLine(" - is this machine on the printer's broadcast domain?"); Console.WriteLine(" - does the firewall allow inbound UDP on this port?"); Console.WriteLine($" - try flipping bindSourcePort9100 (currently {bindSourcePort9100})."); return 1; } Console.WriteLine($"[OK] {found.Count} printer(s) answered SPRT discovery."); 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(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) { var sb = new StringBuilder(); for (int offset = 0; offset < bytes.Length; offset += 16) { int count = Math.Min(16, bytes.Length - offset); sb.Append($" {offset:X4} "); for (int i = 0; i < 16; i++) { sb.Append(i < count ? $"{bytes[offset + i]:X2} " : " "); } sb.Append(' '); for (int i = 0; i < count; i++) { byte b = bytes[offset + i]; sb.Append(b >= 0x20 && b < 0x7F ? (char)b : '.'); } sb.AppendLine(); } 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 { 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); } ===================================================================== */