Skip to main content

phoxal_api/api/v0_1/
drive.rs

1#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
2#[serde(rename_all = "snake_case")]
3pub enum StopReason {
4    TargetStale,
5    TargetNotFinite,
6    ActuatorCommandNotFinite,
7    EmergencyStop,
8    Fault,
9}
10
11/// A finite requested or limited planar velocity in the current
12/// control wire revision.
13#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct Target {
16    #[serde(deserialize_with = "crate::api::v0_1::drive::deserialize_finite_target_scalar")]
17    pub(crate) linear_x_mps: f32,
18    #[serde(deserialize_with = "crate::api::v0_1::drive::deserialize_finite_target_scalar")]
19    pub(crate) angular_z_radps: f32,
20}
21
22impl Target {
23    pub fn try_new(linear_x_mps: f32, angular_z_radps: f32) -> Result<Self, InvalidTarget> {
24        if !linear_x_mps.is_finite() {
25            return Err(InvalidTarget::LinearXNotFinite);
26        }
27        if !angular_z_radps.is_finite() {
28            return Err(InvalidTarget::AngularZNotFinite);
29        }
30        Ok(Self {
31            linear_x_mps,
32            angular_z_radps,
33        })
34    }
35
36    #[must_use]
37    pub const fn linear_x_mps(&self) -> f32 {
38        self.linear_x_mps
39    }
40
41    #[must_use]
42    pub const fn angular_z_radps(&self) -> f32 {
43        self.angular_z_radps
44    }
45
46    #[must_use]
47    pub const fn stopped() -> Self {
48        Self {
49            linear_x_mps: 0.0,
50            angular_z_radps: 0.0,
51        }
52    }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum InvalidTarget {
57    LinearXNotFinite,
58    AngularZNotFinite,
59}
60
61impl std::fmt::Display for InvalidTarget {
62    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        let field = match self {
64            Self::LinearXNotFinite => "linear_x_mps",
65            Self::AngularZNotFinite => "angular_z_radps",
66        };
67        write!(formatter, "control target field {field} must be finite")
68    }
69}
70
71impl std::error::Error for InvalidTarget {}
72
73/// The drive participant's exclusive control state.
74#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
75pub enum State {
76    Active {
77        target: Target,
78        limited_target: Target,
79    },
80    Stopped {
81        target: Target,
82        reason: StopReason,
83    },
84}
85
86/// Deserialize a control-target scalar without allowing a non-finite value to
87/// enter a lease or a wheel-mixing calculation. The target fields remain plain
88/// `f32`s on the wire; their explicitly restricted visibility and this serde
89/// hook make serde enforce the same invariant as `Target::try_new`.
90pub(crate) fn deserialize_finite_target_scalar<'de, D>(deserializer: D) -> Result<f32, D::Error>
91where
92    D: serde::Deserializer<'de>,
93{
94    let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
95    value
96        .is_finite()
97        .then_some(value)
98        .ok_or_else(|| serde::de::Error::custom("control target scalar must be finite"))
99}
100
101phoxal_macros::phoxal_api_fragment! {
102    path drive;
103
104    version v0_1;
105
106    command target: Setpoint<Target>;
107    topic state: State<State>;
108}