From 64fceb0fee7c86effe34335bb822fb85004869d7 Mon Sep 17 00:00:00 2001 From: Jonas Arnold Date: Sat, 6 May 2023 16:57:17 +0200 Subject: [PATCH] implemented forcing state change of settingsVM when UnintentionalDisconnect happened, made Sending and Receiving with USB HID more robust --- MultiTerm.Core/ViewModel/TerminalViewModel.cs | 12 +++++ MultiTerm.Protocols/CommunicationProtocol.cs | 18 ++++--- .../IProtocolSettingsViewModel.cs | 18 ++++++- .../ProtocolSettingsViewModel.cs | 32 +++++++++--- MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs | 50 ++++++++++++------- 5 files changed, 99 insertions(+), 31 deletions(-) diff --git a/MultiTerm.Core/ViewModel/TerminalViewModel.cs b/MultiTerm.Core/ViewModel/TerminalViewModel.cs index b4c8c9c..c3045ba 100644 --- a/MultiTerm.Core/ViewModel/TerminalViewModel.cs +++ b/MultiTerm.Core/ViewModel/TerminalViewModel.cs @@ -108,6 +108,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; // remove reference to object this.CommunicationProtocol = null; } @@ -125,6 +126,7 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie { this.CommunicationProtocol.ReceivedDataEvent += CommunicationProtocol_ReceivedDataEvent; this.CommunicationProtocol.SentDataEvent += CommunicationProtocol_SentDataEvent; + this.CommunicationProtocol.DisconnectedEvent += CommunicationProtocol_DisconnectedEvent; } // initializes default newline separators, updates them directly inside ReceivedData and SentData objects @@ -151,6 +153,16 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie this.SentData.HandleNewData(e.SentData); } + private void CommunicationProtocol_DisconnectedEvent(object? sender, DisconnectedEventArgs e) + { + // on unintentional disconnect the view model may still display connected => force to disconnected state + // allows user to change settings + if(e.Unintentional == true && this.ProtocolSettings!.DisplaysConnected == true) + { + this.ProtocolSettings.ForceConnectedState(false); + } + } + #endregion /// diff --git a/MultiTerm.Protocols/CommunicationProtocol.cs b/MultiTerm.Protocols/CommunicationProtocol.cs index 50a065f..f95a327 100644 --- a/MultiTerm.Protocols/CommunicationProtocol.cs +++ b/MultiTerm.Protocols/CommunicationProtocol.cs @@ -49,14 +49,18 @@ public abstract class CommunicationProtocol : ICommunicationProtocol /// protected void OnUnintentionallyDisconnected() { - // update state - this.IsConnected = false; + // 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)); + this.CancelThreads(); + this.logger.LogError($"'{nameof(OnUnintentionallyDisconnected)}()' called.", nameof(CommunicationProtocol)); - // raise event indicating an unintentional disconnect - this.DisconnectedEvent?.Invoke(this, new DisconnectedEventArgs(true)); + // raise event indicating an unintentional disconnect + this.DisconnectedEvent?.Invoke(this, new DisconnectedEventArgs(true)); + }); } /// @@ -84,7 +88,7 @@ public abstract class CommunicationProtocol : ICommunicationProtocol if (success == false) { this.logger.LogError($"'{nameof(SendBytes)}()' failed to send during {nameof(InternalSendBytes)}.", nameof(CommunicationProtocol)); - this.messenger.Send(new GenericUserInterfaceMessage("Failed to send message", MessageImportance.High)); + this.messenger.Send(new GenericUserInterfaceMessage("Failed to send data, for more information please check logfile.", MessageImportance.High)); } return success; diff --git a/MultiTerm.Protocols/IProtocolSettingsViewModel.cs b/MultiTerm.Protocols/IProtocolSettingsViewModel.cs index 8306773..51fcf9e 100644 --- a/MultiTerm.Protocols/IProtocolSettingsViewModel.cs +++ b/MultiTerm.Protocols/IProtocolSettingsViewModel.cs @@ -1,4 +1,5 @@ -using MultiTerm.Protocols.Types; +using CommunityToolkit.Mvvm.Input; +using MultiTerm.Protocols.Types; namespace MultiTerm.Protocols; @@ -9,6 +10,11 @@ public interface IProtocolSettingsViewModel /// ProtocolType ProtocolType { get; } + /// + /// Indicates if the displays ifself as if the protocol is connected. + /// + bool DisplaysConnected { get; } + /// /// Indicates wether the settings can currently be edited. /// @@ -24,4 +30,14 @@ public interface IProtocolSettingsViewModel /// Event that is thrown when the user requested to disconnect from the device. /// event EventHandler? DisconnectRequested; + + /// + /// Change the state from connected to disconnected or from disconnected to connected. + /// Does not throw or . + /// + /// + /// if true changes the state from disconnected to connected. + /// if false changes the state from connected to disconnected. + /// + void ForceConnectedState(bool connected); } \ No newline at end of file diff --git a/MultiTerm.Protocols/ProtocolSettingsViewModel.cs b/MultiTerm.Protocols/ProtocolSettingsViewModel.cs index af23709..86093bb 100644 --- a/MultiTerm.Protocols/ProtocolSettingsViewModel.cs +++ b/MultiTerm.Protocols/ProtocolSettingsViewModel.cs @@ -19,6 +19,15 @@ public abstract partial class ProtocolSettingsViewModel : ObservableObject, IPro public event EventHandler? ConnectRequested; public event EventHandler? DisconnectRequested; + public bool DisplaysConnected { + get + { + if (this.ConnectDisconnectButtonText == connectedStateButtonText) return true; + else if (this.ConnectDisconnectButtonText == disconnectedStateButtonText) return false; + else { throw new Exception($"'{nameof(DisplaysConnected)}' has not recognized button text, cannot identify state of connection."); } + } + } + public abstract ProtocolType ProtocolType { get; } [ObservableProperty] @@ -44,7 +53,7 @@ public abstract partial class ProtocolSettingsViewModel : ObservableObject, IPro await Task.Factory.StartNew(() => { // if currently disconnected - if (this.ConnectDisconnectButtonText == disconnectedStateButtonText) + if (this.DisplaysConnected == false) { // CONNECT this.AreEditable = false; @@ -64,20 +73,31 @@ public abstract partial class ProtocolSettingsViewModel : ObservableObject, IPro { // rollback this.AreEditable = true; + } } // if currently connected - else if (this.ConnectDisconnectButtonText == connectedStateButtonText) + else { // DISCONNECT this.DisconnectRequested?.Invoke(this, EventArgs.Empty); this.ConnectDisconnectButtonText = disconnectedStateButtonText; this.AreEditable = true; } - else - { - throw new Exception($"'{nameof(ConnectDisconnectAsync)}()' has not recognized button text, cannot identify state of connection."); - } }); } + + public void ForceConnectedState(bool connected) + { + if (connected) + { + this.ConnectDisconnectButtonText = connectedStateButtonText; + this.AreEditable = false; + } + else + { + this.ConnectDisconnectButtonText = disconnectedStateButtonText; + this.AreEditable = true; + } + } } diff --git a/MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs b/MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs index 99be545..3b1e970 100644 --- a/MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs +++ b/MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs @@ -77,27 +77,34 @@ public class UsbHidProtocol : CommunicationProtocol { while (ct.IsCancellationRequested == false) { + // if usb hid device is null => something is wrong => break loop + if (this.usbHidDevice == null) + { + this.OnUnintentionallyDisconnected(); + break; + } + ReadOnlySpan readData; - if (this.usbHidDevice != null) + try { // 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 + catch // on exception => break loop { this.OnUnintentionallyDisconnected(); break; } + + // any data received? + if (readData.Length > 0) + { + foreach (var readByte in readData) + { + // report new byte with event + this.OnReceivedData(new ExtendedByte((byte)readByte)); + } + } } } @@ -108,7 +115,7 @@ public class UsbHidProtocol : CommunicationProtocol Debug.WriteLine($"{nameof(InternalSendBytes)}() of {nameof(UsbHidProtocol)} has {bytesToSend.Count} bytes to send."); - while(bytesToSend.Count > 0) + 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)); @@ -120,7 +127,7 @@ public class UsbHidProtocol : CommunicationProtocol // check number of bytes to send int numBytesToSend = nextBytes.Count(); - if(numBytesToSend > MaxBytesPerReport || numBytesToSend > byte.MaxValue || numBytesToSend <= 0) + if (numBytesToSend > MaxBytesPerReport || numBytesToSend > byte.MaxValue || numBytesToSend <= 0) { throw new Exception($"'{nameof(InternalSendBytes)}()': Invalid of bytes to send ({nameof(numBytesToSend)})"); } @@ -137,10 +144,19 @@ public class UsbHidProtocol : CommunicationProtocol return false; } - this.usbHidDevice.Write(bytes); + try + { + this.usbHidDevice.Write(sendableBytes.ToArray()); + } + catch(Exception ex) + { + this.logger.LogException(ex, $"Failed to Write Data to USB HID device " + + $"VID={this.usbHidSettings!.VendorId}, PID={this.usbHidSettings.ProductId}.", nameof(UsbHidProtocol)); + return false; + } // report sent bytes - foreach (byte b in bytes) + foreach (byte b in sendableBytes) { this.OnSentData(new ExtendedByte(b)); } @@ -148,7 +164,7 @@ public class UsbHidProtocol : CommunicationProtocol return true; } - + public static IEnumerable GetDevices() { var libDevicesInfo = Hid.Enumerate();