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}