# 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 `Request` client
- **Publish/Subscribe (Pub/Sub)** — real-time streaming of parameter groups via the `Subscribe` client
Communication uses NNG sockets with optional TLS encryption via WebSocket Secure (WSS).
---
## Getting Started
### Prerequisites
- Rust toolchain (install via [rustup](https://rustup.rs/))
- TLS certificate for secure connections (`mcx.cert.crt`, downloadable from [docs.motorcortex.io](https://docs.motorcortex.io/mcx.cert.crt))
### Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
motorcortex-rust = { git = "https://git.vectioneer.com/pub/motorcortex-rust" }
```
Or clone and build locally:
```bash
git clone https://git.vectioneer.com/pub/motorcortex-rust
cd motorcortex-rust
cargo build
```
### Minimal Example
```rust
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`
```rust
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
```rust
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 `?`
```rust
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
```rust
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.
```rust
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:**
```rust
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:
```rust
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
```rust
let mut request = Request::new();
request.connect("wss://127.0.0.1:5568", conn_options)?;
```
### Authentication
```rust
// 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:
```rust
request.request_parameter_tree()?;
```
You can also retrieve the tree without caching it, or fetch only its hash for change detection:
```rust
let (status, tree) = request.get_parameter_tree()?;
let hash: u32 = request.get_parameter_tree_hash()?;
```
### Getting Parameters
Single parameter with automatic type casting:
```rust
// 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):
```rust
let (a, b): (f64, i32) = request.get_parameters(vec![
"root/Control/param1",
"root/Control/param2",
])?;
```
### Setting Parameters
Single scalar value:
```rust
request.set_parameter("root/Control/dummyDouble", 2.345)?;
```
Fixed-size array:
```rust
request.set_parameter("root/Control/dummyDoubleVec", [1.0, 2.0, 3.0])?;
```
Dynamic vector:
```rust
request.set_parameter("root/Control/dummyDoubleVec", vec![1.35, 2.34, 3.45])?;
```
Multiple parameters with a tuple:
```rust
request.set_parameters(
vec!["root/Control/speed", "root/Control/position"],
(3.14_f64, 100_i32),
)?;
```
### Disconnecting
```rust
request.disconnect()?;
```
---
## Subscribe Client
Real-time streaming client that receives parameter updates from the server at a configurable frequency.
### Creating and Connecting
```rust
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`.
```rust
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):**
```rust
// 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:**
```rust
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:
```rust
println!("[{:?}] New value: {}", ts, val);
}
});
```
**Important notes:**
- Calling `notify` again **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
```rust
let id = sub.id();
subscribe.unsubscribe(&request, id)?;
subscribe.disconnect()?;
```
### `ReadOnlySubscription` Reference
| `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:
```rust
pub struct TimeSpec {
pub sec: i64, // Seconds since Unix epoch
pub nsec: i64, // Nanoseconds
}
```
- `to_date_time()` → `DateTime<Local>` — converts to local time
- `to_utc_date_time()` → `DateTime<Utc>` — converts to UTC
---
## Parameter Tree
The `ParameterTree` provides lookup methods for parameter metadata after calling `request_parameter_tree()`.
| `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
| `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
| `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:
| `&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:
```rust
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:
```rust
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:
```rust
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
```rust
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
```rust
// 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
```rust
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 `&Request` or `&Subscribe` across threads concurrently
This is by design. The Req/Rep protocol requires strict send→receive ordering, which cannot be guaranteed with concurrent callers.
```rust
// ✅ Move to another thread
let mut request = Request::new();
request.connect("wss://127.0.0.1:5568", opts)?;
});
// ✅ Each thread creates its own
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;
});
```
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](LICENSE).