using Common.Helpers; using Common.Logging; using Common.Messaging; using CommunityToolkit.Mvvm.Messaging; using MultiTerm.Protocols.Model; using MultiTerm.Protocols.Types; using System.Collections.Concurrent; namespace MultiTerm.Protocols; public abstract class CommunicationProtocol : ICommunicationProtocol { protected readonly ILogger logger; protected readonly IMessenger messenger; private CancellationTokenSource cancellationTokenSource; private Thread? readingThread; 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; public event EventHandler? ReceivedDataEvent; public event EventHandler? SentDataEvent; public event EventHandler? ConnectionStateChangedEvent; public abstract ProtocolType ProtocolType { get; } public abstract string InstanceIdentifier { get; protected set; } public abstract string LongInstanceIdentifier { get; protected set; } 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) { // initialize other this.cancellationTokenSource = new CancellationTokenSource(); this.logger = logger; this.messenger = messenger; } /// /// To be called when data was received from the connected device. /// protected void OnReceivedData(ExtendedByte receivedByte) { this.receivedDataQueue?.Enqueue(receivedByte); } /// /// To be called when data was sent to the connected device. /// protected void OnSentData(ExtendedByte sentByte) { this.sentDataQueue?.Enqueue(sentByte); } /// /// To be called whenever the protocol detected that it is disconnected, but this is not a known fact. /// protected void OnUnintentionallyDisconnected() { // perform all steps in a task, to prevent deadlock when this method is called from a thread that is joined. Task.Run(() => { this.CancelOngoingOperations(); this.logger.LogError($"'{nameof(OnUnintentionallyDisconnected)}()' called.", nameof(CommunicationProtocol)); // update state this.State = ProtocolConnectionState.UnintentionallyDisconnected; // inform user this.messenger.Send(new GenericUserInterfaceMessage($"'{this.GetProtocolAndInstanceIdentifier()}' unintentionally disconnected.", MessageImportance.High)); }); } /// /// Allows to send bytes using the implemented protocol. /// /// data as bytes /// true if the data was sent successfully protected abstract bool InternalSendBytes(byte[] bytes); public bool SendBytes(byte[] bytes) { 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)); return false; // return and do not send } // send bytes success = this.InternalSendBytes(bytes); // if the sending was cancelled report error if (success == false) { this.logger.LogError($"'{nameof(SendBytes)}()' failed to send during {nameof(InternalSendBytes)}.", nameof(CommunicationProtocol)); this.messenger.Send(new GenericUserInterfaceMessage("Failed to send data, for more information please check logfile.", MessageImportance.High)); } return success; } /// /// Allows to connect to the selected device using the implemented protocol. /// /// protocol settings required to connect /// true on success protected abstract bool InternalConnect(IProtocolSettings settings); /// /// Reads from the connected device in an endless loop. /// The endless loop must be ended when the has set. /// /// cancellation token protected abstract void InternalRead(CancellationToken ct); public bool Connect(IProtocolSettings settings) { // check if not already connected if (this.State == ProtocolConnectionState.Connected) { this.logger.LogWarn($"'{nameof(Connect)}()' was reached with wrong {nameof(ProtocolConnectionState)} of {this.State}", nameof(CommunicationProtocol)); return true; } // check if settings are valid, cancel if not if (settings.AreValid() == false) { this.logger.LogError($"'{nameof(Connect)}()' failed since the provided protocol settings are invalid", nameof(CommunicationProtocol)); return false; } // try connecting if settings are valid if (this.InternalConnect(settings)) { // renew token source and data buffers this.cancellationTokenSource = new CancellationTokenSource(); this.receivedDataQueue = new ConcurrentQueue(); this.sentDataQueue = new ConcurrentQueue(); // start interal buffer handling thread 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(); // update state this.State = ProtocolConnectionState.Connected; return true; } else { this.logger.LogWarn($"'{nameof(Connect)}()' failed to connect to protocol, did not start reading thread.", nameof(CommunicationProtocol)); this.messenger.Send(new GenericUserInterfaceMessage($"Failed to connect to '{this.GetProtocolAndInstanceIdentifier()}'. For more information please check logfile.", MessageImportance.High)); return false; } } /// /// Allows to disconnect from the connected device. /// protected abstract void InternalDisconnect(); public void Disconnect() { this.CancelOngoingOperations(); this.InternalDisconnect(); this.State = ProtocolConnectionState.NotConnected; } /// /// Sets cancellation token and makes sure ongoing operations do not run anymore. /// private void CancelOngoingOperations() { this.cancellationTokenSource.Cancel(); // stop thread and timer this.readingThread?.Join(); this.bufferHandlingTimer?.Stop(); } /// /// Internal handling of internal data queues. /// Arranges characters into blocks and raises and . /// private void HandleDataQueues() { bool receivedDataAvailable = this.receivedDataQueue != null && this.receivedDataQueue.TryPeek(out _); bool sentDataAvailable = this.sentDataQueue != null && this.sentDataQueue.TryPeek(out _); List internalList = new(); List duplicateInternalList() { // 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) { // while data is available while (this.receivedDataQueue!.TryDequeue(out ExtendedByte? newReceivedByte)) { // fill it into the internal list internalList.Add(newReceivedByte); if(internalList.Count > eventBlockSize) { // raise event with block of data this.ReceivedDataEvent?.Invoke(this, new ReceivedDataEventArgs(duplicateInternalList())); } } // 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) { // while data is available while (this.sentDataQueue!.TryDequeue(out ExtendedByte? newSentByte)) { // fill it into the internal list internalList.Add(newSentByte); if (internalList.Count > eventBlockSize) { // raise event with block of data this.SentDataEvent?.Invoke(this, new SentDataEventArgs(duplicateInternalList())); } } // no more new data but still some data in the list => send it if (internalList.Count > 0) { this.SentDataEvent?.Invoke(this, new SentDataEventArgs(duplicateInternalList())); } } } #region Helpers public string GetProtocolAndInstanceIdentifier() { return $"{EnumHelpers.GetEnumDescription(this.ProtocolType)} {this.InstanceIdentifier}"; } #endregion }