[ UDS Write Operations on FCA / Stellantis ]

UDS write operations on FCA / Stellantis vehicles have real-world consequences. The same protocol that lets you blink the 3rd brake light for a demo can leave your horn stuck on, your immobilizer confused, or an ECU bricked into needing a dealer reflash. Reading via Service 0x22 or OBD-II Mode 01 is safe and cheap; writing via Service 0x2F, 0x2E, 0x31, or 0x27 is not. This page is the shared scaffolding that the actuator-control scripts on the site sit on top of, with the safety discipline brought to the front instead of buried in each script's docstring.

[ Scripts implementing this pattern ]

Three scripts on this site write to FCA ECUs via UDS, all following the scaffolding documented in the sections below: NM wake → Extended Diagnostic Session → service-specific request → cleanup. They live here so you can browse the patterns and the implementations on the same page.

3rd_brakelight.sh — 05.2026
Demo of UDS Service 0x2F (IOControlByIdentifier) on a JEEP platform —
toggles the cargo-area / 3rd brake light on and off via the request sequence
walked through in the Bus & Message
Reference: NM wake → Extended Diagnostic Session → IOControl ON
→ IOControl OFF, plus a background TesterPresent keepalive so the ECU's
S3 timer doesn't drop the session and a SIGINT trap that issues
returnControlToECU on exit (never leaves the brake light in test-controlled
state).

UDS rides on CAN-C alongside the broadcast traffic but uses an entirely
different framing — every request gets a positive or negative response
from the target ECU, and writes (clearing DTCs, forcing actuators, programming)
are what 2018+ FCA vehicles gate behind the Secure
Gateway Module. Direct CAN access via the 13-way connectors behind the
glovebox bypasses the SGW; UDS over the OBD-II port does not.

CLI: --once (single toggle for testing), --verbose (echoes every cansend),
--help. Python alternative for serious UDS work:
udsoncan.
horn.sh / 3honk.sh — 05.2026
Two implementations of the same UDS-driven horn honk. Both target the
BCM ($620 / $504) and use Service 0x2F on DID $D0AD; byte-level
walkthrough at Bus & Message
Reference. The difference is which can-utils tool generates the
frames:

    horn.sh    cansend with hand-padded 8-byte CAN frames. Adds CLI
               flags (--press DUR_MS, --burst N, --tap-ms MS,
               --gap-ms MS, --verbose, --help), a SIGINT trap that
               silences the horn + issues returnControlToECU on exit,
               and a background TesterPresent keepalive so the ECU's
               S3 timer doesn't drop the session mid-honk.

    3honk.sh   isotpsend, which prepends the PCI length byte and pads
               the frame to 8 bytes for you. Hardcoded three quick
               taps, no CLI.

When to pick which:

  - For a one-byte actuator command, the two are functionally
    equivalent. Use horn.sh if you want the press/burst CLI surface
    and the safety scaffolding.

  - Once a request grows past 7 UDS bytes — WriteDataByIdentifier
    with multi-byte values, SecurityAccess seed/key exchange,
    ReadDTCInformation streams — isotpsend handles multi-frame
    ISO-TP framing. cansend can't, and you'd have to write the FF + CF
    + Flow-Control state machine yourself.

  - When adding a SIGINT trap or other safety wrapping around an
    actuator with real-world side effects, horn.sh's cansend version
    is easier to start from (every frame is explicit; you can see
    what's on the wire).

Don't run either at 2am in a dense neighbourhood. Both originals by
jmccorm; horn.sh polished, 3honk.sh preserved closer to its minimal
form.
2k.sh — 05.2026
Holds engine RPM at 2000 via UDS Service 0x31 (RoutineControl) on the
Engine Control Module ($7E0 / $7E8). Press Ctrl+C to release. Differences
from the horn / brake-light demos:

  - Target ECM uses OBD-II-style 11-bit IDs ($7E0 / $7E8), not the
    FCA-internal $620 / $504 BCM pair.
  - Session sub-function 0x92 (Chrysler manufacturer-specific), not
    0x03 (extended).
  - Service 0x31 (RoutineControl, startRoutine 0x07D0) instead of
    0x2F (IOControlByIdentifier).

There's no explicit cancel command. The script holds the session open
by sending TesterPresent every 0.25s; once Ctrl+C stops that, the
ECM's S3 timer expires the session and the RPM hold is released on
its own.

The routine identifier 0x07D0 = 2000 decimal looks suspiciously like
the target RPM encoded directly. Worth testing whether
31 05 05 DC (0x05DC = 1500) sets 1500 RPM; not yet
confirmed.

Safety: engine running, park brake set, transmission
in PARK, not in a closed garage. Hit Ctrl+C if anything sounds wrong.
"Lightly tested" by jmccorm — worked for cold-morning warm-up,
failure modes not explored. Original by jmccorm; preserved with
header annotations by magikh0e.

[ Reads vs writes — why they're different ]

The UDS protocol treats reads and writes symmetrically at the wire level. Both are request/response, both use the same module ID pairs, both follow the +0x40 positive-response rule. But the operational distinction is significant:

Reads (Service 0x22 ReadDataByIdentifier, OBD-II Mode 01)
    - Idempotent: no state change on the ECU
    - Safe to retry, safe to spam, safe to script naively
    - Pass through the SGW unauthenticated on 2018+ FCA
    - Worst case: you get NRC 0x12 (subFunctionNotSupported) or
      NRC 0x31 (requestOutOfRange) for a DID the ECU doesn't know
    - Bench-testing not strictly required

Writes (Services 0x2F, 0x2E, 0x31, 0x27)
    - State-changing: the ECU does something different after
    - Some writes persist across battery disconnect (0x2E to NVRAM)
    - Most are gated by the SGW on 2018+ FCA OBD-II access
    - Cleanup is YOUR responsibility -- if the script dies mid-write,
      the ECU may stay in test-controlled state
    - Worst case: bricked module, stuck immobilizer, ABS fault that
      needs a dealer reflash to clear
    - Bench-testing on a salvage ECU is the responsible default
      when stakes are real

Everything below is about the write side. For the read side, see the Bus & Message Reference UDS walkthrough and the OBD-II section.

[ The shared pattern ]

Every UDS-write script on the site ( 3rd_brakelight.sh, horn.sh / 3honk.sh, 2k.sh) follows the same four-step scaffolding. The DID changes, the service might change, the cleanup model varies, but the spine stays the same:

1. WAKE the bus (optional, only needed if you can't guarantee the target ECU is already awake):

cansend $WAKE_BUS  2D3#0700000000000000
sleep 0.1

2. ENTER an extended diagnostic session on the target ECU:

cansend $UDS_BUS  $REQ_ID#02 10 SS 00 00 00 00 00
# SS = 0x03 extended, 0x92 Chrysler-specific, etc.

3. WRITE — the service-specific request. Pick from Service 0x2F (IOControlByIdentifier, toggle an actuator), 0x2E (WriteDataByIdentifier, persist a value), 0x31 (RoutineControl, start/stop a routine), or 0x27 (SecurityAccess, seed/key, if gated).

4. CLEANUP — either explicit (toggle-and-release) or implicit (let the session time out), depending on which write you used. See the cleanup-patterns section below.

The optional fifth piece is a background TesterPresent keepalive (Service 0x3E) that prevents the ECU's S3 timer from dropping the session during long writes. Needed for any write that holds state for more than a few seconds; not needed for fast toggles.

Every script on the site does (1) + (2), and a different combination of (3) and (4) depending on what it's trying to accomplish.

[ Service overview — when to reach for which ]

  Service 0x2F  IOControlByIdentifier
  -----------------------------------
    Short-term actuator takeover. The ECU forces the addressed I/O
    to a specified state for the duration of the session, then
    reverts. Use for: horn, brake light, lights, any actuator where
    you want point-in-time control. Frame layout:

        02 2F  HH LL  CC  SS                  request
              \__DID__/  ctrl state
                          0x03 = shortTermAdjustment
                          0x00 = returnControlToECU (releases takeover)
                          0x01..0xFF = additional control flavours

    Demo scripts: 3rd_brakelight.sh,
    horn.sh,
    3honk.sh


  Service 0x2E  WriteDataByIdentifier
  -----------------------------------
    Persistent write to ECU NVRAM. The new value survives ignition
    cycles, battery disconnect, and session termination. Use for:
    permanent vehicle configuration changes -- regional settings,
    comfort options, calibration bytes. Frame layout:

        XX 2E  HH LL  D0 D1 D2 ...             request
              \__DID__/  \__data bytes__/

    Where things get dangerous: 0x2E does NOT roll back if you write
    a value the ECU later rejects. Always 0x22-read the DID first to
    capture the original, then 0x2E-write your change, then 0x22-read
    the result to verify. If the read-back doesn't match, write the
    original back IMMEDIATELY before the session times out.

    No on-site demo script for this yet. Bench-testing strongly
    recommended.


  Service 0x31  RoutineControl
  -----------------------------------
    Start, stop, or query a routine running on the ECU. The routine
    runs as long as the diagnostic session stays open; ending the
    session ends the routine. Use for: continuous-behaviour writes
    like RPM hold, automated tests, brake-bleed routines, key-learn
    sequences. Three subfunctions:

        02 31 01  HH LL                        requestResults
        02 31 02  HH LL                        stopRoutine
        02 31 05  HH LL                        startRoutine
                 \__routine ID__/

    On Chrysler, the routine identifier sometimes encodes the
    target value directly (2k.sh uses RID 0x07D0 = 2000 decimal =
    target RPM; whether 0x05DC = 1500 RPM is still untested).

    Demo script: 2k.sh


  Service 0x27  SecurityAccess
  -----------------------------------
    Seed/key unlock for ECUs that gate write services behind
    cryptographic challenge-response. The ECU returns a seed value;
    the tester computes a key from the seed using a vendor-specific
    algorithm; the tester sends the key back; the ECU unlocks the
    deeper write services for the rest of the session.

        02 27 SS                                requestSeed
              sub: 0x01, 0x03, 0x05... (level)
        N  27 SS  SS SS SS SS ...               sendKey

    Most consumer-grade actuator writes on FCA we've seen do NOT
    require 0x27 unlock; the SGW handles their access control
    instead. ECU programming, immobilizer-related operations, and
    some key-learn flows DO require 0x27 with manufacturer-specific
    seed/key algorithms. Out of scope for this site.

[ Cleanup patterns ]

Two distinct cleanup models on the site. They're not interchangeable — the model you choose has to match what the write actually does.

TOGGLE-AND-RELEASE. Used by 3rd_brakelight.sh, horn.sh, 3honk.sh. Service 0x2F IOControlByIdentifier. The script issues an explicit OFF (state byte 0x00) and a returnControlToECU (control byte 0x00) at exit, including from a SIGINT trap. If the script crashes mid-toggle, the ECU is left in test-controlled state until something else writes the DID — bad for a stuck-on horn, bad for a brake light at noon. Sketch:

trap cleanup INT TERM
cleanup() {
    # Explicit OFF
    cansend $BUS  $REQ#052F${DID}03000000
    # returnControlToECU
    cansend $BUS  $REQ#042F${DID}00000000
    exit 0
}

enter_session
iocontrol_on   # state=0x01
sleep 0.5
iocontrol_off  # state=0x00
cleanup

HOLD-VIA-TESTERPRESENT. Used by 2k.sh. Service 0x31 RoutineControl. No explicit cancel command. The script holds the session open by sending TesterPresent every ~0.25s. The moment Ctrl+C kills that loop, the ECU's S3 timer expires the session and the routine releases automatically. Elegant when it fits — no possibility of "script died, ECU stuck" because the protocol handles cleanup. Sketch:

enter_session
start_routine
while true; do
    sleep 0.25
    cansend $BUS  $REQ#023E800000000000   # TesterPresent
done
# Ctrl+C kills the loop. S3 timer fires. Routine releases.

WHEN TO PICK WHICH. Reach for toggle-and-release for point-in-time changes (single action, then revert immediately); 0x2F IOControl flows; anything where the ECU needs an explicit "go back to normal" message. Reach for hold-via-TesterPresent for continuous-behaviour writes (the action IS the duration); 0x31 RoutineControl flows; anything where session-timeout naturally means "stop".

[ Known writable targets on JEEP platforms ]

Verified write targets on the JEEP-platform vehicles this site is built around. All require direct CAN access via the behind-the-glovebox connectors on 2018+ vehicles; OBD-II-port access is gated by the SGW.

  Target       Service  Module             Effect                Demo script
  -----------  -------  ----------------   --------------------- ------------------------
  $D1B3        0x2F     BCM ($620/$504)    3rd brake light       3rd_brakelight.sh
                                            on/off (short-term)
  $D0AD        0x2F     BCM ($620/$504)    Horn on/off           horn.sh / 3honk.sh
                                            (short-term)
  RID 0x07D0   0x31     ECM ($7E0/$7E8)    Engine RPM hold       2k.sh
                                            at 2000 RPM
                                            (manuf session 0x92)

  Candidate targets (UNVERIFIED, drawn from JL Wrangler community
  RE work; treat as research leads, not facts):

    $26F      Sway bar disconnect request   (see BMR #candidate-ids)
    $25D      Front/rear locker request     (see BMR #candidate-ids)
    Various   Door locks (RKE-style, but via UDS at the BCM)
    Various   Mirror fold / unfold
    Various   Trailer brake controller test routines

The candidate targets above are documented in the BMR candidate-IDs section. As verified write recipes get added to the site, this table grows. The hardest part of building a new UDS-write script is usually NOT the scaffolding (which is identical across all of them) — it's confirming the right DID / RID / control byte combination for the action you want, on the specific platform you have. Sniff first with a known-good UDS tool (JScan, wiTECH, Autel) using the JScan UDS intro technique, then script.

[ SGW interaction ]

On 2018+ FCA / Stellantis vehicles the Secure Gateway Module gates all UDS writes that arrive via the OBD-II port. The distinction matters per-script:

  OBD-II port access (the diagnostic connector under the dash):

      Allowed unauthenticated:  Mode 01 reads, Service 0x22 reads
                                of specific DID ranges, basic
                                identification queries
      Blocked unauthenticated:  Service 0x2F (IOControl),
                                Service 0x2E (WriteDataByIdentifier),
                                Service 0x31 (RoutineControl),
                                Service 0x04 (ClearDTCs),
                                Service 0x27 (SecurityAccess flows
                                that touch ECU programming)

      Result on blocked writes: NRC 0x33 (securityAccessDenied)
                                or the request silently drops with
                                no response at all.

      Workaround: AutoAuth subscription + certified scan tool.
                  Per-session token; can't be scripted from a Pi.

  Behind-the-glovebox 13-way connectors:

      Allowed unauthenticated:  EVERYTHING. These connectors sit
                                INSIDE the SGW boundary; traffic
                                sourced here reaches the internal
                                CAN-C and CAN-IHS buses directly
                                without the SGW seeing it.
      Required:                 Physical access (~10 minutes of
                                glovebox disassembly); a CAN
                                interface that talks SocketCAN
                                (Pi + Waveshare 2-channel HAT,
                                or any USB-CAN adapter).

      This is why all the UDS-write scripts on this site assume
      direct CAN access. The 3rd_brakelight / horn / 2k scripts
      all use can0 / can1 named interfaces wired to the
      behind-glovebox tap, not the OBD-II port.

Reads (Service 0x22, Mode 01) work through the OBD-II port unchanged — that's why obd.sh and getVIN.sh don't have this restriction. The split is reads vs writes, not what kind of cable you have.

[ Safety ]

  RULES THAT APPLY TO EVERY UDS WRITE
  ===================================

  1. Bench-test before vehicle-test. A salvage ECU on a workbench
     is much cheaper than a dealer reflash.

  2. Always 0x22-read before 0x2E-write. Capture the original
     value so you can write it back if the change doesn't behave.

  3. Send returnControlToECU on exit for every 0x2F write.
     Trap SIGINT, write it in the exit path, write it AGAIN at
     normal exit. Better to send it twice than to send it zero
     times.

  4. Use Service 0x31 only when the action is naturally continuous.
     RoutineControl works because the protocol handles cleanup;
     forcing 0x31 patterns onto point-in-time actions just hides
     the cleanup problem behind a TesterPresent loop.

  5. Don't poke at unknown DIDs blindly. The candidate-IDs section
     of the BMR is full of "this looks like it might be X" entries.
     Validate with a known-good UDS tool's reaction BEFORE you
     script anything.

  6. Engine-RPM writes (2k.sh and similar) need transmission
     in PARK, parking brake set, ventilation. The script can't
     enforce any of this; it can only refuse to run if something
     is obviously wrong, and it doesn't even do that yet.

  7. If you see NRC 0x33 (securityAccessDenied) and you're going
     through the OBD-II port, you need direct CAN access -- not
     a longer timeout, not more retries. NRC 0x33 means "the
     SGW said no."

  8. If you see NRC 0x12 (subFunctionNotSupported) or 0x7F (unknown
     service), the target ECU genuinely doesn't speak that service
     or sub-function. Try a different session sub (0x03 vs 0x92)
     or a different module.

  9. WriteDataByIdentifier (0x2E) to immobilizer / key-learn /
     security DIDs is in a separate risk tier from actuator writes.
     Wrong values here brick the immobilizer; brick the immobilizer
     and the vehicle won't start until you pay a locksmith. We don't
     have demo scripts for that category on this site, and won't.

[ See also ]