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.

The driver door module's 512 bytes, read before the patch, as a light-on-dark grid. The three seventeen-byte blocks and the three mirror bytes are outlined.

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.

One byte of the door module across eighteen reads through the day. It climbs from DB to E0 with no write from me, while the other 511 bytes hold still.

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.