2using System.Collections.Concurrent;
3using System.Collections.Generic;
5using System.Threading.Tasks;
6using Microsoft.Extensions.Logging;
23 private readonly ILoggerFactory _loggerFactory;
24 private readonly ILogger _logger;
25 private readonly ConcurrentQueue<PayloadData> _pendingPollReplies =
new();
27 private volatile int _connectionContextCounter;
30 private CancellationTokenSource _cancellationTokenSource;
31 private DateTime _lastValidReceivedCommand = DateTime.MinValue;
38 _deviceConfiguration = config;
39 _loggerFactory = loggerFactory;
40 _logger = loggerFactory?.CreateLogger<
Device>();
47 GC.SuppressFinalize(
this);
54 public bool IsConnected => _connectionListener?.ConnectionCount > 0 && (
55 _lastValidReceivedCommand + TimeSpan.FromSeconds(8) >= DateTime.UtcNow);
78 protected virtual void Dispose(
bool disposing)
92 _connectionListener = connectionListener ??
throw new ArgumentNullException(nameof(connectionListener));
93 _cancellationTokenSource =
new CancellationTokenSource();
94 await _connectionListener.Start(ClientListenLoop);
101 var currentContextCount = _connectionContextCounter;
102 var channel =
new PdMessageSecureChannel(
105 Address = _deviceConfiguration.
Address,
106 SecurityMode = !_deviceConfiguration.RequireSecurity
107 ? SecurityMode.Unsecured
108 : (_deviceConfiguration.SecurityKey ==
null ||
110 ? SecurityMode.InstallMode
111 : SecurityMode.FullSecurity,
112 AllowUnsecured = _deviceConfiguration.AllowUnsecured ?? [],
115 while (incomingConnection.
IsOpen && !_cancellationTokenSource.Token.IsCancellationRequested)
117 var command = await channel.ReadNextCommand(_cancellationTokenSource.Token);
119 if (command ==
null)
continue;
121 var reply = HandleCommand(command);
122 await channel.SendReply(reply);
124 if (currentContextCount != _connectionContextCounter)
126 _logger?.LogInformation(
"Interrupting existing connection due to 'force disconnect' flag");
131 catch (OperationCanceledException)
133 _logger?.LogInformation(
"Connection loop cancelled");
135 catch (Exception exception)
137 _logger?.LogError(exception, $
"Unexpected exception in polling loop");
141 await incomingConnection.
Close();
150 _cancellationTokenSource?.Cancel();
151 await (_connectionListener?.Stop() ?? Task.CompletedTask);
152 _cancellationTokenSource?.Dispose();
153 _cancellationTokenSource =
null;
154 _connectionListener =
null;
166 _lastValidReceivedCommand = DateTime.UtcNow;
169 var reply = commandType
switch
193 _ => HandleUnknownCommand(command)
196 return new OutgoingReply(command, EnsureValidReply(commandType, reply));
204 private static readonly Dictionary<CommandType, ReplyType[]> MandatoryReportReplies =
new()
209 [
CommandType.InputStatus] =
new[] { ReplyType.InputStatusReport },
210 [
CommandType.OutputStatus] =
new[] { ReplyType.OutputStatusReport },
211 [
CommandType.ReaderStatus] =
new[] { ReplyType.ReaderStatusReport },
212 [
CommandType.FileTransfer] =
new[] { ReplyType.FileTransferStatus }
220 internal PayloadData EnsureValidReply(
CommandType commandType, PayloadData reply)
222 if (!MandatoryReportReplies.TryGetValue(commandType, out var validReplies))
229 Array.IndexOf(validReplies, replyType) >= 0)
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));
242 private PayloadData HandlePoll()
244 return _pendingPollReplies.TryDequeue(out var reply) ? reply :
new Ack();
247 private PayloadData DispatchIdReport(ReadOnlySpan<byte> payload)
250 if (payload.Length > 0 && payload[0] == 0x01)
287 return HandleUnknownCommand(
CommandType.TextOutput);
297 return HandleUnknownCommand(
CommandType.BuzzerControl);
307 return HandleUnknownCommand(
CommandType.OutputControl);
316 return HandleUnknownCommand(
CommandType.DeviceCapabilities);
336 return HandleUnknownCommand(
CommandType.ManufacturerSpecific);
355 return HandleUnknownCommand(
CommandType.ExtendedWrite);
365 return HandleUnknownCommand(
CommandType.KeepActive);
393 _logger?.LogInformation(
"Received a file transfer command: {CommandPayload}", commandPayload.
ToString());
394 return HandleUnknownCommand(
CommandType.FileTransfer);
404 return HandleUnknownCommand(
CommandType.MaxReplySize);
411 if (response.Code == (
byte)
ReplyType.Ack)
413 UpdateDeviceConfig(c => c.SecurityKey = commandPayload.
KeyData);
461 if (response.Code == (
byte)
ReplyType.PdCommunicationsConfigurationReport)
464 var previousAddress = _deviceConfiguration.
Address;
465 var previousBaudRate = _connectionListener.
BaudRate;
467 if (previousAddress != config.Address)
469 UpdateDeviceConfig(c => c.Address = config.Address);
472 if (previousBaudRate != config.BaudRate || previousAddress != config.Address)
475 if (updatedEvent !=
null)
483 updatedEvent.Invoke(
this,
new DeviceComSetUpdatedEventArgs()
485 OldAddress = previousAddress,
486 OldBaudRate = previousBaudRate,
487 NewAddress = config.Address,
488 NewBaudRate = config.BaudRate,
514 return HandleUnknownCommand(
CommandType.CommunicationSet);
524 return HandleUnknownCommand(
CommandType.LEDControl);
533 return HandleUnknownCommand(
CommandType.ReaderStatus);
542 return HandleUnknownCommand(
CommandType.OutputStatus);
551 return HandleUnknownCommand(
CommandType.InputStatus);
560 return HandleUnknownCommand(
CommandType.LocalStatus);
565 _logger?.LogInformation(
"Unexpected Command: {CommandType}", (
CommandType)command.
Type);
572 _logger?.LogInformation(
"Unexpected Command: {CommandType}", commandType);
577 private void UpdateDeviceConfig(Action<DeviceConfiguration> updateAction,
bool resetConnection =
false)
579 var configCopy = _deviceConfiguration.
Clone();
580 updateAction(configCopy);
581 _deviceConfiguration = configCopy;
585 Interlocked.Add(ref _connectionContextCounter, 1);
Represents a set of configuration options to be used when initializing a new instance of the Device c...
byte Address
Address the device is assigned.
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"...
byte[] SecurityKey
Security Key if one was previously set via osdp_KeySet command or some other out-of-band means.
DeviceConfiguration(ClientIdentification identification)
Creates a new DeviceConfiguration with the required client identification.
ClientIdentification Identification
Client identification (cUID) used during secure channel establishment.
bool RequireSecurity
Indicates whether the device will require an establishment of a secure channel.
Event arguments for DeviceComSetUpdated event, which is raised whenever ACU requests the device to up...
byte NewAddress
New address value.
byte OldAddress
Old address value.
int OldBaudRate
Old baud rate.
int NewBaudRate
New baud rate.
virtual PayloadData HandleCommunicationSet(CommunicationConfiguration commandPayload)
If deriving PD class is intending to support updating the communication settings, it MUST override th...
virtual PayloadData HandleMaxReplySize(ACUReceiveSize commandPayload)
Handles the maximum ACU receive size command received from the OSDP device.
Device(DeviceConfiguration config, ILoggerFactory loggerFactory=null)
Represents a Peripheral Device (PD) that communicates over the OSDP protocol.
virtual PayloadData HandleExtendedWrite(ExtendedWrite commandPayload)
Handles the extended write (transparent mode) command received from the ACU.
virtual PayloadData HandleBiometricRead(BiometricReadData commandPayload)
Handles the biometric read command received from the OSDP device.
virtual PayloadData HandleOutputStatusReport()
Handles the output status command received from the OSDP device.
virtual PayloadData HandleInputStatusReport()
Handles the input status command received from the OSDP device.
virtual PayloadData HandleLocalStatusReport()
Handles the reader local status command received from the OSDP device.
virtual PayloadData HandleOutputControl(OutputControls commandPayload)
Handles the output controls command received from the OSDP device.
async Task StopListening()
Stops listening for OSDP messages on the device.
virtual PayloadData HandleReaderStatusReport()
Handles the reader status command received from the OSDP device.
async Task StartListening(IOsdpConnectionListener connectionListener)
Starts listening for commands from the ACU through the specified connection listener.
virtual PayloadData HandleDeviceCapabilities()
Handles the device capabilities request command received from the OSDP device.
virtual PayloadData HandlePivData(GetPIVData commandPayload)
Handles the get PIV data command received from the OSDP device.
virtual PayloadData HandleBuzzerControl(ReaderBuzzerControl commandPayload)
Handles the reader buzzer control command received from the OSDP device.
bool IsConnected
Gets a value indicating whether the device is currently connected.
virtual PayloadData HandleReaderLEDControl(ReaderLedControls commandPayload)
Handles the reader LED controls command received from the OSDP device.
virtual PayloadData HandleKeepActive(KeepReaderActive commandPayload)
Handles the keep active command received from the OSDP device.
virtual PayloadData HandleIdReport()
Handles the ID Report Request command received from the OSDP device.
virtual PayloadData HandleBiometricMatch(BiometricTemplateData commandPayload)
Handles the biometric match command received from the OSDP device.
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.
virtual PayloadData HandleAbortRequest()
Handles the abort request command received from the OSDP device.
virtual void Dispose(bool disposing)
Disposes the Device instance.
virtual PayloadData HandleManufacturerCommand(ManufacturerSpecific commandPayload)
Handles the manufacturer-specific command received from the OSDP device.
virtual PayloadData HandleKeySettings(EncryptionKeyConfiguration commandPayload)
If deriving PD class is intending to support secure connections, it MUST override this method to prov...
virtual PayloadData HandleExtendedIdReport()
Handles the Extended ID Report Request command received from the OSDP device.
EventHandler< DeviceComSetUpdatedEventArgs > DeviceComSetUpdated
Gets raised whenever osdp_ComSet command is successfully processed, and there is a change in either d...
virtual PayloadData HandleFileTransfer(FileTransferFragment commandPayload)
Handles the file transfer command received from the OSDP device.
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.
byte[] KeyData
Gets 16 bytes of key data.
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.
override string ToString()
static FileTransferFragment ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Command data to get the PIV data.
static GetPIVData ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
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.
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.
@ ExtendedPdIdReport
osdp_EXT_PDID - extended device identification report
@ Nak
osdp_NAK - acknowledge; error response
@ LocalStatusReport
osdp_LSTATR - local status report
@ PdCapabilitiesReport
osdp_PDCAP - device capabilities report
@ Ack
osdp_ACK - general acknowledge; nothing to report
CommandType
Identifies the type of OSDP protocol command (ACU -> PD) message.
@ 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
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.