Multiprocotol Terminalprogram (BAT)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MultiTerm/MultiTerm.Protocols/CommunicationProtocol.cs

262 lines
10 KiB

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<ExtendedByte>? receivedDataQueue;
private ConcurrentQueue<ExtendedByte>? sentDataQueue;
public event EventHandler<ReceivedDataEventArgs>? ReceivedDataEvent;
public event EventHandler<SentDataEventArgs>? SentDataEvent;
public event EventHandler<ProtocolConnectionState>? 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;
}
/// <summary>
/// To be called when data was received from the connected device.
/// </summary>
protected void OnReceivedData(ExtendedByte receivedByte) { this.receivedDataQueue?.Enqueue(receivedByte); }
/// <summary>
/// To be called when data was sent to the connected device.
/// </summary>
protected void OnSentData(ExtendedByte sentByte) { this.sentDataQueue?.Enqueue(sentByte); }
/// <summary>
/// To be called whenever the protocol detected that it is disconnected, but this is not a known fact.
/// </summary>
protected void OnUnintentionallyDisconnected(bool informUser = true)
{
// 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
if (informUser)
{
this.messenger.Send<IUserInterfaceMessage>(new GenericUserInterfaceMessage($"'{this.GetProtocolAndInstanceIdentifier()}' unintentionally disconnected.", MessageImportance.High));
}
});
}
/// <summary>
/// Allows to send bytes using the implemented protocol.
/// </summary>
/// <param name="bytes">data as bytes</param>
/// <returns>true if the data was sent successfully</returns>
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<IUserInterfaceMessage>(new GenericUserInterfaceMessage("Failed to send data, for more information please check logfile.", MessageImportance.High));
}
return success;
}
/// <summary>
/// Allows to connect to the selected device using the implemented protocol.
/// </summary>
/// <param name="settings">protocol settings required to connect</param>
/// <returns>true on success</returns>
protected abstract bool InternalConnect(IProtocolSettings settings);
/// <summary>
/// Reads from the connected device in an endless loop.
/// The endless loop must be ended when the <paramref name="ct"/> has <see cref="CancellationToken.IsCancellationRequested"/> set.
/// </summary>
/// <param name="ct">cancellation token</param>
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<ExtendedByte>();
this.sentDataQueue = new ConcurrentQueue<ExtendedByte>();
// 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<IUserInterfaceMessage>(new GenericUserInterfaceMessage($"Failed to connect to '{this.GetProtocolAndInstanceIdentifier()}'. For more information please check logfile.", MessageImportance.High));
return false;
}
}
/// <summary>
/// Allows to disconnect from the connected device.
/// </summary>
protected abstract void InternalDisconnect();
public void Disconnect()
{
this.CancelOngoingOperations();
this.InternalDisconnect();
this.State = ProtocolConnectionState.NotConnected;
}
/// <summary>
/// Sets cancellation token and makes sure ongoing operations do not run anymore.
/// </summary>
private void CancelOngoingOperations()
{
this.cancellationTokenSource.Cancel();
// stop thread and timer
this.readingThread?.Join();
this.bufferHandlingTimer?.Stop();
}
/// <summary>
/// Internal handling of internal data queues.
/// Arranges characters into blocks and raises <see cref="ReceivedDataEvent"/> and <see cref="SentDataEvent"/>.
/// </summary>
private void HandleDataQueues()
{
bool receivedDataAvailable = this.receivedDataQueue != null && this.receivedDataQueue.TryPeek(out _);
bool sentDataAvailable = this.sentDataQueue != null && this.sentDataQueue.TryPeek(out _);
List<ExtendedByte> internalList = new();
List<ExtendedByte> 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
}