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