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