zendo-protocol 0.1.3

Wire-protocol constants and binary frame decoders for the Zendo motion-tracking WebSocket stream.
Documentation
//! Value types and the joint / landmark vocabularies.

use crate::constants::{HAND_SIDE_LEFT, HAND_SIDE_RIGHT};

/// A unit quaternion describing a joint orientation: `w + xi + yj + zk`.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Quaternion {
    pub w: f64,
    pub x: f64,
    pub y: f64,
    pub z: f64,
}

/// A 3D landmark position with a detection confidence in `[0, 1]`.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Landmark {
    pub x: f64,
    pub y: f64,
    pub z: f64,
    pub confidence: f64,
}

/// Which hand a hand frame describes.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum HandSide {
    Right,
    Left,
}

impl HandSide {
    /// Decodes the side byte that prefixes hand-frame payloads.
    pub const fn from_byte(byte: u8) -> Option<Self> {
        match byte {
            HAND_SIDE_RIGHT => Some(Self::Right),
            HAND_SIDE_LEFT => Some(Self::Left),
            _ => None,
        }
    }

    /// The side byte for this hand.
    pub const fn as_byte(self) -> u8 {
        match self {
            Self::Right => HAND_SIDE_RIGHT,
            Self::Left => HAND_SIDE_LEFT,
        }
    }

    /// The lowercase name of this hand (`"right"` or `"left"`).
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Right => "right",
            Self::Left => "left",
        }
    }
}

/// Generates a fieldless enum whose variants carry a stable wire order and a
/// lowercase string label (the same labels Zendo writes to its CSV logs).
macro_rules! name_enum {
    (
        $(#[$attr:meta])*
        $name:ident { $( $variant:ident => $label:literal ),+ $(,)? }
    ) => {
        $(#[$attr])*
        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
        pub enum $name {
            $( $variant ),+
        }

        impl $name {
            /// Every variant, in wire order.
            pub const ALL: &'static [$name] = &[ $( $name::$variant ),+ ];

            /// The lowercase string label for this variant.
            pub const fn as_str(self) -> &'static str {
                match self {
                    $( $name::$variant => $label ),+
                }
            }

            /// This variant's position in wire order.
            pub const fn index(self) -> usize {
                self as usize
            }

            /// The variant at `index` in wire order, or `None` if out of range.
            pub const fn from_index(index: usize) -> Option<$name> {
                let all: &[$name] = $name::ALL;
                if index < all.len() {
                    Some(all[index])
                } else {
                    None
                }
            }
        }
    };
}

name_enum! {
    /// A body joint in a [`BodyQuaternionFrame`](crate::BodyQuaternionFrame).
    Joint {
        Hips => "hips",
        Spine => "spine",
        Neck => "neck",
        RightArm => "right_arm",
        RightForearm => "right_forearm",
        LeftArm => "left_arm",
        LeftForearm => "left_forearm",
        RightUpLeg => "right_upleg",
        RightLeg => "right_leg",
        RightFoot => "right_foot",
        LeftUpLeg => "left_upleg",
        LeftLeg => "left_leg",
        LeftFoot => "left_foot",
    }
}

name_enum! {
    /// A body landmark in a [`BodyLandmarkFrame`](crate::BodyLandmarkFrame).
    LandmarkName {
        SacroiliacJoint => "sacroiliac_joint",
        SuprasternalNotch => "suprasternal_notch",
        Nose => "nose",
        LeftEar => "left_ear",
        RightEar => "right_ear",
        LeftShoulder => "left_shoulder",
        RightShoulder => "right_shoulder",
        LeftElbow => "left_elbow",
        RightElbow => "right_elbow",
        LeftWrist => "left_wrist",
        RightWrist => "right_wrist",
        LeftHip => "left_hip",
        RightHip => "right_hip",
        LeftKnee => "left_knee",
        RightKnee => "right_knee",
        LeftAnkle => "left_ankle",
        RightAnkle => "right_ankle",
        LeftFootIndex => "left_foot_index",
        RightFootIndex => "right_foot_index",
    }
}

name_enum! {
    /// A hand joint in a [`HandQuaternionFrame`](crate::HandQuaternionFrame).
    HandJoint {
        Wrist => "wrist",
        ThumbMcp => "thumb_mcp",
        ThumbPip => "thumb_pip",
        ThumbDip => "thumb_dip",
        IndexMcp => "index_mcp",
        IndexPip => "index_pip",
        IndexDip => "index_dip",
        MiddleMcp => "middle_mcp",
        MiddlePip => "middle_pip",
        MiddleDip => "middle_dip",
        RingMcp => "ring_mcp",
        RingPip => "ring_pip",
        RingDip => "ring_dip",
        PinkyMcp => "pinky_mcp",
        PinkyPip => "pinky_pip",
        PinkyDip => "pinky_dip",
    }
}

name_enum! {
    /// A hand landmark in a [`HandLandmarkFrame`](crate::HandLandmarkFrame).
    HandLandmarkName {
        Wrist => "wrist",
        ThumbCmc => "thumb_cmc",
        ThumbMcp => "thumb_mcp",
        ThumbIp => "thumb_ip",
        ThumbTip => "thumb_tip",
        IndexMcp => "index_mcp",
        IndexPip => "index_pip",
        IndexDip => "index_dip",
        IndexTip => "index_tip",
        MiddleMcp => "middle_mcp",
        MiddlePip => "middle_pip",
        MiddleDip => "middle_dip",
        MiddleTip => "middle_tip",
        RingMcp => "ring_mcp",
        RingPip => "ring_pip",
        RingDip => "ring_dip",
        RingTip => "ring_tip",
        PinkyMcp => "pinky_mcp",
        PinkyPip => "pinky_pip",
        PinkyDip => "pinky_dip",
        PinkyTip => "pinky_tip",
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::constants::{
        BODY_JOINT_COUNT, BODY_LANDMARK_COUNT, HAND_JOINT_COUNT, HAND_LANDMARK_COUNT,
    };

    #[test]
    fn enum_lengths_match_protocol_counts() {
        // Arrange / Act / Assert
        assert_eq!(Joint::ALL.len(), BODY_JOINT_COUNT);
        assert_eq!(LandmarkName::ALL.len(), BODY_LANDMARK_COUNT);
        assert_eq!(HandJoint::ALL.len(), HAND_JOINT_COUNT);
        assert_eq!(HandLandmarkName::ALL.len(), HAND_LANDMARK_COUNT);
    }

    #[test]
    fn index_round_trips_through_from_index() {
        // Arrange
        for (i, joint) in Joint::ALL.iter().enumerate() {
            // Act
            let recovered = Joint::from_index(i);

            // Assert
            assert_eq!(recovered, Some(*joint));
            assert_eq!(joint.index(), i);
        }
    }

    #[test]
    fn from_index_rejects_out_of_range() {
        // Arrange / Act / Assert
        assert_eq!(Joint::from_index(BODY_JOINT_COUNT), None);
        assert_eq!(HandLandmarkName::from_index(usize::MAX), None);
    }

    #[test]
    fn hand_side_byte_round_trips() {
        // Arrange / Act / Assert
        assert_eq!(
            HandSide::from_byte(HandSide::Right.as_byte()),
            Some(HandSide::Right)
        );
        assert_eq!(
            HandSide::from_byte(HandSide::Left.as_byte()),
            Some(HandSide::Left)
        );
        assert_eq!(HandSide::from_byte(2), None);
    }

    #[test]
    fn labels_are_stable() {
        // Arrange / Act / Assert
        assert_eq!(Joint::Hips.as_str(), "hips");
        assert_eq!(Joint::LeftFoot.as_str(), "left_foot");
        assert_eq!(LandmarkName::SacroiliacJoint.as_str(), "sacroiliac_joint");
        assert_eq!(HandJoint::PinkyDip.as_str(), "pinky_dip");
        assert_eq!(HandLandmarkName::ThumbTip.as_str(), "thumb_tip");
    }
}