Folding mirrors on lock
The reason this project exists is small and slightly ridiculous. I wanted my Scirocco to fold its wing mirrors when I lock it, the way it already folds them from the switch inside the door. The car can do it. The feature is disabled in a control unit's memory, and enabling it means changing three bytes.
Last time ended with the two
door modules identified over an older Volkswagen transport called TP2.0: part
1Q0 959 701 H, software 2162, the driver's side at logical address 22. This
post is the day I read that module's memory, backed it up, and wrote to it. The
hardware has not changed since last time: the same
OBDLink CX adapter on the
diagnostic port, the same
ESP32-P4 panel
driving it.
Every command in this build is read-only except one specific, reviewed, single-shot write with its own gate. I am changing three bytes of my own car, with the door in front of me, and I still built the firmware so it cannot do a fourth. The refusals are in here too. They are where the real shape of the thing shows.
The lock
The mirror bytes sit behind SecurityAccess, service 27. The module sends a four-byte seed, and I have to send back a key it accepts, or the request to read memory goes nowhere. The standard defines the wire format and explicitly does not define how the key comes from the seed. So the key is the whole problem.
The first attempts, the evening before, were refusals worth keeping. Asking for
a seed in the ordinary diagnostic session got 7F 27 12, subFunction not
supported, every time. No seed, no way in. The community material for these
modules kept naming an engineering session and a five-digit login 04354, and
kept describing it as additive: you take the seed as a number and add a constant
to it. Decimal 4354 is 0x1102.
The test was one exchange, gated so it could run exactly once per boot and never
retry, because a wrong key on a control unit earns a lockout with a time penalty
that stacks. Starting engineering session 86 returned 50 86. Then:
27 03 -> 67 03 00 08 BD B8
0x0008BDB8 + 0x1102 = 0x0008CEBA
27 04 00 08 CE BA -> 67 04 34
67 04 34. The key was accepted, and the trailing 34 is the status byte for
securityAccessAllowed. The additive login is real on this exact door. One
attempt, no retry, and the code disconnected immediately without reading a
single byte, because reading was a separate decision.
Reading is a different service than I expected
I had assumed the read would be ReadMemoryByAddress, service 23. It is not. The
first attempt at anything memory-shaped returned 75 3F, which is a positive
response to service 35, RequestUpload. On this module the way to read memory is
to request an upload from the car's point of view, then pull the bytes with
TransferData. The naming is backwards from the direction the data actually
travels, and it cost me a rewrite to see it.
One byte first, at the address the community procedure points at, 0300BD:
35 03 00 BD 00 00 00 01 -> 75 3F
36 -> 76 C0
37 -> 77
C0. Then the same read at 0300CE on a fresh authenticated connection, and
again at 0300DF on another: C0 both times. Three bytes reading C0, where
the procedure wants them to become D0.
Then 48 contiguous bytes from 0300BD, and the structure underneath the three
candidates showed itself:
0300BD C0 06 EF FF 3D B0 EF FF 00
0300C6 37 52 00 76 83 02 07 2F C0 06 EF FF 3D B0 EF FF 00
0300D7 37 52 00 76 83 02 07 2F C0 06 EF FF 3D B0 EF FF 00
0300E8 87 85 1F 17 0C
Two complete seventeen-byte blocks, byte for byte identical, and each candidate
is the ninth byte of its block. An external write-up describes exactly this shape:
three seventeen-byte blocks spaced from B5 onwards, with the mirror byte at
position eight of each. The spacing and the position matched. The block contents did not,
because that write-up is somebody else's module, so it told me where to look and
nothing whatsoever about what my own car holds.
Finding the edges before trusting the map
How big was this memory, and where did it start? I had no idea, and a full read
needs both answers, so I probed the boundaries one byte at a time. 030000 read
FF. 0301FF read 4E. 030200 accepted the upload request and then refused
the data with 7F 36 74, illegalAddressInBlockTransfer. So the readable window
runs 030000 through 0301FF: 512 bytes, matching the size the vendor tools
quote for this EEPROM. That refusal one byte past the end drew the boundary for
me instead of my guessing it.

Then the backup. Eleven read slices, each on its own fresh login, assembled into
one 512-byte image, and the whole thing done a second time independently. Both
matched to the byte, same SHA-256. The module's own part number sits near the
start as plain ASCII, the three seventeen-byte blocks are identical inside the
image, and all three mirror bytes read C0. Two matching reads is no proof that
the memory holds still forever. It is a backup, and it is repeatable.
The obvious write does not exist
With a verified backup I tried the write I expected to work: WriteMemoryByAddress,
service 3D, the same value back, C0 to C0, so that even a success changed
nothing. The module refused it: 7F 3D 11, serviceNotSupported. That door does
not implement the direct memory write at all.
Which took away the safe experiment. The only remaining path was the RequestDownload and TransferData pair the community procedures use, and that pair carries risk I cannot bound from outside the module: a download might reserve or erase a whole unit far larger than the single byte I mean to change, the point at which it commits is not something I have identified, and an interruption partway through could leave the door in a state that no read of mine can describe and no write of mine can undo.
So I did the most cautious version I could think of. RequestDownload, then TransferData with the same value already in the cell, then exit. If the module accepted a same-value transaction and the full image afterwards was byte for byte identical to the backup, I would know the mechanism works without having changed anything yet.
That took several tries, and the failures are the interesting part. One run
started the download, got a positive 74 3F, then returned 76 C0 to the data
frame where my firmware was waiting for a bare 76, so it stopped with the
transaction half-done and held the adapter rather than guessing. Another stopped
in preflight because the adapter's CAN transmit error counter was non-zero, which
on this setup meant a bus-off I had to clear with an adapter reset before going
near the module again. Only on the fourth namespaced attempt did a clean same-value
download complete, 34 / 36 / 37 all positive, and four independent images, two
before and two after, all match to the byte.
The byte that moved on its own
While all this was going on, one byte kept changing when I was not writing
anything. Address 0300194. It read DB in the first backup at 2pm. By the
late-afternoon reads it was DD, then DE, and by evening E0. I never wrote
to it. Between those reads the car had been locked and opened again, had its
windows run from the remote, and had its ignition cycled.

I ran a small read-only investigation to pin the cause, and could not. An identification-only connection did not move it. A login-only connection with no memory service did not move it. It moved across intervals where the owner had used the car normally, and every other byte in the sampled region held still. A byte that increments through normal use looks like a counter, and I have written exactly that down and nothing stronger, because I never isolated a single cause. It mattered for one concrete reason: my verification demanded that the post-patch image match the backup with only the three mirror bytes changed. A drifting byte would have tripped that check for a reason unrelated to my write, so every comparison used a fresh baseline taken minutes before, not the morning's image.
Three bytes
The patch itself was three fixed stages, 0300BD, 0300CE, 0300DF, each on its
own authenticated connection, each pre-reading its own byte as C0, writing D0
to that one address, and reading it straight back as D0 before the next stage
was allowed to start. The download exchange per stage:
34 03 00 BD 00 00 00 01 -> 74 3F
36 D0 -> 76 D0
37 -> 77
35 03 00 BD 00 00 00 01 -> 75 3F (read back)
36 -> 76 D0
37 -> 77
Then two full independent 512-byte images afterwards. Both matched each other, and both matched the immediate baseline with exactly three differences:
0300BD C0 -> D0
0300CE C0 -> D0
0300DF C0 -> D0
Every other byte, including the drifting 0300194, unchanged. No masked offsets,
no tolerated fields, one write per address and no retries.
The mirrors fold
The diagnostic side proves three bytes changed and nothing else did. Whether the mirrors actually fold is a separate fact, and the only way to get it was to stand in the driveway with the key. Holding the lock button folds both mirrors. They stay folded when the car is opened again, and come back out when the ignition goes on.
Both mirrors moved from a change on the driver's side alone, which fits how these modules drive the pair. It is hold-to-fold on the remote rather than a single short press, and I have not established what a short press does. The thing the whole project was for now happens in the car.
OBDLink CX over BLE -> CAN -> TP2.0 -> KWP2000
-> SecurityAccess 04354 -> EEPROM read and write -> mirrors that fold
What this does not prove
The honest limits, because they are the parts most likely to bite someone reading
this as a recipe. The same-value download completed and left the image unchanged,
but that does not tell me the module's actual programming unit, or the exact point
where it commits, or whether a power loss mid-write is recoverable. My backup is a
verified read of the 512-byte window; it is not proof that the window is the whole
physical memory, and clean diagnostic restoration is not the same as being able to
undo a write. The additive login and the three offsets are established on this one
1Q0 959 701 H at software 2162, and nowhere else. Another car is another
experiment.
Next is the companion side: a phone talking to the panel rather than to the adapter, and the panel showing the car's live data instead of a status word.