using System.Net; using System.Net.Sockets; using System.Text; namespace Inspectron.Epson; /// /// Service Location Protocol (SLP) Discovery for Epson Printers /// Implements SLPv2 (RFC 2608) for printer discovery on port 427 /// public class EpsonPrinterDiscovery { private const int SLP_PORT = 427; private const int DISCOVER_TIMEOUT_MS = 5000; private const string SLP_MULTICAST_ADDRESS = "239.255.255.253"; // SLP multicast group // SLP Message Function IDs (SLPv2) private const byte FUNC_SRVRQST = 1; // Service Request private const byte FUNC_SRVRPLY = 2; // Service Reply private const byte FUNC_DAADVERT = 8; // Directory Agent Advertisement private const byte FUNC_ATTRRQST = 6; // Attribute Request private const byte FUNC_ATTRRPLY = 7; // Attribute Reply private static ushort _nextXid = 1; /// /// Discovers printers using SLP (Service Location Protocol) /// /// Service type to search for (default: "service:printer") /// Scope to search in (default: "DEFAULT") /// Timeout in milliseconds /// List of discovered printers public static List DiscoverPrinters( string serviceType = "service:printer", string scope = "DEFAULT", int timeoutMs = DISCOVER_TIMEOUT_MS) { var discoveredPrinters = new List(); var printerUrls = new HashSet(); using (var udpClient = new UdpClient()) { try { // Bind to local port udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 0)); // Join multicast group var multicastAddress = IPAddress.Parse(SLP_MULTICAST_ADDRESS); udpClient.JoinMulticastGroup(multicastAddress); // Set receive timeout udpClient.Client.ReceiveTimeout = timeoutMs; // Build and send Service Request var xid = _nextXid++; byte[] serviceRequest = BuildServiceRequest(serviceType, scope, xid); var multicastEndpoint = new IPEndPoint(multicastAddress, SLP_PORT); Console.WriteLine($"[SLP Discovery] Sending Service Request to {multicastEndpoint}..."); Console.WriteLine($"[SLP Discovery] Service Type: {serviceType}, Scope: {scope}"); int bytesSent = udpClient.Send(serviceRequest, serviceRequest.Length, multicastEndpoint); Console.WriteLine($"[SLP Discovery] Sent {bytesSent} bytes"); // Wait for responses var startTime = DateTime.Now; int receiveCount = 0; while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs) { try { IPEndPoint remoteEndpoint = null; byte[] responseData = udpClient.Receive(ref remoteEndpoint); receiveCount++; Console.WriteLine($"[SLP Discovery] Received {responseData.Length} bytes from {remoteEndpoint.Address}"); // Parse SLP response if (IsValidSlpResponse(responseData, xid)) { var printers = ParseServiceReply(responseData, remoteEndpoint.Address.ToString()); foreach (var printer in printers) { if (!printerUrls.Contains(printer.ServiceUrl)) { discoveredPrinters.Add(printer); printerUrls.Add(printer.ServiceUrl); Console.WriteLine($"[SLP Discovery] ✓ Found printer: {printer.ServiceUrl}"); } } } } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut) { // Timeout - no more responses break; } } Console.WriteLine($"[SLP Discovery] Discovery complete. Found {discoveredPrinters.Count} printer(s)"); // Leave multicast group udpClient.DropMulticastGroup(multicastAddress); } catch (Exception ex) { Console.WriteLine($"[SLP Discovery] Error: {ex.Message}"); } } return discoveredPrinters; } /// /// Discovers printers asynchronously /// public static async Task> DiscoverPrintersAsync( string serviceType = "service:printer", string scope = "DEFAULT", int timeoutMs = DISCOVER_TIMEOUT_MS) { return await Task.Run(() => DiscoverPrinters(serviceType, scope, timeoutMs)); } /// /// Builds an SLP Service Request packet (SLPv2 - RFC 2608) /// private static byte[] BuildServiceRequest(string serviceType, string scope, ushort xid) { using (var ms = new System.IO.MemoryStream()) using (var writer = new System.IO.BinaryWriter(ms)) { // SLP Header (14 bytes for SLPv2) writer.Write((byte)2); // Version (SLPv2) writer.Write(FUNC_SRVRQST); // Function: Service Request // Length placeholder (will be updated at end) long lengthPosition = ms.Position; writer.Write((byte)0); writer.Write((ushort)0); // Length (3 bytes, big-endian) // Flags (2 bytes) ushort flags = 0x2000; // Request Multicast flag writer.Write((byte)((flags >> 8) & 0xFF)); writer.Write((byte)(flags & 0xFF)); // Next Extension Offset (3 bytes) - 0 for no extensions writer.Write((byte)0); writer.Write((ushort)0); // XID (Transaction ID) - 2 bytes writer.Write((byte)((xid >> 8) & 0xFF)); writer.Write((byte)(xid & 0xFF)); // Language Tag (2 + length bytes) - "en" string langTag = "en"; writer.Write((ushort)langTag.Length); writer.Write(Encoding.ASCII.GetBytes(langTag)); // --- Service Request Body --- // Previous Responder List (2 bytes for count) writer.Write((ushort)0); // No previous responders // Service Type (2 + length bytes) writer.Write((ushort)serviceType.Length); writer.Write(Encoding.ASCII.GetBytes(serviceType)); // Scope List (2 + length bytes) writer.Write((ushort)scope.Length); writer.Write(Encoding.ASCII.GetBytes(scope)); // Predicate (2 + length bytes) - Empty for "all" writer.Write((ushort)0); // No predicate (match all) // SLP SPI String (2 + length bytes) - Empty (no authentication) writer.Write((ushort)0); // Update length field byte[] packet = ms.ToArray(); int totalLength = packet.Length; // Write length in big-endian format (3 bytes starting at position 2) packet[2] = (byte)((totalLength >> 16) & 0xFF); packet[3] = (byte)((totalLength >> 8) & 0xFF); packet[4] = (byte)(totalLength & 0xFF); return packet; } } /// /// Validates if response is a proper SLP Service Reply /// private static bool IsValidSlpResponse(byte[] response, ushort expectedXid) { if (response == null || response.Length < 14) return false; // Check version (must be 2) if (response[0] != 2) return false; // Check function (should be Service Reply = 2) if (response[1] != FUNC_SRVRPLY) return false; // Check XID matches ushort xid = (ushort)((response[12] << 8) | response[13]); if (xid != expectedXid) return false; return true; } /// /// Parses SLP Service Reply packet /// private static List ParseServiceReply(byte[] response, string sourceIp) { var printers = new List(); try { int offset = 14; // Skip SLP header // Read Language Tag ushort langTagLen = ReadUInt16(response, ref offset); string langTag = ReadString(response, ref offset, langTagLen); // Read Error Code (2 bytes) ushort errorCode = ReadUInt16(response, ref offset); if (errorCode != 0) { Console.WriteLine($"[SLP Parser] Error code in response: {errorCode}"); return printers; } // Read URL Entry Count ushort urlCount = ReadUInt16(response, ref offset); Console.WriteLine($"[SLP Parser] Found {urlCount} URL(s) in response"); // Parse each URL entry for (int i = 0; i < urlCount; i++) { try { var printer = new DiscoveredPrinter { DiscoveredAt = DateTime.Now, RawResponse = response }; // Reserved byte offset++; // Lifetime (2 bytes) printer.Lifetime = ReadUInt16(response, ref offset); // URL Length and URL ushort urlLen = ReadUInt16(response, ref offset); printer.ServiceUrl = ReadString(response, ref offset, urlLen); // Parse URL to extract IP and port ParseServiceUrl(printer.ServiceUrl, printer); // Auth Block Count (1 byte) byte authBlockCount = response[offset++]; // Skip auth blocks if present for (int j = 0; j < authBlockCount; j++) { // Skip authentication blocks (not typically used) ushort blockLen = ReadUInt16(response, ref offset); offset += blockLen; } printers.Add(printer); } catch (Exception ex) { Console.WriteLine($"[SLP Parser] Error parsing URL entry {i}: {ex.Message}"); } } } catch (Exception ex) { Console.WriteLine($"[SLP Parser] Error parsing response: {ex.Message}"); } return printers; } /// /// Parses a service URL to extract IP address and port /// Examples: /// service:printer:ipp://192.168.1.100:631/printers/lp /// service:printer:lpr://192.168.1.100/queue /// private static void ParseServiceUrl(string serviceUrl, DiscoveredPrinter printer) { try { // Extract service type if (serviceUrl.StartsWith("service:")) { var parts = serviceUrl.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2) { printer.ServiceType = parts[1]; // e.g., "printer" } } // Extract IP and port from URL // Pattern: protocol://ip:port/path var protocolIndex = serviceUrl.IndexOf("://"); if (protocolIndex > 0) { var afterProtocol = serviceUrl.Substring(protocolIndex + 3); var slashIndex = afterProtocol.IndexOf('/'); var hostPort = slashIndex > 0 ? afterProtocol.Substring(0, slashIndex) : afterProtocol; // Check for port var colonIndex = hostPort.LastIndexOf(':'); if (colonIndex > 0 && colonIndex < hostPort.Length - 1) { printer.IPAddress = hostPort.Substring(0, colonIndex); if (int.TryParse(hostPort.Substring(colonIndex + 1), out int port)) { printer.Port = port; } } else { printer.IPAddress = hostPort; // Default ports based on protocol if (serviceUrl.Contains("ipp://")) printer.Port = 631; else if (serviceUrl.Contains("lpr://")) printer.Port = 515; else printer.Port = 9100; // Default ESC/POS } } } catch (Exception ex) { Console.WriteLine($"[SLP Parser] Error parsing service URL: {ex.Message}"); } } /// /// Reads a 16-bit unsigned integer in big-endian format /// private static ushort ReadUInt16(byte[] data, ref int offset) { if (offset + 2 > data.Length) throw new InvalidOperationException("Not enough data to read UInt16"); ushort value = (ushort)((data[offset] << 8) | data[offset + 1]); offset += 2; return value; } /// /// Reads a string of specified length /// private static string ReadString(byte[] data, ref int offset, int length) { if (offset + length > data.Length) throw new InvalidOperationException("Not enough data to read string"); string value = Encoding.UTF8.GetString(data, offset, length); offset += length; return value; } /// /// Prints hex dump of byte array for debugging /// public static void PrintHexDump(byte[] data, int bytesPerLine = 16) { Console.WriteLine("\n=== SLP Hex Dump ==="); for (int i = 0; i < data.Length; i += bytesPerLine) { Console.Write($"{i:X4}: "); for (int j = 0; j < bytesPerLine; j++) { if (i + j < data.Length) Console.Write($"{data[i + j]:X2} "); else Console.Write(" "); } Console.Write(" | "); for (int j = 0; j < bytesPerLine && i + j < data.Length; j++) { byte b = data[i + j]; char c = (b >= 32 && b < 127) ? (char)b : '.'; Console.Write(c); } Console.WriteLine(); } Console.WriteLine("===================\n"); } }