API
7 minute read
Hyperlinks may not work here, but you can view the API directly in the Motorcortex-rust repository.
motorcortex-rust — API Documentation
Overview
motorcortex-rust is a Rust communication library for developing client applications that interact with the MotorCortex Core real-time control system. It implements the low-level API defined in motorcortex.proto, providing type-safe access to the Parameter Tree — a hierarchical structure of parameters exposed by the MotorCortex server.
The library supports two communication patterns:
- Request/Reply (Req/Rep) — synchronous parameter queries and modifications via the
Requestclient - Publish/Subscribe (Pub/Sub) — real-time streaming of parameter groups via the
Subscribeclient
Communication uses NNG sockets with optional TLS encryption via WebSocket Secure (WSS).
Getting Started
Prerequisites
- Rust toolchain (install via rustup)
- TLS certificate for secure connections (
mcx.cert.crt, downloadable from docs.motorcortex.io)
Installation
Add to your Cargo.toml:
[dependencies]
motorcortex-rust = { git = "https://git.vectioneer.com/pub/motorcortex-rust" }
Or clone and build locally:
git clone https://git.vectioneer.com/pub/motorcortex-rust
cd motorcortex-rust
cargo build
Minimal Example
use motorcortex_rust::{ConnectionOptions, Request, Result};
fn main() -> Result<()> {
let mut request = Request::new();
let conn_options = ConnectionOptions::new("mcx.cert.crt".to_string(), 1000, 1000);
request.connect("wss://127.0.0.1:5568", conn_options)?;
request.request_parameter_tree()?;
// Set a parameter
request.set_parameter("root/Control/dummyDouble", 2.345)?;
// Get a parameter (type is inferred from the target variable)
let value: f64 = request.get_parameter("root/Control/dummyDouble")?;
println!("Value: {}", value);
request.disconnect()?;
Ok(())
}
Error Handling
All fallible operations return Result<T>, which is an alias for std::result::Result<T, MotorcortexError>.
MotorcortexError
pub enum MotorcortexError {
Connection(String), // TLS, socket, or timeout failures
Encode(String), // Message encoding failed
Decode(String), // Message decoding failed
ParameterNotFound(String), // Parameter path not in the tree
Status(StatusCode), // Server returned a non-OK status
Io(String), // NNG send/receive failures
Subscription(String), // Subscription operation failed
}
Pattern Matching on Errors
use motorcortex_rust::{MotorcortexError, Result};
match request.get_parameter::<f64>("root/Control/nonexistent") {
Ok(val) => println!("Value: {}", val),
Err(MotorcortexError::ParameterNotFound(path)) => {
println!("No such parameter: {}", path);
}
Err(MotorcortexError::Io(msg)) => {
println!("Connection issue: {}", msg);
}
Err(e) => println!("Other error: {}", e),
}
Propagation with ?
fn read_control_values(request: &Request) -> Result<(f64, f64)> {
request.request_parameter_tree()?;
let speed: f64 = request.get_parameter("root/Control/speed")?;
let torque: f64 = request.get_parameter("root/Control/torque")?;
Ok((speed, torque))
}
Retry Logic
fn connect_with_retry(request: &mut Request, url: &str, opts: ConnectionOptions) -> Result<()> {
for attempt in 1..=3 {
match request.connect(url, opts.clone()) {
Ok(()) => return Ok(()),
Err(MotorcortexError::Connection(msg)) if msg.contains("timeout") => {
println!("Attempt {} timed out, retrying...", attempt);
}
Err(e) => return Err(e),
}
}
Err(MotorcortexError::Connection("All retries exhausted".to_string()))
}
Connection
ConnectionOptions
Configuration for establishing a connection to the MotorCortex server.
pub struct ConnectionOptions {
pub certificate: String, // Path to TLS certificate file
pub conn_timeout_ms: u32, // Connection establishment timeout (ms)
pub io_timeout_ms: u32, // I/O operation timeout (ms)
}
Constructor:
let opts = ConnectionOptions::new(
"mcx.cert.crt".to_string(), // certificate path (empty string disables TLS)
1000, // connection timeout: 1 second
1000, // I/O timeout: 1 second
);
Connection Trait
Both Request and Subscribe implement the Connection trait:
pub trait Connection {
fn connect(&mut self, url: &str, options: ConnectionOptions) -> Result<()>;
fn disconnect(&mut self) -> Result<()>;
}
Request Client
The Request client handles synchronous operations against the MotorCortex server.
Creating and Connecting
let mut request = Request::new();
request.connect("wss://127.0.0.1:5568", conn_options)?;
Authentication
// Login — returns StatusCode (Ok, ReadOnlyMode, WrongPassword, FailedToDecode)
let status = request.login("admin".to_string(), "password".to_string())?;
// Logout
let status = request.logout()?;
Parameter Tree
Before getting or setting parameters, the parameter tree must be fetched from the server:
request.request_parameter_tree()?;
You can also retrieve the tree without caching it, or fetch only its hash for change detection:
let (status, tree) = request.get_parameter_tree()?;
let hash: u32 = request.get_parameter_tree_hash()?;
Getting Parameters
Single parameter with automatic type casting:
// The return type is inferred — the server value is converted automatically
let val_f32: f32 = request.get_parameter("root/Control/dummyDouble")?;
let val_str: String = request.get_parameter("root/Control/dummyDouble")?;
let val_i64: i64 = request.get_parameter("root/Control/dummyDouble")?;
Multiple parameters as a tuple (up to 10 elements, heterogeneous types):
let (a, b): (f64, i32) = request.get_parameters(vec![
"root/Control/param1",
"root/Control/param2",
])?;
Setting Parameters
Single scalar value:
request.set_parameter("root/Control/dummyDouble", 2.345)?;
Fixed-size array:
request.set_parameter("root/Control/dummyDoubleVec", [1.0, 2.0, 3.0])?;
Dynamic vector:
request.set_parameter("root/Control/dummyDoubleVec", vec![1.35, 2.34, 3.45])?;
Multiple parameters with a tuple:
request.set_parameters(
vec!["root/Control/speed", "root/Control/position"],
(3.14_f64, 100_i32),
)?;
Disconnecting
request.disconnect()?;
Subscribe Client
Real-time streaming client that receives parameter updates from the server at a configurable frequency.
Creating and Connecting
let mut subscribe = Subscribe::new();
subscribe.connect("wss://127.0.0.1:5569", conn_opts)?;
Creating a Subscription
A Request client must be passed to subscribe() and unsubscribe(). This is because subscription group management (creation and removal) is a Req/Rep operation — the SUB socket used by Subscribe can only receive data, not send requests. The Request client handles these server-side group operations on behalf of Subscribe.
let sub: ReadOnlySubscription = subscribe.subscribe(
&request, // Request client (for group management)
["root/Control/param1", "root/Control/param2"], // parameter paths
"my_group", // group name
10, // frequency divider
)?;
The frequency_divider controls the update rate relative to the server’s base frequency. A divider of 10 means updates arrive at 1/10th of the base rate.
Reading Subscription Data
Single value or tuple (typed readback):
// Read latest value (without timestamp)
if let Some(value) = sub.read::<f64>() {
println!("Current value: {}", value);
}
// Read with timestamp
if let Some((timestamp, value)) = sub.read_with_timestamp::<f64>() {
println!("[{}] Value: {}", timestamp.to_date_time(), value);
}
// Read multiple parameters as a tuple
if let Some((a, b)) = sub.read::<(f64, i32)>() {
println!("param1={}, param2={}", a, b);
}
All parameters as a flat vector:
if let Some((timestamp, values)) = sub.read_all::<f64>() {
println!("All values: {:?}", values);
}
// Also works with other types
if let Some((ts, strings)) = sub.read_all::<String>() {
println!("As strings: {:?}", strings);
}
read_all decodes every element of every subscribed parameter into a single Vec<V>, regardless of array sizes. This is useful for logging or bulk processing.
Notifications (Callbacks)
Register a callback function that fires on every update:
sub.notify(|subscription| {
if let Some((ts, val)) = subscription.read::<f64>() {
println!("[{:?}] New value: {}", ts, val);
}
});
Important notes:
- Calling
notifyagain replaces the previous callback — only one callback can be active per subscription at a time. - The callback is invoked on the receive thread. Avoid blocking operations (heavy computation, synchronous I/O, locking contested mutexes) inside the callback, as this will delay processing of subsequent subscription updates.
Unsubscribing and Disconnecting
let id = sub.id();
subscribe.unsubscribe(&request, id)?;
subscribe.disconnect()?;
ReadOnlySubscription Reference
| Method | Returns | Description |
|---|---|---|
read::<V>() |
Option<V> |
Latest value(s) as typed tuple |
read_with_timestamp::<V>() |
Option<(TimeSpec, V)> |
Value(s) with server timestamp |
read_all::<V>() |
Option<(TimeSpec, Vec<V>)> |
All elements as flat vector |
notify(cb) |
() |
Register update callback |
name() |
String |
Group alias |
id() |
u32 |
Subscription ID |
TimeSpec
Server timestamp with nanosecond precision:
pub struct TimeSpec {
pub sec: i64, // Seconds since Unix epoch
pub nsec: i64, // Nanoseconds
}
to_date_time()→DateTime<Local>— converts to local timeto_utc_date_time()→DateTime<Utc>— converts to UTC
Parameter Tree
The ParameterTree provides lookup methods for parameter metadata after calling request_parameter_tree().
| Method | Returns | Description |
|---|---|---|
get_parameter_info(path) |
Option<&ParameterInfo> |
Full parameter metadata |
get_parameter_data_type(path) |
Option<u32> |
Data type tag |
has_parameter(path) |
bool |
Check if a path exists |
parameters() |
Iterator<(&str, &ParameterInfo)> |
Iterate all leaf parameters |
ParameterInfo Fields
| Field | Type | Description |
|---|---|---|
id |
u32 |
Unique server-assigned ID |
data_type |
u32 |
Data type tag (see supported types below) |
data_size |
u32 |
Size of one element in bytes |
number_of_elements |
u32 |
Array length (1 for scalars) |
flags |
u32 |
Parameter flags |
permissions |
u32 |
Access permissions |
param_type |
ParameterType |
Parameter vs. group indicator |
group_id |
UserGroup |
Owner group |
unit |
Unit |
SI unit |
path |
String |
Full hierarchical path |
Supported Data Types
| DataType | Rust Equivalent | Size (bytes) |
|---|---|---|
Bool |
bool |
1 |
Int8 |
i8 |
1 |
Uint8 |
u8 |
1 |
Int16 |
i16 |
2 |
Uint16 |
u16 |
2 |
Int32 |
i32 |
4 |
Uint32 |
u32 |
4 |
Int64 |
i64 |
8 |
Uint64 |
u64 |
8 |
Float |
f32 |
4 |
Double |
f64 |
8 |
String |
String |
variable |
The library performs automatic type conversion between the server’s data type and the requested Rust type. For example, a Double parameter on the server can be read as f32, i64, String, or any other supported type.
Values can be passed as scalars, fixed-size arrays ([V; N] up to N=10), or vectors (Vec<V>). Multiple parameters can be read/written in a single call using tuples of up to 10 heterogeneous elements.
Flexible Path Input
Parameter paths can be passed in multiple formats wherever a Parameters argument is expected:
| Type | Example |
|---|---|
&str |
"root/Control/param" |
Vec<String> |
vec!["path1".to_string(), "path2".to_string()] |
&[&str] |
&["path1", "path2"] |
[&str; N] |
["path1", "path2"] |
NNG Configuration
Thread Initialization
Configure NNG thread pools before creating any connections:
use motorcortex_rust::{init_threads, init_threads_with_defaults};
// Custom thread counts
init_threads(
2, // task threads
1, // expire threads
1, // poller threads
1, // resolver threads
);
// Or use defaults (2, 1, 1, 1)
init_threads_with_defaults();
Logging
Enable NNG logging for debugging:
use motorcortex_rust::{init_logger, init_debug_logger, LogLevel};
// Set a specific log level
init_logger(LogLevel::Warn);
// Or enable full debug output
init_debug_logger();
Log levels: None, Debug, Info, Warn, Error
Message Hashing
Every protobuf message type has a compile-time hash for wire-format identification:
use motorcortex_rust::{get_hash, get_hash_size, SessionTokenMsg};
let hash: u32 = get_hash::<SessionTokenMsg>();
let size: usize = get_hash_size(); // always 4 bytes
Complete Examples
Req/Rep: Set and Get with Type Casting
use motorcortex_rust::{ConnectionOptions, Request, Result};
fn main() -> Result<()> {
let mut request = Request::new();
let opts = ConnectionOptions::new("mcx.cert.crt".to_string(), 1000, 1000);
request.connect("wss://127.0.0.1:5568", opts)?;
request.request_parameter_tree()?;
// Set a scalar
request.set_parameter("root/Control/dummyDouble", 2.345)?;
// Read back as different types
let as_f32: f32 = request.get_parameter("root/Control/dummyDouble")?;
let as_string: String = request.get_parameter("root/Control/dummyDouble")?;
let as_i64: i64 = request.get_parameter("root/Control/dummyDouble")?;
println!("f32: {}, string: {}, i64: {}", as_f32, as_string, as_i64);
request.disconnect()?;
Ok(())
}
Req/Rep: Array and Vector Parameters
// Fixed-size array
request.set_parameter("root/Control/dummyDoubleVec", [1.0, 2.0, 3.0])?;
let arr: [f64; 3] = request.get_parameter("root/Control/dummyDoubleVec")?;
// Dynamic vector
request.set_parameter("root/Control/dummyDoubleVec", vec![1.35, 2.34, 3.45])?;
let vec_val: Vec<f64> = request.get_parameter("root/Control/dummyDoubleVec")?;
Pub/Sub: Real-Time Subscription
use motorcortex_rust::{ConnectionOptions, Request, Subscribe, Connection, Result};
fn main() -> Result<()> {
// Set up request client (needed for group management)
let mut request = Request::new();
let req_opts = ConnectionOptions::new("mcx.cert.crt".to_string(), 1000, 1000);
request.connect("wss://127.0.0.1:5568", req_opts)?;
request.request_parameter_tree()?;
// Set up subscribe client
let mut subscribe = Subscribe::new();
let sub_opts = ConnectionOptions::new("mcx.cert.crt".to_string(), 1000, 1000);
subscribe.connect("wss://127.0.0.1:5569", sub_opts)?;
// Subscribe to parameters
let sub = subscribe.subscribe(
&request,
["root/Control/param1", "root/Control/param2"],
"my_group",
10,
)?;
// Option 1: Poll for data
loop {
if let Some((ts, (a, b))) = sub.read_with_timestamp::<(f64, i32)>() {
println!("[{}] param1={}, param2={}", ts.to_date_time(), a, b);
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
// Option 2: Use callbacks
sub.notify(|subscription| {
if let Some((ts, values)) = subscription.read_all::<f64>() {
println!("Update at {:?}: {:?}", ts, values);
}
});
// Cleanup
let id = sub.id();
subscribe.unsubscribe(&request, id)?;
subscribe.disconnect()?;
request.disconnect()?;
Ok(())
}
Thread Safety
Request and Subscribe are Send but not Sync:
Send— you can move ownership to another thread- Not
Sync— you cannot share&Requestor&Subscribeacross threads concurrently
This is by design. The Req/Rep protocol requires strict send→receive ordering, which cannot be guaranteed with concurrent callers.
// ✅ Move to another thread
let mut request = Request::new();
request.connect("wss://127.0.0.1:5568", opts)?;
thread::spawn(move || {
let val: f64 = request.get_parameter("root/Control/param").unwrap();
});
// ✅ Each thread creates its own
thread::spawn(|| {
let mut request = Request::new();
request.connect("wss://127.0.0.1:5568", opts).unwrap();
});
// ❌ Won't compile — can't share &Request across threads
let request = Request::new();
let r = &request;
thread::spawn(move || {
r.get_parameter::<f64>("path");
});
If you need shared access from multiple threads, wrap in Mutex<Request> or use a channel-based pattern.
Note: ReadOnlySubscription is thread-safe — it uses Arc<RwLock<Subscription>> internally and can be freely shared across threads.
License
This project is licensed under the MIT License.