标签: 远程车联

  • 设备身份链:有缓存 DongleId 就跳过注册

    结论:设备身份链把「本地密钥 → 注册 → 令牌签发 → 各消费方」串成一条线。关键判定很简单:有公钥且 Params 已有 DongleId 时直接写回、不联系注册 API;没有公钥时把 ID 设为 UnregisteredDevice,即使已有缓存 ID 也是如此。而 is_registered_device() 只检查 Params 中 DongleId 不为 None 或哨兵值,不重新证明密钥、后端账户状态或设备真实性。设备注册、账户配对和车辆控制安全是三个不同问题。本文为对源码快照 c8fb906 的静态分析与隔离探针,未访问注册后端、未读取真实设备密钥、未验证实板网络。

    启动与注册链

    manager_init() 先写入 HardwareSerial,再调用 register(show_spinner=True),把非空结果设为 DONGLE_ID 环境变量并绑定日志上下文。因为 UnregisteredDevice 也是非空字符串,所以它不会触发「注册失败」异常;随后 manager_thread() 依据 DongleId 值排除 manage_athenad 和 uploader。可见未注册并不等于整个系统停止运行。

    阶段 关键函数/组件 实际行为
    密钥定位 get_key_pair() / Paths.persist_root() 依次查找 /persist/comma/ 下 RSA、ECDSA 私钥和公钥,返回首个完整文件对及 RS256/ES256;两对都无则返回三个 None,只读不生成
    本地标识 register() 优先读 Params 的 DongleId,仅当其为 None 时才尝试持久分区的 comma/dongle_id;有公钥且已有 ID 时直接写回,不联系注册 API
    无密钥 register() 没有公钥就把 ID 设为 UnregisteredDevice,即使已有缓存 ID 也是如此,并切换 Offroad_UnregisteredHardware 告警(PC 除外)
    在线注册 register() / api_get() 取序列号与 IMEI,用私钥签发含 register: true、一小时到期的 JWT,向 v2/pilotauth/ POST;这些值经 params=... 作 URL 查询参数传入,而非 JSON 请求体
    注册结果 register() HTTP 402/403 视为未注册;其他状态解析 JSON 中 dongle_id;异常线性退避,最长睡眠 15 秒后无限重试,60 秒后仅更新 spinner 文案
    注册判定 is_registered_device() 只检查 Params 中 DongleId 不为 None 或哨兵值,不重新证明密钥、后端账户或设备真实性

    这里还有两处细节:HARDWARE.get_serial() 在 comma 平台读取启动命令行序列号,get_imei() 从 modem 状态取值(基类默认返回空字符串);注册循环只以 None 判断 IMEI 是否就绪,所以空字符串并不会被这一循环拒绝。私钥的出厂写入/轮换机制不在这些函数中,不能据此推断供应链安全性。

    manager_init() 把 register() 的返回值直接作为 DONGLE_ID 环境变量使用,因此注册结果既决定日志上下文,也决定哪些进程被排除;但「未注册」不是异常路径,设备仍可进入离线运行,只是对应的云端相关进程被略过。

    由此,启动链的第一判据并不是「能否联网」,而是「本机是否存在完整密钥对」:密钥决定注册走向,网络只在无缓存 ID 时被使用。

    令牌与消费者

    Api.get_token() 以本地私钥生成含 identity、nbf、iat、exp 的 JWT,默认一小时有效;缺私钥时抛 RuntimeError。api_get() 只有显式传入 access_token 才加入 Authorization: JWT ...,并始终加入版本 User-Agent;Api.get()/post() 本身不会自动附带认证令牌。

    缺私钥时 Api.get_token() 直接拒绝签发,因此依赖令牌的消费方在无密钥设备上根本拿不到授权;这也解释了为何缺密钥会走 UnregisteredDevice 分支而非带着旧 ID 继续联网。

    消费方 关键入口 使用方式
    Athena 主连接 athenad.main() 连接 WebSocket 时将新令牌放入 jwt cookie;连接与重连另由 Athena 管理
    本地代理 startLocalProxy() 另签令牌连接远端 WebSocket;本地转发还有限定端口白名单,令牌不是本地端口授权的全部条件
    上传 Uploader.do_upload() 为获取上传 URL 显式传 access_token=self.api.get_token();实际上传仍走返回的 URL 与 headers
    配对 PairingDialog._get_pairing_url() 生成附加 pair: true 的 JWT 并嵌入二维码 URL;二维码应视为敏感凭据
    状态查询 PrimeState._fetch_prime_status() 未注册时不请求;UI 辅助函数要求系统时间有效,签发两小时令牌,并以单调时间约一小时为桶缓存一个令牌
    GNSS 辅助 pigeond 向设备辅助定位 API 显式传身份令牌;不参与车辆控制许可

    注册 JWT 与常规身份 JWT 的载荷、用途不同,pair 令牌又增加配对声明。源码显示的是客户端如何签发和传送,并不显示服务端如何验证签名、声明、撤销、过期或配对授权;因此不能仅凭本地 DongleId、Prime 缓存或可生成 JWT 就推断设备已被云端接受。

    消费者侧的差别同样明显:Athena 主连接与本地代理用不同令牌连接远端 WebSocket,上传是显式传令牌换取 URL,配对令牌带 pair 声明,状态查询则缓存约一小时令牌。令牌用途不同,彼此不能替代,这也是把身份能力按用途收窄的现实依据。

    自制硬件与插件化落点

    1. 硬件身份适配器:让平台提供序列号、网络模组标识和持久密钥能力;不要把 comma 的 /persist/comma 文件布局当成所有新硬件的永久 ABI。是否接入原服务端需由服务提供方确认。
    2. 受信任身份服务:把私钥读取与签名封装在固定核心或硬件安全模块;插件只获得按用途、受限时效的令牌请求能力,不获得私钥文件路径或任意 payload_extra 权限。
    3. 云端插件隔离:Athena、上传、配对与 GNSS 分别声明所需身份能力;令牌用途/受众分离、最小权限、脱敏日志、网络超时与离线降级由核心契约统一约束,不得把云端订阅/配对状态当作执行器安全门的替代品。
    4. 启动策略显式化:把「无密钥」「有密钥但无缓存」「后端拒绝」「网络长期不可用」建成不同设备状态;当前 register() 的无限重试可能阻塞 manager 初始化,须先确定离线启动与降级策略。

    验证与缺口

    仓库已有 test_registration.py,覆盖缓存命中、缺密钥、首次注册成功及 402 拒绝;但没有覆盖 ECDSA 路径、403、IMEI 空值、长时间网络失败、损坏 JSON、密钥轮换或服务端令牌授权。隔离探针确认:缺公钥会把已有缓存 ID 覆盖为 UnregisteredDevice;空字符串 IMEI 可进入注册请求;一次后端异常使重试睡眠 1 秒后继续,NotImplementedError 不重试;Api.get_token() 对额外载荷使用 payload.update(),调用方可覆盖默认声明,缺私钥则拒绝签发。

    需要强调边界:覆盖默认声明只是「客户端可构造能力」,不代表服务端接受任意声明;探针不执行真实密钥读取、JWT 编码、HTTP 或设备串号查询,上游测试也未在本环境运行。

    常见问题

    有了 DongleId 就说明设备已在云端注册了吗?

    不能这样推断。is_registered_device() 只检查 Params 中 DongleId 是否为 None 或哨兵值,不重新证明密钥、后端账户状态或设备真实性;有缓存 ID 时 register() 甚至直接写回、不联系注册 API。

    没有公钥但有缓存的 DongleId 会怎样?

    会被覆盖为 UnregisteredDevice。只要没有公钥,register() 就把 ID 设为该哨兵值,即使已有缓存 ID 也是如此,并切换 Offroad_UnregisteredHardware 告警(PC 除外)。

    哪些 API 请求会自动带上令牌?

    不会自动。api_get() 只有显式传入 access_token 才加入 Authorization: JWT ...;Api.get()/post() 本身不会自动获取身份令牌,Uploader.do_upload() 则是显式传入。

    注册失败的设备会不会直接停车?

    不会。UnregisteredDevice 是非空字符串,不会触发 manager_init() 的注册失败异常;manager_thread() 只是依据 DongleId 排除 manage_athenad 和 uploader。不过无限重试可能阻塞 manager 初始化。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DEVICE_IDENTITY_REGISTRATION_DEEP_DIVE.md(启动注册链、令牌签发与各消费方边界)
    • 项目 02 —— openpilot-research/probes/test_device_registration_contract.py(注册与 is_registered_device() 的假依赖隔离探针)
    • 项目 02 —— openpilot-research/probes/test_device_api_key_token_contract.py(get_key_pair() 与令牌签发契约探针)

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

    更新记录

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

  • Athena 片段导出:createClip 入队不校验时间顺序、码率与倍速

    结论:离线视频片段导出由 Athena RPC 排队:createClip() 入队、getClipState()/getClipChunk() 读取、deleteClip() 取消,工作线程把起止时间映射成连续的 60 秒分段,先写到 clips/.<filename> 点号临时文件,编码成功后再用 os.replace() 原子改名。关键边界是:入队层只校验 route、摄像头文件名与输出 basename,不校验时间顺序、bitrate 或 speedup,而 PC 回退路径没有 comma 硬件那套校验;硬件工作器按 floor(start*20) 到 ceil((start+duration)*20) 选帧,输出按 20 FPS 重建。本文为对源码快照 c8fb906 的静态阅读与假依赖探针,未在 comma 设备上运行 Qualcomm V4L2 解码/编码,也未生成实际 MP4。

    从远程请求到可下载文件

    VideoClips.createClip() 在非 PC 设备上仅在 IsOffroad 时接受创建;它校验 route、摄像头文件名和输出 basename,并按输出文件名维护任务。_worker() 依据起止时间映射连续 60 秒分段,先输出到点号临时路径,编码后确认仍是同一任务对象才 os.replace() 为正式文件;异常时移除当前队列项并尝试清理临时文件。

    _encode() 负责选后端:PC 用 ffmpeg concat/libx264,comma 硬件调用 encoderd --clip,码率由 Mbps 转 bps,并传递倍速与 JSON 元数据;子进程非零退出视为失败。注意片段与 uploader 的自动 qlog 上传不同:生成并经分块读取或显式上传接口交付,不能把 clips/ 目录出现当作云端已持有视频。

    边界 入口 已核对行为
    入队 VideoClips.createClip() 非 PC 仅 IsOffroad 时接受;校验 route、摄像头文件名与输出 basename;不校验时间顺序、bitrate、speedup
    工作线程 VideoClips._worker() 按起止时间映射连续 60 秒分段,先写 clips/.<filename>,编码后仍是同一任务才原子改名
    后端选择 VideoClips._encode() PC 用 ffmpeg concat/libx264;硬件调 encoderd --clip,码率 Mbps→bps,传倍速与元数据
    状态/读取 getClipState() / getClipChunk() 成品需 ffprobe 读到元数据且 requested_at 为数值才列 ready;同名内存任务覆盖磁盘 ready
    取消/删除 deleteClip() 先从内存队列移除,再在锁内终止同名编码进程,最后删除正式输出文件

    _available_ranges() 依赖任何以 camera.hevc 结尾的文件是否存在,据此推算名义 60 秒范围,并不探测真实视频长度;这就是「名义时长」与「实际可解码时长」分离的来源。

    RPC 层契约:隐藏名、负偏移与倒置区间

    四组原函数隔离探针确认了入队层与读取层的多处不对称。PC 分支可把结束时间早于起点、负码率和零倍速原样加入内存任务;非 PC 分支则在入队前要求 IsOffroad。.hidden.mp4 会通过 createClip() 的 basename 检查,却被 getClipChunk() 的「不得以点号开头」检查拒绝,_on_disk() 也跳过隐藏名。这种「可排队却不可读取」是层间契约不一致,而非异常参数一定编码成功。

    读取侧还有两处细节:getClipChunk() 接受等于文件长度的偏移并返回空数据,同时拒绝负数与非整数偏移;状态查询用 re.search 允许带前后缀的 route 字符串提取合法路段,而创建请求用 re.fullmatch。工作器探针还确认,请求 10–130 秒会映射到三个名义 60 秒段,向 _encode() 传相对起点 10 秒、时长 120 秒;真正如何拒绝倒置区间(如 60→-1 秒)未在探针执行,PC/硬件编码器的实际拒绝行为需另行验证。

    encoderd --clip 与硬件工作器

    encoderd.main() 仅在 comma 硬件编译分支接受 --clip,解析 OUTPUT START DURATION、--bitrate、--speedup、--metadata 与输入段,转入 encode_clip()。该函数要求非空输入、有限且非负的起点、正的时长/码率/倍速,并要求请求落在「输入文件数 × 60 秒」的名义范围内;它会跳过起点之前的整段,检查首段视频编码为 HEVC 且宽高有效。

    环节 实现 边界
    参数校验 encode_clip() 要求非空输入、有限非负起点、正时长/码率/倍速 名义范围 = 输入文件数 × 60 秒,不等于每段实际可解码时长
    解码 FFmpeg 解复用 HEVC,input_frame+1 作 token 排入 V4LDecoder.queuePacket() 捕获面 token 经 handleCapture() 还原源帧序号
    抽帧 按 floor(start*20) 到 ceil((start+duration)*20) 选帧,speedup 用全局帧偏移模运算 跳过的解码面立即归还,送编码器的面由回调释放
    输出 frame_id 与 timestampSof/Eof 从零按 20 FPS 重建 倍速靠抽帧实现,输出仍标注为 20 FPS,不保留原始 SOF/EOF

    V4LEncoder::encode_frame() 提交 NV12_UBWC 面,避免解码面过早重用;工作器发 EOS、等待已排输入有对应解码结果,最多连续五次空轮询,随后关闭编码器并检查实际抽帧数。VideoWriter::write() 用微秒时间戳写 MP4,析构时写 trailer 并移除 .lock。

    长片并行与失败边界

    对不足两个剩余段或时长小于 120 秒的请求,encode_clip() 串行处理;其余请求按接近中点的段边界拆成两个工作器:第一段直接写目标,第二段先写到已打开后 unlink 的临时 spool,两个工作器完成后按第一段编码帧数换算微秒偏移,回放第二段包并拼接。并行分支的 frame_offsets 保证倍速抽帧跨分片连续,第二段 codec config 被跳过而沿用第一段输出头。

    源码显式拒绝短的中间段,以免把源视频缺口误拼成连续时间;但工作器的成功条件比较的是实际读到的 input_frame 所推导的帧数,没有强制 input_frame >= end_frame。因此最后一段提前结束而仍有部分选中帧时,静态看可能以短于请求时长的文件返回成功,_available_ranges() 也会把末段按满 60 秒展示。这需要截断末段的实板样本或硬件回放来验证,不能仅凭退出码保证时长完整。

    常见问题

    createClip 会拒绝不合理的时间区间吗?

    入队层不会。PC 分支可把结束时间早于起点、负码率和零倍速原样加入内存任务;非 PC 分支只额外要求 IsOffroad。真正校验落在硬件编码入口 encode_clip(),而 PC 回退路径没有同一套硬件校验。

    clips 目录出现文件就等于云端已持有视频吗?

    不等价。片段由 Athena RPC 排队、生成,再经分块读取或显式上传接口交付,与 uploader 的自动 qlog 上传是两条路径;目录里出现文件只说明本地已生成。

    为什么导出的片段可能比请求时长短?

    工作器的成功条件只比较实际读到的 input_frame 推导帧数,没有强制 input_frame >= end_frame;末段提前结束而仍有部分选中帧时,静态看可能以更短文件返回成功,_available_ranges() 还会把末段按满 60 秒展示。

    倍速导出会改变帧率吗?

    不会标注变化。倍速通过全局帧偏移的模运算抽帧实现,输出 frame_id 与 timestampSof/Eof 从零按 20 FPS 重建,文件仍标为 20 FPS。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CLIP_EXPORT_DEEP_DIVE.md(Athena 片段 RPC、worker 分段与 encoderd --clip 硬件工作器)
    • 项目 02 —— openpilot-research/V4L_DECODER_RECONFIG_SURFACE_LIFECYCLE_DEEP_DIVE.md(源文档指向的硬解端口重配与外借面归还专题)

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

    更新记录

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