implemented first version of UsbHid Protocol,

implemented UsbHid Settings View etc,
cleaned up SerialProtocol
master
Jonas Arnold 3 years ago
parent 93e275c15d
commit 2a3e37e7cc
  1. 3
      MultiTerm.Protocols/Helpers/ServiceExtensions.cs
  2. 19
      MultiTerm.Protocols/MultiTerm.Protocols.csproj
  3. 12
      MultiTerm.Protocols/Serial/ISerialProtocolSettings.cs
  4. 14
      MultiTerm.Protocols/Serial/SerialProtocol.cs
  5. 20
      MultiTerm.Protocols/UsbHid/IUsbHidProtocolSettings.cs
  6. 12
      MultiTerm.Protocols/UsbHid/UsbHidDeviceInfo.cs
  7. 168
      MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs
  8. 100
      MultiTerm.Protocols/UsbHid/UsbHidProtocolSettingsViewModel.cs
  9. BIN
      MultiTerm.Protocols/UsbHid/hidapi.dll
  10. BIN
      MultiTerm.Protocols/UsbHid/hidapi.lib
  11. 4
      MultiTerm.Wpf/View/SendReceiveView.xaml
  12. 102
      MultiTerm.Wpf/View/SettingsView/UsbHidSettingsView.xaml
  13. 11
      MultiTerm.Wpf/View/SettingsView/UsbHidSettingsView.xaml.cs

@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using MultiTerm.Protocols.Factories; using MultiTerm.Protocols.Factories;
using MultiTerm.Protocols.Serial; using MultiTerm.Protocols.Serial;
using MultiTerm.Protocols.UsbHid;
namespace MultiTerm.Protocols.Helpers; namespace MultiTerm.Protocols.Helpers;
@ -16,10 +17,12 @@ public static class ServiceExtensions
{ {
// add all protocol implementations to the services collection // add all protocol implementations to the services collection
services.AddTransient<ICommunicationProtocol, SerialProtocol>(); services.AddTransient<ICommunicationProtocol, SerialProtocol>();
services.AddTransient<ICommunicationProtocol, UsbHidProtocol>();
// TODO extend // TODO extend
// add all settings view model implementations to the services collection // add all settings view model implementations to the services collection
services.AddTransient<IProtocolSettingsViewModel, SerialProtocolSettingsViewModel>(); services.AddTransient<IProtocolSettingsViewModel, SerialProtocolSettingsViewModel>();
services.AddTransient<IProtocolSettingsViewModel, UsbHidProtocolSettingsViewModel>();
// TODO extend // TODO extend
// add a function to the services collection, which is used by the CommunicationProtocolFactory // add a function to the services collection, which is used by the CommunicationProtocolFactory

@ -6,8 +6,27 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="UsbHid\hidapi.dll" />
<None Remove="UsbHid\hidapi.lib" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="UsbHid\hidapi.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<!-- directly to output folder, do not create subfolder UsbHid -->
<TargetPath>%(Filename)%(Extension)</TargetPath>
</EmbeddedResource>
<EmbeddedResource Include="UsbHid\hidapi.lib">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<!-- directly to output folder, do not create subfolder UsbHid -->
<TargetPath>%(Filename)%(Extension)</TargetPath>
</EmbeddedResource>
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" />
<PackageReference Include="HidApi.Net" Version="0.3.0" />
<PackageReference Include="SerialPortStream" Version="2.4.1" /> <PackageReference Include="SerialPortStream" Version="2.4.1" />
</ItemGroup> </ItemGroup>

@ -5,31 +5,31 @@ public interface ISerialProtocolSettings : IProtocolSettings
/// <summary> /// <summary>
/// Port for communication, e.g. COM3 or com50. /// Port for communication, e.g. COM3 or com50.
/// </summary> /// </summary>
string PortName { get; set; } string PortName { get; internal set; }
/// <summary> /// <summary>
/// Serial Baud rate. /// Serial Baud rate.
/// </summary> /// </summary>
int BaudRate { get; set; } int BaudRate { get; }
/// <summary> /// <summary>
/// The type of parity to use. /// The type of parity to use.
/// </summary> /// </summary>
Parity Parity { get; set; } Parity Parity { get; }
/// <summary> /// <summary>
/// The standard length of data bits per byte. /// The standard length of data bits per byte.
/// </summary> /// </summary>
int DataBits { get; set; } int DataBits { get; }
/// <summary> /// <summary>
/// Number of stop bits to use. /// Number of stop bits to use.
/// </summary> /// </summary>
StopBits StopBits { get; set; } StopBits StopBits { get; }
/// <summary> /// <summary>
/// Handshaking protocol for serial port transmission of data. /// Handshaking protocol for serial port transmission of data.
/// </summary> /// </summary>
Handshake Handshake { get; set; } Handshake Handshake { get; }
} }

@ -19,14 +19,14 @@ public class SerialProtocol : CommunicationProtocol
public SerialProtocol(ILogger logger, IMessenger messenger) : base(logger, messenger) { } 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 // check if settings are of correct type
if (protocolSettings is not ISerialProtocolSettings serialProtocolSettings) if (settings is not ISerialProtocolSettings serialProtocolSettings)
{ {
this.serialSettings = null; this.serialSettings = null;
throw new ArgumentException($"Cannot connect due to wrong type of Protocol Settings. " + 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 // store locally
@ -55,7 +55,7 @@ public class SerialProtocol : CommunicationProtocol
// try opening serial port // try opening serial port
try try
{ {
serialPort.Open(); this.serialPort.Open();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -67,7 +67,7 @@ public class SerialProtocol : CommunicationProtocol
protected override void InternalDisconnect() protected override void InternalDisconnect()
{ {
serialPort.Close(); this.serialPort.Close();
this.serialSettings = null; this.serialSettings = null;
} }
@ -76,7 +76,7 @@ public class SerialProtocol : CommunicationProtocol
while(ct.IsCancellationRequested == false) while(ct.IsCancellationRequested == false)
{ {
// reads character based on configured encoding (here ASCII) // reads character based on configured encoding (here ASCII)
int readByte = serialPort.ReadByte(); int readByte = this.serialPort.ReadByte();
if (readByte != -1) // -1 = end of stream if (readByte != -1) // -1 = end of stream
{ {
// report new data with event // report new data with event
@ -91,7 +91,7 @@ public class SerialProtocol : CommunicationProtocol
{ {
try try
{ {
serialPort.WriteByte(b); this.serialPort.WriteByte(b);
} }
// When the Serial Port is closed and InvalidOperationException is thrown => report error // When the Serial Port is closed and InvalidOperationException is thrown => report error
catch(InvalidOperationException) catch(InvalidOperationException)

@ -0,0 +1,20 @@
namespace MultiTerm.Protocols.UsbHid;
public interface IUsbHidProtocolSettings : IProtocolSettings
{
/// <summary>
/// Vendor ID of the target device. 16bit number.
/// </summary>
ushort VendorId { get; }
/// <summary>
/// Product ID of the target device. 16bit number.
/// </summary>
ushort ProductId { get; }
/// <summary>
/// Serial number of the target device.
/// If null, not used to connect.
/// </summary>
string SerialNumber { get; set; }
}

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

@ -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<byte> 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<byte> 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<byte> 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<UsbHidDeviceInfo> GetDevices()
{
var libDevicesInfo = Hid.Enumerate();
var usbHidDevicesInfo = new List<UsbHidDeviceInfo>();
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;
}
}

@ -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<UsbHidDeviceInfo> devices = new();
[ObservableProperty]
private UsbHidDeviceInfo? selectedDevice;
[RelayCommand]
private void ReloadDevices()
{
this.Devices = new ObservableCollection<UsbHidDeviceInfo>(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();
}
/// <summary>
/// Updates other fields when <see cref="SelectedDevice"/> was changed.
/// </summary>
/// <param name="value"></param>
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<IUserInterfaceMessage>(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<IUserInterfaceMessage>(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<IUserInterfaceMessage>(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<IUserInterfaceMessage>(new ProtocolSettingsInvalidMessage(nameof(this.ProductId), "Invalid. Not a 16bit number."));
return false;
}
// set internal
this.ProductId = productId;
return true;
}
}

@ -7,6 +7,7 @@
xmlns:local="clr-namespace:MultiTerm.Wpf.View" xmlns:local="clr-namespace:MultiTerm.Wpf.View"
xmlns:conv="clr-namespace:MultiTerm.Wpf.ValueConverters" xmlns:conv="clr-namespace:MultiTerm.Wpf.ValueConverters"
xmlns:protocol_serial="clr-namespace:MultiTerm.Protocols.Serial;assembly=MultiTerm.Protocols" 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:types="clr-namespace:MultiTerm.Core.Types;assembly=MultiTerm.Core"
xmlns:settings_view="clr-namespace:MultiTerm.Wpf.View.SettingsView" xmlns:settings_view="clr-namespace:MultiTerm.Wpf.View.SettingsView"
xmlns:custom_controls="clr-namespace:MultiTerm.Wpf.CustomControl;assembly=MultiTerm.Wpf.CustomControl" xmlns:custom_controls="clr-namespace:MultiTerm.Wpf.CustomControl;assembly=MultiTerm.Wpf.CustomControl"
@ -46,6 +47,9 @@
<DataTemplate DataType="{x:Type protocol_serial:SerialProtocolSettingsViewModel}"> <DataTemplate DataType="{x:Type protocol_serial:SerialProtocolSettingsViewModel}">
<settings_view:SerialSettingsView/> <settings_view:SerialSettingsView/>
</DataTemplate> </DataTemplate>
<DataTemplate DataType="{x:Type protocol_usbhid:UsbHidProtocolSettingsViewModel}">
<settings_view:UsbHidSettingsView/>
</DataTemplate>
</ContentControl.Resources> </ContentControl.Resources>
</ContentControl> </ContentControl>
</GroupBox> </GroupBox>

@ -0,0 +1,102 @@
<UserControl x:Class="MultiTerm.Wpf.View.SettingsView.UsbHidSettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MultiTerm.Wpf.View.SettingsView"
mc:Ignorable="d"
d:DesignHeight="50" d:DesignWidth="800">
<DockPanel>
<Button DockPanel.Dock="Left" Command="{Binding ConnectDisconnectCommand}" Content="{Binding ConnectDisconnectButtonText}" Width="80"/>
<StackPanel Orientation="Horizontal" IsEnabled="{Binding AreEditable}">
<StackPanel.Resources>
<!-- Template for Combobox Item -->
<ControlTemplate x:Key="NotDroppedTemplate">
<StackPanel Orientation="Horizontal">
<Label Content="VID:" FontSize="11" VerticalAlignment="Center" Padding="0 0 5 0"/>
<TextBlock Text="0x"/>
<TextBlock Text="{Binding VendorId}" FontWeight="DemiBold"/>
<Label Content="PID:" FontSize="11" VerticalAlignment="Center" Padding="10 0 5 0"/>
<TextBlock Text="0x"/>
<TextBlock Text="{Binding ProductId}" FontWeight="DemiBold"/>
</StackPanel>
</ControlTemplate>
<ControlTemplate x:Key="DroppedTemplate">
<StackPanel Orientation="Vertical">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- VID and PID -->
<Label Content="Vendor ID:" FontSize="11" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="1"
HorizontalAlignment="Left" Padding="0"/>
<Label Content="Product ID:" FontSize="11" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="1"
HorizontalAlignment="Left" Padding="0"/>
<TextBlock Text="{Binding VendorId}" FontSize="15" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="1"
HorizontalAlignment="Left" FontWeight="DemiBold"/>
<TextBlock Text="{Binding ProductId}" FontSize="15" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="1"
HorizontalAlignment="Left" FontWeight="DemiBold"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Manufacturer -->
<Label Content="Manufacturer:" FontSize="11" Grid.Row="0" Grid.Column="0"
Padding="0 0 10 0" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Manufacturer}" FontSize="12" Grid.Row="0" Grid.Column="1"
VerticalAlignment="Center" FontWeight="DemiBold"/>
<!-- Serial Number-->
<Label Content="Serial Num:" FontSize="11" Grid.Row="1" Grid.Column="0"
Padding="0 0 10 0" VerticalAlignment="Center"/>
<TextBlock Text="{Binding SerialNumber}" FontSize="12" Grid.Row="1" Grid.Column="1"
VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</ControlTemplate>
<DataTemplate x:Key="HidDeviceComboBoxTemplate">
<Control x:Name="control" Focusable="False" Template="{StaticResource DroppedTemplate}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
<Setter TargetName="control" Property="Template" Value="{StaticResource NotDroppedTemplate}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</StackPanel.Resources>
<Label Margin="10 0" VerticalAlignment="Center">Devices:</Label>
<ComboBox Width="190" ItemsSource="{Binding Path=Devices}" SelectedItem="{Binding Path=SelectedDevice, Mode=TwoWay}"
VerticalContentAlignment="Center"
ItemTemplate="{StaticResource HidDeviceComboBoxTemplate}">
</ComboBox>
<Button Margin="5 0" Content="R" VerticalContentAlignment="Center"
Command="{Binding ReloadDevicesCommand}" Width="25"/>
<Label Margin="20 0 10 0" VerticalAlignment="Center">Vendor ID:</Label>
<TextBox Width="50" VerticalContentAlignment="Center" Text="{Binding Path=VendorIdHex, Mode=TwoWay}" />
<Label Margin="20 0 10 0" VerticalAlignment="Center">Product ID:</Label>
<TextBox Width="50" VerticalContentAlignment="Center" Text="{Binding Path=ProductIdHex, Mode=TwoWay}" />
<Label Margin="20 0 10 0" VerticalAlignment="Center">Serial Number:</Label>
<TextBox Width="Auto" MinWidth="100" VerticalContentAlignment="Center" Text="{Binding Path=SerialNumber, Mode=TwoWay}" />
</StackPanel>
</DockPanel>
</UserControl>

@ -0,0 +1,11 @@
using System.Windows.Controls;
namespace MultiTerm.Wpf.View.SettingsView;
public partial class UsbHidSettingsView : UserControl
{
public UsbHidSettingsView()
{
InitializeComponent();
}
}
Loading…
Cancel
Save