标签: 固件更新

  • 设置页写了参数不等于生效:ADB、alpha 纵向与总开关的下游链差在哪

    结论:车载设置页的开关只做一件事——往 Params 写值并(在少数情况下)请求一次行车周期;参数写进去不等于硬件行为已经验证,UI 显示门也不是安全授权。软件更新页的「检查/下载」是发给更新器的信号请求,安装按钮只写 DoReboot;开发者页的 ADB/SSH、动作模式与 alpha 纵向涉及不同下游消费者;驾驶开关则通过 card、selfdrived 影响使能。大屏与 mici 两套界面在时间门、异步调度与锁键语义上并不完全对齐。以下为固定源码快照 c8fb906 的静态分析。

    软件更新页:请求不是直接安装

    大屏 SoftwareLayout 从 Params 显示当前描述/发行说明、UpdaterState、失败次数、可拉取/可安装状态与目标分支。_update_state() 只在 offroad 显示「检查/下载」和「安装」,且安装可见还要求 UpdateAvailable。按钮点击后的约 10 秒等待窗只处理「更新器未响应」的 UI 状态,不保证下载成功。

    _on_download_update() 依据按钮文案选择 SIGUSR1 检查或 SIGHUP 拉取;_on_select_branch() 写 UpdaterTargetBranch 后发送检查信号。WaitTimeHelper 把 SIGUSR1 转成 CHECK、SIGHUP 转成 FETCH,由更新器负责分支解析与 staging/finalizing,再写 UpdaterFetchAvailable、UpdateAvailable 等 Params。两屏卸载都经确认后写 DoUninstall;manager_thread() 检出 DoUninstall/DoReboot/DoShutdown 后退出循环,由 main() 调用平台卸载或重启。实际固件切槽/卸载动作仍需平台级验证。

    开发者页:参数到运行链

    开发者页的控件覆盖 ADB/SSH 开关、SSH 公钥拉取、joystick 与纵横向动作模式、alpha 纵向控制确认以及 UI FPS/触摸调试。它们大多只写一个 Params 键,是否会真正改变硬件行为取决于设备系统层或进程表:例如 ADB 控件 offroad 禁用,而 SSH 开关未使用相同的 offroad 门;动作模式三者在 UI 回调中互斥写 Params,但参数可由其他进程直接修改,UI 互斥不是进程表的全局不变量。

    SSH 公钥走 SshKeyFetcher:请求 GitHub 的 <username>.keys,把用户名与返回文本写 GithubUsername/GithubSshKeys,出错时清理;这是双键非原子写,系统级 authorized_keys 生效链仍需平台审计。

    控件 下游或边界
    ADB / SSH 写 AdbEnabled、SshEnabled;服务实际启停取决于设备系统层。ADB 控件 offroad 禁用,SSH 未使用相同门。
    SSH 公钥 请求 GitHub <username>.keys,写 GithubUsername/GithubSshKeys;双键非原子写,错误时清理。
    joystick / 纵向 / 横向动作 三者在 UI 回调中互斥写 Params;process_config.py 据参数选调试进程,参数可被其他进程改。
    Alpha 纵向控制 非 release、车型 alphaLongitudinalAvailable、确认页之后写 AlphaLongitudinalEnabled 并请求行车周期;可能改变原车 ACC/AEB 组合。
    UI 调试 写 ShowDebugInfo 并在本 UI 进程切换触摸点/FPS 叠加,不影响 CAN 安全许可。

    行车周期与控制模式

    restart_needed_callback() 写 OnroadCycleRequested。hardwared 消费后清零,并在一段时间内使 not_onroad_cycle 条件为假,迫使 started 周期重建——这不是操作系统硬重启。新设备需要测试「参数写入 → deviceState.started 边沿 → manager 进程重选 → card 车型参数重建 → panda 安全模式重新配置」的顺序。

    这里有一个两屏不一致点值得注意:大屏动作模式回调写互斥参数,但并未像 mici 那样对纵/横向动作模式调用 restart_needed_callback(),两屏的即时生效路径可能不同。因此不能从「UI 控件已改变」推断当前控制进程已安全切换,需要通过回放或实机验证 manager 对参数变化的读取节奏。

    驾驶开关与消费关系

    TogglesLayout 定义 OpenpilotEnabledToggle、ExperimentalMode、加速踏板退出、LDW、Always-On DM、座舱摄像头/麦克风记录及单位切换。它按车型 CarParams 是否有 openpilot 纵向控制动态限制实验模式与驾驶风格;实验模式首次启用要求确认,涉及重启的开关在 engaged 时禁用。

    消费侧,card 在初始化时结合 OpenpilotEnabledToggle、控制器是否存在与车型 dashcamOnly 决定 CP.passive;selfdrived 与 card 的运行状态读取实验模式。LongitudinalPersonality 还可通过车辆距离按钮变化并反映到 UI,因此 UI 选项不是唯一写入者。两套页面的限制也不等价:大屏遍历各开关的 *Lock 参数,mici 在当前页面只显式处理 RecordFrontLock,移植配置策略时须逐键核对。

    对比项 大屏 SoftwareLayout / TogglesLayout mici 对应界面
    更新请求发送 直接发 SIGUSR1/SIGHUP,约 10 秒等待窗 后台线程发送,系统时间无效时阻止按钮请求
    动作模式重启 写互斥参数,未调用 restart_needed_callback() 对纵/横向动作模式调用 restart_needed_callback()
    锁键处理 遍历各开关的 *Lock 参数 当前页面只显式处理 RecordFrontLock

    自制硬件与插件化建议

    源文档给出一组建议:把设置描述为「能力 + 策略 + 执行者」,UI 插件只呈现能力,更新器/进程管理器/平台服务执行请求;给更新与危险操作定义结构化请求及结果,而非让多个 UI 复制 pkill、布尔 Params、10 秒本地计时;保留 offroad/engaged 二次校验,确保从外部 API 调用也无法绕过。

    对 alpha 纵向、动作模式、openpilot 总开关,应采用车型能力声明和独立安全审核;插件不得替换 panda 的 TX/RX 安全策略,硬件安全默认态仍为失效安全。所有这些端到端行为(ADB/SSH 真实启停、公钥更新原子性、下载中断与升级回滚、断电恢复)都需在目标板上验证,本文只证明代码路径存在。

    常见问题

    点了「安装」是不是就完成更新了?

    不是。两屏的安装按钮最终都写 DoReboot,并非在 UI 里直接把 overlay 写入当前分区;真正的前台切换由启动脚本完成。UI 的检查与下载也只是请求语义。

    改了 ADB 或 SSH 开关就一定能连上设备吗?

    不一定。源码快照只能确认参数被写入 AdbEnabled/SshEnabled,服务实际启停取决于设备系统层;而且 ADB 控件 offroad 禁用,SSH 开关未使用相同的 offroad 门。

    动作模式切换为什么有时要重启?

    因为 restart_needed_callback() 会写 OnroadCycleRequested,由 hardwared 触发一次行车周期重建(不是操作系统硬重启)。大屏动作模式未调用该回调,mici 调用了,两屏即时生效路径可能不同。

    UI 里的实验模式和驾驶风格为什么有时不显示?

    因为 TogglesLayout 按车型 CarParams 是否有 openpilot 纵向控制动态隐藏实验模式与驾驶风格;没有纵向能力的车型看不到这两项,实验模式首次启用还要求确认。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/SOFTWARE_DEVELOPER_SETTINGS_DEEP_DIVE.md(软件/开发者/驾驶开关三页的 UI 函数、Params 与下游消费者)。
    • 项目 02 —— openpilot-research/ 同目录探针记录:更新请求信号、行车周期重建与两屏锁键差异的静态核对结论。

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

    更新记录

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

  • 横向限幅四种算法:主机候选与 panda 判决为什么不是一件事

    结论:横向限幅分成扭矩、转角、曲率三条路径,共享默认横向加速度约 3.6 m/s²、jerk 约 3.6 m/s³(含道路横滚容差)。它们只是主机侧的候选值计算,不能等同于 panda 的发送许可;同一场景下主机函数输出并不保证每个瞬时输入同时满足加速度与 jerk 两项物理限值。插件可以输出带单位的候选扭矩/角度/曲率,但固件必须保留独立最终许可。

    三类配置与四条算法路径

    AngleSteeringLimits、AngleSteeringLimitsVM、CurvatureSteeringLimits 分别对应速度表转角、车辆模型转角和曲率。它们的共同特点是:配置类不检查车型身份、传感器健康或 CAN 权限,具体值由车型提供。下面是四条主要路径:

    入口 主机行为 主要消费者
    apply_driver_steer_torque_limits() 据驾驶员力矩与 allowance/factor/multiplier 算上下限,再对上次请求作非对称上升/下降速率限制,最后取整 Hyundai、GM、Subaru、Mazda、MG、Rivian、Volkswagen 等扭矩车型
    apply_dist_to_meas_limits() / apply_meas_steer_torque_limits() 先约束命令与实测量的误差带和绝对上限,再按上次请求限制每帧变化;误差带以 STEER_ERROR_MAX 给零附近保留范围 Toyota、Chrysler 等 EPS 电机力矩测量
    apply_std_steer_angle_limits() 按请求是否同向增大绝对值,在两张车速-角速度表间选择;插值后限单步变化,停用横向时改用实测转角,再限绝对角度 Nissan、PSA、Toyota 角度路径
    apply_steer_angle_limits_vm() 用 MAX_LATERAL_JERK/v² 反算每次可变转角,用 MAX_LATERAL_ACCEL/v² 反算绝对角;车速至少取 1 m/s Tesla、BYD 以固定安全车型参数建模

    注意 apply_driver_steer_torque_limits() 里的 steer_max 可动态覆盖静态上限。VM 路径还会用 STEER_STEP*DT_CTRL 将角速度换算为每次发送增量——调用周期由车型控制器决定,不是函数自己假设的常量。

    非扭矩路径的数值顺序

    曲率路径 CurvatureSteeringLimits.apply_limits() 的裁剪顺序固定,且这个顺序会产生反直觉的结果:

    步骤 动作
    1 按 a_max/v² 限曲率
    2 按 jerk_max/v² * steer_step*DT_CTRL 限相对上次请求的增量
    3 停用时以调用方传入的当前曲率替换
    4 最后限固定 CURVATURE_MAX

    当上次命令在当前车速下已经超出新的加速度界限时,jerk 区间可能与加速度区间不相交;后一道 np.clip 会把先前的加速度限值「拉回」上次命令附近。源文档给了一个可复算的例子:用 Ford 量级的 CURVATURE_MAX=0.02 1/m、MAX_LATERAL_ACCEL=MAX_LATERAL_JERK=3、车速 20 m/s、上次与本次请求均为 0.02 1/m、steer_step=1,本地原函数输出 0.019925 1/m——它高于当前加速度导出的 3/20²=0.0075 1/m,但仍低于固定绝对曲率上限。

    这说明了什么?主机此函数不保证每个瞬时输入同时满足两项物理限值。源文档很克制地补了一句:这并非证明真实 Ford 配置或 panda 固件会放行该帧;实际车型的速度、加速度配置、帧步长及固件独立检查需另核对。停用时的行为也值得记住:曲率和两种转角函数先前的动态限幅结果会被当前测量值替换,再只受最终绝对上限约束。

    主机不等于固件:同一场景的 safety helper 对照

    板端 steer_curvature_cmd_checks() 对启用的曲率命令分别 OR 累积固定绝对曲率、按板端车速推导的横向加速度、每帧 jerk 和实时发报频率等违例——加速度检查不会被后续 jerk 检查覆盖。这与主机的顺序差异,正是「主机候选」和「固件判决」不是同一件事的根源。

    Ford FORD_STEERING_LIMITS 将 0.02 1/m 映射为 1000 CAN 单位,20 Hz 发报;主机 CarController 每五个 100 Hz 周期调用一次共享曲率限幅。跨层探针的对照结果是:车速 20 m/s、上次曲率 0.02 1/m 的候选值被拒;同速度下界内的 0.008 1/m 候选通过。源文档强调这是共享函数与独立板端 helper 的对照证据,不是实际整车拒帧率或驾驶性能证明。

    辅助逻辑:故障规避与摩擦前馈

    common_fault_avoidance() 只在连续 request && fault_condition 时累计;超过阈值暂时拉低请求位,到 max_above_limit_frames+max_mismatching_frames 重置计数。Hyundai 的大转角、Toyota/Subaru 的高转角速度等复用此逻辑。它有两条边界:返回的是新计数和请求位,不清除同帧扭矩,也不自行核验 MCU 对请求位不匹配的容忍度。

    摩擦侧,apply_center_deadzone() 在死区内把误差归零,get_friction() 在 +/-FRICTION_THRESHOLD 之间线性插值摩擦前馈、区间外饱和。摩擦输出按 friction*latAccelFactor 缩放,不是原始扭矩命令——把它当作扭矩值会算错整整一个量纲。

    插件与硬件建议

    源文档给出的插件契约是:把横向算法插件限制为输出带单位的候选扭矩/角度/曲率;宿主负责车型身份和参数绑定、消息新鲜度、活动门及主机限幅,panda 固件保留独立最终许可。每个参数包应带车型、总线布局、固件安全模式、版本和发帧周期,拒绝跨车型或不完整配置;动态标定不能自动改变固件许可。

    在验证缺口上,test_lateral_limits.py 只对非 mock、非 notCar 的扭矩平台做静态估算,且明确跳过非扭矩平台,不直接执行限幅函数,也不验证 CAN 帧或 panda 判决;CarController.update() 全路径、NaN、真实车辆 CAN、目标固件和实车反馈均尚未验证。硬件验证的顺序应是:离线回放/隔离台架比较主机候选值与固件判决,覆盖驾驶员抢方向、车速跳变、实测量失联、请求位切断以及停用瞬间,再讨论受控场地测试。

    常见问题

    主机限幅通过就等于车辆会执行吗?

    不等于。主机函数只产生候选值;steer_torque_cmd_checks() 与 steer_curvature_cmd_checks() 在 panda 内另有状态化检查(固件车速、双车速差、曲率绝对值、加速度、jerk、实测误差等)。源文档明确主机函数不是这些状态化检查的替身。

    为什么曲率限幅可能不满足加速度门?

    因为 CurvatureSteeringLimits.apply_limits() 的顺序是加速度 → 相对上次命令的 jerk → 停用时实测曲率 → 固定绝对曲率。当上次命令已超出新的加速度界限时,后一道 np.clip 会把先前的加速度限值拉回上次命令附近,因此不保证每个瞬时输入同时满足两项限值。

    3.6 m/s² 和 3.6 m/s³ 是通用限制吗?

    它们是三类配置共享的默认量级(含道路横滚容差),但具体值由车型提供。例如 VM 路径用固定安全车型参数,Ford 量级例子用的是 CURVATURE_MAX=0.02 1/m 与 MAX_LATERAL_ACCEL=MAX_LATERAL_JERK=3。所以不能把它当成跨车型的通用上限。

    摩擦前馈的输出是扭矩命令吗?

    不是。get_friction() 的输出按 friction*latAccelFactor 缩放,由 LatControlTorque.update() 消费,不是原始扭矩命令。混淆两者的单位会得到错误结果。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LATERAL_LIMITS_HOST_SAFETY_PLUGIN_CONTRACT_DEEP_DIVE.md(取用了四条算法路径、曲率裁剪顺序与数值例子、板端 helper 对照与插件建议)。
    • 项目 02 —— openpilot-research/CAR_PROTOCOL_IMPLEMENTATIONS.md(仅用于确认 safety_tx_hook 与 CANPacker 在横向候选之后的分工)。

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

    更新记录

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

  • 更新到启动要跨 3 个标记:一致标记不是签名证明

    结论:openpilot 的代码更新从「候选版本」走到「启动版本」要跨过三个独立标记:staging 目录、finalized 定稿、以及 .overlay_consistent 一致标记。它们各自的含义不同——一致标记只是「定稿流程走完」的本地记录,不是代码签名证明;UI 的「可安装」与「更新进程退出」也都不等于新代码已经激活。真正切换发生在启动脚本移动目录并 exec 重启的那一刻。以下为固定源码快照 c8fb906 的静态分析与假依赖探针结论。

    生命周期与关键阶段

    updated 被配置为非 PC 的驻车进程;main() 在 DisableUpdates 时退出,随后取得排他锁、初始化参数与等待器。初次循环或系统时间无效会等待 60 秒;之后检查远端,下载成功后长间隔等待,失败后较短间隔重试。需要强调:更新进程退出或 UI 显示「可安装」都不等于已经激活新代码。

    人工请求走信号而非持久任务:UI 给 updater 发 SIGUSR1 表示只检查、SIGHUP 请求下载,请求由内存状态消费,不是持久队列。远端检查用 git ls-remote 获取分支 SHA,跳过旧 release2 分支,tizi/mici 对 release3 目标分支有设备映射。

    阶段 关键入口 实际行为
    人工请求 WaitTimeHelper、SoftwareLayout._on_download_update() UI 发 SIGUSR1 只检查、SIGHUP 请求下载;由内存状态消费,非持久队列。
    overlay 初始化 init_overlay() 依据 .overlay_init 与 .git 修改时间决定复用或重建 staging;重建会删除整个 STAGING_ROOT。
    远端检查 Updater.check_for_update()、target_branch 用 git ls-remote 获取分支 SHA,跳过旧 release2;设备型号映射 release3 目标分支。
    下载与系统维护 Updater.fetch_update()、handle_agnos_update() 先清一致标记,再 fetch/强制 checkout/reset/clean 与递归子模块更新;AGNOS 目标版本不同时写目标 slot。
    finalized 定稿 finalize_update()、set_consistent_flag() 把 merged 拷到独立 finalized,重置主仓/子模块并尝试 Git/LFS 清理,最后写 .overlay_consistent。
    状态发布 Updater.set_params() 写失败计数、分支、版本/说明、下载/安装可用标志与离线告警。
    激活 launch_chffrplus.sh 检查标记与 mtime,把旧目录移作备份、把 finalized 移入运行目录,exec 重启脚本。

    staging:overlay 的复用与重建

    init_overlay() 依据 .overlay_init 与 .git 修改时间决定复用还是重建 staging。重建前会清 UpdateAvailable 和一致标记、卸载旧 merged,再创建 OverlayFS 并记录 GitDiff;重建会删除整个 STAGING_ROOT。fetch_update() 先清一致标记,再在 merged 中 fetch、强制 checkout/reset/clean 和递归子模块更新。假命令探针确认了这一阶段顺序,并提示破坏性 Git 命令不应在研究工作区执行。

    三个标记分别证明什么

    第一个标记是 staging 的 .overlay_init,表示本地 overlay 已初始化;启动脚本用它判断是否复用。第二个是 finalized 目录,表示 merged 已被复制成独立候选,且主仓与子模块被重置。第三个是 .overlay_consistent,由 finalize_update() 最后写入;隔离探针确认:git gc 抛 CalledProcessError 时 LFS prune 被跳过但仍置标记,LFS prune 自身抛错也仍置标记;主仓 reset 抛错则函数离开且不置标记。也就是说,一致标记不等于复制完整性、fsync、签名或断电恢复已通过。

    标记或状态 计算依据 不保证什么
    .overlay_init 本地 overlay 是否初始化 不代表候选代码有效或可启动。
    finalized + .overlay_consistent 定稿流程是否走完 不校验 finalized 提交签名,清理失败仍可能置位。
    UpdateAvailable finalized 一致标记 + 当前版本差异 + 目标分支一致 探针显示远端分支表为空时仍可能为真,语义是「可拉取」。
    UpdaterFetchAvailable 分支表非空后比较 merged 与远端 SHA/分支 表示「可拉取」,不是「可启动」。

    激活与失败路径

    启动脚本 launch_chffrplus.sh 在启动时检查当前目录 .overlay_init、Git 目录 mtime、finalized 一致标记,且不能已有 old_openpilot;然后把旧目录移到 staging 备份、把 finalized 移入运行目录并 exec 重启脚本。UI 的安装按钮只是写 DoReboot,不是就地切换。当启动脚本发现 old_openpilot 已存在时只打印「backup found, not updating」,源码中的 TODO 明确尚未实现自动恢复。

    失败路径同样需要看清:main() 对 CalledProcessError 与其他异常记录错误并删除 .overlay_init,下一轮倾向重建 overlay;失败次数在进程内重新从 0 开始(源码有加载 Params 的 TODO)。成功检查但因计费网络跳过 fetch 时,循环仍把失败次数归零并更新检查时间,只有实际 fetch 成功才写 UpdaterLastFetchTime。因此不能把目录备份等同于经验证的断电回滚方案:若新版本能启动到 init_overlay(),重建 STAGING_ROOT 会清掉旧备份;若新版本在此之前无法启动,当前脚本没有自动回滚分支。

    对自制硬件的约束

    源文档给出一组建议:插件包、代码、模型、DBC 与独立安全固件需要同一个可验证兼容性集合,仅依赖 .overlay_consistent 或 Git 分支名不足以证明可上车;下载、完整性验证、激活、启动成功确认与回滚应是分离的受信任阶段,控制相关插件只能在驻车维护窗口替换且失败后保留可启动的旧版本。

    此外,UI 的 UpdateAvailable、下载状态与安装按钮只是 Params 视图;硬件平台应另有启动槽健康、镜像签名、反回滚与 watchdog 证据,不能据页面状态宣称部署完成。设计断电、存储满、子模块缺失、镜像版本不匹配等注入测试,验证「旧版本仍能启动」,是当前源码没有提供的验证结果。

    常见问题

    UI 显示「可安装」是不是就能直接用了?

    不是。UI 的安装按钮只写 DoReboot,真正切换由启动脚本移动目录并 exec 重启完成。更新进程退出或显示「可安装」都不等于新代码已激活。

    .overlay_consistent 能证明代码签名吗?

    不能。它由定稿流程最后写入,探针显示清理命令失败仍会置标记,主仓 reset 失败才不置位;源码没有单独校验 finalized 提交的签名。它不是代码签名证明。

    升级失败会自动回滚到旧版本吗?

    依源码没有自动回滚实现。发现 old_openpilot 时只打印「backup found, not updating」,源码 TODO 明确尚未实现;若新版本无法启动,启动脚本也没有自动回滚分支。

    UpdateAvailable 和 UpdaterFetchAvailable 有何区别?

    UpdateAvailable 依据 finalized 一致标记、当前版本差异与目标分支一致性计算;UpdaterFetchAvailable 在分支表非空后比较 merged 与远端 SHA/分支,语义是「可拉取」。两者都不是「可启动」的证明。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/UPDATER_STAGING_ACTIVATION_DEEP_DIVE.md(updated 生命周期、overlay staging、三个标记、激活与失败路径)。
    • 项目 02 —— openpilot-research/ 同目录探针记录:定稿、远端分支与拉取阶段各四项假依赖隔离探针结论。

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

    更新记录

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

  • test_runner 的并行分组:继承的 setUpClass 为什么会被拆开

    结论:test_runner 的分组只认「类自身」定义的 fixture。make_batches() 检查类的 __dict__,只有类自己写了 setUpClass/tearDownClass 才按类合并;仅仅继承 setUpClass 的测试会被切到多个批次、甚至不同 worker,于是类级 fixture 分别运行。这不是「测试失败」,而是分组契约的静态结果。退出码由批次与收集决定:收集错误或失败返回 1,无记录返回 5,中断返回 2。

    调用与数据流

    main() 先切换到仓库根目录、设置警告策略,再在主进程收集测试。多进程模式把 test ID 字符串而不是测试对象交给 worker,每个批次在 worker 内重新导入/加载。默认 -j 为 CPU 数、-W error 将警告升级为异常,只有 workers >= 2 才创建进程池。

    环节 关键函数/组件 静态契约与观察
    路径转模块名 module_name() 目标必须位于 ROOT 下;将相对 .py 路径转为 dotted 模块名。
    测试收集 collect()、flatten() 默认递归 openpilot/test_*.py;仅无显式目标时应用 IGNORED;导入错误进入 errors,过滤后按完整 ID 去重。
    批次规划 make_batches() 模块有 setUpModule/tearDownModule 则按模块分组;否则仅类自身 __dict__ 含 setUpClass/tearDownClass 才按类分组;其他 ID 按 ceil(tests/(jobs*4)) 分块。
    单批执行 run_batch() 每批重载 suite;批次外层捕获 fixture/导入输出。
    多进程调度 run_parallel() future 完成顺序决定进度输出顺序;批次抛异常时,仅生成该批首个 ID 的 error 记录。
    输出捕获 Capture dup2 在进程级替换 stdout/stderr 文件描述符;仅失败、错误、意外通过保留测试输出。
    结果聚合 Result 六种状态:passed、skipped、xfailed、failed、error、xpassed;子测试失败/错误合并在父测试记录中。
    汇总退出 report() 收集错误或失败状态返回 1;有记录且无失败返回 0;无记录且无收集错误返回 5。

    fixture 与失败路径

    unittest.TestSuite 负责调用模块/类 fixture,runner 只决定哪些 test ID 同批。因为 make_batches() 检查的是类自身字典,它不会把仅继承 setUpClass/tearDownClass 的测试自动按类合并。例如 TestParams 继承 OpenpilotTestCase.setUpClass();若被切到多个批次,其类级 fixture 会分别运行,也可能在不同 worker 中运行。这是静态分组结论,不代表该测试已出现并发故障。

    反向地,显式定义类级 fixture 的测试按类同批,有模块级 fixture 的按模块同批。而 TestSensord 和 TestOnroad 只定义小写 setup_class,会被基类调用,但同样不满足 runner 的按类分组条件;对其硬件副作用应使用串行/独占设备执行。

    OpenpilotTestCase.run() 在每个测试外包住环境变量快照、OpenpilotPrefix、manager 清理与 GC 恢复;setUpClass() 另有类级环境快照。它们是测试辅助类自己的隔离语义,不是 runner 对任意测试的全局保证。带 COMMA_HARDWARE_TEST 的用例还会在 setUp() 初始化硬件、关闭省电并终止 athena 进程。

    失败记录也有讲究:Result.mark() 在 fixture 早于 startTest 失败时生成独立的 fixture ID 记录,严重状态 error 优先于 failed;跳过和预期失败的详细原因/堆栈未写入报告。单 worker 模式如果 run_batch() 抛非中断异常,main() 没有对应兜底,不能期待得到统一汇总。

    因此对带硬件副作用的测试,应显式串行并声明独占资源,而不是假设 runner 会自动隔离;显式目标也不会因此变得无副作用。

    参数与副作用的边界

    现象 源码事实 含义
    -j 0 没有参数校验,会在批次大小计算中除零 负数同样不构成可靠 worker 配置。
    -k 过滤 过滤发生在加载 suite 之后 不能阻止模块导入副作用。
    IGNORED 只约束默认无目标扫描 它不是安全沙箱,显式目录仍会递归发现 test_*.py。
    worker 异常 仅首个批次 ID 被标为 error 不能把未列出的同批测试当作通过。

    另外两点会影响结论的可信度:其一,收集会导入测试模块,即使只用 -k 过滤,过滤也发生在加载 suite 之后,不能阻止模块导入副作用;IGNORED 只约束默认无目标扫描。其二,CLI 的退出码固定为 0/1/2/5:main() 捕获 KeyboardInterrupt 返回 2,空结果返回 5,收集错误或失败返回 1。假 loader 探针验证了重复目标只加载一次、过滤发生在加载后,假 future 验证批次异常时仅首个 ID 被记为 error;这些均不是实进程池或真实 fixture 的执行证据。

    对硬件与插件化重构的意义

    1. 权限分层:纯算法/协议解析测试可以进入默认并行池;任何枚举真实 panda、串口、CAN、相机、刷写、Params 全局路径或终止系统进程的测试都应有显式硬件标签与独立执行环境。
    2. 资源声明:硬件插件测试应声明设备实例、总线、固件模式和独占锁;不要仅依赖类级 fixture 或 -j 1 防止其他进程争用。
    3. 可复现证据:记录源码提交、子模块版本、插件版本、硬件序列号、-W/-j/目标 ID、日志来源与结果;批次异常须标记整批为「未完成/未知」。
    4. 分级门禁:单元测试、离线回放、SIL、隔离台架 HIL、实车验证逐级放行;车端控制或固件修改不可由开发机 runner 的绿色结果单独授权。

    常见问题

    为什么继承来的 setUpClass 不会被按类分组?

    因为 make_batches() 检查的是类自身 __dict__,仅继承 setUpClass/tearDownClass 不满足条件。例如 TestParams 继承 OpenpilotTestCase.setUpClass(),若被切到多个批次,其类级 fixture 会分别运行,也可能在不同 worker 中运行。

    -j 0 会怎样?

    没有参数校验,会在批次大小计算中除零;负数同样不构成可靠 worker 配置。该行为由隔离探针验证,不是实进程池执行证据。

    跳过和预期失败的原因会写进报告吗?

    不会。源码中跳过和预期失败的详细原因/堆栈未写入报告,只有测试级耗时与六种状态。因此不能仅凭报告里的 skipped 数量判断原因。

    单 worker 抛异常会得到统一汇总吗?

    不一定。单 worker 模式如果 run_batch() 抛非中断异常,main() 没有对应兜底;多 worker 模式则仅首个批次 ID 被标为 error,不能把未列出的同批测试当作通过。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TEST_RUNNER_PARALLEL_FIXTURE_CONTRACT_DEEP_DIVE.md(取用了收集/分组/执行契约、fixture 与失败路径、结果与退出码,以及硬件插件重构意义)。
    • 项目 02 —— openpilot-research/NATIVE_TEST_DISCOVERY_BUILD_SKIP_CONTRACT_DEEP_DIVE.md(仅用于确认原生测试缺失即跳过的发现契约)。

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

    更新记录

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

  • Toyota CarState:bus_timeout 为什么不等于信号健康

    结论:在 Toyota 输入链里,can_valid 与 bus_timeout 是两种不同的健康语义。can_valid 只对已注册报文做活性检查;bus_timeout 只代表总线流量空窗,而且 CANParser.update() 对该总线的任何帧都会刷新 last_nonempty_nanos,即使不是已注册地址。因此「总线有流量」不等于「每个必需信号都健康」,单看 vl 值也无法区分「未收到有效样本」和「物理零」。

    入口与关键函数

    Toyota 的输入链从 card 解码 CAN 帧开始,经车型接口把信号归一成 CarState,再向下游发布与消费。

    组件/函数 源码 作用
    CarInterface 绑定 interface.py 指定 Toyota CarState/CarController/雷达接口;参数构建决定车型标志、角度模式和纵向能力。
    card.state_update() card.py 接收 can,转为帧列表,再调用车型接口和雷达接口。
    CarInterfaceBase.update() interfaces.py 先更新所有 CANParser,后调用 CarState.update(),再汇总 canValid/canTimeout、速度显示迟滞和按钮启用。
    CarState.get_can_parsers() carstate.py 为动力总线 0 和相机总线 2 创建解析器,仅预注册 BLINKERS_STATE。
    parse_wheel_speeds() interfaces.py 四轮均值乘单位及车型系数,再进入 KF1D 得到 vEgo/aEgo。

    总线注册与健康的准确含义

    Toyota 的动力总线只预注册 BLINKERS_STATE,频率传入 NaN;NaN 在 _add_message() 中令该报文 ignore_alive=True,所以转向灯报文本身不参与超时失效,并非所有 Toyota 输入都忽略超时。其余报文是在 CarState.update() 第一次读 cp.vl[...] 时由 VLDict 动态注册;首次读发生在本轮 cp.update() 之后,即该轮此前收到的报文尚未由新注册状态解码。

    普通动态报文先以 1 Hz 假定频率、10 秒超时阈值注册,收到足够样本后解析器估计频率并改为约十个周期的阈值。这里有两个容易混淆的点:其一,CANParser.update() 对该总线的任何帧都会刷新 last_nonempty_nanos,所以 bus_timeout 只代表总线流量空窗;其二,MessageState.parse() 遇 DBC checksum 或 counter 错误不更新缓存值与时间戳。最终 CarInterfaceBase.update() 对两路解析器取 all(can_valid) 和 any(bus_timeout)。

    还有一个细节:转角偏移更新分支内部也读取 cp.can_valid,接口汇总时又读取一次;当前 can_valid 属性读取会更新 can_invalid_cnt,不是纯查询。因此不能把它解释为严格的「连续五次 update 失败后失效」。

    信号到 CarState 的映射

    类别 来源与条件 输出/状态
    车身与踏板 动力总线 BODY_CONTROL_STATE、BRAKE_MODULE、ESP_CONTROL;SecOC 用 GAS_PEDAL,其他用 PCM_CRUISE.GAS_RELEASED doorOpen、安全带、驻车制动、制动/保持、gasPressed。
    档位与车速 SecOC 用 GEAR_PACKET_HYBRID,其他用 GEAR_PACKET;四轮 WHEEL_SPEEDS 枚举映射档位;轮速先得 vEgoRaw,再滤波为 vEgo/aEgo;vEgoCluster 初始设为 vEgo * 1.015。
    转向 STEER_ANGLE_SENSOR、STEER_TORQUE_SENSOR、EPS_STATUS 转角/角速度、扭矩、驾驶员转向判定、LKA/LTA 临时与永久故障。
    巡航 UNSUPPORTED_DSU 用 DSU_CRUISE 与 PCM_CRUISE_ALT;其他用 PCM_CRUISE_2 与 PCM_CRUISE_SM;运行态来自 PCM_CRUISE 主开关、设定车速、仪表设定车速、启用/停止/非自适应状态和故障。
    按键/HUD 相机 LKAS_HUD;部分平台的 ACC_CONTROL.DISTANCE LKAS 边沿合成按钮事件、gap 调整事件;相机 HUD 缓存供控制器构帧。

    其中 stockAeb 仅在非 SecOC、未禁雷达路径读取,且要求 PRECOLLISION_ACTIVE 与负 FORCE;accFaulted 的低速 lockout 附加判据仅在源码指定的 TSS2/ACC 类型和 openpilot 纵向控制条件下执行,不应泛化为所有 Toyota 车型。

    来源选择也是常被误解的点:cp_acc 在 TSS2 且非 RADAR_ACC 时指相机解析器,否则指动力解析器;这是 ACC/PCS 的信号来源选择,不是对两路数据作一致性比对。相机解析器初始没有注册报文,全部依赖动态注册;原车 ADAS 的 stockAeb、stockFcw 与左右盲区还受 TSS2、HAS_BSM、DISABLE_RADAR 条件控制,ACC_CONTROL.ACC_TYPE 影响低速 lockout 分支。

    转角标定、故障与按钮时序

    STEER_TORQUE_SENSOR.STEER_ANGLE 在非零、未初始化时才让 accurate_steer_angle_seen 永久锁存为真。此后仅在原角度小于 90 度、角速度小于 100 度/秒且 cp.can_valid 为真时,才更新 60 秒时间常数的 FirstOrderFilter 偏移;首次合格样本直接初始化。偏移一旦初始化,后续即便本轮不再更新,也会继续用于 torque_sensor_angle - offset。这里没有单独的偏移年龄或置信度字段。

    steeringPressed 是驾驶员力矩绝对值直接大于 STEER_THRESHOLD,不是调用基类的去抖函数。EPS_STATUS.LKA_STATE 总是参与故障映射,仅角度控制模式再并入 LTA_STATE;代码列出的临时故障为 0/9/11/21/25,永久故障为 3/17,这只是软件分类。TSS2 相机 LDA_ON_MESSAGE 从非零值切换到不同值时,代码合成一次 LKAS 按下和释放事件。lkas_hud 与 SecOC 同步字段是从解析器字典浅拷贝的最近缓存,没有在此单独检查样本年龄。

    消费与插件设计约束

    card 将 CS.canValid 作为 carState 外层 valid,也用于 sendcan 消息的外层有效位;后者是否真正被 MCU 拒绝仍取决于车端传输和 safety 链,不能把外层位等同于物理禁发。CarEvents 把 vehicleSensorsInvalid、accFaulted、转向故障、驾驶员施力等转成事件。重构成车型输入插件时,输出至少应为逐信号的来源总线、DBC/车型版本、原始采样时间、年龄、有效/缺失/故障状态及标准物理单位。

    常见问题

    can_valid 和 bus_timeout 到底有什么区别?

    can_valid 对已注册报文做活性检查,并把计数器达到失败阈值视为无效;bus_timeout 只代表总线流量空窗,任何帧(即使未注册)都会刷新 last_nonempty_nanos。所以有总线流量仍可能缺少某个必需信号。

    为什么转向灯报文不参与超时失效?

    因为动力总线只预注册 BLINKERS_STATE,其频率传入 NaN,在 _add_message() 中令该报文 ignore_alive=True。这只针对转向灯报文本身,并非所有 Toyota 输入都忽略超时。

    动态注册的报文首轮会被解码吗?

    不会。其他 cp.vl[...] 首次读取时 VLDict 才动态注册,而首次读发生在该轮 cp.update() 之后,因此该轮此前收到的报文尚未由新注册状态解码,初值零可能只是「未收到有效样本」。

    can_valid 的读取会改变状态吗?

    会。当前 can_valid 属性读取会更新 can_invalid_cnt,不是纯查询;而且转角偏移分支与接口汇总都会读取它。因此不能把它当成「连续五次失败后才失效」的严格计数语义。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TOYOTA_CARSTATE_INPUT_HEALTH_CONTRACT_DEEP_DIVE.md(取用了总线注册与健康语义、信号到 CarState 映射、转角标定与按钮时序、消费与插件约束)。
    • 项目 02 —— openpilot-research/TOYOTA_SECOC_KEY_FRESHNESS_SAFETY_BOUNDARY_DEEP_DIVE.md(仅用于确认 SecOC 同步 MAC 与 freshness 的上游验证边界)。

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

    更新记录

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

  • panda 主机 CAN 组包:70 字节线格式与 XOR 校验为什么晚于安全钩子

    结论:主机到 MCU 的 CAN 字节流是一台分片组包机:CANPacket_t 头部固定 6 字节,DLC 查表最长对应 70 字节线格式,packed, aligned(4) 结构在本机编译为 72 字节。校验顺序是「先安全钩子、后 XOR 校验」:can_send() 先运行 safety_tx_hook(),坏 XOR 帧虽然不会上总线,却可能先触达有状态的安全钩子。returned 回执只代表已提交硬件 FIFO,不等于车端已接收。

    6 字节头与 72 字节结构:线格式到底传多少

    报文的字节布局由 CANPacket_t 与 pack_can_buffer() 定义:6 字节头包含 DLC、bus、FD、地址、标志与 XOR;DLC 查表对应 0–64 字节载荷,因此最长有效线格式为 70 字节。C 结构带 packed, aligned(4),在本机编译为 72 字节,尾部的对齐字节不应传输。

    Python 编码显式组出首字节 DLC:4 | bus:3 | fd:1、随后四字节小端地址/状态位和 XOR;原生 C++ can_header 则把首位命名为 reserved,发送时零初始化。两者的 6 字节头部形状相同,但发送 FD 的策略不同——这一点在下一节展开。

    环节 关键值 含义
    头部长度 6 字节 DLC、bus、FD、地址、标志与 XOR
    载荷范围 0–64 字节 由 DLC 查表决定
    最长线格式 70 字节 6 字节头 + 最大载荷
    本机 C 结构 72 字节 尾部对齐字节不传输
    MCU 写缓冲 72 字节数组 容纳一个最大协议帧

    主机到车辆:一半包留在哪里、什么时候补齐

    Python 把完整消息串接成字节流:can_send_many() 经 USB EP3,SPI 由 endpoint3 自身事务长度承载,两者进入同一 comms_can_write()。MCU 侧用 can_write_buffer 保存不完整帧及剩余字节数;补齐后构造 CANPacket_t 并调用 can_send(..., false),其余输入逐帧处理。

    环节 实现 行为
    主机分块写入 can_send_many() / USB EP3 / SPI endpoint3 USB 按客户端块切分,SPI 按自身事务长度;进入同一 comms_can_write()
    MCU 写入组包 comms_can_write() 半包存于 72 字节缓冲;补齐后调用 can_send(..., false)
    MCU 读出分片 comms_can_read() 先发上次帧尾,再取 RX 队列;按 max_len 返回实际字节数
    主机接收重组 can_recv() / unpack_can_buffer() 拼接残包,逐帧验证 XOR 并保留下次所需残包
    状态清空 comms_can_reset() 连接时主机发 0xc0,清 MCU 读/写半包指针;不清 CAN RX/TX 队列
    容量流控 refresh_can_tx_slots_available() 三路 TX 队列同时满足 USB 51 帧或 SPI 170 帧余量时恢复接收

    两个边界必须分清:读出侧不保证传输块与 CAN 帧边界重合,所以主机必须能拼接残包;主机缓存与 MCU 的两个缓存(写半包、读残包)是三份不同状态。另外,51 帧 / 170 帧是背压门槛,不代表安全钩子放行或总线已发送。

    校验与许可的真实顺序

    这是整篇文章最容易被写反的一点。对完整帧,comms_can_write() 直接调用 can_send();can_send() 先运行 safety_tx_hook(),再决定入 TX 软件队列或生成 rejected 回执。直到 process_can() 从队列取出时,才运行 can_check_checksum();校验失败只增加 total_tx_checksum_error_cnt。

    顺序 环节 结果
    1 USB/SPI 字节进入 comms_can_write 组出完整包
    2 can_send 调用 safety_tx_hook 拒绝则 rejected 回执入 RX 队列
    3 允许则入 TX 软件队列 尚未校验 XOR
    4 process_can 校验 XOR 错误则增加 tx checksum error
    5 正确则提交 FDCAN TX FIFO 产生 returned 回执

    由此得到两个结论:坏 XOR 帧不会由这条发送路径上总线,但可能先触达有状态的安全钩子——例如 Toyota 的 toyota_tx_hook() 会调用 steer_torque_cmd_checks(),其中可能更新 desired_torque_last。源文档对此的措辞很克制:具体可触发条件及后续有效帧影响需要隔离测试,本文不宣称已复现控制偏差。另一个结论是:returned 仅代表提交硬件 FIFO,不能解释为车端已确认接收。

    读出侧还有一个实现细节:comms_can_read() 用自定义 memcpy() 把残包向左搬移。当前实现是正向复制,因此该方向可工作;但换成标准库 memcpy 后,重叠内存没有可移植保证。

    CAN FD 位:主机两条路径的差异

    原生 pandad connect() 给三路总线打开 canfd_auto,而原生 pack_can_buffer() 不显式置 FD 位。板端 process_can() 据 canfd_auto ? canfd_enabled : to_send.fd 决定 FDF,其中 canfd_enabled 在收到 CAN FD 帧后才设置。因此在首次收到 FD 帧之前,原生主机候选帧的 FDF 取决于板端尚未观察到 FD 的状态;即使 DLC 表可编码 12–64 字节,也不能据此断言硬件已经按 CAN FD 发送。

    Python 侧相反:Panda.connect() 关闭自动切换,pack_can_buffer(..., fd=...) 明确设置首位。此外固件 0xdd 暴露的 CAN 包版本由 can.h 原文字节 SHA-256 前四字节形成;Python 的 can_send_many()/can_recv() 用装饰器比对这个值,而原生 C++ pandad 不显式调用 0xdd,主要依赖固件签名一致性门。该哈希不是对主机 can_header 源文件或运行时硬件 FD 状态的证明。

    插件化边界

    源码 test_comms.py 有半包 reset、随机 64 字节块写入和 5 万条接收帧重组用例,但这些用例在 allOutput 模式下验证主要组包行为,没有直接证明坏 XOR 帧经过车型安全钩子前后状态不变,也不覆盖中断并发、物理 CAN 时序或全部车型许可分支。

    对自制硬件或「万物即插件」重构,主机 CAN 流适配、分块与遥测可以插件化,但需要在受信任入口先验证包长、DLC、bus、XOR/传输完整性,再交给有状态的安全策略;最终发送仍须固件独立许可。插件不得通过原始 USB/SPI 端点绕过验证,也不能把 returned 回执解释为车端已确认接收。

    常见问题

    为什么说 XOR 校验晚于安全钩子?

    因为完整帧进入 comms_can_write() 后直接调用 can_send(),而 can_send() 先运行 safety_tx_hook(),之后才入 TX 队列;can_check_checksum() 要等到 process_can() 从队列取出时才执行。所以坏 XOR 帧不会上总线,却可能先触达有状态的安全钩子。

    6 字节头和 72 字节结构到底哪个上总线?

    线格式只传 6 字节头加 DLC 对应的有效载荷,最长 70 字节;data 偏移为 6。带 packed, aligned(4) 的完整 C 结构在本机编译为 72 字节,但其尾部对齐字节不应传输。

    returned 回执能说明车端收到了吗?

    不能。returned 仅代表已提交硬件 TX FIFO,不等于车端 ECU 已确认接收。源文档明确要求不要把它解释为车端确认。

    为什么主机两条路径的 CAN FD 行为不同?

    原生 pandad 给三路总线打开 canfd_auto,且原生 pack_can_buffer() 不显式置 FD 位,FDF 由板端在收到 FD 帧后才置的 canfd_enabled 决定;Python 的 Panda.connect() 关闭自动切换,由 pack_can_buffer(..., fd=...) 明确设置首位。新硬件须分别验证这两条路径及冷启动。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDA_CAN_COMMS_FRAGMENT_ASSEMBLY_DEEP_DIVE.md(取用了 CANPacket_t 字节布局、组包函数契约、安全钩子与 XOR 的真实顺序、CAN FD 位差异与插件边界)。
    • 项目 02 —— openpilot-research/CAR_PROTOCOL_IMPLEMENTATIONS.md(仅用于确认 can_comms.h 调用 can_send(..., false) 与 safety_tx_hook 的衔接位置)。

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

    更新记录

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

  • AGNOS A/B 升级:sparse 解包与 64 字节尾标记

    结论:AGNOS 的 A/B 系统更新是一条「拉取 staging → 标非活动槽不可启动 → 流式解压并解 sparse → 写分区与哈希校验 → abctl 切槽 → 重启后标记成功」的链。校验分两层:system 等非全量校验分区只比较尾部 64 字节 marker,其余分区重算 SHA-256。因此 marker 是性能优化,不是加密签名或全量介质自检。本文为固定源码快照的静态分析,不是可在车辆上照做的刷机手册。

    平台辅助函数:设备路径、GPIO 与功放

    Paths 按 PC/AGNOS 选择日志、持久化、下载缓存和共享内存路径;LOG_ROOT、COMMA_CACHE、OPENPILOT_PREFIX 可影响部分路径。GPIO 统一列出 panda、GNSS、蜂窝、相机、蜂鸣器和 IMU 中断 GPIO 编号;注释明确某些 _RST_N 名称与实际高有效复位相反,不能按名字推电平。功放侧 Amplifier._set_configs() 对 MAX98089 执行 I2C 读改写,初始化时先全局关闭、配置 DAC/I2S/扬声器、再开启;Amplifier.set_configs() 最多 15 次重试并递增等待,仅捕获 OSError,全部失败返回 False。

    省电策略 HardwareComma.set_power_save() 在驻车时关闭功放、大核下线并调整 CPU governor/IRQ,恢复时重新初始化功放;它紧耦合 SoC sysfs 与 IRQ 名称。台架功率工具 read_power()/sample_power() 从固定 I2C hwmon 节点读取瓦特,按 123 Hz 定时采样,这是测量工具,不是 hardwared 中的 PowerMonitoring 驻车电量估计器。

    组件 源码入口 行为与移植边界
    路径选择 Paths 按 PC/AGNOS 选择日志、持久化、下载缓存与共享内存路径。
    引脚表 GPIO 列出各外设 GPIO 编号;_RST_N 名称与实际复位极性可能相反。
    功放配置 Amplifier._set_configs() MAX98089 I2C 读改写;失败返回 False,调用方未检查布尔值。
    省电策略 HardwareComma.set_power_save() 驻车关功放、大核下线并调整 governor/IRQ。
    台架功率工具 read_power()/sample_power() 按 123 Hz 读取 I2C hwmon 瓦特,非驻车电量估计器。

    A/B 更新调用链

    updated.handle_agnos_update() 只在 AGNOS 版本变化时触发,清除 staging 的一致标记后刷目标 slot;updated 由 manager 作为驻车进程运行。launch_chffrplus.sh 则在启动时先验证/切换已下载的目标系统,或循环调用设备更新器。清单在 common/hardware/comma/agnos.json,system/hardware/comma/agnos.json 是指向它的符号链接,故两处路径并非两份独立配置。

    选择槽时 get_target_slot_number() 读取 abctl --boot_slot,选择另一槽;get_partition_path() 按清单 has_ab 拼分区标签。下载解压由 StreamingDecompressor.read() 流式处理 HTTP/XZ,同时计算解压后字节的 SHA-256;unsparsify() 解析 Android sparse 的 raw/fill/don’t-care 三类 chunk。分区写入 flash_partition() 先快检;非全量分区先清尾部 hash 标记,再由 extract_compressed_image() 写设备、核对 raw/hash/大小,成功后回写标记。重试与切槽上,flash_agnos_update() 先 --set_unbootable,仅下载类异常最多重试 10 次;swap() 清 marker 后调用 abctl --set_active。

    阶段 核心函数与契约
    选择槽与路径 get_target_slot_number() 读 abctl --boot_slot 选另一槽;get_partition_path() 按 has_ab 拼标签。
    下载解压 StreamingDecompressor.read() 流式 HTTP/XZ 解压并计算解压后 SHA-256。
    稀疏镜像 unsparsify() 解析 Android sparse 的 raw/fill/don’t-care chunk。
    分区写入 flash_partition() 快检、清尾标记、写设备并核对 raw/hash/大小、回写标记。
    校验策略 verify_partition() 全量校验分区重算 SHA-256;其他分区只比较末尾 64 字节 marker。
    重试与切槽 flash_agnos_update() 先 --set_unbootable,下载类异常最多重试 10 次;swap() 清 marker 后 abctl --set_active。

    清单中的 hash 指解压后的稀疏输入流,hash_raw 指写入的原始分区数据;对于非稀疏分区两者相同。system 是 sparse 且采用尾部 hash 标记,其余本快照清单分区采用全量哈希。

    静态发现的故障边界

    unsparsify() 遇到 don’t-care chunk 只 yield b"",extract_compressed_image() 对空字节不推进输出文件位置;用 1 个 4096 字节 don’t-care 块的合成 sparse 输入验证过该函数只输出 0 字节。若发布镜像实际包含此类 chunk,写入布局/大小不会保留空洞,并可能因大小或哈希校验失败——但当前未下载真实镜像,不能断言发布镜像触发了该路径。

    swap() 在 abctl 返回不含成功标记的输出时无限循环,源码中没有退避或截止时间;若命令直接抛异常则退出。隔离探针还显示:前两次刷写抛 RequestException、第三次成功时,同一分区被调用三次、前两次各睡 10 秒;刷写抛非下载类 ValueError 时第一次异常直接传播,不进入十次重试;abctl --set_unbootable 假返回码为 1 时仍继续调用刷写,因为源码调用 subprocess.run() 后未检查 returncode。

    verify_partition() 对非全量校验分区只信任尾部 marker,无法检测 marker 写成之后发生的实体损坏;隔离探针验证了「载荷已变而尾部仍放预期哈希字符串」时非全量 verify_partition() 返回真、强制全量检查返回假。test_manifest() 只检查 URL 的 HEAD 可达性、内容类型和非稀疏分区的两个 hash 字段相等,不覆盖 sparse 解包、分区写入、切槽或电源中断。

    常见问题

    AGNOS 更新时断电会怎样?

    源码在写入前先 abctl --set_unbootable 标记目标槽不可启动,写入后由 swap() 用 abctl --set_active 切槽,新系统启动后再以 abctl --set_success 标记成功。本文只做静态分析,未做断电注入实板验证;插件化更新服务应把「镜像校验通过」「目标槽可启动」「切槽完成」作为分离状态回执。

    为什么有些分区只比较 64 字节尾部标记?

    因为 verify_partition() 对 full_check 分区重算指定大小的 SHA-256,其他分区只比较末尾 64 字节 marker,并不每次重读分区实体。这个 marker 是性能优化,无法检测写标记之后的实体损坏。

    sparse 镜像里的 don’t-care 块会被保留吗?

    源码里 unsparsify() 遇到 don’t-care chunk 只 yield b"",而 extract_compressed_image() 对空字节不推进输出位置;隔离探针用合成输入验证了它只输出 0 字节,所以空洞不会按原布局保留。

    A/B 切槽失败会重试吗?

    源码中 swap() 在 abctl 输出不含成功标记时无限循环,没有退避或截止时间;隔离探针连续尝试三次、中间无 sleep()。新平台应对切槽失败设置有界重试、告警和恢复策略。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/AGNOS_PLATFORM_UPDATE_DEEP_DIVE.md(平台辅助组件、A/B 安装与校验策略、sparse/XZ 边界)。
    • 项目 02 —— 同目录探针记录:四项更新控制流、五项 sparse/校验、四项 XZ 流隔离探针结论。

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

    更新记录

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

  • Toyota 固件指纹:子版本不同也能命中同一车型的三级匹配

    结论:Toyota 的固件指纹识别是三级顺序匹配:先精确固件匹配,再通用「至少两个唯一 ECU」模糊匹配,最后才调用品牌自定义的平台码回退。平台码只比较「零件号-平台-主版本」键,不比较子版本,所以小版本更新仍可能命中旧车型数据库;而「模糊」只是容忍子版本差异,并不容忍任意 ECU 集合。候选不唯一时必须保持被动或人工核验,不能在控制插件里猜。

    平台配置不是运行时健康

    CAR 的每个枚举成员包含 CarSpecs、车型文档、动力/雷达 DBC 映射和静态 flags。构造平台时执行配置 init(),创建 CAR 枚举时写入平台名并冻结配置对象——这是一份冻结的静态表,不是运行时健康状态。

    几个容易混淆的映射:TSS2 配置默认指向 toyota_nodsu_pt_generated 和 toyota_tss2_adas;SecOC 配置改用 toyota_secoc_pt_generated。若配置含 RADAR_ACC,两个配置类都只留下动力 DBC,表示本快照不按对应雷达 DBC 解析该路径——不能推论车辆没有物理雷达。

    同理,ToyotaCarDocs 默认标注 Toyota A 线束,SecOC 车型文档另用 CUSTOM 支持类型。源文档强调:文档展示字段、DBC 可用性、CarParams.radarUnavailable 和实车传感器健康是四个不同维度。EPS_SCALE 对大多数车型默认 73,少数车型显式覆盖;它进入 Toyota safety 参数低字节与状态侧 EPS 扭矩缩放,不能作为插件自报的可信运行标定。

    固件查询的实际覆盖

    FW_QUERY_CONFIG 声明三组总线 0 诊断请求,各组有独立 ECU 白名单:

    组别 请求流程 边界
    KWP tester-present 后查询 0x1a8801 与其他组共享 ECU 白名单机制
    OBD 版本 短 tester-present 后查询 OBD 版本 独立白名单
    UDS 版本 tester-present / 默认会话 / 扩展会话后查询 UDS 版本 独立白名单

    其中 extra_ecus 只列出额外可查地址,并不表示都参与模糊匹配;部分车型的 ABS 或发动机 ECU 被列为 non_essential_ecus,用来处理缺报或多地址。源文档特别提醒:不能把「可缺席」理解成「该 ECU 的所有错误版本都允许通过」,具体精确匹配语义由通用的 match_fw_to_car_exact() 实施。

    匹配顺序:精确、通用模糊、品牌回退

    通用识别顺序是固定的三级,越靠后越宽松,但每一级都不做「最像的一款」打分:

    阶段 条件 结果处理
    精确固件匹配 固件版本与数据库精确命中 返回是否精确及候选集
    通用唯一 ECU 模糊匹配 「至少两个唯一 ECU」 可返回多个候选
    Toyota 平台码回退 仅当前品牌在模糊阶段尚无候选时调用 返回所有通过的候选

    关键规则在 get_car():只有固件候选恰好一个时,才用它覆盖 CAN 指纹结果;零个或多个候选则保留 CAN 识别结果,显式固定车型设置又可覆盖两者。源文档明确这里没有把唯一固件候选与 CAN 指纹做一致性合取。此外,match_fw_to_car_fuzzy() 虽然接收 vin 参数,但 Toyota 此实现没有读取该参数——VIN 字符串不能消解平台码歧义。

    平台码算法逐步拆解

    get_platform_codes() 从固件字节提取零件号/平台/主版本键,并保留子版本集合。它的步骤可以逐条核对:

    1. 检查首字节是否为 0x01 至 0x03 的块数;未命中则按单块处理。
    2. 总字节长度必须等于块数乘 16,否则跳过该固件。
    3. 每块去掉头尾零和空格,但仅解析第一块;第一块长度 8、10、12 时分别尝试 short、medium、long 正则,且正则使用 search(),不是额外的全字节锚定校验。
    4. 返回字典键为 platform-major(短格式)或 part-platform-major(中/长格式),值是子版本集合。
    5. 第二块和子版本可被保留或解析出来,却不参与最终车型键比较——因此小版本更新可以命中旧车型数据库。

    在模糊匹配阶段,match_fw_to_car_fuzzy() 对每个候选车型遍历数据库里类型属于 fwdCamera、fwdRadar、eps 的 ECU,以地址/子地址从实车固件表取版本;每个预期 ECU 至少要找到一个平台键,否则该候选失败;通过的部分必须覆盖候选的这些预期地址。最后减去 FUZZY_EXCLUDED_PLATFORMS——该集合在本快照为空。

    歧义与插件契约

    跨地址、漏回包、固件格式异常会导致匹配缺席;共享平台/主版本键则可能导致多个候选。源文档对此的定性很直白:这里的「模糊」是容忍子版本差异,不是容忍任意 ECU 集合,更不是把单条相机固件当作充分身份凭证。

    对于硬件自制或插件化平台,错误识别会连带选择错误的 DBC、转向模式和 safety 参数。因此源文档提出一条比现状更严格的拟议契约:车型识别服务应返回 CAN 候选、固件候选及其交叉一致性、证据 ECU/地址/原始回包、精确或模糊等级、冲突原因和数据集版本;安全核心在候选唯一、且 DBC/固件/safety 组合得到验证前,保持 noOutput。这是重构建议,不是当前行为。

    测试层面,test_toyota.py 检查角度/RADAR_ACC 标志属于 TSS2、当前 LTA 车型集合、TSS2 动力 DBC、必要 ECU 项和固件格式,并遍历数据库版本检查专属回退的唯一性与排除集合。但这只证明「现有固件数据库样本」受到约束,不能证明未知地区、替换 ECU 或故意构造的相似固件不会误识别;且本轮未运行这些测试。

    常见问题

    为什么小版本固件更新后还能识别成同一车型?

    因为 get_platform_codes() 返回的键只到「零件号-平台-主版本」,值是子版本集合,而子版本不参与最终车型键比较。第二块和子版本可被保留或解析出来,却不会被用来区分车型,所以小版本更新可以命中旧车型数据库。

    固件匹配失败一定会退回到品牌专用逻辑吗?

    不一定。顺序是精确固件匹配、通用模糊匹配,最后一层才调用 Toyota 自定义平台码回退,而且仅当当前品牌在模糊阶段尚无候选时才调用。若前面的阶段已经返回候选,就不会进入品牌回退。

    VIN 能帮助消除固件候选歧义吗?

    在这份实现里不能。match_fw_to_car_fuzzy() 虽然接收 vin 参数,但 Toyota 该实现没有读取该参数,因此 VIN 字符串不能消解平台码歧义。

    多个固件候选时,最终选哪个车型?

    get_car() 只有在固件候选恰好一个时才用它覆盖 CAN 指纹结果;零个或多个候选则保留 CAN 识别结果,显式固定车型设置又可覆盖两者。源文档说明这里没有把唯一固件候选与 CAN 指纹做一致性合取,因此歧义不会被自动「合取」消解。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TOYOTA_PLATFORM_FIRMWARE_MATCHING_DEEP_DIVE.md(取用了 FW_QUERY_CONFIG 三组请求、三级匹配顺序、平台码算法步骤与拟议插件契约)。
    • 项目 02 —— openpilot-research/CAR_PROTOCOL_IMPLEMENTATIONS.md(仅用于确认 match_fw_to_car() 与 get_car() 在整条识别链中的位置)。

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

    更新记录

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

  • EdgeDeploy:BLE 与 USB 两条通道怎么把签名包送到 ESP32-S3

    很多 MCU 升级方案把「签名校验」当作终点,实际上它只是中间检查点。真正难的是链路:BLE 有 MTU 限制、USB 会掉线、UART 会丢字节。EdgeDeploy 就是把「包可信」与「包能完整到达」拆成两件事分别解决的那一层。

    三条链路,同一套帧语义

    EdgeDeploy v1 的协议语义与承载链路解耦:无论走 UART、原生 USB 还是 BLE,都使用固定帧头、CRC32、传输 ID、分片 ACK、丢包重试、断点续传、接收端重启恢复、提交结果重放和整包 SHA-256。差别只在分片大小与传输载体:BLE 通道把 512 字节的 EdgeDeploy DATA 拆成 128 字节的 GATT 写片,靠通知流重组、超时、取消与重试补齐。

    链路 分片与载体 已完成的软件/实板验证 仍未验证
    原生 USB 序列包分帧,独立流解码器 + 共享加锁接收器 COM6 从 3,504 字节断点续传并成功 COMMIT;序列 4 包七帧 DATA 完整安装;B 槽 Flash 回读 SHA-256 与发送包一致 掉电恢复、目视显示确认
    BLE(GATT) 512 字节 DATA → 128 字节写片;固定 128 位服务/Request/Response UUID ESP32-S3 加密写入/通知服务、NimBLE Host 复用、浏览器闭环 16/16 场景通过 真实射频与配对不在任务验收内
    UART 按传输 ID 分片 ACK 与重传 框架测试覆盖;外接 UART 实传仍待验证 外接 UART 实传、掉电恢复
    Android APK 系统配对 + 签名 EFP 文件选择 + 分片/重试/断点/提交 + 进度界面 ARM64 构建、3 项协议测试、Lint 0 错误;APK 2,987,883 字节 真机配对、安装、重启健康确认、断线恢复与后台行为

    断点续传和提交结果重放解决的是两个不同的失败

    断点续传解决「写到一半断了」:传输 ID 与序号让接收端知道已经收了多少,重连后从偏移继续,而不是从 0 重来。提交结果重放解决「提交后不知道成功没有」:接收端重启后能回答上一次 COMMIT 的结果,避免主机重复提交或误判失败。

    这两条都服务于同一个下游动作——只有当整包 SHA-256 与签名校验通过,C99 接收器才把完整 EFP 交给 A/B 包管理器,随后才是暂存、试运行、原子激活与 5 秒健康窗口确认。也就是说,传输层只保证「完整送到」,是否运行仍由 96 字节事务状态记录与三态机制决定。

    现在已经验证到什么程度

    微雪 ESP32-S3-Touch-AMOLED-1.75 是目前走得最远的实板:EF-106 记录它已从 ota_1 启动并保留 ota_0 原应用与完整 Flash 备份;EF-106D 的串口观察显示 COM6 复位后 7 个信号、2 条管线、4 个组件就绪,65 秒无复位,UI 任务最低剩余栈 7,576 字节。框架侧自动测试当前共 46 项,本机 44 项通过,另 2 项 Linux POSIX 与 SocketCAN vcan0 集成测试在非 Linux 主机按条件跳过。

    需要说清楚的边界是:以上都是串口可见的软件行为,不等于整机通过。屏幕像素、手指触摸、外部传感器、掉电与长期运行尚未验收;Wasm 后端属于 P3 计划,尚未完成。把「编译通过」或「COM 口有输出」说成「产品可用」,是这类端侧项目最常见的自我误导。

    这对赛道数据链路意味着什么

    赛道日的现实是:设备已经在车上、线束已经走好,此时才发现某个信号解析有误,可用的补救窗口只有几十分钟。因此部署通道的价值不在「无线很方便」,而在可断点、可校验、可回退——包传一半断连不必重来,装上去不健康能在 5 秒内自动回到旧槽。

    这与车载采集和复盘环节的诉求是一致的:SR-RaceBox 车载数据采集器 负责把姿态与位置数据完整落盘,SR-Data 圈速分析器 负责事后复盘,任何一次「改完就回不去」的更新都会让整段数据失去连续性。对 Toyota GR86 这类以 OBD 车速作为计时输入的场景尤其明显——计时口径一旦在更新中改变,同一条赛道(例如 宁波国际赛车场)的历史圈速就无法再横向比较。

    FAQ

    EdgeDeploy 和 OTA 是一回事吗?

    不是。OTA 通常指整份固件的无线替换;EdgeDeploy 传的是签名的功能包 EFP,落地后要经 A/B 包管理器暂存、试运行与原子激活,且传输层自带断点续传与提交结果重放。

    BLE 传固件为什么一定要拆成 128 字节?

    因为 GATT 写片受 MTU 限制。EdgeDeploy 把 512 字节 DATA 拆成 128 字节写片,配合通知流重组与重传,才能在受限链路上保证完整性。

    断点续传后还需要重新验签吗?

    需要。整包 SHA-256 与 Ed25519 验签在接收端完成后才交给包管理器,续传只解决字节完整性,不替代来源校验。

    现在能说 EdgeDeploy 已经量产可用吗?

    不能。外接 UART 实传、掉电恢复、Android 真机配对与长期运行仍未验证,Wasm 后端属 P3 未完成项。

    参考资料

    • 项目 05 —— D:/Backup/Documents/开发板侧端模型/EdgeForge自编译软件框架方案.md(V1.4,平台层五类接口、EdgeDeploy v1、MVU 与 P3 路线)
    • 项目 05 —— D:/Backup/Documents/开发板侧端模型/DEVELOPMENT_TASKS.md(EF-105 / EF-105A / EF-105C / EF-106 / EF-106D / EF-114A 状态)

    更新记录

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

  • EdgeSpec 这门语言为什么故意做小:禁掉递归、指针与无界循环的取舍

    结论:EdgeSpec 是一门故意做小的语言。它禁掉了递归、动态代码加载、任意指针、无界字符串、无界循环和未声明 I/O 这六样能力——不是为了省字节,而是为了让每一条规则在编译期就能被证明「跑得完、算得准、不越权」。MCU 侧因此不需要预处理器、汇编器和链接器,只做一次受限翻译。

    为什么把「编译」拆成两段

    把完整工具链塞进 MCU 是个看起来更省事、实际更贵的选择。ESP32-S3 的片内 SRAM 只有 512 KB,勉强跑 Clang 或 GCC 会带来编译延迟、RAM 峰值、ABI 兼容、恶意代码与崩溃恢复这一连串问题。更根本的是:通用编译器给用户的自由度远超车载仪表的需要。

    EdgeForge 的拆法是让重活留在性能充足的 Studio 侧——语法、类型、量纲、越界、权限推导;MCU 只负责把平台无关中间表示(EFIR)翻译成 EdgeVM 字节码,并逐字节核对。这样「能力边界」就在编译期被写死,而不是等到运行时才发现。

    受限语言禁掉的是哪六样

    被禁止的能力 禁它的直接原因 替代做法
    递归 栈深不可静态判定,无法给出确定的内存上限 用有限状态机或固定层数的表达式
    动态代码加载 绕过签名与权限检查,信任根失效 功能包走签名安装通道
    任意指针 越界访问无法在编译期证明 信号按命名空间索引访问
    无界字符串 长度不可预算,缓冲区不可确定 定长字段与枚举值
    无界循环 无法保证在墙钟预算内结束 固定迭代次数或窗口统计原语
    未声明 I/O 插件可触达未授权设备 在设备能力清单内显式声明

    这六条的共同点是:它们都让「这段代码要花多少资源」变成一个无法提前回答的问题。而在车载环境下,无法预算就等于无法承诺。

    预算是语言的一部分,不是运行时的运气

    EdgeVM 的每条 pipeline 都带三重预算:指令预算、墙钟预算与输出预算。触发非法 opcode、越界跳转或栈溢出时,当前插件会被立即停止,而不是降级继续跑。

    这也是它与「在 MCU 上跑 Lua」的关键差别:Lua 提供通用语言能力,很难在编译期证明指令预算、栈深与资源占用;EdgeVM 把这三项做成了代码结构本身的属性。语法检查、类型检查、单位与权限检查、资源检查依次通过之后,才允许生成字节码。

    36 字节是怎么压出来的

    EFIR 使用定长头部与任务表,表达式只允许有限 opcode,事件动作走索引表。ESP32、STM32 与 RP2040 的 C99 编译器都在调用者提供的固定缓冲区内完成转换,全程不申请堆内存——没有堆分配,就没有分配失败这条故障路径。

    产物 输入体积 输出体积 适用条件
    基础示例应用 88 字节 EFIR 36 字节 EdgeVM 常量、信号读取与本地事件
    8 样本平均窗口规则 58 字节 EFIR 20 字节 EdgeVM 最少 4 样本有效,Host API 1.3.0
    Studio 四规则草稿 212 字节 EFIR 132 字节 EdgeVM 草稿态,尚未签名发布

    体积的意义在于更新成本:一次页面布局或阈值调整,从「重新编译并烧录整机固件」降到「传输几百字节并验签」。代价就是上面那六条限制。

    确定性怎么验证

    同一份 EFIR 在三个目标平台上生成完全一致的字节码,这意味着编译结果可以作为制品被审计。基础示例的 88 字节 IR 生成 36 字节字节码,已与 Node 参考编译器逐字节一致;这类比对让「翻译器对不对」从主观判断变成可回归的断言。

    需要区分的是:编译成功只代表确定性校验通过,不代表获得发布许可。AI 生成、开发者编译、维护方签名是三个独立角色,不能互相替代。

    AI 在这套语言里能做什么

    AI 只能输出 EdgeSpec 提案,不产出可运行代码。它可以做需求理解、信号选择、布局组合、规则生成、适配器模板与测试用例;不能扩展 Host API、绕过签名、直接发送总线帧,也不能修改信任根,更不能把猜测的协议发布成正式信号。

    这条边界对产品线的意义是具体的:AI 可以在 SR-RaceBox 车载数据采集器 采集到的信号上组合出新的告警规则,但不能替我们宣称某款车型的私有 PID 已获验证;SR-ECU 赛车 ECU 标定工具 的写入动作属于受控能力,不在插件可自行申请的权限集合内。以 Toyota GR86 这类 200 PS 的后驱入门车为例,车主真正需要的往往是一组「转速阈值 + 水温上限 + 单圈计时」的组合规则,完全落在功能包可覆盖的范围内,不需要触碰任何车辆控制权限。

    FAQ

    EdgeSpec 和直接在 MCU 上跑 Lua 有什么区别?

    区别在于可静态验证。EdgeVM 每条 pipeline 都有指令预算、墙钟预算和输出预算,非法 opcode、越界跳转与栈溢出会立即停止当前插件;Lua 难以在编译期证明这些。

    禁掉递归和循环,还能写出有用的规则吗?

    能。窗口统计、阈值与迟滞、有限状态机、单位显示、记录策略这类车载常用能力都不需要递归或无界循环,用固定迭代次数与窗口原语即可表达。

    有了功能包机制,固件还需要 OTA 吗?

    需要。新外设驱动、新无线协议栈、底层 CAN/USB/显示驱动、VM 漏洞修复与加密库更新都必须走固件通道,功能包降低的是更新频率。

    88 字节与 36 字节是典型值还是保证值?

    是当前原型的实测值,且已验证与 Node 参考编译器逐字节一致;体积随规则复杂度变化,例如 8 样本窗口规则为 58 字节 EFIR、20 字节字节码。

    ESP32-S3 上能顺便跑对话模型吗?

    不适合。S3 只运行规则与显示,通用对话模型应放在手机或 Linux/手机级设备,并按内存、上下文、发热与时延评估量化模型。

    参考资料

    • 项目 05 EdgeForge 开发板侧端框架 —— D:/Backup/Documents/开发板侧端模型/EdgeForge自编译软件框架方案.md(V1.4,2026-09-21)
    • 项目 05 —— LINK端侧传感器软件设计说明.md(V0.1,2026-09-13,设备能力清单)
    • 项目 05 —— OBD_AI插件化平台方案书.md(V1.0,2026-09-13,权限与签名边界)

    更新记录

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