Guidance for AI coding agents (and the humans supervising them) working in this repository. Read this before making changes.
Reverse-engineering notes and tooling for PSA/Stellantis SMEG+ head units. The headline goal is an AUX auto-switch: an aftermarket CarPlay/Android-Auto piggyback feeds audio into the unit's AUX input, and the unit should select AUX by itself.
The work is split in two:
- Application patches — in-place edits to the PowerPC image inside
AppBin/f_BigQuick.bin, applied and checksum-cascaded bytools/patch_smeg.py. This is the shipped, tested path. - Media-partition edits — ringing tones, the cheatcode menu, version markers. These
need
system.binunpacked, edited and repacked. This path is not finished — see issue #35 for the tool and #36 for the one open detail.
-
Never commit vendor firmware. No Peugeot/Citroën/DS/Stellantis/Magneti Marelli binaries, no upgrade packages, no symbol maps, no extracted images or tones. The repo documents and patches; it does not distribute.
.gitignoreblocks the usual extensions, but check before you commit. If a task seems to need a vendor binary in the repo, it needs a synthetic fixture instead. -
Never upload firmware anywhere — no CI artefacts, no release assets, no issue attachments.
-
Conventional commit titles. The repository squash-merges, so the PR title becomes the commit on
mainand drives Release Please.feat:→ minor,fix:→ patch,feat!:/fix!:(or aBREAKING CHANGE:footer) → major, everything else → no release. See docs/RELEASING.md. Getting this wrong silently produces no release. This is enforced bytools/check_commit_msg.pyin two places — acommit-msghook and a CI check on the PR title. If you are an agent, write the message in the right form the first time;--titlewill tell you before you push:python3 tools/check_commit_msg.py --title "feat: ...". -
Warn the user before anything that can destroy their settings. Some changes are not recoverable by reflashing because they overwrite state the car owns rather than state we ship. Shipping a
USER_DATApayload is the current example: it replaces databases on the unit's user partition, which hold paired phones, navigation destinations and presets. Say so plainly, in those terms, and wait to be told it is acceptable. Never treat a person's "I don't care about my settings" as covering a different person's car.build_package.pyenforces this: a manifest withuser_datais refused unless it also setsaccept_data_loss: true, and the warning is printed either way. -
mainis protected. No direct pushes — work on a branch and open a PR. Deletion, force-push and non-linear history are blocked. -
A release PR needs a human approval click. That is expected — do not automate it. Release Please opens its PR with the default
GITHUB_TOKEN, and GitHub will not run workflows on a PR created that way until someone approves them, so the release PR sits atBLOCKEDwith no checks reported and every run showingaction_required. That is the designed behaviour, not a fault, and it is deliberately left to a person: approving a release is a decision, not a chore.If you are an agent, do not work around it — do not call the run-approval API, do not add a PAT, do not weaken the ruleset. Report that the release PR is waiting on a human and move on. The same applies to merging a release PR.
.venv/bin/python -m pytest tests -q # the suite, no firmware required
.venv/bin/python -m ruff check tools tests # lint (E9 + F)
.venv/bin/python tools/patch_studio.py # the GUI
.venv/bin/zensical serve # live docs previewpre-commit install wires the pre-commit, commit-msg and pre-push hooks in one
go. The fast checks (lint, hygiene, no-firmware) run per commit; the slow ones (tests,
strict docs build, bandit) run on push, so a push that would go red in CI fails locally
first.
.venv/ is gitignored, so a fresh clone has none — build it first (Python 3.13, because
Homebrew's python3 is 3.14, where ensurepip is broken and PySide6 has no wheels):
uv venv --seed --python 3.13 .venv && uv pip install --python .venv/bin/python PySide6 pytest zensical ruff.
tests/helpers.py builds a synthetic package from scratch — header + zlib container,
.inf, smeg.inf, module and root manifests — so the whole patch/repack path is
exercisable without any vendor file. Use it. Adding a fixture is cheap; adding a binary
is not allowed.
Building those tests immediately caught two fixture bugs, so it is worth the effort.
| area | notes |
|---|---|
tools/patch_smeg.py |
Checks expect bytes before writing, then rebuilds the whole CRC cascade. Prefer adding a patches/*.json entry over new code. |
tools/ringtones.py |
Needs ffmpeg for non-WAV input, but degrades gracefully. Slot formats matter: ring/status tones are 16-bit mono 44.1 kHz, wait tones 16-bit stereo 8 kHz. |
tools/patch_studio.py |
Qt GUI. Set QT_QPA_PLATFORM=offscreen to test it headlessly. |
tools/elfsyms.py |
The package's *.out updater binaries are unstripped PowerPC ELFs. Before reverse-engineering anything in the flash chain, check whether it already has a name. |
tools/ppcemu.py |
Executes one function at a time on an emulated PowerPC core (Unicorn). Reachability is proof; stub return values are assumptions. Prefer it over reasoning about a branch by eye — it has already overturned one conclusion. |
tools/ppcdis.py, xref.py, callers.py, mkelf.py |
The analysis tools every patch address was derived with. Need capstone. Untested — see #38. |
docs/ |
Published with Zensical to https://smeg.kroper.uk/. A broken anchor fails the build; run zensical build before pushing docs. |
- The application image is not encrypted:
f_BigQuick.binis a 0x800-byte header, a0x08marker at 0x800, then a zlib stream from 0x801, inflating to a raw PPC image at0x01000000. The shippedabs_symbols_base.txt.gzlines up with it exactly, so patch by symbol, not by pattern. - Addresses are per build.
AUDIO_BTandAUDIO_BT_256usually match each other; the NAV build is offset. Never copy an address between builds without checking. - Addresses are also per firmware version. The NAV image from
SMEG_5.42.B.R4is the 5.43 one displaced by 152 bytes, so every address inpatches/*.jsonis wrong on it — and the AUX handler differs by more than the shift. Every variant declares the version it came from ("firmware": "5.43.A.R2") andpatch_smeg.pyrefuses any other image; theexpectbytes alone are not a sufficient guard (two entries match at the same address on 5.42). Do not defeat either check. See docs/PATCHES.md. - The media partition is a gzip'd tar, and
system_ctrl.binholds a per-file CRC for everything inside it.SIZE/SIZE_ninsystem.bin.infare computable —SIZEis the sum of the file sizes in the tar,SIZE_nthe same rounded up per file to n KiB. - Version strings are not a safe marker. Display reads
Data_base/smeg.infinside the media partition, so patching the app image changes nothing visible. Editingmedia.infcan block the update outright.GUI_VERis the only safe visible field. - The updater reboots the unit during the BootROM and Renesas steps. Never propose updating while driving.
- A modified package must be re-sealed before flashing, or the unit rejects it with
string 2099. Always run
tools/patch_contract.pyafter any change that alters a file the contract covers. Seedocs/MEDIA_PROTECTION.md. - The contract's RSA key material lives in the firmware image and must never be
committed or reproduced in docs.
patch_contract.pyextracts it from the user's own package at runtime; keep it that way.
- Prefer data-driven changes: a new
patches/*.jsonbeats new Python. - The patches are not validated on hardware by the maintainer. Say so plainly; do not claim a patch "works". Report what was verified statically and what needs a car test.
- Add a regression test for any bug fixed, and a synthetic fixture for any new file format.
- Keep docs current in the same PR — the user-facing pages are the product here.
- README.md — overview and tool table
- CONTRIBUTING.md — the human-facing version of the rules above
- docs/ARCHITECTURE.md — how the whole firmware fits together
- docs/FLASH_CHAIN.md — the boot and update chain
- docs/PATCHES.md — exact addresses and bytes
- docs/AUX_CHAIN.md — the AUX auto-switch gate by gate, and which claims are executed rather than read