Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ci/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
displayName: 'Install .NET Core SDK'
inputs:
packageType: 'sdk'
version: '8.x'
version: '10.x'

- task: DotNetCoreCLI@2
displayName: 'Unit Test Core'
Expand Down
5 changes: 5 additions & 0 deletions docs/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
- Provides detailed reports with resource values and comments for cleanup decisions
- Can be integrated into Azure DevOps pipelines using `ci/azure-pipeline-resource-check.yml`

## Localization
- **Do not manually translate resource strings** - Translations are handled by a separate automated service
- Only add new resource strings to the main `Resources.resx` file with English values
- The automated translation service will populate the language-specific `.resx` files (de, es, fr, ja, zh)

## Release Process
- Create a release: `pwsh ci/release.ps1`
- The script automates the release workflow:
Expand Down
5 changes: 2 additions & 3 deletions src/Core/Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<Version>1.0.2</Version>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>OSDPBench.Core</AssemblyName>
Expand All @@ -24,8 +24,7 @@

<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="OSDP.Net" Version="4.1.7" />
<PackageReference Include="System.Text.Json" Version="9.0.6" />
<PackageReference Include="OSDP.Net" Version="5.0.37" />
</ItemGroup>

<ItemGroup>
Expand Down
27 changes: 19 additions & 8 deletions src/Core/Models/PacketTraceEntry.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Text.RegularExpressions;
using OSDP.Net.Messages;
using OSDP.Net.Model;
using OSDP.Net.Tracing;

Expand Down Expand Up @@ -44,10 +44,10 @@ public string Type
{
if (Packet.CommandType != null)
{
return ToSpacedString(Packet.CommandType);
return Packet.CommandType.Value.GetDisplayName();
}

return Packet.ReplyType != null ? ToSpacedString(Packet.ReplyType) : "Unknown";
return Packet.ReplyType != null ? Packet.ReplyType.Value.GetDisplayName() : "Unknown";
}
}

Expand All @@ -63,12 +63,23 @@ public string Type
/// This property parses and formats the payload data of the packet,
/// or returns "Empty" if no data is available.
/// </summary>
public string Details => Packet.ParsePayloadData()?.ToString() ?? "Empty";

private static string ToSpacedString(Enum enumValue)
public string Details
{
// Use Regex to insert spaces before any capital letter followed by a lowercase letter, ignoring the first capital.
return Regex.Replace(enumValue.ToString(), "(?<!^)([A-Z](?=[a-z]))", " $1");
get
{
var payload = Packet.ParsePayloadData();
if (payload == null) return "Empty";

// Format byte arrays as hex strings instead of "System.Byte[]"
if (payload is byte[] bytes)
{
return bytes.Length > 0
? BitConverter.ToString(bytes).Replace("-", " ")
: "Empty";
}

return payload.ToString() ?? "Empty";
}
}

// Private constructor
Expand Down
54 changes: 49 additions & 5 deletions src/Core/Models/PacketTraceEntryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,45 @@ namespace OSDPBench.Core.Models;
/// </summary>
public class PacketTraceEntryBuilder
{
private MessageSpy _messageSpy;
private TraceEntry _traceEntry;
private PacketTraceEntry? _lastTraceEntry;
private DateTime _timestamp;

/// <summary>
/// Initializes a new instance of the <see cref="PacketTraceEntryBuilder"/> class
/// with a default MessageSpy (no secure channel decryption).
/// </summary>
public PacketTraceEntryBuilder() : this(new MessageSpy())
{
}

/// <summary>
/// Initializes a new instance of the <see cref="PacketTraceEntryBuilder"/> class.
/// </summary>
/// <param name="messageSpy">The MessageSpy instance to use for parsing packets.
/// Should be reused across packets to maintain secure channel state.</param>
public PacketTraceEntryBuilder(MessageSpy messageSpy)
{
_messageSpy = messageSpy;
}

/// <summary>
/// Configures the security key for secure channel decryption.
/// Creates a new MessageSpy with the specified key, resetting secure channel state.
/// </summary>
/// <param name="securityKey">The security key for decryption, or null for no encryption.</param>
/// <returns>The current builder instance for method chaining.</returns>
/// <remarks>
/// Call this method BEFORE any packets are processed to ensure the MessageSpy
/// can track the secure channel negotiation from the beginning.
/// </remarks>
public PacketTraceEntryBuilder WithSecurityKey(byte[]? securityKey)
{
_messageSpy = securityKey != null ? new MessageSpy(securityKey) : new MessageSpy();
return this;
}

/// <summary>
/// Initializes the <see cref="PacketTraceEntryBuilder"/> instance with the specified trace entry and previous trace entry
/// while also recording the current timestamp.
Expand All @@ -23,18 +58,27 @@ public PacketTraceEntryBuilder FromTraceEntry(TraceEntry traceEntry, PacketTrace
_traceEntry = traceEntry;
_lastTraceEntry = lastTraceEntry;
_timestamp = DateTime.UtcNow;

return this;
}

/// <summary>
/// Creates and returns a new instance of the <see cref="PacketTraceEntry"/> class.
/// </summary>
/// <returns>A new instance of the <see cref="PacketTraceEntry"/> class, fully constructed based on the specified trace entry details.</returns>
public PacketTraceEntry Build()
/// <returns>A new instance of the <see cref="PacketTraceEntry"/> class, or null if parsing failed.</returns>
/// <remarks>
/// Uses <see cref="MessageSpy"/> to parse the packet data, which supports secure channel decryption
/// when a security key was provided to the MessageSpy constructor.
/// </remarks>
public PacketTraceEntry? Build()
{
if (!_messageSpy.TryParsePacket(_traceEntry.Data, out var packet) || packet == null)
{
return null;
}

return PacketTraceEntry.Create(_traceEntry.Direction, _timestamp,
_lastTraceEntry != null ? _timestamp - _lastTraceEntry.Timestamp : TimeSpan.Zero,
PacketDecoding.ParseMessage(_traceEntry.Data));
packet);
}
}
}
12 changes: 12 additions & 0 deletions src/Core/Resources/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,18 @@
<value>An update will be out soon that supports secure channel</value>
<comment>Message about future secure channel support</comment>
</data>
<data name="Monitor_ClearText" xml:space="preserve">
<value>Clear Text</value>
<comment>Badge shown when communication is not encrypted</comment>
</data>
<data name="Monitor_EncryptedDefaultKey" xml:space="preserve">
<value>Encrypted - Default Key</value>
<comment>Badge shown when using secure channel with the default security key</comment>
</data>
<data name="Monitor_Encrypted" xml:space="preserve">
<value>Encrypted</value>
<comment>Badge shown when using secure channel with a custom security key</comment>
</data>
<data name="Monitor_TimeStamp" xml:space="preserve">
<value>TimeStamp</value>
<comment>Column header for timestamp in monitoring grid</comment>
Expand Down
49 changes: 46 additions & 3 deletions src/Core/Services/DeviceManagementService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ public DeviceManagementService()
/// <inheritdoc />
public bool UsesDefaultSecurityKey { get; private set; }

/// <inheritdoc />
public byte[]? SecurityKey => _securityKey;

/// <inheritdoc />
public bool IsConnected { get; private set; }

Expand Down Expand Up @@ -243,9 +246,9 @@ private async Task WaitUntilDeviceIsOffline()
{
return;
}

using var cts = new CancellationTokenSource(_defaultShutdownTimeout);

// Check if the connection exists before querying its status
try
{
Expand All @@ -264,10 +267,47 @@ private async Task WaitUntilDeviceIsOffline()
// Connection was already removed from the panel, which is fine during shutdown
return;
}

await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
}

private async Task WaitUntilDeviceIsOnline()
{
// Skip waiting if we don't have a valid connection
if (_connectionId == Guid.Empty)
{
return;
}

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

try
{
while (!cts.Token.IsCancellationRequested)
{
try
{
if (_panel.IsOnline(_connectionId, Address))
{
return;
}
}
catch (KeyNotFoundException)
{
// Connection doesn't exist yet, keep waiting
}

await Task.Delay(TimeSpan.FromMilliseconds(100), cts.Token);
}
}
catch (OperationCanceledException)
{
// Timeout occurred
}

throw new TimeoutException("The device did not come online within the specified timeout.");
}

/// <inheritdoc />
public event EventHandler<ConnectionStatus>? ConnectionStatusChange;

Expand Down Expand Up @@ -304,6 +344,9 @@ public async Task ReconnectAfterCommunicationChange(IOsdpConnection osdpConnecti
_connectionId = _panel.StartConnection(osdpConnection, _defaultPollInterval, Tracer);
_panel.AddDevice(_connectionId, Address, true, IsUsingSecureChannel,
UsesDefaultSecurityKey ? null : _securityKey);

// Wait for the connection to fully establish before returning
await WaitUntilDeviceIsOnline();
}

/// <summary>
Expand Down
9 changes: 9 additions & 0 deletions src/Core/Services/IDeviceManagementService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ public interface IDeviceManagementService
/// </value>
bool UsesDefaultSecurityKey { get; }

/// <summary>
/// Gets the security key used for secure channel communication.
/// </summary>
/// <remarks>
/// Returns the custom security key if one was provided, or null if using the default key or not using secure channel.
/// Used by the tracing system to decrypt secure channel traffic.
/// </remarks>
byte[]? SecurityKey { get; }

/// <summary>
/// Gets the connection status of the device.
/// </summary>
Expand Down
46 changes: 39 additions & 7 deletions src/Core/ViewModels/Pages/ConnectViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ public partial class ConnectViewModel : ObservableObject, IDisposable
private readonly IUsbDeviceMonitorService? _usbDeviceMonitorService;

private ISerialPortConnectionService _serialPortConnectionService;
private readonly PacketTraceEntryBuilder _traceEntryBuilder = new();
private PacketTraceEntry? _lastPacketEntry;
private byte[]? _lastConfiguredSecurityKey;
private bool _securityKeyConfigured;
private bool _isDisposed;
private Timer? _usbStatusTimer;
private readonly TaskCompletionSource<bool> _initializationComplete = new();
Expand Down Expand Up @@ -86,28 +89,53 @@ private void UpdateConnectionTypes()

private void OnDeviceManagementServiceOnTraceEntryReceived(object? sender, TraceEntry traceEntry)
{
UsingSecureChannel = _deviceManagementService.IsUsingSecureChannel;
UsesDefaultSecurityKey = _deviceManagementService.UsesDefaultSecurityKey;

// Configure security key on first trace entry or when key changes
// This must happen before processing any packets so MessageSpy can track secure channel state
EnsureSecurityKeyConfigured();

// Update activity indicators based on a raw trace entry direction (works for encrypted packets too)
UpdateActivityIndicators(traceEntry.Direction);

PacketTraceEntry? packetTraceEntry = BuildPacketTraceEntry(traceEntry);
if (packetTraceEntry == null) return;

_lastPacketEntry = packetTraceEntry;
}

private void EnsureSecurityKeyConfigured()
{
var currentKey = _deviceManagementService.SecurityKey;
if (!_securityKeyConfigured || !SecurityKeysEqual(_lastConfiguredSecurityKey, currentKey))
{
_traceEntryBuilder.WithSecurityKey(currentKey);
_lastConfiguredSecurityKey = currentKey;
_securityKeyConfigured = true;
}
}

private static bool SecurityKeysEqual(byte[]? key1, byte[]? key2)
{
if (key1 == null && key2 == null) return true;
if (key1 == null || key2 == null) return false;
if (key1.Length != key2.Length) return false;
return key1.AsSpan().SequenceEqual(key2);
}

private PacketTraceEntry? BuildPacketTraceEntry(TraceEntry traceEntry)
{
try
{
var builder = new PacketTraceEntryBuilder();
return builder.FromTraceEntry(traceEntry, _lastPacketEntry).Build();
return _traceEntryBuilder.FromTraceEntry(traceEntry, _lastPacketEntry).Build();
}
catch (Exception)
{
return null;
}
}

private void UpdateActivityIndicators(TraceDirection direction)
{
switch (direction)
Expand Down Expand Up @@ -187,9 +215,13 @@ private void DeviceManagementServiceOnNakReplyReceived(object? sender, string na
[ObservableProperty] private string _securityKey = string.Empty;

[ObservableProperty] private DateTime _lastTxActiveTime;

[ObservableProperty] private DateTime _lastRxActiveTime;


[ObservableProperty] private bool _usingSecureChannel;

[ObservableProperty] private bool _usesDefaultSecurityKey;

[ObservableProperty] private string _usbStatusText = string.Empty;

/// <summary>
Expand Down
Loading