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
master
Jonas Arnold 3 years ago
parent eac4757427
commit 92beb2ec45
  1. 29
      MultiTerm.Core/ViewModel/TerminalViewModel.cs
  2. 34
      MultiTerm.Protocols/CommunicationProtocol.cs
  3. 11
      MultiTerm.Protocols/ICommunicationProtocol.cs
  4. 17
      MultiTerm.Protocols/Serial/SerialProtocol.cs
  5. 24
      MultiTerm.Protocols/Types/ProtocolConnectionState.cs
  6. 4
      MultiTerm.Protocols/UsbHid/UsbHidProtocol.cs
  7. 1
      MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.cs
  8. 34
      MultiTerm.Wpf/ValueConverters/ProtocolConnectionStateToBrushConverter.cs
  9. 10
      MultiTerm.Wpf/View/ShellView.xaml

@ -54,9 +54,10 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie
{ {
this.Title = $"{this.CommunicationProtocol?.InstanceIdentifier}"; this.Title = $"{this.CommunicationProtocol?.InstanceIdentifier}";
} }
} }
#endregion #endregion
#region Observable Properties
/// <summary> /// <summary>
/// Holds communication data that was received via the communication protocol. /// Holds communication data that was received via the communication protocol.
/// </summary> /// </summary>
@ -87,6 +88,15 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie
[ObservableProperty] [ObservableProperty]
private IProtocolSettingsViewModel? protocolSettings; private IProtocolSettingsViewModel? protocolSettings;
/// <summary>
/// <see cref="ProtocolConnectionState"/> of <see cref="CommunicationProtocol"/> as ObservableProperty.
/// Required since <see cref="CommunicationProtocol"/> does not implement <see cref="ObservableObject"/>.
/// </summary>
[ObservableProperty]
private ProtocolConnectionState communicationProtocolConnectionState;
#endregion
public TerminalViewModel(IAppSettingsProvider appSettings, IMessenger messenger, IContext context) public TerminalViewModel(IAppSettingsProvider appSettings, IMessenger messenger, IContext context)
{ {
@ -108,7 +118,7 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie
// unsubscribe events // unsubscribe events
this.CommunicationProtocol.ReceivedDataEvent -= CommunicationProtocol_ReceivedDataEvent; this.CommunicationProtocol.ReceivedDataEvent -= CommunicationProtocol_ReceivedDataEvent;
this.CommunicationProtocol.SentDataEvent -= CommunicationProtocol_SentDataEvent; this.CommunicationProtocol.SentDataEvent -= CommunicationProtocol_SentDataEvent;
this.CommunicationProtocol.DisconnectedEvent -= CommunicationProtocol_DisconnectedEvent; this.CommunicationProtocol.ConnectionStateChangedEvent -= CommunicationProtocol_ConnectionStateChangedEvent;
// remove reference to object // remove reference to object
this.CommunicationProtocol = null; this.CommunicationProtocol = null;
} }
@ -126,7 +136,7 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie
{ {
this.CommunicationProtocol.ReceivedDataEvent += CommunicationProtocol_ReceivedDataEvent; this.CommunicationProtocol.ReceivedDataEvent += CommunicationProtocol_ReceivedDataEvent;
this.CommunicationProtocol.SentDataEvent += CommunicationProtocol_SentDataEvent; 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 // 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); 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 // 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); 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."); } 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 // inform user and quit if communication protocol is not connected
if (this.CommunicationProtocol.IsConnected == false) if (this.CommunicationProtocol.State != ProtocolConnectionState.Connected)
{ {
this.messenger.Send<IUserInterfaceMessage>(new ProtocolNotConnectedMessage("Cannot send message")); this.messenger.Send<IUserInterfaceMessage>(new ProtocolNotConnectedMessage("Cannot send message, Protocol is not connected."));
return false; return false;
} }

@ -19,12 +19,21 @@ public abstract class CommunicationProtocol : ICommunicationProtocol
public event EventHandler<ReceivedDataEventArgs>? ReceivedDataEvent; public event EventHandler<ReceivedDataEventArgs>? ReceivedDataEvent;
public event EventHandler<SentDataEventArgs>? SentDataEvent; public event EventHandler<SentDataEventArgs>? SentDataEvent;
public event EventHandler<DisconnectedEventArgs>? DisconnectedEvent; public event EventHandler<ProtocolConnectionState>? ConnectionStateChangedEvent;
public abstract ProtocolType ProtocolType { get; } public abstract ProtocolType ProtocolType { get; }
public abstract string InstanceIdentifier { get; protected set; } 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) 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. // perform all steps in a task, to prevent deadlock when this method is called from a thread that is joined.
Task.Run(() => Task.Run(() =>
{ {
// update state
this.IsConnected = false;
this.CancelThreads(); this.CancelThreads();
this.logger.LogError($"'{nameof(OnUnintentionallyDisconnected)}()' called.", nameof(CommunicationProtocol)); this.logger.LogError($"'{nameof(OnUnintentionallyDisconnected)}()' called.", nameof(CommunicationProtocol));
// raise event indicating an unintentional disconnect // update state
this.DisconnectedEvent?.Invoke(this, new DisconnectedEventArgs(true)); this.State = ProtocolConnectionState.UnintentionallyDisconnected;
}); });
} }
@ -75,9 +81,9 @@ public abstract class CommunicationProtocol : ICommunicationProtocol
bool success = true; bool success = true;
// guard is not connected => log warning. user of this function shall only use SendBytes if IsConnected is 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 return false; // return and do not send
} }
@ -111,9 +117,9 @@ public abstract class CommunicationProtocol : ICommunicationProtocol
public bool Connect(IProtocolSettings settings) public bool Connect(IProtocolSettings settings)
{ {
// check if not already connected // 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; return true;
} }
@ -138,7 +144,7 @@ public abstract class CommunicationProtocol : ICommunicationProtocol
this.readingThread = new Thread(() => this.InternalRead(this.cancellationTokenSource.Token)); this.readingThread = new Thread(() => this.InternalRead(this.cancellationTokenSource.Token));
this.readingThread.Start(); this.readingThread.Start();
// update state // update state
this.IsConnected = true; this.State = ProtocolConnectionState.Connected;
return true; return true;
} }
else else
@ -156,10 +162,8 @@ public abstract class CommunicationProtocol : ICommunicationProtocol
{ {
this.CancelThreads(); this.CancelThreads();
this.InternalDisconnect(); this.InternalDisconnect();
this.IsConnected = false;
// raise event indicating an intentional disconnect this.State = ProtocolConnectionState.NotConnected;
this.DisconnectedEvent?.Invoke(this, new DisconnectedEventArgs(false));
} }
/// <summary> /// <summary>

@ -28,15 +28,16 @@ public interface ICommunicationProtocol
event EventHandler<SentDataEventArgs>? SentDataEvent; event EventHandler<SentDataEventArgs>? SentDataEvent;
/// <summary> /// <summary>
/// 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 <see cref="SendBytes(byte[])"/>.
/// </summary> /// </summary>
event EventHandler<DisconnectedEventArgs>? DisconnectedEvent; ProtocolConnectionState State { get; }
/// <summary> /// <summary>
/// Indicates wether the communication protocol is connected. /// Raised when the <see cref="State"/> changed.
/// True if connected and ready to receive data via <see cref="SendBytes(byte[])"/>, false if not. /// The handed <see cref="ProtocolConnectionState"/> is the new state.
/// </summary> /// </summary>
bool IsConnected { get; } event EventHandler<ProtocolConnectionState>? ConnectionStateChangedEvent;
/// <summary> /// <summary>
/// Connect to the device. /// Connect to the device.

@ -75,8 +75,21 @@ public class SerialProtocol : CommunicationProtocol
{ {
while(ct.IsCancellationRequested == false) while(ct.IsCancellationRequested == false)
{ {
// reads character based on configured encoding (here ASCII) int readByte = -1;
int readByte = this.serialPort.ReadByte();
// 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 if (readByte != -1) // -1 = end of stream
{ {
// report new data with event // report new data with event

@ -0,0 +1,24 @@
using System.ComponentModel;
namespace MultiTerm.Protocols.Types;
public enum ProtocolConnectionState
{
/// <summary>
/// Indicates that the protocol is currently not connected or has not yet been connected.
/// </summary>
[Description("Not Connected")]
NotConnected,
/// <summary>
/// Indicates that the protocol is currently connected and ready to transmit/receive data.
/// </summary>
[Description("Connected")]
Connected,
/// <summary>
/// Indicates that the protocol disconnected unintentionally (e.g. device connection was interrupted).
/// </summary>
[Description("Unintentionally Disconnected")]
UnintentionallyDisconnected
}

@ -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 // 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)); 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 // check number of bytes to send
int numBytesToSend = nextBytes.Count(); 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)})"); throw new Exception($"'{nameof(InternalSendBytes)}()': Invalid of bytes to send ({nameof(numBytesToSend)})");

@ -241,7 +241,6 @@ public class MultiFormatDataView : Control
{ {
var newSelection = new ObservableCollection<ByteDataViewModel>(); var newSelection = new ObservableCollection<ByteDataViewModel>();
int selectionStartIndex = this.tbCharOnlyView!.SelectionStart; int selectionStartIndex = this.tbCharOnlyView!.SelectionStart;
// TEMP OLD
// extract text from the beginning to the start of the selected text // extract text from the beginning to the start of the selected text
var textFromBeginningToStartOfSelection = this.tbCharOnlyView!.Text.Substring(0, selectionStartIndex); 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!) // count amount of manually introduced newline sequences in this text section (these to not exist in the data source!)

@ -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;
/// <summary>
/// Converts a <see cref="ProtocolConnectionState"/> to a acoordingly colored <see cref="Brush"/>.
/// </summary>
[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();
}
}

@ -20,6 +20,7 @@
<conv:MessageImportanceToBrushConverter x:Key="MsgImportanceBrushConverter"/> <conv:MessageImportanceToBrushConverter x:Key="MsgImportanceBrushConverter"/>
<conv:MessageImportanceToFontWeightConverter x:Key="MsgImportanceFontWeightConverter"/> <conv:MessageImportanceToFontWeightConverter x:Key="MsgImportanceFontWeightConverter"/>
<conv:ProtocolTypeToIconConverter x:Key="ProtocolTypeIconConverter"/> <conv:ProtocolTypeToIconConverter x:Key="ProtocolTypeIconConverter"/>
<conv:ProtocolConnectionStateToBrushConverter x:Key="PrtclConnectionStateBrushConverter"/>
<!-- Data Sources --> <!-- Data Sources -->
<ObjectDataProvider x:Key="NewlineSeparatorTypeValues" <ObjectDataProvider x:Key="NewlineSeparatorTypeValues"
@ -144,10 +145,15 @@
<!-- Tab Template --> <!-- Tab Template -->
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Image Height="20" Stretch="Uniform" Margin="0 0 8 0" <Ellipse Width="15" Height="15" Margin="0 0 8 0"
Stroke="Black" StrokeThickness="0.7"
Fill="{Binding Path=CommunicationProtocolConnectionState, Converter={StaticResource PrtclConnectionStateBrushConverter}}"
ToolTip="{Binding Path=CommunicationProtocolConnectionState, Converter={StaticResource EnumDescriptionConverter}}"
ToolTipService.InitialShowDelay="200"/>
<Image Height="20" Stretch="Uniform" Margin="0 0 8 0"
Source="{Binding Path=ProtocolType, Converter={StaticResource ProtocolTypeIconConverter}}" Source="{Binding Path=ProtocolType, Converter={StaticResource ProtocolTypeIconConverter}}"
ToolTip="{Binding Path=ProtocolType,Converter={StaticResource EnumDescriptionConverter}}" ToolTip="{Binding Path=ProtocolType,Converter={StaticResource EnumDescriptionConverter}}"
ToolTipService.InitialShowDelay="0"/> ToolTipService.InitialShowDelay="200"/>
<TextBlock Text="{Binding Title, Mode=OneWay}" /> <TextBlock Text="{Binding Title, Mode=OneWay}" />
<Button Command="{Binding CloseRequestCommand}" Width="20" Padding="0" Margin="8 0 0 0" Content="X"> <Button Command="{Binding CloseRequestCommand}" Width="20" Padding="0" Margin="8 0 0 0" Content="X">
<Button.Style> <Button.Style>

Loading…
Cancel
Save