blackbox_log/
headers.rs

1//! Types for the header section of blackbox logs.
2
3use alloc::borrow::ToOwned;
4use alloc::string::String;
5use core::str::FromStr;
6use core::{cmp, fmt, str};
7
8use hashbrown::HashMap;
9use time::PrimitiveDateTime;
10
11use crate::frame::gps::{GpsFrameDef, GpsFrameDefBuilder};
12use crate::frame::gps_home::{GpsHomeFrameDef, GpsHomeFrameDefBuilder};
13use crate::frame::main::{MainFrameDef, MainFrameDefBuilder};
14use crate::frame::slow::{SlowFrameDef, SlowFrameDefBuilder};
15use crate::frame::{is_frame_def_header, parse_frame_def_header, DataFrameKind};
16use crate::parser::{InternalError, InternalResult};
17use crate::predictor::Predictor;
18use crate::utils::as_u32;
19use crate::{DataParser, FilterSet, Reader, Unit};
20
21include_generated!("debug_mode");
22include_generated!("disabled_fields");
23include_generated!("features");
24include_generated!("pwm_protocol");
25
26pub type ParseResult<T> = Result<T, ParseError>;
27
28/// A fatal error encountered while parsing the headers of a log.
29#[derive(Debug, Clone)]
30#[cfg_attr(feature = "_serde", derive(serde::Serialize))]
31pub enum ParseError {
32    /// The log uses a data format version that is unsupported or could not be
33    /// parsed.
34    UnsupportedDataVersion,
35    /// The `Firmware revision` header could not be parsed, or is from an
36    /// unsupported firmware.
37    InvalidFirmware(String),
38    /// The log comes from an unsupported version of a known firmware.
39    UnsupportedFirmwareVersion(Firmware),
40    /// Could not parse the value in header `header`.
41    InvalidHeader { header: String, value: String },
42    // TODO: include header
43    /// Did not find a required header.
44    MissingHeader,
45    /// The file ended before the start of the data section.
46    IncompleteHeaders,
47    /// Definition for frame type `frame` is missing required a required field.
48    MissingField { frame: DataFrameKind, field: String },
49    /// Unknown unrecoverable error in the frame definition.
50    MalformedFrameDef(DataFrameKind),
51}
52
53impl fmt::Display for ParseError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            Self::UnsupportedDataVersion => write!(f, "unsupported or invalid data version"),
57            Self::InvalidFirmware(firmware) => write!(f, "could not parse firmware: `{firmware}`"),
58            Self::UnsupportedFirmwareVersion(firmware) => {
59                let name = firmware.name();
60                let version = firmware.version();
61                write!(f, "logs from {name} v{version} are not supported")
62            }
63            Self::InvalidHeader { header, value } => {
64                write!(f, "invalid value for header `{header}`: `{value}`")
65            }
66            Self::MissingHeader => {
67                write!(f, "one or more headers required for parsing are missing")
68            }
69            Self::IncompleteHeaders => write!(f, "end of file found before data section"),
70            Self::MissingField { frame, field } => {
71                write!(f, "missing field `{field}` in `{frame}` frame definition")
72            }
73            Self::MalformedFrameDef(frame) => write!(f, "malformed {frame} frame definition"),
74        }
75    }
76}
77
78impl core::error::Error for ParseError {}
79
80/// Decoded headers containing metadata for a blackbox log.
81#[derive(Debug, Clone)]
82#[non_exhaustive]
83pub struct Headers<'data> {
84    data: Reader<'data>,
85
86    main_frame_def: MainFrameDef<'data>,
87    slow_frame_def: SlowFrameDef<'data>,
88    gps_frame_def: Option<GpsFrameDef<'data>>,
89    gps_home_frame_def: Option<GpsHomeFrameDef<'data>>,
90
91    firmware_revision: &'data str,
92    pub(crate) internal_firmware: InternalFirmware,
93    firmware: Firmware,
94    firmware_date: Option<&'data str>,
95    board_info: Option<&'data str>,
96    craft_name: Option<&'data str>,
97
98    debug_mode: DebugMode,
99    disabled_fields: DisabledFields,
100    features: FeatureSet,
101    pwm_protocol: PwmProtocol,
102
103    /// The battery voltage measured at arm.
104    pub(crate) vbat_reference: Option<u16>,
105    /// Calibration for the accelerometer.
106    pub(crate) acceleration_1g: Option<u16>,
107    /// Calibration for the gyro in radians / second.
108    pub(crate) gyro_scale: Option<f32>,
109
110    pub(crate) min_throttle: Option<u16>,
111    pub(crate) motor_output_range: Option<MotorOutputRange>,
112
113    unknown: HashMap<&'data str, &'data str>,
114}
115
116impl<'data> Headers<'data> {
117    /// Parses only the headers of a blackbox log.
118    ///
119    /// `data` will be advanced to the start of the data section of the log,
120    /// ready to be passed to [`DataParser::new`][crate::DataParser::new].
121    ///
122    /// **Note:** This assumes that `data` is aligned to the start of a log.
123    pub(crate) fn parse(data: &'data [u8]) -> ParseResult<Self> {
124        let mut data = Reader::new(data);
125
126        // Skip product header
127        let product = data.read_line();
128        debug_assert_eq!(crate::MARKER.strip_suffix(b"\n"), product);
129        let data_version = data.read_line();
130        if !matches!(data_version, Some(b"H Data version:2")) {
131            return Err(ParseError::UnsupportedDataVersion);
132        }
133
134        let mut state = State::new();
135
136        loop {
137            if data.peek() != Some(b'H') {
138                break;
139            }
140
141            let restore = data.get_restore_point();
142            let (name, value) = match parse_header(&mut data) {
143                Ok(x) => x,
144                Err(InternalError::Retry) => {
145                    tracing::debug!("found corrupted header");
146                    data.restore(restore);
147                    break;
148                }
149                Err(InternalError::Eof) => return Err(ParseError::IncompleteHeaders),
150            };
151
152            if !state.update(name, value) {
153                return Err(ParseError::InvalidHeader {
154                    header: name.to_owned(),
155                    value: value.to_owned(),
156                });
157            }
158        }
159
160        state.finish(data)
161    }
162
163    fn validate(&self) -> ParseResult<()> {
164        let has_accel = self.acceleration_1g.is_some();
165        let has_min_throttle = self.min_throttle.is_some();
166        // TODO: also check it is in a main frame
167        let motor_0 = self.main_frame_def.index_motor_0;
168        let has_vbat_ref = self.vbat_reference.is_some();
169        let has_min_motor = self.motor_output_range.is_some();
170        let has_gps_home = self.gps_home_frame_def.is_some();
171
172        let predictor = |frame, field, predictor, index| {
173            let ok = match predictor {
174                Predictor::MinThrottle => has_min_throttle,
175                Predictor::Motor0 => motor_0.is_some() && index > motor_0.unwrap(),
176                Predictor::HomeLat | Predictor::HomeLon => has_gps_home,
177                Predictor::VBatReference => has_vbat_ref,
178                Predictor::MinMotor => has_min_motor,
179                Predictor::Zero
180                | Predictor::Previous
181                | Predictor::StraightLine
182                | Predictor::Average2
183                | Predictor::Increment
184                | Predictor::FifteenHundred
185                | Predictor::LastMainFrameTime => true,
186            };
187
188            if ok {
189                Ok(())
190            } else {
191                tracing::error!(field, ?predictor, "bad predictor");
192                Err(ParseError::MalformedFrameDef(frame))
193            }
194        };
195
196        let unit = |frame, field, unit| {
197            if unit == Unit::Acceleration && !has_accel {
198                tracing::error!(field, ?unit, "bad unit");
199                Err(ParseError::MalformedFrameDef(frame))
200            } else {
201                Ok(())
202            }
203        };
204
205        self.main_frame_def.validate(predictor, unit)?;
206        self.slow_frame_def.validate(predictor, unit)?;
207
208        if let Some(ref def) = self.gps_frame_def {
209            def.validate(predictor, unit)?;
210        }
211
212        if let Some(ref def) = self.gps_home_frame_def {
213            def.validate(predictor, unit)?;
214        }
215
216        Ok(())
217    }
218}
219
220impl<'data> Headers<'data> {
221    /// Returns a new [`DataParser`] without beginning parsing.
222    pub fn data_parser<'headers>(&'headers self) -> DataParser<'data, 'headers> {
223        DataParser::new(self.data.clone(), self, &FilterSet::default())
224    }
225
226    pub fn data_parser_with_filters<'headers>(
227        &'headers self,
228        filters: &FilterSet,
229    ) -> DataParser<'data, 'headers> {
230        DataParser::new(self.data.clone(), self, filters)
231    }
232}
233
234/// Getters for various log headers.
235impl<'data> Headers<'data> {
236    #[inline]
237    pub fn main_frame_def(&self) -> &MainFrameDef<'data> {
238        &self.main_frame_def
239    }
240
241    #[inline]
242    pub fn slow_frame_def(&self) -> &SlowFrameDef<'data> {
243        &self.slow_frame_def
244    }
245
246    #[inline]
247    pub fn gps_frame_def(&self) -> Option<&GpsFrameDef<'data>> {
248        self.gps_frame_def.as_ref()
249    }
250
251    #[inline]
252    pub(crate) fn gps_home_frame_def(&self) -> Option<&GpsHomeFrameDef<'data>> {
253        self.gps_home_frame_def.as_ref()
254    }
255
256    /// The full `Firmware revision` header.
257    ///
258    /// Consider using the [`firmware`][Self::firmware] method instead.
259    #[inline]
260    pub fn firmware_revision(&self) -> &'data str {
261        self.firmware_revision
262    }
263
264    /// The firmware that wrote the log.
265    #[inline]
266    pub fn firmware(&self) -> Firmware {
267        self.firmware
268    }
269
270    /// The `Firmware date` header
271    pub fn firmware_date(&self) -> Option<Result<PrimitiveDateTime, &'data str>> {
272        let format = time::macros::format_description!(
273            "[month repr:short case_sensitive:false] [day padding:space] [year] [hour \
274             repr:24]:[minute]:[second]"
275        );
276        self.firmware_date
277            .map(|date| PrimitiveDateTime::parse(date, &format).map_err(|_| date))
278    }
279
280    /// The `Board info` header.
281    #[inline]
282    pub fn board_info(&self) -> Option<&'data str> {
283        self.board_info
284    }
285
286    /// The `Craft name` header.
287    #[inline]
288    pub fn craft_name(&self) -> Option<&'data str> {
289        self.craft_name
290    }
291
292    #[inline]
293    pub fn debug_mode(&self) -> DebugMode {
294        self.debug_mode
295    }
296
297    #[inline]
298    pub fn disabled_fields(&self) -> DisabledFields {
299        self.disabled_fields
300    }
301
302    #[inline]
303    pub fn features(&self) -> FeatureSet {
304        self.features
305    }
306
307    #[inline]
308    pub fn pwm_protocol(&self) -> PwmProtocol {
309        self.pwm_protocol
310    }
311
312    /// Any unknown headers.
313    #[inline]
314    pub fn unknown(&self) -> &HashMap<&'data str, &'data str> {
315        &self.unknown
316    }
317}
318
319/// A supported firmware.
320///
321/// This is not the same as the `Firmware type` header since all modern
322/// firmwares set that to `Cleanflight`. This is instead decoded from `Firmware
323/// revision`.
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
325#[cfg_attr(feature = "_serde", derive(serde::Serialize))]
326pub enum Firmware {
327    /// [Betaflight](https://github.com/betaflight/betaflight/)
328    Betaflight(FirmwareVersion),
329    /// [INAV](https://github.com/iNavFlight/inav/)
330    Inav(FirmwareVersion),
331}
332
333impl Firmware {
334    pub const fn name(&self) -> &'static str {
335        match self {
336            Firmware::Betaflight(_) => "Betaflight",
337            Firmware::Inav(_) => "INAV",
338        }
339    }
340
341    pub const fn version(&self) -> FirmwareVersion {
342        let (Self::Betaflight(version) | Self::Inav(version)) = self;
343        *version
344    }
345
346    fn parse(firmware_revision: &str) -> Result<Self, ParseError> {
347        let invalid_fw = || Err(ParseError::InvalidFirmware(firmware_revision.to_owned()));
348
349        let mut iter = firmware_revision.split(' ');
350
351        let kind = iter.next().map(str::to_ascii_lowercase);
352        let Some(version) = iter.next().and_then(FirmwareVersion::parse) else {
353            return invalid_fw();
354        };
355
356        let (fw, is_supported) = match kind.as_deref() {
357            Some("betaflight") => (
358                Firmware::Betaflight(version),
359                crate::BETAFLIGHT_SUPPORT.contains(&version),
360            ),
361            Some("inav") => (
362                Firmware::Inav(version),
363                crate::INAV_SUPPORT.contains(&version),
364            ),
365            Some("emuflight") => {
366                tracing::error!("EmuFlight is not supported");
367                return invalid_fw();
368            }
369            _ => {
370                tracing::error!("Could not parse firmware revision");
371                return invalid_fw();
372            }
373        };
374
375        if is_supported {
376            Ok(fw)
377        } else {
378            Err(ParseError::UnsupportedFirmwareVersion(fw))
379        }
380    }
381}
382
383impl PartialOrd for Firmware {
384    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
385        match (self, other) {
386            (Firmware::Betaflight(fw_self), Firmware::Betaflight(fw_other))
387            | (Firmware::Inav(fw_self), Firmware::Inav(fw_other)) => fw_self.partial_cmp(fw_other),
388            _ => None,
389        }
390    }
391}
392
393#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
394pub struct FirmwareVersion {
395    pub major: u8,
396    pub minor: u8,
397    pub patch: u8,
398}
399
400impl FirmwareVersion {
401    pub const fn new(major: u8, minor: u8, patch: u8) -> Self {
402        Self {
403            major,
404            minor,
405            patch,
406        }
407    }
408
409    fn parse(s: &str) -> Option<Self> {
410        let mut components = s.splitn(3, '.').map(|s| s.parse().ok());
411
412        let major = components.next()??;
413        let minor = components.next()??;
414        let patch = components.next()??;
415
416        Some(Self {
417            major,
418            minor,
419            patch,
420        })
421    }
422}
423
424impl fmt::Display for FirmwareVersion {
425    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
427    }
428}
429
430#[cfg(feature = "_serde")]
431impl serde::Serialize for FirmwareVersion {
432    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
433    where
434        S: serde::Serializer,
435    {
436        use alloc::string::ToString;
437        serializer.serialize_str(&self.to_string())
438    }
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
442pub(crate) enum InternalFirmware {
443    Betaflight4_2,
444    Betaflight4_3,
445    Betaflight4_4,
446    Betaflight4_5,
447    Inav5,
448    Inav6,
449    Inav7,
450    Inav8,
451}
452
453impl InternalFirmware {
454    pub(crate) const fn is_betaflight(self) -> bool {
455        match self {
456            Self::Betaflight4_2
457            | Self::Betaflight4_3
458            | Self::Betaflight4_4
459            | Self::Betaflight4_5 => true,
460            Self::Inav5 | Self::Inav6 | Self::Inav7 | Self::Inav8 => false,
461        }
462    }
463
464    #[expect(unused)]
465    pub(crate) const fn is_inav(self) -> bool {
466        // Will need to be changed if any new firmwares are added
467        !self.is_betaflight()
468    }
469}
470
471impl From<Firmware> for InternalFirmware {
472    fn from(fw: Firmware) -> Self {
473        #[expect(clippy::wildcard_enum_match_arm)]
474        match fw {
475            Firmware::Betaflight(FirmwareVersion {
476                major: 4, minor: 2, ..
477            }) => Self::Betaflight4_2,
478            Firmware::Betaflight(FirmwareVersion {
479                major: 4, minor: 3, ..
480            }) => Self::Betaflight4_3,
481            Firmware::Betaflight(FirmwareVersion {
482                major: 4, minor: 4, ..
483            }) => Self::Betaflight4_4,
484            Firmware::Betaflight(FirmwareVersion {
485                major: 4, minor: 5, ..
486            }) => Self::Betaflight4_5,
487            Firmware::Inav(FirmwareVersion { major: 5, .. }) => Self::Inav5,
488            Firmware::Inav(FirmwareVersion { major: 6, .. }) => Self::Inav6,
489            Firmware::Inav(FirmwareVersion { major: 7, .. }) => Self::Inav7,
490            Firmware::Inav(FirmwareVersion { major: 8, .. }) => Self::Inav8,
491            _ => unreachable!(),
492        }
493    }
494}
495
496impl PartialOrd for InternalFirmware {
497    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
498        if self.is_betaflight() != other.is_betaflight() {
499            return None;
500        }
501
502        Some((*self as u8).cmp(&(*other as u8)))
503    }
504}
505
506#[derive(Debug, Clone, Copy)]
507pub(crate) struct MotorOutputRange {
508    pub(crate) min: u16,
509    #[expect(dead_code)]
510    pub(crate) max: u16,
511}
512
513impl MotorOutputRange {
514    pub(crate) fn from_str(s: &str) -> Option<Self> {
515        s.split_once(',').and_then(|(min, max)| {
516            let min = min.parse().ok()?;
517            let max = max.parse().ok()?;
518            Some(Self { min, max })
519        })
520    }
521}
522
523#[derive(Debug)]
524struct RawHeaderValue<'data, T> {
525    header: &'data str,
526    raw: &'data str,
527    value: T,
528}
529
530impl<T> RawHeaderValue<'_, T> {
531    fn invalid_header_error(&self) -> ParseError {
532        ParseError::InvalidHeader {
533            header: self.header.to_owned(),
534            value: self.raw.to_owned(),
535        }
536    }
537}
538
539impl<'data, T: FromStr> RawHeaderValue<'data, T> {
540    fn parse(header: &'data str, raw: &'data str) -> Result<Self, <T as FromStr>::Err> {
541        Ok(Self {
542            header,
543            raw,
544            value: raw.parse()?,
545        })
546    }
547}
548
549#[derive(Debug)]
550struct State<'data> {
551    main_frames: MainFrameDefBuilder<'data>,
552    slow_frames: SlowFrameDefBuilder<'data>,
553    gps_frames: GpsFrameDefBuilder<'data>,
554    gps_home_frames: GpsHomeFrameDefBuilder<'data>,
555
556    firmware_revision: Option<&'data str>,
557    firmware_date: Option<&'data str>,
558    firmware_kind: Option<&'data str>,
559    board_info: Option<&'data str>,
560    craft_name: Option<&'data str>,
561
562    debug_mode: Option<RawHeaderValue<'data, u32>>,
563    disabled_fields: u32,
564    features: u32,
565    pwm_protocol: Option<RawHeaderValue<'data, u32>>,
566
567    vbat_reference: Option<u16>,
568    acceleration_1g: Option<u16>,
569    gyro_scale: Option<f32>,
570
571    min_throttle: Option<u16>,
572    motor_output_range: Option<MotorOutputRange>,
573
574    unknown: HashMap<&'data str, &'data str>,
575}
576
577impl<'data> State<'data> {
578    fn new() -> Self {
579        Self {
580            main_frames: MainFrameDef::builder(),
581            slow_frames: SlowFrameDef::builder(),
582            gps_frames: GpsFrameDef::builder(),
583            gps_home_frames: GpsHomeFrameDef::builder(),
584
585            firmware_revision: None,
586            firmware_date: None,
587            firmware_kind: None,
588            board_info: None,
589            craft_name: None,
590
591            debug_mode: None,
592            disabled_fields: 0,
593            features: 0,
594            pwm_protocol: None,
595
596            vbat_reference: None,
597            acceleration_1g: None,
598            gyro_scale: None,
599
600            min_throttle: None,
601            motor_output_range: None,
602
603            unknown: HashMap::new(),
604        }
605    }
606
607    /// Returns `true` if the header/value pair was valid
608    fn update(&mut self, header: &'data str, value: &'data str) -> bool {
609        // TODO: try block
610        (|| -> Result<(), ()> {
611            match header {
612                "Firmware revision" => self.firmware_revision = Some(value),
613                "Firmware date" => self.firmware_date = Some(value),
614                "Firmware type" => self.firmware_kind = Some(value),
615                "Board information" => self.board_info = Some(value),
616                "Craft name" => self.craft_name = Some(value),
617
618                "debug_mode" => {
619                    let debug_mode = RawHeaderValue::parse(header, value).map_err(|_| ())?;
620                    self.debug_mode = Some(debug_mode);
621                }
622                "fields_disabled_mask" => self.disabled_fields = value.parse().map_err(|_| ())?,
623                "features" => self.features = as_u32(value.parse().map_err(|_| ())?),
624                "motor_pwm_protocol" => {
625                    let protocol = RawHeaderValue::parse(header, value).map_err(|_| ())?;
626                    self.pwm_protocol = Some(protocol);
627                }
628
629                "vbatref" => {
630                    let vbat_reference = value.parse().map_err(|_| ())?;
631                    self.vbat_reference = Some(vbat_reference);
632                }
633                "acc_1G" => {
634                    let one_g = value.parse().map_err(|_| ())?;
635                    self.acceleration_1g = Some(one_g);
636                }
637                "gyro.scale" | "gyro_scale" => {
638                    let scale = if let Some(hex) = value.strip_prefix("0x") {
639                        u32::from_str_radix(hex, 16).map_err(|_| ())?
640                    } else {
641                        value.parse().map_err(|_| ())?
642                    };
643
644                    let scale = f32::from_bits(scale);
645                    self.gyro_scale = Some(scale.to_radians());
646                }
647                "minthrottle" => {
648                    let min_throttle = value.parse().map_err(|_| ())?;
649                    self.min_throttle = Some(min_throttle);
650                }
651                "motorOutput" => {
652                    let range = MotorOutputRange::from_str(value).ok_or(())?;
653                    self.motor_output_range = Some(range);
654                }
655
656                _ if is_frame_def_header(header) => {
657                    let (frame_kind, property) = parse_frame_def_header(header).unwrap();
658
659                    match frame_kind {
660                        DataFrameKind::Inter | DataFrameKind::Intra => {
661                            self.main_frames.update(frame_kind, property, value);
662                        }
663                        DataFrameKind::Slow => self.slow_frames.update(property, value),
664                        DataFrameKind::Gps => self.gps_frames.update(property, value),
665                        DataFrameKind::GpsHome => self.gps_home_frames.update(property, value),
666                    }
667                }
668
669                // Legacy calibration headers
670                "vbatscale" | "vbat_scale" | "currentMeter" | "currentSensor" => {}
671
672                header => {
673                    tracing::debug!("skipping unknown header: `{header}` = `{value}`");
674                    self.unknown.insert(header, value);
675                }
676            };
677
678            Ok(())
679        })()
680        .is_ok()
681    }
682
683    fn finish(self, data: Reader<'data>) -> ParseResult<Headers<'data>> {
684        let not_empty = |s: &&str| !s.is_empty();
685
686        let firmware_revision = self.firmware_revision.ok_or(ParseError::MissingHeader)?;
687        let firmware = Firmware::parse(firmware_revision)?;
688        let internal_firmware = firmware.into();
689
690        // TODO: log where each error comes from
691        let headers = Headers {
692            data,
693
694            main_frame_def: self.main_frames.parse()?,
695            slow_frame_def: self.slow_frames.parse()?,
696            gps_frame_def: self.gps_frames.parse()?,
697            gps_home_frame_def: self.gps_home_frames.parse()?,
698
699            firmware_revision,
700            internal_firmware,
701            firmware,
702            firmware_date: self.firmware_date,
703            board_info: self.board_info.map(str::trim).filter(not_empty),
704            craft_name: self.craft_name.map(str::trim).filter(not_empty),
705
706            debug_mode: self.debug_mode.map_or(Ok(DebugMode::None), |raw| {
707                DebugMode::new(raw.value, internal_firmware)
708                    .ok_or_else(|| raw.invalid_header_error())
709            })?,
710            disabled_fields: DisabledFields::new(self.disabled_fields, internal_firmware),
711            features: FeatureSet::new(self.features, internal_firmware),
712            pwm_protocol: self
713                .pwm_protocol
714                .ok_or(ParseError::MissingHeader)
715                .and_then(|raw| {
716                    PwmProtocol::new(raw.value, internal_firmware)
717                        .ok_or_else(|| raw.invalid_header_error())
718                })?,
719
720            vbat_reference: self.vbat_reference,
721            acceleration_1g: self.acceleration_1g,
722            gyro_scale: self.gyro_scale,
723
724            min_throttle: self.min_throttle,
725            motor_output_range: self.motor_output_range,
726
727            unknown: self.unknown,
728        };
729
730        headers.validate()?;
731
732        Ok(headers)
733    }
734}
735
736/// Expects the next character to be the leading H
737fn parse_header<'data>(bytes: &mut Reader<'data>) -> InternalResult<(&'data str, &'data str)> {
738    match bytes.read_u8() {
739        Some(b'H') => {}
740        Some(_) => return Err(InternalError::Retry),
741        None => return Err(InternalError::Eof),
742    }
743
744    let line = bytes.read_line().ok_or(InternalError::Eof)?;
745
746    let line = str::from_utf8(line).map_err(|_| InternalError::Retry)?;
747    let line = line.strip_prefix(' ').unwrap_or(line);
748    let (name, value) = line.split_once(':').ok_or(InternalError::Retry)?;
749
750    tracing::trace!("read header `{name}` = `{value}`");
751
752    Ok((name, value))
753}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758
759    #[test]
760    #[should_panic(expected = "Retry")]
761    fn invalid_utf8() {
762        let mut b = Reader::new(b"H \xFF:\xFF\n");
763        parse_header(&mut b).unwrap();
764    }
765}