motorcortex-rust 0.5.0

Motorcortex Rust: a Rust client for the Motorcortex Core real-time control system (async + blocking).
Documentation
# Change Log

## [0.5.0] — 2026-04-20

First release of the async-first rewrite. Breaking change vs.
`0.4.0` — the entire public surface was rebuilt around an actor
pattern with two frontends (async + blocking). The pre-0.5 sync
prototype (`Request` / `Subscribe` directly at the crate root) is
gone; migrate via the `motorcortex_rust::core::*` (async) or
`motorcortex_rust::blocking::*` (sync façade) modules.

### Headline changes

- **Actor-style async core** — `core::Request` + `core::Subscribe`
  handles (`Clone + Send + Sync`) backed by dedicated driver
  threads. Multiple cloned handles multiplex onto one driver; no
  user-visible `Mutex` needed to serialise NNG req/rep ordering.
- **`core::Subscription` with three opt-in sinks** — sync
  `notify(cb)`, async `latest().await` (watch-backed, lossy),
  async `stream(capacity)` (broadcast-backed, lossless within the
  ring, explicit `Err(Missed(n))` on lag).
- **Blocking façade** at `motorcortex_rust::blocking::*` — same
  method names and semantics as the async core, just without
  `.await`. Each handle owns a hidden current-thread tokio runtime.
- **`ConnectionState`** (`Disconnected` / `Connected` /
  `ConnectionLost` / `SessionExpired`) published through
  `tokio::sync::watch` on both `Request` and `Subscribe`.
- **Session tokens + automatic reconnect** — driver periodically
  refreshes the server-issued session token on a live pipe, pauses
  while `ConnectionLost` / `SessionExpired`, and on reconnect
  calls `RestoreSession(token)` to resume without a re-login.
  `ConnectionOptions` gained `reconnect: bool` (default `true`),
  `reconnect_min` / `reconnect_max` (100 ms / 30 s defaults on the
  NNG dialer), `token_refresh_interval` (30 s default, matches
  python), and `max_reconnect_attempts: Option<u32>` safety net.
- **`Subscribe::resubscribe(&req)`** — re-registers every active
  subscription after a reconnect that lost server-side group
  state. Outstanding `Subscription` handles stay valid; the
  driver rebinds each one in place with the fresh server-assigned
  id.
- **`parse_url`** free function — splits Motorcortex's
  `scheme://host:req_port:sub_port` convention into two dial URLs.
  Handles hostnames, IPv4, and bracketed IPv6. `Request::connect_to`
  / `Subscribe::connect_to` convenience constructors on top.

### Error handling rules

- RPCs returning `Result<StatusCode>` never convert a non-OK server
  reply into `Err` — callers branch on the returned code. `Err`
  means transport / decode trouble. Applies uniformly to
  `login` / `logout` / `set_parameter` / `set_parameters` /
  `request_parameter_tree` / `remove_group` /
  `restore_session`. `remove_group` used to error on non-OK —
  that's the one behavioural change from `0.4.0` if you were
  catching `MotorcortexError::Status` on it.
- Fixed several panic paths on user-controllable input:
  `CString::new(cert_path)` and `CString::new(url)` with an
  embedded NUL byte now return `MotorcortexError::Connection`;
  `DataType::try_from` in the parameter-value dispatchers returns
  `V::default()` (decoder) / empty `Vec` (encoder) for unknown
  wire codes instead of `.unwrap()`-panicking.

### Dependencies

- Added `tokio` (features: `rt`, `rt-multi-thread`, `sync`,
  `time`, `macros`) and `futures` (for the `Stream` trait on
  `Subscription::stream`).

### Tests and tooling

- `cargo test --lib` — 84 tests including the pure state-machine
  helper for the reconnect handler.
- `cargo test --test unit` — 53 offline tests.
- `cargo test --test integration -- --test-threads=1` — 39 tests
  plus one `#[ignore]`d stress test
  (`stress_16_clones_hot_get_parameter`).
- Merged coverage (lib + unit + integration) on the release tip:
  **90.69 %** regions / **90.07 %** lines / **93.10 %** functions.
  Every `core/*` module and every `blocking/*` module clears the
  80-90 % phase-5 gate.
- Four runnable examples under `examples/`: `async_request`,
  `blocking_request`, `subscribe_latest`, `subscribe_stream`.
- `tests/server/main.cpp` gained the `MCX_TEST_SERVER_LIFETIME_SEC`
  watchdog so reconnect tests can simulate a crash without harness
  support.

### Removed

- Legacy sync `Request`, `Subscribe`, `Subscription`, and
  `ReadOnlySubscription` at the crate root — replaced by
  `core::*`.
- `Connection` trait — existed only to unify the deleted sync
  types.
- `API.md` — the hand-maintained reference doc for the old sync
  surface. Superseded by `README.md` (getting started + examples)
  + `ARCHITECTURE.md` (design + internals) + `cargo doc`.

### Housekeeping

- `CHANGE.md` renamed to `CHANGELOG.md`.
- Added a `LICENSE` file (MIT) matching the existing
  `license = "MIT"` in `Cargo.toml`.
- `Cargo.toml` gained `readme`, `documentation`, `keywords`,
  `categories`, and `rust-version = "1.85"` for crates.io /
  docs.rs discoverability.
- README picked up a "Running behind nginx" section covering the
  token-refresh-vs-proxy-timeout story.

---

## [0.4.0] and earlier — pre-rewrite synchronous surface

Re-architects the crate around an actor pattern with two usable
frontends (async and blocking). Async is the primary surface; the
legacy synchronous types are gone.

### Added

**`motorcortex_rust::core::*` — the async core.**

- `Request` handle (`Clone + Send + Sync`), backed by a dedicated
  `std::thread` driver that owns a `ConnectionManager`. Commands
  cross into the driver via `tokio::sync::mpsc::UnboundedSender`;
  replies come back on `tokio::sync::oneshot::Receiver`.
  Multiple cloned handles multiplex onto one driver, which
  enforces NNG's Req/Rep ordering in the type system without a
  user-visible `Mutex<…>`.
  - Methods: `new` / `default` / `clone` / `state()` /
    `parameter_tree()` / `connect` / `disconnect` / `connect_to` /
    `login` / `logout` / `request_parameter_tree` /
    `get_parameter` / `set_parameter` / `get_parameters` /
    `set_parameters` (tuple types) / `create_group` /
    `remove_group` / `get_parameter_tree_hash`.
- `Subscribe` handle (`Clone + Send + Sync`) with a second
  dedicated thread on connect that loops on `nng_recv` and
  dispatches payloads to the matching `Subscription::update`.
  Methods: `new` / `connect` / `disconnect` / `connect_to` /
  `subscribe(&Request, paths, alias, fdiv)` /
  `unsubscribe(&Request, Subscription)` /
  `resubscribe(&Request)` / `state()`.
- `Subscription` handle (`Clone + Send + Sync`) with three
  independent opt-in sinks:
  - `notify<F>(cb)` — sync fire-and-forget callback, runs on the
    receive thread, zero allocation.
  - `latest<V>().await` — `tokio::sync::watch` backed; resolves to
    the most recent payload, lossy by design.
  - `stream<V>(capacity)` — lazy `tokio::sync::broadcast`,
    returns `impl Stream<Item = StreamResult<V>>`. Bounded ring
    with explicit `Err(Missed(n))` back-pressure when the consumer
    lags. Not allocated unless someone actually calls it.
  - Plus sync `read<V>` / `read_all<V>` decoding the latest
    payload, `id` / `name` / `paths` / `fdiv` for descriptive
    metadata. `id` is atomic and `paths` reads through an internal
    `RwLock` so `Subscribe::resubscribe` can swap the server-side
    group descriptor in place without invalidating outstanding
    clones.
- `ConnectionState` enum (`Disconnected` / `Connected` /
  `ConnectionLost` / `SessionExpired`) published via
  `tokio::sync::watch`. Only Disconnected / Connected emitted in
  Phase 1–3; pipe-event-driven transitions wire up in Phase 4
  alongside session-token reconnect.
- Shared parameter-tree cache: `Request::parameter_tree()` returns
  an `Arc<RwLock<ParameterTree>>` shared with the driver. Cloned
  handles observe the same cache. Populated by
  `request_parameter_tree`, used client-side by
  `get_parameter` / `set_parameter` so RPCs stay a single channel
  hop.

**`motorcortex_rust::blocking::*` — sync façade over the async core.**

- `blocking::Request` / `Subscribe` / `Subscription` with the
  same method names as `core::*`, all sync (no `.await`). Each
  handle owns a single-threaded `tokio::runtime::Runtime` and
  forwards every call through `rt.block_on(self.inner.method())`.
- `Subscription::iter::<V>(capacity)` returns a blocking
  `Iterator` over the broadcast ring — same `Missed(n)` semantics
  as the async `stream()`.
- `as_async()` on each blocking handle exposes the inner async
  type for the hybrid case where one part of an app is blocking
  and another is async.

**Free helpers.**

- `motorcortex_rust::parse_url(&str) -> Result<(String, String)>`
  splits the Motorcortex dual-port convention
  (`scheme://host:req_port:sub_port`) into two dial URLs. Handles
  hostnames, IPv4, and bracketed IPv6. Mirrors the role of
  `motorcortex-python`'s `parseUrl` but with just one output
  flavour (two URLs, no path-based fallback).

**Tests and docs.**

- `tests/integration/blocking.rs` — end-to-end smoke tests for
  the sync façade (lifecycle, callback fires, blocking iter
  surfaces the value we wrote).
- `ARCHITECTURE.md` now documents the three-sink Subscription
  model and the full `stream()` data flow (fan-out, unfold, back-
  pressure ring semantics). Phase 4 reconnect design is baked in.
- `README.md` rewritten around the new public surface with four
  example snippets (async, subscribe, blocking, state observer).
- `TODO.md` tracks phase progress and the remaining Phase 4
  (reconnect + session tokens) + Phase 5 (polish) work.

### Changed

- **Async core replaces the legacy sync types** entirely — see
  *Removed* below.
- CI `unit-tests` job runs `cargo llvm-cov --lib` so
  `#[cfg(test)]` modules inside `src/` land in the merged
  coverage report.
- README pipeline / coverage badges temporarily point at
  `async-rewrite`; swap back once the rewrite merges.
- `TimeSpec` moved from `src/client/subscription.rs` to a
  dedicated `src/time_spec.rs`. Same semantics, same public path
  at the crate root (`motorcortex_rust::TimeSpec`).

### Removed

- Legacy sync `Request`, `Subscribe`, `Subscription`, and
  `ReadOnlySubscription` — replaced by `core::*`.
  ~1280 lines deleted from `src/client/`.
- `Connection` trait — it existed only to unify the two deleted
  sync types; nobody consumes it.
- `tests/integration/{connect,session,parameters,subscribe,tree}.rs`
  — every scenario already covered by the new
  `tests/integration/async_*.rs` suite against `core::*`.
- `tests/common/connection.rs` — `setup_connection` /
  `safe_disconnect` only existed to glue the legacy sync handle
  to the ctor-managed test_server; the async tests use
  `Request::connect_to(&url, opts).await` directly.
- `tests/integration/subscribe.rs::test_subscribe_read_all` —
  had been `#[ignore]`d indefinitely because its
  `elapsed_time <= 1000 ms` assertion bracketed a deliberate
  `sleep(1000 ms)` and could never pass. Fresh coverage of the
  flatten-everything API lands against `core::Subscription`
  against the new stack.

### Fixed (kept from `development`, carried into this branch)

- Same `Request::get_parameter_tree_hash` reply-type fix from
  `development` is baked into the new `core::Request`
  implementation — `GetParameterTreeHashMsg` is decoded as
  `ParameterTreeHashMsg` from day one.

### Tests — running tally on branch tip

- `cargo test --lib` — **60** (up from 2 on `development`). Of
  those, 48 are async / core::* tests; 12 cover `parse_url`.
- `cargo test --test unit` — **53** (down from 80, since the
  redundant legacy `request_error_paths` and `subscription`
  modules were replaced by lib tests inside `src/core/{proto,
  request,subscription}.rs`).
- `cargo test --test integration -- --test-threads=1` — **23**,
  **zero ignored** (up from 10 tests + 1 ignored on
  `development`).
- `cargo test --doc` — 3 (new).
- Merged coverage on branch tip: **94.95 %** regions, **94.83 %**
  lines, **96.57 %** functions. Ahead of `development`'s 93.99 %.

---

## Unreleased

### Added
- Split test suite into `tests/unit.rs` (offline) and `tests/integration/` (server-driven), mirroring the motorcortex-python layout.
- GitLab CI pipeline (`.gitlab-ci.yml`) with `build` / `test` / `coverage` / `release` stages; coverage collected via `cargo-llvm-cov` and published as Cobertura.
- `tests/README.md` with the testing walkthrough, layout diagram, and a *Known gaps* section.
- README badges: pipeline status, coverage report, crates.io version, Rust edition, MIT license.
- README *CI runner setup* section documenting the prerequisites needed to provision a GitLab runner for the pipeline.
- Committed TLS test certs in the repo (`tests/server/config/motorcortex.pem` + `tests/mcx.cert.crt`, Root CA). Integration tests resolve the CA via `CARGO_MANIFEST_DIR` instead of relying on `/etc/ssl/certs/mcx.cert.pem` on the host — mirrors motorcortex-python and lets CI run without runner-side cert setup.
- Expanded `tests/unit.rs` (2 → 80 offline tests) mirroring motorcortex-python's layout. New modules: `version`, `parameter_tree` (empty/populate/non-OK/unknown-path/iterator filter), `primitive_types` (wire-level `SetParameterValue`/`GetParameterValue` round-trips for every scalar dtype + arrays/vecs), `message_types` (proto round-trips for `LoginMsg`, `StatusMsg`, `ParameterTreeMsg`), `status_code` (`Ok = 0` invariant + `as_str_name`/`from_str_name` round-trip over every variant), `init_threads` (smoke tests for the NNG init wrapper), `logger` (`LogLevel` variants + init entry points), `timespec` (field access, `from_buffer` layout + short-input guard, chrono `DateTime` round-trips), `error_display` (every `MotorcortexError` variant + `From<prost::DecodeError>`), `set_parameter_value_coverage` / `get_parameter_value_coverage` (exhaustive per-impl method sweeps over scalars / `[T; 2]` / `Vec<T>`), `parameters_coverage` (all four `Parameters::into_vec` impls), `subscription` (protocol-1 buffer decode via hand-crafted fixtures — single scalar, two-param tuple, array parameter; protocol-0 / unknown-version bail-outs; callback invocation via `update`; full `ReadOnlySubscription` delegation), `request_error_paths` (`decode_message` hash/size/body errors, every `Request` method without `connect()` hits `Socket is not available`, empty-tree `ParameterNotFound`). Also added `#[cfg(test)] mod tests` inside `parameter_value/processing.rs` for crate-private `encode_parameter_value`/`decode_parameter_value` fallthrough coverage (`Bytes`/`Char`/`UserType`/`Undefined`); CI's `unit-tests` job now runs `cargo llvm-cov --lib` alongside `--test unit` so those lib tests land in the merged report. Two integration tests added in `tree.rs` (`test_get_parameter_tree_hash`, `test_remove_nonexistent_group_returns_error_status`). Region coverage climbs from ~0.5% to ~94%.

### Changed
- README test snippet now documents the two-binary workflow (`--test unit` + `--test integration -- --test-threads=1`).
- CI now installs rustup + the stable toolchain into `$CI_PROJECT_DIR/.rustup` via a shared `.rust-setup` `before_script`, removing the need for a runner-side Rust install. Toolchain and `cargo-llvm-cov` binary are cached alongside the cargo registry.
- Re-exported `TimeSpec` from the crate root (previously reachable only as `client::subscription::TimeSpec`, which the crate never made `pub`). It already leaked through `Subscription::read` return types, so surfacing it is a no-op for downstream code and unblocks writing unit tests against it.

### Fixed
- `Request::get_parameter_tree_hash` now decodes the reply as `ParameterTreeHashMsg` (it was decoding `ParameterTreeMsg`, whose hash doesn't match the server's reply — every call errored with `Invalid message hash`). Surfaced by the new `test_get_parameter_tree_hash` integration test.

### Removed
- `tests/integration/subscribe.rs::test_subscribe_read_all` — the
  only integration test covering `Subscription::read_all::<V>`. Its
  timing assertion was broken by construction (elapsed measured
  across an explicit 1 s sleep + asserted `≤ 1000 ms`), so the test
  had been `#[ignore]`d indefinitely. Fresh coverage of the
  flatten-everything API will land against `core::Subscription::stream`
  in Phase 2 instead of reviving the legacy test.

### Known issues
- Integration tests share `root/Control/dummy*` parameters on a single server instance, so `--test-threads=1` is mandatory.

## 4.0

### Added
- `Send` implementation for `request` and `subscribe`.
- Errors infrastructure (#2417309).
- API documentation (#e925fb9).
- Interface returning an iterator for the parameter tree (#ad68262).
- Number of threads initialization (#fc6c2e5).

### Fixed
- Improved possible resource leak (#c30d250).
- Fixed IP issues (#f1ccf89).
- Decoding bug fix (#922d409).
- Fixed mbedtls resolution to use the local version (#c8e30a3).

### Changed
- Moved socket creation from `new` to `connect` (#8efa9bb).
- Updated documentation for `request` and `subscribe` relationship (#93444c5).
- Versions updated multiple times (#fef6b30, #fcffaf0, #b18fd0c).
- Refactored large sections of codebase (#fd59a31, #d0caf45).

### Removed
- Vendor and lock files (#d84583c).