OSDP.Net 5.2.1
A .NET library for implementing the Open Supervised Device Protocol (OSDP)
Loading...
Searching...
No Matches
Bus.cs
1using System;
2using System.Collections.Concurrent;
3using System.Collections.Generic;
4using System.Collections.Immutable;
5using System.Collections.ObjectModel;
6using System.Linq;
7using System.Threading;
8using System.Threading.Tasks;
9
10using Microsoft.Extensions.Logging;
11
18
19// ReSharper disable TemplateIsNotCompileTimeConstantProblem
20
21namespace OSDP.Net
22{
26 internal class Bus : IDisposable
27 {
28 internal const byte DriverByte = 0xFF;
29
30 public static readonly TimeSpan DefaultPollInterval = TimeSpan.FromMilliseconds(200);
31
32 private readonly Dictionary<byte, bool> _lastOnlineConnectionStatus = new ();
33 private readonly Dictionary<byte, bool> _lastSecureConnectionStatus = new ();
34
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;
41
42 private CancellationTokenSource _cancellationTokenSource;
43 private Task _pollingTask;
44 private ImmutableSortedSet<DeviceProxy> _configuredDevices = ImmutableSortedSet<DeviceProxy>.Empty;
45
46 public Bus(IOsdpConnection connection, BlockingCollection<ReplyTracker> replies, TimeSpan pollInterval,
47 Action<TraceEntry> tracer, IDeviceProxyFactory deviceProxyFactory,
48 // ReSharper disable once ContextualLoggerProblem
49 ILogger<ControlPanel> logger = null)
50 {
51 Connection = connection ?? throw new ArgumentNullException(nameof(connection));
52 _replies = replies ?? throw new ArgumentNullException(nameof(replies));
53 _pollInterval = pollInterval;
54 _tracer = tracer;
55 Id = Guid.NewGuid();
56 _logger = logger;
57 _deviceProxyFactory = deviceProxyFactory;
58 }
59
60 private bool IsPolling => _pollInterval > TimeSpan.Zero;
61
65 public Guid Id { get; }
66
70 public IOsdpConnection Connection { get; private set; }
71
72 public IEnumerable<byte> ConfigureDeviceAddresses => _configuredDevices.Select(device => device.Address);
73
74 public void Dispose()
75 {
76 _commandAvailableEvent?.Dispose();
77 _cancellationTokenSource?.Dispose();
78 }
79
80 private static TimeSpan IdleLineDelay(IOsdpConnection connection, int numberOfBytes) =>
81 LineTiming.ForCharacters(connection.BaudRate, numberOfBytes);
82
86 public async Task Close()
87 {
88 var cancellationTokenSource = _cancellationTokenSource;
89 var pollingTask = _pollingTask;
90
91 if (cancellationTokenSource != null)
92 {
93 cancellationTokenSource.Cancel();
94
95 if (pollingTask != null)
96 {
97 await pollingTask.ConfigureAwait(false);
98 }
99
100 _cancellationTokenSource = null;
101 _pollingTask = null;
102 }
103 }
104
110 public void SendCommand(byte address, CommandData command)
111 {
112 var foundDevice = _configuredDevices.First(device => device.Address == address);
113 foundDevice.SendCommand(command);
114 _commandAvailableEvent.Set();
115 }
116
124 public void AddDevice(byte address, bool useCrc, bool useSecureChannel, byte[] secureChannelKey = null)
125 {
126 var configuredDevices = _configuredDevices.ToImmutableSortedSet();
127
128 var foundDevice = configuredDevices.FirstOrDefault(device => device.Address == address);
129
130 if (foundDevice != null)
131 {
132 configuredDevices = configuredDevices.Remove(foundDevice);
133 }
134
135 var addedDevice = _deviceProxyFactory.Create(address, useCrc, useSecureChannel, secureChannelKey);
136
137 configuredDevices = configuredDevices.Add(addedDevice);
138
139 _configuredDevices = configuredDevices;
140 }
141
146 public void RemoveDevice(byte address)
147 {
148 var configuredDevices = _configuredDevices.ToImmutableSortedSet();
149
150 var foundDevice = configuredDevices.FirstOrDefault(device => device.Address == address);
151 if (foundDevice == null) return;
152
153 configuredDevices = configuredDevices.Remove(foundDevice);
154
155 _configuredDevices = configuredDevices;
156 }
157
163 public bool IsOnline(byte address)
164 {
165 var foundDevice = _configuredDevices.First(device => device.Address == address);
166
167 return foundDevice.IsConnected;
168 }
169
174 public void StartPolling()
175 {
176 var cancellationTokenSource = _cancellationTokenSource;
177 if (cancellationTokenSource != null) return;
178 _cancellationTokenSource = cancellationTokenSource = new CancellationTokenSource();
179
180 _pollingTask = Task.Factory.StartNew(async () =>
181 {
182 try
183 {
184 await PollingLoop(cancellationTokenSource.Token).ConfigureAwait(false);
185 }
186 catch (Exception exception)
187 {
188 _logger?.LogError(exception,
189 "[{OsdpConnection}] Unexpected exception in polling loop for connection {Guid}", Connection,
190 Id);
191 }
192 }, TaskCreationOptions.LongRunning).Unwrap();
193 }
194
199 private async Task PollingLoop(CancellationToken cancellationToken)
200 {
201 DateTime lastMessageSentTime = DateTime.MinValue;
202 using var delayTime = new AutoResetEvent(false);
203
204 while (!cancellationToken.IsCancellationRequested)
205 {
206 if (!Connection.IsOpen)
207 {
208 try
209 {
210 await Connection.Open();
211 }
212 catch (Exception exception)
213 {
214 _logger?.LogError(exception, $"[{Connection}] Error while opening connection");
215 foreach (var device in _configuredDevices)
216 {
217 ResetDevice(device);
218 UpdateConnectionStatus(device);
219 }
220
221 delayTime.WaitOne(TimeSpan.FromSeconds(5));
222
223 continue;
224 }
225 }
226
227 if (IsPolling)
228 {
229 // Allow for immediate processing of commands in queue or incoming multipart messages
230 while (_pollInterval - (DateTime.UtcNow - lastMessageSentTime) > TimeSpan.Zero &&
231 !_configuredDevices.Any(device1 => device1.HasQueuedCommand) &&
232 !_configuredDevices.Any(device2 => device2.IsReceivingMultipartMessage))
233 {
234 delayTime.WaitOne(TimeSpan.FromMilliseconds(10));
235 }
236
237 lastMessageSentTime = DateTime.UtcNow;
238
239 if (!_configuredDevices.Any())
240 {
241 continue;
242 }
243 }
244 else
245 {
246 // Keep CPU usage down while waiting for next command
247 _commandAvailableEvent.WaitOne(TimeSpan.FromMilliseconds(10));
248 }
249
250 foreach (var device in _configuredDevices)
251 {
252 // Right now it always sends sequence 0
253 if (!IsPolling)
254 {
255 device.MessageControl.ResetSequence();
256 }
257
258 // Requested delay for multi-messages and resets
259 if (device.RequestDelay > DateTime.UtcNow)
260 {
261 continue;
262 }
263
264 var commandMessage = device.GetNextCommandData(IsPolling);
265 if (commandMessage == null || WaitingForNextMultiMessage(commandMessage.Code, device.IsSendingMultipartMessage))
266 {
267 continue;
268 }
269
270 ReplyTracker reply;
271
272 try
273 {
274 // Reset the device if it loses connection
275 if (IsPolling && UpdateConnectionStatus(device) && !device.IsConnected)
276 {
277 ResetDevice(device);
278 }
279 else if(IsPolling && !device.IsConnected)
280 {
281 device.RequestDelay = DateTime.UtcNow + TimeSpan.FromSeconds(1);
282 }
283 }
284 catch (Exception exception)
285 {
286 _logger?.LogError(exception, $"[{Connection}] Error while notifying connection status for address {commandMessage.Address}");
287 }
288
289 try
290 {
291 reply = await SendCommandAndReceiveReply(commandMessage, device, cancellationToken).ConfigureAwait(false);
292
293 // Prevent plain text message replies when secure channel has been established
294 // The busy and Nak reply types are a special case which is allowed to be sent as insecure message on a secure channel
295 // Workaround for KeySet command sending back a clear text Ack
296 if (reply.ReplyMessage.Type != (byte)ReplyType.Busy && reply.ReplyMessage.Type != (byte)ReplyType.Nak && device.UseSecureChannel &&
297 device.IsSecurityEstablished && !reply.ReplyMessage.IsSecureMessage && commandMessage.Code != (byte)CommandType.KeySet)
298 {
299 _logger?.LogWarning(
300 "A plain text message was received when the secure channel had been established");
301 device.ResetSecureChannelSession();
302 ResetDevice(device);
303 continue;
304 }
305 }
306 catch (TimeoutException exception)
307 {
308 switch (IsPolling)
309 {
310 // Make sure the security is reset properly if reader goes offline
311 case true when device.IsSecurityEstablished && !IsOnline(commandMessage.Address):
312 ResetDevice(device);
313 break;
314 default:
315 _logger?.LogDebug($"[{Connection}] Retrying command {commandMessage} on connection {Id} because \"{exception.Message}\".");
316 device.RetryCommand(commandMessage.PayloadData as CommandData, isBusyRetry: false);
317 break;
318 }
319
320 continue;
321 }
322 catch (Exception exception)
323 {
324 if (!cancellationToken.IsCancellationRequested)
325 {
326 _logger?.LogError(exception,
327 $"[{Connection}] Error while sending command {commandMessage} to address {commandMessage.Address}. Connection {Id}");
328 }
329
330 continue;
331 }
332
333 try
334 {
335 if (reply.ReplyMessage.Type == (byte)ReplyType.Busy)
336 {
337 _logger?.LogDebug($"[{Connection}] Retrying command {commandMessage} on connection {Id} because the device is busy.");
338 device.RetryCommand(commandMessage.PayloadData as CommandData, isBusyRetry: true);
339 }
340 else
341 {
342 ProcessReply(reply, device);
343 }
344 }
345 catch (Exception exception)
346 {
347 _logger?.LogError(exception, $"[{Connection}] Error while processing reply {reply} from address {reply.ReplyMessage.Address}");
348 continue;
349 }
350
351 delayTime.WaitOne(IdleLineDelay(Connection, 2));
352 }
353 }
354
355 // The polling task is complete. Time to close the connection.
356 try
357 {
358 await Connection.Close();
359 }
360 catch(Exception exception)
361 {
362 _logger?.LogError(exception, $"[{Connection}] Error while closing connection {Id}.");
363 }
364 }
365
366 private static bool WaitingForNextMultiMessage(byte messageCode, bool sendingMultiMessage)
367 {
368 // File transfer override, it doesn't need to wait
369 return sendingMultiMessage && messageCode != (byte)CommandType.FileTransfer;
370 }
371
372 public event EventHandler<ConnectionStatusEventArgs> ConnectionStatusChanged;
373
379 private bool UpdateConnectionStatus(DeviceProxy device)
380 {
381 bool isConnected = device.IsConnected;
382 bool isSecureChannelEstablished = device.IsSecurityEstablished;
383
384 // Default to false when initializing status checks
385 _lastOnlineConnectionStatus.TryAdd(device.Address, false);
386 _lastSecureConnectionStatus.TryAdd(device.Address, false);
387
388 bool onlineConnectionChanged = _lastOnlineConnectionStatus[device.Address] != isConnected;
389 bool secureChannelStatusChanged = _lastSecureConnectionStatus[device.Address] != isSecureChannelEstablished;
390
391 if (!onlineConnectionChanged && !secureChannelStatusChanged) return false;
392
393 _lastOnlineConnectionStatus[device.Address] = isConnected;
394 _lastSecureConnectionStatus[device.Address] = isSecureChannelEstablished;
395
396 var handler = ConnectionStatusChanged;
397 handler?.Invoke(this,
398 new ConnectionStatusEventArgs(device.Address, isConnected, device.IsSecurityEstablished));
399
400 return true;
401 }
402
403 private void ProcessReply(ReplyTracker reply, DeviceProxy device)
404 {
405 // Request from PD to reset connection
406 if (device.IsConnected && reply.ReplyMessage.ControlBlock.Sequence == 0)
407 {
408 ResetDevice(device);
409 return;
410 }
411
412 if (!reply.IsValidReply)
413 {
414 return;
415 }
416
417 if (reply.ReplyMessage.IsSecureMessage)
418 {
419 if (!reply.ReplyMessage.IsValidMac)
420 {
421 ResetDevice(device);
422 return;
423 }
424 }
425
426 if (reply.ReplyMessage.Type != (byte)ReplyType.Busy)
427 {
428 device.ValidReplyHasBeenReceived(reply.ReplyMessage.Sequence);
429 }
430 else
431 {
432 return;
433 }
434
435 if (reply.ReplyMessage.Type == (byte)ReplyType.Nak)
436 {
437 var errorCode = (ErrorCode)reply.ReplyMessage.Payload.First();
438 if ((device.IsSecurityEstablished &&
439 errorCode is ErrorCode.DoesNotSupportSecurityBlock or ErrorCode.CommunicationSecurityNotMet
440 or ErrorCode.UnableToProcessCommand) ||
441 errorCode == ErrorCode.UnexpectedSequenceNumber)
442 {
443 ResetDevice(device);
444 }
445 }
446
447 switch ((ReplyType)reply.ReplyMessage.Type)
448 {
449 case ReplyType.CrypticData:
450 device.InitializeSecureChannel(reply.ReplyMessage.Payload, reply.ReplyMessage.SecureBlockData);
451 break;
452 case ReplyType.InitialRMac:
453 if (!reply.ValidateSecureChannelEstablishment())
454 {
455 _logger?.LogError($"[{Connection}] Cryptogram not accepted by address {reply.ReplyMessage.Address}");
456 }
457 break;
458 }
459
460 _replies.Add(reply);
461 }
462
463 public void ResetDevice(int address)
464 {
465 var foundDevice = _configuredDevices.First(device => device.Address == address);
466
467 ResetDevice(foundDevice);
468 }
469
470 public void SetSendingMultipartMessage(byte address, bool isSendingMultipartMessage)
471 {
472 var foundDevice = _configuredDevices.First(device => device.Address == address);
473
474 foundDevice.IsSendingMultipartMessage = isSendingMultipartMessage;
475 }
476
477 public void SetReceivingMultipartMessage(byte address, bool isReceivingMultipartMessage)
478 {
479 var foundDevice = _configuredDevices.First(device => device.Address == address);
480
481 foundDevice.IsReceivingMultipartMessage = isReceivingMultipartMessage;
482 }
483
484 public void SetSendingMultiMessageNoSecureChannel(byte address, bool isSendingMultiMessageNoSecureChannel)
485 {
486 var foundDevice = _configuredDevices.First(device => device.Address == address);
487
488 foundDevice.IsSendingMultiMessageNoSecureChannel = isSendingMultiMessageNoSecureChannel;
489 foundDevice.MessageControl.IsSendingMultiMessageNoSecureChannel = isSendingMultiMessageNoSecureChannel;
490 if (isSendingMultiMessageNoSecureChannel)
491 {
492 foundDevice.ResetSecureChannelSession();
493 }
494 }
495
496 public void SetRequestDelay(byte address, DateTime requestDelay)
497 {
498 var foundDevice = _configuredDevices.First(device => device.Address == address);
499
500 foundDevice.RequestDelay = requestDelay;
501 }
502
503 private void ResetDevice(DeviceProxy device)
504 {
505 device.RequestDelay = DateTime.UtcNow + TimeSpan.FromSeconds(1);
506 AddDevice(device.Address, device.MessageControl.UseCrc, device.UseSecureChannel, device.SecureChannelKey);
507 }
508
509 private async Task<ReplyTracker> SendCommandAndReceiveReply(OutgoingMessage command, DeviceProxy device, CancellationToken cancellationToken)
510 {
511 byte[] commandData;
512 try
513 {
514 commandData = command.BuildMessage(device.MessageSecureChannel);
515 }
516 catch (Exception exception)
517 {
518 _logger?.LogError(exception, $"[{Connection}] Error while building command {command}");
519 throw;
520 }
521
522 await Connection.WriteAsync(commandData).ConfigureAwait(false);
523
524 _tracer(new TraceEntry(TraceDirection.Output, Id, commandData.Skip(1).ToArray()));
525
526 using var delayTime = new AutoResetEvent(false);
527 delayTime.WaitOne(IdleLineDelay(Connection, commandData.Length));
528
529 return await ReceiveReply(command, device, cancellationToken);
530 }
531
532 private async Task<ReplyTracker> ReceiveReply(OutgoingMessage command, DeviceProxy device, CancellationToken cancellationToken)
533 {
534 var replyBuffer = new Collection<byte>();
535
536 if (!await WaitForStartOfMessage(Connection, replyBuffer, device.IsSendingMultipartMessage ? TimeSpan.FromSeconds(1) : TimeSpan.Zero, cancellationToken).ConfigureAwait(false))
537 {
538 throw new TimeoutException("Timeout waiting for reply message");
539 }
540
541 if (!await WaitForMessageLength(Connection, replyBuffer, cancellationToken).ConfigureAwait(false))
542 {
543 throw new TimeoutException("Timeout waiting for reply message length");
544 }
545
546 if (!await WaitForRestOfMessage(Connection, replyBuffer, ExtractMessageLength(replyBuffer), cancellationToken).ConfigureAwait(false))
547 {
548 throw new TimeoutException("Timeout waiting for rest of reply message");
549 }
550
551 _tracer(new TraceEntry(TraceDirection.Input, Id, replyBuffer.ToArray()));
552
553 return new ReplyTracker(Id, new IncomingMessage(replyBuffer.ToArray(), device.MessageSecureChannel), command, device);
554 }
555
556 internal static ushort ExtractMessageLength(IReadOnlyList<byte> replyBuffer)
557 {
558 return Message.ConvertBytesToUnsignedShort(new[] {replyBuffer[2], replyBuffer[3]});
559 }
560
561 internal static async Task<bool> WaitForRestOfMessage(IOsdpConnection connection, ICollection<byte> replyBuffer, ushort replyLength, CancellationToken cancellationToken)
562 {
563 while (replyBuffer.Count < replyLength)
564 {
565 int maxReadBufferLength = connection.BaudRate / 40;
566 int remainingLength = replyLength - replyBuffer.Count;
567 byte[] readBuffer = new byte[Math.Min(maxReadBufferLength, remainingLength)];
568 int bytesRead =
569 await TimeOutReadAsync(connection, readBuffer, connection.ReplyTimeout + IdleLineDelay(connection, readBuffer.Length),
570 cancellationToken)
571 .ConfigureAwait(false);
572 if (bytesRead > 0)
573 {
574 for (int index = 0; index < bytesRead; index++)
575 {
576 replyBuffer.Add(readBuffer[index]);
577 }
578 }
579 else
580 {
581 return false;
582 }
583 }
584
585 return true;
586 }
587
588 internal static async Task<bool> WaitForMessageLength(IOsdpConnection connection, ICollection<byte> replyBuffer, CancellationToken cancellationToken)
589 {
590 while (replyBuffer.Count < 4)
591 {
592 byte[] readBuffer = new byte[4];
593 int bytesRead =
594 await TimeOutReadAsync(connection, readBuffer, connection.ReplyTimeout, cancellationToken)
595 .ConfigureAwait(false);
596 if (bytesRead > 0)
597 {
598 for (int index = 0; index < bytesRead; index++)
599 {
600 replyBuffer.Add(readBuffer[index]);
601 }
602 }
603 else
604 {
605 return false;
606 }
607 }
608
609 return true;
610 }
611
612 internal static async Task<bool> WaitForStartOfMessage(IOsdpConnection connection, ICollection<byte> replyBuffer, TimeSpan extraWait, CancellationToken cancellationToken)
613 {
614 while (true)
615 {
616 byte[] readBuffer = new byte[1];
617 int bytesRead = await TimeOutReadAsync(connection, readBuffer,
618 connection.ReplyTimeout + extraWait,
619 cancellationToken)
620 .ConfigureAwait(false);
621 if (bytesRead == 0)
622 {
623 return false;
624 }
625
626 if (readBuffer[0] != Message.StartOfMessage)
627 {
628 continue;
629 }
630
631 replyBuffer.Add(readBuffer[0]);
632 break;
633 }
634
635 return true;
636 }
637
638 private static async Task<int> TimeOutReadAsync(IOsdpConnection connection, byte[] buffer, TimeSpan timeout, CancellationToken cancellationToken)
639 {
640 using var timedCancellationTokenSource = new CancellationTokenSource(timeout);
641 using var linkedCancellationTokenSource =
642 CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timedCancellationTokenSource.Token);
643 try
644 {
645 return await connection.ReadAsync(buffer, linkedCancellationTokenSource.Token).ConfigureAwait(false);
646 }
647 catch
648 {
649 // TODO: be more selective with exception types?
650 return 0;
651 }
652 }
653
654 public class ConnectionStatusEventArgs
655 {
656 public ConnectionStatusEventArgs(byte address, bool isConnected, bool isSecureSessionEstablished)
657 {
658 Address = address;
659 IsConnected = isConnected;
660 IsSecureSessionEstablished = isSecureSessionEstablished;
661 }
662
663 public byte Address { get; }
664
665 public bool IsConnected { get; }
666
667 public bool IsSecureSessionEstablished { get; }
668 }
669 }
670}
Represents a message that was received from the wire.
bool IsValidMac
Indicates if the message has a valid MAC signature which was validated via local message channel cont...
byte Type
Command/reply code of the message.
byte Sequence
Message sequence number.
byte[] SecureBlockData
Raw security block bytes.
bool IsSecureMessage
Indicates if the message was sent via an established secure channel.
byte[] Payload
Returns the raw message payload.
A base message class for sending or replying.
Definition Message.cs:12
const byte StartOfMessage
OSDP message SOM (start of the message) byte.
Definition Message.cs:21
Defines a connection for communicating via OSDP protocol.
Task< int > ReadAsync(byte[] buffer, CancellationToken token)
Read from connection.
TimeSpan ReplyTimeout
Timeout value waiting for a reply.
Task WriteAsync(byte[] buffer)
Write to connection.
Task Close()
Close the connection for communications.
int BaudRate
Speed of the connection.
bool IsOpen
Is the connection open and ready to communicate.
Task Open()
Open the connection for communications.
ReplyType
Identifies the type of OSDP protocol reply (PD -> ACU) message.
Definition ReplyType.cs:7
CommandType
Identifies the type of OSDP protocol command (ACU -> PD) message.
Definition CommandType.cs:7
ErrorCode
Error code.
Definition Nak.cs:99
TraceDirection
Direction of trace.
Definition TraceEntry.cs:9
Represents low level data sent on the wire between the control panel and the devices.
Definition TraceEntry.cs:22