Skip to main content

franka/robot/
mod.rs

1//! The public `Robot` API.
2//!
3//! Port of `franka::Robot` (libfranka 0.21.2 `include/franka/robot.h`, `src/robot.cpp`). The
4//! doc comments are libfranka's, adapted to Rust's error type.
5//!
6//! # Threading
7//! [`Robot`] is `Send + Sync` and every method takes `&self`, so a control loop and a
8//! [`Robot::stop`] call can run on two threads at the same time; share the robot as an
9//! `Arc<Robot>`. Only one control or read operation may run at a time: a second one returns
10//! [`FrankaError::InvalidOperation`].
11//!
12//! # Example
13//! ```no_run
14//! use franka::{JointPositions, RealtimeConfig, Robot};
15//!
16//! # fn main() -> franka::FrankaResult<()> {
17//! let robot = Robot::new("172.16.0.2", RealtimeConfig::Enforce)?;
18//! let initial = robot.read_once()?.q;
19//! let mut time = 0.0;
20//! robot.control_joint_positions(
21//!     |_state, period| {
22//!         time += period.as_secs_f64();
23//!         let delta = std::f64::consts::PI / 8.0 * (1.0 - (std::f64::consts::PI / 2.5 * time).cos());
24//!         let mut q = initial;
25//!         q[3] += delta;
26//!         let output = JointPositions::new(q);
27//!         if time >= 5.0 {
28//!             franka::motion_finished(output)
29//!         } else {
30//!             output
31//!         }
32//!     },
33//!     franka::ControllerMode::JointImpedance,
34//!     true,
35//!     franka::DEFAULT_CUTOFF_FREQUENCY,
36//! )?;
37//! # Ok(())
38//! # }
39//! ```
40
41pub mod active_control;
42pub mod commands;
43pub mod control_loop;
44pub mod logger;
45pub mod robot_impl;
46pub mod target_control;
47
48mod active;
49mod control;
50pub mod options;
51
52#[cfg(test)]
53mod mock_tests;
54
55use std::sync::{Mutex, MutexGuard, TryLockError};
56
57use crate::control_types::{
58    CartesianPose, CartesianVelocities, ControllerMode, JointPositions, JointVelocities, Torques,
59};
60use crate::duration::Duration;
61use crate::error::{FrankaError, FrankaResult};
62use crate::model::Model;
63use crate::realtime::RealtimeConfig;
64use crate::robot_state::RobotState;
65use crate::wire::robot::codec::FciVersion;
66use crate::wire::robot::{Deviation, MoveControllerMode, MoveMotionGeneratorMode};
67
68pub use active_control::{ActiveMotionGenerator, ActiveMotionInput, ActiveTorqueControl};
69use control_loop::{ControlLoop, ControlLoopMotion};
70pub use options::{RobotOptions, VersionPolicy, VirtualWallCuboid};
71use robot_impl::{RobotImpl, DEFAULT_DEVIATION, NUM_JOINTS};
72pub use target_control::{
73    impedance_torques, Backend, CartesianSent, CartesianTargetControl, IkOptions, ImpedanceGains,
74    ImpedanceOptions, JointSent, JointTargetControl, JointTargetControlOptions, Leash, Settle,
75    TargetControlOptions,
76};
77
78/// Default size of the control log attached to a [`crate::error::ControlException`]
79/// (`franka::Robot`'s `log_size` default).
80pub const DEFAULT_LOG_SIZE: usize = 50;
81
82/// Text of the exception libfranka raises when a second control or read operation is started
83/// (`franka::assertOwningLock`).
84pub const CONTROL_LOCK_MESSAGE: &str =
85    "libfranka robot: Cannot perform this operation while another control or read operation is \
86     running.";
87
88/// Compile-time proof of the `Send + Sync` promise this module's docs make (the contrast the
89/// `!Send` [`ActiveTorqueControl`] / [`ActiveMotionGenerator`] handles are documented against).
90const _: fn() = || {
91    fn assert_send_sync<T: Send + Sync>() {}
92    assert_send_sync::<Robot>();
93};
94
95/// Maintains a network connection to the robot, provides the current robot state, gives access
96/// to the robot model and allows to control the robot.
97///
98/// Port of `franka::Robot`.
99#[derive(Debug)]
100pub struct Robot {
101    robot: RobotImpl,
102    control_lock: Mutex<()>,
103}
104
105impl Robot {
106    /// Establishes a connection with the robot.
107    ///
108    /// `franka_address` is the IP/hostname of the robot, optionally with a `":port"` suffix;
109    /// without one the FCI command port 1337 is used.
110    ///
111    /// # Errors
112    /// [`FrankaError::Network`] if the connection could not be established,
113    /// [`FrankaError::IncompatibleVersion`] if the robot speaks another FCI version,
114    /// [`FrankaError::Realtime`] with [`RealtimeConfig::Enforce`] if realtime priority or a
115    /// realtime kernel is unavailable.
116    pub fn new(franka_address: &str, realtime_config: RealtimeConfig) -> FrankaResult<Robot> {
117        Robot::new_with_log_size(franka_address, realtime_config, DEFAULT_LOG_SIZE)
118    }
119
120    /// [`Robot::new`] with an explicit size for the control log that is attached to a
121    /// [`crate::error::ControlException`]; `0` disables logging.
122    pub fn new_with_log_size(
123        franka_address: &str,
124        realtime_config: RealtimeConfig,
125        log_size: usize,
126    ) -> FrankaResult<Robot> {
127        Robot::with_options(
128            franka_address,
129            RobotOptions::new(realtime_config).with_log_size(log_size),
130        )
131    }
132
133    /// [`Robot::new`] with an explicit [`RobotOptions`], which is how a Franka Emika Robot (FER)
134    /// (FCI v5) is connected.
135    ///
136    /// # Example
137    /// ```no_run
138    /// use franka::{FciVersion, RealtimeConfig, Robot, RobotOptions, VersionPolicy};
139    ///
140    /// # fn main() -> franka::FrankaResult<()> {
141    /// let options = RobotOptions::new(RealtimeConfig::Ignore)
142    ///     .with_version(VersionPolicy::Exact(FciVersion::V5));
143    /// let robot = Robot::with_options("172.16.0.2", options)?;
144    /// assert_eq!(robot.fci_version(), FciVersion::V5);
145    /// # Ok(())
146    /// # }
147    /// ```
148    pub fn with_options(franka_address: &str, options: RobotOptions) -> FrankaResult<Robot> {
149        Ok(Robot {
150            robot: RobotImpl::new_with_policy(
151                franka_address,
152                options.realtime_config,
153                options.log_size,
154                options.version,
155            )?,
156            control_lock: Mutex::new(()),
157        })
158    }
159
160    /// Returns the software version reported by the connected server.
161    pub fn server_version(&self) -> u16 {
162        self.robot.server_version()
163    }
164
165    /// The FCI protocol version this connection negotiated.
166    pub fn fci_version(&self) -> FciVersion {
167        self.robot.version()
168    }
169
170    /// Waits for a robot state arriving over the UDP stream and returns it.
171    ///
172    /// Unlike [`Robot::read`] this does not take the control lock, so it can be called while
173    /// nothing else is running.
174    pub fn read_once(&self) -> FrankaResult<RobotState> {
175        self.robot.read_once()
176    }
177
178    /// Starts a loop for reading the current robot state.
179    ///
180    /// The callback is invoked for every received state; returning `false` ends the loop.
181    ///
182    /// # Errors
183    /// [`FrankaError::InvalidOperation`] if another control or read operation is running.
184    pub fn read<F: FnMut(&RobotState) -> bool>(&self, mut read_callback: F) -> FrankaResult<()> {
185        let _lock = self.acquire_control_lock()?;
186        loop {
187            let robot_state = self.robot.update_motion(None, None)?;
188            if !read_callback(&robot_state) {
189                return Ok(());
190            }
191        }
192    }
193
194    /// Returns the robot's URDF model as a string (`GetRobotModel`).
195    ///
196    /// # Errors
197    /// [`FrankaError::InvalidOperation`] on FCI v5, which has no `GetRobotModel`; an FER
198    /// serves its model as a shared object, so use [`Robot::load_model`] there.
199    pub fn robot_model(&self) -> FrankaResult<String> {
200        self.robot.get_robot_model()
201    }
202
203    /// Loads the kinematic and dynamic model of the connected robot.
204    ///
205    /// Neither version downloads executable code:
206    ///
207    /// * **FCI v10 (FR3)** asks the robot for its URDF (`GetRobotModel`) and
208    ///   evaluates it with the native backend.
209    /// * **FCI v5 (FER)** has no `GetRobotModel`, and this returns
210    ///   [`Model::native_fer`] — the same native backend over the built-in
211    ///   [`crate::model::FER_URDF`], whose inertial parameters were identified
212    ///   from an FER's own `libfcimodels_x64.so`. Nothing is fetched, so this
213    ///   cannot fail, needs no `model-library` feature and works on any host.
214    ///
215    /// Use [`Robot::load_model_from_robot`] to download and `dlopen` the FER's
216    /// own shared object instead, which is what libfranka 0.9.2 does. The two
217    /// agree on kinematics to 9e-16 and on gravity
218    /// to 5e-14; with a payload attached their mass matrices differ by up to
219    /// 3e-3 kg m^2, because the shared object's `M_NE` is not a rigid-body model
220    /// of the payload. See `docs/book/src/reference/model.md`.
221    ///
222    /// # Errors
223    ///
224    /// [`FrankaError::Model`] if the model cannot be obtained or parsed;
225    /// [`FrankaError::Network`] / [`FrankaError::Protocol`] for the usual
226    /// command-socket failures. On FCI v5 none of these can happen.
227    pub fn load_model(&self) -> FrankaResult<Model> {
228        match self.fci_version() {
229            FciVersion::V5 => Ok(Model::native_fer()),
230            FciVersion::V10 => Model::from_urdf(&self.robot_model()?),
231        }
232    }
233
234    /// Loads the model the robot itself serves.
235    ///
236    /// On FCI v10 this is [`Robot::load_model`] exactly: an FR3 serves a URDF.
237    /// On FCI v5 it is libfranka 0.9.2's behaviour — `LoadModelLibrary`
238    /// downloads `libfcimodels.so` and the client `dlopen`s it. This requires
239    /// the opt-in `model-library` feature; it is disabled by default.
240    ///
241    /// # Security
242    ///
243    /// **On FCI v5 (FER) this downloads a shared object served by the robot,
244    /// writes it to a temporary file and `dlopen`s it** — that is, it executes
245    /// native code chosen by the FCI peer, in this process, with this process's
246    /// privileges. Nothing validates the blob: the command socket is plaintext
247    /// TCP with no authentication, and there is no signature to check.
248    ///
249    /// This is deliberate, and it is exactly what libfranka 0.9.2 does
250    /// (`franka::Model::Model(Network&)` -> `LibraryDownloader` ->
251    /// `LibraryLoader`): the FCI peer is already fully trusted, because it is
252    /// the thing that commands the arm. Anyone who can impersonate the robot on
253    /// this socket can already move it. So the method stays *safe*, like
254    /// libfranka's, and the trust boundary is the network you put the robot on:
255    /// give the FCI its own isolated link, as Franka's own setup guide requires.
256    ///
257    /// Without the opt-in `model-library` feature, this method returns
258    /// [`FrankaError::Model`] on an FER without downloading anything, and
259    /// `libloading` is not linked. [`Robot::load_model`] uses the built-in
260    /// native FER model regardless of the feature. FCI v10 is unaffected:
261    /// an FR3 serves a URDF, which is parsed, not executed.
262    ///
263    /// `Model::from_model_library_bytes` and `Model::from_model_library_path`
264    /// (both `model-library` only) expose the same load as `unsafe fn`s, for
265    /// callers who supply the bytes themselves.
266    ///
267    /// # Errors
268    ///
269    /// [`FrankaError::Model`] if the model cannot be obtained, parsed, saved or
270    /// loaded; [`FrankaError::Network`] / [`FrankaError::Protocol`] for the
271    /// usual command-socket failures.
272    pub fn load_model_from_robot(&self) -> FrankaResult<Model> {
273        match self.fci_version() {
274            FciVersion::V5 => self.robot.load_model_v5(),
275            FciVersion::V10 => Model::from_urdf(&self.robot_model()?),
276        }
277    }
278
279    /// Sets the cut-off frequencies of the robot-side filters, in hertz (`Robot::setFilters`).
280    ///
281    /// # Errors
282    /// [`FrankaError::InvalidOperation`] on FCI v10, which dropped the command;
283    /// [`FrankaError::Command`] if the robot rejected it.
284    pub fn set_filters(
285        &self,
286        joint_position_filter_frequency: f64,
287        joint_velocity_filter_frequency: f64,
288        cartesian_position_filter_frequency: f64,
289        cartesian_velocity_filter_frequency: f64,
290        controller_filter_frequency: f64,
291    ) -> FrankaResult<()> {
292        self.robot.set_filters(
293            joint_position_filter_frequency,
294            joint_velocity_filter_frequency,
295            cartesian_position_filter_frequency,
296            cartesian_velocity_filter_frequency,
297            controller_filter_frequency,
298        )
299    }
300
301    /// Returns the parameters of the virtual wall with the given `id`
302    /// (`Robot::getVirtualWall`).
303    ///
304    /// # Errors
305    /// [`FrankaError::InvalidOperation`] on FCI v10, which dropped the command;
306    /// [`FrankaError::Command`] if the robot rejected it.
307    pub fn virtual_wall(&self, id: i32) -> FrankaResult<VirtualWallCuboid> {
308        self.robot.virtual_wall(id)
309    }
310
311    /// Stops all currently running motions.
312    ///
313    /// Can be called from a second thread while a control loop is running; the control loop
314    /// then fails with a [`FrankaError::Control`] carrying
315    /// `"libfranka: Move command preempted!"`.
316    ///
317    /// # Errors
318    /// [`FrankaError::Command`] if the robot rejected the `StopMove`.
319    pub fn stop(&self) -> FrankaResult<()> {
320        self.robot.stop()
321    }
322
323    /// Runs automatic error recovery on the robot, clearing the errors of a reflex or a
324    /// collision so that a new motion can be started.
325    ///
326    /// # Errors
327    /// [`FrankaError::Command`] if the robot rejected the command, e.g. because manual error
328    /// recovery is required.
329    pub fn automatic_error_recovery(&self) -> FrankaResult<()> {
330        self.robot.automatic_error_recovery()
331    }
332
333    /// Changes the collision behavior.
334    ///
335    /// Set separate torque and force boundaries for acceleration/deceleration and constant
336    /// velocity movement phases. Forces or torques between lower and upper threshold are shown
337    /// as contacts in the robot state; above the upper threshold the robot stops and enters an
338    /// error state.
339    ///
340    /// # Errors
341    /// [`FrankaError::Command`] if the robot rejected the command.
342    #[allow(clippy::too_many_arguments)]
343    pub fn set_collision_behavior(
344        &self,
345        lower_torque_thresholds_acceleration: [f64; 7],
346        upper_torque_thresholds_acceleration: [f64; 7],
347        lower_torque_thresholds_nominal: [f64; 7],
348        upper_torque_thresholds_nominal: [f64; 7],
349        lower_force_thresholds_acceleration: [f64; 6],
350        upper_force_thresholds_acceleration: [f64; 6],
351        lower_force_thresholds_nominal: [f64; 6],
352        upper_force_thresholds_nominal: [f64; 6],
353    ) -> FrankaResult<()> {
354        self.robot.set_collision_behavior(
355            &lower_torque_thresholds_acceleration,
356            &upper_torque_thresholds_acceleration,
357            &lower_torque_thresholds_nominal,
358            &upper_torque_thresholds_nominal,
359            &lower_force_thresholds_acceleration,
360            &upper_force_thresholds_acceleration,
361            &lower_force_thresholds_nominal,
362            &upper_force_thresholds_nominal,
363        )
364    }
365
366    /// [`Robot::set_collision_behavior`] with the same thresholds for the acceleration and the
367    /// constant velocity phase.
368    pub fn set_collision_behavior_simple(
369        &self,
370        lower_torque_thresholds: [f64; 7],
371        upper_torque_thresholds: [f64; 7],
372        lower_force_thresholds: [f64; 6],
373        upper_force_thresholds: [f64; 6],
374    ) -> FrankaResult<()> {
375        self.robot.set_collision_behavior(
376            &lower_torque_thresholds,
377            &upper_torque_thresholds,
378            &lower_torque_thresholds,
379            &upper_torque_thresholds,
380            &lower_force_thresholds,
381            &upper_force_thresholds,
382            &lower_force_thresholds,
383            &upper_force_thresholds,
384        )
385    }
386
387    /// Sets the impedance for each joint in the internal controller \[Nm/rad\].
388    ///
389    /// User-provided torques are not affected by this setting.
390    pub fn set_joint_impedance(&self, K_theta: [f64; 7]) -> FrankaResult<()> {
391        self.robot.set_joint_impedance(&K_theta)
392    }
393
394    /// Sets the Cartesian impedance for (x, y, z, roll, pitch, yaw) in the internal controller.
395    ///
396    /// User-provided torques are not affected by this setting.
397    pub fn set_cartesian_impedance(&self, K_x: [f64; 6]) -> FrankaResult<()> {
398        self.robot.set_cartesian_impedance(&K_x)
399    }
400
401    /// Locks or unlocks guiding mode movement in (x, y, z, roll, pitch, yaw).
402    ///
403    /// If `elbow` is `true` the elbow is locked and the flag for the 3rd and 5th joint is
404    /// ignored.
405    pub fn set_guiding_mode(&self, guiding_mode: [bool; 6], elbow: bool) -> FrankaResult<()> {
406        self.robot.set_guiding_mode(&guiding_mode, elbow)
407    }
408
409    /// Sets the transformation from the end effector frame `EE` to the stiffness frame `K`,
410    /// column-major.
411    pub fn set_k(&self, EE_T_K: [f64; 16]) -> FrankaResult<()> {
412        self.robot.set_k(&EE_T_K)
413    }
414
415    /// Sets the transformation from the nominal end effector frame `NE` to the end effector
416    /// frame `EE`, column-major.
417    ///
418    /// The transformation from flange to `NE` is set in Desk.
419    pub fn set_ee(&self, NE_T_EE: [f64; 16]) -> FrankaResult<()> {
420        self.robot.set_ee(&NE_T_EE)
421    }
422
423    /// Sets dynamic parameters of a payload: mass in \[kg\], centre of mass in the flange
424    /// frame and the inertia matrix with respect to the centre of mass, column-major.
425    ///
426    /// This is not for setting end effector parameters, which have to be set in the
427    /// administrator's interface.
428    pub fn set_load(
429        &self,
430        load_mass: f64,
431        F_x_Cload: [f64; 3],
432        load_inertia: [f64; 9],
433    ) -> FrankaResult<()> {
434        self.robot.set_load(load_mass, &F_x_Cload, &load_inertia)
435    }
436
437    /// Position-dependent upper joint velocity limits at `q`, as used by the rate limiter.
438    pub fn upper_joint_velocity_limits(&self, q: &[f64; NUM_JOINTS]) -> [f64; NUM_JOINTS] {
439        self.robot.upper_joint_velocity_limits(q)
440    }
441
442    /// Position-dependent lower joint velocity limits at `q`, as used by the rate limiter.
443    pub fn lower_joint_velocity_limits(&self, q: &[f64; NUM_JOINTS]) -> [f64; NUM_JOINTS] {
444        self.robot.lower_joint_velocity_limits(q)
445    }
446
447    /// Takes the control lock, or reports that another operation owns it
448    /// (`franka::assertOwningLock`).
449    fn acquire_control_lock(&self) -> FrankaResult<MutexGuard<'_, ()>> {
450        match self.control_lock.try_lock() {
451            Ok(guard) => Ok(guard),
452            // A panic in a previous control loop leaves the mutex poisoned; the data it guards
453            // is `()`, so recovering is always safe.
454            Err(TryLockError::Poisoned(poisoned)) => Ok(poisoned.into_inner()),
455            Err(TryLockError::WouldBlock) => Err(FrankaError::InvalidOperation(
456                CONTROL_LOCK_MESSAGE.to_string(),
457            )),
458        }
459    }
460}