标签: eSIM

  • eSIM 异步操作:busy 布尔值与乐观切卡都不等于成功

    结论: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 注册与数据连通性这几种状态。

    1. 多请求状态按操作 ID 计数或排队,不应由一个 busy 布尔值代表。
    2. 硬件状态读取失败要保留错误并约定重试,而不是吞掉或抑制下一次调用。
    3. 禁止第三方 UI 插件直接操作 AT/电源,或以 busy 和乐观 enabled 位作为切卡完成证明。
    4. 台架应注入连续请求、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 不重抛。

    延伸阅读

    参考资料

    • 项目 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

  • 二维码编码只到版本 20、纠错固定 L:openpilot 编解码两端并不对称

    结论:openpilot 的二维码模块编码端与解码端能力并不对称:编码只覆盖版本 1 到 20、只构造 byte mode,并固定纠错等级 L 与 mask 0;解码端则支持版本 1 到 40 与四个纠错等级,还会尝试相邻版本、alignment 与转置等候选。更重要的产品边界是:二维码只是载体不是授权——配对令牌取不到时会退化成空令牌 URL,而这个 URL 仍能成功编码成图形,所以「屏幕上有二维码」不证明令牌有效;eSIM 扫码的前置检查也只校验字符串形状。以下为固定源码快照 c8fb906 的静态分析与隔离测试结论。

    编码与屏幕呈现

    make_texture() 把输入字符串编码为 UTF-8,按 _capacity() 从版本 1 到 20 选择首个可容纳的码型,超过上限抛 ValueError。_data_codewords() 只构造 QR 的 byte mode,写入 8/16 位长度、终止位与交替填充字节;_codewords() 按 _EC 表分块、计算 GF(256) Reed-Solomon 余数并交织。当前编码固定纠错等级 L 和 mask 0,并不搜索视觉质量最佳的 mask,编码版本范围也比解码范围窄。

    _Qr 绘制 finder/timing/alignment、格式位与版本位,再以两列之字形填入数据。普通模式外加 4 模块 quiet zone,反色模式不加边框;两者都把每个模块放大为 10×10 RGBA 像素后通过 raylib 上传纹理。make_texture() 的注释认为上传会复制像素,因此中间 NumPy 数组无需保持存活,但实际 GPU 驱动资源生命周期仍由 UI 调用方负责。

    项 编码端 解码端
    版本范围 1 到 20 1 到 40
    纠错等级 固定 L 四个等级均支持
    字符模式 仅 byte mode(UTF-8) 数字、字母数字、byte、Kanji 与受支持 ECI
    mask 固定 mask 0,不搜索 从格式位恢复级别/mask

    配对 QR 的展示边界

    大屏 PairingDialog._generate_qr_code() 先释放旧纹理,再把 DongleId 与配对 JWT 拼入 URL 制作新纹理,每 300 秒刷新,失败时记录日志并清空纹理。二维码中可能有配对令牌,视觉截图、投屏与诊断日志应视为敏感路径;生成纹理只负责表示 URL,不能证明 URL 仍有效或账户已授权。

    mici 的 QR 控件用反色纹理,析构时卸载;__init__() 立即生成纹理,失败时 _texture=None,_render() 改画错误标签。显示缩放只按矩形高度计算,不按宽度取较小比例:若插件给非方形且宽度更窄的矩形,二维码可能横向溢出(当前控件默认建为正方形,配对页还会把它设为屏幕高度的正方形,因此这是组件复用边界而非已实测故障)。

    未配对时 PrimeScroller.__init__() 先为 QR 调用一次 _get_pairing_url() 并生成纹理,但 _last_pairing_qr_generation 初始化为负无穷,首次 _render() 会再次请求令牌、释放初始纹理并生成新纹理,之后按 300 秒周期刷新;这意味着首次展示有两份请求/纹理工作,并可能在两个请求之间取到不同令牌。刷新的时间戳即使 _generate_qr_code() 失败也会更新,错误标签将持续到下一次刷新或重建。若令牌 API 抛异常,_get_pairing_url() 返回 https://connect.comma.ai/?pair=;该字符串仍可成功编码为二维码,故有 QR 图形并不证明配对令牌有效。

    图像到文本的解码链

    顶层 decode() 先定位 finder,再尝试估计版本及相邻两个版本、是否使用 alignment、矩阵是否转置;候选均失败返回 None。它仅捕获 QRError,并非对任意形状、任意 dtype 或超大数组都提供「失败返回 None」的保证。二值图与游程/透视运算会分配与输入像素数相关的内存,外部插件传入图像前必须限制形状、大小与处理预算。

    阶段 关键函数与限制
    灰度二值化 _binarize() 分块取样、算局部对比度并参考邻块/全局阈值;边长小于 21 像素抛 QRError,假定二维灰度数组。
    finder 定位 _find_patterns() 以 1:1:3:1:1 横向候选、纵向复查去重,至少约 2 像素/模块;_pick_finders() 从前十个候选中选几何最接近直角等边的三点。
    透视采样 _perspective() 解单应矩阵;_sample() 可用 alignment pattern 修正右下角,采样越出图像抛 QRError。
    矩阵恢复 decode_matrix() 只接受 21..177 且边长满足 4v+17 的方阵;从两份格式位选汉明距离不超过 3 的级别/mask,再反掩码、去交织。
    纠错与文本 _rs_correct() 做 syndrome、Berlekamp-Massey、错误定位/幅值求解并复验;无 ECI 的 byte 段先试 UTF-8,失败退回 Latin-1。

    eSIM 扫码调用与信任边界

    QRScannerDialog 使用座舱 CabinCameraView:显示时打开驾驶员视角并关闭红外,隐藏时反向恢复;它最多约每 0.25 秒启动一次扫描,上一扫描线程仍活跃时不再启动新线程。取帧时只复制 NV12 的 Y 平面有效宽度,避免 VisionIPC 缓冲回收后后台线程读到复用数据,随后在后台调用 qrcode.decode(gray)。扫码 UI 的轮询与相机/线程生命周期不等于二维码算法内置超时。

    识别到文本后,UI 先用 parse_lpa_activation_code() 检查 LPA: 前缀与三个非空 $ 分段,失败展示短时错误;通过则让用户输入昵称,再交给蜂窝管理器下载 profile。这个前置检查只是字符串形状校验,不认证 SM-DP+ 服务器、激活码归属或 eUICC 安装授权。插件化时应把「二维码像素识别」「业务数据格式校验」「服务端授权」分为三层:配对展示插件只接收短时令牌的受控呈现能力,扫码插件只输出有长度上限的候选字符串,不直接触发下载或持有 eUICC/网络权限。

    已有测试与待验证项

    test_qrcode.py 使用独立生成、打包的 .npz 矩阵 fixture,覆盖 1 到 40 版本、四种纠错级别、多种文本模式/ECI、噪声、旋转、镜像、透视、边缘贴码与无二维码图像,还检查自有编码器版本 1 到 20 的矩阵往返。通过隔离脚本执行原测试文件的 13 个测试方法全部通过,其中版本/纠错等级循环含 160 组矩阵与渲染图检查,纹理编码往返含 20 个版本;隔离器仅提供 pyray.Texture 的导入期类型桩。

    但该测试不覆盖 make_texture() 的 raylib 上传,也不验证真实车载相机曝光/畸变、GPU 纹理生命周期、恶意超大图像或 eSIM 服务端授权。新显示硬件需在重复开闭、令牌失败、图形上下文销毁/重建与非方形布局下检查 GPU 纹理数量与 QR 可扫性。

    常见问题

    屏幕上有配对二维码就说明令牌有效吗?

    不能。令牌 API 抛异常时 _get_pairing_url() 返回 https://connect.comma.ai/?pair=,这个空令牌 URL 仍可成功编码成二维码。生成纹理只负责表示 URL,不证明令牌有效或账户已授权。

    为什么首次进入配对页会出现两次令牌请求?

    因为 PrimeScroller.__init__() 已为 QR 调用过一次 _get_pairing_url() 并生成纹理,而 _last_pairing_qr_generation 初始化为负无穷,首次 _render() 会再次请求令牌、释放初始纹理并生成新纹理;两个请求之间可能取到不同令牌。

    扫码识别到 LPA 激活码就等于能装成功吗?

    不等于。parse_lpa_activation_code() 只检查 LPA: 前缀与三个非空 $ 分段,是字符串形状校验,不认证 SM-DP+ 服务器、激活码归属或 eUICC 安装授权。

    编码端和解码端的版本范围一样吗?

    不一样。编码端只覆盖版本 1 到 20、固定纠错等级 L 与 mask 0;解码端支持版本 1 到 40 与四个纠错等级,并会尝试相邻版本、alignment 与转置等候选。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/QR_CODE_PAIRING_ESIM_PIPELINE_DEEP_DIVE.md(编码/解码两端能力、配对 QR 展示边界与 eSIM 扫码信任边界)。
    • 项目 02 —— openpilot-research/ 同目录探针记录:原二维码测试文件的 13 个隔离测试方法与 160 组矩阵检查结论。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 车载网络设置:Wi-Fi 5 秒扫描、热点密码与 eSIM 页面三层门

    结论:车载网络设置分三层:大屏 NetworkUI 只有 Wi-Fi 与高级网络(未接入 eSIM 页),mici 才把 Wi-Fi、eSIM、热点、APN 与计费并置;Wi-Fi 由 WifiManager 通过 NetworkManager D-Bus 管理,后台约每 5 秒扫描一次,并用用户 epoch 防止慢查询覆盖刚选择的网络;eSIM 操作经 CellularManager 后台线程串行化后落到 LPABase。UI 显示/操作只是体验门,不是操作系统级授权。本文为固定源码快照 c8fb906 的静态分析,未连接 NetworkManager、真实 eUICC 或蜂窝模组。

    三层界面:大屏、mici 与平台接口

    大屏侧 SettingsLayout 创建共享的 WifiManager,在 Wi-Fi 与高级网络两页间切换;大屏代码未接入 CellularManager 或 eSIM 管理页。WifiManagerUI 负责扫描结果、保存网络、连接/忘记、隐藏 SSID 与密码重试;AdvancedNetworkSettings 管理热点、热点密码、IP、APN、漫游及蜂窝与 Wi-Fi 计费设置。mici 侧 NetworkLayoutMici 把 Wi-Fi、eSIM、热点、APN、漫游及计费共置在导航滚动页,EsimUI 提供配置文件列表、切换/删除/命名与扫码添加。

    两屏的策略判定目前并不统一:大屏 AdvancedNetworkSettings 使用 PrimeType.NONE/LITE 判断显示项,mici 使用 not ui_state.prime_state.is_full_prime()。两者意图相近,但不是同一个策略函数,插件化时应避免继续复制业务判定。UI 显示隐藏不能保护直接调用 Params、D-Bus 或 LPA 的本地进程。

    入口 组件和重要函数 作用
    大屏设置 SettingsLayout、NetworkUI 创建共享 WifiManager,在 Wi-Fi 与高级网络两页间切换;未接入 eSIM 管理页。
    大屏 Wi-Fi WifiManagerUI 扫描结果、保存网络、连接/忘记、隐藏 SSID、密码重试。
    大屏高级网络 AdvancedNetworkSettings 热点、APN、漫游及蜂窝/Wi-Fi 计费设置。
    mici 设置 NetworkLayoutMici Wi-Fi、eSIM、热点、APN、漫游及计费共置在滚动页。
    mici eSIM EsimUI 配置文件列表、切换/删除/命名、扫码添加;操作受 UI 门限制。

    Wi-Fi 状态机与用户 epoch

    WifiManager.__init__() 连接系统 D-Bus,维护 ssid -> connection path、WifiState、网络列表和待执行回调。初始化线程等待无线设备,启动扫描与 NetworkManager 信号监听,再同步已有连接、热点连接和当前状态;扫描线程约每 5 秒发起一次扫描,set_active() 在页面重新激活时立刻异步同步 Wi-Fi 状态和列表。

    连接流程是:用户选择 SSID 后 _set_connecting() 进入 CONNECTING;已保存网络走 ActivateConnection,新网络先删除同 SSID 旧配置再 AddAndActivateConnection2(volatile);收到 ACTIVATED 说明连接成功,随后调用 NetworkManager Save() 持久化临时连接;NEED_AUTH 或 FAILED/NO_SECRETS 组合触发密码重试回调。用户操作调用 _set_connecting() 递增 _user_epoch;监听线程在慢 D-Bus 查询前后比较该值,避免旧查询覆盖用户刚选择的新网络。这是针对已知竞态的保护,不能推断整个管理器所有共享字段都具有完整同步保证。

    connect_to_network() 先删除同 SSID 配置,再以 persist=volatile 建立连接;activate_connection() 使用已有连接配置;forget_connection() 删除配置。process_callbacks() 在 UI 线程执行回调,避免后台线程直接操作控件。get_security_type() 当前只返回 OPEN、WPA 或 UNSUPPORTED,不能根据 WPA2/WPA3 枚举名称推断兼容性。

    热点与计费标志

    _add_tethering_connection() 创建 AP 模式、共享 IPv4 连接,SSID 为 weedle 加设备 ID 前四位(若有),默认 PSK 是 DEFAULT_TETHERING_PASSWORD;已有热点密码可由用户修改,不能据常量断定任意在用设备仍使用默认值。量产硬件应在首次启用前强制生成唯一强密码,并审计恢复出厂与日志泄漏路径。

    set_current_network_metered() 更新 NetworkManager 当前 Wi-Fi connection 的 metered 字段,热点时跳过;mici 页面注释指出计费控件在切换网络后可能短暂显示旧状态,且重新启用按钮依赖扫描更新而非直接设置信号。计费标志最终汇入 NetworkMetered:hardwared 从硬件网络状态计算,经 deviceState 并写 Params,updated 读取它,在计费网络且未超时、非用户主动请求时推迟常规更新拉取;超过三天或用户请求可继续,不能解释为绝对禁止联网。

    用户设置 写入与消费 车载含义
    GsmRoaming 大屏/mici 写 Params;modem 读取 蜂窝漫游策略;变更生效时序按 modem 状态机验证。
    GsmApn 两种 UI 写/删除 Params;modem 初始化并检查变化 蜂窝接入点;空白表示回到自动配置。
    GsmMetered 两种 UI 写 Params;HardwareComma.get_network_metered() 读取 蜂窝链路是否被视为计费网络。
    Wi-Fi metered WifiManager 写 NetworkManager 配置 Wi-Fi 计费标志;来源不是 GsmMetered。
    NetworkMetered hardwared 计算并写 Params;updated 读取 常规更新在计费网络推迟,超三天或用户请求可继续。

    eSIM:从扫码到 LPA

    CellularManager.process_callbacks() 由 UI tick 调用,执行后台结果回调,并在空闲时按 5 秒间隔读取 modem 状态和轮询 eUICC。_run_operation() 将下载、删除、命名、切换放入后台线程,以锁串行化 LPA 调用,完成后回到 UI 线程更新列表/错误。切换 profile 先乐观更新 UI,失败时触发重新轮询;_finish_poll() 对偶发 is_euicc=False 再确认一次,避免 AT 忙时立刻清空显示。这些是呈现策略,不代表 modem 已注册到新运营商网络。

    平台接口是 LPABase,comma 设备的 HardwareComma.get_sim_lpa() 提供实体实现。LPA._acquire_channel() 先文件锁再打开 ISD-R 通道,结束时关闭;AtClient 负责串口 AT 命令、错误/超时与重连。parse_lpa_activation_code() 检查 LPA:...$...$... 基本格式,download_profile() 通过 ES9+ 依次认证服务端/客户端、获取 Bound Profile Package 并经 APDU 安装。EsimUI 侧非 Full Prime 才允许配置文件操作,添加时还要求设备网络类型非 none,这属于 UI 门、不是后端安全授权。

    扫码页 QRScannerDialog 从座舱相机帧复制灰度 Y 平面给解码线程,避免底层视频缓冲被复用;显示时写 DisableDriverCameraIR=True、IsDriverViewEnabled=True,离开时写回 False,并没有保存/恢复进入前的原值。和其他座舱相机使用者并存时可能覆盖其意图,需在新硬件上做互斥/租约设计与测试。

    常见问题

    大屏设置里为什么找不到 eSIM 页?

    因为大屏 NetworkUI 只创建共享 WifiManager,代码没有接入 CellularManager 或 eSIM 管理页;eSIM 配置文件列表与扫码添加位于 mici 的 EsimUI。

    切换 Wi-Fi 时为什么新选的网络不会被旧查询覆盖?

    因为用户操作会递增 _user_epoch,监听线程在慢 D-Bus 查询前后比较该值,避免旧查询结果覆盖用户刚选择的新网络。这是针对已知竞态的保护,不代表所有共享字段都有完整同步保证。

    热点默认密码是固定的吗?

    源码确认默认 PSK 来自 DEFAULT_TETHERING_PASSWORD,SSID 为 weedle 加设备 ID 前四位(若有),已有热点密码可由用户修改;因此不能据常量断定任意在用设备仍使用默认值,量产硬件应在首次启用前强制生成唯一强密码。

    UI 上隐藏了蜂窝高级项就等于进程无权操作吗?

    不等于。大屏用 PrimeType.NONE/LITE、mici 用 is_full_prime() 判定可见性,这属于 UI 门,不是安全授权;UI 显示隐藏不能保护直接调用 Params、D-Bus 或 LPA 的本地进程。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/NETWORK_SETTINGS_ESIM_DEEP_DIVE.md(网络设置三层界面、Wi-Fi 状态机、热点与计费、eSIM 到 LPA 的链路)。
    • 项目 02 —— 同目录记录:test_handle_state_change.py 的密码错误、忘记网络、自动连接与竞态用例,以及各屏布局未在本次运行验证的说明。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • eSIM LPA 下载链路:120 字节分片与第 257 片序号回绕

    结论:eSIM 的 LPA 下载不是一次独立的串口调用,而是一段横跨 AT 锁、eUICC 逻辑通道与 HTTPS 的长交易:AT+CCHO/CGLA/CCHC 打开 ISD-R 通道,profile 数据按 120 字节分片经 STORE DATA APDU 下发,整笔 ES9+ 下载在持有同一把 AT 锁期间完成。因此网络慢或安装耗时会持续占用共享串口,阻塞同样抢锁的 modem 管理。本文为固定源码快照的静态结构分析,不是可直接在车端照做的配置步骤。

    AT 锁的作用域比单条指令长

    平台接口 LPABase 定义了 profile 列举、下载、删除、命名、切换与 eUICC 探测;comma 平台实现是 LPA。CellularManager._run_operation() 在后台线程串行化 UI 操作,再把结果回调排到 UI 线程,CLI 也走同一接口。源码在这里明确:UI 可见性不是底层授权。

    LPA._acquire_channel() 先经 _acquire_lock() 对 /dev/shm/modem.lock 做排他 flock,再打开 ISD-R 逻辑通道,退出时尝试关闭。AtClient.query() 以 AT+CCHO/CGLA/CCHC 访问 eUICC,发生 SerialException 会重连并重试一次;TimeoutError 不进入该重试。open_isdr() 至多十次,第六次失败之后执行一次模组重置,第十次失败后仍先睡眠再抛最终异常。send_apdu() 在协议结果异常时可重试最多三次。

    关键在锁的作用域:LPA.download_profile() 在 _acquire_channel() 内部调用完整的 download_profile(),其中串行执行多次 HTTPS ES9+ 请求与 BPP 安装;process_notifications() 同样在锁内访问 HTTPS。而每个 HTTP 请求的 HTTP_TIMEOUT=30 秒只是单请求超时,不是整笔交易的总 deadline。共享串口的 Serial.write()/flush() 在本层也没有写超时。插件化后应分离「短 AT 交易租约」与「长网络等待」。

    TLV 与 APDU 的数据边界

    iter_tlv() 按 tag、短/长形式 length、value 迭代,encode_tlv() 执行反向编码;string_to_tbcd() 把 ICCID 十进制数字编码为 TBCD,奇数位填 F;decode_struct() 按字段映射转 profile 属性。解析器遇截断 tag、length 或 value 时只是 break,不会抛出「数据损坏」;size=0x80 被当作长格式且后续长度字节数为 0,结果是零长度值,而不是明确拒绝 BER indefinite length。

    es10x_command() 把请求按 120 字节分片,构造 STORE DATA APDU;遇 SW1=0x61 继续 GET RESPONSE,并把 SW1 高半字节为 0x9 的状态视为该片完成,其他状态抛异常。序号是 sequence & 0xFF,本层未限制超过 256 个分片的请求;也未为 APDU 响应累计设置全局大小或循环次数。

    构造输入 原函数观察结果 边界
    合法 5A 02 31 32 产出 (0x5A, b'12') 正常 TLV 解出。
    截断 5A 03 01 产出空列表 截断静默停止,保留前面已解析条目。
    5A 80 产出零长度 5A 值 被当作长格式,非拒绝。
    121 字节请求 拆为 120+1 两片并拼接响应 分片边界为 120 字节。
    257 个满长片 第 257 片序号回到 0 序号按 8 位回绕,无全局上限。
    SW1=0x91xx / 0x6A82 前者视为成功,后者抛异常 仅高半字节 0x9 计入成功。

    某些调用方的 require_tag() 会把缺失的必需 tag 转为异常,但可选字段、列表和尾部损坏仍可能被静默忽略。这些结论来自八项 TLV/APDU 原函数隔离探针,探针没有模拟无限 0x61 响应或真实 eUICC。

    BPP 分块安装的早停与通知队列

    BPP 安装由 _split_bpp() 将结构化包拆成 eUICC 接受的片段,load_bpp() 逐片发送并解释安装结果:A1 容器拆为容器头和各子 TLV,找不到 BF36 根时抛错。若根 TLV 之前还有无关 TLV,首个 BF23 命令块会从整份输入起点切片,连前缀一起发送。安装结果解析可从通知元数据取得序号、从 A2/A0 判成功、从 A2/A1 取得命令与错误码。

    load_bpp() 一旦收到首个可解析安装结果就停止发送后续块,不论其成功与否;无结果时报错。这意味着畸形包、超大包和重试后的非幂等行为需要离线协议测试,不能以小 profile 安装成功推断边界正确。

    通知侧,process_notifications() 逐条取 eUICC 通知,上传 SM-DP+ 后再从本地删除;进入单条 try 后的失败只打印并继续。第一条上传失败时不会发送本地删除 APDU,但会继续第二条并在其成功后删除;而 list_notifications() 抛错,或某条通知缺少 seqNumber/notificationAddress,均发生在逐条 try 之外,会终止整批。因此 UI 操作「完成」不等于远端通知可靠送达。

    下载交易与会话取消

    parse_lpa_activation_code() 只要求 LPA: 前缀和 $ 分隔后的三个非空字段,返回 SM-DP+ 地址与 matching ID;没有在此处校验首字段版本或限定服务器域名。download_profile() 先要求系统时间有效,再依次获取 eUICC challenge/info、向 SM-DP+ 发起认证、让 eUICC 验证服务器、让服务器验证客户端、解析 metadata、准备下载并获取/安装 BPP。HTTPS 封装 es9p_request() 使用固定 GSMA CI 证书包、单次 30 秒超时和服务端状态码映射。

    若取得 transaction ID 后发生异常,代码调用 _cancel_session_safe(),尽力在 eUICC 和 SM-DP+ 取消,取消过程异常会被吞掉;会话正常关闭时原错误继续上抛。需要注意:若 session.close() 自身抛错,finally 中的新异常会覆盖原始请求错误,日志只留最外层异常时可能误诊根因。

    switch_profile() 遇 PROFILE_CAT_BUSY 会重置模组并重试一次,PROFILE_NOT_IN_DISABLED_STATE 被当作可接受结果;它没有在该函数中确认蜂窝网已用新 profile 注册。get_active_profile() 始终返回 None,UI 实际依据列表中的 enabled 字段推断当前 profile。本地保护方面,Profile.is_comma 同时要求运营商字符串精确等于 Webbing 和 ICCID 前缀 8985235,大小写或运营商字段变化都会绕开它。

    环节 捕获的错误 重试预算
    AtClient.query() SerialException 重建串口重发一次;TimeoutError 不重试。
    open_isdr() 打开通道失败 至多十次;第六次失败后重置模组;第十次失败先睡眠再抛。
    send_apdu() RuntimeError/ValueError 最多三次;TimeoutError 直接传播。
    ES9+ 请求 HTTP 状态映射 单次 30 秒超时,非整笔交易 deadline。

    CLI 侧 resolve_iccid() 对全数字输入只按 1-based 序号解析并重新列举,因此正常全数字 ICCID 无法作为字面值传给切换/删除/命名,越界会报「无此序号」。若用户查看列表后 profile 集合变化,下一次按旧序号执行可能指向不同 profile。

    常见问题

    eSIM 下载时为什么整台设备的 modem AT 都会被占住?

    因为整笔 ES9+ 下载与 BPP 安装在持有 /dev/shm/modem.lock 期间完成,锁的作用域比单条 AT 指令长得多。网络慢、服务器超时或安装耗时都会持续占用该锁,阻塞使用同一把锁的 modem 管理与 qcomgpsd AT 操作。

    TLV 解析遇到截断数据会报错吗?

    不会。解析器遇截断 tag、length 或 value 时只是 break,保留前面已解析的条目;size=0x80 会被当作长格式并产生零长度值。只有调用方通过 require_tag() 检查必需 tag 时,缺失才会转为异常。

    一段 profile 数据是怎么分片发给 eUICC 的?

    经 es10x_command() 按 120 字节分片,构造 STORE DATA APDU;遇 SW1=0x61 继续 GET RESPONSE,SW1 高半字节为 0x9 视为该片完成。序号按 8 位回绕,源文档未给出超过 256 片时的全局上限。

    为什么「操作完成」不等于运营商已注册?

    switch_profile() 不确认蜂窝网已用新 profile 注册,get_active_profile() 始终返回 None,而通知上传失败只打印并继续。UI 成功只表示对应 LPA 方法没有抛异常,不等于 profile 已完成运营商注册或 PPP 数据链恢复。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/ESIM_LPA_TLV_APDU_DOWNLOAD_DEEP_DIVE.md(TLV/APDU 分片、BPP 安装早停、ES9+ 交易与共享 AT 锁结构)。
    • 项目 02 —— 同目录探针记录:八项 TLV/APDU、八项 BPP、八项 AT/通道、十项 ES9+、八项通知/profile 隔离探针结论。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11