Files
STM32F4-Base/Drivers/BSP/UI/ui_net_sink.c
T
super_agent_bot 493fa6ba0e 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。
2026-11-02 22:19:58 +08:00

776 lines
36 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称: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 真消失:交回主循环重试 */
}
}
}
}