blackbox_log/
headers.rs

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