diff --git a/Common/Helpers/RecurringTimer.cs b/Common/Helpers/RecurringTimer.cs
new file mode 100644
index 0000000..a21f973
--- /dev/null
+++ b/Common/Helpers/RecurringTimer.cs
@@ -0,0 +1,51 @@
+namespace Common.Helpers;
+
+///
+/// A that recurrs after a given interval.
+/// The callback is non-reentrant, unline a standard .
+///
+public class RecurringTimer : IDisposable
+{
+ private readonly System.Timers.Timer _timer;
+
+ ///
+ /// Instanciates the recurring timer, with the given.
+ ///
+ /// interval to act on callback, in milliseconds
+ /// action to perform when the timer elapses
+ public RecurringTimer(int intervalMs, Action callbackAction)
+ {
+ _timer = new System.Timers.Timer()
+ {
+ AutoReset = false,
+ Interval = intervalMs
+ };
+
+ _timer.Elapsed += delegate
+ {
+ callbackAction(); // perform callback action
+ _timer.Start(); // manual restart after action finished
+ };
+ }
+
+ ///
+ /// Starts the timing by setting to .
+ ///
+ public void Start()
+ {
+ _timer.Start();
+ }
+
+ ///
+ /// Stops the timing by setting to .
+ ///
+ public void Stop()
+ {
+ _timer.Stop();
+ }
+
+ public void Dispose()
+ {
+ _timer?.Dispose();
+ }
+}
diff --git a/MultiTerm.Core/ViewModel/MultiFormatDataViewModel.cs b/MultiTerm.Core/ViewModel/MultiFormatDataViewModel.cs
index 86053e5..1055366 100644
--- a/MultiTerm.Core/ViewModel/MultiFormatDataViewModel.cs
+++ b/MultiTerm.Core/ViewModel/MultiFormatDataViewModel.cs
@@ -16,8 +16,7 @@ public partial class MultiFormatDataViewModel : ObservableObject
private int dataCharacterCount = 0;
private List? listOfPreviousCharacters = null;
- #region ICommunicationDataViewModel Implementation
-
+ #region Observable Properties
[ObservableProperty]
private ObservableCollection data = new();
@@ -40,7 +39,7 @@ public partial class MultiFormatDataViewModel : ObservableObject
///
/// Newline Sequence string inserted on every newline in the .
///
- public static string NewlineSequence = Environment.NewLine;
+ public static readonly string NewlineSequence = Environment.NewLine;
public MultiFormatDataViewModel(IContext context)
{
diff --git a/MultiTerm.Core/ViewModel/TerminalViewModel.cs b/MultiTerm.Core/ViewModel/TerminalViewModel.cs
index fa9aec9..a1fcb48 100644
--- a/MultiTerm.Core/ViewModel/TerminalViewModel.cs
+++ b/MultiTerm.Core/ViewModel/TerminalViewModel.cs
@@ -1,5 +1,4 @@
-using Common;
-using Common.AppSettings;
+using Common.AppSettings;
using Common.Helpers;
using Common.Messaging;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -9,6 +8,7 @@ using MultiTerm.Core.Types;
using MultiTerm.Protocols;
using MultiTerm.Protocols.Model;
using MultiTerm.Protocols.Types;
+using System.Collections.Concurrent;
namespace MultiTerm.Core.ViewModel;
@@ -18,6 +18,11 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie
private const string defaultSendNewlineSeparatorAppSettingsKey = "DefaultSendNewlineSeparator";
private readonly IAppSettingsProvider appSettings;
private readonly IMessenger messenger;
+
+ private readonly RecurringTimer displayNewDataTimer; // Timer for data updates
+ private const int displayNewDataTimerIntervalMs = 20; // after x milliseconds new data is fed forward to the UI
+ private readonly ConcurrentQueue> receivedDataQueue = new(); // Queue to buffer received data from communication protocol
+ private readonly ConcurrentQueue> sentDataQueue = new(); // Queue to buffer sent data (as reported from communication protocol)
public abstract TerminalViewType ViewType { get; }
@@ -96,10 +101,14 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie
{
this.appSettings = appSettings;
this.messenger = messenger;
+ this.displayNewDataTimer = new RecurringTimer(displayNewDataTimerIntervalMs, delegate() { this.UpdateDisplayedData(); });
+ this.displayNewDataTimer.Start();
}
~TerminalViewModel()
{
+ this.displayNewDataTimer?.Stop();
+
if (this.CommunicationProtocol != null)
{
// disconnect communication protocol
@@ -128,47 +137,61 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie
this.CommunicationProtocol.ConnectionStateChangedEvent += CommunicationProtocol_ConnectionStateChangedEvent;
}
- // initializes default newline separators, updates them directly inside ReceivedData and SentData objects
+ // initializes default newline separators, updates them directly
this.InitializeNewlineSeparatorsFromAppSettings();
+ }
- // update newline settings in data objects
- // TODO!!!
- //this.ReceivedData.NewlineSeparator = this.DataDisplayNewlineSeparatorType;
- //this.SentData.NewlineSeparator = this.DataDisplayNewlineSeparatorType;
+ private void CommunicationProtocol_ConnectionStateChangedEvent(object? sender, ProtocolConnectionState e)
+ {
+ // update internal state
+ this.CommunicationProtocolConnectionState = e;
+ this.CommunicationProtocolLongIdentifier = this.CommunicationProtocol!.LongInstanceIdentifier;
+
+ // If ViewModel still displays connected after unintentional disconnect => force to disconnected state
+ // allows user to change settings
+ if (e == ProtocolConnectionState.UnintentionallyDisconnected && this.ProtocolSettings!.DisplaysConnected == true)
+ {
+ this.ProtocolSettings.ForceConnectedState(false);
+ }
}
+ #region Communication Protocol Data Handling
+ ///
+ /// Add newly received data to display.
+ ///
public abstract void DisplayNewReceivedData(IEnumerable newData);
+ ///
+ /// Add newly sent data to display.
+ ///
+ public abstract void DisplayNewSentData(IEnumerable newData);
private void CommunicationProtocol_ReceivedDataEvent(object? sender, ReceivedDataEventArgs e)
{
- // handover data
- this.DisplayNewReceivedData(e.ReceivedData);
+ this.receivedDataQueue.Enqueue(e.ReceivedData);
}
- public abstract void DisplayNewSentData(IEnumerable newData);
-
private void CommunicationProtocol_SentDataEvent(object? sender, SentDataEventArgs e)
{
- // handover data
- this.DisplayNewSentData(e.SentData);
+ this.sentDataQueue.Enqueue(e.SentData);
}
- private void CommunicationProtocol_ConnectionStateChangedEvent(object? sender, ProtocolConnectionState e)
+ private async void UpdateDisplayedData()
{
- // update internal state
- this.CommunicationProtocolConnectionState = e;
- this.CommunicationProtocolLongIdentifier = this.CommunicationProtocol!.LongInstanceIdentifier;
-
- // If ViewModel still displays connected after unintentional disconnect => force to disconnected state
- // allows user to change settings
- if (e == ProtocolConnectionState.UnintentionallyDisconnected && this.ProtocolSettings!.DisplaysConnected == true)
+ // display new data in seperate tasks
+ if(this.receivedDataQueue.TryDequeue(out var newReceivedData))
{
- this.ProtocolSettings.ForceConnectedState(false);
+ await Task.Run(() => this.DisplayNewReceivedData(newReceivedData));
+ }
+ if(this.sentDataQueue.TryDequeue(out var newSentData))
+ {
+ await Task.Run(() => this.DisplayNewSentData(newSentData));
}
}
#endregion
+ #endregion
+
///
/// Sends the given data to this instances .
/// Appends configured to the end of string.
@@ -294,10 +317,16 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie
catch { }
}
+ ///
+ /// Act when DisplayData NewlineSeparator Changed.
+ /// For example rearrange data according to new newline separator.
+ ///
+ ///
protected abstract void ActOnDataDisplayNewlineSeparatorTypeChanged(NewlineSeparatorType newType);
partial void OnDataDisplayNewlineSeparatorTypeChanged(NewlineSeparatorType value)
{
+ // inform derivatives
this.ActOnDataDisplayNewlineSeparatorTypeChanged(value);
}
#endregion
diff --git a/MultiTerm.Protocols/CommunicationProtocol.cs b/MultiTerm.Protocols/CommunicationProtocol.cs
index 8d92cad..d056d81 100644
--- a/MultiTerm.Protocols/CommunicationProtocol.cs
+++ b/MultiTerm.Protocols/CommunicationProtocol.cs
@@ -14,7 +14,9 @@ public abstract class CommunicationProtocol : ICommunicationProtocol
protected readonly IMessenger messenger;
private CancellationTokenSource cancellationTokenSource;
private Thread? readingThread;
- private Thread? bufferHandlingThread;
+ private RecurringTimer? bufferHandlingTimer;
+ private const int bufferHandlingTimerIntervalMs = 20; // After x milliseconds the timer shall be called
+ private const int eventBlockSize = 50; // Block size for Received/Sent Data events
private ConcurrentQueue? receivedDataQueue;
private ConcurrentQueue? sentDataQueue;
@@ -30,8 +32,8 @@ public abstract class CommunicationProtocol : ICommunicationProtocol
public ProtocolConnectionState State
{
get { return state; }
- set
- {
+ set
+ {
state = value;
this.ConnectionStateChangedEvent?.Invoke(this, state);
}
@@ -63,7 +65,7 @@ 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(() =>
{
- this.CancelThreads();
+ this.CancelOngoingOperations();
this.logger.LogError($"'{nameof(OnUnintentionallyDisconnected)}()' called.", nameof(CommunicationProtocol));
// update state
@@ -86,9 +88,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.State != ProtocolConnectionState.Connected)
- {
- this.logger.LogWarn($"'{nameof(SendBytes)}()' was reached with wrong {nameof(ProtocolConnectionState)} of {this.State}", nameof(CommunicationProtocol));
+ if (this.State != ProtocolConnectionState.Connected)
+ {
+ this.logger.LogWarn($"'{nameof(SendBytes)}()' was reached with wrong {nameof(ProtocolConnectionState)} of {this.State}", nameof(CommunicationProtocol));
return false; // return and do not send
}
@@ -143,8 +145,8 @@ public abstract class CommunicationProtocol : ICommunicationProtocol
this.receivedDataQueue = new ConcurrentQueue();
this.sentDataQueue = new ConcurrentQueue();
// start interal buffer handling thread
- this.bufferHandlingThread = new Thread(() => this.HandleDataQueues(this.cancellationTokenSource.Token));
- this.bufferHandlingThread.Start();
+ this.bufferHandlingTimer = new(bufferHandlingTimerIntervalMs, delegate() { this.HandleDataQueues(); });
+ this.bufferHandlingTimer.Start();
// start internal reading thread
this.readingThread = new Thread(() => this.InternalRead(this.cancellationTokenSource.Token));
this.readingThread.Start();
@@ -166,69 +168,85 @@ public abstract class CommunicationProtocol : ICommunicationProtocol
protected abstract void InternalDisconnect();
public void Disconnect()
{
- this.CancelThreads();
+ this.CancelOngoingOperations();
this.InternalDisconnect();
this.State = ProtocolConnectionState.NotConnected;
}
///
- /// Sets cancellation token and makes sure both threads do not run anymore.
+ /// Sets cancellation token and makes sure ongoing operations do not run anymore.
///
- private void CancelThreads()
+ private void CancelOngoingOperations()
{
this.cancellationTokenSource.Cancel();
- // join both threads if they exist
+ // stop thread and timer
this.readingThread?.Join();
- this.bufferHandlingThread?.Join();
+ this.bufferHandlingTimer?.Stop();
}
///
/// Internal handling of internal data queues.
+ /// Arranges characters into blocks and raises and .
///
- /// cancellation token
- private void HandleDataQueues(CancellationToken ct)
+ private void HandleDataQueues()
{
- while(ct.IsCancellationRequested == false)
+ bool receivedDataAvailable = this.receivedDataQueue != null && this.receivedDataQueue.TryPeek(out _);
+ bool sentDataAvailable = this.sentDataQueue != null && this.sentDataQueue.TryPeek(out _);
+ List internalList = new();
+
+ List duplicateInternalList()
{
- bool receivedDataAvailable = this.receivedDataQueue != null && this.receivedDataQueue.TryPeek(out _);
- bool sentDataAvailable = this.sentDataQueue != null && this.sentDataQueue.TryPeek(out _);
+ // shallow copy of data
+ var duplicatedList = internalList.Select(item => (ExtendedByte)item.Clone()).ToList();
+ // clear internal list
+ internalList.Clear();
+ return duplicatedList;
+ }
- // collect received data
- if (receivedDataAvailable)
+ // collect received data
+ if (receivedDataAvailable)
+ {
+ // while data is available
+ while (this.receivedDataQueue!.TryDequeue(out ExtendedByte? newReceivedByte))
{
- // something is in the queue => block thread and try dequeue
- ExtendedByte? newReceivedData;
- while (this.receivedDataQueue!.TryDequeue(out newReceivedData) == false)
+ // fill it into the internal list
+ internalList.Add(newReceivedByte);
+ if(internalList.Count > eventBlockSize)
{
- Thread.Sleep(1);
+ // raise event with block of data
+ this.ReceivedDataEvent?.Invoke(this, new ReceivedDataEventArgs(duplicateInternalList()));
}
- // raise event
- this.ReceivedDataEvent?.Invoke(this, new ReceivedDataEventArgs(new ExtendedByte[] { newReceivedData }));
}
+ // no more new data but still some data in the list => send it
+ if (internalList.Count > 0)
+ {
+ this.ReceivedDataEvent?.Invoke(this, new ReceivedDataEventArgs(duplicateInternalList()));
+ }
+ // reset list for sent data
+ internalList = new();
+ }
- // collect sent data
- if (sentDataAvailable)
+ // collect sent data
+ if (sentDataAvailable)
+ {
+ // while data is available
+ while (this.sentDataQueue!.TryDequeue(out ExtendedByte? newSentByte))
{
- // something is in the queue => block thread and try dequeue
- ExtendedByte? newSentData;
- while (this.sentDataQueue!.TryDequeue(out newSentData) == false)
+ // fill it into the internal list
+ internalList.Add(newSentByte);
+ if (internalList.Count > eventBlockSize)
{
- Thread.Sleep(1);
+ // raise event with block of data
+ this.SentDataEvent?.Invoke(this, new SentDataEventArgs(duplicateInternalList()));
}
- // raise event
- this.SentDataEvent?.Invoke(this, new SentDataEventArgs(new ExtendedByte[] { newSentData }));
}
-
- // generally wait some time to slow down thread, if no data available
- if (!receivedDataAvailable && !sentDataAvailable)
+ // no more new data but still some data in the list => send it
+ if (internalList.Count > 0)
{
- Thread.Sleep(5);
+ this.SentDataEvent?.Invoke(this, new SentDataEventArgs(duplicateInternalList()));
}
-
- // always sleep 1ms to keep UI rolling
- Thread.Sleep(1);
}
}
diff --git a/MultiTerm.Protocols/Model/ExtendedByte.cs b/MultiTerm.Protocols/Model/ExtendedByte.cs
index b207c37..86674e6 100644
--- a/MultiTerm.Protocols/Model/ExtendedByte.cs
+++ b/MultiTerm.Protocols/Model/ExtendedByte.cs
@@ -7,7 +7,7 @@ namespace MultiTerm.Protocols.Model;
/// A time can be stored in combination with this using the property. E.g. to represent arrived or sent time.
/// Several methods to display the in other formats are provided.
///
-public partial class ExtendedByte
+public partial class ExtendedByte : ICloneable
{
///
/// Data in the form of a byte.
@@ -127,4 +127,11 @@ public partial class ExtendedByte
return resultString;
}
+
+ #region ICloneable
+ public object Clone()
+ {
+ return this.MemberwiseClone();
+ }
+ #endregion
}