piper_client/control/
loop_runner.rs1use super::controller::Controller;
31use crate::Piper;
32use crate::state::{Active, MitMode};
33use crate::types::RobotError;
34use std::time::{Duration, Instant};
35
36#[derive(Debug, Clone)]
38pub struct LoopConfig {
39 pub frequency_hz: f64,
43
44 pub dt_clamp_multiplier: f64,
50
51 pub max_iterations: Option<usize>,
55}
56
57impl Default for LoopConfig {
58 fn default() -> Self {
59 LoopConfig {
60 frequency_hz: 100.0, dt_clamp_multiplier: 2.0, max_iterations: None, }
64 }
65}
66
67pub fn run_controller<C>(
112 piper: Piper<Active<MitMode>>,
113 mut controller: C,
114 config: LoopConfig,
115) -> Result<(), RobotError>
116where
117 C: Controller,
118 RobotError: From<C::Error>,
119{
120 if config.frequency_hz <= 0.0 {
122 return Err(RobotError::ConfigError(format!(
123 "Invalid frequency_hz: {} (must be > 0)",
124 config.frequency_hz
125 )));
126 }
127 if config.frequency_hz > 10000.0 {
128 tracing::warn!(
129 "Very high control frequency: {} Hz. This may cause performance issues.",
130 config.frequency_hz
131 );
132 }
133 if config.dt_clamp_multiplier <= 0.0 {
134 return Err(RobotError::ConfigError(format!(
135 "Invalid dt_clamp_multiplier: {} (must be > 0)",
136 config.dt_clamp_multiplier
137 )));
138 }
139
140 let nominal_period = Duration::from_secs_f64(1.0 / config.frequency_hz);
142 let max_dt = nominal_period.mul_f64(config.dt_clamp_multiplier);
143
144 let mut last_time = Instant::now();
145 let mut iteration = 0;
146
147 loop {
148 if let Some(max_iter) = config.max_iterations
150 && iteration >= max_iter
151 {
152 return Ok(());
153 }
154
155 let now = Instant::now();
157 let real_dt = now - last_time;
158 let mut dt = real_dt;
159
160 if real_dt > max_dt {
162 controller.on_time_jump(real_dt).map_err(RobotError::from)?;
164
165 dt = max_dt;
167 }
168
169 let current = piper.observer().joint_positions();
171
172 let torques = controller.tick(¤t, dt).map_err(RobotError::from)?;
174
175 let zero_positions = crate::types::JointArray::from([crate::types::Rad(0.0); 6]);
177 let zero_velocities = crate::types::JointArray::from([0.0; 6]);
178 let zero_kp = crate::types::JointArray::from([0.0; 6]);
179 let zero_kd = crate::types::JointArray::from([0.0; 6]);
180 piper.command_torques(
181 &zero_positions,
182 &zero_velocities,
183 &zero_kp,
184 &zero_kd,
185 &torques,
186 )?;
187
188 last_time = now;
190 iteration += 1;
191
192 std::thread::sleep(nominal_period);
194 }
195}
196
197pub fn run_controller_spin<C>(
203 piper: Piper<Active<MitMode>>,
204 mut controller: C,
205 config: LoopConfig,
206) -> Result<(), RobotError>
207where
208 C: Controller,
209 RobotError: From<C::Error>,
210{
211 use spin_sleep::SpinSleeper;
212
213 if config.frequency_hz <= 0.0 {
215 return Err(RobotError::ConfigError(format!(
216 "Invalid frequency_hz: {} (must be > 0)",
217 config.frequency_hz
218 )));
219 }
220 if config.frequency_hz > 10000.0 {
221 tracing::warn!(
222 "Very high control frequency: {} Hz. This may cause performance issues.",
223 config.frequency_hz
224 );
225 }
226 if config.dt_clamp_multiplier <= 0.0 {
227 return Err(RobotError::ConfigError(format!(
228 "Invalid dt_clamp_multiplier: {} (must be > 0)",
229 config.dt_clamp_multiplier
230 )));
231 }
232
233 let nominal_period = Duration::from_secs_f64(1.0 / config.frequency_hz);
234 let max_dt = nominal_period.mul_f64(config.dt_clamp_multiplier);
235 let sleeper = SpinSleeper::default();
236
237 let mut last_time = Instant::now();
238 let mut iteration = 0;
239
240 loop {
241 if let Some(max_iter) = config.max_iterations
242 && iteration >= max_iter
243 {
244 return Ok(());
245 }
246
247 let now = Instant::now();
248 let real_dt = now - last_time;
249 let mut dt = real_dt;
250
251 if real_dt > max_dt {
252 controller.on_time_jump(real_dt).map_err(RobotError::from)?;
253 dt = max_dt;
254 }
255
256 let current = piper.observer().joint_positions();
257 let torques = controller.tick(¤t, dt).map_err(RobotError::from)?;
258
259 let zero_positions = crate::types::JointArray::from([crate::types::Rad(0.0); 6]);
260 let zero_velocities = crate::types::JointArray::from([0.0; 6]);
261 let zero_kp = crate::types::JointArray::from([0.0; 6]);
262 let zero_kd = crate::types::JointArray::from([0.0; 6]);
263 piper.command_torques(
264 &zero_positions,
265 &zero_velocities,
266 &zero_kp,
267 &zero_kd,
268 &torques,
269 )?;
270
271 last_time = now;
272 iteration += 1;
273
274 sleeper.sleep(nominal_period);
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_loop_config_default() {
284 let config = LoopConfig::default();
285 assert_eq!(config.frequency_hz, 100.0);
286 assert_eq!(config.dt_clamp_multiplier, 2.0);
287 assert_eq!(config.max_iterations, None);
288 }
289
290 #[test]
291 fn test_loop_config_custom() {
292 let config = LoopConfig {
293 frequency_hz: 200.0,
294 dt_clamp_multiplier: 1.5,
295 max_iterations: Some(1000),
296 };
297 assert_eq!(config.frequency_hz, 200.0);
298 assert_eq!(config.dt_clamp_multiplier, 1.5);
299 assert_eq!(config.max_iterations, Some(1000));
300 }
301
302 #[test]
303 fn test_invalid_frequency() {
304 let config = LoopConfig {
305 frequency_hz: -1.0,
306 ..Default::default()
307 };
308 assert_eq!(config.frequency_hz, -1.0);
311 }
312}