OSDP.Net 5.2.1
A .NET library for implementing the Open Supervised Device Protocol (OSDP)
Loading...
Searching...
No Matches
Device.cs
1using System;
2using System.Collections.Concurrent;
3using System.Collections.Generic;
4using System.Threading;
5using System.Threading.Tasks;
6using Microsoft.Extensions.Logging;
10using OSDP.Net.Model;
15
16namespace OSDP.Net;
17
21public class Device : IDisposable
22{
23 private readonly ILoggerFactory _loggerFactory;
24 private readonly ILogger _logger;
25 private readonly ConcurrentQueue<PayloadData> _pendingPollReplies = new();
26
27 private volatile int _connectionContextCounter;
28 private DeviceConfiguration _deviceConfiguration;
29 private IOsdpConnectionListener _connectionListener;
30 private CancellationTokenSource _cancellationTokenSource;
31 private DateTime _lastValidReceivedCommand = DateTime.MinValue;
32
36 public Device(DeviceConfiguration config, ILoggerFactory loggerFactory = null)
37 {
38 _deviceConfiguration = config;
39 _loggerFactory = loggerFactory;
40 _logger = loggerFactory?.CreateLogger<Device>();
41 }
42
44 public void Dispose()
45 {
46 Dispose(true);
47 GC.SuppressFinalize(this);
48 }
49
54 public bool IsConnected => _connectionListener?.ConnectionCount > 0 && (
55 _lastValidReceivedCommand + TimeSpan.FromSeconds(8) >= DateTime.UtcNow);
56
70 public event EventHandler<DeviceComSetUpdatedEventArgs> DeviceComSetUpdated;
71
78 protected virtual void Dispose(bool disposing)
79 {
80 if (disposing)
81 {
82 var _ = StopListening();
83 }
84 }
85
90 public async Task StartListening(IOsdpConnectionListener connectionListener)
91 {
92 _connectionListener = connectionListener ?? throw new ArgumentNullException(nameof(connectionListener));
93 _cancellationTokenSource = new CancellationTokenSource();
94 await _connectionListener.Start(ClientListenLoop);
95 }
96
97 private async Task ClientListenLoop(IOsdpConnection incomingConnection)
98 {
99 try
100 {
101 var currentContextCount = _connectionContextCounter;
102 var channel = new PdMessageSecureChannel(
103 incomingConnection, _deviceConfiguration.SecurityKey, _deviceConfiguration.Identification.ToBytes(), _loggerFactory)
104 {
105 Address = _deviceConfiguration.Address,
106 SecurityMode = !_deviceConfiguration.RequireSecurity
107 ? SecurityMode.Unsecured
108 : (_deviceConfiguration.SecurityKey == null ||
109 _deviceConfiguration.SecurityKey.SequenceEqual(SecurityContext.DefaultKey))
110 ? SecurityMode.InstallMode
111 : SecurityMode.FullSecurity,
112 AllowUnsecured = _deviceConfiguration.AllowUnsecured ?? [],
113 };
114
115 while (incomingConnection.IsOpen && !_cancellationTokenSource.Token.IsCancellationRequested)
116 {
117 var command = await channel.ReadNextCommand(_cancellationTokenSource.Token);
118
119 if (command == null) continue;
120
121 var reply = HandleCommand(command);
122 await channel.SendReply(reply);
123
124 if (currentContextCount != _connectionContextCounter)
125 {
126 _logger?.LogInformation("Interrupting existing connection due to 'force disconnect' flag");
127 break;
128 }
129 }
130 }
131 catch (OperationCanceledException)
132 {
133 _logger?.LogInformation("Connection loop cancelled");
134 }
135 catch (Exception exception)
136 {
137 _logger?.LogError(exception, $"Unexpected exception in polling loop");
138 }
139 finally
140 {
141 await incomingConnection.Close();
142 }
143 }
144
148 public async Task StopListening()
149 {
150 _cancellationTokenSource?.Cancel();
151 await (_connectionListener?.Stop() ?? Task.CompletedTask);
152 _cancellationTokenSource?.Dispose();
153 _cancellationTokenSource = null;
154 _connectionListener = null;
155 }
156
161 public void EnqueuePollReply(PayloadData reply) => _pendingPollReplies.Enqueue(reply);
162
163 internal virtual OutgoingReply HandleCommand(IncomingMessage command)
164 {
165 if (command.IsDataCorrect && Enum.IsDefined(typeof(CommandType), command.Type))
166 _lastValidReceivedCommand = DateTime.UtcNow;
167
168 var commandType = (CommandType)command.Type;
169 var reply = commandType switch
170 {
171 CommandType.Poll => HandlePoll(),
172 CommandType.IdReport => DispatchIdReport(command.Payload),
185 CommandType.KeySet => _HandleKeySettings(EncryptionKeyConfiguration.ParseData(command.Payload)),
193 _ => HandleUnknownCommand(command)
194 };
195
196 return new OutgoingReply(command, EnsureValidReply(commandType, reply));
197 }
198
204 private static readonly Dictionary<CommandType, ReplyType[]> MandatoryReportReplies = new()
205 {
206 [CommandType.IdReport] = new[] { ReplyType.PdIdReport, ReplyType.ExtendedPdIdReport },
207 [CommandType.DeviceCapabilities] = new[] { ReplyType.PdCapabilitiesReport },
208 [CommandType.LocalStatus] = new[] { ReplyType.LocalStatusReport },
209 [CommandType.InputStatus] = new[] { ReplyType.InputStatusReport },
210 [CommandType.OutputStatus] = new[] { ReplyType.OutputStatusReport },
211 [CommandType.ReaderStatus] = new[] { ReplyType.ReaderStatusReport },
212 [CommandType.FileTransfer] = new[] { ReplyType.FileTransferStatus }
213 };
214
220 internal PayloadData EnsureValidReply(CommandType commandType, PayloadData reply)
221 {
222 if (!MandatoryReportReplies.TryGetValue(commandType, out var validReplies))
223 {
224 return reply;
225 }
226
227 var replyType = (ReplyType)reply.Code;
228 if (replyType == ReplyType.Nak || replyType == ReplyType.Busy ||
229 Array.IndexOf(validReplies, replyType) >= 0)
230 {
231 return reply;
232 }
233
234 _logger?.LogError(
235 "Handler for {CommandType} returned an invalid reply ({ReplyType}); substituting NAK. " +
236 "A spec-compliant PD must answer with one of [{ValidReplies}], or NAK/BUSY.",
237 commandType, replyType, string.Join(", ", validReplies));
238
239 return new Nak(ErrorCode.UnknownCommandCode);
240 }
241
242 private PayloadData HandlePoll()
243 {
244 return _pendingPollReplies.TryDequeue(out var reply) ? reply : new Ack();
245 }
246
247 private PayloadData DispatchIdReport(ReadOnlySpan<byte> payload)
248 {
249 // Check if the request is for extended ID (data byte = 0x01)
250 if (payload.Length > 0 && payload[0] == 0x01)
251 {
252 return HandleExtendedIdReport();
253 }
254
255 return HandleIdReport();
256 }
257
262 protected virtual PayloadData HandleIdReport()
263 {
264 return HandleUnknownCommand(CommandType.IdReport);
265 }
266
276 {
277 return HandleUnknownCommand(CommandType.IdReport);
278 }
279
285 protected virtual PayloadData HandleTextOutput(ReaderTextOutput commandPayload)
286 {
287 return HandleUnknownCommand(CommandType.TextOutput);
288 }
289
295 protected virtual PayloadData HandleBuzzerControl(ReaderBuzzerControl commandPayload)
296 {
297 return HandleUnknownCommand(CommandType.BuzzerControl);
298 }
299
305 protected virtual PayloadData HandleOutputControl(OutputControls commandPayload)
306 {
307 return HandleUnknownCommand(CommandType.OutputControl);
308 }
309
315 {
316 return HandleUnknownCommand(CommandType.DeviceCapabilities);
317 }
318
324 protected virtual PayloadData HandlePivData(GetPIVData commandPayload)
325 {
326 return HandleUnknownCommand(CommandType.PivData);
327 }
328
335 {
336 return HandleUnknownCommand(CommandType.ManufacturerSpecific);
337 }
338
353 protected virtual PayloadData HandleExtendedWrite(ExtendedWrite commandPayload)
354 {
355 return HandleUnknownCommand(CommandType.ExtendedWrite);
356 }
357
363 protected virtual PayloadData HandleKeepActive(KeepReaderActive commandPayload)
364 {
365 return HandleUnknownCommand(CommandType.KeepActive);
366 }
367
372 protected virtual PayloadData HandleAbortRequest()
373 {
374 return HandleUnknownCommand(CommandType.Abort);
375 }
376
391 protected virtual PayloadData HandleFileTransfer(FileTransferFragment commandPayload)
392 {
393 _logger?.LogInformation("Received a file transfer command: {CommandPayload}", commandPayload.ToString());
394 return HandleUnknownCommand(CommandType.FileTransfer);
395 }
396
402 protected virtual PayloadData HandleMaxReplySize(ACUReceiveSize commandPayload)
403 {
404 return HandleUnknownCommand(CommandType.MaxReplySize);
405 }
406
407 private PayloadData _HandleKeySettings(EncryptionKeyConfiguration commandPayload)
408 {
409 var response = HandleKeySettings(commandPayload);
410
411 if (response.Code == (byte)ReplyType.Ack)
412 {
413 UpdateDeviceConfig(c => c.SecurityKey = commandPayload.KeyData);
414 }
415
416 return response;
417 }
418
433 {
434 return HandleUnknownCommand(CommandType.KeySet);
435 }
436
443 {
444 return HandleUnknownCommand(CommandType.BioMatch);
445 }
446
452 protected virtual PayloadData HandleBiometricRead(BiometricReadData commandPayload)
453 {
454 return HandleUnknownCommand(CommandType.BioRead);
455 }
456
457 private PayloadData _HandleCommunicationSet(CommunicationConfiguration commandPayload)
458 {
459 var response = HandleCommunicationSet(commandPayload);
460
461 if (response.Code == (byte)ReplyType.PdCommunicationsConfigurationReport)
462 {
463 var config = (Model.ReplyData.CommunicationConfiguration)response;
464 var previousAddress = _deviceConfiguration.Address;
465 var previousBaudRate = _connectionListener.BaudRate;
466
467 if (previousAddress != config.Address)
468 {
469 UpdateDeviceConfig(c => c.Address = config.Address);
470 }
471
472 if (previousBaudRate != config.BaudRate || previousAddress != config.Address)
473 {
474 var updatedEvent = DeviceComSetUpdated;
475 if (updatedEvent != null)
476 {
477 // Decouple a current call stack from the event invocation, which could result
478 // in the event subscriber resetting the entire connection so that the current
479 // command has a chance to run to completion, and we don't have any deadlock
480 // situations.
481 Task.Run(() =>
482 {
483 updatedEvent.Invoke(this, new DeviceComSetUpdatedEventArgs()
484 {
485 OldAddress = previousAddress,
486 OldBaudRate = previousBaudRate,
487 NewAddress = config.Address,
488 NewBaudRate = config.BaudRate,
489 });
490 });
491 }
492 }
493 }
494
495 return response;
496 }
497
513 {
514 return HandleUnknownCommand(CommandType.CommunicationSet);
515 }
516
522 protected virtual PayloadData HandleReaderLEDControl(ReaderLedControls commandPayload)
523 {
524 return HandleUnknownCommand(CommandType.LEDControl);
525 }
526
532 {
533 return HandleUnknownCommand(CommandType.ReaderStatus);
534 }
535
541 {
542 return HandleUnknownCommand(CommandType.OutputStatus);
543 }
544
550 {
551 return HandleUnknownCommand(CommandType.InputStatus);
552 }
553
559 {
560 return HandleUnknownCommand(CommandType.LocalStatus);
561 }
562
563 private PayloadData HandleUnknownCommand(IncomingMessage command)
564 {
565 _logger?.LogInformation("Unexpected Command: {CommandType}", (CommandType)command.Type);
566
567 return new Nak(ErrorCode.UnknownCommandCode);
568 }
569
570 private PayloadData HandleUnknownCommand(CommandType commandType)
571 {
572 _logger?.LogInformation("Unexpected Command: {CommandType}", commandType);
573
574 return new Nak(ErrorCode.UnknownCommandCode);
575 }
576
577 private void UpdateDeviceConfig(Action<DeviceConfiguration> updateAction, bool resetConnection = false)
578 {
579 var configCopy = _deviceConfiguration.Clone();
580 updateAction(configCopy);
581 _deviceConfiguration = configCopy;
582
583 if (resetConnection)
584 {
585 Interlocked.Add(ref _connectionContextCounter, 1);
586 }
587 }
588
593 public class DeviceComSetUpdatedEventArgs : EventArgs
594 {
598 public byte OldAddress { get; set; }
599
603 public byte NewAddress { get; set; }
604
608 public int OldBaudRate { get; set; }
609
613 public int NewBaudRate { get; set; }
614 }
615}
616
617
622public class DeviceConfiguration : ICloneable
623{
629 {
630 Identification = identification;
631 }
632
636 public byte Address { get; set; }
637
644 public bool RequireSecurity { get; set; } = true;
645
650 public byte[] SecurityKey { get; set; } = SecurityContext.DefaultKey;
651
658 public CommandType[] AllowUnsecured { get; set; } =
659 [
660 CommandType.IdReport, CommandType.DeviceCapabilities, CommandType.CommunicationSet
661 ];
662
670
674 public DeviceConfiguration Clone() => (DeviceConfiguration)this.MemberwiseClone();
675
677 object ICloneable.Clone() => Clone();
678}
679
680
Represents a set of configuration options to be used when initializing a new instance of the Device c...
Definition Device.cs:623
byte Address
Address the device is assigned.
Definition Device.cs:636
DeviceConfiguration Clone()
Creates a new object that is a copy of the current instance.
CommandType[] AllowUnsecured
List of commands the PD will allow to be sent unsecured when a device is operating in "Full Security"...
Definition Device.cs:658
byte[] SecurityKey
Security Key if one was previously set via osdp_KeySet command or some other out-of-band means.
Definition Device.cs:650
DeviceConfiguration(ClientIdentification identification)
Creates a new DeviceConfiguration with the required client identification.
Definition Device.cs:628
ClientIdentification Identification
Client identification (cUID) used during secure channel establishment.
Definition Device.cs:669
bool RequireSecurity
Indicates whether the device will require an establishment of a secure channel.
Definition Device.cs:644
Event arguments for DeviceComSetUpdated event, which is raised whenever ACU requests the device to up...
Definition Device.cs:594
virtual PayloadData HandleCommunicationSet(CommunicationConfiguration commandPayload)
If deriving PD class is intending to support updating the communication settings, it MUST override th...
Definition Device.cs:512
virtual PayloadData HandleMaxReplySize(ACUReceiveSize commandPayload)
Handles the maximum ACU receive size command received from the OSDP device.
Definition Device.cs:402
Device(DeviceConfiguration config, ILoggerFactory loggerFactory=null)
Represents a Peripheral Device (PD) that communicates over the OSDP protocol.
Definition Device.cs:36
virtual PayloadData HandleExtendedWrite(ExtendedWrite commandPayload)
Handles the extended write (transparent mode) command received from the ACU.
Definition Device.cs:353
virtual PayloadData HandleBiometricRead(BiometricReadData commandPayload)
Handles the biometric read command received from the OSDP device.
Definition Device.cs:452
virtual PayloadData HandleOutputStatusReport()
Handles the output status command received from the OSDP device.
Definition Device.cs:540
virtual PayloadData HandleInputStatusReport()
Handles the input status command received from the OSDP device.
Definition Device.cs:549
virtual PayloadData HandleLocalStatusReport()
Handles the reader local status command received from the OSDP device.
Definition Device.cs:558
virtual PayloadData HandleOutputControl(OutputControls commandPayload)
Handles the output controls command received from the OSDP device.
Definition Device.cs:305
async Task StopListening()
Stops listening for OSDP messages on the device.
Definition Device.cs:148
virtual PayloadData HandleReaderStatusReport()
Handles the reader status command received from the OSDP device.
Definition Device.cs:531
async Task StartListening(IOsdpConnectionListener connectionListener)
Starts listening for commands from the ACU through the specified connection listener.
Definition Device.cs:90
virtual PayloadData HandleDeviceCapabilities()
Handles the device capabilities request command received from the OSDP device.
Definition Device.cs:314
virtual PayloadData HandlePivData(GetPIVData commandPayload)
Handles the get PIV data command received from the OSDP device.
Definition Device.cs:324
virtual PayloadData HandleBuzzerControl(ReaderBuzzerControl commandPayload)
Handles the reader buzzer control command received from the OSDP device.
Definition Device.cs:295
bool IsConnected
Gets a value indicating whether the device is currently connected.
Definition Device.cs:54
virtual PayloadData HandleReaderLEDControl(ReaderLedControls commandPayload)
Handles the reader LED controls command received from the OSDP device.
Definition Device.cs:522
virtual PayloadData HandleKeepActive(KeepReaderActive commandPayload)
Handles the keep active command received from the OSDP device.
Definition Device.cs:363
virtual PayloadData HandleIdReport()
Handles the ID Report Request command received from the OSDP device.
Definition Device.cs:262
virtual PayloadData HandleBiometricMatch(BiometricTemplateData commandPayload)
Handles the biometric match command received from the OSDP device.
Definition Device.cs:442
void EnqueuePollReply(PayloadData reply)
Enqueues a reply into the pending poll reply queue.
virtual PayloadData HandleTextOutput(ReaderTextOutput commandPayload)
Handles the text output command received from the OSDP device.
Definition Device.cs:285
virtual PayloadData HandleAbortRequest()
Handles the abort request command received from the OSDP device.
Definition Device.cs:372
void Dispose()
Definition Device.cs:44
virtual void Dispose(bool disposing)
Disposes the Device instance.
Definition Device.cs:78
virtual PayloadData HandleManufacturerCommand(ManufacturerSpecific commandPayload)
Handles the manufacturer-specific command received from the OSDP device.
Definition Device.cs:334
virtual PayloadData HandleKeySettings(EncryptionKeyConfiguration commandPayload)
If deriving PD class is intending to support secure connections, it MUST override this method to prov...
Definition Device.cs:432
virtual PayloadData HandleExtendedIdReport()
Handles the Extended ID Report Request command received from the OSDP device.
Definition Device.cs:275
EventHandler< DeviceComSetUpdatedEventArgs > DeviceComSetUpdated
Gets raised whenever osdp_ComSet command is successfully processed, and there is a change in either d...
Definition Device.cs:70
virtual PayloadData HandleFileTransfer(FileTransferFragment commandPayload)
Handles the file transfer command received from the OSDP device.
Definition Device.cs:391
Represents a message that was received from the wire.
byte Type
Command/reply code of the message.
bool IsDataCorrect
Gets a value indicating whether the data in the incoming message is correct.
byte[] Payload
Returns the raw message payload.
Security context used within MessageSecureChannel.
Command data to set the maximum message size the ACU can receive.
static ACUReceiveSize ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Command data to send a request to the PD to perform a biometric scan and match.
static BiometricReadData ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Command data to send a request to the PD to perform a biometric scan and return data.
static BiometricTemplateData ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Command data to set the communication configuration on a PD.
static CommunicationConfiguration ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Command data to set the encryption key configuration on a PD.
static EncryptionKeyConfiguration ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Extended write (transparent mode) command data sent to a PD.
static ExtendedWrite ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Command data to send a data fragment of a file to a PD.
static FileTransferFragment ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Command data to get the PIV data.
Definition GetPIVData.cs:13
static GetPIVData ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Definition GetPIVData.cs:76
Command data to set the keep alive time.
static KeepReaderActive ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Manufacture specific command data sent to a PD.
static ManufacturerSpecific ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Command data to control the outputs of a PD.
static OutputControls ParseData(ReadOnlySpan< byte > payloadData)
Parses the message payload bytes.
Command data to control the buzzer on a PD.
static ReaderBuzzerControl ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Command data to control the color of LEDs.
static ReaderLedControls ParseData(ReadOnlySpan< byte > payloadData)
Parses the message payload bytes.
Command data for sending text to be shown on a PD.
static ReaderTextOutput ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Base class representing a payload an OSDP message.
Definition PayloadData.cs:9
byte Code
Gets the command or reply code byte value that represent the type of data.
The actual communication configuration of the PD sent as a reply.
Defines a connection listener for OSDP Peripheral Devices (PDs) that need to accept incoming connecti...
int BaudRate
Gets the baud rate for serial connections.
Defines a connection for communicating via OSDP protocol.
Task Close()
Close the connection for communications.
bool IsOpen
Is the connection open and ready to communicate.
ReplyType
Identifies the type of OSDP protocol reply (PD -> ACU) message.
Definition ReplyType.cs:7
@ ExtendedPdIdReport
osdp_EXT_PDID - extended device identification report
Definition ReplyType.cs:81
@ Nak
osdp_NAK - acknowledge; error response
Definition ReplyType.cs:16
@ LocalStatusReport
osdp_LSTATR - local status report
Definition ReplyType.cs:31
@ PdCapabilitiesReport
osdp_PDCAP - device capabilities report
Definition ReplyType.cs:26
@ Ack
osdp_ACK - general acknowledge; nothing to report
Definition ReplyType.cs:11
CommandType
Identifies the type of OSDP protocol command (ACU -> PD) message.
Definition CommandType.cs:7
@ IdReport
osdp_ID - ID report request
@ KeepActive
osdp_KEEPACTIVE - keep reader active
@ ReaderStatus
osdp_RSTAT - reader status report request
@ LocalStatus
osdp_LSTAT - local status report request
@ InputStatus
osdp_ISTAT - input status report request
@ TextOutput
osdp_TEXT - Reader text output command
@ BuzzerControl
osdp_BUZ - Reader buzzer control command
@ BioRead
osdp_BIOREAD - scan and match biometric data
@ Abort
osdp_ABORT - abort current operation
@ CommunicationSet
osdp_COMSET - communication configuration command
@ Poll
osdp_POLL - poll request
@ BioMatch
osdp_BIOMATCH - scan and match biometric template
@ ManufacturerSpecific
osdp_MFG - manufacturer specific command
@ LEDControl
osdp_LED - Reader LED control command
@ KeySet
osdp_KEYSET - Encryption key set
@ FileTransfer
osdp_FILETRANSFER - file transfer command
@ PivData
osdp_PIVDATA - get PIV data
@ DeviceCapabilities
osdp_CAP - peripheral device capabilities request
@ ExtendedWrite
osdp_XWR - extended write data
@ OutputControl
osdp_OUT - output control command
@ OutputStatus
osdp_OSTAT - output status report request
@ MaxReplySize
osdp_ACURXSIZE - ACU receive size
ErrorCode
Error code.
Definition Nak.cs:99
Definition Bus.cs:22
Represents the Client Unique Identifier (cUID) used during an OSDP secure channel establishment.
byte[] ToBytes()
Converts the client identification to an 8-byte array for use in the secure channel protocol.