using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using System.Net.Sockets; using System.Reflection; using System.Text; namespace Inspectron.Epson; /// /// Main SDK class for Epson TM-m30III printer operations /// public class EpsonPrinter : IEpsonPrinter { private readonly ILogger? _logger; private TcpClient? _tcpClient; private NetworkStream? _stream; private string? _printerIp; private int _printerPort; private int _timeoutSeconds; private static bool _initialized; /// /// Returns true if connected to the printer /// public bool IsConnected => _tcpClient?.Connected ?? false; /// /// Initialize a new instance of EpsonPrinter /// /// Optional logger for diagnostic output public EpsonPrinter(ILogger? logger = null) { _logger = logger; RegisterCodepages(); } private static void RegisterCodepages() { if (_initialized) return; Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); _initialized = true; } /// /// Connect to the printer /// /// Printer IP address /// Printer port (default 9100) /// Connection timeout in seconds (default 5) /// Thrown when connection fails 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); } } /// /// Disconnect from the printer /// public void Disconnect() { _logger?.LogDebug("Disconnecting from printer"); _stream?.Dispose(); _stream = null; _tcpClient?.Dispose(); _tcpClient = null; } /// /// Send a command and receive a 1-byte response (for DLE EOT commands) /// private async Task 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); } } /// /// Send a command without expecting a response (for printing commands) /// 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); } } /// /// Get general printer status /// /// Printer status information public async Task GetPrinterStatusAsync() { _logger?.LogDebug("Querying printer status"); var response = await SendCommandAsync(EpsonCommands.GetPrinterStatus); return PrinterStatus.Parse(response); } /// /// Get offline status (reasons for offline) /// /// Offline status information public async Task GetOfflineStatusAsync() { _logger?.LogDebug("Querying offline status"); var response = await SendCommandAsync(EpsonCommands.GetOfflineStatus); return OfflineStatus.Parse(response); } /// /// Get error status /// /// Error status information public async Task GetErrorStatusAsync() { _logger?.LogDebug("Querying error status"); var response = await SendCommandAsync(EpsonCommands.GetErrorStatus); return ErrorStatus.Parse(response); } /// /// Get paper sensor status /// /// Paper sensor status information public async Task GetPaperSensorStatusAsync() { _logger?.LogDebug("Querying paper sensor status"); var response = await SendCommandAsync(EpsonCommands.GetPaperSensorStatus); return PaperSensorStatus.Parse(response); } public async Task GetTM220StatusAsync() { _logger?.LogDebug("Querying TM220 status"); var response = await SendCommandAsync([0x10,0x04,0x01]); return response; } /// /// Get printer model/ID /// public async Task 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; } } /// /// Get comprehensive overall status with health assessment /// /// Overall status with recommendations public async Task 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]); } /// /// Print text to the printer /// /// Text to print 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); } /// /// Print text, feed paper, and cut /// /// Text to print /// Number of lines to feed before cutting (default 5) 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); } /// /// Print an image from a file path /// /// Path to the image file /// Maximum width in pixels (default 384 for 80mm thermal printer) 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(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(), ex); } } public async Task PrintLoadedImage() { var command = EpsonCommands.PrintLoadedImage; await SendRawAsync(command); } /// /// Print an image from a stream /// /// Stream containing the image data /// Maximum width in pixels (default 384 for 80mm thermal printer) public async Task LoadImageAsync(Stream imageStream, int maxWidth = 384) { _logger?.LogInformation("Printing image from stream"); try { using var image = await Image.LoadAsync(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(), 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); } /// /// Print an image from an ImageSharp Image object /// /// ImageSharp Image object /// Maximum width in pixels (default 384 for 80mm thermal printer) public async Task LoadImageAsync(Image 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(), ex); } } /// /// Print an image using bit-image mode (ESC * command) from a file path /// /// Path to the image file /// Bit image mode (8-dot or 24-dot, single or double density) /// Maximum width in pixels (default 384 for 80mm thermal printer) 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(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(), ex); } } /// /// Print an image using bit-image mode (ESC * command) from a stream /// /// Stream containing the image data /// Bit image mode (8-dot or 24-dot, single or double density) /// Maximum width in pixels (default 384 for 80mm thermal printer) 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(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(), ex); } } /// /// 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. /// /// ImageSharp Image object /// Bit image mode (8-dot or 24-dot, single or double density) /// Maximum width in pixels (default 384 for 80mm thermal printer) public async Task PrintImageBitModeAsync(Image 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(), ex); } } /// /// Send raw ESC/POS command bytes to the printer /// /// Raw command bytes public async Task SendRawCommandAsync(byte[] command) { _logger?.LogDebug("Sending raw command: {Length} bytes", command.Length); await SendRawAsync(command); } /// /// Determine overall health status from individual status components /// private OverallStatus DetermineOverallStatus( byte? printerModel, PrinterStatus printerStatus, OfflineStatus offlineStatus, ErrorStatus errorStatus, PaperSensorStatus paperStatus) { var recommendations = new List(); 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(); 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; } }