implemented first version of UDP protocol

master
Jonas Arnold 3 years ago
parent 92beb2ec45
commit 6606e30d5b
  1. 1
      MultiTerm.Protocols/CommunicationProtocol.cs
  2. 3
      MultiTerm.Protocols/Helpers/ServiceExtensions.cs
  3. 1
      MultiTerm.Protocols/MultiTerm.Protocols.csproj
  4. 2
      MultiTerm.Protocols/Types/ProtocolType.cs
  5. 15
      MultiTerm.Protocols/Udp/IUdpProtocolSettings.cs
  6. 9
      MultiTerm.Protocols/Udp/UdpConnectedMessage.cs
  7. 197
      MultiTerm.Protocols/Udp/UdpProtocol.cs
  8. 129
      MultiTerm.Protocols/Udp/UdpProtocolSettingsViewModel.cs
  9. 4
      MultiTerm.Wpf/View/SendReceiveView.xaml
  10. 76
      MultiTerm.Wpf/View/SettingsView/UdpSettingsView.xaml
  11. 11
      MultiTerm.Wpf/View/SettingsView/UdpSettingsView.xaml.cs

@ -150,6 +150,7 @@ public abstract class CommunicationProtocol : ICommunicationProtocol
else else
{ {
this.logger.LogWarn($"'{nameof(Connect)}()' failed to connect to protocol, did not start reading thread.", nameof(CommunicationProtocol)); this.logger.LogWarn($"'{nameof(Connect)}()' failed to connect to protocol, did not start reading thread.", nameof(CommunicationProtocol));
this.messenger.Send<IUserInterfaceMessage>(new GenericUserInterfaceMessage("Failed to connect to protocol, for more information please check logfile.", MessageImportance.High));
return false; return false;
} }
} }

@ -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.Udp;
using MultiTerm.Protocols.UsbHid; using MultiTerm.Protocols.UsbHid;
namespace MultiTerm.Protocols.Helpers; namespace MultiTerm.Protocols.Helpers;
@ -18,11 +19,13 @@ 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>(); services.AddTransient<ICommunicationProtocol, UsbHidProtocol>();
services.AddTransient<ICommunicationProtocol, UdpProtocol>();
// 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>(); services.AddTransient<IProtocolSettingsViewModel, UsbHidProtocolSettingsViewModel>();
services.AddTransient<IProtocolSettingsViewModel, UdpProtocolSettingsViewModel>();
// 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

@ -28,6 +28,7 @@
<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="HidApi.Net" Version="0.3.0" />
<PackageReference Include="SerialPortStream" Version="2.4.1" /> <PackageReference Include="SerialPortStream" Version="2.4.1" />
<PackageReference Include="System.Net.Sockets" Version="4.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -25,6 +25,6 @@ public enum ProtocolType
/// <summary> /// <summary>
/// UDP Protocol /// UDP Protocol
/// </summary> /// </summary>
[Description("UCP")] [Description("UDP")]
Udp Udp
} }

@ -0,0 +1,15 @@
namespace MultiTerm.Protocols.Udp;
internal interface IUdpProtocolSettings : IProtocolSettings
{
/// <summary>
/// Hostname to connect to.
/// Can be a hostname with or without domain or an IPv4/IVv6 address.
/// </summary>
string Hostname {get; internal set; }
/// <summary>
/// Port of the end device. Will also be the port that is listened on.
/// </summary>
int Port { get; internal set; }
}

@ -0,0 +1,9 @@
using System.Net;
namespace MultiTerm.Protocols.Udp;
/// <summary>
/// A message that is sent by the <see cref="UdpProtocol"/> to report that it has connected to an endpoint.
/// Main reason to use this message is to find out the <see cref="ResolvedAddress"/> of the endpoint.
/// </summary>
internal record UdpConnectedMessage(IPAddress ResolvedAddress);

@ -0,0 +1,197 @@
using Common.Logging;
using Common.Messaging;
using CommunityToolkit.Mvvm.Messaging;
using MultiTerm.Protocols.Model;
using System.Net.Sockets;
namespace MultiTerm.Protocols.Udp;
public class UdpProtocol : CommunicationProtocol
{
public override Types.ProtocolType ProtocolType => Types.ProtocolType.Udp;
public override string InstanceIdentifier { get; protected set; } = string.Empty;
private IUdpProtocolSettings? udpSettings;
private UdpClient? receivingUdpClient;
private UdpClient? sendingUdpClient;
private const int MaxInstanceIdentifierLength = 20; // maximum number of characters for the InstanceIdentifier
public UdpProtocol(ILogger logger, IMessenger messenger) : base(logger, messenger) { }
protected override bool InternalConnect(IProtocolSettings settings)
{
// check if settings are of correct type
if (settings is not IUdpProtocolSettings udpProtocolSettings)
{
this.udpSettings = null;
throw new ArgumentException($"Cannot connect due to wrong type of Protocol Settings. " +
$"Check parameter {nameof(settings)}' of '{nameof(InternalConnect)}()' in {nameof(UdpProtocol)}.");
}
// store locally
this.udpSettings = udpProtocolSettings;
// update identifier
if(this.udpSettings.Hostname != null && this.udpSettings.Hostname.Length >= MaxInstanceIdentifierLength)
{
this.InstanceIdentifier = $"...{this.udpSettings.Hostname.Substring(this.udpSettings.Hostname.Length - MaxInstanceIdentifierLength, MaxInstanceIdentifierLength)}";
}
else if(this.udpSettings.Hostname != null)
{
this.InstanceIdentifier = $"{this.udpSettings.Hostname}";
}
else
{
this.InstanceIdentifier = "invalid";
}
// check if clients are null
if(this.receivingUdpClient != null || this.sendingUdpClient != null)
{
throw new Exception($"A UDP client was not null when {nameof(InternalConnect)} was called: " +
$"{nameof(receivingUdpClient)} isnull={this.receivingUdpClient == null}," +
$"{nameof(sendingUdpClient)} isnull={this.sendingUdpClient == null}");
}
/* create udp clients */
// try opening receiving udp socket
try
{
this.receivingUdpClient = new UdpClient(this.udpSettings.Port);
}
catch (Exception ex)
{
this.logger.LogException(ex, $"'{nameof(InternalConnect)}()'Opening Receiving UDP socket failed:", nameof(UdpProtocol));
// rollback
this.InternalDisconnect();
return false;
}
// try opening sending udp socket
try
{
this.sendingUdpClient = new UdpClient();
this.sendingUdpClient.Connect(this.udpSettings.Hostname!, this.udpSettings.Port);
}
catch (Exception ex)
{
this.logger.LogException(ex, $"'{nameof(InternalConnect)}()'Opening Sending UDP Socket failed:", nameof(UdpProtocol));
// rollback
this.InternalDisconnect();
return false;
}
return true;
}
protected override void InternalDisconnect()
{
// close receiving udp client
if(this.receivingUdpClient != null)
{
this.receivingUdpClient?.Close();
this.receivingUdpClient?.Dispose();
this.receivingUdpClient = null;
}
// close sending udp client
if (this.sendingUdpClient != null)
{
this.sendingUdpClient?.Close();
this.sendingUdpClient?.Dispose();
this.sendingUdpClient = null;
}
}
protected override void InternalRead(CancellationToken ct)
{
while(ct.IsCancellationRequested == false)
{
// if receiving Udp Client is null => break
if (this.receivingUdpClient == null)
{
this.OnUnintentionallyDisconnected();
break; // break loop
}
// try receive message
UdpReceiveResult receivedResult;
try
{
receivedResult = this.receivingUdpClient.ReceiveAsync(ct).GetAwaiter().GetResult();
}
catch (OperationCanceledException) // intentionally cancelled => just break
{
break;
}
catch (ObjectDisposedException objex)
{
this.logger.LogException(objex, $"ObjectDisposedException while sending data in {nameof(InternalRead)}", nameof(UdpProtocol));
this.OnUnintentionallyDisconnected();
break; // break loop
}
catch (Exception ex)
{
this.logger.LogException(ex, $"Exception while reading data in {nameof(InternalRead)}", nameof(UdpProtocol));
this.messenger.Send<IUserInterfaceMessage>(new GenericUserInterfaceMessage($"UDP client ended reading on port " +
$"{this.udpSettings!.Port} because of exception.", MessageImportance.Medium));
break; // break loop
}
// any data received?
byte[] receivedBytes = receivedResult.Buffer;
if (receivedBytes.Length > 0)
{
foreach (byte b in receivedBytes)
{
// report received data
this.OnReceivedData(new ExtendedByte(b));
}
}
}
}
protected override bool InternalSendBytes(byte[] bytes)
{
// if sending Udp Client is null => return error
if (this.sendingUdpClient == null)
{
this.OnUnintentionallyDisconnected();
return false;
}
// check for empty bytes array
if (bytes == null || bytes.Length == 0)
{
this.logger.LogWarn($"'{nameof(InternalSendBytes)}()' got null or empty bytes array.", nameof(UdpProtocol));
return false;
}
// try sending the data
try
{
this.sendingUdpClient.Send(bytes, bytes.Length);
}
catch (ObjectDisposedException objex)
{
this.logger.LogException(objex, $"ObjectDisposedException while sending data in {nameof(InternalSendBytes)}", nameof(UdpProtocol));
this.OnUnintentionallyDisconnected();
return false;
}
catch (Exception ex)
{
this.logger.LogException(ex, $"Exception while sending data in {nameof(InternalSendBytes)}", nameof(UdpProtocol));
return false;
}
foreach (byte b in bytes)
{
// report sent data
this.OnSentData(new ExtendedByte(b));
}
return true;
}
}

@ -0,0 +1,129 @@
using Common.Messaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using MultiTerm.Protocols.Types;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
namespace MultiTerm.Protocols.Udp;
public partial class UdpProtocolSettingsViewModel : ProtocolSettingsViewModel, IUdpProtocolSettings, IRecipient<UdpConnectedMessage>
{
public override Types.ProtocolType ProtocolType => Types.ProtocolType.Udp;
#region IUdpProtocolSettings Implementation
[ObservableProperty]
private string hostname = string.Empty;
[ObservableProperty]
private int port = 10000;
#endregion
/// <summary>
/// A property to display the resolved IP address that the protocol connected to.
/// </summary>
[ObservableProperty]
private string resolvedAddress = string.Empty;
public UdpProtocolSettingsViewModel(IMessenger messenger) : base(messenger)
{
// register for messages
messenger.Register(this);
// initialize IP address with first ethernet adapter local IP
this.Hostname = GetFirstEthernetAdaptersSubnet();
}
public bool AreValid()
{
if (String.IsNullOrEmpty(this.Hostname))
{
this.messenger.Send<IUserInterfaceMessage>(new ProtocolSettingsInvalidMessage(nameof(this.Hostname), "Null or empty"));
return false;
}
if (this.Port < IPEndPoint.MinPort && this.Port > IPEndPoint.MaxPort)
{
this.messenger.Send<IUserInterfaceMessage>(new ProtocolSettingsInvalidMessage(nameof(this.Port), "Out of range"));
return false;
}
// try parse to IP address
if (IPAddress.TryParse(this.Hostname, out _))
{
return true;
}
// else try to resolve hostname
else
{
IPHostEntry host;
try
{
host = Dns.GetHostEntry(this.Hostname);
}
catch (ArgumentOutOfRangeException)
{
this.messenger.Send<IUserInterfaceMessage>(new ProtocolSettingsInvalidMessage(nameof(this.Hostname), "Length is greater than 255 characters"));
return false;
}
catch (SocketException)
{
this.messenger.Send<IUserInterfaceMessage>(new ProtocolSettingsInvalidMessage(nameof(this.Hostname), "Error encountered when resolving hostname"));
return false;
}
catch (ArgumentException)
{
this.messenger.Send<IUserInterfaceMessage>(new ProtocolSettingsInvalidMessage(nameof(this.Hostname), "Invalid IP address"));
return false;
}
// host not null and found at least one address => success
return host != null && host.AddressList.Length > 0;
}
}
/// <summary>
/// Sets the <see cref="ResolvedAddress"/> proprty to the resolved address from the message.
/// </summary>
/// <param name="message"></param>
void IRecipient<UdpConnectedMessage>.Receive(UdpConnectedMessage message)
{
this.ResolvedAddress = message.ResolvedAddress.ToString();
}
/// <summary>
/// Tries to retrieve the first ethernet adapters subnet address.
/// </summary>
/// <returns>string of <see cref="IPAddress"/> of the first ethernet adapters subnet address, or <see cref="string.Empty"/> if not found</returns>
private static string GetFirstEthernetAdaptersSubnet()
{
// iterate through all network interfaces
NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces();
foreach (var adapter in interfaces)
{
// return first found ipv4 adapter subnet address
if (adapter.OperationalStatus == OperationalStatus.Up)
{
try
{
var unicastAddresses = adapter.GetIPProperties().UnicastAddresses;
foreach (var unicastAddress in unicastAddresses)
{
// only interested in IPv4
if (unicastAddress.Address.AddressFamily != AddressFamily.InterNetwork)
continue;
// Ignore loopback addresses (e.g., 127.0.0.1)
if (IPAddress.IsLoopback(unicastAddress.Address))
continue;
// split and only return first three parts of the IPv4 address
var splitIpv4 = unicastAddress.Address.ToString().Split('.');
return $"{splitIpv4[0]}.{splitIpv4[1]}.{splitIpv4[2]}.";
}
}
catch { }
}
}
return string.Empty;
}
}

@ -8,6 +8,7 @@
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:protocol_usbhid="clr-namespace:MultiTerm.Protocols.UsbHid;assembly=MultiTerm.Protocols"
xmlns:protocol_udp="clr-namespace:MultiTerm.Protocols.Udp;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"
@ -50,6 +51,9 @@
<DataTemplate DataType="{x:Type protocol_usbhid:UsbHidProtocolSettingsViewModel}"> <DataTemplate DataType="{x:Type protocol_usbhid:UsbHidProtocolSettingsViewModel}">
<settings_view:UsbHidSettingsView/> <settings_view:UsbHidSettingsView/>
</DataTemplate> </DataTemplate>
<DataTemplate DataType="{x:Type protocol_udp:UdpProtocolSettingsViewModel}">
<settings_view:UdpSettingsView/>
</DataTemplate>
</ContentControl.Resources> </ContentControl.Resources>
</ContentControl> </ContentControl>
</GroupBox> </GroupBox>

@ -0,0 +1,76 @@
<UserControl x:Class="MultiTerm.Wpf.View.SettingsView.UdpSettingsView"
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:conv="clr-namespace:MultiTerm.Wpf.ValueConverters"
xmlns:local="clr-namespace:MultiTerm.Wpf.View.SettingsView"
mc:Ignorable="d"
d:DesignHeight="50" d:DesignWidth="800">
<UserControl.Resources>
<!-- Converter -->
<conv:IntToStringConverter x:Key="IntToStringConverter"/>
</UserControl.Resources>
<DockPanel>
<Button DockPanel.Dock="Left" Command="{Binding ConnectDisconnectCommand}" Content="{Binding ConnectDisconnectButtonText}" Width="80"/>
<StackPanel Orientation="Horizontal" IsEnabled="{Binding AreEditable}">
<!-- Hostname / IP -->
<StackPanel Orientation="Horizontal" ToolTipService.InitialShowDelay="200">
<StackPanel.ToolTip>
<StackPanel Orientation="Vertical">
<TextBlock Text="Examples:"></TextBlock>
<UniformGrid Columns="2">
<TextBlock Text="IPv4:"/>
<TextBlock Text="'192.168.1.20'"/>
<TextBlock Text="IPv6:"/>
<TextBlock Text="'FE80::AB8'"/>
<TextBlock Text="Hostname:"/>
<TextBlock Text="'exampleDevice'"/>
<TextBlock Text="Hostname+Domain:"/>
<TextBlock Text="'exampleDevice.local'"/>
</UniformGrid>
</StackPanel>
</StackPanel.ToolTip>
<Label Margin="10 0" VerticalAlignment="Center">Hostname/IP:</Label>
<TextBox Text="{Binding Path=Hostname, Mode=TwoWay}" VerticalContentAlignment="Center" MinWidth="120" Width="Auto"/>
</StackPanel>
<!-- Port -->
<StackPanel Orientation="Horizontal" ToolTipService.InitialShowDelay="200">
<StackPanel.ToolTip>
<TextBlock>Port Number (0...65535)</TextBlock>
</StackPanel.ToolTip>
<Label Margin="20 0 10 0" VerticalAlignment="Center" HorizontalAlignment="Center">Port:</Label>
<TextBox Text="{Binding Path=Port, Mode=TwoWay, Converter={StaticResource IntToStringConverter}}" Width="50" VerticalContentAlignment="Center"/>
</StackPanel>
<!-- Display for Resolved IP Address -->
<StackPanel Orientation="Vertical" Margin="20 0 10 0">
<StackPanel.Style>
<Style TargetType="{x:Type StackPanel}">
<!-- Defaults to visible -->
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<!-- Collapsed when Resolved Address String length is 0 -->
<DataTrigger Binding="{Binding Path=ResolvedAddress.Length, FallbackValue=0, TargetNullValue=0}" Value="0">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<Label VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="DarkGray">Resolved IP Address:</Label>
<TextBox IsReadOnly="True" Text="{Binding ResolvedAddress}"/>
</StackPanel>
<!--<StackPanel Orientation="Horizontal">
<Label Margin="20 0 0 0" VerticalAlignment="Center" HorizontalAlignment="Center" Padding="0">Serial Number:</Label>
<Label Margin="5 0 10 0" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="DarkGray" FontSize="12" Padding="0 0">(optional)</Label>
</StackPanel>
<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 UdpSettingsView : UserControl
{
public UdpSettingsView()
{
InitializeComponent();
}
}
Loading…
Cancel
Save