# OPC UA Client Requirements ## Overview Three new .NET 10 cross-platform projects providing a shared OPC UA client library, a CLI tool, and an Avalonia desktop UI. All projects target Windows and macOS. ## Projects | Project | Type | Purpose | |---------|------|---------| | `ZB.MOM.WW.LmxOpcUa.Client.Shared` | Class library | Core OPC UA client, models, interfaces | | `ZB.MOM.WW.LmxOpcUa.Client.CLI` | Console app | Command-line interface using CliFx | | `ZB.MOM.WW.LmxOpcUa.Client.UI` | Avalonia app | Desktop UI with tree browser, subscriptions, alarms | | `ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests` | Test project | Unit tests for shared library | | `ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests` | Test project | Unit tests for CLI commands | | `ZB.MOM.WW.LmxOpcUa.Client.UI.Tests` | Test project | Unit tests for UI view models | ## Technology Stack - .NET 10, C# - OPC UA: OPCFoundation.NetStandard.Opc.Ua.Client - Logging: Serilog - CLI: CliFx - UI: Avalonia 11.x with CommunityToolkit.Mvvm - Tests: xUnit 3, Shouldly, Microsoft.Testing.Platform runner ## Client.Shared ### ConnectionSettings Model ``` EndpointUrl: string (required) FailoverUrls: string[] (optional) Username: string? (optional, first-class property) Password: string? (optional, first-class property) SecurityMode: enum (None, Sign, SignAndEncrypt) — default None SessionTimeoutSeconds: int — default 60 AutoAcceptCertificates: bool — default true CertificateStorePath: string? — default platform-appropriate location ``` ### IOpcUaClientService Interface Single service interface covering all OPC UA operations: **Lifecycle:** - `ConnectAsync(ConnectionSettings)` — connect to server, handle endpoint discovery, security, auth - `DisconnectAsync()` — close session cleanly - `IsConnected` property **Read/Write:** - `ReadValueAsync(NodeId)` — returns DataValue (value, status, timestamps) - `WriteValueAsync(NodeId, object value)` — auto-detects target type, returns StatusCode **Browse:** - `BrowseAsync(NodeId? parent)` — returns list of BrowseResult (NodeId, DisplayName, NodeClass) - Lazy-load compatible (browse one level at a time) **Subscribe:** - `SubscribeAsync(NodeId, int intervalMs)` — create monitored item subscription - `UnsubscribeAsync(NodeId)` — remove monitored item - `event DataChanged` — fires on value change with (NodeId, DataValue) **Alarms:** - `SubscribeAlarmsAsync(NodeId? source, int intervalMs)` — subscribe to alarm events - `UnsubscribeAlarmsAsync()` — remove alarm subscription - `RequestConditionRefreshAsync()` — trigger condition refresh - `event AlarmEvent` — fires on alarm state change with AlarmEventArgs **History:** - `HistoryReadRawAsync(NodeId, DateTime start, DateTime end, int maxValues)` — raw historical values - `HistoryReadAggregateAsync(NodeId, DateTime start, DateTime end, AggregateType, double intervalMs)` — aggregated values **Redundancy:** - `GetRedundancyInfoAsync()` — returns RedundancyInfo (mode, service level, server URIs, app URI) **Failover:** - Automatic failover across FailoverUrls with keep-alive monitoring - `event ConnectionStateChanged` — fires on connect/disconnect/failover ### Models - `BrowseResult`: NodeId, DisplayName, NodeClass, HasChildren - `AlarmEventArgs`: SourceName, ConditionName, Severity, Message, Retain, ActiveState, AckedState, Time - `RedundancyInfo`: Mode, ServiceLevel, ServerUris, ApplicationUri - `ConnectionState`: enum (Disconnected, Connecting, Connected, Reconnecting) - `AggregateType`: enum (Average, Minimum, Maximum, Count, Start, End) ### Type Conversion Port the existing `ConvertValue` logic from the CLI tool: reads the current node value to determine the target type, then coerces the input value. ### Certificate Management - Cross-platform certificate store path (default: `{AppData}/LmxOpcUaClient/pki/`) - Auto-generate client certificate on first use - Auto-accept untrusted server certificates (configurable) ### Logging Serilog with `ILogger` passed via constructor or `Log.ForContext()`. No sinks configured in the library — consumers configure sinks. ## Client.CLI ### Commands Port all 8 commands from the existing `tools/opcuacli-dotnet/`: | Command | Description | |---------|-------------| | `connect` | Test server connectivity | | `read` | Read a node value | | `write` | Write a value to a node | | `browse` | Browse address space (with depth/recursive) | | `subscribe` | Monitor node for value changes | | `historyread` | Read historical data (raw + aggregates) | | `alarms` | Subscribe to alarm events | | `redundancy` | Query redundancy state | All commands use the shared `IOpcUaClientService`. Each command: 1. Creates `ConnectionSettings` from CLI options 2. Creates `OpcUaClientService` 3. Calls the appropriate method 4. Formats and prints results ### Common Options (all commands) - `-u, --url` (required): Endpoint URL - `-U, --username`: Username - `-P, --password`: Password - `-S, --security`: Security mode (none/sign/encrypt) - `-F, --failover-urls`: Comma-separated failover endpoints ### Logging Serilog console sink at Warning level by default, with `--verbose` flag for Debug. ## Client.UI ### Window Layout Single-window Avalonia application: ``` ┌─────────────────────────────────────────────────────────┐ │ [Endpoint URL] [User] [Pass] [Security▼] [Connect] │ │ Redundancy: Mode=Warm ServiceLevel=200 AppUri=... │ ├──────────────┬──────────────────────────────────────────┤ │ │ ┌─Read/Write─┬─Subscriptions─┬─Alarms─┬─History─┐│ │ Address │ │ Node: ns=3;s=Tag.Attr ││ │ Space │ │ Value: 42.5 ││ │ Tree │ │ Status: Good ││ │ Browser │ │ [Write: ____] [Send] ││ │ │ │ ││ │ (lazy-load) │ │ ││ │ │ └──────────────────────────────────────┘│ ├──────────────┴──────────────────────────────────────────┤ │ Status: Connected | Session: abc123 | 3 subscriptions │ └─────────────────────────────────────────────────────────┘ ``` ### Views and ViewModels (CommunityToolkit.Mvvm) **MainWindowViewModel:** - Connection settings properties (bound to top bar inputs) - ConnectCommand / DisconnectCommand (RelayCommand) - ConnectionState property - RedundancyInfo property - SelectedTreeNode property - StatusMessage property **BrowseTreeViewModel:** - Root nodes collection (ObservableCollection) - Lazy-load children on expand via `BrowseAsync` - TreeNodeViewModel: NodeId, DisplayName, NodeClass, Children, IsExpanded, HasChildren **ReadWriteViewModel:** - SelectedNode (from tree selection) - CurrentValue, Status, SourceTimestamp - WriteValue input + WriteCommand - Auto-read on node selection **SubscriptionsViewModel:** - ActiveSubscriptions collection (ObservableCollection) - AddSubscription / RemoveSubscription commands - Live value updates dispatched to UI thread - Columns: NodeId, Value, Status, Timestamp **AlarmsViewModel:** - AlarmEvents collection (ObservableCollection) - SubscribeCommand / UnsubscribeCommand / RefreshCommand - MonitoredNode property - Live alarm events dispatched to UI thread **HistoryViewModel:** - SelectedNode (from tree selection) - StartTime, EndTime, MaxValues, AggregateType, Interval - ReadCommand - Results collection (ObservableCollection) - Columns: Timestamp, Value, Status ### UI Thread Dispatch All events from `IOpcUaClientService` must be dispatched to the Avalonia UI thread via `Dispatcher.UIThread.Post()` before updating ObservableCollections. ## Test Projects ### Client.Shared.Tests - ConnectionSettings validation - Type conversion (ConvertValue) - BrowseResult model construction - AlarmEventArgs model construction - FailoverUrl parsing ### Client.CLI.Tests - Command option parsing (via CliFx test infrastructure) - Output formatting ### Client.UI.Tests - ViewModel property change notifications - Command can-execute logic - Tree node lazy-load behavior (with mocked IOpcUaClientService) ### Test Framework - xUnit 3 with Microsoft.Testing.Platform runner - Shouldly for assertions - No live OPC UA server required — mock IOpcUaClientService for unit tests