motorcortex-rust
Rust client for the Motorcortex Core. Implements the low-level wire
protocol defined by motorcortex.proto: Request/Reply for parameter
access, Publish/Subscribe for real-time streaming, all over WebSocket
Security (TLS).
Ships in two flavours:
motorcortex_rust::core::*— async, tokio-driven. The recommended surface. Handles areClone + Send + Sync; multiple tasks share one connection safely.motorcortex_rust::blocking::*— thin synchronous wrapper. Useful for scripts, tests, and embedded-adjacent code that doesn't want a tokio runtime inmain().
Both share the same actor-style driver — no duplicated protocol logic.
Features
- Req/Rep RPCs:
login/logout,get_parameter/set_parameter, batch variants with tuple types,request_parameter_tree,create_group/remove_group,get_parameter_tree_hash. - Pub/Sub:
Subscriptionhandles with three independent sinks — syncnotify(cb)callback, asynclatest().await(watch-style, lossy), asyncstream(capacity)/ blockingiter(capacity)(bounded broadcast with explicitMissed(n)back-pressure). ConnectionStatepublished viatokio::sync::watchso callers can observeConnected/Disconnected/ConnectionLost/SessionExpiredtransitions.- Shared parameter-tree cache accessible via
request.parameter_tree(). - Compile-time message hashes via the
Hashtrait — drift between client and server is arustcerror, not a runtime exception.
Getting started
# Cargo.toml
[]
= "0.5"
= { = "1", = ["full"] } # optional — async callers only
Or pin to the master branch on the Vectioneer GitLab:
[]
= { = "https://git.vectioneer.com/pub/motorcortex-rust", = "master" }
The TLS certificate the examples expect (mcx.cert.crt) is downloadable
from https://docs.motorcortex.io/mcx.cert.crt.
Async example (recommended)
use Request;
use ;
async
Subscribe example
use StreamExt;
use ;
use ;
async
Blocking example (no tokio in main())
use Request;
use ;
Same method names as the async API, same error type, same semantics —
just drop the .awaits. The handle owns a hidden current-thread tokio
runtime internally.
Runnable examples
Four small binaries under examples/ that map to the
snippets above. Each takes its URL, cert path, credentials, and a
parameter path as CLI args; the defaults point at the in-repo
tests/mcx.cert.crt and a local test_server on ports 5568/5567,
so cargo run --example foo works out of the box once the server
is running (see Running the tests for how to
build and start it):
# Pointing at a real server:
Observing connection state
# use Request;
# async
Running behind nginx (or any reverse proxy)
Motorcortex's websocket sessions are long-lived. Reverse proxies
default to closing idle websockets after ~60 s, which looks like a
transport drop to the client. To survive that, the driver periodically
refreshes the server-issued session token
(ConnectionOptions::token_refresh_interval, default 30 s). When the
pipe does drop, NNG redials and the driver hands the cached token back
via RestoreSession, so RPCs resume without a re-login.
For the loop to work end-to-end, nginx needs websocket upgrade forwarding and a read timeout that's at least as long as the refresh interval. Minimal config:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl http2;
server_name mcx.example.com;
ssl_certificate /etc/ssl/certs/mcx.crt;
ssl_certificate_key /etc/ssl/private/mcx.key;
# REQ socket.
location /mcx_req {
proxy_pass https://127.0.0.1:5568;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# At least 2× the refresh interval. 60 s covers the 30 s default
# with headroom; bump both if you raise `token_refresh_interval`.
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
# SUB socket — same headers, different upstream port.
location /mcx_sub {
proxy_pass https://127.0.0.1:5567;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}
If the proxy sits between the client and server and does drop the
pipe, you'll see ConnectionState::ConnectionLost on the state watch;
the driver will then quietly reconnect and transition back to
Connected without user code having to do anything. On a server
restart that actually loses session state, the driver publishes
SessionExpired instead so your code knows a fresh login() is
needed.
Running the tests
# Offline — lib tests + integration-test-independent unit tests.
# Server-driven. Build the vendored C++ test_server once, then run.
# `--test-threads=1` is mandatory — tests share a single server instance.
See tests/README.md for the full walkthrough,
including coverage (cargo llvm-cov) and parallelism caveats.
Architecture
Design doc: ARCHITECTURE.md — actor-style driver,
three-sink subscription model, blocking façade as a thin block_on
layer.
Rollout plan for the remaining phases (reconnect + session tokens,
polish): TODO.md.
CI runner setup
The GitLab pipeline (.gitlab-ci.yml) expects a runner tagged rust
with curl, cmake, a C++20 g++, pkg-config, libnng-dev, and
the Motorcortex-proprietary mcx-core package already provisioned
(same stack as the motorcortex-python runner).
The Rust toolchain itself is not installed on the runner — the
pipeline downloads rustup into $CI_PROJECT_DIR/.rustup on first run
(minimal profile + llvm-tools-preview) and caches it under the
cargo cache key alongside cargo-llvm-cov. Subsequent runs reuse
the cached toolchain.
Documentation
Inline rustdoc on every public item:
License
MIT — see LICENSE.