franka/robot/active_control.rs
1//! Externally driven control: `read_once` / `write_once` instead of a callback loop.
2//!
3//! Port of `franka::ActiveControl`, `franka::ActiveTorqueControl` and
4//! `franka::ActiveMotionGenerator` (libfranka 0.21.2 `src/active_control.cpp`,
5//! `src/active_torque_control.cpp`, `src/active_motion_generator.cpp`).
6//!
7//! Neither rate limiting nor low-pass filtering is applied here — exactly like libfranka, the
8//! caller is responsible for sending smooth setpoints.
9//!
10//! Both types hold the robot's control lock for their whole lifetime, so no other control or
11//! read operation can run in parallel, and both cancel an unfinished motion when dropped.
12
13use std::sync::MutexGuard;
14
15use crate::control_types::{
16 CartesianPose, CartesianVelocities, Finishable, JointPositions, JointVelocities,
17 MotionGenerator, Torques,
18};
19use crate::duration::Duration;
20#[allow(unused_imports)] // `FrankaError` is referenced by the `# Errors` doc sections below.
21use crate::error::{FrankaError, FrankaResult};
22use crate::robot::robot_impl::{control_error, time_since, RobotImpl};
23use crate::robot_state::RobotState;
24use crate::wire::f64s_to_wire;
25use crate::wire::robot::{ControllerCommand, MotionGeneratorCommand, MoveControllerMode};
26
27/// Motion command types that can drive an [`ActiveMotionGenerator`].
28///
29/// Implemented for exactly the four `franka::MotionGenerator` command types; the method is the
30/// Rust equivalent of the `Robot::Impl::createMotionCommand` overload set, which validates the
31/// user's values before they are put on the wire.
32pub trait ActiveMotionInput: MotionGenerator {
33 /// Validates the command and converts it into the wire motion command.
34 fn create_motion_command(&self) -> FrankaResult<MotionGeneratorCommand>;
35}
36
37impl ActiveMotionInput for JointPositions {
38 fn create_motion_command(&self) -> FrankaResult<MotionGeneratorCommand> {
39 self.validate()?;
40 Ok(MotionGeneratorCommand {
41 q_c: f64s_to_wire(&self.q),
42 ..MotionGeneratorCommand::default()
43 })
44 }
45}
46
47impl ActiveMotionInput for JointVelocities {
48 fn create_motion_command(&self) -> FrankaResult<MotionGeneratorCommand> {
49 self.validate()?;
50 Ok(MotionGeneratorCommand {
51 dq_c: f64s_to_wire(&self.dq),
52 ..MotionGeneratorCommand::default()
53 })
54 }
55}
56
57impl ActiveMotionInput for CartesianPose {
58 fn create_motion_command(&self) -> FrankaResult<MotionGeneratorCommand> {
59 self.validate()?;
60 Ok(MotionGeneratorCommand {
61 O_T_EE_c: f64s_to_wire(&self.O_T_EE),
62 valid_elbow: u8::from(self.has_elbow),
63 elbow_c: f64s_to_wire(if self.has_elbow {
64 &self.elbow
65 } else {
66 &[0.0; 2]
67 }),
68 ..MotionGeneratorCommand::default()
69 })
70 }
71}
72
73impl ActiveMotionInput for CartesianVelocities {
74 fn create_motion_command(&self) -> FrankaResult<MotionGeneratorCommand> {
75 self.validate()?;
76 Ok(MotionGeneratorCommand {
77 O_dP_EE_c: f64s_to_wire(&self.O_dP_EE),
78 valid_elbow: u8::from(self.has_elbow),
79 elbow_c: f64s_to_wire(if self.has_elbow {
80 &self.elbow
81 } else {
82 &[0.0; 2]
83 }),
84 ..MotionGeneratorCommand::default()
85 })
86 }
87}
88
89/// Validates a torque command and converts it (`Robot::Impl::createControllerCommand`).
90pub(crate) fn create_controller_command(
91 control_input: &Torques,
92) -> FrankaResult<ControllerCommand> {
93 control_input.validate()?;
94 Ok(ControllerCommand {
95 tau_J_d: f64s_to_wire(&control_input.tau_J),
96 torque_command_finished: 0,
97 })
98}
99
100/// State shared by both active-control types (the C++ `ActiveControl` base class).
101struct ActiveControl<'a> {
102 robot: &'a RobotImpl,
103 motion_id: u32,
104 lock: Option<MutexGuard<'a, ()>>,
105 control_finished: bool,
106 last_read_access: Option<Duration>,
107}
108
109impl<'a> ActiveControl<'a> {
110 fn new(robot: &'a RobotImpl, motion_id: u32, lock: MutexGuard<'a, ()>) -> ActiveControl<'a> {
111 ActiveControl {
112 robot,
113 motion_id,
114 lock: Some(lock),
115 control_finished: false,
116 last_read_access: None,
117 }
118 }
119
120 /// `ActiveControl::readOnce`.
121 fn read_once(&mut self) -> FrankaResult<(RobotState, Duration)> {
122 let robot_state = self.robot.read_once()?;
123 self.robot
124 .throw_on_motion_error(&robot_state, self.motion_id)?;
125
126 let time_since_last_read = time_since(self.last_read_access, robot_state.time);
127 self.last_read_access = Some(robot_state.time);
128
129 Ok((robot_state, time_since_last_read))
130 }
131
132 /// Marks the control as finished and releases the robot's control lock.
133 ///
134 /// Only called after a *successful* `finish_motion`: `active_torque_control.cpp:61-64` and
135 /// `active_motion_generator.cpp:112-123` set `control_finished` after `finishMotion`
136 /// returns, so a failing finish leaves the flag false and the destructor sends the
137 /// `StopMove`. See [`ActiveControl::release_lock`] for the failing path.
138 fn finish(&mut self) {
139 self.control_finished = true;
140 self.lock = None;
141 }
142
143 /// Releases the robot's control lock without marking the control as finished, so that
144 /// [`Drop`] still cancels the motion.
145 fn release_lock(&mut self) {
146 self.lock = None;
147 }
148
149 /// Applies the outcome of a `finish_motion`: on success the control is over, on failure the
150 /// motion is still considered running and the drop guard cancels it.
151 fn apply_finish(&mut self, result: FrankaResult<()>) -> FrankaResult<()> {
152 match result {
153 Ok(()) => {
154 self.finish();
155 Ok(())
156 }
157 Err(error) => {
158 self.release_lock();
159 Err(error)
160 }
161 }
162 }
163
164 fn check_not_finished(&self) -> FrankaResult<()> {
165 if self.control_finished {
166 return Err(control_error(
167 "writeOnce must not be called after the motion has finished.",
168 ));
169 }
170 Ok(())
171 }
172}
173
174impl Drop for ActiveControl<'_> {
175 /// `ActiveControl::~ActiveControl`: an unfinished motion is cancelled.
176 fn drop(&mut self) {
177 if !self.control_finished {
178 let _ = self.robot.cancel_motion(self.motion_id);
179 }
180 }
181}
182
183/// An external torque controller driven by the caller
184/// (`Robot::start_torque_control`).
185///
186/// # FCI v5
187/// An FER has no torque-only motion generator mode, so the motion is started with a joint
188/// velocity generator and every [`ActiveTorqueControl::write_once`] sends an all-zero
189/// `dq_c` alongside the torques, ending with `motion_generation_finished` instead of
190/// `torque_command_finished`. That is exactly what libfranka 0.9.2's
191/// `Robot::control(control_callback, ...)` does (`src/robot.cpp:41-57`); the API is
192/// unchanged.
193///
194/// # Example
195/// ```no_run
196/// # use franka::{Robot, RealtimeConfig, Torques};
197/// # fn main() -> franka::FrankaResult<()> {
198/// let robot = Robot::new("192.168.0.1", RealtimeConfig::Ignore)?;
199/// let mut control = robot.start_torque_control()?;
200/// for _ in 0..1000 {
201/// let (_state, _period) = control.read_once()?;
202/// control.write_once(&Torques::new([0.0; 7]))?;
203/// }
204/// control.write_once(&franka::motion_finished(Torques::new([0.0; 7])))?;
205/// # Ok(())
206/// # }
207/// ```
208///
209/// # Threading
210/// Unlike [`crate::Robot`], which is `Send + Sync`, this type is **`!Send`**: it holds the
211/// [`std::sync::MutexGuard`] for the robot's control lock (`franka::assertOwningLock`) for as
212/// long as the motion runs, and a `MutexGuard` may not cross threads. Read and write the motion
213/// from the thread that started it; a [`crate::Robot::stop`] from another thread still works,
214/// because it goes through the `Arc<Robot>` and not through this handle.
215pub struct ActiveTorqueControl<'a> {
216 inner: ActiveControl<'a>,
217}
218
219impl<'a> ActiveTorqueControl<'a> {
220 pub(crate) fn new(
221 robot: &'a RobotImpl,
222 motion_id: u32,
223 lock: MutexGuard<'a, ()>,
224 ) -> ActiveTorqueControl<'a> {
225 ActiveTorqueControl {
226 inner: ActiveControl::new(robot, motion_id, lock),
227 }
228 }
229
230 /// Waits for the next robot state and returns it together with the time elapsed since the
231 /// previous `read_once` (zero on the first call).
232 ///
233 /// # Errors
234 /// [`FrankaError::Control`] if the motion was aborted, [`FrankaError::Network`] if the
235 /// connection was lost.
236 pub fn read_once(&mut self) -> FrankaResult<(RobotState, Duration)> {
237 self.inner.read_once()
238 }
239
240 /// Sends the given torques.
241 ///
242 /// Setting `motion_finished` on `control_input` ends the control process; the control lock
243 /// is released and any further call fails.
244 ///
245 /// # Errors
246 /// [`FrankaError::Control`] if the motion has already finished or was aborted,
247 /// [`FrankaError::InvalidArgument`] if a torque is NaN or infinite,
248 /// [`FrankaError::Network`] if the connection was lost.
249 pub fn write_once(&mut self, control_input: &Torques) -> FrankaResult<()> {
250 self.inner.check_not_finished()?;
251
252 let control_command = create_controller_command(control_input)?;
253 if control_input.is_finished() {
254 let result = self
255 .inner
256 .robot
257 .finish_torque_only(self.inner.motion_id, &control_command);
258 return self.inner.apply_finish(result);
259 }
260
261 self.inner
262 .robot
263 .network()
264 .tcp
265 .throw_if_connection_closed()?;
266 self.inner.robot.write_once_torque_only(&control_command)
267 }
268}
269
270/// An externally driven motion generator (`Robot::start_*_control`).
271///
272/// `T` is one of [`JointPositions`], [`JointVelocities`], [`CartesianPose`] or
273/// [`CartesianVelocities`].
274///
275/// # Threading
276/// Unlike [`crate::Robot`], which is `Send + Sync`, this type is **`!Send`**: it holds the
277/// [`std::sync::MutexGuard`] for the robot's control lock (`franka::assertOwningLock`) for as
278/// long as the motion runs, and a `MutexGuard` may not cross threads. Read and write the motion
279/// from the thread that started it; a [`crate::Robot::stop`] from another thread still works,
280/// because it goes through the `Arc<Robot>` and not through this handle.
281pub struct ActiveMotionGenerator<'a, T: ActiveMotionInput> {
282 inner: ActiveControl<'a>,
283 controller_type: MoveControllerMode,
284 marker: std::marker::PhantomData<T>,
285}
286
287impl<'a, T: ActiveMotionInput> ActiveMotionGenerator<'a, T> {
288 pub(crate) fn new(
289 robot: &'a RobotImpl,
290 motion_id: u32,
291 lock: MutexGuard<'a, ()>,
292 controller_type: MoveControllerMode,
293 ) -> ActiveMotionGenerator<'a, T> {
294 ActiveMotionGenerator {
295 inner: ActiveControl::new(robot, motion_id, lock),
296 controller_type,
297 marker: std::marker::PhantomData,
298 }
299 }
300
301 /// Waits for the next robot state and returns it together with the time elapsed since the
302 /// previous `read_once` (zero on the first call).
303 pub fn read_once(&mut self) -> FrankaResult<(RobotState, Duration)> {
304 self.inner.read_once()
305 }
306
307 /// Sends the next motion setpoint, and the torques when the motion runs with an external
308 /// controller.
309 ///
310 /// `control_input` must be `Some` if and only if the motion was started with
311 /// `MoveControllerMode::ExternalController`. Setting `motion_finished` on either input ends
312 /// the control process.
313 ///
314 /// # Errors
315 /// [`FrankaError::Control`] if the motion has already finished, if torques are given
316 /// without an external controller (or missing with one), or if the motion was aborted;
317 /// [`FrankaError::InvalidArgument`] for non-finite values, an invalid transformation matrix
318 /// or an invalid elbow configuration.
319 pub fn write_once(
320 &mut self,
321 motion_input: &T,
322 control_input: Option<&Torques>,
323 ) -> FrankaResult<()> {
324 self.inner.check_not_finished()?;
325
326 let external = self.controller_type == MoveControllerMode::ExternalController;
327 if control_input.is_some() && !external {
328 return Err(control_error(
329 "Torques can only be commanded in kExternalController mode.",
330 ));
331 }
332 if control_input.is_none() && external {
333 return Err(control_error(
334 "Torque command missing, please use writeOnce(const MotionGeneratorType& \
335 motion_generator_input, const Torques& control_input) for external controllers.",
336 ));
337 }
338
339 let torque_finished = control_input.map(|t| t.is_finished()).unwrap_or(false);
340 if motion_input.is_finished() || torque_finished {
341 let motion_command = motion_input.create_motion_command()?;
342 let result = match control_input {
343 None => self.inner.robot.finish_motion(
344 self.inner.motion_id,
345 Some(&motion_command),
346 None,
347 ),
348 Some(control_input) => {
349 let control_command = create_controller_command(control_input)?;
350 self.inner.robot.finish_motion(
351 self.inner.motion_id,
352 Some(&motion_command),
353 Some(&control_command),
354 )
355 }
356 };
357 return self.inner.apply_finish(result);
358 }
359
360 let motion_command = motion_input.create_motion_command()?;
361 match control_input {
362 None => {
363 self.inner
364 .robot
365 .network()
366 .tcp
367 .throw_if_connection_closed()?;
368 self.inner.robot.write_once_motion(&motion_command)
369 }
370 Some(control_input) => {
371 let control_command = create_controller_command(control_input)?;
372 self.inner
373 .robot
374 .network()
375 .tcp
376 .throw_if_connection_closed()?;
377 self.inner
378 .robot
379 .write_once_motion_and_control(&motion_command, &control_command)
380 }
381 }
382 }
383}