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}