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}