Initial commit
Initial commit til Git. V2 er deployed
This commit is contained in:
@@ -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 < 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ESCPOS_NET.Emitters.BaseCommandValues
|
||||
{
|
||||
public static class Display
|
||||
{
|
||||
public static readonly byte CLR = 0x0C;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ESCPOS_NET.Emitters
|
||||
{
|
||||
public class EPSON : BaseCommandEmitter
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ESCPOS_NET.Emitters
|
||||
{
|
||||
public enum BarcodeCode
|
||||
{
|
||||
CODE_A = 0x41,
|
||||
CODE_B = 0x42,
|
||||
CODE_C = 0x43,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")]
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace ESCPOS_NET
|
||||
{
|
||||
public class ConnectionEventArgs : EventArgs
|
||||
{
|
||||
public bool IsConnected;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ESCPOS_NET
|
||||
{
|
||||
public class PrinterOptions
|
||||
{
|
||||
int IdleTimeoutSeconds { get; set; } = 60;
|
||||
int StatusPollingIntervalMilliseconds { get; set; } = 500;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// TODO: Implement this class
|
||||
|
||||
namespace ESCPOS_NET
|
||||
{
|
||||
public class VirtualPrinter
|
||||
{
|
||||
public byte[] Output { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user