From 92beb2ec452feadb98e07ef81341a44b5d1bf4ed Mon Sep 17 00:00:00 2001 From: Jonas Arnold Date: Sun, 7 May 2023 20:45:50 +0200 Subject: [PATCH] changed IsConnected boolean to State of Enum ProtocolConnectionState, changed DisconnectedEvent to ConnectionStateChanged event, fixed exception while unintentional disconnect in SerialProtocol, implemented connection state indicator in tab header, resolved warnings --- MultiTerm.Core/ViewModel/TerminalViewModel.cs | 29 +++++++++++----- MultiTerm.Protocols/CommunicationProtocol.cs | 34 +++++++++++-------- MultiTerm.Protocols/ICommunicationProtocol.cs | 11 +++--- MultiTerm.Protocols/Serial/SerialProtocol.cs | 17 ++++++++-- .../Types/ProtocolConnectionState.cs | 24 +++++++++++++ MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs | 4 +-- .../MultiFormatDataView.cs | 1 - ...ProtocolConnectionStateToBrushConverter.cs | 34 +++++++++++++++++++ MultiTerm.Wpf/View/ShellView.xaml | 10 ++++-- 9 files changed, 129 insertions(+), 35 deletions(-) create mode 100644 MultiTerm.Protocols/Types/ProtocolConnectionState.cs create mode 100644 MultiTerm.Wpf/ValueConverters/ProtocolConnectionStateToBrushConverter.cs diff --git a/MultiTerm.Core/ViewModel/TerminalViewModel.cs b/MultiTerm.Core/ViewModel/TerminalViewModel.cs index f3a5c69..5b266a8 100644 --- a/MultiTerm.Core/ViewModel/TerminalViewModel.cs +++ b/MultiTerm.Core/ViewModel/TerminalViewModel.cs @@ -54,9 +54,10 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie { this.Title = $"{this.CommunicationProtocol?.InstanceIdentifier}"; } - } + } #endregion + #region Observable Properties /// /// Holds communication data that was received via the communication protocol. /// @@ -87,6 +88,15 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie [ObservableProperty] private IProtocolSettingsViewModel? protocolSettings; + /// + /// of as ObservableProperty. + /// Required since does not implement . + /// + [ObservableProperty] + private ProtocolConnectionState communicationProtocolConnectionState; + + #endregion + public TerminalViewModel(IAppSettingsProvider appSettings, IMessenger messenger, IContext context) { @@ -108,7 +118,7 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie // unsubscribe events this.CommunicationProtocol.ReceivedDataEvent -= CommunicationProtocol_ReceivedDataEvent; this.CommunicationProtocol.SentDataEvent -= CommunicationProtocol_SentDataEvent; - this.CommunicationProtocol.DisconnectedEvent -= CommunicationProtocol_DisconnectedEvent; + this.CommunicationProtocol.ConnectionStateChangedEvent -= CommunicationProtocol_ConnectionStateChangedEvent; // remove reference to object this.CommunicationProtocol = null; } @@ -126,7 +136,7 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie { this.CommunicationProtocol.ReceivedDataEvent += CommunicationProtocol_ReceivedDataEvent; this.CommunicationProtocol.SentDataEvent += CommunicationProtocol_SentDataEvent; - this.CommunicationProtocol.DisconnectedEvent += CommunicationProtocol_DisconnectedEvent; + this.CommunicationProtocol.ConnectionStateChangedEvent += CommunicationProtocol_ConnectionStateChangedEvent; } // initializes default newline separators, updates them directly inside ReceivedData and SentData objects @@ -153,11 +163,14 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie this.SentData.HandleNewData(e.SentData); } - private void CommunicationProtocol_DisconnectedEvent(object? sender, DisconnectedEventArgs e) + private void CommunicationProtocol_ConnectionStateChangedEvent(object? sender, ProtocolConnectionState e) { - // on unintentional disconnect the view model may still display connected => force to disconnected state + // update internal state + this.CommunicationProtocolConnectionState = e; + + // If ViewModel still displays connected after unintentional disconnect => force to disconnected state // allows user to change settings - if(e.Unintentional == true && this.ProtocolSettings!.DisplaysConnected == true) + if (e == ProtocolConnectionState.UnintentionallyDisconnected && this.ProtocolSettings!.DisplaysConnected == true) { this.ProtocolSettings.ForceConnectedState(false); } @@ -177,9 +190,9 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie 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) + if (this.CommunicationProtocol.State != ProtocolConnectionState.Connected) { - this.messenger.Send(new ProtocolNotConnectedMessage("Cannot send message")); + this.messenger.Send(new ProtocolNotConnectedMessage("Cannot send message, Protocol is not connected.")); return false; } diff --git a/MultiTerm.Protocols/CommunicationProtocol.cs b/MultiTerm.Protocols/CommunicationProtocol.cs index f95a327..cf95bb6 100644 --- a/MultiTerm.Protocols/CommunicationProtocol.cs +++ b/MultiTerm.Protocols/CommunicationProtocol.cs @@ -19,12 +19,21 @@ public abstract class CommunicationProtocol : ICommunicationProtocol public event EventHandler? ReceivedDataEvent; public event EventHandler? SentDataEvent; - public event EventHandler? DisconnectedEvent; + public event EventHandler? ConnectionStateChangedEvent; public abstract ProtocolType ProtocolType { get; } public abstract string InstanceIdentifier { get; protected set; } - public bool IsConnected { get; private set; } = false; + private ProtocolConnectionState state = ProtocolConnectionState.NotConnected; + public ProtocolConnectionState State + { + get { return state; } + set + { + state = value; + this.ConnectionStateChangedEvent?.Invoke(this, state); + } + } public CommunicationProtocol(ILogger logger, IMessenger messenger) { @@ -52,14 +61,11 @@ public abstract class CommunicationProtocol : ICommunicationProtocol // perform all steps in a task, to prevent deadlock when this method is called from a thread that is joined. Task.Run(() => { - // update state - this.IsConnected = false; - this.CancelThreads(); this.logger.LogError($"'{nameof(OnUnintentionallyDisconnected)}()' called.", nameof(CommunicationProtocol)); - // raise event indicating an unintentional disconnect - this.DisconnectedEvent?.Invoke(this, new DisconnectedEventArgs(true)); + // update state + this.State = ProtocolConnectionState.UnintentionallyDisconnected; }); } @@ -75,9 +81,9 @@ public abstract class CommunicationProtocol : ICommunicationProtocol bool success = true; // guard is not connected => log warning. user of this function shall only use SendBytes if IsConnected is true - if (this.IsConnected == false) + if (this.State != ProtocolConnectionState.Connected) { - this.logger.LogWarn($"'{nameof(SendBytes)}()' was reached with {nameof(IsConnected)} being false", nameof(CommunicationProtocol)); + this.logger.LogWarn($"'{nameof(SendBytes)}()' was reached with wrong {nameof(ProtocolConnectionState)} of {this.State}", nameof(CommunicationProtocol)); return false; // return and do not send } @@ -111,9 +117,9 @@ public abstract class CommunicationProtocol : ICommunicationProtocol public bool Connect(IProtocolSettings settings) { // check if not already connected - if (this.IsConnected == true) + if (this.State == ProtocolConnectionState.Connected) { - this.logger.LogWarn($"'{nameof(Connect)}()' was reached even if {nameof(IsConnected)} is already true.", nameof(CommunicationProtocol)); + this.logger.LogWarn($"'{nameof(Connect)}()' was reached with wrong {nameof(ProtocolConnectionState)} of {this.State}", nameof(CommunicationProtocol)); return true; } @@ -138,7 +144,7 @@ public abstract class CommunicationProtocol : ICommunicationProtocol this.readingThread = new Thread(() => this.InternalRead(this.cancellationTokenSource.Token)); this.readingThread.Start(); // update state - this.IsConnected = true; + this.State = ProtocolConnectionState.Connected; return true; } else @@ -156,10 +162,8 @@ public abstract class CommunicationProtocol : ICommunicationProtocol { this.CancelThreads(); this.InternalDisconnect(); - this.IsConnected = false; - // raise event indicating an intentional disconnect - this.DisconnectedEvent?.Invoke(this, new DisconnectedEventArgs(false)); + this.State = ProtocolConnectionState.NotConnected; } /// diff --git a/MultiTerm.Protocols/ICommunicationProtocol.cs b/MultiTerm.Protocols/ICommunicationProtocol.cs index 58aa487..8753739 100644 --- a/MultiTerm.Protocols/ICommunicationProtocol.cs +++ b/MultiTerm.Protocols/ICommunicationProtocol.cs @@ -28,15 +28,16 @@ public interface ICommunicationProtocol event EventHandler? SentDataEvent; /// - /// Thrown when the device disconnected. + /// Reports the state of connection to the communication protocol. + /// This property shall be used to check if the protocol is ready to receive data via . /// - event EventHandler? DisconnectedEvent; + ProtocolConnectionState State { get; } /// - /// Indicates wether the communication protocol is connected. - /// True if connected and ready to receive data via , false if not. + /// Raised when the changed. + /// The handed is the new state. /// - bool IsConnected { get; } + event EventHandler? ConnectionStateChangedEvent; /// /// Connect to the device. diff --git a/MultiTerm.Protocols/Serial/SerialProtocol.cs b/MultiTerm.Protocols/Serial/SerialProtocol.cs index fed8d56..48674bb 100644 --- a/MultiTerm.Protocols/Serial/SerialProtocol.cs +++ b/MultiTerm.Protocols/Serial/SerialProtocol.cs @@ -75,8 +75,21 @@ public class SerialProtocol : CommunicationProtocol { while(ct.IsCancellationRequested == false) { - // reads character based on configured encoding (here ASCII) - int readByte = this.serialPort.ReadByte(); + int readByte = -1; + + // try reading + try + { + // reads character based on configured encoding (here ASCII) + readByte = this.serialPort.ReadByte(); + } + catch (IOException ex) // thrown when a device disconnected + { + this.logger.LogException(ex, $"Exception while reading data in {nameof(InternalRead)}", nameof(SerialProtocol)); + this.OnUnintentionallyDisconnected(); // report disconnect + break; // break loop + } + if (readByte != -1) // -1 = end of stream { // report new data with event diff --git a/MultiTerm.Protocols/Types/ProtocolConnectionState.cs b/MultiTerm.Protocols/Types/ProtocolConnectionState.cs new file mode 100644 index 0000000..e8c83a1 --- /dev/null +++ b/MultiTerm.Protocols/Types/ProtocolConnectionState.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; + +namespace MultiTerm.Protocols.Types; + +public enum ProtocolConnectionState +{ + /// + /// Indicates that the protocol is currently not connected or has not yet been connected. + /// + [Description("Not Connected")] + NotConnected, + + /// + /// Indicates that the protocol is currently connected and ready to transmit/receive data. + /// + [Description("Connected")] + Connected, + + /// + /// Indicates that the protocol disconnected unintentionally (e.g. device connection was interrupted). + /// + [Description("Unintentionally Disconnected")] + UnintentionallyDisconnected +} diff --git a/MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs b/MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs index 6a64d60..8c576dc 100644 --- a/MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs +++ b/MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs @@ -125,10 +125,10 @@ public class UsbHidProtocol : CommunicationProtocol // 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."); + 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(); + int numBytesToSend = nextBytes.Count; if (numBytesToSend > MaxBytesPerReport || numBytesToSend > byte.MaxValue || numBytesToSend <= 0) { throw new Exception($"'{nameof(InternalSendBytes)}()': Invalid of bytes to send ({nameof(numBytesToSend)})"); diff --git a/MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.cs b/MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.cs index 32b17be..d83c39c 100644 --- a/MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.cs +++ b/MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.cs @@ -241,7 +241,6 @@ public class MultiFormatDataView : Control { var newSelection = new ObservableCollection(); int selectionStartIndex = this.tbCharOnlyView!.SelectionStart; - // TEMP OLD // extract text from the beginning to the start of the selected text var textFromBeginningToStartOfSelection = this.tbCharOnlyView!.Text.Substring(0, selectionStartIndex); // count amount of manually introduced newline sequences in this text section (these to not exist in the data source!) diff --git a/MultiTerm.Wpf/ValueConverters/ProtocolConnectionStateToBrushConverter.cs b/MultiTerm.Wpf/ValueConverters/ProtocolConnectionStateToBrushConverter.cs new file mode 100644 index 0000000..1b11e58 --- /dev/null +++ b/MultiTerm.Wpf/ValueConverters/ProtocolConnectionStateToBrushConverter.cs @@ -0,0 +1,34 @@ +using MultiTerm.Protocols.Types; +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +namespace MultiTerm.Wpf.ValueConverters; + +/// +/// Converts a to a acoordingly colored . +/// +[ValueConversion(typeof(ProtocolConnectionState), typeof(Brush))] +public class ProtocolConnectionStateToBrushConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not ProtocolConnectionState protocolConnectionState) + { throw new ArgumentException($"Wrong object provided, can only convert from type {nameof(ProtocolConnectionState)}"); } + + return protocolConnectionState switch + { + ProtocolConnectionState.NotConnected => Brushes.LightGray, + ProtocolConnectionState.Connected => Brushes.LightGreen, + ProtocolConnectionState.UnintentionallyDisconnected => Brushes.OrangeRed, + _ => throw new NotImplementedException(), + }; + + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/MultiTerm.Wpf/View/ShellView.xaml b/MultiTerm.Wpf/View/ShellView.xaml index b93566f..ca10d39 100644 --- a/MultiTerm.Wpf/View/ShellView.xaml +++ b/MultiTerm.Wpf/View/ShellView.xaml @@ -20,6 +20,7 @@ + - + + ToolTipService.InitialShowDelay="200"/>