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    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// ------------------------------
71// Battery Packet
72// ------------------------------
73
74#[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// ------------------------------
142// IMU Packet
143// ------------------------------
144
145#[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// ------------------------------
388// Baro Packet
389// ------------------------------
390
391#[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, // Sum of squares of differences from the current mean
403    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// ------------------------------
534// Pitot Packet
535// ------------------------------
536
537#[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// ------------------------------
659// Mag Packet
660// ------------------------------
661
662#[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            // Apply hard-iron biases from parameters
679            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            // Get soft-iron correction matrix parameters
684            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            // Apply soft-iron corrections (matrix multiplication)
695            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    // Direct port of ROSflight C's Quaternion::from_RPY followed by Quaternion::rotate.
785    // ROSflight's rotate applies the passive/sensor-to-body form of the quaternion matrix.
786    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// ------------------------------
835// Rc Packet
836// ------------------------------
837
838#[derive(Default, Copy, Clone)]
839pub struct PassthroughRcProcessor;
840impl_passthrough_sensor_packet_processor!(PassthroughRcProcessor, RcPacket);
841
842// ------------------------------
843// Range Packet
844// ------------------------------
845
846#[derive(Default, Copy, Clone)]
847pub struct PassthroughRangeProcessor;
848impl_passthrough_sensor_packet_processor!(PassthroughRangeProcessor, RangePacket);
849
850// ------------------------------
851// GNSS Packet
852// ------------------------------
853
854#[derive(Default, Copy, Clone)]
855pub struct PassthroughGNSSProcessor;
856impl_passthrough_sensor_packet_processor!(PassthroughGNSSProcessor, GNSSPacket);
857
858// ------------------------------
859// PPS Packet
860// ------------------------------
861
862#[derive(Default, Copy, Clone)]
863pub struct PassthroughPpsProcessor;
864impl_passthrough_sensor_packet_processor!(PassthroughPpsProcessor, PpsPacket);
865
866// ------------------------------
867// Attitude Packet
868// ------------------------------
869
870#[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        // ROSflight's passive yaw rotation maps [1, 0, 2] to [0, -1, 2]. After
1052        // hard-iron Y correction the vector is [0, -2, 2].
1053        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                    // Deliberately fixed: calibration cadence must come from
1090                    // the board clock, not the sensor packet timestamp.
1091                    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                    // Deliberately fixed: calibration cadence must come from
1126                    // the board clock, not the sensor packet timestamp.
1127                    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(&params, 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(&params, ParamId::PARAM_ACC_X_BIAS) - 0.1).abs() < 1e-6);
1270        assert!((param_float(&params, ParamId::PARAM_ACC_Y_BIAS) + 0.2).abs() < 1e-6);
1271        assert!((param_float(&params, ParamId::PARAM_ACC_Z_BIAS) - 0.3).abs() < 1e-5);
1272        assert!((param_float(&params, ParamId::PARAM_GYRO_X_BIAS) - 0.2).abs() < 1e-6);
1273        assert!((param_float(&params, ParamId::PARAM_GYRO_Y_BIAS) + 0.1).abs() < 1e-6);
1274        assert!((param_float(&params, ParamId::PARAM_GYRO_Z_BIAS) - 0.3).abs() < 1e-6);
1275    }
1276}