Skip to content

canair investigate

usage: canair investigate [-h] <kind> ...

[UDS+CAN] Point this at an unknown signal and get one ranked table telling you
everything worth knowing about each of its bytes. Choose a domain kind:
  uds   a diagnostic PID (default) — per byte: mapped? / state F /
        best co-polled anchor / unit guess (domain A). A bare
        `canair investigate MCU 2102` is shorthand for this.
  can   an arbitration ID in a raw broadcast-CAN frame log (domain B) —
        per byte: best cross-ID anchor + linear fit + unit guess.

Read-only: analyses captures/ only, never talks to the device.

positional arguments:
  <kind>
    uds       Explain a diagnostic PID (domain A)
    can       Explain an arbitration ID in a raw broadcast-CAN frame log
              (domain B)

options:
  -h, --help  show this help message and exit

canair investigate uds

usage: canair investigate uds [-h] [--min-r R] [--min-n N]
                              [--join-tol SECONDS] [--fill {auto,hold,none}]
                              [--max-hold SECONDS] [--all] [--bits] [--events]
                              [--dwell] [--field NAME] [--counters]
                              [--min-bits BITS] [--counter-width N]
                              [--unmapped-only]
                              [--independent-of ECU:PID:PARAM]
                              [--independent-of-file FILE] [--json] [--top N]
                              [--notation NAME] [--since WHEN] [--until WHEN]
                              [--date YYYY-MM-DD] [--today]
                              [--last-sessions [N]] [--last-session]
                              [--state STATE] [--label SUBSTR]
                              [ecu] [pid]

Point this at an unknown PID and get one ranked table telling you
everything worth knowing about each of its bytes — the fastest way to
start decoding.

For every varying data byte of ECU PID it reports, in one pass:
  - mapped?   whether a defined parameter already decodes this byte
              (a verified param hides the byte by default; an
              unverified [param?] mapping is shown as still-open work)
  - stateF    how cleanly the byte separates across power states
              (sleep/acc/ready/charging) — high F = a mode/relay/thermal
              signal a driving correlation would miss
  - anchor    the strongest-correlating known signal on another
              co-polled ECU/PID (Pearson r + linear fit y=m·x+c)
  - unit      a physical-unit guess for that fit (e.g. raw-40 degC,
              x1.609 mph->km/h)

Bytes are ranked strongest-anchor-first, then by state separation, so
the most decodable bytes float to the top. This bundles the manual
coverage -> discriminate -> correlate -> hunt loop into a single call.

ECU and PID are optional: give both for the full per-byte deep-dive,
an ECU (or a QUERY) alone to sweep its PIDs, or nothing to sweep the
whole profile. A sweep prints a ranked SUMMARY per PID (--top caps it)
rather than N full reports.

--counters switches to a different question — 'which bytes here only
ever go UP?' — sweeping multi-byte windows for odometers, operating-hour
tallies, power-cycle counts and uptime timers. Those are invisible to
the default view (a slow counter looks constant within one session),
so they need the whole capture history rather than a scoped window.

Read-only: analyses captures/ only, never talks to the device. Once a
byte looks promising, confirm the exact expression with `canair hunt
ECU PID --against ...` and write it with `canair pids upsert-param`.

positional arguments:
  ecu                   Target ECU (e.g. MCU), or a QUERY (e.g.
                        "BMS:2101,2102"). Omit ECU and PID to sweep the whole
                        profile; give an ECU alone to sweep its PIDs.
  pid                   Target PID (e.g. 2102). Omit to sweep the ECU.

options:
  -h, --help            show this help message and exit
  --min-r R             Only report an anchor when |r| ≥ this (default 0.6)
  --min-n N             Min aligned points (default 15)
  --all                 Include bytes a verified param already maps (default:
                        hide only verified-mapped)
  --bits                Also analyse individual toggling bits (Bn:k) — the
                        body/status-ECU finder
  --events              Report each bit/byte rising/falling edge with its
                        timestamp, aligned to the nearest capture note (the
                        narrated event timeline)
  --dwell               Summarise each event bit/byte by how long it stays ON
                        — median on-duration + a momentary|sustained class.
                        Separates a briefly-pulsed bit (a door flicked open)
                        from one held for minutes (a hood left up), so
                        body/event signals are identifiable without capture-
                        note narration. Needs --keep-all/--keep-changes data
                        (keep:unique drops falling edges)
  --field NAME          With --events: track ONE defined param (a typed
                        enum/bitmask/struct date field) as a single logical
                        signal — emit one transition per change of its DECODED
                        value (e.g. {Mon 08:00}->{Tue 07:30}), not scattered
                        per-byte edges. NAME is a parameter of the target
                        ECU:PID.
  --counters            Hunt MONOTONIC COUNTERS instead: sweep every 1-4-byte
                        window x endianness for a value that only ever rises
                        across the capture corpus (odometer, operating-hours,
                        ignition/power-cycle count) or ramps and resets per
                        session (uptime). Ranks by bits of monotonic evidence
  --min-bits BITS       With --counters: minimum bits of monotonic evidence
                        (default 4). Each clean up-step with no down-step is 1
                        bit, so 8 bits ≈ 8 rises and no falls (1-in-256 by
                        chance). Lower it to surface sparse long-horizon
                        counters read only a handful of times
  --counter-width N     With --counters: widest byte window to test (default
                        4)
  --unmapped-only       With --counters: hide windows a VERIFIED parameter
                        already decodes. A window mapped only by an unverified
                        guess is kept (tagged [NAME?]) — monotonicity is often
                        the evidence that refutes such a guess
  --independent-of ECU:PID:PARAM
                        Rank bytes that separate by state yet DON'T track this
                        driver signal — the 'active-but-independent' finder
                        (e.g. AC voltage: varies while charging but is
                        uncorrelated with charge current). Adds a driver-r
                        column and re-ranks by state separation weighted by
                        independence from the driver
  --independent-of-file FILE
                        Like --independent-of, but the driver is an external
                        timestamp,value CSV (mutually exclusive with
                        --independent-of)
  --json                Machine-readable output
  --top N               In a corpus/ECU sweep (ECU or PID omitted): cap the
                        ranked summary to the top N rows (default 40; 0 = no
                        cap). Ignored for a single PID.
  --notation NAME       byte-index notation for output labels: wican
                        (default), isotp, torque, bix. Overrides the
                        display.byte_notation config key.

time joining:
  --join-tol SECONDS    Nearest-timestamp join window (default 5s)
  --fill {auto,hold,none}
                        Carry a run-length (keep:changes) value forward to
                        reference instants it has no sample at: 'auto'
                        (default) fills only keep:changes sessions, 'hold'
                        forces it everywhere, 'none' keeps strict point
                        semantics
  --max-hold SECONDS    Cap how long a filled value may be carried (default:
                        until the next sample or the end of its recording
                        session)

scoping:
  Restrict to captures within a date/time range (inclusive) and/or by session state (token-matched) or label substring. --since/--until accept a date (YYYY-MM-DD) or a timestamp (YYYY-MM-DD HH:MM[:SS[.ffffff]])

  --since WHEN          Only captures on or after this date/time (YYYY-MM-DD[
                        HH:MM:SS])
  --until WHEN          Only captures on or before this date/time (YYYY-MM-DD[
                        HH:MM:SS])
  --date YYYY-MM-DD     Only captures on this exact date (shorthand for
                        --since X --until X)
  --today               Only captures recorded today (shorthand for --date
                        <today>)
  --last-sessions [N]   Only the most recent N recorded sessions in scope (N
                        defaults to 1)
  --last-session        Only the most recent recorded session in scope (alias
                        for --last-sessions 1)
  --state STATE         Only captures recorded in STATE, matched by token and
                        widened by the profile's implies: hierarchy (--state
                        ready also matches DRIVING). Comma-separate
                        alternatives (--state ready,driving); repeat the flag
                        to require several at once (--state charging --state
                        parked)
  --label SUBSTR        Only captures whose session/capture label contains
                        SUBSTR (case-insensitive)

examples:
  canair investigate MCU 2102              # rank unmapped + unverified-mapped bytes of MCU 2102
  canair investigate MCU 2102 --all        # include bytes a verified param already maps
  canair investigate BMS                   # sweep every captured BMS PID (ranked summary)
  canair investigate                       # sweep the whole profile (ranked summary)
  canair investigate --counters            # find every monotonic counter in the car
  canair investigate BMS --counters        # every counter across BMS's PIDs
  canair investigate IGPM 22BC03 --bits    # rank toggling bits (body/status-ECU work)
  canair investigate IGPM 22BC03 --events  # bit/byte edges aligned to the event timeline
  canair investigate CLU 22B002 --counters # hunt monotonic counters (odometer / cycle count)
  canair investigate BMS 2101 --counters --unmapped-only   # only counters not settled yet
  canair investigate BMS 2101 --state driving   # only consider drive captures
  canair investigate ESC 22C101 --min-r 0.8      # only show strong anchors (|r| >= 0.8)
  canair investigate AAF 2181 --json       # machine-readable output

  # active-but-independent: rank bytes that separate by state yet DON'T track a
  # named driver — the fingerprint of AC voltage vs charge current
  canair investigate OBC 2101 --independent-of OBC:2101:OBC_DC_A --state charging

tip: no anchors found? widen scope (drop --state), lower --min-r, or grow the
     capture set — an anchor needs another co-polled signal it can align to. For
     a body/comfort PID with no co-polled partner, use --bits / --events (the
     signals are toggling status bits, ranked by state separation + edge time).

canair investigate can

usage: canair investigate can [-h] --id ID
                              [--can-format {auto,asc,blf,csv,log,gvret}]
                              [--min-r R] [--min-n N] [--join-tol SECONDS]
                              [--fill {auto,hold,none}] [--max-hold SECONDS]
                              [--bits] [--json]
                              FILE

Explain one arbitration ID in a raw broadcast-CAN frame log: for every
varying data byte, report its strongest cross-ID anchor (Pearson r +
linear fit y=m·x+c) and a physical-unit guess, ranked strongest first.

The domain-B analogue of `investigate uds`: frames have no defined-param
mapping (signals live in signals/, decoded via Stage-4 tooling) and no
power-state metadata, so the report is anchor-centric. Bytes are
labelled 0xID:rN (raw-CAN space, no PCI). Read-only.

To pin the exact byte against a known reference use `canair hunt can`;
to see every relationship at once use `canair correlate can`.

positional arguments:
  FILE                  Path to a raw broadcast-CAN frame log
                        (.asc/.blf/candump .log/.trc/GVRET .csv)

options:
  -h, --help            show this help message and exit
  --id ID               Arbitration ID to explain (e.g. 0x386)
  --can-format {auto,asc,blf,csv,log,gvret}
                        Log format (default: auto-detect by extension)
  --min-r R             Only report an anchor when |r| ≥ this (default 0.6)
  --min-n N             Min aligned points (default 15)
  --bits                Also analyse individual toggling bits (rN:k)
  --json                Machine-readable output

time joining:
  --join-tol SECONDS    Nearest-timestamp join window (default 5s)
  --fill {auto,hold,none}
                        Carry a run-length (keep:changes) value forward to
                        reference instants it has no sample at: 'auto'
                        (default) fills only keep:changes sessions, 'hold'
                        forces it everywhere, 'none' keeps strict point
                        semantics
  --max-hold SECONDS    Cap how long a filled value may be carried (default:
                        until the next sample or the end of its recording
                        session)

examples:
  canair investigate can drive.blf --id 0x386        # rank each byte of 0x386 by best cross-ID anchor
  canair investigate can drive.csv --id 0x220 --bits # include toggling bits
  canair investigate can drive.asc --id 0x386 --json # machine-readable