Add project files.

This commit is contained in:
EugeneTes
2026-01-13 09:06:47 +01:00
parent dd935fe1fa
commit 7390693f50
187 changed files with 83457 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"WebSearch"
],
"deny": [],
"ask": []
}
}

View 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; }
}
}

View 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}";
}
}

View 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}";
}
}

View 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;
}
}

View 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();
}
}

View 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}");
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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");
}
}

View 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; } = "❓";
}

View 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>

View File

@@ -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();
}
}

View File

@@ -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"
};
}
}

View File

@@ -0,0 +1,6 @@
namespace Inspectron.Epson.PrintServer;
public interface IPrintJobSource
{
Task<PrintJob> GetNextJobAsync(CancellationToken cancellationToken);
}

View 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);
}
}
}

View 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);
}
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,6 @@
namespace Inspectron.Epson.PrintServer.PrintServices;
public interface IAssignedPrinterRepository
{
string? GetAssignedPrinter(string workAreaId);
}

View 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();
}

View File

@@ -0,0 +1,6 @@
namespace Inspectron.Epson.PrintServer.PrintServices;
public interface IPrinterConfigurationSource
{
PrinterConfiguration GetConfiguration(string printerAddress);
}

View File

@@ -0,0 +1,6 @@
namespace Inspectron.Epson.PrintServer.PrintServices;
public interface IPrinterFactory
{
IPrinter CreatePrinterFromId(byte printerId, EpsonPrinter epsonPrinter);
}

View File

@@ -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; }
}

View File

@@ -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";
}
}

View 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.");
}
}
}

View 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();
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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
{
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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;
}
}

View 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);
}
}

View File

@@ -0,0 +1,6 @@
namespace Inspectron.Epson.PrintServer.WorkAreaSources;
public interface IWorkAreasSource
{
public Task<List<WorkArea>> GetWorkAreasAsync();
}

View File

@@ -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!;
}
}

View 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;
}

View File

@@ -0,0 +1,6 @@
using System.Threading.Tasks;
public interface IPrintService
{
Task<bool> PrintAsync(string printerIp, PrintJob job);
}

View 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;
}
}

View 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");
}
}

View 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
View 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.

View 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
}
}

View 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; }
}
}

View 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}";
}
}
}

View 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
{
"<" => "&lt;",
">" => "&gt;",
"&" => "&amp;",
"'" => "&apos;",
"\"" => "&quot;",
"\t" => "&#9;",
"\n" => "&#10;",
"\r" => "&#13;",
_ => 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; }
}
}