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 }