763 lines
27 KiB
C#
763 lines
27 KiB
C#
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 : IEpsonPrinter
|
|
{
|
|
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);
|
|
}
|
|
/// <summary>
|
|
/// Change to bigger font mode for TM-T20/220 printers
|
|
/// </summary>
|
|
/// <param name="biggerWidth"></param>
|
|
/// <param name="biggerHeight"></param>
|
|
/// <param name="secondaryFont"></param>
|
|
/// <see cref="https://download4.epson.biz/sec_pubs/pos/reference_en/escpos/esc_exclamation.html"/>
|
|
public async Task SetBiggerFontTM220(bool biggerWidth, bool biggerHeight=false, bool secondaryFont=true)
|
|
{
|
|
_logger?.LogInformation("Setting bigger font mode: Width={BiggerWidth}, Height={BiggerHeight}, SecondaryFont={SecondaryFont}",
|
|
biggerWidth, biggerHeight, secondaryFont);
|
|
|
|
byte value = 0x00;
|
|
if (biggerWidth)
|
|
value += 0x20;
|
|
if (biggerHeight)
|
|
value += 0x10;
|
|
if (secondaryFont)
|
|
value += 0x01;
|
|
|
|
await SendRawCommandAsync([0x1b, 0x21, value]);
|
|
}
|
|
|
|
|
|
/// <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(bool fullCut=true)
|
|
{
|
|
_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;
|
|
}
|
|
}
|