rover_nexus_core 0.2.0

Wire-format message types (Cap'n Proto + JSON) for communication between robotic vehicles and a robot orchestration server: Rover Nexus
Documentation
# Using rover_nexus_core

A library for Cap'n Proto message serialization between robots and fleet management servers.

## Add Dependency

```toml
# Cargo.toml
[dependencies]
rover_nexus_core = { path = "../rover_nexus_core" }  # or git URL
```

## Quick Start

```rust
use rover_nexus_core::core_model::*;
use rover_nexus_core::spatial_types::*; // GeoPose, LocalPose, Pose, SpatialData, …
use rover_nexus_core::capnp_messages::{
    serialize_robot_uplink_msg, deserialize_robot_uplink_msg,
    serialize_robot_command, deserialize_robot_command,
};
```

## Robot → Server (RobotUplinkMsg)

### Create and Send Telemetry

```rust
use rover_nexus_core::core_model::*;
use rover_nexus_core::spatial_types::*;
use rover_nexus_core::capnp_messages::serialize_robot_uplink_msg;

// Create global (GPS) motion telemetry
let motion = GlobalMotionTelemetry {
    unix_time_ms: 1_700_000_000_000, // milliseconds since Unix epoch
    pose: Some(GeoPose {
        lon_deg: -122.4194,
        lat_deg: 37.7749,
        altitude_m: 10.0,
        heading_deg: 45.0,
    }),
    velocity: Some(VelTwist {
        forward_mps: 1.5,
        angular_radps: 0.1,
    }),
    gps_fix: Some(GpsFixType::FixedRTK),
    gps_source: GpsSource::Real,
};

// Wrap in RobotUplinkMsg and serialize
let msg = RobotUplinkMsg::GlobalMotionTelemetry(motion);
let bytes: Vec<u8> = serialize_robot_uplink_msg(&msg).expect("serialization failed");

// Send `bytes` over the transport (SecureLink QUIC to the server, or Zenoh to
// the on-device agent), framed with a wire_envelope MessageClass byte.
```

Robots that report odometry in a local frame (rather than GPS) send
`LocalMotionTelemetry` (with a `LocalPose` and required `VelTwist`), wrapped as
`RobotUplinkMsg::LocalMotionTelemetry`.

### Create Status Telemetry

```rust
let status = StatusTelemetry {
    unix_time_ms: 1_700_000_000_000,
    battery: Some(BatteryStatus {
        soc_pct: 85.0, // state of charge, 0 to 100
        voltage_v: Some(48.2),
        current_a: Some(-2.5),
        charge_ah: None,
        capacity_ah: None,
        design_capacity_ah: None,
        temperature: Some(25.0),
        power_supply_status: PowerSupplyStatus::Discharging,
        power_supply_health: PowerSupplyHealth::Good,
        power_supply_technology: PowerSupplyTechnology::LiIon,
    }),
    fuel: None,
    range_remaining_m: Some(1200.0),
    runtime_remaining_s: Some(3600.0),
    mode: Some(RobotMode::Auto),
    estop: EStop {
        unix_time_ms: 1_700_000_000_000,
        active: false,
        ids: vec![],
    },
    faulted: false,
    accepting_missions: true,
    status: "Mowing zone A".to_string(),
};

let msg = RobotUplinkMsg::StatusTelemetry(status);
let bytes = serialize_robot_uplink_msg(&msg).unwrap();
```

### Receive RobotUplinkMsg (Server Side)

```rust
use rover_nexus_core::capnp_messages::deserialize_robot_uplink_msg;

fn handle_uplink(bytes: &[u8]) {
    match deserialize_robot_uplink_msg(bytes) {
        Ok(RobotUplinkMsg::GlobalMotionTelemetry(m)) => {
            println!("Robot at {:?}", m.pose);
        }
        Ok(RobotUplinkMsg::StatusTelemetry(s)) => {
            println!("Battery: {}%", s.battery.map(|b| b.soc_pct).unwrap_or(0.0));
        }
        Ok(RobotUplinkMsg::Fault(f)) => {
            println!("FAULT: {} - {}", f.fault_id, f.description);
        }
        Ok(other) => println!("Received: {:?}", other),
        Err(e) => eprintln!("Deserialize error: {}", e),
    }
}
```

## Server → Robot (RobotCommand)

### Send Commands

```rust
use rover_nexus_core::core_model::*;
use rover_nexus_core::spatial_types::*;
use rover_nexus_core::capnp_messages::serialize_robot_command;

// Velocity command (teleop)
let cmd = RobotCommand::VelocityCmd(VelTwist {
    forward_mps: 0.5,
    angular_radps: 0.2,
});
let bytes = serialize_robot_command(&cmd).unwrap();

// Change mode
let cmd = RobotCommand::SetRobotMode(RobotMode::Auto);

// Navigate to a location (Pose is WGS84 or local-frame)
let cmd = RobotCommand::NavigateTo(Pose::GeoPose(GeoPose {
    lon_deg: -122.418,
    lat_deg: 37.775,
    altitude_m: 0.0,
    heading_deg: 90.0,
}));
```

### Receive Commands (Robot Side)

```rust
use rover_nexus_core::capnp_messages::deserialize_robot_command;

fn handle_command(bytes: &[u8]) {
    match deserialize_robot_command(bytes) {
        Ok(RobotCommand::VelocityCmd(vel)) => {
            drive(vel.forward_mps, vel.angular_radps);
        }
        Ok(RobotCommand::SetRobotMode(mode)) => {
            set_robot_mode(mode);
        }
        Ok(RobotCommand::NavigateTo(target)) => {
            navigate_to(target);
        }
        Ok(cmd) => println!("Unhandled command: {:?}", cmd),
        Err(e) => eprintln!("Deserialize error: {}", e),
    }
}
```

### Wrapping a command with a UUID (NexusCommand)

The server wraps each `RobotCommand` in a `NexusCommand` carrying a per-command
UUID. The robot agent acks the UUID (see `RobotAck` in `internal_model`) and
forwards the inner `command` to the robot software.

```rust
use rover_nexus_core::core_model::{NexusCommand, RobotCommand, RobotMode};
use rover_nexus_core::capnp_messages::{serialize_nexus_command, deserialize_nexus_command};
use uuid::Uuid;

let nexus_cmd = NexusCommand {
    id: Uuid::new_v4(),
    command: RobotCommand::SetRobotMode(RobotMode::Auto),
};
let bytes = serialize_nexus_command(&nexus_cmd).unwrap();
let parsed = deserialize_nexus_command(&bytes).unwrap();
```

## JSON Serialization

All types derive `Serialize`/`Deserialize` for JSON:

```rust
use rover_nexus_core::core_model::*;

let motion = GlobalMotionTelemetry { /* ... */ };

// To JSON
let json = serde_json::to_string(&motion).unwrap();

// From JSON
let parsed: GlobalMotionTelemetry = serde_json::from_str(&json).unwrap();
```

Note: Wall-clock time fields are `i64` milliseconds since the Unix epoch and
serialize as `unixTimeMs` (camelCase). There is no nanosecond representation.
Enums and fields serialize in camelCase via `#[serde(rename_all = "camelCase")]`.

## Capabilities

For advertising robot capabilities, sensors, and settings:

```rust
use rover_nexus_core::core_model::*;

// Advertise custom services
let capabilities = Capabilities {
    services: vec![
        ServiceCapability {
            name: "start_mowing".into(),
            description: "Begin autonomous mowing routine".into(),
            capability_type: CapabilityType::Trigger,
            confirmation_message: Some("This will start the blades".into()),
            state_key: None,
            requires_role: Role::Operator,
            danger_level: DangerLevel::Warning,
        },
        ServiceCapability {
            name: "blade_toggle".into(),
            description: "Toggle blade on/off".into(),
            capability_type: CapabilityType::SetBool,
            confirmation_message: None,
            state_key: Some("blade_on".into()),
            requires_role: Role::Operator,
            danger_level: DangerLevel::Normal,
        },
    ],
    sensors: vec![
        SensorDescriptor {
            key: "blade_rpm".into(),
            label: "Blade Speed".into(),
            value_type: ValueType::Int,
            unit: Some("RPM".into()),
            range: Some(Range { min: 0.0, max: 5000.0 }),
            display_hint: DisplayHint::Gauge,
        },
    ],
    settings: vec![
        SettingDescriptor {
            key: "max_speed".into(),
            label: "Maximum Speed".into(),
            value_type: ValueType::Number,
            default: serde_json::json!(1.0),
            range: Some(Range { min: 0.0, max: 2.0 }),
            step: Some(0.1),
        },
    ],
    // Shared features/resources the robot can produce for missions & operations
    resource_producers: vec![
        ResourceProducerDescriptor {
            resource_producer_id: "coverage".into(),
            label: "Mowed Area".into(),
            description: Some("Area the robot has mowed".into()),
            layer_role: LayerRole::Coverage,
            geometry_type: GeometryType::MultiPolygon,
        },
    ],
};

// Send arbitrary key-value sensor telemetry
let extras = SensorTelemetry {
    unix_time_ms: 1_700_000_000_000,
    kv: vec![
        SensorReading {
            key: "blade_rpm".into(),
            value: Value::Int(3200),
            unix_time_ms: None,
        },
        SensorReading {
            key: "deck_lowered".into(),
            value: Value::Bool(true),
            unix_time_ms: None,
        },
    ],
};
```

## All RobotUplinkMsg Variants

```rust
pub enum RobotUplinkMsg {
    MissionRunStatus(MissionRunStatus),
    GlobalMotionTelemetry(GlobalMotionTelemetry),
    LocalMotionTelemetry(LocalMotionTelemetry),
    StatusTelemetry(StatusTelemetry),
    SensorTelemetry(SensorTelemetry),
    Capabilities(Capabilities),
    AllowedCommands(AllowedCommands),
    Fault(Fault),
    Feature(ReportedFeatureUpdate),
    Object(ObjectOp),
    SystemHealth(SystemHealth),
    UsageTelemetry(UsageTelemetry),
    ConsumableStatus(ConsumableStatus),
    CurrentSettings(Vec<SettingUpdate>),
    Message(Message),
    AgentInfo(AgentInfo),
    SpatialDirectiveStatus(SpatialDirectiveStatus),
}
```

## All RobotCommand Variants

```rust
pub enum RobotCommand {
    SetRobotMode(RobotMode),
    Pause,
    Resume,
    AssignMission(MissionCommand),
    ControlMissionRun(ControlMissionRun),
    InvokeService(ServiceCall),
    UpdateSettings(Vec<SettingUpdate>),
    UpdateFeature(FeatureOp),
    SpatialDirective(SpatialDirectiveOp),
    Object(ObjectOp),
    NavigateTo(Pose),
    VelocityCmd(VelTwist),
    TeleopJoy(TeleopJoy),
    AgentTextRequest(AgentTextRequest),
    SayText(SayTextRequest),
    MessageConfirmation(MessageConfirmation),
}
```