Files
STM32F4-Base/Drivers/BSP/UI/ui_net_sink.c
T
wandering1 5a9988b557 chore(注释): 清掉源码里不可检索的过程编码
清掉的(读者无从解析、且所指文档已不存在)
- 已删任务文档的编号:Task #32、其内部 §5 / §4.3、P1-5、P0-2、P1-4a、
  "方案 a"、"Lead 批准 2026-10-12"
- 正文里当代词用的文件版本号:"(v2.7 起为 AboveNormal)""已随 v2.0 事件
  驱动改造删除""(v2.0:DMA 消费)""见文件头 v1.3 说明"等 → 改写成陈述现状
- 测试与脚本同类:adc_input 桩头"v2.0:DMA 模型"、stub/adc.h 的 v2.0、
  check_cubemx_survival.py 段头的"2026-10-10 v2.8"、run_all_suites.py 的"方案 A"

刻意保留
- 可检索登记簿锚点:P-AUDIT 真条目(P0-1/P1-8/P2-1/P2-9/P3-11)、S6/S11、
  E-31/E-44、N-18/N-23/N-32、B1/B5/B7/B8、L1-x~L5-x 功能号
- 文件头修改记录块内的 vN.M(规范 V1.0 §2.1/§2.2 要求保留该块;块内编号
  在本文件内自洽可读)

顺带纠正注释与代码已经不一致的过时事实
- 仍把 TIM6 写成现行节拍源:Src/main.c 的 include 注释与回调注释、
  Src/freertos.c 的 adcTask 块 → 改 TIM3;adc_input.h 修订条目里"ADC1 只认
  TIM2/TIM3/TIM8 的 TRGO"不准确 → 换成指向判据(ADC采样节拍设计说明 §3.2)
- App/algo/phasor.c 双区注释称"唯一消费者是 adcTask、为后续保护任务预留",
  实际 protectTask 已跨上下文读(prot_elm_run -> phasor_get)→ 改为"读者跨
  adcTask 心跳与 protectTask 判据两个上下文,勿改加锁、勿取消双区",并同步
  修正 docs/算法/采集与算法层设计说明.md 同一句
- adc_tick_irq_notify() 的"拒绝 ISR 内计算"理由仍写"内含 2 ms 忙等"(DMA 化
  后已无忙等)→ 换成三条真实理由:任务级共享数据 / 计算量 / 生成区回调
- 裸 §5(指代不明)与"规划 §1.6 / 规划 §4"昵称式 → 写成完整路径紧邻式

字节卫生(Edit 会静默剥 BOM)
- App/task/ui_task.c 与 Drivers/BSP/UI/ui_net_sink.c 被剥掉 BOM 两次,均按
  3 字节 EF BB BF 前缀补回并复验;全部 20 个文件 CR=0、U+FFFD=0、无行尾空白

门禁
- @build 0 错误 0 警告,且 Program Size 与清理前逐项相同
  (Code=213356 RO=18580 RW=1184 ZI=131480)→ 纯注释改动、中文未被误重编码
- ui-golden 首跑红:新工作树缺被 gitignore 的黄金参考 simulator/tests/ref/
  (不入库、只在主工作树生成过);从主工作树复制该基线后复跑通过 → 注释改动
  零像素影响
- 全套 pre-commit 在这 20 个文件上 Passed(coding-standard / cppcheck /
  6 个主机单测 / doc-links / task-table / mem-numbers / cubemx-survival /
  registration-check / mem-budget / font-charset / ui-golden / id-check)
- id-check 可判定比例 82.71% -> 82.88%(删掉的是"形似 P-AUDIT 但查无定义"的
  假锚点),悬空引用 863 -> 854
2026-10-21 11:39:53 +08:00

606 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 兜底(销项 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 "ui_tokens.h" /* 屏幕几何令牌(UI_SCREEN_W/H、UI_FB_SIZE 权威在 ui_port.h) */
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
#include "net_latency_test.h" /* M3 探针:prod_cycle_mcu(LCDMEM 处理净成本) */
/* 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
/* 屏幕尺寸与帧大小取 ui_tokens.h / ui_port.h 的屏幕令牌(160x160 1bpp = 3200 字节),
* 本模块不再自带同值字面量——显示几何与协议几何必须是同一个事实 */
#define RM_MAX_RX_PAYLOAD 256 /* 客户端→服务端 payload 上限,防长度字段损坏引发失步 */
#define RM_RX_BUF 64 /* 接收暂存区:整块 recv 后逐字节喂入解析状态机 */
static uint8_t g_tx[4 + 4 + UI_FB_SIZE + 1]; /* 发送组帧缓冲:[TAG][CMD][LEN][DATA][CRC] */
static uint8_t g_lcdmem_data[4 + UI_FB_SIZE]; /* LCDMEM 应答缓冲:4B 偏移 + 1bpp 位图 */
static uint8_t g_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_SCREEN_W);
put_u16(init + 2, UI_SCREEN_H);
put_u32(init + 4, UI_FB_SIZE);
(void)remo_send(CMD_INIT, init, 8);
}
/*
* 函数功能:处理一帧完整的远端命令(客户端→设备),校验通过后按命令分发执行
* 入口参数:f - 完整帧 [TAG][CMD][LEN][DATA][CRC](数据段自偏移 4 起)
* n - 帧总长度(字节)
* 返回值:无
* 限定条件:f 指向至少 n 字节可读空间;由解析状态机 rm_parser_feed 在 CRC 通过后调用
* 函数说明:1. 前置校验(任一失败即静默丢弃:不应答、不断连接):
* a) n >= 5:最短合法帧 5 字节(LEN=0 时 TAG/CMD/LEN_H/LEN_LO/CRC)
* b) 4+len+1 <= n:长度字段与帧长一致,防长度损坏引发越界读
* c) CRC 与数据段逐字节异或一致(仅覆盖数据段,不含头与 CRC 自身)
* d) TAG 必须为 TAG_CLIENT(0xAA):服务端只收客户端方向帧
* 状态机已做过等价校验,此处复核使本函数自包含(可独立调用)
* 2. 按命令分发:
* KEEPLIVE(0) - 原样回显空心跳帧,维持连接保活
* INIT(1) - 回送屏幕几何 [宽u16][高u16][帧字节数u32](大端,6 字节)
* KEY(2) - 数据段首字节按键码经平台后端注入 UI,回空帧作确认
* LCDMEM(3) - 数据段为 4B 大端偏移;偏移 0=整帧,经平台后端取 1bpp
* 位图后回送 [偏移u32][位图 3200B],非 0 偏移暂不支持
* 3. 未知命令码静默丢弃
*/
static void remo_handle(const uint8_t *f, int n) {
uint8_t tag;
uint8_t cmd;
uint16_t len;
const uint8_t *data;
uint8_t crc;
/* a) 最小帧长:TAG+CMD+LEN_H+LEN_LO+CRC 共 5 字节,不足必是残帧/噪声 */
if (n < 5) {
return;
}
/* 解析帧头:TAG=方向(0xAA 客户端帧 / 0xBB 服务端帧),CMD=命令码,
* LEN=数据段长度(16 位大端),数据段自偏移 4 起 */
tag = f[0];
cmd = f[1];
len = (uint16_t)(((uint16_t)f[2] << 8) | (uint16_t)f[3]);
data = f + 4;
/* b) 长度一致性:整帧长须恰为 4+len+1,防长度字段损坏导致越界读帧缓冲 */
if ((int)(4 + len + 1) > n) {
return;
}
/* c) CRC 校验:数据段逐字节异或(LEN=0 时恒 0)须与帧尾一致,坏帧静默丢弃 */
crc = f[4 + len];
if (crc != remo_crc(data, len)) {
return;
}
/* d) 方向校验:只接受客户端帧 0xAA;0xBB 为服务端→客户端方向,出现在收包路径即非法 */
if (tag != TAG_CLIENT) {
return;
}
switch (cmd) {
case CMD_KEEPLIVE:
/* 心跳保活:原样回显一个空 KEEPLIVE 帧,维持连接活性 */
(void)remo_send(CMD_KEEPLIVE, NULL, 0);
break;
case CMD_INIT:
/* 查询屏幕参数:回送 [宽u16][高u16][帧字节数u32](大端,共 6 字节) */
remo_send_init();
break;
case CMD_KEY:
/* 远端导航按键:数据段首字节为按键码,经平台后端注入 UI 按键队列 */
if (len >= 1) {
ui_net_sink_inject_key(data[0]);
}
/* 回空帧作“已接收”确认(LEN=0,CRC=0) */
(void)remo_send(CMD_KEY, NULL, 0);
break;
case CMD_LCDMEM: {
/* 读取显存:数据段为 4 字节大端起始偏移,不足 4 字节判非法丢弃 */
if (len < 4) {
break;
}
{
/* 偏移按大端 32 位拼装(协议约定多字节字段大端) */
uint32_t off = ((uint32_t)data[0] << 24) |
((uint32_t)data[1] << 16) |
((uint32_t)data[2] << 8) |
((uint32_t)data[3]);
/* 目前仅支持偏移 0(整帧)请求;非 0 偏移暂不支持,静默忽略 */
if (off == 0) {
#if defined(TEST_LATENCY_PROBE)
/* M3 探针 prod_cycle_mcu:从"请求已解析完"到"应答交入芯片"
* 的 MCU 净成本(1bpp 取帧 + 组帧 CRC + net_send 队列/芯片)。
* 不含 uiTask 10ms 轮询拾取拍(由 peer 侧 cycle 与 L11 覆盖)。 */
lat_tmark_t tm;
lat_mark(&tm);
#endif
/* 整帧请求:经平台后端取 1bpp 位图(MCU: ui_port_get_framebuffer /
* 模拟器: sim_port_get_screen_mono)写入应答缓冲尾部,失败则静默丢弃 */
if (ui_net_sink_get_lcdmem(g_lcdmem_data + 4, UI_FB_SIZE) == 0) {
/* 应答数据段 = [偏移u32(回填请求原值)] + [1bpp 位图 3200B] */
memcpy(g_lcdmem_data, data, 4);
(void)remo_send(CMD_LCDMEM, g_lcdmem_data, (uint16_t)(4 + UI_FB_SIZE));
}
#if defined(TEST_LATENCY_PROBE)
lat_probe_add_seg("prod_cycle_mcu", lat_mark_us(&tm));
#endif
}
}
break;
}
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 兜底;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 真消失:交回主循环重试 */
}
}
}
}