Skip to main content

datui_lib/
candump.rs

1//! CAN logs written by `candump`, decoded with DBC files.
2//!
3//! Each line is one frame: `(1436509052.249713) can0 123#DEADBEEF` as `candump -l` and
4//! `-L` write it (`##` for CAN FD, `#R` for a remote request), or the default
5//! `can0  123   [4]  DE AD BE EF`, with or without a `(timestamp)` in front. One pass
6//! records where each frame's line starts and its id and interface; the raw table is
7//! then read line by line where it is shown, from a map of the file.
8//!
9//! With a DBC file ([`crate::dbc`]) that names the log's messages, each message is a
10//! table of its own: `ts` and a column per signal, with units, value names as text,
11//! and a multiplexed signal null in the frames its multiplexer does not select. The
12//! home screen lists them, with the raw `frames` and a long `signals` table of every
13//! decoded value (`ts`, `message`, `signal`, `value`, `unit`).
14
15use 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
32/// What datui does with a candump log: see [`crate::readers`].
33pub(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
49/// The longest line read as a frame; a longer one is not one.
50const MAX_LINE: usize = 4096;
51/// Interfaces told apart; past this many, the rest share the last.
52const MAX_INTERFACES: usize = 255;
53/// Long-table parts: message signals unpivoted.
54const MAX_LONG_PARTS: usize = 10_000;
55
56/// The raw table, and the long table of decoded values.
57pub const FRAMES: &str = "frames";
58pub const SIGNALS: &str = "signals";
59
60/// One frame, as a line gives it.
61#[derive(Debug, Clone, PartialEq)]
62pub struct Frame<'a> {
63    /// Microseconds, from the epoch or from the start of the capture.
64    pub ts: Option<i64>,
65    pub iface: &'a str,
66    pub id: u32,
67    pub extended: bool,
68    pub fd: bool,
69    /// CAN FD flags (BRS, ESI).
70    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
87/// An id as written: three hex digits standard, eight extended.
88fn 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
99/// A timestamp in parentheses: seconds with a fraction, or a date and time.
100fn 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    // `candump -ta` writes a local date and time: `2024-01-31 08:15:00.123456`.
113    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
117/// Parse one line, as `candump -l` writes it or as `candump` prints it.
118pub 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    // The printed form: an id, then `[n]` and the bytes, maybe after `-x` fields.
137    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        // CAN FD: a flags digit, then the data.
208        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    // `_X` after the data: a length code past 8 for eight bytes of data.
230    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
243/// Whether `head` begins a candump log: its first line that is not blank is a frame.
244pub 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    // A head cut mid-line is judged on the frame so far: a byte cut in half is
251    // left off.
252    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/// What one pass over a candump log found.
262#[derive(Debug, Default)]
263pub struct Index {
264    /// Where each frame's line starts.
265    pub offsets: Arc<Offsets>,
266    /// Each frame's id, with bit 31 for an extended one.
267    pub keys: Vec<u32>,
268    /// Each frame's interface, as an index into `interfaces`.
269    pub ifaces: Vec<u8>,
270    pub interfaces: Vec<String>,
271    /// Whether the timestamps are from the epoch (`-l`), not from the capture's start.
272    pub absolute: bool,
273    /// Lines that are not frames, passed over.
274    pub skipped: usize,
275    pub past_limit: usize,
276}
277
278/// Index the candump log in `data`: one pass, start to end.
279pub 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    // Seconds since 2001 and later are wall-clock time; less is time since the start.
327    index.absolute = first_ts.is_some_and(|ts| ts >= 978_307_200_000_000);
328    Ok(index)
329}
330
331/// The line of frame `row`.
332fn 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
354/// The raw table: a row per frame, read from its line where it is shown.
355pub 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
500/// One message's frames, decoded by its DBC entry.
501pub struct Decoded {
502    bytes: Arc<Bytes>,
503    offsets: Arc<Offsets>,
504    /// The raw rows of this message's frames.
505    rows: Arc<Vec<u32>>,
506    message: Arc<Message>,
507    absolute: bool,
508    /// Value names as text, or every value as a number (for the long table).
509    named: bool,
510    schema: SchemaRef,
511}
512
513/// How a signal's values are typed: an integer when factor and offset keep it one, a
514/// float otherwise, its names as text when it has them.
515fn 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// --- DBC layers ----------------------------------------------------------------------
658
659/// The DBC files a log is read with, in order: the search path's, then each `--dict`.
660#[derive(Debug, Clone, Default)]
661pub struct Layers {
662    pub dbcs: Vec<Arc<Dbc>>,
663}
664
665impl Layers {
666    /// The search path's DBC files and each `--dict`.
667    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                    // A TOML dictionary's error may be in the file it names.
681                    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/// What a log's frames resolve to under its layers: each message with frames, the
695/// rows of its frames.
696#[derive(Debug, Default)]
697pub struct Listing {
698    pub layers: Layers,
699    /// Message tables by name: the message and its frames' raw rows.
700    pub messages: BTreeMap<String, (Arc<Message>, Arc<Vec<u32>>)>,
701    /// Frames no DBC names.
702    pub unknown: usize,
703}
704
705impl Listing {
706    pub fn resolve(index: &Index, layers: Layers) -> Self {
707        // For each interface and id, the last layer that applies and names it.
708        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
788/// The index of the candump log at `path`, made by one pass or kept from one.
789pub 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
796/// The tables the log at `path` was last listed with, for the home screen.
797pub fn listed(path: &Path) -> Option<Vec<Table>> {
798    crate::indexed::peek::<Listing>(path).map(|l| l.tables())
799}
800
801/// What the Info panel's CAN tab says.
802fn 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
862/// What opening a candump log finds.
863pub enum Open {
864    Table {
865        lf: Box<LazyFrame>,
866        opened: Box<crate::members::Opened>,
867    },
868    Several(Vec<String>),
869}
870
871/// Open the candump log at `path` with `layers`: the table `wanted` names, its frames
872/// when no DBC names its messages, or the list.
873pub fn open(path: &Path, wanted: Option<&str>, layers: Layers) -> Result<Open> {
874    let (bytes, index) = indexed(path)?;
875    // A message opened from the home screen's list is read with the DBC files the list
876    // was made with, `--dict` among them.
877    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        // The DBC files changed: resolve again and keep that.
891        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    // Without a DBC file only the frames are read: a message or the signals asked for
899    // by name wants one.
900    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
974/// Every decoded value, one row each: each message's signals unpivoted, in time order.
975fn 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
1033/// The scan of a candump log: its frames, or with DBC files that name its messages,
1034/// the table `--table` names or the list of them. The pass that indexes the log is
1035/// kept, as a flight log's is.
1036fn 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    /// A log of no frames names itself; a table that wants a DBC file says the flag
1057    /// that gives one, and a DBC file that is not one names the DBC file.
1058    #[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        // The classic frame and the FD one.
1168        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}