1use 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
24pub(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];
40pub(crate) const FMT: u8 = 0x80;
42pub(crate) const FMT_LEN: usize = 89;
44
45pub 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
55fn 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 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#[derive(Debug)]
85pub struct MessageType {
86 pub id: u8,
87 pub name: String,
88 pub length: usize,
90 pub format: String,
91 pub labels: Vec<String>,
92 pub offsets: Arc<Offsets>,
93 pub unit_ids: Option<String>,
95 pub mult_ids: Option<String>,
96}
97
98#[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 pub bad_formats: Vec<String>,
110 pub cut_short: bool,
111}
112
113impl Index {
114 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 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
137fn text(bytes: &[u8]) -> String {
139 crate::formats::fixed_records::text(bytes)
140}
141
142pub(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
150fn 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
168pub 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 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 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 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
322pub 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 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 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 #[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 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}