diff --git a/.gitignore b/.gitignore
index 21ea1d1a..30d7a858 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,5 @@ _UpgradeReport_Files/
# For those who use p4diff/p4merge, ignore .orig files that
# those tools seem to leave behind
*.orig
+
+.claude
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..c2d00945
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,42 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Build Commands
+- Build project: `dotnet build`
+- Build with specific configuration: `dotnet build --configuration Release`
+
+## Test Commands
+- Run all tests: `dotnet test`
+- Run a specific test: `dotnet test --filter "FullyQualifiedName=OSDP.Net.Tests.{TestClass}.{TestMethod}"`
+- Run tests with specific configuration: `dotnet test --configuration Release`
+
+## Code Style Guidelines
+- Follow default ReSharper C# coding style conventions
+- Maintain abbreviations in uppercase (ACU, LED, OSDP, PIN, PIV, UID, SCBK)
+- Follow async/await patterns for asynchronous operations
+- Use dependency injection for testability
+- Follow Arrange-Act-Assert pattern in tests
+- Implement proper exception handling with descriptive messages
+- Avoid blocking event threads
+- Use interfaces for abstraction (e.g., IOsdpConnection)
+- New commands should follow the existing command/reply model pattern
+- Place commands in appropriate namespaces (Model/CommandData or Model/ReplyData)
+
+## Project Structure
+- Core library in `/src/OSDP.Net`
+- Tests in `/src/OSDP.Net.Tests`
+- Console application in `/src/Console`
+- Sample applications in `/src/samples`
+
+## OSDP Implementation
+- **Command Implementation Status**: See `/docs/supported_commands.md` for current implementation status of OSDP v2.2 commands and replies
+- **Device (PD) Implementation**: The `Device` class in `/src/OSDP.Net/Device.cs` provides the base implementation for OSDP Peripheral Devices
+- **Command Handlers**: All command handlers are virtual methods in the Device class that can be overridden by specific device implementations
+- **Connection Architecture**:
+ - Use `TcpConnectionListener` + `TcpOsdpConnection` for PDs accepting ACU connections
+ - Use `TcpServerOsdpConnection` for ACUs accepting device connections
+ - Use `SerialPortConnectionListener` for serial-based PD implementations
+
+## Domain-Specific Terms
+- Maintain consistent terminology for domain-specific terms like APDU, INCITS, OSDP, osdpcap, rmac, Wiegand
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 00000000..0844eb19
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,31 @@
+
+
+
+ 5.0.5
+
+
+ Jonathan Horvath
+ Z-bit Systems LLC
+ Copyright Ā© $(Company) $([System.DateTime]::Now.Year)
+ OSDP.Net
+ Apache-2.0
+ https://github.com/bytedreamer/OSDP.Net
+ OSDP;access-control;security;card-reader;biometric;ACU;PD;protocol;serial;tcp;door-access;physical-security
+ See https://github.com/bytedreamer/OSDP.Net/releases for release notes.
+ https://github.com/bytedreamer/OSDP.Net.git
+ git
+ true
+ true
+
+
+ true
+ snupkg
+
+
+ true
+
+
+
+
+
+
diff --git a/README.md b/README.md
index c7c79c36..09024180 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,21 @@
-[](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md)
-
# OSDP.Net
[](https://dev.azure.com/jonathanhorvath/OSDP.Net/_build/latest?definitionId=1&branchName=develop)
[](https://www.nuget.org/packages/OSDP.Net/)
-OSDP.Net is a .NET framework implementation of the Open Supervised Device Protocol (OSDP).
+OSDP.Net is a .NET implementation of the Open Supervised Device Protocol (OSDP).
This protocol has been adopted by the Security Industry Association (SIA) to standardize access control hardware communication.
Further information can be found at [SIA OSDP Homepage](https://www.securityindustry.org/industry-standards/open-supervised-device-protocol/).
+## Prerequisites
+
+OSDP.Net supports the following .NET implementations:
+- .NET Framework 4.6.2 and later
+- NET 5.0 and later
+
## Getting Started
-The OSDP.Net library provides a Nuget package to quickly add OSDP capability to a .NET Framework or Core project.
+The OSDP.Net library provides a Nuget package to quickly add OSDP capability to a .NET project.
You can install it using the NuGet Package Console window:
```shell
@@ -40,7 +44,7 @@ Once the connection has started, add Peripheral Devices (PD).
panel.AddDevice(connectionId, address, useCrc, useSecureChannel, secureChannelKey);
```
-The following code will install a PD with an unique Secure Channel key. The OSDP standard requires that setting the secure key can only occur while communications are secure.
+The following code will install a PD with a unique Secure Channel key. The OSDP standard requires that setting the secure key can only occur while communications are secure.
```c#
panel.AddDevice(connectionId, address, useCrc, useSecureChannel); // connect using default SC key
@@ -62,10 +66,94 @@ var returnReplyData = await panel.OutputControl(connectionId, address, new Outpu
The reader number parameter found in some commands is used for devices with multiple readers attached. If the device has a single reader, a value of zero should be used.
```c#
byte defaultReaderNumber = 0;
-bool success = await ReaderBuzzerControl(connectionId, address,
+bool success = await panel.ReaderBuzzerControl(connectionId, address,
new ReaderBuzzerControl(defaultReaderNumber, ToneCode.Default, 2, 2, repeatNumber))
```
+## Common Usage Examples
+
+### Reading Card Data
+```c#
+// Register for card read events
+panel.CardRead += async (sender, eventArgs) =>
+{
+ await Task.Run(() =>
+ {
+ Console.WriteLine($"Card read from device {eventArgs.Address}");
+ if (eventArgs.CardData is RawCardData rawData)
+ {
+ Console.WriteLine($"Raw card data: {BitConverter.ToString(rawData.Data)}");
+ }
+ else if (eventArgs.CardData is FormattedCardData formattedData)
+ {
+ Console.WriteLine($"Formatted card data: {formattedData.CardNumber}");
+ }
+ });
+};
+```
+
+### Handling Device Events
+```c#
+// Monitor device status changes
+panel.InputStatusReport += async (sender, eventArgs) =>
+{
+ await Task.Run(() =>
+ {
+ Console.WriteLine($"Input status changed on device {eventArgs.Address}");
+ foreach (var input in eventArgs.InputStatuses)
+ {
+ Console.WriteLine($"Input {input.Number}: {(input.Active ? "Active" : "Inactive")}");
+ }
+ });
+};
+
+// Handle NAK responses
+panel.NakReplyReceived += async (sender, eventArgs) =>
+{
+ await Task.Run(() =>
+ {
+ Console.WriteLine($"NAK received from device {eventArgs.Address}: {eventArgs.Nak.ErrorCode}");
+ });
+};
+```
+
+### Managing Multiple Devices
+```c#
+// Add multiple devices on the same connection
+var devices = new[] { 0, 1, 2, 3 }; // Device addresses
+foreach (var address in devices)
+{
+ panel.AddDevice(connectionId, address, useCrc: true, useSecureChannel: true);
+}
+
+// Send commands to all devices
+foreach (var address in devices)
+{
+ await panel.ReaderLedControl(connectionId, address, new ReaderLedControls(new[]
+ {
+ new ReaderLedControl(0, 0, LedColor.Green, LedColor.Black,
+ 30, 30, PermanentControlCode.SetPermanentState)
+ }));
+}
+```
+
+### Error Handling
+```c#
+try
+{
+ var deviceId = await panel.IdReport(connectionId, address);
+ Console.WriteLine($"Device ID: {deviceId.VendorCode:X}-{deviceId.ModelNumber}-{deviceId.Version}");
+}
+catch (TimeoutException)
+{
+ Console.WriteLine("Device communication timeout");
+}
+catch (Exception ex)
+{
+ Console.WriteLine($"Error communicating with device: {ex.Message}");
+}
+```
+
## Custom Communication Implementations
OSDP.Net is able to plugin different methods of communications beyond what is included with the default package.
@@ -79,7 +167,7 @@ It simply requires the installation a new NuGet package. The code needs to be up
## Test Console
There is compiled version of the test console application for all the major platforms available for download.
-It has all the required assemblies included to run as a self containsed executable.
+It has all the required assemblies included to run as a self-contained executable.
The latest version of the package can be found at [https://www.z-bitco.com/downloads/OSDPTestConsole.zip](https://www.z-bitco.com/downloads/OSDPTestConsole.zip)
NOTE: First determine the COM port identifier of the 485 bus connected to the computer.
@@ -93,4 +181,4 @@ Be sure to save configuration before exiting.
## Contributing
The current goal is to properly support all the commands and replies outlined the OSDP v2.2 standard.
-The document that outlines the specific of the standard can be found on the [SIA website](https://mysia.securityindustry.org/ProductCatalog/Product.aspx?ID=16773). Contact me through my consulting company [Z-bit System, LLC](https://z-bitco.com), if interested in further collaboration with the OSDP.Net library.
+The document that outlines the specific of the standard can be found on the [SIA website](https://mysia.securityindustry.org/ProductCatalog/Product.aspx?ID=16773). Contact me through my consulting company [Z-bit System, LLC](https://z-bitco.com) if you're interested in collaborating on the OSDP.Net library.
diff --git a/api-baseline.txt b/api-baseline.txt
new file mode 100644
index 00000000..442de6fd
--- /dev/null
+++ b/api-baseline.txt
@@ -0,0 +1,140 @@
+# OSDP.Net Public API Baseline
+# Generated: 2025-08-24 12:00:00
+# Configuration: Release
+#
+# Format: [TYPE] FullTypeName
+#
+
+# Core Classes
+[CLASS] OSDP.Net.ControlPanel
+[CLASS] OSDP.Net.Device
+[CLASS] OSDP.Net.DeviceConfiguration
+[CLASS] OSDP.Net.DeviceComSetUpdatedEventArgs
+[CLASS] OSDP.Net.Packet
+
+# Interfaces
+[INTERFACE] OSDP.Net.Connections.IOsdpConnection
+[INTERFACE] OSDP.Net.Connections.IOsdpConnectionListener
+[INTERFACE] OSDP.Net.Messages.SecureChannel.IMessageSecureChannel
+
+# Connection Classes
+[CLASS] OSDP.Net.Connections.OsdpConnection
+[CLASS] OSDP.Net.Connections.OsdpConnectionListener
+[CLASS] OSDP.Net.Connections.SerialPortOsdpConnection
+[CLASS] OSDP.Net.Connections.SerialPortConnectionListener
+[CLASS] OSDP.Net.Connections.TcpClientOsdpConnection
+[CLASS] OSDP.Net.Connections.TcpServerOsdpConnection
+[CLASS] OSDP.Net.Connections.TcpConnectionListener
+
+# Exception Classes
+[CLASS] OSDP.Net.OSDPNetException
+[CLASS] OSDP.Net.NackReplyException
+[CLASS] OSDP.Net.InvalidPayloadException
+[CLASS] OSDP.Net.SecureChannelRequired
+
+# Panel Commands and Discovery
+[CLASS] OSDP.Net.PanelCommands.DeviceDiscover.DeviceDiscoveryException
+[CLASS] OSDP.Net.PanelCommands.DeviceDiscover.ControlPanelInUseException
+[CLASS] OSDP.Net.PanelCommands.DeviceDiscover.DiscoveryOptions
+[CLASS] OSDP.Net.PanelCommands.DeviceDiscover.DiscoveryResult
+
+# Event Args Classes
+[CLASS] OSDP.Net.Bus.ConnectionStatusEventArgs
+[CLASS] OSDP.Net.ControlPanel.NakReplyEventArgs
+[CLASS] OSDP.Net.ControlPanel.ConnectionStatusEventArgs
+[CLASS] OSDP.Net.ControlPanel.LocalStatusReportReplyEventArgs
+[CLASS] OSDP.Net.ControlPanel.InputStatusReportReplyEventArgs
+[CLASS] OSDP.Net.ControlPanel.OutputStatusReportReplyEventArgs
+[CLASS] OSDP.Net.ControlPanel.ReaderStatusReportReplyEventArgs
+[CLASS] OSDP.Net.ControlPanel.RawCardDataReplyEventArgs
+[CLASS] OSDP.Net.ControlPanel.FormattedCardDataReplyEventArgs
+[CLASS] OSDP.Net.ControlPanel.ManufacturerSpecificReplyEventArgs
+[CLASS] OSDP.Net.ControlPanel.KeypadReplyEventArgs
+[CLASS] OSDP.Net.ControlPanel.FileTransferStatus
+[CLASS] OSDP.Net.ControlPanel.FileTransferException
+
+# Messages and Secure Channel
+[CLASS] OSDP.Net.Messages.Message
+[CLASS] OSDP.Net.Messages.SecureChannel.MessageSecureChannel
+[CLASS] OSDP.Net.Messages.SecureChannel.ACUMessageSecureChannel
+[CLASS] OSDP.Net.Messages.SecureChannel.SecurityContext
+[CLASS] OSDP.Net.Messages.SecureChannel.SecurityBlock
+
+# Base Model Classes
+[CLASS] OSDP.Net.Model.PayloadData
+[CLASS] OSDP.Net.Model.CommandData.CommandData
+
+# Command Data Classes
+[CLASS] OSDP.Net.Model.CommandData.ACUReceiveSize
+[CLASS] OSDP.Net.Model.CommandData.BiometricReadData
+[CLASS] OSDP.Net.Model.CommandData.BiometricTemplateData
+[CLASS] OSDP.Net.Model.CommandData.CommunicationConfiguration
+[CLASS] OSDP.Net.Model.CommandData.EncryptionKeyConfiguration
+[CLASS] OSDP.Net.Model.CommandData.GetPIVData
+[CLASS] OSDP.Net.Model.CommandData.KeepReaderActive
+[CLASS] OSDP.Net.Model.CommandData.ManufacturerSpecific
+[CLASS] OSDP.Net.Model.CommandData.OutputControl
+[CLASS] OSDP.Net.Model.CommandData.OutputControls
+[CLASS] OSDP.Net.Model.CommandData.ReaderBuzzerControl
+[CLASS] OSDP.Net.Model.CommandData.ReaderLedControl
+[CLASS] OSDP.Net.Model.CommandData.ReaderLedControls
+[CLASS] OSDP.Net.Model.CommandData.ReaderTextOutput
+
+# Reply Data Classes
+[CLASS] OSDP.Net.Model.ReplyData.Ack
+[CLASS] OSDP.Net.Model.ReplyData.BiometricMatchResult
+[CLASS] OSDP.Net.Model.ReplyData.BiometricReadResult
+[CLASS] OSDP.Net.Model.ReplyData.CommunicationConfiguration
+[CLASS] OSDP.Net.Model.ReplyData.DataFragmentResponse
+[CLASS] OSDP.Net.Model.ReplyData.DeviceCapabilities
+[CLASS] OSDP.Net.Model.ReplyData.DeviceCapability
+[CLASS] OSDP.Net.Model.ReplyData.MsgSizeDeviceCap
+[CLASS] OSDP.Net.Model.ReplyData.RcvBuffSizeDeviceCap
+[CLASS] OSDP.Net.Model.ReplyData.LargestCombMsgSizeDeviceCap
+[CLASS] OSDP.Net.Model.ReplyData.CommSecurityDeviceCap
+[CLASS] OSDP.Net.Model.ReplyData.DeviceIdentification
+[CLASS] OSDP.Net.Model.ReplyData.FileTransferStatus
+[CLASS] OSDP.Net.Model.ReplyData.FormattedCardData
+[CLASS] OSDP.Net.Model.ReplyData.InputStatus
+[CLASS] OSDP.Net.Model.ReplyData.KeypadData
+[CLASS] OSDP.Net.Model.ReplyData.LocalStatus
+[CLASS] OSDP.Net.Model.ReplyData.ManufacturerSpecific
+[CLASS] OSDP.Net.Model.ReplyData.Nak
+[CLASS] OSDP.Net.Model.ReplyData.OutputStatus
+[CLASS] OSDP.Net.Model.ReplyData.RawCardData
+[CLASS] OSDP.Net.Model.ReplyData.ReaderStatus
+[CLASS] OSDP.Net.Model.ReplyData.ReturnReplyData
+
+# Utility Classes
+[CLASS] OSDP.Net.Utilities.BinaryExtensions
+[CLASS] OSDP.Net.Utilities.BinaryUtils
+[CLASS] OSDP.Net.Utilities.Pollyfill
+
+# Tracing Classes
+[CLASS] OSDP.Net.Tracing.OSDPCapEntry
+[CLASS] OSDP.Net.Tracing.PacketDecoding
+
+# Enums
+[ENUM] OSDP.Net.Messages.CommandType
+[ENUM] OSDP.Net.Messages.ReplyType
+[ENUM] OSDP.Net.Messages.MessageType
+[ENUM] OSDP.Net.Messages.SecureChannel.SecurityBlockType
+[ENUM] OSDP.Net.Model.CommandData.BiometricFormat
+[ENUM] OSDP.Net.Model.CommandData.BiometricType
+[ENUM] OSDP.Net.Model.CommandData.GetPIVData.ObjectId
+[ENUM] OSDP.Net.Model.CommandData.OutputControlCode
+[ENUM] OSDP.Net.Model.CommandData.ReaderBuzzerControl.ToneCode
+[ENUM] OSDP.Net.Model.CommandData.ReaderLedControl.TemporaryReaderControlCode
+[ENUM] OSDP.Net.Model.CommandData.ReaderLedControl.PermanentReaderControlCode
+[ENUM] OSDP.Net.Model.CommandData.ReaderLedControl.LedColor
+[ENUM] OSDP.Net.Model.CommandData.ReaderTextOutput.TextCommand
+[ENUM] OSDP.Net.Model.CommandData.SecureChannelConfiguration.KeyType
+[ENUM] OSDP.Net.Model.ReplyData.BiometricStatus
+[ENUM] OSDP.Net.Model.ReplyData.CapabilityFunction
+[ENUM] OSDP.Net.Model.ReplyData.FileTransferStatus.StatusDetail
+[ENUM] OSDP.Net.Model.ReplyData.FormattedCardData.ReadDirection
+[ENUM] OSDP.Net.Model.ReplyData.Nak.ErrorCode
+[ENUM] OSDP.Net.Model.ReplyData.RawCardData.FormatCode
+[ENUM] OSDP.Net.Model.ReplyData.ReaderTamperStatus
+[ENUM] OSDP.Net.PanelCommands.DeviceDiscover.DiscoveryStatus
+[ENUM] OSDP.Net.Tracing.TraceDirection
\ No newline at end of file
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index c4c9dd71..37507164 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -14,18 +14,23 @@ variables:
solution: '**/*.sln'
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'
- major: 5
- minor: 0
- patch: 5
- AssemblyVersion: $(major).$(minor).$(patch)
- NugetVersion: $(major).$(minor).$(patch)-beta
jobs:
- job: build
pool:
vmImage: 'windows-latest'
steps:
+ - checkout: self
+ fetchDepth: 0
- template: ci/build.yml
+ - task: PowerShell@2
+ displayName: 'Set GitVersion variables for package job'
+ inputs:
+ targetType: 'inline'
+ script: |
+ Write-Host "##vso[task.setvariable variable=GitVersion.SemVer;isOutput=true]$(GITVERSION_SEMVER)"
+ Write-Host "##vso[task.setvariable variable=GitVersion.MajorMinorPatch;isOutput=true]$(GITVERSION_MAJORMINORPATCH)"
+ name: GitVersionOutput
- job: package
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
@@ -33,5 +38,10 @@ jobs:
vmImage: 'windows-latest'
dependsOn:
build
+ variables:
+ GitVersionSemVer: $[ dependencies.build.outputs['GitVersionOutput.GitVersion.SemVer'] ]
+ GitVersionAssembly: $[ dependencies.build.outputs['GitVersionOutput.GitVersion.MajorMinorPatch'] ]
steps:
+ - checkout: self
+ fetchDepth: 0
- template: ci/package.yml
diff --git a/ci/GitVersion.yml b/ci/GitVersion.yml
new file mode 100644
index 00000000..a374f41a
--- /dev/null
+++ b/ci/GitVersion.yml
@@ -0,0 +1,19 @@
+workflow: 'GitHubFlow/v1'
+branches:
+ main:
+ mode: ContinuousDeployment
+ label: 'beta'
+ increment: Patch
+ track-merge-target: false
+ tracks-release-branches: false
+ regex: ^master$|^main$
+ is-release-branch: true
+ is-main-branch: true
+ develop:
+ label: 'beta'
+ increment: None
+ regex: ^dev(elop)?(ment)?$
+ source-branches:
+ - main
+ignore:
+ sha: []
diff --git a/ci/build.yml b/ci/build.yml
index 906d8d3f..8693125e 100644
--- a/ci/build.yml
+++ b/ci/build.yml
@@ -5,6 +5,17 @@ steps:
packageType: 'sdk'
version: '8.x'
+ - task: gitversion-setup@4.1.0
+ displayName: 'Install GitVersion'
+ inputs:
+ versionSpec: '6.3.x'
+
+ - task: gitversion-execute@4.1.0
+ displayName: 'Execute GitVersion'
+ inputs:
+ useConfigFile: true
+ configFilePath: 'ci/GitVersion.yml'
+
- task: DotNetCoreCLI@2
displayName: 'dotnet build'
inputs:
@@ -12,7 +23,7 @@ steps:
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration)'
versioningScheme: 'byEnvVar'
- versionEnvVar: 'AssemblyVersion'
+ versionEnvVar: 'GitVersion.MajorMinorPatch'
- task: DotNetCoreCLI@2
displayName: 'dotnet test'
@@ -35,4 +46,35 @@ steps:
- task: PublishPipelineArtifact@1
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)'
- artifactName: CodeAnalysisLogs
\ No newline at end of file
+ artifactName: CodeAnalysisLogs
+
+ - task: PowerShell@2
+ displayName: 'Check inspection results and fail if needed'
+ inputs:
+ targetType: 'inline'
+ script: |
+ $sarifPath = "$(Build.ArtifactStagingDirectory)/Resharper.sarif"
+ $sarif = Get-Content $sarifPath | ConvertFrom-Json
+
+ $errorCount = 0
+ $warningCount = 0
+
+ foreach ($run in $sarif.runs) {
+ if ($run.results) {
+ foreach ($result in $run.results) {
+ if ($result.level -eq "error") {
+ $errorCount++
+ } elseif ($result.level -eq "warning") {
+ $warningCount++
+ }
+ }
+ }
+ }
+
+ Write-Host "Found $errorCount errors and $warningCount warnings"
+
+ # Fail on both errors and warnings
+ if ($errorCount -gt 0 -or $warningCount -gt 0) {
+ Write-Host "##vso[task.logissue type=error]Code inspection found $errorCount errors and $warningCount warnings"
+ exit 1
+ }
diff --git a/ci/package.yml b/ci/package.yml
index 3fe5aad3..2d07471d 100644
--- a/ci/package.yml
+++ b/ci/package.yml
@@ -3,28 +3,24 @@ steps:
displayName: 'Install .NET 8 SDK'
inputs:
packageType: 'sdk'
- version: '8.x'
-
+ version: '8.x'
+
- task: DotNetCoreCLI@2
displayName: 'dotnet pack'
inputs:
command: 'pack'
- arguments: '--configuration $(buildConfiguration)'
+ arguments: '--configuration $(buildConfiguration) /p:PackageVersion=$(GitVersionNuGet)'
packagesToPack: 'src/OSDP.Net/OSDP.Net.csproj'
- versioningScheme: 'byEnvVar'
- versionEnvVar: 'NugetVersion'
- task: DotNetCoreCLI@2
- displayName: 'dotnet publish for osx-x64'
+ displayName: 'dotnet publish for osx-arm64'
inputs:
command: 'publish'
publishWebProjects: false
projects: 'src/Console/Console.csproj'
- arguments: '-r osx-x64 --configuration $(BuildConfiguration) /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true --self-contained true --output $(Build.ArtifactStagingDirectory)/TestConsole/osx-x64'
+ arguments: '-r osx-arm64 --configuration $(BuildConfiguration) /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true --self-contained true --output $(Build.ArtifactStagingDirectory)/TestConsole/osx-arm64 /p:Version=$(GitVersionAssembly)'
zipAfterPublish: false
modifyOutputPath: false
- versioningScheme: 'byEnvVar'
- versionEnvVar: 'AssemblyVersion'
- task: DotNetCoreCLI@2
displayName: 'dotnet publish for win-x64'
@@ -34,9 +30,7 @@ steps:
zipAfterPublish: false
modifyOutputPath: false
projects: 'src/Console/Console.csproj'
- arguments: '-r win-x64 --configuration $(BuildConfiguration) /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true --self-contained true --output $(Build.ArtifactStagingDirectory)/TestConsole/win-x64'
- versioningScheme: 'byEnvVar'
- versionEnvVar: 'AssemblyVersion'
+ arguments: '-r win-x64 --configuration $(BuildConfiguration) /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true --self-contained true --output $(Build.ArtifactStagingDirectory)/TestConsole/win-x64 /p:Version=$(GitVersionAssembly)'
- task: DotNetCoreCLI@2
displayName: 'dotnet publish for linux-x64'
@@ -46,9 +40,7 @@ steps:
zipAfterPublish: false
modifyOutputPath: false
projects: 'src/Console/Console.csproj'
- arguments: '-r linux-x64 --configuration $(BuildConfiguration) /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true --self-contained true --output $(Build.ArtifactStagingDirectory)/TestConsole/linux-x64'
- versioningScheme: 'byEnvVar'
- versionEnvVar: 'AssemblyVersion'
+ arguments: '-r linux-x64 --configuration $(BuildConfiguration) /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true --self-contained true --output $(Build.ArtifactStagingDirectory)/TestConsole/linux-x64 /p:Version=$(GitVersionAssembly)'
- task: DotNetCoreCLI@2
displayName: 'dotnet publish for linux-arm64'
@@ -58,9 +50,7 @@ steps:
zipAfterPublish: false
modifyOutputPath: false
projects: 'src/Console/Console.csproj'
- arguments: '-r linux-arm64 --configuration $(BuildConfiguration) --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:IncludeAllContentForSelfExtract=true --output $(Build.ArtifactStagingDirectory)/TestConsole/linux-arm64'
- versioningScheme: 'byEnvVar'
- versionEnvVar: 'AssemblyVersion'
+ arguments: '-r linux-arm64 --configuration $(BuildConfiguration) --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:IncludeAllContentForSelfExtract=true --output $(Build.ArtifactStagingDirectory)/TestConsole/linux-arm64 /p:Version=$(GitVersionAssembly)'
- task: ArchiveFiles@2
inputs:
diff --git a/ci/release.ps1 b/ci/release.ps1
new file mode 100644
index 00000000..43db9148
--- /dev/null
+++ b/ci/release.ps1
@@ -0,0 +1,197 @@
+#!/usr/bin/env pwsh
+<#
+.SYNOPSIS
+ Release script for OSDP.Net project
+.DESCRIPTION
+ This script automates the release process by merging develop into master and triggering CI/CD pipeline
+.PARAMETER DryRun
+ Perform a dry run without making actual changes
+#>
+
+param(
+ [switch]$DryRun = $false
+)
+
+# Color functions for better output
+function Write-ColorOutput($ForegroundColor) {
+ # Store the current color
+ $fc = $host.UI.RawUI.ForegroundColor
+ # Set the new color
+ $host.UI.RawUI.ForegroundColor = $ForegroundColor
+
+ # Output
+ if ($args) {
+ Write-Output $args
+ } else {
+ $input | Write-Output
+ }
+
+ # Restore the original color
+ $host.UI.RawUI.ForegroundColor = $fc
+}
+
+function Write-Info($message) { Write-ColorOutput Cyan $message }
+function Write-Success($message) { Write-ColorOutput Green $message }
+function Write-Warning($message) { Write-ColorOutput Yellow $message }
+function Write-Error($message) { Write-ColorOutput Red $message }
+
+Write-Info "=== OSDP.Net Release Script ==="
+Write-Info ""
+
+if ($DryRun) {
+ Write-Warning "DRY RUN MODE - No changes will be made"
+ Write-Info ""
+}
+
+# Check if we're in a git repository
+if (-not (Test-Path ".git")) {
+ Write-Error "Error: Not in a git repository"
+ exit 1
+}
+
+# Check for uncommitted changes
+$status = git status --porcelain
+if ($status) {
+ Write-Error "Error: You have uncommitted changes. Please commit or stash them first."
+ Write-Info "Uncommitted changes:"
+ git status --short
+ exit 1
+}
+
+# Get current branch
+$currentBranch = git rev-parse --abbrev-ref HEAD
+Write-Info "Current branch: $currentBranch"
+
+# Ensure we're on develop branch
+if ($currentBranch -ne "develop") {
+ Write-Error "Error: You must be on the 'develop' branch to create a release."
+ Write-Info "Current branch: $currentBranch"
+ Write-Info "Please checkout develop branch: git checkout develop"
+ exit 1
+}
+
+# Fetch latest changes
+Write-Info "Fetching latest changes from remote..."
+if (-not $DryRun) {
+ git fetch origin
+}
+
+# Check if develop is ahead of master
+$developCommits = git rev-list --count origin/master..develop 2>$null
+if (-not $developCommits -or $developCommits -eq "0") {
+ Write-Warning "Warning: develop branch is not ahead of master. No changes to release."
+ $continue = Read-Host "Continue anyway? (y/N)"
+ if ($continue -ne "y" -and $continue -ne "Y") {
+ Write-Info "Release cancelled."
+ exit 0
+ }
+} else {
+ Write-Success "Found $developCommits commit(s) to release"
+}
+
+# Show what will be released
+Write-Info ""
+Write-Info "Changes to be released:"
+Write-Info "======================="
+git log --oneline origin/master..develop
+
+Write-Info ""
+Write-Info "The release process will:"
+Write-Info "1. Checkout master branch"
+Write-Info "2. Merge develop into master"
+Write-Info "3. Push master to trigger CI/CD pipeline"
+Write-Info "4. Return to develop branch"
+Write-Info ""
+Write-Info "The CI pipeline will automatically:"
+Write-Info "- Calculate version using GitVersion"
+Write-Info "- Run tests"
+Write-Info "- Create NuGet packages"
+Write-Info "- Create GitHub release"
+Write-Info ""
+
+if (-not $DryRun) {
+ $confirm = Read-Host "Proceed with release? (y/N)"
+ if ($confirm -ne "y" -and $confirm -ne "Y") {
+ Write-Info "Release cancelled."
+ exit 0
+ }
+}
+
+Write-Info ""
+Write-Info "Starting release process..."
+
+# Checkout master branch
+Write-Info "Switching to master branch..."
+if (-not $DryRun) {
+ $result = git checkout master 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "Error: Failed to checkout master branch"
+ Write-Error $result
+ exit 1
+ }
+}
+
+# Pull latest master
+Write-Info "Pulling latest master..."
+if (-not $DryRun) {
+ $result = git pull origin master 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "Error: Failed to pull latest master"
+ Write-Error $result
+ exit 1
+ }
+}
+
+# Merge develop into master
+Write-Info "Merging develop into master..."
+if (-not $DryRun) {
+ $result = git merge develop --no-ff -m "Release: Merge develop into master" 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "Error: Failed to merge develop into master"
+ Write-Error $result
+ Write-Info "You may need to resolve conflicts manually"
+ exit 1
+ }
+}
+
+# Push master
+Write-Info "Pushing master to trigger CI/CD pipeline..."
+if (-not $DryRun) {
+ $result = git push origin master 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "Error: Failed to push master"
+ Write-Error $result
+ exit 1
+ }
+}
+
+# Return to develop branch
+Write-Info "Returning to develop branch..."
+if (-not $DryRun) {
+ $result = git checkout develop 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "Error: Failed to return to develop branch"
+ Write-Error $result
+ exit 1
+ }
+}
+
+Write-Info ""
+if ($DryRun) {
+ Write-Success "Dry run completed successfully!"
+ Write-Info "Run without -DryRun flag to perform actual release."
+} else {
+ Write-Success "Release process completed successfully!"
+ Write-Info ""
+ Write-Info "The CI pipeline will automatically:"
+ Write-Info "1. Run tests"
+ Write-Info "2. Calculate version using GitVersion"
+ Write-Info "3. Create NuGet packages"
+ Write-Info "4. Publish to NuGet (if configured)"
+ Write-Info "5. Create GitHub release"
+ Write-Info ""
+ Write-Info "Monitor the pipeline at: https://dev.azure.com/your-org/your-project/_build"
+}
+
+Write-Info ""
+Write-Info "Release script completed."
\ No newline at end of file
diff --git a/docs/api-checklist.md b/docs/api-checklist.md
new file mode 100644
index 00000000..53f01c11
--- /dev/null
+++ b/docs/api-checklist.md
@@ -0,0 +1,102 @@
+# OSDP.Net Public API Checklist
+
+This checklist ensures the correct classes and methods remain publicly accessible for NuGet consumers.
+
+## ā
Required Public Classes
+
+### Core API
+- [ ] `ControlPanel` - Main ACU class
+- [ ] `Device` - Main PD base class
+- [ ] `DeviceConfiguration` - Device configuration
+- [ ] `DeviceComSetUpdatedEventArgs` - Event arguments
+
+### Connection Types
+- [ ] `IOsdpConnection` - Connection interface
+- [ ] `IOsdpConnectionListener` - Connection listener interface
+- [ ] `TcpClientOsdpConnection` - TCP client connection
+- [ ] `TcpServerOsdpConnection` - TCP server connection
+- [ ] `SerialPortOsdpConnection` - Serial port connection
+- [ ] `TcpConnectionListener` - TCP connection listener
+- [ ] `SerialPortConnectionListener` - Serial connection listener
+
+### Exception Types
+- [ ] `OSDPNetException` - Base exception
+- [ ] `NackReplyException` - NACK reply exception
+- [ ] `InvalidPayloadException` - Invalid payload exception
+- [ ] `SecureChannelRequired` - Security requirement exception
+
+### Enums
+- [ ] `CommandType` - OSDP command types
+- [ ] `ReplyType` - OSDP reply types
+- [ ] `MessageType` - Message type enum
+- [ ] `BiometricFormat` - Biometric data formats
+- [ ] `BiometricType` - Biometric types
+- [ ] `CapabilityFunction` - Device capability functions
+- [ ] `OutputControlCode` - Output control codes
+- [ ] All LED, buzzer, and control enums
+
+### Command Data Models
+- [ ] `CommandData` - Base command class
+- [ ] All classes in `OSDP.Net.Model.CommandData` namespace
+- [ ] `OutputControls`, `ReaderLedControls`, `ReaderBuzzerControl`, etc.
+
+### Reply Data Models
+- [ ] `PayloadData` - Base payload class
+- [ ] All classes in `OSDP.Net.Model.ReplyData` namespace
+- [ ] `DeviceIdentification`, `DeviceCapabilities`, `Ack`, `Nak`, etc.
+
+### Discovery System
+- [ ] `DiscoveryOptions` - Discovery configuration
+- [ ] `DiscoveryResult` - Discovery results
+- [ ] `DiscoveryStatus` - Discovery status enum
+- [ ] Related exception types
+
+### Utilities & Extensions
+- [ ] `BinaryExtensions` - Binary utility methods
+- [ ] `SecurityContext` - Security utilities
+
+## ā Internal Implementation (Should NOT be public)
+
+- [ ] `DeviceProxy` - Internal device proxy
+- [ ] `Bus` - Internal message bus
+- [ ] `IncomingMessage` - Internal message handling
+- [ ] `OutgoingMessage` - Internal message handling
+- [ ] `ReplyTracker` - Internal reply tracking
+- [ ] `MessageSpy` - Internal message tracing
+
+## š Validation Commands
+
+### Build Test
+```bash
+dotnet build src/OSDP.Net/OSDP.Net.csproj --verbosity quiet
+```
+
+### API Validation
+```powershell
+# Generate current API baseline
+./scripts/generate-api-baseline.ps1
+
+# Check current API count and validate
+./scripts/validate-api.ps1
+```
+
+### Manual Inspection
+```csharp
+// Test key public APIs are accessible
+var controlPanel = new OSDP.Net.ControlPanel();
+var config = new OSDP.Net.DeviceConfiguration();
+var connection = new OSDP.Net.Connections.TcpClientOsdpConnection(IPAddress.Any, 4000);
+```
+
+## š Review Process
+
+1. **Pre-Release**: Run validation scripts
+2. **API Changes**: Document in release notes
+3. **Breaking Changes**: Increment major version
+4. **New APIs**: Add to this checklist
+
+## š References
+
+- [API Usage Guide](api-usage-guide.md)
+- [Supported Commands](../docs/supported_commands.md)
+- [Project Instructions](../CLAUDE.md)
\ No newline at end of file
diff --git a/docs/api-usage-guide.md b/docs/api-usage-guide.md
new file mode 100644
index 00000000..9798a173
--- /dev/null
+++ b/docs/api-usage-guide.md
@@ -0,0 +1,376 @@
+# OSDP.Net API Usage Guide
+
+This guide provides examples and best practices for using the OSDP.Net library to build Access Control Units (ACU) and Peripheral Devices (PD).
+
+## Table of Contents
+
+1. [Getting Started](#getting-started)
+2. [Building an Access Control Unit (ACU)](#building-an-access-control-unit-acu)
+3. [Building a Peripheral Device (PD)](#building-a-peripheral-device-pd)
+4. [Connection Types](#connection-types)
+5. [Security Configuration](#security-configuration)
+6. [Command and Reply Handling](#command-and-reply-handling)
+7. [Error Handling](#error-handling)
+8. [Logging and Tracing](#logging-and-tracing)
+
+## Getting Started
+
+Install the NuGet package:
+
+```bash
+dotnet add package OSDP.Net
+```
+
+Basic using statements:
+
+```csharp
+using OSDP.Net;
+using OSDP.Net.Connections;
+using OSDP.Net.Model.CommandData;
+using OSDP.Net.Model.ReplyData;
+using Microsoft.Extensions.Logging;
+```
+
+## Building an Access Control Unit (ACU)
+
+An ACU manages and communicates with multiple peripheral devices.
+
+### Basic ACU Setup
+
+```csharp
+// Create a control panel (ACU)
+var controlPanel = new ControlPanel();
+
+// Set up TCP connection to a device
+var connection = new TcpClientOsdpConnection(IPAddress.Parse("192.168.1.100"), 4000);
+var connectionId = controlPanel.StartConnection(connection);
+
+// Add a device to the connection
+controlPanel.AddDevice(connectionId, 0, true, true); // address=0, useSecureChannel=true, useCrc=true
+```
+
+### Sending Commands
+
+```csharp
+// Send an LED control command
+var ledCommand = new ReaderLedControls
+{
+ LedControls = new[]
+ {
+ new ReaderLedControl
+ {
+ LedNumber = 0,
+ TemporaryReaderControlCode = TemporaryReaderControlCode.SetTemporaryState,
+ TemporaryOnTime = 5,
+ TemporaryOffTime = 5,
+ PermanentReaderControlCode = PermanentReaderControlCode.SetPermanentState,
+ PermanentOnTime = 0,
+ PermanentOffTime = 0,
+ LedColor = LedColor.Red
+ }
+ }
+};
+
+await controlPanel.ReaderLedControl(connectionId, 0, ledCommand);
+
+// Send a buzzer control command
+var buzzerCommand = new ReaderBuzzerControl
+{
+ ToneCode = ToneCode.Default,
+ OnTime = 3,
+ OffTime = 1,
+ RepeatCount = 2
+};
+
+await controlPanel.ReaderBuzzerControl(connectionId, 0, buzzerCommand);
+```
+
+### Reading Device Information
+
+```csharp
+// Get device identification
+var deviceId = await controlPanel.IdReport(connectionId, 0);
+Console.WriteLine($"Device: {deviceId.VendorCode}, Model: {deviceId.ModelNumber}");
+
+// Get device capabilities
+var capabilities = await controlPanel.DeviceCapabilities(connectionId, 0);
+foreach (var capability in capabilities.Capabilities)
+{
+ Console.WriteLine($"Function: {capability.Function}, Compliance: {capability.Compliance}");
+}
+
+// Check device status
+var isOnline = controlPanel.IsOnline(connectionId, 0);
+Console.WriteLine($"Device online: {isOnline}");
+```
+
+## Building a Peripheral Device (PD)
+
+A PD responds to commands from an ACU and can report events.
+
+### Basic PD Setup
+
+```csharp
+public class MyDevice : Device
+{
+ public MyDevice() : base(new DeviceConfiguration
+ {
+ Address = 0,
+ RequireSecurity = true,
+ SecurityKey = SecurityContext.DefaultKey
+ })
+ {
+ }
+
+ // Override command handlers as needed
+ protected override PayloadData HandleIdReport()
+ {
+ return new DeviceIdentification
+ {
+ VendorCode = new byte[] { 0x12, 0x34, 0x56 },
+ ModelNumber = 1,
+ Version = 1,
+ SerialNumber = 12345,
+ FirmwareMajor = 1,
+ FirmwareMinor = 0,
+ FirmwareBuild = 1
+ };
+ }
+
+ protected override PayloadData HandleDeviceCapabilities()
+ {
+ return new Model.ReplyData.DeviceCapabilities
+ {
+ Capabilities = new[]
+ {
+ new DeviceCapability
+ {
+ Function = CapabilityFunction.ContactStatusMonitoring,
+ Compliance = 1,
+ NumberOf = 4
+ }
+ }
+ };
+ }
+}
+
+// Start the device
+var device = new MyDevice();
+var listener = new TcpConnectionListener(IPAddress.Any, 4000);
+device.StartListening(listener);
+```
+
+### Handling Commands
+
+```csharp
+public class MyDevice : Device
+{
+ protected override PayloadData HandleOutputControl(OutputControls commandPayload)
+ {
+ foreach (var control in commandPayload.Controls)
+ {
+ Console.WriteLine($"Setting output {control.OutputNumber} to {control.ControlCode}");
+ // Implement your output control logic here
+ }
+ return new Ack();
+ }
+
+ protected override PayloadData HandleReaderLEDControl(ReaderLedControls commandPayload)
+ {
+ foreach (var led in commandPayload.LedControls)
+ {
+ Console.WriteLine($"Setting LED {led.LedNumber} to {led.LedColor}");
+ // Implement your LED control logic here
+ }
+ return new Ack();
+ }
+}
+```
+
+## Connection Types
+
+### TCP Connections
+
+```csharp
+// TCP Client (ACU connecting to PD)
+var tcpClient = new TcpClientOsdpConnection(IPAddress.Parse("192.168.1.100"), 4000);
+
+// TCP Server (ACU accepting connections from PDs)
+var tcpServer = new TcpServerOsdpConnection(IPAddress.Any, 4000);
+
+// TCP Listener (PD accepting connections from ACU)
+var tcpListener = new TcpConnectionListener(IPAddress.Any, 4000);
+```
+
+### Serial Connections
+
+```csharp
+// Serial connection
+var serialConnection = new SerialPortOsdpConnection("COM1", 9600);
+
+// Serial listener
+var serialListener = new SerialPortConnectionListener("COM1", 9600);
+```
+
+## Security Configuration
+
+### Setting Up Secure Communication
+
+```csharp
+// Device configuration with security
+var deviceConfig = new DeviceConfiguration
+{
+ Address = 0,
+ RequireSecurity = true,
+ SecurityKey = new byte[] { 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
+ 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F }
+};
+
+// Allow certain commands without security (if needed)
+deviceConfig.AllowUnsecured = new[] { CommandType.Poll, CommandType.IdReport };
+```
+
+### Updating Security Keys
+
+```csharp
+// ACU can update device security key
+var newKey = new byte[16]; // Your new 16-byte key
+new Random().NextBytes(newKey);
+
+var keyConfig = new EncryptionKeyConfiguration
+{
+ KeyData = newKey
+};
+
+await controlPanel.EncryptionKeySet(connectionId, deviceAddress, keyConfig);
+```
+
+## Command and Reply Handling
+
+### Sending Multiple Commands
+
+```csharp
+// Send multiple commands in sequence
+var tasks = new List
+{
+ controlPanel.ReaderBuzzerControl(connectionId, 0, buzzerCommand),
+ controlPanel.ReaderLedControl(connectionId, 0, ledCommand),
+ controlPanel.OutputControl(connectionId, 0, outputCommand)
+};
+
+await Task.WhenAll(tasks);
+```
+
+### Handling Events
+
+```csharp
+// Subscribe to device events
+controlPanel.ConnectionStatusChanged += (sender, args) =>
+{
+ Console.WriteLine($"Connection {args.ConnectionId} status: {args.IsConnected}");
+};
+
+controlPanel.ReplyReceived += (sender, args) =>
+{
+ Console.WriteLine($"Reply from device {args.Address}: {args.Reply.GetType().Name}");
+};
+```
+
+## Error Handling
+
+### Exception Handling
+
+```csharp
+try
+{
+ var result = await controlPanel.IdReport(connectionId, 0);
+}
+catch (NackReplyException ex)
+{
+ Console.WriteLine($"Device returned NACK: {ex.ErrorCode}");
+}
+catch (TimeoutException)
+{
+ Console.WriteLine("Command timed out");
+}
+catch (InvalidPayloadException ex)
+{
+ Console.WriteLine($"Invalid payload: {ex.Message}");
+}
+```
+
+### Checking Connection Status
+
+```csharp
+if (!controlPanel.IsOnline(connectionId, deviceAddress))
+{
+ Console.WriteLine("Device is not responding");
+ // Handle offline device
+}
+```
+
+## Logging and Tracing
+
+### Setting Up Logging
+
+```csharp
+using Microsoft.Extensions.Logging;
+
+var loggerFactory = LoggerFactory.Create(builder =>
+ builder.AddConsole().SetMinimumLevel(LogLevel.Debug));
+
+var controlPanel = new ControlPanel(loggerFactory);
+```
+
+### Enabling Packet Tracing
+
+```csharp
+// Enable tracing to .osdpcap file
+var connectionId = controlPanel.StartConnection(connection, TimeSpan.FromSeconds(5), true);
+
+// Custom tracing
+controlPanel.StartConnection(connection, TimeSpan.FromSeconds(5),
+ packet => Console.WriteLine($"Packet: {BitConverter.ToString(packet.Data)}"));
+```
+
+## Best Practices
+
+1. **Always use secure channels in production** - Set `RequireSecurity = true`
+2. **Handle timeouts gracefully** - Network issues are common in access control systems
+3. **Implement proper logging** - Essential for debugging and monitoring
+4. **Use appropriate polling intervals** - Balance responsiveness with network overhead
+5. **Validate device responses** - Check for NACK replies and handle appropriately
+6. **Implement connection recovery** - Automatically reconnect when connections are lost
+7. **Test with real hardware** - Simulators may not catch all edge cases
+
+## Common Patterns
+
+### Device Discovery
+
+```csharp
+// Discover devices on a connection
+var discovery = await controlPanel.DiscoverDevice(connection);
+foreach (var result in discovery.DevicesFound)
+{
+ Console.WriteLine($"Found device at address {result.Address}");
+}
+```
+
+### Periodic Status Checks
+
+```csharp
+// Periodic device health check
+var timer = new Timer(async _ =>
+{
+ try
+ {
+ await controlPanel.LocalStatus(connectionId, deviceAddress);
+ }
+ catch
+ {
+ Console.WriteLine("Device health check failed");
+ }
+}, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
+```
+
+This guide covers the most common scenarios. For complete API documentation, refer to the XML documentation comments in the source code.
\ No newline at end of file
diff --git a/scripts/api-diff-check.ps1 b/scripts/api-diff-check.ps1
new file mode 100644
index 00000000..1e34dca5
--- /dev/null
+++ b/scripts/api-diff-check.ps1
@@ -0,0 +1,80 @@
+# PowerShell script to check API changes between versions
+param(
+ [string]$BaseVersion = "main",
+ [string]$CurrentBranch = "HEAD"
+)
+
+Write-Host "š Checking API differences between $BaseVersion and $CurrentBranch" -ForegroundColor Cyan
+
+function Get-PublicAPI {
+ param([string]$commit)
+
+ # Checkout commit
+ git checkout $commit --quiet
+
+ # Build project
+ dotnet build src/OSDP.Net/OSDP.Net.csproj --verbosity quiet --configuration Release
+
+ # Extract public API using reflection
+ $assemblyPath = "src/OSDP.Net/bin/Release/net8.0/OSDP.Net.dll"
+ if (-not (Test-Path $assemblyPath)) {
+ Write-Host "ā Assembly not found for commit $commit" -ForegroundColor Red
+ return @()
+ }
+
+ Add-Type -Path $assemblyPath
+ $assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $assemblyPath))
+
+ $api = @()
+ foreach ($type in $assembly.GetExportedTypes()) {
+ $api += "$($type.FullName)"
+
+ # Add public members
+ foreach ($member in $type.GetMembers([System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::Static)) {
+ if ($member.DeclaringType -eq $type) {
+ $api += " $($type.FullName).$($member.Name)"
+ }
+ }
+ }
+
+ return $api | Sort-Object
+}
+
+# Save current branch
+$currentBranch = git rev-parse --abbrev-ref HEAD
+
+try {
+ Write-Host "Analyzing base version ($BaseVersion)..." -ForegroundColor Yellow
+ $baseAPI = Get-PublicAPI $BaseVersion
+
+ Write-Host "Analyzing current version..." -ForegroundColor Yellow
+ git checkout $currentBranch --quiet
+ $currentAPI = Get-PublicAPI $CurrentBranch
+
+ # Compare APIs
+ $added = $currentAPI | Where-Object { $_ -notin $baseAPI }
+ $removed = $baseAPI | Where-Object { $_ -notin $currentAPI }
+
+ Write-Host "`nš API Changes Summary:" -ForegroundColor Cyan
+
+ if ($added.Count -gt 0) {
+ Write-Host "ā
Added ($($added.Count) items):" -ForegroundColor Green
+ $added | ForEach-Object { Write-Host " + $_" -ForegroundColor Green }
+ }
+
+ if ($removed.Count -gt 0) {
+ Write-Host "ā Removed ($($removed.Count) items):" -ForegroundColor Red
+ $removed | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
+ Write-Host "`nā ļø WARNING: Breaking changes detected!" -ForegroundColor Yellow
+ }
+
+ if ($added.Count -eq 0 -and $removed.Count -eq 0) {
+ Write-Host "ā
No API changes detected" -ForegroundColor Green
+ }
+
+} finally {
+ # Restore current branch
+ git checkout $currentBranch --quiet
+}
+
+Write-Host "`nš API diff check complete!" -ForegroundColor Green
\ No newline at end of file
diff --git a/scripts/generate-api-baseline.ps1 b/scripts/generate-api-baseline.ps1
new file mode 100644
index 00000000..b08efa8d
--- /dev/null
+++ b/scripts/generate-api-baseline.ps1
@@ -0,0 +1,102 @@
+# Simple PowerShell script to generate the API baseline
+param(
+ [string]$ProjectPath = "src/OSDP.Net/OSDP.Net.csproj",
+ [string]$BaselineFile = "api-baseline.txt",
+ [string]$Configuration = "Release"
+)
+
+Write-Host "š Generating OSDP.Net API Baseline..." -ForegroundColor Cyan
+
+# Build the project
+Write-Host "Building project..." -ForegroundColor Yellow
+dotnet build $ProjectPath --configuration $Configuration --verbosity quiet
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "ā Build failed!" -ForegroundColor Red
+ exit 1
+}
+
+Write-Host "ā
Build successful" -ForegroundColor Green
+
+# Find the assembly
+$assemblyPath = "src/OSDP.Net/bin/$Configuration/net8.0/OSDP.Net.dll"
+if (-not (Test-Path $assemblyPath)) {
+ Write-Host "ā Assembly not found at $assemblyPath" -ForegroundColor Red
+ exit 1
+}
+
+# Load assembly and extract public API
+Write-Host "š Extracting public API..." -ForegroundColor Yellow
+
+try {
+ Add-Type -Path $assemblyPath
+ $assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $assemblyPath))
+
+ $api = @()
+ $api += "# OSDP.Net Public API Baseline"
+ $api += "# Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
+ $api += "# Configuration: $Configuration"
+ $api += "#"
+ $api += ""
+
+ $publicTypes = $assembly.GetExportedTypes() | Sort-Object FullName
+
+ foreach ($type in $publicTypes) {
+ $category = "CLASS"
+ if ($type.IsInterface) { $category = "INTERFACE" }
+ elseif ($type.IsEnum) { $category = "ENUM" }
+ elseif ($type.IsValueType) { $category = "STRUCT" }
+
+ $api += "[$category] $($type.FullName)"
+
+ # Add constructors
+ $constructors = $type.GetConstructors() | Where-Object { $_.IsPublic }
+ foreach ($ctor in $constructors) {
+ $params = ($ctor.GetParameters() | ForEach-Object { "$($_.ParameterType.Name) $($_.Name)" }) -join ", "
+ $api += " [CONSTRUCTOR] $($type.FullName)($params)"
+ }
+
+ # Add methods (non-special)
+ $methods = $type.GetMethods() | Where-Object { $_.IsPublic -and -not $_.IsSpecialName -and $_.DeclaringType -eq $type }
+ foreach ($method in $methods) {
+ $params = ($method.GetParameters() | ForEach-Object { "$($_.ParameterType.Name) $($_.Name)" }) -join ", "
+ $api += " [METHOD] $($type.FullName).$($method.Name)($params) -> $($method.ReturnType.Name)"
+ }
+
+ # Add properties
+ $properties = $type.GetProperties() | Where-Object { $_.DeclaringType -eq $type }
+ foreach ($prop in $properties) {
+ $access = @()
+ if ($prop.CanRead -and $prop.GetMethod.IsPublic) { $access += "get" }
+ if ($prop.CanWrite -and $prop.SetMethod.IsPublic) { $access += "set" }
+ $api += " [PROPERTY] $($type.FullName).$($prop.Name) { $($access -join "; ") } -> $($prop.PropertyType.Name)"
+ }
+
+ # Add events
+ $events = $type.GetEvents() | Where-Object { $_.DeclaringType -eq $type }
+ foreach ($event in $events) {
+ $api += " [EVENT] $($type.FullName).$($event.Name) -> $($event.EventHandlerType.Name)"
+ }
+
+ # Add enum values
+ if ($type.IsEnum) {
+ $enumValues = [Enum]::GetValues($type)
+ foreach ($value in $enumValues) {
+ $api += " [ENUM_VALUE] $($type.FullName).$value = $([int]$value)"
+ }
+ }
+
+ $api += ""
+ }
+
+ # Save to file
+ $api | Out-File -FilePath $BaselineFile -Encoding UTF8
+
+ Write-Host "ā
API baseline generated with $($publicTypes.Count) public types" -ForegroundColor Green
+ Write-Host "š Saved to: $BaselineFile" -ForegroundColor Green
+
+} catch {
+ Write-Host "ā Error extracting API: $($_.Exception.Message)" -ForegroundColor Red
+ exit 1
+}
+
+Write-Host "š Complete!" -ForegroundColor Green
\ No newline at end of file
diff --git a/scripts/validate-api.ps1 b/scripts/validate-api.ps1
new file mode 100644
index 00000000..66881189
--- /dev/null
+++ b/scripts/validate-api.ps1
@@ -0,0 +1,66 @@
+# PowerShell script to validate public API surface
+param(
+ [string]$ProjectPath = "src/OSDP.Net/OSDP.Net.csproj"
+)
+
+Write-Host "š Validating OSDP.Net Public API..." -ForegroundColor Cyan
+
+# Build the project
+Write-Host "Building project..." -ForegroundColor Yellow
+$buildResult = dotnet build $ProjectPath --verbosity quiet
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "ā Build failed!" -ForegroundColor Red
+ exit 1
+}
+
+Write-Host "ā
Build successful" -ForegroundColor Green
+
+# Check for main public classes
+$requiredPublicClasses = @(
+ "OSDP.Net.ControlPanel",
+ "OSDP.Net.Device",
+ "OSDP.Net.DeviceConfiguration",
+ "OSDP.Net.Connections.IOsdpConnection",
+ "OSDP.Net.Connections.TcpClientOsdpConnection",
+ "OSDP.Net.Connections.SerialPortOsdpConnection"
+)
+
+# Use reflection to check API
+$assemblyPath = "src/OSDP.Net/bin/Debug/net8.0/OSDP.Net.dll"
+if (Test-Path $assemblyPath) {
+ Write-Host "š Checking public API surface..." -ForegroundColor Yellow
+
+ Add-Type -Path $assemblyPath
+ $assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $assemblyPath))
+
+ $publicTypes = $assembly.GetExportedTypes()
+ $publicTypeNames = $publicTypes | ForEach-Object { $_.FullName }
+
+ Write-Host "Found $($publicTypes.Count) public types" -ForegroundColor Green
+
+ $missing = @()
+ foreach ($required in $requiredPublicClasses) {
+ if ($required -notin $publicTypeNames) {
+ $missing += $required
+ }
+ }
+
+ if ($missing.Count -gt 0) {
+ Write-Host "ā Missing required public classes:" -ForegroundColor Red
+ $missing | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
+ exit 1
+ } else {
+ Write-Host "ā
All required public classes are available" -ForegroundColor Green
+ }
+
+ # List public types for review
+ Write-Host "`nš Public API Summary:" -ForegroundColor Cyan
+ $publicTypes | Sort-Object FullName | ForEach-Object {
+ Write-Host " $($_.FullName)" -ForegroundColor White
+ }
+} else {
+ Write-Host "ā Assembly not found at $assemblyPath" -ForegroundColor Red
+ exit 1
+}
+
+Write-Host "`nš Public API validation complete!" -ForegroundColor Green
\ No newline at end of file
diff --git a/src/Console/Console.csproj b/src/ACUConsole/ACUConsole.csproj
similarity index 93%
rename from src/Console/Console.csproj
rename to src/ACUConsole/ACUConsole.csproj
index 740a49b8..642910d0 100644
--- a/src/Console/Console.csproj
+++ b/src/ACUConsole/ACUConsole.csproj
@@ -4,6 +4,8 @@
Exe
net8.0
default
+ ACUConsole
+ ACUConsole
diff --git a/src/ACUConsole/ACUConsolePresenter.cs b/src/ACUConsole/ACUConsolePresenter.cs
new file mode 100644
index 00000000..c60f4289
--- /dev/null
+++ b/src/ACUConsole/ACUConsolePresenter.cs
@@ -0,0 +1,710 @@
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using ACUConsole.Configuration;
+using ACUConsole.Model;
+using log4net;
+using log4net.Config;
+using Microsoft.Extensions.Logging;
+using OSDP.Net;
+using OSDP.Net.Connections;
+using OSDP.Net.Model.CommandData;
+using OSDP.Net.PanelCommands.DeviceDiscover;
+using OSDP.Net.Tracing;
+using CommunicationConfiguration = OSDP.Net.Model.CommandData.CommunicationConfiguration;
+using ManufacturerSpecific = OSDP.Net.Model.CommandData.ManufacturerSpecific;
+
+namespace ACUConsole
+{
+ ///
+ /// Presenter class that manages the ACU Console business logic and device interactions
+ ///
+ public class ACUConsolePresenter : IACUConsolePresenter
+ {
+ private ControlPanel _controlPanel;
+ private ILoggerFactory _loggerFactory;
+ private readonly List _messageHistory = new();
+ private readonly object _messageLock = new();
+ private readonly ConcurrentDictionary _lastNak = new();
+
+ private Guid _connectionId = Guid.Empty;
+ private Settings _settings;
+ private string _lastConfigFilePath;
+ private string _lastOsdpConfigFilePath;
+
+ // Events
+ public event EventHandler MessageReceived;
+ public event EventHandler StatusChanged;
+ public event EventHandler ConnectionStatusChanged;
+ public event EventHandler ErrorOccurred;
+
+ // Properties
+ public bool IsConnected => _connectionId != Guid.Empty;
+ public Guid ConnectionId => _connectionId;
+ public IReadOnlyList MessageHistory
+ {
+ get
+ {
+ lock (_messageLock)
+ {
+ return _messageHistory.ToList().AsReadOnly();
+ }
+ }
+ }
+ public Settings Settings => _settings;
+
+ public ACUConsolePresenter()
+ {
+ InitializeLogging();
+ InitializePaths();
+ InitializeControlPanel();
+ LoadSettings();
+ }
+
+
+ private void InitializeLogging()
+ {
+ XmlConfigurator.Configure(
+ LogManager.GetRepository(Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly()),
+ new FileInfo("log4net.config"));
+
+ _loggerFactory = new LoggerFactory();
+ _loggerFactory.AddLog4Net();
+
+ // Set up custom appender to redirect log messages
+ CustomAppender.MessageHandler = AddLogMessage;
+ }
+
+ private void InitializePaths()
+ {
+ _lastConfigFilePath = Path.Combine(Environment.CurrentDirectory, "appsettings.config");
+ _lastOsdpConfigFilePath = Environment.CurrentDirectory;
+ }
+
+ private void InitializeControlPanel()
+ {
+ _controlPanel = new ControlPanel(_loggerFactory);
+ RegisterControlPanelEvents();
+ }
+
+ private void LoadSettings()
+ {
+ try
+ {
+ string json = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.config"));
+ _settings = JsonSerializer.Deserialize(json) ?? new Settings();
+ }
+ catch
+ {
+ _settings = new Settings();
+ }
+ }
+
+ private void RegisterControlPanelEvents()
+ {
+ _controlPanel.ConnectionStatusChanged += OnConnectionStatusChanged;
+ _controlPanel.NakReplyReceived += OnNakReplyReceived;
+ _controlPanel.LocalStatusReportReplyReceived += OnLocalStatusReportReplyReceived;
+ _controlPanel.InputStatusReportReplyReceived += OnInputStatusReportReplyReceived;
+ _controlPanel.OutputStatusReportReplyReceived += OnOutputStatusReportReplyReceived;
+ _controlPanel.ReaderStatusReportReplyReceived += OnReaderStatusReportReplyReceived;
+ _controlPanel.RawCardDataReplyReceived += OnRawCardDataReplyReceived;
+ _controlPanel.KeypadReplyReceived += OnKeypadReplyReceived;
+ }
+
+ // Connection Methods
+ public async Task StartSerialConnection(string portName, int baudRate, int replyTimeout)
+ {
+ var connection = new SerialPortOsdpConnection(portName, baudRate)
+ {
+ ReplyTimeout = TimeSpan.FromMilliseconds(replyTimeout)
+ };
+
+ await StartConnection(connection);
+
+ _settings.SerialConnectionSettings.PortName = portName;
+ _settings.SerialConnectionSettings.BaudRate = baudRate;
+ _settings.SerialConnectionSettings.ReplyTimeout = replyTimeout;
+ }
+
+ public async Task StartTcpServerConnection(int portNumber, int baudRate, int replyTimeout)
+ {
+ var connection = new TcpServerOsdpConnection(portNumber, baudRate, _loggerFactory)
+ {
+ ReplyTimeout = TimeSpan.FromMilliseconds(replyTimeout)
+ };
+
+ await StartConnection(connection);
+
+ _settings.TcpServerConnectionSettings.PortNumber = portNumber;
+ _settings.TcpServerConnectionSettings.BaudRate = baudRate;
+ _settings.TcpServerConnectionSettings.ReplyTimeout = replyTimeout;
+ }
+
+ public async Task StartTcpClientConnection(string host, int portNumber, int baudRate, int replyTimeout)
+ {
+ var connection = new TcpClientOsdpConnection(host, portNumber, baudRate);
+
+ await StartConnection(connection);
+
+ _settings.TcpClientConnectionSettings.Host = host;
+ _settings.TcpClientConnectionSettings.PortNumber = portNumber;
+ _settings.TcpClientConnectionSettings.BaudRate = baudRate;
+ _settings.TcpClientConnectionSettings.ReplyTimeout = replyTimeout;
+ }
+
+ public async Task StopConnection()
+ {
+ _connectionId = Guid.Empty;
+ await _controlPanel.Shutdown();
+ AddLogMessage("Connection stopped");
+ }
+
+ private async Task StartConnection(IOsdpConnection osdpConnection)
+ {
+ _lastNak.Clear();
+
+ if (_connectionId != Guid.Empty)
+ {
+ await _controlPanel.Shutdown();
+ }
+
+ _connectionId = _controlPanel.StartConnection(osdpConnection,
+ TimeSpan.FromMilliseconds(_settings.PollingInterval),
+ _settings.IsTracing);
+
+ foreach (var device in _settings.Devices)
+ {
+ _controlPanel.AddDevice(_connectionId, device.Address, device.UseCrc,
+ device.UseSecureChannel, device.SecureChannelKey);
+ }
+
+ AddLogMessage($"Connection started with ID: {_connectionId}");
+ }
+
+ // Device Management Methods
+ public void AddDevice(string name, byte address, bool useCrc, bool useSecureChannel, byte[] secureChannelKey)
+ {
+ if (!IsConnected)
+ {
+ throw new InvalidOperationException("Start a connection before adding devices.");
+ }
+
+ _lastNak.TryRemove(address, out _);
+ _controlPanel.AddDevice(_connectionId, address, useCrc, useSecureChannel, secureChannelKey);
+
+ var foundDevice = _settings.Devices.FirstOrDefault(device => device.Address == address);
+ if (foundDevice != null)
+ {
+ _settings.Devices.Remove(foundDevice);
+ }
+
+ _settings.Devices.Add(new DeviceSetting
+ {
+ Address = address,
+ Name = name,
+ UseSecureChannel = useSecureChannel,
+ UseCrc = useCrc,
+ SecureChannelKey = secureChannelKey
+ });
+
+ AddLogMessage($"Device '{name}' added at address {address}");
+ }
+
+ public void RemoveDevice(byte address)
+ {
+ if (!IsConnected)
+ {
+ throw new InvalidOperationException("Start a connection before removing devices.");
+ }
+
+ var removedDevice = _settings.Devices.FirstOrDefault(d => d.Address == address);
+ if (removedDevice != null)
+ {
+ _controlPanel.RemoveDevice(_connectionId, address);
+ _lastNak.TryRemove(address, out _);
+ _settings.Devices.Remove(removedDevice);
+ AddLogMessage($"Device '{removedDevice.Name}' removed from address {address}");
+ }
+ }
+
+ public async Task DiscoverDevice(string portName, int pingTimeout, int reconnectDelay, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var result = await _controlPanel.DiscoverDevice(
+ SerialPortOsdpConnection.EnumBaudRates(portName),
+ new DiscoveryOptions
+ {
+ ProgressCallback = OnDiscoveryProgress,
+ ResponseTimeout = TimeSpan.FromMilliseconds(pingTimeout),
+ CancellationToken = cancellationToken,
+ ReconnectDelay = TimeSpan.FromMilliseconds(reconnectDelay),
+ }.WithDefaultTracer(_settings.IsTracing));
+
+ var resultMessage = result != null
+ ? $"Device discovered successfully:\n{result}"
+ : "Device was not found";
+
+ AddLogMessage(resultMessage);
+ return resultMessage;
+ }
+ catch (OperationCanceledException)
+ {
+ AddLogMessage("Device discovery cancelled");
+ return "Device discovery cancelled";
+ }
+ catch (Exception ex)
+ {
+ AddLogMessage($"Device Discovery Error:\n{ex}");
+ throw;
+ }
+ }
+
+ // Command Methods - Individual command implementations
+ public async Task SendDeviceCapabilities(byte address)
+ {
+ await ExecuteCommand("Device capabilities", address,
+ () => _controlPanel.DeviceCapabilities(_connectionId, address));
+ }
+
+ public async Task SendIdReport(byte address)
+ {
+ await ExecuteCommand("ID report", address,
+ () => _controlPanel.IdReport(_connectionId, address));
+ }
+
+ public async Task SendInputStatus(byte address)
+ {
+ await ExecuteCommand("Input status", address,
+ () => _controlPanel.InputStatus(_connectionId, address));
+ }
+
+ public async Task SendLocalStatus(byte address)
+ {
+ await ExecuteCommand("Local Status", address,
+ () => _controlPanel.LocalStatus(_connectionId, address));
+ }
+
+ public async Task SendOutputStatus(byte address)
+ {
+ await ExecuteCommand("Output status", address,
+ () => _controlPanel.OutputStatus(_connectionId, address));
+ }
+
+ public async Task SendReaderStatus(byte address)
+ {
+ await ExecuteCommand("Reader status", address,
+ () => _controlPanel.ReaderStatus(_connectionId, address));
+ }
+
+ public async Task SendCommunicationConfiguration(byte address, byte newAddress, int newBaudRate)
+ {
+ var config = new CommunicationConfiguration(newAddress, newBaudRate);
+ await ExecuteCommand("Communication Configuration", address,
+ () => _controlPanel.CommunicationConfiguration(_connectionId, address, config));
+
+ // Handle device address change
+ var device = _settings.Devices.FirstOrDefault(d => d.Address == address);
+ if (device != null)
+ {
+ _controlPanel.RemoveDevice(_connectionId, address);
+ _lastNak.TryRemove(address, out _);
+
+ device.Address = newAddress;
+ _controlPanel.AddDevice(_connectionId, device.Address, device.UseCrc,
+ device.UseSecureChannel, device.SecureChannelKey);
+ }
+ }
+
+ public async Task SendOutputControl(byte address, byte outputNumber, bool activate)
+ {
+ var outputControls = new OutputControls([
+ new OutputControl(outputNumber, activate
+ ? OutputControlCode.PermanentStateOnAbortTimedOperation
+ : OutputControlCode.PermanentStateOffAbortTimedOperation, 0)
+ ]);
+
+ await ExecuteCommand("Output Control Command", address,
+ () => _controlPanel.OutputControl(_connectionId, address, outputControls));
+ }
+
+ public async Task SendReaderLedControl(byte address, byte ledNumber, LedColor color)
+ {
+ var ledControls = new ReaderLedControls([
+ new ReaderLedControl(0, ledNumber,
+ TemporaryReaderControlCode.CancelAnyTemporaryAndDisplayPermanent, 1, 0,
+ LedColor.Red, LedColor.Green, 0,
+ PermanentReaderControlCode.SetPermanentState, 1, 0, color, color)
+ ]);
+
+ await ExecuteCommand("Reader LED Control Command", address,
+ () => _controlPanel.ReaderLedControl(_connectionId, address, ledControls));
+ }
+
+ public async Task SendReaderBuzzerControl(byte address, byte readerNumber, byte repeatTimes)
+ {
+ var buzzerControl = new ReaderBuzzerControl(readerNumber, ToneCode.Default, 2, 2, repeatTimes);
+ await ExecuteCommand("Reader Buzzer Control Command", address,
+ () => _controlPanel.ReaderBuzzerControl(_connectionId, address, buzzerControl));
+ }
+
+ public async Task SendReaderTextOutput(byte address, byte readerNumber, string text)
+ {
+ var textOutput = new ReaderTextOutput(readerNumber, TextCommand.PermanentTextNoWrap, 0, 1, 1, text);
+ await ExecuteCommand("Reader Text Output Command", address,
+ () => _controlPanel.ReaderTextOutput(_connectionId, address, textOutput));
+ }
+
+ public async Task SendManufacturerSpecific(byte address, byte[] vendorCode, byte[] data)
+ {
+ var manufacturerSpecific = new ManufacturerSpecific(vendorCode, data);
+ await ExecuteCommand("Manufacturer Specific Command", address,
+ () => _controlPanel.ManufacturerSpecificCommand(_connectionId, address, manufacturerSpecific));
+ }
+
+ public async Task SendEncryptionKeySet(byte address, byte[] key)
+ {
+ var keyConfig = new EncryptionKeyConfiguration(KeyType.SecureChannelBaseKey, key);
+ var result = await ExecuteCommand("Encryption Key Configuration", address,
+ () => _controlPanel.EncryptionKeySet(_connectionId, address, keyConfig));
+
+ if (result)
+ {
+ _lastNak.TryRemove(address, out _);
+ var device = _settings.Devices.FirstOrDefault(d => d.Address == address);
+ if (device != null)
+ {
+ device.UseSecureChannel = true;
+ device.SecureChannelKey = key;
+ _controlPanel.AddDevice(_connectionId, device.Address, device.UseCrc,
+ device.UseSecureChannel, device.SecureChannelKey);
+ }
+ }
+ }
+
+ public async Task SendBiometricRead(byte address, byte readerNumber, byte type, byte format, byte quality)
+ {
+ var biometricData = new BiometricReadData(readerNumber, (BiometricType)type, (BiometricFormat)format, quality);
+ var result = await ExecuteCommandWithTimeout("Biometric Read Command", address,
+ () => _controlPanel.ScanAndSendBiometricData(_connectionId, address, biometricData,
+ TimeSpan.FromSeconds(30), CancellationToken.None));
+
+ if (result.TemplateData.Length > 0)
+ {
+ await File.WriteAllBytesAsync("BioReadTemplate", result.TemplateData);
+ }
+ }
+
+ public async Task SendBiometricMatch(byte address, byte readerNumber, byte type, byte format, byte qualityThreshold, byte[] templateData)
+ {
+ var biometricTemplate = new BiometricTemplateData(readerNumber, (BiometricType)type, (BiometricFormat)format,
+ qualityThreshold, templateData);
+ await ExecuteCommandWithTimeout("Biometric Match Command", address,
+ () => _controlPanel.ScanAndMatchBiometricTemplate(_connectionId, address, biometricTemplate,
+ TimeSpan.FromSeconds(30), CancellationToken.None));
+ }
+
+ public async Task SendFileTransfer(byte address, byte type, byte[] data, byte messageSize)
+ {
+ var result = await _controlPanel.FileTransfer(_connectionId, address, type, data, messageSize,
+ status => AddLogMessage($"File transfer status: {status?.Status.ToString()}"), CancellationToken.None);
+ return (int)result;
+ }
+
+ public async Task SendCustomCommand(byte address, CommandData commandData)
+ {
+ await _controlPanel.SendCustomCommand(_connectionId, address, commandData);
+ }
+
+ // Configuration Methods
+ public void UpdateConnectionSettings(int pollingInterval, bool isTracing)
+ {
+ _settings.PollingInterval = pollingInterval;
+ _settings.IsTracing = isTracing;
+ StatusChanged?.Invoke(this, "Connection settings updated");
+ }
+
+ public void SaveConfiguration()
+ {
+ try
+ {
+ var json = JsonSerializer.Serialize(_settings, new JsonSerializerOptions { WriteIndented = true });
+ File.WriteAllText(_lastConfigFilePath, json);
+ AddLogMessage("Configuration saved successfully");
+ }
+ catch (Exception ex)
+ {
+ ErrorOccurred?.Invoke(this, ex);
+ }
+ }
+
+ public void LoadConfiguration()
+ {
+ try
+ {
+ LoadSettings();
+ AddLogMessage("Configuration loaded successfully");
+ }
+ catch (Exception ex)
+ {
+ ErrorOccurred?.Invoke(this, ex);
+ }
+ }
+
+ public void ParseOSDPCapFile(string filePath, byte? filterAddress, bool ignorePollsAndAcks, byte[] key)
+ {
+ try
+ {
+ var json = File.ReadAllText(filePath);
+ var entries = PacketDecoding.OSDPCapParser(json, key)
+ .Where(entry => FilterAddress(entry, filterAddress) && FilterPollsAndAcks(entry, ignorePollsAndAcks));
+
+ var textBuilder = BuildTextFromEntries(entries);
+ var outputPath = Path.ChangeExtension(filePath, ".txt");
+ File.WriteAllText(outputPath, textBuilder.ToString());
+
+ // Update the last used directory for next time
+ _lastOsdpConfigFilePath = Path.GetDirectoryName(filePath) ?? Environment.CurrentDirectory;
+
+ AddLogMessage($"OSDP Cap file parsed successfully. Output saved to: {outputPath}");
+ }
+ catch (Exception ex)
+ {
+ ErrorOccurred?.Invoke(this, ex);
+ }
+ }
+
+ // Utility Methods
+ public void ClearHistory()
+ {
+ lock (_messageLock)
+ {
+ _messageHistory.Clear();
+ }
+ StatusChanged?.Invoke(this, "Message history cleared");
+ }
+
+ public void AddLogMessage(string message)
+ {
+ lock (_messageLock)
+ {
+ var acuEvent = new ACUEvent
+ {
+ Timestamp = DateTime.Now,
+ Title = "System",
+ Message = message,
+ Type = ACUEventType.Information
+ };
+
+ _messageHistory.Add(acuEvent);
+
+ // Keep only the last 100 messages
+ if (_messageHistory.Count > 100)
+ {
+ _messageHistory.RemoveAt(0);
+ }
+
+ MessageReceived?.Invoke(this, acuEvent);
+ }
+ }
+
+ public bool CanSendCommand()
+ {
+ return IsConnected && _settings.Devices.Count > 0;
+ }
+
+ public string[] GetDeviceList()
+ {
+ return _settings.Devices
+ .OrderBy(device => device.Address)
+ .Select(device => $"{device.Address} : {device.Name}")
+ .ToArray();
+ }
+
+ public string GetLastOsdpConfigDirectory()
+ {
+ return _lastOsdpConfigFilePath;
+ }
+
+ // Private helper methods
+ private async Task ExecuteCommand(string commandName, byte address, Func> commandFunction)
+ {
+ try
+ {
+ var result = await commandFunction();
+ AddLogMessage($"{commandName} for address {address}\n{result}\n{new string('*', 30)}");
+ return result;
+ }
+ catch (Exception ex)
+ {
+ ErrorOccurred?.Invoke(this, ex);
+ throw;
+ }
+ }
+
+ private async Task ExecuteCommandWithTimeout(string commandName, byte address, Func> commandFunction)
+ {
+ try
+ {
+ var result = await commandFunction();
+ AddLogMessage($"{commandName} for address {address}\n{result}\n{new string('*', 30)}");
+ return result;
+ }
+ catch (Exception ex)
+ {
+ ErrorOccurred?.Invoke(this, ex);
+ throw;
+ }
+ }
+
+ private void OnDiscoveryProgress(DiscoveryResult current)
+ {
+ string additionalInfo = current.Status switch
+ {
+ DiscoveryStatus.Started => string.Empty,
+ DiscoveryStatus.LookingForDeviceOnConnection => $"\n Connection baud rate {current.Connection.BaudRate}...",
+ DiscoveryStatus.ConnectionWithDeviceFound => $"\n Connection baud rate {current.Connection.BaudRate}",
+ DiscoveryStatus.LookingForDeviceAtAddress => $"\n Address {current.Address}...",
+ _ => string.Empty
+ };
+
+ AddLogMessage($"Device Discovery Progress: {current.Status}{additionalInfo}");
+ }
+
+ private bool FilterAddress(OSDPCapEntry entry, byte? address)
+ {
+ return !address.HasValue || entry.Packet.Address == address.Value;
+ }
+
+ private bool FilterPollsAndAcks(OSDPCapEntry entry, bool ignorePollsAndAcks)
+ {
+ if (!ignorePollsAndAcks) return true;
+
+ return (entry.Packet.CommandType != null && entry.Packet.CommandType != OSDP.Net.Messages.CommandType.Poll) ||
+ (entry.Packet.ReplyType != null && entry.Packet.ReplyType != OSDP.Net.Messages.ReplyType.Ack);
+ }
+
+ private StringBuilder BuildTextFromEntries(IEnumerable entries)
+ {
+ var textBuilder = new StringBuilder();
+ DateTime lastEntryTimeStamp = DateTime.MinValue;
+
+ foreach (var entry in entries)
+ {
+ TimeSpan difference = lastEntryTimeStamp > DateTime.MinValue
+ ? entry.TimeStamp - lastEntryTimeStamp
+ : TimeSpan.Zero;
+ lastEntryTimeStamp = entry.TimeStamp;
+
+ string direction = "Unknown";
+ string type = "Unknown";
+
+ if (entry.Packet.CommandType != null)
+ {
+ direction = "ACU -> PD";
+ type = entry.Packet.CommandType.ToString();
+ }
+ else if (entry.Packet.ReplyType != null)
+ {
+ direction = "PD -> ACU";
+ type = entry.Packet.ReplyType.ToString();
+ }
+
+ var payloadData = entry.Packet.ParsePayloadData();
+
+ var payloadDataString = payloadData switch
+ {
+ null => string.Empty,
+ byte[] data => $" {BitConverter.ToString(data)}\n",
+ string data => $" {data}\n",
+ _ => payloadData.ToString()
+ };
+
+ textBuilder.AppendLine($"{entry.TimeStamp:yy-MM-dd HH:mm:ss.fff} [ {difference:g} ] {direction}: {type}");
+ textBuilder.AppendLine($" Address: {entry.Packet.Address} Sequence: {entry.Packet.Sequence}");
+ textBuilder.AppendLine(payloadDataString);
+ }
+
+ return textBuilder;
+ }
+
+ // Event handlers for ControlPanel events
+ private void OnConnectionStatusChanged(object sender, ControlPanel.ConnectionStatusEventArgs args)
+ {
+ var deviceName = _settings.Devices.SingleOrDefault(device => device.Address == args.Address)?.Name ?? "[Unknown]";
+ var eventArgs = new ConnectionStatusChangedEventArgs
+ {
+ Address = args.Address,
+ IsConnected = args.IsConnected,
+ IsSecureChannelEstablished = args.IsSecureChannelEstablished,
+ DeviceName = deviceName
+ };
+
+ ConnectionStatusChanged?.Invoke(this, eventArgs);
+
+ var statusMessage = $"Device '{deviceName}' at address {args.Address} is now " +
+ $"{(args.IsConnected ? (args.IsSecureChannelEstablished ? "connected with secure channel" : "connected with clear text") : "disconnected")}";
+
+ AddLogMessage(statusMessage);
+ }
+
+ private void OnNakReplyReceived(object sender, ControlPanel.NakReplyEventArgs args)
+ {
+ _lastNak.TryRemove(args.Address, out var lastNak);
+ _lastNak.TryAdd(args.Address, args);
+
+ if (lastNak != null && lastNak.Address == args.Address &&
+ lastNak.Nak.ErrorCode == args.Nak.ErrorCode)
+ {
+ return;
+ }
+
+ AddLogMessage($"!!! Received NAK reply for address {args.Address} !!!\n{args.Nak}");
+ }
+
+ private void OnLocalStatusReportReplyReceived(object sender, ControlPanel.LocalStatusReportReplyEventArgs args)
+ {
+ AddLogMessage($"Local status updated for address {args.Address}\n{args.LocalStatus}");
+ }
+
+ private void OnInputStatusReportReplyReceived(object sender, ControlPanel.InputStatusReportReplyEventArgs args)
+ {
+ AddLogMessage($"Input status updated for address {args.Address}\n{args.InputStatus}");
+ }
+
+ private void OnOutputStatusReportReplyReceived(object sender, ControlPanel.OutputStatusReportReplyEventArgs args)
+ {
+ AddLogMessage($"Output status updated for address {args.Address}\n{args.OutputStatus}");
+ }
+
+ private void OnReaderStatusReportReplyReceived(object sender, ControlPanel.ReaderStatusReportReplyEventArgs args)
+ {
+ AddLogMessage($"Reader tamper status updated for address {args.Address}\n{args.ReaderStatus}");
+ }
+
+ private void OnRawCardDataReplyReceived(object sender, ControlPanel.RawCardDataReplyEventArgs args)
+ {
+ AddLogMessage($"Received raw card data reply for address {args.Address}\n{args.RawCardData}");
+ }
+
+ private void OnKeypadReplyReceived(object sender, ControlPanel.KeypadReplyEventArgs args)
+ {
+ AddLogMessage($"Received keypad data reply for address {args.Address}\n{args.KeypadData}");
+ }
+
+ public void Dispose()
+ {
+ _controlPanel?.Shutdown().Wait();
+ _loggerFactory?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/ACUConsoleView.cs b/src/ACUConsole/ACUConsoleView.cs
new file mode 100644
index 00000000..c91f00b0
--- /dev/null
+++ b/src/ACUConsole/ACUConsoleView.cs
@@ -0,0 +1,774 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using ACUConsole.Dialogs;
+using ACUConsole.Model;
+using OSDP.Net.Model.CommandData;
+using Terminal.Gui;
+
+namespace ACUConsole
+{
+ ///
+ /// View class that handles all Terminal.Gui UI elements and interactions for ACU Console
+ ///
+ public class ACUConsoleView : IACUConsoleView
+ {
+ private readonly IACUConsolePresenter _presenter;
+
+ // UI Components
+ private Window _window;
+ private ScrollView _scrollView;
+ private MenuBar _menuBar;
+ private readonly MenuItem _discoverMenuItem;
+
+ public ACUConsoleView(IACUConsolePresenter presenter)
+ {
+ _presenter = presenter ?? throw new ArgumentNullException(nameof(presenter));
+
+ // Create discover menu item that can be updated
+ _discoverMenuItem = new MenuItem("_Discover", string.Empty, () => _ = DiscoverDevice());
+
+ // Subscribe to presenter events
+ _presenter.MessageReceived += OnMessageReceived;
+ _presenter.StatusChanged += OnStatusChanged;
+ _presenter.ConnectionStatusChanged += OnConnectionStatusChanged;
+ _presenter.ErrorOccurred += OnErrorOccurred;
+ }
+
+ public void Initialize()
+ {
+ Application.Init();
+ CreateMainWindow();
+ CreateMenuBar();
+ CreateScrollView();
+ Application.Top.Add(_menuBar, _window);
+ }
+
+ public void Run()
+ {
+ Application.Run();
+ }
+
+ private void CreateMainWindow()
+ {
+ _window = new Window("OSDP.Net ACU Console")
+ {
+ X = 0,
+ Y = 1, // Leave one row for the toplevel menu
+ Width = Dim.Fill(),
+ Height = Dim.Fill() - 1
+ };
+ }
+
+ private void CreateMenuBar()
+ {
+ _menuBar = new MenuBar([
+ new MenuBarItem("_System", [
+ new MenuItem("_About", "", ShowAbout),
+ new MenuItem("_Connection Settings", "", UpdateConnectionSettings),
+ new MenuItem("_Parse OSDP Cap File", "", ParseOSDPCapFile),
+ new MenuItem("_Load Configuration", "", LoadConfigurationSettings),
+ new MenuItem("_Save Configuration", "", () => _presenter.SaveConfiguration()),
+ new MenuItem("_Quit", "", Quit)
+ ]),
+ new MenuBarItem("Co_nnections", [
+ new MenuItem("Start Serial Connection", "", () => _ = StartSerialConnection()),
+ new MenuItem("Start TCP Server Connection", "", () => _ = StartTcpServerConnection()),
+ new MenuItem("Start TCP Client Connection", "", () => _ = StartTcpClientConnection()),
+ new MenuItem("Stop Connections", "", () => _ = _presenter.StopConnection())
+ ]),
+ new MenuBarItem("_Devices", [
+ new MenuItem("_Add", string.Empty, AddDevice),
+ new MenuItem("_Remove", string.Empty, RemoveDevice),
+ _discoverMenuItem
+ ]),
+ new MenuBarItem("_Commands", [
+ new MenuItem("Communication Configuration", "", () => _ = SendCommunicationConfiguration()),
+ new MenuItem("Biometric Read", "", () => _ = SendBiometricReadCommand()),
+ new MenuItem("Biometric Match", "", () => _ = SendBiometricMatchCommand()),
+ new MenuItem("_Device Capabilities", "", () => SendSimpleCommand("Device capabilities", _presenter.SendDeviceCapabilities)),
+ new MenuItem("Encryption Key Set", "", () => _ = SendEncryptionKeySetCommand()),
+ new MenuItem("File Transfer", "", () => _ = SendFileTransferCommand()),
+ new MenuItem("_ID Report", "", () => SendSimpleCommand("ID report", _presenter.SendIdReport)),
+ new MenuItem("Input Status", "", () => SendSimpleCommand("Input status", _presenter.SendInputStatus)),
+ new MenuItem("_Local Status", "", () => SendSimpleCommand("Local Status", _presenter.SendLocalStatus)),
+ new MenuItem("Manufacturer Specific", "", () => _ = SendManufacturerSpecificCommand()),
+ new MenuItem("Output Control", "", () => _ = SendOutputControlCommand()),
+ new MenuItem("Output Status", "", () => SendSimpleCommand("Output status", _presenter.SendOutputStatus)),
+ new MenuItem("Reader Buzzer Control", "", () => _ = SendReaderBuzzerControlCommand()),
+ new MenuItem("Reader LED Control", "", () => _ = SendReaderLedControlCommand()),
+ new MenuItem("Reader Text Output", "", () => _ = SendReaderTextOutputCommand()),
+ new MenuItem("_Reader Status", "", () => SendSimpleCommand("Reader status", _presenter.SendReaderStatus))
+ ]),
+ new MenuBarItem("_Invalid Commands", [
+ new MenuItem("_Bad CRC/Checksum", "", () => SendCustomCommand("Bad CRC/Checksum", new Commands.InvalidCrcPollCommand())),
+ new MenuItem("Invalid Command Length", "", () => SendCustomCommand("Invalid Command Length", new Commands.InvalidLengthPollCommand())),
+ new MenuItem("Invalid Command", "", () => SendCustomCommand("Invalid Command", new Commands.InvalidCommand()))
+ ])
+ ]);
+ }
+
+ private void CreateScrollView()
+ {
+ _scrollView = new ScrollView(new Rect(0, 0, 0, 0))
+ {
+ ContentSize = new Size(500, 100),
+ ShowVerticalScrollIndicator = true,
+ ShowHorizontalScrollIndicator = true
+ };
+ _window.Add(_scrollView);
+ }
+
+ // System Menu Actions
+ private void ShowAbout()
+ {
+ var version = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version;
+ MessageBox.Query(40, 6, "About", $"OSDP.Net ACU Console\nVersion: {version}", 0, "OK");
+ }
+
+ private void Quit()
+ {
+ var result = MessageBox.Query(60, 8, "Exit Application",
+ "Do you want to save your configuration before exiting?",
+ 2, "Cancel", "Don't Save", "Save");
+
+ switch (result)
+ {
+ case 0: // Cancel
+ // Do nothing, stay in application
+ break;
+ case 1: // Don't Save
+ Application.Shutdown();
+ break;
+ case 2: // Save
+ _presenter.SaveConfiguration();
+ Application.Shutdown();
+ break;
+ }
+ }
+
+ // Connection Methods - Using extracted dialog classes
+ private async Task StartSerialConnection()
+ {
+ var input = SerialConnectionDialog.Show(_presenter.Settings.SerialConnectionSettings);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ await _presenter.StartSerialConnection(input.PortName, input.BaudRate, input.ReplyTimeout);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Connection Error", ex.Message);
+ }
+ }
+ }
+
+ private async Task StartTcpServerConnection()
+ {
+ var input = TcpServerConnectionDialog.Show(_presenter.Settings.TcpServerConnectionSettings);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ await _presenter.StartTcpServerConnection(input.PortNumber, input.BaudRate, input.ReplyTimeout);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Connection Error", ex.Message);
+ }
+ }
+ }
+
+ private async Task StartTcpClientConnection()
+ {
+ var input = TcpClientConnectionDialog.Show(_presenter.Settings.TcpClientConnectionSettings);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ await _presenter.StartTcpClientConnection(input.Host, input.PortNumber, input.BaudRate, input.ReplyTimeout);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Connection Error", ex.Message);
+ }
+ }
+ }
+
+ private void UpdateConnectionSettings()
+ {
+ var input = ConnectionSettingsDialog.Show(_presenter.Settings.PollingInterval, _presenter.Settings.IsTracing);
+
+ if (!input.WasCancelled)
+ {
+ _presenter.UpdateConnectionSettings(input.PollingInterval, input.IsTracing);
+ }
+ }
+
+ private void ParseOSDPCapFile()
+ {
+ var input = ParseOSDPCapFileDialog.Show(_presenter.GetLastOsdpConfigDirectory());
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ _presenter.ParseOSDPCapFile(input.FilePath, input.FilterAddress, input.IgnorePollsAndAcks, input.SecureKey);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", $"Unable to parse. {ex.Message}");
+ }
+ }
+ }
+
+ private void LoadConfigurationSettings()
+ {
+ var openDialog = new OpenDialog("Load Configuration", string.Empty, [".config"]);
+ Application.Run(openDialog);
+
+ if (!openDialog.Canceled && File.Exists(openDialog.FilePath?.ToString()))
+ {
+ try
+ {
+ _presenter.LoadConfiguration();
+ MessageBox.Query(40, 6, "Load Configuration", "Load completed successfully", "OK");
+ }
+ catch (Exception ex)
+ {
+ MessageBox.ErrorQuery(40, 8, "Error", ex.Message, "OK");
+ }
+ }
+ }
+
+ // Device Management Methods - Using extracted dialog classes
+ private void AddDevice()
+ {
+ if (!_presenter.IsConnected)
+ {
+ ShowError("Information", "Start a connection before adding devices.");
+ return;
+ }
+
+ var input = AddDeviceDialog.Show(_presenter.Settings.Devices.ToArray());
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ _presenter.AddDevice(input.Name, input.Address, input.UseCrc,
+ input.UseSecureChannel, input.SecureChannelKey);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", ex.Message);
+ }
+ }
+ }
+
+ private void RemoveDevice()
+ {
+ if (!_presenter.IsConnected)
+ {
+ ShowError("Information", "Start a connection before removing devices.");
+ return;
+ }
+
+ var deviceList = _presenter.GetDeviceList();
+ var input = RemoveDeviceDialog.Show(_presenter.Settings.Devices.ToArray(), deviceList);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ _presenter.RemoveDevice(input.DeviceAddress);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", ex.Message);
+ }
+ }
+ }
+
+ private async Task DiscoverDevice()
+ {
+ var input = DiscoverDeviceDialog.Show(_presenter.Settings.SerialConnectionSettings.PortName);
+
+ if (!input.WasCancelled)
+ {
+ var cancellationTokenSource = new CancellationTokenSource();
+
+ void CancelDiscovery()
+ {
+
+ // ReSharper disable AccessToDisposedClosure
+ cancellationTokenSource?.Cancel();
+ cancellationTokenSource?.Dispose();
+ // ReSharper restore AccessToDisposedClosure
+ cancellationTokenSource = null;
+ }
+
+ void CompleteDiscovery()
+ {
+ _discoverMenuItem.Title = "_Discover";
+ _discoverMenuItem.Action = () => _ = DiscoverDevice();
+ }
+
+ try
+ {
+ _discoverMenuItem.Title = "Cancel _Discover";
+ _discoverMenuItem.Action = CancelDiscovery;
+
+ await _presenter.DiscoverDevice(input.PortName, input.PingTimeout, input.ReconnectDelay, cancellationTokenSource.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ // Discovery was cancelled - this is expected, no need to show error
+ }
+ catch (Exception ex)
+ {
+ ShowError("Exception in Device Discovery", ex.Message);
+ }
+ finally
+ {
+ CompleteDiscovery();
+ cancellationTokenSource?.Dispose();
+ }
+ }
+ }
+
+ // Command Methods - Simplified
+ private void SendSimpleCommand(string title, Func commandFunction)
+ {
+ if (!_presenter.CanSendCommand())
+ {
+ ShowCommandRequirementsError();
+ return;
+ }
+
+ _ = ShowDeviceSelectionDialog(title, async (address) =>
+ {
+ try
+ {
+ await commandFunction(address);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.ErrorQuery(40, 10, $"Error on address {address}", ex.Message, "OK");
+ }
+ });
+ }
+
+ private async Task SendCommunicationConfiguration()
+ {
+ if (!_presenter.CanSendCommand())
+ {
+ ShowCommandRequirementsError();
+ return;
+ }
+
+ var deviceList = _presenter.GetDeviceList();
+ var input = CommunicationConfigurationDialog.Show(_presenter.Settings.Devices.ToArray(), deviceList, _presenter.Settings.SerialConnectionSettings.BaudRate);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ await _presenter.SendCommunicationConfiguration(input.DeviceAddress, input.NewAddress, input.NewBaudRate);
+
+ if (_presenter.Settings.SerialConnectionSettings.BaudRate != input.NewBaudRate)
+ {
+ ShowInformation("Info", $"The connection needs to be restarted with baud rate of {input.NewBaudRate}");
+ }
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", ex.Message);
+ }
+ }
+ }
+
+ private async Task SendOutputControlCommand()
+ {
+ if (!_presenter.CanSendCommand())
+ {
+ ShowCommandRequirementsError();
+ return;
+ }
+
+ var deviceList = _presenter.GetDeviceList();
+ var input = OutputControlDialog.Show(_presenter.Settings.Devices.ToArray(), deviceList);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ await _presenter.SendOutputControl(input.DeviceAddress, input.OutputNumber, input.ActivateOutput);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", ex.Message);
+ }
+ }
+ }
+
+ private async Task SendReaderLedControlCommand()
+ {
+ if (!_presenter.CanSendCommand())
+ {
+ ShowCommandRequirementsError();
+ return;
+ }
+
+ var deviceList = _presenter.GetDeviceList();
+ var input = ReaderLedControlDialog.Show(_presenter.Settings.Devices.ToArray(), deviceList);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ await _presenter.SendReaderLedControl(input.DeviceAddress, input.LedNumber, input.Color);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", ex.Message);
+ }
+ }
+ }
+
+ private async Task SendReaderBuzzerControlCommand()
+ {
+ if (!_presenter.CanSendCommand())
+ {
+ ShowCommandRequirementsError();
+ return;
+ }
+
+ var deviceList = _presenter.GetDeviceList();
+ var input = ReaderBuzzerControlDialog.Show(_presenter.Settings.Devices.ToArray(), deviceList);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ await _presenter.SendReaderBuzzerControl(input.DeviceAddress, input.ReaderNumber, input.RepeatTimes);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", ex.Message);
+ }
+ }
+ }
+
+ private async Task SendReaderTextOutputCommand()
+ {
+ if (!_presenter.CanSendCommand())
+ {
+ ShowCommandRequirementsError();
+ return;
+ }
+
+ var deviceList = _presenter.GetDeviceList();
+ var input = ReaderTextOutputDialog.Show(_presenter.Settings.Devices.ToArray(), deviceList);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ await _presenter.SendReaderTextOutput(input.DeviceAddress, input.ReaderNumber, input.Text);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", ex.Message);
+ }
+ }
+ }
+
+ private async Task SendManufacturerSpecificCommand()
+ {
+ if (!_presenter.CanSendCommand())
+ {
+ ShowCommandRequirementsError();
+ return;
+ }
+
+ var deviceList = _presenter.GetDeviceList();
+ var input = ManufacturerSpecificDialog.Show(_presenter.Settings.Devices.ToArray(), deviceList);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ await _presenter.SendManufacturerSpecific(input.DeviceAddress, input.VendorCode, input.Data);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", ex.Message);
+ }
+ }
+ }
+
+ private async Task SendEncryptionKeySetCommand()
+ {
+ if (!_presenter.CanSendCommand())
+ {
+ ShowCommandRequirementsError();
+ return;
+ }
+
+ var deviceList = _presenter.GetDeviceList();
+ var input = EncryptionKeySetDialog.Show(_presenter.Settings.Devices.ToArray(), deviceList);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ await _presenter.SendEncryptionKeySet(input.DeviceAddress, input.EncryptionKey);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", ex.Message);
+ }
+ }
+ }
+
+ private async Task SendBiometricReadCommand()
+ {
+ if (!_presenter.CanSendCommand())
+ {
+ ShowCommandRequirementsError();
+ return;
+ }
+
+ var deviceList = _presenter.GetDeviceList();
+ var input = BiometricReadDialog.Show(_presenter.Settings.Devices.ToArray(), deviceList);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ await _presenter.SendBiometricRead(input.DeviceAddress, input.ReaderNumber, input.Type, input.Format, input.Quality);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", ex.Message);
+ }
+ }
+ }
+
+ private async Task SendBiometricMatchCommand()
+ {
+ if (!_presenter.CanSendCommand())
+ {
+ ShowCommandRequirementsError();
+ return;
+ }
+
+ var deviceList = _presenter.GetDeviceList();
+ var input = BiometricMatchDialog.Show(_presenter.Settings.Devices.ToArray(), deviceList);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ await _presenter.SendBiometricMatch(input.DeviceAddress, input.ReaderNumber, input.Type, input.Format, input.QualityThreshold, input.TemplateData);
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", ex.Message);
+ }
+ }
+ }
+
+ private async Task SendFileTransferCommand()
+ {
+ if (!_presenter.CanSendCommand())
+ {
+ ShowCommandRequirementsError();
+ return;
+ }
+
+ var deviceList = _presenter.GetDeviceList();
+ var input = FileTransferDialog.Show(_presenter.Settings.Devices.ToArray(), deviceList);
+
+ if (!input.WasCancelled)
+ {
+ try
+ {
+ var totalFragments = await _presenter.SendFileTransfer(input.DeviceAddress, input.Type, input.FileData, input.MessageSize);
+ ShowInformation("File Transfer Complete", $"File transferred successfully in {totalFragments} fragments.");
+ }
+ catch (Exception ex)
+ {
+ ShowError("Error", ex.Message);
+ }
+ }
+ }
+
+ private void SendCustomCommand(string title, CommandData commandData)
+ {
+ if (!_presenter.CanSendCommand())
+ {
+ ShowCommandRequirementsError();
+ return;
+ }
+
+ _ = ShowDeviceSelectionDialog(title, async (address) =>
+ {
+ try
+ {
+ await _presenter.SendCustomCommand(address, commandData);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.ErrorQuery(40, 10, $"Error on address {address}", ex.Message, "OK");
+ }
+ });
+ }
+
+ // Helper Methods
+ private void ShowCommandRequirementsError()
+ {
+ if (!_presenter.IsConnected)
+ {
+ MessageBox.ErrorQuery(60, 10, "Warning", "Start a connection before sending commands.", "OK");
+ }
+ else if (_presenter.Settings.Devices.Count == 0)
+ {
+ MessageBox.ErrorQuery(60, 10, "Warning", "Add a device before sending commands.", "OK");
+ }
+ }
+
+ private async Task ShowDeviceSelectionDialog(string title, Func actionFunction)
+ {
+ var deviceList = _presenter.GetDeviceList();
+ var deviceSelection = DeviceSelectionDialog.Show(title, _presenter.Settings.Devices.ToArray(), deviceList);
+
+ if (!deviceSelection.WasCancelled)
+ {
+ await actionFunction(deviceSelection.SelectedDeviceAddress);
+ }
+ }
+
+
+ // Event Handlers
+ private void OnMessageReceived(object sender, ACUEvent acuEvent)
+ {
+ UpdateMessageDisplay();
+ }
+
+ private void OnStatusChanged(object sender, string status)
+ {
+ // Status updates can be displayed in a status bar if needed
+ }
+
+ private void OnConnectionStatusChanged(object sender, ConnectionStatusChangedEventArgs args)
+ {
+ // Connection status updates are handled through messages
+ }
+
+ private void OnErrorOccurred(object sender, Exception ex)
+ {
+ Application.MainLoop.Invoke(() =>
+ {
+ MessageBox.ErrorQuery("Error", ex.Message, "OK");
+ });
+ }
+
+ private void UpdateMessageDisplay()
+ {
+ Application.MainLoop.Invoke(() =>
+ {
+ if (!_window.HasFocus && _menuBar.HasFocus)
+ {
+ return;
+ }
+
+ _scrollView.Frame = new Rect(1, 0, _window.Frame.Width - 3, _window.Frame.Height - 2);
+ _scrollView.RemoveAll();
+
+ int index = 0;
+ foreach (var message in _presenter.MessageHistory.Reverse())
+ {
+ var messageText = message.ToString().TrimEnd();
+ var label = new Label(0, index, messageText);
+ index += label.Bounds.Height;
+
+ // Color code messages based on type
+ if (messageText.Contains("| WARN |") || messageText.Contains("NAK") || message.Type == ACUEventType.Warning)
+ {
+ label.ColorScheme = new ColorScheme
+ { Normal = Terminal.Gui.Attribute.Make(Color.Black, Color.BrightYellow) };
+ }
+ else if (messageText.Contains("| ERROR |") || message.Type == ACUEventType.Error)
+ {
+ label.ColorScheme = new ColorScheme
+ { Normal = Terminal.Gui.Attribute.Make(Color.White, Color.BrightRed) };
+ }
+
+ _scrollView.Add(label);
+ }
+ });
+ }
+
+ // IACUConsoleView interface implementation
+ public void ShowInformation(string title, string message)
+ {
+ Application.MainLoop.Invoke(() =>
+ {
+ MessageBox.Query(60, 8, title, message, "OK");
+ });
+ }
+
+ public void ShowError(string title, string message)
+ {
+ Application.MainLoop.Invoke(() =>
+ {
+ MessageBox.ErrorQuery(60, 8, title, message, "OK");
+ });
+ }
+
+ public void ShowWarning(string title, string message)
+ {
+ Application.MainLoop.Invoke(() =>
+ {
+ MessageBox.Query(60, 8, title, message, "OK");
+ });
+ }
+
+ public bool AskYesNo(string title, string message)
+ {
+ var result = false;
+ Application.MainLoop.Invoke(() =>
+ {
+ result = MessageBox.Query(60, 8, title, message, 1, "No", "Yes") == 1;
+ });
+ return result;
+ }
+
+ public void UpdateDiscoverMenuItem(string title, Action action)
+ {
+ Application.MainLoop.Invoke(() =>
+ {
+ _discoverMenuItem.Title = title;
+ _discoverMenuItem.Action = action;
+ });
+ }
+
+ public void RefreshMessageDisplay()
+ {
+ UpdateMessageDisplay();
+ }
+
+ public void Shutdown()
+ {
+ Application.Shutdown();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Console/Commands/InvalidCommand.cs b/src/ACUConsole/Commands/InvalidCommand.cs
similarity index 95%
rename from src/Console/Commands/InvalidCommand.cs
rename to src/ACUConsole/Commands/InvalidCommand.cs
index 49856dc8..37c43f59 100644
--- a/src/Console/Commands/InvalidCommand.cs
+++ b/src/ACUConsole/Commands/InvalidCommand.cs
@@ -3,7 +3,7 @@
using OSDP.Net.Messages.SecureChannel;
using OSDP.Net.Model.CommandData;
-namespace Console.Commands
+namespace ACUConsole.Commands
{
///
///
diff --git a/src/Console/Commands/InvalidCrcPollCommand.cs b/src/ACUConsole/Commands/InvalidCrcPollCommand.cs
similarity index 96%
rename from src/Console/Commands/InvalidCrcPollCommand.cs
rename to src/ACUConsole/Commands/InvalidCrcPollCommand.cs
index 8a3f7dfe..99719c63 100644
--- a/src/Console/Commands/InvalidCrcPollCommand.cs
+++ b/src/ACUConsole/Commands/InvalidCrcPollCommand.cs
@@ -3,7 +3,7 @@
using OSDP.Net.Messages.SecureChannel;
using OSDP.Net.Model.CommandData;
-namespace Console.Commands
+namespace ACUConsole.Commands
{
///
/// Change the CRC on a poll command
diff --git a/src/Console/Commands/InvalidLengthPollCommand.cs b/src/ACUConsole/Commands/InvalidLengthPollCommand.cs
similarity index 96%
rename from src/Console/Commands/InvalidLengthPollCommand.cs
rename to src/ACUConsole/Commands/InvalidLengthPollCommand.cs
index c64ce5b5..35efd12c 100644
--- a/src/Console/Commands/InvalidLengthPollCommand.cs
+++ b/src/ACUConsole/Commands/InvalidLengthPollCommand.cs
@@ -3,7 +3,7 @@
using OSDP.Net.Messages.SecureChannel;
using OSDP.Net.Model.CommandData;
-namespace Console.Commands
+namespace ACUConsole.Commands
{
///
/// Change the length on a poll command
diff --git a/src/Console/Configuration/DeviceSetting.cs b/src/ACUConsole/Configuration/DeviceSetting.cs
similarity index 93%
rename from src/Console/Configuration/DeviceSetting.cs
rename to src/ACUConsole/Configuration/DeviceSetting.cs
index a1a071b7..cc54c530 100644
--- a/src/Console/Configuration/DeviceSetting.cs
+++ b/src/ACUConsole/Configuration/DeviceSetting.cs
@@ -1,4 +1,4 @@
-namespace Console.Configuration
+namespace ACUConsole.Configuration
{
public class DeviceSetting
{
diff --git a/src/Console/Configuration/SerialConnectionSettings.cs b/src/ACUConsole/Configuration/SerialConnectionSettings.cs
similarity index 87%
rename from src/Console/Configuration/SerialConnectionSettings.cs
rename to src/ACUConsole/Configuration/SerialConnectionSettings.cs
index 21da52fe..ae612cb5 100644
--- a/src/Console/Configuration/SerialConnectionSettings.cs
+++ b/src/ACUConsole/Configuration/SerialConnectionSettings.cs
@@ -1,4 +1,4 @@
-namespace Console.Configuration
+namespace ACUConsole.Configuration
{
public class SerialConnectionSettings
{
diff --git a/src/Console/Configuration/Settings.cs b/src/ACUConsole/Configuration/Settings.cs
similarity index 94%
rename from src/Console/Configuration/Settings.cs
rename to src/ACUConsole/Configuration/Settings.cs
index fcb8aee7..253dccb5 100644
--- a/src/Console/Configuration/Settings.cs
+++ b/src/ACUConsole/Configuration/Settings.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
-namespace Console.Configuration
+namespace ACUConsole.Configuration
{
public class Settings
{
diff --git a/src/Console/Configuration/TcpClientConnectionSettings.cs b/src/ACUConsole/Configuration/TcpClientConnectionSettings.cs
similarity index 88%
rename from src/Console/Configuration/TcpClientConnectionSettings.cs
rename to src/ACUConsole/Configuration/TcpClientConnectionSettings.cs
index 6d77f336..856c51a3 100644
--- a/src/Console/Configuration/TcpClientConnectionSettings.cs
+++ b/src/ACUConsole/Configuration/TcpClientConnectionSettings.cs
@@ -1,4 +1,4 @@
-namespace Console.Configuration
+namespace ACUConsole.Configuration
{
public class TcpClientConnectionSettings
{
diff --git a/src/Console/Configuration/TcpServerConnectionSettings.cs b/src/ACUConsole/Configuration/TcpServerConnectionSettings.cs
similarity index 86%
rename from src/Console/Configuration/TcpServerConnectionSettings.cs
rename to src/ACUConsole/Configuration/TcpServerConnectionSettings.cs
index 3fe1d2ea..100f42cd 100644
--- a/src/Console/Configuration/TcpServerConnectionSettings.cs
+++ b/src/ACUConsole/Configuration/TcpServerConnectionSettings.cs
@@ -1,4 +1,4 @@
-namespace Console.Configuration
+namespace ACUConsole.Configuration
{
public class TcpServerConnectionSettings
{
diff --git a/src/Console/CustomAppender.cs b/src/ACUConsole/CustomAppender.cs
similarity index 60%
rename from src/Console/CustomAppender.cs
rename to src/ACUConsole/CustomAppender.cs
index d3ca4d77..5933a20b 100644
--- a/src/Console/CustomAppender.cs
+++ b/src/ACUConsole/CustomAppender.cs
@@ -1,15 +1,18 @@
+using System;
using log4net.Appender;
using log4net.Core;
-namespace Console
+namespace ACUConsole
{
public class CustomAppender : AppenderSkeleton
{
+ public static Action MessageHandler { get; set; }
+
protected override void Append(LoggingEvent loggingEvent)
{
if (loggingEvent.Level > Level.Debug)
{
- Program.AddLogMessage(RenderLoggingEvent(loggingEvent));
+ MessageHandler?.Invoke(RenderLoggingEvent(loggingEvent));
}
}
}
diff --git a/src/ACUConsole/Dialogs/AddDeviceDialog.cs b/src/ACUConsole/Dialogs/AddDeviceDialog.cs
new file mode 100644
index 00000000..778ab197
--- /dev/null
+++ b/src/ACUConsole/Dialogs/AddDeviceDialog.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Linq;
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting device addition parameters
+ ///
+ public static class AddDeviceDialog
+ {
+ ///
+ /// Shows the add device dialog and returns user input
+ ///
+ /// List of existing devices to check for duplicates
+ /// AddDeviceInput with user's choices
+ public static AddDeviceInput Show(DeviceSetting[] existingDevices)
+ {
+ var result = new AddDeviceInput { WasCancelled = true };
+
+ var nameTextField = new TextField(15, 1, 35, string.Empty);
+ var addressTextField = new TextField(15, 3, 35, string.Empty);
+ var useCrcCheckBox = new CheckBox(1, 5, "Use CRC", true);
+ var useSecureChannelCheckBox = new CheckBox(1, 6, "Use Secure Channel", true);
+ var keyTextField = new TextField(15, 8, 35, Convert.ToHexString(DeviceSetting.DefaultKey));
+
+ void AddDeviceButtonClicked()
+ {
+ // Validate address
+ if (!byte.TryParse(addressTextField.Text.ToString(), out var address) || address > 127)
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid address entered!", "OK");
+ return;
+ }
+
+ // Validate key length
+ if (keyTextField.Text == null || keyTextField.Text.Length != 32)
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid key length entered!", "OK");
+ return;
+ }
+
+ // Validate hex key format
+ byte[] key;
+ try
+ {
+ key = Convert.FromHexString(keyTextField.Text.ToString()!);
+ }
+ catch
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid hex characters!", "OK");
+ return;
+ }
+
+ // Check for existing device at address
+ var existingDevice = existingDevices.FirstOrDefault(d => d.Address == address);
+ bool overwriteExisting = false;
+ if (existingDevice != null)
+ {
+ if (MessageBox.Query(60, 10, "Overwrite", "Device already exists at that address, overwrite?", 1, "No", "Yes") == 0)
+ {
+ return;
+ }
+ overwriteExisting = true;
+ }
+
+ // All validation passed - collect the data
+ result.Name = nameTextField.Text.ToString();
+ result.Address = address;
+ result.UseCrc = useCrcCheckBox.Checked;
+ result.UseSecureChannel = useSecureChannelCheckBox.Checked;
+ result.SecureChannelKey = key;
+ result.OverwriteExisting = overwriteExisting;
+ result.WasCancelled = false;
+
+ Application.RequestStop();
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var addButton = new Button("Add", true);
+ addButton.Clicked += AddDeviceButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Add Device", 60, 13, cancelButton, addButton);
+ dialog.Add(new Label(1, 1, "Name:"), nameTextField,
+ new Label(1, 3, "Address:"), addressTextField,
+ useCrcCheckBox,
+ useSecureChannelCheckBox,
+ new Label(1, 8, "Secure Key:"), keyTextField);
+ nameTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/BiometricMatchDialog.cs b/src/ACUConsole/Dialogs/BiometricMatchDialog.cs
new file mode 100644
index 00000000..edc94362
--- /dev/null
+++ b/src/ACUConsole/Dialogs/BiometricMatchDialog.cs
@@ -0,0 +1,116 @@
+using System;
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting biometric match parameters and device selection
+ ///
+ public static class BiometricMatchDialog
+ {
+ ///
+ /// Shows the biometric match dialog and returns user input
+ ///
+ /// Available devices for selection
+ /// Formatted device list for display
+ /// BiometricMatchInput with user's choices
+ public static BiometricMatchInput Show(DeviceSetting[] devices, string[] deviceList)
+ {
+ var result = new BiometricMatchInput { WasCancelled = true };
+
+ // First, collect biometric match parameters
+ var readerNumberTextField = new TextField(25, 1, 25, "0");
+ var typeTextField = new TextField(25, 3, 25, "1");
+ var formatTextField = new TextField(25, 5, 25, "0");
+ var qualityThresholdTextField = new TextField(25, 7, 25, "1");
+ var templateDataTextField = new TextField(25, 9, 40, "");
+
+ void NextButtonClicked()
+ {
+ if (!byte.TryParse(readerNumberTextField.Text.ToString(), out var readerNumber))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid reader number entered!", "OK");
+ return;
+ }
+
+ if (!byte.TryParse(typeTextField.Text.ToString(), out var type))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid type entered!", "OK");
+ return;
+ }
+
+ if (!byte.TryParse(formatTextField.Text.ToString(), out var format))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid format entered!", "OK");
+ return;
+ }
+
+ if (!byte.TryParse(qualityThresholdTextField.Text.ToString(), out var qualityThreshold))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid quality threshold entered!", "OK");
+ return;
+ }
+
+ byte[] templateData;
+ try
+ {
+ var templateDataStr = templateDataTextField.Text.ToString();
+ if (string.IsNullOrWhiteSpace(templateDataStr))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Please enter template data!", "OK");
+ return;
+ }
+ templateData = Convert.FromHexString(templateDataStr);
+ }
+ catch
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid template data hex format!", "OK");
+ return;
+ }
+
+ Application.RequestStop();
+
+ // Show device selection dialog
+ var deviceSelection = DeviceSelectionDialog.Show("Biometric Match", devices, deviceList);
+
+ if (!deviceSelection.WasCancelled)
+ {
+ // All validation passed - collect the data
+ result.ReaderNumber = readerNumber;
+ result.Type = type;
+ result.Format = format;
+ result.QualityThreshold = qualityThreshold;
+ result.TemplateData = templateData;
+ result.DeviceAddress = deviceSelection.SelectedDeviceAddress;
+ result.WasCancelled = false;
+ }
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var sendButton = new Button("Next", true);
+ sendButton.Clicked += NextButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Biometric Match", 70, 17, cancelButton, sendButton);
+ dialog.Add(new Label(1, 1, "Reader Number:"), readerNumberTextField,
+ new Label(1, 3, "Type:"), typeTextField,
+ new Label(1, 5, "Format:"), formatTextField,
+ new Label(1, 7, "Quality Threshold:"), qualityThresholdTextField,
+ new Label(1, 9, "Template Data (hex):"), templateDataTextField,
+ new Label(1, 11, "Example: '010203040506070809'"));
+ readerNumberTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/BiometricReadDialog.cs b/src/ACUConsole/Dialogs/BiometricReadDialog.cs
new file mode 100644
index 00000000..e91f6691
--- /dev/null
+++ b/src/ACUConsole/Dialogs/BiometricReadDialog.cs
@@ -0,0 +1,98 @@
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting biometric read parameters and device selection
+ ///
+ public static class BiometricReadDialog
+ {
+ ///
+ /// Shows the biometric read dialog and returns user input
+ ///
+ /// Available devices for selection
+ /// Formatted device list for display
+ /// BiometricReadInput with user's choices
+ public static BiometricReadInput Show(DeviceSetting[] devices, string[] deviceList)
+ {
+ var result = new BiometricReadInput { WasCancelled = true };
+
+ // First, collect biometric read parameters
+ var readerNumberTextField = new TextField(25, 1, 25, "0");
+ var typeTextField = new TextField(25, 3, 25, "1");
+ var formatTextField = new TextField(25, 5, 25, "0");
+ var qualityTextField = new TextField(25, 7, 25, "1");
+
+ void NextButtonClicked()
+ {
+ // Validate reader number
+ if (!byte.TryParse(readerNumberTextField.Text.ToString(), out var readerNumber))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid reader number entered!", "OK");
+ return;
+ }
+
+ // Validate type
+ if (!byte.TryParse(typeTextField.Text.ToString(), out var type))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid type entered!", "OK");
+ return;
+ }
+
+ // Validate format
+ if (!byte.TryParse(formatTextField.Text.ToString(), out var format))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid format entered!", "OK");
+ return;
+ }
+
+ // Validate quality
+ if (!byte.TryParse(qualityTextField.Text.ToString(), out var quality))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid quality entered!", "OK");
+ return;
+ }
+
+ Application.RequestStop();
+
+ // Show device selection dialog
+ var deviceSelection = DeviceSelectionDialog.Show("Biometric Read", devices, deviceList);
+
+ if (!deviceSelection.WasCancelled)
+ {
+ // All validation passed - collect the data
+ result.ReaderNumber = readerNumber;
+ result.Type = type;
+ result.Format = format;
+ result.Quality = quality;
+ result.DeviceAddress = deviceSelection.SelectedDeviceAddress;
+ result.WasCancelled = false;
+ }
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var nextButton = new Button("Next", true);
+ nextButton.Clicked += NextButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Biometric Read", 60, 13, cancelButton, nextButton);
+ dialog.Add(new Label(1, 1, "Reader Number:"), readerNumberTextField,
+ new Label(1, 3, "Type:"), typeTextField,
+ new Label(1, 5, "Format:"), formatTextField,
+ new Label(1, 7, "Quality:"), qualityTextField);
+ readerNumberTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/CommunicationConfigurationDialog.cs b/src/ACUConsole/Dialogs/CommunicationConfigurationDialog.cs
new file mode 100644
index 00000000..6f7ec9f7
--- /dev/null
+++ b/src/ACUConsole/Dialogs/CommunicationConfigurationDialog.cs
@@ -0,0 +1,83 @@
+using System.Linq;
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting communication configuration parameters and device selection
+ ///
+ public static class CommunicationConfigurationDialog
+ {
+ ///
+ /// Shows the communication configuration dialog and returns user input
+ ///
+ /// Available devices for selection
+ /// Formatted device list for display
+ /// Current baud rate for default value
+ /// CommunicationConfigurationInput with user's choices
+ public static CommunicationConfigurationInput Show(DeviceSetting[] devices, string[] deviceList, int currentBaudRate)
+ {
+ var result = new CommunicationConfigurationInput { WasCancelled = true };
+
+ // Calculate suggested new address (highest existing + 1)
+ var suggestedAddress = ((devices.MaxBy(device => device.Address)?.Address ?? 0) + 1).ToString();
+
+ // First, collect communication configuration parameters
+ var newAddressTextField = new TextField(25, 1, 25, suggestedAddress);
+ var newBaudRateTextField = new TextField(25, 3, 25, currentBaudRate.ToString());
+
+ void NextButtonClicked()
+ {
+ // Validate new address
+ if (!byte.TryParse(newAddressTextField.Text.ToString(), out var newAddress) || newAddress > 127)
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid address entered (0-127)!", "OK");
+ return;
+ }
+
+ // Validate new baud rate
+ if (!int.TryParse(newBaudRateTextField.Text.ToString(), out var newBaudRate))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid baud rate entered!", "OK");
+ return;
+ }
+
+ Application.RequestStop();
+
+ // Show device selection dialog
+ var deviceSelection = DeviceSelectionDialog.Show("Communication Configuration", devices, deviceList);
+
+ if (!deviceSelection.WasCancelled)
+ {
+ // All validation passed - collect the data
+ result.NewAddress = newAddress;
+ result.NewBaudRate = newBaudRate;
+ result.DeviceAddress = deviceSelection.SelectedDeviceAddress;
+ result.WasCancelled = false;
+ }
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var nextButton = new Button("Next", true);
+ nextButton.Clicked += NextButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Communication Configuration", 60, 11, cancelButton, nextButton);
+ dialog.Add(new Label(1, 1, "New Address:"), newAddressTextField,
+ new Label(1, 3, "New Baud Rate:"), newBaudRateTextField);
+ newAddressTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/ConnectionSettingsDialog.cs b/src/ACUConsole/Dialogs/ConnectionSettingsDialog.cs
new file mode 100644
index 00000000..52ab4efe
--- /dev/null
+++ b/src/ACUConsole/Dialogs/ConnectionSettingsDialog.cs
@@ -0,0 +1,63 @@
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for updating connection settings
+ ///
+ public static class ConnectionSettingsDialog
+ {
+ ///
+ /// Shows the connection settings dialog and returns user input
+ ///
+ /// Current polling interval value
+ /// Current tracing setting
+ /// ConnectionSettingsInput with user's choices
+ public static ConnectionSettingsInput Show(int currentPollingInterval, bool currentIsTracing)
+ {
+ var result = new ConnectionSettingsInput { WasCancelled = true };
+
+ var pollingIntervalTextField = new TextField(25, 4, 25, currentPollingInterval.ToString());
+ var tracingCheckBox = new CheckBox(1, 6, "Write packet data to file", currentIsTracing);
+
+ void UpdateConnectionSettingsButtonClicked()
+ {
+ // Validate polling interval
+ if (!int.TryParse(pollingIntervalTextField.Text.ToString(), out var pollingInterval))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid polling interval entered!", "OK");
+ return;
+ }
+
+ // All validation passed - collect the data
+ result.PollingInterval = pollingInterval;
+ result.IsTracing = tracingCheckBox.Checked;
+ result.WasCancelled = false;
+
+ Application.RequestStop();
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var updateButton = new Button("Update", true);
+ updateButton.Clicked += UpdateConnectionSettingsButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Update Connection Settings", 60, 12, cancelButton, updateButton);
+ dialog.Add(new Label(new Rect(1, 1, 55, 2), "Connection will need to be restarted for setting to take effect."),
+ new Label(1, 4, "Polling Interval(ms):"), pollingIntervalTextField,
+ tracingCheckBox);
+ pollingIntervalTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/DeviceSelectionDialog.cs b/src/ACUConsole/Dialogs/DeviceSelectionDialog.cs
new file mode 100644
index 00000000..7a83f586
--- /dev/null
+++ b/src/ACUConsole/Dialogs/DeviceSelectionDialog.cs
@@ -0,0 +1,66 @@
+using System.Linq;
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using NStack;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for selecting a device from available devices
+ ///
+ public static class DeviceSelectionDialog
+ {
+ ///
+ /// Shows the device selection dialog and returns user selection
+ ///
+ /// Dialog title
+ /// Available devices to choose from
+ /// Formatted device list for display
+ /// DeviceSelectionInput with user's choice
+ public static DeviceSelectionInput Show(string title, DeviceSetting[] devices, string[] deviceList)
+ {
+ var result = new DeviceSelectionInput { WasCancelled = true };
+
+ var scrollView = new ScrollView(new Rect(6, 1, 50, 6))
+ {
+ ContentSize = new Size(40, deviceList.Length * 2),
+ ShowVerticalScrollIndicator = deviceList.Length > 6,
+ ShowHorizontalScrollIndicator = false
+ };
+
+ var deviceRadioGroup = new RadioGroup(0, 0, deviceList.Select(ustring.Make).ToArray())
+ {
+ SelectedItem = 0
+ };
+ scrollView.Add(deviceRadioGroup);
+
+ void SendCommandButtonClicked()
+ {
+ var selectedDevice = devices.OrderBy(device => device.Address).ToArray()[deviceRadioGroup.SelectedItem];
+ result.SelectedDeviceAddress = selectedDevice.Address;
+ result.WasCancelled = false;
+ Application.RequestStop();
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var sendButton = new Button("Send", true);
+ sendButton.Clicked += SendCommandButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog(title, 60, 13, cancelButton, sendButton);
+ dialog.Add(scrollView);
+ sendButton.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/DiscoverDeviceDialog.cs b/src/ACUConsole/Dialogs/DiscoverDeviceDialog.cs
new file mode 100644
index 00000000..cb7c85cd
--- /dev/null
+++ b/src/ACUConsole/Dialogs/DiscoverDeviceDialog.cs
@@ -0,0 +1,79 @@
+using System;
+using System.IO.Ports;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting device discovery parameters
+ ///
+ public static class DiscoverDeviceDialog
+ {
+ ///
+ /// Shows the discover device dialog and returns user input
+ ///
+ /// Default port name to select
+ /// DiscoverDeviceInput with user's choices
+ public static DiscoverDeviceInput Show(string defaultPortName)
+ {
+ var result = new DiscoverDeviceInput { WasCancelled = true };
+
+ var portNames = SerialPort.GetPortNames();
+ var portNameComboBox = new ComboBox(new Rect(15, 1, 35, 5), portNames);
+
+ // Select default port name
+ if (portNames.Length > 0)
+ {
+ portNameComboBox.SelectedItem = Math.Max(
+ Array.FindIndex(portNames, port =>
+ string.Equals(port, defaultPortName)), 0);
+ }
+ var pingTimeoutTextField = new TextField(25, 3, 25, "1000");
+ var reconnectDelayTextField = new TextField(25, 5, 25, "0");
+
+ void DiscoverButtonClicked()
+ {
+ if (string.IsNullOrEmpty(portNameComboBox.Text.ToString()))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "No port name entered!", "OK");
+ return;
+ }
+
+ if (!int.TryParse(pingTimeoutTextField.Text.ToString(), out var pingTimeout))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid reply timeout entered!", "OK");
+ return;
+ }
+
+ if (!int.TryParse(reconnectDelayTextField.Text.ToString(), out var reconnectDelay))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid reconnect delay entered!", "OK");
+ return;
+ }
+
+ // All validation passed - collect the data
+ result.PortName = portNameComboBox.Text.ToString();
+ result.PingTimeout = pingTimeout;
+ result.ReconnectDelay = reconnectDelay;
+ result.WasCancelled = false;
+ Application.RequestStop();
+ }
+
+ var discoverButton = new Button("Discover", true);
+ discoverButton.Clicked += DiscoverButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += () => Application.RequestStop();
+
+ var dialog = new Dialog("Discover Device", 60, 11, cancelButton, discoverButton);
+ dialog.Add(new Label(1, 1, "Port:"), portNameComboBox,
+ new Label(1, 3, "Ping Timeout(ms):"), pingTimeoutTextField,
+ new Label(1, 5, "Reconnect Delay(ms):"), reconnectDelayTextField);
+ pingTimeoutTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/EncryptionKeySetDialog.cs b/src/ACUConsole/Dialogs/EncryptionKeySetDialog.cs
new file mode 100644
index 00000000..442fc4be
--- /dev/null
+++ b/src/ACUConsole/Dialogs/EncryptionKeySetDialog.cs
@@ -0,0 +1,87 @@
+using System;
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting encryption key set parameters and device selection
+ ///
+ public static class EncryptionKeySetDialog
+ {
+ ///
+ /// Shows the encryption key set dialog and returns user input
+ ///
+ /// Available devices for selection
+ /// Formatted device list for display
+ /// EncryptionKeySetInput with user's choices
+ public static EncryptionKeySetInput Show(DeviceSetting[] devices, string[] deviceList)
+ {
+ var result = new EncryptionKeySetInput { WasCancelled = true };
+
+ // First, collect encryption key
+ var keyTextField = new TextField(1, 3, 35, "");
+
+ void NextButtonClicked()
+ {
+ var keyStr = keyTextField.Text.ToString();
+ if (string.IsNullOrWhiteSpace(keyStr))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Please enter encryption key!", "OK");
+ return;
+ }
+
+ byte[] key;
+ try
+ {
+ key = Convert.FromHexString(keyStr);
+ }
+ catch
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid hex format!", "OK");
+ return;
+ }
+
+ if (key.Length != 16)
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Key must be exactly 16 bytes (32 hex chars)!", "OK");
+ return;
+ }
+
+ Application.RequestStop();
+
+ // Show device selection dialog
+ var deviceSelection = DeviceSelectionDialog.Show("Encryption Key Set", devices, deviceList);
+
+ if (!deviceSelection.WasCancelled)
+ {
+ // All validation passed - collect the data
+ result.EncryptionKey = key;
+ result.DeviceAddress = deviceSelection.SelectedDeviceAddress;
+ result.WasCancelled = false;
+ }
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var sendButton = new Button("Next", true);
+ sendButton.Clicked += NextButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Encryption Key Set", 60, 12, cancelButton, sendButton);
+ dialog.Add(new Label(1, 1, "Encryption Key (16 bytes hex):"), keyTextField,
+ new Label(1, 5, "Example: '0102030405060708090A0B0C0D0E0F10'"));
+ keyTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/FileTransferDialog.cs b/src/ACUConsole/Dialogs/FileTransferDialog.cs
new file mode 100644
index 00000000..a8e35caf
--- /dev/null
+++ b/src/ACUConsole/Dialogs/FileTransferDialog.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting file transfer parameters and device selection
+ ///
+ public static class FileTransferDialog
+ {
+ ///
+ /// Shows the file transfer dialog and returns user input
+ ///
+ /// Available devices for selection
+ /// Formatted device list for display
+ /// FileTransferInput with user's choices
+ public static FileTransferInput Show(DeviceSetting[] devices, string[] deviceList)
+ {
+ var result = new FileTransferInput { WasCancelled = true };
+
+ // First, collect file transfer parameters
+ var typeTextField = new TextField(25, 1, 25, "1");
+ var messageSizeTextField = new TextField(25, 3, 25, "128");
+ var filePathTextField = new TextField(25, 5, 40, "");
+
+ void BrowseFileButtonClicked()
+ {
+ var openDialog = new OpenDialog("Select File to Transfer", "", new List());
+ Application.Run(openDialog);
+
+ if (!openDialog.Canceled && !string.IsNullOrEmpty(openDialog.FilePath?.ToString()))
+ {
+ filePathTextField.Text = openDialog.FilePath.ToString();
+ }
+ }
+
+ void NextButtonClicked()
+ {
+ if (!byte.TryParse(typeTextField.Text.ToString(), out var type))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid type entered!", "OK");
+ return;
+ }
+
+ if (!byte.TryParse(messageSizeTextField.Text.ToString(), out var messageSize))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid message size entered!", "OK");
+ return;
+ }
+
+ var filePath = filePathTextField.Text.ToString();
+ if (string.IsNullOrWhiteSpace(filePath))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Please enter file path!", "OK");
+ return;
+ }
+
+ byte[] fileData;
+ try
+ {
+ if (!File.Exists(filePath))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "File does not exist!", "OK");
+ return;
+ }
+ fileData = File.ReadAllBytes(filePath);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.ErrorQuery(60, 10, "Error", $"Failed to read file: {ex.Message}", "OK");
+ return;
+ }
+
+ Application.RequestStop();
+
+ // Show device selection dialog
+ var deviceSelection = DeviceSelectionDialog.Show("File Transfer", devices, deviceList);
+
+ if (!deviceSelection.WasCancelled)
+ {
+ // All validation passed - collect the data
+ result.Type = type;
+ result.MessageSize = messageSize;
+ result.FilePath = filePath;
+ result.FileData = fileData;
+ result.DeviceAddress = deviceSelection.SelectedDeviceAddress;
+ result.WasCancelled = false;
+ }
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var sendButton = new Button("Next", true);
+ sendButton.Clicked += NextButtonClicked;
+ var browseButton = new Button("Browse");
+ browseButton.Clicked += BrowseFileButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("File Transfer", 80, 15, cancelButton, sendButton);
+ dialog.Add(new Label(1, 1, "Type:"), typeTextField,
+ new Label(1, 3, "Message Size:"), messageSizeTextField,
+ new Label(1, 5, "File Path:"), filePathTextField);
+
+ browseButton.X = Pos.Right(filePathTextField) + 2;
+ browseButton.Y = 5;
+ dialog.Add(browseButton);
+
+ typeTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/ManufacturerSpecificDialog.cs b/src/ACUConsole/Dialogs/ManufacturerSpecificDialog.cs
new file mode 100644
index 00000000..3db29822
--- /dev/null
+++ b/src/ACUConsole/Dialogs/ManufacturerSpecificDialog.cs
@@ -0,0 +1,109 @@
+using System;
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting manufacturer specific command parameters and device selection
+ ///
+ public static class ManufacturerSpecificDialog
+ {
+ ///
+ /// Shows the manufacturer specific command dialog and returns user input
+ ///
+ /// Available devices for selection
+ /// Formatted device list for display
+ /// ManufacturerSpecificInput with user's choices
+ public static ManufacturerSpecificInput Show(DeviceSetting[] devices, string[] deviceList)
+ {
+ var result = new ManufacturerSpecificInput { WasCancelled = true };
+
+ // First, collect manufacturer specific parameters
+ var vendorCodeTextField = new TextField(25, 1, 25, "");
+ var dataTextField = new TextField(25, 3, 40, "");
+
+ void NextButtonClicked()
+ {
+ // Validate vendor code
+ byte[] vendorCode;
+ try
+ {
+ var vendorCodeStr = vendorCodeTextField.Text.ToString();
+ if (string.IsNullOrWhiteSpace(vendorCodeStr))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Please enter vendor code!", "OK");
+ return;
+ }
+ vendorCode = Convert.FromHexString(vendorCodeStr);
+ }
+ catch
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid vendor code hex format!", "OK");
+ return;
+ }
+
+ if (vendorCode.Length != 3)
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Vendor code must be exactly 3 bytes!", "OK");
+ return;
+ }
+
+ // Validate data
+ byte[] data;
+ try
+ {
+ var dataStr = dataTextField.Text.ToString();
+ if (string.IsNullOrWhiteSpace(dataStr))
+ {
+ data = Array.Empty();
+ }
+ else
+ {
+ data = Convert.FromHexString(dataStr);
+ }
+ }
+ catch
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid data hex format!", "OK");
+ return;
+ }
+
+ Application.RequestStop();
+
+ // Show device selection dialog
+ var deviceSelection = DeviceSelectionDialog.Show("Manufacturer Specific", devices, deviceList);
+
+ if (!deviceSelection.WasCancelled)
+ {
+ // All validation passed - collect the data
+ result.VendorCode = vendorCode;
+ result.Data = data;
+ result.DeviceAddress = deviceSelection.SelectedDeviceAddress;
+ result.WasCancelled = false;
+ }
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var nextButton = new Button("Next", true);
+ nextButton.Clicked += NextButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Manufacturer Specific Command", 70, 11, cancelButton, nextButton);
+ dialog.Add(new Label(1, 1, "Vendor Code (hex):"), vendorCodeTextField,
+ new Label(1, 3, "Data (hex):"), dataTextField);
+ vendorCodeTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/OutputControlDialog.cs b/src/ACUConsole/Dialogs/OutputControlDialog.cs
new file mode 100644
index 00000000..321f9b2d
--- /dev/null
+++ b/src/ACUConsole/Dialogs/OutputControlDialog.cs
@@ -0,0 +1,71 @@
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting output control parameters and device selection
+ ///
+ public static class OutputControlDialog
+ {
+ ///
+ /// Shows the output control dialog and returns user input
+ ///
+ /// Available devices for selection
+ /// Formatted device list for display
+ /// OutputControlInput with user's choices
+ public static OutputControlInput Show(DeviceSetting[] devices, string[] deviceList)
+ {
+ var result = new OutputControlInput { WasCancelled = true };
+
+ // First, collect output control parameters
+ var outputNumberTextField = new TextField(25, 1, 25, "0");
+ var activateOutputCheckBox = new CheckBox(1, 3, "Activate Output", false);
+
+ void NextButtonClicked()
+ {
+ // Validate output number
+ if (!byte.TryParse(outputNumberTextField.Text.ToString(), out var outputNumber))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid output number entered!", "OK");
+ return;
+ }
+
+ Application.RequestStop();
+
+ // Show device selection dialog
+ var deviceSelection = DeviceSelectionDialog.Show("Output Control", devices, deviceList);
+
+ if (!deviceSelection.WasCancelled)
+ {
+ // All validation passed - collect the data
+ result.OutputNumber = outputNumber;
+ result.ActivateOutput = activateOutputCheckBox.Checked;
+ result.DeviceAddress = deviceSelection.SelectedDeviceAddress;
+ result.WasCancelled = false;
+ }
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var nextButton = new Button("Next", true);
+ nextButton.Clicked += NextButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Output Control", 60, 10, cancelButton, nextButton);
+ dialog.Add(new Label(1, 1, "Output Number:"), outputNumberTextField,
+ activateOutputCheckBox);
+ outputNumberTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/ParseOSDPCapFileDialog.cs b/src/ACUConsole/Dialogs/ParseOSDPCapFileDialog.cs
new file mode 100644
index 00000000..16c1729e
--- /dev/null
+++ b/src/ACUConsole/Dialogs/ParseOSDPCapFileDialog.cs
@@ -0,0 +1,94 @@
+using System;
+using System.IO;
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for parsing OSDP Cap files with filtering options
+ ///
+ public static class ParseOSDPCapFileDialog
+ {
+ ///
+ /// Shows the parse OSDP cap file dialog and returns user input
+ ///
+ /// The initial directory to show in the file dialog
+ /// ParseOSDPCapFileInput with user's choices
+ public static ParseOSDPCapFileInput Show(string initialDirectory = "")
+ {
+ var result = new ParseOSDPCapFileInput { WasCancelled = true };
+
+ // First, show file selection dialog
+ var openDialog = new OpenDialog("Load OSDPCap File", initialDirectory ?? string.Empty, new() { ".osdpcap" });
+ Application.Run(openDialog);
+
+ if (openDialog.Canceled || !File.Exists(openDialog.FilePath?.ToString()))
+ {
+ return result;
+ }
+
+ var filePath = openDialog.FilePath.ToString();
+
+ // Then show parsing options dialog
+ var addressTextField = new TextField(30, 1, 20, string.Empty);
+ var ignorePollsAndAcksCheckBox = new CheckBox(1, 3, "Ignore Polls And Acks", false);
+ var keyTextField = new TextField(15, 5, 35, Convert.ToHexString(DeviceSetting.DefaultKey));
+
+ void ParseButtonClicked()
+ {
+ byte? address = null;
+ if (!string.IsNullOrWhiteSpace(addressTextField.Text.ToString()))
+ {
+ if (!byte.TryParse(addressTextField.Text.ToString(), out var addr) || addr > 127)
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid address entered!", "OK");
+ return;
+ }
+ address = addr;
+ }
+
+ if (keyTextField.Text != null && keyTextField.Text.Length != 32)
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid key length entered!", "OK");
+ return;
+ }
+
+ byte[] key;
+ try
+ {
+ key = keyTextField.Text != null ? Convert.FromHexString(keyTextField.Text.ToString()!) : null;
+ }
+ catch
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid hex characters!", "OK");
+ return;
+ }
+
+ // All validation passed - collect the data
+ result.FilePath = filePath;
+ result.FilterAddress = address;
+ result.IgnorePollsAndAcks = ignorePollsAndAcksCheckBox.Checked;
+ result.SecureKey = key ?? [];
+ result.WasCancelled = false;
+ Application.RequestStop();
+ }
+
+ var parseButton = new Button("Parse", true);
+ parseButton.Clicked += ParseButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += () => Application.RequestStop();
+
+ var dialog = new Dialog("Parse settings", 60, 13, cancelButton, parseButton);
+ dialog.Add(new Label(1, 1, "Filter Specific Address:"), addressTextField,
+ ignorePollsAndAcksCheckBox,
+ new Label(1, 5, "Secure Key:"), keyTextField);
+ addressTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/ReaderBuzzerControlDialog.cs b/src/ACUConsole/Dialogs/ReaderBuzzerControlDialog.cs
new file mode 100644
index 00000000..f048d973
--- /dev/null
+++ b/src/ACUConsole/Dialogs/ReaderBuzzerControlDialog.cs
@@ -0,0 +1,78 @@
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting reader buzzer control parameters and device selection
+ ///
+ public static class ReaderBuzzerControlDialog
+ {
+ ///
+ /// Shows the reader buzzer control dialog and returns user input
+ ///
+ /// Available devices for selection
+ /// Formatted device list for display
+ /// ReaderBuzzerControlInput with user's choices
+ public static ReaderBuzzerControlInput Show(DeviceSetting[] devices, string[] deviceList)
+ {
+ var result = new ReaderBuzzerControlInput { WasCancelled = true };
+
+ // First, collect buzzer control parameters
+ var readerNumberTextField = new TextField(25, 1, 25, "0");
+ var repeatTimesTextField = new TextField(25, 3, 25, "1");
+
+ void NextButtonClicked()
+ {
+ // Validate reader number
+ if (!byte.TryParse(readerNumberTextField.Text.ToString(), out var readerNumber))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid reader number entered!", "OK");
+ return;
+ }
+
+ // Validate repeat times
+ if (!byte.TryParse(repeatTimesTextField.Text.ToString(), out var repeatTimes))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid repeat times entered!", "OK");
+ return;
+ }
+
+ Application.RequestStop();
+
+ // Show device selection dialog
+ var deviceSelection = DeviceSelectionDialog.Show("Reader Buzzer Control", devices, deviceList);
+
+ if (!deviceSelection.WasCancelled)
+ {
+ // All validation passed - collect the data
+ result.ReaderNumber = readerNumber;
+ result.RepeatTimes = repeatTimes;
+ result.DeviceAddress = deviceSelection.SelectedDeviceAddress;
+ result.WasCancelled = false;
+ }
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var nextButton = new Button("Next", true);
+ nextButton.Clicked += NextButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Reader Buzzer Control", 60, 11, cancelButton, nextButton);
+ dialog.Add(new Label(1, 1, "Reader Number:"), readerNumberTextField,
+ new Label(1, 3, "Repeat Times:"), repeatTimesTextField);
+ readerNumberTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/ReaderLedControlDialog.cs b/src/ACUConsole/Dialogs/ReaderLedControlDialog.cs
new file mode 100644
index 00000000..e4023400
--- /dev/null
+++ b/src/ACUConsole/Dialogs/ReaderLedControlDialog.cs
@@ -0,0 +1,76 @@
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using OSDP.Net.Model.CommandData;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting reader LED control parameters and device selection
+ ///
+ public static class ReaderLedControlDialog
+ {
+ ///
+ /// Shows the reader LED control dialog and returns user input
+ ///
+ /// Available devices for selection
+ /// Formatted device list for display
+ /// ReaderLedControlInput with user's choices
+ public static ReaderLedControlInput Show(DeviceSetting[] devices, string[] deviceList)
+ {
+ var result = new ReaderLedControlInput { WasCancelled = true };
+
+ // First, collect LED control parameters
+ var ledNumberTextField = new TextField(25, 1, 25, "0");
+ var colorComboBox = new ComboBox(new Rect(25, 3, 25, 8), new[] { "Black", "Red", "Green", "Amber", "Blue", "Magenta", "Cyan", "White" })
+ {
+ SelectedItem = 1 // Default to Red
+ };
+
+ void NextButtonClicked()
+ {
+ // Validate LED number
+ if (!byte.TryParse(ledNumberTextField.Text.ToString(), out var ledNumber))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid LED number entered!", "OK");
+ return;
+ }
+
+ var selectedColor = (LedColor)colorComboBox.SelectedItem;
+ Application.RequestStop();
+
+ // Show device selection dialog
+ var deviceSelection = DeviceSelectionDialog.Show("Reader LED Control", devices, deviceList);
+
+ if (!deviceSelection.WasCancelled)
+ {
+ // All validation passed - collect the data
+ result.LedNumber = ledNumber;
+ result.Color = selectedColor;
+ result.DeviceAddress = deviceSelection.SelectedDeviceAddress;
+ result.WasCancelled = false;
+ }
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var nextButton = new Button("Next", true);
+ nextButton.Clicked += NextButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Reader LED Control", 60, 12, cancelButton, nextButton);
+ dialog.Add(new Label(1, 1, "LED Number:"), ledNumberTextField,
+ new Label(1, 3, "Color:"), colorComboBox);
+ ledNumberTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/ReaderTextOutputDialog.cs b/src/ACUConsole/Dialogs/ReaderTextOutputDialog.cs
new file mode 100644
index 00000000..7203ed3b
--- /dev/null
+++ b/src/ACUConsole/Dialogs/ReaderTextOutputDialog.cs
@@ -0,0 +1,79 @@
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting reader text output parameters and device selection
+ ///
+ public static class ReaderTextOutputDialog
+ {
+ ///
+ /// Shows the reader text output dialog and returns user input
+ ///
+ /// Available devices for selection
+ /// Formatted device list for display
+ /// ReaderTextOutputInput with user's choices
+ public static ReaderTextOutputInput Show(DeviceSetting[] devices, string[] deviceList)
+ {
+ var result = new ReaderTextOutputInput { WasCancelled = true };
+
+ // First, collect text output parameters
+ var readerNumberTextField = new TextField(25, 1, 25, "0");
+ var textTextField = new TextField(25, 3, 40, "Hello World");
+
+ void NextButtonClicked()
+ {
+ // Validate reader number
+ if (!byte.TryParse(readerNumberTextField.Text.ToString(), out var readerNumber))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid reader number entered!", "OK");
+ return;
+ }
+
+ // Validate text input
+ var text = textTextField.Text.ToString();
+ if (string.IsNullOrEmpty(text))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Please enter text to display!", "OK");
+ return;
+ }
+
+ Application.RequestStop();
+
+ // Show device selection dialog
+ var deviceSelection = DeviceSelectionDialog.Show("Reader Text Output", devices, deviceList);
+
+ if (!deviceSelection.WasCancelled)
+ {
+ // All validation passed - collect the data
+ result.ReaderNumber = readerNumber;
+ result.Text = text;
+ result.DeviceAddress = deviceSelection.SelectedDeviceAddress;
+ result.WasCancelled = false;
+ }
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var nextButton = new Button("Next", true);
+ nextButton.Clicked += NextButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Reader Text Output", 70, 11, cancelButton, nextButton);
+ dialog.Add(new Label(1, 1, "Reader Number:"), readerNumberTextField,
+ new Label(1, 3, "Text:"), textTextField);
+ readerNumberTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/RemoveDeviceDialog.cs b/src/ACUConsole/Dialogs/RemoveDeviceDialog.cs
new file mode 100644
index 00000000..9696434d
--- /dev/null
+++ b/src/ACUConsole/Dialogs/RemoveDeviceDialog.cs
@@ -0,0 +1,68 @@
+using System.Linq;
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using NStack;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for selecting a device to remove
+ ///
+ public static class RemoveDeviceDialog
+ {
+ ///
+ /// Shows the remove device dialog and returns user selection
+ ///
+ /// List of available devices to remove
+ /// Formatted device list for display
+ /// RemoveDeviceInput with user's choice
+ public static RemoveDeviceInput Show(DeviceSetting[] devices, string[] deviceList)
+ {
+ var result = new RemoveDeviceInput { WasCancelled = true };
+
+ if (deviceList.Length == 0)
+ {
+ MessageBox.ErrorQuery(60, 10, "Information", "No devices to remove.", "OK");
+ return result;
+ }
+
+ var scrollView = new ScrollView(new Rect(6, 1, 50, 6))
+ {
+ ContentSize = new Size(40, deviceList.Length * 2),
+ ShowVerticalScrollIndicator = deviceList.Length > 6,
+ ShowHorizontalScrollIndicator = false
+ };
+
+ var deviceRadioGroup = new RadioGroup(0, 0, deviceList.Select(ustring.Make).ToArray());
+ scrollView.Add(deviceRadioGroup);
+
+ void RemoveDeviceButtonClicked()
+ {
+ var selectedDevice = devices.OrderBy(d => d.Address).ToArray()[deviceRadioGroup.SelectedItem];
+ result.DeviceAddress = selectedDevice.Address;
+ result.WasCancelled = false;
+ Application.RequestStop();
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var removeButton = new Button("Remove", true);
+ removeButton.Clicked += RemoveDeviceButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Remove Device", 60, 13, cancelButton, removeButton);
+ dialog.Add(scrollView);
+ removeButton.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/SerialConnectionDialog.cs b/src/ACUConsole/Dialogs/SerialConnectionDialog.cs
new file mode 100644
index 00000000..d0ec54c0
--- /dev/null
+++ b/src/ACUConsole/Dialogs/SerialConnectionDialog.cs
@@ -0,0 +1,97 @@
+using System;
+using System.IO.Ports;
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting serial connection parameters
+ ///
+ public static class SerialConnectionDialog
+ {
+ ///
+ /// Shows the serial connection dialog and returns user input
+ ///
+ /// Current serial connection settings for defaults
+ /// SerialConnectionInput with user's choices
+ public static SerialConnectionInput Show(SerialConnectionSettings currentSettings)
+ {
+ var result = new SerialConnectionInput { WasCancelled = true };
+
+ var portNameComboBox = CreatePortNameComboBox(15, 1, currentSettings.PortName);
+ var baudRateTextField = new TextField(25, 3, 25, currentSettings.BaudRate.ToString());
+ var replyTimeoutTextField = new TextField(25, 5, 25, currentSettings.ReplyTimeout.ToString());
+
+ void StartConnectionButtonClicked()
+ {
+ // Validate port name
+ if (string.IsNullOrEmpty(portNameComboBox.Text.ToString()))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "No port name entered!", "OK");
+ return;
+ }
+
+ // Validate baud rate
+ if (!int.TryParse(baudRateTextField.Text.ToString(), out var baudRate))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid baud rate entered!", "OK");
+ return;
+ }
+
+ // Validate reply timeout
+ if (!int.TryParse(replyTimeoutTextField.Text.ToString(), out var replyTimeout))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid reply timeout entered!", "OK");
+ return;
+ }
+
+ // All validation passed - collect the data
+ result.PortName = portNameComboBox.Text.ToString();
+ result.BaudRate = baudRate;
+ result.ReplyTimeout = replyTimeout;
+ result.WasCancelled = false;
+
+ Application.RequestStop();
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var startButton = new Button("Start", true);
+ startButton.Clicked += StartConnectionButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Start Serial Connection", 70, 12, cancelButton, startButton);
+ dialog.Add(new Label(1, 1, "Port:"), portNameComboBox,
+ new Label(1, 3, "Baud Rate:"), baudRateTextField,
+ new Label(1, 5, "Reply Timeout(ms):"), replyTimeoutTextField);
+ portNameComboBox.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+
+ private static ComboBox CreatePortNameComboBox(int x, int y, string currentPortName)
+ {
+ var portNames = SerialPort.GetPortNames();
+ var portNameComboBox = new ComboBox(new Rect(x, y, 35, 5), portNames);
+
+ // Select default port name
+ if (portNames.Length > 0)
+ {
+ portNameComboBox.SelectedItem = Math.Max(
+ Array.FindIndex(portNames, port =>
+ string.Equals(port, currentPortName)), 0);
+ }
+
+ return portNameComboBox;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/TcpClientConnectionDialog.cs b/src/ACUConsole/Dialogs/TcpClientConnectionDialog.cs
new file mode 100644
index 00000000..17d356a2
--- /dev/null
+++ b/src/ACUConsole/Dialogs/TcpClientConnectionDialog.cs
@@ -0,0 +1,82 @@
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting TCP client connection parameters
+ ///
+ public static class TcpClientConnectionDialog
+ {
+ ///
+ /// Shows the TCP client connection dialog and returns user input
+ ///
+ /// Current TCP client connection settings for defaults
+ /// TcpClientConnectionInput with user's choices
+ public static TcpClientConnectionInput Show(TcpClientConnectionSettings currentSettings)
+ {
+ var result = new TcpClientConnectionInput { WasCancelled = true };
+
+ var hostTextField = new TextField(15, 1, 35, currentSettings.Host);
+ var portNumberTextField = new TextField(25, 3, 25, currentSettings.PortNumber.ToString());
+ var baudRateTextField = new TextField(25, 5, 25, currentSettings.BaudRate.ToString());
+ var replyTimeoutTextField = new TextField(25, 7, 25, currentSettings.ReplyTimeout.ToString());
+
+ void StartConnectionButtonClicked()
+ {
+ // Validate port number
+ if (!int.TryParse(portNumberTextField.Text.ToString(), out var portNumber))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid port number entered!", "OK");
+ return;
+ }
+
+ // Validate baud rate
+ if (!int.TryParse(baudRateTextField.Text.ToString(), out var baudRate))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid baud rate entered!", "OK");
+ return;
+ }
+
+ // Validate reply timeout
+ if (!int.TryParse(replyTimeoutTextField.Text.ToString(), out var replyTimeout))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid reply timeout entered!", "OK");
+ return;
+ }
+
+ // All validation passed - collect the data
+ result.Host = hostTextField.Text.ToString();
+ result.PortNumber = portNumber;
+ result.BaudRate = baudRate;
+ result.ReplyTimeout = replyTimeout;
+ result.WasCancelled = false;
+
+ Application.RequestStop();
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var startButton = new Button("Start", true);
+ startButton.Clicked += StartConnectionButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Start TCP Client Connection", 60, 15, cancelButton, startButton);
+ dialog.Add(new Label(1, 1, "Host Name:"), hostTextField,
+ new Label(1, 3, "Port Number:"), portNumberTextField,
+ new Label(1, 5, "Baud Rate:"), baudRateTextField,
+ new Label(1, 7, "Reply Timeout(ms):"), replyTimeoutTextField);
+ hostTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Dialogs/TcpServerConnectionDialog.cs b/src/ACUConsole/Dialogs/TcpServerConnectionDialog.cs
new file mode 100644
index 00000000..a4b5c56e
--- /dev/null
+++ b/src/ACUConsole/Dialogs/TcpServerConnectionDialog.cs
@@ -0,0 +1,79 @@
+using ACUConsole.Configuration;
+using ACUConsole.Model.DialogInputs;
+using Terminal.Gui;
+
+namespace ACUConsole.Dialogs
+{
+ ///
+ /// Dialog for collecting TCP server connection parameters
+ ///
+ public static class TcpServerConnectionDialog
+ {
+ ///
+ /// Shows the TCP server connection dialog and returns user input
+ ///
+ /// Current TCP server connection settings for defaults
+ /// TcpServerConnectionInput with user's choices
+ public static TcpServerConnectionInput Show(TcpServerConnectionSettings currentSettings)
+ {
+ var result = new TcpServerConnectionInput { WasCancelled = true };
+
+ var portNumberTextField = new TextField(25, 1, 25, currentSettings.PortNumber.ToString());
+ var baudRateTextField = new TextField(25, 3, 25, currentSettings.BaudRate.ToString());
+ var replyTimeoutTextField = new TextField(25, 5, 25, currentSettings.ReplyTimeout.ToString());
+
+ void StartConnectionButtonClicked()
+ {
+ // Validate port number
+ if (!int.TryParse(portNumberTextField.Text.ToString(), out var portNumber))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid port number entered!", "OK");
+ return;
+ }
+
+ // Validate baud rate
+ if (!int.TryParse(baudRateTextField.Text.ToString(), out var baudRate))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid baud rate entered!", "OK");
+ return;
+ }
+
+ // Validate reply timeout
+ if (!int.TryParse(replyTimeoutTextField.Text.ToString(), out var replyTimeout))
+ {
+ MessageBox.ErrorQuery(40, 10, "Error", "Invalid reply timeout entered!", "OK");
+ return;
+ }
+
+ // All validation passed - collect the data
+ result.PortNumber = portNumber;
+ result.BaudRate = baudRate;
+ result.ReplyTimeout = replyTimeout;
+ result.WasCancelled = false;
+
+ Application.RequestStop();
+ }
+
+ void CancelButtonClicked()
+ {
+ result.WasCancelled = true;
+ Application.RequestStop();
+ }
+
+ var startButton = new Button("Start", true);
+ startButton.Clicked += StartConnectionButtonClicked;
+ var cancelButton = new Button("Cancel");
+ cancelButton.Clicked += CancelButtonClicked;
+
+ var dialog = new Dialog("Start TCP Server Connection", 60, 12, cancelButton, startButton);
+ dialog.Add(new Label(1, 1, "Port Number:"), portNumberTextField,
+ new Label(1, 3, "Baud Rate:"), baudRateTextField,
+ new Label(1, 5, "Reply Timeout(ms):"), replyTimeoutTextField);
+ portNumberTextField.SetFocus();
+
+ Application.Run(dialog);
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/IACUConsolePresenter.cs b/src/ACUConsole/IACUConsolePresenter.cs
new file mode 100644
index 00000000..fc40a058
--- /dev/null
+++ b/src/ACUConsole/IACUConsolePresenter.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using ACUConsole.Configuration;
+using ACUConsole.Model;
+using OSDP.Net.Model.CommandData;
+
+namespace ACUConsole
+{
+ ///
+ /// Interface for ACU Console presenter to enable testing and separation of concerns
+ ///
+ public interface IACUConsolePresenter : IDisposable
+ {
+ // Events
+ event EventHandler MessageReceived;
+ event EventHandler StatusChanged;
+ event EventHandler ConnectionStatusChanged;
+ event EventHandler ErrorOccurred;
+
+ // Properties
+ bool IsConnected { get; }
+ Guid ConnectionId { get; }
+ IReadOnlyList MessageHistory { get; }
+ Settings Settings { get; }
+
+ // Connection Methods
+ Task StartSerialConnection(string portName, int baudRate, int replyTimeout);
+ Task StartTcpServerConnection(int portNumber, int baudRate, int replyTimeout);
+ Task StartTcpClientConnection(string host, int portNumber, int baudRate, int replyTimeout);
+ Task StopConnection();
+
+ // Device Management Methods
+ void AddDevice(string name, byte address, bool useCrc, bool useSecureChannel, byte[] secureChannelKey);
+ void RemoveDevice(byte address);
+ Task DiscoverDevice(string portName, int pingTimeout, int reconnectDelay, CancellationToken cancellationToken = default);
+
+ // Command Methods
+ Task SendDeviceCapabilities(byte address);
+ Task SendIdReport(byte address);
+ Task SendInputStatus(byte address);
+ Task SendLocalStatus(byte address);
+ Task SendOutputStatus(byte address);
+ Task SendReaderStatus(byte address);
+ Task SendCommunicationConfiguration(byte address, byte newAddress, int newBaudRate);
+ Task SendOutputControl(byte address, byte outputNumber, bool activate);
+ Task SendReaderLedControl(byte address, byte ledNumber, LedColor color);
+ Task SendReaderBuzzerControl(byte address, byte readerNumber, byte repeatTimes);
+ Task SendReaderTextOutput(byte address, byte readerNumber, string text);
+ Task SendManufacturerSpecific(byte address, byte[] vendorCode, byte[] data);
+ Task SendEncryptionKeySet(byte address, byte[] key);
+ Task SendBiometricRead(byte address, byte readerNumber, byte type, byte format, byte quality);
+ Task SendBiometricMatch(byte address, byte readerNumber, byte type, byte format, byte qualityThreshold, byte[] templateData);
+ Task SendFileTransfer(byte address, byte type, byte[] data, byte messageSize);
+
+ // Custom Commands
+ Task SendCustomCommand(byte address, CommandData commandData);
+
+ // Configuration Methods
+ void UpdateConnectionSettings(int pollingInterval, bool isTracing);
+ void SaveConfiguration();
+ void LoadConfiguration();
+ void ParseOSDPCapFile(string filePath, byte? filterAddress, bool ignorePollsAndAcks, byte[] key);
+
+ // Utility Methods
+ void ClearHistory();
+ void AddLogMessage(string message);
+ bool CanSendCommand();
+ string[] GetDeviceList();
+ string GetLastOsdpConfigDirectory();
+ }
+
+ public class ConnectionStatusChangedEventArgs : EventArgs
+ {
+ public byte Address { get; init; }
+ public bool IsConnected { get; init; }
+ public bool IsSecureChannelEstablished { get; init; }
+ public string DeviceName { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/IACUConsoleView.cs b/src/ACUConsole/IACUConsoleView.cs
new file mode 100644
index 00000000..d4b4d8c9
--- /dev/null
+++ b/src/ACUConsole/IACUConsoleView.cs
@@ -0,0 +1,67 @@
+using System;
+
+namespace ACUConsole
+{
+ ///
+ /// Interface defining the contract between the View and Presenter in MVP pattern
+ /// The View is responsible for UI rendering and collecting user input
+ ///
+ public interface IACUConsoleView
+ {
+ ///
+ /// Initializes the view and sets up UI components
+ ///
+ void Initialize();
+
+ ///
+ /// Starts the main application loop
+ ///
+ void Run();
+
+ ///
+ /// Shuts down the view and cleans up resources
+ ///
+ void Shutdown();
+
+ ///
+ /// Shows an informational message to the user
+ ///
+ /// Message title
+ /// Message content
+ void ShowInformation(string title, string message);
+
+ ///
+ /// Shows an error message to the user
+ ///
+ /// Error title
+ /// Error message
+ void ShowError(string title, string message);
+
+ ///
+ /// Shows a warning message to the user
+ ///
+ /// Warning title
+ /// Warning message
+ void ShowWarning(string title, string message);
+
+ ///
+ /// Asks the user a yes/no question
+ ///
+ /// Question title
+ /// Question text
+ /// True if user chose Yes, false if No
+ bool AskYesNo(string title, string message);
+
+ ///
+ /// Updates the discover menu item state
+ ///
+ /// New menu item title
+ /// New action to perform when clicked
+ void UpdateDiscoverMenuItem(string title, Action action);
+
+ ///
+ /// Forces a refresh of the message display
+ ///
+ void RefreshMessageDisplay();
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/ACUEvent.cs b/src/ACUConsole/Model/ACUEvent.cs
new file mode 100644
index 00000000..d2c3d44e
--- /dev/null
+++ b/src/ACUConsole/Model/ACUEvent.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace ACUConsole.Model
+{
+ ///
+ /// Represents an event or message in the ACU Console
+ ///
+ public class ACUEvent
+ {
+ public DateTime Timestamp { get; init; } = DateTime.Now;
+ public string Title { get; init; } = string.Empty;
+ public string Message { get; init; } = string.Empty;
+ public ACUEventType Type { get; init; } = ACUEventType.Information;
+ public byte? DeviceAddress { get; init; }
+
+ public override string ToString()
+ {
+ var deviceInfo = DeviceAddress.HasValue ? $" [Device {DeviceAddress}]" : string.Empty;
+ return $"{Timestamp:HH:mm:ss.fff}{deviceInfo} - {Title}: {Message}";
+ }
+ }
+
+ public enum ACUEventType
+ {
+ Information,
+ Warning,
+ Error,
+ DeviceReply,
+ ConnectionStatus,
+ CommandSent
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/AddDeviceInput.cs b/src/ACUConsole/Model/DialogInputs/AddDeviceInput.cs
new file mode 100644
index 00000000..205b1e87
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/AddDeviceInput.cs
@@ -0,0 +1,16 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for add device dialog input
+ ///
+ public class AddDeviceInput
+ {
+ public string Name { get; set; } = string.Empty;
+ public byte Address { get; set; }
+ public bool UseCrc { get; set; }
+ public bool UseSecureChannel { get; set; }
+ public byte[] SecureChannelKey { get; set; } = [];
+ public bool WasCancelled { get; set; }
+ public bool OverwriteExisting { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/BiometricMatchInput.cs b/src/ACUConsole/Model/DialogInputs/BiometricMatchInput.cs
new file mode 100644
index 00000000..7ca0b983
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/BiometricMatchInput.cs
@@ -0,0 +1,16 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Input data for Biometric Match dialog
+ ///
+ public class BiometricMatchInput
+ {
+ public byte ReaderNumber { get; set; }
+ public byte Type { get; set; }
+ public byte Format { get; set; }
+ public byte QualityThreshold { get; set; }
+ public byte[] TemplateData { get; set; } = [];
+ public byte DeviceAddress { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/BiometricReadInput.cs b/src/ACUConsole/Model/DialogInputs/BiometricReadInput.cs
new file mode 100644
index 00000000..9f507c99
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/BiometricReadInput.cs
@@ -0,0 +1,15 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for biometric read dialog input
+ ///
+ public class BiometricReadInput
+ {
+ public byte ReaderNumber { get; set; }
+ public byte Type { get; set; }
+ public byte Format { get; set; }
+ public byte Quality { get; set; }
+ public byte DeviceAddress { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/CommunicationConfigurationInput.cs b/src/ACUConsole/Model/DialogInputs/CommunicationConfigurationInput.cs
new file mode 100644
index 00000000..fa10c067
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/CommunicationConfigurationInput.cs
@@ -0,0 +1,13 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for communication configuration dialog input
+ ///
+ public class CommunicationConfigurationInput
+ {
+ public byte NewAddress { get; set; }
+ public int NewBaudRate { get; set; }
+ public byte DeviceAddress { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/ConnectionSettingsInput.cs b/src/ACUConsole/Model/DialogInputs/ConnectionSettingsInput.cs
new file mode 100644
index 00000000..f297b66b
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/ConnectionSettingsInput.cs
@@ -0,0 +1,12 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for connection settings dialog input
+ ///
+ public class ConnectionSettingsInput
+ {
+ public int PollingInterval { get; set; }
+ public bool IsTracing { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/DeviceSelectionInput.cs b/src/ACUConsole/Model/DialogInputs/DeviceSelectionInput.cs
new file mode 100644
index 00000000..7a281aac
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/DeviceSelectionInput.cs
@@ -0,0 +1,11 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for device selection dialog input
+ ///
+ public class DeviceSelectionInput
+ {
+ public byte SelectedDeviceAddress { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/DiscoverDeviceInput.cs b/src/ACUConsole/Model/DialogInputs/DiscoverDeviceInput.cs
new file mode 100644
index 00000000..99bebef1
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/DiscoverDeviceInput.cs
@@ -0,0 +1,13 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Input data for Discover Device dialog
+ ///
+ public class DiscoverDeviceInput
+ {
+ public string PortName { get; set; } = string.Empty;
+ public int PingTimeout { get; set; }
+ public int ReconnectDelay { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/EncryptionKeySetInput.cs b/src/ACUConsole/Model/DialogInputs/EncryptionKeySetInput.cs
new file mode 100644
index 00000000..cdeb21bf
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/EncryptionKeySetInput.cs
@@ -0,0 +1,12 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Input data for Encryption Key Set dialog
+ ///
+ public class EncryptionKeySetInput
+ {
+ public byte[] EncryptionKey { get; set; } = [];
+ public byte DeviceAddress { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/FileTransferInput.cs b/src/ACUConsole/Model/DialogInputs/FileTransferInput.cs
new file mode 100644
index 00000000..3ad90ffe
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/FileTransferInput.cs
@@ -0,0 +1,15 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Input data for File Transfer dialog
+ ///
+ public class FileTransferInput
+ {
+ public byte Type { get; set; }
+ public byte MessageSize { get; set; }
+ public string FilePath { get; set; } = string.Empty;
+ public byte[] FileData { get; set; } = [];
+ public byte DeviceAddress { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/ManufacturerSpecificInput.cs b/src/ACUConsole/Model/DialogInputs/ManufacturerSpecificInput.cs
new file mode 100644
index 00000000..9ed3c97d
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/ManufacturerSpecificInput.cs
@@ -0,0 +1,13 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for manufacturer specific command dialog input
+ ///
+ public class ManufacturerSpecificInput
+ {
+ public byte[] VendorCode { get; set; } = [];
+ public byte[] Data { get; set; } = [];
+ public byte DeviceAddress { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/OutputControlInput.cs b/src/ACUConsole/Model/DialogInputs/OutputControlInput.cs
new file mode 100644
index 00000000..d33847ef
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/OutputControlInput.cs
@@ -0,0 +1,13 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for output control dialog input
+ ///
+ public class OutputControlInput
+ {
+ public byte OutputNumber { get; set; }
+ public bool ActivateOutput { get; set; }
+ public byte DeviceAddress { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/ParseOSDPCapFileInput.cs b/src/ACUConsole/Model/DialogInputs/ParseOSDPCapFileInput.cs
new file mode 100644
index 00000000..b48cbecf
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/ParseOSDPCapFileInput.cs
@@ -0,0 +1,14 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Input data for Parse OSDP Cap File dialog
+ ///
+ public class ParseOSDPCapFileInput
+ {
+ public string FilePath { get; set; } = string.Empty;
+ public byte? FilterAddress { get; set; }
+ public bool IgnorePollsAndAcks { get; set; }
+ public byte[] SecureKey { get; set; } = [];
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/ReaderBuzzerControlInput.cs b/src/ACUConsole/Model/DialogInputs/ReaderBuzzerControlInput.cs
new file mode 100644
index 00000000..6b281a80
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/ReaderBuzzerControlInput.cs
@@ -0,0 +1,13 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for reader buzzer control dialog input
+ ///
+ public class ReaderBuzzerControlInput
+ {
+ public byte ReaderNumber { get; set; }
+ public byte RepeatTimes { get; set; }
+ public byte DeviceAddress { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/ReaderLedControlInput.cs b/src/ACUConsole/Model/DialogInputs/ReaderLedControlInput.cs
new file mode 100644
index 00000000..1355955f
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/ReaderLedControlInput.cs
@@ -0,0 +1,15 @@
+using OSDP.Net.Model.CommandData;
+
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for reader LED control dialog input
+ ///
+ public class ReaderLedControlInput
+ {
+ public byte LedNumber { get; set; }
+ public LedColor Color { get; set; }
+ public byte DeviceAddress { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/ReaderTextOutputInput.cs b/src/ACUConsole/Model/DialogInputs/ReaderTextOutputInput.cs
new file mode 100644
index 00000000..aec52620
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/ReaderTextOutputInput.cs
@@ -0,0 +1,13 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for reader text output dialog input
+ ///
+ public class ReaderTextOutputInput
+ {
+ public byte ReaderNumber { get; set; }
+ public string Text { get; set; } = string.Empty;
+ public byte DeviceAddress { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/RemoveDeviceInput.cs b/src/ACUConsole/Model/DialogInputs/RemoveDeviceInput.cs
new file mode 100644
index 00000000..7967de14
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/RemoveDeviceInput.cs
@@ -0,0 +1,11 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for remove device dialog input
+ ///
+ public class RemoveDeviceInput
+ {
+ public byte DeviceAddress { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/SerialConnectionInput.cs b/src/ACUConsole/Model/DialogInputs/SerialConnectionInput.cs
new file mode 100644
index 00000000..0c8e05ec
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/SerialConnectionInput.cs
@@ -0,0 +1,13 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for serial connection dialog input
+ ///
+ public class SerialConnectionInput
+ {
+ public string PortName { get; set; } = string.Empty;
+ public int BaudRate { get; set; }
+ public int ReplyTimeout { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/TcpClientConnectionInput.cs b/src/ACUConsole/Model/DialogInputs/TcpClientConnectionInput.cs
new file mode 100644
index 00000000..d5ed5c91
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/TcpClientConnectionInput.cs
@@ -0,0 +1,14 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for TCP client connection dialog input
+ ///
+ public class TcpClientConnectionInput
+ {
+ public string Host { get; set; } = string.Empty;
+ public int PortNumber { get; set; }
+ public int BaudRate { get; set; }
+ public int ReplyTimeout { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Model/DialogInputs/TcpServerConnectionInput.cs b/src/ACUConsole/Model/DialogInputs/TcpServerConnectionInput.cs
new file mode 100644
index 00000000..ae565e08
--- /dev/null
+++ b/src/ACUConsole/Model/DialogInputs/TcpServerConnectionInput.cs
@@ -0,0 +1,13 @@
+namespace ACUConsole.Model.DialogInputs
+{
+ ///
+ /// Data transfer object for TCP server connection dialog input
+ ///
+ public class TcpServerConnectionInput
+ {
+ public int PortNumber { get; set; }
+ public int BaudRate { get; set; }
+ public int ReplyTimeout { get; set; }
+ public bool WasCancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ACUConsole/Program.cs b/src/ACUConsole/Program.cs
new file mode 100644
index 00000000..927351df
--- /dev/null
+++ b/src/ACUConsole/Program.cs
@@ -0,0 +1,50 @@
+using System;
+
+namespace ACUConsole
+{
+ ///
+ /// Main program class for ACU Console using MVP pattern
+ ///
+ internal static class Program
+ {
+ private static ACUConsolePresenter _presenter;
+ private static ACUConsoleView _view;
+
+ private static void Main()
+ {
+ try
+ {
+ // Create presenter (handles business logic)
+ _presenter = new ACUConsolePresenter();
+
+ // Create view (handles UI)
+ _view = new ACUConsoleView(_presenter);
+
+ // Initialize and run the application
+ _view.Initialize();
+ _view.Run();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Fatal error: {ex.Message}");
+ }
+ finally
+ {
+ Cleanup();
+ }
+ }
+
+ private static void Cleanup()
+ {
+ try
+ {
+ _presenter?.Dispose();
+ _view?.Shutdown();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Cleanup error: {ex.Message}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Console/log4net.config b/src/ACUConsole/log4net.config
similarity index 77%
rename from src/Console/log4net.config
rename to src/ACUConsole/log4net.config
index 0e3bb158..1b9bf2c4 100644
--- a/src/Console/log4net.config
+++ b/src/ACUConsole/log4net.config
@@ -1,5 +1,5 @@
-
+
diff --git a/src/Console/Program.cs b/src/Console/Program.cs
deleted file mode 100644
index dc1161c0..00000000
--- a/src/Console/Program.cs
+++ /dev/null
@@ -1,1862 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.IO;
-using System.IO.Ports;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using Console.Commands;
-using Console.Configuration;
-using log4net;
-using log4net.Config;
-using Microsoft.Extensions.Logging;
-using NStack;
-using OSDP.Net;
-using OSDP.Net.Connections;
-using OSDP.Net.Messages;
-using OSDP.Net.Model.CommandData;
-using OSDP.Net.Model.ReplyData;
-using OSDP.Net.PanelCommands.DeviceDiscover;
-using OSDP.Net.Tracing;
-using Terminal.Gui;
-using CommunicationConfiguration = OSDP.Net.Model.CommandData.CommunicationConfiguration;
-using ManufacturerSpecific = OSDP.Net.Model.CommandData.ManufacturerSpecific;
-
-namespace Console;
-
-internal static class Program
-{
- private static ControlPanel _controlPanel;
- private static readonly Queue Messages = new ();
- private static readonly object MessageLock = new ();
-
- private static readonly MenuItem DiscoverMenuItem =
- new MenuItem("_Discover", string.Empty, DiscoverDevice);
- private static readonly MenuBarItem DevicesMenuBarItem =
- new ("_Devices", new[]
- {
- new MenuItem("_Add", string.Empty, AddDevice),
- new MenuItem("_Remove", string.Empty, RemoveDevice),
- DiscoverMenuItem,
- });
-
- private static Guid _connectionId = Guid.Empty;
- private static Window _window;
- private static ScrollView _scrollView;
- private static MenuBar _menuBar;
- private static readonly ConcurrentDictionary LastNak = new ();
-
- private static string _lastConfigFilePath;
- private static string _lastOsdpConfigFilePath;
- private static Settings _settings;
-
- private static async Task Main()
- {
- XmlConfigurator.Configure(
- LogManager.GetRepository(Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly()),
- new FileInfo("log4net.config"));
-
- _lastConfigFilePath = Path.Combine(Environment.CurrentDirectory, "appsettings.config");
- _lastOsdpConfigFilePath = Environment.CurrentDirectory;
-
- var factory = new LoggerFactory();
- factory.AddLog4Net();
-
- _controlPanel = new ControlPanel(factory);
-
- _settings = GetConnectionSettings();
-
- Application.Init();
-
- _window = new Window("OSDP.Net")
- {
- X = 0,
- Y = 1, // Leave one row for the toplevel menu
-
- Width = Dim.Fill(),
- Height = Dim.Fill() - 1
- };
-
- _menuBar = new MenuBar(new[]
- {
- new MenuBarItem("_System", new[]
- {
- new MenuItem("_About", "", () => MessageBox.Query(40, 6,"About",
- $"Version: {Assembly.GetEntryAssembly()?.GetName().Version}",0, "OK")),
- new MenuItem("_Connection Settings", "", UpdateConnectionSettings),
- new MenuItem("_Parse OSDP Cap File", "", ParseOSDPCapFile),
- new MenuItem("_Load Configuration", "", LoadConfigurationSettings),
- new MenuItem("_Save Configuration", "", () => SaveConfigurationSettings(_settings)),
- new MenuItem("_Quit", "", () =>
- {
- SaveConfigurationSettings(_settings);
-
- Application.Shutdown();
- })
- }),
- new MenuBarItem("Co_nnections", new[]
- {
- new MenuItem("Start Serial Connection", "", StartSerialConnection),
- new MenuItem("Start TCP Server Connection", "", StartTcpServerConnection),
- new MenuItem("Start TCP Client Connection", "", StartTcpClientConnection),
- new MenuItem("Stop Connections", "", () =>
- {
- _connectionId = Guid.Empty;
- _ = _controlPanel.Shutdown();
- })
- }),
- DevicesMenuBarItem,
- new MenuBarItem("_Commands", new[]
- {
- new MenuItem("Communication Configuration", "", SendCommunicationConfiguration),
- new MenuItem("Biometric Read", "", SendBiometricReadCommand),
- new MenuItem("Biometric Match", "", SendBiometricMatchCommand),
- new MenuItem("_Device Capabilities", "",
- () => SendCommand("Device capabilities", _connectionId, _controlPanel.DeviceCapabilities)),
- new MenuItem("Encryption Key Set", "", SendEncryptionKeySetCommand),
- new MenuItem("File Transfer", "", SendFileTransferCommand),
- new MenuItem("_ID Report", "",
- () => SendCommand("ID report", _connectionId, _controlPanel.IdReport)),
- new MenuItem("Input Status", "",
- () => SendCommand("Input status", _connectionId, _controlPanel.InputStatus)),
- new MenuItem("_Local Status", "",
- () => SendCommand("Local Status", _connectionId, _controlPanel.LocalStatus)),
- new MenuItem("Manufacturer Specific", "", SendManufacturerSpecificCommand),
- new MenuItem("Output Control", "", SendOutputControlCommand),
- new MenuItem("Output Status", "",
- () => SendCommand("Output status", _connectionId, _controlPanel.OutputStatus)),
- new MenuItem("Reader Buzzer Control", "", SendReaderBuzzerControlCommand),
- new MenuItem("Reader LED Control", "", SendReaderLedControlCommand),
- new MenuItem("Reader Text Output", "", SendReaderTextOutputCommand),
- new MenuItem("_Reader Status", "",
- () => SendCommand("Reader status", _connectionId, _controlPanel.ReaderStatus))
-
- }),
- new MenuBarItem("_Invalid Commands", new[]
- {
- new MenuItem("_Bad CRC/Checksum", "",
- () => SendCustomCommand("Bad CRC/Checksum", _connectionId, _controlPanel.SendCustomCommand,
- new InvalidCrcPollCommand())),
- new MenuItem("Invalid Command Length", "",
- () => SendCustomCommand("Invalid Command Length", _connectionId, _controlPanel.SendCustomCommand,
- new InvalidLengthPollCommand())),
- new MenuItem("Invalid Command", "",
- () => SendCustomCommand("Invalid Command Length", _connectionId, _controlPanel.SendCustomCommand,
- new InvalidCommand()))
- })
- });
-
- Application.Top.Add(_menuBar, _window);
-
-
- _scrollView = new ScrollView(new Rect(0, 0, 0, 0))
- {
- ContentSize = new Size(500, 100),
- ShowVerticalScrollIndicator = true,
- ShowHorizontalScrollIndicator = true
- };
- _window.Add(_scrollView);
-
- RegisterEvents();
-
- try
- {
- Application.Run();
- }
- catch
- {
- // ignored
- }
-
- await _controlPanel.Shutdown();
- }
-
- private static void RegisterEvents()
- {
- _controlPanel.ConnectionStatusChanged += (_, args) =>
- {
- DisplayReceivedReply(
- $"Device '{_settings.Devices.SingleOrDefault(device => device.Address == args.Address, new DeviceSetting() { Name="[Unknown]"}).Name}' " +
- $"at address {args.Address} is now " +
- $"{(args.IsConnected ? (args.IsSecureChannelEstablished ? "connected with secure channel" : "connected with clear text") : "disconnected")}",
- string.Empty);
- };
- _controlPanel.NakReplyReceived += (_, args) =>
- {
- LastNak.TryRemove(args.Address, out var lastNak);
- LastNak.TryAdd(args.Address, args);
- if (lastNak != null && lastNak.Address == args.Address &&
- lastNak.Nak.ErrorCode == args.Nak.ErrorCode)
- {
- return;
- }
-
- AddLogMessage($"!!! Received NAK reply for address {args.Address} !!!{Environment.NewLine}{args.Nak}");
- };
- _controlPanel.LocalStatusReportReplyReceived += (_, args) =>
- {
- DisplayReceivedReply($"Local status updated for address {args.Address}",
- args.LocalStatus.ToString());
- };
- _controlPanel.InputStatusReportReplyReceived += (_, args) =>
- {
- DisplayReceivedReply($"Input status updated for address {args.Address}",
- args.InputStatus.ToString());
- };
- _controlPanel.OutputStatusReportReplyReceived += (_, args) =>
- {
- DisplayReceivedReply($"Output status updated for address {args.Address}",
- args.OutputStatus.ToString());
- };
- _controlPanel.ReaderStatusReportReplyReceived += (_, args) =>
- {
- DisplayReceivedReply($"Reader tamper status updated for address {args.Address}",
- args.ReaderStatus.ToString());
- };
- _controlPanel.RawCardDataReplyReceived += (_, args) =>
- {
- DisplayReceivedReply($"Received raw card data reply for address {args.Address}",
- args.RawCardData.ToString());
- };
- _controlPanel.KeypadReplyReceived += (_, args) =>
- {
- DisplayReceivedReply($"Received keypad data reply for address {args.Address}",
- args.KeypadData.ToString());
- };
- }
-
- private static void StartSerialConnection()
- {
- var portNameComboBox = CreatePortNameComboBox(15, 1);
-
- var baudRateTextField = new TextField(25, 3, 25, _settings.SerialConnectionSettings.BaudRate.ToString());
- var replyTimeoutTextField =
- new TextField(25, 5, 25, _settings.SerialConnectionSettings.ReplyTimeout.ToString());
-
- async void StartConnectionButtonClicked()
- {
- if (string.IsNullOrEmpty(portNameComboBox.Text.ToString()))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "No port name entered!", "OK");
- return;
- }
-
- if (!int.TryParse(baudRateTextField.Text.ToString(), out var baudRate))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid baud rate entered!", "OK");
- return;
- }
-
- if (!int.TryParse(replyTimeoutTextField.Text.ToString(), out var replyTimeout))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid reply timeout entered!", "OK");
- return;
- }
-
- _settings.SerialConnectionSettings.PortName = portNameComboBox.Text.ToString();
- _settings.SerialConnectionSettings.BaudRate = baudRate;
- _settings.SerialConnectionSettings.ReplyTimeout = replyTimeout;
-
- await StartConnection(new SerialPortOsdpConnection(_settings.SerialConnectionSettings.PortName,
- _settings.SerialConnectionSettings.BaudRate)
- { ReplyTimeout = TimeSpan.FromMilliseconds(_settings.SerialConnectionSettings.ReplyTimeout) });
-
- Application.RequestStop();
- }
-
- var startButton = new Button("Start", true);
- startButton.Clicked += StartConnectionButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Start Serial Connection", 70, 12,
- cancelButton, startButton);
- dialog.Add(new Label(1, 1, "Port:"),
- portNameComboBox,
- new Label(1, 3, "Baud Rate:"),
- baudRateTextField,
- new Label(1, 5, "Reply Timeout(ms):"),
- replyTimeoutTextField);
- portNameComboBox.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void StartTcpServerConnection()
- {
- var portNumberTextField =
- new TextField(25, 1, 25, _settings.TcpServerConnectionSettings.PortNumber.ToString());
- var baudRateTextField = new TextField(25, 3, 25, _settings.TcpServerConnectionSettings.BaudRate.ToString());
- var replyTimeoutTextField =
- new TextField(25, 5, 25, _settings.SerialConnectionSettings.ReplyTimeout.ToString());
-
- async void StartConnectionButtonClicked()
- {
- if (!int.TryParse(portNumberTextField.Text.ToString(), out var portNumber))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid port number entered!", "OK");
- return;
- }
-
- if (!int.TryParse(baudRateTextField.Text.ToString(), out var baudRate))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid baud rate entered!", "OK");
- return;
- }
-
- if (!int.TryParse(replyTimeoutTextField.Text.ToString(), out var replyTimeout))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid reply timeout entered!", "OK");
- return;
- }
-
- _settings.TcpServerConnectionSettings.PortNumber = portNumber;
- _settings.TcpServerConnectionSettings.BaudRate = baudRate;
- _settings.TcpServerConnectionSettings.ReplyTimeout = replyTimeout;
-
- await StartConnection(new TcpServerOsdpConnection(_settings.TcpServerConnectionSettings.PortNumber,
- _settings.TcpServerConnectionSettings.BaudRate)
- { ReplyTimeout = TimeSpan.FromMilliseconds(_settings.TcpServerConnectionSettings.ReplyTimeout) });
-
- Application.RequestStop();
- }
-
- var startButton = new Button("Start", true);
- startButton.Clicked += StartConnectionButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Start TCP Server Connection", 60, 12, cancelButton,
- startButton);
- dialog.Add(new Label(1, 1, "Port Number:"),
- portNumberTextField,
- new Label(1, 3, "Baud Rate:"),
- baudRateTextField,
- new Label(1, 5, "Reply Timeout(ms):"),
- replyTimeoutTextField);
- portNumberTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void StartTcpClientConnection()
- {
- var hostTextField = new TextField(15, 1, 35, _settings.TcpClientConnectionSettings.Host);
- var portNumberTextField =
- new TextField(25, 3, 25, _settings.TcpClientConnectionSettings.PortNumber.ToString());
- var baudRateTextField = new TextField(25, 5, 25, _settings.TcpClientConnectionSettings.BaudRate.ToString());
- var replyTimeoutTextField = new TextField(25, 7, 25, _settings.SerialConnectionSettings.ReplyTimeout.ToString());
-
- async void StartConnectionButtonClicked()
- {
- if (!int.TryParse(portNumberTextField.Text.ToString(), out var portNumber))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid port number entered!", "OK");
- return;
- }
-
- if (!int.TryParse(baudRateTextField.Text.ToString(), out var baudRate))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid baud rate entered!", "OK");
- return;
- }
-
- if (!int.TryParse(replyTimeoutTextField.Text.ToString(), out var replyTimeout))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid reply timeout entered!", "OK");
- return;
- }
-
- _settings.TcpClientConnectionSettings.Host = hostTextField.Text.ToString();
- _settings.TcpClientConnectionSettings.BaudRate = baudRate;
- _settings.TcpClientConnectionSettings.PortNumber = portNumber;
- _settings.TcpClientConnectionSettings.ReplyTimeout = replyTimeout;
-
- await StartConnection(new TcpClientOsdpConnection(
- _settings.TcpClientConnectionSettings.Host,
- _settings.TcpClientConnectionSettings.PortNumber,
- _settings.TcpClientConnectionSettings.BaudRate));
-
- Application.RequestStop();
- }
-
- var startButton = new Button("Start", true);
- startButton.Clicked += StartConnectionButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Start TCP Client Connection", 60, 15, cancelButton, startButton);
- dialog.Add(new Label(1, 1, "Host Name:"),
- hostTextField,
- new Label(1, 3, "Port Number:"),
- portNumberTextField,
- new Label(1, 5, "Baud Rate:"),
- baudRateTextField,
- new Label(1, 7, "Reply Timeout(ms):"),
- replyTimeoutTextField);
- hostTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void UpdateConnectionSettings()
- {
- var pollingIntervalTextField = new TextField(25, 4, 25, _settings.PollingInterval.ToString());
- var tracingCheckBox = new CheckBox(1, 6, "Write packet data to file", _settings.IsTracing);
-
- void UpdateConnectionSettingsButtonClicked()
- {
- if (!int.TryParse(pollingIntervalTextField.Text.ToString(), out var pollingInterval))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid polling interval entered!", "OK");
- return;
- }
-
- _settings.PollingInterval = pollingInterval;
- _settings.IsTracing = tracingCheckBox.Checked;
-
- Application.RequestStop();
- }
-
- var updateButton = new Button("Update", true);
- updateButton.Clicked += UpdateConnectionSettingsButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Update Connection Settings", 60, 12, cancelButton, updateButton);
- dialog.Add(new Label(new Rect(1, 1, 55, 2), "Connection will need to be restarted for setting to take effect."),
- new Label(1, 4, "Polling Interval(ms):"),
- pollingIntervalTextField,
- tracingCheckBox);
- pollingIntervalTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void ParseOSDPCapFile()
- {
- string json = ReadJsonFromFile();
-
- if (string.IsNullOrWhiteSpace(json)) return;
-
- ParseEntriesWithSettings(json);
-
- return;
-
- string ReadJsonFromFile()
- {
- var openDialog = new OpenDialog("Load OSDPCap File", string.Empty, new List { ".osdpcap" });
- openDialog.DirectoryPath = ustring.Make(Path.GetDirectoryName(_lastOsdpConfigFilePath));
- openDialog.FilePath = ustring.Make(Path.GetFileName(_lastOsdpConfigFilePath));
-
- Application.Run(openDialog);
-
- string openFilePath = openDialog.FilePath?.ToString() ?? string.Empty;
-
- if (openDialog.Canceled || !File.Exists(openFilePath)) return string.Empty;
-
- _lastOsdpConfigFilePath = openFilePath;
-
- return File.ReadAllText(openFilePath);
- }
- }
-
- private static void ParseEntriesWithSettings(string json)
- {
- var addressTextField = new TextField(30, 1, 20, string.Empty);
- var ignorePollsAndAcksCheckBox = new CheckBox(1, 3, "Ignore Polls And Acks", false);
- var keyTextField = new TextField(15, 5, 35, Convert.ToHexString(DeviceSetting.DefaultKey));
-
- void ParseButtonClicked()
- {
- byte address = 0x00;
- if (!string.IsNullOrWhiteSpace(addressTextField.Text.ToString()) &&
- (!byte.TryParse(addressTextField.Text.ToString(), out address) || address > 127))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid address entered!", "OK");
- return;
- }
-
- if (keyTextField.Text != null && keyTextField.Text.Length != 32)
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid key length entered!", "OK");
- return;
- }
-
- byte[] key = null;
- try
- {
- if (keyTextField.Text != null)
- {
- key = Convert.FromHexString(keyTextField.Text.ToString()!);
- }
- }
- catch
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid hex characters!", "OK");
- return;
- }
-
- StringBuilder builder;
- try
- {
- builder = BuildTextFromEntries(PacketDecoding.OSDPCapParser(json, key).Where(entry =>
- FilterAddress(entry, address) && IgnorePollsAndAcks(entry)));
- }
- catch (Exception exception)
- {
- MessageBox.ErrorQuery(40, 10, "Error", $"Unable to parse. {exception.Message}", "OK");
- return;
- }
-
- var saveDialog = new SaveDialog("Save Parsed File",
- "Successfully completed parsing of file, select location to save file.", new List { ".txt" });
- saveDialog.DirectoryPath = ustring.Make(Path.GetDirectoryName(_lastOsdpConfigFilePath));
- saveDialog.FilePath = ustring.Make(Path.GetFileName(Path.ChangeExtension(_lastOsdpConfigFilePath, ".txt")));
- Application.Run(saveDialog);
-
- string savedFilePath = saveDialog.FilePath?.ToString() ?? string.Empty;
-
- if (string.IsNullOrWhiteSpace(savedFilePath) || saveDialog.Canceled) return;
-
- try
- {
- File.WriteAllText(savedFilePath, builder.ToString());
- }
- catch (Exception exception)
- {
- MessageBox.ErrorQuery(40, 8, "Error", exception.Message, "OK");
- }
-
- Application.RequestStop();
- }
-
- var parseButton = new Button("Parse", true);
- parseButton.Clicked += ParseButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Parse settings", 60, 13, cancelButton, parseButton);
- dialog.Add(new Label(1, 1, "Filter Specific Address:"),
- addressTextField,
- ignorePollsAndAcksCheckBox,
- new Label(1, 5, "Secure Key:"),
- keyTextField);
- addressTextField.SetFocus();
-
- Application.Run(dialog);
-
- return;
-
- StringBuilder BuildTextFromEntries(IEnumerable entries)
- {
- StringBuilder textFromEntries = new StringBuilder();
- DateTime lastEntryTimeStamp = DateTime.MinValue;
- foreach (var osdpCapEntry in entries)
- {
- TimeSpan difference = lastEntryTimeStamp > DateTime.MinValue
- ? osdpCapEntry.TimeStamp - lastEntryTimeStamp
- : TimeSpan.Zero;
- lastEntryTimeStamp = osdpCapEntry.TimeStamp;
- string direction = "Unknown";
- string type = "Unknown";
- if (osdpCapEntry.Packet.CommandType != null)
- {
- direction = "ACU -> PD";
- type = osdpCapEntry.Packet.CommandType.ToString();
- }
- else if (osdpCapEntry.Packet.ReplyType != null)
- {
- direction = "PD -> ACU";
- type = osdpCapEntry.Packet.ReplyType.ToString();
- }
-
- string payloadDataString = string.Empty;
- var payloadData = osdpCapEntry.Packet.ParsePayloadData();
- switch (payloadData)
- {
- case null:
- break;
- case byte[] data:
- payloadDataString = $" {BitConverter.ToString(data)}{Environment.NewLine}";
- break;
- case string data:
- payloadDataString = $" {data}{Environment.NewLine}";
- break;
- default:
- payloadDataString = payloadData.ToString();
- break;
- }
-
- textFromEntries.AppendLine(
- $"{osdpCapEntry.TimeStamp:yy-MM-dd HH:mm:ss.fff} [ {difference:g} ] {direction}: {type}");
- textFromEntries.AppendLine(
- $" Address: {osdpCapEntry.Packet.Address} Sequence: {osdpCapEntry.Packet.Sequence}");
- textFromEntries.AppendLine(payloadDataString);
- }
-
- return textFromEntries;
- }
-
- bool FilterAddress(OSDPCapEntry entry, byte address)
- {
- return string.IsNullOrWhiteSpace(addressTextField.Text.ToString()) || entry.Packet.Address == address;
- }
-
- bool IgnorePollsAndAcks(OSDPCapEntry entry)
- {
- return !ignorePollsAndAcksCheckBox.Checked ||
- (entry.Packet.CommandType != null && entry.Packet.CommandType != CommandType.Poll) ||
- (entry.Packet.ReplyType != null && entry.Packet.ReplyType != ReplyType.Ack);
- }
- }
-
- private static async Task StartConnection(IOsdpConnection osdpConnection)
- {
- LastNak.Clear();
-
- if (_connectionId != Guid.Empty)
- {
- await _controlPanel.Shutdown();
- }
-
- _connectionId =
- _controlPanel.StartConnection(osdpConnection, TimeSpan.FromMilliseconds(_settings.PollingInterval),
- _settings.IsTracing);
-
- foreach (var device in _settings.Devices)
- {
- _controlPanel.AddDevice(_connectionId, device.Address, device.UseCrc, device.UseSecureChannel,
- device.SecureChannelKey);
- }
- }
-
- private static ComboBox CreatePortNameComboBox(int x, int y)
- {
- var portNames = SerialPort.GetPortNames();
- var portNameComboBox = new ComboBox(new Rect(x, y, 35, 5), portNames);
-
- // Select default port name
- if (portNames.Length > 0)
- {
- portNameComboBox.SelectedItem = Math.Max(
- Array.FindIndex(portNames, (port) =>
- String.Equals(port, _settings.SerialConnectionSettings.PortName)), 0);
- }
-
- return portNameComboBox;
- }
-
- private static void DisplayReceivedReply(string title, string message)
- {
- AddLogMessage($"{title}{Environment.NewLine}{message}{Environment.NewLine}{new string('*', 30)}");
- }
-
- public static void AddLogMessage(string message)
- {
- Application.MainLoop.Invoke(() =>
- {
- lock (MessageLock)
- {
- Messages.Enqueue(message);
- while (Messages.Count > 100)
- {
- Messages.Dequeue();
- }
-
- // Not sure why this is here but it is. When the window is not focused, the client area will not
- // get updated but when we return to the window it will also not be updated. And... if the user
- // clicks on the menubar, that is also considered to be "outside" of the window. For now to make
- // output updates work when user is navigating submenus, just adding _menuBar check here
- // -- DXM 2022-11-03
- // p.s. this was a while loop???
- if (!_window.HasFocus && _menuBar.HasFocus)
- {
- return;
- }
-
- _scrollView.Frame = new Rect(1, 0, _window.Frame.Width - 3, _window.Frame.Height - 2);
- _scrollView.RemoveAll();
-
- // This is one hell of an approach in this function. Every time we add a line, we nuke entire view
- // and add a bunch of labels. Is it possible to use something like a TextView set to read-only here
- // instead?
- // -- DXM 2022-11-03
-
- int index = 0;
- foreach (string outputMessage in Messages.Reverse())
- {
- var label = new Label(0, index, outputMessage.TrimEnd());
- index += label.Bounds.Height;
-
- if (outputMessage.Contains("| WARN |") || outputMessage.Contains("NAK"))
- {
- label.ColorScheme = new ColorScheme
- { Normal = Terminal.Gui.Attribute.Make(Color.Black, Color.BrightYellow) };
- }
-
- if (outputMessage.Contains("| ERROR |"))
- {
- label.ColorScheme = new ColorScheme
- { Normal = Terminal.Gui.Attribute.Make(Color.White, Color.BrightRed) };
- }
-
- _scrollView.Add(label);
- }
- }
- });
- }
-
- private static Settings GetConnectionSettings()
- {
- try
- {
- string json = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.config"));
- return JsonSerializer.Deserialize(json);
- }
- catch
- {
- return new Settings();
- }
- }
-
- private static void SaveConfigurationSettings(Settings connectionSettings)
- {
- var saveDialog = new SaveDialog("Save Configuration", string.Empty, new List{".config"});
- saveDialog.DirectoryPath = ustring.Make(Path.GetDirectoryName(_lastConfigFilePath));
- saveDialog.FilePath = ustring.Make(Path.GetFileName(_lastConfigFilePath));
- Application.Run(saveDialog);
-
- string savedFilePath = saveDialog.FilePath?.ToString() ?? string.Empty;
-
- if (string.IsNullOrWhiteSpace(savedFilePath) || saveDialog.Canceled) return;
-
- try
- {
- File.WriteAllText(savedFilePath,JsonSerializer.Serialize(connectionSettings));
- _lastConfigFilePath = savedFilePath;
- MessageBox.Query(40, 6, "Save Configuration", "Save completed successfully", "OK");
- }
- catch (Exception exception)
- {
- MessageBox.ErrorQuery(40, 8, "Error", exception.Message, "OK");
- }
- }
-
- private static void LoadConfigurationSettings()
- {
- var openDialog = new OpenDialog("Load Configuration", string.Empty, new List{".config"});
- openDialog.DirectoryPath = ustring.Make(Path.GetDirectoryName(_lastConfigFilePath));
- openDialog.FilePath = ustring.Make(Path.GetFileName(_lastConfigFilePath));
-
- Application.Run(openDialog);
-
- string openFilePath = openDialog.FilePath?.ToString() ?? string.Empty;
-
- if (openDialog.Canceled || !File.Exists(openFilePath)) return;
-
- try
- {
- string json = File.ReadAllText(openFilePath);
- _settings = JsonSerializer.Deserialize(json);
- _lastConfigFilePath = openFilePath;
- MessageBox.Query(40, 6, "Load Configuration", "Load completed successfully", "OK");
- }
- catch (Exception exception)
- {
- MessageBox.ErrorQuery(40, 8, "Error", exception.Message, "OK");
- }
- }
-
- private static void AddDevice()
- {
- if (_connectionId == Guid.Empty)
- {
- MessageBox.ErrorQuery(60, 12, "Information", "Start a connection before adding devices.", "OK");
- return;
- }
-
- var nameTextField = new TextField(15, 1, 35, string.Empty);
- var addressTextField = new TextField(15, 3, 35, string.Empty);
- var useCrcCheckBox = new CheckBox(1, 5, "Use CRC", true);
- var useSecureChannelCheckBox = new CheckBox(1, 6, "Use Secure Channel", true);
- var keyTextField = new TextField(15, 8, 35, Convert.ToHexString(DeviceSetting.DefaultKey));
-
- void AddDeviceButtonClicked()
- {
- if (!byte.TryParse(addressTextField.Text.ToString(), out var address) || address > 127)
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid address entered!", "OK");
- return;
- }
-
- if (keyTextField.Text == null || keyTextField.Text.Length != 32)
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid key length entered!", "OK");
- return;
- }
-
- byte[] key;
- try
- {
- key = Convert.FromHexString(keyTextField.Text.ToString()!);
- }
- catch
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid hex characters!", "OK");
- return;
- }
-
- if (_settings.Devices.Any(device => device.Address == address))
- {
- if (MessageBox.Query(60, 10, "Overwrite", "Device already exists at that address, overwrite?", 1,
- "No", "Yes") == 0)
- {
- return;
- }
- }
-
- LastNak.TryRemove(address, out _);
- _controlPanel.AddDevice(_connectionId, address, useCrcCheckBox.Checked,
- useSecureChannelCheckBox.Checked, key);
-
- var foundDevice = _settings.Devices.FirstOrDefault(device => device.Address == address);
- if (foundDevice != null)
- {
- _settings.Devices.Remove(foundDevice);
- }
-
- _settings.Devices.Add(new DeviceSetting
- {
- Address = address, Name = nameTextField.Text.ToString(),
- UseSecureChannel = useSecureChannelCheckBox.Checked,
- UseCrc = useCrcCheckBox.Checked,
- SecureChannelKey = key
- });
-
- Application.RequestStop();
- }
-
- var addButton = new Button("Add", true);
- addButton.Clicked += AddDeviceButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Add Device", 60, 13, cancelButton, addButton);
- dialog.Add(new Label(1, 1, "Name:"),
- nameTextField,
- new Label(1, 3, "Address:"),
- addressTextField,
- useCrcCheckBox,
- useSecureChannelCheckBox,
- new Label(1, 8, "Secure Key:"),
- keyTextField);
- nameTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void RemoveDevice()
- {
- if (_connectionId == Guid.Empty)
- {
- MessageBox.ErrorQuery(60, 10, "Information", "Start a connection before removing devices.", "OK");
- return;
- }
-
- var orderedDevices = _settings.Devices.OrderBy(device => device.Address).ToArray();
- var scrollView = new ScrollView(new Rect(6, 1, 50, 6))
- {
- ContentSize = new Size(40, orderedDevices.Length * 2),
- ShowVerticalScrollIndicator = orderedDevices.Length > 6,
- ShowHorizontalScrollIndicator = false
- };
-
- var deviceRadioGroup = new RadioGroup(0, 0,
- orderedDevices.Select(device => ustring.Make($"{device.Address} : {device.Name}")).ToArray());
- scrollView.Add(deviceRadioGroup);
-
- void RemoveDeviceButtonClicked()
- {
- var removedDevice = orderedDevices[deviceRadioGroup.SelectedItem];
- _controlPanel.RemoveDevice(_connectionId, removedDevice.Address);
- LastNak.TryRemove(removedDevice.Address, out _);
- _settings.Devices.Remove(removedDevice);
- Application.RequestStop();
- }
-
- var removeButton = new Button("Remove", true);
- removeButton.Clicked += RemoveDeviceButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Remove Device", 60, 13, cancelButton, removeButton);
- dialog.Add(scrollView);
- removeButton.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void DiscoverDevice()
- {
- var cancelTokenSrc = new CancellationTokenSource();
- var portNameComboBox = CreatePortNameComboBox(15, 1);
- var pingTimeoutTextField = new TextField(25, 3, 25, "1000");
- var reconnectDelayTextField = new TextField(25, 5, 25, "0");
-
- void CloseDialog() => Application.RequestStop();
-
- void OnProgress(DiscoveryResult current)
- {
- string additionalInfo = "";
-
- switch(current.Status)
- {
- case DiscoveryStatus.Started:
- DisplayReceivedReply("Device Discovery Started", String.Empty);
- // NOTE Unlike other statuses, for this one we are intentionally not dropping down
- return;
- case DiscoveryStatus.LookingForDeviceOnConnection:
- additionalInfo = $"{Environment.NewLine} Connection baud rate {current.Connection.BaudRate}...";
- break;
- case DiscoveryStatus.ConnectionWithDeviceFound:
- additionalInfo = $"{Environment.NewLine} Connection baud rate {current.Connection.BaudRate}";
- break;
- case DiscoveryStatus.LookingForDeviceAtAddress:
- additionalInfo = $"{Environment.NewLine} Address {current.Address}...";
- break;
- }
-
- AddLogMessage($"Device Discovery Progress: {current.Status}{additionalInfo}{Environment.NewLine}");
- }
-
- void CancelDiscover()
- {
- cancelTokenSrc?.Cancel();
- cancelTokenSrc?.Dispose();
- cancelTokenSrc = null;
- }
-
- void CompleteDiscover()
- {
- DiscoverMenuItem.Title = "_Discover";
- DiscoverMenuItem.Action = DiscoverDevice;
- }
-
- async void OnClickDiscover()
- {
- if (string.IsNullOrEmpty(portNameComboBox.Text.ToString()))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "No port name entered!", "OK");
- return;
- }
-
- if (!int.TryParse(pingTimeoutTextField.Text.ToString(), out var pingTimeout))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid reply timeout entered!", "OK");
- return;
- }
-
- if (!int.TryParse(reconnectDelayTextField.Text.ToString(), out var reconnectDelay))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid reply timeout entered!", "OK");
- return;
- }
-
- CloseDialog();
-
- try
- {
- DiscoverMenuItem.Title = "Cancel _Discover";
- DiscoverMenuItem.Action = CancelDiscover;
-
- var result = await _controlPanel.DiscoverDevice(
- SerialPortOsdpConnection.EnumBaudRates(portNameComboBox.Text.ToString()),
- new DiscoveryOptions
- {
- ProgressCallback = OnProgress,
- ResponseTimeout = TimeSpan.FromMilliseconds(pingTimeout),
- CancellationToken = cancelTokenSrc.Token,
- ReconnectDelay = TimeSpan.FromMilliseconds(reconnectDelay),
- }.WithDefaultTracer(_settings.IsTracing));
-
- AddLogMessage(result != null
- ? $"Device discovered successfully:{Environment.NewLine}{result}"
- : "Device was not found");
- }
- catch (OperationCanceledException)
- {
- AddLogMessage("Device discovery cancelled");
- }
- catch (Exception exception)
- {
- MessageBox.ErrorQuery(40, 10, "Exception in Device Discovery", exception.Message, "OK");
- AddLogMessage($"Device Discovery Error:{Environment.NewLine}{exception}");
- }
- finally
- {
- CompleteDiscover();
- }
- }
-
- var cancelButton = new Button("Cancel");
- var discoverButton = new Button("Discover", true);
- cancelButton.Clicked += CloseDialog;
- discoverButton.Clicked += OnClickDiscover;
-
- var dialog = new Dialog("Discover Device", 60, 11, cancelButton, discoverButton);
- dialog.Add(new Label(1, 1, "Port:"),
- portNameComboBox,
- new Label(1, 3, "Ping Timeout(ms):"),
- pingTimeoutTextField,
- new Label(1, 5, "Reconnect Delay(ms):"),
- reconnectDelayTextField
- );
- discoverButton.SetFocus();
- Application.Run(dialog);
- }
-
- private static void SendCommunicationConfiguration()
- {
- if (!CanSendCommand()) return;
-
- var addressTextField = new TextField(20, 1, 20,
- ((_settings.Devices.MaxBy(device => device.Address)?.Address ?? 0) + 1).ToString());
- var baudRateTextField = new TextField(20, 3, 20, _settings.SerialConnectionSettings.BaudRate.ToString());
-
- void SendCommunicationConfigurationButtonClicked()
- {
- if (!byte.TryParse(addressTextField.Text.ToString(), out var updatedAddress))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid updated address entered!", "OK");
- return;
- }
-
- if (!int.TryParse(baudRateTextField.Text.ToString(), out var updatedBaudRate))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid updated baud rate entered!", "OK");
- return;
- }
-
- SendCommand("Communication Configuration", _connectionId,
- new CommunicationConfiguration(updatedAddress, updatedBaudRate),
- (connectionId, deviceAddress, communicationConfiguration) => _controlPanel.CommunicationConfiguration(
- connectionId, deviceAddress,
- communicationConfiguration),
- (address, configuration) =>
- {
- if (_settings.SerialConnectionSettings.BaudRate != configuration.BaudRate)
- {
- _settings.SerialConnectionSettings.BaudRate = configuration.BaudRate;
- Application.MainLoop.Invoke(() =>
- {
- MessageBox.Query(40, 10, "Info",
- $"The connection needs to started again with baud rate of {configuration.BaudRate}",
- "OK");
- });
- }
-
- _controlPanel.RemoveDevice(_connectionId, address);
- LastNak.TryRemove(address, out _);
-
- var updatedDevice = _settings.Devices.First(device => device.Address == address);
- updatedDevice.Address = configuration.Address;
- _controlPanel.AddDevice(_connectionId, updatedDevice.Address, updatedDevice.UseCrc,
- updatedDevice.UseSecureChannel, updatedDevice.SecureChannelKey);
- });
-
- Application.RequestStop();
- }
-
- var sendButton = new Button("Send", true);
- sendButton.Clicked += SendCommunicationConfigurationButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Send Communication Configuration Command", 60, 10, cancelButton, sendButton);
- dialog.Add(new Label(1, 1, "Updated Address:"),
- addressTextField,
- new Label(1, 3, "Updated Baud Rate:"),
- baudRateTextField);
- addressTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendFileTransferCommand()
- {
- if (!CanSendCommand()) return;
-
- var typeTextField = new TextField(20, 1, 20, "1");
- var messageSizeTextField = new TextField(20, 3, 20, "128");
-
- void FileTransferButtonClicked()
- {
- if (!byte.TryParse(typeTextField.Text.ToString(), out byte type))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid file transfer type entered!", "OK");
- return;
- }
-
- if (!byte.TryParse(messageSizeTextField.Text.ToString(), out byte messageSize))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid message size entered!", "OK");
- return;
- }
-
- var openDialog = new OpenDialog("File Transfer", "Select file to transfer");
- if (File.Exists(_settings.LastFileTransferDirectory))
- {
- var fileInfo = new FileInfo(_settings.LastFileTransferDirectory);
- openDialog.DirectoryPath = ustring.Make(fileInfo.DirectoryName);
- }
- Application.Run(openDialog);
-
- string path = openDialog.FilePath.ToString() ?? string.Empty;
- if (!File.Exists(path))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "No file selected!", "OK");
- return;
- }
-
- _settings.LastFileTransferDirectory = path;
-
- SendCommand("File Transfer", _connectionId, async (connectionId, address) =>
- {
- var tokenSource = new CancellationTokenSource();
- var cancelFileTransferButton = new Button("Cancel");
- cancelFileTransferButton.Clicked += () =>
- {
- tokenSource.Cancel();
- tokenSource.Dispose();
- Application.RequestStop();
- };
-
- var transferStatusLabel = new Label(new Rect(20, 1, 45, 1), "None");
- var progressBar = new ProgressBar(new Rect(1, 3, 35, 1));
- var progressPercentage = new Label(new Rect(40, 3, 10, 1), "0%");
-
- Application.MainLoop.Invoke(() =>
- {
- var statusDialog = new Dialog("File Transfer Status", 60, 10, cancelFileTransferButton);
- statusDialog.Add(new Label(1, 1, "Status:"),
- transferStatusLabel,
- progressBar,
- progressPercentage);
-
- Application.Run(statusDialog);
- });
-
- var data = await File.ReadAllBytesAsync(path, tokenSource.Token);
- int fileSize = data.Length;
- var result = await _controlPanel.FileTransfer(connectionId, address, type, data, messageSize,
- status =>
- {
- Application.MainLoop.Invoke(() =>
- {
- transferStatusLabel.Text = status?.Status.ToString();
- float percentage = (status?.CurrentOffset ?? 0) / (float) fileSize;
- progressBar.Fraction = percentage;
- progressPercentage.Text = percentage.ToString("P");
-
- if (status?.Status is not (FileTransferStatus.StatusDetail.OkToProceed or FileTransferStatus.StatusDetail.FinishingFileTransfer))
- {
- cancelFileTransferButton.Text = "Close";
- }
- });
- }, tokenSource.Token);
-
- return result > 0;
- });
-
- Application.RequestStop();
- }
-
- var sendButton = new Button("Send", true);
- sendButton.Clicked += FileTransferButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("File Transfer", 60, 10,cancelButton, sendButton);
- dialog.Add(new Label(1, 1, "Type:"),
- typeTextField,
- new Label(1, 3, "Message Size:"),
- messageSizeTextField);
- typeTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendOutputControlCommand()
- {
- if (!CanSendCommand()) return;
-
- var outputAddressTextField = new TextField(20, 1, 20, "0");
- var activateOutputCheckBox = new CheckBox(15, 3, "Activate Output", false);
-
- void SendOutputControlButtonClicked()
- {
- if (!byte.TryParse(outputAddressTextField.Text.ToString(), out var outputNumber))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid output address entered!", "OK");
- return;
- }
-
- SendCommand("Output Control Command", _connectionId, new OutputControls(new[]
- {
- new OutputControl(outputNumber, activateOutputCheckBox.Checked
- ? OutputControlCode.PermanentStateOnAbortTimedOperation
- : OutputControlCode.PermanentStateOffAbortTimedOperation, 0)
- }), _controlPanel.OutputControl, (_, _) => { });
-
- Application.RequestStop();
- }
-
- var sendButton = new Button("Send", true);
- sendButton.Clicked += SendOutputControlButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Send Output Control Command", 60, 10, cancelButton, sendButton);
- dialog.Add( new Label(1, 1, "Output Number:"),
- outputAddressTextField,
- activateOutputCheckBox);
- outputAddressTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendReaderLedControlCommand()
- {
- if (!CanSendCommand()) return;
-
- var ledNumberTextField = new TextField(20, 1, 20, "0");
- var colorComboBox = new ComboBox(new Rect(20, 3, 20, 5), Enum.GetNames(typeof(LedColor))) {Text = "Red"};
-
- void SendReaderLedControlButtonClicked()
- {
- if (!byte.TryParse(ledNumberTextField.Text.ToString(), out var ledNumber))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid LED number entered!", "OK");
- return;
- }
-
- if (!Enum.TryParse(colorComboBox.Text.ToString(), out LedColor color))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid LED color entered!", "OK");
- return;
- }
-
- SendCommand("Reader LED Control Command", _connectionId, new ReaderLedControls(new[]
- {
- new ReaderLedControl(0, ledNumber,
- TemporaryReaderControlCode.CancelAnyTemporaryAndDisplayPermanent, 1, 0,
- LedColor.Red, LedColor.Green, 0,
- PermanentReaderControlCode.SetPermanentState, 1, 0, color, color)
- }), _controlPanel.ReaderLedControl, (_, _) => { });
-
- Application.RequestStop();
- }
-
- var sendButton = new Button("Send");
- sendButton.Clicked += SendReaderLedControlButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Send Reader LED Control Command", 60, 10, cancelButton, sendButton);
- dialog.Add(new Label(1, 1, "LED Number:"),
- ledNumberTextField,
- new Label(1, 3, "Color:"),
- colorComboBox);
- ledNumberTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendManufacturerSpecificCommand()
- {
- if (!CanSendCommand()) return;
-
- var vendorCodeTextField = new TextField(20, 1, 20, string.Empty);
- var dataTextField = new TextField(20, 3, 20, string.Empty);
-
- void SendOutputControlButtonClicked()
- {
- byte[] vendorCode;
- try
- {
- vendorCode = Convert.FromHexString(vendorCodeTextField.Text.ToString() ?? string.Empty);
- }
- catch
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid vendor code entered!", "OK");
- return;
- }
-
- if (vendorCode.Length != 3)
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Vendor code needs to be 3 bytes!", "OK");
- return;
- }
-
- byte[] data;
- try
- {
- data = Convert.FromHexString(dataTextField.Text.ToString() ?? string.Empty);
- }
- catch
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid data entered!", "OK");
- return;
- }
-
- SendCommand("Manufacturer Specific Command", _connectionId,
- new ManufacturerSpecific(vendorCode.ToArray(), data.ToArray()),
- _controlPanel.ManufacturerSpecificCommand, (_, _) => { });
-
- Application.RequestStop();
- }
-
- var sendButton = new Button("Send", true);
- sendButton.Clicked += SendOutputControlButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Send Manufacturer Specific Command (Enter Hex Strings)", 60, 10,
- cancelButton, sendButton);
- dialog.Add(new Label(1, 1, "Vendor Code:"),
- vendorCodeTextField,
- new Label(1, 3, "Data:"),
- dataTextField);
- vendorCodeTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendReaderBuzzerControlCommand()
- {
- if (!CanSendCommand()) return;
-
- var readerAddressTextField = new TextField(20, 1, 20, "0");
- var repeatTimesTextField = new TextField(20, 3, 20, "1");
-
- void SendReaderBuzzerControlButtonClicked()
- {
- if (!byte.TryParse(readerAddressTextField.Text.ToString(), out byte readerNumber))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid reader number entered!", "OK");
- return;
- }
-
- if (!byte.TryParse(repeatTimesTextField.Text.ToString(), out byte repeatNumber))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid repeat number entered!", "OK");
- return;
- }
-
- SendCommand("Reader Buzzer Control Command", _connectionId,
- new ReaderBuzzerControl(readerNumber, ToneCode.Default, 2, 2, repeatNumber),
- _controlPanel.ReaderBuzzerControl, (_, _) => { });
-
- Application.RequestStop();
- }
-
- var sendButton = new Button("Send", true);
- sendButton.Clicked += SendReaderBuzzerControlButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Send Reader Buzzer Control Command", 60, 10, cancelButton, sendButton);
- dialog.Add(new Label(1, 1, "Reader Number:"),
- readerAddressTextField,
- new Label(1, 3, "Repeat Times:"),
- repeatTimesTextField);
- readerAddressTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendBiometricReadCommand()
- {
- if (!CanSendCommand()) return;
-
- var readerAddressTextField = new TextField(20, 1, 20, "0");
- var typeTextField = new TextField(20, 3, 20, "0");
- var formatTextField = new TextField(20, 5, 20, "2");
- var qualityTextField = new TextField(20, 7, 20, "50");
-
- void SendBiometricReadButtonClicked()
- {
- if (!byte.TryParse(readerAddressTextField.Text.ToString(), out byte readerNumber))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid reader number entered!", "OK");
- return;
- }
- if (!byte.TryParse(typeTextField.Text.ToString(), out byte type))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid Bio type number entered!", "OK");
- return;
- }
- if (!byte.TryParse(formatTextField.Text.ToString(), out byte format))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid format number entered!", "OK");
- return;
- }
- if (!byte.TryParse(qualityTextField.Text.ToString(), out byte quality))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid quality number entered!", "OK");
- return;
- }
-
- SendCommand("Biometric Read Command", _connectionId,
- new BiometricReadData(readerNumber, (BiometricType)type, (BiometricFormat)format, quality), TimeSpan.FromSeconds(30),
- // ReSharper disable once AsyncVoidLambda
- _controlPanel.ScanAndSendBiometricData, async (_, result) =>
- {
- DisplayReceivedReply($"Received Bio Read", result.ToString());
-
- if (result.TemplateData.Length > 0)
- {
- await File.WriteAllBytesAsync("BioReadTemplate", result.TemplateData);
- }
- });
-
- Application.RequestStop();
- }
-
- var sendButton = new Button("Send", true);
- sendButton.Clicked += SendBiometricReadButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Biometric Read Command", 60, 12, cancelButton, sendButton);
- dialog.Add(new Label(1, 1, "Reader Number:"),
- readerAddressTextField);
- dialog.Add(new Label(1, 3, "Bio Type:"),
- typeTextField);
- dialog.Add(new Label(1, 5, "Bio Format:"),
- formatTextField);
- dialog.Add(new Label(1, 7, "Quality:"),
- qualityTextField);
- readerAddressTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendBiometricMatchCommand()
- {
- if (!CanSendCommand()) return;
-
- var readerAddressTextField = new TextField(20, 1, 20, "0");
- var typeTextField = new TextField(20, 3, 20, "0");
- var formatTextField = new TextField(20, 5, 20, "2");
- var qualityThresholdTextField = new TextField(20, 7, 20, "50");
-
- void SendBiometricMatchButtonClicked()
- {
- if (!byte.TryParse(readerAddressTextField.Text.ToString(), out byte readerNumber))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid reader number entered!", "OK");
- return;
- }
- if (!byte.TryParse(typeTextField.Text.ToString(), out byte type))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid Bio type number entered!", "OK");
- return;
- }
- if (!byte.TryParse(formatTextField.Text.ToString(), out byte format))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid format number entered!", "OK");
- return;
- }
- if (!byte.TryParse(qualityThresholdTextField.Text.ToString(), out byte qualityThreshold))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid quality threshold number entered!", "OK");
- return;
- }
-
- var openDialog = new OpenDialog("Biometric Match", "Select a template to match");
- openDialog.DirectoryPath = ustring.Make(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
- Application.Run(openDialog);
-
- string path = openDialog.FilePath.ToString() ?? string.Empty;
- if (!File.Exists(path))
- {
- MessageBox.ErrorQuery(40, 10, "Error", "No file selected!", "OK");
- return;
- }
-
- SendCommand("Biometric Match Command", _connectionId,
- new BiometricTemplateData(readerNumber, (BiometricType)type, (BiometricFormat)format,
- qualityThreshold, File.ReadAllBytes(path)), TimeSpan.FromSeconds(30),
- _controlPanel.ScanAndMatchBiometricTemplate, (_, _) => { });
-
- Application.RequestStop();
- }
-
- var sendButton = new Button("Send", true);
- sendButton.Clicked += SendBiometricMatchButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Biometric Match Command", 60, 12, cancelButton, sendButton);
- dialog.Add(new Label(1, 1, "Reader Number:"),
- readerAddressTextField);
- dialog.Add(new Label(1, 3, "Bio Type:"),
- typeTextField);
- dialog.Add(new Label(1, 5, "Bio Format:"),
- formatTextField);
- dialog.Add(new Label(1, 7, "Quality Threshold:"),
- qualityThresholdTextField);
- readerAddressTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendReaderTextOutputCommand()
- {
- if (!CanSendCommand()) return;
-
- var readerAddressTextField = new TextField(20, 1, 20, "0");
- var textOutputTextField = new TextField(20, 3, 20, "Some Text");
-
- void SendReaderTextOutputButtonClicked()
- {
- if (!byte.TryParse(readerAddressTextField.Text.ToString(), out byte readerNumber))
- {
-
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid reader number entered!", "OK");
- return;
- }
-
- SendCommand("Reader Text Output Command", _connectionId,
- new ReaderTextOutput(readerNumber, TextCommand.PermanentTextNoWrap, 0, 1, 1,
- textOutputTextField.Text.ToString()),
- _controlPanel.ReaderTextOutput, (_, _) => { });
-
- Application.RequestStop();
- }
-
- var sendButton = new Button("Send", true);
- sendButton.Clicked += SendReaderTextOutputButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Reader Text Output Command", 60, 10, cancelButton, sendButton);
- dialog.Add(new Label(1, 1, "Reader Number:"),
- readerAddressTextField,
- new Label(1, 3, "Text Output:"),
- textOutputTextField);
- readerAddressTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendEncryptionKeySetCommand()
- {
- if (!CanSendCommand()) return;
-
- var keyTextField = new TextField(20, 1, 32, string.Empty);
-
- void SendButtonClicked()
- {
- if (keyTextField.Text == null || keyTextField.Text.Length != 32)
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid key length entered!", "OK");
- return;
- }
-
- byte[] key;
- try
- {
- key = Convert.FromHexString(keyTextField.Text.ToString()!);
- }
- catch
- {
- MessageBox.ErrorQuery(40, 10, "Error", "Invalid hex characters!", "OK");
- return;
- }
-
- MessageBox.ErrorQuery(40, 10, "Warning", "The new key will be required to access the device in the future. Saving the updated configuration will store the key in clear text.", "OK");
-
- SendCommand("Encryption Key Configuration", _connectionId,
- new EncryptionKeyConfiguration(KeyType.SecureChannelBaseKey, key),
- _controlPanel.EncryptionKeySet,
- (address, result) =>
- {
- if (!result)
- {
- return;
- }
-
- LastNak.TryRemove(address, out _);
-
- var updatedDevice = _settings.Devices.First(device => device.Address == address);
- updatedDevice.UseSecureChannel = true;
- updatedDevice.SecureChannelKey = key;
-
- _controlPanel.AddDevice(_connectionId, updatedDevice.Address, updatedDevice.UseCrc,
- updatedDevice.UseSecureChannel, updatedDevice.SecureChannelKey);
- }, true);
-
- Application.RequestStop();
- }
-
- var sendButton = new Button("Send", true);
- sendButton.Clicked += SendButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog("Encryption Key Set Command (Enter Hex String)", 60, 8, cancelButton, sendButton);
- dialog.Add(new Label(1, 1, "Key:"),
- keyTextField);
- keyTextField.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendCommand(string title, Guid connectionId, Func> sendCommandFunction)
- {
- if (!CanSendCommand()) return;
-
- var deviceSelectionView = CreateDeviceSelectionView(out var orderedDevices, out var deviceRadioGroup);
-
- void SendCommandButtonClicked()
- {
- var selectedDevice = orderedDevices[deviceRadioGroup.SelectedItem];
- byte address = selectedDevice.Address;
- Application.RequestStop();
-
- Task.Run(async () =>
- {
- try
- {
- var result = await sendCommandFunction(connectionId, address);
- AddLogMessage($"{title} for address {address}{Environment.NewLine}{result}{Environment.NewLine}{new string('*', 30)}");
- }
- catch (Exception exception)
- {
- Application.MainLoop.Invoke(() =>
- {
- MessageBox.ErrorQuery(40, 10, $"Error on address {address}", exception.Message,
- "OK");
- });
- }
- });
- }
-
- var sendButton = new Button("Send", true);
- sendButton.Clicked += SendCommandButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog(title, 60, 13, cancelButton, sendButton);
- dialog.Add(deviceSelectionView);
- sendButton.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendCommand(string title, Guid connectionId, TU commandData,
- Func> sendCommandFunction, Action handleResult, bool requireSecurity = false)
- {
- if (!CanSendCommand()) return;
-
- var deviceSelectionView = CreateDeviceSelectionView(out var orderedDevices, out var deviceRadioGroup);
-
- void SendCommandButtonClicked()
- {
- var selectedDevice = orderedDevices[deviceRadioGroup.SelectedItem];
- byte address = selectedDevice.Address;
- Application.RequestStop();
-
- if (requireSecurity && !selectedDevice.UseSecureChannel)
- {
- MessageBox.ErrorQuery(60, 10, "Warning", "Requires secure channel to process this command.", "OK");
- return;
- }
-
- Task.Run(async () =>
- {
- try
- {
- var result = await sendCommandFunction(connectionId, address, commandData);
- AddLogMessage(
- $"{title} for address {address}{Environment.NewLine}{result}{Environment.NewLine}{new string('*', 30)}");
- handleResult(address, result);
- }
- catch (Exception exception)
- {
- Application.MainLoop.Invoke(() =>
- {
- MessageBox.ErrorQuery(40, 10, $"Error on address {address}", exception.Message,
- "OK");
- });
- }
- });
- }
-
- var sendButton = new Button("Send", true);
- sendButton.Clicked += SendCommandButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog(title, 60, 13, cancelButton, sendButton);
- dialog.Add(deviceSelectionView);
- sendButton.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendCommand(string title, Guid connectionId, T2 commandData, T3 timeOut,
- Func> sendCommandFunction, Action handleResult, bool requireSecurity = false)
- {
- if (!CanSendCommand()) return;
-
- var deviceSelectionView = CreateDeviceSelectionView(out var orderedDevices, out var deviceRadioGroup);
-
- void SendCommandButtonClicked()
- {
- var selectedDevice = orderedDevices[deviceRadioGroup.SelectedItem];
- byte address = selectedDevice.Address;
- Application.RequestStop();
-
- if (requireSecurity && !selectedDevice.UseSecureChannel)
- {
- MessageBox.ErrorQuery(60, 10, "Warning", "Requires secure channel to process this command.", "OK");
- return;
- }
-
- Task.Run(async () =>
- {
- try
- {
- var result = await sendCommandFunction(connectionId, address, commandData, timeOut, CancellationToken.None);
- AddLogMessage(
- $"{title} for address {address}{Environment.NewLine}{result}{Environment.NewLine}{new string('*', 30)}");
- handleResult(address, result);
- }
- catch (Exception exception)
- {
- Application.MainLoop.Invoke(() =>
- {
- MessageBox.ErrorQuery(40, 10, $"Error on address {address}", exception.Message,
- "OK");
- });
- }
- });
- }
-
- var sendButton = new Button("Send", true);
- sendButton.Clicked += SendCommandButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog(title, 60, 13, cancelButton, sendButton);
- dialog.Add(deviceSelectionView);
- sendButton.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static void SendCustomCommand(string title, Guid connectionId,
- Func sendCommandFunction, CommandData commandData)
- {
- if (!CanSendCommand()) return;
-
- var deviceSelectionView = CreateDeviceSelectionView(out var orderedDevices, out var deviceRadioGroup);
-
- void SendCommandButtonClicked()
- {
- var selectedDevice = orderedDevices[deviceRadioGroup.SelectedItem];
- byte address = selectedDevice.Address;
- Application.RequestStop();
-
- Task.Run(async () =>
- {
- try
- {
- await sendCommandFunction(connectionId, address, commandData);
- }
- catch (Exception exception)
- {
- Application.MainLoop.Invoke(() =>
- {
- MessageBox.ErrorQuery(40, 10, $"Error on address {address}", exception.Message,
- "OK");
- });
- }
- });
- }
-
- var sendButton = new Button("Send");
- sendButton.Clicked += SendCommandButtonClicked;
- var cancelButton = new Button("Cancel");
- cancelButton.Clicked += () => Application.RequestStop();
-
- var dialog = new Dialog(title, 60, 13, cancelButton, sendButton);
- dialog.Add(deviceSelectionView);
- sendButton.SetFocus();
-
- Application.Run(dialog);
- }
-
- private static ScrollView CreateDeviceSelectionView(out DeviceSetting[] orderedDevices,
- out RadioGroup deviceRadioGroup)
- {
- orderedDevices = _settings.Devices.OrderBy(device => device.Address).ToArray();
- var scrollView = new ScrollView(new Rect(6, 1, 50, 6))
- {
- ContentSize = new Size(40, orderedDevices.Length * 2),
- ShowVerticalScrollIndicator = orderedDevices.Length > 6,
- ShowHorizontalScrollIndicator = false
- };
-
- deviceRadioGroup = new RadioGroup(0, 0,
- orderedDevices.Select(device => ustring.Make($"{device.Address} : {device.Name}")).ToArray())
- {
- SelectedItem = 0
- };
- scrollView.Add(deviceRadioGroup);
- return scrollView;
- }
-
- private static bool CanSendCommand()
- {
- if (_connectionId == Guid.Empty)
- {
- MessageBox.ErrorQuery(60, 10, "Warning", "Start a connection before sending commands.", "OK");
- return false;
- }
-
- if (_settings.Devices.Count == 0)
- {
- MessageBox.ErrorQuery(60, 10, "Warning", "Add a device before sending commands.", "OK");
- return false;
- }
-
- return true;
- }
-}
\ No newline at end of file
diff --git a/src/OSDP.Net.Tests/ControlPanelTest.cs b/src/OSDP.Net.Tests/ControlPanelTest.cs
index 92e886ae..2a3872c2 100644
--- a/src/OSDP.Net.Tests/ControlPanelTest.cs
+++ b/src/OSDP.Net.Tests/ControlPanelTest.cs
@@ -17,6 +17,7 @@
namespace OSDP.Net.Tests
{
[TestFixture]
+ [Category("ComponentTest")]
public class ControlPanelTest
{
[Test]
diff --git a/src/OSDP.Net.Tests/IntegrationTests/IntegrationTestFixtureBase.cs b/src/OSDP.Net.Tests/IntegrationTests/IntegrationTestFixtureBase.cs
index ac76aa60..f6ac96d1 100644
--- a/src/OSDP.Net.Tests/IntegrationTests/IntegrationTestFixtureBase.cs
+++ b/src/OSDP.Net.Tests/IntegrationTests/IntegrationTestFixtureBase.cs
@@ -133,7 +133,7 @@ protected void InitTestTargetDevice(
DeviceAddress = deviceConfig.Address;
TargetDevice = new TestDevice(deviceConfig, LoggerFactory);
- TargetDevice.StartListening(new TcpOsdpServer(6000, baudRate, LoggerFactory));
+ TargetDevice.StartListening(new TcpConnectionListener(6000, baudRate, LoggerFactory));
}
protected void AddDeviceToPanel(
diff --git a/src/OSDP.Net.Tests/IntegrationTests/SecurityTests.cs b/src/OSDP.Net.Tests/IntegrationTests/SecurityTests.cs
index 88a23d52..e54cad11 100644
--- a/src/OSDP.Net.Tests/IntegrationTests/SecurityTests.cs
+++ b/src/OSDP.Net.Tests/IntegrationTests/SecurityTests.cs
@@ -4,6 +4,7 @@
namespace OSDP.Net.Tests.IntegrationTests
{
+ [Category("Integration")]
public class SecurityTests : IntegrationTestFixtureBase
{
[Test]
diff --git a/src/OSDP.Net.Tests/Messages/CommandOutgoingMessageTest.cs b/src/OSDP.Net.Tests/Messages/CommandOutgoingMessageTest.cs
index 8032cd14..6636ca55 100644
--- a/src/OSDP.Net.Tests/Messages/CommandOutgoingMessageTest.cs
+++ b/src/OSDP.Net.Tests/Messages/CommandOutgoingMessageTest.cs
@@ -8,6 +8,7 @@
namespace OSDP.Net.Tests.Messages;
[TestFixture]
+[Category("Unit")]
internal class CommandOutgoingMessageTest
{
[TestCaseSource(typeof(CommunicationConfigurationBuildMessageTestClass),
diff --git a/src/OSDP.Net.Tests/Messages/ControlTest.cs b/src/OSDP.Net.Tests/Messages/ControlTest.cs
index 9b9979d4..ddea2568 100644
--- a/src/OSDP.Net.Tests/Messages/ControlTest.cs
+++ b/src/OSDP.Net.Tests/Messages/ControlTest.cs
@@ -4,6 +4,7 @@
namespace OSDP.Net.Tests.Messages
{
[TestFixture]
+ [Category("Unit")]
public class ControlTest
{
[TestCase(0, true, false, ExpectedResult = 0x04)]
diff --git a/src/OSDP.Net.Tests/Messages/MessageTest.cs b/src/OSDP.Net.Tests/Messages/MessageTest.cs
index 2ad24ae1..b2eff3a8 100644
--- a/src/OSDP.Net.Tests/Messages/MessageTest.cs
+++ b/src/OSDP.Net.Tests/Messages/MessageTest.cs
@@ -2,11 +2,13 @@
using System.Linq;
using NUnit.Framework;
using OSDP.Net.Messages;
+using OSDP.Net.Messages.SecureChannel;
using OSDP.Net.Utilities;
namespace OSDP.Net.Tests.Messages
{
[TestFixture]
+ [Category("Unit")]
public class MessageTest
{
[Test]
@@ -58,8 +60,8 @@ public void CalculateMaximumMessageSize_Encrypted()
"80-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00")]
public string PadThisData(string buffer)
{
- return BitConverter.ToString(Message.PadTheData(BinaryUtils.HexToBytes(buffer).ToArray(), 16,
- Message.FirstPaddingByte));
+ var channel = new ACUMessageSecureChannel();
+ return BitConverter.ToString(channel.PadTheData(BinaryUtils.HexToBytes(buffer).ToArray()).ToArray());
}
}
}
\ No newline at end of file
diff --git a/src/OSDP.Net.Tests/Messages/ReplyTest.cs b/src/OSDP.Net.Tests/Messages/ReplyTest.cs
index b404f1f6..47c1ca90 100644
--- a/src/OSDP.Net.Tests/Messages/ReplyTest.cs
+++ b/src/OSDP.Net.Tests/Messages/ReplyTest.cs
@@ -7,6 +7,7 @@
namespace OSDP.Net.Tests.Messages
{
[TestFixture]
+ [Category("Unit")]
public class ReplyTest
{
[Test]
diff --git a/src/OSDP.Net.Tests/Messages/SecureChannel/SecureContextTest.cs b/src/OSDP.Net.Tests/Messages/SecureChannel/SecureContextTest.cs
index 44a230f8..4bf6fe55 100644
--- a/src/OSDP.Net.Tests/Messages/SecureChannel/SecureContextTest.cs
+++ b/src/OSDP.Net.Tests/Messages/SecureChannel/SecureContextTest.cs
@@ -4,6 +4,7 @@
namespace OSDP.Net.Tests.Messages.SecureChannel
{
[TestFixture]
+ [Category("Unit")]
public class SecureContextTest
{
[Test]
diff --git a/src/OSDP.Net.Tests/Model/CommandData/BiometricReadDataTest.cs b/src/OSDP.Net.Tests/Model/CommandData/BiometricReadDataTest.cs
index 7ffc53d1..365b0df5 100644
--- a/src/OSDP.Net.Tests/Model/CommandData/BiometricReadDataTest.cs
+++ b/src/OSDP.Net.Tests/Model/CommandData/BiometricReadDataTest.cs
@@ -5,6 +5,7 @@
namespace OSDP.Net.Tests.Model.CommandData;
+[Category("Unit")]
internal class BiometricReadDataTest
{
private byte[] TestData => [0x00, 0x07, 0x02, 0x04];
diff --git a/src/OSDP.Net.Tests/Model/CommandData/BiometricTemplateDataTest.cs b/src/OSDP.Net.Tests/Model/CommandData/BiometricTemplateDataTest.cs
index 2e206c62..28d9a6fc 100644
--- a/src/OSDP.Net.Tests/Model/CommandData/BiometricTemplateDataTest.cs
+++ b/src/OSDP.Net.Tests/Model/CommandData/BiometricTemplateDataTest.cs
@@ -5,6 +5,7 @@
namespace OSDP.Net.Tests.Model.CommandData;
+[Category("Unit")]
internal class BiometricTemplateDataTest
{
private byte[] TestData => [0x00, 0x07, 0x02, 0x04, 0x03, 0x00, 0x01, 0x02, 0x05];
diff --git a/src/OSDP.Net.Tests/Model/CommandData/CommunicationConfigurationTest.cs b/src/OSDP.Net.Tests/Model/CommandData/CommunicationConfigurationTest.cs
index b2748a4a..838b84d3 100644
--- a/src/OSDP.Net.Tests/Model/CommandData/CommunicationConfigurationTest.cs
+++ b/src/OSDP.Net.Tests/Model/CommandData/CommunicationConfigurationTest.cs
@@ -5,6 +5,7 @@
namespace OSDP.Net.Tests.Model.CommandData;
+[Category("Unit")]
internal class CommunicationConfigurationTest
{
private byte[] TestData => [0x02, 0x80, 0x25, 0x00, 0x00];
diff --git a/src/OSDP.Net.Tests/Model/CommandData/EncryptionKeyConfigurationTest.cs b/src/OSDP.Net.Tests/Model/CommandData/EncryptionKeyConfigurationTest.cs
index eceea7a7..f18252a3 100644
--- a/src/OSDP.Net.Tests/Model/CommandData/EncryptionKeyConfigurationTest.cs
+++ b/src/OSDP.Net.Tests/Model/CommandData/EncryptionKeyConfigurationTest.cs
@@ -5,6 +5,7 @@
namespace OSDP.Net.Tests.Model.CommandData;
+[Category("Unit")]
internal class EncryptionKeyConfigurationTest
{
private byte[] TestData =>
diff --git a/src/OSDP.Net.Tests/Model/CommandData/FileTransferFragmentTest.cs b/src/OSDP.Net.Tests/Model/CommandData/FileTransferFragmentTest.cs
index d85aaed0..21aa2139 100644
--- a/src/OSDP.Net.Tests/Model/CommandData/FileTransferFragmentTest.cs
+++ b/src/OSDP.Net.Tests/Model/CommandData/FileTransferFragmentTest.cs
@@ -5,6 +5,7 @@
namespace OSDP.Net.Tests.Model.CommandData;
+[Category("Unit")]
internal class FileTransferFragmentTest
{
private byte[] TestData =>
diff --git a/src/OSDP.Net.Tests/Model/CommandData/GetPIVDataTest.cs b/src/OSDP.Net.Tests/Model/CommandData/GetPIVDataTest.cs
index 3b1cf36d..6eedf0e6 100644
--- a/src/OSDP.Net.Tests/Model/CommandData/GetPIVDataTest.cs
+++ b/src/OSDP.Net.Tests/Model/CommandData/GetPIVDataTest.cs
@@ -7,6 +7,7 @@
namespace OSDP.Net.Tests.Model.CommandData
{
+ [Category("Unit")]
internal class GetPIVDataTest
{
private byte[] TestData => [0x5F, 0xC1, 0x02, 0x01, 0x19, 0x00];
diff --git a/src/OSDP.Net.Tests/Model/CommandData/ManufacturerSpecificTest.cs b/src/OSDP.Net.Tests/Model/CommandData/ManufacturerSpecificTest.cs
index c0701d6f..7952d77f 100644
--- a/src/OSDP.Net.Tests/Model/CommandData/ManufacturerSpecificTest.cs
+++ b/src/OSDP.Net.Tests/Model/CommandData/ManufacturerSpecificTest.cs
@@ -5,6 +5,7 @@
namespace OSDP.Net.Tests.Model.CommandData;
+[Category("Unit")]
internal class ManufacturerSpecificTest
{
private byte[] TestData =>
diff --git a/src/OSDP.Net.Tests/Model/CommandData/OutputControlTest.cs b/src/OSDP.Net.Tests/Model/CommandData/OutputControlTest.cs
index fc03cff5..0e7c62b5 100644
--- a/src/OSDP.Net.Tests/Model/CommandData/OutputControlTest.cs
+++ b/src/OSDP.Net.Tests/Model/CommandData/OutputControlTest.cs
@@ -6,6 +6,7 @@
namespace OSDP.Net.Tests.Model.CommandData
{
+ [Category("Unit")]
public class OutputControlTest
{
[Test]
diff --git a/src/OSDP.Net.Tests/Model/CommandData/OutputControlsTest.cs b/src/OSDP.Net.Tests/Model/CommandData/OutputControlsTest.cs
index 11c021d5..f7e34159 100644
--- a/src/OSDP.Net.Tests/Model/CommandData/OutputControlsTest.cs
+++ b/src/OSDP.Net.Tests/Model/CommandData/OutputControlsTest.cs
@@ -7,6 +7,7 @@
namespace OSDP.Net.Tests.Model.CommandData;
[TestFixture]
+[Category("Unit")]
public class OutputControlsTest
{
private byte[] TestData => [0x03, 0x01, 0xC4, 0x09, 0x05, 0x04, 0x10, 0x27];
diff --git a/src/OSDP.Net.Tests/Model/CommandData/ReaderBuzzerControlTest.cs b/src/OSDP.Net.Tests/Model/CommandData/ReaderBuzzerControlTest.cs
index 43a89a81..60bfb963 100644
--- a/src/OSDP.Net.Tests/Model/CommandData/ReaderBuzzerControlTest.cs
+++ b/src/OSDP.Net.Tests/Model/CommandData/ReaderBuzzerControlTest.cs
@@ -5,6 +5,7 @@
namespace OSDP.Net.Tests.Model.CommandData
{
+ [Category("Unit")]
internal class ReaderBuzzerControlTest
{
private byte[] TestData => [0x00, 0x02, 0x05, 0x02, 0x01];
diff --git a/src/OSDP.Net.Tests/Model/CommandData/ReaderLedControlsTest.cs b/src/OSDP.Net.Tests/Model/CommandData/ReaderLedControlsTest.cs
index 020177d7..ddc39644 100644
--- a/src/OSDP.Net.Tests/Model/CommandData/ReaderLedControlsTest.cs
+++ b/src/OSDP.Net.Tests/Model/CommandData/ReaderLedControlsTest.cs
@@ -7,6 +7,7 @@
namespace OSDP.Net.Tests.Model.CommandData;
[TestFixture]
+[Category("Unit")]
public class ReaderLedControlsTest
{
private byte[] TestData => [0x02, 0x03, 0x02, 0x01, 0x02, 0x06, 0x00, 0x04, 0x00, 0x01, 0x02, 0x06, 0x04, 0x03,
diff --git a/src/OSDP.Net.Tests/Model/CommandData/ReaderTextOutputTest.cs b/src/OSDP.Net.Tests/Model/CommandData/ReaderTextOutputTest.cs
index b7697c90..9e2ff785 100644
--- a/src/OSDP.Net.Tests/Model/CommandData/ReaderTextOutputTest.cs
+++ b/src/OSDP.Net.Tests/Model/CommandData/ReaderTextOutputTest.cs
@@ -6,6 +6,7 @@
namespace OSDP.Net.Tests.Model.CommandData
{
[TestFixture]
+ [Category("Unit")]
internal class ReaderTextOutputTest
{
private byte[] TestData =>
diff --git a/src/OSDP.Net.Tests/Model/ReplyData/BiometricReadResultTest.cs b/src/OSDP.Net.Tests/Model/ReplyData/BiometricReadResultTest.cs
index 18ad2a63..7eddd871 100644
--- a/src/OSDP.Net.Tests/Model/ReplyData/BiometricReadResultTest.cs
+++ b/src/OSDP.Net.Tests/Model/ReplyData/BiometricReadResultTest.cs
@@ -6,6 +6,7 @@
namespace OSDP.Net.Tests.Model.ReplyData;
+[Category("Unit")]
public class BiometricReadResultTest
{
[Test]
diff --git a/src/OSDP.Net.Tests/Model/ReplyData/ChallengeResponseTest.cs b/src/OSDP.Net.Tests/Model/ReplyData/ChallengeResponseTest.cs
index 590b3abb..247ea7e6 100644
--- a/src/OSDP.Net.Tests/Model/ReplyData/ChallengeResponseTest.cs
+++ b/src/OSDP.Net.Tests/Model/ReplyData/ChallengeResponseTest.cs
@@ -6,6 +6,7 @@
namespace OSDP.Net.Tests.Model.ReplyData
{
+ [Category("Unit")]
internal class ChallengeResponseTest
{
[Test]
diff --git a/src/OSDP.Net.Tests/Model/ReplyData/CommunicationConfigurationTest.cs b/src/OSDP.Net.Tests/Model/ReplyData/CommunicationConfigurationTest.cs
index 5bf60f59..7c40a368 100644
--- a/src/OSDP.Net.Tests/Model/ReplyData/CommunicationConfigurationTest.cs
+++ b/src/OSDP.Net.Tests/Model/ReplyData/CommunicationConfigurationTest.cs
@@ -6,6 +6,7 @@
namespace OSDP.Net.Tests.Model.ReplyData
{
[TestFixture]
+ [Category("Unit")]
public class CommunicationConfigurationTest
{
[Test]
diff --git a/src/OSDP.Net.Tests/Model/ReplyData/DeviceCapabilitiesTest.cs b/src/OSDP.Net.Tests/Model/ReplyData/DeviceCapabilitiesTest.cs
index 6436641b..249400ee 100644
--- a/src/OSDP.Net.Tests/Model/ReplyData/DeviceCapabilitiesTest.cs
+++ b/src/OSDP.Net.Tests/Model/ReplyData/DeviceCapabilitiesTest.cs
@@ -8,6 +8,7 @@
namespace OSDP.Net.Tests.Model.ReplyData
{
[TestFixture]
+ [Category("Unit")]
public class DeviceCapabilitiesTest
{
private readonly byte[] _rawCapsFromDennisBrivoKeypad = new byte[] {
diff --git a/src/OSDP.Net.Tests/Model/ReplyData/FormattedCardDataTest.cs b/src/OSDP.Net.Tests/Model/ReplyData/FormattedCardDataTest.cs
index ea23c72d..c0b50f67 100644
--- a/src/OSDP.Net.Tests/Model/ReplyData/FormattedCardDataTest.cs
+++ b/src/OSDP.Net.Tests/Model/ReplyData/FormattedCardDataTest.cs
@@ -4,6 +4,7 @@
namespace OSDP.Net.Tests.Model.ReplyData;
+[Category("Unit")]
public class FormattedCardDataTest
{
[Test]
diff --git a/src/OSDP.Net.Tests/Model/ReplyData/KeypadDataTest.cs b/src/OSDP.Net.Tests/Model/ReplyData/KeypadDataTest.cs
index 9cb81a31..3f6ae25d 100644
--- a/src/OSDP.Net.Tests/Model/ReplyData/KeypadDataTest.cs
+++ b/src/OSDP.Net.Tests/Model/ReplyData/KeypadDataTest.cs
@@ -4,6 +4,7 @@
namespace OSDP.Net.Tests.Model.ReplyData
{
+ [Category("Unit")]
public class KeypadDataTest
{
[Test]
diff --git a/src/OSDP.Net.Tests/Model/ReplyData/PIVDataTest.cs b/src/OSDP.Net.Tests/Model/ReplyData/PIVDataTest.cs
index efe25ad7..1b133fe1 100644
--- a/src/OSDP.Net.Tests/Model/ReplyData/PIVDataTest.cs
+++ b/src/OSDP.Net.Tests/Model/ReplyData/PIVDataTest.cs
@@ -5,6 +5,7 @@
namespace OSDP.Net.Tests.Model.ReplyData
{
+ [Category("Unit")]
public class PIVDataTest
{
[Test]
diff --git a/src/OSDP.Net.Tests/Model/ReplyData/RawCardDataTest.cs b/src/OSDP.Net.Tests/Model/ReplyData/RawCardDataTest.cs
index 60743918..de5d1a8b 100644
--- a/src/OSDP.Net.Tests/Model/ReplyData/RawCardDataTest.cs
+++ b/src/OSDP.Net.Tests/Model/ReplyData/RawCardDataTest.cs
@@ -7,6 +7,7 @@
namespace OSDP.Net.Tests.Model.ReplyData
{
[TestFixture]
+ [Category("Unit")]
public class RawCardDataTest
{
[Test]
diff --git a/src/OSDP.Net.Tests/Tracing/PacketDecodingTest.cs b/src/OSDP.Net.Tests/Tracing/PacketDecodingTest.cs
index b54d84b4..90a58f3c 100644
--- a/src/OSDP.Net.Tests/Tracing/PacketDecodingTest.cs
+++ b/src/OSDP.Net.Tests/Tracing/PacketDecodingTest.cs
@@ -11,6 +11,7 @@
namespace OSDP.Net.Tests.Tracing;
[TestFixture]
+[Category("Unit")]
public class PacketDecodingTest
{
[Test]
diff --git a/src/OSDP.Net.Tests/Utilities/BinaryTest.cs b/src/OSDP.Net.Tests/Utilities/BinaryTest.cs
index 68581701..647b0701 100644
--- a/src/OSDP.Net.Tests/Utilities/BinaryTest.cs
+++ b/src/OSDP.Net.Tests/Utilities/BinaryTest.cs
@@ -5,6 +5,7 @@
namespace OSDP.Net.Tests.Utilities
{
[TestFixture]
+ [Category("Unit")]
internal class BinaryUtilsTest
{
[Test]
diff --git a/src/OSDP.Net.sln b/src/OSDP.Net.sln
index 2cf6e3a6..9dedf1f4 100644
--- a/src/OSDP.Net.sln
+++ b/src/OSDP.Net.sln
@@ -5,10 +5,12 @@ VisualStudioVersion = 17.8.34408.163
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OSDP.Net", "OSDP.Net\OSDP.Net.csproj", "{50AC5FD2-8C50-48E4-AC4E-4A275D0A879E}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Console", "Console\Console.csproj", "{182D66E4-7909-4B18-AF01-BF0FE8B932BC}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ACUConsole", "ACUConsole\ACUConsole.csproj", "{182D66E4-7909-4B18-AF01-BF0FE8B932BC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OSDP.Net.Tests", "OSDP.Net.Tests\OSDP.Net.Tests.csproj", "{0018DA90-BBB2-491D-A6C3-086F21D7C29A}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PDConsole", "PDConsole\PDConsole.csproj", "{9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Settings", "Settings", "{5CE38FA4-377F-4C0D-A680-9CEA9A7CDBEE}"
ProjectSection(SolutionItems) = preProject
..\azure-pipelines.yml = ..\azure-pipelines.yml
@@ -32,8 +34,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ci", "ci", "{B139D674-6612-
ProjectSection(SolutionItems) = preProject
..\ci\build.yml = ..\ci\build.yml
..\ci\package.yml = ..\ci\package.yml
+ ..\ci\GitVersion.yml = ..\ci\GitVersion.yml
+ ..\azure-pipelines.yml = ..\azure-pipelines.yml
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimplePDDevice", "samples\SimplePDDevice\SimplePDDevice.csproj", "{6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -104,6 +110,30 @@ Global
{B6E1FC35-3DEC-495F-8FD2-A0CB9C3B4C6A}.Release|x64.Build.0 = Release|Any CPU
{B6E1FC35-3DEC-495F-8FD2-A0CB9C3B4C6A}.Release|x86.ActiveCfg = Release|Any CPU
{B6E1FC35-3DEC-495F-8FD2-A0CB9C3B4C6A}.Release|x86.Build.0 = Release|Any CPU
+ {9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}.Debug|x64.Build.0 = Debug|Any CPU
+ {9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}.Debug|x86.Build.0 = Debug|Any CPU
+ {9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}.Release|x64.ActiveCfg = Release|Any CPU
+ {9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}.Release|x64.Build.0 = Release|Any CPU
+ {9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}.Release|x86.ActiveCfg = Release|Any CPU
+ {9F1C4E3D-8B7A-4C2E-9D1F-5A8B3C7E9F4D}.Release|x86.Build.0 = Release|Any CPU
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}.Debug|x64.Build.0 = Debug|Any CPU
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}.Debug|x86.Build.0 = Debug|Any CPU
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}.Release|x64.ActiveCfg = Release|Any CPU
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}.Release|x64.Build.0 = Release|Any CPU
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}.Release|x86.ActiveCfg = Release|Any CPU
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -111,6 +141,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{AC0ADC7D-3A78-4937-80B0-F8AA3B7B17BD} = {A57B307A-A240-4F4F-A3A8-A078093B2809}
{B6E1FC35-3DEC-495F-8FD2-A0CB9C3B4C6A} = {A57B307A-A240-4F4F-A3A8-A078093B2809}
+ {6A18B8B0-C7E2-49E0-A739-E181BCABFAC9} = {A57B307A-A240-4F4F-A3A8-A078093B2809}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {07D0756C-2DA8-4FA1-8A4A-1FA6BCFBEB46}
diff --git a/src/OSDP.Net.sln.DotSettings b/src/OSDP.Net.sln.DotSettings
index e3e4671e..c9d5f15c 100644
--- a/src/OSDP.Net.sln.DotSettings
+++ b/src/OSDP.Net.sln.DotSettings
@@ -3,6 +3,7 @@
ACU
LED
OSDP
+ PD
PIN
PIV
UID
diff --git a/src/OSDP.Net/Bus.cs b/src/OSDP.Net/Bus.cs
index 8df0da4d..e0b95bc0 100644
--- a/src/OSDP.Net/Bus.cs
+++ b/src/OSDP.Net/Bus.cs
@@ -33,7 +33,7 @@ internal class Bus : IDisposable
private readonly Dictionary _lastOnlineConnectionStatus = new ();
private readonly Dictionary _lastSecureConnectionStatus = new ();
-
+
private readonly ILogger _logger;
private readonly TimeSpan _pollInterval;
private readonly BlockingCollection _replies;
diff --git a/src/OSDP.Net/Connections/IOsdpConnectionListener.cs b/src/OSDP.Net/Connections/IOsdpConnectionListener.cs
new file mode 100644
index 00000000..733afcb2
--- /dev/null
+++ b/src/OSDP.Net/Connections/IOsdpConnectionListener.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Threading.Tasks;
+
+namespace OSDP.Net.Connections;
+
+///
+/// Defines a connection listener for OSDP Peripheral Devices (PDs) that need to accept
+/// incoming connections from Access Control Units (ACUs).
+///
+///
+/// In the OSDP protocol, ACUs (Control Panels) are masters that initiate communication,
+/// while PDs (Peripheral Devices) are slaves that respond to commands. This interface
+/// represents a transport-layer listener that PDs use to accept incoming connections
+/// from ACUs. It is not an "OSDP server" in the protocol sense, but rather a connection
+/// factory that creates IOsdpConnection instances when transport connections are established.
+///
+public interface IOsdpConnectionListener : IDisposable
+{
+ ///
+ /// Gets the baud rate for serial connections. For TCP connections, this value may not be applicable.
+ ///
+ int BaudRate { get; }
+
+ ///
+ /// Starts listening for incoming connections from ACUs.
+ ///
+ /// Callback invoked when a new connection is accepted.
+ /// The callback receives the IOsdpConnection instance representing the established connection.
+ /// A task representing the asynchronous operation.
+ Task Start(Func newConnectionHandler);
+
+ ///
+ /// Stops the listener and terminates any active connections.
+ ///
+ /// A task representing the asynchronous operation.
+ Task Stop();
+
+ ///
+ /// Gets a value indicating whether the listener is currently running and accepting connections.
+ ///
+ bool IsRunning { get; }
+
+ ///
+ /// Gets the number of active connections currently being managed by this listener.
+ ///
+ int ConnectionCount { get; }
+}
\ No newline at end of file
diff --git a/src/OSDP.Net/Connections/IOsdpServer.cs b/src/OSDP.Net/Connections/IOsdpServer.cs
deleted file mode 100644
index 06ee20b8..00000000
--- a/src/OSDP.Net/Connections/IOsdpServer.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-namespace OSDP.Net.Connections;
-
-///
-/// Defines a server side of OSDP connection which is intended to listen for
-/// incoming connections as they are established
-///
-
-public interface IOsdpServer : IDisposable
-{
- ///
- /// Baud rate for the current connection
- ///
- int BaudRate { get; }
-
- ///
- /// Starts listening for incoming connections
- ///
- /// Callback to be invoked whenever a new connection is accepted
- Task Start(Func newConnectionHandler);
-
- ///
- /// Stops the server, which stops the listener and terminates
- /// any presently open connections to the server
- ///
- ///
- Task Stop();
-
- ///
- /// Indicates whether or not the server is running
- ///
- bool IsRunning { get; }
-
- ///
- /// The number of active connections being tracked by the server
- ///
- int ConnectionCount { get; }
-}
\ No newline at end of file
diff --git a/src/OSDP.Net/Connections/OsdpConnectionListener.cs b/src/OSDP.Net/Connections/OsdpConnectionListener.cs
new file mode 100644
index 00000000..5b04c798
--- /dev/null
+++ b/src/OSDP.Net/Connections/OsdpConnectionListener.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace OSDP.Net.Connections;
+
+///
+/// Base class for OSDP connection listeners that accept incoming connections from Access Control Units (ACUs).
+///
+///
+/// This abstract class provides the foundation for transport-specific listeners (TCP, Serial) that
+/// OSDP Peripheral Devices use to accept connections. Despite the previous "Server" naming, this class
+/// does not implement an OSDP protocol server. Instead, it manages transport-layer connections that
+/// enable OSDP communication between ACUs (masters) and PDs (slaves).
+///
+public abstract class OsdpConnectionListener : IOsdpConnectionListener
+{
+ private bool _disposedValue;
+ private readonly ConcurrentDictionary _connections = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The baud rate for serial connections. May not apply to TCP listeners.
+ /// Optional logger factory for diagnostic logging.
+ protected OsdpConnectionListener(int baudRate, ILoggerFactory loggerFactory = null)
+ {
+ LoggerFactory = loggerFactory;
+ Logger = loggerFactory?.CreateLogger();
+ BaudRate = baudRate;
+ }
+
+ ///
+ public bool IsRunning { get; protected set; }
+
+ ///
+ public int ConnectionCount => _connections.Count;
+
+ ///
+ /// Gets the logger factory instance if one was provided during instantiation.
+ ///
+ protected ILoggerFactory LoggerFactory { get; }
+
+ ///
+ /// Gets the logger instance for this listener if a logger factory was provided.
+ ///
+ protected ILogger Logger { get; }
+
+ ///
+ public int BaudRate { get; }
+
+ ///
+ public abstract Task Start(Func newConnectionHandler);
+
+ ///
+ public virtual async Task Stop()
+ {
+ IsRunning = false;
+
+ Logger?.LogDebug("Stopping OSDP connection listener...");
+
+ while (true)
+ {
+ var entries = _connections.ToArray();
+ if (entries.Length == 0) break;
+
+ await Task.WhenAll(entries.Select(item => item.Value.Close()));
+ await Task.WhenAll(entries.Select(x => x.Key));
+ }
+
+ Logger?.LogDebug("OSDP connection listener stopped");
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Registers a new connection and its associated handling task with the listener.
+ ///
+ /// The OSDP connection to register.
+ /// The task that handles communication for this connection.
+ ///
+ /// This method should be called by derived classes when they create a new connection
+ /// in response to an incoming transport connection (TCP accept, serial port open, etc.).
+ /// The listener tracks all active connections and ensures they are properly closed when
+ /// the listener stops.
+ ///
+ protected void RegisterConnection(OsdpConnection connection, Task task)
+ {
+ Task.Run(async () =>
+ {
+ _connections.TryAdd(task, connection);
+ if (!IsRunning) await connection.Close();
+ Logger?.LogDebug("New OSDP connection opened - total connections: {ConnectionCount}", _connections.Count);
+ await task;
+ _connections.TryRemove(task, out _);
+ Logger?.LogDebug("OSDP connection terminated - remaining connections: {ConnectionCount}", _connections.Count);
+ });
+ }
+
+ ///
+ /// Releases the resources used by the instance.
+ ///
+ /// True if disposing managed resources; false if finalizing.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ var _ = Stop();
+ }
+
+ _disposedValue = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OSDP.Net/Connections/OsdpServer.cs b/src/OSDP.Net/Connections/OsdpServer.cs
deleted file mode 100644
index d49ebb8c..00000000
--- a/src/OSDP.Net/Connections/OsdpServer.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.Extensions.Logging;
-
-namespace OSDP.Net.Connections;
-
-///
-/// Base class for an OSDP server that listens for incoming connections
-///
-public abstract class OsdpServer : IOsdpServer
-{
- private bool _disposedValue;
- private readonly ConcurrentDictionary _connections = new();
-
- ///
- /// Creates a new instance of OsdpServer
- ///
- ///
- /// Optional logger factory
- protected OsdpServer(int baudRate, ILoggerFactory loggerFactory = null)
- {
- LoggerFactory = loggerFactory;
- Logger = loggerFactory?.CreateLogger();
- BaudRate = baudRate;
- }
-
- ///
- public bool IsRunning { get; protected set; }
-
- ///
- public int ConnectionCount => _connections.Count;
-
- ///
- /// Logger factory if one was specified at instantitation
- ///
- protected ILoggerFactory LoggerFactory { get; }
-
- ///
- /// Logger instance used by the server if a factory was specified at instantiation
- ///
- protected ILogger Logger { get; }
-
- ///
- public int BaudRate { get; }
-
- ///
- public abstract Task Start(Func newConnectionHandler);
-
- ///
- public virtual async Task Stop()
- {
- IsRunning = false;
-
- Logger?.LogDebug("Stopping OSDP Server connections...");
-
- while (true)
- {
- var entries = _connections.ToArray();
- if (entries.Length == 0) break;
-
- await Task.WhenAll(entries.Select(item => item.Value.Close()));
- await Task.WhenAll(entries.Select(x => x.Key));
- }
-
- Logger?.LogDebug("OSDP Server STOPPED");
- }
-
- ///
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- ///
- /// Intended to be called by a deriving class whenever it spins off a dedicated
- /// listening loop task for a newly created OsdpConnection
- ///
- ///
- ///
- protected void RegisterConnection(OsdpConnection connection, Task task)
- {
- Task.Run(async () =>
- {
- _connections.TryAdd(task, connection);
- if (!IsRunning) await connection.Close();
- Logger?.LogDebug("New OSDP connection opened - {}", _connections.Count);
- await task;
- _connections.TryRemove(task, out _);
- Logger?.LogDebug("OSDP connection terminated - {}", _connections.Count);
- });
- }
-
-
- ///
- /// Releases the resources used by the instance.
- ///
- protected virtual void Dispose(bool disposing)
- {
- if (!_disposedValue)
- {
- if (disposing)
- {
- var _ = Stop();
- }
-
- _disposedValue = true;
- }
- }
-}
\ No newline at end of file
diff --git a/src/OSDP.Net/Connections/SerialPortConnectionListener.cs b/src/OSDP.Net/Connections/SerialPortConnectionListener.cs
new file mode 100644
index 00000000..e027b92b
--- /dev/null
+++ b/src/OSDP.Net/Connections/SerialPortConnectionListener.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace OSDP.Net.Connections;
+
+///
+/// Implements a serial port connection listener for OSDP Peripheral Devices.
+///
+///
+/// Unlike TCP listeners that wait for incoming connections, serial communication doesn't have a
+/// connection establishment phase. This listener immediately opens the serial port and creates
+/// an IOsdpConnection for OSDP communication. When the connection is closed (e.g., due to errors
+/// or device disconnection), it automatically reopens the port to maintain availability. This behavior
+/// is essential for serial-based OSDP devices that need to remain accessible to ACUs over RS-485
+/// or similar serial interfaces.
+///
+public class SerialPortConnectionListener : OsdpConnectionListener
+{
+ private readonly string _portName;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the serial port (e.g., "COM1", "/dev/ttyS0").
+ /// The baud rate for serial communication.
+ /// Optional logger factory for diagnostic logging.
+ public SerialPortConnectionListener(
+ string portName, int baudRate, ILoggerFactory loggerFactory = null) : base(baudRate, loggerFactory)
+ {
+ _portName = portName;
+ }
+
+ ///
+ public override async Task Start(Func newConnectionHandler)
+ {
+ IsRunning = true;
+
+ Logger?.LogInformation("Starting serial port listener on {Port} @ {BaudRate} baud", _portName, BaudRate);
+
+ await OpenSerialPort(newConnectionHandler);
+ }
+
+ ///
+ /// Opens the serial port and creates a connection, automatically reopening if the connection closes.
+ ///
+ /// The handler to process the new connection.
+ private async Task OpenSerialPort(Func newConnectionHandler)
+ {
+ try
+ {
+ var connection = new SerialPortOsdpConnection(_portName, BaudRate);
+ await connection.Open();
+
+ Logger?.LogDebug("Serial port {Port} opened successfully", _portName);
+
+ var task = Task.Run(async () =>
+ {
+ try
+ {
+ await newConnectionHandler(connection);
+ }
+ catch (Exception ex)
+ {
+ Logger?.LogError(ex, "Error in serial connection handler");
+ }
+ finally
+ {
+ // If still running, reopen the serial port after a brief delay
+ if (IsRunning)
+ {
+ Logger?.LogDebug("Serial connection closed, reopening port {Port}", _portName);
+ await Task.Delay(1000); // Brief delay before reopening
+ await OpenSerialPort(newConnectionHandler);
+ }
+ }
+ });
+
+ RegisterConnection(connection, task);
+ }
+ catch (Exception ex)
+ {
+ Logger?.LogError(ex, "Failed to open serial port {Port}", _portName);
+
+ // Retry after delay if still running
+ if (IsRunning)
+ {
+ await Task.Delay(5000); // Longer delay on error
+ await OpenSerialPort(newConnectionHandler);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OSDP.Net/Connections/SerialPortOsdpServer.cs b/src/OSDP.Net/Connections/SerialPortOsdpServer.cs
deleted file mode 100644
index 40cea83a..00000000
--- a/src/OSDP.Net/Connections/SerialPortOsdpServer.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using Microsoft.Extensions.Logging;
-
-namespace OSDP.Net.Connections;
-
-///
-/// Implements OSDP server side which communicates via a serial port
-///
-///
-/// Whereas TCP/IP server creates a new connection whenever listener detects a new client,
-/// serial server operates differently. It will instantaneously connect to the serial port
-/// and open its side of comms without waiting for anyone/anything to connect to the other
-/// side of the serial cable.
-///
-public class SerialPortOsdpServer : OsdpServer
-{
- private readonly string _portName;
-
- ///
- /// Creates a new instance of SerialPortOsdpServer
- ///
- /// Name of the serial port
- /// Baud rate at which to communicate
- /// Optional logger factory
- public SerialPortOsdpServer(
- string portName, int baudRate, ILoggerFactory loggerFactory = null) : base(baudRate, loggerFactory)
- {
- _portName = portName;
- }
-
- ///
- public override async Task Start(Func newConnectionHandler)
- {
- IsRunning = true;
-
- Logger?.LogInformation("Opening {Port} @ {Baud} serial port...", _portName, BaudRate);
-
- await OpenSerialPort(newConnectionHandler);
- }
-
- private async Task OpenSerialPort(Func newConnectionHandler)
- {
- var connection = new SerialPortOsdpConnection(_portName, BaudRate);
- await connection.Open();
- var task = Task.Run(async () =>
- {
- await newConnectionHandler(connection);
- if (IsRunning)
- {
- await Task.Delay(1);
- await OpenSerialPort(newConnectionHandler);
- }
- });
- RegisterConnection(connection, task);
- }
-}
\ No newline at end of file
diff --git a/src/OSDP.Net/Connections/TcpConnectionListener.cs b/src/OSDP.Net/Connections/TcpConnectionListener.cs
new file mode 100644
index 00000000..42651d33
--- /dev/null
+++ b/src/OSDP.Net/Connections/TcpConnectionListener.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace OSDP.Net.Connections;
+
+///
+/// Implements a TCP/IP connection listener for OSDP Peripheral Devices to accept incoming connections from ACUs.
+///
+///
+/// This listener allows OSDP devices to accept TCP connections from Access Control Units. When an ACU
+/// connects via TCP, this listener creates a new IOsdpConnection instance to handle the OSDP communication
+/// over that TCP connection. This is commonly used when PDs need to be accessible over network connections
+/// rather than traditional serial connections.
+///
+public class TcpConnectionListener : OsdpConnectionListener
+{
+ private readonly TcpListener _listener;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The TCP port number to listen on for incoming connections.
+ /// The simulated baud rate for OSDP communication timing.
+ /// Optional logger factory for diagnostic logging.
+ public TcpConnectionListener(
+ int portNumber, int baudRate, ILoggerFactory loggerFactory = null) : base(baudRate, loggerFactory)
+ {
+ _listener = TcpListener.Create(portNumber);
+ }
+
+ ///
+ public override Task Start(Func newConnectionHandler)
+ {
+ if (IsRunning) return Task.CompletedTask;
+
+ IsRunning = true;
+ _listener.Start();
+
+ Logger?.LogInformation("TCP listener started on {Endpoint} for incoming OSDP connections", _listener.LocalEndpoint.ToString());
+
+ Task.Run(async () =>
+ {
+ while (IsRunning)
+ {
+ try
+ {
+ var client = await _listener.AcceptTcpClientAsync();
+ Logger?.LogDebug("Accepted TCP connection from {RemoteEndpoint}", client.Client.RemoteEndPoint?.ToString());
+
+ var connection = new TcpOsdpConnection(client, BaudRate, LoggerFactory);
+ var task = newConnectionHandler(connection);
+ RegisterConnection(connection, task);
+ }
+ catch (ObjectDisposedException)
+ {
+ // Expected when stopping the listener
+ break;
+ }
+ catch (Exception ex)
+ {
+ Logger?.LogError(ex, "Error accepting TCP connection");
+ }
+ }
+ });
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public override Task Stop()
+ {
+ IsRunning = false;
+ _listener.Stop();
+ Logger?.LogInformation("TCP listener stopped");
+ return base.Stop();
+ }
+}
\ No newline at end of file
diff --git a/src/OSDP.Net/Connections/TcpServerOsdpConnection2.cs b/src/OSDP.Net/Connections/TcpOsdpConnection.cs
similarity index 57%
rename from src/OSDP.Net/Connections/TcpServerOsdpConnection2.cs
rename to src/OSDP.Net/Connections/TcpOsdpConnection.cs
index c4aff61b..419bdf4b 100644
--- a/src/OSDP.Net/Connections/TcpServerOsdpConnection2.cs
+++ b/src/OSDP.Net/Connections/TcpOsdpConnection.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
@@ -7,21 +7,37 @@
namespace OSDP.Net.Connections;
-internal sealed class TcpServerOsdpConnection2 : OsdpConnection
+///
+/// Represents a TCP-based OSDP connection that wraps an already-established TCP client connection.
+///
+///
+/// This class is designed to work with connection listeners that accept TCP connections and then
+/// create instances of this class to handle the OSDP communication over the established TCP connection.
+/// It does not handle the listening aspect - that responsibility belongs to connection listeners
+/// like TcpConnectionListener.
+///
+internal sealed class TcpOsdpConnection : OsdpConnection
{
private readonly ILogger _logger;
private TcpClient _tcpClient;
private NetworkStream _stream;
- public TcpServerOsdpConnection2(
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An already-connected TCP client.
+ /// The simulated baud rate for OSDP communication timing.
+ /// Optional logger factory for diagnostic logging.
+ public TcpOsdpConnection(
TcpClient tcpClient, int baudRate, ILoggerFactory loggerFactory) : base(baudRate)
{
IsOpen = true;
_tcpClient = tcpClient;
_stream = tcpClient.GetStream();
- _logger = loggerFactory?.CreateLogger();
+ _logger = loggerFactory?.CreateLogger();
}
+ ///
public override async Task ReadAsync(byte[] buffer, CancellationToken token)
{
try
@@ -36,11 +52,11 @@ public override async Task ReadAsync(byte[] buffer, CancellationToken token
{
if (exception is IOException && exception.InnerException is SocketException)
{
- _logger?.LogInformation("Error reading tcp stream: {ExceptionMessage}", exception.Message);
+ _logger?.LogInformation("Error reading TCP stream: {ExceptionMessage}", exception.Message);
}
else
{
- _logger?.LogWarning(exception, "Error reading tcp stream");
+ _logger?.LogWarning(exception, "Error reading TCP stream");
}
IsOpen = false;
@@ -49,6 +65,7 @@ public override async Task ReadAsync(byte[] buffer, CancellationToken token
}
}
+ ///
public override async Task WriteAsync(byte[] buffer)
{
try
@@ -59,15 +76,19 @@ public override async Task WriteAsync(byte[] buffer)
{
if (IsOpen)
{
- _logger?.LogWarning(ex, "Error writing tcp stream");
+ _logger?.LogWarning(ex, "Error writing TCP stream");
IsOpen = false;
}
}
}
///
- public override Task Open() => throw new NotSupportedException();
+ ///
+ /// This method is not supported because the connection is already established when this class is instantiated.
+ ///
+ public override Task Open() => throw new NotSupportedException("Connection is already established");
+ ///
public override Task Close()
{
IsOpen = false;
diff --git a/src/OSDP.Net/Connections/TcpOsdpServer.cs b/src/OSDP.Net/Connections/TcpOsdpServer.cs
deleted file mode 100644
index 7800c0e8..00000000
--- a/src/OSDP.Net/Connections/TcpOsdpServer.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using System;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-using Microsoft.Extensions.Logging;
-
-namespace OSDP.Net.Connections;
-
-///
-/// Implements TCP/IP OSDP server which listens for incoming connections
-///
-public class TcpOsdpServer : OsdpServer
-{
- private readonly TcpListener _listener;
-
- ///
- /// Creates a new instance of TcpOsdpServer
- ///
- /// Port to listen on
- /// Baud rate at which comms are expected to take place
- /// Optional logger factory
- public TcpOsdpServer(
- int portNumber, int baudRate, ILoggerFactory loggerFactory = null) : base(baudRate, loggerFactory)
- {
- _listener = TcpListener.Create(portNumber);
- }
-
- ///
- public override Task Start(Func newConnectionHandler)
- {
- if (IsRunning) return Task.CompletedTask;
-
- IsRunning = true;
- _listener.Start();
-
- Logger?.LogInformation("Listening on {Endpoint} for incoming connections...", _listener.LocalEndpoint.ToString());
-
- Task.Run(async () =>
- {
- while (IsRunning)
- {
- var client = await _listener.AcceptTcpClientAsync();
-
- var connection = new TcpServerOsdpConnection2(client, BaudRate, LoggerFactory);
- var task = newConnectionHandler(connection);
- RegisterConnection(connection, task);
- }
- });
-
- return Task.CompletedTask;
- }
-
- ///
- public override Task Stop()
- {
- IsRunning = false;
- _listener.Stop();
- return base.Stop();
- }
-}
\ No newline at end of file
diff --git a/src/OSDP.Net/Connections/TcpServerOsdpConnection.cs b/src/OSDP.Net/Connections/TcpServerOsdpConnection.cs
index bb5ef964..679bdba6 100644
--- a/src/OSDP.Net/Connections/TcpServerOsdpConnection.cs
+++ b/src/OSDP.Net/Connections/TcpServerOsdpConnection.cs
@@ -1,30 +1,38 @@
+using System;
+using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
namespace OSDP.Net.Connections
{
///
- /// Initial implementation of TCP server OSDP connection which combines
- /// the listener as well as the accepted connection in a single class.
- ///
- /// The use of this class might be questionable as TCP/IP protocol
- /// inherently behaves differently enough that this class has some limitations
- /// which have been addressed by TcpOsdpServer
+ /// TCP server OSDP connection that allows a ControlPanel (ACU) to act as a TCP server,
+ /// accepting connections from OSDP devices.
///
+ ///
+ /// This class combines TCP listening and connection handling in a single IOsdpConnection implementation,
+ /// making it suitable for use with ControlPanel instances that need to accept incoming device connections.
+ /// For scenarios where devices (PDs) need to accept ACU connections, use TcpConnectionListener instead.
+ ///
public class TcpServerOsdpConnection : OsdpConnection
{
private readonly TcpListener _listener;
+ private readonly ILogger _logger;
private TcpClient _tcpClient;
+ private NetworkStream _stream;
///
/// Initializes a new instance of the class.
///
- /// The port number.
- /// The baud rate.
- public TcpServerOsdpConnection(int portNumber, int baudRate) : base(baudRate)
+ /// The TCP port number to listen on.
+ /// The simulated baud rate for OSDP communication timing.
+ /// Optional logger factory for diagnostic logging.
+ public TcpServerOsdpConnection(int portNumber, int baudRate, ILoggerFactory loggerFactory = null) : base(baudRate)
{
_listener = TcpListener.Create(portNumber);
+ _logger = loggerFactory?.CreateLogger();
}
///
@@ -40,50 +48,115 @@ public override bool IsOpen
///
public override async Task Open()
{
- _listener.Start();
- var newTcpClient = await _listener.AcceptTcpClientAsync();
+ try
+ {
+ _listener.Start();
+ _logger?.LogInformation("TCP server listening on {@Endpoint} for device connections", _listener.LocalEndpoint);
+
+ var newTcpClient = await _listener.AcceptTcpClientAsync();
+ _logger?.LogInformation("Accepted device connection from {@RemoteEndpoint}", newTcpClient.Client.RemoteEndPoint);
- await Close();
+ // Close any existing connection before accepting the new one
+ await Close();
- _tcpClient = newTcpClient;
+ _tcpClient = newTcpClient;
+ _stream = _tcpClient.GetStream();
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "Error opening TCP server connection");
+ throw;
+ }
}
///
public override Task Close()
{
- var tcpClient = _tcpClient;
- _tcpClient = null;
- if (tcpClient?.Connected ?? false) tcpClient?.GetStream().Close();
- tcpClient?.Close();
+ try
+ {
+ _stream?.Dispose();
+ _tcpClient?.Dispose();
+ _stream = null;
+ _tcpClient = null;
+
+ _logger?.LogDebug("TCP server connection closed");
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogWarning(ex, "Error closing TCP server connection");
+ }
+
return Task.CompletedTask;
}
///
public override async Task WriteAsync(byte[] buffer)
{
- var tcpClient = _tcpClient;
- if (tcpClient != null)
+ try
+ {
+ var stream = _stream;
+ if (stream != null)
+ {
+ await stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+ }
+ }
+ catch (Exception ex)
{
- await tcpClient.GetStream().WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+ _logger?.LogWarning(ex, "Error writing to TCP stream");
+ // Don't set IsOpen to false here as the base class will handle a connection state
+ throw;
}
}
///
public override async Task ReadAsync(byte[] buffer, CancellationToken token)
{
- var tcpClient = _tcpClient;
- if (tcpClient != null)
+ try
{
- return await tcpClient.GetStream().ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false);
+ var stream = _stream;
+ if (stream != null)
+ {
+ var bytes = await stream.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false);
+ if (bytes == 0)
+ {
+ _logger?.LogInformation("TCP stream closed by remote device");
+ }
+ return bytes;
+ }
+ return 0;
+ }
+ catch (Exception exception)
+ {
+ if (exception is not OperationCanceledException)
+ {
+ if (exception is IOException && exception.InnerException is SocketException)
+ {
+ _logger?.LogInformation("Device disconnected: {ExceptionMessage}", exception.Message);
+ }
+ else
+ {
+ _logger?.LogWarning(exception, "Error reading from TCP stream");
+ }
+ }
+ return 0;
}
-
- return 0;
}
///
public override string ToString()
{
- return _listener?.LocalEndpoint.ToString();
+ return _listener?.LocalEndpoint.ToString() ?? "TcpServerOsdpConnection";
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ var _ = Close();
+ _listener?.Stop();
+ }
+ base.Dispose(disposing);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/OSDP.Net/Device.cs b/src/OSDP.Net/Device.cs
index 25107beb..60bf3f45 100644
--- a/src/OSDP.Net/Device.cs
+++ b/src/OSDP.Net/Device.cs
@@ -26,7 +26,7 @@ public class Device : IDisposable
private volatile int _connectionContextCounter;
private DeviceConfiguration _deviceConfiguration;
- private IOsdpServer _osdpServer;
+ private IOsdpConnectionListener _connectionListener;
private DateTime _lastValidReceivedCommand = DateTime.MinValue;
///
@@ -50,7 +50,7 @@ public void Dispose()
/// Gets a value indicating whether the device is currently connected.
///
/// true if the device is connected; otherwise, false.
- public bool IsConnected => _osdpServer?.ConnectionCount > 0 && (
+ public bool IsConnected => _connectionListener?.ConnectionCount > 0 && (
_lastValidReceivedCommand + TimeSpan.FromSeconds(8) >= DateTime.UtcNow);
///
@@ -83,13 +83,13 @@ protected virtual void Dispose(bool disposing)
}
///
- /// Starts listening for commands from the OSDP device through the specified connection.
+ /// Starts listening for commands from the ACU through the specified connection listener.
///
- /// The I/O server used for communication with the OSDP client.
- public async void StartListening(IOsdpServer server)
+ /// The connection listener used to accept incoming connections from ACUs.
+ public async void StartListening(IOsdpConnectionListener connectionListener)
{
- _osdpServer = server ?? throw new ArgumentNullException(nameof(server));
- await _osdpServer.Start(ClientListenLoop);
+ _connectionListener = connectionListener ?? throw new ArgumentNullException(nameof(connectionListener));
+ await _connectionListener.Start(ClientListenLoop);
}
private async Task ClientListenLoop(IOsdpConnection incomingConnection)
@@ -141,8 +141,8 @@ private async Task ClientListenLoop(IOsdpConnection incomingConnection)
///
public async Task StopListening()
{
- await (_osdpServer?.Stop() ?? Task.CompletedTask);
- _osdpServer = null;
+ await (_connectionListener?.Stop() ?? Task.CompletedTask);
+ _connectionListener = null;
}
///
@@ -191,7 +191,7 @@ private PayloadData HandlePoll()
///
/// Handles the ID Report Request command received from the OSDP device.
///
- ///
+ /// A payload data response to the ID report request. Override this method to provide device identification information.
protected virtual PayloadData HandleIdReport()
{
return HandleUnknownCommand(CommandType.IdReport);
@@ -201,7 +201,7 @@ protected virtual PayloadData HandleIdReport()
/// Handles the text output command received from the OSDP device.
///
/// The incoming reader text output command payload.
- ///
+ /// A payload data response indicating the result of the text output operation.
protected virtual PayloadData HandleTextOutput(ReaderTextOutput commandPayload)
{
return HandleUnknownCommand(CommandType.TextOutput);
@@ -211,7 +211,7 @@ protected virtual PayloadData HandleTextOutput(ReaderTextOutput commandPayload)
/// Handles the reader buzzer control command received from the OSDP device.
///
/// The incoming reader buzzer control command payload.
- ///
+ /// A payload data response indicating the result of the buzzer control operation.
protected virtual PayloadData HandleBuzzerControl(ReaderBuzzerControl commandPayload)
{
return HandleUnknownCommand(CommandType.BuzzerControl);
@@ -221,16 +221,16 @@ protected virtual PayloadData HandleBuzzerControl(ReaderBuzzerControl commandPay
/// Handles the output controls command received from the OSDP device.
///
/// The incoming output controls command payload.
- ///
+ /// A payload data response indicating the result of the output control operation.
protected virtual PayloadData HandleOutputControl(OutputControls commandPayload)
{
return HandleUnknownCommand(CommandType.OutputControl);
}
///
- /// Handles the output control command received from the OSDP device.
+ /// Handles the device capabilities request command received from the OSDP device.
///
- ///
+ /// A payload data response containing the device capabilities. Override this method to provide actual device capabilities.
protected virtual PayloadData HandleDeviceCapabilities()
{
return HandleUnknownCommand(CommandType.DeviceCapabilities);
@@ -240,17 +240,17 @@ protected virtual PayloadData HandleDeviceCapabilities()
/// Handles the get PIV data command received from the OSDP device.
///
/// The incoming get PIV data command payload.
- ///
+ /// A payload data response containing the requested PIV data or appropriate error response.
protected virtual PayloadData HandlePivData(GetPIVData commandPayload)
{
return HandleUnknownCommand(CommandType.PivData);
}
///
- /// Handles the manufacture command received from the OSDP device.
+ /// Handles the manufacturer-specific command received from the OSDP device.
///
- /// The incoming manufacture command payload.
- ///
+ /// The incoming manufacturer-specific command payload.
+ /// A payload data response for the manufacturer-specific command.
protected virtual PayloadData HandleManufacturerCommand(ManufacturerSpecific commandPayload)
{
return HandleUnknownCommand(CommandType.ManufacturerSpecific);
@@ -260,7 +260,7 @@ protected virtual PayloadData HandleManufacturerCommand(ManufacturerSpecific com
/// Handles the keep active command received from the OSDP device.
///
/// The incoming keep active command payload.
- ///
+ /// A payload data response acknowledging the keep active command.
protected virtual PayloadData HandleKeepActive(KeepReaderActive commandPayload)
{
return HandleUnknownCommand(CommandType.KeepActive);
@@ -269,7 +269,7 @@ protected virtual PayloadData HandleKeepActive(KeepReaderActive commandPayload)
///
/// Handles the abort request command received from the OSDP device.
///
- ///
+ /// A payload data response acknowledging the abort request.
protected virtual PayloadData HandleAbortRequest()
{
return HandleUnknownCommand(CommandType.Abort);
@@ -287,10 +287,10 @@ private PayloadData HandleFileTransfer(FileTransferFragment commandPayload)
}
///
- /// Handles the maximum ACU maximum receive size command received from the OSDP device.
+ /// Handles the maximum ACU receive size command received from the OSDP device.
///
/// The ACU maximum receive size command payload.
- ///
+ /// A payload data response acknowledging the maximum receive size setting.
protected virtual PayloadData HandleMaxReplySize(ACUReceiveSize commandPayload)
{
return HandleUnknownCommand(CommandType.MaxReplySize);
@@ -330,17 +330,17 @@ protected virtual PayloadData HandleKeySettings(EncryptionKeyConfiguration comma
/// Handles the biometric match command received from the OSDP device.
///
/// The biometric match command payload.
- ///
+ /// A payload data response containing the biometric match result.
protected virtual PayloadData HandleBiometricMatch(BiometricTemplateData commandPayload)
{
return HandleUnknownCommand(CommandType.BioMatch);
}
///
- /// Handles the biometric match command received from the OSDP device.
+ /// Handles the biometric read command received from the OSDP device.
///
- /// The biometric match command payload.
- ///
+ /// The biometric read command payload.
+ /// A payload data response containing the biometric read result.
protected virtual PayloadData HandleBiometricRead(BiometricReadData commandPayload)
{
return HandleUnknownCommand(CommandType.BioRead);
@@ -354,7 +354,7 @@ private PayloadData _HandleCommunicationSet(CommunicationConfiguration commandPa
{
var config = (Model.ReplyData.CommunicationConfiguration)response;
var previousAddress = _deviceConfiguration.Address;
- var previousBaudRate = _osdpServer.BaudRate;
+ var previousBaudRate = _connectionListener.BaudRate;
if (previousAddress != config.Address)
{
@@ -410,7 +410,7 @@ protected virtual PayloadData HandleCommunicationSet(CommunicationConfiguration
/// Handles the reader LED controls command received from the OSDP device.
///
/// The reader LED controls command payload.
- ///
+ /// A payload data response indicating the result of the LED control operation.
protected virtual PayloadData HandleReaderLEDControl(ReaderLedControls commandPayload)
{
return HandleUnknownCommand(CommandType.LEDControl);
@@ -419,7 +419,7 @@ protected virtual PayloadData HandleReaderLEDControl(ReaderLedControls commandPa
///
/// Handles the reader status command received from the OSDP device.
///
- ///
+ /// A payload data response containing the current reader status information.
protected virtual PayloadData HandleReaderStatusReport()
{
return HandleUnknownCommand(CommandType.ReaderStatus);
@@ -428,7 +428,7 @@ protected virtual PayloadData HandleReaderStatusReport()
///
/// Handles the output status command received from the OSDP device.
///
- ///
+ /// A payload data response containing the current output status information.
protected virtual PayloadData HandleOutputStatusReport()
{
return HandleUnknownCommand(CommandType.OutputStatus);
@@ -437,7 +437,7 @@ protected virtual PayloadData HandleOutputStatusReport()
///
/// Handles the input status command received from the OSDP device.
///
- ///
+ /// A payload data response containing the current input status information.
protected virtual PayloadData HandleInputStatusReport()
{
return HandleUnknownCommand(CommandType.InputStatus);
@@ -446,7 +446,7 @@ protected virtual PayloadData HandleInputStatusReport()
///
/// Handles the reader local status command received from the OSDP device.
///
- ///
+ /// A payload data response containing the current local status information.
protected virtual PayloadData HandleLocalStatusReport()
{
return HandleUnknownCommand(CommandType.LocalStatus);
diff --git a/src/OSDP.Net/DeviceProxy.cs b/src/OSDP.Net/DeviceProxy.cs
index 56ad4bb0..ade03bbd 100644
--- a/src/OSDP.Net/DeviceProxy.cs
+++ b/src/OSDP.Net/DeviceProxy.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using OSDP.Net.Messages;
@@ -180,20 +179,6 @@ internal ReadOnlySpan GenerateMac(ReadOnlySpan message, bool isIncom
{
return MessageSecureChannel.GenerateMac(message, isIncoming);
}
-
- internal ReadOnlySpan EncryptData(ReadOnlySpan payload)
- {
- var paddedData = MessageSecureChannel.PadTheData(payload);
-
- var encryptedData = new Span(new byte[paddedData.Length]);
- MessageSecureChannel.EncodePayload(paddedData.ToArray(), encryptedData);
- return encryptedData;
- }
-
- internal IEnumerable DecryptData(ReadOnlySpan payload)
- {
- return MessageSecureChannel.DecodePayload(payload.ToArray());
- }
}
internal interface IDeviceProxyFactory
diff --git a/src/OSDP.Net/Messages/IncomingMessage.cs b/src/OSDP.Net/Messages/IncomingMessage.cs
index f6d4beed..3e9b865d 100644
--- a/src/OSDP.Net/Messages/IncomingMessage.cs
+++ b/src/OSDP.Net/Messages/IncomingMessage.cs
@@ -1,4 +1,5 @@
-using System;
+
+using System;
using System.Collections.Generic;
using System.Linq;
using OSDP.Net.Messages.SecureChannel;
@@ -10,7 +11,7 @@ namespace OSDP.Net.Messages
/// class with extra properties/methods that specifically indicate the parsing and
/// validation of incoming raw bytes.
///
- public class IncomingMessage : Message
+ internal class IncomingMessage : Message
{
private const ushort MessageHeaderSize = 6;
private readonly byte[] _originalMessage;
diff --git a/src/OSDP.Net/Messages/Message.cs b/src/OSDP.Net/Messages/Message.cs
index 958bb910..8e4ffc72 100644
--- a/src/OSDP.Net/Messages/Message.cs
+++ b/src/OSDP.Net/Messages/Message.cs
@@ -162,13 +162,6 @@ protected static void AddChecksum(IList packet)
packet[packet.Count - 1] = CalculateChecksum(packet.Take(packet.Count - 1).ToArray());
}
- internal ReadOnlySpan EncryptedData(DeviceProxy device)
- {
- var data = Data();
-
- return !data.IsEmpty ? device.EncryptData(data) : data;
- }
-
internal static int ConvertBytesToInt(byte[] bytes)
{
if (!BitConverter.IsLittleEndian)
@@ -205,22 +198,5 @@ internal static ushort CalculateMaximumMessageSize(ushort dataSize, bool isEncry
return (ushort)(dataSize - (isEncrypted ? encryptedDifference + (dataSize % cryptoLength) : clearTextDifference));
}
-
- internal static byte[] PadTheData(ReadOnlySpan data, byte cryptoLength, byte paddingStart)
- {
- int paddingLength = data.Length + cryptoLength - data.Length % cryptoLength;
-
- Span buffer = stackalloc byte[paddingLength];
- buffer.Clear();
-
- var cursor = buffer.Slice(0);
-
- data.CopyTo(cursor);
- cursor = cursor.Slice(data.Length);
-
- cursor[0] = paddingStart;
-
- return buffer.ToArray();
- }
}
}
\ No newline at end of file
diff --git a/src/OSDP.Net/Messages/OutgoingMessage.cs b/src/OSDP.Net/Messages/OutgoingMessage.cs
index 6c83d1a2..30d23e1c 100644
--- a/src/OSDP.Net/Messages/OutgoingMessage.cs
+++ b/src/OSDP.Net/Messages/OutgoingMessage.cs
@@ -29,7 +29,7 @@ internal byte[] BuildMessage(IMessageSecureChannel secureChannel)
if (securityEstablished && payload.Length > 0)
{
- payload = PadTheData(payload, 16, FirstPaddingByte);
+ payload = secureChannel.PadTheData(payload).ToArray();
}
bool isSecurityBlockPresent = securityEstablished || PayloadData.IsSecurityInitialization;
diff --git a/src/OSDP.Net/Messages/SecureChannel/ACUMessageSecureChannel.cs b/src/OSDP.Net/Messages/SecureChannel/ACUMessageSecureChannel.cs
index 89d01c77..f4894f12 100644
--- a/src/OSDP.Net/Messages/SecureChannel/ACUMessageSecureChannel.cs
+++ b/src/OSDP.Net/Messages/SecureChannel/ACUMessageSecureChannel.cs
@@ -7,8 +7,15 @@ namespace OSDP.Net.Messages.SecureChannel;
/// Message channel which represents the Access Control Unit (ACU) side of the OSDP
/// communications (i.e. OSDP commands are sent out and replies are received)
///
-internal class ACUMessageSecureChannel : MessageSecureChannel
+public class ACUMessageSecureChannel : MessageSecureChannel
{
+ ///
+ /// Initializes a new instance of the ACUMessageChannel with logger factory
+ ///
+ /// Optional logger factory from which a logger object for the
+ /// message channel will be acquired
+ public ACUMessageSecureChannel(ILoggerFactory loggerFactory) : base(null, loggerFactory) {}
+
///
/// Initializes a new instance of the ACUMessageChannel
///
@@ -19,7 +26,7 @@ internal class ACUMessageSecureChannel : MessageSecureChannel
/// channels
/// Optional logger factory from which a logger object for the
/// message channel will be acquired
- public ACUMessageSecureChannel(SecurityContext context = null, ILoggerFactory loggerFactory = null)
+ internal ACUMessageSecureChannel(SecurityContext context = null, ILoggerFactory loggerFactory = null)
: base(context, loggerFactory) {}
///
diff --git a/src/OSDP.Net/Messages/SecureChannel/MessageSecureChannel.cs b/src/OSDP.Net/Messages/SecureChannel/MessageSecureChannel.cs
index f16b1a36..dd504e02 100644
--- a/src/OSDP.Net/Messages/SecureChannel/MessageSecureChannel.cs
+++ b/src/OSDP.Net/Messages/SecureChannel/MessageSecureChannel.cs
@@ -199,17 +199,17 @@ public ReadOnlySpan PadTheData(ReadOnlySpan data)
int dataLength = data.Length + 1;
int paddingLength = dataLength + (cryptoLength - dataLength % cryptoLength) % cryptoLength;
-
+
Span buffer = stackalloc byte[paddingLength];
buffer.Clear();
-
+
var cursor = buffer.Slice(0);
data.CopyTo(cursor);
cursor = cursor.Slice(data.Length);
-
+
cursor[0] = paddingStart;
-
+
return buffer.ToArray();
}
}
\ No newline at end of file
diff --git a/src/OSDP.Net/Messages/SecureChannel/SecurityBlockType.cs b/src/OSDP.Net/Messages/SecureChannel/SecurityBlockType.cs
index 9c8c11fa..01d63f9f 100644
--- a/src/OSDP.Net/Messages/SecureChannel/SecurityBlockType.cs
+++ b/src/OSDP.Net/Messages/SecureChannel/SecurityBlockType.cs
@@ -3,7 +3,7 @@ namespace OSDP.Net.Messages.SecureChannel
///
/// Security Block Type values as defined by OSDP protocol
///
- internal enum SecurityBlockType : byte
+ public enum SecurityBlockType : byte
{
///
/// SCS_11 - Sent along with osdp_CHLNG command when ACU initiates
diff --git a/src/OSDP.Net/Messages/SecureChannel/SecurityContext.cs b/src/OSDP.Net/Messages/SecureChannel/SecurityContext.cs
index c2800bd2..eaa42959 100644
--- a/src/OSDP.Net/Messages/SecureChannel/SecurityContext.cs
+++ b/src/OSDP.Net/Messages/SecureChannel/SecurityContext.cs
@@ -122,7 +122,7 @@ internal Aes CreateCypher(bool isForSessionSetup, byte[] key = null)
var crypto = Aes.Create();
if (crypto == null)
{
- throw new Exception("Unable to create key algorithm");
+ throw new Exception("Unable to create AES algorithm");
}
if (!isForSessionSetup)
@@ -135,6 +135,7 @@ internal Aes CreateCypher(bool isForSessionSetup, byte[] key = null)
crypto.Mode = CipherMode.ECB;
crypto.Padding = PaddingMode.Zeros;
}
+
crypto.KeySize = 128;
crypto.BlockSize = 128;
crypto.Key = key ?? _securityKey;
@@ -157,13 +158,14 @@ internal static byte[] GenerateKey(Aes aes, params byte[][] input)
{
var buffer = new byte[16];
int currentSize = 0;
+
foreach (byte[] x in input)
{
x.CopyTo(buffer, currentSize);
currentSize += x.Length;
}
+
using var encryptor = aes.CreateEncryptor();
-
return encryptor.TransformFinalBlock(buffer, 0, buffer.Length);
}
diff --git a/src/OSDP.Net/Model/CommandData/ACUReceiveSize.cs b/src/OSDP.Net/Model/CommandData/ACUReceiveSize.cs
index d66efc9f..58b0a33d 100644
--- a/src/OSDP.Net/Model/CommandData/ACUReceiveSize.cs
+++ b/src/OSDP.Net/Model/CommandData/ACUReceiveSize.cs
@@ -1,4 +1,5 @@
using System;
+using System.Text;
using OSDP.Net.Messages;
using OSDP.Net.Messages.SecureChannel;
@@ -42,4 +43,12 @@ public static ACUReceiveSize ParseData(ReadOnlySpan data)
{
return new ACUReceiveSize(Message.ConvertBytesToUnsignedShort(data));
}
+
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($"Max Receive Size: {MaximumReceiveSize} bytes");
+ return sb.ToString();
+ }
}
\ No newline at end of file
diff --git a/src/OSDP.Net/Model/CommandData/BiometricReadData.cs b/src/OSDP.Net/Model/CommandData/BiometricReadData.cs
index 75f036df..c7882445 100644
--- a/src/OSDP.Net/Model/CommandData/BiometricReadData.cs
+++ b/src/OSDP.Net/Model/CommandData/BiometricReadData.cs
@@ -1,4 +1,5 @@
using System;
+using System.Text;
using OSDP.Net.Messages;
using OSDP.Net.Messages.SecureChannel;
@@ -77,5 +78,16 @@ public static BiometricReadData ParseData(ReadOnlySpan data)
(BiometricFormat)data[2],
data[3]);
}
+
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($" Reader #: {ReaderNumber}");
+ sb.AppendLine($"Bio Type: {BiometricType}");
+ sb.AppendLine($" Format: {BiometricFormatType}");
+ sb.AppendLine($" Quality: {Quality}");
+ return sb.ToString();
+ }
}
}
\ No newline at end of file
diff --git a/src/OSDP.Net/Model/CommandData/CommunicationConfiguration.cs b/src/OSDP.Net/Model/CommandData/CommunicationConfiguration.cs
index ea38dcdd..8861df9d 100644
--- a/src/OSDP.Net/Model/CommandData/CommunicationConfiguration.cs
+++ b/src/OSDP.Net/Model/CommandData/CommunicationConfiguration.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using System.Text;
using OSDP.Net.Messages;
using OSDP.Net.Messages.SecureChannel;
@@ -62,4 +63,14 @@ public static CommunicationConfiguration ParseData(ReadOnlySpan data)
{
return new CommunicationConfiguration(data[0], Message.ConvertBytesToInt(data.Slice(1, 4).ToArray()));
}
+
+ ///
+ public override string ToString()
+ {
+ var build = new StringBuilder();
+ build.AppendLine($" Address: {Address}");
+ build.AppendLine($"Baud Rate: {BaudRate}");
+
+ return build.ToString();
+ }
}
\ No newline at end of file
diff --git a/src/OSDP.Net/Model/CommandData/EncryptionKeyConfiguration.cs b/src/OSDP.Net/Model/CommandData/EncryptionKeyConfiguration.cs
index 4029ee77..626d8e4d 100644
--- a/src/OSDP.Net/Model/CommandData/EncryptionKeyConfiguration.cs
+++ b/src/OSDP.Net/Model/CommandData/EncryptionKeyConfiguration.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Text;
using OSDP.Net.Messages;
using OSDP.Net.Messages.SecureChannel;
@@ -62,5 +63,15 @@ public static EncryptionKeyConfiguration ParseData(ReadOnlySpan data)
return new EncryptionKeyConfiguration((KeyType)data[0], data.Slice(2, keyLength).ToArray());
}
+
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($" Key Type: {KeyType}");
+ sb.AppendLine($"Key Length: {KeyData.Length} bytes");
+ sb.AppendLine($" Key Data: {BitConverter.ToString(KeyData)}");
+ return sb.ToString();
+ }
}
}
\ No newline at end of file
diff --git a/src/OSDP.Net/Model/CommandData/GetPIVData.cs b/src/OSDP.Net/Model/CommandData/GetPIVData.cs
index e0890697..c07f9d7b 100644
--- a/src/OSDP.Net/Model/CommandData/GetPIVData.cs
+++ b/src/OSDP.Net/Model/CommandData/GetPIVData.cs
@@ -24,10 +24,10 @@ public GetPIVData(ObjectId objectId, byte elementId, byte dataOffset)
{
ObjectId = objectId switch
{
- Model.CommandData.ObjectId.CardholderUniqueIdentifier => new byte[] { 0x5F, 0xC1, 0x02 },
- Model.CommandData.ObjectId.CertificateForPIVAuthentication => new byte[] { 0x5F, 0xC1, 0x05 },
- Model.CommandData.ObjectId.CertificateForCardAuthentication => new byte[] { 0xDF, 0xC1, 0x01 },
- Model.CommandData.ObjectId.CardholderFingerprintTemplate => new byte[] { 0xDF, 0xC1, 0x03 },
+ Model.CommandData.ObjectId.CardholderUniqueIdentifier => [0x5F, 0xC1, 0x02],
+ Model.CommandData.ObjectId.CertificateForPIVAuthentication => [0x5F, 0xC1, 0x05],
+ Model.CommandData.ObjectId.CertificateForCardAuthentication => [0xDF, 0xC1, 0x01],
+ Model.CommandData.ObjectId.CardholderFingerprintTemplate => [0xDF, 0xC1, 0x03],
_ => throw new ArgumentOutOfRangeException()
};
@@ -113,21 +113,13 @@ public override byte[] BuildData()
}
///
- public override string ToString() => ToString(0);
-
- ///
- /// Returns a string representation of the current object
- ///
- /// Number of ' ' chars to add to beginning of every line
- /// String representation of the current object
- public override string ToString(int indent)
+ public override string ToString()
{
- var padding = new string(' ', indent);
- var build = new StringBuilder();
- build.AppendLine($"{padding} Object ID: {BitConverter.ToString(ObjectId)}");
- build.AppendLine($"{padding} Element ID: {ElementId}");
- build.AppendLine($"{padding}Data Offset: {DataOffset}");
- return build.ToString();
+ var sb = new StringBuilder();
+ sb.AppendLine($" Object ID: {BitConverter.ToString(ObjectId)}");
+ sb.AppendLine($" Element ID: {ElementId}");
+ sb.AppendLine($"Data Offset: {DataOffset}");
+ return sb.ToString();
}
}
diff --git a/src/OSDP.Net/Model/CommandData/ManufacturerSpecific.cs b/src/OSDP.Net/Model/CommandData/ManufacturerSpecific.cs
index 1abf235d..a4642ce6 100644
--- a/src/OSDP.Net/Model/CommandData/ManufacturerSpecific.cs
+++ b/src/OSDP.Net/Model/CommandData/ManufacturerSpecific.cs
@@ -72,20 +72,12 @@ public static ManufacturerSpecific ParseData(ReadOnlySpan data)
}
///
- public override string ToString() => ToString(0);
-
- ///
- /// Returns a string representation of the current object
- ///
- /// Number of ' ' chars to add to beginning of every line
- /// String representation of the current object
- public override string ToString(int indent)
+ public override string ToString()
{
- var padding = new string(' ', indent);
- var build = new StringBuilder();
- build.AppendLine($"{padding}Vendor Code: {BitConverter.ToString(VendorCode.ToArray())}");
- build.AppendLine($"{padding} Data: {BitConverter.ToString(Data.ToArray())}");
- return build.ToString();
+ var sb = new StringBuilder();
+ sb.AppendLine($"Vendor Code: {BitConverter.ToString(VendorCode.ToArray())}");
+ sb.AppendLine($" Data: {BitConverter.ToString(Data.ToArray())}");
+ return sb.ToString();
}
}
}
diff --git a/src/OSDP.Net/Model/CommandData/NoPayloadCommandData.cs b/src/OSDP.Net/Model/CommandData/NoPayloadCommandData.cs
index 8d440831..eef78629 100644
--- a/src/OSDP.Net/Model/CommandData/NoPayloadCommandData.cs
+++ b/src/OSDP.Net/Model/CommandData/NoPayloadCommandData.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using System.Text;
using OSDP.Net.Messages;
using OSDP.Net.Messages.SecureChannel;
@@ -44,4 +45,13 @@ public override byte[] BuildData()
{
return Array.Empty();
}
+
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($"Command Type: {CommandType}");
+ sb.AppendLine($" Payload: (none)");
+ return sb.ToString();
+ }
}
\ No newline at end of file
diff --git a/src/OSDP.Net/Model/CommandData/OutputControl.cs b/src/OSDP.Net/Model/CommandData/OutputControl.cs
index 5379561b..ba21b391 100644
--- a/src/OSDP.Net/Model/CommandData/OutputControl.cs
+++ b/src/OSDP.Net/Model/CommandData/OutputControl.cs
@@ -60,20 +60,12 @@ public static OutputControl ParseData(ReadOnlySpan data)
}
///
- public override string ToString() => ToString(0);
-
- ///
- /// Returns a string representation of the current object
- ///
- /// Number of ' ' chars to add to beginning of every line
- /// String representation of the current object
- public string ToString(int indent)
+ public override string ToString()
{
- var padding = new string(' ', indent);
var sb = new StringBuilder();
- sb.AppendLine($"{padding} Output #: {OutputNumber}");
- sb.AppendLine($"{padding}Ctrl Code: {OutputControlCode}");
- sb.AppendLine($"{padding} Timer: {Timer}");
+ sb.AppendLine($" Output #: {OutputNumber}");
+ sb.AppendLine($"Ctrl Code: {OutputControlCode}");
+ sb.AppendLine($" Timer: {Timer}");
return sb.ToString();
}
}
diff --git a/src/OSDP.Net/Model/CommandData/OutputControls.cs b/src/OSDP.Net/Model/CommandData/OutputControls.cs
index 68d6da78..51533af6 100644
--- a/src/OSDP.Net/Model/CommandData/OutputControls.cs
+++ b/src/OSDP.Net/Model/CommandData/OutputControls.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using OSDP.Net.Messages;
using OSDP.Net.Messages.SecureChannel;
@@ -60,5 +61,22 @@ public static OutputControls ParseData(ReadOnlySpan payloadData)
{
return new OutputControls(SplitData(4, data => OutputControl.ParseData(data), payloadData));
}
+
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($"Output Count: {Controls.Count()}");
+ var index = 1;
+ foreach (var control in Controls)
+ {
+ sb.AppendLine($"Output #{index}:");
+ sb.AppendLine($" Output #: {control.OutputNumber}");
+ sb.AppendLine($" Code: {control.OutputControlCode}");
+ sb.AppendLine($" Time: {control.Timer} (100ms units)");
+ index++;
+ }
+ return sb.ToString();
+ }
}
}
\ No newline at end of file
diff --git a/src/OSDP.Net/Model/CommandData/ReaderBuzzerControl.cs b/src/OSDP.Net/Model/CommandData/ReaderBuzzerControl.cs
index 8e63e90a..299d7e13 100644
--- a/src/OSDP.Net/Model/CommandData/ReaderBuzzerControl.cs
+++ b/src/OSDP.Net/Model/CommandData/ReaderBuzzerControl.cs
@@ -78,26 +78,18 @@ public static ReaderBuzzerControl ParseData(ReadOnlySpan data)
///
public override byte[] BuildData()
{
- return new[] {ReaderNumber, (byte) ToneCode, OnTime, OffTime, Count};
+ return [ReaderNumber, (byte) ToneCode, OnTime, OffTime, Count];
}
///
- public override string ToString() => ToString(0);
-
- ///
- /// Returns a string representation of the current object
- ///
- /// Number of ' ' chars to add to beginning of every line
- /// String representation of the current object
- public new string ToString(int indent)
+ public override string ToString()
{
- var padding = new string(' ', indent);
var sb = new StringBuilder();
- sb.AppendLine($"{padding} Reader #: {ReaderNumber}");
- sb.AppendLine($"{padding}Tone Code: {ToneCode}");
- sb.AppendLine($"{padding} On Time: {OnTime}");
- sb.AppendLine($"{padding} Off Time: {OffTime}");
- sb.AppendLine($"{padding} Count: {Count}");
+ sb.AppendLine($" Reader #: {ReaderNumber}");
+ sb.AppendLine($"Tone Code: {ToneCode}");
+ sb.AppendLine($" On Time: {OnTime}");
+ sb.AppendLine($" Off Time: {OffTime}");
+ sb.AppendLine($" Count: {Count}");
return sb.ToString();
}
}
diff --git a/src/OSDP.Net/Model/CommandData/ReaderLedControls.cs b/src/OSDP.Net/Model/CommandData/ReaderLedControls.cs
index c0dfb33b..e258927a 100644
--- a/src/OSDP.Net/Model/CommandData/ReaderLedControls.cs
+++ b/src/OSDP.Net/Model/CommandData/ReaderLedControls.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using OSDP.Net.Messages;
using OSDP.Net.Messages.SecureChannel;
@@ -63,5 +64,27 @@ public static ReaderLedControls ParseData(ReadOnlySpan payloadData)
{
return new ReaderLedControls(SplitData(14, data => ReaderLedControl.ParseData(data), payloadData));
}
+
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($"LED Count: {Controls.Count()}");
+ var index = 1;
+ foreach (var control in Controls)
+ {
+ sb.AppendLine($"LED #{index}:");
+ sb.AppendLine($" Reader #: {control.ReaderNumber}");
+ sb.AppendLine($" LED #: {control.LedNumber}");
+ sb.AppendLine($"Temp Mode: {control.TemporaryMode}");
+ sb.AppendLine($" On Time: {control.TemporaryOnTime} (100ms)");
+ sb.AppendLine($" Off Time: {control.TemporaryOffTime} (100ms)");
+ sb.AppendLine($" On Color: {control.TemporaryOnColor}");
+ sb.AppendLine($"Off Color: {control.TemporaryOffColor}");
+ sb.AppendLine($" Timer: {control.TemporaryTimer} (100ms)");
+ index++;
+ }
+ return sb.ToString();
+ }
}
}
\ No newline at end of file
diff --git a/src/OSDP.Net/Model/CommandData/ReaderTextOutput.cs b/src/OSDP.Net/Model/CommandData/ReaderTextOutput.cs
index bb1977fb..d24176cd 100644
--- a/src/OSDP.Net/Model/CommandData/ReaderTextOutput.cs
+++ b/src/OSDP.Net/Model/CommandData/ReaderTextOutput.cs
@@ -87,25 +87,15 @@ public override byte[] BuildData()
}
///
- public override string ToString() => ToString(0);
-
- ///
- /// Returns a string representation of the current object
- ///
- /// Number of ' ' chars to add to beginning of every line
- /// String representation of the current object
- public new string ToString(int indent)
+ public override string ToString()
{
- string padding = new string(' ', indent);
-
- var build = new StringBuilder();
- build.AppendLine($"{padding}Reader Number: {ReaderNumber}");
- build.AppendLine($"{padding} Text Command: {TextCommand}");
- build.AppendLine($"{padding}Temp Text Time: {TemporaryTextTime}");
- build.AppendLine($"{padding} Row, Column: {Row}, {Column}");
- build.AppendLine($"{padding} Display Text: {Text}");
-
- return build.ToString();
+ var sb = new StringBuilder();
+ sb.AppendLine($"Reader Number: {ReaderNumber}");
+ sb.AppendLine($" Text Command: {TextCommand}");
+ sb.AppendLine($"Temp Text Time: {TemporaryTextTime}");
+ sb.AppendLine($" Row, Column: {Row}, {Column}");
+ sb.AppendLine($" Display Text: {Text}");
+ return sb.ToString();
}
}
diff --git a/src/OSDP.Net/Model/Packet.cs b/src/OSDP.Net/Model/Packet.cs
index 0389bc65..26570eb8 100644
--- a/src/OSDP.Net/Model/Packet.cs
+++ b/src/OSDP.Net/Model/Packet.cs
@@ -65,9 +65,9 @@ internal Packet(IncomingMessage message)
public bool IsUsingCrc { get; }
///
- /// The parse the payload data into an object
+ /// Parse the payload data into an object
///
- /// An message data object representation of the payload data
+ /// A message data object representation of the payload data
public object ParsePayloadData()
{
if (IncomingMessage.HasSecureData && !IncomingMessage.IsValidMac)
@@ -134,7 +134,7 @@ public object ParsePayloadData()
switch (ReplyType)
{
case Messages.ReplyType.Ack:
- return null;
+ break;
case Messages.ReplyType.Nak:
return Nak.ParseData(RawPayloadData);
case Messages.ReplyType.PdIdReport:
@@ -152,7 +152,7 @@ public object ParsePayloadData()
case Messages.ReplyType.RawReaderData:
return RawCardData.ParseData(RawPayloadData);
case Messages.ReplyType.FormattedReaderData:
- return null;
+ break;
case Messages.ReplyType.KeypadData:
return KeypadData.ParseData(RawPayloadData);
case Messages.ReplyType.PdCommunicationsConfigurationReport:
@@ -166,7 +166,7 @@ public object ParsePayloadData()
case Messages.ReplyType.InitialRMac:
return _rawPayloadData;
case Messages.ReplyType.Busy:
- return null;
+ break;
case Messages.ReplyType.FileTransferStatus:
return FileTransferStatus.ParseData(RawPayloadData);
case Messages.ReplyType.PIVData:
@@ -176,7 +176,7 @@ public object ParsePayloadData()
case Messages.ReplyType.ManufactureSpecific:
return ReplyData.ManufacturerSpecific.ParseData(RawPayloadData);
case Messages.ReplyType.ExtendedRead:
- return null;
+ break;
}
return null;
diff --git a/src/OSDP.Net/OSDP.Net.csproj b/src/OSDP.Net/OSDP.Net.csproj
index f13bf8d8..54a544c3 100644
--- a/src/OSDP.Net/OSDP.Net.csproj
+++ b/src/OSDP.Net/OSDP.Net.csproj
@@ -2,21 +2,19 @@
OSDP.Net
- Jonathan Horvath
OSDP.Net is a .NET framework implementation of the Open Supervised Device Protocol(OSDP).
- Apache-2.0
icon.png
default
- net6.0;netstandard2.0;net8.0
+ netstandard2.0;net8.0
true
- true
- snupkg
- https://github.com/bytedreamer/OSDP.Net.git
- git
- true
- true
+
+
+ <_Parameter1>OSDP.Net.Tests
+
+
+
true
@@ -25,18 +23,29 @@
-
+
-
-
+
+
+
+
diff --git a/src/OSDP.Net/SecureChannel.cs b/src/OSDP.Net/SecureChannel.cs
deleted file mode 100644
index 97cdecd2..00000000
--- a/src/OSDP.Net/SecureChannel.cs
+++ /dev/null
@@ -1,244 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Security.Cryptography;
-
-namespace OSDP.Net;
-
-internal class SecureChannel
-{
- private readonly byte[] _serverRandomNumber = new byte[8];
- private byte[] _cmac = new byte[16];
- private byte[] _enc = new byte[16];
- private byte[] _rmac = new byte[16];
- private byte[] _smac1 = new byte[16];
- private byte[] _smac2 = new byte[16];
-
- public SecureChannel()
- {
- CreateNewRandomNumber();
- IsInitialized = false;
- IsEstablished = false;
- }
-
- public byte[] ServerCryptogram { get; private set; }
-
- public bool IsInitialized { get; private set; }
-
- public bool IsEstablished { get; private set; }
-
- public IEnumerable ServerRandomNumber() => _serverRandomNumber;
-
- public void Initialize(byte[] clientRandomNumber, byte[] clientCryptogram, byte[] secureChannelKey)
- {
- using var keyAlgorithm = CreateKeyAlgorithm();
- _enc = GenerateKey(keyAlgorithm,
- new byte[]
- {
- 0x01, 0x82, _serverRandomNumber[0], _serverRandomNumber[1], _serverRandomNumber[2],
- _serverRandomNumber[3], _serverRandomNumber[4], _serverRandomNumber[5]
- }, new byte[8], secureChannelKey);
-
- if (!clientCryptogram.SequenceEqual(GenerateKey(keyAlgorithm,
- _serverRandomNumber, clientRandomNumber, _enc)))
- {
- throw new Exception("Invalid client cryptogram");
- }
-
- _smac1 = GenerateKey(keyAlgorithm,
- new byte[]
- {
- 0x01, 0x01, _serverRandomNumber[0], _serverRandomNumber[1], _serverRandomNumber[2],
- _serverRandomNumber[3], _serverRandomNumber[4], _serverRandomNumber[5]
- }, new byte[8], secureChannelKey);
- _smac2 = GenerateKey(keyAlgorithm,
- new byte[]
- {
- 0x01, 0x02, _serverRandomNumber[0], _serverRandomNumber[1], _serverRandomNumber[2],
- _serverRandomNumber[3], _serverRandomNumber[4], _serverRandomNumber[5]
- }, new byte[8], secureChannelKey);
-
- ServerCryptogram = GenerateKey(keyAlgorithm, clientRandomNumber, _serverRandomNumber, _enc);
- IsInitialized = true;
- }
-
- public void Establish(byte[] rmac)
- {
- _rmac = rmac;
- IsEstablished = true;
- }
-
- public ReadOnlySpan GenerateMac(ReadOnlySpan message, bool isCommand)
- {
- const byte cryptoLength = 16;
- const byte paddingStart = 0x80;
-
- Span mac = stackalloc byte[cryptoLength];
- mac.Clear();
- int currentLocation = 0;
-
- using var messageAuthenticationCodeAlgorithm = Aes.Create();
-
- if (messageAuthenticationCodeAlgorithm == null)
- {
- throw new Exception("Unable to create key algorithm");
- }
-
- messageAuthenticationCodeAlgorithm.Mode = CipherMode.CBC;
- messageAuthenticationCodeAlgorithm.KeySize = 128;
- messageAuthenticationCodeAlgorithm.BlockSize = 128;
- messageAuthenticationCodeAlgorithm.Padding = PaddingMode.None;
- messageAuthenticationCodeAlgorithm.IV = isCommand ? _rmac : _cmac;
- messageAuthenticationCodeAlgorithm.Key = _smac1;
-
- int messageLength = message.Length;
- while (currentLocation < messageLength)
- {
- // Get first 16
- var inputBuffer = new byte[cryptoLength];
- message.Slice(currentLocation,
- currentLocation + cryptoLength < messageLength
- ? cryptoLength
- : messageLength - currentLocation)
- .CopyTo(inputBuffer);
-
- currentLocation += cryptoLength;
- if (currentLocation > messageLength)
- {
- messageAuthenticationCodeAlgorithm.Key = _smac2;
- if (messageLength % cryptoLength != 0)
- {
- inputBuffer[messageLength % cryptoLength] = paddingStart;
- }
- }
-
- using (var encryptor = messageAuthenticationCodeAlgorithm.CreateEncryptor())
- {
- mac = encryptor.TransformFinalBlock(inputBuffer.ToArray(), 0, inputBuffer.Length);
- }
-
- messageAuthenticationCodeAlgorithm.IV = mac.ToArray();
- }
-
- if (isCommand)
- {
- _cmac = mac.ToArray();
- }
- else
- {
- _rmac = mac.ToArray();
- }
-
- return mac.ToArray();
- }
-
- public IEnumerable DecryptData(ReadOnlySpan data)
- {
- const byte paddingStart = 0x80;
-
- using var messageAuthenticationCodeAlgorithm = Aes.Create();
- if (messageAuthenticationCodeAlgorithm == null)
- {
- throw new Exception("Unable to create key algorithm");
- }
-
- messageAuthenticationCodeAlgorithm.Mode = CipherMode.CBC;
- messageAuthenticationCodeAlgorithm.KeySize = 128;
- messageAuthenticationCodeAlgorithm.BlockSize = 128;
- messageAuthenticationCodeAlgorithm.Padding = PaddingMode.None;
- messageAuthenticationCodeAlgorithm.IV = _cmac.Select(b => (byte) ~b).ToArray();
- messageAuthenticationCodeAlgorithm.Key = _enc;
-
- List decryptedData = new List();
-
- using (var encryptor = messageAuthenticationCodeAlgorithm.CreateDecryptor())
- {
- decryptedData.AddRange(encryptor.TransformFinalBlock(data.ToArray(), 0, data.Length));
- }
-
- while (decryptedData.Any() && decryptedData.Last() != paddingStart)
- {
- decryptedData.RemoveAt(decryptedData.Count - 1);
- }
-
- if (decryptedData.Any() && decryptedData.Last() == paddingStart)
- {
- decryptedData.RemoveAt(decryptedData.Count - 1);
- }
-
- return decryptedData;
- }
-
- public ReadOnlySpan EncryptData(ReadOnlySpan data)
- {
- const byte cryptoLength = 16;
- const byte paddingStart = 0x80;
-
- using var messageAuthenticationCodeAlgorithm = Aes.Create();
- if (messageAuthenticationCodeAlgorithm == null)
- {
- throw new Exception("Unable to create key algorithm");
- }
-
- messageAuthenticationCodeAlgorithm.Mode = CipherMode.CBC;
- messageAuthenticationCodeAlgorithm.KeySize = 128;
- messageAuthenticationCodeAlgorithm.BlockSize = 128;
- messageAuthenticationCodeAlgorithm.Padding = PaddingMode.None;
- messageAuthenticationCodeAlgorithm.IV = _rmac.Select(b => (byte) ~b).ToArray();
- messageAuthenticationCodeAlgorithm.Key = _enc;
-
- var paddedData = PadTheData(data, cryptoLength, paddingStart);
-
- using var encryptor = messageAuthenticationCodeAlgorithm.CreateEncryptor();
- return encryptor.TransformFinalBlock(paddedData, 0, paddedData.Length);
- }
-
- private static byte[] PadTheData(ReadOnlySpan data, byte cryptoLength, byte paddingStart)
- {
- int dataLength = data.Length + 1;
- int paddingLength = dataLength + (cryptoLength - dataLength % cryptoLength) % cryptoLength;
-
- Span buffer = stackalloc byte[paddingLength];
- buffer.Clear();
-
- var cursor = buffer.Slice(0);
-
- data.CopyTo(cursor);
- cursor = cursor.Slice(data.Length);
-
- cursor[0] = paddingStart;
-
- return buffer.ToArray();
- }
-
- private static Aes CreateKeyAlgorithm()
- {
- var keyAlgorithm = Aes.Create();
- if (keyAlgorithm == null)
- {
- throw new Exception("Unable to create key algorithm");
- }
-
- keyAlgorithm.Mode = CipherMode.ECB;
- keyAlgorithm.KeySize = 128;
- keyAlgorithm.BlockSize = 128;
- keyAlgorithm.Padding = PaddingMode.Zeros;
- return keyAlgorithm;
- }
-
- private static byte[] GenerateKey(SymmetricAlgorithm algorithm, byte[] first, byte[] second, byte[] key)
- {
- var buffer = new byte[16];
- first.CopyTo(buffer, 0);
- second.CopyTo(buffer, 8);
-
- algorithm.Key = key;
- using var encryptor = algorithm.CreateEncryptor();
- return encryptor.TransformFinalBlock(buffer, 0, buffer.Length);
- }
-
- public void CreateNewRandomNumber()
- {
- new Random().NextBytes(_serverRandomNumber);
- }
-}
\ No newline at end of file
diff --git a/src/PDConsole/Configuration/Settings.cs b/src/PDConsole/Configuration/Settings.cs
new file mode 100644
index 00000000..e9cf359a
--- /dev/null
+++ b/src/PDConsole/Configuration/Settings.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using OSDP.Net.Model.ReplyData;
+
+namespace PDConsole.Configuration
+{
+ public class Settings
+ {
+ public ConnectionSettings Connection { get; set; } = new();
+
+ public DeviceSettings Device { get; set; } = new();
+
+ public SecuritySettings Security { get; set; } = new();
+
+ public bool EnableLogging { get; set; } = true;
+
+ public bool EnableTracing { get; set; } = false;
+ }
+
+ public class ConnectionSettings
+ {
+ public ConnectionType Type { get; set; } = ConnectionType.Serial;
+
+ public string SerialPortName { get; set; } = "COM3";
+
+ public int SerialBaudRate { get; set; } = 9600;
+
+ public string TcpServerAddress { get; set; } = "0.0.0.0";
+
+ public int TcpServerPort { get; set; } = 12000;
+ }
+
+ public enum ConnectionType
+ {
+ Serial,
+ TcpServer
+ }
+
+ public class DeviceSettings
+ {
+ public byte Address { get; set; } = 0;
+
+ public bool UseCrc { get; set; } = true;
+
+ public string VendorCode { get; set; } = "000000";
+
+ public string Model { get; set; } = "PDConsole";
+
+ public string SerialNumber { get; set; } = "123456789";
+
+ public byte FirmwareMajor { get; set; } = 1;
+
+ public byte FirmwareMinor { get; set; } = 0;
+
+ public byte FirmwareBuild { get; set; } = 0;
+
+ public List Capabilities { get; set; } = new()
+ {
+ new DeviceCapability(CapabilityFunction.CardDataFormat, 1, 1),
+ new DeviceCapability(CapabilityFunction.ReaderLEDControl, 1, 2),
+ new DeviceCapability(CapabilityFunction.ReaderAudibleOutput, 1, 1),
+ new DeviceCapability(CapabilityFunction.ReaderTextOutput, 1, 1),
+ new DeviceCapability(CapabilityFunction.CheckCharacterSupport, 1, 0),
+ new DeviceCapability(CapabilityFunction.CommunicationSecurity, 1, 1),
+ new DeviceCapability(CapabilityFunction.OSDPVersion, 2, 0)
+ };
+ }
+
+ public class SecuritySettings
+ {
+ public static readonly byte[] DefaultKey =
+ [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F];
+
+ public bool RequireSecureChannel { get; set; } = false;
+
+ public byte[] SecureChannelKey { get; set; } = DefaultKey;
+ }
+}
\ No newline at end of file
diff --git a/src/PDConsole/IPDConsoleController.cs b/src/PDConsole/IPDConsoleController.cs
new file mode 100644
index 00000000..1ebe7aa6
--- /dev/null
+++ b/src/PDConsole/IPDConsoleController.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using PDConsole.Configuration;
+
+namespace PDConsole
+{
+ ///
+ /// Interface for PDConsole controller to enable testing and alternative implementations
+ ///
+ public interface IPDConsoleController : IDisposable
+ {
+ // Events
+ event EventHandler CommandReceived;
+ event EventHandler StatusChanged;
+ event EventHandler ConnectionStatusChanged;
+ event EventHandler ErrorOccurred;
+
+ // Properties
+ bool IsDeviceRunning { get; }
+ IReadOnlyList CommandHistory { get; }
+ Settings Settings { get; }
+
+ // Methods
+ void StartDevice();
+ void StopDevice();
+ void SendSimulatedCardRead(string cardData);
+ void SimulateKeypadEntry(string keys);
+ void ClearHistory();
+ string GetDeviceStatusText();
+ }
+}
\ No newline at end of file
diff --git a/src/PDConsole/PDConsole.csproj b/src/PDConsole/PDConsole.csproj
new file mode 100644
index 00000000..52886a42
--- /dev/null
+++ b/src/PDConsole/PDConsole.csproj
@@ -0,0 +1,38 @@
+
+
+
+ Exe
+ net8.0
+ default
+ PDConsole
+ PDConsole
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
\ No newline at end of file
diff --git a/src/PDConsole/PDConsoleController.cs b/src/PDConsole/PDConsoleController.cs
new file mode 100644
index 00000000..eddef991
--- /dev/null
+++ b/src/PDConsole/PDConsoleController.cs
@@ -0,0 +1,193 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using OSDP.Net;
+using OSDP.Net.Connections;
+using PDConsole.Configuration;
+
+namespace PDConsole
+{
+ ///
+ /// Controller class that manages the PDConsole business logic and device interactions
+ ///
+ public class PDConsoleController(Settings settings) : IPDConsoleController
+ {
+ private readonly Settings _settings = settings ?? throw new ArgumentNullException(nameof(settings));
+ private readonly List _commandHistory = new();
+
+ private PDDevice _device;
+ private IOsdpConnectionListener _connectionListener;
+ private CancellationTokenSource _cancellationTokenSource;
+
+ // Events
+ public event EventHandler CommandReceived;
+ public event EventHandler StatusChanged;
+ public event EventHandler ConnectionStatusChanged;
+ public event EventHandler ErrorOccurred;
+
+ // Properties
+ public bool IsDeviceRunning => _device != null && _connectionListener != null;
+ public IReadOnlyList CommandHistory => _commandHistory.AsReadOnly();
+ public Settings Settings => _settings;
+
+ // Device Control Methods
+ public void StartDevice()
+ {
+ if (IsDeviceRunning)
+ {
+ throw new InvalidOperationException("Device is already running");
+ }
+
+ try
+ {
+ _cancellationTokenSource = new CancellationTokenSource();
+
+ // Create device configuration
+ var deviceConfig = new DeviceConfiguration
+ {
+ Address = _settings.Device.Address,
+ RequireSecurity = _settings.Security.RequireSecureChannel,
+ SecurityKey = _settings.Security.SecureChannelKey
+ };
+
+ // Create the device
+ _device = new PDDevice(deviceConfig, _settings.Device);
+ _device.CommandReceived += OnDeviceCommandReceived;
+
+ // Create a connection listener based on type
+ _connectionListener = CreateConnectionListener();
+
+ // Start listening
+ _device.StartListening(_connectionListener);
+
+ var connectionString = GetConnectionString();
+ ConnectionStatusChanged?.Invoke(this, $"Listening on {connectionString}");
+ StatusChanged?.Invoke(this, "Device started successfully");
+ }
+ catch (Exception ex)
+ {
+ StopDevice();
+ ErrorOccurred?.Invoke(this, ex);
+ throw;
+ }
+ }
+
+ public void StopDevice()
+ {
+ try
+ {
+ _cancellationTokenSource?.Cancel();
+ _connectionListener?.Dispose();
+
+ if (_device != null)
+ {
+ _device.CommandReceived -= OnDeviceCommandReceived;
+ _ = _device.StopListening();
+ }
+
+ ConnectionStatusChanged?.Invoke(this, "Not Started");
+ StatusChanged?.Invoke(this, "Device stopped");
+ }
+ catch (Exception ex)
+ {
+ ErrorOccurred?.Invoke(this, ex);
+ }
+ finally
+ {
+ _device = null;
+ _connectionListener = null;
+ _cancellationTokenSource = null;
+ }
+ }
+
+ public void SendSimulatedCardRead(string cardData)
+ {
+ if (!IsDeviceRunning)
+ {
+ throw new InvalidOperationException("Device is not running");
+ }
+
+ if (string.IsNullOrEmpty(cardData))
+ {
+ throw new ArgumentException("Card data cannot be empty", nameof(cardData));
+ }
+
+ _device.SendSimulatedCardRead(cardData);
+ }
+
+ public void SimulateKeypadEntry(string keys)
+ {
+ if (!IsDeviceRunning)
+ {
+ throw new InvalidOperationException("Device is not running");
+ }
+
+ if (string.IsNullOrEmpty(keys))
+ {
+ throw new ArgumentException("Keypad data cannot be empty", nameof(keys));
+ }
+
+ _device.SimulateKeypadEntry(keys);
+ }
+
+ public void ClearHistory()
+ {
+ _commandHistory.Clear();
+ StatusChanged?.Invoke(this, "Command history cleared");
+ }
+
+ public string GetDeviceStatusText()
+ {
+ return $"Address: {_settings.Device.Address} | Security: {(_settings.Security.RequireSecureChannel ? "Enabled" : "Disabled")}";
+ }
+
+ // Private Methods
+ private IOsdpConnectionListener CreateConnectionListener()
+ {
+ switch (_settings.Connection.Type)
+ {
+ case ConnectionType.Serial:
+ return new SerialPortConnectionListener(
+ _settings.Connection.SerialPortName,
+ _settings.Connection.SerialBaudRate);
+
+ case ConnectionType.TcpServer:
+ return new TcpConnectionListener(
+ _settings.Connection.TcpServerPort,
+ 9600);
+
+ default:
+ throw new NotSupportedException($"Connection type {_settings.Connection.Type} not supported");
+ }
+ }
+
+ private string GetConnectionString()
+ {
+ return _settings.Connection.Type switch
+ {
+ ConnectionType.Serial => $"{_settings.Connection.SerialPortName} @ {_settings.Connection.SerialBaudRate}",
+ ConnectionType.TcpServer => $"{_settings.Connection.TcpServerAddress}:{_settings.Connection.TcpServerPort}",
+ _ => "Unknown"
+ };
+ }
+
+ private void OnDeviceCommandReceived(object sender, CommandEvent e)
+ {
+ _commandHistory.Add(e);
+
+ // Keep only the last 100 commands
+ if (_commandHistory.Count > 100)
+ {
+ _commandHistory.RemoveAt(0);
+ }
+
+ CommandReceived?.Invoke(this, e);
+ }
+
+ public void Dispose()
+ {
+ StopDevice();
+ _cancellationTokenSource?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PDConsole/PDConsoleView.cs b/src/PDConsole/PDConsoleView.cs
new file mode 100644
index 00000000..a1d87078
--- /dev/null
+++ b/src/PDConsole/PDConsoleView.cs
@@ -0,0 +1,355 @@
+using System;
+using System.Linq;
+using Terminal.Gui;
+
+namespace PDConsole
+{
+ ///
+ /// View class that handles all Terminal.Gui UI elements and interactions
+ ///
+ public class PDConsoleView
+ {
+ private readonly IPDConsoleController _controller;
+
+ // UI Controls
+ private Window _mainWindow;
+ private Label _statusLabel;
+ private Label _connectionLabel;
+ private ListView _commandHistoryView;
+ private TextField _cardDataField;
+ private TextField _keypadField;
+ private Button _sendCardButton;
+ private Button _sendKeypadButton;
+
+ public PDConsoleView(IPDConsoleController controller)
+ {
+ _controller = controller ?? throw new ArgumentNullException(nameof(controller));
+
+ // Subscribe to controller events
+ _controller.CommandReceived += OnCommandReceived;
+ _controller.StatusChanged += OnStatusChanged;
+ _controller.ConnectionStatusChanged += OnConnectionStatusChanged;
+ _controller.ErrorOccurred += OnErrorOccurred;
+ }
+
+ public Window CreateMainWindow()
+ {
+ _mainWindow = new Window("OSDP.Net PD Console")
+ {
+ X = 0,
+ Y = 1,
+ Width = Dim.Fill(),
+ Height = Dim.Fill()
+ };
+
+ // Create a menu bar
+ Application.Top.Add(CreateMenuBar());
+
+ // Create UI sections
+ var statusFrame = CreateStatusFrame();
+ var simulationFrame = CreateSimulationFrame();
+ var historyFrame = CreateHistoryFrame();
+
+ // Position frames
+ statusFrame.X = 0;
+ statusFrame.Y = 0;
+
+ simulationFrame.X = 0;
+ simulationFrame.Y = Pos.Bottom(statusFrame);
+
+ historyFrame.X = 0;
+ historyFrame.Y = Pos.Bottom(simulationFrame);
+
+ _mainWindow.Add(statusFrame, simulationFrame, historyFrame);
+
+ return _mainWindow;
+ }
+
+ private MenuBar CreateMenuBar()
+ {
+ return new MenuBar([
+ new MenuBarItem("_File", [
+ new MenuItem("_Settings", "", ShowSettingsDialog),
+ new MenuItem("_Quit", "", () => Application.RequestStop())
+ ]),
+ new MenuBarItem("_Device", [
+ new MenuItem("_Start", "", StartDevice),
+ new MenuItem("S_top", "", StopDevice),
+ new MenuItem("_Clear History", "", ClearHistory)
+ ])
+ ]);
+ }
+
+ private FrameView CreateStatusFrame()
+ {
+ var frame = new FrameView("Device Status")
+ {
+ Width = Dim.Fill(),
+ Height = 5
+ };
+
+ _connectionLabel = new Label("Connection: Not Started")
+ {
+ X = 1,
+ Y = 0
+ };
+
+ _statusLabel = new Label(_controller.GetDeviceStatusText())
+ {
+ X = 1,
+ Y = 1
+ };
+
+ frame.Add(_connectionLabel, _statusLabel);
+ return frame;
+ }
+
+ private FrameView CreateSimulationFrame()
+ {
+ var frame = new FrameView("Simulation Controls")
+ {
+ Width = Dim.Fill(),
+ Height = 6
+ };
+
+ // Card data controls
+ var cardDataLabel = new Label("Card Data (Hex):")
+ {
+ X = 1,
+ Y = 1
+ };
+
+ _cardDataField = new TextField("0123456789ABCDEF")
+ {
+ X = Pos.Right(cardDataLabel) + 1,
+ Y = 1,
+ Width = 30
+ };
+
+ _sendCardButton = new Button("Send Card")
+ {
+ X = Pos.Right(_cardDataField) + 1,
+ Y = 1
+ };
+ _sendCardButton.Clicked += SendCardClicked;
+
+ // Keypad controls
+ var keypadLabel = new Label("Keypad Data:")
+ {
+ X = 1,
+ Y = 3
+ };
+
+ _keypadField = new TextField("1234")
+ {
+ X = Pos.Right(keypadLabel) + 1,
+ Y = 3,
+ Width = 20
+ };
+
+ _sendKeypadButton = new Button("Send Keypad")
+ {
+ X = Pos.Right(_keypadField) + 1,
+ Y = 3
+ };
+ _sendKeypadButton.Clicked += SendKeypadClicked;
+
+ frame.Add(cardDataLabel, _cardDataField, _sendCardButton,
+ keypadLabel, _keypadField, _sendKeypadButton);
+
+ return frame;
+ }
+
+ private FrameView CreateHistoryFrame()
+ {
+ var frame = new FrameView("Command History")
+ {
+ Width = Dim.Fill(),
+ Height = Dim.Fill()
+ };
+
+ _commandHistoryView = new ListView()
+ {
+ X = 0,
+ Y = 0,
+ Width = Dim.Fill(),
+ Height = Dim.Fill()
+ };
+ _commandHistoryView.OpenSelectedItem += ShowCommandDetails;
+
+ frame.Add(_commandHistoryView);
+ return frame;
+ }
+
+ // UI Event Handlers
+ private void StartDevice()
+ {
+ try
+ {
+ _controller.StartDevice();
+ UpdateButtonStates();
+ }
+ catch (Exception ex)
+ {
+ MessageBox.ErrorQuery("Error", $"Failed to start device: {ex.Message}", "OK");
+ }
+ }
+
+ private void StopDevice()
+ {
+ try
+ {
+ _controller.StopDevice();
+ UpdateButtonStates();
+ }
+ catch (Exception ex)
+ {
+ MessageBox.ErrorQuery("Error", $"Failed to stop device: {ex.Message}", "OK");
+ }
+ }
+
+ private void ClearHistory()
+ {
+ _controller.ClearHistory();
+ UpdateCommandHistoryView();
+ }
+
+ private void SendCardClicked()
+ {
+ try
+ {
+ var cardData = _cardDataField.Text.ToString();
+ _controller.SendSimulatedCardRead(cardData);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.ErrorQuery("Error", $"Failed to send card data: {ex.Message}", "OK");
+ }
+ }
+
+ private void SendKeypadClicked()
+ {
+ try
+ {
+ var keys = _keypadField.Text.ToString();
+ _controller.SimulateKeypadEntry(keys);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.ErrorQuery("Error", $"Failed to send keypad data: {ex.Message}", "OK");
+ }
+ }
+
+ private void ShowCommandDetails(ListViewItemEventArgs e)
+ {
+ if (e.Item >= 0 && e.Item < _controller.CommandHistory.Count)
+ {
+ var commandEvent = _controller.CommandHistory[e.Item];
+ ShowCommandDetailsDialog(commandEvent);
+ }
+ }
+
+ private void ShowCommandDetailsDialog(CommandEvent commandEvent)
+ {
+ var details = string.IsNullOrEmpty(commandEvent.Details)
+ ? "No additional details available."
+ : commandEvent.Details;
+
+ var dialog = new Dialog("Command Details")
+ {
+ Width = Dim.Percent(80),
+ Height = Dim.Percent(70)
+ };
+
+ var textView = new TextView()
+ {
+ X = 1,
+ Y = 1,
+ Width = Dim.Fill(1),
+ Height = Dim.Fill(2),
+ ReadOnly = true,
+ Text = $" Command: {commandEvent.Description}\n" +
+ $" Time: {commandEvent.Timestamp:s} {commandEvent.Timestamp:t}\n" +
+ $"\n" +
+ $" {new string('ā', 60)}\n" +
+ $"\n" +
+ string.Join("\n", details.Split('\n').Select(line => $" {line}"))
+ };
+
+ var okButton = new Button("OK")
+ {
+ X = Pos.Center(),
+ Y = Pos.Bottom(dialog) - 3,
+ IsDefault = true
+ };
+ okButton.Clicked += () => Application.RequestStop(dialog);
+
+ dialog.Add(textView, okButton);
+ dialog.AddButton(okButton);
+
+ Application.Run(dialog);
+ }
+
+ private void ShowSettingsDialog()
+ {
+ MessageBox.Query("Settings", "Settings dialog not yet implemented.\nEdit appsettings.json manually.", "OK");
+ }
+
+ // Controller Event Handlers
+ private void OnCommandReceived(object sender, CommandEvent e)
+ {
+ Application.MainLoop.Invoke(UpdateCommandHistoryView);
+ }
+
+ private void OnStatusChanged(object sender, string status)
+ {
+ Application.MainLoop.Invoke(() =>
+ {
+ // You could add a status bar or status message area if needed
+ });
+ }
+
+ private void OnConnectionStatusChanged(object sender, string status)
+ {
+ Application.MainLoop.Invoke(() =>
+ {
+ if (_connectionLabel != null)
+ _connectionLabel.Text = $"Connection: {status}";
+ });
+ }
+
+ private void OnErrorOccurred(object sender, Exception ex)
+ {
+ Application.MainLoop.Invoke(() =>
+ {
+ MessageBox.ErrorQuery("Error", ex.Message, "OK");
+ });
+ }
+
+ // Helper Methods
+ private void UpdateCommandHistoryView()
+ {
+ var displayItems = _controller.CommandHistory
+ .Select(e => $"{e.Timestamp:T} - {e.Description}")
+ .ToArray();
+
+ _commandHistoryView.SetSource(displayItems);
+
+ if (displayItems.Length == 0) return;
+
+ _commandHistoryView.SelectedItem = displayItems.Length - 1;
+ _commandHistoryView.EnsureSelectedItemVisible();
+ }
+
+ private void UpdateButtonStates()
+ {
+ var isRunning = _controller.IsDeviceRunning;
+
+ if (_sendCardButton != null)
+ _sendCardButton.Enabled = isRunning;
+
+ if (_sendKeypadButton != null)
+ _sendKeypadButton.Enabled = isRunning;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PDConsole/PDDevice.cs b/src/PDConsole/PDDevice.cs
new file mode 100644
index 00000000..8a3e44c9
--- /dev/null
+++ b/src/PDConsole/PDDevice.cs
@@ -0,0 +1,216 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using OSDP.Net;
+using OSDP.Net.Model;
+using OSDP.Net.Model.CommandData;
+using OSDP.Net.Model.ReplyData;
+using PDConsole.Configuration;
+using CommunicationConfiguration = OSDP.Net.Model.CommandData.CommunicationConfiguration;
+
+namespace PDConsole
+{
+ public class PDDevice(DeviceConfiguration config, DeviceSettings settings, ILoggerFactory loggerFactory = null)
+ : Device(config, loggerFactory)
+ {
+ private readonly List _commandHistory = new();
+
+ public event EventHandler CommandReceived;
+
+ protected override PayloadData HandleIdReport()
+ {
+ LogCommand("ID Report");
+
+ var vendorCode = ConvertHexStringToBytes(settings.VendorCode, 3);
+ return new DeviceIdentification(
+ vendorCode,
+ (byte)settings.Model[0],
+ settings.FirmwareMajor,
+ settings.FirmwareMinor,
+ settings.FirmwareBuild,
+ (byte)ConvertStringToBytes(settings.SerialNumber, 4),
+ settings.FirmwareBuild);
+ }
+
+ protected override PayloadData HandleDeviceCapabilities()
+ {
+ LogCommand("Device Capabilities");
+ return new DeviceCapabilities(settings.Capabilities.ToArray());
+ }
+
+ protected override PayloadData HandleCommunicationSet(CommunicationConfiguration commandPayload)
+ {
+ LogCommand("Communication Set", commandPayload.ToString());
+
+ return new OSDP.Net.Model.ReplyData.CommunicationConfiguration(
+ commandPayload.Address,
+ commandPayload.BaudRate);
+ }
+
+ protected override PayloadData HandleKeySettings(EncryptionKeyConfiguration commandPayload)
+ {
+ LogCommand("Key Settings", commandPayload);
+ return new Ack();
+ }
+
+ // Override other handlers to just return ACK or NAK
+ protected override PayloadData HandleLocalStatusReport()
+ {
+ LogCommand("Local Status Report");
+ return new Ack(); // Simplified - just return ACK
+ }
+
+ protected override PayloadData HandleInputStatusReport()
+ {
+ LogCommand("Input Status Report");
+ return new Ack(); // Simplified - just return ACK
+ }
+
+ protected override PayloadData HandleOutputStatusReport()
+ {
+ LogCommand("Output Status Report");
+ return new Ack(); // Simplified - just return ACK
+ }
+
+ protected override PayloadData HandleReaderStatusReport()
+ {
+ LogCommand("Reader Status Report");
+ return new Ack(); // Simplified - just return ACK
+ }
+
+ protected override PayloadData HandleReaderLEDControl(ReaderLedControls commandPayload)
+ {
+ LogCommand("LED Control", commandPayload);
+ return new Ack();
+ }
+
+ protected override PayloadData HandleBuzzerControl(ReaderBuzzerControl commandPayload)
+ {
+ LogCommand("Buzzer Control", commandPayload);
+ return new Ack();
+ }
+
+ protected override PayloadData HandleTextOutput(ReaderTextOutput commandPayload)
+ {
+ LogCommand("Text Output", commandPayload);
+ return new Ack();
+ }
+
+ protected override PayloadData HandleOutputControl(OutputControls commandPayload)
+ {
+ LogCommand("Output Control", commandPayload);
+ return new Ack();
+ }
+
+ protected override PayloadData HandleBiometricRead(BiometricReadData commandPayload)
+ {
+ LogCommand("Biometric Read", commandPayload);
+ return new Nak(ErrorCode.UnableToProcessCommand);
+ }
+
+ protected override PayloadData HandleManufacturerCommand(OSDP.Net.Model.CommandData.ManufacturerSpecific commandPayload)
+ {
+ LogCommand("Manufacturer Specific", commandPayload);
+ return new Ack();
+ }
+
+ protected override PayloadData HandlePivData(GetPIVData commandPayload)
+ {
+ LogCommand("Get PIV Data", commandPayload);
+ return new Nak(ErrorCode.UnableToProcessCommand);
+ }
+
+ protected override PayloadData HandleAbortRequest()
+ {
+ LogCommand("Abort Request");
+ return new Ack();
+ }
+
+ // Method to send a simulated card read
+ public void SendSimulatedCardRead(string cardData)
+ {
+ if (!string.IsNullOrEmpty(cardData))
+ {
+ try
+ {
+ var cardBytes = ConvertHexStringToBytes(cardData, cardData.Length / 2);
+ var bitArray = new BitArray(cardBytes);
+
+ // Enqueue the card data reply for the next poll
+ EnqueuePollReply(new RawCardData(0, FormatCode.NotSpecified, bitArray));
+ LogCommand("Simulated Card Read", new { CardData = cardData });
+ }
+ catch (Exception)
+ {
+ LogCommand("Error Simulating Card Read");
+ }
+ }
+ }
+
+ // Method to simulate keypad entry (using formatted card data as a workaround)
+ public void SimulateKeypadEntry(string keys)
+ {
+ if (string.IsNullOrEmpty(keys)) return;
+
+ try
+ {
+ // Note: KeypadData doesn't inherit from PayloadData, so we use FormattedCardData as a workaround
+ EnqueuePollReply(new FormattedCardData(0, ReadDirection.Forward, keys));
+ LogCommand("Simulated Keypad Entry", new { Keys = keys });
+ }
+ catch (Exception)
+ {
+ LogCommand("Error Simulating Keypad Entry");
+ }
+ }
+
+ private void LogCommand(string commandDescription, object payload = null)
+ {
+ var commandEvent = new CommandEvent
+ {
+ Timestamp = DateTime.Now,
+ Description = commandDescription,
+ Details = payload?.ToString() ?? string.Empty
+ };
+
+ _commandHistory.Add(commandEvent);
+ if (_commandHistory.Count > 100) // Keep only the last 100 commands
+ {
+ _commandHistory.RemoveAt(0);
+ }
+
+ CommandReceived?.Invoke(this, commandEvent);
+ }
+
+ private static byte[] ConvertHexStringToBytes(string hex, int expectedLength)
+ {
+ hex = hex.Replace(" ", "").Replace("-", "");
+ var bytes = new byte[expectedLength];
+
+ for (int i = 0; i < Math.Min(hex.Length / 2, expectedLength); i++)
+ {
+ bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
+ }
+
+ return bytes;
+ }
+
+ private static uint ConvertStringToBytes(string str, int byteCount)
+ {
+ uint result = 0;
+ for (int i = 0; i < Math.Min(str.Length, byteCount); i++)
+ {
+ result = (result << 8) | str[i];
+ }
+ return result;
+ }
+ }
+
+ public class CommandEvent
+ {
+ public DateTime Timestamp { get; init; }
+ public string Description { get; init; }
+ public string Details { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/PDConsole/Program.cs b/src/PDConsole/Program.cs
new file mode 100644
index 00000000..51f33593
--- /dev/null
+++ b/src/PDConsole/Program.cs
@@ -0,0 +1,107 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using PDConsole.Configuration;
+using Terminal.Gui;
+
+namespace PDConsole
+{
+ ///
+ /// Main program class
+ ///
+ class Program
+ {
+ private static PDConsoleController _controller;
+ private static PDConsoleView _view;
+
+ static void Main()
+ {
+ try
+ {
+ // Load settings
+ var settings = LoadSettings();
+
+ // Create controller (ViewModel)
+ _controller = new PDConsoleController(settings);
+
+ // Initialize Terminal.Gui
+ Application.Init();
+
+ // Create view
+ _view = new PDConsoleView(_controller);
+
+ // Create and add a main window
+ var mainWindow = _view.CreateMainWindow();
+ Application.Top.Add(mainWindow);
+
+ // Run the application
+ Application.Run();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Fatal error: {ex.Message}");
+ }
+ finally
+ {
+ Cleanup();
+ }
+ }
+
+ private static Settings LoadSettings()
+ {
+ const string settingsFile = "appsettings.json";
+
+ if (File.Exists(settingsFile))
+ {
+ try
+ {
+ var json = File.ReadAllText(settingsFile);
+ return JsonSerializer.Deserialize(json, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Fatal error: {ex.Message}");
+ return new Settings();
+ }
+ }
+ else
+ {
+ var defaultSettings = new Settings();
+ SaveSettings(defaultSettings);
+ return defaultSettings;
+ }
+ }
+
+ private static void SaveSettings(Settings settings)
+ {
+ try
+ {
+ var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions
+ {
+ WriteIndented = true
+ });
+ File.WriteAllText("appsettings.json", json);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Fatal error: {ex.Message}");
+ }
+ }
+
+ private static void Cleanup()
+ {
+ try
+ {
+ _controller?.Dispose();
+ Application.Shutdown();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Fatal error: {ex.Message}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PDConsole/appsettings.json b/src/PDConsole/appsettings.json
new file mode 100644
index 00000000..1654c688
--- /dev/null
+++ b/src/PDConsole/appsettings.json
@@ -0,0 +1,108 @@
+{
+ "Connection": {
+ "Type": "Serial",
+ "SerialPortName": "COM3",
+ "SerialBaudRate": 9600,
+ "TcpServerAddress": "0.0.0.0",
+ "TcpServerPort": 12000
+ },
+ "Device": {
+ "Address": 0,
+ "UseCrc": true,
+ "VendorCode": "000000",
+ "Model": "PDConsole",
+ "SerialNumber": "123456789",
+
+ "FirmwareMajor": 1,
+ "FirmwareMinor": 0,
+ "FirmwareBuild": 0,
+ "Capabilities": [
+ {
+ "Function": "ContactStatusMonitoring",
+ "Compliance": 1,
+ "NumberOf": 0
+ },
+ {
+ "Function": "OutputControl",
+ "Compliance": 1,
+ "NumberOf": 0
+ },
+ {
+ "Function": "CardDataFormat",
+ "Compliance": 1,
+ "NumberOf": 1
+ },
+ {
+ "Function": "ReaderLEDControl",
+ "Compliance": 1,
+ "NumberOf": 2
+ },
+ {
+ "Function": "ReaderAudibleControl",
+ "Compliance": 1,
+ "NumberOf": 1
+ },
+ {
+ "Function": "ReaderTextOutput",
+ "Compliance": 1,
+ "NumberOf": 1
+ },
+ {
+ "Function": "TimeKeeping",
+ "Compliance": 0,
+ "NumberOf": 0
+ },
+ {
+ "Function": "CheckCharacterSupport",
+ "Compliance": 1,
+ "NumberOf": 0
+ },
+ {
+ "Function": "CommunicationSecurity",
+ "Compliance": 1,
+ "NumberOf": 1
+ },
+ {
+ "Function": "ReceiveBufferSize",
+ "Compliance": 0,
+ "NumberOf": 1
+ },
+ {
+ "Function": "CombinedMessageSize",
+ "Compliance": 0,
+ "NumberOf": 1
+ },
+ {
+ "Function": "SmartCardSupport",
+ "Compliance": 0,
+ "NumberOf": 0
+ },
+ {
+ "Function": "ReaderAuthentication",
+ "Compliance": 0,
+ "NumberOf": 0
+ },
+ {
+ "Function": "BiometricSupport",
+ "Compliance": 1,
+ "NumberOf": 1
+ },
+ {
+ "Function": "SecurePinEntrySupport",
+ "Compliance": 0,
+ "NumberOf": 0
+ },
+ {
+ "Function": "OSDPVersion",
+ "Compliance": 2,
+ "NumberOf": 0
+ }
+ ]
+ },
+ "Security": {
+ "RequireSecureChannel": false,
+ "SecureChannelKey": [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
+ },
+ "EnableLogging": true,
+ "EnableTracing": false
+}
\ No newline at end of file
diff --git a/src/PDConsole/log4net.config b/src/PDConsole/log4net.config
new file mode 100644
index 00000000..cf63a2dc
--- /dev/null
+++ b/src/PDConsole/log4net.config
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/samples/CardReader/Program.cs b/src/samples/CardReader/Program.cs
index 9d85070f..fd52df71 100644
--- a/src/samples/CardReader/Program.cs
+++ b/src/samples/CardReader/Program.cs
@@ -38,8 +38,8 @@ private static async Task Main()
};
// Replace commented out code for test reader to listen on TCP port rather than serial
- //var communications = new TcpOsdpServer(5000, baudRate, loggerFactory);
- var communications = new SerialPortOsdpServer(portName, baudRate, loggerFactory);
+ //var communications = new TcpConnectionListener(5000, baudRate, loggerFactory);
+ var communications = new SerialPortConnectionListener(portName, baudRate, loggerFactory);
using var device = new MySampleDevice(deviceConfiguration, loggerFactory);
device.DeviceComSetUpdated += async (sender, args) =>
@@ -52,7 +52,7 @@ private static async Task Main()
if (sender is MySampleDevice mySampleDevice && args.OldBaudRate != args.NewBaudRate)
{
Console.WriteLine("Restarting communications with new baud rate");
- communications = new SerialPortOsdpServer(portName, args.NewBaudRate, loggerFactory);
+ communications = new SerialPortConnectionListener(portName, args.NewBaudRate, loggerFactory);
await mySampleDevice.StopListening();
mySampleDevice.StartListening(communications);
}
diff --git a/src/samples/PivDataReader/PivDataReader.csproj b/src/samples/PivDataReader/PivDataReader.csproj
index 8e089df4..c290663f 100644
--- a/src/samples/PivDataReader/PivDataReader.csproj
+++ b/src/samples/PivDataReader/PivDataReader.csproj
@@ -2,7 +2,7 @@
Exe
- net6.0
+ net8.0
enable
enable
diff --git a/src/samples/SimplePDDevice/Program.cs b/src/samples/SimplePDDevice/Program.cs
new file mode 100644
index 00000000..f8bca9f7
--- /dev/null
+++ b/src/samples/SimplePDDevice/Program.cs
@@ -0,0 +1,89 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using OSDP.Net;
+using OSDP.Net.Connections;
+
+namespace SimplePDDevice;
+
+///
+/// Simple console application that demonstrates a basic OSDP Peripheral Device
+///
+internal class Program
+{
+ private static async Task Main()
+ {
+ Console.WriteLine("Simple OSDP Peripheral Device");
+ Console.WriteLine("============================");
+
+ // Load configuration
+ var config = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
+ .Build();
+
+ var osdpSection = config.GetSection("OSDP");
+
+ // Configuration with defaults
+ int tcpPort = int.Parse(osdpSection["TcpPort"] ?? "4900");
+ byte deviceAddress = byte.Parse(osdpSection["DeviceAddress"] ?? "1");
+ bool requireSecurity = bool.Parse(osdpSection["RequireSecurity"] ?? "false");
+ var securityKey = System.Text.Encoding.ASCII.GetBytes(osdpSection["SecurityKey"] ?? "0011223344556677889900AABBCCDDEEFF");
+
+ // Setup logging
+ var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder
+ .AddConsole()
+ .SetMinimumLevel(LogLevel.Information)
+ .AddFilter("SimplePDDevice", LogLevel.Information)
+ .AddFilter("OSDP.Net", LogLevel.Warning); // Reduce OSDP.Net noise
+ });
+
+ var logger = loggerFactory.CreateLogger();
+
+ // Device configuration
+ var deviceConfiguration = new DeviceConfiguration
+ {
+ Address = deviceAddress,
+ RequireSecurity = requireSecurity,
+ SecurityKey = securityKey
+ };
+
+ // Setup TCP connection listener
+ var connectionListener = new TcpConnectionListener(tcpPort, 9600);
+
+ // Create and start the device
+ using var device = new SimplePDDevice(deviceConfiguration);
+
+ logger.LogInformation("Starting OSDP Peripheral Device on TCP port {Port}", tcpPort);
+ logger.LogInformation("Device Address: {Address}", deviceAddress);
+ logger.LogInformation("Security Required: {RequireSecurity}", requireSecurity);
+
+ device.StartListening(connectionListener);
+
+ logger.LogInformation("Device is now listening for ACU connections...");
+ logger.LogInformation("Press 'q' to quit");
+
+ // Simple console loop - check for 'q' or run for 30 seconds, then exit
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+
+ try
+ {
+ while (!cts.Token.IsCancellationRequested)
+ {
+ await Task.Delay(1000, cts.Token);
+
+ Console.WriteLine(device.IsConnected
+ ? $"[{DateTime.Now:HH:mm:ss}] Device is connected to ACU"
+ : $"[{DateTime.Now:HH:mm:ss}] Waiting for ACU connection...");
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when timeout occurs
+ }
+
+ logger.LogInformation("Shutting down device...");
+ await device.StopListening();
+ logger.LogInformation("Device stopped");
+ }
+}
\ No newline at end of file
diff --git a/src/samples/SimplePDDevice/README.md b/src/samples/SimplePDDevice/README.md
new file mode 100644
index 00000000..2bc13042
--- /dev/null
+++ b/src/samples/SimplePDDevice/README.md
@@ -0,0 +1,61 @@
+# SimplePDDevice
+
+A minimal OSDP Peripheral Device (PD) implementation that demonstrates the essential handlers for OSDP communication.
+
+## Features
+
+This simplified device implements only the core handlers that are guaranteed to work:
+
+- **HandleIdReport()** - Returns basic device identification
+- **HandleDeviceCapabilities()** - Returns supported device capabilities
+- **HandleCommunicationSet()** - Acknowledges communication settings changes
+- **HandleKeySettings()** - Acknowledges security key configuration
+
+All other commands are handled by the base Device class with default behavior.
+
+## Configuration
+
+The device can be configured via `appsettings.json`:
+
+```json
+{
+ "OSDP": {
+ "TcpPort": 4900,
+ "DeviceAddress": 1,
+ "RequireSecurity": false,
+ "SecurityKey": "0011223344556677889900AABBCCDDEEFF"
+ }
+}
+```
+
+## Running
+
+```bash
+dotnet run
+```
+
+The device will:
+- Listen on TCP port 4900 (configurable)
+- Use device address 1 (configurable)
+- Run without security by default (configurable)
+- Display connection status every second
+- Automatically stop after 30 seconds (for demo purposes)
+
+## Usage
+
+This device is designed to be connected to by an OSDP Access Control Unit (ACU). Once an ACU connects, it can:
+
+- Request device identification
+- Query device capabilities
+- Send communication configuration commands
+- Send security key configuration commands
+
+## Building
+
+```bash
+dotnet build
+```
+
+## Purpose
+
+This implementation serves as a starting point for OSDP device development, focusing on simplicity and the essential functionality that works reliably. Additional features can be added incrementally as needed.
\ No newline at end of file
diff --git a/src/samples/SimplePDDevice/SimplePDDevice.cs b/src/samples/SimplePDDevice/SimplePDDevice.cs
new file mode 100644
index 00000000..8f712567
--- /dev/null
+++ b/src/samples/SimplePDDevice/SimplePDDevice.cs
@@ -0,0 +1,70 @@
+using OSDP.Net;
+using OSDP.Net.Model;
+using OSDP.Net.Model.CommandData;
+using OSDP.Net.Model.ReplyData;
+using CommunicationConfiguration = OSDP.Net.Model.CommandData.CommunicationConfiguration;
+
+namespace SimplePDDevice;
+
+///
+/// Simplified OSDP Peripheral Device implementation with only essential handlers
+///
+public class SimplePDDevice(DeviceConfiguration config) : Device(config)
+{
+ ///
+ /// Handle ID Report command - returns basic device identification
+ ///
+ protected override PayloadData HandleIdReport()
+ {
+ // Return simple device identification
+ return new DeviceIdentification(
+ vendorCode: [0x01, 0x02, 0x03], // Vendor code (3 bytes)
+ modelNumber: 1, // Model number
+ version: 1, // Hardware version
+ serialNumber: 12345, // Serial number
+ firmwareMajor: 1, // Firmware major version
+ firmwareMinor: 0, // Firmware minor version
+ firmwareBuild: 0 // Firmware build version
+ );
+ }
+
+ ///
+ /// Handle Device Capabilities command - returns supported capabilities
+ ///
+ protected override PayloadData HandleDeviceCapabilities()
+ {
+ var deviceCapabilities = new DeviceCapabilities([
+ new DeviceCapability(CapabilityFunction.CardDataFormat, 1, 0),
+ new DeviceCapability(CapabilityFunction.ReaderLEDControl, 1, 0),
+ new DeviceCapability(CapabilityFunction.ReaderTextOutput, 0, 0),
+ new DeviceCapability(CapabilityFunction.CheckCharacterSupport, 1, 0),
+ new DeviceCapability(CapabilityFunction.CommunicationSecurity, 1, 1),
+ new DeviceCapability(CapabilityFunction.ReceiveBufferSize, 0, 1),
+ new DeviceCapability(CapabilityFunction.OSDPVersion, 2, 0)
+ ]);
+
+ return deviceCapabilities;
+ }
+
+ ///
+ /// Handle Communication Set command - acknowledges communication settings
+ ///
+ protected override PayloadData HandleCommunicationSet(CommunicationConfiguration commandPayload)
+ {
+ // Simply acknowledge the new communication settings
+ return new OSDP.Net.Model.ReplyData.CommunicationConfiguration(
+ commandPayload.Address,
+ commandPayload.BaudRate
+ );
+ }
+
+ ///
+ /// Handle Key Settings command - acknowledges security key configuration
+ ///
+ protected override PayloadData HandleKeySettings(EncryptionKeyConfiguration commandPayload)
+ {
+ // Simply acknowledge the key settings
+ return new Ack();
+ }
+
+}
\ No newline at end of file
diff --git a/src/samples/SimplePDDevice/SimplePDDevice.csproj b/src/samples/SimplePDDevice/SimplePDDevice.csproj
new file mode 100644
index 00000000..7544bd86
--- /dev/null
+++ b/src/samples/SimplePDDevice/SimplePDDevice.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/samples/SimplePDDevice/TESTING.md b/src/samples/SimplePDDevice/TESTING.md
new file mode 100644
index 00000000..f60a98c2
--- /dev/null
+++ b/src/samples/SimplePDDevice/TESTING.md
@@ -0,0 +1,91 @@
+# Testing SimplePDDevice
+
+## Manual Testing
+
+### 1. Start the Device
+
+```bash
+cd /path/to/SimplePDDevice
+dotnet run
+```
+
+Expected output:
+```
+Simple OSDP Peripheral Device
+============================
+info: SimplePDDevice.Program[0]
+ Starting OSDP Peripheral Device on TCP port 4900
+info: SimplePDDevice.Program[0]
+ Device Address: 1
+info: SimplePDDevice.Program[0]
+ Security Required: False
+info: SimplePDDevice.Program[0]
+ Device is now listening for ACU connections...
+info: SimplePDDevice.Program[0]
+ Press 'q' to quit
+[19:22:13] Waiting for ACU connection...
+```
+
+### 2. Test TCP Connection
+
+In another terminal, test that the TCP port is listening:
+
+```bash
+# Test if port is open (should succeed)
+nc -z localhost 4900 && echo "Port is open" || echo "Port is closed"
+
+# Or using telnet
+telnet localhost 4900
+```
+
+### 3. Connect with OSDP ACU
+
+To fully test the device, you need an OSDP Access Control Unit (ACU). The device implements these handlers:
+
+- **ID Report (0x61)** - Returns device identification
+- **Device Capabilities (0x62)** - Returns supported capabilities
+- **Communication Set (0x6E)** - Acknowledges communication changes
+- **Key Set (0x75)** - Acknowledges security key settings
+
+### 4. Expected Behavior
+
+When an ACU connects:
+- Device status will change to "Device is connected to ACU"
+- The device will respond to the four implemented commands
+- All other commands will be handled by the base Device class
+
+### 5. Configuration Testing
+
+Test different configurations by modifying `appsettings.json`:
+
+```json
+{
+ "OSDP": {
+ "TcpPort": 5000,
+ "DeviceAddress": 2,
+ "RequireSecurity": true,
+ "SecurityKey": "1122334455667788AABBCCDDEEFF0011"
+ }
+}
+```
+
+### 6. Build Verification
+
+```bash
+dotnet build
+# Should succeed with no warnings or errors
+```
+
+### 7. Integration with Console App
+
+The Console application in this project can act as an ACU. To test full integration:
+
+1. Start SimplePDDevice: `dotnet run` (in SimplePDDevice directory)
+2. In another terminal, start Console app as ACU with TCP connection to localhost:4900
+3. The Console app should be able to discover and communicate with the SimplePDDevice
+
+## Troubleshooting
+
+- **Port already in use**: Change TcpPort in appsettings.json
+- **Connection refused**: Ensure device is started before ACU connection
+- **Build errors**: Ensure OSDP.Net project builds successfully first
\ No newline at end of file
diff --git a/src/samples/SimplePDDevice/appsettings.json b/src/samples/SimplePDDevice/appsettings.json
new file mode 100644
index 00000000..5783a238
--- /dev/null
+++ b/src/samples/SimplePDDevice/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "OSDP": {
+ "TcpPort": 4900,
+ "DeviceAddress": 1,
+ "RequireSecurity": false,
+ "SecurityKey": "0011223344556677889900AABBCCDDEEFF"
+ }
+}
\ No newline at end of file