168 lines
5.4 KiB
C#
168 lines
5.4 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Inspectron.Epson;
|
|
|
|
/// <summary>
|
|
/// Discovery for SPRT-family ESC/POS printers (POS895 / POS896), which do not
|
|
/// answer Epson's ENPC broadcast.
|
|
///
|
|
/// Protocol, reverse engineered from a packet capture of the vendor's EasySet
|
|
/// tool and documented in POS895_POS896_integration_notes.md:
|
|
///
|
|
/// 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 -->||<- 12 sp ->||<- serial ->|
|
|
/// </summary>
|
|
public class SprtDiscoveryService : IDiscoveryService
|
|
{
|
|
private const string ProbeMagic = "SPRT-Infor";
|
|
private const int MacLength = 17;
|
|
private const int DiscoveryPort = 3289;
|
|
private const int ReplyPort = 9100;
|
|
|
|
/// <summary>Model reported for anything answering SPRT. The reply carries no
|
|
/// model name, and a POS896 is indistinguishable from a POS895 here.</summary>
|
|
public const string SprtModelName = "POS895";
|
|
|
|
private readonly ILogger? _logger;
|
|
|
|
public SprtDiscoveryService(ILogger? logger = null)
|
|
{
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>The 13-byte discovery query, byte-identical to EasySet's.</summary>
|
|
public static byte[] BuildProbe()
|
|
{
|
|
var probe = new List<byte>(Encoding.ASCII.GetBytes(ProbeMagic));
|
|
probe.AddRange(new byte[] { 0x00, 0x00, 0x0D });
|
|
return probe.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a discovery reply. Returns false for anything that is not a
|
|
/// dashed 17-character MAC followed by an optional serial, so unrelated
|
|
/// broadcasts landing on our port are ignored rather than thrown on.
|
|
/// </summary>
|
|
public static bool TryParseReply(byte[] payload, string ipAddress, out DiscoveredPrinter? printer)
|
|
{
|
|
printer = null;
|
|
|
|
if (payload.Length < MacLength)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string text = Encoding.ASCII.GetString(payload);
|
|
string mac = text[..MacLength];
|
|
|
|
string[] octets = mac.Split('-');
|
|
if (octets.Length != 6 || !octets.All(o => o.Length == 2 && o.All(Uri.IsHexDigit)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
printer = new DiscoveredPrinter
|
|
{
|
|
IPAddress = ipAddress,
|
|
MACAddress = mac,
|
|
Serial = text[MacLength..].Trim(),
|
|
ModelName = SprtModelName,
|
|
Port = 9100,
|
|
ServiceType = "SPRT",
|
|
RawResponse = payload,
|
|
DiscoveredAt = DateTime.UtcNow
|
|
};
|
|
|
|
return true;
|
|
}
|
|
|
|
public async Task<List<DiscoveredPrinter>> DiscoverPrintersAsync(
|
|
TimeSpan? timeout = null,
|
|
Action<DiscoveredPrinter>? onPrinterDiscovered = null)
|
|
{
|
|
timeout ??= TimeSpan.FromSeconds(5);
|
|
var discovered = new List<DiscoveredPrinter>();
|
|
|
|
using var udpClient = new UdpClient(AddressFamily.InterNetwork);
|
|
udpClient.EnableBroadcast = true;
|
|
|
|
try
|
|
{
|
|
// EasySet sends from 9100 and the printer replies to 9100. Whether the
|
|
// firmware honours the sender's port or hard-codes 9100 is unknown, so
|
|
// bind the port that is proven to work.
|
|
udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, ReplyPort));
|
|
}
|
|
catch (SocketException ex)
|
|
{
|
|
_logger?.LogWarning(ex,
|
|
"SPRT discovery could not bind UDP {Port}; skipping this pass", ReplyPort);
|
|
return discovered;
|
|
}
|
|
|
|
var probe = BuildProbe();
|
|
|
|
try
|
|
{
|
|
await udpClient.SendAsync(probe, probe.Length,
|
|
new IPEndPoint(IPAddress.Broadcast, DiscoveryPort));
|
|
}
|
|
catch (SocketException ex)
|
|
{
|
|
_logger?.LogWarning(ex, "SPRT discovery broadcast failed");
|
|
return discovered;
|
|
}
|
|
|
|
DateTime deadline = DateTime.UtcNow.Add(timeout.Value);
|
|
|
|
while (DateTime.UtcNow < deadline)
|
|
{
|
|
using var cts = new CancellationTokenSource(deadline - DateTime.UtcNow);
|
|
UdpReceiveResult result;
|
|
|
|
try
|
|
{
|
|
result = await udpClient.ReceiveAsync(cts.Token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
catch (SocketException ex)
|
|
{
|
|
_logger?.LogDebug(ex, "SPRT discovery receive error");
|
|
continue;
|
|
}
|
|
|
|
string ip = result.RemoteEndPoint.Address.ToString();
|
|
|
|
if (discovered.Any(p => p.IPAddress == ip))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!TryParseReply(result.Buffer, ip, out var printer) || printer is null)
|
|
{
|
|
_logger?.LogDebug("Ignoring {Length}-byte non-SPRT datagram from {Ip}",
|
|
result.Buffer.Length, ip);
|
|
continue;
|
|
}
|
|
|
|
_logger?.LogInformation("SPRT discovery found {Model} at {Ip} (MAC {Mac}, serial {Serial})",
|
|
printer.ModelName, printer.IPAddress, printer.MACAddress, printer.Serial);
|
|
|
|
discovered.Add(printer);
|
|
onPrinterDiscovered?.Invoke(printer);
|
|
}
|
|
|
|
return discovered;
|
|
}
|
|
}
|