API

This chapter provides information about the motorcortex-rust API.

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 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)
  • 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 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

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 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().

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 &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.

// ✅ 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.