From 6606e30d5b7b2d8e11ce9bde09524893f3354076 Mon Sep 17 00:00:00 2001 From: Jonas Arnold Date: Mon, 8 May 2023 21:49:58 +0200 Subject: [PATCH] implemented first version of UDP protocol --- MultiTerm.Protocols/CommunicationProtocol.cs | 1 + .../Helpers/ServiceExtensions.cs | 3 + .../MultiTerm.Protocols.csproj | 1 + MultiTerm.Protocols/Types/ProtocolType.cs | 2 +- .../Udp/IUdpProtocolSettings.cs | 15 ++ .../Udp/UdpConnectedMessage.cs | 9 + MultiTerm.Protocols/Udp/UdpProtocol.cs | 197 ++++++++++++++++++ .../Udp/UdpProtocolSettingsViewModel.cs | 129 ++++++++++++ MultiTerm.Wpf/View/SendReceiveView.xaml | 4 + .../View/SettingsView/UdpSettingsView.xaml | 76 +++++++ .../View/SettingsView/UdpSettingsView.xaml.cs | 11 + 11 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 MultiTerm.Protocols/Udp/IUdpProtocolSettings.cs create mode 100644 MultiTerm.Protocols/Udp/UdpConnectedMessage.cs create mode 100644 MultiTerm.Protocols/Udp/UdpProtocol.cs create mode 100644 MultiTerm.Protocols/Udp/UdpProtocolSettingsViewModel.cs create mode 100644 MultiTerm.Wpf/View/SettingsView/UdpSettingsView.xaml create mode 100644 MultiTerm.Wpf/View/SettingsView/UdpSettingsView.xaml.cs diff --git a/MultiTerm.Protocols/CommunicationProtocol.cs b/MultiTerm.Protocols/CommunicationProtocol.cs index cf95bb6..836fda0 100644 --- a/MultiTerm.Protocols/CommunicationProtocol.cs +++ b/MultiTerm.Protocols/CommunicationProtocol.cs @@ -150,6 +150,7 @@ public abstract class CommunicationProtocol : ICommunicationProtocol else { this.logger.LogWarn($"'{nameof(Connect)}()' failed to connect to protocol, did not start reading thread.", nameof(CommunicationProtocol)); + this.messenger.Send(new GenericUserInterfaceMessage("Failed to connect to protocol, for more information please check logfile.", MessageImportance.High)); return false; } } diff --git a/MultiTerm.Protocols/Helpers/ServiceExtensions.cs b/MultiTerm.Protocols/Helpers/ServiceExtensions.cs index d1cb97c..822156e 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.Udp; using MultiTerm.Protocols.UsbHid; namespace MultiTerm.Protocols.Helpers; @@ -18,11 +19,13 @@ public static class ServiceExtensions // add all protocol implementations to the services collection services.AddTransient(); services.AddTransient(); + services.AddTransient(); // TODO extend // add all settings view model implementations to the services collection services.AddTransient(); 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 6e53b42..2a81e61 100644 --- a/MultiTerm.Protocols/MultiTerm.Protocols.csproj +++ b/MultiTerm.Protocols/MultiTerm.Protocols.csproj @@ -28,6 +28,7 @@ + diff --git a/MultiTerm.Protocols/Types/ProtocolType.cs b/MultiTerm.Protocols/Types/ProtocolType.cs index 9807666..18f3e56 100644 --- a/MultiTerm.Protocols/Types/ProtocolType.cs +++ b/MultiTerm.Protocols/Types/ProtocolType.cs @@ -25,6 +25,6 @@ public enum ProtocolType /// /// UDP Protocol /// - [Description("UCP")] + [Description("UDP")] Udp } diff --git a/MultiTerm.Protocols/Udp/IUdpProtocolSettings.cs b/MultiTerm.Protocols/Udp/IUdpProtocolSettings.cs new file mode 100644 index 0000000..a9ba156 --- /dev/null +++ b/MultiTerm.Protocols/Udp/IUdpProtocolSettings.cs @@ -0,0 +1,15 @@ +namespace MultiTerm.Protocols.Udp; + +internal interface IUdpProtocolSettings : IProtocolSettings +{ + /// + /// Hostname to connect to. + /// Can be a hostname with or without domain or an IPv4/IVv6 address. + /// + string Hostname {get; internal set; } + + /// + /// Port of the end device. Will also be the port that is listened on. + /// + int Port { get; internal set; } +} diff --git a/MultiTerm.Protocols/Udp/UdpConnectedMessage.cs b/MultiTerm.Protocols/Udp/UdpConnectedMessage.cs new file mode 100644 index 0000000..3862c61 --- /dev/null +++ b/MultiTerm.Protocols/Udp/UdpConnectedMessage.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MultiTerm.Protocols.Udp; + +/// +/// A message that is sent by the to report that it has connected to an endpoint. +/// Main reason to use this message is to find out the of the endpoint. +/// +internal record UdpConnectedMessage(IPAddress ResolvedAddress); \ No newline at end of file diff --git a/MultiTerm.Protocols/Udp/UdpProtocol.cs b/MultiTerm.Protocols/Udp/UdpProtocol.cs new file mode 100644 index 0000000..f8b2696 --- /dev/null +++ b/MultiTerm.Protocols/Udp/UdpProtocol.cs @@ -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(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; + } +} diff --git a/MultiTerm.Protocols/Udp/UdpProtocolSettingsViewModel.cs b/MultiTerm.Protocols/Udp/UdpProtocolSettingsViewModel.cs new file mode 100644 index 0000000..43d0e95 --- /dev/null +++ b/MultiTerm.Protocols/Udp/UdpProtocolSettingsViewModel.cs @@ -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 +{ + public override Types.ProtocolType ProtocolType => Types.ProtocolType.Udp; + + #region IUdpProtocolSettings Implementation + [ObservableProperty] + private string hostname = string.Empty; + + [ObservableProperty] + private int port = 10000; + #endregion + + /// + /// A property to display the resolved IP address that the protocol connected to. + /// + [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(new ProtocolSettingsInvalidMessage(nameof(this.Hostname), "Null or empty")); + return false; + } + if (this.Port < IPEndPoint.MinPort && this.Port > IPEndPoint.MaxPort) + { + this.messenger.Send(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(new ProtocolSettingsInvalidMessage(nameof(this.Hostname), "Length is greater than 255 characters")); + return false; + } + catch (SocketException) + { + this.messenger.Send(new ProtocolSettingsInvalidMessage(nameof(this.Hostname), "Error encountered when resolving hostname")); + return false; + } + catch (ArgumentException) + { + this.messenger.Send(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; + } + } + + /// + /// Sets the proprty to the resolved address from the message. + /// + /// + void IRecipient.Receive(UdpConnectedMessage message) + { + this.ResolvedAddress = message.ResolvedAddress.ToString(); + } + + /// + /// Tries to retrieve the first ethernet adapters subnet address. + /// + /// string of of the first ethernet adapters subnet address, or if not found + 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; + } +} diff --git a/MultiTerm.Wpf/View/SendReceiveView.xaml b/MultiTerm.Wpf/View/SendReceiveView.xaml index c4f7a55..d82b647 100644 --- a/MultiTerm.Wpf/View/SendReceiveView.xaml +++ b/MultiTerm.Wpf/View/SendReceiveView.xaml @@ -8,6 +8,7 @@ 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:protocol_udp="clr-namespace:MultiTerm.Protocols.Udp;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" @@ -50,6 +51,9 @@ + + + diff --git a/MultiTerm.Wpf/View/SettingsView/UdpSettingsView.xaml b/MultiTerm.Wpf/View/SettingsView/UdpSettingsView.xaml new file mode 100644 index 0000000..e9e27a2 --- /dev/null +++ b/MultiTerm.Wpf/View/SettingsView/UdpSettingsView.xaml @@ -0,0 +1,76 @@ + + + + + + + +