Skip to main content

franka/
realtime.rs

1//! Realtime scheduling helpers — a port of `src/control_tools.cpp` (libfranka 0.21.2).
2//!
3//! A 1 kHz FCI control loop needs `SCHED_FIFO` and a `PREEMPT_RT` kernel to keep its deadline.
4//! libfranka checks both before starting a motion; [`RealtimeConfig`] selects whether this
5//! crate does the same.
6
7/// Whether a control loop insists on realtime scheduling (mirrors `franka::RealtimeConfig`).
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum RealtimeConfig {
10    /// Require a realtime kernel and raise the control thread to the highest `SCHED_FIFO`
11    /// priority; fail with [`crate::error::FrankaError::Realtime`] if either is impossible.
12    /// This is libfranka's default.
13    #[default]
14    Enforce,
15    /// Run anyway on a non-realtime kernel or without the privileges to change the scheduler.
16    ///
17    /// Useful against franka-sim and for exploratory work; the loop will miss cycles under
18    /// load, which the robot reports as a falling `control_command_success_rate` and,
19    /// eventually, a `communication_constraints_violation` reflex.
20    Ignore,
21}
22
23/// Whether the running kernel advertises realtime capabilities.
24///
25/// Port of `franka::hasRealtimeKernel`: read `/sys/kernel/realtime` and parse it as a boolean
26/// the way `std::istream >> bool` does, i.e. `1` is true and everything else is false.
27pub fn has_realtime_kernel() -> bool {
28    match std::fs::read_to_string("/sys/kernel/realtime") {
29        Ok(contents) => contents.trim() == "1",
30        Err(_) => false,
31    }
32}
33
34/// Raises the calling thread to the highest `SCHED_FIFO` priority.
35///
36/// Port of `franka::setCurrentThreadToHighestSchedulerPriority`; the `Err` strings are
37/// libfranka's, verbatim.
38pub fn set_current_thread_to_highest_scheduler_priority() -> Result<(), String> {
39    let thread_priority = unsafe { libc::sched_get_priority_max(libc::SCHED_FIFO) };
40    if thread_priority == -1 {
41        return Err(format!(
42            "libfranka: unable to get maximum possible thread priority: {}",
43            strerror(errno())
44        ));
45    }
46    set_current_thread_scheduler_priority(thread_priority)
47}
48
49/// Puts the calling thread on `SCHED_FIFO` at `priority` (1 to 99 on Linux).
50///
51/// The generalisation of [`set_current_thread_to_highest_scheduler_priority`], which is
52/// this at `sched_get_priority_max(SCHED_FIFO)`. A lower priority is what a program that
53/// runs other realtime threads (a robot-side driver, a second arm) gives a
54/// [`crate::robot::target_control`] loop; the `Err` text is libfranka's.
55pub fn set_current_thread_scheduler_priority(priority: i32) -> Result<(), String> {
56    let thread_priority = priority;
57    // `libc::sched_param` carries extra `SCHED_SPORADIC` fields on musl that glibc's
58    // `sched_param` doesn't have; zero-initializing the rest keeps this portable across libc
59    // flavors (e.g. cross-building for aarch64-unknown-linux-musl). On glibc, where
60    // `sched_priority` is the only field, clippy reads the update as redundant.
61    #[allow(clippy::needless_update)]
62    let param = libc::sched_param {
63        sched_priority: thread_priority,
64        ..unsafe { std::mem::zeroed() }
65    };
66    let rc = unsafe { libc::pthread_setschedparam(libc::pthread_self(), libc::SCHED_FIFO, &param) };
67    if rc != 0 {
68        // libfranka formats `std::strerror(errno)` here even though `pthread_setschedparam`
69        // reports its failure through the return value. glibc's implementation does set errno
70        // as well, but we fall back to the return code so the message can never read
71        // "Success".
72        let code = match errno() {
73            0 => rc,
74            e => e,
75        };
76        return Err(format!(
77            "libfranka: unable to set realtime scheduling: {}",
78            strerror(code)
79        ));
80    }
81    Ok(())
82}
83
84/// Error text libfranka raises when `RealtimeConfig::Enforce` meets a non-realtime kernel
85/// (`franka::Robot`'s constructor).
86pub const NO_REALTIME_KERNEL_MESSAGE: &str =
87    "libfranka: Running kernel does not have realtime capabilities.";
88
89/// Checks the realtime prerequisites for `config`, returning libfranka's error text.
90///
91/// With [`RealtimeConfig::Enforce`] this is the check `franka::Robot::Robot` performs before
92/// connecting; with [`RealtimeConfig::Ignore`] it always succeeds.
93pub fn check_realtime(config: RealtimeConfig) -> crate::error::FrankaResult<()> {
94    if config == RealtimeConfig::Enforce && !has_realtime_kernel() {
95        return Err(crate::error::FrankaError::Realtime(
96            NO_REALTIME_KERNEL_MESSAGE.to_string(),
97        ));
98    }
99    Ok(())
100}
101
102fn errno() -> i32 {
103    std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
104}
105
106/// `std::strerror`, so the messages are byte-identical to libfranka's.
107fn strerror(code: i32) -> String {
108    unsafe {
109        std::ffi::CStr::from_ptr(libc::strerror(code))
110            .to_string_lossy()
111            .into_owned()
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn realtime_kernel_detection_matches_sysfs() {
121        let expected = std::fs::read_to_string("/sys/kernel/realtime")
122            .map(|c| c.trim() == "1")
123            .unwrap_or(false);
124        assert_eq!(has_realtime_kernel(), expected);
125    }
126
127    #[test]
128    fn enforce_fails_without_a_realtime_kernel() {
129        assert!(check_realtime(RealtimeConfig::Ignore).is_ok());
130        if has_realtime_kernel() {
131            assert!(check_realtime(RealtimeConfig::Enforce).is_ok());
132        } else {
133            let error = check_realtime(RealtimeConfig::Enforce).unwrap_err();
134            assert_eq!(
135                error.to_string(),
136                "libfranka: Running kernel does not have realtime capabilities."
137            );
138        }
139    }
140
141    #[test]
142    fn highest_scheduler_priority_reports_libfranka_text_on_failure() {
143        // On this machine `RLIMIT_RTPRIO` may or may not allow SCHED_FIFO for an unprivileged
144        // process, so both outcomes are valid; only the failure text is fixed by libfranka.
145        // The call runs on a dedicated thread so that a success cannot leave the test harness
146        // scheduled SCHED_FIFO.
147        let result = std::thread::spawn(set_current_thread_to_highest_scheduler_priority)
148            .join()
149            .unwrap();
150        if let Err(error) = result {
151            assert!(
152                error.starts_with("libfranka: unable to set realtime scheduling: ")
153                    || error
154                        .starts_with("libfranka: unable to get maximum possible thread priority: "),
155                "unexpected message: {error}"
156            );
157        }
158    }
159}