Files
STM32F4-Base/simulator/sim/sim_syslog.c
T
wandering1 77779d49d6 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),
已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
2026-10-26 15:51:12 +08:00

355 lines
15 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.
/*
* 模块名称:System Log (模拟器后端) — 运行/自检记录落盘
* 模块功能:宿主文件七份(sim_run / sim_selftest / sim_accid / sim_alarm / sim_start /
* sim_oper / sim_yk,均 .log,工作目录),与 MCU 端同格式、同 id 口径
* (定长 64B 记录);单线程直写,无队列;含「清除记录」后端(与 MCU 端
* 同一套 API 与同一组判据:删后重建可继续写 / 计数归零 / 文件不在时幂等)
* 适用平台:PC 模拟器
* 作者:王建锋
* 创建日期:2026-09-14
* 修改记录:
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4)
* v1.1 2026-10-26 王建锋 新增「记录删除」后端(道F task-4,与 sys_log_mcu.c
* 同一份 sys_log.h 契约):syslog_request_clear() /
* syslog_clear_state()。两端**时序有别、判据相同**:
* 模拟器单线程(本文件里 syslog_post 也是直写不排队,
* 同一条先例),请求在调用内同步落地,因此
* SYSLOG_CLEAR_WAIT 在本端永不出现;判据(幂等 0 /
* 删后可重建继续写 / 失败不动原文件)逐条与 MCU 端一致
* v1.2 2026-10-26 王建锋 task-10 扩五类记录:s_paths/s_caps 同步到七项。
* **顺手修一处既有隐患**(Lead 批准,须在提交信息留痕):
* 原 `s_paths[SYSLOG_COUNT][16]` 里 "sim_selftest.log"
* 正好 16 字符 → 初值**装不满、没有 NUL 位**,而它又是
* 数组末行,后面没有任何保证为 0 的字节:stat()/remove()
* 拿它当 C 串会读越过数组尾,目前纯粹是"后面那块 .rodata
* 碰巧是 0"才没出事(未定义行为,换编译器布局即可能变成
* 打开一个错名字的文件)。行宽加到 24 并逐项写出,
* 越界这条随扩容一起消失。
*/
/* 头文件包含区 */
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>
#include "sys_log.h"
/* 静态变量定义区 */
/* 行宽 24 = 最长宿主名 "sim_selftest.log"(16) + NUL,留余量给后续类别。
* ⚠ 原来这里是 [16]:正好等于最长名的字符数,**没给 NUL 留位**(v1.2 修的隐患,
* 详见文件头修改记录)——加宽数组是这条修复的一部分,不要"看着没用"再改回去。
* ⚠ 下标 = syslog_id_t 枚举序,与 sys_log_mcu.c 的 s_paths[] 逐项同序(两端一致是
* "宿主与真机行为等价"这条口径的前提)。 */
static const char s_paths[SYSLOG_COUNT][24] = {
"sim_run.log", "sim_selftest.log", "sim_accid.log", "sim_alarm.log",
"sim_start.log", "sim_oper.log", "sim_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
};
/* 「清除记录」的结果字节(SYSLOG_CLEAR_NONE/OK/FAIL)。本端同步落地,没有
* "排队中"这一态,所以只需要结果一枚,MCU 端那枚 s_clear_req[] 在这里是多余的。
* 宿主进程态,不参与 MCU 的 RAM 预算台账。 */
static int8_t s_clear_st[SYSLOG_COUNT];
/* 函数定义区 */
/*
* 函数功能:取当前宿主本地时间(ISO 8601 空格变体 + epoch)
* 入口参数:p_ts - 输出缓冲(≥SYSLOG_TS_LEN+1 字节,不可为 NULL) char *
* p_epoch- 输出 epoch 秒(不可为 NULL) uint32_t *
* 返回值:无
*/
static void s_now(char *p_ts, uint32_t *p_epoch) {
time_t t = time(NULL);
struct tm tmv = {0}; /* 显式清零:localtime_s/r 是"输出型"参数,静态分析按未初始化报 */
#if defined(_WIN32)
localtime_s(&tmv, &t);
#else
localtime_r(&t, &tmv);
#endif
(void)snprintf(p_ts, SYSLOG_TS_LEN + 1u, "%04d-%02d-%02d %02d:%02d:%02d",
tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday,
tmv.tm_hour, tmv.tm_min, tmv.tm_sec);
*p_epoch = (uint32_t)t;
}
/*
* 函数功能:追加一条记录(格式化 64B;容量满先删除重建)
* 入口参数:id - 日志 ID syslog_id_t
* desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char *
* 返回值:0 - 成功;-1 - 写失败 int
*/
static int s_append(syslog_id_t id, const char *desc) {
FILE *fp;
uint8_t rec[SYSLOG_REC_SIZE];
char ts[SYSLOG_TS_LEN + 1u];
uint32_t epoch = 0u;
size_t dl;
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) {
return -1;
}
if (syslog_count(id) >= s_caps[id]) {
(void)remove(s_paths[id]);
}
memset(rec, 0u, sizeof(rec));
s_now(ts, &epoch);
memcpy(rec, ts, SYSLOG_TS_LEN);
rec[SYSLOG_TS_LEN] = ' ';
/* +1 = 容纳 NUL;字段本身固定 10 位数字,与旧 sprintf 写入字节数完全一致 */
(void)snprintf((char *)(rec + SYSLOG_EPOCH_OFFS), SYSLOG_EPOCH_LEN + 1u,
"%010u", (unsigned int)epoch);
rec[SYSLOG_DESC_OFFS - 1u] = ' ';
dl = strlen(desc);
if (dl > SYSLOG_DESC_MAX) {
dl = SYSLOG_DESC_MAX;
}
memcpy(rec + SYSLOG_DESC_OFFS, desc, dl);
fp = fopen(s_paths[id], "ab");
if (fp == NULL) {
return -1;
}
if (fwrite(rec, 1u, sizeof(rec), fp) != sizeof(rec)) {
(void)fclose(fp);
return -1;
}
(void)fclose(fp);
return 0;
}
/*
* 函数功能:读取第 idx 条原始记录(64B)
* 入口参数:id - 日志 ID syslog_id_t
* idx - 记录序号(0 起) uint32_t
* p_rec- 输出缓冲(SYSLOG_REC_SIZE 字节,不可为 NULL) uint8_t *
* 返回值:0 - 成功;-1 - 越界/读失败 int
*/
static int s_read_raw(syslog_id_t id, uint32_t idx, uint8_t *p_rec) {
FILE *fp;
if (p_rec == NULL || idx >= syslog_count(id)) {
return -1;
}
fp = fopen(s_paths[id], "rb");
if (fp == NULL) {
return -1;
}
if (fseek(fp, (long)idx * SYSLOG_REC_SIZE, SEEK_SET) != 0 ||
fread(p_rec, 1u, sizeof(uint8_t) * SYSLOG_REC_SIZE, fp) != SYSLOG_REC_SIZE) {
(void)fclose(fp);
return -1;
}
(void)fclose(fp);
return 0;
}
/*
* 函数功能:投递一条日志(单线程直写宿主文件)
* 入口参数:id - 日志 ID syslog_id_t
* desc - 描述串(UTF-8,超长截断) const char *
* 返回值:无
*/
void syslog_post(syslog_id_t id, const char *desc) {
(void)s_append(id, desc);
}
/*
* 函数功能:投递一条日志(零日志缓冲版本;宿主实现)
* 入口参数:id - 日志 ID syslog_id_t
* desc - 描述串(UTF-8,超长截断) const char *
* 返回值:无
* 函数说明:MCU 端设这个入口是为了绕开 DBG 的内联行缓冲(protectTask /
* iec104Task 的静态最坏链,理由写在 sys_log.h 同号声明处);宿主进程
* 没有那条硬线,也**没有队列与丢弃**,所以这里与 syslog_post 完全等价。
* 刻意不做成"宿主端另起一套语义"——五类记录的写入点在两端跑同一份
* 调用,语义差异越少,模拟器作为回归面的价值越高。
*/
void syslog_post_quiet(syslog_id_t id, const char *desc) {
(void)s_append(id, desc);
}
/*
* 函数功能:取 quiet 通道累计丢弃条数(宿主实现)
* 入口参数:无
* 返回值:恒 0——本端直写宿主文件、无队列无数组,不存在"满态丢弃"这条路径 uint32_t
* 函数说明:不要为了"看起来对称"在这里造一个假计数;MCU 端那条路径的丢条数由
* test/host/sys_log_mcu 的 TC 直接注入队列满来验,宿主验不了也不该装。
*/
uint32_t syslog_post_dropped(void) {
return 0u;
}
/*
* 函数功能:删掉本端的记录文件(「清除记录」的落地半边,宿主实现)
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:0 - 已删除,或文件本就不存在(幂等成功,对齐 MCU 端的 FR_NO_FILE);
* -1 - 删除失败,**原文件未被改动** int
* 限定条件:调用方即本端写者(模拟器单线程,syslog_post 在这里也是直写不排队);
* 本端没有卷锁,也就没有 MCU 端"卷占用 = 顺延"那一态
* 函数说明:先 stat 再 remove,是为了把"文件不在"与"删除失败"分开——
* remove() 对不存在的文件同样返回 -1,直接拿它的返回值判失败会把
* **幂等成功误报成故障**(与 MCU 端把 FR_NO_FILE 算成功不对称)。
* 与 MCU 端同一条纪律:只动自己那两个记录文件,不碰同目录里的其它文件。
*/
static int s_clear_apply(syslog_id_t id) {
struct stat st;
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
return -1;
}
if (stat(s_paths[id], &st) != 0) {
return 0;
}
return (remove(s_paths[id]) == 0) ? 0 : -1;
}
/*
* 函数功能:登记「清除某日志全部记录」的请求(宿主后端:调用内同步落地)
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:0 - 已执行(结果读 syslog_clear_state);-1 - id 越界(存量文件未动) int
* 限定条件:任意调用点(模拟器单线程,这里没有"非写者上下文"可言)
* 函数说明:与 MCU 端的**唯一**差异是时序:MCU 端只置请求位、等 defaultTask 的
* syslog_drain() 尾部才碰盘,本端在本调用内就删完了,因此
* SYSLOG_CLEAR_WAIT 在模拟器上永不出现(同 syslog_post:MCU 有队列与
* 丢弃、本端直写无队列)。判据一条没少:删后再投日志会重建文件并正常
* 写入、syslog_count() 归零、文件不在时幂等、失败不动原文件。
*/
int syslog_request_clear(syslog_id_t id) {
/* 与 MCU 端 s_clear_rec[] 同一组文案(一类一枚 ROM 常量、零拼接;改动必须
* 两处同改,两端"同格式同口径"是模拟器作为回归面的前提)。SYSLOG_OPER
* 那一格同样永不被使用——清 oper.log 时不投递,否则"我清除了记录"这条会把
* 刚删空的 oper.log 又写成 1 条,砸掉「删除成功后 count 归零」这条判据。 */
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"
};
int rc;
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
return -1;
}
rc = s_clear_apply(id);
s_clear_st[id] = (rc == 0) ? (int8_t)SYSLOG_CLEAR_OK : (int8_t)SYSLOG_CLEAR_FAIL;
/* 成功才留痕(失败那次什么都没改),且跳过 oper.log 自己——理由见上面表头。
* 本端 syslog_post_quiet 等价于直写,所以这条操作记录在函数返回前就已落盘,
* 与 MCU 端"下一轮 drain 才落"时序不同(两端差异已登记在
* docs/测试说明/模拟器差距清单.md,task-4 那次也写在那里)。 */
if ((rc == 0) && ((uint32_t)id != (uint32_t)SYSLOG_OPER)) {
syslog_post_quiet(SYSLOG_OPER, s_clear_rec[id]);
}
return 0;
}
/*
* 函数功能:查「清除记录」的状态(宿主后端)
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:SYSLOG_CLEAR_NONE(0) / OK(1) / FAIL(-2);本端不产出 WAIT(-1),
* id 越界按 NONE 返回 int
* 限定条件:任意调用点(单线程只读一字节)
* 函数说明:与 MCU 端共用同一组常量与同一段消费代码语义,UI 页面两端一份逻辑
* 就能同时处理"排队中"(真机)与"立即完成"(模拟器)。
*/
int syslog_clear_state(syslog_id_t id) {
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
return (int)SYSLOG_CLEAR_NONE;
}
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
* 返回值:条数;文件缺失返回 0 uint32_t
*/
uint32_t syslog_count(syslog_id_t id) {
struct stat st;
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
return 0u;
}
if (stat(s_paths[id], &st) != 0) {
return 0u;
}
return (uint32_t)st.st_size / SYSLOG_REC_SIZE;
}
/*
* 函数功能:读取第 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
*/
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];
size_t dl = 0u;
if (p_ts == NULL || p_desc == NULL || ts_len <= SYSLOG_TS_LEN || desc_len <= SYSLOG_DESC_MAX) {
return -1;
}
if (s_read_raw(id, idx, rec) != 0) {
return -1;
}
memcpy(p_ts, rec, SYSLOG_TS_LEN);
p_ts[SYSLOG_TS_LEN] = '\0';
while (dl < SYSLOG_DESC_MAX && rec[SYSLOG_DESC_OFFS + dl] != '\0') {
dl++;
}
if (dl >= desc_len) {
dl = desc_len - 1u;
}
memcpy(p_desc, rec + SYSLOG_DESC_OFFS, dl);
p_desc[dl] = '\0';
return 0;
}
/*
* 函数功能:读取末条记录的 epoch 秒(上电/重启分类用)
* 入口参数:id - 日志 ID syslog_id_t
* p_epoch- 输出缓冲(不可为 NULL;无记录时置 0) uint32_t *
* 返回值:0 - 成功读到末条;-1 - 文件缺失/空 int
*/
int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch) {
uint8_t rec[SYSLOG_REC_SIZE];
uint32_t v = 0u;
uint32_t i;
/* 先判空再写:旧写法 `if (p_epoch == NULL || ...)` 之后立刻 *p_epoch = 0,
* 一旦调用方真传 NULL 就是空指针解引用(cppcheck nullPointerRedundantCheck 实报) */
if (p_epoch == NULL) {
return -1;
}
if (syslog_count(id) == 0u) {
*p_epoch = 0u;
return -1;
}
if (s_read_raw(id, syslog_count(id) - 1u, rec) != 0) {
*p_epoch = 0u;
return -1;
}
for (i = 0u; i < SYSLOG_EPOCH_LEN; i++) {
char c = (char)rec[SYSLOG_EPOCH_OFFS + i];
if (c >= '0' && c <= '9') {
v = v * 10u + (uint32_t)(c - '0');
}
}
*p_epoch = v;
return 0;
}