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("192.168.0.1", 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("192.168.0.1", 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 and what
217    /// franka-rs did before. The two 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.
239    ///
240    /// # Security
241    ///
242    /// **On FCI v5 (FER) this downloads a shared object served by the robot,
243    /// writes it to a temporary file and `dlopen`s it** — that is, it executes
244    /// native code chosen by the FCI peer, in this process, with this process's
245    /// privileges. Nothing validates the blob: the command socket is plaintext
246    /// TCP with no authentication, and there is no signature to check.
247    ///
248    /// This is deliberate, and it is exactly what libfranka 0.9.2 does
249    /// (`franka::Model::Model(Network&)` -> `LibraryDownloader` ->
250    /// `LibraryLoader`): the FCI peer is already fully trusted, because it is
251    /// the thing that commands the arm. Anyone who can impersonate the robot on
252    /// this socket can already move it. So the method stays *safe*, like
253    /// libfranka's, and the trust boundary is the network you put the robot on:
254    /// give the FCI its own isolated link, as Franka's own setup guide requires.
255    ///
256    /// To opt out, use [`Robot::load_model`], which needs no download at all, or
257    /// build without the default `model-library` feature — then this method
258    /// returns [`FrankaError::Model`] on an FER instead of loading anything,
259    /// and `libloading` is not linked at all. FCI v10 is unaffected either way:
260    /// an FR3 serves a URDF, which is parsed, not executed.
261    ///
262    /// `Model::from_model_library_bytes` and `Model::from_model_library_path`
263    /// (both `model-library` only) expose the same load as `unsafe fn`s, for
264    /// callers who supply the bytes themselves.
265    ///
266    /// # Errors
267    ///
268    /// [`FrankaError::Model`] if the model cannot be obtained, parsed, saved or
269    /// loaded; [`FrankaError::Network`] / [`FrankaError::Protocol`] for the
270    /// usual command-socket failures.
271    pub fn load_model_from_robot(&self) -> FrankaResult<Model> {
272        match self.fci_version() {
273            FciVersion::V5 => self.robot.load_model_v5(),
274            FciVersion::V10 => Model::from_urdf(&self.robot_model()?),
275        }
276    }
277
278    /// Sets the cut-off frequencies of the robot-side filters, in hertz (`Robot::setFilters`).
279    ///
280    /// # Errors
281    /// [`FrankaError::InvalidOperation`] on FCI v10, which dropped the command;
282    /// [`FrankaError::Command`] if the robot rejected it.
283    pub fn set_filters(
284        &self,
285        joint_position_filter_frequency: f64,
286        joint_velocity_filter_frequency: f64,
287        cartesian_position_filter_frequency: f64,
288        cartesian_velocity_filter_frequency: f64,
289        controller_filter_frequency: f64,
290    ) -> FrankaResult<()> {
291        self.robot.set_filters(
292            joint_position_filter_frequency,
293            joint_velocity_filter_frequency,
294            cartesian_position_filter_frequency,
295            cartesian_velocity_filter_frequency,
296            controller_filter_frequency,
297        )
298    }
299
300    /// Returns the parameters of the virtual wall with the given `id`
301    /// (`Robot::getVirtualWall`).
302    ///
303    /// # Errors
304    /// [`FrankaError::InvalidOperation`] on FCI v10, which dropped the command;
305    /// [`FrankaError::Command`] if the robot rejected it.
306    pub fn virtual_wall(&self, id: i32) -> FrankaResult<VirtualWallCuboid> {
307        self.robot.virtual_wall(id)
308    }
309
310    /// Stops all currently running motions.
311    ///
312    /// Can be called from a second thread while a control loop is running; the control loop
313    /// then fails with a [`FrankaError::Control`] carrying
314    /// `"libfranka: Move command preempted!"`.
315    ///
316    /// # Errors
317    /// [`FrankaError::Command`] if the robot rejected the `StopMove`.
318    pub fn stop(&self) -> FrankaResult<()> {
319        self.robot.stop()
320    }
321
322    /// Runs automatic error recovery on the robot, clearing the errors of a reflex or a
323    /// collision so that a new motion can be started.
324    ///
325    /// # Errors
326    /// [`FrankaError::Command`] if the robot rejected the command, e.g. because manual error
327    /// recovery is required.
328    pub fn automatic_error_recovery(&self) -> FrankaResult<()> {
329        self.robot.automatic_error_recovery()
330    }
331
332    /// Changes the collision behavior.
333    ///
334    /// Set separate torque and force boundaries for acceleration/deceleration and constant
335    /// velocity movement phases. Forces or torques between lower and upper threshold are shown
336    /// as contacts in the robot state; above the upper threshold the robot stops and enters an
337    /// error state.
338    ///
339    /// # Errors
340    /// [`FrankaError::Command`] if the robot rejected the command.
341    #[allow(clippy::too_many_arguments)]
342    pub fn set_collision_behavior(
343        &self,
344        lower_torque_thresholds_acceleration: [f64; 7],
345        upper_torque_thresholds_acceleration: [f64; 7],
346        lower_torque_thresholds_nominal: [f64; 7],
347        upper_torque_thresholds_nominal: [f64; 7],
348        lower_force_thresholds_acceleration: [f64; 6],
349        upper_force_thresholds_acceleration: [f64; 6],
350        lower_force_thresholds_nominal: [f64; 6],
351        upper_force_thresholds_nominal: [f64; 6],
352    ) -> FrankaResult<()> {
353        self.robot.set_collision_behavior(
354            &lower_torque_thresholds_acceleration,
355            &upper_torque_thresholds_acceleration,
356            &lower_torque_thresholds_nominal,
357            &upper_torque_thresholds_nominal,
358            &lower_force_thresholds_acceleration,
359            &upper_force_thresholds_acceleration,
360            &lower_force_thresholds_nominal,
361            &upper_force_thresholds_nominal,
362        )
363    }
364
365    /// [`Robot::set_collision_behavior`] with the same thresholds for the acceleration and the
366    /// constant velocity phase.
367    pub fn set_collision_behavior_simple(
368        &self,
369        lower_torque_thresholds: [f64; 7],
370        upper_torque_thresholds: [f64; 7],
371        lower_force_thresholds: [f64; 6],
372        upper_force_thresholds: [f64; 6],
373    ) -> FrankaResult<()> {
374        self.robot.set_collision_behavior(
375            &lower_torque_thresholds,
376            &upper_torque_thresholds,
377            &lower_torque_thresholds,
378            &upper_torque_thresholds,
379            &lower_force_thresholds,
380            &upper_force_thresholds,
381            &lower_force_thresholds,
382            &upper_force_thresholds,
383        )
384    }
385
386    /// Sets the impedance for each joint in the internal controller \[Nm/rad\].
387    ///
388    /// User-provided torques are not affected by this setting.
389    pub fn set_joint_impedance(&self, K_theta: [f64; 7]) -> FrankaResult<()> {
390        self.robot.set_joint_impedance(&K_theta)
391    }
392
393    /// Sets the Cartesian impedance for (x, y, z, roll, pitch, yaw) in the internal controller.
394    ///
395    /// User-provided torques are not affected by this setting.
396    pub fn set_cartesian_impedance(&self, K_x: [f64; 6]) -> FrankaResult<()> {
397        self.robot.set_cartesian_impedance(&K_x)
398    }
399
400    /// Locks or unlocks guiding mode movement in (x, y, z, roll, pitch, yaw).
401    ///
402    /// If `elbow` is `true` the elbow is locked and the flag for the 3rd and 5th joint is
403    /// ignored.
404    pub fn set_guiding_mode(&self, guiding_mode: [bool; 6], elbow: bool) -> FrankaResult<()> {
405        self.robot.set_guiding_mode(&guiding_mode, elbow)
406    }
407
408    /// Sets the transformation from the end effector frame `EE` to the stiffness frame `K`,
409    /// column-major.
410    pub fn set_k(&self, EE_T_K: [f64; 16]) -> FrankaResult<()> {
411        self.robot.set_k(&EE_T_K)
412    }
413
414    /// Sets the transformation from the nominal end effector frame `NE` to the end effector
415    /// frame `EE`, column-major.
416    ///
417    /// The transformation from flange to `NE` is set in Desk.
418    pub fn set_ee(&self, NE_T_EE: [f64; 16]) -> FrankaResult<()> {
419        self.robot.set_ee(&NE_T_EE)
420    }
421
422    /// Sets dynamic parameters of a payload: mass in \[kg\], centre of mass in the flange
423    /// frame and the inertia matrix with respect to the centre of mass, column-major.
424    ///
425    /// This is not for setting end effector parameters, which have to be set in the
426    /// administrator's interface.
427    pub fn set_load(
428        &self,
429        load_mass: f64,
430        F_x_Cload: [f64; 3],
431        load_inertia: [f64; 9],
432    ) -> FrankaResult<()> {
433        self.robot.set_load(load_mass, &F_x_Cload, &load_inertia)
434    }
435
436    /// Position-dependent upper joint velocity limits at `q`, as used by the rate limiter.
437    pub fn upper_joint_velocity_limits(&self, q: &[f64; NUM_JOINTS]) -> [f64; NUM_JOINTS] {
438        self.robot.upper_joint_velocity_limits(q)
439    }
440
441    /// Position-dependent lower joint velocity limits at `q`, as used by the rate limiter.
442    pub fn lower_joint_velocity_limits(&self, q: &[f64; NUM_JOINTS]) -> [f64; NUM_JOINTS] {
443        self.robot.lower_joint_velocity_limits(q)
444    }
445
446    /// Takes the control lock, or reports that another operation owns it
447    /// (`franka::assertOwningLock`).
448    fn acquire_control_lock(&self) -> FrankaResult<MutexGuard<'_, ()>> {
449        match self.control_lock.try_lock() {
450            Ok(guard) => Ok(guard),
451            // A panic in a previous control loop leaves the mutex poisoned; the data it guards
452            // is `()`, so recovering is always safe.
453            Err(TryLockError::Poisoned(poisoned)) => Ok(poisoned.into_inner()),
454            Err(TryLockError::WouldBlock) => Err(FrankaError::InvalidOperation(
455                CONTROL_LOCK_MESSAGE.to_string(),
456            )),
457        }
458    }
459}