9eedf9d6a9
A consuming project hit two MXAccess parity surprises: a plain Write only records its user_id when the item has an active supervisory advise (the path to take when not authenticating), and array writes replace the whole array rather than patching individual elements. Document both across the five client READMEs and gateway.md's compatibility baseline, and expose the missing advise-supervisory subcommand in the go/python/rust/java CLIs (plus the .NET help text) so callers can establish the supervisory advise without dropping to the raw command API.
327 lines
13 KiB
Markdown
327 lines
13 KiB
Markdown
# Rust Client Workspace
|
|
|
|
The Rust client workspace contains the MXAccess Gateway client library, a
|
|
test CLI, and tests for generated contract wiring plus wrapper behavior. The
|
|
library uses
|
|
the shared protobuf inputs documented in
|
|
`../../docs/ClientProtoGeneration.md` so the Rust bindings compile against
|
|
the same public gateway and worker contracts as the server.
|
|
|
|
## Layout
|
|
|
|
```text
|
|
clients/rust/
|
|
Cargo.toml
|
|
build.rs
|
|
src/
|
|
tests/
|
|
crates/mxgw-cli/
|
|
```
|
|
|
|
`build.rs` reads the `.proto` files from
|
|
`../../src/ZB.MOM.WW.MxGateway.Contracts/Protos` and generates `tonic`/`prost` bindings
|
|
into Cargo build output. `src/generated.rs` declares the Rust modules that
|
|
include those generated files. `src/generated` remains reserved for checked-in
|
|
generator output if the crate later changes to source-tree generation.
|
|
|
|
## Build And Test
|
|
|
|
Run the Rust workspace checks from `clients/rust`:
|
|
|
|
```powershell
|
|
cargo fmt --all --check
|
|
cargo test --workspace
|
|
cargo check --workspace
|
|
cargo clippy --workspace --all-targets -- -D warnings
|
|
```
|
|
|
|
The build script uses `protoc` from `PATH` or the Windows path recorded in
|
|
`../../docs/ToolchainLinks.md`.
|
|
|
|
## Packaging
|
|
|
|
Create local release artifacts from `clients/rust`:
|
|
|
|
```powershell
|
|
cargo build --workspace --release
|
|
cargo install --path crates/mxgw-cli --locked --force
|
|
```
|
|
|
|
`cargo check --workspace` regenerates the `tonic` and `prost` modules into
|
|
Cargo build output through `build.rs`.
|
|
|
|
## CLI
|
|
|
|
The CLI exposes version, session, command, event stream, write, and smoke
|
|
commands over the same client wrapper used by tests:
|
|
|
|
```powershell
|
|
cargo run -p mxgw-cli -- version --json
|
|
cargo run -p mxgw-cli -- open-session --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
|
cargo run -p mxgw-cli -- register --session-id <session-id> --client-name mxgw-rust-cli --json
|
|
cargo run -p mxgw-cli -- add-item --session-id <session-id> --server-handle 1 --item TestChildObject.TestInt --json
|
|
cargo run -p mxgw-cli -- advise --session-id <session-id> --server-handle 1 --item-handle 1 --json
|
|
cargo run -p mxgw-cli -- stream-events --session-id <session-id> --max-events 1 --json
|
|
cargo run -p mxgw-cli -- stream-alarms --session-id <session-id> --max-messages 1 --json
|
|
cargo run -p mxgw-cli -- acknowledge-alarm --session-id <session-id> --alarm-reference "\\Galaxy\Area001.Pump001.PumpFault" --json
|
|
cargo run -p mxgw-cli -- write --session-id <session-id> --server-handle 1 --item-handle 1 --value-type int32 --value 123 --json
|
|
```
|
|
|
|
Use `--tls`, `--ca-file`, and `--server-name-override` for TLS endpoints. The
|
|
CLI reads the API key from `--api-key` or from `--api-key-env`, which defaults
|
|
to `MXGATEWAY_API_KEY`. API keys are redacted by the library option and secret
|
|
types.
|
|
|
|
```powershell
|
|
cargo run -p mxgw-cli -- smoke --endpoint https://mxgateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt --json
|
|
```
|
|
|
|
### TLS trust (pin-only)
|
|
|
|
The gateway can auto-generate its own self-signed certificate (it has no PKI).
|
|
Unlike the other clients, the Rust client is **not** lenient: tonic 0.13.1
|
|
exposes no public hook to inject a custom certificate verifier, so TLS over Rust
|
|
cannot accept an *arbitrary* self-signed certificate. A TLS connection requires
|
|
one of two trust paths:
|
|
|
|
- `--ca-file` / `ClientOptions::with_ca_file(...)` to pin a CA (export the
|
|
gateway's self-signed certificate and pin it). This is the path for a
|
|
self-signed gateway.
|
|
- `--require-certificate-validation` / `with_require_certificate_validation(true)`
|
|
to verify against the operating system's trust roots (`tls-native-roots`). This
|
|
only succeeds for a certificate that chains to a root the host already trusts —
|
|
i.e. a gateway fronted by a publicly- or enterprise-CA-issued certificate, not a
|
|
bare self-signed one.
|
|
|
|
TLS with neither set fails `connect` with a clear, actionable error rather
|
|
than accepting the certificate. See
|
|
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).
|
|
|
|
## Library Surface
|
|
|
|
`ClientOptions` configures endpoint, API key, plaintext or TLS transport,
|
|
timeouts, custom CA files, and server name override. `GatewayClient::connect`
|
|
creates an authenticated `tonic` client and attaches `authorization: Bearer
|
|
<api-key>` metadata to unary and streaming calls.
|
|
|
|
`GatewayClient` exposes raw generated calls through `open_session_raw`,
|
|
`close_session_raw`, `invoke_raw`, `stream_events`, `query_active_alarms`,
|
|
`stream_alarms`, `acknowledge_alarm`, and `raw_client`. `stream_alarms`
|
|
returns an `AlarmFeedStream` async stream of alarm-feed messages and
|
|
shares the gateway's central alarm monitor with every other client. The
|
|
session helpers keep MXAccess handles visible:
|
|
|
|
```rust
|
|
let session = client.open_session(request).await?;
|
|
let server_handle = session.register("mxgw-rust").await?;
|
|
let item_handle = session.add_item(server_handle, "TestChildObject.TestInt").await?;
|
|
session.advise(server_handle, item_handle).await?;
|
|
let mut events = session.events().await?;
|
|
session.close().await?;
|
|
```
|
|
|
|
`MxValue`, `MxArrayValue`, and `MxStatus` wrap generated protobuf messages while
|
|
preserving the raw message for parity diagnostics. Command replies whose
|
|
protocol status is not `PROTOCOL_STATUS_CODE_OK` become `Error::Command` and
|
|
retain the raw `MxCommandReply`.
|
|
|
|
## Write Semantics And Common Pitfalls
|
|
|
|
These are MXAccess parity behaviors that surprise new callers. The gateway
|
|
forwards them unchanged — it does not paper over them.
|
|
|
|
### Attributing a write to a user without `authenticate_user`
|
|
|
|
MXAccess only stamps a plain `write`/`write2` with a Galaxy user id when the
|
|
item carries an active *supervisory* advise. If you are **not** using the
|
|
verified/secured path (`authenticate_user` → `write_secured`/`write_secured2`)
|
|
but still need the write attributed to a user id, you must first advise the
|
|
item supervisory and then pass that user id on the write. Without the
|
|
supervisory advise the `user_id` on a plain write is ignored.
|
|
|
|
The session exposes `advise`/`un_advise` but not supervisory advise, so send it
|
|
through the generic command channel:
|
|
|
|
```rust
|
|
session
|
|
.invoke(
|
|
MxCommandKind::AdviseSupervisory,
|
|
Payload::AdviseSupervisory(AdviseSupervisoryCommand {
|
|
server_handle,
|
|
item_handle,
|
|
}),
|
|
)
|
|
.await?;
|
|
|
|
session.write(server_handle, item_handle, value, user_id).await?;
|
|
```
|
|
|
|
The CLI exposes the same command as `advise-supervisory`, and `write` /
|
|
`write2` take `--user-id`.
|
|
|
|
### Array writes replace the whole array
|
|
|
|
A write to an array attribute **replaces the entire array**; it is not an
|
|
element-wise patch. To change a subset of elements, send the full array with
|
|
the unchanged elements included. For example, to change 2 elements of a
|
|
20-element array, build the `MxValue` from all 20 values (the 18 unchanged plus
|
|
the 2 new ones). Sending only the 2 changed values overwrites the attribute
|
|
with a 2-element array.
|
|
|
|
## Galaxy Repository browse
|
|
|
|
The Galaxy Repository service exposes a read-only browse over the AVEVA System
|
|
Platform Galaxy Repository (ZB SQL database). It uses the same API-key auth as
|
|
the gateway service but requires the `metadata:read` scope on the server.
|
|
|
|
[`GalaxyClient`](src/galaxy.rs) wraps the generated Galaxy bindings the same
|
|
way [`GatewayClient`](src/client.rs) wraps the gateway bindings:
|
|
|
|
```rust
|
|
let mut galaxy = GalaxyClient::connect(
|
|
ClientOptions::new("http://localhost:5000")
|
|
.with_api_key(ApiKey::new(api_key)),
|
|
).await?;
|
|
|
|
let ok = galaxy.test_connection().await?;
|
|
let last_deploy = galaxy.get_last_deploy_time().await?; // Option<prost_types::Timestamp>
|
|
let objects = galaxy.discover_hierarchy().await?; // Vec<GalaxyObject>
|
|
```
|
|
|
|
`get_last_deploy_time` returns `None` when the server reports
|
|
`present = false`. `discover_hierarchy` returns the generated
|
|
`GalaxyObject` proto type (re-exported via
|
|
`zb_mom_ww_mxgateway_client::generated::galaxy_repository::v1`) with all attributes
|
|
attached.
|
|
|
|
The CLI ships matching subcommands under `galaxy`:
|
|
|
|
```powershell
|
|
cargo run -p mxgw-cli -- galaxy test-connection --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
|
cargo run -p mxgw-cli -- galaxy last-deploy-time --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
|
cargo run -p mxgw-cli -- galaxy discover-hierarchy --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
|
```
|
|
|
|
### Browsing lazily
|
|
|
|
For UI trees or OPC UA bridges, use `browse_children_raw` to walk one level at a
|
|
time instead of paging the full hierarchy. Pass a default request for root
|
|
objects; subsequent calls set `parent_gobject_id`, `parent_tag_name`, or
|
|
`parent_contained_path`. Filter fields match `discover_hierarchy`. Each response
|
|
pairs `children` with `child_has_children` so you know which nodes to expand. See
|
|
[Galaxy Repository](../../docs/GalaxyRepository.md#browsechildren) for full
|
|
request and filter semantics.
|
|
|
|
```rust
|
|
use zb_mom_ww_mxgateway_client::generated::galaxy_repository::v1::BrowseChildrenRequest;
|
|
|
|
let reply = galaxy.browse_children_raw(BrowseChildrenRequest::default()).await?;
|
|
for (child, has_children) in reply.children.iter().zip(reply.child_has_children.iter()) {
|
|
println!("{} expand={}", child.tag_name, has_children);
|
|
}
|
|
```
|
|
|
|
#### High-level walker
|
|
|
|
For UI trees, the client provides a `LazyBrowseNode` walker that handles
|
|
sibling pagination and the `child_has_children` hint for you:
|
|
|
|
```rust
|
|
let mut client = GalaxyClient::connect(
|
|
ClientOptions::new("http://localhost:5000").with_api_key(ApiKey::new(api_key)),
|
|
).await?;
|
|
let roots = client.browse(None).await?;
|
|
for root in &roots {
|
|
if root.has_children_hint() {
|
|
root.expand().await?;
|
|
}
|
|
for child in root.children().await {
|
|
let kind = if child.has_children_hint() { "has children" } else { "leaf" };
|
|
println!("{} ({kind})", child.object().tag_name);
|
|
}
|
|
}
|
|
```
|
|
|
|
`expand` is idempotent — calling it twice fires only one RPC,
|
|
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
|
|
`browse` again from the root.
|
|
|
|
### Watching deploy events
|
|
|
|
`watch_deploy_events` opens the `WatchDeployEvents` server stream. The
|
|
server emits a bootstrap [`DeployEvent`](src/galaxy.rs) describing the
|
|
current cache state on subscribe, then one event each time the cached
|
|
`galaxy.time_of_last_deploy` changes. `sequence` is monotonic per server
|
|
start; gaps signal that the per-subscriber buffer dropped older events.
|
|
Pass `last_seen_deploy_time` to suppress the bootstrap event when the
|
|
client's cached deploy time matches the server's.
|
|
|
|
```rust
|
|
use futures_util::StreamExt;
|
|
|
|
let mut stream = galaxy.watch_deploy_events(None).await?;
|
|
while let Some(event) = stream.next().await {
|
|
let event = event?;
|
|
println!(
|
|
"seq={} objects={} attributes={}",
|
|
event.sequence, event.object_count, event.attribute_count,
|
|
);
|
|
}
|
|
// Drop the stream to cancel the gRPC call.
|
|
```
|
|
|
|
The matching CLI subcommand prints one line per event (`--json` switches to
|
|
one JSON object per event). `--last-seen-deploy-time` accepts an RFC3339
|
|
timestamp and is forwarded to the server. `--max-events` (default 0 = no
|
|
cap) lets you stop after a fixed number of events; otherwise the command
|
|
runs until the stream ends or `Ctrl+C` is pressed.
|
|
|
|
```powershell
|
|
cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY
|
|
cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
|
cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --last-seen-deploy-time 2026-04-28T15:30:00Z
|
|
```
|
|
|
|
## Integration Checks
|
|
|
|
Run live checks only when a gateway and MXAccess-backed worker are available:
|
|
|
|
```powershell
|
|
$env:MXGATEWAY_INTEGRATION = '1'
|
|
$env:MXGATEWAY_ENDPOINT = 'http://127.0.0.1:5000'
|
|
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
|
|
$env:MXGATEWAY_TEST_ITEM = 'TestChildObject.TestInt'
|
|
cargo run -p mxgw-cli -- smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
|
```
|
|
|
|
## Related Documentation
|
|
|
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
|
- [Rust Client Detailed Design](./RustClientDesign.md)
|
|
- [Rust Style Guide](../../docs/style-guides/RustStyleGuide.md)
|
|
|
|
## Installing from the Gitea Cargo registry
|
|
|
|
The crate publishes to the internal Gitea Cargo registry. Register the
|
|
registry once in your global `~/.cargo/config.toml`:
|
|
|
|
```toml
|
|
[registries.dohertj2-gitea]
|
|
index = "sparse+https://gitea.dohertylan.com/api/packages/dohertj2/cargo/"
|
|
```
|
|
|
|
Authentication: cargo reads credentials from `~/.cargo/credentials.toml`:
|
|
|
|
```toml
|
|
[registries.dohertj2-gitea]
|
|
token = "Bearer <your-gitea-token>"
|
|
```
|
|
|
|
Then add the dependency:
|
|
|
|
```toml
|
|
[dependencies]
|
|
zb-mom-ww-mxgateway-client = { version = "0.1.1", registry = "dohertj2-gitea" }
|
|
```
|