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