标签: JotPluggler

  • JotPluggler 视频与缩略图:8 帧缓存与解码器共享

    结论:JotPluggler 的相机与缩略图路径是离线路段媒体查看——实时模式会排除四类编码索引/编码数据、直播编码索引及 thumbnail,因此不能把它描述成实时行车画面输入。build_camera_index() 按数组最短长度合成 (timestamp, segment, decode_index, frame_id) 并按时间排序,但不逐项核对多条字段曲线时间戳一致性、也不在建索引时验证 decode_index 在媒体帧数内,无效索引要到取帧时才失败。解码器按 (CameraType, width, height) 复用,锁只覆盖获取/建立过程,若两路映射到相同组合并发解码,存在共享可变解码状态竞争风险(跨文件静态推断,未复现)。以下为固定快照 c8fb906 的静态阅读,未下载视频、未解码损坏媒体、未运行 GUI、未测内存/帧时延。

    路段中的图像索引

    load_route_data() 在路段数据归一化后,调用 build_camera_index() 分别建立窄路、座舱、广角路和 qcamera 四类索引。它先收集有文件路径的片段,再从对应 *EncodeIdx 曲线读取 segmentNum、segmentId(缺失时 segmentIdEncode)及可选 frameId;按数组最短长度和同一索引组合成 (timestamp, segment, decode_index, frame_id),最后按时间排序。缺少索引曲线时虽然有文件路径,也不会形成可按时间选帧的条目。

    frame_id 会保留在结构中,但本工具取帧请求使用的是 decode_index。build_camera_index() 没有逐项核对多条字段曲线时间戳一致性,也没有在建索引时验证 decode_index 在媒体文件帧数内;实际无效索引要到 FrameReader::get() 才失败。这与 GPS 字段按索引拼接的风险类似:对丢字段、乱序、跨段切换需要带真实日志的对齐测试。

    extract_segment_thumbnails() 另从 THUMBNAIL 事件提取 JPEG,时间优先用 timestampEof,为零时退回事件 logMonoTime;build_route_data() 排序并与其他数据共用时间原点。

    相机请求、解码与纹理

    四个 CameraFeedView 各自有需求线程,围绕「选帧 → 解码 → 上传纹理」运转。下表列出实现行为与边界。

    函数 / 组件 实现行为 边界
    CameraFeedView::Impl::setCameraIndex() 替换相机索引、清空待处理请求和 8 帧结果缓存、递增 generation,按首个片段组织预加载 切换路段会重置纹理与请求;旧下载/解码通过 generation/serial 检查避免结果误入新路段
    request_for_time() / update() 根据 tracker 时间选最近索引帧;优先当前帧或缓存,允许先显示同片段附近帧,再排入最新请求 附近帧只是临时可视回退,不应作为精确时间证据;失败帧记入 failed_request,相同请求不立即重试
    demand_worker_loop() 加载片段的 FrameReader,按 decode_index 解码进 VisionBuf,NV12 转 RGBA;完成后向 UI 交付并向前预取两帧 RGBA 按 width * height * 4 分配;媒体尺寸异常时需端到端限制和压力测试
    preload_worker_loop() / ensure_reader_loaded() 两个预加载线程按与当前片段距离预开其他视频,需求线程可等待同片段的在途加载 readers 是按片段保存的 map,本路径未见固定数量/字节上限
    upload_pending_result() / drawSized() UI 线程上传 RGBA 到 GL 纹理;Fit 模式裁切填满 pane,另一模式保持原比例完整显示 8 帧 RGBA 缓存之外还有纹理、解码缓冲及预加载 reader
    FrameReader::load() / DecoderManager::acquire() HTTP(S) 视频先走本地缓存再用 FFmpeg 探测/建帧索引;按相机类型和尺寸复用解码器 锁只覆盖获取/建立过程;FFmpegVideoDecoder::decode() 使用实例上下文与帧缓冲,未见同实例解码互斥

    这套链路也有资源上界缺口:readers 是按片段保存的 map,本路径未见固定数量或字节上限,长路段可能积累解析器与索引;四个 CameraFeedView 各自有需求线程,若两路映射到相同 (CameraType, width, height) 并发解码,存在共享可变解码状态竞争风险(跨文件静态推断,需并发用例确认)。

    缩略图与实时限制

    ThumbnailView::Impl::update() 用二分查找选离 tracker 最近的缩略图;decode_jpeg() 使用 FFmpeg MJPEG 解码,仅接受 YUV420/422/444 类格式,手工转换成 RGBA,随后上传单个 GL 纹理。成功图像按比例居中并叠加时间、片段、序号;失败索引在当前缩略图集合内不反复解码。rgba.resize(width * height * 4) 前只检查解码尺寸大于零,未见更小的像素/内存预算,应以异常 JPEG 测试资源约束。

    环节 行为 边界
    缩略图选图 二分查找选离 tracker 最近的缩略图,时间优先 timestampEof、为零退回 logMonoTime 失败索引在当前集合内不反复解码
    JPEG 解码 FFmpeg MJPEG,仅接受 YUV420/422/444 类格式,手工转 RGBA 后上传单个 GL 纹理 rgba.resize(width * height * 4) 前只检查解码尺寸大于零,无更小像素/内存预算
    实时订阅排除 实时模式排除四类编码索引/编码数据、直播编码索引及 thumbnail 相机与缩略图路径是离线路段媒体查看,不是实时行车画面
    隐私 座舱视频与缩略图可能涉及人脸、乘员与道路位置 导出、远程下载、缓存清理及访问控制应分开设计

    关键限制来自 should_subscribe_stream_service():它在实时模式排除四类编码索引/编码数据、直播编码索引及 thumbnail,因此上述相机和缩略图路径是离线路段媒体查看,不能把它描述成实时行车画面输入。工具读到的座舱视频和缩略图可能涉及人脸、乘员与道路位置;导出、远程下载、缓存清理及访问控制应分开设计。

    对「万物即插件」的约束

    建议拆成 VideoIndexProvider、FrameDecoder、ThumbnailDecoder 和只读 MediaPane。索引契约必须声明时间基准、片段编号、解码帧序号及缺帧语义;解码器实例要么独占线程,要么明确同步。宿主统一限制像素数、缓存字节、打开的 reader 数及并发下载;出错时向用户标出「正在显示附近帧/上一帧」,避免将近似画面误当当前时刻。媒体插件不得获取 CAN TX、固件更新或安全配置权限。

    同目录测试仅检查帮助输出;本文未运行损坏 HEVC/JPEG、并发多路、远程下载、GPU 纹理或内存压力测试。

    常见问题

    这是实时行车画面吗?

    不是。实时模式会排除四类编码索引/编码数据、直播编码索引及 thumbnail,因此相机与缩略图路径是离线路段媒体查看,不能描述成实时行车画面输入。

    8 帧缓存就是内存上界吗?

    不是。8 帧 RGBA 缓存之外还有纹理、解码缓冲及预加载 reader,不能用「8 帧」估算总内存上界。

    索引里的 decode_index 一定有效吗?

    不一定。build_camera_index() 不在建索引时验证 decode_index 在媒体文件帧数内,实际无效要到 FrameReader::get() 才失败。

    两路相机同时解码安全吗?

    不能假定安全。解码器按 (CameraType, width, height) 复用,锁只覆盖获取/建立过程;若两路映射到相同组合并发解码,存在共享可变解码状态竞争风险。这是跨文件静态推断,需并发视频用例或 ThreadSanitizer 确认,不能视为已复现故障。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/JOTPLUGGLER_VIDEO_THUMBNAIL_DEEP_DIVE.md(相机索引、请求/解码/纹理链与缩略图边界)
    • 项目 02 —— openpilot-research/JOTPLUGGLER_DATA_PIPELINES_DEEP_DIVE.md(图像链所在的离线/实时数据管线,作为本文的补充阅读)

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

    更新记录

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

  • JotPluggler 绘图:状态块、分桶抽样与时间交互边界

    结论:JotPluggler 的绘图是一条显示语义链,不是控制语义链。曲线进入绘图前会经过时间窗裁剪、导数/缩放变换和分桶抽样,跟踪时间的图例值还会按曲线类型做阶梯取值或线性插值;这些都属于屏幕显示行为。抽样结果只用于显示,导出与控制决策必须回到原始数据;车端部署应把诊断绘图插件限定为只读,不能因其消费 CAN 曲线就获得 CAN TX 或安全策略写权限。

    曲线进入绘图的路径

    基线是主仓库 c8fb906 本地源码归档,仅静态阅读;未编译、运行 GUI 或测量大路段绘制性能。draw_pane_windows() 按 pane 类型分派地图、缩略图、相机或 draw_plot()。图表仅处理 visible 且有本地样本或有效路段样本的曲线;RouteSeries 至少需要两个等长的时间和值样本。

    build_curve_series() 将曲线转换成 PreparedCurve:普通路段曲线先按共享时间窗二分查找,保留窗边相邻样本;本地曲线直接复制已有 xs/ys,不走该时间窗裁剪。路段样本在收集阶段经时间排序,但这里未重新排序本地样本;插件提供本地序列时应明确单调时间契约。

    导数模式逐相邻点计算差分,derivative_dt > 0 时使用指定间隔,否则使用实际时间差;dt <= 0 的样本对被跳过。随后统一施加 value_scale 和 value_offset。枚举元数据只在未变换的原始路段曲线上保留。跟踪时间的图例值由采样函数获得:阶梯曲线取前一值,普通曲线线性插值,越界时间取端点值——这是显示语义,不应直接作为控制回路的时序/插值语义。

    渲染与抽样语义

    函数/组件 关键行为 边界
    app_decimate_samples_impl() 将样本分桶,每桶保留首点、最小、最大和末点,按原索引去重;目标点数从绘图区宽度的约两倍计算。 显示抽样,不是无损数据导出;保留极值不保证每个瞬态的形状与准确持续时间。
    build_curve_series() integer_like 曲线改用阶梯模式,直接保留全部变换后样本;非阶梯曲线才按桶抽样。 大量整数/CAN 状态样本仍可能全量进入每帧绘制;格式判断最多约每 256 个样本取一步,稀疏小数可能被漏判。
    compute_plot_bounds() 依据已准备曲线求 Y 极值,布尔型至少覆盖 0/1,补边距,再应用 pane 的显式 Y 限值。 Y 范围来源于准备后的显示样本;非有限值、极端值和混合单位需单独测试。
    draw_state_blocks_pane() 当可见曲线全部具有枚举元数据时,按连续相同状态聚合为彩色块,悬停显示标签、时间区间和持续时间。 块按 llround 后的整数状态划分;不是原始事件边沿的独立校验。
    draw_plot() 普通曲线用 ImPlot PlotLine,整数型用 PlotStairs;共享 X 轴、图例当前值、tracker 竖线和点击选时。 状态块、阶梯与线图有不同采样和显示语义,不应合并成一个无差别插件接口。

    时间窗、播放与持久化

    ensure_shared_range() 优先按布局中的 tab 范围初始化离线路段,实时模式按缓冲时长从最新时间回看;clamp_shared_range() 限制最小水平跨度为 2 秒,并分别处理离线边界和流式右边界;update_follow_range() 将流式窗口保持在最新样本处,infer_stream_follow_state() 根据窗口右缘和目标跨度判断是否恢复跟随。绘图时 ImPlot::SetupAxisLinks 直接绑定 UI 中的 x_view_min/max,所以 pane 之间共用水平视窗。

    函数 行为 边界
    ensure_shared_range() 离线路段按布局 tab 范围初始化,实时模式按缓冲时长从最新时间回看。 实时窗口依赖缓冲时长,不等于数据源已完整。
    clamp_shared_range() 限制最小水平跨度为 2 秒,分别处理离线边界与流式右边界。 跨度限制是显示约束,不改动原始样本。
    advance_playback() 按 ImGui 帧间隔与播放倍率推进 tracker,支持视窗循环,非循环时光标越过 70% 视窗后滚动。 播放是 UI 时间光标行为,不控制车辆。
    apply_axis_limits_editor() 检查有限数与上下界次序,把共享 X 范围写到 tab 所有 pane、Y 限值写回布局,入撤销栈并自动保存草稿。 继承撤销栈「修改前快照」语义问题。

    render_layout() 每帧先维护时间窗,再处理快捷键、播放器和工作区。advance_playback() 按 ImGui 帧间隔与播放倍率推进 tracker,支持选定视窗循环,非循环时在光标越过 70% 视窗后滚动;step_tracker() 以配置步长单步移动。绘图区点击更新 tracker 并画竖线;播放/点击都是 UI 时间光标行为,不会直接控制车辆或改变日志源。

    apply_axis_limits_editor() 检查有限数和上下界次序,把共享 X 范围写到 tab 的所有 pane,并把该 pane 的 Y 限值写回布局,随后入撤销栈、自动保存草稿;它继承布局专题指出的撤销栈「修改前快照」语义问题。draw_pane_context_menu() 提供轴编辑、分割、重置视窗与清空曲线;「曲线样式」、复制图片、保存图表和统计项在本版本被显式禁用,不能列为已实现功能。

    对「万物即插件」的约束

    建议将 SeriesProvider(时间和值及来源)、SeriesTransform(导数/缩放/自定义计算)、PlotRenderer(线/阶梯/状态块)和 TimeCursor(共享窗口/播放)分别定义契约。输入需带时间基准、单调性、单位、枚举表、无值/非有限值规则及最大点数;渲染端有可取消任务与逐帧预算。抽样结果只用于屏幕显示,导出与控制决策须回到原始数据。车端部署仍应把诊断绘图插件限定为只读。

    当前同目录 test_help() 仅覆盖 -h。本篇未执行绘图性能、异常时间戳、NaN/Inf、轴编辑或跨 pane 交互测试;地图、相机/缩略图、日志浏览器和导出仍待专题分析。

    常见问题

    绘图里的曲线抽样会不会丢数据?

    抽样只影响显示。app_decimate_samples_impl() 把样本分桶、每桶保留首点/最小/最大/末点并按原索引去重,源码明确它是显示抽样、不是无损数据导出,保留极值并不保证每个瞬态的形状与准确持续时间。导出与控制决策必须回到原始数据。

    状态块是按事件边沿画的吗?

    不是。draw_state_blocks_pane() 在可见曲线全部具有枚举元数据时,按连续相同状态聚合为彩色块,块按 llround 后的整数状态划分,悬停显示标签、时间区间和持续时间。源码明确这不是原始事件边沿的独立校验。

    跟踪时间的图例值能当控制插值用吗?

    不能。图例值由采样函数获得:阶梯曲线取前一值,普通曲线线性插值,越界时间取端点值。源码说明这是显示语义,不应直接作为控制回路的时序/插值语义;状态块、阶梯与线图也各有不同采样语义,不应合并成一个无差别插件接口。

    JotPluggler 能直接控制车辆吗?

    不能。绘图区的点击与播放都只是 UI 时间光标行为,源码明确它们不会直接控制车辆或改变日志源。车端部署应把诊断绘图插件限定为只读,不能因其消费 CAN 曲线而获得 CAN TX 或安全策略写权限。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/JOTPLUGGLER_PLOT_INTERACTION_DEEP_DIVE.md(取用了曲线进入绘图路径、抽样与状态块语义、时间窗/播放/轴编辑边界与插件约束)。
    • 项目 02 —— openpilot-research/JOTPLUGGLER_LAYOUT_SESSION_DEEP_DIVE.md(仅用于确认撤销栈「修改前快照」语义的接续关系,未取用其未公开内容)。

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

    更新记录

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

  • JotPluggler 布局会话:前态入栈与草稿遮蔽

    结论:JotPluggler 的布局不只是视觉数据——曲线可携带 custom_python,因此加载布局不应自动授予代码执行、文件、网络或 CAN 权限。撤销栈入栈的是修改前快照,由 reset(A) -> 编辑成 B -> push(A) 可推导历史为 [A,A],此时 Undo 得到 A、Redo 也只能得到 A,不能恢复 B。草稿按文件名 stem 放在统一 .jotpluggler_autosave/ 目录,不同目录同名布局可能串用同一草稿。以下为静态源码分析,未编译、未运行 GUI、未验证断电恢复。

    布局模型与加载

    布局以 JSON 描述 tabs 与工作区递归 split 树,叶节点带 curves 时形成 pane。加载路径先解析文本再建模型,二者都不构成信任验证。

    组件 / 函数 源码行为 需要注意
    resolve_layout_path() 直接 JSON 路径优先;XML 路径有同名 JSON 时改用 JSON;否则按名称查内置 layouts/ 路径解析不是信任验证;外部布局中的代码见下文
    parse_layout() 解析 tabs、current_tab_index(兼容 currentTabIndex);无 tab 抛错,当前页索引夹到有效范围 未在该入口做布局大小、递归深度或来源签名限制
    parse_workspace_node() / parse_tab() 递归把带 curves 的叶节点加入 pane 数组,内部节点保存横/纵分割与尺寸 非法节点可能形成空工作区;未做畸形 JSON 压测
    parse_dock_area() / parse_curve() 恢复图表、地图、缩略图、相机类型及视窗范围;曲线可携带变换参数与 custom_python 布局并非纯视觉数据
    run() / reload_layout() 启动及切换布局时,若对应草稿存在则优先加载草稿,再重置 UI 与撤销状态 同名草稿可以遮蔽用户选定的正式布局

    解析入口并未限制布局大小、递归深度或来源签名,也未做畸形 JSON 压测;resolve_layout_path() 只是定位文件,本身不构成信任验证。启动与切换布局时,若对应草稿存在会优先加载草稿,再重置 UI 与撤销状态,因此同名草稿可以遮蔽用户选定的正式布局。

    编辑链与 Dock 树

    编辑入口围绕 pane 树与 tab 两组操作。split_pane_node() 把叶 pane 改成两个子节点,或在方向相同的父节点中插入新 pane;remove_pane_node() 删除 pane 后整理尺寸与索引。split_pane() 与 close_pane() 是上层入口,apply_pane_menu_action() 处理分割、重置视窗、清空及关闭,apply_pane_drop_action() 处理浏览器曲线、已有曲线和地图/相机等特殊 pane 的拖放。修改后经 commit_tab_layout_change() 更新 pane UI 状态、记录历史并写草稿。

    tab 侧由 draw_inline_tab_editor() 提交重命名,另一分支新建、复制和关闭 tab,并调用同一历史/草稿机制。build_dock_tree() 递归把布局树映射为 ImGui Dock 节点,ensure_dockspace() 删除旧节点后按树重建。

    这里要区分两种所有权:模型树是持久化状态,ImGui Dock 节点是运行时投影,不应把二者混成插件的同一所有权对象。

    撤销、草稿与正式保存

    撤销栈保存最多 50 个快照。UndoStack 在加载/重置时把当前布局放入;编辑路径在修改完成后调用 push(before_layout),入栈的是修改前快照而不是当前结果。apply_undo()/apply_redo() 从历史取布局,再由 restore_undo_layout() 同步 UI 并保存草稿。由 reset(A) -> 编辑成 B -> push(A) 可直接推导历史为 [A,A];这是源码逻辑结论,尚未通过 GUI 测试。

    撤销与草稿之间存在联动:加载或重置时 UndoStack.reset() 会把当前布局放入历史;每次编辑入栈的是修改前快照;Undo/Redo 从历史取布局后由 restore_undo_layout() 同步 UI 并写草稿。另一方面,mark_layout_dirty() 直接触发 autosave_layout(),记录当前 tab、序列化全布局并置 layout_dirty,因此草稿在用户显式保存之前就已经落盘。重构时应先补状态历史单测,再规定「当前状态入栈」或「前态/后态命令」的统一语义。

    阶段 行为 边界
    标记脏状态 mark_layout_dirty() 直接调用 autosave_layout(),记录当前 tab、序列化全布局 修改即写草稿,不等用户保存
    草稿路径 autosave_path_for_layout() 按文件名 stem 清洗后放入统一 .jotpluggler_autosave/ 不同目录同名布局、甚至清洗后同名 stem 会映射到同一草稿文件(静态推断)
    正式保存 save_layout_json() 写 tab、分割树、pane 视窗和非运行时曲线;curve_to_json() 保留 Python 代码 保留代码意味着布局文件不是纯数据
    写入方式 write_file_or_throw() 使用 O_TRUNC 直接写目标文件 未见临时文件、原子 rename 或 fsync,不能保证崩溃/断电后一致性
    保存与重置 save_layout() 写正式路径后清草稿与 dirty;reset_layout() 删草稿并重载正式布局 草稿损坏时优先解析草稿的路径会报错,代码未见自动回退正式布局

    对自制硬件与插件架构的约束

    布局可作为可视化配置插件,但其 custom_python 必须拆成独立的代码执行能力:加载布局不应自动授予文件、网络、Params 或 CAN TX 权限。车端部署建议把此类工具与控制进程隔离,默认只读日志/回放,外部布局先做来源确认、版本/schema 校验和执行授权。

    草稿键应基于规范化布局身份(例如完整路径哈希或稳定 UUID),并对正式文件与草稿采用临时文件、校验、原子替换及可回退恢复流程。插件 API 应分开 LayoutModel、PaneRenderer、DataSeriesProvider 与 ExecutableTransform;渲染插件只接收经校验的数据视图,不直接获得设备总线或安全配置写权限。这是重构建议,并非现有实现。

    同目录测试只检查 -h 输出,不能覆盖布局解析、拖放、撤销重做、草稿碰撞、损坏恢复或断电一致性。本篇未执行测试;绘图、地图、视频/缩略图、日志浏览器与导出仍待分别拆解。

    常见问题

    加载一个布局就会执行里面的代码吗?

    布局解析会保留 custom_python,路线加载还会刷新自定义曲线;但按边界设计,加载布局不应自动授予文件、网络、Params 或 CAN TX 权限,代码执行应作为单独能力另行授权。

    Undo 之后再 Redo,能回到被撤销的状态吗?

    不能一概而论。由 reset(A) -> 编辑成 B -> push(A) 可推导历史为 [A,A]:Undo 得到 A,Redo 也只能得到 A,无法恢复 B。这是源码逻辑结论,尚未通过 GUI 测试。

    为什么不同布局会串用同一个草稿?

    因为草稿路径由 autosave_path_for_layout() 按文件名 stem 清洗后放在统一目录;不同目录中的同名布局,甚至清洗后同名的 stem,会映射到同一个草稿文件,存在跨布局串用风险(静态推断)。

    保存布局是原子写吗?

    不是。write_file_or_throw() 使用 O_TRUNC 直接写目标文件,此路径未见临时文件、原子 rename 或 fsync,因此不能据此保证崩溃/断电后的一致性。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/JOTPLUGGLER_LAYOUT_SESSION_DEEP_DIVE.md(布局解析、编辑链、撤销栈与草稿持久化)
    • 项目 02 —— openpilot-research/JOTPLUGGLER_DATA_PIPELINES_DEEP_DIVE.md(布局依赖的数据管线,作为本文的补充阅读)
    • 项目 02 —— openpilot-research/JOTPLUGGLER_EXTRACTION_CUSTOM_CODE_DEEP_DIVE.md(自定义代码的执行链,支撑本文的代码能力拆分建议)

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

    更新记录

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