Initial commit

Initial commit til Git.
V2 er deployed
This commit is contained in:
2026-06-13 17:31:50 +02:00
parent 9fcd2b145e
commit 41e23b6184
375 changed files with 15956 additions and 0 deletions

View File

@@ -0,0 +1,378 @@
using ESCPOS_NET.Emitters.BaseCommandValues;
using ESCPOS_NET.Utilities;
using ESCPOS_NET.Utils;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
namespace ESCPOS_NET
{
public abstract partial class BasePrinter : IPrinter, IDisposable
{
private bool disposed = false;
//private volatile bool _isMonitoring;
private CancellationTokenSource _readCancellationTokenSource;
private CancellationTokenSource _writeCancellationTokenSource;
private readonly int _maxBytesPerWrite = 15000; // max byte chunks to write at once.
public PrinterStatusEventArgs Status { get; private set; } = new PrinterStatusEventArgs();
public event EventHandler StatusChanged;
public event EventHandler Disconnected;
public event EventHandler Connected;
//public event EventHandler WriteFailed;
//public event EventHandler Idle;
protected BinaryWriter Writer { get; set; }
protected BinaryReader Reader { get; set; }
protected ConcurrentQueue<byte> ReadBuffer { get; set; } = new ConcurrentQueue<byte>();
protected ConcurrentQueue<byte[]> WriteBuffer { get; set; } = new ConcurrentQueue<byte[]>();
protected int BytesWrittenSinceLastFlush { get; set; } = 0;
protected volatile bool IsConnected = true;
public string PrinterName { get; protected set; }
protected BasePrinter()
{
PrinterName = Guid.NewGuid().ToString();
Init();
}
protected BasePrinter(string printerName)
{
if (string.IsNullOrEmpty(printerName))
{
printerName = Guid.NewGuid().ToString();
}
PrinterName = printerName;
Init();
}
private void Init()
{
_readCancellationTokenSource = new CancellationTokenSource();
_writeCancellationTokenSource = new CancellationTokenSource();
Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Initializing Task Threads...", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
//Task.Factory.StartNew(MonitorPrinterStatusLongRunningTask, _connectivityCancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false);
Task.Factory.StartNew(WriteLongRunningTask, _writeCancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false);
Task.Factory.StartNew(ReadLongRunningTask, _readCancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false);
// TODO: read and status monitoring probably won't work for fileprinter, should let printer types disable this feature.
Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Task Threads started", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
}
protected void InvokeConnect()
{
Task.Run(() => Connected?.Invoke(this, new ConnectionEventArgs() { IsConnected = true }));
}
protected void InvokeDisconnect()
{
Task.Run(() => Disconnected?.Invoke(this, new ConnectionEventArgs() { IsConnected = false }));
}
protected virtual void Reconnect()
{
// Implemented in the network printer
}
protected virtual async void WriteLongRunningTask()
{
while (true)
{
if (_writeCancellationTokenSource != null && _writeCancellationTokenSource.IsCancellationRequested)
{
Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Write Long-Running Task Cancellation was requested.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
break;
}
await Task.Delay(100);
if (!IsConnected)
{
continue;
}
try
{
var didDequeue = WriteBuffer.TryDequeue(out var nextBytes);
if (didDequeue && nextBytes?.Length > 0)
{
WriteToBinaryWriter(nextBytes);
}
}
catch (IOException ex)
{
// Thrown if the printer times out the socket connection
// default is 90 seconds
//Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Swallowing IOException... sometimes happens with network printers. Should get reconnected automatically.");
}
catch (Exception ex)
{
// Swallow the exception
//Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Swallowing generic read exception... sometimes happens with serial port printers.");
}
}
}
protected virtual async void ReadLongRunningTask()
{
while (true)
{
if (_readCancellationTokenSource != null && _readCancellationTokenSource.IsCancellationRequested)
{
Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Read Long-Running Task Cancellation was requested.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
break;
}
await Task.Delay(100);
if (Reader == null) continue;
if (!IsConnected) continue;
try
{
// Sometimes the serial port lib will throw an exception and read past the end of the queue if a
// status changes while data is being written. We just ignore these bytes.
var b = Reader.BaseStream.ReadByte();
if (b >= 0 && b <= 255)
{
ReadBuffer.Enqueue((byte)b);
DataAvailable();
}
}
catch (Exception ex)
{
// Swallow the exception
//Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Swallowing generic read exception... sometimes happens with serial port printers.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
}
}
}
public virtual void Write(params byte[][] arrays)
{
Write(ByteSplicer.Combine(arrays));
}
public virtual void Write(byte[] bytes)
{
WriteBuffer.Enqueue(bytes);
}
protected virtual void WriteToBinaryWriter(byte[] bytes)
{
if (!IsConnected)
{
Logging.Logger?.LogInformation("[{Function}]:[{PrinterName}] Attempted to write but printer isn't connected. Attempting to reconnect...", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
Reconnect();
}
if (!IsConnected)
{
Logging.Logger?.LogError("[{Function}]:[{PrinterName}] Unrecoverable connectivity error writing to printer.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
throw new IOException("Unrecoverable connectivity error writing to printer.");
}
int bytePointer = 0;
int bytesLeft = bytes.Length;
bool hasFlushed = false;
while (bytesLeft > 0)
{
int count = Math.Min(_maxBytesPerWrite, bytesLeft);
try
{
Writer.Write(bytes, bytePointer, count);
}
catch (IOException e)
{
Reconnect();
if (!IsConnected)
{
Logging.Logger?.LogError(e, "[{Function}]:[{PrinterName}] Unrecoverable connectivity error writing to printer.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
}
Writer.Write(bytes, bytePointer, count);
}
BytesWrittenSinceLastFlush += count;
if (BytesWrittenSinceLastFlush >= 200)
{
// Immediately trigger a flush before proceeding so the output buffer will not be delayed.
hasFlushed = true;
Flush(null, null);
}
bytePointer += count;
bytesLeft -= count;
}
if (!hasFlushed)
{
Task.Run(async () => { await Task.Delay(50); Flush(null, null); });
}
}
protected virtual void Flush(object sender, ElapsedEventArgs e)
{
try
{
BytesWrittenSinceLastFlush = 0;
Writer.Flush();
}
catch (Exception ex)
{
Logging.Logger?.LogError(ex, "[{Function}]:[{PrinterName}] Flush threw exception.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
}
}
public virtual void DataAvailable()
{
if (ReadBuffer.Count() % 4 == 0)
{
var bytes = new byte[4];
for (int i = 0; i < 4; i++)
{
if (!ReadBuffer.TryDequeue(out bytes[i]))
{
// Ran out of bytes unexpectedly.
return;
}
}
TryUpdatePrinterStatus(bytes);
// TODO: call other update handlers.
}
}
private void TryUpdatePrinterStatus(byte[] bytes)
{
var bytesToString = BitConverter.ToString(bytes);
Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] TryUpdatePrinterStatus: Received flag values {bytesToString}", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName, bytesToString);
// Check header bits 0, 1 and 7 are 0, and 4 is 1
if (bytes[0].IsBitNotSet(0) && bytes[0].IsBitNotSet(1) && bytes[0].IsBitSet(4) && bytes[0].IsBitNotSet(7))
{
Status = new PrinterStatusEventArgs()
{
// byte[0] == 20 cash drawer closed
// byte[0] == 16 cash drawer open
// Note some cash drawers do not close properly.
IsCashDrawerOpen = bytes[0].IsBitNotSet(2),
IsPrinterOnline = bytes[0].IsBitNotSet(3),
IsCoverOpen = bytes[0].IsBitSet(5),
IsPaperCurrentlyFeeding = bytes[0].IsBitSet(6),
IsWaitingForOnlineRecovery = bytes[1].IsBitSet(0),
IsPaperFeedButtonPushed = bytes[1].IsBitSet(1),
DidRecoverableNonAutocutterErrorOccur = bytes[1].IsBitSet(2),
DidAutocutterErrorOccur = bytes[1].IsBitSet(3),
DidUnrecoverableErrorOccur = bytes[1].IsBitSet(5),
DidRecoverableErrorOccur = bytes[1].IsBitSet(6),
IsPaperLow = bytes[2].IsBitSet(0) && bytes[2].IsBitSet(1),
IsPaperOut = bytes[2].IsBitSet(2) && bytes[2].IsBitSet(3),
};
}
if (StatusChanged != null)
{
Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Invoking Status Changed Event Handler...", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
StatusChanged?.Invoke(this, Status);
}
}
~BasePrinter()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void OverridableDispose() // This method should only be called by the Dispose method. // It allows synchronous disposing of derived class dependencies with base class disposes.
{
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
try
{
_readCancellationTokenSource?.Cancel();
}
catch (Exception e)
{
Logging.Logger?.LogDebug(e, "[{Function}]:[{PrinterName}] Dispose Issue during cancellation token cancellation call.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
}
try
{
Reader?.Close();
}
catch (Exception e)
{
Logging.Logger?.LogDebug(e, "[{Function}]:[{PrinterName}] Dispose Issue closing reader.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
}
try
{
Reader?.Dispose();
}
catch (Exception e)
{
Logging.Logger?.LogDebug(e, "[{Function}]:[{PrinterName}] Dispose Issue disposing reader.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
}
try
{
Writer?.Close();
}
catch (Exception e)
{
Logging.Logger?.LogDebug(e, "[{Function}]:[{PrinterName}] Dispose Issue closing writer.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
}
try
{
Writer?.Dispose();
}
catch (Exception e)
{
Logging.Logger?.LogDebug(e, "[{Function}]:[{PrinterName}] Dispose Issue disposing writer.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
}
try
{
OverridableDispose();
}
catch (Exception e)
{
Logging.Logger?.LogDebug(e, "[{Function}]:[{PrinterName}] Dispose Issue during overridable dispose.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName);
}
}
disposed = true;
}
public PrinterStatusEventArgs GetStatus()
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace ESCPOS_NET
{
public class ConnectionEventArgs : EventArgs
{
public bool IsConnected;
}
}

View File

@@ -0,0 +1,36 @@
using System.IO;
namespace ESCPOS_NET
{
public class FilePrinter : BasePrinter
{
private readonly FileStream _file;
// TODO: default values to their default values in ctor.
public FilePrinter(string filePath, bool createIfNotExists = false)
: base()
{
if (createIfNotExists)
{
_file = File.Open(filePath, FileMode.OpenOrCreate);
}
else
{
_file = File.Open(filePath, FileMode.Open);
}
Writer = new BinaryWriter(_file);
Reader = new BinaryReader(_file);
}
~FilePrinter()
{
Dispose(false);
}
protected override void OverridableDispose()
{
_file?.Close();
_file?.Dispose();
}
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace ESCPOS_NET
{
public interface IPrinter
{
PrinterStatusEventArgs GetStatus();
void Write(params byte[][] arrays);
void Write(byte[] bytes);
event EventHandler StatusChanged;
event EventHandler Disconnected;
event EventHandler Connected;
//event EventHandler WriteFailed;
//event EventHandler Idle;
//event EventHandler IdleDisconnected; is this useful? to know that it disconnected because of idle? probably better to have this as info in disconnected event object instead.
}
}

View File

@@ -0,0 +1,33 @@
using System.IO;
namespace ESCPOS_NET
{
public class MemoryPrinter : BasePrinter
{
private readonly MemoryStream _ms;
// TODO: default values to their default values in ctor.
public MemoryPrinter()
: base()
{
_ms = new MemoryStream();
Writer = new BinaryWriter(_ms);
}
~MemoryPrinter()
{
Dispose(false);
}
public byte[] GetAllData()
{
return _ms.ToArray();
}
protected override void OverridableDispose()
{
_ms?.Close();
_ms?.Dispose();
}
}
}

View File

@@ -0,0 +1,101 @@
using Microsoft.Extensions.Logging;
using SimpleTcp;
using System;
using System.IO;
using System.Net;
using System.Text.Json;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Reflection;
namespace ESCPOS_NET
{
public class NetworkPrinterSettings
{
// Connection string is of the form printer_name:port or ip:port
public string ConnectionString { get; set; }
public EventHandler ConnectedHandler { get; set; }
public EventHandler DisconnectedHandler { get; set; }
//public bool ReconnectOnTimeout { get; set; }
//public uint? ReceiveTimeoutMs { get; set; }
//public uint? SendTimeoutMs { get; set; }
//public uint? ConnectTimeoutMs { get; set; }
//public uint? ReconnectTimeoutMs { get; set; }
//public uint? MaxReconnectAttempts { get; set; }
public string PrinterName { get; set; }
}
public class NetworkPrinter : BasePrinter
{
private readonly NetworkPrinterSettings _settings;
private TCPConnection _tcpConnection;
public NetworkPrinter(NetworkPrinterSettings settings) : base(settings.PrinterName)
{
_settings = settings;
if (settings.ConnectedHandler != null)
{
Connected += settings.ConnectedHandler;
}
if (settings.DisconnectedHandler != null)
{
Disconnected += settings.DisconnectedHandler;
}
Connect();
}
private void ConnectedEvent(object sender, ClientConnectedEventArgs e)
{
Logging.Logger?.LogInformation("[{Function}]:[{PrinterName}] Connected successfully to network printer! Connection String: {ConnectionString}", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName, _settings.ConnectionString);
IsConnected = true;
InvokeConnect();
}
private void DisconnectedEvent(object sender, ClientDisconnectedEventArgs e)
{
IsConnected = false;
InvokeDisconnect();
Logging.Logger?.LogWarning("[{Function}]:[{PrinterName}] Network printer connection terminated. Attempting to reconnect. Connection String: {ConnectionString}", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName, _settings.ConnectionString);
Connect();
}
private void AttemptReconnectInfinitely()
{
try
{
//_tcpConnection.ConnectWithRetries(300000);
_tcpConnection.ConnectWithRetries(3000);
}
catch (Exception e)
{
//Logging.Logger?.LogWarning("[{Function}]:[{PrinterName}] Network printer unable to connect after 5 minutes. Attempting to reconnect. Settings: {Settings}", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName, JsonSerializer.Serialize(_settings));
Task.Run(async () => { await Task.Delay(250); Connect(); });
}
}
private void Connect()
{
if (_tcpConnection != null)
{
_tcpConnection.Connected -= ConnectedEvent;
_tcpConnection.Disconnected -= DisconnectedEvent;
}
// instantiate
_tcpConnection = new TCPConnection(_settings.ConnectionString);
// set events
_tcpConnection.Connected += ConnectedEvent;
_tcpConnection.Disconnected += DisconnectedEvent;
Reader = new BinaryReader(_tcpConnection.ReadStream);
Writer = new BinaryWriter(_tcpConnection.WriteStream);
Task.Run(() => { AttemptReconnectInfinitely(); });
}
protected override void OverridableDispose()
{
_tcpConnection = null;
}
}
}

View File

@@ -0,0 +1,8 @@
namespace ESCPOS_NET
{
public class PrinterOptions
{
int IdleTimeoutSeconds { get; set; } = 60;
int StatusPollingIntervalMilliseconds { get; set; } = 500;
}
}

View File

@@ -0,0 +1,34 @@
using System;
namespace ESCPOS_NET
{
public class PrinterStatusEventArgs : EventArgs
{
public bool? IsWaitingForOnlineRecovery { get; set; }
public bool? IsPaperCurrentlyFeeding { get; set; }
public bool? IsPaperFeedButtonPushed { get; set; }
public bool? IsPrinterOnline { get; set; }
public bool? IsCashDrawerOpen { get; set; }
public bool? IsCoverOpen { get; set; }
public bool? IsPaperLow { get; set; }
public bool? IsPaperOut { get; set; }
public bool? IsInErrorState => (DidRecoverableErrorOccur ?? false) || (DidUnrecoverableErrorOccur ?? false) || (DidAutocutterErrorOccur ?? false) || (DidRecoverableNonAutocutterErrorOccur ?? false);
public bool? DidRecoverableErrorOccur { get; set; }
public bool? DidUnrecoverableErrorOccur { get; set; }
public bool? DidAutocutterErrorOccur { get; set; }
public bool? DidRecoverableNonAutocutterErrorOccur { get; set; }
}
}

View File

@@ -0,0 +1,32 @@
using System.IO;
using System.IO.Ports;
using System.Threading.Tasks;
namespace ESCPOS_NET
{
public class SerialPrinter : BasePrinter
{
private readonly SerialPort _serialPort;
public SerialPrinter(string portName, int baudRate)
: base()
{
_serialPort = new SerialPort(portName, baudRate);
_serialPort.Open();
Writer = new BinaryWriter(_serialPort.BaseStream);
Reader = new BinaryReader(_serialPort.BaseStream);
}
protected override void OverridableDispose()
{
_serialPort?.Close();
_serialPort?.Dispose();
Task.Delay(250).Wait(); // Based on MSDN Documentation, should sleep after calling Close or some functionality will not be determinant.
}
public void Disable()
{
OverridableDispose();
}
}
}

View File

@@ -0,0 +1,9 @@
// TODO: Implement this class
namespace ESCPOS_NET
{
public class VirtualPrinter
{
public byte[] Output { get; private set; }
}
}