Add project files.
This commit is contained in:
9
Inspectron.Epson/.claude/settings.local.json
Normal file
9
Inspectron.Epson/.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebSearch"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
579
Inspectron.Epson/BonjourDiscovery.cs
Normal file
579
Inspectron.Epson/BonjourDiscovery.cs
Normal file
@@ -0,0 +1,579 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// Bonjour/mDNS (Multicast DNS) Service Discovery
|
||||
/// Implements RFC 6762 (mDNS) and RFC 6763 (DNS-SD)
|
||||
/// Port 5353, Multicast 224.0.0.251 (IPv4) / FF02::FB (IPv6)
|
||||
/// </summary>
|
||||
public class BonjourDiscovery
|
||||
{
|
||||
private const int MDNS_PORT = 5353;
|
||||
private const string MDNS_MULTICAST_IPV4 = "224.0.0.251";
|
||||
//private const string MDNS_MULTICAST_IPV4 = "255.255.255.255";
|
||||
private const string MDNS_MULTICAST_IPV6 = "FF02::FB";
|
||||
private const int DEFAULT_TIMEOUT_MS = 10000;
|
||||
|
||||
private static ushort _nextTransactionId = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Well-known service types for Bonjour/mDNS
|
||||
/// </summary>
|
||||
public static class ServiceTypes
|
||||
{
|
||||
public const string PRINTER_IPP = "_ipp._tcp.local.";
|
||||
public const string PRINTER_IPP_TLS = "_ipps._tcp.local.";
|
||||
public const string PRINTER_LPD = "_printer._tcp.local.";
|
||||
public const string PRINTER_PDL_DATASTREAM = "_pdl-datastream._tcp.local.";
|
||||
public const string SCANNER = "_scanner._tcp.local.";
|
||||
public const string HTTP = "_http._tcp.local.";
|
||||
public const string HTTPS = "_https._tcp.local.";
|
||||
public const string SSH = "_ssh._tcp.local.";
|
||||
public const string FTP = "_ftp._tcp.local.";
|
||||
public const string SFTP = "_sftp-ssh._tcp.local.";
|
||||
public const string SMB = "_smb._tcp.local.";
|
||||
public const string AFP = "_afpovertcp._tcp.local.";
|
||||
public const string NFS = "_nfs._tcp.local.";
|
||||
public const string AIRPLAY = "_airplay._tcp.local.";
|
||||
public const string AIRPRINT = "_ipp._tcp.local.";
|
||||
public const string RAOP = "_raop._tcp.local.";
|
||||
public const string HOMEKIT = "_hap._tcp.local.";
|
||||
public const string CHROMECAST = "_googlecast._tcp.local.";
|
||||
public const string SPOTIFY_CONNECT = "_spotify-connect._tcp.local.";
|
||||
public const string MQTT = "_mqtt._tcp.local.";
|
||||
public const string WORKSTATION = "_workstation._tcp.local.";
|
||||
public const string SERVICES_DNS_SD = "_services._dns-sd._udp.local.";
|
||||
}
|
||||
|
||||
// _dosvc._tcp.local - discovery of all devices
|
||||
|
||||
/// <summary>
|
||||
/// Discovers services of specified type via mDNS/Bonjour
|
||||
/// </summary>
|
||||
public static List<BonjourService> DiscoverServices(
|
||||
string serviceType = "_ipp._tcp.local.",
|
||||
int timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
bool useIPv6 = false)
|
||||
{
|
||||
var discoveredServices = new List<BonjourService>();
|
||||
var serviceKeys = new HashSet<string>();
|
||||
|
||||
using (var udpClient = new UdpClient())
|
||||
{
|
||||
try
|
||||
{
|
||||
// Configure socket
|
||||
udpClient.Client.SetSocketOption(
|
||||
SocketOptionLevel.Socket,
|
||||
SocketOptionName.ReuseAddress,
|
||||
true);
|
||||
|
||||
if (useIPv6)
|
||||
{
|
||||
udpClient.Client.Bind(new IPEndPoint(IPAddress.IPv6Any, MDNS_PORT));
|
||||
var multicastAddress = IPAddress.Parse(MDNS_MULTICAST_IPV6);
|
||||
udpClient.JoinMulticastGroup(multicastAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, MDNS_PORT));
|
||||
var multicastAddress = IPAddress.Parse(MDNS_MULTICAST_IPV4);
|
||||
udpClient.JoinMulticastGroup(multicastAddress);
|
||||
}
|
||||
|
||||
udpClient.Client.ReceiveTimeout = timeoutMs;
|
||||
|
||||
// Build and send mDNS query
|
||||
var transactionId = _nextTransactionId++;
|
||||
byte[] query = BuildMdnsQuery(serviceType, transactionId);
|
||||
|
||||
var multicastEndpoint = new IPEndPoint(
|
||||
useIPv6 ? IPAddress.Parse(MDNS_MULTICAST_IPV6) : IPAddress.Parse(MDNS_MULTICAST_IPV4),
|
||||
MDNS_PORT);
|
||||
|
||||
Console.WriteLine($"[mDNS] Querying: {serviceType}");
|
||||
udpClient.Send(query, query.Length, multicastEndpoint);
|
||||
|
||||
// Collect responses
|
||||
var startTime = DateTime.Now;
|
||||
while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs)
|
||||
{
|
||||
try
|
||||
{
|
||||
IPEndPoint remoteEndpoint = null;
|
||||
byte[] responseData = udpClient.Receive(ref remoteEndpoint);
|
||||
|
||||
if (IsValidMdnsResponse(responseData))
|
||||
{
|
||||
var services = ParseMdnsResponse(responseData, remoteEndpoint.Address.ToString());
|
||||
|
||||
foreach (var service in services)
|
||||
{
|
||||
string key = $"{service.ServiceName}.{service.ServiceType}";
|
||||
if (!serviceKeys.Contains(key))
|
||||
{
|
||||
discoveredServices.Add(service);
|
||||
serviceKeys.Add(key);
|
||||
Console.WriteLine($"[mDNS] ✓ Found: {service.ToString()}");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"[mDNS] Found {discoveredServices.Count} service(s)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[mDNS] Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return discoveredServices;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers services asynchronously
|
||||
/// </summary>
|
||||
public static async Task<List<BonjourService>> DiscoverServicesAsync(
|
||||
string serviceType = "_ipp._tcp.local.",
|
||||
int timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
bool useIPv6 = false)
|
||||
{
|
||||
return await Task.Run(() => DiscoverServices(serviceType, timeoutMs, useIPv6));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers all available service types on the network
|
||||
/// </summary>
|
||||
public static List<string> DiscoverServiceTypes(int timeoutMs = DEFAULT_TIMEOUT_MS)
|
||||
{
|
||||
Console.WriteLine("[mDNS] Discovering available service types...");
|
||||
|
||||
var services = DiscoverServices("_services._dns-sd._udp.local.", timeoutMs);
|
||||
var serviceTypes = new HashSet<string>();
|
||||
|
||||
foreach (var service in services)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(service.ServiceType))
|
||||
{
|
||||
serviceTypes.Add(service.ServiceType);
|
||||
}
|
||||
}
|
||||
|
||||
return serviceTypes.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an mDNS query packet
|
||||
/// </summary>
|
||||
private static byte[] BuildMdnsQuery(string serviceType, ushort transactionId)
|
||||
{
|
||||
using (var ms = new System.IO.MemoryStream())
|
||||
using (var writer = new System.IO.BinaryWriter(ms))
|
||||
{
|
||||
// DNS Header
|
||||
writer.Write((byte)((transactionId >> 8) & 0xFF));
|
||||
writer.Write((byte)(transactionId & 0xFF));
|
||||
|
||||
// Flags: Standard query (0x0000)
|
||||
writer.Write((byte)0x00);
|
||||
writer.Write((byte)0x00);
|
||||
|
||||
// Question count: 1
|
||||
writer.Write((byte)0x00);
|
||||
writer.Write((byte)0x01);
|
||||
|
||||
// Answer count: 0
|
||||
writer.Write((byte)0x00);
|
||||
writer.Write((byte)0x00);
|
||||
|
||||
// Authority count: 0
|
||||
writer.Write((byte)0x00);
|
||||
writer.Write((byte)0x00);
|
||||
|
||||
// Additional count: 0
|
||||
writer.Write((byte)0x00);
|
||||
writer.Write((byte)0x00);
|
||||
|
||||
// Question section
|
||||
WriteQName(writer, serviceType);
|
||||
|
||||
// Type: PTR (12)
|
||||
writer.Write((byte)0x00);
|
||||
writer.Write((byte)0x0C);
|
||||
|
||||
// Class: IN (1)
|
||||
writer.Write((byte)0x00);
|
||||
writer.Write((byte)0x01);
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a DNS name in QNAME format
|
||||
/// </summary>
|
||||
private static void WriteQName(System.IO.BinaryWriter writer, string name)
|
||||
{
|
||||
var labels = name.TrimEnd('.').Split('.');
|
||||
|
||||
foreach (var label in labels)
|
||||
{
|
||||
if (string.IsNullOrEmpty(label))
|
||||
continue;
|
||||
|
||||
writer.Write((byte)label.Length);
|
||||
writer.Write(Encoding.ASCII.GetBytes(label));
|
||||
}
|
||||
|
||||
writer.Write((byte)0x00); // Null terminator
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates if response is a proper mDNS response
|
||||
/// </summary>
|
||||
private static bool IsValidMdnsResponse(byte[] response)
|
||||
{
|
||||
if (response == null || response.Length < 12)
|
||||
return false;
|
||||
|
||||
// Check if it's a response (QR bit set)
|
||||
return (response[2] & 0x80) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses mDNS response packet
|
||||
/// </summary>
|
||||
private static List<BonjourService> ParseMdnsResponse(byte[] response, string sourceIp)
|
||||
{
|
||||
var services = new List<BonjourService>();
|
||||
|
||||
try
|
||||
{
|
||||
int offset = 0;
|
||||
|
||||
// Parse header
|
||||
ushort transactionId = ReadUInt16(response, ref offset);
|
||||
ushort flags = ReadUInt16(response, ref offset);
|
||||
ushort questionCount = ReadUInt16(response, ref offset);
|
||||
ushort answerCount = ReadUInt16(response, ref offset);
|
||||
ushort authorityCount = ReadUInt16(response, ref offset);
|
||||
ushort additionalCount = ReadUInt16(response, ref offset);
|
||||
|
||||
// Skip questions
|
||||
for (int i = 0; i < questionCount; i++)
|
||||
{
|
||||
SkipQName(response, ref offset);
|
||||
offset += 4; // Type and Class
|
||||
}
|
||||
|
||||
// Parse answers
|
||||
var ptrRecords = new Dictionary<string, List<string>>();
|
||||
var srvRecords = new Dictionary<string, SrvRecord>();
|
||||
var txtRecords = new Dictionary<string, Dictionary<string, string>>();
|
||||
var aRecords = new Dictionary<string, string>();
|
||||
|
||||
// Parse all record sections
|
||||
int totalRecords = answerCount + authorityCount + additionalCount;
|
||||
|
||||
for (int i = 0; i < totalRecords; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
string name = ReadQName(response, ref offset);
|
||||
ushort type = ReadUInt16(response, ref offset);
|
||||
ushort rclass = ReadUInt16(response, ref offset);
|
||||
uint ttl = ReadUInt32(response, ref offset);
|
||||
ushort dataLength = ReadUInt16(response, ref offset);
|
||||
int dataStart = offset;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case 12: // PTR
|
||||
string ptrTarget = ReadQName(response, ref offset);
|
||||
if (!ptrRecords.ContainsKey(name))
|
||||
ptrRecords[name] = new List<string>();
|
||||
ptrRecords[name].Add(ptrTarget);
|
||||
break;
|
||||
|
||||
case 33: // SRV
|
||||
ushort priority = ReadUInt16(response, ref offset);
|
||||
ushort weight = ReadUInt16(response, ref offset);
|
||||
ushort port = ReadUInt16(response, ref offset);
|
||||
string target = ReadQName(response, ref offset);
|
||||
srvRecords[name] = new SrvRecord
|
||||
{
|
||||
Port = port,
|
||||
Target = target,
|
||||
Priority = priority,
|
||||
Weight = weight,
|
||||
TTL = ttl
|
||||
};
|
||||
break;
|
||||
|
||||
case 16: // TXT
|
||||
var txtData = ParseTxtRecord(response, offset, dataLength);
|
||||
txtRecords[name] = txtData;
|
||||
offset += dataLength;
|
||||
break;
|
||||
|
||||
case 1: // A (IPv4)
|
||||
if (dataLength == 4)
|
||||
{
|
||||
var ip = new IPAddress(new[]
|
||||
{
|
||||
response[offset],
|
||||
response[offset + 1],
|
||||
response[offset + 2],
|
||||
response[offset + 3]
|
||||
});
|
||||
aRecords[name] = ip.ToString();
|
||||
offset += 4;
|
||||
}
|
||||
break;
|
||||
|
||||
case 28: // AAAA (IPv6)
|
||||
if (dataLength == 16)
|
||||
{
|
||||
byte[] ipBytes = new byte[16];
|
||||
Array.Copy(response, offset, ipBytes, 0, 16);
|
||||
var ip = new IPAddress(ipBytes);
|
||||
aRecords[name] = ip.ToString();
|
||||
offset += 16;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
offset += dataLength;
|
||||
break;
|
||||
}
|
||||
|
||||
// Ensure we're at the right position
|
||||
offset = dataStart + dataLength;
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Build service objects from collected records
|
||||
foreach (var ptrEntry in ptrRecords)
|
||||
{
|
||||
foreach (var instanceName in ptrEntry.Value)
|
||||
{
|
||||
var service = new BonjourService
|
||||
{
|
||||
DiscoveredAt = DateTime.Now,
|
||||
RawResponse = response
|
||||
};
|
||||
|
||||
// Parse instance name (e.g., "My Printer._ipp._tcp.local.")
|
||||
ParseServiceName(instanceName, service);
|
||||
|
||||
// Get SRV record
|
||||
if (srvRecords.ContainsKey(instanceName))
|
||||
{
|
||||
var srv = srvRecords[instanceName];
|
||||
service.Port = srv.Port;
|
||||
service.HostName = srv.Target;
|
||||
service.TTL = srv.TTL;
|
||||
|
||||
// Get IP address from A/AAAA record
|
||||
if (aRecords.ContainsKey(srv.Target))
|
||||
{
|
||||
service.IPAddress = aRecords[srv.Target];
|
||||
}
|
||||
}
|
||||
|
||||
// Get TXT record
|
||||
if (txtRecords.ContainsKey(instanceName))
|
||||
{
|
||||
service.TxtRecords = txtRecords[instanceName];
|
||||
}
|
||||
|
||||
services.Add(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[mDNS Parser] Error: {ex.Message}");
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses service name components
|
||||
/// </summary>
|
||||
private static void ParseServiceName(string fullName, BonjourService service)
|
||||
{
|
||||
service.FullServiceName = fullName;
|
||||
|
||||
var parts = fullName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length >= 3)
|
||||
{
|
||||
service.ServiceName = parts[0];
|
||||
service.ServiceType = $"{parts[1]}.{parts[2]}";
|
||||
|
||||
if (parts.Length >= 4)
|
||||
{
|
||||
service.Domain = string.Join(".", parts.Skip(3));
|
||||
}
|
||||
else
|
||||
{
|
||||
service.Domain = "local";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses TXT record key-value pairs
|
||||
/// </summary>
|
||||
private static Dictionary<string, string> ParseTxtRecord(byte[] data, int offset, int length)
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
int end = offset + length;
|
||||
|
||||
while (offset < end)
|
||||
{
|
||||
byte len = data[offset++];
|
||||
if (len == 0 || offset + len > end)
|
||||
break;
|
||||
|
||||
string txt = Encoding.UTF8.GetString(data, offset, len);
|
||||
offset += len;
|
||||
|
||||
var eqIndex = txt.IndexOf('=');
|
||||
if (eqIndex > 0)
|
||||
{
|
||||
string key = txt.Substring(0, eqIndex);
|
||||
string value = txt.Substring(eqIndex + 1);
|
||||
result[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
result[txt] = "";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a DNS name from the packet (handles compression)
|
||||
/// </summary>
|
||||
private static string ReadQName(byte[] data, ref int offset)
|
||||
{
|
||||
var labels = new List<string>();
|
||||
int jumps = 0;
|
||||
int maxJumps = 10;
|
||||
bool jumped = false;
|
||||
int returnOffset = -1;
|
||||
|
||||
while (offset < data.Length)
|
||||
{
|
||||
byte length = data[offset];
|
||||
|
||||
// Check for compression pointer
|
||||
if ((length & 0xC0) == 0xC0)
|
||||
{
|
||||
if (!jumped)
|
||||
returnOffset = offset + 2;
|
||||
|
||||
int pointer = ((length & 0x3F) << 8) | data[offset + 1];
|
||||
offset = pointer;
|
||||
jumped = true;
|
||||
jumps++;
|
||||
|
||||
if (jumps > maxJumps)
|
||||
break;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
offset++;
|
||||
|
||||
if (length == 0)
|
||||
break;
|
||||
|
||||
if (offset + length > data.Length)
|
||||
break;
|
||||
|
||||
string label = Encoding.ASCII.GetString(data, offset, length);
|
||||
labels.Add(label);
|
||||
offset += length;
|
||||
}
|
||||
|
||||
if (jumped && returnOffset != -1)
|
||||
offset = returnOffset;
|
||||
|
||||
return string.Join(".", labels);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skips a DNS name in the packet
|
||||
/// </summary>
|
||||
private static void SkipQName(byte[] data, ref int offset)
|
||||
{
|
||||
while (offset < data.Length)
|
||||
{
|
||||
byte length = data[offset++];
|
||||
|
||||
if (length == 0)
|
||||
break;
|
||||
|
||||
if ((length & 0xC0) == 0xC0)
|
||||
{
|
||||
offset++;
|
||||
break;
|
||||
}
|
||||
|
||||
offset += length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads 16-bit unsigned integer (big-endian)
|
||||
/// </summary>
|
||||
private static ushort ReadUInt16(byte[] data, ref int offset)
|
||||
{
|
||||
ushort value = (ushort)((data[offset] << 8) | data[offset + 1]);
|
||||
offset += 2;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads 32-bit unsigned integer (big-endian)
|
||||
/// </summary>
|
||||
private static uint ReadUInt32(byte[] data, ref int offset)
|
||||
{
|
||||
uint value = (uint)((data[offset] << 24) | (data[offset + 1] << 16) |
|
||||
(data[offset + 2] << 8) | data[offset + 3]);
|
||||
offset += 4;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SRV record data structure
|
||||
/// </summary>
|
||||
private class SrvRecord
|
||||
{
|
||||
public ushort Priority { get; set; }
|
||||
public ushort Weight { get; set; }
|
||||
public ushort Port { get; set; }
|
||||
public string Target { get; set; }
|
||||
public uint TTL { get; set; }
|
||||
}
|
||||
}
|
||||
29
Inspectron.Epson/BonjourService.cs
Normal file
29
Inspectron.Epson/BonjourService.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a discovered service via mDNS/Bonjour
|
||||
/// </summary>
|
||||
public class BonjourService
|
||||
{
|
||||
public string ServiceName { get; set; }
|
||||
public string ServiceType { get; set; }
|
||||
public string Domain { get; set; }
|
||||
public string FullServiceName { get; set; }
|
||||
public string HostName { get; set; }
|
||||
public string IPAddress { get; set; }
|
||||
public int Port { get; set; }
|
||||
public Dictionary<string, string> TxtRecords { get; set; }
|
||||
public uint TTL { get; set; }
|
||||
public DateTime DiscoveredAt { get; set; }
|
||||
public byte[] RawResponse { get; set; }
|
||||
|
||||
public BonjourService()
|
||||
{
|
||||
TxtRecords = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{ServiceName}.{ServiceType}.{Domain} - {IPAddress}:{Port}";
|
||||
}
|
||||
}
|
||||
22
Inspectron.Epson/DiscoveredPrinter.cs
Normal file
22
Inspectron.Epson/DiscoveredPrinter.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a discovered Epson printer
|
||||
/// </summary>
|
||||
public class DiscoveredPrinter
|
||||
{
|
||||
public string ModelName { get; set; }
|
||||
public string IPAddress { get; set; }
|
||||
public string MACAddress { get; set; }
|
||||
public int Port { get; set; }
|
||||
public DateTime DiscoveredAt { get; set; }
|
||||
public byte[] RawResponse { get; set; }
|
||||
public string ServiceUrl { get; set; }
|
||||
public ushort Lifetime { get; set; }
|
||||
public string ServiceType { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Model: {ModelName}, IP: {IPAddress}, MAC: {MACAddress}, Type: {ServiceType}";
|
||||
}
|
||||
}
|
||||
96
Inspectron.Epson/ENPCDiscoveryService.cs
Normal file
96
Inspectron.Epson/ENPCDiscoveryService.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
public class ENPCDiscoveryService
|
||||
{
|
||||
private const int DiscoveryPort = 3289;
|
||||
private const string DiscoveryMessage = "EPSONQ";
|
||||
private const int MessageLength = 14;
|
||||
|
||||
/// <summary>
|
||||
/// Discovers Epson printers on the local network using UDP broadcast
|
||||
/// </summary>
|
||||
/// <param name="timeout">How long to wait for responses (default: 5 seconds)</param>
|
||||
/// <param name="onPrinterDiscovered">Optional callback invoked when a printer is discovered</param>
|
||||
/// <returns>List of discovered printer information</returns>
|
||||
public async Task<List<DiscoveredPrinter>> DiscoverPrintersAsync(
|
||||
TimeSpan? timeout = null,
|
||||
Action<DiscoveredPrinter>? 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<DiscoveredPrinter>();
|
||||
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 printer = new DiscoveredPrinter
|
||||
{
|
||||
IPAddress = result.RemoteEndPoint.Address.ToString(),
|
||||
DiscoveredAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
193
Inspectron.Epson/EpsonCommands.cs
Normal file
193
Inspectron.Epson/EpsonCommands.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// ESC/POS command constants for Epson TM-m30III printer
|
||||
/// </summary>
|
||||
public static class EpsonCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// ESC @ - Initialize printer
|
||||
/// </summary>
|
||||
public static readonly byte[] Initialize = { 0x1B, 0x40 };
|
||||
|
||||
/// <summary>
|
||||
/// DLE EOT 1 - Transmit printer status
|
||||
/// </summary>
|
||||
public static readonly byte[] GetPrinterStatus = { 0x10, 0x04, 0x01 };
|
||||
|
||||
/// <summary>
|
||||
/// DLE EOT 2 - Transmit offline status
|
||||
/// </summary>
|
||||
public static readonly byte[] GetOfflineStatus = { 0x10, 0x04, 0x02 };
|
||||
|
||||
/// <summary>
|
||||
/// DLE EOT 3 - Transmit error status
|
||||
/// </summary>
|
||||
public static readonly byte[] GetErrorStatus = { 0x10, 0x04, 0x03 };
|
||||
|
||||
/// <summary>
|
||||
/// DLE EOT 4 - Transmit paper roll sensor status
|
||||
/// </summary>
|
||||
public static readonly byte[] GetPaperSensorStatus = { 0x10, 0x04, 0x04 };
|
||||
|
||||
/// <summary>
|
||||
/// GS I 1 - Get printer model ID
|
||||
/// </summary>
|
||||
public static readonly byte[] GetPrinterId = { 0x1D, 0x49, 0x01 };
|
||||
|
||||
/// <summary>
|
||||
/// GS V m - Cut paper (full cut)
|
||||
/// </summary>
|
||||
public static readonly byte[] CutPaperFull = { 0x1D, 0x56, 0x00 };
|
||||
|
||||
public static readonly byte[] PrintAndFeed = {0x0A };
|
||||
|
||||
/// <summary>
|
||||
/// GS V m - Cut paper (partial cut)
|
||||
/// </summary>
|
||||
public static readonly byte[] CutPaperPartial = { 0x1D, 0x56, 0x01 };
|
||||
|
||||
public static readonly byte[] UTF8Encoding = { 0x1B, 0x74, 0xFF };
|
||||
public static readonly byte[] CP852Encoding = { 0x1B, 0x74, 0x12 };
|
||||
|
||||
public static readonly byte[] SelectFontHeader = { 0x1B, 0x4D };
|
||||
|
||||
public static readonly byte[] PrintLoadedImage =
|
||||
{
|
||||
0x1D, 0x28, 0x4C, 0x02, 0x00, 0x30, 0x02
|
||||
};
|
||||
|
||||
public enum PrinterFont
|
||||
{
|
||||
A=0,
|
||||
B= 1,
|
||||
C=2,
|
||||
D= 3,
|
||||
E = 4
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ESC d n - Print and feed n lines
|
||||
/// </summary>
|
||||
/// <param name="lines">Number of lines to feed (0-255)</param>
|
||||
/// <returns>Command bytes</returns>
|
||||
public static byte[] FeedLines(byte lines)
|
||||
{
|
||||
return new byte[] { 0x1B, 0x64, lines };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LF - Line feed
|
||||
/// </summary>
|
||||
public static readonly byte[] LineFeed = { 0x0A };
|
||||
|
||||
/// <summary>
|
||||
/// Bit image mode for ESC * command
|
||||
/// </summary>
|
||||
public enum BitImageMode : byte
|
||||
{
|
||||
/// <summary>8-dot single-density (90 DPI horizontal)</summary>
|
||||
SingleDensity8Dot = 0,
|
||||
/// <summary>8-dot double-density (180 DPI horizontal)</summary>
|
||||
DoubleDensity8Dot = 1,
|
||||
/// <summary>24-dot single-density (90 DPI horizontal)</summary>
|
||||
SingleDensity24Dot = 32,
|
||||
/// <summary>24-dot double-density (180 DPI horizontal)</summary>
|
||||
DoubleDensity24Dot = 33
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create ESC * command for bit-image mode printing
|
||||
/// </summary>
|
||||
/// <param name="mode">Bit image mode (8-dot or 24-dot, single or double density)</param>
|
||||
/// <param name="imageData">Column format bit image data</param>
|
||||
/// <param name="widthDots">Width in dots (1-2400 depending on printer model)</param>
|
||||
/// <returns>Complete ESC * command sequence</returns>
|
||||
public static byte[] CreateBitImageCommand(BitImageMode mode, byte[] imageData, int widthDots)
|
||||
{
|
||||
// ESC * m nL nH d1...dk
|
||||
// Format: 0x1B 0x2A m nL nH [data]
|
||||
|
||||
if (widthDots < 1 || widthDots > 2400)
|
||||
throw new ArgumentOutOfRangeException(nameof(widthDots), "Width must be between 1 and 2400 dots");
|
||||
|
||||
byte nL = (byte)(widthDots & 0xFF);
|
||||
byte nH = (byte)((widthDots >> 8) & 0xFF);
|
||||
|
||||
var commands = new List<byte>
|
||||
{
|
||||
0x1B, // ESC
|
||||
0x2A, // *
|
||||
(byte)mode, // m (mode)
|
||||
nL, // nL (width low byte)
|
||||
nH // nH (width high byte)
|
||||
};
|
||||
|
||||
// Add image data
|
||||
commands.AddRange(imageData);
|
||||
|
||||
return commands.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create GS ( L raster image command sequence for printing images
|
||||
/// </summary>
|
||||
/// <param name="imageData">Monochrome raster data (1 bit per pixel, packed into bytes)</param>
|
||||
/// <param name="width">Image width in pixels</param>
|
||||
/// <param name="height">Image height in pixels</param>
|
||||
/// <returns>Complete command sequence to store and print raster image</returns>
|
||||
public static byte[] CreateRasterImageCommand(byte[] imageData, int width, int height)
|
||||
{
|
||||
// Calculate dimensions
|
||||
int widthBytes = width;
|
||||
|
||||
// Build the command sequence
|
||||
var commands = new List<byte>();
|
||||
|
||||
// Command 1: GS ( L - Store raster format graphics data (Function 112/0x70)
|
||||
// Header: GS ( L pL pH m fn a bx by c xL xH yL yH [data]
|
||||
|
||||
int payloadSize = 10 + imageData.Length; // 10 bytes of parameters + image data
|
||||
byte pL = (byte)(payloadSize & 0xFF);
|
||||
byte pH = (byte)((payloadSize >> 8) & 0xFF);
|
||||
|
||||
commands.Add(0x1D); // GS
|
||||
commands.Add(0x28); // (
|
||||
commands.Add(0x4C); // L
|
||||
commands.Add(pL); // pL
|
||||
commands.Add(pH); // pH
|
||||
commands.Add(0x30); // m = 48 (function type)
|
||||
commands.Add(0x70); // fn = 112 (store raster format data)
|
||||
commands.Add(0x30); // a = 48 (normal)
|
||||
commands.Add(0x01); // bx = 1 (horizontal scaling)
|
||||
commands.Add(0x01); // by = 1 (vertical scaling)
|
||||
commands.Add(0x31); // c = 49 (monochrome, single color)
|
||||
|
||||
// Width in bytes (xL + xH * 256)
|
||||
byte xL = (byte)(widthBytes & 0xFF);
|
||||
byte xH = (byte)((widthBytes >> 8) & 0xFF);
|
||||
commands.Add(xL);
|
||||
commands.Add(xH);
|
||||
|
||||
// Height in pixels (yL + yH * 256)
|
||||
byte yL = (byte)(height & 0xFF);
|
||||
byte yH = (byte)((height >> 8) & 0xFF);
|
||||
commands.Add(yL);
|
||||
commands.Add(yH);
|
||||
|
||||
// Add raster data
|
||||
commands.AddRange(imageData);
|
||||
|
||||
// Command 2: GS ( L - Print graphics data (Function 50/0x32)
|
||||
commands.Add(0x1D); // GS
|
||||
commands.Add(0x28); // (
|
||||
commands.Add(0x4C); // L
|
||||
commands.Add(0x02); // pL = 2 (2 bytes follow)
|
||||
commands.Add(0x00); // pH = 0
|
||||
commands.Add(0x30); // m = 48 (function type)
|
||||
commands.Add(0x32); // fn = 50 (print graphics data)
|
||||
|
||||
return commands.ToArray();
|
||||
}
|
||||
}
|
||||
239
Inspectron.Epson/EpsonDiagnostics.cs
Normal file
239
Inspectron.Epson/EpsonDiagnostics.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// Comprehensive diagnostics for Epson TM-m30III printer
|
||||
/// </summary>
|
||||
public class EpsonDiagnostics
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of EpsonDiagnostics
|
||||
/// </summary>
|
||||
/// <param name="logger">Optional logger for diagnostic output</param>
|
||||
public EpsonDiagnostics(ILogger logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run full diagnostic check and return formatted report
|
||||
/// </summary>
|
||||
/// <param name="ip">Printer IP address</param>
|
||||
/// <param name="port">Printer port (default 9100)</param>
|
||||
/// <param name="timeoutSeconds">Connection timeout in seconds (default 5)</param>
|
||||
/// <returns>Formatted diagnostic report as string</returns>
|
||||
public async Task<string> RunFullDiagnosticsAsync(string ip, int port = 9100, int timeoutSeconds = 5)
|
||||
{
|
||||
var report = new StringBuilder();
|
||||
var separator = new string('=', 70);
|
||||
|
||||
report.AppendLine();
|
||||
report.AppendLine(separator);
|
||||
report.AppendLine(" EPSON TM-m30III PRINTER DIAGNOSTICS");
|
||||
report.AppendLine(separator);
|
||||
report.AppendLine($" Printer IP: {ip}:{port}");
|
||||
report.AppendLine($" Timestamp: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||
report.AppendLine(separator);
|
||||
|
||||
_logger?.LogInformation("Starting diagnostics for printer at {Ip}:{Port}", ip, port);
|
||||
|
||||
await using var printer = new EpsonPrinter(_logger);
|
||||
|
||||
// Connect to printer
|
||||
report.AppendLine();
|
||||
report.AppendLine("⏳ Connecting to printer...");
|
||||
|
||||
try
|
||||
{
|
||||
await printer.ConnectAsync(ip, port, timeoutSeconds);
|
||||
report.AppendLine("✅ Connection established");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
report.AppendLine();
|
||||
report.AppendLine("❌ DIAGNOSTIC FAILED: Cannot connect to printer");
|
||||
report.AppendLine($" {ex.Message}");
|
||||
report.AppendLine($" Make sure the printer is powered on and accessible at {ip}");
|
||||
report.AppendLine();
|
||||
report.AppendLine(separator);
|
||||
report.AppendLine(" DIAGNOSTIC COMPLETE");
|
||||
report.AppendLine(separator);
|
||||
|
||||
_logger?.LogError(ex, "Diagnostics failed - cannot connect to printer");
|
||||
return report.ToString();
|
||||
}
|
||||
|
||||
// Gather status information
|
||||
report.AppendLine();
|
||||
report.AppendLine("⏳ Retrieving printer information...");
|
||||
report.AppendLine("⏳ Querying printer status...");
|
||||
|
||||
try
|
||||
{
|
||||
var overallStatus = await printer.GetOverallStatusAsync();
|
||||
|
||||
// Overall Status Section
|
||||
PrintSectionHeader(report, "OVERALL STATUS");
|
||||
report.AppendLine();
|
||||
report.AppendLine($" {overallStatus.StatusIcon} {overallStatus.StatusText}");
|
||||
report.AppendLine();
|
||||
|
||||
if (!string.IsNullOrEmpty(overallStatus.PrinterModel))
|
||||
{
|
||||
PrintStatusLine(report, "Printer Model", overallStatus.PrinterModel, "🖨️");
|
||||
}
|
||||
|
||||
// General Printer Status Section
|
||||
if (overallStatus.PrinterStatus != null)
|
||||
{
|
||||
var ps = overallStatus.PrinterStatus;
|
||||
PrintSectionHeader(report, "GENERAL PRINTER STATUS");
|
||||
PrintStatusLine(report, "Raw Status Byte", ps.Hex, "📊");
|
||||
PrintStatusLine(report, "Binary", ps.Binary, "📊");
|
||||
report.AppendLine();
|
||||
PrintStatusLine(report, "Printer Online", ps.IsOnline);
|
||||
PrintStatusLine(report, "Cover Closed", ps.IsCoverClosed);
|
||||
PrintStatusLine(report, "Paper Feed Button Pressed", ps.PaperFeedButtonPressed);
|
||||
PrintStatusLine(report, "Drawer Open Signal", ps.DrawerOpen);
|
||||
}
|
||||
|
||||
// Offline Status Section
|
||||
if (overallStatus.OfflineStatus != null)
|
||||
{
|
||||
var os = overallStatus.OfflineStatus;
|
||||
PrintSectionHeader(report, "OFFLINE STATUS (Reasons for Offline)");
|
||||
PrintStatusLine(report, "Raw Status Byte", os.Hex, "📊");
|
||||
PrintStatusLine(report, "Binary", os.Binary, "📊");
|
||||
report.AppendLine();
|
||||
PrintStatusLine(report, "Cover Open", os.CoverOpen);
|
||||
PrintStatusLine(report, "Paper Feed Button Active", os.PaperFeedButton);
|
||||
PrintStatusLine(report, "Paper End Detected", os.PaperEnd);
|
||||
PrintStatusLine(report, "Error Occurred", os.ErrorOccurred);
|
||||
}
|
||||
|
||||
// Error Status Section
|
||||
if (overallStatus.ErrorStatus != null)
|
||||
{
|
||||
var es = overallStatus.ErrorStatus;
|
||||
PrintSectionHeader(report, "ERROR STATUS");
|
||||
PrintStatusLine(report, "Raw Status Byte", es.Hex, "📊");
|
||||
PrintStatusLine(report, "Binary", es.Binary, "📊");
|
||||
report.AppendLine();
|
||||
PrintStatusLine(report, "Recoverable Error", es.RecoverableError);
|
||||
PrintStatusLine(report, "Auto-Cutter Error", es.AutoCutterError);
|
||||
PrintStatusLine(report, "Unrecoverable Error", es.UnrecoverableError);
|
||||
PrintStatusLine(report, "Auto-Recovery Error", es.AutoRecoveryError);
|
||||
|
||||
// Error guidance
|
||||
if (es.UnrecoverableError)
|
||||
{
|
||||
report.AppendLine();
|
||||
report.AppendLine(" ⚠️ UNRECOVERABLE ERROR DETECTED:");
|
||||
report.AppendLine(" - Power cycle the printer");
|
||||
report.AppendLine(" - Check for hardware failures");
|
||||
report.AppendLine(" - Contact technical support if issue persists");
|
||||
}
|
||||
else if (es.RecoverableError)
|
||||
{
|
||||
report.AppendLine();
|
||||
report.AppendLine(" ⚠️ RECOVERABLE ERROR DETECTED:");
|
||||
report.AppendLine(" - Check for paper jams");
|
||||
report.AppendLine(" - Ensure paper is loaded correctly");
|
||||
report.AppendLine(" - Close the printer cover");
|
||||
}
|
||||
|
||||
if (es.AutoCutterError)
|
||||
{
|
||||
report.AppendLine();
|
||||
report.AppendLine(" ⚠️ AUTO-CUTTER ERROR DETECTED:");
|
||||
report.AppendLine(" - Check for paper jams in cutter");
|
||||
report.AppendLine(" - Remove any obstructions");
|
||||
report.AppendLine(" - May require service");
|
||||
}
|
||||
}
|
||||
|
||||
// Paper Sensor Status Section
|
||||
if (overallStatus.PaperStatus != null)
|
||||
{
|
||||
var pps = overallStatus.PaperStatus;
|
||||
PrintSectionHeader(report, "PAPER SENSOR STATUS");
|
||||
PrintStatusLine(report, "Raw Status Byte", pps.Hex, "📊");
|
||||
PrintStatusLine(report, "Binary", pps.Binary, "📊");
|
||||
report.AppendLine();
|
||||
PrintStatusLine(report, "Paper Present", pps.PaperPresent);
|
||||
PrintStatusLine(report, "Paper Near End", pps.PaperNearEnd);
|
||||
|
||||
if (!pps.PaperPresent)
|
||||
{
|
||||
report.AppendLine();
|
||||
report.AppendLine(" ⚠️ NO PAPER DETECTED:");
|
||||
report.AppendLine(" - Load paper roll");
|
||||
report.AppendLine(" - Ensure paper is inserted correctly");
|
||||
}
|
||||
else if (pps.PaperNearEnd)
|
||||
{
|
||||
report.AppendLine();
|
||||
report.AppendLine(" ⚠️ PAPER LOW:");
|
||||
report.AppendLine(" - Replace paper roll soon to avoid interruption");
|
||||
}
|
||||
}
|
||||
|
||||
// Recommendations Section
|
||||
PrintSectionHeader(report, "RECOMMENDATIONS");
|
||||
if (overallStatus.Recommendations.Count == 0)
|
||||
{
|
||||
report.AppendLine();
|
||||
report.AppendLine(" ✅ No issues detected - printer is ready for operation");
|
||||
}
|
||||
else
|
||||
{
|
||||
report.AppendLine();
|
||||
for (int i = 0; i < overallStatus.Recommendations.Count; i++)
|
||||
{
|
||||
report.AppendLine($" {i + 1}. {overallStatus.Recommendations[i]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
report.AppendLine();
|
||||
report.AppendLine($"❌ Error during diagnostics: {ex.Message}");
|
||||
_logger?.LogError(ex, "Error during diagnostics");
|
||||
}
|
||||
|
||||
// Footer
|
||||
report.AppendLine();
|
||||
report.AppendLine(separator);
|
||||
report.AppendLine(" DIAGNOSTIC COMPLETE");
|
||||
report.AppendLine(separator);
|
||||
report.AppendLine();
|
||||
|
||||
_logger?.LogInformation("Diagnostics completed");
|
||||
return report.ToString();
|
||||
}
|
||||
|
||||
private static void PrintSectionHeader(StringBuilder report, string title)
|
||||
{
|
||||
var separator = new string('=', 70);
|
||||
report.AppendLine();
|
||||
report.AppendLine(separator);
|
||||
report.AppendLine($" {title}");
|
||||
report.AppendLine(separator);
|
||||
}
|
||||
|
||||
private static void PrintStatusLine(StringBuilder report, string label, bool value)
|
||||
{
|
||||
var icon = value ? "✅" : "❌";
|
||||
var valueStr = value ? "YES" : "NO";
|
||||
report.AppendLine($" {icon} {label,-35} {valueStr}");
|
||||
}
|
||||
|
||||
private static void PrintStatusLine(StringBuilder report, string label, string value, string icon = "ℹ️")
|
||||
{
|
||||
report.AppendLine($" {icon} {label,-35} {value}");
|
||||
}
|
||||
}
|
||||
64
Inspectron.Epson/EpsonExceptions.cs
Normal file
64
Inspectron.Epson/EpsonExceptions.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// Base exception for all Epson printer-related errors
|
||||
/// </summary>
|
||||
public class EpsonPrinterException : Exception
|
||||
{
|
||||
public EpsonPrinterException() { }
|
||||
public EpsonPrinterException(string message) : base(message) { }
|
||||
public EpsonPrinterException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when connection to the printer fails or is lost
|
||||
/// </summary>
|
||||
public class EpsonConnectionException : EpsonPrinterException
|
||||
{
|
||||
public string? PrinterIp { get; }
|
||||
public int? PrinterPort { get; }
|
||||
|
||||
public EpsonConnectionException() { }
|
||||
|
||||
public EpsonConnectionException(string message) : base(message) { }
|
||||
|
||||
public EpsonConnectionException(string message, Exception innerException) : base(message, innerException) { }
|
||||
|
||||
public EpsonConnectionException(string message, string printerIp, int printerPort) : base(message)
|
||||
{
|
||||
PrinterIp = printerIp;
|
||||
PrinterPort = printerPort;
|
||||
}
|
||||
|
||||
public EpsonConnectionException(string message, string printerIp, int printerPort, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
PrinterIp = printerIp;
|
||||
PrinterPort = printerPort;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a command sent to the printer fails
|
||||
/// </summary>
|
||||
public class EpsonCommandException : EpsonPrinterException
|
||||
{
|
||||
public byte[]? Command { get; }
|
||||
|
||||
public EpsonCommandException() { }
|
||||
|
||||
public EpsonCommandException(string message) : base(message) { }
|
||||
|
||||
public EpsonCommandException(string message, Exception innerException) : base(message, innerException) { }
|
||||
|
||||
public EpsonCommandException(string message, byte[] command) : base(message)
|
||||
{
|
||||
Command = command;
|
||||
}
|
||||
|
||||
public EpsonCommandException(string message, byte[] command, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
Command = command;
|
||||
}
|
||||
}
|
||||
303
Inspectron.Epson/EpsonImageConverter.cs
Normal file
303
Inspectron.Epson/EpsonImageConverter.cs
Normal file
@@ -0,0 +1,303 @@
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for converting images to ESC/POS raster format
|
||||
/// </summary>
|
||||
public static class EpsonImageConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert an image to ESC/POS raster format
|
||||
/// </summary>
|
||||
/// <param name="image">Source image</param>
|
||||
/// <param name="maxWidth">Maximum width in pixels (will maintain aspect ratio)</param>
|
||||
/// <param name="finalWidth">Output: final width after processing (byte-aligned)</param>
|
||||
/// <param name="finalHeight">Output: final height after processing</param>
|
||||
/// <returns>Byte array containing monochrome raster data</returns>
|
||||
public static byte[] ConvertToRasterData(
|
||||
Image<Rgba32> image,
|
||||
int maxWidth,
|
||||
out int finalWidth,
|
||||
out int finalHeight)
|
||||
{
|
||||
// Clone the image to avoid modifying the original
|
||||
using var processedImage = image.Clone();
|
||||
|
||||
// Step 1: Resize if needed
|
||||
ResizeImage(processedImage, maxWidth);
|
||||
|
||||
// Step 2: Convert to grayscale
|
||||
using var grayscaleImage = ConvertToGrayscale(processedImage);
|
||||
|
||||
// Step 3: Apply dithering for better quality 1-bit conversion
|
||||
ApplyFloydSteinbergDithering(grayscaleImage);
|
||||
|
||||
// Step 4: Calculate padded width (must be divisible by 8)
|
||||
int originalWidth = grayscaleImage.Width;
|
||||
int paddedWidth = ((originalWidth + 7) / 8) * 8;
|
||||
|
||||
finalWidth = paddedWidth;
|
||||
finalHeight = grayscaleImage.Height;
|
||||
|
||||
// Step 5: Pack pixels into bytes
|
||||
return PackPixelsToBytes(grayscaleImage, paddedWidth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resize image to fit within max width while maintaining aspect ratio
|
||||
/// </summary>
|
||||
public static void ResizeImage(Image<Rgba32> image, int maxWidth)
|
||||
{
|
||||
|
||||
int newWidth = maxWidth;
|
||||
int newHeight = (int)((float)image.Height / image.Width * maxWidth);
|
||||
|
||||
image.Mutate(x => x.Resize(new ResizeOptions
|
||||
{
|
||||
Size = new Size(newWidth, newHeight),
|
||||
Mode = ResizeMode.Max,
|
||||
Sampler = KnownResamplers.Lanczos3
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert RGBA image to grayscale
|
||||
/// </summary>
|
||||
public static Image<L8> ConvertToGrayscale(Image<Rgba32> image)
|
||||
{
|
||||
var grayscaleImage = new Image<L8>(image.Width, image.Height);
|
||||
|
||||
image.ProcessPixelRows(grayscaleImage, (sourceAccessor, targetAccessor) =>
|
||||
{
|
||||
for (int y = 0; y < sourceAccessor.Height; y++)
|
||||
{
|
||||
var sourceRow = sourceAccessor.GetRowSpan(y);
|
||||
var targetRow = targetAccessor.GetRowSpan(y);
|
||||
|
||||
for (int x = 0; x < sourceAccessor.Width; x++)
|
||||
{
|
||||
var pixel = sourceRow[x];
|
||||
|
||||
// Standard luminance calculation (ITU-R BT.709)
|
||||
byte luminance = (byte)(
|
||||
0.2126 * pixel.R +
|
||||
0.7152 * pixel.G +
|
||||
0.0722 * pixel.B
|
||||
);
|
||||
|
||||
targetRow[x] = new L8(luminance);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return grayscaleImage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply Floyd-Steinberg dithering algorithm for smooth 1-bit conversion
|
||||
/// </summary>
|
||||
public static void ApplyFloydSteinbergDithering(Image<L8> image)
|
||||
{
|
||||
int width = image.Width;
|
||||
int height = image.Height;
|
||||
|
||||
// Create a copy of pixel data to work with
|
||||
var pixels = new float[height, width];
|
||||
|
||||
image.ProcessPixelRows(accessor =>
|
||||
{
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
var row = accessor.GetRowSpan(y);
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
pixels[y, x] = row[x].PackedValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Apply Floyd-Steinberg dithering
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
float oldPixel = pixels[y, x];
|
||||
float newPixel = oldPixel < 128 ? 0 : 255;
|
||||
pixels[y, x] = newPixel;
|
||||
|
||||
float error = oldPixel - newPixel;
|
||||
|
||||
// Distribute error to neighboring pixels
|
||||
if (x + 1 < width)
|
||||
pixels[y, x + 1] += error * 7 / 16;
|
||||
|
||||
if (y + 1 < height)
|
||||
{
|
||||
if (x > 0)
|
||||
pixels[y + 1, x - 1] += error * 3 / 16;
|
||||
|
||||
pixels[y + 1, x] += error * 5 / 16;
|
||||
|
||||
if (x + 1 < width)
|
||||
pixels[y + 1, x + 1] += error * 1 / 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write dithered pixels back to image
|
||||
image.ProcessPixelRows(accessor =>
|
||||
{
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
var row = accessor.GetRowSpan(y);
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
byte value = (byte)Math.Clamp(pixels[y, x], 0, 255);
|
||||
row[x] = new L8(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert image to column format bit image data for ESC * command
|
||||
/// </summary>
|
||||
/// <param name="image">Source image</param>
|
||||
/// <param name="maxWidth">Maximum width in pixels</param>
|
||||
/// <param name="mode">Bit image mode (8-dot or 24-dot)</param>
|
||||
/// <param name="finalWidth">Output: final width in dots</param>
|
||||
/// <param name="finalHeight">Output: final height in dots</param>
|
||||
/// <returns>Column format bit image data</returns>
|
||||
public static byte[] ConvertToColumnFormat(
|
||||
Image<Rgba32> image,
|
||||
int maxWidth,
|
||||
EpsonCommands.BitImageMode mode,
|
||||
out int finalWidth,
|
||||
out int finalHeight)
|
||||
{
|
||||
// Clone the image to avoid modifying the original
|
||||
using var processedImage = image.Clone();
|
||||
|
||||
// Resize if needed
|
||||
ResizeImage(processedImage, maxWidth);
|
||||
|
||||
// Convert to grayscale
|
||||
using var grayscaleImage = ConvertToGrayscale(processedImage);
|
||||
|
||||
// Apply dithering
|
||||
ApplyFloydSteinbergDithering(grayscaleImage);
|
||||
|
||||
int bitsPerColumn = mode == EpsonCommands.BitImageMode.SingleDensity8Dot ||
|
||||
mode == EpsonCommands.BitImageMode.DoubleDensity8Dot ? 8 : 24;
|
||||
|
||||
// Calculate padded height (must be divisible by column height)
|
||||
int originalHeight = grayscaleImage.Height;
|
||||
int paddedHeight = ((originalHeight + bitsPerColumn - 1) / bitsPerColumn) * bitsPerColumn;
|
||||
|
||||
finalWidth = grayscaleImage.Width;
|
||||
finalHeight = paddedHeight;
|
||||
|
||||
// Pack pixels into column format
|
||||
return PackPixelsToColumnFormat(grayscaleImage, bitsPerColumn, paddedHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pack pixels into column format (vertical bytes, left to right)
|
||||
/// </summary>
|
||||
private static byte[] PackPixelsToColumnFormat(Image<L8> image, int bitsPerColumn, int paddedHeight)
|
||||
{
|
||||
int width = image.Width;
|
||||
int height = image.Height;
|
||||
int bytesPerColumn = bitsPerColumn / 8;
|
||||
int totalColumns = width;
|
||||
int totalSlices = paddedHeight / bitsPerColumn;
|
||||
|
||||
var columnData = new byte[totalColumns * totalSlices * bytesPerColumn];
|
||||
int dataIndex = 0;
|
||||
|
||||
// Process image slice by slice (for 24-dot mode, we process 24 rows at a time)
|
||||
for (int slice = 0; slice < totalSlices; slice++)
|
||||
{
|
||||
int sliceStartY = slice * bitsPerColumn;
|
||||
|
||||
// Process each column (x position) in this slice
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
// For each byte in the column (1 byte for 8-dot, 3 bytes for 24-dot)
|
||||
for (int byteInColumn = 0; byteInColumn < bytesPerColumn; byteInColumn++)
|
||||
{
|
||||
byte columnByte = 0;
|
||||
|
||||
// Pack 8 vertical pixels into one byte
|
||||
for (int bit = 0; bit < 8; bit++)
|
||||
{
|
||||
int y = sliceStartY + (byteInColumn * 8) + bit;
|
||||
|
||||
// Get pixel value (white if outside image bounds)
|
||||
byte pixelValue = 255; // Default to white
|
||||
if (y < height)
|
||||
{
|
||||
image.ProcessPixelRows(accessor =>
|
||||
{
|
||||
var row = accessor.GetRowSpan(y);
|
||||
pixelValue = row[x].PackedValue;
|
||||
});
|
||||
}
|
||||
|
||||
// Threshold: < 128 = black (1), >= 128 = white (0)
|
||||
bool isBlack = pixelValue < 128;
|
||||
|
||||
if (isBlack)
|
||||
{
|
||||
columnByte |= (byte)(1 << (7 - bit)); // MSB = top pixel
|
||||
}
|
||||
}
|
||||
|
||||
columnData[dataIndex++] = columnByte;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return columnData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pack grayscale pixels into monochrome bytes (8 pixels per byte)
|
||||
/// </summary>
|
||||
private static byte[] PackPixelsToBytes(Image<L8> image, int paddedWidth)
|
||||
{
|
||||
int bytesPerRow = paddedWidth / 8;
|
||||
int height = image.Height;
|
||||
var rasterData = new byte[bytesPerRow * height];
|
||||
|
||||
image.ProcessPixelRows(accessor =>
|
||||
{
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
var row = accessor.GetRowSpan(y);
|
||||
int rowOffset = y * bytesPerRow;
|
||||
|
||||
for (int x = 0; x < paddedWidth; x++)
|
||||
{
|
||||
// Get pixel value (0 or 255 after dithering)
|
||||
byte pixelValue = x < image.Width ? row[x].PackedValue : (byte)255;
|
||||
|
||||
// Threshold: < 128 = black (1), >= 128 = white (0)
|
||||
bool isBlack = pixelValue < 128;
|
||||
|
||||
if (isBlack)
|
||||
{
|
||||
int byteIndex = rowOffset + (x / 8);
|
||||
int bitPosition = 7 - (x % 8); // MSB first
|
||||
rasterData[byteIndex] |= (byte)(1 << bitPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return rasterData;
|
||||
}
|
||||
}
|
||||
750
Inspectron.Epson/EpsonPrinter.cs
Normal file
750
Inspectron.Epson/EpsonPrinter.cs
Normal file
@@ -0,0 +1,750 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// Main SDK class for Epson TM-m30III printer operations
|
||||
/// </summary>
|
||||
public class EpsonPrinter : IAsyncDisposable
|
||||
{
|
||||
private readonly ILogger? _logger;
|
||||
private TcpClient? _tcpClient;
|
||||
private NetworkStream? _stream;
|
||||
private string? _printerIp;
|
||||
private int _printerPort;
|
||||
private int _timeoutSeconds;
|
||||
private static bool _initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if connected to the printer
|
||||
/// </summary>
|
||||
public bool IsConnected => _tcpClient?.Connected ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of EpsonPrinter
|
||||
/// </summary>
|
||||
/// <param name="logger">Optional logger for diagnostic output</param>
|
||||
public EpsonPrinter(ILogger? logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
RegisterCodepages();
|
||||
}
|
||||
|
||||
private static void RegisterCodepages()
|
||||
{
|
||||
if (_initialized) return;
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect to the printer
|
||||
/// </summary>
|
||||
/// <param name="ip">Printer IP address</param>
|
||||
/// <param name="port">Printer port (default 9100)</param>
|
||||
/// <param name="timeoutSeconds">Connection timeout in seconds (default 5)</param>
|
||||
/// <exception cref="EpsonConnectionException">Thrown when connection fails</exception>
|
||||
public async Task ConnectAsync(string ip, int port = 9100, int timeoutSeconds = 10)
|
||||
{
|
||||
_printerIp = ip;
|
||||
_printerPort = port;
|
||||
_timeoutSeconds = timeoutSeconds;
|
||||
|
||||
try
|
||||
{
|
||||
_logger?.LogInformation("Connecting to printer at {Ip}:{Port}", ip, port);
|
||||
|
||||
_tcpClient = new TcpClient();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds));
|
||||
|
||||
await _tcpClient.ConnectAsync(ip, port, cts.Token);
|
||||
_stream = _tcpClient.GetStream();
|
||||
|
||||
_logger?.LogInformation("Successfully connected to printer");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to connect to printer at {Ip}:{Port}", ip, port);
|
||||
Disconnect();
|
||||
throw new EpsonConnectionException(
|
||||
$"Failed to connect to printer at {ip}:{port}", ip, port, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect from the printer
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
_logger?.LogDebug("Disconnecting from printer");
|
||||
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
|
||||
_tcpClient?.Dispose();
|
||||
_tcpClient = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a command and receive a 1-byte response (for DLE EOT commands)
|
||||
/// </summary>
|
||||
private async Task<byte> SendCommandAsync(byte[] command)
|
||||
{
|
||||
EnsureConnected();
|
||||
|
||||
try
|
||||
{
|
||||
_logger?.LogTrace("Sending command: {Command}", BitConverter.ToString(command));
|
||||
|
||||
await _stream!.WriteAsync(command);
|
||||
await _stream.FlushAsync();
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(_timeoutSeconds));
|
||||
var buffer = new byte[1];
|
||||
var bytesRead = await _stream.ReadAsync(buffer, cts.Token);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
throw new EpsonCommandException("No response received from printer", command);
|
||||
}
|
||||
|
||||
_logger?.LogTrace("Received response: 0x{Response:X2}", buffer[0]);
|
||||
return buffer[0];
|
||||
}
|
||||
catch (Exception ex) when (ex is not EpsonCommandException)
|
||||
{
|
||||
_logger?.LogError(ex, "Error sending command to printer");
|
||||
throw new EpsonCommandException("Failed to send command to printer", command, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a command without expecting a response (for printing commands)
|
||||
/// </summary>
|
||||
private async Task SendRawAsync(byte[] data)
|
||||
{
|
||||
EnsureConnected();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
|
||||
_logger?.LogTrace("Sending raw data: {Length} bytes", data.Length);
|
||||
|
||||
await _stream!.WriteAsync(data);
|
||||
await _stream.FlushAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error sending raw data to printer");
|
||||
throw new EpsonCommandException("Failed to send data to printer", data, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get general printer status
|
||||
/// </summary>
|
||||
/// <returns>Printer status information</returns>
|
||||
public async Task<PrinterStatus> GetPrinterStatusAsync()
|
||||
{
|
||||
_logger?.LogDebug("Querying printer status");
|
||||
var response = await SendCommandAsync(EpsonCommands.GetPrinterStatus);
|
||||
return PrinterStatus.Parse(response);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get offline status (reasons for offline)
|
||||
/// </summary>
|
||||
/// <returns>Offline status information</returns>
|
||||
public async Task<OfflineStatus> GetOfflineStatusAsync()
|
||||
{
|
||||
_logger?.LogDebug("Querying offline status");
|
||||
var response = await SendCommandAsync(EpsonCommands.GetOfflineStatus);
|
||||
return OfflineStatus.Parse(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get error status
|
||||
/// </summary>
|
||||
/// <returns>Error status information</returns>
|
||||
public async Task<ErrorStatus> GetErrorStatusAsync()
|
||||
{
|
||||
_logger?.LogDebug("Querying error status");
|
||||
var response = await SendCommandAsync(EpsonCommands.GetErrorStatus);
|
||||
return ErrorStatus.Parse(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get paper sensor status
|
||||
/// </summary>
|
||||
/// <returns>Paper sensor status information</returns>
|
||||
public async Task<PaperSensorStatus> GetPaperSensorStatusAsync()
|
||||
{
|
||||
_logger?.LogDebug("Querying paper sensor status");
|
||||
var response = await SendCommandAsync(EpsonCommands.GetPaperSensorStatus);
|
||||
return PaperSensorStatus.Parse(response);
|
||||
}
|
||||
|
||||
public async Task<byte> GetTM220StatusAsync()
|
||||
{
|
||||
_logger?.LogDebug("Querying TM220 status");
|
||||
var response = await SendCommandAsync([0x10,0x04,0x01]);
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get printer model/ID
|
||||
/// </summary>
|
||||
public async Task<byte?> GetPrinterIdAsync()
|
||||
{
|
||||
EnsureConnected();
|
||||
|
||||
try
|
||||
{
|
||||
_logger?.LogDebug("Querying printer ID");
|
||||
|
||||
await _stream!.WriteAsync(EpsonCommands.GetPrinterId);
|
||||
await _stream.FlushAsync();
|
||||
|
||||
await Task.Delay(100); // Give printer time to respond
|
||||
|
||||
var buffer = new byte[1];
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(_timeoutSeconds));
|
||||
var bytesRead = await _stream.ReadAsync(buffer, cts.Token);
|
||||
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
return buffer[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to get printer ID");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get comprehensive overall status with health assessment
|
||||
/// </summary>
|
||||
/// <returns>Overall status with recommendations</returns>
|
||||
public async Task<OverallStatus> GetOverallStatusAsync()
|
||||
{
|
||||
_logger?.LogInformation("Getting overall printer status");
|
||||
|
||||
var printerStatus = await GetPrinterStatusAsync();
|
||||
var offlineStatus = await GetOfflineStatusAsync();
|
||||
var errorStatus = await GetErrorStatusAsync();
|
||||
var paperStatus = await GetPaperSensorStatusAsync();
|
||||
var printerModel = await GetPrinterIdAsync();
|
||||
|
||||
return DetermineOverallStatus(printerModel, printerStatus, offlineStatus, errorStatus, paperStatus);
|
||||
}
|
||||
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
_logger?.LogInformation("Initializing printer");
|
||||
await SendRawAsync(EpsonCommands.Initialize);
|
||||
}
|
||||
|
||||
public async Task SetQuadrupleMode(bool enable)
|
||||
{
|
||||
byte[] command = enable
|
||||
? new byte[] { 0x1C, 0x57, 0x01 } // Enable quadruple size
|
||||
: new byte[] { 0x1C, 0x57, 0x00 }; // Disable quadruple size
|
||||
_logger?.LogInformation("{Action} quadruple mode", enable ? "Enabling" : "Disabling");
|
||||
await SendRawAsync(command);
|
||||
}
|
||||
|
||||
public async Task SetRedColor(bool enable)
|
||||
{
|
||||
byte[] command = enable
|
||||
? new byte[] { 0x1B, 0x72, 0x01 } // Enable red color
|
||||
: new byte[] { 0x1B, 0x72, 0x00 }; // Disable red color
|
||||
_logger?.LogInformation("{Action} red color mode", enable ? "Enabling" : "Disabling");
|
||||
await SendRawAsync(command);
|
||||
}
|
||||
|
||||
public async Task SetEmphasized(bool enable)
|
||||
{
|
||||
byte[] command = enable
|
||||
? new byte[] { 0x1B, 0x45, 0x01 } // Enable emphasized mode
|
||||
: new byte[] { 0x1B, 0x45, 0x00 }; // Disable emphasized mode
|
||||
_logger?.LogInformation("{Action} emphasized mode", enable ? "Enabling" : "Disabling");
|
||||
await SendRawAsync(command);
|
||||
}
|
||||
|
||||
public async Task SelectFont(EpsonCommands.PrinterFont font )
|
||||
{
|
||||
var header = EpsonCommands.SelectFontHeader;
|
||||
byte[] selectedFont;
|
||||
selectedFont = font switch
|
||||
{
|
||||
EpsonCommands.PrinterFont.A => [0],
|
||||
EpsonCommands.PrinterFont.B => [0x01],
|
||||
EpsonCommands.PrinterFont.C => [0x02],
|
||||
EpsonCommands.PrinterFont.D => [0x03],
|
||||
EpsonCommands.PrinterFont.E => [0x04],
|
||||
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(font), "Invalid font selection")
|
||||
};
|
||||
var command = new byte[header.Length + selectedFont.Length];
|
||||
Array.Copy(header, command, header.Length);
|
||||
Array.Copy(selectedFont, 0, command, header.Length, selectedFont.Length);
|
||||
_logger?.LogInformation("Selecting font: {Font}", font);
|
||||
await SendRawAsync(command);
|
||||
}
|
||||
|
||||
public async Task SetBiggerFontTM220(bool enabled)
|
||||
{
|
||||
_logger?.LogInformation("{Action} bigger font mode for TM-T20/220",
|
||||
enabled ? "Enabling" : "Disabling");
|
||||
if (enabled)
|
||||
await SendRawCommandAsync([0x1b, 0x21, 0x20 + 0x01]);
|
||||
else
|
||||
await SendRawCommandAsync([0x1b, 0x21, 0x00]);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Print text to the printer
|
||||
/// </summary>
|
||||
/// <param name="text">Text to print</param>
|
||||
public async Task PrintTextAsync(string text)
|
||||
{
|
||||
_logger?.LogInformation("Printing text: {Length} characters", text.Length);
|
||||
|
||||
await SendRawAsync(EpsonCommands.CP852Encoding);
|
||||
Encoding cp852 = Encoding.GetEncoding(852);
|
||||
var textBytes = cp852.GetBytes(text);
|
||||
|
||||
await SendRawAsync(textBytes);
|
||||
}
|
||||
|
||||
public async Task SetFontSizeAsync(int width, int height)
|
||||
{
|
||||
// width and height: 1-8
|
||||
if (width < 1 || width > 8 || height < 1 || height > 8)
|
||||
throw new ArgumentException("Size must be between 1 and 8");
|
||||
|
||||
byte size = (byte)(((height - 1) & 0x07) | (((width - 1) & 0x07) << 4));
|
||||
byte[] cmd = new byte[] { 0x1D, 0x21, size };
|
||||
await SendRawAsync(cmd);
|
||||
}
|
||||
/// <summary>
|
||||
/// Print text, feed paper, and cut
|
||||
/// </summary>
|
||||
/// <param name="text">Text to print</param>
|
||||
/// <param name="feedLines">Number of lines to feed before cutting (default 5)</param>
|
||||
public async Task PrintTextAndCutAsync(string text, int feedLines = 5)
|
||||
{
|
||||
_logger?.LogInformation("Printing text with cut: {Length} characters, {FeedLines} feed lines",
|
||||
text.Length, feedLines);
|
||||
|
||||
// Initialize printer
|
||||
await SendRawAsync(EpsonCommands.Initialize);
|
||||
|
||||
// Print text
|
||||
await PrintTextAsync(text);
|
||||
await PrintTextAsync("\n");
|
||||
|
||||
// Feed lines
|
||||
await SendRawAsync(EpsonCommands.FeedLines((byte)feedLines));
|
||||
|
||||
// Cut paper
|
||||
await SendRawAsync(EpsonCommands.CutPaperFull);
|
||||
|
||||
_logger?.LogInformation("Print job completed");
|
||||
}
|
||||
|
||||
public async Task PrintBuffer()
|
||||
{
|
||||
var header = EpsonCommands.PrintAndFeed;
|
||||
var command = new byte[header.Length];
|
||||
await SendRawCommandAsync(command);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public async Task FeedLinesAsync(int lines)
|
||||
{
|
||||
_logger?.LogInformation("Feeding {Lines} lines", lines);
|
||||
await SendRawAsync(EpsonCommands.FeedLines((byte)lines));
|
||||
}
|
||||
|
||||
public async Task CutAsync()
|
||||
{
|
||||
_logger?.LogInformation("Cutting paper");
|
||||
await SendRawAsync(EpsonCommands.CutPaperFull);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print an image from a file path
|
||||
/// </summary>
|
||||
/// <param name="imagePath">Path to the image file</param>
|
||||
/// <param name="maxWidth">Maximum width in pixels (default 384 for 80mm thermal printer)</param>
|
||||
public async Task LoadImageAsync(string imagePath, int maxWidth = 384)
|
||||
{
|
||||
_logger?.LogInformation("Printing image from file: {Path}", imagePath);
|
||||
|
||||
if (!File.Exists(imagePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Image file not found: {imagePath}", imagePath);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var image = await Image.LoadAsync<Rgba32>(imagePath);
|
||||
await LoadImageAsync(image, maxWidth);
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileNotFoundException)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to load or print image from file: {Path}", imagePath);
|
||||
throw new EpsonCommandException($"Failed to process image file: {imagePath}", Array.Empty<byte>(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PrintLoadedImage()
|
||||
{
|
||||
var command = EpsonCommands.PrintLoadedImage;
|
||||
await SendRawAsync(command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print an image from a stream
|
||||
/// </summary>
|
||||
/// <param name="imageStream">Stream containing the image data</param>
|
||||
/// <param name="maxWidth">Maximum width in pixels (default 384 for 80mm thermal printer)</param>
|
||||
public async Task LoadImageAsync(Stream imageStream, int maxWidth = 384)
|
||||
{
|
||||
_logger?.LogInformation("Printing image from stream");
|
||||
|
||||
try
|
||||
{
|
||||
using var image = await Image.LoadAsync<Rgba32>(imageStream);
|
||||
await LoadImageAsync(image, maxWidth);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to load or print image from stream");
|
||||
throw new EpsonCommandException("Failed to process image stream", Array.Empty<byte>(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetAbsolutePrintPosition(int x)
|
||||
{
|
||||
if (x < 0 || x > 65535)
|
||||
throw new ArgumentOutOfRangeException(nameof(x), "Position must be between 0 and 65535");
|
||||
byte nL = (byte)(x & 0xFF);
|
||||
byte nH = (byte)((x >> 8) & 0xFF);
|
||||
var command = new byte[] { 0x1B, 0x24, nL, nH };
|
||||
_logger?.LogInformation("Setting absolute print position to {X} (nL={nL}, nH={nH})", x, nL, nH);
|
||||
await SendRawAsync(command);
|
||||
|
||||
}
|
||||
|
||||
public async Task SetDefaultLineSpacing()
|
||||
{
|
||||
_logger?.LogInformation("Setting default line spacing");
|
||||
await SendRawAsync([0x1B,0x32]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task SetCustomLineSpacing(int spacing = 24)
|
||||
{
|
||||
if (spacing < 0 || spacing > 255)
|
||||
throw new ArgumentOutOfRangeException(nameof(spacing), "Spacing must be between 0 and 255");
|
||||
var command = new byte[] { 0x1B, 0x33, (byte)spacing };
|
||||
_logger?.LogInformation("Setting custom line spacing to {Spacing}", spacing);
|
||||
await SendRawAsync(command);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print an image from an ImageSharp Image object
|
||||
/// </summary>
|
||||
/// <param name="image">ImageSharp Image object</param>
|
||||
/// <param name="maxWidth">Maximum width in pixels (default 384 for 80mm thermal printer)</param>
|
||||
public async Task LoadImageAsync(Image<Rgba32> image, int maxWidth = 384)
|
||||
{
|
||||
EnsureConnected();
|
||||
|
||||
_logger?.LogInformation("Processing image: {Width}x{Height} pixels, max width: {MaxWidth}",
|
||||
image.Width, image.Height, maxWidth);
|
||||
|
||||
try
|
||||
{
|
||||
// Convert image to raster format
|
||||
var rasterData = EpsonImageConverter.ConvertToRasterData(
|
||||
image,
|
||||
maxWidth,
|
||||
out int finalWidth,
|
||||
out int finalHeight);
|
||||
|
||||
_logger?.LogDebug("Image converted to raster: {Width}x{Height} pixels, {DataSize} bytes",
|
||||
finalWidth, finalHeight, rasterData.Length);
|
||||
|
||||
// Create ESC/POS command
|
||||
var command = EpsonCommands.CreateRasterImageCommand(rasterData, finalWidth, finalHeight);
|
||||
|
||||
_logger?.LogDebug("Sending image command: {CommandSize} bytes", command.Length);
|
||||
|
||||
// Send to printer
|
||||
await SendRawAsync(command);
|
||||
|
||||
_logger?.LogInformation("Image printed successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to process or print image");
|
||||
throw new EpsonCommandException("Failed to print image", Array.Empty<byte>(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print an image using bit-image mode (ESC * command) from a file path
|
||||
/// </summary>
|
||||
/// <param name="imagePath">Path to the image file</param>
|
||||
/// <param name="mode">Bit image mode (8-dot or 24-dot, single or double density)</param>
|
||||
/// <param name="maxWidth">Maximum width in pixels (default 384 for 80mm thermal printer)</param>
|
||||
public async Task PrintImageBitModeAsync(string imagePath, EpsonCommands.BitImageMode mode = EpsonCommands.BitImageMode.SingleDensity8Dot, int maxWidth = 10)
|
||||
{
|
||||
_logger?.LogInformation("Printing image using bit-image mode from file: {Path}", imagePath);
|
||||
|
||||
if (!File.Exists(imagePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Image file not found: {imagePath}", imagePath);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var image = await Image.LoadAsync<Rgba32>(imagePath);
|
||||
await PrintImageBitModeAsync(image, mode, maxWidth);
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileNotFoundException)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to load or print image from file: {Path}", imagePath);
|
||||
throw new EpsonCommandException($"Failed to process image file: {imagePath}", Array.Empty<byte>(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print an image using bit-image mode (ESC * command) from a stream
|
||||
/// </summary>
|
||||
/// <param name="imageStream">Stream containing the image data</param>
|
||||
/// <param name="mode">Bit image mode (8-dot or 24-dot, single or double density)</param>
|
||||
/// <param name="maxWidth">Maximum width in pixels (default 384 for 80mm thermal printer)</param>
|
||||
public async Task PrintImageBitModeAsync(Stream imageStream, EpsonCommands.BitImageMode mode = EpsonCommands.BitImageMode.DoubleDensity24Dot, int maxWidth = 384)
|
||||
{
|
||||
_logger?.LogInformation("Printing image using bit-image mode from stream");
|
||||
|
||||
try
|
||||
{
|
||||
using var image = await Image.LoadAsync<Rgba32>(imageStream);
|
||||
await PrintImageBitModeAsync(image, mode, maxWidth);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to load or print image from stream");
|
||||
throw new EpsonCommandException("Failed to process image stream", Array.Empty<byte>(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print an image using bit-image mode (ESC * command) from an ImageSharp Image object.
|
||||
/// This uses the legacy ESC * command which may be compatible with more printer models.
|
||||
/// For 24-dot modes, the image is printed in multiple horizontal slices of 24 dots height each.
|
||||
/// </summary>
|
||||
/// <param name="image">ImageSharp Image object</param>
|
||||
/// <param name="mode">Bit image mode (8-dot or 24-dot, single or double density)</param>
|
||||
/// <param name="maxWidth">Maximum width in pixels (default 384 for 80mm thermal printer)</param>
|
||||
public async Task PrintImageBitModeAsync(Image<Rgba32> image, EpsonCommands.BitImageMode mode, int maxWidth)
|
||||
{
|
||||
//EnsureConnected();
|
||||
|
||||
_logger?.LogInformation("Processing image for bit-image mode: {Width}x{Height} pixels, mode: {Mode}, max width: {MaxWidth}",
|
||||
image.Width, image.Height, mode, maxWidth);
|
||||
|
||||
try
|
||||
{
|
||||
// Convert image to column format
|
||||
var columnData = EpsonImageConverter.ConvertToColumnFormat(
|
||||
image,
|
||||
maxWidth,
|
||||
mode,
|
||||
out int finalWidth,
|
||||
out int finalHeight);
|
||||
|
||||
_logger?.LogDebug("Image converted to column format: {Width}x{Height} pixels, {DataSize} bytes",
|
||||
finalWidth, finalHeight, columnData.Length);
|
||||
|
||||
int bitsPerSlice = mode == EpsonCommands.BitImageMode.SingleDensity8Dot ||
|
||||
mode == EpsonCommands.BitImageMode.DoubleDensity8Dot ? 8 : 24;
|
||||
int bytesPerColumn = bitsPerSlice / 8;
|
||||
int totalSlices = finalHeight / bitsPerSlice;
|
||||
|
||||
_logger?.LogDebug("Printing in {SliceCount} slices of {BitsPerSlice} dots each", totalSlices, bitsPerSlice);
|
||||
|
||||
// Print each horizontal slice
|
||||
for (int slice = 0; slice < totalSlices; slice++)
|
||||
{
|
||||
int sliceOffset = slice * finalWidth * bytesPerColumn;
|
||||
int sliceLength = finalWidth * bytesPerColumn;
|
||||
|
||||
var sliceData = new byte[sliceLength];
|
||||
Array.Copy(columnData, sliceOffset, sliceData, 0, sliceLength);
|
||||
|
||||
// Create and send ESC * command for this slice
|
||||
var command = EpsonCommands.CreateBitImageCommand(mode, sliceData, finalWidth);
|
||||
|
||||
await SendRawAsync(command);
|
||||
|
||||
// Move to next line after each slice (except the last one)
|
||||
if (slice < totalSlices - 1)
|
||||
{
|
||||
await SendRawAsync(EpsonCommands.LineFeed);
|
||||
}
|
||||
}
|
||||
|
||||
_logger?.LogInformation("Image printed successfully using bit-image mode");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to process or print image in bit-image mode");
|
||||
throw new EpsonCommandException("Failed to print image in bit-image mode", Array.Empty<byte>(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send raw ESC/POS command bytes to the printer
|
||||
/// </summary>
|
||||
/// <param name="command">Raw command bytes</param>
|
||||
public async Task SendRawCommandAsync(byte[] command)
|
||||
{
|
||||
_logger?.LogDebug("Sending raw command: {Length} bytes", command.Length);
|
||||
await SendRawAsync(command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine overall health status from individual status components
|
||||
/// </summary>
|
||||
private OverallStatus DetermineOverallStatus(
|
||||
byte? printerModel,
|
||||
PrinterStatus printerStatus,
|
||||
OfflineStatus offlineStatus,
|
||||
ErrorStatus errorStatus,
|
||||
PaperSensorStatus paperStatus)
|
||||
{
|
||||
var recommendations = new List<string>();
|
||||
string statusText;
|
||||
string statusIcon;
|
||||
bool isReady;
|
||||
|
||||
// Check for critical errors
|
||||
if (errorStatus.UnrecoverableError)
|
||||
{
|
||||
statusText = "CRITICAL ERROR - Unrecoverable error detected";
|
||||
statusIcon = "🔴";
|
||||
isReady = false;
|
||||
recommendations.Add("Power cycle the printer and contact support");
|
||||
}
|
||||
else if (errorStatus.RecoverableError || errorStatus.AutoCutterError)
|
||||
{
|
||||
statusText = "ERROR - Recoverable error detected";
|
||||
statusIcon = "🟠";
|
||||
isReady = false;
|
||||
recommendations.Add("Clear any errors by fixing the underlying issue");
|
||||
|
||||
if (errorStatus.RecoverableError)
|
||||
{
|
||||
recommendations.Add("Check for paper jams and ensure paper is loaded correctly");
|
||||
}
|
||||
if (errorStatus.AutoCutterError)
|
||||
{
|
||||
recommendations.Add("Check for paper jams in cutter and remove any obstructions");
|
||||
}
|
||||
}
|
||||
// Check offline status
|
||||
else if (!printerStatus.IsOnline)
|
||||
{
|
||||
var reasons = new List<string>();
|
||||
if (offlineStatus.CoverOpen) reasons.Add("Cover open");
|
||||
if (offlineStatus.PaperEnd) reasons.Add("Paper out");
|
||||
if (offlineStatus.ErrorOccurred) reasons.Add("Error occurred");
|
||||
|
||||
var reasonText = reasons.Count > 0 ? string.Join(", ", reasons) : "Unknown reason";
|
||||
statusText = $"OFFLINE - {reasonText}";
|
||||
statusIcon = "🟡";
|
||||
isReady = false;
|
||||
recommendations.Add("Bring printer online by resolving offline causes");
|
||||
}
|
||||
// Check paper status
|
||||
else if (!paperStatus.PaperPresent)
|
||||
{
|
||||
statusText = "WARNING - Paper out or not detected";
|
||||
statusIcon = "🟡";
|
||||
isReady = false;
|
||||
recommendations.Add("Load paper into the printer");
|
||||
}
|
||||
else if (paperStatus.PaperNearEnd)
|
||||
{
|
||||
statusText = "WARNING - Paper near end";
|
||||
statusIcon = "🟡";
|
||||
isReady = true; // Can still print, but warning
|
||||
recommendations.Add("Replace paper roll soon");
|
||||
}
|
||||
// Check cover
|
||||
else if (!printerStatus.IsCoverClosed)
|
||||
{
|
||||
statusText = "WARNING - Cover open";
|
||||
statusIcon = "🟡";
|
||||
isReady = false;
|
||||
recommendations.Add("Close the printer cover");
|
||||
}
|
||||
// All checks passed
|
||||
else
|
||||
{
|
||||
statusText = "READY - Printer is operational";
|
||||
statusIcon = "🟢";
|
||||
isReady = true;
|
||||
}
|
||||
|
||||
return new OverallStatus
|
||||
{
|
||||
PrinterModel = printerModel?.ToString("X2"),
|
||||
PrinterStatus = printerStatus,
|
||||
OfflineStatus = offlineStatus,
|
||||
ErrorStatus = errorStatus,
|
||||
PaperStatus = paperStatus,
|
||||
StatusText = statusText,
|
||||
StatusIcon = statusIcon,
|
||||
IsReady = isReady,
|
||||
Recommendations = recommendations
|
||||
};
|
||||
}
|
||||
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
throw new EpsonConnectionException("Not connected to printer. Call ConnectAsync first.");
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
Disconnect();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
415
Inspectron.Epson/EpsonPrinterDiscovery.cs
Normal file
415
Inspectron.Epson/EpsonPrinterDiscovery.cs
Normal file
@@ -0,0 +1,415 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// Service Location Protocol (SLP) Discovery for Epson Printers
|
||||
/// Implements SLPv2 (RFC 2608) for printer discovery on port 427
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Discovers printers using SLP (Service Location Protocol)
|
||||
/// </summary>
|
||||
/// <param name="serviceType">Service type to search for (default: "service:printer")</param>
|
||||
/// <param name="scope">Scope to search in (default: "DEFAULT")</param>
|
||||
/// <param name="timeoutMs">Timeout in milliseconds</param>
|
||||
/// <returns>List of discovered printers</returns>
|
||||
public static List<DiscoveredPrinter> DiscoverPrinters(
|
||||
string serviceType = "service:printer",
|
||||
string scope = "DEFAULT",
|
||||
int timeoutMs = DISCOVER_TIMEOUT_MS)
|
||||
{
|
||||
var discoveredPrinters = new List<DiscoveredPrinter>();
|
||||
var printerUrls = new HashSet<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers printers asynchronously
|
||||
/// </summary>
|
||||
public static async Task<List<DiscoveredPrinter>> DiscoverPrintersAsync(
|
||||
string serviceType = "service:printer",
|
||||
string scope = "DEFAULT",
|
||||
int timeoutMs = DISCOVER_TIMEOUT_MS)
|
||||
{
|
||||
return await Task.Run(() => DiscoverPrinters(serviceType, scope, timeoutMs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an SLP Service Request packet (SLPv2 - RFC 2608)
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates if response is a proper SLP Service Reply
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses SLP Service Reply packet
|
||||
/// </summary>
|
||||
private static List<DiscoveredPrinter> ParseServiceReply(byte[] response, string sourceIp)
|
||||
{
|
||||
var printers = new List<DiscoveredPrinter>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 16-bit unsigned integer in big-endian format
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a string of specified length
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints hex dump of byte array for debugging
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
}
|
||||
273
Inspectron.Epson/EpsonPrinterStatus.cs
Normal file
273
Inspectron.Epson/EpsonPrinterStatus.cs
Normal file
@@ -0,0 +1,273 @@
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// General printer status (DLE EOT 1)
|
||||
/// Format: 0xx1xx10b
|
||||
/// </summary>
|
||||
public record PrinterStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Raw status byte received from printer
|
||||
/// </summary>
|
||||
public byte RawByte { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Binary representation of status byte
|
||||
/// </summary>
|
||||
public string Binary => Convert.ToString(RawByte, 2).PadLeft(8, '0');
|
||||
|
||||
/// <summary>
|
||||
/// Hex representation of status byte
|
||||
/// </summary>
|
||||
public string Hex => $"0x{RawByte:X2}";
|
||||
|
||||
/// <summary>
|
||||
/// Drawer kick-out connector pin 3 is HIGH (Bit 2)
|
||||
/// </summary>
|
||||
public bool DrawerOpen { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Printer is online (Bit 3: 0=online, 1=offline)
|
||||
/// </summary>
|
||||
public bool IsOnline { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Cover is closed (Bit 5: 0=closed, 1=open)
|
||||
/// </summary>
|
||||
public bool IsCoverClosed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Paper feed button is being pressed (Bit 6)
|
||||
/// </summary>
|
||||
public bool PaperFeedButtonPressed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Parse printer status byte
|
||||
/// </summary>
|
||||
public static PrinterStatus Parse(byte statusByte)
|
||||
{
|
||||
return new PrinterStatus
|
||||
{
|
||||
RawByte = statusByte,
|
||||
DrawerOpen = (statusByte & 0x04) != 0,
|
||||
IsOnline = (statusByte & 0x08) == 0,
|
||||
IsCoverClosed = (statusByte & 0x20) == 0,
|
||||
PaperFeedButtonPressed = (statusByte & 0x40) != 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offline status (DLE EOT 2) - Reasons for offline
|
||||
/// Format: 0xx1xx10b
|
||||
/// </summary>
|
||||
public record OfflineStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Raw status byte received from printer
|
||||
/// </summary>
|
||||
public byte RawByte { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Binary representation of status byte
|
||||
/// </summary>
|
||||
public string Binary => Convert.ToString(RawByte, 2).PadLeft(8, '0');
|
||||
|
||||
/// <summary>
|
||||
/// Hex representation of status byte
|
||||
/// </summary>
|
||||
public string Hex => $"0x{RawByte:X2}";
|
||||
|
||||
/// <summary>
|
||||
/// Cover is open (Bit 2)
|
||||
/// </summary>
|
||||
public bool CoverOpen { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Paper feed button is being pressed (Bit 3)
|
||||
/// </summary>
|
||||
public bool PaperFeedButton { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Paper end detected (Bit 5)
|
||||
/// </summary>
|
||||
public bool PaperEnd { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error occurred (Bit 6)
|
||||
/// </summary>
|
||||
public bool ErrorOccurred { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Parse offline status byte
|
||||
/// </summary>
|
||||
public static OfflineStatus Parse(byte statusByte)
|
||||
{
|
||||
return new OfflineStatus
|
||||
{
|
||||
RawByte = statusByte,
|
||||
CoverOpen = (statusByte & 0x04) != 0,
|
||||
PaperFeedButton = (statusByte & 0x08) != 0,
|
||||
PaperEnd = (statusByte & 0x20) != 0,
|
||||
ErrorOccurred = (statusByte & 0x40) != 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error status (DLE EOT 3)
|
||||
/// Format: 0xx1xx10b
|
||||
/// </summary>
|
||||
public record ErrorStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Raw status byte received from printer
|
||||
/// </summary>
|
||||
public byte RawByte { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Binary representation of status byte
|
||||
/// </summary>
|
||||
public string Binary => Convert.ToString(RawByte, 2).PadLeft(8, '0');
|
||||
|
||||
/// <summary>
|
||||
/// Hex representation of status byte
|
||||
/// </summary>
|
||||
public string Hex => $"0x{RawByte:X2}";
|
||||
|
||||
/// <summary>
|
||||
/// Recoverable error occurred (Bit 2)
|
||||
/// </summary>
|
||||
public bool RecoverableError { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Auto-cutter error occurred (Bit 3)
|
||||
/// </summary>
|
||||
public bool AutoCutterError { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Unrecoverable error occurred (Bit 5)
|
||||
/// </summary>
|
||||
public bool UnrecoverableError { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Auto-recovery error occurred (Bit 6)
|
||||
/// </summary>
|
||||
public bool AutoRecoveryError { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if any error is present
|
||||
/// </summary>
|
||||
public bool HasError => RecoverableError || AutoCutterError || UnrecoverableError || AutoRecoveryError;
|
||||
|
||||
/// <summary>
|
||||
/// Parse error status byte
|
||||
/// </summary>
|
||||
public static ErrorStatus Parse(byte statusByte)
|
||||
{
|
||||
return new ErrorStatus
|
||||
{
|
||||
RawByte = statusByte,
|
||||
RecoverableError = (statusByte & 0x04) != 0,
|
||||
AutoCutterError = (statusByte & 0x08) != 0,
|
||||
UnrecoverableError = (statusByte & 0x20) != 0,
|
||||
AutoRecoveryError = (statusByte & 0x40) != 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Paper sensor status (DLE EOT 4)
|
||||
/// Format: 0xx1xx10b
|
||||
/// </summary>
|
||||
public record PaperSensorStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Raw status byte received from printer
|
||||
/// </summary>
|
||||
public byte RawByte { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Binary representation of status byte
|
||||
/// </summary>
|
||||
public string Binary => Convert.ToString(RawByte, 2).PadLeft(8, '0');
|
||||
|
||||
/// <summary>
|
||||
/// Hex representation of status byte
|
||||
/// </summary>
|
||||
public string Hex => $"0x{RawByte:X2}";
|
||||
|
||||
/// <summary>
|
||||
/// Paper roll is near end (Bits 2-3)
|
||||
/// </summary>
|
||||
public bool PaperNearEnd { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Paper is present (Bits 5-6, inverted logic)
|
||||
/// </summary>
|
||||
public bool PaperPresent { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Parse paper sensor status byte
|
||||
/// </summary>
|
||||
public static PaperSensorStatus Parse(byte statusByte)
|
||||
{
|
||||
return new PaperSensorStatus
|
||||
{
|
||||
RawByte = statusByte,
|
||||
PaperNearEnd = (statusByte & 0x0C) != 0,
|
||||
PaperPresent = (statusByte & 0x60) == 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overall printer status with aggregated information and health status
|
||||
/// </summary>
|
||||
public record OverallStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Printer model/ID
|
||||
/// </summary>
|
||||
public string? PrinterModel { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// General printer status
|
||||
/// </summary>
|
||||
public PrinterStatus? PrinterStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Offline status
|
||||
/// </summary>
|
||||
public OfflineStatus? OfflineStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error status
|
||||
/// </summary>
|
||||
public ErrorStatus? ErrorStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Paper sensor status
|
||||
/// </summary>
|
||||
public PaperSensorStatus? PaperStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Overall status text description
|
||||
/// </summary>
|
||||
public string StatusText { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// True if printer is ready for operation
|
||||
/// </summary>
|
||||
public bool IsReady { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// List of recommendations to resolve issues
|
||||
/// </summary>
|
||||
public List<string> Recommendations { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Status emoji/icon
|
||||
/// </summary>
|
||||
public string StatusIcon { get; init; } = "❓";
|
||||
}
|
||||
16
Inspectron.Epson/Inspectron.Epson.csproj
Normal file
16
Inspectron.Epson/Inspectron.Epson.csproj
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
using Inspectron.Epson.PrintServer.PrintServices;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.ConfigurationSources;
|
||||
|
||||
public class EpsonPrintServiceConfiguration: IPrinterConfigurationSource, IAssignedPrinterRepository
|
||||
{
|
||||
public string GroupId { get; set; }
|
||||
public string RestaurantId { get; set; }
|
||||
|
||||
public Dictionary<string, PrinterConfiguration> PrinterConfigurations { get; set; }
|
||||
public PrinterConfiguration GetConfiguration(string printerAddress)
|
||||
{
|
||||
return PrinterConfigurations[printerAddress];
|
||||
}
|
||||
|
||||
public string? GetAssignedPrinter(string workAreaId)
|
||||
{
|
||||
return PrinterConfigurations.Values.Where(x => x.AreaId == workAreaId).Select(x=>x.Address).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Inspectron.Epson.PrintServer.PrintServices;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.ConfigurationSources;
|
||||
|
||||
public class FixedConfigurationSource: IPrinterConfigurationSource
|
||||
{
|
||||
public PrinterConfiguration GetConfiguration(string printerAddress)
|
||||
{
|
||||
return new PrinterConfiguration()
|
||||
{
|
||||
Address = "127.0.0.1:8888",
|
||||
FontSize = 1,
|
||||
LogoFilename = "test.png"
|
||||
};
|
||||
}
|
||||
}
|
||||
6
Inspectron.Epson/PrintServer/IPrintJobSource.cs
Normal file
6
Inspectron.Epson/PrintServer/IPrintJobSource.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Inspectron.Epson.PrintServer;
|
||||
|
||||
public interface IPrintJobSource
|
||||
{
|
||||
Task<PrintJob> GetNextJobAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
122
Inspectron.Epson/PrintServer/JobSources/SignalRPrintJobSource.cs
Normal file
122
Inspectron.Epson/PrintServer/JobSources/SignalRPrintJobSource.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Channels;
|
||||
using Inspectron.Epson.PrintServer.ConfigurationSources;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.JobSources;
|
||||
|
||||
public class SignalRPrintJobSource: IPrintJobSource
|
||||
{
|
||||
private readonly EpsonPrintServiceConfiguration _groupConfiguration;
|
||||
private readonly ILogger _logger;
|
||||
private readonly HubConnection _connection;
|
||||
private readonly Channel<PrintJob> _printJobChannel = Channel.CreateUnbounded<PrintJob>();
|
||||
|
||||
public SignalRPrintJobSource(EpsonPrintServiceConfiguration groupConfiguration, ILogger logger)
|
||||
{
|
||||
_groupConfiguration = groupConfiguration;
|
||||
_logger = logger;
|
||||
|
||||
_connection = new HubConnectionBuilder()
|
||||
.WithUrl("https://api.stage.gastrojames.ch/hubs/internal?access_token=https://api.stage.gastrojames.ch/hubs/internal")
|
||||
.WithAutomaticReconnect(new InfiniteRetryPolicy())
|
||||
.Build();
|
||||
|
||||
// Register handlers BEFORE starting connection
|
||||
_connection.On<PrintJobFromSignalR>("PrintJob", OnPrintJobReceived);
|
||||
|
||||
_connection.Reconnecting += OnReconnecting;
|
||||
_connection.Reconnected += OnReconnected;
|
||||
_connection.Closed += OnClosed;
|
||||
|
||||
_ = InitializeConnectionAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeConnectionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _connection.StartAsync();
|
||||
await _connection.InvokeAsync("JoinGroup", _groupConfiguration.GroupId);
|
||||
_logger.LogInformation("SignalR connection started and joined group {GroupId}.", _groupConfiguration.GroupId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to initialize SignalR connection.");
|
||||
}
|
||||
}
|
||||
|
||||
private Task OnReconnecting(Exception? exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "SignalR connection lost. Reconnecting...");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OnReconnected(string? connectionId)
|
||||
{
|
||||
_logger.LogInformation("SignalR reconnected with connection ID: {ConnectionId}. Rejoining group...", connectionId);
|
||||
try
|
||||
{
|
||||
await _connection.InvokeAsync("JoinGroup", _groupConfiguration.GroupId);
|
||||
_logger.LogInformation("Rejoined SignalR group {GroupId} after reconnection.", _groupConfiguration.GroupId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to rejoin group after reconnection.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnClosed(Exception? exception)
|
||||
{
|
||||
_logger.LogError(exception, "SignalR connection closed. Attempting manual restart...");
|
||||
|
||||
// Manual reconnection loop if automatic reconnection exhausted (shouldn't happen with infinite retry)
|
||||
await Task.Delay(5000); // Wait before retry
|
||||
try
|
||||
{
|
||||
await _connection.StartAsync();
|
||||
await _connection.InvokeAsync("JoinGroup", _groupConfiguration.GroupId);
|
||||
_logger.LogInformation("Manually reconnected to SignalR.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Manual reconnection failed. Will retry on next connection closed event.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPrintJobReceived(PrintJobFromSignalR args)
|
||||
{
|
||||
|
||||
_logger.LogInformation("Received print job for working area: {PrintJob}", JsonSerializer.Serialize(args));
|
||||
_printJobChannel.Writer.TryWrite(new PrintJob
|
||||
{
|
||||
AreaId = args.WorkingAreaId,
|
||||
Document = args.Content,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<PrintJob> GetNextJobAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return await _printJobChannel.Reader.ReadAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public class PrintJobFromSignalR
|
||||
{
|
||||
[JsonPropertyName("workingAreaId")]
|
||||
public string WorkingAreaId { get; set; }
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; }
|
||||
}
|
||||
|
||||
private class InfiniteRetryPolicy : IRetryPolicy
|
||||
{
|
||||
public TimeSpan? NextRetryDelay(RetryContext retryContext)
|
||||
{
|
||||
// Exponential backoff with a cap at 30 seconds
|
||||
var delay = Math.Min(Math.Pow(2, retryContext.PreviousRetryCount), 30);
|
||||
return TimeSpan.FromSeconds(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Inspectron.Epson/PrintServer/JobSources/SingleJobSource.cs
Normal file
18
Inspectron.Epson/PrintServer/JobSources/SingleJobSource.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.JobSources;
|
||||
|
||||
public class SingleJobSource: IPrintJobSource
|
||||
{
|
||||
public SingleJobSource(PrintJob job)
|
||||
{
|
||||
_channel.Writer.WriteAsync(job);
|
||||
}
|
||||
|
||||
private Channel<PrintJob> _channel = Channel.CreateUnbounded<PrintJob>();
|
||||
public async Task<PrintJob> GetNextJobAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return await _channel.Reader.ReadAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
53
Inspectron.Epson/PrintServer/PrintLoop.cs
Normal file
53
Inspectron.Epson/PrintServer/PrintLoop.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Inspectron.Epson.PrintServer.PrintServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer;
|
||||
|
||||
public class PrintLoop
|
||||
{
|
||||
private readonly global::PrintServer _printServer;
|
||||
private readonly IPrintService _printService;
|
||||
private readonly IPrintJobSource _jobSource;
|
||||
private readonly IAssignedPrinterRepository _assignedPrinterRepository;
|
||||
private readonly ILogger _logger;
|
||||
private CancellationTokenSource? _cancellationSource;
|
||||
private CancellationToken _cancellationToken;
|
||||
|
||||
public PrintLoop(global::PrintServer printServer,IPrintService printService, IPrintJobSource jobSource, IAssignedPrinterRepository assignedPrinterRepository, ILogger logger)
|
||||
{
|
||||
_printServer = printServer;
|
||||
_printService = printService;
|
||||
_jobSource = jobSource;
|
||||
_assignedPrinterRepository = assignedPrinterRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task StartAsync()
|
||||
{
|
||||
_cancellationSource = new CancellationTokenSource();
|
||||
_cancellationToken = _cancellationSource.Token;
|
||||
_= Task.Run(Loop);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public async Task Loop()
|
||||
{
|
||||
while (!_cancellationSource!.Token.IsCancellationRequested)
|
||||
{
|
||||
var job = await _jobSource.GetNextJobAsync(_cancellationToken);
|
||||
var assigned = _assignedPrinterRepository.GetAssignedPrinter(job.AreaId);
|
||||
if (assigned == null)
|
||||
{
|
||||
_logger.LogWarning("Received print job for unassigned area {AreaId}, skipping", job.AreaId);
|
||||
continue;
|
||||
}
|
||||
|
||||
_printServer.SubmitJob(assigned, job);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync()
|
||||
{
|
||||
_cancellationSource!.Cancel();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.PrintServices;
|
||||
|
||||
public class EpsonPrintService: IPrintService
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IPrinterFactory _printerFactory;
|
||||
private readonly IPrinterConfigurationSource _printerConfigurationSource;
|
||||
|
||||
public EpsonPrintService(ILogger logger, IPrinterFactory printerFactory, IPrinterConfigurationSource printerConfigurationSource)
|
||||
{
|
||||
_logger = logger;
|
||||
_printerFactory = printerFactory;
|
||||
_printerConfigurationSource = printerConfigurationSource;
|
||||
}
|
||||
|
||||
public async Task<bool> PrintAsync(string printerIp, PrintJob job)
|
||||
{
|
||||
try
|
||||
{
|
||||
var configuration = _printerConfigurationSource.GetConfiguration(printerIp);
|
||||
await using var epsonPrinter = new EpsonPrinter(_logger);
|
||||
string address;
|
||||
int port = 9100;
|
||||
var printerAddress = configuration.Address;
|
||||
if (printerAddress.Contains(":"))
|
||||
{
|
||||
var parts = printerAddress.Split(':');
|
||||
address = parts[0];
|
||||
port = int.Parse(parts[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
address = printerAddress;
|
||||
}
|
||||
|
||||
await epsonPrinter.ConnectAsync(address, port);
|
||||
|
||||
var printerId = await epsonPrinter.GetPrinterIdAsync();
|
||||
|
||||
var status = await epsonPrinter.GetPrinterStatusAsync();
|
||||
if (!status.IsOnline)
|
||||
{
|
||||
throw new InvalidOperationException("Printer is offline or out of paper.");
|
||||
}
|
||||
|
||||
var printerAdapter = _printerFactory.CreatePrinterFromId(printerId.Value, epsonPrinter);
|
||||
|
||||
await printerAdapter.InitAsync();
|
||||
|
||||
if (configuration.LogoFilename != null)
|
||||
{
|
||||
await printerAdapter.PrintImageAsync(configuration.LogoFilename);
|
||||
}
|
||||
|
||||
await printerAdapter.SetFontSizeAsync(configuration.FontSize);
|
||||
|
||||
await printerAdapter.PrintTextAsync(job.Document);
|
||||
await Task.Delay(200);
|
||||
status = await epsonPrinter.GetPrinterStatusAsync();
|
||||
if (!status.IsOnline)
|
||||
{
|
||||
throw new InvalidOperationException("Printer is offline or out of paper.");
|
||||
}
|
||||
|
||||
await printerAdapter.Cut();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning( e, "Failed to print job for printer {PrinterUid}", printerIp);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Inspectron.Epson.PrintServer.PrintServices;
|
||||
|
||||
public interface IAssignedPrinterRepository
|
||||
{
|
||||
string? GetAssignedPrinter(string workAreaId);
|
||||
}
|
||||
10
Inspectron.Epson/PrintServer/PrintServices/IPrinter.cs
Normal file
10
Inspectron.Epson/PrintServer/PrintServices/IPrinter.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Inspectron.Epson.PrintServer.PrintServices;
|
||||
|
||||
public interface IPrinter
|
||||
{
|
||||
public Task InitAsync();
|
||||
public Task PrintImageAsync(string path);
|
||||
public Task SetFontSizeAsync(int fontSize);
|
||||
public Task PrintTextAsync(string text);
|
||||
public Task Cut();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Inspectron.Epson.PrintServer.PrintServices;
|
||||
|
||||
public interface IPrinterConfigurationSource
|
||||
{
|
||||
PrinterConfiguration GetConfiguration(string printerAddress);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Inspectron.Epson.PrintServer.PrintServices;
|
||||
|
||||
public interface IPrinterFactory
|
||||
{
|
||||
IPrinter CreatePrinterFromId(byte printerId, EpsonPrinter epsonPrinter);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Inspectron.Epson.PrintServer.PrintServices;
|
||||
|
||||
public class PrinterConfiguration
|
||||
{
|
||||
public string? LogoFilename { get; set; }
|
||||
public int FontSize { get; set; }
|
||||
public string Address { get; set; }
|
||||
|
||||
public string AreaId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Inspectron.Epson.PrintServer.PrintServices;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.PrinterAssinment;
|
||||
|
||||
public class TestAssignedPrinterRepository:IAssignedPrinterRepository
|
||||
{
|
||||
public string? GetAssignedPrinter(string workAreaId)
|
||||
{
|
||||
|
||||
return "127.0.0.1";
|
||||
}
|
||||
}
|
||||
21
Inspectron.Epson/PrintServer/Printers/PrinterFactory.cs
Normal file
21
Inspectron.Epson/PrintServer/Printers/PrinterFactory.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Inspectron.Epson.PrintServer.PrintServices;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.Printers;
|
||||
|
||||
public class PrinterFactory:IPrinterFactory
|
||||
{
|
||||
public IPrinter CreatePrinterFromId(byte printerId, EpsonPrinter epsonPrinter)
|
||||
{
|
||||
switch (printerId)
|
||||
{
|
||||
case 0x13:
|
||||
return new TM_U220IITranslated(epsonPrinter);
|
||||
case 0x0D:
|
||||
return new TM_U220IITranslated(epsonPrinter);
|
||||
case 0x01:
|
||||
return new TM_T30IIITranslated(epsonPrinter);
|
||||
default:
|
||||
throw new NotSupportedException($"Printer with ID {printerId:X2} is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
47
Inspectron.Epson/PrintServer/Printers/TM-T30III.cs
Normal file
47
Inspectron.Epson/PrintServer/Printers/TM-T30III.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Inspectron.Epson.PrintServer.PrintServices;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.Printers;
|
||||
|
||||
public class TM_T30III:IPrinter
|
||||
{
|
||||
private readonly EpsonPrinter _printer;
|
||||
|
||||
public TM_T30III(EpsonPrinter printer)
|
||||
{
|
||||
_printer = printer;
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await _printer.InitAsync();
|
||||
}
|
||||
|
||||
public async Task PrintImageAsync(string path)
|
||||
{
|
||||
await _printer.SetAbsolutePrintPosition(100);
|
||||
await _printer.LoadImageAsync(Path.Combine("logos", path), 384);
|
||||
await Task.Delay(100);
|
||||
//await _printer.PrintLoadedImage();
|
||||
await _printer.FeedLinesAsync(1);
|
||||
await _printer.SetAbsolutePrintPosition(0);
|
||||
}
|
||||
|
||||
public async Task SetFontSizeAsync(int fontSize)
|
||||
{
|
||||
await _printer.SetFontSizeAsync(fontSize, fontSize);
|
||||
}
|
||||
|
||||
public async Task PrintTextAsync(string text)
|
||||
{
|
||||
await _printer.PrintTextAsync(text);
|
||||
await _printer.FeedLinesAsync(5);
|
||||
var status = await _printer.GetPrinterStatusAsync();
|
||||
}
|
||||
|
||||
public async Task Cut()
|
||||
{
|
||||
await _printer.CutAsync();
|
||||
}
|
||||
}
|
||||
118
Inspectron.Epson/PrintServer/Printers/TM-T30IIITranslated.cs
Normal file
118
Inspectron.Epson/PrintServer/Printers/TM-T30IIITranslated.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System.Text.Json;
|
||||
using Inspectron.Epson.PrintServer.Printers.Utils;
|
||||
using Inspectron.Epson.PrintServer.Printers.Utils.FinalRecipe;
|
||||
using Inspectron.Epson.PrintServer.PrintServices;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.Printers;
|
||||
|
||||
public class TM_T30IIITranslated:IPrinter
|
||||
{
|
||||
private readonly EpsonPrinter _printer;
|
||||
|
||||
public TM_T30IIITranslated(EpsonPrinter printer)
|
||||
{
|
||||
_printer = printer;
|
||||
}
|
||||
|
||||
public Task InitAsync()
|
||||
{
|
||||
return _printer.InitAsync();
|
||||
}
|
||||
|
||||
public async Task PrintImageAsync(string path)
|
||||
{
|
||||
await _printer.SetAbsolutePrintPosition(100);
|
||||
await _printer.LoadImageAsync(Path.Combine("logos", path), 384);
|
||||
await Task.Delay(100);
|
||||
await _printer.FeedLinesAsync(1);
|
||||
await _printer.SetAbsolutePrintPosition(0);
|
||||
}
|
||||
|
||||
public async Task SetFontSizeAsync(int fontSize)
|
||||
{
|
||||
//await _printer.SetFontSizeAsync(fontSize, fontSize);
|
||||
}
|
||||
|
||||
public async Task PrintTextAsync(string text)
|
||||
{
|
||||
Receipt? receipt;
|
||||
try
|
||||
{
|
||||
receipt = JsonSerializer.Deserialize<Receipt>(text);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback to kitchen receipt
|
||||
await PrintKitchen(text);
|
||||
return;
|
||||
}
|
||||
var converter = new ReceiptConverter(lineWidth: 48, bigFontLineWidth: 24);
|
||||
var printCommands = converter.ConvertToPrintCommands(receipt);
|
||||
await _printer.FeedLinesAsync(1);
|
||||
await _printer.SetCustomLineSpacing(22);
|
||||
|
||||
foreach (var command in printCommands)
|
||||
{
|
||||
string attributes = "";
|
||||
if (command.IsBig) attributes += "[BIG]\n";
|
||||
if (command.IsBold) attributes += "[BOLD]\n";
|
||||
|
||||
Console.WriteLine($"{attributes}{command.Text}");
|
||||
|
||||
if (command.IsBig)
|
||||
{
|
||||
await _printer.SetFontSizeAsync(2, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _printer.SetFontSizeAsync(1, 1);
|
||||
}
|
||||
|
||||
await _printer.SetEmphasized(command.IsBold);
|
||||
|
||||
await _printer.PrintTextAsync(command.Text + "\n");
|
||||
|
||||
}
|
||||
|
||||
await _printer.SetDefaultLineSpacing();
|
||||
await _printer.FeedLinesAsync(10);
|
||||
await Task.Delay(200);
|
||||
}
|
||||
|
||||
private async Task PrintKitchen(string text)
|
||||
{
|
||||
ReceiptTranslator translator = new ReceiptTranslator();
|
||||
var ast = translator.ParseReceipt(text);
|
||||
KitchenReceiptPrinter printer = new KitchenReceiptPrinter(lineWidth: 33, 21);
|
||||
var commands = printer.ConvertToCommands((KitchenReceipt)ast);
|
||||
foreach (KitchenPrintCommand command in commands)
|
||||
{
|
||||
if (command.IsBig)
|
||||
{
|
||||
await _printer.SetFontSizeAsync(2,2);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _printer.SetFontSizeAsync(1, 1);
|
||||
}
|
||||
|
||||
|
||||
await Task.Delay(200);
|
||||
await _printer.SetRedColor(command.IsRed);
|
||||
await Task.Delay(200);
|
||||
await _printer.SetEmphasized(command.IsBold);
|
||||
await Task.Delay(200);
|
||||
await _printer.PrintTextAsync(command.Text + "\n");
|
||||
await Task.Delay(200);
|
||||
}
|
||||
|
||||
await _printer.FeedLinesAsync(10);
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
public async Task Cut()
|
||||
{
|
||||
await _printer.CutAsync();
|
||||
await Task.Delay(200);
|
||||
}
|
||||
}
|
||||
51
Inspectron.Epson/PrintServer/Printers/TM-U220II.cs
Normal file
51
Inspectron.Epson/PrintServer/Printers/TM-U220II.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Inspectron.Epson.PrintServer.PrintServices;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.Printers;
|
||||
|
||||
public class TM_U220II:IPrinter
|
||||
{
|
||||
private readonly EpsonPrinter _printer;
|
||||
|
||||
public TM_U220II(EpsonPrinter printer)
|
||||
{
|
||||
_printer = printer;
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await _printer.InitAsync();
|
||||
await Task.Delay(200);
|
||||
}
|
||||
|
||||
public Task PrintImageAsync(string path)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task SetFontSizeAsync(int fontSize)
|
||||
{
|
||||
await _printer.SetBiggerFontTM220(fontSize == 2);
|
||||
await Task.Delay(200);
|
||||
await _printer.SelectFont(EpsonCommands.PrinterFont.A);
|
||||
await Task.Delay(200);
|
||||
}
|
||||
|
||||
public async Task PrintTextAsync(string text)
|
||||
{
|
||||
await _printer.PrintTextAsync(text);
|
||||
var lines = text.Split('\n').Length;
|
||||
for (int i = 0; i < lines; i++)
|
||||
{
|
||||
await Task.Delay(200);
|
||||
}
|
||||
await _printer.FeedLinesAsync(5);
|
||||
await Task.Delay(200);
|
||||
}
|
||||
|
||||
public async Task Cut()
|
||||
{
|
||||
await _printer.CutAsync();
|
||||
await Task.Delay(200);
|
||||
}
|
||||
}
|
||||
67
Inspectron.Epson/PrintServer/Printers/TM-U220IITranslated.cs
Normal file
67
Inspectron.Epson/PrintServer/Printers/TM-U220IITranslated.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using Inspectron.Epson.PrintServer.Printers.Utils;
|
||||
using Inspectron.Epson.PrintServer.PrintServices;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.Printers;
|
||||
|
||||
public class TM_U220IITranslated:IPrinter
|
||||
{
|
||||
private readonly EpsonPrinter _printer;
|
||||
|
||||
public TM_U220IITranslated(EpsonPrinter printer)
|
||||
{
|
||||
_printer = printer;
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await _printer.InitAsync();
|
||||
await Task.Delay(200);
|
||||
}
|
||||
|
||||
public Task PrintImageAsync(string path)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task SetFontSizeAsync(int fontSize)
|
||||
{
|
||||
//await _printer.SetBiggerFontTM220(fontSize == 2);
|
||||
//await Task.Delay(200);
|
||||
//await _printer.SelectFont(EpsonCommands.PrinterFont.A);
|
||||
//await Task.Delay(200);
|
||||
}
|
||||
|
||||
public async Task PrintTextAsync(string text)
|
||||
{
|
||||
ReceiptTranslator translator = new ReceiptTranslator();
|
||||
var ast = translator.ParseReceipt(text);
|
||||
KitchenReceiptPrinter printer = new KitchenReceiptPrinter(lineWidth:33,21);
|
||||
var commands = printer.ConvertToCommands((KitchenReceipt)ast);
|
||||
foreach (KitchenPrintCommand command in commands)
|
||||
{
|
||||
await _printer.SetBiggerFontTM220(command.IsBig);
|
||||
await Task.Delay(200);
|
||||
await _printer.SetRedColor(command.IsRed);
|
||||
await Task.Delay(200);
|
||||
await _printer.SetEmphasized(command.IsBold);
|
||||
await Task.Delay(200);
|
||||
await _printer.PrintTextAsync(command.Text + "\n");
|
||||
await Task.Delay(200);
|
||||
}
|
||||
//await _printer.PrintTextAsync(text);
|
||||
//var lines = text.Split('\n').Length;
|
||||
//for (int i = 0; i < lines; i++)
|
||||
//{
|
||||
// await Task.Delay(200);
|
||||
//}
|
||||
await _printer.FeedLinesAsync(10);
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
public async Task Cut()
|
||||
{
|
||||
await _printer.CutAsync();
|
||||
await Task.Delay(200);
|
||||
}
|
||||
}
|
||||
78
Inspectron.Epson/PrintServer/Printers/Utils/ASTNode.cs
Normal file
78
Inspectron.Epson/PrintServer/Printers/Utils/ASTNode.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.Printers.Utils;
|
||||
|
||||
public record ASTNode
|
||||
{
|
||||
public string Accept(IAccepter compiler)
|
||||
{
|
||||
return ((string)compiler.GetType()
|
||||
.GetMethod("Visit", BindingFlags.Public | BindingFlags.Instance, new[] { this.GetType() })!
|
||||
.Invoke(compiler, new[] { this })!)!;
|
||||
}
|
||||
|
||||
public static string PrintNode(ASTNode node)
|
||||
{
|
||||
// print type name and string representation of all its fields
|
||||
var type = node.GetType();
|
||||
var fields = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(type.Name);
|
||||
sb.Append("(");
|
||||
int i = 0;
|
||||
foreach (var field in fields)
|
||||
{
|
||||
if (i++ > 0) sb.Append(",");
|
||||
var value = field.GetValue(node);
|
||||
if (value is ASTNode valueNode)
|
||||
{
|
||||
sb.Append(PrintNode(valueNode));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(value);
|
||||
}
|
||||
|
||||
// if field is list - print all its elements
|
||||
if (field.PropertyType.IsGenericType && field.PropertyType.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
var list = (IList)field.GetValue(node);
|
||||
sb.Append("[");
|
||||
int j = 0;
|
||||
sb.AppendLine();
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (j++ > 0)
|
||||
{
|
||||
sb.Append(",");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (item is ASTNode itemNode)
|
||||
{
|
||||
sb.Append(PrintNode(itemNode));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(item);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sb.Append("]");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append(")");
|
||||
return sb.ToString();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAccepter
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace Inspectron.Epson.PrintServer.Printers.Utils.FinalRecipe;
|
||||
|
||||
public class Receipt
|
||||
{
|
||||
public string CompanyName { get; set; }
|
||||
public string Address1 { get; set; }
|
||||
public string Address2 { get; set; }
|
||||
public string Phone { get; set; }
|
||||
public string ReceiptNumber { get; set; }
|
||||
public DateTime DateTime { get; set; }
|
||||
public int Guests { get; set; }
|
||||
public List<ReceiptItem> Items { get; set; } = new List<ReceiptItem>();
|
||||
public decimal Total { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public decimal? TotalInAlternateCurrency { get; set; }
|
||||
public string AlternateCurrency { get; set; }
|
||||
public string PaymentMethod { get; set; }
|
||||
public decimal PaymentAmount { get; set; }
|
||||
public List<TaxInfo> TaxBreakdown { get; set; } = new List<TaxInfo>();
|
||||
public string ServerName { get; set; }
|
||||
public string Terminal { get; set; }
|
||||
public string TableNumber { get; set; }
|
||||
public string VatNumber { get; set; }
|
||||
public string ThankYouMessage { get; set; }
|
||||
public string GoodbyeMessageLine1 { get; set; }
|
||||
public string GoodbyeMessageLine2 { get; set; }
|
||||
}
|
||||
|
||||
public class ReceiptItem
|
||||
{
|
||||
public int Quantity { get; set; }
|
||||
public string Description { get; set; }
|
||||
public decimal UnitPrice { get; set; }
|
||||
public decimal TotalPrice { get; set; }
|
||||
public string TaxCategory { get; set; }
|
||||
}
|
||||
|
||||
public class TaxInfo
|
||||
{
|
||||
public string Category { get; set; }
|
||||
public decimal Rate { get; set; }
|
||||
public decimal Gross { get; set; }
|
||||
public decimal Net { get; set; }
|
||||
public decimal TaxAmount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
}
|
||||
|
||||
public class PrintCommand
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public bool IsBig { get; set; }
|
||||
public bool IsBold { get; set; }
|
||||
|
||||
public PrintCommand(string text, bool isBig = false, bool isBold = false)
|
||||
{
|
||||
Text = text;
|
||||
IsBig = isBig;
|
||||
IsBold = isBold;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
namespace Inspectron.Epson.PrintServer.Printers.Utils.FinalRecipe;
|
||||
|
||||
public class ReceiptConverter
|
||||
{
|
||||
private readonly int _lineWidth;
|
||||
private readonly int _bigFontLineWidth;
|
||||
|
||||
public ReceiptConverter(int lineWidth = 42, int bigFontLineWidth = 21)
|
||||
{
|
||||
_lineWidth = lineWidth;
|
||||
_bigFontLineWidth = bigFontLineWidth;
|
||||
}
|
||||
|
||||
public List<PrintCommand> ConvertToPrintCommands(Receipt receipt)
|
||||
{
|
||||
var commands = new List<PrintCommand>();
|
||||
|
||||
// Header - Company info
|
||||
commands.Add(new PrintCommand(Center(receipt.CompanyName, false)));
|
||||
commands.Add(new PrintCommand(Center(receipt.Address1, false)));
|
||||
commands.Add(new PrintCommand(Center(receipt.Address2, false)));
|
||||
commands.Add(new PrintCommand(Center(receipt.Phone, false)));
|
||||
commands.Add(new PrintCommand(""));
|
||||
commands.Add(new PrintCommand(""));
|
||||
|
||||
// Receipt info
|
||||
string receiptLine = $"Rechnung Nr. {receipt.ReceiptNumber}".PadRight(_lineWidth/2)+$"{receipt.DateTime:HH:mm dd.MM.yyyy}".PadLeft(_lineWidth/2);
|
||||
commands.Add(new PrintCommand(receiptLine,isBold:true));
|
||||
commands.Add(new PrintCommand($"Guests: {receipt.Guests}"));
|
||||
commands.Add(new PrintCommand(""));
|
||||
|
||||
// Items
|
||||
foreach (var item in receipt.Items)
|
||||
{
|
||||
string quantityDesc = $"{item.Quantity}x {item.Description}";
|
||||
string prices = $"{item.UnitPrice:F2}"+$"{item.TotalPrice:F2}".PadLeft(7)+$" {item.TaxCategory}";
|
||||
|
||||
// Calculate spacing to align prices to the right
|
||||
int spacesNeeded = _lineWidth - quantityDesc.Length - prices.Length;
|
||||
if (spacesNeeded < 1) spacesNeeded = 1;
|
||||
|
||||
string itemLine = quantityDesc + new string(' ', spacesNeeded) + prices;
|
||||
commands.Add(new PrintCommand(itemLine));
|
||||
}
|
||||
|
||||
commands.Add(new PrintCommand("")); // Reini
|
||||
commands.Add(new PrintCommand("---------".PadLeft(_lineWidth)));
|
||||
commands.Add(new PrintCommand("")); //Reini
|
||||
|
||||
// Total
|
||||
string totalLine = $"Summe: {receipt.Total:F2} {receipt.Currency}";
|
||||
commands.Add(new PrintCommand(Center(totalLine, true), true, true));
|
||||
commands.Add(new PrintCommand(""));
|
||||
|
||||
// Alternate currency
|
||||
if (receipt.TotalInAlternateCurrency.HasValue)
|
||||
{
|
||||
string altCurrencyLine = $"{receipt.TotalInAlternateCurrency:F2} {receipt.AlternateCurrency}";
|
||||
commands.Add(new PrintCommand(altCurrencyLine.PadLeft(_lineWidth)));
|
||||
commands.Add(new PrintCommand(""));
|
||||
}
|
||||
|
||||
// Payment method
|
||||
string paymentLine = $"{receipt.PaymentMethod}";
|
||||
string paymentAmount = $"{receipt.PaymentAmount:F2} {receipt.Currency}";
|
||||
int paymentSpaces = _lineWidth - paymentLine.Length - paymentAmount.Length;
|
||||
if (paymentSpaces < 1) paymentSpaces = 1;
|
||||
commands.Add(new PrintCommand(paymentLine + new string(' ', paymentSpaces) + paymentAmount,isBold:true));
|
||||
commands.Add(new PrintCommand(""));
|
||||
|
||||
// Tax breakdown
|
||||
foreach (var tax in receipt.TaxBreakdown)
|
||||
{
|
||||
string taxLine = "MwSt %".PadRight(_lineWidth/4)+" Brutto".PadRight(_lineWidth / 4) + " Netto".PadRight(_lineWidth / 4) + "MwSt".PadLeft(_lineWidth / 4);
|
||||
if (receipt.TaxBreakdown.IndexOf(tax) == 0)
|
||||
{
|
||||
commands.Add(new PrintCommand(taxLine));
|
||||
}
|
||||
|
||||
string taxDetail = ($"{tax.Category}:"+ $"{tax.Rate}%".PadLeft(5)).PadRight(_lineWidth / 4) + $"{tax.Gross:F2} {tax.Currency}".PadLeft(_lineWidth/4)+$"{tax.Net:F2} {tax.Currency}".PadLeft(_lineWidth/4)+$"{tax.TaxAmount:F2} {tax.Currency}".PadLeft(_lineWidth / 4);
|
||||
commands.Add(new PrintCommand(taxDetail));
|
||||
}
|
||||
commands.Add(new PrintCommand(""));
|
||||
|
||||
// Footer info
|
||||
//commands.Add(new PrintCommand(Center($"Bedient von: {receipt.ServerName}", false)));
|
||||
//commands.Add(new PrintCommand(Center($"Terminal: {receipt.Terminal}", false)));
|
||||
//commands.Add(new PrintCommand(Center($"Tisch: {receipt.TableNumber}", false)));
|
||||
commands.Add(new PrintCommand($"Bedient von:".PadLeft(_lineWidth / 2) + $" {receipt.ServerName}"));
|
||||
commands.Add(new PrintCommand($"Terminal:".PadLeft(_lineWidth / 2) + $" {receipt.Terminal}"));
|
||||
commands.Add(new PrintCommand($"Tisch:".PadLeft(_lineWidth / 2)+$" {receipt.TableNumber}"));
|
||||
commands.Add(new PrintCommand(""));
|
||||
commands.Add(new PrintCommand(""));
|
||||
|
||||
// VAT number
|
||||
commands.Add(new PrintCommand(Center(receipt.VatNumber, false)));
|
||||
|
||||
// Thank you message
|
||||
commands.Add(new PrintCommand(Center(receipt.ThankYouMessage, false)));
|
||||
commands.Add(new PrintCommand(Center(receipt.GoodbyeMessageLine1, false)));
|
||||
commands.Add(new PrintCommand(Center(receipt.GoodbyeMessageLine2, false)));
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
private string Center(string text, bool isBigFont)
|
||||
{
|
||||
int effectiveLineWidth = isBigFont ? _bigFontLineWidth : _lineWidth;
|
||||
|
||||
if (string.IsNullOrEmpty(text) || text.Length >= effectiveLineWidth)
|
||||
return text;
|
||||
|
||||
int totalPadding = effectiveLineWidth - text.Length;
|
||||
int leftPadding = totalPadding / 2;
|
||||
|
||||
return new string(' ', leftPadding) + text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Inspectron.Epson.PrintServer.Printers.Utils;
|
||||
|
||||
public class KitchenPrintCommand
|
||||
{
|
||||
public bool IsBig { get; set; }
|
||||
public bool IsBold { get; set; }
|
||||
public bool IsRed { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
namespace Inspectron.Epson.PrintServer.Printers.Utils;
|
||||
|
||||
public class KitchenReceiptPrinter
|
||||
{
|
||||
private readonly int _lineWidth;
|
||||
private readonly int _bigFontLineWidth;
|
||||
|
||||
public KitchenReceiptPrinter(int lineWidth = 42, int bigFontLineWidth = 21)
|
||||
{
|
||||
_lineWidth = lineWidth;
|
||||
_bigFontLineWidth = bigFontLineWidth;
|
||||
}
|
||||
|
||||
public List<KitchenPrintCommand> ConvertToCommands(KitchenReceipt receipt)
|
||||
{
|
||||
var commands = new List<KitchenPrintCommand>();
|
||||
|
||||
// Header: "Warme Küche" - Big, Bold, Red, Centered
|
||||
commands.Add(new KitchenPrintCommand
|
||||
{
|
||||
Text = Center(receipt.Location, isBigFont: true),
|
||||
IsBig = true,
|
||||
IsBold = true,
|
||||
IsRed = true
|
||||
});
|
||||
|
||||
commands.Add(Separator());
|
||||
|
||||
// Date and Owner info - Normal, Left-aligned
|
||||
commands.Add(new KitchenPrintCommand { Text = Center(receipt.Date,false) });
|
||||
var ownerLines = receipt.Owner.Split('\n');
|
||||
foreach (var line in ownerLines)
|
||||
{
|
||||
commands.Add(new KitchenPrintCommand { Text = Center(line, false) });
|
||||
}
|
||||
|
||||
// Table number - Big, Bold, Centered
|
||||
commands.Add(new KitchenPrintCommand
|
||||
{
|
||||
Text = Center($"Tisch: {receipt.Tisch}", isBigFont: true),
|
||||
IsBig = true,
|
||||
IsBold = true
|
||||
});
|
||||
|
||||
commands.Add(Separator());
|
||||
|
||||
// Items - Left-aligned
|
||||
foreach (var item in receipt.items)
|
||||
{
|
||||
if (item is KitchenProduct kp)
|
||||
{
|
||||
commands.Add(new KitchenPrintCommand
|
||||
{
|
||||
Text = $"{kp.Amount}x {item.Name}"
|
||||
});
|
||||
}
|
||||
if(item is KitchenGang kg)
|
||||
{
|
||||
commands.Add(new KitchenPrintCommand
|
||||
{
|
||||
IsRed = true,
|
||||
IsBig = true,
|
||||
IsBold = true,
|
||||
Text = Center(item.Name,true)
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
commands.Add(Separator());
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
private KitchenPrintCommand Separator()
|
||||
{
|
||||
return new KitchenPrintCommand
|
||||
{
|
||||
Text = new string('-', _lineWidth)
|
||||
};
|
||||
}
|
||||
|
||||
private string Center(string text, bool isBigFont)
|
||||
{
|
||||
int effectiveLineWidth = isBigFont ? _bigFontLineWidth : _lineWidth;
|
||||
|
||||
if (string.IsNullOrEmpty(text) || text.Length >= effectiveLineWidth)
|
||||
return text;
|
||||
|
||||
int totalPadding = effectiveLineWidth - text.Length;
|
||||
int leftPadding = totalPadding / 2;
|
||||
|
||||
return new string(' ', leftPadding) + text;
|
||||
}
|
||||
}
|
||||
206
Inspectron.Epson/PrintServer/Printers/Utils/ReceiptTranslator.cs
Normal file
206
Inspectron.Epson/PrintServer/Printers/Utils/ReceiptTranslator.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
namespace Inspectron.Epson.PrintServer.Printers.Utils;
|
||||
|
||||
|
||||
public record KitchenReceipt(string Location, string Date, string Owner, string Tisch, List<KitchenItem> items) :ASTNode;
|
||||
public record KitchenItem(string Name) : ASTNode;
|
||||
public record KitchenProduct(int Amount, string Name) : KitchenItem(Name);
|
||||
public record KitchenGang(string Name) : KitchenItem(Name);
|
||||
|
||||
public record FinalReceipt(string Location, string Phone, string URL, string Date, List<FinalReceiptItem> items, string total, string MWST, string Comment, string Thanks) : ASTNode;
|
||||
public record FinalReceiptItem(string Name, int Amount, string Price, string Total) : ASTNode;
|
||||
|
||||
|
||||
public class ReceiptTranslator
|
||||
{
|
||||
public ASTNode ParseReceipt(string receiptText)
|
||||
{
|
||||
// Determine receipt type based on content
|
||||
bool isFinalReceipt = receiptText.Contains("http") ||
|
||||
receiptText.Contains("+41") ||
|
||||
receiptText.Contains("Summe CHF") ||
|
||||
receiptText.Contains("MWST") ||
|
||||
receiptText.Contains("Thank you");
|
||||
|
||||
bool isKitchenReceipt = receiptText.Contains("TISCH:");
|
||||
|
||||
if (isKitchenReceipt && !isFinalReceipt)
|
||||
{
|
||||
return ParseKitchenReceipt(receiptText);
|
||||
}
|
||||
else if (isFinalReceipt)
|
||||
{
|
||||
return ParseFinalReceipt(receiptText);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default to kitchen receipt if unclear
|
||||
return ParseKitchenReceipt(receiptText);
|
||||
}
|
||||
}
|
||||
|
||||
public static FinalReceipt ParseFinalReceipt(string receiptText)
|
||||
{
|
||||
var lines = receiptText.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(l => l.Trim())
|
||||
.ToList();
|
||||
|
||||
// Extract header information
|
||||
string location = lines[0];
|
||||
string phone = lines[1];
|
||||
string url = lines[2];
|
||||
|
||||
// Find and extract date
|
||||
var dateLine = lines.FirstOrDefault(l => l.StartsWith("Datum:"));
|
||||
string date = dateLine?.Replace("Datum:", "").Trim() ?? "";
|
||||
|
||||
// Find total line
|
||||
var totalLine = lines.FirstOrDefault(l => l.Contains("Summe CHF :"));
|
||||
string total = totalLine?.Split(':').Last().Trim() ?? "";
|
||||
|
||||
// Find MWST line (comes after "TOTAL MWST" header)
|
||||
var mwstLineIndex = lines.FindIndex(l => l.StartsWith("TOTAL") && l.Contains("MWST"));
|
||||
string mwst = mwstLineIndex >= 0 && mwstLineIndex + 1 < lines.Count
|
||||
? lines[mwstLineIndex + 1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? ""
|
||||
: "";
|
||||
|
||||
// Extract comment (line after MWST values)
|
||||
string comment = mwstLineIndex >= 0 && mwstLineIndex + 2 < lines.Count
|
||||
? lines[mwstLineIndex + 2]
|
||||
: "";
|
||||
|
||||
// Extract thank you message (last non-separator line)
|
||||
string thanks = lines.LastOrDefault(l => !l.Contains("--")) ?? "";
|
||||
|
||||
// Parse items
|
||||
var items = new List<FinalReceiptItem>();
|
||||
var startIndex = lines.FindIndex(l => l.StartsWith("Datum:")) + 2; // Skip date and separator
|
||||
var endIndex = lines.FindIndex(l => l.Contains("Summe CHF"));
|
||||
|
||||
for (int i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
var line = lines[i];
|
||||
|
||||
// Skip separator lines and empty lines
|
||||
if (line.Contains("---") || string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
// Check if line contains item data (has * separator)
|
||||
if (line.Contains("*"))
|
||||
{
|
||||
// Parse format: "Name Amount * Price Total"
|
||||
var parts = line.Split('*');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
var leftPart = parts[0].Trim();
|
||||
var rightPart = parts[1].Trim();
|
||||
|
||||
// Extract name and amount from left part
|
||||
var leftTokens = leftPart.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var amount = int.Parse(leftTokens.Last());
|
||||
var name = string.Join(" ", leftTokens.Take(leftTokens.Length - 1));
|
||||
|
||||
// Extract price and total from right part
|
||||
var rightTokens = rightPart.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var price = rightTokens.Length > 0 ? rightTokens[0] : "";
|
||||
var itemTotal = rightTokens.Length > 1 ? rightTokens[1] : "";
|
||||
|
||||
items.Add(new FinalReceiptItem(name, amount, price, itemTotal));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new FinalReceipt(location, phone, url, date, items, total, mwst, comment, thanks);
|
||||
}
|
||||
|
||||
public static KitchenReceipt ParseKitchenReceipt(string receiptText)
|
||||
{
|
||||
var lines = receiptText.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(l => l.Trim())
|
||||
.ToList();
|
||||
|
||||
int currentIndex = 0;
|
||||
|
||||
// Skip optional "*** KOPIE ***" header
|
||||
if (lines[currentIndex].Contains("***"))
|
||||
{
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
// Extract location (can be any phrase)
|
||||
string location = lines[currentIndex];
|
||||
currentIndex++;
|
||||
|
||||
// Skip separator line
|
||||
while (currentIndex < lines.Count && lines[currentIndex].Contains("---"))
|
||||
{
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
// Extract date line
|
||||
string date = lines[currentIndex];
|
||||
currentIndex++;
|
||||
|
||||
// Extract owner
|
||||
string owner = "";
|
||||
do
|
||||
{
|
||||
if (owner != "")
|
||||
{
|
||||
owner += "/n";
|
||||
}
|
||||
owner += lines[currentIndex];
|
||||
currentIndex++;
|
||||
} while (!lines[currentIndex].ToLower().Contains("tisch:"));
|
||||
|
||||
|
||||
// Extract table (extract text after "TISCH:")
|
||||
string tisch = lines[currentIndex].Replace("TISCH:", "").Trim();
|
||||
currentIndex++;
|
||||
|
||||
// Skip empty lines and separators until we reach items
|
||||
while (currentIndex < lines.Count &&
|
||||
(string.IsNullOrWhiteSpace(lines[currentIndex]) || lines[currentIndex].Contains("-")))
|
||||
{
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
// Parse items
|
||||
var items = new List<KitchenItem>();
|
||||
while (currentIndex < lines.Count)
|
||||
{
|
||||
var line = lines[currentIndex];
|
||||
|
||||
// Stop at separator or end
|
||||
if (line.Contains("---") || string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (line.ToLower().Contains(". gang"))
|
||||
{
|
||||
items.Add(new KitchenGang(line.Trim()));
|
||||
currentIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse format: "1x Espresso" or " 1x Espresso"
|
||||
var trimmedLine = line.Trim();
|
||||
if (trimmedLine.Contains("x"))
|
||||
{
|
||||
var parts = trimmedLine.Split('x', 2);
|
||||
if (parts.Length == 2 && int.TryParse(parts[0].Trim(), out int amount))
|
||||
{
|
||||
var itemName = parts[1].Trim();
|
||||
items.Add(new KitchenProduct(amount, itemName));
|
||||
}
|
||||
}
|
||||
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
return new KitchenReceipt(location, date, owner, tisch, items);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Inspectron.Epson.PrintServer.WorkAreaSources;
|
||||
|
||||
public interface IWorkAreasSource
|
||||
{
|
||||
public Task<List<WorkArea>> GetWorkAreasAsync();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Net.Http.Json;
|
||||
using Inspectron.Epson.PrintServer.ConfigurationSources;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.WorkAreaSources;
|
||||
|
||||
public class JamesWorkAreaSource: IWorkAreasSource
|
||||
{
|
||||
private readonly EpsonPrintServiceConfiguration _configuration;
|
||||
|
||||
public JamesWorkAreaSource(EpsonPrintServiceConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task<List<WorkArea>> GetWorkAreasAsync()
|
||||
{
|
||||
var url = $@"https://api.stage.gastrojames.ch/api/Restaurant/{_configuration.RestaurantId}/work-areas";
|
||||
|
||||
HttpClient client = new();
|
||||
var response = await client.GetFromJsonAsync<List<WorkArea>>(url);
|
||||
response.Add(new WorkArea()
|
||||
{
|
||||
Id = "print-receipt",
|
||||
Name = "Receipt"
|
||||
});
|
||||
return response!;
|
||||
}
|
||||
}
|
||||
7
Inspectron.Epson/PrintServer/WorkAreaSources/WorkArea.cs
Normal file
7
Inspectron.Epson/PrintServer/WorkAreaSources/WorkArea.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Inspectron.Epson.PrintServer.WorkAreaSources;
|
||||
|
||||
public class WorkArea
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
6
Inspectron.Epson/Queue/IPrintService.cs
Normal file
6
Inspectron.Epson/Queue/IPrintService.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public interface IPrintService
|
||||
{
|
||||
Task<bool> PrintAsync(string printerIp, PrintJob job);
|
||||
}
|
||||
14
Inspectron.Epson/Queue/PrintJob.cs
Normal file
14
Inspectron.Epson/Queue/PrintJob.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
public class PrintJob
|
||||
{
|
||||
public string AreaId { get; set; }
|
||||
public string Document { get; set; }
|
||||
public DateTime QueuedAt { get; set; }
|
||||
public int RetryCount { get; set; }
|
||||
|
||||
public PrintJob()
|
||||
{
|
||||
AreaId = Guid.NewGuid().ToString();
|
||||
QueuedAt = DateTime.UtcNow;
|
||||
RetryCount = 0;
|
||||
}
|
||||
}
|
||||
66
Inspectron.Epson/Queue/PrintServer.cs
Normal file
66
Inspectron.Epson/Queue/PrintServer.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
public class PrintServer
|
||||
{
|
||||
private readonly IPrintService _printService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Dictionary<string, PrinterQueue> _printerQueues;
|
||||
|
||||
public PrintServer(IPrintService printService, ILogger logger)
|
||||
{
|
||||
_printService = printService;
|
||||
_logger = logger;
|
||||
_printerQueues = new Dictionary<string, PrinterQueue>();
|
||||
|
||||
}
|
||||
|
||||
public void RegisterPrinter(string printerIp)
|
||||
{
|
||||
if (_printerQueues.ContainsKey(printerIp))
|
||||
return;
|
||||
|
||||
var queue = new PrinterQueue(printerIp, _printService,_logger);
|
||||
_printerQueues[printerIp] = queue;
|
||||
queue.Start();
|
||||
Console.WriteLine($"Printer {printerIp} registered");
|
||||
}
|
||||
|
||||
public void UnregisterPrinter(string printerIp)
|
||||
{
|
||||
if (_printerQueues.TryGetValue(printerIp, out var queue))
|
||||
{
|
||||
queue.StopAsync().Wait();
|
||||
_printerQueues.Remove(printerIp);
|
||||
Console.WriteLine($"Printer {printerIp} unregistered");
|
||||
}
|
||||
}
|
||||
|
||||
public void SubmitJob(string printerIp, PrintJob job)
|
||||
{
|
||||
if (_printerQueues.TryGetValue(printerIp, out var queue))
|
||||
{
|
||||
queue.Enqueue(job);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning( $"Printer {printerIp} not found. Job cannot be submitted.");
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, int> GetQueueStatus()
|
||||
{
|
||||
var status = new Dictionary<string, int>();
|
||||
foreach (var kvp in _printerQueues)
|
||||
{
|
||||
status[kvp.Key] = kvp.Value.QueueLength;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
public async Task ShutdownAsync()
|
||||
{
|
||||
var stopTasks = _printerQueues.Values.Select(q => q.StopAsync());
|
||||
await Task.WhenAll(stopTasks);
|
||||
Console.WriteLine("Print server shut down");
|
||||
}
|
||||
}
|
||||
124
Inspectron.Epson/Queue/PrinterQueue.cs
Normal file
124
Inspectron.Epson/Queue/PrinterQueue.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
public class PrinterQueue
|
||||
{
|
||||
public string PrinterIp { get; }
|
||||
private readonly ConcurrentQueue<PrintJob> _queue;
|
||||
private readonly ConcurrentStack<PrintJob> _priorityQueue; // For failed jobs
|
||||
private readonly SemaphoreSlim _signal;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
private Task _processingTask;
|
||||
private readonly IPrintService _printService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public bool IsProcessing { get; private set; }
|
||||
public int QueueLength => _queue.Count + _priorityQueue.Count;
|
||||
|
||||
public PrinterQueue(string printerIp, IPrintService printService, ILogger logger)
|
||||
{
|
||||
PrinterIp = printerIp;
|
||||
_queue = new ConcurrentQueue<PrintJob>();
|
||||
_priorityQueue = new ConcurrentStack<PrintJob>();
|
||||
_signal = new SemaphoreSlim(0);
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_printService = printService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Enqueue(PrintJob job)
|
||||
{
|
||||
_queue.Enqueue(job);
|
||||
_signal.Release(); // Signal that there's work to do
|
||||
_logger.LogInformation("Job {JobId} queued for printer {PrinterId}. Queue length: {QueueLength}", job.AreaId, PrinterIp, QueueLength);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_processingTask != null)
|
||||
return;
|
||||
|
||||
IsProcessing = true;
|
||||
_processingTask = Task.Run(() => ProcessQueueAsync(_cancellationTokenSource.Token));
|
||||
_logger.LogInformation("Printer queue {PrinterId} started", PrinterIp);
|
||||
}
|
||||
private void EnqueuePriority(PrintJob job)
|
||||
{
|
||||
_priorityQueue.Push(job);
|
||||
_signal.Release();
|
||||
_logger.LogInformation("Job {JobId} priority queued for printer {PrinterId}. Queue length: {QueueLength}", job.AreaId, PrinterIp, QueueLength);
|
||||
}
|
||||
private async Task ProcessQueueAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Wait for signal that there's work or cancellation
|
||||
await _signal.WaitAsync(cancellationToken);
|
||||
PrintJob job = null;
|
||||
if (!_priorityQueue.TryPop(out job))
|
||||
{
|
||||
_queue.TryDequeue(out job);
|
||||
}
|
||||
|
||||
if (job!=null)
|
||||
{
|
||||
_logger.LogInformation("Processing job {JobId} on printer {PrinterId}", job.AreaId, PrinterIp);
|
||||
|
||||
try
|
||||
{
|
||||
// Call the actual print function
|
||||
bool success = await _printService.PrintAsync(PrinterIp, job);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
// Re-queue with retry logic
|
||||
job.RetryCount++;
|
||||
_logger.LogWarning("Job {JobId} failed, retrying ({RetryCount}/3)", job.AreaId, job.RetryCount);
|
||||
await Task.Delay(5000, cancellationToken); // Wait before retry
|
||||
EnqueuePriority(job);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Job {JobId} completed successfully on printer {PrinterId}", job.AreaId, PrinterIp);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing job {JobId}", job.AreaId);
|
||||
// Handle exception (retry, log, etc.)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
IsProcessing = false;
|
||||
_logger.LogInformation("Printer queue {PrinterId} stopped", PrinterIp);
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
_signal.Release(); // Release to unblock the wait
|
||||
|
||||
if (_processingTask != null)
|
||||
{
|
||||
await _processingTask;
|
||||
}
|
||||
}
|
||||
|
||||
public List<PrintJob> GetPendingJobs()
|
||||
{
|
||||
return new List<PrintJob>(_queue);
|
||||
}
|
||||
}
|
||||
|
||||
461
Inspectron.Epson/README.md
Normal file
461
Inspectron.Epson/README.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# Inspectron.Epson - Epson TM-m30III Printer SDK
|
||||
|
||||
A comprehensive C# SDK for the Epson TM-m30III thermal receipt printer, providing easy-to-use APIs for printing, status checking, and diagnostics.
|
||||
|
||||
## Features
|
||||
|
||||
- **Async/Await Pattern** - Modern asynchronous API throughout
|
||||
- **Connection Management** - Reliable TCP/IP connection handling
|
||||
- **Status Monitoring** - Comprehensive printer status queries
|
||||
- **Error Detection** - Detailed error reporting and diagnostics
|
||||
- **Printing Operations** - Simple text printing with ESC/POS commands
|
||||
- **Logging Support** - Optional ILogger integration for diagnostics
|
||||
- **Type-Safe** - Strong typing with C# records for status data
|
||||
|
||||
## Installation
|
||||
|
||||
Add the project reference to your solution:
|
||||
|
||||
```bash
|
||||
dotnet add reference path/to/Inspectron.Epson/Inspectron.Epson.csproj
|
||||
```
|
||||
|
||||
Or include the compiled DLL in your project.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- .NET 8.0 or later
|
||||
- Microsoft.Extensions.Logging.Abstractions 8.0.0
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Printing
|
||||
|
||||
```csharp
|
||||
using Inspectron.Epson;
|
||||
|
||||
// Create printer instance
|
||||
await using var printer = new EpsonPrinter();
|
||||
|
||||
// Connect to printer
|
||||
await printer.ConnectAsync("192.168.1.100");
|
||||
|
||||
// Print text with paper cut
|
||||
await printer.PrintTextAndCutAsync("Hello World!\nThis is a test receipt.");
|
||||
```
|
||||
|
||||
### With Logging
|
||||
|
||||
```csharp
|
||||
using Inspectron.Epson;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
// Create logger
|
||||
var loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.AddConsole();
|
||||
builder.SetMinimumLevel(LogLevel.Information);
|
||||
});
|
||||
|
||||
var logger = loggerFactory.CreateLogger<EpsonPrinter>();
|
||||
|
||||
// Create printer with logging
|
||||
await using var printer = new EpsonPrinter(logger);
|
||||
await printer.ConnectAsync("192.168.1.100");
|
||||
await printer.PrintTextAndCutAsync("Hello World!");
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Checking Printer Status
|
||||
|
||||
```csharp
|
||||
await using var printer = new EpsonPrinter();
|
||||
await printer.ConnectAsync("192.168.1.100");
|
||||
|
||||
// Get overall status with health assessment
|
||||
var status = await printer.GetOverallStatusAsync();
|
||||
|
||||
Console.WriteLine($"Status: {status.StatusText}");
|
||||
Console.WriteLine($"Ready: {status.IsReady}");
|
||||
Console.WriteLine($"Model: {status.PrinterModel}");
|
||||
|
||||
if (!status.IsReady)
|
||||
{
|
||||
Console.WriteLine("Recommendations:");
|
||||
foreach (var recommendation in status.Recommendations)
|
||||
{
|
||||
Console.WriteLine($" - {recommendation}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Individual Status Queries
|
||||
|
||||
```csharp
|
||||
await using var printer = new EpsonPrinter();
|
||||
await printer.ConnectAsync("192.168.1.100");
|
||||
|
||||
// Get specific status information
|
||||
var printerStatus = await printer.GetPrinterStatusAsync();
|
||||
Console.WriteLine($"Online: {printerStatus.IsOnline}");
|
||||
Console.WriteLine($"Cover Closed: {printerStatus.IsCoverClosed}");
|
||||
|
||||
var paperStatus = await printer.GetPaperSensorStatusAsync();
|
||||
Console.WriteLine($"Paper Present: {paperStatus.PaperPresent}");
|
||||
Console.WriteLine($"Paper Near End: {paperStatus.PaperNearEnd}");
|
||||
|
||||
var errorStatus = await printer.GetErrorStatusAsync();
|
||||
Console.WriteLine($"Has Error: {errorStatus.HasError}");
|
||||
Console.WriteLine($"Recoverable Error: {errorStatus.RecoverableError}");
|
||||
```
|
||||
|
||||
### Running Diagnostics
|
||||
|
||||
```csharp
|
||||
using Inspectron.Epson;
|
||||
|
||||
var diagnostics = new EpsonDiagnostics();
|
||||
var report = await diagnostics.RunFullDiagnosticsAsync("192.168.1.100");
|
||||
|
||||
Console.WriteLine(report);
|
||||
```
|
||||
|
||||
Sample diagnostic output:
|
||||
|
||||
```
|
||||
======================================================================
|
||||
EPSON TM-m30III PRINTER DIAGNOSTICS
|
||||
======================================================================
|
||||
Printer IP: 192.168.1.100:9100
|
||||
Timestamp: 2024-01-15 14:30:45
|
||||
======================================================================
|
||||
|
||||
⏳ Connecting to printer...
|
||||
✅ Connection established
|
||||
|
||||
⏳ Retrieving printer information...
|
||||
⏳ Querying printer status...
|
||||
|
||||
======================================================================
|
||||
OVERALL STATUS
|
||||
======================================================================
|
||||
|
||||
🟢 READY - Printer is operational
|
||||
|
||||
🖨️ Printer Model TM-m30III
|
||||
|
||||
======================================================================
|
||||
GENERAL PRINTER STATUS
|
||||
======================================================================
|
||||
📊 Raw Status Byte 0x12
|
||||
📊 Binary 00010010
|
||||
|
||||
✅ Printer Online YES
|
||||
✅ Cover Closed YES
|
||||
❌ Paper Feed Button Pressed NO
|
||||
❌ Drawer Open Signal NO
|
||||
```
|
||||
|
||||
### Advanced Printing
|
||||
|
||||
```csharp
|
||||
await using var printer = new EpsonPrinter();
|
||||
await printer.ConnectAsync("192.168.1.100");
|
||||
|
||||
// Print without cutting
|
||||
await printer.PrintTextAsync("Line 1\n");
|
||||
await printer.PrintTextAsync("Line 2\n");
|
||||
await printer.PrintTextAsync("Line 3\n");
|
||||
|
||||
// Feed and cut
|
||||
await printer.SendRawCommandAsync(EpsonCommands.FeedLines(5));
|
||||
await printer.SendRawCommandAsync(EpsonCommands.CutPaperFull);
|
||||
```
|
||||
|
||||
### Sending Raw ESC/POS Commands
|
||||
|
||||
```csharp
|
||||
await using var printer = new EpsonPrinter();
|
||||
await printer.ConnectAsync("192.168.1.100");
|
||||
|
||||
// Use predefined commands
|
||||
await printer.SendRawCommandAsync(EpsonCommands.Initialize);
|
||||
await printer.PrintTextAsync("Initialized printer\n");
|
||||
|
||||
// Or create custom commands
|
||||
byte[] customCommand = { 0x1B, 0x40 }; // ESC @ (Initialize)
|
||||
await printer.SendRawCommandAsync(customCommand);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### EpsonPrinter Class
|
||||
|
||||
Main class for printer operations.
|
||||
|
||||
#### Constructor
|
||||
|
||||
```csharp
|
||||
public EpsonPrinter(ILogger<EpsonPrinter>? logger = null)
|
||||
```
|
||||
|
||||
Creates a new printer instance with optional logging support.
|
||||
|
||||
#### Connection Methods
|
||||
|
||||
```csharp
|
||||
Task ConnectAsync(string ip, int port = 9100, int timeoutSeconds = 5)
|
||||
```
|
||||
|
||||
Connect to the printer at the specified IP address and port.
|
||||
|
||||
```csharp
|
||||
void Disconnect()
|
||||
```
|
||||
|
||||
Disconnect from the printer.
|
||||
|
||||
```csharp
|
||||
bool IsConnected { get; }
|
||||
```
|
||||
|
||||
Returns true if currently connected to the printer.
|
||||
|
||||
#### Status Query Methods
|
||||
|
||||
```csharp
|
||||
Task<PrinterStatus> GetPrinterStatusAsync()
|
||||
```
|
||||
|
||||
Get general printer status (online, cover, drawer, etc.).
|
||||
|
||||
```csharp
|
||||
Task<OfflineStatus> GetOfflineStatusAsync()
|
||||
```
|
||||
|
||||
Get offline status (reasons why printer is offline).
|
||||
|
||||
```csharp
|
||||
Task<ErrorStatus> GetErrorStatusAsync()
|
||||
```
|
||||
|
||||
Get error status (recoverable, unrecoverable, cutter errors).
|
||||
|
||||
```csharp
|
||||
Task<PaperSensorStatus> GetPaperSensorStatusAsync()
|
||||
```
|
||||
|
||||
Get paper sensor status (paper present, near end).
|
||||
|
||||
```csharp
|
||||
Task<string?> GetPrinterIdAsync()
|
||||
```
|
||||
|
||||
Get printer model/ID string.
|
||||
|
||||
```csharp
|
||||
Task<OverallStatus> GetOverallStatusAsync()
|
||||
```
|
||||
|
||||
Get comprehensive status with health assessment and recommendations.
|
||||
|
||||
#### Printing Methods
|
||||
|
||||
```csharp
|
||||
Task PrintTextAsync(string text)
|
||||
```
|
||||
|
||||
Print text to the printer.
|
||||
|
||||
```csharp
|
||||
Task PrintTextAndCutAsync(string text, int feedLines = 5)
|
||||
```
|
||||
|
||||
Print text, feed paper, and cut.
|
||||
|
||||
```csharp
|
||||
Task SendRawCommandAsync(byte[] command)
|
||||
```
|
||||
|
||||
Send raw ESC/POS command bytes.
|
||||
|
||||
### EpsonDiagnostics Class
|
||||
|
||||
Comprehensive diagnostics tool.
|
||||
|
||||
```csharp
|
||||
public EpsonDiagnostics(ILogger<EpsonDiagnostics>? logger = null)
|
||||
```
|
||||
|
||||
```csharp
|
||||
Task<string> RunFullDiagnosticsAsync(string ip, int port = 9100, int timeoutSeconds = 5)
|
||||
```
|
||||
|
||||
Run full diagnostic check and return formatted report.
|
||||
|
||||
### Status Data Models
|
||||
|
||||
All status classes are immutable records with these common properties:
|
||||
|
||||
- `byte RawByte` - Raw status byte from printer
|
||||
- `string Binary` - Binary representation (8 bits)
|
||||
- `string Hex` - Hexadecimal representation
|
||||
|
||||
#### PrinterStatus
|
||||
|
||||
- `bool DrawerOpen` - Drawer kick-out connector pin 3 is HIGH
|
||||
- `bool IsOnline` - Printer is online
|
||||
- `bool IsCoverClosed` - Cover is closed
|
||||
- `bool PaperFeedButtonPressed` - Paper feed button is pressed
|
||||
|
||||
#### OfflineStatus
|
||||
|
||||
- `bool CoverOpen` - Cover is open (offline reason)
|
||||
- `bool PaperFeedButton` - Paper feed button active (offline reason)
|
||||
- `bool PaperEnd` - Paper end detected (offline reason)
|
||||
- `bool ErrorOccurred` - Error occurred (offline reason)
|
||||
|
||||
#### ErrorStatus
|
||||
|
||||
- `bool RecoverableError` - Recoverable error occurred
|
||||
- `bool AutoCutterError` - Auto-cutter error occurred
|
||||
- `bool UnrecoverableError` - Unrecoverable error occurred
|
||||
- `bool AutoRecoveryError` - Auto-recovery error occurred
|
||||
- `bool HasError` - True if any error is present
|
||||
|
||||
#### PaperSensorStatus
|
||||
|
||||
- `bool PaperNearEnd` - Paper roll is near end
|
||||
- `bool PaperPresent` - Paper is present
|
||||
|
||||
#### OverallStatus
|
||||
|
||||
- `string? PrinterModel` - Printer model/ID
|
||||
- `PrinterStatus? PrinterStatus` - General printer status
|
||||
- `OfflineStatus? OfflineStatus` - Offline status
|
||||
- `ErrorStatus? ErrorStatus` - Error status
|
||||
- `PaperSensorStatus? PaperStatus` - Paper sensor status
|
||||
- `string StatusText` - Overall status description
|
||||
- `bool IsReady` - True if printer is ready
|
||||
- `List<string> Recommendations` - List of recommendations
|
||||
- `string StatusIcon` - Status emoji (🟢/🟡/🟠/🔴/❓)
|
||||
|
||||
### EpsonCommands Class
|
||||
|
||||
Static class containing ESC/POS command constants:
|
||||
|
||||
- `byte[] Initialize` - Initialize printer (ESC @)
|
||||
- `byte[] GetPrinterStatus` - DLE EOT 1
|
||||
- `byte[] GetOfflineStatus` - DLE EOT 2
|
||||
- `byte[] GetErrorStatus` - DLE EOT 3
|
||||
- `byte[] GetPaperSensorStatus` - DLE EOT 4
|
||||
- `byte[] GetPrinterId` - GS I 1
|
||||
- `byte[] CutPaperFull` - Full paper cut (GS V 0)
|
||||
- `byte[] CutPaperPartial` - Partial paper cut (GS V 1)
|
||||
- `byte[] FeedLines(byte lines)` - Feed n lines (ESC d n)
|
||||
- `byte[] LineFeed` - Line feed (LF)
|
||||
|
||||
### Exception Types
|
||||
|
||||
#### EpsonPrinterException
|
||||
|
||||
Base exception for all Epson printer errors.
|
||||
|
||||
#### EpsonConnectionException
|
||||
|
||||
Thrown when connection fails or is lost.
|
||||
|
||||
Properties:
|
||||
- `string? PrinterIp` - Printer IP address
|
||||
- `int? PrinterPort` - Printer port
|
||||
|
||||
#### EpsonCommandException
|
||||
|
||||
Thrown when a command fails.
|
||||
|
||||
Properties:
|
||||
- `byte[]? Command` - The command that failed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
|
||||
**Problem:** Cannot connect to printer
|
||||
|
||||
**Solutions:**
|
||||
- Verify printer IP address is correct
|
||||
- Ensure printer is powered on
|
||||
- Check network connectivity (ping the printer)
|
||||
- Verify printer is on the same network
|
||||
- Check firewall settings
|
||||
- Ensure port 9100 is accessible
|
||||
|
||||
### Timeout Errors
|
||||
|
||||
**Problem:** Operations timeout
|
||||
|
||||
**Solutions:**
|
||||
- Increase timeout parameter in ConnectAsync
|
||||
- Check printer is not busy with another job
|
||||
- Verify printer is not in an error state
|
||||
- Check network latency
|
||||
|
||||
### Printer Offline
|
||||
|
||||
**Problem:** Printer shows as offline
|
||||
|
||||
**Solutions:**
|
||||
- Check cover is closed
|
||||
- Ensure paper is loaded
|
||||
- Clear any error conditions
|
||||
- Check offline status for specific reasons
|
||||
|
||||
### Paper Issues
|
||||
|
||||
**Problem:** Paper not detected or near end
|
||||
|
||||
**Solutions:**
|
||||
- Load paper roll correctly
|
||||
- Ensure paper is feeding through sensor
|
||||
- Replace paper if near end
|
||||
- Check paper roll is compatible
|
||||
|
||||
### Auto-Cutter Errors
|
||||
|
||||
**Problem:** Cutter error reported
|
||||
|
||||
**Solutions:**
|
||||
- Check for paper jams in cutter
|
||||
- Remove any obstructions
|
||||
- Open and close cover to reset
|
||||
- May require service if persistent
|
||||
|
||||
## Technical Details
|
||||
|
||||
### ESC/POS Protocol
|
||||
|
||||
This SDK uses ESC/POS commands via TCP/IP on port 9100. The DLE EOT (0x10 0x04) real-time status commands are used for status queries.
|
||||
|
||||
### Status Byte Format
|
||||
|
||||
All status responses follow the format: `0xx1xx10b` where:
|
||||
- Bit 7: Always 0
|
||||
- Bits 6,5,3,2: Status-specific flags
|
||||
- Bit 4: Always 1
|
||||
- Bits 1,0: Fixed pattern (10b)
|
||||
|
||||
### Connection
|
||||
|
||||
- Protocol: TCP/IP
|
||||
- Default Port: 9100
|
||||
- Character Encoding: UTF-8 for text, ASCII for responses
|
||||
- Timeout: Configurable (default 5 seconds)
|
||||
|
||||
## License
|
||||
|
||||
Copyright Inspectron. All rights reserved.
|
||||
|
||||
## Support
|
||||
|
||||
For issues, questions, or contributions, please contact the development team.
|
||||
51
Inspectron.Epson/SOAP/Enums.cs
Normal file
51
Inspectron.Epson/SOAP/Enums.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace Inspectron.Epson.SOAP
|
||||
{
|
||||
public enum Font
|
||||
{
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
E
|
||||
}
|
||||
|
||||
public enum ErrorCorrection
|
||||
{
|
||||
LEVEL_1,
|
||||
LEVEL_2,
|
||||
LEVEL_3,
|
||||
LEVEL_4,
|
||||
LEVEL_5,
|
||||
LEVEL_6,
|
||||
LEVEL_7,
|
||||
LEVEL_8,
|
||||
LEVEL_L,
|
||||
LEVEL_M,
|
||||
LEVEL_Q,
|
||||
LEVEL_H,
|
||||
LEVEL_DEFAULT
|
||||
}
|
||||
|
||||
public enum Symbol
|
||||
{
|
||||
PDF417_STANDARD,
|
||||
PDF417_TRUNCATED,
|
||||
QRCODE_MODEL_1,
|
||||
QRCODE_MODEL_2,
|
||||
QRCODE_MICRO,
|
||||
MAXICODE_MODE_2,
|
||||
MAXICODE_MODE_3,
|
||||
MAXICODE_MODE_4,
|
||||
MAXICODE_MODE_5,
|
||||
MAXICODE_MODE_6,
|
||||
GS1_DATABAR_STACKED,
|
||||
GS1_DATABAR_STACKED_OMNIDIRECTIONAL,
|
||||
GS1_DATABAR_EXPANDED_STACKED,
|
||||
AZTECCODE_FULLRANGE,
|
||||
AZTECCODE_COMPACT,
|
||||
DATAMATRIX_SQUARE,
|
||||
DATAMATRIX_RECTANGLE_8,
|
||||
DATAMATRIX_RECTANGLE_12,
|
||||
DATAMATRIX_RECTANGLE_16
|
||||
}
|
||||
}
|
||||
503
Inspectron.Epson/SOAP/SoapEpsonPrint.cs
Normal file
503
Inspectron.Epson/SOAP/SoapEpsonPrint.cs
Normal file
@@ -0,0 +1,503 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Inspectron.Epson.SOAP
|
||||
{
|
||||
public class SoapEpsonPrint
|
||||
{
|
||||
private StringBuilder message = new StringBuilder();
|
||||
private int halftone = 0;
|
||||
private double brightness = 1.0;
|
||||
private bool force = false;
|
||||
|
||||
// Font constants
|
||||
public const string FONT_A = "font_a";
|
||||
public const string FONT_B = "font_b";
|
||||
public const string FONT_C = "font_c";
|
||||
public const string FONT_D = "font_d";
|
||||
public const string FONT_E = "font_e";
|
||||
public const string FONT_SPECIAL_A = "special_a";
|
||||
public const string FONT_SPECIAL_B = "special_b";
|
||||
|
||||
// Alignment constants
|
||||
public const string ALIGN_LEFT = "left";
|
||||
public const string ALIGN_CENTER = "center";
|
||||
public const string ALIGN_RIGHT = "right";
|
||||
|
||||
// Color constants
|
||||
public const string COLOR_NONE = "none";
|
||||
public const string COLOR_1 = "color_1";
|
||||
public const string COLOR_2 = "color_2";
|
||||
public const string COLOR_3 = "color_3";
|
||||
public const string COLOR_4 = "color_4";
|
||||
|
||||
// Feed constants
|
||||
public const string FEED_PEELING = "peeling";
|
||||
public const string FEED_CUTTING = "cutting";
|
||||
public const string FEED_CURRENT_TOF = "current_tof";
|
||||
public const string FEED_NEXT_TOF = "next_tof";
|
||||
|
||||
// Mode constants
|
||||
public const string MODE_MONO = "mono";
|
||||
public const string MODE_GRAY16 = "gray16";
|
||||
|
||||
// Barcode constants
|
||||
public const string BARCODE_UPC_A = "upc_a";
|
||||
public const string BARCODE_UPC_E = "upc_e";
|
||||
public const string BARCODE_EAN13 = "ean13";
|
||||
public const string BARCODE_JAN13 = "jan13";
|
||||
public const string BARCODE_EAN8 = "ean8";
|
||||
public const string BARCODE_JAN8 = "jan8";
|
||||
public const string BARCODE_CODE39 = "code39";
|
||||
public const string BARCODE_ITF = "itf";
|
||||
public const string BARCODE_CODABAR = "codabar";
|
||||
public const string BARCODE_CODE93 = "code93";
|
||||
public const string BARCODE_CODE128 = "code128";
|
||||
public const string BARCODE_GS1_128 = "gs1_128";
|
||||
public const string BARCODE_GS1_DATABAR_OMNIDIRECTIONAL = "gs1_databar_omnidirectional";
|
||||
public const string BARCODE_GS1_DATABAR_TRUNCATED = "gs1_databar_truncated";
|
||||
public const string BARCODE_GS1_DATABAR_LIMITED = "gs1_databar_limited";
|
||||
public const string BARCODE_GS1_DATABAR_EXPANDED = "gs1_databar_expanded";
|
||||
public const string BARCODE_CODE128_AUTO = "code128_auto";
|
||||
|
||||
// HRI constants
|
||||
public const string HRI_NONE = "none";
|
||||
public const string HRI_ABOVE = "above";
|
||||
public const string HRI_BELOW = "below";
|
||||
public const string HRI_BOTH = "both";
|
||||
|
||||
// Level constants
|
||||
public const string LEVEL_0 = "level_0";
|
||||
public const string LEVEL_1 = "level_1";
|
||||
public const string LEVEL_2 = "level_2";
|
||||
public const string LEVEL_3 = "level_3";
|
||||
public const string LEVEL_4 = "level_4";
|
||||
public const string LEVEL_5 = "level_5";
|
||||
public const string LEVEL_6 = "level_6";
|
||||
public const string LEVEL_7 = "level_7";
|
||||
public const string LEVEL_8 = "level_8";
|
||||
public const string LEVEL_L = "level_l";
|
||||
public const string LEVEL_M = "level_m";
|
||||
public const string LEVEL_Q = "level_q";
|
||||
public const string LEVEL_H = "level_h";
|
||||
public const string LEVEL_DEFAULT = "default";
|
||||
|
||||
// Line constants
|
||||
public const string LINE_THIN = "thin";
|
||||
public const string LINE_MEDIUM = "medium";
|
||||
public const string LINE_THICK = "thick";
|
||||
public const string LINE_THIN_DOUBLE = "thin_double";
|
||||
public const string LINE_MEDIUM_DOUBLE = "medium_double";
|
||||
public const string LINE_THICK_DOUBLE = "thick_double";
|
||||
|
||||
// Direction constants
|
||||
public const string DIRECTION_LEFT_TO_RIGHT = "left_to_right";
|
||||
public const string DIRECTION_BOTTOM_TO_TOP = "bottom_to_top";
|
||||
public const string DIRECTION_RIGHT_TO_LEFT = "right_to_left";
|
||||
public const string DIRECTION_TOP_TO_BOTTOM = "top_to_bottom";
|
||||
|
||||
// Cut constants
|
||||
public const string CUT_NO_FEED = "no_feed";
|
||||
public const string CUT_FEED = "feed";
|
||||
public const string CUT_RESERVE = "reserve";
|
||||
public const string FULL_CUT_NO_FEED = "no_feed_fullcut";
|
||||
public const string FULL_CUT_FEED = "feed_fullcut";
|
||||
public const string FULL_CUT_RESERVE = "reserve_fullcut";
|
||||
|
||||
// Drawer constants
|
||||
public const string DRAWER_1 = "drawer_1";
|
||||
public const string DRAWER_2 = "drawer_2";
|
||||
|
||||
// Pulse constants
|
||||
public const string PULSE_100 = "pulse_100";
|
||||
public const string PULSE_200 = "pulse_200";
|
||||
public const string PULSE_300 = "pulse_300";
|
||||
public const string PULSE_400 = "pulse_400";
|
||||
public const string PULSE_500 = "pulse_500";
|
||||
|
||||
// Pattern constants
|
||||
public const string PATTERN_NONE = "none";
|
||||
public const string PATTERN_0 = "pattern_0";
|
||||
public const string PATTERN_1 = "pattern_1";
|
||||
public const string PATTERN_2 = "pattern_2";
|
||||
public const string PATTERN_3 = "pattern_3";
|
||||
public const string PATTERN_4 = "pattern_4";
|
||||
public const string PATTERN_5 = "pattern_5";
|
||||
public const string PATTERN_6 = "pattern_6";
|
||||
public const string PATTERN_7 = "pattern_7";
|
||||
public const string PATTERN_8 = "pattern_8";
|
||||
public const string PATTERN_9 = "pattern_9";
|
||||
public const string PATTERN_10 = "pattern_10";
|
||||
public const string PATTERN_A = "pattern_a";
|
||||
public const string PATTERN_B = "pattern_b";
|
||||
public const string PATTERN_C = "pattern_c";
|
||||
public const string PATTERN_D = "pattern_d";
|
||||
public const string PATTERN_E = "pattern_e";
|
||||
public const string PATTERN_ERROR = "error";
|
||||
public const string PATTERN_PAPER_END = "paper_end";
|
||||
|
||||
// Layout constants
|
||||
public const string LAYOUT_RECEIPT = "receipt";
|
||||
public const string LAYOUT_RECEIPT_BM = "receipt_bm";
|
||||
public const string LAYOUT_LABEL = "label";
|
||||
public const string LAYOUT_LABEL_BM = "label_bm";
|
||||
|
||||
// Halftone constants
|
||||
public const int HALFTONE_DITHER = 0;
|
||||
public const int HALFTONE_ERROR_DIFFUSION = 1;
|
||||
public const int HALFTONE_THRESHOLD = 2;
|
||||
|
||||
public SoapEpsonPrint AddText(string data)
|
||||
{
|
||||
AddRow("text", Utils.EscapeMarkup(data));
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddTextLang(string lang)
|
||||
{
|
||||
message.Append($"<text lang=\"{lang}\"/>");
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddTextAlign(string align)
|
||||
{
|
||||
AddRow("text", null, new Dictionary<string, object> { { "align", align } });
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddTextRotate(string rotate)
|
||||
{
|
||||
var s = Utils.GetBoolAttr("rotate", rotate);
|
||||
message.Append($"<text{s}/>");
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddTextLineSpace(string linespc)
|
||||
{
|
||||
AddRow("text", null, new Dictionary<string, object> { { "linespc", linespc } });
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddTextFont(string font)
|
||||
{
|
||||
AddRow("text", null, new Dictionary<string, object> { { "font", font } });
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddTextSmooth(string smooth)
|
||||
{
|
||||
var s = Utils.GetBoolAttr("smooth", smooth);
|
||||
message.Append($"<text{s}/>");
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddTextDouble(string dw, string dh)
|
||||
{
|
||||
var s = new StringBuilder();
|
||||
if (dw != null)
|
||||
{
|
||||
s.Append(Utils.GetBoolAttr("dw", dw));
|
||||
}
|
||||
if (dh != null)
|
||||
{
|
||||
s.Append(Utils.GetBoolAttr("dh", dh));
|
||||
}
|
||||
message.Append($"<text{s}/>");
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddTextSize(int width, int height)
|
||||
{
|
||||
AddRow("text", null, new Dictionary<string, object>
|
||||
{
|
||||
{ "width", width },
|
||||
{ "height", height }
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddTextStyle(bool reverse, bool ul, bool em, string color)
|
||||
{
|
||||
AddRow("text", null, new Dictionary<string, object>
|
||||
{
|
||||
{ "reverse", reverse },
|
||||
{ "ul", ul },
|
||||
{ "em", em },
|
||||
{ "color", color }
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddTextPosition(int x)
|
||||
{
|
||||
AddRow("text", null, new Dictionary<string, object> { { "x", x } });
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddTextVPosition(int y)
|
||||
{
|
||||
AddRow("text", null, new Dictionary<string, object> { { "y", y } });
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddSymbol(string data, Symbol type, SymbolOptions options = null)
|
||||
{
|
||||
var attrs = new Dictionary<string, object>
|
||||
{
|
||||
{ "type", GetSymbolTypeString(type) }
|
||||
};
|
||||
|
||||
if (options != null)
|
||||
{
|
||||
if (options.ErrorCorrectionLevel.HasValue)
|
||||
attrs["level"] = options.ErrorCorrectionLevel.Value;
|
||||
if (options.Width.HasValue)
|
||||
attrs["width"] = options.Width.Value;
|
||||
if (options.Height.HasValue)
|
||||
attrs["height"] = options.Height.Value;
|
||||
if (options.Size.HasValue)
|
||||
attrs["size"] = options.Size.Value;
|
||||
}
|
||||
|
||||
AddRow("symbol", Utils.EscapeControl(Utils.EscapeMarkup(data)), attrs);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddQRCode(string data, QRCodeOptions options = null)
|
||||
{
|
||||
var attrs = new Dictionary<string, object>
|
||||
{
|
||||
{ "type", GetSymbolTypeString(Symbol.QRCODE_MODEL_2) }
|
||||
};
|
||||
|
||||
if (options != null)
|
||||
{
|
||||
if (options.ErrorCorrectionLevel.HasValue)
|
||||
attrs["level"] = options.ErrorCorrectionLevel.Value;
|
||||
if (options.Size.HasValue)
|
||||
attrs["width"] = options.Size.Value;
|
||||
}
|
||||
|
||||
AddRow("symbol", Utils.EscapeMarkup(data), attrs);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddFeedUnit(string unit)
|
||||
{
|
||||
message.Append($"<feed unit=\"{unit}\"/>");
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddFeedLine(int line)
|
||||
{
|
||||
AddRow("feed", null, new Dictionary<string, object> { { "line", line } });
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddFeed()
|
||||
{
|
||||
message.Append("<feed/>");
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddFeedPosition(string pos)
|
||||
{
|
||||
AddRow("feed", null, new Dictionary<string, object> { { "pos", pos } });
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddImage(string base64ImageData, string width, string height)
|
||||
{
|
||||
AddRow("image", base64ImageData, new Dictionary<string, object>
|
||||
{
|
||||
{ "height", height },
|
||||
{ "width", width },
|
||||
{ "color", "color_1" },
|
||||
{ "mode", "mono" }
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddLogo(string key1, string key2)
|
||||
{
|
||||
AddRow("logo", null, new Dictionary<string, object>
|
||||
{
|
||||
{ "key1", key1 },
|
||||
{ "key2", key2 }
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddBarcode(string data, string type, int hri, string font, int width, int height)
|
||||
{
|
||||
AddRow("barcode", Utils.EscapeControl(Utils.EscapeMarkup(data)), new Dictionary<string, object>
|
||||
{
|
||||
{ "type", type },
|
||||
{ "hri", hri },
|
||||
{ "font", font },
|
||||
{ "width", width },
|
||||
{ "height", height }
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddHLine(int x1, int x2, string style)
|
||||
{
|
||||
AddRow("hline", null, new Dictionary<string, object>
|
||||
{
|
||||
{ "x1", x1 },
|
||||
{ "x2", x2 },
|
||||
{ "style", style }
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddVLineBegin(int x, string style)
|
||||
{
|
||||
AddRow("vline", null, new Dictionary<string, object>
|
||||
{
|
||||
{ "x", x },
|
||||
{ "style", style }
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddVLineEnd(int x, string style)
|
||||
{
|
||||
AddRow("vline", null, new Dictionary<string, object>
|
||||
{
|
||||
{ "x", x },
|
||||
{ "style", style }
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddRotateBegin()
|
||||
{
|
||||
message.Append("<rotate-begin/>");
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddRotateEnd()
|
||||
{
|
||||
message.Append("<rotate-end/>");
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddCut(string type)
|
||||
{
|
||||
AddRow("cut", null, new Dictionary<string, object> { { "type", type } });
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddSound(object pattern, object repeat, string cycle)
|
||||
{
|
||||
AddRow("sound", null, new Dictionary<string, object>
|
||||
{
|
||||
{ "pattern", pattern },
|
||||
{ "repeat", repeat },
|
||||
{ "cycle", cycle }
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddRecovery()
|
||||
{
|
||||
AddRow("recovery");
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddReset()
|
||||
{
|
||||
AddRow("reset");
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint AddCommand(string data)
|
||||
{
|
||||
message.Append($"<command>{Utils.ToHexBinary(data)}</command>");
|
||||
return this;
|
||||
}
|
||||
|
||||
public SoapEpsonPrint KickOutDrawer(string drawer = DRAWER_1, string pulse = PULSE_100)
|
||||
{
|
||||
AddRow("pulse", null, new Dictionary<string, object>
|
||||
{
|
||||
{ "drawer", drawer },
|
||||
{ "time", pulse }
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var s = force ? " force=\"true\"" : "";
|
||||
return $"<epos-print xmlns=\"http://www.epson-pos.com/schemas/2011/03/epos-print\"{s}>{message}</epos-print>";
|
||||
}
|
||||
|
||||
public void AddRow(string field, string value = null, Dictionary<string, object> fields = null)
|
||||
{
|
||||
message.Append($"<{field}");
|
||||
|
||||
if (fields != null)
|
||||
{
|
||||
foreach (var kvp in fields)
|
||||
{
|
||||
if (kvp.Value == null)
|
||||
continue;
|
||||
|
||||
message.Append($" {kvp.Key}=\"{kvp.Value}\"");
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
message.Append("/>");
|
||||
}
|
||||
else
|
||||
{
|
||||
message.Append($">{value}</{field}>");
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSymbolTypeString(Symbol symbol)
|
||||
{
|
||||
return symbol switch
|
||||
{
|
||||
Symbol.PDF417_STANDARD => "pdf417_standard",
|
||||
Symbol.PDF417_TRUNCATED => "pdf417_truncated",
|
||||
Symbol.QRCODE_MODEL_1 => "qrcode_model_1",
|
||||
Symbol.QRCODE_MODEL_2 => "qrcode_model_2",
|
||||
Symbol.QRCODE_MICRO => "qrcode_micro",
|
||||
Symbol.MAXICODE_MODE_2 => "maxicode_mode_2",
|
||||
Symbol.MAXICODE_MODE_3 => "maxicode_mode_3",
|
||||
Symbol.MAXICODE_MODE_4 => "maxicode_mode_4",
|
||||
Symbol.MAXICODE_MODE_5 => "maxicode_mode_5",
|
||||
Symbol.MAXICODE_MODE_6 => "maxicode_mode_6",
|
||||
Symbol.GS1_DATABAR_STACKED => "gs1_databar_stacked",
|
||||
Symbol.GS1_DATABAR_STACKED_OMNIDIRECTIONAL => "gs1_databar_stacked_omnidirectional",
|
||||
Symbol.GS1_DATABAR_EXPANDED_STACKED => "gs1_databar_expanded_stacked",
|
||||
Symbol.AZTECCODE_FULLRANGE => "azteccode_fullrange",
|
||||
Symbol.AZTECCODE_COMPACT => "azteccode_compact",
|
||||
Symbol.DATAMATRIX_SQUARE => "datamatrix_square",
|
||||
Symbol.DATAMATRIX_RECTANGLE_8 => "datamatrix_rectangle_8",
|
||||
Symbol.DATAMATRIX_RECTANGLE_12 => "datamatrix_rectangle_12",
|
||||
Symbol.DATAMATRIX_RECTANGLE_16 => "datamatrix_rectangle_16",
|
||||
_ => throw new ArgumentException($"Invalid symbol type: {symbol}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class SymbolOptions
|
||||
{
|
||||
public int? ErrorCorrectionLevel { get; set; }
|
||||
public int? Width { get; set; }
|
||||
public int? Height { get; set; }
|
||||
public int? Size { get; set; }
|
||||
}
|
||||
|
||||
public class QRCodeOptions
|
||||
{
|
||||
public int? Size { get; set; }
|
||||
public int? ErrorCorrectionLevel { get; set; }
|
||||
}
|
||||
}
|
||||
152
Inspectron.Epson/SOAP/SoapEpsonPrinter.cs
Normal file
152
Inspectron.Epson/SOAP/SoapEpsonPrinter.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Inspectron.Epson.SOAP
|
||||
{
|
||||
public class SoapEpsonPrinter
|
||||
{
|
||||
private readonly string url;
|
||||
private static readonly HttpClient httpClient = new HttpClient(new HttpClientHandler
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
|
||||
});
|
||||
|
||||
private const string EPSON_XML_HEADER =
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
|
||||
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
|
||||
"<s:Header>" +
|
||||
"<parameter xmlns=\"http://www.epson-pos.com/schemas/2011/03/epos-print\" />" +
|
||||
"</s:Header>" +
|
||||
"<s:Body>";
|
||||
|
||||
public SoapEpsonPrinter(string ip)
|
||||
{
|
||||
url = $"https://{ip}/cgi-bin/epos/service.cgi?devid=local_printer&timeout=10000";
|
||||
}
|
||||
|
||||
public async Task<XElement> Send(SoapEpsonPrint print)
|
||||
{
|
||||
var data = print.ToString();
|
||||
Console.WriteLine(data.ToString());
|
||||
var xml = await Request(data);
|
||||
Console.WriteLine(xml.ToString());
|
||||
var response = xml.Descendants(XName.Get("response", "http://www.epson-pos.com/schemas/2011/03/epos-print"))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
throw new Exception("INVALID_RESPONSE");
|
||||
}
|
||||
|
||||
var successAttr = response.Attribute("success");
|
||||
var success = successAttr?.Value == "true";
|
||||
|
||||
var codeAttr = response.Attribute("code");
|
||||
var code = codeAttr?.Value ?? "";
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Exception(code);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<PrinterStatus> GetStatus(SoapEpsonPrint print)
|
||||
{
|
||||
var data = print.ToString();
|
||||
var xml = await Request(data);
|
||||
|
||||
Console.WriteLine(xml.ToString());
|
||||
|
||||
var response = xml.Descendants(XName.Get("response", "http://www.epson-pos.com/schemas/2011/03/epos-print"))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
throw new Exception("INVALID_RESPONSE");
|
||||
}
|
||||
|
||||
var statusAttr = response.Attribute("status");
|
||||
var status = int.Parse(statusAttr?.Value ?? "0");
|
||||
|
||||
var statuses = new PrinterStatus
|
||||
{
|
||||
IsResponsive = true,
|
||||
DrawerIsOpen = true,
|
||||
CoverIsOpen = false,
|
||||
IsOffline = false,
|
||||
PaperNearEmpty = false,
|
||||
PaperEmpty = false
|
||||
};
|
||||
|
||||
if ((status & 0x00000001) != 0)
|
||||
{
|
||||
statuses.IsResponsive = false;
|
||||
}
|
||||
|
||||
if ((status & 0x00000004) != 0)
|
||||
{
|
||||
statuses.DrawerIsOpen = false;
|
||||
}
|
||||
|
||||
if ((status & 0x00000008) != 0)
|
||||
{
|
||||
statuses.IsOffline = true;
|
||||
}
|
||||
|
||||
if ((status & 0x00000020) != 0)
|
||||
{
|
||||
statuses.CoverIsOpen = true;
|
||||
}
|
||||
|
||||
if ((status & 0x00020000) != 0)
|
||||
{
|
||||
statuses.PaperNearEmpty = true;
|
||||
}
|
||||
|
||||
if ((status & 0x00080000) != 0)
|
||||
{
|
||||
statuses.PaperEmpty = true;
|
||||
}
|
||||
|
||||
return statuses;
|
||||
}
|
||||
|
||||
private async Task<XDocument> Request(string data)
|
||||
{
|
||||
var body = $"{EPSON_XML_HEADER}{data}</s:Body></s:Envelope>";
|
||||
|
||||
var content = new StringContent(body, Encoding.UTF8, "text/xml");
|
||||
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Post, url))
|
||||
{
|
||||
request.Content = content;
|
||||
//request.Headers.Add("If-Modified-Since", "Thu, 01 Jun 1970 00:00:00 GMT");
|
||||
//request.Headers.Add("SOAPAction", "\"\"");
|
||||
|
||||
var response = await httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
return XDocument.Parse(responseText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PrinterStatus
|
||||
{
|
||||
public bool IsResponsive { get; set; }
|
||||
public bool DrawerIsOpen { get; set; }
|
||||
public bool CoverIsOpen { get; set; }
|
||||
public bool IsOffline { get; set; }
|
||||
public bool PaperNearEmpty { get; set; }
|
||||
public bool PaperEmpty { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"IsResponsive: {IsResponsive}, DrawerIsOpen: {DrawerIsOpen}, CoverIsOpen: {CoverIsOpen}, IsOffline: {IsOffline}, PaperNearEmpty: {PaperNearEmpty}, PaperEmpty: {PaperEmpty}";
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
242
Inspectron.Epson/SOAP/Utils.cs
Normal file
242
Inspectron.Epson/SOAP/Utils.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Inspectron.Epson.SOAP
|
||||
{
|
||||
public static class Utils
|
||||
{
|
||||
public static string GetEnumAttr(string name, string value, Regex regex)
|
||||
{
|
||||
if (!regex.IsMatch(value))
|
||||
{
|
||||
throw new ArgumentException($"Parameter \"{name}\" is invalid");
|
||||
}
|
||||
return $" {name}=\"{value}\"";
|
||||
}
|
||||
|
||||
public static string GetBoolAttr(string name, string value)
|
||||
{
|
||||
return $" {name}=\"{!string.IsNullOrEmpty(value)}\"";
|
||||
}
|
||||
|
||||
public static string GetIntAttr(string name, int value, int min, int max)
|
||||
{
|
||||
if (value < min || value > max)
|
||||
{
|
||||
throw new ArgumentException($"Parameter \"{name}\" is invalid");
|
||||
}
|
||||
return $" {name}=\"{value}\"";
|
||||
}
|
||||
|
||||
public static string GetUByteAttr(string name, int value)
|
||||
{
|
||||
return GetIntAttr(name, value, 0, 255);
|
||||
}
|
||||
|
||||
public static string GetUShortAttr(string name, int value)
|
||||
{
|
||||
return GetIntAttr(name, value, 0, 65535);
|
||||
}
|
||||
|
||||
public static string GetShortAttr(string name, int value)
|
||||
{
|
||||
return GetIntAttr(name, value, -32768, 32767);
|
||||
}
|
||||
|
||||
public static string ToHexBinary(string s)
|
||||
{
|
||||
var result = new StringBuilder(s.Length * 2);
|
||||
foreach (char c in s)
|
||||
{
|
||||
result.Append(((int)c).ToString("x2"));
|
||||
}
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
public static string EscapeMarkup(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s))
|
||||
return s;
|
||||
|
||||
var markup = new Regex(@"[<>&'""\t\n\r]");
|
||||
if (markup.IsMatch(s))
|
||||
{
|
||||
s = markup.Replace(s, match =>
|
||||
{
|
||||
return match.Value switch
|
||||
{
|
||||
"<" => "<",
|
||||
">" => ">",
|
||||
"&" => "&",
|
||||
"'" => "'",
|
||||
"\"" => """,
|
||||
"\t" => "	",
|
||||
"\n" => " ",
|
||||
"\r" => " ",
|
||||
_ => match.Value
|
||||
};
|
||||
});
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
public static string EscapeControl(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s))
|
||||
return s;
|
||||
|
||||
var control = new Regex(@"[\\\x00-\x1f\x7f-\xff]");
|
||||
if (control.IsMatch(s))
|
||||
{
|
||||
s = control.Replace(s, match =>
|
||||
{
|
||||
char c = match.Value[0];
|
||||
if (c == '\\')
|
||||
return "\\\\";
|
||||
else
|
||||
return $"\\x{((int)c):x2}";
|
||||
});
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
public static string ToMonoImage(ImageData imgdata, int s, double g)
|
||||
{
|
||||
var x = new Func<int, char>(code => (char)code);
|
||||
var m8 = new int[8][]
|
||||
{
|
||||
new int[] { 2, 130, 34, 162, 10, 138, 42, 170 },
|
||||
new int[] { 194, 66, 226, 98, 202, 74, 234, 106 },
|
||||
new int[] { 50, 178, 18, 146, 58, 186, 26, 154 },
|
||||
new int[] { 242, 114, 210, 82, 250, 122, 218, 90 },
|
||||
new int[] { 14, 142, 46, 174, 6, 134, 38, 166 },
|
||||
new int[] { 206, 78, 238, 110, 198, 70, 230, 102 },
|
||||
new int[] { 62, 190, 30, 158, 54, 182, 22, 150 },
|
||||
new int[] { 254, 126, 222, 94, 246, 118, 214, 86 }
|
||||
};
|
||||
|
||||
var d = imgdata.Data;
|
||||
var w = imgdata.Width;
|
||||
var h = imgdata.Height;
|
||||
var r = new StringBuilder(((w + 7) >> 3) * h);
|
||||
int n = 0;
|
||||
int p = 0;
|
||||
int t = 128;
|
||||
var e = new int[w];
|
||||
|
||||
for (int j = 0; j < h; j++)
|
||||
{
|
||||
int e1 = 0;
|
||||
int e2 = 0;
|
||||
int i = 0;
|
||||
while (i < w)
|
||||
{
|
||||
int b = i & 7;
|
||||
if (s == 0)
|
||||
{
|
||||
t = m8[j & 7][b];
|
||||
}
|
||||
|
||||
int v = (int)(Math.Pow(
|
||||
(((d[p++] * 0.29891 + d[p++] * 0.58661 + d[p++] * 0.11448) * d[p]) / 255 +
|
||||
255 - d[p++]) / 255,
|
||||
1 / g) * 255);
|
||||
|
||||
if (s == 1)
|
||||
{
|
||||
v += (e[i] + e1) >> 4;
|
||||
int f = v - (v < t ? 0 : 255);
|
||||
if (i > 0)
|
||||
{
|
||||
e[i - 1] += f;
|
||||
}
|
||||
e[i] = f * 7 + e2;
|
||||
e1 = f * 5;
|
||||
e2 = f * 3;
|
||||
}
|
||||
|
||||
if (v < t)
|
||||
{
|
||||
n |= 128 >> b;
|
||||
}
|
||||
|
||||
i++;
|
||||
if (b == 7 || i == w)
|
||||
{
|
||||
r.Append(x(n == 16 ? 32 : n));
|
||||
n = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.ToString();
|
||||
}
|
||||
|
||||
public static string ToGrayImage(int[] data, int width, int height, double g)
|
||||
{
|
||||
var x = new Func<int, char>(code => (char)code);
|
||||
var m4 = new int[4][]
|
||||
{
|
||||
new int[] { 0, 9, 2, 11 },
|
||||
new int[] { 13, 4, 15, 6 },
|
||||
new int[] { 3, 12, 1, 10 },
|
||||
new int[] { 16, 7, 14, 5 }
|
||||
};
|
||||
|
||||
var thermal = new int[]
|
||||
{
|
||||
0, 7, 13, 19, 23, 27, 31, 35, 40, 44, 49, 52, 54, 55, 57, 59, 61, 62, 64, 66, 67, 69, 70, 70,
|
||||
71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 83, 84, 85, 86, 86, 87, 88, 88, 89, 90,
|
||||
90, 91, 91, 92, 93, 93, 94, 94, 95, 96, 96, 97, 98, 98, 99, 99, 100, 101, 101, 102, 102, 103,
|
||||
103, 104, 104, 105, 105, 106, 106, 107, 107, 108, 108, 109, 109, 110, 110, 111, 111, 112, 112,
|
||||
112, 113, 113, 114, 114, 115, 115, 116, 116, 117, 117, 118, 118, 119, 119, 120, 120, 120, 121,
|
||||
121, 122, 122, 123, 123, 123, 124, 124, 125, 125, 125, 126, 126, 127, 127, 127, 128, 128, 129,
|
||||
129, 130, 130, 130, 131, 131, 132, 132, 132, 133, 133, 134, 134, 135, 135, 135, 136, 136, 137,
|
||||
137, 137, 138, 138, 139, 139, 139, 140, 140, 141, 141, 141, 142, 142, 143, 143, 143, 144, 144,
|
||||
145, 145, 146, 146, 146, 147, 147, 148, 148, 148, 149, 149, 150, 150, 150, 151, 151, 152, 152,
|
||||
152, 153, 153, 154, 154, 155, 155, 155, 156, 156, 157, 157, 158, 158, 159, 159, 160, 160, 161,
|
||||
161, 161, 162, 162, 163, 163, 164, 164, 165, 165, 166, 166, 166, 167, 167, 168, 168, 169, 169,
|
||||
170, 170, 171, 171, 172, 173, 173, 174, 175, 175, 176, 177, 178, 178, 179, 180, 180, 181, 182,
|
||||
182, 183, 184, 184, 185, 186, 186, 187, 189, 191, 193, 195, 198, 200, 202, 255
|
||||
};
|
||||
|
||||
var r = new StringBuilder(((width + 1) >> 1) * height);
|
||||
int n = 0;
|
||||
int p = 0;
|
||||
|
||||
for (int j = 0; j < height; j++)
|
||||
{
|
||||
int i = 0;
|
||||
while (i < width)
|
||||
{
|
||||
int b = i & 1;
|
||||
int v = thermal[(int)(Math.Pow(
|
||||
(((data[p++] * 0.29891 + data[p++] * 0.58661 + data[p++] * 0.11448) * data[p]) / 255 +
|
||||
255 - data[p++]) / 255,
|
||||
1 / g) * 255)];
|
||||
|
||||
int v1 = v / 17;
|
||||
if (m4[j & 3][i & 3] < v % 17)
|
||||
{
|
||||
v1++;
|
||||
}
|
||||
|
||||
n |= v1 << ((1 - b) << 2);
|
||||
i++;
|
||||
if (b == 1 || i == width)
|
||||
{
|
||||
r.Append(x(n));
|
||||
n = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class ImageData
|
||||
{
|
||||
public int[] Data { get; set; }
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user