The AUX chain¶
How the unit chooses AUX: at boot, where the C_MGR_SRC source scheduler restores a saved
source, and afterwards, where the media app's AUX handler asks for it. Every address is in the
NAV SMEG5.43.A.R2 image; executed means run under the emulator or
observed on the car, as marked.
Current state
- Boot to AUX works on hardware.
aux-autoswitch+aux-boot-default+ the three-editaux-boot-restoreboot the unit to AUX; neither boot set works alone.builds/aux-boot.jsonis the minimal build. The car evidence is summarised below and logged in Hardware verification. - Stock firmware has no switch-on-signal. The AUX handler reacts to the saved AUX input
setting, not to audio arriving; see
What the handler actually reacts to.
aux-signal-switchadds one, is verified under emulation only, has not been flashed, and is not being pursued; see The AUX signal path.
How the boot source is actually chosen¶
Read from the NAV image; the runtime evidence is the user spy archives listed in Evidence (2026-09-14 is an unpatched boot). Each step carries its tier.
Init runs before StartUp. Inferred from the code. Both are C_MGR_SRC virtuals, at
vtable slots 0x0307aa6c and 0x0307aa74. Init creates the lock at +0x80, and StartUp
takes that lock before it restores anything. Init also writes +0xb4 = 1 and +0xac = 1
unconditionally (0x016995b0–0x016995b8), so whatever StartUp restores lands on top.
StartUp restores two keys, logs one, then starts a timer. Read.
- It reads
Last_Source_Priorityinto+0xac(0x01699448), andLast_Sourceinto+0xb4(0x01699490), mirroring it to the global0x035e4cd0and into+0xe4. Neither value is validated. The key names are the strings at0x0300a578and0x0300a584. - It logs
Last_Sourcewith the saved value (lwz r5,8(r1)passed toWriteMgrSrcSpyat0x01699488) before the store at0x0169948c, which is the siteaux-boot-defaultreplaces. So the spy lineLast_Source : nalways shows what was saved, never the patched value. - It starts a 7.5 s timer (
0x1d4cms). Its callback,Mgr_src_SCHED_INIT_TIMEOUT, reachesSchedulerInitTimeoutthrough a private message (HandlePrivateMessage+0x1bc).
Until that timer fires, the first allocation pass ignores +0xb4. Read.
ExecuteAllocationFirstRound (0x016957ec) branches on a mode word at 0x035e4cd4, which
SetCurrentStatus (0x01695424) sets to 1 in the normal case (not locked, +0x7c clear). In
mode 1 the +0xb4 match (0x01695b84, reading at 0x01695b90) runs only when the byte at
this+0x3c0 is non-zero. That byte is 0 from the constructor and Init; SchedulerInitTimeout
sets it, and four sites in AddRequest set or clear it. Allocation is reached only through
ExecuteAllocation, from request and scheduler paths; StartUp never enters it.
A saved source is restored only through the ScheduledInit path in AddRequest. Read —
disassembly, confirmed against Ghidra's decompilation of AddRequest (0x0169815c). A type-5
or type-6 request is first added to the list at +0xd4 (jump table at 0x02f4b080), the list
the first pass matches against. Then, only if the request's byte at +0x28 (PrOnly) is
clear (0x0169846c–0x01698474):
the request's (Sched_Pos, priority) pair… |
effect |
|---|---|
is already in the ScheduledInit table at +0x370 (IsInitialized) |
cancels the init timer, sets the flag, ForceSchedulerPosition(Sched_Pos) — a source that asks again after boot is switched to straight away |
| is new | SetScheduledInit adds it to the table. If it also equals (+0xb4, +0xac) — i.e. (Last_Source, Last_Source_Priority) — the init timer is cancelled and the flag set. Then ExecuteAllocation runs, and the first pass matches +0xb4: this is the restore |
A request with PrOnly set skips all of that and goes straight to ExecuteAllocation: it never
enters the table and can never be restored. Type-4 requests take a separate branch with the same
table-and-compare logic but are never added to a request list. The request's type is the field
at request +0x14, the spy line's Type= (format string at 0x0300a63c).
If nothing matches in time, the timer picks FM. Read. SchedulerInitTimeout calls
ChangeToNextSchedulerPosition(0, 0). If that finds an eligible request in the list at +0xd4,
it moves the old +0xb4 into +0xe4, stores a literal 1 into +0xb4
(0x01697b44–0x01697b48) and runs allocation. Position 1 is POS_TUNER. If it finds none, the
flag is cleared and a 5 s retry timer is started. The position it computes before storing 1 is
discarded, and would be the highest queued position, so patching the literal does not help; it
is also what C_SRV_AUDIO::bcm_ActivateNextSource calls, so the edit would change normal source
cycling. Releasing the current source already tries the previous position first. Both are
in The source scheduler.
AUX's stock request has PrOnly set, so AUX never enters the table. Executed — the
2026-09-14 spy request-list dump, whose last column is the byte at +0x28:
Nb|src|id|Status|Norm|NoSr|Lock|Type|sche|Post|Susp|PrOnly|
1|0xa |0xbc00 |WAITING| 10| 254| 254| 5| 1| 1| 1| 0| false|
2|0x3 |0x17400 |WAITING| 20| 254| 254| 5| 9| 0| 1| 1| false|
5|0x3 |0xe200 |ACKNOWL| 20| 255| 255| 5| 7| 0| 1| 1| true|
The rows are the tuner (0xbc00, position 1), USB (0x17400, position 9) and AUX (0xe200,
position 7). AUX's priority (Norm) is 20, and AUX is the only request with PrOnly true.
Every source whose request has PrOnly false has a ScheduledInit row (columns
PNormal | SchedPos), and AUX has none:
m_Mgr_src_ScheduledInit[0] -> 10 | 1 |POS_TUNER
m_Mgr_src_ScheduledInit[1] -> 20 | 9 |POS_USB
m_Mgr_src_ScheduledInit[2] -> 10 | 10 |POS_IPOD
m_Mgr_src_ScheduledInit[3] -> 10 | 8 |POS_BT
m_Mgr_src_ScheduledInit[4] -> 10 | 4 |POS_CDC
On that unpatched boot (executed, ms since boot) the tuner was granted at 10221 ms, about
3.7 s after Last_Source (1) was read — inside the 7.5 s window, so by the restore, not the
timer. AUX's request waited until AUX was selected by hand at 25789 ms:
6511::Last_Source : 1 (0x1)
9727::AllocateSource : MsgSrc = 10, SrcId= 0xbc00,Type=5, Sched_Pos= 1, ...
10062::AllocateSource : MsgSrc = 3, SrcId= 0xe200,Type=5, Sched_Pos= 7, ...
10062::SendWAIT [MsgSrc + Source_ID] : 3, 0xe200
10221::SendACK [MsgSrc + Source_ID] : 10, 0xbc00
25789::ActivateSourceID::p_source_ID : 57856 (0xe200)
The settings dump taken after that selection holds Last_Source = 7 and
Last_Source_Priority = 20, exactly AUX's pair. So stock firmware cannot resume AUX after
you switch off on AUX: the saved values are right, but AUX's request is excluded by PrOnly
before the comparison. Inferred from the path and the executed values.
Where PrOnly comes from. Read — Ghidra decompilation and disassembly. The HMI's
C_HMI_SrcMgntBase keeps its request at this+0x10, so the PrOnly byte is this+0x38.
ActivateSource(bool) writes its argument there for the one AllocateSource it sends, then puts
the old value back. Two AUX sites pass true:
C_HMI_MEDIA_APP_BASE::InitApp(li r4,1at0x022c0678) sends AUX's boot request. It is the only one ofInitApp's six activations that passestrue, identified by the label it opens with,user_HMI.AUX.Equalizer_aux. The order of the boot requests (USB, iPod, BT, CDC, AUX) isInitApp's order.HandleAudioAuxInputStatusChnged(li r4,1at0x02303474) sends it when the AUX input setting becomes non-zero (below).
The video app passes true as well (HandleMediaStateReady, HandleVideoTrackFound). Inside
C_MGR_SRC, the byte is read only by AddRequest's restore gate and by AllocateSource's spy
line. Every call site and its argument is in How HMI apps request sources.
The boot-to-AUX patches¶
Three patch sets, all needed; builds/aux-boot.json applies exactly these. The addresses
below are NAV's; all three sets also have AUDIO_BT/AUDIO_BT_256 variants, derived and
emulated but never flashed (Patches).
| set | site | original | patched | effect |
|---|---|---|---|---|
aux-autoswitch |
0x02247858 IsAUXSRCAvailable |
9421ffa0 |
li r3,1; blr |
AUX stays a valid, selectable source (confirmed on hardware) |
aux-autoswitch |
0x02303428 handler gate 3 |
419e014c |
nop |
no effect: the gate never fires (executed, below); kept so the bytes match the confirmed build |
aux-boot-default |
0x0169948c in StartUp |
81210008 lwz r9,8(r1) |
39200007 li r9,7 |
the restore target +0xb4 is always POS_AUX (7), whatever was saved |
aux-boot-restore |
0x022c0678 in InitApp |
li r4,1 |
li r4,0 |
AUX's boot request is sent with PrOnly clear, so it enters the table and meets the restore |
aux-boot-restore |
0x02303474 in the handler |
li r4,1 |
li r4,0 |
the same for AUX's later requests (the once-per-boot re-announcement and setting changes); no other source changes |
aux-boot-restore |
0x01699444 in StartUp |
lwz r0,8(r1) |
li r0,20 |
the restored Last_Source_Priority is always AUX's 20 |
PrOnly is cleared at AUX's two call sites rather than by removing the gate in AddRequest
(nop at 0x01698474), which emulated identically but would also change the video sources.
Emulated (tools/ppcemu.py, NAV image; AddRequest run for real on a synthetic C_MGR_SRC
whose +0xb4/+0xac hold (7, 20) and whose init timer exists; memcpy, the node allocator,
wdCancel, ExecuteAllocation and ForceSchedulerPosition stubbed), fed AUX's request (type 5,
position 7, priority 20). The same runs are in tests/test_firmware_nav.py:
| request | ScheduledInit |
restore flag +0x3c0 |
init timer cancelled |
|---|---|---|---|
PrOnly set (stock AUX) |
empty | 0 | no |
PrOnly clear |
(7, 20) |
1 | yes |
PrOnly clear, but +0xac = 10 (priority edit absent) |
(7, 20) |
0 | no |
PrOnly clear, second request with (7, 20) already in the table |
— | — | ForceSchedulerPosition reached |
The handler edit (0x02303474) was executed in the aux-signal-switch handler runs: with it,
the activation passes PrOnly 0 (Emulation results).
Beyond boot, a second AUX request is forced to the front (IsInitialized →
ForceSchedulerPosition, emulated above). The handler sends one only when the AUX input
setting goes from zero to non-zero, so turning the AUX input on in the menu selects AUX
(inferred); audio arriving does not.
Evidence from the car¶
Each flash is logged in Hardware verification; the captures
were read with tools/spy_read.py.
What the first car test established¶
2026-09-27, aux-autoswitch + aux-boot-default: the unit booted to FM with audio playing
into AUX (observed). aux-boot-default is applied and correctly located (39200007 at
0x0169948c inside C_MGR_SRC::StartUp, 0x016990b8–0x016994a4), and AUX was visible and
selectable. So forcing the restore target alone is not enough: AUX's request never reaches the
comparison.
What the second car test established¶
2026-09-28, aux-boot-restore with the handler edit only: still FM. The capture (executed):
8847::Last_Source : 7 (0x7)
10109::AllocateSource : MsgSrc = 3, SrcId= 0xe200,Type=5, Sched_Pos= 7, ...
10438::AllocateSource : MsgSrc = 10, SrcId= 0xbc00,Type=5, Sched_Pos= 1, ...
16347::SendACK [MsgSrc + Source_ID] : 10, 0xbc00
3|0x3 |0xe200 |WAITING| 20| 255| 255| 5| 7| 0| 1| 1| true|
AUX's boot request still had PrOnly true and ScheduledInit had no position-7 row, because the
boot request comes from InitApp, not the handler. FM was chosen by the init timer: 8847 +
7500 = 16347 ms, the tuner's ACK. AUX's request arrived 1.3 s into the window, in time to match
had PrOnly been clear. (Last_Source : 7 is the saved value; that session had ended on AUX.)
What the third car test established¶
2026-09-28, the three-edit aux-boot-restore + aux-boot-default: booted to AUX three times
(observed). The capture of the last boot (executed):
6531 ms saved Last_Source = 1
10228 ms request SrcId 0xe200 pos 7 type 5 PrOnly false <- AUX
10228 ms SendACK SrcId 0xe200
ScheduledInit: POS_TUNER (1, 10), POS_USB (9, 20), POS_IPOD (10, 10), POS_BT (8, 10), POS_CDC (4, 10), POS_AUX (7, 20)
verdict: AUX was acknowledged first, at 10228 ms
AUX's boot request carries PrOnly false, enters ScheduledInit as (7, 20), and is acknowledged
in the same millisecond it asks, with no tuner ACK at the 7.5 s mark. The saved Last_Source was
1, so aux-boot-default's override is what the request matched. Why the saved value was 1 after
three AUX boots is not known; when C_MGR_SRC saves the source is described in
the scheduler.
The AUX input handler¶
The path from the audio server to ActivateSource for AUX:
flowchart TD
AS["audio server"] -->|"DBUS AUDIO_AUX_INPUT_STATUS_CHANGED"| CL["C_BCM_HMI_AUDIO_CLIENT"]
CL --> LA{"LINK A<br/>client->0x50 == NULL?"}:::unknown
LA -->|null| STOP["return — no listener"]
LA -->|set| P203["post message 203 (0xcb)<br/>@ 0x025cdefc"]
P203 -->|message 203| HDB["C_HMI_MEDIA_APP_BASE::HandleDBUSMessage<br/>@ 0x02309398 · case 0xcb"]
HDB --> H["HandleAudioAuxInputStatusChnged<br/>@ 0x0230331c"]
H --> G1{"gate 1 · app NULL?<br/>@ 0x02303358"}
G1 -->|null| R["shared return path"]
G1 --> G2{"gate 2 · setting unchanged?<br/>@ 0x023033d4"}
G2 -->|unchanged| R
G2 --> G3{"gate 3 · GetMediaDevice AUX failed?<br/>@ 0x02303428"}:::never
G3 -->|failed| R
G3 --> G4{"gate 4 · source mgr NULL?<br/>@ 0x02303468"}:::never
G4 -->|null| R
G4 --> ACT["ActivateSource srcMgr, true<br/>@ 0x02303484"]:::ok
classDef unknown fill:#fff3cd,stroke:#b8860b,stroke-width:2px;
classDef never fill:#e3f2fd,stroke:#1565c0;
classDef ok fill:#e6f4ea,stroke:#2e7d32,stroke-width:2px;
LINK A (amber) is the only link never observed; it depends on runtime state. Gates 3 and 4 (blue) never fire (executed). The same trace as a call listing:
audio server
--DBUS AUDIO_AUX_INPUT_STATUS_CHANGED--> C_BCM_HMI_AUDIO_CLIENT
if (client->0x50 == NULL) return; <-- LINK A: no listener
post internal message 203 (0xcb) @ 0x025cdefc
--message 203--> C_HMI_MEDIA_APP_BASE::HandleDBUSMessage @ 0x02309398
case 0xcb @ 0x02309638 -> 0x02309fbc
--direct call--> HandleAudioAuxInputStatusChnged @ 0x0230331c
gate 1 app = this->0x50df4, NULL? @ 0x02303358
gate 2 setting unchanged? @ 0x023033d4
gate 3 GetMediaDevice(AUX) failed? @ 0x02303428
gate 4 source manager NULL? @ 0x02303468
ActivateSource(srcMgr, true) @ 0x02303484
The handler is also registered in the HMI event table: an entry of size 0x2c with event id
0x613dc, the handler pointer, function size 0x290 and type 7 (read).
Link A — the listener¶
C_BCM_AUDIO_CLIENT::AUDIO_AUX_INPUT_STATUS_CHANGED (0x025c9e78, the input-setting
notification) posts message 203 only if something registered for it:
void C_BCM_AUDIO_CLIENT::AUDIO_AUX_INPUT_STATUS_CHANGED(client) { // 0x025c9e78
if (client->0x50 == NULL) return; // 0x025c9e9c
Post203(client->0x50); // 0x025cdefc
}
client->0x50 is written by one setter (0x025c97f0), called from two places, both of which
return early when app->0xc is NULL:
So the listener exists only if app->0xc does, and that same app->0xc is what the AUX
setting query dereferences (0x025cb280), returning -1 without touching its out-param when
it is null — a return value gate 2 discards. app is mediaApp->0x50df4, assigned at
0x022bc4e8 from a DBUS client factory (0x025fc3e8) called as Create(3, 0xc8, this); the
factory has several paths that return NULL, and needs a DBUS connection when the media app
initialises. Whether any of them is taken on a real unit is not known: it depends on runtime
state no static reading settles.
The four gates in the handler¶
Recovered by executing the function:
void HandleAudioAuxInputStatusChnged(this) { // 0x0230331c
app = this->0x50df4; // 0x022ad2c0 — a plain getter
if (app == NULL) return; // gate 1 @ 0x02303358
ctor(&obj); // 0x02368080 — zeroes obj entirely
Get_aux_status(app, &setting); // 0x025cb258 — RETURN VALUE DISCARDED
state = (setting != 0); // the saved AUX input setting
if (state == this->0x51449) return; // gate 2 @ 0x023033d4
this->0x51449 = state;
if (GetMediaDevice(mgr, AUX, &obj)) return; // gate 3 @ 0x02303428
SetMediaDeviceState(mgr, AUX, 2); // 0x022f388c
if (obj[0x10] == NULL) return; // gate 4 @ 0x02303468
ActivateSource(obj[0x10], true); // 0x0273a248
}
| gate | tests | status |
|---|---|---|
| 1 | the audio client exists | not known — link A |
| 2 | the saved AUX input setting changed between zero and non-zero | a real gate — acts only on a transition |
| 3 | GetMediaDevice(AUX) succeeded |
never fires — executed |
| 4 | the device carries a source manager | never fires once gate 3 passes — executed |
Gate 2 is a change detector on the AUX input setting, cached at this+0x51449. A setting
already non-zero when the state is first recorded produces no activation. Because the query's
return value is thrown away, a failed query reads as "setting 0": a broken link A and an AUX
input switched off in the menu are indistinguishable here. When the setting goes to zero, the
handler releases the source.
Gate 3 never fires. The AUX media device is registered unconditionally at start-up, so
GetMediaDevice(AUX) succeeds. Executed: running the registration function fills the table
with types {0, 1, 2, 3, 5}. Gate 4 could not be bypassed by removing gate 3 either:
GetMediaDevice writes nothing to its out-param when it fails, so the field gate 4 tests stays
the zero the constructor left. The truth table is in Emulating the firmware.
Do not nop gate 4
0x0230346c loads that field straight into r3 as ActivateSource's this. Removing
the guard calls a C++ method on a null pointer, on the HMI thread.
Every exit path reaches the handler's own log call on the shared return path (executed), but
this build's Log_msg sink is stubbed; the spy collect is the way to observe it
(Cheatcodes).
What the handler actually reacts to¶
The handler follows the saved AUX input setting, not audio arriving on the input. Read from the disassembly:
C_MODULE_AUDIO::Get_aux_status(0x013b9c8c) returnslwz r0,0x8c(r31)whenever the module's lifecycle state+0x74is non-zero.- The only writers of that field on the module object are
setAUXGain(0x013bba28,stw r30,0x8c(r29), the setting it was given) andread_sqlite_AudioUserData(0x013caba8), which loads the user keyAuxiliary_Status; the string is in the image. - So
+0x8cis the AUX input setting from the media options menu, 0..3. Signal presence is a different call,Get_AUX_signal_status, which reads the radio front-end (The AUX signal path).
Which notification reaches the handler, read: C_BCM_HMI_AUDIO_CLIENT's
AUDIO_AUX_INPUT_STATUS_CHANGED posts HMI message 0xcb (li r4,0xcb at 0x025cdf1c), and
AUDIO_AUX_SIGNAL_STATUS_CHANGED posts 0xcc (li r4,0xcc at 0x025cded0). The media app
dispatches the handler on 0xcb and has no 0xcc case; the audio app uses 0xcc only to refresh
its menu.
When the setting notification is raised, read (see The audio module):
- when the setting is written from the menu (
Set_aux_status→setAUXGain); - once per boot, at the end of
ElabRADIO_READY_FOR_INIT_0, which callssetAUXGain(+0x8c, 1). Whether the media app is listening by then is not known.
What follows:
- Stock firmware has no path that switches to AUX because a signal appeared. For the media
dispatch this is executed: the stock window sends
0xccto its default case (Emulation results). For the whole image it is inferred from the routing, since other paths are not exhaustively excluded. AUX greying out and re-enabling with the signal on the car isIsAUXSRCAvailable()in the audio app, a separate path. aux-autoswitchdoes not produce an automatic switch. Its first edit,IsAUXSRCAvailable(), keeps AUX selectable (confirmed on hardware); its second removes gate 3, which never fires, and the handler would act on a setting change in any case.- A signal-triggered switch needs new wiring: route
0xccinto the media app's AUX handler and make the handler testGet_AUX_signal_status. That ispatches/aux-signal-switch.json, executed under emulation, not flashed; the detection path, its constraints and the emulation results are in The AUX signal path. aux-boot-restoredoes not depend on this handler at boot. Its boot edit is inInitApp; its handler edit covers the once-per-boot re-announcement and later setting changes.
The media device table¶
GetMediaDevice and its neighbours operate on a fixed array hanging off the media app at
this+0x50e60:
Each 32-byte record holds its type at +0x00 and its source manager at +0x10:
| function | address | behaviour |
|---|---|---|
FindDevice(mgr, type) |
0x022f3608 |
linear scan of the first mgr[0xc0] slots; returns the record or NULL |
GetMediaDevice(mgr, type, out) |
0x022f3750 |
FindDevice, then field-by-field copy into out; returns 0, or -1 writing nothing |
AddDevice(mgr, src) |
0x022f3ad0 |
append; refuses when mgr[0xc0] > 5 |
Registration runs once, from media-app init (0x022bf934 → 0x022b833c), and is
unconditional — every early-out branch in the caller rejoins before the call. It registers types
0, 1, 2, 3 and 5 outright, then type 4 only if the byte at this+0x51450 is set.
AUX is media device type 5. Every GetMediaDevice/SetMediaDeviceState call site that
passes type 5 — 0x02303404, 0x02303450, 0x02303564 — is inside the AUX handler, and no other
function uses it. Type 4, the only other candidate, is used by unrelated call sites and gated on a
byte written in exactly two places, both constructors, both storing 0, so it is never registered.
(Inferred from the call sites, strongly.)
Six source numberings, and which one Last_Source uses¶
The source enums overlap numerically; never carry a value from one into another.
| numbering | where it comes from | AUX is |
|---|---|---|
| media device type | the table GetMediaDevice searches |
5 |
| HMI source | OnEventSelect* → CreateNotificationCommand |
7 |
audio module SRC_* |
the name table at 0x02f9d60c, printed as Current_source |
5 |
C_MGR_SRC scheduler position (POS_*) |
AllocateSource's Sched_Pos field and the SPY dump's switch |
7 |
C_MGR_SRC request SrcId |
AllocateSource's SrcId field |
0xe200 (executed, 2026-09-14 spy archive); observed values are 0xba00, 0xbc00, 0xc000, 0xe200, 0x17400, 0x1e200, 0x1e600 |
| screen position | GetSourceAtPosition, 0-based |
4 |
The audio module's table is contiguous from SRC_NO_SOURCE = 0:
0 SRC_NO_SOURCE 1 SRC_TUNER 2 SRC_CD 3 SRC_MP3 4 SRC_CDC
5 SRC_AUX 6 SRC_PHONE 7 SRC_TTS 8 SRC_TA_PTY 9 SRC_TTS_ON_AUX
10 SRC_AUX_CONVERGENCE 11 SRC_BLUETOOTH 12 SRC_MTB 13 SRC_MLDIPO_RECO_PHONE
C_MGR_SRC's own position enum is read from the switch in its SPY dump (0x0169a2e4, on a
per-source field at +0x370), not from the order of the string literals, where POS_AUX sits
between POS_MP3 and POS_BT:
| value | name | value | name |
|---|---|---|---|
0 |
POS_NULL |
6 |
POS_VIDEO |
1 |
POS_TUNER |
7 |
POS_AUX |
2 |
POS_1CD |
8 |
POS_BT |
3 |
POS_MP3 |
9 |
POS_USB |
4 |
POS_CDC |
10 |
POS_IPOD |
5 |
POS_JBX |
0xff |
POS_NOT_SCHEDULED |
| other | POS_MGR_SCR_NOT_SCHEDULED |
Last_Source is a scheduler position. Executed: at boot the tuner was active with
Last_Source 1, request SrcId 0xbc00 and Sched_Pos 1:
6639::Last_Source : 1 (0x1)
9742::AllocateSource : MsgSrc=10, SrcId=0xbc00, Sched_Pos=1, ...
Current_source ... SRC_TUNER
So Last_Source follows the Sched_Pos/POS_* namespace, not the raw SrcId, and for AUX it is
7 — confirmed on hardware by aux-boot-default, which writes 7.
The key belongs to C_MGR_SRC, and the value must match a live source request¶
The key is read and written through the generic config loader in the core middleware — read at
0x01699390, written at 0x01695e2c from +0xb4. Its UP_Keys names (Last_Source,
Last_Source_Priority, Src_Radio_SchedPos, Src_Media_SchedPos, Src_Radio_Priority,
Src_Media_Priority) sit contiguously at 0x0300a578, immediately ahead of the class's own log
strings at 0x0300a6a4.
| address | what it does |
|---|---|
0x01699490 |
boot restore: the saved Last_Source is stored straight to +0xb4 and mirrored to 0x035e4cd0, with no validation |
0x016995b4 |
constructor: +0xb4 and 0x035e4cd0 are both initialised to 1 — the value the factory database ships |
0x0169c6ac |
reads 0x035e4cd0 and passes it as the scheduler-position argument to 0x016977d0 |
0x016977d0 |
the position setter — walks the request lists and writes +0xb4 only on a matching Sched_Pos; a miss leaves +0xb4 alone |
0x016957ec |
the scheduler — selects the source to restore by matching node+0x18 (Sched_Pos) against +0xb4 |
0x0169815c |
C_MGR_SRC::AddRequest — copies SrcId to node+0x04 and Sched_Pos to node+0x18 |
0x0169c51c |
the C_MGR_SRC message dispatcher (message id 0x62d5): case 0 → AddRequest, case 1 → RemoveRequest at 0x01697d60, case 2 → re-apply Last_Source |
0x0169bf74 |
C_MGR_SRC::AllocateSource — the client entry that packages a request and sends it into the dispatcher |
0x0169a2e4 |
the MGR_SRC SPY dump — switches on Sched_Pos to name each position (POS_*) |
0x016977d0 walks the first request list, anchored at this+0xd4 (next at +0x3c), comparing
its scheduler-position argument against each node's +0x18 (Sched_Pos), and writes +0xb4 on a
match. A position that matches nothing falls straight to the return path without touching
+0xb4:
016977dc lwz r9, 0xd4(r3) ; head of the first request list
016977e0 cmpwi cr7, r9, 0
016977e4 bne cr7, 0x16977f8
016977e8 b 0x1697854 ; empty list -> give up
016977ec lwz r9, 0x3c(r9) ; node = node->next
016977f0 cmpwi cr7, r9, 0
016977f4 beq cr7, 0x1697854 ; ran off the end -> give up
016977f8 lwz r0, 0x18(r9) ; node->Sched_Pos
016977fc cmpw cr7, r4, r0
01697800 bne cr7, 0x16977ec ; no match -> keep walking
01697804 ... ; matched: commit to +0xb4
So the setter does not validate the stored value: boot restore has already written +0xb4
unconditionally, and the message-2 path only re-affirms it from 0x035e4cd0. The value's meaning
comes from the scheduler at 0x016957ec, which picks the node whose +0x18 equals +0xb4:
iVar7 = DAT_035e4cd0; // the mirrored Last_Source
...
do {
if (*(int *)(iVar6 + 0x18) == *(int *)(param_1 + 0xb4)) { // node Sched_Pos == Last_Source
... remember this node as the source to restore ...
}
iVar6 = *(int *)(iVar6 + 0x3c);
} while (iVar6 != 0);
Last_Source must therefore equal the Sched_Pos a source request registered, or nothing is
restored. (Read from decompiled code; the runtime tuner tuple corroborates it.)
What a request node is, and who writes +0x18¶
AddRequest logs under the C_MGR_SRC / AddRequest strings (0x0300a6a4 / 0x0300a6b0) and is
reached from the class's message dispatcher, keyed on message id 0x62d5:
source module
-> send(object, 0x62d5, opcode, flags) common helper 0x014da78c
-> C_MGR_SRC dispatch FUN_0169c51c (0x62d5 matched at 0x1698e18)
case 0 -> FUN_0169815c AddRequest
case 1 -> FUN_01697d60 RemoveRequest
case 2 -> FUN_016977d0(this, DAT_035e4cd0, 0, 0)
The message body is a 0x6a-byte payload; the request is the 0x2c-byte struct at +0xc of it
(FUN_0169c71c is a plain memcpy). AddRequest copies it into a freshly allocated node, links
it and commits:
node = SUB_01695590(this, idx * 4 + this + 0xc4);
if (*(int *)(idx * 4 + this + 0xd4) == 0) *(int *)(idx * 4 + this + 0xd4) = node;
*(short *)(node + 0x00) = req.MsgSrc;
*(int *) (node + 0x04) = req.SrcId;
*(int *) (node + 0x14) = req.Type;
*(int *) (node + 0x18) = req.Sched_Pos; // the field the setter and scheduler compare
...
FUN_016977d0(this, req.Sched_Pos, 1, 1);
this+0xd4 is the first of four list heads (+0xd4, +0xd8, +0xdc, +0xe0) selected by a
mapping of the request Type; the nodes are pending requests, not a registry. The setter's
"must match a node" condition is satisfied trivially here, because AddRequest inserts the node
and then calls the setter with the same id. The request carries SrcId at request +0x04 (byte
0x10 of the payload) and Sched_Pos at request +0x18 (byte 0x24).
Requests reach the dispatcher through C_MGR_SRC::AllocateSource (0x0169bf74), which copies the
0x2c-byte request and sends it. It is reached from the client wrapper 0x016bc1dc (the function
starts there; 0x016bc1f0 is four instructions inside it), which resolves the service with
FUN_0103155c(0x62d4, 0). The ~30 other 0x62d5 sites are not source registration: eighteen at
0x014dc1a8–0x014dd358 use the helper 0x014da78c, which emits a 0x14-byte control body.
The client API is a fixed cluster: eight wrappers at 0x016bbec4–0x016bc2ac, each with one
caller; those callers are the public API at 0x016c09bc–0x016c0d3c, exported through a service
API table at 0x0307aedc–0x0307af00 and reached through the dynamic service registry
(FUN_0103155c(0x62d4, …), FUN_01001178()). The per-source callers of AllocateSource
cannot be found by xref; they need the registry resolved at run time. On the HMI side, every
ActivateSource call site is in How HMI apps request sources.
Everything in this subsection is read from decompiled code, not executed; the setter's shape is also confirmed by disassembly.
Setting Last_Source through USER_DATA¶
Shipping Last_Source in a USER_DATA payload is not how boot to AUX works, and it has not
worked on this unit: payloads with 4 and with 7 both left the live value at 1 (the later SPY
settings dump reads supervisor.Last_Source.0 <int> : 1, and the updater copied the payload into
an uppercase SQLITE directory) (executed). aux-default-retry.json carried a beacon,
clock.Time_Zone moved from 16 to 0; the time zone did not change, so that payload was skipped
too. The updater-side cause is written up in Hardware verification. The
confirmed route is the application patches above.
Tracing notes¶
- A symbol map is available. The SPYSTORE dump carries
abs_symbols_base.txt.gzand four companions — 98,365 symbols for this image. tools/callers.pyfinds directblcalls only (108,907 sites, 12,982 in-image targets). This firmware mostly calls throughlis/addi+mtctr/bctrl, and virtuals through vtables, so "0 callers" means "no direct callers".C_MGR_SRC::StartUphas 0 because it is virtual (one vtable pointer, at0x0307aa74);ExecuteAllocationFirstRound's single caller,ExecuteAllocation+0x4c, is found only by an address-materialisation scan. Usetools/survey.py(Firmware map).InitializeMetaNavat0x01000000also has 0 callers, correctly: the boot loader jumps to it.traces.binis a VxWorks exception log, not a source-manager trace: aFILE DATAheader, then tenEXCEPTION DATArecords of0x8e64bytes from firmwareSMEG5.2.A.R9, 2017–2020. The source manager's runtime lines are in the user spy archive,SPY/<stamp>/TAR/<stamp>-USER.tar.gz, underRAMDISK_SPY/(buffer25300), present only afterSPYTAKE(log_error.txt: "collect of spy asked by user"). Read it withtools/spy_read.py; see Cheatcodes.
Open questions¶
- Link A on a real unit — whether the media app's audio client ever fails to register. Boot to AUX does not depend on it.
HandleMediaStateReady(5, true)could send an AUX request withPrOnlyset outside the patched sites (mr r4,r0at0x02306a18). The boot-to-AUX capture shows a single AUX request, withPrOnlyfalse (executed); see How HMI apps request sources.- Why the saved
Last_Sourcewas 1 after three AUX boots. - Whether the detector measures AUX while another source plays — only relevant to
aux-signal-switch; see The AUX signal path.
Status of each claim¶
| claim | how it is known |
|---|---|
aux-autoswitch + aux-boot-default + three-edit aux-boot-restore boot the unit to AUX |
executed on hardware — three boots and the spy capture, 2026-09-28 |
aux-boot-default alone does not change the boot source |
executed on hardware — still FM, with audio playing into AUX, 2026-09-27 |
the handler-only aux-boot-restore does not either |
executed on hardware — still FM, 2026-09-28 |
AUX's boot request comes from InitApp, with PrOnly true |
executed (spy trace, 2026-09-28) and read (li r4,1 at 0x022c0678) |
| on that boot, FM was chosen by the 7.5 s init timer | executed — tuner ACK at 16347 ms, Last_Source read at 8847 ms |
the Last_Source spy line logs the saved value, before aux-boot-default's store |
read — 0x01699488 then 0x0169948c |
aux-boot-default applies and is correctly located in C_MGR_SRC::StartUp |
executed — 39200007 verified in the shipped image at 0x0169948c |
| the chain's addresses and dispatch | read from the image |
| gates 1–4 and their order | executed |
| gate 3 never fires; the registered types | executed |
| gate 4 blocks a nopped gate 3 | executed |
| every exit path logs | executed |
Log_msg is gated by a BSS mask |
executed |
| type 5 is AUX | inferred from call sites, strongly |
aux-sticky's second edit does what it says |
executed on all three builds |
| link A's state on a real unit | not known |
HandleAudioAuxInputStatusChnged reacts to the saved AUX input setting (C_MODULE_AUDIO+0x8c, Auxiliary_Status), not to signal presence |
read — Get_aux_status, its two writers, and the 0xcb/0xcc posts |
| stock firmware has no path that switches to AUX when a signal appears | executed for the media dispatch; inferred for the whole image |
aux-signal-switch routes 0xcc to the handler and makes it follow the signal |
executed under emulation (two functions); not flashed |
every boot re-announces the AUX setting once (ElabRADIO_READY_FOR_INIT_0 → setAUXGain) |
read |
HandleMediaStateReady can activate AUX with a computed PrOnly (0x02306a18) |
read; whether it runs for AUX is not known |
AddRequest writes SrcId to node+0x04 and Sched_Pos to node+0x18; the lists at +0xd4 are requests, not a registry |
read from disassembly/decompiled code |
the scheduler matches node+0x18 (Sched_Pos) against +0xb4 |
read statically; runtime tuner tuple corroborates it |
boot restore writes +0xb4 with no validation |
read from decompiled code |
POS_AUX = 7, POS_JBX = 5, POS_TUNER = 1 |
read from the SPY switch; POS_TUNER=1 executed in the captured AllocateSource tuple |
AllocateSource is the client entry that feeds AddRequest |
read statically; the function's own runtime lines were captured |
Last_Source uses Sched_Pos, not raw SrcId; AUX is 7 |
executed — tuner tuple; AUX's 7 confirmed by the boot-to-AUX build |
ExecuteAllocationFirstRound reads +0xb4 at 0x01695b90; StartUp never enters allocation |
read from disassembly |
Init runs before StartUp |
inferred — StartUp takes the lock Init creates |
StartUp restores Last_Source → +0xb4 and Last_Source_Priority → +0xac, then starts a 7.5 s init timer |
read from disassembly |
the first pass ignores +0xb4 until the init timer fires or a matching request arrives (+0x3c0) |
read from disassembly |
restore needs the request's (Sched_Pos, priority) to equal (Last_Source, Last_Source_Priority) as it enters ScheduledInit; requests with PrOnly set skip this |
read — disassembly and Ghidra decompilation of AddRequest; executed under emulation |
the timer path writes a literal 1 (POS_TUNER) into +0xb4 |
read from disassembly — 0x01697b44 |
stock AUX has PrOnly set and no ScheduledInit row; every PrOnly-false source has one |
executed — spy dumps, 2026-09-14 |
| on an unpatched boot the tuner won about 3.7 s after the restore, and AUX's request waited | executed — spy lines, 2026-09-14 |
| AUX's request priority is 20 | executed — Norm column of the spy request-list dump |
stock firmware cannot resume AUX even with Last_Source 7 / priority 20 saved |
inferred — from the path and the executed values |
PrOnly is ActivateSource's argument, and the AUX input handler passes true (0x02303474) |
read — decompilation and disassembly |
the vtable for C_MGR_SRC holds exactly one pointer to StartUp, at 0x0307aa74 |
read from the image |
callers.py finds direct bl callers only; most C_MGR_SRC calls are lis/addi + bctrl |
executed — 108,907 sites; an address-materialisation scan finds the rest |
traces.bin holds source-manager output |
false — it is a 2017–2020 exception log |