1use std::collections::{BTreeMap, HashMap};
16use std::path::{Path, PathBuf};
17use std::sync::Arc;
18
19use color_eyre::Result;
20use color_eyre::eyre::eyre;
21
22use crate::error_display::{FileError, in_file};
23use polars::prelude::*;
24
25use crate::dbc::{Dbc, Message, Mux, Signal};
26use crate::fixed_records::{Bytes, ColumnLayout, Logical, Physical};
27use crate::indexed::Offsets;
28use crate::model_files::MetaValue;
29use crate::sqlite::Table;
30use crate::text_formats::Detail;
31
32pub(crate) const READER: crate::readers::Reader = crate::readers::Reader {
34 scan,
35 signatures: &[crate::readers::Signature {
36 says: |head, _| looks_like(head),
37 kind: crate::readers::Kind::Text,
38 trusted: crate::readers::Trusted {
39 tables: true,
40 ..crate::readers::EVERYWHERE
41 },
42 }],
43 tables: Some(|path| {
44 listed(path).ok_or_else(|| color_eyre::eyre::eyre!("Open the log to list its tables."))
45 }),
46 ..crate::readers::BASE
47};
48
49const MAX_LINE: usize = 4096;
51const MAX_INTERFACES: usize = 255;
53const MAX_LONG_PARTS: usize = 10_000;
55
56pub const FRAMES: &str = "frames";
58pub const SIGNALS: &str = "signals";
59
60#[derive(Debug, Clone, PartialEq)]
62pub struct Frame<'a> {
63 pub ts: Option<i64>,
65 pub iface: &'a str,
66 pub id: u32,
67 pub extended: bool,
68 pub fd: bool,
69 pub flags: Option<u8>,
71 pub remote: bool,
72 pub error: bool,
73 pub dlc: u8,
74 pub data: Vec<u8>,
75}
76
77fn hex_bytes(text: &str) -> Option<Vec<u8>> {
78 if !text.len().is_multiple_of(2) || text.len() > 128 {
79 return None;
80 }
81 (0..text.len())
82 .step_by(2)
83 .map(|i| u8::from_str_radix(text.get(i..i + 2)?, 16).ok())
84 .collect()
85}
86
87fn parse_id(text: &str) -> Option<(u32, bool)> {
89 if !text.bytes().all(|b| b.is_ascii_hexdigit()) {
90 return None;
91 }
92 match text.len() {
93 1..=3 => Some((u32::from_str_radix(text, 16).ok()?, false)),
94 8 => Some((u32::from_str_radix(text, 16).ok()?, true)),
95 _ => None,
96 }
97}
98
99fn parse_ts(text: &str) -> Option<i64> {
101 if let Some((s, frac)) = text.split_once('.')
102 && !s.is_empty()
103 && s.bytes().all(|b| b.is_ascii_digit())
104 && !frac.is_empty()
105 && frac.bytes().all(|b| b.is_ascii_digit())
106 {
107 let secs: i64 = s.parse().ok()?;
108 let digits = &frac[..frac.len().min(6)];
109 let micros: i64 = digits.parse::<i64>().ok()? * 10i64.pow(6 - digits.len() as u32);
110 return secs.checked_mul(1_000_000)?.checked_add(micros);
111 }
112 let parsed = chrono::NaiveDateTime::parse_from_str(text, "%Y-%m-%d %H:%M:%S%.f").ok()?;
114 Some(parsed.and_utc().timestamp_micros())
115}
116
117pub fn parse_line(line: &str) -> Option<Frame<'_>> {
119 let line = line.trim();
120 if line.is_empty() || line.len() > MAX_LINE {
121 return None;
122 }
123 let (ts, rest) = match line.strip_prefix('(') {
124 Some(after) => {
125 let (inside, rest) = after.split_once(')')?;
126 (Some(parse_ts(inside.trim())?), rest.trim_start())
127 }
128 None => (None, line),
129 };
130 let mut words = rest.split_whitespace();
131 let iface = words.next()?;
132 let next = words.next()?;
133 if let Some((id, frame)) = next.split_once('#') {
134 return parse_compact(ts, iface, id, frame);
135 }
136 let mut id_word = next;
138 let mut dlc_word = words.next()?;
139 let mut guard = 0;
140 while !(dlc_word.starts_with('[') && dlc_word.ends_with(']')) {
141 id_word = dlc_word;
142 dlc_word = words.next()?;
143 guard += 1;
144 if guard > 4 {
145 return None;
146 }
147 }
148 let (id, extended) = parse_id(id_word)?;
149 let dlc: u8 = dlc_word[1..dlc_word.len() - 1].parse().ok()?;
150 let rest: Vec<&str> = words.collect();
151 if rest.first() == Some(&"remote") {
152 return Some(Frame {
153 ts,
154 iface,
155 id,
156 extended,
157 fd: false,
158 flags: None,
159 remote: true,
160 error: false,
161 dlc,
162 data: Vec::new(),
163 });
164 }
165 let data: Vec<u8> = rest
166 .iter()
167 .take(dlc as usize)
168 .map(|b| {
169 (b.len() == 2)
170 .then(|| u8::from_str_radix(b, 16).ok())
171 .flatten()
172 })
173 .collect::<Option<_>>()?;
174 if data.len() != dlc as usize || dlc > 64 {
175 return None;
176 }
177 Some(Frame {
178 ts,
179 iface,
180 id,
181 extended,
182 fd: dlc > 8,
183 flags: None,
184 remote: false,
185 error: extended && id & 0x2000_0000 != 0,
186 dlc,
187 data,
188 })
189}
190
191fn parse_compact<'a>(ts: Option<i64>, iface: &'a str, id: &str, frame: &str) -> Option<Frame<'a>> {
192 let (id, extended) = parse_id(id)?;
193 let error = extended && id & 0x2000_0000 != 0;
194 let mut out = Frame {
195 ts,
196 iface,
197 id,
198 extended,
199 fd: false,
200 flags: None,
201 remote: false,
202 error,
203 dlc: 0,
204 data: Vec::new(),
205 };
206 if let Some(fd) = frame.strip_prefix('#') {
207 if fd.starts_with('#') {
209 return None;
210 }
211 let mut chars = fd.chars();
212 let flags = chars.next()?.to_digit(16)? as u8;
213 let data = hex_bytes(chars.as_str())?;
214 out.fd = true;
215 out.flags = Some(flags);
216 out.dlc = data.len() as u8;
217 out.data = data;
218 return Some(out);
219 }
220 if let Some(remote) = frame.strip_prefix('R').or_else(|| frame.strip_prefix('r')) {
221 out.remote = true;
222 out.dlc = if remote.is_empty() {
223 0
224 } else {
225 remote.parse().ok().filter(|d| *d <= 8)?
226 };
227 return Some(out);
228 }
229 let (data, code) = match frame.split_once('_') {
231 Some((data, code)) => (data, u8::from_str_radix(code, 16).ok()),
232 None => (frame, None),
233 };
234 let data = hex_bytes(data)?;
235 if data.len() > 8 {
236 return None;
237 }
238 out.dlc = code.unwrap_or(data.len() as u8);
239 out.data = data;
240 Some(out)
241}
242
243pub fn looks_like(head: &[u8]) -> bool {
245 let text = String::from_utf8_lossy(head);
246 let mut lines = text.lines().filter(|l| !l.trim().is_empty());
247 let Some(first) = lines.next() else {
248 return false;
249 };
250 let cut = !text.contains('\n');
253 parse_line(first).is_some()
254 || (cut
255 && first
256 .char_indices()
257 .last()
258 .is_some_and(|(at, _)| parse_line(&first[..at]).is_some()))
259}
260
261#[derive(Debug, Default)]
263pub struct Index {
264 pub offsets: Arc<Offsets>,
266 pub keys: Vec<u32>,
268 pub ifaces: Vec<u8>,
270 pub interfaces: Vec<String>,
271 pub absolute: bool,
273 pub skipped: usize,
275 pub past_limit: usize,
276}
277
278pub fn index(data: &[u8]) -> std::result::Result<Index, String> {
280 let mut offsets = Offsets::for_file(data.len());
281 let mut index = Index::default();
282 let mut ifaces: HashMap<String, u8> = HashMap::new();
283 let mut first_ts = None;
284 let mut at = 0usize;
285 while at < data.len() {
286 let end = memchr::memchr(b'\n', &data[at..]).map_or(data.len(), |i| at + i);
287 let line = &data[at..end];
288 let parsed = (line.len() <= MAX_LINE)
289 .then(|| std::str::from_utf8(line).ok())
290 .flatten()
291 .and_then(parse_line);
292 match parsed {
293 Some(frame) if offsets.len() < crate::indexed::MAX_RECORDS => {
294 if first_ts.is_none() {
295 first_ts = frame.ts;
296 }
297 let iface = match ifaces.get(frame.iface) {
298 Some(&i) => i,
299 None if index.interfaces.len() < MAX_INTERFACES => {
300 let i = index.interfaces.len() as u8;
301 index.interfaces.push(frame.iface.to_string());
302 ifaces.insert(frame.iface.to_string(), i);
303 i
304 }
305 None => (MAX_INTERFACES - 1) as u8,
306 };
307 offsets.push(at);
308 index
309 .keys
310 .push(frame.id | (u32::from(frame.extended) << 31));
311 index.ifaces.push(iface);
312 }
313 Some(_) => index.past_limit += 1,
314 None if line.iter().all(|b| b.is_ascii_whitespace()) => {}
315 None => index.skipped += 1,
316 }
317 at = end + 1;
318 }
319 if offsets.is_empty() {
320 return Err("no line is a CAN frame as candump writes them".into());
321 }
322 offsets.shrink();
323 index.keys.shrink_to_fit();
324 index.ifaces.shrink_to_fit();
325 index.offsets = Arc::new(offsets);
326 index.absolute = first_ts.is_some_and(|ts| ts >= 978_307_200_000_000);
328 Ok(index)
329}
330
331fn line_of(bytes: &[u8], at: usize) -> &str {
333 let end = memchr::memchr(b'\n', &bytes[at..]).map_or(bytes.len(), |i| at + i);
334 std::str::from_utf8(&bytes[at..end.min(at + MAX_LINE)]).unwrap_or_default()
335}
336
337fn ts_dtype(absolute: bool) -> DataType {
338 if absolute {
339 DataType::Datetime(TimeUnit::Microseconds, None)
340 } else {
341 DataType::Duration(TimeUnit::Microseconds)
342 }
343}
344
345fn ts_series(values: Vec<Option<i64>>, absolute: bool) -> Series {
346 let ca: Int64Chunked = values.into_iter().collect();
347 if absolute {
348 ca.into_datetime(TimeUnit::Microseconds, None).into_series()
349 } else {
350 ca.into_duration(TimeUnit::Microseconds).into_series()
351 }
352}
353
354pub struct RawFrames {
356 bytes: Arc<Bytes>,
357 offsets: Arc<Offsets>,
358 absolute: bool,
359 schema: SchemaRef,
360}
361
362impl RawFrames {
363 pub fn new(bytes: Arc<Bytes>, index: &Index) -> Self {
364 let schema = Schema::from_iter([
365 Field::new("ts".into(), ts_dtype(index.absolute)),
366 Field::new("iface".into(), DataType::String),
367 Field::new("id".into(), DataType::String),
368 Field::new("ext".into(), DataType::Boolean),
369 Field::new("dlc".into(), DataType::UInt8),
370 Field::new("data".into(), DataType::Binary),
371 Field::new("fd".into(), DataType::Boolean),
372 Field::new("flags".into(), DataType::UInt8),
373 Field::new("kind".into(), DataType::String),
374 ]);
375 Self {
376 bytes,
377 offsets: index.offsets.clone(),
378 absolute: index.absolute,
379 schema: Arc::new(schema),
380 }
381 }
382
383 pub fn rows(&self) -> usize {
384 self.offsets.len().min(crate::row_index::MAX_ROWS)
385 }
386
387 fn column(&self, column: usize, rows: impl Iterator<Item = usize>) -> PolarsResult<Column> {
388 self.bytes.still_whole()?;
389 let bytes = self.bytes.as_slice();
390 let frames: Vec<Option<Frame<'_>>> = rows
391 .map(|r| parse_line(line_of(bytes, self.offsets.get(r))))
392 .collect();
393 let name: PlSmallStr = self
394 .schema
395 .get_at_index(column)
396 .expect("a column")
397 .0
398 .clone();
399 let series = match column {
400 0 => ts_series(
401 frames
402 .iter()
403 .map(|f| f.as_ref().and_then(|f| f.ts))
404 .collect(),
405 self.absolute,
406 ),
407 1 => frames
408 .iter()
409 .map(|f| f.as_ref().map(|f| f.iface))
410 .collect::<StringChunked>()
411 .into_series(),
412 2 => frames
413 .iter()
414 .map(|f| {
415 f.as_ref().map(|f| {
416 if f.extended {
417 format!("{:08X}", f.id)
418 } else {
419 format!("{:03X}", f.id)
420 }
421 })
422 })
423 .collect::<StringChunked>()
424 .into_series(),
425 3 => frames
426 .iter()
427 .map(|f| f.as_ref().map(|f| f.extended))
428 .collect::<BooleanChunked>()
429 .into_series(),
430 4 => frames
431 .iter()
432 .map(|f| f.as_ref().map(|f| f.dlc))
433 .collect::<UInt8Chunked>()
434 .into_series(),
435 5 => frames
436 .iter()
437 .map(|f| f.as_ref().map(|f| f.data.as_slice()))
438 .collect::<BinaryChunked>()
439 .into_series(),
440 6 => frames
441 .iter()
442 .map(|f| f.as_ref().map(|f| f.fd))
443 .collect::<BooleanChunked>()
444 .into_series(),
445 7 => frames
446 .iter()
447 .map(|f| f.as_ref().and_then(|f| f.flags))
448 .collect::<UInt8Chunked>()
449 .into_series(),
450 _ => frames
451 .iter()
452 .map(|f| {
453 f.as_ref().map(|f| {
454 if f.error {
455 "error"
456 } else if f.remote {
457 "remote"
458 } else {
459 "data"
460 }
461 })
462 })
463 .collect::<StringChunked>()
464 .into_series(),
465 };
466 Ok(series.with_name(name).into_column())
467 }
468
469 pub fn collect_window(&self, start: usize, len: usize) -> PolarsResult<DataFrame> {
470 let start = start.min(self.rows());
471 let len = len.min(self.rows() - start);
472 let columns = (0..self.schema.len())
473 .map(|c| self.column(c, start..start + len))
474 .collect::<PolarsResult<Vec<_>>>()?;
475 DataFrame::new(len, columns)
476 }
477}
478
479impl crate::row_index::RowSource for RawFrames {
480 fn height(&self) -> usize {
481 self.rows()
482 }
483
484 fn schema(&self) -> SchemaRef {
485 self.schema.clone()
486 }
487
488 fn decode(&self, column: usize, index: &IdxCa) -> PolarsResult<Column> {
489 let rows = crate::row_index::checked(index, self.rows())?;
490 self.column(column, rows.iter().map(|&r| r as usize))
491 }
492}
493
494impl crate::pushdown::Windowed for RawFrames {
495 fn window(&self, start: usize, len: usize) -> PolarsResult<LazyFrame> {
496 Ok(self.collect_window(start, len)?.lazy())
497 }
498}
499
500pub struct Decoded {
502 bytes: Arc<Bytes>,
503 offsets: Arc<Offsets>,
504 rows: Arc<Vec<u32>>,
506 message: Arc<Message>,
507 absolute: bool,
508 named: bool,
510 schema: SchemaRef,
511}
512
513fn signal_layout(signal: &Signal, named: bool) -> ColumnLayout {
516 let whole = signal.float == 0 && signal.factor == 1.0 && signal.offset.fract() == 0.0;
517 let physical = if signal.signed || signal.offset < 0.0 {
518 Physical::Signed(8)
519 } else {
520 Physical::Unsigned(8)
521 };
522 let mut layout = ColumnLayout::new(&signal.name, 0, 8, physical, 8);
523 layout.logical = if named && !signal.values.is_empty() && whole && signal.offset == 0.0 {
524 Logical::Enum(Arc::new(
525 signal.values.iter().map(|(k, v)| (*k, v.clone())).collect(),
526 ))
527 } else if whole && signal.offset == 0.0 {
528 Logical::Plain
529 } else {
530 Logical::Linear {
531 factor: signal.factor,
532 offset: signal.offset,
533 }
534 };
535 layout
536}
537
538impl Decoded {
539 pub fn new(
540 bytes: Arc<Bytes>,
541 index: &Index,
542 rows: Arc<Vec<u32>>,
543 message: Arc<Message>,
544 named: bool,
545 ) -> PolarsResult<Self> {
546 let mut fields = vec![Field::new("ts".into(), ts_dtype(index.absolute))];
547 for s in &message.signals {
548 let dtype = if s.float != 0 {
549 DataType::Float64
550 } else {
551 signal_layout(s, named).dtype()
552 };
553 fields.push(Field::new(s.name.as_str().into(), dtype));
554 }
555 let schema: Schema = fields.into_iter().collect();
556 polars_ensure!(
557 schema.len() == message.signals.len() + 1,
558 Duplicate: "{} has two signals of one name, or one named ts", message.name
559 );
560 Ok(Self {
561 bytes,
562 offsets: index.offsets.clone(),
563 rows,
564 message,
565 absolute: index.absolute,
566 named,
567 schema: Arc::new(schema),
568 })
569 }
570
571 pub fn height(&self) -> usize {
572 self.rows.len().min(crate::row_index::MAX_ROWS)
573 }
574
575 fn column(&self, column: usize, rows: impl Iterator<Item = usize>) -> PolarsResult<Column> {
576 self.bytes.still_whole()?;
577 let bytes = self.bytes.as_slice();
578 let frames: Vec<Option<Frame<'_>>> = rows
579 .map(|r| parse_line(line_of(bytes, self.offsets.get(self.rows[r] as usize))))
580 .collect();
581 let name = self
582 .schema
583 .get_at_index(column)
584 .expect("a column")
585 .0
586 .clone();
587 if column == 0 {
588 let ts = frames
589 .iter()
590 .map(|f| f.as_ref().and_then(|f| f.ts))
591 .collect();
592 return Ok(ts_series(ts, self.absolute).with_name(name).into_column());
593 }
594 let signal = &self.message.signals[column - 1];
595 let multiplexer = self
596 .message
597 .signals
598 .iter()
599 .find(|s| s.mux == Mux::Multiplexer);
600 let present = |f: &Frame<'_>| {
601 let mux = multiplexer.and_then(|m| crate::dbc::raw(m, &f.data));
602 crate::dbc::present(signal, mux)
603 };
604 let series = if signal.float != 0 {
605 frames
606 .iter()
607 .map(|f| {
608 let f = f.as_ref()?;
609 present(f).then(|| crate::dbc::physical(signal, &f.data))?
610 })
611 .collect::<Float64Chunked>()
612 .into_series()
613 } else {
614 let ints: Vec<Option<i128>> = frames
615 .iter()
616 .map(|f| {
617 let f = f.as_ref()?;
618 present(f).then(|| crate::dbc::integer(signal, &f.data))?
619 })
620 .collect();
621 crate::fixed_records::integers(&signal_layout(signal, self.named), ints)?
622 };
623 Ok(series.with_name(name).into_column())
624 }
625
626 pub fn collect_window(&self, start: usize, len: usize) -> PolarsResult<DataFrame> {
627 let start = start.min(self.height());
628 let len = len.min(self.height() - start);
629 let columns = (0..self.schema.len())
630 .map(|c| self.column(c, start..start + len))
631 .collect::<PolarsResult<Vec<_>>>()?;
632 DataFrame::new(len, columns)
633 }
634}
635
636impl crate::row_index::RowSource for Decoded {
637 fn height(&self) -> usize {
638 Decoded::height(self)
639 }
640
641 fn schema(&self) -> SchemaRef {
642 self.schema.clone()
643 }
644
645 fn decode(&self, column: usize, index: &IdxCa) -> PolarsResult<Column> {
646 let rows = crate::row_index::checked(index, self.height())?;
647 self.column(column, rows.iter().map(|&r| r as usize))
648 }
649}
650
651impl crate::pushdown::Windowed for Decoded {
652 fn window(&self, start: usize, len: usize) -> PolarsResult<LazyFrame> {
653 Ok(self.collect_window(start, len)?.lazy())
654 }
655}
656
657#[derive(Debug, Clone, Default)]
661pub struct Layers {
662 pub dbcs: Vec<Arc<Dbc>>,
663}
664
665impl Layers {
666 pub fn new(registry: &crate::formats::Registry, dicts: &[PathBuf]) -> Result<Self> {
668 let mut dbcs: Vec<Arc<Dbc>> = registry.dbc.iter().map(|f| f.dbc.clone()).collect();
669 for path in dicts {
670 match crate::dbc::load(path) {
671 Ok(Some(d)) => dbcs.push(Arc::new(d)),
672 Ok(None) => {
673 return Err(FileError::new(
674 path,
675 "not a DBC dictionary. --dict takes a .dbc file, or TOML with kind = \"dbc\".",
676 )
677 .into());
678 }
679 Err(e) => {
680 let at = e.path.as_deref().unwrap_or(path);
682 return Err(FileError::at(at, e.line, e.column, e.message).into());
683 }
684 }
685 }
686 Ok(Self { dbcs })
687 }
688
689 fn files(&self) -> Vec<PathBuf> {
690 self.dbcs.iter().filter_map(|d| d.path.clone()).collect()
691 }
692}
693
694#[derive(Debug, Default)]
697pub struct Listing {
698 pub layers: Layers,
699 pub messages: BTreeMap<String, (Arc<Message>, Arc<Vec<u32>>)>,
701 pub unknown: usize,
703}
704
705impl Listing {
706 pub fn resolve(index: &Index, layers: Layers) -> Self {
707 let mut known: HashMap<(u8, u32), Option<(usize, usize)>> = HashMap::new();
709 let lookup = |iface: u8, key: u32| -> Option<(usize, usize)> {
710 let name = index.interfaces.get(iface as usize)?;
711 let (id, extended) = (key & 0x7FFF_FFFF, key >> 31 == 1);
712 layers.dbcs.iter().enumerate().rev().find_map(|(l, dbc)| {
713 if !dbc.applies(name) {
714 return None;
715 }
716 dbc.messages
717 .iter()
718 .position(|m| m.id == id && m.extended == extended)
719 .map(|m| (l, m))
720 })
721 };
722 let mut rows: HashMap<(usize, usize), Vec<u32>> = HashMap::new();
723 let mut unknown = 0;
724 for (row, (&key, &iface)) in index.keys.iter().zip(&index.ifaces).enumerate() {
725 let found = *known
726 .entry((iface, key))
727 .or_insert_with(|| lookup(iface, key));
728 match found {
729 Some(at) => rows.entry(at).or_default().push(row as u32),
730 None => unknown += 1,
731 }
732 }
733 let mut messages = BTreeMap::new();
734 for ((l, m), rows) in rows {
735 let message = &layers.dbcs[l].messages[m];
736 let mut name = message.name.clone();
737 if name == FRAMES || name == SIGNALS || messages.contains_key(&name) {
738 name = format!("{name}.{}", layers.dbcs[l].name);
739 }
740 messages.insert(name, (Arc::new(message.clone()), Arc::new(rows)));
741 }
742 Self {
743 layers,
744 messages,
745 unknown,
746 }
747 }
748
749 pub fn tables(&self) -> Vec<Table> {
750 let mut tables = vec![Table {
751 name: FRAMES.to_string(),
752 kind: "frames".to_string(),
753 internal: false,
754 columns: [
755 "ts", "iface", "id", "ext", "dlc", "data", "fd", "flags", "kind",
756 ]
757 .iter()
758 .map(|c| (c.to_string(), String::new()))
759 .collect(),
760 }];
761 if self.messages.is_empty() {
762 return tables;
763 }
764 tables.push(Table {
765 name: SIGNALS.to_string(),
766 kind: "signals".to_string(),
767 internal: false,
768 columns: ["ts", "message", "signal", "value", "unit"]
769 .iter()
770 .map(|c| (c.to_string(), String::new()))
771 .collect(),
772 });
773 for (name, (message, _)) in &self.messages {
774 tables.push(Table {
775 name: name.clone(),
776 kind: "message".to_string(),
777 internal: false,
778 columns: std::iter::once("ts".to_string())
779 .chain(message.signals.iter().map(|s| s.name.clone()))
780 .map(|c| (c, String::new()))
781 .collect(),
782 });
783 }
784 tables
785 }
786}
787
788pub fn indexed(path: &Path) -> Result<(Arc<Bytes>, Arc<Index>)> {
790 let bytes = Arc::new(Bytes::map(path).map_err(|e| in_file(path, e.into()))?);
791 let index = crate::indexed::cached(path, || index(bytes.as_slice()))
792 .map_err(|e| FileError::new(path, e))?;
793 Ok((bytes, index))
794}
795
796pub fn listed(path: &Path) -> Option<Vec<Table>> {
798 crate::indexed::peek::<Listing>(path).map(|l| l.tables())
799}
800
801fn detail(index: &Index, listing: &Listing) -> Detail {
803 let group = crate::numfmt::group_chrome;
804 let mut lines = vec![
805 format!("Frames: {}", group(index.keys.len())),
806 format!("Interfaces: {}", index.interfaces.join(", ")),
807 format!(
808 "Timestamps: {}",
809 if index.absolute {
810 "wall clock"
811 } else {
812 "from the start of the capture"
813 }
814 ),
815 ];
816 if listing.layers.dbcs.is_empty() {
817 lines.push("DBC: none; --dict FILE or the format search path decodes signals".into());
818 } else {
819 for dbc in &listing.layers.dbcs {
820 lines.push(format!(
821 "DBC: {}{}, {} messages",
822 dbc.name,
823 dbc.interface
824 .as_ref()
825 .map(|i| format!(" on {i}"))
826 .unwrap_or_default(),
827 dbc.messages.len()
828 ));
829 }
830 lines.push(format!("Frames no DBC names: {}", group(listing.unknown)));
831 }
832 let list = listing.messages.iter().map(|(name, (message, rows))| {
833 (
834 name.clone(),
835 MetaValue::Text(format!(
836 "id {}, {} frames, {} signals{}",
837 if message.extended {
838 format!("{:08X}", message.id)
839 } else {
840 format!("{:03X}", message.id)
841 },
842 group(rows.len()),
843 message.signals.len(),
844 message
845 .comment
846 .as_ref()
847 .map(|c| format!(": {c}"))
848 .unwrap_or_default()
849 )),
850 )
851 });
852 Detail {
853 tab: crate::text_formats::tab(crate::FileFormat::Candump),
854 lines,
855 list_title: "Messages",
856 list: crate::text_formats::capped_list(list, listing.messages.len()),
857 first: false,
858 ..Default::default()
859 }
860}
861
862pub enum Open {
864 Table {
865 lf: Box<LazyFrame>,
866 opened: Box<crate::members::Opened>,
867 },
868 Several(Vec<String>),
869}
870
871pub fn open(path: &Path, wanted: Option<&str>, layers: Layers) -> Result<Open> {
874 let (bytes, index) = indexed(path)?;
875 let layers = match crate::indexed::peek::<Listing>(path) {
878 Some(last) if layers.files().is_empty() || last.layers.files() == layers.files() => {
879 last.layers.clone()
880 }
881 _ => layers,
882 };
883 let listing = crate::indexed::cached::<Listing, std::convert::Infallible>(path, || {
884 Ok(Listing::resolve(&index, layers.clone()))
885 })
886 .unwrap_or_else(|never| match never {});
887 let listing = if listing.layers.files() == layers.files() {
888 listing
889 } else {
890 crate::indexed::forget::<Listing>(path);
892 crate::indexed::cached::<Listing, std::convert::Infallible>(path, || {
893 Ok(Listing::resolve(&index, layers))
894 })
895 .unwrap_or_else(|never| match never {})
896 };
897 let tables = listing.tables();
898 if let Some(wanted) = wanted
901 && listing.layers.dbcs.is_empty()
902 && !tables.iter().any(|t| t.name.eq_ignore_ascii_case(wanted))
903 {
904 return Err(FileError::new(
905 path,
906 format!(
907 "no table \"{wanted}\": with no dictionary only its {FRAMES} are read. --dict names one that decodes its messages."
908 ),
909 )
910 .into());
911 }
912 let picked = match crate::members::pick(tables.clone(), wanted, path, "")? {
913 crate::sqlite::Pick::One(table) => table.name,
914 crate::sqlite::Pick::Several(tables) => {
915 return Ok(Open::Several(tables.into_iter().map(|t| t.name).collect()));
916 }
917 };
918 let mut notes: Vec<String> = Vec::new();
919 if index.skipped > 0 {
920 notes.push(format!(
921 "{} non-frame lines skipped",
922 crate::numfmt::group_chrome(index.skipped)
923 ));
924 }
925 if index.past_limit > 0 {
926 notes.push(format!(
927 "{} frames left out: past the first {}",
928 crate::numfmt::group_chrome(index.past_limit),
929 crate::numfmt::group_chrome(crate::indexed::MAX_RECORDS)
930 ));
931 }
932 for dbc in &listing.layers.dbcs {
933 notes.extend(dbc.notes.iter().cloned());
934 }
935 let mut opened = crate::members::Opened {
936 detail: Some(Arc::new(detail(&index, &listing))),
937 other_tables: crate::members::others(&tables, &picked),
938 notes: notes
939 .into_iter()
940 .map(|n| crate::text_formats::note(n, "the log".to_string()))
941 .collect(),
942 ..Default::default()
943 };
944 let lf = if picked == FRAMES {
945 let raw = Arc::new(RawFrames::new(bytes, &index));
946 opened.window = Some((raw.clone(), raw.rows()));
947 crate::row_index::lazy(&raw)
948 } else if picked == SIGNALS {
949 long_table(&bytes, &index, &listing, &mut opened)?
950 } else {
951 let (message, rows) = listing
952 .messages
953 .get(&picked)
954 .ok_or_else(|| FileError::new(path, format!("no message \"{picked}\"")))?;
955 let decoded = Arc::new(
956 Decoded::new(bytes, &index, rows.clone(), message.clone(), true)
957 .map_err(|e| eyre!("{e}"))?,
958 );
959 opened.units = message
960 .signals
961 .iter()
962 .filter(|s| !s.unit.is_empty())
963 .map(|s| (s.name.clone(), s.unit.clone()))
964 .collect();
965 opened.window = Some((decoded.clone(), decoded.height()));
966 crate::row_index::lazy(&decoded)
967 };
968 Ok(Open::Table {
969 lf: Box::new(lf),
970 opened: Box::new(opened),
971 })
972}
973
974fn long_table(
976 bytes: &Arc<Bytes>,
977 index: &Index,
978 listing: &Listing,
979 opened: &mut crate::members::Opened,
980) -> Result<LazyFrame> {
981 let mut parts = Vec::new();
982 let mut left_out = 0usize;
983 for (name, (message, rows)) in &listing.messages {
984 let decoded = Arc::new(
985 Decoded::new(bytes.clone(), index, rows.clone(), message.clone(), false)
986 .map_err(|e| eyre!("{e}"))?,
987 );
988 let lf = crate::row_index::lazy(&decoded);
989 for signal in &message.signals {
990 if parts.len() >= MAX_LONG_PARTS {
991 left_out += 1;
992 continue;
993 }
994 parts.push(
995 lf.clone()
996 .select([
997 col("ts"),
998 lit(name.as_str()).alias("message"),
999 lit(signal.name.as_str()).alias("signal"),
1000 col(signal.name.as_str())
1001 .cast(DataType::Float64)
1002 .alias("value"),
1003 lit(signal.unit.as_str()).alias("unit"),
1004 ])
1005 .filter(col("value").is_not_null()),
1006 );
1007 }
1008 }
1009 if left_out > 0 {
1010 opened.notes.push(crate::text_formats::note(
1011 format!(
1012 "{} signals left out: past the first {MAX_LONG_PARTS} {} each message's table has them",
1013 crate::numfmt::group_chrome(left_out),
1014 crate::glyphs::get().middot
1015 ),
1016 "the dictionaries".to_string(),
1017 ));
1018 }
1019 if parts.is_empty() {
1020 return Err(eyre!(
1021 "no signals to decode: no frame of the log is a message of the dictionaries"
1022 ));
1023 }
1024 let all = concat(parts, UnionArgs::default())?;
1025 Ok(all.sort(
1026 ["ts"],
1027 SortMultipleOptions::default()
1028 .with_maintain_order(true)
1029 .with_nulls_last(true),
1030 ))
1031}
1032
1033fn scan(input: crate::readers::ScanIn<'_>) -> Result<crate::scan::Scan> {
1037 let file = input.path();
1038 let layers = Layers::new(input.formats, &input.options.dicts)?;
1039 Ok(match open(file, input.options.table.as_deref(), layers)? {
1040 Open::Table { lf, opened } => {
1041 input.report.opened = Some(Arc::new(*opened));
1042 (*lf).into()
1043 }
1044 Open::Several(tables) => crate::scan::Scan::Tables {
1045 file: file.to_path_buf(),
1046 tables,
1047 format: input.format,
1048 },
1049 })
1050}
1051
1052#[cfg(test)]
1053pub(crate) mod tests {
1054 use super::*;
1055
1056 #[test]
1059 fn errors_name_the_file() {
1060 use crate::readers::bad_input::{assert_shape, each_names_its_file, opening};
1061 each_names_its_file(
1062 crate::FileFormat::Candump,
1063 &[("text.log", b"hello there\n", "No line is a CAN frame")],
1064 );
1065 let dir = tempfile::tempdir().unwrap();
1066 let log = b"(1436509052.249713) can0 123#DEADBEEF\n";
1067 let dbc = dir.path().join("plain.toml");
1068 std::fs::write(&dbc, "a = 1\n").unwrap();
1069 for (options, named, says) in [
1070 (
1071 crate::OpenOptions {
1072 table: Some("Engine".into()),
1073 ..Default::default()
1074 },
1075 dir.path().join("a.log"),
1076 "--dict names one",
1077 ),
1078 (
1079 crate::OpenOptions {
1080 dicts: vec![dbc.clone()],
1081 ..Default::default()
1082 },
1083 dbc.clone(),
1084 "--dict takes",
1085 ),
1086 ] {
1087 let message = opening(
1088 dir.path(),
1089 "a.log",
1090 log,
1091 crate::FileFormat::Candump,
1092 &options,
1093 )
1094 .expect("refused");
1095 eprintln!("{message}");
1096 assert_shape(&message, &named);
1097 assert!(message.contains(says), "{message}");
1098 }
1099 }
1100
1101 pub(crate) const LOG: &str = "(1700000000.000100) can0 123#401F7602\n\
1102(1700000000.000200) can1 18FEF1FE#1234FFF000000000\n\
1103# a comment\n\
1104(1700000000.000300) can0 200#01102700\n\
1105(1700000000.000400) can0 200#02F6FF0000000000\n\
1106(1700000000.000500) can0 7DF#R\n\
1107(1700000000.000600) can0 123##3112233445566778899AABBCC\n\
1108(1700000000.000700) can0 456#DEAD\n";
1109
1110 #[test]
1111 fn each_line_form() {
1112 let f = parse_line("(1436509052.249713) vcan0 044#2A366C2BBA").unwrap();
1113 assert_eq!(f.ts, Some(1_436_509_052_249_713));
1114 assert_eq!((f.id, f.extended, f.dlc), (0x44, false, 5));
1115 let f = parse_line("(0.5) can0 12345678#R").unwrap();
1116 assert!(f.remote && f.extended);
1117 let f = parse_line("(1.0) can0 123##1DEADBEEF").unwrap();
1118 assert!(f.fd);
1119 assert_eq!(f.flags, Some(1));
1120 assert_eq!(f.data, [0xDE, 0xAD, 0xBE, 0xEF]);
1121 let f = parse_line(" can0 123 [4] DE AD BE EF").unwrap();
1122 assert_eq!((f.ts, f.dlc), (None, 4));
1123 let f = parse_line(" (1436509052.249713) can0 1F334455 [2] 01 02").unwrap();
1124 assert!(f.extended);
1125 let f = parse_line("(2024-01-31 08:15:00.123456) can0 123 [1] FF").unwrap();
1126 assert!(f.ts.is_some());
1127 let f = parse_line(" can0 321 [8] remote request").unwrap();
1128 assert!(f.remote);
1129 assert_eq!(parse_line("can0 123#ABC"), None);
1130 assert_eq!(parse_line("hello world"), None);
1131 assert!(looks_like(LOG.as_bytes()));
1132 assert!(!looks_like(b"a,b,c\n1,2,3\n"));
1133 }
1134
1135 #[test]
1136 fn frames_and_decoded_messages() {
1137 let index = index(LOG.as_bytes()).unwrap();
1138 assert_eq!(index.keys.len(), 7);
1139 assert_eq!(index.skipped, 1);
1140 assert!(index.absolute);
1141 let bytes = Arc::new(Bytes::Owned(LOG.as_bytes().to_vec()));
1142 let raw = Arc::new(RawFrames::new(bytes.clone(), &index));
1143 let df = crate::row_index::lazy(&raw).collect().unwrap();
1144 assert_eq!(df.height(), 7);
1145 assert_eq!(
1146 df.column("id").unwrap().str().unwrap().get(1),
1147 Some("18FEF1FE")
1148 );
1149 assert_eq!(
1150 df.column("kind").unwrap().str().unwrap().get(4),
1151 Some("remote")
1152 );
1153
1154 let dbc = crate::dbc::parse(crate::dbc::tests::SAMPLE, "car", None).unwrap();
1155 let layers = Layers {
1156 dbcs: vec![Arc::new(dbc)],
1157 };
1158 let listing = Listing::resolve(&index, layers);
1159 let names: Vec<&String> = listing.messages.keys().collect();
1160 assert_eq!(names, ["BODY", "ENGINE", "MUXED"]);
1161 assert_eq!(listing.unknown, 2);
1162 let (message, rows) = &listing.messages["ENGINE"];
1163 let decoded = Arc::new(
1164 Decoded::new(bytes.clone(), &index, rows.clone(), message.clone(), true).unwrap(),
1165 );
1166 let df = crate::row_index::lazy(&decoded).collect().unwrap();
1167 assert_eq!(df.height(), 2);
1169 assert_eq!(
1170 df.column("Speed").unwrap().f64().unwrap().get(0),
1171 Some(1000.0)
1172 );
1173 assert_eq!(df.column("Temp").unwrap().f64().unwrap().get(0), Some(78.0));
1174 assert_eq!(
1175 df.column("Gear").unwrap().str().unwrap().get(0),
1176 Some("Second")
1177 );
1178 let (message, rows) = &listing.messages["MUXED"];
1179 let decoded = Arc::new(
1180 Decoded::new(bytes.clone(), &index, rows.clone(), message.clone(), true).unwrap(),
1181 );
1182 let df = crate::row_index::lazy(&decoded).collect().unwrap();
1183 let volts = df.column("Volts").unwrap().f64().unwrap();
1184 let amps = df.column("Amps").unwrap().f64().unwrap();
1185 assert_eq!(volts.get(0), Some(100.0));
1186 assert_eq!(amps.get(0), None);
1187 assert_eq!(volts.get(1), None);
1188 assert!((amps.get(1).unwrap() - -1.0).abs() < 1e-9);
1189 }
1190}