4a4ad6bc5f
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名, 不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体 run_boot_test.py 的真实调用。 - AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改 「编译(/keil-build)」「串口回归(/serial-boot-test)」 - keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree 方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围) - 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入 等 41 行历史证据保留旧名 - gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑 - 顺手补并行方案同行漏网计数 51→52 钩子 验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52 钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
346 lines
31 KiB
Markdown
346 lines
31 KiB
Markdown
# 运行/自检记录与待机显示设计说明
|
||
|
||
> **版本**:v1.3(§4.4 新增"卷锁 fail-open 边沿补投自检记录"一条,道G task-40;v1.2 为 §4.3 随七类记录更新,v1.1 的原始设计口径不变)
|
||
> **日期**:2026-09-14(v1.2 更新 2026-10-26)
|
||
> **状态**:步骤 1–9 全部完成——双端构建 0/0、`--logtest` PASS、ui-verify 五门禁全绿;真机 /serial-boot-test 待上电回归(§8.2)
|
||
> **关联文档**:`项目架构总览.md` · `STM32F4-Base存储架构说明.md` · `系统墙钟时间维护说明.md` · `UI设计/内容页架构.md`(如存在)· `docs/UI设计/mockups/`(待机显示页面.jpg / 运行记录页.jpg / 自检记录页.jpg / 密码页面实际机器的示例图片.jpg)
|
||
|
||
## 1. 背景与需求
|
||
|
||
| # | 需求 | 说明 |
|
||
|---|------|------|
|
||
| R1 | 密码错误提示 | 用户密钥页输错密码时提示"密码输入错误",并允许重新输入(现状:仅清空重输、无提示) |
|
||
| R2 | 待机显示页 | 长时间无操作时默认显示的界面(参考 `mockups/待机显示页面.jpg`:6 路电流值 + 底部时钟);**只有按确认键才进入菜单** |
|
||
| R3 | 运行记录页 | 记录装置上电时间、重启时间(参考 `mockups/运行记录页.jpg`:标题 + 逐条"时间戳 + 事件" + 页脚分页) |
|
||
| R4 | 自检记录页 | 记录开机自检结果,有问题才记录(参考 `mockups/自检记录页.jpg`) |
|
||
| R5 | FTP 可读存储 | 记录以**纯文本文件**落盘,FTP(根目录 = FatFS 卷根)可直接下载读取 |
|
||
| R6 | 先调研后设计 | 网络调研存储方式选型 → 写入本设计文档 → 按文档实施 |
|
||
|
||
## 2. 现状分析
|
||
|
||
### 2.1 UI 框架(`Drivers/BSP/UI/`,MCU/模拟器共用)
|
||
|
||
- **内容页 = 描述符驱动**:`content_page_def_t`(标题/几何/行数据源/4 钩子),框架 `ui_content_page.c` 统一建屏、切屏、焦点、按键分发与绘制;页面模块零 LVGL 对象。
|
||
- **分页已内建**:`selectable=1` + `rows_per_page`(↑↓移选、窗口跟随)+ 页脚"第 X 页,共 Y 页"自动计算;动态行经 `refresh(row, name, value)` 逐行取数(栈缓冲 **32B/列**),行数经 `count_cb()`。
|
||
- **菜单树已含目标叶子**:`ui_menu_tree.txt` "记录查询"下已有 **自检记录 = node 39、运行记录 = node 40**(当前为空白终点框,无内容页映射);无需改树。
|
||
- **密码页** `ui_page_password.c`:3 位数字框 + ↑↓←→/ENTER;输错仅清空重输(R1 缺口)。
|
||
- **按键流**:物理/远端键 → LVGL keypad → 默认输入组 → 焦点对象事件回调(菜单容器 `menu_key_cb` / 内容页容器 `cp_key_cb`)——空闲检测可在此统一挂钩。
|
||
- **字体**:子集位图字体 `ui_font_cn_12`,字符集 `ui_cn_chars.txt`(当前 224 汉字/符号 + ASCII 0x20~0x7E,共 319 字形;2026-10-29 实测。122/217 是 2026-09-14 的历史快照,见 §6.1)。
|
||
|
||
### 2.2 存储与时间
|
||
|
||
| 项 | 现状 |
|
||
|---|------|
|
||
| 卷 | GD5F2GQ5UE SPI NAND **256MB**(2048 块 × 64 页 × 2KB),dhara FTL(磨损均衡/坏块管理),FatFS FAT32,`FF_USE_LFN=0` → **文件名必须 8.3** |
|
||
| FTP | lftpd 根目录 = 卷根 `/`;现有文件 `/config.ini`(8.3);旧档 `/cfg/netcfg.bin` 已废弃——现固件不读不写也不删(仅可能残留在旧设备上) |
|
||
| 并发 | `FF_FS_REENTRANT=1` + 系统锁(`ffsystem.c` osMutex):单次 `f_*` 调用受保护,**多调用序列(open→seek→write→close)需单写者纪律** |
|
||
| 墙钟 | `sys_clock_get_str(buf,20)` → `"YYYY-MM-DD HH:MM:SS"`;`sys_clock_get()` → Unix 秒(失败 = `UINT32_MAX`);SD2506 RTC + timeSyncTask TCP 校时 |
|
||
| 挂载 | defaultTask:`f_mount(forced=1)`,空卷自动 `f_mkfs` FAT32,成功后置 `SYS_EVT_STORAGE_READY` |
|
||
|
||
### 2.3 现有自检检查点(v1 埋点位置)
|
||
|
||
| 检查点 | 位置 | 失败表现 |
|
||
|---|------|---------|
|
||
| RTC 读取/墙钟建立 | `app_main_init()`:`sd2506_init()` + `sys_clock_init()`(FreeRTOS 启动前) | 时间戳无效,后续日志无意义时间 |
|
||
| NAND 初始化 | `app_main_init()`:`gd5f2gq5ue_init()` 返回非 `GD5F_OK` | 存储不可用 |
|
||
| ADC 初始化 | `app_main_init()`:`tpafe5160_init()` 超时 | 采样不可用 |
|
||
| FatFS 挂载/建卷 | defaultTask:`f_mount`/`f_mkfs` 失败(置位 STORAGE_READY 前) | FTP/配置/记录全部不可写 |
|
||
| 网络链路 | net_task `net_stack_bring_up()`:PHY 等待 ≤3s 后仍 DISCONNECT | 组网可用但无链路 |
|
||
|
||
### 2.4 RAM/LVGL 池预算(依据 `docs/内存管理/RAM使用分析与优化方案.md`)
|
||
|
||
- FreeRTOS 堆真实余量 ≈ **11~12KB**;LVGL 池 16KB 静态,菜单常驻占用 ≈5KB。
|
||
- 本设计新增 RAM:**全部 < 1KB**(§7),不新增任务、不动任务栈。
|
||
|
||
## 3. 网络调研摘要(R6)
|
||
|
||
| 主题 | 结论 | 来源 |
|
||
|---|------|------|
|
||
| 日志行格式与时间戳 | syslog(RFC 5424):一行一条记录;TIMESTAMP 采用 **ISO 8601/RFC 3339**(`YYYY-MM-DDTHH:MM:SS[.frac][Z]`,机器可读、字典序可排序);MSG 用 UTF-8。本设计取空格变体 `YYYY-MM-DD HH:MM:SS`(与既有 `sys_clock_get_str` 输出一致),另附 epoch 秒字段供程序化解析 | RFC 5424 §6.2.3/§6.4(datatracker.ietf.org/doc/html/rfc5424) |
|
||
| 写入模型 | Linux printk:消息先进 **RAM 环形缓冲**,由**专用线程**异步刷持久存储;热路径禁止同步 I/O。本设计对应"RAM 待发队列 + defaultTask 单写者落盘" | kernel.org/doc/html/latest/core-api/printk-basics.html |
|
||
| FatFs 追加语义 | `FA_OPEN_APPEND`(= FA_WRITE\|FA_SEEKEND):打开即定位 EOF,文件不存在则创建;配合 `f_lseek(fp, FILE_SIZE)` 可取文件大小。本设计每条记录一次 `f_open→f_write(64B)→f_close` | 工程内 `Lib/FatFs/ff.h:399`、`ff.c:3871`(与官方文档一致) |
|
||
| NAND 写入代价 | dhara FTL 已做磨损均衡;FAT32 下每次小追加约消耗 1 个簇(4KB)。本场景**写频率极低**(每上电 1~N 条、故障才写),簇开销与磨损可接受;以文件容量上限约束增长 | `docs/架构设计/STM32F4-Base存储架构说明.md` §6.3 + GD5F 驱动参数 |
|
||
|
||
**选型结论**:纯文本、**定长 64B 记录、一行一条**(ISO 时间戳 + epoch + UTF-8 描述);追加写 + 容量上限截断;RAM 队列 + 单写者任务落盘。
|
||
不选变长行(需扫描计数/解析,掉电半行难界定);不选二进制格式(违背 R5 FTP 直读)。
|
||
|
||
## 4. 总体设计
|
||
|
||
### 4.1 模块与文件清单
|
||
|
||
| 文件 | 端 | 说明 |
|
||
|---|---|------|
|
||
| `App/log/sys_log.h` | 共享 | 公共 API + 记录格式常量 + 行拆分内联函数(无 OS 依赖) |
|
||
| `App/log/sys_log_mcu.c` | MCU | FatFS 后端:待发队列(启动前静态数组 + FreeRTOS 消息队列)、defaultTask 落盘、容量截断、上电/重启分类 |
|
||
| `sim/sim_syslog.c` | 模拟器 | 宿主文件后端(CWD 下与七类一一对应的 `sim_*.log` 七份),直接追加,`time()` 时间戳;**没有队列与丢弃**,`syslog_post_quiet()` 在本端等价于 `syslog_post()`(两端差异登记在 `docs/测试说明/专项验证/模拟器差距清单.md` §2.2) |
|
||
| `Drivers/BSP/UI/ui_standby.h/.c` | 共享 | 空闲检测 + 待机屏(屏幕覆盖层:保存/还原前屏与焦点) |
|
||
| `Drivers/BSP/UI/ui_page_runlog.h/.c` | 共享 | 运行记录内容页描述符(动态行,数据源 = sys_log API) |
|
||
| `Drivers/BSP/UI/ui_page_selftest.h/.c` | 共享 | 自检记录内容页描述符(同上) |
|
||
| `App/app/app_main.c` | MCU | 埋点:RTC/NAND/ADC 失败 → `syslog_post(SYSLOG_SELFTEST, …)` |
|
||
| `Src/freertos.c`(defaultTask USER CODE) | MCU | 挂载失败埋点 + **上电/重启分类记录** + 队列排空落盘循环 |
|
||
| `App/task/net_task.c` | MCU | 埋点:链路超时 → `syslog_post(SYSLOG_SELFTEST, "通讯异常")` |
|
||
| `Drivers/BSP/UI/ui_page_password.c` | 共享 | R1:错误提示 + 重输 |
|
||
| `Drivers/BSP/UI/ui_content_page.h/.c`、`ui_menu.c` | 共享 | 新增 2 个内容页 ID/描述符表项/node 39·40 映射;键事件挂钩空闲通知 |
|
||
| `ui_cn_chars.txt` + `tools/font_gen/` | — | 补 10 字重生成字体(§6) |
|
||
| `MDK-ARM/*.uvprojx`、`simulator/CMakeLists.txt` | — | 新文件双端注册(new-source-file skill) |
|
||
| `simulator/main.c` | 模拟器 | `--logtest` / `--standby` / `--idle <s>` 无头自检与测试钩子 |
|
||
|
||
### 4.2 日志记录格式(定长 64B,一行一条)
|
||
|
||
```
|
||
偏移 长度 内容
|
||
[0..18] 19 "YYYY-MM-DD HH:MM:SS"(ISO 8601 空格变体;墙钟无效时 = "1970-01-01 08:00:00")
|
||
[19] 1 ' '
|
||
[20..29] 10 epoch 秒 ASCII("%010u",机器解析/上电重启分类用;墙钟无效 = "0000000000")
|
||
[30] 1 ' '
|
||
[31..58] 28 描述 UTF-8(≤28B = 9 个汉字),不足补 '\0'
|
||
[59..63] 5 '\0' 填充
|
||
```
|
||
|
||
示例行:`2026-09-14 16:48:34 1789465714 装置上电记录`(后随 NUL 填充至 64B)。
|
||
|
||
**定长收益**:条数 = `filesize / 64`(O(1),免扫描);第 k 条偏移 = `k*64`(随机读,UI 翻页零解析成本);掉电半行被 `floor(size/64)` 自然排除。
|
||
**epoch 10 位约束**:2026 年 Unix 秒已达 10 位(2001-09-23 起),字段按 `%010u` 定宽;uint32 上限 4294967295(2106 年)内不溢出。
|
||
**描述 ≤28B 约束**:受框架动态行栈缓冲 `CP_ROW_BUF_LEN=32`(含 NUL)限制,不改框架。
|
||
|
||
### 4.3 文件与容量
|
||
|
||
**记录类型 = 日志 ID 本身,一类记录 = `/log` 下一个文件**(记录体内没有类型字段位,
|
||
理由与逐类判据、写入点清单见 `App/log/README.md`「五类记录与『记录类型 = 日志 ID』」——
|
||
本节只登记"有哪些文件、各多大"这一层,判据表**不在这里抄第二份**)。
|
||
|
||
| 文件 | 记录类型(= `syslog_id_t`) | 上限 | 满时策略 |
|
||
|---|------|------|---------|
|
||
| `/log/run.log` | `SYSLOG_RUN` 上电/重启与运行事件 | **1024 条(64KB)** | 删除重建(整卷清空,串口告警);DTU 上电频率低,容量充裕 |
|
||
| `/log/selftest.log` | `SYSLOG_SELFTEST` 自检故障(有问题才记;来源含**卷锁 fail-open 边沿留痕**,见 §4.4 最后一条) | **512 条(32KB)** | 同上 |
|
||
| `/log/accid.log` | `SYSLOG_ACCIDENT` 事故(跳闸型动作) | **512 条(32KB)** | 同上 |
|
||
| `/log/alarm.log` | `SYSLOG_ALARM` 保护告警 | **1024 条(64KB)** | 同上 |
|
||
| `/log/start.log` | `SYSLOG_START` 保护启动(越限电平) | **1024 条(64KB)** | 同上 |
|
||
| `/log/oper.log` | `SYSLOG_OPER` 操作(改动了装置状态的运维动作) | **512 条(32KB)** | 同上 |
|
||
| `/log/yk.log` | `SYSLOG_YK` 遥控(104 命令的选择/执行/拒绝) | **256 条(16KB)** | 同上 |
|
||
|
||
- 表内上限是**逻辑上限,不是预分配**:文件按追加写自然增长,一条不写就不占簇,到顶才
|
||
删除重建;七类全满的名义峰值 = 4864 条 × 64 B = **304 KB**(NAND 2 GB,且这些都只在
|
||
事件边沿写,不构成容量或磨损瓶颈)。
|
||
- 告警/启动给到 1024 条:一次系统故障会连片触发多个元件,别让最能说明故障过程的两类
|
||
记录最先被截掉。
|
||
- `id` 只准**尾部追加**:枚举序 = 两端 `s_paths[]`/`s_caps[]` 的下标,中间插值会让
|
||
某类记录安静地写进别的文件(不报错、屏上照看照翻),机械防线是
|
||
`TC-SYSLOG-130`/`131`/`131b`(逐 id 验"哪类落哪个文件、各用各的上限")。
|
||
- 另有两类**不属于本表**的文件各有各的属主与通道:`/log/wdev.log`(停喂取证,
|
||
`sys_log_evidence()`,§6)、`/log/soe.log`(二进制 SOE,64 B 定长但**另一套
|
||
布局与读者**,见本节末尾与 `App/log/README.md` soe 小节)。
|
||
- 目录 `/log/` 首次落盘前 `f_mkdir`(已存在则忽略);文件名均**严格 8.3**
|
||
(`FF_USE_LFN=0`:主名 ≤8、扩展 ≤3、不以点开头),逐条机器核
|
||
`TC-SYSLOG-081`/`130c`。
|
||
- FTP 视角:卷根下 `LOG` 目录内上述记录文件均为**同一格式**的 64 B 定长文本记录,
|
||
可直接下载后按固定偏移解析(R5)。
|
||
- 记录**只增不改不删**;删除只有两条既有途径——满时重建,以及 UI「清除记录」
|
||
(`syslog_request_clear(id)` 请求 / defaultTask `syslog_drain()` 尾部落地,
|
||
语义与判据见 `App/log/README.md`,O3 已由此关闭)。
|
||
|
||
### 4.4 写路径(单写者,调研结论落地)
|
||
|
||
**待发条目存副本而非指针(B 方案整改,2026-10-24,台架实证驱动)**:`syslog_entry_t` 原为
|
||
`{ uint8_t id; const char *desc; }`——只存调用方指针,隐含「desc 必须活到落盘」的契约,
|
||
但该契约既没写进 `sys_log.h`、也没有任何机械检查。一次可控台架实验把它照了出来:临时在
|
||
testTask 挂投递钩子,复用**同一块缓冲**连投 12 条带序号记录,并与一次 8 MB FTP STOR(占住
|
||
卷锁)重叠——落盘的 12 条变成「1~5 + 第 12 条七份」,即所有滞留条目别名到该缓冲的最后一次
|
||
写入。危险不止于内容错:卷忙整批回投让条目最长滞留到卷空闲,生产者若投栈缓冲,函数返回后
|
||
就是悬垂读。现改为条目内嵌定长缓冲 `char desc[SYSLOG_DESC_MAX + 1u]`,`syslog_post` 投递时按
|
||
`SYSLOG_DESC_MAX` 截断拷贝并补终止符,调用方缓冲区可立即复用;`sys_log.h` 的参数说明同步
|
||
改写。RAM 代价(`scripts/ram_report.py` 同口径前后各跑一次):`ZI 131,500 → 131,852 B`
|
||
(+352 B,`RW_IRAM1` 57.4% → 57.7%)、`Code +60 B`、`RW` 不变;`RW_CCM` 静态不变,队列
|
||
存储多要的 192 B 取自 FreeRTOS 堆池,烧录后串口水位行 `[STACK] heap free=15624
|
||
min_ever=8464` 说明余量充足。回归由 **TC-SYSLOG-108**(投出去之后改写调用方缓冲,落盘必须
|
||
仍是投递时的原文)与 **TC-SYSLOG-108b**(超长在条目内钳位)钉住。**为什么主机侧此前照不出
|
||
来**:用例传的都是字符串字面量、静态存储期无限,正好绕开生命周期维度——要照出这类问题必须
|
||
专门构造可变缓冲。
|
||
|
||
```
|
||
任意上下文 syslog_post(id, desc)
|
||
├─ FreeRTOS 未运行(app_main_init 期)→ 静态待发数组(8 槽,满则丢弃+计数)
|
||
└─ 调度器已运行 → osMessageQueueSend(8 槽,满则丢弃+计数;日志不得阻塞业务)
|
||
|
||
defaultTask(唯一写者,挂载成功后):
|
||
1) f_mkdir("/log")(幂等)
|
||
2) 排空启动前数组 + 队列 → 按文件合并落盘(一批最多 8 条:一次
|
||
f_open FA_OPEN_APPEND → f_write(n×64B) → f_close,见下方「日志读写放大收口」说明)
|
||
3) 写"上电/重启"分类记录(§5.3,基于 run.log 末条 epoch)
|
||
4) 主循环:osMessageQueueGet(阻塞, 10s 超时) → 落盘;超时空转继续 LED 闪烁
|
||
```
|
||
|
||
- **日志读写放大收口(2026-10-15,写侧):一次排空合并成"每文件一次 open/write/close"**。旧实现每条记录一轮,而 `f_close` 要回写目录项、`f_write` 要对目标扇区做读-改-写 → **每条日志 2~4 次 NAND 页读写**。现在同一文件的一批(≤8 条 = 512 B 静态缓冲 `s_batch`)合并成一次追加写:
|
||
- **掉电语义不变**:批次仍在**同一次 `syslog_drain()` 调用内**写完并 `f_close`;`f_close` 内部 `sync_fs` → `disk_ioctl(CTRL_SYNC)` 会 `ftl_flush_cache()` 把 FTL 的单页写回缓存刷进 NAND(`Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` 的 `CTRL_SYNC` 分支)。落盘时机与旧实现每条 close 等价,**批次不跨调用驻留 RAM**:"写完即关"的路径关文件即落盘,残余只到"单次写事务进行中掉电/复位"为止(`../任务规划/登记簿.md` 条目「写回缓存空闲 flush」:无空闲/掉电前周期 flush,2026-10-22 用户裁定暂不做)。
|
||
- **条目不丢**:启动前数组按下标"看而不删",整批成功才删;队列条目在落盘失败(-2 卷忙 / -1 写失败 / -3 容量不足)时**回投队列**。
|
||
⚠ **2026-10-24 更正本条**:旧实现里 -2 只在**按条慢路径**被正确回投,**批量路径根本没有 -2 分支**(`done` 保持 `n` → 回投循环空转)→ 卷忙时一批(≤8 条)既不落盘也不回投、静默丢失,而本节长期写着"条目不丢"替它背书。现已补 `rc == -2` 分支整批回投(不计故障、不打日志、保序),由 `test/host/sys_log_mcu/` 的 TC-SYSLOG-035 组逐条钉住。**真机必验**:FTP 传输期间持续投日志,传输结束后条数 == 投递条数、顺序不乱。
|
||
- **容量轮转仍只在按条慢路径做**:批量路径发现剩余容量装不下一批时返回 -3,退回逐条 `s_append()`("满了 unlink 重建"只保留一处实现,避免一批跨多轮重建)。
|
||
- 自愈重试(L1 丢弃重建 + 重试一次)与错误去重(L2 只报第一次)判据与单条路径一致。
|
||
- **自愈触发集分类(2026-10-24,代码侧唯一入口 `s_is_selfheal_code()`)**:只有"删了这个文件真能修好"的介质/结构类码才触发 unlink 重建——`FR_DISK_ERR` / `FR_INT_ERR` / `FR_NO_FILESYSTEM` / `FR_DENIED`;**名字/参数类严禁归入**(`FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT`,另显式排除 `FR_NOT_READY` / `FR_NOT_ENABLED` / `FR_TIMEOUT`)。同名同因下 unlink 必然同样失败,旧集合的真实后果是**每轮排空删一次自己的历史日志且永远修不好**,违反 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 10 条。`f_write` 之后的"失败/短写 → unlink 重试一次"**不接**本判定(能走到那里说明已打开成功;而簇分配失败是 `FR_OK` + 短写、根本没有码可判,必须无条件重试)。钉住:TC-SYSLOG-101 / 101b / 102 / 103;`App/log/soe_store.c` 侧同源但取**更保守子集**(TC-SOESTORE-022 / 022b)。
|
||
- **卷锁 fail-open(零互斥那一态)边沿补投一条自检记录(2026-10-27 道G task-40)**:锁层只自增计数、
|
||
绝不产日志——唯一能落盘的通道(`/log/wdev.log` 等)自己也要取这把**非递归**卷锁,被卷忙
|
||
挡住时既写不出、也不许在失败分支里再取同一把锁(机理与四态语义的唯一出处是
|
||
`App/fs/vol_lock.h` 的「可观测性」段,本文不复述)。因此留痕由唯一写者在 `syslog_drain()`
|
||
尾部按**边沿**补投:判据是"计数比上一轮读到的大了",不是"计数大于零"(后者是电平,每轮一条
|
||
= 每轮一次 NAND 写,诊断自己变成负载);**一个 episode(= 一次上电)只投一条**,重新上电
|
||
复位 latch 后故障复发仍会再投(否则第二次开机起永久静默)。文本形如
|
||
`vollock failopen=3`(累计值就是证据,不带态号),落 `selftest.log`。
|
||
⚠ 三条约束(与 task-23「日志让路不计 `[ERR]`」同一根判据):① 不在失败调用点就地打日志;
|
||
② 不打 DBG 错级、不打 `[ERR]`——`[ERR]` 是"设备出故障"的唯一信号通道(`/serial-boot-test` 的 Forbidden
|
||
也依赖它),塞进可观测性事件就同时不再是故障、不再是禁令;③ 记录自带的时间戳是
|
||
**被观测到的时刻**(粒度 = 一轮 `syslog_drain()`),不是发生时刻,所以本模块刻意不再另造
|
||
`obs_first_ms`/`obs_last_ms` 字段去"看起来更精确"。主机判据:`TC-SYSLOG-137` 至
|
||
`TC-SYSLOG-137d`(首投一条并带累计值 / 同 boot 不再投 / 计数不变不投 / 重新上电可再投),
|
||
其中 `g_sys_err_count == 0` 就是"不打错级"的机器证据。
|
||
- **留痕节流分两层、各自独立抑制位**:drain 侧"整批回投"只在正常→故障那一次报(`write fail, requeue %lu entries`,落盘成功一轮复位;旧文案写作 "drop N entries" 与实际行为相反——整批其实一条没丢);post 侧 `queue full, drop` / `preboot full, drop` 各一位(**丢弃语义不变,满则丢照旧**,只压留痕次数)。节流之后**没有丢弃计数**——要计数属新增对外可见信息,另列后续项。
|
||
- 读路径与写路径并发安全:UI 页在 uiTask 经 `syslog_get_record()` 读记录(内部走 `s_read_raw()`,见 §4.5);读写两侧都取卷锁 try 语义,FatFS 系统锁保护单次调用。
|
||
- **存储未就绪/挂载失败**:待发条目滞留 RAM(下次上电丢失),串口打印告警——存储故障时本就无法持久化,属预期行为。
|
||
|
||
### 4.5 读路径(UI / 模拟器)
|
||
|
||
```c
|
||
uint32_t syslog_count(syslog_id_t id); /* = filesize/64 */
|
||
int syslog_get_record(syslog_id_t id, uint32_t idx, char *ts, size_t ts_len,
|
||
char *desc, size_t desc_len); /* 越界 / 读失败 / 缓冲容量不足 均返回 -1 */
|
||
```
|
||
|
||
- MCU:**8 条滑动读窗口**(日志读写放大收口,读侧)——`s_read_raw()` 先查窗口(同 id + 代数未变 + 落在区间内 → **零 I/O** 直接从 512 B 静态窗口返回);未命中才 `f_open(FA_READ) → f_lseek(窗口首条×64) → f_read(≤8×64B) → f_close` **一次取回连续 8 条**,再从中切出请求的那条。旧实现每读一条都要 open/lseek/read/close 一轮,UI 翻 20 条记录 = 20 次目录项/FAT 往返。
|
||
- 失效机制:写者每次落盘后把 `s_rd_gen[id]` 自增,读者比对填充时记录的代数,不匹配就重填。对齐 32 位在 ARM 上原子,跨任务(uiTask 填、defaultTask 增)无需临界区;最坏情况多读一轮,不会读错。
|
||
- **API 与调用方一律不变**(`syslog_get_record()` 签名照旧,`ui_page_runlog` / `ui_page_selftest` 零改动)。
|
||
- **容量契约(2026-10-24 写死在 `App/log/sys_log.h`)**:`ts_len > SYSLOG_TS_LEN`、`desc_len > SYSLOG_DESC_MAX`,**不足一律 -1,既不静默截断也不写调用方一个字节**(描述在写入侧已截到 `SYSLOG_DESC_MAX`,按最小容量给就一定装得下,读侧不需要第二道截断)。旧实现里那句 `if (dl >= desc_len) dl = desc_len - 1` 由上面的守卫保证**永不可达**,已删除;它的存在还会误导调用方"给 28 字节缓冲能读到 27 字符"——实际是整条读不到。UI 侧现用 32 B 列缓冲,走允许路径、行为不变。钉住:TC-SYSLOG-057、057b(0x7A 哨兵逐字节回比)、098。
|
||
- 模拟器:宿主文件同语义(UI 页双端零差异)。
|
||
- UI 页每帧仅读当前窗口 ≤8 条记录(每次重绘 8×64B SPI 读,用户翻页触发,频率极低)。
|
||
|
||
## 5. 功能设计
|
||
|
||
### 5.1 R1 密码错误提示(`ui_page_password.c`)
|
||
|
||
- ENTER 比对失败 → `s_err=1`:3 框清零、光标回首框,**数字框下方居中显示"密码输入错误"**(y≈104)。
|
||
- 错误态下按 ↑↓←→:**先清除提示**再正常执行该键(即"让用户重新输入");再按 ENTER = 重新比对。
|
||
- 比对成功路径不变(回调授权/关页)。布局常量与绘制沿用现有 culling 模式,零 LVGL 对象。
|
||
|
||
### 5.2 R2 待机显示页(`ui_standby.c/h`)
|
||
|
||
**进入条件**:UI 初始化完成后,**60s(默认,可配)内无任何按键** → 切待机屏。
|
||
- 空闲检测 = `lv_timer`(1s 周期,MCU/模拟器均经 `lv_timer_handler` 驱动,无 OS 依赖);
|
||
- 活动通知挂钩:`menu_key_cb`、`cp_key_cb` 每收到一个键调 `ui_standby_notify_activity()`(重置计时)。
|
||
|
||
**待机屏内容**(参考 mockup,全 ASCII 无需新字体字):
|
||
|
||
```
|
||
Ib = 0.00A ← 6 行电流值(v1 占位 0.00A,与 AC/DC 页现状一致;
|
||
Ic = 0.00A 待交流量标定管线落地后接 TPAFE5160 实时值)
|
||
Io = 0.00A
|
||
Ia2 = 0.00A
|
||
Ib2 = 0.00A
|
||
Ic2 = 0.00A
|
||
────────────────────── ← 分隔线(y≈132)
|
||
YYYY-MM-DD HH:MM:SS ← 底部实时时钟(每秒重绘,lv_timer 驱动 invalidate)
|
||
```
|
||
|
||
**键语义(用户定案)**:待机屏上**只有 ENTER(确认键)进入菜单**;其余按键忽略(同时重置空闲计时)。
|
||
- 实现为**屏幕覆盖层**:进入时保存当前活动屏 + 组内焦点对象,加载待机屏并聚焦其容器;ENTER 时还原前屏与焦点。
|
||
- 内容页打开期间超时 → 同样切待机(页面对象保留在底层,ENTER 返回后原样恢复,编辑态不丢)。
|
||
|
||
### 5.3 R3 运行记录(上电/重启分类)
|
||
|
||
defaultTask 挂载成功、排空待发队列**之后**写一条:
|
||
|
||
```
|
||
读 run.log 末条记录的 epoch 字段 E:
|
||
墙钟有效 且 (now - E) < 600s → "装置重启记录"(近期刚运行过 = 软复位/看门狗)
|
||
其余(无文件/首条/间隔≥10min/墙钟无效)→ "装置上电记录"
|
||
```
|
||
|
||
- 阈值 `SYSLOG_REBOOT_WINDOW_S = 600`:断电 <10min 会被误判为重启——无掉电检测硬件时该歧义不可消除,取工程折中并在此声明。
|
||
- 串口标记(供 /serial-boot-test):`runlog boot entry: 装置上电记录 / 装置重启记录`(`sys_log_mcu.c` `DBG_INFO`,与落盘描述串同源)。
|
||
|
||
### 5.4 R4 自检记录(v1 项表)
|
||
|
||
| 检查点 | 触发时机 | 描述串(落盘 + UI 显示同源) |
|
||
|---|------|------|
|
||
| RTC/墙钟建立失败 | app_main_init | `时钟芯片异常` |
|
||
| NAND 初始化失败 | app_main_init | `NAND异常` |
|
||
| ADC 初始化超时 | app_main_init | `ADC异常` |
|
||
| FatFS 挂载/建卷失败 | defaultTask(仅串口,存储不可写) | (无法落盘,串口 `[SELFTEST] fs: FAIL`) |
|
||
| 网络链路 3s 未建立 | net_task bring_up 后 | `通讯异常` |
|
||
|
||
- **只记故障**;全部通过不写记录(与 mockup"有问题进行记录"一致)。
|
||
- v1 不做"已复归/动作/标志字"(mockup 中为保护继电器专有语义)——列入开放问题 O2。
|
||
- UI 页空态:`count_cb` 返回 0 时以单条伪行 `无记录` 呈现(页脚"共 1 页",避免"共 0 页")。
|
||
|
||
## 6. UI 布局与字体
|
||
|
||
### 6.1 新增字符集(11 字)→ 重生成 `ui_font_cn_12.c`
|
||
|
||
| 用途 | 新字 |
|
||
|---|------|
|
||
| 密码错误提示"**密码输入错误**" | 输、入、错、误 |
|
||
| 运行记录"装置**上电**记录 / 装置**重**启记录" | 上、电、重 |
|
||
| 自检故障名"时**钟芯片异常**"等(异/常为公共后缀) | 芯、片、异、常 |
|
||
|
||
流程按 `font-regen` skill:改 `ui_cn_chars.txt` → extract_cn → make_font → diff。注意:**extract_cn 只扫 UI 源码**,故障描述串在非 UI 源码(sys_log_mcu.c/app_main.c/net_task.c)中,须手动补入字符集;其余文案(运行记录/自检记录/无记录/第 X 页共 Y 页)均已在字符集内;待机页全 ASCII。
|
||
|
||
### 6.2 页面几何(160×160,沿用框架常量体系)
|
||
|
||
| 页 | 参数 |
|
||
|---|------|
|
||
| 运行记录 / 自检记录 | `selectable=1, rows_per_page=8, top_y=24, pitch=14, band_h=13`(同 AC/DC 约定;**偏离 mockup**:实机照片无选中带,但框架 selectable 页选中行恒黑字、band_h=0 时黑字黑底不可见,故保留标准白带),`left_x=7`;**每条记录 = 2 行**:偶数行 = 时间戳(`YYYY-MM-DD HH:MM:SS`,19 ASCII ≈114px 可容)、奇数行 = 描述(≤9 汉字);↑↓按行移动(8 行/页 = 4 条/页),页脚自动分页 |
|
||
| 待机屏 | 6 数据行 y=28 起 pitch=18(label x≈24 / `=` x≈72 / 值 x≈96);分隔线 y≈132;时钟 y≈140 居中 |
|
||
| 密码页错误提示 | "密码输入错误" 居中,y≈104(数字框行 y=78..92 之下) |
|
||
|
||
## 7. 内存预算
|
||
|
||
| 项 | 大小 | 位置 |
|
||
|---|------|------|
|
||
| sys_log MCU:启动前待发数组 8×12B + 队列 8×12B + 状态 | ≈300B(静态)+ ≈150B(FreeRTOS 堆,消息队列) | .bss / heap_4 |
|
||
| ui_standby:待机屏 + 容器 LVGL 对象(常驻) | ≈300B | LVGL 池(16KB,当前占用 ≈5KB) |
|
||
| 记录页模块 | 0(描述符驱动,零 LVGL 对象;仅栈缓冲) | — |
|
||
| 字体 +10 字形 | ≈250B Flash | .rodata |
|
||
|
||
**结论:不新增任务、不动任务栈、FreeRTOS 堆增量 <200B,LVGL 池余量充足。**
|
||
|
||
## 8. 测试计划
|
||
|
||
### 8.1 模拟器(无头自动化)
|
||
|
||
| 用例 | 手段 |
|
||
|---|------|
|
||
| 日志引擎闭环 `--logtest` | post N 条 → count/get 回读断言 + 宿主文件字节级校验(64B 定长、ISO 时间戳、NUL 填充),0=PASS(仿 `--cfgtest`) |
|
||
| 待机屏渲染 `--standby` | 无头注键后直接调测试钩子强制进入待机 → 截图比对(布局/时钟行);窗口模式 `--idle <s>` 可人工验证真实超时路径 |
|
||
| 记录页像素回归 | `--keys` 导航至 node 39/40 → 截图 + 文本断言(时间戳行/描述行/页脚分页),纳入 ui-verify 门禁 |
|
||
| 密码错误提示 | `--keys` 输错密码序列 → 截图断言"密码输入错误"可见;再按方向键 → 提示消失 |
|
||
|
||
### 8.2 真机(/serial-boot-test 串口标记)
|
||
|
||
Required:`App init OK`、`Network Init`、`net init: OK`、**`runlog boot entry:`**(装置上电记录 / 装置重启记录)、**`UI standby: ready (idle=60s)`**;
|
||
Forbidden:`FAIL`/`ERROR`/`HardFault`。
|
||
人工目检:待机屏(静置 60s)、记录页翻页、FTP 下载 `/log/*.log` 文本可读性。
|
||
|
||
## 9. 风险与开放问题
|
||
|
||
| # | 项 | 策略 |
|
||
|---|----|------|
|
||
| O1 | 断电 <10min 的"上电/重启"歧义 | §5.3 阈值折中,文档声明;如需精确可后续加掉电标志(PVD) |
|
||
| O2 | 故障"已复归"/持续监测(mockup 语义) | v2:周期自检 + 活动故障状态机 + 恢复记录 |
|
||
| O3 | ~~"清除记录"菜单项~~ **已实施(2026-10-26 道F task-4)** | 数据面 = `syslog_request_clear(id)` + `syslog_clear_state(id)`,落地在 defaultTask(唯一写者)的 `syslog_drain()` 尾部;UI 侧页面归记录/维护页批次。细口径见 `App/log/README.md` |
|
||
| O4 | 待机页电流值接实时数据 | 待交流量标定管线(AC 页占位数据替换)落地后接入 `tpafe5160_snapshot()`(原子快照;旧 `tpafe5160_get_buf()` 已于 2026-10-10 删除,见 `AGENTS.md`) |
|
||
| — | FAT32 小追加簇开销 | §3 调研结论:写频率极低可接受;容量上限约束增长 |
|
||
| — | UI 读与落盘并发 | 只读既有内容 + EOF 追加 + FatFS 系统锁,无互斥需求(§4.4) |
|
||
|
||
## 10. 实施步骤(按序执行,每步双端构建 0/0)
|
||
|
||
> ✅ **步骤 1–9 已于 2026-09-14 全部完成**:Keil `@build` 0 错 0 警 + 模拟器 0 警告;步骤 8 = `--logtest` PASS(期间修复 ts 19 字符 / epoch 10 位两处格式 bug)+ ui-verify 五门禁全绿(池基线 5440/5520B、pwc 参考更新)+ 记录页/待机屏/密码错误提示截图目检通过;步骤 9 = 文档同步。
|
||
|
||
| # | 步骤 | 产物 |
|
||
|---|------|------|
|
||
| 步骤 1 | 字体补字 + 重生成 | `ui_cn_chars.txt`、`ui_font_cn_12.c` |
|
||
| 步骤 2 | 日志引擎:`sys_log.h`(共享 API/格式)+ `sys_log_mcu.c` + `sim/sim_syslog.c` | 双端可 post/count/get |
|
||
| 步骤 3 | 自检埋点 + defaultTask 落盘循环 + 上电/重启分类 | app_main/freertos/net_task 接线 |
|
||
| 步骤 4 | 密码错误提示 | ui_page_password.c |
|
||
| 步骤 5 | 待机屏 `ui_standby`(空闲检测 + 覆盖层)+ 键挂钩 | ui_menu/ui_content_page 各一行通知 |
|
||
| 步骤 6 | 记录页 ×2 + 内容页框架接线(枚举/描述符表/node 映射) | ui_page_runlog/selftest、ui_content_page.h/.c、ui_menu.c |
|
||
| 步骤 7 | 双端注册新文件(uvprojx + CMakeLists)+ 构建 0/0 | new-source-file skill |
|
||
| 步骤 8 | 模拟器无头回归(--logtest/--standby/截图断言)+ 真机 /serial-boot-test | 测试通过记录 |
|
||
| 步骤 9 | 文档同步(项目架构总览、AGENTS.md、本文档状态) | docs |
|