feat(ui/nav): 7003 STATUS 身份回读 + 台架 GOTO 直开 + 排帧流落地(#69 五项实施) #70
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: board-ui-nav
|
||||
description: 板端 UI 交互的唯一正确入口:先问固件"我在哪"(STATUS 身份查询),再决定按键;到页用闭环 --to 或 bench 直开 --goto。附 #69 实证的四类走错页根因与恢复配方。
|
||||
whenToUse: 需要上真机/台架进某页面看图(如故障录波、回放、定值)、在板端做 UI 取证、或从错误页面/待机态/未知屏恢复时。**写任何注键/取帧代码之前先读本技能**。
|
||||
---
|
||||
|
||||
# 板端 UI 导航(唯一入口 = scripts/board_ui_nav.py)
|
||||
|
||||
一句话纪律:**一切"进页面看图"都走 `scripts/board_ui_nav.py`,禁止另写一次性注键脚本。**
|
||||
2026-11-02 #38 调试里"总是进错误页面浪费大量时间"的直接原因,就是临时手搓
|
||||
`board_walk*.py` 盲按 `d*9`——而闭环导航器 2026-10-25 起就已存在(10-25 盲走事故
|
||||
9 个目标只有 3 个落对,`docs/UI设计/UI_自动化测试流程.md` §7.1)。
|
||||
|
||||
## 为什么不能自己搓(#69 复盘四类根因,全部有实证)
|
||||
|
||||
1. **协议旧版零身份反馈**:KEY 的 ACK 是空帧,"发了 5 个键"≠"到了第 5 层"。(已修:
|
||||
7003 新增 STATUS 命令,固件直读 `s_depth/s_path/s_sel` + 当前内容页 id,见下)
|
||||
2. **N−1 屏坑**:KEY ACK 与 LCDMEM 推流共用同一条 TCP,`sleep(0.30)` 就抓帧拿到的是
|
||||
上一屏(规则①:排空到帧流静默 ≥900 ms;现在 `key()` 代码已真正执行排空+双帧比对)。
|
||||
3. **物理陷阱**:待机态按 `e` 是**重开最近页**不是进菜单;级联列光标记忆上次行不归零;
|
||||
根列最后两行的子列弹框被 160×160 屏裁掉(selftest/runlog 因此"读不到选带")。
|
||||
4. **认屏误判**:菜单子列标签与页标题同字(记录查询子列里的 事故记录/操作记录…),
|
||||
像素分类器会把级联菜单判成 PAGE——**有 STATUS 时以 STATUS 为准**,trace 留痕不一致。
|
||||
|
||||
## 命令(python 一律 .venv;真机动作必须已持 bench 锁并加 --i-hold-bench-lock)
|
||||
|
||||
```powershell
|
||||
# 我在哪?(固件身份 + 像素双口径,不一致会留痕打印)
|
||||
.venv\Scripts\python.exe scripts\board_ui_nav.py --where --i-hold-bench-lock
|
||||
|
||||
# 到页:闭环步行(像素态设备/老固件也能用;每键重认屏,绝不猜步数)
|
||||
.venv\Scripts\python.exe scripts\board_ui_nav.py --board --to wave_play --grab C:\temp\wx --i-hold-bench-lock
|
||||
|
||||
# 到页:bench 固件直开页(一次往返直达,不注键;见下"直开页的边界")
|
||||
.venv\Scripts\python.exe scripts\board_ui_nav.py --goto wave_play --grab C:\temp\wx --i-hold-bench-lock
|
||||
|
||||
# 提交门禁快检(无台架、无模拟器产物也要绿)
|
||||
.venv\Scripts\python.exe scripts\board_ui_nav.py --selftest-quick
|
||||
```
|
||||
|
||||
`--list` 列出全部合法页 id 与菜单路径;`--trace 路径` 落步迹 jsonl(排错先看它)。
|
||||
退出码:0 成功 / 1 分类或导航失败 / 2 环境错误 / 3 拒动(未持锁确认位等)。
|
||||
|
||||
## 身份回读与直开页(#69 新增,两端共享 ui_net_sink.h 契约)
|
||||
|
||||
- **STATUS(命令 4)**:应答 `[ver][kind][depth][n][id_len][path][sel][page id]`;
|
||||
kind = MENU/PAGE/STANDBY(待机优先判)。老固件不应答 → 主机打印一次后自动退回纯像素。
|
||||
生产固件也支持 STATUS(只读、零画面副作用)。
|
||||
- **GOTO(命令 5)**:**编译期双闸** `DTU_BENCH`(临时 bench 固件)或模拟器宏
|
||||
`UI_REMO_ALLOW_GOTO`;生产固件恒拒 `status=4`。拒绝码:1 无此页 id / 2 执行类页 /
|
||||
3 开页失败 / 5 待机态(先按键唤醒再来)。直开按查看权限 `UI_PERM_VIEW` 进入,
|
||||
**不弹密码**——所以它只用于调试取证,不能当作"过密码"的旁路写路径。
|
||||
- 执行类页面 `clear`/`reset`(第二次 ENTER 即真执行)主机与固件**双侧**默认拒绝,
|
||||
`--allow-exec-pages` 才放行,放行 = 接受后果。
|
||||
|
||||
## 走错页 / 未知屏恢复配方(按序,不跳步)
|
||||
|
||||
1. `--where` 先看 STATUS:`PAGE` → 按 `s` 关页(导航器 `--to` 自动做);
|
||||
`STANDBY` → 按 `e` 唤醒(注意规则②:home 上 `e` 可能重开最近页);
|
||||
`MENU depth>0` → 按 `s` 或 `l` 都收一级(台架复核 2026-11-02:菜单态 `s` 与 `l` 同走
|
||||
`menu_back`,父列选中留在原地;旧说法"`s` 不收列"系把待机态 `s` 无效误读成菜单行为)。
|
||||
2. STATUS 无应答(老固件)才吃退避键序(默认 `esl`,`--backoff-keys sl` 可去 ENTER);
|
||||
ENTER 上限 1 次是硬闸(BACKOFF_MAX_E),报停机就 dump 帧人眼看,不许连按。
|
||||
3. 反复落错页且 trace 里出现"像素与 STATUS 不一致" → 分类器或固件有账要记,
|
||||
把留痕帧 `--dump-fail` 存下交 Lead,**不要**改判据迁就现状。
|
||||
|
||||
## 边界
|
||||
|
||||
- 本脚本不申请锁、不碰串口与烧录;锁的取还见 task-worktree 技能
|
||||
(`lock acquire bench --task <ID>`)。
|
||||
- 台架证据只进 `ci-artifacts/`(`--walk` 默认 `ci-artifacts/uinav/walk/`,与 BOARD_TRUTH
|
||||
读取目录已并一),不进工作树。
|
||||
- UI 源码改动的前后回归是 ui-verify 技能(5 门禁),本技能只管"人到板上去看"。
|
||||
@@ -185,6 +185,7 @@
|
||||
| 烧录与复位语义 | `docs/测试说明/测试工具/ST-LINK_CLI_复位使用说明.md` |
|
||||
| 串口助手 | **`serial-assistant` skill**(命令/判据/陷阱,用到自动加载)、`docs/测试说明/测试工具/串口助手使用指南.md`、`串口助手设计说明.md` |
|
||||
| UI 架构 / 模拟器 / UI 自动化回归 | **`lvgl-simulator` skill**(构建/自检/报错速查,用到自动加载)、`docs/UI设计/UI_设计架构.md`、`LVGL模拟器部署与使用说明.md`、`UI_自动化测试流程.md`;UI 改动回归走 **`ui-verify` skill** |
|
||||
| 板端进页看图 / 注键 / 认屏 / 直开页(真机 UI 交互) | **`board-ui-nav` skill**(唯一入口,**写任何注键脚本之前先读**)、`scripts/board_ui_nav.py`(STATUS 身份回读 + `--where`/`--goto`),`UI_自动化测试流程.md` §7 |
|
||||
| 待办与审计发现登记簿 | `docs/任务规划/README.md` |
|
||||
| 多任务并行开发(开空间 / 合并回收 / 台架与构建排队);**DSH worktree 插件的仓内树怎么接上**(`.dsh-worktrees/` 缺 `.venv`、`plugin adopt`、锁归属 `pw:<name>`) | **`task-worktree` skill**(`@task` 仅为文档别名非可执行命令,显式调用 `/task-worktree`,用到自动加载;插件桥 → 其 F 节)、`docs/环境与部署/并行任务工作树方案.md` §3.4 |
|
||||
| 换机 / 工具路径环境变量 | `docs/环境与部署/` |
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
* 描述符指针、s_open 变指针(NULL = 未打开);7 个页面头包含移交
|
||||
* ui_page_registry.c;日志页身份改打描述符 id(ASCII 页名,比原
|
||||
* id=%u 更可定位);绘制/按键/几何一律未动
|
||||
* 2026-11-02 DTU 项目组 新增 ui_content_page_current_desc(#69 板端导航身份查询:
|
||||
* 页 id 从"只有串口开/关沿可见"升级为可按帧查询)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -599,3 +601,15 @@ uint16_t ui_content_page_selected_row(void) {
|
||||
}
|
||||
return s_sel;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查询当前打开页面的描述符(契约见 ui_content_page.h,Issue #69)
|
||||
* 入口参数:无
|
||||
* 返回值:当前页面描述符指针(Flash 常量,只读);无页打开 = NULL
|
||||
* 限定条件:同上下文(uiTask)读取才有一致性语义;本函数零副作用、不打日志
|
||||
* 函数说明:与 ui_content_page_is_open 的区别只在于顺带交出身份(desc->id),
|
||||
* 推流 STATUS 用它把"开着哪一页"随帧带回,不再只有串口开/关沿可见
|
||||
*/
|
||||
const content_page_def_t *ui_content_page_current_desc(void) {
|
||||
return s_open;
|
||||
}
|
||||
|
||||
@@ -124,6 +124,15 @@ void ui_content_page_invalidate(void);
|
||||
*/
|
||||
bool ui_content_page_is_open(void);
|
||||
|
||||
/*
|
||||
* 函数功能:查询当前打开页面的描述符(身份查询/调试消费,Issue #69)
|
||||
* 入口参数:无
|
||||
* 返回值:当前页面描述符指针(Flash 常量,只读);无页打开 = NULL
|
||||
* 限定条件:与框架同上下文(uiTask)读取才有一致性语义
|
||||
* 函数说明:消费方(推流 STATUS)据 desc->id 回答"开着哪一页";零副作用、不打日志
|
||||
*/
|
||||
const content_page_def_t *ui_content_page_current_desc(void);
|
||||
|
||||
/*
|
||||
* 函数功能:查询当前打开的页面是否可编辑
|
||||
* 入口参数:无
|
||||
|
||||
@@ -283,3 +283,23 @@ int ui_menu_init(void) {
|
||||
(unsigned)(mon.total_size - mon.free_size));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取菜单导航状态(契约见 ui_menu.h,Issue #69)
|
||||
* 入口参数:p_depth / p_path / p_sel - 三个只读出参,任一可为 NULL 表示不取
|
||||
* 返回值:无
|
||||
* 限定条件:同上下文(uiTask)读取;本函数零副作用、不打日志(每帧查询不刷日志)
|
||||
* 函数说明:直接交还静态数组指针(导航状态本身仅 17 B,拷贝反而是第二份要同步的
|
||||
* 事实);消费方按"深度+1"项数读取,越界由消费方的深度钳位负责
|
||||
*/
|
||||
void ui_menu_get_nav_state(uint8_t *p_depth, const uint8_t **p_path, const uint8_t **p_sel) {
|
||||
if (p_depth != NULL) {
|
||||
*p_depth = s_depth;
|
||||
}
|
||||
if (p_path != NULL) {
|
||||
*p_path = s_path;
|
||||
}
|
||||
if (p_sel != NULL) {
|
||||
*p_sel = s_sel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,14 @@
|
||||
* 2026-09-02 王建锋 弃用已废弃的 lv_menu 控件,改为基元控件页面导航
|
||||
* 2026-09-04 王建锋 重构为"横向级联框"模型(菜单页面设计.md):拆出显示层
|
||||
* ui_menu_view 与数据层 ui_menu_tab;本层只留导航状态机
|
||||
* 2026-11-02 DTU 项目组 新增 ui_menu_get_nav_state(#69 板端导航身份查询:
|
||||
* 主机侧不再只靠像素认屏猜"在哪一列选中哪一项")
|
||||
*/
|
||||
#ifndef UI_MENU_H
|
||||
#define UI_MENU_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -31,6 +35,20 @@ extern "C" {
|
||||
*/
|
||||
int ui_menu_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取菜单导航状态(身份查询/调试消费的只读快照,Issue #69)
|
||||
* 入口参数:p_depth - 输出当前深度(0 = 根框) uint8_t *,可为 NULL
|
||||
* p_path - 输出路径数组指针(节点下标) const uint8_t **,可为 NULL
|
||||
* p_sel - 输出各层选中下标数组指针 const uint8_t **,可为 NULL
|
||||
* 返回值:无
|
||||
* 限定条件:与导航同上下文(uiTask/模拟器主循环)调用才有一致性语义;
|
||||
* ui_menu_init() 前调用返回初始零值,不崩
|
||||
* 函数说明:输出指针指向本模块静态数组(共 17 B 导航状态,见设计 §7.2),
|
||||
* 调用方只读、最多读"深度+1"项(上限 MENU_PAGE_MAX)、不得改写;
|
||||
* 弹框被屏裁时像素认屏看不见选带,本接口是主机侧唯一的菜单真值出口
|
||||
*/
|
||||
void ui_menu_get_nav_state(uint8_t *p_depth, const uint8_t **p_path, const uint8_t **p_sel);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
* 在往返边界续命。钩子未注册 = 零行为变化(模拟器/黄金
|
||||
* 回归不注册)。判读与设计口径见
|
||||
* docs/UI设计/UI_设计架构.md §5.8
|
||||
* 2026-11-02 DTU 项目组 #69:新增 STATUS(4) 身份查询应答(kind+菜单路径/选中+页 id,
|
||||
* v1 契约在 ui_net_sink.h)与 GOTO(5) 直开页(台架/模拟器构建,
|
||||
* DTU_BENCH / UI_REMO_ALLOW_GOTO 门控;产品构建应答 NOSUPP)
|
||||
*/
|
||||
#include "ui_net_sink.h"
|
||||
|
||||
@@ -50,6 +53,12 @@
|
||||
#include "ui_tokens.h" /* 屏幕几何令牌(UI_SCREEN_W/H、UI_FB_SIZE 权威在 ui_port.h) */
|
||||
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
|
||||
#include "net_latency_test.h" /* M3 探针:prod_cycle_mcu(LCDMEM 处理净成本) */
|
||||
#include "ui_menu.h" /* 菜单导航状态只读快照(STATUS 身份查询,#69) */
|
||||
#include "ui_menu_view.h" /* MENU_PAGE_MAX(STATUS 深度钳位上界) */
|
||||
#include "ui_content_page.h" /* 当前内容页描述符(STATUS 页 id,#69) */
|
||||
#include "ui_standby.h" /* 待机判定(STATUS)/ 待机拒开(GOTO) */
|
||||
#include "ui_page_registry.h" /* 叶节点→页注册表(GOTO 按 id 直开,#69) */
|
||||
#include "ui_pwd_def.h" /* UI_PERM_VIEW(GOTO 授只读权限,#69) */
|
||||
|
||||
/* (2026-10-02):发送短写/失败告警(MCU=控制台日志,模拟器=printf 桩) */
|
||||
#define DBG_TAG "[UI_NET_SINK]"
|
||||
@@ -62,6 +71,8 @@
|
||||
#define CMD_INIT UI_REMO_CMD_INIT
|
||||
#define CMD_KEY UI_REMO_CMD_KEY
|
||||
#define CMD_LCDMEM UI_REMO_CMD_LCDMEM
|
||||
#define CMD_STATUS UI_REMO_CMD_STATUS
|
||||
#define CMD_GOTO UI_REMO_CMD_GOTO
|
||||
|
||||
/* 屏幕尺寸与帧大小取 ui_tokens.h / ui_port.h 的屏幕令牌(160x160 1bpp = 3200 字节),
|
||||
* 本模块不再自带同值字面量——显示几何与协议几何必须是同一个事实 */
|
||||
@@ -71,6 +82,7 @@
|
||||
|
||||
static uint8_t g_tx[4 + 4 + UI_FB_SIZE + 1]; /* 发送组帧缓冲:[TAG][CMD][LEN][DATA][CRC] */
|
||||
static uint8_t g_lcdmem_data[4 + UI_FB_SIZE]; /* LCDMEM 应答缓冲:4B 偏移 + 1bpp 位图 */
|
||||
static uint8_t g_status_data[32]; /* STATUS 应答缓冲:最大 28 B,见应答注释 */
|
||||
static uint8_t g_rx[RM_RX_BUF]; /* 接收暂存区:整块 recv 后逐字节喂入状态机 */
|
||||
|
||||
static int g_listen = -1; /* 监听 Socket fd(未启动或已停止时为 -1) */
|
||||
@@ -343,6 +355,122 @@ static void remo_handle(const uint8_t *f, int n) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CMD_STATUS: {
|
||||
/* 身份查询 v1(#69,2026-11-02):一帧回答"现在在哪个画面"——
|
||||
* kind + 菜单路径/选中 + 页 id。本模块由 uiTask 轮询驱动(宿主契约
|
||||
* 见 ui_net_sink.h poll 文档),同上下文读静态导航状态,免锁、只读、
|
||||
* 零画面副作用。布局:[ver][kind][depth][n][id_len][path n B][sel n B]
|
||||
* [页 id ASCII id_len B];上界 5 + 2n(≤8) + id_len(≤15) = 28 ≤ 32 */
|
||||
uint8_t depth = 0u;
|
||||
const uint8_t *path = NULL;
|
||||
const uint8_t *sel = NULL;
|
||||
const content_page_def_t *desc;
|
||||
uint8_t npath; /* 帧内 [n] 字段:path/sel 公共长度 */
|
||||
uint8_t id_len;
|
||||
uint8_t kind;
|
||||
uint16_t i;
|
||||
uint16_t p;
|
||||
|
||||
ui_menu_get_nav_state(&depth, &path, &sel);
|
||||
if (depth >= MENU_PAGE_MAX) {
|
||||
depth = (uint8_t)(MENU_PAGE_MAX - 1u); /* 防御钳位(正常不可达) */
|
||||
}
|
||||
npath = (uint8_t)(depth + 1u);
|
||||
desc = ui_content_page_current_desc();
|
||||
id_len = 0u;
|
||||
if (ui_standby_is_active()) {
|
||||
kind = UI_REMO_ST_STANDBY; /* 待机优先:path 仍为底层菜单态,页 id 不附 */
|
||||
} else if (desc != NULL) {
|
||||
kind = UI_REMO_ST_PAGE;
|
||||
if (desc->id != NULL) {
|
||||
size_t l = strlen(desc->id);
|
||||
id_len = (l > UI_REMO_MAX_ID) ? (uint8_t)UI_REMO_MAX_ID : (uint8_t)l;
|
||||
}
|
||||
} else {
|
||||
kind = UI_REMO_ST_MENU;
|
||||
}
|
||||
p = 0u;
|
||||
g_status_data[p++] = UI_REMO_STATUS_VER;
|
||||
g_status_data[p++] = kind;
|
||||
g_status_data[p++] = depth;
|
||||
g_status_data[p++] = npath;
|
||||
g_status_data[p++] = id_len;
|
||||
for (i = 0u; i < npath; i++) {
|
||||
g_status_data[p++] = path[i];
|
||||
}
|
||||
for (i = 0u; i < npath; i++) {
|
||||
g_status_data[p++] = sel[i];
|
||||
}
|
||||
if (id_len > 0u) {
|
||||
memcpy(g_status_data + p, desc->id, id_len);
|
||||
}
|
||||
(void)remo_send(CMD_STATUS, g_status_data, (uint16_t)(p + id_len));
|
||||
break;
|
||||
}
|
||||
#if defined(DTU_BENCH) || defined(UI_REMO_ALLOW_GOTO)
|
||||
case CMD_GOTO: {
|
||||
/* 直开页(#69,2026-11-02):按页 id 一步开内容页,替代多步按键盲走
|
||||
* (主机侧 scripts/board_ui_nav.py --goto)。仅台架构建(DTU_BENCH)
|
||||
* 与模拟器(UI_REMO_ALLOW_GOTO)存在;产品构建走下方 NOSUPP 分支。
|
||||
* 契约:只读授权——与"过查看密码"的进入态同档;执行页(clear/reset)
|
||||
* 拒(清单与主机 EXEC_PAGE_IDS 同一事实,动作前拒);待机中先拒
|
||||
* (STANDBY),主机按键唤醒后再来——不在此处双份判屏。
|
||||
* 应答 [ver][status][node],node = 目标叶节点下标(成功有意义,余 0xFF)。 */
|
||||
uint8_t rsp[3];
|
||||
char want[UI_REMO_MAX_ID + 1u];
|
||||
uint8_t hit;
|
||||
uint8_t i;
|
||||
|
||||
rsp[0] = UI_REMO_STATUS_VER;
|
||||
rsp[2] = 0xFFu;
|
||||
if ((len == 0u) || (len > UI_REMO_MAX_ID)) {
|
||||
rsp[1] = UI_REMO_GOTO_NO_ID;
|
||||
(void)remo_send(CMD_GOTO, rsp, 3u);
|
||||
break;
|
||||
}
|
||||
memcpy(want, data, len);
|
||||
want[len] = '\0';
|
||||
hit = 0xFFu; /* 哨兵 = 未命中 */
|
||||
for (i = 0u; i < (uint8_t)UI_PAGE_MAP_COUNT; i++) {
|
||||
if (strcmp(g_ui_page_map[i].desc->id, want) == 0) {
|
||||
hit = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hit == 0xFFu) {
|
||||
rsp[1] = UI_REMO_GOTO_NO_ID;
|
||||
} else if ((strcmp(want, "clear") == 0) || (strcmp(want, "reset") == 0)) {
|
||||
rsp[1] = UI_REMO_GOTO_EXEC; /* 执行页拒(清单与主机 EXEC_PAGE_IDS 一致) */
|
||||
} else if (ui_standby_is_active()) {
|
||||
rsp[1] = UI_REMO_GOTO_STANDBY; /* 先按键唤醒再查(主机侧自动处理) */
|
||||
} else {
|
||||
if (ui_content_page_is_open()) {
|
||||
ui_content_page_close(); /* 先关已开页(open 守卫:同时只开一页) */
|
||||
}
|
||||
if (ui_content_page_open(g_ui_page_map[hit].desc, (uint8_t)UI_PERM_VIEW)) {
|
||||
rsp[1] = UI_REMO_GOTO_OK;
|
||||
rsp[2] = g_ui_page_map[hit].node;
|
||||
/* 身份留痕:开页本身有框架自带 "page open: id=" 边沿日志,
|
||||
* 这里只补 goto 来源一条,便于台架日志对账 */
|
||||
DBG_INFO("ui jump ok id=%s node=%u", want, (unsigned)rsp[2]);
|
||||
} else {
|
||||
rsp[1] = UI_REMO_GOTO_FAIL;
|
||||
}
|
||||
}
|
||||
(void)remo_send(CMD_GOTO, rsp, 3u);
|
||||
break;
|
||||
}
|
||||
#else
|
||||
case CMD_GOTO: {
|
||||
/* 产品构建:GOTO 编译期剔除,应答 NOSUPP(主机侧据此回退按键走法) */
|
||||
uint8_t rsp[3];
|
||||
rsp[0] = UI_REMO_STATUS_VER;
|
||||
rsp[1] = UI_REMO_GOTO_NOSUPP;
|
||||
rsp[2] = 0xFFu;
|
||||
(void)remo_send(CMD_GOTO, rsp, 3u);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
/* 未知命令码:静默丢弃,保持连接等待后续帧 */
|
||||
break;
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
* 2026-08-29 王建锋 修正监听端口为 7003,与 DTU-RemoteLCD 客户端默认端口对齐
|
||||
* 2026-10-25 DTU 项目组 F3:新增 ui_net_sink_set_beat_cb()(看门狗心跳
|
||||
* 回调注入,往返返回点喂位;未注册 = 零行为变化)
|
||||
* 2026-11-02 DTU 项目组 #69:新增 STATUS(4) 身份查询与 GOTO(5) 直开页命令
|
||||
* (协议契约 UI_REMO_STATUS_VER / UI_REMO_ST_* /
|
||||
* UI_REMO_GOTO_*,v1;主机侧解析器 = scripts/board_ui_nav.py)
|
||||
*/
|
||||
#ifndef UI_NET_SINK_H
|
||||
#define UI_NET_SINK_H
|
||||
@@ -26,6 +29,27 @@ extern "C" {
|
||||
#define UI_REMO_CMD_INIT 1
|
||||
#define UI_REMO_CMD_KEY 2
|
||||
#define UI_REMO_CMD_LCDMEM 3
|
||||
#define UI_REMO_CMD_STATUS 4 /* 身份查询:应答"现在在哪个画面"(v1,2026-11-02) */
|
||||
#define UI_REMO_CMD_GOTO 5 /* 直开页:按页 id 一步开内容页(台架/模拟器构建,2026-11-02) */
|
||||
|
||||
/* STATUS/GOTO 协议契约 v1(#69,2026-11-02;主机侧解析器 = scripts/board_ui_nav.py,
|
||||
* 两端共用本组常量,禁止各自复制字面量)
|
||||
* STATUS 应答布局:[ver][kind][depth][n][id_len][path n B][sel n B][页 id ASCII id_len B]
|
||||
* kind 取值:MENU(菜单屏,path/sel 有效)/ PAGE(内容页开,页 id 尾附)/ STANDBY(待机屏)
|
||||
* GOTO payload:页 id ASCII(≤ UI_REMO_MAX_ID 字节);应答 [ver][status][node]
|
||||
* status 取值:OK(node=目标叶节点)/ NO_ID / EXEC(clear、reset 拒)/ FAIL /
|
||||
* NOSUPP(产品构建,编译期剔除)/ STANDBY(先唤醒再查) */
|
||||
#define UI_REMO_STATUS_VER 1u
|
||||
#define UI_REMO_MAX_ID 15u /* 页 id 字符串上限(现最长 wave_manual 10 字符,留余量) */
|
||||
#define UI_REMO_ST_MENU 0u
|
||||
#define UI_REMO_ST_PAGE 1u
|
||||
#define UI_REMO_ST_STANDBY 2u
|
||||
#define UI_REMO_GOTO_OK 0u
|
||||
#define UI_REMO_GOTO_NO_ID 1u
|
||||
#define UI_REMO_GOTO_EXEC 2u
|
||||
#define UI_REMO_GOTO_FAIL 3u
|
||||
#define UI_REMO_GOTO_NOSUPP 4u
|
||||
#define UI_REMO_GOTO_STANDBY 5u
|
||||
|
||||
/* UI 推流监听端口(须与 DTU-RemoteLCD 客户端默认端口 7003 一致) */
|
||||
#define UI_NET_SINK_PORT 7003
|
||||
@@ -48,7 +72,7 @@ int ui_net_sink_start(uint16_t port);
|
||||
* 限定条件:ui_net_sink_start() 已成功调用
|
||||
* 函数说明:1. 无客户端时先门控(net_listener_armed,纯 RAM 读)再尝试接受连接
|
||||
* 2. 有客户端时先门控(net_can_read)再用 net_recv_nb 非阻塞收帧并解析
|
||||
* (KEEPLIVE/INIT/KEY/LCDMEM);门控判为无数据时仍每 32 轮兜底收一次
|
||||
* (KEEPLIVE/INIT/KEY/LCDMEM/STATUS/GOTO);门控判为无数据时仍每 32 轮兜底收一次
|
||||
* (DONTWAIT 已转正为具名 API;uiTask 受 IWDG 监控,
|
||||
* 非阻塞是契约义务——见 net_socket.h net_recv 文档契约)
|
||||
* 3. 对端关闭或异常时复位客户端,等待下次重监听
|
||||
|
||||
+60
-2
@@ -130,6 +130,12 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
|
||||
——写路径判据(批 5)因此必须把「过密码」当显式状态处理,口径见 §7.5。
|
||||
- 结论:判据脚本只能信画面。脚本因此**每按一键重新认屏**再决定下一个键,且「到达」只由
|
||||
**页面身份**裁定;中途任何假设都写进轨迹,绝不当成结论。
|
||||
- **2026-11-02 #69 复犯与收口**:修 #38(回放页枚举恒空)的会话里又走了弯路——当时没有走
|
||||
本脚本,而是临时手搓 `board_walk*.py` 盲按,落错页后反复重来,浪费大量调试时间。四条根因
|
||||
(协议无身份回读 / `sleep(0.30)` 抓帧拿到 N−1 屏 / 待机·光标·被裁弹框三个物理陷阱 / 技能
|
||||
路由缺位)与成熟项目对照(accessibility 树优先于截图、auto-wait 断言、可复用工作流)见
|
||||
issue #69 评论区;前两条已在代码里收口(§7.6 的 STATUS 与排空双帧),后两条靠
|
||||
**`board-ui-nav` skill** 的「唯一入口」纪律兜住。
|
||||
|
||||
### 7.2 认屏的三条判据(精确字形匹配,复用同一份 `ui_font_cn_12`)
|
||||
|
||||
@@ -141,7 +147,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
|
||||
|
||||
另两条实测纪律(理由写死在代码注释里,此处不重述):
|
||||
|
||||
- **收级联列按 `l`、不按 `s`**:菜单态按 `s` 不收列,用 `ssssssss` 回根正是深页错位的来源之一;
|
||||
- **收级联列的键语义已台架复核定型(2026-11-02,#69)**:菜单态按 `s` 与 `l` 同走 `menu_back`,**都收一级**且父列选中留在原位(实测 depth 1→0、sel 不变)——旧断言「菜单态按 `s` 不收列」是把**待机态 `s` 无效**(task-24 实证)误读成了菜单行为;`ssssssss` 盲按回根不可信这个结论仍然成立,不可信的是"不看回读连按",不是键本身。有 STATUS 时按 depth 收(弹框被屏裁时像素 `n_cols` 会漏数、固件 depth 不会),老固件继续按 `l` 收 + 验终点;
|
||||
- 菜单树的 `#P` 注记可能写在 `#page=` **之前**(`网口设置 #P #page=net_cfg`),解析顺序错了就
|
||||
剥不掉,那一叶的页面 id 也永远认不出来。
|
||||
|
||||
@@ -158,6 +164,10 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
|
||||
# 只走一页并抓图;--trace 落步迹、--dump-fail 落「认错的那一屏」,--outdir 改存证目录
|
||||
& '.venv\Scripts\python.exe' scripts/board_ui_nav.py --board --to yx --grab D:/tmp/yx \
|
||||
--i-hold-bench-lock --trace D:/tmp/uinav_trace.json
|
||||
# 我在哪(固件 STATUS 身份 + 像素双口径)/ bench 固件直开页(#69,见 §7.6)
|
||||
& '.venv\Scripts\python.exe' scripts/board_ui_nav.py --where --i-hold-bench-lock
|
||||
& '.venv\Scripts\python.exe' scripts/board_ui_nav.py --goto wave_play --grab D:/tmp/wx \
|
||||
--i-hold-bench-lock
|
||||
```
|
||||
|
||||
- 提交钩子 `uinav-selftest` 跑的是**快检档**(`--selftest-quick`)。为什么不挂完整档:闭环
|
||||
@@ -186,12 +196,25 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
|
||||
身份裁定,所以它会大声报错而不是静默走错页。实测 `自检记录` / `运行记录` 两页在模拟器
|
||||
上 5 次枚举内没吃到,现场帧见
|
||||
[popup_clipped](../测试说明/测试数据/boot_log_data/20261025_uinav_popup_clipped.png);
|
||||
**真机上那层弹框究竟画在哪,留台架窗口取证后再定修法**——这是取证题,不是编码题。
|
||||
**真机上弹框位置的取证已完成(2026-11-02 台架,#69)**:菜单步进**依旧**吃不到这两叶
|
||||
(末列枚举落到同胞页 `alarm` / `selftest`——但落点现在由 STATUS 如实报出,不再谎称到达);
|
||||
GOTO 直开两叶都到达且完整渲染(自检记录 ink 4073 / 运行记录 ink 3559,证据在 PR #70
|
||||
台架段)。故 `KNOWN_UNREACHABLE` 对 `--to` 步进**保留**(对称设计的销项条件"真走到"
|
||||
尚未满足),已验证的绕行路径是 `--goto`;真机(新固件)侧"回根"改认 STATUS `depth`:
|
||||
`depth==0` 直接返根、`depth>0` 按 s 逐级收(弹框被裁时像素漏计、固件 depth 不漏),
|
||||
旧的"补按 `l` 兜底"仪式只留给无 STATUS 的老固件像素路径。
|
||||
2. 同一标题的多个显示态(网口设置的 只读 / 编辑 / 保存)在分类里都是同一个页面 id
|
||||
`net_cfg`;要区分得看行内文本,写路径判据需要时再加。
|
||||
3. 认屏复用黄金参考的文案:改了页面文案而没同步 `simulator/tests/` 的文本断言表,本节脚本
|
||||
会一起红。那是**有意的耦合**(两处必须同时改),不是脚本脆弱。
|
||||
4. 快检档只证「认屏认得对」,不证「导航走得对」;后者只有完整档的闭环那一段会响。
|
||||
5. **纯像素路径对"子列标签 ≡ 页标题"的级联会误判**:记录查询子列里的 事故记录 / 操作记录 /
|
||||
报警记录 / 启动记录 / 遥控记录 / 运行报告 等标签与对应页标题同字,开子列那一屏会被判成
|
||||
PAGE——`--selftest` 闭环因此对 记录查询 下 6 叶(五本记录 + 录波回放)恒报「进入 记录查询
|
||||
后不是菜单态而是 PAGE」(2026-11-02 对 master 基线复跑确认为**存量**,非 #69 引入;
|
||||
`clear`/`reset` 两页的"失败"是执行类硬闸的刻意拒绝,另计)。真机新固件侧 STATUS 会把它
|
||||
纠成 MENU(trace 里留"像素与 STATUS 不一致"痕);模拟器侧无 STATUS,这 8 项维持红灯直至
|
||||
像素判据补上级联上下文——**别把它们当 #69 的回归**。
|
||||
|
||||
### 7.5 过密码的口径(与黄金回归同源,只有一套走法)
|
||||
|
||||
@@ -200,3 +223,38 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
|
||||
(两套走法迟早分叉,而分叉的那一边往往是真机)。导航器**默认只过查看密码**:判据需要写
|
||||
路径权限时必须显式传 `modify=True`,**不因「能进去」就自动升格成可写**——只读判据偷偷拿到
|
||||
修改权限,等于把判据的边界让给了实现。
|
||||
|
||||
### 7.6 身份回读 STATUS 与直开页 GOTO(#69,2026-11-02)
|
||||
|
||||
旧协议四个命令(KEEP/INIT/KEY/LCDMEM)里没有一个是"我是谁",闭环只能靠像素反推——本节
|
||||
判据的全部不确定性都源于此。#69 在 7003(RemoDispBus)上补了两条,两端共享同一份契约头
|
||||
`Drivers/BSP/UI/ui_net_sink.h`(**布局以该文件注释为唯一权威**,此处只记语义):
|
||||
|
||||
| 命令 | 语义 | 生产固件 | bench 固件 / 模拟器 |
|
||||
|---|---|---|---|
|
||||
| STATUS(4) | 直读固件导航态:kind(MENU/PAGE/STANDBY,待机先判)+ `depth/path/sel`(`ui_menu.c` 的 `s_*` 快照,经 `ui_menu_get_nav_state()`)+ 当前内容页 id(`ui_content_page_current_desc()`) | ✅ 支持(只读、零画面副作用) | ✅ |
|
||||
| GOTO(5) | 按页 id 直开页:查 `g_ui_page_map`,以查看权限 `UI_PERM_VIEW` 开页并回节点号 | ❌ 编译期闸外,恒回 status=4(不支持) | ✅ 受理(闸 = `DTU_BENCH` 或 `UI_REMO_ALLOW_GOTO`) |
|
||||
|
||||
- **主机侧优先问、像素兜底**:`Navigator` 每次认屏先 STATUS、后像素,两边打架以 STATUS 为准
|
||||
并在 trace 留痕;老固件不回 STATUS 时打印一次并永久退回纯像素路径。`--where` 现场打印双
|
||||
口径;`--goto <页 id>` 直开后仍走与 `--to` 相同的终点双验(STATUS + 像素),拒绝码 1 无此
|
||||
页 / 2 执行类页 / 3 开页失败 / 5 待机态。
|
||||
- **KEY 后稳帧已代码化**(规则①不再是纸面纪律):`key()` = 读掉 ACK → 排空到帧流静默
|
||||
≥900 ms(上限 5 s)→ 连取两帧比对,不等重试至多 8 次并在 trace 标 `stable=false`
|
||||
(时钟页秒针恒在走,属预期)。
|
||||
- **GOTO 的边界**:只用于调试取证——它按查看权限直达,**不弹密码**,因此不是"过密码"的旁路
|
||||
写路径;`clear`/`reset` 主机与固件双侧默认拒绝(直开即到达可执行态,没有第二段确认的
|
||||
余地)。MCU 侧要拿到 bench 能力须临时给 `MDK-ARM/STM32F407-Demo.uvprojx` 加 `DTU_BENCH`
|
||||
宏烧录,测完**必须还原宏并重烧产品构建**(uvprojx 抖动不入库,同构建节律)。
|
||||
- **留档帧目录已并一**:`WALK_DIR` 与 `--walk` 存证目录同指 `ci-artifacts/uinav/walk/`
|
||||
(#69 前指 `ci-artifacts/uiwalk/`,走的新帧 BOARD_TRUTH 段永远读不到,静默"跳过(无
|
||||
留档)")。
|
||||
- **实机验证记录(2026-11-02 台架,两档实烧,输出全文贴在 PR #70 的台架证据评论)**:
|
||||
bench 档(`fw build: Nov 2 2026 22:25:12`)上 STATUS 首答 `{kind=MENU depth=0 path=[0]
|
||||
sel=[0]}`;GOTO `wave_play` 一次往返到达——#38 那趟"进故障录波看波形"的级联盲走就此降为
|
||||
一条命令;`selftest` / `runlog` 直开渲染完整(见 §7.4 第 1 条取证结论);PAGE 上按 s 回根
|
||||
由 STATUS 证实;级联收列按 s 逐级收回(§7.2 纪律据此改写);根列光标可经 STATUS `sel`
|
||||
对位(记录查询 = 根行 6,对齐一击命中再 ENTER)。换回**产品档**(`fw build: Nov 2 2026
|
||||
22:29:56 suite=NONE`,同一提交、未加宏)复烧后:STATUS 照常应答(它不设编译期闸),
|
||||
GOTO 回 **status=4(不支持)**——同一对命令在生产/bench 两侧的行为差异由真机钉死,不止
|
||||
纸面推理。烧录两轮均过 `/serial-boot-test` 闭环(身份戳匹配 + 回读 `no difference`)。
|
||||
|
||||
@@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1`
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| ER_IROM1 | 277,124 B (270.63 KB) | 1,048,576 B (1024 KB) | 26.4% |
|
||||
| RW_IRAM1 | 94,336 B (92.12 KB) | 131,072 B (128 KB) | 72.0% |
|
||||
| ER_IROM1 | 277,384 B (270.88 KB) | 1,048,576 B (1024 KB) | 26.5% |
|
||||
| RW_IRAM1 | 94,368 B (92.16 KB) | 131,072 B (128 KB) | 72.0% |
|
||||
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
|
||||
| **Flash (ROM 合计)** | **279,176 B (272.6 KB)** | 1,048,576 B (1024 KB) | **26.6%** |
|
||||
| **Flash (ROM 合计)** | **279,436 B (272.9 KB)** | 1,048,576 B (1024 KB) | **26.6%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
|
||||
@@ -15,16 +15,16 @@
|
||||
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|
||||
|---|---|---|---|---|
|
||||
| ER_IROM1 (Flash) | 277,124 B | 1,048,576 B | 26.4% | 参考 |
|
||||
| ER_IROM1 (Flash) | 277,384 B | 1,048,576 B | 26.5% | 参考 |
|
||||
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
|
||||
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
|
||||
| RW_IRAM1 | 94,336 B | 131,072 B | 72.0% | PASS |
|
||||
| RW_IRAM1 | 94,368 B | 131,072 B | 72.0% | PASS |
|
||||
|
||||
| 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 网络 | 13,702 B | 12 KB | ⚠ 超 1.4 KB |
|
||||
| 存储 | 7,072 B | 12 KB | OK |
|
||||
| UI | 36,441 B | 16 KB | ⚠ 超 19.6 KB |
|
||||
| UI | 36,473 B | 16 KB | ⚠ 超 19.6 KB |
|
||||
| RTOS 堆 | 57,376 B | 56 KB | 堆=配额 |
|
||||
| 应用 | 26,333 B | 8 KB | ⚠ 超 17.7 KB |
|
||||
| 其它 | 10,780 B | — | 参考 |
|
||||
|
||||
+296
-12
@@ -37,6 +37,15 @@
|
||||
落点是内容页就 `s` 关回 home、再按一次 `e` 才是菜单,盲连按走不动;
|
||||
③ 级联列光标**开列时记忆上次停留行**、不归零——"进列后按 k 次下"的算式
|
||||
只在光标已知位于列首时成立;稳妥做法 = 先上溢钳顶(`u` × 列高)再数 k。
|
||||
2026-11-02 #69 身份回读补两条(根因与方案调研见 issue #69 评论区):
|
||||
④ 板端 7003 新增 STATUS 身份查询(kind/depth/path/sel/page id,固件态直读,
|
||||
与像素认屏同源互证):认屏先问板子、像素退为兜底;老固件不应答则自动改走
|
||||
纯像素路径(只损失一次超时)。像素与 STATUS 打架时以 STATUS 为准并留痕。
|
||||
--where 现场打印两侧结论;GOTO 直开页仅 bench 固件(DTU_BENCH / 模拟器编译
|
||||
宏 UI_REMO_ALLOW_GOTO),生产固件恒拒 status=4——见 --goto;
|
||||
⑤ key() 从"sleep(0.30)+单帧 grab"改为真正执行规则①:先排空到**帧流静默
|
||||
≥900 ms**(上限 5 s 不死等),再连取两帧比对;两帧不等(时钟页秒针在走)
|
||||
最多重试 8 次并标注 stable=False——拿 N−1 屏当现行的坑就此封住。
|
||||
退出码: 0 = 成功;1 = 分类或导航失败;2 = 环境错误(exe 缺失 / 板子连不上)
|
||||
创建日期:2026-10-25(道E,Lead 大窗反馈后的判据脚本升级)
|
||||
"""
|
||||
@@ -70,7 +79,9 @@ HARNESS = os.path.join(REPO, "simulator", "tests", "check_content_pages.py")
|
||||
SIM_EXE = os.path.join(REPO, "simulator", "build", "Debug", "lvgl_sim.exe")
|
||||
MENU_TREE = os.path.join(REPO, "ui_menu_tree.txt")
|
||||
# 真机留档帧目录:不在版本控制里(各次台架各自的产物),缺就跳过那一段并说明
|
||||
WALK_DIR = os.environ.get("DSH_UINAV_WALK") or os.path.join(REPO, "ci-artifacts", "uiwalk")
|
||||
WALK_DIR = os.environ.get("DSH_UINAV_WALK") or os.path.join(REPO, "ci-artifacts", "uinav", "walk")
|
||||
# 2026-11-02 #69:原默认 ci-artifacts/uiwalk 与 --walk 的存证目录 uinav/walk 两处不一——
|
||||
# 每次台架走新帧,BOARD_TRUTH 那段永远读不到(静默"跳过(无留档)"),两处并一。
|
||||
# 轨迹/失败帧**默认不写进仓库**:只有 --trace / --dump-fail 显式给了路径才落盘
|
||||
# (判据脚本自己在提交门禁里跑,顺手往 scripts/ 写 json 就是把工作树弄脏)
|
||||
TRACE = None
|
||||
@@ -496,6 +507,28 @@ class SimDevice(object):
|
||||
return Frame.file(out)
|
||||
|
||||
|
||||
def parse_remo_status(data):
|
||||
"""解 CMD_STATUS 应答(v1):[ver][kind][depth][n][id_len][path n B][sel n B][id ASCII]。
|
||||
|
||||
布局唯一权威是 ui_net_sink.h 的 UI_REMO_STATUS_VER 注释块(两端共享同一份头文件,
|
||||
这里按同口径做防御式解析):版本/长度对不上一律 None——宁可退回像素,不可错认身份。
|
||||
"""
|
||||
if not data or len(data) < 5 or data[0] != 1:
|
||||
return None
|
||||
kind_b, depth, n, id_len = data[1], data[2], data[3], data[4]
|
||||
if not (1 <= n <= 4) or len(data) < 5 + 2 * n + id_len:
|
||||
return None
|
||||
pid_b = data[5 + 2 * n:5 + 2 * n + id_len]
|
||||
try:
|
||||
pid = pid_b.decode("ascii") if id_len else None
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
kinds = {0: "MENU", 1: "PAGE", 2: "STANDBY"}
|
||||
return {"ver": 1, "kind": kinds.get(kind_b, "?%d" % kind_b), "depth": depth,
|
||||
"path": list(data[5:5 + n]), "sel": list(data[5 + n:5 + 2 * n]),
|
||||
"page": pid}
|
||||
|
||||
|
||||
class BoardDevice(object):
|
||||
"""RemoDispBus(TCP 7003):CMD_KEY 注键 + CMD_LCDMEM 取 3200 B 帧。
|
||||
|
||||
@@ -506,10 +539,17 @@ class BoardDevice(object):
|
||||
TAG_C = 0xAA
|
||||
CMD_KEY = 2
|
||||
CMD_LCDMEM = 3
|
||||
CMD_STATUS = 4 # #69:身份查询(kind/depth/path/sel/page id,固件态直读)
|
||||
CMD_GOTO = 5 # #69:直开页(仅 bench 固件/模拟器受理,生产固件恒拒)
|
||||
QUIET_S = 0.90 # 规则①帧流静默下限(2026-10-31 板级实测:900 ms 以下拿到的是 N−1 屏)
|
||||
DRAIN_CAP_S = 5.0 # 排空上限:板子疯狂推流也不能挂死在这
|
||||
|
||||
|
||||
def __init__(self, ip=bench_defaults.DEVICE_IP, port=7003):
|
||||
self.s = socket.create_connection((ip, port), timeout=5)
|
||||
self.s.settimeout(3)
|
||||
self._status_supported = True # 老固件不回 STATUS 时置 False:之后只走像素,不再白等
|
||||
self.stable_last = None # 上次 grab_settled 双帧是否一致(时钟页恒在变→False)
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
@@ -548,13 +588,13 @@ class BoardDevice(object):
|
||||
|
||||
def key(self, ch):
|
||||
self._send(self.CMD_KEY, bytes([KEYS[ch]]))
|
||||
# ACK 是空应答:这里**不**试图从应答里读状态(读不到才是本次改造的起因)
|
||||
# ACK 是空应答:读掉它是不让空帧混进后面的排空窗口冒充"新帧"。#69 前这里是
|
||||
# sleep(0.30)+单帧 grab——正是规则①点名的 N−1 屏坑,现在整套换成 settle(⑤)。
|
||||
try:
|
||||
self._read_frame(time.time() + 2.0)
|
||||
except (TimeoutError, ConnectionError):
|
||||
pass
|
||||
time.sleep(0.30)
|
||||
return self.grab()
|
||||
return self.grab_settled()
|
||||
|
||||
def boot(self):
|
||||
return self.grab()
|
||||
@@ -567,6 +607,90 @@ class BoardDevice(object):
|
||||
raise ValueError("LCDMEM 帧短:%d 字节" % len(bmp))
|
||||
return Frame(bmp, "board")
|
||||
|
||||
def _pump(self, want, deadline):
|
||||
"""丢掉积压陈帧(KEY 空 ACK / LCDMEM 推送残留),直到 cmd==want 的应答。
|
||||
|
||||
不能"发一个读一个":推流期间 socket 里排着若干与本次请求无关的帧,先读到的
|
||||
往往是旧 LCDMEM——错认应答比没应答更毒,所以只认命令号。
|
||||
"""
|
||||
while True:
|
||||
cmd, payload = self._read_frame(deadline)
|
||||
if cmd == want:
|
||||
return payload
|
||||
|
||||
def status(self):
|
||||
"""CMD_STATUS:固件直读当前身份。老固件不应答 → None 并记住"别问了"。"""
|
||||
if self._status_supported is False:
|
||||
return None
|
||||
self._send(self.CMD_STATUS)
|
||||
try:
|
||||
data = self._pump(self.CMD_STATUS, time.time() + 3.0)
|
||||
except (TimeoutError, ConnectionError, OSError):
|
||||
data = None
|
||||
s = parse_remo_status(data) if data is not None else None
|
||||
if data is None:
|
||||
self._status_supported = False
|
||||
print("[STATUS] 板端未应答身份查询(老固件?)→ 后续只用像素认屏")
|
||||
return s
|
||||
|
||||
def goto(self, pid):
|
||||
"""CMD_GOTO:直开页。仅 bench 固件/模拟器受理;生产固件回 status=4,老固件不应答。"""
|
||||
b = pid.encode("ascii")
|
||||
if not (1 <= len(b) <= 15):
|
||||
raise ValueError("页 id 需 1..15 个 ASCII 字符:%r" % pid)
|
||||
self._send(self.CMD_GOTO, b)
|
||||
try:
|
||||
data = self._pump(self.CMD_GOTO, time.time() + 4.0)
|
||||
except (TimeoutError, ConnectionError, OSError):
|
||||
return None
|
||||
if not data or len(data) < 2 or data[0] != 1:
|
||||
return None
|
||||
return {"status": data[1], "node": data[2] if len(data) >= 3 else 0xFF}
|
||||
|
||||
def _drain(self):
|
||||
"""排空推流直到**帧流静默 >= QUIET_S**(规则①)。上限 DRAIN_CAP_S 不死等。
|
||||
|
||||
中途 socket 层异常按"流已静默"处理直接收工——宁可少排一会儿(后面双帧比对
|
||||
还会兜住),也不要半帧丢弃把整个流读成错位。
|
||||
"""
|
||||
last = time.time()
|
||||
cap = last + self.DRAIN_CAP_S
|
||||
self.s.settimeout(0.20)
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
self._read_frame(time.time() + 0.25)
|
||||
last = time.time()
|
||||
except TimeoutError:
|
||||
if time.time() - last >= self.QUIET_S:
|
||||
return
|
||||
except (ConnectionError, OSError):
|
||||
return
|
||||
if time.time() > cap:
|
||||
print("[settle] %d s 内帧流未静默,停止排空(结果按不稳定处理)"
|
||||
% self.DRAIN_CAP_S)
|
||||
return
|
||||
finally:
|
||||
self.s.settimeout(3)
|
||||
|
||||
def grab_settled(self):
|
||||
"""规则①执行体:排空(静默>=900 ms) + 连取两帧比对;不等再取,最多 8 次。
|
||||
|
||||
两帧相同才敢说"这是现行"。时钟页秒针永远在走,到顶返回最后一帧并标
|
||||
stable_last=False,让 trace 里看得见"这帧没能双帧自证"。
|
||||
"""
|
||||
self._drain()
|
||||
fr1 = self.grab()
|
||||
for _ in range(8):
|
||||
fr2 = self.grab()
|
||||
if fr2.data == fr1.data:
|
||||
self.stable_last = True
|
||||
return fr2
|
||||
time.sleep(0.25)
|
||||
fr1 = fr2
|
||||
self.stable_last = False
|
||||
return fr1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 闭环导航
|
||||
|
||||
@@ -591,9 +715,64 @@ class Navigator(object):
|
||||
self.clf = clf
|
||||
self.log = log if log is not None else []
|
||||
|
||||
def _ident(self):
|
||||
"""向设备要固件身份;SimDevice 没有这个通道 → None,闭环自检零影响。"""
|
||||
fn = getattr(self.dev, "status", None)
|
||||
if fn is None:
|
||||
return None
|
||||
try:
|
||||
return fn()
|
||||
except Exception as exc:
|
||||
self.log.append({"key": "-", "why": "STATUS 读取失败,本轮只用像素:%s" % exc,
|
||||
"ink": 0, "kind": "-", "page": None, "cols": None})
|
||||
return None
|
||||
|
||||
def _merge_ident(self, fr, st):
|
||||
"""STATUS 为准、像素为佐:打架时按固件身份改写 kind/page,并在 trace 里留痕。
|
||||
|
||||
留痕不是形式主义:像素与固件两头对不上的那一帧,就是分类器或固件该修的现场。
|
||||
密码页特判:固件里它是内容页 id=password,像素侧有专门的 PWD 态与 #P 过密码流程,
|
||||
归一化回 PWD,不让这里的改写把 _descend 的密码闸绕过去。
|
||||
"""
|
||||
s = self._ident()
|
||||
if not s:
|
||||
return st
|
||||
st["status"] = s
|
||||
k = s["kind"]
|
||||
if k == "STANDBY":
|
||||
if st["kind"] not in ("BLANK", "UNKNOWN", "STANDBY"):
|
||||
self.log.append({"key": "-", "why": "不一致:像素=%s 但 STATUS=待机,按待机"
|
||||
% st["kind"], "ink": fr.ink, "kind": st["kind"],
|
||||
"page": st["page"], "cols": None})
|
||||
st["kind"] = "STANDBY"
|
||||
st["page"] = None
|
||||
elif k == "PAGE":
|
||||
pid = s["page"]
|
||||
if st["kind"] == "PAGE" and st["page"] and pid and st["page"] != pid:
|
||||
self.log.append({"key": "-", "why": "像素与 STATUS 不一致:像素=%s 按 STATUS=%s"
|
||||
% (st["page"], pid), "ink": fr.ink, "kind": st["kind"],
|
||||
"page": st["page"], "cols": None})
|
||||
elif st["kind"] in ("UNKNOWN", "BLANK", "PWD"):
|
||||
self.log.append({"key": "-", "why": "像素=%s 没认出,按 STATUS=页 %s"
|
||||
% (st["kind"], pid or "?"), "ink": fr.ink,
|
||||
"kind": st["kind"], "page": st["page"], "cols": None})
|
||||
st["kind"] = "PWD" if pid == "password" else "PAGE"
|
||||
st["page"] = None if pid == "password" else pid
|
||||
if pid == "password":
|
||||
st["page"] = "password"
|
||||
elif k == "MENU":
|
||||
if st["kind"] in ("PAGE", "STANDBY"):
|
||||
self.log.append({"key": "-", "why": "不一致:像素=%s 但 STATUS=菜单,按菜单"
|
||||
% st["kind"], "ink": fr.ink, "kind": st["kind"],
|
||||
"page": st["page"], "cols": None})
|
||||
if st["kind"] != "MENU":
|
||||
st["kind"] = "MENU"
|
||||
st["page"] = None
|
||||
return st
|
||||
|
||||
def snap(self):
|
||||
fr = self.dev.grab()
|
||||
st = classify(fr, self.clf)
|
||||
st = self._merge_ident(fr, classify(fr, self.clf))
|
||||
st["ink"] = fr.ink
|
||||
return fr, st
|
||||
|
||||
@@ -608,9 +787,13 @@ class Navigator(object):
|
||||
|
||||
def _step(self, ch, why):
|
||||
fr = self.dev.key(ch)
|
||||
st = classify(fr, self.clf)
|
||||
self.log.append({"key": ch, "why": why, "ink": fr.ink, "kind": st["kind"],
|
||||
"page": st["page"], "cols": st["cols"]})
|
||||
st = self._merge_ident(fr, classify(fr, self.clf))
|
||||
ent = {"key": ch, "why": why, "ink": fr.ink, "kind": st["kind"],
|
||||
"page": st["page"], "cols": st["cols"]}
|
||||
stable = getattr(self.dev, "stable_last", None)
|
||||
if stable is not None:
|
||||
ent["stable"] = stable
|
||||
self.log.append(ent)
|
||||
return fr, st
|
||||
|
||||
def _dump_unknown(self, fr, st):
|
||||
@@ -652,15 +835,36 @@ class Navigator(object):
|
||||
def to_root(self, limit=14):
|
||||
"""回到"只有一列打开"的根菜单。
|
||||
|
||||
两条实测纪律写在这里:
|
||||
两条实测纪律写在这里(第二条已于 2026-11-02 台架复核改写,#69):
|
||||
· 页面态用 s 退(s = 退出页);
|
||||
· **级联列只能用 l 收**,菜单态按 s 不收列(Lead 大窗实测)——沿用盲走脚本
|
||||
的 ssssssss 回根法,正是深页错位的来源之一。
|
||||
· **级联列收级看回读**:有 STATUS 时按 depth 收——菜单态 s 与 l 同走
|
||||
menu_back、都收一级且父列选中留在原地(台架实测 depth 1→0、sel 不变);
|
||||
旧断言"菜单态按 s 不收列(Lead 大窗实测)"是把待机态 s 无效(task-24)
|
||||
误读成了菜单行为。盲按 ssssssss 回根依旧禁止:不可信的是"不看回读连按",
|
||||
不是键本身。老固件无 STATUS 时仍用 l 收(下方像素分支保留原路)。
|
||||
"""
|
||||
unk = 0
|
||||
unk_e = 0 # 本次 to_root() 已投出去的 ENTER 次数(见 BACKOFF_MAX_E)
|
||||
for _ in range(limit):
|
||||
fr, st = self.snap()
|
||||
s = st.get("status")
|
||||
if s and s["kind"] == "MENU" and s.get("depth") == 0:
|
||||
# 固件直说只有根列打开:被裁弹框根本不存在,旧的"补按 l 兜底"仪式可免
|
||||
# (那条路正是 selftest/runlog 两次掉进同胞页的现场:见 go_page 上方注释)
|
||||
return st
|
||||
if st["kind"] == "STANDBY":
|
||||
# 待机态:s/l 无效、e 唤醒进菜单(2026-10-26 task-24 台架实测)。
|
||||
# 以前它被像素判成 BLANK 走"s 关页"分支——在待机页上按 s 是空转。
|
||||
self._step("e", "待机态(STATUS)→ e 唤醒进菜单")
|
||||
continue
|
||||
if s and s["kind"] == "MENU" and s.get("depth", 0) > 0:
|
||||
# 有固件身份回读时按 depth 收级(2026-11-02 台架复核,#69):菜单态
|
||||
# s = menu_back,收一级且父列选中留在原地。子列弹框被屏裁时像素
|
||||
# n_cols 会漏数、固件 depth 不漏——这正替掉了旧"补按 l 兜底"仪式要
|
||||
# 防的那类漏计。s 在根列上无效但无害(台架同轮实测 depth0 按 s 原地)。
|
||||
self._step("s", "STATUS depth=%d 级联 → s 收一级(台架复核 2026-11-02)"
|
||||
% s["depth"])
|
||||
continue
|
||||
if st["kind"] == "MENU" and self.n_cols(st) == 1:
|
||||
# "只有一列"不等于"在根":子列弹框被屏幕裁掉时它的选带根本不在可扫窗口
|
||||
# 里,n_cols 会漏数它——于是上一轮留在弹框里的选中项还在,直接按 e 就
|
||||
@@ -814,6 +1018,36 @@ class Navigator(object):
|
||||
self._step("s", "未到目标页,退出重走")
|
||||
raise RuntimeError(str(last_err) + ";末列枚举 %d 次仍未到达" % tries)
|
||||
|
||||
def jump_page(self, pid):
|
||||
"""GOTO 直开页(bench 固件/模拟器):言简答复也要终点双验,与 go_page 同一验收口径。
|
||||
|
||||
存在的意义是把"进故障录波看波形"这类调试动作从 5 分钟级联步行降为一次往返;
|
||||
生产固件恒拒 status=4(编译期闸),执行类页面(clear/reset)主机侧同样先拒。
|
||||
"""
|
||||
if pid in EXEC_PAGE_IDS and not ALLOW_EXEC_PAGES:
|
||||
raise RuntimeError("GOTO 同样拒绝执行类页面 " + pid + ":直开即到达可执行态,"
|
||||
"没有第二段确认的余地")
|
||||
names = {0: "opened", 1: "unknown id", 2: "exec refused", 3: "open failed",
|
||||
4: "unsupported(生产固件不带直开页)", 5: "standby active(先 e 唤醒)"}
|
||||
if not hasattr(self.dev, "goto"):
|
||||
raise RuntimeError("该设备后端没有 GOTO 通道(--goto 只对真机 bench 固件有意义)")
|
||||
r = self.dev.goto(pid)
|
||||
self.log.append({"key": "-", "why": "GOTO %s -> %s node=%s"
|
||||
% (pid, names.get(r["status"], r["status"]) if r else "无应答",
|
||||
r["node"] if r else "-"),
|
||||
"ink": 0, "kind": "GOTO", "page": pid, "cols": None})
|
||||
if r is None:
|
||||
raise RuntimeError("GOTO 无应答:老固件不认识该命令(用 --to 步行导航)")
|
||||
if r["status"] != 0:
|
||||
raise RuntimeError("GOTO %s 被拒:%s" % (pid, names.get(r["status"], r["status"])))
|
||||
fr = self.dev.grab_settled()
|
||||
st = self._merge_ident(fr, classify(fr, self.clf))
|
||||
st["ink"] = fr.ink
|
||||
if not (st["kind"] == "PAGE" and st["page"] == pid):
|
||||
raise RuntimeError("GOTO 回了 opened,但终点是 %s:%s(期望 %s)" %
|
||||
(st["kind"], st["page"], pid))
|
||||
return fr, st
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 自检与入口
|
||||
@@ -827,7 +1061,11 @@ class Navigator(object):
|
||||
# 理由;把它写成断言而不是评论,是因为下次改 UI 后这条结论也要能被重新检查。
|
||||
# 已声明的导航限度(不是"允许失败",是**记账**):这两页落在根列最后两行,其子菜单
|
||||
# 弹框会被屏幕裁掉、选带不在可扫窗口内,"枚举 + 验终点"目前吃不到它们。
|
||||
# 依据与现场帧见 docs/UI设计/UI_自动化测试流程.md §7.4;真机上弹框画在哪要等台架取证。
|
||||
# 依据与现场帧见 docs/UI设计/UI_自动化测试流程.md §7.4。台架复核 2026-11-02(#69,
|
||||
# bench 固件 + STATUS):菜单步进**依旧**吃不到这两叶(末列枚举落到同胞页 alarm /
|
||||
# selftest,落点由 STATUS 如实报出、不再谎称到达);但 GOTO 直开两叶都能到达并完整
|
||||
# 渲染(自检记录 ink=4073 / 运行记录 ink=3559,证据在 PR #70 台架段)。本限度对 --to
|
||||
# 步进继续有效,解法是 --goto 绕开被裁弹框,不是继续修枚举。
|
||||
# ⚠ 对称设计:这两页哪天真走到了,本表就是**过期**——脚本会当成失败报出来,逼着同步删
|
||||
# 条目与文档限度,免得限度清单变成永久遮羞布。
|
||||
KNOWN_UNREACHABLE = {
|
||||
@@ -1253,6 +1491,52 @@ def main(argv):
|
||||
dev.close()
|
||||
print("走完 %d 页:成功 %d,失败 %d;轨迹 %s" % (ok + len(bad), ok, len(bad), TRACE))
|
||||
return 1 if bad else 0
|
||||
if "--where" in argv:
|
||||
rc = bench_gate(argv, "--where")
|
||||
if rc:
|
||||
return rc
|
||||
dev = BoardDevice()
|
||||
try:
|
||||
nav = Navigator(dev, clf)
|
||||
fr, st = nav.snap()
|
||||
s = st.get("status")
|
||||
print("STATUS:", ("kind=%s depth=%s path=%s sel=%s page=%s"
|
||||
% (s["kind"], s["depth"], s["path"], s["sel"], s["page"]))
|
||||
if s else "无应答(老固件无身份查询)→ 下面仅为像素认屏")
|
||||
print("像素+STATUS:", st["kind"], st["page"] or "-",
|
||||
"cols=%s" % ([c[0] for c in st["cols"]] if st.get("cols") else None),
|
||||
"ink=%d" % fr.ink)
|
||||
if nav.log:
|
||||
print("留痕:", nav.log[-1]["why"])
|
||||
finally:
|
||||
dev.close()
|
||||
return 0
|
||||
if "--goto" in argv:
|
||||
rc = bench_gate(argv, "--goto")
|
||||
if rc:
|
||||
return rc
|
||||
pid = argv[argv.index("--goto") + 1]
|
||||
print("⚠ --goto 直连真机:调用方必须已持 bench 锁;直开页仅 bench 固件受理")
|
||||
dev = BoardDevice()
|
||||
try:
|
||||
nav = Navigator(dev, clf)
|
||||
try:
|
||||
fr, st = nav.jump_page(pid)
|
||||
print("GOTO 到达", pid, "evid=", st["evid"], "ink=", fr.ink)
|
||||
out = argv[argv.index("--grab") + 1] + "_" + pid + ".bin" \
|
||||
if "--grab" in argv else None
|
||||
if out:
|
||||
with open(out, "wb") as f:
|
||||
f.write(fr.data)
|
||||
print("帧已存", out)
|
||||
rc2 = 0
|
||||
except Exception as exc:
|
||||
print("[GOTO FAIL]", exc)
|
||||
rc2 = 1
|
||||
_dump_trace(nav.log)
|
||||
return rc2
|
||||
finally:
|
||||
dev.close()
|
||||
if "--board" in argv:
|
||||
rc = bench_gate(argv, "--board")
|
||||
if rc:
|
||||
|
||||
@@ -14,4 +14,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
|
||||
2026-10-29 238936 29140 1932 141396 268076 25.6 85904 65.5 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;sb_link.o=4364;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055
|
||||
2026-10-29 243552 28784 1972 145388 272336 26.0 89936 68.6 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;wave_ring.o=5296;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055
|
||||
2026-10-30 246720 29836 2052 149708 276556 26.4 94336 72.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132
|
||||
2026-11-02 247288 29836 2052 149708 277124 26.4 94336 72.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132
|
||||
2026-11-02 247548 29836 2052 149740 277384 26.5 94368 72.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132
|
||||
|
||||
|
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"built_at": "2026-11-02T18:21:27+08:00",
|
||||
"fingerprint": "a4435f138bc6dc5fe18779977ec5a2515e15e72ceeecb9653ce2930b640e71c2",
|
||||
"built_at": "2026-11-02T22:19:57+08:00",
|
||||
"fingerprint": "19f80087a70f1c74afb1d71fb6d754b4b647321da90b312068144a8f31a9a935",
|
||||
"fingerprint_files": 1466,
|
||||
"fingerprint_source": "git-index",
|
||||
"gated_regions": [
|
||||
"RW_IRAM1",
|
||||
"RW_CCM"
|
||||
],
|
||||
"git_head": "d6253aa974f4357779d0a94ff24ba66a58abf094",
|
||||
"git_head": "b8e1de5f2e2515b528b0af21bcb8b423a406b8f2",
|
||||
"map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map",
|
||||
"map_mtime": 1793614877,
|
||||
"map_mtime": 1793629184,
|
||||
"pct": {
|
||||
"ER_IROM1": 26.43,
|
||||
"ER_IROM1": 26.45,
|
||||
"RW_CCM": 87.89,
|
||||
"RW_IRAM1": 71.97,
|
||||
"RW_IRAM1": 72.0,
|
||||
"UNINIT_CCM": 18.75
|
||||
},
|
||||
"regions": {
|
||||
"ER_IROM1": {
|
||||
"max": 1048576,
|
||||
"size": 277124
|
||||
"size": 277384
|
||||
},
|
||||
"RW_CCM": {
|
||||
"max": 65280,
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"RW_IRAM1": {
|
||||
"max": 131072,
|
||||
"size": 94336
|
||||
"size": 94368
|
||||
},
|
||||
"UNINIT_CCM": {
|
||||
"max": 256,
|
||||
@@ -37,10 +37,10 @@
|
||||
"schema": 2,
|
||||
"scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring",
|
||||
"sizes": {
|
||||
"code": 247288,
|
||||
"code": 247548,
|
||||
"ro_data": 29836,
|
||||
"rw_data": 2052,
|
||||
"zi_data": 149708
|
||||
"zi_data": 149740
|
||||
},
|
||||
"threshold_pct": 92.0,
|
||||
"worktree_dirt_at_stamp": 0,
|
||||
|
||||
@@ -179,6 +179,7 @@ target_compile_definitions(lvgl_sim PRIVATE
|
||||
LV_LVGL_H_INCLUDE_SIMPLE # ui_font_cn_12.c 内部 include 简化
|
||||
WIN32_LEAN_AND_MEAN # 防止 Windows.h 拉入 winsock.h(与 winsock2.h 冲突)
|
||||
NET_NO_OS # 剥离 net_socket.h 的 FreeRTOS 相关接口(纯 BSD Socket)
|
||||
UI_REMO_ALLOW_GOTO # 模拟器构建启用 GOTO(5) 直开页(#69,2026-11-02;台架 MCU 门控 DTU_BENCH,见 ui_net_sink.c 注释)
|
||||
)
|
||||
|
||||
target_include_directories(lvgl_sim PRIVATE
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#ifndef TEST_UI_SINK_STUB_LVGL_H
|
||||
#define TEST_UI_SINK_STUB_LVGL_H
|
||||
|
||||
/*
|
||||
* 测试存根 lvgl.h(#69,2026-11-02 新增)
|
||||
*
|
||||
* 为什么存在:ui_net_sink.c 新增 STATUS/GOTO 处理(#69 板端导航身份查询)后,
|
||||
* 间接引用 ui_menu_view.h / ui_content_page.h,两者 include 真实 "lvgl.h"。
|
||||
* 本单测只覆盖成帧 / 心跳 / 状态机纯逻辑(见 run_unit_test.py 文件头),
|
||||
* 运行期不触任何 LVGL,故此处仅提供上述头文件声明处用到的最小类型;
|
||||
* 不模拟任何 LVGL 行为,也不参与 MCU 构建(stub 目录仅本测试使用)。
|
||||
*
|
||||
* 维护约定:ui_content_page.h / ui_menu_view.h 的声明若新增 lv_* 类型依赖,
|
||||
* 在此按需补最小 typedef;禁止在此写真实 LVGL 语义来"凑过"编译。
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct _lv_obj_t lv_obj_t;
|
||||
typedef struct _lv_area_t lv_area_t;
|
||||
typedef struct _lv_layer_t lv_layer_t;
|
||||
typedef uint32_t lv_key_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t a;
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
} lv_color_t;
|
||||
|
||||
/* ui_tokens.h 的 UI_COL_* 宏用到;本测试不断言其值,仅需可编译 */
|
||||
static inline lv_color_t lv_color_white(void)
|
||||
{
|
||||
lv_color_t c;
|
||||
c.a = 0xFFu;
|
||||
c.r = 0xFFu;
|
||||
c.g = 0xFFu;
|
||||
c.b = 0xFFu;
|
||||
return c;
|
||||
}
|
||||
|
||||
static inline lv_color_t lv_color_black(void)
|
||||
{
|
||||
lv_color_t c;
|
||||
c.a = 0xFFu;
|
||||
c.r = 0x00u;
|
||||
c.g = 0x00u;
|
||||
c.b = 0x00u;
|
||||
return c;
|
||||
}
|
||||
|
||||
/* ui_content_page.h 注释提及;为防后续声明引用,按真实 LVGL 键码给出 */
|
||||
#define LV_KEY_LEFT 0x14u
|
||||
#define LV_KEY_ESC 0x1Bu
|
||||
|
||||
#endif /* TEST_UI_SINK_STUB_LVGL_H */
|
||||
@@ -93,6 +93,38 @@ void ui_net_sink_inject_key(uint8_t k) {
|
||||
if (s_key_n < 16) { s_keys[s_key_n++] = k; }
|
||||
}
|
||||
|
||||
/* ============ 假件:导航侧(#69 STATUS/GOTO,2026-11-02 新增) ============
|
||||
* 本单测不发 STATUS/GOTO 帧,以下仅为链接占位:STATUS 恒为根菜单
|
||||
* depth=0、无页、非待机。若后续要断言 STATUS/GOTO 语义,把这些假件
|
||||
* 改成可控状态并新增用例,不要动生产分支。 */
|
||||
#include "ui_menu.h"
|
||||
#include "ui_content_page.h"
|
||||
#include "ui_page_registry.h"
|
||||
#include "ui_standby.h"
|
||||
|
||||
static const uint8_t s_nav_zero[4] = { 0u, 0u, 0u, 0u }; /* 根菜单 path/sel */
|
||||
|
||||
void ui_menu_get_nav_state(uint8_t *p_depth, const uint8_t **p_path,
|
||||
const uint8_t **p_sel)
|
||||
{
|
||||
if (p_depth != NULL) { *p_depth = 0u; }
|
||||
if (p_path != NULL) { *p_path = s_nav_zero; }
|
||||
if (p_sel != NULL) { *p_sel = s_nav_zero; }
|
||||
}
|
||||
const content_page_def_t *ui_content_page_current_desc(void) { return NULL; }
|
||||
bool ui_content_page_open(const content_page_def_t *desc, uint8_t perm)
|
||||
{
|
||||
(void)desc; (void)perm; return false;
|
||||
}
|
||||
void ui_content_page_close(void) { }
|
||||
bool ui_standby_is_active(void) { return false; }
|
||||
|
||||
/* 生成表占位:尺寸必须按 ui_page_registry.h 的固定条数(本头文件 extern
|
||||
* 了 g_ui_page_map[UI_PAGE_MAP_COUNT])。静态零初始化 = 全表 {node=0,
|
||||
* desc=NULL}:本单测从不发 GOTO 帧,生产的 desc 扫描不会被触达 */
|
||||
const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = { { 0u,
|
||||
(const content_page_def_t *)0 } };
|
||||
|
||||
/* ===================== 被测代码 ===================== */
|
||||
#include "ui_net_sink.c"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user