276 lines
11 KiB
Markdown
276 lines
11 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
|
|
is pin-only. A TLS connection requires either `--ca-file` /
|
|
`ClientOptions::with_ca_file(...)` to pin a CA (export the gateway's self-signed
|
|
certificate and pin it), or `--require-certificate-validation` /
|
|
`with_require_certificate_validation(true)` to verify against the system trust
|
|
roots. 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`.
|
|
|
|
## 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` 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(BrowseChildrenRequest::default()).await?.into_inner();
|
|
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.0", registry = "dohertj2-gitea" }
|
|
```
|