diff --git a/MultiTerm.Protocols/Helpers/ServiceExtensions.cs b/MultiTerm.Protocols/Helpers/ServiceExtensions.cs index 59de2c0..d1cb97c 100644 --- a/MultiTerm.Protocols/Helpers/ServiceExtensions.cs +++ b/MultiTerm.Protocols/Helpers/ServiceExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using MultiTerm.Protocols.Factories; using MultiTerm.Protocols.Serial; +using MultiTerm.Protocols.UsbHid; namespace MultiTerm.Protocols.Helpers; @@ -16,10 +17,12 @@ public static class ServiceExtensions { // add all protocol implementations to the services collection services.AddTransient(); + services.AddTransient(); // TODO extend // add all settings view model implementations to the services collection services.AddTransient(); + services.AddTransient(); // TODO extend // add a function to the services collection, which is used by the CommunicationProtocolFactory diff --git a/MultiTerm.Protocols/MultiTerm.Protocols.csproj b/MultiTerm.Protocols/MultiTerm.Protocols.csproj index cfcec65..6e53b42 100644 --- a/MultiTerm.Protocols/MultiTerm.Protocols.csproj +++ b/MultiTerm.Protocols/MultiTerm.Protocols.csproj @@ -6,8 +6,27 @@ enable + + + + + + + + PreserveNewest + + %(Filename)%(Extension) + + + PreserveNewest + + %(Filename)%(Extension) + + + + diff --git a/MultiTerm.Protocols/Serial/ISerialProtocolSettings.cs b/MultiTerm.Protocols/Serial/ISerialProtocolSettings.cs index 300b34e..ad19f31 100644 --- a/MultiTerm.Protocols/Serial/ISerialProtocolSettings.cs +++ b/MultiTerm.Protocols/Serial/ISerialProtocolSettings.cs @@ -5,31 +5,31 @@ public interface ISerialProtocolSettings : IProtocolSettings /// /// Port for communication, e.g. COM3 or com50. /// - string PortName { get; set; } + string PortName { get; internal set; } /// /// Serial Baud rate. /// - int BaudRate { get; set; } + int BaudRate { get; } /// /// The type of parity to use. /// - Parity Parity { get; set; } + Parity Parity { get; } /// /// The standard length of data bits per byte. /// - int DataBits { get; set; } + int DataBits { get; } /// /// Number of stop bits to use. /// - StopBits StopBits { get; set; } + StopBits StopBits { get; } /// /// Handshaking protocol for serial port transmission of data. /// - Handshake Handshake { get; set; } + Handshake Handshake { get; } } diff --git a/MultiTerm.Protocols/Serial/SerialProtocol.cs b/MultiTerm.Protocols/Serial/SerialProtocol.cs index 09b78d0..fed8d56 100644 --- a/MultiTerm.Protocols/Serial/SerialProtocol.cs +++ b/MultiTerm.Protocols/Serial/SerialProtocol.cs @@ -19,14 +19,14 @@ public class SerialProtocol : CommunicationProtocol public SerialProtocol(ILogger logger, IMessenger messenger) : base(logger, messenger) { } - protected override bool InternalConnect(IProtocolSettings protocolSettings) + protected override bool InternalConnect(IProtocolSettings settings) { // check if settings are of correct type - if (protocolSettings is not ISerialProtocolSettings serialProtocolSettings) + if (settings is not ISerialProtocolSettings serialProtocolSettings) { this.serialSettings = null; throw new ArgumentException($"Cannot connect due to wrong type of Protocol Settings. " + - $"Check parameter {nameof(protocolSettings)}' of '{nameof(InternalConnect)}()' in {nameof(SerialProtocol)}."); + $"Check parameter {nameof(settings)}' of '{nameof(InternalConnect)}()' in {nameof(SerialProtocol)}."); } // store locally @@ -55,7 +55,7 @@ public class SerialProtocol : CommunicationProtocol // try opening serial port try { - serialPort.Open(); + this.serialPort.Open(); } catch (Exception ex) { @@ -67,7 +67,7 @@ public class SerialProtocol : CommunicationProtocol protected override void InternalDisconnect() { - serialPort.Close(); + this.serialPort.Close(); this.serialSettings = null; } @@ -76,7 +76,7 @@ public class SerialProtocol : CommunicationProtocol while(ct.IsCancellationRequested == false) { // reads character based on configured encoding (here ASCII) - int readByte = serialPort.ReadByte(); + int readByte = this.serialPort.ReadByte(); if (readByte != -1) // -1 = end of stream { // report new data with event @@ -91,7 +91,7 @@ public class SerialProtocol : CommunicationProtocol { try { - serialPort.WriteByte(b); + this.serialPort.WriteByte(b); } // When the Serial Port is closed and InvalidOperationException is thrown => report error catch(InvalidOperationException) diff --git a/MultiTerm.Protocols/UsbHid/IUsbHidProtocolSettings.cs b/MultiTerm.Protocols/UsbHid/IUsbHidProtocolSettings.cs new file mode 100644 index 0000000..a5953e7 --- /dev/null +++ b/MultiTerm.Protocols/UsbHid/IUsbHidProtocolSettings.cs @@ -0,0 +1,20 @@ +namespace MultiTerm.Protocols.UsbHid; + +public interface IUsbHidProtocolSettings : IProtocolSettings +{ + /// + /// Vendor ID of the target device. 16bit number. + /// + ushort VendorId { get; } + + /// + /// Product ID of the target device. 16bit number. + /// + ushort ProductId { get; } + + /// + /// Serial number of the target device. + /// If null, not used to connect. + /// + string SerialNumber { get; set; } +} diff --git a/MultiTerm.Protocols/UsbHid/UsbHidDeviceInfo.cs b/MultiTerm.Protocols/UsbHid/UsbHidDeviceInfo.cs new file mode 100644 index 0000000..fe13112 --- /dev/null +++ b/MultiTerm.Protocols/UsbHid/UsbHidDeviceInfo.cs @@ -0,0 +1,12 @@ +namespace MultiTerm.Protocols.UsbHid; + +public class UsbHidDeviceInfo +{ + public string VendorId { get; internal set; } = string.Empty; + + public string ProductId { get; internal set; } = string.Empty; + + public string Manufacturer { get; internal set; } = string.Empty; + + public string SerialNumber { get; internal set; } = string.Empty; +} diff --git a/MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs b/MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs new file mode 100644 index 0000000..99be545 --- /dev/null +++ b/MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs @@ -0,0 +1,168 @@ +using Common.Logging; +using CommunityToolkit.Mvvm.Messaging; +using HidApi; +using MultiTerm.Protocols.Model; +using MultiTerm.Protocols.Types; +using System.Diagnostics; + +namespace MultiTerm.Protocols.UsbHid; + +public class UsbHidProtocol : CommunicationProtocol +{ + public override ProtocolType ProtocolType => ProtocolType.UsbHid; + + public override string InstanceIdentifier { get; protected set; } = string.Empty; + + private const uint ReadTimeoutMs = 100; // timeout after which a new read cycle is started + private const uint MaxBytesPerReport = 100; // maximum number of bytes to send per report, including report ID and pther prefixes + + private IUsbHidProtocolSettings? usbHidSettings; + private Device? usbHidDevice = null; + + + public UsbHidProtocol(ILogger logger, IMessenger messenger) : base(logger, messenger) { } + + protected override bool InternalConnect(IProtocolSettings settings) + { + // check if settings are of correct type + if (settings is not IUsbHidProtocolSettings usbHidProtocolSettings) + { + this.usbHidSettings = null; + throw new ArgumentException($"Cannot connect due to wrong type of Protocol Settings. " + + $"Check parameter {nameof(settings)}' of '{nameof(InternalConnect)}()' in {nameof(UsbHidProtocol)}."); + } + + // store locally + this.usbHidSettings = usbHidProtocolSettings; + + // update Identifier + this.InstanceIdentifier = $"V:{this.usbHidSettings.VendorId:X4} P:{this.usbHidSettings.ProductId:X4}"; + + // try create usb hid device + try + { + // if serial number is emtpy => connect without serial number + if (string.IsNullOrEmpty(this.usbHidSettings.SerialNumber)) + { + this.usbHidDevice = new Device(this.usbHidSettings.VendorId, this.usbHidSettings.ProductId); + } + else + { + this.usbHidDevice = new Device(this.usbHidSettings.VendorId, this.usbHidSettings.ProductId, this.usbHidSettings.SerialNumber); + } + } + catch (Exception ex) + { + this.logger.LogException(ex, $"'{nameof(InternalConnect)}()'Opening USB HID Device failed:", nameof(UsbHidProtocol)); + this.usbHidDevice = null; + } + + // device not found or had exception + if (usbHidDevice == null) + { + return false; + } + + return true; + } + + protected override void InternalDisconnect() + { + this.usbHidDevice?.Dispose(); + this.usbHidDevice = null; + this.usbHidSettings = null; + } + + protected override void InternalRead(CancellationToken ct) + { + while (ct.IsCancellationRequested == false) + { + ReadOnlySpan readData; + if (this.usbHidDevice != null) + { + // read with timeout + readData = this.usbHidDevice.ReadTimeout(200, (int)ReadTimeoutMs); + // any data received? + if(readData.Length > 0) + { + foreach (var readByte in readData) + { + // report new byte with event + this.OnReceivedData(new ExtendedByte((byte)readByte)); + } + } + } + // if usb hid device is null => something is wrong => break loop + else + { + this.OnUnintentionallyDisconnected(); + break; + } + } + } + + protected override bool InternalSendBytes(byte[] bytes) + { + List bytesToSend = new(bytes); + int numBytesToTake = (int)MaxBytesPerReport - 2; + + Debug.WriteLine($"{nameof(InternalSendBytes)}() of {nameof(UsbHidProtocol)} has {bytesToSend.Count} bytes to send."); + + while(bytesToSend.Count > 0) + { + // take from the list the amount of bytes to send. ToList creates a shallow copy. + //bytesToSend.CopyTo(0, nextBytes, 0, Math.Min(numBytesToTake, bytesToSend.Count)); + var nextBytes = bytesToSend.Take(numBytesToTake).ToList(); + // remove from start of original list either; taken amount of bytes or remaining count of bytes in list + bytesToSend.RemoveRange(0, Math.Min(numBytesToTake, bytesToSend.Count)); + + Debug.WriteLine($"{nameof(InternalSendBytes)}() of {nameof(UsbHidProtocol)} took {nextBytes.Count()} bytes and has {bytesToSend.Count} left on queue."); + + // check number of bytes to send + int numBytesToSend = nextBytes.Count(); + if(numBytesToSend > MaxBytesPerReport || numBytesToSend > byte.MaxValue || numBytesToSend <= 0) + { + throw new Exception($"'{nameof(InternalSendBytes)}()': Invalid of bytes to send ({nameof(numBytesToSend)})"); + } + + // create list with sendable bytes + // Structure: (1Byte)Report Id, (1Byte)Payload Num Bytes, (n Bytes)Payload + List sendableBytes = new() { 0x00, (byte)numBytesToSend }; + sendableBytes.AddRange(nextBytes); + + // if usb hid device is null => something is wrong => quit sending + if (this.usbHidDevice == null) + { + this.OnUnintentionallyDisconnected(); + return false; + } + + this.usbHidDevice.Write(bytes); + + // report sent bytes + foreach (byte b in bytes) + { + this.OnSentData(new ExtendedByte(b)); + } + } + + return true; + } + + public static IEnumerable GetDevices() + { + var libDevicesInfo = Hid.Enumerate(); + var usbHidDevicesInfo = new List(); + foreach (var deviceInfo in libDevicesInfo) + { + usbHidDevicesInfo.Add(new UsbHidDeviceInfo + { + VendorId = $"{deviceInfo.VendorId:X4}", + ProductId = $"{deviceInfo.ProductId:X4}", + Manufacturer = deviceInfo.ManufacturerString, + SerialNumber = deviceInfo.SerialNumber + }); + } + return usbHidDevicesInfo; + } +} diff --git a/MultiTerm.Protocols/UsbHid/UsbHidProtocolSettingsViewModel.cs b/MultiTerm.Protocols/UsbHid/UsbHidProtocolSettingsViewModel.cs new file mode 100644 index 0000000..76f6cea --- /dev/null +++ b/MultiTerm.Protocols/UsbHid/UsbHidProtocolSettingsViewModel.cs @@ -0,0 +1,100 @@ +using Common.Messaging; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using MultiTerm.Protocols.Types; +using System.Collections.ObjectModel; + +namespace MultiTerm.Protocols.UsbHid; + +public partial class UsbHidProtocolSettingsViewModel : ProtocolSettingsViewModel, IUsbHidProtocolSettings +{ + public override ProtocolType ProtocolType => ProtocolType.UsbHid; + + public ushort VendorId { get; set; } = ushort.MinValue; + public ushort ProductId { get; set; } = ushort.MinValue; + + [ObservableProperty] + private string serialNumber = string.Empty; + + [ObservableProperty] + private string vendorIdHex = string.Empty; + + [ObservableProperty] + private string productIdHex = string.Empty; + + + [ObservableProperty] + private ObservableCollection devices = new(); + + [ObservableProperty] + private UsbHidDeviceInfo? selectedDevice; + + [RelayCommand] + private void ReloadDevices() + { + this.Devices = new ObservableCollection(UsbHidProtocol.GetDevices()); + + // select first deivce, if none is selected yet and there are some available + if (this.SelectedDevice == null && this.Devices.Any()) + { + this.SelectedDevice = this.Devices.First(); + } + } + + public UsbHidProtocolSettingsViewModel(IMessenger messenger) : base(messenger) + { + // load devices initially + this.ReloadDevices(); + this.SelectedDevice = this.Devices.FirstOrDefault(); + } + + /// + /// Updates other fields when was changed. + /// + /// + partial void OnSelectedDeviceChanged(UsbHidDeviceInfo? value) + { + if(value != null) + { + this.VendorIdHex = value.VendorId; + this.ProductIdHex = value.ProductId; + this.SerialNumber = value.SerialNumber; + } + } + + public bool AreValid() + { + // pad left (fill up zeroes) + this.VendorIdHex = this.VendorIdHex.PadLeft(4, '0'); + if (this.VendorIdHex.Length > 4) + { + this.messenger.Send(new ProtocolSettingsInvalidMessage(nameof(this.VendorId), "Must consist of a maximum of 4 hexadecimal characters")); + return false; + } + if (ushort.TryParse(this.VendorIdHex, System.Globalization.NumberStyles.HexNumber, null, out ushort vendorId) == false) + { + this.messenger.Send(new ProtocolSettingsInvalidMessage(nameof(this.VendorId), "Invalid. Not a 16bit number.")); + return false; + } + // set internal + this.VendorId = vendorId; + + // pad left (fill up zeroes) + this.ProductIdHex = this.ProductIdHex.PadLeft(4, '0'); + if (this.ProductIdHex.Length > 4) + { + this.messenger.Send(new ProtocolSettingsInvalidMessage(nameof(this.ProductId), "Must consist of a maximum of 4 hexadecimal characters")); + return false; + } + if (ushort.TryParse(this.ProductIdHex, System.Globalization.NumberStyles.HexNumber, null, out ushort productId) == false) + { + this.messenger.Send(new ProtocolSettingsInvalidMessage(nameof(this.ProductId), "Invalid. Not a 16bit number.")); + return false; + } + // set internal + this.ProductId = productId; + + return true; + } +} diff --git a/MultiTerm.Protocols/UsbHid/hidapi.dll b/MultiTerm.Protocols/UsbHid/hidapi.dll new file mode 100644 index 0000000..55a11e9 Binary files /dev/null and b/MultiTerm.Protocols/UsbHid/hidapi.dll differ diff --git a/MultiTerm.Protocols/UsbHid/hidapi.lib b/MultiTerm.Protocols/UsbHid/hidapi.lib new file mode 100644 index 0000000..c8dd7f7 Binary files /dev/null and b/MultiTerm.Protocols/UsbHid/hidapi.lib differ diff --git a/MultiTerm.Wpf/View/SendReceiveView.xaml b/MultiTerm.Wpf/View/SendReceiveView.xaml index 91988b1..c4f7a55 100644 --- a/MultiTerm.Wpf/View/SendReceiveView.xaml +++ b/MultiTerm.Wpf/View/SendReceiveView.xaml @@ -7,6 +7,7 @@ xmlns:local="clr-namespace:MultiTerm.Wpf.View" xmlns:conv="clr-namespace:MultiTerm.Wpf.ValueConverters" xmlns:protocol_serial="clr-namespace:MultiTerm.Protocols.Serial;assembly=MultiTerm.Protocols" + xmlns:protocol_usbhid="clr-namespace:MultiTerm.Protocols.UsbHid;assembly=MultiTerm.Protocols" xmlns:types="clr-namespace:MultiTerm.Core.Types;assembly=MultiTerm.Core" xmlns:settings_view="clr-namespace:MultiTerm.Wpf.View.SettingsView" xmlns:custom_controls="clr-namespace:MultiTerm.Wpf.CustomControl;assembly=MultiTerm.Wpf.CustomControl" @@ -46,6 +47,9 @@ + + + diff --git a/MultiTerm.Wpf/View/SettingsView/UsbHidSettingsView.xaml b/MultiTerm.Wpf/View/SettingsView/UsbHidSettingsView.xaml new file mode 100644 index 0000000..08cf7a4 --- /dev/null +++ b/MultiTerm.Wpf/View/SettingsView/UsbHidSettingsView.xaml @@ -0,0 +1,102 @@ + + +