OSDP.Net 5.2.1
A .NET library for implementing the Open Supervised Device Protocol (OSDP)
Loading...
Searching...
No Matches
ControlPanel.cs
1using System;
2using System.Collections;
3using System.Collections.Concurrent;
4using System.Collections.Generic;
5using System.Linq;
6using System.Threading;
7using System.Threading.Tasks;
8using Microsoft.Extensions.Logging;
9using Microsoft.Extensions.Logging.Abstractions;
20
21namespace OSDP.Net
22{
27 public class ControlPanel
28 {
29 internal const byte ConfigurationAddress = 0x7f;
30
31 private readonly object _lockBusCreation = new ();
32 private readonly ConcurrentDictionary<Guid, Bus> _buses = new();
33 private readonly ILogger<ControlPanel> _logger;
34 private readonly BlockingCollection<ReplyTracker> _replies = new();
35 private TimeSpan _replyResponseTimeout = TimeSpan.FromSeconds(8);
36 private readonly ConcurrentDictionary<int, SemaphoreSlim> _requestLocks = new();
37 private readonly TimeSpan _timeToWaitToCheckOnData = TimeSpan.FromMilliseconds(10);
38 private readonly IDeviceProxyFactory _deviceProxyFactory;
39 private CancellationTokenSource _shutdownCts = new();
40 private int _inFlightCommands;
41
44 [Obsolete("Sending a ILogger is deprecated, please send ILoggerFactory instead.")]
45 public ControlPanel(ILogger<ControlPanel> logger) : this(null, null, logger) { }
46
49 public ControlPanel(ILoggerFactory loggerFactory = null) : this(null, loggerFactory, null) { }
50
51 internal ControlPanel(IDeviceProxyFactory deviceProxyFactory, ILoggerFactory loggerFactory = null)
52 : this(deviceProxyFactory, loggerFactory, null) { }
53
54 private ControlPanel(IDeviceProxyFactory deviceProxyFactory, ILoggerFactory loggerFactory,
55 ILogger<ControlPanel> logger)
56 {
57 _logger = logger
58 ?? loggerFactory?.CreateLogger<ControlPanel>()
59 ?? NullLoggerFactory.Instance.CreateLogger<ControlPanel>();
60 _deviceProxyFactory = deviceProxyFactory ?? new DeviceProxyFactory();
61
62 Task.Factory.StartNew(() =>
63 {
64 foreach (var reply in _replies.GetConsumingEnumerable())
65 {
66 OnReplyReceived(reply);
67 }
68 }, TaskCreationOptions.LongRunning);
69 }
70
76 public Guid StartConnection(IOsdpConnection connection) => StartConnection(connection, Bus.DefaultPollInterval);
77
85 public Guid StartConnection(IOsdpConnection connection, TimeSpan pollInterval, bool isTracing) =>
86 StartConnection(connection, pollInterval, isTracing ? OSDPFileCaptureTracer.Trace : _ => { });
87
94 public Guid StartConnection(IOsdpConnection connection, TimeSpan pollInterval) =>
95 StartConnection(connection, pollInterval, _ => { });
96
108 public Guid StartConnection(IOsdpConnection connection, TimeSpan pollInterval, Action<TraceEntry> tracer)
109 {
110 var newBus = CreateBus(connection, pollInterval, tracer);
111 newBus.ConnectionStatusChanged += BusOnConnectionStatusChanged;
112 newBus.StartPolling();
113 return newBus.Id;
114 }
115
122 private Bus CreateBus(IOsdpConnection connection, TimeSpan pollInterval, Action<TraceEntry> tracer)
123 {
124 // Lock while we check/create the bus. This is a very quick operation, so the lock is not a performance problem.
125 lock (_lockBusCreation)
126 {
127 var existingBusWithThisConnection = _buses.Values
128 .FirstOrDefault(bus => bus.Connection == connection);
129 if (existingBusWithThisConnection != null)
130 throw new InvalidOperationException(
131 $"The IOsdpConnection is already active in connection {existingBusWithThisConnection.Id}. " +
132 "That connection must be stopped before starting a new one.");
133
134 var newBus = new Bus(
135 connection,
136 _replies,
137 pollInterval,
138 tracer,
139 _deviceProxyFactory,
140 _logger);
141
142 _buses[newBus.Id] = newBus;
143 return newBus;
144 }
145 }
146
151 public async Task StopConnection(Guid connectionId)
152 {
153 if (!_buses.TryGetValue(connectionId, out Bus bus))
154 {
155 return;
156 }
157
158 // At this point, there might be multiple threads running concurrently trying to stop the connection.
159 // No worries, the method is reentrant. All threads will complete safely.
160
161 bus.ConnectionStatusChanged -= BusOnConnectionStatusChanged;
162 await bus.Close().ConfigureAwait(false);
163
164 foreach (byte address in bus.ConfigureDeviceAddresses)
165 {
166 // This will fire twice if two threads call StopConnection for the same connection simultaneously.
167 OnConnectionStatusChanged(bus.Id, address, false, false);
168 _requestLocks.TryRemove(new { connectionId, address }.GetHashCode(), out _);
169 }
170 bus.Dispose();
171 _buses.TryRemove(connectionId, out bus);
172 }
173
174 private void BusOnConnectionStatusChanged(object sender, Bus.ConnectionStatusEventArgs eventArgs)
175 {
176 if (sender is Bus bus)
177 OnConnectionStatusChanged(bus.Id, eventArgs.Address, eventArgs.IsConnected,
178 eventArgs.IsSecureSessionEstablished);
179 }
180
187 public async Task SendCustomCommand(Guid connectionId, byte address, CommandData command)
188 {
189 await SendCommand(connectionId, address, command).ConfigureAwait(false);
190 }
191
198 public void EnqueueCustomCommand(Guid connectionId, byte address, CommandData command)
199 {
200 if (!_buses.TryGetValue(connectionId, out var bus))
201 {
202 throw new ArgumentException("Connection could not be found", nameof(connectionId));
203 }
204
205 bus.SendCommand(address, command);
206 }
207
212 public async Task<DeviceIdentification> IdReport(Guid connectionId, byte address) =>
213 await IdReport(connectionId, address, CancellationToken.None).ConfigureAwait(false);
214
220 public async Task<DeviceIdentification> IdReport(Guid connectionId, byte address, CancellationToken cancellationToken)
221 {
222 var message = await SendCommand(
223 connectionId,
224 address,
225 new IdReport(),
226 cancellationToken).ConfigureAwait(false);
227
228 return DeviceIdentification.ParseData(message.Payload);
229 }
230
238 public async Task<ExtendedDeviceIdentification> ExtendedIdReport(Guid connectionId, byte address,
239 TimeSpan timeout, CancellationToken cancellationToken = default)
240 {
241 using var scope = await BeginCommandAsync(connectionId, address, timeout, cancellationToken);
242 return await WaitForExtendedIdData(connectionId, address, timeout, scope.Token);
243 }
244
245 private async Task<T> WaitForMultiPartData<T>(
246 Guid connectionId,
247 byte address,
248 TimeSpan timeout,
249 CommandData command,
250 Func<byte[], T> parser,
251 Action<EventHandler<MultiPartMessageDataReplyEventArgs>> subscribe,
252 Action<EventHandler<MultiPartMessageDataReplyEventArgs>> unsubscribe,
253 CancellationToken cancellationToken)
254 {
255 bool complete = false;
256 byte[] data = null;
257
258 void Handler(object sender, MultiPartMessageDataReplyEventArgs args)
259 {
260 // Only process matching replies
261 if (args.ConnectionId != connectionId || args.Address != address) return;
262
263 var fragment = args.DataFragmentResponse;
264 data ??= new byte[fragment.WholeMessageLength];
265
266 complete = Message.BuildMultiPartMessageData(fragment.WholeMessageLength, fragment.Offset,
267 fragment.LengthOfFragment, fragment.Data, data);
268 }
269
270 subscribe(Handler);
271 SetReceivingMultipartMessaging(connectionId, address, true);
272
273 try
274 {
275 await SendCommand(connectionId, address, command, cancellationToken, throwOnNak: false)
276 .ConfigureAwait(false);
277
278 DateTime endTime = DateTime.UtcNow + timeout;
279
280 while (DateTime.UtcNow <= endTime)
281 {
282 if (complete)
283 {
284 return parser(data);
285 }
286
287 await Task.Delay(_timeToWaitToCheckOnData, cancellationToken);
288 }
289
290 throw new TimeoutException("Timeout waiting to receive multi-part data.");
291 }
292 finally
293 {
294 unsubscribe(Handler);
295 SetReceivingMultipartMessaging(connectionId, address, false);
296 }
297 }
298
299 private async Task<ExtendedDeviceIdentification> WaitForExtendedIdData(Guid connectionId, byte address,
300 TimeSpan timeout, CancellationToken cancellationToken)
301 {
302 return await WaitForMultiPartData(
303 connectionId,
304 address,
305 timeout,
306 new IdReport(requestExtended: true),
307 data => ExtendedDeviceIdentification.ParseData(data),
308 handler => ExtendedIdReplyReceived += handler,
309 handler => ExtendedIdReplyReceived -= handler,
310 cancellationToken).ConfigureAwait(false);
311 }
312
317 public async Task<DeviceCapabilities> DeviceCapabilities(Guid connectionId, byte address)
318 {
319 return Model.ReplyData.DeviceCapabilities.ParseData((await SendCommand(
320 connectionId,
321 address,
322 new Model.CommandData.DeviceCapabilities()).ConfigureAwait(false)).Payload);
323 }
324
331 public async Task<LocalStatus> LocalStatus(Guid connectionId, byte address)
332 {
333 return Model.ReplyData.LocalStatus.ParseData((await SendCommand(
334 connectionId,
335 address,
336 new NoPayloadCommandData(CommandType.LocalStatus)).ConfigureAwait(false)).Payload);
337 }
338
345 public async Task<InputStatus> InputStatus(Guid connectionId, byte address)
346 {
347 return Model.ReplyData.InputStatus.ParseData((await SendCommand(
348 connectionId,
349 address,
350 new NoPayloadCommandData(CommandType.InputStatus)).ConfigureAwait(false)).Payload);
351 }
352
359 public async Task<OutputStatus> OutputStatus(Guid connectionId, byte address)
360 {
361 return Model.ReplyData.OutputStatus.ParseData((await SendCommand(
362 connectionId,
363 address,
364 new NoPayloadCommandData(CommandType.OutputStatus)).ConfigureAwait(false)).Payload);
365 }
366
376 public async Task<byte[]> GetPIVData(Guid connectionId, byte address, GetPIVData getPIVData, TimeSpan timeout,
377 CancellationToken cancellationToken = default)
378 {
379 using var scope = await BeginCommandAsync(connectionId, address, timeout, cancellationToken);
380 return await WaitForPIVData(connectionId, address, getPIVData, timeout, scope.Token);
381 }
382
383 private SemaphoreSlim GetRequestLock(Guid connectionId, byte address)
384 {
385 int hash = new { connectionId, address }.GetHashCode();
386
387 if (_requestLocks.TryGetValue(hash, out var requestLock))
388 {
389 return requestLock;
390 }
391
392 var newRequestLock = new SemaphoreSlim(1, 1);
393 _requestLocks[hash] = newRequestLock;
394 return newRequestLock;
395 }
396
397 private async Task<CommandScope> BeginCommandAsync(Guid connectionId, byte address, TimeSpan timeout,
398 CancellationToken cancellationToken)
399 {
400 var linkedCts =
401 CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _shutdownCts.Token);
402 Interlocked.Increment(ref _inFlightCommands);
403 try
404 {
405 var requestLock = GetRequestLock(connectionId, address);
406 if (!await requestLock.WaitAsync(timeout, linkedCts.Token).ConfigureAwait(false))
407 {
408 throw new TimeoutException("Timeout waiting for another request to complete.");
409 }
410 return new CommandScope(this, requestLock, linkedCts);
411 }
412 catch
413 {
414 linkedCts.Dispose();
415 Interlocked.Decrement(ref _inFlightCommands);
416 throw;
417 }
418 }
419
420 private sealed class CommandScope : IDisposable
421 {
422 private readonly ControlPanel _owner;
423 private readonly SemaphoreSlim _requestLock;
424 private readonly CancellationTokenSource _linkedCts;
425
426 public CommandScope(ControlPanel owner, SemaphoreSlim requestLock, CancellationTokenSource linkedCts)
427 {
428 _owner = owner;
429 _requestLock = requestLock;
430 _linkedCts = linkedCts;
431 }
432
433 public CancellationToken Token => _linkedCts.Token;
434
435 public void Dispose()
436 {
437 _requestLock.Release();
438 _linkedCts.Dispose();
439 Interlocked.Decrement(ref _owner._inFlightCommands);
440 }
441 }
442
443 private async Task<byte[]> WaitForPIVData(Guid connectionId, byte address, GetPIVData getPIVData, TimeSpan timeout,
444 CancellationToken cancellationToken)
445 {
446 return await WaitForMultiPartData(
447 connectionId,
448 address,
449 timeout,
450 getPIVData,
451 data => data,
452 handler => PIVDataReplyReceived += handler,
453 handler => PIVDataReplyReceived -= handler,
454 cancellationToken).ConfigureAwait(false);
455 }
456
463 public async Task<ReaderStatus> ReaderStatus(Guid connectionId, byte address)
464 {
465 return Model.ReplyData.ReaderStatus.ParseData((await SendCommand(connectionId, address,
466 new NoPayloadCommandData(CommandType.ReaderStatus)).ConfigureAwait(false)).Payload);
467 }
468
476 public async Task<ReturnReplyData<ManufacturerSpecific>> ManufacturerSpecificCommand(Guid connectionId,
477 byte address,
478 Model.CommandData.ManufacturerSpecific manufacturerSpecific)
479 {
480 var reply = await SendCommand(connectionId, address, manufacturerSpecific).ConfigureAwait(false);
481
482 return new ReturnReplyData<ManufacturerSpecific>
483 {
484 Ack = reply.Type == (byte)ReplyType.Ack,
485 ReplyData = reply.Type == (byte)ReplyType.ManufactureSpecific
486 ? ManufacturerSpecific.ParseData(reply.Payload)
487 : null
488 };
489 }
490
499 public async Task<ReturnReplyData<ExtendedRead>> ExtendedWriteData(Guid connectionId, byte address,
500 ExtendedWrite extendedWrite)
501 {
502 var reply = await SendCommand(connectionId, address, extendedWrite).ConfigureAwait(false);
503
504 return new ReturnReplyData<ExtendedRead>
505 {
506 Ack = reply.Type == (byte)ReplyType.Ack,
507 ReplyData = reply.Type == (byte)ReplyType.ExtendedRead
508 ? ExtendedRead.ParseData(reply.Payload)
509 : null
510 };
511 }
512
523 public async Task<ReturnReplyData<ManufacturerSpecific>> ManufacturerSpecificCommand(Guid connectionId, byte address,
524 Model.CommandData.ManufacturerSpecific manufacturerSpecific, ushort maximumFragmentSize, TimeSpan timeout,
525 CancellationToken cancellationToken = default)
526 {
527 using var scope = await BeginCommandAsync(connectionId, address, timeout, cancellationToken);
528 return await WaitForManufactureResponse(connectionId, address, manufacturerSpecific, maximumFragmentSize,
529 scope.Token);
530 }
531
532 private async Task<ReturnReplyData<ManufacturerSpecific>> WaitForManufactureResponse(Guid connectionId, byte address,
533 Model.CommandData.ManufacturerSpecific manufacturerSpecific, ushort maximumFragmentSize, CancellationToken cancellationToken)
534 {
535 SetReceivingMultipartMessaging(connectionId, address, true);
536
537 var manufactureCommandCode = manufacturerSpecific.Data.Skip(0).Take(1).ToArray();
538 var manufactureCommandData = manufacturerSpecific.Data.Skip(1).ToArray();
539 ushort totalSize = (ushort)manufactureCommandData.Length;
540 ushort offset = 0;
541 bool continueTransfer = true;
542 IncomingMessage reply = null;
543
544 try
545 {
546 while (!cancellationToken.IsCancellationRequested && continueTransfer)
547 {
548 ushort fragmentSize = (ushort)Math.Min(maximumFragmentSize, totalSize - offset);
549 reply = await SendCommand(connectionId, address, new Model.CommandData.ManufacturerSpecific(
550 manufacturerSpecific.VendorCode, manufactureCommandCode.Concat(
551 new MessageDataFragment(totalSize, offset, fragmentSize,
552 manufactureCommandData.Skip(offset).Take(fragmentSize).ToArray(),
553 MessageDataFragmentFieldSize.FourBytes).BuildData()
554 .ToArray()).ToArray()), cancellationToken)
555 .ConfigureAwait(false);
556
557 if (reply.Type != (byte)ReplyType.Ack) break;
558
559 offset += maximumFragmentSize;
560
561 // Determine if we should continue on successful status
562 continueTransfer = offset < totalSize;
563 }
564
565 if (reply == null)
566 {
567 throw new Exception("Command didn't return a valid reply");
568 }
569
570 return new ReturnReplyData<ManufacturerSpecific>
571 {
572 Ack = reply.Type == (byte)ReplyType.Ack,
574 };
575 }
576 finally
577 {
578 SetReceivingMultipartMessaging(connectionId, address, false);
579 }
580 }
581
589 public async Task<ReturnReplyData<OutputStatus>> OutputControl(Guid connectionId, byte address,
590 OutputControls outputControls)
591 {
592 var reply = await SendCommand(connectionId, address, outputControls).ConfigureAwait(false);
593
594 return new ReturnReplyData<OutputStatus>
595 {
596 Ack = reply.Type == (byte)ReplyType.Ack,
597 ReplyData = reply.Type == (byte)ReplyType.OutputStatusReport
598 ? Model.ReplyData.OutputStatus.ParseData(reply.Payload)
599 : null
600 };
601 }
602
610 public async Task<bool> ReaderLedControl(Guid connectionId, byte address, ReaderLedControls readerLedControls)
611 {
612 var reply = await SendCommand(connectionId, address, readerLedControls).ConfigureAwait(false);
613
614 return reply.Type == (byte)ReplyType.Ack;
615 }
616
624 public async Task<bool> ReaderBuzzerControl(Guid connectionId, byte address,
625 ReaderBuzzerControl readerBuzzerControl)
626 {
627 var reply = await SendCommand(connectionId, address, readerBuzzerControl).ConfigureAwait(false);
628
629 return reply.Type == (byte)ReplyType.Ack;
630 }
631
639 public async Task<bool> ReaderTextOutput(Guid connectionId, byte address, ReaderTextOutput readerTextOutput)
640 {
641 var reply = await SendCommand(connectionId, address, readerTextOutput).ConfigureAwait(false);
642
643 return reply.Type == (byte)ReplyType.Ack;
644 }
645
654 byte address, CommunicationConfiguration communicationConfiguration)
655 {
656 if (_buses.Any(bus =>
657 bus.Key == connectionId &&
658 address != communicationConfiguration.Address &&
659 bus.Value.ConfigureDeviceAddresses.Any(configuredAddress =>
660 configuredAddress == communicationConfiguration.Address)))
661 {
662 throw new Exception("Address is already configured on the bus.");
663 }
664
666 (await SendCommand(connectionId, address, communicationConfiguration).ConfigureAwait(false))
667 .Payload);
668 }
669
677 public async Task<bool> EncryptionKeySet(Guid connectionId, byte address,
678 EncryptionKeyConfiguration encryptionKeyConfiguration)
679 {
680 var reply = await SendCommand(connectionId, address, encryptionKeyConfiguration).ConfigureAwait(false);
681
682 return reply.Type == (byte)ReplyType.Ack;
683 }
684
697 public Task<Model.ReplyData.FileTransferStatus.StatusDetail> FileTransfer(Guid connectionId, byte address, byte fileType, byte[] fileData, ushort fragmentSize,
698 Action<FileTransferStatus> callback, CancellationToken cancellationToken = default)
699 {
700 var bus = _buses[connectionId];
701 return Task.Run(async () =>
702 {
703 bus.SetSendingMultipartMessage(address, true);
704 try
705 {
706 return await SendFileTransferCommands(connectionId, address, fileType, fileData, fragmentSize, callback,
707 cancellationToken);
708 }
709 finally
710 {
711 bus.SetSendingMultipartMessage(address, false);
712 bus.SetSendingMultiMessageNoSecureChannel(address, false);
713 }
714 }, cancellationToken);
715 }
716
717 private async Task<Model.ReplyData.FileTransferStatus.StatusDetail> SendFileTransferCommands(Guid connectionId,
718 byte address, byte fileType, byte[] fileData,
719 ushort fragmentSize, Action<FileTransferStatus> callback, CancellationToken cancellationToken)
720 {
721 int totalSize = fileData.Length;
722 int offset = 0;
723 bool isFirstMessage = true;
724
725 // Keep going until
726 // * operation's cancelled
727 // * an error occurs
728 // * transfer's completed
729 while (true)
730 {
731 cancellationToken.ThrowIfCancellationRequested();
732
733 // Get the fragment size if it doesn't exceed the total size
734 var nextFragmentSize = (ushort)Math.Min(fragmentSize, totalSize - offset);
735
736 var reply = await SendCommand(connectionId, address,
737 new FileTransferFragment(fileType,
738 new MessageDataFragment(totalSize, offset, nextFragmentSize,
739 fileData.Skip(offset).Take(nextFragmentSize).ToArray(),
741 cancellationToken, throwOnNak: false)
742 .ConfigureAwait(false);
743
744 // On the first message, calculate the proper fragment size based on whether encryption is being used
745 if (isFirstMessage)
746 {
747 isFirstMessage = false;
748 fragmentSize = Message.CalculateMaximumMessageSize(fragmentSize, reply.IsSecureMessage);
749 }
750
751 // Update offset
752 offset += nextFragmentSize;
753
754 // Parse the fileTransfer status
755 var fileTransferStatusReply = reply.Type == (byte)ReplyType.FileTransferStatus
756 ? Model.ReplyData.FileTransferStatus.ParseData(reply.Payload)
757 : null;
758
759 // Check reply to see if PD has requested some change in com procedures.
760 if (fileTransferStatusReply != null)
761 {
762 // Leave the secure channel if needed
763 if ((fileTransferStatusReply.Action &
764 Model.ReplyData.FileTransferStatus.ControlFlags.LeaveSecureChannel) ==
766 {
767 _buses[connectionId].SetSendingMultiMessageNoSecureChannel(address, true);
768 }
769
770 // Set request delay if specified
771 if (fileTransferStatusReply is { RequestedDelay: > 0 })
772 {
773 _buses[connectionId].SetRequestDelay(address,
774 DateTime.UtcNow.AddMilliseconds(fileTransferStatusReply.RequestedDelay));
775 }
776
777 // Set fragment size if requested
778 if (fileTransferStatusReply is { UpdateMessageMaximum: > 0 })
779 {
780 fragmentSize = Message.CalculateMaximumMessageSize(fileTransferStatusReply.UpdateMessageMaximum,
781 reply.IsSecureMessage);
782 }
783 }
784
785 var status = new FileTransferStatus(
786 fileTransferStatusReply?.Detail ?? Model.ReplyData.FileTransferStatus.StatusDetail.UnknownError,
787 offset,
788 reply.Type == (byte)ReplyType.Nak ? Nak.ParseData(reply.Payload) : null);
789
790 // Report status to progress listeners
791 callback(status);
792
793 switch (status.Status)
794 {
795 // Abort transfer on error.
796 case < 0:
797 throw new FileTransferException("File transfer failed", status);
798 // File transfer is completed, but PD wants us to keep sending "idling" file transfer message (FtFragmentSize = 0) until we receive another status.
799 case Model.ReplyData.FileTransferStatus.StatusDetail.FinishingFileTransfer:
800 fragmentSize = 0;
801 break;
802 default:
803 {
804 if (offset >= totalSize)
805 {
806 // We're done. Return the last successful (=positive) status code.
807 return status.Status;
808 }
809
810 break;
811 }
812 }
813 }
814 }
815
825 public async Task<BiometricReadResult> ScanAndSendBiometricData(Guid connectionId, byte address,
826 BiometricReadData biometricReadData, TimeSpan timeout,
827 CancellationToken cancellationToken = default)
828 {
829 using var scope = await BeginCommandAsync(connectionId, address, timeout, cancellationToken);
830 return await WaitForBiometricRead(connectionId, address, biometricReadData, timeout, scope.Token);
831 }
832
833 private async Task<BiometricReadResult> WaitForBiometricRead(Guid connectionId, byte address,
834 BiometricReadData biometricReadData, TimeSpan timeout,
835 CancellationToken cancellationToken)
836 {
837 bool complete = false;
838 BiometricReadResult result = null;
839
840 void Handler(object sender, BiometricReadResultsReplyEventArgs args)
841 {
842 // Only process matching replies
843 if (args.ConnectionId != connectionId || args.Address != address) return;
844
845 complete = true;
846
847 result = args.BiometricReadResult;
848 }
849
850 BiometricReadResultsReplyReceived += Handler;
851 SetReceivingMultipartMessaging(connectionId, address, true);
852
853 try
854 {
855 await SendCommand(connectionId, address, biometricReadData, cancellationToken, throwOnNak: false)
856 .ConfigureAwait(false);
857
858 DateTime endTime = DateTime.UtcNow + timeout;
859
860 while (DateTime.UtcNow <= endTime)
861 {
862 if (complete)
863 {
864 return result;
865 }
866
867 await Task.Delay(_timeToWaitToCheckOnData, cancellationToken);
868 }
869
870 throw new TimeoutException("Timeout waiting to for biometric read data.");
871 }
872 finally
873 {
874 BiometricReadResultsReplyReceived -= Handler;
875 SetReceivingMultipartMessaging(connectionId, address, false);
876 }
877 }
878
879
889 public async Task<BiometricMatchResult> ScanAndMatchBiometricTemplate(Guid connectionId, byte address,
890 BiometricTemplateData biometricTemplateData, TimeSpan timeout,
891 CancellationToken cancellationToken = default)
892 {
893 using var scope = await BeginCommandAsync(connectionId, address, timeout, cancellationToken);
894 return await WaitForBiometricMatch(connectionId, address, biometricTemplateData, timeout, scope.Token);
895 }
896
897 private async Task<BiometricMatchResult> WaitForBiometricMatch(Guid connectionId, byte address,
898 BiometricTemplateData biometricTemplateData, TimeSpan timeout,
899 CancellationToken cancellationToken)
900 {
901 bool complete = false;
902 BiometricMatchResult result = null;
903
904 void Handler(object sender, BiometricMatchReplyEventArgs args)
905 {
906 // Only process matching replies
907 if (args.ConnectionId != connectionId || args.Address != address) return;
908
909 complete = true;
910
911 result = args.BiometricMatchResult;
912 }
913
914 BiometricMatchReplyReceived += Handler;
915 SetReceivingMultipartMessaging(connectionId, address, true);
916
917 try
918 {
919 await SendCommand(connectionId, address, biometricTemplateData, cancellationToken, throwOnNak: false)
920 .ConfigureAwait(false);
921
922 DateTime endTime = DateTime.UtcNow + timeout;
923
924 while (DateTime.UtcNow <= endTime)
925 {
926 if (complete)
927 {
928 return result;
929 }
930
931 await Task.Delay(_timeToWaitToCheckOnData, cancellationToken);
932 }
933
934 throw new TimeoutException("Timeout waiting to for biometric match.");
935 }
936 finally
937 {
938 BiometricMatchReplyReceived -= Handler;
939 SetReceivingMultipartMessaging(connectionId, address, false);
940 }
941 }
942
955 public async Task<byte[]> AuthenticationChallenge(Guid connectionId, byte address,
956 byte algorithm, byte key, byte[] challenge, ushort maximumFragmentSize, TimeSpan timeout,
957 CancellationToken cancellationToken = default)
958 {
959 using var scope = await BeginCommandAsync(connectionId, address, timeout, cancellationToken);
960 return await WaitForChallengeResponse(connectionId, address, algorithm, key, challenge, maximumFragmentSize,
961 timeout, scope.Token);
962 }
963
964 private async Task<byte[]> WaitForChallengeResponse(Guid connectionId, byte address, byte algorithm, byte key,
965 byte[] challenge, ushort maximumFragmentSize, TimeSpan timeout,
966 CancellationToken cancellationToken)
967 {
968 bool complete = false;
969 var requestData = new List<byte> { algorithm, key };
970 requestData.AddRange(challenge);
971 byte[] responseData = null;
972
973 void Handler(object sender, MultiPartMessageDataReplyEventArgs args)
974 {
975 // Only process matching replies
976 if (args.ConnectionId != connectionId || args.Address != address) return;
977
978 var dataFragmentResponse = args.DataFragmentResponse;
979 responseData ??= new byte[dataFragmentResponse.WholeMessageLength];
980
981 complete = Message.BuildMultiPartMessageData(dataFragmentResponse.WholeMessageLength, dataFragmentResponse.Offset,
982 dataFragmentResponse.LengthOfFragment, dataFragmentResponse.Data, responseData);
983 }
984
985 AuthenticationChallengeResponseReceived += Handler;
986 SetReceivingMultipartMessaging(connectionId, address, true);
987
988 ushort totalSize = (ushort)requestData.Count;
989 ushort offset = 0;
990 bool continueTransfer = true;
991
992 try
993 {
994 while (!cancellationToken.IsCancellationRequested && continueTransfer)
995 {
996 ushort fragmentSize = (ushort)Math.Min(maximumFragmentSize, totalSize - offset);
997 await SendCommand(connectionId, address, new AuthenticationChallengeFragment(
998 new MessageDataFragment(totalSize, offset, fragmentSize,
999 requestData.Skip(offset).Take((ushort)Math.Min(fragmentSize, totalSize - offset))
1000 .ToArray(),
1001 MessageDataFragmentFieldSize.TwoBytes)), cancellationToken)
1002 .ConfigureAwait(false);
1003
1004 offset += fragmentSize;
1005
1006 // Determine if we should continue on successful status
1007 continueTransfer = offset < totalSize;
1008 }
1009
1010 DateTime endTime = DateTime.UtcNow + timeout;
1011
1012 while (DateTime.UtcNow <= endTime)
1013 {
1014 if (complete)
1015 {
1016 return responseData;
1017 }
1018
1019 await Task.Delay(_timeToWaitToCheckOnData, cancellationToken);
1020 }
1021
1022 throw new TimeoutException("Timeout waiting to receive challenge response.");
1023 }
1024 finally
1025 {
1026 AuthenticationChallengeResponseReceived -= Handler;
1027 SetReceivingMultipartMessaging(connectionId, address, false);
1028 }
1029 }
1030
1038 public async Task<bool> ACUReceiveSize(Guid connectionId, byte address, ushort maximumReceiveSize)
1039 {
1040 var reply = await SendCommand(connectionId, address, new ACUReceiveSize(maximumReceiveSize))
1041 .ConfigureAwait(false);
1042
1043 return reply.Type == (byte)ReplyType.Ack;
1044 }
1045
1053 public async Task<bool> KeepReaderActive(Guid connectionId, byte address, ushort keepAliveTimeInMilliseconds)
1054 {
1055 var reply = await SendCommand(connectionId, address, new KeepReaderActive(keepAliveTimeInMilliseconds))
1056 .ConfigureAwait(false);
1057
1058 return reply.Type == (byte)ReplyType.Ack;
1059 }
1060
1067 public async Task<bool> AbortCurrentOperation(Guid connectionId, byte address)
1068 {
1069 var reply = await SendCommand(connectionId, address, new NoPayloadCommandData(CommandType.Abort))
1070 .ConfigureAwait(false);
1071
1072 return reply.Type == (byte)ReplyType.Ack;
1073 }
1074
1081 public bool IsOnline(Guid connectionId, byte address)
1082 {
1083 return _buses[connectionId].IsOnline(address);
1084 }
1085
1086 private void SetReceivingMultipartMessaging(Guid connectionId, byte address, bool isReceivingMultipartMessaging)
1087 {
1088 if (!_buses.TryGetValue(connectionId, out var bus))
1089 {
1090 throw new ArgumentException("Connection could not be found", nameof(connectionId));
1091 }
1092
1093 bus.SetReceivingMultipartMessage(address, isReceivingMultipartMessaging);
1094 }
1095
1096 private async Task<IncomingMessage> SendCommand(Guid connectionId, byte address, CommandData command,
1097 CancellationToken cancellationToken = default, bool throwOnNak = true)
1098 {
1099 var source = new TaskCompletionSource<IncomingMessage>();
1100
1101 void EventHandler(object sender, ReplyEventArgs replyEventArgs)
1102 {
1103 var reply = replyEventArgs.Reply;
1104 if (!reply.MatchIssuingCommand(address, command.Code)) return;
1105
1106 if (throwOnNak && replyEventArgs.Reply.ReplyMessage.Type == (byte)ReplyType.Nak)
1107 {
1108 source.SetException(new NackReplyException(Nak.ParseData(reply.ReplyMessage.Payload)));
1109 }
1110 else
1111 {
1112 source.SetResult(reply.ReplyMessage);
1113 }
1114 }
1115
1116 if (!_buses.TryGetValue(connectionId, out var bus))
1117 {
1118 throw new ArgumentException("Connection could not be found", nameof(connectionId));
1119 }
1120
1121 ReplyReceived += EventHandler;
1122
1123 try
1124 {
1125 bus.SendCommand(address, command);
1126
1127 var task = await Task.WhenAny(
1128 source.Task, Task.Delay(_replyResponseTimeout, cancellationToken)).ConfigureAwait(false);
1129
1130 if (source.Task != task)
1131 {
1132 throw task.IsCanceled
1133 ? new OperationCanceledException(cancellationToken)
1134 : new TimeoutException();
1135 }
1136
1137 return await source.Task;
1138 }
1139 finally
1140 {
1141 ReplyReceived -= EventHandler;
1142 }
1143 }
1144
1148 public async Task Shutdown()
1149 {
1150 var shutdownCts = _shutdownCts;
1151 shutdownCts.Cancel();
1152
1153 foreach (var bus in _buses.Values)
1154 {
1155 bus.ConnectionStatusChanged -= BusOnConnectionStatusChanged;
1156 await bus.Close().ConfigureAwait(false);
1157
1158 foreach (byte address in bus.ConfigureDeviceAddresses)
1159 {
1160 OnConnectionStatusChanged(bus.Id, address, false, false);
1161 }
1162 bus.Dispose();
1163 }
1164 _buses.Clear();
1165
1166 while (Volatile.Read(ref _inFlightCommands) > 0)
1167 {
1168 await Task.Delay(_timeToWaitToCheckOnData).ConfigureAwait(false);
1169 }
1170
1171 foreach (var requestLock in _requestLocks.Values)
1172 {
1173 requestLock.Dispose();
1174 }
1175 _requestLocks.Clear();
1176
1177 _shutdownCts = new CancellationTokenSource();
1178 shutdownCts.Dispose();
1179
1180 OSDPFileCaptureTracer.CloseAllWriters();
1181 }
1182
1188 public void ResetDevice(Guid connectionId, int address)
1189 {
1190 if (_buses.TryGetValue(connectionId, out Bus bus))
1191 {
1192 bus.ResetDevice(address);
1193 }
1194 }
1195
1204 public void AddDevice(Guid connectionId, byte address, bool useCrc, bool useSecureChannel, byte[] secureChannelKey = null)
1205 {
1206 if (!_buses.TryGetValue(connectionId, out Bus foundBus))
1207 {
1208 throw new ArgumentException("Connection could not be found", nameof(connectionId));
1209 }
1210
1211 if (address > 127)
1212 {
1213 throw new ArgumentOutOfRangeException(nameof(address), "Address is out of range, it must between 0 and 127.");
1214 }
1215
1216 foundBus.AddDevice(address, useCrc, useSecureChannel, useSecureChannel ? secureChannelKey : null);
1217 }
1218
1224 public void RemoveDevice(Guid connectionId, byte address)
1225 {
1226 if (_buses.TryGetValue(connectionId, out Bus bus))
1227 {
1228 bus.RemoveDevice(address);
1229 }
1230 }
1231
1246 public async Task<DiscoveryResult> DiscoverDevice(IEnumerable<IOsdpConnection> connections, DiscoveryOptions options = null)
1247 {
1248 options ??= new DiscoveryOptions();
1249 DiscoveryResult result = new();
1250 Guid connectionId = Guid.Empty;
1251
1252 // ReSharper disable AccessToModifiedClosure
1253 // Disable the warning which doesn't apply to us as we are accumulating/modifying results of the discovery
1254 // in the above variables which are closed over. We are good with with
1255
1256 void UpdateStatus(DiscoveryStatus status)
1257 {
1258 result.Status = status;
1259 options.ProgressCallback?.Invoke(result);
1260 }
1261
1262 async Task<DeviceIdentification> TryIdReport(byte address)
1263 {
1264 try
1265 {
1266 return await IdReport(connectionId, address, options.CancellationToken).ConfigureAwait(false);
1267 }
1268 catch (TimeoutException)
1269 {
1270 return null;
1271 }
1272 }
1273
1274 async Task<bool> FindConnectionWithActiveDevice()
1275 {
1276 bool isFirst = true;
1277
1278 foreach (IOsdpConnection connection in connections)
1279 {
1280 if (!isFirst)
1281 {
1282 await Task.Delay(options.ReconnectDelay, options.CancellationToken);
1283 }
1284 isFirst = false;
1285
1286 // WARNING: We are specifying zero timespan here so that Polling is
1287 // DISABLED on this entire connection. If we don't do this, it seems
1288 // non-existent devices will attempt to start polling and those commands
1289 // ends up getting "stuck" so that even when we get to the valid device
1290 // we'll end up timing out and not finding it :(
1291 // Polling on "other" devices should be doing this, but there's something
1292 // funky in there. If the device we are looking for happens to be at addr
1293 // 0, everything will work, but anything else doesn't unless we disable
1294 // polling
1295 connectionId = StartConnection(connection, TimeSpan.Zero, options.Tracer ?? (_ => {}));
1296
1297 AddDevice(connectionId, ConfigurationAddress, true, false);
1298
1299 result.Connection = connection;
1300 UpdateStatus(DiscoveryStatus.LookingForDeviceOnConnection);
1301
1302 var deviceIdentification = await TryIdReport(ConfigurationAddress).ConfigureAwait(false);
1303 if (deviceIdentification != null)
1304 {
1305 result.Id = deviceIdentification;
1306 RemoveDevice(connectionId, ConfigurationAddress);
1307 await StopConnection(connectionId).ConfigureAwait(false);
1308 UpdateStatus(DiscoveryStatus.ConnectionWithDeviceFound);
1309 return true;
1310 }
1311
1312 await StopConnection(connectionId).ConfigureAwait(false);
1313 }
1314
1315 result.Connection = null;
1316 return false;
1317 }
1318
1319 async Task<bool> FindDeviceAddress()
1320 {
1321 connectionId = StartConnection(result.Connection, TimeSpan.Zero, options.Tracer ?? (_ => {}));
1322 for (byte address = 0; address < ConfigurationAddress; address++)
1323 {
1324 AddDevice(connectionId, address, true, false);
1325
1326 result.Address = address;
1327 UpdateStatus(DiscoveryStatus.LookingForDeviceAtAddress);
1328 var deviceIdentification = await TryIdReport(address).ConfigureAwait(false);
1329
1330 if (deviceIdentification != null)
1331 {
1332 result.Id = deviceIdentification;
1333 UpdateStatus(DiscoveryStatus.DeviceIdentified);
1334 return true;
1335 }
1336
1337 RemoveDevice(connectionId, address);
1338 }
1339 await StopConnection(connectionId).ConfigureAwait(false);
1340
1341 // Since we didn't find a valid device, for an unexpected reason
1342 // let's just leave the at the configuration address
1343 result.Address = ConfigurationAddress;
1344 return false;
1345 }
1346
1347 async Task CheckForDefaultSecurityKey()
1348 {
1349 var waitForSecureConnection = new TaskCompletionSource<bool>();
1350
1351 void HandleStatusChange(object sender, ConnectionStatusEventArgs args)
1352 {
1353 if (args.Address == result.Address && args.IsConnected && args.IsSecureChannelEstablished)
1354 {
1355 waitForSecureConnection.SetResult(true);
1356 }
1357 }
1358
1359 ConnectionStatusChanged += HandleStatusChange;
1360
1361 try
1362 {
1363 // Right now control panel API doesn't indicate connection errors due invalid credentials, even
1364 // though internally the bus knows it got an error response from the device. Until underlying APIs give
1365 // us that data, the best we can do here is wait for a successful connection and timeout when one
1366 // isn't established
1367 connectionId = StartConnection(result.Connection, Bus.DefaultPollInterval,
1368 options.Tracer ?? (_ => { }));
1369 AddDevice(connectionId, result.Address, true, true);
1370
1371 using var waitForTask = await Task.WhenAny(waitForSecureConnection.Task,
1372 Task.Delay(TimeSpan.FromSeconds(8), options.CancellationToken));
1373 result.UsesDefaultSecurityKey = waitForTask == waitForSecureConnection.Task;
1374 }
1375 finally
1376 {
1377 ConnectionStatusChanged -= HandleStatusChange;
1378 }
1379 }
1380
1381 UpdateStatus(DiscoveryStatus.Started);
1382
1383 // While we are doing discovery, we want to override default behavior which allows
1384 // underlying wire protocols to retry things if something goes wrong. In the
1385 // interests for time while cycling through every possible address, we are going to
1386 // not be as gracious
1387 var origResponseTimeout = _replyResponseTimeout;
1388 _replyResponseTimeout = options.ResponseTimeout;
1389
1390 try
1391 {
1392 if (!_buses.IsEmpty)
1393 {
1394 throw new ControlPanelInUseException();
1395 }
1396
1397 if (!await FindConnectionWithActiveDevice().ConfigureAwait(false))
1398 {
1399 UpdateStatus(DiscoveryStatus.DeviceNotFound);
1400 return null;
1401 }
1402
1403 if (!await FindDeviceAddress().ConfigureAwait(false))
1404 {
1405 throw new DeviceDiscoveryException(
1406 $"Unable to determine address of device that responded to a configuration address on baud rate {result.Connection.BaudRate}");
1407 }
1408
1409 result.Capabilities = await DeviceCapabilities(connectionId, result.Address).ConfigureAwait(false);
1410 UpdateStatus(DiscoveryStatus.CapabilitiesDiscovered);
1411
1412 // Connection above was opened intentionally with no polling (see above comments). Now we
1413 // need to open a new connection with polling before doing the next step
1414 await StopConnection(connectionId).ConfigureAwait(false);
1415 connectionId = Guid.Empty;
1416
1417 await CheckForDefaultSecurityKey().ConfigureAwait(false);
1418
1419 UpdateStatus(DiscoveryStatus.Succeeded);
1420 return result;
1421 }
1422 catch (Exception exception)
1423 {
1424 result.Error = exception;
1425 UpdateStatus(exception is OperationCanceledException
1426 ? DiscoveryStatus.Cancelled : DiscoveryStatus.Error);
1427 throw;
1428 }
1429 finally
1430 {
1431 if (connectionId != Guid.Empty) await StopConnection(connectionId).ConfigureAwait(false);
1432 _replyResponseTimeout = origResponseTimeout;
1433 }
1434
1435 // ReSharper enable AccessToModifiedClosure
1436 }
1437
1438 private void OnConnectionStatusChanged(Guid connectionId, byte address, bool isConnected,
1439 bool isSecureChannelEstablished)
1440 {
1441 ConnectionStatusChanged?.Invoke(this,
1442 new ConnectionStatusEventArgs(connectionId, address, isConnected, isSecureChannelEstablished));
1443 }
1444
1445 private void OnReplyReceived(ReplyTracker reply)
1446 {
1447 // Not wrapped in try/catch: ReplyReceived is a private event whose subscribers are
1448 // internal SendCommand handlers. Exceptions must propagate back to callers via the
1449 // TaskCompletionSource so they receive the actual error instead of a timeout.
1450 ReplyReceived?.Invoke(this, new ReplyEventArgs { Reply = reply });
1451
1452 try
1453 {
1454 switch ((ReplyType)reply.ReplyMessage.Type)
1455 {
1456 case ReplyType.Nak:
1457 NakReplyReceived?.Invoke(this,
1458 new NakReplyEventArgs(reply.ConnectionId, reply.ReplyMessage.Address,
1459 Nak.ParseData(reply.ReplyMessage.Payload)));
1460 break;
1461 case ReplyType.LocalStatusReport:
1462 LocalStatusReportReplyReceived?.Invoke(this,
1463 new LocalStatusReportReplyEventArgs(reply.ConnectionId,
1464 reply.ReplyMessage.Address,
1465 Model.ReplyData.LocalStatus.ParseData(reply.ReplyMessage.Payload)));
1466 break;
1467 case ReplyType.InputStatusReport:
1468 InputStatusReportReplyReceived?.Invoke(this,
1469 new InputStatusReportReplyEventArgs(reply.ConnectionId,
1470 reply.ReplyMessage.Address,
1471 Model.ReplyData.InputStatus.ParseData(reply.ReplyMessage.Payload)));
1472 break;
1473 case ReplyType.OutputStatusReport:
1475 new OutputStatusReportReplyEventArgs(reply.ConnectionId,
1476 reply.ReplyMessage.Address,
1477 Model.ReplyData.OutputStatus.ParseData(reply.ReplyMessage.Payload)));
1478 break;
1479 case ReplyType.ReaderStatusReport:
1481 new ReaderStatusReportReplyEventArgs(reply.ConnectionId,
1482 reply.ReplyMessage.Address,
1483 Model.ReplyData.ReaderStatus.ParseData(reply.ReplyMessage.Payload)));
1484 break;
1485 case ReplyType.RawReaderData:
1486 RawCardDataReplyReceived?.Invoke(this,
1487 new RawCardDataReplyEventArgs(reply.ConnectionId,
1488 reply.ReplyMessage.Address,
1489 RawCardData.ParseData(reply.ReplyMessage.Payload)));
1490 break;
1491 case ReplyType.FormattedReaderData:
1492 FormattedCardDataReplyReceived?.Invoke(this,
1493 new FormattedCardDataReplyEventArgs(reply.ConnectionId,
1494 reply.ReplyMessage.Address,
1495 FormattedCardData.ParseData(reply.ReplyMessage.Payload)));
1496 break;
1497 case ReplyType.ManufactureSpecific:
1499 new ManufacturerSpecificReplyEventArgs(reply.ConnectionId,
1500 reply.ReplyMessage.Address,
1501 ManufacturerSpecific.ParseData(reply.ReplyMessage.Payload)));
1502 break;
1503 case ReplyType.ExtendedRead:
1504 ExtendedReadReplyReceived?.Invoke(this,
1505 new ExtendedReadReplyEventArgs(reply.ConnectionId,
1506 reply.ReplyMessage.Address,
1507 ExtendedRead.ParseData(reply.ReplyMessage.Payload)));
1508 break;
1509 case ReplyType.PIVData:
1510 PIVDataReplyReceived?.Invoke(this,
1511 new MultiPartMessageDataReplyEventArgs(reply.ConnectionId,
1512 reply.ReplyMessage.Address,
1513 DataFragmentResponse.ParseData(reply.ReplyMessage.Payload)));
1514 break;
1515 case ReplyType.ExtendedPdIdReport:
1516 ExtendedIdReplyReceived?.Invoke(this,
1517 new MultiPartMessageDataReplyEventArgs(reply.ConnectionId,
1518 reply.ReplyMessage.Address,
1519 DataFragmentResponse.ParseData(reply.ReplyMessage.Payload)));
1520 break;
1521 case ReplyType.ResponseToChallenge:
1522 AuthenticationChallengeResponseReceived?.Invoke(this,
1523 new MultiPartMessageDataReplyEventArgs(reply.ConnectionId,
1524 reply.ReplyMessage.Address,
1525 DataFragmentResponse.ParseData(reply.ReplyMessage.Payload)));
1526 break;
1527 case ReplyType.KeypadData:
1528 KeypadReplyReceived?.Invoke(this,
1529 new KeypadReplyEventArgs(reply.ConnectionId, reply.ReplyMessage.Address,
1530 KeypadData.ParseData(reply.ReplyMessage.Payload)));
1531 break;
1532 case ReplyType.BiometricData:
1533 BiometricReadResultsReplyReceived?.Invoke(this,
1534 new BiometricReadResultsReplyEventArgs(reply.ConnectionId,
1535 reply.ReplyMessage.Address,
1536 BiometricReadResult.ParseData(reply.ReplyMessage.Payload)));
1537 break;
1538 case ReplyType.BiometricMatchResult:
1539 BiometricMatchReplyReceived?.Invoke(this,
1540 new BiometricMatchReplyEventArgs(reply.ConnectionId,
1541 reply.ReplyMessage.Address,
1542 BiometricMatchResult.ParseData(reply.ReplyMessage.Payload)));
1543 break;
1544 }
1545 }
1546 catch (Exception ex)
1547 {
1548 _logger.LogError(ex,
1549 "Error in typed reply event handler for reply {ReplyType} from address {Address}",
1550 (ReplyType)reply.ReplyMessage.Type, reply.ReplyMessage.Address);
1551 }
1552
1553 try
1554 {
1555 RawReplyReceived?.Invoke(this,
1556 new RawReplyEventArgs(reply.ConnectionId, reply.ReplyMessage.Address,
1557 reply.ReplyMessage.Type, reply.ReplyMessage.Payload));
1558 }
1559 catch (Exception ex)
1560 {
1561 _logger.LogError(ex,
1562 "Error in raw reply event handler for reply {ReplyType} from address {Address}",
1563 (ReplyType)reply.ReplyMessage.Type, reply.ReplyMessage.Address);
1564 }
1565 }
1566
1567 private event EventHandler<ReplyEventArgs> ReplyReceived;
1568
1572 public event EventHandler<RawReplyEventArgs> RawReplyReceived;
1573
1577 public event EventHandler<ConnectionStatusEventArgs> ConnectionStatusChanged;
1578
1582 public event EventHandler<NakReplyEventArgs> NakReplyReceived;
1583
1587 public event EventHandler<LocalStatusReportReplyEventArgs> LocalStatusReportReplyReceived;
1588
1592 public event EventHandler<InputStatusReportReplyEventArgs> InputStatusReportReplyReceived;
1593
1597 public event EventHandler<OutputStatusReportReplyEventArgs> OutputStatusReportReplyReceived;
1598
1602 public event EventHandler<ReaderStatusReportReplyEventArgs> ReaderStatusReportReplyReceived;
1603
1607 public event EventHandler<RawCardDataReplyEventArgs> RawCardDataReplyReceived;
1608
1612 [Obsolete("Use Raw Card Data for incoming card reads.")]
1613 public event EventHandler<FormattedCardDataReplyEventArgs> FormattedCardDataReplyReceived;
1614
1618 public event EventHandler<ManufacturerSpecificReplyEventArgs> ManufacturerSpecificReplyReceived;
1619
1623 public event EventHandler<ExtendedReadReplyEventArgs> ExtendedReadReplyReceived;
1624
1628 public event EventHandler<KeypadReplyEventArgs> KeypadReplyReceived;
1629
1633 private event EventHandler<BiometricReadResultsReplyEventArgs> BiometricReadResultsReplyReceived;
1634
1638 private event EventHandler<BiometricMatchReplyEventArgs> BiometricMatchReplyReceived;
1639
1643 private event EventHandler<MultiPartMessageDataReplyEventArgs> PIVDataReplyReceived;
1644
1648 private event EventHandler<MultiPartMessageDataReplyEventArgs> ExtendedIdReplyReceived;
1649
1653 private event EventHandler<MultiPartMessageDataReplyEventArgs> AuthenticationChallengeResponseReceived;
1654
1658 public class NakReplyEventArgs : EventArgs
1659 {
1666 public NakReplyEventArgs(Guid connectionId, byte address, Nak nak)
1667 {
1668 ConnectionId = connectionId;
1669 Address = address;
1670 Nak = nak;
1671 }
1672
1676 public Guid ConnectionId { get; }
1677
1681 public byte Address { get; }
1682
1686 public Nak Nak { get; }
1687 }
1688
1692 public class ConnectionStatusEventArgs : EventArgs
1693 {
1701 public ConnectionStatusEventArgs(Guid connectionId, byte address, bool isConnected,
1702 bool isSecureChannelEstablished)
1703 {
1704 ConnectionId = connectionId;
1705 Address = address;
1706 IsConnected = isConnected;
1707 IsSecureChannelEstablished = isSecureChannelEstablished;
1708 }
1709
1713 public Guid ConnectionId { get; }
1714
1718 public byte Address { get; }
1719
1723 public bool IsConnected { get; }
1724
1728 public bool IsSecureChannelEstablished { get; }
1729
1731 public override string ToString() =>
1732 $"{ConnectionId}:{Address} - Conn: {IsConnected}; Sec: {IsSecureChannelEstablished}";
1733 }
1734
1738 public class LocalStatusReportReplyEventArgs : EventArgs
1739 {
1746 public LocalStatusReportReplyEventArgs(Guid connectionId, byte address, LocalStatus localStatus)
1747 {
1748 ConnectionId = connectionId;
1749 Address = address;
1750 LocalStatus = localStatus;
1751 }
1752
1756 public Guid ConnectionId { get; }
1757
1761 public byte Address { get; }
1762
1766 public LocalStatus LocalStatus { get; }
1767 }
1768
1772 public class InputStatusReportReplyEventArgs : EventArgs
1773 {
1780 public InputStatusReportReplyEventArgs(Guid connectionId, byte address, InputStatus inputStatus)
1781 {
1782 ConnectionId = connectionId;
1783 Address = address;
1784 InputStatus = inputStatus;
1785 }
1786
1790 public Guid ConnectionId { get; }
1791
1795 public byte Address { get; }
1796
1800 public InputStatus InputStatus { get; }
1801 }
1802
1806 public class OutputStatusReportReplyEventArgs : EventArgs
1807 {
1814 public OutputStatusReportReplyEventArgs(Guid connectionId, byte address, OutputStatus outputStatus)
1815 {
1816 ConnectionId = connectionId;
1817 Address = address;
1818 OutputStatus = outputStatus;
1819 }
1820
1824 public Guid ConnectionId { get; }
1825
1829 public byte Address { get; }
1830
1835 }
1836
1840 public class ReaderStatusReportReplyEventArgs : EventArgs
1841 {
1848 public ReaderStatusReportReplyEventArgs(Guid connectionId, byte address, ReaderStatus readerStatus)
1849 {
1850 ConnectionId = connectionId;
1851 Address = address;
1852 ReaderStatus = readerStatus;
1853 }
1854
1858 public Guid ConnectionId { get; }
1859
1863 public byte Address { get; }
1864
1869 }
1870
1874 public class RawCardDataReplyEventArgs : EventArgs
1875 {
1882 public RawCardDataReplyEventArgs(Guid connectionId, byte address, RawCardData rawCardData)
1883 {
1884 ConnectionId = connectionId;
1885 Address = address;
1886 RawCardData = rawCardData;
1887 }
1888
1892 public Guid ConnectionId { get; }
1893
1897 public byte Address { get; }
1898
1902 public RawCardData RawCardData { get; }
1903 }
1904
1908 public class FormattedCardDataReplyEventArgs : EventArgs
1909 {
1916 public FormattedCardDataReplyEventArgs(Guid connectionId, byte address, FormattedCardData formattedCardData)
1917 {
1918 ConnectionId = connectionId;
1919 Address = address;
1920 FormattedCardData = formattedCardData;
1921 }
1922
1926 public Guid ConnectionId { get; }
1927
1931 public byte Address { get; }
1932
1937 }
1938
1942 public class ManufacturerSpecificReplyEventArgs : EventArgs
1943 {
1950 public ManufacturerSpecificReplyEventArgs(Guid connectionId, byte address, ManufacturerSpecific manufacturerSpecific)
1951 {
1952 ConnectionId = connectionId;
1953 Address = address;
1954 ManufacturerSpecific = manufacturerSpecific;
1955 }
1956
1960 public Guid ConnectionId { get; }
1961
1965 public byte Address { get; }
1966
1971 }
1972
1976 public class ExtendedReadReplyEventArgs : EventArgs
1977 {
1984 public ExtendedReadReplyEventArgs(Guid connectionId, byte address, ExtendedRead extendedRead)
1985 {
1986 ConnectionId = connectionId;
1987 Address = address;
1988 ExtendedRead = extendedRead;
1989 }
1990
1994 public Guid ConnectionId { get; }
1995
1999 public byte Address { get; }
2000
2005 }
2006
2010 private class MultiPartMessageDataReplyEventArgs : EventArgs
2011 {
2018 public MultiPartMessageDataReplyEventArgs(Guid connectionId, byte address, DataFragmentResponse dataFragmentResponse)
2019 {
2020 ConnectionId = connectionId;
2021 Address = address;
2022 DataFragmentResponse = dataFragmentResponse;
2023 }
2024
2028 public Guid ConnectionId { get; }
2029
2033 public byte Address { get; }
2034
2038 public DataFragmentResponse DataFragmentResponse { get; }
2039 }
2040
2044 public class KeypadReplyEventArgs : EventArgs
2045 {
2052 public KeypadReplyEventArgs(Guid connectionId, byte address, KeypadData keypadData)
2053 {
2054 ConnectionId = connectionId;
2055 Address = address;
2056 KeypadData = keypadData;
2057 }
2058
2062 public Guid ConnectionId { get; }
2063
2067 public byte Address { get; }
2068
2072 public KeypadData KeypadData { get; }
2073 }
2074
2078 private class BiometricReadResultsReplyEventArgs : EventArgs
2079 {
2086 public BiometricReadResultsReplyEventArgs(Guid connectionId, byte address,
2087 BiometricReadResult biometricReadResult)
2088 {
2089 ConnectionId = connectionId;
2090 Address = address;
2091 BiometricReadResult = biometricReadResult;
2092 }
2093
2097 public Guid ConnectionId { get; }
2098
2102 public byte Address { get; }
2103
2107 public BiometricReadResult BiometricReadResult { get; }
2108 }
2109
2113 private class BiometricMatchReplyEventArgs : EventArgs
2114 {
2121 public BiometricMatchReplyEventArgs(Guid connectionId, byte address,
2122 BiometricMatchResult biometricMatchResult)
2123 {
2124 ConnectionId = connectionId;
2125 Address = address;
2126 BiometricMatchResult = biometricMatchResult;
2127 }
2128
2132 public Guid ConnectionId { get; }
2133
2137 public byte Address { get; }
2138
2142 public BiometricMatchResult BiometricMatchResult { get; }
2143 }
2144
2149 {
2157 Nak nak)
2158 {
2159 Status = status;
2160 CurrentOffset = currentOffset;
2161 Nak = nak;
2162 }
2163
2168
2172 public int CurrentOffset { get; }
2173
2177 public Nak Nak { get; }
2178 }
2179
2183 public class FileTransferException : Exception
2184 {
2185 internal FileTransferException(string msg, FileTransferStatus status) : base(msg)
2186 {
2187 Status = status;
2188 }
2189
2194 }
2195
2199 public class RawReplyEventArgs : EventArgs
2200 {
2208 public RawReplyEventArgs(Guid connectionId, byte address, byte replyType, ReadOnlyMemory<byte> payload)
2209 {
2210 ConnectionId = connectionId;
2211 Address = address;
2212 ReplyType = replyType;
2213 Payload = payload;
2214 }
2215
2219 public Guid ConnectionId { get; }
2220
2224 public byte Address { get; }
2225
2229 public byte ReplyType { get; }
2230
2234 public ReadOnlyMemory<byte> Payload { get; }
2235 }
2236
2237 private class ReplyEventArgs : EventArgs
2238 {
2239 public ReplyTracker Reply { get; set; }
2240 }
2241 }
2242}
A connection status has been changed.
byte Address
Address assigned to the device.
Guid ConnectionId
Identify the connection for communicating to the device.
ConnectionStatusEventArgs(Guid connectionId, byte address, bool isConnected, bool isSecureChannelEstablished)
Initializes a new instance of the ConnectionStatusEventArgs class.
bool IsSecureChannelEstablished
Is the secure channel currently established.
bool IsConnected
Is the device currently connected?
byte Address
Address assigned to the device.
ExtendedReadReplyEventArgs(Guid connectionId, byte address, ExtendedRead extendedRead)
Initializes a new instance of the ExtendedReadReplyEventArgs class.
ExtendedRead ExtendedRead
An extended read reply.
Guid ConnectionId
Identify the connection for communicating to the device.
An error has occurred during a file transfer operation.
FileTransferStatus Status
The last received status from the PD that indicates what error has occurred.
Track the status of a file transfer to a PD.
Model.ReplyData.FileTransferStatus.StatusDetail Status
The last status returned from the PD.
Nak Nak
Contains Nak reply if returned.
FileTransferStatus(Model.ReplyData.FileTransferStatus.StatusDetail status, int currentOffset, Nak nak)
Initializes a new instance of the FileTransferStatus class.
int CurrentOffset
The current offset in the data of the file transfer.
FormattedCardData FormattedCardData
A formatted card data reply.
byte Address
Address assigned to the device.
FormattedCardDataReplyEventArgs(Guid connectionId, byte address, FormattedCardData formattedCardData)
Initializes a new instance of the FormattedCardDataReplyEventArgs class.
Guid ConnectionId
Identify the connection for communicating to the device.
InputStatusReportReplyEventArgs(Guid connectionId, byte address, InputStatus inputStatus)
Initializes a new instance of the LocalStatusReportReplyEventArgs class.
InputStatus InputStatus
An input status report reply.
byte Address
Address assigned to the device.
Guid ConnectionId
Identify the connection for communicating to the device.
KeypadReplyEventArgs(Guid connectionId, byte address, KeypadData keypadData)
Initializes a new instance of the KeypadReplyEventArgs class.
Guid ConnectionId
Identify the connection for communicating to the device.
byte Address
Address assigned to the device.
LocalStatus LocalStatus
A local status report reply.
Guid ConnectionId
Identify the connection for communicating to the device.
byte Address
Address assigned to the device.
LocalStatusReportReplyEventArgs(Guid connectionId, byte address, LocalStatus localStatus)
Initializes a new instance of the LocalStatusReportReplyEventArgs class.
Guid ConnectionId
Identify the connection for communicating to the device.
ManufacturerSpecificReplyEventArgs(Guid connectionId, byte address, ManufacturerSpecific manufacturerSpecific)
Initializes a new instance of the ManufacturerSpecificReplyEventArgs class.
ManufacturerSpecific ManufacturerSpecific
A manufacturer specific reply.
NakReplyEventArgs(Guid connectionId, byte address, Nak nak)
Initializes a new instance of the NakReplyEventArgs class.
Nak Nak
A negative reply that has been received.
Guid ConnectionId
Identify the connection for communicating to the device.
byte Address
Address assigned to the device.
OutputStatusReportReplyEventArgs(Guid connectionId, byte address, OutputStatus outputStatus)
Initializes a new instance of the OutputStatusReportReplyEventArgs class.
Guid ConnectionId
Identify the connection for communicating to the device.
OutputStatus OutputStatus
An output status report reply.
byte Address
Address assigned to the device.
RawCardDataReplyEventArgs(Guid connectionId, byte address, RawCardData rawCardData)
Initializes a new instance of the RawCardDataReplyEventArgs class.
Guid ConnectionId
Identify the connection for communicating to the device.
RawCardData RawCardData
A raw card data reply.
Guid ConnectionId
Identify the connection for communicating to the device.
byte ReplyType
The reply type code of the message.
byte Address
Address assigned to the device.
RawReplyEventArgs(Guid connectionId, byte address, byte replyType, ReadOnlyMemory< byte > payload)
Initializes a new instance of the RawReplyEventArgs class.
ReadOnlyMemory< byte > Payload
The raw reply payload.
Guid ConnectionId
Identify the connection for communicating to the device.
ReaderStatusReportReplyEventArgs(Guid connectionId, byte address, ReaderStatus readerStatus)
Initializes a new instance of the ReaderStatusReportReplyEventArgs class.
ReaderStatus ReaderStatus
A reader status report reply.
The OSDP control panel used to communicate to Peripheral Devices (PDs) as an Access Control Unit (ACU...
Guid StartConnection(IOsdpConnection connection, TimeSpan pollInterval, Action< TraceEntry > tracer)
Start polling on the defined connection.
async Task< ReaderStatus > ReaderStatus(Guid connectionId, byte address)
Request to get the status of all the readers of a PD.
async Task Shutdown()
Shutdown the control panel and stop all communication to PDs.
async Task< ReturnReplyData< ManufacturerSpecific > > ManufacturerSpecificCommand(Guid connectionId, byte address, Model.CommandData.ManufacturerSpecific manufacturerSpecific)
Sends a manufacture specific command to a PD.
Task< Model.ReplyData.FileTransferStatus.StatusDetail > FileTransfer(Guid connectionId, byte address, byte fileType, byte[] fileData, ushort fragmentSize, Action< FileTransferStatus > callback, CancellationToken cancellationToken=default)
Send a file to a PD.
async Task< bool > ACUReceiveSize(Guid connectionId, byte address, ushort maximumReceiveSize)
Inform the PD the maximum size that the ACU can receive.
async Task StopConnection(Guid connectionId)
Stop the bus for a specific connection.
async Task< ExtendedDeviceIdentification > ExtendedIdReport(Guid connectionId, byte address, TimeSpan timeout, CancellationToken cancellationToken=default)
Request to get an Extended ID Report from the PD.
async Task< DiscoveryResult > DiscoverDevice(IEnumerable< IOsdpConnection > connections, DiscoveryOptions options=null)
Attempts a device discovery on a given connection.
async Task< ReturnReplyData< ManufacturerSpecific > > ManufacturerSpecificCommand(Guid connectionId, byte address, Model.CommandData.ManufacturerSpecific manufacturerSpecific, ushort maximumFragmentSize, TimeSpan timeout, CancellationToken cancellationToken=default)
Sends a manufacturer specific multi-part command to a PD.
void ResetDevice(Guid connectionId, int address)
Reset communication sequence with the PD specified.
async Task< DeviceCapabilities > DeviceCapabilities(Guid connectionId, byte address)
Request to get the capabilities of the PD.
async Task< ReturnReplyData< ExtendedRead > > ExtendedWriteData(Guid connectionId, byte address, ExtendedWrite extendedWrite)
Sends an extended write (transparent mode) command to a PD to facilitate communications with an ISO 7...
ControlPanel(ILoggerFactory loggerFactory=null)
Initializes a new instance of the T:OSDP.Net.ControlPanel class.
async Task< InputStatus > InputStatus(Guid connectionId, byte address)
Request to get the status of all the inputs of a PD.
async Task< byte[]> AuthenticationChallenge(Guid connectionId, byte address, byte algorithm, byte key, byte[] challenge, ushort maximumFragmentSize, TimeSpan timeout, CancellationToken cancellationToken=default)
Instruct the PD to perform a challenge/response sequence.
async Task< byte[]> GetPIVData(Guid connectionId, byte address, GetPIVData getPIVData, TimeSpan timeout, CancellationToken cancellationToken=default)
Request to get PIV data from PD.
async Task< Model.ReplyData.CommunicationConfiguration > CommunicationConfiguration(Guid connectionId, byte address, CommunicationConfiguration communicationConfiguration)
Send a command that sets the communication configuration on a PD.
EventHandler< ManufacturerSpecificReplyEventArgs > ManufacturerSpecificReplyReceived
Occurs when manufacturer specific reply is received.
async Task< LocalStatus > LocalStatus(Guid connectionId, byte address)
Request to get the local status of a PD.
async Task< bool > ReaderTextOutput(Guid connectionId, byte address, ReaderTextOutput readerTextOutput)
Send a command that sends text to be shown on a PD.
EventHandler< OutputStatusReportReplyEventArgs > OutputStatusReportReplyReceived
Occurs when output status report reply is received.
EventHandler< ExtendedReadReplyEventArgs > ExtendedReadReplyReceived
Occurs when an extended read reply (transparent mode) is received.
Guid StartConnection(IOsdpConnection connection, TimeSpan pollInterval)
Start polling on the defined connection.
async Task< DeviceIdentification > IdReport(Guid connectionId, byte address)
Request to get an ID Report from the PD.
async Task< ReturnReplyData< OutputStatus > > OutputControl(Guid connectionId, byte address, OutputControls outputControls)
Send a command to alter the state of one or more outputs on a PD.
EventHandler< FormattedCardDataReplyEventArgs > FormattedCardDataReplyReceived
Occurs when formatted card data reply is received.
EventHandler< ConnectionStatusEventArgs > ConnectionStatusChanged
Occurs when connection status changed.
async Task SendCustomCommand(Guid connectionId, byte address, CommandData command)
Send a custom command for testing.
EventHandler< NakReplyEventArgs > NakReplyReceived
Occurs when a negative reply is received.
EventHandler< LocalStatusReportReplyEventArgs > LocalStatusReportReplyReceived
Occurs when local status report reply is received.
void EnqueueCustomCommand(Guid connectionId, byte address, CommandData command)
Enqueue a custom command to be sent without waiting for a response.
void AddDevice(Guid connectionId, byte address, bool useCrc, bool useSecureChannel, byte[] secureChannelKey=null)
Add a PD to the control panel.
async Task< bool > ReaderLedControl(Guid connectionId, byte address, ReaderLedControls readerLedControls)
Send a command to alter the LED color on a PD.
ControlPanel(ILogger< ControlPanel > logger)
Initializes a new instance of the T:OSDP.Net.ControlPanel class.
EventHandler< RawReplyEventArgs > RawReplyReceived
Occurs when any reply is received.
async Task< BiometricReadResult > ScanAndSendBiometricData(Guid connectionId, byte address, BiometricReadData biometricReadData, TimeSpan timeout, CancellationToken cancellationToken=default)
Send a request to the PD to read from a biometric scan and send back the data template.
async Task< OutputStatus > OutputStatus(Guid connectionId, byte address)
Request to get the status of all the outputs of a PD.
async Task< bool > EncryptionKeySet(Guid connectionId, byte address, EncryptionKeyConfiguration encryptionKeyConfiguration)
Send a command that sets the encryption key configuration on a PD.
async Task< bool > KeepReaderActive(Guid connectionId, byte address, ushort keepAliveTimeInMilliseconds)
Instructs the PD to maintain communication with the credential for a specified time.
Guid StartConnection(IOsdpConnection connection)
Start polling on the defined connection.
async Task< bool > ReaderBuzzerControl(Guid connectionId, byte address, ReaderBuzzerControl readerBuzzerControl)
Send a command to control the buzzer on a PD.
Guid StartConnection(IOsdpConnection connection, TimeSpan pollInterval, bool isTracing)
Start polling on the defined connection.
EventHandler< ReaderStatusReportReplyEventArgs > ReaderStatusReportReplyReceived
Occurs when reader status report reply is received.
async Task< BiometricMatchResult > ScanAndMatchBiometricTemplate(Guid connectionId, byte address, BiometricTemplateData biometricTemplateData, TimeSpan timeout, CancellationToken cancellationToken=default)
Send a request to the PD to perform a biometric scan and match it to the provided template.
void RemoveDevice(Guid connectionId, byte address)
Remove a PD from the control panel.
EventHandler< RawCardDataReplyEventArgs > RawCardDataReplyReceived
Occurs when raw card data reply is received.
bool IsOnline(Guid connectionId, byte address)
Is the PD online.
async Task< bool > AbortCurrentOperation(Guid connectionId, byte address)
Instructs the PD to abort the current operation.
EventHandler< InputStatusReportReplyEventArgs > InputStatusReportReplyReceived
Occurs when input status report reply is received.
EventHandler< KeypadReplyEventArgs > KeypadReplyReceived
Occurs when key pad data reply is received.
async Task< DeviceIdentification > IdReport(Guid connectionId, byte address, CancellationToken cancellationToken)
Request to get an ID Report from the PD.
Represents a message that was received from the wire.
byte Type
Command/reply code of the message.
byte[] Payload
Returns the raw message payload.
A base message class for sending or replying.
Definition Message.cs:12
byte Address
Address assigned to the device.
Definition Message.cs:68
static ushort CalculateMaximumMessageSize(ushort dataSize, bool isEncrypted=false, ushort cryptoLength=16)
Calculates the largest payload that fits in a message of the given total size, accounting for the mes...
Definition Message.cs:202
Command data to send a request to the PD to perform a biometric scan and match.
Command data to send a request to the PD to perform a biometric scan and return data.
Command data to set the communication configuration on a PD.
Represents the capabilities of a device within the context of OSDP (Open Supervised Device Protocol).
Command data to set the encryption key configuration on a PD.
Extended write (transparent mode) command data sent to a PD.
Command data to send a data fragment of a file to a PD.
Manufacture specific command data sent to a PD.
static ManufacturerSpecific ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Represents a fragment of data for a multipart message.
Command data to control the outputs of a PD.
Command data to control the color of LEDs.
byte Code
Gets the command or reply code byte value that represent the type of data.
Generic ACK reply.
Definition Ack.cs:11
A biometric match reply after a scan.
The actual communication configuration of the PD sent as a reply.
A multi-part message data fragment reply.
static DataFragmentResponse ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
The PD device capabilities data sent as a reply.
The PD identification data sent as a reply.
static DeviceIdentification ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
An extended read (transparent mode) reply.
static ExtendedRead ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
The PD file transfer status sent as a reply.
static FormattedCardData ParseData(ReadOnlySpan< byte > data)
Parses the data.
A input status report reply as defined in OSDP v2.2.2 specification.
static KeypadData ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
A local status report reply.
A negative reply.
Definition Nak.cs:14
A output status report reply.
static OutputStatus ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
static RawCardData ParseData(ReadOnlySpan< byte > data)
Parses the data.
A reader status report reply.
This exception will be thrown if a caller attempts to perform device discovery on a control panel tha...
Represents an error condition encountered during device discovery.
Defines a set of optional parameters the caller can pass in to the ControlPanel.DiscoverDevice(System...
Contains the results of the device discovery.
IOsdpConnection Connection
When Status is DiscoveryStatus.LookingForDeviceOnConnection this property identifies connection being...
byte Address
When Status is DiscoveryStatus.LookingForDeviceAtAddress, this property identifies the address being ...
Defines a connection for communicating via OSDP protocol.
ReplyType
Identifies the type of OSDP protocol reply (PD -> ACU) message.
Definition ReplyType.cs:7
@ ManufactureSpecific
osdp_MFGREP - manufacturer specific reply
Definition ReplyType.cs:120
@ Nak
osdp_NAK - acknowledge; error response
Definition ReplyType.cs:16
@ FileTransferStatus
osdp_FTSTAT - file transfer status
Definition ReplyType.cs:101
@ ExtendedRead
osdp_XRD - extended read reply
Definition ReplyType.cs:125
@ OutputStatusReport
osdp_OSTATR - output status report
Definition ReplyType.cs:41
CommandType
Identifies the type of OSDP protocol command (ACU -> PD) message.
Definition CommandType.cs:7
MessageDataFragmentFieldSize
Defines the field size used for the MessageDataFragment.TotalSize and MessageDataFragment....
DiscoveryStatus
Represents current state of the device discovery process.
Definition Bus.cs:22