1use crate::errors;
2use crate::math::FlightFloat;
3use crate::packets::*;
4use crate::params::{ParamId, ParamValue, Params};
5use crate::{log_error, log_info};
6use bitflags::bitflags;
7
8fn deg_to_rad<R: FlightFloat>() -> R {
9 <R as FlightFloat>::from_f32(0.017453293)
10}
11
12bitflags! {
13 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14 pub struct CalibrationFlags: u16 {
15 const GYRO = 1 << 0;
16 const ACCEL = 1 << 1;
17 const BARO = 1 << 2;
18 const PITOT = 1 << 3;
19 const GYRO_FAILED = 1 << 4;
20 const ACCEL_FAILED = 1 << 5;
21 const BARO_FAILED = 1 << 6;
22 const PITOT_FAILED = 1 << 7;
23
24 const IMU = Self::GYRO.bits() | Self::ACCEL.bits();
26 }
27}
28
29pub trait SensorPacketProcessor<P> {
30 fn process(
31 &mut self,
32 packet: &mut Option<Result<P, errors::SensorError>>,
33 flags: &mut CalibrationFlags,
34 params: &mut Params,
35 ) -> Option<P>;
36
37 fn process_at(
38 &mut self,
39 packet: &mut Option<Result<P, errors::SensorError>>,
40 flags: &mut CalibrationFlags,
41 params: &mut Params,
42 _now_ms: u32,
43 ) -> Option<P> {
44 self.process(packet, flags, params)
45 }
46}
47
48fn take_ok_packet<P>(packet: &mut Option<Result<P, errors::SensorError>>) -> Option<P> {
49 match packet.take() {
50 Some(Ok(packet)) => Some(packet),
51 _ => None,
52 }
53}
54
55macro_rules! impl_passthrough_sensor_packet_processor {
56 ($processor:ty, $packet:ty) => {
57 impl SensorPacketProcessor<$packet> for $processor {
58 fn process(
59 &mut self,
60 packet: &mut Option<Result<$packet, errors::SensorError>>,
61 _flags: &mut CalibrationFlags,
62 _params: &mut Params,
63 ) -> Option<$packet> {
64 take_ok_packet(packet)
65 }
66 }
67 };
68}
69
70#[derive(Default, Copy, Clone)]
75pub struct PassthroughBatteryProcessor;
76impl_passthrough_sensor_packet_processor!(PassthroughBatteryProcessor, BatteryPacket);
77
78#[derive(Copy, Clone)]
79pub struct BatteryProcessor {
80 previous_voltage: f32,
81 previous_current: f32,
82 latest_packet: Option<BatteryPacket>,
83 initialized: bool,
84}
85
86impl Default for BatteryProcessor {
87 fn default() -> Self {
88 Self {
89 previous_voltage: 0.0,
90 previous_current: 0.0,
91 latest_packet: None,
92 initialized: false,
93 }
94 }
95}
96
97impl BatteryProcessor {
98 fn process_packet(
99 &mut self,
100 packet: &mut Option<Result<BatteryPacket, errors::SensorError>>,
101 _flags: &mut CalibrationFlags,
102 params: &mut Params,
103 ) -> Option<BatteryPacket> {
104 let Some(mut packet) = take_ok_packet(packet) else {
105 return self.latest_packet;
106 };
107
108 if !self.initialized {
109 self.previous_voltage = param_float(params, ParamId::PARAM_VOLT_MAX);
110 self.previous_current = 0.0;
111 self.initialized = true;
112 }
113
114 let voltage_alpha = param_float(params, ParamId::PARAM_BATTERY_VOLTAGE_ALPHA);
115 let current_alpha = param_float(params, ParamId::PARAM_BATTERY_CURRENT_ALPHA);
116
117 packet.voltage =
118 packet.voltage * (1.0 - voltage_alpha) + self.previous_voltage * voltage_alpha;
119 packet.current =
120 packet.current * (1.0 - current_alpha) + self.previous_current * current_alpha;
121
122 self.previous_voltage = packet.voltage;
123 self.previous_current = packet.current;
124 self.latest_packet = Some(packet);
125
126 Some(packet)
127 }
128}
129
130impl SensorPacketProcessor<BatteryPacket> for BatteryProcessor {
131 fn process(
132 &mut self,
133 packet: &mut Option<Result<BatteryPacket, errors::SensorError>>,
134 flags: &mut CalibrationFlags,
135 params: &mut Params,
136 ) -> Option<BatteryPacket> {
137 self.process_packet(packet, flags, params)
138 }
139}
140
141#[derive(Default, Copy, Clone)]
146pub struct PassthroughImuProcessor;
147
148impl<R: FlightFloat> SensorPacketProcessor<ImuPacket<R>> for PassthroughImuProcessor {
149 fn process(
150 &mut self,
151 packet: &mut Option<Result<ImuPacket<R>, errors::SensorError>>,
152 _flags: &mut CalibrationFlags,
153 _params: &mut Params,
154 ) -> Option<ImuPacket<R>> {
155 take_ok_packet(packet)
156 }
157}
158
159#[derive(Default, Copy, Clone)]
160pub struct ImuCalibrationState<R: FlightFloat> {
161 gyro_sum: [R; 3],
162 gyro_calibration_count: u16,
163 accel_sum: [R; 3],
164 accel_temp_sum: R,
165 accel_calibration_count: u16,
166 max_accel: [R; 3],
167 min_accel: [R; 3],
168}
169
170#[derive(Copy, Clone)]
171pub struct ImuProcessor<R: FlightFloat> {
172 calibration_state: ImuCalibrationState<R>,
173 mount_rotation: ImuMountRotation<R>,
174}
175
176impl<R: FlightFloat> Default for ImuProcessor<R> {
177 fn default() -> Self {
178 ImuProcessor::new()
179 }
180}
181
182impl<R: FlightFloat> ImuProcessor<R> {
183 pub fn new() -> Self {
184 Self {
185 calibration_state: ImuCalibrationState {
186 max_accel: [<R as FlightFloat>::from_f32(-1000.0); 3],
187 min_accel: [<R as FlightFloat>::from_f32(1000.0); 3],
188 ..Default::default()
189 },
190 mount_rotation: ImuMountRotation::default(),
191 }
192 }
193
194 fn process_packet(
195 &mut self,
196 packet: &mut Option<Result<ImuPacket<R>, errors::SensorError>>,
197 flags: &mut CalibrationFlags,
198 params: &mut Params,
199 ) -> Option<ImuPacket<R>> {
200 if let Some(Ok(mut packet)) = packet.take() {
201 self.mount_rotation.rotate_imu_in_place(&mut packet, params);
202 let is_calibrating = flags.intersects(CalibrationFlags::IMU);
203
204 if is_calibrating {
205 if flags.contains(CalibrationFlags::GYRO) {
206 self.calibration_state.gyro_sum[0] += packet.gyro[0];
207 self.calibration_state.gyro_sum[1] += packet.gyro[1];
208 self.calibration_state.gyro_sum[2] += packet.gyro[2];
209 self.calibration_state.gyro_calibration_count += 1;
210 if self.calibration_state.gyro_calibration_count > 1000 {
211 let count = <R as FlightFloat>::from_u64(
212 self.calibration_state.gyro_calibration_count as u64,
213 );
214 let bias_x = self.calibration_state.gyro_sum[0] / count;
215 let bias_y = self.calibration_state.gyro_sum[1] / count;
216 let bias_z = self.calibration_state.gyro_sum[2] / count;
217
218 if vector_norm([bias_x, bias_y, bias_z]) < <R as FlightFloat>::from_f32(1.0)
219 {
220 params.set_by_id(
221 ParamId::PARAM_GYRO_X_BIAS,
222 ParamValue::Float(bias_x.to_f32_lossy()),
223 );
224 params.set_by_id(
225 ParamId::PARAM_GYRO_Y_BIAS,
226 ParamValue::Float(bias_y.to_f32_lossy()),
227 );
228 params.set_by_id(
229 ParamId::PARAM_GYRO_Z_BIAS,
230 ParamValue::Float(bias_z.to_f32_lossy()),
231 );
232 log_info!("Gyro Calibration complete!");
233 } else {
234 flags.insert(CalibrationFlags::GYRO_FAILED);
235 log_error!("Gyro calibration failed");
236 }
237
238 self.calibration_state.gyro_sum = [<R as FlightFloat>::from_f32(0.0); 3];
239 self.calibration_state.gyro_calibration_count = 0;
240 flags.remove(CalibrationFlags::GYRO);
241 }
242 }
243
244 if flags.contains(CalibrationFlags::ACCEL) {
245 let gravity = <R as FlightFloat>::from_f32(9.80665);
246 self.calibration_state.accel_sum[0] += packet.accel[0];
247 self.calibration_state.accel_sum[1] += packet.accel[1];
248 self.calibration_state.accel_sum[2] += packet.accel[2] + gravity;
249 self.calibration_state.accel_temp_sum +=
250 <R as FlightFloat>::from_f32(packet.temperature);
251 self.calibration_state.accel_calibration_count += 1;
252
253 self.calibration_state.max_accel[0] =
254 self.calibration_state.max_accel[0].max(packet.accel[0]);
255 self.calibration_state.min_accel[0] =
256 self.calibration_state.min_accel[0].min(packet.accel[0]);
257 self.calibration_state.max_accel[1] =
258 self.calibration_state.max_accel[1].max(packet.accel[1]);
259 self.calibration_state.min_accel[1] =
260 self.calibration_state.min_accel[1].min(packet.accel[1]);
261 self.calibration_state.max_accel[2] =
262 self.calibration_state.max_accel[2].max(packet.accel[2]);
263 self.calibration_state.min_accel[2] =
264 self.calibration_state.min_accel[2].min(packet.accel[2]);
265 if self.calibration_state.accel_calibration_count > 1000 {
266 let accel_delta_x = self.calibration_state.max_accel[0]
267 - self.calibration_state.min_accel[0];
268 let accel_delta_y = self.calibration_state.max_accel[1]
269 - self.calibration_state.min_accel[1];
270 let accel_delta_z = self.calibration_state.max_accel[2]
271 - self.calibration_state.min_accel[2];
272 let max_delta = (accel_delta_x * accel_delta_x
273 + accel_delta_y * accel_delta_y
274 + accel_delta_z * accel_delta_z)
275 .sqrt();
276
277 if max_delta < <R as FlightFloat>::from_f32(1.0) {
278 let count = <R as FlightFloat>::from_u64(
279 self.calibration_state.accel_calibration_count as u64,
280 );
281 let temp_comp_x = if let ParamValue::Float(v) =
282 params.get_by_id(ParamId::PARAM_ACC_X_TEMP_COMP)
283 {
284 <R as FlightFloat>::from_f32(v)
285 } else {
286 <R as FlightFloat>::from_f32(0.0)
287 };
288 let temp_comp_y = if let ParamValue::Float(v) =
289 params.get_by_id(ParamId::PARAM_ACC_Y_TEMP_COMP)
290 {
291 <R as FlightFloat>::from_f32(v)
292 } else {
293 <R as FlightFloat>::from_f32(0.0)
294 };
295 let temp_comp_z = if let ParamValue::Float(v) =
296 params.get_by_id(ParamId::PARAM_ACC_Z_TEMP_COMP)
297 {
298 <R as FlightFloat>::from_f32(v)
299 } else {
300 <R as FlightFloat>::from_f32(0.0)
301 };
302
303 let bias_x = (self.calibration_state.accel_sum[0]
304 - temp_comp_x * self.calibration_state.accel_temp_sum)
305 / count;
306 let bias_y = (self.calibration_state.accel_sum[1]
307 - temp_comp_y * self.calibration_state.accel_temp_sum)
308 / count;
309 let bias_z = (self.calibration_state.accel_sum[2]
310 - temp_comp_z * self.calibration_state.accel_temp_sum)
311 / count;
312
313 if vector_norm([bias_x, bias_y, bias_z])
314 < <R as FlightFloat>::from_f32(3.0)
315 {
316 params.set_by_id(
317 ParamId::PARAM_ACC_X_BIAS,
318 ParamValue::Float(bias_x.to_f32_lossy()),
319 );
320 params.set_by_id(
321 ParamId::PARAM_ACC_Y_BIAS,
322 ParamValue::Float(bias_y.to_f32_lossy()),
323 );
324 params.set_by_id(
325 ParamId::PARAM_ACC_Z_BIAS,
326 ParamValue::Float(bias_z.to_f32_lossy()),
327 );
328 log_info!("Accelerometer Calibration Complete!");
329 } else {
330 flags.insert(CalibrationFlags::ACCEL_FAILED);
331 log_error!("Accelerometer calibration failed");
332 }
333 } else {
334 flags.insert(CalibrationFlags::ACCEL_FAILED);
335 log_error!("Accelerometer calibration failed: too much movement");
336 }
337
338 self.calibration_state.accel_sum = [<R as FlightFloat>::from_f32(0.0); 3];
339 self.calibration_state.accel_calibration_count = 0;
340 self.calibration_state.accel_temp_sum = <R as FlightFloat>::from_f32(0.0);
341 self.calibration_state.max_accel =
342 [<R as FlightFloat>::from_f32(-1000.0); 3];
343 self.calibration_state.min_accel =
344 [<R as FlightFloat>::from_f32(1000.0); 3];
345 flags.remove(CalibrationFlags::ACCEL);
346 }
347 }
348 }
349 packet.gyro[0] -=
350 <R as FlightFloat>::from_f32(param_float(params, ParamId::PARAM_GYRO_X_BIAS));
351 packet.gyro[1] -=
352 <R as FlightFloat>::from_f32(param_float(params, ParamId::PARAM_GYRO_Y_BIAS));
353 packet.gyro[2] -=
354 <R as FlightFloat>::from_f32(param_float(params, ParamId::PARAM_GYRO_Z_BIAS));
355
356 let temp = <R as FlightFloat>::from_f32(packet.temperature);
357 packet.accel[0] -=
358 <R as FlightFloat>::from_f32(param_float(params, ParamId::PARAM_ACC_X_TEMP_COMP))
359 * temp
360 + <R as FlightFloat>::from_f32(param_float(params, ParamId::PARAM_ACC_X_BIAS));
361 packet.accel[1] -=
362 <R as FlightFloat>::from_f32(param_float(params, ParamId::PARAM_ACC_Y_TEMP_COMP))
363 * temp
364 + <R as FlightFloat>::from_f32(param_float(params, ParamId::PARAM_ACC_Y_BIAS));
365 packet.accel[2] -=
366 <R as FlightFloat>::from_f32(param_float(params, ParamId::PARAM_ACC_Z_TEMP_COMP))
367 * temp
368 + <R as FlightFloat>::from_f32(param_float(params, ParamId::PARAM_ACC_Z_BIAS));
369 Some(packet)
370 } else {
371 None
372 }
373 }
374}
375
376impl<R: FlightFloat> SensorPacketProcessor<ImuPacket<R>> for ImuProcessor<R> {
377 fn process(
378 &mut self,
379 packet: &mut Option<Result<ImuPacket<R>, errors::SensorError>>,
380 flags: &mut CalibrationFlags,
381 params: &mut Params,
382 ) -> Option<ImuPacket<R>> {
383 self.process_packet(packet, flags, params)
384 }
385}
386
387#[derive(Default, Copy, Clone)]
392pub struct PassthroughBaroProcessor;
393impl_passthrough_sensor_packet_processor!(PassthroughBaroProcessor, BaroPacket);
394
395const SENSOR_CAL_DELAY_CYCLES: u16 = 128;
396const SENSOR_CAL_CYCLES: u16 = 127;
397const BARO_MAX_CALIBRATION_VARIANCE: f32 = 25.0;
398
399#[derive(Default, Copy, Clone)]
400pub struct BaroCalibrationState {
401 mean: f32,
402 m2: f32, count: u16,
404 last_iter_ms: u32,
405 calibrated: bool,
406 request_active: bool,
407}
408
409#[derive(Default, Copy, Clone)]
410pub struct BaroProcessor {
411 calibration_state: BaroCalibrationState,
412}
413
414impl BaroProcessor {
415 pub fn new() -> Self {
416 Self::default()
417 }
418
419 fn process_packet(
420 &mut self,
421 packet: &mut Option<Result<BaroPacket, errors::SensorError>>,
422 flags: &mut CalibrationFlags,
423 params: &mut Params,
424 now_ms: u32,
425 ) -> Option<BaroPacket> {
426 if let Some(Ok(mut packet)) = packet.take() {
427 if flags.contains(CalibrationFlags::BARO) && !self.calibration_state.request_active {
428 self.calibration_state = BaroCalibrationState::default();
429 self.calibration_state.request_active = true;
430 }
431 if !self.calibration_state.calibrated {
432 self.calibrate(&packet, flags, params, now_ms);
433 }
434 packet.altitude = pressure_to_altitude(packet.pressure);
435 Some(packet)
436 } else {
437 None
438 }
439 }
440
441 fn calibrate(
442 &mut self,
443 packet: &BaroPacket,
444 flags: &mut CalibrationFlags,
445 params: &mut Params,
446 now_ms: u32,
447 ) {
448 if now_ms <= self.calibration_state.last_iter_ms + 20 {
449 return;
450 }
451
452 self.calibration_state.count += 1;
453 let total_cycles = SENSOR_CAL_DELAY_CYCLES + SENSOR_CAL_CYCLES;
454
455 if self.calibration_state.count > total_cycles {
456 if self.calibration_state.m2 < BARO_MAX_CALIBRATION_VARIANCE {
457 params.set_by_id(
458 ParamId::PARAM_BARO_BIAS,
459 ParamValue::Float(self.calibration_state.mean),
460 );
461 let ground_alt = pressure_to_altitude(self.calibration_state.mean);
462 params.set_by_id(ParamId::PARAM_GROUND_LEVEL, ParamValue::Float(ground_alt));
463 self.calibration_state.calibrated = true;
464 flags.remove(CalibrationFlags::BARO);
465 self.calibration_state.request_active = false;
466 log_info!("Baro ground pressure cal successful!");
467 } else {
468 flags.insert(CalibrationFlags::BARO_FAILED);
469 log_error!("Too much movement for barometer ground pressure cal");
470 }
471
472 self.calibration_state.mean = 0.0;
473 self.calibration_state.m2 = 0.0;
474 self.calibration_state.count = 0;
475 } else if self.calibration_state.count > SENSOR_CAL_DELAY_CYCLES {
476 let n = (self.calibration_state.count - SENSOR_CAL_DELAY_CYCLES) as f32;
477 let delta = packet.pressure - self.calibration_state.mean;
478 self.calibration_state.mean += delta / n;
479 let delta2 = packet.pressure - self.calibration_state.mean;
480 self.calibration_state.m2 += delta * delta2 / (SENSOR_CAL_CYCLES - 1) as f32;
481 }
482
483 self.calibration_state.last_iter_ms = now_ms;
484 }
485}
486
487fn pressure_to_altitude(pressure: f32) -> f32 {
488 pressure_to_altitude_real::<f32>(pressure)
489}
490
491fn pressure_to_altitude_real<R: FlightFloat>(pressure: R) -> R {
492 <R as FlightFloat>::from_f32(44330.0)
493 * (<R as FlightFloat>::from_f32(1.0)
494 - (pressure / <R as FlightFloat>::from_f32(101325.0))
495 .powf(<R as FlightFloat>::from_f32(0.190295)))
496}
497
498fn indicated_airspeed(dp: f32) -> f32 {
499 indicated_airspeed_real::<f32>(dp)
500}
501
502fn indicated_airspeed_real<R: FlightFloat>(dp: R) -> R {
503 const RHO: f32 = 1.225;
504 (<R as FlightFloat>::from_f32(2.0) * dp.abs() / <R as FlightFloat>::from_f32(RHO)).sqrt()
505 * dp.signum()
506}
507
508impl SensorPacketProcessor<BaroPacket> for BaroProcessor {
509 fn process(
510 &mut self,
511 packet: &mut Option<Result<BaroPacket, errors::SensorError>>,
512 flags: &mut CalibrationFlags,
513 params: &mut Params,
514 ) -> Option<BaroPacket> {
515 let packet_time_ms = match packet.as_ref() {
516 Some(Ok(packet)) => (packet.header.timestamp / 1000) as u32,
517 _ => 0,
518 };
519 self.process_packet(packet, flags, params, packet_time_ms)
520 }
521
522 fn process_at(
523 &mut self,
524 packet: &mut Option<Result<BaroPacket, errors::SensorError>>,
525 flags: &mut CalibrationFlags,
526 params: &mut Params,
527 now_ms: u32,
528 ) -> Option<BaroPacket> {
529 self.process_packet(packet, flags, params, now_ms)
530 }
531}
532
533#[derive(Default, Copy, Clone)]
538pub struct PassthroughPitotProcessor;
539impl_passthrough_sensor_packet_processor!(PassthroughPitotProcessor, PitotPacket);
540
541const PITOT_MAX_CALIBRATION_VARIANCE: f32 = 100.0;
542
543#[derive(Default, Copy, Clone)]
544pub struct PitotCalibrationState {
545 mean: f32,
546 m2: f32,
547 count: u16,
548 last_iter_ms: u32,
549 calibrated: bool,
550 request_active: bool,
551}
552
553#[derive(Default, Copy, Clone)]
554pub struct PitotProcessor {
555 calibration_state: PitotCalibrationState,
556}
557
558impl PitotProcessor {
559 pub fn new() -> Self {
560 Self::default()
561 }
562
563 fn process_packet(
564 &mut self,
565 packet: &mut Option<Result<PitotPacket, errors::SensorError>>,
566 flags: &mut CalibrationFlags,
567 params: &mut Params,
568 now_ms: u32,
569 ) -> Option<PitotPacket> {
570 if let Some(Ok(mut packet)) = packet.take() {
571 if flags.contains(CalibrationFlags::PITOT) && !self.calibration_state.request_active {
572 self.calibration_state = PitotCalibrationState::default();
573 self.calibration_state.request_active = true;
574 }
575 if !self.calibration_state.calibrated {
576 self.calibrate(&packet, flags, params, now_ms);
577 }
578
579 packet.differential_pressure -= param_float(params, ParamId::PARAM_DIFF_PRESS_BIAS);
580
581 let dp = packet.differential_pressure;
582 packet.indicated_airspeed = indicated_airspeed(dp);
583
584 Some(packet)
585 } else {
586 None
587 }
588 }
589
590 fn calibrate(
591 &mut self,
592 packet: &PitotPacket,
593 flags: &mut CalibrationFlags,
594 params: &mut Params,
595 now_ms: u32,
596 ) {
597 if now_ms <= self.calibration_state.last_iter_ms + 20 {
598 return;
599 }
600
601 self.calibration_state.count += 1;
602 let total_cycles = SENSOR_CAL_DELAY_CYCLES + SENSOR_CAL_CYCLES;
603
604 if self.calibration_state.count > total_cycles {
605 if self.calibration_state.m2 < PITOT_MAX_CALIBRATION_VARIANCE {
606 params.set_by_id(
607 ParamId::PARAM_DIFF_PRESS_BIAS,
608 ParamValue::Float(self.calibration_state.mean),
609 );
610 self.calibration_state.calibrated = true;
611 flags.remove(CalibrationFlags::PITOT);
612 self.calibration_state.request_active = false;
613 } else {
614 flags.insert(CalibrationFlags::PITOT_FAILED);
615 log_error!("Airspeed calibration failed");
616 }
617
618 self.calibration_state.mean = 0.0;
619 self.calibration_state.m2 = 0.0;
620 self.calibration_state.count = 0;
621 } else if self.calibration_state.count > SENSOR_CAL_DELAY_CYCLES {
622 let n = (self.calibration_state.count - SENSOR_CAL_DELAY_CYCLES) as f32;
623 let delta = packet.differential_pressure - self.calibration_state.mean;
624 self.calibration_state.mean += delta / n;
625 let delta2 = packet.differential_pressure - self.calibration_state.mean;
626 self.calibration_state.m2 += delta * delta2 / (SENSOR_CAL_CYCLES - 1) as f32;
627 }
628
629 self.calibration_state.last_iter_ms = now_ms;
630 }
631}
632
633impl SensorPacketProcessor<PitotPacket> for PitotProcessor {
634 fn process(
635 &mut self,
636 packet: &mut Option<Result<PitotPacket, errors::SensorError>>,
637 flags: &mut CalibrationFlags,
638 params: &mut Params,
639 ) -> Option<PitotPacket> {
640 let packet_time_ms = match packet.as_ref() {
641 Some(Ok(packet)) => (packet.header.timestamp / 1000) as u32,
642 _ => 0,
643 };
644 self.process_packet(packet, flags, params, packet_time_ms)
645 }
646
647 fn process_at(
648 &mut self,
649 packet: &mut Option<Result<PitotPacket, errors::SensorError>>,
650 flags: &mut CalibrationFlags,
651 params: &mut Params,
652 now_ms: u32,
653 ) -> Option<PitotPacket> {
654 self.process_packet(packet, flags, params, now_ms)
655 }
656}
657
658#[derive(Default, Copy, Clone)]
663pub struct PassthroughMagProcessor;
664impl_passthrough_sensor_packet_processor!(PassthroughMagProcessor, MagPacket);
665
666#[derive(Default, Copy, Clone)]
667pub struct MagProcessor;
668
669impl MagProcessor {
670 fn process_packet(
671 &mut self,
672 packet: &mut Option<Result<MagPacket, errors::SensorError>>,
673 _flags: &mut CalibrationFlags,
674 params: &mut Params,
675 ) -> Option<MagPacket> {
676 if let Some(Ok(mut packet)) = packet.take() {
677 rotate_mag_in_place(&mut packet, params);
678 let mag_hard_x = packet.flux[0] - param_float(params, ParamId::PARAM_MAG_X_BIAS);
680 let mag_hard_y = packet.flux[1] - param_float(params, ParamId::PARAM_MAG_Y_BIAS);
681 let mag_hard_z = packet.flux[2] - param_float(params, ParamId::PARAM_MAG_Z_BIAS);
682
683 let a00 = param_float(params, ParamId::PARAM_MAG_A00_COMP);
685 let a01 = param_float(params, ParamId::PARAM_MAG_A01_COMP);
686 let a02 = param_float(params, ParamId::PARAM_MAG_A02_COMP);
687 let a10 = param_float(params, ParamId::PARAM_MAG_A10_COMP);
688 let a11 = param_float(params, ParamId::PARAM_MAG_A11_COMP);
689 let a12 = param_float(params, ParamId::PARAM_MAG_A12_COMP);
690 let a20 = param_float(params, ParamId::PARAM_MAG_A20_COMP);
691 let a21 = param_float(params, ParamId::PARAM_MAG_A21_COMP);
692 let a22 = param_float(params, ParamId::PARAM_MAG_A22_COMP);
693
694 packet.flux[0] = a00 * mag_hard_x + a01 * mag_hard_y + a02 * mag_hard_z;
696 packet.flux[1] = a10 * mag_hard_x + a11 * mag_hard_y + a12 * mag_hard_z;
697 packet.flux[2] = a20 * mag_hard_x + a21 * mag_hard_y + a22 * mag_hard_z;
698
699 Some(packet)
700 } else {
701 None
702 }
703 }
704}
705
706fn param_float(params: &Params, param_id: ParamId) -> f32 {
707 match params.get_by_id(param_id) {
708 ParamValue::Float(value) => value,
709 _ => 0.0,
710 }
711}
712
713fn vector_norm<R: FlightFloat>(vector: [R; 3]) -> R {
714 (vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]).sqrt()
715}
716
717fn rotate_mag_in_place(packet: &mut MagPacket, params: &Params) {
718 let rotation = rosflight_orientation_matrix(
719 param_float(params, ParamId::PARAM_MAG_ROLL) * deg_to_rad::<f32>(),
720 param_float(params, ParamId::PARAM_MAG_PITCH) * deg_to_rad::<f32>(),
721 param_float(params, ParamId::PARAM_MAG_YAW) * deg_to_rad::<f32>(),
722 );
723 let rotated = rotate_vector_real(rotation, [packet.flux[0], packet.flux[1], packet.flux[2]]);
724 packet.flux = rotated;
725}
726
727#[derive(Copy, Clone)]
728struct ImuMountRotation<R: FlightFloat> {
729 angles_deg: [f32; 3],
730 rotation: [[R; 3]; 3],
731}
732
733impl<R: FlightFloat> ImuMountRotation<R> {
734 fn rotate_imu_in_place(&mut self, packet: &mut ImuPacket<R>, params: &Params) {
735 self.update_from_params(params);
736 packet.accel = rotate_vector_real(self.rotation, packet.accel);
737 packet.gyro = rotate_vector_real(self.rotation, packet.gyro);
738 }
739
740 fn update_from_params(&mut self, params: &Params) {
741 let angles_deg = [
742 param_float(params, ParamId::PARAM_IMU_ROLL),
743 param_float(params, ParamId::PARAM_IMU_PITCH),
744 param_float(params, ParamId::PARAM_IMU_YAW),
745 ];
746
747 if angles_deg == self.angles_deg {
748 return;
749 }
750
751 self.angles_deg = angles_deg;
752 self.rotation = imu_mount_rotation_matrix(angles_deg);
753 }
754}
755
756impl<R: FlightFloat> Default for ImuMountRotation<R> {
757 fn default() -> Self {
758 let angles_deg = [0.0; 3];
759 Self {
760 angles_deg,
761 rotation: imu_mount_rotation_matrix(angles_deg),
762 }
763 }
764}
765
766fn imu_mount_rotation_matrix<R: FlightFloat>(angles_deg: [f32; 3]) -> [[R; 3]; 3] {
767 rosflight_orientation_matrix(
768 <R as FlightFloat>::from_f32(angles_deg[0]) * deg_to_rad(),
769 <R as FlightFloat>::from_f32(angles_deg[1]) * deg_to_rad(),
770 <R as FlightFloat>::from_f32(angles_deg[2]) * deg_to_rad(),
771 )
772}
773
774fn rosflight_orientation_matrix<R: FlightFloat>(roll: R, pitch: R, yaw: R) -> [[R; 3]; 3] {
775 let two = <R as FlightFloat>::from_f32(2.0);
776 let half = <R as FlightFloat>::from_f32(0.5);
777 let roll_cos = (roll * half).cos();
778 let roll_sin = (roll * half).sin();
779 let pitch_cos = (pitch * half).cos();
780 let pitch_sin = (pitch * half).sin();
781 let yaw_cos = (yaw * half).cos();
782 let yaw_sin = (yaw * half).sin();
783
784 let mut w = yaw_cos * pitch_cos * roll_cos + yaw_sin * pitch_sin * roll_sin;
787 let mut x = yaw_cos * pitch_cos * roll_sin - yaw_sin * pitch_sin * roll_cos;
788 let mut y = yaw_cos * pitch_sin * roll_cos + yaw_sin * pitch_cos * roll_sin;
789 let mut z = yaw_sin * pitch_cos * roll_cos - yaw_cos * pitch_sin * roll_sin;
790 let norm = (w * w + x * x + y * y + z * z).sqrt();
791 w /= norm;
792 x /= norm;
793 y /= norm;
794 z /= norm;
795
796 [
797 [
798 R::one() - two * y * y - two * z * z,
799 two * (x * y + w * z),
800 two * (x * z - w * y),
801 ],
802 [
803 two * (x * y - w * z),
804 R::one() - two * x * x - two * z * z,
805 two * (y * z + w * x),
806 ],
807 [
808 two * (x * z + w * y),
809 two * (y * z - w * x),
810 R::one() - two * x * x - two * y * y,
811 ],
812 ]
813}
814
815fn rotate_vector_real<R: FlightFloat>(rotation: [[R; 3]; 3], vector: [R; 3]) -> [R; 3] {
816 [
817 rotation[0][0] * vector[0] + rotation[0][1] * vector[1] + rotation[0][2] * vector[2],
818 rotation[1][0] * vector[0] + rotation[1][1] * vector[1] + rotation[1][2] * vector[2],
819 rotation[2][0] * vector[0] + rotation[2][1] * vector[1] + rotation[2][2] * vector[2],
820 ]
821}
822
823impl SensorPacketProcessor<MagPacket> for MagProcessor {
824 fn process(
825 &mut self,
826 packet: &mut Option<Result<MagPacket, errors::SensorError>>,
827 flags: &mut CalibrationFlags,
828 params: &mut Params,
829 ) -> Option<MagPacket> {
830 self.process_packet(packet, flags, params)
831 }
832}
833
834#[derive(Default, Copy, Clone)]
839pub struct PassthroughRcProcessor;
840impl_passthrough_sensor_packet_processor!(PassthroughRcProcessor, RcPacket);
841
842#[derive(Default, Copy, Clone)]
847pub struct PassthroughRangeProcessor;
848impl_passthrough_sensor_packet_processor!(PassthroughRangeProcessor, RangePacket);
849
850#[derive(Default, Copy, Clone)]
855pub struct PassthroughGNSSProcessor;
856impl_passthrough_sensor_packet_processor!(PassthroughGNSSProcessor, GNSSPacket);
857
858#[derive(Default, Copy, Clone)]
863pub struct PassthroughPpsProcessor;
864impl_passthrough_sensor_packet_processor!(PassthroughPpsProcessor, PpsPacket);
865
866#[derive(Default, Copy, Clone)]
871pub struct PassthroughAttitudeProcessor;
872impl_passthrough_sensor_packet_processor!(PassthroughAttitudeProcessor, AttitudePacket);
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877
878 fn process_one<P, Proc>(processor: &mut Proc, packet: P, params: &mut Params) -> P
879 where
880 Proc: SensorPacketProcessor<P>,
881 {
882 let mut raw = Some(Ok(packet));
883 let mut flags = CalibrationFlags::empty();
884 processor.process(&mut raw, &mut flags, params).unwrap()
885 }
886
887 fn assert_vector_close(actual: [f64; 3], expected: [f64; 3]) {
888 for axis in 0..3 {
889 assert!(
890 (actual[axis] - expected[axis]).abs() < 1e-6,
891 "axis {axis}: expected {}, got {}",
892 expected[axis],
893 actual[axis]
894 );
895 }
896 }
897
898 #[test]
899 fn rosflight_mount_rotation_matches_positive_and_negative_cardinal_axes() {
900 let cases = [
901 ([90.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]),
902 ([-90.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]),
903 ([0.0, 90.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]),
904 ([0.0, -90.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]),
905 ([0.0, 0.0, 90.0], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0]),
906 ([0.0, 0.0, -90.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
907 ];
908
909 for (angles_deg, input, expected) in cases {
910 let rotation = imu_mount_rotation_matrix::<f64>(angles_deg);
911 assert_vector_close(rotate_vector_real(rotation, input), expected);
912 }
913 }
914
915 #[test]
916 fn rosflight_mount_rotation_matches_combined_rpy_reference() {
917 let rotation = imu_mount_rotation_matrix::<f64>([30.0, -20.0, 40.0]);
918 assert_vector_close(
919 rotate_vector_real(rotation, [0.3, -0.4, 0.5]),
920 [
921 0.14535485535869916,
922 -0.19277467629484094,
923 0.6646125865517978,
924 ],
925 );
926 }
927
928 #[test]
929 fn zero_mount_rotation_is_exact_identity() {
930 assert_eq!(
931 imu_mount_rotation_matrix::<f64>([0.0; 3]),
932 [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
933 );
934 }
935
936 #[test]
937 fn imu_processor_applies_rosflight_orientation_before_correction() {
938 let mut params = Params::new();
939 params.set_by_id(ParamId::PARAM_IMU_YAW, ParamValue::Float(90.0));
940 params.set_by_id(ParamId::PARAM_GYRO_Y_BIAS, ParamValue::Float(0.5));
941 let mut processor = ImuProcessor::<f64>::new();
942
943 let processed: ImuPacket<f64> = process_one(
944 &mut processor,
945 ImuPacket {
946 accel: [1.0, 0.0, 0.0],
947 gyro: [1.0, 0.0, 0.0],
948 ..Default::default()
949 },
950 &mut params,
951 );
952
953 assert!(processed.accel[0].abs() < 1e-6);
954 assert!((processed.accel[1] + 1.0).abs() < 1e-6);
955 assert!(processed.gyro[0].abs() < 1e-6);
956 assert!((processed.gyro[1] + 1.5).abs() < 1e-6);
957 }
958
959 #[test]
960 fn imu_processor_refreshes_mount_rotation_after_param_change() {
961 let mut params = Params::new();
962 let mut processor = ImuProcessor::<f64>::new();
963
964 let unchanged: ImuPacket<f64> = process_one(
965 &mut processor,
966 ImuPacket {
967 accel: [1.0, 0.0, 0.0],
968 gyro: [1.0, 0.0, 0.0],
969 ..Default::default()
970 },
971 &mut params,
972 );
973 assert!((unchanged.accel[0] - 1.0).abs() < 1e-6);
974 assert!(unchanged.accel[1].abs() < 1e-6);
975
976 params.set_by_id(ParamId::PARAM_IMU_YAW, ParamValue::Float(90.0));
977 let rotated: ImuPacket<f64> = process_one(
978 &mut processor,
979 ImuPacket {
980 accel: [1.0, 0.0, 0.0],
981 gyro: [1.0, 0.0, 0.0],
982 ..Default::default()
983 },
984 &mut params,
985 );
986
987 assert!(rotated.accel[0].abs() < 1e-6);
988 assert!((rotated.accel[1] + 1.0).abs() < 1e-6);
989 assert!(rotated.gyro[0].abs() < 1e-6);
990 assert!((rotated.gyro[1] + 1.0).abs() < 1e-6);
991 }
992
993 #[test]
994 fn imu_processor_matches_rosflight_c_bias_and_temperature_compensation() {
995 let mut params = Params::new();
996 params.set_by_id(ParamId::PARAM_GYRO_X_BIAS, ParamValue::Float(0.1));
997 params.set_by_id(ParamId::PARAM_GYRO_Y_BIAS, ParamValue::Float(-0.2));
998 params.set_by_id(ParamId::PARAM_GYRO_Z_BIAS, ParamValue::Float(0.3));
999 params.set_by_id(ParamId::PARAM_ACC_X_BIAS, ParamValue::Float(0.4));
1000 params.set_by_id(ParamId::PARAM_ACC_Y_BIAS, ParamValue::Float(-0.5));
1001 params.set_by_id(ParamId::PARAM_ACC_Z_BIAS, ParamValue::Float(0.6));
1002 params.set_by_id(ParamId::PARAM_ACC_X_TEMP_COMP, ParamValue::Float(0.01));
1003 params.set_by_id(ParamId::PARAM_ACC_Y_TEMP_COMP, ParamValue::Float(-0.02));
1004 params.set_by_id(ParamId::PARAM_ACC_Z_TEMP_COMP, ParamValue::Float(0.03));
1005 let mut processor = ImuProcessor::<f64>::new();
1006
1007 let processed: ImuPacket<f64> = process_one(
1008 &mut processor,
1009 ImuPacket {
1010 accel: [1.4, -2.5, -8.00665],
1011 gyro: [0.6, -0.7, 1.1],
1012 temperature: 20.0,
1013 ..Default::default()
1014 },
1015 &mut params,
1016 );
1017
1018 assert!((processed.gyro[0] - 0.5).abs() < 1e-6);
1019 assert!((processed.gyro[1] + 0.5).abs() < 1e-6);
1020 assert!((processed.gyro[2] - 0.8).abs() < 1e-6);
1021 assert!((processed.accel[0] - 0.8).abs() < 1e-6);
1022 assert!((processed.accel[1] + 1.6).abs() < 1e-6);
1023 assert!((processed.accel[2] + 9.20665).abs() < 1e-5);
1024 }
1025
1026 #[test]
1027 fn mag_processor_applies_orientation_hard_iron_and_rosflight_matrix() {
1028 let mut params = Params::new();
1029 params.set_by_id(ParamId::PARAM_MAG_YAW, ParamValue::Float(90.0));
1030 params.set_by_id(ParamId::PARAM_MAG_Y_BIAS, ParamValue::Float(1.0));
1031 params.set_by_id(ParamId::PARAM_MAG_A00_COMP, ParamValue::Float(2.0));
1032 params.set_by_id(ParamId::PARAM_MAG_A01_COMP, ParamValue::Float(3.0));
1033 params.set_by_id(ParamId::PARAM_MAG_A02_COMP, ParamValue::Float(5.0));
1034 params.set_by_id(ParamId::PARAM_MAG_A10_COMP, ParamValue::Float(7.0));
1035 params.set_by_id(ParamId::PARAM_MAG_A11_COMP, ParamValue::Float(11.0));
1036 params.set_by_id(ParamId::PARAM_MAG_A12_COMP, ParamValue::Float(13.0));
1037 params.set_by_id(ParamId::PARAM_MAG_A20_COMP, ParamValue::Float(17.0));
1038 params.set_by_id(ParamId::PARAM_MAG_A21_COMP, ParamValue::Float(19.0));
1039 params.set_by_id(ParamId::PARAM_MAG_A22_COMP, ParamValue::Float(23.0));
1040 let mut processor = MagProcessor;
1041
1042 let processed: MagPacket = process_one(
1043 &mut processor,
1044 MagPacket {
1045 flux: [1.0, 0.0, 2.0],
1046 ..Default::default()
1047 },
1048 &mut params,
1049 );
1050
1051 assert!((processed.flux[0] - 4.0).abs() < 1e-5);
1054 assert!((processed.flux[1] - 4.0).abs() < 1e-5);
1055 assert!((processed.flux[2] - 8.0).abs() < 1e-5);
1056 }
1057
1058 #[test]
1059 fn baro_processor_matches_rosflight_correction_without_subtracting_bias() {
1060 let mut params = Params::new();
1061 params.set_by_id(ParamId::PARAM_BARO_BIAS, ParamValue::Float(1000.0));
1062 let mut processor = BaroProcessor::new();
1063
1064 let processed: BaroPacket = process_one(
1065 &mut processor,
1066 BaroPacket {
1067 pressure: 80_000.0,
1068 ..Default::default()
1069 },
1070 &mut params,
1071 );
1072
1073 let expected = pressure_to_altitude(80_000.0) as f32;
1074 assert_eq!(processed.pressure, 80_000.0);
1075 assert!((processed.altitude - expected).abs() < 1e-3);
1076 }
1077
1078 #[test]
1079 fn baro_processor_calibration_uses_board_clock_and_rosflight_mean() {
1080 while crate::log::Logger::pop().is_some() {}
1081
1082 let mut params = Params::new();
1083 let mut processor = BaroProcessor::new();
1084 let mut flags = CalibrationFlags::BARO;
1085
1086 for sample in 0..=SENSOR_CAL_DELAY_CYCLES + SENSOR_CAL_CYCLES {
1087 let mut raw = Some(Ok(BaroPacket {
1088 header: RosflightPacketHeader {
1089 timestamp: 1,
1092 status: 0,
1093 },
1094 pressure: 90_000.0,
1095 ..Default::default()
1096 }));
1097 let now_ms = (sample as u32 + 1) * 21;
1098 let _ = processor.process_at(&mut raw, &mut flags, &mut params, now_ms);
1099 }
1100
1101 assert!(!flags.contains(CalibrationFlags::BARO));
1102 assert_eq!(
1103 params.get_by_id(ParamId::PARAM_BARO_BIAS),
1104 ParamValue::Float(90_000.0)
1105 );
1106 assert_eq!(
1107 params.get_by_id(ParamId::PARAM_GROUND_LEVEL),
1108 ParamValue::Float(pressure_to_altitude(90_000.0))
1109 );
1110 assert_eq!(
1111 crate::log::Logger::pop().unwrap().message.as_str(),
1112 "Baro ground pressure cal successful!"
1113 );
1114 }
1115
1116 #[test]
1117 fn pitot_processor_calibrates_from_board_clock_then_corrects_pressure_and_airspeed() {
1118 let mut params = Params::new();
1119 let mut processor = PitotProcessor::new();
1120 let mut flags = CalibrationFlags::PITOT;
1121
1122 for sample in 0..=SENSOR_CAL_DELAY_CYCLES + SENSOR_CAL_CYCLES {
1123 let mut raw = Some(Ok(PitotPacket {
1124 header: RosflightPacketHeader {
1125 timestamp: 1,
1128 status: 0,
1129 },
1130 differential_pressure: 4.0,
1131 ..Default::default()
1132 }));
1133 let now_ms = (sample as u32 + 1) * 21;
1134 let _ = processor.process_at(&mut raw, &mut flags, &mut params, now_ms);
1135 }
1136
1137 assert!(!flags.contains(CalibrationFlags::PITOT));
1138 assert_eq!(
1139 params.get_by_id(ParamId::PARAM_DIFF_PRESS_BIAS),
1140 ParamValue::Float(4.0)
1141 );
1142
1143 let processed: PitotPacket = process_one(
1144 &mut processor,
1145 PitotPacket {
1146 differential_pressure: 6.0,
1147 ..Default::default()
1148 },
1149 &mut params,
1150 );
1151 assert_eq!(processed.differential_pressure, 2.0);
1152 assert!((processed.indicated_airspeed - (4.0_f32 / 1.225).sqrt()).abs() < 1e-6);
1153 }
1154
1155 #[test]
1156 fn battery_processor_matches_rosflight_lpf_initialization() {
1157 let mut params = Params::new();
1158 params.set_by_id(ParamId::PARAM_VOLT_MAX, ParamValue::Float(20.0));
1159 params.set_by_id(ParamId::PARAM_BATTERY_VOLTAGE_ALPHA, ParamValue::Float(0.5));
1160 params.set_by_id(
1161 ParamId::PARAM_BATTERY_CURRENT_ALPHA,
1162 ParamValue::Float(0.25),
1163 );
1164 let mut processor = BatteryProcessor::default();
1165
1166 let first = process_one(
1167 &mut processor,
1168 BatteryPacket {
1169 voltage: 10.0,
1170 current: 8.0,
1171 ..Default::default()
1172 },
1173 &mut params,
1174 );
1175 let second = process_one(
1176 &mut processor,
1177 BatteryPacket {
1178 voltage: 14.0,
1179 current: 4.0,
1180 ..Default::default()
1181 },
1182 &mut params,
1183 );
1184
1185 assert_eq!(first.voltage, 15.0);
1186 assert_eq!(first.current, 6.0);
1187 assert_eq!(second.voltage, 14.5);
1188 assert_eq!(second.current, 4.5);
1189 }
1190
1191 #[test]
1192 fn battery_processor_reuses_latest_sample_between_battery_updates() {
1193 let mut params = Params::new();
1194 params.set_by_id(ParamId::PARAM_VOLT_MAX, ParamValue::Float(23.5));
1195 params.set_by_id(ParamId::PARAM_BATTERY_VOLTAGE_ALPHA, ParamValue::Float(0.0));
1196 params.set_by_id(ParamId::PARAM_BATTERY_CURRENT_ALPHA, ParamValue::Float(0.0));
1197 let mut processor = BatteryProcessor::default();
1198 let mut flags = CalibrationFlags::empty();
1199 let mut raw = Some(Ok(BatteryPacket {
1200 voltage: 22.0,
1201 current: 3.0,
1202 ..Default::default()
1203 }));
1204
1205 let first = processor
1206 .process(&mut raw, &mut flags, &mut params)
1207 .unwrap();
1208 let mut no_new_packet = None;
1209 let second = processor
1210 .process(&mut no_new_packet, &mut flags, &mut params)
1211 .unwrap();
1212
1213 assert_eq!(first.voltage, 22.0);
1214 assert_eq!(second.voltage, first.voltage);
1215 assert_eq!(second.current, first.current);
1216 }
1217
1218 #[test]
1219 fn imu_calibration_uses_rosflight_gravity_sign_and_sanity_gates() {
1220 let mut params = Params::new();
1221 let mut processor = ImuProcessor::<f64>::new();
1222 let mut flags = CalibrationFlags::ACCEL | CalibrationFlags::GYRO;
1223
1224 for seq in 0..=1000 {
1225 let mut raw = Some(Ok(ImuPacket {
1226 accel: [0.1, -0.2, -9.50665],
1227 gyro: [0.2, -0.1, 0.3],
1228 seq,
1229 ..Default::default()
1230 }));
1231 let _ = processor.process(&mut raw, &mut flags, &mut params);
1232 }
1233
1234 assert!(!flags.intersects(CalibrationFlags::IMU));
1235 assert_eq!(
1236 params.get_by_id(ParamId::PARAM_ACC_X_BIAS),
1237 ParamValue::Float(0.1)
1238 );
1239 assert_eq!(
1240 params.get_by_id(ParamId::PARAM_ACC_Y_BIAS),
1241 ParamValue::Float(-0.2)
1242 );
1243 assert!((param_float(¶ms, ParamId::PARAM_ACC_Z_BIAS) - 0.3).abs() < 1e-5);
1244 assert_eq!(
1245 params.get_by_id(ParamId::PARAM_GYRO_X_BIAS),
1246 ParamValue::Float(0.2)
1247 );
1248 }
1249
1250 #[test]
1251 fn imu_calibration_uses_mount_rotation_before_gravity_sanity_gate() {
1252 let mut params = Params::new();
1253 params.set_by_id(ParamId::PARAM_IMU_ROLL, ParamValue::Float(90.0));
1254 let mut processor = ImuProcessor::<f64>::new();
1255 let mut flags = CalibrationFlags::ACCEL | CalibrationFlags::GYRO;
1256
1257 for seq in 0..=1000 {
1258 let mut raw = Some(Ok(ImuPacket {
1259 accel: [0.1, 9.50665, -0.2],
1260 gyro: [0.2, -0.3, -0.1],
1261 seq,
1262 ..Default::default()
1263 }));
1264 let _ = processor.process(&mut raw, &mut flags, &mut params);
1265 }
1266
1267 assert!(!flags.intersects(CalibrationFlags::IMU));
1268 assert!(!flags.intersects(CalibrationFlags::ACCEL_FAILED | CalibrationFlags::GYRO_FAILED));
1269 assert!((param_float(¶ms, ParamId::PARAM_ACC_X_BIAS) - 0.1).abs() < 1e-6);
1270 assert!((param_float(¶ms, ParamId::PARAM_ACC_Y_BIAS) + 0.2).abs() < 1e-6);
1271 assert!((param_float(¶ms, ParamId::PARAM_ACC_Z_BIAS) - 0.3).abs() < 1e-5);
1272 assert!((param_float(¶ms, ParamId::PARAM_GYRO_X_BIAS) - 0.2).abs() < 1e-6);
1273 assert!((param_float(¶ms, ParamId::PARAM_GYRO_Y_BIAS) + 0.1).abs() < 1e-6);
1274 assert!((param_float(¶ms, ParamId::PARAM_GYRO_Z_BIAS) - 0.3).abs() < 1e-6);
1275 }
1276}