| name | reverse-engineer-signal |
| description | Generic, vehicle-agnostic reverse-engineering workflow for canair — the whole flow from orient/discover through capture, analyze, define, verify, integrate, for ANY signal (a PID/DID parameter, a raw broadcast frame field, a routine, an IOControl actuator) on ANY car. Covers WiCAN Bnn / ISO-TP / PCI byte indexing, expression syntax, the analysis reasoning (signal types, physics/EE, statistics), and writing/validating definitions. Use when discovering, decoding, or verifying anything on a vehicle bus, writing or fixing an expression, working out a byte offset, or working a research: backlog — on the bundled Ioniq profile or a profile you built for another car. Examples use the Ioniq for concreteness; the method is generic. |
Reverse-engineering a vehicle signal (generic)
This is the vehicle-agnostic, end-to-end workflow for taking a signal from
"unknown" to a verified, decoded parameter in a profile's ecus/. It is not
Ioniq-specific and not PID-specific: the same orient → discover → capture →
inspect → hypothesize → define → validate → verify → integrate loop applies to
any car and any kind of signal — a service-22/21 PID/DID, a passively-sniffed
broadcast frame field, a routine, or an IOControl actuator. The concrete
examples below use the bundled 2017 Ioniq profile because it's a fully-worked
reference, but treat the method as the transferable part, not the byte offsets.
For vehicle-specific facts (which ECU carries which signal, marque quirks,
the exact addresses) consult the active profile's ecus/ and, for the bundled
car, the ioniq-reverse-engineering skill. This skill is the procedure; the
profile is the data.
Related skills: ioniq-reverse-engineering carries the bundled-car facts
(ECU status table, device/transport details, the canair/wican-cli command
reference) — load it too when working the Ioniq specifically. When working
another car, this skill plus that car's profile is what you need; the Ioniq
skill is then just an illustrative example of a finished profile.
Safety first (non-negotiable)
- NEVER use UDS programming session (
10 02) or any firmware write/upload.
A car's ECUs can be bricked — treat every bus as a real, in-use vehicle.
- Be gentle: ECUs vary in speed and some are finicky, especially the first
request after idle. One
canair connection at a time, any transport —
canair enforces a flock mutex (/tmp/wican-connection.lock); a second
slcan-tcp client hangs unserved and a second wican-ws WebSocket can lock up
the device (power-cycle to recover). No concurrent requests to the same ECU.
- Never reboot the device without asking. Using the WebSocket terminal
overrides AutoPID; ask before rebooting to restore the AutoPID/MQTT feed.
- Treat
0x22Fxxx (flash/cal) as read-only. 2E writes and 2F IOControl can
brick or actuate hardware — out of scope for signal decoding.
- Disable device sleep during a session:
wican sleep --disable (re-enable
after).
Working principles
- Put yourself in the shoes of the ECU's automotive systems engineer. Before
guessing at bytes, ask: if I designed this module, what would it need to
measure, report, and control? A BMS engineer thinks in cell voltages, pack
current, temperatures, SOC, contactor and relay states, isolation resistance;
an ESC engineer thinks in wheel speeds, yaw rate, lateral/longitudinal accel,
brake pressure. Signals cluster by the ECU's job, come in physically sensible
units and ranges, are laid out in orderly blocks (e.g. four wheel speeds in a
row), and are scaled to fit their field width. Let that domain model generate
your hypotheses and sanity-check your results — a decode that no real systems
engineer would design is probably wrong.
- Be rigorous. Reverse engineering is evidence, not vibes. Don't accept a
byte interpretation because it "looks about right" — confirm it with data
(range, distribution, correlation, physical plausibility across states). State
your confidence honestly: a hypothesis is a hypothesis until it's validated.
Prefer "unverified until proven" over an optimistic guess promoted to fact.
- NEVER mark a parameter
verified: true without proof. verified is a
factual claim that the decode was checked against real data from this vehicle
— a capture whose values match known physical state, a scan-tool cross-check, or
a definitive constant. It is not a statement that the expression looks
plausible, was copied from another car's sheet, or "should" be right. With no
capture, there is no proof, so it stays --unverified — no exceptions, even
when porting a structure that's verified on a different profile (a sibling
car's byte offset is a hypothesis here, not a verification). Marking something
verified on a guess is a correctness bug that poisons every downstream user and
every correlation that trusts it. When in doubt, leave it unverified and record
what proof is still missing in notes:.
- Write notes to the point. Notes on ECUs/PIDs/research are technical
records, not prose. State the facts — byte offset, observed range, per-state
values, correlation results, the decision and why — and stop. Cut filler,
hedging, and narration. Hold off on speculation: record what the data shows
and, at most, a one-line best interpretation; leave extended theorizing to the
reader. These files are read repeatedly and grow forever — every excess word is
a tax on everyone after you. Terse and factual beats thorough and unwieldy.
The lifecycle
orient → prerequisites → discover → capture → inspect → hypothesize
→ define → decode/validate → verify → integrate
Progress is tracked per-ECU in the research: block of the profile's ecus/<ecu>.yaml
(schema in canlib/schema/pids_schema.yaml), graduating:
pending → captured → (decoded) → verify → done, at which point a real
parameters: entry exists and is marked verified: true.
1. Orient — pick a target
canair research --summary
canair research --priority P1
canair research --ecu MCU
canair coverage --no-capture
canair coverage --unmapped
canair research surfaces planned work; canair coverage surfaces undecoded
bytes in PIDs you already capture. canair ecu (and, for the bundled car, the
ioniq-reverse-engineering skill's ECU status table) shows which ECUs carry
which kind of signal and are worth probing.
2. Prerequisites — power state & access
Decide the car power state the PID needs (vehicle_states:
sleep, plugged, acc, acc2, ready, charging — the same field on PIDs/ECUs and
in research: entries) and whether the ECU needs waking /
an extended session. IGPM (0x770) and BCM (0x7A0) wake from CAN activity;
powertrain ECUs (BMS/VCU/MCU) generally need ACC/ignition or charging.
canair discover
3. Discover — which DIDs respond
canair scan MCU --service 21 --range 01-FF --save
canair scan IGPM --service 22 --range BC00-BCFF --session --wake --save
Record a research lead as you go:
canair pids add-research MCU --type decode --target 2102 \
--status captured --priority P1 --prereq charging --notes "62 bytes, undecoded"
Always record the scan outcome — a discovered DID must never be lost:
- New responding DID → register it immediately as a
status: draft placeholder
PID in ecus/<ecu>.yaml, with the raw payload pasted into notes (and an empty
expression), then add a decode research lead. This is the established project
convention (e.g. ESC 22C102, EPS 220101/220102, CLU 22B001/B003 are all such
placeholders). status: draft means it is tracked, queryable and captured but stays
out of the generated WiCAN profile until it's actually decoded (set canair pids set-pid-status <ECU> <PID> active to ship it, or ignored for a dead DID). Before
adding, always check whether the DID is already registered — a re-scan of a known DID
should just refresh the payload/notes, not create a duplicate.
- Negative probe (NRC / no response) → close the scan lead with
canair pids set-status <ECU> "<target>" nrc --type scan so nobody re-probes it.
Use nrc for "probed, ECU said no / silent" and done for "scan complete, responders
found and registered". (Example: on the Ioniq the powertrain ECUs — BMS/VCU/MCU/LDC —
are KWP2000/service-21 and reliably NRC every 22 xxxx DID ported from the Ioniq 5;
confirm once, mark nrc, move on. Watch for the analogous mismatch on your own car.)
4. Capture — record real payloads across states
canair query "query MCU:2102" --save --label "MCU 2102 driving" \
--state "ready, driving" --notes "hard launches + regen"
canair query "query MCU 2102" --monitor 1 --keep-all --save
Capture the SAME PID in DIFFERENT states (park vs drive, cold vs warm, charging
vs ready) — contrast is what lets you separate signal bytes from constants. For
cross-signal analysis (step 7), co-poll the target PID together with an ECU
carrying a known reference (speed on ESC, RPM on MCU) in one canair query /
--monitor run — they'll share a drive so hunt/correlate/cross-ECU --corr
can time-align them. Every payload capture is now timestamped automatically, so
any co-polled drive is joinable; only one-shot scans/identity reads stay untimed.
Never hand-edit captures/ — and never read the raw captures/*.yaml files
directly. Always inspect captures through canair captures/canair decode
(next step): reading the YAML by hand gives you undecoded raw payloads and skips
byte-diffing, decoding, and state/date scoping. Saves are journaled to captures/.journal/ and
reconciled on exit (a killed/disconnected --monitor session is recoverable with
canair captures --recover); in --monitor the state is auto-suggested from
decoded values (press s to edit metadata live). After saving, run
canair captures --summary.
5. Inspect — see the bytes
canair captures --sessions
canair captures --sessions --state driving
canair captures --sessions --json
canair captures --summary
canair captures --latest MCU
canair captures MCU 2102
canair captures MCU:2102 --diff
canair captures MCU:2102 --diff --all
canair captures MCU:2102 --diff --rulers
canair captures MCU:2102 --diff --since 2026-07-19
canair captures MCU:2102 --diff --state driving
canair captures MCU:2102 --step
canair captures --recover
canair bix -1 --annotate 6101FFFF...
The QUERY mini-language is shared with canair decode: MCU 2102 (one PID),
MCU:2102,2103 (several PIDs), MCU (all PIDs for an ECU), "VCU:2101 BMS:2101"
(cross-ECU — quote the space), and BCM:22 (substring PID match — all 22xxxx
DIDs on an ECU).
Start with canair captures --sessions to see what data exists (labels, states,
notes per session — no payloads) and pick a drive/state to analyze; --json
gives a machine-readable index.
Byte-diff highlights which bytes moved between states — your candidate signal
bytes; add --rulers to overlay the byte-index ruler and --all to include
duplicate payloads. Both captures and decode share the same scoping flags —
--since/--until/--date, --state SUBSTR/--label SUBSTR, --first/
--last N — so you can isolate a single drive (--state driving) before
diffing/decoding. canair bix --annotate tells you each byte's WiCAN index and
flags the PCI bytes you must not read across (see Reference below); add
--ecu ECU --pid PID to overlay which defined parameter (and bit) maps each byte
and flag unmapped data bytes — the fastest way to catch a wrong byte offset in
an expression.
6. Hypothesize — form an expression
Cross-reference any external signal maps you have for this car or a close
relative (for the Ioniq: the Kia Soul/Niro PID sheets in
profiles/ioniq-2017/references/ and the Obsidian vault); watch the PCI-boundary
caution for [Bnn:Bmm]. See the Byte Index & Expression reference at the bottom.
Hypothesizing is not just guessing a byte offset — it's reasoning from domain
knowledge about what a signal must physically be, then confirming it in the
data. Use every discipline you have.
Let the ECU narrow the search space
What an ECU is tells you what signals to expect. Reason about the
component's job before you look at bytes (the examples below are EV modules; the
same "reason from the ECU's role" applies to any powertrain):
- BMS (battery) — cell/pack voltages (tight clusters of similar 2-byte
values), currents (signed, symmetric about zero, charge vs discharge),
temperatures (slow, per-module), state-of-charge/health (bounded 0–100%),
contactor/relay states (enum/bit), cell-balancing flags (bitfields).
- MCU/inverter — motor RPM (signed, ±, symmetric under regen), torque
(signed), phase currents (large, load-tracking), DC-link voltage, and several
temperatures of different components (see thermal-mass reasoning below).
- VCU — gear/drive-mode (enum), vehicle speed, pedal positions (0–100%),
ready/charging state machine (enum/bits).
- OBC/LDC — AC/DC input & output voltages/currents, charger/converter
temperatures, charge-state enums.
- BCM/IGPM (body) — mostly discrete signals: lights, locks, doors,
switches → bitfields and enums, not continuous analog. Decode these with
the bit-level, event-driven workflow: capture a narrated event sequence
(
--monitor --keep-unique, noting each physical action), then
canair investigate <ECU> <PID> --events --bits to get the rising/falling-edge
timeline aligned to your notes, and canair correlate --find-mirrors --bits to
find the same bit exposed on a second ECU (e.g. an IGPM door bit mirrored in
BCM). This is how DOOR_DRV_OPEN / HOOD_OPEN / the BC05 unlock+trunk bits were
decoded (2026-07-24).
- HVAC/AAF — temperatures (ambient/evaporator/heatsink), fan/compressor
states, flap positions.
A byte's plausible identity is constrained by its ECU: a load-tracking current
on the BCM is unlikely; a door-ajar bit on the MCU is unlikely.
Reason from physics / electrical engineering / power electronics
The dynamics of a signal reveal its nature even before you know its scale:
- Thermal mass (the most useful physics lever). Temperatures change slowly
and are decoupled from instantaneous load: high lag-1 autocorrelation, tiny
per-sample step. Crucially, thermal mass varies by component, so how
slowly a temperature moves tells you which component it measures:
- A small-die IGBT junction temperature can rise/fall within ~1 s (low
thermal mass) yet still sit near coolant temp at idle — it looks fast but
keeps a temperature-like baseline.
- A heatsink / coolant / motor-winding temperature drifts over minutes
(large thermal mass) and lags load heavily.
- Which state a temperature is hottest in disambiguates the component: a byte
hottest while charging (motor idle) is inverter/charger/power-stage; one
that warms only with driving is motor/coolant. Use
--stats --group-by state to read these per-state means straight out.
- Signed vs unsigned & symmetry. Motor RPM, torque, and battery current are
signed and roughly symmetric about zero (regen ≈ –drive). A value that
never goes negative and tracks |load| is a magnitude (current RMS, power),
not a signed torque.
- Conservation / relationships you can check. Power ≈ V·I; DC-link current ≈
motor power / DC-link voltage; pack current integrates toward SOC change. A
candidate that violates a physical identity is wrong even if its range "looks
right." Validate these with
--corr against an already-known signal.
- Rate limits. Real physical quantities can't step arbitrarily fast — a
"temperature" that jumps 5 °C between two 5 s samples is a load/current metric,
not a temperature (a real example from this project).
Reason from computer science (state machines, counters, logic)
Not every byte is analog. Discrete/logic signals have distinct fingerprints:
- Enums / state machines — a small set of distinct integer values (
--stats
shows low distinct), transitions only between "adjacent" states (park→ready→
driving), and correlation with a known mode. Map with per-bit or whole-byte
reads and label each value.
- Bitfields — individual bits toggle independently with discrete events
(a light, a door, a relay). Read bit-by-bit (
Bnn:k); canair coverage --bitfields flags bytes only partly decoded.
- Counters / alive / checksum — monotonic wrap-around, or high-distinct noise
with no physical correlation to anything (a rolling counter or CRC). Don't
try to give these a physical unit; mark them as such.
- Constants / calibration — never (or rarely) change across all states →
cal/identity block, not live data. Confirm with
--stats (distinct = 1–2).
Reason from statistics & mathematics
The tooling exposes real statistical levers — use them as evidence, not decoration:
- Distribution shape (
--stats: n / distinct / mean / median / stdev) —
continuous vs enum vs constant; a bimodal/low-distinct byte is likely discrete.
- Correlation (
--corr PARAM, Pearson r) — the single strongest validation:
test a candidate against a known signal it should relate to (a temp vs
|torque| ≈ 0; a current vs |torque| ≈ high). Correlate against a derived
reference too (e.g. |Δbyte| vs |Δload|) to separate fast load-trackers from
slow temps.
- Cross-ECU correlation — a signal on one ECU often mirrors a verified
signal on another (speed appears on ESC/EPS/VCU/AAF; RPM on MCU/VCU). Three
ways to exploit it, all time-aligned by nearest timestamp over a co-polled
drive:
canair decode <ECU> <PID> --corr ESC:22C101:REAL_SPEED_KMH — correlate a
PID's params against a cross-ECU reference (ECU:PID:PARAM or
ECU:PID:EXPR). Add --corr-transform delta to test level-vs-rate, or
--method spearman for monotone-but-nonlinear/quantized links Pearson misses.
canair hunt <ECU> <PID> --against ESC:22C101:REAL_SPEED_KMH — "which byte
is this known signal?": sweeps every byte×interpretation, ranks by |r|,
and prints the linear fit + a unit guess (×1.609 mph→km/h, raw−40 °C).
Collapses to the best interpretation per offset (--all-interps to expand).
--transform delta hunts the byte tracking the reference's rate (torque vs
acceleration); --promote NAME writes the winner into ecus/ as an
enabled-unverified candidate. Fastest path from "unknown byte" to "candidate".
canair correlate --state driving — rank every strong cross-signal
relationship in a drive at once (--against REF to focus one, --bytes/
--bits to include raw bytes/toggling bits, --promote NAME on the top
raw-byte hit). --lag-scan N reports the apparent lead/lag (command→response
ordering; "apparent" because sequential polling adds a fixed offset);
--gate '> 0' restricts to a regime (while-moving); near-perfect co-linear
groups collapse to one line. Run canair correlate --overlap first to see
which ECU:PID pairs actually share time-aligned samples (pick a viable
--against reference instead of guessing).
canair investigate <ECU> <PID> — the one-shot "explain this PID": per byte,
is it mapped?, its state-discriminability F, and its strongest co-polled
anchor (r + fit + unit) in a single ranked table. Point it at an unknown PID
before hand-running the loop below. Add --bits to rank toggling bits
(Bn:k) — the body/comfort finder (a body PID with no anchor is still ranked
by state separation, not dropped). Add --events for the bit/byte edge
timeline — every rising/falling transition with its timestamp, aligned to
the nearest capture note; the fastest way to decode a narrated event capture
(door/lock/hood). Both warn on keep:unique scope (rising-edge only).
- Cross-ECU mirror detection (
canair correlate --find-mirrors [--bits]) —
reports byte/bit positions time-aligned equal across co-polled ECU/PIDs (a
door bit in IGPM also present in BCM); the cross-ECU companion to
decode --find-mirrors (single-PID). Beware small --min-n: noisy low bits
(e.g. a voltage ADC's LSBs) can match by chance over a handful of samples.
- State discrimination (
canair decode … --discriminate state, add --bytes
for raw bytes / --bits for toggling bits — no --try needed) — ranks
bytes/params by between-state vs within-state variance (F). Surfaces
thermal/mode/relay signals that shift by power state (charging vs ready vs
driving) rather than by a driving anchor — how the MCU inverter-temp byte was
confirmed (charging 22 °C vs driving 90 °C). Complements correlation.
- Mirror detection (
canair decode … --find-mirrors [--bits]) — reports
byte/bit positions exactly equal across all captures: redundant status mirrors
and unit-variants (the km/h-vs-MPH speed pair, an ignition bit echoed in two
places). A fast way to prune "new" bytes that are just copies.
- Autocorrelation / step size — lag-1 autocorrelation and mean |Δ| per sample
separate slow (thermal, integrating) from fast (load, switching) signals.
- Differencing / transforms (
--plot delta/abs/cumsum/normalize/smooth) —
a byte whose cumulative sum tracks SOC is a current; a byte whose delta
correlates with load acceleration is a torque/power proxy.
- Endianness & word width sweeps (
--plot t/e) — try u8/i8/u16/i16/…
×endianness; the physically-plausible, smooth, correctly-signed interpretation
is usually the right one. Beware readings that only look smooth because they
cross a PCI byte (garbage).
- Linear fit for scale/offset — once a byte tracks a known engineering value,
a straight-line fit (value = a·byte + b) gives the scale and offset; sanity
check the intercept physically (a temperature's cold-park reading ≈ ambient).
Expect thematic grouping (but don't rely on it)
Related signals are often laid out contiguously — a run of cell voltages, a
cluster of temperatures, a phase-current pair next to the temperatures that track
it. Finding one member of a group is a strong hint the neighbors are related
(e.g. the MCU B18–B21 "thermal cluster"). Use adjacency as a lead: if Bn is a
temperature, probe Bn±1 for more temperatures. But grouping is a heuristic,
not a rule — manufacturers interleave unrelated bytes, pad with calibration
constants, and reorder across DIDs. Always confirm each byte on its own evidence;
never assume a neighbor's identity.
Trust but verify existing PIDs
Cross-referencing the profile's existing PIDs is essential, but existing
definitions can be wrong — even ones marked verified: true. A "verified"
flag records that someone checked it once, possibly against a single short
drive, a cross-vehicle sheet with a byte offset, or a plausible-but-untested
hypothesis. Sources of error to watch for:
- Offset-by-one / cross-vehicle drift — Kia Soul/Niro sheets are offset by 1
byte from the Ioniq; a definition ported from them may read the wrong byte.
- Misclassified signal — a byte labeled a temperature that is actually a
load/current metric (this project has real examples that were later demoted to
enabled: false).
- Right byte, wrong scale/sign/endianness — plausible range but subtly wrong
transform.
- Stale after re-analysis — a fuller capture corpus can overturn an earlier
single-drive conclusion.
So: use existing PIDs as priors and correlation references, but re-validate
when they contradict your data or physical reasoning. If you find a mistake in a
verified param, that is the finding — demote it (enabled: false /
--unverified), record the corrected reasoning in notes:, and open/adjust a
research: lead rather than silently trusting it. Trust, but verify.
7. Test the expression WITHOUT committing
canair decode --try evaluates a candidate against every capture — no YAML edit:
canair decode MCU 2102 --try "MOTOR_RPM:RPM=[S10:S11]"
canair decode MCU 2102 --try "TORQUE:Nm=[S12:S13]/100" --stats
canair decode MCU 2102 --try "T=[S17:S18]" --corr MCU_MOTOR_RPM
canair decode MCU 2102 --plot
canair decode MCU 2102 --plot --corr MCU_MOTOR_RPM
Cross-ECU / cross-PID — the fastest lever when a known signal on another
ECU (speed on ESC, RPM on MCU) should relate to your unknown byte. All three
time-align by nearest timestamp over a co-polled drive (see the "Cross-ECU
correlation" bullet in step 6 for the full detail):
canair hunt AAF 2181 --against ESC:22C101:REAL_SPEED_KMH --state driving
canair hunt AAF 2181 --against ESC:22C101:REAL_SPEED_KMH --promote AAF_SPEED
canair decode MCU 2101 --corr MCU:2102:[S10:S11] --corr-transform delta
canair correlate --state driving --against ESC:22C101:REAL_SPEED_KMH
Scope the captures so a candidate is judged on the relevant drive/state, not
the whole history (shared with canair captures): --since/--until/--date,
--state SUBSTR/--label SUBSTR (case-insensitive; the natural unit of drive
analysis, e.g. --state driving), and --first N/--last N. Combine with
--stats --group-by state to contrast a candidate across drive segments, or
--compact --changes-only to watch it evolve with stationary runs collapsed:
canair decode MCU 2102 --try "T=[S12:S13]/100" --state driving --stats
canair decode MCU 2102 --stats --group-by state --state driving
canair decode ESC 22C101 --param REAL_SPEED_KMH --state driving --compact --changes-only
Iterate until the range is physical, the distribution makes sense (constant?
enum? continuous?), and — where a relationship should exist — the correlation
confirms it. A bad expression shows ERROR rather than hiding.
--plot (interactive signal explorer) is the fastest way to find a signal
when you don't yet have a candidate expression. It works even on a not-yet-defined
PID (raw payloads only). Keys:
←/→ move the byte offset (byte mode) or switch parameter (param mode)
t/T cycle the interpretation type (u8 i8 u16 i16 u24 i24 u32 i32 u64 i64 f16 f32 f64)
e toggle endianness · f cycle post-transform (raw delta abs cumsum normalize smooth)
+/- zoom the x-axis · ,/. pan · 0 reset x-range
i toggle a modal listing the captures behind the current view (date/time, state, label, notes, file)
m toggle byte↔param source · o overlay the --corr reference (with live Pearson r) · q quit
The caption under the chart shows the visible capture index range and its
date/time span, both tracking zoom/pan, so you always know which captures —
and when — the plotted segment came from. Byte mode shows the equivalent
WiCAN expression for the current interpretation (e.g. [S10:S11]) — copy it
straight into step 8 — and whether that byte is already mapped by a defined
parameter (= mapped: NAME, or ~ reads Bn: … for a partial overlap, or
unmapped), so you don't re-decode known bytes. It also warns when a multi-byte
read crosses a PCI byte (garbage); endianness and float types with no direct
WiCAN expression are flagged as such. Zoom/pan (+/-/,/.) narrows the
x-axis to inspect a segment (e.g. a single launch or regen event); i lists the
exact captures in that segment.
8. Define — write it to ecus/
Use canair pids (surgical, comment-preserving, auto-validated + auto-reverted
on schema failure) rather than hand-editing:
canair pids upsert-param MCU 2102 MCU_MOTOR_RPM "[S10:S11]" \
--unit RPM --min -10500 --max 10500 --unverified \
--source "Kia Soul VMCU CSV" --notes "signed 16-bit BE at B10:B11 (ISO-TP 0x07:0x08)"
If you found the byte with canair hunt, skip the manual step: hunt … --promote NAME writes the top hit straight to ecus/ as an enabled, unverified candidate
(same validated path) with the correlation r/n, linear fit, and unit guess
auto-filled into notes.
New params start --unverified and --enabled — this is the default.
Enabled+unverified means the candidate is generated into the WiCAN profile and
streams live, so it's easy to test against reality (that's the whole point of a
candidate). Do not add candidates as --disabled; only reach for
--disabled (or enabled: false) when a byte is proven bogus/redundant and
you're keeping it solely for the research trail (e.g. a constant/FF-padding byte,
or an exact mirror of an already-mapped param). A plausible-but-unconfirmed
hypothesis belongs enabled so you can watch it move. (Hand-editing ecus/ is
allowed, but the tool keeps field order/quoting correct and runs
canair validate pids for you.)
Keep --notes terse and factual (see Working principles). State the byte
offset, observed range/per-state values, and the one key piece of evidence
(e.g. correlation) — not a narrative. Record what the data shows; leave
speculation to the reader.
9. Verify — confirm against reality
Confirm the decoded values across the full history and against known physical
state, then flip to verified:
canair decode MCU 2102
canair decode MCU 2102 --stats
canair decode MCU 2102 --param MCU_MOTOR_RPM
canair decode MCU 2102 --unverified
canair coverage MCU 2102
canair validate pids
canair pids upsert-param MCU 2102 MCU_MOTOR_RPM "[S10:S11]" --verified
canair pids set-status MCU 2102 done --type decode
A parameter is verified: true only when validated against real data / known
state (physical correlation, matching a scan tool, or a definitive constant).
No capture ⇒ no proof ⇒ stays --unverified (see Working principles) — never
promote on a plausible guess or a byte offset ported from another car's profile.
Always mark off a worked lead (do not leave it open)
Every time you touch a research: lead you MUST update its status before moving
on — a lead you investigated but left pending/captured will be re-surfaced by
canair research and re-worked from scratch, wasting effort (and risking
re-probing the car). Close it to match reality. Valid statuses are
pending → captured → done (plus nrc for a dead scan); a lead awaiting live
confirmation is a verify-type item that stays captured until confirmed,
then goes done:
canair pids set-status <ECU> "<target>" done --type decode
canair pids set-status <ECU> "<target>" captured --type verify
canair pids set-status <ECU> "<target>" nrc --type scan
This applies to every outcome, not just success:
- Fully decoded / verified →
done (a real parameters: entry exists,
promoted to verified where possible; for a verify-type lead, done once
confirmed against reality).
- Decoded a candidate but it still needs a live/physical check → keep it
captured (as a decode- or verify-type item) with the enabled+unverified
param in place, and note exactly what to test.
- "Nothing to decode here" is also a result → mark it
done. If analysis proves
the unmapped bytes are constants/padding, message counters, checksums, or exact
mirrors of an already-mapped param, record that finding in the lead's notes
(with the evidence) and set it done — the negative result is the deliverable
(e.g. MCU 2102 B52/B53 = counter/checksum, HVAC 220100 FF-padding tail). Do NOT
silently drop it.
- Only part done → keep it open, but add a follow-up. If you did part of the
work (e.g. registered candidates but they need a drive to verify), update the
notes/what_to_test to reflect exactly what's left so the next pass starts where
you stopped, rather than re-deriving it.
Rule of thumb: after any analysis or capture session, run canair research --ecu <ECU> and confirm no lead you touched is still showing its old status. Prefer
canair pids set-status (surgical, validated) over hand-editing the research:
block.
10. Integrate
canair wican autopid write
canair wican autopid diff --wican home
python3 -m pytest -q
Then consider contributing the profile back (and, for the bundled car, an
upstream wican-fw PR — see the ioniq-reverse-engineering skill's goals).
Tool cheat-sheet (this workflow)
| Step | Tool |
|---|
| what to work on | canair research, canair coverage |
| what's captured | canair captures --sessions (TOC: date/state/label/notes/ECUs; --json) |
| talk to the car | canair query/scan/discover (--monitor, --save) |
| see captures | canair captures (--diff/--step/--rulers/--all/--latest/--summary/--since/--until/--state/--label) |
| map bytes | canair bix --annotate (+ --ecu ECU --pid PID to overlay which param maps each byte / flag unmapped) |
| reason about a signal | step 6 Hypothesize — ECU context, physics/EE (thermal mass), CS (enums/counters), statistics (--corr/--stats/autocorr) |
| test expressions | canair decode --try / --stats / --corr / --plot |
| explain an unknown PID | canair investigate <ECU> <PID> (mapped? / state F / best anchor + unit, one table; --bits for toggling bits, --events for the edge timeline) |
| decode a body event capture | canair investigate <ECU> <PID> --events --bits (edge timeline vs capture notes) + canair correlate --find-mirrors --bits (cross-ECU bit mirrors) |
| what's co-polled here | canair correlate --overlap (which ECU:PID pairs share timed samples) |
| cross-ECU correlate | canair decode … --corr ECU:PID:PARAM (+ --corr-transform, --method spearman); canair correlate [--against REF] [--bytes/--bits] [--lag-scan N] [--gate '>0'] [--promote NAME] |
| which byte is signal Y | canair hunt <ECU> <PID> --against ECU:PID:PARAM (linear fit + unit guess; --transform delta, --promote NAME, --all-interps) |
| find state-dependent signals | canair decode … --discriminate state [--bytes] [--bits] |
| find redundant mirrors | canair decode … --find-mirrors [--bits] (single-PID); canair correlate --find-mirrors [--bits] (cross-ECU) |
| scope a drive | --state driving / --since/--until/--date / --first/--last N (both captures + decode) |
| per-segment stats | canair decode … --stats --group-by state |
| watch evolution | canair decode … --compact --changes-only |
| write definitions | canair pids upsert-param / rename-param / rm-param / add-research / set-status |
| validate | canair validate pids, canair coverage |
| ship | canair wican autopid write |
Reference: WiCAN byte index notation
WiCAN expressions index into the raw CAN frame data including PCI bytes. The
firmware's ELM327 parser (parse_elm327_response() in autopid.c) runs headers
ON and copies ALL 8 CAN data bytes per frame (including ISO-TP PCI bytes)
sequentially into a flat byte array.
Byte layout (AutoPID internal format)
For a multi-frame response to 2101 on BMS (0x7E4):
Frame 0 (First Frame): [10 3B] [61 01 FF FF FF FF] → B00-B07
Frame 1 (Consecutive): [21] [d d d d d d d] → B08-B15
Frame 2 (Consecutive): [22] [d d d d d d d] → B16-B23
...
B00 = PCI high byte (0x10), B01 = PCI low byte (length)
B02 = SID response (0x61), B03 = PID echo (0x01)
B08 = PCI consecutive (0x21), B09 = first actual data byte of frame 1
- PCI bytes occupy indices 0, 8, 16, 24, 32, 40, 48, 56, ...
Byte indexing examples
For a 0x21 service request (PID 01), the response starts 61 01 <data...>:
B0 = 0x61 (service response ID)
B1 = 0x01 (PID echo)
B2 = first data byte
For a 0x22 service request (DID C00B), the response starts 62 C0 0B <data...>:
B0 = 0x62 (service response ID)
B1 = 0xC0 (DID high byte)
B2 = 0x0B (DID low byte)
B3 = first data byte
Expression syntax
Bnn (unsigned byte), Snn (signed), [Bnn:Bmm] (multi-byte unsigned),
[Snn:Smm] (multi-byte signed), Bnn:k (bit k, 0=LSB). Operators:
+ - * / << >> & | ^. See expression_parser.c for the full reference.
CAUTION: [Bnn:Bmm] reads consecutive raw bytes — it does NOT skip PCI
bytes. If a multi-byte value spans a CAN frame boundary (B07-B08, B15-B16,
etc.), the PCI byte at B08/B16/... is included, producing garbage. Use manual
bit-shifting instead: (B07 << 8) | B09 to skip the PCI byte at B08. Always use
canair bix to check whether a byte range crosses a PCI boundary.
canair bix w9
canair bix E
canair bix -2 w5
canair bix --table
canair bix -2 --annotate 62B0047402990C0040A000AAAA
canair bix --annotate 6101FFFF...
--annotate (-a) reconstructs the WiCAN frame with PCI bytes inserted and
prints each byte's WiCAN Bnn, ISO-TP index, Torque letter, bix, and role. Use
-1 (default) for service 21, -2 for service 22 DIDs.
Conversion table (WiCAN ↔ ISO-TP ↔ Torque ↔ bix)
You rarely need the raw table — canair bix does the conversion
(canair bix w9, canair bix --table, canair bix --annotate <payload>). The
full lookup table lives in the sibling file
byte-index-conversion-table.md; load it only
when converting by hand.
Reference: UDS decoding conventions (Hyundai/Kia example)
Marque-specific DID conventions for the bundled Ioniq (and other Hyundai/Kia) —
PID categories, DID paging vs indexing, DID range semantics (22Bxxx cluster,
22Cxxx body, 22Fxxx flash), and the Hyundai/Kia identity-DID -1 offset — are
in the sibling file
hyundai-kia-uds-conventions.md. These are
marque-specific, not universal; for another car, expect a different scheme and
re-derive it. The generic UDS-0x22-vs-KWP2000-0x21 distinction (per each ECU's
id_protocol) is summarized in the skill body above and in
docs/concepts/ecu-protocols.md.