using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging; namespace Inspectron.Epson; public class DiscoveryService : IDiscoveryService { private const int DiscoveryPort = 3289; private const string DiscoveryMessage = "EPSONQ"; private const int MessageLength = 14; private readonly ILogger? _logger; public DiscoveryService(ILogger? logger = null) { _logger = logger; } /// /// Discovers Epson printers on the local network using UDP broadcast /// /// How long to wait for responses (default: 5 seconds) /// Optional callback invoked when a printer is discovered /// List of discovered printer information public async Task> DiscoverPrintersAsync( TimeSpan? timeout = null, Action? onPrinterDiscovered = null) { timeout ??= TimeSpan.FromSeconds(5); using var udpClient = new UdpClient(); udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 0)); // Send discovery broadcast message var msg = BuildDiscoveryMessage(); var broadcastEndpoint = new IPEndPoint(IPAddress.Broadcast, DiscoveryPort); await udpClient.SendAsync(msg, msg.Length, broadcastEndpoint); // Receive responses var discoveredPrinters = new List(); var endTime = DateTime.UtcNow.Add(timeout.Value); while (DateTime.UtcNow < endTime) { try { var remainingTime = endTime - DateTime.UtcNow; if (remainingTime <= TimeSpan.Zero) break; var receiveTask = udpClient.ReceiveAsync(); var timeoutTask = Task.Delay(remainingTime); var completedTask = await Task.WhenAny(receiveTask, timeoutTask); if (completedTask == receiveTask) { var result = await receiveTask; // Check if we've already discovered this printer if (!discoveredPrinters.Any(p => p.IPAddress.Equals(result.RemoteEndPoint.Address))) { var ipAddress = result.RemoteEndPoint.Address.ToString(); var printer = new DiscoveredPrinter { IPAddress = ipAddress, DiscoveredAt = DateTime.UtcNow }; // Try to get printer model by connecting and querying printer.ModelName = await GetPrinterModelAsync(ipAddress); discoveredPrinters.Add(printer); onPrinterDiscovered?.Invoke(printer); } } else { break; // Timeout reached } } catch (Exception) { // Ignore errors and continue waiting for other responses break; } } return discoveredPrinters; } private static byte[] BuildDiscoveryMessage() { // msg = b'EPSONQ' + bytes([0x00] * 8) var msg = new byte[MessageLength]; Array.Copy(System.Text.Encoding.ASCII.GetBytes(DiscoveryMessage), msg, 6); msg[6] = 3; for (int i = 7; i < msg.Length; i++) { msg[i] = 0x00; } msg[10] = 0x10; return msg; } /// /// Connects to a printer and retrieves its model name /// private async Task GetPrinterModelAsync(string ipAddress, int port = 9100) { try { _logger?.LogDebug("Connecting to printer at {IP} to retrieve model", ipAddress); await using var epsonPrinter = new EpsonPrinter(); await epsonPrinter.ConnectAsync(ipAddress, port, timeoutSeconds: 3); var printerId = await epsonPrinter.GetPrinterIdAsync(); if (printerId.HasValue) { var modelName = GetModelNameFromId(printerId.Value); _logger?.LogDebug("Printer at {IP} identified as {Model} (ID: 0x{Id:X2})", ipAddress, modelName, printerId.Value); return modelName; } _logger?.LogDebug("Could not retrieve printer ID from {IP}", ipAddress); return null; } catch (Exception ex) { _logger?.LogWarning(ex, "Failed to get printer model from {IP}", ipAddress); return null; } } /// /// Converts a printer ID byte to a model name string /// private static string GetModelNameFromId(byte printerId) { return printerId switch { 0x01 => "TM-T30III", 0x0D => "TM-U220II", 0x13 => "TM-U220II", _ => $"Unknown (0x{printerId:X2})" }; } }