Skip to main content

datui_lib/formats/
dataflash.rs

1//! ArduPilot DataFlash logs (`.bin`).
2//!
3//! A DataFlash log describes its own messages: each `FMT` record gives a message
4//! type's id, length, name, format characters and field labels; every other record is
5//! `A3 95`, its type's id, and its fields packed end to end. One pass records where
6//! each type's records start ([`crate::formats::indexed`]) and reads the units and multipliers
7//! of `FMTU`, `UNIT` and `MULT` when the log has them; each type is then a table
8//! decoded from a map of the file, its columns typed by their format characters.
9//!
10//! Every length is bounded: a record by its type's length and the file's end. A byte
11//! that does not start a record is passed over to the next `A3 95` and counted.
12
13use std::collections::{BTreeMap, HashMap};
14use std::sync::Arc;
15
16use polars::prelude::*;
17
18use crate::formats::fixed_records::{Bytes, ColumnLayout, Logical, Physical};
19use crate::formats::indexed::{IndexedRecords, Offsets};
20use crate::formats::model_files::MetaValue;
21use crate::formats::sqlite::Table;
22use crate::formats::text_formats::Detail;
23
24/// What datui does with a DataFlash log: see [`crate::formats::readers`].
25pub(crate) const READER: crate::formats::readers::Reader = crate::formats::readers::Reader {
26    scan: crate::formats::indexed::scan::<Index>,
27    signatures: &[crate::formats::readers::Signature {
28        says: |head, _| looks_like(head),
29        kind: crate::formats::readers::Kind::Magic,
30        trusted: crate::formats::readers::Trusted {
31            tables: true,
32            ..crate::formats::readers::EVERYWHERE
33        },
34    }],
35    tables: Some(crate::formats::indexed::listed::<Index>),
36    ..crate::formats::readers::BASE
37};
38
39const HEAD: [u8; 2] = [0xA3, 0x95];
40/// The type id of `FMT`, the record that defines the others.
41pub(crate) const FMT: u8 = 0x80;
42/// An `FMT` record's length: header, type, length, name, format, labels.
43pub(crate) const FMT_LEN: usize = 89;
44
45/// Whether `head`, the first bytes of a file, begins a DataFlash log: an `FMT` record
46/// that defines `FMT` itself.
47pub fn looks_like(head: &[u8]) -> bool {
48    head.len() >= 8
49        && head[..3] == [0xA3, 0x95, FMT]
50        && head[3] == FMT
51        && head[4] as usize == FMT_LEN
52        && &head[5..8] == b"FMT"
53}
54
55/// A format character's storage, width, values, and the scale its value carries.
56fn format_char(c: u8) -> Option<(Physical, usize, usize, Option<f64>)> {
57    Some(match c {
58        b'b' => (Physical::Signed(1), 1, 1, None),
59        b'B' | b'M' => (Physical::Unsigned(1), 1, 1, None),
60        b'h' => (Physical::Signed(2), 2, 1, None),
61        b'H' => (Physical::Unsigned(2), 2, 1, None),
62        b'i' => (Physical::Signed(4), 4, 1, None),
63        b'I' => (Physical::Unsigned(4), 4, 1, None),
64        b'q' => (Physical::Signed(8), 8, 1, None),
65        b'Q' => (Physical::Unsigned(8), 8, 1, None),
66        b'f' => (Physical::Float(4), 4, 1, None),
67        b'd' => (Physical::Float(8), 8, 1, None),
68        b'g' => (Physical::Float(2), 2, 1, None),
69        b'n' => (Physical::Text, 4, 1, None),
70        b'N' => (Physical::Text, 16, 1, None),
71        b'Z' => (Physical::Text, 64, 1, None),
72        b'c' => (Physical::Signed(2), 2, 1, Some(0.01)),
73        b'C' => (Physical::Unsigned(2), 2, 1, Some(0.01)),
74        b'e' => (Physical::Signed(4), 4, 1, Some(0.01)),
75        b'E' => (Physical::Unsigned(4), 4, 1, Some(0.01)),
76        // Latitude and longitude in 1e-7 degrees.
77        b'L' => (Physical::Signed(4), 4, 1, Some(1e-7)),
78        b'a' => (Physical::Signed(2), 2, 32, None),
79        _ => return None,
80    })
81}
82
83/// One message type, as its `FMT` record defines it.
84#[derive(Debug)]
85pub struct MessageType {
86    pub id: u8,
87    pub name: String,
88    /// The whole record's length, header included.
89    pub length: usize,
90    pub format: String,
91    pub labels: Vec<String>,
92    pub offsets: Arc<Offsets>,
93    /// Unit and multiplier ids per field, from `FMTU`.
94    pub unit_ids: Option<String>,
95    pub mult_ids: Option<String>,
96}
97
98/// What one pass over a DataFlash log found.
99#[derive(Debug, Default)]
100pub struct Index {
101    pub types: BTreeMap<u8, MessageType>,
102    pub units: HashMap<u8, String>,
103    pub mults: HashMap<u8, f64>,
104    pub skipped: usize,
105    pub damaged: usize,
106    pub past_limit: usize,
107    /// `FMT` records whose format does not add up to their length, or has a character
108    /// datui does not know.
109    pub bad_formats: Vec<String>,
110    pub cut_short: bool,
111}
112
113impl Index {
114    /// The tables with records, by name.
115    pub fn names(&self) -> Vec<(String, u8)> {
116        let mut names: Vec<(String, u8)> = self
117            .types
118            .values()
119            .filter(|t| !t.offsets.is_empty() && t.id != FMT)
120            .map(|t| (t.name.clone(), t.id))
121            .collect();
122        names.sort();
123        // Two types of one name (a log that redefines one) keep their ids apart.
124        let mut seen: HashMap<String, usize> = HashMap::new();
125        for (name, _) in &names {
126            *seen.entry(name.clone()).or_default() += 1;
127        }
128        for (name, id) in &mut names {
129            if seen[name.as_str()] > 1 {
130                *name = format!("{name}.{id}");
131            }
132        }
133        names
134    }
135}
136
137/// Text from a fixed-width field.
138fn text(bytes: &[u8]) -> String {
139    crate::formats::fixed_records::text(bytes)
140}
141
142/// The size a format's characters add up to, `None` for one datui does not know.
143pub(crate) fn format_size(format: &str) -> Option<usize> {
144    format.bytes().try_fold(0usize, |n, c| {
145        let (_, width, count, _) = format_char(c)?;
146        n.checked_add(width * count)
147    })
148}
149
150/// The fields of a record of `t` read as plain values, for `FMTU`, `UNIT` and `MULT`.
151fn fields<'a>(t: &MessageType, record: &'a [u8]) -> Vec<(&'a [u8], u8)> {
152    let mut at = 3;
153    let mut out = Vec::new();
154    for c in t.format.bytes() {
155        let Some((_, width, count, _)) = format_char(c) else {
156            break;
157        };
158        let len = width * count;
159        let Some(bytes) = record.get(at..at + len) else {
160            break;
161        };
162        out.push((bytes, c));
163        at += len;
164    }
165    out
166}
167
168/// Index the DataFlash log in `data`: one pass, start to end.
169pub fn index(data: &[u8]) -> Result<Index, String> {
170    if !looks_like(data) {
171        return Err("not a DataFlash log: it does not start with an FMT record".into());
172    }
173    let mut index = Index::default();
174    let mut records = 0usize;
175    let limit = crate::limits::get().indexed_records;
176    let mut fmtu: Vec<(u8, String, String)> = Vec::new();
177    let mut building: HashMap<u8, Offsets> = HashMap::new();
178    let mut at = 0usize;
179    while at < data.len() {
180        if data.get(at..at + 2) != Some(&HEAD[..]) {
181            // Not a record: the next header, if there is one.
182            match memchr::memmem::find(&data[at + 1..], &HEAD) {
183                Some(i) => {
184                    index.skipped += i + 1;
185                    index.damaged += 1;
186                    at += i + 1;
187                    continue;
188                }
189                None => {
190                    index.skipped += data.len() - at;
191                    index.damaged += 1;
192                    break;
193                }
194            }
195        }
196        let Some(&id) = data.get(at + 2) else {
197            index.cut_short = true;
198            break;
199        };
200        if id == FMT {
201            let Some(record) = data.get(at..at + FMT_LEN) else {
202                index.cut_short = true;
203                break;
204            };
205            let defined = record[3];
206            let length = record[4] as usize;
207            let name = text(&record[5..9]);
208            let format = text(&record[9..25]);
209            let labels: Vec<String> = text(&record[25..89])
210                .split(',')
211                .map(str::to_string)
212                .collect();
213            match format_size(&format) {
214                Some(size) if size + 3 == length && length >= 3 => {
215                    // A type redefined alike keeps the records it had; one redefined
216                    // otherwise starts again, as its records are read differently.
217                    let same = index
218                        .types
219                        .get(&defined)
220                        .is_some_and(|t| t.length == length && t.format == format);
221                    if !same {
222                        building.insert(defined, Offsets::for_file(data.len()));
223                    }
224                    index.types.insert(
225                        defined,
226                        MessageType {
227                            id: defined,
228                            name,
229                            length,
230                            format,
231                            labels,
232                            offsets: Arc::new(Offsets::Narrow(Vec::new())),
233                            unit_ids: None,
234                            mult_ids: None,
235                        },
236                    );
237                }
238                _ => {
239                    if index.bad_formats.len() < 1000 {
240                        index.bad_formats.push(format!("{name} ({format})"));
241                    }
242                }
243            }
244            at += FMT_LEN;
245            continue;
246        }
247        let Some(t) = index.types.get_mut(&id) else {
248            // A type no FMT defined: its length is unknown, so look for the next header.
249            match memchr::memmem::find(&data[at + 2..], &HEAD) {
250                Some(i) => {
251                    index.skipped += i + 2;
252                    index.damaged += 1;
253                    at += i + 2;
254                    continue;
255                }
256                None => {
257                    index.skipped += data.len() - at;
258                    index.damaged += 1;
259                    break;
260                }
261            }
262        };
263        let Some(record) = data.get(at..at + t.length) else {
264            index.cut_short = true;
265            break;
266        };
267        match t.name.as_str() {
268            "FMTU" => {
269                let f = fields(t, record);
270                if let [_, (ty, _), (units, _), (mults, _), ..] = f.as_slice()
271                    && let Some(&ty) = ty.first()
272                    && fmtu.len() < 1000
273                {
274                    fmtu.push((ty, text(units), text(mults)));
275                }
276            }
277            "UNIT" => {
278                let f = fields(t, record);
279                if let [_, (unit_id, _), (label, _), ..] = f.as_slice()
280                    && let Some(&unit_id) = unit_id.first()
281                {
282                    index.units.insert(unit_id, text(label));
283                }
284            }
285            "MULT" => {
286                let f = fields(t, record);
287                if let [_, (mult_id, _), (mult, b'd'), ..] = f.as_slice()
288                    && let Some(&mult_id) = mult_id.first()
289                    && let Ok(raw) = <[u8; 8]>::try_from(*mult)
290                {
291                    index.mults.insert(mult_id, f64::from_le_bytes(raw));
292                }
293            }
294            _ => {}
295        }
296        if records < limit {
297            building
298                .entry(id)
299                .or_insert_with(|| Offsets::for_file(data.len()))
300                .push(at);
301            records += 1;
302        } else {
303            index.past_limit += 1;
304        }
305        at += t.length;
306    }
307    for (ty, units, mults) in fmtu {
308        if let Some(t) = index.types.get_mut(&ty) {
309            t.unit_ids = Some(units);
310            t.mult_ids = Some(mults);
311        }
312    }
313    for (id, mut offsets) in building {
314        if let Some(t) = index.types.get_mut(&id) {
315            offsets.shrink();
316            t.offsets = Arc::new(offsets);
317        }
318    }
319    Ok(index)
320}
321
322/// The columns of a message type, and each one's unit.
323pub fn columns(index: &Index, t: &MessageType) -> (Vec<ColumnLayout>, Vec<(String, String)>) {
324    let mut columns = Vec::new();
325    let mut units = Vec::new();
326    let mut at = 3;
327    for (i, c) in t.format.bytes().enumerate() {
328        let Some((physical, width, count, scale)) = format_char(c) else {
329            break;
330        };
331        let mut name = t
332            .labels
333            .get(i)
334            .filter(|l| !l.is_empty())
335            .cloned()
336            .unwrap_or_else(|| format!("field{i}"));
337        // A label a log repeats keeps both columns, the later by its place.
338        if columns
339            .iter()
340            .any(|c: &ColumnLayout| c.name == name.as_str())
341        {
342            name = format!("{name}_{i}");
343        }
344        let mut column = match physical {
345            Physical::Text => ColumnLayout::new(&name, at, 0, physical, width),
346            _ => {
347                let mut c = ColumnLayout::new(&name, at, 0, physical, width);
348                c.count = count;
349                c
350            }
351        };
352        let mult = t
353            .mult_ids
354            .as_ref()
355            .and_then(|m| m.as_bytes().get(i))
356            .and_then(|id| index.mults.get(id))
357            .copied();
358        if (name == "TimeUS" && c == b'Q') || (name == "TimeMS" && c == b'I') {
359            column.logical = Logical::Duration {
360                unit: if c == b'Q' {
361                    TimeUnit::Microseconds
362                } else {
363                    TimeUnit::Milliseconds
364                },
365                multiplier: 1,
366            };
367        } else if let Some(scale) = scale {
368            column.logical = Logical::Linear {
369                factor: scale,
370                offset: 0.0,
371            };
372        } else if physical.is_integer()
373            && count == 1
374            && let Some(mult) = mult.filter(|m| *m != 1.0 && *m != 0.0 && m.is_finite())
375        {
376            // An integer the log scales: FMTU says by what.
377            column.logical = Logical::Linear {
378                factor: mult,
379                offset: 0.0,
380            };
381        }
382        if let Some(unit) = t
383            .unit_ids
384            .as_ref()
385            .and_then(|u| u.as_bytes().get(i))
386            .and_then(|id| index.units.get(id))
387            .filter(|u| !u.is_empty())
388        {
389            units.push((name.clone(), unit.clone()));
390        }
391        columns.push(column);
392        at += width * count;
393    }
394    (columns, units)
395}
396
397impl crate::formats::indexed::Log for Index {
398    const EMPTY: &'static str = " The log has no records but its formats.";
399
400    fn index(data: &[u8]) -> Result<Self, String> {
401        index(data)
402    }
403
404    fn tables(&self) -> Vec<Table> {
405        self.names()
406            .into_iter()
407            .map(|(name, id)| {
408                let columns = columns(self, &self.types[&id]).0;
409                Table::plain(name, "message", columns.iter().map(|c| c.name.as_str()))
410            })
411            .collect()
412    }
413
414    fn detail(&self) -> Detail {
415        let names = self.names();
416        let records: usize = names
417            .iter()
418            .map(|(_, id)| self.types[id].offsets.len())
419            .sum();
420        let lines = vec![
421            format!(
422                "Message types: {} with records, {} defined",
423                names.len(),
424                self.types.len()
425            ),
426            format!("Records: {}", crate::numfmt::group_chrome(records)),
427            format!(
428                "Units: {}",
429                if self.units.is_empty() {
430                    "none in the log".to_string()
431                } else {
432                    format!("{} (FMTU, UNIT, MULT)", self.units.len())
433                }
434            ),
435        ];
436        let list = names.iter().map(|(name, id)| {
437            let t = &self.types[id];
438            (
439                name.clone(),
440                MetaValue::Text(format!(
441                    "{} records, format {}, {} bytes",
442                    crate::numfmt::group_chrome(t.offsets.len()),
443                    t.format,
444                    t.length
445                )),
446            )
447        });
448        Detail {
449            tab: crate::formats::text_formats::tab(crate::FileFormat::Dataflash),
450            lines,
451            list_title: "Messages",
452            list: crate::formats::text_formats::capped_list(list, names.len()),
453            first: false,
454            ..Default::default()
455        }
456    }
457
458    fn notes(&self) -> Vec<String> {
459        let group = |n: usize| crate::numfmt::group_chrome(n);
460        let mut notes = Vec::new();
461        if self.damaged > 0 {
462            notes.push(format!(
463                "{} damaged stretches skipped ({} bytes)",
464                group(self.damaged),
465                group(self.skipped)
466            ));
467        }
468        if self.cut_short {
469            notes.push("log cut short mid-record".to_string());
470        }
471        if self.past_limit > 0 {
472            notes.push(crate::limits::left_out(
473                &format!("{} records", group(self.past_limit)),
474                crate::limits::get().indexed_records,
475                "indexed_records",
476            ));
477        }
478        if !self.bad_formats.is_empty() {
479            notes.push(format!(
480                "{} message types not read, format unreadable: {}",
481                self.bad_formats.len(),
482                self.bad_formats
483                    .iter()
484                    .take(10)
485                    .cloned()
486                    .collect::<Vec<_>>()
487                    .join(", ")
488            ));
489        }
490        notes
491    }
492
493    fn table(
494        &self,
495        bytes: Arc<Bytes>,
496        name: &str,
497        opened: &mut crate::formats::members::Opened,
498    ) -> Result<LazyFrame, String> {
499        let id = self
500            .names()
501            .into_iter()
502            .find(|(n, _)| n == name)
503            .map(|(_, id)| id)
504            .expect("picked from the names");
505        let t = &self.types[&id];
506        let (columns, units) = columns(self, t);
507        let records = Arc::new(
508            IndexedRecords::new(bytes, t.offsets.clone(), columns)
509                .map_err(|e| format!("table \"{name}\": {e}"))?,
510        );
511        opened.window = Some((records.clone(), records.rows()));
512        opened.units = units;
513        Ok(records.lazy())
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    /// A file that is not a DataFlash log names itself, in the one shape.
522    #[test]
523    fn errors_name_the_file() {
524        crate::formats::readers::bad_input::each_names_its_file(
525            crate::FileFormat::Dataflash,
526            &[(
527                "text.bin",
528                b"hello there, this is text",
529                "Not a DataFlash log",
530            )],
531        );
532    }
533
534    #[test]
535    fn types_records_units_and_damage() {
536        let data = crate::tests::fixtures::dataflash();
537        assert!(looks_like(&data));
538        let index = index(&data).unwrap();
539        let names: Vec<String> = index.names().into_iter().map(|(n, _)| n).collect();
540        assert_eq!(names, ["ATT", "BARO", "FMTU", "MULT", "UNIT"]);
541        assert_eq!(index.types[&140].offsets.len(), 3);
542        assert_eq!(index.damaged, 1);
543        assert!(index.cut_short);
544        let (columns, units) = columns(&index, &index.types[&141]);
545        assert_eq!(
546            units,
547            [
548                ("Alt".to_string(), "m".to_string()),
549                ("Press".to_string(), "Pa".to_string()),
550                ("Lat".to_string(), "deg".to_string())
551            ]
552        );
553        let records = Arc::new(
554            IndexedRecords::new(
555                Arc::new(Bytes::Owned(data)),
556                index.types[&141].offsets.clone(),
557                columns,
558            )
559            .unwrap(),
560        );
561        let df = records.lazy().collect().unwrap();
562        assert_eq!(
563            df.column("TimeUS").unwrap().dtype(),
564            &DataType::Duration(TimeUnit::Microseconds)
565        );
566        // The format scales latitude; FMTU's multiplier scales the pressure.
567        let lat = df.column("Lat").unwrap().f64().unwrap().get(0).unwrap();
568        assert!((lat - 47.3977418).abs() < 1e-9);
569        assert_eq!(
570            df.column("Press").unwrap().f64().unwrap().get(0),
571            Some(10_132_500.0)
572        );
573    }
574
575    #[test]
576    fn scaled_characters() {
577        let data = crate::tests::fixtures::dataflash();
578        let index = index(&data).unwrap();
579        let (columns, _) = columns(&index, &index.types[&140]);
580        let records = Arc::new(
581            IndexedRecords::new(
582                Arc::new(Bytes::Owned(data)),
583                index.types[&140].offsets.clone(),
584                columns,
585            )
586            .unwrap(),
587        );
588        let df = records.lazy().collect().unwrap();
589        assert_eq!(
590            df.column("Roll").unwrap().f64().unwrap().to_vec(),
591            [Some(1.5), Some(1.51), Some(1.52)]
592        );
593        assert_eq!(df.column("Yaw").unwrap().u16().unwrap().get(0), Some(9000));
594    }
595
596    #[test]
597    fn garbage_is_bounded() {
598        assert!(index(b"nope").is_err());
599        let mut data = crate::tests::fixtures::dataflash_fmt(
600            FMT,
601            "FMT",
602            "BBnNZ",
603            "Type,Length,Name,Format,Columns",
604        );
605        data.extend([0xA3, 0x95, 0x80, 7, 200]);
606        data.extend([0xA3; 50]);
607        let index = index(&data).unwrap();
608        assert!(index.names().is_empty());
609    }
610}