结论: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 数据链恢复。
延伸阅读
- 车载网络设置:Wi-Fi 5 秒扫描与 eSIM 页面的三层界面 —— 从 UI 到物理接口的完整调用链。
- 蜂窝 PPP 路由与 DNS:metric 1000 与 table 1000 的所有权边界 —— 同一把 modem 锁的下游网络所有权。
- 扫码激活 eSIM:座舱相机到 SM-DP+ 的下载管线 —— QR 激活码如何进入 LPA 下载交易。
- UDS 诊断工具为什么不能随手调:只读、会话与写 ECU 的分权设计 —— 设备专属副作用为什么要分权与授权。
参考资料
- 项目 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
发表回复