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
680 lines
37 KiB
Markdown
680 lines
37 KiB
Markdown
# 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 10–30 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 |
|