wired up real communication protocol to the CommunicationDataViewModel,

added DisconnectedEvent to CommunicationProtocol,
added handling for sent data while not connected,
introduced IContext and WpfContext to handover UI context to backend (required to add to ObservableCollection)
master
Jonas Arnold 3 years ago
parent 9ad7ae3430
commit 1fab46d5c5
  1. 21
      Common/Helpers/ContextHelpers.cs
  2. 24
      Common/IContext.cs
  3. 31
      Common/Messaging/GenericUserInterfaceMessage.cs
  4. 27
      MultiTerm.Core/Types/ProtocolNotConnectedMessage.cs
  5. 17
      MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs
  6. 11
      MultiTerm.Core/ViewModel/SendReceiveViewModel.cs
  7. 74
      MultiTerm.Core/ViewModel/TerminalViewModel.cs
  8. 57
      MultiTerm.Protocols/CommunicationProtocol.cs
  9. 16
      MultiTerm.Protocols/DisconnectedEventArgs.cs
  10. 15
      MultiTerm.Protocols/ICommunicationProtocol.cs
  11. 18
      MultiTerm.Protocols/Serial/SerialProtocol.cs
  12. 3
      MultiTerm.Wpf.CustomControl/ValueConverter/DataViewModelToStringConverter.cs
  13. 2
      MultiTerm.Wpf/App.xaml.cs
  14. 54
      MultiTerm.Wpf/WpfContext.cs

@ -0,0 +1,21 @@
namespace Common.Helpers;
public static class ContextHelpers
{
/// <summary>
/// Invokes context if necessary and runs action. If Threads are synchronized, runs action without switching context.
/// </summary>
/// <param name="context">context to run action on</param>
/// <param name="action">action to run</param>
public static void InvokeIfNecessary(IContext context, Action action)
{
if (context.IsSynchronized == false)
{
context.Invoke(action);
}
else
{
action.Invoke();
}
}
}

@ -0,0 +1,24 @@
namespace Common;
/// <summary>
/// Provides Context of some thread. Allows user of the interface to run actions on the thread of the implementer.
/// </summary>
public interface IContext
{
/// <summary>
/// Indicates wether the thread this property gotten from is equal to the implementers thread.
/// </summary>
bool IsSynchronized { get; }
/// <summary>
/// Run <paramref name="action"/> on the implementers thread.
/// </summary>
/// <param name="action">action to run</param>
void Invoke(Action action);
/// <summary>
/// Start running <paramref name="action"/> on the implementers thread.
/// </summary>
/// <param name="action">action to start</param>
void BeginInvoke(Action action);
}

@ -0,0 +1,31 @@
namespace Common.Messaging;
public class GenericUserInterfaceMessage : IUserInterfaceMessage
{
/// <summary>
/// Description of the Message. Containes messsage text.
/// </summary>
public string Message { get; private set; }
/// <summary>
/// Importance of this message.
/// </summary>
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;
}
}

@ -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.";
}
}
}

@ -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<DataViewModel>? 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))
{

@ -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.
/// </summary>
/// <param name="appSettings"></param>
public SendReceiveViewModel(IAppSettingsProvider appSettings) : base(appSettings) { }
public SendReceiveViewModel(IAppSettingsProvider appSettings, IMessenger messenger, IContext context) : base(appSettings, messenger, context) { }
/// <summary>
/// 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();
}
}

@ -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.
/// </summary>
[ObservableProperty]
private CommunicationDataViewModel communicationData = new(null);
private CommunicationDataViewModel? communicationData;
/// <summary>
/// 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;
}
/// <summary>
/// Sends the given string to this instances <see cref="CommunicationProtocol"/>, UTF-16 encoded.
/// Appends configured <see cref="SendNewlineSeparatorType"/> to the end of string.
/// </summary>
/// <param name="data">data string</param>
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<IUserInterfaceMessage>(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

@ -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<ReceivedDataEventArgs>? ReceivedDataEvent;
public event EventHandler<SentDataEventArgs>? SentDataEvent;
public event EventHandler<DisconnectedEventArgs>? 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;
}
/// <summary>
/// To be called when data was received from the connected device.
/// </summary>
/// <param name="e"><see cref="ReceivedDataEventArgs"/></param>
protected void OnReceivedData(ReceivedDataEventArgs e) { this.ReceivedDataEvent?.Invoke(this, e); }
/// <summary>
/// To be called when data was sent to the connected device.
/// </summary>
/// <param name="e"><see cref="SentDataEventArgs"/></param>
protected void OnSentData(SentDataEventArgs e) { this.SentDataEvent?.Invoke(this, e); }
/// <summary>
/// To be called whenever the protocol detected that it is disconnected, but this is not a known fact.
/// </summary>
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));
}
/// <summary>
/// Allows to send bytes using the implemented protocol.
/// </summary>
/// <param name="bytes">data as bytes</param>
protected abstract void InternalSendBytes(byte[] bytes);
/// <returns>true if the data was sent successfully</returns>
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<IUserInterfaceMessage>(new GenericUserInterfaceMessage("Failed to send message", MessageImportance.High));
}
}
/// <summary>
@ -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));
}
}

@ -0,0 +1,16 @@
namespace MultiTerm.Protocols;
public class DisconnectedEventArgs : EventArgs
{
/// <summary>
/// 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.
/// </summary>
public bool Unintentional { get; private set; }
public DisconnectedEventArgs(bool unintentional)
{
this.Unintentional = unintentional;
}
}

@ -22,10 +22,25 @@ public interface ICommunicationProtocol
/// </summary>
event EventHandler<SentDataEventArgs>? SentDataEvent;
/// <summary>
/// Thrown when the device disconnected.
/// </summary>
event EventHandler<DisconnectedEventArgs>? DisconnectedEvent;
/// <summary>
/// Indicates wether the communication protocol is connected.
/// True if connected and ready to receive data via <see cref="SendBytes(byte[])"/>, false if not.
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Connect to the device.
/// </summary>
/// <param name="settings">settings required to connect and use the protocol</param>
/// <returns>
/// true if connected sucessfully or already connected,
/// false if the provided settings are invalid or connection was unsuccessful
/// </returns>
bool Connect(IProtocolSettings settings);
/// <summary>

@ -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<string> GetPortNames()

@ -33,6 +33,9 @@ public class DataViewModelToStringConverter : IValueConverter
// guard type
if (value is not IEnumerable<DataViewModel> enumerable) { throw new ArgumentException($"Can only convert from {nameof(IEnumerable<DataViewModel>)} value"); }
// guard empty enumerable
if (enumerable.Count() == 0) { return string.Empty; }
string returnString = string.Empty;
// iterate through data, adding content to textbox

@ -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<MainWindow>();
services.AddSingleton<IContext>(new WpfContext());
services.AddSingleton<ILogger>(new SerilogLogger("C:/log/multiterm-log-.txt", true));
services.AddSingleton<IAppSettingsProvider>(new XmlAppSettingsProvider("C:/log/multiterm-config.xml"));
services.AddSingleton<IMessenger, WeakReferenceMessenger>();

@ -0,0 +1,54 @@
using Common;
using System;
using System.Threading;
using System.Windows.Threading;
namespace MultiTerm.Wpf;
/// <summary>
/// Provides an implementation of the <see cref="IContext"/> for WPF.
/// </summary>
public class WpfContext : IContext
{
private readonly Dispatcher dispatcher;
public bool IsSynchronized
{
get
{
return this.dispatcher.Thread == Thread.CurrentThread;
}
}
/// <summary>
/// Instanciates a <see cref="WpfContext"/> using the CurrentDispatcher.
/// </summary>
public WpfContext() : this(Dispatcher.CurrentDispatcher) { }
/// <summary>
/// Instanciates a <see cref="WpfContext"/> using the given <paramref name="dispatcher"/>.
/// </summary>
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);
}
}
Loading…
Cancel
Save