Skip to main content

lager/nets/
arm.rs

1//! Robot-arm nets (Rotrics Dexarm): positioning for physical-interaction
2//! tests (pressing buttons, flipping switches).
3//!
4//! Moves block on the box until the arm reaches the target; the HTTP budget
5//! is widened past the box-side move timeout so a healthy slow move is never
6//! aborted client-side. The box enforces the Dexarm workspace bounds and
7//! rejects out-of-range targets.
8
9use serde::Deserialize;
10
11use super::net_handle;
12pub use crate::wire::ArmPosition;
13
14/// The arm's acceleration settings (mm/s², G-code M204), as the arm reports
15/// them.
16#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
17#[non_exhaustive]
18pub struct ArmAcceleration {
19    /// Print acceleration (M204 P).
20    pub acceleration: f64,
21    /// Travel acceleration (M204 T).
22    pub travel_acceleration: f64,
23    /// Retract acceleration (M204 R).
24    pub retract_acceleration: f64,
25}
26
27pub(crate) mod ops {
28    use std::time::Duration;
29
30    use serde_json::json;
31
32    use super::ArmAcceleration;
33    use crate::error::{Error, Result};
34    use crate::wire::{
35        net_command, unit, value_arm_position, value_as, ArmPosition, CommandResponse, Op, Timeout,
36    };
37
38    const ROLE: &str = "arm";
39
40    /// Box-side move timeout (s), matching the CLI default.
41    const MOVE_TIMEOUT_S: f64 = 15.0;
42
43    /// Budget for a move: the box waits up to the move timeout plus its own
44    /// proxy margin, so give the HTTP layer that plus headroom.
45    fn move_budget(timeout_s: f64) -> Timeout {
46        Timeout::After(Duration::from_secs_f64(timeout_s + 30.0))
47    }
48
49    /// Non-move actions still touch the serial port (and may queue behind a
50    /// move on the arm's device lock), so they get a widened budget too.
51    fn quick_budget() -> Timeout {
52        Timeout::After(Duration::from_secs(45))
53    }
54
55    pub(crate) fn position(name: &str) -> Op<ArmPosition> {
56        Op {
57            req: net_command(name, ROLE, "position", json!({}), quick_budget()),
58            parse: value_arm_position,
59        }
60    }
61
62    pub(crate) fn move_to_with_timeout(
63        name: &str,
64        x: f64,
65        y: f64,
66        z: f64,
67        timeout_s: f64,
68    ) -> Op<ArmPosition> {
69        Op {
70            req: net_command(
71                name,
72                ROLE,
73                "move",
74                json!({ "x": x, "y": y, "z": z, "timeout": timeout_s }),
75                move_budget(timeout_s),
76            ),
77            parse: value_arm_position,
78        }
79    }
80
81    pub(crate) fn move_to(name: &str, x: f64, y: f64, z: f64) -> Op<ArmPosition> {
82        move_to_with_timeout(name, x, y, z, MOVE_TIMEOUT_S)
83    }
84
85    pub(crate) fn move_by_with_timeout(
86        name: &str,
87        dx: f64,
88        dy: f64,
89        dz: f64,
90        timeout_s: f64,
91    ) -> Op<ArmPosition> {
92        Op {
93            req: net_command(
94                name,
95                ROLE,
96                "move_by",
97                json!({ "dx": dx, "dy": dy, "dz": dz, "timeout": timeout_s }),
98                move_budget(timeout_s),
99            ),
100            parse: value_arm_position,
101        }
102    }
103
104    pub(crate) fn move_by(name: &str, dx: f64, dy: f64, dz: f64) -> Op<ArmPosition> {
105        move_by_with_timeout(name, dx, dy, dz, MOVE_TIMEOUT_S)
106    }
107
108    pub(crate) fn go_home(name: &str) -> Op<()> {
109        Op {
110            req: net_command(name, ROLE, "go_home", json!({}), quick_budget()),
111            parse: unit,
112        }
113    }
114
115    pub(crate) fn enable_motor(name: &str) -> Op<()> {
116        Op {
117            req: net_command(name, ROLE, "enable_motor", json!({}), quick_budget()),
118            parse: unit,
119        }
120    }
121
122    pub(crate) fn disable_motor(name: &str) -> Op<()> {
123        Op {
124            req: net_command(name, ROLE, "disable_motor", json!({}), quick_budget()),
125            parse: unit,
126        }
127    }
128
129    pub(crate) fn read_and_save_position(name: &str) -> Op<ArmPosition> {
130        Op {
131            req: net_command(
132                name,
133                ROLE,
134                "read_and_save_position",
135                json!({}),
136                quick_budget(),
137            ),
138            parse: value_arm_position,
139        }
140    }
141
142    pub(crate) fn get_acceleration(name: &str) -> Op<ArmAcceleration> {
143        Op {
144            req: net_command(name, ROLE, "get_acceleration", json!({}), quick_budget()),
145            parse: value_as::<ArmAcceleration>,
146        }
147    }
148
149    /// A box >= 0.60.0 answers with the arm's readback; older boxes
150    /// send no value.
151    fn parse_readback(resp: CommandResponse) -> Result<Option<ArmAcceleration>> {
152        match resp.value {
153            None | Some(serde_json::Value::Null) => Ok(None),
154            Some(v) => serde_json::from_value(v)
155                .map(Some)
156                .map_err(|e| Error::Decode(format!("invalid acceleration readback: {e}"))),
157        }
158    }
159
160    pub(crate) fn set_acceleration(
161        name: &str,
162        acceleration: u32,
163        travel_acceleration: u32,
164        retract_acceleration: Option<u32>,
165    ) -> Op<Option<ArmAcceleration>> {
166        let mut params = json!({
167            "acceleration": acceleration,
168            "travel_acceleration": travel_acceleration,
169        });
170        if let Some(r) = retract_acceleration {
171            params["retract_acceleration"] = json!(r);
172        }
173        Op {
174            req: net_command(name, ROLE, "set_acceleration", params, quick_budget()),
175            parse: parse_readback,
176        }
177    }
178
179    /// The arm actions box 0.60.0 added: `get_acceleration`,
180    /// and `set_acceleration` without a retract value.
181    pub(crate) fn acceleration_compat(err: Error) -> Error {
182        let err = crate::wire::unknown_action_compat(err, "get_acceleration", ">= 0.60.0");
183        match err {
184            Error::Box {
185                status: 400,
186                ref message,
187            } if message.contains("Missing required value")
188                && message.contains("retract_acceleration") =>
189            {
190                Error::UnsupportedByBox {
191                    message: "set_acceleration without a retract value requires box \
192                              software >= 0.60.0; pass all three values"
193                        .to_string(),
194                }
195            }
196            other => other,
197        }
198    }
199}
200
201net_handle! {
202    /// Handle for a robot-arm net.
203    ///
204    /// Coordinates are millimeters in the arm's workspace. Moves block until
205    /// the arm reaches the target (box-side timeout 15s).
206    sync: Arm,
207    async: AsyncArm,
208    methods: {
209        /// Read the current end-effector position.
210        fn position() -> ArmPosition = ops::position;
211        /// Move to an absolute position; returns the position after the move.
212        fn move_to(x: f64, y: f64, z: f64) -> ArmPosition = ops::move_to;
213        /// [`Arm::move_to`] with an explicit box-side move timeout in seconds
214        /// (default 15 s), for slow or long moves. The HTTP budget widens
215        /// automatically to stay above it.
216        fn move_to_with_timeout(x: f64, y: f64, z: f64, timeout_s: f64) -> ArmPosition = ops::move_to_with_timeout;
217        /// Move relative to the current position; returns the new position.
218        fn move_by(dx: f64, dy: f64, dz: f64) -> ArmPosition = ops::move_by;
219        /// [`Arm::move_by`] with an explicit box-side move timeout in seconds
220        /// (default 15 s).
221        fn move_by_with_timeout(dx: f64, dy: f64, dz: f64, timeout_s: f64) -> ArmPosition = ops::move_by_with_timeout;
222        /// Move the arm to its home position (X0 Y300 Z0).
223        fn go_home() -> () = ops::go_home;
224        /// Energize the arm motors.
225        fn enable_motor() -> () = ops::enable_motor;
226        /// Release the arm motors (the arm can then be moved by hand).
227        fn disable_motor() -> () = ops::disable_motor;
228        /// Read the current position and persist it on the arm; returns it.
229        fn read_and_save_position() -> ArmPosition = ops::read_and_save_position;
230    }
231}
232
233// The acceleration methods live outside the macro so an older box's refusal
234// can be mapped to `Error::UnsupportedByBox`.
235
236#[cfg(feature = "blocking")]
237impl Arm<'_> {
238    /// Read the arm's print, travel and retract acceleration (mm/s², from
239    /// its settings report).
240    ///
241    /// Requires box software >= 0.60.0; older boxes fail with
242    /// [`crate::Error::UnsupportedByBox`].
243    pub fn get_acceleration(&self) -> crate::error::Result<ArmAcceleration> {
244        self.client
245            .run(ops::get_acceleration(&self.name))
246            .map_err(ops::acceleration_compat)
247    }
248
249    /// Set print/travel/retract acceleration (mm/s², G-code M204).
250    ///
251    /// Returns what the arm reports after the change, so a value it did not
252    /// take shows up here; `None` from a box at 0.59.0 or older, which sends
253    /// no readback.
254    pub fn set_acceleration(
255        &self,
256        acceleration: u32,
257        travel_acceleration: u32,
258        retract_acceleration: u32,
259    ) -> crate::error::Result<Option<ArmAcceleration>> {
260        self.client.run(ops::set_acceleration(
261            &self.name,
262            acceleration,
263            travel_acceleration,
264            Some(retract_acceleration),
265        ))
266    }
267
268    /// Set print and travel acceleration, leaving retract as it is.
269    ///
270    /// Requires box software >= 0.60.0; older boxes fail with
271    /// [`crate::Error::UnsupportedByBox`].
272    pub fn set_acceleration_keep_retract(
273        &self,
274        acceleration: u32,
275        travel_acceleration: u32,
276    ) -> crate::error::Result<Option<ArmAcceleration>> {
277        self.client
278            .run(ops::set_acceleration(
279                &self.name,
280                acceleration,
281                travel_acceleration,
282                None,
283            ))
284            .map_err(ops::acceleration_compat)
285    }
286}
287
288#[cfg(feature = "async")]
289impl AsyncArm<'_> {
290    /// See [`Arm::get_acceleration`].
291    pub async fn get_acceleration(&self) -> crate::error::Result<ArmAcceleration> {
292        self.client
293            .run(ops::get_acceleration(&self.name))
294            .await
295            .map_err(ops::acceleration_compat)
296    }
297
298    /// See [`Arm::set_acceleration`].
299    pub async fn set_acceleration(
300        &self,
301        acceleration: u32,
302        travel_acceleration: u32,
303        retract_acceleration: u32,
304    ) -> crate::error::Result<Option<ArmAcceleration>> {
305        self.client
306            .run(ops::set_acceleration(
307                &self.name,
308                acceleration,
309                travel_acceleration,
310                Some(retract_acceleration),
311            ))
312            .await
313    }
314
315    /// See [`Arm::set_acceleration_keep_retract`].
316    pub async fn set_acceleration_keep_retract(
317        &self,
318        acceleration: u32,
319        travel_acceleration: u32,
320    ) -> crate::error::Result<Option<ArmAcceleration>> {
321        self.client
322            .run(ops::set_acceleration(
323                &self.name,
324                acceleration,
325                travel_acceleration,
326                None,
327            ))
328            .await
329            .map_err(ops::acceleration_compat)
330    }
331}