Skip to main content

franka/model/
model_library.rs

1//! Downloading the FCI v5 model library from the robot (`LoadModelLibrary`).
2//!
3//! Port of `franka::LibraryDownloader` (`src/library_downloader.{h,cpp}`,
4//! libfranka 0.9.2). On FCI v5 the robot has no URDF to hand out; instead it
5//! serves a *compiled* model for the architecture and operating system the
6//! client asks for, and libfranka `dlopen`s it. The download is a single
7//! command on the ordinary TCP command socket:
8//!
9//! * request: `u8 architecture, u8 system` (2 bytes),
10//! * response: `u8 status`, followed by the shared object as the message tail
11//!   (`header.size - 13` bytes: the 12-byte command header plus the status byte
12//!   are subtracted).
13//!
14//! [`load_from_robot`] performs that exchange and hands the bytes to
15//! [`SoModelBackend::from_bytes`]; the offline entry points
16//! [`crate::model::Model::from_model_library_bytes`] and
17//! [`crate::model::Model::from_model_library_path`] skip the network.
18
19#[cfg(feature = "model-library")]
20use std::path::Path;
21
22#[cfg(feature = "model-library")]
23use zerocopy::IntoBytes;
24
25use crate::error::{FrankaError, FrankaResult};
26#[cfg(feature = "model-library")]
27use crate::model::so_backend::SoModelBackend;
28use crate::model::Model;
29use crate::network::Network;
30use crate::wire::robot::codec::{command_id, CommandKind, FciVersion};
31#[cfg(feature = "model-library")]
32use crate::wire::robot::v5;
33#[cfg(feature = "model-library")]
34use crate::wire::{message_payload, parse_response, HeaderLayout};
35
36/// The wire command id of `LoadModelLibrary` under `version`.
37///
38/// # Errors
39///
40/// [`FrankaError::InvalidOperation`] on a version that has no such command,
41/// i.e. FCI v10, whose FR3 serves a URDF through `GetRobotModel` instead.
42fn load_model_library_command(version: FciVersion) -> FrankaResult<u32> {
43    command_id(version, CommandKind::LoadModelLibrary).ok_or_else(|| {
44        FrankaError::InvalidOperation(format!(
45            "libfranka: {} is not available on FCI version {}.",
46            CommandKind::LoadModelLibrary.name(),
47            version.number()
48        ))
49    })
50}
51
52/// The `LoadModelLibrary::Architecture` this build asks the robot for.
53///
54/// libfranka picks the value from the `LIBFRANKA_X64` / `LIBFRANKA_X86` /
55/// `LIBFRANKA_ARM64` / `LIBFRANKA_ARM` macros its CMake sets from the target
56/// processor (`src/platform.h`, `library_downloader.cpp`); `cfg!(target_arch)`
57/// is the direct equivalent.
58///
59/// # Errors
60///
61/// [`FrankaError::Model`] with libfranka's own
62/// `"libfranka: Unsupported architecture!"` on any other target.
63#[cfg(feature = "model-library")]
64fn architecture() -> FrankaResult<v5::LoadModelLibraryArchitecture> {
65    if cfg!(target_arch = "x86_64") {
66        Ok(v5::LoadModelLibraryArchitecture::X64)
67    } else if cfg!(target_arch = "x86") {
68        Ok(v5::LoadModelLibraryArchitecture::X86)
69    } else if cfg!(target_arch = "aarch64") {
70        Ok(v5::LoadModelLibraryArchitecture::ARM64)
71    } else if cfg!(target_arch = "arm") {
72        Ok(v5::LoadModelLibraryArchitecture::ARM)
73    } else {
74        Err(FrankaError::Model(
75            "libfranka: Unsupported architecture!".to_string(),
76        ))
77    }
78}
79
80/// The `LoadModelLibrary::System` this build asks the robot for.
81///
82/// # Errors
83///
84/// [`FrankaError::Model`] with libfranka's own
85/// `"libfranka: Unsupported operating system!"` on any other target.
86#[cfg(feature = "model-library")]
87fn system() -> FrankaResult<v5::LoadModelLibrarySystem> {
88    if cfg!(target_os = "linux") {
89        Ok(v5::LoadModelLibrarySystem::Linux)
90    } else if cfg!(target_os = "windows") {
91        Ok(v5::LoadModelLibrarySystem::Windows)
92    } else {
93        Err(FrankaError::Model(
94            "libfranka: Unsupported operating system!".to_string(),
95        ))
96    }
97}
98
99/// Downloads the robot's model library over `network` and builds a [`Model`].
100///
101/// Port of `franka::Model::Model(Network&)` (0.9.2), which is
102/// `ModelLibrary(network)` and therefore `LibraryDownloader(network)` followed
103/// by `LibraryLoader`.
104///
105/// # Temp-file lifecycle
106///
107/// The downloaded bytes are written to a uniquely named, mode `0600` file under
108/// [`std::env::temp_dir`] (libfranka: `Poco::TemporaryFile::tempName()`), which
109/// is `dlopen`ed and then **removed when the returned [`Model`] is dropped** —
110/// the file guard lives inside the [`SoModelBackend`] the model owns. The guard
111/// covers a normal drop and an unwind, but not `SIGINT`/`SIGTERM`,
112/// `std::process::abort` or `std::process::exit`; a control loop killed with
113/// Ctrl-C therefore leaves one ~330 KB file behind in the temp directory. Two
114/// concurrent downloads (in the same process or in different ones) cannot
115/// collide on the name.
116///
117/// # Platform limitation
118///
119/// The robot serves a *native* shared object, so this only works where the
120/// robot has a build for the current `(architecture, system)` pair and where
121/// the running process can load it. The request encodes whatever the host is
122/// (`X64`/`X86`/`ARM64`/`ARM` and Linux/Windows, from `cfg!`), but in practice
123/// only **x86-64 Linux**
124/// works: the FER control unit ships `libfcimodels_x64.so`. Cross-compiled or
125/// unusual targets get [`FrankaError::Model`] from the robot
126/// (`"libfranka: Server reports error when loading model library."`) or from
127/// `dlopen`.
128///
129/// # Errors
130///
131/// * [`FrankaError::InvalidOperation`] when `version` is not
132///   [`FciVersion::V5`]: FCI v10 has no `LoadModelLibrary` command — an FR3
133///   serves a URDF through `GetRobotModel` instead.
134/// * [`FrankaError::Model`] when the target architecture or operating system
135///   has no `LoadModelLibrary` encoding, when the robot answers
136///   `Status::kError`, or when the shared object cannot be saved, loaded or
137///   bound.
138/// * [`FrankaError::Network`] / [`FrankaError::Protocol`] for the usual
139///   command-socket failures.
140#[cfg(feature = "model-library")]
141pub fn load_from_robot(network: &Network, version: FciVersion) -> FrankaResult<Model> {
142    let command = load_model_library_command(version)?;
143
144    let request = v5::LoadModelLibraryRequest::new(architecture()?, system()?);
145    let command_id = network.tcp.send_request(command, request.as_bytes())?;
146    let message = network.tcp.blocking_receive_response(command_id)?;
147    let response: v5::LoadModelLibraryResponse = parse_response(HeaderLayout::Robot, &message)?;
148
149    // libfranka compares against `kSuccess` and reports one message for every other value,
150    // including bytes that are not valid `Status` enumerators.
151    if response.status != v5::LoadModelLibraryStatus::Success.to_u8() {
152        return Err(FrankaError::Model(
153            "libfranka: Server reports error when loading model library.".to_string(),
154        ));
155    }
156
157    // The tail is everything after the 12-byte header and the status byte, i.e.
158    // `header.size - 13` bytes; `message_payload` strips the header and the `[1..]` the
159    // status. `parse_response` above has already established that the payload is non-empty.
160    let library_bytes = &message_payload(HeaderLayout::Robot, &message)[1..];
161    if library_bytes.is_empty() {
162        return Err(FrankaError::Protocol(
163            "libfranka: Incorrect TCP message size.".to_string(),
164        ));
165    }
166    // SAFETY: `library_bytes` is the `libfcimodels` build the connected robot served over
167    // the FCI command socket, and `dlopen`ing it executes its code. That is the trust
168    // boundary libfranka 0.9.2 sits on (`LibraryDownloader` + `LibraryLoader`): the FCI
169    // peer is already fully trusted, because it is the thing that commands the arm, and a
170    // caller unwilling to extend that trust disables the `model-library` feature. See the
171    // `# Security` section on `Robot::load_model`.
172    unsafe { Model::from_model_library_bytes(library_bytes) }
173}
174
175/// [`load_from_robot`] as compiled **without** the `model-library` feature.
176///
177/// The signature is identical in both configurations so that callers
178/// (`RobotImpl::load_model`) need no `cfg` of their own; the version check still
179/// runs first, so an FCI v10 caller gets the same
180/// [`FrankaError::InvalidOperation`] either way, and an FCI v5 caller gets a
181/// [`FrankaError::Model`] explaining that this build cannot load one. No bytes
182/// are requested from the robot, because there would be nothing to `dlopen`
183/// them with.
184#[cfg(not(feature = "model-library"))]
185pub fn load_from_robot(_network: &Network, version: FciVersion) -> FrankaResult<Model> {
186    load_model_library_command(version)?;
187    Err(FrankaError::Model(
188        "libfranka: this build of franka-rs was compiled without the `model-library` \
189         feature, so the FCI v5 model library cannot be loaded."
190            .to_string(),
191    ))
192}
193
194#[cfg(feature = "model-library")]
195impl Model {
196    /// Builds a model from the bytes of a `libfcimodels` shared object.
197    ///
198    /// The offline twin of [`load_from_robot`]: same temp-file lifecycle (a
199    /// `0600` file under [`std::env::temp_dir`], removed when the model is
200    /// dropped), no network. Useful for a captured library and for tests.
201    ///
202    /// # Safety
203    ///
204    /// The bytes are written to a temporary file and `dlopen`ed, which executes
205    /// the shared object's initialisers and, afterwards, its exported entry
206    /// points in this process. The caller asserts that `bytes` are a **trusted**
207    /// `libfcimodels` build for the current platform — served by a Franka
208    /// control unit, or captured from one and kept where only trusted
209    /// principals can write. See [`SoModelBackend::from_bytes`].
210    ///
211    /// # Errors
212    ///
213    /// [`FrankaError::Model`] when the file cannot be written, `dlopen`ed, or
214    /// when any of the thirty `libfcimodels` symbols is missing.
215    pub unsafe fn from_model_library_bytes(bytes: &[u8]) -> FrankaResult<Model> {
216        // SAFETY: delegated to this function's own contract.
217        let backend = unsafe { SoModelBackend::from_bytes(bytes)? };
218        Ok(Model::from_backend(Box::new(backend)))
219    }
220
221    /// Builds a model from a `libfcimodels` shared object already on disk.
222    ///
223    /// Unlike [`Model::from_model_library_bytes`] the file is left alone when
224    /// the model is dropped.
225    ///
226    /// # Safety
227    ///
228    /// The file is `dlopen`ed, which executes its code in this process. The
229    /// caller asserts that `path` names a **trusted** `libfcimodels` build for
230    /// the current platform and that it cannot be swapped before the load. See
231    /// [`SoModelBackend::open`].
232    ///
233    /// # Errors
234    ///
235    /// [`FrankaError::Model`] when the file cannot be `dlopen`ed or when any of
236    /// the thirty `libfcimodels` symbols is missing.
237    pub unsafe fn from_model_library_path(path: &Path) -> FrankaResult<Model> {
238        // SAFETY: delegated to this function's own contract.
239        let backend = unsafe { SoModelBackend::open(path)? };
240        Ok(Model::from_backend(Box::new(backend)))
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn v10_has_no_load_model_library() {
250        // A `Network` is not needed to reach the version check: it is the first thing
251        // `load_from_robot` does, and `command_id` is a pure function of the version.
252        assert!(command_id(FciVersion::V10, CommandKind::LoadModelLibrary).is_none());
253        assert_eq!(
254            command_id(FciVersion::V5, CommandKind::LoadModelLibrary),
255            Some(13)
256        );
257    }
258
259    #[test]
260    fn the_unavailable_message_matches_the_agreed_wording() {
261        let message = format!(
262            "libfranka: {} is not available on FCI version {}.",
263            CommandKind::LoadModelLibrary.name(),
264            FciVersion::V10.number()
265        );
266        assert_eq!(
267            message,
268            "libfranka: Load Model Library is not available on FCI version 10."
269        );
270    }
271
272    #[cfg(feature = "model-library")]
273    #[test]
274    fn this_build_asks_for_a_supported_platform() {
275        // The CI and development target is x86-64 Linux, which is the only pair the FER
276        // actually serves; the point of the assertion is that the mapping is wired up.
277        if cfg!(all(target_arch = "x86_64", target_os = "linux")) {
278            assert_eq!(
279                architecture().unwrap(),
280                v5::LoadModelLibraryArchitecture::X64
281            );
282            assert_eq!(system().unwrap(), v5::LoadModelLibrarySystem::Linux);
283        }
284    }
285}