zendo-protocol 0.2.0

Wire-protocol constants and binary frame decoders for the Zendo motion-tracking WebSocket stream.
Documentation
# zendo-protocol

[![crates.io](https://img.shields.io/crates/v/zendo-protocol.svg)](https://crates.io/crates/zendo-protocol)
[![docs.rs](https://docs.rs/zendo-protocol/badge.svg)](https://docs.rs/zendo-protocol)

The wire-protocol definitions for the [Zendo](https://github.com/akina-health/zendo-sdk)
motion-tracking WebSocket stream: port range, message-type tags, the joint and
landmark vocabularies, and the pure decode/encode routines.

This crate is the single source of truth for the protocol. It has **no
dependencies**, performs **no I/O**, **never allocates**, and is `no_std` by
disabling the default `std` feature. Most users want
[`zendo-sdk`](https://crates.io/crates/zendo-sdk) (Rust) or the
[`zendo-sdk`](https://pypi.org/project/zendo-sdk/) PyPI package (Python), which
build a connection on top of this crate. Depend on `zendo-protocol` directly
only if you bring your own transport.

## Frame layout

Every frame is one binary WebSocket message: byte 0 is the type tag, the rest is
the payload. All numeric values are little-endian `f64`.

| Message | Tag | Payload |
|---|---|---|
| Hello            | `0x01` | protocol version (`u16` LE) |
| Body quaternions | `0x02` | 13 joints × (w, x, y, z) |
| Body landmarks   | `0x03` | 19 landmarks × (x, y, z, confidence) |
| Hand quaternions | `0x04` | 1 side byte + 16 joints × (w, x, y, z) |
| Hand landmarks   | `0x05` | 1 side byte + 21 landmarks × (x, y, z, confidence) |
| Body ISB angles  | `0x06` | 21 scalar joint angles (radians) |

The server sends the hello frame first on every connection, carrying
`PROTOCOL_VERSION`; clients require an exact match. The side byte is `0` for the
right hand and `1` for the left. Body messages stream in body-tracking mode;
hand messages stream in hand-tracking mode; the two never appear in the same
session. The shoulder `plane_of_elevation` angles in a body ISB-angles frame are
`NaN` when the shoulder is near neutral (gimbal singularity).

## Example

```rust
use zendo_protocol::{decode, Message};

fn handle(frame: &[u8]) {
    match decode(frame) {
        Ok(Message::BodyQuaternions(q)) => println!("hips: {:?}", q.hips),
        Ok(Message::BodyLandmarks(l)) => println!("nose: {:?}", l.nose),
        Ok(Message::HandQuaternions { side, frame }) => {
            println!("{} wrist: {:?}", side.as_str(), frame.wrist)
        }
        Ok(Message::HandLandmarks { side, frame }) => {
            println!("{} thumb tip: {:?}", side.as_str(), frame.thumb_tip)
        }
        Ok(Message::BodyIsbAngles(frame)) => {
            println!("right elbow flexion: {}", frame.right_elbow_flexion)
        }
        Err(e) => eprintln!("bad frame: {e}"),
    }
}
```

## License

Licensed under either of [Apache-2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT) at
your option.