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, ¶m) };
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}