feat(ui+记录): 整定写路径与四类页落地,五类记录写入方补齐

UI 面:ui_data 增加深度 4 参数写队列(请求 168 B、队列 .bss 672 B)与状态机
OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY,ui_data_feed 出队后按注入 ops 走「闭锁 → 整批
预检 → 逐项 prot_set_set_sealed」,只提交改动位图;par 帧尾追加 cal_block
(口径 = UA/UN/IA 三路 ADC_CAL_F_VALID,与 protect_task 判据同构,不用 rt.eng
反推,V0.1 下 eng 恒 0)、UI_DATA_VER 升到 2。参数/定值/控制字/软压板四叶绑定
内容页(UI_PAGE_MAP_COUNT 12→16,复用四个查看模块,零新增 .c),编辑态按
is_editable 分派,LOCK(写不进去)与「未标定」(仍可改值)两源两通道两词。

记录面:syslog_request_clear/state/apply 三段式删除 API(UI 只发请求,落地在
defaultTask 的 drain 尾部且必须后置,卷锁一律 try,幂等只认 FR_NO_FILE);
事故/告警/启动/操作/遥控五类各有一个真实写入点,记录类型 = 日志 ID 本身
(一类一个 /log 严格 8.3 文件;64B 定长体没有类型字段位,塞字段会砸掉既有读者)。
为守 protectTask 872 B 静态最坏链另拆 syslog_post_quiet()(无 DBG 的投递核,
丢弃留痕挪到 drain 侧差值补发),syslog_post 对外语义逐字不变。

为什么两道合成一笔而不是各一笔(实测,不是省事):simulator/main.c 同时消费
两道新增 API(整定 pump 与 logtest 清除用例),按道切文件会让中间提交编不过;
RAM 受管快照与体积同码凭证描述的是合并后的 .map,拆开必落在「台账与 .map 不同
码」这条红线上。pre-commit 跑钩子前会 stash 未 staged 部分,这类耦合没法绕。

实测:@build 0 错误 0 警告;Code 228,260 / RO 24,960 / RW 1,440 / ZI 135,976 B;
RW_IRAM1 79,992 B = 61.0%、RW_CCM 87.9%(阈值 92%);九个任务静态最坏链全 PASS,
protectTask 872 B / 57% 改前后逐字节不变(记录投递没垫栈)。主机单测 ui_data 82→
164、sys_log_mcu 106→137、prot_elm 47→60、iec104 141→154、iec104_serv 28→34,
既有断言零删改;48 个钩子 --all-files 全绿;编号门禁可判定 86.34%(基线 86.0%)、
不可判定 1,153 处低于上限 1,166;doc-links 无法判定 0;UI 黄金 18 页扩到 27 页
连跑两次逐字节一致;字库 diff 空。

顺手修掉三处既有缺陷(都是本轮第一个消费者才暴露):sim_syslog.c 的 s_paths 行宽
16 而 "sim_selftest.log" 正好 16 字符没有 NUL 位(stat/remove 读越过数组尾,此前
靠后面 .rodata 碰巧为 0);ui_w_setlist 二态行 ENTER 不回绕(控制字与软压板出厂值
全在最后一项,按了像没反应而叠层里已是越界值);生成物 ui_menu_tab.h 与 gen_menu.py
模板漂移(上一笔改了模板没重跑生成器),现随生成器输出对齐。

未收口(不算完成):保存后重启值保持与主站 C_RD 读回两条只能上板,台架窗口执行;
LOCK 屏显与越界反显的页面侧在模拟器造不出来(模拟器不跑定值自检、无 NAND),
已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
This commit is contained in:
2026-10-26 15:51:12 +08:00
parent 0f7b5a87fb
commit 77779d49d6
55 changed files with 4236 additions and 289 deletions
+372 -40
View File
@@ -1,11 +1,33 @@
/*
* 模块名称:System Log (MCU 后端) — 运行/自检记录落盘
* 模块功能:FatFS /log 目录日志文件(run.log/selftest.log)单写者落盘;启动前静态待发数组 + FreeRTOS 消息队列,
* 模块功能:FatFS /log 目录**七类**定长 64B 记录(run/selftest/accid/alarm/start/oper/yk,
* 一类记录 = 一个文件 = 一种记录类型)单写者落盘;启动前静态待发数组 + FreeRTOS 消息队列,
* defaultTask(唯一写者)排空落盘;容量满删除重建;上电/重启分类
* 适用平台:STM32F407ZGTx
* 作者:王建锋
* 创建日期:2026-09-14
* 修改记录:
* v1.7 2026-10-26 王建锋 「记录 5 页」的数据面(task-10):syslog_id_t 尾部追加
* SYSLOG_ACCIDENT / ALARM / START / OPER / YK 五枚,本文件
* s_paths[]/s_caps[] 同步到七项(**必须与枚举同序**,越界
* 守卫只挡 id>=SYSLOG_COUNT,序错会静默把事故写进告警文件)。
* 记录体内不加类型字段——文件即类型(决策与理由见 sys_log.h
* 的 syslog_id_t 注释、App/log/README.md)。
* 成本:.bss **+40 B**(s_rd_gen 4B×5 + s_requeue_reported/
* s_clear_req/s_clear_st/s_fault_reported 各 1B×5),
* .const **+111 B**(5 枚路径指针 20B + 5 项容量 20B +
* 5 条 8.3 路径串 71B);零新增缓冲、零堆分配、零栈增量。
* 写入点全部在别处(protectTask / iec104 / 配置保存 /
* 本模块 request_clear),本文件只多七张表的五项。
* v1.6 2026-10-26 王建锋 新增「记录删除」(道F task-4,UI 装置维护「清除记录」
* 的前置):请求 syslog_request_clear() / 状态
* syslog_clear_state() / 落地 syslog_clear_apply()。
* 仍然只有 defaultTask 碰盘——非写者上下文只置一个
* volatile 单字节,f_unlink 在 syslog_drain() 尾部执行
* (范式同 prot_elm_request_reset);卷锁一律 try,
* 本任务 = 看门狗喂狗点绝不阻塞。
* 新增静态 4 B .bss(s_clear_req[2] + s_clear_st[2]),
* 零新增缓冲、零堆分配。
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4.4)
* v1.1 2026-09-21 王建锋 消息队列改为内核启动后由 syslog_drain 惰性创建
* (修复 osKernelStart 前创建 FreeRTOS 内核对象导致的启动失败)
@@ -58,8 +80,30 @@
/* 静态变量定义区 */
static const char *const s_paths[SYSLOG_COUNT] = { "/log/run.log", "/log/selftest.log" };
static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_MAX };
/* ⚠ 两张表的下标 = syslog_id_t 枚举序,**逐项同序追加**,中间不得插值(错序 = 静默
* 把某类记录写进别的文件,越界守卫拦不住)。文件名一律严格 8.3(FF_USE_LFN=0:
* 主名 ≤8 字符、扩展 ≤3、不得以点开头),由 TC-SYSLOG-081 逐条机器核。
* 七项合计 .const ≈ 111 B(含 5 条路径串 71 B);容量是**逻辑上限非预分配**。 */
static const char *const s_paths[SYSLOG_COUNT] = {
"/log/run.log", "/log/selftest.log", "/log/accid.log", "/log/alarm.log",
"/log/start.log", "/log/oper.log", "/log/yk.log"
};
static const uint32_t s_caps[SYSLOG_COUNT] = {
SYSLOG_RUN_MAX, SYSLOG_SELFTEST_MAX, SYSLOG_ACCIDENT_MAX, SYSLOG_ALARM_MAX,
SYSLOG_START_MAX, SYSLOG_OPER_MAX, SYSLOG_YK_MAX
};
/* 「清除记录」成功后的操作记录文案(task-10 的第五个操作写入点)。一类一枚 ROM
* 常量、零拼接:投递发生在 defaultTask 的 drain 尾部,那里不该为一条日志再放
* 格式化缓冲;最长 20 字符 << SYSLOG_DESC_MAX(28)。
* ⚠ SYSLOG_OPER 自己那一格**永远不会被用到**:清 oper.log 时不投递操作记录,
* 否则"我清除了记录"这条会把刚删空的 oper.log 又写成 1 条,直接砸掉 task-4
* 定死、UI 页面照它出反馈的判据「删除成功后 syslog_count() 归零」(与
* sys_log.h 里刻意不写 "records cleared" 留痕是同一条理由的两个面)。 */
static const char *const s_clear_rec[SYSLOG_COUNT] = {
"clear run.log", "clear selftest.log", "clear accid.log", "clear alarm.log",
"clear start.log", "clear oper.log", "clear yk.log"
};
#define SYSLOG_QUEUE_MAX 8u
@@ -108,6 +152,16 @@ static uint8_t s_requeue_reported[SYSLOG_COUNT];
static volatile uint8_t s_post_qfull_reported; /* 队列满丢弃:已留痕? */
static volatile uint8_t s_post_pboot_full_reported;/* 启动前数组满丢弃:已留痕? */
/* syslog_post_quiet() 通道(task-10,2026-10-26):给"静态最坏链放不下日志内联
* 行缓冲"的任务用的投递口(protectTask 872 B / 57% 是 B9 的 DoD 硬线,当年在
* post 里加一条 DBG_* 实测把它垫到 1192 B = stack_report FAIL)。本通道丢弃时
* **只累加计数**,留痕挪到 syslog_drain()(defaultTask)按差值上报——与
* App/log/soe_store.c 的 s_drop_cnt + drain 侧差值上报完全同构。
* RAM:两枚 uint32 = **8 B .bss**;volatile 单字自增在 Cortex-M 上原子,最坏
* 少报一条,不会漏计到永久静默(水位只在本函数读到差值时才前进)。 */
static volatile uint32_t s_quiet_drop; /* quiet 通道累计丢弃条数 */
static uint32_t s_quiet_reported; /* drain 侧已上报水位(唯一写者私有) */
/* 日志读写放大收口(2026-10-15,读侧):8 条滑动读窗口 + 写者代数。
* 窗口 512 B 走静态(不吃任务栈;defaultTask/uiTask 最坏链见 stack_report.py,
* 预算余量见 docs/内存管理/RAM预算台账.md)。s_rd_gen 由写者自增、读者比对,
@@ -120,6 +174,21 @@ static uint32_t s_rd_win_cnt; /* 窗口内有效条数 */
static uint32_t s_rd_win_gen; /* 填充时看到的写者代数 */
static volatile uint32_t s_rd_gen[SYSLOG_COUNT]; /* 每次成功落盘 / unlink 后自增 */
/* 「清除记录」的请求 / 落地交接(道F task-4)。两枚各 SYSLOG_COUNT 字节 = **共 4 B
* .bss**(本模块唯一的新增静态;无缓冲、无堆,MicroLIB C 堆只有 512 B):
* s_clear_req[id] —— 1 = 有未落地的删除请求。非写者上下文(uiTask)置 1,
* defaultTask 落地时先清 0 再执行 → 期间到达的新请求会
* 重新置 1,**下一次排空再删一次**,不会因为"正在删"而丢;
* s_clear_st[id] —— 最近一次落地的结果,取值 SYSLOG_CLEAR_NONE/OK/FAIL
* (WAIT 不落在这里,由 s_clear_req 现场推,见 syslog_clear_state)。
* 为什么拆两枚而不是合成一个四态状态机:单字节状态机在"写者读完状态到写回结果"
* 之间收到的新请求会被结果覆盖掉(用户的第二次点击被无声吞掉)。拆开后各自的写者
* 分明(req:任意上下文置 1 / 写者清 0;st:只有写者写),单字节写在 Cortex-M 上
* 原子,剩下的竞态只有"读到上一次结果"这一种同向偏差 → 无锁,代价换来 0 个临界区、
* 0 个头文件依赖(本文件不 include FreeRTOS.h)。 */
static volatile uint8_t s_clear_req[SYSLOG_COUNT]; /* 1 = 待落地的清除请求 */
static volatile int8_t s_clear_st[SYSLOG_COUNT]; /* SYSLOG_CLEAR_NONE / OK / FAIL */
/* 函数定义区 */
/*
@@ -484,6 +553,102 @@ static int s_read_raw(syslog_id_t id, uint32_t idx, uint8_t *p_rec) {
return 0;
}
/*
* 函数功能:落地执行"删掉本模块自己的日志文件"(记录删除的落地半边,内部三态版)
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:0 - 已删除,或文件本就不存在(FR_NO_FILE = 幂等成功);
* -1 - 删除真失败(原文件一个字节都没少);
* -2 - 卷被占用(本轮顺延,非故障,与 s_append_rec 的 -2 同一含义) int
* 限定条件:仅 defaultTask(syslog 唯一写者)上下文;调用处不得持卷锁
* (卷锁非递归锁,重入即自死锁,见 App/fs/vol_lock.h 使用约定)
* 函数说明:本函数只做一件事——对自己那一个文件 f_unlink 一次。因此存储架构说明
* §10.1 第 10 条(名字/参数类错误码严禁升级成卷操作)在这里的形态是
* "压根没有卷级动作":不 f_mkdir、不 f_mkfs、不重开、不重试,
* FR_INVALID_NAME / FR_NO_PATH / FR_DENIED 一律如实返 -1。
* 幂等只认 FR_NO_FILE(文件不在 = 没有记录可删,目标已达成);
* FR_NO_PATH 刻意不算幂等,理由写在 syslog_clear_apply() 的头注释里。
* 只有真删成功(含幂等路径)才让 s_rd_gen[id] 自增:文件确实不在盘上了,
* 读者的 8 条窗口必须作废;失败路径内容没变,窗口仍是有效数据,
* 不去做无谓的失效(与 s_append_rec"失败也一律失效"不同——那边失败
* 伴随过 unlink/重建,本函数失败时盘上什么都没动)。
* 刻意不在删除成功后补一条 "records cleared":判据要求 syslog_count()
* 归零,写了立刻变 1;要留痕归操作记录/SOE(UI补齐.md 第 6 项)。
*/
static int s_clear_try(syslog_id_t id) {
FRESULT fr;
int rc;
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
return -1; /* 守卫在取锁之前:非法入参一个字节都不碰盘,也不占用卷 */
}
/* 卷锁 try:本任务 = 看门狗喂狗点,绝不阻塞等锁(与 s_append_rec 同一条铁律) */
if (vol_lock_wait(0u) == 0u) {
return -2;
}
fr = f_unlink(s_paths[id]);
if ((fr == FR_OK) || (fr == FR_NO_FILE)) {
s_rd_gen[id]++; /* 文件已不在盘上:读者的 8 条窗口作废重填 */
rc = 0;
} else {
rc = -1;
}
vol_lock_release();
if (rc != 0) {
/* 一次用户动作至多一条留痕(失败不重试,见 s_clear_service),不会刷屏,
* 所以这里不需要像 B/F 项那样另立节流位。
* ⚠ 留这条的成本约 90 B Flash(AGENTS「日志与工具链」);留它的理由:
* "清除失败但盘上还有记录"在串口之外没有任何可观测面(UI 只有一个整数状态)。 */
DBG_ERROR("clear %s fail (%d)", s_paths[id], (int)fr);
}
return rc;
}
/*
* 函数功能:消费「清除记录」请求(syslog_request_clear 的请求 → 落地执行点)
* 入口参数:无
* 返回值:无
* 限定条件:仅 syslog_drain() 调用 = defaultTask(唯一写者)上下文,且不持卷锁;
* 卷未就绪 / 队列未建成时 syslog_drain 在本函数之前就已早退,
* 请求原地保持挂起(与日志条目滞留同一处置),状态一直是 WAIT
* 函数说明:两个决定都是刻意的:
* 1) 排在条目排空**之后**(调用位置见 syslog_drain):删除必须是本轮对
* run.log/selftest.log 的最后一次盘上动作,否则本轮刚排空的旧记录
* 会在删除之后又落回去,count 不归零,"清除"就成了没实际效果的按钮
* (判据出处 docs/任务规划/UI补齐.md §2 第 3 行)。
* 副作用照实登记:**先排空再删** = 请求受理时仍在队列/preboot 里的
* 条目也一并被删掉(用户按下的就是"把现在这些记录清掉"),
* 只有删除之后新投递的记录才会留下。
* 2) 卷占用(-2)把请求**挂回**而不是判失败:与 A 项整改后的条目
* "整批回投、静默顺延"同一纪律(本函数每 50 ms 被排空调一次,
* 一打日志就自己刷屏);真失败(-1)则销账 + 置 FAIL,绝不自动重试
* ——名字/参数类错误码反复 unlink 自己的文件 = §10.1 第 10 条老账。
*/
static void s_clear_service(void) {
uint32_t i;
for (i = 0u; i < (uint32_t)SYSLOG_COUNT; i++) {
int rc;
if (s_clear_req[i] == 0u) {
continue;
}
s_clear_req[i] = 0u; /* 先销账再执行:期间的新请求留到下轮,不丢 */
rc = s_clear_try((syslog_id_t)i);
if (rc == -2) {
s_clear_req[i] = 1u; /* 卷占用:挂回请求,状态继续是 WAIT,静默 */
} else {
s_clear_st[i] = (rc == 0) ? (int8_t)SYSLOG_CLEAR_OK : (int8_t)SYSLOG_CLEAR_FAIL;
/* 清除是一次真实的运维动作 → 操作记录(为何只在成功时记、为何
* 独独跳过 SYSLOG_OPER,见上面 s_clear_rec 的表头注释)。
* 失败不记:那一次什么都没改,记了反而让"操作记录"里出现
* 一个没发生过的动作。 */
if ((rc == 0) && (i != (uint32_t)SYSLOG_OPER)) {
syslog_post_quiet(SYSLOG_OPER, s_clear_rec[i]);
}
}
}
}
/*
* 函数功能:日志引擎初始化(内核启动前安全空操作)
* 入口参数:无
@@ -671,12 +836,42 @@ void syslog_drain(void) {
break; /* 本轮到此为止,下轮再试 */
}
}
/* quiet 通道的丢弃留痕(task-10):投递侧不许打日志(见上面 s_quiet_drop 的
* 存在理由),所以由唯一写者按差值补一条。与 soe_store 的 dropped 上报同款,
* 也是"日志自己不许变成负载"的既有纪律:只在计数真的前进时打。 */
if (s_quiet_drop != s_quiet_reported) {
s_quiet_reported = s_quiet_drop;
DBG_ERROR("quiet drop total=%lu", (unsigned long)s_quiet_drop);
}
/* 「清除记录」的落地半边——必须在上面的条目排空**之后**(顺序是判据的一部分:
* 先删后写会把本轮刚排空的旧记录又落回去,count 不归零 = 没实际效果的按钮),
* 也在 soe_store_drain() 之前:本函数自带卷锁 try,绝不在此持锁状态下调用。
* 卷未就绪/队列未建时上面已经早退,请求保持挂起(状态 WAIT),不会丢。 */
s_clear_service();
/* B9:SOE 落盘挂本函数尾部——同一唯一写者(defaultTask)、同一卷就绪门
(上方 s_fs_ready 早退蕴含 /log 已建)、同一卷锁 try 语义;protectTask
只投递不写盘(动作路径零 NAND I/O,DoD 硬约束)。 */
soe_store_drain();
}
/*
* 函数功能:立即删除某日志的全部记录(「清除记录」的落地半边,MCU 后端专属)
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:0 - 记录已清空(含"文件本就不存在"的幂等成功)
* -1 - 未清空:卷被占用或删除失败,**原文件未被删除** int
* 限定条件:仅 defaultTask(syslog 唯一写者)上下文,且调用处不得持卷锁;
* 非写者上下文一律走 syslog_request_clear()(本文件不实现"跨上下文删除",
* 那是把 uiTask 直接接上 FAT 回写的开始)
* 函数说明:内部三态里"卷占用 = -2(顺延)"在本 API 折成 -1 —— 对调用方来说
* "没删掉、记录还在"是同一个事实,该不该重试由调用方定;
* 请求侧的顺延重试策略(-2 挂回请求、-1 判失败不重试)在 s_clear_service(),
* 不外溢成第三种返回值。逐码依据与幂等口径见 sys_log.h 里本函数声明处的
* 头注释,以及 s_clear_try() 的实现注释。
*/
int syslog_clear_apply(syslog_id_t id) {
return (s_clear_try(id) == 0) ? 0 : -1;
}
/*
* 函数功能:投递一条日志(任意上下文;非阻塞,队列满则丢弃)
* 入口参数:id - 日志 ID syslog_id_t
@@ -690,55 +885,192 @@ void syslog_drain(void) {
* 串口就按业务投递频率刷"queue full, drop",日志自己变成负载。
* 抑制位是 volatile 单字节、由任意上下文读写:竞态最坏只多打/少打一行。
*/
void syslog_post(syslog_id_t id, const char *desc) {
/* 投递结果编码(s_put 返回值)。分通道返回是**必须的**,不是讲究:F 项那两个节流
* 抑制位按通道分开(队列 / 启动前数组),塌成一个"失败"就会把"数组满"报成"队列
* 满",还会把另一通道的抑制位误复位(TC-SYSLOG-105..107c 逐条钉着这条)。 */
#define SYSLOG_PUT_Q_OK 0u /* 队列收下 */
#define SYSLOG_PUT_PB_OK 1u /* 启动前待发数组收下 */
#define SYSLOG_PUT_Q_FULL 2u /* 队列满:本条丢弃 */
#define SYSLOG_PUT_PB_FULL 3u /* 数组满:本条丢弃 */
/*
* 函数功能:填一条待发条目——投递时就拷贝描述(post / post_quiet 共用,内部)
* 入口参数:p_e - 输出条目(不可为 NULL) syslog_entry_t *
* id - 日志 ID(调用方已核过界) syslog_id_t
* desc - 描述串,NUL 结尾;静态串或调用方的临时缓冲都可以 const char *
* 返 回 值:无
* 函数说明:拷贝之后待发链不再引用调用方的缓冲区——这条纪律是 B 方案整改用真机
* 实测换来的(旧实现只存指针,复用栈缓冲时一整批 12 条全部别名到最后一次
* 写入的内容),见本文件上方 syslog_entry_t 的注释。
*/
static void s_entry_fill(syslog_entry_t *p_e, syslog_id_t id, const char *desc) {
size_t dl = strlen(desc);
p_e->id = (uint8_t)id;
if (dl > SYSLOG_DESC_MAX) {
dl = SYSLOG_DESC_MAX;
}
memcpy(p_e->desc, desc, dl);
p_e->desc[dl] = '\0';
}
/*
* 函数功能:把条目送进消息队列;队列不存在或内核未跑则进启动前待发数组(内部)
* 入口参数:p_e - 已填好的条目(不可为 NULL) const syslog_entry_t *
* 返 回 值:SYSLOG_PUT_Q_OK / SYSLOG_PUT_PB_OK / SYSLOG_PUT_Q_FULL /
* SYSLOG_PUT_PB_FULL uint8_t
* 限定条件:⚠ **本函数内禁止出现任何 DBG_***(照 soe_store_post 的同款禁令)
* 函数说明:它是 post 与 post_quiet 的共同落点,而 post_quiet 存在的唯一理由就是
* "调用方任务的静态最坏链放不下日志的内联行缓冲"(每处约 216 B 栈)。
* 这里一旦出现日志,quiet 就退化成 post 的别名,protectTask / iec104Task
* 的记录投递又会把链垫回 stack_report 的 FAIL 区——B9 当年实测
* 872 B -> 1192 B 就是这条,别用"这里只有一条"说服自己。
* 留痕因此分两侧:post 的调用侧节流位 + drain 的 quiet 差值上报。
*/
static uint8_t s_put(const syslog_entry_t *p_e) {
if (s_queue != NULL && osKernelGetState() == osKernelRunning) {
return (osMessageQueuePut(s_queue, p_e, 0u, 0u) == osOK) ?
(uint8_t)SYSLOG_PUT_Q_OK : (uint8_t)SYSLOG_PUT_Q_FULL;
}
if (s_preboot_cnt < SYSLOG_QUEUE_MAX) {
s_preboot[s_preboot_cnt] = *p_e;
s_preboot_cnt = (uint8_t)(s_preboot_cnt + 1u);
return (uint8_t)SYSLOG_PUT_PB_OK;
}
return (uint8_t)SYSLOG_PUT_PB_FULL;
}
/*
* 函数功能:投递一条日志(零日志缓冲版本;收/丢语义与 syslog_post 完全一致)
* 入口参数:id - 日志 ID syslog_id_t
* desc - 描述串(NUL 结尾,静态/常量串;超长截断) const char *
* 返 回 值:无
* 限定条件:任意上下文;不碰 FatFs、不碰卷锁、不阻塞。**专为静态最坏链吃紧的
* 任务准备**(本仓当前 = protectTask、iec104Task)
* 函数说明:与 syslog_post 的差别只有两处:① 本调用链上不内联任何 DBG 行缓冲,
* 丢弃时只累加 s_quiet_drop,由 defaultTask 在 syslog_drain() 里按差值
* 补一条留痕;② 不参与 F 项那两个节流位——那两个位描述的是 syslog_post
* 自己的历史,混用会让"post 已恢复"被 quiet 的成功投递误复位。
* 丢弃计数读 syslog_post_dropped()。其它任务请继续用 syslog_post:
* 即时留痕,可观测性更好,别为不存在的栈压力付两倍的认知成本。
*/
void syslog_post_quiet(syslog_id_t id, const char *desc) {
syslog_entry_t e;
size_t dl;
uint8_t res;
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) {
return;
}
e.id = (uint8_t)id;
/* 投递即拷贝(含截断):待发条目不再引用调用方缓冲区,栈缓冲/临时格式化结果
* 都能安全投递(见上方 syslog_entry_t 注释的台架实证) */
dl = strlen(desc);
if (dl > SYSLOG_DESC_MAX) {
dl = SYSLOG_DESC_MAX;
}
memcpy(e.desc, desc, dl);
e.desc[dl] = '\0';
if (s_queue != NULL && osKernelGetState() == osKernelRunning) {
if (osMessageQueuePut(s_queue, &e, 0u, 0u) != osOK) {
/* 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;
s_entry_fill(&e, id, desc);
res = s_put(&e);
if ((res == (uint8_t)SYSLOG_PUT_Q_FULL) || (res == (uint8_t)SYSLOG_PUT_PB_FULL)) {
if (s_quiet_drop < UINT32_MAX) {
s_quiet_drop = s_quiet_drop + 1u; /* 留痕在 drain 侧,理由见函数头 */
}
} else {
uint8_t n = s_preboot_cnt;
if (n < SYSLOG_QUEUE_MAX) {
s_preboot[n] = e;
s_preboot_cnt = n + 1u;
s_post_pboot_full_reported = 0u; /* 恢复正常:同上,无条件复位 */
} else if (s_post_pboot_full_reported == 0u) {
/* 同一纪律的另一通道:启动前数组只有 SYSLOG_QUEUE_MAX 槽,挂载慢 +
* 开机事件密集时会在整个"未就绪窗口"里每条投递打一行(比队列通道更早
* 发生、也更密集)。数组重新收下第一条即视为恢复。 */
}
}
/*
* 函数功能:取 quiet 通道累计丢弃条数(可观测性;照 soe_store_dropped 同款)
* 入口参数:无
* 返 回 值:累计丢弃条数 uint32_t
*/
uint32_t syslog_post_dropped(void) {
return s_quiet_drop;
}
void syslog_post(syslog_id_t id, const char *desc) {
syslog_entry_t e;
uint8_t res;
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) {
return;
}
s_entry_fill(&e, id, desc);
res = s_put(&e);
switch (res) {
case SYSLOG_PUT_Q_OK:
/* 恢复正常:清抑制位,下一次故障重新留痕。这里刻意无条件写
* (曾试成"仅置位时才清",被 cppcheck duplicateConditionalAssign
* 判成等价赋值并拦提交闸;本函数上方已做过一次条目拷贝,多一条
* STR 可以忽略,别为它加 inline-suppr)。 */
s_post_qfull_reported = 0u;
break;
case SYSLOG_PUT_PB_OK:
s_post_pboot_full_reported = 0u; /* 恢复正常:同上,无条件复位 */
break;
case SYSLOG_PUT_Q_FULL:
/* F 项:只报"从正常转入故障"那一次;期间条目照常丢弃(丢弃语义不变,
* 变的只是留痕次数)。下一次投递成功即复位。 */
if (s_post_qfull_reported == 0u) {
s_post_qfull_reported = 1u;
DBG_ERROR("queue full, drop: %s", desc);
}
break;
default:
/* SYSLOG_PUT_PB_FULL:同一纪律的另一通道——启动前数组只有
* SYSLOG_QUEUE_MAX 槽,挂载慢 + 开机事件密集时会在整个"未就绪窗口"里
* 每条投递打一行(比队列通道更早发生、也更密集)。数组重新收下第一条
* 即视为恢复(上面 PB_OK 已复位)。故障期间静默,条目照常丢弃。 */
if (s_post_pboot_full_reported == 0u) {
s_post_pboot_full_reported = 1u;
DBG_ERROR("preboot full, drop: %s", desc);
} else {
/* 故障期间静默,条目照常丢弃 */
}
break;
}
}
/*
* 函数功能:登记「清除某日志全部记录」的请求(任意上下文;不碰存储、不阻塞)
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:0 - 请求已登记(同一 id 重复登记合并为一次,幂等)
* -1 - id 越界(该 id 的请求位与结果字节一个都没动) int
* 限定条件:任意任务上下文(含 uiTask);零 FatFs 触点、零卷锁触点,
* 因此**不会**把调用者的喂狗节拍拖进存储路径
* 函数说明:MCU 端本函数只代表"已受理",真正的 f_unlink 在 defaultTask 的
* syslog_drain() 尾部(s_clear_service)执行,结果读 syslog_clear_state()。
* 先清结果再置请求,是为了让 UI 在"排队中"这一拍不会拿上一次的
* OK/FAIL 当本次的结论(顺序反了就会出现"按了按钮仍显示上次失败")。
* 竞态面(写在 sys_log.h 同一位置,勿只信这里):置请求与写者清请求都是
* 单字节写,Cortex-M 上原子;最坏是写者已清账、本函数又置回 1 →
* 下一轮多删一次(unlink 幂等),不会丢请求、也不会误清别人的记录。
*/
int syslog_request_clear(syslog_id_t id) {
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
return -1;
}
s_clear_st[id] = (int8_t)SYSLOG_CLEAR_NONE;
s_clear_req[id] = 1u;
return 0;
}
/*
* 函数功能:查「清除记录」的状态(给 UI 出真实反馈,不是"按了就算成功")
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:SYSLOG_CLEAR_NONE(0) / SYSLOG_CLEAR_OK(1) / SYSLOG_CLEAR_WAIT(-1) /
* SYSLOG_CLEAR_FAIL(-2);id 越界按 NONE 返回 int
* 限定条件:任意上下文(只读两个 volatile 单字节,无锁)
* 函数说明:排队位优先于结果字节 —— 否则连点两次时会先把上一次的结果当成本次的
* 结论,还在排队的请求被显示成"已清空"。两个字节都是 defaultTask 与
* uiTask 之间的无锁交接,最坏竞态与为什么不加临界区,见 sys_log.h
* syslog_clear_state() 的头注释(同一事实只在那一处详述)。
*/
int syslog_clear_state(syslog_id_t id) {
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
return (int)SYSLOG_CLEAR_NONE;
}
if (s_clear_req[id] != 0u) {
return (int)SYSLOG_CLEAR_WAIT;
}
if (s_clear_st[id] == (int8_t)SYSLOG_CLEAR_OK) {
return (int)SYSLOG_CLEAR_OK;
}
if (s_clear_st[id] == (int8_t)SYSLOG_CLEAR_FAIL) {
return (int)SYSLOG_CLEAR_FAIL;
}
return (int)SYSLOG_CLEAR_NONE;
}
/*
* 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1))
* 入口参数:id - 日志 ID syslog_id_t