Files
STM32F4-Base/Drivers/BSP/UI/ui_net_sink.c
T
wandering1 a12367981d fix(N34): E14 丢中断复核——芯片不丢事件;listen 排事件挪至 LISTEN 前+全局读收口+全库归因更正
48 轮直读 SINT 矩阵(8 周期×6 变体)复核历史"丢中断"说法:芯片侧事件
丢失 = 0。归类:drain 读清消费 3/48(drain-int 探针现场抓获)、门控
竞态 2/48、主机同步 1/48、幽灵态 0/48 未复现。

根除性修复:net_listen_locked / net_listener_try 的排事件读清从
listen 后挪至 LISTEN 命令之前——新连接 CONNECT 落进该窗即被本方读清
吞掉,即"数据被吞"的生产侧自伤机制(台架 3/48 为同形态放大版)。

全局读收口:全局中断寄存器唯一读者 = net_poll(I7);生产/测试/台架
游离全局读全部删除,ch395f.h 声明处加护栏注释。

归因更正:17 文件约 40 处"丢中断"措辞改为"事件未收到/读清消费竞态"
(机制全部保留,仅归因更正);设计文档 §5.2 新增 E14 锚点;Trap
Records TRAP-NET-17/-20/B3 归因更正段;配图生成器与 SVG 重生成。

台架:事件未收到复核探针保留(drain-int / SINT 补读×3 / 超时归类),
下次跑矩阵自动归类;实验注册(main.c/uvprojx)已完全还原。

验证:@build 0 错误 0 警告;ci_local L0 32/0 + gitlint + L0.5
953/953;doc-links --all --sections-strict 通过;@test PASS(无
[ERR],net init 619ms);回归 :7003 300ms×10 = 10/10、:21 n30×50 =
0/50 失败。
2026-10-14 02:48:50 +08:00

607 lines
27 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 王建锋 E-04:remo_send 校验 net_send 返回值(原丢弃)——短写/失败
* 告警,连续 3 次失败阻断推送,新连接/网络重建解除阻断
* (MCU 单连接模型不得 net_close 数据 fd,会连监听一起毁掉)
* 2026-10-10 王建锋 I-1 门控 + I-2 兜底(Task #32 §9.3,销项 N-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 王建锋 N-32(B10 根治 P1,设计文档 §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
*/
#include "ui_net_sink.h"
#include <string.h>
#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 处理净成本) */
/* E-04(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
/* 屏幕尺寸与帧大小(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; /* 建立监听时的网络代次号(变化 = 网络已重建) */
static uint8_t s_send_fail_run; /* 连续发送失败计数(E-04:0 = 正常) */
static uint8_t s_sink_blocked; /* 推送阻断标志(1 = 停发帧,等客户端重连解除) */
/*
* 兜底收帧节拍(门控失效保险,2026-10-10 I-2):
* 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; /* 兜底计数器(单写者 = 推流宿主任务) */
/*
* 函数功能:计算 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 异或;
* 无连接时直接返回
* E-04(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);
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_W);
put_u16(init + 2, UI_H);
put_u32(init + 4, UI_MEMSIZE);
(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_MEMSIZE) == 0) {
/* 应答数据段 = [偏移u32(回填请求原值)] + [1bpp 位图 3200B] */
memcpy(g_lcdmem_data, data, 4);
(void)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. 无客户端时**先门控**(net_listener_armed,纯 RAM 读)再 accept,成功则复位状态机。
* 门控判据 = "监听器还在服役"(LISTENING 或 CLOSED+auto_relisten)而非
* "本轮必然有连接"——后者会让断连待重建窗口内永不进 net_accept,
* 把 CONNECT 事件未收到的补 accept 探测永久挡在门外(N-32)
* 4. 有客户端时**先门控**(net_can_read)再非阻塞 recv 逐字节喂入状态机;
* 门控判为"无数据"时仍每 RM_LIVENESS_DIV 轮无条件收一次(兜底)
* 5. 对端关闭/真错误时关闭客户端。**不再自行重建监听**(N-32 不变量 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;
}
g_client = -1;
rm_parser_reset();
s_send_fail_run = 0U; /* 网络重建:失败累计清零、解除推送阻断(E-04) */
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;
}
/* 无客户端时尝试接受连接(N-32 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);
if (c >= 0) {
g_client = c;
rm_parser_reset();
s_send_fail_run = 0U; /* 新连接:失败累计清零、解除推送阻断(E-04) */
s_sink_blocked = 0U;
s_liveness_ctr = 0U; /* 新连接:兜底节拍重新起算 */
}
return;
}
/* 有客户端时非阻塞收帧(I-1 门控 + I-2 兜底;自愈收敛阶段 1.5: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));
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();
/* N-32(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) {
/* N-32(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 模拟器):数据通道独立,需显式关闭 */
}
g_client = -1;
rm_parser_reset();
/* N-32(P1 不变量 II):同 EOF 分支——不再自行 bind_listen,
* 重建交回 net 层(recv 的错误分支同样会走 relisten)。仅 fd 真消失
* 时置 -1 交回主循环走 start */
if (net_get_sock(g_listen) == NULL) {
g_listen = -1; /* fd 真消失:交回主循环重试 */
}
}
}
}