haply 1.0.0

Haply Robotics Client Library for the Inverse Service
Documentation
//! Outgoing command types, configure types, and the service message structure.

use serde::{Deserialize, Serialize};
use ts_rs::TS;

use super::base_types::*;
use super::devices::CoordinateSystem;
use super::navigation::NavigationConfigure;
use super::sdf::{SdfCommand, SdfHfxObject, SdfOutputConfigure};

// ============================================================
// Convenience input types for public API
// ============================================================

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct ForceInput {
    pub device_id: String,
    pub forces: Force,
}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct PositionInput {
    pub device_id: String,
    pub positions: Linear3D,
}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct ExtensionDataInput {
    pub device_id: String,
    pub extension_data: Vec<i32>,
}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct AngularTorqueInput {
    pub device_id: String,
    pub torques: Angular3D,
}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct AngularPositionInput {
    pub device_id: String,
    pub angles: Angular3D,
}

// ============================================================
// Transform with optional sub-fields for outgoing partial updates
// ============================================================

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct TransformPatch {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub position: Option<Linear3D>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rotation: Option<Orientation4D>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scale: Option<Linear3D>,
}

// ============================================================
// Individual command payloads (3.5 field names)
// ============================================================

fn default_true() -> bool {
    true
}

#[derive(Copy, Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct SetCursorForceCmd {
    pub vector: Force,
    #[serde(default = "default_true")]
    pub execute: bool,
}

#[derive(Copy, Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct SetCursorPositionCmd {
    pub position: Linear3D,
    #[serde(default = "default_true")]
    pub execute: bool,
}

#[derive(Copy, Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct SetAngularTorquesCmd {
    pub torques: Angular3D,
    #[serde(default = "default_true")]
    pub execute: bool,
}

#[derive(Copy, Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct SetAngularPositionCmd {
    pub angles: Angular3D,
    #[serde(default = "default_true")]
    pub execute: bool,
}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct SetTransformCmd {
    pub transform: TransformPatch,
    #[serde(default = "default_true")]
    pub execute: bool,
}

#[derive(Copy, Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct ProbeCmd {}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct SetExtensionDataCmd {
    pub extension_data: Vec<i32>,
}

// ============================================================
// Per-tick command structs (all optional, multiple per device per frame)
// ============================================================

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct Inverse3Commands {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub set_cursor_force: Option<SetCursorForceCmd>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub set_cursor_position: Option<SetCursorPositionCmd>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub set_angular_torques: Option<SetAngularTorquesCmd>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub set_angular_position: Option<SetAngularPositionCmd>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub set_transform: Option<SetTransformCmd>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub probe_position: Option<ProbeCmd>,
    // SDF HFX command (per-tick, not repeated by zero-order hold)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sdf: Option<SdfCommand>,
}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct VerseGripCommands {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub set_transform: Option<SetTransformCmd>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub probe_orientation: Option<ProbeCmd>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub set_extension_data: Option<SetExtensionDataCmd>,
}

// ============================================================
// One-shot configure structs
// ============================================================

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct PresetConfig {
    pub preset: String,
}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct MountConfig {
    pub transform: TransformPatch,
}

#[derive(Copy, Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct DampingConfig {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scalar: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vector: Option<Linear3D>,
}

#[derive(Copy, Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct ForceGateConfig {
    pub gain: f32,
}

// Navigation configure types are in super::navigation, re-exported via mod.rs

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct ProfileConfig {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub required_version: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct Inverse3Configure {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub preset: Option<PresetConfig>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub basis: Option<CoordinateSystem>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mount: Option<MountConfig>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub damping: Option<DampingConfig>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub force_gate: Option<ForceGateConfig>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub navigation: Option<NavigationConfigure>,
    // SDF output toggle
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sdf: Option<SdfOutputConfigure>,
}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct VerseGripConfigure {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub preset: Option<PresetConfig>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub basis: Option<CoordinateSystem>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mount: Option<MountConfig>,
}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct SessionConfigure {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub profile: Option<ProfileConfig>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub basis: Option<CoordinateSystem>,
}

// ============================================================
// Outgoing device message structs
// ============================================================

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct Inverse3OutMsg {
    pub device_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub commands: Option<Inverse3Commands>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub configure: Option<Inverse3Configure>,
}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct VerseGripOutMsg {
    pub device_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub commands: Option<VerseGripCommands>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub configure: Option<VerseGripConfigure>,
}

// ============================================================
// Session message
// ============================================================

#[derive(Copy, Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct ForceRenderFullStateMsg {}

#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct SessionMsg {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub force_render_full_state: Option<ForceRenderFullStateMsg>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub configure: Option<SessionConfigure>,
}

// ============================================================
// Full outgoing service message
// ============================================================

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, TS)]
pub struct ServiceMsg {
    pub session: SessionMsg,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub inverse3: Vec<Inverse3OutMsg>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub verse_grip: Vec<VerseGripOutMsg>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub wireless_verse_grip: Vec<VerseGripOutMsg>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub custom_verse_grip: Vec<VerseGripOutMsg>,
}

impl Default for ServiceMsg {
    fn default() -> Self {
        ServiceMsg {
            session: SessionMsg {
                force_render_full_state: Some(ForceRenderFullStateMsg {}),
                configure: None,
            },
            inverse3: Vec::new(),
            verse_grip: Vec::new(),
            wireless_verse_grip: Vec::new(),
            custom_verse_grip: Vec::new(),
        }
    }
}

// ============================================================
// High-level command enum for building outgoing frames
// ============================================================

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, TS)]
pub enum Command {
    // Inverse3 per-tick commands
    SetCursorForce { device_id: String, vector: Force, execute: bool },
    SetCursorPosition { device_id: String, position: Linear3D, execute: bool },
    SetAngularTorques { device_id: String, torques: Angular3D, execute: bool },
    SetAngularPosition { device_id: String, angles: Angular3D, execute: bool },
    SetI3Transform { device_id: String, transform: TransformPatch, execute: bool },
    ProbePosition { device_id: String },
    // VerseGrip per-tick commands
    ProbeOrientation { device_id: String },
    SetExtensionData { device_id: String, extension_data: Vec<i32> },
    SetVgTransform { device_id: String, transform: TransformPatch, execute: bool },
    // SDF HFX commands
    SdfSet { device_id: String, objects: Vec<SdfHfxObject>, from_space: Option<String> },
    SdfUpdate { device_id: String, objects: Vec<SdfHfxObject> },
    SdfRemove { device_id: String, ids: Vec<String> },
    // Configure (one-shot)
    ConfigureInverse3 { device_id: String, config: Inverse3Configure },
    ConfigureVerseGrip { device_id: String, config: VerseGripConfigure },
    ConfigureSession(SessionConfigure),
    ConfigureSdfOutput { device_id: String, state_output: bool },
    // Session-level
    ForceRenderFullState,
    PingService,
}