diff --git a/Common/Helpers/ContextHelpers.cs b/Common/Helpers/ContextHelpers.cs new file mode 100644 index 0000000..2fea4bf --- /dev/null +++ b/Common/Helpers/ContextHelpers.cs @@ -0,0 +1,21 @@ +namespace Common.Helpers; + +public static class ContextHelpers +{ + /// + /// Invokes context if necessary and runs action. If Threads are synchronized, runs action without switching context. + /// + /// context to run action on + /// action to run + public static void InvokeIfNecessary(IContext context, Action action) + { + if (context.IsSynchronized == false) + { + context.Invoke(action); + } + else + { + action.Invoke(); + } + } +} diff --git a/Common/IContext.cs b/Common/IContext.cs new file mode 100644 index 0000000..0b1471f --- /dev/null +++ b/Common/IContext.cs @@ -0,0 +1,24 @@ +namespace Common; + +/// +/// Provides Context of some thread. Allows user of the interface to run actions on the thread of the implementer. +/// +public interface IContext +{ + /// + /// Indicates wether the thread this property gotten from is equal to the implementers thread. + /// + bool IsSynchronized { get; } + + /// + /// Run on the implementers thread. + /// + /// action to run + void Invoke(Action action); + + /// + /// Start running on the implementers thread. + /// + /// action to start + void BeginInvoke(Action action); +} diff --git a/Common/Messaging/GenericUserInterfaceMessage.cs b/Common/Messaging/GenericUserInterfaceMessage.cs new file mode 100644 index 0000000..abfeb4d --- /dev/null +++ b/Common/Messaging/GenericUserInterfaceMessage.cs @@ -0,0 +1,31 @@ +namespace Common.Messaging; + +public class GenericUserInterfaceMessage : IUserInterfaceMessage +{ + /// + /// Description of the Message. Containes messsage text. + /// + public string Message { get; private set; } + + /// + /// Importance of this message. + /// + public MessageImportance Importance { get; private set; } + + public GenericUserInterfaceMessage(string message, MessageImportance importance) + { + // throw when message is empty + if (string.IsNullOrEmpty(message)) + { + throw new NotImplementedException($"Please provide a valid message for the {nameof(GenericUserInterfaceMessage)}"); + } + + this.Message = message; + this.Importance = importance; + } + + public override string ToString() + { + return this.Message; + } +} diff --git a/MultiTerm.Core/Types/ProtocolNotConnectedMessage.cs b/MultiTerm.Core/Types/ProtocolNotConnectedMessage.cs new file mode 100644 index 0000000..d1e3da3 --- /dev/null +++ b/MultiTerm.Core/Types/ProtocolNotConnectedMessage.cs @@ -0,0 +1,27 @@ +using Common.Messaging; + +namespace MultiTerm.Core.Types; + +public class ProtocolNotConnectedMessage : IUserInterfaceMessage +{ + private readonly string reason; + + public MessageImportance Importance => MessageImportance.Medium; + + public ProtocolNotConnectedMessage(string reason) + { + this.reason = reason; + } + + public override string ToString() + { + if (string.IsNullOrEmpty(reason)) + { + return $"Protocol is not connected."; + } + else + { + return $"{reason}: Protocol is not connected."; + } + } +} diff --git a/MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs b/MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs index d16682a..0330a9f 100644 --- a/MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs +++ b/MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs @@ -1,15 +1,19 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using Common; +using Common.Helpers; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MultiTerm.Core.Types; using MultiTerm.Protocols; using MultiTerm.Protocols.Model; using System.Collections.ObjectModel; + namespace MultiTerm.Core.ViewModel; public partial class CommunicationDataViewModel : ObservableObject { private ICommunicationProtocol? communicationProtocol; + private readonly IContext uiContext; private NewlineSeparatorType currentReceiveNewlineSeparatorType = NewlineSeparatorType.None; private NewlineSeparatorType currentSentNewlineSeparatorType = NewlineSeparatorType.None; @@ -37,8 +41,9 @@ public partial class CommunicationDataViewModel : ObservableObject [ObservableProperty] private List? selectedSentData; - public CommunicationDataViewModel(ICommunicationProtocol? communicationProtocol) + public CommunicationDataViewModel(ICommunicationProtocol? communicationProtocol, IContext context) { + this.uiContext = context; this.communicationProtocol = communicationProtocol; if (this.communicationProtocol != null) { @@ -261,8 +266,12 @@ public partial class CommunicationDataViewModel : ObservableObject // go through every character foreach (var newExtdChar in characters) { - // add to collection with the current counter - dataCollection.Add(new DataViewModel(newExtdChar, collectionLineCounter)); + // add to collection with the current counter, invoking UI context if necssary + var currentLineCounter = collectionLineCounter; + ContextHelpers.InvokeIfNecessary(this.uiContext, (Action)delegate + { + dataCollection.Add(new DataViewModel(newExtdChar, currentLineCounter)); + }); switch (ShouldIntroduceNewlineAfterThisCharacter(newExtdChar.Character, previousCharacters, newlineSeparatorType)) { diff --git a/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs b/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs index f0d99dd..de43187 100644 --- a/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs +++ b/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs @@ -1,6 +1,8 @@ -using Common.AppSettings; +using Common; +using Common.AppSettings; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; using MultiTerm.Core.Model; using MultiTerm.Core.Types; using System.Diagnostics; @@ -35,7 +37,7 @@ public partial class SendReceiveViewModel : TerminalViewModel /// Constructor. /// /// - public SendReceiveViewModel(IAppSettingsProvider appSettings) : base(appSettings) { } + public SendReceiveViewModel(IAppSettingsProvider appSettings, IMessenger messenger, IContext context) : base(appSettings, messenger, context) { } /// /// Send command. @@ -47,5 +49,10 @@ public partial class SendReceiveViewModel : TerminalViewModel var items = this.CommunicationData.SelectedReceivedData; Debugger.Break(); this.SentData = this.SendData.ToString(); + // send data + this.SendToCommunicationProtocol(this.SendData.ToString()); + + // clear textbox + this.SendData = new MultiFormatString(); } } diff --git a/MultiTerm.Core/ViewModel/TerminalViewModel.cs b/MultiTerm.Core/ViewModel/TerminalViewModel.cs index 1ef9dd8..7cc1c88 100644 --- a/MultiTerm.Core/ViewModel/TerminalViewModel.cs +++ b/MultiTerm.Core/ViewModel/TerminalViewModel.cs @@ -1,10 +1,14 @@ -using Common.AppSettings; +using Common; +using Common.AppSettings; using Common.Helpers; +using Common.Messaging; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; using MultiTerm.Core.Types; using MultiTerm.Protocols; using MultiTerm.Protocols.Types; +using System.Text; namespace MultiTerm.Core.ViewModel; @@ -13,6 +17,8 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie private const string defaultReceiveNewlineSeparatorAppSettingsKey = "DefaultReceiveNewlineSeparator"; private const string defaultSendNewlineSeparatorAppSettingsKey = "DefaultSendNewlineSeparator"; private readonly IAppSettingsProvider appSettings; + private readonly IMessenger messenger; + private readonly IContext context; public abstract string Title { get; } public abstract TerminalViewType ViewType { get; } @@ -22,7 +28,7 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie /// Holds communication data, meaning data that was sent to or received over the communication protocol. /// [ObservableProperty] - private CommunicationDataViewModel communicationData = new(null); + private CommunicationDataViewModel? communicationData; /// /// Newline Separator Type that is selected for receival data of this Terminal. Defaults to none. @@ -39,12 +45,15 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie private ICommunicationProtocol? communicationProtocol; public ICommunicationProtocol? CommunicationProtocol { - get { return communicationProtocol; } + get { return this.communicationProtocol; } set { - communicationProtocol = value; + this.communicationProtocol = value; // register communication protocol in the Communication Data View Model - //this.CommunicationData = new CommunicationDataViewModel(communicationProtocol); // TEMP + this.CommunicationData = new CommunicationDataViewModel(this.communicationProtocol, this.context); + + // initializes default newline separators, requires communicationData to be not null + this.InitializeNewlineSeparatorsFromAppSettings(); // initialize both newline Separators. initializes with null if they are not available. this.CommunicationData.ConfigureNewlineSeparators(this.ReceiveNewlineSeparatorType, this.SendNewlineSeparatorType); @@ -54,23 +63,60 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie private IProtocolSettingsViewModel? protocolSettings; public IProtocolSettingsViewModel? ProtocolSettings { - get { return protocolSettings; } + get { return this.protocolSettings; } set - { - protocolSettings = value; + { + this.protocolSettings = value; // register event handler for connection request from viewmodel if(value != null) { - protocolSettings!.ConnectRequested += OnViewModelRequestedConnect; - protocolSettings!.DisconnectRequested += OnViewModelRequestedDisconnect; + this.protocolSettings!.ConnectRequested += OnViewModelRequestedConnect; + this.protocolSettings!.DisconnectRequested += OnViewModelRequestedDisconnect; } } } - public TerminalViewModel(IAppSettingsProvider appSettings) + public TerminalViewModel(IAppSettingsProvider appSettings, IMessenger messenger, IContext context) { this.appSettings = appSettings; - this.InitializeNewlineSeparatorsFromAppSettings(); + this.messenger = messenger; + this.context = context; + } + + /// + /// Sends the given string to this instances , UTF-16 encoded. + /// Appends configured to the end of string. + /// + /// data string + protected void SendToCommunicationProtocol(string data) + { + // guard communication protocol null + if(this.CommunicationProtocol == null) { throw new NullReferenceException($"'{nameof(SendToCommunicationProtocol)}()' was called but {nameof(CommunicationProtocol)} is null."); } + + // inform user and quit if communication protocol is not connected + if (this.CommunicationProtocol.IsConnected == false) + { + this.messenger.Send(new ProtocolNotConnectedMessage("Cannot send message")); + return; + } + + // add newline sequence to end of data + string newlineSequence = this.SendNewlineSeparatorType switch + { + NewlineSeparatorType.None => String.Empty, + NewlineSeparatorType.CR => "\r", + NewlineSeparatorType.LF => "\n", + NewlineSeparatorType.CR_LF => "\r\n", + _ => throw new NotImplementedException($"'{nameof(SendToCommunicationProtocol)}()' " + + $"does not implement handling for {nameof(NewlineSeparatorType)} {this.SendNewlineSeparatorType}.") + }; + string modifiedData = data + newlineSequence; + + // extract unicode byte array + var byteArray = Encoding.Unicode.GetBytes(modifiedData); + + // send + this.CommunicationProtocol.SendBytes(byteArray); } #region Connect/Disconnect @@ -141,13 +187,13 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie partial void OnReceiveNewlineSeparatorTypeChanged(NewlineSeparatorType value) { // triggers rearranging the received data with the new NewlineSeparatorType - this.CommunicationData.ConfigureNewlineSeparators(value, null); + this.CommunicationData?.ConfigureNewlineSeparators(value, null); } partial void OnSendNewlineSeparatorTypeChanged(NewlineSeparatorType value) { // triggers rearranging the sent data with the new NewlineSeparatorType - this.CommunicationData.ConfigureNewlineSeparators(null, value); + this.CommunicationData?.ConfigureNewlineSeparators(null, value); } #endregion diff --git a/MultiTerm.Protocols/CommunicationProtocol.cs b/MultiTerm.Protocols/CommunicationProtocol.cs index 68b5e27..b63592a 100644 --- a/MultiTerm.Protocols/CommunicationProtocol.cs +++ b/MultiTerm.Protocols/CommunicationProtocol.cs @@ -1,4 +1,5 @@ using Common.Logging; +using Common.Messaging; using CommunityToolkit.Mvvm.Messaging; using MultiTerm.Protocols.Types; @@ -6,43 +7,72 @@ namespace MultiTerm.Protocols; public abstract class CommunicationProtocol : ICommunicationProtocol { - protected ILogger logger; + protected readonly ILogger logger; + protected readonly IMessenger messenger; private CancellationTokenSource cancellationTokenSource; private Thread? readingThread; public event EventHandler? ReceivedDataEvent; public event EventHandler? SentDataEvent; + public event EventHandler? DisconnectedEvent; public abstract ProtocolType ProtocolType { get; } - public CommunicationProtocol(ILogger logger) + public bool IsConnected { get; private set; } = false; + + public CommunicationProtocol(ILogger logger, IMessenger messenger) { // initialize other this.cancellationTokenSource = new CancellationTokenSource(); this.logger = logger; + this.messenger = messenger; } /// /// To be called when data was received from the connected device. /// - /// protected void OnReceivedData(ReceivedDataEventArgs e) { this.ReceivedDataEvent?.Invoke(this, e); } /// /// To be called when data was sent to the connected device. /// - /// protected void OnSentData(SentDataEventArgs e) { this.SentDataEvent?.Invoke(this, e); } + /// + /// To be called whenever the protocol detected that it is disconnected, but this is not a known fact. + /// + protected void OnUnintentionallyDisconnected() + { + // update state + this.IsConnected = false; + // log + this.logger.LogError($"'{nameof(OnUnintentionallyDisconnected)}()' called.", nameof(CommunicationProtocol)); + // raise event indicating an unintentional disconnect + this.DisconnectedEvent?.Invoke(this, new DisconnectedEventArgs(true)); + } + /// /// Allows to send bytes using the implemented protocol. /// /// data as bytes - protected abstract void InternalSendBytes(byte[] bytes); + /// true if the data was sent successfully + protected abstract bool InternalSendBytes(byte[] bytes); public void SendBytes(byte[] bytes) { - this.InternalSendBytes(bytes); + // guard is not connected => log warning. user of this function shall only use SendBytes if IsConnected is true + if (this.IsConnected == false) { this.logger.LogWarn($"'{nameof(SendBytes)}()' was reached with {nameof(IsConnected)} being false", nameof(CommunicationProtocol)); } + + if (this.InternalSendBytes(bytes)) + { + // todo implement sent bytes + // ONsentBytes() + } + else + { + this.logger.LogError($"'{nameof(SendBytes)}()' failed to send during {nameof(InternalSendBytes)}.", nameof(CommunicationProtocol)); + this.messenger.Send(new GenericUserInterfaceMessage("Failed to send message", MessageImportance.High)); + } } /// @@ -61,6 +91,13 @@ public abstract class CommunicationProtocol : ICommunicationProtocol public bool Connect(IProtocolSettings settings) { + // check if not already connected + if (this.IsConnected == true) + { + this.logger.LogWarn($"'{nameof(Connect)}()' was reached even if {nameof(IsConnected)} is already true.", nameof(CommunicationProtocol)); + return true; + } + // check if settings are valid, cancel if not if (settings.AreValid() == false) { @@ -68,7 +105,7 @@ public abstract class CommunicationProtocol : ICommunicationProtocol return false; } - // try connecting if serttings are valid + // try connecting if settings are valid if (this.InternalConnect(settings)) { // renew token source @@ -76,6 +113,8 @@ public abstract class CommunicationProtocol : ICommunicationProtocol // start internal reading thread this.readingThread = new Thread(() => this.InternalRead(this.cancellationTokenSource.Token)); this.readingThread.Start(); + // update state + this.IsConnected = true; return true; } else @@ -98,5 +137,9 @@ public abstract class CommunicationProtocol : ICommunicationProtocol this.readingThread.Join(); } this.InternalDisconnect(); + this.IsConnected = false; + + // raise event indicating an intentional disconnect + this.DisconnectedEvent?.Invoke(this, new DisconnectedEventArgs(false)); } } diff --git a/MultiTerm.Protocols/DisconnectedEventArgs.cs b/MultiTerm.Protocols/DisconnectedEventArgs.cs new file mode 100644 index 0000000..420c2a5 --- /dev/null +++ b/MultiTerm.Protocols/DisconnectedEventArgs.cs @@ -0,0 +1,16 @@ +namespace MultiTerm.Protocols; + +public class DisconnectedEventArgs : EventArgs +{ + /// + /// Indicates wether the disconnect was unintentional. + /// It False the disconnect was likely triggered by a user (e.g. manually clicking Disconnect). + /// If True the disconnect was unintentional. + /// + public bool Unintentional { get; private set; } + + public DisconnectedEventArgs(bool unintentional) + { + this.Unintentional = unintentional; + } +} diff --git a/MultiTerm.Protocols/ICommunicationProtocol.cs b/MultiTerm.Protocols/ICommunicationProtocol.cs index c0c42f5..5ab2c65 100644 --- a/MultiTerm.Protocols/ICommunicationProtocol.cs +++ b/MultiTerm.Protocols/ICommunicationProtocol.cs @@ -22,10 +22,25 @@ public interface ICommunicationProtocol /// event EventHandler? SentDataEvent; + /// + /// Thrown when the device disconnected. + /// + event EventHandler? DisconnectedEvent; + + /// + /// Indicates wether the communication protocol is connected. + /// True if connected and ready to receive data via , false if not. + /// + bool IsConnected { get; } + /// /// Connect to the device. /// /// settings required to connect and use the protocol + /// + /// true if connected sucessfully or already connected, + /// false if the provided settings are invalid or connection was unsuccessful + /// bool Connect(IProtocolSettings settings); /// diff --git a/MultiTerm.Protocols/Serial/SerialProtocol.cs b/MultiTerm.Protocols/Serial/SerialProtocol.cs index d85515a..4d45227 100644 --- a/MultiTerm.Protocols/Serial/SerialProtocol.cs +++ b/MultiTerm.Protocols/Serial/SerialProtocol.cs @@ -1,4 +1,5 @@ using Common.Logging; +using CommunityToolkit.Mvvm.Messaging; using MultiTerm.Protocols.Model; using MultiTerm.Protocols.Types; using RJCP.IO.Ports; @@ -13,7 +14,7 @@ public class SerialProtocol : CommunicationProtocol private SerialPortStream serialPort = new(); - public SerialProtocol(ILogger logger) : base(logger) { } + public SerialProtocol(ILogger logger, IMessenger messenger) : base(logger, messenger) { } protected override bool InternalConnect(IProtocolSettings protocolSettings) { @@ -40,7 +41,7 @@ public class SerialProtocol : CommunicationProtocol Handshake = new HandshakeLibraryEquivalentConverter().ConvertToLibraryType(this.serialSettings.Handshake), // define static settings - Encoding = Encoding.Unicode, + Encoding = Encoding.ASCII, ReadTimeout = 500, WriteTimeout = 500 }; @@ -81,12 +82,21 @@ public class SerialProtocol : CommunicationProtocol } } - protected override void InternalSendBytes(byte[] bytes) + protected override bool InternalSendBytes(byte[] bytes) { foreach (byte b in bytes) { - serialPort.WriteByte(b); + try + { + serialPort.WriteByte(b); + } + // When the Serial Port is closed and InvalidOperationException is thrown + catch(InvalidOperationException) + { + this.OnUnintentionallyDisconnected(); + } } + return true; } public static IEnumerable GetPortNames() diff --git a/MultiTerm.Wpf.CustomControl/ValueConverter/DataViewModelToStringConverter.cs b/MultiTerm.Wpf.CustomControl/ValueConverter/DataViewModelToStringConverter.cs index 375ca5d..f90e32d 100644 --- a/MultiTerm.Wpf.CustomControl/ValueConverter/DataViewModelToStringConverter.cs +++ b/MultiTerm.Wpf.CustomControl/ValueConverter/DataViewModelToStringConverter.cs @@ -33,6 +33,9 @@ public class DataViewModelToStringConverter : IValueConverter // guard type if (value is not IEnumerable enumerable) { throw new ArgumentException($"Can only convert from {nameof(IEnumerable)} value"); } + // guard empty enumerable + if (enumerable.Count() == 0) { return string.Empty; } + string returnString = string.Empty; // iterate through data, adding content to textbox diff --git a/MultiTerm.Wpf/App.xaml.cs b/MultiTerm.Wpf/App.xaml.cs index e0fbf58..0ec00b8 100644 --- a/MultiTerm.Wpf/App.xaml.cs +++ b/MultiTerm.Wpf/App.xaml.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Common.AppSettings; using MultiTerm.Protocols.Helpers; using CommunityToolkit.Mvvm.Messaging; +using Common; namespace MultiTerm.Wpf; @@ -25,6 +26,7 @@ public partial class App : Application .ConfigureServices((hostContext, services) => { services.AddSingleton(); + services.AddSingleton(new WpfContext()); services.AddSingleton(new SerilogLogger("C:/log/multiterm-log-.txt", true)); services.AddSingleton(new XmlAppSettingsProvider("C:/log/multiterm-config.xml")); services.AddSingleton(); diff --git a/MultiTerm.Wpf/WpfContext.cs b/MultiTerm.Wpf/WpfContext.cs new file mode 100644 index 0000000..446ea9c --- /dev/null +++ b/MultiTerm.Wpf/WpfContext.cs @@ -0,0 +1,54 @@ +using Common; +using System; +using System.Threading; +using System.Windows.Threading; + +namespace MultiTerm.Wpf; + +/// +/// Provides an implementation of the for WPF. +/// +public class WpfContext : IContext +{ + private readonly Dispatcher dispatcher; + + public bool IsSynchronized + { + get + { + return this.dispatcher.Thread == Thread.CurrentThread; + } + } + + /// + /// Instanciates a using the CurrentDispatcher. + /// + public WpfContext() : this(Dispatcher.CurrentDispatcher) { } + + /// + /// Instanciates a using the given . + /// + public WpfContext(Dispatcher dispatcher) + { + // guard null + if(dispatcher == null) { throw new ArgumentNullException(nameof(dispatcher)); } + + this.dispatcher = dispatcher; + } + + public void BeginInvoke(Action action) + { + // guard null + if (action == null) { throw new ArgumentNullException(nameof(action)); } + + this.dispatcher.BeginInvoke(action); + } + + public void Invoke(Action action) + { + // guard null + if (action == null) { throw new ArgumentNullException(nameof(action)); } + + this.dispatcher.Invoke(action); + } +}