motorcortex-rust 0.4.0

Motorcortex Rust: A library for Rust client applications to control Motorcortex Core.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# 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
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

```rust
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:

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

| 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:

```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)?;
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](LICENSE).