[ UDS Read Operations on FCA / Stellantis ]
Read-side counterpart to the UDS Write Operations reference guide. UDS (Unified Diagnostic Services, ISO 14229) Service 0x22 ReadDataByIdentifier lets you ask any ECU on the CAN bus for the current value stored under a 16-bit Data Identifier — VIN, serial number, software version, configuration byte, sensor calibration, or any of the thousand manufacturer-specific values that scan tools display. Non-destructive by definition: 0x22 only carries data out of the ECU, never in. The Secure Gateway Module on 2018+ FCA vehicles lets it through without AutoAuth for exactly this reason — reads are safe to expose; writes are not.
[ Scripts implementing this pattern ]
Three scripts on this site read DIDs via Service 0x22, sharing the same request frame format and module-catalog mapping. They live here alongside the protocol material so you can browse the pattern and the implementations on the same page.
UDS Service 0x22 ReadDataByIdentifier driver in bash. Translates a friendly module name (bcm, sccm, radio, ipcm [alias evic], hvac) into the request / response arbitration-ID pair plus the correct CAN bus — CAN-C for BCM / SCCM / IPCM, CAN-IHS for Radio / HVAC. Framing is handled by the kernel ISO-TP stack viaisotpsend/isotprecv, so single-frame, first-frame + flow-control, and multi-frame string returns all work without manual PCI byte arithmetic. $ ./rid.sh bcm F190 Read VIN from BCM $ ./rid.sh bcm F18C Read BCM serial number $ ./rid.sh hvac 0298 Read current HVAC vent mode $ ./rid.sh ipcm F195 Read IPCM software version Optional disk archive: positive responses (service byte 0x62 = 0x22 + 0x40) are written to$RID_DATA_DIR/<module>/rid/<DID>/<data>— the data is the filename, so a recursivefindacross the archive shows every distinct value ever observed for a given DID on a given module without any database. Pairs withridscan.shbelow to populate the archive automatically. SetRID_DATA_DIR=to relocate (default /home/pi/modules) orRID_NO_ARCHIVE=1to disable. Exit codes are explicit so wrappers can distinguish failure modes: 0 positive response, 1 negative response (NRC printed), 2 invalid CLI args, 3 invalid module name, 99 unknown response shape. The ridscan.sh wrapper relies on 99 to trigger bounded retries vs 1 to mean "no such DID, move on." Original by jmccorm. Polish by magikh0e: fixed a duplicatesccmlookup block where the second block silently overwrote the first (per jmccorm's ownINVALID INFORMATION - NEEDS CORRECTEDcomment); refactored the if/then chain to a singlecaseso the duplicate bug is structurally impossible to reintroduce; addedset -eu, explicit exit codes, env-var-based archive configuration. Original preserved as rid.legacy.txt.
Sweeps a range of UDS DIDs against a module by calling rid.sh once per DID. Used to discover which DIDs a module actually responds to — helpful when you have a candidate range from a service manual or a scan-tool capture and want to know which entries are populated on YOUR vehicle. $ ./ridscan.sh bcm Full 0x0000-0xFFFF sweep $ ./ridscan.sh bcm F100 F1FF Just the identification range $ SCAN_DELAY=0.1 ./ridscan.sh hvac Throttled overnight sweep Pairs naturally withrid.sh's archive mode: a full sweep populates$RID_DATA_DIR/<module>/rid/<DID>/<data>with every positive response, and you can diff the resulting tree against another vehicle / another scan run to spot which DIDs hold configuration that varies vs which are constants. Env vars:SCAN_DELAY(sleep between iterations, default 0),MAX_RETRIES(retries when rid returns code 99, default 3),RID_CMD(rid binary path, defaultridon PATH). Progress markers print every 256 DIDs — a full scan is 65,536 iterations and otherwise gives no indication it isn't wedged. Original by jmccorm. Polish by magikh0e fixed a real bug: the legacy retry-on-99 inner loop had no upper bound, so a DID that structurally always returned UNKNOWN RESPONSE (rare but possible with malformed module replies) would spin the scanner forever. Capped atMAX_RETRIESper DID. Original preserved as ridscan.legacy.txt.
Python counterpart torid.sh, built on the python-can + python-can-isotp + udsoncan stack. Reads VIN from the BCM via Service 0x22 on DID F190, but its real value is as a starting template — the same scaffold retargets to any other DID or module. Use rid.sh when you want a one-liner from the shell. Use the Python stack when you're about to do anything richer: NRC introspection (udsoncan parses NRC codes into named exceptions, not just hex bytes), structured DID definitions across an interview script, paired read/write sequences with explicit session management, or secured reads gated by Service 0x27 SecurityAccess seed/key unlock. Originally by jmccorm (03.2023, with acknowledged ChatGPT-4 assist). Polish by magikh0e: replaced deprecated python-canbustype=kwarg withinterface=and pulled CAN_IFACE / TX_ID / RX_ID / DID to top-level constants.
[ Service 0x22 — the on-wire protocol ]
Service 0x22 ReadDataByIdentifier is the simplest of the UDS read services. One request, one response, no session unlock for the standard identification range, no manufacturer-specific seed/key dance.
Request format
Service ID | DID HI | DID LO
0x22 XX XX
Example: read VIN (DID F190)
22 F1 90
On the wire as ISO-TP single frame (PCI 0x03 = 3 data bytes):
03 22 F1 90 00 00 00 00
Positive response
Service ID | DID HI | DID LO | Data...
0x62 XX XX N bytes
Example response (VIN, multi-frame because the VIN is 17 chars):
First frame (PCI 10): 10 14 62 F1 90 31 43 36
1 C 6
Consec frame (PCI 21): 21 4A 4A 54 41 47 31 4D
J J T A G 1 M
Consec frame (PCI 22): 22 4C 38 35 33 32 32 31
L 8 5 3 2 2 1
Reassembled data: 31 43 36 4A 4A 54 41 47 31 4D 4C 38 35 33 32 32 31
1 C 6 J J T A G 1 M L 8 5 3 2 2 1
= "1C6JJTAG1ML853221"
The kernel ISO-TP stack handles the multi-frame reassembly when you
use isotprecv; the example above shows what you'd see on
the raw candump output.
Negative response codes
NRCs you'll see from Service 0x22 (full table at ISO 14229; only the common ones listed here):
NRC 0x10 generalReject Catch-all "no" from the ECU
NRC 0x11 serviceNotSupported Module doesn't speak 0x22 at all
(extremely rare; almost any
modern ECU supports it)
NRC 0x12 subFunctionNotSupported Not applicable to 0x22 (no
sub-function); shouldn't appear
NRC 0x13 incorrectMessageLength Request is malformed
(you sent something other than
3 bytes after the PCI)
NRC 0x22 conditionsNotCorrect Engine off when the module
wants it on, ignition state
wrong, etc.
NRC 0x31 requestOutOfRange The DID is not valid for this
module. Most common NRC you'll
see while scanning DID space.
NRC 0x33 securityAccessDenied Going through SGW from OBD-II
on a non-pass-through DID, OR
target DID requires 0x27 unlock.
NRC 0x7E subFunctionNotSupportedInSession Switch to extended session
(Service 0x10 sub 0x03) and
retry.
NRC 0x7F serviceNotSupportedInActiveSession Same fix as 0x7E.
rid.sh prints the raw NRC byte; you can decode it from the table above. The udsoncan-based scripts (read_vin_uds.py, wid.py) raise named Python exceptions instead of returning the raw byte — one of the reasons to prefer the Python stack once your workflow gets past one-liners.
[ Module catalog — FCA / Stellantis arbitration IDs ]
The friendly-module-name mapping inside rid.sh and read_vin_uds.py is this table. Same arbitration-ID pairs the UDS Write Operations scripts use; the difference is purely the service byte (0x22 read vs 0x2F / 0x2E / 0x31 write).
Module Request Response Bus Speed
------------------------------------ --------- --------- -------- ----------
BCM Body Control Module $620 $504 CAN-C 500 kbps
SCCM Steering Column Control Module $763 $4E3 CAN-C 500 kbps
Radio / Head Unit $7BF $53F CAN-IHS 125 kbps
IPCM Instrument Panel Cluster $742 $4C2 CAN-C 500 kbps
(alias: evic, ipc)
HVAC Climate Control $783 $503 CAN-IHS 125 kbps
ECM Engine Control Module $7E0 $7E8 CAN-C 500 kbps
(OBD-II-standard pair)
The ECM is reachable via the OBD-II-standard $7E0 / $7E8 pair on CAN-C and is the target for engine PIDs read via OBD-II Mode 01 (the obd.sh / obd2.py path). UDS Service 0x22 also works against $7E0 / $7E8 for vehicle-info DIDs in the F1xx range — rid.sh doesn't have an "ecm" entry but you can extend the case statement; the on-wire protocol is identical.
JT-vs-JL deltas: on the JT Gladiator the BCM moved to $720 / $728 in some model years — see the JT vs JL deltas section of the Bus & Message Reference for current platform-by-platform mapping. The rid.sh case statement reflects the JL convention; verify against candump on any non-JL target before assuming.
[ DID discovery — finding what to read ]
The 16-bit DID address space is sparse. Most DIDs return NRC 0x31 (requestOutOfRange) on any given module; the populated subset is manufacturer-specific and only partially documented in service manuals. There are four useful starting points:
ISO-standardised F1xx range
The F100-F1FF range is reserved by ISO 14229 for cross-vendor compatibility. Every UDS-speaking ECU on every vehicle should respond to the standard entries the same way:
DID F18A System supplier identifier (manufacturer string)
DID F18B ECU manufacturing date (BCD: YY MM DD)
DID F18C ECU serial number (ASCII string)
DID F190 VIN (17-char ASCII)
DID F191 Vehicle manufacturer ECU h/w part number
DID F192 System supplier ECU h/w part number
DID F193 System supplier ECU h/w version number
DID F194 System supplier ECU s/w number
DID F195 System supplier ECU s/w version
DID F197 System name or engine type
DID F198 Repair shop code / tester serial
DID F199 Programming date (BCD: YY MM DD)
DID F19D ECU installation date
DID F1A0-F1EF Vehicle-manufacturer-specific identifications
Start here on any unfamiliar module. F18C and F195 tell you the module's identity and software version, which is what scan tools use to match the module against a coding database. F190 against the BCM gives you the VIN, which is the canonical "am I talking to the right vehicle?" sanity check.
Manufacturer-specific ranges — observed patterns on FCA
From sweeping populated FCA BCMs and HVAC modules over time, the populated DID ranges cluster like this:
0x0000-0x00FF System / session config
(e.g. HVAC $0298 = current vent mode)
0x2000-0x2FFF Calibration data, sensor offsets
0x4000-0x4FFF Diagnostic info (extended DTC data, snapshots)
0xA000-0xAFFF Statistical / lifetime data
(e.g. BCM $A01A = oil-change statistics)
0xD000-0xDFFF IOControl target catalog (the DIDs $2F drives)
(e.g. BCM $D0AD = horn IOControl)
0xF1xx ISO identification (see above)
None of this is binding — vendors put DIDs wherever they want — but the clustering is consistent enough across the FCA BCMs sampled on this site that it's a reasonable place to start a discovery sweep when service-manual data is unavailable.
ridscan.sh sweep strategy
For a brand-new module where no DID list is known:
# Cheap first pass -- ISO identification range only (~256 DIDs)
$ ridscan.sh <module> F100 F1FF
# Likely-populated ranges per the table above (~5000 DIDs each)
$ SCAN_DELAY=0.1 ridscan.sh <module> 0000 00FF
$ SCAN_DELAY=0.1 ridscan.sh <module> 2000 2FFF
$ SCAN_DELAY=0.1 ridscan.sh <module> A000 AFFF
$ SCAN_DELAY=0.1 ridscan.sh <module> D000 DFFF
# Full sweep, last resort, leave running overnight
$ SCAN_DELAY=0.1 ridscan.sh <module>
The archive directory at $RID_DATA_DIR/<module>/rid/
accumulates everything that returned a positive response, with the data
itself as the filename. After a sweep:
$ find $RID_DATA_DIR/bcm/rid/ -type f | head
/home/pi/modules/bcm/rid/F100/...
/home/pi/modules/bcm/rid/F18C/...
/home/pi/modules/bcm/rid/F190/31 43 36 4A 4A 54 41 47 31 4D ...
/home/pi/modules/bcm/rid/F195/01 02 00
...
For known-good DID lists on specific FCA platforms, the second tab of the JL Wrangler RE spreadsheet catalogs JEEP DIDs that the community has decoded. Cross-reference that list against your own sweep results to label populated DIDs.
Scan-tool capture as a discovery shortcut
A known-good scan tool (wiTECH, AlphaOBD, FCAScan) running normal operations against the same module is the highest-yield DID-discovery source on the planet. The walkthrough at Reverse Engineering UDS with JScan covers the workflow: capture the tool's request/response stream during a specific operation, decode the request bytes, infer what each DID does from the corresponding tool UI action. Then validate by issuing the same request via rid.sh and observing the response yourself.
[ ISO-TP framing — for responses > 7 bytes ]
A raw CAN frame has 8 data bytes. UDS responses routinely exceed that — the VIN alone is 17 ASCII chars + 3 bytes of service + DID prefix = 20 bytes, which spans three CAN frames. ISO-TP is the ISO 15765-2 transport-layer protocol that adds segmentation, flow control, and reassembly on top of plain CAN.
Frame types
Single Frame (SF) PCI byte = 0x0L (L = data length, 0-7)
Whole response in one frame.
Example: 03 62 F1 90 00 00 00 00
\ \________/
\ data (3 bytes)
PCI: length = 3
First Frame (FF) PCI = 0x1LLL (12-bit total length)
Tells receiver "I have L bytes coming
in total, here's the first 6."
Consecutive Frame (CF) PCI = 0x2S (S = sequence number, 1-15)
Continuation of a First Frame.
Flow Control (FC) PCI = 0x30 (continue) / 0x31 (wait) / 0x32 (abort)
Sent by the receiver in response to a
First Frame to indicate readiness.
Who handles the framing?
Three layers, three different framing experiences:
cansend / candump Raw CAN frames. You handle ISO-TP yourself.
dtc.sh does this for its multi-frame
responses; see
dtc.sh for a
manual FC implementation.
isotpsend / isotprecv Userspace tools on top of the in-kernel
can-isotp module. Framing handled
automatically. Used by rid.sh.
python-can-isotp + Library-level framing. Handles SF / FF /
udsoncan CF / FC plus session timing, NRC parsing,
concurrent transactions. Used by
read_vin_uds.py.
The middle option (isotpsend / isotprecv) is the sweet spot for shell scripting — you write 0x22 plus the DID into the sender's stdin, you read the response data back from the receiver's stdout, and ISO-TP framing is entirely transparent. No PCI byte arithmetic, no sequence-number tracking, no Flow Control to send. The kernel does it all.
[ SGW pass-through — why 0x22 is special ]
On 2018+ FCA / Stellantis vehicles, the Secure Gateway Module sits between the OBD-II port (and the Uconnect head unit) and the rest of the vehicle's CAN buses. It filters traffic by service ID:
Service ID Direction SGW behaviour
----------- ------------------ --------------------------------------------
OBD-II Mode 01 (read) PASS — standardised, read-only
Mode 03/07/0A PASS — DTC reads, read-only
Mode 09 (info) PASS — VIN / ECU name, read-only
Mode 04 (clear) BLOCK — ClearDTCs is a write
UDS 0x22 (RID) PASS — read-only by definition
0x10 (session) PASS — metadata, no state change
0x3E (tester) PASS — keepalive only
0x2F (IOControl) BLOCK — actuator drive
0x2E (WID) BLOCK — NVRAM write
0x31 (routine) BLOCK — state-change routine
0x27 (security) BLOCK — gate for write services
0x34 / 36 / 37 BLOCK — flash / programming
The principle is simple: read = pass, write = require AutoAuth. Service 0x22 is read-only by definition; nothing about a Service 0x22 request can modify state on the target ECU. So the SGW passes it through unauthenticated regardless of which DID is being queried.
This is why DID-discovery sweeps via ridscan.sh against the OBD-II port work on 2018+ FCA vehicles, even though writing to those same DIDs via Service 0x2E would return NRC 0x33 (securityAccessDenied) from the SGW. For the write side, the workaround is the behind-the-glovebox 13-way connector taps that sit inside the SGW boundary; for reads, no workaround is needed.
The exception, rare but worth knowing: a small subset of manufacturer-specific DIDs are gated behind Service 0x27 SecurityAccess even for reads. These return NRC 0x33 from the target ECU itself (not the SGW), and the fix is the seed/key unlock for that specific module — usually documented only in service manuals or available via certified scan tools. The DIDs in this category are typically security-relevant (immobilizer secrets, key fob hash, encryption keys), so the gating is deliberate.
[ Safety — reads are safe, but read with care ]
Service 0x22 is one of the lowest-risk UDS services to experiment with. Three things to still watch out for:
1. Bus traffic load. A full ridscan.sh sweep is 65,536 requests with
full request/response cycles. On CAN-IHS (125 kbps, shared with
radio / HVAC / cluster) that's enough traffic to noticeably slow
down legitimate broadcast updates. Set SCAN_DELAY=0.1 or higher
during live-vehicle sweeps. CAN-C at 500 kbps is more forgiving
but still benefits from throttling.
2. Wake-state cycling. ridscan.sh's outer loop calls rid.sh, which
issues isotpsend / isotprecv. If the bus is idle when the scan
starts, each iteration may trigger NM-wake activity from the
module. Over a long sweep this keeps the vehicle's modules
awake longer than they would be otherwise — not a problem
for short scans, but an overnight 65K-DID sweep on a parked
vehicle WILL drain the battery faster than normal.
3. The rare 0x22 -> state-change DIDs. Almost all DIDs are pure
reads, but a handful of manufacturer-specific DIDs use 0x22 as
a trigger for diagnostic actions (sensor zeroing, NVM flush,
learn routines). Service manuals call these out explicitly when
they exist. If a sweep encounters a DID that takes unusually
long to respond, or whose subsequent reads return different
values, treat it as suspect and skip it on future sweeps.
4. DIDs that return secrets. Some manufacturer-specific DIDs return
immobilizer challenge values, RKE rolling counters, or key-fob
hash material. These DIDs are usually gated by Service 0x27, but
not always. If a response looks like 8-16 bytes of high-entropy
data with no obvious encoding, write it down once for posterity
and don't post it publicly — it might be an unintended leak
of a security-relevant value.
