26 internal class Bus : IDisposable
28 internal const byte DriverByte = 0xFF;
30 public static readonly TimeSpan DefaultPollInterval = TimeSpan.FromMilliseconds(200);
32 private readonly Dictionary<byte, bool> _lastOnlineConnectionStatus =
new ();
33 private readonly Dictionary<byte, bool> _lastSecureConnectionStatus =
new ();
35 private readonly ILogger<ControlPanel> _logger;
36 private readonly TimeSpan _pollInterval;
37 private readonly BlockingCollection<ReplyTracker> _replies;
38 private readonly Action<TraceEntry> _tracer;
39 private readonly AutoResetEvent _commandAvailableEvent =
new (
false);
40 private readonly IDeviceProxyFactory _deviceProxyFactory;
42 private CancellationTokenSource _cancellationTokenSource;
43 private Task _pollingTask;
44 private ImmutableSortedSet<DeviceProxy> _configuredDevices = ImmutableSortedSet<DeviceProxy>.Empty;
46 public Bus(
IOsdpConnection connection, BlockingCollection<ReplyTracker> replies, TimeSpan pollInterval,
47 Action<TraceEntry> tracer, IDeviceProxyFactory deviceProxyFactory,
49 ILogger<ControlPanel> logger =
null)
51 Connection = connection ??
throw new ArgumentNullException(nameof(connection));
52 _replies = replies ??
throw new ArgumentNullException(nameof(replies));
53 _pollInterval = pollInterval;
57 _deviceProxyFactory = deviceProxyFactory;
60 private bool IsPolling => _pollInterval > TimeSpan.Zero;
65 public Guid Id {
get; }
72 public IEnumerable<byte> ConfigureDeviceAddresses => _configuredDevices.Select(device => device.Address);
76 _commandAvailableEvent?.Dispose();
77 _cancellationTokenSource?.Dispose();
80 private static TimeSpan IdleLineDelay(
IOsdpConnection connection,
int numberOfBytes) =>
81 LineTiming.ForCharacters(connection.
BaudRate, numberOfBytes);
86 public async Task Close()
88 var cancellationTokenSource = _cancellationTokenSource;
89 var pollingTask = _pollingTask;
91 if (cancellationTokenSource !=
null)
93 cancellationTokenSource.Cancel();
95 if (pollingTask !=
null)
97 await pollingTask.ConfigureAwait(
false);
100 _cancellationTokenSource =
null;
110 public void SendCommand(
byte address,
CommandData command)
112 var foundDevice = _configuredDevices.First(device => device.Address == address);
113 foundDevice.SendCommand(command);
114 _commandAvailableEvent.Set();
124 public void AddDevice(
byte address,
bool useCrc,
bool useSecureChannel,
byte[] secureChannelKey =
null)
126 var configuredDevices = _configuredDevices.ToImmutableSortedSet();
128 var foundDevice = configuredDevices.FirstOrDefault(device => device.Address == address);
130 if (foundDevice !=
null)
132 configuredDevices = configuredDevices.Remove(foundDevice);
135 var addedDevice = _deviceProxyFactory.Create(address, useCrc, useSecureChannel, secureChannelKey);
137 configuredDevices = configuredDevices.Add(addedDevice);
139 _configuredDevices = configuredDevices;
146 public void RemoveDevice(
byte address)
148 var configuredDevices = _configuredDevices.ToImmutableSortedSet();
150 var foundDevice = configuredDevices.FirstOrDefault(device => device.Address == address);
151 if (foundDevice ==
null)
return;
153 configuredDevices = configuredDevices.Remove(foundDevice);
155 _configuredDevices = configuredDevices;
163 public bool IsOnline(
byte address)
165 var foundDevice = _configuredDevices.First(device => device.Address == address);
167 return foundDevice.IsConnected;
174 public void StartPolling()
176 var cancellationTokenSource = _cancellationTokenSource;
177 if (cancellationTokenSource !=
null)
return;
178 _cancellationTokenSource = cancellationTokenSource =
new CancellationTokenSource();
180 _pollingTask = Task.Factory.StartNew(async () =>
184 await PollingLoop(cancellationTokenSource.Token).ConfigureAwait(
false);
186 catch (Exception exception)
188 _logger?.LogError(exception,
189 "[{OsdpConnection}] Unexpected exception in polling loop for connection {Guid}", Connection,
192 }, TaskCreationOptions.LongRunning).Unwrap();
199 private async Task PollingLoop(CancellationToken cancellationToken)
201 DateTime lastMessageSentTime = DateTime.MinValue;
202 using var delayTime =
new AutoResetEvent(
false);
204 while (!cancellationToken.IsCancellationRequested)
210 await Connection.
Open();
212 catch (Exception exception)
214 _logger?.LogError(exception, $
"[{Connection}] Error while opening connection");
215 foreach (var device
in _configuredDevices)
218 UpdateConnectionStatus(device);
221 delayTime.WaitOne(TimeSpan.FromSeconds(5));
230 while (_pollInterval - (DateTime.UtcNow - lastMessageSentTime) > TimeSpan.Zero &&
231 !_configuredDevices.Any(device1 => device1.HasQueuedCommand) &&
232 !_configuredDevices.Any(device2 => device2.IsReceivingMultipartMessage))
234 delayTime.WaitOne(TimeSpan.FromMilliseconds(10));
237 lastMessageSentTime = DateTime.UtcNow;
239 if (!_configuredDevices.Any())
247 _commandAvailableEvent.WaitOne(TimeSpan.FromMilliseconds(10));
250 foreach (var device
in _configuredDevices)
255 device.MessageControl.ResetSequence();
259 if (device.RequestDelay > DateTime.UtcNow)
264 var commandMessage = device.GetNextCommandData(IsPolling);
265 if (commandMessage ==
null || WaitingForNextMultiMessage(commandMessage.Code, device.IsSendingMultipartMessage))
275 if (IsPolling && UpdateConnectionStatus(device) && !device.IsConnected)
279 else if(IsPolling && !device.IsConnected)
281 device.RequestDelay = DateTime.UtcNow + TimeSpan.FromSeconds(1);
284 catch (Exception exception)
286 _logger?.LogError(exception, $
"[{Connection}] Error while notifying connection status for address {commandMessage.Address}");
291 reply = await SendCommandAndReceiveReply(commandMessage, device, cancellationToken).ConfigureAwait(
false);
296 if (reply.ReplyMessage.
Type != (
byte)
ReplyType.Busy && reply.ReplyMessage.
Type != (
byte)
ReplyType.Nak && device.UseSecureChannel &&
300 "A plain text message was received when the secure channel had been established");
301 device.ResetSecureChannelSession();
306 catch (TimeoutException exception)
311 case true when device.IsSecurityEstablished && !IsOnline(commandMessage.Address):
315 _logger?.LogDebug($
"[{Connection}] Retrying command {commandMessage} on connection {Id} because \"{exception.Message}\".");
316 device.RetryCommand(commandMessage.PayloadData as
CommandData, isBusyRetry:
false);
322 catch (Exception exception)
324 if (!cancellationToken.IsCancellationRequested)
326 _logger?.LogError(exception,
327 $
"[{Connection}] Error while sending command {commandMessage} to address {commandMessage.Address}. Connection {Id}");
337 _logger?.LogDebug($
"[{Connection}] Retrying command {commandMessage} on connection {Id} because the device is busy.");
338 device.RetryCommand(commandMessage.PayloadData as
CommandData, isBusyRetry:
true);
342 ProcessReply(reply, device);
345 catch (Exception exception)
347 _logger?.LogError(exception, $
"[{Connection}] Error while processing reply {reply} from address {reply.ReplyMessage.Address}");
351 delayTime.WaitOne(IdleLineDelay(Connection, 2));
358 await Connection.
Close();
360 catch(Exception exception)
362 _logger?.LogError(exception, $
"[{Connection}] Error while closing connection {Id}.");
366 private static bool WaitingForNextMultiMessage(
byte messageCode,
bool sendingMultiMessage)
369 return sendingMultiMessage && messageCode != (byte)
CommandType.FileTransfer;
372 public event EventHandler<ConnectionStatusEventArgs> ConnectionStatusChanged;
379 private bool UpdateConnectionStatus(DeviceProxy device)
381 bool isConnected = device.IsConnected;
382 bool isSecureChannelEstablished = device.IsSecurityEstablished;
385 _lastOnlineConnectionStatus.TryAdd(device.Address,
false);
386 _lastSecureConnectionStatus.TryAdd(device.Address,
false);
388 bool onlineConnectionChanged = _lastOnlineConnectionStatus[device.Address] != isConnected;
389 bool secureChannelStatusChanged = _lastSecureConnectionStatus[device.Address] != isSecureChannelEstablished;
391 if (!onlineConnectionChanged && !secureChannelStatusChanged)
return false;
393 _lastOnlineConnectionStatus[device.Address] = isConnected;
394 _lastSecureConnectionStatus[device.Address] = isSecureChannelEstablished;
396 var handler = ConnectionStatusChanged;
397 handler?.Invoke(
this,
398 new ConnectionStatusEventArgs(device.Address, isConnected, device.IsSecurityEstablished));
403 private void ProcessReply(ReplyTracker reply, DeviceProxy device)
406 if (device.IsConnected && reply.ReplyMessage.ControlBlock.Sequence == 0)
412 if (!reply.IsValidReply)
428 device.ValidReplyHasBeenReceived(reply.ReplyMessage.
Sequence);
438 if ((device.IsSecurityEstablished &&
439 errorCode is
ErrorCode.DoesNotSupportSecurityBlock or
ErrorCode.CommunicationSecurityNotMet
441 errorCode ==
ErrorCode.UnexpectedSequenceNumber)
453 if (!reply.ValidateSecureChannelEstablishment())
455 _logger?.LogError($
"[{Connection}] Cryptogram not accepted by address {reply.ReplyMessage.Address}");
463 public void ResetDevice(
int address)
465 var foundDevice = _configuredDevices.First(device => device.Address == address);
467 ResetDevice(foundDevice);
470 public void SetSendingMultipartMessage(
byte address,
bool isSendingMultipartMessage)
472 var foundDevice = _configuredDevices.First(device => device.Address == address);
474 foundDevice.IsSendingMultipartMessage = isSendingMultipartMessage;
477 public void SetReceivingMultipartMessage(
byte address,
bool isReceivingMultipartMessage)
479 var foundDevice = _configuredDevices.First(device => device.Address == address);
481 foundDevice.IsReceivingMultipartMessage = isReceivingMultipartMessage;
484 public void SetSendingMultiMessageNoSecureChannel(
byte address,
bool isSendingMultiMessageNoSecureChannel)
486 var foundDevice = _configuredDevices.First(device => device.Address == address);
488 foundDevice.IsSendingMultiMessageNoSecureChannel = isSendingMultiMessageNoSecureChannel;
489 foundDevice.MessageControl.IsSendingMultiMessageNoSecureChannel = isSendingMultiMessageNoSecureChannel;
490 if (isSendingMultiMessageNoSecureChannel)
492 foundDevice.ResetSecureChannelSession();
496 public void SetRequestDelay(
byte address, DateTime requestDelay)
498 var foundDevice = _configuredDevices.First(device => device.Address == address);
500 foundDevice.RequestDelay = requestDelay;
503 private void ResetDevice(DeviceProxy device)
505 device.RequestDelay = DateTime.UtcNow + TimeSpan.FromSeconds(1);
506 AddDevice(device.Address, device.MessageControl.UseCrc, device.UseSecureChannel, device.SecureChannelKey);
509 private async Task<ReplyTracker> SendCommandAndReceiveReply(OutgoingMessage command, DeviceProxy device, CancellationToken cancellationToken)
514 commandData = command.BuildMessage(device.MessageSecureChannel);
516 catch (Exception exception)
518 _logger?.LogError(exception, $
"[{Connection}] Error while building command {command}");
522 await Connection.
WriteAsync(commandData).ConfigureAwait(
false);
526 using var delayTime =
new AutoResetEvent(
false);
527 delayTime.WaitOne(IdleLineDelay(Connection, commandData.Length));
529 return await ReceiveReply(command, device, cancellationToken);
532 private async Task<ReplyTracker> ReceiveReply(OutgoingMessage command, DeviceProxy device, CancellationToken cancellationToken)
534 var replyBuffer =
new Collection<byte>();
536 if (!await WaitForStartOfMessage(Connection, replyBuffer, device.IsSendingMultipartMessage ? TimeSpan.FromSeconds(1) : TimeSpan.Zero, cancellationToken).ConfigureAwait(
false))
538 throw new TimeoutException(
"Timeout waiting for reply message");
541 if (!await WaitForMessageLength(Connection, replyBuffer, cancellationToken).ConfigureAwait(
false))
543 throw new TimeoutException(
"Timeout waiting for reply message length");
546 if (!await WaitForRestOfMessage(Connection, replyBuffer, ExtractMessageLength(replyBuffer), cancellationToken).ConfigureAwait(
false))
548 throw new TimeoutException(
"Timeout waiting for rest of reply message");
553 return new ReplyTracker(Id,
new IncomingMessage(replyBuffer.ToArray(), device.MessageSecureChannel), command, device);
556 internal static ushort ExtractMessageLength(IReadOnlyList<byte> replyBuffer)
558 return Message.ConvertBytesToUnsignedShort(
new[] {replyBuffer[2], replyBuffer[3]});
561 internal static async Task<bool> WaitForRestOfMessage(
IOsdpConnection connection, ICollection<byte> replyBuffer, ushort replyLength, CancellationToken cancellationToken)
563 while (replyBuffer.Count < replyLength)
565 int maxReadBufferLength = connection.BaudRate / 40;
566 int remainingLength = replyLength - replyBuffer.Count;
567 byte[] readBuffer =
new byte[Math.Min(maxReadBufferLength, remainingLength)];
569 await TimeOutReadAsync(connection, readBuffer, connection.
ReplyTimeout + IdleLineDelay(connection, readBuffer.Length),
571 .ConfigureAwait(
false);
574 for (
int index = 0; index < bytesRead; index++)
576 replyBuffer.Add(readBuffer[index]);
588 internal static async Task<bool> WaitForMessageLength(
IOsdpConnection connection, ICollection<byte> replyBuffer, CancellationToken cancellationToken)
590 while (replyBuffer.Count < 4)
592 byte[] readBuffer =
new byte[4];
594 await TimeOutReadAsync(connection, readBuffer, connection.
ReplyTimeout, cancellationToken)
595 .ConfigureAwait(
false);
598 for (
int index = 0; index < bytesRead; index++)
600 replyBuffer.Add(readBuffer[index]);
612 internal static async Task<bool> WaitForStartOfMessage(
IOsdpConnection connection, ICollection<byte> replyBuffer, TimeSpan extraWait, CancellationToken cancellationToken)
616 byte[] readBuffer =
new byte[1];
617 int bytesRead = await TimeOutReadAsync(connection, readBuffer,
620 .ConfigureAwait(
false);
631 replyBuffer.Add(readBuffer[0]);
638 private static async Task<int> TimeOutReadAsync(
IOsdpConnection connection,
byte[] buffer, TimeSpan timeout, CancellationToken cancellationToken)
640 using var timedCancellationTokenSource =
new CancellationTokenSource(timeout);
641 using var linkedCancellationTokenSource =
642 CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timedCancellationTokenSource.Token);
645 return await connection.
ReadAsync(buffer, linkedCancellationTokenSource.Token).ConfigureAwait(
false);
654 public class ConnectionStatusEventArgs
656 public ConnectionStatusEventArgs(
byte address,
bool isConnected,
bool isSecureSessionEstablished)
659 IsConnected = isConnected;
660 IsSecureSessionEstablished = isSecureSessionEstablished;
663 public byte Address {
get; }
665 public bool IsConnected {
get; }
667 public bool IsSecureSessionEstablished {
get; }