fix(log,net): 处置主机单测照出的 10 条现状,撤销 1 条误报

用户裁定「全部修」。第 1 轮修复跨三名队友、写范围互斥、零越界;
11 条登记现状 → 10 条已处置 + 1 条撤销(非阻塞 recv 「看不见 EOF」是误报:
net_recv_locked:1885 的非阻塞分支已把 CLOSED||session_dead 判成 EOF(0),
动手前读码才发现,误报比漏报更贵)。

存储与日志(App/log/sys_log_mcu.c、soe_store.c、cfg_file_store_mcu.c、sys_log.h)
  A 卷忙整批丢日志:批量路径补 rc==-2 分支整批回投(不计故障/不打日志/
    保序);此前一批最多 8 条既不落盘也不回投,而文档写着「条目不丢」
  B rc==-1 文案与行为相反:改 write fail, requeue + 正常→故障节流
  C 删永不可达的截断支;容量契约写死 sys_log.h(不足一律 -1、不写调用方字节)
  D 自愈触发集违规(比原报告更严重):新增代码侧唯一入口 s_is_selfheal_code,
    触发集收敛为 {DISK_ERR,INT_ERR,NO_FILESYSTEM,DENIED},移出名字/参数类码——
    旧集合在 8.3 同名同因下每轮删一次自己的历史日志且永不自愈,正是存储架构
    §10.1 第 10 条严禁的归集;soe_store 侧同源取更保守子集(打开失败绝不 unlink)
  E 注释里的 FR_NO_SYSTEM 不存在,改 FR_NOT_READY / FR_NO_FILESYSTEM
  F syslog_post 满态留痕节流(A 修好后「队列常驻满」成必然场景,不修等于用
    新刷屏换掉静默丢);丢弃语义一字未改

NET(net_socket.c/.h、net_types.h、net_socket_sim.c、lftpd.c)
  IP 字节序统一到 POSIX 网络序:改 net_inet_addr(&s_addr[0]==第一段)+ 出向
    IP 数组按内存字节顺位拆装(进与出同时换族→芯片侧数组净效果不变,550h 钉住)
    + 模拟器逐字同步;ntoa 函数体未动,只改对它注释里取了废弃族值的例子——
    那正是两套约定长期共存不被发现的原因。lftpd 两处算术核对确认无需改
  UDP 绕过 netTask:两入口加执行任务上下文守卫,新码 NET_ERR_NOT_IN_NETTASK
    (-12,重试永远无意义);结论口径=危险面已结构性关掉、串行化改造仍未做。
    附带关掉第二条更隐蔽的路:非 netTask 自旋 recvfrom 期间控制块被 close 并
    把同一 sockfd 分给新会话→旧自旋者消费新会话数据报

控制台(console_log.c/.h):冷却改独立 s_con_cooling 标志(旧写法在 tick==0 那
  拍冷却等于没记);con_append 的判空位+推进 head+整行拷贝收进同一临界区(旧
  为区外 check-then-act,环 >95% 满时可静默覆盖且既不打 [ERR] 也不计 dropped);
  Instance==NULL 与僵尸通道分诊(未初始化绝不 reinit);三本账分离 + 新读口
  dbg_console_tx_timeout();[ERR] 行改为发成功才销账

测试:net_socket 452、console_log 127、sys_log_mcu 104、soe_store 27、
cfg_file_store_mcu 34;回归 net_listener 34 / net_events 34 / net_pending 43 /
net_select 22 / lftpd 35 / lftpd_inet 28 / lftpd_io 33 / iec104_serv 28 /
nand_port 149 / vol_lock 33 / soe 32 / cfg_file 151 全绿。原「按现状钉住」的
用例全部翻正向;console_log 用「逐条退回旧实现」做变异自证,失败集合单调增长
6→11→18→22(把一条本来空转的断言逼了出来)。
游标抬升:TC-NETSOCK-545→560、TC-SYSLOG-100→107、TC-CONLOG-135→167。

文档:控制台设计说明 §4.1 口径与 §13-O2 销项 + v1.5;运行与自检记录 §4.4/§4.5
四条;存储架构 §10.1 第 10 条推论(指向 s_is_selfheal_code);TRAP-NET-21 后续
条目;BSD 指南新增 §9.0 s_addr 唯一约定;覆盖率口径 §9「第 1 轮修复处置」表 +
后续项 5 条 + 台架复验 7 条 + 新坑 8 条 + 变更记录。

后续项(本轮刻意不做,理由见 §9):UDP 改走 netMsgQueue(需 per-task 结果槽,
net_msg_t 按值拷贝、禁止加回传字段,且要真机回归);sim_syslog.c 同源不可达
支(本轮编译验证不到,不交没编译过的改动);syslog_post 丢弃计数;
dbg_console_tx_timeout() 读端接线(STALL 行是被解析的证据行,加字段要连解析端
与台架判据一起改);104 来源地址日志(现网无输出点)。

gate: @build 0 errors 0 warnings(Code=213720 RO=18860 RW=1204 ZI=131500)。
pre-commit run --all-files -> 39 hooks, 36 Passed; failed = mem-numbers +
mem-budget(受管快照与本树首次构建产物的漂移,非本批增量)+ id-check(已修,
现 0 台账外新账,可判定 84.63% > 基线 82.6%)。实测水位 RW_IRAM1 57.4%、
RW_CCM 87.9%,均低于 92% 红线;RAM/Flash 本批增量为数个 static 变量。
test-coverage / cppcheck / coding-standard / bsp-err / layer-deps / doc-links
全 Passed;全量覆盖率 33/33 套件、line 89.3%、func 92.3%、逐文件下降 0。
按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
This commit is contained in:
2026-10-24 19:43:56 +08:00
parent 794906cdc8
commit 5a806d8a26
29 changed files with 1690 additions and 278 deletions
+13 -1
View File
@@ -18,6 +18,10 @@
* dhara journal 竞争实证);busy 返回 -1 由调用方
* 按既有失败语义处理(poll 保留 RAM/保存待重试),
* 本任务 = 看门狗喂狗点,绝不阻塞等锁
* v1.5 2026-10-24 王建锋 仅注释更正:cfg_store_read 的"限定条件"引用了
* FRESULT 里不存在的 FR_NO_SYSTEM,改成实际会返回的
* FR_NOT_READY / FR_NO_FILESYSTEM 并写明"非 FR_OK 一律
* 转 -1,不要枚举错误码集合"(无行为改动)
* v1.4 2026-10-05 王建锋 原子写临时文件由 p_path 推导(P3-2):与目标同目录的
* 固定 8.3 名 config.TMP,不再硬编码卷根;目标恒为
* "/config.ini" 时推导结果与旧值逐字节一致
@@ -47,7 +51,15 @@ static const char s_path_tmp[] = "/config.TMP";
* cap - 缓冲容量 size_t
* p_nread - 实际读字节数输出(不可为 NULL) size_t *
* 返回值:0 - 成功;-1 - 文件缺失/读失败 int
* 限定条件:卷已挂载(未挂载时 f_open 返回 FR_NO_SYSTEM,转 -1)
* 限定条件:卷已挂载。未挂载时本函数一律转 -1,但**别照着不存在的错误码去写
* 判据**:Lib/FatFs/ff.h 的 FRESULT 共 20 个码(FR_OK..FR_INVALID_PARAMETER),
* 没有 FR_NO_SYSTEM(旧注释此处笔误,2026-10-24 更正)。本移植里卷未挂载
* 实际返回 FR_NOT_READY(3)(物理驱动器不可用)或 FF_VOLUMES 内无已挂载卷时
* 由 mount_volume 直接给错;卷上无有效 FAT 则返回 FR_NO_FILESYSTEM(13)。
* 本函数(以及 write_atomic / stat / remove)对**任何**非 FR_OK 一律转 -1,
* 所以这些码的差异不影响对外语义——也正因如此,谁都不该去枚举错误码集合,
* 除了自愈触发集那种"必须区分能不能靠删文件修好"的场合(见 sys_log_mcu.c
* 的 s_is_selfheal_code)。
* 函数说明:FIL 为**函数级 static**而非局部——本 FatFs 移植 FF_FS_TINY=0,
* `FIL` 内含 `buf[FF_MAX_SS]`=512 B(Lib/FatFs/ff.h),一个局部对象就吃掉
* 528 B 栈,是配置/syslog 调用链栈深的主要来源(2026-10-10 栈预算整改)。
+98 -33
View File
@@ -31,6 +31,17 @@
* adcTask(B1)等周期任务此时落日志会与 defaultTask 的存储初始化
* 日志字节级交错(台架实测 [ADC_TASK] 首帧行与 [NAND_FTL] 行交错)。
* 提供就绪查询供周期任务门控自身日志,不改直发路径本身。
* 2026-10-24 王建锋 主机单测(test/host/console_log)照出的四条一并修复:
* 冷却改判独立标志 s_con_cooling(旧写法拿 tick 值当"是否在冷却"哨兵,
* 故障恰好落在 HAL_GetTick()==0 那一拍时冷却等于没记);
* con_append 的"复核空位 + 整行拷贝 + 推进 head"收进同一临界区(旧写法
* 判据在区外,多生产者可在环将满时同时过判据 -> head 越过容量把未发出
* 的旧行静默覆盖,既不报错也不计丢弃);
* drain 失败分诊:Instance==NULL(未初始化)不再走外设软复位自愈(那会
* 在 HAL 未就绪时重配 RCC/GPIO),改记 dropped;僵尸通道仍自愈;
* 三本账分开(docs §13-O2):新增 dbg_console_tx_timeout(),dropped 只记
* 真丢的,tx_fault 仍只记软复位自愈次数;[ERR] dropped 行改为"发成功才
* 销账",通道不可用期间不再把丢弃证据一起冲掉。
*/
#include "console_log.h"
@@ -89,10 +100,12 @@
static uint8_t s_con_ring[DBG_CON_RING_SIZE];
static volatile uint16_t s_con_head; /* 生产者写入位置 */
static volatile uint16_t s_con_tail; /* 写者读取位置 */
static volatile uint32_t s_con_drop; /* 因缓冲满丢弃的行数 */
static volatile uint32_t s_con_drop; /* 真没落地的丢弃数(一次一行/一块) */
static volatile uint32_t s_con_tx_fault; /* TX 失败(僵尸通道)次数,均经外设软复位自愈 */
static volatile uint8_t s_con_tx_fault_log; /* TX 故障留痕待上报(由写者任务在 drain 之外补记 run.log) */
static volatile uint32_t s_con_cool_until; /* 故障冷却截止 tick(0 = 无冷却) */
static volatile uint32_t s_con_cool_until; /* 故障冷却截止 tick(只存时刻,不再兼作哨兵) */
static volatile uint8_t s_con_cooling; /* 1 = 正在故障冷却(独立标志:tick 恰好等于 0 的那一拍也必须算在冷却) */
static volatile uint32_t s_con_tx_timeout; /* 发送块超时次数(写者排空所见,与丢弃账分开) */
static volatile uint8_t s_con_draining; /* 1 = 处于 drain 中(重入保护) */
static volatile uint8_t s_con_writer_up; /* 1 = 写者已注册(此后走环形缓冲) */
static volatile uint8_t s_con_dwt_up; /* 1 = DWT CYCCNT 已使能(硬上界基准可用) */
@@ -179,7 +192,7 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) {
con_dwt_ensure();
if (huart1.Instance == NULL) {
return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位 */
return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位(不是超时,不记超时账) */
}
if (s_con_dwt_up == 0U) {
return con_tx_counted(huart1.Instance, p_buf, len);
@@ -190,14 +203,14 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) {
for (i = 0U; i < len; i++) {
while ((u->SR & USART_SR_TXE) == 0U) {
if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) {
return HAL_TIMEOUT; /* 有界:本块放弃(调用方自愈) */
return HAL_TIMEOUT; /* 有界:本块放弃(记账由调用方按返回值做) */
}
}
u->DR = (uint16_t)p_buf[i];
}
while ((u->SR & USART_SR_TC) == 0U) {
if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) {
return HAL_TIMEOUT;
return HAL_TIMEOUT; /* TC 超时:字节已交给发送器,调用方分账 */
}
}
return HAL_OK;
@@ -281,13 +294,16 @@ void dbg_console_drain(void) {
/* 故障冷却:僵尸通道(ORE 重锁)秒内不自愈,冷却期内不发送——
* 本轮 drain 直接让位主循环,保住喂狗节奏(gen-288 停喂误复位的
* 直接诱因 = 背靠连发的块超时链)。无符号差值跨 49.7 天回绕安全 */
if (s_con_cool_until != 0U) {
* 直接诱因 = 背靠连发的块超时链)。是否在冷却看 s_con_cooling 独立标志,
* **不再拿 tick 值当哨兵**(2026-10-24 实测:故障恰好落在 tick==0 那一拍时,旧写法
* 记出的 cool_until==0 等于没记冷却,下一轮立刻再耗一次 100ms 块超时)。
* 无符号差值跨 49.7 天回绕安全 */
if (s_con_cooling != 0U) {
if ((uint32_t)(t0 - s_con_cool_until) < DBG_CON_FAULT_COOL_MS) {
s_con_draining = 0U;
return;
}
s_con_cool_until = 0U;
s_con_cooling = 0U;
}
while ((used = con_used()) != 0U) {
@@ -309,12 +325,28 @@ void dbg_console_drain(void) {
taskEXIT_CRITICAL();
if (st != HAL_OK) {
if (st == HAL_ERROR) {
/* 2026-10-24 修复:USART1 **未初始化**(huart1.Instance == NULL,
* 只可能出现在 HAL_Init 早期或断言/故障上报路径)与"僵尸通道"是两回事:
* 这里**绝不做外设重初始化**——那会在 HAL 尚未就绪时跑 MspInit 重配
* RCC/GPIO;也不计 tx_fault(那本账的语义是"软复位自愈次数")。
* 本块字节确实一个都没上线 -> 计入 dropped(只记真丢的),
* 末尾的 [ERR] dropped 行负责留痕;本轮到此为止(失败即止)。 */
s_con_drop++;
break;
}
/* 僵尸通道:软复位自愈 + 进入发送冷却(ORE 毫秒级重锁,
* 继续背靠连发只会把主循环耗在块超时链上);本块丢弃
* (行已出环,不可重放) */
* (行已出环,不可重放),本块在超时账与丢弃账上各记一笔 */
s_con_tx_timeout++; /* 新增账:块超时次数(TXE 与 TC 两条硬上界
* 都算,口径 = 写者排空时观察到的块超时) */
con_uart_reinit();
s_con_tx_fault++;
s_con_drop++; /* 确认不了的块算真丢(一次丢弃事件),
* 旧写法只涨 tx_fault -> 账上看不出日志在丢,
* docs §13-O2 记的正是这条 */
s_con_cool_until = (uint32_t)HAL_GetTick();
s_con_cooling = 1U;
if (s_con_tx_fault == 1U) {
static const char note[] = "\r\n[WARN] CONSOLE tx fault: UART reinit (self-healed)\r\n";
@@ -344,14 +376,21 @@ void dbg_console_drain(void) {
uint32_t d = s_con_drop;
int n;
s_con_drop = 0U;
n = snprintf(note, sizeof(note), "\r\n[ERR] CONSOLE dropped %lu lines\r\n",
(unsigned long)d);
if (n > 0) {
if (n > (int)sizeof(note) - 1) {
n = (int)sizeof(note) - 1;
}
dbg_console_write_raw(note, (unsigned)n);
/* 只有**真发出去了才销账**(2026-10-24 一并修正):旧写法先把计数清零
* 再裸发,通道不可用/僵尸时那一发也失败 -> 丢弃证据被一起冲掉,[ERR] 行
* 永远出不来("永不静默"变成"永不显示")。留到下一轮或串口恢复后补发。 */
if (con_tx_bounded((const uint8_t *)note, (uint16_t)n) == HAL_OK) {
/* 按"本次报出去的这条"扣账而不是清零:期间生产者新丢的行留下轮报。
* 递减只发生在这一处(单写者 + s_con_draining 重入保护),生产者只增,
* 故 s_con_drop >= d 恒成立,无需再写下溢分支(写了也永远测不到)。 */
s_con_drop -= d;
}
}
}
}
@@ -360,6 +399,8 @@ void dbg_console_drain(void) {
* 函数功能:把一段文本放入控制台(写者已注册)/直接发出(未注册或中断上下文)
* 返回值:0 - 已入缓冲或已直发;-1 - 缓冲满丢弃(计入 dbg_console_dropped)
* 函数说明:生产者不持锁等待过久:最多 DBG_CON_WAIT_MS,仍满则丢弃该行;
* "复核空位 -> 整行拷贝 -> 推进 head"是一个原子的临界区动作;
* 环满等待(vTaskDelay / DWT 判据)一律在临界区之外,区内绝不睡眠。
* 写者自身记录日志时先排空缓冲,避免自等。
* 等待上界优先用 DWT 真实时间(CON_APPEND_WAIT_CYC,时间源无关)——
* tick 停摆时 vTaskDelay 本身也会停,故不能用它作唯一判据(round 8b)。
@@ -392,10 +433,37 @@ static int con_append(const char *buf, uint16_t len) {
}
for (;;) {
uint16_t used = con_used();
uint16_t free_space = (uint16_t)((DBG_CON_RING_SIZE - 1U) - used);
uint16_t free_space;
int fitted = 0;
/* 复核空位 + 整行拷贝 + 推进 head **必须在同一个临界区内**(2026-10-24 修复):
* 旧写法先出区判 free_space >= len 再进区写,两个生产者可在环 >95% 满时同时过
* 判据,head 合计越过容量 -> con_used() 回绕变小 -> 后写的行覆盖尚未发出的旧行,
* 且既不报错也不计丢弃(静默损坏)。本区最长拷 192B(168MHz 亚微秒级),相对
* 115200 波特下一行约 16ms 可忽略;等待(vTaskDelay/DWT 判据)刻意留在区外。 */
taskENTER_CRITICAL();
free_space = (uint16_t)((DBG_CON_RING_SIZE - 1U) - con_used());
if (free_space >= len) {
uint16_t head = s_con_head;
uint16_t first = (uint16_t)(DBG_CON_RING_SIZE - head);
uint16_t i;
if (first > len) {
first = len;
}
for (i = 0U; i < first; i++) {
s_con_ring[head + i] = (uint8_t)buf[i];
}
for (i = first; i < len; i++) {
s_con_ring[(uint16_t)((uint16_t)(head + i) & (DBG_CON_RING_SIZE - 1U))] =
(uint8_t)buf[i];
}
s_con_head = (uint16_t)((uint16_t)(head + len) & (DBG_CON_RING_SIZE - 1U));
fitted = 1;
}
taskEXIT_CRITICAL();
if (fitted != 0) {
break;
}
if (have_dwt != 0U) {
@@ -411,26 +479,6 @@ static int con_append(const char *buf, uint16_t len) {
waited++;
}
taskENTER_CRITICAL();
{
uint16_t head = s_con_head;
uint16_t first = (uint16_t)(DBG_CON_RING_SIZE - head);
uint16_t i;
if (first > len) {
first = len;
}
for (i = 0U; i < first; i++) {
s_con_ring[head + i] = (uint8_t)buf[i];
}
for (i = first; i < len; i++) {
s_con_ring[(uint16_t)((uint16_t)(head + i) & (DBG_CON_RING_SIZE - 1U))] =
(uint8_t)buf[i];
}
s_con_head = (uint16_t)((uint16_t)(head + len) & (DBG_CON_RING_SIZE - 1U));
}
taskEXIT_CRITICAL();
if (sys_evtHandle != NULL) {
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_CONSOLE_TX); /* 唤醒写者 */
}
@@ -472,6 +520,23 @@ uint32_t dbg_console_tx_fault(void) {
return s_con_tx_fault;
}
/*
* 函数功能:已发生的发送块超时次数(诊断用)
* 返回值:块超时累计次数 uint32_t
* 限定条件:无(任意上下文可读,含 ISR)
* 函数说明:三本账分开(docs/子系统设计/控制台单写者日志设计说明.md §13-O2)——
* dbg_console_dropped() = **真没落地**的丢弃数(缓冲满丢的一行,或一个
* 没能确认送达而出环的块;[ERR] 行报的就是它);
* dbg_console_tx_fault() = 外设软复位**自愈次数**(语义不变);
* dbg_console_tx_timeout() = 本账:写者排空时观察到的**块超时**次数
* (TXE 与 TC 两条硬上界都算)。
* 裸发路径(内核前 / ISR / 两条告警行自身)的超时不记本账——那些上下文里
* 没有"块"的概念,也只可能发生在启动极早期,混进来只会让运维读数失去意义。
* UART 物理失效但字节已发出的情形只涨本账,不污染另两本。 */
uint32_t dbg_console_tx_timeout(void) {
return s_con_tx_timeout;
}
/*
* 函数功能:取走"控制台 TX 故障待上报"标志(读后清)
* 返回值:1 - 需要补记一条 run.log 证据,0 - 无
+8 -2
View File
@@ -39,8 +39,13 @@ extern "C" {
* (仅假设 defaultTask 在日志),其余任务此时落日志会与
* defaultTask 的存储初始化日志字节级交错(2026-10-10 台架实测)
* dbg_console_drain - 写者任务排空缓冲并发送(单次有界;TX 失败自动软复位自愈)
* dbg_console_dropped - 因缓冲满丢弃的行数(诊断)
* dbg_console_tx_fault - 因僵尸通道软复位自愈的次数(诊断)
* dbg_console_dropped - 真没落地的丢弃数:缓冲满丢的一行,或一个没能确认送达
* 而出环的块(诊断;[ERR] CONSOLE dropped 行报的就是它)
* dbg_console_tx_fault - 因僵尸通道软复位自愈的次数(诊断,语义不变)
* dbg_console_tx_timeout - 写者排空时观察到的块超时次数(TXE / TC 两条硬上界都算)。
* 三本账分开:看日志有没有在丢 -> dropped;看外设自愈了几次
* -> tx_fault;看 UART 物理失效(含"字节已发出但 TC 不置位")
* -> tx_timeout。别把三本账混成一个数(docs §13-O2 )
* dbg_console_tx_fault_log_take
* - 取走"TX 故障待上报"标志(读后清)。**必须**由写者任务在
* dbg_console_drain() 之外调用后自行 syslog_post——本模块
@@ -55,6 +60,7 @@ uint8_t dbg_console_writer_ready(void);
void dbg_console_drain(void);
uint32_t dbg_console_dropped(void);
uint32_t dbg_console_tx_fault(void);
uint32_t dbg_console_tx_timeout(void);
uint8_t dbg_console_tx_fault_log_take(void);
/*
+51 -5
View File
@@ -3,6 +3,16 @@
* 模块功能:实现 soe_store.h——protectTask 投递(取时标 + soe_rec_build +
* SPSC 待发环 / 消息队列,零 FS);defaultTask(syslog_drain 尾部)
* 排空落 /log/soe.log(64B 定长,容量满删除重建)。
* 修改记录:
* v1.0 2026-10-12 王建锋 创建初始版本(B9,设计见 docs/算法/继电保护功能复现规划.md §3.4)
* v1.1 2026-10-24 王建锋 仅注释/文档更正(D 项,无行为改动):s_soe_append
* 函数头原写"打开/写入结构性错误 → unlink 重试一次
* (同 run.log)",与实现不符且方向错误——实现里
* f_open 失败直接返回 -1、不 unlink,这是**有意**的:
* SOE 是事件序列,打开失败就删文件等于销毁已发生的
* 事件。改为如实描述"仅写失败才重试一次",并补上
* 与 sys_log_mcu.c s_is_selfheal_code 同源的逐码表
* (名字/参数类错误码两侧都绝不 unlink)
* 创建日期:2026-10-12(B9)
*/
@@ -88,6 +98,30 @@ void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value) {
}
}
/*
* ── 自愈触发集逐码依据(与 sys_log_mcu.c 的同名资料块同源;2026-10-24 逐条核对
* Lib/FatFs/ff.c)─────────────────────────────────────────────────────────
* 进触发集(run.log 侧允许 unlink 重建一次):介质/结构类 FR_DISK_ERR(1) /
* FR_INT_ERR(2) / FR_NO_FILESYSTEM(13) / FR_DENIED(7)。本模块**只在写失败路径**
* 上碰自己的文件——打开失败连这些码也不 unlink。
* 不算自愈:FR_NO_FILE(4)——带 FA_OPEN_APPEND 打开时文件会被直接建出来;返回本
* 码的唯一现实场景是同名对象是目录(ff.c:3854),unlink 修不好它。
* 两侧都绝不 unlink:名字/参数类 FR_INVALID_NAME(6) / FR_NO_PATH(5) /
* FR_INVALID_OBJECT(9),以及 FR_NOT_READY(3) / FR_NOT_ENABLED(12) /
* FR_TIMEOUT(15)——同名同因,删了也修不好,反而每轮丢一份历史(§10.1 第 10 条
* 严禁的正是这种归集)。
* 写失败路径不接这套判定:f_write 的返回码里不存在名字/参数类(能走到那里说明
* 已经打开成功),而簇分配失败是 FR_OK + 短写、根本没有错误码可判,只能无条件
* 重试一次。
* 为什么 SOE 比 run.log 保守(两侧资料块各写一份、互指,改动必须同步):run.log
* 是滚动运行日志,坏了重建损失有限;SOE 是**事件序列**,文件里是一条条已经发生
* 过的保护事件,"打开失败就删整个历史文件"等于**销毁事件证据**——SOE 的存在就
* 是为了留这些事件,删了比写不进去更糟。正确出路是等 syslog_mcu_mounted() 每 2s
* 重试补目录 / 等卷恢复,条目留在待发环与队列里,不丢。
* (run.log 侧的同一张表 + 同一理由见 App/log/sys_log_mcu.c 的同名资料块,测试
* 侧两侧对齐由 TC-SYSLOG-101/102 与 TC-SOESTORE-022/022b 分别钉住。)
*/
/*
* 函数功能:追加一条 64B SOE 记录到 /log/soe.log
* 入口参数:rec - 完整记录(SOE_REC_SIZE 字节) const uint8_t *
@@ -98,11 +132,20 @@ void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value) {
* ② FA_WRITE|FA_OPEN_APPEND(本移植 OPEN_APPEND 不含 WRITE,
* 缺写位 f_write 恒 FR_DENIED,2026-09-20 实证);
* ③ 判失败必须校验写回字节数(FR_OK+短写 = 失败,簇分配无错误码);
* ④ 容量满 unlink 重建;打开/写入结构性错误 → unlink 重试一次
* (L1 文件级自愈,同 run.log)。
* ④ 容量满 unlink 重建;**仅写入失败(含 FR_OK+短写)才 unlink 重试
* 一次**——f_open 失败一律直接返回 -1、不 unlink。
* (2026-10-24 定案:本条旧注释写的是"打开/写入结构性错误都 unlink
* 重试一次(同 run.log)",与实际实现不符,也与本模块的语义相反:
* run.log 是**滚动运行日志**,坏了重建损失有限;SOE 是**事件序列**,
* 文件里是已经发生的事件,打开失败就把整个历史文件删掉等于销毁
* 证据,方向是错的。正确出路是让 syslog_mcu_mounted() 每 2s 重试
* 补目录/等卷恢复,条目留在待发环与队列里,不丢。)
* ⑤ 自愈触发集与 sys_log_mcu.c 的 s_is_selfheal_code() 同源,但本模块取
* 更保守的子集:打开失败**任何错误码都不 unlink**,只有写失败才重试
* 一次。逐码依据与两侧差异见上方同名资料块。
* ⚠ 本模块**没有**任何"卷损坏"归类/重格式化路径:unlink 失败、
* 名字/参数类错误码一律只留痕(存储架构 §10.1 教训),条目丢弃
* 计数,绝不升级成卷操作。
* 计数,绝不升级成卷操作(全模块无 f_mount/f_mkfs/f_opendir)。
*/
static int s_soe_append(const uint8_t *rec) {
static FIL f; /* static:FF_FS_TINY=0 → FIL=528B 栈;
@@ -129,8 +172,9 @@ static int s_soe_append(const uint8_t *rec) {
fr = f_write(&f, rec, SOE_REC_SIZE, &bw);
(void)f_close(&f);
if ((fr != FR_OK) || (bw != SOE_REC_SIZE)) {
/* L1 文件级自愈:陈旧/损坏条目 → 删除重建、重试一次(同 run.log;
* unlink 返回值刻意不检查——失败也只是重试失败 → 留痕,不升级) */
/* L1 文件级自愈:**只有写失败**才删除重建、重试一次(打开失败走下面
* 的 else,不删——见函数头 ④)。unlink 返回值刻意不检查:失败也只是
* 重试失败 → 留痕计数,绝不升级成卷操作。 */
(void)f_unlink(SOE_PATH);
bw = 0u;
fr = f_open(&f, SOE_PATH, FA_WRITE | FA_OPEN_APPEND);
@@ -143,6 +187,8 @@ static int s_soe_append(const uint8_t *rec) {
}
}
} else {
/* 打开失败:只留痕、**不 unlink**(SOE 是事件序列,删文件 = 销毁历史;
* 名字/参数类错误码 unlink 也修不好,见函数头的逐码表) */
rc = -1;
}
if (rc == 0) {
+28 -5
View File
@@ -7,6 +7,13 @@
* 创建日期:2026-09-14
* 修改记录:
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4)
* v1.1 2026-10-24 王建锋 仅契约注释更正,与 App/log/sys_log_mcu.c 的 C/F 两项
* 整改同步(无签名、无行为改动):
* syslog_get_record 写死容量要求(ts_len > SYSLOG_TS_LEN、
* desc_len > SYSLOG_DESC_MAX)并明确"容量不足一律 -1、
* 不截断、不写调用方缓冲"——旧注释像是"给少了会被截断",
* 实际是整条读不到;syslog_post 写明必须假设丢弃存在、
* 且丢弃留痕已按通道节流(节流后无丢弃计数)
*/
#ifndef SYS_LOG_H
@@ -61,6 +68,14 @@ typedef enum {
* desc - 描述串(UTF-8,≤SYSLOG_DESC_MAX 字节;超长截断) const char *
* 返回值:无
* 限定条件:MCU 端 FreeRTOS 启动前调用走静态待发数组(由 defaultTask 挂载后落盘)
* 函数说明:调用方**必须假设投递可能被丢弃**——MCU 后端(sys_log_mcu.c)队列 8 槽、
* 启动前待发数组 8 槽,卷被 ftpTask 长期占用时 defaultTask 会把整批回投
* 队列(队列因此可长时间处于满态),此时本函数直接丢弃该条,并只在
* "转入故障"的那一次留痕(丢弃留痕按正常→故障节流,F 项整改 2026-10-24;
* 节流后**丢弃总数没有计数**,要计量得另加计数器)。必须留住的证据走
* sys_log_evidence(),不要靠 syslog_post 重试。
* 模拟器后端(simulator/sim/sim_syslog.c)是单线程直写宿主文件、**没有
* 队列与丢弃**,因此上面这段满态语义只约束 MCU 端。
*/
void syslog_post(syslog_id_t id, const char *desc);
@@ -75,11 +90,19 @@ uint32_t syslog_count(syslog_id_t id);
* 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾)
* 入口参数:id - 日志 ID syslog_id_t
* idx - 记录序号(0 起) uint32_t
* p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节;不可为 NULL) char *
* ts_len- p_ts 容量 size_t
* p_desc- 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节;不可为 NULL) char *
* desc_len - p_desc 容量 size_t
* 返回值:0 - 成功;-1 - 越界/读失败 int
* p_ts - 时间戳输出缓冲(不可为 NULL) char *
* ts_len- p_ts 容量,必须 > SYSLOG_TS_LEN(即 ≥ SYSLOG_TS_LEN+1 = 20 字节) size_t
* p_desc- 描述输出缓冲(不可为 NULL) char *
* desc_len - p_desc 容量,必须 > SYSLOG_DESC_MAX(即 ≥ 29 字节) size_t
* 返回值:0 - 成功;-1 - 越界 / 读失败 / **缓冲容量不足** int
* 限定条件:容量不足一律返回 -1,**不做静默截断、也不写调用方一个字节**
* (C 项整改 2026-10-24;旧注释只写"≥…+1 字节"像是"给少了会被截断",
* 照它给 28 字节缓冲的结果是整条读不到而不是读到 27 字符)。
* 描述字段在写入侧已截到 SYSLOG_DESC_MAX,所以按上面的最小容量给缓冲
* 就一定装得下,读侧不需要第二道截断。
* MCU 后端与模拟器后端(sim_syslog.c)在这条守卫上取值相同,行为一致;
* 模拟器侧那份同样不可达的截断支还留在文件里(未列入本轮改动范围)。
* 调用方:uiTask 的日志页(行缓冲 32B)与 SYSLOG 取证打印。
*/
int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len);
+168 -23
View File
@@ -16,6 +16,24 @@
* v1.3 2026-10-02 王建锋 新增 sys_log_evidence():把看门狗停喂现场
* 证据落 /log/wdev.log(掉电不丢)+ flush 诱导
* (round 7 纯取证,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)
* v1.5 2026-10-24 王建锋 主机单测批次照出的缺陷一起修,本文件内 5 条
* (A/B/C/D/F;E 在 App/cfg/cfg_file_store_mcu.c):
* A 卷忙整批静默丢日志——syslog_drain 批量路径补
* rc==-2 分支把 done 置 0,整批回投队列(不计故障、
* 不打日志),与 -3 慢路径遇 -2 的处置对齐;
* B 同分支文案 "drop N entries" 与行为相反(其实全
* 部回投),改成如实描述并按"正常→故障"节流一条;
* C 删掉 syslog_get_record 里永不可达的截断支,容量
* 要求写进函数头与 sys_log.h(宁可拒绝不静默截断);
* D 自愈触发集按语义拆类(新增 s_is_selfheal_code),
* 把 FR_INVALID_NAME / FR_NO_PATH / FR_NO_FILE /
* FR_INVALID_OBJECT 移出 unlink 重建集合——旧集合在
* 名字/参数类错误下每轮删一次自己的历史日志且永不
* 自愈,违反存储架构 §10.1 第 10 条
* F syslog_post 的丢弃留痕按通道节流(队列满 / 启动前
* 数组满各立 volatile 抑制位,与 B 的 drain 侧那位分
* 开):卷被长期占用 + A 项回投让队列常驻满时,串口
* 不再按投递频率刷 "queue full, drop"
* v1.4 2026-10-04 王建锋 P2-9:syslog_mcu_mounted() 改为幂等 + 卷锁保护;
* defaultTask 每 2s 复查,建 /log 曾失败时自动重试
* (原"仅调用一次"→ 失败后 drain 永久 no-op)。
@@ -65,6 +83,22 @@ static osMessageQueueId_t s_queue; /* 运行期消息队列(s
static const osMessageQueueAttr_t s_queue_attr = { .name = "syslog" };
static volatile uint8_t s_fs_ready; /* 卷挂载 + /log 目录就绪后置位 */
static volatile uint8_t s_mount_fail_reported; /* P2-9:建目录失败日志去重(成功复位) */
/* B 项整改(2026-10-24):syslog_drain 批量写失败"整批回投"这条留痕的节流位——
* 只在"从正常转入故障"的那一次打一条,成功一轮后复位。与 s_append_many 内部的
* s_batch_fault 各管一层:那位压的是"这个文件写坏了",本位压的是"这批条目回投了"。
* 唯一写者 defaultTask,免锁。 */
static uint8_t s_requeue_reported[SYSLOG_COUNT];
/* F 项整改(2026-10-24,Lead 批准并入本轮):syslog_post 的丢弃留痕节流。
* 场景:卷被 ftpTask 长期占用 -> A 项修复后整批回投 -> 队列常驻满 -> 此后每一条
* 日志投递都会"队列满 + 打一条 DBG_ERROR",串口按业务投递频率刷屏(日志本身
* 变成负载)。与上面 s_requeue_reported **分开**是两个必然:那位归 defaultTask
* (唯一写者,免锁),本位由**任意上下文**的投递者读写,且恢复时机是"下一次投递
* 成功"而不是"下一批落盘成功"。两个位各管一个通道:队列 / 启动前待发数组。
* volatile + 单字节:Cortex-M 上对齐字节读写原子;跨任务竞态的最坏后果是"同一轮
* 故障多打一行"或"少打一行",不会漏丢条目、也不会长时间静默(下一次成功会复位)。 */
static volatile uint8_t s_post_qfull_reported; /* 队列满丢弃:已留痕? */
static volatile uint8_t s_post_pboot_full_reported;/* 启动前数组满丢弃:已留痕? */
/* N-24(2026-10-15,报告 N-16 读侧):8 条滑动读窗口 + 写者代数。
* 窗口 512 B 走静态(不吃任务栈;defaultTask/uiTask 最坏链见 stack_report.py,
@@ -80,6 +114,72 @@ static volatile uint32_t s_rd_gen[SYSLOG_COUNT]; /* 每次成功落盘 / unli
/* 函数定义区 */
/*
* ── 自愈触发集逐码依据(配套下方 s_is_selfheal_code;2026-10-24 逐条核对
* Lib/FatFs/ff.c,行号即该文件)────────────────────────────────────────
* 旧实现把 FR_INVALID_NAME / FR_NO_PATH / FR_NO_FILE / FR_INVALID_OBJECT 一并
* 放进触发集,违反《STM32F4-Base 存储架构说明》§10.1 第 10 条:名字/参数类错误
* 码严禁归入"卷损坏"处置集——这类码下 unlink 自己的文件必然同样失败(同名同因),
* 后果是每轮排空删一次自己的历史日志、且永远修不好(run.log 1024 条 /
* selftest.log 全是被删对象)。
*
* 进触发集(删了这个文件真能修好):
* FR_DISK_ERR(1) 低层 I/O 失败(ff.c:1683/1739/1785 等)——陈旧簇链或坏
* 目录项,unlink 后重建换新链有效;
* FR_INT_ERR(2) 内部一致性断言失败(ff.c:1379/1460/3869)——本文件的目录
* 项/簇号已不自洽,只能废弃重建;
* FR_NO_FILESYSTEM(13) 卷上找不到有效 FAT(ff.c:3483 起)——本模块**不**做任何
* 卷级动作(全模块无 f_mount/f_mkfs),只对本文件废弃重建
* 一次并留痕,卷怎么办归挂载门;
* FR_DENIED(7) 两条真实成因都能被 unlink 治好:目标被置只读属性
* (ff.c:3808/3859),以及目录区无空目录项(ff.c:1850 由
* FR_NO_FILE 改写而来)——后者删掉自己这个文件正好腾出一
* 个目录项。陷阱一那类"缺写位"的 FR_DENIED 出现在 f_write
* 上,不走本判定。
*
* 不进触发集(一律立即失败,不 unlink、不重开、不计卷损坏):
* FR_INVALID_NAME(6) 8.3 短名违规 / 非法字符 / 以点开头(非 LFN 配置下的
* create_name)——unlink 同名同因失败,永不自愈;
* FR_NO_PATH(5) 中间路径段缺失(ff.c:3144/4357)= /log 目录不在:重建
* 文件不解决目录,补目录归 syslog_mcu_mounted() 的每 2s 重试
* (见该函数注释里的幂等改造说明),两侧各管一层;
* FR_NO_FILE(4) 本模块用 FA_WRITE|FA_OPEN_APPEND(含 FA_OPEN_ALWAYS),
* "文件不存在"会被直接创建出来;返回本码的唯一现实场景是
* 同名对象是目录(ff.c:3854),unlink 一个目录修不好它;
* FR_INVALID_OBJECT(9) 仅 ff.c:3774 的 !fp 分支,即句柄指针为 NULL——本模块用
* 函数级 static FIL,地址恒非 0,属不可达码;
* FR_NOT_READY(3) / FR_NOT_ENABLED(12) / FR_TIMEOUT(15)——卷未挂载、无工作区、
* FF_FS_REENTRANT 取卷超时,属挂载门与并发范畴,与"这个
* 文件坏了"无关。
*
* 两点边界,免得后人当成漏改:
* 1) f_write 之后那条"失败/短写 -> unlink 重建重试一次"**不接**本判定:其前提
* 是已经打开成功,名字/参数类错误到不了那里;而簇分配失败是 FR_OK + 短写
* (根本没有错误码可判),必须无条件重试。
* 2) App/log/soe_store.c 对同一张表取**更保守的子集**:那边**打开失败连介质类
* 码也不 unlink**。原因不是它漏改,而是两个文件的对象不同——run.log 是滚动
* 运行日志,文件坏了重建损失有限;soe.log 是**事件序列**,里面是一条条已经
* 发生过的保护事件,"打开失败就删掉整个文件"等于**销毁事件证据**(SOE 的存
* 在即目的,删了比写不进去更糟)。所以 SOE 侧的出路是等卷/目录恢复(条目留
* 在待发环与队列里,不丢),只有"已经打开成功之后写失败"才允许废弃重建。
* 两侧资料块必须同步修改、互指(彼处在 App/log/soe_store.c 的同名资料块)。
*/
/*
* 函数功能:判定一个 f_open 返回码是否属于"把自己的日志文件删掉重建一次"能修好
* 的那一类,即 L1 文件级自愈的触发集(D 项整改,逐码依据见上方资料块)
* 入口参数:fr - f_open 的返回码 FRESULT
* 返回值:1 - 属触发集,可 unlink 后重开一次;0 - 不属,一律立即失败、不 unlink、
* 不计卷损坏 int
* 限定条件:纯分类、无任何副作用;调用点只有 s_append_rec() 与 s_append_many()
* 的"打开失败"分支
* 函数说明:拆成函数是为了两处调用点共用同一张表,避免历史上"改一处漏一处"
*/
static int s_is_selfheal_code(FRESULT fr) {
return ((fr == FR_DISK_ERR) || (fr == FR_INT_ERR) ||
(fr == FR_NO_FILESYSTEM) || (fr == FR_DENIED)) ? 1 : 0;
}
/*
* 函数功能:格式化一条 64 B 定长记录(时间戳 + 空格 + epoch + 空格 + 描述)
* 入口参数:id - 日志 ID(当前不参与编码,仅为接口对称) syslog_id_t
@@ -155,10 +255,9 @@ static int s_append_rec(syslog_id_t id, const uint8_t *p_rec) {
* FA_OPEN_APPEND(0x30) 不含 FA_WRITE(0x02),不带写位打开的文件 f_write
* 恒返回 FR_DENIED(7)——2026-09-20 台架实测 /log 写失败 ×5/开机 真根因 */
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
if (fr != FR_OK && (fr == FR_DENIED || fr == FR_INVALID_OBJECT ||
fr == FR_INT_ERR || fr == FR_DISK_ERR ||
fr == FR_NO_FILE || fr == FR_NO_PATH ||
fr == FR_INVALID_NAME || fr == FR_NO_FILESYSTEM)) {
/* D 项整改(2026-10-24):只有"删了这个文件能修好"的介质/结构类错误码才
* 进这里;名字/参数类一律不 unlink —— 判据见 s_is_selfheal_code() */
if ((fr != FR_OK) && (s_is_selfheal_code(fr) != 0)) {
(void)f_unlink(s_paths[id]);
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
}
@@ -255,10 +354,9 @@ static int s_append_many(syslog_id_t id, const uint8_t *p_buf, uint32_t n) {
total = (UINT)(n * SYSLOG_REC_SIZE);
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
if (fr != FR_OK && (fr == FR_DENIED || fr == FR_INVALID_OBJECT ||
fr == FR_INT_ERR || fr == FR_DISK_ERR ||
fr == FR_NO_FILE || fr == FR_NO_PATH ||
fr == FR_INVALID_NAME || fr == FR_NO_FILESYSTEM)) {
/* D 项整改(2026-10-24):只有"删了这个文件能修好"的介质/结构类错误码才
* 进这里;名字/参数类一律不 unlink —— 判据见 s_is_selfheal_code() */
if ((fr != FR_OK) && (s_is_selfheal_code(fr) != 0)) {
(void)f_unlink(s_paths[id]);
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
}
@@ -524,19 +622,36 @@ void syslog_drain(void) {
break; /* 写失败:该条已丢,尾部回投 */
}
}
} else if (rc == -1) {
} else if (rc == -2) {
/* A 项整改(2026-10-24,主机单测照出):卷忙 = **整批回投队列**,
* 静默顺延——不计故障、不打日志(本函数每 2s 排空一次,一打就刷屏),
* 与上面 -3 慢路径遇 -2 的处置对齐。旧实现在这里没有分支:done 保持 n
* → 下面的回投循环空转 → 最多 8 条既不落盘也不回投,静默丢失。 */
done = 0u;
DBG_ERROR("drop %lu entries id=%d", (unsigned long)n, (int)bid);
} else if (rc == -1) {
/* B 项整改(2026-10-24):旧文案写 "drop %lu entries",但本分支
* done = 0 → 整批其实**全部回投**、一条没丢(下轮重试),文案与行为
* 相反会把排障方向带错;改成如实描述,并加"正常→故障"节流(持续故障
* 时每 2s 一条会自己刷成新一轮刷屏,真丢另有下面 hold-over drop 兜底)。 */
done = 0u;
if (s_requeue_reported[bid] == 0u) {
s_requeue_reported[bid] = 1u;
DBG_ERROR("write fail, requeue %lu entries id=%d",
(unsigned long)n, (int)bid);
}
}
/* rc == -2(卷忙):整批回投队列,静默顺延,不计故障 */
for (i = done; i < n; i++) {
if (osMessageQueuePut(s_queue, &s_batch_src[i], 0u, 0u) != osOK) {
DBG_ERROR("hold-over drop (queue full): id=%d", (int)s_batch_src[i].id);
}
}
/* 本批消耗的 preboot 条目:已落盘的删除;未落盘的已转投队列,同样删除
* (否则会重复落盘) */
if (rc == 0) {
s_requeue_reported[bid] = 0u; /* 恢复正常:允许下一次故障重新留痕 */
}
/* 本批消耗的 preboot 条目:已落盘的删除;未落盘的(-1/-2/-3 尾部)已在
* 上面转投队列,同样从数组删除——**由队列接手重投**,不是丢弃
* (A 项整改前 -2 走不到回投,这里删掉就等于吞掉) */
if (n_pre > 0u) {
memmove(&s_preboot[0], &s_preboot[n_pre],
sizeof(syslog_entry_t) * (size_t)(s_preboot_cnt - n_pre));
@@ -558,6 +673,12 @@ void syslog_drain(void) {
* desc - 描述串(UTF-8,静态存储期,超长截断) const char *
* 返回值:无
* 限定条件:FreeRTOS 启动前走静态待发数组;desc 必须为静态/常量串
* 函数说明:丢弃留痕按"正常→故障"节流(F 项整改 2026-10-24):每个通道只在
* 转入故障的那一次打一条,恢复(下一次成功收下)后复位。
* **丢弃语义本身没变**(队列满/数组满仍然是丢),变的只有留痕次数——
* 卷被 FTP 长期占用时 A 项修复让整批回投、队列常驻满,若这里不节流,
* 串口就按业务投递频率刷"queue full, drop",日志自己变成负载。
* 抑制位是 volatile 单字节、由任意上下文读写:竞态最坏只多打/少打一行。
*/
void syslog_post(syslog_id_t id, const char *desc) {
syslog_entry_t e;
@@ -569,15 +690,33 @@ void syslog_post(syslog_id_t id, const char *desc) {
e.desc = desc;
if (s_queue != NULL && osKernelGetState() == osKernelRunning) {
if (osMessageQueuePut(s_queue, &e, 0u, 0u) != osOK) {
DBG_ERROR("queue full, drop: %s", desc);
/* F 项:只报"从正常转入故障"那一次;期间条目照常丢弃(丢弃语义不变,
* 变的只是留痕次数)。下一次投递成功即复位。 */
if (s_post_qfull_reported == 0u) {
s_post_qfull_reported = 1u;
DBG_ERROR("queue full, drop: %s", desc);
}
} else {
/* 恢复正常:清抑制位,下一次故障重新留痕。这里刻意无条件写
* (曾试成"仅置位时才清",被 cppcheck duplicateConditionalAssign
* 判成等价赋值并拦提交闸;本函数上方已做过一次条目拷贝,多一条
* STR 可以忽略,别为它加 inline-suppr)。 */
s_post_qfull_reported = 0u;
}
} else {
uint8_t n = s_preboot_cnt;
if (n < SYSLOG_QUEUE_MAX) {
s_preboot[n] = e;
s_preboot_cnt = n + 1u;
} else {
s_post_pboot_full_reported = 0u; /* 恢复正常:同上,无条件复位 */
} else if (s_post_pboot_full_reported == 0u) {
/* 同一纪律的另一通道:启动前数组只有 SYSLOG_QUEUE_MAX 槽,挂载慢 +
* 开机事件密集时会在整个"未就绪窗口"里每条投递打一行(比队列通道更早
* 发生、也更密集)。数组重新收下第一条即视为恢复。 */
s_post_pboot_full_reported = 1u;
DBG_ERROR("preboot full, drop: %s", desc);
} else {
/* 故障期间静默,条目照常丢弃 */
}
}
}
@@ -603,11 +742,14 @@ uint32_t syslog_count(syslog_id_t id) {
* 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾)
* 入口参数:id - 日志 ID syslog_id_t
* idx - 记录序号(0 起) uint32_t
* p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节,不可为 NULL) char *
* ts_len - p_ts 容量 size_t
* p_desc - 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节,不可为 NULL) char *
* desc_len - p_desc 容量 size_t
* 返回值:0 - 成功;-1 - 越界/读失败 int
* p_ts - 时间戳输出缓冲(不可为 NULL) char *
* ts_len - p_ts 容量,**必须 > SYSLOG_TS_LEN**(即至少 20 字节) size_t
* p_desc - 描述输出缓冲(不可为 NULL) char *
* desc_len - p_desc 容量,**必须 > SYSLOG_DESC_MAX**(即至少 29 字节) size_t
* 返回值:0 - 成功;-1 - 越界/读失败/缓冲容量不足(不截断、不写入调用方缓冲) int
* 限定条件:容量不足一律拒绝而不是截断(C 项整改 2026-10-24;理由见函数体注释)。
* 记录里的描述字段本身已在写入侧截到 SYSLOG_DESC_MAX,所以按上面 minimum
* 给缓冲就一定装得下,读侧无需第二道截断。
*/
int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len) {
uint8_t rec[SYSLOG_REC_SIZE];
@@ -624,9 +766,12 @@ int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, c
while (dl < SYSLOG_DESC_MAX && rec[SYSLOG_DESC_OFFS + dl] != '\0') {
dl++;
}
if (dl >= desc_len) {
dl = desc_len - 1u;
}
/* C 项整改(2026-10-24):这里原来还有一句 "if (dl >= desc_len) dl = desc_len
* - 1u"——**永不可达**:上面的守卫已把 desc_len <= SYSLOG_DESC_MAX 全部挡掉,
* 而 dl 由定长记录的内容决定、恒 <= SYSLOG_DESC_MAX。删掉它,容量要求改由
* 函数头与 sys_log.h 写死:**宁可整条拒绝,也不做静默截断**(UI 拿到截断串
* 会显示成一条被删短过的日志,比读不到更难查)。desc_len 的校验仍保留,
* 它是"调用方给够缓冲"这条契约的执行点,不是截断开关。 */
memcpy(p_desc, rec + SYSLOG_DESC_OFFS, dl);
p_desc[dl] = '\0';
return 0;