Skip to main content

veloxity_core/sensors/
processors.rs

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