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,182 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
/* FROM: https://www.codeproject.com/Tips/674256/ByteArrayBuilder-a-StringBuilder-for-Bytes 2/9/2019 */
namespace ESCPOS_NET.Utilities
{
/// <summary>
/// Provides similar functionality to a StringBuilder, but for bytes.
/// </summary>
/// <remarks>
/// To fill the builder, construct a new, empty builder, and call the
/// appropriate Append method overloads.
/// To read data from the builder, either use Rewind on an existing
/// builder, or construct a new builder by passing it the byte array
/// from a previous builder - which you can get with the ToArray
/// method.
/// </remarks>
/// <example>
///
/// ByteArrayBuilder bab = new ByteArrayBuilder();
/// string[] lines = File.ReadAllLines(@"D:\Temp\myText.txt");
/// bab.Append(lines.Length);
/// foreach (string s in lines)
/// {
/// bab.Append(s);
/// }
/// byte[] data = bab.ToArray();
/// ...
/// ByteArrayBuilder babOut = new ByteArrayBuilder(data);
/// int count = bab.GetInt();
/// string[] linesOut = new string[count];
/// for (int lineNo = 0; lineNo &lt; count; lineNo++)
/// {
/// linesOut[lineNo](babOut.GetString());
/// }
/// .
/// </example>
public class ByteArrayBuilder : IDisposable
{
/// <summary>
/// True in a byte form of the Line.
/// </summary>
private const byte StreamTrue = (byte)1;
/// <summary>
/// False in the byte form of a line.
/// </summary>
private const byte StreamFalse = (byte)0;
/// <summary>
/// Holds the actual bytes.
/// </summary>
private MemoryStream store = new MemoryStream();
/// <summary>
/// Gets bytes in the store.
/// </summary>
public int Length => (int)store.Length;
/// <summary>
/// Create a new, empty builder ready to be filled.
/// </summary>
public ByteArrayBuilder()
{
}
public ByteArrayBuilder Append(byte b)
{
AddBytes(new byte[] { b });
return this;
}
/// <summary>
/// Adds an IEnumerable of bytes to an array.
/// </summary>
/// <param name="b">Value to append to existing builder data.</param>
/// </param>
public ByteArrayBuilder Append(IEnumerable<byte> b)
{
if (b is byte[])
{
AddBytes((byte[])b);
return this;
}
AddBytes(b.ToArray());
return this;
}
/// <summary>
/// Clear all content from the builder.
/// </summary>
public void Clear()
{
store.Close();
store.Dispose();
store = new MemoryStream();
}
/// <summary>
/// Rewind the builder ready to read data.
/// </summary>
public void Rewind()
{
store.Seek(0, SeekOrigin.Begin);
}
/// <summary>
/// Set an absolute position in the builder.
/// **WARNING**
/// If you add any variable size objects to the builder, the results of
/// reading after a Seek to a non-zero value are unpredictable.
/// A builder does not store just objects - for some it stores additional
/// information as well.
/// </summary>
/// <param name="position">the position to seek.</param>
public void Seek(int position)
{
store.Seek((long)position, SeekOrigin.Begin);
}
/// <summary>
/// Returns the builder as an array of bytes.
/// </summary>
/// <returns>An array.</returns>
public byte[] ToArray()
{
byte[] data = new byte[Length];
Array.Copy(store.GetBuffer(), data, Length);
return data;
}
/// <summary>
/// Returns a text based (Base64) string version of the current content.
/// </summary>
/// <returns>The converted string.</returns>
public override string ToString()
{
return Convert.ToBase64String(ToArray());
}
/// <summary>
/// Add a string of raw bytes to the store.
/// </summary>
/// <param name="byteArray">the byte array.</param>
private void AddBytes(byte[] byteArray)
{
store.Write(byteArray, 0, byteArray.Length);
}
/// <summary>
/// Reads a specific number of bytes from the store.
/// </summary>
/// <param name="length">The length.</param>
/// <returns>The byte array.</returns>
private byte[] GetBytes(int length)
{
byte[] data = new byte[length];
if (length > 0)
{
int read = store.Read(data, 0, length);
if (read != length)
{
throw new ApplicationException("Buffer did not contain " + length + " bytes");
}
}
return data;
}
/// <summary>
/// Dispose of this builder and its resources.
/// </summary>
public void Dispose()
{
store.Close();
store.Dispose();
}
}
}

View File

@@ -0,0 +1,15 @@
namespace ESCPOS_NET
{
public static class ByteExtensions
{
public static bool IsBitSet(this byte b, int offset)
{
return (b & (1 << offset)) != 0;
}
public static bool IsBitNotSet(this byte b, int offset)
{
return (b & (1 << offset)) == 0;
}
}
}

View File

@@ -0,0 +1,15 @@
namespace ESCPOS_NET.Utilities
{
public static class ByteSplicer
{
public static byte[] Combine(params byte[][] byteArrays)
{
ByteArrayBuilder builder = new ByteArrayBuilder();
foreach (var byteArray in byteArrays)
{
builder.Append(byteArray);
}
return builder.ToArray();
}
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace ESCPOS_NET.DataValidation
{
public class DataConstraint
{
public int MinLength { get; set; }
public int MaxLength { get; set; }
public List<int> ValidLengths { get; set; }
public string ValidChars { get; set; }
}
}

View File

@@ -0,0 +1,193 @@
using ESCPOS_NET.Emitters;
using System;
using System.Collections.Generic;
using System.Linq;
namespace ESCPOS_NET.DataValidation
{
public abstract class BaseDataValidator<T> where T : Enum
{
protected Dictionary<T, DataConstraint> _constraints;
public void Validate(T type, string data, BarcodeCode? code = null)
{
if (data is null)
{
throw new ArgumentNullException(nameof(data));
}
// Validate constraints on barcode.
_constraints.TryGetValue(type, out var constraints);
if (constraints is null)
{
return;
}
// Check lengths
if (constraints.ValidLengths != null)
{
if (!constraints.ValidLengths.Contains(data.Length))
{
throw new ArgumentException($"Code '{data}' is not a valid length from: [{string.Join(", ", constraints.ValidLengths)}].");
}
}
else if (data.Length < constraints.MinLength)
{
throw new ArgumentException($"Code '{data}' is shorter than minimum length {constraints.MinLength}.");
}
else if (data.Length > constraints.MaxLength)
{
throw new ArgumentException($"Code '{data}' is longer than maximum length {constraints.MaxLength}.");
}
// Check if barcode contains invalid characters.
if (constraints.ValidChars == "7BIT-ASCII")
{
if (!data.All(x => x <= 127 && x >= 0))
{
throw new ArgumentException($"Code '{data}' contained invalid characters not in: {constraints.ValidChars}.");
}
}
else if (constraints.ValidChars != null && !data.All(x => constraints.ValidChars.Contains(x)))
{
throw new ArgumentException($"Code '{data}' contained invalid characters not in: {constraints.ValidChars}.");
}
RunSpecificValidations(type, data, code);
}
protected abstract void RunSpecificValidations(T type, string data, BarcodeCode? code);
}
public static class DataValidator
{
private static BarcodeDataValidator singletonBarcode = null;
private static TwoDimensionCodeDataValidator singleton2DCode = null;
public static void ValidateBarcode(BarcodeType type, BarcodeCode code, string data)
{
if (singletonBarcode is null)
{
singletonBarcode = new BarcodeDataValidator();
}
singletonBarcode.Validate(type, data, code);
}
public static void Validate2DCode(TwoDimensionCodeType type, string data)
{
if (singleton2DCode is null)
{
singleton2DCode = new TwoDimensionCodeDataValidator();
}
singleton2DCode.Validate(type, data);
}
private class BarcodeDataValidator : BaseDataValidator<BarcodeType>
{
public BarcodeDataValidator()
{
_constraints = new Dictionary<BarcodeType, DataConstraint>()
{
{ BarcodeType.UPC_A, new DataConstraint() { MinLength = 11, MaxLength = 12, ValidChars = "0123456789" } },
{ BarcodeType.UPC_E, new DataConstraint() { ValidLengths = new List<int>() { 6, 7, 8, 11, 12 }, ValidChars = "0123456789" } },
{ BarcodeType.JAN13_EAN13, new DataConstraint() { MinLength = 12, MaxLength = 13, ValidChars = "0123456789" } },
{ BarcodeType.JAN8_EAN8, new DataConstraint() { MinLength = 7, MaxLength = 8, ValidChars = "0123456789" } },
{ BarcodeType.CODE39, new DataConstraint() { MinLength = 1, MaxLength = 255, ValidChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./" } },
{ BarcodeType.ITF, new DataConstraint() { MinLength = 2, MaxLength = 255, ValidChars = "0123456789" } },
{ BarcodeType.CODABAR_NW_7, new DataConstraint() { MinLength = 2, MaxLength = 255, ValidChars = "0123456789ABCDabcd$+-./:" } },
{ BarcodeType.CODE93, new DataConstraint() { MinLength = 1, MaxLength = 255, ValidChars = "7BIT-ASCII" } },
{ BarcodeType.CODE128, new DataConstraint() { MinLength = 1, MaxLength = 253, ValidChars = "7BIT-ASCII" } },
{ BarcodeType.GS1_128, new DataConstraint() { MinLength = 1, MaxLength = 253, ValidChars = "7BIT-ASCII" } },
{ BarcodeType.GS1_DATABAR_OMNIDIRECTIONAL, new DataConstraint() { MinLength = 13, MaxLength = 13, ValidChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./" } },
{ BarcodeType.GS1_DATABAR_TRUNCATED, new DataConstraint() { MinLength = 13, MaxLength = 13, ValidChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./" } },
{ BarcodeType.GS1_DATABAR_LIMITED, new DataConstraint() { MinLength = 13, MaxLength = 13, ValidChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./" } },
{ BarcodeType.GS1_DATABAR_EXPANDED, new DataConstraint() { MinLength = 2, MaxLength = 255, ValidChars = "0123456789ABCDabcd !\"%$'()*+,-./:;<=>?_{" } },
};
}
protected override void RunSpecificValidations(BarcodeType type, string barcode, BarcodeCode? code)
{
switch (type)
{
case BarcodeType.UPC_E:
if (barcode.Length != 6 && !barcode.StartsWith("0", StringComparison.InvariantCulture))
{
throw new ArgumentException($"UPC_E Barcode {barcode} with length of 7, 8, 11, or 12 must start with 0.");
}
break;
case BarcodeType.ITF:
if (barcode.Length % 2 != 0)
{
throw new ArgumentException($"ITF Barcode {barcode} has length {barcode.Length}, which is not an even number.");
}
break;
case BarcodeType.CODABAR_NW_7:
if (!"ABCD".Contains(barcode[0]) || !"ABCD".Contains(barcode[barcode.Length - 1]))
{
throw new ArgumentException($"CODABAR_NW_7 Barcode {barcode} must start and end with an ABCD character.");
}
if (barcode.Skip(1).Take(barcode.Length - 2).Any(x => "ABCD".Contains(x)))
{
throw new ArgumentException($"CODABAR_NW_7 Barcode {barcode} must not include ABCD characters in the body of the barcode.");
}
break;
case BarcodeType.CODE93:
if (!barcode.StartsWith("*", StringComparison.InvariantCulture) || !barcode.EndsWith("*", StringComparison.InvariantCulture))
{
throw new ArgumentException($"CODE93 Barcode {barcode} must start and end with * characters.");
}
break;
case BarcodeType.CODE128:
if (code == BarcodeCode.CODE_C)
{
if (barcode.Length % 2 != 0)
{
throw new ArgumentException($"{nameof(barcode)} length must be divisible by 2");
}
if (!barcode.All(x => x <= '9' && x >= '0'))
{
throw new ArgumentException($"Barcode {barcode} is invalid. CODE128 CODE_C barcodes only support numeric characters.");
}
}
break;
}
}
}
private class TwoDimensionCodeDataValidator : BaseDataValidator<TwoDimensionCodeType>
{
public TwoDimensionCodeDataValidator()
{
_constraints = new Dictionary<TwoDimensionCodeType, DataConstraint>()
{
{ TwoDimensionCodeType.PDF417, new DataConstraint() { MinLength = 0, MaxLength = 255 } },
{ TwoDimensionCodeType.QRCODE_MODEL1, new DataConstraint() { MinLength = 0, MaxLength = 707 } },
{ TwoDimensionCodeType.QRCODE_MODEL2, new DataConstraint() { MinLength = 0, MaxLength = 4296 } },
{ TwoDimensionCodeType.QRCODE_MICRO, new DataConstraint() { MinLength = 0, MaxLength = 21 } },
};
}
// TODO: Research specific validations for QRCode & PDF417
protected override void RunSpecificValidations(TwoDimensionCodeType type, string barcode, BarcodeCode? code)
{
if (code != null)
{
throw new ArgumentException($"Barcode code should be always null for 2D Codes.", nameof(code));
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Company>animalistic.io</Company>
<Product>ESCPOS_NET</Product>
<Authors>Luke Paireepinart</Authors>
<Description>.NET Standard 2.0 Implementation of ESC/POS command generation and integration with thermal printers.
Allows creating receipts with all common functionality supported.</Description>
<Copyright>Copyright 2021 CandL Development, LLC.</Copyright>
<PackageLicenseUrl></PackageLicenseUrl>
<RepositoryUrl>https://github.com/lukevp/ESC-POS-.NET</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>epson thermal receipt printing printer usb wifi bluetooth serial esc pos escpos escp esc/pos esc/pos.net esc_pos esc_pos_net esc_pos_usb esc_pos_usb_net</PackageTags>
<Version>2.0.0</Version>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<FileVersion>2.0.0.0</FileVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/lukevp/ESC-POS-.NET</PackageProjectUrl>
<PackageReleaseNotes>- Completely rebuilt the network printer code</PackageReleaseNotes>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0" />
<PackageReference Include="SuperSimpleTcp" Version="2.4.0" />
<PackageReference Include="System.IO.Ports" Version="4.6.0" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
using ESCPOS_NET.Emitters.BaseCommandValues;
namespace ESCPOS_NET.Emitters
{
public abstract partial class BaseCommandEmitter : ICommandEmitter
{
/* Action Commands */
public virtual byte[] FullCut() => new byte[] { Cmd.GS, Ops.PaperCut, Functions.PaperCutFullCut };
public virtual byte[] PartialCut() => new byte[] { Cmd.GS, Ops.PaperCut, Functions.PaperCutPartialCut };
public virtual byte[] FullCutAfterFeed(int lineCount) => new byte[] { Cmd.GS, Ops.PaperCut, Functions.PaperCutFullCutWithFeed, (byte)lineCount };
public virtual byte[] PartialCutAfterFeed(int lineCount) => new byte[] { Cmd.GS, Ops.PaperCut, Functions.PaperCutPartialCutWithFeed, (byte)lineCount };
}
}

View File

@@ -0,0 +1,115 @@
using ESCPOS_NET.DataValidation;
using ESCPOS_NET.Emitters.BaseCommandValues;
using ESCPOS_NET.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ESCPOS_NET.Emitters
{
public abstract partial class BaseCommandEmitter : ICommandEmitter
{
/* Barcode Commands */
public virtual byte[] PrintBarcode(BarcodeType type, string barcode, BarcodeCode code = BarcodeCode.CODE_B)
{
DataValidator.ValidateBarcode(type, code, barcode);
return BarcodeBytes(type, barcode, code);
}
protected virtual byte[] BarcodeBytes(BarcodeType type, string barcode, BarcodeCode code)
{
// For CODE128, prepend the first 2 characters as 0x7B and the CODE type, and escape 0x7B characters.
if (type == BarcodeType.CODE128)
{
if (code == BarcodeCode.CODE_C)
{
byte[] b = Encoding.ASCII.GetBytes(barcode);
byte[] ob = new byte[b.Length / 2];
for (int i = 0, obc = 0; i < b.Length; i += 2)
{
ob[obc++] = (byte)(((b[i] - '0') * 10) + (b[i + 1] - '0'));
}
barcode = Encoding.ASCII.GetString(ob);
}
barcode = barcode.Replace("{", "{{");
barcode = $"{(char)0x7B}{(char)code}" + barcode;
}
var command = new List<byte> { Cmd.GS, Barcodes.PrintBarcode, (byte)type, (byte)barcode.Length };
command.AddRange(barcode.ToCharArray().Select(x => (byte)x));
return command.ToArray();
}
public virtual byte[] PrintQRCode(string data, TwoDimensionCodeType type = TwoDimensionCodeType.QRCODE_MODEL2, Size2DCode size = Size2DCode.NORMAL, CorrectionLevel2DCode correction = CorrectionLevel2DCode.PERCENT_7)
{
if (type == TwoDimensionCodeType.PDF417)
{
throw new ArgumentException($"{nameof(TwoDimensionCodeType.PDF417)} is not a valid QRCode type. Please use {nameof(Print2DCode)} method", nameof(type));
}
return Print2DCode(type, data, size, correction);
}
public virtual byte[] Print2DCode(TwoDimensionCodeType type, string data, Size2DCode size = Size2DCode.NORMAL, CorrectionLevel2DCode correction = CorrectionLevel2DCode.PERCENT_7)
{
DataValidator.Validate2DCode(type, data);
return TwoDimensionCodeBytes(type, data, size, correction);
}
protected virtual byte[] TwoDimensionCodeBytes(TwoDimensionCodeType type, string data, Size2DCode size, CorrectionLevel2DCode correction)
{
List<byte> command = new List<byte>();
byte[] initial = { Cmd.GS, Barcodes.Set2DCode, Barcodes.PrintBarcode };
switch (type)
{
case TwoDimensionCodeType.PDF417:
command.AddRange(initial, Barcodes.SetPDF417NumberOfColumns, Barcodes.AutoEnding);
command.AddRange(initial, Barcodes.SetPDF417NumberOfRows, Barcodes.AutoEnding);
command.AddRange(initial, Barcodes.SetPDF417DotSize, (byte)size);
command.AddRange(initial, Barcodes.SetPDF417CorrectionLevel, (byte)correction);
// k = (pL + pH * 256) - 3 --> But pH is always 0.
int k = data.Length;
int l = k + 3;
command.AddRange(initial, (byte)l, Barcodes.StorePDF417Data);
command.AddRange(data.ToCharArray().Select(x => (byte)x));
// Prints stored PDF417
command.AddRange(initial, Barcodes.PrintPDF417);
break;
case TwoDimensionCodeType.QRCODE_MODEL1:
case TwoDimensionCodeType.QRCODE_MODEL2:
case TwoDimensionCodeType.QRCODE_MICRO:
command.AddRange(initial, Barcodes.SelectQRCodeModel, (byte)type, Barcodes.AutoEnding);
command.AddRange(initial, Barcodes.SetQRCodeDotSize, (byte)size);
command.AddRange(initial, Barcodes.SetQRCodeCorrectionLevel, (byte)correction);
int num = data.Length + 3;
int pL = num % 256;
int pH = num / 256;
command.AddRange(initial, (byte)pL, (byte)pH, Barcodes.StoreQRCodeData);
command.AddRange(data.ToCharArray().Select(x => (byte)x));
// Prints stored QRCode
command.AddRange(initial, Barcodes.PrintQRCode);
break;
default:
throw new NotImplementedException($"2D Code '{type}' was not implemented yet.");
}
return command.ToArray();
}
public virtual byte[] SetBarcodeHeightInDots(int height) => new byte[] { Cmd.GS, Barcodes.SetBarcodeHeightInDots, (byte)height };
public virtual byte[] SetBarWidth(BarWidth width) => new byte[] { Cmd.GS, Barcodes.SetBarWidth, (byte)width };
public virtual byte[] SetBarLabelPosition(BarLabelPrintPosition position) => new byte[] { Cmd.GS, Barcodes.SetBarLabelPosition, (byte)position };
public virtual byte[] SetBarLabelFontB(bool fontB) => new byte[] { Cmd.GS, Barcodes.SetBarLabelFont, (byte)(fontB ? 1 : 0) };
}
}

View File

@@ -0,0 +1,16 @@
using ESCPOS_NET.Emitters.BaseCommandValues;
namespace ESCPOS_NET.Emitters
{
public abstract partial class BaseCommandEmitter : ICommandEmitter
{
/* Cash Drawer Commands */
public virtual byte[] CashDrawerOpenPin2(int pulseOnTimeMs = 120, int pulseOffTimeMs = 240) {
return new byte[] { Cmd.ESC, Ops.CashDrawerPulse, 0x00, (byte) (pulseOnTimeMs / 2), (byte) (pulseOffTimeMs / 2) };
}
public virtual byte[] CashDrawerOpenPin5(int pulseOnTimeMs = 120, int pulseOffTimeMs = 240) {
return new byte[] { Cmd.ESC, Ops.CashDrawerPulse, 0x01, (byte) (pulseOnTimeMs / 2), (byte) (pulseOffTimeMs / 2) };
}
}
}

View File

@@ -0,0 +1,22 @@
using ESCPOS_NET.Emitters.BaseCommandValues;
namespace ESCPOS_NET.Emitters
{
public abstract partial class BaseCommandEmitter : ICommandEmitter
{
/* Character Commands */
public virtual byte[] SetStyles(PrintStyle style) => new byte[] { Cmd.ESC, Chars.StyleMode, (byte)style };
public virtual byte[] LeftAlign() => new byte[] { Cmd.ESC, Chars.Alignment, (byte)Align.Left };
public virtual byte[] CenterAlign() => new byte[] { Cmd.ESC, Chars.Alignment, (byte)Align.Center };
public virtual byte[] RightAlign() => new byte[] { Cmd.ESC, Chars.Alignment, (byte)Align.Right };
public virtual byte[] RightCharacterSpacing(int spaceCount) => new byte[] { Cmd.ESC, Chars.RightCharacterSpacing, (byte)spaceCount };
public virtual byte[] CodePage(CodePage codePage) => new byte[] { Cmd.ESC, Chars.CodePage, (byte)codePage };
public virtual byte[] Color(Color color) => new byte[] { Cmd.ESC, Chars.Color, (byte)color };
}
}

View File

@@ -0,0 +1,10 @@
using ESCPOS_NET.Emitters.BaseCommandValues;
namespace ESCPOS_NET.Emitters
{
public abstract partial class BaseCommandEmitter : ICommandEmitter
{
/* Display Commands */
public virtual byte[] Clear() => new byte[] { Display.CLR };
}
}

View File

@@ -0,0 +1,122 @@
using ESCPOS_NET.Emitters.BaseCommandValues;
using ESCPOS_NET.Utilities;
using SixLabors.ImageSharp;
namespace ESCPOS_NET.Emitters
{
public abstract partial class BaseCommandEmitter : ICommandEmitter
{
private byte[] GetImageHeader(int commandLength)
{
byte[] lengths = new byte[4];
int i = 0;
while (commandLength > 0)
{
lengths[i] = (byte)(commandLength & 0xFF);
commandLength >>= 8;
i++;
}
if (i >= 3)
{
return new byte[] { Cmd.GS, Images.ImageCmd8, Images.ImageCmdL, lengths[0], lengths[1], lengths[2], lengths[3] };
}
else
{
return new byte[] { Cmd.GS, Images.ImageCmdParen, Images.ImageCmdL, lengths[0], lengths[1] };
}
}
/* Image Commands */
public virtual byte[] SetImageDensity(bool isHiDPI)
{
ByteArrayBuilder builder = new ByteArrayBuilder();
byte dpiSetting = isHiDPI ? (byte)0x33 : (byte)0x32; // TODO: is this right??
byte[] baseCommand = new byte[] { 0x30, 0x31, dpiSetting, dpiSetting };
builder.Append(GetImageHeader(baseCommand.Length));
builder.Append(baseCommand);
return builder.ToArray();
}
public virtual byte[] BufferImage(byte[] image, int maxWidth = -1, bool isLegacy = false, int color = 1)
{
ByteArrayBuilder imageCommand = new ByteArrayBuilder();
byte colorByte;
switch (color)
{
case 2:
colorByte = 0x32;
break;
case 3:
colorByte = 0x33;
break;
default:
colorByte = 0x31;
break;
}
int width;
int height;
byte[] imageData;
using (var img = Image.Load(image))
{
imageData = img.ToSingleBitPixelByteArray(maxWidth: maxWidth == -1 ? (int?)null : maxWidth);
height = img.Height;
width = img.Width;
}
byte heightL = (byte)height;
byte heightH = (byte)(height >> 8);
if (isLegacy)
{
var byteWidth = (width + 7 & -8) / 8;
byte widthL = (byte)byteWidth;
byte widthH = (byte)(byteWidth >> 8);
imageCommand.Append(new byte[] { Cmd.GS, Images.ImageCmdLegacy, 0x30, 0x00, widthL, widthH, heightL, heightH });
}
else
{
byte widthL = (byte)width;
byte widthH = (byte)(width >> 8);
imageCommand.Append(new byte[] { 0x30, 0x70, 0x30, 0x01, 0x01, colorByte, widthL, widthH, heightL, heightH });
}
imageCommand.Append(imageData);
// Load image to print buffer
ByteArrayBuilder response = new ByteArrayBuilder();
byte[] imageCommandBytes = imageCommand.ToArray();
if (!isLegacy)
{
response.Append(GetImageHeader(imageCommandBytes.Length));
}
response.Append(imageCommandBytes);
return response.ToArray();
}
public virtual byte[] WriteImageFromBuffer()
{
// Print image that's already buffered.
ByteArrayBuilder response = new ByteArrayBuilder();
byte[] printCommandBytes = new byte[] { 0x30, 0x32 };
response.Append(GetImageHeader(printCommandBytes.Length));
response.Append(printCommandBytes);
return response.ToArray();
}
public virtual byte[] PrintImage(byte[] image, bool isHiDPI, bool isLegacy = false, int maxWidth = -1, int color = 1)
{
if (isLegacy)
{
return ByteSplicer.Combine(BufferImage(image, maxWidth, isLegacy));
}
else
{
return ByteSplicer.Combine(SetImageDensity(isHiDPI), BufferImage(image, maxWidth, isLegacy, color), WriteImageFromBuffer());
}
}
}
}

View File

@@ -0,0 +1,11 @@
using ESCPOS_NET.Emitters.BaseCommandValues;
namespace ESCPOS_NET.Emitters
{
public abstract partial class BaseCommandEmitter : ICommandEmitter
{
public virtual byte[] ResetLineSpacing() => new byte[] { Cmd.ESC, Whitespace.ResetLineSpacing };
public virtual byte[] SetLineSpacingInDots(int dots) => new byte[] { Cmd.ESC, Whitespace.LineSpacingInDots, (byte)dots };
}
}

View File

@@ -0,0 +1,14 @@
using ESCPOS_NET.Emitters.BaseCommandValues;
namespace ESCPOS_NET.Emitters
{
public abstract partial class BaseCommandEmitter : ICommandEmitter
{
/* Operational Commands */
public virtual byte[] Initialize() => new byte[] { Cmd.ESC, Ops.Initialize };
public virtual byte[] Enable() => new byte[] { Cmd.ESC, Ops.EnableDisable, 1 };
public virtual byte[] Disable() => new byte[] { Cmd.ESC, Ops.EnableDisable, 0 };
}
}

View File

@@ -0,0 +1,39 @@
using System.Linq;
using ESCPOS_NET.Emitters.BaseCommandValues;
namespace ESCPOS_NET.Emitters
{
public abstract partial class BaseCommandEmitter : ICommandEmitter
{
/* Printing Commands */
public virtual byte[] Print(string data)
{
// Fix OSX or Windows-style newlines
data = data.Replace("\r\n", "\n");
data = data.Replace("\r", "\n");
// TODO: Sanitize...
return data.ToCharArray().Select(x => (byte)x).ToArray();
}
public virtual byte[] PrintLine(string line)
{
if (line == null)
{
return Print("\n");
}
return Print(line.Replace("\r", string.Empty).Replace("\n", string.Empty) + "\n");
}
public virtual byte[] FeedLines(int lineCount) => new byte[] { Cmd.ESC, Whitespace.FeedLines, (byte)lineCount };
public virtual byte[] FeedLinesReverse(int lineCount) => new byte[] { Cmd.ESC, Whitespace.FeedLinesReverse, (byte)lineCount };
public virtual byte[] FeedDots(int dotCount) => new byte[] { Cmd.ESC, Whitespace.FeedDots, (byte)dotCount };
public virtual byte[] ReverseMode(bool enable) => new byte[] { Cmd.GS, Chars.ReversePrintMode, enable ? (byte)0x01 : (byte)0x00 };
public virtual byte[] UpsideDownMode(bool enable) => new byte[] { Cmd.ESC, Chars.UpsideDownMode, enable ? (byte)0x01 : (byte)0x00 };
}
}

View File

@@ -0,0 +1,18 @@
using ESCPOS_NET.Emitters.BaseCommandValues;
namespace ESCPOS_NET.Emitters
{
public abstract partial class BaseCommandEmitter : ICommandEmitter
{
/* Status Commands */
public virtual byte[] EnableAutomaticStatusBack() => new byte[] { Cmd.GS, Status.AutomaticStatusBack, 0xFF };
public virtual byte[] EnableAutomaticInkStatusBack() => new byte[] { Cmd.GS, Status.AutomaticInkStatusBack, 0xFF };
public virtual byte[] DisableAutomaticStatusBack() => new byte[] { Cmd.GS, Status.AutomaticStatusBack, 0x00 };
public virtual byte[] DisableAutomaticInkStatusBack() => new byte[] { Cmd.GS, Status.AutomaticInkStatusBack, 0x00 };
public virtual byte[] RequestPaperStatus() => new byte[] { Cmd.GS, Status.RequestStatus, 0x31 };
public virtual byte[] RequestDrawerStatus() => new byte[] { Cmd.GS, Status.RequestStatus, 0x32 };
public virtual byte[] RequestInkStatus() => new byte[] { Cmd.GS, Status.RequestStatus, 0x34 };
}
}

View File

@@ -0,0 +1,26 @@
namespace ESCPOS_NET.Emitters.BaseCommandValues
{
public static class Barcodes
{
public static readonly byte PrintBarcode = 0x6B;
public static readonly byte SetBarcodeHeightInDots = 0x68;
public static readonly byte SetBarWidth = 0x77;
public static readonly byte SetBarLabelPosition = 0x48;
public static readonly byte SetBarLabelFont = 0x66;
public static readonly byte Set2DCode = 0x28;
public static readonly byte AutoEnding = 0x00;
public static readonly byte[] SetPDF417NumberOfColumns = { 0x03, 0x00, 0x30, 0x41 };
public static readonly byte[] SetPDF417NumberOfRows = { 0x03, 0x00, 0x30, 0x42 };
public static readonly byte[] SetPDF417DotSize = { 0x03, 0x00, 0x30, 0x43 };
public static readonly byte[] SetPDF417CorrectionLevel = { 0x04, 0x00, 0x30, 0x45, 0x30 };
public static readonly byte[] StorePDF417Data = { 0x00, 0x30, 0x50, 0x30 };
public static readonly byte[] PrintPDF417 = { 0x03, 0x00, 0x30, 0x51, 0x30 };
public static readonly byte[] SelectQRCodeModel = { 0x04, 0x00, 0x31, 0x41 };
public static readonly byte[] SetQRCodeDotSize = { 0x03, 0x00, 0x31, 0x43 };
public static readonly byte[] SetQRCodeCorrectionLevel = { 0x03, 0x00, 0x31, 0x45 };
public static readonly byte[] StoreQRCodeData = { 0x31, 0x50, 0x30 };
public static readonly byte[] PrintQRCode = { 0x03, 0x00, 0x31, 0x51, 0x30 };
}
}

View File

@@ -0,0 +1,13 @@
namespace ESCPOS_NET.Emitters.BaseCommandValues
{
public static class Chars
{
public static readonly byte StyleMode = 0x21;
public static readonly byte Alignment = 0x61;
public static readonly byte ReversePrintMode = 0x42;
public static readonly byte RightCharacterSpacing = 0x20;
public static readonly byte UpsideDownMode = 0x7B;
public static readonly byte CodePage = 0x74;
public static readonly byte Color = 0x72;
}
}

View File

@@ -0,0 +1,8 @@
namespace ESCPOS_NET.Emitters.BaseCommandValues
{
public static class Cmd
{
public static readonly byte ESC = 0x1B;
public static readonly byte GS = 0x1D;
}
}

View File

@@ -0,0 +1,7 @@
namespace ESCPOS_NET.Emitters.BaseCommandValues
{
public static class Display
{
public static readonly byte CLR = 0x0C;
}
}

View File

@@ -0,0 +1,10 @@
namespace ESCPOS_NET.Emitters.BaseCommandValues
{
public static class Functions
{
public static readonly byte PaperCutFullCut = 0x00;
public static readonly byte PaperCutFullCutWithFeed = 0x41;
public static readonly byte PaperCutPartialCut = 0x01;
public static readonly byte PaperCutPartialCutWithFeed = 0x42;
}
}

View File

@@ -0,0 +1,10 @@
namespace ESCPOS_NET.Emitters.BaseCommandValues
{
public static class Images
{
public static readonly byte ImageCmdParen = 0x28;
public static readonly byte ImageCmdLegacy = 0x76;
public static readonly byte ImageCmd8 = 0x38;
public static readonly byte ImageCmdL = 0x4C;
}
}

View File

@@ -0,0 +1,10 @@
namespace ESCPOS_NET.Emitters.BaseCommandValues
{
public static class Ops
{
public static readonly byte Initialize = 0x40;
public static readonly byte EnableDisable = 0x3D;
public static readonly byte PaperCut = 0x56;
public static readonly byte CashDrawerPulse = 0x70;
}
}

View File

@@ -0,0 +1,9 @@
namespace ESCPOS_NET.Emitters.BaseCommandValues
{
public static class Status
{
public static readonly byte AutomaticStatusBack = 0x61;
public static readonly byte AutomaticInkStatusBack = 0x6A;
public static readonly byte RequestStatus = 0x72;
}
}

View File

@@ -0,0 +1,13 @@
namespace ESCPOS_NET.Emitters.BaseCommandValues
{
public static class Whitespace
{
// TODO: tabs?
public static readonly byte Linefeed = 0x0A;
public static readonly byte FeedLines = 0x64;
public static readonly byte FeedLinesReverse = 0x65;
public static readonly byte FeedDots = 0x4A;
public static readonly byte ResetLineSpacing = 0x32;
public static readonly byte LineSpacingInDots = 0x33;
}
}

View File

@@ -0,0 +1,6 @@
namespace ESCPOS_NET.Emitters
{
public class EPSON : BaseCommandEmitter
{
}
}

View File

@@ -0,0 +1,30 @@
namespace ESCPOS_NET.Emitters
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Enums are easier to read if they have whitespace alignment.")]
public enum TwoDimensionCodeType
{
PDF417 = 0,
QRCODE_MODEL1 = 49,
QRCODE_MODEL2 = 50,
QRCODE_MICRO = 51,
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Enums are easier to read if they have whitespace alignment.")]
public enum Size2DCode
{
TINY = 2,
SMALL = 3,
NORMAL = 4,
LARGE = 5,
EXTRA = 6,
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Enums are easier to read if they have whitespace alignment.")]
public enum CorrectionLevel2DCode
{
PERCENT_7 = 48,
PERCENT_15 = 49,
PERCENT_25 = 50,
PERCENT_30 = 51,
}
}

View File

@@ -0,0 +1,10 @@
namespace ESCPOS_NET.Emitters
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Enums are easier to read if they have whitespace alignment.")]
public enum Align
{
Left = 0,
Center = 1,
Right = 2,
}
}

View File

@@ -0,0 +1,11 @@
namespace ESCPOS_NET.Emitters
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Enums are easier to read if they have whitespace alignment..")]
public enum BarLabelPrintPosition
{
None = 0,
Above = 1,
Below = 2,
Both = 3,
}
}

View File

@@ -0,0 +1,12 @@
namespace ESCPOS_NET.Emitters
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Enums are easier to read if they have whitespace alignment.")]
public enum BarWidth
{
Thinnest = 2,
Thin = 3,
Default = 4,
Thick = 5,
Thickest = 6,
}
}

View File

@@ -0,0 +1,9 @@
namespace ESCPOS_NET.Emitters
{
public enum BarcodeCode
{
CODE_A = 0x41,
CODE_B = 0x42,
CODE_C = 0x43,
}
}

View File

@@ -0,0 +1,21 @@
namespace ESCPOS_NET.Emitters
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Enums are easier to read if they have whitespace alignment.")]
public enum BarcodeType // All from Function B
{
UPC_A = 0x41,
UPC_E = 0x42,
JAN13_EAN13 = 0x43,
JAN8_EAN8 = 0x44,
CODE39 = 0x45,
ITF = 0x46,
CODABAR_NW_7 = 0x47,
CODE93 = 0x48,
CODE128 = 0x49,
GS1_128 = 0x4A,
GS1_DATABAR_OMNIDIRECTIONAL = 0x4B,
GS1_DATABAR_TRUNCATED = 0x4C,
GS1_DATABAR_LIMITED = 0x4D,
GS1_DATABAR_EXPANDED = 0x4E,
}
}

View File

@@ -0,0 +1,68 @@
namespace ESCPOS_NET.Emitters
{
#pragma warning disable SA1602 // Enumeration items should be documented
#pragma warning disable SA1600 // Elements should be documented
public enum CodePage
{
PC437_USA_STANDARD_EUROPE_DEFAULT = 0,
KATAKANA = 1,
PC850_MULTILINGUAL = 2,
PC860_PORTUGUESE = 3,
PC863_CANADIAN_FRENCH = 4,
PC865_NORDIC = 5,
HIRAGANA = 6,
ONE_PASS_KANJI = 7,
ONE_PASS_KANJI2 = 8,
PC851_GREEK = 11,
PC853_TURKISH = 12,
PC857_TURKISH = 13,
PC737_GREEK = 14,
ISO8859_7_GREEK = 15,
WPC1252 = 16,
PC866_CYRILLIC2 = 17,
PC852_LATIN2 = 18,
PC858_EURO = 19,
KU42_THAI = 20,
TIS11_THAI = 21,
TIS13_THAI = 22,
TIS14_THAI = 23,
TIS16_THAI = 24,
TIS17_THAI = 25,
TIS18_THAI = 26,
TCVN3_VIETNAMESE_L = 30,
TCVN3_VIETNAMESE_U = 31,
PC720_ARABIC = 32,
WPC775_BALTIC_RIM = 33,
PC855_CYRILLIC = 34,
PC861_ICELANDIC = 35,
PC862_HEBREW = 36,
PC864_ARABIC = 37,
PC869_GREEK = 38,
ISO8859_2_LATIN2 = 39,
ISO8859_15_LATIN9 = 40,
PC1098_FARSI = 41,
PC1118_LITHUANIAN = 42,
PC1119_LITHUANIAN = 43,
PC1125_UKRANIAN = 44,
WPC1250_LATIN2 = 45,
WPC1251_CYRILLIC = 46,
WPC1253_GREEK = 47,
WPC1254_TURKISH = 48,
WPC1255_HEBREW = 49,
WPC1256_ARABIC = 50,
WPC1257_BALTIC_RIM = 51,
WPC1258_VIETNAMESE = 52,
KZ1048_KAZAKHSTAN = 53,
DEVANAGARI = 66,
BENGALI = 67,
TAMIL = 68,
TELUGU = 69,
ASSAMESE = 70,
ORIYA = 71,
KANNADA = 72,
MALAYALAM = 73,
GUJARATI = 74,
PUNJABI = 75,
MARATHI = 82,
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace ESCPOS_NET.Emitters
{
[Flags]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Enums are easier to read if they have whitespace alignment.")]
public enum Color
{
Black = 0,
Red = 1
}
}

View File

@@ -0,0 +1,16 @@
using System;
namespace ESCPOS_NET.Emitters
{
[Flags]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Enums are easier to read if they have whitespace alignment.")]
public enum PrintStyle
{
None = 0,
FontB = 1,
Bold = 1 << 3,
DoubleHeight = 1 << 4,
DoubleWidth = 1 << 5,
Underline = 1 << 7,
}
}

View File

@@ -0,0 +1,92 @@
namespace ESCPOS_NET.Emitters
{
public interface ICommandEmitter
{
/* Print Commands */
byte[] PrintLine(string line = null);
byte[] Print(string line);
byte[] FeedLines(int lineCount);
byte[] FeedLinesReverse(int lineCount);
byte[] FeedDots(int dotCount);
/* Line Spacing Commands */
byte[] ResetLineSpacing();
byte[] SetLineSpacingInDots(int dots);
/* Operational Commands */
byte[] Initialize();
byte[] Enable();
byte[] Disable();
/* Cash Drawer Commands */
byte[] CashDrawerOpenPin2(int pulseOnTimeMs = 120, int pulseOffTimeMs = 240);
byte[] CashDrawerOpenPin5(int pulseOnTimeMs = 120, int pulseOffTimeMs = 240);
/* Character Commands */
byte[] SetStyles(PrintStyle style);
byte[] LeftAlign();
byte[] RightAlign();
byte[] CenterAlign();
byte[] ReverseMode(bool activate);
byte[] RightCharacterSpacing(int spaceCount);
byte[] UpsideDownMode(bool activate);
byte[] CodePage(CodePage codePage);
byte[] Color(Color color);
/* Action Commands */
byte[] FullCut();
byte[] PartialCut();
byte[] FullCutAfterFeed(int lineCount);
byte[] PartialCutAfterFeed(int lineCount);
/* Image Commands */
byte[] SetImageDensity(bool isHiDPI);
byte[] BufferImage(byte[] image, int maxWidth, bool isLegacy = false, int color = 1);
byte[] WriteImageFromBuffer();
byte[] PrintImage(byte[] image, bool isHiDPI, bool isLegacy = false, int maxWidth = -1, int color = 1);
/* Status Commands */
byte[] EnableAutomaticStatusBack();
byte[] EnableAutomaticInkStatusBack();
/* Barcode Commands */
byte[] PrintBarcode(BarcodeType type, string barcode, BarcodeCode code = BarcodeCode.CODE_B);
byte[] PrintQRCode(string data, TwoDimensionCodeType type = TwoDimensionCodeType.QRCODE_MODEL2, Size2DCode size = Size2DCode.NORMAL, CorrectionLevel2DCode correction = CorrectionLevel2DCode.PERCENT_7);
byte[] Print2DCode(TwoDimensionCodeType type, string data, Size2DCode size = Size2DCode.NORMAL, CorrectionLevel2DCode correction = CorrectionLevel2DCode.PERCENT_7);
byte[] SetBarcodeHeightInDots(int height);
byte[] SetBarWidth(BarWidth width);
byte[] SetBarLabelPosition(BarLabelPrintPosition position);
byte[] SetBarLabelFontB(bool fontB);
/* 2D-Code Commands */
}
}

View File

@@ -0,0 +1,64 @@
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp
{
public static class ImageSharpExtensions
{
public static byte[] ToSingleBitPixelByteArray(this Image<Rgba32> image, bool rasterFormat = true, int? maxWidth = null, int? maxHeight = null, float threshold = 0.5F)
{
image.Mutate(img =>
{
if (maxWidth.HasValue || maxHeight.HasValue)
{
img.Resize(new ResizeOptions
{
Mode = ResizeMode.Max,
Size = new Size(maxWidth ?? int.MaxValue, maxHeight ?? int.MaxValue),
});
}
img.BackgroundColor(Color.White);
img.Grayscale().BinaryDither(KnownDitherings.Stucki);
if (!rasterFormat)
{
img.Rotate(RotateMode.Rotate90);
img.Flip(FlipMode.Horizontal);
}
});
var result = image.ToSingleBitPixelByteArray();
return result;
}
private static byte[] ToSingleBitPixelByteArray(this Image<Rgba32> image)
{
var bytesPerRow = (image.Width + 7 & -8) / 8;
var result = new byte[bytesPerRow * image.Height];
for (int y = 0; y < image.Height; y++)
{
var row = image.GetPixelRowSpan(y);
var rowStartPosition = y * bytesPerRow;
for (int x = 0; x < row.Length; x++)
{
if (!row[x].IsBlack())
{
continue;
}
result[rowStartPosition + (x / 8)] |= (byte)(0x01 << (7 - (x % 8)));
}
}
return result;
}
private static bool IsBlack(this Rgba32 rgba)
{
return rgba.R == 0 && rgba.G == 0 && rgba.B == 0;
}
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace ESCPOS_NET.Extensions
{
public static class TaskExtensions
{
public static void LogExceptions(this Task task)
{
task.ContinueWith(t =>
{
var aggException = t.Exception.Flatten();
foreach (var exception in aggException.InnerExceptions)
{
Logging.Logger?.LogError(exception, "Uncaught Thread Exception.");
}
},
TaskContinuationOptions.OnlyOnFaulted);
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
namespace ESCPOS_NET.Extensions
{
public static class Util
{
/// <summary>
/// Adds to the list of T items and/or enumerables of T. All other types will be ignored and not added to the list.
/// </summary>
/// <typeparam name="T">List's items type.</typeparam>
/// <param name="list">List to be added the items.</param>
/// <param name="items">Items to be added.</param>
/// <returns>True if no item was ignored, otherwise False.</returns>
public static bool AddRange<T>(this List<T> list, params object[] items)
{
bool ignoredItems = false;
foreach (var item in items)
{
if (item is T itemT)
{
list.Add(itemT);
}
else if (item is IEnumerable<T> arrayT)
{
list.AddRange(arrayT);
}
else
{
ignoredItems = true;
}
}
return !ignoredItems;
}
}
}

View File

@@ -0,0 +1,9 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "Placing usings within namespaces is not default in VS or in generated code.")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Visual Studio by default recommends removing prefixed 'this'.")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "MS style guide uses _ for private members.")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "<Pending>", Scope = "type", Target = "~T:ESCPOS_NET.Emitters.CodePage")]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Luke Paireepinart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,19 @@
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
namespace ESCPOS_NET
{
public static class Logging
{
public static ILogger Logger { get; set; }
static Logging()
{
TaskScheduler.UnobservedTaskException += (s, e) =>
{
//LogUnhandledException(e.Exception, "TaskScheduler.UnobservedTaskException");
e.SetObserved();
};
}
}
}

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

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace ESCPOS_NET.Utils
{
public static class Consts
{
public static readonly string LIBNAME = "ESCPOS_NET";
}
}

View File

@@ -0,0 +1,185 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Concurrent;
namespace ESCPOS_NET.Utils
{
public class EchoStream : Stream
{
public override bool CanTimeout { get; } = true;
public override int ReadTimeout { get; set; } = Timeout.Infinite;
public override int WriteTimeout { get; set; } = Timeout.Infinite;
public override bool CanRead { get; } = true;
public override bool CanSeek { get; } = false;
public override bool CanWrite { get; } = true;
public bool CopyBufferOnWrite { get; set; } = false;
private readonly object _lock = new object();
// Default underlying mechanism for BlockingCollection is ConcurrentQueue<T>, which is what we want
private readonly BlockingCollection<byte[]> _Buffers;
private int _maxQueueDepth = 10;
private byte[] m_buffer = null;
private int m_offset = 0;
private int m_count = 0;
private bool m_Closed = false;
public override void Close()
{
m_Closed = true;
// release any waiting writes
_Buffers.CompleteAdding();
}
public bool DataAvailable
{
get
{
return _Buffers.Count > 0;
}
}
private long _Length = 0L;
public override long Length
{
get
{
return _Length;
}
}
private long _Position = 0L;
public override long Position
{
get
{
return _Position;
}
set
{
throw new NotImplementedException();
}
}
public EchoStream() : this(10)
{
}
public EchoStream(int maxQueueDepth)
{
_maxQueueDepth = maxQueueDepth;
_Buffers = new BlockingCollection<byte[]>(_maxQueueDepth);
}
// we override the xxxxAsync functions because the default base class shares state between ReadAsync and WriteAsync, which causes a hang if both are called at once
public new Task WriteAsync(byte[] buffer, int offset, int count)
{
return Task.Run(() => Write(buffer, offset, count));
}
// we override the xxxxAsync functions because the default base class shares state between ReadAsync and WriteAsync, which causes a hang if both are called at once
public new Task<int> ReadAsync(byte[] buffer, int offset, int count)
{
return Task.Run(() =>
{
return Read(buffer, offset, count);
});
}
public override void Write(byte[] buffer, int offset, int count)
{
if (m_Closed || buffer.Length - offset < count || count <= 0)
return;
byte[] newBuffer;
if (!CopyBufferOnWrite && offset == 0 && count == buffer.Length)
newBuffer = buffer;
else
{
newBuffer = new byte[count];
System.Buffer.BlockCopy(buffer, offset, newBuffer, 0, count);
}
if (!_Buffers.TryAdd(newBuffer, WriteTimeout))
throw new TimeoutException("EchoStream Write() Timeout");
_Length += count;
}
public override int Read(byte[] buffer, int offset, int count)
{
if (count == 0)
return 0;
lock (_lock)
{
if (m_count == 0 && _Buffers.Count == 0)
{
if (m_Closed)
return -1;
if (_Buffers.TryTake(out m_buffer, ReadTimeout))
{
m_offset = 0;
m_count = m_buffer.Length;
}
else
return m_Closed ? -1 : 0;
}
int returnBytes = 0;
while (count > 0)
{
if (m_count == 0)
{
if (_Buffers.TryTake(out m_buffer, 0))
{
m_offset = 0;
m_count = m_buffer.Length;
}
else
break;
}
var bytesToCopy = (count < m_count) ? count : m_count;
System.Buffer.BlockCopy(m_buffer, m_offset, buffer, offset, bytesToCopy);
m_offset += bytesToCopy;
m_count -= bytesToCopy;
offset += bytesToCopy;
count -= bytesToCopy;
returnBytes += bytesToCopy;
}
_Position += returnBytes;
return returnBytes;
}
}
public override int ReadByte()
{
byte[] returnValue = new byte[1];
return (Read(returnValue, 0, 1) <= 0 ? -1 : (int)returnValue[0]);
}
public override void Flush()
{
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.IO;
namespace ESCPOS_NET
{
public class InterceptableWriteMemoryStream : Stream
{
private MemoryStream _innerStream = new MemoryStream();
public override bool CanRead => _innerStream.CanRead;
public override bool CanSeek => _innerStream.CanSeek;
public override bool CanWrite => _innerStream.CanWrite;
public override long Length => _innerStream.Length;
public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; }
private Action<byte[]> _writeAction { get; set; }
public InterceptableWriteMemoryStream(Action<byte[]> writeAction)
{
_writeAction = writeAction;
}
public override void Flush()
{
_innerStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
return _innerStream.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
return _innerStream.Seek(offset, origin);
}
public override void SetLength(long value)
{
_innerStream.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
var writeArray = new byte[count];
Buffer.BlockCopy(buffer, offset, writeArray, 0, count);
_writeAction(writeArray);
}
}
}

View File

@@ -0,0 +1,59 @@
using ESCPOS_NET.Utils;
using SimpleTcp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace ESCPOS_NET
{
public class TCPConnection
{
public Stream ReadStream { get; private set; } = new EchoStream();
public Stream WriteStream { get; private set; }
public event EventHandler<ClientConnectedEventArgs> Connected;
public event EventHandler<ClientDisconnectedEventArgs> Disconnected;
public bool IsConnected => _client?.IsConnected ?? false;
private SimpleTcpClient _client;
//public event EventHandler<DataReceivedEventArgs> DataReceived;
public TCPConnection(string destination)
{
_client = new SimpleTcpClient(destination);
_client.Events.Connected += ConnectedEventHandler;
_client.Events.Disconnected += DisconnectedEventHandler;
_client.Events.DataReceived += DataReceivedEventHandler;
_client.Keepalive = new SimpleTcpKeepaliveSettings() { EnableTcpKeepAlives = true, TcpKeepAliveInterval = 1, TcpKeepAliveTime = 1, TcpKeepAliveRetryCount = 3 };
ReadStream.ReadTimeout = 1500;
WriteStream = new InterceptableWriteMemoryStream(bytes => _client.Send(bytes));
}
private void ConnectedEventHandler(object sender, ClientConnectedEventArgs e)
{
Connected?.Invoke(sender, e);
}
private void DisconnectedEventHandler(object sender, ClientDisconnectedEventArgs e)
{
Disconnected?.Invoke(sender, e);
}
private void DataReceivedEventHandler(object sender, DataReceivedEventArgs e)
{
ReadStream.Write(e.Data, 0, e.Data.Length);
}
public void ConnectWithRetries(int timeoutMs)
{
_client.ConnectWithRetries(timeoutMs);
}
~TCPConnection()
{
try
{
_client.Events.DataReceived -= DataReceivedEventHandler;
_client.Events.Connected -= ConnectedEventHandler;
_client.Events.Disconnected -= DisconnectedEventHandler;
_client?.Dispose();
}
catch { }
}
}
}