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
37 KiB
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:
- Open QMI device (QRTR node)
- Allocate PDC client
- Connect
load-configindication handler - Loop:
QMI_PDC_LOAD_CONFIG_REQ(chunk) → indication → next chunk - 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:
- Open QMI device
- Allocate PDC client
QMI_PDC_SET_SELECTED_CONFIG_REQ(type + ID + token)- On
set-selected-configindication →QMI_PDC_ACTIVATE_CONFIG_REQ(type + token) - On
activate-configindication → 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:
persist.vendor.radio.hw_mbn_loaded/sw_mbn_loadedis set AND version matches modem active config → skip reload (fast path).- On warm SSR: query get_active_config_info; if active matches → skip reload.
- 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
REQUEST_MBN_HW_INIT→qcril_mbn_hw_update_init_hdlr: Initialize HW update state machine.REQUEST_MBN_HW_GET_SELECTED_CONFIG→qcril_mbn_hw_query_selected_config_hndlr: CallQMI_PDC_GET_SELECTED_CONFIGfor type=PLATFORM. If active config matches candidate, skip to SW phase.REQUEST_MBN_HW_LOAD_CONFIG→qcril_mbn_hw_load_config_hndlr: ChunkedQMI_PDC_LOAD_CONFIGfor the selected HW MBN file.REQUEST_MBN_HW_SELECT_CONFIG→qcril_mbn_hw_select_config_hndlr:QMI_PDC_SET_SELECTED_CONFIGfor type=PLATFORM with the just-loaded config ID.REQUEST_MBN_HW_ACTIVATE_CONFIG→qcril_mbn_hw_activate_config_hndlr:QMI_PDC_ACTIVATE_CONFIGfor 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:
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).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).REQUEST_MBN_SW_GET_SELECTED_CONFIG→qcril_mbn_sw_query_selected_config_hndlr: QueryQMI_PDC_GET_SELECTED_CONFIGfor type=SOFTWARE. Version-check against candidate.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.REQUEST_MBN_SW_LOAD_CONFIG→qcril_mbn_sw_load_config_hndlr: ChunkedQMI_PDC_LOAD_CONFIGfor the selected SW MBN file (type=SOFTWARE).REQUEST_MBN_SW_SELECT_CONFIG→qcril_mbn_sw_select_config_hndlr:QMI_PDC_SET_SELECTED_CONFIGfor type=SOFTWARE.REQUEST_MBN_SW_ACTIVATE_CONFIG→qcril_mbn_sw_activate_config_hndlr:QMI_PDC_ACTIVATE_CONFIGfor type=SOFTWARE. Second modem restart.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_storageserves EFS read-write; the modem writes ~2 MB tomodem_fs2AFTER 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 L38is 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
[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-statusto get ICCID, thenqmicli --nas-get-home-networkor--uim-get-imsito 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)
- Load: use
pdc_load.pylogic (already working) with type=PLATFORM. - Get config ID: after load completes, call
QMI_PDC_LIST_CONFIGStype=PLATFORM to find the ID of the just-loaded config. (The LoadConfig indication does not return the ID; ListConfigs does.) - Select:
QMI_PDC_SET_SELECTED_CONFIGtype=PLATFORM, id=. - Activate:
QMI_PDC_ACTIVATE_CONFIGtype=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.
- Load (chunked, type=SOFTWARE)
- List configs → get ID
- SetSelectedConfig (type=SOFTWARE)
- 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):
# 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.mdfrontier #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_updatedfindsprev_ver == cur_verand 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 anypdc_enable_auto_selectioncall) 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 +modemstpersistence (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:
# 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)
-
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-refreshexperiment or timing capture. -
What is
pdc_enable_auto_selection(ii)/qcril_qmi_pdc_enable_modem_update? The symbol_Z25pdc_enable_auto_selectioniiexists in the binary. It may correspond toQMI_PDC_SET_FEATURE_VERSIONor 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 ofqcril_mbn_hw_update_initto determine if it's called before the load sequence. This is the only remaining significant unknown. -
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.
-
Is the HW MBN name match on "SDM845-LA-SS" or another string?
qcril_mbn_hw_get_hw_name_to_look_forreturns 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 |