结论:CellularManager 用一个 busy 布尔值代表 eSIM 异步操作状态,但它不是互斥锁,也没有拒绝重入;switch_profile() 会先把本地缓存里目标 ICCID 标成 enabled 并立刻刷新页面,属于乐观切换,不等于运营商注册成功。轮询失败只把状态从「真」降为「未知」并保留旧列表,必须连续两次为假才清空——因此「界面上还列着 profile」不证明本次探测健康。
本文基于固定源码快照对 CellularManager 的并发与 UI 回执做静态分析,不执行 APDU、下载、切卡或网络注册。eUICC 协议本身的下载流程、LPA 通道归属,以及页面入口属于相邻专题;这里只关注「请求提交后,主机到底知道什么」这一层。
并发:一个 busy 布尔值管不了两个操作
_run_operation() 被调用时立刻把 busy 置为真,然后启动一个 daemon worker;worker 持有 _lock 执行 LPA 调用,默认随后调用 list_profiles(),无论成功还是异常,都只把结果排队交给 UI 线程的 _finish() 处理。_lock 保证 LPA 调用被串行化,但不保证请求唯一、不保证 deadline、不保证可取消,也不校验调用方身份。方法自身没有 if busy 分支去拒绝第二个操作。
_finish() 在 UI 回调里把 busy 置假,并把下一次周期轮询延后约 5 秒。问题在于两个操作的完成回调会依次清同一个布尔量:第一个回调执行时,第二个操作可能仍在运行。mici 页面多数按钮在调用前检查 busy,但管理器公共方法不设这道门,所以「UI 看起来只有一个操作在跑」是页面层的行为,不是管理器的并发契约。
| 状态字段 | 维护位置 | 它真正代表什么 | 它不保证什么 |
|---|---|---|---|
busy |
_run_operation() 置位,_finish() 清位 |
「至少有一个操作处于已提交或执行中」 | 不是互斥锁;不计数、不拒绝重入、不代表唯一请求 |
polling |
_poll_profiles() 置位,_finish_poll() 清位 |
「本轮 profile 轮询正在进行」 | 轮询异常只清标记并记日志,不触发操作错误回调 |
profiles |
UI 线程按结果回调更新 | eUICC/LPA 视图下的 profile 缓存列表 | 不代表 modem 已注册蜂窝网络;乐观切换会先改此缓存 |
is_euicc |
每轮 _poll_profiles() 重新探测 |
本轮探测的 SIM 是否 eUICC,取值含未知 | 连续两次为假才清列表;单次为假只降为未知 |
modem_state |
process_callbacks() 空闲时读取 |
硬件快照 | 读取无异常捕获、无独立 deadline;抛错会传播到 UI 调用方 |
_ensure_lpa() 与 _enqueue() 负责首次按平台取得 LPABase 并缓存实例,后台线程用短锁保护回调队列。SIM 是否为 eUICC 每轮重新探测,但 LPA 对象本身不会每轮重建。这一点决定了锁的粒度:锁保护的是「队列与单次 LPA 调用」,而不是「一次业务事务」。
乐观切卡与结果回执:两个不同的层面
switch_profile() 先用 replace() 把目标 ICCID 在缓存列表中标记为 enabled,并立即触发页面更新;worker 随后执行切换与通知处理,但显式指定 relist=False,成功后不马上读取新的 profile 列表。真正的校正要等下一次周期轮询。EsimUI._on_profile_clicked() 还会把按钮移到前端,进一步强化「已经切好了」的观感。
即使随后的 LPA 调用成功,运营商注册、数据可用性和路由切换仍是其他系统的状态,不在这个布尔量或这个列表里。delete_profile()、download_profile()、nickname_profile() 都通过同一 worker 入口运行,默认在操作后重列;这份列表代表 eUICC/LPA 视图,不证明 modem 已注册蜂窝网络。失败会展示错误并安排刷新,但错误与回滚之间存在异步窗口。
| 操作 | 成功路径的即时行为 | 回执边界 |
|---|---|---|
switch_profile() |
先改本地 enabled 位并刷新页面;relist=False,成功不立即重列 |
乐观状态要等下次周期轮询校正;不等于激活确认 |
delete_profile() |
经同一 worker 运行,默认操作后重列 | 列表刷新不等于运营商注册或数据连通 |
download_profile() |
经同一 worker 运行,默认操作后重列 | 下载完成仍须核对新实体是否恰好新增一个 |
nickname_profile() |
经同一 worker 运行,默认操作后重列 | 仅改标签,不改变注册状态 |
通知处理这一层也独立:execute_and_process_notifications() 在操作后等待 1 秒,并在 finally 中尝试处理通知;通知处理异常只打印到 stderr,不重新抛出。管理器可能把核心操作视为成功,但通知投递单独失败,二者不能合并成同一个布尔值。
轮询与缺测:失败不等于无卡
_poll_profiles() 在后台线程锁内先调用 is_euicc(),为真才列 profile,结果回 UI。_finish_poll() 在忙碌时丢弃本轮结果;若轮询抛异常,只清 polling 并记日志,没有操作错误回调。更关键的是状态迁移:先前为真而本轮为假时,会把状态设为 None 且保留旧列表,下一次再为假才清空。所以 UI 上仍有 profile,并不证明本轮探测健康。
process_callbacks() 在空闲且距上次轮询至少 5 秒时,于 UI 调用线程同步读取 HARDWARE.get_modem_state(),这里没有捕获硬件状态读取异常,也没有该读取的独立 deadline。离线探针确认:空闲轮询会在读取硬件状态之前刷新 _last_profile_poll,因此 get_modem_state() 抛错会传播到 UI 调用方,并在同一时间点的下次调用中抑制重试。
| 观测现象 | 源码行为 | 不能推出的结论 |
|---|---|---|
本轮 is_euicc 为假,前次为真 |
状态降为未知,保留旧列表;第二次为假才清空 | 不能判定「现在无卡」 |
| 轮询抛异常 | 仅清 polling 并记日志,无错误回调 |
不能判定当前列表是新鲜结果 |
| 操作失败并展示错误 | 安排重新轮询,但不回滚乐观列表 | 错误与回滚之间仍有余量窗口 |
| 安装弹窗存在时列表更新 | 只有恰好新增一个 ICCID 才走普通激活,其他情况也关弹窗 | 不能把「刷新成功」当「安装成功且可用」 |
下载完成后的核对同样独立:EsimUI._on_profiles_updated() 在安装弹窗存在时比较新旧 ICCID,只有恰好新增一个才进入普通激活流程,其他情况也会关闭安装弹窗。列表刷新、安装成功、新 profile 可用不能压缩成同一布尔。
面向硬件与插件的契约建议
把 eUICC 能力视为受信任设备服务时,每个请求应带唯一 ID、SIM/卡槽身份、操作种类、deadline 与可恢复阶段,LPA 通道锁只由该服务持有。服务要分别回报 accepted、APDU/HTTPS 操作完成、通知处理、profile 重列、modem 注册与数据连通性这几种状态。
- 多请求状态按操作 ID 计数或排队,不应由一个
busy布尔值代表。 - 硬件状态读取失败要保留错误并约定重试,而不是吞掉或抑制下一次调用。
- 禁止第三方 UI 插件直接操作 AT/电源,或以
busy和乐观 enabled 位作为切卡完成证明。 - 台架应注入连续请求、SIM 热插拔、AT 忙/超时、通知失败、列表短暂陈旧、网络失联及 UI 离页后回调。
需要保留的边界:本归档没有实卡,也没有上述故障注入结果,探针只复现 Python 回调顺序,不构成原生线程抢占、UI 帧调度或实卡状态的验证。
常见问题
busy 为真时再提交一个操作会发生什么?
管理器公共方法没有 if busy 拒绝分支,第二个操作仍会进入 worker。worker 的 LPA 段由 _lock 串行,但两个完成回调会依次把同一个 busy 清零——第一个回调执行时第二个操作可能还在运行。这是条件性源码推导,源文档明确未做并发交错实测。
点了切卡、界面已经高亮,是不是就切好了?
不是。switch_profile() 先用 replace() 把目标 ICCID 标为 enabled 并立即刷新页面,且设置 relist=False,成功不立即重列,属于乐观切换。运营商注册、数据可用性与路由切换是其他系统状态,本地 enabled 位不等于激活确认。
轮询失败会不会把 profile 列表清掉?
不会一次清空。先前为真而本轮为假时,状态先转成未知并保留旧列表,第二次再为假才清空;轮询异常只清 polling 并记日志,没有操作错误回调。所以列表还在,并不表示本轮探测健康。
下载完成就等于新卡可用吗?
不等于。源码在安装弹窗存在时比较新旧 ICCID,只有恰好新增一个才进入普通激活流程;列表刷新、安装成功、新 profile 可用是三种不同状态。通知处理还可能单独失败,只打印到 stderr 不重抛。
延伸阅读
- eSIM LPA:TLV、APDU 与下载流程的边界 —— 本文关注主机回执,该篇补齐 eUICC 协议侧的下载步骤。
- 网络设置里的 eSIM 入口:页面如何驱动切卡与轮询 —— 解释
mici页面按钮的busy检查为何只是页面层行为。 - modem 的 PPP、路由与 DNS 归属契约 —— 说明「profile 已启用」与「蜂窝已注册、路由已切换」为何分属不同层。
参考资料
- 项目 02 ——
openpilot-research/CELLULAR_MANAGER_ESIM_ASYNC_RESULT_CONTRACT_DEEP_DIVE.md(取用了CellularManager的函数级时序、五项结果竞态边界与无设备回调/轮询探针结论)。 - 项目 02 ——
openpilot-research/ESIM_LPA_TLV_APDU_DOWNLOAD_DEEP_DIVE.md(仅用于区分主机回执与 eUICC 下载协议,未取用其实卡验证内容)。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复