Handles the new MxSparseArray wire type (proto field 19 on MxValue::Kind): - value.rs: map SparseArrayValue to MxValueProjection::Unset (write-only; never emitted on read path) - session.rs: add write_array_elements() that builds the sparse proto value and delegates to write() - tests: three unit tests asserting proto shape, empty-elements case, and read-path Unset projection - README: document write_array_elements default-fill semantics and bare-name [] normalisation
14 KiB
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
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:
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:
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:
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.
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.
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:
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:
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.
Default-fill partial array writes
When you only need to set a handful of indices and want every other position to
take the element type's default (zero / false / empty string / Unix epoch for
timestamps), use Session::write_array_elements instead:
// Write a 10-element integer array; index 0 = 42, index 7 = 99,
// all other indices default to 0 (not preserved from the previous value).
session
.write_array_elements(
server_handle,
item_handle,
MxDataType::Integer,
10,
[(0, MxValue::int32(42)), (7, MxValue::int32(99))],
user_id,
)
.await?;
The gateway expands the sparse representation into a full MxArray before
forwarding to the worker — the worker and MXAccess COM never see the sparse
form. Unmentioned indices are reset to the type default, not preserved from
the existing attribute value.
Bare-name array AddItem normalisation
AddItem for a bare array attribute name (e.g. Tank01.Temperature) is
automatically normalised to Tank01.Temperature[] by the gateway so the
worker can resolve the full array. You do not need to append [] in client
code; the gateway handles it.
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 wraps the generated Galaxy bindings the same
way GatewayClient wraps the gateway bindings:
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:
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 for full
request and filter semantics.
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:
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 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.
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.
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:
$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
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:
[registries.dohertj2-gitea]
index = "sparse+https://gitea.dohertylan.com/api/packages/dohertj2/cargo/"
Authentication: cargo reads credentials from ~/.cargo/credentials.toml:
[registries.dohertj2-gitea]
token = "Bearer <your-gitea-token>"
Then add the dependency:
[dependencies]
zb-mom-ww-mxgateway-client = { version = "0.1.1", registry = "dohertj2-gitea" }