diff --git a/libsharperang/Base.cs b/libsharperang/Base.cs index 370d468..0c0f760 100644 --- a/libsharperang/Base.cs +++ b/libsharperang/Base.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Vadavo.NEscPos; -using Vadavo.NEscPos.Printable; +using Crc; namespace libsharperang { public abstract class Base { @@ -12,16 +8,15 @@ namespace libsharperang { USB, Bluetooth } - public Printer printer; - public string Model { get; internal set; } + internal DataTransforms transform=new DataTransforms(); + //public Printer printer; + public string Model { get; internal set; } public string FirmwareVer { get; internal set; } public int Battery { get; internal set; } public ConnectionType ActiveConnectionType { get; internal set; } = ConnectionType.None; - - internal bool InitialiseConnection() => false; - internal bool DestroyConnection() => false; - - public Base() => InitialiseConnection(); - ~Base() => DestroyConnection(); + internal bool InitialiseConnection() => false; + internal bool DestroyConnection() => false; + public Base() => InitialiseConnection(); + ~Base() => DestroyConnection(); } } diff --git a/libsharperang/DataTransforms.cs b/libsharperang/DataTransforms.cs new file mode 100644 index 0000000..25fdf4c --- /dev/null +++ b/libsharperang/DataTransforms.cs @@ -0,0 +1,26 @@ +using System; +using Crc; + +namespace libsharperang { + public class DataTransforms { + public class CrcSum : Crc32Base { + public uint CrcKey; + public CrcSum() : base(0x77c40d4d^0x35769521, 0xffffffff, 0xffffffff, false, false) => CrcKey=0x77c40d4d; + public CrcSum(uint Key) : base(Key^0x35769521, 0xffffffff, 0xffffffff, false, false) => CrcKey=Key; + public CrcSum(uint Key, bool _) : base(Key, 0xffffffff, 0xffffffff, false, false) => CrcKey=Key; + } + public CrcSum CRC; + public bool IsCrcInitialised() => (CRC!=null); + public void InitialiseCrc() => CRC=new CrcSum(); + public void InitialiseCrc(uint Key) => CRC=new CrcSum(Key); + public void InitialiseCrc(uint Key, bool _) => CRC=new CrcSum(Key, _); + public uint GetCrcKey() { + if (!IsCrcInitialised()) InitialiseCrc(); + return CRC.CrcKey; + } + public byte[] GetCrcKeyBytes() { + if (!IsCrcInitialised()) InitialiseCrc(); + return BitConverter.GetBytes(GetCrcKey()); + } + } +} diff --git a/libsharperang/IPrinter.cs b/libsharperang/IPrinter.cs new file mode 100644 index 0000000..ae66c44 --- /dev/null +++ b/libsharperang/IPrinter.cs @@ -0,0 +1,12 @@ +using LibUsbDotNet; + +namespace libsharperang { + interface IPrinter { + UsbDevice uDv { get; set; } + UsbEndpointWriter uWr { get; set; } + UsbEndpointReader uRd { get; set; } + bool Initialised(); + bool WriteBytes(byte[]Frame); + byte[] ReadBytes(); + } +} diff --git a/libsharperang/IUsb.cs b/libsharperang/IUsb.cs deleted file mode 100644 index b0f4da2..0000000 --- a/libsharperang/IUsb.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using LibUsbDotNet; -using Vadavo.NEscPos.Connectors; - -namespace libsharperang -{ - public class IUsb : IPrinterConnector - { - internal UsbDevice iPrinter; - private UsbEndpointReader pReader; - private UsbEndpointWriter pWriter; - public bool Initialised() => (bool)iPrinter?.IsOpen; - public void Dispose() - { - pReader?.Dispose(); - pWriter?.Dispose(); - IUsbDevice p = iPrinter as IUsbDevice; - _ = p?.ReleaseInterface(0); - _ = iPrinter?.Close(); - throw new NotImplementedException(); - } - - public byte[] Read() - { - if (pReader == null) pReader = iPrinter?.OpenEndpointReader(LibUsbDotNet.Main.ReadEndpointID.Ep01); - byte[] bRead = new byte[1024]; - _ = pReader.Read(bRead, 100, out int _); - return bRead; - } - - public void Write(byte[] data) - { - if (pWriter == null) pWriter = iPrinter?.OpenEndpointWriter(LibUsbDotNet.Main.WriteEndpointID.Ep01); - pWriter?.Write(data, 100, out int _); - } - } -} diff --git a/libsharperang/libsharperang.csproj b/libsharperang/libsharperang.csproj index 3c6eac6..8695c91 100644 --- a/libsharperang/libsharperang.csproj +++ b/libsharperang/libsharperang.csproj @@ -5,11 +5,8 @@ - + - - - diff --git a/libsharperang/usb.cs b/libsharperang/usb.cs index b16c1a4..d465c58 100644 --- a/libsharperang/usb.cs +++ b/libsharperang/usb.cs @@ -1,99 +1,207 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using LibUsbDotNet; using LibUsbDotNet.Main; -using Vadavo.NEscPos; + + +/* + * the official software seems to transmit a CRC "token" on first detecting the printer (Paperang P1; MiaoMiaoJi): + * Frame.Begin, PrinterCmd.TransmitCrc, 0x4d0dc477, 0x699295ed, Frame.End + * This differs when connected to a Paperang P2: + * Frame.Begin, PrinterCmd.TransmitCrc, 0x18d2a3df, 0x4ec92265, Frame.End + * + * It's not immediately clear to me whether the segment normally containing the CRC is itself a CRC of the token + * or if the token is simply 8 bytes rather than 4 and the CRC segment of the frame is omitted for this operation + * + * From what I can tell, calculation of a CRC32 checksum can involve a polynomial and a 'check' value. + * It may thus be possible that the Data value is the 'check' value, and the CRC value is the polynomial.. + * In ihciah/miaomiaoji:message_process:crc32(data), the CRC32 value is generated by the zlib.crc32(d,v) function + * however I've been unable to reproduce a checksum of 0x906f4576 from a starting value of 0x00 and a polynomial + * of any variation of 0x4d0dc477, 0x699295ed + * Disassembly of the official software indicates it -is- crc32 that's in use, however further investigation is needed + * Finally, it's not clear whether this value transmitted is required to remain the same at all times + * or if the software may use whatever value it wants (whether this is simply a handshake, or whether the specific + * value "unlocks" communication with the printer + * + * standard presence checks performed by the official software consist of simply sending two messages every two seconds: + * Frame.Begin, PrinterCmd.SessionStart, 0x0000, 0x906f4576, Frame.End + * Frame.Begin, PrinterCmd.SessionEnd, 0x00, 0x906f4576, Frame.End + * + * printing out a blank page yields a 1018-byte payload formed of: + * Frame.Begin, PrinterCmd.BeginPrint, 0x00*1008, 0x83c0e20f, Frame.End + * followed by a 634-byte payload formed of: + * Frame.Begin, PrinterCmd.ContinuePrint, 0x00*624, 0xbc1b6bf4, Frame.End + * followed by a 12-byte payload formed of: + * Frame.Begin, PrinterCmd.SessionEnd, 0x2c01, 0xa8347338, Frame.end + * + * printing out a single dash ('-') yields a 1018-byte payload formed of: + * Frame.Begin, PrinterCmd.BeginPrint, 0x00*858, 0x0fe0, 0x00*46, 0xfe0, 0x00*94, 0xc53ad770, Frame.End + * followed by a 634-byte payload formed of: + * Frame.Begin, PrinterCmd.ContinuePrint, 0x00*624, 0xbc1b6bf4, Frame.End + * followed by a 12-byte payload formed of: + * Frame.Begin, PrinterCmd.SessionEnd, 0x2c01, 0xa8347338, Frame.end + * + * based on the above we can extrapolate that: + * - 12 bytes is the minimum size of a payload, and 1018 bytes is the maximum + * - all frames sent to the printer follow the structure of: + * -- Frame.Begin (1 byte) + * -- PrinterCmd.* (4 bytes) + * -- Data (min. 2 bytes? max. 1008 bytes) + * -- CRC32 sum of the Data block (4 bytes) + * -- Frame.End (1 byte) + * + * Also of interest is that, when a model P2 is connected, the printer will actually reply to messages + * eg., for the standard presence check of Frame.Begin, 0x1a000200, 0x0000, CRC, Frame.End + * the printer will reply with an almost identical message: + * Frame.Begin, 0x1a000100, 0x00, CRC, Frame.End + * The Data segment is a different size (one byte) and the printer appears to be doing its own CRC calculation + * Finally, the command, 0x1a000100, appears to be an acknowledgement of sorts in response to PrinterCmd.SessionEnd (byte 3 changing from 0x02 to 0x01) + */ namespace libsharperang { - public class USB : Base { - //both P1 and P2 models share the same idV and idP over USB - public readonly ushort idVendor=0x4348; - public readonly ushort idProduct=0x5584; - //differentiating between the two is a case of checking the product name - //P1 is identified as "MiaoMiaoji" or "MTTII Printer" - //P2 is identified as "Paperang_P2" - //Notable is that the idV and idP appear to belong not to Paperang, but to a generic CH34x printer adaptor device - // so it should be a constraint to check that the device descriptors match known details for supported devices, - // not just idV and idP - public UsbDevice pDevice; - public List pInstances; - public List pIds; - public IUsb pUsb; + public class Frame { + public DataTransforms transformer=new DataTransforms(); + public enum Opcode { + SessionBegin = 10, + SessionEnd, + PrintBegin = 20, + PrintContinue, + CrcTransmit = 30 + } + private byte[] ResolveOpcode(Opcode opcode) { + switch (opcode) { + case Opcode.SessionBegin: return new byte[] { 0x06, 0x00, 0x02, 0x00 }; + case Opcode.SessionEnd: return new byte[] { 0x1a, 0x00, 0x02, 0x00 }; + case Opcode.PrintBegin: return new byte[] { 0x00, 0x01, 0xf0, 0x03 }; + case Opcode.PrintContinue: return new byte[] { 0x00, 0x01, 0xf0, 0x02 }; + case Opcode.CrcTransmit: return new byte[] { 0x18, 0x01, 0x04, 0x00 }; + default: throw new NullReferenceException(); + } + } + private byte[] SwapEndianness(uint value) { + uint b1 = (value >> 0) & 0xff; + uint b2 = (value >> 8) & 0xff; + uint b3 = (value >> 16) & 0xff; + uint b4 = (value >> 24) & 0xff; + return BitConverter.GetBytes((b1 << 24) | (b2 << 16) | (b3 << 8) | (b4 << 0)); + } + public byte[] Build(Opcode opcode, byte[] data) => Build(opcode, data, transformer); + public byte[] Build(Opcode opcode, byte[] data, DataTransforms transformer) { + if (!transformer.IsCrcInitialised()) transformer.InitialiseCrc(); + byte[] result=new byte[data.Length+10]; + result[0]=0x02; + result[result.Length-1]=0x03; + Buffer.BlockCopy(ResolveOpcode(opcode), 0, result, 1, 4); + Buffer.BlockCopy(data, 0, result, 5, data.Length); + Buffer.BlockCopy(transformer.CRC.ComputeHash(data), 0, result, result.Length-5, 4); + return result; + } + public byte[] BuildTransmitCrc() { + DataTransforms _=new DataTransforms(); + _.InitialiseCrc(0x35769521, true); + return Build(Opcode.CrcTransmit, transformer.GetCrcKeyBytes(), _); + } + } + public class USBPrinter : Base, IPrinter { + private UsbDevice _uDv; + private UsbEndpointWriter _uWr; + private UsbEndpointReader _uRd; + UsbDevice IPrinter.uDv { get => _uDv; set { } } + UsbEndpointWriter IPrinter.uWr { get => _uWr; set { } } + UsbEndpointReader IPrinter.uRd { get => _uRd; set { } } - public USB() { + private ushort idVendor=0x4348; + private ushort idProduct=0x5584; + public Frame builder=new Frame(); + public List IDs; + public List Devices; + + public USBPrinter() { ActiveConnectionType=ConnectionType.USB; - InitUSB(); + Initialise(); } - ~USB() => CloseUSB(); - - new internal bool InitialiseConnection() => InitUSB(); - - - public bool InitUSB() { - //NOTE - in order for this to work, you must use Zadig to change the driver for - // the printer from usbprint to WinUSB - //On MacOS and Linux libusb should "just work" here - testing is pending however. - if (UsbDevice.AllDevices.Count == 0) return false; - //genuinely just having some fun with Linq, don't judge me. - //TODO: work out a way of distinguishing multiple devices with same GUID - // which happens when two of the same device are connected - // the only thing that differs seemingly is the Address - pInstances = (from d in UsbDevice.AllDevices - where d.Vid == idVendor && d.Pid == idProduct - select d) - .ToList(); - pIds = (from d in pInstances - select d.DeviceInterfaceGuids[0]) - .Distinct() - .ToList(); - return (pIds.Count > 0); + ~USBPrinter() { + Close(); + ActiveConnectionType=ConnectionType.None; } + + public void Initialise() { + if (UsbDevice.AllDevices.Count == 0) throw new KeyNotFoundException("No WinUSB or LibUSB devices found"); + Devices=(from d in UsbDevice.AllDevices + where d.Vid == idVendor && d.Pid == idProduct + select d) + .ToList(); + if (Devices.Count == 0) throw new KeyNotFoundException("No supported devices found!"); + IDs=(from d in Devices + select d.DeviceInterfaceGuids[0]) + .Distinct() + .ToList(); + } + public List GetAddressesFromGuid(Guid deviceId) => (from d in UsbDevice.AllDevices - where d.DeviceInterfaceGuids.Contains(deviceId) - select d.DeviceProperties.Where(k => k.Key=="Address").FirstOrDefault().Value).ToList().ConvertAll(v => (int)v); - public bool OpenUSB() { - if (UsbDevice.AllDevices.Count == 0) return false; - if (pIds.Count == 0) return false; - return OpenUSB(pIds.FirstOrDefault()); - } - public bool OpenUSB(Guid deviceId) => OpenUSB(deviceId, GetAddressesFromGuid(deviceId).FirstOrDefault()); - public bool OpenUSB(Guid deviceId, int deviceIndex) { - //first thought is to say "forgive me lord for i have sinned" but i am absolutely not repentant for this - bool OpenResult = (from d in UsbDevice.AllDevices - where d.DeviceInterfaceGuids.Contains(deviceId) && - d.DeviceProperties.Where(k=>k.Key=="Address" && (int)k.Value==deviceIndex).Count() > 0 - select d) - .FirstOrDefault() - .Open(out UsbDevice handle); - pDevice=handle; - return OpenResult; - } - - public bool ClaimUSB() - { - if (pDevice is null || pDevice.IsOpen) return false; - IUsbDevice p = pDevice as IUsbDevice; - p?.SetConfiguration(1); - p?.ClaimInterface(0); - pUsb.iPrinter = pDevice; - printer = new Printer(pUsb); - return true; - } - - public bool CloseUSB() - { - pUsb?.Dispose(); - pDevice?.Close(); - return true; - } - - public string FoundPrinterGuids() => pIds + where d.DeviceInterfaceGuids.Contains(deviceId) + select d.DeviceProperties + .Where(k => k.Key=="Address") + .FirstOrDefault().Value) + .ToList().ConvertAll(v => (int)v); + public string FoundPrinterGuids() => IDs? .ConvertAll(p => p.ToString()) .Aggregate((a, b) => a+","+b); public string FoundPrinterGuidAddrs(Guid deviceId) => GetAddressesFromGuid(deviceId) .ConvertAll(a => a.ToString()) .Aggregate((a, b) => a+","+b); - public string FoundProdIds() => pDevice?.Info.ProductString; + public string FoundProdIds() => _uDv?.Info.ProductString; + public bool Open() { + if (UsbDevice.AllDevices.Count == 0 || + !IsPrinterPresent()) return false; + return Open(IDs.FirstOrDefault()); + } + public bool Open(Guid deviceId) => Open(deviceId, + GetAddressesFromGuid(deviceId).FirstOrDefault()); + public bool Open(Guid deviceId, int deviceIndex) { + if (!IsPrinterPresent()) return false; + //first thought is to say "forgive me lord for i have sinned" but i am absolutely not repentant for this + bool OpenResult = (from d in UsbDevice.AllDevices + where d.DeviceInterfaceGuids.Contains(deviceId) && + d.DeviceProperties.Where(k=>k.Key=="Address" && (int)k.Value==deviceIndex).Count() > 0 + select d) + .FirstOrDefault() + .Open(out UsbDevice handle); + _uDv=handle; + return OpenResult; + } + public bool Claim() { + if (!IsPrinterPresent() || + _uDv is null || + !_uDv.IsOpen) return false; + IUsbDevice p = _uDv as IUsbDevice; + p?.SetConfiguration(1); + p?.ClaimInterface(0); + return true; + } + public void Close() { + _uWr?.Dispose(); + _uRd?.Dispose(); + IUsbDevice p=_uDv as IUsbDevice; + _=p?.ReleaseInterface(0); + _=p?.Close(); + _uDv?.Close(); + } + public bool IsPrinterPresent() => (Devices != null && Devices.Count > 0); + bool IPrinter.Initialised() => Initialised(); + byte[] IPrinter.ReadBytes() => ReadBytes(); + bool IPrinter.WriteBytes(byte[] Frame) => WriteBytes(Frame); + public bool Initialised() => (_uDv!=null); + public byte[] ReadBytes() { + if (_uRd==null) _uRd=_uDv?.OpenEndpointReader(ReadEndpointID.Ep01); + byte[] bRead=new byte[1024]; + _=_uRd.Read(bRead, 100, out int _); + return bRead; + } + public bool WriteBytes(byte[] Frame) { + if (_uWr==null) _uWr=_uDv?.OpenEndpointWriter(WriteEndpointID.Ep02); + return (_uWr.Write(Frame, 100, out int _) == ErrorCode.None); + } } -} +} \ No newline at end of file diff --git a/sharperang/App.xaml.cs b/sharperang/App.xaml.cs index e3b1843..b3977a9 100644 --- a/sharperang/App.xaml.cs +++ b/sharperang/App.xaml.cs @@ -1,17 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; +using System.Windows; -namespace sharperang -{ - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application - { - } +namespace sharperang { + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application { + } } diff --git a/sharperang/MainWindow.xaml b/sharperang/MainWindow.xaml index 70fea8c..a7c1f99 100644 --- a/sharperang/MainWindow.xaml +++ b/sharperang/MainWindow.xaml @@ -1,11 +1,11 @@  + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:local="clr-namespace:sharperang" + mc:Ignorable="d" + Title="MainWindow" Height="450" Width="800"> @@ -16,6 +16,7 @@