feat(ui/remo): 7003 总线新增 STATUS 身份查询与 GOTO 直达页(#69)

STATUS(命令 4)一帧回答"现在在哪个画面":kind(MENU/PAGE/STANDBY)
+ 菜单路径/选中 + 页 id,布局契约与字节序见 ui_net_sink.h。由 uiTask
轮询驱动,同上下文读静态导航状态:零画面副作用、免锁、不发日志。
新增只读接口 ui_menu_get_nav_state / ui_content_page_current_desc。

GOTO(命令 5)按页 id 查生成表直开内容页,以 VIEW 权限进入(与
"已过查看密码"同档,不代输任何密码)。编译期门控
DTU_BENCH || UI_REMO_ALLOW_GOTO:产品构建恒拒(status=4),模拟器
构建默认开启便于无板验证;执行类页(clear/reset)固件侧同样拒绝
(status=2,与主机 EXEC_PAGE_IDS 同一事实源);待机中拒绝(status=5),
由主机按键唤醒后再来。成功应答带回目标叶节点下标便于对账。

ui_sink 主机单测补导航侧链接假件 + 最小 lvgl 桩(仅本测试目录)。
产品构建 Keil 0 Error 0 Warning;ui_sink 单测 17/17。
This commit is contained in:
2026-11-02 22:19:58 +08:00
parent b8e1de5f2e
commit 493fa6ba0e
13 changed files with 320 additions and 18 deletions
+128
View File
@@ -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;