Files
lmxopcua/docs/Client.UI.md
Joseph Doherty f9bc301c33 Client rename residuals: lmxopcua-cli → otopcua-cli + LmxOpcUaClient → OtOpcUaClient with migration shim. Closes task #208 (the executable-name + LocalAppData-folder slice that was called out in Client.CLI.md / Client.UI.md as a deliberately-deferred residual of the Phase 0 rename). Six source references flipped to the canonical OtOpcUaClient spelling: Program.cs CliFx executable name + description (lmxopcua-cli → otopcua-cli), DefaultApplicationConfigurationFactory.cs ApplicationName + ApplicationUri (LmxOpcUaClient + urn:localhost:LmxOpcUaClient → OtOpcUaClient + urn:localhost:OtOpcUaClient), OpcUaClientService.CreateSessionAsync session-name arg, ConnectionSettings.CertificateStorePath default, MainWindowViewModel.CertificateStorePath default, JsonSettingsService.SettingsDir. Two consuming tests (ConnectionSettingsTests + MainWindowViewModelTests) updated to assert the new canonical name. New ClientStoragePaths static helper at src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs is the migration shim — single entry point for the PKI root + pki subpath, runs a one-shot legacy-folder probe on first resolution: if {LocalAppData}/LmxOpcUaClient/ exists + {LocalAppData}/OtOpcUaClient/ does not, Directory.Move renames it in place (atomic on NTFS within the same volume) so trusted server certs + saved connection settings persist across the rename without operator action. Idempotent per-process via a Lock-guarded _migrationChecked flag so repeated CertificateStorePath getter calls on the hot path pay no IO cost beyond the first. Fresh-install path (neither folder exists) + already-migrated path (only canonical exists) + manual-override path (both exist — developer has set up something explicit) are all no-ops that leave state alone. IOException on the Directory.Move is swallowed + logged as a false return so a concurrent peer process losing the race doesn't crash the consumer; the losing process falls back to whatever state exists. Five new ClientStoragePathsTests assert: GetRoot ends with canonical name under LocalAppData, GetPkiPath nests pki under root, CanonicalFolderName is OtOpcUaClient, LegacyFolderName is LmxOpcUaClient (the migration contract — a typo here would leak the legacy folder past the shim), repeat invocation returns false after first-touch arms the in-process guard. Doc-side residual-explanation notes in docs/Client.CLI.md + docs/Client.UI.md are dropped now that the rename is real; replaced with a short "pre-#208 dev boxes migrate automatically on first launch" note that points at ClientStoragePaths. Sample CLI invocations in Client.CLI.md updated via sed from lmxopcua-cli to otopcua-cli across every command block (14 replacements). Pre-existing staleness in SubscribeCommandTests.Execute_PrintsSubscriptionMessage surfaced during the test run — the CLI's subscribe command has long since switched to an aggregate "Subscribed to {count}/{total} nodes (interval: ...)" output format but the test still asserted the original single-node form. Updated the assertion to match current output + added a comment explaining the change; this is unrelated to the rename but was blocking a green Client.CLI.Tests run. Full solution build 0 errors; Client.Shared.Tests 136/136 + 5 new shim tests passing; Client.UI.Tests 98/98; Client.CLI.Tests 52/52 (was 51/52 before the subscribe-test fix). No Admin/Core/Server changes — this touches only the client layer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 01:50:40 -04:00

11 KiB
Raw Blame History

Client UI

Overview

ZB.MOM.WW.OtOpcUa.Client.UI is a cross-platform Avalonia desktop application for connecting to and interacting with the OtOpcUa OPC UA server. It targets .NET 10 and uses the shared IOpcUaClientService from Client.Shared for all OPC UA operations.

The UI provides a single-window interface for browsing the address space, reading and writing values, monitoring live subscriptions, managing alarms, and querying historical data.

Build and Run

cd src/ZB.MOM.WW.OtOpcUa.Client.UI
dotnet build
dotnet run

Technology Stack

Component Technology
Framework .NET 10
UI Toolkit Avalonia 11.2
MVVM CommunityToolkit.Mvvm
OPC UA OPCFoundation.NetStandard.Opc.Ua.Client
Logging Serilog
Theme Avalonia Fluent

Window Layout

The application uses a single-window layout with five main areas:

┌─────────────────────────────────────────────────────────────┐
│ [Endpoint URL                    ] [Connect] [Disconnect]   │
│ ▸ Connection Settings                                       │
│ Redundancy: Warm  Service Level: 200  URI: urn:...          │
├──────────────┬──────────────────────────────────────────────┤
│              │ ┌─Read/Write─┬─Subscriptions─┬─Alarms─┬─History─┐│
│  Address     │ │                                              ││
│  Space       │ │                                              ││
│  Tree        │ │         (active tab content)                 ││
│  Browser     │ │                                              ││
│              │ │                                              ││
│  (lazy-load) │ └──────────────────────────────────────────────┘│
├──────────────┴──────────────────────────────────────────────┤
│ Connected to opc.tcp://... | OtOpcUa Server | Session: ... | 3 subs│
└─────────────────────────────────────────────────────────────┘

Connection Panel

Connection Panel

The top bar provides the endpoint URL, Connect, and Disconnect buttons. The Connection Settings expander reveals additional options when expanded:

Setting Description
Endpoint URL OPC UA server endpoint (e.g., opc.tcp://localhost:4840/OtOpcUa)
Username / Password Credentials for UserName token authentication
Security Mode Transport security: None, Sign, SignAndEncrypt
Failover URLs Comma-separated backup endpoints for redundancy failover
Session Timeout Session timeout in seconds (13600, default 60)
Certificate Store Path Path to the client certificate store (folder chooser)
Auto-accept certificates Whether to accept untrusted server certificates

Settings Persistence

Connection settings are saved to {LocalAppData}/OtOpcUaClient/settings.json after each successful connection and on window close. Dev boxes upgrading from a pre-task-#208 build still have the legacy LmxOpcUaClient/ folder on disk; ClientStoragePaths in Client.Shared moves it to the canonical path on first launch so existing trusted certs + saved settings persist without operator action. The settings are reloaded on next launch, including:

  • All connection parameters
  • Active subscription node IDs (restored after reconnection)
  • Alarm subscription source node (restored with condition refresh)

Redundancy

When connected, the redundancy row displays the server's redundancy mode, service level, and application URI. The shared service handles automatic failover to backup endpoints if configured.

Browse Tree

The left panel shows the OPC UA address space as a lazy-loaded tree. Nodes are loaded on demand when expanded.

Context Menu

Right-click on tree nodes to access:

Action Description
Subscribe Subscribe to data changes on the selected node(s). For Object nodes, recursively subscribes all Variable descendants (up to 10 levels deep). Switches to the Subscriptions tab.
View History Set the selected Variable node as the target in the History tab and switch to it. Only enabled for Variable nodes.
Monitor Alarms Stop any active alarm subscription and subscribe to alarm events on the selected node. Switches to the Alarms tab with automatic condition refresh.

Multi-select is supported (Ctrl+Click, Shift+Click) for the Subscribe action.

Read/Write Tab

Select a node in the browse tree to auto-read its current value. The tab displays:

  • Node ID
  • Current value (arrays displayed as [0,1,2,3])
  • Status code (e.g., 0x00000000 (Good))
  • Source and server timestamps

To write a value, enter the new value and click Send. The service reads the current value first to determine the target type, then converts and writes.

Subscriptions Tab

Subscriptions Tab

Monitor live data changes from subscribed nodes. The tab shows a data grid with:

Column Description
Node ID The monitored node identifier
Value Current value (arrays formatted as [v1,v2,...])
Status OPC UA status code with description (e.g., 0x00000000 (Good))
Timestamp Source timestamp in ISO 8601 format

Adding Subscriptions

  • Type a node ID and click Add, or
  • Right-click nodes in the browse tree and select Subscribe

Removing Subscriptions

Select one or more rows (Ctrl+Click for multi-select) and click Remove.

Writing Values

Double-click a subscription row to open a write dialog. The dialog:

  1. Pre-fills the current value
  2. Validates the input can parse to the target type before writing
  3. Shows the write result status
  4. Closes automatically on success, shows error in red on failure

Tab Header

The tab header shows the active subscription count: Subscriptions (26).

Persistence

Active subscription node IDs are saved when the application closes or disconnects, and restored on the next connection.

Alarms Tab

Alarms Tab

Monitor alarm/condition events from the server.

Subscribing

Enter an optional source node ID and click Subscribe. A condition refresh is automatically requested to display current retained alarms. Alternatively, right-click a node in the browse tree and select Monitor Alarms.

Alarm Display

The data grid shows retained alarm conditions with color-coded rows:

Severity Range Color
Inactive Light grey
Low (0332) Light blue
Medium (333665) Light yellow
High (666899) Light red
Critical (9001000) Red

Alarms are updated in place when the server re-sends condition state changes. Non-retained alarms are automatically removed.

Acknowledging Alarms

Right-click an active, unacknowledged alarm and select Acknowledge.... Enter an acknowledgment comment in the popup dialog. The alarm is acknowledged via the OPC UA Acknowledge method on the condition node.

Tab Header

The tab header shows active unacknowledged alarm count: Alarms (2).

Persistence

The alarm subscription source node is saved and restored on reconnection with automatic condition refresh.

History Tab

History Tab

Read historical data from the Wonderware Historian.

Time Range

Date/Time Range Picker

The date/time range picker provides:

  • Text input — Type start and end times in yyyy-MM-dd HH:mm:ss format (UTC)
  • Preset buttons — Quick selection: 5m (last 5 minutes), 1h (last hour), 1d (last day), 1w (last week)

All times are in UTC. Invalid input turns red on blur.

Query Options

Option Description
Aggregate Raw (default), Average, Minimum, Maximum, Count, Start, End
Interval (ms) Processing interval for aggregate queries (shown only for aggregates)
Max Values Maximum number of raw values to return (default 1000)

Results

The results grid displays:

Column Description
Value The historical value (arrays formatted)
Status OPC UA status code with description
Source Timestamp When the value was recorded
Server Timestamp When the server processed the value

Status Bar

The bottom status bar shows:

  • Connection state and endpoint URL
  • Server name and session identifier
  • Active subscription count

Architecture

Deferred Initialization

The OPC UA SDK is not loaded until the user clicks Connect. This keeps application startup instant. The IOpcUaClientService and all child ViewModels are created on first connection.

UI Thread Dispatch

All service event handlers (data changes, alarm events, connection state changes) are dispatched through an IUiDispatcher abstraction before updating ObservableCollections. In production this wraps Dispatcher.UIThread.Post(); in tests it runs synchronously.

ViewModels

ViewModel Responsibility
MainWindowViewModel Connection lifecycle, tab coordination, settings persistence
BrowseTreeViewModel Root node loading, tree clearing
TreeNodeViewModel Lazy-load children on expand via BrowseAsync
ReadWriteViewModel Auto-read on selection, write with type coercion
SubscriptionsViewModel Add/remove subscriptions, DataChanged event handling
AlarmsViewModel Alarm subscribe/unsubscribe, event filtering, acknowledge
HistoryViewModel Raw and aggregate history reads

Custom Controls

Control Description
DateTimeRangePicker UTC start/end text inputs with preset duration buttons

Testing

The UI has 102 unit tests covering ViewModel logic and headless rendering:

dotnet test tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests

Tests use:

  • FakeOpcUaClientService — configurable fake implementing IOpcUaClientService
  • SynchronousUiDispatcher — runs dispatch actions inline for deterministic testing
  • FakeSettingsService — tracks save/load calls for settings persistence tests
  • Avalonia headless rendering — screenshot capture for visual verification