# App/log/ — 日志子系统 > 两类日志:**持久化运行日志**(`sys_log`,落 NAND)与**控制台日志**(`console_log`,走串口)。 > 两者都遵循**单写者**模型,避免多任务交错输出。 ## 文件一览 | 文件 | 作用 | |---|---| | `sys_log.h` | **日志接口(平台无关)**:定长 64B 记录,七类各一个 `/log` 文件(run / selftest / accid / alarm / start / oper / yk)| | `sys_log_mcu.c` | `sys_log` 的 MCU 端后端实现(NAND + FatFs) | | `console_log.c/h` | **控制台单写者日志**:多任务日志经队列汇入唯一写者,避免串口输出交错 | | `soe_rec.c/h` | **SOE 记录纯编解码**:64B 定长记录(CP56Time2a 时标 + 类型 + 元件号 + 单调序号 + 值 + sum8 撕裂校验),零 I/O 零 OS 依赖,主机单测覆盖 | | `soe_store.c/h` | **SOE 落盘胶水**:`/log/soe.log`,protectTask 投递(零文件系统)→ defaultTask 排空落盘 | ## 关键机制 ### syslog(运行/自检记录) - **唯一写者 = defaultTask**;其它任务只投递,不直接写盘 - **单记录定长 64 B**,循环写;待机页可显示记录 - **内核启动前的日志**走**无锁静态缓冲** `s_preboot[8]` 暂存(因为内核启动前不能建队列), 内核启动后由 `syslog_drain` 惰性创建真队列并排空 - ⚠️ 写盘必须校验 `f_write` 返回的**字节数**(该 FatFs 移植在簇分配失败时返回 `FR_OK` + 短写, 不报错误码);追加写必须 `FA_WRITE|FA_OPEN_APPEND`(`FA_OPEN_APPEND` 单独用**不含写位**) - **清除记录 = 请求 / 落地分离**:非写者上下文(uiTask 等)只能 `syslog_request_clear(id)` 置一个标志(零盘触点、零卷锁),真正的 `f_unlink` 在 **defaultTask** 的 `syslog_drain()` **尾部**执行——顺序也是判据的一部分:排在条目排空之后,否则本轮刚排空的旧记录会被 写回去、`syslog_count()` 不归零。结果读 `syslog_clear_state(id)` (`NONE/OK/WAIT/FAIL`),事实复核读 `syslog_count()`。 落地函数 `syslog_clear_apply(id)` 是 MCU 后端专属(模拟器不实现 → 共享代码误调在 链接期就报错):卷锁一律 try,卷占用返 -1 且**不动原文件**;幂等只认 `FR_NO_FILE`, `FR_NO_PATH` 如实返 -1 且**不做任何卷级动作**(不补目录、不重格式化) - 删除的作用域 = 上表七个记录文件(`syslog_request_clear(id)` 逐 id 独立请求);取证文件 `wdev.log` 与 `soe.log` 各有各的属主与通道,不在本 API 作用域,想清它们要另开 API。 清除成功会往**操作记录**里落一条 `clear <文件>`——但清 `oper.log` 自己时**刻意不投**, 否则"我清除了记录"这条会把刚删空的文件又写成 1 条,直接砸掉「删除后 `syslog_count()` 归零」这条判据(`TC-SYSLOG-133b` 钉住) - ⚠ 宿主后端(`simulator/sim/sim_syslog.c`)与 MCU 端**判据相同、时序不同**:模拟器单线程, 七类对应工作目录下七份宿主文件(`sim_accid.log` 等),`syslog_post_quiet()` 在本端 等价于 `syslog_post()`(宿主进程没有那条栈硬线,也没有队列与丢弃); `syslog_request_clear()` 调用内就删完了,因此 `WAIT` 态在模拟器上永不出现 (同 `syslog_post` 在两端"有队列 / 直写无队列"的差异) ### 五类记录与「记录类型 = 日志 ID」(道F task-10,2026-10-26;task-11 记录 5 页按此做,2026-10-28 五页已落地) **决策一:记录类型就是 `syslog_id_t` 本身,一类记录 = `/log` 下一个严格 8.3 短名的文件。** 64B 定长记录体里**没有类型字段位**:`[0..18]` 时标 / `[19]` 空格 / `[20..29]` epoch / `[30]` 空格 / `[31..58]` 描述 / 尾部补零,这个布局被 UI 记录页与主机解码工具按**固定偏移** 读了了很久(run.log / selftest.log 是既有文件,既有读者)。塞类型字节要么挪走 epoch (上电/重启分类就断),要么挪走描述(屏上少字),且两端读者同批改——所以 "这条记录是什么类型"由**它在哪个文件里**回答。`TC-SYSLOG-136` 把这条钉成机器断言: 同一条内容在七类之间逐字节相同,记录体不编码类型。 **决策二:不动 `soe_type_t`(二进制 SOE 通道保持现有 8 型)。** 加 SOE 类型必须同步 UI 侧 `ui_w_soe.c` 的类型名表(那是另一位队友的写域),而 SOE 页与这五页本就是两套读者 (二进制解码器 vs 文本列表)。**别把新类型顺手加进 SOE**:那会同时造出 "同一个事件两个真源"。 | 记录 | 日志 ID / 文件 | 容量上限 | 写入点 | 触发判据 | |---|---|---|---|---| | 事故 | `SYSLOG_ACCIDENT` / `accid.log` | 512 | `App/task/protect_task.c:188` | `asserted` 上升沿**且**该元件跳闸投退项 = 1 | | 保护告警 | `SYSLOG_ALARM` / `alarm.log` | 1024 | 同文件 `191` | `asserted` 上升沿**且**跳闸投退项 = 0 或该元件无此项 | | 保护启动 | `SYSLOG_START` / `start.log` | 1024 | 同文件 `183` | `picked` 上升沿(越限电平,早于/等于动作) | | 操作 | `SYSLOG_OPER` / `oper.log` | 512 | `App/protocol/iec104/iec104_pts.c:582/610`、`App/task/default_task.c:266`、`App/log/sys_log_mcu.c:682`(宿主 `sim_syslog.c:245`) | 强制复归受理 / 软压板投退落地 / 定值**真落盘成功** / 清除记录成功 | | 遥控 | `SYSLOG_YK` / `yk.log` | 256 | `App/protocol/iec104/iec104_pts.c:581/584/590/594/604/606/617` | 104 双点命令的选择段、执行段、非法 DPC、未知 IOA 各一条 | - **"哪些元件算跳闸"只有一个真源**:定值表 `g_prot_set_meta` 里的 `PS_*_TRIP`(枚举注释 即"0 告警 / 1 跳闸")+ 实例表 `k_pe[]` 的 `trip_ctl` 列。分类入口 `prot_elm_record_kind()`(`App/algo/prot_elm.c`)里**没有第二份元件名单**, 没有该投退项的五项(OV/UV/3U0/OF/UF)填 `PROT_ELM_NO_TRIP=0xFF` → 恒告警; `TC-PROTELEM-015` 逐元钉住"逐项独立 / 改定值即改类别 / 无此项者不受别人定值影响"。 - **描述只用 ROM 常量或 `prot_elm_name()` 直传**:所有写入点零 `snprintf`、零拼接。 最长一条 20 字节 < `SYSLOG_DESC_MAX`(28);`TC-IEC104-030` 逐条量长度(超 28 会被 投递侧静默截断,屏上少几个字,没人会去数)。 - ⚠️ **两个投递口,按调用者的栈预算选**:`syslog_post()` 的两个"满态丢弃留痕"分支 内联着日志行缓冲(每处约 216 B 栈),因此 `syslog_post_quiet()` 给静态最坏链吃紧的 任务用(**protectTask、iec104Task 必须用它**),丢弃只累加 `syslog_post_dropped()`、 由唯一写者 defaultTask 在 `syslog_drain()` 里按差值补一条留痕——同 `soe_store_post` 当年那条 `872 → 1192 B FAIL` 的实证纪律。收/丢判据两个口完全一致。 - **读侧契约(给 task-11;2026-10-28 五页已按此接上)**:`syslog_count(id)` + `syslog_get_record(id, idx, ts, ts_len, desc, desc_len)` 与运行/自检记录**同一枚 reader**,"最近 20 条"= 从 `count-20` 起读; `count()==0` 就是空态,页面显示"无记录"。**不要**在共享代码里调 MCU 专属的 `syslog_clear_apply()`(模拟器不实现 → 链接期报错是故意的);清除一律走 `syslog_request_clear()` + `syslog_clear_state()`,并把 `WAIT` 当正常态看(模拟器端 永不出现 `WAIT`,见下条)。 ### console_log(控制台单写者) - 多任务的生产者 → **单写者消费者**,消除任务间日志行交错 - 设计详见 `docs/子系统设计/控制台单写者日志设计说明.md` ### soe(SOE 事件顺序记录,SOE 记录) - **记录 = 64 B 定长二进制**(`soe_rec.h` 布局),落 `/log/soe.log`; 与 run.log 分文件——run.log 是文本记录、有 UI/主机读取方,二进制 SOE 混入会破坏读取器;分文件保持"定长 64B + 条数=fsize/64 + 满删重建 + FTP 下载 + 主机解码"同一套先例 - **生产者 = protectTask**(元件动作/返回、软压板投退、采样闭锁/恢复六类 边沿):`soe_store_post()` 纯 RAM(取时标 + 组记录 + 队列/待发环), **零 NAND I/O**;⚠️ **post 内禁 `DBG_*`**——会把 protectTask 静态最坏链 垫高一帧(实证 872→1192 B FAIL),丢弃留痕由 drain(defaultTask)差值上报 - **唯一写者 = defaultTask**:挂 `syslog_drain()` 尾部(继承其卷就绪门), 50 ms 节拍 → SOE 落盘延迟 ≤50 ms;写盘纪律全套照 syslog(卷锁 try / `FA_WRITE|FA_OPEN_APPEND` / 校验字节数 / 满删重建 / 失败留痕去重), **绝不把名字/参数类错误码归入卷损坏触发集** - **时标口径**:sys_clock 秒域时基 → CP56 ms 域 = 秒×1000(亚秒恒 0, 不造假);墙钟未建 → IV 位如实置位;seq 仅上电生命周期内单调 (容量重建不重置,仅复位重置——主机解码按 boot 段分段断言) - 主机侧:`test/host/soe/decode_soe.py` 解码 + 断言(校验和/seq 单调/ CP56↔epoch UTC 交叉);⚠️ 交叉校验用 `calendar.timegm`,不是本地时区的 `dt.timestamp()`(+08 机差 28800 s,首跑实证坑) ## 相关文档 - `docs/子系统设计/控制台单写者日志设计说明.md` - `docs/子系统设计/运行与自检记录设计说明.md` - FatFs 陷阱:`AGENTS.md`「平台约束」