Files
STM32F4-Base/Drivers/BSP/UI/ui_net_sink.c
T

447 lines
14 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 单连接模型断线后无法重连的隐患)
*/
#include "ui_net_sink.h"
#include <string.h>
#include "net_socket.h"
#include "net_types.h"
#include "ui_net_sink_backend.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
/* 屏幕尺寸与帧大小(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],共 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);
remo_send(CMD_INIT, init, 6);
}
/*
* 函数功能:处理一帧完整的远端命令(客户端→设备)
* 入口参数:f - 完整帧 [TAG][CMD][LEN][DATA][CRC];n - 帧总长度
* 返回值:无
* 限定条件:调用前帧已通过 CRC 与 TAG 校验
* 函数说明:按命令分发:KEEPLIVE 回心跳 / INIT 回尺寸 / KEY 注入按键 /
* LCDMEM 回送 1bpp 帧缓冲(经平台后端取帧);非法情况静默丢弃
*/
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;
if (n < 5) {
return;
}
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;
}
crc = f[4 + len];
if (crc != remo_crc(data, len)) {
return;
}
if (tag != TAG_CLIENT) {
return;
}
switch (cmd) {
case CMD_KEEPLIVE:
remo_send(CMD_KEEPLIVE, NULL, 0);
break;
case CMD_INIT:
remo_send_init();
break;
case CMD_KEY:
if (len >= 1) {
ui_net_sink_inject_key(data[0]);
}
remo_send(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) {
/* 整帧请求:经平台后端取 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;
}
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;
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;
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) {
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 (i = 0; i < n; i++) {
rm_parser_feed((uint8_t)g_rx[i]);
}
} else if (n == 0) {
/* 对端关闭:关闭客户端,必要时重建监听(单连接模型下监听随客户端失效) */
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();
}
}
}
}