Skip to main content

leash_harness/
types.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{accelerator::AcceleratorStatus, capability::CapabilityDescriptor, module::ModuleInfo};
7
8#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
9#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
10#[serde(rename_all = "lowercase")]
11pub enum SpeedMode {
12    Low,
13    #[default]
14    Medium,
15    High,
16}
17
18impl SpeedMode {
19    pub fn cap(self) -> f64 {
20        match self {
21            Self::Low => 0.22,
22            Self::Medium => 0.35,
23            Self::High => 1.0,
24        }
25    }
26}
27
28pub const VISUALIZATION_FRAME_VERSION: &str = "leash-visualization-v1";
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
32pub struct Health {
33    pub ok: bool,
34    pub mode: String,
35    pub replay: bool,
36    pub role: String,
37    pub profile: String,
38    pub uptime_ms: u128,
39    pub estop: bool,
40    pub deadman_ok: bool,
41    pub physical_actuation_enabled: bool,
42    pub accelerator: AcceleratorStatus,
43    pub modules: Vec<ModuleInfo>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
48pub struct Capabilities {
49    pub ok: bool,
50    pub mode: String,
51    pub replay: bool,
52    pub role: String,
53    pub profile: String,
54    pub physical: bool,
55    pub stream_transport: String,
56    pub endpoints: Vec<String>,
57    pub mcp_tools: Vec<String>,
58    pub speed_modes: Vec<SpeedMode>,
59    pub accelerator: AcceleratorStatus,
60    pub modules: Vec<ModuleInfo>,
61    pub capabilities: Vec<CapabilityDescriptor>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
66pub struct TelemetryFrame {
67    pub ts_ms: u128,
68    pub robot: String,
69    pub profile: String,
70    pub battery_v: Option<f64>,
71    pub left_cmd: f64,
72    pub right_cmd: f64,
73    pub odometry_left: Option<f64>,
74    pub odometry_right: Option<f64>,
75    pub session_id: Option<String>,
76    pub deadman_ok: bool,
77    pub estop: bool,
78    pub stopped_by_deadman: bool,
79    pub soft_odometry_limited: bool,
80    pub soft_odometry_limit_m: f64,
81    pub speed_mode: SpeedMode,
82    pub max_speed: f64,
83    pub sensors: SensorSnapshot,
84    pub resource: Option<ResourceSample>,
85    pub source: String,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
90pub struct ResourceSample {
91    pub sampled_at_ms: u128,
92    pub process_id: u32,
93    pub cpu_time_ticks: Option<u64>,
94    pub memory_rss_bytes: Option<u64>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
99pub struct RunLogEntry {
100    pub timestamp: u128,
101    pub run_id: String,
102    pub module: String,
103    pub event: String,
104    pub level: String,
105    pub fields: BTreeMap<String, Value>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
109#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
110pub struct AgentMessage {
111    pub id: u64,
112    pub ts_ms: u128,
113    pub source: String,
114    pub text: String,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
119pub struct AgentMessageAck {
120    pub ok: bool,
121    pub message: AgentMessage,
122    pub response: Option<AgentModelResponse>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
127pub struct AgentMessageList {
128    pub ok: bool,
129    pub messages: Vec<AgentMessage>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
134pub struct AgentModelResponse {
135    pub ok: bool,
136    pub provider: String,
137    pub model: String,
138    pub prompt: String,
139    pub text: String,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
144pub struct TelemetryStreamFrame {
145    pub kind: String,
146    pub ts_ms: u128,
147    pub telemetry: TelemetryFrame,
148    pub health: Health,
149    pub command: CommandStreamState,
150    pub safety: SafetyStreamState,
151    #[serde(default)]
152    pub visualization: VisualizationFrame,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
157pub struct CommandStreamState {
158    pub left_cmd: f64,
159    pub right_cmd: f64,
160    pub session_id: Option<String>,
161    pub speed_mode: SpeedMode,
162    pub max_speed: f64,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
167pub struct SafetyStreamState {
168    pub estop: bool,
169    pub deadman_ok: bool,
170    pub stopped_by_deadman: bool,
171    pub soft_odometry_limited: bool,
172    pub soft_odometry_limit_m: f64,
173    pub physical_actuation_enabled: bool,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
177#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
178pub struct VisualizationFrame {
179    pub version: String,
180    pub ts_ms: u128,
181    pub robot: String,
182    pub profile: String,
183    pub pose: Pose2d,
184    pub path: VisualizationPath,
185    pub occupancy_grid: OccupancyGridFrame,
186    pub point_cloud: PointCloudMetadata,
187    pub detections: Vec<DetectionFrame>,
188    pub command: CommandOverlay,
189}
190
191impl Default for VisualizationFrame {
192    fn default() -> Self {
193        Self {
194            version: VISUALIZATION_FRAME_VERSION.to_string(),
195            ts_ms: 0,
196            robot: String::new(),
197            profile: String::new(),
198            pose: Pose2d::default(),
199            path: VisualizationPath::default(),
200            occupancy_grid: OccupancyGridFrame::default(),
201            point_cloud: PointCloudMetadata::default(),
202            detections: Vec::new(),
203            command: CommandOverlay::default(),
204        }
205    }
206}
207
208#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
209#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
210pub struct Pose2d {
211    pub frame_id: String,
212    pub x_m: f64,
213    pub y_m: f64,
214    pub yaw_rad: f64,
215}
216
217#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
218#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
219pub struct VisualizationPath {
220    pub frame_id: String,
221    pub poses: Vec<Pose2d>,
222}
223
224#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
225#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
226pub struct OccupancyGridFrame {
227    pub frame_id: String,
228    pub width: u32,
229    pub height: u32,
230    pub resolution_m: f64,
231    pub origin: Pose2d,
232    pub cells: Vec<i8>,
233}
234
235#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
236#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
237pub struct PointCloudMetadata {
238    pub frame_id: String,
239    pub point_count: u32,
240    pub fields: Vec<String>,
241    pub source: String,
242}
243
244#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
245#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
246pub struct DetectionFrame {
247    pub frame_id: String,
248    pub id: String,
249    pub label: String,
250    pub confidence: f64,
251    pub x_m: f64,
252    pub y_m: f64,
253    pub width_m: f64,
254    pub height_m: f64,
255}
256
257#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
258#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
259pub struct CommandOverlay {
260    pub left_cmd: f64,
261    pub right_cmd: f64,
262    pub speed_mode: SpeedMode,
263    pub max_speed: f64,
264    pub estop: bool,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
268#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
269pub struct SensorSnapshot {
270    pub battery: BatteryStatus,
271    pub odometry: OdometryStatus,
272    pub camera: CameraStatus,
273    pub raw_frame: RawFrameStatus,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
277#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
278pub struct BatteryStatus {
279    pub status: String,
280    pub voltage_v: Option<f64>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
285pub struct OdometryStatus {
286    pub status: String,
287    pub left_m: Option<f64>,
288    pub right_m: Option<f64>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
292#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
293pub struct CameraStatus {
294    pub status: String,
295    pub health: String,
296    pub stream_url: Option<String>,
297    pub snapshot_url: Option<String>,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
301#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
302pub struct RawFrameStatus {
303    pub status: String,
304    pub source: String,
305    pub last_ms: Option<u128>,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
309#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
310pub struct CaptureResult {
311    pub ok: bool,
312    pub source: String,
313    pub content_type: String,
314    pub byte_len: usize,
315    pub captured_at_ms: u128,
316    pub sha256: String,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
320#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
321pub struct DriveOutcome {
322    pub ok: bool,
323    pub left: f64,
324    pub right: f64,
325    pub speed_mode: SpeedMode,
326    pub max_speed: f64,
327    pub stopped_by_deadman: bool,
328    pub soft_odometry_limited: bool,
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn visualization_frame_round_trips_as_versioned_json() {
337        let frame = VisualizationFrame {
338            version: VISUALIZATION_FRAME_VERSION.to_string(),
339            ts_ms: 42,
340            robot: "robot".to_string(),
341            profile: "sim".to_string(),
342            pose: Pose2d {
343                frame_id: "map".to_string(),
344                x_m: 1.0,
345                y_m: 2.0,
346                yaw_rad: 0.5,
347            },
348            path: VisualizationPath {
349                frame_id: "map".to_string(),
350                poses: vec![Pose2d {
351                    frame_id: "map".to_string(),
352                    x_m: 1.0,
353                    y_m: 2.0,
354                    yaw_rad: 0.5,
355                }],
356            },
357            occupancy_grid: OccupancyGridFrame {
358                frame_id: "map".to_string(),
359                width: 2,
360                height: 2,
361                resolution_m: 0.25,
362                origin: Pose2d::default(),
363                cells: vec![0, 0, 50, 100],
364            },
365            point_cloud: PointCloudMetadata {
366                frame_id: "base_link".to_string(),
367                point_count: 0,
368                fields: vec!["x".to_string(), "y".to_string(), "z".to_string()],
369                source: "sim".to_string(),
370            },
371            detections: vec![DetectionFrame {
372                frame_id: "camera".to_string(),
373                id: "det-1".to_string(),
374                label: "fixture".to_string(),
375                confidence: 0.9,
376                x_m: 0.1,
377                y_m: 0.2,
378                width_m: 0.3,
379                height_m: 0.4,
380            }],
381            command: CommandOverlay {
382                left_cmd: 0.1,
383                right_cmd: 0.1,
384                speed_mode: SpeedMode::Low,
385                max_speed: SpeedMode::Low.cap(),
386                estop: false,
387            },
388        };
389
390        let value = serde_json::to_value(&frame).unwrap();
391        assert_eq!(value["version"], VISUALIZATION_FRAME_VERSION);
392        assert_eq!(value["pose"]["frame_id"], "map");
393        assert_eq!(
394            value["occupancy_grid"]["cells"].as_array().unwrap().len(),
395            4
396        );
397
398        let parsed: VisualizationFrame = serde_json::from_value(value).unwrap();
399        assert_eq!(parsed.version, VISUALIZATION_FRAME_VERSION);
400        assert_eq!(parsed.path.poses.len(), 1);
401        assert_eq!(parsed.detections[0].label, "fixture");
402    }
403}