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 Thread? bufferHandlingThread; private ConcurrentQueue? receivedDataQueue; private ConcurrentQueue? sentDataQueue; public event EventHandler? ReceivedDataEvent; public event EventHandler? SentDataEvent; public event EventHandler? DisconnectedEvent; public abstract ProtocolType ProtocolType { get; } public abstract string InstanceIdentifier { get; protected set; } 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; } /// /// 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(() => { // update state this.IsConnected = false; this.CancelThreads(); this.logger.LogError($"'{nameof(OnUnintentionallyDisconnected)}()' called.", nameof(CommunicationProtocol)); // raise event indicating an unintentional disconnect this.DisconnectedEvent?.Invoke(this, new DisconnectedEventArgs(true)); }); } /// /// 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.IsConnected == false) { this.logger.LogWarn($"'{nameof(SendBytes)}()' was reached with {nameof(IsConnected)} being false", 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.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) { 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.bufferHandlingThread = new Thread(() => this.HandleDataQueues(this.cancellationTokenSource.Token)); this.bufferHandlingThread.Start(); // start internal reading thread this.readingThread = new Thread(() => this.InternalRead(this.cancellationTokenSource.Token)); this.readingThread.Start(); // update state this.IsConnected = true; return true; } else { this.logger.LogWarn($"'{nameof(Connect)}()' failed to connect to protocol, did not start reading thread.", nameof(CommunicationProtocol)); return false; } } /// /// Allows to disconnect from the connected device. /// protected abstract void InternalDisconnect(); public void Disconnect() { this.CancelThreads(); this.InternalDisconnect(); this.IsConnected = false; // raise event indicating an intentional disconnect this.DisconnectedEvent?.Invoke(this, new DisconnectedEventArgs(false)); } /// /// Sets cancellation token and makes sure both threads do not run anymore. /// private void CancelThreads() { this.cancellationTokenSource.Cancel(); // join both threads if they exist this.readingThread?.Join(); this.bufferHandlingThread?.Join(); } /// /// Internal handling of internal data queues. /// /// cancellation token private void HandleDataQueues(CancellationToken ct) { while(ct.IsCancellationRequested == false) { bool receivedDataAvailable = this.receivedDataQueue != null && this.receivedDataQueue.TryPeek(out _); bool sentDataAvailable = this.sentDataQueue != null && this.sentDataQueue.TryPeek(out _); // collect received data if (receivedDataAvailable) { // something is in the queue => block thread and try dequeue ExtendedByte? newReceivedData; while (this.receivedDataQueue!.TryDequeue(out newReceivedData) == false) { Thread.Sleep(1); } // raise event this.ReceivedDataEvent?.Invoke(this, new ReceivedDataEventArgs(new ExtendedByte[] { newReceivedData })); } // collect sent data if (sentDataAvailable) { // something is in the queue => block thread and try dequeue ExtendedByte? newSentData; while (this.sentDataQueue!.TryDequeue(out newSentData) == false) { Thread.Sleep(1); } // 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) { Thread.Sleep(5); } // always sleep 1ms to keep UI rolling Thread.Sleep(1); } } }