/* * 模块名称: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 单连接模型断线后无法重连的隐患) */ #include "ui_net_sink.h" #include #include "net_socket.h" #include "net_types.h" #include "ui_net_sink_backend.h" #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ #include "net_latency_test.h" /* M3 探针:prod_cycle_mcu(LCDMEM 处理净成本) */ /* 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 /* 屏幕尺寸与帧大小(160x160 1bpp = 3200 字节) */ #define UI_W 160 #define UI_H 160 #define UI_MEMSIZE (UI_W * UI_H / 8) #define RM_MAX_RX_PAYLOAD 256 /* 客户端→服务端 payload 上限,防长度字段损坏引发失步 */ #define RM_RX_BUF 64 /* 接收暂存区:整块 recv 后逐字节喂入解析状态机 */ static uint8_t g_tx[4 + 4 + UI_MEMSIZE + 1]; /* 发送组帧缓冲:[TAG][CMD][LEN][DATA][CRC] */ static uint8_t g_lcdmem_data[4 + UI_MEMSIZE]; /* LCDMEM 应答缓冲:4B 偏移 + 1bpp 位图 */ 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; /* 建立监听时的网络代次号(变化 = 网络已重建) */ /* * 函数功能:计算 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 长度 * 返回值:无 * 限定条件:g_tx 空间足够容纳 4+len+1 字节;g_client >= 0(已有连接) * 函数说明:帧格式 [TAG_DEVICE][CMD][LEN_H][LEN_LO][DATA][CRC];CRC 为 payload 异或; * 无连接时直接返回 */ static void remo_send(uint8_t cmd, const uint8_t *data, uint16_t len) { uint8_t *p; if (g_client < 0) { return; } 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); net_send(g_client, p, (int)(4 + len + 1), 0); } /* * 函数功能:回送 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_W); put_u16(init + 2, UI_H); put_u32(init + 4, UI_MEMSIZE); 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 帧,维持连接活性 */ 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) */ 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_MEMSIZE) == 0) { /* 应答数据段 = [偏移u32(回填请求原值)] + [1bpp 位图 3200B] */ memcpy(g_lcdmem_data, data, 4); remo_send(CMD_LCDMEM, g_lcdmem_data, (uint16_t)(4 + UI_MEMSIZE)); } #if defined(TEST_LATENCY_PROBE) lat_probe_add_seg("prod_cycle_mcu", lat_mark_us(&tm)); #endif } } break; } 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; 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. 无客户端时尝试 accept,成功则复位状态机 * 4. 有客户端时非阻塞 recv 逐字节喂入状态机 * 5. 对端关闭/真错误时关闭客户端;若监听 Socket 随客户端失效 * (MCU 单连接模型)则重建监听,等待重连 */ 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; } g_client = -1; rm_parser_reset(); (void)ui_net_sink_start(g_port); return; } if (g_listen < 0) { /* 尚未启动或启动失败:重试(成功则 g_listen 更新) */ (void)ui_net_sink_start(g_port); return; } /* 无客户端时尝试接受连接 */ if (g_client < 0) { int c = net_accept(g_listen, NULL, NULL); if (c >= 0) { g_client = c; rm_parser_reset(); } return; } /* 有客户端时非阻塞收帧,逐字节喂入解析状态机 */ n = net_recv(g_client, g_rx, (int)sizeof(g_rx), NET_MSG_DONTWAIT); 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 的 CLOSED 分支自动把该 Socket 放回 LISTEN;此处绝不能再 * net_close 它,否则监听一并被毁(后续连接被拒),且重建监听受 Trap 09 时序限制 */ if (g_client != g_listen) { net_close(g_client); /* 多连接后端(PC 模拟器):数据通道独立,需显式关闭 */ } g_client = -1; rm_parser_reset(); if (net_get_sock(g_listen) == NULL && bind_listen() < 0) { g_listen = -1; /* 重建失败:交回主循环重试,避免拿失效 fd 反复 accept */ } } else { /* n < 0:区分"暂无数据"(继续等待)与"真错误"(断开并视情况重建监听) */ if (net_get_errno() != NET_ERR_WOULDBLOCK) { net_close(g_client); g_client = -1; rm_parser_reset(); if (net_get_sock(g_listen) == NULL && bind_listen() < 0) { g_listen = -1; /* 重建失败:交回主循环重试 */ } } } }