[ 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.

rid.sh — 05.2026
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 via isotpsend /
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 recursive find
across the archive shows every distinct value ever observed for a given
DID on a given module without any database. Pairs with
ridscan.sh below to populate the archive automatically. Set
RID_DATA_DIR= to relocate (default /home/pi/modules) or
RID_NO_ARCHIVE=1 to 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 duplicate
sccm lookup block where the second block silently
overwrote the first (per jmccorm's own INVALID INFORMATION -
NEEDS CORRECTED comment); refactored the if/then chain to a
single case so the duplicate bug is structurally
impossible to reintroduce; added set -eu, explicit exit
codes, env-var-based archive configuration. Original preserved as
rid.legacy.txt.
ridscan.sh — 05.2026
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 with rid.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, default rid on 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 at
MAX_RETRIES per DID. Original preserved as
ridscan.legacy.txt.
read_vin_uds.py — 05.2026
Python counterpart to rid.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-can bustype=
kwarg with interface= 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.

[ See also ]