Imported from jiesou/OPi5-RK3588-ElectricDrive (
AGENTS.md). Install upstream withnpx skills add jiesou/OPi5-RK3588-ElectricDrive. Copyright stays with the author.
学员电气测验端侧 AI 平台 ElectricDrive
一切都发生在 slintui 下,pyqt 弃用了 Python 工具链用 uv Slint 相关文档可以通过 context7 mcp 来获取 这个代码是在 OrangePi 5 Plus 上部署的,PC 上,你可以用 python main_debug.py 来调试 slint 部分, slint 在 .venv 里 将你对存储库的理解和对问题的理解、操作的进度存放到工程根目录的 AGENTS.md 下
代码风格约定
Viewport 线程模式
- 参考
slintui/facesignin/face_signin_viewport.py的写法 - 后台轮询线程方法命名为
_xxx_loop - 在循环内调用具体方法处理,减少 try/except 嵌套
- 使用
requests同步请求,设置timeout=3
API 调用
- 异步场景使用
api_client.xxx_async()方法
相机帧回调
- 参考
slintui/deskclean/deskclean_viewport.py的request_xxx_frame写法 - 使用
camera_service.get_frame()获取帧 - 使用
cv2.cvtColor+np.ascontiguousarray+slint.Image.load_from_array转换
数据类型化
- 服务器返回的消息使用
@dataclass定义,如CvClientXiaoxinUpdateMessage - 静态配置(如故障解决方案)直接写格式化好的文本,不需要运行时转换
2026-03-13 xiaoxin 线程崩溃修复记录
- 问题现象:
xiaoxin_viewport.py后台轮询线程中更新window.XiaoxinPageData,触发ComponentInstance is unsendable跨线程崩溃。 - 第一版修复问题:使用了
slint.invoke_in_main_thread,但当前环境slint顶层无该 API,抛出AttributeError。 - 文档与运行时确认:当前环境可用
invoke_from_event_loop,路径为slint.slint.invoke_from_event_loop(slint.native.invoke_from_event_loop也可用)。 - 本次修复:新增
_invoke_on_ui_thread()兼容调度函数,优先尝试invoke_in_main_thread,否则回退到invoke_from_event_loop的多个可用入口;轮询线程内只做数据拉取,UI 更新统一调度回 UI 线程执行。
2026-05-15 7S RadarChart 实现
背景
xiaoxin-page 中原 7S 数据用 ProgressIndicator 列表展示,改为用 Slint Path 元素绘制雷达图。
关键发现
- Slint
for-in语法不支持在 Path 内部使用(LineTo 等 Path 子元素不能用 for 循环生成),Issue: https://github.com/slint-ui/slint/issues/754 - Slint
for-in在 Path 外部可以正常生成多个 Path 元素(如网格层级、轴线、标签) - Path 子元素坐标是
float类型(无单位),物理坐标需用/ 1px转换为 float - Slint 支持
sin()/cos()三角函数,参数需带deg/rad单位 - Path viewbox 是 1:1 像素映射的关键:默认情况下 Path 会将 path 坐标的 bounding box 映射到元素尺寸,导致坐标被缩放。必须显式设置
viewbox-x/y/width/height才能得到 1:1 像素映射 - Slint 不支持
float * percent:v * 1%会报错 "Cannot convert float to percent",所以数据模型改用float存储比值(1.0 = 100%) - Slider 在 Slint 1.15.1 std-widgets 中可用,有
minimum/maximum/value/changed(float)属性
实现方案
radar-chart.slint:独立 RadarChart 组件,用 Path 绘制雷达图- 4 级同心网格多边形(25%/50%/75%/100%)由外层
for生成独立 Path - 数据填充多边形:硬编码 8 个 LineTo(支持最多 8 个维度),用三元条件跳过超出 axes.length 的顶点
- 标签用
for生成独立 Text 元素 - 每个 Path 都显式设置了 viewbox 以确保 1:1 像素映射
offset: 90deg让第一个轴从顶部(12点钟方向)开始
- 4 级同心网格多边形(25%/50%/75%/100%)由外层
- 受限于 Slint 不支持父元素遍历子元素,NavBar/NavItem 模式无法实现。采用基于
[RadarAxis]数据数组的 API RadarAxis字段:label: string, value: float(比值,1.0 = 100%)- xiaoxin-page 中集成了 7 个 Slider 拖动条,可以直接拖动修改 7S 数据并实时观察雷达图变化
- Slint 版本:1.15.1b1
2026-05-17 xiaoxin 雷达图切换页面消失修复
问题
来回切换界面时 xiaoxin page 的雷达图间歇性消失。
根因
Tab 切换时 Slint 的 if 块会销毁/重建 XiaoxinPage 及其子组件 RadarChart。重建后首次布局前,self.width 和 self.height 瞬态为 0,导致:
r计算为-72(负半径 → 退化的多边形几何)viewbox-width/viewbox-height为0,Path 元素拒绝渲染或缓存了无效几何
Slint 1.15.1b1 在 Path 元素上可能不会在布局完成后正确地重新计算/重绘缓存的几何。
修复
radar-chart.slint:16:r加max(..., 0)守卫,防止负半径radar-chart.slint:29-30, 76-77:viewbox-width/height加max(..., 1)守卫,防止零尺寸视口xiaoxin-page.slint:374-377: 去除雷达图容器的in-out透明度动画(出现时不再有 300ms 动画),确保页面切换回来时立即可见
验证
slint.load_file("ui/app-window.slint") 编译成功。
2026-05-15 xiaoxin-page VL 流式响应与摄像头布局优化
背景
- 摄像头画面 1440x720 (2:1),放在右侧全高区域时
image-fit: contain造成上下巨大黑边 - VL 模型返回的详细分析结果(场景观察、7S逐项评估、思维链)只提取了 JSON 评分,其余内容丢弃
- VL API 为同步阻塞请求,无流式响应,等待时间长且体验差
变更
api_vl_client.py — 新增流式 API
- 新增
analyze_image_stream()生成器方法 - 使用
stream: true开启 OpenAI 兼容流式响应(SSE) max_tokens: 1024(原 256),确保长推理不被截断- 逐 token yield,支持首 token 延迟(TTFT)计时
xiaoxin-page-data.slint — 新增响应数据字段
vl-response-text: string— 流式 VL 模型完整响应文本vl-is-streaming: bool— 流式生成状态指示
xiaoxin_viewport.py — _vl_loop 改用流式
_vl_loop改为调用analyze_image_stream()逐 chunk 接收- 每个 chunk 通过
invoke_from_event_loop实时更新vl_response_text - 流开始时设
vl_is_streaming = true,结束时设为false - 流结束后再对整个响应做 JSON 解析(提取 7S 评分),不影响原始响应展示
xiaoxin-page.slint — 右侧面板布局重构
- 右侧面板改为 VerticalLayout 上下分栏:
- VL 模型分析面板(上方):
- 绿色/灰色圆点指示流式状态
clip: true矩形容纳多行滚动文本- 空白时显示占位文本"等待 VL 模型分析…"
- 摄像头画面(下方):
height: parent.width / 2精确匹配 2:1 宽高比- 消除上下黑边,画面紧凑填充
- VL 模型分析面板(上方):
- 添加 opacity 动画与左侧面板一致的展开/收起效果
关键技术点
parent.width / 2在 Slint 中是合法的 length 表达式,可直接用于子元素高度- Slint
Text的vertical-alignment接受top/center/bottom,不接受start(那是 layout item 的 alignment) invoke_from_event_loop在循环中多次调用是安全的,调用顺序与调度顺序一致
2026-05-17 yolo_tools 模型加载修复
问题
deskclean_viewport.py 中 from .yolo_tools import yolo_tools, ToolBox 报 ModuleNotFoundError,且 YOLO 推理无效果。
根因
- 文件名含连字符: 源文件命名为
yolo-tools.py,Python 无法将连字符文件名作为模块导入(import yolo_tools无法匹配yolo-tools.py) - 模型路径依赖 CWD:
YoloTools.__init__使用"./batch1-electricdrive-tools-v10.rknn"相对路径,当 CWD 不是slintui/时加载失败,self.rknn = None,detect()静默返回空结果
修复
- 重命名
yolo-tools.py→yolo_tools.py - 模型路径改为基于模块文件的绝对路径:
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))定位到slintui/目录
2026-05-19 face_signin_viewport 重构:分辨率缩放坐标错位修复
问题
人脸签到页面的可视化框框和实际人脸位置因为分辨率缩放而错位,代码存在多个 patch,结构混乱。
根因
- 竞态条件导致缩放错位: 推理线程独立更新
img_scale和latest_result.faces(无锁保护),request_signin_frame回调分别读取两个值。若两次读取之间img_scale发生变化(分辨率重初始化),则 face 坐标用错误的 scale 映射到原始帧,导致框框偏移。 - 分辨率变化检测逻辑错误:
max(h, w) != max(self.img_size[1], self.img_size[0])比较的是原始帧长边与缩放后长边,这两个值永远不等,导致_reinit_detector在每个循环都被调用。 cam_id参数无用:camera_service.get_frame(cam_id)实现中完全忽略cam_id参数,直接返回self._frame。- 职责混杂: 坐标映射、绘制逻辑散落在 callback 函数中,未遵循 deskclean_viewport 的静态绘制方法模式。
修复
- 坐标在检测时立即映射: 推理线程完成检测后,立即将 face 坐标从缩放空间映射到原始帧空间(除以
_img_scale),存入latest_result。绘制时直接使用坐标,消除img_scale跨线程竞态。 - 锁保护共享结果: 添加
_result_lock,使用get_latest_result()访问器,遵循deskclean_viewport.py模式。 - 修正分辨率检测: 跟踪
_last_cam_w/_last_cam_h记录上次检测时的原始分辨率,仅在变化时调用_reinit_detector。 - 分辨率自适应绘制:
_draw_faces静态方法通过result.frame_w/result.frame_h与当前叠加帧尺寸计算sx/sy缩放因子,处理检测帧与绘制帧分辨率不一致的边界情况。 _draw_faces静态方法: 绘制逻辑从 callback 提取到视图类,与 deskclean 架构一致。- 去除无效的
cam_id参数传递。
2026-05-22 evaluation yolo.py 切图策略适配 1920x1080
背景
idx=0 摄像头从 1280x720 换为 1920x1080,原切图策略(缩放到 640x640 后左右切图 + 全局缩略)需要随分辨率变更。
变更
EXPECTED_H/EXPECTED_W: 720/1280 → 1080/1920- 切图策略改为左下角裁取方案:
- 从 1920x1080 原图左下角裁取 640x640:
bgr[h-640:h, 0:640] - 上半 320x640 → 上下各 pad 160 到 640x640(保长宽比)
- 下半 320x640 → 上下各 pad 160 到 640x640
- 全局 640x640(不 pad)
- 三图并行推理(source 0/1/2)
- 从 1920x1080 原图左下角裁取 640x640:
- 坐标映射 metas 同步调整:
pad_y加入内部 160px padding,offset_y根据 crop 在 letterboxed 图中的位置计算 ratio_crop恒为 1.0(不再 resize 切图),ratio = scale_fit直接控制
2026-09-07 evaluation OFF 档绑单核 NPU(降低 NPU 占用)
背景
- ON/OFF 两个模型都绑 NPU_CORE_0_1_2(三核全开),单图那点算量三核并行收益很小但三个核都被唤醒,利用率下不来
- 实测 v31 单图:单核 ~100ms vs 三核 ~85ms,只慢 15ms,无感
变更(仅 slintui/evaluation/yolo.py)
_load_model加core_mask参数;OFF 模型(v31)绑 NPU_CORE_0,ON(v322)保持三核- 双实例各绑各的 core,互不干扰
验证
- OFF 单核端到端 38 框/118ms;ON 切回 53 框正常;高频拨动 20 次零异常
- 板上 App 正在 ON 三核模式跑,系统级 load 读数被污染,Core1/2 掉零需重启 App 切 OFF 后看
2026-09-07 evaluation OFF 档限 5fps(真降 NPU 占用)
根因
- 推理线程是打满全场的死循环(sleep 仅 1ms);OFF 单图 60ms/帧比 ON 三切图 82ms/帧还快,省下的时间直接变成更高 NPU 占空比(OFF ~53% vs ON ~44%)
- core_mask 绑定无效:实测绑 Core0 与绑 Core2 的 load 读数完全一样,驱动层未按 mask 隔离,三核 load 表永远一起动;故 OFF 模型的 NPU_CORE_0 绑定保留(无害)但不指望它省占用
变更(slintui/evaluation/camera_viewport.py)
- OFF 档每轮按 5fps 封顶(推理 60ms 则 sleep 140ms);ON 档保持打满
验证(真实链路:camera_service 取帧 + 推理循环,40s 采样)
- OFF:total_ms 63~65,NPU 三核 ~16%
- ON:total_ms 69~72,NPU 三核 ~47%,不受影响
2026-09-13 evaluation 拿掉 OFF 档 5fps 限速(去假慢)
背景
- OFF 档 5fps 封顶是人为 sleep 营造的慢,不是模型真差异;用户要求去假、速度/NPU 如实展示
- OFF 保持 v31 单图 + thr=0.7 真弱档(dense 图 65→38,实拍工作台图 3→0/2),有框但明显更差
变更
- slintui/evaluation/camera_viewport.py:删掉 OFF 档 5fps 封顶,ON/OFF 都只 sleep 1ms 打满
- slintui/ui/evaluation-page.slint:切图数量显示 3:0 → 3:1(OFF 也是真跑 1 张整图)
实测(bench3 双图 + verify 端到端)
- batch3 静态 shape:单图/三切图推理都是约 52ms,core_mask 被驱动忽略(三核永远同动,单核绑定 59
64% vs 三核 5663% 无区别) - 端到端:dense 图 ON 53 框/68ms、OFF 38 框/61ms;实拍图 ON 2 框、OFF 0 框;高频拨动 x20 零异常