Watch
1
0
Fork
You've already forked SouveraineOS
0

saf: one spine — device, state, and work under the index

PAF becomes saf/device (history kept), STATE.md dissolves
into saf/state.md with the dated era archived, the substrate
SAF moves up from souveraine, and every agreement points at
saf/INDEX.md and nowhere else. one map, nothing to remember
This commit is contained in:
Fimeg 2026-08-18 09:47:30 -04:00
commit bde961c6f2
90 changed files with 4590 additions and 68 deletions

112
saf/device/README.md Normal file
View file

@ -0,0 +1,112 @@
# PAF — Pixel 3 (blueline) architecture
The living spine for the phone, reached from the SAF index as **device**. It
began in the pmOS project, moved to Pixel3Arch in July, was copied to
SouveraineOS on 2026-07-24, and became `saf/device/` on 2026-08-18 when the
SAF moved up to the umbrella and took the whole spine with it. Device code
remains in `../../Pixel3Arch/`.
**Update the relevant layer in place.** Dated captures are evidence, not a
second spine. Git holds the old arrangements: SouveraineOS `72aaa53` is the
July import; Pixel3Arch `9b1afd1` is the last full in-repo PAF snapshot.
## Start here — "I want to work on my phone"
The six things you need before any of the layer docs below matter.
**Where it is.** `10.10.20.234` over WiFi (stable since 2026-07-20; the MAC is
pinned, so it does not drift the way this doc used to claim). `172.16.42.1`
over USB, which is the fallback when WiFi is wedged.
ssh -i ~/.ssh/ani casey@10.10.20.234
**What it runs.** Souveraine OS — Arch Linux ARM, systemd, pacman, bash. Our
own kernel (`uname -r``7.1.1-sdm845-g<hash>`; the hash is ground truth for
what is actually booted). Slot A is ours, slot B is LineageOS and is the
fallback — never touch it. `casey` has passwordless sudo.
**Where things get built.** Never here, and never on the laptop:
ArchDev, `casey@10.10.20.123`, same key. The phone has 3.5 GB of RAM and is the
daily driver; the laptop is for device ops only. This holds for `cargo test`
and `cargo clippy` too, not just release builds.
**How it updates.** `sudo pacman -Syu`. Everything we build ships as a signed
package — kernel included. Reaching for fastboot because a kernel changed is a
regression, not a fallback; see `../../START-HERE.md` and
`../../Pixel3Arch/CLAUDE.md` for the few real reasons to flash. COLD boots only
(a warm reboot poisons TZ/remoteproc), and reboots are Casey's, at the device.
**When it will not answer.** Two failures look identical (ping works, ssh
hangs) and both are routing, not the phone:
- *USB:* networkd matches `Name=usb*`, so a stale down `usb0` and the live
`usb1` both hold 172.16.42.1 and replies leave by the dead one.
`sudo ip addr flush dev usb0`, and on the laptop
`sudo ip neigh del 172.16.42.1 dev <iface>` if the MAC looks wrong.
- *WiFi:* ath10k_snoc wedges with auth timeouts — reload the module, then
`systemctl restart wpa_supplicant`.
**Work that started on the phone.** This is the one with no tooling, so it is
the one that bites. Things get hacked on-device and then live only there —
`~/stevia-pkg` was four pkgrels deep before any of it came back. To find it:
ssh -i ~/.ssh/ani casey@10.10.20.234 'ls -lt ~ | head -20'
pacman -Qm # foreign packages — built here, owned by nobody upstream
pacman -Qo /usr/bin/<x> # "No package owns" = it was hand-copied
Finishing it means three steps, in order, and the middle one is the one that
gets skipped: **merge it back** into `../../Pixel3Arch/pkgs/<name>/` (the repo
copy may have moved too — diff both ways, they can both claim the same pkgrel),
**build it on ArchDev** (`CARCH=aarch64 makepkg -f --ignorearch --nodeps`;
`../../Pixel3Arch/pkgs/stevia` is the worked cross example), then **install the
built package** rather than the on-device one. A thing that only exists on the
phone is one reflash from gone, and `../../Pixel3Arch/rootfs-overlay/` only
reaches a device at provision time — which is TASK-27's whole subject.
---
- **[platform.md](platform.md)** — hardware map, live partition layout,
reaching the phone, boot chain, recovery.
- **[build.md](build.md)** — gitea → ArchDev → deploy pipeline, and the
gotchas that have each cost a session.
- **[display.md](display.md)** — panel/DSI stack, the warm-cycle doctrine,
SWIRE bias rails. SOLVED; read before touching the panel driver.
- **[touch.md](touch.md)** — FTS driver, reset polarity truth, GPI-DMA
telemetry, the tap-to-wake/SLPI handoff goal.
- **[slpi.md](slpi.md)** — sensor island: the five-fix bring-up chain and
the `sar.cc:27` wall. Arch status header + pmOS-era record.
- **[audio.md](audio.md)** — WCD9340/SLIMbus audio: working HiFi speaker and
microphone profiles, working two-way calls, and the lifecycle archaeology.
- **[edge-sense.md](edge-sense.md)** — Active Edge force path, calibration,
sessiond gating, and the remaining doze gap.
- **[modem.md](modem.md)** — WORKING on Arch. Status header + the full
52-wall research record (modemsmem root cause).
- **[evidence/](evidence/)** — dated capture/research docs the spine cites:
audio handoffs under `evidence/audio/`, power handoffs under
`evidence/power/`, plus modem/QCRIL/TZ archaeology and pmOS-era snapshots.
Read-only history; amend the living layer doc instead.
Related: `../../START-HERE.md` for triage, `../../docs/tasks/` for live work, and
`../../Pixel3Arch/` for kernel, packages, overlays, and device tooling.
## Status at a glance (reconciled 2026-08-07)
| Layer | State |
|---|---|
| Boot chain (our kernel + pmOS-chain ramdisk → Arch_root) | 🟢 stable; qbootctl closes the A/B lockout |
| Display (SW43408 panel, DPMS off/on wake) | 🟠 Warm-cycle baseline remains the only working architecture, but current `g56b0a053bef8` glass testing shows a green flash at boot/wake and occasional stale visible frame; no DSI/DPU error was logged — see display.md |
| Touch (FTS, multi-touch + gestures) | 🟠 Daytime touch and gesture detection work on current glass. DT2W can half-wake on the first double-tap (prepare immediately followed by unprepare); a second tap wakes — see touch.md |
| Modem / RF (LTE data) | 🟢 WORKING on Arch — mmcli connected, LTE, Fido, clat up (verified live 2026-07-10) |
| WiFi (ath10k_snoc) | 🟢 True unplugged cold boot verified 2026-07-11: no `rejected:90`, associates, gets DHCP, and reaches gateway + Internet. `failed to install key … -110` still occurs during association/roam but did not break connectivity; `cryptmode=1` remains a fallback only if it does. |
| Bluetooth (bluetoothd) | 🟢 WORKING 2026-07-10: controller was invisible because WCN3990 ships no BD address (kernel leaves hci0 unconfigured) — blueline-bt-bdaddr.service sets a persisted one; live scan verified |
| USB device / KVM | 🟠 Full GUD + HID + NCM + smoo composite builds on ArchDev with transactional rollback and a sessiond-owned power-sheet verb. Not installed on glass yet: the phone still runs usb-signaller 0.3.1-3 and has no smoo package — see `../../USB/README.md` and TASK-62. |
| SLPI sensor island | 🟢 `sar.cc` fatal is gone (regression, not a real limit) — `iio-sensor-proxy` runs always-on via `blueline-sensors-enable.service` (After=blueline-hexagonrpcd-sdsp), no more on-demand start/stop. Proximity (TMD2725) and accelerometer both live — see slpi.md |
| Tap-to-wake | 🟠 AP-side gesture mode detects DBLTAP and delivers wake, but the current screen-toggle path can half-wake on the first tap. DT2W now gates on proximity — `blueline-screen-toggle on` checks `ProximityNear` over D-Bus and suppresses the wake if blocked (phone in pocket). The SLPI gesture-nanoapp path is unblocked by the sar.cc fix but not required for AP-side DT2W. |
| Audio | 🟢 WORKING. PipeWire/WirePlumber exposes the HiFi speaker and microphone; Casey uses the mic for STT. The Voice Call profile carries real two-way calls, with earpiece/speaker switching handled by `blueline-callaudio-hook` — see audio.md. |
| Active Edge (squeeze) | 🟢 WORKING 2026-07-28 — force detected (2502-2990 deflection) after the rail became a DT gpio-hog; producer packaged, enabled, and gated through sessiond's pocket veto. No detection while the AP sleeps — see edge-sense.md |
| Fingerprint (FPC1020) | 🟠 driver written and building 2026-08-01 — `drivers/input/misc/fpc1020.c` on branch `fingerprint-fpc1020`, reset gpio7 / IRQ gpio121 verified against this unit's DTBO, reports `KEY_WAKEUP`. **Never run on the device.** Electrical half only; capture/matching are a TrustZone app we do not load — see TASK-44 |
| Camera | 🟠 Front IMX355 streams real frames. Rear IMX363 DT exists but remains unbuilt — see TASK-46. |
| NFC / haptics | ⚪ Open — see TASK-45 and the live task index. |
**One-line state:** daily-driver Arch on slot A with LTE, WiFi, Bluetooth,
speaker/microphone/call audio, daytime touch, Active Edge, always-on sensors,
and a working front camera. Android (LOS) remains intact on slot B.

836
saf/device/audio.md Normal file
View file

@ -0,0 +1,836 @@
# Audio — WCD9340 / SLIMbus
## Current — 2026-08-07
This is a working phone audio stack, not an active microphone bring-up.
- **Runtime:** PipeWire + WirePlumber + pipewire-pulse + pipewire-alsa. Native
PulseAudio and the ABI-provider shims are historical experiments, not the
running authority.
- **HiFi:** both CS35L36 speakers play; the WCD9340 microphone captures clean
mono audio through DMIC4 → DEC5/TX5. Casey uses it regularly for STT.
- **Calls:** the `Voice Call` UCM profile, `souveraine-callaudio`,
`souveraine-q6voiced`, and `souveraine-callaudiod` carry real two-way calls
and the earpiece/speaker path.
- **Persistence:** WirePlumber restores volumes and the selected profile. Do
not add another volume writer; role policy remains TASK-39.
- **One hardware debt remains:** the kernel permanently holds codec sysclk and
MICB2 from probe. Audio works, but the hold costs idle power and blocks a
clean suspend story. TASK-07 owns removing it.
The July **“UN-PIN TEST PASSED”** below removed the userspace always-open mic
pin. It did **not** remove kernel commit `cd7758eae`'s sysclk + MICB2 hold; that
similar noun is the likely source of the remembered “we resolved it.”
The implementation lives in Pixel3Arch under `pkgs/souveraine-ucm-blueline`,
`pkgs/souveraine-wireplumber-blueline` (which packages the canonical
WirePlumber policy from `rootfs-overlay/`), `pkgs/blueline-callaudio` (package
name `souveraine-callaudio`), `pkgs/souveraine-callaudiod`, and
`pkgs/q6voiced` (package name `souveraine-q6voiced`).
Everything below is the evidence ledger. Headings such as “still open” describe
the date beneath them unless the current block above repeats the defect.
## UCM PROFILE GONE AT BOOT — 2026-08-16, fixed same day
Boot-time music died after kernel `7.1.1.r162.g457a667bd3eb` (installed
2026-08-13, first boot 08-16 00:46): the card came up with only `off` and
`pro-audio`, no UCM profiles, so no HiFi sink existed at boot. In-session
audio (calls, STT mic) kept working — the regression only hit the fresh-boot
state, which is why it surfaced as “music at boot”.
Measured chain, all on-device:
- **Kernel made routed capture strictly mono 48k.** The wcd934x
recorder-state work in r162 (`b30e849be` “program all DMIC interface pads”,
`9610ef9cf` “replay complete blueline recorder state”) rejects stereo
capture hw_params on the routed MultiMedia2 (`CHANNELS: [1 4]` advertised,
but `arecord -c 2` → -EINVAL, `-c 1` → fine).
- **ACP probes with declared UCM values.** The Mic devices declared no
`CaptureChannels`/`CaptureRate`, so ACP assumed stereo at the FE-default
rate → capture probe failed -EINVAL → every profile “not supported” → ACP
fell back to `pro-audio` (priority 1) at boot.
- **Secondary lock, not the cause:** `90-blueline.conf` sets
`session.suspend-timeout-seconds = 0` on `~alsa_output.*`, so the pro-audio
sink never released hw:0,0; later probes died “Device or resource busy”.
The persisted `default-profile` state (wireplumber saves the selected
profile, by design) then re-pinned `pro-audio` each boot.
- **Fix:** declared `CaptureChannels 1` + `CaptureRate 48000` in both Mic
devices (HiFi + VoiceCall). ACP probe now succeeds; all three UCM profiles
form; card activates HiFi at boot. Tone verified audible on glass; mic
capture nonzero through the UCM source. Shipped as
`souveraine-ucm-blueline 0.1.0-2` (commit `9f0f103`, CI run 1696);
rootfs-overlay UCM copies kept identical to the package.
- **Not shipped, known hardening:** scope the never-suspend output rule to
`~alsa_output.platform-sound.*__sink` (the UCM sinks) so a stray pro-audio
fallback can never hold hw:0,0 forever.
Probe mechanics learned (canonical, 2026-08-16):
- `spa-acp-tool -v -v hw:0` is the ACP probe; “Probing profile X” with no
failure lines after it = supported. The trailing “unknown command hw:0” is
a harmless arg-parsing quirk.
- `alsaucm -c hw:0 set _verb HiFi` is the real-card UCM branch; it runs the
verb EnableSequence (and enables devices — TX5 mapping included). `-c G3`
and `-c "Google Pixel 3"` hit the *no-hw* branch (conf.virt.d / legacy
paths) and fail ENOENT even when UCM is healthy — red herrings.
- `alsaucm list profiles` / `get _verbs` fail with ENOENT quirks on this
box; they are not valid probes.
- `cat /proc/asound/card0/pcm*p/sub0/status` shows `owner_pid` — the way to
find who holds a PCM.
- Raw `arecord -D hw:0,1` only works while the verb routes are applied;
`spa-acp-tool` runs DisableSequence on exit and tears them. Re-apply the
verb before raw capture tests.
## PIPEWIRE RESTORED — 2026-07-19 late evening
The PA-native arrangement is retired. Live state, all verified on-device:
- **Stack:** pipewire 1.6.8 + wireplumber 0.5.15 + pipewire-pulse +
pipewire-alsa. The shims (`pipewire-abi-provider`, `pulse-native-provider`)
and the patched `pulseaudio-q6` are removed (rollback pkg kept at
`~/build/pulseaudio-q6-r3/`; `/etc/pulse` backed up as
`/etc/pulse.bak-20260719`). The `enable-pipewire` gate drop-ins were
deleted — PipeWire is unconditional now.
- **UCM discovery works under ACP/wireplumber.** HiFi verb → Speaker sink +
Mic source appear named from the UCM comments. The old `alsaucm -c G3`
`-2` failure is an id-vs-longname CLI quirk; `-c "Google Pixel 3"` parses
both verbs fine. Real clients are unaffected.
- **Playback:** `51-blueline-audio-s16.conf` (S16LE + disable-mmap +
generous periods, recovered verbatim from `8920c3c`) reinstated; chime
confirmed audible on glass.
- **Capture:** mic source pinned mono S16LE 48k, suspend disabled,
period 4800 / headroom 9600 (`90-blueline.conf`). Nonzero on every test
capture.
- **UN-PIN TEST PASSED.** With the kernel sysclk hold (`cd7758eae`) and no
loopback hold, two consecutive same-map reopens both captured nonzero
(451k/470k nonzero samples). The userspace mic pin is DELETED — kernel
hold alone is sufficient. Cold-boot confirmation pending.
- **New kernel bug found and fixed:** any xrun → userspace
`snd_pcm_prepare()` re-enters wcd934x prepare without shutdown →
`slim_stream_prepare` -EINVAL "Stream already Prepared" retried ~50/s
forever (PipeWire recover loop starved the whole graph). Fix
`f56b1ae08` on `mic-race-fix`: per-dai `prepared` flag, re-prepare
no-ops, stream stays armed (alternation-law friendly). Wireplumber
headroom masks the common case until the kernel is installed.
- **VoiceCall.conf `hw:0,5` → `hw:0,2`** fixed on-device + repo.
- **Voice-call discovery:** VoiceMMode1 (`hw:0,2`) open fails -EINVAL until
the voice BE routes (`SLIMBUS_0_RX Voice Mixer VoiceMMode1`,
`VoiceMMode1 Capture Mixer SLIMBUS_0_TX`) are applied — DPCM
no-backend rejection. With routes applied the PCM opens. This is why
q6voiced logged "Failed to open tx/rx: Invalid argument". ~~Because the
PCM is unprobeable, ACP can never synthesize a VoiceCall profile, so
callaudiod's profile-switching path is structurally dead on this card.~~
**Wrong — see 2026-07-29.** ACP never probes `hw:0,2`; it probes the PCMs
the verb *declares*, and the profile was lost for two reasons of our own
making. Shipped `blueline-callaudio-hook` (system service): MM `CallAdded`
→ UCM voice verb, last call terminating → HiFi verb restore. q6voiced then
opens an already-routed PCM.
## PHONE CALLS WORK — TWO-WAY AUDIO ON A REAL BEARER — 2026-07-29
Incoming call, answered, audio both directions, confirmed by ear at both ends.
Five defects had to fall, every one of them ours:
- **Verb named `VoiceCall`.** alsa-lib's `SND_USE_CASE_VERB_VOICECALL` is
`"Voice Call"`, with the space, and callaudiod accepts only a profile
carrying that prefix (`cad-pulse.c:508`). Renamed.
- **The voice verb gave MultiMedia1 no backend.** HiFi routes it through
`QUAT_TDM_RX_0`; the voice verb routed nothing, so opening `hw:0,0` under
the verb returned -EINVAL and ACP dropped the profile — *"Profile 'Voice
Call' mapping 'Voice Call: Speaker: sink': output PCM open failed"*,
`spa-acp-tool -v -v`. That, not an unprobeable `hw:0,2`, is why no voice
profile ever appeared. The card carries one now.
- **Downlink went to `SLIMBUS_0_RX`.** blueline's earpiece and loudspeaker are
both CS35L36 amps on `QUAT_TDM_RX_0`; the WCD9340 is the uplink and the
headset jack. Voice sent to the codec reached nothing — audible as a call
the far end heard perfectly while this end heard silence.
- **A front-end takes exactly one backend.** These mixers are *not* additive:
enabling a second silently clears the first, measured in both directions.
Setting `QUAT` and `SLIMBUS` together is not belt-and-braces, it is a no-op.
- **q6voiced latches a failed open.** tinyalsa's `pcm_open()` returns a
non-NULL handle on failure and `q6voiced_open()` guards on `if (v->tx)`, so
one lost race silences the whole call. Reachable on any outgoing call:
`mm_state_is_active()` counts DIALING, emitted in the same instant as
`CallAdded`. Incoming calls are safe only by accident — RINGING_IN is not
counted active, so their open lands well after the routes. Patched in
`souveraine-q6voiced` to release and retry.
Two further findings:
- **The mic died after every call**, not just occasionally. `alsaucm` holds its
UCM context in-process, so a one-shot `set _verb HiFi` has no previous verb
to dismantle and never runs its `DisableSequence`. TX5 and TX7 both stayed
mapped into `AIF1_CAP`, SLIM_0_TX carried two channels into a mono front-end,
and capture read exact zeros. The hook now unmaps the outgoing leg explicitly.
- **callaudiod cannot own earpiece/speaker on this card.** It switches a sink
*port* (`cad-pulse.c:961`) and needs both port types on one sink, but ACP
models UCM devices as one profile per combination — `Voice Call (Earpiece,
Mic)` and `Voice Call (Mic, Speaker)`. `ConflictingDevices` does not change
it. enchilada sidesteps this by giving each transducer its own PCM; blueline
has both amps behind one. The toggle is therefore
`blueline-callaudio-hook speaker on|off`, writing the amps directly.
## ROOT CAUSE FOUND AND FIXED — MICS CAPTURE REAL AUDIO — 2026-07-19
**The wall was the codec master clock.** Mainline DT fed the WCD9340's `extclk`
from `RPMH_LN_BB_CLK2` (inherited from the reference-board dtsi). On blueline,
Android's own DT gives **LN_BB_CLK2 to the Pixel Visual Core (`mnh-sm`)** and
feeds the codec from **`clock_rpmh` index 4 = LN_BB_CLK3** (verified by
decompiling the phone's `dtbo_b`; `audio_ext_clk_lnbb``<&clock_rpmh 0x04>`).
The RPMh vote on the wrong buffer succeeds, so nothing errors — the codec just
never receives 19.2 MHz. That single fault explained the entire residue:
- eFuse sensing timed out every boot (`-110`, `status=0x0`) — needs MCLK, no
ports involved. After the fix: `eFuse sense complete: status=0x01 out14=0xda
out15=0x01` — first time ever on mainline.
- Analog AND digital capture read zeros (whole audio core unclocked) while
every SLIM control write ACKed (SLIMbus clock is separate).
- IRQ 213 (codec SLIM data line) never fired in the machine's life; after the
fix it fires on every capture.
- MBHC read garbage impedance at probe.
Fix: `&wcd9340 { clocks = <&rpmhcc RPMH_LN_BB_CLK3>; }` in
`sdm845-google-common.dtsi` (committed in the kernel tree). Deployed initially
as a verified one-byte DTB patch (`/boot/dtbs/sdm845-google-blueline.dtb`,
backup `.lnbb2-orig` beside it; kexec chain reads the DTB from `/boot`, NOT
from the boot_a image — see below).
**Live capture results after the fix (kernel `g8cbfbb939327`, same boot):**
- AMIC1 → ADC1 → DEC0 → TX0: 121,724 / 144,000 nonzero samples
- DMIC0 → DEC7 → TX7 (`hw:0,1`): 94,198 nonzero
- DMIC4 → DEC5 → TX5 (Android triple-mic route): 81,545 nonzero
- DMIC3 → DEC6 → TX6 (Android triple-mic route): 92,216 nonzero
- DMIC4 → DEC7 → TX7 (Android handset-mic route): 124,050 nonzero
All three physical mics (DMIC0/3/4 per Android's `triple-mic` path) and the
analog path produce real ambient audio through plain ALSA `hw:0,1`
**no HAL, no ADM usecase, no ACDB OEM tree in the loop**. This falsifies the
"ADM usecase routing is required for samples to flow" hypothesis from the
2026-07-19 live-reconciliation doc, and retroactively confirms every "codec
registers are bit-perfect but silent" audit: the registers were right; the
silicon was unclocked.
### Boot chain discovery (why kernel deploys "never landed")
`boot_a` holds pocketboot (LinuxBoot, a 7.2.0-rc1 first-stage kernel). It
kexecs `/boot/vmlinuz-blueline` + `/boot/dtbs/sdm845-google-blueline.dtb` per
`/boot/extlinux/extlinux.conf` on Arch_root (`linux,booted-from-kexec` in the
live chosen node). Flashing boot_a via fastboot does NOT change the running
kernel/DTB — install to `/boot` on the rootfs instead.
### Then-open capture intermittency — superseded later on 2026-07-19
- Ordering effect observed on the first post-fix boot: DMIC3/4 through
MUX6/MUX7 read zero until the DEC5/DEC6 (Android-native) routes ran once;
after that, the previously-dead MUX7+DMIC4 handset route worked. Some
first-use state sticks. Full codec+IFD regmap snapshots of the known-good
state saved on-phone (`/tmp/codec-good-state.txt`, `/tmp/ifd-good-state.txt`
— copy off before they evaporate; /tmp is tmpfs).
- The last test of the session (speaker-tone loopback) read all-zero again
right after a string of successes — cause unestablished. Per the
live-reconciliation doc's warning, record airplane/radio/mode state before
interpreting ANY zero capture from now on.
- Android runtime parity notes for the diff: Android recording state has
`0xd25 = 0x11` (ADC MUX3/7 mode reg) where ours reads 0x00 even while
working; pads `0x8015-0x8018 = 0x00`, `0x8019/1a = 0x0a` — identical to what
our kernel programs (my in-session debugfs pad pokes don't persist).
- Regmap-cache caveat from the reconciliation doc applies to OUR debugfs reads
of IFD/PGD registers too: prefer live-read paths when auditing those.
### THE ALTERNATION LAW — exact zombie mechanism (2026-07-19, evening)
Every capture result from all three boots today reduces to ONE deterministic
rule: **a SLIM TX capture produces samples only when its channel map differs
from the previous capture session's map. Re-opening with the identical map
always reads zeros.** Verified live: TX5→TX5 = work,zero; TX5→TX6→TX5 =
work,work,work; boot-first TX7 always zero (ch135 is pre-touched by ADSP
boot/cal state, so the first TX7 session counts as a same-map reopen).
Everything earlier read as input-specific (DMIC3/4 dead), power-related
(PA holding sysclk), or "priming" was test-order artifact of this law.
- Codec side is fully correct per-session (DEF_ACT_CHAN wbuf identical
between dead and working sessions except TID; teardown calls
slim_stream_unprepare which sends DEACTIVATE/REMOVE/RECONFIGURE per
drivers/slimbus/stream.c).
- Clock gates during a zombie capture are bit-identical to Android's working
recording (0x711=0x91 0xd41=0x01 0xd42=0x0d).
- Conclusion: the ADSP's SLIM-master/AFE side treats an identical re-DEFINE
as a no-op and never re-arms the data path. Android never hits this because
its HAL/ADM re-arms per session (the reconciliation doc's ADM hypothesis,
refined: not "ADM required for routing" but "ADM masks the stale-restart").
- NEXT INSTRUMENTATION: log outgoing MCs in qcom_slim_ngd xfer path and
diff same-map vs changed-map session teardown/setup on the wire; then
candidate kernel fix = force a map perturbation or explicit port
disconnect+reconnect on every SLIM capture start.
- Userspace mitigation shipped meanwhile (see 90-blueline-alsa.pa): a
null-sink loopback holds blueline_mic open from PA start — the boot-time
open is on virgin TX5 (works), and a held-open source never re-opens, so
the law is never violated. VALIDATED on true cold boot 2026-07-19 12:09:
blueline_mic RUNNING at boot, 262k/263k nonzero samples first client,
two back-to-back client sessions both live (peaks 5256/5590/26044).
Costs continuous capture power (remove once the kernel fix lands).
### THE BOOT RACE — pinned boot capture is a coin flip; kernel + config were red herrings (2026-07-19 evening)
Journal survey of ALL 9 boots of 2026-07-19 (`journalctl -b -8 .. -b 0`,
MICDBG sysclk lines at the pin's boot-time capture open):
| boots | kernel | at pin open | MICB2 | outcome |
|---|---|---|---|---|
| -7 | g323b297 | `users=2 skip` (joined a live hold) | 0x50 | 262k WORKING |
| -3 | gbdf098937 | `users=2 skip` | 0x50 | never tested (!) |
| -8 | g323b297 | `users=1 full bring-up` after `users=0 HW POWER-OFF` | 0x10 | zero |
| -6 -5 -4, 0 | gbdf098937 | same losing pattern | 0x10 | zero |
| -2 -1 | gfdb22e5 | same losing pattern | 0x10 | zero |
An early boot-time actor grabs codec sysclk ~3s after boot, holds ~3s, and has
MICB2's 0x40 bit up while it holds. If PA's pin lands INSIDE that window the
capture joins as user 2 and samples flow; if it lands after the release (codec
did a HW POWER-OFF), capture does its own full bring-up and reads zero on every
route for the rest of the boot — the same persistent zombie the PA-stop
experiment produced. Same kernel both wins and loses (g323b297: won -7, lost
-8; gbdf098937: won -3, lost four). Therefore:
- **QRTR=y (`fdb22e5`/`f245323`) is exonerated.** Every "dead on fdb22e5"
observation was a lost race, sampled. Config stays QRTR=m / PD_MAPPER=m
(Kconfig forces PD_MAPPER ≤ QRTR anyway); it is orthogonal to the mic.
- **The CHAN_CTRL-remove commit (`bdf098937`) is exonerated for "dead at
boot"** — its code path is open-inert (only adds `disable_stream`, verified
by diff) and it won the race on boot -3.
- **The un-pin question is STILL unanswered.** It needs a race-winning boot:
pinned baseline nonzero, `pactl unload-module` the loopback, then same-map
reopen; nonzero reopen = pin can be deleted.
- The early holder is unidentified; MBHC headset-detect is the prime suspect
(boot-time codec user, raises MICB2 for impedance sensing). MICB2
0x50-vs-0x10 tracks outcome 8/8 but is NOT yet proven causal — it may just
fingerprint the hold. Verify before building on it: read Android's ANA_MICB2
during a working slot-B recording, and/or set the 0x40 bit via debugfs during
a fresh lost-race boot's capture.
- Kernel fix direction (pick after causality check): wire the DMIC supply
(micbias) into the capture DAPM path, or hold sysclk from probe until first
capture so the boot window cannot close early.
### Afternoon findings — zombie state + wedge reproduction (2026-07-19)
- Cold-boot experiment (radios all on, recorded): first `arecord` after boot
opened ONLY the FE — zero kernel-side codec/BE activity, zeros. Second and
third captures (any route) worked. The morning "DEC5 unlocks MUX7" story is
partly this: two distinct zero-classes exist — (a) BE-never-opened,
(b) BE-open-but-silent.
- Stopping PulseAudio (releasing the card → last sysclk user drops →
`wcd934x_disable_ana_bias_and_syclk` powers the codec down) put capture into
a persistent all-zero state on EVERY route, and restarting PA did not
recover it. Mixer state is bit-identical PA-up vs PA-down (diff empty), so
it is a power-lifecycle effect, not mixer state.
- In the zombie state, live volatile reads DURING capture show the analog
domain healthy: `ANA_BIAS=0x80 BUCK=0x07 MICB4=0x90`. The bias/buck theory
is dead. Codec-internal clock gates (0xd41/0xd42) were not yet live-readable
— instrumented build `323b29715` makes them volatile and logs
enable/disable/sido lifecycle + per-capture clock-gate reads.
- Manually zeroing `CLK_SYS_MCLK_PRG` (0x711) via debugfs wedged the SLIM bus
into the historical `-110` NACK loop (`Failed to read IRQ status: -110`,
`Error Interrupt 0x82000000`, 1 Hz). One historical "SLIM wedge" class is
therefore reproducible as pure codec-clock-state loss — consistent with the
2026-07-19 warning that not every wedge was a bus/driver fault. Phone needs
a cold boot after this (self-inflicted, cause exactly known).
- Upstream comparison: `enable_ana_bias_and_sysclk`/`disable` and the
sido-cache skip are upstream-inherited structure, not our divergence; our
only body change is the Android-parity buck sequence (captures work with
it). No structural smoking gun found by code audit — hence instrumentation.
## SLIM NACK loop confirmed boot-invariant on a real cold boot — 2026-07-15
- Fixed three misidentified raw-hex register writes in
`wcd934x_apply_blueline_android_tx7_capture_setup()` (kernel `5199244ee0ee`):
`0x0b07-0x0b3f` were RX playback compander `CTL6` registers (not TX
per-path enables), `0x0c96` is `WCD934X_SWR_AHB_BRIDGE_ACCESS_STATUS` (a
status register, not a TX enable — writing it was the suspected cause of a
SLIM wedge seen on the prior build `9610ef9cf45a`), and `0x0d1e`/`0x0d25`
are `ADC_MUX0_CFG1`/`ADC_MUX3_CFG1` (TX0/TX3, not TX7/TX8 as the old
comment claimed). Verified against downstream
`techpack/include/asoc/wcd934x_registers.h`. This fix did NOT resolve the
SLIM wedge — see below.
- The recurring error is a genuine **NACK**, not a bus hang:
`qcom,slim-ngd-ctrl: Error Interrupt received 0x82000000` decodes to
`NGD_INT_TX_MSG_SENT (BIT 31) | NGD_INT_TX_NACKED_2 (BIT 25)` — the
message transmitted and the target actively refused it. The target is
`LA:0xcf` (the WCD934x codec's assigned logical address, confirmed not
`SLIM_LA_MGR`/`0xff`), message code `0x60` =
`SLIM_MSG_MC_REQUEST_VALUE`. The read is `regmap-irq`'s generic
`regmap_irq_thread()` polling the codec's IRQ status registers
(`drivers/base/regmap/regmap-irq.c`) — the codec's IRQ line is
`IRQF_TRIGGER_HIGH` (level), so a failed status read never clears it and
it refires every ~1s, forever, from ~1-2min post-boot onward.
- Tested with a **user-operated true power-off (`shutdown -h now`, not
`fastboot reboot`) followed by manual power-on**, specifically to rule
out the in-place-ADSP-restart degradation `b0b813f` warned about. Result:
the NACK loop reproduces identically on this real cold boot (confirmed
climbing at 110s uptime, same `MC:0x60,LA:0xcf` signature). **This is not
a warm-reboot or ADSP-restart artifact** — it is boot-invariant, matching
the entry below from earlier the same day.
- `blueline-hexagonrpcd-adsp.service` (`b0b813f`, still uncommitted to
gitea as of this entry) attaches cleanly on this same cold boot — no
fluence/ECNS/AVS module errors, only the unrelated `lowi_lp.conf` miss.
So the ADSP audio-PD HexagonFS gap is fixed and not the current blocker;
capture still fails at the ALSA `hw_params` layer (`arecord` "Unable to
install hw params") before the ADSP module question is even reachable,
because the SLIM NACK loop blocks it upstream.
- **Current standing blocker, plainly:** the SLIM NACK loop on `LA:0xcf`.
Not yet root-caused. Two real, separate bugs were found and fixed
tonight (the register-write bug above, and the ADSP HexagonFS gap in
`b0b813f`); this is a third, still-open one.
## Cold-boot SLIM-NGD correction — 2026-07-14
- A user-operated full power-off while unplugged, followed by power-on, still
logs `qcom,slim-ngd-ctrl ... QMI wait timeout` at 12:29:52, immediately
after ADSP boot/WCD934x probe, followed by repeated `SLIM SAT: Rcvd master
capability`. This happened before any ALSA/Pulse microphone client opened.
- Therefore the timeout/capability loop is **not merely a warm-reboot or
long-uptime artifact**. ADSP, CDSP, SLPI, and MSS all reach `running`, so
the fault is specifically in the ADSP↔NGD SLIM initialization exchange and
is now the primary boot-invariant microphone lead. Do not restart ADSP or
NGD while investigating; preserve this state for tracing.
- The adjacent WCD MBHC threshold/impedance messages concern headset/accessory
detection (including the available USB-to-mic adapter), not the target
internal DMIC4 → DEC7 → TX7 capture path.
## Q6 VoiceMMode1 call path — staged 2026-07-14
- This is deliberately separate from the still-silent raw `MultiMedia1/2`
capture path. Android proves the board microphone works; normal PCM remains
an ADM/calibration problem, while modem calls use the hostless Q6Voice path.
- The missing board wiring was real: `sdm845.dtsi` left Q6 MVM/CVS/CVP
disabled and blueline had no `VoiceMMode1` DAI/card link. Kernel commit
`551d3bb1b` enables those services and instantiates the link on the tested
Type-C baseline. Its matching module tree is installed on the phone at
`7.1.1-sdm845-g551d3bb1bd35`; its boot image is staged locally.
- The preserved prior device package, found at
`archive/pmos-20260709/pmaports/device/testing/device-google-blueline/`,
is the authoritative board data: `VoiceMMode1` enumerates as **`hw:0,5`**,
not the raw MultiMedia devices. Its exact `VoiceCall.conf` routing
(`SLIMBUS_0_RX Voice Mixer VoiceMMode1`, `VoiceMMode1 Capture Mixer
SLIMBUS_0_TX`, DMIC4→DEC7→TX7) is installed in the Arch UCM overlay.
- `q6voiced-blueline` was built and installed natively, with that exact PCM-5
configuration. Its system unit is deliberately **disabled until the new
kernel cold-boot confirms `00-05: VoiceMMode1`**; enabling it on the old
kernel would create meaningless restart noise. First test must be a clean
cold boot and an actual ModemManager call *before any HiFi/raw-capture
experiment*, because the historical second call failure was SLIMBUS RX
already claimed by earlier HiFi activity.
## Live state — 2026-07-12 evening (mic)
**Live Android Recorder ground truth — 2026-07-14:** On the rooted Lineage
slot-B reference system (`4.9.337-gb929452c5c49`), an ordinary
`org.lineageos.recorder` recording was observed live, not inferred from XML.
It opens **MultiMedia1 / PCM device 0** (not MultiMedia2): its userspace input
is mono S16_LE at 8 kHz, while the active backend is
`SLIMBUS_0_TX`, mono S16_LE at 48 kHz. The downstream HAL enables
`DMIC4 -> DEC7 -> TX7`; `SLIM_0_TX Channels=One`; and its active capture
app-type control is `[69938, 4, 48000, 3]`: downstream's documented field
order makes that `app_type=69938` (the Qualcomm default TX-path app type),
`acdb_dev_id=4`, `sample_rate=48000`, `backend_id=3`. The capture track was active and
non-silent in AudioFlinger. Crucially, Android's dedicated WCD SLIM-interface
debug dump during that recording read `0x032=0x80`, `0x057=0x05`,
`0x097=0x21`, and `0x11c=0x80`. (`tavil-slim-pgd` regmap is codec register
space, not this interface-device space.) Every earlier Arch microphone
negative test targeted `hw:0,1` / MultiMedia2 at 48 kHz. The exact
**MultiMedia1 `hw:0,0` 8-kHz FE / 48-kHz SLIMBUS backend** retest then ran on
Arch with TX7/DMIC4 and the active speaker RX route preserved: its 8-second
raw payload was 128,000 bytes / **zero non-zero samples**. (An earlier
apparent 19-sample result was immediately identified as `arecord`'s 44-byte
WAV header and corrected with `-t raw`.) This closes the FE/rate mismatch as a
microphone fix. The Android HAL's ADM identity and ACDB path is now the
leading differential: it programs this FE's app type, topology, and device
calibration, whereas mainline q6routing passes zeros.
**DEF_ACT_CHAN client-field test — 2026-07-13:** The Android downstream NGD
driver encodes the first `DEF_ACT_CHAN` payload byte as
`(data_fmt << 5) | (laddr & 0x1f)`; mainline had sent the full WCD9340 logical
address (`0xcf`) and consequently put its high bits into the data-format field.
The one-line correction was built on the current Type-C/G56 test base as
`3ceab71eded0`, its matching module tree was installed before flashing, and
the resulting `7.1.1-sdm845-g3ceab71eded0` boot was verified on glass. A
controlled single-direction `DMIC4 → DEC7 → TX7 → MultiMedia2` 48 kHz mono
capture (TX6 disabled) produced `384000` bytes / `192000` samples, with **0
non-zero bytes** and signed-16 range `0..0`. ALSA state restored (only the
known read-only HPH controls complained); no new SLIM/Q6/NGD timeout or audio
error appeared. This fix is a valid protocol correction and must remain, but
it is **not sufficient to make blueline microphone capture work**.
**Android DMIC-pad + analog split test — 2026-07-13:** Android's WCD9340
initial register table writes `DMIC3_CLK_PINCFG`/`DMIC3_DATA_PINCFG`
(`0x8019`/`0x801a`) to `0x0a`; mainline defined but never programmed those
registers. That exact pair was added to WCD934x component initialization,
built as `959f0d5e6`, and booted as
`7.1.1-sdm845-g959f0d5e65fc`. The normal Android handset route
`DMIC4 -> DEC7 -> TX7 -> SLIMBUS_0_TX -> MultiMedia2` remained exactly zero
(384000 bytes / 192000 signed-16 samples / zero non-zero bytes), without a
capture-time SLIM, Q6, or ADSP error. A separate reversible replay of
Android's analog fallback `AMIC1 -> ADC1 -> DEC6 -> TX6` also produced the
same exact zero stream. Thus neither the DMIC3 pad configuration nor a
bottom-DMIC/flex-only fault can explain the failure: the common remaining
boundary is codec TX channel activation and/or the ADM/ADSP capture path.
The only ALSA restore errors were the known read-only HPH impedance/type
controls.
**ADM/calibration lead — 2026-07-13 (refined 2026-07-14):** Mainline `q6routing_stream_open()`
unconditionally passes `NULL_COPP_TOPOLOGY` and app/acdb values of zero to
`q6adm_open()`. Pixel Android's `msm-pcm-routing-v2.c` starts with that
fallback but looks up topology, app type, and ACDB device ID by FE/BE/session
from audio-calibration tables before opening ADM. Do not replace the topology
with a guessed constant: the live Android `MultiMedia1 + SLIMBUS_0_TX`
recorder has now identified `app_type=69938`, `acdb_dev_id=4`,
`sample_rate=48000` (and backend ID 3). The exact ADM topology was decoded
from the extracted board-matched ACDB on 2026-07-14; see the next entry.
**ADM identity-only test — 2026-07-14 (negative):** Kernel
`7.1.1-sdm845-g3b9cf346af3a` passed Android's observed `app_type=69938` and
`acdb_dev_id=4` only for live `SLIMBUS_0_TX` capture, leaving the topology
`NULL` rather than inventing one. The matching modules were installed before
the pmOS-chain `boot_a` image was flashed; the running kernel and modules
matched. A controlled `MultiMedia1` / `hw:0,0`, mono S16_LE 8 kHz raw capture
with the Android-equivalent `DMIC4 -> DEC7 -> TX7` route yielded 128,000 bytes
with **zero non-zero samples**. ALSA state was restored (only the known
read-only HPH controls refused writes). This eliminates the two ADM identity
numbers alone as a microphone fix. The remaining Android differential is the
calibration-selected ADM topology and/or downstream codec-side programming.
**Board ACDB topology decoded — 2026-07-14:** The local Lineage vendor blob
`blobs/los-vendor/etc/acdbdata/OEM/sdm845-tavil-b1-snd-card/Handset_cal.acdb`
is not merely a future reference: `acdb-ldp` decodes its `HANDSET_MIC` device
4 configuration map as `69938 -> 0x00010315` (at 48 kHz). Downstream's
`apr_audio-v2.h` names `0x00010315` as
`ADM_CMD_COPP_OPENOPOLOGY_ID_MIC_MONO_AUDIO_COPP`; it is the specific Android
mono-microphone COPP, rather than the mainline null passthrough
`0x00010312` or a guessed default. This explains why the identity-only test
was necessarily incomplete. The present 7.1.1 tree also contains q6core's
optional ACDB topology-registration code, but blueline's q6core DT node has
no `qcom,acdb-name` and no `Global_cal.acdb` is installed as firmware, so that
path is currently dormant.
**ADM topology + AudProc test — 2026-07-14 (negative):** The current kernel
opens `SLIMBUS_0_TX` capture with topology `0x10315`, app type 69938, and ACDB
device 4, then parses and sends the board ACDB's complete 524-byte AudProc
table. Live dmesg confirms `SLIMBUS_0_TX applied ACDB AudProc calibration:
device=4 rate=48000 app=69938 (524 bytes)`, but capture remains exactly zero.
This eliminates the selected COPP topology plus AudProc table as a complete
fix; Android also sends the separate AudVol table.
**AFE calibration delivery audit — 2026-07-14:** All three local Pixel
Handset ACDB variants agree. Device 4 has no `AFECLUT0` common-calibration
row, and its `AFE_CMN_TOPID_V2` device property is `0x000112fb`, Qualcomm's
`AFE_TX_TOPOLOGY_ID_NONE`. The only device-4 AFE datum is a 48-kHz AV-sync
delay of 578, which cannot account for an all-zero stream. Qualcomm's SDM670
ACDB loader passes the same device ID 4 to AFE (there is no hidden remap to
the unrelated AFE-common IDs 100/128/130/149), tries topology, common table,
then hardware delay. Therefore adding an AFE common/topology sender here
would deliver no microphone payload. Do not revisit AFE delivery without new
Android evidence.
**ADM AudVol + explicit SLIM-error test — 2026-07-14 (negative):** Device 4,
app type 69938, volume index 0 decodes to a 40-byte table: soft-volume
stepping parameters and mono gain `0x2000` (unity). Kernel
`7.1.1-sdm845-gfb8f067e12f2` parses `AVOLLUT0`/`AVOLCDFT`/`AVOLCDOT` with the
same validated generic table builder and sends AudProc then AudVol, matching
Android's order. The same build propagates previously ignored failures from
SLIM port connection and WCD934x stream prepare/enable. A controlled 5-second
`DMIC4 -> DEC7 -> TX7 -> MultiMedia2` mono 48-kHz capture completed normally
and produced 480,000 bytes / 240,000 samples, all exactly zero. Live dmesg
confirms `SLIMBUS_0_TX applied ACDB AudProc/AudVol calibration: device=4
rate=48000 app=69938 (524/40 bytes)` and contains no capture-time SLIM
connect/prepare/enable, WCD934x, Q6, permission, or missing-file error.
Therefore neither the missing AudVol table nor a silently discarded SLIM
setup failure explains the zero stream.
Do not stream the ADSP-owned `217:250:0:0` debugfs regmap `registers` file
during capture: a filtered read blocked after the capture had completed and
had to be killed. This is consistent with the earlier observation that IFD
`0x097` is controlled by the ADSP SLIM master; it is not a safe codec-style
bulk register snapshot path and yielded no new state evidence.
**True cold-boot re-validation — 2026-07-13:** Kernel
`7.1.1-sdm845-g44d43e6ef336` includes the WCD934x `.prepare`/`.shutdown`
stream-lifecycle experiment. On a physical shutdown/power-on with native WCN
boot healthy, the controlled DMIC4 → DEC7 → TX7 → MultiMedia2 raw capture was
again exactly zero (`192000` bytes, `0` non-zero bytes). The temporary
`mic-ngd` trace proves the entire kernel-visible sequence ran successfully:
`q6afe_port_start ret=0`, `slim_stream_enable` entered,
`qcom_slim_ngd_enable_stream` entered and returned `0`. There was no SLIM/Q6
timeout. ALSA was restored immediately; the three HPH-impedance restore errors
are documented read-only controls. **This falsifies the prepare-order patch as
a microphone fix and makes silent ADSP-side channel activation (not userspace,
Q6 return handling, or NGD error propagation) the current boundary.**
**Stable-SLPI follow-up capture — 2026-07-13:** After the repaired sensor
registry cleared CHRE's five-minute watchdog and the SSC desktop sensor clients
were exercised successfully, the same safe single-direction TX7/DMIC4 route
was captured as 48 kHz mono raw audio for five seconds. It produced 480,000
bytes / 240,000 samples, all exactly zero. ALSA state was restored by trap;
SLPI, hexagonrpcd, and iio-sensor-proxy stayed active, with no `MC:0x2c`,
`-110`, or CHRE fatal. The sensor repair therefore improves platform stability
but does **not** open the microphone path.
**NGD/update audit — 2026-07-13:** The running 7.1.1 source already contains
the complete upstream May-2026 NGD probe/SSR race-fix series that landed for
7.2-rc: deferred IRQ enable, controller-owned workqueue initialization, safe
SSR/PDR cleanup, and the `tx_lock` ABBA deadlock fix. It therefore must not be
re-applied as a speculative microphone patch. This boot still logged one
`QMI wait timeout`; that is the driver's one-second wait for the remote QMI
service, not an `EPERM` or file-access failure. It remains a boot-stability
lead, but the subsequent raw capture's core `CONNECT_SOURCE` transaction
returned `0` and the audio data was still all zero.
- The on-device session's mic doc (collected to
`docs/tasks/mic-slim-tx-20260712.md`, source
phone `~/projects/unfinished/pixel3-microphone/`) found every capture
start at ~23h uptime logging 3x `TX timed out:MC:0x2c` /
`failed:-110` — the SLIMbus `CONNECT_SRC` user message to the ADSP
master never answered — and called that the localized root cause.
- **Re-test after a WARM reboot (same day, laptop-driven): the 0x2c
timeout did not reproduce — but the boot was NOT clean.** That boot was
produced by `sudo reboot` (warm), and its dmesg shows `QMI wait timeout`
at 8.1s plus the repeating "Rcvd master capability" retry loop — the
documented degraded slim-ngd state that warm reboots carry over (same
TZ-poison family as the WiFi MSA issue; only an UNPLUGGED cold power-off
clears it). Within that caveat: a `hw:0,1` capture with the doc's exact
TX7/DMIC4 arming logged 37 slim transfers — all MC `0x60`/`0x68` value
messages, zero errors, zero timeouts — and was still bit-for-bit zero,
with IFD `0097/0320/0330/0970` all `00` mid-capture. **Both datapoints
(0x2c timeout at 23h uptime; silent no-activation here) need
re-validation on a true cold boot before either is trusted.**
- No root-cause conclusion is valid yet. The `-110` may be a degraded-NGD
artifact of long uptime / remoteproc restarts, while the silent all-zero
capture may be a separate normal-state failure; the only attempted contrast
was tainted by a warm reboot. Kernel-visible Q6 and NGD success plus Android's
`IFD 0x097=21` versus mainline `00` remain observations, **not a proven
boot-invariant activation failure**.
- Note: `0320/0330` read `00` here vs `c0/01` in the 07-11 canonical 2ch
trace — arming differed (single TX7 vs TX7+TX8); observation only.
- What the phone doc contributes regardless: OnePlus 6 (enchilada) mic
works on pmOS 6.x kernels with this same driver stack (6.x->7.x
regression candidate in slimbus/ngd/wcd934x, or blueline-specific ADSP
behavior); pmOS's blueline UCM ships with the whole mic section
commented out (nobody has solved this on mainline); operational
hazards — never restart the adsp remoteproc while debugging slim (NGD
stays degraded until reboot, reproducible), never leave MultiMedia2
with both `SLIMBUS_0_RX` and `SLIMBUS_0_TX` routes enabled (q6adm/q6asm
wedge); QUAT_TDM_TX_0 in DT would give a WCD-free CS35L36 VMON/IMON
capture reference.
- Refined next steps: trace the `qcom_slim_ngd_enable_stream` user-message
exchange on a fresh boot (is CONNECT_SRC sent and ACKed, or skipped?);
diff `drivers/slimbus/{qcom-ngd-ctrl.c,stream.c,messaging.c}` +
`wcd934x.c` against pmOS's 6.x sdm845 tree; if an enchilada is handy,
boot this 7.1.1 kernel on it to split regression vs blueline-specific;
ask #sdm845-mainline about silent no-activation with working value
messages.
### Upstream/device-branch archaeology — 2026-07-12 (no phone test)
- The current `sdm845/7.1-dev` upstream line is active (its tip is two months
newer than our 7.1.1 base) but its audio delta is the CS35L36 speaker DTS
support already mined for the working speaker route; it contains no WCD934x
microphone/SLIM-TX fix. Do not hold a cold-boot test for an imagined pending
7.1 microphone update.
- **Real candidate to test after a true cold baseline:** historical
`joel/op6-audio-fixes-wip` commit `1d5776ca` changes WCD934x SLIM lifecycle
from `.trigger` (enable on PCM START) to `.prepare` plus `.shutdown` (enable
before START, disable on close). Our current `wcd934x.c` still has the old
`.trigger` implementation. This is not upstream and not proof of a fix, but
it moves the `slim_stream_enable()` / CONNECT_SRC exchange out of PCM START
into the backend prepare phase. Static inspection confirms the Q6 SLIM DAI
starts its AFE port in `.prepare`, while DPCM prepares BEs before the FE;
that makes this a concrete stream-ordering hypothesis rather than a generic
codec tweak. First make an instrumentation-only patch or trace plan that
establishes the exact per-DAI callback order; only then consider a
rollback-approved kernel test.
- Related branch commits were inspected and are **not** default candidates for
blueline DMIC capture: `75ad5bb0` adds separate AIF2/AIF3 SLIM DAI links for
OnePlus use cases (our AIF1 `SLIMBUS_0_TX` link already matches its AIF1
portion); `4a05bee2` and beryllium `07078b5c` repair MBHC/MIC_BIAS2 headset
handling, not the DMIC4/MIC_BIAS4 path. Keep them as topology references,
not a grab-bag patch series.
- **Candidate build/deploy and cold-boot result (2026-07-13):** the lifecycle
change was ported exactly onto the running Type-C-bisect base
(`56b0a053b`) as `44d43e6ef` / Gitea branch
`mic-slim-prepare-order-g56`, built on ArchDev, module tree installed, and
boot image flashed to slot A. Casey then performed an unplugged shutdown and
power-on; on `7.1.1-sdm845-g44d43e6ef336`, the controlled
`DMIC4 → DEC7 → TX7 → MultiMedia2` `hw:0,1` capture still contained only
zero samples. No `MC:0x2c` / `-110` occurred during that capture. Thus the
prepare-order hypothesis is a valid deployed negative result, not an
untested lead; retain the kernel as the current microphone-test baseline.
## Live state — 2026-07-12 (speakers)
- BOTH speakers now fire: the earlier "speaker playback works" state was in
fact top-speaker-only — both CS35L36 amps had `Channel Mux = RX1`, so the
bottom loudspeaker never received a channel. Fixed with the pmOS backup
image's routing (Left=RX1 top, Right=RX2 bottom, plus LDM select / PDM
ramps / zero-cross / per-amp ASPTX sources) in the overlay UCM
(commit 2f39c06), installed on-device and persisted via `alsactl store`.
Verified by ear: distinct left/right/stereo tones.
- Phone-side handoff doc `~/projects/unfinished/pixel3-speaker-routing/`
(README + baseline state + test tones) documents the diagnosis trail;
its "next step" (pmOS MRs online) was superseded by reading the local
pmos-20260703 rootfs image directly (debugfs, no mount needed).
- **Regression and guardrail — 2026-07-13:** the live TDM split was still
correct (`Left Channel Mux=RX1`, `Right Channel Mux=RX2`), but both amp and
boost switches were off. Re-enabled all four and stored the corrected state
on the phone. `docs/tasks/asound-baseline-20260712.state` is a *pre-fix
diagnostic snapshot* (`Right Channel Mux=RX1`); it must never be passed to
`alsactl restore`, because doing so regresses the bottom speaker. Use the
deployed UCM and current `/var/lib/alsa/asound.state` as the restore source.
## Prior live state — 2026-07-11
- Speaker playback works end-to-end through PipeWire, UCM, QUAT TDM and the
CS35L36 amplifiers (top speaker only — see above).
- `arecord -D hw:0,1 -f S16_LE -r 48000 -c 2` (the canonical MultiMedia2
front-end, whose `SLIMBUS_0_TX` link is enabled) opens and runs, but capture is
bit-for-bit zero. This is a hardware/ASoC transport investigation, not a
PipeWire source-selection failure; no PipeWire source can exist until ALSA
produces samples.
- This work did not start `iio-sensor-proxy`. There is no evidence that the
sensor-client HACK or SLPI wake-lockup mechanism gates this audio path.
## Verified zero-capture path — historical 2026-07-11
The controlled route is Android's documented handset/endfire pair:
- DMIC4 -> DEC7 -> SLIM TX7 and DMIC3 -> DEC8 -> SLIM TX8.
- Q6 function tracing during `arecord` observed, in order:
`q6slim_set_channel_map`, `wcd934x_hw_params`,
`q6afe_slim_port_prepare`, `q6afe_port_start`, and `wcd934x_trigger`.
- A temporary argument probe observed the two expected shared-channel IDs
delivered to Q6 when TX6 is off: `tx_num=2`, `0x87`, `0x88` (135, 136).
The resulting 478,078 signed-16 samples on `hw:0,1` were all zero.
- The WCD9340 interface device (`217:250:0:0`, `WCD9340-IFC-DEV`) receives
the expected driver writes for both ports: TX7 map/config `0x11c=80`,
`0x11d=01`, `0x57=05`; TX8 `0x120=80`, `0x121=01`, `0x58=05`.
Its live interrupt enables are `0x32=c0` and `0x33=01`, so their absence is
**not** the explanation for silence.
- On that same canonical front-end, `q6afe_port_start()` returns `0`, then
`slim_stream_enable()` invokes `qcom_slim_ngd_enable_stream()`, which returns
`0` after the controller's `DEF_ACT_CHAN` and `RECONFIG_NOW` operations.
Kernel-visible Q6 and NGD schedule activation therefore succeeded.
- **Corrected DAPM observation:** a clean live `hw:0,1` capture powers
codec-side `AIF1 CAP` and `AIF1_CAP Mixer`. Its prepare phase calls
`wcd934x_codec_enable_slim` and two `wcd934x_codec_enable_dec` events.
The earlier report that these callbacks never fired was false: the trace
missed the prepare-phase calls. Do not use that earlier result as evidence.
- The *persistent* mixer state leaves `CDC_IF TX7 MUX` and `CDC_IF TX8 MUX`
at `ZERO`, despite the corresponding AIF mixer switches and DMIC muxes
being selected. Consequently the TX7/TX8 branch is off during an ordinary
capture and no `wcd934x_codec_enable_dmic` event occurs. This is a real UCM
route omission, not an ADSP conclusion.
- A controlled, automatically reverted test set those muxes to `DEC7` and
`DEC8` and disabled stray TX6. It powered the entire intended branch and
invoked `wcd934x_codec_enable_dmic` twice (with matching shutdown calls).
The independent two-second PCM capture was nevertheless 384,000 zero bytes.
Thus the codec DAPM path and physical DMIC endpoints are viable once the
muxes are selected, but that route omission is not the final silence cause.
- This is **not** a missing FE-to-BE connection: runtime ASoC state reports
`MultiMedia2 Capture: start` with `SLIM Capture 1: start` as its active
backend, and the codec's AIF/decimator event callbacks run. With the CDC
muxes selected, the DMIC callbacks run too; the unresolved point at that
date was after the fully powered codec capture path.
- Reducing the route to the downstream-style single TX7/DMIC4 channel did not
change the result: a four-second `hw:0,1` mono capture contained 192,000
zero samples. Channel count/configuration is therefore not the activation
trigger.
## What the 2026-07-11 evidence did and did not say
Android's captured recording state has IFD `0x097=21`; mainline reads `00`.
During a five-second live TX7/TX8 capture, writing `0097 21` through the
debugfs regmap returned success but read back `0097: 00`, and all 478,078
samples remained zero. This confirms that a userspace register replay cannot
enable that state on this path. It does **not** establish what `0x097` means
or prove that it is the root cause. Do not turn it into an unconditional
codec write or call it an “ADSP-master fix” without an implementation-level
source or Android-side transition capture.
TX6 is enabled by the current mixer default alongside TX7/TX8. It makes the
unconstrained map three channels (`0x86`, `0x87`, `0x88`), while disabling it
produces the correct TX7/TX8 pair above. That is a real UCM/machine-routing
cleanup item, but it did not restore capture and is not the root cause.
## Proposed next work on 2026-07-11 — superseded
1. Capture the working Android interface-device transition, or find the
downstream owner/meaning of IFD `0x097`; compare the actual write sequence,
not just idle/recording register snapshots.
2. Add the TX7/TX8 `CDC_IF` mux selections to the eventual microphone UCM
enable sequence (and keep TX6 off for this two-channel route). Do not
expose a broken PipeWire source before its capture is non-zero.
3. Repeat the WCD9340 interface/ADSP comparison only with that fully powered
codec path. The still-zero full-path test strengthens, but does not prove,
the relevance of the non-sticking Android IFD `0x097=21` delta. Capture an
Android transition or find the downstream owner before making a firmware
or register claim. Do not flash a kernel test without an explicit
rollback-approved deployment plan.
4. Only after that, compare the remaining post-activation state with Android
at the firmware boundary. Q6 and NGD success alone do not show that the
codec/ADSP hardware emits decimator samples.
5. Fix the stray TX6 default separately in the persistent UCM/machine route,
then verify it with the channel-map probe. Do not claim mic support until a
live capture contains non-zero samples.
## Instrumentation
`/usr/local/sbin/blueline-lockup-trace` is a root helper deployed from
`rootfs-overlay/`. `mic-start` traces the Q6/WCD stream functions and the
three WCD codec DAPM event callbacks; `mic-map-start`, `mic-ifc-start`, and
`mic-ngd-start` collect Q6 channel-map, WCD interface-register-write, and
Q6/NGD return traces respectively. Trace sessions are temporary and must
always be paired with the matching `*-stop` command; the current phone was
checked clean afterwards (`tracing_on=0`, tracer `nop`, no kprobes).
## 2026-07-18 full Android power-state reconciliation
A clean, single-channel PulseAudio/UCM capture on kernel `4c3a94153` proved
that the intended TX7/DMIC4 path is selected and fully ACKed, but every one of
55,680 S16 samples is exactly zero. A simultaneous live codec dump exposed
two remaining Android/mainline power-state differences which earlier tests had
not actually eliminated:
- `ANA_MICB4` is `0x50` on mainline versus `0x90` during Android recording.
The DT opt-in `qcom,blueline-dmic-micbias-pullup` is on the `codec@1,0`
node, but `wcd934x_codec_parse_data()` queried `dev->parent->of_node` (the
SLIM controller). Therefore `micbias_pullup` was always false and the
earlier claim that pull-up mode had been tested is invalid. The code now
queries `dev->of_node`, selecting Qualcomm's downstream
`MICB_PULLUP_ENABLE` path (`0x90`).
- `ANA_BUCK_CTL` is `0x00` on mainline versus `0x07` on Android. Qualcomm's
resource manager sets RCO followed by buck `PRE_EN1`, `PRE_EN2`, and
`HIGH_ACCURACY_EN`, with 100 us between stages. The mainline driver had
dropped this sequence. It is restored as ordered masked writes, including
the reverse transition; this is not the harmful unsequenced Android
end-state replay removed earlier.
The same correction (`a75371b49`) also matches Android's board state by
programming only DMIC interface 3 pads (the real DMIC3/4 inputs) and avoids
leaving TX7 at the transient 150 Hz HPF setting (`CFG0=0xd0`) indefinitely;
Android's steady recording state is `CFG0=0x90`. One boot-time log reports
whether pull-up mode was parsed, and the capture log reports live BUCK and all
MICB registers so the next capture validates the complete state in one pass.
## 2026-07-18 eFuse initialization regression and cache fix
On the clean `3b85e688f` boot, the complete downstream AFE codec-control map
registered successfully and TX7 again reached `port_start`, CONNECT_SOURCE,
DEF_ACT_CHAN, and RECONFIG_NOW with positive ACKs. PulseAudio captured 760,318
raw bytes (380,159 S16 samples), all exactly zero. The journal exposed a real
codec boot failure: `wcd934x_enable_efuse_sensing` read `EFUSE_STATUS=0` and
reported that sensing never completed.
History showed that `1c095f7a7` had added the downstream-required
internal-SIDO start before eFuse sensing, but `c1b6371c6` removed the call while
dropping unrelated experiments. The exact downstream SIDO/buck transition
helper was later restored by `a75371b49` without reconnecting it to eFuse init.
The audit also found that downstream treats `EFUSE_CTL`, all eFuse value
outputs, and status as volatile, while mainline cached them in `REGCACHE_MAPLE`.
Commit `82355eac0` batches the complete correction: internal SIDO before the
sense enable, the required 5 ms delay before RCO/BG, downstream-parity volatile
register handling for the eFuse/clock/buck sequence, live completion polling,
register error propagation, and explicit status/out14/out15 logging. Do not
interpret another zero capture until a cold boot proves the codec prints
`eFuse sense complete`; otherwise the physical codec trim/version state is
still uninitialized even though SLIM management commands ACK normally.

64
saf/device/build.md Normal file
View file

@ -0,0 +1,64 @@
# PAF · Build & deploy (Arch era)
The loop that turns a kernel commit into a verified boot on glass. pmOS-era
pmbootstrap notes preserved at `evidence/build-pmos-era.md` — dead path now.
## The three places
| Where | Role |
|---|---|
| gitea `10.10.20.120:4455``Fimeg/Pixel3Arch` + `Fimeg/linux-blueline` (branch `blueline`) | **Source of truth.** Uncommitted work does not build — commit or it doesn't exist. |
| ArchDev LXC `casey@10.10.20.123` (ani key) | **Build host.** Cross toolchain installed, MAKEFLAGS system-wide. |
| Laptop | **Device ops only** — flashing, fastboot, USB tether, boot.img assembly. |
## The loop
1. Commit + push kernel changes to gitea (`kernel/src/linux-blueline-src`
on the laptop is a clone with remote `gitea`).
2. Build on ArchDev:
`ssh ... casey@10.10.20.123 'cd ~/Projects/Pixel3Arch/kernel && ./build.sh blueline'`
- build.sh **self-locks with flock** — invoke it directly, never wrap it
in an outer flock on the same lockfile (deadlock, cost a session).
- It force-fresh-clones from gitea and **hard-asserts the built package's
embedded commit hash matches the gitea branch tip** — a mismatch means
stale source; the build fails loud instead of shipping it.
3. Pull `kernel/linux-blueline-7.1.1-1-aarch64.pkg.tar.zst` to the laptop.
4. Assemble the boot image (pmOS-chain — the currently proven ramdisk path):
```
bash boot/build-bootimg-pmos-chain.sh <vmlinuz-from-pkg> <dtb-from-pkg> \
staging/deploy-139aa/boot-139aa7748181-pmoschain.img <out.img>
```
Kernel is Image.gz + **appended DTB** (header v0 ignores --dtb); ramdisk
and cmdline are lifted from the reference image. Migrating to our own
mkinitcpio ramdisk (`boot/build-bootimg.sh`) is a parked one-variable task.
5. Deploy: `scripts/deploy-kernel.sh <pkg.tar.zst> <boot.img>`
## Deploy doctrine — no chimera boots
`deploy-kernel.sh` exists because a kernel was once booted 28 seconds before
its module tree was installed: no evdev, no input, dead services, and a
night of void test results. The script enforces the only safe order:
1. **Modules first** — scp the pkg, `pacman -U`, verify the module dir exists.
2. Flash boot_a, reboot.
3. **Refuse to bless the boot** unless `uname -r` matches the installed tree.
Never flash-then-install. If `uname -r` and `/usr/lib/modules/` disagree,
every test result on that boot is garbage — reboot properly before
concluding anything.
## Gotchas ledger (each cost a session)
- makepkg needs `CARCH=aarch64 ... --ignorearch` — CARCH comes from the
environment, not the PKGBUILD, or you get x86_64-labeled aarch64 packages.
- makepkg builds in its own clone of `source=()`, not your working tree —
a dirty tree silently builds without your changes.
- The kernel release string embeds the git hash (`-g<hash>`) — it is the
ground truth for "what am I actually running" (`uname -r`).
- `modules_install` needs CROSS_COMPILE too, or strip fails on aarch64 .ko.
- ArchDev is unprivileged LXC: qemu-emulated execution (binfmt) does NOT
work there — chroot-based work stays on the laptop or on-device.
- Commit messages: history in this repo proved that "evidence" cited in
rapid-fire commits can be wrong (see touch.md, display.md post-mortems).
Cite the actual file:line / register / boot you verified on, and note
whether the boot was clean.

109
saf/device/display.md Normal file
View file

@ -0,0 +1,109 @@
# PAF · Display — SW43408 panel, DSI, bias rails (SOLVED 2026-07-10)
## Current regression — verified on glass 2026-07-11
On current kernel `7.1.1-sdm845-g56b0a053bef8`, a true unplugged cold boot
still produces a green flash during boot. Green flashes also remain on wake.
During AP-side DT2W testing the first double tap sometimes does a visible
green-flash/black half-wake and the second wakes the panel. The kernel trace
shows the first gesture reaches panel prepare and is immediately followed by
unprepare; there are no DSI/DPU errors. In one successful wake the visible
frame remained stale despite a synchronized system clock and live Hyprland +
Quickshell processes. This is an observation, **not** proof of a panel-only
root cause; preserve it for the display/DPMS investigation.
**Recovery and clock distinction, 2026-07-11:** after an instrumented
libssc-triggered lockup, `blueline-screen-toggle on` re-prepared the panel but
it unprepared again seconds later even with `hypridle` stopped. Do not churn
panel/FTS resets in that state. A normal reboot of the known-good current slot
restored the panel and compositor. The first post-boot `ii` screenshot still
showed the Jul-06 20:00 clock floor even though NTP had corrected system time;
restarting only Quickshell (`qs -c ii kill`, then `qs -c ii`) refreshed it to
the real time. Therefore an old ii clock is not alone evidence of a frozen
scanout. The remaining task is to prevent ii from retaining the pre-NTP clock,
not to reset the panel again.
Cold boot → Hyprland AND power-button sleep/wake visually verified on
kernel `g766205b60588`. Read this whole file before touching
`panel-lg-sw43408.c`, the labibb regulator, or the display DT.
## The doctrine: WARM-CYCLE. Do not dismantle again.
The working design in `drivers/gpu/drm/panel/panel-lg-sw43408.c`:
- `.prepare` = regulators + reset pulse only. No DCS.
- `.enable` = the ENTIRE DCS init (gamma, TE, sleep-out, vendor regs, PPS,
compression, display-on) — runs over a fully-up DSI link, onto a
still-powered DDIC, on every DPMS-on.
- `.disable` = display_off + enter_sleep (HS mode). That is ALL DPMS-off does.
- **No `.unprepare` power-off.** The panel keeps vddi/vpnl forever after
first boot; reset is never re-asserted at runtime.
Why: **upstream's prepare-time cold-cycle design cannot cold-start this
panel.** Full power-off + reset + re-init produces a green flash (display-on
latching onto random GRAM) then permanent black, on every transition. That
is the original "screen won't wake without the sdm845-mainline hack" bug
(David's report). The warm-cycle shape IS the fix — the panel is
re-initialized warm, never cold.
History: the working design was dismantled on 2026-07-09 by ee6611adb
("adopt upstream prepare-time init") and a day of ordering experiments
failed to make cold-start work (display-on before PPS, after PPS, downstream
power sequencing — all green-flash-black). 766205b60 restored the warm-cycle
architecture. Any future "align with upstream" refactor must first prove
cold-start on real glass.
## Bias rails: LAB/IBB under SWIRE (hardware) control
Matching downstream: the DDIC drives the OLED bias rails over the SWIRE
wire, not software.
- DT: `qcom,swire-control` on the pmi8998 labibb node (1b02532a4).
- Driver: SWIRE ops are software no-ops (enable/disable/set_voltage return 0);
probe writes IBB `ENABLE_CTL=0x40` (SWIRE_RDY, module-enable cleared).
LAB `ENABLE_CTL=0x00` is CORRECT in SWIRE mode (slaved to IBB in hardware).
- Behavior: panel sleep-in → rails physically drop; sleep-out → rails rise.
Register-verified live across DPMS cycles: STATUS1 (IBB 0xdc08 / LAB
0xde08 on regmap 0-03) reads 00/00 off → 80/a0 on.
- The panel driver's supplies are vddi + vpnl ONLY. It never touches
LAB/IBB. c789daaa8 fixed the tail end of the old design (unprepare
disabling never-enabled rails → unbalanced-disable → `panel->prepared`
stuck true → EBUSY zombie); moot under warm-cycle but the lesson stands:
an error return from unprepare wedges the prepared flag forever.
- Voltage: software set_voltage is a no-op; hardware stays at the
bootloader's 5.5V programming.
## Userspace wake path
- Hyprland (Lua build) power-button / tap-to-wake toggle:
`blueline-screen-toggle` on the phone. Wake needs BOTH
`hl.dsp.dpms("on")` AND `hl.dsp.force_renderer_reload()` — dpms-on alone
can issue a commit with no real CRTC transition (crtc=NULL skips
drm_panel_prepare; long-standing compositor-side hole).
**Gotcha fixed 2026-07-10:** the script called `forceRendererReload()`,
which the Lua Hyprland renamed to `force_renderer_reload()` — every
power-button wake silently no-op'd. Dispatcher names must be verified
against the running Hyprland, errors surface only in the hyprctl reply.
- greetd → `start-hyprland` as initial session; the boot chain
fbcon → greetd → Hyprland does one panel takeover, warm.
## Diagnostic quick kit
- Rails now: `sudo grep -E "^(dc08|de08):" /sys/kernel/debug/regmap/0-03/registers`
(80/a0 = up, 00/00 = SWIRE-dropped).
- Compositor truth: `grim` screenshot — separates "compositor renders" from
"glass shows" (they diverged for two days; the screenshot was pixel-perfect
while the glass was black).
- DPMS from SSH:
`HYPRLAND_INSTANCE_SIGNATURE=$(ls /run/user/1000/hypr/) XDG_RUNTIME_DIR=/run/user/1000 hyprctl dispatch 'hl.dsp.dpms("off"|"on")'`.
- drm.debug=0x1ff wraps the dmesg ring in ~130ms of transition traffic —
use `journalctl -k` or a lower mask for transition captures.
- Boot-time `DSI PLL lock failed` + `already disabled/unprepared` clk
warnings at ~0.4s: probe-time deferred-probe noise, once per boot,
benign-but-ugly (error path double-disables a clk). Not a transition bug.
## Known-good fallback
`../staging/deploy-139aa/boot-139aa7748181-pmoschain.img` — pre-SWIRE-era
kernel (stmfts touch, warm-cycle display of that era). Module tree already
on the phone. Prefer fixing forward on the blueline tip.

166
saf/device/edge-sense.md Normal file
View file

@ -0,0 +1,166 @@
# PAF · Active Edge (squeeze)
The Pixel 3's squeeze gesture. **Working on Arch as of 2026-07-28** — force is
detected, and the gesture reaches the shell through the session authority.
Spine doc: current truth, updated in place. The narrative of how it was found
lives in `../../SouveraineOS/docs/tasks/13-active-edge-squeeze.md`, which keeps
each superseded conclusion verbatim beneath its correction.
## The hardware chain
Six strain gauges bonded to the frame → **MAX11261** ADC on the SLPI's own I²C
bus → the `max11259` SEE driver on the sensor island → published over SSC as the
data type **`elmyra_raw`**, SUID `15717249801838747009 / 6291402259395006624`.
Not evdev, not a GPIO key. There is no input node and nothing bridges the SUID
to one; the only way to it is an SSC client. `sns_touch_gesture` is a **different
sensor** — the FTS double-tap nanoapp on the touch controller (slave 0x49, bus 5,
irq 125). Opening that one and squeezing correctly produces nothing.
**Power: PM8998 GPIO 2**, gpiochip0 line 1, held by a devicetree `gpio-hog`
(`edge_sense_pwr_hog` in `sdm845-google-common.dtsi`). Confirm with:
sudo gpioinfo | sed -n '3p'
# line 1: unnamed output consumer="edge-sense"
## Why it did not work before, and what fixed it
Through 2026-07-27 the gauges converted and drifted but **ignored force**: 399
samples across 35 s of hard squeezes gave a median deflection of 49 and a max of
127, against a resting noise floor of ~50. The conclusion at the time was that
the analog front end is only configurable from inside the `elmyra` CHRE nanoapp
via `ashConfigureElmyraFilters`, and that an SSC client gets whatever the AFE
defaults to.
On 2026-07-28 the **same unchanged binary** with **no nanoapp loaded** measured
deflections of 25022990 under real squeezes.
What changed between the two measurements is the rail. It became a DT
`gpio-hog` (`318f7d0`) instead of a `gpioset` unit that ran during boot. The
likely mechanism is **ordering, not voltage**: the hog asserts the rail at
kernel probe, before SSC enumerates the MAX11261, where the unit powered it
after the ADC had already been probed unpowered. An AFE initialised without its
bridge excited stays wrong until reset.
That is the leading explanation and it is **not proven**. Confirming it means
booting a pre-hog kernel and re-measuring. The electrical prediction made at the
time — that a pinctrl node changes nothing, since PM8998 GPIO 2 reads `vin-0`
on both stacks because neither selects one — still stands and was never the
issue.
## Calibration is real and per-unit
`/persist/sensors/elmyra/calibration.{0..5}`, factory-written 2019, one file per
gauge:
touch_2_sensitivity: 0.2402884277733113
This unit: `0.240 0.295 0.176 0.359 0.329 0.342`. They normalise the gauges
against each other; the spread is why Google ships a factory calibration step
(`edgesense_roller_cal/`) at all. The producer reads them at startup and logs
them — if that line shows six `1.000`s, the persist partition is not mounted or
the files are gone, and detection will be lopsided rather than absent.
These are consumed on the **AP side** by our producer. Nothing serves them over
HexagonFS: `hexagonrpcd`'s virtual `/persist/sensors/` contains only `registry`
(`rpcd_builder.c`). That matters only if the nanoapp path is ever taken.
## The threshold model
From Google's own sweep, `edgesense_roller_cal/0_raw/sensor_data.csv`, which
captures the gauges at a known 10 N and 20 N — the loaded bank moves 4002000
counts per 10 N step. Raw values sit near ±100000, so an absolute threshold
against them is meaningless; the first version compared exactly that and never
fired.
- `DEFAULT_THRESHOLD 2500` — summed normalised deflection from a rolling baseline
- `BASELINE_ALPHA 0.02` — the baseline must follow temperature and grip drift,
and is frozen while a squeeze is held or the release reads as a squeeze the
other way
- `RELEASE_FRACTION 0.6` — hysteresis, same reason proximity has it
Observed squeezes land at 25022990, which is close to the line. The binary
takes the threshold as an argument, so tuning needs no rebuild.
## It reports; it does not decide
The producer does **not** call the shell. It asks `souveraine-sessiond` first:
{"op":"input","trigger":"squeeze"} → $XDG_RUNTIME_DIR/souveraine/sessiond.sock
and delivers `qs -c souveraine ipc call gesture squeeze` only on `ok:true`.
sessiond treats a squeeze as real user input — it resets the idle budget and
lands in the forensic trail — but applies the pocket veto first: a covered
proximity sensor refuses it with `refused_by_state`, exactly as it already
refused tap-to-wake, because a phone in a tight pocket *is* a squeezed chassis.
Calling the shell directly, which is what it did first, made this an eighth
blind actor: no idle reset, nothing in the trail, no veto, and no way for the
machine to notice the source dying.
**On an unreachable authority it fails closed** and says so. A squeeze that
cannot be checked is not delivered; the alternative would mean the pocket veto
silently ceases to exist every time sessiond restarts.
What a squeeze *means* is the shell's routing table (`services/Gestures.qml`,
`squeezeAction`, default `dial``dial toggle`), never this producer's business.
## Packaging
`pkgs/blueline-edge-sense``blueline-edge-sense 0.2.0-1`, aarch64, built by
`build.sh` on archdev. Ships the binary, the user unit, and a
`souveraine-sessiond.service.wants` symlink so it is **enabled by default** and
survives a reprovision — the reader wants the authority it reports to, not the
session at large. It hung off `graphical-session.target` until 2026-07-29,
which started nothing then; the target is live as of TASK-76, but sessiond
remains the correct thing to want.
Before 2026-07-28 the binary sat unowned in `/usr/bin` since 07-26 — one of
TASK-25's 5,624. Installing the package hit that exact conflict; the old copy is
at `/var/backups/unowned-preupgrade/blueline-edge-sense.20260728`.
## Known gaps
- **No detection while the AP sleeps.** This is an AP-side process sampling six
channels at 100 Hz forever, which is precisely what `DozeDeep` exists to kill.
Squeeze-to-wake from deep doze is only possible from the DSP side — the
nanoapp. Grip and doze are currently mutually exclusive and nothing in the
code knows it.
- **Our detector, not Google's.** `elmyra.so` ships `AggregateDetector`,
`DetectorSetup`, `Filter`, `decision_threshold_min/max`, `global_sensitivity`
and a `GESTURE_PROGRESS` stream that would give hold-vs-tap. Ours is a
baseline-delta threshold firing 2 counts above its own line.
- **No haptic confirmation from the right place.** `elmyra_haptics_control` and
`haptic_gpio 88` say the buzz is the nanoapp's job downstream; ours is the
shell guessing.
- **Nothing proves which binary is reporting.** sessiond takes an `input` from
whoever connects to the socket, and the shell takes a gesture from whoever
calls its IPC — both demonstrated from an SSH shell on 2026-07-28. TASK-41.
- **Inference posture unresolved.** TASK-40 designates the raw 100 Hz six-gauge
stream a bring-up instrument, not a product surface: it is a typing
side-channel and a grip biometric. Enabled anyway by Casey's call, because the
exposure is not created by this reader — any AP process linking libssc can
open `elmyra_raw` today. Confining *that* is TASK-40's half.
## Where the code is
| What | Where |
|---|---|
| Producer | `pkgs/blueline-edge-sense/edge-sense.c` |
| Package + unit | `pkgs/blueline-edge-sense/{PKGBUILD,build.sh,*.service}` |
| Rail (gpio-hog) | kernel `sdm845-google-common.dtsi`, `edge_sense_pwr_hog` |
| libssc fixes | `pkgs/libssc-souveraine` — single-output sensors, fastest advertised rate (1 Hz → 100 Hz) |
| Authority side | souveraine `src/sessiond/{protocol,device_state,server}.rs``InputTrigger::Squeeze`, `suppress_wake`, `note_input_gated` |
| Routing | souveraine `surfaces/quickshell/services/Gestures.qml` |
| Nanoapp host | `pkgs/hexagonrpc` (chrecd) — loads `elmyra`, sends `RECOGNIZER_START` |
## Verifying it live
systemctl --user status blueline-edge-sense
journalctl --user -u blueline-edge-sense -f
A healthy start logs the six sensitivities, the SSC open, and a six-value
baseline. A squeeze logs `SQUEEZE (deflection N)` then either `squeeze
delivered` or the authority's refusal — and a refusal is normal operation, not
a fault.

View file

@ -0,0 +1,51 @@
# PAF — Pixel 3 (blueline) pmOS Architecture Framework
The single living spine for this port. Replaces the accreting handoffs/findings: instead of
appending a new dated doc each session, **update the relevant layer in place.** The spine:
- **[modem.md](modem.md)** — the modem bring-up stack as layers, status per layer, the
eliminated-leads ledger (don't re-chase these), and the one live frontier.
- **[platform.md](platform.md)** — device hardware map, boot/storage architecture, how to
reach the phone, recovery procedures, and the hard hardware lessons.
- **[build.md](build.md)** — how the kernel + device package are actually built with
pmbootstrap, the two-package split, and the build-system gotchas that have each cost a session.
- **[slpi.md](slpi.md)** — SLPI sensor island bring-up and the `sar.cc:27` crash-loop fix.
- **[secure_load_tz_frontier.md](secure_load_tz_frontier.md)** — 🔴 SUPERSEDED: secure-load is
resolved (modem loads clean via DELTA-1). Historical 22/fastrpc_mem record; the live frontier is
the firmware OFFLINE/52 park — see `modem.md` + `diag_capture.md`.
- **[README.md](README.md)** — this index.
Deeper/dated capture docs (`modem_*`, `qcril_*`, `pmos_fix_plan.md`, `diag_capture.md`, …) sit
alongside as evidence; the spine above is what you read first. Sibling docs kept outside the PAF
on purpose: `../README.md` (the generic port guide) and `../SIDE-TASKS.md` (parked non-modem work).
## Status at a glance (2026-07-01)
| Layer | State |
|---|---|
| Firmware load (MBA→MPSS) | 🟢 healthy — loads clean, **no 22 on the modem path** (DELTA-1 `c2923cb49`) |
| Power (rpmhpd cx/mx/mss → mss.lvl) | 🟢 healthy, proven equivalent to Android |
| QMI / QRTR transport | 🟢 healthy |
| rmtfs / tqftpserv | 🟢 healthy (rmtfs in `-P -s` rw partition mode) |
| pd-mapper | 🟢 fixed live (2026-06-29 late). `CONFIG_QCOM_PD_MAPPER=m` is in the flashed kernel and `qcom_pd_mapper` loads. This restored WLAN PD advertisement and `ath10k_snoc` probe; `wlan0` exists. |
| SIM / UIM provisioning | 🟢 healthy (usim ready, Primary-GW slot1/app1) |
| PDC MCFG autoselect | 🟡 works (configs go Active) — **NOT the gate** (modem parks OFFLINE regardless) |
| SLPI sensor island (`sar.cc:27`) | 🟠 real firmware crash, still unfixed — **but now cleanly bounded and PROVEN independent of the modem 52 wall** (2026-07-01, three live boots: crashed continuously / running / cleanly parked all gave byte-identical `DeviceNotReady`). Ordering + crash-recovery both fixed (see `slpi.md`); the sensor island itself is out of scope for modem bring-up now. |
| **RF arm (`Set Operating Mode=online`)** | **🔴 BLOCKED — offline/mode 0/52. THE frontier; proven kernel- and SLPI-independent.** `dms-ui-ready.py` (DMS 0x0064, the one load-bearing QCRIL pre-online step never replayed) built 2026-07-01, untested on hardware — next pmOS boot is the test. |
| Data / voice (IPA, MM, calls) | gated behind RF arm |
**One-line state:** the modem MSS loads clean, enumerates fully (IMEI/SIM/firmware all read
reliably), and the phone is stable on 7.1.1 with WiFi working. The 52 wall is the modem's
firmware-internal RF-init decision (pmOS parks op-mode 3 OFFLINE vs Android's op-mode 1
LOW_POWER) — now **proven independent of SLPI** with hard repeated evidence, closing out the prior
"make SLPI healthy first" ranked move as a dead end for this specific blocker. The live frontier is
`dms-ui-ready.py`: the one QCRIL pre-online QMI step (disassembly-confirmed load-bearing) that pmOS
has built but never fired on real hardware yet.
**Build/packaging milestone (2026-06-22):** all fixes unified into one **7.1.1** kernel tree as
commits (not scattered patches), built via pmbootstrap; the PDC self-provisioner
(`qcril-autoselect`) moved out of hand-deployed `tools/` glue and **into the
`device-google-blueline` package**, auto-enabled via the systemd preset and ordered
`After=rmtfs` / `Before=ModemManager`. One piece still hand-built: the openimsd **libqmi fork**
(PDC Set Feature) is not yet its own aport — see [build.md](build.md). This is "how the PAF
ought to look": kernel fixes in-tree, provisioner in the device package, nothing scp'd by hand.

View file

@ -0,0 +1,20 @@
# PAF evidence
Dated captures, handoffs, and archaeology live here. They preserve how a
conclusion was reached; they do not compete with the layer docs one directory
up. When evidence changes the answer, update the layer doc and leave the dated
record legible as history.
- `audio/` — microphone, PipeWire, and call-audio handoffs from 2026-07-19.
Current authority: `../audio.md`.
- `power/` — charge-control implementation handoff. Current authority:
`../platform.md` and TASK-08.
- The remaining files preserve pmOS, modem/QCRIL, TrustZone, provisioning, and
diagnostic research cited by the living spine.
Recovery anchors for the consolidation:
- SouveraineOS `72aaa53` — first copied PAF tree, 2026-07-24.
- Pixel3Arch `9b1afd1` — last complete PAF tree before its retirement.
- Pixel3Arch tree `27350cd5f5bec5062f361cbbb329cf7a04efb2f2` — exact snapshot
promoted into SouveraineOS before reconciliation.

View file

@ -0,0 +1,74 @@
# PAF · Android modem bring-up — COMPLETE captured sequence (the working reference)
The full modem boot Android does, end to end, extracted from the real captures in
`android-capture-20260620/` (modem SSR full re-init -> ONLINE, and the cold online capture).
This is the ENTIRE process — not one diff. Each phase lists what Android does (with file:line)
and the pmOS gap. Android reaches **IN_SERVICE / LTE / Fido (302/720)** at the end; pmOS stalls
at phase 1.
Source files:
- `qcril-ssr-fullinit.txt` (66k lines, modem SSR -> full re-init)
- `qcril-online-capture.txt` (24k lines, cold bring-up to ONLINE)
- `android-radio-log.txt`, `qcril-ssr-TIMELINE.txt` (digest)
SIM: ICCID <redacted>, IMSI <redacted>, MCC/MNC 302/370 (Fido).
---
## PHASE 0 — kernel/firmware secure-load (pre-QMI; NOT in these AP logs)
The modem PIL loads MBA -> PBL -> MPSS with the TZ memory-assign + XPU unlock. Android passes this.
pmOS is where the **-22 / XPU violation** lives — see `secure_load_tz_frontier.md`. NOTE: downstream
(`peripheral-loader.c:334` `pil_assign_mem_to_subsys`) makes the **same** HLOS->subsys_vmid RW
hyp_assign that mainline `qcom_q6v5_mss.c:463` does — the assign is load-bearing, the skip was wrong.
## PHASE 1 — firmware comes up at op-mode 1 = LOW_POWER ← THE DIVERGENCE
- Android: `qcril_qmi_nas_query_cur_power_state: known modem operating mode 1` then
`dms_handle_modem_operating_mode: modem's operating mode needs to be changed`
(`qcril-online-capture.txt:90,96`), and the DMS event report shows `oprt 1 / 0`
(`qcril-online-capture.txt`, qcril_qmi_nas.cpp:6562).
- **pmOS: modem comes up at op-mode 3 = OFFLINE** (`tools/modem-online-provisioner/RESULTS.md`).
OFFLINE->online = **52 DeviceNotReady**; low_power->online would be a legal transition.
- QMI DMS operating-mode enum: 0=ONLINE 1=LOW_POWER 2=FACTORY 3=OFFLINE 4=RESET 5=SHUTDOWN
6=PERSISTENT_LOW_POWER.
- **The real question is NOT "how to force OFFLINE->online" (it can't) — it is "why does our
modem boot to OFFLINE when Android's boots to LOW_POWER".** OFFLINE is the firmware's
"something-wrong / not-provisioned" state. Prime suspects: phase-0 secure-load health (degraded
boot), or EFS/rmtfs provisioning (modem can't read/write its NV -> falls to OFFLINE).
## PHASE 2 — UIM / SIM card init (Android: OK)
- `card_state 3` (present), app `state 5` (READY), app `type 2`=USIM / `type 5`=ISIM
(`qcril-online-capture.txt:336-337`). SIM ICCID/IMSI read fine.
- pmOS gap: untested past phase 1 — modem must be online first.
## PHASE 3 — PDC: select carrier MBN config (per SIM slot) ← pmOS likely missing
- Android: `qcril_qmi_pdc_get_selected_mbn_config_by_sim_slot` (`qcril-ssr-fullinit.txt:4308…`)
and `qcril_qmi_pdc_get_selection_carrier: cnv is valid, cnv=[72]` (`qcril-online-capture.txt:9367`,
`qcril_qmi_nas: prop_cnv = 72`). cnv 72 = the Fido carrier config.
- This is the carrier/modem config (MBN) activation. pmOS has no RIL doing it; the planned
replacement is the PDC self-provisioner `qcril-autoselect.py` (see `build.md`,
`device-google-blueline`). Without a selected config the modem may stay inert on RF.
## PHASE 4 — drive operating mode -> ONLINE
- Android: NAS decides "needs to be changed" (phase 1) and sets the modem ONLINE; subsequent
state shows `is_online 1` (`qcril-online-capture.txt`, qcril_qmi_nas.cpp:26131).
- pmOS: this is exactly the `--dms-set-operating-mode=online` we fire — returns **52** because we
start from OFFLINE (phase 1), not LOW_POWER.
## PHASE 5 — NAS attach / network registration (full QMI choreography)
- Module/QMI send order (from `qcril-ssr-TIMELINE.txt` section A): Nas, Pdc, Voice, DSD, Auth, WDS,
Pbm, Sms, Vs — dozens of NasModem sends, all rc=0. This is the post-online network attach.
## PHASE 6 — IN_SERVICE (the goal)
- `combinePsRegistrationStates: mVoiceRegState=0(IN_SERVICE), mDataRegState=0(IN_SERVICE) …
getRilVoiceRadioTechnology=14(LTE) … mOperatorAlphaLong=Fido` (`qcril-online-capture.txt:24110`).
- CS domain: VOICE,SMS,VIDEO; PS domain: DATA,MMS; registrationState=HOME; LTE EARFCN 2000, Fido
302/720 (`qcril-online-capture.txt:24107-24132`). IMS registered (`:24083`).
---
## What "the whole job" actually is (so it stops getting half-done)
1. **Phase 0** — fix the secure-load so the modem boots HEALTHY (the -22 / XPU). Not the skip.
2. **Phase 1** — get the modem to come up at LOW_POWER, not OFFLINE. Likely a consequence of (1)
and/or EFS/rmtfs provisioning. This is the current wall.
3. **Phase 3** — replicate Android's PDC MBN carrier-config selection (qcril-autoselect provisioner).
4. **Phase 4** — only then does `set-operating-mode=online` succeed instead of 52.
Doing (4) without (1)-(3) is why every "flash and test" returns 52. All four are required.

View file

@ -0,0 +1,225 @@
# HANDOFF — Put the Pixel 3 audio to bed (Fable, evening 2026-07-19)
> Historical handoff. The current contract is `../../audio.md`; this file is
> retained as dated evidence and may describe work that was later completed.
We are *this close*. Mic captures nonzero on every boot now, both loudspeakers
fire, modem connects. What's left: reinstate PipeWire (the reason it was banned
is gone), keep the mic/speaker gains, and make the phone **call-ready**
(two-way call audio). This doc is your orders, not a draft to critique.
---
## ⚠️ READ THIS FIRST — the references are ALL local, and they are VAST
Casey's #1 instruction for this handoff: **stop and internalize that nearly
every fact you could want is already on this drive** — every bring-up attempt,
every Android LIVE capture, every modem trace, every regmap dump, the full
Android vendor mixer map, the pmOS known-good config, the raw partition images.
Even the outgoing session (me) *barely* knew this reference surface existed and
wasted hours re-deriving things that were sitting in a file. **Do not repeat
that.** Before you form any theory or run any experiment, grep/read the local
references. If you catch yourself reverse-engineering ALSA/UCM/modem internals
from scratch, you have already failed — go find the file.
### The reference surface (4 repos, ~70 GB, all local)
**1. `~/Projects/Pixel3Arch/PAF/` — the then-current base of truth.** Doctrine + current state.
- `audio.md` — the entire audio saga. THE ALTERNATION LAW, THE BOOT RACE,
speaker routing provenance, mic transport. Read top-to-bottom.
- `audio-2026-07-19-LIVE-reconciliation.md` — **supersedes the top of
audio.md.** LIVE Android register captures (recorder vs voice call),
the ADM-usecase hypothesis, and the **airplane/radio-mode wedge warning**
(some historical "wedges" were mode toggles, not driver faults — rule
mode out before logging any wedge).
- `HANDOFF-mic-unpin.md` — the un-pin experiment (corrected header; read
audio.md "THE BOOT RACE" first).
- `modem.md`, `pmos_fix_plan.md`, `qcril_*_capture.md`,
`modem_dt_rf_analysis_*.md`, `tz_listener_archaeology.md`,
`secure_load_tz_frontier.md`, `qrtr_tools_reference.md`,
`tier1_los_vs_mainline_diff.md` — the **modem bring-up archaeology**.
Deep. Detailed. Read before touching the modem.
- `evidence/`, `captures/` — raw traces (e.g. glink).
**2. `~/Projects/PostMarketOS-Blueline/` — the pmOS port + all Android captures.**
- `android-reference/`**the gold.** `mixer_paths_tavil_b1.xml` (the FULL
Android vendor route map — every named route: `handset`, `speaker`,
`voice-handset`, `voice-speaker`, all mics), CS35L36 regmaps
(idle/playing), tavil PGD dumps, `wcd9xxx_core-android-LIVE-*.txt`
(recording AND voicecall, captured 2026-07-19), `wcd934x-android-ground-truth.md`.
- `android-capture-2026*` — nine per-day Android trace dirs.
- `PAF/` — a second full modem-trace tree (`android_modem_bringup_capture.md`,
`qcril_full_provisioning.md`, `qcril_online_sequence.md`,
`modem_consumer_chain_*.md`, `los-mss-translated.c`).
- `HANDOFF.md` (80 KB), `PROGRESS.md`, `START-HERE.md`, `README.md`.
**3. `~/Projects/pixel3-downstream-audio/` — the Android HAL source.**
`techpack/audio/` — downstream `q6adm.c`, `q6voice*.c`, ASoC machine driver.
This is what the working Android side actually runs; diff mainline behavior
against it when a route/topology question comes up.
**4. `~/pixel3-backup/` (in $HOME, not Projects) — raw partition dumps + pmOS config.**
`modem_a.img`, `persist.img`, `vendor_a.img`, `system_a.img` (raw Android),
plus `pmos-20260703/`**the known-good pmOS config backup** the speaker
routing was lifted from (see the memory note: this is the source of truth for
working modem/cellular + speaker config; restore from here, never guess).
If a route value, a mixer control, a modem sequence, or a UCM verb is in
question — it is decoded in one of the above. Cite `file:line`. Do not invent.
---
## CURRENT STATE (verified live this session, phone at WiFi `10.10.20.234`, USB `172.16.42.1`)
**Kernel:** `7.1.1-sdm845-gcd7758eaeca1` (mic-race-fix, HEAD). Boot-race fix
LIVE and working — `MICDBG boot sysclk hold armed` fires in dmesg, capture open
joins a live codec (`users=2 skip`) instead of racing bring-up.
**Mic — WORKING.** `parecord --device=blueline_mic` yields nonzero PCM every
boot. Deterministic now (kernel sysclk hold from `wcd934x_comp_probe`).
**Speakers — WORKING.** Both CS35L36 amps armed, correct route (measured live):
`Left/Right AMP Enable = on`, `BOOST = on`, `PCM Volume = 700`,
`Left Channel Mux = RX1` (top), `Right Channel Mux = RX2` (bottom). Matches the
07-12/07-13 known-good. The `handset` (earpiece) vs `speaker` distinction is a
**single control**: `Right AMP Enable` — Android's `handset` route runs the top
amp only (`R AMP Enable=0`, low gain); `speaker` runs both. Decoded from
`mixer_paths_tavil_b1.xml`.
**Modem — connects.** Registered (Fido, MNC 720). Call *signaling* works. Note:
saw a transient roaming reject (MNC 490, `roaming-not-allowed`) mid-session —
that's network, not us, and consistent with the reconciliation doc's mode/radio
caveat. Rule mode out before chasing modem "faults."
**Calls — NO AUDIO yet.** `q6voiced` is now `enabled + running` holding
`hw:0,2` (the live VoiceMMode1 PCM — NOT device 5 as the staged conf wrongly
said; fixed this session in both `/etc/q6voiced.conf` on-device and
`pkgs/q6voiced/q6voiced.conf`). But it logs `Failed to open tx/rx: Invalid
argument` on call-start because nothing applies the VoiceCall route. See below.
---
## THE TWO HOLDS ON THE MIC (know the difference)
1. **PA loopback pin** (`90-blueline-alsa.pa`, modules 20/21): null-sink +
`module-loopback source=blueline_mic ... source_dont_move sink_dont_move`.
Holds the mic open from boot. **Nobody else does this** — verified via GitHub
code search: no working sdm845 phone (OnePlus 6 enchilada/fajita, holioka)
pins the mic. It's a boot-race workaround.
2. **Kernel sysclk hold** (today's fix, in `wcd934x_comp_probe`): permanent
sysclk ref + MICB2 bias from probe. Honest hack — pins the winning-boot
state so every boot wins.
**With the kernel hold in place, the PA pin is very likely now redundant.** The
un-pin test was never run on a race-won boot. Running it de-hacks the daily
driver. See `HANDOFF-mic-unpin.md` for the exact procedure (unload loopback,
same-map reopen ×2, watch for `CHAN_CTRL remove ret=0` + fresh `DEF_ACT_CHAN`).
If reopen is nonzero → delete the PA pin, keep the kernel hold. If zero → keep
both, root-cause stays open.
---
## PIPEWIRE — the ban is lifted; bring it back
**Why it was removed (`da53133`):** the *only* documented reason was the
**premature capture source** — wireplumber published the mic before ALSA
capture was nonzero (the boot race). PAF audio.md:516/590. PipeWire *playback*
was never the problem — `8920c3c` had PW playing on glass fine once the sink
was pinned S16LE + RW + no-mmap (the DSP lies about S24/mmap; both play silent).
**Why it's safe now:** the boot race is fixed in-kernel — **capture is nonzero
from boot, deterministically.** The precondition PipeWire failed on is
satisfied. Casey has explicitly cleared reinstating it.
### PipeWire task
- Reinstall the full PipeWire + WirePlumber stack. Note the current shims:
`pkgs/pipewire-abi-provider`, `pkgs/pulse-native-provider` (they provide the
PW ABI *without* a server — you're replacing that arrangement with a real
server, so reconcile/remove the shims cleanly, don't leave a half state).
- **Re-apply the S16LE playback rule** (the wireplumber
`51-blueline-audio-s16.conf` that `8920c3c` created and `da53133` deleted —
recover it from git: `git show 8920c3c`). Without it, playback is silent.
- Verify on the fixed kernel, in this order, and DO NOT proceed past a failure:
1. mic still nonzero via PW (`pw-record`) — this is the thing PW broke before;
2. speakers play via PW (`pw-play` a chime) — both top+bottom;
3. the mic source is NOT published dead at boot (the old failure mode).
- Keep the new features intact: the QS audio service fix
(`Audio.qml``blueline_mic` description matcher), the mic/speaker gains,
the speaker Channel Mux routing.
**Ponytail guard:** PipeWire is the bigger hammer. Its payoff is that
**callaudiod can then route calls via UCM** — which is the whole reason we want
it back. If PW re-breaks the mic despite the race fix, fall back to the raw-PA
call hook (below) rather than fighting it — the mic is non-negotiable.
---
## MAKE IT CALL-READY (the actual finish line)
The working sdm845 reference (OnePlus 6, `chayleaf/dotfiles`
`system/hardware/oneplus-enchilada/default.nix`, and `syntheit/nix`
`hosts/fajita`) routes call audio through **UCM (consumed by PipeWire) +
callaudiod + q6voiced tied to ModemManager**. Key facts pulled from it:
- `q6voiced hw:0,6` there (per-board PCM number; **ours is `hw:0,2`** — always
read `/proc/asound/pcm`, never copy the number).
- q6voiced is `After=`/`wantedBy=ModemManager.service`.
- `alsa-ucm-conf` is shipped and `alsa-ucm-meld = true` — **UCM is the routing
layer.** callaudiod flips the verb on call state; q6voiced carries the audio.
### The call chain we need
`gnome-calls → callaudiod → UCM VoiceCall verb → q6voiced carries modem audio`
Both middle links are currently broken/absent on our raw-PA setup:
- **callaudiod** is installed and running but has zero log activity — it routes
via PipeWire ports/profiles, which don't exist under raw-PA. **PipeWire back
gives callaudiod something to route with.** This is the clean path and the
reason PipeWire is worth the risk.
- **UCM VoiceCall verb** exists (`Google/blueline/VoiceCall.conf`) but has the
**same `hw:0,5` bug** (`PlaybackPCM/CapturePCM "hw:${CardId},5"`) — fix to
`,2`. Its EnableSequence is the real voice route:
`SLIMBUS_0_RX Voice Mixer VoiceMMode1=1`,
`VoiceMMode1 Capture Mixer SLIMBUS_0_TX=1`. Cross-check the RX/TX and DMIC/DEC
selections against `mixer_paths_tavil_b1.xml` `voice-handset`/`voice-speaker`
and the downstream `q6voice*.c` before trusting them.
- **UCM currently fails to load via `alsaucm -c G3`** (`-2`). Under raw-PA this
is moot (PA doesn't consume UCM). Once PipeWire+wireplumber drive UCM this
must actually resolve — verify wireplumber opens the card's UCM cleanly.
(The card driver/id: `id=G3`, longname `Google Pixel 3`, ASoC machine
`msm-snd-sdm845`; conf.d lives in `conf.d/sdm845/`. If UCM won't match, this
driver-name-vs-dir mapping is the first suspect — but let a real UCM client,
not the bare CLI, be your test.)
### Definition of done — ALL MET 2026-07-29
1. PipeWire back, mic + both speakers verified on the fixed kernel. **Done
2026-07-19.**
2. Place a real call (Casey → his wife). **Two-way audio.** Downlink out the
speaker, uplink from the mic, on the call. **Done 2026-07-29** — incoming
call answered, both directions confirmed by ear at both ends. Five defects
had to fall first; see `PAF/audio.md`, 2026-07-29.
3. On hangup, audio reverts to normal HiFi (no stuck voice route). **Done**
and note the reverse was silently broken: the mic read exact zeros after
*every* call until the outgoing capture leg was unmapped explicitly.
4. q6voiced device number correct (`hw:0,2`) in every place it's written.
**Done.**
5. New features + gains preserved. Mic still nonzero every boot. **Done**
~90k nonzero frames verified repeatedly across the session.
Not in the original list, completed 2026-08-02: the earpiece/speaker executor
(`blueline-callaudio-hook speaker on|off`) is driven by the patched
`souveraine-callaudiod`; see `../../audio.md` and TASK-39.
### Fallback (if PipeWire re-breaks the mic)
Raw-PA call hook: a small ModemManager call-state trigger that applies the
VoiceCall mixer controls directly via `amixer`/`alsaucm cset` on call-start and
reverts on hangup. ~15 lines. No server swap, zero risk to the working mic.
Smaller diff, fewer niceties. Only take this if PipeWire loses the mic.
---
## RULES OF ENGAGEMENT
- Base of truth = PAF. Every reference is local (the 4 repos above). **Read
before theorizing.** Cite `file:line` or say "not in the code."
- Rule out airplane/radio mode before logging any modem/audio "wedge."
- The mic is the crown jewel. Never trade it away for a nicety.
- Verify claims live on the phone; don't assert kernel/route state from memory.
- The doctrine holds: build the finished thing, not a v1. This is the last mile.

View file

@ -0,0 +1,114 @@
# Handoff — Pixel 3 mic: un-pin the microphone
> Historical handoff. Current functional status and the remaining power debt
> live in `../../audio.md` and `../../../docs/tasks/07-mic-wcd9340-slim-tx.md`.
> **CORRECTED 2026-07-19 evening — read audio.md "THE BOOT RACE" first.**
> The QRTR=y contamination theory below is disproven: all of today's dead-mic
> boots (every kernel, every config) lost a boot-time sysclk race; the working
> 262k boot and one untested bdf098937 boot won it. bdf098937 is clean and
> deployed; the un-pin test below is still valid but ONLY on a boot whose
> pinned baseline reads nonzero. Cold boot until baseline is nonzero, then run
> the test as written.
## The one task
Fable got the mic **working** via a PA mic-hold that pins `blueline_mic` open
from boot (`rootfs-overlay/etc/pulse/default.pa.d/90-blueline-alsa.pa`, the
`module-loopback` into a null sink). The **only** job left: make the mic
survive close/reopen so the always-on pin can be removed. Nothing else. Do not
expand scope.
## Known-good state (trust this, do not re-derive)
- Working-mic kernel: **`323b29715`** (branch `mic-fix`, gitea
`Fimeg/linux-blueline`). MCLK root cause already fixed upstream of it
(LN_BB_CLK3, commit `7c6559e0c`). With the PA pin loaded, capture is real
audio: ~262k/263k nonzero samples/3s, verified this morning.
- The un-pin attempt: **`bdf098937`** = `323b29715` + one commit only
("slimbus: qcom-ngd: send CHAN_CTRL remove on stream disable"). Its parent
IS the working kernel, so `bdf098937` is exactly "working mic + un-pin,
nothing else."
## What `bdf098937` does (already reviewed, wire format is correct)
- `drivers/slimbus/qcom-ngd-ctrl.c`: adds `qcom_slim_ngd_disable_stream`
sends `CHAN_CTRL(REMOVE)` + `RECONFIG_NOW` to the ADSP master on stream
disable. Mainline had dropped all 0x400x5F reconfig messages, so the master
kept the channel "active" after close and a same-map reopen no-op'd → zombie
zeros. Wire format matches downstream `slim-msm-ngd.c` exactly:
`(SLIM_CH_REMOVE<<6)|(laddr&0x1f)`, TID, chan ids, `mc=SLIM_USR_MC_CHAN_CTRL`.
- `sound/soc/codecs/wcd934x.c`: swaps teardown order to `disable` **before**
`unprepare` (unprepare frees `stream->ports`, which made disable a silent
`-EINVAL` no-op).
- On-phone evidence it reached the wire: `MICDBG CHAN_CTRL remove ... ret=0`,
GENERIC_ACK code 0x20. So the removal is sent and ACKed.
## The actual open question
`bdf098937` has **never had a clean test.** Every observation today was
contaminated:
1. Tested with the PA pin still loaded → `disable` never fires (0 shutdowns in
dmesg), so the new code path never ran.
2. Then it got built ON TOP of a QRTR/PD_MAPPER `=y` config change into
`fdb22e5` (see contamination below), and every "still zero" result was on
that kernel, not on clean `bdf098937`.
So it is unknown whether the CHAN_CTRL-remove actually un-pins the mic. That is
the thing to find out.
## Contamination to be aware of (my mess — Opus, this session)
- Commit `fdb22e5` on `mic-fix` flips `CONFIG_QRTR=y` + `CONFIG_QCOM_PD_MAPPER=y`
(was `=m`/`=m`). This is a **boot-time change** stacked on top of the un-pin
commit and it correlates with the mic going dead. Whether QRTR=y is causally
breaking the mic or is a red herring is NOT established — but it must NOT be
in the kernel used to test the un-pin. Ruled out as a probe-race: on
`fdb22e5`, audio_pd came UP at 1.3s, codec armed capture at 13.5s (12s later,
no race). `0x1fffffff` = SERVREG_SERVICE_STATE_UP (I first misread it as
down — it is UP).
- The repo config `kernel/config-blueline.aarch64` currently has QRTR/PD_MAPPER
`=y` (my commit `f245323` in `Fimeg/Pixel3Arch`). For a clean un-pin test the
build config must be QRTR=m/PD_MAPPER=m (the working-mic config).
- Phone is currently running the contaminated `gfdb22e5`; mic reads zero.
- ArchDev `~/build/pixel3arch-kernel` was hand-abused with direct `sed` +
`makepkg` (NOT the pipeline). Ignore that dir. `config-blueline.aarch64`
there was left at `=y`.
## The correct next step (build ONE kernel, via the pipeline)
Build `bdf098937` with the working-mic config (QRTR=m, PD_MAPPER=m), clean, and
test the un-pin. Concretely:
1. Point the build at commit `bdf098937` (its config, in `Fimeg/Pixel3Arch`,
must be QRTR=m/PD_MAPPER=m — the pre-`f245323` values).
2. Build through the real pipeline only: `~/Projects/Pixel3Arch/kernel` on
ArchDev, `./build.sh <branch-at-bdf098937>` (see PAF/build.md — it
fresh-clones from gitea and hard-asserts built hash == gitea tip; never
hand-makepkg, never the `~/build` side dir).
3. Deploy with `scripts/deploy-kernel.sh` (modules-first, refuses to bless a
boot where `uname -r` != installed module tree — PAF/build.md doctrine).
4. Cold boot (true power-off, unplug — no warm reboot; warm reboots poison
TZ/remoteproc on blueline).
## The test that answers it
On the clean `bdf098937` boot:
1. Confirm mic works pinned: `parecord --device=blueline_mic` → nonzero
(baseline sanity; pin is loaded from boot).
2. `pactl unload-module module-loopback` (drop the pin → the source can now
actually close).
3. `parecord` again, close it, `parecord` a second time on the SAME TX5 map.
4. Watch `dmesg | grep MICDBG` for `shutdown` + `CHAN_CTRL remove ret=0` on the
close, then a fresh `DEF_ACT_CHAN` on the reopen.
5. **Success = the second/third reopen reads nonzero.** That means CHAN_CTRL
remove re-armed the path and the pin can be deleted from
`90-blueline-alsa.pa`.
6. If it still zeros: the un-pin theory needs more than channel-remove. Next
thing to check is whether the **port** (not just the channel) needs
explicit disconnect/reconnect — downstream `slim-msm-ngd.c` handles
`DISCONNECT_PORT` (mainline does it in `slim_stream_unprepare` via
`slim_disconnect_port`); verify it actually fires on close in dmesg, and
whether a port re-CONNECT is needed on reopen. Downstream source is local
at `~/Projects/pixel3-downstream-audio/drivers/slimbus/slim-msm-ngd.c`
(git: `git show HEAD:drivers/slimbus/slim-msm-ngd.c`).
## Do NOT
- Do not roll back / chase old commits beyond `bdf098937`.
- Do not remove QRTR/PD_MAPPER as the permanent config — pd-mapper being
built-in is wanted; it just must not be in the *un-pin test* kernel.
- Do not hand-`sed` configs on ArchDev or hand-`makepkg`. One change, in the
repo, pushed, built by `build.sh`.
- Do not tear down the working pin except as step 2 of the test above.

View file

@ -0,0 +1,210 @@
# 2026-07-19 LIVE Android reconciliation — READ THIS FIRST (supersedes the top of audio.md)
> Historical capture. The reconciled current contract is `../../audio.md`.
The `## 0x097/0x032/0x11c are 00 on a WORKING recording` section currently at the
top of `audio.md` is **partly WRONG** — it was written after reading only the
regmap-cache node. A second live read minutes later (same confirmed-active
recording, user watching the phone) via the live-PGD node corrected it. This file
is the accurate version. Trust this, not that section.
## ⭐ VOICE-CALL reconciliation — 0x097 is voice-usecase-specific (the biggest finding)
Captured the working Android side LIVE via the live node
(`/sys/kernel/debug/wcd9xxx_core/slimslave_reg_dump`) under TWO different working
usecases, both user-confirmed-live (timer / wife on the call):
```
recorder capture VOICE CALL
0x032 irq_en_TX 0x80 0x80
0x057 TX7 port cfg 0x05 0x05
0x097 TX7 region 00 0x21 <-- voice ONLY
0x098 TX7 region 00 0x21 <-- voice ONLY
0x11c TX7 MULTI_CHNL 0x80 0x80
0x120 TX8 MULTI_CHNL 00 0x80 <-- voice ONLY (2nd mic, dual-mic)
```
Saved: `android-reference/wcd9xxx_core-android-LIVE-voicecall-20260719.txt`
(plus `...-LIVE-recording-20260719.txt`).
**THE RECONCILIATION:** the 2026-07-03 ground-truth doc's `0x097=0x21` was RIGHT
— it was captured during a **voice call** (dual-mic handset+endfire, TX7+TX8), not
a multimedia recorder capture. The recorder path never sets `0x097/0x098` or TX8's
`0x120`. So `0x097` was never a "samples flow for multimedia capture" register —
it's set by the **voice usecase's** ADM topology (dual-mic, AANC/fluence).
**Chasing `0x097` for the recorder/multimedia path was a category error.**
This is **strong evidence for the ADM/usecase hypothesis** (see below): the working
Android side programs different DSP topology per usecase (recorder = single TX7
multimedia; voice = dual TX7+TX8 with AANC). `0x097/0x098` + TX8 `0x120` are the
voice-topology footprint.
**For our broken multimedia mic:** our `MultiMedia1/2` capture on Arch corresponds
to Android's **recorder** path. For THAT path the working registers are
`0x032=0x80, 0x057=0x05, 0x11c=0x80` only — which we MATCH. So the multimedia-
capture register layer genuinely matches working↔broken. The bug for multimedia is
NOT in IFD/PGD registers (now confirmed for the recorder usecase specifically).
The voice path (`VoiceMMode1`/`hw:0,2`) is a separate, untested question — and if
voice is what's broken, `0x097/0x098/0x120` become relevant again, but as voice
topology, not as a general "samples flow" reg.
Do NOT chase `0x097` for the multimedia/recorder mic.
## What actually happened
Two debug nodes on Android (LineageOS 22, slot B) read **LIVE during a
confirmed-active recording** (LineageOS Recorder, timer counting up, user at the
phone said "recording"):
```
regmap/tavil-slim-pgd/registers wcd9xxx_core/slimslave_reg_dump
(regmap CACHE — reads 00/stale) (live PGD read over SLIMbus)
0x032 PGD irq_en_TX 00 0x80
0x057 TX7 port cfg 00 0x05
0x097 "mystery" TX7 reg 00 00
0x11c TX7 MULTI_CHNL_0 00 0x80
```
**The two nodes read different things.** `regmap/.../registers` reads the regmap
CACHE (all 00, not live). `wcd9xxx_core/slimslave_reg_dump` reads the live PGD and
shows the real recording values.
## What this means (correct conclusions)
1. **The original 2026-07-03 ground-truth doc was RIGHT about 0x032/0x057/0x11c**
(it used `wcd9xxx_core`, the live node): working recording = `0x032=0x80`,
`0x057=0x05`, `0x11c=0x80`. Those are real.
2. **The doc's `0x097=0x21` claim was RIGHT — but it was a VOICE call, not a
recorder capture.** See the ⭐ VOICE-CALL section above: `0x097/0x098=0x21` and
TX8 `0x120=0x80` are voice-usecase-specific. They are `00` during a recorder
capture (which is the usecase our multimedia mic maps to). So `0x097` is NOT a
multimedia-capture register at all — stop chasing it for the recorder path.
3. **Our side (Arch / SouveraineOS) matches the working side on
0x032/0x057/0x11c** (measured this session after the MCLK fix: our IFD shows
`0x032=0x80`, `0x057=0x05`, `0x11c=0x80`). So the IFD/PGD register layer
genuinely matches between working Android and our broken capture.
**The differentiator is NOT in the IFD/PGD register layer.**
4. **Methodological correction for our side:** when reading PGD regs, do NOT
trust `regmap/.../registers` (it's the cache — lies with 00s). Use the live
equivalent. Several "this reg is never set" conclusions on our side this
session were probably cache-read artifacts, not real states.
## Saved artifacts (both LIVE, same recording, 2026-07-19)
- `PostMarketOS-Blueline/android-reference/tavil-pgd-android-LIVE-recording-20260719.txt`
(regmap cache view — the misleading 00s)
- `PostMarketOS-Blueline/android-reference/wcd9xxx_core-android-LIVE-recording-20260719.txt`
(live PGD — the real recording state)
## Verified-correct stack as of 2026-07-19 (live-confirmed)
- MCLK to codec: FIXED (LN_BB_CLK2→LN_BB_CLK3 in `sdm845-google-common.dtsi`
`&wcd9340` override, uncommitted; live on DTB). eFuse sense completes
(`status=0x01`, version `0x401`) — no more `-110`.
- Codec init/capture regs: bit-for-bit identical to Android ground truth.
- Codec DAPM power-up: fires (`0x0601/0x0626/0x0aa1` toggle on capture).
- SLIMbus NGD DEF_ACT_CHAN: sent (qcom-ngd-ctrl.c:1020).
- AFE slave-config send (`AFE_PARAM_ID_CDC_SLIMBUS_SLAVE_CFG` + REG_PAGE +
REG_INIT): already built in mainline as `q6afe_send_cdc_codec_config()`
(q6afe.c:1854), sent with correct codec enum addr `ea=0x21702500100`, `ret=0`.
DSP accepts (no "DSP returned error"). **A prior session already built the
"port the missing slave-config" fix — do NOT re-derive/re-port it.**
- IFD/PGD regs (0x032/0x057/0x11c): MATCH between working and broken (via live
node). `0x097`: 00 on both — ruled out.
- ADSP file-serving: PD requests only `adsp_avs_config.acdb`, served. OEM ACDB
tree not requested via FastRPC — not the gap (falsified).
## Where the differentiator actually is (still open)
Everything the kernel can observe/send/serve — codec regs, SLIMbus NGD, AFE
command ack, IFD/PGD regs, file-serving — is identical between working Android
and our broken capture. The fault is past all of those.
**Top hypothesis (NOT yet tested) — ADM usecase routing, not AFE:**
On Android, opening the Recorder app goes through the Android audio HAL:
`adev_open_input_stream` → platform HAL → `audio_extn`**`adm_open` with a
usecase + device + flags**, which programs the ADM (Audio Dynamic Mixer) topology
on the DSP that actually routes capture data into the AFE port. Without an ADM
usecase the DSP has an open AFE port with nothing routed into it → samples never
flow, every command ACKs. That fits the exact signature.
**We are Arch / SouveraineOS — there is no Android audio HAL in our stack at
all.** We drive ALSA directly (PulseAudio → ALSA `hw:0,0`). No userspace ever
calls `adm_open`. No usecase is ever set. Either the kernel does it or it
doesn't — and mainline's `q6adm.c` is much thinner than downstream's
(`pixel3-downstream-audio/techpack/audio/dsp/q6adm.c`).
**This is the unexplored-at-this-depth difference, unlike the AFE slave-config
which was already ported.**
Concrete next move on our side: check whether our kernel/driver has a `q6adm`
path wired for capture usecases, and whether opening capture sends ANY
`ADM_CMD_ASM` / ADM commands at all. If zero — that's the gap.
Secondary candidates (also not falsified):
- **AFE/APR command sequence on the WIRE (payload bytes), not register state.**
No payload-bearing APR capture exists on either side. The one glink trace
(`PAF/captures/glink_trace_pmos-20260702.txt`) is framing-only AND modem-context
(qmi-proxy traffic), not a mic capture — no AFE opcodes. The Android side has
`msm_apr_debug` + `ipc_logging/apr` nodes but they returned empty even during a
live recording on this LOS build (APR packet logging not enabled in this image).
Getting a payload-bearing APR trace needs either a different LOS build with APR
debug enabled, or a kernel tracepoint patch on our side.
- On Android, APR debug nodes exist but were empty live: `/sys/kernel/debug/
msm_apr_debug` (0 bytes), `/sys/kernel/debug/ipc_logging/apr/{log,log_cont}`
(0 bytes even while recording). Glink lpass edge was moving real bytes
(TX/RX fifo nonzero) — traffic flows, just not logged at APR layer on this image.
- Android's silent/vibrate/sound/airplane modes are audio-policy/stream-mute
states, NOT hardware triggers — but they shape WHICH ADM usecase the HAL opens,
which is why the usecase-routing question matters.
## System identity (don't mislabel — this was called "pmOS" by mistake earlier)
- **Our phone runs Arch / SouveraineOS**, NOT pmOS. pmOS is only the historical
lineage of the kernel/DTB. The running userspace is Arch + SouveraineOS shell
+ PulseAudio driving ALSA directly. No Android audio HAL, no `audio_extn`.
- Android/LineageOS reference is slot B (boots fine; user was in it this session
recording). The mic works there.
## ⚠️ METHODOLOGICAL WARNING — some historical "wedges" were a real radio/mode switch, not a driver fault
Casey flagged 2026-07-19: **at least some of the recurring "wedge" / "seizure" /
"the modem grabbed the card" / "SLIM -110 wedge" / "capture wedge" / "chip-wide
wedge" events in the audio/mic/modem docs were NOT kernel/codec/SLIM faults at
all — they were the phone having switched to airplane mode (or an equivalent
radio-off / audio-policy-reroute state), and the resulting silence / dead bus /
NACK was misread as a driver-level hang.**
This means:
- Some "wedge" investigations across multiple sessions may have been chasing a
mode toggle, not a bug. Treat any historical "wedge" / "seizure" / "poison"
conclusion as suspect until reconciled against the mode/radio state at the time.
- We do NOT know exactly which wedges were real faults vs mode swaps. Both kinds
exist. Don't assume all wedges were phantom — and don't assume all were real.
- **BEFORE logging a wedge / NACK / seizure / dead-bus / silence on our side**,
check and record: is the phone in airplane mode? Is the modem/radio off (check
rfkill, modem state, ofono/ModemManager, the cellular/quickshell overlay)?
Is an audio policy rerouting happening? Rule the mode out FIRST, every time.
- On Arch/SouveraineOS a radio-off / airplane-equivalent state may be less
obvious than Android's UI toggle (a Souveraine service, the cellular overlay,
a quickshell switch, a default). It could be getting toggled accidentally or
by default, which would explain how it kept biting us unnamed.
- Relevant docs to revisit with this lens: `PAF/audio.md` ("SLIM wedge",
"chip-wide wedge", "TZ-poison"), `docs/mic-slim-findings-2026-07-17.md`
("the SLIM -110 wedge", "capture wedge", "TX7-contaminated wedge",
"status-read NACK → 1 Hz wedge loop"). Not all of these are phantom — but the
possibility that some are must be checked before re-investigating.
## Process note for the resuming session
- The codec/SLIM/AFE/file-serving layers are exhaustively ruled out (with the
wedge caveat above — if a "ruled out" result was taken during a mode-swap, it
is NOT valid). Do not re-investigate them UNLESS a prior test's mode state
wasn't recorded.
- Do NOT re-port the SLIMBUS_SLAVE_CONFIG send (already done, ret=0).
- Do NOT chase `0x097`.
- Top lead: ADM usecase routing — mainline q6adm vs downstream, and whether our
capture sends any ADM commands. We have no HAL to do it, so the kernel must.
- Secondary: payload-bearing APR/glink wire capture during a capture on BOTH
sides, then byte-diff. That artifact does not exist yet.
- **Record mode/radio state with every test going forward.** No more unattributed
"wedge" logs.
- Android access (airplanemode kills the NCM gadget): `adb` (device serial
89JX0AAJP), `adb shell 'su -c "..."'` (magisk root works). Our-side access:
ssh `~/.ssh/ani` to the USB gadget IP or WiFi `10.10.20.234`.

View file

@ -0,0 +1,150 @@
# PAF · Build & packaging (pmbootstrap, the proper path)
How the blueline kernel + device package are actually built and shipped. This
is the canonical build layer — earlier sessions wasted hours on host `make`
and on undocumented pmbootstrap behaviours. Build with **pmbootstrap**; it is
what every other sdm845-mainline contributor uses and what produces installable
APKs. Host `make ARCH=arm64 LLVM=1` is not the path (it needs llvm-readelf/nm/
objcopy on the host and does not produce a package).
## The two packages — do not conflate them
- **`linux-postmarketos-qcom-sdm845`** (`device/community/`) — the KERNEL
package. Kernel image (`vmlinuz`/Image.gz), all modules, the blueline DTB.
Userspace never goes here.
- **`device-google-blueline`** (`device/testing/`) — the DEVICE package.
Device userspace: firmware deps, hexagonrpcd/q6voiced confs, the systemd
preset, and now the **modem PDC self-provisioner** (`qcril-autoselect.py` +
`.service`, enabled via the preset). This is where per-device userspace
belongs — modelled on how `q6voiced.conf` is already installed.
## Unified 7.1.1 kernel build (2026-06-22)
Base is the sdm845-mainline tree at tag `85f1df2a` (= 7.1.1), checked out at
`references/linux-7.1.1-sdm845-CANONICAL`. All accumulated fixes are now **commits in that
tree**, not scattered `source=` patches:
- SLPI keep-proxy-power-on (the `sar.cc:27` crash-loop fix — see slpi.md)
- SLPI enabled in the blueline DTS
- fastrpc SDSP hyp-assign tolerance
Built via source override:
```
cd ~/pmos/pmaports
pmbootstrap build --src=$REPO/references/linux-7.1.1-sdm845-CANONICAL \
linux-postmarketos-qcom-sdm845
```
APK lands in `~/.local/var/pmbootstrap/packages/edge/aarch64/`.
## pmbootstrap build gotchas (each cost real time — RECORD)
0. **The in-tree `arch/arm64/configs/sdm845.config` is merged LAST and silently
overrides our pmaports config.** `prepare()` runs
`merge_config.sh -m <pmaports config-*.aarch64> arch/$_carch/configs/sdm845.config`
— and `merge_config -m` is last-wins. So any symbol set in the **in-tree**
fragment beats the same symbol in the pmaports config, even though the
pmaports config "looks" authoritative. **This silently broke wifi
(2026-06-29):** pmaports set `CONFIG_QCOM_PD_MAPPER=m`, but the upstream
sdm845-mainline in-tree fragment carried `=n` (their "use userspace daemon,
poco-f1 mic race" default), so every build shipped pd-mapper OFF → no wlan PD
`ath10k_snoc` probe defers → no `wlan0`. The `=n` rode in via the vanilla
import commit `310c17cc6`, not a deliberate choice. **Fix:** set the symbol in
the in-tree fragment (`references/linux-7.1.1-sdm845-CANONICAL/arch/arm64/configs/sdm845.config`),
not just pmaports. **Audit rule:** when a built kernel disagrees with the
pmaports config, `grep` the in-tree fragment first — it wins. Verify on-device
with `zcat /proc/config.gz | grep <SYMBOL>`.
1. **`olddefconfig` is mandatory on a version bump.** 7.1.1 added Kconfig
symbols not in the rc1 fragment config; abuild's build runs non-interactive,
so a bare `make` hits the `oldconfig` EOF prompt and dies at `prepare0`.
The APKBUILD `build()` now runs `make ARCH=$_carch LLVM=1 olddefconfig`
first, then a serial `make ... -j1 prepare`, then the parallel build.
2. **The `*.bc` gitignore drops a tracked kernel source file.** pmbootstrap's
source-override rsync is gitignore-aware but honors `*.bc` (line 16) WITHOUT
the negation `!kernel/time/timeconst.bc` (line 180). So `timeconst.bc` — a
real bc(1) source needed by `prepare0` for `include/generated/timeconst.h`
is silently excluded → `No rule to make target 'kernel/time/timeconst.bc'`.
Same class of breakage hits the negated dotfiles (`.clang-format`, etc.).
**Fix:** the kernel APKBUILD `prepare()` restores these from the source mount
(`/mnt/pmbootstrap/source-override/`) after `default_prepare`.
3. **The persistent build copy poisons incremental builds.** pmbootstrap rsyncs
the override into `/tmp/pmbootstrap-local-source-copy` (inside the native
chroot) WITHOUT `--delete`. A prior interrupted build leaves stale host-tool
binaries (`scripts/basic/fixdep`, `scripts/mod/mk_elfconfig`) + `.cmd` files;
the next build's `make` then trusts the stale stamps, never recompiles them,
and fails `mk_elfconfig: not found` at `prepare0`. If you see host-tool
"not found" with no preceding `HOSTCC` line, the copy is dirty.
**Recovery:** let pmbootstrap recreate it cleanly (delete the copy dir;
a fresh rsync from the clean host tree rebuilds host tools from scratch).
Note: `git clean -dfx` on the HOST tree does NOT touch this chroot copy.
## Kernel-swap / boot.img (from modem.md, kept here for the build flow)
The modem stack is fully modular (`qcom_q6v5_mss`, `qrtr*`, `qcom_glink_smem`,
`mhi*` all `=m`), so a kernel swap needs the matching `lib/modules/<kver>/` on
the rootfs or the modem silently no-loads (a false 52). UFS + USB-gadget-NCM are
`=y`, so a module-mismatched boot still reaches rootfs + USB.
Boot image: `cat vmlinuz <blueline>.dtb` + the standing initramfs, `mkbootimg`
header v0, base 0x0, kernel_off 0x8000, ramdisk_off 0x1000000, tags 0x100, real
cmdline `quiet splash … pmos_boot_uuid=2CE8-836F pmos_root_uuid=26733d78…`.
Rootfs is found by `pmos_root_uuid` (the ext4 FS UUID), so the slot LETTER only
decides which block device we write into — the cmdline is slot-agnostic.
SLOT MAP (verified live via `fastboot getvar current-slot` 2026-06-26):
**Android = slot B** (`current-slot: b`, `slot-successful:b: yes`). **pmOS = slot A.**
Flash ONLY `boot_a` (`fastboot flash boot_a …; fastboot --set-active=a`) and write
the rootfs to `system_a`. NEVER touch `boot_b` / `system_b` (that is Android now),
EFS(modemst1/2), userdata (single SHARED partition, ~119 GB), or TZ/XBL/ABL.
(Earlier revs of this doc had the slots reversed — they were wrong; the device is
authoritative.)
### FLASH GOTCHAS (each cost a boot this session — RECORD)
1. **`system_a` rejects bootloader-`fastboot flash` → must use TWRP cat (or fastbootd).**
`fastboot flash system_a` from the bootloader fails `Partition should be flashed
in fastbootd`. This fastboot/bootloader gates system writes to userspace. We DON'T
have a working `reboot fastbootd` (binary says "unknown reboot target"), so the
path is: `fastboot boot ~/Downloads/twrp-3.7.1_12-0-blueline.img`, wait for adb
`recovery`, then `adb shell -T "cat > /dev/block/by-name/system_a; sync" < raw.img`
(`system_a``/dev/block/sda5`, a real 2.95 GB physical partition, NOT dynamic).
2. **pmbootstrap's `google-blueline.img` is ANDROID-SPARSE, not raw — un-sparse before cat.**
THE bug that gave a kernel-7.1.0rc1 **debug-shell boot** (initramfs can't find root).
deviceinfo has `deviceinfo_flash_sparse="true"`, so the final image starts with the
sparse magic `3a ff 26 ed`, not a partition table. `cat`'ing it raw onto `system_a`
writes a sparse container, so there's no GPT/ext4 on the partition → initramfs
`findfs UUID=` fails → debug shell. **Fix:** `simg2img google-blueline.img raw.img`
first, THEN cat `raw.img`. Verify: a correct raw image starts with `0000`, reads as
GPT @ **4096-byte sectors** (`fdisk -b 4096 -l`: p1 ≈236M EFI/boot, p2 ext4 root at
byte 255852544), and has the ext4 magic `53ef` at `255852544+1080`. This matches the
known-good `slotb-backup` byte-for-byte. (UFS is 4096-sector — `fdisk` default 512
shows NO partitions on the raw image; that's a red herring, not corruption.)
3. **Quarantine stale `_p<timestamp>` kernel apks before `install`, or apk grabs the wrong one.**
A `--src` build auto-versions as `7.1.1_p20260625…`, which apk sorts ABOVE the tagged
`7.1.1-r2` (the `_p` post-suffix outranks the bare tag). `pmbootstrap install` then
silently pulls the OLD kernel. Move the `_p` apks out of
`~/.local/var/pmbootstrap/packages/edge/aarch64/`, rebuild the index (`apk index
*.apk` + `abuild-sign` in the native chroot — the `pmbootstrap index` wrapper failed
on its own quoting), then `pmbootstrap zap` (the rootfs chroot has the `_p` kernel
installed and apk won't downgrade) and re-install.
## The libqmi fork — packaged as the temp/libqmi override
`qcril-autoselect.py` needs the **openimsd libqmi fork** (PDC Set Feature msg
0x2E) via GObject-introspection; stock libqmi lacks both the message and the
typelib. It is packaged as **`temp/libqmi`** (pkgname=`libqmi`, source
`gitlab.postmarketos.org/modem/openimsd/libqmi` commit `b683efb`, built with
`-Dintrospection=true`, provides `libqmi` + `qmi-utils`). Because the aport's
pkgname is `libqmi`, it shadows Alpine's stock libqmi, and `device-google-blueline`
pulls it in via its `libqmi` / `qmi-utils` deps — a fresh `pmbootstrap install`
lays down kernel + provisioner + fork as one unit. The built apk carries
`usr/lib/girepository-1.0/Qmi-1.0.typelib` (verified).
Watch-item: the override is pinned at `1.39.0-r0`, equal to current Alpine edge.
If Alpine bumps libqmi above this, apk could prefer stock at install — bump the
override's pkgver/pkgrel to stay ahead. (`references/openimsd-libqmi` +
`tools/build-openimsd-libqmi.sh` remain as the on-device overlay fallback.)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,750 @@
From 2e349ddf2fe35102ef7c281d356c0e9a53c0f526 Mon Sep 17 00:00:00 2001
From: Casey Tunturi <casey@samaritansolutions.net>
Date: Mon, 10 Aug 2026 02:05:11 +0000
Subject: [PATCH] apple: d10: touch runtime reports and the display pipe
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
apple_z2: D10 shares D11 gen2 runtime reports. Without the enable, boot
frames arrive and the panel goes silent about a second later.
adp: first display pipe on T8010. disp0 be 0x06200000/0x9000, fe
0x06400000/0x4000, irqs 154/158; dart 0x06304000 irq 156; dsi
0x06600000/0x100000 — byte-identical to T8012, which is A10 silicon.
Values read off the D10 ADT. panel-summit gains a 750x1334 mode; the
loader lights the panel, so there is no init sequence either way.
---
arch/arm64/boot/dts/apple/t8010-d10.dts | 179 ++++++++++++++++++++++++
arch/arm64/boot/dts/apple/t8010.dtsi | 57 ++++++++
drivers/gpu/drm/panel/panel-summit.c | 26 +++-
drivers/input/touchscreen/apple_z2.c | 110 +++++++++------
4 files changed, 327 insertions(+), 45 deletions(-)
diff --git a/arch/arm64/boot/dts/apple/t8010-d10.dts b/arch/arm64/boot/dts/apple/t8010-d10.dts
index 39cdd12db..5f00bbe19 100644
--- a/arch/arm64/boot/dts/apple/t8010-d10.dts
+++ b/arch/arm64/boot/dts/apple/t8010-d10.dts
@@ -2,6 +2,10 @@
/*
* Apple iPhone 7 (Qualcomm), D10, iPhone9,1 (A1660/A1778/A1779/A1780)
* Copyright (c) 2022, Konrad Dybcio <konradybcio@kernel.org>
+ *
+ * Board values derived from the D10 ADT read off hardware 2026-08-08.
+ * Bus topology is identical to D11; see d10-port/DELTAS.md for the five
+ * values that differ and which of them still need driver work.
*/
/dts-v1/;
@@ -11,4 +15,179 @@
/ {
compatible = "apple,d10", "apple,t8010", "apple,arm-platform";
model = "Apple iPhone 7 (Qualcomm)";
+
+ aliases {
+ touchscreen0 = &touchscreen;
+ };
+
+ d10_battery: battery {
+ compatible = "simple-battery";
+ constant-charge-current-max-microamp = <1950000>;
+ };
+};
+
+/*
+ * Wi-Fi is deliberately absent. The ADT describes BCM4350 on PCIe port 3
+ * (perst 15, clkreq 19, device_wake 88, reg_on pmu_gpio 10, module-instance
+ * "kristoff"), but no driver claims it, so PCI pwrctrl never completes and the
+ * deferred host bridge takes NVMe down with it:
+ * pcie-apple-h9p: waiting for PCI power controls
+ * Restore the port node only alongside a BCM4350 driver. Values: d10-port/DELTAS.md.
+ */
+
+&i2c0 {
+ status = "okay";
+
+ pmic@27 {
+ compatible = "apple,chestnut-pmic";
+ reg = <0x27>;
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ touch_hv: regulator-touch-hv {
+ compatible = "apple,chestnut-touch-hv-regulator";
+ regulator-name = "touch-hv";
+ };
+ };
+};
+
+&i2c1 {
+ charger@75 {
+ compatible = "apple,sn2400";
+ reg = <0x75>;
+
+ sn2400_charger: charger {
+ compatible = "apple,sn2400-charger";
+ monitored-battery = <&d10_battery>;
+ input-current-limit-microamp = <2000000>;
+ mux-states = <&hdq_mux 0>;
+ mux-state-names = "hdq";
+ };
+
+ hdq_mux: hdq-mux {
+ compatible = "apple,sn2400-hdq-mux";
+ #mux-state-cells = <1>;
+ };
+ };
+};
+
+&i2c2 {
+ status = "okay";
+
+ light-sensor@29 {
+ compatible = "apple,ct821", "amstaos,tsl2581";
+ reg = <0x29>;
+ interrupts-extended = <&pinctrl_ap 141 IRQ_TYPE_LEVEL_LOW>;
+ };
+};
+
+&pinctrl_ap {
+ battery_swi_pins: battery-swi-pins {
+ pinmux = <APPLE_PINMUX(173, 1)>;
+ };
+
+};
+
+&serial5 {
+ pinctrl-0 = <&battery_swi_pins>;
+ pinctrl-names = "default";
+ status = "okay";
+
+ onewire {
+ compatible = "ti,hdq-uart";
+ mux-states = <&hdq_mux 1>;
+ mux-state-names = "hdq";
+
+ /* ADT: gas-gauge,bq27540 — D11 is a bq27545. */
+ fuel-gauge {
+ compatible = "apple,d10-bq27540", "ti,bq27540-hdq";
+ monitored-battery = <&d10_battery>;
+ };
+ };
+};
+
+&spi2 {
+ cs-gpios = <&pinctrl_ap 81 GPIO_ACTIVE_LOW>;
+ dmas = <&sio 0x18>;
+ dma-names = "tx";
+ status = "okay";
+
+ touchscreen: touchscreen@0 {
+ compatible = "apple,d10-touchscreen";
+ reg = <0>;
+ spi-max-frequency = <3000000>;
+ spi-cpol;
+ spi-cpha;
+ apple,z2-no-init-ack;
+ interrupts-extended = <&pinctrl_ap 166 IRQ_TYPE_EDGE_FALLING>;
+ reset-gpios = <&pinctrl_ap 161 GPIO_ACTIVE_LOW>;
+ hv-supply = <&touch_hv>;
+ core-supply = <&touch_core>;
+ clocks = <&touchclk>;
+ /* ADT hid-merge-personality = C1F5D,2 (D11/D111 are C1F5E,2). */
+ firmware-name = "apple/dfrmtfw-d10-c1f5d-2.bin";
+ touchscreen-size-x = <750>;
+ touchscreen-size-y = <1334>;
+ };
+};
+
+&sio {
+ status = "okay";
+};
+
+&touchclk {
+ clock-frequency = <32768>;
+};
+
+/*
+ * Apple Display Pipe. ADP inherits bootloader-initialised state, so the
+ * back-end domain must stay powered or the driver loses the panel m1n1 lit.
+ */
+&ps_disp0_be {
+ apple,always-on;
+};
+
+&disp0 {
+ status = "okay";
+};
+
+&disp0_dart {
+ status = "okay";
+};
+
+&disp0_mipi {
+ status = "okay";
+};
+
+&mipi_out {
+ mipi_out_panel: endpoint@0 {
+ reg = <0>;
+ remote-endpoint = <&panel_in>;
+ };
+};
+
+&disp0_mipi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ panel@0 {
+ compatible = "apple,d10-panel";
+ reg = <0>;
+ max-brightness = <2047>;
+
+ port {
+ panel_in: endpoint {
+ remote-endpoint = <&mipi_out_panel>;
+ };
+ };
+ };
+};
+
+/*
+ * ADP owns the panel, so the loader framebuffer must not also claim it:
+ * simpledrm binds first as the boot console and udev then names it the
+ * primary GPU, which is the card the compositor picks. One writer.
+ */
+&framebuffer0 {
+ status = "disabled";
};
diff --git a/arch/arm64/boot/dts/apple/t8010.dtsi b/arch/arm64/boot/dts/apple/t8010.dtsi
index 8dc02eb60..7ec65050e 100644
--- a/arch/arm64/boot/dts/apple/t8010.dtsi
+++ b/arch/arm64/boot/dts/apple/t8010.dtsi
@@ -358,6 +358,63 @@ ehci1: usb-ehci@20c400000 {
#endif
};
+ disp0: display-pipe@206200000 {
+ compatible = "apple,t8010-display-pipe", "apple,h7-display-pipe";
+ reg = <0x2 0x06200000 0x0 0x9000>,
+ <0x2 0x06400000 0x0 0x4000>;
+ reg-names = "be", "fe";
+ power-domains = <&ps_disp0_fe>, <&ps_disp0_be>;
+ interrupts = <AIC_IRQ 154 IRQ_TYPE_LEVEL_HIGH>,
+ <AIC_IRQ 158 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-names = "be", "fe";
+ iommus = <&disp0_dart 0>;
+ status = "disabled";
+
+ port {
+ adp_out_mipi: endpoint {
+ remote-endpoint = <&mipi_in_adp>;
+ };
+ };
+ };
+
+ disp0_dart: iommu@206304000 {
+ compatible = "apple,t8010-dart", "apple,s5l8960x-dart";
+ reg = <0x2 0x06304000 0x0 0x4000>;
+ interrupts = <AIC_IRQ 156 IRQ_TYPE_LEVEL_HIGH>;
+ #iommu-cells = <1>;
+ power-domains = <&ps_disp0_fe>;
+ status = "disabled";
+ };
+
+ disp0_mipi: dsi@206600000 {
+ compatible = "apple,t8010-display-pipe-mipi", "apple,h7-display-pipe-mipi";
+ reg = <0x2 0x06600000 0x0 0x100000>;
+ power-domains = <&ps_mipi_dsi>;
+ status = "disabled";
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ mipi_in: port@0 {
+ reg = <0>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ mipi_in_adp: endpoint@0 {
+ reg = <0>;
+ remote-endpoint = <&adp_out_mipi>;
+ };
+ };
+
+ mipi_out: port@1 {
+ reg = <1>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+ };
+ };
+ };
+
pcie0_dart0: iommu@601008000 {
compatible = "apple,t8010-dart", "apple,s5l8960x-dart";
reg = <0x6 0x01008000 0x0 0x4000>;
diff --git a/drivers/gpu/drm/panel/panel-summit.c b/drivers/gpu/drm/panel/panel-summit.c
index 6d40b9ddf..42e4c24fb 100644
--- a/drivers/gpu/drm/panel/panel-summit.c
+++ b/drivers/gpu/drm/panel/panel-summit.c
@@ -2,6 +2,7 @@
#include <linux/backlight.h>
#include <linux/mod_devicetable.h>
+#include <linux/of.h>
#include <linux/property.h>
#include <drm/drm_device.h>
#include <drm/drm_mipi_dsi.h>
@@ -48,15 +49,35 @@ static struct drm_display_mode summit_mode = {
.flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC,
};
+/* iPhone 7 (D10): 750x1334, lit by m1n1 before Linux starts. */
+static struct drm_display_mode d10_mode = {
+ .hdisplay = 750,
+ .vdisplay = 1334,
+ .hsync_start = 750 + 8,
+ .hsync_end = 750 + 8 + 16,
+ .htotal = 750 + 8 + 16 + 16,
+ .vsync_start = 1334 + 4,
+ .vsync_end = 1334 + 4 + 8,
+ .vtotal = 1334 + 4 + 8 + 8,
+ .clock = ((750 + 8 + 16 + 16) * (1334 + 4 + 8 + 8) * 60) / 1000,
+ .type = DRM_MODE_TYPE_DRIVER,
+ .flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC,
+};
+
static int summit_get_modes(struct drm_panel *panel,
struct drm_connector *connector)
{
+ const struct drm_display_mode *mode =
+ of_device_get_match_data(panel->dev);
+
+ if (!mode)
+ mode = &summit_mode;
connector->display_info.non_desktop = true;
drm_object_property_set_value(&connector->base,
connector->dev->mode_config.non_desktop_property,
connector->display_info.non_desktop);
- return drm_connector_helper_get_modes_fixed(connector, &summit_mode);
+ return drm_connector_helper_get_modes_fixed(connector, mode);
}
static const struct drm_panel_funcs summit_panel_funcs = {
@@ -113,7 +134,8 @@ static DEFINE_SIMPLE_DEV_PM_OPS(summit_pm_ops, summit_suspend,
summit_set_brightness);
static const struct of_device_id summit_of_match[] = {
- { .compatible = "apple,summit" },
+ { .compatible = "apple,summit", .data = &summit_mode },
+ { .compatible = "apple,d10-panel", .data = &d10_mode },
{},
};
diff --git a/drivers/input/touchscreen/apple_z2.c b/drivers/input/touchscreen/apple_z2.c
index ec84de96f..065fa8214 100644
--- a/drivers/input/touchscreen/apple_z2.c
+++ b/drivers/input/touchscreen/apple_z2.c
@@ -93,6 +93,7 @@ enum apple_z2_protocol_state {
enum apple_z2_variant {
APPLE_Z2_VARIANT_GENERIC,
APPLE_Z2_VARIANT_J172,
+ APPLE_Z2_VARIANT_D10,
APPLE_Z2_VARIANT_D11,
APPLE_Z2_VARIANT_D111,
};
@@ -150,19 +151,35 @@ static bool apple_z2_is_j172(const struct apple_z2 *z2)
return z2->variant == APPLE_Z2_VARIANT_J172;
}
+static bool apple_z2_is_d10(const struct apple_z2 *z2)
+{
+ return z2->variant == APPLE_Z2_VARIANT_D10;
+}
+
static bool apple_z2_is_d11(const struct apple_z2 *z2)
{
return z2->variant == APPLE_Z2_VARIANT_D11;
}
+/* D10 and D11 share the gen2 runtime report protocol. Without the enable,
+ * boot frames arrive and the panel then goes silent — measured on d10
+ * 2026-08-09: touch reported for about a second after probe, then nothing. */
+static bool apple_z2_is_gen2_reports(const struct apple_z2 *z2)
+{
+ return z2->variant == APPLE_Z2_VARIANT_D10 ||
+ z2->variant == APPLE_Z2_VARIANT_D11;
+}
+
static bool apple_z2_is_d111(const struct apple_z2 *z2)
{
return z2->variant == APPLE_Z2_VARIANT_D111;
}
-static bool apple_z2_is_iphone7_plus(const struct apple_z2 *z2)
+/* Whole T8010 iPhone 7 generation, both panel sizes. */
+static bool apple_z2_is_iphone7(const struct apple_z2 *z2)
{
- return apple_z2_is_d11(z2) || apple_z2_is_d111(z2);
+ return apple_z2_is_d10(z2) || apple_z2_is_d11(z2) ||
+ apple_z2_is_d111(z2);
}
struct apple_z2_finger {
@@ -404,7 +421,7 @@ static void apple_z2_parse_touches(struct apple_z2 *z2,
msg_len, nfingers);
return;
}
- if (apple_z2_is_iphone7_plus(z2) && nfingers &&
+ if (apple_z2_is_iphone7(z2) && nfingers &&
!z2->surface_descriptor_valid) {
dev_warn_ratelimited(&z2->spidev->dev,
"iPhone 7 Plus touch packet without surface descriptor\n");
@@ -422,7 +439,7 @@ static void apple_z2_parse_touches(struct apple_z2 *z2,
input_mt_slot(z2->input_dev, slot);
if (!input_mt_report_slot_state(z2->input_dev, MT_TOOL_FINGER, slot_valid))
continue;
- if (apple_z2_is_iphone7_plus(z2)) {
+ if (apple_z2_is_iphone7(z2)) {
abs_x = (s16)le16_to_cpu(fingers[i].abs_x);
abs_y = (s16)le16_to_cpu(fingers[i].abs_y);
coords_valid = apple_z2_scale_iphone7_coord(abs_x,
@@ -454,7 +471,7 @@ static void apple_z2_parse_touches(struct apple_z2 *z2,
input_report_abs(z2->input_dev, ABS_MT_WIDTH_MINOR,
le16_to_cpu(fingers[i].tool_minor));
orientation = (s16)le16_to_cpu(fingers[i].orientation);
- if (apple_z2_is_iphone7_plus(z2))
+ if (apple_z2_is_iphone7(z2))
orientation = (s16)(0x4000 - orientation);
input_report_abs(z2->input_dev, ABS_MT_ORIENTATION, orientation);
input_report_abs(z2->input_dev, ABS_MT_TOUCH_MAJOR,
@@ -473,7 +490,7 @@ static void apple_z2_dispatch_frame(struct apple_z2 *z2, const u8 *payload,
u16 report_len;
/* iPhone 7 Plus Gen2 packets carry the legacy report header directly. */
- if (apple_z2_is_iphone7_plus(z2)) {
+ if (apple_z2_is_iphone7(z2)) {
if (payload_len < APPLE_Z2_FINGERS_OFFSET)
return;
@@ -572,11 +589,11 @@ static int apple_z2_read_packet(struct apple_z2 *z2)
return -EMSGSIZE;
}
wire_len = pkt_len;
- if (apple_z2_is_d11(z2))
+ if (apple_z2_is_gen2_reports(z2))
wire_len = max_t(size_t, wire_len,
APPLE_Z2_GEN2_MIN_RESULT_SIZE);
- if (apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) {
+ if (apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) {
memset(z2->rx_buf, 0xa5, wire_len);
xfer.tx_buf = z2->rx_buf;
xfer.rx_buf = z2->rx_buf;
@@ -587,11 +604,11 @@ static int apple_z2_read_packet(struct apple_z2 *z2)
}
if (error)
return error;
- if (apple_z2_is_iphone7_plus(z2))
+ if (apple_z2_is_iphone7(z2))
apple_z2_post_z2_xfer_delay(z2);
/* D11 touch reports do not follow the requested alternating tag. */
- strict_counter = !apple_z2_is_d11(z2) ||
+ strict_counter = !apple_z2_is_gen2_reports(z2) ||
(pkt_len > 5 && z2->rx_buf[5] ==
APPLE_Z2_D11_STATUS_REPORT);
z2->runtime_frame_valid =
@@ -705,7 +722,7 @@ static void apple_z2_avoid_short_dma(struct apple_z2 *z2,
* an RX buffer makes shorter writes full-duplex, which the T8010
* controller handles through PIO without changing the wire transfer.
*/
- if (!apple_z2_is_iphone7_plus(z2) || !z2->dma_min_len ||
+ if (!apple_z2_is_iphone7(z2) || !z2->dma_min_len ||
!xfer->tx_buf || xfer->rx_buf ||
xfer->len >= z2->dma_min_len)
return;
@@ -831,7 +848,7 @@ static int apple_z2_write_report_locked(struct apple_z2 *z2, u8 report,
if (error)
return error;
- if (apple_z2_is_d11(z2) &&
+ if (apple_z2_is_gen2_reports(z2) &&
(z2->rx_buf[0] != APPLE_Z2_CMD_LAST ||
!apple_z2_z2_checksum_valid(z2->rx_buf, APPLE_Z2_CMD_SIZE)))
return -EPROTO;
@@ -978,7 +995,7 @@ static int apple_z2_store_surface_descriptor(struct apple_z2 *z2, u16 len)
return 0;
}
-static int apple_z2_iphone7_plus_init_locked(struct apple_z2 *z2)
+static int apple_z2_iphone7_init_locked(struct apple_z2 *z2)
{
static const u8 disable_legacy_reports[] = { 0 };
u16 surface_len;
@@ -1289,17 +1306,17 @@ static void apple_z2_platform_power_off(struct apple_z2 *z2)
{
int error;
- if (apple_z2_is_iphone7_plus(z2))
+ if (apple_z2_is_iphone7(z2))
gpiod_set_value_cansleep(z2->reset_gpio, 1);
if (z2->clk_enabled) {
clk_disable_unprepare(z2->clk);
z2->clk_enabled = false;
}
- if (apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2))
+ if (apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2))
usleep_range(1000, 2000);
- if (apple_z2_is_iphone7_plus(z2)) {
+ if (apple_z2_is_iphone7(z2)) {
if (z2->core_enabled) {
error = regulator_disable(z2->core_supply);
if (error) {
@@ -1381,7 +1398,7 @@ static int apple_z2_j172_power_on(struct apple_z2 *z2)
APPLE_Z2_STATE_POWERED);
}
-static int apple_z2_iphone7_plus_power_on(struct apple_z2 *z2)
+static int apple_z2_iphone7_power_on(struct apple_z2 *z2)
{
int error;
@@ -1422,8 +1439,8 @@ static int apple_z2_platform_power_on(struct apple_z2 *z2)
if (apple_z2_is_j172(z2))
return apple_z2_j172_power_on(z2);
- if (apple_z2_is_iphone7_plus(z2))
- return apple_z2_iphone7_plus_power_on(z2);
+ if (apple_z2_is_iphone7(z2))
+ return apple_z2_iphone7_power_on(z2);
apple_z2_set_gpio(z2->power_ana_gpio, 1);
usleep_range(1000, 2000);
@@ -1609,14 +1626,14 @@ static int apple_z2_send_firmware_blob(struct apple_z2 *z2, const u8 *data,
* SmartIO may still DMA these transfers, but using 16-bit SPI words would
* swap every byte pair a second time.
*/
- if (!init && !apple_z2_is_iphone7_plus(z2) &&
+ if (!init && !apple_z2_is_iphone7(z2) &&
size >= z2->bpw16_min_len)
blob_xfer.bits_per_word = 16;
dev_dbg(&z2->spidev->dev, "firmware blob len=%u bpw=%u\n",
size, blob_xfer.bits_per_word);
- if ((apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) && init) {
+ if ((apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) && init) {
if (size != 4 || data[0] != 0x1a || data[1] != 0xa1 ||
data[2] != 0x18 || data[3] != 0xe1)
return dev_err_probe(&z2->spidev->dev, -EINVAL,
@@ -1634,7 +1651,7 @@ static int apple_z2_send_firmware_blob(struct apple_z2 *z2, const u8 *data,
if (error)
return error;
- if ((apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) && init) {
+ if ((apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) && init) {
ready = z2->rx_buf[0] == 0x1f && z2->rx_buf[1] == 0x01;
if (!ready)
return dev_err_probe(&z2->spidev->dev, -EPROTO,
@@ -1648,7 +1665,7 @@ static int apple_z2_send_firmware_blob(struct apple_z2 *z2, const u8 *data,
return error;
apple_z2_post_ack_delay(z2);
}
- if ((apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) && init) {
+ if ((apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) && init) {
error = apple_z2_advance_protocol(z2, APPLE_Z2_STATE_BOOT_IRQ,
APPLE_Z2_STATE_HBPP_READY);
if (error)
@@ -1741,7 +1758,7 @@ static int apple_z2_wait_ready_irq(struct apple_z2 *z2, u32 timeout_ms)
{
if (!apple_z2_wait_firmware_irq(z2, timeout_ms))
return -ETIMEDOUT;
- if (!apple_z2_is_j172(z2) && !apple_z2_is_iphone7_plus(z2))
+ if (!apple_z2_is_j172(z2) && !apple_z2_is_iphone7(z2))
return 0;
if (apple_z2_is_j172(z2) &&
@@ -1778,7 +1795,7 @@ static int apple_z2_apply_fw_config(struct apple_z2 *z2, const u8 *data,
cpol = apple_z2_fw_config_word(data, 5);
boot_timeout = apple_z2_fw_config_word(data, 9);
if ((valid & APPLE_Z2_FW_CONFIG_MIN_DMA) && min_dma) {
- if (apple_z2_is_iphone7_plus(z2)) {
+ if (apple_z2_is_iphone7(z2)) {
if (min_dma > APPLE_Z2_RX_BUF_SIZE)
return -EINVAL;
z2->dma_min_len = min_dma;
@@ -1790,7 +1807,7 @@ static int apple_z2_apply_fw_config(struct apple_z2 *z2, const u8 *data,
z2->z2_inter_packet_delay_us = z2_delay;
/* iPhone 7 Plus uses zero to retain the required 1 ms CS delay. */
if ((valid & APPLE_Z2_FW_CONFIG_CS_DELAY) &&
- (!apple_z2_is_iphone7_plus(z2) || cs_delay))
+ (!apple_z2_is_iphone7(z2) || cs_delay))
z2->z2_cs_delay_us = cs_delay;
if ((valid & APPLE_Z2_FW_CONFIG_BOOT_TIMEOUT) && boot_timeout)
z2->boot_timeout_ms = boot_timeout;
@@ -1922,7 +1939,7 @@ static int apple_z2_upload_firmware(struct apple_z2 *z2)
u32 max_size;
u32 provider;
- if (!apple_z2_is_iphone7_plus(z2))
+ if (!apple_z2_is_iphone7(z2))
return -EINVAL;
if (size != sizeof(struct apple_z2_fw_calibration) ||
size > fw->size - fw_idx)
@@ -1969,19 +1986,19 @@ static int apple_z2_upload_firmware(struct apple_z2 *z2)
fw_idx = round_up(fw_idx, 4);
}
- if (apple_z2_is_iphone7_plus(z2)) {
+ if (apple_z2_is_iphone7(z2)) {
msleep(50);
mutex_lock(&z2->io_lock);
- error = apple_z2_iphone7_plus_init_locked(z2);
+ error = apple_z2_iphone7_init_locked(z2);
if (!error) {
z2->booted = true;
error = apple_z2_read_packet(z2);
}
- if (!error && apple_z2_is_d11(z2) &&
+ if (!error && apple_z2_is_gen2_reports(z2) &&
(!z2->runtime_frame_valid ||
z2->rx_buf[5] != APPLE_Z2_D11_STATUS_REPORT))
error = -EPROTO;
- if (!error && apple_z2_is_d11(z2))
+ if (!error && apple_z2_is_gen2_reports(z2))
error = apple_z2_enable_d11_reports_locked(z2);
mutex_unlock(&z2->io_lock);
return error;
@@ -2087,7 +2104,7 @@ static int apple_z2_boot_preamble(struct apple_z2 *z2, bool full_duplex)
APPLE_Z2_STATE_SPI_CONFIGURED);
}
-static int apple_z2_iphone7_plus_post_boot_preamble(struct apple_z2 *z2)
+static int apple_z2_iphone7_post_boot_preamble(struct apple_z2 *z2)
{
struct spi_transfer xfer = {
.tx_buf = z2->tx_buf,
@@ -2114,7 +2131,7 @@ static int apple_z2_boot(struct apple_z2 *z2)
if (error)
return error;
- if (apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) {
+ if (apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) {
error = apple_z2_apply_initial_config(z2);
if (error)
goto err_stop;
@@ -2123,7 +2140,7 @@ static int apple_z2_boot(struct apple_z2 *z2)
error = apple_z2_boot_preamble(z2, false);
if (error)
goto err_stop;
- } else if (apple_z2_is_iphone7_plus(z2)) {
+ } else if (apple_z2_is_iphone7(z2)) {
/* iPhone 7 Plus performs a legacy reset and zero preamble first. */
error = apple_z2_pulse_reset(z2);
if (error)
@@ -2142,7 +2159,7 @@ static int apple_z2_boot(struct apple_z2 *z2)
irq_enabled = true;
if (apple_z2_is_j172(z2)) {
error = apple_z2_pulse_reset(z2);
- } else if (apple_z2_is_iphone7_plus(z2)) {
+ } else if (apple_z2_is_iphone7(z2)) {
gpiod_set_value_cansleep(z2->reset_gpio, 0);
usleep_range(1000, 2000);
error = 0;
@@ -2158,20 +2175,20 @@ static int apple_z2_boot(struct apple_z2 *z2)
error = -ETIMEDOUT;
goto err_stop;
}
- if (apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) {
+ if (apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) {
error = apple_z2_advance_protocol(z2,
APPLE_Z2_STATE_SPI_CONFIGURED,
APPLE_Z2_STATE_BOOT_IRQ);
if (error)
goto err_stop;
}
- if (apple_z2_is_iphone7_plus(z2)) {
- error = apple_z2_iphone7_plus_post_boot_preamble(z2);
+ if (apple_z2_is_iphone7(z2)) {
+ error = apple_z2_iphone7_post_boot_preamble(z2);
if (error)
goto err_stop;
}
- if (apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) {
+ if (apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) {
disable_irq(z2->spidev->irq);
z2->upload_irq_masked = true;
}
@@ -2211,6 +2228,11 @@ static const struct apple_z2_chip_info apple_z2_j172_info = {
.variant = APPLE_Z2_VARIANT_J172,
};
+static const struct apple_z2_chip_info apple_z2_d10_info = {
+ .name = "iPhone9,1 Touchscreen",
+ .variant = APPLE_Z2_VARIANT_D10,
+};
+
static const struct apple_z2_chip_info apple_z2_d11_info = {
.name = "iPhone9,2 Touchscreen",
.variant = APPLE_Z2_VARIANT_D11,
@@ -2249,7 +2271,7 @@ static int apple_z2_probe(struct spi_device *spi)
z2->spidev = spi;
z2->variant = info->variant;
z2->boot_timeout_ms = 20;
- if (apple_z2_is_iphone7_plus(z2)) {
+ if (apple_z2_is_iphone7(z2)) {
z2->z2_inter_packet_delay_us = 1000;
z2->z2_cs_delay_us = 1000;
z2->boot_timeout_ms = 500;
@@ -2269,7 +2291,7 @@ static int apple_z2_probe(struct spi_device *spi)
if (IS_ERR(z2->reset_gpio))
return dev_err_probe(dev, PTR_ERR(z2->reset_gpio),
"unable to get reset GPIO\n");
- if (apple_z2_is_iphone7_plus(z2)) {
+ if (apple_z2_is_iphone7(z2)) {
z2->hv_supply = devm_regulator_get(dev, "hv");
if (IS_ERR(z2->hv_supply))
return dev_err_probe(dev, PTR_ERR(z2->hv_supply),
@@ -2316,7 +2338,7 @@ static int apple_z2_probe(struct spi_device *spi)
error = device_property_read_string(dev, "firmware-name", &z2->fw_name);
if (error)
return dev_err_probe(dev, error, "unable to get firmware name\n");
- if (apple_z2_is_iphone7_plus(z2)) {
+ if (apple_z2_is_iphone7(z2)) {
static const struct {
const char *name;
unsigned int max_size;
@@ -2361,7 +2383,7 @@ static int apple_z2_probe(struct spi_device *spi)
if (apple_z2_is_j172(z2)) {
slots = APPLE_Z2_J172_MAX_CONTACTS;
- } else if (apple_z2_is_iphone7_plus(z2)) {
+ } else if (apple_z2_is_iphone7(z2)) {
slots = 10;
if (apple_z2_is_d11(z2))
mt_flags |= INPUT_MT_DROP_UNUSED;
@@ -2415,6 +2437,7 @@ static const struct of_device_id apple_z2_of_match[] = {
{ .compatible = "apple,j293-touchbar", .data = &apple_z2_j293_info },
{ .compatible = "apple,j493-touchbar", .data = &apple_z2_j493_info },
{ .compatible = "apple,j172-touchscreen", .data = &apple_z2_j172_info },
+ { .compatible = "apple,d10-touchscreen", .data = &apple_z2_d10_info },
{ .compatible = "apple,d11-touchscreen", .data = &apple_z2_d11_info },
{ .compatible = "apple,d111-touchscreen", .data = &apple_z2_d111_info },
{}
@@ -2425,6 +2448,7 @@ static struct spi_device_id apple_z2_of_id[] = {
{ .name = "j293-touchbar", .driver_data = (kernel_ulong_t)&apple_z2_j293_info },
{ .name = "j493-touchbar", .driver_data = (kernel_ulong_t)&apple_z2_j493_info },
{ .name = "j172-touchscreen", .driver_data = (kernel_ulong_t)&apple_z2_j172_info },
+ { .name = "d10-touchscreen", .driver_data = (kernel_ulong_t)&apple_z2_d10_info },
{ .name = "d11-touchscreen", .driver_data = (kernel_ulong_t)&apple_z2_d11_info },
{ .name = "d111-touchscreen", .driver_data = (kernel_ulong_t)&apple_z2_d111_info },
{}
--
2.55.0

View file

@ -0,0 +1,104 @@
# PAF · DIAG capture runbook (Route B instrument)
Goal: get the modem's OWN view of RF-init — why it never self-completes, parks `offline`, and
refuses `online` (DeviceNotReady 52). Everything so far is QMI reporting a *refusal code*; DIAG
(QCDM/DM) gives the modem's internal **F3 debug messages**, radio frames, and **NV/EFS** state.
This is the byte-for-byte trace every prior frontier (#5/#7/#8) named as the missing piece.
## Why DIAG (vs the coredump path)
- F3/DM debug messages = the modem narrating its RF/NAS/MCFG init in its own words. Richer than a
remoteproc coredump/SSR reason.
- NV item + EFS read/write over DIAG = Route B's EFS/NV diff (GLM thread #2) at the authoritative
source, and the write path for `mcfg_autoselect_by_uim` / power-up operating mode.
- The decisive comparison: run the SAME tool on BOTH OSes against the SAME modem silicon and diff.
## Channel facts (confirmed)
- DIAG is exposed on Android via `/dev/diag`, created by the **`diagchar`** kernel module (root).
- Enable frame capture: `DIAG_IOCTL_SWITCH_LOGGING``MEMORY_DEVICE_MODE = 2`; check
`DIAG_IOCTL_REMOTE_DEV` for the extra device_type byte.
- Packet framing: `cmd_code(1B) + payload + CRC16-CCITT(2B) + 0x7e`, HDLC escaping
(`0x7e``0x7d 0x5e`, `0x7d``0x7d 0x5d`).
- Message classes: `DIAG_LOG_F` (binary log packets / radio frames), `DIAG_MSG_F` /
`DIAG_EXT_MSG_F` (F3 debug strings — the narration we want). NV/EFS/L1-3 commands supported.
- Tool: **QCSuper** (P1sec, open source) for frame + F3 capture → pcap/GSMTAP. For arbitrary NV
item read/write, a raw DIAG client (libqcdm-style framer, or QCSuper's diag layer directly).
- CHECK FIRST: `sdm845-mainline/pmtools` may already ship a diag/modem helper — look before
reinventing the framer.
---
## Side A — Android (slot B, rooted): the WORKING reference [HIGH VALUE — never captured]
`diagchar` + `/dev/diag` are present on the downstream Android kernel. Two ways in:
- **USB-DIAG** (exposes a serial/COM port): `adb root; adb shell``su`
`setprop sys.usb.config diag,serial_cdev,rmnet_gsi,adb` (XDA-confirmed on Pixel 3). Our ledger
saw this HAL-revert to adb-only on this LineageOS build — RETRY it; if it reverts, use local.
- **Local `/dev/diag`** (reliable fallback): run the client on-device over adb, not USB-DIAG.
1. `adb root; adb shell ls -l /dev/diag` — confirm node exists.
2. Push/run QCSuper on-device (or proxy /dev/diag over adb). Start F3 + frame capture.
3. Trigger a clean offline→online: **airplane mode OFF→ON** (we already have this at QCRIL-function
granularity in `android-capture-20260620/`; DIAG gets it at message/F3 granularity).
4. Save: F3 stream + radio frames spanning the transition. This is the modem self-onlining at its
own RF-init — the thing pmOS never reaches.
5. While here, DIAG-read the NV/EFS state the working modem holds:
- power-up operating mode NV
- `mcfg_autoselect_by_uim`
- RF-calibration NV / QCN region
(Confirm exact NV item indices from the modem's NV table / QCSuper — do NOT guess indices.)
Output → `android-capture-YYYYMMDD/diag/`.
## Side B — pmOS (slot A, mainline): the FAILURE side [verify availability first]
`diagchar` is a downstream module; mainline SDM845 does NOT ship it. So `/dev/diag` may be absent.
Determine the transport before assuming symmetry:
1. `ls -l /dev/diag*` ; `dmesg | grep -i diag` ; `zcat /proc/config.gz | grep -i diag` (or check
the built kernel config) ; look for a glink/rpmsg channel named `DIAG`.
2. If a DIAG transport exists → run QCSuper, attempt `--dms-set-operating-mode=online`, capture the
F3 stream of the FAILED RF-init. Diff F3 against Side A around the offline→online point — the
first divergence is the answer.
3. If NO DIAG transport on mainline → fall back to GLM thread #1: remoteproc coredump/SSR reason +
`tqftpserv`/`rmtfs` request trace during the failed online. Diff that against Side A's DIAG F3.
(Retry-safe: online attempts do NOT poison the modem — the old "one-shot per boot" rule is
disproven. A QMI `Set Operating Mode = RESET (4)` cleanly reinitializes it without reboot.
Retry freely. See HANDOFF.md CORRECTIONS.)
---
## What we're diffing for
**KEY: a static NV/QCN diff between the two OSes will be NULL.** modemst1/2 + fsg/fsc + persist are
on UNSLOTTED partitions → the modem reads BYTE-IDENTICAL persisted EFS/NV/cal on both Android and
pmOS (ledger L83: Android-primed EFS → still fails). The modem has no separate flash; its NV lives
in that shared EFS. So a QPST QCN backup / NV-item dump will read the same bytes on both sides — it
will NOT reveal the delta. **The difference is necessarily RUNTIME behavior**, which is exactly why
the F3 stream (not a static snapshot) is the instrument:
- The F3 line(s) where Android's modem advances RF-init past the point pmOS's stalls.
- Whether the working modem boots into LPM and flips LPM→online, vs pmOS sitting in `offline`
(mode 3) — and what in the F3 stream gates that state.
- Whether the working modem's online even TOUCHES NV/PDC (frontier #4 says steady-state Android is
verify-only) — if it doesn't, the gate is pure firmware-state/timing the AP can't feed.
(NV reads in Side A step 5 are still worth doing ONCE — to confirm the shared-EFS assumption and
record the working values — but expect them equal to pmOS. Don't build a Route B fix around a static
NV diff until F3 shows the modem actually consuming that item at init.)
## Tools reality (we're on Arch, not Windows)
- **QPST / QXDM are Windows-only proprietary.** QPST = QCN backup/restore; QXDM = deep F3 logging.
Given the shared-EFS point above, QPST's QCN backup buys little here.
- **QCSuper (Linux, open) is the path** for F3 + frame capture. For raw NV item read/write, a
libqcdm-style framer or QCSuper's diag layer. Check `pmtools` first.
## SIDE note — glink rpmsg teardown is broken upstream (applies to CHANNEL open/close, NOT online)
Dynamically opening/closing glink rpmsg channels is documented as broken on the upstreaming list
(the `wwan_remove_port`/`rpmsg_wwan_ctrl` 614s hung_task seen on op6t). This is a real risk when
*creating/tearing down a glink channel* (e.g. opening a raw DIAG rpmsg endpoint). It is NOT the
"one failed online poisons the modem" rule that used to be here — that rule is disproven: a plain
`--dms-set-operating-mode=online` is an ordinary QMI message on the existing IPCRTR transport, it
does not open a glink channel, and it is retry-safe (RESET reinitializes). Keep the caution for
raw glink channel-open probes only. An AT read of the modem's own reason (`+CEER`, `+CFUN?`) has
no free path anyway: QMI service 8 is AT-*forwarding*, not AT-execution (see qrtr_tools_reference
2026-07-02), and a raw glink AT port is exactly the channel-open that this note warns about.
## Refs
- QCSuper: https://github.com/P1sec/QCSuper (docs/The Diag protocol.md)
- pmtools (check for existing diag helper): https://gitlab.com/sdm845-mainline/pmtools

View file

@ -0,0 +1,108 @@
# First-Provision Capture — force QCRIL to run the real MBN load sequence
**Why:** Every Android capture we have (`android-live-20260619/`) was an *already-provisioned*
device, so QCRIL took the verify-only fast path (`is_modem_mbn_updated: prev_ver == cur_ver`
skip). We have **never recorded** the actual load→select→activate sequence, nor whether
`pdc_enable_auto_selection` / `qcril_qmi_pdc_enable_modem_update` is a required precondition.
This runbook forces a first-provision and captures it. See `PAF/modem.md` frontier #4.
**Run from the host with the phone on Android slot B, rooted (`adb root`).** All commands are
host-side `adb`; device shell is Android (toybox), not pmOS ash.
---
## 0. Preconditions + backup (reversible-safety)
```sh
adb root && adb wait-for-device
adb shell getprop ro.product.device # expect: blueline
adb shell getprop gsm.version.baseband # expect: g845-00194-...
# Back up QCRIL's writable provisioning cache + the trigger props, so we can prove
# what changed and restore if needed.
adb shell 'tar -czf /data/local/tmp/modem_config.bak.tgz -C /data/vendor modem_config 2>/dev/null'
adb pull /data/local/tmp/modem_config.bak.tgz ./fp-capture/
adb shell 'getprop | grep -E "vendor.radio.(cnv|hw_mbn|sw_mbn|mbn)" ' | tee ./fp-capture/props.before.txt
```
> Note: we deliberately do **NOT** touch the modem's EFS (`modemst1/2`, `fsg`, `fsc`). Those hold
> the modem's own persisted config + calibration; wiping them is destructive and not needed. We
> only clear QCRIL's *userspace* cache + the version prop — that is the documented trigger for the
> `prev_ver != cur_ver` reload branch.
## 1. Enlarge the radio log ring so early-boot lines survive until we dump
```sh
adb shell setprop persist.vendor.radio.smlog_switch 1 # verbose radio logging (best-effort)
adb logcat -b radio -G 64M # 64 MB radio buffer
adb logcat -b system -G 16M
```
## 2. Force the first-provision trigger, then reboot
```sh
# Clear QCRIL's writable provisioning state (rebuilt automatically on next provision).
adb shell 'rm -rf /data/vendor/modem_config/*'
# Blank the cached version string QCRIL compares against (this is the prev_ver in
# is_modem_mbn_updated). Empty prev_ver => mismatch => QCRIL must re-evaluate the load path.
adb shell 'setprop persist.vendor.radio.cnv.ver_info ""'
adb shell 'setprop persist.vendor.radio.hw_mbn_loaded ""'
adb shell 'setprop persist.vendor.radio.sw_mbn_loaded ""'
adb shell sync
adb reboot
```
## 3. Capture from as early as possible after boot
```sh
adb wait-for-device
adb root && adb wait-for-device
# Dump the whole radio ring immediately — the 64M buffer should still hold the early
# provision window (~10-40s into boot).
adb logcat -b radio -d > ./fp-capture/radio-firstprov.txt
adb logcat -b system -d > ./fp-capture/system-firstprov.txt
adb shell 'getprop | grep -E "vendor.radio.(cnv|hw_mbn|sw_mbn|mbn)"' | tee ./fp-capture/props.after.txt
```
If the early window was missed (buffer wrapped), repeat from §2 but instead of a plain dump, run
a **streaming** capture across the reboot: `adb logcat -b radio > radio-firstprov.txt &` then
`adb reboot`; logcat auto-reconnects on `wait-for-device` with `adb logcat ... -T 1`.
## 4. Analyze — the three questions
```sh
cd ./fp-capture
# (Q1) Did QCRIL actually run the load path this time (vs skip again)?
grep -niE 'load_config|select_config|activate_config|REQUEST_MBN|start_mbn_update|is_ssr_or_bootup' radio-firstprov.txt | head -40
# (Q2) THE unknown: is pdc_enable_auto_selection / enable_modem_update called, and when
# relative to load? This settles gap-analysis item #8 / open question #2.
grep -niE 'enable_auto_select|auto.?select|enable_modem_update|disable_modem_update|set_feature_version' radio-firstprov.txt
# (Q3) The decision point — what did prev_ver/cur_ver read this time?
grep -niE 'is_modem_mbn_updated|prev_ver_info|cur_ver_info' radio-firstprov.txt
```
## 5. Interpreting the outcome (both results are informative)
- **QCRIL runs load→select→activate** → we finally have the real sequence + ordering + timing, and
Q2 tells us if `enable_auto_selection` is a required precondition. Feeds path A (`qcril-prov.py`).
- **QCRIL still skips** (queries modem, finds the active config already present in modem EFS, just
rewrites the prop) → strong confirmation of **frontier #4 path B**: the modem self-persists in
`modemst` and userspace never re-loads in normal operation. That makes the pmOS fix
"provision-once + ensure `modemst` persistence," not a per-boot loader.
## 6. Restore (only if anything misbehaves)
QCRIL repopulates `/data/vendor/modem_config/` and the props automatically on the provision pass,
so normally no restore is needed. If you want the exact prior cache back:
```sh
adb push ./fp-capture/modem_config.bak.tgz /data/local/tmp/
adb shell 'cd /data/vendor && rm -rf modem_config && tar -xzf /data/local/tmp/modem_config.bak.tgz'
adb reboot
```
EFS baseline (untouched by this procedure) remains at `efs-postandroid/{modemst1,modemst2,fsg,fsc}.img`.

View file

@ -0,0 +1,392 @@
/*
* LOS pil-msa.c / peripheral-loader.c modem boot sequence
* TRANSLATED to mainline qcom_q6v5_mss.c coding style.
*
* Purpose: make the downstream boot ordering readable so the delta
* with mainline is obvious. Not compilable a reference document.
*
* LEGEND:
* [SAME] mainline does the same thing
* [DELTA] mainline does something different (the interesting part)
* [EXTRA] downstream does this, mainline does not
* [MISS] mainline does this, downstream does not
*
* Source: los-kernel-blueline-4.9/drivers/soc/qcom/pil-msa.c
* los-kernel-blueline-4.9/drivers/soc/qcom/peripheral-loader.c
* Target: linux-7.1.1-sdm845/drivers/remoteproc/qcom_q6v5_mss.c
*/
/* ------------------------------------------------------------------ */
/* DOWNSTREAM MEMORY ASSIGNMENT HELPERS (translated to mainline API) */
/* ------------------------------------------------------------------ */
/*
* [DELTA] Downstream uses hyp_assign_phys() from secure_buffer.c which
* allocates its argument buffers via kzalloc + dmac_flush_range.
* Mainline uses qcom_scm_assign_mem() which allocates from the
* qcom_tzmem pool. Same SCM call (SVC=0x0C CMD=0x16), different
* buffer provenance. Unlikely to matter but noted.
*/
/* HLOS → MSS exclusively */
static int los_assign_to_subsys(struct q6v5 *qproc,
phys_addr_t addr, size_t size)
{
struct qcom_scm_vmperm dest = {
.vmid = QCOM_SCM_VMID_MSS_MSA, /* 0xF */
.perm = QCOM_SCM_PERM_RW,
};
u64 src = BIT(QCOM_SCM_VMID_HLOS);
return qcom_scm_assign_mem(addr, ALIGN(size, SZ_4K), &src, &dest, 1);
}
/* MSS → HLOS exclusively (panics on failure in LOS — they're serious) */
static int los_assign_to_linux(struct q6v5 *qproc,
phys_addr_t addr, size_t size)
{
struct qcom_scm_vmperm dest = {
.vmid = QCOM_SCM_VMID_HLOS,
.perm = QCOM_SCM_PERM_RWX,
};
u64 src = BIT(QCOM_SCM_VMID_MSS_MSA);
return qcom_scm_assign_mem(addr, ALIGN(size, SZ_4K), &src, &dest, 1);
}
/* HLOS → HLOS+MSS shared */
static int los_assign_shared(struct q6v5 *qproc,
phys_addr_t addr, size_t size)
{
struct qcom_scm_vmperm dest[2] = {
{ .vmid = QCOM_SCM_VMID_HLOS, .perm = QCOM_SCM_PERM_RW },
{ .vmid = QCOM_SCM_VMID_MSS_MSA, .perm = QCOM_SCM_PERM_RW },
};
u64 src = BIT(QCOM_SCM_VMID_HLOS);
return qcom_scm_assign_mem(addr, ALIGN(size, SZ_4K), &src, dest, 2);
}
/* HLOS+MSS → target_vmid exclusively (the final handoff) */
static int los_reclaim_mem(struct q6v5 *qproc,
phys_addr_t addr, size_t size, int target_vmid)
{
struct qcom_scm_vmperm dest = {
.vmid = target_vmid,
.perm = (target_vmid == QCOM_SCM_VMID_HLOS)
? QCOM_SCM_PERM_RWX : QCOM_SCM_PERM_RW,
};
u64 src = BIT(QCOM_SCM_VMID_HLOS) | BIT(QCOM_SCM_VMID_MSS_MSA);
return qcom_scm_assign_mem(addr, ALIGN(size, SZ_4K), &src, &dest, 1);
}
/* ------------------------------------------------------------------ */
/* THE BOOT SEQUENCE (downstream pil_boot + pil_msa_mss_ops_selfauth) */
/* ------------------------------------------------------------------ */
/*
* This is the FULL modem cold-boot path on LOS blueline, translated
* to mainline style. It's called like mainline's q6v5_start().
*
* Downstream splits this across:
* pil_boot() ops->init_image() ops->mem_setup()
* segment load ops->auth_and_reset()
*
* where init_image = pil_msa_mss_reset_mba_load_auth_mdt()
* mem_setup = pil_mss_mem_setup() [no-op on SDM845]
* auth_and_reset = pil_msa_mba_auth()
*/
static int los_q6v5_start(struct rproc *rproc)
{
struct q6v5 *qproc = rproc->priv;
dma_addr_t mba_phys;
void *mba_virt;
dma_addr_t mdata_phys;
void *mdata_virt;
int ret;
/* ============================================================
* PHASE 1: Load MBA (from pil_mss_reset_load_mba)
* ============================================================ */
/*
* [DELTA] MBA buffer: downstream allocates a DMA buffer from
* general DDR. Mainline uses the static reserved-memory node
* mba@98500000 (2 MB). Both work for MBA load, but TZ may
* treat them differently for hyp-assign. On blueline, the
* mainline approach works (MBA boots, no -22).
*/
mba_virt = dma_alloc_attrs(qproc->dev, SZ_1M, &mba_phys,
GFP_KERNEL, DMA_ATTR_SKIP_ZEROING);
memcpy(mba_virt, mba_fw->data, mba_fw->size);
wmb();
/* Load debug policy if present (same in both) */
/* [SAME] */
/*
* [SAME] Assign MBA buffer to MSS so PBL can read it.
* Downstream: pil_assign_mem_to_subsys(mba_dp_phys, mba_dp_size)
* Mainline: q6v5_xfer_mem_ownership(mba_perm, false, true, mba_phys, mba_size)
*/
if (qproc->need_mem_protection) {
ret = los_assign_to_subsys(qproc, mba_phys, SZ_1M);
if (ret)
goto err_mba;
}
/*
* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
* [DELTA THE KEY DIFFERENCE]
*
* Downstream does NOT pre-assign the MPSS region here.
* Mainline (before our fix) did:
* q6v5_xfer_mem_ownership(mpss_perm, false, true,
* mpss_phys, mpss_size);
* This 120-152 MB exclusive HLOSMSS assignment happens BEFORE
* MBA boots, and blueline TZ may reject it (-22), causing the
* XPU crash-loop.
*
* Our commit c2923cb49 gates this on (version != MSS_SDM845),
* matching downstream. VERIFIED: modem boots clean on 7.1.1.
* <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
*/
/* [SAME] Power up MSS, enable clocks, assert/deassert reset */
ret = pil_mss_power_up(qproc);
ret = pil_mss_enable_clks(qproc);
pil_mss_assert_resets(qproc);
udelay(200);
pil_mss_deassert_resets(qproc);
/* [SAME] Program MBA address into RMB register */
writel(mba_phys, qproc->rmb_base + RMB_MBA_IMAGE);
mb();
/* [SAME] Bring Q6 out of reset */
pil_q6v5_reset(qproc);
/* [SAME] Wait for PBL success, then XPU_UNLOCKED from MBA */
ret = q6v5_rmb_pbl_wait(qproc, 1000);
ret = q6v5_rmb_mba_wait(qproc, RMB_MBA_XPU_UNLOCKED, 1000);
dev_info(qproc->dev, "MBA boot done\n");
/* ============================================================
* PHASE 2: Auth modem metadata (from pil_msa_auth_modem_mdt)
* ============================================================ */
/*
* [DELTA] Metadata buffer: downstream allocates a DMA buffer,
* copies metadata, assigns to MSS, programs RMB, waits for
* META_DATA_AUTH_SUCCESS, then reclaims to Linux.
*
* Mainline (q6v5_mpss_init_image) does essentially the same
* but uses the mpss-metadata reserved-memory region instead
* of a DMA allocation. Functionally equivalent.
*/
mdata_virt = dma_alloc_attrs(qproc->dev, mdata_size, &mdata_phys,
GFP_KERNEL, DMA_ATTR_SKIP_ZEROING);
memcpy(mdata_virt, mdt_metadata, mdata_size);
wmb();
if (qproc->need_mem_protection)
los_assign_to_subsys(qproc, mdata_phys, mdata_size);
writel(0, qproc->rmb_base + RMB_PMI_CODE_LENGTH_REG);
writel(mdata_phys, qproc->rmb_base + RMB_PMI_META_DATA_REG);
writel(CMD_META_DATA_READY, qproc->rmb_base + RMB_MBA_COMMAND_REG);
ret = q6v5_rmb_mba_wait(qproc, RMB_MBA_META_DATA_AUTH_SUCCESS, 10000);
if (qproc->need_mem_protection)
los_assign_to_linux(qproc, mdata_phys, mdata_size);
dma_free_attrs(qproc->dev, mdata_size, mdata_virt, mdata_phys, 0);
/* ============================================================
* PHASE 3: mem_setup (from pil_mss_mem_setup)
* ============================================================ */
/*
* [SAME] SDM845 does NOT set pil_mss_memsetup in DT.
* Downstream: this function returns 0 immediately.
* Mainline: need_pas_mem_setup = false for SDM845.
* Both skip PAS_MEM_SETUP_CMD. No delta.
*/
/* ============================================================
* PHASE 4: MPSS region assignment + segment loading
* (from pil_boot lines 1063-1119)
* ============================================================ */
/*
* [SAME on cold boot, DELTA on SSR]
*
* Cold boot (modem_ssr == false):
* - Skip the reclaim-to-linux (MPSS is already Linux-owned)
* - Go straight to shared assignment
*
* SSR (modem_ssr == true):
* - First reclaim: MSSHLOS exclusively
* - Then share: HLOSHLOS+MSS
*
* Mainline does the reclaim unconditionally (lines 1504-1505)
* but on cold boot it's a no-op because mpss_perm already
* equals HLOS. So functionally equivalent on cold boot.
*/
if (modem_ssr) {
/* SSR only: reclaim from previous boot */
los_assign_to_linux(qproc, qproc->mpss_phys, qproc->mpss_size);
}
/*
* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
* [SAME this matches mainline line 1508]
*
* Share MPSS between HLOS and MSS during segment loading.
* Downstream: pil_assign_mem_to_subsys_and_linux()
* srcVM = [HLOS], destVM = [HLOS, MSS_MSA], perm = [RW, RW]
* Mainline: q6v5_xfer_mem_ownership(mpss_perm, true, true, ...)
* srcVM from current_perm, destVM = [HLOS, MSS_MSA]
* Same SCM call. VERIFIED matching.
* <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
*/
ret = los_assign_shared(qproc, qproc->mpss_phys, qproc->mpss_size);
if (ret)
goto err_mpss;
/* [SAME] Load each MPSS segment into the shared region */
for_each_segment(seg) {
ptr = memremap(qproc->mpss_phys + offset, seg->memsz,
MEMREMAP_WC);
/* copy from firmware or zero-fill */
memcpy(ptr, seg_data, seg->filesz);
memset(ptr + seg->filesz, 0, seg->memsz - seg->filesz);
memunmap(ptr);
/* [SAME] Program RMB for MBA to verify each segment */
if (!code_length) {
writel(boot_addr, qproc->rmb_base + RMB_PMI_CODE_START_REG);
writel(RMB_CMD_LOAD_READY, qproc->rmb_base + RMB_MBA_COMMAND_REG);
}
code_length += seg->memsz;
writel(code_length, qproc->rmb_base + RMB_PMI_CODE_LENGTH_REG);
}
/* ============================================================
* PHASE 5: Final MPSS handoff + auth complete
* (from pil_boot lines 1108-1127)
* ============================================================ */
/*
* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
* [DELTA SUBTLE DIFFERENCE IN THE FINAL HANDOFF]
*
* Downstream: pil_reclaim_mem(region, subsys_vmid)
* srcVM = [HLOS, MSS_MSA] (both listed explicitly)
* destVM = [MSS_MSA]
* This transfers from shared MSS exclusive.
*
* Mainline: q6v5_xfer_mem_ownership(mpss_perm, false, true, ...)
* srcVM = from current_perm (which is HLOS|MSS after the share)
* destVM = [MSS_MSA]
* Same result but mainline tracks perm state in mpss_perm
* while downstream hardcodes the srcVM.
*
* FUNCTIONALLY EQUIVALENT. The end state (MSS exclusive) is
* the same. VERIFIED.
* <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
*/
ret = los_reclaim_mem(qproc, qproc->mpss_phys, qproc->mpss_size,
QCOM_SCM_VMID_MSS_MSA);
if (ret)
goto err_mpss;
/* [SAME] Wait for MBA to report authentication complete */
ret = q6v5_rmb_mba_wait(qproc, RMB_MBA_AUTH_COMPLETE, 10000);
/* ============================================================
* PHASE 6: Reclaim MBA + cleanup
* (from pil_msa_mba_auth)
* ============================================================ */
/*
* [DELTA] Downstream reclaims MBA DMA buffer: MSSHLOS, then
* frees the DMA allocation. Mainline reclaims the mba_region
* reserved-memory. Same assign direction, different region type.
*/
if (qproc->need_mem_protection)
los_assign_to_linux(qproc, mba_phys, SZ_1M);
dma_free_attrs(qproc->dev, SZ_1M, mba_virt, mba_phys, 0);
return 0;
err_mpss:
err_mba:
return ret;
}
/* ================================================================== */
/* SUMMARY OF DELTAS */
/* ================================================================== */
/*
* 1. MBA BUFFER LOCATION
* Downstream: dma_alloc_attrs() general DDR, dynamic
* Mainline: reserved-memory mba@98500000 static carveout
* Impact: None observed MBA boots fine both ways.
*
* 2. MPSS PRE-ASSIGN (the one we fixed)
* Downstream: NEVER assigns MPSS before MBA boot
* Mainline: Was HLOSMSS exclusively before MBA boot
* Fix: c2923cb49 skips for MSS_SDM845
* Status: VERIFIED WORKING modem boots clean, no -22
*
* 3. METADATA BUFFER LOCATION
* Downstream: dma_alloc_attrs() general DDR
* Mainline: reserved-memory mpss-metadata@bfffc000 (16 KB)
* Impact: None observed.
*
* 4. rmtfs REGION SCM ASSIGN
* Downstream: NO SCM call uses qcom,sharedmem-uio, no TZ
* Mainline: qcom_scm_assign_mem(0xf2700000, 0x202000,
* HLOSHLOS+MSS_MSA)
* Impact: Assign succeeds on blueline (verified). But this
* EXTRA TZ interaction doesn't exist downstream.
*
* 5. FASTRPC / SLPI VMIDs
* Downstream: Not applicable (different fastrpc driver)
* Mainline: qcom_scm_assign_mem for fastrpc heap with
* [HLOS, SSC_Q6] TZ rejects with -22
* Impact: SLPI crash-loops. Independent of modem.
*
* 6. OPERATING MODE AT BOOT
* Downstream: modem firmware enters LPM at boot
* Mainline: modem firmware enters OFFLINE at boot
* Impact: THIS IS THE REMAINING BLOCKER.
* The driver-level boot sequence is now correct
* (matching downstream ordering). The firmware
* itself decides LPM vs OFFLINE based on... what?
* NOT the driver the assign ordering is matching.
* Candidates:
* - ABL/bootloader state (verified-boot chain)
* - QCRIL "UI ready" handshake (rejected by firmware)
* - Some EFS NV item the firmware checks at RF-init
* - Subsystem coordination (SLPI PD state?)
*
* CONCLUSION:
* The driver-level memory assignment is now correct. The modem boots
* clean with zero XPU/assign errors. The remaining 52/offline blocker
* is ABOVE the driver layer in the modem firmware's own RF-init
* decision logic.
*/

View file

@ -0,0 +1,134 @@
# Community research — blueline modem on mainline (2026-06-22)
Purpose: GLM's "cheap parallel" ask — is there a KNOWN blueline/SDM845 modem-on-mainline fix we'd
otherwise rediscover? Done read-only (phone was on Android). Verdict + a new lead that reorders the
frontier.
## Headline: there is no known blueline fix to rediscover
- The **sdm845-mainline** project's actively-supported, modem-working devices are **OnePlus 6/6T
(enchilada/fajita) and Pocophone F1 (beryllium)** — NOT the Pixel 3 generation.
- **crosshatch (Pixel 3 XL — blueline's near-identical twin) modem is also NOT confirmed working** on
mainline (kernel boots, SSH ok; WiFi/BT/modem are the open items). So this is a **Pixel-3-generation
gap, not a blueline-unique one.** Nobody has shipped this modem on mainline.
- Implication: don't wait on / search for a blueline-specific recipe — there isn't one. The play is to
port the **enchilada** (known-good) path and isolate the **device delta**.
## The concrete bring-up chain (now confirmed from sources)
Modem firmware **RF-init → reads radio CALIBRATION from EFS (`modemst1`/`modemst2`) → via `rmtfs`
the `qcom_rmtfs_mem` reserved-memory carveout**. kernel.org confirms `qcom_rmtfs_mem`'s client "is
generally found in the modem firmware and is used for accessing persistent storage for things such as
**radio calibration**." `modemst1/2` hold the NV/cal the modem needs to come online.
### Why this reorders the frontier (the new lead)
GLM's strongest surviving lead is **Route B: a modem-side EFS NV item** (mcfg_autoselect_by_uim /
power-up op-mode), to be read over **DIAG**. But DIAG NV-read uses the **AP↔modem DIAG channel** — a
*different* path than the modem's **own rmtfs-mediated EFS access during RF-init**. So:
> The NV *content* can be byte-perfect (it is — shared EFS) and the modem can STILL park offline if the
> **rmtfs ↔ qcom_rmtfs_mem ↔ EFS** bridge is broken/misconfigured on pmOS. That bridge is **upstream**
> of the NV-content question and is exactly the kind of device-specific thing that's wrong on an
> untested device tree.
This is consistent with everything GLM proved equal (firmware, EFS bytes, configs, stack, DT
"byte-identical to upstream") — because **upstream blueline DT modem support is itself unproven**
(nobody got the modem up), so a wrong/incomplete `qcom,rmtfs-mem` carveout or modem `memory-region`
would not have been caught.
## What enchilada (works) does that we should check against blueline
- Ships a curated **`firmware-oneplus-sdm845`** set; modem cal/region files matter (VoLTE needs extra
carrier config files).
- **`rmtfs-copy-files` (pmaports MR !4674, draft):** the modem WRITES to its EFS at runtime and those
writes must persist; they copy the partitions at boot and write changes back. EFS is "a black box —
a diff before/after shows almost every byte changed." (We share EFS unslotted, so reads are fine;
but note the modem expects a writable EFS path via rmtfs.)
- Uses **`hexagonrpcd`** (open Hexagon DSP RPC) — DSP/sensors, but part of the enchilada stack.
- Voice/VoLTE historically needed **provisioning from a specific OxygenOS build first** — i.e. the
working modem was put in a known EFS/NV state from Android before pmOS. (Rhymes with Route B.)
## Proposed instruments (complementary to GLM's DIAG NV probe — do on the SAME pmOS boot)
All mostly read-only; one clean boot.
1. **Verify the rmtfs/carveout chain is actually bridging during the failed RF-init** (NEW, highest value):
- Is `qcom_rmtfs_mem` reserved-memory present and at the address the modem firmware expects?
`cat /proc/iomem | grep -i rmtfs`, `dmesg | grep -i rmtfs`, check the DT `qcom,rmtfs-mem` node.
- Is `rmtfs` serving `modemst1/2`? Run it foreground/verbose or `strace -f` during modem boot:
does the modem issue rmtfs read requests for the EFS, and do they **succeed**?
- modem issues reads that **fail** → that's the gate (wrong partition map / perms).
- modem issues reads that **succeed** but still offline → bridge is fine, push to GLM's NV-content + SSR reason.
- modem issues **no** rmtfs reads → RF-init never reaches EFS; gate is earlier (carveout/glink/firmware load).
- Cross-check rmtfs invocation/flags vs enchilada's (`device-oneplus-enchilada` + its rmtfs service).
2. **GLM's DIAG NV read** (mcfg_autoselect_by_uim, power-up op-mode) via edl-pixel `qc_diag` + nvitems.xml — still worth it, but now interpreted as "content check," with #1 as the bridge check.
3. **Forced modem SSR + devcoredump/minidump for the firmware's own failure reason** (escalation):
- `echo enabled > /sys/kernel/debug/remoteproc/remoteprocN/coredump`, trigger SSR via
`/sys/kernel/debug/msm_subsys/modem` or a failed online, then read `/sys/class/devcoredump/devcdN/data`.
- Also check `sdm845-mainline/pmtools` for an existing diag/modem helper (per diag_capture.md note).
## Open question worth one cheap action
Does **crosshatch** (the twin) hit the **identical** DeviceNotReady(52)? If yes, it's confirmed a
family/DT-level gap and any crosshatch progress is directly portable. A targeted ask in
sdm845-mainline (Caleb Connolly / Joel Selvaraj orbit) about Pixel-3-gen modem status would settle it
without us reverse-engineering blind. (The DeviceNotReady QMI-52 itself is generic — the modemmanager
list thread on it reached no root cause; it just means "modem firmware not in a ready state," which
points back to RF-init/cal, i.e. the chain above.)
## Sources
- [SDM845 Mainlining — postmarketOS Wiki](https://wiki.postmarketos.org/wiki/SDM845_Mainlining)
- [OnePlus 6 (oneplus-enchilada) — postmarketOS Wiki](https://wiki.postmarketos.org/wiki/OnePlus_6_(oneplus-enchilada))
- [Pixel 3 XL (google-crosshatch) — postmarketOS Wiki](https://wiki.postmarketos.org/wiki/Google_Pixel_3_XL_(google-crosshatch))
- [pmaports MR !4674 — modem/rmtfs: add rmtfs-copy-files](https://gitlab.com/postmarketOS/pmaports/-/merge_requests/4674)
- [pmaports MR !1640 — make modem work on downstream again](https://gitlab.com/postmarketOS/pmaports/-/merge_requests/1640)
- [kernel.org — qcom,rmtfs-mem.txt binding](https://www.kernel.org/doc/Documentation/devicetree/bindings/reserved-memory/qcom,rmtfs-mem.txt)
- [linux-msm/rmtfs](https://github.com/linux-msm/rmtfs)
- [Caleb Connolly — state of mainline on SDM845 (5.14)](https://connolly.tech/posts/2021_07_20-sdm845-mainline-5.14/)
- [FOSDEM 2022 — From Android to mainline on the Snapdragon 845 (slides)](https://archive.fosdem.org/2022/schedule/event/mobile_kernel_snapdragon_845/)
- [sdm845-mainline GitLab group](https://gitlab.com/sdm845-mainline)
- [crosshatch mainlining attempt (#153)](https://gitlab.com/postmarketOS/pmaports/-/issues/153)
- [modemmanager-devel — DeviceNotReady on set operating mode](https://www.mail-archive.com/modemmanager-devel@lists.freedesktop.org/msg07101.html)
---
## pmOS-side LIVE RESULTS (2026-06-22 boot, slot A, kernel 7.1.0-rc1) — instruments run, doors closed
Ran the proposed battery on a fresh pmOS boot. Results eliminate the rmtfs lead AND both pmOS deep-probe paths, and corroborate GLM's reframe from a second angle.
1. **rmtfs bridge: HEALTHY this boot — eliminated as the gate.** `rmtfs -P -s` (pid 697) has every link
open: `/dev/qcom_rmtfs_mem1` (fd5), `remoteproc2/state` (fd6), `modemst1`/`modemst2` (sdf3/sdf4),
`fsg`/`fsc` (sdf5/sdf6), and OEM copies in `/var/lib/rmtfs/`. Carveout `rmtfs-region@f2700000`
present. The `Failed to get rprocfd` crashes are *other* boots, not this one. **Modem reads its EFS
via rmtfs successfully and still parks offline** → bridge is not the gate.
2. **DIAG unavailable on pmOS** — no `/dev/diag`, no diagchar module (mainline doesn't ship it, as
diag_capture.md warned). **GLM's Route-B DIAG NV read cannot run on pmOS.** It's Android-only.
3. **SIM/UIM fully ready**`uim-get-card-status`: card present, USIM app `ready`, Primary GW
provisioned (slot 1 / app 1), PIN1 disabled. Reads work.
4. **NEW corroborating signal — modem refuses STATE CHANGES, serves all READS:**
- `dms-set-operating-mode=online` → instant `DeviceNotReady (52)` (clean QMI: Alloc CID → Set Op
Mode → FAILURE → Release CID; modem replies immediately, does not hang).
- `uim-change-provisioning-session` (what `msm-modem-uim-selection` runs) → **Transaction timed out**,
repeatably, even with modem fully up.
- vs reads (card status, op-mode get, qrtr service list) → all succeed instantly.
- The modem registered its QMI services on qrtr (NAS, Voice, PDC, **Modem EFS svc 21**, etc.).
5. **devcoredump path yields nothing** — armed `remoteproc2/coredump=enabled`, attempted online: modem
does NOT crash/SSR, no `devcdN`, no SMEM failure string, no dmesg modem reaction. **There is no crash
reason to harvest** — the modem is in a stable, deliberate "offline / not RF-ready" state.
6. ModemManager is **masked** (intentional, for manual qmicli testing). Modem not poisoned by the
attempt (clean refusal); phone still usable on pmOS.
### What this leaves (frontier now)
Every STATIC AP-side layer is proven equal/healthy: firmware loads, rmtfs serves EFS, EFS bytes shared
& identical to Android, SIM ready, configs active (GLM), DT byte-identical. The modem firmware itself
decides RF-init is incomplete and **fast-refuses every state transition** while answering queries — and
exposes no reason on pmOS (no DIAG, no crash). So a *static* NV/EFS diff is NULL (confirmed direction)
and the deep-probe instruments are exhausted on pmOS.
**The remaining differentiator is RUNTIME: the QMI choreography Android's `qcril` performs before/around
going online** that pmOS (cold `set-operating-mode=online`) never does. The qcril strings we pulled show
exactly this shape — `INTERNAL_MMGSDI_CARD_POWER_UP`, indication-register / set-event-report across
DMS/NAS/UIM, `qcril_qmi_nas_trigger_polling_operating_mode`, `POST_OPRT_ONLINE_ACTION`, the bootup
optimization state machine. The `uim-change-provisioning-session` timeout is plausibly part of the same
missing handshake.
### Next action (the fix path, not another probe)
Decompile the pulled `qcrild` / `libril-qc-hal-qmi.so` (`android-reference/vendor-stack-20260621/`) to
extract the exact pre-online QMI request sequence, then **replay that choreography on pmOS over
libqmi/qmicli before `set-operating-mode=online`** (the qcom-imsd / provisioner pattern). That is the
discriminating *and* corrective experiment; the read-only probes have told us all they can.

View file

@ -0,0 +1,76 @@
# PAF · Community research + reconciliation (2026-06-26)
Prior-art sweep run against our live evidence from this session (the modem
XPU-violation crash-loop = the 52; see modem.md TOP LEAD #11). Conclusion up
front: **the community's known fix does NOT match our failure mode; our wall
(secure-load / XPU) is likely uncharted in the public SDM845 mainline ecosystem.**
## What the evidence RULES OUT
- **tqftpserv `rsize`/`seek` MCFG bug** (the Fairphone 4 / SM7225 unblock).
- Refs: ModemManager issue #484
(gitlab.freedesktop.org/mobile-broadband/ModemManager/-/issues/484);
tqftpserv PR #13 (github.com/andersson/tqftpserv/pull/13); pmaports !4747 / #2550.
- Their modem fetched MCFG with `rsize=`/`seek=` TFTP options that an old
tqftpserv mis-served → modem never RF-inits → permanent DeviceNotReady(52).
- **NOT us:** our tqftpserv RRQ lines show `rsize=0 seek=0`, and tqftpserv is
actively serving `/readwrite/mcfg.tmp`. Transport works. Our modem also does
NOT sit stably offline — it **crash-loops** (theirs stayed up and refused).
## What the evidence RULES IN / leaves open
- **pd-mapper / MPSS protection domains.** Research: SDM845 needs rmtfs +
pd-mapper + tqftpserv; pd-mapper brokers `mpss_root_pd` / `mpss_wlan_pd`.
- Refs: in-kernel pd-mapper v9 (mail-archive linux-kernel msg2570727);
LWN 973399; RFC linux-remoteproc msg00281 (modemuw.jsn side-load).
- **Our state:** userspace `pd-mapper` = `inactive / not-found` (likely not
installed). Kernel PDR framework is partly alive (`PDR: Indication …
adsp/audio_pd`). OPEN next-session check: does the **MPSS** protection domain
actually register in-kernel, or is the modem's PD coordination missing? PD =
the secure-memory layer the XPU violation lives in, so this is the lead that
fits our evidence.
- Note: no public source explicitly ties pd-mapper absence to DeviceNotReady(52)
or to an XPU violation — inference, not documented.
- **Platform MCFG missing (0 platform configs, 25 software, Rogers active).**
- openimsd `qcom-baseband-profile-manager` (openimsd.com; NLnet VoLTE-Qualcom
grant) selects among EXISTING software configs — it does not manufacture a
missing platform/HW MCFG. OnePlus 6 ships `firmware-oneplus-sdm845` with
factory MCFG extracted from Android; blueline's EFS lacks the platform MCFG.
- **No prior art** for the zero-platform-MCFG case or `--pdc-load-config` of a
platform MBN from scratch. Possibly novel. But per our timeline the modem
crashes (XPU) *before* the platform-MCFG layer matters, so this is downstream
of the secure-load fault, not the first blocker.
## Confirmed external bugs (not ours to fix)
- **`qmicli --pdc-list-configs` segfault = upstream libqmi issue #24**
(gitlab.freedesktop.org/mobile-broadband/libqmi/-/issues/24, since 1.24.0,
segfaults in PDC response cleanup AFTER printing). Not the openimsd fork's
fault. Workaround: ignore exit code / `2>&1`. Watch libqmi git log on
`qmi-pdc.c` cleanup for the fix version.
- **PDC Set Feature 0x2E timeout** — zero public reports. Inferred cause: PDC
service present on QRTR but not processing because the modem hasn't finished
init (consistent with our crash-loop). Not independently a blocker.
## Net direction (for next session)
The 52 is a secure-load / TZ-XPU problem unique enough that the community has no
recipe. Concrete next experiments, cheapest first:
1. Confirm whether **MPSS protection domain** registers in-kernel; if not, try
installing/running userspace `pd-mapper` (apk add pd-mapper) and re-capture
whether the XPU violation changes.
2. Pull the **`qcom_scm_assign_mem` -22** thread: diff LOS pil-tz secure-load
region/VMID setup (mba `qcom,mba-mem`, mpss assign) vs mainline `q6v5_mpss`
on this device — the XPU violation means the modem's secure regions aren't
assigned the way TZ expects.
3. Verify whether 6.16.7 *also* XPU-crash-loops — but NOT as a regression test.
**Correction (Casey, 2026-06-26):** no mainline kernel ever had a working modem.
6.16.7 "booted + QMI registered" (lead #10) but never went online either — that
is NOT a working-modem baseline. The ONLY known-working modem state is
**LineageOS on its downstream kernel** (PAF provenance: `msm-4.9` 4.9.337, slot B
— Casey recalls it as ~4.x; the substance is *downstream LOS, not mainline*).
So the only valid parity yardstick is **mainline vs LineageOS**, never
6.16-vs-7.1.1. Checking whether 6.16.7 XPU-loops is still useful (tells us if the
XPU fault is mainline-wide vs 7.1.1-specific), but frame it as a mainline-wide
question, not "did we regress from a working 6.16."

View file

@ -0,0 +1,311 @@
# PAF · The modem userspace CONSUMER CHAIN — why the modem stays OFFLINE (2026-06-26, Opus)
> ## SESSION-3 UPDATE (2026-06-27, Opus) — SLPI SAR CRASH LOOP **FIXED**; ~~PROVEN the SLPI does NOT gate the modem 52~~
>
> ### TL;DR
> Built the missing **AP-side SNS registry provider** (a patched `hexagonrpcd`) that feeds the
> SLPI its sensor registry over FastRPC `apps_std`. The SLPI's `sar.cc:27:chre_utils fatal`
> crash-loop (150+ crashes/boot) is **broken — SLPI now stays up indefinitely** (0 crashes over
> multiple 2540s windows). **Then enabled the modem anyway → still `QMI error 52
> DeviceNotReady`.** So the SLPI/SAR subsystem is **independent of** the modem-52 wall. That
> question (could not be answered by *disabling* SLPI — only by making it actually work) is now
> settled: **error 52 is not SLPI-gated.**
>
> **🟠 SUPERSEDED 2026-06-29 (Casey) — this conclusion is INVALID.** The "SLPI fixed / 0 crashes / stays
> up" above was only *remoteproc-not-crashing*: this doc's own body (search *"never exposes an SNS QMI
> service"*) shows the SLPI firmware never completed CHRE init / never exposed SNS QMI — i.e. **not a
> functioning sensor stack.** The reasoning "disabling couldn't answer it, only making it work can" was
> correct — but "making it work" was never achieved, only "stopped crashing." If the modem's RF-init
> queries the SLPI and gets silence from a never-initialized sensor stack, it parks OFFLINE regardless.
> The SLPI is a **candidate root-cause**, NOT proven-independent. See `slpi.md` top banner + `HANDOFF.md`.
>
> ### HOW the SLPI was fixed (the real consumer chain Android calls `sscrpcd`)
> The SLPI boots → makes FastRPC **reverse** calls (`apps_std` fopen/fread) back to the AP to read
> `/persist/sensors/registry/...`. pmOS shipped **no AP-side listener**, so the registry sensor
> starved and the SAR (sx932x) CHRE nanoapp fataled → crash loop. The pmOS-native listener
> **`hexagonrpcd`** (linux-msm, already in the repo, `-s` = attach sensorspd on `/dev/fastrpc-sdsp`)
> is the right tool but had two gaps:
> 1. **Ran as `User=fastrpc`** → couldn't read the persist registry files. Fixed with a systemd
> drop-in `User=root` (`/etc/systemd/system/hexagonrpcd-sdsp.service.d/10-root.conf`).
> 2. **`construct_root_dir()` (rpcd_builder.c) only mapped `registry/registry/*`** — its virtual
> FS had **no nodes** for the top-level `sns_reg_config` / `sns_reg_version` / `sns_reg_ctrl`
> that blueline's SLPI requests at `/persist/sensors/registry/<file>`. hexagonfs is node-based
> (no real-FS fallthrough), so those ENOENT'd no matter what was on disk. **Patched** to add
> those three nodes (`hfs_map_or_empty`). NOTE: `hfs_map_or_empty` presents a missing physical
> file as an empty **directory** (`S_IFDIR`), which still fails a *file* open — so the physical
> file must EXIST (even 0 bytes). `sns_reg_version` is absent from the Android dump → `touch` an
> empty one and it serves as a regular empty file, which satisfies the SLPI.
> 3. Registry DATA staged to **`/usr/share/qcom/sensors/registry/`** (hexagonrpcd's default `-R`
> root; the 177 groups from the LineageOS persist dump + `sns_reg_config`, empty `sns_reg_version`,
> `sns_reg_ctrl`). Persist files also `chmod -R a+rX` (they were `0600 casey:casey` from a tar
> restore).
>
> ### STATE OF THE FIXES — now converted from live temps to REAL in-code fixes (2026-06-27)
> All of the live drop-in/temp changes have been turned into aport sources so a clean
> `pmbootstrap install` reproduces them (idempotent fresh + upgrade). What's in code now:
> - **hexagonrpcd patch → local aport** `pmaports/temp/hexagonrpcd/` (fork of Alpine community
> aport, `pkgrel=4`, adds `hexagonrpcd-blueline-registry-nodes.patch`, checksums regenerated).
> `pmbootstrap build hexagonrpcd` produces the patched binary instead of the live hand-installed
> `/usr/bin/hexagonrpcd` overwrite.
> - **root drop-in**`device-google-blueline/hexagonrpcd-sdsp-root.conf`, installed to
> `/usr/lib/systemd/system/hexagonrpcd-sdsp.service.d/10-root.conf` (APKBUILD `package()`).
> - **registry data** → NOT shipped as blobs. `blueline-sns-registry-stage.sh` +
> `.service` (oneshot, `Before=hexagonrpcd-sdsp`, ordered after persist mount) stage the registry
> from the device's OWN `/mnt/vendor/persist/sensors/registry` into `/usr/share/qcom/sensors/registry`
> (hexagonrpcd's served root; persist layout is off-by-one for a single `-R`). Touches the empty
> `sns_reg_version`. Idempotent, ships no proprietary calibration.
> - **service enable** → preset enables `blueline-sns-registry-stage.service` + `hexagonrpcd-sdsp.service`.
> - **persist perms**: handled by the stage script's `chmod -R a+rX` on the served copy; the persist
> `chmod` I did live is NOT needed by the real fix (hexagonrpcd reads `/usr/share/qcom`, not persist).
>
> **REMAINING build/verify steps (need device + reflash — do NOT half-run unattended):**
> 1. `pmbootstrap build hexagonrpcd` (local aport) + `pmbootstrap build device-google-blueline`.
> 2. Install/upgrade on device (`pmbootstrap` sideload or apk add the built .apk) — replaces the
> hand-installed binary + lands the drop-in/stage-service/preset.
> 3. Reboot clean and VERIFY idempotency: SLPI stays up (0 `crash #` delta), provider active,
> `blueline-sns-registry-stage` populated `/usr/share/qcom/sensors/registry`.
> 4. Revert the hand-installed temps once the packages own them: restore `/usr/bin/hexagonrpcd.orig`
> is NOT needed (apk upgrade overwrites), remove the manual `systemctl enable` (preset handles it).
> - **Kernel DT vmid change** (`sdm845.dtsi` sdsp fastrpc `qcom,vmids = <HLOS SSC_Q6>`): I hand-patched
> the DTB into boot_a to test the TZ hyp-assign theory. LOGGED RESULT: with `<3 5>` live (confirmed
> via `dtc` decompile of `/sys/firmware/fdt`), `dmesg` still prints
> `SDSP fastrpc hyp-assign rejected by TZ (-22); registering node without reserved-heap assignment`
> (32 occurrences). So the vmid change did NOT change the TZ result — the crash fix was the registry
> serving, not the heap. The change is harmless but unnecessary.
> - Orig boot.img backups: scratchpad `boot.img` + `/tmp/boot-backup_a.img` on device.
> - NOTE on a reboot during testing: `lsusb` showed `18d1:d001` and SSH returned `Network is
> unreachable`; root cause was the HOST iface `enp0s20f0u6 DOWN` (per `ip -br addr`), fixed by
> `ip link set up` + `ip addr add 172.16.42.2/24` → phone answered, `uptime` = up 2 min. The
> phone had booted normally; it was a host USB-net enumeration issue, not a device boot failure.
>
> ### THE REMAINING WALL — modem `QMI 52 DeviceNotReady`, now provably INDEPENDENT of SLPI
> With SLPI fully up + sensors registry served, `mmcli -m 0 -e` still fails at DMS
> `set-operating-mode` with **QMI 52**. modem0 enumerates (QUALCOMM, SIM present, slot 2 active),
> sits `disabled / power off`, enable → `enabling → (QMI 52) → disabled`. The MSS remoteproc
> (`4080000.remoteproc`) is `running`. LOGGED: `mmcli -m 0 -e` → `state changed (disabled ->
> enabling)`, `couldn't enable interface: 'Couldn't set operating mode: QMI protocol error (52):
> DeviceNotReady'`, `state changed (enabling -> disabled)`. So 52 comes from the modem path, with the
> SLPI fully up — the two are independent.
> **Scope note:** the goal is the WHOLE device, not just telephony. The SLPI/sensors fix is a real
> bringup win on its own (sensors infra now functional). For the 52 specifically, the SLPI is not the
> lever. **Next session for 52: modem NV/EFS/identity** (the `model 0` / LOW_POWER hypothesis from
> session 2), rmtfs/EFS sync, or modem firmware-init.
>
> ### Open loose end (minor): hexagonrpcd reconnect churn
> Provider shows `activating` with ~18 restarts/min — it exits on FastRPC "Broken pipe" when the
> SLPI closes the session after serving, then `Restart=always` reattaches. SLPI stays up regardless
> (0 crashes), so it's cosmetic, but worth a `RestartSec`/keepalive tweak later.
>
> ---
>
> ## SESSION-2 UPDATE (2026-06-26 late, Opus) — CHAIN BUILT + WORKING; wall is now the modem 52 / SLPI-SAR firmware crash
>
> ### What got BUILT this session (all in pmaports `device/testing/device-google-blueline/`, deployed live + wired into APKBUILD/preset — idempotent)
> The whole consumer chain now comes up automatically on a clean boot and ModemManager
> starts + creates the modem. Verified on hardware.
> 1. **`modem-qrtr-ready.service` + `modem-qrtr-ready.sh`** (NEW) — the pmOS equivalent of
> Android's `wait_for_prop vendor.qcom.devup 1`. Blocks until modem QMI answers and is stable
> (2 probes/3s) before releasing consumers. Killed the boot race
> (`node with id 0 not found in QRTR bus`). VERIFIED: gate waits, then releases.
> 2. **UIM-selection fix**`blueline-uim-select.sh` (NEW) wraps the stock binary: it selects the
> SIM app fine but then a trailing QMI op times out (SLPI/XPU churn) and exits 1 — a false
> negative that failed MM's hard `Requires=`. Wrapper treats "app selected" as success.
> `uim-wait.conf` rewritten: gate on modem-qrtr-ready, Type=oneshot, run the wrapper. VERIFIED
> `msm-modem-uim-selection` now = active.
> 3. **`qcril-prov.py` — fully fixed** (was crashing on `out.get_configs()`):
> - `list_configs` + `get_selected_config` reworked to read the PDC **indication**
> (`IndicationPdc*Output.get_configs()/get_active_id()`), not the response.
> - GI array-length args fixed (`set_id(id)`, `set_type_with_id_v2(t,id)`).
> - Targets configs by **sha1(.mbn) == PDC config id**; selects+activates HW + Rogers SW.
> - **Skip-if-already-active**: checks get_selected_config; if the inherited config is already
> active, skips select/activate → **0 modem restarts** (was 2). VERIFIED:
> "HW/SW already SELECTED+ACTIVE — no restart".
> - **Deadlock fix**: exits 0 after provisioning even if RF not armed (RF arming is MM's job,
> and MM is gated behind this service — requiring RF here deadlocked). VERIFIED exit 0.
> 4. **`blueline-dpm-open-port.service` / `qcril-prov.service`** — gated on modem-qrtr-ready + retry.
> 5. Kernel: mem-protect skip reverted (`980c4ae1f`); on clean baseline 7.1.1; `verify-fixes.sh` green.
>
> ### THE WALL NOW (single, precise)
> Full chain runs → **ModemManager starts, creates `modem0`** (qcom-soc plugin, 3 ports), reads the
> SIM ATR, goes `disabled → enabling` → calls set-operating-mode → **`QMI error 52 DeviceNotReady`**
> → falls back to `disabled`. This is the SAME 52 from day one. The modem is **OFFLINE, model `0`**
> (up-but-unprovisioned) and **refuses to enable even when driven correctly by MM**. So the entire
> userspace plumbing is now proven-good; the wall is the modem itself refusing RF.
>
> ### SLPI / SAR — the suspected gate (Casey's thesis), what we proved
> - SLPI **crash-loops `sar.cc:27:chre_utils fatal` every ~10s** (crash #15+). SAR = Semtech
> **SX932x**, the modem's RF SAR/TX-power sensor.
> - It is a **CHRE nanoapp crash INSIDE the SLPI firmware at init**, and **qrtr-lookup is empty —
> the SLPI never exposes an SNS QMI service before it dies**. So an AP-side QMI "SAR consumer"
> has nothing to attach to; the producer fatals first. A downstream subscribe-consumer is NOT
> the missing piece.
> - The real missing piece is whatever **feeds the SLPI its sensor registry / CHRE settings at
> init**: the SAR registry data (`sx932x_0.sar`, `.config`, `_platform.*`) IS present at
> `/persist/sensors/registry/registry/` but is **NOT delivered to the SLPI** (its RFS dir
> `/persist/rfs/msm/slpi` is empty; tqftpserv serves only `/persist/rfs`, not `/sensors`).
> - Android's consumer stack (proprietary, NO open-source equivalent):
> **`chre_daemon_msm` + `sensors.qti` + `sscrpcd`** (in `android-reference/vendor-stack-.../vendor/bin/`),
> plus the registry under `android-reference/persist-sensors-20260625/`. Our
> `drivers/soc/qcom/qcom_ssc_sensors.c` is a **passive ABI shim** — it does NOT deliver the
> registry or run CHRE, so it cannot prevent the firmware crash. This is the "shim lacking
> components LineageOS has" Casey referenced.
>
> ### NEXT ANGLE (Casey's call — start here next session)
> 1. **DECISIVE CHEAP TEST FIRST — is SAR actually the modem gate?** Stop the SLPI remoteproc so it
> is NOT crash-looping (`echo stop > /sys/class/remoteproc/remoteproc2/state`), then let MM try
> to enable the modem (or `qmicli --dms-set-operating-mode=online` once). If still **52**
> SLPI/SAR is NOT the modem gate (independent symptoms) → stop chasing SAR for telephony and look
> elsewhere for the 52. If it **goes online** → the SLPI churn was the blocker and the SAR/CHRE
> consumer becomes critical. **Run this before building anything.**
> 2. If SAR IS the gate: the buildable "consumer" is a **registry/CHRE provider** that feeds the
> SLPI at boot (reverse the SNS/CHRE QMI registry protocol; or port pieces; or boot SLPI
> on-demand AFTER preparing its env, like LineageOS `sensors_ssc.c` does — it does NOT auto-boot
> the SLPI raw). Investigate `chre_daemon_msm` / `sscrpcd` behaviour + how the registry reaches
> the SLPI on Android (RFS path? SNS QMI? direct?).
> 3. Independent of SAR: chase **why the modem boots OFFLINE/model-0** (vs Android's LOW_POWER).
> model `0` = NV/identity not fully loaded — possible EFS/NV or firmware-init issue.
> 4. Fallback: downstream-kernel hybrid (Droidian/Halium) — the realistic telephony path if mainline
> SLPI/CHRE proves intractable.
>
> ### SAR consumer — EXACT mechanism (from slpi.mbn strings) + the build blocker
> The SLPI firmware reveals precisely what the consumer must be:
> - Reads registry from **`/persist/sensors/registry/registry`** (+ `sns_reg_config`, `sns_reg_version`).
> - **`Sending registry request for group name:%s` / `SNS_REG_INIT` / `Decoding registry group %s` /
> `Error decoding registry event`** → the SLPI **requests registry groups over the SNS QMI service
> and expects encoded registry-event responses**. The SAR (sx932x) nanoapp fatals (`sar.cc:27
> chre_utils fatal`) when it can't get its group → crash-loop.
> - Fallback strings exist: **`Registry disabled, leaving default mode enabled` / `LPI Registry
> Disabled`** — there may be a registry-disabled mode worth forcing (SAR likely still needs its
> device config though).
> - **The consumer = an SNS registry provider** (QRTR/QMI daemon) that reads
> `/persist/sensors/registry/registry/<group>` and answers the SLPI's requests. On Android this is
> `sensors.qti` + `sscrpcd` + `chre_daemon_msm` (binary-only in
> `android-reference/vendor-stack-20260621/vendor/bin/`).
> - **BUILD BLOCKER (why it's not done):** the SNS protocol is Qualcomm proprietary protobuf
> (`sns_client.proto` / `sns_registry`); there are **NO .proto defs or SNS source anywhere in the
> tree** — only the registry DATA and the binary daemons. A working provider needs that encoding;
> building blind yields non-functional code. Options: (a) extract/RE the SNS protobuf from the
> vendor .so (`libsnsapi`/`sensors.qti`) — multi-day; (b) find an open SNS impl to port;
> (c) run the vendor `sscrpcd`/`sensors.qti` binaries under a libhybris/Halium shim;
> (d) downstream-kernel hybrid (sidesteps mainline SLPI entirely).
>
> ### CLEAN decisive test to run next boot (settles whether SAR even gates the modem — do BEFORE any SNS build)
> The session test was contaminated (modem already poisoned by prior online attempts). Clean version:
> `systemctl mask ModemManager qcril-prov qcril-autoselect blueline-dpm-open-port` → reboot →
> on boot immediately `echo stop > /sys/class/remoteproc/remoteproc2/state` (kill SLPI before it
> crash-loops) → wait for modem QMI up → ONE `qmicli -p -d qrtr://0 --dms-set-operating-mode=online`.
> - Still **52** with SLPI cleanly stopped from boot → **SLPI/SAR is NOT the modem gate** (the 52 /
> model-0 is independent — chase modem NV/EFS/firmware-init instead; don't invest in the SNS build).
> - **Goes online** → SLPI/SAR churn WAS the blocker → the SNS registry provider is worth the build.
> Unmask afterwards: `systemctl unmask ModemManager qcril-prov qcril-autoselect blueline-dpm-open-port`.
>
> ### Files touched this session (for the next agent)
> - `references/linux-7.1.1-sdm845-CANONICAL` @ `980c4ae1f` (revert skip); `tools/verify-fixes.sh` (skip-absent check)
> - pmaports `device/testing/device-google-blueline/`: `qcril-prov.py`, `modem-qrtr-ready.sh`+`.service`,
> `blueline-uim-select.sh`, `uim-wait.conf`, `blueline-dpm-open-port.service`, `qcril-prov.service`,
> `APKBUILD`, `80-device-google-blueline.preset`. All also deployed live to the phone.
> - NOTE: APKBUILD `sha512sums` NOT yet regenerated for the new/changed sources — run
> `pmbootstrap checksum device-google-blueline` before the next package build.
---
THE session that reframed the whole problem. The modem-online blocker is **not** (only) a
kernel/secure-load issue — it is that the **userspace consumer chain that drives the modem is
half-built / failing**, exactly as Casey kept saying. Everything below is VERIFIED LIVE on the
reverted-baseline 7.1.1 build, not inference.
## TL;DR causal chain (verified end to end)
1. Modem firmware boots, MBA loads MPSS, modem reaches `running`. It has identity (IMEI
990012001581294, ESN 8074BB5D, MEID, SW `g845-00194-210812`). **The assign/-22/secure-load is
NOT the modem blocker** — the modem loads. (The only `-22`s are fastrpc + ath10k wifi, both
tolerated.) One XPU violation at boot, self-recovers (Casey was right).
2. BUT the modem comes up **op-mode OFFLINE (3), model `0`** = up-but-unprovisioned/not-ready.
`--dms-set-operating-mode=online`**QMI 52 DeviceNotReady**. Forcing online does nothing.
3. **SLPI crash-loops every ~10s**: `err_qdi.c:456:EF:sensor_process:1:sar.cc:27:chre_utils fatal`
→ recover → up → crash. SAR (Semtech SX932x) is the modem's RF SAR/TX-power sensor.
4. The modem+SLPI instability makes the **QRTR node flap**, so the boot-time consumer services
race an unready modem and **fail**:
- `msm-modem-uim-selection.service``node with id 0 not found in QRTR bus` (boot) /
`Transaction timed out` (later). **This is a hard `Requires=` of ModemManager.**
- `blueline-dpm-open-port.service``node with id 0 not found` at boot (succeeds if re-run later).
- `qcril-prov.service` → crashed on a code bug (now FIXED, see below).
5. Because `msm-modem-uim-selection` (hard dep) failed → **ModemManager never started at boot.**
So nothing ever drove the modem: enable → set-power → register. It just sits OFFLINE.
6. Started MM manually (bypassing deps): it DOES reach the modem over qrtr://0, creates `modem0`,
reads the SIM ATR — then fails: **`couldn't start parent initialization: Couldn't get primary
port`** and `couldn't load carrier config: Operation timed out`. MM can't initialize a modem
that's OFFLINE/flapping.
## What is SOLID now (do not redo)
- On reverted-baseline 7.1.1 (`blueline` branch, mem-protect skip reverted `980c4ae1f`). Built,
flashed `boot_a`, deployed. `verify-fixes.sh` green (13 checks; mem-protect check now asserts the
skip is ABSENT). Android slot B / EFS / userdata untouched.
- ~~pd-mapper: in-kernel `CONFIG_QCOM_PD_MAPPER=m`, module loaded … **pd-mapper is fine — ruled
out.**~~ **WRONG (corrected 2026-06-29).** The pmaports config sets `=m`, but the running kernel
has `# CONFIG_QCOM_PD_MAPPER is not set` — the in-tree `arch/arm64/configs/sdm845.config` carries
the upstream `=n` and `merge_config.sh -m` applies it *after* the pmaports config, so `=n` wins
every build. Userspace pd-mapper is also uninstalled ⇒ **no pd-mapper of either kind runs.** Caught
via the wifi outage (no `wlan0`; ath10k_snoc bound but probe defers with no wlan PD). The Mobian
0171 observation stands, but "pd-mapper is fine" did not. Fixed: in-tree fragment → `=m` (commit
pending), then rebuild+reflash. Whether this also unblocks the modem-52 is now an open re-test, not
a closed lead.
- Infra present & correct: `rmtfs -P -s` (block EFS), `tqftpserv` (serves `/persist/rfs`
modem mcfg), sensor registry 177 files at `/mnt/vendor/persist/sensors/registry/registry/`,
all mcfg carrier configs incl `mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn`.
- udev tags MM correctly: `ID_MM_QCOM_SOC=1`, `ID_MM_CANDIDATE=1` on rmnet_ipa0; MM qcom-soc
plugin loads. Transport (QMI-over-QRTR) works — MM exchanges QMI with the modem.
## FIXED this session — qcril-prov.py (the MBN provisioner)
Source: `pmaports/device/testing/device-google-blueline/qcril-prov.py` (also live-deployed to
`/usr/bin/qcril-prov.py` on phone). Was crashing; now runs end-to-end and provisions Rogers.
Bugs fixed (all real, in code):
1. `list_configs` read configs from the response (`out.get_configs()` — doesn't exist;
`MessagePdcListConfigsOutput` only has `get_result`). The config list arrives via the
**`list-configs` INDICATION** → `IndicationPdcListConfigsOutput.get_configs()`; each element has
`.id` + `.config_type` only. Reworked to indication-driven (mirrors the working `load-config`).
2. GI array-length args: `set_id(cfg_id, len)``set_id(cfg_id)`; `set_type_with_id_v2(t,id,len)`
`set_type_with_id_v2(t,id)`. (`set_config_chunk` was already correct.)
3. Strategy: blueline INHERITS Android's full mcfg set (1 platform + 25 software) on shared
persist. Don't delete+reload (delete is a no-op; reload → error 41 "already exists"). Instead:
compute target id = **sha1 of the .mbn file** (== the PDC config id, verified: HW sha1
`6fe571a9…` matched the on-modem id), confirm present, **select + activate by sha1**. Treat
load error_code=41 as success. Helpers added: `_sha1_id`, `_hex`, `_id_present`.
Result: provisioner selects+activates HW (platform) and Rogers (software) configs cleanly. The
modem still ends OFFLINE afterward (carrier config alone doesn't lift it — needs MM to drive it).
## THE PLAN (next session — ordered)
Goal: get ModemManager to fully initialize the modem and drive OFFLINE → online → registered.
Two intertwined blockers: (A) modem won't initialize while OFFLINE/flapping; (B) SLPI crash-loop.
1. **Make MM actually manage the modem (primary blocker).**
- Why `Couldn't get primary port`? Check MM qcom-soc plugin port expectations for a pure-QRTR
SDM845 modem; check nothing else holds the QMI client (our qcril-* one-shots, the stray
`dbus-monitor`, manual `qmicli -p`). Try MM with everything else stopped.
- Fix the boot ORDERING/retry so `msm-modem-uim-selection` + `blueline-dpm-open-port` +
`qcril-prov` don't race the unready modem: add wait-for-QRTR-node / Restart=on-failure /
proper `After=` on a "modem-qrtr-up" condition. This is how the chain stays up like Lineage
(Lineage's RIL waits for the modem to be ready before UIM select / PDC).
- Once MM inits: it should set-power-up and the OFFLINE→online may resolve via MM's sequence
(it owns DMS set-operating-mode). The manual `online`=52 is because nothing did the proper
enable sequence first.
2. **SLPI SAR crash-loop (`sar.cc:27`).** SAR = SX932x, governs RF TX-power; Casey's position is
it gates modem RF. The SLPI sensor REGISTRY (`/persist/sensors/registry`) is NOT served to the
SLPI — tqftpserv serves `/persist/rfs` only (sibling tree). Leads:
- Does the SLPI read registry via tqftpserv (needs `/sensors` served too) or a different path?
The tqftp log shows only modem mcfg requests, no `/sensors` requests → SAR may crash BEFORE
requesting registry (early init / SX932x i2c on the SLPI bus). Investigate how LineageOS
`sensors_ssc.c` + sensor HAL bring SAR up (registry delivery + SX932x i2c/GPIO).
- Our `qcom_ssc_sensors.c` is a passive ABI shim; it does NOT boot or feed the SLPI. Mainline
auto-boots the SLPI (DTS firmware-name) with no consumer → SAR talks to nobody → crash.
LineageOS boots SLPI ON DEMAND after the sensor HAL is up.
3. **Verify on a clean boot**: fix the services so the whole chain comes up at boot (not manually),
reboot, confirm MM auto-starts, modem → registered. One online attempt per boot still applies.
## Hard constraints (unchanged)
- pmOS = slot A; flash ONLY boot_a. NEVER touch Android slot B / EFS (modemst1/2,fsg,fsc) /
userdata / TZ-XBL-ABL. rmtfs `-P -s`. No pmic@4/5 SPMI nodes. One `online` attempt per boot.
sudo pw 147147. USB 172.16.42.1 (host iface enp0s20f0u6 → `ip addr add 172.16.42.10/24`).
- pmOS is Alpine/ash: embedded `sh -c "...(...)..."` with parens fails — use script files via scp.

View file

@ -0,0 +1,153 @@
# Modem DT/RF gap analysis — pmOS vs Android-downstream vs enchilada (2026-06-22)
Phase 1 of the "is the RF/DT lever still alive?" task. All comparisons are between three device
trees on the SAME SoC (SDM845):
- **blueline-pmos** (FAILS RF arm) — `dt-work/blueline-pmos.dts`, sourced from upstream
`sdm845-google-blueline.dts` + `sdm845-google-common.dtsi`.
- **blueline-Android downstream** (modem WORKS, Fido LTE) — `dt-work/android-dtb/android-base.dts`
(base DTB) + `dt-work/blueline-live.dts` (live capture from /proc/device-tree, includes DTBO).
- **enchilada (OnePlus 6) mainline** (modem WORKS on the exact same mainline kernel/pmaports stack)
`dt-work/sdm845-oneplus-enchilada.dts`.
Two working references on the same silicon is the strongest possible control. Where BOTH working
trees agree and pmOS differs, that difference is a real, citable suspect.
---
## VERDICT: DT IS NOT THE GATE
The modem DT is **functionally identical** between working Android and non-working pmOS.
Every modem-relevant node (remoteproc@4080000, reserved-memory, rmtfs, power-domains, clocks,
glink, SPMI/PMIC) is structurally the same. The remaining differentiator is **runtime kernel
driver behavior**, not the device tree.
---
## RANKED CANDIDATES
### #1 (LIVE — Android applies the 32MB offset map; pmOS must carry it too) — Modem reserved-memory map
The subagent's top candidate claimed pmOS uses a non-standard MPSS size (0x9800000) and MBA
address (0x98500000) while Android uses the standard map (0x7800000 / 0x96500000). **This is wrong.**
The subagent compared pmOS against `android-base.dts` (the raw base DTB) and `enchilada.dts`
(standard map). But it failed to account for the **DTBO overlay** (dtbo_idx=8) that the bootloader
applies at boot. The DTBO transforms the base standard map into the Pixel 3 override on the live
Android device. The live captured DTS (`blueline-live.dts`, from /proc/device-tree) proves this:
| Region | Android base DTB | Android LIVE (with DTBO) | pmOS | enchilada |
|---|---|---|---|---|
| MPSS | 0x8e000000 sz **0x7800000** | 0x8e000000 sz **0x9800000** | 0x8e000000 sz **0x9800000** | 0x8e000000 sz **0x7800000** |
| MBA | **0x96500000** | **0x98500000** | **0x98500000** | **0x96500000** |
| venus | 0x95800000 | 0x97800000 | 0x97800000 | 0x95800000 |
| cdsp | 0x95d00000 | 0x97d00000 | 0x97d00000 | 0x95d00000 |
**Live Android and pmOS use the IDENTICAL Pixel 3 override map.** The modem works on Android with
this map. Therefore the memory map cannot be the gate. (The override comes from
`sdm845-google-common.dtsi:17-77` which `/delete-node/`s the standard regions and redefines them.
pmOS includes this via the mainline DTS chain; Android gets it via the DTBO overlay.)
Citations:
- Base DTB: `android-dtb/android-base.dts:19146` (reg 0x7800000), `:19167` (mba@96500000)
- Live Android: `blueline-live.dts:931` (reg 0x9800000), `:949` (mba@98500000)
- pmOS: `blueline-pmos.dts:906` (reg 0x9800000), `:924` (mba@98500000)
- enchilada: `sdm845-oneplus-enchilada.dts:884` (reg 0x7800000), `:902` (mba@96500000)
- DTBO source: `references/sdm845-linux-20260610-ONHOLD-DAVID-TREE/arch/arm64/boot/dts/qcom/sdm845-google-common.dtsi:17-77`
### #2 (REFUTED twice) — pm8998 LDOs absent from pmOS rpmh-regulators
`dt-work/regulator-diff-android-vs-pmos.txt`: Android defines ldo4/6/8/9/10/11/15/16/18/23/27 that
pmOS omits. **Already refuted twice** (modem.md #9/#10): (a) enchilada's WORKING modem does NOT
declare these RF LDOs either — same minimal mainline rpmh-regulators set as pmOS; (b) all 11 show
`use=0` from the AP on live working Android (`rf-rail-diff-FINAL.txt`) — the modem votes its RF rails
via RPMh directly, not the AP regulator framework. Since enchilada arms RF without them, they are not
the gate. **Do NOT add the LDOs / do NOT flash `blueline-live.dtb`.**
### #3 (eliminated) — RFFE / SPMI bus / RF-frontend regulator nodes
No RFFE node and no WTR/QFE/PA/FEM/GRFC regulator node exists in ANY of the three trees, including
both working ones. RF front-end power/control is modem-firmware-driven over RFFE/GRFC, not an AP DT
resource. Dead.
### #4 (eliminated) — modem node clocks / power-domains / smem-states / glink
`remoteproc@4080000` is structurally identical across all three trees: same
`qcom,sdm845-mss-pil`, same clocks, same power-domains, same glink-edge. Only firmware-name path
and phandles differ. rmtfs carveout structurally identical. Dead.
### #5 (eliminated) — DTBO overlay modem content
The DTBO (dtbo_a.img.dtb8, 112 fixups) touches display, audio, camera, charger, thermal, GPIO,
touchscreen, NFC, UFS, PCIe, debug/ramoops. **Zero modem/remoteproc/rmtfs/glink fragments.**
`modemsmem` node is a no-op placeholder (compatible only, no properties). The DTBO's only
modem-relevant effect is transforming the memory map from standard to Pixel 3 override (fragment@45
for MPSS, fragment@48 for MBA) — which pmOS already has natively. Dead.
### #6 (eliminated) — CDT / bootargs / hardware identifiers
Android has a `chosen/cdt/cdb2` node (IMEI, MEID, BT/WLAN MACs, SKU, PCB config, eSIM) and
`androidboot.*` bootargs that pmOS lacks. These are AP-side identifiers — the modem gets IMEI from
EFS, not DT. Mainline kernel parses none of the `androidboot.*` params. Dead.
---
### #2 (WEAK / for-the-record) — pm8998 LDOs absent from pmOS rpmh-regulators
`dt-work/regulator-diff-android-vs-pmos.txt`: Android defines ldo4/6/8/9/10/11/15/16/18/23/27 that
pmOS omits. **Already refuted twice** (modem.md #9/#10): (a) enchilada's WORKING modem does NOT
declare these RF LDOs either — same minimal mainline rpmh-regulators set as pmOS; (b) all 11 show
`use=0` from the AP on live working Android (`rf-rail-diff-FINAL.txt`) — the modem votes its RF rails
via RPMh directly, not the AP regulator framework. Since enchilada arms RF without them, they are not
the gate. Kept here only so it is not re-chased. **Do NOT add the LDOs / do NOT flash
`blueline-live.dtb`.**
### #3 (eliminated) — RFFE / SPMI bus / RF-frontend regulator nodes
No RFFE node and no WTR/QFE/PA/FEM/GRFC regulator node exists in ANY of the three trees, including
both working ones (`rf-rail-diff-FINAL.txt`, re-verified today: `qcom,rffe` absent everywhere; SPMI
bus `spmi@c440000` `qcom,spmi-pmic-arb` present and identical in pmOS + enchilada + Android). RF
front-end power/control is modem-firmware-driven over RFFE/GRFC, not an AP DT resource. Nothing to
add. Dead.
### #4 (eliminated) — modem node clocks / power-domains / smem-states / glink
`remoteproc@4080000` is structurally identical between blueline-pmos and enchilada: same
`qcom,sdm845-mss-pil`, same `clock-names` (iface/bus/mem/gpll0_mss/snoc_axi/mnoc_axi/prng/xo), same
`power-domains` (cx/mx/mss), same `qcom,smem-states "stop"`, same glink-edge label "modem"
remote-pid 1 (`dt-work/modem-blueline.txt` vs enchilada `:3647`). Only firmware-name path and
phandles differ (correct). rmtfs carveout structurally identical (size `0x202000`, vmid 0x0f,
client-id 1, guard-pages); base differs (`0xf2700000` vs enchilada `0xf5b00000`) which is normal
per-device placement and already proven to serve EFS fine on pmOS
(`modem_community_research_20260622.md` live result #1). Dead.
---
## Runtime time-injection angle (secondary lead from the task)
Checked: pmOS does NOT run a modem time daemon. The qrtr "Time" service (TS, svc 64) sync that
Android's RIL feeds is not provided by ModemManager. However this is downstream of the 52 wall —
the modem refuses the `dms-set-operating-mode=online` *write itself* instantly
(`modem_community_research_20260622.md` result #4: clean Alloc CID -> Set Op Mode -> FAILURE 52,
immediate), before any time-dependent NAS attach logic runs. Time injection gates *attach*, not the
op-mode flip. Only worth chasing if the 52 gate is cleared and the modem then fails to attach.
---
## VERDICT (Phase 1) — CORRECTED
**The DT is not the gate.** Every modem-relevant node is byte-identical between working Android and
non-working pmOS. The subagent's #1 candidate (memory map difference) was refuted by accounting for
the DTBO overlay: live Android and pmOS use the identical Pixel 3 override map.
The remaining differentiator is **runtime**: the mainline kernel's remoteproc/PIL driver, firmware
load sequence, power-domain sequencing, QMP link bring-up, or glink/mailbox initialization differs
from the downstream Android kernel. The modem firmware is identical (same files, same EFS, same
shared partition). The DT is identical. The kernel and its drivers are what's different.
### Next steps (kernel/driver level, not DT)
1. **Compare remoteproc driver behavior**: dmesg on Android vs pmOS during modem boot — look for
differences in power-domain enable order, QMP link state, firmware load timing.
2. **Check if mainline qcom_q6v5_pas driver handles the MSS PIL differently** from the downstream
driver — e.g., missing a handover step, wrong power-domain sequence, or not waiting for a
firmware-ready signal.
3. **diag_mdlog F3 trace on Android**: capture the modem's own RF-init narration to see exactly
where it succeeds on Android — this is the one instrument that can name the gate directly.

View file

@ -0,0 +1,135 @@
# PAF · Platform — hardware, boot, storage, recovery
Load-bearing device facts. Add a line when something costs more than 2 minutes to relearn.
## Reaching the phone
- WiFi (preferred): `ssh -i ~/.ssh/ani_id_ed25519 casey@10.10.20.220` (Samaritan Solutions).
**Drops on reboot / ~6090 s to rejoin** — poll, don't assume.
- USB fallback: `casey@172.16.42.1`. Helper: `../phone.sh {ssh,vnc,push,pull,run}`.
- sudo password `147147``echo 147147 | sudo -S <cmd>`. **Never** pipe file content into a
`sudo -S tee` (password and content fight over stdin). Write `/tmp/x` as user, then
`echo PW | sudo -S cp /tmp/x /dest`.
- pmOS is **Alpine — shell is ash/busybox, not bash.** No `[[ ]]`, no arrays, no `function`.
- Host (Arch) ↔ Android: `adb` works with root (`magisk`); `fastboot` for slot/boot ops.
## Slots & boot architecture
- **slot A = pmOS, slot B = LineageOS (Android).** Verified 2026-06-25 on-device: pmOS
ran with `androidboot.slot_suffix=_a`; `fastboot getvar current-slot` = a; flipping
active to b booted Android; Android (slot B) had `system_b`/sda6 mounted. So pmOS =
`boot_a` (sda11) + `system_a` (sda5); Android = `boot_b` (sda12) + `system_b` (sda6).
Older PAF entries saying A=Android/B=pmOS or rootfs in system_b are pre-swap and stale.
Switch active slot: from pmOS `sudo reboot bootloader` (~12 s → fastboot), then
`fastboot --set-active={a,b} && fastboot reboot`.
- pmOS rootfs lives **inside** `system_a` (sda5) as a packed **GPT image with 4096-byte
sectors** (UFS). Two inner partitions:
- boot (vfat, ~236 MB) at **byte offset 8388608** in sda6 — holds the kernel, initramfs,
`linux.efi`, and DTBs. The appended DTB in the boot_b partition image IS used by ABL
(it is baked into the kernel image in the boot.img). The `dtbs/qcom/` copies in the vfat
are for a U-Boot/EFI path that is NOT used on this device currently.
- root (ext4, ~2.5 GB) at **byte offset 255852544**.
UUID: `26733d78-aa54-454d-ba43-0fa1eabfb3fd` (pmOS_root label)
- boot vfat UUID: `2CE8-836F` (pmOS_boot label)
- `fastboot flash system` is blocked (needs fastbootd, which won't enumerate on this device).
Write the rootfs via `dd` over ADB from Android, after `simg2img`.
- 4K sectors: `sgdisk` needs `-a 256` for 1 MiB alignment; `gdisk` reports "GPT not present"
if it assumes 512-byte sectors — the image is valid.
- **`fastboot boot <img>` has a hardware watchdog timeout (~60 s)** — the phone reboots itself
if the image doesn't complete boot. Always `fastboot flash boot_b <img>` instead.
- **/tmp is tmpfs (volatile, wiped on reboot) — never park load-bearing artifacts there.**
## Boot image parameters (slot A — pmOS)
The canonical pmOS boot image for slot A was `/tmp/pmos-boot-b.img` (29245440 bytes) — **wiped on 2026-06-21 reboot (/tmp is tmpfs).** The rescued recovery boot is now at `artifacts/tmp-rescue-20260626/recovery-boots/boot_a_restore.img` (65M). The filename predated the slot swap; always flash to `boot_a`, not `boot_b`.
Rebuild targets `/tmp/pmos-debug2.img` (debug shell) and `/tmp/pmos-nodebug.img` (normal boot)
must be regenerated. To rebuild from rescued kernel+ramdisk in `artifacts/tmp-rescue-20260626/bootimg-workspace/`:
```sh
# Normal boot (no debug shell):
mkbootimg --kernel artifacts/tmp-rescue-20260626/bootimg-workspace/kernel --ramdisk artifacts/tmp-rescue-20260626/bootimg-workspace/ramdisk \
--base 0x0 --kernel_offset 0x00008000 --ramdisk_offset 0x01000000 \
--tags_offset 0x00000100 --pagesize 4096 --header_version 0 \
--cmdline "console=tty0 console=ttyMSM0,115200 consoleblank=0 \
pmos_boot_uuid=2CE8-836F pmos_root_uuid=26733d78-aa54-454d-ba43-0fa1eabfb3fd \
pmos_rootfsopts=defaults" \
-o /tmp/pmos-nodebug.img
# Debug shell (drops to telnet:23 before mounting rootfs, adds pmos.debug-shell):
# ... same but append: pmos.debug-shell
```
Flash to slot A (pmOS) and reboot:
```sh
fastboot flash boot_a /tmp/pmos-nodebug.img && fastboot set_active a && fastboot reboot
```
**Emergency debug shell** (when pmOS won't boot — flash debug image to boot_a):
```sh
fastboot flash boot_a /tmp/pmos-debug2.img && fastboot set_active a && fastboot reboot
# USB gadget comes up as 18d1:d001 at 172.16.42.1 (~8 s after reboot)
# telnet 172.16.42.1 23 → shell with pmos_continue_boot / pmos_logdump
# After diagnosing, re-flash the normal image and reboot.
```
**USB host setup** (permanent, already configured 06-19):
- `/etc/NetworkManager/conf.d/99-pmos-usb.conf` — NM ignores `cdc_ncm`/`rndis_host`/`cdc_ether`
- `/etc/udev/rules.d/99-pmos-usb.rules` — auto-assigns `172.16.42.2/24` on interface up
**Disk full recovery** (happened 06-19 — logs filled rootfs to 100%):
Boot debug image → telnet in → mount_subpartitions → mount /dev/loop0p2 /sysroot →
`rm -rf /sysroot/var/log/*` → pmos_continue_boot. Frees ~94 MB.
The rootfs will slowly refill — **set up log rotation on pmOS** (`/etc/logrotate.d/` or
`journalctl --vacuum-size=10M`) once booted.
## Recovery: swap the slot-B DTB from rooted Android (no TWRP)
Used 06-19 to un-brick slot B after the SPMI-pmic DTB panic. Reversible; backs up the old DTB.
```sh
adb push <good>.dtb /data/local/tmp/recovery.dtb
adb shell 'su -c "
for l in /dev/block/loop*; do losetup -d \$l 2>/dev/null; done
LO=\$(losetup -f); losetup -o 8388608 \$LO /dev/block/by-name/system_b
mkdir -p /data/local/tmp/bmnt; mount -t vfat \$LO /data/local/tmp/bmnt
T=/data/local/tmp/bmnt/dtbs/qcom/sdm845-google-blueline.dtb
cp \$T \$T.broken.bak
cp /data/local/tmp/recovery.dtb \$T
sync; md5sum \$T /data/local/tmp/recovery.dtb
umount /data/local/tmp/bmnt"' # loop auto-detaches on umount
```
Building a DTB: base = the pristine kernel-package DTB (decompile
`linux-postmarketos-qcom-sdm845-*.apk`, or `/tmp/pmos-packages/blueline.dts`). Edit, then
`dtc -I dts -O dtb -o out.dtb in.dts`. The pristine DTB is 111641 bytes; verify
`grep -c 'pmic@4\|pmic@5'` is **0** (those panic).
## Storage situation
- Root (ext4 inside sda6) is **2.4 GB, ~80 MB free** (94 MB logs cleared 06-19), 828 packages.
Nothing significant installs until userdata is resized. `apk cache clean` buys ~300 MB
temporarily. **Logs will refill** — set up log rotation once booted.
- **userdata (sda21, 111 GB) is FBE + metadata-encrypted** (dm-default-key, AES-256-XTS; key
KeyMint-wrapped in TEE, stored in `/metadata` sda20). **Cannot shrink offline** — no plaintext
F2FS superblock outside Android; key can't be re-derived in TWRP. Shrinking = full wipe.
Decision (06-18): do **not** wipe until calls + Linux desktop both justify it.
- GPT backup of the outer table: `../gpt-sda-20260618.bak`. Nothing on sda was modified.
## Hardware lessons (do not relearn)
1. **Never `misc:vfr = 0`, never DPMS off/on** in Hyprland — wedges the DSI panel + stmfts
touch IC. Recovery: `systemctl restart getty@tty1` for a clean modeset. Use `vfr = true` +
`cursor:no_hardware_cursors = true`. Skip earboxer's power-button DPMS binds.
2. **hyprgrass must match the Hyprland version exactly** (build against the `hl-x.y.z` tag).
3. Display already correct: `monitor = DSI-1,1080x2160@60,0x0,2.0` (scale 2.0 → 540×1080
logical). ~443 PPI panel.
4. Restarting rmtfs blips ath10k → WiFi can drop; prefer USB during modem work.
5. USB-net flaps because NetworkManager grabs `enp0s20f0u6`. Fix with **pkexec** (not sudo):
`nmcli dev set <if> managed no; ip addr add 172.16.42.2/24 dev <if>; ip link set <if> up`.
## Backups on the laptop
`~/pixel3-backup/` — full Android partition images (gzipped): `boot_a` (downstream DTB +
kernel), `modem_a` (FAT16 with `mba.mbn` + `modem.b00..b25`), `persist` (cal: audio/sensors/
rfs/DRM), `system_a`, `vendor_a`, `vbmeta_a`, `dtbo_a`, `userdata` (87 GB). EFS snapshot:
`../efs-backup-20260618/` (modemst1/2, fsg, fsc).

View file

@ -0,0 +1,148 @@
# pmOS Modem Fix — Implementation Plan (autoselect, EFS-persisted)
Consolidates the 2026-06-20 live-Android + binary-RE session. This is the build plan for the
pmOS side. Evidence is cited to live capture lines or binary addresses; nothing here is inferred
without a source.
## The mechanism (works — but is NOT the gate)
> **🟡 RE-STAMPED 2026-06-29.** The MCFG auto-select-by-UIM mechanism is real and PDC configs do go
> Active (`qcril-autoselect` runs, Set Feature accepted) — but this is **proven NOT to be the gate**:
> the modem parks OFFLINE / model 0 and refuses every pre-RF write (online→52, low_power→60,
> NAS SSP→25) regardless of provisioning state. Choreography is downstream of the OFFLINE state.
> See `modem.md` eliminated-leads ledger. Kept as the mechanism reference (how MCFG/EFS autoselect
> works), not as a live fix path.
The modem provisions itself by **MCFG auto-selection keyed on the SIM (UIM)**, applied at the
modem's own firmware RF-init, and **persisted in the modem's EFS** (`modemst1/2`, which is
**shared** between the Android and pmOS slots — `sdf3/sdf4`, not slotted).
1. **Persistent form (community-proven on SDM845 / OnePlus 6/6T):** an EFS NV item
`/nv/item_files/mcfg/mcfg_autoselect_by_uim` plus the mcfg uploaded into EFS. Written over
DIAG (EfsTools). Once set, the modem self-selects + self-applies every boot.
2. **Runtime form (reverse-engineered from `libril-qc-hal-qmi.so`):**
`pdc_enable_auto_selection(enable, id)` @ `0x1d45c40` builds an 80-byte
`pdc_set_feature_req_msg_v01` (feature value `3` when enabling, `0` when disabling) and calls
`pdc_set_feature` @ `0x1d45ac0`, which issues **QMI_PDC_SET_FEATURE**:
- **message ID `0x2E` (46)** (`mov w1,#0x2e` at `0x1d45b4c`)
- request length `0x50` = 80 bytes (`mov w3,#0x50`), response 8 bytes (`mov w5,#0x8`),
timeout 10000 ms (`mov w6,#0x2710`).
## Live-Android ground truth (this session, device 89JX0AAJP)
- Modem fully working: `dumpsys telephony` → IN_SERVICE, LTE, Fido 302/720, voice+data.
- **Self-persist confirmed:** blanked `persist.vendor.radio.cnv.ver_info`, restarted live
`qcrild`. `is_modem_mbn_updated` logged `version info updated` (prev empty), `start_mbn_update`
fired — but QCRIL only queried `get_active_config_info` (`config_version=0x801481a`, CNV
`0x48`=72), re-synced, and **never loaded/activated**. The modem already held the config in its
own NV. → userspace is reconcile-only; the modem persists. (`/tmp/qcril-re/live-firstprov.txt`)
- **Online-arm reference captured** (the step that returns `DeviceNotReady` on pmOS):
airplane toggle → `dms_handle_modem_operating_mode → "operating mode needs to be changed" →
SETTING_OP_MODE → modem ONLINE`. (`/tmp/qcril-re/airplane-arm.txt`)
- `/data/vendor/modem_config/` does **not exist** on this device — QCRIL reads MBNs straight from
`/vendor/rfs/msm/mpss/readonly/vendor/mbn/` (the readonly vendor tree), not a `/data` cache.
## Why pmOS fails, and what the fix must do
pmOS boots with the modem at 0/0 configs and `DeviceNotReady`. The Android-equivalent state is:
modem set to auto-select by UIM + the mcfg tree reachable at RF-init + EFS writable so the modem
persists what it applied. The fix must guarantee all three on pmOS:
1. **mcfg tree served to the modem at RF-init**`tqftpserv` serving
`/mnt/vendor/persist/rfs/readonly/vendor/mbn/` (present, per modem.md). Verify the modem's
requested path matches.
2. **EFS writable**`rmtfs -P -s` (read-write, drop `-r`) so the modem can commit/persist its
applied config (frontier #4; the read-only `-r` shadow is why nothing persisted before).
3. **Auto-selection enabled** — set it so the modem self-applies. Two routes below.
## Build — two routes (do A first; B is the persistent backstop)
### Route A — runtime QMI via PDC Set Feature (ALREADY AVAILABLE in pmOS's libqmi fork)
**This is the path. It is not new work — the pmOS community already built the libqmi side.**
Stock libqmi PDC has no Set Feature. But the **openimsd libqmi fork**
(`https://gitlab.postmarketos.org/modem/openimsd/libqmi`, base 1.39.0) already defines
**`QMI_PDC_SET_FEATURE` at id `0x2E`** — the *exact* message ID we independently reverse-engineered
from `pdc_set_feature` (`0x1d45b4c: mov w1,#0x2e`). Two sources, same message. Its request TLVs
(`data/qmi-service-pdc.json`, "since 1.40"):
| TLV | id | type | meaning |
|---|---|---|---|
| Slot ID | common-ref | guint32 | slot |
| Token | common-ref | guint32 | request token |
| **Selection Mode** | 0x11 | guint32 | **set to 3 = auto-select-by-UIM** (matches the `enable→feature 3` in `pdc_enable_auto_selection`) |
| Refresh Mode | 0x14 | gboolean | refresh on change |
| IMS | 0x19 | gboolean | ims feature |
Output: Operation Result (+ a Set Feature *indication* carrying the Token).
Implementation on pmOS:
- Build/install the **openimsd libqmi fork** (it generates `Qmi.MessagePdcSetFeatureInput` with
`set_selection_mode` / `set_refresh_mode` / `set_ims` / `set_slot_id` / `set_token`).
- Call it from **python-GI** — the fork's **qmicli has no `--pdc-set-feature` CLI**, so do it the
way `qcom-imsd` does (Qmi GI over `qrtr://0`), not via a qmicli flag. Set Selection Mode = 3,
Refresh Mode on. This is the runtime equivalent of QCRIL's `pdc_enable_auto_selection`.
- pmOS boot service: enable auto-select, let the modem self-provision from the served mbn tree.
Note: the fork still carries the **load-config `g_free` segfault at `qmicli-pdc.c:1130`** (it's
1.39.0). Only relevant if we fall back to explicit `--pdc-load-config`; the autoselect path
shouldn't need it. Our one-line patch + `tools/build-qmicli.sh` still apply if so.
**Prior art / proof:** `qcom-imsd` (same fork, libqmi-GI over qrtr PDC) is **known-working on
Pixel 3a (sargo)** — blueline's SDM845 sister. It does IMS-layer PDC (select/activate/register);
we add the lower-layer Set Feature autoselect. The pmOS PDC-over-qrtr approach is proven on this
silicon.
### Route B — persistent EFS NV item (DIAG, the community method)
Write `/nv/item_files/mcfg/mcfg_autoselect_by_uim` (enable) into the modem EFS via a DIAG-EFS2
client on pmOS. pmOS diag is **not** HAL-locked the way the production-Android Pixel gadget is
(this session: the Pixel USB gadget HAL reverted configfs to adb-only, blocking USB-diag;
`/dev/diag` local access remained available). Because `modemst` is shared, this is a one-time
write that both slots inherit. Tooling: EfsTools (Linux/.NET — `dotnet` is available) or a small
DIAG-EFS2 client against pmOS `/dev/diag`.
## Test loop (on pmOS, next boot)
1. Confirm rmtfs `-P -s` (rw) + tqftpserv serving the mbn tree.
2. Apply Route A (or B) to enable auto-selection.
3. Reboot; check `qmicli --dms-get-capabilities``Networks` non-empty = RF armed.
4. If armed: confirm it survives a second reboot (persistence). If not, apply Route B so the
modem commits to EFS.
## LIVE RESULT 2026-06-20 (Route A run on pmOS hardware — config blocker SOLVED)
Ran the full fix on pmOS slot B (kernel 7.1.0-rc1-sdm845), live modem over qrtr://0:
1. Built + installed the **openimsd libqmi fork** on-device (Alpine); `Qmi.MessagePdcSetFeatureInput
present: True`.
2. Ran `qcril-autoselect.py` → PDC client allocated, **Set Feature (0x2E) accepted**. Signal names
`set-feature`/`get-feature` were correct (indications arrived; only a strict-parse warning:
the modem's Get Feature indication omits the mandatory Result TLV 0x02 libqmi expects — cosmetic).
3. **Immediately after Set Feature, the modem self-loaded its configs: PDC 0/0 → 2 platform
(SS-LA-SDM845 ACTIVE) + 25 software (Rogers_Canada).** It pulled them itself from the
tqftpserv-served mbn tree. This is the modem auto-selecting by UIM — the mechanism working.
4. **Rebooted with nothing enabled → the modem self-loaded 2+25 again at RF-init, platform ACTIVE.**
So the autoselect **persisted** (Set Feature is sticky, not just runtime). This is the documented
project blocker (0/0 every cold boot, frontier #3/#4) **SOLVED**: blueline's modem now
self-provisions MCFG persistently on mainline pmOS — believed a first for Pixel 3.
**Remaining wall (NEW, deeper than config):** despite firmware up (mba+mpss booted, IPA running,
handover signaled), platform config ACTIVE, and ModemManager seeing the configs,
`--dms-set-operating-mode=online` still returns **DeviceNotReady (QMI 52)**; modem stays offline,
Networks ''. `low-power` → InvalidTransition (already LPM). A full reboot with configs active did
NOT arm RF — so it is not a re-init-count problem.
**Leading hypothesis for the remaining wall → Route B (firmware-level autoselect via EFS NV).**
Our Set Feature operates at the **PDC/RIL layer** (post-firmware-boot): the modem loads+activates
the config in PDC, but the **firmware RF-init at ~12s already ran without it**, so PDC-ACTIVE ≠
RF-consumed. The Android/OnePlus mechanism writes the EFS NV item
`/nv/item_files/mcfg/mcfg_autoselect_by_uim` so the **firmware itself** auto-selects at its
earliest RF-init, before RF comes up. Next step: write that EFS NV item on pmOS (diag/EfsTools —
not HAL-locked here) so the firmware applies MCFG at RF-init. Alternative to rule out: a genuinely
separate non-config blocker (cal/IMEI) — but the same modem arms on Android, so config-apply timing
is the stronger lead.
## Artifacts from this session
`/tmp/qcril-re/`: `live-firstprov.txt` (qcrild restart, reconcile-only proof),
`airplane-arm.txt` (online-arm reference), `auto_sel.txt` + objdump of
`pdc_enable_auto_selection` / `pdc_set_feature` (msg 0x2E). EFS clones already in repo:
`efs-postandroid/`, `efs-backup-20260618/` (`modemst1/2`, `fsg`, `fsc`).

View file

@ -0,0 +1,158 @@
# HANDOFF — pmi8998 charge control (kernel + userspace)
> Historical handoff. Current platform status belongs in `../../platform.md`.
Written 2026-07-24. For a fresh model/reviewer picking this up cold.
## What this is
Full read/write charge control for the Pixel 3 (blueline) charger, so the
phone can report its charge stage and cap the charge for battery longevity —
the capability LineageOS exposed downstream and mainline dropped.
Two layers, in two separate repos:
- **Kernel driver**`drivers/power/supply/qcom_smbx.c` in the
`linux-blueline` tree. Adds charge_type reporting + charge-cap control.
- **Userspace**`rootfs-overlay/…` in the `Pixel3Arch` repo. A CLI, a udev
rule, a persistence unit. **Currently stale — see "Known gaps" #1.**
Nothing is flashed. The phone is untouched. The driver builds clean but has
not been packaged since the last fix, and has NOT run on hardware.
## State of the tree — READ THIS FIRST
**The gitea branch is behind local by two commits.**
| commit | what | where |
|---|---|---|
| `f08dc70b3` | original: charge_type + charge control | **pushed to gitea `blueline`** |
| `6b733d3ea` | fix pass after review #1 | **LOCAL ONLY** |
| `588929024` | cell-aware current cap + probe guard | **LOCAL ONLY** |
`gitea.wiuf.net`/`10.20…:4455` `Fimeg/linux-blueline` branch `blueline` tip is
`f08dc70b369a` — the **flawed original**. A reviewer who clones from gitea gets
code with the defects listed under "Review #1 found" still present. To review
the real thing, use the local tree at
`~/Projects/Pixel3Arch/kernel/linux-blueline-current` (branch `mic-race-fix`,
tip `588929024`) or push the two commits first.
`build.sh` clones from gitea and hard-asserts the built package's hash matches
the branch tip, so **a package build will not include the fixes until they are
pushed.**
## Files
### Kernel (linux-blueline-current, branch mic-race-fix)
- `drivers/power/supply/qcom_smbx.c` — the only changed file. +241 lines.
- `smb_get_prop_charge_type` — read-only, maps BATTERY_CHARGER_STATUS_1
bits 0:2 (trickle/pre/fast/fullon/taper/…) to POWER_SUPPLY_CHARGE_TYPE_*.
- `smb_{get,set}_prop_voltage_max` — CV target, FLOAT_VOLTAGE_CFG (0x70).
- `smb_{get,set}_prop_constant_charge_current` — FAST_CHARGE_CURRENT_CFG (0x61).
- `smb_{get,set}_prop_charge_control_end` — percentage form of voltage_max,
on POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD.
- `smb_fast_charge_current_max` — 1C ceiling from charge_full_design_uah.
- `smbx_ov_status` — one-char fix, `reg & mask``val & mask`.
- probe: design-limit guard + constant substitution.
### Userspace (Pixel3Arch repo, committed at 8a35c45 / 194c422)
- `rootfs-overlay/usr/bin/blueline-charge` — CLI. **STALE.**
- `rootfs-overlay/etc/udev/rules.d/60-blueline-charge.rules` — grants the
writable attrs to group wheel. **STALE.**
- `rootfs-overlay/etc/systemd/system/blueline-charge-limit.service`
reapplies the cap on boot (registers are volatile).
- `PAF/platform.md` — "Charging" section documents the register map.
## Property provenance (asked and answered)
Every property name is a **pre-existing mainline `power_supply` core constant**
(`include/linux/power_supply.h`), not invented here and not from any PR of
mine. `charge_control_end_threshold` has a documented ABI contract dated April
2019 (`Documentation/ABI/testing/sysfs-class-power`) — "battery percentage
level, above which charging will stop" — and is already implemented by seven
in-tree drivers, including the sibling `qcom_battmgr.c` on this same SoC family.
The one thing authored here is the charge_type register-state→enum mapping.
## Review history
Three independent cold-start adversarial reviews were run (subagents, each told
to refute safety). Summary of what each found:
**Review #1 (against f08dc70b3) — four real defects, all since fixed in 6b733d3ea:**
1. `smbx_ov_status` masked the register *address* not its *value*
(`reg & mask`), so the overvoltage backstop returned 0 on both SMB2 and
SMB5 and never fired — pre-existing, but the write path leaned on it.
2. `charge_control_limit` is microamps by ABI; it was implemented as a
percentage, inverting a thermal daemon's write. Moved to
`charge_control_end_threshold` (percent, ABI-correct).
3. `voltage_max` floored at the register base (3.4875V), not the cell design
minimum (3.6V). `echo 3487500 > voltage_max` set CV below terminal voltage;
with CHARGER_INHIBIT the charger stalls and the phone drains on a cable.
Now floored at voltage_min_design_uv.
4. Current setter clamped against the DCP *input* limit (a different register
and concept), so the getter returned 1.95A while the setter rejected it.
**Review #2 (against 6b733d3ea) — confirmed the four fixes; flagged the current
ceiling. Fixed in 588929024:**
- The current cap was bounded by register width = 6.375A = **2.15C** into a
2970mAh cell. Relying on "the adapter/AICL gate it" is the same
external-backstop reasoning that hid the dead OV check. Now bounded at **1C**
(2.97A from charge_full_design_uah), register width as fallback.
- Probe didn't validate voltage design limits (latent, not blueline). Guarded.
**Review #3 (against 588929024) — verdict FLASH for blueline.** No overcharge
(clamp-before-encode, encoder rounds down, 8-bit field can't truncate a legal
value unsafe), no stall (3.6V floor on every path, registers volatile →
reboot recovers), 1C cap confirmed, dispatch clean.
## Known gaps — WHAT STILL NEEDS DOING
1. **Userspace is stale.** `blueline-charge` and `60-blueline-charge.rules`
still reference `charge_control_limit`, which the driver no longer exposes.
On the fixed kernel the CLI fails its own `[ -e "$PSY/charge_control_limit" ]`
guard. They must be updated to `charge_control_end_threshold`. NOT done.
2. **Only `end_threshold`, not the `start`/`end` pair.** The ABI defines
`charge_control_start_threshold` and `_end_threshold` as a pair; some
userspace (GNOME/UPower, some tlp configs) expects both. Only `end` is
implemented. `drivers/power/supply/cros_charge-control.c` is the in-tree
reference for doing both.
3. **The SMB2 overvoltage bit is unverified.** No auditor could confirm from
code alone that BIT(5) of BATTERY_CHARGER_STATUS_2 is the pmi8998 OV bit —
there's no datasheet or downstream reference in-tree. It's pre-existing and
read-only (can't overcharge/stall), but worth confirming against a datasheet.
4. **Cosmetic:** the max voltage write (4.4V → reg 122) reads back as 4.395V
and 99% via the threshold getter, because the 7.5mV encoding step doesn't
divide the range evenly. Errs low (safe). Not fixed.
5. **Not packaged, not flashed.** Single-object compiles pass under clang/LLD/
CFI with W=1. Full `build.sh` not re-run since the fixes (and can't include
them until pushed — see "State of the tree"). No hardware test.
## What is NOT verified without hardware
- Whether lowering the float voltage actually *stops* the charge on the
pmi8998, versus the fuel gauge continuing to report rising capacity.
- Whether charge_type reports sane transitions (Fast under load → Standard as
it tapers near full).
## Build & deploy (per PAF/build.md)
- Commit + **push to gitea** (uncommitted/unpushed work does not build).
- `ssh casey@10.10.20.123 'cd ~/Projects/Pixel3Arch/kernel && ./build.sh blueline'`
— self-locks with flock, invoke directly; asserts pkg hash == gitea tip.
- Deploy: modules first, then flash boot_a, then refuse to bless unless
`uname -r` matches the installed tree (`7.1.1-sdm845-g<hash>`).
- Rollback: `staging/deploy-139aa/boot-139aa7748181-pmoschain.img`. Slot B
(LineageOS) is untouched and is the ultimate fallback.
## Single-object compile check (fast, no full build)
scp qcom_smbx.c casey@10.10.20.123:/tmp/
ssh casey@10.10.20.123 'cd ~/Projects/Pixel3Arch/kernel/src/linux-blueline-src && \
cp /tmp/qcom_smbx.c drivers/power/supply/ && \
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- W=1 \
drivers/power/supply/qcom_smbx.o'
# restore the tree afterward — this is makepkg's throwaway clone

View file

@ -0,0 +1,103 @@
# QCRIL modem bring-up — CAPTURED exact sequence (recreate this on pmOS)
This is the captured, ordered sequence QCRIL runs to bring the modem online on the **working**
blueline Android. The standing goal (Casey): **recreate this exactly on pmOS** — approximating has
been the failure. Do NOT re-capture this; analyze the saved files below.
## Capture files (in repo, `android-capture-20260620/`)
- `qcril-ssr-fullinit.txt`**the primary capture.** Modem SSR (`echo restart >
/sys/kernel/debug/msm_subsys/modem`) with verbose radio logging → QCRIL re-runs its FULL bring-up
→ modem ONLINE. 66k lines, ts 21:07:0421:07:35.
- `qcril-ssr-TIMELINE.txt` — distilled ordered timeline (section A: QMI sends per endpoint;
section B: per-module handling order).
- `qcril-online-capture.txt` — airplane off→on (the LPM→online flip in isolation).
- `android-radio-log.txt` — steady-state runtime reference (attached, Fido LTE).
## SIM identity (for replay)
ICCID `<redacted>`, IMSI `<redacted>`, MCC/MNC **302/370 (Fido)**, carrierId 1962,
GW app1 AID `A0000000871002F310FFFF89B00140FF`, slot 1.
## The exact ordered sequence (SSR → ONLINE), with evidence
1. **Modem SHUTTING_DOWN → re-init.** `qcril_qmi_nas_log_radio_power_process_state: telephony ONLINE
| ril NONE | card UP | modem SHUTTING_DOWN`.
2. **Endpoint-status indications fire for EVERY service as it re-registers** (qcril-ssr-fullinit
lines ~2521325): IMSS, IMSA, **PDC** (→ MbnModule), SMS, NAS, PBM, **DMS** (→ DmsModule AND
MbnModule), UIM (`uim.card_status_update slot 0`), VS, Voice, WDS, Auth. QCRIL drives each
service's endpoint, not just DMS.
3. **MbnModule re-syncs PDC — VERIFY ONLY, no load/select/activate.** `PDC_ENDPOINT_STATUS_IND` and
`DMS_ENDPOINT_STATUS_IND` both forwarded to MbnModule; no `load_config`/`select_config`/
`activate_config`/`enable_auto_selection` anywhere in the SSR window. MCFG persisted in modem NV
across the SSR (consistent with frontier #4). **→ On pmOS we must get MCFG in modem NV ONCE; it
then persists.**
4. **UIM provisioning is NOT re-run on SSR** (no `change_provisioning_session` in the capture) — it
persisted. **→ On pmOS, primary-GW provisioning must be established once and stick.**
5. **NAS config burst (the "QCRIL does more" set), then power.** QCRIL calls, in the bring-up window:
`qcril_qmi_nas_init`, `request_set_preferred_network_type` (tells the modem which RATs to
acquire), `request_radio_tech`, `set_reported_voice_radio_tech`, `request_set_unsol_response_filter`,
`configure_sig_info`, `set_lpm_after_ssr_in_apm`, `set_bootup_power_optimization_state`,
`request_power`. ModemManager issues only a small subset of these.
6. **DMS operating-mode flip: LPM → ONLINE.** `qcril_qmi_nas_dms_handle_modem_operating_mode:
modem's operating mode needs to be changed` → ril SETTING_OP_MODE → `modem ONLINE` (~270 ms after
SSR; line ~15967). post: `set_bootup_power_optimization_state=2`, ATEL UI-READY, enable
networking/sys-info indications → `is_online 1` → forces NW search.
## The decisive state difference (Android vs pmOS)
- **Android: the modem is held in `LPM` (low_power, powered, RF parked); QCRIL flips LPM→online.**
- **pmOS: the modem sits in `offline` (deeper) and `offline→online` is what returns DeviceNotReady /
no-ops.** Plus pmOS boots with 0 PDC configs (autoselect not active that boot) and provisioning
failing on the early-boot race.
## Recreate-exactly plan for pmOS (the test we have NEVER fully run)
On one clean slot-B boot, in order, all three prereqs together:
1. Get MCFG into modem NV (PDC autoselect / load+select+activate) → configs ACTIVE.
2. Establish primary-GW provisioning cleanly (run AFTER the QRTR race; it succeeds manually:
`--uim-change-provisioning-session=slot=1,activate=yes,session-type=primary-gw-provisioning,aid=
A0000000871002F310FFFF89B00140FF`).
3. Set the NAS preferred-network-type / radio-tech (QCRIL step 5) — replicate via libqmi NAS.
4. Bring the modem up through **LPM** (set `--dms-set-operating-mode=low_power` if it's in `offline`)
THEN `=online`. Do NOT flip offline→online directly.
5. Verify `--dms-get-capabilities` Networks non-empty + NAS serving system registers.
Replay vehicle: the openimsd libqmi fork (Set Feature for autoselect) + libqmi NAS/DMS/UIM via
python-GI. The point is the ORDER and the LPM state, captured above — not a partial subset.
## 2026-06-21 LIVE — clean provisioning + configs is NOT sufficient (eliminated)
Fresh slot-B boot, modem running (remoteproc2 up, no fatal in dmesg, zero modem rail failure).
State: mode=offline, configs PRESENT (sw 0x8013303 + platform loaded), USIM app=ready.
- msm-modem-uim-selection FAILED at boot (early-QRTR race: "Application was already selected"
then Transaction timed out) — the #7 symptom.
- Replicated the service BY HAND post-boot: deactivate timed out, but ACTIVATE returned
"Successfully changed provisioning session" => primary-gw provisioning now cleanly active.
- Immediately retried: low_power => InvalidTransition(60); online => DeviceNotReady(52).
Mode stayed offline. Networks '', Max TX 0.
CONCLUSION: configs-active + clean-primary-gw-provisioning + USIM-ready + healthy rails is
NOT enough to arm RF. The modem is wedged in `offline` (RF subsystem not-ready) and refuses
BOTH offline->low_power and offline->online. PAF #7's "clean provisioning -> online stops
returning 52" did NOT reproduce. The remaining differentiator is QCRIL's boot-time QMI/NAS
choreography (#8: radio-power state machine + UI-ready/poweron_opt) that brings the modem to
LPM. Android boots the modem INTO low_power; pmOS boots it into the deeper `offline`.
NEXT: capture how QCRIL puts/keeps the modem in LPM on rooted Android, replay on pmOS.
## 2026-06-21 REFRAME — Android modem SELF-ONLINES (mode 0); there is no QCRIL online handshake
Mined existing captures (android-live-20260619/deep/radio-boot.txt, android-capture-20260620/
qcril-ssr-fullinit.txt). The op-mode VALUE settles the "LPM" confusion:
- radio-boot.txt:3694 qcril_qmi_nas_dms_get_current_power_state: ".. known modem operating mode 0"
(DMS op-mode 0 = ONLINE).
- radio-boot.txt:3949 "telephony LPM | ril NONE | card UP | modem ONLINE"
=> the "LPM" is the AP/telephony airplane-state layer; the MODEM is ONLINE.
- radio-boot.txt:3862 qcril_qmi_nas_telephony_ready: "poweron_opt: 'UI ready' NOT sent to modem"
(skipped BECAUSE modem already online).
- ssr-fullinit: QCRIL only POLLS op-mode ("Operating mode is not available, polling...") and sets
bootup_power_optimization_state=2. It never issues DMS Set Operating Mode = online.
CONCLUSION (corrects #8's "Android boots modem into LPM"): on Android the modem firmware
SELF-COMPLETES RF-init and SELF-TRANSITIONS to ONLINE (mode 0) with NO online command from
QCRIL. The Android "LPM" is the RIL/airplane layer, not the modem. Therefore replaying a
"QCRIL online handshake" on pmOS is a dead end -- there isn't one.
The pmOS gate, precisely: the modem firmware's own RF-init does NOT complete on pmOS, so it
parks in OFFLINE (mode 3) and returns DeviceNotReady(52) when we ASK it to online (Android
never has to ask). Everything AP-side is equal/eliminated. The remaining differentiator is
what the modem reads/does at its OWN firmware RF-init -- strongest lead = modem-side EFS NV
state (Route B: /nv/item_files/.../power-up operating mode and/or mcfg_autoselect_by_uim),
i.e. something the Android modem persisted that makes RF-init succeed and self-online.

View file

@ -0,0 +1,680 @@
# QCRIL Full Provisioning — Gap Analysis & Design
**Purpose:** Reconstruct what QCRIL does to provision the modem every boot, identify every gap
vs. our current Python tools, and design a full replacement provisioner. This feeds a real
hardware bring-up; every structural claim cites a symbol, string, or file.
---
## A. What Our Tools Do Today
Two scripts, manually invoked in sequence.
### pdc_load.py (tools/pdc_load.py)
A chunked PDC LoadConfig implementation via libqmi GI bindings (1024-byte chunks). Reads an MBN
file, computes its SHA-1, and uploads it to the modem via `QMI_PDC_LOAD_CONFIG` with a rolling
token. Uses `EXPECT_INDICATIONS` flag so the modem can ack each chunk. On `remaining_size == 0`
the upload is complete. Accepts `platform` or `software` config type on the CLI.
**QMI sequence:**
1. Open QMI device (QRTR node)
2. Allocate PDC client
3. Connect `load-config` indication handler
4. Loop: `QMI_PDC_LOAD_CONFIG_REQ` (chunk) → indication → next chunk
5. Terminate on `remaining_size == 0`
No version checking. No selection. No activation. No carrier selection. No EFS interaction.
No SIM awareness. No timing logic.
### pdc_activate.py (tools/pdc_activate.py)
Implements the `SetSelectedConfig``ActivateConfig` two-step per the libqmi qmicli source
for `run_activate_config`. Takes a hex config ID on the CLI.
**QMI sequence:**
1. Open QMI device
2. Allocate PDC client
3. `QMI_PDC_SET_SELECTED_CONFIG_REQ` (type + ID + token)
4. On `set-selected-config` indication → `QMI_PDC_ACTIVATE_CONFIG_REQ` (type + token)
5. On `activate-config` indication → done (modem restart expected)
No version check (always activates). No load-first. No sequencing of HW before SW.
No handling of the restart-then-load ordering. No awareness of what was already active.
### modem_health.sh (tools/modem_health.sh)
Read-only snapshot: remoteproc state, QMI up/down, PDC list for platform and software configs
with active flags, DMS get-capabilities (the RF-arm signal), NAS home network, operating mode,
ModemManager status. Non-mutating by contract.
**What this tells us:** the `DMS Networks: ''` == RF not armed. `PDC Total: 0` == modem reset
since last provisioning.
---
## B. What QCRIL Actually Does
Evidence sources: `libril-qc-hal-qmi.so` (34 MB, arm64 ELF, class `main`); embedded source
paths `vendor/qcom/sm7250/proprietary/qcril-hal/modules/mbn/src/`; `qcril.db` (sqlite3);
`mbn_hw.txt`, `mbn_sw.txt`, `mcfg.version`; `qcrild.rc`, `init.sdm845.rc`.
### B.1 Timing — When QCRIL Runs vs Modem Boot
From `qcrild.rc`:
```
service vendor.qcrild /vendor/bin/hw/qcrild
class main
user radio
```
`class main` services start at the `main` class trigger, which fires after `post-fs-data`
completes. From `init.sdm845.rc`, the modem subsystem is NOT explicitly gated here — there is
no `wait_for_prop` on a modem-ready property before qcrild starts. `rmt_storage` (EFS daemon)
is also `class core` and starts earlier.
The modem firmware itself (MPSS via remoteproc `q6v5-mss`) loads at ~12.8 s after Linux boots,
independent of Android init stages. QCRIL starts asynchronously as part of `class main` (roughly
after `post-fs-data`, typically 1030 s into boot, well before userspace is fully up).
**The decisive timing insight** (from PROGRESS.md 2026-06-20 entry): the modem applies MCFG at
its own firmware RF-init window, which occurs during/after MPSS comes up (~12 s). QCRIL must
deliver configs within this window. QCRIL achieves this because `qcrild` (class main) starts
early and its MBN update sequence begins as soon as the DMS client becomes ready — triggered by
`DmsModule::handleDmsEndpointStatusIndMessage``qcril_qmi_start_mbn_update()`.
**The trigger:** strings `"DMS client is ready. start MBN update"` and
`"Module is ready,Start Mbn update"` both come from the `DmsModule` source
(`vendor/qcom/sm7250/proprietary/qcril-hal/modules/dms/src/DmsModule.cpp`). When DMS QMI
service becomes available on QRTR, `DmsModule::handleDmsEndpointStatusIndMessage` fires and
calls `qcril_qmi_start_mbn_update()` (nm: `_Z26qcril_qmi_start_mbn_updatev`). This is the
entry point into the full HW+SW MBN update state machine.
**Critical observation:** QCRIL does NOT wait for the modem to be "online" or for an RF-init
complete signal before loading configs. It loads as soon as DMS is up (the modem is in some
early QMI-ready state but not yet RF-armed). The modem then applies the loaded MCFG at its
own RF-init pass, which happens after configs are in place.
The string `"is_ssr_or_bootup %d"` (`qcril_mbn_sw_update.cpp`) shows that the SW update path
distinguishes cold boot from SSR. On warm SSR (subsystem restart), QCRIL only queries — does
NOT reload (`qcril_qmi_pdc_get_active_config_info`, `qcril_qmi_pdc_get_selected_mbn_config`).
On cold bootup, it runs the full load sequence. This matches the Android forensics finding:
QCRIL skips reload on warm SSR (configs survived) but loads fresh on cold boot.
### B.2 Config Discovery — Where QCRIL Finds MBN Files
Two source directories (strings in libril-qc-hal-qmi.so):
- Primary: `/data/vendor/modem_config/` (writable, symlinked or copied from vendor)
- Vendor source: `/vendor/rfs/msm/mpss/readonly/vendor/mbn/` (read-only vendor partition)
On Android, QCRIL reads `mcfg.version` from `/vendor/rfs/msm/mpss/readonly/vendor/mbn/mcfg.version`
(path literal in strings). It stores local DB metadata at
`/data/vendor/modem_config/ver_info.txt` and in `qcril.db` tables.
The hardware config tree is indexed by `mbn_hw.txt`; the software config tree by `mbn_sw.txt`.
QCRIL enumerates these lists to populate its internal DB (`qcril_mbn_hw_load_to_db`,
`qcril_mbn_sw_load_to_db`) with version metadata parsed from each MBN file's header.
Error strings `"QCRIL_ERROR:IO: No hw mbn config directory"` and
`"QCRIL_ERROR:IO: No sw mbn config directory"` confirm it expects both
`/data/vendor/modem_config/mcfg_hw/` and `/data/vendor/modem_config/mcfg_sw/` to exist and
be populated. The MBN path is also stored in `persist.vendor.radio.mbn_path` (property string
in libril-qc-hal-qmi.so).
**On pmOS:** our MBN files live at `/mnt/vendor/persist/rfs/readonly/vendor/mbn/` (bind-mounted
from the persist partition, analogous to tqftpserv's serve path). We have both `mcfg_hw/` and
`mcfg_sw/` trees. The provisioner must be told where to find them.
### B.3 Config Selection — How QCRIL Picks the Right MBN
#### Hardware MBN Selection
Symbol `qcril_mbn_hw_get_hw_name_to_look_for` (T, exported): looks up the hardware platform
name to match against. Symbol `qcril_mbn_hw_get_hw_config_from_db` looks up the best matching
HW config from the internal DB by that name.
The HW name comes from the MBN metadata header embedded in each `mcfg_hw.mbn` file. Symbol
`qcril_mbn_meta_retrieve_hw_name` reads the `HW_NAME` field from the MBN binary. The DB table
`qcril_hw_mbn_file_type_table` stores `(FILE, HW_NAME, SHORT_NAME, CONFIG_ID, version fields)`.
The selection is a name-match: QCRIL identifies the running SoC/platform (from a system property
or DMS device ID) and picks the HW MBN whose `HW_NAME` matches. For blueline/SDM845 + LA + SS
(single-SIM), the match is `mcfg_hw/generic/common/SDM845/LA/SS/mcfg_hw.mbn`
(verified: only SDM845/LA/SS and SDM845/LA/7+7_mode/SR_DSDS are in `mbn_hw.txt`; blueline is
single-SIM so SS wins). The `HW_NAME` field in that MBN file must contain the identifier QCRIL
matches against.
**Note:** The `HW_NAME` used for matching is extracted from the MBN binary header by
`qcril_mbn_meta_retrieve_hw_name` — we don't have a text listing of what that string IS. For
blueline we know the correct MBN from Android forensics: `mcfg_hw/generic/common/SDM845/LA/SS/mcfg_hw.mbn`.
Our provisioner can hardcode this for SDM845/LA/SS rather than implement the full name extraction.
#### Software MBN Selection (Carrier)
This is the complex path. QCRIL uses a multi-level lookup in priority order:
**Level 1 — ICCID long-IIN (9-digit):** `qcril_mbn_db_retrieve_sw_mbn_file_for_long_iccid`
queries `qcril_sw_mbn_iin_table` with the first 9 digits of the SIM's ICCID (`MCFG_LONG_IIN`
field). If matched, uses that SW MBN. The ICCID is stored in `qcril_mbn_sw_iccid` (B, bss) and
the current ICCID is read from `/data/vendor/radio/iccid` (path string in libril-qc-hal-qmi.so)
or via UIM QMI `get_iccid` (`com.qualcomm.qti.qcril.uim.get_iccid_sync_request`).
**Level 2 — ICCID short-IIN (6-digit):** `qcril_mbn_db_retrieve_sw_mbn_file_for_iccid` queries
`qcril_sw_mbn_iin_table` with the first 6 digits (`MCFG_IIN`).
**Level 3 — MCC/MNC:** `qcril_mbn_db_query_sw_mbn_file_with_mcc_mnc` queries
`qcril_sw_mbn_mcc_mnc_table` with the SIM's MCC+MNC. MCC/MNC is retrieved via
`com.qualcomm.qti.qcril.legacy.event.INTERNAL_UIM_GET_MCC_MNC` and/or the UIM IMSI path.
The DB tables (`qcril_sw_mbn_iin_table`, `qcril_sw_mbn_mcc_mnc_table`) are empty in the
`qcril.db` we have — QCRIL populates them at runtime by parsing the MBN files and inserting
rows. The `qcril.db` version in `qcril-config/` is a prebuilt baseline; QCRIL rebuilds the MBN
tables on first boot or when `mcfg.version` changes.
The `qcril_manual_prov_table` contains 4 ICCID entries with `USER_PREF=1` — these are
user-overridden manual provisioning entries that skip the automatic lookup. Our Fido SIM is
unlikely to match these.
**Exception tables:** `qcril_mbn_iccid_exception_table` and `qcril_mbn_imsi_exception_table`
are both empty in this build — no exceptions defined.
**For Rogers/Fido (MCC 302):** Fido is an MVNO on the Rogers network. The MBN tree contains
`mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn` (line 25 in `mbn_sw.txt`). Rogers MCC
is 302. Whether QCRIL matches Fido (sub-MVNO) to the Rogers MBN depends on whether Rogers's
ICCID/IIN prefix or MCC/MNC 302-480 (Rogers) vs 302-370 (Fido) maps to that file in the
runtime-built DB. QCRIL's logic also has a `persist.vendor.radio.sw_mbn_openmkt` (open-market)
flag that may affect fallback behavior — evidence: property string in libril-qc-hal-qmi.so.
**IMEI awareness:** QCRIL does NOT use the IMEI for MBN selection. IMEI is read via
`RilRequestGetDeviceIdentityMessage` / `DmsModule::handleDeviceIdentiyRequestMessage` for
device identity reporting, not for PDC config selection. The `qcril_mbn_cur_instance_id` (B)
is the slot/instance ID (0/1), not the IMEI.
**Subscription tracking:** QCRIL caches the current subscription's ICCID
(`qcril_mbn_sw_iccid`), MCC (`qcril_mbn_cur_instance_mcc`), MNC (`qcril_mbn_cur_instance_mnc`),
and sub_id (`qcril_mbn_cur_sub_id`). When any of these change (SIM swap), `qcril_mbn_sw_is_sim_info_different_from_cache`
detects it and `qcril_mbn_sw_if_restart_needed` decides whether to re-run the SW provisioning
sequence. String: `"restart needed due to mcc/mnc/iccid/sub_id change"`.
### B.4 Version/Diff/Skip Logic
Symbols `qcril_mbn_db_is_sw_version_updated` (T, exported) and the string
`"file_name: %s, config_name: %s, version: 0x%08x, is_matched: %d"` describe the core
version-check logic:
QCRIL parses each candidate MBN's version from its header (`mcfg_get_oem_version`,
`mcfg_get_qc_version` — both exported T symbols) and compares them to what the modem currently
holds. The modem's active config version is retrieved via `qcril_qmi_pdc_get_active_config_info`
(which calls `QMI_PDC_GET_CONFIG_INFO` with the active config ID).
Version is stored as a 32-bit split field: `MCFG_VERSION_FAMILY`, `MCFG_VERSION_OEM`,
`MCFG_VERSION_CARRIER`, `MCFG_VERSION_MINOR` (function `qcril_mbn_db_split_version`, T exported).
If the modem's active config version matches the candidate MBN's version (`is_matched == 1`),
QCRIL skips the reload. This is the "already provisioned" fast path tracked via Android
property `persist.vendor.radio.hw_mbn_loaded` / `persist.vendor.radio.sw_mbn_loaded` and
`persist.vendor.radio.cnv.ver_info`. The string `"prev_ver_info: %s, cur_ver_info: %s"`
shows QCRIL compares a cached version string (from persist prop) to the current MBN version
before deciding to reload.
The event `QMI_RIL_PDC_PARSE_DIFF_RESULT` (`qcril_evt_id_QMI_RIL_PDC_PARSE_DIFF_RESULT`) and
function `parse_mbn_diff_result` indicate that QCRIL can also interpret a diff-result indication
from the modem to decide whether the new config differs enough from the active one to warrant
a restart. String: `"mbn differences length ="`.
**Summary of skip conditions:**
1. `persist.vendor.radio.hw_mbn_loaded` / `sw_mbn_loaded` is set AND version matches modem active config → skip reload (fast path).
2. On warm SSR: query get_active_config_info; if active matches → skip reload.
3. On cold boot with no active config (count=0): always load.
**For pmOS:** Since configs are volatile (proven: 0/0 after every cold boot, PROGRESS.md
2026-06-20), version check is moot — there is never an existing active config on our cold boot.
We always need to load. The skip logic matters only when we build a persistent-prop mechanism
or if we add SIM-change re-provisioning.
### B.5 The Full Load → Select → Activate Sequence
Reconstructed from the symbol table event chain:
#### Phase 1: HW MBN (platform config) — `REQUEST_MBN_HW_*` events
1. `REQUEST_MBN_HW_INIT``qcril_mbn_hw_update_init_hdlr`: Initialize HW update state machine.
2. `REQUEST_MBN_HW_GET_SELECTED_CONFIG``qcril_mbn_hw_query_selected_config_hndlr`:
Call `QMI_PDC_GET_SELECTED_CONFIG` for type=PLATFORM. If active config matches candidate,
skip to SW phase.
3. `REQUEST_MBN_HW_LOAD_CONFIG``qcril_mbn_hw_load_config_hndlr`:
Chunked `QMI_PDC_LOAD_CONFIG` for the selected HW MBN file.
4. `REQUEST_MBN_HW_SELECT_CONFIG``qcril_mbn_hw_select_config_hndlr`:
`QMI_PDC_SET_SELECTED_CONFIG` for type=PLATFORM with the just-loaded config ID.
5. `REQUEST_MBN_HW_ACTIVATE_CONFIG``qcril_mbn_hw_activate_config_hndlr`:
`QMI_PDC_ACTIVATE_CONFIG` for type=PLATFORM. Modem restarts MPSS to apply MCFG.
Interleaved: `REQUEST_MBN_HW_DELETE_CONFIG` and `REQUEST_MBN_HW_DEACTIVATE_CONFIG` are also
in the symbol table; QCRIL cleans up old/stale configs. `qcril_mbn_cleanup_inactive_configs`
and `qcril_mbn_pdc_delete_all_sw_configs` delete inactive entries from modem RAM before loading
new ones (to avoid hitting the modem's config count limit).
#### Phase 2: SW MBN (carrier config) — `REQUEST_MBN_SW_*` events
After HW phase completes (modem has restarted), SW phase runs:
1. `REQUEST_MBN_SW_INIT``qcril_mbn_sw_update_init_hdlr`: Start SW state machine; read ICCID
and MCC/MNC from SIM (requires UIM to be up and SIM app selected).
2. `VERIFY_MBN_SW_INIT` / `REQUEST_VERIFY_MBN_SW_INIT`: Check whether modem supports the MBN
update feature at all (`qcril_qmi_imss_query_modem_supported_features`). String:
`"Modem feature not supported. Continue with sw mbn update"` — if unsupported, SW update
continues anyway (the feature check gates VoLTE-specific paths, not the basic PDC load).
3. `REQUEST_MBN_SW_GET_SELECTED_CONFIG``qcril_mbn_sw_query_selected_config_hndlr`:
Query `QMI_PDC_GET_SELECTED_CONFIG` for type=SOFTWARE. Version-check against candidate.
4. `REQUEST_MBN_SW_COUNT_PENDING_CONFIGS``qcril_mbn_sw_count_pending_configs_hndlr`:
Count pending (loaded but not yet selected) SW configs on the modem. String:
`"has pending configuration"`. If a pending config already matches the desired SW MBN, QCRIL
may skip the load step and go directly to select.
5. `REQUEST_MBN_SW_LOAD_CONFIG``qcril_mbn_sw_load_config_hndlr`:
Chunked `QMI_PDC_LOAD_CONFIG` for the selected SW MBN file (type=SOFTWARE).
6. `REQUEST_MBN_SW_SELECT_CONFIG``qcril_mbn_sw_select_config_hndlr`:
`QMI_PDC_SET_SELECTED_CONFIG` for type=SOFTWARE.
7. `REQUEST_MBN_SW_ACTIVATE_CONFIG``qcril_mbn_sw_activate_config_hndlr`:
`QMI_PDC_ACTIVATE_CONFIG` for type=SOFTWARE. Second modem restart.
8. `REQUEST_MBN_SW_CLEANUP_CONFIG``qcril_mbn_sw_cleanup_config_hndlr`: Delete stale configs.
#### Phase 3: PDC Refresh Indication
`qcril_qmi_pdc_refresh_ind_hdlr` (`_Z30qcril_qmi_pdc_refresh_ind_hdlrPvj`) and
`PDCRefreshIndication` / `"handlePDCRefreshInd(): "` — the modem can emit a PDC refresh
indication after activation. QCRIL handles this to re-query config state. The `DataModule` also
handles `PDCRefreshIndication` (`_ZN7rildata10DataModule26handlePDCRefreshIndicationENSt3__110shared_ptrI7MessageEE`).
This is the modem telling userspace "I've applied the new configs." QCRIL listens for it but
does NOT depend on it to proceed (it's a notification, not a gate).
#### The enable/disable modem update mechanism
Symbols `qcril_qmi_pdc_enable_modem_update`, `qcril_qmi_pdc_disable_modem_update`,
`pdc_enable_auto_selection` indicate that before loading configs, QCRIL may call
`QMI_PDC_SET_FEATURE_VERSION` or a similar PDC command to enable/lock auto-selection on the
modem side. `qcril_qmi_pdc_is_modem_mbn_updated` checks if the modem already has the desired
config loaded (query path, not load path). **The exact QMI message ID for enable/disable is not
visible from strings alone** — this is an under-documented PDC message not exposed via libqmi
public API. Evidence is ambiguous; mark as uncertain.
### B.6 EFS Interaction
Minimal and indirect. QCRIL itself does NOT write to EFS for config provisioning. The evidence:
- `current MBNs in modem EFS:` (string in libril-qc-hal-qmi.so) — QCRIL can enumerate what's
in modem EFS via a PDC query command (list configs), but this is a read.
- Android forensics (PROGRESS.md 2026-06-19): `rmt_storage` serves EFS read-write; the modem
writes ~2 MB to `modem_fs2` AFTER going online. This EFS write happens INSIDE the modem
firmware after RF-init completes — not driven by QCRIL.
- The `EFS lead at PAF/modem.md L38` is marked DEAD: rmtfs writable + Android-primed EFS still
gave DeviceNotReady. EFS content is not what's missing.
**Conclusion:** QCRIL does not write EFS for PDC provisioning. It loads configs into modem RAM
via QMI PDC, the modem applies them at RF-init and then writes its own EFS state. Our provisioner
does not need to write EFS.
### B.7 The OTA Update Path (not needed for us)
`qcril_mbn_kick_ota_update_in_dedicated_thread` handles over-the-air config updates (carrier
pushing new MBNs). This runs in a dedicated thread, separate from boot provisioning. Not
relevant to our bring-up.
### B.8 Properties Used by QCRIL
| Property | Purpose |
|---|---|
| `persist.vendor.radio.hw_mbn_loaded` | Cached: HW MBN was loaded (skip on next boot if version matches) |
| `persist.vendor.radio.sw_mbn_loaded` | Cached: SW MBN was loaded |
| `persist.vendor.radio.mbn%d` | Per-slot carrier index (e.g., `mbn0=72`) |
| `persist.vendor.radio.mbn_path` | Path to the active MBN directory |
| `persist.vendor.radio.cnv.ver_info` | Cached version info string (prev vs current comparison) |
| `persist.vendor.radio.ver_info` | Version info |
| `persist.vendor.radio.sw_mbn_update` | SW update enabled/disabled flag |
| `persist.vendor.radio.hw_mbn_update` | HW update enabled/disabled flag |
| `persist.vendor.radio.sw_mbn_openmkt` | Open-market SIM handling flag |
| `persist.vendor.radio.long_iin_mbn` | Use long-IIN (9-digit) matching for SW MBN |
These are Android system properties not directly available on pmOS. Our provisioner should
implement equivalent state tracking in a config file (e.g., `/var/lib/qcril-prov/state.json`).
---
## C. The Gap — Itemized
Each item marked: **(RF-arm)** = required to arm RF at all, **(carrier)** = carrier-correctness,
**(nice)** = quality improvement.
| # | Gap | Severity |
|---|---|---|
| 1 | **Timing: provisioner must run before RF-init window closes** — our scripts run manually; QCRIL runs automatically on DMS-service-ready event, before `sys.boot_completed`. A systemd service that fires before ModemManager and monitors DMS readiness is absent. | **(RF-arm)** |
| 2 | **HW MBN must be loaded before SW MBN** — our scripts don't enforce ordering. QCRIL runs the full HW phase (load+select+activate → modem restart) before starting SW phase. Interleaving or reversing causes modem inconsistency. | **(RF-arm)** |
| 3 | **Post-activate modem restart handling** — on `QMI_PDC_ACTIVATE_CONFIG` the modem subsystem restarts. QCRIL re-waits for DMS to become ready after each restart before proceeding to the next phase (SW after HW). Our script ignores the restart and the QMI transport drop. | **(RF-arm)** |
| 4 | **SW MBN carrier selection by ICCID/MCC-MNC** — we hardcode the Rogers MBN path. QCRIL reads ICCID from `/data/vendor/radio/iccid` and queries UIM MCC/MNC, then does a DB lookup. Without this, a SIM swap would need a manual provisioner edit. | **(carrier)** |
| 5 | **Stale config cleanup before loading** — QCRIL calls `qcril_mbn_cleanup_inactive_configs` / `qcril_mbn_pdc_delete_all_sw_configs` to delete old configs before loading new ones. Without cleanup, the modem accumulates stale RAM-configs and may hit internal limits or pick the wrong active config. | **(RF-arm / carrier)** |
| 6 | **SW pending config check** — QCRIL calls `QMI_PDC_QUERY_PENDING_CONFIG` (event `REQUEST_MBN_SW_QUERY_PENDING_CONFIG`) before loading to see if the desired config is already pending. Missing this means redundant loads, not a breakage, but it is part of the correct sequence. | **(nice)** |
| 7 | **Version skip logic** — QCRIL skips reload when modem already holds matching config version. Without this, we reload every boot unconditionally (harmless functionally but slow and causes an extra modem restart). | **(nice)** |
| 8 | **PDC modem-update enable/disable** — QCRIL may call a PDC feature-version command around the load. The exact behavior is unclear from strings alone (ambiguous evidence). May be needed to unlock PDC loading on certain modem states. **UNCERTAIN — needs disassembly to confirm.** | **(RF-arm, uncertain)** |
| 9 | **SIM-change re-provisioning** — QCRIL monitors UIM refresh indications and re-runs SW provisioning on SIM swap. Not needed for single-boot bring-up but needed for production use. | **(nice)** |
| 10 | **HW name extraction from MBN header** — QCRIL reads the `HW_NAME` field from each `mcfg_hw.mbn` binary to populate the DB, then matches it against the running platform. We can hardcode SDM845/LA/SS for blueline, deferring this. | **(nice for generality)** |
---
## D. Design — Full Provisioner
### D.0 Guiding Constraints
- Must complete (both HW and SW activated) before the modem's RF-init window closes. From
PROGRESS.md: PDC LoadConfig times out if attempted before ~45 s uptime; succeeds by ~226 s.
The RF-init window is within the modem's post-MPSS-up initialization, which is between ~12 s
and some unknown deadline (likely within 60 s of MPSS-up). **The critical open question:**
does the modem wait indefinitely for a config or does it time out and RF-init with whatever
is loaded? This must be determined empirically (see Next Steps).
- Run as a systemd service, ordered before ModemManager, after QRTR/QMI is up.
- Read-only EFS — no EFS writes needed from our side.
- Python (libqmi GI) to reuse pdc_load.py/pdc_activate.py primitives.
### D.1 Service Unit
File: `/etc/systemd/system/qcril-prov.service`
```ini
[Unit]
Description=QMI PDC MBN provisioner (QCRIL equivalent)
After=qrtr.service rmtfs.service
Before=ModemManager.service
Wants=qrtr.service rmtfs.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/qcril-prov.py
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
```
### D.2 Provisioner Script Design (`qcril-prov.py`)
#### Stage 0: Wait for DMS
Poll `qmicli -d qrtr://0 --dms-get-ids` until it returns IMEI without error. Timeout 120 s.
This replicates QCRIL's `DmsModule::handleDmsEndpointStatusIndMessage` trigger. Retry interval:
2 s. Add a jitter-free deadline check (not a sleep loop — use subprocess with timeout).
EVIDENCE: string `"DMS client is ready. start MBN update"` from DmsModule.
#### Stage 1: Read SIM Identity (for SW MBN selection)
Read ICCID and MCC/MNC before touching PDC, as QCRIL does.
- Option A (simple): read `/data/vendor/radio/iccid` (path string in libril-qc-hal-qmi.so).
On pmOS this file may not be populated by anything. Fallback to QMI.
- Option B (robust): `qmicli --uim-get-card-status` to get ICCID, then
`qmicli --nas-get-home-network` or `--uim-get-imsi` to get MCC/MNC.
For Rogers/Fido: MCC=302, MNC=480 (Rogers) or 302-370 (Fido).
The mbn_sw.txt has `mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn` — this is the file
to use. Fido uses Rogers infrastructure (same MCC prefix 302); whether the DB lookup maps Fido's
IIN/MNC to the Rogers MBN depends on what was in the runtime-built DB on Android. Empirically,
the modem ran Fido LTE on Android with the Rogers MBN active, so Rogers/CA is the correct SW
MBN for both carriers on this device. Hardcode Rogers for now; add MCC/MNC dispatch later.
#### Stage 2: Delete All Existing Configs
Replicate `qcril_mbn_cleanup_inactive_configs` / `qcril_mbn_pdc_delete_all_sw_configs`.
Use `QMI_PDC_LIST_CONFIGS` (both types) → for each config ID returned, call
`QMI_PDC_DELETE_CONFIG`. This clears the modem's RAM before we load fresh.
EVIDENCE: event `QMI_RIL_PDC_DELETE_ALL` (`qcril_evt_id_QMI_RIL_PDC_DELETE_ALL`) and
`qcril_mbn_pdc_delete_all_sw_configs`.
Implementation via libqmi GI: `Qmi.MessagePdcDeleteConfigInput.new()`, set type and config_id,
call `client.delete_config()`. Loop over both PLATFORM and SOFTWARE types.
**Why needed:** avoids stale config accumulation and ensures we control what's active. On cold
boot the list is 0/0 (PROGRESS.md 2026-06-20), so this is a no-op on cold boot but defensive
for warm SSR.
#### Stage 3: Load + Select + Activate HW MBN
MBN file: `/mnt/vendor/persist/rfs/readonly/vendor/mbn/mcfg_hw/generic/common/SDM845/LA/SS/mcfg_hw.mbn`
(56 KB, confirmed in PAF/modem.md)
1. **Load:** use `pdc_load.py` logic (already working) with type=PLATFORM.
2. **Get config ID:** after load completes, call `QMI_PDC_LIST_CONFIGS` type=PLATFORM to find
the ID of the just-loaded config. (The LoadConfig indication does not return the ID;
ListConfigs does.)
3. **Select:** `QMI_PDC_SET_SELECTED_CONFIG` type=PLATFORM, id=<returned ID>.
4. **Activate:** `QMI_PDC_ACTIVATE_CONFIG` type=PLATFORM. Modem MPSS restarts.
EVIDENCE: full symbol chain `qcril_mbn_hw_load_config_hndlr``qcril_mbn_hw_select_config_hndlr`
`qcril_mbn_hw_activate_config_hndlr` in libril-qc-hal-qmi.so nm output.
#### Stage 4: Wait for Modem to Come Back After HW Restart
After HW activation, the modem MPSS restarts. QCRIL detects this via QMI transport drop +
re-registration of DMS service. Poll same as Stage 0. Timeout 60 s.
Without this wait, the SW load in Stage 5 will fail (PDC client not available / modem not yet
QMI-ready).
EVIDENCE: pdc_activate.py comment: "config activation is expected to reboot the device"; modem
restart events `QCRIL_EVT_QMI_RIL_MODEM_RESTART_*`; PROGRESS.md 2026-06-19 "qmicli
--pdc-activate-config=platform triggered modem restart that cleared the platform config."
#### Stage 5: Load + Select + Activate SW MBN
MBN file (Rogers/CA): `/mnt/vendor/persist/rfs/readonly/vendor/mbn/mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn`
(MCC=302 carrier confirmed active on Android in PROGRESS.md 2026-06-19)
Same sequence as Stage 3 but type=SOFTWARE.
1. Load (chunked, type=SOFTWARE)
2. List configs → get ID
3. SetSelectedConfig (type=SOFTWARE)
4. ActivateConfig (type=SOFTWARE) → second modem restart
EVIDENCE: `qcril_mbn_sw_load_config_hndlr``qcril_mbn_sw_select_config_hndlr`
`qcril_mbn_sw_activate_config_hndlr` in nm output.
#### Stage 6: Wait and Verify
After SW activation, wait for DMS to come back (Stage 0 again). Then run the modem_health.sh
check logic inline: call `QMI_DMS_GET_CAPABILITIES` and verify `Networks` is non-empty.
If `Networks` is non-empty, provisioning succeeded. Set a state file
(`/var/lib/qcril-prov/last_provisioned`) and exit 0 so systemd marks the service complete.
If `Networks` is still empty: log a diagnostic (PDC list state, DMS caps) and exit non-zero.
Systemd will not start ModemManager (it's ordered After this service).
#### Stage 7: Hand Off to ModemManager
With both configs active and RF armed (Networks populated), ModemManager's
`Set Operating Mode=online` should succeed. ModemManager starts as normal.
### D.3 SW MBN Selection Logic (future)
When we want auto-carrier selection (beyond hardcoded Rogers):
```python
# Selection priority order (matches QCRIL):
# 1. ICCID long-IIN (9 digits) → qcril_sw_mbn_iin_table.MCFG_LONG_IIN
# 2. ICCID short-IIN (6 digits) → qcril_sw_mbn_iin_table.MCFG_IIN
# 3. MCC+MNC → qcril_sw_mbn_mcc_mnc_table
# 4. Wildcard → mcfg_sw/generic/common/WildCard/Wildcard/mcfg_sw.mbn
def select_sw_mbn(iccid, mcc, mnc, db_path):
import sqlite3
db = sqlite3.connect(db_path)
# Populate DB from MBN tree if empty (QCRIL does this at runtime)
# ... (qcril_mbn_sw_load_to_db equivalent)
long_iin = iccid[:9] if iccid else None
short_iin = iccid[:6] if iccid else None
if long_iin:
row = db.execute("SELECT FILE FROM qcril_sw_mbn_iin_table WHERE MCFG_LONG_IIN=?",
[long_iin]).fetchone()
if row: return row[0]
if short_iin:
row = db.execute("SELECT FILE FROM qcril_sw_mbn_iin_table WHERE MCFG_IIN=?",
[short_iin]).fetchone()
if row: return row[0]
if mcc and mnc:
row = db.execute("SELECT FILE FROM qcril_sw_mbn_mcc_mnc_table WHERE MCC=? AND MNC=?",
[mcc, mnc]).fetchone()
if row: return row[0]
return "mcfg_sw/generic/common/WildCard/Wildcard/mcfg_sw.mbn" # fallback
```
**Note:** The `qcril.db` tables will be empty until we populate them. QCRIL populates them by
parsing each MBN binary's header (IIN/MCC-MNC metadata embedded in the MCFG blob). We need to
implement MBN metadata parsing to extract IIN and MCC-MNC lists from the MBN binary format to
build the DB. This is deferred — hardcoded Rogers is correct for this device and SIM.
### D.4 Solving the Timing Wall
> **CORRECTION 2026-06-20 (capture analysis — see `PAF/modem.md` frontier #4):** The "load +
> activate every boot, accept two restarts" model below is a *reconstruction of the unobserved
> first-provision path*, NOT what Android does in steady state. The captured Android radio logs
> (`android-live-20260619/deep/`) show steady-state boot is **verify-only**: `is_modem_mbn_updated`
> finds `prev_ver == cur_ver` and SKIPS the entire load/select/activate sequence; the modem
> self-applies MCFG persisted in its own EFS (`modemst`) at firmware RF-init. The full load path
> (and any `pdc_enable_auto_selection` call) runs only on first-provision / version change, which
> we have never captured. This section is retained as the design for the per-boot *workaround*
> (`tools/qcril-prov.py`, path A); the hardware-faithful target is provision-once + `modemst`
> persistence (path B). Resolve via the first-provision capture: `PAF/first_provision_capture.md`.
**The question:** Does the modem wait indefinitely for configs before RF-init, or does it have
a deadline?
**What we know:** PROGRESS.md 2026-06-20 states "PDC LoadConfig times out if attempted too
early (uptime ~45 s, modem not yet PDC-ready) but succeeds by ~226 s." This is the PDC-client
readiness window (when QRTR route is established). The RF-init window is a separate question.
**Hypothesis A:** The modem RF-inits as part of MPSS boot (~12 s) with whatever configs are
loaded. On cold pmOS boot there are none (0/0), so RF-init proceeds with no MCFG = no RF arm.
Provisioning after the fact cannot retroactively arm RF. The modem must be restarted (via
PDC ActivateConfig) after configs are loaded, which triggers a new RF-init pass WITH the
configs. This is the mechanism QCRIL uses.
**This matches all evidence:** PROGRESS.md 2026-06-20 states "A post-init userspace load cannot
arm an already-initialized RF, and nothing persists to carry configs to next boot." And:
"configs are PURELY VOLATILE, NOT persisted."
**Implication for our provisioner:** The timing wall is NOT about being early enough — it's
about triggering a PDC ActivateConfig (which causes modem restart + re-RF-init with configs
loaded). QCRIL's "early" timing is an optimization to reduce the number of restarts (load before
first RF-init = one restart; load after first RF-init = two restarts). Our provisioner will
cause two MPSS restarts per boot (once for HW, once for SW). This is acceptable for bring-up.
**The `--pdc-monitor-refresh` question** (PROGRESS.md 2026-06-20 "NEXT"): The modem may emit
a PDC refresh indication (`PDCRefreshIndication`, `qcril_qmi_pdc_refresh_ind_hdlr`) after
ActivateConfig, signaling completion. We should subscribe to this indication (via libqmi
`RegisterForPDCIndication`) to confirm activation before proceeding to the SW phase.
EVIDENCE: `"handleRegisterForPDCIndication(): failed with rc="` and
`"]: pdc refresh Completed"` strings.
**Recommended approach for `--pdc-monitor-refresh`:** Add a PDC indication registration at
startup and listen for `QMI_PDC_CONFIG_CHANGE_IND` (which is what `pdc_refresh_ind_hdlr`
handles). After each ActivateConfig, wait for the refresh-completed indication before polling
DMS. Timeout 30 s if indication doesn't arrive (fall through to DMS poll).
---
## E. Implementation Sequence (Recommended)
### Step 0 (validation experiment, before any code): confirm two-restart approach works
Manual test on device:
```sh
# Clean slate
qmicli -d qrtr://0 --pdc-list-configs=platform # should show 0
qmicli -d qrtr://0 --pdc-list-configs=software # should show 0
# Load + select + activate HW
python3 pdc_load.py qrtr://0 platform .../mcfg_hw.mbn
# get ID from pdc-list-configs=platform
python3 pdc_activate.py qrtr://0 platform <ID>
# wait ~30s for modem restart and DMS back
# Load + select + activate SW (Rogers)
python3 pdc_load.py qrtr://0 software .../mcfg_sw.mbn
# get ID from pdc-list-configs=software
python3 pdc_activate.py qrtr://0 software <ID>
# wait ~30s
# Check RF
qmicli -d qrtr://0 --dms-get-capabilities # expect Networks non-empty
```
This is the minimal end-to-end proof before building the daemon.
### Step 1: `qcril-prov.py` v1 (hardcoded)
Single script implementing the full D.2 flow with:
- Hardcoded HW MBN path (SDM845/LA/SS)
- Hardcoded SW MBN path (Rogers/CA)
- DMS poll loop (Stage 0)
- Delete-all existing configs (Stage 2)
- HW load/select/activate (Stage 3)
- Wait for modem restart (Stage 4, DMS poll again)
- SW load/select/activate (Stage 5)
- Verify Networks non-empty (Stage 6)
No carrier selection DB needed yet.
### Step 2: systemd integration
Wire the service unit (D.1), enable it, reboot, confirm RF arms without manual intervention.
### Step 3: add carrier selection (future, when needed for other SIMs)
Implement MBN binary parser for IIN/MCC-MNC metadata, populate a local sqlite3 DB, implement
`select_sw_mbn()` (D.3).
---
## F. Open Questions (evidence-bounded)
1. **Does the modem accept ActivateConfig before DMS is "fully" ready?** We know DMS must be
up for QCRIL to start — but does DMS-up precede or follow the RF-init window? Unknown without
`--pdc-monitor-refresh` experiment or timing capture.
2. **What is `pdc_enable_auto_selection(ii)` / `qcril_qmi_pdc_enable_modem_update`?** The
symbol `_Z25pdc_enable_auto_selectionii` exists in the binary. It may correspond to
`QMI_PDC_SET_FEATURE_VERSION` or a similar private PDC message that tells the modem to
accept auto-selected configs. If this is required, LoadConfig will succeed but ActivateConfig
may no-op. **Needs disassembly of `qcril_mbn_hw_update_init` to determine if it's called
before the load sequence.** This is the only remaining significant unknown.
3. **Does the Fido SIM (MVNO on Rogers) match the Rogers/CA MBN via MCC-MNC or IIN?** Fido's
MCC-MNC is 302-370; Rogers is 302-720. The Rogers MBN was active on Android with this Fido
SIM. So Rogers/CA is confirmed correct. The IIN or MCC-MNC lookup that produced this result
is unknown without the runtime-populated DB — but the outcome is known.
4. **Is the HW MBN name match on "SDM845-LA-SS" or another string?** `qcril_mbn_hw_get_hw_name_to_look_for`
returns the string used to match HW configs. The actual string is not visible without
disassembly. For blueline, only one SDM845/LA/SS entry exists in mbn_hw.txt; even if our
provisioner gets the name wrong, we can hardcode the file path.
---
## Sources
| File | Role |
|---|---|
| `android-reference/qcril-libs/libril-qc-hal-qmi.so` | Primary evidence: symbols (nm -D), strings |
| `android-reference/vendor-mbn/mbn/mbn_hw.txt` | HW MBN index (2 entries for SDM845) |
| `android-reference/vendor-mbn/mbn/mbn_sw.txt` | SW MBN index (55 entries, includes Rogers/CA) |
| `android-reference/vendor-mbn/mbn/mcfg.version` | MCFG version: `g845-00194-220517-B-8604334` |
| `android-reference/qcril-config/qcril.db` | DB schema; tables empty in prebuilt (QCRIL populates at runtime) |
| `android-reference/init-rc/qcrild.rc` | `class main`, confirms qcrild timing |
| `android-reference/init-rc/init.sdm845.rc` | Boot ordering; `rmt_storage` = `class core` (earlier than main) |
| `PAF/modem.md` | Eliminated leads; confirmed facts about PDC volatility and EFS |
| `PROGRESS.md` (2026-06-20 entry) | Decisive timing proof; QCRIL symbol inventory |
| `tools/pdc_load.py`, `tools/pdc_activate.py` | Our current implementation baseline |

View file

@ -0,0 +1,229 @@
# QCRIL pre-online QMI choreography — extracted from libril-qc-hal-qmi.so
Binary: `android-reference/vendor-stack-20260621/vendor/lib64/libril-qc-hal-qmi.so`
(ARM64, stripped of locals but **full dynamic symbol table present** — 22045 syms, qcril
function names intact). Decompiler unavailable (no rz-ghidra / pdg); extraction is from
disassembly of named functions + QMI message-id immediates + the log-string call flow +
the working-Android logs in `android-capture-20260621/diag/`.
All addresses are file offsets into the `.so` `.text`. Disassembly cited inline.
---
## 0. What the binary settles (resolves the doc contradiction)
`PAF/qcril_bringup_capture.md` ends with a REFRAME claiming "the Android modem self-onlines;
there is no QCRIL online handshake." The binary shows that is **half right**: QCRIL **does**
issue DMS Set Operating Mode, but only as the **last step of a gated state machine**
(`qcril_qmi_nas_boot_up_optimization_state_machine` @ **0xfe4730**). The thing pmOS skips is
not "an online command" — it is the **pre-conditioning QCRIL performs on the modem before it
asks it to go online**. That pre-conditioning is the choreography below.
The "self-online" observation on Android (op-mode already 0 at QCRIL attach) is because on a
*warm* / SSR path the modem was already conditioned in a prior boot and the state persisted;
the cold first-boot path runs the full state machine. pmOS only ever does the cold bare
`dms-set-operating-mode=online` with **none** of the pre-conditioning → DeviceNotReady(52).
---
## 1. The bring-up orchestrator — `qcril_qmi_nas_boot_up_optimization_state_machine` @ 0xfe4730
Disassembled call order (filtered to the load-bearing `bl`s):
```
fe4850 bl qcril_qmi_nas_get_bootup_power_optimization_state
fe4880 bl qcril_qmi_nas_decide_mode_pref_to_set_on_modem(&pref) ; 0xfe4c00
fe489c bl qmi_ril_nwreg_request_mode_pref(i, &pref) ; 0x10f6f98 <-- NAS mode-pref push
fe48cc bl qcril_qmi_nas_save_android_mode_pref(i)
fe48d8 bl qcril_qmi_nas_get_atel_ui_ready_req_came_from_cache() ; 0xfe4ec4
fe48f0 bl qcril_qmi_nas_set_atel_ui_status_to_modem(h) ; 0xfe5070 <-- DMS UI-ready push
fe48f8 bl qcril_qmi_nas_set_atel_ui_status_to_cache(h)
fe4904 bl qcril_qmi_nas_set_operating_mode_state(enum) ; 0xfe53c8 <-- DMS Set Operating Mode
fe490c bl qcril_qmi_nas_set_bootup_power_optimization_state(2)
```
So the ordered, **load-bearing** pre-online sequence the modem is conditioned with is:
1. **Decide + push the NAS mode preference (RAT set)** — tell the modem which radio
technologies to acquire — BEFORE asking it to power the RF.
2. **Push the ATEL "UI ready" status to the modem over DMS** — a Qualcomm OEM DMS message.
3. **Only then** issue DMS Set Operating Mode.
`qcril_qmi_nas_telephony_ready` @ 0x1015a50 carries the log
`poweron_opt: 'UI ready' NOT sent to modem` (string confirmed in binary) — i.e. the UI-ready
push is conditionally skipped when the modem is already online, which is exactly the
"self-online" warm path that misled the earlier reframe.
---
## 2. Step detail + QMI message IDs (from the immediates)
### Step A — NAS mode preference (LOAD-BEARING)
`qmi_ril_nwreg_request_mode_pref` @ **0x10f6f98** builds the mode/band/acq-order prefs
(`qcril_qmi_nas_get_mode_pref` @0x1da4890, `qcril_qmi_nas_get_gw_acq_order_pref` @0x1da4d50,
`qcril_qmi_nas_get_5g_mode_pref` @0x1da3940) and dispatches through the shared sender at
`qcril_qmi_nas_request_set_preferred_network_type+0x340` (0x10f6f68 region). That sender
issues **NAS Set System Selection Preference** (QMI NAS id **0x0033**, confirmed present in
`references/libqmi/data/qmi-service-nas.json` line 1437 "Set System Selection Preference").
→ **Replayable via `qmicli --nas-set-system-selection-preference`.**
### Step B — ATEL UI-ready push (LOAD-BEARING, **NOT replayable with stock tooling**)
`qcril_qmi_nas_set_atel_ui_status_to_modem` @ **0xfe5070**:
```
fe5124 mov w0, #0x4 ; qcril client enum 4 = DMS
fe5128 bl qcril_qmi_client_get_user_handle
fe512c mov w1, #0x64 ; <-- QMI message id 0x64 (100) on DMS
fe5134 mov w3, #0x2 ; req_len = 2 bytes (the UI-ready u8 + TLV hdr)
fe5140 mov w5, #0x8 ; resp_len = 8
fe5148 movk w6,#0x1,lsl#16 / 0x2110 ; sync timeout
fe514c bl qmi_client_send_msg_sync_with_shm
```
DMS message **0x0064 is a Qualcomm OEM extension** (string
`com.qualcomm.qti.qcril.nas.telephony_ui_ready`) and is **not defined in libqmi** (checked
`references/libqmi/data/qmi-service-dms.json` — 0x0064 absent; only 0x002E Set Operating Mode
etc.). There is **no qmicli flag** for it and **no `Qmi.MessageDms*` GI binding**. Sending it
requires a hand-built raw QMI PDU via `qmi_device_command_full`. See gap note in §4.
### Step C — DMS Set Operating Mode (the step pmOS already does)
`qcril_qmi_nas_set_operating_mode_state` @ **0xfe53c8**:
```
fe547c mov w0, #0x4 ; DMS client
fe5484 mov w1, #0x2e ; QMI DMS id 0x2E = Set Operating Mode (libqmi dms.json:498)
fe548c req @ x29-0x18, len 0x10
fe549c movk ... 0x12110 timeout
fe54a4 bl qmi_client_send_msg_sync_with_shm
```
The op-mode enum value comes in as the function argument (`dms_operating_mode_enum_v01`);
`online` = 0. → This is exactly `qmicli --dms-set-operating-mode=online`.
---
## 3. Surrounding init context (incidental vs load-bearing)
`qcril_qmi_nas_init` @ 0xfc7058 also runs, at NAS bring-up (de-mangled call trace):
- `qcril_qmi_nas_configure_sig_info2` — signal-strength delta config (incidental to RF arm)
- `qcril_qmi_util_enable_networking_indications(int,int,int)` — NAS indication-register
(sys-info / serving-system). NAS Indication Register = QMI NAS **0x0003**
(`qmi-service-nas.json:395 "Register Indications"`). Replayable, but **incidental** to RF
arming — it only controls which *unsolicited* indications the modem pushes back; it does not
gate op-mode. Useful so we can *observe* the modem reacting.
- `qcril_qmi_nas_register_for_5g_modem_vote_indication`, sig-config — incidental.
UIM path (separate module, runs from card-status-update, not the op-mode state machine):
- `qcril_qmi_uim_power_up` @ 0x11dc650 (MMGSDI card power-up) and
`qcril_qmi_uim_change_provisioning_session` @ 0x11e07e0. On Android these establish the
USIM session before NAS asks for RF. On pmOS the SIM already reads `ready` and primary-GW
is provisioned (per live results), so card power-up is **already satisfied** — not the gap.
`uim-change-provisioning-session` *timing out* on pmOS is a **symptom** of the same modem
not-ready state, not an independent cause (it answers reads, refuses state changes).
`qmi_ril_nwreg_post_oprt_online_action_handler` @ 0x104ddd8 runs **after** online succeeds
(enables more indications) — not part of the pre-online arm.
### Load-bearing-for-RF-init, in order (the replay target)
1. NAS Set System Selection Preference (mode/RAT pref) — **0x0033**, qmicli-able
2. DMS ATEL UI-ready push — **DMS 0x0064**, OEM, *not* qmicli-able
3. DMS Set Operating Mode = online — **0x002E**, qmicli-able
### Incidental (safe to include for observability, not expected to gate)
- NAS Register Indications (0x0003), sig-info config
- UIM card power-up / provisioning (already satisfied on pmOS)
---
## 4. Precise gap (what the binary requires that stock pmOS tooling can't send)
**DMS message 0x0064 ("telephony UI ready") is the one pre-online step QCRIL performs that
neither qmicli nor libqmi-GI exposes.** It is a 2-byte request on the DMS service. We can:
(a) replay steps 1 + 3 with qmicli now (cheap, may be sufficient if the mode-pref is the real
missing precondition), and
(b) if 1+3 still give 52, send 0x0064 as a raw QMI PDU via `Qmi.Device.command_full` (the TLV
layout is a single mandatory u8 = 1; we have the message id and req_len=2 from the binary,
which is `[type=0x01][len=0x01 0x00][val=0x01]` = 4 bytes payload after the QMI msg header
— note binary's "req_len 2" counts the c-struct, libqmi will frame the TLV).
The provisioner (`tools/modem-online-provisioner/`) implements (a) as the primary path and
documents (b) as the staged escalation, so we spend device boots in the right order.
**UPDATE 2026-07-01 — (b) is now built AND run on hardware. Result: real response, still 52.**
`device-google-blueline/dms-ui-ready.py` sends the raw 0x0064 PDU using libqmi's own untyped
raw-message primitives (`qmi_message_new` + `qmi_message_tlv_write_init/guint8/complete` +
`qmi_device_command_full`). **First attempt (Python/GI) failed immediately**: `Qmi.Message` is
NOT exposed via GObject-Introspection in this build — confirmed live (`AttributeError:
'gi.repository.Qmi' object has no attribute 'Message'`) and by enumerating the actual typelib
(only per-message typed classes exist, e.g. `MessageDmsGetOperatingModeOutput`; no generic
`Message`/raw-`Command` type). The doc-comment annotations on `qmi_message_new`/
`qmi_message_tlv_write_*` in the C headers do NOT mean GI exposes them — they don't here.
**Second attempt (C, direct against libqmi's real C API) worked and got a genuine protocol
response.** Cross-compiled a small standalone binary (`pmbootstrap chroot -b aarch64`,
`libqmi-dev` 1.39.0 — the openimsd fork, matching the phone's installed version) linking
directly against `libqmi-glib.so.5` already on the phone; pushed and ran it live:
```
dms-ui-ready: sending DMS 0x0064 (telephony UI ready=1)
dms-ui-ready: response: is_response=1 message_id=0x0064 length=20
dms-ui-ready: RESULT: FAILURE (result=1 error=58)
```
The modem answered `0x0064` as a real, understood message (not "unsupported message" / not a
transport reject) — this alone is new information: the OEM message ID and TLV framing are
correct. It returned a structured FAILURE with error code 58. Standard libqmi enum 58 is
`QMI_PROTOCOL_ERROR_WMS_ENCODING` (an SMS-service error) which cannot be its real meaning here —
this is a Qualcomm OEM message, so its error-code namespace is very likely private/vendor-defined
and just numerically reuses slot 58; not confidently decodable from public libqmi headers.
Retried `mmcli --enable` immediately after (on both the pre-existing and, after ModemManager's
own transport reprobe, a freshly re-created modem object): **`DeviceNotReady` (52), byte-identical
to every prior attempt all night, with or without this message.** Sending it also correlated with
a transient QMI transport hiccup (ModemManager's own connection self-recovered via reprobe within
~1s; no lasting harm, but note for next time: release the DMS client / close the device cleanly
before exit, which this quick test skipped).
**Verdict:** DMS 0x0064 is real, reachable, and rejects with a specific (undecoded) error — but
does not clear the 52 wall by itself. Either the TLV value/precondition isn't exactly right, or
this step genuinely isn't sufficient alone and step 1 (NAS mode-preference, `qmicli
--nas-set-system-selection-preference`, previously observed to fail with `DeviceUnsupported`
pre-RF) needs to succeed first, or the real gate is elsewhere entirely (EFS NV item, bootloader/
ABL verified-boot state — see `los-mss-translated.c`'s remaining two candidates). Kept wired
in the boot chain (best-effort, harmless) since it's now proven to do something real, just not
(yet, alone) the fix.
**UPDATE 2026-07-02 — full ordered trilogy executed on hardware. Candidate #2 CLOSED.**
Run conditions: ModemManager stopped, fresh mss restart, first commands into the new
firmware instance, QCRIL's exact order. Results:
1. NAS 0x0033 (mode-pref) → DeviceUnsupported(25), identical to every out-of-order
attempt. Order does not rescue it. But `--nas-get-system-selection-preference`
succeeds and shows the stored pref is ALREADY complete (all RATs, full band lists,
persisted from Android) — step 1's content is pre-satisfied; the push is refused
AND redundant.
2. DMS 0x0064 (UI-ready) → delivered, NoEffect(26), as always.
3. DMS 0x002E online → DeviceNotReady(52), byte-identical.
Same night context: rfs file serving fully fixed (ota_firewall/ruleset + modem_efs;
XPU-violation boot crashes eliminated), zero TQFTP rejects, clean crash-free modem
boot. The pre-online conditioning theory is now closed in ALL variants: the modem is
conditioned, fed, and healthy, answers every read, refuses every state change.
Remaining candidates: EFS NV item (#3), ABL/bootloader-passed state (#5), with the
TZ/SFS secure-storage identity path (test IMEI 990012001581294) as the concrete
mechanism consistent with both. Next experiment queued: QSEECOM listeners started
pre-modem at cold boot (registration proven; receive/resume relay still unexercised).
**UPDATE 2026-07-03 — DMS 0x0064 NOT_READY-then-READY handshake tested live. FALSIFIED.**
Real vendor QCRIL source (leaked SDM670/710 BSP,
`qcril-hal/modules/nas/src/qcril_qmi_nas.cpp`, boot-triggered-ONLINE branch of the
unsolicited operating-mode indication handler) shows the real sequence sends
`ui_status=FALSE` (NOT_READY) first, then later `TRUE` (READY) — our prior test
(2026-07-01) only ever sent READY standalone. Built a corrected C tool
(`tools/modem-online-provisioner/dms-ui-ready-sequence.c`, cross-compiled via
`pmbootstrap chroot -b aarch64` against a sysroot pulled live from the phone's own
`/usr/include/libqmi-glib` + `.so`, since neither `glib-dev` nor `libqmi-glib-dev`
are in the buildroot's own repo index) and ran the full corrected sequence live on
pmOS: baseline `offline` confirmed, `0x0064(0)` sent → `result=1 error=58`,
`0x0064(1)` sent 1s later → `result=1 error=58` (identical failure, order didn't
matter), final `dms-get-operating-mode` read → still `offline`. Both values fail
identically — this rules out "wrong value" or "wrong order" as the cause of error
58. Error 58 is very likely a hard precondition-not-met rejection independent of
what value is sent, not a sequencing bug. Do not re-attempt this specific
NOT_READY/READY ordering fix without new information; it's closed. Tool kept in
`tools/modem-online-provisioner/` for reference (working example of raw libqmi
C message construction against DMS OEM messages, useful if a future OEM message
needs the same treatment).

View file

@ -0,0 +1,262 @@
# PAF · QRTR tools reference — what we have, what each does, what's untested
Written 2026-07-01 because we'd been using `qrtr-lookup` all session without ever checking
whether it was the only tool available, or what its full capability actually is. It wasn't —
`qrtr-cfg` exists too and had never been touched. This file exists so that doesn't happen again.
## Package: `qrtr-1.2-r1` (Alpine/pmOS)
Ships exactly two binaries. Confirmed via `apk info -L qrtr`:
```
usr/bin/qrtr-cfg
usr/bin/qrtr-lookup
```
Both come from the upstream `qrtr-utils` project (Qualcomm's own AF_QIPCRTR userspace
tools). There is no `qrtr-ns` (nameserver) package installed separately — check whether one
is needed; if the kernel-side QRTR nameserver is absent, service registration/lookup could
be running in a degraded mode we haven't checked for.
## `qrtr-lookup` — service registry dump
**Usage:** `qrtr-lookup [<service> [<instance> [<filter>]]]`
Run with no arguments, dumps the full QRTR service registry: every registered
`{service, version, instance, node, port}` tuple currently known to the local nameserver,
with a decoded service name where the local database has one (`<unknown>` otherwise).
**Untested this session:** the `<service>` / `<instance>` / `<filter>` positional
arguments. We only ever ran it bare. Querying a **specific** service number directly
(e.g. `qrtr-lookup 24` for PDC) may return additional detail the bare dump doesn't show,
or may support a watch/blocking mode for a single service — not yet checked. Worth trying
before assuming the bare dump is the whole picture.
### Full service table observed this session (node 0 = modem edge, node 5/10 = other DSPs)
| Service ID | Name (as decoded by qrtr-lookup) | Notes |
|---|---|---|
| 66 | Service registry notification service | meta |
| 43 | Subsystem control service | SSR |
| 15 | Test service | — |
| 51 | CoreSight remote tracing service | debug/trace, multiple instances across DSPs |
| 21 | Modem embedded file system service | EFS — **never directly queried this session** |
| 24 | Thermal mitigation device service | — |
| 23 | Thermal sensors service | — |
| 22 | Time service | used by `blueline-timed.sh`/`modem-time-set.py` (base=2/ATS_USER only) |
| 36 | **Persistent device configuration service (PDC)** | the config/carrier-selection service — today's main thread |
| 49 | IPA control service | data-plane |
| 17 | Specific absorption rate service (SAR) | — |
| 34 | Coexistence service | — |
| 231 | Vendor-specific service | **never queried — unknown OEM extension, worth probing** |
| 55 | QMI-SLIM service | — |
| 9 | Voice service | — |
| 3 | Network Access Service (NAS) | RAT/mode-pref, serving-system |
| 12 | Phonebook Management service | — |
| 10 | Card Application Toolkit service (v2) | UIM/SIM toolkit |
| 47 | Data Port Mapper service | — |
| 11 | User Identity Module service (UIM) | SIM reads — this is what confirms ICCID/IMSI, no RF needed |
| 1 | Wireless Data Service (WDS) | — |
| 4 | Quality Of Service service | — |
| 26 | Wireless data administrative service | — |
| 7 | Authentication service | — |
| 8 | AT service | **never queried — a raw AT-command channel may exist here, unexplored** |
| 29 | Circuit switched videotelephony service | — |
| 71 | UIMHTTP service | — |
| 5 | Wireless Messaging Service (SMS) | — |
| 42 | Data system determination service | — |
| 2 | Device Management Service (DMS) | operating-mode, capabilities, IDs |
| 48 | QMI DFS service | — |
| 16 | Location service (~PDS v2) | GNSS |
| 69 | ATH10k WLAN firmware service | WiFi, separate from modem entirely |
| 77 | IMS QMI Priv service | — |
| 57 | WLPS service | — |
| 14 | Remote file system service | rmtfs — this is what actually serves modemst1/2 |
| 64 | Service registry locator service | meta |
| 770 | IMS data service | — |
| 4096 | TFTP | tqftpserv |
| 769 | SLIMbus control service | audio |
| 4098/4099 | `<unknown>` | **never decoded — no name in qrtr-lookup's local DB, worth checking libqmi's `qmi-service-*.json` files for these numeric IDs directly instead of relying on qrtr-lookup's built-in names** |
**Services we have NEVER directly queried this whole session, despite them being live and
registered:** Modem EFS service (21), Vendor-specific service (231), AT service (8),
CoreSight remote tracing (51). Any of these could carry signal about the RF-init decision
we haven't looked at. AT service (8) in particular is notable — a raw AT-command interface
bypassing QMI entirely is a completely different instrument than anything tried today.
## `qrtr-cfg` — node ID configuration
**Usage:** `qrtr-cfg <node-id>`
Attempts to set this machine's own local QRTR node ID via the `SIOCSIFADDR` ioctl on an
`AF_QIPCRTR` socket. **Confirmed non-functional on this kernel build**, tested live
2026-07-01: `qrtr-cfg 0``ERROR qrtr-cfg: failed to configure node id: Not supported`.
Root cause, confirmed by reading the kernel driver directly
(`references/linux-7.1.1-sdm845-CANONICAL/net/qrtr/af_qrtr.c`, ioctl handler ~line 1140+):
only `SIOCGIFADDR` (get local address) is implemented. `SIOCSIFADDR` (set) is not handled —
falls through to the unsupported/default case. The local node ID
(`qrtr_local_nid`, default `1`, `af_qrtr.c:102`) is fixed at this value on this kernel;
mainline's `qrtr` core doesn't currently expose a way to change it at runtime on this
build. Not a bug we introduced — this is upstream kernel behavior on this SDM845 mainline
tree. `qrtr-cfg` is effectively dead weight on this specific system; don't spend more time
on it unless the kernel driver changes.
## Update 2026-07-01 evening — services 8/21/231 confirmed LIVE, real RE progress
Built `tools/probe-qmi-service.py`-equivalent (raw AF_QIPCRTR socket, `NEW_LOOKUP`, then a
raw QMI message send — same mechanism as `modem-time-set.py`, no risky glink channel
creation needed since these are QMI services multiplexed over the existing IPCRTR
transport, not separate glink channels). Result: **all three respond**, not silent:
| Service | qrtr node/port | Result |
|---|---|---|
| AT service (8) | 0 / 58 | responded (rejected a CTL-service message sent to the wrong dispatch — see below, not a dead port) |
| Vendor-specific (231) | 0 / 43 | responded (same rejection pattern) |
| Modem EFS service (21) | 0 / 24 | responded (same rejection pattern) |
**Why the response was a rejection, and why that's not a dead end:** the probe sent a
`CTL` service message (`Get Client ID`, msg `0x0022`) directly to each service's own port.
That's the wrong message for their dispatch tables — `modem-time-set.py` never did a CID
handshake either, it sends the real functional message straight to the looked-up port.
Next probe needs each service's actual message ID, not a generic CTL probe.
**AT service (`QMI_SERVICE_AT` = `0x08`) — real progress via modem blob strings.**
`libqmi`'s own enum documents this service by name (`qmi-enums.h:50`) but implements zero
messages for it (no `qmi-service-at.json`, no `qmicli --at-*` flags) — same gap class as
Time (`TS`) before it got RE'd this session. Found real function names in `modem.mbn`'s
strings (`strings -n 6 modem.mbn`, file offset ~0x172b653+):
```
ds_qmi_at.c
qmi_ati_reset / qmi_ati_reset_client
qmi_ati_reg_at_cmd_fwd / qmi_ati_reg_at_cmd_fwd_ex
qmi_ati_fwd_at_cmd_cback / qmi_ati_fwd_at_cmd_ind
qmi_ati_send_at_repsonse (sic, real modem-firmware typo) / qmi_ati_send_at_urc
qmi_ati_process_cmd / qmi_ati_process_cmd_hdlr
qmi_ati_process_alloc_clid / qmi_ati_process_dealloc_clid
qmi_ati_initialize_client
qmi_ati_process_at_client_reg_status / qmi_ati_process_at_cmd_reg_status
qmi_ati_send_response
qmi_ati_alloc_clid_cb / qmi_ati_dealloc_clid_cb / qmi_ati_cmd_hdlr_cb
```
Confirms `ATI` = "AT Interface", a real, structured, table-dispatched QMI service — not
guesswork. Tried to find the exact numeric message IDs via:
- **Byte-scan for the string's own virtual address as a literal pointer** (file offset
0x172b653 → vaddr 0xc177b653, computed from the ELF program headers): zero hits. Hexagon
code doesn't appear to store bare absolute pointers to this table; PC-relative
addressing likely, not greppable without real disassembly.
- **`llvm-objdump -d` DOES support Hexagon** (`llc --version` lists `hexagon - Hexagon`;
confirmed disassembles `mba.mbn` correctly) — but `modem.mbn`/`mba.mbn` are fully
stripped (`readelf -S`: "There are no sections in this file", no symtab). Raw
disassembly without function boundaries or xrefs isn't tractable by hand at 67MB scale
without a real Hexagon-aware decompiler (Ghidra + Hexagon processor module, or IDA) —
don't have one in this environment.
- **Public docs**: none found. `QMI_SERVICE_AT`'s real message IDs aren't published
anywhere searchable. Checked `quic/qmi-framework` (Qualcomm's own open-source repo) —
generic transport only, no service-specific IDL.
- **`atfwd_daemon`** (the usual Android userspace AT-forwarding daemon that would exercise
this service): **not present anywhere in `android-reference`'s extracted vendor stack**.
Pixel 3 doesn't ship it — dead end specific to this device, not a search failure.
- **Closest real reference found**: `QMI_SERVICE_ATR` (`0x?`, "Telit AT Relay Service",
`qmi-service-atr.json`) IS fully implemented in libqmi (different vendor extension, but
same architectural family). Protocol: `Send` (msg `0x0000`, TLV `0x01` = AT command
string, size-prefixed, max 1024B) → Operation Result; `Received` (indication `0x0001`,
same string TLV) delivers the response async. This is the best structural template for
what the real `AT` service's wire format probably looks like, even though it's a
different service ID — worth trying as a first guess against service 8 directly
(`msg 0x0000`, single string TLV `0x01`) next time the phone's available, though
`qmi_ati_reg_at_cmd_fwd` existing as a SEPARATE step from send/receive suggests the real
`AT` service may require an explicit registration message first, unlike ATR's simpler
always-on relay.
**Modem EFS service (21) and Vendor-specific (231)**: confirmed alive, zero further
research done — same "find the real message ID" problem, not yet attempted.
## What we should try next with these tools, specifically
1. Against AT service (8): try the ATR-shaped guess first (`msg 0x0000`, TLV `0x01` =
AT command string like `"AT\r\n"`) directly at node 0 port 58. Cheap, safe (worst case
a clean rejection like the CTL probe, not a crash — this isn't a glink channel-open,
it's an ordinary QMI message on the existing IPCRTR transport).
2. If that's rejected, try `qmi_ati_reg_at_cmd_fwd`-shaped registration first (a
`Register`-style message, unknown ID — try 0x0001-0x0005 range as a first sweep) before
the send.
3. **Speculative but cheap dispatch-table-order guess**: the 5 strings carrying the
`(qmi_svc_hdlr_ftype)` cast prefix in `modem.mbn` appear in this exact sequence:
`qmi_ati_reset`, `qmi_ati_reg_at_cmd_fwd`, `qmi_ati_send_at_repsonse`,
`qmi_ati_send_at_urc`, `qmi_ati_reg_at_cmd_fwd_ex`. If the string table mirrors the
dispatch table's own ordering (common but not guaranteed), these would be 5
consecutive message IDs. Worth a sweep starting at a plausible custom-message base
(`0x0020`): try `0x0020`=Reset, `0x0021`=RegAtCmdFwd, `0x0022`=SendAtResponse,
`0x0023`=SendAtUrc, `0x0024`=RegAtCmdFwdEx against service 8, watching for anything
other than a uniform rejection.
4. `qrtr-lookup 21`/`231` — still completely unexplored beyond confirming they're alive.
No `qmi_mfs_`/`qmi_efs_`-style strings found anywhere in `modem.mbn` (searched both
narrow and broad patterns) — either stripped entirely, or MFS's handler code lives in
a different firmware image than the main MPSS blob. Vendor-specific (231) has zero
name leads at all; would need blind message-ID sweeping, no better starting point.
5. Try `qrtr-lookup <n>` with explicit service numbers to see if per-service querying
surfaces anything the bare dump suppresses (untested all session).
## Update 2026-07-02 — services 8 / 21 / 231 all mapped live. Frontier CLOSED (negative).
Ran `tools/at-service-probe.py` and the new `tools/qmi-service-map.py` (empty-payload
dispatch-table sweep, reads the QMI result-TLV error pattern: 71/94 = no handler, 1/3/17/etc
= real handler that rejected the args). All three "never-queried, worth-probing" services are
now characterized on the live modem. None exposes a lever on RF-init or narrates the 52 wall.
- **AT service (8), node 0 port 58 — AT-command FORWARDING, wrong direction. DEAD END.**
Dispatch table: `0x0000`=RESET (returns OK with or without a TLV — the "accepted an AT
string" earlier was a false positive; empty payload also returns OK). `0x0020`/`0x0022`/
`0x0024` = real handlers (`qmi_ati_reg_at_cmd_fwd` / `send_at_repsonse` / `reg_at_cmd_fwd_ex`
— INTERNAL(3) on empty, MALFORMED(1) on a wrong TLV). `0x0021`/`0x0023` = indications
(modem→AP). This is `ds_qmi_at.c`: apps *register* to receive AT commands the modem
forwards to them (Android's `atfwd_daemon` role). There is no AT-*execution* path here to
command the modem's own RF. Four `0x0020` registration-TLV shapes all returned MALFORMED;
nailing the shape only registers us as a forwarding target — it can't move 52. Not pursued
further.
- **Modem EFS service (21), node 0 port 24 — file-ops gated, low-yield. NOT pursued.**
Most of `0x0000``0x0030` return error 57 (a service-state/"not open" gate). Real handler
cluster at `0x001e` (OK on empty — a no-arg open/action), `0x001f`/`0x0020`/`0x0021`
(MISSING_ARG — the read/write/list file ops). Cracking the file-path TLV shape is possible
but pointless for 52: per `diag_capture.md`, EFS content is byte-identical to Android
(shared unslotted partition), so any static NV/file read reads the same bytes both OSes.
Empty-payload probes only were sent to 21 (an empty request can't carry a write).
- **Vendor-specific service (231), node 0 port 43 — modem telemetry / event-log. DEAD END.**
This is a stats/bugreport service, self-describing: msg `0x000f` = "Report all modem stats",
returns result OK with TLV `0x16` = **16 bytes, all zero** (the rest of its fixed 4136-byte
frame is unused). Msg `0x0012` is an event-log *indication* carrying timestamped lines
(`"<ts> Bugreport triggered"` — an echo of our own trigger). Fired across a live
`mmcli --enable` (the 52 moment) and re-fired immediately after a failed enable: the stat
block stays all-zero, no RF-init narration, no error counter moves. It is NOT a DIAG/F3
substitute — the counters are empty and the bugreport content is not delivered through this
channel (fetched/stored elsewhere; EFS-backed at best, which loops back to 21's null diff).
Many `0x0000``0x0017` handlers want args (MISSING_ARG) but blind TLV-shape guessing here is
the same low-yield trap as AT(8)'s registration TLV; not worth it without a name/IDL lead.
**Net:** the "probe the other live services" frontier is exhausted with a negative result. No
standard, AT, EFS, or vendor-telemetry QMI service on this modem exposes the RF-init lever or
its reason. Consistent with `modem-online-provisioner/RESULTS.md`: the gate is the modem
firmware's own RF-init decision, and the only instrument that could narrate it (DIAG F3) is
walled by production fusing ("MBA booted without debug policy") on both OSes — see
`HANDOFF.md`'s DIAG-over-glink note.
## `tools/pdc-pending-test.py` — ready to run, not yet executed live
Built and syntax-checked (not yet run — needs phone access) to correctly test whether the
`Pending` PDC config status (found live 2026-07-01, see `HANDOFF.md`) ever resolves to
`Active` on its own, or needs a forced modem restart. Fixes two real mistakes from earlier
manual testing, both confirmed by reading the actual working scripts rather than guessing:
- `--pdc-monitor-refresh` watches the **wrong indication** (`Refresh`, `0x2F`) — the one
that actually reports Active/Pending transitions is **`Config Change`** (`0x21`),
delivered only to a client that called `Register` with reporting+refresh enabled
*before* touching config state. No test this session had done that.
- Device must be opened via `Qrtr.Bus → peek_node → Qmi.Device.new_from_node` with
`Qmi.DeviceOpenFlags.EXPECT_INDICATIONS` (verified against `qcril-autoselect.py`'s
`open_device()`) — a plain `Gio.File` open (my first draft) would have silently never
delivered any indication at all, making a "no signal" result meaningless. Caught before
running, not after.

View file

@ -0,0 +1,259 @@
# PAF · Secure-load / TrustZone frontier
> **🔴 SUPERSEDED 2026-06-29.** Secure-load is **RESOLVED** for the modem — the MSS loads clean
> (MBA boots, MPSS loads, `remoteproc running`, **zero 22 on the modem path**) via DELTA-1
> (`c2923cb49` MPSS assign-ordering). This doc is kept as the **historical 22 / fastrpc_mem
> investigation record**. The 22 that still floods the logs is the **SLPI's** SDSP heap, not the
> modem's — see `slpi.md`. The current frontier is the firmware-internal OFFLINE/52 park, not
> secure-load. (Body below is the 2026-06-26 investigation as-written.)
## 2026-06-26 (OPUS session) — mem-protect SKIP broke the boot on hardware; reverted. LOS does mem-protect differently, not skip it.
**Platform is now clean and reproducible (this part is solid, keep it):**
- ONE canonical branch **`blueline`** (was split across `mobian-sdm845-7.1` + `blueline-slpi-build`,
neither complete — consolidated). It = Mobian `mobian-sdm845-7.1` base (mainline 7.1.1 tag +
full 179-patch sdm845 device series: camera/imx sensors, q6voice, haptics, fuel-gauge, panels)
+ our 6 fixes. Other branches renamed `reference/mobian-7.1.1-base`, `archive/superseded-*`.
- Guard against drift: **`tools/verify-fixes.sh`** (13 checks, asserts SOURCE CONTENT not commit
messages; covers our 6 fixes AND the inherited Mobian migrations). Run before every build.
- Build: `pmbootstrap build --src=references/linux-7.1.1-sdm845-CANONICAL linux-postmarketos-qcom-sdm845`
(host `make` is NOT the path — build.md:7). Deploy: on-device `apk add --allow-untrusted`
`boot-deploy` auto-flashes `boot_a`. `uname -r = 7.1.1-sdm845`. Rollback image:
`build-output/boot_a-rollback-20260626-1605.img.gz`.
- **Found + fixed the silent killer behind every prior "flash and test":** the phone's kernel and
its `/lib/modules` were ABI-mismatched — `qcom_q6v5_mss` gave `Exec format error`, the modem
driver never loaded, so the modem was dead regardless of any fix. Matched build → driver loads,
modem remoteproc `4080000.remoteproc` now registers.
**The mem-protect skip (`need_mem_protection = false` for sdm845) is REFUTED — likely roll back:**
- It removed the `-22` from the modem path as designed — but the modem now fails EARLIER:
`Booting .../mba.mbn → PBL returned unexpected status 0xEF110000 → can't start rproc: -22`.
It never reaches "MBA booted".
- The r2 baseline (below) had the modem boot MBA then XPU-crash-loop at 52. So the skip REGRESSED
the boot. `qcom_scm_assign_mem` (`q6v5_xfer_mem_ownership`, qcom_q6v5_mss.c:463/489) transfers the
MBA region HLOS→MSS_MSA so the modem PBL can read MBA; skipping starves PBL. **The assign is
load-bearing. The real gate is WHY TrustZone returns -22 (EINVAL), not the call's existence.**
**The LOS claim in `HANDOFF_20260626_OPUS.md` was unverified inference — treat as RETRACTED:**
- It blended mainline (`need_mem_protection`) with downstream (`subsys_vmid` / `qcom,mem-protect-id`)
concepts into one story.
- LOS Pixel 3 runs the **downstream msm-google 4.9 kernel**; its modem loader is `subsys-pil-msa`,
NOT mainline `qcom_q6v5_mss.c`. We have NOT pulled that source (Tier 1 still open) — so what LOS
does was never read off real code.
- Hardware contradicts the inference: if LOS did no mem-setup and MBA self-unlocked XPU, the skip
would have worked. It made things worse.
**PRINCIPLE (Casey, 2026-06-26): LineageOS is the ONLY known-working modem reference on this device.
Other sdm845 devices (op6 / enchilada / beryllium / shift) are IRRELEVANT to the secure-load / XPU
question — do not cite them as modem-bringup evidence. The downstream Pixel-3 stack is the authority.**
**Current phone state:** booted on `blueline` 7.1.1 (the skip build). Modem PBL-fails every boot;
SLPI crash-loops ~10s (the `-22` spam every 10s is the SDSP/SLPI hyp-assign, NOT the modem).
**OPTIONS — left for Casey to choose (not picking one):**
1. **Revert the skip** → restore the MBA-boots-then-XPU-52 baseline on the clean 7.1.1 platform,
then work secure-load Tier 0/1 from a known state.
2. **Pull `msm-google` downstream kernel** → diff `subsys-pil-msa` mem-setup + hyp-assign vs
mainline. Answers "what does LOS actually do" with real code (Tier 1).
3. **Downstream-kernel hybrid** (Droidian / Halium pattern) — this doc's honest realistic
12 week path to working telephony.
4. **Chase the TZ `-22` directly** — why does mainline's `qcom_scm_assign_mem` get EINVAL on
blueline (VMID / mem-protect-id / TZ expectation)? Open-ended.
---
## 2026-06-26 — TRIGGER RESOLVED: this doc is the plan. Hard secure-memory evidence captured.
The trigger condition below ("still 52, modem boots then refuses RF → the gate is the
secure-load/TZ layer → this doc is the plan") is now MET, with a concrete error string instead of
inference. On the rebuilt r2 LLVM kernel (slot A, live ssh), `dmesg` shows the modem **crash-looping
on a hardware secure-memory protection fault**:
```
qcom-q6v5-mss 4080000.remoteproc: fatal error: modem_xpu_int.c:456:[modem_sec_xpu_I]XPU violation
(also crash #1: "SFR Init: wdog or kernel error suspected")
```
and EVERY modem + slpi restart logs the shared TZ rejection:
```
qcom_scm firmware:scm: Assign memory protection call failed -22 (qcom_scm_assign_mem → -EINVAL)
SDSP fastrpc hyp-assign rejected by TZ (-22)
```
**XPU = eXtreme Protection Unit**, the hw memory guard TZ programs. `modem_sec_xpu XPU violation` =
the modem firmware touched a region the XPU forbids → its secure memory regions aren't assigned
right for mainline → crash → q6v5 recovers → repeat → never RF-inits → **52**. The fastrpc/SLPI
`assign -22` is the same mechanism (TZ refusing mainline's hyp-assign); fastrpc tolerates it, the
modem can't. **The gate is concretely the secure-memory-assign / XPU region setup — pull the
`assign_mem -22` thread.** (See modem.md TOP LEAD #11, slpi.md.)
CAVEAT (don't overclaim): this XPU crash-loop was captured on **7.1.1 r2**. The 6.16.7 / 7.1rc1
captures recorded the modem "up + QMI registers" (lead #10) — QMI still answers here too, in the
up-windows between crashes, so a single-sample `running` looked fine before. OPEN: re-check whether
6.16.7 *also* XPU-crash-loops (i.e. always-present, just not noticed) or whether 7.1.1 worsened it —
diff `journalctl -p err` across both kernels before assuming XPU is identical on the older tree.
Original status (pre-2026-06-26): **alternative, not primary.** The primary lever was Opus's
reserved-memory geometry fix (`modem_dt_rf_analysis_20260622.md` candidate #1: revert the
`sdm845-google-common.dtsi` override so MPSS = `0x7800000` / MBA = `0x96500000`, matching both
working devices). That swing did not clear the 52; the secure-load/TZ layer (this doc) is now primary.
It is written assuming the reader has `modem.md`, `modem_dt_rf_analysis_20260622.md`,
`tools/modem-online-provisioner/RESULTS.md`, and `android-capture-20260622/EFS_FINDINGS.md` in head.
## Trigger condition (the decision point)
Flash the reserved-memory DT fix to `boot_a`, cold-boot pmOS, one `--dms-set-operating-mode=online`:
- **online works** → geometry was the gate. Stay mainline; push the fix upstream (sdm845-mainline /
pmaports), bisect the `google-common` override. **This doc is not needed.**
- **modem fails to load** (`remoteproc Boot failed`) → the firmware was signed for the old geometry;
the override existed for a reason. Revert; geometry is load-bearing-correct as-is. Come here.
- **still 52 (modem boots clean, refuses RF)** → the gate is **not** the DT geometry. It is in the
secure-load / TrustZone / subsystem-coordination layer. **This doc is the plan.**
## The gate, precisely (today's hard proof — do not re-derive)
Source: `tools/modem-online-provisioner/RESULTS.md` (raw QMI over `AF_QIPCRTR`, 2026-06-22).
- Modem is in **op-mode 3 = OFFLINE** (deepest non-persistent off state). Not LPM, not transient.
- **Accepts** every read, non-RF writes (DMS/NAS indication registration), and **DMS Set Operating
Mode = RESET (0x002E mode 4) → OK**.
- **Refuses every RF-requiring transition**: online → **52 DeviceNotReady**; low_power → **60
InvalidTransition**; NAS Set System Selection Preference (0x0033) → **25 DeviceUnsupported**; DMS
ATEL UI-ready (0x0064) → **58** (arg-rejected, message id confirmed correct).
- After the accepted **RESET**, firmware re-initialises **straight back to OFFLINE** and refuses
again, with **zero** sys-info/serving-system indications in an 8 s listen. The firmware is inert
on RF by decision, not attempting and failing.
**Conclusion (raw-level, not inference):** no AP-side QMI message or ordering unlocks RF. The
choreography-replay path (`qcril_online_sequence.md`) is closed: the modem rejects the pre-online
writes *themselves* before we reach the online flip. QCRIL only succeeds on Android because the
firmware has **already self-completed RF-init** there; on pmOS it never does.
## Why this points at secure-load / TZ (the RESET argument)
A QMI RESET re-initialises the modem firmware **without re-doing the cold-boot setup** — PIL image
auth, the TZ/SCM state, the bootloader (ABL/XBL) modem init, and sibling-subsystem coordination are
all established **once at cold boot** and persist through a QMI reset. The RESET reproducing OFFLINE
tells us the gate lives in that **persistent cold-boot environment**, not in a one-shot race or a
userspace omission. That environment is exactly four things, in suspect order:
1. **DT reserved-memory geometry** — Opus's test (may already settle it).
2. **Sibling subsystem / Protection-Domain coordination** — the modem RF-init can gate on a PD
registered over glink/pd-mapper by ADSP/CDSP/**SLPI**. pmOS DT has **no SLPI region at all**
(absent in the `modem_dt_rf_analysis` table; Android + enchilada both carry `slpi@0x96700000`).
Never confirmed up on pmOS. Mainline-fixable if so — *not* TZ.
3. **PIL / SCM secondary auth** — image auth clearly succeeded (modem boots), but a non-fatal
secondary secure step (a hash-table segment, a secure-regulator vote, a TUST handshake) may not
have completed, leaving the firmware in a degraded RF-locked mode.
4. **Bootloader / verified-boot state** — pmOS boots a hand-built `--header_version 0` image with a
custom cmdline. If ABL sees an unverified/inconsistent chain, it can skip modem secure-env init
that stock Android gets.
## Ranked investigation — cheapest discriminator first
### Tier 0 — free, same boot (run the instant reserved-memory fails, before any build)
1. **Mine dmesg for the secure/PD layer.** Image auth succeeded, so look for *non-fatal* warnings:
`dmesg | grep -iE 'pas|scm|auth|hash|mba|qcom_q6v5|remoteproc|glink|pdr|pil'`.
A non-zero `qcom_scm_pas_*` return, a PDR restart-notification timeout, or a glink intent-failure
is a citable pointer into #2/#3.
2. **Confirm sibling DSPs are up and their PDs registered.**
`for r in /sys/class/remoteproc/remoteproc*; do printf '%s: %s %s\n' "$r" "$(cat $r/name 2>/dev/null)" "$(cat $r/state 2>/dev/null)"; done`
and `qrtr-lookup | grep -iE 'adsp|slpi|cdsp|sns|domain'`. If SLPI/ADSP is `offline`/absent and
the modem waits on its PD, **that is the gate and it is mainline-fixable** (firmware path +
remoteproc node + a `pd-mapper` entry), not TZ. This is the highest-value cheap check — the whole
elimination tracked `remoteproc2` (modem) closely and never verified the siblings.
3. **Timing/missed-response.** `strace -f -p $(pidof pd-mapper)` and rmtfs during a QMI RESET. If
the modem's RF-init window closes before pd-mapper/rmtfs answer a request, you'll see the missed
response. (Likely already excluded — services are confirmed up post-boot — but cheap to confirm.)
4. **LDO13 rail state — the one rail NOT on the eliminated list.** Unlike the refuted #9 LDO set
(ldo4/6/8/9/10/11/15/16/18/23/27, which were *absent* from pmOS), `ldo13` IS defined in
`dt-work/blueline-pmos.dts:5466`, and on SDM845 LDO13 feeds RFFE (per OpenStick prior art — the
`l13: voltage operation not allowed` line is benign on MSM8917 but not automatically so here).
Static dumps show a state delta: pmOS `ldo13 … 0 0 0 …` vs Android `pm8998_l13 … 0 1 0 …`, and
there is **no** `l13`/`voltage operation not allowed` line in the captured pmOS dmesg. **Live
check at the moment of `online`:**
`for r in /sys/class/regulator/regulator.*/; do n=$(cat $r/name 2>/dev/null); [ "$n" = ldo13 ] && echo "$n state=$(cat $r/state) @ $(cat $r/microvolts)uV"; done`
— is it `on` @ 1.8 V while the modem is booted-but-refusing-RF? Then retry `online` and watch
whether L13 flips. Likely falls to the same "modem votes RF via RPMh, not the AP framework"
refutation as #9 — but it's the one rail never explicitly probed, so confirm live before closing.
Plus `dmesg | grep -iE 'l13|voltage operation not allowed'` on the same boot.
### Tier 1 — diff the downstream kernel (the real working reference for secure-load)
Enchilada proves the mainline *stack* works on SDM845; it does **not** prove the secure-load code
paths work on blueline's exact modem. The authority for those is **Google's downstream Pixel-3
kernel** (`msm-google`, 4.4-era — confirm the exact branch against `boot_a`'s kernel version on
`android.googlesource.com/kernel/msm-google` before cloning). Pull it and diff, against mainline,
exactly three things:
- `drivers/remoteproc/qcom_q6v5_mss.c` — the PAS auth + mba mem-setup + reset sequence.
- `drivers/soc/qcom/{scm,pdr_interface,smem}` — the secure-channel and PD-restart path.
- modem `reserved-memory` + `qcom,smem-states` + `power-domains` in the DT.
The reserved-memory hit is the first output of this method; the SCM/auth/PDR delta is the next.
This is the strongest "honest swing" if reserved-memory alone fails — methodical, sourced, no RE
of opaque blobs. **Artifact we have but haven't used this way:** `android-reference/vendor-stack-20260621/`
(vendor RIL + MBN tree). **Artifact we still need:** the downstream kernel source tree.
### Tier 2 — instrument the firmware's own RF-init (the only inside view)
Every capture so far is userspace (QCRIL/radio logs). Nobody has captured the **MPSS firmware's own
RF-init trace** — the ordered set of things it reads and waits for. On rooted Android (slot A),
QCSuper continuous diag over **`/dev/diag`** (local — confirmed working; USB-diag is HAL-locked on
this production Pixel, per `pmos_fix_plan.md`) captures the F3/log stream during a cold boot. An
open F3 decoder (linux-msm / sdm845-mainline ecosystem; QCSuper has partial decode) turns it into
the firmware's RF-init step list. Diff which of those steps can't complete on pmOS. This is the one
instrument that can name the gate from inside the firmware; everything on pmOS only observes the
refusal. **Read-only capture — do not write EFS.**
### Tier 3 — TZ / SCM RE (the hard pole, deprioritize)
If Tiers 02 point at a specific SCM call or secure state: disassemble the QSEE trustlets or diff
XBL/ABL's modem init. Long, uncertain, opaque. Keep as the escalation, not the plan.
## The strategic fork (be honest about the 12 week telephony goal)
Mainline-native secure-load RE (Tier 23) is realistically **not** a 12 week path to working
telephony/SMS/data — it's open-ended research. If Tier 0 + reserved-memory don't clear 52, the
fastest route to "telephony on Linux like I have on rooted Android" is to use the stack that already
works on this silicon:
- **Downstream-kernel hybrid (Droidian / Halium / pmOS-downstream pattern):** boot the downstream
`msm-google` kernel under a Linux userspace, where the modem firmware gets the exact cold-boot
environment it expects so RF-init completes, then drive it with the vendor `qcrild` already pulled
in `android-reference/`. Trades mainline purity for a known-good outcome. This is the realistic
12 week path.
- **WRONG TURN — do not burn time on this:** "run vendor QCRIL on the *mainline* kernel via
libhybris" will **not** fix it. QCRIL does not trigger RF-init (the firmware self-arms on Android;
QCRIL only queries/confirms — proven by today's raw results). The gate is the cold-boot
environment, so only changing that environment (DT → subsystems → TZ → bootloader) **or** booting
the downstream kernel helps. Vendor-RIL-on-mainline addresses a different (already-solved) layer.
Net: **reserved-memory fix + Tier 0 (free) → if clear, stay mainline; if not, decide
mainline-secure-load-research vs downstream-kernel-hybrid on evidence, not on momentum.**
## Artifacts
- **Have:** `android-reference/vendor-stack-20260621/` (libril-qc-hal-qmi.so + 174-file MBN tree +
selection DB); `tools/modem-online-provisioner/{qrtr_raw.py,qrtr_seq.py}` (send any QMI PDU over
AF_QIPCRTR — validated against DMS 0x002D); QCSuper efs-shell + diag over `/dev/diag` on rooted
slot A; `slotb-backup-20260621/pmos-7.1-boot.img` recovery; 6.16.7 known-good kernel; EFS clones.
- **Need (not yet pulled):** downstream `msm-google` kernel source (Tier 1 ground truth); an open
Hexagon F3/diag decoder (Tier 2); possibly a devcoredump/minidump parser if SSR ever yields one
(today it did not — modem doesn't crash, it deliberates).
## Guardrails (secure-load-specific)
- pmOS-side secure-load investigation is **read-only**: `dmesg`, `/sys/class/remoteproc/*`,
`/sys/kernel/debug/{remoteproc,msm_subsys}/*`, `qrtr-lookup`. Safe.
- diag/F3 capture is on **rooted slot A `/dev/diag`** — read-only capture. Never write EFS or NV
from these probes (shared unslotted `modemst1/2` — a write is seen by both slots).
- **Never flash/patch TZ, trustlets, XBL, or ABL** — brick territory. `references/edl-pixel` exists
as last-resort recovery but **assume no EDL safety net**; behave as if a bad flash is permanent.
- Only ever touch `boot_a` / pmOS. Never slot B, `system_a`, the shared EFS, or userdata.
- Recovery image: `slotb-backup-20260621/pmos-7.1-boot.img`. Document every slot switch + flash.
- One `online` attempt per boot (a failed attempt poisons the modem until a power cycle).
- Standard hard constraints apply: pmOS is Alpine/ash (no `[[ ]]`/arrays); `rmtfs -P -s` (never
`-r` alone); never add `pmic@4`/`pmic@5` SPMI nodes; sudo pw `147147`.
## On link-in
Not yet referenced from `START-HERE.md` or `PAF/README.md` (left for the Opus handoff to wire, to
avoid colliding with in-flight edits). If reserved-memory fails, point START-HERE's "actual
frontier" at this doc and demote the reserved-memory lever to the trigger-condition note above.

View file

@ -0,0 +1,146 @@
# PAF Tier 1: LOS `pil-q6v5-mss` (4.9.337) vs Mainline `qcom_q6v5_mss` (7.1.1) — Complete Diff
**Date:** 2026-06-27
**Source LOS:** `references/los-kernel-blueline-4.9/drivers/soc/qcom/{pil-msa.c, peripheral-loader.c, pil-q6v5-mss.c, secure_buffer.c}`
**Source Mainline:** `Downloads/mobian-sdm845-7.1/drivers/remoteproc/qcom_q6v5_mss.c` + `drivers/firmware/qcom/qcom_scm.c`
**Live Android capture:** 4.9.337 cold boot, `89JX0AAJP`, 2026-06-27
## Live Android ground truth (captured this session)
```
pil-q6v5-mss 4080000.qcom,mss: modem: loading from 0x8e000000 to 0x97800000 (MPSS 152MB)
pil-q6v5-mss 4080000.qcom,mss: Loading MBA and DP from 0x98500000 to 0x98600000 (MBA 1MB)
pil-q6v5-mss 4080000.qcom,mss: MBA boot done
pil-q6v5-mss 4080000.qcom,mss: modem: Brought out of reset
```
Zero `hyp_assign` errors. Zero `xpu`. Zero `-22`. The modem boots clean on Android.
Running Android reserved-memory (from `/sys/firmware/devicetree/base/reserved-memory/`):
| Region | Address | Size |
|--------|---------|------|
| modem (MPSS) | `0x8e000000` | `0x9800000` (152MB) |
| MBA | `0x98500000` | `0x200000` (2MB) |
| SLPI | `0x98700000` | `0x1400000` (20MB) |
| video | `0x97800000` | `0x500000` (5MB) |
| CDSP | `0x97d00000` | `0x800000` (8MB) |
These match the mainline `sdm845-google-common.dtsi` override exactly. TZ accepts this layout.
---
## Area 1: Driver boot sequence — MBA load + MPSS load
### MBA load
| Step | LOS (`pil_mss_reset_load_mba`) | Mainline (`q6v5_mba_load`) |
|------|------|---------|
| 1 | `pil_notify_aop("on")` | `q6v5_load_state_toggle(true)` via QMP — **same message** |
| 2 | Enable proxy clocks/regulators | Enable proxy clocks/regulators + power-domains |
| 3 | `dma_alloc_attrs(mba_mem_dev_fixed, ...)` — alloc from reserved `pil_mba_mem` at `0x98500000` | `memremap(mba_phys)` — directly maps reserved `mba_region` at `0x98500000` |
| 4 | `memcpy` MBA fw into buffer | `memcpy` MBA fw into mapped region |
| 5 | — | **`q6v5_xfer_mem_ownership(&mpss_perm, false, true, mpss_phys, mpss_size)` — HLOS->MSS for 152MB MPSS** |
| 6 | `pil_assign_mem_to_subsys(mba_dp_phys, mba_dp_size)` — HLOS->MSS for MBA only | `q6v5_xfer_mem_ownership(&mba_perm, false, true, mba_phys, mba_size)` — HLOS->MSS for MBA |
| 7 | Trigger boot FSM, wait PBL + MBA `XPU_UNLOCKED` | Trigger boot FSM, wait PBL + MBA `XPU_UNLOCKED` |
**DELTA 1 (step 5): Mainline pre-assigns the ENTIRE 152MB MPSS region to MSS before MBA boots. LOS does not touch MPSS until after MBA boots and segment loading begins.** This is a 152MB hyp-assign call that LOS never makes at this stage.
### MPSS load (segment loading + final handoff)
| Step | LOS (`pil_boot` in `peripheral-loader.c`) | Mainline (`q6v5_mpss_load`) |
|------|------|---------|
| 1 | `init_image` (MBA authenticates metadata) | `q6v5_mpss_init_image` (MBA authenticates metadata) |
| 2 | `mem_setup` (MBA sets up MPSS memory) | `qcom_scm_pas_mem_setup`**SKIPPED for SDM845** (`need_pas_mem_setup=false`) |
| 3 | SSR: `pil_assign_mem_to_linux(region)` — reclaim from MSS back to HLOS | SSR: `q6v5_xfer_mem_ownership(true, false)` — reclaim |
| 4 | `pil_assign_mem_to_subsys_and_linux(region)` — src={HLOS}, dest={HLOS+MSS}, both RW | `q6v5_xfer_mem_ownership(true, true)` — shared HLOS+MSS |
| 5 | Load segments | Load segments |
| 6 | `pil_reclaim_mem(region, subsys_vmid)` — src={HLOS,MSS}, dest={MSS only} | `q6v5_xfer_mem_ownership(false, true)` — MSS only |
| 7 | `auth_and_reset` | Wait `RMB_MBA_AUTH_COMPLETE` |
**DELTA 2 (step 3-4): Cold boot ordering difference.** On first boot (non-SSR), LOS skips step 3 (comment: "linux already owns it") and goes straight to step 4 (shared). Mainline at step 3 does `q6v5_xfer_mem_ownership(&mpss_perm, true, false, ...)` — but `mpss_perm` was already set to `BIT(MSS_MSA)` by the pre-assign in DELTA 1. So mainline's step 3 is MSS->HLOS (reclaim), then step 4 is HLOS->{HLOS+MSS} (share). LOS goes straight from HLOS->{HLOS+MSS}. The net result should be the same IF the pre-assign succeeded, but if DELTA 1 failed with -22, `mpss_perm` is still `BIT(HLOS)` and step 3 would be a no-op (already HLOS-owned), then step 4 works. Except — the pre-assign failure should have already aborted `q6v5_mba_load`.
### Metadata authentication
| Step | LOS (`pil_msa_mss_reset_mba_load_auth_mdt`) | Mainline (`q6v5_mpss_init_image`) |
|------|------|---------|
| buffer | `dma_alloc_attrs` — dynamic | From `mdata_mem` reserved region if present, else `dma_alloc_attrs` |
| assign | `pil_assign_mem_to_subsys(mdata, size)` — HLOS->MSS | `q6v5_xfer_mem_ownership(false, true, phys, size)` — HLOS->MSS |
| auth | Write to RMB, wait `META_DATA_AUTH_SUCCESS` | Write to RMB, wait `META_DATA_AUTH_SUCCESS` |
| reclaim | `pil_assign_mem_to_linux(mdata)` — MSS->HLOS | `q6v5_xfer_mem_ownership(true, false)` — MSS->HLOS |
**DELTA 3: Mainline has a dedicated `mdata_mem` reserved region (`alloc-ranges 0xa0000000`, 16KB, `no-map`). LOS uses a dynamic DMA allocation.** If TZ doesn't know about this `mdata_mem` range (it's an `alloc-ranges` not a fixed `reg`), the assign could fail.
---
## Area 2: SCM / secure-channel path
### The SCM call itself
| | LOS | Mainline |
|--|-----|---------|
| SCM SVC/CMD | `SCM_SVC_MP (0x0C)` / `MEM_PROT_ASSIGN_ID (0x16)` | `QCOM_SCM_SVC_MP (0x0C)` / `QCOM_SCM_MP_ASSIGN (0x16)` |
| Call convention | `scm_call2(SCM_SIP_FNID(...), &desc)` — 7-arg SIP | `qcom_scm_call(dev, &desc, &res)` via `scm_smc_call` — 7-arg SIP |
| VMID_HLOS | `0x3` | `0x3` |
| VMID_MSS_MSA | `0xF` | `0xF` |
| Perms | `PERM_READ=4, PERM_WRITE=2, PERM_EXEC=1` | `QCOM_SCM_PERM_READ=4, WRITE=2, EXEC=1` |
| Dest struct | `{u32 vm, u32 perm, u64 ctx, u32 ctx_size}` (20B, pads to 24B on arm64) | `{__le32 vmid, __le32 perm, __le64 ctx, __le32 ctx_size, __le32 unused}` (24B) |
| Src struct | `mem_prot_info {phys_addr_t addr, u64 size}` | `qcom_scm_mem_map_info {__le64 mem_addr, __le64 mem_size}` |
| Buffer alloc | `kzalloc` + `virt_to_phys` + `dmac_flush_range` | `qcom_tzmem_alloc` from dedicated TZ memory pool |
| Endianness | native (cpu-endian u32/u64) | explicit `cpu_to_le32`/`cpu_to_le64` |
**DELTA 4: SCM buffer memory source.** LOS uses `kzalloc` (normal kernel memory) + `virt_to_phys` for the SCM descriptor buffers. Mainline uses `qcom_tzmem_alloc` from a special TZ-visible memory pool. If `qcom_tzmem` is misconfigured or the pool isn't set up correctly on SDM845, TZ can't read the descriptor and returns -22.
**DELTA 5: Endianness handling.** LOS writes native-endian `u32`/`u64` to the descriptor buffers. Mainline uses `cpu_to_le32`/`cpu_to_le64`. On arm64 (little-endian), these are no-ops. Not a functional difference.
**DELTA 6: src (mem_to_map) struct.** LOS: `{phys_addr_t addr, u64 size}` — on arm64 this is `{u64 addr, u64 size}` = 16 bytes. Mainline: `{__le64 mem_addr, __le64 mem_size}` = 16 bytes. Same wire format.
---
## Area 3: DT — reserved-memory, power-domains, smem-states
### Reserved memory
| Region | LOS DTS (`sdm845.dtsi`) | Mainline (`sdm845-google-common.dtsi`) | Live Android |
|--------|----|----|-----|
| MPSS | `0x8e000000`, `0x7800000` (120MB) | `0x8e000000`, `0x9800000` (152MB) | `0x8e000000`, `0x9800000` (152MB) |
| MBA | `0x96500000`, `0x200000` (2MB) | `0x98500000`, `0x200000` (2MB) | `0x98500000`, `0x200000` (2MB) |
| SLPI | `0x96700000`, `0x1400000` (20MB) | `0x98700000`, `0x1400000` (20MB) | `0x98700000`, `0x1400000` (20MB) |
| Metadata | (none — uses dma_alloc) | `alloc-ranges 0xa0000000`, `0x4000` (16KB), `no-map` | N/A |
The LOS DTS has different BASE ADDRESSES from what Android actually uses. The google-common override produces the correct values that match the live device.
**DELTA 7: LOS DTS says 120MB MPSS, but Android actually runs with 152MB.** The google-common override (which LOS applies via board-level .dtsi) matches. Mainline has the same override. Geometry is the same in practice.
**DELTA 8: Mainline has a `mdata_mem` reserved region. LOS does not.** Mainline's `q6v5_mpss_init_image` checks for a `metadata` child node with `memory-region` and uses that reserved region for metadata. If absent, falls back to `dma_alloc_attrs`. LOS always uses DMA. If the `mdata_mem` alloc-ranges region (`0xa0000000`) is not TZ-known, the metadata assign would fail. But `q6v5_mpss_init_image` is called AFTER MBA boots, so this wouldn't cause the MBA-level failure.
### MSS DT node differences
| Property | LOS | Mainline |
|----------|-----|---------|
| compatible | `qcom,pil-q6v55-mss` | `qcom,sdm845-mss-pil` |
| reg | 8 regions (qdsp6, halt_q6/modem/nc, rmb, restart, pdc_sync, alt_reset) | 2 regions (qdsp6, rmb) — halts via syscon, reset via reset-controller |
| `memory-region` | `<&pil_modem_mem>` on the node itself | Via `mpss` child node |
| `qcom,mem-protect-id` | `<0xF>` — read by `peripheral-loader.c:912` into `subsys_vmid` | **ABSENT** — mainline uses hardcoded `need_mem_protection=true` in platform descriptor |
| Regulators | `vdd_cx`, `vdd_mx`, `vdd_mss` as direct supplies with TURBO voltage | `power-domains` via `rpmhpd` (cx, mx, mss) |
| AOP | `qcom,signal-aop` + `mboxes = <&qmp_aop 0>` | `qcom,qmp = <&aoss_qmp>` — same path, different binding |
| `qcom,pil-self-auth` | present | **ABSENT** — mainline always self-auths |
| `qcom,override-acc` | present | **ABSENT** — mainline handles ACC differently |
| `qcom,qdsp6v65-1-0` | present (selects q6v65 reset sequence) | **ABSENT** — mainline selects via `version = MSS_SDM845` |
| `qcom,mss_pdc_offset` | `<8>` | **ABSENT** — mainline uses `pdc_reset` reset-controller |
| MBA child | `qcom,mba-mem@0` with `qcom,pil-mba-mem` compatible + `memory-region` | `mba` child with just `memory-region` |
| Metadata child | (none) | `metadata` child with `memory-region = <&mdata_mem>` |
---
## Summary of deltas, ranked by likelihood of causing the -22
1. **DELTA 1 — MPSS pre-assign before MBA boot.** Mainline assigns 152MB MPSS to MSS before MBA boots. LOS does not. This is a large, early hyp-assign that LOS avoids entirely. If this returns -22, the modem never starts. This is the most likely trigger — it's the first hyp-assign call mainline makes that LOS doesn't, and it's for the biggest region.
2. **DELTA 4 — SCM buffer allocation (tzmem vs kzalloc).** If `qcom_tzmem` allocates from a region TZ can't read, every SCM call fails. Would affect ALL assign calls, not just modem.
3. **DELTA 3 — Metadata reserved region.** Mainline uses a fixed `mdata_mem` at alloc-ranges `0xa0000000`. LOS uses dynamic DMA. If TZ doesn't know about this range, metadata auth assign fails. But this is after MBA boot, so it wouldn't cause the PBL-level failure.
4. **DELTA 8 — No `qcom,mem-protect-id` in mainline DT.** LOS reads VMID `0xF` from DT. Mainline hardcodes `need_mem_protection=true` and uses `QCOM_SCM_VMID_MSS_MSA` (also `0xF`). Functionally equivalent.
**The test:** Remove the MPSS pre-assign from `q6v5_mba_load` (lines 1236-1241 in `qcom_q6v5_mss.c`) — let MBA boot with only the MBA region assigned, matching LOS behavior. The MPSS assign happens later in `q6v5_mpss_load` anyway. If this clears the -22, DELTA 1 was the gate.

View file

@ -0,0 +1,378 @@
# PAF · qseecomd / TZ-listener archaeology on the LOS vendor extraction
Scope: identify exactly which AP-side TrustZone components (qseecomd + listeners) the modem's
secure-storage (SFS) path depends on, using the already-extracted vendor stack. No device/ssh
touched; read-only archaeology on `android-reference/`.
Prior art read first (do not re-derive): `HANDOFF.md:560-587` (the IMEI-mismatch finding that
opened this frontier — "SFS/secure-EFS layer... Keymaster decrypts persist blobs | none"),
`PAF/modem.md:610-634` (Android-vs-pmOS service parity table, `Keymaster | decrypts persist blobs
| none | IMEI encrypted at rest`), `android-capture-20260622/EFS_FINDINGS.md` (2026-06-22, **now
superseded** by the later IMEI finding — its "no persisted modem-side state is missing" conclusion
was written before the TEST-IMEI-under-pmOS discovery).
## Materials actually available (correction to task brief)
`pixel3-work/los-vendor/vendor.img` does **not exist** in this project. What exists instead is
better: a fully unpacked vendor partition tree at
`android-reference/vendor-stack-20260621/vendor/{bin,lib,lib64}/` — no debugfs/7z mounting needed,
direct file/strings access. Also present: `android-reference/init-rc/init.sdm845.rc` (29KB, the
real device init.rc), `init.sdm845.mpssrfs.rc`, `init.sdm845.diag.rc`, `qcrild.rc`; and
`android-live-20260619/` (boot/radio logs, partition map, rmt_storage rc — no qseecomd rc captured
live).
## CONFIRMED — binaries and libraries present
`find android-reference/vendor-stack-20260621/vendor -iname '*qseecom*' -o -iname '*listener*' -o
-iname '*keymaster*' -o -iname '*rpmb*' -o -iname '*ssd*' -o -iname '*sfs*' -o -iname '*drmfs*'`:
- `bin/qseecomd` — the listener-service host daemon (stripped, ELF aarch64, Android 31 ABI)
- `lib64/libQSEEComAPI.so` — QSEECom userspace API (talks to `/dev/qseecom` char device via ioctl)
- `lib64/librpmb.so`, `lib/librpmb.so` — RPMB listener backend
- `lib64/libssd.so`, `lib/libssd.so` — SSD ("Secure Storage Data") listener backend
- `lib64/libdrmfs.so`, `lib/libdrmfs.so` — DRM/persist-file listener backend
- `lib64/libadsp_default_listener.so`, `libcdsp_default_listener.so`, `libssc_default_listener.so`
— per-DSP FastRPC default listeners (ADSP/CDSP/SLPI-adjacent, not modem-SFS)
- Keymaster stack: `android.hardware.keymaster@{3.0,4.0,4.1}.so`, `libqtikeymaster4.so`,
`libkeymaster4support.so`, `libkeymasterprovision.so`, `nos_app_keymaster.so`,
`bin/hw/android.hardware.keymaster@4.0-service-qti`,
`bin/hw/android.hardware.keymaster@4.1-service.citadel` (Citadel = discrete security chip
StrongBox, separate from the SDM845-embedded TZ/QSEE path — Pixel 3 has both)
- **No dedicated modem-SFS listener library exists in the extraction.** No `libsfs*`, no
`*modem*sec*`, no `*qsee*modem*` name anywhere in `vendor/`. Grepping
`libril-qc-hal-qmi.so` for `modemst|persist|tzstorage|sfs` returns nothing — the RIL HAL doesn't
touch secure storage directly either. This means modem SFS most plausibly rides through the
**generic** `ssd`/`rpmb`/`drmfs` listeners rather than a modem-specific one (Qualcomm's modem SFS
client library lives in the *modem firmware*, calling out to these generic AP listeners — nothing
AP-side is modem-branded).
## CONFIRMED — init.rc service definitions and ordering
`android-reference/init-rc/init.sdm845.rc`:
- Line 607-610: `service vendor.qseecomd /vendor/bin/qseecomd``class core`, `user root`, `group
root`.
- Lines 73-84 (in the `on init && property:vendor.skip.init=0` block, i.e. very early):
```
# QSEE rpmb listener need symlink for bootdevice
wait /dev/block/platform/soc/${ro.boot.bootdevice}
symlink /dev/block/platform/soc/${ro.boot.bootdevice} /dev/block/bootdevice
# start qseecomd early as we mount system/ vendor/ early
# vendor needs keymaster that needs qseecomd
start vendor.qseecomd
start keymaster-4-0
# start Citadel keymaster (StrongBox) early
start vendor.citadeld
start vendor.keymaster-4-1-citadel
```
The comment **"QSEE rpmb listener need symlink for bootdevice"** confirms the RPMB listener
requires `/dev/block/bootdevice` to exist before qseecomd's listener threads can start — an
explicit ordering dependency the AP init script encodes and pmOS's init/OpenRC does not.
- Line 281 (`on post-fs-data`): `wait_for_prop vendor.sys.listeners.registered true` — init
**blocks** IPA firmware loading on this property.
- The property `vendor.sys.listeners.registered` is set by `qseecomd` itself — confirmed by
`strings bin/qseecomd`: `Total listener services to start = %d`, `%s: Started (Listener ID =
%d)`, and the literal string `vendor.sys.listeners.registered` present in the binary.
- `qseecomd` strings also show two fatal-if-failed gates:
`ERROR: RPMB_INIT failed, shall not start listener services`
`ERROR: SSD_INIT failed, shall not start listener services`
`SEC_UI_INIT failed, shall not start secure ui listener` — i.e. RPMB and SSD backend init are
hard prerequisites for the listener framework to come up at all; if either fails, **no** listener
(including whatever handles modem SFS) starts.
- `rmt_storage` (line 644-648, `class core`) and `modem_svc` (line 655-659, `class core`) are
declared **after** qseecomd/keymaster in the file but all are `class core` — Android's init classes
don't strictly serialize within a class by file order alone; the *documented* explicit dependency
is the `wait_for_prop vendor.sys.listeners.registered true` gate on IPA (data path), not on
rmt_storage/modem load directly. **No explicit `wait_for_prop` ties modem/rmt_storage start to
qseecomd** in this rc — the dependency for modem SFS is therefore INFERRED (via the modem firmware's
own runtime SFS calls reaching TZ reaching the AP listener), not an init-script ordering fact.
## CONFIRMED — where the secure blobs plausibly live
From `strings` on the listener backend libraries themselves:
- `libssd.so`: `/dev/block/bootdevice/by-name/ssd`, `/dev/block/platform/msm_sdcc.1/by-name/ssd`
a **dedicated GPT partition named `ssd`** ("Secure Storage Data"). `QSEECom_register_listener` /
`ssd_init_service` / `ssd_read` / `ssd_write` confirm this backs a QSEECom listener that a TZ
trustlet calls into for opaque encrypted blob storage.
- `libdrmfs.so`: `/data/vendor/tzstorage/`, `/data/misc/qsee/`, `/mnt/vendor/persist/data/`,
`/persist/data/`, plus XML-driven `gp_persist_path` config — a **file-backed** persist store
(DRM/general-purpose TZ file storage), separate from the raw `ssd` partition.
- `librpmb.so`: `/dev/block/bootdevice` (via `by-name` symlink), `/dev/block/mmcblk0rpmb`,
`RPMB.128KB/256KB/512KB/1MB` size buckets — the actual **RPMB (Replay Protected Memory Block)**
hardware region on the UFS/eMMC boot device, accessed via `rpmb_emmc_read/write` ioctls.
**Partition-map check — RESOLVED 2026-07-02, live on pmOS:** the June partition-map capture was
incomplete, not authoritative. `ls /dev/disk/by-partlabel/` on the live phone tonight shows the
`ssd` partition **does exist**. Also confirmed live: the UFS well-known RPMB LUN is present
(`ufs-bsg0`, scsi target `0:0:0:49488`), and `/mnt/vendor/persist/data/` holds real factory (Oct
2018 / May-Jul 2019 dated) SFS-shaped blob trees — main+`_Alt` twin dirs with `.bak` twins per
file, matching Qualcomm's dual-copy anti-rollback layout. Both listener backends (`ssd`, `rpmb`)
have real backing storage on this exact device; nothing is missing at the hardware/partition
level. The gap is entirely software (no AP-side listener driver/daemon under mainline).
**INFERRED (not proven on disk):** modem SFS blobs (IMEI, RF identity, calibration trust anchors)
most likely live in either the `ssd` partition (if present) or under `/persist/data/`
(`libdrmfs.so`'s default paths overlap with the `persist` partition pmOS already mounts and rmtfs
already serves plaintext EFS from). If modem SFS uses `/persist/data/` via `libdrmfs`, the **bytes
are already visible to pmOS at the block level** (persist is not RPMB) but are TZ-key-encrypted
**in the file contents**, decrypted only via a live TZ round-trip through `qseecomd`'s listener —
consistent with "plain EFS I/O works, secure layer doesn't" exactly as HANDOFF.md describes.
## CONFIRMED — no modem-specific listener ID found
Searched every `.so` under `vendor/lib64` for modem+SFS co-occurring strings
(`modem.*sfs|sfs.*modem|modemst|modem_fsg`): only hits were `android.hardware.radio@1.{3,4,5}.so`
and `libril-qc-hal-qmi.so`, and those are RIL/QMI Android HAL symbols (`getModemStackStatus` etc.),
**not** QSEEComAPI listener registration. No listener ID constant (e.g. `0x3000`/`8192`) was found
readable in any stripped binary via `strings` — listener IDs are typically passed as integer
arguments at `QSEECom_register_listener()` call sites, not embedded as readable strings; extracting
them requires disassembly (Tier 3, out of scope for this pass).
## Could a mainline userspace replace this?
**What the dependency IS, precisely:** the modem firmware's own SFS client (inside the MPSS
firmware blob, not AP code) makes a TZ syscall that TZ cannot service locally; TZ round-trips to
the AP via a **QSEECom listener** (RPMB and/or SSD and/or DRM-persist backend, hosted by
`qseecomd`), which does raw block I/O against either the RPMB hardware region or the `ssd`/`persist`
partition and returns the (TZ-en/decrypted) blob. Without `qseecomd` alive and its
`RPMB_INIT`/`SSD_INIT` having succeeded, TZ's callback to the AP has no listener to answer it —
the modem's SFS call would time out or the TZ syscall would return an error, which is consistent
with the modem parking itself in OFFLINE rather than crashing (a "deliberate refusal," matching
`secure_load_tz_frontier.md`'s framing).
**Feasibility notes (brief — this doc identifies the dependency, does not scope the fix):**
- Open-source reimplementations of `qseecomd`'s listener framework exist in the wild for other
Qualcomm mainline bring-ups (postmarketOS/msm8916, sdm660, sdm845 devices with eMMC RPMB
trustlets) — typically a minimal userspace daemon that opens `/dev/qseecom`, registers the RPMB
and SSD listener IDs, and does raw partition I/O, without needing Android's HIDL/keymaster stack
around it. This is architecturally plausible to port: `librpmb.so`/`libssd.so`'s job is generic
block I/O, not Android-specific.
- The harder unknown is whether TZ on this specific signed XBL/TZ image expects listener
**registration handshake details** (listener ID, buffer negotiation, `QSEECom_register_listener`
call sequence) that differ from a generic mainline shim — that requires either disassembling
`libssd.so`/`librpmb.so` (small, no external deps, tractable) or the QSEECom kernel driver's ioctl
ABI (mainline has a `qcom_scm`/`qseecom` driver already, so the kernel side may already exist —
not checked in this pass).
- Keymaster/Citadel (StrongBox) is a red herring for the modem specifically — HANDOFF's "Keymaster
decrypts persist blobs" framing is plausible for *other* persist data but the modem SFS path more
directly implicates RPMB/SSD listeners, not the Keymaster HAL itself (Keymaster protects Android
keystore keys; the modem's own TZ trustlet does its own key derivation independent of Android
Keymaster, per Qualcomm's usual architecture — **INFERRED**, not confirmed on this disk).
## What to try next
1. **Settle whether an `ssd` partition exists on this device.** Not in
`android-live-20260619/partition-map.txt`; re-run a full `by-name` partition enumeration
(`ls -la /dev/block/bootdevice/by-name/` on rooted Android, read-only) specifically looking for
`ssd`. This single fact discriminates between the RPMB-only path and the SSD-partition path.
2. **Capture `qseecomd`'s runtime behavior on Android** (read-only): `logcat -b all | grep -i
qseecom`, and `getprop vendor.sys.listeners.registered` at various boot stages, to see exactly
when listeners come up relative to `rmt_storage`/modem PIL load. Confirms whether the ordering
this doc inferred from init.rc is real at runtime.
3. **DONE 2026-07-02 — see "Live disassembly results" section below.** Listener IDs recovered:
SSD = `0x5000`, RPMB = `0x6400`. (Both differ from the earlier chat-only agent's string-based
guesses of `0x3000`/`0x2000` — those were unread relocation offsets, not real constants. Trust
this section's numbers, not that prior report.)
4. **Check whether mainline's existing `drivers/misc/qcom-qseecom` / `qcom_scm` kernel driver
(if present in `references/sdm845-mainline-*`) already exposes `/dev/qseecom`** — if the kernel
ABI is already there, only a userspace listener daemon is missing, which is a much smaller lift
than expected.
5. Cross-check this doc's ordering claims against `secure_load_tz_frontier.md`'s XPU/`assign_mem
-22` findings — if the XPU crash-loop and the SFS-listener gap are the same root cause
(TZ-side memory/permission setup Android does and mainline doesn't), the fixes may converge.
## Live disassembly results — 2026-07-02
Toolchain note: this system's `objdump` is x86-only (no AArch64 target compiled in — confirmed
via `objdump -i`). `rizin` (0.8.2, installed) has its own capstone-based AArch64 disassembler and
does NOT depend on binutils target support. Use `rizin -q -c 'e asm.arch=arm; e asm.bits=64; af @
<addr>; pdf @ <addr>' <file>` for any ARM64 .so in this project. This works for stripped Android
libs (dynsym exports still give function boundaries even with no local symtab).
### `libssd.so` (lib64, 11KB) — thin wrapper, confirms the theory exactly
Exports only 3 functions: `ssd_init_service`, `ssd_start`, `ssd_close`. All real work is in
`libQSEEComAPI.so` (NEEDED entry) via `QSEECom_register_listener`/`_receive_req`/`_send_resp`.
- `ssd_init_service` (0x1014): opens `/dev/block/platform/msm_sdcc.1/by-name/ssd`, falls back to
`/dev/block/bootdevice/by-name/ssd` on failure. Stores fd at offset `0x290` in a global struct.
Confirms live phone finding: the `ssd` GPT partition is the real backing store, block-device
raw I/O, no filesystem.
- `ssd_start` (0x10e0): `QSEECom_register_listener(&global+0x298, 0x5000, 0)`.
**SSD listener ID = 0x5000.**
### `librpmb.so` (lib64, 29KB) — real UFS RPMB driver, much richer than libssd
Exports: `rpmb_init_service`, `rpmb_start`, `rpmb_init`, `rpmb_default_init`, `rpmb_close`,
`rpmb_exit`, `rpmb_read`/`rpmb_write`, plus separate eMMC (`rpmb_emmc_*`) and UFS (`rpmb_ufs_*`)
backends selected at init — `rpmb_ufs_sg_dev_open`/`_close` uses the SCSI generic (`sg`) device,
matching the live phone's `ufs-bsg0` / `0:0:0:49488` well-known LUN found tonight.
- `rpmb_start` (0x4a68): `QSEECom_register_listener(&global+0x918, 0x6400, 0)`.
**RPMB listener ID = 0x6400.** (Rizin's disassembly initially mis-labeled the immediate-load
instruction as `reloc.rpmb_ufs_read` because 0x6400 collides with a PLT slot address in its
symbol table — decoded the raw instruction bytes by hand to confirm: `52 8c 80 02` =
`MOVZ w2, #0x6400`. Don't trust rizin's auto-symbolization on immediate loads without
double-checking raw bytes when the "symbol" name looks suspicious.)
- `rpmb_ufs_send_request` (0x38b0, 536 bytes) is the actual TZ-facing wire call for UFS RPMB
frames — not yet disassembled, next concrete step (see below).
### `libQSEEComAPI.so` (lib64, 32KB) — the real kernel ABI boundary
`QSEECom_register_listener(void **handle, uint32_t listener_id, uint32_t sb_size)` fully traced
(0x43bc, 684 bytes):
1. If `*handle == NULL`: `open("/dev/qseecom", O_RDWR)`, fd stored at struct offset `0x10`.
2. ION shared-buffer alloc via internal `__QSEECom_ION_memalloc(handle, listener_id, 0x1b, sb_size)`
`0x1b` is the ION heap-id mask argument.
3. `ioctl(fd, 0xc004970a, &mem_param_req)` — this is `QSEECOM_IOCTL_SET_MEM_PARAM_REQ` by ioctl
number shape (`_IOWR` direction bits `0xc0`, size `0x04`, type `0x97`, nr `0x0a`).
4. `ioctl(fd, 0xc0189701, &listener_req)``QSEECOM_IOCTL_REGISTER_LISTENER_REQ`. Request struct
built at `var_60h`: `{listener_id: w21, ion_fd: w22 (or -1 on alloc fail path), sb_virt: x0,
sb_size: w21 (dup)}` — needs one more pass to nail exact field order/padding against upstream
downstream `qseecom.h`'s `qseecom_register_listener_req` struct definition (not yet fetched).
5. On ioctl failure with `errno == 16 (EBUSY)`: returns `-2` ("already registered", not a hard
error) instead of `-1`. `ssd_start`/`rpmb_start` both treat `-2` as success-equivalent (skip
the error path, fall through to the pthread_create for the receive loop).
This is enough to write a minimal mainline `/dev/qseecom` listener client IF mainline's QSEECOM
driver ever grows `SET_MEM_PARAM`/`REGISTER_LISTENER` ioctl support — which it currently doesn't
(confirmed separately: `qcom_qseecom.c` has no ioctl surface at all, single hardcoded app table).
So this ABI trace is necessary-but-not-sufficient; the missing piece is still kernel-side.
### Not yet done (next session, in priority order)
1. **`rpmb_ufs_send_request`** (librpmb.so 0x38b0) — the actual SCM/TZ syscall wire format for
RPMB frames. This is the piece that would tell us whether RPMB access could theoretically be
done via raw `qcom_scm_call()` from kernel space without the full listener ABI, or whether it
genuinely needs the userspace round-trip through `/dev/qseecom`.
2. **`QSEECom_receive_req`/`QSEECom_send_resp`** (libQSEEComAPI.so) — the listener's blocking
receive-request / send-response loop, i.e. what actually happens after registration succeeds.
This is the runtime protocol a mainline replacement daemon would need to implement.
2b. Decode the exact `qseecom_register_listener_req` struct field layout — need upstream/downstream
`qseecom.h` (check `references/los-kernel-blueline-4.9/drivers/misc/qseecom.h` if it exists,
agent's TZ report cited `qseecom.c` but layout header wasn't confirmed read).
3. **`libdrmfs.so`** — not touched this pass at all. Handles `/data/vendor/tzstorage/` and
`/persist/data/` per the prior agent's string analysis; the `/mnt/vendor/persist/data/` factory
SFS trees found live tonight are exactly its target. Same rizin workflow applies.
4. Cross-check ioctl numbers `0xc004970a`/`0xc0189701` against mainline's `include/uapi/misc/
qcom_essivtm.h`-style headers if any QSEECOM uapi header exists anywhere in the kernel tree —
would confirm/deny whether mainline's ioctl numbering (if it ever adds these) needs to match
Android's exactly, or if TZ itself doesn't care about the AP-side ioctl numbers (likely, since
ioctls are a kernel-driver-internal convention, not part of the SCM wire protocol to TZ).
## `libdrmfs.so` disassembly — 2026-07-02
Two independent listener registrations found, NOT one:
- `fs_start` (0x239c, 112 bytes): `QSEECom_register_listener(&global+0x440, 0x5000, 0)`.
**Registers the SAME listener ID as `ssd_start` (0x5000).** Not a bug/collision — either this
is dead/legacy code in this build (superseded by `gpfs_start`), or `fs`/`ssd` are two client
libraries that both legitimately attach to one shared TZ-side listener namespace. Not resolved
which; `fs_start` has zero XML/dynamic-path logic (unlike `gpfs_start`), suggesting it's the
older/simpler of the two and `gpfs_start` is what's actually active at runtime — check
qseecomd's actual startup log strings (`Total listener services to start = %d`) against a live
Android boot to see which of `fs`/`gpfs`/`ssd` actually get invoked, if that's ever recaptured.
- `gpfs_start` (0x3ae8, 1260 bytes) — the real, dynamic one: `opendir()`-probes candidate config
dirs, `xmlParseFile()`s an XML descriptor (this is the "XML-driven `gp_persist_path`" the prior
agent found via strings), then `QSEECom_register_listener(&global+0x450, 0x7e000, 0x80)`.
**GPFS (general-purpose filesystem) listener ID = 0x7e000.** Distinct ID space from ssd/rpmb —
0x7e000 is far larger, consistent with Qualcomm's listener-ID allocation using a different
range for "general purpose"/OEM-extensible listeners vs. the fixed system ones.
**Confirmed listener ID table (all three, disassembly-verified, not string-guessed):**
| Backend | Listener ID | Source function | Backing storage (live-verified tonight) |
|---|---|---|---|
| SSD | `0x5000` | `libssd.so:ssd_start` (+ `libdrmfs.so:fs_start`, same ID) | `ssd` GPT partition — confirmed present |
| RPMB | `0x6400` | `librpmb.so:rpmb_start` | UFS RPMB well-known LUN — confirmed present (`ufs-bsg0`) |
| GPFS | `0x7e000` | `libdrmfs.so:gpfs_start` | XML-configured paths, likely `/persist/data/`, `/data/vendor/tzstorage/` |
## RPMB transport mechanism — architecturally important finding
`rpmb_ufs_send_request_sense` (librpmb.so, 0x38b0) disassembled: builds a SCSI CDB (opcode byte
`0x60`, a JEDEC UFS RPMB SECURITY PROTOCOL frame shape) and submits it via
`ioctl(fd, 0x2285 /* SG_IO */, &sg_io_hdr)` against a raw SCSI-generic (`/dev/sg*`) device handle
opened earlier by `rpmb_ufs_sg_dev_open`.
**This is plain kernel block-layer I/O — SG_IO passthrough, zero TrustZone/SCM/QSEECOM
involvement in the actual RPMB frame transport.** The QSEECOM listener registration
(`rpmb_start`, ID `0x6400`) is a SEPARATE mechanism: it's how the *modem firmware*, running in
its own TZ-adjacent execution context, asks the AP to relay RPMB frames on ITS behalf (modem
firmware has no direct UFS access — a TZ trustlet calls out to the registered AP listener, which
does the SG_IO work and returns the result back through TZ to the modem).
**Why this matters for the mainline fix:** it means a mainline replacement doesn't need to
reverse-engineer any SCM/TZ RPMB protocol — the RPMB wire format is bog-standard JEDEC UFS
SECURITY PROTOCOL IN/OUT over SG_IO, which mainline's own kernel UFS/BSG stack already fully
understands (this is not modem-specific or Qualcomm-specific at all). The ONLY missing piece is
the QSEECOM listener registration/dispatch loop that lets TZ hand a pending RPMB request to
userspace and get the SG_IO result back — i.e. exactly the `/dev/qseecom`
`SET_MEM_PARAM`/`REGISTER_LISTENER`/receive-request/send-response ABI traced in the
`libQSEEComAPI.so` section above. **The actual RPMB disk I/O is a solved problem already in
mainline; only the TZ-listener relay is missing.**
## Updated priority for next session
Given the above, the size of "minimal mainline replacement" shrinks:
1. A mainline QSEECOM listener client (kernel: needs `/dev/qseecom` ioctl support that doesn't
exist yet in `qcom_qseecom.c` — this is the real blocking gap, confirmed structural earlier
tonight) that can register listener IDs `0x5000`/`0x6400`/`0x7e000`.
2. A userspace daemon that, on receiving a relayed request, does exactly what `librpmb.so`/
`libssd.so` do: raw block I/O (SG_IO for RPMB, plain read/write for the `ssd` partition) —
this part has NO remaining unknowns, it's now fully specified by tonight's disassembly.
3. `QSEECom_receive_req`/`QSEECom_send_resp` (libQSEEComAPI.so) — still not disassembled; this is
the blocking-loop protocol the daemon would need to implement against the new kernel ioctls.
Next concrete disassembly target.
## `QSEECom_receive_req`/`QSEECom_send_resp` — full listener loop protocol MAPPED
The complete Android-side listener runtime protocol, fully traced (libQSEEComAPI.so):
**`QSEECom_receive_req(handle, out_buf, out_buf_len)`** (0x4d40):
1. `ioctl(fd, 0x9705 /* QSEECOM_IOCTL_RECEIVE_REQ */, NULL)`**blocks** until TZ has queued a
request for this listener's shared buffer. No payload pointer passed to the ioctl itself — the
request data is already sitting in the ION shared buffer registered at listener-registration
time; this ioctl is purely a "wake me up" signal.
2. On `ioctl` failure with `errno == -0x200` (i.e. `ERESTARTSYS`/interrupted): logs and treats
distinctly from a hard error (retry-worthy, not fatal) — same handle-internal cached fields
(`shared_buf_ptr`, `shared_buf_len`) at offsets `0x0`/`0x8` used to bounds-check.
3. Once woken: bounds-checks the pending request size against `out_buf_len`, `memcpy`s the
request out of the shared ION buffer into the caller-provided `out_buf`, then `memset`s the
shared buffer clean.
**`QSEECom_send_resp(handle, resp_buf, resp_len)`** (0x4ea4):
1. Bounds-checks `resp_len` against the registered shared-buffer size (handle offset `0x8`).
2. `memcpy`s `resp_buf` into the shared ION buffer.
3. Special-cases command type `0x13` at handle offset `0x1c` — zeroes an extra field, likely a
listener-specific response-tag/status clear (not fully resolved, low priority).
4. `ioctl(fd, 0x9706 /* QSEECOM_IOCTL_SEND_RESP_REQ */, NULL)` — hands the response back to TZ,
also payload-free (data already placed in shared memory by the memcpy above).
**Full ioctl number set now confirmed (all four, disassembly-verified):**
| ioctl | Number | Direction | Purpose |
|---|---|---|---|
| `SET_MEM_PARAM_REQ` | `0xc004970a` | `_IOWR` | register the ION shared buffer with the kernel driver |
| `REGISTER_LISTENER_REQ` | `0xc0189701` | `_IOWR` | register `{listener_id, ion_fd, vaddr, size}` |
| `RECEIVE_REQ` | `0x9705` | `_IO` (no payload) | block until TZ has a pending request in shared mem |
| `SEND_RESP_REQ` | `0x9706` | `_IO` (no payload) | tell TZ the shared-mem response is ready |
**This closes out the userspace-side unknowns entirely.** A minimal mainline listener daemon now
has a complete, disassembly-verified spec to implement against, IF the kernel gains the matching
`/dev/qseecom` ioctl surface. The three listener IDs (`0x5000` ssd, `0x6400` rpmb, `0x7e000` gpfs)
plus this four-ioctl protocol plus the plain-SG_IO RPMB transport (previous section) is a complete
picture of "what Android does" — the remaining gap is 100% kernel-side (`qcom_qseecom.c` needs
these four ioctls implemented against real SCM calls) and TZ-handshake-side (whether this exact
signed TZ image accepts registration from a differently-built AP driver — unknown, untestable
without trying it).
## Session-end status (2026-07-02)
Every planned disassembly target for this pass is done: libssd.so, librpmb.so (both listener
registration + RPMB transport), libdrmfs.so (fs_start + gpfs_start), libQSEEComAPI.so
(register_listener, receive_req, send_resp — the full 4-ioctl protocol). Not yet touched:
`libQSEEComAPI.so`'s `QSEECom_start_app`/`QSEECom_load_external_elf` family (TZ *app* loading,
as opposed to *listener* registration — likely irrelevant to SFS/RPMB but unconfirmed) and
`qseecomd` binary's own main()/startup logic (which of ssd/rpmb/gpfs it actually launches and in
what order — inferred from init.rc gates so far, never disassembled directly).

193
saf/device/mms.md Normal file
View file

@ -0,0 +1,193 @@
# PAF · MMS (picture messaging)
## STATUS (2026-07-15): MMS SEND WORKS end-to-end. Two loose ends remain.
**A real picture message sent successfully today** — image to +12897767709,
508 KB, POSTed through Fido's MMS proxy and cleared from the queue. The core
problem (reaching Fido's MMS servers with the right APN) is SOLVED. See
"THE WORKING RECIPE" below.
Two things still not fully nailed:
1. **No auto-start / lifecycle for the daemon.** `mmsd-tng` has no D-Bus
auto-spawn or systemd service — it only runs when launched by hand. When it
isn't running, sends silently pile up in the queue (that's how 4 messages
got stuck). Needs a proper service unit.
2. **Chatty says "MMSC is not configured."** The daemon's *internal* config is
correct and it sends fine, but it does NOT expose the
`org.ofono.mms.ModemManager` D-Bus interface (only `org.ofono.mms.Service`),
which is what Chatty's settings menu reads. So Chatty can't see the MMSC even
though sending works. Setting values in Chatty's own menu did NOT bring that
interface up. Daemon-side interface-registration issue, not a wrong-value
issue — the file `~/.mms/modemmanager/mms` already reads
`MMS_APN=netsvcs`, `CarrierMMSC=http://mms.fido.ca`,
`CarrierMMSProxy=205.151.11.13:80` (all correct).
---
## THE WORKING RECIPE (proven live 2026-07-15)
1. **APN = `netsvcs`** (NOT `mms.fido.ca`). Patched into the carrier database at
`rootfs-overlay/usr/share/mobile-broadband-provider-info/serviceproviders.xml`
(Fido entry, `<apn value="netsvcs">`, mmsc `http://mms.fido.ca`, proxy
`205.151.11.13:80`). `mmsd-tng` re-reads that DB and writes the right values
into `~/.mms/modemmanager/mms` — BUT only re-looks-up when its current MMSC is
the placeholder `http://mms.invalid`, so to force a refresh you must
`rm ~/.mms/modemmanager/mms` before restarting the daemon.
2. **The `netsvcs` bearer must be connected AND have its IPv4 applied to the
netdev.** ModemManager reports the IP but nothing installs it — apply
manually (see cheat-sheet). Once the IP is on `qmapmux0.X`, the proxy becomes
reachable and `mmsd-tng`'s `SO_BINDTODEVICE` binds sends to it correctly.
3. With both in place, `mmsd-tng` POSTs queued messages through the proxy and
they send. `AutoProcessOnConnection=true` means it auto-flushes the queue
when a usable bearer appears.
**To reproduce a send right now:** ensure the `netsvcs` bearer is up with its IP
applied, `rm ~/.mms/modemmanager/mms`, then
`mmsdtng --debug` — it re-looks-up `netsvcs` from the DB and flushes the queue.
---
Texting (SMS) already works. This doc is about **MMS** — the picture/group
messages. They still don't send/receive on their own yet, but today we found
*why*, and proved the hard part (reaching Fido's MMS servers) actually works.
---
## ELI5 — what MMS even is, and why it's fussy
A plain text (SMS) is tiny, so it rides along on the same signaling channel the
phone uses to talk to the tower — no internet needed. A picture message (MMS) is
too big for that. So MMS actually works like this:
1. The tower sends you a tiny SMS that secretly means *"there's a picture waiting
for you."*
2. Your phone then has to **open a special little internet connection** and go
**download the picture from the carrier's own server** (the "MMSC").
3. To send a picture, your phone uploads it to that same server the same way.
The catch: that "special little internet connection" is **not** your normal data
or WiFi. Carriers put their MMS server on a **private, members-only road** that
only exists inside their own cellular network. You can't get to it from WiFi, and
you can't get to it from the normal data connection either. You have to dial a
**second, separate connection** specifically for MMS.
Every piece of confusion we hit came from that one fact: **MMS needs its own
special door, and we kept knocking on the wrong ones.**
---
## The pieces (and the plain-English name for each)
| Thing | What it really is | Value for this SIM |
|---|---|---|
| **APN** | The *name of the door* you dial for a given service | `netsvcs` (NOT `mms.fido.ca`) |
| **MMSC** | The *server* that actually holds the pictures | `http://mms.fido.ca``209.4.229.111` |
| **MMS proxy** | A *doorman* you must go through to reach the MMSC | `205.151.11.13:80` |
| **The daemon** | `mmsd-tng`, the background program that does all this for you | already installed |
| **The phone book** | `serviceproviders.xml`, a big list of carrier settings the daemon trusts | had the WRONG APN |
---
## What we got wrong (and how we found the right answer)
### Wrong door #1: `mms.fido.ca`
Every website, and the settings file already on the phone, said Fido's MMS APN is
`mms.fido.ca`. We dialed it. The network slammed the door:
`ServiceOptionNotSubscribed` — *"you're not signed up for that."*
That error sounds like an account/billing problem, but it wasn't. It just means
**that door name doesn't exist on this SIM.** The public instructions are for
manually typing settings into an Android phone; they don't necessarily match what
the SIM is actually *provisioned* for underneath.
**How we found the truth:** we asked the modem to list the doors it *actually*
knows about (`mmcli -m 0 --3gpp-profile-manager-list`). It answered with a real
list, and there it was:
```
profile 4: apn: netsvcs apn type: mms
```
The real MMS door on this SIM is called **`netsvcs`**, not `mms.fido.ca`. Dialed
`netsvcs` → it **connected immediately.** (Confirmed the same name shows up in the
raw Android radio logs from this same SIM, so it's genuinely the carrier's name
for it, not a fluke.)
> **Gotcha:** `ltemobile.apn` is the *generic Rogers-family* internet door
> (Rogers/Fido/Chatr all share it). Some guides say "just use that for MMS too."
> It's a red herring here — it's not Fido-specific, and it came up **IPv6-only**,
> which can't even reach the IPv4 MMS server. `netsvcs` is the right door.
### Wrong door #2: trying to reach the server the normal way
Once `netsvcs` connected, we tried to reach the MMS proxy (`205.151.11.13`) — and
it hung, dead silence.
Two reasons, both now understood:
1. **The connection was "connected" but had no address actually plugged in.**
The modem dialed the door and the carrier handed us an address
(`10.145.x.x`) — but *nothing put that address onto the network card.* It's
like being handed the keys to a car that nobody parked in your driveway. On the
normal data connection, a program called NetworkManager does this parking step
automatically. For our hand-dialed MMS connection, **nobody did it.** The
interface had no IPv4 address at all.
The moment we manually "parked the car" (`ip addr add 10.145.146.168/28` onto
the interface, plus routes to the proxy through the carrier gateway) — **the
proxy answered.** Reliably, every time. That was the real unlock.
2. **The proxy only speaks "MMS language."** After it started answering, a plain
web request (`curl`) to it still got silence. That's expected: Fido's proxy is
a **WAP proxy** — it only understands the special MMS protocol (WSP), and
ignores normal web traffic. So a normal browser test will *always* look like a
failure even when the path is perfectly open. Only `mmsd-tng` speaks the right
language to it.
---
## Where it actually stands right now
**PROVEN working:**
- The correct APN is `netsvcs`, and it connects.
- Once its IP is applied to the interface, **Fido's MMS proxy is reachable** (TCP
handshake succeeds every time, from the bearer's own address).
**Still missing (the automation):**
- Nothing yet automatically does the "park the car" step — dial the `netsvcs`
MMS connection, apply its IP + routes to the interface, and hand it to
`mmsd-tng` — at the moment a picture needs to go out, then tear it down after.
- `mmsd-tng`'s own config keeps regenerating from the carrier phone book
(`serviceproviders.xml`) with the wrong `mms.fido.ca` APN, so editing its
local config by hand gets overwritten. Fixing the phone book entry is the
durable path (started: patched to `netsvcs` in `rootfs-overlay/usr/share/
mobile-broadband-provider-info/serviceproviders.xml`), **but** `mmsd-tng`'s
lookup only matches an entry when the *active* bearer's APN equals the entry's
`value=`, so the exact match behavior still needs verifying against the real
code path before calling it solved.
**Next step:** re-test `mmsd-tng` end-to-end now that the network road is
manually open (send one real MMS), to confirm the daemon itself works, *before*
building the automation that opens the road on its own.
---
## Cheat-sheet: manually open the MMS road (for testing)
```sh
# 1. dial the real MMS door, IPv4
mmcli -m 0 --create-bearer="apn=netsvcs,apn-type=mms,ip-type=ipv4"
mmcli -m 0 -b <N> --connect # note the interface (qmapmux0.X) + address
# 2. "park the car": put the handed-out IP onto the interface
sudo ip addr add <address>/<prefix> dev qmapmux0.X
# 3. lay down the private road to the proxy + MMSC via the carrier gateway
sudo ip route add 205.151.11.13/32 via <gateway> dev qmapmux0.X # proxy
sudo ip route add 209.4.229.111/32 via <gateway> dev qmapmux0.X # MMSC
# 4. proof the road is open (TCP handshake to the proxy should succeed):
python3 -c "import socket;s=socket.socket();s.bind(('<address>',0));s.connect(('205.151.11.13',80));print('proxy OPEN')"
```
(A plain `curl` through the proxy will still hang — that's normal, it only
speaks MMS, not web. Real proof is `mmsd-tng` sending a message.)

837
saf/device/modem.md Normal file
View file

@ -0,0 +1,837 @@
# PAF · Modem bring-up
## ARCH STATUS (2026-07-10): WORKING — verified live on the Arch install.
`mmcli`: state connected, LTE, operator Fido, packet service attached,
signal 73%. The whole solved stack made the jump to Arch: `modemsmem`
is in our kernel tree (0c2eb6bf3 "soc: qcom: add modemsmem, wire up on
blueline"), rmtfs/tqftpserv/pd-mapper/ModemManager run as services, and
`blueline-clat.service` handles 464XLAT. Everything below is the research
record of HOW the 52 wall fell (pmOS era, 2026-06/07) — the mechanism is
identical on Arch; read it before touching anything modem-adjacent.
Deep evidence docs referenced below live in `evidence/`.
## RESOLVED 2026-07-03 — modem ONLINE, registered, passing LTE traffic. Root cause of the 52 wall: missing modemsmem driver (SMEM item 134).
Google Pixel modem firmware reads SoC info from SMEM_ID_VENDOR0 (item 134) during boot;
downstream writes it via `drivers/soc/qcom/modemsmem/`. Mainline never had the driver, so
the MPSS RF task found nothing there and parked op-mode OFFLINE → every RF transition
illegal → QMI 52. Fix: Richard Acayan's sdm670 mainline port of the driver (commit
d9afe44b) + `google,modemsmem`/`qcom,smem = <134>` DT node — canonical tree `4ef9b6943`,
pmaports `9d52b47813` (CONFIG_QCOM_MODEMSMEM=y). Verified live: op-mode online, Fido LTE
registration home, IPv6 bearer passing packets. Full details in HANDOFF.md (2026-07-03).
Everything below this line is the (closed) investigation record; the leads/ledger remain
correct as history. Remaining open threads are identity (SFS/test-IMEI) and polish
(IPv4 bearer, calls/SMS), not RF.
## TOP LEAD 2026-07-02 #15 — DIAG_DATA glink channel: AP CAN request it, modem acks but never completes its own open. Real firmware-behavior finding, not a kernel gap.
**Correction to this doc's own earlier framing tonight**: an initial pass concluded mainline's glink
core lacked any AP-initiated ("locally initiated") channel-open primitive at all, and that reaching
DIAG_DATA would need core `qcom_glink` infrastructure work. **That was wrong** — re-read
`drivers/rpmsg/qcom_glink_native.c` properly: `qcom_glink_create_local()` (a complete, working
implementation of exactly this, sends `GLINK_CMD_OPEN`, waits for both acks) already exists and is
already wired into the standard `.create_ept` op every transport uses. The real, and much smaller,
entry-point gap was just: no *driver* was calling `rpmsg_create_ept()` against a fresh channel name
from an already-bound `rpdev`. Fixed with **zero new kernel code** — userspace's existing
`RPMSG_CREATE_EPT_IOCTL` on `/dev/rpmsg_ctrl2` (the modem-edge control device; `rpmsg_ctrl0`/`1` are
CDSP/ADSP respectively, confirmed via `/sys/devices/.../4080000.remoteproc/.../rpmsg_ctrl2`) reaches
this exact path already. Built a 60-line test binary (`diag_open_test.c`, cross-compiled via
pmbootstrap chroot) to drive it.
**Live result, confirmed clean and repeatable across multiple fresh single-attempt modem boots**:
- `GLINK_CMD_OPEN` sent for `DIAG_DATA` → modem.
- Modem sends `GLINK_CMD_OPEN_ACK` back, consistently in <10ms when it responds at all.
- **The modem never sends its own reciprocal `GLINK_CMD_OPEN` back** — the other half of glink's
bidirectional handshake `qcom_glink_create_local()` requires (`wait_for_completion_timeout(&channel
->open_req, ...)`) before a channel counts as usable.
- Tested whether this was just slow: bumped the wait from 5s→60s (kernel commit `7441ca3dc`), rebuilt,
reflashed, ran ONE clean attempt on a fresh modem boot, waited the full 63.84s. **Still nothing.**
Falsified the "just needs more time" theory outright. Reverted the bump (`b4185e71b`) — no reason
to ship a 60s stall for a case that structurally doesn't complete.
- Cross-checked against downstream (`references/los-kernel-blueline-4.9/drivers/soc/qcom/glink.c`):
its `GLINK_CONNECTED` notification requires the exact same bidirectional
`ch_is_fully_opened(ctx)` condition (checked from both the incoming-open and the open-ack RX
handlers) — downstream isn't looser about what counts as "open," it's just event-driven with no
hard timeout instead of blocking-with-timeout. So this isn't mainline enforcing an extra
requirement Android doesn't have; it's the same requirement, and the modem doesn't satisfy it
either way under mainline's bring-up.
**What this means**: the modem's DIAG service is reachable and responsive at the glink-negotiation
level (it correctly acks a request for a channel it recognizes by name) but will not commit to
opening it — a "deliberate partial refusal" shape that rhymes with the *entire* rest of this frontier
(SIM/QMI all healthy, DMS answers reads, refuses RF-arming writes; modem parks `offline` not `LPM`).
Whatever state gate blocks RF-arm plausibly also gates DIAG. **F3 access via this channel is not
reachable by any change on the AP side alone** — this is now a real, evidenced, narrow finding, not
an assumption. Retest if/when the LPM-vs-offline or any other frontier item moves; a modem that
reaches a healthier init state might complete this handshake as a side effect.
Test binary + method preserved: `diag_open_test.c` (cross-built via `pmbootstrap chroot -b aarch64`),
`RPMSG_CREATE_EPT_IOCTL` against `/dev/rpmsg_ctrl2`, then `open()` the resulting `/dev/rpmsgN` — same
approach works for probing ANY named glink channel on ANY edge (swap `/dev/rpmsg_ctrl0/1/2` for
cdsp/adsp/modem) if a similar question comes up for a different service.
---
## TOP LEAD 2026-07-02 #14 — frontier #8's 3-precondition test finally run: provisioning-churn fixed & shipped, LPM-vs-offline confirmed unreachable from AP side
**Fixed and shipped precondition (b) from frontier #8 below**: `blueline-uim-select.sh` was
unconditionally deactivating+reactivating the primary-gw provisioning session even when it already
matched the slot/AID it was about to select — churn on an already-correct session, and exactly the
class of write the raw-QMI RESULTS.md proved times out during the modem's not-ready window. Patched
to skip the deactivate/reactivate when the existing binding already matches
(`device-google-blueline-modem-tools` 7-r18→7-r21, committed `1ed2df1788`). **Verified live**: the
log now reads "primary-gw session already bound to slot 1 app 1 ... skipping stock
deactivate/reactivate" — the session is genuinely left alone across a modem restart for the first
time this whole investigation.
**Result: does NOT flip DMS online.** Immediately after the clean-session restart,
`--dms-set-operating-mode=online` still returns `DeviceNotReady (52)`. Polled operating-mode every
3s for 18s post-restart: **steady `offline` the entire window, never once observed `LPM`.** So
precondition (c) — "modem in LPM (not offline) for the online flip" — is now confirmed, not just
suspected, to never occur spontaneously on pmOS: the firmware decides `offline` vs `LPM` before any
AP-side QMI write is even possible, so no host script/wrapper/choreography (this fix included) can
create the window frontier #8 needed.
**Where this leaves it:** frontier #8's 3-precondition experiment (configs active + clean
provisioning + LPM-not-offline) is no longer "untested" — it's tested, with (a) and (b) now cheaply
satisfiable and (b) fixed for real, and (c) confirmed firmware-internal and unreachable from any
AP-side lever. This converges with TOP LEAD #13's conclusion (SLPI/ADSP/fastrpc/choreography all
closed, F3 narration is the remaining instrument) via a different path — both land on "the
offline-vs-LPM decision happens inside the firmware before Linux gets a vote." **Next: same F3
instrument #13 named, now aimed at a sharper question** — not generic RF-init narration, but
specifically why the firmware selects `offline` over `LPM` at its own boot, since that's now the
single remaining precondition standing between here and an online modem.
Also confirmed same session: mainline blueline has **no DIAG transport at all** (`/dev/diag` absent,
no `diagchar`-equivalent config, no `DIAG` glink channel among the live rpmsg devices) — so
`diag_capture.md`'s Side B is a real, confirmed dead end on the AP side as of kernel
`7.1.1_p20260702080259-r2`; F3 access (if pursued) needs either a from-scratch DIAG-over-glink
kernel path or stays Android-side-only (Side A) for comparison purposes.
---
## TOP LEAD 2026-07-01 #13 — fastrpc `pageslen` kernel bug FIXED & shipped; 52 wall now proven independent of adsprpcd/ADSP health too
**Real, named, diff-attached kernel defect — the first of the whole investigation — found, fixed, shipped, verified.** `fastrpc_init_create_static_process()` in `drivers/misc/fastrpc.c` advertised `inbuf.pageslen = 0` while actually passing one remote_heap page in `args[2]`; the sibling dynamic `fastrpc_init_create_process()` correctly sets `pageslen = 1` for the identical one-page layout. The DSP trusts the metadata field, believes zero pages were passed, and aborts `INIT_CREATE_STATIC` — surfacing in userspace as `Broken pipe` and the adsprpcd coredump cascade (`dsp information is incorrect err: -1` for ADSP+CDSP). One-line fix (`0``1`); matches `FisiFla/nabu-cachyos` `0005-fastrpc-fix-init-create-static-pageslen.patch` (SM8250, independently shipped for the identical signature, commit claims it also killed an SLPI crash loop there). Committed `e3c47d44e` in `references/linux-7.1.1-sdm845-CANONICAL`. **Also committed the previously-floating-uncommitted RTC read-only fix** (`68ffb014b` — the load-bearing half of the 0-SPMI-writes result; it had never been committed, only sitting in the worktree). Both shipped in kernel `7.1.1_p20260701093924-r1`, flashed to `boot_a`.
**Process correction (saved to memory `pmos-src-skips-apkbuild-patches`):** `pmbootstrap --src` builds the **working tree as-is**, not committed-only. A first build silently included the two then-uncommitted RTC/DTS files; caught by inspecting `chroot_native/mnt/pmbootstrap/source-override/`. "Bake as commits" is necessary but not sufficient — the worktree itself must be in the intended state at build time.
**Verified live post-flash:**
- **adsprpcd: no longer a fatal crash-loop.** Previously coredumped every boot with `dsp information is incorrect` / `Broken pipe`. Now: coredumps ONCE on first probe, systemd restarts it (NRestarts=1), then runs stably — **successfully opens `adsp_default_listener` handles on ADSP (domain 0)**. Old fatal signatures absent from the journal. Residual non-fatal noise remains: `remote_handle64_invoke ... method 2 ... Operation not permitted` + `open_mod_table_handle_invoke ... Error 0xe` on domain 0 — a *separate* ADSP-invoke-permissions issue, not the pageslen bug; next fastrpc thread if pursued.
- **SPMI write flood: still 0** — RTC read-only fix preserved across the rebuild.
- **`/dev/fastrpc-sdsp` still ABSENT** → `hexagonrpcd-sdsp` still "Could not attach to FastRPC node: Operation canceled". The pageslen fix targets ADSP/CDSP static-process creation; the SDSP node is a different path (depends on SLPI remoteproc fastrpc edges), so the SLPI/sensor-island story is **not** changed by this fix.
- **dms-ui-ready (r17 C binary): ran clean**, same `DMS 0x0064 RESULT: FAILURE (result=1 error=58)` as before (unchanged, expected — orthogonal to fastrpc).
**Modem 52 wall: STILL THERE — now proven independent of adsprpcd/ADSP health too.** First `mmcli --enable` of the entire session with a non-crashing adsprpcd (all chain units active: modem-qrtr-ready, qcril-autoselect, qcril-prov, dms-ui-ready, ModemManager): identical `QMI protocol error (52): 'DeviceNotReady'` on `Set Operating Mode=online`, op-mode stays `offline`, state `disabled`, lock `sim-pin2`, SIM on slot 2 (correct). Combined with TOP LEAD #12's SLPI proof, **the modem's RF-ready gate is now robust to the entire host-side DSP coordination layer** (SLPI state × 3 boots, adsprpcd/ADSP health × 1 boot). The ADSP-PD/servreg candidate is substantially weakened. The wall is the modem firmware's own internal decision; nothing host-side we can move has touched it.
**Where that leaves the candidate space:** of `los-mss-translated.c`'s four deltas — (1) EFS NV item, (2) bootloader/ABL verified-boot state, (3) subsystem coordination [SLPI/PD], (4) host-QMI-replay timing — #3 is now closed and #4 is substantially exhausted (DMS 0x0064 sends+responds but error 58; NAS mode-pref fails DeviceUnsupported pre-RF). #1/#2 were researched to ground with no static delta. The one instrument that could show the modem's *own* RF-init reasoning — F3 narration — remains uncaptured (QCSuper's blanket arm skips the NAS/RRC SSID ranges; per-range SET_RT_MASK arms cleanly but yields no traffic, and we lack a Qualcomm SSID name table to confirm those ranges *are* NAS/RRC). That F3 capture + SSID identification is the most likely remaining source of new signal.
---
## TOP LEAD 2026-07-01 #12 — SLPI independence now PROVEN (not just argued); DMS 0x0064 "UI ready" built, untested
**SLPI-state dependency is now definitively closed, with hard repeated evidence** (2026-06-30
night → 2026-07-01, three separate boots, live hardware). Fixed a real regression along the way:
`blueline-slpi-ready.sh` disabled SLPI crash-recovery for a "single-shot" start; when the SAR/CHRE
firmware fault crashed it, the disabled-recovery path left the glink-edge channel permanently torn
`intent request timed out` storming every ~10s indefinitely, which took the modem's own QMI/QRTR
transport down with it (`endpoint hangup`, `Connection reset by peer`). Fix: recovery must stay
**enabled** (matches Android's `ssr_setup`/`ssr_diag`, which never disables it) so the kernel's own
crash handler tears down/rebuilds glink cleanly; the script then does ONE clean attempt and parks
SLPI (`echo stop`) once caught in a healthy `running` window, instead of crash-looping forever.
Verified over three clean boots: SLPI crashed (0 times / 50 times / 0 times depending on timing),
**every single time `DeviceNotReady` on RF-enable was byte-for-byte identical** regardless of SLPI's
state (crashed continuously, running, offline, or cleanly parked). Also wired `modem-qrtr-ready.service`
`After=`/`Wants=blueline-slpi-ready.service` to match Android's real architecture — `vendor.qcom.devup`
(what `modem-qrtr-ready` mirrors) is **literally set by** `init.qcom.devstart.sh` right after it kicks
SLPI (`echo 1 > /sys/kernel/boot_slpi/boot; setprop vendor.qcom.devup 1`), confirmed by reading the
real Android script off the mounted LOS vendor image. Architecturally correct, safe, zero cost — but
does not move the 52 wall. **Candidate #4 ("Subsystem coordination — SLPI PD state?") in
`los-mss-translated.c`'s delta list is now closed.**
**Checked and closed a real diagnostic gap**: `qmicli --dms-get-operating-mode`'s output includes an
optional "Offline reason:" line (libqmi `qmi_message_dms_get_operating_mode_output_get_offline_reason`,
bitmask of `HOST_IMAGE_MISCONFIGURATION`/`PRI_IMAGE_MISCONFIGURATION`/`PRI_VERSION_INCOMPATIBLE`/
`DEVICE_MEMORY_FULL`) that no prior capture had actually looked for. Checked the existing captured
output (`tools/modem-online-provisioner/modem-online-provision-20260622-063154.log`): the field never
appears — the modem doesn't populate any of the four documented reasons. Dead end, but a clean one.
**Built AND tested: `dms-ui-ready.py`/`.service` + a raw-C fallback. Sends clean, modem responds,
52 unchanged.** `PAF/qcril_online_sequence.md` (prior session, full disassembly of
`libril-qc-hal-qmi.so`) identified DMS message **0x0064** ("telephony UI ready") as the one
load-bearing pre-online step QCRIL performs that pmOS had never replayed. The Python/GI
implementation (`Qmi.Message` + raw TLV writers) **failed at runtime**: this libqmi build does
not expose a generic raw-message type via GObject-Introspection at all — only per-message typed
classes exist in the typelib, confirmed by enumerating `dir(Qmi)` live on the phone. Built a
small C program instead, cross-compiled against the same libqmi (openimsd fork, 1.39.0) via
`pmbootstrap chroot -b aarch64`, linked directly against the `.so` already on the phone. Ran it
live: the modem answered 0x0064 with a real, understood protocol response (not "unsupported
message"), `RESULT: FAILURE (result=1 error=58)`. Error 58's standard libqmi meaning
(`WMS_ENCODING`, an SMS-service code) can't be its true meaning for an OEM DMS message — almost
certainly a vendor-private error space reusing that numeric slot. Immediately retried
`mmcli --enable`: **`DeviceNotReady` (52), byte-identical to every attempt all night.** Full
detail + the C source path in `qcril_online_sequence.md`. Kept `dms-ui-ready.service` wired in
the boot chain (best-effort, harmless, now proven to do something real) — the Python version in
the package needs porting to the same raw-C approach before it'll actually work at boot; that's
the concrete next step, not another blind guess.
---
## TOP LEAD 2026-06-26 #11 — THE 52 IS A MODEM XPU (SECURE-MEMORY) CRASH-LOOP
> **🔴 SUPERSEDED 2026-06-29.** Captured on the **pre-DELTA-1 r2 LLVM kernel**, where the modem
> genuinely crash-looped on an XPU violation. **DELTA-1 (`c2923cb49` MPSS assign-ordering) fixed the
> load** — on the current kernel the modem MSS runs *steady* (MBA boots, MPSS loads, `remoteproc
> running`, **no XPU crash-loop, zero 22 on `4080000`**; the only crash-lines are the SLPI's
> `SNS_REG_INIT`). Yet it **still parks OFFLINE / model 0 / 52**. So **52 is NOT an XPU crash-loop**
> — it's a firmware-internal RF-init decision, kernel-independent (6.16.7 and 7.1-rc1 both give 52,
> see #10). The 22 referenced below is the SLPI's fastrpc heap. Kept as the XPU/load investigation
> record.
**Captured live on the rebuilt r2 LLVM kernel (slot A, full system, ssh).** The
`DeviceNotReady (52)` wall is now explained at the mechanism level: the modem
**boots then crash-loops on a hardware secure-memory protection violation**, so it
can never complete RF-init and parks `offline`. dmesg (`qcom-q6v5-mss
4080000.remoteproc`, remoteproc3):
```
Booting fw image qcom/sdm845/Google/blueline/mba.mbn ... MBA booted ... loading mpss
remote processor 4080000.remoteproc is now up
crash #1: fatal error received: SFR Init: wdog or kernel error suspected.
crash #2: fatal error received: modem_xpu_int.c:456:[modem_sec_xpu_I]XPU violation
... port failed halt ... modem offline ... starting ... up ... crash ... (loop)
```
**XPU = Qualcomm eXtreme Protection Unit** — hardware memory guard programmed by
TrustZone. `modem_sec_xpu XPU violation` = the modem firmware accessed a memory
region the XPU does not permit it → the modem's **secure memory regions are not
assigned/programmed correctly** for mainline. The modem comes up, touches a
protected region, XPU faults it, q6v5 recovers it, repeat. The "running" state in
a one-shot `remoteproc/state` read is just a momentary up-window between crashes —
do not trust a single-sample `running`; watch `journalctl -b -p err` over ~30s.
**This is the same TZ layer as the SLPI/fastrpc failure.** Every modem AND slpi
restart also logs `qcom_scm firmware:scm: Assign memory protection call failed -22`
(hyp-assign / `qcom_scm_assign_mem` → -EINVAL). The fastrpc patch *tolerates* it;
the modem cannot — it needs its secure regions actually assigned. **Shared root:
this blueline's TZ rejects mainline's secure-memory-assign, → modem XPU violation
→ 52.** See `secure_load_tz_frontier.md`.
**Ruled OUT as the cause of 52 (this session, hard evidence):**
- *Provisioning / MCFG* — software MCFG `Rogers_Canada` (Casey's real carrier) is
`Active` (PDC `--pdc-list-configs=software`). Selecting the carrier config — the
thing qcril/Set-Feature does on Android — is already done, and 52 reproduces
anyway. Platform configs = 0 (blueline EFS gap) and PDC Set Feature 0x2E times
out, but neither is the 52: the modem crashes below the QMI layer.
- *Kernel config regression* — 52 reproduces on the corrected 1135-module r2 LLVM
kernel with SLPI/SSC in. Not the GCC-stripped-config disaster.
- *libqmi* — confirmed the openimsd fork is installed (apk desc: "openimsd fork,
adds PDC Set Feature msg 0x2E"; ships `Qmi-1.0.typelib`). Not the blocker.
**[RETIRED 2026-07-01 — see HANDOFF CORRECTIONS]** ~~The one-per-boot online attempt was
spent this boot ... Modem now poisoned until power-cycle.~~ Online attempts are retry-safe
within a boot; QMI `Set Operating Mode = RESET (4)` cleanly reinitializes, no reboot needed
(`tools/modem-online-provisioner/RESULTS.md`). Kept for history: that boot's attempt gave 52,
mode stayed `offline`, serving-system not-registered/detached, signal InformationUnavailable.
**Next frontier:** make TZ accept the modem's secure-memory assignment — i.e. the
XPU/hyp-assign region setup. Compare LOS pil-tz secure-load (mba region
`qcom,mba-mem`, mpss region assign, `qcom_scm_assign_mem` VMID lists) vs mainline
`q6v5_mpss` region handling on this device. The `assign -22` is the thread to pull.
(qmicli also **segfaults** on `--pdc-list-configs` — separate libqmi/fork bug to log.)
## TOP LEAD 2026-06-21 #10 — KERNEL-REGRESSION HYPOTHESIS: DISPROVEN. Kernel layer ruled out airtight.
**Result (clean-boot test, 2026-06-21 eve):** built blueline on `sdm845-6.16.7-r0` (last pre-regression
stable, 2025-09-19), flashed boot_b, booted clean. The modem boots **fully** on 6.16.7 — firmware
loads (`Booting fw image …/mba.mbn``remote processor is now up`), ALL QMI services register
(qrtr-lookup shows DMS/NAS/UIM/Voice/WDS/PDC…), UIM is healthy (card present, USIM `ready`, Primary-GW
slot1/app1), `remoteproc2` = `running`. Yet `--dms-set-operating-mode=online` → **DeviceNotReady (QMI
52)**, mode stuck `offline`, Networks `''`, Max TX/RX 0 — **byte-for-byte identical to 7.1rc1.** A
second run on a *clean* boot (modem auto-started at boot-time, UIM freshly provisioned) reproduced the
same 52, removing the manual-start confound. Per this lead's own triage rule (still-52 on the
pre-regression kernel) = **the kernel is ruled out airtight.** The online wall is NOT a kernel
regression; it is device-internal (Route B) — the modem's own firmware RF-init never self-completes, so
it parks `offline` and refuses `online` (Android never has to ask; see frontier #8 REFRAME).
6.16.7 is now a known-good working kernel for blueline (modem boots clean); either 6.16.7 or 7.1rc1 is
fine going forward (both give 52). **The live frontier is Route B — see `frontier #8 REFRAME` below.**
### Firmware-path gotcha found + fixed (RECORD — bites any upstream-tree blueline DTB)
The 6.16.7 *upstream* `sdm845-google-blueline.dts` requests firmware at
`qcom/sdm845/pixel3/{mba,modem,adsp,cdsp,ipa_fws,a630_zap}.mbn`, but pmaports packages blueline
firmware at `qcom/sdm845/Google/blueline/`. So the upstream-tree DTB silently fails `request_firmware`
(ENOENT -2) → modem/ADSP/CDSP never boot (`remoteproc: Boot failed: -2`). The 7.1rc1 *pmaports* DTB
uses `Google/blueline/` (correct) — which is why 7.1rc1 loaded firmware and 6.16.7 did not until fixed.
**Fix applied on the rootfs (persists across reboots):**
`ln -snf /lib/firmware/qcom/sdm845/Google/blueline /lib/firmware/qcom/sdm845/pixel3`
Any future build using the upstream blueline DTB needs this symlink (or a DT patch correcting
firmware-name paths) or the modem will not load firmware. (Stray: the 6.16.7 DTB also references
`qcom/sdm845/oneplus6/venus.mbn` for venus — harmless, video-decoder only.)
### Kernel-swap procedure (RECORD — the modular-modem-stack gotcha)
The blueline modem stack is **fully modular** on this config: `qcom_q6v5_mss`, `qrtr`/`qrtr-smd`,
`qcom_glink_smem`, `qcom_sysmon`, `mhi*` all `=m`. So swapping kernels needs the matching modules on the
rootfs or the modem never loads (a false 52). UFS + USB-gadget-NCM are built-in (`=y`), so a
module-mismatched boot still reaches rootfs + USB. Working procedure (validated this session):
build kernel .apk → `tar xzf` it → extract `lib/modules/<kver>/` → scp to phone → `sudo cp -a` into
`/lib/modules/` + `depmod <kver>` → reboot. Boot.img = `cat vmlinuz <blueline>.dtb` + the standing
initramfs, `mkbootimg` (header v0, base 0x0, kernel_off 0x8000, ramdisk_off 0x1000000, tags 0x100, the
real cmdline `quiet splash … pmos_boot_uuid=2CE8-836F pmos_root_uuid=26733d78…`). USB iface
`enp0s20f0u6` (cdc_ncm) needs manual bring-up each boot: `nmcli dev set … managed no; ip addr add
172.16.42.2/24 dev …; ip link set … up`.
### Original #10 thesis + bisect-window analysis (HISTORICAL — hypothesis disproven above)
Thesis (Casey): the 7.1rc1 bump (pmaports MR !8446, merged 2026-04-28, "basic smoke testing… Pixel 3")
never validated blueline's modem; the modem may have regressed there. Corroboration: beryllium wiki
flags 7.1.0-rc1-sdm845 for modem breakage on the same SoC; Joel Selvaraj had SDM845 modem working on
mainline back at 5.11, so the capability predates the regression. **Running test:** rebuild blueline
kernel against `sdm845-6.16.7-r0` (last pre-regression stable, 2025-09-19), flash boot_b, retry
`--dms-set-operating-mode=online`. Outcomes: online → regression confirmed, bisect the bump; flaky
like beryllium → kernel layer validated, inherit beryllium recipe; still DeviceNotReady(52) → kernel
ruled out airtight, it's device-internal firmware/EFS state (Route B).
### Kernel-tree pull (2026-06-21) — what the AP-driver diff says about the bisect window
Pulled `references/sdm845-linux-20260610-ONHOLD-DAVID-TREE` (codeberg `sdm845-next-20260610`, base **7.1.0**) and the
`sdm845-next-20260306` tarball (base **7.0.0**) and diffed the modem-relevant subsystems:
- **`drivers/remoteproc/qcom_q6v5_mss.c`: functionally identical** 7.0→7.1 (brace-whitespace only).
- **`drivers/soc/qcom/pdr_interface.c` + `pdr_internal.h`: rename-only** — `SERVREG_LOCATOR_SERVICE`
`QMI_SERVICE_ID_SERVREG_LOC` (both 0x40), `SERVREG_NOTIFIER_SERVICE``QMI_SERVICE_ID_SERVREG_NOTIF`
(both 0x42), `reason[257]``reason[SERVREG_PFR_LENGTH+1]`(=257). No behaviour change.
- **`qcom_pd_mapper.c` present in BOTH trees** → the in-kernel pd-mapper is NOT new to 7.1 (already
in-kernel by 20260306 / 7.0.0; it landed ~mainline 6.13, so 6.16.7 has it too). The "in-kernel
pd-mapper is the regression" sub-hypothesis is weak; the **sdm845-mainline rebase** is the live one.
- `qmi_interface.c`, `qmi_encdec.c`, `rmtfs_mem.c`: identical 7.0→7.1.
**Implication for the bisect:** in the codeberg 7.0→7.1 window there is NO functional change in
mss/pdr/qmi/rmtfs — so if the regression is real and in that window, it is NOT in those drivers
(look at clk/regulator/interconnect/scm/smem/glink/IPA instead). **Caveat:** pmaports tracks
`gitlab.com/sdm845-mainline/linux`, NOT codeberg `sdm845/linux` (the two repos the Matrix thread
argues over), and my window (7.0→7.1) does not cover the 6.16.7→7.0 jump. So this scopes, it does
not pin — Casey's 6.16.7 build is still the right swing.
### Upstream blueline DTS confirms our DT is already correct (kills #9 a second way)
The 20260610 tree ships an **in-tree `sdm845-google-blueline.dts` + `sdm845-google-common.dtsi`**
(model "Google Pixel 3"). Its modem nodes: `&mss_pil` = `blueline/mba.mbn`+`blueline/modem.mbn`,
`&ipa` = `blueline/ipa_fws.mbn`, `mpss memory@8e000000` size `0x9800000`, `rmtfs-region@f2700000`
vmid MSS_MSA. **`dt-work/blueline-pmos.dts` already matches all of these byte-for-byte.** Zero
WTR/QFE/RFFE/FEM/GRFC regulator nodes in the upstream blueline DTS — same as fajita, same as pmOS.
So the pmOS modem DT is upstream-aligned, not a fajita guess. RF power is modem-firmware/RFFE driven
(see `dt-work/rf-rail-diff-FINAL.txt`). **Lead #9 (add 11 pm8998 LDOs) is refuted** by (a) the
upstream blueline DTS not declaring them either, and (b) all 11 candidate LDOs showing use=0 from the
AP on live working Android. Do NOT flash `dt-work/blueline-live.dtb` for this reason.
### qrild finding folded in — there is no hidden online handshake
`references/qrild` (abandoned AOSPM RIL): its modem bring-up (`qrild_android_modem.cc` ctor) is
DMS Set Operating Mode RESET → powerup → NAS register-indications, with `setRadioPower` a no-op stub.
Same levers ModemManager already pulls — **no secret QMI sequence** that arms RF. This corroborates
the #8 REFRAME (`qcril_bringup_capture.md`): on Android the modem firmware SELF-onlines at its own
RF-init; replaying a "QCRIL/RIL online handshake" on pmOS is a dead end because there isn't one.
---
## (REFUTED 2026-06-21) #9 — pmOS DT under-defines the pm8998 regulator set
Refuted by #10 above (upstream blueline DTS does not declare these LDOs either; all 11 are use=0 on
live Android) and by `dt-work/rf-rail-diff-FINAL.txt` (RF power is firmware/RFFE-driven, not AP-DT).
Kept for the record; do NOT flash the LDO DTB. Original reasoning below.
Direction (Casey + GLM): the fix is in OUR pmOS implementation/DT, make it mimic Android. Concrete,
evidenced bug found this session:
**Our mainline blueline DT instantiates only ~14 of the 24 pm8998 LDOs. Android defines all of them.**
Missing from `dt-work/blueline-pmos.dts` rpmh-regulators (present in downstream Android DTB):
`ldo4, ldo6, ldo8, ldo9, ldo10, ldo11, ldo15, ldo16, ldo18, ldo23, ldo27`.
Voltages (from `dt-work/android-dtb/android-base.dts`, full diff in `dt-work/regulator-diff-android-vs-pmos.txt`):
ldo6=1.856V ldo8=1.2V ldo9=1.704V ldo10=1.704V ldo11=1.0V ldo15=1.8V ldo16=2.704V ldo18=2.704V ldo23=3.0V.
Why this matters even though all 11 are `deviceless`/use=0 in BOTH regulator_summaries: **the modem
votes its RF-frontend rails (WTR5975/PA/tuners) directly via RPMh, not the AP regulator framework.**
If the AP DT doesn't define a rail in the rpmh-regulators node, it isn't in the Linux RPMh regulator
set — so when the modem's RF-init requests it, the vote can't be satisfied/arbitrated. Modem RF-init
fails → modem parks in `offline` and refuses LPM/online (DeviceNotReady / silent no-op). This fits
every symptom: modem boots (DSP up) but RF never inits; `offline` won't flip; configs don't help.
Supporting evidence this session:
- Modem FATAL crash in pmOS dmesg: `qcom-q6v5-mss: fatal error received: ...Asserted in
wlan_vdev.c:_wlan_vdev_create` + `port failed halt` on stop (subsystem not halting cleanly →
modem can't flush NV → MCFG selection lost across restart → 0 configs every boot).
- Modem comes up `offline` (not LPM like Android); `set low_power`/`online` return rc=0 but mode
never changes; PDC autoselect (Set Feature) does NOT persist across modem restart (RIL-layer, not
firmware NV).
### Executable plan (start here next session — do NOT re-derive):
1. **Diff our blueline DTS rpmh-regulators block vs UPSTREAM linux-next `sdm845-google-blueline.dts`**
(Heidelberg's). Our pmaports kernel build may predate the full regulator set. If upstream already
defines all LDOs, bump/patch the kernel DTS; if not, add them.
2. **Add the 11 missing LDOs** to the `pm8998-rpmh-regulators` node in the blueline DTS at the Android
voltages above; mark the RF-candidate analog rails `regulator-always-on` so they're up at RF-init.
3. Build via pmbootstrap (`linux-postmarketos-qcom-sdm845`), flash `boot_b` (see platform.md), boot.
4. `dmesg | grep -iE 'regulator|vreg|q6v5|fatal'` — confirm no rail/halt errors; then
`--dms-set-operating-mode=online``--dms-get-capabilities` Networks non-empty = RF armed.
5. **Leverage other devs (Casey): do NOT silo.** Cross-check the SDM845 mainlining wiki, sargo/bonito
(Google sister devices) DTS, and the `#sdm845:postmarketos.org` channel for how RF rails are
declared. Heidelberg's blueline series on lore.kernel.org is the authoritative reference.
Artifacts: `dt-work/regulator-diff-android-vs-pmos.txt`, `dt-work/android-dtb/android-base.dts`
(decompiled downstream DTB), `dt-work/blueline-pmos.dts`.
---
The modem is a self-contained DSP (MPSS) running Qualcomm firmware. Linux only loads it, powers
it, and talks QMI to it. RF is the modem's own internal state machine — Linux cannot drive RF
directly. Read this stack bottom-up; the blocker is at L6.
## The bring-up stack
| L | Layer | What it is | Status (2026-06-19) |
|---|---|---|---|
| L0 | Silicon / RF | SDM845 SoC; **SDR845 + WTR5975** transceivers; pm8005/pm8998 PMICs | fixed HW |
| L1 | Firmware load | `mba.mbn` (238 KB) → `modem.mbn` (67 MB MPSS), via remoteproc `q6v5-mss` | healthy — "remote processor is now up" ~12.8 s |
| L2 | Power | rpmhpd domains `cx`/`mx`/`mss`; `mss``mss.lvl` ARC→`pm8005_s2_level` | healthy — `q6v5` votes `mss` INT_MAX; verified vs Android |
| L3 | Transport | QMI over QRTR; `glink-edge` to the modem PID | healthy — all QMI services register |
| L4 | Host services | `rmtfs` (EFS), `pd-mapper`, `tqftpserv`, `qrtr-ns` | **CRITICAL 06-19**: rmtfs binary was replaced (69KB→141KB) and config changed from `-r -P -s` to `-P -s`, causing EFS corruption + modem crash + system freeze at "[stack] dump". Original binary at `/usr/bin/rmtfs.orig-20260618` must be restored. **CORRECTION (06-19 #2): `-r` = read-only is WRONG for arming the modem — Android serves EFS read-write and the modem writes modemst2 on boot. Use `-P -s` (read-write). The 02:38 corruption was the wrong binary, not the rw flag.** |
| L5 | SIM / UIM | card detect, app select, primary-GW provisioning | healthy — `usim (2) ready`, slot1/app1 |
| **L6** | **RF arm** | `DMS Set Operating Mode → online` arms the RF stack | **BLOCKED — DeviceNotReady (QMI 52).** The 06-19 #2 "rmtfs read-only" root-cause is **DISPROVEN (2026-07-02)**: rmtfs runs `-P -s` read-write live (drop-in enforced, all EFS partitions present) and 52 persists. rmtfs ruled out. Real cause is the modem's own boot-time RF-init decision (Route B), not an AP EFS-write path. |
| L7 | Data / voice | IPA/rmnet, ModemManager, calls/SMS/data | gated behind L6 |
**The blocker, precisely:** `DMS Set Operating Mode=online`**DeviceNotReady (QMI 52)**;
`DMS Get Capabilities` → zero channel rates / empty networks = the RF stack never initialized
inside the DSP. Everything upstream (L1L5) is proven healthy. ModemManager reaches `enabling`,
hits this, falls back to `disabled`, cycles.
## Eliminated leads — DO NOT RE-CHASE
Each killed with proof. The "proof" column is why it's dead, not a hypothesis.
| Lead | Verdict | Proof |
|---|---|---|
| rmtfs NV-files / `msm-cros-efs-loader` (serve EFS from file copies) | DEAD ( UPDATED 06-19 ) | **REAL CAUSE FOUND**: rmtfs binary was replaced (69KB→141KB) on 06-19 02:38 and service config changed from `-r -P -s` to `-P -s`. This broke EFS partition access, causing modem crash → "[stack] dump" → system freeze. Fixed by restoring `/usr/bin/rmtfs.orig-20260618` (69192 bytes) and resetting `/etc/systemd/system/rmtfs.service.d/writable.conf` to `-r -P -s`. The NV-files approach is dead; the original partition-mode rmtfs MUST be used. |
| `removed-region@88f00000` carveout (was "lead #1") | DEAD | Android iomem: `88f00000-8aafffff : System RAM`. The device whose modem **works** leaves it allocatable. OnePlus-specific quirk. (06-19) |
| cmd-db maps `mss.lvl` to wrong PMIC (was "lead #3") | DEAD | Android regulator tree: `4080000.qcom,mss → pm8005_s2_level` (correct rail). cmd-db is the same firmware blob on both OSes. pm8005 absent from pmOS `regulator_summary` is **expected** — mainline drives it via rpmhpd/genpd, not the regulator framework. (06-19) |
| `vdd_mss-supply` / SPMI `pm8005@4` in DT | DEAD | Mainline `q6v5-mss` is power-domains-only for SDM845 (no `devm_regulator_get`). `qcom,pm8005` has **no SPMI driver** in mainline → adding `pmic@4/5` panics the kernel. Only `smps1/2` under the rpmh-regulators-2 block is safe. (06-18/19) |
| "blueline lacks `soc-qcom-sdm845-modem` dep" | DEAD (stale doc) | Current pmaports pkg is `soc-qcom-modem`; blueline depends on it just like enchilada. Device-pkg deps + firmware manifests are equivalent. (06-19) |
| fcc-unlock missing | DEAD | `fcc-unlock` exists nowhere in pmaports; no SDM845 device uses one. Modem is not FCC-locked-waiting. (06-19) |
| Modem DT node differs from working device | DEAD | blueline modem node is byte-identical to fajita (same power-domains/clocks/resets); only firmware path + phandles differ. (06-18) |
| Storage / EFS calibration is missing | DEAD | rmtfs writable + Android-primed EFS (modem online in Android first) → still DeviceNotReady in pmOS. EFS content is not what's missing. (06-18) |
**Net:** every kernel / DT / power / packaging layer is proven equal to blueline-**Android**
(which runs the modem online, Fido LTE) on the **same silicon**. The blocker is not there.
## The frontier (REVISED 2026-06-20 #8 — QCRIL does more at bring-up; mainline stack under-initializes the modem)
Casey's standing thesis (correct, and the through-line of this whole effort): **QCRIL powers and
initializes more of the modem than ModemManager does** — and mainline pmOS, as a minimal port,
under-powers/under-initializes the board. We are hunting the specific delta. Evidence gathered:
1. **QCRIL sends a `'UI ready'` / power-on-optimization handshake to the modem that MM never sends.**
Android cold-boot capture (`android-live-20260619/deep/radio-boot.txt`): `qcril_qmi_nas_telephony_ready:
poweron_opt: send 'UI ready' to modem` (qcril_qmi_nas.cpp:17527), plus an active radio-power
state machine (`qcril_qmi_nas_update_radio_power_request_info``BOOTUP_RADIO_POWER_OFF`,
`qcril_qmi_nas_radio_power_transaction_handler`, state line `telephony LPM | ril NONE | card UP |
modem ONLINE`). ModemManager has no equivalent of this poweron_opt / UI-ready signalling.
2. **No DT RF node on either OS** (see retracted #6) — so the delta is QMI-level (modem-side
init/power), not a Linux regulator we can add. The mainline DT brings up cx/mx/mss + the basic
stack; it does NOT replicate QCRIL's QMI bring-up choreography.
3. **Provisioning race (real bug, partially validated).** `msm-modem-uim-selection` fails every boot:
it fires during the early QRTR window ("node with id 0 not found"), then its primary-GW provisioning
ACTIVATE times out → service exits FAILURE → the live `primary-gw-provisioning` session loops
`end-with-failure`/`app-reset`. Run manually AFTER boot it SUCCEEDS (`Successfully changed
provisioning session`). KEY: with provisioning freshly+cleanly active, `--dms-set-operating-mode=
online` **no longer returns DeviceNotReady(52)** (returns rc=0) — though the modem still stays
`offline` because PDC configs were 0 that boot. We have never had configs-active AND
clean-provisioning AND the QCRIL init handshake all at once.
4. **QCRIL online path captured (airplane off→on, `android-capture-20260620/qcril-online-capture.txt`):**
`qcril_qmi_nas_dms_handle_modem_operating_mode` (qcril_qmi_nas.cpp:18289) → "modem's operating mode
needs to be changed" → ril SETTING_OP_MODE → **modem LPM → ONLINE** in ~30ms (a plain DMS
set-operating-mode=online). post-transition: `set_bootup_power_optimization_state=2`, ATEL UI-READY,
enable networking/sys-info indications. **No hidden RF-power QMI command** — the modem is already
powered (LPM) and the flip arms RF. **KEY STATE DIFFERENCE:** Android modem starts in **LPM**
(low_power) and does LPM→online; the pmOS modem sits in **`offline`** (deeper state) and
offline→online is what fails. So the gate is the combination: (a) MCFG configs loaded, (b)
primary-GW provisioning active, (c) the modem in LPM (not offline) for the online flip. We have
NEVER had all three at once on pmOS — that is the decisive untested experiment.
**The decisive instrument (next):** a QMI-message-level capture of QCRIL's bring-up on the rooted
Android (enable QMI verbose radio logging, cold boot or airplane-toggle), then diff the QMI request
sequence against ModemManager's (we already have MM's via `--debug`). The delta = exactly what to
replay on pmOS (UI-ready/poweron_opt, any NAS/DMS/RF config QCRIL issues). Then replay on pmOS with
configs active + provisioning clean. Rooted Android (slot B) is the instrument; keep this doc current
as the capture lands.
---
## The frontier (REVISED 2026-06-20 #7 — live RF-init trace: blocker reframed to primary-GW provisioning, NOT RF cal)
Ran the decisive pmOS-side RF-init trace (slot B, kernel 7.1.0-rc1, modem over qrtr://0). Two
big results — one closes a thread, one opens a sharper one.
**The modem is not faulting and nothing it requests is missing.** dmesg: clean boot, mba→mpss→
running→handover, NO crash/coredump/fatal in the RF-arm window; an `online` attempt produces ZERO
dmesg delta. tqftpserv serves only the readonly MBN tree (read OK) + an empty `mcfg.tmp` scratch
file the modem RRQ/WRQ-loops on — and `mcfg.tmp` being 0 bytes is NORMAL (tqftpserv is bind-mounted
to the **persistent** persist partition `/dev/sdf2[/rfs]` — NOT volatile tmpfs as first feared —
and persist is SHARED with Android, so if the working modem needed content there it would persist;
it's empty on both). So serving is correct and complete. **The modem boots healthy and simply
REFUSES `online` at the QMI layer (DeviceNotReady = clean refusal, not a load failure).**
**NEW lead — primary-GW SIM provisioning session fails.** ModemManager's log during the refusal:
`refresh indication received: session type 'primary-gw-provisioning', stage 'end-with-failure',
mode 'app-reset'` — looping every MM retry (~72s), alongside a NAS system-info indication that
returns `FAILURE: InformationUnavailable`. Operating mode `offline`, **HW restricted `no`** (not
FCC/HW-locked), serving system fully detached, radio interface `none`. AND `msm-modem-uim-selection`
**failed this boot**: "Application was already selected" → `operation failed: Transaction timed out`
→ service exit FAILURE. Yet the static card read is healthy: Primary GW bound (slot1/app1), USIM
app `ready`, personalization `ready`, PIN1 `disabled`. So the binding exists but the live
provisioning SESSION keeps failing + app-resetting. This is distinct from (and more proximate than)
the RF-cal lead — the modem may refuse `online` because primary-GW subscription provisioning never
completes, not because RF is uncalibrated. (mBands=[] is non-diagnostic per #6; generic IMEI works
on Android per Casey — so cal/QCN lead is weak.)
**OPEN — causality not yet proven.** Why does the provisioning session end-with-failure while the
GW binding shows bound+ready? Candidates: (a) `msm-modem-uim-selection` timing out leaves the
subscription half-selected; (b) the modem triggers an app-reset refresh (post-MCFG) that nothing
on the AP side completes the way Android's RIL does; (c) downstream symptom of offline state.
**Decisive next test (fresh boot, one clean shot):** on a fresh slot-B boot, before MM enables,
(1) confirm whether the primary-gw-provisioning refresh fails on its own; (2) re-run UIM selection
cleanly (the service timed out) and retry `online`; (3) capture the UIM/provisioning QMI sequence.
Compare to what Android's RIL does on the same refresh. Known: a remoteproc reset (online is retry-safe, see CORRECTIONS)
both clear UIM provisioning, so this needs a clean boot.
---
## The frontier (REVISED 2026-06-20 #5 — MCFG SOLVED; online blocker isolated to device-specific RF bring-up)
Live pmOS run this session SOLVED the MCFG provisioning blocker AND, by doing so, proved MCFG was
never the RF-arm blocker. The online wall (`DeviceNotReady` QMI 52) survives every config/stack/SIM
fix. Full elimination, all on hardware (slot B, kernel 7.1.0-rc1-sdm845):
**SOLVED — persistent MCFG self-provisioning.** `qcril-autoselect.py` (PDC Set Feature 0x2E,
Selection Mode=3) made the modem self-load 2 platform (SS-LA-SDM845) + 25 software (Rogers_Canada)
configs from the tqftpserv tree; survives reboot (self-loads at RF-init). Both ACTIVE; software
version `0x801481A` == Android's exact value. The "0/0 every cold boot" blocker (frontier #3/#4)
is dead. Tooling: `tools/qcril-autoselect.py` + `tools/build-openimsd-libqmi.sh` (openimsd libqmi
fork, has Set Feature). Believed a Pixel 3 mainline first.
**RULED OUT as the online (RF-arm) blocker — every one tested on hardware:**
- MCFG/PDC — both configs ACTIVE, matching Android. online still 52.
- UIM/SIM — card present, USIM app `ready`, Primary GW slot1 app1 provisioned.
- timing/lock — MM stopped, 10 patient online retries over 30s, all 52.
- ModemManager — its full 9-step enable (power-state → op-mode indication register → Set Operating
Mode 0x2E=online) returns the same 52. Not a manual-qmicli artifact.
- missing stack — pd-mapper, hexagonrpcd, q6voiced, rmtfs (`-P -s` rw), tqftpserv ALL running;
`soc-qcom-modem` installed; PD JSONs (modemr.jsn/modemuw.jsn) + firmware present; no errors.
Matches working SDM845 devices (enchilada/fajita) component-for-component.
- EFS not served — modemst1/2, fsg, fsc all populated (~260K nonzero) + served. (modemcal sdd4 is
all-zero; persist sdf2 has data.)
- firmware mismatch — modem sw version `g845-00194-210812-B-7635520` IDENTICAL to Android;
revision `MPSS.AT.4.0.c2.17-00194-SDM845_GEN_PACK`.
**THE REMAINING BLOCKER = device-specific RF bring-up.** Same firmware + same shared EFS + complete
stack + DT byte-identical to fajita, yet: NAS `get-system-info` empty, `nas-get-rf-band-info`
no bands, mode stuck offline, `DeviceNotReady` on online. The modem boots but its RF never inits.
Two leads:
1. **RF cal / QCN** — generic test IMEI `990012001581294`, model `0`, no bands. Classic
no-calibration signature. UNCONFIRMED: does the WORKING Android show the REAL IMEI (→ pmOS isn't
loading cal/QCN) or the same generic one (→ red herring)? Decisive next test: reboot slot A,
read IMEI + DMS caps + bands; compare. (Not done autonomously — avoids leaving the phone
off-pmOS while unattended.)
2. **Board RF-frontend** — fajita (OnePlus 6T) works on mainline; blueline (Pixel 3) shares the
q6v5-mss DT node but has a different PMIC/RF frontend (WTR/QFE/antenna tuners). If blueline's RF
frontend needs power/GPIO/cal mainline doesn't provide for THIS board, the modem can't arm RF
even with identical firmware. (Distinct from the DEAD pm8005/mss-rail lead — this is the RF
frontend, not the modem-subsystem rail.)
### 2026-06-20 #6 — RETRACTED: do NOT treat the board-power lead as closed
I previously wrote this section as "lead #2 CLOSED with positive proof." That was an overclaim and
Casey was right to reject it. What the regulator/DT evidence below actually shows is narrower: there
is no *labeled RF-frontend regulator node* in the device tree — confirmed on BOTH mainline AND the
**downstream Android DTB** (`dt-work/android-dtb/android-base.dts`, decompiled from slot-A boot_a;
its `qcom,mss@4080000` declares only vdd_cx/vdd_mx/vdd_mss, no RF/WTR/antenna node anywhere). That
means we can't fix this by copying an "RF rail" node — NOT that board power is irrelevant. Mainline
blueline is a **minimal fresh port**: it instantiates only rails with explicit consumers and leaves
every other board rail off, whereas the downstream stack + QCRIL bring up far more (subsystems,
power-on handshakes, NV/RF init via QMI). The live lead remains: **what does the modem need powered/
initialized — via Linux rails the mainline port omits, or via QMI commands QCRIL sends — that pmOS
does not provide.** See the #8 frontier (top of file) for the QCRIL-does-more evidence.
(original text retained below for the evidence it contains)
Brought the "power Android provides" thread full circle on live hardware (Android slot A, rooted):
- **No Linux-side RF rail exists on EITHER OS.** In both the Android (`rf-compare/regulators-android.txt`)
and pmOS (`regulators-pmos.txt`) regulator dumps, the modem `4080000.qcom,mss` consumes ONLY the
Q6 DSP level rails (s6_level=CX, s9_level=MX, s2_level=MSS). There is no WTR/PA/tuner regulator
in Linux on Android either — RF-frontend power is driven by the modem firmware over RFFE + GRFC,
not the Linux regulator framework. So a "missing regulator in the pmOS DT" was never possible.
- **DT modem node is correct + complete** (`dt-work/blueline-pmos.dts`): power-domains cx/mx/mss,
xo/bus/mem clocks, glink "modem" transport, `gpio-reserved-ranges = <0 4, 81 4>` (Linux leaves
the modem-owned GPIOs alone). No regulator-supply phandles — correct for mainline q6v5-mss.
- **Pinctrl: no AP/modem GPIO conflict.** Android TLMM (`3400000.pinctrl`) owns only camera-CCI,
gpio-keys, and USB-extcon pins; the modem's GRFC/RF-control pins are MSS-internal and not
AP-routed. The AP cannot be stepping on an RF pin.
- **"No bands" is NOT a distinguishing signal.** The fresh working-Android radio capture
(`android-capture-20260620/android-radio-log.txt`) shows `mBands=[]` in CellIdentityLte while
fully attached to Fido LTE (REG_HOME, EARFCN 2000 = B4). So the empty mBands we saw on pmOS does
not by itself mean RF is dead — weakens (does not kill) lead #1's reasoning.
**Verdict:** every AP-side power/GPIO/pinctrl/DT input the modem receives is provably equal to
blueline-Android (which arms RF on the same silicon). Lead #2 is closed the same way IMEI closed
lead #1. The blocker lives strictly inside the modem firmware's RF-init, which the AP only feeds
via: firmware blobs, EFS (rmtfs), persist/rfs (tqftpserv), power-domains, clocks, QMI. All proven
present/equal — EXCEPT we have never traced what the modem *requests* from rmtfs/tqftpserv during
its RF-init, nor captured the modem's own failure reason.
**Decisive next experiment (pmOS-side, not yet run):** boot slot A, attempt `online`, and capture
(a) `tqftpserv`/`rmtfs` request trace during RF-init — which file/NV path the modem asks for and
whether it's served (frontier #4 showed Android rmt_storage *reads* on boot; we never logged the
pmOS read side), and (b) the modem remoteproc coredump / SSR reason on the failed online. This is
the one byte-for-byte trace we still lack; it points at the specific RF-init resource that differs.
Route B (autoselect EFS NV item) is DEPRIORITIZED: it would only persist the config state we
already achieved, and config-active does not fix online.
---
## The frontier (REVISED 2026-06-20 #4 — Android boot is VERIFY-ONLY; the modem persists MCFG itself)
Source: analysis of the captured Android radio logs in `android-live-20260619/deep/`
(`radio-boot.txt`, `radio-full.txt`, `ssr-capture.txt`), 2026-06-20. This **overturns** both
frontier #3's "just make the platform config active" and the `qcril_full_provisioning.md §D.4`
"load+activate every boot" model. Read this before either.
### What the captures actually show
Across all three logs (~12K PDC log lines, including a full modem SSR) QCRIL made **only query
calls** — never load/select/activate, never `pdc_enable_auto_selection`:
- `ssr-capture.txt:24293` — the decision point:
`qcril_qmi_pdc_is_modem_mbn_updated: prev_ver_info: MCFG-g845-00194-220517-B-8604334,
cur_ver_info: MCFG-g845-00194-220517-B-8604334` → **prev == cur → reload SKIPPED.**
- `ssr-capture.txt:23949``get_active_config_info_ind: config_version = 0x801481a`,
`Update CNV from 0x48 to 0x48` (0x48 = **72** = `cnv=[72]` = `persist.vendor.radio.mbn0=72`).
- Only PDC functions logged anywhere: `get_active_config_info`, `get_selected_mbn_config`,
`get_selection_carrier`, `is_modem_mbn_updated`. **Zero** `*_load_config`, `*_select_config`,
`*_activate_config`, `enable_auto_selection`, `enable_modem_update`.
### What that means
**Android's steady-state boot does almost nothing to PDC.** The modem holds its MCFG persisted in
its own EFS (`modemst1/2` — the 2 MB write to `modem_fs2` the `rmt_storage` capture caught),
**self-applies it at firmware RF-init every boot**, and QCRIL just confirms the active version
matches `persist.vendor.radio.cnv.ver_info` and stops. The full load→select→activate path runs
**only on first provision or an MCFG version change** (the `prev_ver != cur_ver` branch) — which
we have **never captured**, because every capture was an already-provisioned device. That is
exactly why `pdc_enable_auto_selection` never appears: it lives on the unobserved path.
### Consequences for the two prior theories
- **Frontier #3 ("just need an active platform config")** — necessary but NOT sufficient. PROGRESS
L165172 had BOTH configs active (SS-LA-SDM845 + Rogers) and DMS Networks stayed empty. Active
≠ armed. Don't chase "make it active" as the finish line.
- **`§D.4` ("load+activate every boot, accept two restarts")** — that is a *reconstruction of the
unobserved first-provision path*, not what Android does in steady state. It remains a valid
pmOS *workaround* (see `tools/qcril-prov.py`), but it is unproven on hardware and is not the
mechanism that actually arms RF on Android.
### The pmOS problem, restated from the evidence
The "0/0 configs every cold boot" volatility (PROGRESS L140144) is **not** something QCRIL fixes
per-boot. It's that **the modem cannot persist/restore its PDC selection in EFS the way Android
does.** The earlier "rmtfs read-write didn't fix it" result (ledger L38) is a chicken-and-egg:
writable EFS is useless if the modem never had a valid MCFG to commit, and a one-shot userspace
activate is useless if it cannot persist to survive the next firmware RF-init. **Both are
required: a valid config loaded AND modem-side persistence that survives reboot.**
### Two candidate paths forward
- **(A) Per-boot provisioner workaround** — `tools/qcril-prov.py` loads HW+SW, selects, activates
each boot. Mirrors the reconstructed first-provision path. Unproven; "active ≠ armed" is the
open risk.
- **(B) Provision-once + persistence (the Android mechanism)** — get a valid MCFG into the modem
once, then ensure `modemst` persistence so the modem self-restores + self-applies at RF-init,
and userspace reverts to verify-only. This is what the captures show actually works on hardware.
### The capture we still need (next experiment)
A normal-boot capture only re-shows the skip path. To record the **real first-provision load
sequence** (and settle whether `pdc_enable_auto_selection` / `enable_modem_update` is a required
precondition), **force a first-provision on Android**: clear `/data/vendor/modem_config/` and
`persist.vendor.radio.cnv.ver_info`, reboot, then capture. Runbook:
`PAF/first_provision_capture.md`.
---
## The frontier (SUPERSEDED 2026-06-19 #3 — see #4 above — PDC platform config is the blocker)
After a full day of Android-vs-pmOS forensics, the blocker is identified:
### What's actually wrong
The modem's **PDC (Policy Decision Control)** stores MBN configs in modem NV:
- **25 software (carrier) configs** loaded — **Rogers_Canada is ACTIVE** (133KB, v0x801481A)
- **2 platform (hardware) configs** loaded — **SS-LA-SDM845 is INACTIVE** (56KB)
Without an **active platform config**, the modem doesn't know its RF hardware capabilities.
DMS reports `Networks: ''`, NAS home network returns `NotProvisioned (QMI 16)`, and
`Set Operating Mode=online` returns `DeviceNotReady (QMI 52)`.
On Android, QCRIL (`libril-qc-hal-qmi.so`) loads MBN files from `/vendor/rfs/msm/mpss/readonly/vendor/mbn/`
via QMI PDC, activates both platform and software configs, and the modem restarts with full RF.
ModemManager does none of this — it assumes the modem is pre-provisioned.
### Attempted: direct PDC activation
`qmicli --pdc-activate-config=platform,6FE571A9...` returned **"Successfully requested config
activation"** but triggered a modem restart that **cleared the platform config** (0 configs after
restart). The activation request succeeded at the QMI level but the modem's internal handling
removed the config during restart. Needs Android QCRIL to re-load it.
`qmicli --pdc-load-config=...` **segfaults** on MBN binary files (qmicli 1.39.0 / libqmi 1.38.0).
Cannot load MBN files from pmOS side — libqmi bug or MBN format incompatibility.
### Key facts confirmed (06-19)
- **IMEI: identical** on Android and pmOS (`990012001581294`). NOT the blocker. Android works on
Fido LTE with this same test IMEI.
- **Modem firmware: identical** SHA256 (`97c1d19c...`) between slots A and B.
- **Modem crashes** (SFR Init / XPU violation): **intermittent** — some boots 0 crashes, some 2.
Even clean boots get DeviceNotReady. Not the blocker.
- **rmtfs DOES serve correct partitions**: partlabels are GPT-stable; UFS LUN renumbering changes
`/dev/sdX` names between boots but `by-partlabel` symlinks always resolve correctly.
- **DMS Get Capabilities** consistently shows `Networks: ''` and `Max TX/RX: 0` on pmOS.
Android shows populated networks (Fido LTE, Rogers 302720, full band support).
- **NAS Get System Selection Preference** shows FULL band tables on both OSes (43 LTE bands +
extended). NAS has the data; DMS doesn't. The bridge is the platform MBN activation.
- **Android rmt_storage**: reads EFS on boot (no writes), writes 2MB to modem_fs2 AFTER going
online. Normal operation — not initial provisioning.
- **pmOS rmtfs journal**: empty (rmtfs doesn't log successful reads/writes). No errors either.
### Fix path
1. Boot Android to let QCRIL re-load the platform MBN into PDC (it was there before activation)
2. Boot pmOS — do NOT touch PDC, do NOT run `qmicli` activation
3. Verify: `qmicli --pdc-list-configs=platform` shows SS-LA-SDM845 as Active
4. Verify: `qmicli --dms-get-capabilities` shows `Networks: 'gsm, wcdma, lte'`
5. Let ModemManager drive enable→online (NEVER manual qmicli online)
6. If platform config is still Inactive after Android: need to fix `qmicli --pdc-load-config`
segfault, or write a minimal MBN loader using libqmi directly
### What we have on device
- rmtfs: read-write (`-P -s`), serving correct EFS partitions via partlabels
- tqftpserv: bind-mounted to `/mnt/vendor/persist/rfs` (persist partition)
- pd-mapper: installed and running from boot
- MBN files: 179 files (11MB) at `/mnt/vendor/persist/rfs/readonly/vendor/mbn/`
(extracted from Android `/vendor/rfs/msm/mpss/readonly/vendor/mbn/`)
- MCFG version: `g845-00194-220517-B-8604334`
- Rogers Canada MBN: `mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn`
- SDM845 LA/SS platform MBN: `mcfg_hw/generic/common/SDM845/LA/SS/mcfg_hw.mbn` (56KB)
- Kernel: building from `sdm845/7.1-dev` HEAD (`85f1df2a`), config fixed to `sdm845`
### Android vs pmOS service comparison
| Component | Android | pmOS | Notes |
|-----------|---------|------|-------|
| EFS storage | rmt_storage r/w | rmtfs `-P -s` r/w | Parity |
| TFTP | tftp_server → vendor/rfs | tqftpserv → persist bind | Parity |
| pd-mapper | running (maps PDs) | running | Parity |
| RIL | QCRIL (`libril-qc-hal-qmi.so`) | ModemManager | **Missing PDC provisioning** |
| ipacm | running (`/dev/ipa` FDs) | not installed | Data path only |
| Keymaster | decrypts persist blobs | none | IMEI encrypted at rest |
| QMI services | comparable (50+) | comparable (50+) | Parity |
### (superseded) earlier frontiers
**Read-only rmtfs theory (06-19 #2):** rmtfs read-only prevented modem from persisting EFS
writes. Fixing to read-write didn't resolve DeviceNotReady. EFS r/w is necessary but not
sufficient — the modem needs valid MCFG in EFS first, which comes from PDC provisioning.
**QMI-handshake theory (original):** "MM doesn't send QCRIL's init sequence." Dead —
same MM binary works on OnePlus 6. The missing sequence is PDC MBN loading, not a
handshake gap. Other SDM845 devices likely have platform config pre-loaded from factory.
**fsg_oem theory:** Modem requesting `modem_fsg_oem_1/2`. Patched rmtfs to serve them.
Modem never actually requests these when EFS is properly provisioned. Symptom, not cause.
**XPU violation theory:** Modem crashes twice on some boots. Even on clean (0-crash) boots,
DeviceNotReady persists. Not the blocker.
## Useful commands (pmOS)
```sh
qrtr-lookup | awk '$1==11' # UIM service → note Port
qmicli -p -d qrtr://0/<port> --uim-get-card-status
qmicli -d qrtr://0 --dms-get-operating-mode
qmicli -d qrtr://0 --dms-set-operating-mode=online
qmicli -d qrtr://0 --dms-get-capabilities # zero rates = RF uninit
# clean modem re-init (rmtfs runs -P -s read-write; see 2026-07-02 correction):
echo stop | sudo tee /sys/class/remoteproc/remoteproc2/state ; sleep 3
echo start | sudo tee /sys/class/remoteproc/remoteproc2/state
# preferred: QMI Set Operating Mode = RESET (4) reinitializes without touching UIM/provisioning.
```
Forcing a remoteproc reset clears UIM provisioning; `msm-modem-uim-selection` only re-runs at
boot. (The old note here claimed `-r -P -s` read-only was required for re-provisioning — stale
and wrong; rmtfs runs `-P -s` read-write live and the modem re-provisions fine across a
stop→start. For a clean reinit that does NOT drop provisioning, use QMI RESET, not remoteproc.)
## Cross-references worth mining
- **Working SDM845 (same SoC):** OnePlus 6/6T (enchilada/fajita), Poco F1 (beryllium),
SHIFT6mq (axolotl) — data + SMS work via the same mainline kernel + pmaports packages.
Their *AP-side* config is already in `~/pmos/pmaports`; nothing to pull. Their working
firmware/EFS is device-specific and not transplantable.
- **Pixel 3a (sargo) / 3a XL (bonito):** the closest *Google* analogs with modem support in
pmaports (SDM670). Best target if a Google-specific RIL quirk is suspected.
- **Android on this exact device** is the cleanest reference: same silicon, modem works.
Diff blueline-Android vs blueline-pmOS, not against OnePlus.
## TOP LEAD 2026-07-02 #16 — locally-initiated DIAG_CTRL/DIAG_DATA open CRASHES the modem (glink_channel_migration assertion), repeatably
**Real, causal, previously-unknown finding — not a theory.** Following up #15's ack-but-no-reciprocal-
open result: tried the same locally-initiated open against `DIAG_CTRL` (not just `DIAG_DATA`).
Confirmed via `dmesg -T` (not my own client's stderr, which kept getting lost across SSH session
churn — the KERNEL'S OWN log line is the ground truth here): `rpmsg rpmsg0: failed to open DIAG_CTRL`
fires cleanly at the expected 5s mainline timeout, exactly as designed. But roughly 18-40 seconds
LATER, every single time this was attempted (three separate occasions, two of them minutes apart on
different modem boots), the modem hard-crashes:
```
qcom-q6v5-mss 4080000.remoteproc: fatal error received: [disable_ssr]glink_channel_migration.c:602:
[IST336]Assertion status == GLINK_STATUS_SUCCESS failed
remoteproc remoteproc3: crash detected in 4080000.remoteproc: type fatal error
remoteproc remoteproc3: handling crash #N in 4080000.remoteproc
```
This is a **repeatable causal chain**, confirmed across crashes #5 and #6 in the same dmesg buffer,
both immediately following a `failed to open DIAG_CTRL` line, both with the identical
`glink_channel_migration.c:602` assertion. `glink_channel_migration.c` is the FIRMWARE's own glink
channel-transport-migration logic (this is a modem-firmware-side log line, reported via SFR/fatal
error, not an AP-side kernel message) — meaning the incomplete/abandoned local-open handshake left
channel/migration bookkeeping in a state the modem's own firmware asserts on and can't tolerate.
**Practical, urgent implication**: DO NOT locally-initiate an open for `DIAG_DATA` or `DIAG_CTRL`
casually or repeatedly during further investigation — it reliably crashes the whole modem MSS a
short time later, disrupting whatever else was being tested (this crashed WLAN's firmware too, via
the shared XPU/hyp-assign path, matching TOP LEAD #11's earlier finding about shared secure-memory
fragility). If this path is pursued further, it needs to be done RARELY, with full crash-recovery
tolerance, ideally on a boot with nothing else running that a modem SSR would disrupt.
**What this actually tells us, functionally**: the modem's glink/migration layer is NOT robust to an
AP-initiated open attempt that times out — downstream never hits this because (a) it doesn't time out
in 5s (event-driven, no hard deadline, per #15's downstream comparison) and/or (b) Android's DIAG
daemon presumably completes successfully before any abandonment would occur. This is now a second,
independent data point (alongside offline-vs-LPM, #14) that mainline's bring-up leaves this modem's
firmware in a MORE FRAGILE state than Android's does — not just "missing a feature" but "structurally
less tolerant of imperfect AP-side timing/sequencing." Worth weighing against #14's LPM finding: both
point at the same underlying idea -- whatever Android's fuller bring-up sequence does before touching
these channels, mainline's minimal sequence skips, and the modem firmware was never hardened against
being touched without it.
**Honest gap, not yet resolved**: unclear if the crash is specific to DIAG_CTRL/DATA's particular
migration-eligible status, or would happen for ANY locally-initiated open of ANY channel abandoned via
timeout. Worth testing later (carefully, expecting a crash) against a channel we know is NOT normally
migration-sensitive, to isolate whether this is DIAG-specific or a general "don't abandon a local
open" hazard on this modem.

123
saf/device/platform.md Normal file
View file

@ -0,0 +1,123 @@
# PAF · Platform — hardware, boot, storage, recovery (Arch era)
Load-bearing device facts. Add a line when something costs more than 2
minutes to relearn. pmOS-era snapshot preserved at
`evidence/platform-pmos-era.md` — its Alpine/ash and pmOS-slot notes no
longer apply.
## Reaching the phone
- USB (primary): `ssh -F /dev/null -i ~/.ssh/ani -o BatchMode=yes casey@172.16.42.1`
(NCM gadget, phone serves DHCP; laptop autoconfigures).
- WiFi: DHCP on the LAN, drifts — check the router or `nmcli` on-device via USB first.
- User `casey`, passwordless-sudo NO — sudo password `147147`
(`echo 147147 | sudo -S <cmd>`). **Never** stream sudo-gated output through
further pipes over SSH (silent truncation, repeatedly) — write to a file on
the phone, pull it separately.
- Shell is **bash on Arch** (systemd, GNU coreutils, pacman). The pmOS-era
busybox/ash caveats are dead.
- Serial fallback: ACM gadget → laptop-side `ttyACM0`; getty enabled on ttyGS0.
## Slots & boot architecture
- **Slot A = Arch (ours). Slot B = LineageOS (Android, rooted, on the 4.9
vendor kernel).** Slot B is the living downstream reference AND the
fallback OS — never touch `boot_b` or Android's partitions.
- Boot chain: ABL boots `boot_a` (Android bootimg v0, our kernel Image.gz
with **appended DTB** + the pmOS-chain ramdisk — see build.md). Root is
found by UUID from the baked cmdline.
- `qbootctl` (in the device package since ef21637) marks the slot successful
and provides reboot-to-android; closes the A/B retry-lockout that used to
eat boots.
- Switch to Android: `sudo reboot bootloader``fastboot --set-active=b`
`fastboot reboot` (and back with `--set-active=a`).
## Storage (live table, verified on-device 2026-07-10)
UFS `/dev/sda`, GPT, **4096-byte sectors** (sgdisk needs `-a 256`; a tool
assuming 512B sectors will misread the table).
| Part | Size | FS | Name | Role |
|---|---|---|---|---|
| sda21 | 42.9G | — | userdata | **Android's, FBE-encrypted, NEVER TOUCH** (near-formatted once — always verify identity before writing any partition) |
| sda22 | 10.0G | fat32 | shared | Arch↔Android shared data, mounted /shared |
| sda23 | 253M | vfat | Arch_boot | staging/aux boot filesystem |
| sda24 | 66.2G | ext4 | Arch_root | the Arch rootfs, `UUID=a094e031-ea89-4847-a299-082a0ae8c9ab` |
`persist` lives on a **different UFS LUN (sdf2)** — physically immune to sda
operations; mounted ro,nofail. Backup in `../blobs/` +
`~/pixel3-backup/persist.img`.
- Boot-image A/B slots (`boot_a`/`boot_b`, 67MB each, partitions 11/12) are
separate from the rootfs partitions above — `fastboot flash boot_a` is the
only flash operation this project performs.
## Recovery
- TWRP: `fastboot boot ~/Downloads/twrp-3.7.1_12-0-blueline.img` (boot, not
flash). Used for repartition-class work; a live pmOS/Arch root can't
modify its own mounted partitions.
- Known-good fallback kernel image (stmfts touch, pre-SWIRE display):
`../staging/deploy-139aa/boot-139aa7748181-pmoschain.img` → flash to
boot_a. Its module tree is on the phone.
- Full stock partition clone (2026-06-18) at `~/pixel3-backup/`; LOS boot
image at `~/Downloads/lineage-22.2-20260629-boot.img`.
- Last resort: slot B is a complete, working Android.
## Hardware map (what's proven on this exact unit)
- SoC sdm845, 4G RAM. Panel: **LG SW43408** (DSC command-mode, 1080x2160) —
settled from per-unit persist calibration data; the Samsung s6e3ha8/sec_ts
DTBO fragments are the OTHER hardware variant, not ours.
- Touch: **ST FTS @ 0x49 on i2c-2 (888000 geni SE, GPI-DMA-only)**; reset
tlmm 99 (active-low RESETB), IRQ tlmm 125, AP/SLPI bus switch tlmm 136.
- OLED bias: PMI8998 LAB/IBB under **SWIRE (DDIC hardware) control** — see
display.md.
- WiFi wcn3990 (ath10k_snoc), BT (hci_uart/btqca), modem MPSS (see
modem.md), sensors behind SLPI DSP (see slpi.md), audio WCD9340 over
SLIMbus (working HiFi speaker, microphone and two-way calls; see audio.md).
- Power button = pm8941_pwrkey → input event; volume keys GPIO.
## Charging (pmi8998, `qcom_smbx`)
Two supplies: `pmi8998-charger` (the charger) and `qcom-battery` (fuel
gauge). The charger IC does full CC/CV — trickle, pre, fast, full-on,
taper — and `BATTERY_CHARGER_STATUS_1` bits 0:2 report which stage it is
in. Mainline read that register only to derive `status`, collapsing all
five states into "Charging" and discarding the detail.
Since f08dc70b3 the driver exposes them:
| attribute | register | notes |
|---|---|---|
| `charge_type` | `BATTERY_CHARGER_STATUS_1` (0x06) | read-only; Trickle/Fast/Standard |
| `constant_charge_current_max` | `FAST_CHARGE_CURRENT_CFG` (0x61) | 25000µA/step, 8-bit |
| `voltage_max` | `FLOAT_VOLTAGE_CFG` (0x70) | `raw=(uV-3487500)/7500+1`; 4.4V → 122 |
| `charge_control_end_threshold` | same as above | percentage form, 0-100 |
**Lowering the float voltage is what caps the charge.** The cell is held
at whatever CV target is programmed, so 4.1V parks it near 80% and buys
calendar life. Writes are bounded twice — by the DT cell rating
(`voltage-max-design-microvolt = <4400000>` in
`sdm845-google-blueline.dts`) and by what the 8-bit fields can encode, so
neither an overcharge nor a silent masked-off write is possible.
**These registers are volatile — every setting is lost on reboot.** Use
`/etc/blueline-charge-limit` + `blueline-charge-limit.service` to persist
one. Front end is `blueline-charge status|limit|current`.
Not exposed by this driver: `charge_full`/`charge_now` (fuel gauge, see
`qcom-battery`) and any charge *scheduling* — downstream's `google_charger`
ran a taper state machine with DT tunables (`google,chg-itaper`,
`chg-soctaper`); mainline has no equivalent and this patch does not add one.
## Hard lessons (each cost real time — do not relearn)
- **Verify partition identity before any write** (`parted -s /dev/sda print`
+ `blkid`); an unlabeled lsblk row is not free space. The sda21 near-miss
is the standing warning.
- `/tmp` on the phone is tmpfs — never park artifacts there.
- `fastboot boot <img>` has a ~60s hardware watchdog — flash, don't
test-boot, anything that takes long to come up.
- Trust the user's direct visual report of the glass over any remote
telemetry — registers can say "on" while the panel shows black.

909
saf/device/slpi.md Normal file
View file

@ -0,0 +1,909 @@
# PAF · SLPI bring-up (sensor island) — SOLVED 2026-07-10
## Status update — 2026-07-25: SLPI recovers, hexagonrpcd-sdsp does not
**Sequencing regression, live for hours before it was noticed.** The CHRE
fatal documented below recurred on an ordinary running system (not a cold
boot) at 06:04:33:
kernel: qcom_q6v5_pas 5c00000.remoteproc: fatal error received:
err_qdi.c:456:EF:sensor_process:1:fatal_error.cc:22:CHRE
fatal@platform_sensor_manager.cc:158
remoteproc remoteproc2: crash detected in slpi / handling crash #1
remoteproc remoteproc2: remote processor slpi is now up
remoteproc recovery worked — `remoteproc2: slpi = running` afterwards. But
`blueline-hexagonrpcd-sdsp.service` took
`Could not fetch next FastRPC message: Broken pipe` and **"Deactivated
successfully"**: systemd read a clean exit and did not restart it. SLPI came
back; the HexagonFS daemon that serves it did not. The unit sat `inactive
(dead)` while `blueline-hexagonrpcd-adsp` (audio PD) stayed up, which is why
audio was unaffected and the loss was invisible.
Symptom, ~100s apart, restart counter climbing to 5:
iio-sensor-proxy: 'registry' sensor unavailable, is hexagonrpcd running?
net.hadess.SensorProxy ->
HasAccelerometer: false HasAmbientLight: false HasProximity: false
**Every sensor was down** — no autorotate, and `blueline-proximity-lock` kept
running against a proxy with nothing behind it. A standalone consumer of a
dead sensor is indistinguishable from one whose sensor is merely quiet, so
nothing reported the outage.
**Fix belongs in `rootfs-overlay/`, not on the device.** The recovery order
is the bug: SLPI restarts itself, so its filesystem daemon must follow it
(`Restart=` plus a binding/ordering relationship that survives remoteproc
crash-recovery, not just boot). A fix applied live is erased by the next
reprovision — see `SouveraineOS/docs/tasks/25-one-repo-all-packages.md`.
Consumed-by note: this was the concrete argument for evidence source health
being device state, and it is now built — `SouveraineOS/docs/DEVICE-STATE-MACHINE.md`
§10 (2026-07-26). A source that reported and then went silent for 90 s is
recorded `down`, not quiet, and every forensic snapshot taken during the outage
is stamped `sensors_degraded`. The half that made it possible is here:
`blueline-proximity-lock` re-sends its last reading every 30 s, because
`monitor-sensor` only emits on change and a phone on a table is otherwise
indistinguishable from a CHRE fatal. (TASK-26 itself was never written; the
requirement outlived the placeholder.)
## Status update — 2026-07-20: iio-sensor-proxy is always-on now
The wake-lockup caveat below drove `blueline-autorotate` to start/stop the
libssc client on demand — that hazard is confirmed gone. `iio-sensor-proxy`
now runs continuously from boot via `blueline-sensors-enable.service`
(`After=blueline-hexagonrpcd-sdsp.service`), and `AutoRotateToggle` in the
shell only starts/stops the rotation daemon, not the sensor proxy. Proximity
(TMD2725) is live and consumed by `blueline-screen-toggle` (DT2W gate) and
the new `blueline-proximity-lock` user service (blanks an already-on locked
screen on proximity-near). See touch.md and README.md's Tap-to-wake row.
The historical on-demand/no-autostart record below is superseded by this but
left as-is per PAF doctrine — see `docs/tasks/paf-reconciliation.md`.
## Reliability caveat — 2026-07-11: libssc wake lockup, root cause unproven
### New, separate cold-boot incident — 2026-07-13: delayed CHRE sensor-manager fatal
On Casey's confirmed unplugged cold boot, SLPI came up at kernel monotonic
17.692s and then, with `iio-sensor-proxy.service` inactive, crashed at
331.667s: `err_qdi.c:456:EF:sensor_process:1:fatal_error.cc:22:CHRE
fatal@platform_sensor_manager.cc:158`. Remoteproc recovery immediately booted
SLPI again. This is **not** the historical immediate `sar.cc:27` signature and
must be tracked independently; its five-minute delay makes a runtime
CHRE/SEE-sensor-manager timeout or deferred initialization path plausible.
Active firmware was `/lib/firmware/qcom/sdm845/Google/blueline/slpi.mbn`, SHA-256
`83e16ce8bffd8e4b963280dd30fa782becd005b40a3fd4dd6199093f98b82826`.
The raw journal sequence is retained by `journalctl -k -b`; do not clear the
kernel ring before copying a newly observed fatal into this record.
**Source correlation (2026-07-13):** AOSP CHRE's SLPI
`platform_sensor_manager.cc` explicitly schedules `crashAfterSensorNotFound`
for `5 * 60` seconds when required SEE sensors are missing at initialization.
The observed delay is 313.975 seconds from SLPI-up to fatal. This pins the
failure to the required-SEE-sensor discovery set, rather than a random runtime
crash or the audio stack. Identify the absent SUID(s) from the live SSC/SEE
registry before touching firmware or remoteproc state.
**Required-SUID sweep (2026-07-13):** The bounded read-only
`tools/slpi-suid-probe` found SUIDs for
`sensor_temperature` (LIS2MDL), `accel`/`gyro` (BMI160), `mag` (LIS2MDL),
`pressure` (BMP380), and `ambient_light`/`proximity` (TMD2725). `amd` alone
returned no matching SUID. The preserved Pixel Android `sns_amd` group is
byte-for-byte reproduced by the OEM/soc-321 source wrapper. The reverse-file
trace then proved the failure mode: after startup the DSP reads the added
runtime `config/sns_amd.json`, opens the valid static group with write/truncate,
and emits no payload for that group, leaving it zero bytes. This is not a host
permission issue (the daemon successfully writes many other groups). Pixel's
captured Android vendor config has no `sns_amd.json`, so the staged repair keeps
the static group but omits that dynamic config input. **Cold-boot result:** the
served group remained byte-identical to Android (239 bytes) and the DSP read
it without a write-open, yet SLPI still fataled at 322.907s. Therefore the
truncation path was a genuine regression but is not sufficient to restore the
AMD SUID or satisfy CHRE. The probe was made null-safe after libssc returned an
unavailable data type without a `GError`; no SLPI crash was caused by the probe.
**AMD provisioning correction (2026-07-13):** Qualcomm's public split runtime
pair (`sns_amd.json` plus `sns_amd_sw_enabled.json`) was also tested. The
correct vendor-input location is `config/registry/sns_amd.json`, never the
served flat registry and never a physical `registry/registry` tree. Its trace
is decisive: SEE opens that input, opens the valid static `sns_amd` group in
write mode, allocates write-FD 256, and closes it **without an `fwrite`**; it
then writes `sns_reg_config` normally. `O_TRUNC` in hexagonrpcd therefore
turned the valid 239-byte Android-identical group into zero bytes without any
DSP replacement payload. A clean `0014-lazy-truncate-reverse-writes.patch` is
installed for the next boot: `w` opens are truncated only at their first actual
`fwrite`, while append mode is unchanged. This preserves no-write static
groups, but it is a prerequisite—not yet evidence that AMD will publish.
**Validation:** the subsequent boot kept `sns_amd` at the exact 239-byte
Android SHA-256 (`4756a6…2959d2`) through the complete no-write open/close
sequence, yet CHRE still fataled at 325.595s in
`platform_sensor_manager.cc:158`. Registry placement and host-side truncation
are therefore fixed prerequisites, not the remaining AMD-publication cause.
**Required-SUID timeout cleared (2026-07-13):** the public SDM845 AMD split
was retried only after the no-write truncation repair: dynamic
`config/registry/sns_amd.json` supplies parameters while
`sns_amd_sw_enabled.json` supplies enablement; the preserved 239-byte Android
group remains the static registry seed. SEE opened **both** dynamic files,
opened `sns_amd` in write mode without sending a payload, and the lazy-truncate
daemon retained the exact reference hash. At 375 seconds uptime—well past the
former 325-second `platform_sensor_manager.cc:158` deadline—SLPI remained
`running`, `hexagonrpcd` remained active, and no fatal was logged. This is the
first boot to clear CHRE's required-sensor watchdog. Do not immediately start
`iio-sensor-proxy`/`ssccli`: their libssc client connection is the separately
documented wake-lock hazard and needs a bounded follow-up test.
**Bounded client-path validation (2026-07-13):** On that stable boot,
`iio-sensor-proxy` was started explicitly (the package's boot-time udev
autostart remains masked). It discovered the SSC TMD2725 proximity and ambient
light sensors plus BMI160 acceleration. The desktop D-Bus API successfully
claimed and delivered acceleration (`face-up`) and ambient-light samples
(~14 lux); proximity also reports normally. The device rule now explicitly
selects `ssc-accel ssc-proximity ssc-light` on demand, while preserving the
no-autostart policy. Direct `ssccli` reads also returned live TMD2725 light,
BMI160 acceleration, and LIS2MDL magnetometer values. `ssccli`'s compass
client alone still returns `UNKNOWN`. SLPI and hexagonrpcd remained running,
with no CHRE fatal, after these clients. This validates the practical sensor
path without reclassifying the historical boot-time libssc incident as solved.
**Service-loop regression fixed (2026-07-13):** After the one legitimate
five-minute fatal at 326.652s, `hexagonrpcd` correctly exited on its broken
FastRPC pipe with status 0. Its unit unfortunately used `Restart=always`, so
systemd reattached during remoteproc recovery six times in seven seconds. Each
reattach immediately crashed SLPI in `SNS_REG_INIT:39`, then the service hit
systemd's start limit. This was a userspace retry-policy bug, not six
independent firmware failures. The deployed and overlay unit now uses
`Restart=on-failure` with a five-second delay; no service/remoteproc restart
was performed while installing it. A trace-enabled `hexagonrpcd` was also
deployed for the next cold boot: `HEXAGONRPC_TRACE_FILES=1` correlates virtual
open paths with the previously opaque `Could not read file: Is a directory`
FD. The binary and service changes take effect on the next genuine cold boot.
**DMA-heap permission regression fixed (2026-07-13):** The documentation said
the FastRPC allocation heap was owned by `root:fastrpc 0660`, but the actual
cold-boot device node was `root:root 0600`: the overlay had a rule for
`/dev/fastrpc-*` only and never contained the claimed `dma_heap` rule. Added
`11-fastrpc-dma-heap.rules`, narrowly assigning only
`/dev/dma_heap/system` to `fastrpc:0660`; firmware-reserved heaps remain
root-only. It was installed live with `udevadm trigger` and verified as the
`fastrpc` user before any new SLPI action. This removes a genuine local
permissions blocker for non-root FastRPC clients, but does not by itself
explain an EPERM returned from an already-open DSP RPC session.
**CORRECTION — cold-boot validation failed (2026-07-13):** The following
paragraph describes a staged-path hypothesis, **not a completed repair**. On
the next cold boot the registry was readable but SLPI again fataled at 323.7s.
The corrected root-owned listener was then installed and enabled; on its first
validation boot it attached before SLPI's delayed window but the fatal still
occurred at 326.97s. The listener deployment is necessary infrastructure, not
a sensor fix. Its remaining `sns_reg_config` virtual-path error points to
hexagonrpcd's mapping, and CHRE required-sensor discovery remains open.
**Concrete boot-path repair staged and installed (2026-07-13):** before the
fatal, `hexagonrpcd` logged that
`/mnt/vendor/persist/sensors/registry/sns_reg_config` did not exist, even
though the staged registry under `/usr/share/qcom/sensors` was complete. All
required types subsequently resolve through SSC (BMI160 accel/gyro and
motion/stationary; LIS2MDL magnetometer; BMP380 pressure; TMD2725 ambient
light/proximity), so this is an initialization-path race rather than a
permanently missing physical sensor. The enabled
`blueline-sns-registry-stage.service` now creates the guarded persistent link
`/mnt/vendor/persist/sensors → /usr/share/qcom/sensors` before
`hexagonrpcd-sdsp.service`. The next **unplugged shutdown/power-on** is the
validation: retain the kernel journal and confirm that the delayed fatal does
not occur after five minutes.
**Correction — the tracked VFS fix was not in the installed binary
(2026-07-13 evening):** The fresh cold boot again logged
`Could not open /mnt/vendor/persist/sensors/registry/sns_reg_config`, despite
the flat staged host file existing. Inspection of `/usr/local/bin/hexagonrpcd`
and `~/hexagonrpc/hexagonrpcd/rpcd_builder.c` proved that the live
`hexagonrpc-blueline 0.4.0-2` binary had only the old `sns_reg_version` map;
the recipe tracked 0008 but its installed artifact did not contain it. Applied
0008 to the live source, rebuilt, and installed a backed-up replacement. The
new binary embeds maps for `sns_reg_config`, `sns_reg_version`,
`sns_reg_ctrl`, `file1`, and `file2`; `/usr/share/qcom/sensors/registry/`
remains physically flat (no `registry/registry`). No DSP lifecycle action was
taken. The next unplugged shutdown/power-on must confirm the initial ENOENT is
gone and then observe CHRE past its five-minute required-sensor timeout. The
package database still says `0.4.0-2`; rebuild/install the tracked `pkgrel=3`
package before treating this live binary repair as image-persistent.
**Cold-boot validation — VFS control maps fixed; CHRE still missing a required
SEE sensor (2026-07-13 evening):** On the next unplugged shutdown/power-on,
SLPI reached `running` at 13.014s. `hexagonrpcd` successfully opened virtual
`registry/sns_reg_config`, `sns_reg_version`, and `sns_reg_ctrl`; the former
`sns_reg_config` ENOENT was absent. At 327.479s (314.465s after SLPI-up), CHRE
again fataled at `platform_sensor_manager.cc:158`, then remoteproc recovered
SLPI and the daemon exited cleanly. This conclusively fixes the VFS-control
prerequisite without fixing required-SUID discovery. Remaining trace noise is
benign: `testsig*.so` is absent test firmware, and the reported `EISDIR` is
CHRE's expected read-then-`opendir` probe of virtual `registry/registry`.
SLPI itself remains up; this is not a return of the `sar.cc` crash. Toggling
sensors off/on reproducibly identifies the libssc/iio-sensor-proxy SSC QMI
connection as the trigger for a wake lockup: kernel and SSH survive, display
commits stop completing without DSI/DPU errors, FTS becomes calibration-blind,
and GENI I2C GPI DMA times out concurrently. This narrows the mechanism to an
AP-side shared-bus/power interaction on SLPI wake. An rpmh/interconnect vote
race is the leading hypothesis, **not a proven root cause**.
**HACK shipped:** SLPI stays booted, but the auto-rotate toggle starts/stops
its iio-sensor-proxy/libssc client instead of leaving that client enabled from
boot. It reduces exposure only; it is not a fix. Netconsole and pstore_blk are
armed for an instrumented reproduction before any permanent change is attempted.
**Boot-safety correction, 2026-07-11:** the initial HACK was ineffective at
boot: `/usr/lib/udev/rules.d/80-iio-sensor-proxy.rules` starts the proxy on
every `fastrpc-sdsp` add event via `SYSTEMD_WANTS`. The overlay now supplies a
comment-only `/etc/udev/rules.d/80-iio-sensor-proxy.rules`, which shadows that
vendor rule while leaving the explicit polkit/toggle start path intact. Live
`udevadm test` confirmed `/etc` rule precedence and the running boot-started
proxy was stopped. The next reboot must still verify that it remains off.
**Instrumented reproduction, 2026-07-11 10:45 EDT:** netconsole over the USB
gadget was configured dynamically (`usb0` 172.16.42.1:6665 → laptop
172.16.42.2:6666) and packet-capture verified before the proxy was started.
With the proxy active, physical sleep/wake reproduced the stale-frame lockup:
the glass remained at 08:02 while SSH, low system load, Hyprland, and
Quickshell remained alive. No panic or pstore record occurred. Starting 28
seconds after the proxy-start marker, the FTS driver emitted repeated invalid
Mutual/Self Strength calibration events; no DSI/DPU error, GENI timeout, or
rpmh/ICC diagnostic was logged in this run. Netconsole captured only the
explicit console-level start marker, so it proves the transport but not the
missing low-priority driver messages. The proxy was stopped afterwards. This
is further trigger evidence, **not** proof of the rpmh/interconnect hypothesis.
Next capture: raise the temporary console loglevel and add narrowly scoped
rpmh/ICC tracing before another deliberate reproduction.
## ARCH STATUS (2026-07-10 afternoon): THE sar.cc WALL IS DOWN. SLPI runs.
SLPI boots, stays up (0 fatals, was deterministic-fatal at ~10s), SEE fully
initializes (node 9 publishes svc 400 + services that never appeared in the
crash era), the accelerometer streams real data to userspace (ssccli), and
iio-sensor-proxy reports HasAccelerometer=true over D-Bus. Boot-started by
`blueline-slpi.service` (rootfs-overlay).
**Root cause of sar.cc:27 (found by source archaeology, kernel 25d56ec84):**
the SSC firmware's `remote_proc_state` sensor does
`smp2p_register(SMEM_APPS, "sleepstate")` in its init thread and NEVER
publishes if that fails (Qualcomm source: Rivko/android-firmware-qti-sdm670,
adsp_proc/ssc/sensors/remote_proc_state/). Mainline's sdm845 smp2p-slpi node
has no "sleepstate" entry, so registration failed, remote_proc_state never
published, and CHRE's SeeHelper::init exhausted its ~10s findSuidSync retry
budget on it (AOSP chre platform/slpi/see/see_helper.cc — initCalSensors
passes, initRemoteProcSensor fails) → "Failed to initialize ASH SEE helper"
→ chre_utils FATAL at sar.cc:27 → whole sensor_process aborts. The SAR
module was only ever the messenger. FIX: sdm845.dtsi sleepstate smp2p entry
+ new drivers/soc/qcom/smp2p-sleepstate.c asserting bit 12 (= SMP2P_AWAKE
0x1000 in SSC source; = downstream PROC_AWAKE_ID msm-4.9
smp2p_sleepstate.c), dropped across suspend. CONFIG_QCOM_SMP2P_SLEEPSTATE=y.
**Dead ends, so nobody repeats them:** (1) qrtr ns announce-replay patch
(3fe96cf2d, REVERTED in 94ac4a5e1) — replaying all nodes' services on HELLO
wedged node 9's own service registration (flow control on the ns worker);
sar.cc never needed modem services. (2) The whole 2026-07-08 elimination
list stands (registry, socinfo, chrecd, proxy-power, sessions — all real
fixes, none the wall). (3) Registry-diff vs LOS persist: the 42 missing
entries are wrong-device pollution + factory cal, not the cause.
Five-fix SNS_REG_INIT chain (hw_platform OEM socinfo, module tree, chrecd,
sscregistrygen registry, socinfo sysfs) remains prerequisite and correct.
Sensor stack persists across re-provision since cd82f29. AP-side gesture
dt2w works independently (touch.md); the SLPI gesture-nanoapp path is now
UNBLOCKED for future work (CHRE runs). Auto-rotate WORKS on glass
(2026-07-10 ~13:00: blueline-autorotate + ACCEL_MOUNT_MATRIX udev fix,
see rootfs-overlay). Remaining: sx932x "sar" SUID still absent
(grip sensor hardware probe — separate, non-fatal now).
Everything below is the pmOS-era record — the archaeology that fed the
chain above; capture docs it cites live in `evidence/`.
## 2026-06-30 — chronic RTC-write flood found + fixed; wrong listener used in a failed test; dma_heap permissions fixed
**Context:** a fresh SLPI one-shot attempt (manual `sdsprpcd sensorspd` + `recovery=disabled`
+ `echo start`, **not** via `blueline-slpi-ready.service`) wedged the phone. Investigating the
wedge surfaced three unrelated, real bugs — none of them SLPI firmware behavior — that were
corrupting every capture attempt and blocking the correct listener from working at all.
**1. Chronic `disallowed SPMI write to sid=0, addr=0x6046` flood (~40/sec, continuous,
present even with SLPI offline) — unrelated to SLPI, was drowning every capture.**
Traced to `drivers/spmi/spmi-pmic-arb.c:1364` (`pmic_arb_offset_v5`): a hardware EE-ownership
check the AP fails every time. `sid=0`/`0x60xx` is the pm8998 RTC peripheral
(`pm8998.dtsi:104`, `rtc@6000`). Root cause: `drivers/rtc/rtc-pm8xxx.c:622-631` force-overrides
`allow_set_time = true` regardless of the DT `allow-set-time` property, on the premise
(comment at :624-628) that "the modem reads real wall-clock time during RF-init" via this
write. **That premise is wrong** — cross-referenced against LineageOS on the same silicon:
`los-blueline.dts:2371` sets `qcom,qpnp-rtc-write = <0x00>` (write disabled),
`qpnp-rtc.c:474,587-588` defaults to read-only ops and only switches to read-write if that
property is true, and `init.sdm845.rc:233-237` never touches `rtc` at all — only
`start time_daemon`. Android does not write this register either; it can't, for the same
hardware-ownership reason. The actual (real, working, already-verified — see `file.txt`
2026-06-30, "FIRST ACCEPT... modem User time is now real wall-clock") mechanism for feeding
the modem wall-clock time is `tools/blueline-timed.service`, a **QMI TIME-service-22 push
straight to the modem** (`modem-time-set.py`), completely unrelated to the PMIC RTC. The
`rtc-pm8xxx.c` patch never achieved its stated goal either way: `__pm8xxx_rtc_set_time()`
(:294-343) fails at its very first register access (`alarm_ctrl`, same address) every call,
before ever reaching the actual time-write registers.
**Fix:** reverted the force-true override (falls through to the driver's existing
`pm8xxx_rtc_update_offset()` software-offset path, which is what LOS's read-only mode is the
equivalent of); dropped `allow-set-time;` from `sdm845-google-blueline.dts`. Kernel rebuilt
2026-06-30 ~21:56.
**2. The SLPI wedge test used the wrong listener with no ordering — not a firmware crash we
observed correctly.** `device-google-blueline` ships `blueline-slpi-ready.service`, which
`Requires=`+`After=` **`blueline-sns-registry-stage.service`** and **`hexagonrpcd-sdsp.service`**
— exactly the registry-staged/listener-ready ordering the S6 session below already proved is
required. The failed manual test instead started the proprietary `sdsprpcd` (from the
`fastrpc` apk, installed this session) directly, with zero registry staging and zero
wait-for-listener. **Next SLPI attempt: `systemctl start blueline-slpi-ready.service`,
not a hand-rolled sequence.** All three services were `disabled` (not masked) on this
rootfs — never actually run, not deliberately blocked.
**3. `/dev/dma_heap/*` was `root:root 0600`, silently blocking `hexagonrpcd-sdsp.service`
(`User=fastrpc`) from allocating FastRPC memory at all.** `60-fastrpc.rules`'s fix for this
(`RUN+="/bin/setfacl -m g:fastrpc:rw $env{DEVNAME}"`, and only for the `system` heap, not
`adsp_region`/`default_cma_region`/`reserved`) silently no-ops on every boot because
`setfacl`/the `acl` package was never installed. Confirmed live: `fastrpc` user got
`Permission denied` opening `/dev/dma_heap/system` before the fix, clean `open()` after.
**Fixed** both live (`chgrp fastrpc; chmod 0660` on all four) and durably in the rules file
(replaced the `setfacl` RUN directive with `GROUP="fastrpc", MODE="0660"`, matching the
`fastrpc-*` device rules already in the same file) — not yet verified across a real cold
boot.
**4. Separately, `/vendor/dsp/{adsp,cdsp,sdsp}` didn't exist on the rootfs at all** (needed
by `adsprpcd`/`cdsprpcd`/`sdsprpcd`/`hexagonrpcd` alike). The live `vendor_b` partition
(`/dev/sda10`) mounts but reads as directory-corrupted via both the kernel VFS and
`debugfs -R "ls /"` — likely a raw-mount-bypasses-dm-verity artifact, not real corruption of
the working LOS install (left untouched, not fsck'd). Used the already-extracted, genuinely
readable copy from an earlier session, `pixel3-work/los-vendor/vendor.img` (531MB ext2,
volume name "vendor"), and copied just `dsp/{adsp,cdsp,sdsp}` (17MB) to `/vendor/dsp/` on
the phone. This did **not** fix `adsprpcd_audiopd` (its real failure is Qualcomm QDI status
`0x80000414`, not literally ENOENT despite the log text — needs an audio-PD servreg
registration the vendor tree alone doesn't provide; out of scope, re-masked), but the tree
is now in place for whatever `hexagonrpcd`/SDSP actually needs from it.
**Net effect: the next SLPI attempt is the first one with quiet logs, the correct listener
+ ordering, correct dma_heap permissions, and the vendor DSP tree all present
simultaneously.** No prior attempt (including tonight's wedge) had all four.
## 2026-06-30 (cont.) — first clean-ish trace: two racing FastRPC-sdsp consumers, one legit, one not
After the RTC fix (kernel flashed, rebooted, SPMI flood confirmed **0 hits** — first quiet
boot ever), ran a real test: staged the registry (`blueline-sns-registry-stage.service`,
179 files), then a poller script raced `hexagonrpcd -f /dev/fastrpc-sdsp -d sdsp -s` onto
the device the instant it appeared (`/dev/fastrpc-sdsp` doesn't exist pre-boot; neither
`hexagonrpcd` nor any udev rule retries for it — the poller was necessary, see script in
`tools/` history / this session). `echo start` on `remoteproc2`. Captured via
`journalctl -f` to a persistent file (works — unlike `tail -f /dev/kmsg`, which silently
dies on the ring-buffer overflow the RTC bug used to cause; **this is now the standard
capture method**, not kmsg tail).
**What actually happened, in order (journal-confirmed, `slpi-test2/journal-live.txt`):**
1. `remoteproc remoteproc2: powering up slpi` → boots clean, heap already preassigned
(no `-22`, confirms the earlier adsprpc memory conversion still holds).
2. `/dev/fastrpc-sdsp` appears. **Two things react simultaneously**, not one:
- `systemd[1]: Starting sDSP RPC daemon...`**`sdsprpcd.service`**, auto-started by
`60-fastrpc.rules`'s `KERNEL=="fastrpc-sdsp" ... SYSTEMD_WANTS+="sdsprpcd.service"`.
This is the **Qualcomm proprietary daemon from the `fastrpc` apk installed earlier
this same session** — an unintended side effect of installing that package to get
`sdsprpcd` for testing. It attached first (`attachguestos` on domain 2 succeeded,
opened `adsp_default_listener` handles), then hit
`Error 0x4e ... errno Operation not permitted` on an RPC invoke.
- `hexagonrpcd` (the poller-launched, intended listener) tried immediately after and
got `Could not attach to FastRPC node: Operation not permitted` — plausibly because
`sdsprpcd` had already claimed the attach slot, not necessarily a firmware-side
rejection of `hexagonrpcd` specifically.
- A **third** thing also started at the same instant:
`systemd[1]: Starting IIO Sensor Proxy service...` (`iio-sensor-proxy.service`,
pre-existing base-image package, installed 2026-05-08, unrelated to tonight — see
below).
3. ~1s later: `qcom_q6v5_pas ...: watchdog received: SFR Init: wdog or kernel error
suspected.` → `remoteproc2` crashes.
**`sdsprpcd.service` is a real bug (masked) — but not a new one tonight.** Checked the
history properly rather than assume: the 2026-06-29 session *also* installed the `fastrpc`
apk and ran `sdsprpcd sensorspd` manually as its one-shot test — and that is the exact
command HANDOFF's own notes say caused *that* session's wedge too ("the same one-shot
`sdsprpcd sensorspd` + SLPI start caused SSH/network/UI to wedge"). `file.txt`'s later
session was also checking for `sdsprpcd` specifically. So `sdsprpcd`-as-manual-test is a
repeated pattern across at least three sessions, not something introduced tonight — this
session continued that same habit (reinstalling the `fastrpc` apk to get `sdsprpcd` back
after the rootfs rewrite lost it) before realizing `hexagonrpcd` was the actually-intended
listener. `hexagonrpcd`, and the `blueline-slpi-ready.service`/`hexagonrpcd-sdsp.service`
ordering, come from `device-google-blueline` (a separate, independently-developed track
within this same project) and were never actually exercised in isolation until tonight's
`sdsprpcd`-masked re-test. `systemctl mask sdsprpcd.service` done this session — first
time this collision has been diagnosed rather than just repeatedly hit.
**`iio-sensor-proxy.service` is NOT a bug and should NOT be masked long-term** (briefly
masked mid-session, then **unmasked** once this was understood — leave it unmasked).
It's built against `libssc.so.2` (`qmi_client_ssc_control`, "SSC QMI client") — a genuine
**QMI Sensor SubSystem Client**, i.e. exactly the downstream consumer this whole SLPI
effort is trying to feed ("no SNS QMI service exposed" has been the standing symptom
description). Its reacting to SLPI coming up is systemd/udev doing its job correctly, not
a coincidence. The real fix here is **ordering, not suppression**: it needs to start
*after* SLPI/hexagonrpcd are confirmed stable, not the instant the remoteproc powers up —
right now nothing enforces that, so its QMI connection attempt lands during the same
fragile registry-init window the S6 finding already identified as timing-critical, on top
of the `sdsprpcd` collision. Untested whether `iio-sensor-proxy` alone (with `sdsprpcd`
masked) still perturbs the window enough to matter — that's the next isolated test.
**Next step:** re-run the identical poller+`hexagonrpcd` test with `sdsprpcd.service` now
masked. If `iio-sensor-proxy` still auto-fires and coincides with a crash, that tells us
the ordering fix is needed for real; if `hexagonrpcd` attaches cleanly this time, the
`sdsprpcd` collision was the whole story and today's `Operation not permitted` /
`SFR Init` crash was never a hexagonrpcd/firmware problem at all — just two proprietary
and community daemons fighting over one singleton resource.
## 2026-06-30 (cont. 2) — isolated re-test: `sdsprpcd` wasn't the story either. The SAR/CHRE fault is real and reproduces clean.
Re-ran the identical poller+`hexagonrpcd` test with `sdsprpcd.service` masked (previous
section). `iio-sensor-proxy.service` still auto-fired on SLPI coming up (as expected, it's
legitimate) but did **not** coincide with any crash this time — no evidence it perturbs
anything on its own.
**Clean timeline (`journalctl`, unfiltered, this run):**
```
22:25:26 remoteproc remoteproc2: powering up slpi
22:25:27 remoteproc remoteproc2: remote processor slpi is now up
22:25:27 [hexagonrpcd] Starting hexagonrpcd (INIT_ATTACH_SNS) on /dev/fastrpc-sdsp
22:25:27 [hexagonrpcd] Could not attach to FastRPC node: Operation not permitted
22:25:36 qcom_q6v5_pas ...: fatal error received: err_qdi.c:456:EF:sensor_process:1:sar.cc:27:chre_utils fatal
22:25:36 remoteproc remoteproc2: crash detected in slpi: type fatal error
```
No AP-level instability this time — `remoteproc2` settled into `crashed` (recovery still
`disabled`, no relaunch spam) and the phone stayed fully reachable throughout. **The hard
wedge in the first test tonight was very likely the `sdsprpcd`/`hexagonrpcd` collision
itself (two clients contending for one singleton attach), not simply "SLPI crashes."** A
clean, isolated SLPI crash does not wedge the AP.
**Conclusion: this is the same `sar.cc:27` SAR/CHRE fatal documented in the "2026-06-29
live result" section above, now reproduced with everything else this session found and
fixed already in place** — quiet logs (RTC fix), correct listener alone (no `sdsprpcd`
collision), correct `dma_heap` permissions, `/vendor/dsp/` present, registry staged
(179→182 files). **None of tonight's plumbing fixes were the actual blocker.** They were
real bugs and worth fixing (nothing else could be tested cleanly until they were), but the
live frontier is exactly where it was before tonight: `INIT_ATTACH_SNS` returns `EPERM`,
and ~9s later the firmware faults inside CHRE's SAR path. **Next real work is still the
already-identified target**: convert the remaining Lineage sensor/SAR FastRPC path
(`sscrpcd sensorspd` equivalent, sensors PDR/service-location behavior, whatever makes
`INIT_ATTACH_SNS` attach cleanly) — see "Status / next" above. That work has not started;
tonight only cleared the ground for it to be tested honestly.
## 2026-06-30 (cont. 3) — the "proper" pmOS chain has a real, previously-undiagnosed timing bug
Per Casey's explicit direction: stop working around the shipped mechanism, make the
shipped mechanism itself work. The gap: `hexagonrpcd-sdsp.service` has
`ConditionPathExists=/dev/fastrpc-sdsp`, which can never be true at the moment systemd
evaluates it (the device doesn't exist until SLPI starts) — `device-google-blueline` never
shipped a udev rule to re-trigger it once the device appears. Added one, as a local admin
override (not yet upstreamed into the package):
```
# /etc/udev/rules.d/61-hexagonrpcd-sdsp.rules
ACTION=="add", SUBSYSTEM=="misc", KERNEL=="fastrpc-sdsp", TAG+="systemd", ENV{SYSTEMD_WANTS}+="hexagonrpcd-sdsp.service"
```
With this in place, `systemctl start blueline-slpi-ready.service` — the actual, sole,
intended entry point — runs end-to-end with **no manual scripts at all**: registry stages,
`hexagonrpcd-sdsp.service` auto-triggers via udev the instant the device appears (via a
`blueline-hexagonrpcd-sdsp-wait.sh` wrapper this project hadn't inspected before), SLPI
starts, `hexagonrpcd` attaches. This is real progress — the shipped mechanism now actually
runs as designed, on the current rootfs, for the first time.
**But it lost the registry-init race — a real bug, not the earlier `sdsprpcd` collision.**
Clean journal, unfiltered:
```
22:48:43 blueline-slpi-ready.sh: disabling recovery, starting SLPI once
22:48:43 remoteproc remoteproc2: powering up slpi
22:48:44 remoteproc remoteproc2: remote processor slpi is now up
22:48:44 blueline-hexagonrpcd-sdsp-wait.sh: starting hexagonrpcd on /dev/fastrpc-sdsp
22:48:44 hexagonrpcd: Starting hexagonrpcd (INIT_ATTACH_SNS) on /dev/fastrpc-sdsp
22:48:44 qcom_q6v5_pas: fatal error received: err_qdi.c:456:EX:sensor_process:1:SNS_REG_INIT:39:PC=b20226c0
22:48:44 remoteproc remoteproc2: crash detected in slpi: type fatal error
```
This is `SNS_REG_INIT` failing — the **exact same registry-init ordering race the S6
session (2026-06-27) diagnosed and believed `auto_boot=false` had solved** — reproducing
in the *same second* SLPI came up, before `hexagonrpcd`'s attach could complete.
**Compare to the earlier isolated test tonight** (this doc, "isolated re-test" section):
manually racing `hexagonrpcd` onto the device with a raw 20ms-poll bash loop (no systemd
involved) won that same race reliably — SLPI ran a full ~9s before failing later, in
`sar.cc:27:chre_utils` (deeper into CHRE/SAR init, past registry-init). **The manual
poller was faster and more reliable than the real systemd service-start chain.**
`hexagonrpcd-sdsp.service`'s startup path — udev event → systemd service activation →
`blueline-hexagonrpcd-sdsp-wait.sh` → exec `hexagonrpcd` → device open → `INIT_ATTACH_SNS`
— has enough cumulative latency to lose a race a tight bash loop wins.
**FIXED, same session, same night.** Checked `~/pmos/pmaports/device/testing/device-google-blueline/`
(the actual local source for this package) and found the design was already correct — the
`hexagonrpcd-sdsp.service` drop-in (`hexagonrpcd-sdsp-root.conf`) already clears
`ConditionPathExists`, already redirects to a wrapper (`blueline-hexagonrpcd-sdsp-wait.sh`)
that runs a persistent `while :; do wait for device; exec hexagonrpcd; wait for
teardown; done` loop — i.e. it was *already* starting before SLPI, via the normal
`Requires=`/`After=` chain in `blueline-slpi-ready.service`, exactly as the S6 finding
prescribed. (My first instinct — that a missing udev rule was the gap — was wrong; added
`/etc/udev/rules.d/61-hexagonrpcd-sdsp.rules` then confirmed via `systemctl show
hexagonrpcd-sdsp.service -p ConditionResult` → `yes` that it was never needed, and removed
it again.) **The actual bug was one number**: the wrapper's poll interval,
`blueline-hexagonrpcd-sdsp-wait.sh 0.2` (200ms), was too coarse to reliably detect
`/dev/fastrpc-sdsp` and get `hexagonrpcd` attached inside SLPI's registry-init window.
Changed to `0.02` (20ms, matching the interval that won the race in the manual poller
test above). **Retested through the pure, unmodified entry point —
`systemctl start blueline-slpi-ready.service`, zero manual scripts — and it now reliably
reproduces the same deep `sar.cc:27:chre_utils fatal` crash tonight's best manual test
got**, not the shallow `SNS_REG_INIT` race-loss. Fix committed to
`hexagonrpcd-sdsp-root.conf` in the local pmaports source (not yet built into a package/
flashed — currently live-patched onto the running rootfs only).
**Net: the shipped mechanism is now correct and reliable.** No more manual scripts, no
udev workaround, no competing daemons — `systemctl start blueline-slpi-ready.service`
alone gets to the real wall every time. The remaining problem is exactly what it's been
all along: `INIT_ATTACH_SNS``EPERM`, firmware faults in CHRE's SAR path ~10s later.
That's real kernel/source work (the Lineage sensor/SAR FastRPC conversion), not another
timing or plumbing bug.
(Also observed, unrelated background noise in this capture: `qcril-autoselect` PDC
Set Feature requests timing out repeatedly (`Transaction timed out (2)`) — pre-existing,
not investigated tonight, flag for later.)
Note: `qrtr-lookup` is genuinely **absent from this rootfs** (`find / -xdev -iname
'qrtr-lookup*'` finds nothing at all — not a `$PATH` issue). It's used routinely across
`modem.md`, `tools/slpi-capture.sh`, and `tools/phone-slpi-sdsprpcd-collect.sh`, so it was
clearly present and working in earlier sessions, but there is **no apk package or
reference source for it anywhere in this project** (`qrtr-libs` only ships the shared
library, not the CLI tool) — it must have been a manually-built binary on the pre-wedge
rootfs with no packaging record, same category of loss as the SLPI service masks. Needs
building from the upstream `qrtr` tools source (not currently cloned into `references/`)
to restore. Did not block tonight's test since it wasn't needed, but real and unsolved.
> **🟠 REFRAME 2026-06-29 (Casey) — the SLPI is a CANDIDATE ROOT-CAUSE for the modem 52, NOT a side-quest.**
> The prior "SLPI proven independent of the 52" conclusion (`modem_consumer_chain_20260626.md` SESSION-3)
> is **invalid**. It was tested only with an SLPI that was either crash-looping OR merely
> remoteproc-not-crashing — **never a functioning sensor stack** (that doc itself notes: *"qrtr-lookup
> empty — the SLPI never exposes an SNS QMI service before it dies"*; *"CHRE nanoapp crash INSIDE the
> SLPI firmware at init"*). "Stop SLPI → still 52" only proves stopping doesn't worsen it: if the modem's
> RF-init (~12 s window) queries the SLPI for sensor/NV/registry data and gets silence, it parks OFFLINE
> identically whether the SLPI is crashing or stopped. **The SLPI has never been healthy in any modem
> test.** Path to actually test it: get the SLPI *genuinely* healthy — `auto_boot=false` + start after
> the correct SDSP listener is ready + Lineage-compatible FastRPC/adsprpc behavior so the sensor stack
> initializes and SNS QMI exposes — **then** test the modem. That is the first real modem test with a
> functioning SLPI.
## 2026-06-29 late session — current SLPI frontier and phone state
**Phone is stable only with SLPI held offline.** Current handoff state:
- Kernel: `7.1.1-sdm845`, package `linux-postmarketos-qcom-sdm845-7.1.0_p20260629134644-r1.apk`.
- WiFi regression fixed live: `CONFIG_ATH10K_SNOC=m`, `CONFIG_QCOM_PD_MAPPER=m`, `wlan0` present.
- MSS/modem remoteproc running; modem still DMS op-mode `offline`, HW restricted `no`.
- SLPI is **offline**, recovery disabled when touched.
- `blueline-slpi-ready.service` and `hexagonrpcd-sdsp.service` are masked via
`/etc/systemd/system/*.service -> /dev/null`. Do **not** unmask/start SLPI on boot until the
next capture path is planned.
**Conversions landed:**
- `qcom_q6v5_pas`: `sdm845_slpi_resource_init.auto_boot = false`.
- Blueline Lineage/downstream adsprpc memory conversion:
fixed no-map `pil_adsp_mem@8c500000` for `adsp_pas`, reusable `adsp_region` for adsprpc,
SDSP FastRPC VMIDs `HLOS + MSS_MSA + SSC_Q6 + ADSP_Q6`, and a new
`qcom,msm-adsprpc-mem-region` helper in `drivers/misc/fastrpc.c` that preassigns the heap.
- Qualcomm FastRPC userspace installed: `sdsprpcd`, `adsprpcd`, `libsdsp_default_listener.so`.
- `blueline-slpi-ready.sh` now disables remoteproc crash recovery before its one-shot SLPI start.
**What the tests proved:**
1. The old TZ/heap failure is fixed. With the Lineage adsprpc memory conversion, the boot log shows
the heap preassigned and SDSP FastRPC skipping duplicate hyp-assign. The previous
`qcom_scm Assign memory protection call failed -22` / `SDSP fastrpc hyp-assign rejected by TZ`
wall is no longer the current failure.
2. Crash recovery had been hiding first-failure evidence. `auto_boot=false` only prevents PAS probe
boot; remoteproc still relaunches after a crash unless `recovery` is set to `disabled`.
3. Clean one-shot test **before** the SDSP capability-query patch:
`sdsprpcd sensorspd` was started, SLPI recovery disabled, then SLPI started once. Result:
`remoteproc2` became `crashed`. First useful kernel failure:
`qcom,fastrpc ... Error: dsp information is incorrect err: -1`, followed by
`watchdog received: SFR Init: wdog or kernel error suspected`.
4. Kernel compatibility patch then added: for SDSP only, if the DSP utilities capability query is
rejected with `-EPERM`, cache zero capabilities and continue. Rationale: Qualcomm userspace treats
missing capabilities as "not supported"; Lineage downstream adsprpc does not make this optional
query a hard sensors-PD gate.
5. One-shot test **after** that patch caused SSH/network/UI to wedge during the SLPI start window
before post-start logs could be collected. Host later saw the phone in fastboot. So the patch
likely moved execution past the old `dsp information` gate, but we do **not** have a clean next
failure line yet.
**Next real work:** do not keep hand-starting SLPI over SSH and hoping logs flush. Add a capture path
that survives the wedge (pstore/ramoops, serial/console, persistent journal sync, or earlier in-kernel
tracepoints), then repeat the one-shot `sdsprpcd sensorspd` test. In parallel, source-diff mainline
FastRPC SDSP/sensors-PD against Lineage `drivers/char/adsprpc.c`, especially `INIT_ATTACH_SNS`,
sensors static PD, listener1/geteventfd, service-location/PDR, and the domain/session handling around
`sdsprpcd sensorspd`.
## 2026-06-29 (cont.) — FastRPC sensors-PD/servreg gap isolated (move #2 design)
Source-diff of mainline `drivers/misc/fastrpc.c` vs Lineage `drivers/char/adsprpc.c` around the
sensors PD (`INIT_ATTACH_SNS`). This is the concrete next-conversion target (move #2); it is gated
behind making the SLPI-start wedge survivably observable (move #1).
**Mainline sensors attach is a bare RPC** — `references/linux-7.1.1-sdm845-CANONICAL/drivers/misc/fastrpc.c:1735`:
`fastrpc_init_attach(fl, SENSORS_PD)` sends `FASTRPC_RMID_INIT_ATTACH` (method 0, tgid arg), sets
`fl->pd = 2`, returns. `grep` confirms mainline fastrpc.c has NO `pdr`/`servreg`/`service_location`/
`spdname` wiring — only the generic `INIT_CREATE_STATIC` (method 8) ioctl (`:1340`/`:2193`).
**Lineage wraps the sensors PD in a servreg/PDR scaffold mainline lacks:**
- `INIT_ATTACH_SENSORS` tags the client (`adsprpc.c:2250`):
`fl->spdname = SENSORS_PDR_SERVICE_LOCATION_CLIENT_NAME;` (= `"sensors_pdr_adsprpc"`), `fl->pd = 2`.
- At probe, gated on DT prop `qcom,fastrpc-adsp-sensors-pdr` (`adsprpc.c:4371`), it registers a
service-location notifier: `get_service_location("sensors_pdr_adsprpc", "tms/servreg", &get_service_nb);`
So the production SLPI firmware expects the AP to track the sensors PD via `tms/servreg`
(PDR/service-location) around `INIT_ATTACH_SNS`. Mainline supplies servreg via **pd-mapper**
(userspace DB) + the glink reverse channel — so the open question is whether the sensors-PD servreg
domain is present in pd-mapper's DB AND whether the reverse listener is up when `INIT_ATTACH_SNS`
fires (ties to the S6 auto-boot ordering race).
**Conversion target (move #2):** register a PDR service-location lookup for the SDSP channel in
`fastrpc_rpmsg_probe` (`fastrpc.c:2432`) when `domain_id == SDSP_DOMAIN_ID`, using mainline's existing
`pdr_handle_alloc()` (`drivers/soc/qcom/pdr_interface.c`, already used by `apr.c`/`pmic_glink.c`) —
mirroring Lineage's `get_service_location`. Service path TBD from the SLPI firmware expectation /
pd-mapper config (likely an `"msm/slpi/sensors"`-style servreg domain).
**Capture (move #1) — state:**
- Running kernel already has `CONFIG_PSTORE_RAM=y`/`PSTORE_CONSOLE`/`PSTORE_PMSG`/`PSTORE_COMPRESS`
(verified on phone via `/proc/config.gz`). pstore backend is currently `(null)` because there is
**no `ramoops` reserved-memory node** in our DTS (only apq8016/msm8992 boards carry one).
- So ramoops capture needs only a reserved-memory node — NO config change. BUT the physical address
must be bootloader-preserved across the watchdog→reset (the wedge lands the phone in fastboot).
Do NOT guess an address (brick risk, no EDL net). Candidate preserved region to verify against the
live map in `/proc/device-tree/reserved-memory/`: Lineage `removed_region@85fc0000`, or a top-of-RAM
carve. Verify ABL preserves it across reset before flashing.
- Safe, no-flash capture helper staged now: `tools/slpi-capture.sh` — snapshots dmesg to a timestamped
file on the UFS rootfs and tails `/dev/kmsg` continuously through the SLPI start, so a partial
capture survives the wedge on reboot even before ramoops is wired. Inert until invoked; does NOT
start SLPI.
**Coordination note (2026-06-29):** another model is baking the RTC time-fix (pm8998 `rtc@6000`
label + blueline `&rtc { allow-set-time; }`) — uncommitted in the canonical worktree. Do not edit
`pm8998.dtsi`/`sdm845-google-blueline.dts` until that lands; the fastrpc conversion (a different
file) and the ramoops node (`sdm845.dtsi` shared reserved-memory) can proceed collision-free.
**CORRECTION 2026-06-29 (post-Opus, supersedes the "pd-mapper missing" premise above):**
pd-mapper is **IN-KERNEL** here, not userspace — `CONFIG_QCOM_PD_MAPPER=m` (module
`qcom_pd_mapper`, aux devices `qcom_common.pd-mapper.{0,1,3}`), and it is **serving**
(`wlan0` up proves the wlan PD is advertised). The earlier bug was that the in-tree
`arch/arm64/configs/sdm845.config` carried upstream `CONFIG_QCOM_PD_MAPPER=n` and,
being merged *last* by `merge_config.sh -m`, overrode pmaports' `=m` on every build
→ wifi/sound/SLPI-sensor all lost their PD broker. Opus flipped that line to `=m`,
rebuilt, reflashed → wifi + sound came back. See `build.md` gotcha #0.
Crucially, the in-kernel `sdm845_domains[]` table **includes `slpi_root_pd` AND
`slpi_sensor_pd`** — so the sensors PD *is* being advertised now. Therefore:
1. The "servreg server missing → install userspace pd-mapper" premise in this section
was WRONG (I installed then reverted the userspace `pd-mapper` apk; userspace pd-mapper
is explicitly NOT used on blueline). Do not install it.
2. The in-kernel fastrpc `pdr_add_lookup` conversion (move #2 above) is NOT the gap —
the sensors-PD servreg server is present in-kernel. Drop that conversion.
3. **Every prior SLPI conclusion (auto-boot ordering, fastrpc-attach EPERM, sar.cc:27)
was drawn on a substrate with no sensors PD advertised.** Re-test SLPI on the
post-pd-mapper (`=m`) kernel before any more fastrpc/SLPI source work.
## 2026-06-29 live result — Lineage adsprpc memory conversion moved the wall
**This was real progress, not log suppression.** We ported the blueline-specific
Lineage/downstream adsprpc memory behavior into the 7.1.1 mainline tree:
- `adsp_mem` / `fastrpc_mem` are split like Lineage: fixed no-map
`pil_adsp_mem@8c500000` for `adsp_pas`, dynamic reusable `adsp_region` for adsprpc.
- The SDSP FastRPC node uses the Lineage VMID set:
`HLOS + MSS_MSA + SSC_Q6 + ADSP_Q6`.
- A new mainline `qcom,msm-adsprpc-mem-region` helper preassigns `adsp_region`
before SDSP FastRPC probes, matching downstream `qcom,msm-adsprpc-mem` instead
of letting mainline assign the heap late.
**Verified on hardware after deploy to slot A:**
- `qcom_scm Assign memory protection call failed -22` count: **0**.
- Boot log says the adsprpc heap was preassigned and SDSP FastRPC skipped duplicate
hyp-assign.
- With corrected manual ordering (listener watcher running before SLPI start),
`SNS_REG_INIT` count after start: **0**.
- The failure moved later to:
`EF:sensor_process:1:sar.cc:27:chre_utils fatal`.
So the old wall is fixed: the TZ/heap `-22` and `SNS_REG_INIT` are no longer the
current explanation. The current SLPI wall is downstream of attach/init:
`hexagonrpcd -f /dev/fastrpc-sdsp -d sdsp -s` still reports
`Could not attach to FastRPC node: Operation not permitted`, and the firmware later
dies in SAR/CHRE. The next Lineage conversion target is therefore the **sensor/SAR
FastRPC attach path**, not another attempt to mask the crash-loop.
Symptom: SLPI remoteproc loads and reaches `running`, then the firmware
faults in CHRE / SAR sensor init (`sar.cc:27`, chre sar_utils) and
crash-loops every ~10s. hexagonrpcd ends up STOPPED. SLPI runs fine on
the LineageOS/Android side of the same phone, so the gap is on ours.
## Reference sources (corrected 2026-06-22 — was wrong before)
Earlier "Android reference" diffs were against `references/msm-google-crosshatch`
= Google AOSP `kernel/msm` 4.9.270 (`android.googlesource.com/kernel/msm`).
That is STOCK Google, not what the phone runs. The phone runs LineageOS 22.2
(slot B). The authoritative references are now on disk and provenance-verified:
- LOS kernel SOURCE: `references/los-kernel-blueline-4.9/`
= `LineageOS/android_kernel_google_msm-4.9` @ `lineage-22.2`, **4.9.337,
commit b929452c5**. The blueline device tree
(`LineageOS/android_device_google_crosshatch`, `lineage_blueline.mk`,
PRODUCT_MODEL "Pixel 3") pulls this kernel via `lineage.dependencies`
`kernel/google/msm-4.9`.
Provenance match: boot.img string is `4.9.337-gb929452c5c49` (built 0615) —
same sublevel, same commit hash. This is the literal source of the kernel
running on slot B.
- LOS DTB: `/home/casey/Downloads/los-blueline.dts` (decompiled).
- LOS vendor partition: `pixel3-work/vndlos-mnt` (confirmed LOS via
`vendor.lineage.health-service`, `hal_lineage_livedisplay_qti` — not stock).
Note: LineageOS does NOT ship its own qcom remoteproc/PIL/fastrpc drivers —
it inherits `google/msm-4.9`. So crosshatch 4.9 is the correct driver-source
base, just an older point release; the LOS-specific deltas are DTB + vendor HALs.
## Mainline vs LOS SLPI lifecycle — what is and isn't a real gap
LOS uses the downstream `qcom,pil-tz-generic` driver
(`subsys-pil-tz.c` + `subsystem_restart.c` + `peripheral-loader.c`).
Mainline uses `qcom,sdm845-slpi-pas` (`qcom_q6v5_pas.c` + `qcom_q6v5.c`).
The DT property lists differ, but most differences are spelling, not behavior:
NOT gaps (verified):
- **Identity** — PAS id 12, crash-reason SMEM 424, ssctl 0x16, firmware "slpi"
all match mainline match-data (`qcom_q6v5_pas.c:1300-1312`).
- **Lifecycle handshake** — LOS `qcom,gpio-err-fatal/err-ready/stop-ack/force-stop`
resolve to `qcom,smp2pgpio-ssr-smp2p-3-in`, entry "slave-kernel",
remote-pid 3 (`los-blueline.dts:10453`) — i.e. SMP2P, not raw GPIO. Mainline
wires the same SMP2P as `slpi_smp2p_in` fatal/ready/handover/stop-ack +
`slpi_smp2p_out` stop (`blueline-pmos-slpi.dts:3460-3469`). Same hardware.
Behavioral proof: the subsystem reaches `running` (the `ready` SMP2P fired) —
a handshake gap could not produce a post-boot crash.
- **signal-aop / AOP load_state** — LOS `pil_notify_aop()` sends QMP
`{class: image, res: load_state, name: slpi, val: on}`
(`peripheral-loader.c:949-955,980`). Mainline `q6v5_load_state_toggle()`
sends the BYTE-IDENTICAL string via `qmp_send` (`qcom_q6v5.c:31-32`), gated on
`qmp_get` returning non-NULL (`:28,335`). Our DTS wires it: SLPI node has
`qcom,qmp = <0x2f>` (`blueline-pmos-slpi.dts:3464`) → `qcom,sdm845-aoss-qmp`
(`:4912`), and `CONFIG_QCOM_AOSS_QMP=y`. So this signal genuinely fires.
- The LOS GPIO/proxy/mbox/pil-tz DT props are bindings of `subsys-pil-tz.c`/
`subsystem_restart.c`, which mainline does not have. Pasting them onto our
PAS node is inert — the PAS driver never reads them. (This was the earlier
"reconcile the DT" dead end.)
THE REAL GAP (runtime power, SLPI-specific):
- **`qcom,keep-proxy-regs-on`.** Mainline `qcom_pas_handover()` drops ALL proxy
power the instant the remote signals handover — px/cx regulators, xo/aggre2
clocks, and the lcx/lmx power domains (`qcom_q6v5_pas.c:386-397`,
unconditional). LOS deliberately keeps SLPI's vdd_cx/vdd_mx rails enabled for
the subsystem's whole life: `disable_regulators(..., reg_no_disable=true)`
skips `regulator_disable()` when `keep_proxy_regs_on` is set
(`subsys-pil-tz.c:501-502`, via `pil_remove_proxy_vote` `:583`).
Strongest signal it's load-bearing: `qcom,keep-proxy-regs-on` is set on the
SLPI node and NOT on adsp (`los-blueline.dts` ssc node ~3108 vs adsp ~3053).
Hypothesis: SLPI's CHRE/SAR runtime needs the AP to hold CX/MX after boot;
mainline yanking them post-handover collapses the rails → `sar.cc:27` fault.
Matches the boot-then-crash signature exactly, and matches every prior win on
this project (Android holds a resource mainline drops; port it).
(Secondary, same direction: `qcom,proxy-timeout-ms = 10000` — LOS holds proxy
power 10s post-boot then unvotes; mainline releases at handover with no grace.)
FastRPC: LOS `qcom,msm-fastrpc-compute` (`adsprpc.c`, 7 CBs + per-CB IOMMU) vs
mainline `qcom,fastrpc` (vmids + shared-dma-pool) — different drivers, but our
mainline node already attaches (`/dev/fastrpc-sdsp` exists). Not the blocker.
## The patch (2026-06-22)
`pmaports .../linux-postmarketos-qcom-sdm845/slpi-keep-proxy-power-on.patch`
(wired into APKBUILD `source=`, pkgrel 3→4, checksums updated).
Adds a per-SoC opt-in `keep_proxy_pds_on` flag to `qcom_pas_data`/`qcom_pas`,
set true only for `sdm845_slpi_resource_init`, and makes `qcom_pas_handover()`
return early (skipping the proxy teardown) when set. adsp/cdsp/modem unchanged.
This mirrors downstream `keep-proxy-regs-on`: SLPI's cx/mx (lcx/lmx) stay
powered past handover. Patch verified to apply against the build tree
(tag 85f1df2a, identical to `references/linux-7.1.1-sdm845-CANONICAL`); dry-run rc=0.
Known trade-off (acceptable on a throwaway test bench): power is held across
stop/SSR-restart too, so refcounts are not balanced on a clean shutdown — same
"never turns the rails off" behavior downstream has. One online/boot attempt
per cycle anyway.
## Status / next
- [done] References corrected + provenance-verified.
- [done] Behavioral diff (LOS source vs mainline) — gap isolated to proxy power.
- [done] Patch written and proven to apply.
- [done] Folded into the **unified 7.1.1 tree as a commit** (no longer a `source=`
`.patch`): `references/linux-7.1.1-sdm845-CANONICAL`, alongside the SLPI-enable DTS commit
and the fastrpc SDSP tolerance commit. Built via pmbootstrap (see build.md).
- [done] Deploy unified 7.1.1 kernel to slot A, reboot (2026-06-26, kernel r2 LLVM).
- [DISPROVEN 2026-06-26] LIVE TEST: keep-proxy-power-on does **NOT** stop the
crash-loop. On the booted r2 kernel SLPI still faults
`err_qdi.c:456:EF:sensor_process:1:sar.cc:27:chre_utils fatal` and crash-loops
every ~10s (observed crash #66#72 in one capture). So the proxy-rails-held
hypothesis is wrong — holding cx/mx past handover did not save it. The
`qcom_ssc_sensors` module (CONFIG_QCOM_SSC_SENSORS=m) is **present and loaded**
this build, and SLPI still crashes — so the SSC sensor *transport* being absent
was not the (whole) cause either. The fault is inside the SLPI firmware's SAR
(CHRE sar.cc) path itself: mainline isn't giving the firmware the SAR-sensor
hardware/config it expects, and neither rails nor the SSC glink transport fix it.
- [RESOLVED 2026-06-29] The `qcom_scm ... Assign memory protection call failed -22`
/ `SDSP fastrpc hyp-assign rejected by TZ (-22)` wall is fixed by the Lineage
adsprpc memory conversion above. Do **not** chase this as the current failure.
- [current] Next: convert the remaining Lineage sensor/SAR FastRPC path:
`sscrpcd sensorspd`, `libsdsprpc.so`, sensors PDR/service-location behavior, and
whatever makes `INIT_ATTACH_SNS` attach cleanly instead of returning `EPERM`.
## 2026-06-27 (S6, Opus) — VERBOSE TRACE settles the registry theory: it's an auto-boot ORDERING race, not file serving
Built a **verbose hexagonrpcd** (the `-Dhexagonrpcd_verbose=true` meson option only
exists in the newer source `pmos/_src/hexagonrpc` @ dd9ac70, NOT in the v0.4.0 tarball;
our two patches — registry-nodes + method-29 fstat — are already applied in that working
tree). Snapshotted it to `temp/hexagonrpcd/hexagonrpc-0.4.0.tar.gz` and built with verbose
on. Deployed, captured two clean traces of what the SLPI actually requests.
**THE FINDING (instrument-proven, not inferred):**
- Across both captures the SLPI made **zero** registry file requests (`openat`/`opendir`
of real paths = 0) before crashing at `SNS_REG_INIT:36`. hexagonrpcd's reverse listener
was attached and blocked in the listen loop, and **received nothing**.
- The ~981 log lines were all hexagonrpcd's OWN failures to hold the device node:
972 `No such file or directory` (`/dev/fastrpc-sdsp` vanishes while SLPI is down mid
crash-cycle), 139 `Broken pipe`, 94 `Operation not permitted`, 9 `Resource busy`.
- So **`SNS_REG_INIT:36` fails BEFORE the SLPI requests any file.** Serving the registry
(staging path, dsp/sdsp libs, the gate, RestartSec=0) was answering a request that, at
this crash point, never happens. The registry-serving theory is DEAD for SNS_REG_INIT.
**Root cause (circular, self-reinforcing):** the SLPI auto-boots at qcom_q6v5_pas module
probe (~3s) → its registry-init needs a reverse-RPC to the AP → no listener registered yet
(hexagonrpcd is userspace, ~33s) → fails instantly at SNS_REG_INIT → crashes → the crash
tears down `/dev/fastrpc-sdsp` → reappears ~0.3s later → vanishes again. hexagonrpcd can
never hold a stable attach long enough to register its listener; the node thrashes faster
than it can attach + enter the listen loop. The rare earlier glimpses of `sar.cc:27` were a
hammering hexagonrpcd momentarily aligning with one restart's request window — not a stable
state. (Note: crash line drifts :36/:39 boot-to-boot; same failure, pre-file-request.)
**Real fix direction (Casey's architectural read, now proven):** the SLPI must NOT auto-boot
at module probe. Hold it stopped until hexagonrpcd is attached AND in its listen loop, then
start it once so its first registry request hits a ready listener. That's a kernel/remoteproc
ordering change (`auto_boot=false` for the SLPI PAS, or defer module bind), NOT more userspace
plumbing. Mirrors Android: SLPI firmware boots but sensor_process is triggered by the AP
(chrecd `chre_slpi_start_thread`) AFTER the listener/daemon is up — confirmed by subagent that
`INIT_ATTACH_SNS` does NOT itself start sensor traffic; chrecd does.
**Still-valid hygiene from S5/S6 (real, but does NOT fix SNS_REG_INIT):** the registry staging
path bug (`cp -a .../registry/.` was nesting `registry/registry/`; fixed to flatten) and the
`blueline-slpi-ready.service` gate pattern. `libchre_slpi_skel.so` IS needed for chrecd and was
found at `pixel3-work/vndlos-mnt/dsp/sdsp/` (whole `dsp/sdsp/` tree: skel + nanoapps +
fastrpc_shell_2); it's static vendor firmware, belongs in a firmware package, not a one-time copy.
## 2026-07-01 — crash-loop-vs-modem interaction found + fixed; SLPI ruled OUT of the modem 52 wall
Wired `blueline-slpi-ready.service` into the real boot chain (`modem-qrtr-ready.service`
`After=`/`Wants=` it, mirroring Android's `init.qcom.devstart.sh` which sets
`vendor.qcom.devup` — the property `modem-qrtr-ready` was modeled after — immediately after
kicking SLPI). This surfaced a real regression: `blueline-slpi-ready.sh` disabled SLPI crash
recovery for a "single-shot" start. When the SAR/CHRE fault crashed it with recovery disabled,
the driver had nowhere to go — glink-edge was left half torn down, storming
`intent request timed out` every ~10s indefinitely and taking the modem's own QMI/QRTR
transport down with it (`endpoint hangup`). Android never disables recovery for this
(`ssr_setup`/`ssr_diag` in `init.sdm845.rc` exist precisely to handle ongoing SSR cleanly).
**Fix:** recovery stays enabled through the boot attempt (so any crash tears down/rebuilds
glink cleanly via the kernel's own SSR path — verified: crash→recovering→running cycling with
zero storm across 50 crashes in one test), then the script catches SLPI in its brief `running`
window and issues a clean `echo stop` to park it — one attempt, not an infinite crash-loop,
since the modem does not benefit from SLPI staying up (see below).
**Decisive result, three separate live boots:** with SLPI crash-looping 50 times in ~7 minutes,
with SLPI crash-free and running continuously, and with SLPI cleanly parked/offline —
`mmcli -m 0 --enable` returned byte-for-byte identical `QMI protocol error (52): 'DeviceNotReady'`
every time. **SLPI state has zero measurable effect on the modem RF-arm wall.** This closes
"Subsystem coordination (SLPI PD state?)" from `los-mss-translated.c`'s delta list as a modem
blocker. SLPI's own SAR/CHRE crash (this file, above) remains real and unfixed, but is now
fully decoupled from modem bring-up — see `modem.md` TOP LEAD #12 for where the modem hunt
went next (`dms-ui-ready.py`, DMS 0x0064).

132
saf/device/touch.md Normal file
View file

@ -0,0 +1,132 @@
# PAF · Touch — ST FTS, reset polarity, GPI-DMA, tap-to-wake
## Current glass result — 2026-07-11
On kernel `7.1.1-sdm845-g56b0a053bef8`, daytime touch works and the FTS
recognizes DBLTAP. The current defect is downstream of gesture recognition:
the first double tap can prepare the panel and then immediately unprepare it
(green flash then black); a second double tap wakes the display. The first
half-wake is kernel-trace confirmed. Treat DT2W as **detected but unreliable**
until the screen-toggle/DPMS prepare-unprepare ping-pong is fixed. This is not
the old IRQ/FIFO bug.
Status 2026-07-10 (morning): TAP-TO-WAKE WORKS, verified on glass by
Casey on kernel d7a8f7dc9. FTS probes clean, gesture mask armed at boot,
double-tap wakes from armed sleep, touch alive after wake, hypridle 60s
idle screen-off enabled. The one remaining suspend-path defect was the
IRQ handler, not the controller — see the closed item below.
## Suspend-path doctrine (2026-07-10, learned the hard way)
- `gesture_enabled` MUST be 1 before any DPMS-off. Unarmed suspend takes
the SLPI-handoff branch (fts_suspend_work): controller handed to the
PARKED sensor island + IRQ disabled → resume races → level IRQ storm
(millions on IRQ 148) → kernel kills the IRQ → touch dead until driver
rebind or reboot. With SLPI's sar.cc wall unfixed, the handoff target is
unavailable — gesture-armed suspend (AP keeps the bus) is the only safe mode.
- The compiled gesture_mask sysfs is the STASH-THEN-APPLY variant:
`echo "01 20" >` stages only; the following `cat` applies (updateGestureMask)
and sets gesture_enabled=1. blueline-dt2w.sh does both since de4fe40.
- Live recovery from a dead touch IRQ, no reboot:
`echo 2-0049 > /sys/bus/i2c/drivers/fts/unbind && echo 2-0049 > .../bind`
— but Hyprland then holds stale input handles (taps flow at evdev level,
UI ignores them); a session restart or reboot is needed after rebind.
- VERIFIED 2026-07-10 (clean boot-time arm, Casey on glass): daytime touch
works, and touch SURVIVES a full sleep/wake cycle — the regression is
closed. Wake was power-button; double-tap did NOT wake.
- CLOSED 2026-07-10 (kernel d7a8f7dc9) — tap-to-wake works on glass. The
"controller never enters gesture scan" theory was WRONG: enterGestureMode
runs and succeeds at every armed suspend (Sense OFF → gesture mode →
enableGesture DONE in dmesg). The real defect was fts_interrupt_handler:
during armed sleep bus_refmask==0, so the bus-ref gate refused every IRQ
and returned WITHOUT reading the FIFO. Level IRQ line stayed asserted →
refired forever (the "582k IRQs" were the unserviced line, not full-rate
scanning), gesture events were dropped unread (why double-tap never
woke), and a bogus release call in the refusal branch logged one pr_err
per IRQ — 85k lines in one screen-off window, enough printk to freeze
the phone on long sleeps (2026-07-10 morning freezes). Fix: service the
FIFO without taking a bus ref when gesture_enabled && sensor_sleep
(taking one would queue a resume per IRQ), drop the bogus release,
ratelimit the bus-ref pr_err. The port's 30177cb7e (AP keeps bus in
gesture mode) had changed the ownership model without teaching the IRQ
path — classic lost-handler.
## The hardware truth (settled — do not re-litigate)
- This unit is the **LG panel + ST FTS** variant: FTS controller at
**0x49 on i2c-2 (888000 geni SE)**. Settled from per-unit persist
calibration + LOS init.blueline.rc.
- The Samsung `sec_ts@48` nodes in the downstream DTBO (fragment@81,
`sec,reset_gpio`, …) belong to the OTHER hardware variant. **Never cite
fragment@81 as evidence for our touch config** — that mistake shipped
ba0620d5c and killed touch for a day.
- Pins: reset tlmm 99 (**active-low RESETB**), IRQ tlmm 125 (level low),
AP↔SLPI bus switch tlmm 136.
## Reset polarity — the post-mortem
The ported vendor driver (`drivers/input/touchscreen/stm/`) uses **gpiod
logical assert semantics**: acquires reset `GPIOD_OUT_HIGH` ("asserted"),
pulse is `set(1)` → 20ms → `set(0)` → 50ms, and its own comment says
"active-low reset" (fts.c ~4320). Therefore the DT flag must be
**`GPIO_ACTIVE_LOW`**, so logical assert = physical LOW.
With `GPIO_ACTIVE_HIGH` (ba0620d5c, 2026-07-09 14:02) deassert drives the
pin physically LOW → RESETB held forever → controller NACKs every I2C
access → probe fails after 3 reset attempts. Signature in dmesg:
`gpi ... Error in Transaction: code=0x10 status=0x40` on EVERY read
(including the first 8-byte poll), `fts_system_reset: ERROR 80000002`,
`Probe Failed!`. Nobody noticed for a day because every boot in that window
had a black display. f3c5f5a11 reverts to ACTIVE_LOW.
**Triage rule:** all-reads-fail = controller not out of reset / not powered
/ bus switch wrong — a dead chip, not a DMA problem. Large-read-only
failure = the (historic) GPI-DMA wall below.
## GPI-DMA wall (historic — status needs one clean-boot check)
The 888000 SE is GPI-mode-only (its SE firmware has FIFO disabled — no
FIFO fallback exists; verified in i2c-qcom-geni probe). Historically small
reads worked and the first large read (209-byte readSysInfo) died with
`MSM_GPI_TCE_UNEXP_ERR`. Instrumentation ff6e6e2e8 logs completion status +
error-log register on any recurrence.
**ANSWERED 2026-07-10, first clean f3c5f5a11 boot: the wall is GONE.**
Zero `gpi ... Error in Transaction` lines in dmesg; the driver read the
105,140-byte firmware file and ran CX/Panel CRC verification over the bus
(`Fw Update Finished! error = 00000000`), FTS input device registered.
Large GPI-DMA transfers on the 888000 SE work. Whatever ate the 209-byte
read in the 2026-07-08 era was fixed by the intervening driver/power work
or was reset-state fallout all along; if it ever recurs, the ff6e6e2e8
telemetry will say why. Deep diagnosis archive: ../CLAUDE.md "Touch /
FTS-on-mainline key facts".
## Gesture / suspend design (in-tree, working as of 07-09 morning)
- 08687bec9 power+reset sequencing, e31aa9da7 prototype fix,
30177cb7e keep AP bus ownership during gesture mode,
3d9eb1028 sensor_sleep bookkeeping in gesture-mode suspend.
- Gesture mode: on suspend the driver keeps the controller alive for
double-tap; `XF86WakeUp` → Hyprland bind → `blueline-screen-toggle`
(see display.md). dt2w service wiring: `blueline-dt2w.service` in
rootfs-overlay (uncommitted edits pending as of 2026-07-10).
- **Proximity gate (2026-07-20):** `blueline-screen-toggle` checks
`net.hadess.SensorProxy` `ProximityNear` over D-Bus before dispatching
DPMS on; if blocked (phone in pocket) the wake is suppressed and logged,
the FTS gesture still fires XF86WakeUp either way. Fails open if the
D-Bus query errors, so a dead sensor proxy never permanently blocks wake.
Requires `iio-sensor-proxy` running, which is now always-on (see slpi.md).
This closes half the "hot pocket" problem; the other half (screen
already on + locked, phone pocketed) is `blueline-proximity-lock`
(`overlays/systemd-user/`), a `monitor-sensor --proximity` watcher that
blanks via the same `blueline-screen-toggle off` path when locked.
## THE GOAL (unchanged): one FTS driver = daytime touch + tap-to-wake
Like LOS: FTS drives the controller when awake; on suspend it hands the
controller to the SLPI over the tlmm 136 switch, where the
`sns_touch_gesture` nanoapp (SLPI's own i2c bus 5) watches for double-tap
and wakes the AP. stmfts (mainline driver) can never do this — no switch,
no handoff; it is fallback-only. Two tracks converge:
1. FTS daytime path — WORKING modulo the polarity fix verify.
2. SLPI-side tap-to-wake — unblocked by the sar.cc fix (see slpi.md), but not
required for the current AP-side gesture design.