Move ClientRequirements.md to docs/reqs/ and update CliTool.md for Client.CLI

CliTool.md now documents the new Client.CLI project (shared service
abstraction, all 8 commands, verbose logging) instead of the standalone
tools/opcuacli-dotnet tool.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-31 08:04:14 -04:00
parent 41a6b66943
commit 29e917b05f
3 changed files with 133 additions and 154 deletions

View File

@@ -1,15 +1,15 @@
<Solution> <Solution>
<Folder Name="/src/"> <Folder Name="/src/">
<Project Path="src/ZB.MOM.WW.LmxOpcUa.Host/ZB.MOM.WW.LmxOpcUa.Host.csproj" /> <Project Path="src/ZB.MOM.WW.LmxOpcUa.Host/ZB.MOM.WW.LmxOpcUa.Host.csproj"/>
<Project Path="src/ZB.MOM.WW.LmxOpcUa.Client.Shared/ZB.MOM.WW.LmxOpcUa.Client.Shared.csproj" /> <Project Path="src/ZB.MOM.WW.LmxOpcUa.Client.Shared/ZB.MOM.WW.LmxOpcUa.Client.Shared.csproj"/>
<Project Path="src/ZB.MOM.WW.LmxOpcUa.Client.CLI/ZB.MOM.WW.LmxOpcUa.Client.CLI.csproj" /> <Project Path="src/ZB.MOM.WW.LmxOpcUa.Client.CLI/ZB.MOM.WW.LmxOpcUa.Client.CLI.csproj"/>
<Project Path="src/ZB.MOM.WW.LmxOpcUa.Client.UI/ZB.MOM.WW.LmxOpcUa.Client.UI.csproj" /> <Project Path="src/ZB.MOM.WW.LmxOpcUa.Client.UI/ZB.MOM.WW.LmxOpcUa.Client.UI.csproj"/>
</Folder> </Folder>
<Folder Name="/tests/"> <Folder Name="/tests/">
<Project Path="tests/ZB.MOM.WW.LmxOpcUa.Tests/ZB.MOM.WW.LmxOpcUa.Tests.csproj" /> <Project Path="tests/ZB.MOM.WW.LmxOpcUa.Tests/ZB.MOM.WW.LmxOpcUa.Tests.csproj"/>
<Project Path="tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/ZB.MOM.WW.LmxOpcUa.IntegrationTests.csproj" /> <Project Path="tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests/ZB.MOM.WW.LmxOpcUa.IntegrationTests.csproj"/>
<Project Path="tests/ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests/ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests.csproj" /> <Project Path="tests/ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests/ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests.csproj"/>
<Project Path="tests/ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests/ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests.csproj" /> <Project Path="tests/ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests/ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests.csproj"/>
<Project Path="tests/ZB.MOM.WW.LmxOpcUa.Client.UI.Tests/ZB.MOM.WW.LmxOpcUa.Client.UI.Tests.csproj" /> <Project Path="tests/ZB.MOM.WW.LmxOpcUa.Client.UI.Tests/ZB.MOM.WW.LmxOpcUa.Client.UI.Tests.csproj"/>
</Folder> </Folder>
</Solution> </Solution>

View File

@@ -1,175 +1,177 @@
# CLI Tool # Client CLI
## Overview ## Overview
The OPC UA CLI tool at `tools/opcuacli-dotnet/` is a command-line utility for testing OPC UA server functions. It targets .NET 10 and uses the OPC Foundation UA .NET Standard client library for OPC UA operations and [CliFx](https://github.com/Tyrrrz/CliFx) for command routing and argument parsing. `ZB.MOM.WW.LmxOpcUa.Client.CLI` is a cross-platform command-line client for the LmxOpcUa OPC UA server. It targets .NET 10 and uses the shared `IOpcUaClientService` from `Client.Shared` for all OPC UA operations. Commands are routed and parsed by [CliFx](https://github.com/Tyrrrz/CliFx).
The tool is not part of the production service. It exists to verify that the LmxOpcUa server correctly exposes browse nodes, reads, writes, subscriptions, historical data, and alarm events without requiring a full OPC UA client application. The CLI is the primary tool for operators and developers to test and interact with the server from a terminal. It supports all core operations: connectivity testing, browsing, reading, writing, subscriptions, alarm monitoring, history reads, and redundancy queries.
## Build and Run ## Build and Run
```bash ```bash
cd tools/opcuacli-dotnet cd src/ZB.MOM.WW.LmxOpcUa.Client.CLI
dotnet build dotnet build
dotnet run -- <command> [options] dotnet run -- <command> [options]
``` ```
## Shared Session Creation The executable name is `lmxopcua-cli`.
`OpcUaHelper.ConnectAsync()` creates an OPC UA client session used by all commands. It configures the application identity, sets up directory-based certificate stores under `%LocalAppData%\OpcUaCli\pki\`, and auto-accepts untrusted server certificates. The session timeout is 60 seconds. ## Architecture
`OpcUaHelper.ConvertValue()` converts a raw string from the command line into the runtime type expected by the target node. It uses the current node value to infer the type (bool, byte, short, int, float, double, etc.) and falls back to string if the type is not recognized. All commands inherit from `CommandBase`, which provides common connection options and helper methods. Every command follows the same lifecycle:
## Authentication Options 1. Build `ConnectionSettings` from common options
2. Create an `IOpcUaClientService` through the factory
3. Call `ConnectAsync` to establish a session
4. Perform the command-specific operation
5. Call `DisconnectAsync` in a `finally` block
All commands accept optional credentials for `UserName` token authentication: No command accesses the OPC UA `Session` directly; all operations go through the shared service abstraction. This ensures consistent behavior with the desktop UI client.
## Common Options
All commands accept these options:
| Flag | Description | | Flag | Description |
|------|-------------| |------|-------------|
| `-U` / `--username` | Username for OPC UA `UserName` token authentication | | `-u` / `--url` | OPC UA server endpoint URL (required) |
| `-P` / `--password` | Password for OPC UA `UserName` token authentication | | `-U` / `--username` | Username for `UserName` token authentication |
| `-P` / `--password` | Password for `UserName` token authentication |
| `-S` / `--security` | Transport security mode: `none`, `sign`, `encrypt`, `signandencrypt` (default: `none`) |
| `-F` / `--failover-urls` | Comma-separated failover endpoint URLs for redundancy |
| `--verbose` | Enable debug-level Serilog console logging (default: warning) |
When `-U` and `-P` are provided, `OpcUaHelper.ConnectAsync()` passes a `UserIdentity(username, password)` to `Session.Create`. Without credentials, an anonymous `UserIdentity` is used. ### Authentication
Example: When `-U` and `-P` are provided, the shared service passes a `UserIdentity(username, password)` to the OPC UA session. Without credentials, anonymous identity is used.
```bash ```bash
dotnet run -- write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42 -U operator -P op123 lmxopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42 -U operator -P op123
``` ```
## Failover Options ### Failover
All commands accept the `-F` / `--failover-urls` flag for automatic redundancy failover: When `-F` is provided, the shared service tries the primary URL first, then each failover URL in order. For long-running commands (`subscribe`, `alarms`), the service monitors the session via keep-alive and automatically reconnects to the next available server on failure.
| Flag | Description |
|------|-------------|
| `-F` / `--failover-urls` | Comma-separated list of alternate OPC UA endpoint URLs to try if the primary is unreachable |
When failover URLs are provided, the CLI tries the primary URL first, then each failover URL in order until one connects. For long-running commands (`subscribe`), the CLI monitors the session and automatically reconnects to the next available server if the current one drops.
Examples:
```bash ```bash
# Connect with failover lmxopcua-cli connect -u opc.tcp://localhost:4840/LmxOpcUa -F opc.tcp://localhost:4841/LmxOpcUa
dotnet run -- connect -u opc.tcp://localhost:4840/LmxOpcUa -F opc.tcp://localhost:4841/LmxOpcUa
# Subscribe with automatic failover on disconnect
dotnet run -- subscribe -u opc.tcp://localhost:4840/LmxOpcUa -F opc.tcp://localhost:4841/LmxOpcUa -n "ns=1;s=MyNode"
# Read redundancy state with failover
dotnet run -- redundancy -u opc.tcp://localhost:4840/LmxOpcUa -F opc.tcp://localhost:4841/LmxOpcUa
``` ```
## Transport Security Options ### Transport Security
All commands accept the `-S` / `--security` flag to select the transport security mode: When `sign` or `encrypt` is specified, the shared service:
| Flag | Values | Description | 1. Ensures a client application certificate exists under `{LocalAppData}/LmxOpcUaClient/pki/` (auto-created if missing)
|------|--------|-------------| 2. Discovers server endpoints and selects one matching the requested security mode
| `-S` / `--security` | `none`, `sign`, `encrypt` | Transport security mode (default: `none`) |
When `sign` or `encrypt` is specified, the CLI tool:
1. Ensures a client application certificate exists (auto-created if missing)
2. Discovers server endpoints and selects one matching the requested `MessageSecurityMode`
3. Prefers `Basic256Sha256` when multiple matching endpoints exist 3. Prefers `Basic256Sha256` when multiple matching endpoints exist
4. Fails with a clear error if no matching endpoint is found 4. Fails with a clear error if no matching endpoint is found
Examples:
```bash ```bash
# Connect with encrypted transport lmxopcua-cli browse -u opc.tcp://localhost:4840/LmxOpcUa -S encrypt -U admin -P secret -r -d 2
dotnet run -- connect -u opc.tcp://localhost:4840/LmxOpcUa -S encrypt
# Browse with signed transport and credentials
dotnet run -- browse -u opc.tcp://localhost:4840/LmxOpcUa -S sign -U admin -P secret -r -d 2
``` ```
### Verbose Logging
The `--verbose` flag switches Serilog output from `Warning` to `Debug` level, showing internal connection lifecycle, endpoint discovery, and OPC UA SDK diagnostics on the console.
## Commands ## Commands
### connect ### connect
Tests connectivity to an OPC UA server endpoint. Creates a session and immediately disconnects. Tests connectivity to an OPC UA server. Creates a session, prints connection metadata, and disconnects.
```bash ```bash
dotnet run -- connect -u opc.tcp://localhost:4840 lmxopcua-cli connect -u opc.tcp://localhost:4840/LmxOpcUa -U admin -P admin123
``` ```
### browse Output:
Browses the OPC UA address space starting from the Objects folder or a specified node. Supports recursive traversal with a configurable depth limit. ```text
Connected to: opc.tcp://localhost:4840/LmxOpcUa
```bash Server: LmxOpcUa
# Browse top-level Objects folder Security Mode: None
dotnet run -- browse -u opc.tcp://localhost:4840 Security Policy: http://opcfoundation.org/UA/SecurityPolicy#None
Connection successful.
# Browse a specific node
dotnet run -- browse -u opc.tcp://localhost:4840 -n "ns=2;s=MyFolder"
# Browse recursively to depth 3
dotnet run -- browse -u opc.tcp://localhost:4840 -r -d 3
``` ```
| Flag | Description |
|------|-------------|
| `-u` | OPC UA server endpoint URL (required) |
| `-n` | Node ID to browse (default: Objects folder) |
| `-d` | Maximum browse depth (default: 1) |
| `-r` | Browse recursively using `-d` as max depth |
### read ### read
Reads the current value of a single node and prints the value, data type, status code, and timestamps. Reads the current value of a single node and prints the value, status code, and timestamps.
```bash ```bash
dotnet run -- read -u opc.tcp://localhost:4840 -n "ns=2;s=TestMachine_001.SomeAttribute" lmxopcua-cli read -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=DEV.ScanState" -U admin -P admin123
``` ```
| Flag | Description | | Flag | Description |
|------|-------------| |------|-------------|
| `-u` | OPC UA server endpoint URL (required) | | `-n` / `--node` | Node ID to read (required) |
| `-n` | Node ID to read (required) |
Output:
```text
Node: ns=3;s=DEV.ScanState
Value: True
Status: 0x00000000
Source Time: 2026-03-30T19:58:38.0961252Z
Server Time: 2026-03-30T19:58:38.0971257Z
```
### write ### write
Writes a value to a node. The command reads the current value first to determine the target data type, then converts the supplied string value to that type using `OpcUaHelper.ConvertValue()`. Writes a value to a node. The shared service reads the current value first to determine the target data type, then converts the supplied string value using `ValueConverter.ConvertValue()`.
```bash ```bash
dotnet run -- write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42 lmxopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42
``` ```
| Flag | Description | | Flag | Description |
|------|-------------| |------|-------------|
| `-u` | OPC UA server endpoint URL (required) | | `-n` / `--node` | Node ID to write to (required) |
| `-n` | Node ID to write to (required) | | `-v` / `--value` | Value to write (required) |
| `-v` | Value to write (required) |
### browse
Browses the OPC UA address space starting from the Objects folder or a specified node. Supports recursive traversal with a configurable depth limit. Output uses tree-style indentation with `[Object]`, `[Variable]`, and `[Method]` markers.
```bash
# Browse top-level Objects folder
lmxopcua-cli browse -u opc.tcp://localhost:4840/LmxOpcUa -U admin -P admin123
# Browse a specific node recursively to depth 3
lmxopcua-cli browse -u opc.tcp://localhost:4840/LmxOpcUa -U admin -P admin123 -r -d 3 -n "ns=3;s=ZB"
```
| Flag | Description |
|------|-------------|
| `-n` / `--node` | Node ID to browse (default: Objects folder) |
| `-d` / `--depth` | Maximum browse depth (default: 1) |
| `-r` / `--recursive` | Browse recursively using `-d` as max depth |
### subscribe ### subscribe
Monitors a node for value changes using OPC UA subscriptions. Creates a `MonitoredItem` with the specified sampling interval and prints each notification with its source timestamp, value, and status code. Prints periodic tick lines showing session state, subscription ID, publishing status, and last notification value. Runs until Ctrl+C. Monitors a node for value changes using OPC UA subscriptions. Prints each data change notification with timestamp, value, and status code. Runs until Ctrl+C, then unsubscribes and disconnects cleanly.
```bash ```bash
dotnet run -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -i 500 lmxopcua-cli subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -i 500
``` ```
| Flag | Description | | Flag | Description |
|------|-------------| |------|-------------|
| `-u` | OPC UA server endpoint URL (required) | | `-n` / `--node` | Node ID to monitor (required) |
| `-n` | Node ID to monitor (required) | | `-i` / `--interval` | Sampling/publishing interval in milliseconds (default: 1000) |
| `-i` | Sampling/publishing interval in milliseconds (default: 1000) |
### historyread ### historyread
Reads historical data from a node. Supports both raw history reads and aggregate (processed) history reads. Reads historical data from a node. Supports raw history reads and aggregate (processed) history reads.
```bash ```bash
# Raw history # Raw history
dotnet run -- historyread -u opc.tcp://localhost:4840/LmxOpcUa \ lmxopcua-cli historyread -u opc.tcp://localhost:4840/LmxOpcUa \
-n "ns=1;s=TestMachine_001.TestHistoryValue" \ -n "ns=1;s=TestMachine_001.TestHistoryValue" \
--start "2026-03-25" --end "2026-03-30" --start "2026-03-25" --end "2026-03-30"
# Aggregate: 1-hour average # Aggregate: 1-hour average
dotnet run -- historyread -u opc.tcp://localhost:4840/LmxOpcUa \ lmxopcua-cli historyread -u opc.tcp://localhost:4840/LmxOpcUa \
-n "ns=1;s=TestMachine_001.TestHistoryValue" \ -n "ns=1;s=TestMachine_001.TestHistoryValue" \
--start "2026-03-25" --end "2026-03-30" \ --start "2026-03-25" --end "2026-03-30" \
--aggregate Average --interval 3600000 --aggregate Average --interval 3600000
@@ -177,81 +179,49 @@ dotnet run -- historyread -u opc.tcp://localhost:4840/LmxOpcUa \
| Flag | Description | | Flag | Description |
|------|-------------| |------|-------------|
| `-u` | OPC UA server endpoint URL (required) | | `-n` / `--node` | Node ID to read history for (required) |
| `-n` | Node ID to read history for (required) |
| `--start` | Start time, ISO 8601 or date string (default: 24 hours ago) | | `--start` | Start time, ISO 8601 or date string (default: 24 hours ago) |
| `--end` | End time, ISO 8601 or date string (default: now) | | `--end` | End time, ISO 8601 or date string (default: now) |
| `--max` | Maximum number of values (default: 1000) | | `--max` | Maximum number of values (default: 1000) |
| `--aggregate` | Aggregate function: Average, Minimum, Maximum, Count, Start, End | | `--aggregate` | Aggregate function: Average, Minimum, Maximum, Count, Start, End |
| `--interval` | Processing interval in milliseconds for aggregates (default: 3600000) | | `--interval` | Processing interval in milliseconds for aggregates (default: 3600000) |
#### Continuation points
When reading raw history, the server may return more values than fit in a single response. The command handles this by checking the `ContinuationPoint` on each result. If a continuation point is present, it issues follow-up `HistoryRead` calls with `ReleaseContinuationPoints = false` (i.e., `continuationPoint != null` is passed to the next read) until either the continuation point is empty or the `--max` limit is reached. This ensures that large result sets are fetched in full without requiring the caller to manage pagination.
#### Aggregate mapping #### Aggregate mapping
The `--aggregate` option maps human-readable names to OPC UA aggregate function node IDs:
| Name | OPC UA Node ID | | Name | OPC UA Node ID |
|------|---------------| |------|---------------|
| `average` | `AggregateFunction_Average` | | `Average` | `AggregateFunction_Average` |
| `minimum` / `min` | `AggregateFunction_Minimum` | | `Minimum` | `AggregateFunction_Minimum` |
| `maximum` / `max` | `AggregateFunction_Maximum` | | `Maximum` | `AggregateFunction_Maximum` |
| `count` | `AggregateFunction_Count` | | `Count` | `AggregateFunction_Count` |
| `start` / `first` | `AggregateFunction_Start` | | `Start` | `AggregateFunction_Start` |
| `end` / `last` | `AggregateFunction_End` | | `End` | `AggregateFunction_End` |
### alarms ### alarms
Subscribes to alarm events on a node using OPC UA event subscriptions. Creates a `MonitoredItem` with `AttributeId = EventNotifier` and an `EventFilter` that selects alarm-relevant fields from the event stream. Runs until Ctrl+C. Subscribes to alarm events on a node. Prints structured alarm output including source, condition, severity, active/acknowledged state, and message. Runs until Ctrl+C, then unsubscribes and disconnects cleanly.
```bash ```bash
# Subscribe to alarm events # Subscribe to alarm events on the Server node
dotnet run -- alarms -u opc.tcp://localhost:4840/LmxOpcUa \ lmxopcua-cli alarms -u opc.tcp://localhost:4840/LmxOpcUa
-n "ns=1;s=TestMachine_001"
# With condition refresh to get current alarm states # Subscribe to a specific source node with condition refresh
dotnet run -- alarms -u opc.tcp://localhost:4840/LmxOpcUa \ lmxopcua-cli alarms -u opc.tcp://localhost:4840/LmxOpcUa \
-n "ns=1;s=TestMachine_001" --refresh -n "ns=1;s=TestMachine_001" --refresh
``` ```
| Flag | Description | | Flag | Description |
|------|-------------| |------|-------------|
| `-u` | OPC UA server endpoint URL (required) | | `-n` / `--node` | Node ID to monitor for events (default: Server node) |
| `-n` | Node ID to monitor for events (default: Server node) | | `-i` / `--interval` | Publishing interval in milliseconds (default: 1000) |
| `-i` | Publishing interval in milliseconds (default: 1000) |
| `--refresh` | Request a `ConditionRefresh` after subscribing to get current retained alarm states | | `--refresh` | Request a `ConditionRefresh` after subscribing to get current retained alarm states |
#### EventFilter and alarm display
The command builds an `EventFilter` with select clauses for 12 fields from the OPC UA alarm type hierarchy:
| Index | Type | Field |
|-------|------|-------|
| 0 | `BaseEventType` | `EventId` |
| 1 | `BaseEventType` | `EventType` |
| 2 | `BaseEventType` | `SourceName` |
| 3 | `BaseEventType` | `Time` |
| 4 | `BaseEventType` | `Message` |
| 5 | `BaseEventType` | `Severity` |
| 6 | `ConditionType` | `ConditionName` |
| 7 | `ConditionType` | `Retain` |
| 8 | `AcknowledgeableConditionType` | `AckedState/Id` |
| 9 | `AlarmConditionType` | `ActiveState/Id` |
| 10 | `AlarmConditionType` | `EnabledState/Id` |
| 11 | `AlarmConditionType` | `SuppressedOrShelved` |
When an `EventFieldList` notification arrives, the handler extracts these fields by index and prints a structured alarm event to the console showing the source name, condition name, active/acknowledged state, severity, message, retain flag, and suppressed/shelved status.
The `--refresh` flag calls `subscription.ConditionRefreshAsync()` after the subscription is created, which asks the server to re-emit retained condition events so the operator sees the current alarm state immediately rather than waiting for the next transition.
### redundancy ### redundancy
Reads the OPC UA redundancy state from a server: `RedundancySupport`, `ServiceLevel`, `ServerUriArray`, and `ApplicationUri`. Reads the OPC UA redundancy state from a server: redundancy mode, service level, server URIs, and application URI.
```bash ```bash
dotnet run -- redundancy -u opc.tcp://localhost:4840/LmxOpcUa lmxopcua-cli redundancy -u opc.tcp://localhost:4840/LmxOpcUa -U admin -P admin123
``` ```
Example output: Example output:
@@ -265,9 +235,18 @@ Server URIs:
Application URI: urn:localhost:LmxOpcUa:instance1 Application URI: urn:localhost:LmxOpcUa:instance1
``` ```
| Flag | Description | ## Differences from tools/opcuacli-dotnet
|------|-------------|
| `-u` | OPC UA server endpoint URL (required) | The `Client.CLI` replaces the standalone tool at `tools/opcuacli-dotnet/`. Key differences:
| `-S` | Transport security mode (default: none) |
| `-U` | Username for authentication | | Aspect | tools/opcuacli-dotnet | Client.CLI |
| `-P` | Password for authentication | |--------|----------------------|------------|
| OPC UA access | Direct `Session` API | `IOpcUaClientService` abstraction |
| Connection/failover | Per-command `OpcUaFailoverHelper` | Shared service with built-in failover and keep-alive |
| Certificate management | Inline in `OpcUaHelper` | Shared `ApplicationConfigurationFactory` |
| Type conversion | `OpcUaHelper.ConvertValue` | `ValueConverter` in `Client.Shared` |
| Executable name | `opcuacli-dotnet` | `lmxopcua-cli` |
| Verbose logging | Not available | `--verbose` flag with Serilog |
| Testability | No test seam | `IOpcUaClientServiceFactory` injection, 52 unit tests |
The `tools/opcuacli-dotnet/` tool remains available for low-level debugging but is no longer the recommended client CLI.