From d1508105873fbd1efa2848ec8a855f1cf96ef19a Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 11 Sep 2026 21:25:02 +0800 Subject: [PATCH] =?UTF-8?q?ui=5Fnet=5Fsink=20=E6=8B=86=E5=88=86=E4=B8=BA?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=E5=B1=82=E4=B8=8E=E5=B9=B3=E5=8F=B0=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=EF=BC=8C=E6=96=B0=E5=A2=9E=20RemoDispBus=20=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 8 +- Drivers/BSP/NET/net_socket.h | 13 + Drivers/BSP/NET/net_types.h | 11 +- Drivers/BSP/UI/ui_net_sink.c | 382 ++++++--- Drivers/BSP/UI/ui_net_sink_backend.h | 47 ++ Drivers/BSP/UI/ui_net_sink_backend_mcu.c | 52 ++ MDK-ARM/STM32F407-Demo.uvprojx | 5 + README.md | 4 +- docs/RemoDispBus协议与接收设计说明.md | 300 +++++++ docs/UI 设计/LVGL模拟器部署与使用说明.md | 6 +- docs/UI 设计/UI_设计架构.md | 4 +- docs/网络配置说明.md | 2 +- simulator/CMakeLists.txt | 22 +- simulator/README.md | 17 +- .../sim}/net_cfg_store_sim.c | 0 simulator/sim/net_socket_sim.c | 786 ++++++++++++++++++ simulator/{ => sim}/sim_port.c | 0 simulator/{ => sim}/sim_port.h | 0 simulator/sim/ui_net_sink_backend_sim.c | 75 ++ simulator/ui_net_sink.c | 274 ------ 20 files changed, 1600 insertions(+), 408 deletions(-) create mode 100644 Drivers/BSP/UI/ui_net_sink_backend.h create mode 100644 Drivers/BSP/UI/ui_net_sink_backend_mcu.c create mode 100644 docs/RemoDispBus协议与接收设计说明.md rename {Drivers/BSP/NETCFG => simulator/sim}/net_cfg_store_sim.c (100%) create mode 100644 simulator/sim/net_socket_sim.c rename simulator/{ => sim}/sim_port.c (100%) rename simulator/{ => sim}/sim_port.h (100%) create mode 100644 simulator/sim/ui_net_sink_backend_sim.c delete mode 100644 simulator/ui_net_sink.c diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c83f71e7..600bb336 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,8 +62,8 @@ repos: Drivers/CMSIS/| Drivers/STM32F4xx_HAL_Driver/| Drivers/BSP/UI/| - Drivers/BSP/NET/lftpd/| - Drivers/BSP/dhara/| + Drivers/BSP/NET/lftpd/| + Drivers/BSP/dhara/| Lib/| Middlewares/| MDK-ARM/| @@ -86,8 +86,8 @@ repos: Drivers/CMSIS/| Drivers/STM32F4xx_HAL_Driver/| Drivers/BSP/UI/| - Drivers/BSP/NET/lftpd/| - Drivers/BSP/GD5F2GQ5UE/nand_ftl\.| + Drivers/BSP/NET/lftpd/| + Drivers/BSP/GD5F2GQ5UE/nand_ftl\.| Drivers/BSP/dhara/| Lib/| Middlewares/| diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index d4b2f0ff..67b586a4 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -16,9 +16,17 @@ extern "C" { #endif #include "net_types.h" + +/* + * OS 相关接口(FreeRTOS 消息队列 / 任务句柄 / netTask 内部 API)仅在启用 + * RTOS 的构建(MCU)编译;纯 PC 模拟器定义 NET_NO_OS 后只保留 BSD Socket 核心 API。 + */ +#ifndef NET_NO_OS #include "FreeRTOS.h" #include "task.h" +#endif /* NET_NO_OS */ +#ifndef NET_NO_OS /* * 消息队列类型定义 */ @@ -43,6 +51,7 @@ typedef struct { struct net_sockaddr_in addr; /* 连接地址 */ int addrlen; /* 地址长度 */ } net_msg_t; +#endif /* NET_NO_OS */ /* * 网络初始化接口 @@ -294,6 +303,7 @@ uint32_t net_htonl(uint32_t hostlong); */ uint32_t net_ntohl(uint32_t netlong); +#ifndef NET_NO_OS /* * 函数功能:处理消息队列中的请求(必须在 netTask 主循环中调用) * 限定条件:net_init() 已调用 @@ -301,6 +311,7 @@ uint32_t net_ntohl(uint32_t netlong); * 在 netTask 上下文中串行化执行所有 CH395F 操作 */ void net_process_messages(void); +#endif /* NET_NO_OS */ /* * 函数功能:获取 Socket 控制块指针(内部使用) @@ -319,6 +330,7 @@ net_sock_t *net_get_sock(int sockfd); */ int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen); +#ifndef NET_NO_OS /* * 内部 API(需在 netTask 上下文中调用,绕过消息队列) */ @@ -359,6 +371,7 @@ int net_close_sock(net_sock_t *p_sock); * 限定条件:必须在 netTask 上下文中调用 */ int net_listen_locked(int sockfd); +#endif /* NET_NO_OS */ #ifdef __cplusplus } diff --git a/Drivers/BSP/NET/net_types.h b/Drivers/BSP/NET/net_types.h index c02690e8..6e8b4994 100644 --- a/Drivers/BSP/NET/net_types.h +++ b/Drivers/BSP/NET/net_types.h @@ -75,15 +75,20 @@ struct net_sockaddr { char sa_data[14]; /* 地址数据 */ }; +/* + * IPv4 地址(镜像 BSD in_addr;用命名结构体保证 ARMCC / MSVC 双端兼容) + */ +struct net_in_addr { + uint32_t s_addr; /* IP 地址(网络字节序) */ +}; + /* * IPv4 套接字地址结构体 */ struct net_sockaddr_in { uint16_t sin_family; /* 地址族,必须为 AF_INET */ uint16_t sin_port; /* 端口号(网络字节序) */ - struct { - uint32_t s_addr; /* IP 地址(网络字节序) */ - } sin_addr; + struct net_in_addr sin_addr; /* IP 地址(网络字节序) */ char sin_zero[8]; /* 填充字节 */ }; diff --git a/Drivers/BSP/UI/ui_net_sink.c b/Drivers/BSP/UI/ui_net_sink.c index 062930d7..dcadce9d 100644 --- a/Drivers/BSP/UI/ui_net_sink.c +++ b/Drivers/BSP/UI/ui_net_sink.c @@ -1,67 +1,76 @@ /* - * 模块名称:UI Network Sink (RemoDispBus 服务端) - * 模块功能:实现 RemoDispBus 协议服务端,接收远端连接与导航按键,回送 1bpp 帧缓冲 - * 适用平台:STM32F407ZGTx + CH395F 以太网 + * 模块名称: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 "ui_port.h" + #include "net_socket.h" #include "net_types.h" +#include "ui_net_sink_backend.h" -/* ---------------- RemoDispBus 协议 ---------------- */ -#define TAG_CLIENT 0xAA -#define TAG_DEVICE 0xBB -#define CMD_KEEPLIVE 0 -#define CMD_INIT 1 -#define CMD_KEY 2 -#define CMD_LCDMEM 3 +/* 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) /* 3200 字节 */ +#define UI_MEMSIZE (UI_W * UI_H / 8) -/* 发送帧缓冲(最大:LCDMEM 应答 = 4 字节偏移 + 3200 帧 + 头4 + 校验1) */ -static uint8_t g_tx[4 + 4 + UI_MEMSIZE + 1]; -/* LCDMEM 应答数据区(4 字节大端偏移 + 整帧) */ -static uint8_t g_lcdmem_data[4 + UI_MEMSIZE]; -static uint8_t g_rx[64]; +#define RM_MAX_RX_PAYLOAD 256 /* 客户端→服务端 payload 上限,防长度字段损坏引发失步 */ +#define RM_RX_BUF 64 /* 接收暂存区:整块 recv 后逐字节喂入解析状态机 */ -static int g_listen = -1; -static int g_client = -1; -static uint16_t g_port; /* 监听端口(网络重建后重监听用) */ -static uint32_t g_gen; /* 建立监听时的网络代次号(变化 = 网络已重建) */ +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 数据段异或校验值 - * 入口参数:d - 数据首地址 const uint8_t * 非空 - * len - 数据长度 int >= 0 - * 返回值:校验值 uint8_t 0 - 255 + * 函数功能:计算 RemoDispBus 帧 payload 的 CRC 校验值 + * 入口参数:d - 待校验数据指针(len 为 0 时不访问) + * len - 数据长度(字节) + * 返回值:异或累加校验值 * 限定条件:无 - * 函数说明:逐字节异或,结果为单字节,与 DTU-HMI 协议保持一致 + * 函数说明:对全部数据字节做异或累加(协议约定的简单校验);len 为 0 时返回 0 */ static uint8_t remo_crc(const uint8_t *d, int len) { + int i; uint8_t c = 0; - for (int i = 0; i < len; i++) { + + for (i = 0; i < len; i++) { c ^= d[i]; } return c; } /* - * 函数功能:以大端序写入 16 位无符号整数 - * 入口参数:p - 目标缓冲区 uint8_t * 至少 2 字节 - * v - 待写入数值 uint16_t 0 - 65535 + * 函数功能:将 16 位值以大端序(高字节在前)写入缓冲区 + * 入口参数:p - 目标缓冲区(至少 2 字节);v - 待写入值 * 返回值:无 - * 限定条件:p 指向有效内存 - * 函数说明:高位在前、低位在后(网络字节序) + * 限定条件:p 指向至少 2 字节可写空间 + * 函数说明:网络协议多字节字段统一采用大端序传输 */ static void put_u16(uint8_t *p, uint16_t v) { @@ -70,12 +79,11 @@ static void put_u16(uint8_t *p, uint16_t v) } /* - * 函数功能:以大端序写入 32 位无符号整数 - * 入口参数:p - 目标缓冲区 uint8_t * 至少 4 字节 - * v - 待写入数值 uint32_t 0 - 0xFFFFFFFF + * 函数功能:将 32 位值以大端序(高字节在前)写入缓冲区 + * 入口参数:p - 目标缓冲区(至少 4 字节);v - 待写入值 * 返回值:无 - * 限定条件:p 指向有效内存 - * 函数说明:最高位在前,依次存放 4 字节(网络字节序) + * 限定条件:p 指向至少 4 字节可写空间 + * 函数说明:网络协议多字节字段统一采用大端序传输 */ static void put_u32(uint8_t *p, uint32_t v) { @@ -85,24 +93,23 @@ static void put_u32(uint8_t *p, uint32_t v) p[3] = (uint8_t)(v & 0xFF); } -/* 组包并发送一帧(tag = DEVICE) */ /* - * 函数功能:组包并发送一帧(设备 -> 客户端) - * 入口参数:cmd - 命令字 uint8_t CMD_* - * data - 数据段首地址 const uint8_t * 可为 NULL - * len - 数据段长度 uint16_t 0 - 65535 + * 函数功能:构造并发送一帧 RemoDispBus 响应(设备→客户端) + * 入口参数:cmd - 命令码;data - payload 指针(len 非 0 时须非空);len - payload 长度 * 返回值:无 - * 限定条件:g_client >= 0(已有连接) - * 函数说明:1. 帧格式 [TAG_DEVICE][cmd][len_hi][len_lo][data][crc] - * 2. 无连接时直接返回,不发送 - * 3. 阻塞发送,直至整帧写入 Socket + * 限定条件: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; } - uint8_t *p = g_tx; + + p = g_tx; p[0] = TAG_DEVICE; p[1] = cmd; p[2] = (uint8_t)(len >> 8); @@ -111,19 +118,20 @@ static void remo_send(uint8_t cmd, const uint8_t *data, uint16_t len) memcpy(p + 4, data, len); } p[4 + len] = remo_crc(data, len); - net_send(g_client, p, 4 + len + 1, 0); + net_send(g_client, p, (int)(4 + len + 1), 0); } /* - * 函数功能:发送 INIT 应答(上报分辨率与帧大小) + * 函数功能:回送 INIT 帧(告知客户端屏幕尺寸与帧缓冲大小) * 入口参数:无 * 返回值:无 * 限定条件:g_client >= 0 - * 函数说明:数据段为 [宽][高][帧字节数(4)],供客户端分配显示缓冲 + * 函数说明:payload 为 [宽u16][高u16][缓冲字节数u32],共 6 字节(大端) */ 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); @@ -131,30 +139,34 @@ static void remo_send_init(void) } /* - * 函数功能:解析一帧 RemoDispBus 请求并分发处理 - * 入口参数:f - 整帧数据首地址 const uint8_t * 非空 - * n - 帧字节数 int >= 0 + * 函数功能:处理一帧完整的远端命令(客户端→设备) + * 入口参数:f - 完整帧 [TAG][CMD][LEN][DATA][CRC];n - 帧总长度 * 返回值:无 - * 限定条件:f 指向完整帧缓冲 - * 函数说明:1. 校验长度与 CRC,标签必须为客户端 TAG - * 2. 按命令字分发:KEEPLIVE/INIT 应答,KEY 驱动菜单,LCDMEM 回送帧 - * 3. 校验失败或格式错误时静默丢弃 + * 限定条件:调用前帧已通过 CRC 与 TAG 校验 + * 函数说明:按命令分发:KEEPLIVE 回心跳 / INIT 回尺寸 / KEY 注入按键 / + * LCDMEM 回送 1bpp 帧缓冲(经平台后端取帧);非法情况静默丢弃 */ static void remo_handle(const uint8_t *f, int n) { - if (n < 6) { + uint8_t tag; + uint8_t cmd; + uint16_t len; + const uint8_t *data; + uint8_t crc; + + if (n < 5) { return; } - uint8_t tag = f[0]; - uint8_t cmd = f[1]; - uint16_t len = ((uint16_t)f[2] << 8) | f[3]; - const uint8_t *data = f + 4; + tag = f[0]; + cmd = f[1]; + len = (uint16_t)(((uint16_t)f[2] << 8) | (uint16_t)f[3]); + data = f + 4; if ((int)(4 + len + 1) > n) { - return; /* 帧不完整 */ + return; } - uint8_t crc = f[4 + len]; + crc = f[4 + len]; if (crc != remo_crc(data, len)) { - return; /* 校验失败,丢弃 */ + return; } if (tag != TAG_CLIENT) { return; @@ -169,7 +181,7 @@ static void remo_handle(const uint8_t *f, int n) break; case CMD_KEY: if (len >= 1) { - ui_port_queue_key(data[0]); + ui_net_sink_inject_key(data[0]); } remo_send(CMD_KEY, NULL, 0); break; @@ -177,18 +189,18 @@ static void remo_handle(const uint8_t *f, int n) if (len < 4) { break; } - uint32_t off = ((uint32_t)data[0] << 24) | - ((uint32_t)data[1] << 16) | - ((uint32_t)data[2] << 8) | - (uint32_t)data[3]; - if (off == 0) { - uint8_t *fb = ui_port_get_framebuffer(); - g_lcdmem_data[0] = data[0]; - g_lcdmem_data[1] = data[1]; - g_lcdmem_data[2] = data[2]; - g_lcdmem_data[3] = data[3]; - memcpy(g_lcdmem_data + 4, fb, UI_MEMSIZE); - remo_send(CMD_LCDMEM, g_lcdmem_data, 4 + UI_MEMSIZE); + { + uint32_t off = ((uint32_t)data[0] << 24) | + ((uint32_t)data[1] << 16) | + ((uint32_t)data[2] << 8) | + ((uint32_t)data[3]); + if (off == 0) { + /* 整帧请求:经平台后端取 1bpp 位图后回送(失败则静默丢弃) */ + if (ui_net_sink_get_lcdmem(g_lcdmem_data + 4, UI_MEMSIZE) == 0) { + memcpy(g_lcdmem_data, data, 4); + remo_send(CMD_LCDMEM, g_lcdmem_data, (uint16_t)(4 + UI_MEMSIZE)); + } + } } break; } @@ -197,84 +209,238 @@ static void remo_handle(const uint8_t *f, int n) } } +/* ================= 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) */ + /* - * 函数功能:启动 UI 专用 TCP 监听 Socket - * 入口参数:port - 监听端口号 uint16_t 1 - 65535 - * 返回值:0 - 成功,其他 - 失败(socket 创建/绑定/监听失败) - * 限定条件:net_init() 已完成,网络子系统就绪 - * 函数说明:创建流式 Socket 并绑定端口,进入监听等待连接 + * 函数功能:复位接收解析状态机到初始(等待帧头)状态 + * 入口参数:无 + * 返回值:无 + * 限定条件:无 + * 函数说明:新建连接、连接断开或长度越界失步时调用,清除半帧残留 */ -int ui_net_sink_start(uint16_t port) +static void rm_parser_reset(void) { - g_port = port; - g_listen = net_socket(NET_AF_INET, NET_SOCK_STREAM); - if (g_listen < 0) { + 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; } - struct net_sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); addr.sin_family = NET_AF_INET; - addr.sin_port = net_htons(port); + addr.sin_port = net_htons(g_port); addr.sin_addr.s_addr = NET_INADDR_ANY; - if (net_bind(g_listen, (struct net_sockaddr *)&addr, sizeof(addr)) < 0) { - net_close(g_listen); - g_listen = -1; + if (net_bind(ls, (struct net_sockaddr *)&addr, (int)sizeof(addr)) < 0) { + net_close(ls); return -1; } - if (net_listen(g_listen) < 0) { - net_close(g_listen); - g_listen = -1; + if (net_listen(ls) < 0) { + net_close(ls); return -1; } - g_client = -1; - g_gen = net_get_generation(); /* 成功才记录代次;失败保持旧值以便下轮重试 */ + + g_listen = ls; return 0; } /* - * 函数功能:非阻塞轮询 RemoDispBus 协议 + * 函数功能:启动 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. 无客户端时尝试接受连接(不主动上报 INIT,由客户端按需请求) - * 2. 有客户端时非阻塞收帧并解析 - * 3. 连接关闭或异常时复位,等待重监听,不阻塞界面刷新 - * 4. 网络重建(net_init 代次变化)时关闭旧监听并重建(IP 变更后自愈) + * 函数说明:1. 网络代次变化(重建)时关闭旧监听/客户端并重建 + * 2. 无监听(启动失败)时重试启动 + * 3. 无客户端时尝试 accept,成功则复位状态机 + * 4. 有客户端时非阻塞 recv 逐字节喂入状态机 + * 5. 对端关闭/真错误时关闭客户端;若监听 Socket 随客户端失效 + * (MCU 单连接模型)则重建监听,等待重连 */ void ui_net_sink_poll(void) { - /* 网络重建(net_init 代次变化):旧监听/客户端已随芯片复位失效,重建监听 */ + int n; + int i; + int c; + + /* 网络重建(代次变化):旧监听/客户端已随芯片复位失效,重建监听 */ 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; - (void)ui_net_sink_start(g_port); /* 失败则下轮再试(g_gen 未更新) */ + 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); + c = net_accept(g_listen, NULL, NULL); if (c >= 0) { g_client = c; + rm_parser_reset(); } return; } - int n = net_recv(g_client, g_rx, sizeof(g_rx), NET_MSG_DONTWAIT); + /* 有客户端时非阻塞收帧,逐字节喂入解析状态机 */ + n = net_recv(g_client, g_rx, (int)sizeof(g_rx), NET_MSG_DONTWAIT); if (n > 0) { - remo_handle(g_rx, n); + for (i = 0; i < n; i++) { + rm_parser_feed((uint8_t)g_rx[i]); + } } else if (n == 0) { - g_client = -1; /* 对端关闭:等待下次 accept 自动重监听,不释放控制块 */ + /* 对端关闭:关闭客户端,必要时重建监听(单连接模型下监听随客户端失效) */ + net_close(g_client); + g_client = -1; + rm_parser_reset(); + if (net_get_sock(g_listen) == NULL) { + (void)bind_listen(); + } } 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) { + (void)bind_listen(); + } } } } diff --git a/Drivers/BSP/UI/ui_net_sink_backend.h b/Drivers/BSP/UI/ui_net_sink_backend.h new file mode 100644 index 00000000..a2ca901f --- /dev/null +++ b/Drivers/BSP/UI/ui_net_sink_backend.h @@ -0,0 +1,47 @@ +/* + * 模块名称:UI Network Sink Backend (平台后端接口) + * 模块功能:抽象 RemoDispBus 服务端的平台相关操作(LCDMEM 数据源 / 远端按键注入), + * 使 MCU 端与 PC 模拟器共用同一份 ui_net_sink.c 协议实现 + * 适用平台:跨平台接口(MCU / PC 模拟器各提供一份实现) + * 作者:王建锋 + * 创建日期:2026-09-10 + * 修改记录: + * 2026-09-10 王建锋 创建初始版本(RemoDispBus 双端共用协议实现的平台抽象) + */ +#ifndef UI_NET_SINK_BACKEND_H +#define UI_NET_SINK_BACKEND_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 1bpp 帧缓冲尺寸(160x160 / 8 = 3200 字节,须与 ui_net_sink.c 的 UI_MEMSIZE 一致) */ +#define UI_NET_SINK_MONO_SIZE 3200 + +/* + * 函数功能:读取当前 1bpp 屏幕位图(LCDMEM 应答的数据源) + * 入口参数:out - 输出缓冲(调用方分配,≥ UI_NET_SINK_MONO_SIZE 字节) + * out_size - out 容量(字节) + * 返回值:0 - 成功,-1 - 失败 + * 限定条件:显示子系统已初始化 + * 函数说明:MCU 端从 LVGL 帧缓冲拷贝;模拟器端读回 SDL 实际呈现像素再转 1bpp + */ +int ui_net_sink_get_lcdmem(uint8_t *out, uint32_t out_size); + +/* + * 函数功能:向 UI 注入一个远端按键 + * 入口参数:remo_key - 远端按键码(ui_port.h 的 UI_KEY_* 之一) + * 返回值:无 + * 限定条件:输入子系统已初始化 + * 函数说明:MCU 端经 ui_port_queue_key 缓存、由 keypad 回调上报; + * 模拟器端映射为 LV_KEY_* 后经 lv_group_send_data 注入 + */ +void ui_net_sink_inject_key(uint8_t remo_key); + +#ifdef __cplusplus +} +#endif + +#endif /* UI_NET_SINK_BACKEND_H */ diff --git a/Drivers/BSP/UI/ui_net_sink_backend_mcu.c b/Drivers/BSP/UI/ui_net_sink_backend_mcu.c new file mode 100644 index 00000000..92a86b59 --- /dev/null +++ b/Drivers/BSP/UI/ui_net_sink_backend_mcu.c @@ -0,0 +1,52 @@ +/* + * 模块名称:UI Network Sink Backend (MCU 实现) + * 模块功能:RemoDispBus 服务端在 MCU 端的平台相关操作—— + * 读取 LVGL 帧缓冲(LCDMEM 数据源)与经 ui_port 注入远端按键 + * 适用平台:STM32F407ZGTx + * 作者:王建锋 + * 创建日期:2026-09-10 + * 修改记录: + * 2026-09-10 王建锋 创建初始版本(双端共用 ui_net_sink 的平台后端) + */ +#include "ui_net_sink_backend.h" + +#include + +#include "ui_port.h" + +/* + * 函数功能:读取 LVGL 帧缓冲为 1bpp 位图(LCDMEM 数据源) + * 入口参数:out - 输出缓冲(≥ UI_NET_SINK_MONO_SIZE 字节) + * out_size - out 容量(字节) + * 返回值:0 - 成功,-1 - 参数或帧缓冲无效 + * 限定条件:ui_port_init() 已成功 + * 函数说明:帧缓冲本就是 1bpp(布局与协议一致),直接整帧拷贝 + */ +int ui_net_sink_get_lcdmem(uint8_t *out, uint32_t out_size) +{ + const uint8_t *fb; + + if (out == NULL || out_size < UI_NET_SINK_MONO_SIZE) { + return -1; + } + + fb = ui_port_get_framebuffer(); + if (fb == NULL) { + return -1; + } + + memcpy(out, fb, UI_NET_SINK_MONO_SIZE); + return 0; +} + +/* + * 函数功能:向 UI 输入队列注入一个远端按键 + * 入口参数:remo_key - 远端按键码(UI_KEY_* 之一) + * 返回值:无 + * 限定条件:无 + * 函数说明:缓存后由 keypad 读取回调在下次轮询上报(键码映射在 ui_port 内完成) + */ +void ui_net_sink_inject_key(uint8_t remo_key) +{ + ui_port_queue_key(remo_key); +} diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index ac493a20..ce3db698 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -3178,6 +3178,11 @@ 1 ..\Drivers\BSP\UI\ui_net_sink.c + + ui_net_sink_backend_mcu.c + 1 + ..\Drivers\BSP\UI\ui_net_sink_backend_mcu.c + ui_font_cn_12.c 1 diff --git a/README.md b/README.md index a75810b3..a249738f 100644 --- a/README.md +++ b/README.md @@ -161,13 +161,15 @@ STM32F4-Base/ │ ├── 芯片手册/ # 硬件数据手册 │ ├── 测试开发标准流程.md # 测试开发标准流程 │ ├── 测试文档模板规范.md # 测试文档模板规范 +│ ├── 图形绘制流程.md # 文档配图绘制流程(SVG 生成器 / Mermaid 选型 / 约定 / 检查清单) │ └── *.md # 存储架构 / 墙钟时间 / 启动顺序等(UI 相关文档在 UI 设计/) ├── scripts/ # pre-commit 本地钩子脚本 │ ├── check_coding_standard.py # 代码规范机械检查 │ └── check_mojibake.py # U+FFFD 乱码检查 ├── simulator/ # LVGL PC 模拟器(CMake + SDL2,与 MCU 共用 UI 源码) │ ├── CMakeLists.txt # 模拟器构建脚本 -│ ├── main.c / sim_port.c/h # 模拟器入口与移植层 +│ ├── main.c # 模拟器入口(SDL → LVGL → 菜单 → 主循环) +│ ├── sim/ # 平台层(sim_port 显示/输入、net_socket_sim、推流/网口配置 backend) │ ├── lv_conf.h # LVGL 配置(1bpp + SDL) │ ├── i1_to_png.py # 截图 I1(1bpp) → PNG 转换工具 │ ├── tests/ # 无头回归脚本 + 黄金参考(菜单像素回归 / 内容页黄金参考) diff --git a/docs/RemoDispBus协议与接收设计说明.md b/docs/RemoDispBus协议与接收设计说明.md new file mode 100644 index 00000000..7b8c15d2 --- /dev/null +++ b/docs/RemoDispBus协议与接收设计说明.md @@ -0,0 +1,300 @@ +# RemoDispBus 协议与接收设计说明 + +> 版本:v1.0 日期:2026-09-10 +> 适用工程:STM32F4-Base(STM32F407ZGTx + CH395F 以太网 / LVGL UI) +> 范围:UI 远程显示(RemoDispBus)的**协议设计**与**接收解析设计**—— +> 帧格式、字段语义、命令集、交互时序、屏幕位图格式,以及服务端逐字节状态机解析的实现。 +> 对应模块:`Drivers/BSP/UI/ui_net_sink.c`(MCU 与模拟器**共用同一份**源码,协议与解析一致); +> 平台差异仅在网络后端(MCU `net_socket.c`/CH395F vs 模拟器 `net_socket_sim.c`/Winsock) +> 与 LCDMEM/按键注入的平台 backend(`ui_net_sink_backend_mcu.c` / `ui_net_sink_backend_sim.c`)。 + +--- + +## 0. 速览 + +| 项目 | 内容 | +|---|---| +| **协议名** | RemoDispBus(UI 远程显示总线) | +| **用途** | 远端查看设备 UI 画面 + 远端导航按键 | +| **传输** | TCP,端口 **7003**(须与 DTU-RemoteLCD 客户端默认端口一致) | +| **连接模型** | 单连接(服务端 `backlog=1`,同一时刻一个客户端) | +| **帧格式** | `[TAG(1)][CMD(1)][LEN_H(1)][LEN_LO(1)][DATA(n)][CRC(1)]` | +| **方向标识** | TAG `0xAA`=客户端→服务端,`0xBB`=服务端→客户端 | +| **命令集** | KEEPLIVE(0) / INIT(1) / KEY(2) / LCDMEM(3) | +| **校验** | CRC = 数据段逐字节**异或**(取低 8 位;不含 TAG/CMD/LEN) | +| **字节序** | 多字节字段**大端**(网络字节序) | +| **最短帧** | 5 字节(LEN=0 时) | +| **接收方式** | 逐字节有限状态机(TAG 定界 + 长度定长 + CRC 校验,自动重同步) | + +--- + +## 1. 协议设计 + +### 1.1 定位与角色 + +RemoDispBus 让远端(PC 上的 DTU-RemoteLCD 客户端 / 浏览器页面)实时查看设备 UI 的 1bpp 画面,并下发导航按键。角色: + +| 角色 | 进程 | 说明 | +|---|---|---| +| **服务端** | `ui_net_sink`(设备 / 模拟器) | 监听 7003,收命令、回送帧缓冲 | +| **客户端** | DTU-RemoteLCD(PC) | 连接服务端,拉取画面、下发按键 | + +请求—响应模型:客户端发请求帧(TAG `0xAA`),服务端回应答帧(TAG `0xBB`)。 + +### 1.2 帧格式 + +| 偏移 | 字段 | 大小(B) | 说明 | +|---|---|---|---| +| 0 | `TAG` | 1 | 方向标识:`0xAA` 客户端帧 / `0xBB` 服务端帧 | +| 1 | `CMD` | 1 | 命令码(见 §1.5) | +| 2 | `LEN_H` | 1 | 数据段长度高字节(大端) | +| 3 | `LEN_LO` | 1 | 数据段长度低字节 | +| 4 | `DATA` | `LEN` | 数据段(命令相关,见 §1.5) | +| 4+LEN | `CRC` | 1 | 数据段逐字节异或(低 8 位) | + +``` +┌───────┬───────┬─────────┬─────────┬───────────┬───────┐ +│ TAG │ CMD │ LEN_H │ LEN_L │ DATA │ CRC │ +├───────┼───────┼─────────┼─────────┼───────────┼───────┤ +│ 1 B │ 1 B │ 1 B │ 1 B │ LEN B │ 1 B │ +└───────┴───────┴─────────┴─────────┴───────────┴───────┘ +``` + +- 整帧长度 = `4 + LEN + 1`;`LEN=0` 时最短为 5 字节。 +- `LEN` 为 16 位大端,理论最大 65535 数据字节。 + +**图形版**(SVG 矢量,字段宽 ∝ 字节数;由 `tools/arch_diagram/gen_frame_svg.py` 生成,浏览器打开可无损缩放): + +![RemoDispBus 帧格式](images/RemoDispBus帧格式.svg) + +### 1.3 字段语义 + +- **TAG(方向)**:`0xAA` = 客户端→服务端(服务端只接受此标签的帧);`0xBB` = 服务端→客户端。TAG 标识**方向**,不承载命令。 +- **CMD(命令)**:同一命令在两个方向复用同一编码,靠 TAG 区分方向(见 §1.5)。 +- **CRC(校验)**:`CRC = DATA[0] ^ DATA[1] ^ … ^ DATA[LEN-1]`(低 8 位)。**仅覆盖数据段**,不含 TAG/CMD/LEN;`LEN=0` 时 CRC 为 `0`。与 DTU-HMI 协议一致。 + +### 1.4 字节序 + +多字节字段一律**大端**(网络字节序):`LEN`(u16)、INIT 应答的宽/高(u16)与帧大小(u32)、LCDMEM 的偏移(u32)。 + +### 1.5 命令集与数据段 + +| CMD | 名称 | 请求数据段(客户端→服务端) | 应答数据段(服务端→客户端) | 说明 | +|---|---|---|---|---| +| 0 | KEEPLIVE | 无(LEN=0) | 无(LEN=0) | 保活/心跳,服务端原样回显 | +| 1 | INIT | 无(LEN=0,查询) | `[宽u16][高u16][帧字节数u32]`(6B) | 客户端查询屏幕几何与显存大小 | +| 2 | KEY | `[按键码u8]`(1B) | 无(LEN=0,确认) | 远端导航按键,服务端注入 UI | +| 3 | LCDMEM | `[起始偏移u32]`(4B) | `[偏移u32][1bpp位图 3200B]`(3204B) | 读取显存(画面);偏移 0 = 整帧 | + +> **按键码**(`KEY` 数据段,与 `ui_port.h` 一致):`UP=0x02`、`DOWN=0x40`、`LEFT=0x10`、`RIGHT=0x08`、`ENTER=0x20`、`ESC=0x01`。 +> **注意**:客户端(如 Python 参考实现)可不使用 `INIT`(直接按 160×160 处理);协议保留该命令供需查询几何的客户端使用。 + +### 1.6 屏幕位图格式(LCDMEM 数据段) + +- **1bpp** 单色,**MSB 优先**(每字节 8 像素,bit7 为该字节第一个像素)。 +- 像素 (x,y) 定位:`byte_i = (y*W + x) / 8`,`bit_i = 7 - (x % 8)`。 +- 位值 `1` = 白(255),`0` = 黑(0)。 +- 160×160 → `160*160/8 = 3200` 字节。 + +### 1.7 典型交互时序 + +```mermaid +sequenceDiagram + participant C as 客户端 (DTU-RemoteLCD) + participant S as 服务端 (ui_net_sink) + C->>S: TCP 连接 :7003 + opt 查询屏幕参数(可选) + C->>S: INIT 请求 (0xAA, CMD=1, len=0) + S-->>C: INIT 应答 (0xBB, CMD=1, 宽/高/帧大小) + end + loop 屏幕刷新(约 100ms) + C->>S: LCDMEM 请求 (0xAA, CMD=3, 偏移=0) + S-->>C: LCDMEM 应答 (0xBB, CMD=3, 偏移+位图 3200B) + end + opt 远端按键 + C->>S: KEY 请求 (0xAA, CMD=2, 按键码) + S-->>C: KEY 应答 (0xBB, CMD=2, len=0) + end + opt 显式保活 + C->>S: KEEPLIVE (0xAA, CMD=0) + S-->>C: KEEPLIVE (0xBB, CMD=0) + end +``` + +> 连接后服务端**不主动上报 INIT**(避免污染客户端首帧读取);由客户端按需请求(`INIT`)或按 LCDMEM 轮询拉取。 + +--- + +## 2. 接收解析设计(逐字节状态机) + +### 2.1 设计动机 + +TCP 是**字节流**,不保留消息边界:一帧可能被拆到多次 `recv()`,一次 `recv()` 也可能带多帧或前导噪声。整块解析(假设"一次 recv = 一完整帧")在这种流上不可靠。因此服务端采用**逐字节有限状态机**: + +| 关注点 | 整块解析 | 逐字节状态机 | +|---|---|---| +| 帧跨多次 recv | ✗ 会解析半帧 | ✓ 状态跨 recv 保持 | +| 一次 recv 多帧(粘包) | ✗ 只处理首帧 | ✓ 顺序处理每帧 | +| 前导噪声/失步 | ✗ 从头按帧头假设 | ✓ 扫描到 0xAA 重新同步 | +| 长度字段损坏 | ✗ 按错误长度读飞 | ✓ 越界即复位 | +| 内存 | 需容纳整帧 | 小块暂存 + 单帧重组缓冲 | + +### 2.2 状态机 + +6 个状态,逐字节推进: + +```mermaid +stateDiagram-v2 + [*] --> TAG + TAG --> CMD : 0xAA + TAG --> TAG : 其他字节(丢弃,重同步) + CMD --> LEN_H : 任意字节 + LEN_H --> LEN_L : 任意字节 + LEN_L --> DATA : 0 < plen <= 256 + LEN_L --> CRC : plen == 0 + LEN_L --> TAG : plen > 256 (失步复位) + DATA --> DATA : 未收满 + DATA --> CRC : 收满 plen + CRC --> TAG : 校验通过→分发 / 校验错→丢弃 +``` + +| 状态 | 含义 | 收到字节动作 | 迁移 | +|---|---|---|---| +| `TAG` | 等帧头 | 仅收 `0xAA`,否则丢弃 | →`CMD`(命中)/ 停留(未命中) | +| `CMD` | 等命令码 | 存入帧 | →`LEN_H` | +| `LEN_H` | 等长度高字节 | 存入帧 | →`LEN_L` | +| `LEN_L` | 等长度低字节 | 存入帧,算 `plen` | →`DATA` / `CRC` / `TAG`(越界) | +| `DATA` | 等数据段 | 存入帧,计数 | 收满 → `CRC` | +| `CRC` | 等校验字节 | 存入帧,校验 | →`TAG`(分发或丢弃) | + +### 2.3 各状态处理要点 + +- **TAG**:只认 `TAG_CLIENT(0xAA)`。非帧头字节一律丢弃并停留在 `TAG`——这是**重同步**的核心:任意噪声后,下一个真帧头 `0xAA` 即恢复。 +- **LEN_L**:`plen = (LEN_H<<8) | LEN_LO`。若 `plen > RM_MAX_RX_PAYLOAD(256)` 判为**长度损坏/失步**,立即复位回 `TAG`(防止按错误长度"读飞"吞掉后续帧);否则进入 `DATA`(`plen>0`)或 `CRC`(`plen==0`)。 +- **DATA**:逐字节存入并按 `didx` 计数;`didx` 达到 `plen` 转 `CRC`。**数据段内的 `0xAA` 只当数据**(长度定界保证不误判为帧头)。 +- **CRC**:存入校验字节后,重算 `XOR(数据段)` 与收到的 CRC 比较;**相等才回调 `remo_handle` 分发**,随后复位回 `TAG`(半帧/坏帧均丢弃)。 + +### 2.4 重同步与健壮性 + +| 机制 | 作用 | +|---|---| +| TAG 门控 | 非 `0xAA` 字节在 `TAG` 态丢弃,噪声后自动重新锁定帧头 | +| 长度越界复位 | `plen` 超上限 → 判损坏,丢弃半帧回 `TAG`,不被坏长度带偏 | +| CRC 校验 | 只有校验通过的整帧才分发,坏帧静默丢弃 | +| 长度定界 | 数据段内的 `0xAA` 不误判为帧头,避免假重同步 | + +### 2.5 状态持久化(跨 recv) + +解析器状态(`g_rm`:当前状态 + 帧缓冲 + `plen`/`didx`)是**全局常驻**,跨 `poll()` 调用保持——一帧可跨多次 `recv()` 收齐。**复位点**:新连接 `accept`、连接断开、长度越界失步。 + +### 2.6 缓冲设计 + +| 缓冲 | 大小 | 用途 | +|---|---|---| +| `g_rx[RM_RX_BUF]` | 64 B | `net_recv()` 暂存区(每次 poll 的到达字节,喂入后即弃) | +| `g_rm.frame[]` | 4+256+1=261 B | 帧重组缓冲(任一时刻只装一帧,分发后复位) | +| `RM_MAX_RX_PAYLOAD` | 256 | 接收数据段上限(客户端命令帧都很小,此为防损坏护栏 + 定缓冲尺寸) | + +> 客户端→服务端 的命令帧数据段都很小(KEY=1、LCDMEM 请求=4、其余=0),256 上限有充足余量,同时把重组缓冲压到最小。 + +### 2.7 与轮询循环集成 + +```c +int n = net_recv(g_client, g_rx, sizeof(g_rx), NET_MSG_DONTWAIT); /* 非阻塞 */ +if (n > 0) { + for (int i = 0; i < n; i++) { + rm_parser_feed(g_rx[i]); /* 逐字节喂入状态机 */ + } +} +``` + +全程非阻塞,不拖慢 LVGL 刷新;`recv` 无数据(`WOULDBLOCK`)时直接返回。 + +### 2.8 边界情况 + +| 场景 | 状态机行为 | +|---|---| +| 一帧拆到多次 recv | 状态跨 recv 保持,收齐后分发 | +| 一次 recv 含多帧 | 顺序解析、逐帧分发 | +| 前导噪声/垃圾字节 | `TAG` 态丢弃,直到下一个 `0xAA` | +| 数据段内含 `0xAA` | 当数据处理,不误判帧头 | +| 长度字段损坏(超大) | 判失步,复位回 `TAG` 重新扫描 | +| CRC 校验失败 | 整帧丢弃,复位回 `TAG` | +| `LEN=0` 的空数据段 | 跳过 `DATA` 态,期望 CRC=0 | + +--- + +## 3. 双端共用实现 + +协议两端一致(线格式相同,可互通),且**服务端实现已统一**:MCU 与模拟器编译**同一份** +`Drivers/BSP/UI/ui_net_sink.c`(§2 逐字节状态机 + `remo_handle` 分发)。平台差异被隔离到两处 backend: + +| 关注点 | MCU(Keil / CH395F) | 模拟器(CMake / Winsock) | 说明 | +|---|---|---|---| +| 帧收发 | `net_socket.c`(CH395F 中断 + 消息队列) | `net_socket_sim.c`(Winsock 非阻塞) | 同一套 `net_*` BSD API(`socket`/`bind`/`listen`/`accept`/`recv`/`send`/`close`),仅底层不同;模拟器以 `NET_NO_OS` 剥离 FreeRTOS 部分 | +| LCDMEM 取帧 | `ui_net_sink_backend_mcu.c` → `ui_port_get_framebuffer()` | `ui_net_sink_backend_sim.c` → `sim_port_get_screen_mono()` | 同一 `ui_net_sink_get_lcdmem()` 接口,各端提供 1bpp 帧缓冲来源 | +| KEY 注入 | `ui_net_sink_backend_mcu.c` → `ui_port_queue_key()` | `ui_net_sink_backend_sim.c` → `lv_group_send_data()` | 同一 `ui_net_sink_inject_key()` 接口,各端注入 UI | + +要点: + +1. **单一解析实现**:两端均用 §2 的逐字节状态机(`rm_parser_feed`),对"帧跨 recv / 粘包 / 前导噪声"统一具备重同步能力,消除旧 MCU"整块解析"的脆弱性。 +2. **最小帧长判据统一**:`remo_handle` 以 `n < 5` 拦截(最短合法帧 5 字节),两端一致——旧 MCU `n < 6` 会丢 5 字节 KEEPLIVE/INIT 帧的隐患已随统一消除。 +3. **断线重连统一**:`ui_net_sink_poll` 在客户端断开后用 `net_get_sock(g_listen) == NULL` 判监听是否失效(MCU 单连接模型下监听随客户端一起失效),失效即 `bind_listen()` 重建——两端一致,并修复了 MCU 单连接模型断线后无法重连的隐患。 +4. **协议兼容**:线格式与命令语义不变,任一端都能与 DTU-RemoteLCD 参考客户端互通;双端行为一致已由 e2e 回归(`simulator/tests/check_remo_sink.py`,0/25600 位差异)与双端构建(Keil + CMake 均 0 错误 0 警告)验证。 + +--- + +## 4. 关键常量与文件索引 + +| 常量 / 文件 | 值 / 职责 | +|---|---| +| `UI_NET_SINK_PORT` | `7003`(`ui_net_sink.h`,须与客户端默认端口一致) | +| `UI_REMO_TAG_CLIENT` / `UI_REMO_TAG_DEVICE` | `0xAA` / `0xBB` | +| `UI_REMO_CMD_KEEP / INIT / KEY / LCDMEM` | `0` / `1` / `2` / `3` | +| `RM_MAX_RX_PAYLOAD` | `256`(接收数据段上限,防长度字段损坏引发失步) | +| `RM_RX_BUF` | `64`(`net_recv` 暂存区) | +| `Drivers/BSP/UI/ui_net_sink.c` | **双端共用服务端**(§2 逐字节状态机;MCU/CH395F 与模拟器/Winsock 共同编译) | +| `Drivers/BSP/UI/ui_net_sink.h` | 共享头:协议常量 + `ui_net_sink_start`/`poll` 声明 | +| `Drivers/BSP/UI/ui_net_sink_backend.h` | 平台 backend 接口:`ui_net_sink_get_lcdmem()` / `ui_net_sink_inject_key()` | +| `Drivers/BSP/UI/ui_net_sink_backend_mcu.c` | MCU 实现:`ui_port_get_framebuffer()`(LCDMEM)/ `ui_port_queue_key()`(KEY) | +| `simulator/sim/ui_net_sink_backend_sim.c` | 模拟器实现:`sim_port_get_screen_mono()`(LCDMEM)/ `lv_group_send_data()`(KEY) | +| `simulator/sim/net_socket_sim.c` | 模拟器网络后端(Winsock 实现 `net_*` BSD API,`NET_NO_OS` 剥离 FreeRTOS) | +| `Drivers/BSP/NET/net_socket.c` | MCU 网络后端(CH395F 中断 + 消息队列实现 `net_*`) | +| DTU-RemoteLCD `remo_disp_server.py` | 参考客户端(Flask + WebSocket,协议编解码) | + +**对外 API**(`ui_net_sink.h`):`ui_net_sink_start(port)` / `ui_net_sink_poll()`(无 `stop`;监听随网络代次变化 / 客户端断线自动重建)。 + +--- + +## 5. 陷阱 + +1. **CRC 只覆盖数据段**:不含 TAG/CMD/LEN;`LEN=0` 时 CRC 恒为 `0`。收发两端必须一致,否则校验必败。 +2. **TAG 是方向标识**:服务端只接受 `0xAA`(客户端帧);收到 `0xBB` 视为非法(状态机在 `TAG` 态丢弃)。 +3. **大端字节序**:`LEN` 及 payload 内 u16/u32 均大端;MCU(小端核)收发须显式转换(`put_u16`/`put_u32`)。 +4. **位图 MSB 优先**:`bit_i = 7 - (x%8)`;搞反则画面左右镜像/错位。 +5. **连接后不主动发 INIT**:避免污染客户端首帧读取(客户端按 LCDMEM 轮询或显式 INIT 获取)。 +6. **平台须齐备两个后端**:`ui_net_sink.c` 依赖 `net_*` BSD Socket API 与 `ui_net_sink_backend`(`ui_net_sink_get_lcdmem` / `ui_net_sink_inject_key`);接入新平台时二者缺一不可(MCU:`net_socket.c` + `ui_net_sink_backend_mcu.c`;模拟器:`net_socket_sim.c` + `ui_net_sink_backend_sim.c`)。 +7. **单连接**:`backlog=1`,同一时刻仅一个客户端;新连接需旧连接先断开。 + +--- + +## 6. 相关文档 + +| 主题 | 文档 | +|---|---| +| 网口配置(含远程显示服务自愈) | `docs/网络配置说明.md` | +| 项目分层架构 | `docs/项目架构总览.md` | +| 启动顺序 / 就绪屏障 | `docs/项目启动顺序管理.md` | +| UI 设计架构 | `docs/UI 设计/UI_设计架构.md` | +| LVGL 模拟器部署 | `docs/UI 设计/LVGL模拟器部署与使用说明.md` | +| CH395F 网络测试 | `docs/测试说明/CH395F_Test_Guide.md` | + +--- + +## 7. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-09-10 | 创建:RemoDispBus 协议设计 + 模拟器逐字节状态机接收解析设计;补充 MCU 整块解析差异 | +| 2026-09-10 | 双端统一:MCU 与模拟器共用同一份 `ui_net_sink.c`(逐字节状态机);底层改用 `net_*` BSD API(新增 `net_socket_sim.c` Winsock 后端、`net_socket.h` 以 `NET_NO_OS` 剥离 FreeRTOS)+ `ui_net_sink_backend` 平台抽象;消除 MCU 整块解析脆弱性与最小帧长判据差异、修复单连接断线重连;`RM_RX_BUF` 1024→64 | +| 2026-09-11 | 目录整理:PC 平台实现移入 `simulator/sim/`(`net_socket_sim.c` / `ui_net_sink_backend_sim.c` / `net_cfg_store_sim.c` / `sim_port.c/h`),`Drivers/BSP/` 仅留 MCU 端实现;§1.2 帧结构重绘为标准两行框图(字段名 + 字节数) | diff --git a/docs/UI 设计/LVGL模拟器部署与使用说明.md b/docs/UI 设计/LVGL模拟器部署与使用说明.md index 0ac83f44..fc165ac0 100644 --- a/docs/UI 设计/LVGL模拟器部署与使用说明.md +++ b/docs/UI 设计/LVGL模拟器部署与使用说明.md @@ -20,7 +20,7 @@ simulator/main.c ──→ lv_init → sim_port_init(SDL 窗口 + 键盘)→ | 模块 | 文件 | 说明 | |------|------|------| | 入口 | `simulator/main.c` | SDL 初始化、主循环、键盘事件 | -| 端口 | `simulator/sim_port.c` | SDL 显示 + 键盘输入 + mono 主题初始化(替代 MCU 端 `ui_port.c`) | +| 端口 | `simulator/sim/sim_port.c` | SDL 显示 + 键盘输入 + mono 主题初始化(替代 MCU 端 `ui_port.c`) | | 配置 | `simulator/lv_conf.h` | **模拟器专用** LVGL 配置(1bpp + SDL 启用) | | 共享 UI | `Drivers/BSP/UI/ui_menu.c` | 与 MCU 端共用,不做任何修改 | | 共享字体 | `Drivers/BSP/UI/ui_font_cn_12.c` | 与 MCU 端共用 | @@ -160,7 +160,7 @@ LVGL 官方源码**原本没有** CMakeLists.txt(只有 `src/`),本工程 | `LV_FONT_DEFAULT` | `&ui_font_cn_12` | 同 | | `LV_MEM_SIZE` | 40KB | 40KB(模拟器内存充足,可放宽) | -### 5.4 `simulator/sim_port.c` +### 5.4 `simulator/sim/sim_port.c` MCU 端 `ui_port.c` 的 PC 替代实现: @@ -251,7 +251,7 @@ A: 不能。模拟器仅预览 UI,不含 CH395F/NAND/ADC 等硬件业务。 | 路径 | 状态 | 说明 | |------|------|------| | `simulator/main.c` | 新增 | 入口 | -| `simulator/sim_port.c` / `.h` | 新增 | SDL 端口 | +| `simulator/sim/sim_port.c` / `.h` | 新增 | SDL 端口 | | `simulator/lv_conf.h` | 新增 | 模拟器 LVGL 配置 | | `simulator/dbg_log.h` | 新增 | printf 日志 stub | | `simulator/CMakeLists.txt` | 新增 | 构建脚本 | diff --git a/docs/UI 设计/UI_设计架构.md b/docs/UI 设计/UI_设计架构.md index 8f2eefc1..ed97c0e3 100644 --- a/docs/UI 设计/UI_设计架构.md +++ b/docs/UI 设计/UI_设计架构.md @@ -20,11 +20,11 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓 | `Drivers/BSP/UI/ui_menu_view.c/h` | **共用** | 菜单显示层:横向级联框渲染(右贴靠/自适应宽/顶底对齐),持有全部 LVGL 对象与样式、无导航状态 | | `Drivers/BSP/UI/ui_menu_tab.c/h` | **共用** | 菜单数据层:`g_menu_tab[]` 父子索引表 + 查询 API(生成物,勿手改) | | `Drivers/BSP/UI/ui_content_page.c/h` | **共用** | 内容页框架(机制 + 描述符表);装置信息/交流量/直流量/用户密钥四页由 `ui_page_xxx` 模块接入(独立屏按需建毁,自绘行,零常驻池占用);多页面"框架+页面插件"架构见 `内容页架构.md` | -| `Drivers/BSP/UI/ui_net_sink.c/h` | MCU | RemoDispBus 服务端(收键 + 回送帧缓冲) | +| `Drivers/BSP/UI/ui_net_sink.c/h` | **共用** | RemoDispBus 服务端(收键 + 回送帧缓冲),MCU/PC 双端同一份;平台后端 `net_socket` + `ui_net_sink_backend`(见 RemoDispBus 协议文档) | | `Drivers/BSP/UI/ui_font_cn_12.c` | **共用** | 中文 1bpp 子集字体(`lv_font_conv` 生成物) | | `App/task/ui_task.c/h` | MCU | UI 任务:等 NET_READY → 初始化 → 10ms 刷新循环 | | `Lib/lv_conf.h` | MCU | LVGL 配置(1bpp / 40KB 内存池 / 默认字体) | -| `simulator/`(main.c / sim_port.c / lv_conf.h / CMakeLists.txt) | PC | SDL2 模拟器(替代 ui_port,共用全部 UI 源码);支持 `--screenshot` / `--keys ` 无头截图验证 | +| `simulator/`(main.c / sim/ / lv_conf.h / CMakeLists.txt) | PC | SDL2 模拟器(替代 ui_port,共用全部 UI 源码);支持 `--screenshot` / `--keys ` 无头截图验证 | | `tools/font_gen/`(extract_cn.py / make_font.py)+ `ui_cn_chars.txt` | 工具 | 字符集提取 / 字体生成 | | `tools/menu_gen/gen_menu.py` + `ui_menu_tree.txt` | 工具 | 菜单声明源 → 生成 ui_menu_tab.c/h | diff --git a/docs/网络配置说明.md b/docs/网络配置说明.md index e901e154..771108b6 100644 --- a/docs/网络配置说明.md +++ b/docs/网络配置说明.md @@ -194,7 +194,7 @@ ENTER 进入选中地址行 → `↑/↓` 调活动字节(0..255 **环绕**) | `Drivers/BSP/NETCFG/net_cfg.h/.c` | 配置核心:RAM 副本 + 工厂默认 + 加载/保存编排 + MAC 影子 + IPv4 格式化 | | `Drivers/BSP/NETCFG/net_cfg_store.h` | 存储后端接口(读/写) | | `Drivers/BSP/NETCFG/net_cfg_store_mcu.c` | MCU 后端:FatFS `/cfg/netcfg.bin` | -| `Drivers/BSP/NETCFG/net_cfg_store_sim.c` | 模拟器后端:本地 `sim_netcfg.bin` | +| `simulator/sim/net_cfg_store_sim.c` | 模拟器后端:本地 `sim_netcfg.bin` | | `Drivers/BSP/UI/ui_page_net_cfg.c` | 网口设置内容页(显示 / 编辑 / 保存) | | `Drivers/BSP/UI/ui_pwd_def.h` | 编译期密码(查看 000 / 修改 123) | | `App/task/net_task.c` | 上电加载配置 + 消费"立即应用"请求重建网络 | diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 29f086ba..377f38e0 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -7,7 +7,8 @@ project(lvgl_sim C) set(PROJECT_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..") set(LVGL_DIR "${PROJECT_ROOT}/Lib/lvgl") set(UI_DIR "${PROJECT_ROOT}/Drivers/BSP/UI") -set(NETCFG_DIR "${PROJECT_ROOT}/Drivers/BSP/NETCFG") +set(NET_DIR "${PROJECT_ROOT}/Drivers/BSP/NET") +set(NETCFG_DIR "${PROJECT_ROOT}/Drivers/BSP/NETCFG") set(SDL2_DIR "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/SDL2-2.32.10") #============================================================================= @@ -44,10 +45,12 @@ add_subdirectory("${LVGL_DIR}" lvgl_build) # 包含路径 #============================================================================= include_directories( - ${CMAKE_CURRENT_SOURCE_DIR} # sim_port.h, lvgl conf.h (LVGL 找 lv_conf.h), dbg_cfg.h + ${CMAKE_CURRENT_SOURCE_DIR} # lv_conf.h (LVGL 找), dbg_log.h + ${CMAKE_CURRENT_SOURCE_DIR}/sim # sim_port.h 等模拟器平台层 ${LVGL_DIR} # lvgl.h ${LVGL_DIR}/src # lvgl 内部头文件 ${UI_DIR} # ui_menu.h, ui_font_cn_12.h + ${NET_DIR} # net_socket.h / net_types.h(net_* API 双端共享核心) ${NETCFG_DIR} # net_cfg.h(网络配置,双端共享核心) ${PROJECT_ROOT}/Drivers/BSP # dbg_log.h(使用 simulator 的 dbg_cfg.h 配置) ${SDL2_DIR}/include # SDL2 头文件 @@ -64,7 +67,7 @@ set(SDL2_LIB_X64 "${SDL2_DIR}/lib/x64") #============================================================================= set(SIM_SOURCES main.c - sim_port.c + sim/sim_port.c ) # UI 源文件(与 MCU 端共用,直接编译) @@ -80,24 +83,31 @@ set(UI_SOURCES ${UI_DIR}/ui_page_net_cfg.c ${UI_DIR}/ui_menu_tab.c ${UI_DIR}/ui_font_cn_12.c - ${CMAKE_CURRENT_SOURCE_DIR}/ui_net_sink.c + ${UI_DIR}/ui_net_sink.c ) # 网络配置(双端共享核心 + 模拟器存储后端;MCU 端编 net_cfg_store_mcu.c,见 Keil 工程) set(NETCFG_SOURCES ${NETCFG_DIR}/net_cfg.c - ${NETCFG_DIR}/net_cfg_store_sim.c + sim/net_cfg_store_sim.c +) + +# 网络后端(net_* 核心 API 的 Winsock 实现 + UI 推流平台后端;MCU 端另编 net_socket.c 与 _mcu 后端) +set(NET_SOURCES + sim/net_socket_sim.c + sim/ui_net_sink_backend_sim.c ) #============================================================================= # 构建可执行文件 #============================================================================= -add_executable(lvgl_sim WIN32 ${SIM_SOURCES} ${UI_SOURCES} ${NETCFG_SOURCES}) +add_executable(lvgl_sim WIN32 ${SIM_SOURCES} ${UI_SOURCES} ${NETCFG_SOURCES} ${NET_SOURCES}) target_compile_definitions(lvgl_sim PRIVATE LV_CONF_INCLUDE_SIMPLE # 让 lvgl.h 找到 lv_conf.h 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) ) target_include_directories(lvgl_sim PRIVATE diff --git a/simulator/README.md b/simulator/README.md index deabe13b..3808fd5d 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -13,8 +13,11 @@ simulator/ ├── CMakeLists.txt # 构建脚本(CMake + MSVC,x64) ├── main.c # 入口:SDL 初始化 → LVGL → 菜单 → 主循环 -├── sim_port.c / sim_port.h # 模拟器显示/输入端口(SDL 窗口 + 键盘),替代 MCU 端 ui_port.c -├── ui_net_sink.c # RemoDispBus 推流服务端(Winsock TCP 7003),替代 MCU 端同名文件 +├── sim/ # 平台层(PC 专用实现;MCU 端对应实现在各 BSP 目录) +│ ├── sim_port.c / .h # 显示/输入端口(SDL 窗口 + 键盘 + mono 主题),替代 MCU 端 ui_port.c +│ ├── net_socket_sim.c # 网络后端:Winsock 实现 net_* BSD API,替代 MCU 端 net_socket.c(CH395F) +│ ├── ui_net_sink_backend_sim.c # RemoDispBus 平台 backend:取帧/注键(配合共享 ui_net_sink.c) +│ └── net_cfg_store_sim.c # 网口配置存储:本地 sim_netcfg.bin ├── lv_conf.h # 模拟器专用 LVGL 配置(与 MCU 端 Lib/lv_conf.h 相互独立) ├── i1_to_png.py # 截图转 PNG:把 --screenshot 的 I1(1bpp) 原始位图转成 PNG(见 §3.5) ├── tests/ # 无头回归脚本 + 本地黄金参考(见 §3.6,流程文档 docs/UI 设计/UI_自动化测试流程.md) @@ -40,15 +43,17 @@ simulator/ | 文件 | 说明 | |------|------| | `main.c` | 程序入口。`SDL_Init(TIMER\|EVENTS)` → `lv_init()` → `sim_port_init(160,160)` → `ui_menu_init()`(共享代码)→ 主循环(SDL 事件 + `lv_timer_handler` + `lv_refr_now`) | -| `sim_port.c` | 创建 SDL 窗口显示设备(`lv_sdl_window_create`)、初始化 **mono 主题**(`lv_theme_mono_init`,1bpp 必需)、创建键盘输入设备并挂默认输入组。**不创建鼠标**(纯键盘导航,与 MCU 端 keypad 一致)。`sim_port_get_screen_mono()` 经 `SDL_RenderReadPixels` 读回屏幕像素转 1bpp,供推流取帧 | -| `ui_net_sink.c` | RemoDispBus 推流服务端(Winsock TCP 7003,与 MCU 端同名文件协议一致):连接后**不**主动上报 INIT,LCDMEM 取帧调 `sim_port_get_screen_mono` 转 1bpp 回传 | +| `sim/sim_port.c` | 创建 SDL 窗口显示设备(`lv_sdl_window_create`)、初始化 **mono 主题**(`lv_theme_mono_init`,1bpp 必需)、创建键盘输入设备并挂默认输入组。**不创建鼠标**(纯键盘导航,与 MCU 端 keypad 一致)。`sim_port_get_screen_mono()` 经 `SDL_RenderReadPixels` 读回屏幕像素转 1bpp,供推流取帧 | +| `sim/net_socket_sim.c` | 网络后端:Winsock 实现 `net_*` BSD Socket API(`socket`/`bind`/`listen`/`accept`/`recv`/`send`/`close`),替代 MCU 端 `Drivers/BSP/NET/net_socket.c`(CH395F) | +| `sim/ui_net_sink_backend_sim.c` | RemoDispBus 平台 backend:`ui_net_sink_get_lcdmem`→`sim_port_get_screen_mono`、`ui_net_sink_inject_key`→`lv_group_send_data`;配合**共享** `Drivers/BSP/UI/ui_net_sink.c`(协议服务端,双端同一份,TCP 7003) | +| `sim/net_cfg_store_sim.c` | 网口配置存储后端:本地文件 `sim_netcfg.bin`(`fopen`),替代 MCU 端 `Drivers/BSP/NETCFG/net_cfg_store_mcu.c`(Flash) | | `lv_conf.h` | 模拟器专用 LVGL 配置。与 MCU 端 `Lib/lv_conf.h` 的关键差异见 §3.3 | | `dbg_log.h` | MCU 版 `dbg_log.h` 依赖 `sys_clock.h`(RTC 时间戳),PC 无法编译。此 stub 把 `DBG_INFO/ERROR/DEBUG` 映射到 `printf`;因 `simulator/` 在 include 路径最前,**优先生效**覆盖 MCU 版 | -| `CMakeLists.txt` | 收集 LVGL 源码(`../Lib/lvgl`)+ 共享 UI 源码(`../Drivers/BSP/UI`)+ 本目录端口代码,链接预编译 SDL2,产出 `lvgl_sim.exe` | +| `CMakeLists.txt` | 收集 LVGL 源码(`../Lib/lvgl`)+ 共享 UI 源码(`../Drivers/BSP/UI`)+ `sim/` 平台层,链接预编译 SDL2,产出 `lvgl_sim.exe` | | `i1_to_png.py` | 把 `--screenshot` 产出的 I1(1bpp) 原始位图转成 PNG,便于离线比对设计稿。详见 §3.5 | | `tests/` | 无头回归脚本(菜单 8 状态像素回归、内容页文本断言 + 黄金参考回归);`ref/` 为**本地生成物、不入库**(首次 `--update` 生成)。详见 §3.6 | -> **共享代码不复制**:`ui_menu.c` / `ui_font_cn_12.c` 直接引用 `Drivers/BSP/UI/` 下的原件, +> **共享代码不复制**:`ui_menu.c` / `ui_font_cn_12.c` / `ui_net_sink.c` 直接引用 `Drivers/BSP/UI/` 下的原件, > MCU 与 PC 两端永远同步。**不要**在本目录内复制 UI 源码。 --- diff --git a/Drivers/BSP/NETCFG/net_cfg_store_sim.c b/simulator/sim/net_cfg_store_sim.c similarity index 100% rename from Drivers/BSP/NETCFG/net_cfg_store_sim.c rename to simulator/sim/net_cfg_store_sim.c diff --git a/simulator/sim/net_socket_sim.c b/simulator/sim/net_socket_sim.c new file mode 100644 index 00000000..4817f35e --- /dev/null +++ b/simulator/sim/net_socket_sim.c @@ -0,0 +1,786 @@ +/* + * 模块名称:Network Socket API (Winsock 模拟器后端) + * 模块功能:以 Windows Winsock2 实现 net_socket.h 的 BSD Socket 核心 API, + * 使 PC 模拟器与 MCU 端可共用同一份上层网络代码(ui_net_sink.c 等) + * 适用平台:Windows (Winsock2);构建时须定义 NET_NO_OS(剥离 FreeRTOS 相关接口) + * 作者:王建锋 + * 创建日期:2026-09-10 + * 修改记录: + * 2026-09-10 王建锋 创建初始版本(RemoDispBus 双端共用 net_* 后端) + * + * 设计要点: + * 1. fd 采用与 MCU 端一致的"控制块索引"模型(0 ~ NET_MAX_SOCKETS-1) + * 2. 所有 Socket 创建即置为非阻塞(ioctlsocket FIONBIO),使 net_recv / + * net_accept 的 DONTWAIT 语义与 MCU 端一致(无数据返回 -1 + NET_ERR_WOULDBLOCK) + * 3. 代次号在模拟器上恒为 0(无"网络重建"概念),net_get_generation 恒返回 0 + * 4. net_get_sock 以 in_use 判活,供上层判断"监听 Socket 是否随客户端一同失效" + * 5. Winsock 句柄存放于并行数组 s_ws[],不侵入共享的 net_sock_t 结构体 + */ + +/* + * 注意:net_types.h(经 net_socket.h)必须先于 winsock2.h 包含。 + * MSVC 的 C 编译器在 winsock2.h 定义 Windows 宏之后再解析本项目的地址结构体 + * (struct net_in_addr / net_sockaddr_in)会报 C2143 / C2079;ARMCC 无此限制。 + * 故此处先引入 net 类型,再引入 Winsock(net_socket.h 本身不依赖 Winsock)。 + */ +#include "net_socket.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include + +#include +#include + +/* WSA 版本号(MAKEWORD(2,2)) */ +#define NET_SIM_WSA_VERSION 0x0202 +/* 监听 backlog(单客户端) */ +#define NET_SIM_BACKLOG 1 + +/* 控制块公开视图(in_use 判活,供 net_get_sock)与 Winsock 句柄并行数组 */ +static net_sock_t s_socks[NET_MAX_SOCKETS]; +static SOCKET s_ws[NET_MAX_SOCKETS]; +static int s_net_errno = NET_OK; /* 最近一次操作错误码 */ +static int s_generation = 0; /* 网络代次(模拟器恒为 0) */ + +/* + * 函数功能:确保 Winsock 已初始化(幂等) + * 入口参数:无 + * 出口参数:无 + * 返回值:0 - 就绪,-1 - WSAStartup 失败 + * 限定条件:无 + * 函数说明:任何 Socket 操作前先调用;已初始化则直接返回 + */ +static int ensure_wsa(void) +{ + static int s_done = 0; + + if (s_done == 0) { + WSADATA wsa; + if (WSAStartup(NET_SIM_WSA_VERSION, &wsa) != 0) { + return -1; + } + s_done = 1; + } + return 0; +} + +/* + * 函数功能:分配一个空闲 fd(控制块索引) + * 入口参数:无 + * 出口参数:无 + * 返回值:空闲索引(0 ~ NET_MAX_SOCKETS-1),-1 表示无空闲 + * 限定条件:s_socks 已定义 + * 函数说明:线性扫描,返回首个 in_use == 0 的控制块索引 + */ +static int alloc_fd(void) +{ + int i; + + for (i = 0; i < NET_MAX_SOCKETS; i++) { + if (s_socks[i].in_use == 0) { + return i; + } + } + return -1; +} + +/* + * 函数功能:net_sockaddr 拷贝为 Winsock sockaddr_in + * 入口参数:addr - 源地址(net_sockaddr,AF_INET 实为 net_sockaddr_in) + * out - 输出 Winsock 地址 + * 出口参数:out - 填充 + * 返回值:无 + * 限定条件:两结构体布局一致(均 16 字节,字段偏移相同) + * 函数说明:字段偏移一致,按 sizeof 直接拷贝 + */ +static void to_ws_addr(const struct net_sockaddr *addr, struct sockaddr_in *out) +{ + memcpy(out, addr, sizeof(struct sockaddr_in)); +} + +/* + * 函数功能:Winsock sockaddr_in 拷贝为 net_sockaddr + * 入口参数:in - 源 Winsock 地址 + * out - 输出 net_sockaddr + * 出口参数:out - 填充 + * 返回值:无 + * 限定条件:两结构体布局一致(均 16 字节,字段偏移相同) + * 函数说明:字段偏移一致,按 sizeof 直接拷贝 + */ +static void from_ws_addr(const struct sockaddr_in *in, struct net_sockaddr *out) +{ + memcpy(out, in, sizeof(struct sockaddr_in)); +} + +/* + * 函数功能:网络子系统初始化(模拟器:仅确保 Winsock 就绪) + * 入口参数:ip/mask/gateway/mac - 均忽略(模拟器无 CH395F 协议栈) + * 出口参数:无 + * 返回值:0 - 成功,-1 - WSA 初始化失败 + * 限定条件:无 + * 函数说明:与 MCU 端 net_init 语义对齐,供上层统一调用 + */ +int net_init(const char *ip, const char *mask, const char *gateway, const uint8_t *mac) +{ + (void)ip; + (void)mask; + (void)gateway; + (void)mac; + + if (ensure_wsa() != 0) { + s_net_errno = NET_ERR; + return -1; + } + s_net_errno = NET_OK; + return 0; +} + +/* + * 函数功能:取网络代次号 + * 入口参数:无 + * 返回值:代次号(模拟器恒为 0) + * 限定条件:无 + * 函数说明:模拟器无网络重建,代次恒 0,使上层代次自愈逻辑空转 + */ +uint32_t net_get_generation(void) +{ + return (uint32_t)s_generation; +} + +/* + * 函数功能:递增网络代次号 + * 入口参数:无 + * 返回值:无 + * 限定条件:无 + * 函数说明:模拟器不会触发,保留接口以对齐 API + */ +void net_generation_inc(void) +{ + s_generation++; +} + +/* + * 函数功能:轮询所有 Socket 状态 + * 入口参数:无 + * 返回值:0(Winsock 非阻塞 I/O 直接反映事件,无需中断轮询) + * 限定条件:无 + * 函数说明:模拟器无 CH395F 中断,空操作 + */ +int net_poll(void) +{ + return 0; +} + +/* + * 函数功能:创建 socket + * 入口参数:domain - 地址族(仅 AF_INET) + * type - NET_SOCK_STREAM(TCP) 或 NET_SOCK_DGRAM(UDP) + * 出口参数:无 + * 返回值:socket 描述符(0 ~ NET_MAX_SOCKETS-1),失败 -1 + * 限定条件:Winsock 已就绪 + * 函数说明:分配空闲控制块,创建 Winsock SOCKET 并置非阻塞 + */ +int net_socket(int domain, int type) +{ + int idx; + int is_udp; + SOCKET ws; + u_long nb = 1; + + (void)domain; + + if (ensure_wsa() != 0) { + s_net_errno = NET_ERR; + return -1; + } + + idx = alloc_fd(); + if (idx < 0) { + s_net_errno = NET_ERR; + return -1; + } + + is_udp = (type == NET_SOCK_DGRAM); + ws = socket(AF_INET, is_udp ? SOCK_DGRAM : SOCK_STREAM, + is_udp ? IPPROTO_UDP : IPPROTO_TCP); + if (ws == INVALID_SOCKET) { + s_net_errno = NET_ERR; + return -1; + } + + if (ioctlsocket(ws, FIONBIO, &nb) != 0) { + closesocket(ws); + s_net_errno = NET_ERR; + return -1; + } + + memset(&s_socks[idx], 0, sizeof(net_sock_t)); + s_socks[idx].in_use = 1; + s_socks[idx].type = (uint8_t)type; + s_socks[idx].state = NET_SOCK_STATE_CREATED; + s_socks[idx].ch395_sock = (uint8_t)idx; + s_ws[idx] = ws; + + s_net_errno = NET_OK; + return idx; +} + +/* + * 函数功能:绑定本地地址和端口 + * 入口参数:sockfd - socket 描述符 + * addr - 本地地址(net_sockaddr,AF_INET) + * addrlen - 地址结构体长度 + * 出口参数:无 + * 返回值:0 成功,-1 失败 + * 限定条件:socket 已创建 + * 函数说明:设置 SO_REUSEADDR 后 bind,便于断线后重监听复用端口 + */ +int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) +{ + struct sockaddr_in wsaddr; + int yes = 1; + + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (addr == NULL || addrlen < (int)sizeof(struct sockaddr_in)) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + + setsockopt(s_ws[sockfd], SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes)); + to_ws_addr(addr, &wsaddr); + + if (bind(s_ws[sockfd], (struct sockaddr *)&wsaddr, sizeof(wsaddr)) == SOCKET_ERROR) { + s_net_errno = NET_ERR; + return -1; + } + + s_socks[sockfd].state = NET_SOCK_STATE_BOUND; + s_net_errno = NET_OK; + return 0; +} + +/* + * 函数功能:TCP Server 监听 + * 入口参数:sockfd - socket 描述符 + * 出口参数:无 + * 返回值:0 成功,-1 失败 + * 限定条件:socket 已绑定 + * 函数说明:backlog 为 1(单客户端),监听 Socket 保持非阻塞 + */ +int net_listen(int sockfd) +{ + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + + if (listen(s_ws[sockfd], NET_SIM_BACKLOG) == SOCKET_ERROR) { + s_net_errno = NET_ERR; + return -1; + } + + s_socks[sockfd].state = NET_SOCK_STATE_LISTENING; + s_net_errno = NET_OK; + return 0; +} + +/* + * 函数功能:TCP Server 接受连接 + * 入口参数:sockfd - 监听 socket 描述符 + * addr - 输出:客户端地址(可为 NULL) + * addrlen - 输入输出:地址长度(addr 非空时有效) + * 出口参数:addr/addrlen - 填充客户端地址 + * 返回值:新 socket 描述符,失败 -1 + * 限定条件:socket 正在监听 + * 函数说明:非阻塞监听下无新连接时返回 -1 并置 NET_ERR_WOULDBLOCK; + * 与 MCU 端不同,Winsock 的 accept 返回独立于监听 Socket 的新 fd + */ +int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) +{ + struct sockaddr_in wscli; + int wslen; + SOCKET cws; + int nidx; + u_long nb = 1; + + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + + wslen = sizeof(wscli); + cws = accept(s_ws[sockfd], (addr != NULL) ? (struct sockaddr *)&wscli : NULL, + (addr != NULL) ? &wslen : NULL); + if (cws == INVALID_SOCKET) { + int err = WSAGetLastError(); + s_net_errno = (err == WSAEWOULDBLOCK) ? NET_ERR_WOULDBLOCK : NET_ERR; + return -1; + } + + if (addr != NULL && addrlen != NULL) { + from_ws_addr(&wscli, addr); + *addrlen = (int)sizeof(struct net_sockaddr_in); + } + + nidx = alloc_fd(); + if (nidx < 0) { + closesocket(cws); + s_net_errno = NET_ERR; + return -1; + } + + ioctlsocket(cws, FIONBIO, &nb); + memset(&s_socks[nidx], 0, sizeof(net_sock_t)); + s_socks[nidx].in_use = 1; + s_socks[nidx].type = (uint8_t)NET_SOCK_STREAM; + s_socks[nidx].state = NET_SOCK_STATE_ESTABLISHED; + s_socks[nidx].ch395_sock = (uint8_t)nidx; + s_ws[nidx] = cws; + + s_net_errno = NET_OK; + return nidx; +} + +/* + * 函数功能:TCP Client 发起连接 + * 入口参数:sockfd - socket 描述符 + * addr - 服务器地址 + * addrlen - 地址长度 + * 出口参数:无 + * 返回值:0 成功(或已发起),-1 失败 + * 限定条件:socket 已创建 + * 函数说明:非阻塞 Socket 上连接未完成时返回 -1 + NET_ERR_WOULDBLOCK + */ +int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen) +{ + struct sockaddr_in wsaddr; + + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (addr == NULL || addrlen < (int)sizeof(struct sockaddr_in)) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + + to_ws_addr(addr, &wsaddr); + + if (connect(s_ws[sockfd], (struct sockaddr *)&wsaddr, sizeof(wsaddr)) == 0) { + s_socks[sockfd].state = NET_SOCK_STATE_ESTABLISHED; + s_net_errno = NET_OK; + return 0; + } + + { + int err = WSAGetLastError(); + s_net_errno = (err == WSAEWOULDBLOCK) ? NET_ERR_WOULDBLOCK : NET_ERR; + } + return -1; +} + +/* + * 函数功能:发送数据(TCP) + * 入口参数:sockfd - socket 描述符 + * buf - 数据缓冲区 + * len - 数据长度 + * flags - 通常为 0 + * 出口参数:无 + * 返回值:实际发送字节数,-1 错误;发送缓冲满时返回 0 + NET_ERR_WOULDBLOCK + * 限定条件:socket 已建立,buf 非空 + * 函数说明:非阻塞单次发送(保持 UI 主循环不阻塞);协议帧小且对端主动 + * 读取,实践中一次即发完 + */ +int net_send(int sockfd, const void *buf, int len, int flags) +{ + int r; + + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (buf == NULL || len < 0) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + + r = send(s_ws[sockfd], (const char *)buf, len, flags); + if (r >= 0) { + s_socks[sockfd].send_bytes += (uint32_t)r; + s_net_errno = NET_OK; + return r; + } + + { + int err = WSAGetLastError(); + if (err == WSAEWOULDBLOCK) { + s_net_errno = NET_ERR_WOULDBLOCK; + return 0; + } + s_net_errno = NET_ERR; + } + return -1; +} + +/* + * 函数功能:接收数据(TCP) + * 入口参数:sockfd - socket 描述符 + * buf - 接收缓冲区 + * len - 缓冲区大小 + * flags - 通常为 0,可设 NET_MSG_DONTWAIT(Socket 本就非阻塞) + * 出口参数:buf - 填充接收数据 + * 返回值:实际接收字节数;0 = 对端关闭;-1 = 无数据(NET_ERR_WOULDBLOCK)或错误 + * 限定条件:socket 已建立 + * 函数说明:与 MCU 端语义一致——>0 有数据,0 对端关闭,<0 且 errno 区分 + * "暂无数据"与"真错误" + */ +int net_recv(int sockfd, void *buf, int len, int flags) +{ + int r; + + (void)flags; + + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (buf == NULL || len < 0) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + + r = recv(s_ws[sockfd], (char *)buf, len, 0); + if (r > 0) { + s_socks[sockfd].recv_bytes += (uint32_t)r; + s_net_errno = NET_OK; + return r; + } + if (r == 0) { + s_net_errno = NET_OK; + return 0; + } + + { + int err = WSAGetLastError(); + s_net_errno = (err == WSAEWOULDBLOCK) ? NET_ERR_WOULDBLOCK : NET_ERR; + } + return -1; +} + +/* + * 函数功能:发送 UDP 数据报(含目标地址) + * 入口参数:sockfd - socket 描述符 + * buf - 数据缓冲区 + * len - 数据长度 + * flags - 通常为 0 + * dest_addr - 目标地址 + * addrlen - 地址长度 + * 出口参数:无 + * 返回值:实际发送字节数,-1 失败 + * 限定条件:socket 已创建,buf/dest_addr 非空 + */ +int net_sendto(int sockfd, const void *buf, int len, int flags, + const struct net_sockaddr *dest_addr, int addrlen) +{ + struct sockaddr_in wsaddr; + int r; + + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (buf == NULL || len < 0 || dest_addr == NULL || + addrlen < (int)sizeof(struct sockaddr_in)) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + + to_ws_addr(dest_addr, &wsaddr); + + r = sendto(s_ws[sockfd], (const char *)buf, len, flags, + (struct sockaddr *)&wsaddr, sizeof(wsaddr)); + if (r >= 0) { + s_net_errno = NET_OK; + return r; + } + + { + int err = WSAGetLastError(); + s_net_errno = (err == WSAEWOULDBLOCK) ? NET_ERR_WOULDBLOCK : NET_ERR; + } + return -1; +} + +/* + * 函数功能:接收 UDP 数据报(含源地址) + * 入口参数:sockfd - socket 描述符 + * buf - 接收缓冲区 + * len - 缓冲区大小 + * flags - 通常为 0 + * src_addr - 输出:发送方地址(可为 NULL) + * addrlen - 输入输出:源地址长度(可为 NULL) + * 出口参数:buf/src_addr/addrlen - 填充 + * 返回值:实际接收字节数,-1 失败 + * 限定条件:socket 已创建,buf 非空 + */ +int net_recvfrom(int sockfd, void *buf, int len, int flags, + struct net_sockaddr *src_addr, int *addrlen) +{ + struct sockaddr_in wsaddr; + int wslen; + int r; + + (void)flags; + + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (buf == NULL || len < 0) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + + wslen = sizeof(wsaddr); + r = recvfrom(s_ws[sockfd], (char *)buf, len, 0, + (struct sockaddr *)&wsaddr, &wslen); + if (r < 0) { + int err = WSAGetLastError(); + s_net_errno = (err == WSAEWOULDBLOCK) ? NET_ERR_WOULDBLOCK : NET_ERR; + return -1; + } + + if (src_addr != NULL && addrlen != NULL) { + from_ws_addr(&wsaddr, src_addr); + *addrlen = (int)sizeof(struct net_sockaddr_in); + } + + s_net_errno = NET_OK; + return r; +} + +/* + * 函数功能:关闭 socket + * 入口参数:sockfd - socket 描述符 + * 出口参数:无 + * 返回值:0 成功,-1 失败 + * 限定条件:socket 已打开(in_use == 1) + * 函数说明:关闭 Winsock 句柄并释放控制块(in_use 清零,net_get_sock 转返回 NULL) + */ +int net_close(int sockfd) +{ + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + + closesocket(s_ws[sockfd]); + s_socks[sockfd].in_use = 0; + s_socks[sockfd].state = NET_SOCK_STATE_CLOSED; + s_ws[sockfd] = INVALID_SOCKET; + + s_net_errno = NET_OK; + return 0; +} + +/* + * 函数功能:注册事件回调 + * 入口参数:sockfd - socket 描述符 + * cb - 回调函数(NULL 取消注册) + * arg - 用户参数 + * 出口参数:无 + * 返回值:0 成功,-1 失败 + * 限定条件:socket 已创建 + * 函数说明:模拟器以非阻塞 I/O 直接感知事件,回调仅存储不主动触发 + */ +int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg) +{ + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + + s_socks[sockfd].event_cb = cb; + s_socks[sockfd].event_arg = arg; + + s_net_errno = NET_OK; + return 0; +} + +/* + * 函数功能:获取 socket 最后错误码 + * 入口参数:无 + * 返回值:最近一次 net_* 操作设置的错误码(NET_OK / NET_ERR_xxx) + * 限定条件:无 + */ +int net_get_errno(void) +{ + return s_net_errno; +} + +/* + * 函数功能:将点分十进制 IP 字符串转为网络字节序 32 位地址 + * 入口参数:cp - 点分十进制字符串(如 "192.168.1.100") + * 出口参数:无 + * 返回值:网络字节序 IP(参数无效返回 0) + * 限定条件:无 + * 函数说明:逐段解析后按 NET_HTONL 转网络字节序(与 MCU 端实现一致) + */ +uint32_t net_inet_addr(const char *cp) +{ + uint32_t addr = 0; + int i; + + if (cp == NULL) { + return 0; + } + + for (i = 0; i < 4; i++) { + uint32_t val = 0; + while (*cp >= '0' && *cp <= '9') { + val = val * 10 + (uint32_t)(*cp - '0'); + cp++; + } + addr |= (val << (i * 8)); + if (*cp == '.') { + cp++; + } + } + + return NET_HTONL(addr); +} + +/* + * 函数功能:将网络字节序 32 位 IP 转为点分十进制字符串 + * 入口参数:addr - 网络字节序 IP + * buf - 输出缓冲区(至少 16 字节) + * 出口参数:buf - 填充字符串 + * 返回值:buf 指针(buf 为 NULL 时返回 NULL) + * 限定条件:buf 非空且长度足够 + */ +char *net_inet_ntoa(uint32_t addr, char *buf) +{ + uint32_t naddr = NET_NTOHL(addr); + + if (buf == NULL) { + return NULL; + } + + snprintf(buf, 16, "%lu.%lu.%lu.%lu", + (unsigned long)(naddr & 0xFF), + (unsigned long)((naddr >> 8) & 0xFF), + (unsigned long)((naddr >> 16) & 0xFF), + (unsigned long)((naddr >> 24) & 0xFF)); + + return buf; +} + +/* + * 函数功能:端口字节序转换(主机序 -> 网络序) + * 入口参数:hostshort - 主机序端口 + * 返回值:网络序端口 + * 限定条件:无 + */ +uint16_t net_htons(uint16_t hostshort) +{ + return NET_HTONS(hostshort); +} + +/* + * 函数功能:端口字节序转换(网络序 -> 主机序) + * 入口参数:netshort - 网络序端口 + * 返回值:主机序端口 + * 限定条件:无 + */ +uint16_t net_ntohs(uint16_t netshort) +{ + return NET_NTOHS(netshort); +} + +/* + * 函数功能:IP 地址字节序转换(主机序 -> 网络序) + * 入口参数:hostlong - 主机序 IP + * 返回值:网络序 IP + * 限定条件:无 + */ +uint32_t net_htonl(uint32_t hostlong) +{ + return NET_HTONL(hostlong); +} + +/* + * 函数功能:IP 地址字节序转换(网络序 -> 主机序) + * 入口参数:netlong - 网络序 IP + * 返回值:主机序 IP + * 限定条件:无 + */ +uint32_t net_ntohl(uint32_t netlong) +{ + return NET_NTOHL(netlong); +} + +/* + * 函数功能:获取 Socket 控制块指针 + * 入口参数:sockfd - socket 描述符 + * 返回值:控制块指针;描述符越界或已释放(in_use == 0)时返回 NULL + * 限定条件:无 + * 函数说明:上层据此判断"监听 Socket 是否随客户端一同失效"(单连接模型) + */ +net_sock_t *net_get_sock(int sockfd) +{ + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { + return NULL; + } + if (s_socks[sockfd].in_use == 0) { + return NULL; + } + return &s_socks[sockfd]; +} + +/* + * 函数功能:获取 Socket 本地地址(IP + Port) + * 入口参数:sockfd - socket 描述符 + * addr - 输出:本地地址 + * addrlen - 输入输出:地址结构体长度 + * 出口参数:addr/addrlen - 填充 + * 返回值:0 成功,-1 失败 + * 限定条件:socket 已创建 + * 函数说明:经 Winsock getsockname 读取实际绑定地址 + */ +int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen) +{ + struct sockaddr_in wsaddr; + int wslen; + + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (addr == NULL || addrlen == NULL || + *addrlen < (int)sizeof(struct net_sockaddr_in)) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + + wslen = sizeof(wsaddr); + if (getsockname(s_ws[sockfd], (struct sockaddr *)&wsaddr, &wslen) == SOCKET_ERROR) { + s_net_errno = NET_ERR; + return -1; + } + + from_ws_addr(&wsaddr, addr); + *addrlen = (int)sizeof(struct net_sockaddr_in); + + s_net_errno = NET_OK; + return 0; +} diff --git a/simulator/sim_port.c b/simulator/sim/sim_port.c similarity index 100% rename from simulator/sim_port.c rename to simulator/sim/sim_port.c diff --git a/simulator/sim_port.h b/simulator/sim/sim_port.h similarity index 100% rename from simulator/sim_port.h rename to simulator/sim/sim_port.h diff --git a/simulator/sim/ui_net_sink_backend_sim.c b/simulator/sim/ui_net_sink_backend_sim.c new file mode 100644 index 00000000..e24a21d8 --- /dev/null +++ b/simulator/sim/ui_net_sink_backend_sim.c @@ -0,0 +1,75 @@ +/* + * 模块名称:UI Network Sink Backend (模拟器实现) + * 模块功能:RemoDispBus 服务端在 PC 模拟器端的平台相关操作—— + * 读回 SDL 屏幕转 1bpp(LCDMEM 数据源)与经 LVGL 注入远端按键 + * 适用平台:Windows / Linux / macOS (SDL2) + * 作者:王建锋 + * 创建日期:2026-09-10 + * 修改记录: + * 2026-09-10 王建锋 创建初始版本(双端共用 ui_net_sink 的平台后端) + */ +#include "ui_net_sink_backend.h" + +#include "ui_port.h" /* 远端按键码 UI_KEY_* 常量 */ +#include "sim_port.h" /* sim_port_get_screen_mono */ +#include "lvgl.h" /* lv_group_send_data / LV_KEY_* */ + +/* + * 函数功能:将远端按键码映射为 LVGL 按键码 + * 入口参数:remo_key - 远端按键码(UI_KEY_* 之一) + * 返回值:对应 LV_KEY_* 值;无法识别返回 0 + * 限定条件:无 + * 函数说明:远端按键码与 LVGL 按键码一一对应 + */ +static uint32_t sim_map_key(uint8_t remo_key) +{ + switch (remo_key) { + case UI_KEY_UP: return LV_KEY_UP; + case UI_KEY_DOWN: return LV_KEY_DOWN; + case UI_KEY_LEFT: return LV_KEY_LEFT; + case UI_KEY_RIGHT: return LV_KEY_RIGHT; + case UI_KEY_ENTER: return LV_KEY_ENTER; + case UI_KEY_ESC: return LV_KEY_ESC; + default: return 0; + } +} + +/* + * 函数功能:读回 SDL 屏幕实际呈现像素并转为 1bpp 位图(LCDMEM 数据源) + * 入口参数:out - 输出缓冲(≥ UI_NET_SINK_MONO_SIZE 字节) + * out_size - out 容量(字节) + * 返回值:0 - 成功,-1 - 失败 + * 限定条件:sim_port_init() 已成功;须在 SDL 所属(主循环)线程调用 + * 函数说明:窗口模式显示缓冲经 I1→ARGB8888 转换,不能直读, + * 故读回屏幕像素再按红通道阈值打包 1bpp(布局与 MCU 端一致) + */ +int ui_net_sink_get_lcdmem(uint8_t *out, uint32_t out_size) +{ + if (out == NULL || out_size < UI_NET_SINK_MONO_SIZE) { + return -1; + } + return sim_port_get_screen_mono(out, out_size); +} + +/* + * 函数功能:经 LVGL 输入组注入一个远端按键 + * 入口参数:remo_key - 远端按键码(UI_KEY_* 之一) + * 返回值:无 + * 限定条件:默认输入组已建立 + * 函数说明:映射为 LV_KEY_* 后经 lv_group_send_data 发向焦点对象 + */ +void ui_net_sink_inject_key(uint8_t remo_key) +{ + lv_group_t *g; + uint32_t key; + + key = sim_map_key(remo_key); + if (key == 0) { + return; + } + + g = lv_group_get_default(); + if (g != NULL) { + lv_group_send_data(g, key); + } +} diff --git a/simulator/ui_net_sink.c b/simulator/ui_net_sink.c deleted file mode 100644 index d477b146..00000000 --- a/simulator/ui_net_sink.c +++ /dev/null @@ -1,274 +0,0 @@ -/* - * 模块名称:UI Network Sink (RemoDispBus 服务端 — 模拟器 Winsock 版) - * 模块功能:与 MCU 端相同协议,用 Windows Winsock 实现 TCP 监听 + RemoDispBus 帧收发 - * 适用平台:Windows (Winsock2) - */ -#include "ui_net_sink.h" - -#include -#include -#include -#include "lvgl.h" -#include "sim_port.h" - -/* RemoDispBus 标签与命令(与 ui_net_sink.h / MCU 端一致) */ -#define TAG_CLIENT 0xAA -#define TAG_DEVICE 0xBB -#define CMD_KEEPLIVE 0 -#define CMD_INIT 1 -#define CMD_KEY 2 -#define CMD_LCDMEM 3 - -#define UI_W 160 -#define UI_H 160 -#define UI_MEMSIZE (UI_W * UI_H / 8) /* 3200 字节 */ - -/* 远端按键码(与 ui_port.h 一致) */ -#define UI_KEY_UP 0x02 -#define UI_KEY_DOWN 0x40 -#define UI_KEY_LEFT 0x10 -#define UI_KEY_RIGHT 0x08 -#define UI_KEY_ENTER 0x20 -#define UI_KEY_ESC 0x01 - -static uint8_t g_tx[4 + 4 + UI_MEMSIZE + 1]; -static uint8_t g_lcdmem_data[4 + UI_MEMSIZE]; -static uint8_t g_rx[4096]; - -static SOCKET g_listen = INVALID_SOCKET; -static SOCKET g_client = INVALID_SOCKET; -static int g_recv_off = 0; /* g_rx 已累积字节数 */ - -static uint8_t remo_crc(const uint8_t *d, int len) -{ - uint8_t c = 0; - for (int i = 0; i < len; i++) { - c ^= d[i]; - } - return c; -} - -static void put_u16(uint8_t *p, uint16_t v) -{ - p[0] = (uint8_t)(v >> 8); - p[1] = (uint8_t)(v & 0xFF); -} - -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); -} - -static void remo_send(SOCKET s, uint8_t cmd, const uint8_t *data, uint16_t len) -{ - if (s == INVALID_SOCKET) { - return; - } - uint8_t *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); - send(s, (const char *)p, 4 + len + 1, 0); -} - -static void remo_send_init(SOCKET s) -{ - uint8_t init[8]; - put_u16(init, UI_W); - put_u16(init + 2, UI_H); - put_u32(init + 4, UI_MEMSIZE); - remo_send(s, CMD_INIT, init, 6); -} - -static uint32_t sim_map_key(uint8_t remo_key) -{ - switch (remo_key) { - case UI_KEY_UP: return LV_KEY_UP; - case UI_KEY_DOWN: return LV_KEY_DOWN; - case UI_KEY_LEFT: return LV_KEY_LEFT; - case UI_KEY_RIGHT: return LV_KEY_RIGHT; - case UI_KEY_ENTER: return LV_KEY_ENTER; - case UI_KEY_ESC: return LV_KEY_ESC; - default: return 0; - } -} - -static void remo_handle(SOCKET s, const uint8_t *f, int n) -{ - if (n < 5) { - return; - } - uint8_t tag = f[0]; - uint8_t cmd = f[1]; - uint16_t len = ((uint16_t)f[2] << 8) | f[3]; - const uint8_t *data = f + 4; - if ((int)(4 + len + 1) > n) { - return; - } - uint8_t crc = f[4 + len]; - if (crc != remo_crc(data, len)) { - return; - } - if (tag != TAG_CLIENT) { - return; - } - - switch (cmd) { - case CMD_KEEPLIVE: - remo_send(s, CMD_KEEPLIVE, NULL, 0); - break; - case CMD_INIT: - remo_send_init(s); - break; - case CMD_KEY: - if (len >= 1) { - uint32_t key = sim_map_key(data[0]); - if (key != 0) { - lv_group_send_data(lv_group_get_default(), key); - } - } - remo_send(s, CMD_KEY, NULL, 0); - break; - case CMD_LCDMEM: { - if (len < 4) { - break; - } - uint32_t off = ((uint32_t)data[0] << 24) | - ((uint32_t)data[1] << 16) | - ((uint32_t)data[2] << 8) | - (uint32_t)data[3]; - if (off == 0) { - /* 窗口模式 I1+PARTIAL 显示缓冲系分块且经 I1→ARGB8888 转换, - * 不能直接读;读回屏幕实际呈现像素再转 1bpp(布局与 MCU 端一致) */ - if (sim_port_get_screen_mono(g_lcdmem_data + 4, UI_MEMSIZE) == 0) { - memcpy(g_lcdmem_data, data, 4); - remo_send(s, CMD_LCDMEM, g_lcdmem_data, 4 + UI_MEMSIZE); - } - /* 读回失败则静默丢弃(与 MCU 端一致) */ - } - break; - } - default: - break; - } -} - -/* 尝试从已接收数据中解析一帧并处理 */ -static void try_parse_frames(SOCKET s) -{ - for (;;) { - if (g_recv_off < 5) { - break; /* 不够最小帧长 */ - } - uint16_t payload_len = ((uint16_t)g_rx[2] << 8) | g_rx[3]; - int frame_total = 4 + payload_len + 1; - if (g_recv_off < frame_total) { - break; /* 帧不完整,等更多数据 */ - } - remo_handle(s, g_rx, frame_total); - /* 移除已处理帧(RemoDispBus 是请求-响应模式,不会有粘包;但以防万一保留移动逻辑) */ - int remain = g_recv_off - frame_total; - if (remain > 0) { - memmove(g_rx, g_rx + frame_total, remain); - } - g_recv_off = remain; - } -} - -int ui_net_sink_start(uint16_t port) -{ - g_listen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (g_listen == INVALID_SOCKET) { - return -1; - } - - int yes = 1; - setsockopt(g_listen, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes)); - - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = htons(port); - addr.sin_addr.s_addr = INADDR_ANY; - - if (bind(g_listen, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR) { - closesocket(g_listen); - g_listen = INVALID_SOCKET; - return -1; - } - if (listen(g_listen, 1) == SOCKET_ERROR) { - closesocket(g_listen); - g_listen = INVALID_SOCKET; - return -1; - } - - /* 设为非阻塞 */ - u_long mode = 1; - ioctlsocket(g_listen, FIONBIO, &mode); - - g_client = INVALID_SOCKET; - g_recv_off = 0; - return 0; -} - -void ui_net_sink_stop(void) -{ - if (g_client != INVALID_SOCKET) { - closesocket(g_client); - g_client = INVALID_SOCKET; - } - if (g_listen != INVALID_SOCKET) { - closesocket(g_listen); - g_listen = INVALID_SOCKET; - } - g_recv_off = 0; -} - -void ui_net_sink_poll(void) -{ - if (g_listen == INVALID_SOCKET) { - return; - } - - /* 无客户端时尝试接受连接 */ - if (g_client == INVALID_SOCKET) { - struct sockaddr_in cli; - int cli_len = sizeof(cli); - SOCKET c = accept(g_listen, (struct sockaddr *)&cli, &cli_len); - if (c != INVALID_SOCKET) { - u_long mode = 1; - ioctlsocket(c, FIONBIO, &mode); - g_client = c; - g_recv_off = 0; - /* 不主动上报 INIT(与 MCU 端一致:避免污染客户端首帧读取,由客户端按需请求) */ - } - return; - } - - /* 有客户端时非阻塞收帧 */ - int n = recv(g_client, (char *)(g_rx + g_recv_off), - (int)(sizeof(g_rx) - g_recv_off), 0); - if (n > 0) { - g_recv_off += n; - try_parse_frames(g_client); - } else if (n == 0) { - closesocket(g_client); - g_client = INVALID_SOCKET; - g_recv_off = 0; - } else { - int err = WSAGetLastError(); - if (err != WSAEWOULDBLOCK) { - closesocket(g_client); - g_client = INVALID_SOCKET; - g_recv_off = 0; - } - } -}