493fa6ba0e
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。
776 lines
36 KiB
C
776 lines
36 KiB
C
/*
|
||
* 模块名称:UI Network Sink (RemoDispBus 服务端 — 双端共用)
|
||
* 模块功能:实现 RemoDispBus 协议服务端(TCP 监听 + 帧收发 + 1bpp 推流),
|
||
* 底层经 net_* BSD Socket API、平台相关操作经 ui_net_sink_backend 抽象,
|
||
* 由 MCU 端(Keil/CH395F)与 PC 模拟器(CMake/Winsock)共同编译,行为一致
|
||
* 适用平台:STM32F407ZGTx + CH395F / PC 模拟器
|
||
* 作者:王建锋
|
||
* 创建日期:2026-08-29
|
||
* 修改记录:
|
||
* 2026-08-29 王建锋 创建初始版本
|
||
* 2026-08-29 王建锋 修正监听端口为 7003;移除连接后主动上报 INIT(避免污染客户端首帧读取)
|
||
* 2026-09-10 王建锋 改为双端共用:底层改用 net_* API + 平台后端;接收改为逐字节
|
||
* 状态机(失步自动重同步);断开/网络重建后经 net_get_sock 判活
|
||
* 并重建监听(修复 MCU 单连接模型断线后无法重连的隐患)
|
||
* 2026-10-02 王建锋 remo_send 校验 net_send 返回值(原丢弃)——短写/失败
|
||
* 告警,连续 3 次失败阻断推送,新连接/网络重建解除阻断
|
||
* (MCU 单连接模型不得 net_close 数据 fd,会连监听一起毁掉)
|
||
* 2026-10-10 王建锋 判活门控 + 兜底收帧:accept/recv
|
||
* 前先用 net_can_accept()/net_can_read()(纯 RAM 读控制块)判
|
||
* "有没有活",无活直接返回——台架常态(无客户端)从每 10 ms
|
||
* 一次 RPC(100 msg/s,与 FTP 抢深度 8 的队列)降到 0 次。
|
||
* 兜底:readable 仅由 RECV_OK 中断置位且 TRAP-NET-17 对账不含
|
||
* "RX 非空但 readable=0",故每 32 轮无条件 recv 一次(≈320 ms);
|
||
* 门控放行时若监听控制块已消失则置 g_listen=-1 触发重建,
|
||
* 避免"门控把端口永久不再接受新客户端"这一新卡死路径
|
||
* 2026-10-12 王建锋 监听生命周期 P1(B10 根治,设计文档 §2.3 不变量 II):accept 门控
|
||
* 由 net_can_accept()(只认 ESTABLISHED)改判
|
||
* net_listener_armed()(LISTENING 或 CLOSED+auto_relisten)——
|
||
* 旧门控在断连待重建窗口恒 0,:7003 永不进 net_accept,
|
||
* net_lost_connect_probe 对该端口等于从未运行;同时 EOF /
|
||
* 真错误两条路径不再自行 bind_listen(重建交回 net 层,
|
||
* 消灭第三/第四个重建所有者),仅 fd 真消失时置 -1
|
||
* 2026-10-25 DTU 项目组 F3(2026-10-25 大窗判读):看门狗心跳回调钩子
|
||
* ui_net_sink_set_beat_cb(),在每个可等待 net 往返
|
||
* (accept / recv_nb / send / close / 重建三件套)返回点
|
||
* 调用——风暴期单轮多往返串成 UI 心跳缺口(实测 bit4
|
||
* 8 连击、uisn=154ms 仍在段内样本),往返本身不动
|
||
* (语义不变,上界收口为登记簿批 3 事项),心跳
|
||
* 在往返边界续命。钩子未注册 = 零行为变化(模拟器/黄金
|
||
* 回归不注册)。判读与设计口径见
|
||
* 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"
|
||
|
||
#include <string.h>
|
||
|
||
#include "net_socket.h"
|
||
#include "net_types.h"
|
||
#include "ui_net_sink_backend.h"
|
||
#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]"
|
||
#include "dbg_log.h"
|
||
|
||
/* RemoDispBus 标签与命令(与 ui_net_sink.h 的 UI_REMO_* 常量对齐) */
|
||
#define TAG_CLIENT UI_REMO_TAG_CLIENT
|
||
#define TAG_DEVICE UI_REMO_TAG_DEVICE
|
||
#define CMD_KEEPLIVE UI_REMO_CMD_KEEP
|
||
#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 字节),
|
||
* 本模块不再自带同值字面量——显示几何与协议几何必须是同一个事实 */
|
||
|
||
#define RM_MAX_RX_PAYLOAD 256 /* 客户端→服务端 payload 上限,防长度字段损坏引发失步 */
|
||
#define RM_RX_BUF 64 /* 接收暂存区:整块 recv 后逐字节喂入解析状态机 */
|
||
|
||
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) */
|
||
static int g_client = -1; /* 当前客户端 Socket fd(无连接时为 -1) */
|
||
static uint16_t g_port; /* 监听端口(网络重建 / 断线后重监听用) */
|
||
static uint32_t g_gen; /* 建立监听时的网络代次号(变化 = 网络已重建) */
|
||
static uint8_t s_send_fail_run; /* 连续发送失败计数(0 = 正常) */
|
||
static uint8_t s_sink_blocked; /* 推送阻断标志(1 = 停发帧,等客户端重连解除) */
|
||
|
||
/*
|
||
* 兜底收帧节拍(门控失效保险,2026-10-10):
|
||
* net_can_read() 的 readable 只由 RECV_OK 中断置位,而 TRAP-NET-17 的
|
||
* net_reconcile_tick() 三类对账**不含**"芯片 RX 非空但 readable=0" →
|
||
* 该中断一旦丢失,纯门控会把已到达的数据永久藏住(改前靠每轮 recv 内的
|
||
* ch395f_get_recv_len() 顺带发现)。故每 RM_LIVENESS_DIV 轮**无条件**收一次:
|
||
* 10 ms 节拍 → 最坏额外延迟 ≈320 ms,而 RPC 频率仍从 100/s 降到 ≈3/s。
|
||
* 取值必须是 2 的幂(用 & 取模,免除法)。
|
||
*/
|
||
#define RM_LIVENESS_DIV 32U
|
||
static uint8_t s_liveness_ctr; /* 兜底计数器(单写者 = 推流宿主任务) */
|
||
|
||
/* 看门狗心跳回调钩子(F3,契约见 ui_net_sink.h):NULL = 不调用(模拟器形态)。
|
||
* 用回调注入而非直调 task_wd_beat() = 分层边界(Drivers/BSP 不得 include
|
||
* App 侧头,layer-deps 门禁),宿主(uiTask)自己决定喂哪位。 */
|
||
static void (*s_beat_cb)(void);
|
||
|
||
/*
|
||
* 函数功能:心跳钩子安全调用点(本文件所有可等待 net 往返的返回处)
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:轮询上下文(宿主任务)
|
||
*/
|
||
static void sink_beat(void) {
|
||
if (s_beat_cb != NULL) {
|
||
s_beat_cb();
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:注册/注销看门狗心跳回调钩子
|
||
* 入口参数:cb - 回调钩子;NULL = 注销(行为与本特性引入前逐字一致)
|
||
* 返回值:无
|
||
* 限定条件:宿主任务在首次 ui_net_sink_poll() 之前调用一次(契约见头文件)
|
||
*/
|
||
void ui_net_sink_set_beat_cb(void (*cb)(void)) {
|
||
s_beat_cb = cb;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:计算 RemoDispBus 帧 payload 的 CRC 校验值
|
||
* 入口参数:d - 待校验数据指针(len 为 0 时不访问)
|
||
* len - 数据长度(字节)
|
||
* 返回值:异或累加校验值
|
||
* 限定条件:无
|
||
* 函数说明:对全部数据字节做异或累加(协议约定的简单校验);len 为 0 时返回 0
|
||
*/
|
||
static uint8_t remo_crc(const uint8_t *d, int len) {
|
||
int i;
|
||
uint8_t c = 0;
|
||
|
||
for (i = 0; i < len; i++) {
|
||
c ^= d[i];
|
||
}
|
||
return c;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:将 16 位值以大端序(高字节在前)写入缓冲区
|
||
* 入口参数:p - 目标缓冲区(至少 2 字节);v - 待写入值
|
||
* 返回值:无
|
||
* 限定条件:p 指向至少 2 字节可写空间
|
||
* 函数说明:网络协议多字节字段统一采用大端序传输
|
||
*/
|
||
static void put_u16(uint8_t *p, uint16_t v) {
|
||
p[0] = (uint8_t)(v >> 8);
|
||
p[1] = (uint8_t)(v & 0xFF);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:将 32 位值以大端序(高字节在前)写入缓冲区
|
||
* 入口参数:p - 目标缓冲区(至少 4 字节);v - 待写入值
|
||
* 返回值:无
|
||
* 限定条件:p 指向至少 4 字节可写空间
|
||
* 函数说明:网络协议多字节字段统一采用大端序传输
|
||
*/
|
||
static void put_u32(uint8_t *p, uint32_t v) {
|
||
p[0] = (uint8_t)(v >> 24);
|
||
p[1] = (uint8_t)(v >> 16);
|
||
p[2] = (uint8_t)(v >> 8);
|
||
p[3] = (uint8_t)(v & 0xFF);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:构造并发送一帧 RemoDispBus 响应(设备→客户端)
|
||
* 入口参数:cmd - 命令码;data - payload 指针(len 非 0 时须非空);len - payload 长度
|
||
* 返回值:0 - 整帧发出;-1 - 发送失败/短写/推送已阻断
|
||
* 限定条件:g_tx 空间足够容纳 4+len+1 字节;g_client >= 0(已有连接)
|
||
* 函数说明:帧格式 [TAG_DEVICE][CMD][LEN_H][LEN_LO][DATA][CRC];CRC 为 payload 异或;
|
||
* 无连接时直接返回
|
||
* (2026-10-02):校验 net_send 返回值(原丢弃)——短写 = 客户端收到坏帧
|
||
* (CRC 不符 → 画面卡死黑屏,而设备侧日志全正常);连续 3 次失败 →
|
||
* 停发:客户端察觉无帧自行重连,芯片侧 DISCONNECT/TIMEOUT 自动重开
|
||
* 监听重建连接;MCU 单连接模型严禁 net_close 数据 fd(会连监听一起毁)
|
||
*/
|
||
static int remo_send(uint8_t cmd, const uint8_t *data, uint16_t len) {
|
||
uint8_t *p;
|
||
int total;
|
||
int ret;
|
||
|
||
if (g_client < 0) {
|
||
return -1;
|
||
}
|
||
if (s_sink_blocked) {
|
||
return -1; /* 推送已阻断:等客户端重连解除 */
|
||
}
|
||
|
||
p = g_tx;
|
||
p[0] = TAG_DEVICE;
|
||
p[1] = cmd;
|
||
p[2] = (uint8_t)(len >> 8);
|
||
p[3] = (uint8_t)(len & 0xFF);
|
||
if (len && data) {
|
||
memcpy(p + 4, data, len);
|
||
}
|
||
p[4 + len] = remo_crc(data, len);
|
||
total = (int)(4 + len + 1);
|
||
ret = net_send(g_client, p, total, 0);
|
||
sink_beat(); /* F3:发送往返返回即喂心跳(KEEPLIVE/INIT/KEY/LCDMEM 同路) */
|
||
if (ret == total) {
|
||
s_send_fail_run = 0U;
|
||
return 0;
|
||
}
|
||
|
||
/* 短写/发送失败:告警 + 累计连续失败数 */
|
||
if (s_send_fail_run < 255U) {
|
||
s_send_fail_run++;
|
||
}
|
||
DBG_ERROR("remo send short: ret=%d/%d run=%u", ret, total,
|
||
(unsigned)s_send_fail_run);
|
||
if (s_send_fail_run >= 3U) {
|
||
s_sink_blocked = 1U;
|
||
DBG_ERROR("remo sink blocked, await client reconnect");
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:回送 INIT 帧(告知客户端屏幕尺寸与帧缓冲大小)
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:g_client >= 0
|
||
* 函数说明:payload 为 [宽u16][高u16][缓冲字节数u32],共 8 字节(大端)
|
||
* ⚠ 2026-09-22 M3 台架实测修复(D17):本函数原以 len=6 发送,只发出
|
||
* 8 字节数据段的前 6 字节 → u32 帧字节数被截成高半字,客户端读到的
|
||
* "帧字节数"恒为 0(真机与模拟器一致复现)。现按字段定义发满 8B。
|
||
*/
|
||
static void remo_send_init(void) {
|
||
uint8_t init[8];
|
||
|
||
put_u16(init, UI_SCREEN_W);
|
||
put_u16(init + 2, UI_SCREEN_H);
|
||
put_u32(init + 4, UI_FB_SIZE);
|
||
(void)remo_send(CMD_INIT, init, 8);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:处理一帧完整的远端命令(客户端→设备),校验通过后按命令分发执行
|
||
* 入口参数:f - 完整帧 [TAG][CMD][LEN][DATA][CRC](数据段自偏移 4 起)
|
||
* n - 帧总长度(字节)
|
||
* 返回值:无
|
||
* 限定条件:f 指向至少 n 字节可读空间;由解析状态机 rm_parser_feed 在 CRC 通过后调用
|
||
* 函数说明:1. 前置校验(任一失败即静默丢弃:不应答、不断连接):
|
||
* a) n >= 5:最短合法帧 5 字节(LEN=0 时 TAG/CMD/LEN_H/LEN_LO/CRC)
|
||
* b) 4+len+1 <= n:长度字段与帧长一致,防长度损坏引发越界读
|
||
* c) CRC 与数据段逐字节异或一致(仅覆盖数据段,不含头与 CRC 自身)
|
||
* d) TAG 必须为 TAG_CLIENT(0xAA):服务端只收客户端方向帧
|
||
* 状态机已做过等价校验,此处复核使本函数自包含(可独立调用)
|
||
* 2. 按命令分发:
|
||
* KEEPLIVE(0) - 原样回显空心跳帧,维持连接保活
|
||
* INIT(1) - 回送屏幕几何 [宽u16][高u16][帧字节数u32](大端,6 字节)
|
||
* KEY(2) - 数据段首字节按键码经平台后端注入 UI,回空帧作确认
|
||
* LCDMEM(3) - 数据段为 4B 大端偏移;偏移 0=整帧,经平台后端取 1bpp
|
||
* 位图后回送 [偏移u32][位图 3200B],非 0 偏移暂不支持
|
||
* 3. 未知命令码静默丢弃
|
||
*/
|
||
static void remo_handle(const uint8_t *f, int n) {
|
||
uint8_t tag;
|
||
uint8_t cmd;
|
||
uint16_t len;
|
||
const uint8_t *data;
|
||
uint8_t crc;
|
||
|
||
/* a) 最小帧长:TAG+CMD+LEN_H+LEN_LO+CRC 共 5 字节,不足必是残帧/噪声 */
|
||
if (n < 5) {
|
||
return;
|
||
}
|
||
|
||
/* 解析帧头:TAG=方向(0xAA 客户端帧 / 0xBB 服务端帧),CMD=命令码,
|
||
* LEN=数据段长度(16 位大端),数据段自偏移 4 起 */
|
||
tag = f[0];
|
||
cmd = f[1];
|
||
len = (uint16_t)(((uint16_t)f[2] << 8) | (uint16_t)f[3]);
|
||
data = f + 4;
|
||
|
||
/* b) 长度一致性:整帧长须恰为 4+len+1,防长度字段损坏导致越界读帧缓冲 */
|
||
if ((int)(4 + len + 1) > n) {
|
||
return;
|
||
}
|
||
|
||
/* c) CRC 校验:数据段逐字节异或(LEN=0 时恒 0)须与帧尾一致,坏帧静默丢弃 */
|
||
crc = f[4 + len];
|
||
if (crc != remo_crc(data, len)) {
|
||
return;
|
||
}
|
||
|
||
/* d) 方向校验:只接受客户端帧 0xAA;0xBB 为服务端→客户端方向,出现在收包路径即非法 */
|
||
if (tag != TAG_CLIENT) {
|
||
return;
|
||
}
|
||
|
||
switch (cmd) {
|
||
case CMD_KEEPLIVE:
|
||
/* 心跳保活:原样回显一个空 KEEPLIVE 帧,维持连接活性 */
|
||
(void)remo_send(CMD_KEEPLIVE, NULL, 0);
|
||
break;
|
||
case CMD_INIT:
|
||
/* 查询屏幕参数:回送 [宽u16][高u16][帧字节数u32](大端,共 6 字节) */
|
||
remo_send_init();
|
||
break;
|
||
case CMD_KEY:
|
||
/* 远端导航按键:数据段首字节为按键码,经平台后端注入 UI 按键队列 */
|
||
if (len >= 1) {
|
||
ui_net_sink_inject_key(data[0]);
|
||
}
|
||
/* 回空帧作“已接收”确认(LEN=0,CRC=0) */
|
||
(void)remo_send(CMD_KEY, NULL, 0);
|
||
break;
|
||
case CMD_LCDMEM: {
|
||
/* 读取显存:数据段为 4 字节大端起始偏移,不足 4 字节判非法丢弃 */
|
||
if (len < 4) {
|
||
break;
|
||
}
|
||
{
|
||
/* 偏移按大端 32 位拼装(协议约定多字节字段大端) */
|
||
uint32_t off = ((uint32_t)data[0] << 24) |
|
||
((uint32_t)data[1] << 16) |
|
||
((uint32_t)data[2] << 8) |
|
||
((uint32_t)data[3]);
|
||
/* 目前仅支持偏移 0(整帧)请求;非 0 偏移暂不支持,静默忽略 */
|
||
if (off == 0) {
|
||
#if defined(TEST_LATENCY_PROBE)
|
||
/* M3 探针 prod_cycle_mcu:从"请求已解析完"到"应答交入芯片"
|
||
* 的 MCU 净成本(1bpp 取帧 + 组帧 CRC + net_send 队列/芯片)。
|
||
* 不含 uiTask 10ms 轮询拾取拍(由 peer 侧 cycle 与 L11 覆盖)。 */
|
||
lat_tmark_t tm;
|
||
|
||
lat_mark(&tm);
|
||
#endif
|
||
/* 整帧请求:经平台后端取 1bpp 位图(MCU: ui_port_get_framebuffer /
|
||
* 模拟器: sim_port_get_screen_mono)写入应答缓冲尾部,失败则静默丢弃 */
|
||
if (ui_net_sink_get_lcdmem(g_lcdmem_data + 4, UI_FB_SIZE) == 0) {
|
||
/* 应答数据段 = [偏移u32(回填请求原值)] + [1bpp 位图 3200B] */
|
||
memcpy(g_lcdmem_data, data, 4);
|
||
(void)remo_send(CMD_LCDMEM, g_lcdmem_data, (uint16_t)(4 + UI_FB_SIZE));
|
||
}
|
||
#if defined(TEST_LATENCY_PROBE)
|
||
lat_probe_add_seg("prod_cycle_mcu", lat_mark_us(&tm));
|
||
#endif
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
|
||
/* ================= RemoDispBus 接收状态机(逐字节解析) =================
|
||
* 网络接收的二进制流按字节喂入有限状态机重组帧:TAG 定界、长度定长、CRC 校验。
|
||
* 一帧可跨多次 recv;任意偏移或噪声字节后,状态机在下一个 TAG 处自动重新同步。 */
|
||
typedef enum {
|
||
RM_ST_TAG = 0, /* 等待帧头 TAG(0xAA),非帧头字节一律丢弃 */
|
||
RM_ST_CMD, /* 等待 CMD 字节 */
|
||
RM_ST_LEN_H, /* 等待长度高字节 */
|
||
RM_ST_LEN_L, /* 等待长度低字节,据此确定 payload 长度 */
|
||
RM_ST_DATA, /* 等待 payload 数据字节 */
|
||
RM_ST_CRC, /* 等待 CRC 校验字节,帧完整 */
|
||
} rm_st_t;
|
||
|
||
typedef struct {
|
||
rm_st_t st; /* 当前状态 */
|
||
uint8_t frame[4 + RM_MAX_RX_PAYLOAD + 1]; /* 重组缓冲 */
|
||
uint16_t pos; /* frame[] 已写入字节数 */
|
||
uint16_t plen; /* 本帧 payload 长度 */
|
||
uint16_t didx; /* payload 已接收字节数 */
|
||
} rm_parser_t;
|
||
|
||
static rm_parser_t g_rm; /* 全局解析器:状态跨 poll 调用保持(一帧可跨多次 recv) */
|
||
|
||
/*
|
||
* 函数功能:复位接收解析状态机到初始(等待帧头)状态
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:无
|
||
* 函数说明:新建连接、连接断开或长度越界失步时调用,清除半帧残留
|
||
*/
|
||
static void rm_parser_reset(void) {
|
||
g_rm.st = RM_ST_TAG;
|
||
g_rm.pos = 0;
|
||
g_rm.plen = 0;
|
||
g_rm.didx = 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:向解析状态机喂入一个字节并推进状态,凑齐合法整帧时分发
|
||
* 入口参数:b - 待处理字节
|
||
* 返回值:无
|
||
* 限定条件:须在持有唯一客户端连接的上下文中调用
|
||
* 函数说明:1. 状态机 TAG→CMD→LEN_H→LEN_L→DATA→CRC 逐字节重组帧
|
||
* 2. TAG 态丢弃非帧头字节,实现失步后自动重同步
|
||
* 3. payload 长度越界(>RM_MAX_RX_PAYLOAD)判为损坏并复位
|
||
* 4. 整帧 CRC 通过才回调 remo_handle;状态跨多次 recv 保持
|
||
*/
|
||
static void rm_parser_feed(uint8_t b) {
|
||
switch (g_rm.st) {
|
||
case RM_ST_TAG:
|
||
if (b != TAG_CLIENT) {
|
||
break;
|
||
}
|
||
g_rm.frame[g_rm.pos++] = b;
|
||
g_rm.st = RM_ST_CMD;
|
||
break;
|
||
|
||
case RM_ST_CMD:
|
||
g_rm.frame[g_rm.pos++] = b;
|
||
g_rm.st = RM_ST_LEN_H;
|
||
break;
|
||
|
||
case RM_ST_LEN_H:
|
||
g_rm.frame[g_rm.pos++] = b;
|
||
g_rm.st = RM_ST_LEN_L;
|
||
break;
|
||
|
||
case RM_ST_LEN_L:
|
||
g_rm.frame[g_rm.pos++] = b;
|
||
g_rm.plen = (uint16_t)(((uint16_t)g_rm.frame[2] << 8) | (uint16_t)g_rm.frame[3]);
|
||
if (g_rm.plen > RM_MAX_RX_PAYLOAD) {
|
||
rm_parser_reset();
|
||
} else {
|
||
g_rm.didx = 0;
|
||
g_rm.st = (g_rm.plen > 0) ? RM_ST_DATA : RM_ST_CRC;
|
||
}
|
||
break;
|
||
|
||
case RM_ST_DATA:
|
||
g_rm.frame[g_rm.pos++] = b;
|
||
if (++g_rm.didx >= g_rm.plen) {
|
||
g_rm.st = RM_ST_CRC;
|
||
}
|
||
break;
|
||
|
||
case RM_ST_CRC: {
|
||
uint16_t total;
|
||
g_rm.frame[g_rm.pos++] = b;
|
||
total = g_rm.pos;
|
||
if (b == remo_crc(g_rm.frame + 4, g_rm.plen)) {
|
||
remo_handle(g_rm.frame, (int)total);
|
||
}
|
||
rm_parser_reset();
|
||
break;
|
||
}
|
||
|
||
default:
|
||
rm_parser_reset();
|
||
break;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:创建并监听 UI 专用 TCP Socket(内部:socket+bind+listen)
|
||
* 入口参数:无
|
||
* 返回值:0 - 成功,-1 - 失败
|
||
* 限定条件:net_* 网络子系统可用
|
||
* 函数说明:绑定 g_port 与任意本地地址并进入监听;成功后写回 g_listen
|
||
*/
|
||
static int bind_listen(void) {
|
||
int ls;
|
||
struct net_sockaddr_in addr;
|
||
|
||
ls = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||
if (ls < 0) {
|
||
return -1;
|
||
}
|
||
|
||
memset(&addr, 0, sizeof(addr));
|
||
addr.sin_family = NET_AF_INET;
|
||
addr.sin_port = net_htons(g_port);
|
||
addr.sin_addr.s_addr = NET_INADDR_ANY;
|
||
|
||
if (net_bind(ls, (struct net_sockaddr *)&addr, (int)sizeof(addr)) < 0) {
|
||
net_close(ls);
|
||
return -1;
|
||
}
|
||
if (net_listen(ls) < 0) {
|
||
net_close(ls);
|
||
return -1;
|
||
}
|
||
|
||
g_listen = ls;
|
||
sink_beat(); /* F3:重建三件套(socket/bind/listen 三次往返)结束 */
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:启动 RemoDispBus 服务端(创建并监听 TCP Socket)
|
||
* 入口参数:port - 监听端口号
|
||
* 返回值:0 - 成功,-1 - 失败
|
||
* 限定条件:网络子系统已初始化(模拟器端 Winsock 已就绪)
|
||
* 函数说明:创建流式 Socket 并绑定端口进入监听;成功才记录网络代次号
|
||
*/
|
||
int ui_net_sink_start(uint16_t port) {
|
||
g_port = port;
|
||
|
||
if (bind_listen() < 0) {
|
||
g_listen = -1;
|
||
return -1;
|
||
}
|
||
|
||
g_client = -1;
|
||
g_gen = net_get_generation();
|
||
rm_parser_reset();
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:非阻塞轮询 RemoDispBus 协议(接受连接、收帧、逐字节解析)
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:ui_net_sink_start() 已成功调用
|
||
* 函数说明: 1. 网络代次变化(重建)时关闭旧监听/客户端并重建
|
||
* 2. 无监听(启动失败)时重试启动
|
||
* 3. 无客户端时**先门控**(net_listener_armed,纯 RAM 读)再 accept,成功则复位状态机。
|
||
* 门控判据 = "监听器还在服役"(LISTENING 或 CLOSED+auto_relisten)而非
|
||
* "本轮必然有连接"——后者会让断连待重建窗口内永不进 net_accept,
|
||
* 把 CONNECT 事件未收到的补 accept 探测永久挡在门外(监听生命周期模块化)
|
||
* 4. 有客户端时**先门控**(net_can_read)再非阻塞 recv 逐字节喂入状态机;
|
||
* 门控判为"无数据"时仍每 RM_LIVENESS_DIV 轮无条件收一次(兜底)
|
||
* 5. 对端关闭/真错误时关闭客户端。**不再自行重建监听**(监听生命周期不变量 II):
|
||
* recv 的 EOF/错误分支已触发 net 层 auto_relisten,另有对账④兜底;
|
||
* 仅 g_listen 的 fd 真消失(net_get_sock 返 NULL)才置 -1 交回 2 重启
|
||
* ⚠ **门控只消除"无谓的往返",不消除"往返本身无上界"**:一旦真有活要干
|
||
* (accept / recv / 3204 B 的 LCDMEM 应答),仍是"入队 + 等 netTask 回执",
|
||
* 最坏 ≈4 s。宿主 = uiTask 主循环;"搬出 uiTask"已完整评估并**决定不做**
|
||
* (实测刷新最坏周期 = LVGL 帧周期地板,与网络无关),理由与残留暴露
|
||
* 见 docs/UI设计/RemoDispBus协议与接收设计说明.md §3
|
||
*/
|
||
void ui_net_sink_poll(void) {
|
||
int n;
|
||
|
||
/* 网络重建(代次变化):旧监听/客户端已随芯片复位失效,重建监听 */
|
||
if (net_get_generation() != g_gen) {
|
||
if (g_client >= 0 && g_client != g_listen) {
|
||
net_close(g_client);
|
||
}
|
||
if (g_listen >= 0) {
|
||
net_close(g_listen);
|
||
g_listen = -1;
|
||
}
|
||
sink_beat(); /* F3:重建前的一对 close 往返结束 */
|
||
g_client = -1;
|
||
rm_parser_reset();
|
||
s_send_fail_run = 0U; /* 网络重建:失败累计清零、解除推送阻断 */
|
||
s_sink_blocked = 0U;
|
||
s_liveness_ctr = 0U; /* 网络重建:兜底节拍重新起算 */
|
||
(void)ui_net_sink_start(g_port);
|
||
return;
|
||
}
|
||
|
||
if (g_listen < 0) {
|
||
/* 尚未启动或启动失败:重试(成功则 g_listen 更新) */
|
||
(void)ui_net_sink_start(g_port);
|
||
return;
|
||
}
|
||
|
||
/* 无客户端时尝试接受连接(监听生命周期 P1 不变量 II:门控从 net_can_accept〔只认
|
||
* ESTABLISHED〕换成 net_listener_armed〔LISTENING 或 CLOSED+auto_relisten〕)
|
||
* 原因:MCU 单连接模型下断连后 fd 会停在 CLOSED 等 relisten,旧门控在该窗口
|
||
* 内恒 0 → :7003 永不进 net_accept → net_lost_connect_probe 对该端口等于
|
||
* 从未运行(CONNECT 事件未收到时无第二信源);armed 放行后由 net_accept_locked
|
||
* 的状态门自行裁决(含 RX 有数据的 closed-but-readable 接受) */
|
||
if (g_client < 0) {
|
||
if (net_listener_armed(g_listen) == 0) {
|
||
/* 监听器已不在服役(fd 消失 / 非监听型)→ 置 -1 交回上方
|
||
* g_listen < 0 分支重建监听。改前靠"每轮都 accept"撞出 BADF 来暴露,
|
||
* 加了门控就必须显式兜这一条,否则端口永久不再接受新客户端 */
|
||
if (net_get_sock(g_listen) == NULL) {
|
||
g_listen = -1;
|
||
}
|
||
return;
|
||
}
|
||
int c = net_accept(g_listen, NULL, NULL);
|
||
sink_beat(); /* F3:accept 往返返回即喂(风暴期该往返实测 150ms 级) */
|
||
if (c >= 0) {
|
||
g_client = c;
|
||
rm_parser_reset();
|
||
s_send_fail_run = 0U; /* 新连接:失败累计清零、解除推送阻断 */
|
||
s_sink_blocked = 0U;
|
||
s_liveness_ctr = 0U; /* 新连接:兜底节拍重新起算 */
|
||
}
|
||
return;
|
||
}
|
||
|
||
/* 有客户端时非阻塞收帧(判活门控 + 兜底节拍;DONTWAIT 已转正
|
||
* 为具名 API net_recv_nb——uiTask 在 IWDG required_mask 内且有 10ms UI 推流
|
||
* 周期职责,禁止无限阻塞 recv,非阻塞轮询是义务):
|
||
* 无数据可读 → 多数轮次直接返回,省掉一次 recv 往返(该往返内部
|
||
* osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY),无上界);
|
||
* 但每 RM_LIVENESS_DIV 轮无条件收一次,防 RECV_OK 事件未收到把数据永久藏住 */
|
||
if (net_can_read(g_client) == 0) {
|
||
if (((++s_liveness_ctr) & (RM_LIVENESS_DIV - 1U)) != 0U) {
|
||
return;
|
||
}
|
||
} else {
|
||
s_liveness_ctr = 0U;
|
||
}
|
||
n = net_recv_nb(g_client, g_rx, (int)sizeof(g_rx));
|
||
sink_beat(); /* F3:recv 往返返回即喂(nb 不等数据,但队列排队照样等回执) */
|
||
if (n > 0) {
|
||
for (int i = 0; i < n; i++) {
|
||
rm_parser_feed((uint8_t)g_rx[i]);
|
||
}
|
||
} else if (n == 0) {
|
||
/* 对端关闭:单连接模型下监听通道与数据通道是同一个 Socket,net 层已在
|
||
* net_recv_locked 的 EOF 分支自动把该 Socket 放回 LISTEN;此处绝不能再
|
||
* net_close 它,否则监听一并被毁(后续连接被拒),且重建监听受 TRAP-NET-09 时序限制 */
|
||
if (g_client != g_listen) {
|
||
net_close(g_client); /* 多连接后端(PC 模拟器):数据通道独立,需显式关闭 */
|
||
}
|
||
g_client = -1;
|
||
rm_parser_reset();
|
||
/* 监听生命周期 P1 不变量 II:重建**不再**由本处自行 bind_listen。
|
||
* ① net_recv_locked 的 EOF 分支已触发 auto_relisten(D10 非阻塞重试);
|
||
* ② 对账④还会兜底补一次 relisten(TRAP-NET-17 实证事件面不是可靠唯一信源——
|
||
* E14 复核:非芯片丢事件——两层不可互替)。
|
||
* 本处再做一遍 = 第三个重建所有者,正是 §2.3 那 12 个所有者的同类。
|
||
* 仅当 fd 真消失(net_get_sock 返 NULL = 网络重建)才置 -1,交回上方
|
||
* 分支重新走 start → bind_listen */
|
||
if (net_get_sock(g_listen) == NULL) {
|
||
g_listen = -1; /* fd 真消失:交回主循环重试 */
|
||
}
|
||
} else {
|
||
/* n < 0:区分"暂无数据"(继续等待)与"真错误"(断开并视情况重建监听) */
|
||
if (net_get_errno() != NET_ERR_WOULDBLOCK) {
|
||
/* 监听生命周期 P1:加 `g_client != g_listen` 护栏,与上面 n==0 分支
|
||
* 的 :552 一致——MCU 单连接模型下 g_client 可能就是 g_listen 本身,
|
||
* 无护栏的 net_close 会直接关掉监听 fd(:563,与 :134 注释"严禁"
|
||
* 自相矛盾),端口随 TRAP-NET-09 沉降窗短死。PC 模拟器多连接后端下
|
||
* 两者恒不等,护栏不改变其行为 */
|
||
if (g_client != g_listen) {
|
||
net_close(g_client); /* 多连接后端(PC 模拟器):数据通道独立,需显式关闭 */
|
||
}
|
||
sink_beat(); /* F3:EOF/错误路径的 close 往返结束 */
|
||
g_client = -1;
|
||
rm_parser_reset();
|
||
/* 监听生命周期 P1 不变量 II:同 EOF 分支——不再自行 bind_listen,
|
||
* 重建交回 net 层(recv 的错误分支同样会走 relisten)。仅 fd 真消失
|
||
* 时置 -1 交回主循环走 start */
|
||
if (net_get_sock(g_listen) == NULL) {
|
||
g_listen = -1; /* fd 真消失:交回主循环重试 */
|
||
}
|
||
}
|
||
}
|
||
}
|