1use std::collections::{BTreeMap, HashMap};
14use std::path::Path;
15use std::sync::Arc;
16
17use color_eyre::Result;
18use color_eyre::eyre::eyre;
19
20use crate::error_display::{FileError, in_file};
21use polars::prelude::*;
22
23use crate::fixed_records::{Bytes, ColumnLayout, Logical, Physical};
24use crate::indexed::{IndexedRecords, Offsets};
25use crate::model_files::MetaValue;
26use crate::sqlite::Table;
27use crate::text_formats::Detail;
28
29pub(crate) const READER: crate::readers::Reader = crate::readers::Reader {
31 scan,
32 signatures: &[crate::readers::Signature {
33 says: |head, _| looks_like(head),
34 kind: crate::readers::Kind::Magic,
35 trusted: crate::readers::Trusted {
36 tables: true,
37 ..crate::readers::EVERYWHERE
38 },
39 }],
40 tables: Some(listed),
41 ..crate::readers::BASE
42};
43
44const HEAD: [u8; 2] = [0xA3, 0x95];
45const FMT: u8 = 0x80;
47const FMT_LEN: usize = 89;
49
50pub fn looks_like(head: &[u8]) -> bool {
53 head.len() >= 8
54 && head[..3] == [0xA3, 0x95, FMT]
55 && head[3] == FMT
56 && head[4] as usize == FMT_LEN
57 && &head[5..8] == b"FMT"
58}
59
60fn format_char(c: u8) -> Option<(Physical, usize, usize, Option<f64>)> {
62 Some(match c {
63 b'b' => (Physical::Signed(1), 1, 1, None),
64 b'B' | b'M' => (Physical::Unsigned(1), 1, 1, None),
65 b'h' => (Physical::Signed(2), 2, 1, None),
66 b'H' => (Physical::Unsigned(2), 2, 1, None),
67 b'i' => (Physical::Signed(4), 4, 1, None),
68 b'I' => (Physical::Unsigned(4), 4, 1, None),
69 b'q' => (Physical::Signed(8), 8, 1, None),
70 b'Q' => (Physical::Unsigned(8), 8, 1, None),
71 b'f' => (Physical::Float(4), 4, 1, None),
72 b'd' => (Physical::Float(8), 8, 1, None),
73 b'g' => (Physical::Float(2), 2, 1, None),
74 b'n' => (Physical::Text, 4, 1, None),
75 b'N' => (Physical::Text, 16, 1, None),
76 b'Z' => (Physical::Text, 64, 1, None),
77 b'c' => (Physical::Signed(2), 2, 1, Some(0.01)),
78 b'C' => (Physical::Unsigned(2), 2, 1, Some(0.01)),
79 b'e' => (Physical::Signed(4), 4, 1, Some(0.01)),
80 b'E' => (Physical::Unsigned(4), 4, 1, Some(0.01)),
81 b'L' => (Physical::Signed(4), 4, 1, Some(1e-7)),
83 b'a' => (Physical::Signed(2), 2, 32, None),
84 _ => return None,
85 })
86}
87
88#[derive(Debug)]
90pub struct MessageType {
91 pub id: u8,
92 pub name: String,
93 pub length: usize,
95 pub format: String,
96 pub labels: Vec<String>,
97 pub offsets: Arc<Offsets>,
98 pub unit_ids: Option<String>,
100 pub mult_ids: Option<String>,
101}
102
103#[derive(Debug, Default)]
105pub struct Index {
106 pub types: BTreeMap<u8, MessageType>,
107 pub units: HashMap<u8, String>,
108 pub mults: HashMap<u8, f64>,
109 pub skipped: usize,
110 pub damaged: usize,
111 pub past_limit: usize,
112 pub bad_formats: Vec<String>,
115 pub cut_short: bool,
116}
117
118impl Index {
119 pub fn names(&self) -> Vec<(String, u8)> {
121 let mut names: Vec<(String, u8)> = self
122 .types
123 .values()
124 .filter(|t| !t.offsets.is_empty() && t.id != FMT)
125 .map(|t| (t.name.clone(), t.id))
126 .collect();
127 names.sort();
128 let mut seen: HashMap<String, usize> = HashMap::new();
130 for (name, _) in &names {
131 *seen.entry(name.clone()).or_default() += 1;
132 }
133 for (name, id) in &mut names {
134 if seen[name.as_str()] > 1 {
135 *name = format!("{name}.{id}");
136 }
137 }
138 names
139 }
140}
141
142fn text(bytes: &[u8]) -> String {
144 crate::fixed_records::text(bytes)
145}
146
147fn format_size(format: &str) -> Option<usize> {
149 format.bytes().try_fold(0usize, |n, c| {
150 let (_, width, count, _) = format_char(c)?;
151 n.checked_add(width * count)
152 })
153}
154
155fn fields<'a>(t: &MessageType, record: &'a [u8]) -> Vec<(&'a [u8], u8)> {
157 let mut at = 3;
158 let mut out = Vec::new();
159 for c in t.format.bytes() {
160 let Some((_, width, count, _)) = format_char(c) else {
161 break;
162 };
163 let len = width * count;
164 let Some(bytes) = record.get(at..at + len) else {
165 break;
166 };
167 out.push((bytes, c));
168 at += len;
169 }
170 out
171}
172
173pub fn index(data: &[u8]) -> std::result::Result<Index, String> {
175 if !looks_like(data) {
176 return Err("not a DataFlash log: it does not start with an FMT record".into());
177 }
178 let mut index = Index::default();
179 let mut records = 0usize;
180 let mut fmtu: Vec<(u8, String, String)> = Vec::new();
181 let mut building: HashMap<u8, Offsets> = HashMap::new();
182 let mut at = 0usize;
183 while at < data.len() {
184 if data.get(at..at + 2) != Some(&HEAD[..]) {
185 match memchr::memmem::find(&data[at + 1..], &HEAD) {
187 Some(i) => {
188 index.skipped += i + 1;
189 index.damaged += 1;
190 at += i + 1;
191 continue;
192 }
193 None => {
194 index.skipped += data.len() - at;
195 index.damaged += 1;
196 break;
197 }
198 }
199 }
200 let Some(&id) = data.get(at + 2) else {
201 index.cut_short = true;
202 break;
203 };
204 if id == FMT {
205 let Some(record) = data.get(at..at + FMT_LEN) else {
206 index.cut_short = true;
207 break;
208 };
209 let defined = record[3];
210 let length = record[4] as usize;
211 let name = text(&record[5..9]);
212 let format = text(&record[9..25]);
213 let labels: Vec<String> = text(&record[25..89])
214 .split(',')
215 .map(str::to_string)
216 .collect();
217 match format_size(&format) {
218 Some(size) if size + 3 == length && length >= 3 => {
219 let same = index
222 .types
223 .get(&defined)
224 .is_some_and(|t| t.length == length && t.format == format);
225 if !same {
226 building.insert(defined, Offsets::for_file(data.len()));
227 }
228 index.types.insert(
229 defined,
230 MessageType {
231 id: defined,
232 name,
233 length,
234 format,
235 labels,
236 offsets: Arc::new(Offsets::Narrow(Vec::new())),
237 unit_ids: None,
238 mult_ids: None,
239 },
240 );
241 }
242 _ => {
243 if index.bad_formats.len() < 1000 {
244 index.bad_formats.push(format!("{name} ({format})"));
245 }
246 }
247 }
248 at += FMT_LEN;
249 continue;
250 }
251 let Some(t) = index.types.get_mut(&id) else {
252 match memchr::memmem::find(&data[at + 2..], &HEAD) {
254 Some(i) => {
255 index.skipped += i + 2;
256 index.damaged += 1;
257 at += i + 2;
258 continue;
259 }
260 None => {
261 index.skipped += data.len() - at;
262 index.damaged += 1;
263 break;
264 }
265 }
266 };
267 let Some(record) = data.get(at..at + t.length) else {
268 index.cut_short = true;
269 break;
270 };
271 match t.name.as_str() {
272 "FMTU" => {
273 let f = fields(t, record);
274 if let [_, (ty, _), (units, _), (mults, _), ..] = f.as_slice()
275 && let Some(&ty) = ty.first()
276 && fmtu.len() < 1000
277 {
278 fmtu.push((ty, text(units), text(mults)));
279 }
280 }
281 "UNIT" => {
282 let f = fields(t, record);
283 if let [_, (unit_id, _), (label, _), ..] = f.as_slice()
284 && let Some(&unit_id) = unit_id.first()
285 {
286 index.units.insert(unit_id, text(label));
287 }
288 }
289 "MULT" => {
290 let f = fields(t, record);
291 if let [_, (mult_id, _), (mult, b'd'), ..] = f.as_slice()
292 && let Some(&mult_id) = mult_id.first()
293 && let Ok(raw) = <[u8; 8]>::try_from(*mult)
294 {
295 index.mults.insert(mult_id, f64::from_le_bytes(raw));
296 }
297 }
298 _ => {}
299 }
300 if records < crate::indexed::MAX_RECORDS {
301 building
302 .entry(id)
303 .or_insert_with(|| Offsets::for_file(data.len()))
304 .push(at);
305 records += 1;
306 } else {
307 index.past_limit += 1;
308 }
309 at += t.length;
310 }
311 for (ty, units, mults) in fmtu {
312 if let Some(t) = index.types.get_mut(&ty) {
313 t.unit_ids = Some(units);
314 t.mult_ids = Some(mults);
315 }
316 }
317 for (id, mut offsets) in building {
318 if let Some(t) = index.types.get_mut(&id) {
319 offsets.shrink();
320 t.offsets = Arc::new(offsets);
321 }
322 }
323 Ok(index)
324}
325
326pub fn columns(index: &Index, t: &MessageType) -> (Vec<ColumnLayout>, Vec<(String, String)>) {
328 let mut columns = Vec::new();
329 let mut units = Vec::new();
330 let mut at = 3;
331 for (i, c) in t.format.bytes().enumerate() {
332 let Some((physical, width, count, scale)) = format_char(c) else {
333 break;
334 };
335 let mut name = t
336 .labels
337 .get(i)
338 .filter(|l| !l.is_empty())
339 .cloned()
340 .unwrap_or_else(|| format!("field{i}"));
341 if columns
343 .iter()
344 .any(|c: &ColumnLayout| c.name == name.as_str())
345 {
346 name = format!("{name}_{i}");
347 }
348 let mut column = match physical {
349 Physical::Text => ColumnLayout::new(&name, at, 0, physical, width),
350 _ => {
351 let mut c = ColumnLayout::new(&name, at, 0, physical, width);
352 c.count = count;
353 c
354 }
355 };
356 let mult = t
357 .mult_ids
358 .as_ref()
359 .and_then(|m| m.as_bytes().get(i))
360 .and_then(|id| index.mults.get(id))
361 .copied();
362 if (name == "TimeUS" && c == b'Q') || (name == "TimeMS" && c == b'I') {
363 column.logical = Logical::Duration {
364 unit: if c == b'Q' {
365 TimeUnit::Microseconds
366 } else {
367 TimeUnit::Milliseconds
368 },
369 multiplier: 1,
370 };
371 } else if let Some(scale) = scale {
372 column.logical = Logical::Linear {
373 factor: scale,
374 offset: 0.0,
375 };
376 } else if physical.is_integer()
377 && count == 1
378 && let Some(mult) = mult.filter(|m| *m != 1.0 && *m != 0.0 && m.is_finite())
379 {
380 column.logical = Logical::Linear {
382 factor: mult,
383 offset: 0.0,
384 };
385 }
386 if let Some(unit) = t
387 .unit_ids
388 .as_ref()
389 .and_then(|u| u.as_bytes().get(i))
390 .and_then(|id| index.units.get(id))
391 .filter(|u| !u.is_empty())
392 {
393 units.push((name.clone(), unit.clone()));
394 }
395 columns.push(column);
396 at += width * count;
397 }
398 (columns, units)
399}
400
401pub fn listed(file: &Path) -> Result<Vec<Table>> {
404 crate::indexed::peek::<Index>(file)
405 .map(|index| tables(&index))
406 .ok_or_else(|| eyre!("Open the log to list its tables."))
407}
408
409pub fn tables(index: &Index) -> Vec<Table> {
411 index
412 .names()
413 .into_iter()
414 .map(|(name, id)| Table {
415 name,
416 kind: "message".to_string(),
417 internal: false,
418 columns: columns(index, &index.types[&id])
419 .0
420 .iter()
421 .map(|c| (c.name.to_string(), String::new()))
422 .collect(),
423 })
424 .collect()
425}
426
427pub fn detail(index: &Index) -> Detail {
429 let names = index.names();
430 let records: usize = names
431 .iter()
432 .map(|(_, id)| index.types[id].offsets.len())
433 .sum();
434 let lines = vec![
435 format!(
436 "Message types: {} with records, {} defined",
437 names.len(),
438 index.types.len()
439 ),
440 format!("Records: {}", crate::numfmt::group_chrome(records)),
441 format!(
442 "Units: {}",
443 if index.units.is_empty() {
444 "none in the log".to_string()
445 } else {
446 format!("{} (FMTU, UNIT, MULT)", index.units.len())
447 }
448 ),
449 ];
450 let list = names.iter().map(|(name, id)| {
451 let t = &index.types[id];
452 (
453 name.clone(),
454 MetaValue::Text(format!(
455 "{} records, format {}, {} bytes",
456 crate::numfmt::group_chrome(t.offsets.len()),
457 t.format,
458 t.length
459 )),
460 )
461 });
462 Detail {
463 tab: crate::text_formats::tab(crate::FileFormat::Dataflash),
464 lines,
465 list_title: "Messages",
466 list: crate::text_formats::capped_list(list, names.len()),
467 first: false,
468 ..Default::default()
469 }
470}
471
472pub fn notes(index: &Index) -> Vec<String> {
474 let group = |n: usize| crate::numfmt::group_chrome(n);
475 let mut notes = Vec::new();
476 if index.damaged > 0 {
477 notes.push(format!(
478 "{} damaged stretches skipped ({} bytes)",
479 group(index.damaged),
480 group(index.skipped)
481 ));
482 }
483 if index.cut_short {
484 notes.push("log cut short mid-record".to_string());
485 }
486 if index.past_limit > 0 {
487 notes.push(format!(
488 "{} records left out: past the first {}",
489 group(index.past_limit),
490 group(crate::indexed::MAX_RECORDS)
491 ));
492 }
493 if !index.bad_formats.is_empty() {
494 notes.push(format!(
495 "{} message types not read, format unreadable: {}",
496 index.bad_formats.len(),
497 index
498 .bad_formats
499 .iter()
500 .take(10)
501 .cloned()
502 .collect::<Vec<_>>()
503 .join(", ")
504 ));
505 }
506 notes
507}
508
509pub fn indexed(path: &Path) -> Result<(Arc<Bytes>, Arc<Index>)> {
511 let bytes = Arc::new(Bytes::map(path).map_err(|e| in_file(path, e.into()))?);
512 let index = crate::indexed::cached(path, || index(bytes.as_slice()))
513 .map_err(|e| FileError::new(path, e))?;
514 Ok((bytes, index))
515}
516
517pub enum Open {
519 Table {
520 lf: Box<LazyFrame>,
521 opened: Box<crate::members::Opened>,
522 },
523 Several(Vec<String>),
524}
525
526pub fn open(path: &Path, wanted: Option<&str>) -> Result<Open> {
529 let (bytes, index) = indexed(path)?;
530 let tables = tables(&index);
531 let picked = match crate::members::pick(
532 tables.clone(),
533 wanted,
534 path,
535 " The log has no records but its formats.",
536 )? {
537 crate::sqlite::Pick::One(table) => table.name,
538 crate::sqlite::Pick::Several(tables) => {
539 return Ok(Open::Several(tables.into_iter().map(|t| t.name).collect()));
540 }
541 };
542 let id = index
543 .names()
544 .into_iter()
545 .find(|(n, _)| *n == picked)
546 .map(|(_, id)| id)
547 .expect("picked from the names");
548 let t = &index.types[&id];
549 let (columns, units) = columns(&index, t);
550 let records = Arc::new(
551 IndexedRecords::new(bytes, t.offsets.clone(), columns)
552 .map_err(|e| FileError::new(path, format!("table \"{picked}\": {e}")))?,
553 );
554 let opened = crate::members::Opened {
555 window: Some((records.clone(), records.rows())),
556 detail: Some(Arc::new(detail(&index))),
557 other_tables: crate::members::others(&tables, &picked),
558 notes: notes(&index)
559 .into_iter()
560 .map(|n| crate::text_formats::note(n, "the log".to_string()))
561 .collect(),
562 units,
563 indexing: None,
564 numbering: None,
565 };
566 Ok(Open::Table {
567 lf: Box::new(records.lazy()),
568 opened: Box::new(opened),
569 })
570}
571
572fn scan(input: crate::readers::ScanIn<'_>) -> Result<crate::scan::Scan> {
576 let file = input.path();
577 Ok(match open(file, input.options.table.as_deref())? {
578 Open::Table { lf, opened } => {
579 input.report.opened = Some(Arc::new(*opened));
580 (*lf).into()
581 }
582 Open::Several(tables) => crate::scan::Scan::Tables {
583 file: file.to_path_buf(),
584 tables,
585 format: input.format,
586 },
587 })
588}
589
590#[cfg(test)]
591pub(crate) mod tests {
592 use super::*;
593
594 #[test]
596 fn errors_name_the_file() {
597 crate::readers::bad_input::each_names_its_file(
598 crate::FileFormat::Dataflash,
599 &[(
600 "text.bin",
601 b"hello there, this is text",
602 "Not a DataFlash log",
603 )],
604 );
605 }
606
607 fn fmt(id: u8, name: &str, format: &str, labels: &str) -> Vec<u8> {
608 let length = if id == FMT {
609 FMT_LEN
610 } else {
611 format_size(format).unwrap() + 3
612 };
613 let mut r = vec![0xA3, 0x95, FMT, id, length as u8];
614 let pad = |s: &str, n: usize| {
615 let mut b = s.as_bytes().to_vec();
616 b.resize(n, 0);
617 b
618 };
619 r.extend(pad(name, 4));
620 r.extend(pad(format, 16));
621 r.extend(pad(labels, 64));
622 r
623 }
624
625 pub(crate) fn tiny() -> Vec<u8> {
628 let mut log = fmt(FMT, "FMT", "BBnNZ", "Type,Length,Name,Format,Columns");
629 log.extend(fmt(129, "UNIT", "QbZ", "TimeUS,Id,Label"));
630 log.extend(fmt(130, "MULT", "Qbd", "TimeUS,Id,Mult"));
631 log.extend(fmt(131, "FMTU", "QBNN", "TimeUS,FmtType,UnitIds,MultIds"));
632 log.extend(fmt(140, "ATT", "QccH", "TimeUS,Roll,Pitch,Yaw"));
633 log.extend(fmt(141, "BARO", "QfiL", "TimeUS,Alt,Press,Lat"));
634 let unit = |id: u8, label: &str| {
635 let mut r = vec![0xA3, 0x95, 129];
636 r.extend(0u64.to_le_bytes());
637 r.push(id);
638 let mut l = label.as_bytes().to_vec();
639 l.resize(64, 0);
640 r.extend(l);
641 r
642 };
643 log.extend(unit(b'd', "deg"));
644 log.extend(unit(b'm', "m"));
645 log.extend(unit(b'P', "Pa"));
646 let mut mult = vec![0xA3, 0x95, 130];
647 mult.extend(0u64.to_le_bytes());
648 mult.push(b'2');
649 mult.extend(100.0f64.to_le_bytes());
650 log.extend(mult);
651 let fmtu = |ty: u8, units: &str, mults: &str| {
652 let mut r = vec![0xA3, 0x95, 131];
653 r.extend(0u64.to_le_bytes());
654 r.push(ty);
655 for s in [units, mults] {
656 let mut b = s.as_bytes().to_vec();
657 b.resize(16, 0);
658 r.extend(b);
659 }
660 r
661 };
662 log.extend(fmtu(141, "-mPd", "-02?"));
663 for i in 0..3u64 {
664 let mut att = vec![0xA3, 0x95, 140];
665 att.extend((1000 + i * 10).to_le_bytes());
666 att.extend((150i16 + i as i16).to_le_bytes());
667 att.extend((-250i16).to_le_bytes());
668 att.extend(9000u16.to_le_bytes());
669 log.extend(att);
670 let mut baro = vec![0xA3, 0x95, 141];
671 baro.extend((1005 + i * 10).to_le_bytes());
672 baro.extend((12.5f32 + i as f32).to_le_bytes());
673 baro.extend(101_325i32.to_le_bytes());
674 baro.extend(473_977_418i32.to_le_bytes());
675 log.extend(baro);
676 if i == 1 {
677 log.extend([0x00, 0xA3, 0x11]);
678 }
679 }
680 log.extend([0xA3, 0x95, 140, 1, 2]);
681 log
682 }
683
684 #[test]
685 fn types_records_units_and_damage() {
686 let data = tiny();
687 assert!(looks_like(&data));
688 let index = index(&data).unwrap();
689 let names: Vec<String> = index.names().into_iter().map(|(n, _)| n).collect();
690 assert_eq!(names, ["ATT", "BARO", "FMTU", "MULT", "UNIT"]);
691 assert_eq!(index.types[&140].offsets.len(), 3);
692 assert_eq!(index.damaged, 1);
693 assert!(index.cut_short);
694 let (columns, units) = columns(&index, &index.types[&141]);
695 assert_eq!(
696 units,
697 [
698 ("Alt".to_string(), "m".to_string()),
699 ("Press".to_string(), "Pa".to_string()),
700 ("Lat".to_string(), "deg".to_string())
701 ]
702 );
703 let records = Arc::new(
704 IndexedRecords::new(
705 Arc::new(Bytes::Owned(data)),
706 index.types[&141].offsets.clone(),
707 columns,
708 )
709 .unwrap(),
710 );
711 let df = records.lazy().collect().unwrap();
712 assert_eq!(
713 df.column("TimeUS").unwrap().dtype(),
714 &DataType::Duration(TimeUnit::Microseconds)
715 );
716 let lat = df.column("Lat").unwrap().f64().unwrap().get(0).unwrap();
718 assert!((lat - 47.3977418).abs() < 1e-9);
719 assert_eq!(
720 df.column("Press").unwrap().f64().unwrap().get(0),
721 Some(10_132_500.0)
722 );
723 }
724
725 #[test]
726 fn scaled_characters() {
727 let data = tiny();
728 let index = index(&data).unwrap();
729 let (columns, _) = columns(&index, &index.types[&140]);
730 let records = Arc::new(
731 IndexedRecords::new(
732 Arc::new(Bytes::Owned(data)),
733 index.types[&140].offsets.clone(),
734 columns,
735 )
736 .unwrap(),
737 );
738 let df = records.lazy().collect().unwrap();
739 assert_eq!(
740 df.column("Roll").unwrap().f64().unwrap().to_vec(),
741 [Some(1.5), Some(1.51), Some(1.52)]
742 );
743 assert_eq!(df.column("Yaw").unwrap().u16().unwrap().get(0), Some(9000));
744 }
745
746 #[test]
747 fn garbage_is_bounded() {
748 assert!(index(b"nope").is_err());
749 let mut data = fmt(FMT, "FMT", "BBnNZ", "Type,Length,Name,Format,Columns");
750 data.extend([0xA3, 0x95, 0x80, 7, 200]);
751 data.extend([0xA3; 50]);
752 let index = index(&data).unwrap();
753 assert!(index.names().is_empty());
754 }
755}