1use std::fmt;
4
5use crate::identity::{ComponentInstanceId, JointId, LinkId};
6
7#[derive(
14 serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash,
15)]
16#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
17#[serde(rename_all = "snake_case")]
18pub enum CapabilityRole {
19 Localization,
20 Mapping,
21 Traversability,
22 Odometry,
23 Perception,
24 Safety,
25}
26
27impl CapabilityRole {
28 #[must_use]
29 pub const fn as_str(self) -> &'static str {
30 match self {
31 Self::Localization => "localization",
32 Self::Mapping => "mapping",
33 Self::Traversability => "traversability",
34 Self::Odometry => "odometry",
35 Self::Perception => "perception",
36 Self::Safety => "safety",
37 }
38 }
39}
40
41impl fmt::Display for CapabilityRole {
42 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43 formatter.write_str(self.as_str())
44 }
45}
46
47#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
48#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
49pub enum Capability {
50 Motor(Motor),
51 Encoder(Encoder),
52 Accelerometer(Accelerometer),
53 Gyroscope(Gyroscope),
54 Magnetometer(Magnetometer),
55 Imu(Imu),
56 Gnss(Gnss),
57 Camera(Camera),
58 Depth(Depth),
59 EmergencyStop(EmergencyStop),
60 Range(Range),
61 Lidar(Lidar),
62 Mmwave(Mmwave),
63 Microphone(Microphone),
64 Speaker(Speaker),
65 Battery(Battery),
66 Led(Led),
67}
68
69#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
70#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
71#[serde(rename_all = "snake_case")]
72pub enum EncoderType {
73 Incremental,
74 Absolute,
75}
76
77#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
78#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
79#[serde(rename_all = "snake_case")]
80pub enum MotorCommand {
81 Position,
82 Velocity,
83 Torque,
84}
85
86#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
88#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
89pub enum StructuralTarget {
90 Joint { id: JointId },
91 Link { id: LinkId },
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum StructuralKind {
97 Link,
98 Joint,
99}
100
101impl fmt::Display for StructuralKind {
102 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103 formatter.write_str(match self {
104 Self::Link => "link",
105 Self::Joint => "joint",
106 })
107 }
108}
109
110impl StructuralTarget {
111 #[must_use]
113 pub const fn kind(&self) -> StructuralKind {
114 match self {
115 Self::Joint { .. } => StructuralKind::Joint,
116 Self::Link { .. } => StructuralKind::Link,
117 }
118 }
119
120 #[must_use]
123 pub fn namespaced(&self, component_id: &ComponentInstanceId) -> Self {
124 match self {
125 Self::Joint { id } => Self::Joint {
126 id: id.namespaced(component_id),
127 },
128 Self::Link { id } => Self::Link {
129 id: id.namespaced(component_id),
130 },
131 }
132 }
133}
134
135#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
136#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
137#[serde(rename_all = "snake_case")]
138pub enum LidarOutput {
139 Ranges,
140 Points,
141}
142
143#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
144#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
145#[serde(rename_all = "snake_case")]
146pub enum CameraMode {
147 Mono,
148 Rgb,
149}
150
151#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
152#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
153#[serde(rename_all = "snake_case")]
154pub enum GnssCoordinateSystem {
155 #[default]
156 Local,
157 Wgs84,
158}
159
160#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
166pub enum CapabilityKind {
167 Motor,
168 Encoder,
169 Accelerometer,
170 Gyroscope,
171 Magnetometer,
172 Imu,
173 Gnss,
174 Camera,
175 Depth,
176 EmergencyStop,
177 Range,
178 Lidar,
179 Mmwave,
180 Microphone,
181 Speaker,
182 Battery,
183 Led,
184}
185
186impl CapabilityKind {
187 #[must_use]
189 pub const fn as_str(self) -> &'static str {
190 match self {
191 Self::Motor => "motor",
192 Self::Encoder => "encoder",
193 Self::Accelerometer => "accelerometer",
194 Self::Gyroscope => "gyroscope",
195 Self::Magnetometer => "magnetometer",
196 Self::Imu => "imu",
197 Self::Gnss => "gnss",
198 Self::Camera => "camera",
199 Self::Depth => "depth",
200 Self::EmergencyStop => "emergency_stop",
201 Self::Range => "range",
202 Self::Lidar => "lidar",
203 Self::Mmwave => "mmwave",
204 Self::Microphone => "microphone",
205 Self::Speaker => "speaker",
206 Self::Battery => "battery",
207 Self::Led => "led",
208 }
209 }
210}
211
212impl fmt::Display for CapabilityKind {
213 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214 formatter.write_str(self.as_str())
215 }
216}
217
218impl Capability {
219 #[must_use]
221 pub const fn kind(&self) -> CapabilityKind {
222 match self {
223 Self::Motor(_) => CapabilityKind::Motor,
224 Self::Encoder(_) => CapabilityKind::Encoder,
225 Self::Accelerometer(_) => CapabilityKind::Accelerometer,
226 Self::Gyroscope(_) => CapabilityKind::Gyroscope,
227 Self::Magnetometer(_) => CapabilityKind::Magnetometer,
228 Self::Imu(_) => CapabilityKind::Imu,
229 Self::Gnss(_) => CapabilityKind::Gnss,
230 Self::Camera(_) => CapabilityKind::Camera,
231 Self::Depth(_) => CapabilityKind::Depth,
232 Self::EmergencyStop(_) => CapabilityKind::EmergencyStop,
233 Self::Range(_) => CapabilityKind::Range,
234 Self::Lidar(_) => CapabilityKind::Lidar,
235 Self::Mmwave(_) => CapabilityKind::Mmwave,
236 Self::Microphone(_) => CapabilityKind::Microphone,
237 Self::Speaker(_) => CapabilityKind::Speaker,
238 Self::Battery(_) => CapabilityKind::Battery,
239 Self::Led(_) => CapabilityKind::Led,
240 }
241 }
242
243 #[must_use]
245 pub const fn target(&self) -> &StructuralTarget {
246 match self {
247 Self::Motor(value) => &value.target,
248 Self::Encoder(value) => &value.target,
249 Self::Accelerometer(value) => &value.target,
250 Self::Gyroscope(value) => &value.target,
251 Self::Magnetometer(value) => &value.target,
252 Self::Imu(value) => &value.target,
253 Self::Gnss(value) => &value.target,
254 Self::Camera(value) => &value.target,
255 Self::Depth(value) => &value.target,
256 Self::EmergencyStop(value) => &value.target,
257 Self::Range(value) => &value.target,
258 Self::Lidar(value) => &value.target,
259 Self::Mmwave(value) => &value.target,
260 Self::Microphone(value) => &value.target,
261 Self::Speaker(value) => &value.target,
262 Self::Battery(value) => &value.target,
263 Self::Led(value) => &value.target,
264 }
265 }
266}
267
268#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
269#[serde(deny_unknown_fields)]
270pub struct Motor {
271 pub target: StructuralTarget,
272 pub command: MotorCommand,
273 pub gear_ratio: f64,
274 pub max_torque_nm: Option<f64>,
275 pub max_velocity_radps: Option<f64>,
276}
277
278#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
279#[serde(deny_unknown_fields)]
280pub struct Encoder {
281 pub target: StructuralTarget,
282 pub publish_rate_hz: f64,
283 pub gear_ratio: f64,
284 pub encoder_type: EncoderType,
285 pub counts_per_revolution: u32,
286}
287
288#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
289#[serde(deny_unknown_fields)]
290pub struct Accelerometer {
291 pub target: StructuralTarget,
292 pub publish_rate_hz: f64,
293 pub axes: Option<[bool; 3]>,
294}
295
296#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
297#[serde(deny_unknown_fields)]
298pub struct Gyroscope {
299 pub target: StructuralTarget,
300 pub publish_rate_hz: f64,
301 pub axes: Option<[bool; 3]>,
302}
303
304#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
305#[serde(deny_unknown_fields)]
306pub struct Magnetometer {
307 pub target: StructuralTarget,
308 pub publish_rate_hz: f64,
309 pub axes: Option<[bool; 3]>,
310}
311
312#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
313#[serde(deny_unknown_fields)]
314pub struct Imu {
315 pub target: StructuralTarget,
316 pub publish_rate_hz: f64,
317 pub axes: Option<[bool; 3]>,
318}
319
320#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
321#[serde(deny_unknown_fields)]
322pub struct Gnss {
323 pub target: StructuralTarget,
324 pub publish_rate_hz: f64,
325 pub coordinate_system: GnssCoordinateSystem,
326}
327
328#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
329#[serde(deny_unknown_fields)]
330pub struct Camera {
331 pub target: StructuralTarget,
332 pub mode: CameraMode,
333 pub publish_rate_hz: f64,
334 pub width_px: u32,
335 pub height_px: u32,
336 pub field_of_view_rad: Option<f64>,
337}
338
339#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
340#[serde(deny_unknown_fields)]
341pub struct Depth {
342 pub target: StructuralTarget,
343 pub publish_rate_hz: f64,
344 pub width_px: u32,
345 pub height_px: u32,
346 pub field_of_view_rad: Option<f64>,
347 pub min_range_m: Option<f64>,
348 pub max_range_m: Option<f64>,
349}
350
351#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
352#[serde(deny_unknown_fields)]
353pub struct Range {
354 pub target: StructuralTarget,
355 pub publish_rate_hz: f64,
356 pub min_range_m: f64,
357 pub max_range_m: f64,
358 pub field_of_view_rad: f64,
359}
360
361#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
362#[serde(deny_unknown_fields)]
363pub struct EmergencyStop {
364 pub target: StructuralTarget,
365}
366
367#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
368#[serde(deny_unknown_fields)]
369pub struct Lidar {
370 pub target: StructuralTarget,
371 pub publish_rate_hz: f64,
372 pub output: LidarOutput,
373 pub min_range_m: Option<f64>,
374 pub max_range_m: Option<f64>,
375 pub horizontal_fov_rad: Option<f64>,
376 pub horizontal_resolution_rad: Option<f64>,
377 pub vertical_fov_rad: Option<f64>,
378 pub vertical_resolution_rad: Option<f64>,
379}
380
381#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
382#[serde(deny_unknown_fields)]
383pub struct Mmwave {
384 pub target: StructuralTarget,
385 pub publish_rate_hz: f64,
386}
387
388#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
389#[serde(deny_unknown_fields)]
390pub struct Microphone {
391 pub target: StructuralTarget,
392 pub publish_rate_hz: f64,
393}
394
395#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
396#[serde(deny_unknown_fields)]
397pub struct Speaker {
398 pub target: StructuralTarget,
399}
400
401#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
402#[serde(deny_unknown_fields)]
403pub struct Battery {
404 pub target: StructuralTarget,
405 pub publish_rate_hz: f64,
406 pub voltage_v: f64,
407 pub capacity_ah: f64,
408}
409
410#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
411#[serde(deny_unknown_fields)]
412pub struct Led {
413 pub target: StructuralTarget,
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::identity::ComponentInstanceId;
420
421 #[test]
422 fn a_structural_target_is_a_tagged_id_on_the_wire() {
423 for (json, target) in [
426 (
427 r#"{"kind":"joint","id":"axle"}"#,
428 StructuralTarget::Joint {
429 id: JointId::new("axle"),
430 },
431 ),
432 (
433 r#"{"kind":"link","id":"body"}"#,
434 StructuralTarget::Link {
435 id: LinkId::new("body"),
436 },
437 ),
438 ] {
439 assert_eq!(serde_json::to_string(&target).unwrap(), json);
440 assert_eq!(
441 serde_json::from_str::<StructuralTarget>(json).unwrap(),
442 target
443 );
444 }
445 }
446
447 #[test]
448 fn namespacing_a_target_keeps_its_kind() {
449 let instance = ComponentInstanceId::new("left_drive").unwrap();
450 let target = StructuralTarget::Joint {
451 id: JointId::new("axle"),
452 };
453 assert_eq!(
454 target.namespaced(&instance),
455 StructuralTarget::Joint {
456 id: JointId::new("left_drive__axle"),
457 }
458 );
459 assert_eq!(target.namespaced(&instance).kind(), StructuralKind::Joint);
460 }
461
462 #[test]
463 fn a_capability_reports_the_kind_it_is() {
464 let motor = Capability::Motor(Motor {
465 target: StructuralTarget::Link {
466 id: LinkId::new("body"),
467 },
468 command: MotorCommand::Velocity,
469 gear_ratio: 1.0,
470 max_torque_nm: None,
471 max_velocity_radps: None,
472 });
473 assert_eq!(motor.kind(), CapabilityKind::Motor);
474 assert_eq!(motor.kind().as_str(), "motor");
475 }
476}