Flashing notes¶
Re-seal the package before flashing
The unit validates the media against a signed contract, and will reject a patched
package with "The update file is protected and cannot be copied." (string 2099)
unless the contract is regenerated. build_package.py does it for you, in the right
order. By hand, the patch step must write a full package (--copy-package), or the
re-seal finds no contract.dat:
uv run tools/build_package.py --manifest builds/<scheme>.json # recommended
# or, by hand:
uv run tools/patch_smeg.py --src SMEG_PLUS_UPG --out SMEG_PLUS_UPG_mod --copy-package
uv run tools/patch_contract.py --package SMEG_PLUS_UPG_mod
A stock package needs no such step and can be flashed as-is (including a rollback). See Media protection.
These are generic notes for applying a patched SMEG+ package. They are not a substitute for the update instructions that came with your vehicle/software. Do this at your own risk.
Shipping settings: the USER_DATA payload, and why it does not land¶
A package can carry a USER_DATA payload — NAV/USER_DATA/user_data/sqlite/… — which the updater
copies over the unit's live settings partition. It does not work on this unit, for these
reasons:
- The copy is
xcopy_blk("/bd0/SMEG_PLUS_UPG/NAV/USER_DATA", "/USER_DATA"), hard-coded, in the block that continues Phase 1 ofUpgradeTask. - The destination directory is named from what the updater reads off the stick, and FAT gives
out uppercase 8.3 short names. The application reads its live settings from lowercase
/USER_DATA/user_data/sqlite/, so the payload lands in an uppercaseSQLITE/sibling and is never opened.
Observed on a car, with the log to prove it — see
the second flash. A flash with a
correctly-laid-out payload ran the copy, and the unit's own settings dump afterwards still read
the factory supervisor.Last_Source.
Two other things the same log and updater binary settled:
- The live directory holds a
.infsidecar beside every database (up_common.sqlite.inf).C_UPGRADE::ManageSQLiteFilessaysWe have to generate the .inf file!; the generated file found on the stick is exactlyCRC32: <signed decimal>\r\n, and its value matches the edited database.build_package.pywrites that pair up front and pre-flight rejects a missing or stale sidecar. C_UPGRADE::RestoreDataFromUSBcopies from a per-unit directory,/bd0/<unit-id>/, not from the package path.C_UPGRADE::SaveDataOnUSBis what creates it, and it did not run.
Working around it: give the directory a long-filename entry¶
The updater names each destination directory after what it reads off the stick, and it reads
long filenames but not the FAT "lowercase base" bit. So the fix is to make the payload's
sqlite directory carry a long-filename entry.
macOS will not write one for an 8.3-valid name like sqlite — it stores the short name
SQLITE with that bit set and no LFN, which is precisely how this goes wrong. Renaming does
not help; it does the same thing. The entry has to be written or corrected directly:
- Name the directory something that needs an LFN, e.g.
sqlite_dat, so macOS writes one. - Unmount the volume (
diskutil unmount, not eject — eject removes the device node), then rewrite that one LFN entry's characters tosqlite. The short name and its entry checksum are untouched, and it is a single 32-byte read-modify-write:
diskutil unmount /Volumes/SMEGUPDATE
sudo python3 tools/fix_userdata_case.py --dry-run /dev/rdisk4s1
sudo python3 tools/fix_userdata_case.py /dev/rdisk4s1
diskutil mount /dev/disk4s1
- Before flashing, unmount once more and run the read-only check against the FAT volume:
It must print OK: FAT long-filename entry is exactly 'sqlite'. Finder displaying lowercase
is not proof: the checker rejects short-name SQLITE even when its FAT lowercase-display bit
is set, because that is the exact representation the updater mishandles.
Not shown to work on hardware
The filesystem half is verified: the directory reads back as sqlite, and the .inf is
present. A flash laid out this way still did not change the setting — see
the later flash report.
Prefer an application-image patch (boot to AUX is one) or a media-partition edit.
Prepare the USB stick¶
- Use a stick of 8 GB or more (a package with navigation TTS data is roughly 1 GB).
- Format it FAT32 (
MS-DOS (FAT)), partition scheme Master Boot Record. - macOS: Disk Utility → select the device (not the volume) → Erase → Format "MS-DOS (FAT)", Scheme "Master Boot Record".
- Copy the package folder to the root of the stick, so you end up with:
/Volumes/USB/SMEG_PLUS_UPG/ctrl.bin
/Volumes/USB/SMEG_PLUS_UPG/contract.dat
/Volumes/USB/SMEG_PLUS_UPG/NAV_ctrl.bin
/Volumes/USB/SMEG_PLUS_UPG/upgrade.out
/Volumes/USB/SMEG_PLUS_UPG/NAV/…
/Volumes/USB/SMEG_PLUS_UPG/AUDIO_BT/…
/Volumes/USB/SMEG_PLUS_UPG/BSP/…
…
The folder must be called SMEG_PLUS_UPG — especially with a USER_DATA payload
The ordinary update is found by scanning, but the settings payload is not.
C_UPGRADE::UpgradeTask checks one literal path —
IsDirExist("/bd0/SMEG_PLUS_UPG/NAV/USER_DATA") — and only then runs
"Copy of /USERDATA from /bd0 to NAND". Both the folder name and the module are
hard-coded in that string.
So a build written to, say, SMEG_PLUS_UPG_auxdefault still flashes normally and its
payload is skipped without a word, which looks exactly like the setting having had no
effect. tools/preflight.py fails on this, and build_package.py says so at build time.
Let the tool do the copy¶
python3 tools/prepare_usb.py --package out/SMEG_PLUS_UPG --target /Volumes/USB
python3 tools/prepare_usb.py --package out/SMEG_PLUS_UPG --target /Volumes/USB --dry-run
python3 tools/prepare_usb.py --package out/SMEG_PLUS_UPG --target /Volumes/USB --eject
It checks the layout, the target and the free space before copying anything, says which target it is about to write to first, then copies and re-reads every file back off the stick and compares checksums.
That last step is the one that matters: a stick pulled mid-copy, or a copy tool that leaves
AppleDouble ._* files beside the package, produces an update failure in the car that looks
like a firmware fault. ._* and .DS_Store count as a failure, not
a warning: the updater does not expect them, so the exit code is non-zero and nothing is
left to judgement.
A data-only copy does not stop macOS writing ._* files. The kernel tags every newly created
file with a com.apple.provenance attribute, and FAT can only store that as a ._* shadow
beside it. After copying, the tool deletes the shadow of each file and directory it wrote, and
nothing else. Any other litter inside the package still fails the check.
macOS also creates .Spotlight-V100 and .fseventsd at the stick's root when it mounts it,
and Spotlight indexing a freshly written stick keeps the volume busy, so diskutil eject can
fail until retried. The folders are outside SMEG_PLUS_UPG, which is all the updater reads.
On macOS prepare_usb therefore:
- writes
.metadata_never_indexat the root and runsmdutil -i off(a refusal is reported, not fatal); - removes those two folders after the copy is verified;
- with
--eject, ejects the stick, retrying with a back-off while it is busy.
--keep-index skips all of this. Without --eject, eject with diskutil eject before
unplugging.
It only ever writes inside --target, and refuses to copy a package into itself.
It also refuses to write into a package that is already there. Copying into a directory
that already holds one silently merges the two, and the result still passes every checksum
its own manifests declare — so nothing downstream notices, and the stick ends up flashing
something nobody built. Remove the old copy first, or point --target at a clean one.
--force continues past a filesystem complaint — a non-FAT32 or non-MBR target is refused by
default, with the Disk Utility steps below. The probing is macOS-only and deliberately
conservative: when it cannot tell, it says so rather than guessing, because refusing a
perfectly good stick is worse than not checking.
Before you go to the car¶
Audit the package first. It is the cheapest check available and catches the failure that is most expensive to find in the car — a record that disagrees with the file it describes, which the unit treats as a bad flash:
It checks every *.inf sidecar against the file beside it, smeg.inf against the module
image, each <MODULE>_ctrl.bin against its module's files, and ctrl.bin against each module
manifest. Exit is non-zero if anything disagrees; --json is there for scripting.
It also warns (without failing) about a stock-shaped *ctrl.bin whose trailing CRC32 is
wrong, which packages built by older versions of the tools carry. Units have accepted them,
which suggests the trailer is not checked (inferred), but rebuilding makes the manifests
match stock. preflight.py reports the same warning.
It looks for each CRC as a value in the manifest rather than parsing the record layout (documented in Boot & update chain), so it stays correct if a manifest's layout ever differs.
Keeping a rollback package¶
Keep an untouched copy of the original package before you flash anything, and audit it too —
a rollback you have not checked is not a rollback. Re-flash it the same way as any other
package; the original application content differs from the patched one, so it is rewritten.
Copying it to the stick with tools/prepare_usb.py verifies the copy on the way.
Confirm the patched image is present and the checksums agree, e.g. for a NAV unit:
python3 - <<'PY'
import zlib
print(hex(zlib.crc32(open('SMEG_PLUS_UPG_mod/NAV/AppBin/f_BigQuick.bin','rb').read())))
PY
cat SMEG_PLUS_UPG_mod/NAV/AppBin/f_BigQuick.bin.inf
grep BIGQUICK SMEG_PLUS_UPG_mod/NAV/smeg.inf
The .inf value and smeg.inf's BIGQUICK_CRC32 must both equal the CRC printed above.
In the car¶
- Parked, ignition on, engine running. The update takes over 20 minutes and reboots the unit several times; never start one while driving, and the unit must not lose power.
- Insert the stick into the vehicle USB port and let the unit detect the update; follow the on-screen prompts.
- The updater is incremental: an already-current unit will report the boot ROM as done and skip the Renesas MCU, and will rewrite the application when its content differs.
- Do not remove the stick or cut power until it reboots.
- When the normal UI is back, remove the stick. While
SMEG_PLUS_UPG/UpgPlugin.outis on a mounted stick, the unit offers the update again at every start (read; see The update flow).
What you will see¶
The full run takes over 20 minutes, and the unit reboots several times on the way. The screens alternate between the normal touchscreen UI and a blue bootloader screen with yellow monospace text.
UPDATE LEVEL (touchscreen) media detected
Identification of media...
Checking compatibility...
Software update. (touchscreen) same version number in both lines is normal
From version: CD 26482
To version number: CD 26482
Keep the engine running.
The system will restart.
Continue? Yes / No
|
v
PEUGEOT (splash) reboot
|
v
Renesas Upd... (bootloader) front-panel MCU
AppBin Upgrade... (bootloader) <-- the application image is written here
AppBin flashing
|
v
Phase 0 formatting /SYSTEM (bootloader)
Phase 0 defragmenting /USER-DATA/BACKUP
Phase 3 Uncompress /SYSTEM
Phase 3 Check the result of uncompression of /SYSTEM/
Check progression : 6%
Phase 5 Uncompress /SD_DIR -> /SD_DIR_TTS
Phase 6 Management of UserGuide
Phase 6 Management of ZA files
"The product must reboot in 2 s" <-- the updater reboots itself here
|
v
PEUGEOT -> normal UI (splash) done
The Phase 0 / 3 / 5 numbering and the Uncompress lines are the updater working
through the media partition; the free-space figures on those screens change as partitions
are rewritten. Reboots are expected after the BootROM, U-Boot, Renesas and application
steps — see Boot and update chain.
The bootloader screens carry a header identifying the media being flashed. Check it
matches your package — Upgrade version comes from SUBVER in upgrade.out.inf and
Media version from VER in media.inf:
Media version : 26482
Upgrade version : 5.3.3
BootRom version : BSP-215.6.PLUSINT May 26 2017, 13:23:13
UBoot version : 06.03 Apr 22 2013 - 15:57:07
Hardware ID : 155
Hardware diversity : NAV
Renesas version : 05.e3.01
If you see the protected-file error
"The update file is protected and cannot be copied." (string 2099) means the media does not match the signed contract — the re-seal step was skipped, or was run against the wrong directory. The unit will refuse the update. Rebuild and re-seal; nothing is written.
Verify¶
The version strings do not change when re-flashing the same release, so verify by
behaviour. System Information will still read the same SMEG5.43.A.R2 / CD 26482 after
a successful patched flash — see Version strings.
What to check depends on the build. After each, capture (step 5 of the test loop):
aux-autoswitch/aux-always-available: the AUX tile stays selectable with nothing plugged in (on stock it greys out), and SRC steps through to AUX. The unit does not switch to AUX by itself: the handler follows the AUX setting, not the signal.- Boot to AUX (
builds/aux-boot.json): the unit starts on AUX at every boot, whatever source was playing before. A capture shows AUX's request with PrOnly false and AUX acknowledged at once. - Ring tone names (
media.names): the phone's ringtone menu lists the new name. aux-signal-switch(candidate, emulated only): on FM, start playback into AUX — does it switch? See The AUX signal path.
The test loop, end to end¶
- Build.
uv run tools/build_package.py --manifest builds/<scheme>.json— it patches, rebuilds the media partition and the checksum cascade, re-seals, and runs preflight. - Stick.
uv run tools/prepare_usb.py --package <out>/SMEG_PLUS_UPG --target /Volumes/<stick> --eject. - Car. Parked, engine running; accept the update; do not remove the stick until the normal UI is back. Then remove it — it re-offers the update while present.
- Observe the behaviour under test, over at least two restarts.
- Capture. Dial
SPYTAKE: the unit collects every trace buffer, then reboots. Then, with a stick in, dialSPYSTORE, which copiesSPY/<stamp>/to it. If the package is still on that stick, decline the update offer. -
Read.
tools/spy_read.pyreads the archive in memory and redacts the VIN, device addresses and long numbers by default:uv run tools/spy_read.py /Volumes/<stick>/SPY/<stamp> # how the boot source was chosen uv run tools/spy_read.py /Volumes/<stick>/SPY/<stamp> --aux # AUX lines from 25300, 06301, 15400 uv run tools/spy_read.py /Volumes/<stick>/SPY/<stamp> --list # every buffer uv run tools/spy_read.py /Volumes/<stick>/SPY/<stamp> --show 25300The boot report lists each source request with its PrOnly flag, the ScheduledInit table, and which source was acknowledged first. When that is the tuner about 7500 ms after
Last_Source, it calls it the init timer's fallback, which is inferred from the timing. Redaction is best-effort: it catches VIN-shaped tokens, VIN byte runs, device addresses and phone-length numbers, but not a phone's name.By hand:
mkdir cap && tar -xzf /Volumes/<stick>/SPY/<stamp>/TAR/*-USER.tar.gz -C cap less cap/RAMDISK_SPY/25300/*.bin # C_MGR_SRC: requests, Last_Source, the restore table less cap/RAMDISK_SPY/06301/*.bin # the media app: AUX activation, PrOnly less cap/RAMDISK_SPY/15400/*.bin # the audio module (the id is inferred)The buffers are plain text, one
<ms since boot>::<event>per line.traces.binbeside the archive is only the VxWorks exception log. If no restore matched at boot, FM wins on the 7.5 s init timer: in25300that shows as a tuner acknowledgement about 7500 ms afterLast_Sourceis read. See The AUX chain.
A capture is personal data
A spy capture and a settings dump hold the car's VIN, paired phones and other personal settings. Keep them on your own machine: never commit one, and never attach one to a public issue.
Rollback¶
Flash the untouched original package the same way as any other (see Keeping a rollback package). If something has already gone wrong, see Recovery — including what is not documented, which is worth reading before you need it.
For a plain rollback, flash the untouched original package. --stock is a canary for the
packaging path, not a substitute for the original:
uv run tools/patch_smeg.py --src ORIGINAL_PKG --out out/SMEG_PLUS_UPG --copy-package --stock
uv run tools/patch_contract.py --package out/SMEG_PLUS_UPG
--stock applies no patches but does everything else — re-packs, rebuilds the checksum
cascade and verifies it end to end — and patch_contract.py then re-seals it, which a
re-compressed image needs (its size changes). If a re-sealed stock package is refused by the
unit, the fault is in the packaging or sealing rather than in any patch. (These two commands
were run on the stock NAV package: the contract re-sealed and decrypted cleanly, and preflight
reported no problems.)