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:
@@ -639,7 +639,7 @@ repos:
|
||||
# 疑似 MSBuild 节点争用;此后 3 次干净树连跑均绿。判据:单次失败先复跑一次再看,
|
||||
# 连续两次失败才当真(不许直接 --no-verify 绕过)。
|
||||
- id: ui-golden
|
||||
name: UI 像素回归(菜单 8 状态 + 内容页 11 页 黄金参考)
|
||||
name: UI 像素回归(菜单 8 状态 + 内容页 27 页 黄金参考)
|
||||
entry: python scripts/check_ui_golden.py
|
||||
language: system
|
||||
files: ^(Drivers/BSP/UI/|App/task/ui_task\.(c|h)$|simulator/(tests|sim|src)/|simulator/main\.c$|simulator/CMakeLists\.txt$|simulator/lv_conf\.h$|scripts/check_ui_golden\.py$|\.pre-commit-config\.yaml$)
|
||||
|
||||
+68
-10
@@ -18,12 +18,20 @@ typedef struct {
|
||||
uint8_t over; /* 1 = 过量(越上限动作),0 = 欠量(越下限动作) */
|
||||
uint8_t src; /* 测量源:0 = phasor 基波有效值,1 = freq 测频 */
|
||||
prot_set_id_t en_id; /* 投退定值 */
|
||||
uint8_t trip_ctl; /* 跳闸投退定值项(PS_*_TRIP);PROT_ELM_NO_TRIP =
|
||||
* 该元件没有这一项(告警类)→ 动作只出告警记录。
|
||||
* 用 uint8_t 而不是 prot_set_id_t 就是因为要能表达
|
||||
* "无此项"这个第 41 种取值(PS_ID_COUNT 是数组长度,
|
||||
* 拿去喂 prot_set_get_ctl() 会越界读) */
|
||||
prot_set_id_t set_id; /* 动作定值 */
|
||||
prot_set_id_t dly_id; /* 延时定值(ms) */
|
||||
prot_set_id_t ret_id; /* 返回系数定值(RET_OVER / RET_UNDER) */
|
||||
const char *pprefix; /* 相量实例名前缀("C01"…);src=1 时为 0(不用) */
|
||||
} pe_cfg_t;
|
||||
|
||||
/* k_pe[].trip_ctl 的"无此定值项"取值(见上方字段注释) */
|
||||
#define PROT_ELM_NO_TRIP 0xFFu
|
||||
|
||||
/* 测量源枚举(pe_cfg_t.src) */
|
||||
#define PE_SRC_PHASOR 0u
|
||||
#define PE_SRC_FREQ 1u
|
||||
@@ -31,18 +39,24 @@ typedef struct {
|
||||
/* freq 无有效读数时给欠量元件的"必不启动"填充值(远大于任何欠量定值上限) */
|
||||
#define PE_MEAS_BIG 1e30f
|
||||
|
||||
static const pe_cfg_t k_pe[PE_COUNT] = {
|
||||
{ "OC1-IA", 1u, PE_SRC_PHASOR, PS_OC1_EN, PS_OC1_CUR, PS_OC1_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC2-IA", 1u, PE_SRC_PHASOR, PS_OC2_EN, PS_OC2_CUR, PS_OC2_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC3-IA", 1u, PE_SRC_PHASOR, PS_OC3_EN, PS_OC3_CUR, PS_OC3_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OL-IA", 1u, PE_SRC_PHASOR, PS_OL_EN, PS_OL_CUR, PS_OL_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OV-UA", 1u, PE_SRC_PHASOR, PS_OV_EN, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
|
||||
{ "UV-UA", 0u, PE_SRC_PHASOR, PS_UV_EN, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
|
||||
{ "3U0-UN", 1u, PE_SRC_PHASOR, PS_3U0_EN, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
|
||||
/* 列序 = pe_cfg_t 字段序(name, over, src, en_id, trip_ctl, set_id, dly_id, ret_id,
|
||||
* pprefix)。⚠ trip_ctl 紧跟 en_id 插入,新增元件必须照这一列的口径填:有跳闸投退
|
||||
* 定值项的填 PS_*_TRIP,没有的填 PROT_ELM_NO_TRIP(= 该元件动作只出告警记录)。*/
|
||||
static const pe_cfg_t k_pe[PE_COUNT] = { /* ← 表头注释见上,行内列序与之一致 */
|
||||
{ "OC1-IA", 1u, PE_SRC_PHASOR, PS_OC1_EN, PS_OC1_TRIP, PS_OC1_CUR, PS_OC1_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC2-IA", 1u, PE_SRC_PHASOR, PS_OC2_EN, PS_OC2_TRIP, PS_OC2_CUR, PS_OC2_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC3-IA", 1u, PE_SRC_PHASOR, PS_OC3_EN, PS_OC3_TRIP, PS_OC3_CUR, PS_OC3_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OL-IA", 1u, PE_SRC_PHASOR, PS_OL_EN, PS_OL_TRIP, PS_OL_CUR, PS_OL_DLY, PS_RET_OVER, "C01" },
|
||||
/* ↓ 三项 + 下面两项频率元件:参考系统的告警段(0x8027~0x802A)本就没有「跳闸
|
||||
投退」这一档,g_prot_set_meta 也无对应项 → PROT_ELM_NO_TRIP,动作一律归告警。
|
||||
这不是兜底猜测,是照实反映("哪些元件算跳闸"的真源始终是这张表 + 定值表)。*/
|
||||
{ "OV-UA", 1u, PE_SRC_PHASOR, PS_OV_EN, PROT_ELM_NO_TRIP, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
|
||||
{ "UV-UA", 0u, PE_SRC_PHASOR, PS_UV_EN, PROT_ELM_NO_TRIP, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
|
||||
{ "3U0-UN", 1u, PE_SRC_PHASOR, PS_3U0_EN, PROT_ELM_NO_TRIP, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
|
||||
/* 过频/低频(L3-5,B8):测量量 = freq 层测得频率(Hz),返回系数独立
|
||||
* (RET_OF/RET_UF——频率滞回带与电气量不同量级,勿共用 0.95/1.05) */
|
||||
{ "OF-F", 1u, PE_SRC_FREQ, PS_OF_EN, PS_OF_HZ, PS_OF_DLY, PS_RET_OF, 0 },
|
||||
{ "UF-F", 0u, PE_SRC_FREQ, PS_UF_EN, PS_UF_HZ, PS_UF_DLY, PS_RET_UF, 0 },
|
||||
{ "OF-F", 1u, PE_SRC_FREQ, PS_OF_EN, PROT_ELM_NO_TRIP, PS_OF_HZ, PS_OF_DLY, PS_RET_OF, 0 },
|
||||
{ "UF-F", 0u, PE_SRC_FREQ, PS_UF_EN, PROT_ELM_NO_TRIP, PS_UF_HZ, PS_UF_DLY, PS_RET_UF, 0 },
|
||||
};
|
||||
|
||||
/* 运行时状态(picked/asserted 的对外快照 + 延时起点 + 相量实例索引缓存) */
|
||||
@@ -259,6 +273,50 @@ uint32_t prot_elm_active_mask(void) {
|
||||
return mask;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:聚合启动位图
|
||||
* 入口参数:无
|
||||
* 返 回 值:bit i = 元件 i picked(越限电平,早于/等于 asserted)
|
||||
* 限定条件:任意上下文(只读快照,无锁;与 prot_elm_active_mask 同口径)
|
||||
* 函数说明:调用方是 protectTask 的「保护启动」记录边沿检测(task-10)。本函数
|
||||
* **只聚合电平**,不造边沿——边沿属观测者状态(s_prev_pick 在那一侧),
|
||||
* 判据核里再存一份"上一拍"会让 eval 不再幂等。
|
||||
*/
|
||||
uint32_t prot_elm_pick_mask(void) {
|
||||
uint32_t mask = 0u;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
|
||||
if (s_rt[i].picked != 0u) {
|
||||
mask |= (1uL << i);
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:判定该元件动作上升沿应归哪一类记录(见 prot_elm.h 同号声明)
|
||||
* 入口参数:id - 元件编号 prot_elm_id_t
|
||||
* 返 回 值:PROT_REC_TRIP / PROT_REC_ALARM / PROT_REC_NONE(id 越界) prot_rec_kind_t
|
||||
* 限定条件:任意上下文;只读,不改任何状态
|
||||
* 函数说明:判据唯一来源 = k_pe[id].trip_ctl 指向的那枚 PS_*_TRIP 投退定值。
|
||||
* trip_ctl == PROT_ELM_NO_TRIP 时**先返回告警**,绝不把 0xFF 喂给
|
||||
* prot_set_get_ctl()(那是数组越界读,会读到定值区外的 RAM——
|
||||
* 与"未标定拿 ADC 引脚电压当二次值"同类的静默错数据形状)。
|
||||
*/
|
||||
prot_rec_kind_t prot_elm_record_kind(prot_elm_id_t id) {
|
||||
uint8_t trip_ctl;
|
||||
|
||||
if (id >= PE_COUNT) {
|
||||
return PROT_REC_NONE;
|
||||
}
|
||||
trip_ctl = k_pe[(uint32_t)id].trip_ctl;
|
||||
if (trip_ctl == (uint8_t)PROT_ELM_NO_TRIP) {
|
||||
return PROT_REC_ALARM;
|
||||
}
|
||||
return (prot_set_get_ctl((prot_set_id_t)trip_ctl) != 0u) ? PROT_REC_TRIP : PROT_REC_ALARM;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取实例名(日志用)
|
||||
* 入口参数:id - 元件编号
|
||||
|
||||
@@ -100,6 +100,43 @@ void prot_elm_state(prot_elm_id_t id, prot_elm_state_t *out);
|
||||
/* 动作位图(bit i = 元件 i asserted),供 L4 出口 / 心跳日志 */
|
||||
uint32_t prot_elm_active_mask(void);
|
||||
|
||||
/* 启动位图(bit i = 元件 i picked),供「保护启动」记录边沿(task-10,2026-10-26):
|
||||
* picked 是越限滞回后的电平,早于(或同拍于)asserted——只看 active_mask 会让
|
||||
* "启动了但没到点"这一整段在记录里彻底消失,而它正是"保护启动"页要看的东西。 */
|
||||
uint32_t prot_elm_pick_mask(void);
|
||||
|
||||
/*
|
||||
* 元件动作边沿的记录归属(task-10「五类记录」)。
|
||||
* ⚠ 本枚举刻意**不**用 syslog_id_t:那会把 App/log 的头拽进 algo 层,而
|
||||
* App/algo/prot_elm.c 同时被 test/host/prot_dim 编进测试单元(那个运行器的
|
||||
* include 路径里没有 App/log,且不在本道写域)→ 一改就在别人的单测上爆错。
|
||||
* "元件 → 哪一类"是判据域事实(属主在本文件),"哪一类 → 哪个日志文件"是
|
||||
* 记录域映射(属主在 App/task/protect_task.c 的一行三元),两边各自单源。
|
||||
*/
|
||||
typedef enum {
|
||||
PROT_REC_TRIP = 0u, /* 跳闸型动作 → 事故记录 */
|
||||
PROT_REC_ALARM, /* 告警型动作 → 保护告警记录 */
|
||||
PROT_REC_NONE /* id 越界 → 调用方一条都不许落 */
|
||||
} prot_rec_kind_t;
|
||||
|
||||
/*
|
||||
* 函数功能:判定"该元件若发生动作上升沿,应归哪一类记录"
|
||||
* 入口参数:id - 元件编号 prot_elm_id_t
|
||||
* 返回值:PROT_REC_TRIP / PROT_REC_ALARM / PROT_REC_NONE(越界) prot_rec_kind_t
|
||||
* 限定条件:任意上下文(只读 k_pe[] + prot_set RAM,不落任何状态)
|
||||
* 函数说明:判据**唯一来源** = 该元件的跳闸投退定值项 g_prot_set_meta 里的
|
||||
* PS_*_TRIP(其枚举注释即"0 告警 / 1 跳闸")。元件→投退项的对应关系
|
||||
* 放在实例表 k_pe[] 的 trip_ctl 列(与 en_id/set_id/dly_id 同一张表,
|
||||
* 新增元件只加一行);**本函数里没有第二份"哪些元件算跳闸"的名单**,
|
||||
* 所以它不会与 g_prot_set_meta / k_pe[] / 整定视图门禁互相打架。
|
||||
* 过压/欠压/零序过压/过频/低频五项本就没有 TRIP 投退项(告警类)→
|
||||
* trip_ctl = PROT_ELM_NO_TRIP → 恒 PROT_REC_ALARM,这是照实反映,
|
||||
* 不是兜底猜测。
|
||||
* 定值巡检闭锁(prot_set_locked)不影响本判定:闭锁态下元件会被复位、
|
||||
* 根本走不到动作上升沿,调用方无需在此重复判闭锁。
|
||||
*/
|
||||
prot_rec_kind_t prot_elm_record_kind(prot_elm_id_t id);
|
||||
|
||||
/* 实例名(ROM 常量,日志用) */
|
||||
const char *prot_elm_name(prot_elm_id_t id);
|
||||
|
||||
|
||||
+163
-17
@@ -5,6 +5,11 @@
|
||||
* (实时量 rt / 参数 par / 装置信息 info),经 Drivers/BSP/UI/ui_data.h
|
||||
* 的发布 API 推给 UI 层;本模块是快照的**唯一填充者**,不含任何布局、
|
||||
* 格式化与 LVGL 调用(显示规格全在页面行表,同文档 §8)
|
||||
* 写方向(道F task-1):同一条 tick 顺带把页面投递的定值整定请求落定——
|
||||
* ui_data_param_drain() 每轮取一条,执行动作经本文件的 s_param_ops
|
||||
* (locked = prot_set_locked / check = g_prot_set_meta 上下限 /
|
||||
* apply = prot_set_set_sealed)注入给 BSP;值域与落盘事实都留在 App
|
||||
* 侧,BSP 只有队列与状态机(契约 §6.1"UI 不能直写"的落地形态)
|
||||
* 量纲诚实性:eng 字段按**真值**填——docs/任务规划/数据可信.md §2.6 把承诺边界定在
|
||||
* "引脚电压",故未做两点标定前 eng = UI_DATA_ENG_PIN(值 = ADC 引脚电压 V,
|
||||
* 不是端子二次值);电气 8 路 adc_cal_flags() 的 VALID 全置位才升
|
||||
@@ -15,6 +20,10 @@
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-14)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-1:① tick 前置 ui_data_param_drain()
|
||||
* (定值写路径出队 → 闭锁/越界预检 → set_sealed,
|
||||
* 见 s_param_ops);② 参数帧尾填 cal_block(未标定
|
||||
* 闭锁徽标,三路口径同 protect_task.c,不用 eng)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -93,11 +102,18 @@
|
||||
* (契约 docs/UI设计/UI_数据供给设计说明.md §9.4 第 1/2 条) */
|
||||
typedef char feed_chk_yc_n_t[(UI_DATA_YC_N == (uint32_t)ADC_SEM_COUNT) ? 1 : -1];
|
||||
typedef char feed_chk_set_n_t[(UI_DATA_SET_N == (uint32_t)PS_ID_COUNT) ? 1 : -1];
|
||||
/* 写路径注入表的 id 形参是 uint8_t(BSP 侧不透明编号宽度,位图只有 64 位):
|
||||
* 定值项数一旦破 64/255,request 的 mask 与 check/apply 的编号都会静默截断,
|
||||
* 故在写侧也钉一道(BSP 侧同一条守卫在 Drivers/BSP/UI/ui_data.c) */
|
||||
typedef char feed_chk_set_u8_t[((uint32_t)PS_ID_COUNT <= 64u) ? 1 : -1];
|
||||
typedef char feed_chk_fuser_t[(UI_DATA_FUSER_LEN >= FTP_CFG_USER_MAX) ? 1 : -1];
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
/* 组装缓冲:三帧串行填充,共用一块存储(取最大者 = 参数帧),省两份 RAM */
|
||||
/* 组装缓冲:三帧串行填充,共用一块存储,省两份 RAM。
|
||||
* 实际最大者是**实时帧**(208 B > par 192 B > info),故给 par 尾追加字段
|
||||
* 只要不超过 rt 就一字节 RAM 都不多花(这条由主机单测 TC-UIDATA-021g/h 钉住,
|
||||
* 尺寸一变这里就是唯一会撒谎的地方) */
|
||||
static union {
|
||||
ui_data_rt_t rt;
|
||||
ui_data_par_t par;
|
||||
@@ -109,10 +125,17 @@ static union {
|
||||
static int8_t s_yc_phasor[ADC_SEM_COUNT];
|
||||
static uint8_t s_map_done;
|
||||
|
||||
/* 发布节拍与参数变化检测状态 */
|
||||
/* 发布节拍与参数变化检测状态
|
||||
* par 帧有**三个各自独立**的变化源,任一变了就要重发一帧(详见 tick 里的门控注释):
|
||||
* s_par_sum 定值改动(校验和)
|
||||
* s_par_locked 定值闭锁(L5-1 自检失配,校验和不动)
|
||||
* s_par_cal_block 未标定闭锁(卡 12 标定完成,校验和也不动)
|
||||
* 后两个各占一字节——省下来就是"屏上状态与真值脱拍"这笔债 */
|
||||
static uint32_t s_last_rt_ms;
|
||||
static uint32_t s_par_sum;
|
||||
static uint8_t s_par_sum_valid;
|
||||
static uint8_t s_par_locked;
|
||||
static uint8_t s_par_cal_block;
|
||||
static uint8_t s_first_done;
|
||||
|
||||
/* 函数定义区 */
|
||||
@@ -386,8 +409,96 @@ static void feed_fill_rt(ui_data_rt_t *p_snap, uint32_t tick_ms) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 写路径(契约 §6.2):把 prot_set 的三个动作包成注入给 BSP 的函数表 ────── */
|
||||
|
||||
/*
|
||||
* 函数功能:组装参数快照(40 项定值 Q10 + 校验和 + 封存态 + 越界位图)
|
||||
* 函数功能:写路径判定"定值闭锁中吗"(BSP 经 ui_data_param_ops_t 来问)
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 - 闭锁(定值校验和自检失败态,禁一切定值写入);0 - 可写 uint8_t
|
||||
* 限定条件:uiTask 上下文(只读 prot_set 状态位)
|
||||
* 函数说明:这里的"闭锁"是**定值闭锁**(= par.locked = YX31 遥信),**不是**未标定
|
||||
* 闭锁(= par.cal_block):后者仍可改定值、只是判据停走,两者在屏上不得
|
||||
* 互认(三词三源口径 docs/任务规划/数据可信.md §4.3 + 落地简报 §7-6)
|
||||
*/
|
||||
static uint8_t feed_param_locked(void) {
|
||||
return prot_set_locked();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写路径整批预检——单项是否落在 meta 上下限内
|
||||
* 入口参数:a_id - 定值项编号(位图位序;uint8_t 是 BSP 侧不透明编号的宽度)
|
||||
* a_q10 - 待写 Q10 定点值
|
||||
* 返 回 值:0 - 合法;-1 - 编号越界或值越界 int
|
||||
* 限定条件:只读 g_prot_set_meta(ROM 表),任意上下文安全
|
||||
* 函数说明:上下限**只此一处取**——BSP 行表(Drivers/BSP/UI/ui_w_setview.c)是它的
|
||||
* 换算镜像,由 setview-check 钩子核一致;本函数再抄一份数字就是第二真相
|
||||
* ("查看合法、整定越界"两套边界就是这么来的)
|
||||
*/
|
||||
static int feed_param_check(uint8_t a_id, int32_t a_q10) {
|
||||
if (a_id >= (uint8_t)PS_ID_COUNT) {
|
||||
return -1;
|
||||
}
|
||||
if ((a_q10 < g_prot_set_meta[a_id].min) || (a_q10 > g_prot_set_meta[a_id].max)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写路径落值——单项**原子**写入 + 重封校验和 + 置脏
|
||||
* 入口参数:a_id - 定值项编号;a_q10 - Q10 定点值
|
||||
* 返 回 值:0 - 已写入;-1 - 编号越界或 prot_set 拒写(原值不动) int
|
||||
* 限定条件:uiTask 上下文;调用前 BSP 已整批预检通过(两条件与本函数一致)
|
||||
* 函数说明:用 prot_set_set_sealed()(影子拷贝→改→重封→单字交换,**每项自带置脏**,
|
||||
* 落盘由 defaultTask 50 ms 圈扫脏写 /prot_set.bin,本模块不碰 FatFs)。
|
||||
* ⚠ 不得改成 prot_set_set() + 批末 prot_set_seal():两条路径之间有
|
||||
* "已改未封存"窗口,prot_set.c 明令不得交错,撞上就被 L5-1 自检判成
|
||||
* "定值被非法改写"→ 整台保护闭锁
|
||||
*/
|
||||
static int feed_param_apply(uint8_t a_id, int32_t a_q10) {
|
||||
if (a_id >= (uint8_t)PS_ID_COUNT) {
|
||||
return -1;
|
||||
}
|
||||
return prot_set_set_sealed((prot_set_id_t)a_id, a_q10);
|
||||
}
|
||||
|
||||
/* 注入给 ui_data_param_drain() 的执行侧实现(BSP 不保存副本,每次调用现传) */
|
||||
static const ui_data_param_ops_t s_param_ops = {
|
||||
feed_param_locked,
|
||||
feed_param_check,
|
||||
feed_param_apply
|
||||
};
|
||||
|
||||
/*
|
||||
* 函数功能:未标定闭锁判定(整定页"未标定"徽标的数据源)
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 - 未标定闭锁(UA/UN/IA 三路至少一路标定 VALID 未置位);0 - 三路全
|
||||
* VALID(保护判据正在跑) uint8_t
|
||||
* 限定条件:uiTask 上下文(adc_cal_flags 纯读,每 200 ms 一次,代价可忽略)
|
||||
* 函数说明:判据与 App/task/protect_task.c 的 s_cal_locked() **逐字同构**——那是
|
||||
* 停走判据的真源,本函数只是把它搬到显示通道上;两处的例外通道也必须
|
||||
* 一起改(DTU_BENCH 台架旁路,仅本地台架自行定义、产品恒强制)。
|
||||
* ⚠ 已知重复:本仓没有导出该判据的公开 API(protect_task.h 只有线程
|
||||
* 入口),故这里镜像一份;将来 protect_task 导出访问器后应收敛成一处
|
||||
* (Lead 补充 A 明确本任务按此实现,不改 protect_task.*)
|
||||
* ⚠ 不得用 rt.eng 反推:eng 要求电气 8 路全 VALID,V0.1 双转换器下
|
||||
* UB/UC/IB/IC/IN 永不置位 → eng 恒为引脚档,照它画徽标就永不解锁
|
||||
*/
|
||||
static uint8_t feed_cal_block(void) {
|
||||
/* 形态与 protect_task.c 同:用 #ifndef 把产品默认路径留在主分支(而非 #ifdef
|
||||
* 双分支)——cppcheck 对未定义宏取真分支,双分支会把它引到旁路上报假信号 */
|
||||
#ifndef DTU_BENCH
|
||||
if (((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) == 0u) ||
|
||||
((adc_cal_flags(ADC_SEM_UN) & ADC_CAL_F_VALID) == 0u) ||
|
||||
((adc_cal_flags(ADC_SEM_IA) & ADC_CAL_F_VALID) == 0u)) {
|
||||
return 1u;
|
||||
}
|
||||
#endif
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:组装参数快照(40 项定值 Q10 + 校验和 + 封存态 + 越界位图 + 未标定闭锁)
|
||||
* 入口参数:p_snap - 目标帧(调用方已清零) ui_data_par_t *
|
||||
* sum - 本次发布的 prot_set_checksum() 值 uint32_t
|
||||
* 返回值:无
|
||||
@@ -406,6 +517,9 @@ static void feed_fill_par(ui_data_par_t *p_snap, uint32_t sum) {
|
||||
/* 越界位图非 0 = 上一次整表装载/提交有越界项(prot_set_bad_mask 的既有口径),
|
||||
* 页面据此反显;为 0 时无意义位图不参与显示 */
|
||||
p_snap->bad_valid = (p_snap->bad_mask != 0u) ? 1u : 0u;
|
||||
/* v2 尾追加:未标定闭锁徽标(三路口径,与判据停走同源;与上面的 locked 是
|
||||
* 两件事——locked 禁编辑,cal_block 只说明"改了也不会动作") */
|
||||
p_snap->cal_block = feed_cal_block();
|
||||
for (i = 0u; i < (uint8_t)PS_ID_COUNT; i++) {
|
||||
p_snap->q10[i] = prot_set_get((prot_set_id_t)i);
|
||||
}
|
||||
@@ -442,23 +556,40 @@ static void feed_fill_info(ui_data_info_t *p_snap, uint32_t tick_ms) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:装配节拍入口——实时帧与信息帧满 200 ms 发布一次,参数帧仅在校验和
|
||||
* 变化时发布;首轮立即发布(免启动初期出现一闪的 "--")
|
||||
* 函数功能:装配节拍入口——先落定一条定值整定请求(写方向),再发布实时帧与
|
||||
* 信息帧(满 200 ms 一次)、参数帧(仅校验和变化时);首轮立即发布
|
||||
* (免启动初期出现一闪的 "--")
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms)
|
||||
* 函数说明:每轮未到期时代价 = 一次 osKernelGetTickCount + 两次减法比较;
|
||||
* 差值法吸收 tick 回绕(同 App/algo/adc_health.h 的 now_ms 口径)
|
||||
* 函数说明:每轮未到期时代价 = 一次 osKernelGetTickCount + 两次减法比较 +
|
||||
* 一次空队列判断(drain 无请求时只是一次计数比较);
|
||||
* 差值法吸收 tick 回绕(同 App/algo/adc_health.h 的 now_ms 口径)。
|
||||
* 写路径失败态(ERR_RANGE/ERR_LOCKED/ERR_BUSY)走屏上通道,不打日志
|
||||
* (每条 DBG 约 90 B Flash + 216 B 栈,AGENTS「日志不是免费的」)
|
||||
*/
|
||||
void ui_data_feed_tick(void) {
|
||||
uint32_t now = osKernelGetTickCount();
|
||||
uint32_t sum = prot_set_checksum();
|
||||
uint32_t now;
|
||||
uint32_t sum;
|
||||
uint8_t due_rt;
|
||||
|
||||
now = osKernelGetTickCount();
|
||||
|
||||
if (s_map_done == 0u) {
|
||||
feed_map_phasor();
|
||||
}
|
||||
|
||||
/* 写路径:先落定整定请求,**再**取校验和——本轮刚写进 prot_set 的定值就进
|
||||
* 本轮"校验和变化才发"那道门,页面保存行的 OK 与新值同一帧回来,不必多等
|
||||
* 一轮(10 ms)。顺序反了不出错,只是慢一轮。
|
||||
* 每轮最多处理一条(ui_data_param_drain 的限定条件):整定是人工低频事件,
|
||||
* 一轮一条已经把最坏耗时钉住,不必排空整个队列
|
||||
* ⚠ 返回值必须显式丢弃:本函数不消费"处理了几条",但 bsp-err 的
|
||||
* consume-result 只认接住或 (void) 两种写法(实测编译器与 cppcheck 都不报) */
|
||||
(void)ui_data_param_drain(&s_param_ops);
|
||||
|
||||
sum = prot_set_checksum();
|
||||
|
||||
due_rt = 0u;
|
||||
if ((s_first_done == 0u) || ((uint32_t)(now - s_last_rt_ms) >= FEED_RT_PERIOD_MS)) {
|
||||
due_rt = 1u;
|
||||
@@ -475,13 +606,28 @@ void ui_data_feed_tick(void) {
|
||||
(void)ui_data_publish_info(&s_scr.info);
|
||||
}
|
||||
|
||||
/* 参数帧:校验和门控——定值变化是低频事件,不随 200 ms 白拷 184 B
|
||||
* (docs/UI设计/UI_数据供给设计说明.md §5.2) */
|
||||
if ((s_par_sum_valid == 0u) || (sum != s_par_sum)) {
|
||||
(void)memset(&s_scr, 0, sizeof(s_scr));
|
||||
feed_fill_par(&s_scr.par, sum);
|
||||
(void)ui_data_publish_par(&s_scr.par);
|
||||
s_par_sum = sum;
|
||||
s_par_sum_valid = 1u;
|
||||
/* 参数帧门控:定值变化是低频事件,不随 200 ms 白拷一帧(契约 §5.2)。
|
||||
* 但"校验和没变"不等于"页面该看的没变"——本帧里还有两个与校验和无关的事实:
|
||||
* · locked(定值闭锁):自检失配时置位,校验和一字不变,
|
||||
* 而整定页保存行要显示 LOCK 并禁编辑;
|
||||
* · cal_block(未标定闭锁):卡 12 两点标定写完 [adc_cal] 的 valid 就翻转,
|
||||
* 校验和同样不动,而标题上的"未标定"徽标要当场消失。
|
||||
* 只盯校验和,这两件事都到不了屏上(真机现象:标定做完了屏上徽标永远挂着,
|
||||
* 直到某次定值改动才被顺带刷掉——那是把两件不相干的事当同步点)。
|
||||
* 代价:每轮多两次字节比较(无变化时零拷贝),换来屏上状态与真值同拍 */
|
||||
{
|
||||
uint8_t locked_now = prot_set_locked();
|
||||
uint8_t cal_now = feed_cal_block();
|
||||
|
||||
if ((s_par_sum_valid == 0u) || (sum != s_par_sum)
|
||||
|| (locked_now != s_par_locked) || (cal_now != s_par_cal_block)) {
|
||||
(void)memset(&s_scr, 0, sizeof(s_scr));
|
||||
feed_fill_par(&s_scr.par, sum);
|
||||
(void)ui_data_publish_par(&s_scr.par);
|
||||
s_par_sum = sum;
|
||||
s_par_locked = locked_now;
|
||||
s_par_cal_block = cal_now;
|
||||
s_par_sum_valid = 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-6
@@ -17,6 +17,8 @@
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-14)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-1:tick 兼任定值写路径的落定节拍
|
||||
* (契约 §6.2;实现见 .c 的 s_param_ops)
|
||||
*/
|
||||
|
||||
#ifndef UI_DATA_FEED_H
|
||||
@@ -27,15 +29,21 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 函数功能:装配节拍入口——实时帧与信息帧满 200 ms 发布一次,参数帧仅在校验和
|
||||
* 变化时发布;首轮立即发布(免启动后出现一闪的 "--")
|
||||
* 函数功能:装配节拍入口——先落定一条定值整定请求(写方向),再发布实时帧与
|
||||
* 信息帧(满 200 ms 一次)、参数帧(仅校验和变化时);首轮立即发布
|
||||
* (免启动后出现一闪的 "--")
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms)
|
||||
* 函数说明:本函数每轮代价 = 一次 tick 差值比较 + 一次校验和比较;真正的取数
|
||||
* 与组装只在到期时发生(docs/UI设计/UI_数据供给设计说明.md §5.2)。
|
||||
* 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms);页面"保存"按键
|
||||
* 与本页节拍同上下文,故写路径的投递/落定之间不需要任何锁
|
||||
* (Drivers/BSP/UI/ui_data.h 写路径节)
|
||||
* 函数说明:本函数每轮代价 = 一次 tick 差值比较 + 一次校验和比较 + 一次
|
||||
* ui_data_param_drain()(队列空时只是一次计数判断);真正的取数与组装
|
||||
* 只在到期时发生(docs/UI设计/UI_数据供给设计说明.md §5.2)。
|
||||
* ⚠ drain 排在取校验和**之前**:本轮写入的定值要在本轮就被"校验和变化
|
||||
* 才发"那道门看见,页面才能在同一帧里同时显示 OK 与新值
|
||||
* 内部零阻塞、零堆分配、零日志(日志每条约 90 B Flash + 216 B 栈,
|
||||
* 200 ms 周期打不起)
|
||||
* 200 ms 周期打不起);整定失败态走屏上通道,不打日志
|
||||
*/
|
||||
void ui_data_feed_tick(void);
|
||||
|
||||
|
||||
+62
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
| 文件 | 作用 |
|
||||
|---|---|
|
||||
| `sys_log.h` | **日志接口(平台无关)**:定长 64B 记录写入 `/log/run.log`·`selftest.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 记录纯编解码**(B9):64B 定长记录(CP56Time2a 时标 + 类型 + 元件号 + 单调序号 + 值 + sum8 撕裂校验),零 I/O 零 OS 依赖,主机单测覆盖 |
|
||||
@@ -23,6 +23,67 @@
|
||||
内核启动后由 `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 页按此做)
|
||||
|
||||
**决策一:记录类型就是 `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:181` | `asserted` 上升沿**且**该元件跳闸投退项 = 1 |
|
||||
| 保护告警 | `SYSLOG_ALARM` / `alarm.log` | 1024 | 同文件 `184` | `asserted` 上升沿**且**跳闸投退项 = 0 或该元件无此项 |
|
||||
| 保护启动 | `SYSLOG_START` / `start.log` | 1024 | 同文件 `176` | `picked` 上升沿(越限电平,早于/等于动作) |
|
||||
| 操作 | `SYSLOG_OPER` / `oper.log` | 512 | `App/protocol/iec104/iec104_pts.c:574/602`、`App/task/default_task.c:242`、`App/log/sys_log_mcu.c:646`(宿主 `sim_syslog.c:245`) | 强制复归受理 / 软压板投退落地 / 定值**真落盘成功** / 清除记录成功 |
|
||||
| 遥控 | `SYSLOG_YK` / `yk.log` | 256 | `App/protocol/iec104/iec104_pts.c:573/576/582/586/596/598/609` | 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)**:`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(控制台单写者)
|
||||
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
* 创建日期:2026-09-14
|
||||
* 修改记录:
|
||||
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4)
|
||||
* v1.2 2026-10-26 王建锋 新增「记录删除」API(UI 装置维护「清除记录」的前置,
|
||||
* 道F task-4;判据 docs/任务规划/UI补齐.md §2 第 3 行
|
||||
* 「不允许只做一个没有实际效果的按钮」):
|
||||
* 共享层 syslog_request_clear() / syslog_clear_state()
|
||||
* (双端都实现,UI 侧只准用这两个)+ MCU 后端专属
|
||||
* syslog_clear_apply()(落地半边,仅 defaultTask)。
|
||||
* 请求与落地分离的理由与执行点写在下面声明处
|
||||
* v1.1 2026-10-24 王建锋 仅契约注释更正,与 App/log/sys_log_mcu.c 的 C/F 两项
|
||||
* 整改同步(无签名、无行为改动):
|
||||
* syslog_get_record 写死容量要求(ts_len > SYSLOG_TS_LEN、
|
||||
@@ -28,10 +35,28 @@ extern "C" {
|
||||
|
||||
/*
|
||||
* 日志 ID:决定落盘文件与容量上限
|
||||
*
|
||||
* ⚠ **记录类型 = 日志 ID 本身**(一类记录 = /log 下一个严格 8.3 短名的文件),
|
||||
* 记录体内**没有**类型字段位(Lead 裁定 2026-10-26,task-10):64B 定长格式被
|
||||
* UI 记录页(文本读者)与主机解码工具同时按固定偏移读,塞类型字节等于当场砸掉
|
||||
* 这两批既有读者;而运行/自检记录已经在这份格式里跑了很久,不能为五类新记录
|
||||
* 改版本位。于是"这条记录是什么类型"由它在哪个文件里回答。
|
||||
* 同理二进制 SOE 通道的 soe_type_t 本轮**不动**:改它必须同步 UI 侧的类型名表
|
||||
* (类型名表在 UI 侧 <Drivers/BSP/UI/ui_w_soe.c>,不在本道写域),而且 SOE
|
||||
* 页与这五页是两套读者。
|
||||
* 口径与五个写入点已登记进 App/log/README.md(task-11「记录 5 页」按此做)。
|
||||
*/
|
||||
typedef enum {
|
||||
SYSLOG_RUN = 0u, /* 运行记录 /log/run.log:上电/重启事件 */
|
||||
SYSLOG_SELFTEST, /* 自检记录 /log/selftest.log:开机自检故障(仅故障才写) */
|
||||
/* ↓ 五类记录(docs/UI设计/页面扩充详细方案.md §3.6 的五行,task-10 落写入点)
|
||||
* ⚠ 只准**尾部追加**:id 值即枚举序 = 两端 s_paths[]/s_caps[] 的下标,
|
||||
* 中间插值会让老文件被新 id 读成别的类型(静默错类别,比崩更难查)。 */
|
||||
SYSLOG_ACCIDENT, /* 事故记录 /log/accid.log:跳闸投退=1 的元件动作 */
|
||||
SYSLOG_ALARM, /* 保护告警 /log/alarm.log:跳闸投退=0 或该项不存在的元件动作 */
|
||||
SYSLOG_START, /* 保护启动 /log/start.log:元件 picked 上升沿 */
|
||||
SYSLOG_OPER, /* 操作记录 /log/oper.log:强制复归 / 清除记录 / 对时 / 定值保存 */
|
||||
SYSLOG_YK, /* 遥控记录 /log/yk.log:104 遥控的选择 / 执行 / 拒绝 */
|
||||
SYSLOG_COUNT /* 哨兵值,非有效 ID */
|
||||
} syslog_id_t;
|
||||
|
||||
@@ -53,9 +78,21 @@ typedef enum {
|
||||
|
||||
/*
|
||||
* 容量上限(条数;满时删除重建,设计文档 §4.3)
|
||||
*
|
||||
* ⚠ **上限是逻辑上限,不是预分配**:文件按追加写自然增长,一条不写就不占簇;
|
||||
* 到顶才 unlink 重建(照 run.log 先例)。七类全满的名义峰值 =
|
||||
* (1024+512+512+1024+1024+512+256) × 64B = **304 KB**,NAND 2 GB、且这些
|
||||
* 记录只在事件边沿才写(判据动作/遥控/人工操作),不构成容量或磨损瓶颈。
|
||||
* 告警/启动给到 1024 是因为一次系统故障会连片触发多个元件,别让最想了解
|
||||
* 故障过程的两类记录最先被截掉。
|
||||
*/
|
||||
#define SYSLOG_RUN_MAX 1024u /* /log/run.log:64KB */
|
||||
#define SYSLOG_SELFTEST_MAX 512u /* /log/selftest.log:32KB */
|
||||
#define SYSLOG_ACCIDENT_MAX 512u /* /log/accid.log:32KB(跳闸稀有,换长历史) */
|
||||
#define SYSLOG_ALARM_MAX 1024u /* /log/alarm.log:64KB */
|
||||
#define SYSLOG_START_MAX 1024u /* /log/start.log:64KB */
|
||||
#define SYSLOG_OPER_MAX 512u /* /log/oper.log:32KB */
|
||||
#define SYSLOG_YK_MAX 256u /* /log/yk.log:16KB(低频,与事故同稀有) */
|
||||
|
||||
/*
|
||||
* 上电/重启分类窗口(秒):run.log 末条 epoch 距 now < 该值 → "reboot",否则 "power on"
|
||||
@@ -83,6 +120,34 @@ typedef enum {
|
||||
*/
|
||||
void syslog_post(syslog_id_t id, const char *desc);
|
||||
|
||||
/*
|
||||
* 函数功能:投递一条日志——**调用链上不带日志行缓冲**的那一个版本
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* desc - 描述串(NUL 结尾;超过 SYSLOG_DESC_MAX 截断),同 syslog_post
|
||||
* 返回值:无
|
||||
* 限定条件:任意上下文;MCU 端不碰 FatFs、不碰卷锁、不阻塞
|
||||
* 函数说明:为什么要有第二个投递口(不是洁癖):每个 DBG_* 调用点在调用者栈上
|
||||
* 内联约 216 B 行缓冲,而 syslog_post 的两个"满态丢弃留痕"分支就躺在
|
||||
* 它自己的帧里——于是"调一次 post"在静态最坏链上等于"带上这两处缓冲"。
|
||||
* protectTask 的静态最坏链是 B9 写进 DoD 的硬线(872 B / 57%,当年在
|
||||
* post 里加一条 DBG_* 实测垫到 1192 B = stack_report FAIL),iec104Task
|
||||
* 同样是深链任务,所以这五类记录的写入点一律走本函数。
|
||||
* 代价是丢弃时不当场留痕:只累加 syslog_post_dropped(),由唯一写者
|
||||
* defaultTask 在 syslog_drain() 里按差值补一条(照 soe_store 的先例)。
|
||||
* 与 syslog_post 的差别**只有留痕路径**,收/丢的判据逐条相同。
|
||||
* 模拟器后端没有队列也没有栈预算问题,本函数等价于 syslog_post。
|
||||
*/
|
||||
void syslog_post_quiet(syslog_id_t id, const char *desc);
|
||||
|
||||
/*
|
||||
* 函数功能:取 syslog_post_quiet() 通道的累计丢弃条数(可观测性)
|
||||
* 入口参数:无
|
||||
* 返回值:MCU 端 = 队列满 / 启动前数组满累计次数;模拟器端恒 0(直写不丢) uint32_t
|
||||
* 函数说明:与 soe_store_dropped() 同款定位。留痕由 drain 按差值上报,本函数是
|
||||
* 给单测与台架读数用的,不参与任何判定路径。
|
||||
*/
|
||||
uint32_t syslog_post_dropped(void);
|
||||
|
||||
/*
|
||||
* 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1))
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
@@ -118,6 +183,58 @@ int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, c
|
||||
*/
|
||||
int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch);
|
||||
|
||||
/*
|
||||
* 记录删除(「清除记录」):请求 → 落地分离
|
||||
*
|
||||
* 为什么必须分两步:defaultTask 是本模块**唯一写者**(存储架构说明 §10.1 第 7 条),
|
||||
* uiTask 直接 f_unlink 会与写者的追加写/「容量满 unlink 重建」交错,而且删除要走
|
||||
* 卷锁 + FAT 回写——uiTask 在 IWDG required_mask 内、每 100 ms 喂一次心跳,
|
||||
* 给它一条最坏数百 ms 的存储路径就是拿误复位当代价。故 UI 侧只登记请求,
|
||||
* 真正的删除在 defaultTask 的 syslog_drain() 尾部执行
|
||||
* (范式同 App/algo/prot_elm.c 的 prot_elm_request_reset():单字节标志 + 消费者落地)。
|
||||
*
|
||||
* 状态码(syslog_clear_state 返回值;写成宏而不是枚举,与本头既有 SYSLOG_* 常量同族)
|
||||
*/
|
||||
#define SYSLOG_CLEAR_NONE 0 /* 从未请求过清除(上电初值) */
|
||||
#define SYSLOG_CLEAR_OK 1 /* 已清空:文件不在盘上,同拍 syslog_count() 必为 0 */
|
||||
#define SYSLOG_CLEAR_WAIT (-1) /* 请求排队中:等 defaultTask 下一轮 drain(含卷占用顺延、卷未就绪挂起) */
|
||||
#define SYSLOG_CLEAR_FAIL (-2) /* 最近一次落地失败:原文件未被删除,可再发一次请求 */
|
||||
|
||||
/*
|
||||
* 函数功能:登记「清除某日志全部记录」的请求(任意上下文;不碰存储、不阻塞)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* 返回值:0 - 请求已登记(同一 id 重复登记合并为一次,幂等)
|
||||
* -1 - id 越界(请求未登记,状态一个字节都没动) int
|
||||
* 限定条件:非唯一写者的上下文(uiTask / netTask / iec104Task)**只能**用本函数发起
|
||||
* 删除,不得直调 MCU 后端的 syslog_clear_apply()
|
||||
* 函数说明:本函数**不等于"已清除"**,只代表"已受理"——MCU 端的删除动作发生在
|
||||
* defaultTask 的下一次 syslog_drain()(50 ms 节拍),结果读
|
||||
* syslog_clear_state(),事实复核读 syslog_count()。
|
||||
* ⚠ 两端时序有别(同 syslog_post 的队列/直写差异,那边写在 post 的头注释里):
|
||||
* MCU 端返回后状态通常是 SYSLOG_CLEAR_WAIT;模拟器端单线程、本调用内
|
||||
* 同步落地,返回后立即是 OK/FAIL,**永远不会读到 WAIT**。UI 侧要按
|
||||
* "WAIT 也正常"写判断,别把 WAIT 当失败。
|
||||
* 刻意**不**在删除成功后补一条 "records cleared" 留痕:判据要求
|
||||
* syslog_count() 归零,写了立刻变 1;要留痕归操作记录/SOE
|
||||
* (docs/任务规划/UI补齐.md 第 6 项的写入方),不是本 API 的职责。
|
||||
*/
|
||||
int syslog_request_clear(syslog_id_t id);
|
||||
|
||||
/*
|
||||
* 函数功能:查「清除记录」的状态(给 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
|
||||
* 限定条件:任意上下文(MCU 端只读两个 volatile 单字节,Cortex-M 上原子,无锁)
|
||||
* 函数说明:排队位优先于结果字节——否则连点两次会先把上一次的结果当成这次的,
|
||||
* 还在排队的请求被显示成"已清空"。
|
||||
* 无锁交接的最坏竞态只有两种,都不产生错误数据:① 写者已销账、尚未写
|
||||
* 结果的那几个指令周期里读 → 读到上一次的结果(同向,多为 OK);
|
||||
* ② 上一次失败被新请求清零 → 那次失败不再显示(用户已在重新发起,
|
||||
* 语义正确)。为它上临界区要引 FreeRTOS 头 + 宿主桩,不值。
|
||||
*/
|
||||
int syslog_clear_state(syslog_id_t id);
|
||||
|
||||
/*
|
||||
* MCU 后端专属 API(sys_log_mcu.c;模拟器端无对应实现,勿在共享代码调用)
|
||||
*/
|
||||
@@ -143,9 +260,34 @@ void syslog_mcu_mounted(void);
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:仅 defaultTask(唯一写者)调用;卷未就绪时直接返回(条目滞留待重试)
|
||||
* 本函数尾部同时消费「清除记录」请求(syslog_request_clear 的落地半边,
|
||||
* 排在条目排空之后——删除必须是本轮对这两个文件的最后一次盘上动作,
|
||||
* 先删后写会把刚排空的旧记录又落回去,"清除"就变成没效果的按钮)
|
||||
*/
|
||||
void syslog_drain(void);
|
||||
|
||||
/*
|
||||
* 函数功能:立即删除某日志的全部记录(「清除记录」的落地半边,MCU 后端专属)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* 返回值:0 - 记录已清空(含"文件本就不存在"的幂等成功,见下)
|
||||
* -1 - 未清空:卷被占用或删除失败,**原文件一个字节都没少** int
|
||||
* 限定条件:仅 defaultTask(syslog 唯一写者)上下文,且调用处**不得持卷锁**
|
||||
* (卷锁非递归锁);卷锁一律 try(vol_lock_wait(0),本任务 = 看门狗
|
||||
* 喂狗点,阻塞等锁 = 停喂 = IWDG 误复位)
|
||||
* 函数说明:其它上下文请调 syslog_request_clear()(本头文件上方);本函数在
|
||||
* MCU 后端之外**没有任何实现**,共享代码误调会在链接期报错,
|
||||
* 这比运行时断言硬。产品路径上唯一的调用者是 syslog_drain 尾部的
|
||||
* 请求消费循环,直接调用只用于板级套件与主机单测。
|
||||
* 幂等口径:只有 FR_NO_FILE 算"已经没有什么可删了"→ 返 0;
|
||||
* FR_NO_PATH(/log 目录不在)**不算**幂等——记录确实读不到,但成因是
|
||||
* 目录没了而不是"被清空",补目录归 syslog_mcu_mounted() 每 2s 的重试,
|
||||
* 这里如实返 -1,不把卷异常洗成"清除成功"。
|
||||
* 名字/参数类错误码(FR_INVALID_NAME 等)一律只返 -1 不做任何补救动作:
|
||||
* 本函数压根没有卷级动作(无 f_mkdir / 无 f_mkfs / 不重试 unlink),
|
||||
* 这正是存储架构说明 §10.1 第 10 条在删除路径上的落地形态。
|
||||
*/
|
||||
int syslog_clear_apply(syslog_id_t id);
|
||||
|
||||
/*
|
||||
* 函数功能:把一条取证行追加到 /log/wdev.log(掉电不丢;变长行,不走 64B 定长格式)
|
||||
* 入口参数:p_desc - 取证描述行(调用方已格式化;不可为 NULL) const char *
|
||||
|
||||
+372
-40
@@ -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
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "run_par.h" /* 卡 10:死区真源 + 运行参数段 104 读写路由 */
|
||||
#include "prot_elm.h" /* 板端源:遥信 = 元件状态 */
|
||||
#include "prot_set.h" /* 板端源:定值 + 闭锁 */
|
||||
#include "sys_log.h" /* task-10:遥控记录(SYSLOG_YK)/ 操作记录(SYSLOG_OPER) */
|
||||
#include "adc_input.h" /* adc_semantic_t(phasor 实例按语义绑定) */
|
||||
|
||||
/* ---------------- YC 表(§1.5 点号 4011+;死区列 = ROM 默认值) ------------ */
|
||||
@@ -544,25 +545,68 @@ static uint64_t s_src_yx_valid(void) {
|
||||
* YK12 走 prot_set_set_sealed(原子写+封存,B7);protectTask 下一拍
|
||||
* 读到压板态即复位元件并停判据(protect_task.c)。
|
||||
*/
|
||||
/* 遥控记录的文案表(task-10)。**全部 ROM 常量、零拼接**:本回调跑在
|
||||
* iec104Task,而 iec104_serv.c 的日志缓冲已经因为"深链模块"被收到 128 B,
|
||||
* 这里再 snprintf 一条"YK ioa=0x600B dpc=2 sel=0 rc=0"就是往那条链上加缓冲;
|
||||
* 记录类型由 SYSLOG_YK 这个文件本身回答,屏上话术归 UI(App/log/README.md 口径)。
|
||||
* 最长一条 20 字符 < SYSLOG_DESC_MAX(28)。 */
|
||||
#define YK_REC_YK11_SEL "YK11 select"
|
||||
#define YK_REC_YK11_EXEC "YK11 exec"
|
||||
#define YK_REC_YK12_SEL "YK12 select"
|
||||
#define YK_REC_YK12_ON "YK12 exec on"
|
||||
#define YK_REC_YK12_OFF "YK12 exec off"
|
||||
#define YK_REC_YK12_FAIL "YK12 exec fail"
|
||||
#define YK_REC_DPC_BAD "YK12 bad dpc"
|
||||
#define YK_REC_UNKNOWN "YK unknown ioa"
|
||||
|
||||
static int s_src_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select) {
|
||||
switch (ioa) {
|
||||
case 0x600Au: /* YK11 信号复归 → 元件状态复位(异步一拍) */
|
||||
(void)dpc; /* 复归与双点值无关 */
|
||||
if (select == 0u) {
|
||||
prot_elm_request_reset();
|
||||
/* 遥控记录(执行段已受理)+ 操作记录(本地/远端强制复归)。
|
||||
* 两条不是重复留痕:前者回答"主站下了什么命令、结果如何",后者
|
||||
* 回答"装置被谁改动了状态"——UI 的遥控记录页与操作记录页各读一份。
|
||||
* YK11 没有可失败的下游(request_reset 只置单字节标志),所以它
|
||||
* 只有受理态,没有 fail 分支。 */
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_YK11_EXEC);
|
||||
syslog_post_quiet(SYSLOG_OPER, "force reset");
|
||||
} else {
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_YK11_SEL);
|
||||
}
|
||||
return 0;
|
||||
case 0x600Bu: /* YK12 全局软压板(PS_GLOBAL_EN)两段式 */
|
||||
case 0x600Bu: { /* YK12 全局软压板(PS_GLOBAL_EN)两段式 */
|
||||
int rc;
|
||||
if ((dpc != (uint8_t)IEC104_DPC_ON) && (dpc != (uint8_t)IEC104_DPC_OFF)) {
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_DPC_BAD);
|
||||
return -1; /* 非法双点命令:选择段即拒 */
|
||||
}
|
||||
if (select != 0u) {
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_YK12_SEL);
|
||||
return 0; /* 选择段 = 只校验 */
|
||||
}
|
||||
return prot_set_set_sealed(PS_GLOBAL_EN,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON)
|
||||
? PROT_SET_Q10(1) : PROT_SET_Q10(0));
|
||||
/* 执行段:set_sealed 自带范围校验与封存,失败 = 值被拒、RAM 未改。
|
||||
* 这里**必须接住返回值再落记录**(bsp-err:错误码不得裸丢弃;
|
||||
* 原实现是 return 直出,现在多一条记录后仍然原值返回,语义不变)。 */
|
||||
rc = prot_set_set_sealed(PS_GLOBAL_EN,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON)
|
||||
? PROT_SET_Q10(1) : PROT_SET_Q10(0));
|
||||
if (rc != 0) {
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_YK12_FAIL);
|
||||
} else {
|
||||
syslog_post_quiet(SYSLOG_YK,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON) ? YK_REC_YK12_ON
|
||||
: YK_REC_YK12_OFF);
|
||||
/* 压板投退是一次真实的运维动作(保护退出/恢复服务)→ 同时进操作记录 */
|
||||
syslog_post_quiet(SYSLOG_OPER,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON) ? "plate in"
|
||||
: "plate out");
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
default:
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_UNKNOWN);
|
||||
return -1; /* 其余 YK:无出口硬件/无对应能力 → 拒绝 */
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -40,6 +40,7 @@
|
||||
#include "cfg_file.h"
|
||||
#include "run_par.h" /* 运行参数段落盘(卡 10,配置引擎属主 = 本任务) */
|
||||
#include "prot_set_store.h"
|
||||
#include "prot_set.h" /* task-10:prot_set_dirty_peek(区分"无待写"与"真落盘") */
|
||||
#include "sys_log.h"
|
||||
#include "sys_events.h"
|
||||
/* 深调用链模块(本任务 = 存储 bring-up + 配置引擎属主 + syslog 唯一写者,
|
||||
@@ -230,8 +231,16 @@ void default_task_func(void *argument) {
|
||||
task_wd_seg(TASK_WD_SEG_CFGSAVE);
|
||||
net_cfg_save_run();
|
||||
/* 保护定值落盘(B7):内部消费脏标志,无改动 = 空操作;
|
||||
* 写失败脏回置 → iec104Task 下轮 poll 再投递事件 = 天然重试 */
|
||||
(void)prot_set_store_save();
|
||||
* 写失败脏回置 → iec104Task 下轮 poll 再投递事件 = 天然重试。
|
||||
* task-10「操作记录」写入点之一:⚠ 不能按 prot_set_store_save() 的返回值
|
||||
* 记——它对"无待写"同样返 0,照返回值记就是每 50 ms 刷一条**没发生过**的
|
||||
* 操作(正是"不许用静态演示数据充数"的反面)。先 peek 脏位(非消费,
|
||||
* iec104_serv.c 通知属主用的就是同一枚),只有"确实有待写且这趟写成功"
|
||||
* 才算一次整定操作。失败不记(判据 = 动作成功后写一条),脏位保留、
|
||||
* 下轮重试成功后才留痕。 */
|
||||
if ((prot_set_dirty_peek() != 0u) && (prot_set_store_save() == 0)) {
|
||||
syslog_post(SYSLOG_OPER, "settings saved");
|
||||
}
|
||||
/* 运行参数段落盘(卡 10):同款脏语义;save = 引擎整档外科写
|
||||
* ([run_par] 9 键 + 被 adc_cal_set 联动改写的 [adc_cal] 两段一趟落) */
|
||||
(void)run_par_store_save();
|
||||
|
||||
+19
-3
@@ -18,6 +18,7 @@
|
||||
#include "prot_set.h" /* 定值脏标志 peek(B7 落盘通知) */
|
||||
#include "run_par.h" /* 运行参数脏 peek(卡 10 落盘通知,同款语义) */
|
||||
#include "sys_clock.h"
|
||||
#include "sys_log.h" /* task-19:对时成功落一条操作记录(SYSLOG_OPER) */
|
||||
#include "sys_events.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "dbg_log.h"
|
||||
@@ -85,19 +86,34 @@ static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:link 层对时回调——epoch 毫秒落系统墙钟(含 SD2506 RTC 写回)
|
||||
* 函数功能:link 层对时回调——epoch 毫秒落系统墙钟(含 SD2506 RTC 写回),
|
||||
* 成功时顺带落一条操作记录(道F task-19:「时钟设置」是四处操作记录
|
||||
* 写入点之一,另外三处见 App/log/README.md 的五类记录对照表)
|
||||
* 入口参数:ctx 未用;epoch_ms = C_CS 解析出的 UTC 时刻
|
||||
* 返回值:0 成功 / -1 失败(link 层回 actcon+PN)
|
||||
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)
|
||||
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)。
|
||||
* **只在真正改成功之后才投记录**:越界拒绝与 sys_clock_set_unix 失败两条腿
|
||||
* 一条都不投——操作记录是审计面,写进去一条没发生过的"对时"比少一条更坏
|
||||
* (TC-I104SRV-050b / 051 分别钉这两条腿)。
|
||||
* 投递用 syslog_post_quiet():iec104Task 是深链任务(本文件顶部已把
|
||||
* DBG_LINE_BUF_MAX 收到 128 就是同一件事的证据),不许在它的调用链上
|
||||
* 内联日志行缓冲;两个投递口的收/丢判据一致,差别只在留痕位置
|
||||
* (口径与理由见 App/log/README.md「两个投递口」)。
|
||||
* 文案是 ROM 常量、17 字节 < SYSLOG_DESC_MAX(28):iec104Task 里不 snprintf。
|
||||
*/
|
||||
static int s_clock_sync(void *ctx, uint64_t epoch_ms) {
|
||||
uint64_t secs = epoch_ms / 1000ULL;
|
||||
int rc;
|
||||
|
||||
(void)ctx;
|
||||
if (secs > 0xFFFFFFFFULL) {
|
||||
return -1;
|
||||
}
|
||||
return sys_clock_set_unix((uint32_t)secs);
|
||||
rc = sys_clock_set_unix((uint32_t)secs);
|
||||
if (rc == 0) {
|
||||
syslog_post_quiet(SYSLOG_OPER, "clock set by 104");
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
#include "prot_elm.h"
|
||||
#include "prot_set.h"
|
||||
#include "soe_store.h" /* B9:动作/复归边沿 → SOE 投递(零 FS,队列异步) */
|
||||
#include "sys_log.h" /* task-10:事故 / 保护告警 / 保护启动 三类文本记录 */
|
||||
#include "task_watchdog.h"
|
||||
#include "tpafe5160.h" /* tpafe5160_frame_seq():帧序号(BUSY EXTI 写) */
|
||||
|
||||
@@ -74,6 +75,7 @@ static adc_health_age_t s_age_tp; /* TPAFE 车道 */
|
||||
static adc_health_age_t s_age_a1; /* STM32 车道 */
|
||||
|
||||
static uint32_t s_prev_active; /* 上拍动作位图(边沿日志用) */
|
||||
static uint32_t s_prev_pick; /* 上拍启动位图(「保护启动」记录边沿用,task-10) */
|
||||
static uint8_t s_prev_hf_lock; /* 上拍采样闭锁态(边沿日志用) */
|
||||
static uint8_t s_prev_plate_out; /* 上拍全局软压板退出态(边沿日志用,B7) */
|
||||
static uint8_t s_prev_cal_lock; /* 上拍未标定闭锁态(边沿日志用,卡 5) */
|
||||
@@ -138,6 +140,61 @@ static void s_log_active_edge(uint32_t active) {
|
||||
s_prev_active = active;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把元件状态边沿落成三类文本记录(事故 / 保护告警 / 保护启动)
|
||||
* 入口参数:active - 本拍 prot_elm_active_mask()(asserted 位图) uint32_t
|
||||
* pick - 本拍 prot_elm_pick_mask()(picked 位图) uint32_t
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅本任务上下文;**只投递、不碰盘**(唯一写者是 defaultTask),
|
||||
* 投递走 syslog_post_quiet()——本任务静态最坏链是 B9 写进 DoD 的硬线
|
||||
* (872 B / 57%),syslog_post 的两个满态留痕分支各内联约 216 B 行缓冲,
|
||||
* 在这里调它等于把链垫回 stack_report 的 FAIL 区(同本文件不用
|
||||
* snprintf/DBG 拼记录的纪律,见 s_log_active_edge 函数头)
|
||||
* 函数说明:三类记录的**类型就是目标文件**(一类一个 /log 文件,记录体内没有
|
||||
* 类型字段位;决策与理由见 App/log/README.md)。判据:
|
||||
* 1) picked 上升沿 → SYSLOG_START("保护启动"):越限电平一到就记,
|
||||
* 不等定时限到点——只记动作会让"启动了但没到点"那段在记录里消失;
|
||||
* 2) asserted上升沿 → 归 事故 还是 告警 由 prot_elm_record_kind() 判,
|
||||
* 其判据唯一来源是定值表里的 PS_*_TRIP(0 告警 / 1 跳闸);本函数
|
||||
* 不另立"哪些元件算跳闸"的名单,避免与 g_prot_set_meta / k_pe[] 打架。
|
||||
* 返回沿(asserted/picked 掉落)**不记**:复归留痕已由 s_log_active_edge
|
||||
* 的 SOE 通道承担,再写一份就是同一事件两个真源。
|
||||
* 同拍既启动又动作(延时段 0ms / 速断)→ 两条都记,顺序先启动后动作,
|
||||
* 与物理事件次序一致。
|
||||
* desc 直接用 prot_elm_name() 的 ROM 常量("OC1-IA" 等,最长 6 字节
|
||||
* << SYSLOG_DESC_MAX=28),零格式化、零栈缓冲。
|
||||
*/
|
||||
static void s_log_record_edge(uint32_t active, uint32_t pick) {
|
||||
uint32_t p_chg = pick ^ s_prev_pick;
|
||||
uint32_t a_chg = active ^ s_prev_active;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; (i < (uint32_t)PE_COUNT) && (((p_chg | a_chg) != 0u)); i++) {
|
||||
const uint32_t bit = 1u << i;
|
||||
|
||||
if (((p_chg & bit) != 0u) && ((pick & bit) != 0u)) {
|
||||
syslog_post_quiet(SYSLOG_START, prot_elm_name((prot_elm_id_t)i));
|
||||
}
|
||||
if (((a_chg & bit) != 0u) && ((active & bit) != 0u)) {
|
||||
switch (prot_elm_record_kind((prot_elm_id_t)i)) {
|
||||
case PROT_REC_TRIP:
|
||||
syslog_post_quiet(SYSLOG_ACCIDENT, prot_elm_name((prot_elm_id_t)i));
|
||||
break;
|
||||
case PROT_REC_ALARM:
|
||||
syslog_post_quiet(SYSLOG_ALARM, prot_elm_name((prot_elm_id_t)i));
|
||||
break;
|
||||
default:
|
||||
/* PROT_REC_NONE:元件号越界。位图来自 prot_elm 自己的表,正常
|
||||
* 走不到这里;真走到了就是表与位图不一致,一条都不能落——
|
||||
* 落错类别的审计记录比少一条记录更坏。 */
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
s_prev_pick = pick;
|
||||
s_prev_active = active;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:protectTask 线程入口
|
||||
* 入口参数:argument - 未用
|
||||
@@ -242,6 +299,8 @@ void protect_task(void *argument) {
|
||||
}
|
||||
|
||||
s_log_active_edge(prot_elm_active_mask());
|
||||
/* 三类文本记录(task-10):放在 SOE 边沿之后、喂狗之前,纯 RAM 投递 */
|
||||
s_log_record_edge(prot_elm_active_mask(), prot_elm_pick_mask());
|
||||
|
||||
task_wd_beat(TASK_WD_PROTECT);
|
||||
osDelay(PROTECT_TASK_CYCLE_MS);
|
||||
|
||||
+228
-2
@@ -10,11 +10,23 @@
|
||||
* 发布者,保护收在本模块内部(双槽 + 每槽 seq + 单字节活动下标,照
|
||||
* App/algo/phasor.c 双区先例),页面代码不受影响
|
||||
* (判据 docs/UI设计/UI_数据供给设计说明.md §5.1)
|
||||
* 写路径(定值整定请求队列)同理:生产者是页面"保存"按键钩子、消费者是
|
||||
* App 侧节拍里的 ui_data_param_drain(),两者同在 uiTask → 环形队列照旧
|
||||
* 无锁;若将来出现第二个投递上下文,唯一要做的是给 request/drain 两侧
|
||||
* 各加一把互斥(队列本身是单字 head/cnt 推进,页面侧不会看到半成品)
|
||||
* 模块功能补充:除三通道快照外,本模块还持有定值写路径的请求队列与状态字
|
||||
* (ui_data_param_request/state/drain)——本层只做"排队 + 状态机",
|
||||
* 不判断值域、不碰 prot_set、不碰文件系统(值域与写入事实经
|
||||
* ui_data_param_ops_t 由 App 侧注入,理由见 ui_data.h 写路径节)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-14)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-1:新增定值写路径环形队列(深度 4、满返 -1)
|
||||
* 与状态机(IDLE/WAIT/OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY),
|
||||
* 执行经注入函数表回调 App 侧;参数帧新增 cal_block
|
||||
* 字段随 publish 原样入存(本层不解释含义)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -39,15 +51,42 @@ static uint32_t s_rt_cnt;
|
||||
static uint32_t s_par_cnt;
|
||||
static uint32_t s_info_cnt;
|
||||
|
||||
/* ── 写路径:定值整定请求队列(深度 UI_DATA_PARAM_QD 的静态环形数组)──────── */
|
||||
|
||||
/*
|
||||
* mask 合法位集合 = 低 UI_DATA_SET_N 位(40 项 → 0x000000FFFFFFFFFF)。
|
||||
* 用"全 1 右移"而不是"1 左移 40 位再减 1":前者在 UI_DATA_SET_N == 64 时仍是
|
||||
* 良定义(移 0 位),后者会退化成立即未定义行为(移位数 == 位宽)。
|
||||
* 项数一旦超过 64,下面的编译期守卫当场拦下——位图放不下就是表示法该换,
|
||||
* 绝不能静默把高 24 项变成"投不进去也不报错"
|
||||
*/
|
||||
#define UI_DATA_PARAM_MASK_VALID ((uint64_t)(~(uint64_t)0u) >> (64u - UI_DATA_SET_N))
|
||||
|
||||
typedef char ui_data_chk_mask_t[((uint32_t)UI_DATA_SET_N <= 64u) ? 1 : -1];
|
||||
|
||||
/* 环形队列:head = 出队下标,cnt = 在队条数(0..UI_DATA_PARAM_QD)
|
||||
* 为什么用 head+cnt 而不是 head+tail:cnt 直接就是"还有几条没落定",
|
||||
* state() 判 WAIT 不必再做 (tail - head) 取模减法,少一处可算错的地方 */
|
||||
static ui_data_param_req_t s_pq[UI_DATA_PARAM_QD];
|
||||
static uint8_t s_pq_head;
|
||||
static uint8_t s_pq_cnt;
|
||||
|
||||
/* 写路径状态字与越界位图(页面只读;int/uint64 单字读写在 M4 上不撕裂) */
|
||||
static int s_pq_state = UI_DATA_PARAM_ST_IDLE;
|
||||
static uint64_t s_pq_err_mask;
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:快照层初始化——三通道清零(valid=0、seq=0),页面即按"无首帧"渲染
|
||||
* 函数功能:快照层初始化——三通道清零(valid=0、seq=0)+ 写路径队列复位,
|
||||
* 页面即按"无首帧/未投递"渲染
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:内核启动前可调(纯内存操作,零 OS 依赖);可重复调用(幂等)
|
||||
* 函数说明:静态存储上电本已清零,本函数的意义是运行期复位(例如"恢复出厂后
|
||||
* 重开 UI"这类后续路径),以及让宿主侧测试能随时造出"首帧之前"的状态
|
||||
* 重开 UI"这类后续路径),以及让宿主侧测试能随时造出"首帧之前"的状态。
|
||||
* 写路径一并丢弃在队请求是**有意**的:旧请求带的是旧配置下的编辑叠层,
|
||||
* 复位后还把它落进 prot_set,等于拿陈旧草稿覆盖新装载的定值
|
||||
*/
|
||||
void ui_data_init(void) {
|
||||
(void)memset(&s_rt, 0, sizeof(s_rt));
|
||||
@@ -56,6 +95,11 @@ void ui_data_init(void) {
|
||||
s_rt_cnt = 0u;
|
||||
s_par_cnt = 0u;
|
||||
s_info_cnt = 0u;
|
||||
(void)memset(s_pq, 0, sizeof(s_pq));
|
||||
s_pq_head = 0u;
|
||||
s_pq_cnt = 0u;
|
||||
s_pq_state = UI_DATA_PARAM_ST_IDLE;
|
||||
s_pq_err_mask = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -194,3 +238,185 @@ uint32_t ui_data_par_seq(void) {
|
||||
uint32_t ui_data_info_seq(void) {
|
||||
return s_info.seq;
|
||||
}
|
||||
/*
|
||||
* 函数功能:投递一条定值写入请求(页面"保存"行的唯一出口——只投递,不执行)
|
||||
* 入口参数:a_req - 请求(本层整条拷入环形队列,调用方可复用其栈/叠层空间)
|
||||
* const ui_data_param_req_t *
|
||||
* 返回值:0 - 已入队(状态转 WAIT);或 mask 为 0 的空请求(无改动可保存,
|
||||
* 状态直接转 OK,不占槽位)
|
||||
* -1 - 队列满(状态转 ERR_BUSY,在队请求一条不动);
|
||||
* -2 - 参数无效:NULL 或 mask 含 UI_DATA_SET_N 以上的位
|
||||
* (状态与队列都不动——把调用方的编程错误落成屏上"错误"会误导现场) int
|
||||
* 限定条件:uiTask 上下文(页面按键钩子);与 drain 同上下文串行 → 无锁
|
||||
* 函数说明:本层不校验值域(越界与否属执行侧事实,落在 drain 的整批预检),
|
||||
* 不 include App 头、不碰 prot_set、不碰 FatFs(UI 直写定值会把校验和
|
||||
* 打成中间态、直写盘会撞 defaultTask 单写者纪律——契约 §6.1)
|
||||
* 空请求按成功处理是刻意的:保存行 ENTER 时"一个都没改"是正常操作,
|
||||
* 报 ERR 会让用户以为设备有问题
|
||||
*/
|
||||
int ui_data_param_request(const ui_data_param_req_t *a_req) {
|
||||
uint8_t slot;
|
||||
|
||||
if (a_req == NULL) {
|
||||
return -2;
|
||||
}
|
||||
if ((a_req->mask & ~UI_DATA_PARAM_MASK_VALID) != 0u) {
|
||||
return -2;
|
||||
}
|
||||
if (a_req->mask == 0u) {
|
||||
s_pq_state = UI_DATA_PARAM_ST_OK;
|
||||
s_pq_err_mask = 0u;
|
||||
return 0;
|
||||
}
|
||||
if (s_pq_cnt >= (uint8_t)UI_DATA_PARAM_QD) {
|
||||
s_pq_state = UI_DATA_PARAM_ST_ERR_BUSY;
|
||||
return -1;
|
||||
}
|
||||
|
||||
slot = (uint8_t)(((uint32_t)s_pq_head + (uint32_t)s_pq_cnt) % UI_DATA_PARAM_QD);
|
||||
(void)memcpy(&s_pq[slot], a_req, sizeof(s_pq[slot]));
|
||||
s_pq_cnt += 1u;
|
||||
s_pq_state = UI_DATA_PARAM_ST_WAIT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取写路径状态(页面保存行显示)
|
||||
* 入口参数:无
|
||||
* 返回值:UI_DATA_PARAM_ST_IDLE / _WAIT / _OK / _ERR_RANGE / _ERR_LOCKED /
|
||||
* _ERR_BUSY 之一 int
|
||||
* 限定条件:无(只读单字,任意上下文安全)
|
||||
* 函数说明:本函数就是 s_pq_state 的直读,不做"pending 优先"之类的二次加工——
|
||||
* 三条写路径(投递成功→WAIT、投递被拒→ERR_BUSY、drain 落定→终态或
|
||||
* 残留则 WAIT)已把顺序语义收在写侧,读侧再判一次等于两处解释同一件事
|
||||
*/
|
||||
int ui_data_param_state(void) {
|
||||
return s_pq_state;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取最近一条请求的越界项位图(页面高亮越界行)
|
||||
* 入口参数:无
|
||||
* 返回值:bit i = 1 → 第 i 项越界;最近终态不是 ERR_RANGE 时恒 0 uint64_t
|
||||
* 限定条件:无(只读单字)
|
||||
* 函数说明:与 ui_data_par_t.bad_mask 两条来源不同(本函数 = 本次提交被预检挡下
|
||||
* 的项;bad_mask = prot_set 整表装载/解析留下的越界位图),页面在
|
||||
* ERR_RANGE 那一帧用本函数,混用会把上一次装载的越界显示成本次的
|
||||
*/
|
||||
uint64_t ui_data_param_err_mask(void) {
|
||||
return s_pq_err_mask;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:整批预检一条请求(位图内置位的项逐个问执行侧 check())
|
||||
* 入口参数:a_ops - 执行侧函数表(调用方已核非 NULL)
|
||||
* a_req - 待检请求
|
||||
* 返回值:越界项位图(bit i = 第 i 项越界;0 = 本批全部在界内) uint64_t
|
||||
* 限定条件:只调 check(),不写任何东西("预检"与"落值"分成两个函数,
|
||||
* 正是为了让 all-or-nothing 有地方落脚:整批扫完才决定写不写)
|
||||
* 函数说明:一次扫全 40 位而不是"遇到第一个越界就返回"——err_mask 要给页面
|
||||
* **全部**越界行反显,只报第一行等于让用户改完一行再撞下一行
|
||||
*/
|
||||
static uint64_t ui_data_param_scan(const ui_data_param_ops_t *a_ops,
|
||||
const ui_data_param_req_t *a_req) {
|
||||
uint64_t bad = 0u;
|
||||
uint64_t bit = 1u;
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
|
||||
if ((a_req->mask & bit) != 0u) {
|
||||
if (a_ops->check(i, a_req->q10[i]) != 0) {
|
||||
bad |= bit;
|
||||
}
|
||||
}
|
||||
bit <<= 1u;
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:逐项落值一条请求(调用前 ui_data_param_scan() 已确认整批在界内)
|
||||
* 入口参数:a_ops - 执行侧函数表
|
||||
* a_req - 待写请求
|
||||
* 返回值:0 - 位图内全部写入;-1 - 某项被执行侧拒绝(*p_bad 输出该项) int
|
||||
* 限定条件:apply() 的副作用由执行侧定义(App 实现 = prot_set_set_sealed,
|
||||
* 每项自带置脏);本函数不回滚已写项——prot_set 没有回滚接口
|
||||
* 函数说明:走到这里还失败 = 执行侧与预检不一致(两个真源漂移),理论不可达;
|
||||
* 如实报失败项比谎称"整批未写"更诚实,故**可能已有前几项生效**
|
||||
*/
|
||||
static int ui_data_param_write(const ui_data_param_ops_t *a_ops,
|
||||
const ui_data_param_req_t *a_req,
|
||||
uint64_t *p_bad) {
|
||||
uint64_t bit = 1u;
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
|
||||
if ((a_req->mask & bit) != 0u) {
|
||||
if (a_ops->apply(i, a_req->q10[i]) != 0) {
|
||||
*p_bad = bit;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
bit <<= 1u;
|
||||
}
|
||||
*p_bad = 0u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:处理一条定值写入请求(App 装配层节拍入口:出队 → 闭锁 → 预检 → 落定)
|
||||
* 入口参数:a_ops - 执行侧函数表(locked/check/apply 三者都不得为 NULL)
|
||||
* const ui_data_param_ops_t *
|
||||
* 返回值:1 - 处理并提交了一条(状态已转终态,或队列仍有残留则转 WAIT);
|
||||
* 0 - 队列空(状态与队列都不动);
|
||||
* -1 - 参数无效(a_ops 或其成员为 NULL,状态与队列都不动) int
|
||||
* 限定条件:uiTask 上下文(App/app/ui_data_feed.c 的 tick 每轮一次);每轮最多
|
||||
* 处理一条——整定是低频人工事件,一轮一条已够,且把最坏耗时钉在
|
||||
* 2 × 40 次回调(预检一遍 + 写入一遍),不随队列堆积抖动
|
||||
* 函数说明:四步顺序即判据(契约 §6.2),判定本体拆成 scan/write 两个静态函数
|
||||
* (见其函数说明)——
|
||||
* ① 取队首;② locked() → ERR_LOCKED 并**丢弃本条**(闭锁是业务态,不做
|
||||
* "排队等解锁",否则屏上 WAIT 无限期挂着,用户以为设备卡死);
|
||||
* ③ scan() 任一项越界 → ERR_RANGE 且**一字不写**(all-or-nothing,与
|
||||
* prot_set_load_raw 同纪律:半张表落进值区就是"看着保存成功、其实混着
|
||||
* 新旧两套定值");④ write() 全成 → OK。
|
||||
* ⚠ 顺序不可调:先判闭锁再判越界——闭锁态下 check() 的语义仍成立,但
|
||||
* 页面要的是"定值闭锁"这个更高优先级的原因(契约 §6.3 表序)
|
||||
*/
|
||||
int ui_data_param_drain(const ui_data_param_ops_t *a_ops) {
|
||||
const ui_data_param_req_t *p_req;
|
||||
uint64_t bad;
|
||||
int settled;
|
||||
|
||||
if ((a_ops == NULL) || (a_ops->locked == NULL) ||
|
||||
(a_ops->check == NULL) || (a_ops->apply == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
if (s_pq_cnt == 0u) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
p_req = &s_pq[s_pq_head];
|
||||
bad = 0u;
|
||||
|
||||
if (a_ops->locked() != 0u) {
|
||||
settled = UI_DATA_PARAM_ST_ERR_LOCKED;
|
||||
} else {
|
||||
settled = UI_DATA_PARAM_ST_OK;
|
||||
bad = ui_data_param_scan(a_ops, p_req);
|
||||
if (bad != 0u) {
|
||||
settled = UI_DATA_PARAM_ST_ERR_RANGE; /* 预检挡下:一字未写 */
|
||||
} else {
|
||||
if (ui_data_param_write(a_ops, p_req, &bad) != 0) {
|
||||
settled = UI_DATA_PARAM_ST_ERR_RANGE; /* 执行侧拒写:见函数说明 */
|
||||
}
|
||||
/* 两关都过 → settled 保持 OK */
|
||||
}
|
||||
}
|
||||
|
||||
s_pq_err_mask = bad;
|
||||
s_pq_head = (uint8_t)(((uint32_t)s_pq_head + 1u) % UI_DATA_PARAM_QD);
|
||||
s_pq_cnt -= 1u;
|
||||
s_pq_state = (s_pq_cnt != 0u) ? UI_DATA_PARAM_ST_WAIT : settled;
|
||||
return 1;
|
||||
}
|
||||
|
||||
+151
-1
@@ -25,6 +25,13 @@
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-14:契约 §4 快照层落地;相对契约
|
||||
* 两通道新增第三条 info,划分理由见本头"通道划分"注
|
||||
* 与契约 §4.4)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-1(docs/任务规划/UI补齐.md §2 第 1 行):
|
||||
* 新增定值写路径(param_request/param_state/
|
||||
* param_drain,契约 §6.2 深度 4 环形队列)+ 参数帧尾
|
||||
* 追加 cal_block(UI_DATA_VER 1→2,整定页"未标定"徽标
|
||||
* 的数据源);写路径仍不引入任何 App 依赖(执行事实
|
||||
* 经 ui_data_param_ops_t 由 App 侧注入,同契约 §7.2
|
||||
* 记录页注入式 reader 的路子)
|
||||
*/
|
||||
|
||||
#ifndef UI_DATA_H
|
||||
@@ -41,7 +48,9 @@ extern "C" {
|
||||
* (禁止删除或重排字段——页面按名字取,重排是静默错位;
|
||||
* 口径 docs/UI设计/UI_数据供给设计说明.md §4.3)
|
||||
*/
|
||||
#define UI_DATA_VER 1u
|
||||
/* v2(2026-10-26,道F task-1):ui_data_par_t 尾追加 cal_block——版本号是页面与
|
||||
* 写侧唯一的对齐凭据,追加字段不升版 = 老页面按新帧长 memcpy 时静默错位 */
|
||||
#define UI_DATA_VER 2u
|
||||
|
||||
/* ── 通道数量 ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -157,6 +166,16 @@ typedef struct {
|
||||
uint8_t rsv; /* 预留 */
|
||||
uint64_t bad_mask; /* 40 位:置 1 = 该定值项越界(页面反显该行) */
|
||||
int32_t q10[UI_DATA_SET_N]; /* Q10 原值,下标 = prot_set_id_t 枚举序 */
|
||||
/* v2 尾追加:未标定闭锁(整定页"未标定"徽标的数据源)。
|
||||
* 1 = UA/UN/IA 三路里至少一路的标定 VALID 未置位 → 保护判据当前停走,页面必须
|
||||
* 让用户看见"改了定值也不会动作";0 = 三路全 VALID(判据在跑)。
|
||||
* ⚠ 口径**不是** rt.eng:eng 是整帧量纲档(要求电气 8 路全 VALID),V0.1 双
|
||||
* 转换器下 UB/UC/IB/IC/IN 永不置位 → eng 恒为引脚档,照 eng 画徽标就成了
|
||||
* "标定做完屏上仍永不解锁"。三路口径的权威实现 = App/task/protect_task.c
|
||||
* 的 s_cal_locked()(写侧 App/app/ui_data_feed.c 按同一条判据现算,本层只存
|
||||
* 真值、不判断)。与上面 locked(定值校验和自检闭锁 = 禁编辑)是两件事,
|
||||
* 屏上不得互认(三词三源见 docs/任务规划/数据可信.md §4.3) */
|
||||
uint8_t cal_block;
|
||||
} ui_data_par_t;
|
||||
|
||||
/* ── 通道三:装置信息(静态身份 + 通讯参数 + 运行时长)───────────────────── */
|
||||
@@ -276,6 +295,137 @@ uint32_t ui_data_par_seq(void);
|
||||
*/
|
||||
uint32_t ui_data_info_seq(void);
|
||||
|
||||
/* ── 写路径:定值整定请求(页面只投递,执行在 App 侧)───────────────────── */
|
||||
|
||||
/*
|
||||
* 请求队列深度(契约 docs/UI设计/UI_数据供给设计说明.md §6.2 定死 4)
|
||||
* 为什么是 4 不是 1:整定页"保存"是用户按键,一条请求要等 App 侧 drain 才落定;
|
||||
* 深度 1 会让连按两次保存把第二条判成"忙",而 4 格 = 本层 .bss 多
|
||||
* 4 × sizeof(ui_data_param_req_t)(168 B/格,主机单测 TC-UIDATA-016 钉住尺寸,
|
||||
* 内存台账 docs/内存管理/RAM使用分析与优化方案.md)。加深 = 纯 RAM 代价,
|
||||
* 本层不做任何按内存自适应
|
||||
*/
|
||||
#define UI_DATA_PARAM_QD 4u
|
||||
|
||||
/*
|
||||
* 一条定值写入请求 = "本次保存改了哪些项 + 它们的 Q10 新值"
|
||||
*
|
||||
* 为什么是「位图 + 定长 40 槽」而不是「项数 + 变长 id 数组」:表现层编辑叠层本就
|
||||
* 是一张改动位图(uint64_t)+ 一张工作值表(int32_t[UI_DATA_SET_N]),本形状与它
|
||||
* 逐位同构 → 提交侧一次 memcpy、执行侧逐项按位取,两侧都不必再维护第三种表示;
|
||||
* 定长还让队列退化成 O(1) 静态环形数组(零堆、零变长拷贝、零 malloc 门禁风险)。
|
||||
*
|
||||
* ⚠ mask 只有 64 位 → 定值项数硬上限 64;越界位由 ui_data.c 的编译期守卫拦住,
|
||||
* 将来项数破 64 必须换表示(不会静默丢项)。
|
||||
* ⚠ **mask 未置位的槽本次不提交**:本层与执行侧都不读它——调用方可以只填 mask 内
|
||||
* 的槽(整帧 memcpy 入队会把 mask 外的字节一并存下来,但那字节永不被读,不影响
|
||||
* 任何判定;先 memset 再填也完全可以,两种写法都安全)。
|
||||
* 绝不允许"从显示值回提全表"——Q10 存不下 0.95 这类返回系数(真存 972/1024),
|
||||
* 按显示域往返会把用户没动的项顺手改掉
|
||||
* (docs/任务规划/UI补齐.md §2 第 2 行判据"值不落盘"的前提)。
|
||||
*/
|
||||
typedef struct {
|
||||
uint64_t mask; /* bit i = 1 → 第 i 项随本请求提交 */
|
||||
int32_t q10[UI_DATA_SET_N]; /* Q10 新值;仅 mask 置位的槽有意义 */
|
||||
} ui_data_param_req_t;
|
||||
|
||||
/*
|
||||
* 写路径状态(页面保存行显示;语义表 = 契约 §6.3)
|
||||
* IDLE - 从未投递 → 页面显 ENTER
|
||||
* WAIT - 已投递未落定(队列里还有请求在路上)→ 页面显 WAIT
|
||||
* OK - 最近一条已逐项写入并置脏 → 页面显 OK
|
||||
* ERR_RANGE - 最近一条整批拒绝:任一项越界 → 一字未写 → 页面显 ERR 并高亮
|
||||
* 越界行(越界项经 ui_data_param_err_mask() 取)
|
||||
* ERR_LOCKED - 最近一条被定值闭锁拒(写侧自检失败态)→ 页面显 LOCK 并禁编辑
|
||||
* ERR_BUSY - 最近一次投递被拒:队列满 → 页面显 ERR、页脚"忙,请重试"
|
||||
* 状态变量的三条写入路径(本层唯二写者 = request 与 drain,同在 uiTask):
|
||||
* 投递成功 → WAIT;投递被拒(满)→ ERR_BUSY;drain 落定 → 终态,但队列仍有
|
||||
* 残留时改写回 WAIT。故"满被拒"看得见(ERR_BUSY),随后逐步 drain 又回到 WAIT
|
||||
*/
|
||||
#define UI_DATA_PARAM_ST_IDLE 0
|
||||
#define UI_DATA_PARAM_ST_WAIT 1
|
||||
#define UI_DATA_PARAM_ST_OK 2
|
||||
#define UI_DATA_PARAM_ST_ERR_RANGE 3
|
||||
#define UI_DATA_PARAM_ST_ERR_LOCKED 4
|
||||
#define UI_DATA_PARAM_ST_ERR_BUSY 5
|
||||
|
||||
/*
|
||||
* 执行侧函数表(由 App 在 ui_data_feed_tick() 每次调用时传入;本层不保存副本)
|
||||
*
|
||||
* 为什么用注入而不是直接调 prot_set:分层方向硬约束——Drivers/BSP 不得 include
|
||||
* App 头(scripts/check_layer_deps.py 拦提交),而"什么算越界、写了要不要置脏"
|
||||
* 全在 App/algo/prot_set 那侧。同契约 §7.2 记录页的注入式 reader:BSP 声明抽象
|
||||
* 函数表,App 给实现,主机单测给桩 → 队列与状态机的**生产代码**能在 PC 上跑
|
||||
* 全表(越界不改值 / 闭锁拒写 / 正常往返三条判据都覆盖到真代码,不是副本)。
|
||||
* ⚠ 本表不含任何"范围数值"——上下限真源仍是 App/algo/prot_set.c 的
|
||||
* g_prot_set_meta,本层只按 check() 的返回值决定整批写不写(不新增第二真源)
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t (*locked)(void); /* 1 = 定值闭锁中 */
|
||||
int (*check)(uint8_t a_id, int32_t a_q10); /* 0 合法 / -1 越界 */
|
||||
int (*apply)(uint8_t a_id, int32_t a_q10); /* 0 成功 / -1 失败 */
|
||||
} ui_data_param_ops_t;
|
||||
|
||||
/*
|
||||
* 函数功能:投递一条定值写入请求(页面"保存"行的唯一出口——只投递,不执行)
|
||||
* 入口参数:a_req - 请求(mask 与 q10 整条拷入队列,调用方可复用其栈空间)
|
||||
* const ui_data_param_req_t *
|
||||
* 返回值:0 - 已入队(状态转 WAIT);mask 为 0 的空请求也返回 0,但按"无改动可
|
||||
* 保存"直接落终态 OK 且**不占槽位**(保存行 ENTER 时一个都没改
|
||||
* 是正常操作,报 ERR 会让现场以为设备有问题);
|
||||
* -1 - 队列满(状态转 ERR_BUSY,在队请求一条不动);
|
||||
* -2 - 参数无效:NULL、或 mask 含 UI_DATA_SET_N 以上的位
|
||||
* (状态与队列都不动:调用方的编程错误不该落成屏上的"越界") int
|
||||
* 限定条件:uiTask 上下文(页面按键钩子);与 drain 同上下文 → 无锁
|
||||
* 函数说明:本函数**不**校验值域(越界与否是执行侧事实,在 drain 里整批预检),
|
||||
* 也不碰 prot_set、不碰文件系统(落盘归 defaultTask,AGENTS 模块约定)
|
||||
*/
|
||||
int ui_data_param_request(const ui_data_param_req_t *a_req);
|
||||
|
||||
/*
|
||||
* 函数功能:取写路径状态(页面保存行显示)
|
||||
* 入口参数:无
|
||||
* 返回值:UI_DATA_PARAM_ST_* 之一 int
|
||||
* 限定条件:无(只读单字,任意上下文安全)
|
||||
* 函数说明:ERR_TIMEOUT(契约 §6.3 的"3 s 未取走")本层**不产出**——uiTask 每
|
||||
* 10 ms 就 drain 一次,真出现"永不落定"说明 App 侧没接线,属装配缺陷;
|
||||
* 页面若要兜底可自己按投递时刻计时(docs/UI设计/UI_数据供给设计说明.md
|
||||
* §6.3 已把它列为页面本地可选项)
|
||||
*/
|
||||
int ui_data_param_state(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取最近一条请求的越界项位图(页面高亮越界行)
|
||||
* 入口参数:无
|
||||
* 返回值:bit i = 1 → 第 i 项越界;最近终态不是 ERR_RANGE 时恒 0 uint64_t
|
||||
* 限定条件:无(只读单字)
|
||||
* 函数说明:与 ui_data_par_t.bad_mask 是两条不同来源——bad_mask 是 prot_set 整表
|
||||
* 装载/解析留下的越界位图(App 侧事实),本函数是**本次提交**里被预检
|
||||
* 挡下的项;页面在 ERR_RANGE 那一帧用本函数,别把两者混成一份
|
||||
*/
|
||||
uint64_t ui_data_param_err_mask(void);
|
||||
|
||||
/*
|
||||
* 函数功能:处理一条定值写入请求(App 装配层节拍入口;出队 → 判定 → 落定)
|
||||
* 入口参数:a_ops - 执行侧函数表(三个函数指针都不得为 NULL)
|
||||
* const ui_data_param_ops_t *
|
||||
* 返回值:1 - 处理并提交了一条(状态已落终态或转 WAIT);0 - 队列空(状态不动);
|
||||
* -1 - 参数无效(ops 或其函数指针为 NULL,队列与状态都不动) int
|
||||
* 限定条件:uiTask 上下文,紧跟页面之后调(App/app/ui_data_feed.c 的 tick);
|
||||
* 每轮最多处理一条——整定是低频人工事件,一轮一条已够,且把最坏耗时
|
||||
* 钉在 40 次 check + 40 次 apply,不会随队列堆积而抖动
|
||||
* 函数说明:固定四步(顺序即判据,契约 §6.2)——
|
||||
* ① 取队首一条;② ops->locked() 非 0 → ERR_LOCKED,**本条丢弃**(闭锁
|
||||
* 是业务态,用户解闭锁后重新提交;不做"排队等解锁",那会让屏上的 WAIT
|
||||
* 无限期挂着);③ 按 mask 逐项 ops->check() 整批预检,任一项越界 →
|
||||
* ERR_RANGE 且**一字不写**(all-or-nothing,与 prot_set_load_raw 同纪律),
|
||||
* 越界项记入 err_mask;④ 预检全过再逐项 ops->apply(),全成 → OK。
|
||||
* ④ 中途失败(预检已过仍失败 = 执行侧与预检不一致,理论不可达)不回滚:
|
||||
* prot_set 无回滚接口,谎称"整批未写"比如实报 ERR_RANGE 更坏,故按实际
|
||||
* 失败项置 err_mask 后报 ERR_RANGE,并在注释里写明可能部分已生效
|
||||
*/
|
||||
int ui_data_param_drain(const ui_data_param_ops_t *a_ops);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -53,7 +53,7 @@ uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max);
|
||||
* 入口参数:node - 节点下标 uint8_t 0 - 表长-1
|
||||
* 返回值:true - 有子项(显示为菜单,带 ▶);false - 无子项(叶子,可进内容页)
|
||||
* 限定条件:纯查询,无副作用;node 越界返回 false
|
||||
* 函数说明:这是**谓词**(bool 合法),不是成败码——err-type 检查里白名单放行的形态。
|
||||
* 函数说明:这是**谓词**(bool 合法),不是成败码——bsp-err 的 err-type 检查里白名单放行的形态。
|
||||
*/
|
||||
bool ui_menu_tab_has_children(uint8_t node);
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = {
|
||||
{ 3u, &ui_page_ac_desc }, /* node 3 交流量 */
|
||||
{ 4u, &ui_page_dc_desc }, /* node 4 直流量 */
|
||||
{ 5u, &ui_page_yx_desc }, /* node 5 遥信量 */
|
||||
{ 8u, &ui_page_view_param_desc }, /* node 8 参数 */
|
||||
{ 9u, &ui_page_view_set_desc }, /* node 9 定值 */
|
||||
{ 10u, &ui_page_view_word_desc }, /* node 10 控制字 */
|
||||
{ 11u, &ui_page_view_plate_desc }, /* node 11 软压板 */
|
||||
{ 13u, &ui_page_view_param_desc }, /* node 13 参数 */
|
||||
{ 14u, &ui_page_view_set_desc }, /* node 14 定值 */
|
||||
{ 15u, &ui_page_view_word_desc }, /* node 15 控制字 */
|
||||
|
||||
@@ -25,7 +25,7 @@ typedef struct {
|
||||
const content_page_def_t *desc; /* 页面描述符(Flash const) */
|
||||
} ui_page_entry_t;
|
||||
|
||||
#define UI_PAGE_MAP_COUNT 12u /* 绑定了内容页的叶子数 */
|
||||
#define UI_PAGE_MAP_COUNT 16u /* 绑定了内容页的叶子数 */
|
||||
|
||||
extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT];
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
* 模块名称:UI Page view_param (参数查看内容页)
|
||||
* 模块功能:参数定值 → 查看 → 参数 的内容页——8 行数值表(PT/CT 一次二次额定 + 4 个返回系数),每页 8 行、1 页;
|
||||
* 数据 = 参数快照(Q10)按行表小数位换算,越界项由 bad 回调反显;
|
||||
* 本文件零 App include、零写路径(查看页没有"保存"这件事)
|
||||
* 本文件零 App include;写方向只有一个出口:整定态的"保存"经
|
||||
* ui_data_param_request() 投递(执行与落盘在 App 侧,本层不碰 prot_set/FatFs)
|
||||
* 两态一页(道F task-2:整定 4 叶绑定本描述符):同一份描述符既服务"查看"叶子
|
||||
* (只读——不绑 commit → 无保存行、按键全交回框架 → 像素与绑定前逐位一致),
|
||||
* 也服务"整定"叶子(123 授权 → 编辑叠层 + 保存行 + 未标定徽标)。
|
||||
* 分派点只有 pg_init 一处(按 ui_content_page_is_editable() 判),
|
||||
* 两态共用行表/取数/反显,不存在"查看一套边界、整定另一套边界"
|
||||
* 为什么这么薄:行表与取数在 ui_w_setview(四个查看页共用一份视图模型),
|
||||
* 渲染与反显在 ui_w_setlist(与整定页同一组件)——本文件只把视图编号
|
||||
* 和标准骨架几何令牌接进框架的跳板,改一次口径四处一致
|
||||
@@ -11,6 +17,11 @@
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-15 批 3)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-2:接整定态——pg_init 按权限分派
|
||||
* attach/attach_edit、新增 pg_key 转发按键、
|
||||
* pg_draw 追加"未标定"徽标、描述符补
|
||||
* edit_capable/key_intercept/key_cb 三项
|
||||
* (只读分支行为逐字不变)
|
||||
*/
|
||||
/* 头文件包含区 */
|
||||
#include "ui_page_view_param.h"
|
||||
@@ -32,11 +43,21 @@ static ui_w_setlist_t s_sl;
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:框架开页链内调用(uiTask)
|
||||
* 函数说明:每次开页都重新 attach——组件实例内残留的编辑态/游标会跨页带过来,
|
||||
* 开页复位比"信任上一次已经清干净"可靠
|
||||
* 函数说明:1. 每次开页都重新 attach——组件实例内残留的编辑态/游标会跨页带过来,
|
||||
* 开页复位比"信任上一次已经清干净"可靠
|
||||
* 2. **只读 / 整定两态在这里分派一次**,之后所有钩子只看已绑好的回调
|
||||
* (组件用 commit==NULL 判视图类型,不再回头看权限)
|
||||
* 3. 时序依据(为什么 init_cb 里问 is_editable() 拿到的就是本次进入的权限):
|
||||
* 框架 open() 先装 s_perm 与 s_open、最后才调 init_cb
|
||||
* (ui_content_page.c 的 open:s_perm=perm → s_open=desc → init_cb),
|
||||
* 所以这里读到的是"本次进入"的真值,不是上一次的残留
|
||||
*/
|
||||
static void pg_init(void) {
|
||||
ui_w_setview_attach(&s_sl, UI_W_SETVIEW_PARAM);
|
||||
if (ui_content_page_is_editable()) {
|
||||
ui_w_setview_attach_edit(&s_sl, UI_W_SETVIEW_PARAM); /* 整定:编辑 + 保存行 + 徽标 */
|
||||
} else {
|
||||
ui_w_setview_attach(&s_sl, UI_W_SETVIEW_PARAM); /* 查看:与绑定前完全同一条路径 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -60,26 +81,49 @@ static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:自绘层(越界行反显;框架内置区块画完后调用)
|
||||
* 函数功能:自绘层(越界行反显 + 未标定徽标;框架内置区块画完后调用)
|
||||
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
|
||||
* 返回值:无
|
||||
* 限定条件:框架绘制链内
|
||||
* 函数说明:徽标函数自己判"是不是整定视图 + 帧在不在 + cal_block 置没置",
|
||||
* 页面这里无条件调它——四页少写四个 if,也就少四次写漏的机会;
|
||||
* 查看页三个条件第一条就不成立,一个像素都不落(vparam~vplate
|
||||
* 四张黄金参考仍逐位一致)
|
||||
*/
|
||||
static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) {
|
||||
ui_w_setlist_draw(&s_sl, a_layer, a_clip);
|
||||
ui_w_setview_draw_cal_badge(a_layer, a_clip);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按键转发(框架 key_cb)
|
||||
* 入口参数:a_key - LVGL 键值 lv_key_t
|
||||
* 返回值:true - 本页已消费(框架跳过内置);false - 交回框架(↑↓ 移选、← 关页)
|
||||
* 限定条件:框架按键链内;描述符须 edit_capable=1 + key_intercept=1
|
||||
* 函数说明:两态行为的不同**不写在这里**——ui_w_setview_key() 先看视图类型
|
||||
* (查看页未绑 commit → 直接交回框架,等于本页没有 key_cb),
|
||||
* 再看参数帧在不在。所以这里必须原样透传返回值,不得就地"或"上什么
|
||||
*/
|
||||
static bool pg_key(lv_key_t a_key) {
|
||||
return (ui_w_setview_key(&s_sl, a_key) != 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=view_param 绑定引用)
|
||||
* edit_capable=0 + 无 key_cb + key_intercept=0:查看页不接受任何写交互,
|
||||
* ↑↓ 移选与 ← 关页全走框架内置(§3.2.7 表里"权限不足 ENTER 一律忽略"在
|
||||
* 查看页等价于"根本没有 ENTER 这条路")
|
||||
* edit_capable=1 + key_cb + key_intercept=1:同一份描述符两态共用。
|
||||
* · 整定叶子(123 授权 UI_PERM_MODIFY)→ is_editable() 真 → attach_edit +
|
||||
* 编辑叠层 + 保存行;key_intercept=1 是**必需**的:编辑中的 ←→ 移位光标
|
||||
* 要先给页面,否则框架拿 ← 当"关页",编辑到一半就跳出去
|
||||
* · 查看叶子(无 #P,恒 UI_PERM_VIEW)→ is_editable() 假 → 仍走 attach,
|
||||
* ui_w_setview_key() 第一条门就把键全部交回框架,↑↓ 移选与 ← 关页全走内置
|
||||
* ——即"绑了 key_cb 的查看页"与"没有 key_cb 的查看页"行为逐字等价
|
||||
* (§3.2.7:"权限不足 ENTER 一律忽略")
|
||||
*/
|
||||
const content_page_def_t ui_page_view_param_desc = {
|
||||
.id = "view_param",
|
||||
.title = "参数",
|
||||
.selectable = 1u,
|
||||
.edit_capable = 0u,
|
||||
.edit_capable = 1u, /* 两态页:能不能真编辑还要看本次进入的权限(is_editable) */
|
||||
.top_y = UI_LIST_TOP,
|
||||
.pitch = UI_ROW_PITCH,
|
||||
.band_h = UI_ROW_BAND_H,
|
||||
@@ -87,8 +131,10 @@ const content_page_def_t ui_page_view_param_desc = {
|
||||
.left_x = UI_LIST_LEFT_X,
|
||||
.right_x = UI_LIST_RIGHT_X,
|
||||
.rows_per_page = UI_LIST_ROWS,
|
||||
.key_intercept = 1u, /* ← 先交 key_cb:编辑中是"移位光标",非编辑中才轮到关页 */
|
||||
.init_cb = pg_init,
|
||||
.count_cb = pg_count,
|
||||
.refresh = pg_refresh,
|
||||
.draw_cb = pg_draw,
|
||||
.key_cb = pg_key,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
* 模块名称:UI Page view_plate (软压板查看内容页)
|
||||
* 模块功能:参数定值 → 查看 → 软压板 的内容页——10 行二态表(9 个段投退 + 保护总投:投入/退出),每页 8 行、2 页;
|
||||
* 数据 = 参数快照(Q10)按行表小数位换算,越界项由 bad 回调反显;
|
||||
* 本文件零 App include、零写路径(查看页没有"保存"这件事)
|
||||
* 本文件零 App include;写方向只有一个出口:整定态的"保存"经
|
||||
* ui_data_param_request() 投递(执行与落盘在 App 侧,本层不碰 prot_set/FatFs)
|
||||
* 两态一页(道F task-2:整定 4 叶绑定本描述符):同一份描述符既服务"查看"叶子
|
||||
* (只读——不绑 commit → 无保存行、按键全交回框架 → 像素与绑定前逐位一致),
|
||||
* 也服务"整定"叶子(123 授权 → 编辑叠层 + 保存行 + 未标定徽标)。
|
||||
* 分派点只有 pg_init 一处(按 ui_content_page_is_editable() 判),
|
||||
* 两态共用行表/取数/反显,不存在"查看一套边界、整定另一套边界"
|
||||
* 为什么这么薄:行表与取数在 ui_w_setview(四个查看页共用一份视图模型),
|
||||
* 渲染与反显在 ui_w_setlist(与整定页同一组件)——本文件只把视图编号
|
||||
* 和标准骨架几何令牌接进框架的跳板,改一次口径四处一致
|
||||
@@ -11,6 +17,11 @@
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-15 批 3)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-2:接整定态——pg_init 按权限分派
|
||||
* attach/attach_edit、新增 pg_key 转发按键、
|
||||
* pg_draw 追加"未标定"徽标、描述符补
|
||||
* edit_capable/key_intercept/key_cb 三项
|
||||
* (只读分支行为逐字不变)
|
||||
*/
|
||||
/* 头文件包含区 */
|
||||
#include "ui_page_view_plate.h"
|
||||
@@ -32,11 +43,21 @@ static ui_w_setlist_t s_sl;
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:框架开页链内调用(uiTask)
|
||||
* 函数说明:每次开页都重新 attach——组件实例内残留的编辑态/游标会跨页带过来,
|
||||
* 开页复位比"信任上一次已经清干净"可靠
|
||||
* 函数说明:1. 每次开页都重新 attach——组件实例内残留的编辑态/游标会跨页带过来,
|
||||
* 开页复位比"信任上一次已经清干净"可靠
|
||||
* 2. **只读 / 整定两态在这里分派一次**,之后所有钩子只看已绑好的回调
|
||||
* (组件用 commit==NULL 判视图类型,不再回头看权限)
|
||||
* 3. 时序依据(为什么 init_cb 里问 is_editable() 拿到的就是本次进入的权限):
|
||||
* 框架 open() 先装 s_perm 与 s_open、最后才调 init_cb
|
||||
* (ui_content_page.c 的 open:s_perm=perm → s_open=desc → init_cb),
|
||||
* 所以这里读到的是"本次进入"的真值,不是上一次的残留
|
||||
*/
|
||||
static void pg_init(void) {
|
||||
ui_w_setview_attach(&s_sl, UI_W_SETVIEW_PLATE);
|
||||
if (ui_content_page_is_editable()) {
|
||||
ui_w_setview_attach_edit(&s_sl, UI_W_SETVIEW_PLATE); /* 整定:编辑 + 保存行 + 徽标 */
|
||||
} else {
|
||||
ui_w_setview_attach(&s_sl, UI_W_SETVIEW_PLATE); /* 查看:与绑定前完全同一条路径 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -60,26 +81,49 @@ static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:自绘层(越界行反显;框架内置区块画完后调用)
|
||||
* 函数功能:自绘层(越界行反显 + 未标定徽标;框架内置区块画完后调用)
|
||||
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
|
||||
* 返回值:无
|
||||
* 限定条件:框架绘制链内
|
||||
* 函数说明:徽标函数自己判"是不是整定视图 + 帧在不在 + cal_block 置没置",
|
||||
* 页面这里无条件调它——四页少写四个 if,也就少四次写漏的机会;
|
||||
* 查看页三个条件第一条就不成立,一个像素都不落(vparam~vplate
|
||||
* 四张黄金参考仍逐位一致)
|
||||
*/
|
||||
static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) {
|
||||
ui_w_setlist_draw(&s_sl, a_layer, a_clip);
|
||||
ui_w_setview_draw_cal_badge(a_layer, a_clip);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按键转发(框架 key_cb)
|
||||
* 入口参数:a_key - LVGL 键值 lv_key_t
|
||||
* 返回值:true - 本页已消费(框架跳过内置);false - 交回框架(↑↓ 移选、← 关页)
|
||||
* 限定条件:框架按键链内;描述符须 edit_capable=1 + key_intercept=1
|
||||
* 函数说明:两态行为的不同**不写在这里**——ui_w_setview_key() 先看视图类型
|
||||
* (查看页未绑 commit → 直接交回框架,等于本页没有 key_cb),
|
||||
* 再看参数帧在不在。所以这里必须原样透传返回值,不得就地"或"上什么
|
||||
*/
|
||||
static bool pg_key(lv_key_t a_key) {
|
||||
return (ui_w_setview_key(&s_sl, a_key) != 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=view_plate 绑定引用)
|
||||
* edit_capable=0 + 无 key_cb + key_intercept=0:查看页不接受任何写交互,
|
||||
* ↑↓ 移选与 ← 关页全走框架内置(§3.2.7 表里"权限不足 ENTER 一律忽略"在
|
||||
* 查看页等价于"根本没有 ENTER 这条路")
|
||||
* edit_capable=1 + key_cb + key_intercept=1:同一份描述符两态共用。
|
||||
* · 整定叶子(123 授权 UI_PERM_MODIFY)→ is_editable() 真 → attach_edit +
|
||||
* 编辑叠层 + 保存行;key_intercept=1 是**必需**的:编辑中的 ←→ 移位光标
|
||||
* 要先给页面,否则框架拿 ← 当"关页",编辑到一半就跳出去
|
||||
* · 查看叶子(无 #P,恒 UI_PERM_VIEW)→ is_editable() 假 → 仍走 attach,
|
||||
* ui_w_setview_key() 第一条门就把键全部交回框架,↑↓ 移选与 ← 关页全走内置
|
||||
* ——即"绑了 key_cb 的查看页"与"没有 key_cb 的查看页"行为逐字等价
|
||||
* (§3.2.7:"权限不足 ENTER 一律忽略")
|
||||
*/
|
||||
const content_page_def_t ui_page_view_plate_desc = {
|
||||
.id = "view_plate",
|
||||
.title = "软压板",
|
||||
.selectable = 1u,
|
||||
.edit_capable = 0u,
|
||||
.edit_capable = 1u, /* 两态页:能不能真编辑还要看本次进入的权限(is_editable) */
|
||||
.top_y = UI_LIST_TOP,
|
||||
.pitch = UI_ROW_PITCH,
|
||||
.band_h = UI_ROW_BAND_H,
|
||||
@@ -87,8 +131,10 @@ const content_page_def_t ui_page_view_plate_desc = {
|
||||
.left_x = UI_LIST_LEFT_X,
|
||||
.right_x = UI_LIST_RIGHT_X,
|
||||
.rows_per_page = UI_LIST_ROWS,
|
||||
.key_intercept = 1u, /* ← 先交 key_cb:编辑中是"移位光标",非编辑中才轮到关页 */
|
||||
.init_cb = pg_init,
|
||||
.count_cb = pg_count,
|
||||
.refresh = pg_refresh,
|
||||
.draw_cb = pg_draw,
|
||||
.key_cb = pg_key,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
* 模块名称:UI Page view_set (定值查看内容页)
|
||||
* 模块功能:参数定值 → 查看 → 定值 的内容页——18 行数值表(三段过流/过负荷/过压/低压/零序/过频/低频 定值与延时),每页 8 行、3 页;
|
||||
* 数据 = 参数快照(Q10)按行表小数位换算,越界项由 bad 回调反显;
|
||||
* 本文件零 App include、零写路径(查看页没有"保存"这件事)
|
||||
* 本文件零 App include;写方向只有一个出口:整定态的"保存"经
|
||||
* ui_data_param_request() 投递(执行与落盘在 App 侧,本层不碰 prot_set/FatFs)
|
||||
* 两态一页(道F task-2:整定 4 叶绑定本描述符):同一份描述符既服务"查看"叶子
|
||||
* (只读——不绑 commit → 无保存行、按键全交回框架 → 像素与绑定前逐位一致),
|
||||
* 也服务"整定"叶子(123 授权 → 编辑叠层 + 保存行 + 未标定徽标)。
|
||||
* 分派点只有 pg_init 一处(按 ui_content_page_is_editable() 判),
|
||||
* 两态共用行表/取数/反显,不存在"查看一套边界、整定另一套边界"
|
||||
* 为什么这么薄:行表与取数在 ui_w_setview(四个查看页共用一份视图模型),
|
||||
* 渲染与反显在 ui_w_setlist(与整定页同一组件)——本文件只把视图编号
|
||||
* 和标准骨架几何令牌接进框架的跳板,改一次口径四处一致
|
||||
@@ -11,6 +17,11 @@
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-15 批 3)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-2:接整定态——pg_init 按权限分派
|
||||
* attach/attach_edit、新增 pg_key 转发按键、
|
||||
* pg_draw 追加"未标定"徽标、描述符补
|
||||
* edit_capable/key_intercept/key_cb 三项
|
||||
* (只读分支行为逐字不变)
|
||||
*/
|
||||
/* 头文件包含区 */
|
||||
#include "ui_page_view_set.h"
|
||||
@@ -32,11 +43,21 @@ static ui_w_setlist_t s_sl;
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:框架开页链内调用(uiTask)
|
||||
* 函数说明:每次开页都重新 attach——组件实例内残留的编辑态/游标会跨页带过来,
|
||||
* 开页复位比"信任上一次已经清干净"可靠
|
||||
* 函数说明:1. 每次开页都重新 attach——组件实例内残留的编辑态/游标会跨页带过来,
|
||||
* 开页复位比"信任上一次已经清干净"可靠
|
||||
* 2. **只读 / 整定两态在这里分派一次**,之后所有钩子只看已绑好的回调
|
||||
* (组件用 commit==NULL 判视图类型,不再回头看权限)
|
||||
* 3. 时序依据(为什么 init_cb 里问 is_editable() 拿到的就是本次进入的权限):
|
||||
* 框架 open() 先装 s_perm 与 s_open、最后才调 init_cb
|
||||
* (ui_content_page.c 的 open:s_perm=perm → s_open=desc → init_cb),
|
||||
* 所以这里读到的是"本次进入"的真值,不是上一次的残留
|
||||
*/
|
||||
static void pg_init(void) {
|
||||
ui_w_setview_attach(&s_sl, UI_W_SETVIEW_SET);
|
||||
if (ui_content_page_is_editable()) {
|
||||
ui_w_setview_attach_edit(&s_sl, UI_W_SETVIEW_SET); /* 整定:编辑 + 保存行 + 徽标 */
|
||||
} else {
|
||||
ui_w_setview_attach(&s_sl, UI_W_SETVIEW_SET); /* 查看:与绑定前完全同一条路径 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -60,26 +81,49 @@ static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:自绘层(越界行反显;框架内置区块画完后调用)
|
||||
* 函数功能:自绘层(越界行反显 + 未标定徽标;框架内置区块画完后调用)
|
||||
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
|
||||
* 返回值:无
|
||||
* 限定条件:框架绘制链内
|
||||
* 函数说明:徽标函数自己判"是不是整定视图 + 帧在不在 + cal_block 置没置",
|
||||
* 页面这里无条件调它——四页少写四个 if,也就少四次写漏的机会;
|
||||
* 查看页三个条件第一条就不成立,一个像素都不落(vparam~vplate
|
||||
* 四张黄金参考仍逐位一致)
|
||||
*/
|
||||
static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) {
|
||||
ui_w_setlist_draw(&s_sl, a_layer, a_clip);
|
||||
ui_w_setview_draw_cal_badge(a_layer, a_clip);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按键转发(框架 key_cb)
|
||||
* 入口参数:a_key - LVGL 键值 lv_key_t
|
||||
* 返回值:true - 本页已消费(框架跳过内置);false - 交回框架(↑↓ 移选、← 关页)
|
||||
* 限定条件:框架按键链内;描述符须 edit_capable=1 + key_intercept=1
|
||||
* 函数说明:两态行为的不同**不写在这里**——ui_w_setview_key() 先看视图类型
|
||||
* (查看页未绑 commit → 直接交回框架,等于本页没有 key_cb),
|
||||
* 再看参数帧在不在。所以这里必须原样透传返回值,不得就地"或"上什么
|
||||
*/
|
||||
static bool pg_key(lv_key_t a_key) {
|
||||
return (ui_w_setview_key(&s_sl, a_key) != 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=view_set 绑定引用)
|
||||
* edit_capable=0 + 无 key_cb + key_intercept=0:查看页不接受任何写交互,
|
||||
* ↑↓ 移选与 ← 关页全走框架内置(§3.2.7 表里"权限不足 ENTER 一律忽略"在
|
||||
* 查看页等价于"根本没有 ENTER 这条路")
|
||||
* edit_capable=1 + key_cb + key_intercept=1:同一份描述符两态共用。
|
||||
* · 整定叶子(123 授权 UI_PERM_MODIFY)→ is_editable() 真 → attach_edit +
|
||||
* 编辑叠层 + 保存行;key_intercept=1 是**必需**的:编辑中的 ←→ 移位光标
|
||||
* 要先给页面,否则框架拿 ← 当"关页",编辑到一半就跳出去
|
||||
* · 查看叶子(无 #P,恒 UI_PERM_VIEW)→ is_editable() 假 → 仍走 attach,
|
||||
* ui_w_setview_key() 第一条门就把键全部交回框架,↑↓ 移选与 ← 关页全走内置
|
||||
* ——即"绑了 key_cb 的查看页"与"没有 key_cb 的查看页"行为逐字等价
|
||||
* (§3.2.7:"权限不足 ENTER 一律忽略")
|
||||
*/
|
||||
const content_page_def_t ui_page_view_set_desc = {
|
||||
.id = "view_set",
|
||||
.title = "定值",
|
||||
.selectable = 1u,
|
||||
.edit_capable = 0u,
|
||||
.edit_capable = 1u, /* 两态页:能不能真编辑还要看本次进入的权限(is_editable) */
|
||||
.top_y = UI_LIST_TOP,
|
||||
.pitch = UI_ROW_PITCH,
|
||||
.band_h = UI_ROW_BAND_H,
|
||||
@@ -87,8 +131,10 @@ const content_page_def_t ui_page_view_set_desc = {
|
||||
.left_x = UI_LIST_LEFT_X,
|
||||
.right_x = UI_LIST_RIGHT_X,
|
||||
.rows_per_page = UI_LIST_ROWS,
|
||||
.key_intercept = 1u, /* ← 先交 key_cb:编辑中是"移位光标",非编辑中才轮到关页 */
|
||||
.init_cb = pg_init,
|
||||
.count_cb = pg_count,
|
||||
.refresh = pg_refresh,
|
||||
.draw_cb = pg_draw,
|
||||
.key_cb = pg_key,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
* 模块名称:UI Page view_word (控制字查看内容页)
|
||||
* 模块功能:参数定值 → 查看 → 控制字 的内容页——4 行二态表(*_TRIP:告警/跳闸),每页 8 行、1 页;
|
||||
* 数据 = 参数快照(Q10)按行表小数位换算,越界项由 bad 回调反显;
|
||||
* 本文件零 App include、零写路径(查看页没有"保存"这件事)
|
||||
* 本文件零 App include;写方向只有一个出口:整定态的"保存"经
|
||||
* ui_data_param_request() 投递(执行与落盘在 App 侧,本层不碰 prot_set/FatFs)
|
||||
* 两态一页(道F task-2:整定 4 叶绑定本描述符):同一份描述符既服务"查看"叶子
|
||||
* (只读——不绑 commit → 无保存行、按键全交回框架 → 像素与绑定前逐位一致),
|
||||
* 也服务"整定"叶子(123 授权 → 编辑叠层 + 保存行 + 未标定徽标)。
|
||||
* 分派点只有 pg_init 一处(按 ui_content_page_is_editable() 判),
|
||||
* 两态共用行表/取数/反显,不存在"查看一套边界、整定另一套边界"
|
||||
* 为什么这么薄:行表与取数在 ui_w_setview(四个查看页共用一份视图模型),
|
||||
* 渲染与反显在 ui_w_setlist(与整定页同一组件)——本文件只把视图编号
|
||||
* 和标准骨架几何令牌接进框架的跳板,改一次口径四处一致
|
||||
@@ -11,6 +17,11 @@
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-15 批 3)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-2:接整定态——pg_init 按权限分派
|
||||
* attach/attach_edit、新增 pg_key 转发按键、
|
||||
* pg_draw 追加"未标定"徽标、描述符补
|
||||
* edit_capable/key_intercept/key_cb 三项
|
||||
* (只读分支行为逐字不变)
|
||||
*/
|
||||
/* 头文件包含区 */
|
||||
#include "ui_page_view_word.h"
|
||||
@@ -32,11 +43,21 @@ static ui_w_setlist_t s_sl;
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:框架开页链内调用(uiTask)
|
||||
* 函数说明:每次开页都重新 attach——组件实例内残留的编辑态/游标会跨页带过来,
|
||||
* 开页复位比"信任上一次已经清干净"可靠
|
||||
* 函数说明:1. 每次开页都重新 attach——组件实例内残留的编辑态/游标会跨页带过来,
|
||||
* 开页复位比"信任上一次已经清干净"可靠
|
||||
* 2. **只读 / 整定两态在这里分派一次**,之后所有钩子只看已绑好的回调
|
||||
* (组件用 commit==NULL 判视图类型,不再回头看权限)
|
||||
* 3. 时序依据(为什么 init_cb 里问 is_editable() 拿到的就是本次进入的权限):
|
||||
* 框架 open() 先装 s_perm 与 s_open、最后才调 init_cb
|
||||
* (ui_content_page.c 的 open:s_perm=perm → s_open=desc → init_cb),
|
||||
* 所以这里读到的是"本次进入"的真值,不是上一次的残留
|
||||
*/
|
||||
static void pg_init(void) {
|
||||
ui_w_setview_attach(&s_sl, UI_W_SETVIEW_WORD);
|
||||
if (ui_content_page_is_editable()) {
|
||||
ui_w_setview_attach_edit(&s_sl, UI_W_SETVIEW_WORD); /* 整定:编辑 + 保存行 + 徽标 */
|
||||
} else {
|
||||
ui_w_setview_attach(&s_sl, UI_W_SETVIEW_WORD); /* 查看:与绑定前完全同一条路径 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -60,26 +81,49 @@ static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:自绘层(越界行反显;框架内置区块画完后调用)
|
||||
* 函数功能:自绘层(越界行反显 + 未标定徽标;框架内置区块画完后调用)
|
||||
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
|
||||
* 返回值:无
|
||||
* 限定条件:框架绘制链内
|
||||
* 函数说明:徽标函数自己判"是不是整定视图 + 帧在不在 + cal_block 置没置",
|
||||
* 页面这里无条件调它——四页少写四个 if,也就少四次写漏的机会;
|
||||
* 查看页三个条件第一条就不成立,一个像素都不落(vparam~vplate
|
||||
* 四张黄金参考仍逐位一致)
|
||||
*/
|
||||
static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) {
|
||||
ui_w_setlist_draw(&s_sl, a_layer, a_clip);
|
||||
ui_w_setview_draw_cal_badge(a_layer, a_clip);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按键转发(框架 key_cb)
|
||||
* 入口参数:a_key - LVGL 键值 lv_key_t
|
||||
* 返回值:true - 本页已消费(框架跳过内置);false - 交回框架(↑↓ 移选、← 关页)
|
||||
* 限定条件:框架按键链内;描述符须 edit_capable=1 + key_intercept=1
|
||||
* 函数说明:两态行为的不同**不写在这里**——ui_w_setview_key() 先看视图类型
|
||||
* (查看页未绑 commit → 直接交回框架,等于本页没有 key_cb),
|
||||
* 再看参数帧在不在。所以这里必须原样透传返回值,不得就地"或"上什么
|
||||
*/
|
||||
static bool pg_key(lv_key_t a_key) {
|
||||
return (ui_w_setview_key(&s_sl, a_key) != 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=view_word 绑定引用)
|
||||
* edit_capable=0 + 无 key_cb + key_intercept=0:查看页不接受任何写交互,
|
||||
* ↑↓ 移选与 ← 关页全走框架内置(§3.2.7 表里"权限不足 ENTER 一律忽略"在
|
||||
* 查看页等价于"根本没有 ENTER 这条路")
|
||||
* edit_capable=1 + key_cb + key_intercept=1:同一份描述符两态共用。
|
||||
* · 整定叶子(123 授权 UI_PERM_MODIFY)→ is_editable() 真 → attach_edit +
|
||||
* 编辑叠层 + 保存行;key_intercept=1 是**必需**的:编辑中的 ←→ 移位光标
|
||||
* 要先给页面,否则框架拿 ← 当"关页",编辑到一半就跳出去
|
||||
* · 查看叶子(无 #P,恒 UI_PERM_VIEW)→ is_editable() 假 → 仍走 attach,
|
||||
* ui_w_setview_key() 第一条门就把键全部交回框架,↑↓ 移选与 ← 关页全走内置
|
||||
* ——即"绑了 key_cb 的查看页"与"没有 key_cb 的查看页"行为逐字等价
|
||||
* (§3.2.7:"权限不足 ENTER 一律忽略")
|
||||
*/
|
||||
const content_page_def_t ui_page_view_word_desc = {
|
||||
.id = "view_word",
|
||||
.title = "控制字",
|
||||
.selectable = 1u,
|
||||
.edit_capable = 0u,
|
||||
.edit_capable = 1u, /* 两态页:能不能真编辑还要看本次进入的权限(is_editable) */
|
||||
.top_y = UI_LIST_TOP,
|
||||
.pitch = UI_ROW_PITCH,
|
||||
.band_h = UI_ROW_BAND_H,
|
||||
@@ -87,8 +131,10 @@ const content_page_def_t ui_page_view_word_desc = {
|
||||
.left_x = UI_LIST_LEFT_X,
|
||||
.right_x = UI_LIST_RIGHT_X,
|
||||
.rows_per_page = UI_LIST_ROWS,
|
||||
.key_intercept = 1u, /* ← 先交 key_cb:编辑中是"移位光标",非编辑中才轮到关页 */
|
||||
.init_cb = pg_init,
|
||||
.count_cb = pg_count,
|
||||
.refresh = pg_refresh,
|
||||
.draw_cb = pg_draw,
|
||||
.key_cb = pg_key,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道B task-5 三组件之一)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-2:① 保存行新增 LOCK 词 + 闭锁态
|
||||
* ENTER 不消费(禁编辑);② 二态行 ENTER **取模回绕**
|
||||
* (原实现落在最后一项时写出 opts 之外的下标:屏上
|
||||
* 看着没变、叠层已是越界值,保存必被预检挡回;
|
||||
* task-2 是本页第一个消费者,此前无人按过这条键);
|
||||
* 查看视图(commit/state 未绑)路径逐字未变
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -172,6 +178,11 @@ void ui_w_setlist_refresh(ui_w_setlist_t *a_inst, uint16_t a_row,
|
||||
case UI_W_SETLIST_ST_FAIL:
|
||||
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", "ERR");
|
||||
break;
|
||||
case UI_W_SETLIST_ST_LOCK:
|
||||
/* 定值闭锁:写不进去这件事必须写在保存行上(页脚/徽标都不算,
|
||||
* 用户按保存之前看到的就是这一格);词用 ASCII 与 ENTER/WAIT 同排 */
|
||||
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", "LOCK");
|
||||
break;
|
||||
default:
|
||||
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", "ENTER");
|
||||
break;
|
||||
@@ -211,6 +222,16 @@ uint8_t ui_w_setlist_key(ui_w_setlist_t *a_inst, lv_key_t a_key) {
|
||||
if (ui_content_page_is_editable() == false) {
|
||||
return 0u; /* 只读权限 ENTER 一律忽略(框架内置也无 ENTER 语义) */
|
||||
}
|
||||
/* 定值闭锁(state 回调回 LOCK)→ ENTER 一律不消费:不进数值编辑、不切二态、
|
||||
* 不投保存。权限门(上一条)说的是"这人不能写",这一条说的是"这台装置现在
|
||||
* 写不进"——两件事都必须落在键路上,只靠屏显词会让用户白改一轮。
|
||||
* ⚠ state() 回调可能带收口副作用(视图模型用它锁存落定结果),这里调它
|
||||
* 是幂等的:同一次落定只会被消费一次(见 ui_w_setview sv_state 的 s_pending)
|
||||
* ⚠ 已在编辑中的那一格不强行走这条路(编辑分支在上面已返回),其值只落在
|
||||
* RAM 工作副本,投递口已被本门堵住,落不了盘 */
|
||||
if ((a_inst->state != NULL) && (a_inst->state() == UI_W_SETLIST_ST_LOCK)) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
if (sel < a_inst->count) {
|
||||
const ui_w_setlist_row_t *r = &a_inst->rows[sel];
|
||||
@@ -222,14 +243,24 @@ uint8_t ui_w_setlist_key(ui_w_setlist_t *a_inst, lv_key_t a_key) {
|
||||
ui_content_page_invalidate();
|
||||
return 1u;
|
||||
}
|
||||
/* 二态行:循环切换(控制字 告警<->跳闸、软压板 投入<->退出) */
|
||||
/* 二态行:循环切换(控制字 告警<->跳闸、软压板 投入<->退出)
|
||||
* ⚠ **必须取模回绕**(道F task-2 实测坑):原写法 nxt = cur + 1 不回绕,
|
||||
* 落在最后一项时写出 opts 之外的下标 n——显示侧按 [0, n-1] 钳位,
|
||||
* 于是"按 ENTER 屏幕上什么也没变",而叠层里存的已是越界下标,
|
||||
* 随保存投出去就被 App 侧整批预检判成 ERR_RANGE:用户按了两下切换,
|
||||
* 结果是整页保存被"越界"挡回。本组件在 task-2 之前没有整定页消费者,
|
||||
* 这条只有接上写路径才会暴露(二态行 = 控制字 4 行 + 软压板 10 行,
|
||||
* 初始值全为"跳闸/投入"= 最后一项,所以是**必现**而不是边角)
|
||||
*/
|
||||
{
|
||||
int32_t cur = a_inst->get(r->id);
|
||||
uint8_t n = sl_opts_count(r->opts);
|
||||
int32_t nxt = cur + 1;
|
||||
int32_t nxt;
|
||||
|
||||
if ((cur < 0) || (cur >= (int32_t)n)) {
|
||||
nxt = 0;
|
||||
nxt = 0; /* 值区里是 opts 之外的脏值:回指第一项,不把脏值往下传 */
|
||||
} else {
|
||||
nxt = (int32_t)(((uint32_t)cur + 1u) % (uint32_t)n);
|
||||
}
|
||||
a_inst->set(r->id, nxt);
|
||||
ui_content_page_invalidate();
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
* §3.2 四视图表/§3.2.6 查看与整定差别/§3.2.7 编辑
|
||||
* 交互/§4.2 bind 要点;保存行 ENTER/WAIT/OK/ERR
|
||||
* 沿用 ui_page_net_cfg 交接显示先例)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-2(整定 4 页):态枚举尾追加 LOCK
|
||||
* (值列显 LOCK + ENTER 不消费 = 闭锁禁编辑);
|
||||
* 查看视图不绑 commit/state,走不到新分支,
|
||||
* 四张查看页像素零变化(黄金用例 vparam~vplate 为证)
|
||||
*/
|
||||
#ifndef UI_W_SETLIST_H
|
||||
#define UI_W_SETLIST_H
|
||||
@@ -37,12 +41,20 @@ extern "C" {
|
||||
#include "ui_content_page.h" /* lvgl(lv_key_t/lv_layer_t)+ 框架类型 */
|
||||
#include "ui_w_editor.h"
|
||||
|
||||
/* 保存交接显示态(值列 ENTER/WAIT/OK/ERR,与 net_cfg 保存行同字面) */
|
||||
/* 保存交接显示态(值列 ENTER/WAIT/OK/ERR/LOCK,与 net_cfg 保存行同字面)
|
||||
* ⚠ 新增态一律**尾追加**:前四个值按序被各页与先例使用,中间插入会把后续态的
|
||||
* 数值整体挪位——数值挪位在 C 里不报错,只会让屏上的词换成另一个词 */
|
||||
typedef enum {
|
||||
UI_W_SETLIST_ST_IDLE = 0, /* 空闲:保存行显示 ENTER */
|
||||
UI_W_SETLIST_ST_WAIT, /* 请求已投递、属主任务处理中:WAIT */
|
||||
UI_W_SETLIST_ST_OK, /* 上轮提交成功:OK */
|
||||
UI_W_SETLIST_ST_FAIL /* 上轮提交失败:ERR */
|
||||
UI_W_SETLIST_ST_FAIL, /* 上轮提交失败:ERR */
|
||||
UI_W_SETLIST_ST_LOCK /* 定值闭锁(= par.locked,校验和自检失败):值列 LOCK
|
||||
* 且**禁编辑**(组件见 LOCK 即不消费 ENTER)。
|
||||
* ⚠ 与「未标定」是两件事、屏上两条通道:未标定
|
||||
* (= par.cal_block)仍可改定值、只是判据停走,
|
||||
* 用标题右侧徽标表达;LOCK 才是"写不进去"。
|
||||
* 三词三源 → docs/任务规划/数据可信.md §4.3 */
|
||||
} ui_w_setlist_st_t;
|
||||
|
||||
/* 行种类 */
|
||||
@@ -174,6 +186,11 @@ void ui_w_setlist_refresh(ui_w_setlist_t *a_inst, uint16_t a_row,
|
||||
* 整表提交口径见 页面补齐规划.md §3.2)
|
||||
* 3. 只读权限 ENTER 一律不消费;ESC 恒关页(框架,未提交编辑丢弃,
|
||||
* 已经 ENTER 提交到工作副本的调整保留、不落盘——net_cfg 先例)
|
||||
* 4. 整定视图 state()==LOCK(定值闭锁)→ ENTER 同样一律不消费:
|
||||
* 不进数值编辑、不切二态、不投保存——**为什么闭锁不由组件解释**,
|
||||
* 它只按 state 回调给的事实收住写交互(闭锁 vs 未标定的分工见
|
||||
* ui_w_setlist_st_t 的 LOCK 注);已在编辑中的那一格不强制打断,
|
||||
* 但其值只落在工作副本,投递口已被 LOCK 堵住,落不了盘
|
||||
*/
|
||||
uint8_t ui_w_setlist_key(ui_w_setlist_t *a_inst, lv_key_t a_key);
|
||||
|
||||
|
||||
+402
-31
@@ -1,12 +1,20 @@
|
||||
/*
|
||||
* 模块名称:UI 定值查看视图模型(ui_w_setview)
|
||||
* 模块功能:参数 / 定值 / 控制字 / 软压板四张视图表与它们的取数、换算、越界反显——
|
||||
* 供四个查看页薄跳板共用;声明与分层理由见 ui_w_setview.h
|
||||
* 四个查看页与四个整定页**共用同一批表**(attach = 只读,attach_edit =
|
||||
* 可写:另绑 set/commit/state + 编辑叠层);声明与分层理由见 ui_w_setview.h
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(共用源码)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-15 批 3)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-2:整定侧落地——编辑叠层(改动位图 +
|
||||
* 工作值,只提交改动 id)、sv_commit 经
|
||||
* ui_data_param_request() 投递、sv_state 锁存落定
|
||||
* 并映射 LOCK、bad 回调加"本次提交挡下项"第二来源、
|
||||
* 统一按键门 ui_w_setview_key()、标题"未标定"徽标;
|
||||
* 新增 .bss ≈190 B(叠层 160 B + 位图/态),
|
||||
* 查看路径与四张行表逐字未变
|
||||
*/
|
||||
/* 头文件包含区 */
|
||||
#include <string.h>
|
||||
@@ -113,6 +121,21 @@ static const uint8_t s_dec[UI_DATA_SET_N] = {
|
||||
/* 无参数帧时值列的占位串(与全仓"读不到就 --"同一口径) */
|
||||
#define SV_NO_DATA "--"
|
||||
|
||||
/* 参数帧/落定结果轮询节拍(ms):见 sv_poll_cb 的"为什么必须有"。
|
||||
* 100 ms 是"用户看不出延迟"与"每拍一次比较"的折中,比 rt 帧的 200 ms 更细一档,
|
||||
* 这样保存落定与主站写值最迟 100 ms 上屏 */
|
||||
#define SV_POLL_MS 100u
|
||||
|
||||
/* 轮询定时器(LVGL 池对象,首张定值页 attach 时建、只建不删;NULL 初始化是
|
||||
* coding-standard CODE-RULE-15 对本层唯一文件作用域指针的要求) */
|
||||
static lv_timer_t *s_poll_timer = NULL;
|
||||
|
||||
/* 未标定徽标(整定页标题右侧):三字已在字库里(交流量页标定行、SOE type7 在用),
|
||||
* 故本次不新增字面字符;词与 par.cal_block 的屏上口径一致,别换成"标定失败"
|
||||
* 之类没登记过的说法(font-charset 与像素断言都按这个词钉) */
|
||||
#define SV_CAL_BADGE "未标定"
|
||||
#define SV_BADGE_RIGHT_X 141 /* 文字右边界:让开"浅 V"右腿(y7~17 占 x≈147~158) */
|
||||
|
||||
/* 参数帧缓存(四个查看页共用一份:同屏只开一页,无并发读者) */
|
||||
static ui_data_par_t s_par;
|
||||
static uint32_t s_par_seq = 0xFFFFFFFFu; /* 与任何合法发布序号都不同 */
|
||||
@@ -122,6 +145,22 @@ static uint8_t s_par_ok;
|
||||
static const ui_w_setlist_row_t *s_cur_rows;
|
||||
static uint16_t s_cur_n; /* 当前视图行数(bad 回调的边界) */
|
||||
|
||||
/*
|
||||
* 整定视图状态(道F task-2)
|
||||
* ⚠ 与 s_cur_rows 同一假设:**同屏只开一页**,故这些状态是模块级单份而非按实例存
|
||||
* (查看页四份缓存同一条理由)。将来要并排开两页,得连 s_cur_rows 一起收进实例
|
||||
*
|
||||
* RAM 台账:s_wq 160 B + 三个位图 24 B + 态与标志 ≈ **190 B .bss**
|
||||
*(落 RW_IRAM1;口径 docs/内存管理/RAM使用分析与优化方案.md,回报里已单独列给 Lead)
|
||||
*/
|
||||
static int32_t s_wq[UI_DATA_SET_N]; /* 工作值(工程量 × 10^dec,下标 = id) */
|
||||
static uint64_t s_wch; /* 改动位图:bit=1 → 该行本次要提交 */
|
||||
static uint64_t s_commit_mask; /* 在飞请求覆盖的位(落定后按它清叠层) */
|
||||
static uint64_t s_bad_mask; /* 本次提交被挡下的越界项(bad 回调第二个来源) */
|
||||
static ui_w_setlist_st_t s_disp; /* 保存行显示态(锁存,见 sv_state 的注) */
|
||||
static uint8_t s_pending; /* 1 = 已投递未落定 */
|
||||
static uint8_t s_edit; /* 1 = 当前 attach 是整定视图(徽标/按键门都看它) */
|
||||
|
||||
/* 行表长度(编译期算,别手写数字——手写数字与表长不一致是最难查的错) */
|
||||
#define SV_ROW_N(tbl) ((uint16_t)(sizeof(tbl) / sizeof((tbl)[0])))
|
||||
|
||||
@@ -162,6 +201,44 @@ static int32_t sv_q10_to_scaled(int32_t a_q10, uint8_t a_dec) {
|
||||
return r;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:显示值(工程量 × 10^dec)→ Q10 定点(整定提交方向,sv_q10_to_scaled 的逆)
|
||||
* 入口参数:a_scaled - 显示域整数值 int32_t
|
||||
* a_dec - 该行小数位 0..3 uint8_t
|
||||
* 返回值:Q10 定点值(工程量 × 1024) int32_t
|
||||
* 限定条件:a_dec <= 3;a_scaled 非负(组件编辑器契约 min>=0,本工程定值全非负)
|
||||
* 函数说明:q10 = (scaled×1024 + den/2) / den,四舍五入,**纯 32 位整数**——
|
||||
* 不用 float、更不用 __aeabi_ldiv(64 位除),理由与 sv_q10_to_scaled 同一条
|
||||
* (MicroLIB 代价,模拟器照不出来)。溢出界已核:最大显示值 PT一次额定
|
||||
* 500000(dec=0)→ 500000×1024 = 512,000,000 < int32 满
|
||||
* ⚠ 往返精度:这条换算与正向量程不是逐位可逆的(Q10 存 972 → 显示 95 →
|
||||
* 再写回 973,差 1 个 LSB)。所以**只提交改动位图内的 id**:用户没动的
|
||||
* 行根本不进这个函数,也就不会被"顺手改掉"(docs/UI设计/UI_数据供给设计
|
||||
* 说明.md §6.2 的 mask 语义;越界值不落盘这条判据也建在它上面)
|
||||
*/
|
||||
static int32_t sv_scaled_to_q10(int32_t a_scaled, uint8_t a_dec) {
|
||||
static const int32_t p10[4] = { 1, 10, 100, 1000 };
|
||||
int32_t den = p10[a_dec];
|
||||
|
||||
return (a_scaled * 1024 + den / 2) / den;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:判断两区域相交(与本目录各绘制文件同实现:static 局部)
|
||||
* 入口参数:a - 区域 1 const lv_area_t *
|
||||
* b - 区域 2 const lv_area_t *
|
||||
* 返回值:true = 相交 bool
|
||||
* 限定条件:无
|
||||
*/
|
||||
static bool sv_areas_intersect(const lv_area_t *a, const lv_area_t *b) {
|
||||
int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1;
|
||||
int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1;
|
||||
int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2;
|
||||
int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2;
|
||||
|
||||
return (x1 <= x2 && y1 <= y2);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读某定值项的显示值(组件 get 回调)
|
||||
* 入口参数:a_id - 行表登记的 id(= 枚举序) uint8_t
|
||||
@@ -191,6 +268,144 @@ static void sv_set_unused(uint8_t a_id, int32_t a_val) {
|
||||
(void)a_val;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读某定值项的**编辑态**显示值(整定视图的 get 回调)
|
||||
* 入口参数:a_id - 行表登记的 id(= prot_set 枚举序) uint8_t
|
||||
* 返回值:改过 → 叠层工作值;未改 → 参数帧换算值 int32_t
|
||||
* 限定条件:整定视图(attach_edit)专用;帧未就位时由页面值列打 "--",走不到这里
|
||||
* 函数说明:叠层优先——用户改到一半的数必须以叠层为准,否则每帧重绘都会把
|
||||
* 未提交的编辑冲回帧值("改完光标一动就跳回原值"就是这种实现写反了);
|
||||
* 未改的行反过来一律跟随帧,主站 C_SE 写进来的新值 ≤200 ms 自己上屏
|
||||
*/
|
||||
static int32_t sv_get_edit(uint8_t a_id) {
|
||||
if ((a_id < (uint8_t)UI_DATA_SET_N) && ((s_wch & ((uint64_t)1u << a_id)) != 0u)) {
|
||||
return s_wq[a_id];
|
||||
}
|
||||
return sv_get(a_id);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写某定值项的编辑叠层(整定视图的 set 回调;不落任何数据源)
|
||||
* 入口参数:a_id - 行 id uint8_t
|
||||
* a_val - 新显示值(工程量 × 10^dec) int32_t
|
||||
* 返回值:无
|
||||
* 限定条件:整定视图专用;组件只在 ENTER 提交/二态切换时调
|
||||
* 函数说明:只置位不改帧——真正的写入发生在保存行 ENTER → sv_commit → 快照层队列
|
||||
* → App 侧 prot_set_set_sealed()。顺手清掉该行的"上次越界"反显:
|
||||
* 用户已经改过的行还挂着上一次失败的 redflag,是在说一件已经不成立的事
|
||||
*/
|
||||
static void sv_set_edit(uint8_t a_id, int32_t a_val) {
|
||||
if (a_id >= (uint8_t)UI_DATA_SET_N) {
|
||||
return;
|
||||
}
|
||||
s_wq[a_id] = a_val;
|
||||
s_wch |= ((uint64_t)1u << a_id);
|
||||
s_bad_mask &= ~((uint64_t)1u << a_id);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:提交本次改动(整定视图的 commit 回调——只投递,不执行、不等待)
|
||||
* 入口参数:无
|
||||
* 返回值:无(结果经 sv_state 回读,组件保存行显示)
|
||||
* 限定条件:uiTask 按键链内(保存行 ENTER);整定视图专用
|
||||
* 函数说明:请求形状 = 改动位图 + 按位换算的 Q10(ui_data_param_req_t,168 B 栈帧)。
|
||||
* 本函数**不**判断越界、不判闭锁、更不碰 FatFs:那三件事分别属于
|
||||
* prot_set 的 meta、prot_set 的自检态与 defaultTask(契约 §6.1)。
|
||||
* 队列满(-1)时叠层原样保留:用户直接再按一次保存即可,不必重输
|
||||
*/
|
||||
static void sv_commit(void) {
|
||||
ui_data_param_req_t req;
|
||||
uint64_t bit = 1u;
|
||||
uint8_t i;
|
||||
int ret;
|
||||
|
||||
(void)memset(&req, 0, sizeof(req));
|
||||
req.mask = s_wch;
|
||||
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
|
||||
if ((s_wch & bit) != 0u) {
|
||||
req.q10[i] = sv_scaled_to_q10(s_wq[i], s_dec[i]);
|
||||
}
|
||||
bit <<= 1u;
|
||||
}
|
||||
s_bad_mask = 0u; /* 新一轮提交:上一次的越界认定作废 */
|
||||
ret = ui_data_param_request(&req);
|
||||
if (ret == 0) {
|
||||
s_commit_mask = s_wch;
|
||||
s_pending = 1u;
|
||||
s_disp = UI_W_SETLIST_ST_WAIT;
|
||||
} else {
|
||||
s_pending = 0u;
|
||||
s_commit_mask = 0u;
|
||||
s_disp = UI_W_SETLIST_ST_FAIL; /* 忙/参数无效 → 页脚口径"忙,请重试" */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:回读保存行显示态(整定视图的 state 回调;顺带收口一次落定)
|
||||
* 入口参数:无
|
||||
* 返回值:ui_w_setlist_st_t(IDLE/WAIT/OK/FAIL/LOCK)
|
||||
* 限定条件:uiTask(组件绘制保存行时调用;ui_w_setlist_key 的闭锁门也调它)
|
||||
* 函数说明:为什么要把落定"锁存"进 s_disp 而不是每次直读快照层状态——
|
||||
* 快照层的 ui_data_param_state() 是**全局单值**,落定后一直停在那儿;
|
||||
* 直读会让用户移动一下光标(触发整页重绘)就看到别的页留下的旧结果,
|
||||
* 甚至把本次
|
||||
* OK 显示成 ENTER。这里改成事件锁存:本页投过一次才显示那次的答案,
|
||||
* 开页 attach_edit 复位成 ENTER。
|
||||
* 优先级(从高到低,逐条都有各自的"为什么"):
|
||||
* ① 帧里 locked=1 → LOCK:装置现在写不进去,比任何历史结果都该先说;
|
||||
* ② 在飞请求落定 → WAIT/OK/FAIL/LOCK(ERR_LOCKED 也映射成 LOCK:
|
||||
* 帧可能还没刷到 locked 位,但执行侧刚说过的"闭锁"是当下一手事实);
|
||||
* ③ 否则回显锁存值(ENTER/OK/ERR 等)
|
||||
*/
|
||||
static ui_w_setlist_st_t sv_state(void) {
|
||||
ui_w_setview_sync();
|
||||
|
||||
if ((s_par_ok != 0u) && (s_par.locked != 0u)) {
|
||||
return UI_W_SETLIST_ST_LOCK;
|
||||
}
|
||||
if (s_pending != 0u) {
|
||||
int p = ui_data_param_state();
|
||||
|
||||
if (p == UI_DATA_PARAM_ST_WAIT) {
|
||||
s_disp = UI_W_SETLIST_ST_WAIT;
|
||||
} else {
|
||||
s_pending = 0u; /* 一次落定只消费一次 */
|
||||
if (p == UI_DATA_PARAM_ST_OK) {
|
||||
s_wch &= ~s_commit_mask; /* 这批已进值区:叠层让位给新帧 */
|
||||
s_bad_mask = 0u;
|
||||
s_disp = UI_W_SETLIST_ST_OK;
|
||||
} else {
|
||||
s_bad_mask = (p == UI_DATA_PARAM_ST_ERR_RANGE)
|
||||
? ui_data_param_err_mask() : 0u;
|
||||
s_disp = (p == UI_DATA_PARAM_ST_ERR_LOCKED)
|
||||
? UI_W_SETLIST_ST_LOCK : UI_W_SETLIST_ST_FAIL;
|
||||
}
|
||||
s_commit_mask = 0u;
|
||||
}
|
||||
}
|
||||
return s_disp;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:复位整定侧状态(attach / attach_edit 开页时都调)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask(开页 attach 链内,先于组件 reset)
|
||||
* 函数说明:**每次开页都清稿**是刻意的:改动位图与工作值只在本页这一次打开里有意义,
|
||||
* 带进下一次打开会出现"没改任何东西却显示着上次的编辑值"(更糟的是下次
|
||||
* 保存会把那份陈旧草稿投出去)。ESC 关页天然弃稿(不投递),与本函数
|
||||
* 合起来正好是 net_cfg 那套"改到一半退出 = 什么都没发生"的语义
|
||||
*/
|
||||
static void sv_view_reset(void) {
|
||||
(void)memset(s_wq, 0, sizeof(s_wq));
|
||||
s_wch = 0u;
|
||||
s_commit_mask = 0u;
|
||||
s_bad_mask = 0u;
|
||||
s_disp = UI_W_SETLIST_ST_IDLE;
|
||||
s_pending = 0u;
|
||||
s_edit = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:某数据行是否越界(组件 bad 回调 → 反显高亮)
|
||||
* 入口参数:a_row - 数据行下标(不含保存行) uint16_t
|
||||
@@ -205,7 +420,7 @@ static bool sv_bad(uint16_t a_row) {
|
||||
const ui_w_setlist_row_t *r;
|
||||
uint64_t bit;
|
||||
|
||||
if ((s_cur_rows == NULL) || (s_par_ok == 0u) || (s_par.bad_valid == 0u)) {
|
||||
if (s_cur_rows == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (a_row >= s_cur_n) {
|
||||
@@ -216,9 +431,74 @@ static bool sv_bad(uint16_t a_row) {
|
||||
return false;
|
||||
}
|
||||
bit = (uint64_t)1u << r->id;
|
||||
/*
|
||||
* 两个来源,任一命中即反显(整定批次加了①,查看页②逐字不变):
|
||||
* ① s_bad_mask —— **本次提交**被 App 侧整批预检挡下的项(ui_data_param_err_mask()
|
||||
* 经 sv_state 收进来,只在 ERR_RANGE 那一轮非 0;用户再改该行、或重新提交
|
||||
* 即清)。这是"越界行高亮"判据的屏上落点:整批一字未写,但必须说清是哪几行
|
||||
* 把人挡住的,否则用户只能逐行猜。
|
||||
* ② s_par.bad_mask —— prot_set 整表装载/解析留下的越界位图,且必须 bad_valid=1
|
||||
* 才参与(bad_valid=0 时位图无意义,把"没数据"画成"这行有问题"是假告警)
|
||||
*/
|
||||
if ((s_bad_mask & bit) != 0u) {
|
||||
return true;
|
||||
}
|
||||
if ((s_par_ok == 0u) || (s_par.bad_valid == 0u)) {
|
||||
return false;
|
||||
}
|
||||
return (s_par.bad_mask & bit) != 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:参数帧/落定结果轮询(LVGL 定时器,100 ms 一拍)
|
||||
* 入口参数:a_timer - 定时器句柄(未使用) lv_timer_t *
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文(LVGL 定时器在 lv_timer_handler 内跑,与页面同一线程)
|
||||
* 函数说明:**为什么必须有这条定时器**——LVGL 只重绘被 invalidate 的区域,而本页
|
||||
* 的数据是**异步**变的:
|
||||
* ① 参数帧由 App 侧节拍发布(保存落定后校验和变、主站 C_SE 写进来、
|
||||
* 标定完成让 cal_block 翻转),页面自己无从察觉;
|
||||
* ② 整定的落定结果更隐蔽:ERR_RANGE / ERR_LOCKED **不改校验和**,
|
||||
* 也就不会发布新帧——只有 ui_data_param_state() 变了。
|
||||
* 没有这条轮询,用户按完"保存"屏上会一直停在 WAIT,越界行的反显也要
|
||||
* 等下一次按键才出现(判据"越界行高亮"在真机上就落不了地)。
|
||||
* 代价:一拍一次 uint32 比较 + 一次 bool 判断,无变化时零重绘、零拷贝;
|
||||
* 页面没打开时直接返回(菜单屏/待机屏一像素都不动)。
|
||||
* 与 net_cfg 的差别:net_cfg 的保存行状态至今只在按键触发的重绘里更新
|
||||
* (先例的既有局限),本页不能沿用——整定的 OK/ERR/LOCK 是用户判断
|
||||
* "这台装置到底吃了我改的值没有"的唯一凭据
|
||||
*/
|
||||
static void sv_poll_cb(lv_timer_t *a_timer) {
|
||||
(void)a_timer;
|
||||
|
||||
if (ui_content_page_is_open() == false) {
|
||||
return;
|
||||
}
|
||||
if (ui_data_par_seq() != s_par_seq) {
|
||||
ui_content_page_invalidate(); /* 新参数帧:整页重绘(refresh 里同步序号) */
|
||||
return;
|
||||
}
|
||||
if ((s_pending != 0u) && (ui_data_param_state() != UI_DATA_PARAM_ST_WAIT)) {
|
||||
ui_content_page_invalidate(); /* 落定但不改校验和(ERR_RANGE/ERR_LOCKED) */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:确保轮询定时器已建(首张定值相关页面 attach 时建一次,之后不再建)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask(页面 open 链内)
|
||||
* 函数说明:只建不删(同 ui_standby_init 的 sb_idle_timer_cb 先例:模块生命周期 =
|
||||
* UI 生命周期);回调自带"没开页就返回"的门,停在菜单屏时它只是一次比较。
|
||||
* 建失败 = LVGL 池耗尽,此时页面退回"按键才重绘"的旧行为——不另外报错,
|
||||
* 因为那条错误在屏上没有任何可显示的通道(池耗尽本身早已是更大的问题)
|
||||
*/
|
||||
static void sv_poll_ensure(void) {
|
||||
if (s_poll_timer == NULL) {
|
||||
s_poll_timer = lv_timer_create(sv_poll_cb, SV_POLL_MS, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按发布序号取参数帧快照(接各查看页 refresh 前调用)
|
||||
* 入口参数:无
|
||||
@@ -348,34 +628,125 @@ void ui_w_setview_attach(ui_w_setlist_t *a_inst, ui_w_setview_t a_view) {
|
||||
return;
|
||||
}
|
||||
(void)memset(a_inst, 0, sizeof(*a_inst));
|
||||
switch (a_view) {
|
||||
case UI_W_SETVIEW_PARAM:
|
||||
a_inst->rows = s_rows_param;
|
||||
a_inst->count = SV_ROW_N(s_rows_param);
|
||||
break;
|
||||
case UI_W_SETVIEW_SET:
|
||||
a_inst->rows = s_rows_set;
|
||||
a_inst->count = SV_ROW_N(s_rows_set);
|
||||
break;
|
||||
case UI_W_SETVIEW_WORD:
|
||||
a_inst->rows = s_rows_word;
|
||||
a_inst->count = SV_ROW_N(s_rows_word);
|
||||
break;
|
||||
case UI_W_SETVIEW_PLATE:
|
||||
a_inst->rows = s_rows_plate;
|
||||
a_inst->count = SV_ROW_N(s_rows_plate);
|
||||
break;
|
||||
default:
|
||||
a_inst->rows = NULL;
|
||||
a_inst->count = 0u;
|
||||
break;
|
||||
}
|
||||
s_cur_rows = a_inst->rows;
|
||||
s_cur_n = a_inst->count;
|
||||
a_inst->get = sv_get;
|
||||
a_inst->set = sv_set_unused;
|
||||
a_inst->commit = NULL; /* NULL = 查看视图:无保存行、禁编辑(§3.2.6) */
|
||||
a_inst->state = NULL;
|
||||
a_inst->bad = sv_bad;
|
||||
/* 行表取自 ui_w_setview_table()(本模块唯一的行表出口):attach 与 attach_edit
|
||||
* 共用同一张表,才会出现"查看合法、整定也合法"——两处各自 switch 就是两份口径 */
|
||||
a_inst->rows = ui_w_setview_table(a_view, &a_inst->count);
|
||||
s_cur_rows = a_inst->rows;
|
||||
s_cur_n = a_inst->count;
|
||||
a_inst->get = sv_get;
|
||||
a_inst->set = sv_set_unused;
|
||||
/* NULL = 查看视图:无保存行、禁编辑(§3.2.6);state 同 NULL → 保存行也没得显示 */
|
||||
a_inst->commit = NULL;
|
||||
a_inst->state = NULL;
|
||||
a_inst->bad = sv_bad;
|
||||
/* 整定侧状态一律复位:查看页既不该看见上一次的改动位图,也不该在下次开整定页
|
||||
* 时把它带过去(s_edit=0 同时关掉"未标定"徽标——查看页像素零变化的硬条件) */
|
||||
sv_view_reset();
|
||||
sv_poll_ensure(); /* 帧是异步变的:页面必须能自己发现新帧/落定(见 sv_poll_cb) */
|
||||
ui_w_setlist_reset(a_inst);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把某个**整定**视图绑到定值表实例(行表与 attach 同一批,另绑写回调)
|
||||
* 入口参数:a_inst - 页面持有的实例(NULL 直接返回) ui_w_setlist_t *
|
||||
* a_view - 视图编号 ui_w_setview_t
|
||||
* 返回值:无
|
||||
* 限定条件:页面 init_cb 内、且 ui_content_page_is_editable() 为真时才调
|
||||
* (只读权限进来调这个等于给只读用户开了保存行)
|
||||
* 函数说明:与 attach 的差别只有 get/set/commit/state 四个钩子 + s_edit 标志,
|
||||
* 行表、行数、bad 回调、越界反显全部复用——**同一张行表**是这一版
|
||||
* 设计的关键:上下限的镜像只有一份,漂移由 setview-check 钩子拦
|
||||
*/
|
||||
void ui_w_setview_attach_edit(ui_w_setlist_t *a_inst, ui_w_setview_t a_view) {
|
||||
if (a_inst == NULL) {
|
||||
return;
|
||||
}
|
||||
(void)memset(a_inst, 0, sizeof(*a_inst));
|
||||
a_inst->rows = ui_w_setview_table(a_view, &a_inst->count);
|
||||
s_cur_rows = a_inst->rows;
|
||||
s_cur_n = a_inst->count;
|
||||
a_inst->get = sv_get_edit;
|
||||
a_inst->set = sv_set_edit;
|
||||
a_inst->commit = sv_commit; /* 非 NULL = 整定视图:组件画保存行并放开编辑 */
|
||||
a_inst->state = sv_state;
|
||||
a_inst->bad = sv_bad;
|
||||
sv_view_reset();
|
||||
s_edit = 1u;
|
||||
sv_poll_ensure(); /* 同 attach:整定页更要看到"落定"这一刻 */
|
||||
ui_w_setlist_reset(a_inst);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:两态统一按键转发(页面 key_cb 一行转过来)
|
||||
* 入口参数:a_inst - 已 attach 的实例 ui_w_setlist_t *
|
||||
* a_key - LVGL 键值 lv_key_t
|
||||
* 返回值:1 - 本页已消费(框架跳过内置);0 - 交回框架(↑↓ 移选、← 关页)
|
||||
* (键分发标志非错误通道,取 uint8_t——BSP err-type 约定)
|
||||
* 限定条件:uiTask 按键链内(框架 key_cb;描述符须 edit_capable=1 + key_intercept=1)
|
||||
* 函数说明:两道门都在本函数里,四页共用一条规矩(写在页面里就会长出不一致的四个版本):
|
||||
* 1. 只读视图(commit==NULL)直接交回框架:查看页绑了 key_cb 也**没有
|
||||
* 任何行为变化**——这是"绑 key_cb 不改像素"的依据,vparam~vplate
|
||||
* 四张黄金参考仍逐位一致就是它的机器证明
|
||||
* 2. 参数帧未就位时不接受写交互:值列此刻是 "--",拿 "--" 进编辑器等于
|
||||
* 把"还不知道"当成 0 让人改,改完还能投出去("无数据不许打 0"的同一条
|
||||
* 诚实性纪律,见 ui_w_setview_refresh 的注释)
|
||||
* ⚠ 判"就位"之前必须先 ui_w_setview_sync():s_par_ok 原本**只在绘制路径**
|
||||
* 里被置位,而"画过没有"是 LVGL 的事、与按键不同步。模拟器无头回归
|
||||
* 两次注键之间根本不重绘 → 开页后的第一个 ENTER 永远被这道门挡掉;
|
||||
* 真机上开页 10 ms 内会重绘,于是这条坑只在测试侧咬人("代码没错、
|
||||
* 时序有错"最难查)。sync 在帧没变时只是一次 seq 比较,多调零代价
|
||||
*/
|
||||
uint8_t ui_w_setview_key(ui_w_setlist_t *a_inst, lv_key_t a_key) {
|
||||
if ((a_inst == NULL) || (a_inst->commit == NULL)) {
|
||||
return 0u;
|
||||
}
|
||||
ui_w_setview_sync();
|
||||
if (ui_w_setview_ready() == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
return ui_w_setlist_key(a_inst, a_key);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:整定页标题右侧画"未标定"徽标(未标定闭锁的屏上表达)
|
||||
* 入口参数:a_layer - 绘制层 lv_layer_t *
|
||||
* a_clip - 重绘区 const lv_area_t *
|
||||
* 返回值:无
|
||||
* 限定条件:页面 draw_cb 内(框架内置区块之后);三条件全满足才落墨:整定视图 +
|
||||
* 帧已就位 + par.cal_block 非 0
|
||||
* 函数说明:几何为什么这样取:标题带只有 y=2..17 这一条,而"浅 V"右腿在 y7~17
|
||||
* 占掉 x≈147..158(ui_deco.c 的点集),所以徽标右边界取 141、左边界按
|
||||
* 实宽左推(3 个汉字 = 36 px → x≈105..141),既不压装饰也不压居中的标题
|
||||
* (最长标题"软压板/控制字" 3 字到 x≈98 为止)。字色取 UI_COL_FG,
|
||||
* 与标题同一极性;不做反显——反显在 1bpp 下表示"选中",徽标不是可选中项
|
||||
*/
|
||||
void ui_w_setview_draw_cal_badge(lv_layer_t *a_layer, const lv_area_t *a_clip) {
|
||||
lv_area_t area;
|
||||
lv_draw_label_dsc_t ld;
|
||||
const char *txt = SV_CAL_BADGE;
|
||||
size_t len;
|
||||
|
||||
if ((s_edit == 0u) || (s_par_ok == 0u) || (s_par.cal_block == 0u)) {
|
||||
return;
|
||||
}
|
||||
len = strlen(txt);
|
||||
area.x2 = UI_SCREEN_W - 1;
|
||||
area.y1 = UI_TITLE_Y;
|
||||
area.y2 = UI_TITLE_Y + UI_TEXT_BAND_H;
|
||||
/* 右对齐:先用与框架标题同一套测宽(lv_text_get_width + strlen,
|
||||
* 这里 length 必须传 strlen——该版本传 -1/0 要么死循环要么返回 0) */
|
||||
area.x1 = SV_BADGE_RIGHT_X
|
||||
- (int32_t)lv_text_get_width(txt, (uint32_t)len, LV_FONT_DEFAULT, 0);
|
||||
|
||||
if (!sv_areas_intersect(&area, a_clip)) {
|
||||
return;
|
||||
}
|
||||
lv_draw_label_dsc_init(&ld);
|
||||
ld.base.layer = a_layer;
|
||||
ld.text = txt;
|
||||
ld.text_static = 1u; /* Flash 字面量:不拷内存(与框架标题同法) */
|
||||
ld.color = UI_COL_FG;
|
||||
ld.ofs_x = 0;
|
||||
ld.ofs_y = 0;
|
||||
lv_draw_label(a_layer, &ld, &area);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
* 模块名称:UI 定值查看视图模型(ui_w_setview)
|
||||
* 模块功能:参数 / 定值 / 控制字 / 软压板**四张视图表**与它们的取数、越界反显——
|
||||
* 把"40 项定值怎么分组上屏、Q10 怎么换算成带小数位的显示值"收在一处,
|
||||
* 供四个查看页薄跳板(ui_page_view_*)共用;整定页(下一批)复用同一批表,
|
||||
* 只是再绑 set/commit/state 三个回调
|
||||
* 供四个查看页薄跳板(ui_page_view_*)共用;**同一批表**再经
|
||||
* ui_w_setview_attach_edit() 供整定页复用(道F task-2 已落:查看与整定
|
||||
* 只差绑不绑 set/commit/state)。行表只有一份,界内界外就不会两套口径
|
||||
* 写方向:整定页的"保存"经 Drivers/BSP/UI/ui_data.h 的 param_request 投递
|
||||
* (位图 + Q10 批量请求,队列深度 4),执行与落盘在 App 侧——本模块
|
||||
* 仍然零 App include:越界与否、写不写得进去都由快照层回读
|
||||
* 分层:本模块只读 Drivers/BSP/UI/ui_data.h 的参数快照,**不 include 任何 App 头**
|
||||
* (行表里的 id 是**裸数字** + 注释标注 PS_* 名,因为 prot_set_id_t 属 App 层)
|
||||
* ——id 与 App/algo/prot_set.h 枚举序的一致性由 test/host/ui_data 的主机单测
|
||||
@@ -17,6 +21,10 @@
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-15 批 3;规格 =
|
||||
* docs/UI设计/页面扩充详细方案.md §3.2.1~§3.2.3/§3.2.6)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-2:新增整定侧三件事——attach_edit
|
||||
* (改动位图叠层 + 只提交改动 id + 保存态锁存)、
|
||||
* key 转发(两态统一门)、标题"未标定"徽标;
|
||||
* 四张行表与查看路径逐字未变(像素零变化)
|
||||
*/
|
||||
#ifndef UI_W_SETVIEW_H
|
||||
#define UI_W_SETVIEW_H
|
||||
@@ -52,6 +60,58 @@ typedef enum {
|
||||
*/
|
||||
void ui_w_setview_attach(ui_w_setlist_t *a_inst, ui_w_setview_t a_view);
|
||||
|
||||
/*
|
||||
* 函数功能:把一个**整定**视图绑到定值表实例(行表与 attach 同一批,另绑
|
||||
* set/commit/state 三个回调 —— 组件据此画保存行并放开编辑)
|
||||
* 入口参数:a_inst - 页面持有的实例 ui_w_setlist_t *
|
||||
* a_view - 视图编号 ui_w_setview_t
|
||||
* 返回值:无
|
||||
* 限定条件:页面 init_cb 内调用,且**只在 ui_content_page_is_editable() 为真时**调
|
||||
* (只读权限走 attach,本函数根本不该被绑上)
|
||||
* 函数说明:整定与查看的唯一差别就是绑不绑这三个回调(§3.2.6 口径)。三个新增点:
|
||||
* 1. 编辑叠层:改动位图 s_wch + 工作值表 s_wq(工程量 × 10^dec),
|
||||
* get 优先读叠层、未改的行跟随参数帧 —— 用户改到一半退出页面即弃稿
|
||||
* (ESC 关页不投递,同 net_cfg);开页 attach 一律清稿,不带上次的改动
|
||||
* 2. 提交只投**改动位图内的 id**(绝不从显示值回提全表):Q10 存不下
|
||||
* 0.95 这类返回系数(真存 972/1024),按显示域往返会把用户没动的项
|
||||
* 顺手改掉——"越界值不落盘"与"改哪条只落哪条"两条判据都靠这个 mask
|
||||
* 3. 保存行显示态由 sv_state() 锁存(见 .c 内 s_disp 的注),
|
||||
* 闭锁 → LOCK(组件据此禁 ENTER),未标定 → 标题右侧徽标(下面那个)
|
||||
*/
|
||||
void ui_w_setview_attach_edit(ui_w_setlist_t *a_inst, ui_w_setview_t a_view);
|
||||
|
||||
/*
|
||||
* 函数功能:整定/查看两态统一的按键转发(页面 key_cb 一行转过来即可)
|
||||
* 入口参数:a_inst - 已 attach 的实例 ui_w_setlist_t *
|
||||
* a_key - LVGL 键值 lv_key_t
|
||||
* 返回值:1 - 本页已消费(框架跳过内置);0 - 交回框架(↑↓ 移选、← 关页)
|
||||
* (键分发标志非错误通道,取 uint8_t——BSP 公开 API 的 err-type 约定)
|
||||
* 限定条件:uiTask 按键链内(框架 key_cb)
|
||||
* 函数说明:门在这里而不是散在四个页面文件里,为的是"四页同一条规矩":
|
||||
* 1. 只读视图(未绑 commit)一律交回框架 —— 查看页按键行为逐字不变,
|
||||
* 这是"绑了 key_cb 也不会动像素"的依据(黄金用例 vparam~vplate 仍绿)
|
||||
* 2. 参数帧未就位时**不接受任何写交互**:值列此刻打的是 "--",
|
||||
* 拿 "--" 去 begin 编辑等于把"还不知道"当成 0 让人改,改完还能投出去
|
||||
*/
|
||||
uint8_t ui_w_setview_key(ui_w_setlist_t *a_inst, lv_key_t a_key);
|
||||
|
||||
/*
|
||||
* 函数功能:整定页标题右侧画"未标定"徽标(未标定闭锁的屏上表达)
|
||||
* 入口参数:a_layer - 绘制层 lv_layer_t *
|
||||
* a_clip - 重绘区 const lv_area_t *
|
||||
* 返回值:无
|
||||
* 限定条件:页面 draw_cb 内调用(框架内置区块之后);三个条件全满足才落墨:
|
||||
* 当前是整定视图(attach_edit)+ 参数帧已就位 + par.cal_block 非 0
|
||||
* 函数说明:为什么值得占标题带一个角:整定页的存在就是"让用户改保护定值",
|
||||
* 而产品构建下 UA/UN/IA 未标定 → 判据恒停走 → **改完也不会动作**。
|
||||
* 不写这件事,页面就是在暗示"改了就算数"(UI补齐.md §4 末段"保护闭锁
|
||||
* 必须可见";口径 docs/任务规划/数据可信.md §4.3)。
|
||||
* ⚠ 与保存行的 LOCK 是两件事:LOCK = 定值闭锁(写不进去、禁编辑),
|
||||
* 徽标 = 未标定(仍可改,只是判据停走)——合成一个词就等于互相冒充。
|
||||
* ⚠ 查看页不画(s_edit=0):那四页是既有黄金参考,像素零变化是硬约束
|
||||
*/
|
||||
void ui_w_setview_draw_cal_badge(lv_layer_t *a_layer, const lv_area_t *a_clip);
|
||||
|
||||
/*
|
||||
* 函数功能:按发布序号取参数帧快照(页面 refresh 前调用)
|
||||
* 入口参数:无
|
||||
|
||||
+166
-42
@@ -96,13 +96,15 @@ flowchart TB
|
||||
F --> SRC
|
||||
end
|
||||
subgraph BSP["Drivers/BSP/UI(表现层)"]
|
||||
D["ui_data.c/h<br/>快照存储 + 只读访问器"]
|
||||
P["ui_page_ac / ui_page_yx / ui_page_setlist ...<br/>只读快照 + 格式化 + 布局"]
|
||||
D["ui_data.c/h<br/>快照存储 + 只读访问器<br/>+ 定值写入队列与状态机"]
|
||||
P["ui_page_ac / ui_page_yx / ui_w_setview+ui_w_setlist ...<br/>只读快照 + 格式化 + 布局(整定页另加写投递)"]
|
||||
P --> D
|
||||
P -- "整定保存:ui_data_param_request(&req)" --> D
|
||||
end
|
||||
T["App/task/ui_task.c<br/>调度:每轮调 feed_tick()"] --> F
|
||||
T --> P
|
||||
F -- "ui_data_publish_rt/par(&帧)" --> D
|
||||
F -- "落定:ui_data_param_drain(&ops),执行侧由 App 注入" --> D
|
||||
```
|
||||
|
||||
**include 方向**(全部合法):
|
||||
@@ -116,13 +118,17 @@ flowchart TB
|
||||
|
||||
**`ui_data.c` 不 include 任何 App 头,也不 include 任何页面头**——它是叶子。
|
||||
|
||||
写路径没有给上表新增任何一行:它走的是**反向调用**(BSP 声明函数表、App 提供实现并按节拍
|
||||
把表传进来,见 §6.2),所以 include 方向不变、`layer-deps` 基线不变。这类"下层定义抽象、
|
||||
上层实现并注入"的第三条腿在 §7.2 的记录页 reader 上已经用过一次,两次都不新增 `ALLOW` 条目。
|
||||
|
||||
### 3.2 三个模块的职责边界
|
||||
|
||||
| 模块 | 位置 | 只做 | 绝不做 |
|
||||
|---|---|---|---|
|
||||
| `ui_data` | `Drivers/BSP/UI/` | 存最新一帧、给只读访问、记发布序号 | 取数、格式化、判断业务含义 |
|
||||
| `ui_data_feed` | `App/app/` | 按节拍从各 App 模块取数、组装、发布;变化检测 | 布局、字符串格式化、LVGL 调用 |
|
||||
| 页面 `ui_page_*` | `Drivers/BSP/UI/` | 读快照、按行表格式化、交给框架绘制 | 直接调 App、自己算业务量 |
|
||||
| `ui_data` | `Drivers/BSP/UI/` | 存最新一帧、给只读访问、记发布序号;**存定值写入请求队列、跑那台状态机**(越界与闭锁的判定按注入的 ops 问,自己一条业务规则都不持有) | 取数、格式化、判断业务含义、碰 `prot_set` 与文件系统 |
|
||||
| `ui_data_feed` | `App/app/` | 按节拍从各 App 模块取数、组装、发布;变化检测;**每轮 drain 一条定值请求并注入执行侧实现** | 布局、字符串格式化、LVGL 调用 |
|
||||
| 页面 `ui_page_*` | `Drivers/BSP/UI/` | 读快照、按行表格式化、交给框架绘制;整态把改动攒在**编辑叠层**里(改动位图 + 工作值,`ui_w_setview.c` 模块级一份——同屏只开一页,与各查看页的行缓存同一假设),只在按"保存"时投递一次 | 直接调 App、自己算业务量、自己判越界(行内上下限只是**镜像**,用于早拦一手;真源在 App,见 §9.4 一致性测试) |
|
||||
|
||||
---
|
||||
|
||||
@@ -188,21 +194,41 @@ typedef struct {
|
||||
|
||||
```c
|
||||
#define UI_DATA_SET_N 40u /* 定值项数(与 prot_set 枚举同序,见 §9.2 一致性测试) */
|
||||
#define UI_DATA_VER 2u /* 契约版本;字段只尾追加、追加即升版(§4.3) */
|
||||
|
||||
typedef struct {
|
||||
uint32_t seq;
|
||||
uint32_t checksum; /* 与 prot_set_checksum() 同值:页面据此判"参数是否变过" */
|
||||
uint8_t valid;
|
||||
uint8_t locked; /* 定值闭锁(校验和自检失败等) */
|
||||
uint8_t locked; /* 定值闭锁(校验和自检失败等)—— 定值**写不进去** */
|
||||
uint8_t bad_valid; /* 越界位图有效 */
|
||||
uint8_t rsv;
|
||||
uint64_t bad_mask; /* 40 位:置 1 = 该定值项越界 */
|
||||
int32_t q10[UI_DATA_SET_N]; /* Q10 原值;页面按行表的小数位换算显示 */
|
||||
} ui_data_par_t;
|
||||
uint8_t cal_block; /* v2 尾追加:未标定闭锁(判据停走)—— 定值**仍可改** */
|
||||
} ui_data_par_t; /* 当前 192 B(v2 前 184 B;尾字节由 8 字节对齐补齐) */
|
||||
```
|
||||
|
||||
**为什么存 Q10 而不是 float**:定值是**整表校验和覆盖的定点值**,快照存浮点会引入"显示值与校验和口径不一致"的二义性。Q10 → 显示值的小数位换算属表现层(§8)。
|
||||
|
||||
**`locked` 与 `cal_block` 是两个事实,屏上是两条通道、两个词,不得合并**(判据口径
|
||||
`docs/任务规划/数据可信.md` §4.3):
|
||||
|
||||
| 字段 | 真源 | 说的是什么事 | 屏上落点 | 还能改定值吗 |
|
||||
|---|---|---|---|---|
|
||||
| `locked` | `prot_set_locked()`(L5-1 校验和自检失配) | 这台装置现在**写不进去** | 整定页保存行值列 `LOCK`,且 ENTER 一律不消费(禁编辑) | ❌ |
|
||||
| `cal_block` | `adc_cal_flags()` 的 `ADC_CAL_F_VALID`(UA/UN/IA 三路,判据侧同源 `App/task/protect_task.c`) | 定值能改,但**改了也不会动作**(判据停走) | 整定页标题右侧「未标定」徽标 | ✅ |
|
||||
|
||||
合成一个词就等于互相冒充:现场看到 `LOCK` 会以为"改了白改"而不去标定,看到「未标定」
|
||||
会以为"这台装置写不进定值"而放弃正当的整定操作。两个字段的取数都在 `App/app/ui_data_feed.c`
|
||||
完成,页面零 App include(分层方向由 `scripts/check_layer_deps.py` 守住)。
|
||||
|
||||
> 【v2 落地注(2026-10-26,道F task-1)】本次只尾追加 `cal_block`,帧长 184 → 192 B;
|
||||
> 组装缓冲 `s_scr` 是 rt/par/info 的联合体,而最大者是 **rt(208 B)**,所以这笔追加
|
||||
> 实际**没多花一字节 RAM**——这条"谁兜住了谁"的关系由主机单测的尺寸断言钉住
|
||||
> (帧长一变,那处注释就是唯一会撒谎的地方)。升 `UI_DATA_VER` 是必须的:版本号是
|
||||
> 页面与写侧唯一的对齐凭据,追加字段不升版 = 老页面按新帧长 memcpy 时静默错位。
|
||||
|
||||
### 4.3 扩展规则
|
||||
|
||||
- **只允许尾部追加字段**,结构体开头保留 `seq/valid`。
|
||||
@@ -249,9 +275,9 @@ typedef struct {
|
||||
| 通道 | 节拍 | 理由 |
|
||||
|---|---|---|
|
||||
| `rt` | **200 ms**(内部计数分频,UI tick 10 ms × 20) | 与遥测链目标刷新率一致;人眼在 160×160 单色屏上感知不到更快 |
|
||||
| `par` | **变化触发**:每 200 ms 比一次 `prot_set_checksum()`,变则重发 | 避免每拍拷 160 B;定值变化是低频事件 |
|
||||
| `par` | **变化触发**:每轮比三个各自独立的变化源——`prot_set_checksum()`(定值改动)、`prot_set_locked()`(自检闭锁)、未标定判定(标定完成),**任一翻转就重发一帧** | 定值变化是低频事件,只盯校验和能省下每拍的 192 B 拷贝;但 `locked` 与 `cal_block` 都**不改动校验和**,只盯校验和会让保存行的 `LOCK` 与标题的「未标定」永远停在旧值上(真机现象:标定做完了徽标还挂着,直到某次定值改动被顺带刷掉——那是把两件不相干的事当同步点) |
|
||||
|
||||
`ui_data_feed_tick()` 每轮调用是**廉价**的:一次 tick 差值判断 + 一次 uint32 比较。真正的组装只在到期时发生。
|
||||
`ui_data_feed_tick()` 每轮调用是**廉价**的:一次 tick 差值判断 + 一次 uint32 比较 + 两次字节比较(`locked`/`cal_block` 上一帧值)。真正的组装只在到期或有源翻转时发生,无变化时零拷贝。
|
||||
|
||||
### 5.3 启动时序
|
||||
|
||||
@@ -301,36 +327,90 @@ static void pg_refresh(uint16_t row, char *name, char *value) {
|
||||
|
||||
### 6.2 请求 / 状态交接(复用 `net_cfg` 范式)
|
||||
|
||||
落地形态(道F task-1 落本层队列与状态机,task-2 落页面侧消费者,2026-10-26):
|
||||
|
||||
```
|
||||
ui_page_setlist(编辑态)
|
||||
│ 用户按"保存"
|
||||
ui_page_view_param / _set / _word / _plate(整定态:123 授权 → attach_edit 绑 set/commit/state)
|
||||
│ 选中"保存"行按 ENTER(组件 ui_w_setlist_key 先过两道门:可写权限 + state()!=LOCK)
|
||||
▼
|
||||
ui_data_param_request(&req) ← BSP 侧只投递,不执行
|
||||
│ (小环形队列,深度 4;满则返回 -1,页面提示"忙")
|
||||
sv_commit():req.mask = 改动位图;req.q10[] 只按 mask 置位的槽换算(显示值 → Q10)
|
||||
▼
|
||||
ui_data_feed_tick()(App 侧,uiTask 上下文)
|
||||
│ 取出请求 → 校验(调 prot_set 的只读接口预检)
|
||||
ui_data_param_request(&req) ← 本层只投递,不执行、不判越界、不碰文件系统
|
||||
│ 静态环形队列,深度 4(UI_DATA_PARAM_QD);满 → -1(页面显 ERR + 页脚"忙")
|
||||
│ 入队 = 168 B 整条拷入(.bss 4 × 168 = 672 B;零堆、零变长拷贝)
|
||||
▼
|
||||
prot_set_set_sealed() / prot_set_set() + prot_set_seal()
|
||||
│ 成功 → prot_set_dirty_mark() → defaultTask 落盘
|
||||
ui_data_feed_tick()(App 侧,uiTask 上下文;先 drain 再比校验和,顺序即判据)
|
||||
│ ui_data_param_drain(&s_param_ops):每轮最多落定一条
|
||||
│ ② ops->locked() == 1 → ERR_LOCKED,**本条丢弃**(不排队等解锁)
|
||||
│ ③ ops->check() 按 mask 逐项整批预检,任一项越界 → ERR_RANGE + err_mask,一字不写
|
||||
│ ④ 预检全过再逐项 ops->apply() → prot_set_set_sealed()(只写 mask 内的 id)→ OK
|
||||
│ 值真的变了 → 同轮重发 par 帧(页面按 seq 失效缓存)
|
||||
▼
|
||||
ui_data_param_state(OK / ERR_RANGE / ERR_LOCKED / ERR_BUSY)
|
||||
│
|
||||
▼
|
||||
ui_page_setlist 显示"保存成功 / 越界:Ⅲ段定值"
|
||||
ui_data_param_state() / ui_data_param_err_mask()(保存行显词 + 越界行反显)
|
||||
```
|
||||
|
||||
**这套"UI 只投递、属主任务执行、状态回读"的结构,与 `net_cfg` 的 `net_cfg_save_request` / `net_cfg_save_state` 完全同构**(`Drivers/BSP/NETCFG/net_cfg.h`),不引入新范式——这是"低耦合"的另一个含义:**项目里同类问题只有一种解法**。
|
||||
四个公开函数(声明 `Drivers/BSP/UI/ui_data.h`、实现 `ui_data.c`,全在本层,零 App include):
|
||||
|
||||
| 函数 | 作用 | 返回值口径 |
|
||||
|---|---|---|
|
||||
| `ui_data_param_request()` | 投递一条请求(页面"保存"的唯一出口) | `0` 已入队;**`mask=0` 的空请求也返 0**,但不占槽位、直接落终态 OK(一个都没改就按保存是正常操作,报 ERR 会让现场以为装置有问题);`-1` 队列满;`-2` 参数无效(NULL,或 `mask` 含 `UI_DATA_SET_N` 以上的位——调用方的编程错误不该落成屏上的"越界") |
|
||||
| `ui_data_param_state()` | 回读写路径状态 | `UI_DATA_PARAM_ST_{IDLE,WAIT,OK,ERR_RANGE,ERR_LOCKED,ERR_BUSY}` |
|
||||
| `ui_data_param_err_mask()` | 最近一条**被本次预检挡下**的项位图 | bit i = 1 → 第 i 项越界;最近终态不是 `ERR_RANGE` 时恒 0 |
|
||||
| `ui_data_param_drain()` | 落定一条(App 装配节拍入口) | `1` 处理并提交了一条;`0` 队列空(状态不动);`-1` ops 或其函数指针为 NULL |
|
||||
|
||||
**"算不算越界"与"现在能不能写"都是 App 侧事实,本层一份都不复制**:执行能力经
|
||||
`ui_data_param_ops_t`(`locked` / `check` / `apply` 三个函数指针)由 `App/app/ui_data_feed.c`
|
||||
**每次调 drain 时注入**,本层不保存副本。原因有两条,都不含糊:① 分层方向硬约束——
|
||||
`Drivers/BSP/**` 不得 include `App/**`(`scripts/check_layer_deps.py` 拦提交),而上下限真源
|
||||
在 `App/algo/prot_set.c` 的 `g_prot_set_meta`;② 注入表**刻意不含任何范围数值**,多传一份
|
||||
上下限就是第二真源、就会漂。同 §7.2 记录页的注入式 reader 一条路子,额外的好处是主机单测
|
||||
可以把三个指针换成桩件,于是**队列与状态机的生产代码**(不是副本)能在 PC 上把"满队列 /
|
||||
越界不改值 / 闭锁拒写 / 正常往返"跑全(`test/host/ui_data/`)。
|
||||
|
||||
**请求形状 = 「位图 + 定长 40 槽」(168 B)而不是「项数 + 变长 id 数组」**:表现层的编辑叠层
|
||||
本就是一张改动位图 + 一张工作值表,本形状与它逐位同构 → 提交侧一次赋值、执行侧逐项按位取,
|
||||
两侧都不必维护第三种表示;定长还让队列退化成 O(1) 静态数组。边界与代价:`mask` 只有 64 位
|
||||
→ 定值项数硬上限 64(`ui_data.c` 有编译期守卫,破 64 是编译失败而不是静默丢项);队列 672 B
|
||||
+ 控制字 16 B 是买"整批原子 + 零变长 + 零堆"的价钱(`docs/内存管理/RAM使用分析与优化方案.md`)。
|
||||
|
||||
> ⚠ **提交侧必须只提交 `mask` 内置位的 id,绝不"从显示值回提全表"**:Q10 存不下 0.95 这类
|
||||
> 返回系数(实际存 972/1024),按显示域往返会把用户没动的项顺手改掉。判据"越界值不落盘"与
|
||||
> "改哪条只落哪条"都建在这条上(`docs/任务规划/UI补齐.md` §2 第 2 行)。
|
||||
> 换算的往返精度因此也**不需要**逐位可逆:没动的行根本不进换算函数。
|
||||
|
||||
**这套结构与 `net_cfg` 的 `net_cfg_save_request` / `net_cfg_save_state` 完全同构**
|
||||
(`Drivers/BSP/NETCFG/net_cfg.h`),不引入新范式——这是"低耦合"的另一个含义:
|
||||
**项目里同类问题只有一种解法**。
|
||||
|
||||
### 6.3 失败语义
|
||||
|
||||
| 返回 | 含义 | 页面表现 |
|
||||
|---|---|---|
|
||||
| `OK` | 已封存并置脏 | 行值刷新,页脚短暂"已保存" |
|
||||
| `ERR_RANGE` | 某项越界 | 高亮越界行(用 `bad_mask`),页脚"越界" |
|
||||
| `ERR_LOCKED` | 定值闭锁中(自检失败) | 页脚"定值闭锁",禁止编辑 |
|
||||
| `ERR_BUSY` | 请求队列满 | 页脚"忙,请重试" |
|
||||
| `ERR_TIMEOUT` | 3 s 未取走 | 页脚"无响应" |
|
||||
落定结果只有 5 个值(`UI_DATA_PARAM_ST_*`),页面侧的**词与位置**由整定视图统一映射,
|
||||
四张整定页共用一套(组件按 `state()` 回调给的值画保存行值列,页面不参与解释):
|
||||
|
||||
| 状态 | 含义 | 保存行值列 | 页面附带动作 | 产出方 |
|
||||
|---|---|---|---|---|
|
||||
| `IDLE` | 本次打开页面从未投递过 | `ENTER` | — | 本层初值 |
|
||||
| `WAIT` | 已投递、队列里还有请求在路上 | `WAIT` | 编辑叠层**保留**(不清稿) | `request` 受理 / `drain` 未排空 |
|
||||
| `OK` | 已逐项写入并置脏 | `OK` | 按 `commit` 位图清掉叠层,让位给新帧 | `drain` 第④步 |
|
||||
| `ERR_RANGE` | 整批预检有任一项越界 → **一字未写** | `ERR` | 用 `ui_data_param_err_mask()` 反显越界行 | `drain` 第③步 |
|
||||
| `ERR_LOCKED` | 投递时定值闭锁 → **本条丢弃** | `LOCK` | 反显同 `ERR_RANGE`;`drain` 不回滚(`prot_set` 无回滚接口,谎称"整批未写"比如实报更坏) | `drain` 第②步 |
|
||||
| `ERR_BUSY` | 投递被拒:队列满 | `ERR` | 页脚"忙,请重试";**在队请求一条不动**,用户直接再按一次保存 | `request` 返 -1 |
|
||||
|
||||
三条实现侧口径,都是原稿没写死的:
|
||||
|
||||
1. **`LOCK` 还有一个与状态机无关的来源**:帧里 `par.locked == 1` 时保存行**直接显 `LOCK`
|
||||
并禁 ENTER**(优先级高于历史结果)。闭锁是持续状态,不该只在"某次投递被拒"那一帧才说一次;
|
||||
禁编辑必须落在键路上,只靠屏上词会让用户白改一轮。
|
||||
2. **`ERR_TIMEOUT`(原稿的"3 s 未取走")本层不产出**——uiTask 每 10 ms 就 drain 一次,
|
||||
真出现"永不落定"是 App 侧没接线的装配缺陷,不是运行期状态。页面若要兜底,可自己按投递
|
||||
时刻计时(属页面本地可选项,当前没做:没有消费者就别先加一条无人走的分支)。
|
||||
3. **状态是全局单值,页面必须锁存显示**:`ui_data_param_state()` 落定后一直停在那个值上,
|
||||
直读会让用户移动一下光标就看到别的页留下的旧结果。整定视图把它锁存成"本页投过一次
|
||||
才显示那次的答案、开页复位成 `ENTER`"(`ui_w_setview.c` 的 `s_disp`)。
|
||||
|
||||
> 词表刻意用 ASCII(`ENTER`/`WAIT`/`OK`/`ERR`/`LOCK`)与 `net_cfg` 保存行同排;
|
||||
> 中文「未标定」徽标是**另一件事**(`cal_block`),见 §4.2 的两源两通道表,不要为了
|
||||
> 省一个词把它并进 `LOCK`。
|
||||
|
||||
---
|
||||
|
||||
@@ -446,6 +526,18 @@ typedef struct {
|
||||
> 2. 固定帧里的定值**不手抄 40 个数字**,而是把 `App/algo/prot_set.c`(零平台依赖的纯算法,双端共享核心)链进模拟器、`prot_set_init()` 后取出厂默认——抄表等于造第二真相(同 `scripts/check_font_charset.py` 的"单一规则源"口径);
|
||||
> 3. `build_date` 用**固定串**而不是 `__DATE__`:否则跨天重建会把装置信息页的黄金参考刷掉,而黄金参考是判据本身。
|
||||
> 4. 原稿设的 `--live` 合成正弦模式**尚未实现**(当前无消费者,等动态刷新真的需要人眼观察时再加)。
|
||||
> 5. **定值写路径在模拟器侧有一份同构的执行实现**(`sim_param_locked/check/apply` +
|
||||
> `sim_param_pump()`):真机那段在 `App/app/ui_data_feed.c` 的 tick 里,而模拟器不链接装配器,
|
||||
> 所以在 `main.c` 里镜像一份——`check()` 用的仍是同一份 `g_prot_set_meta` 真源(链进来的
|
||||
> `App/algo/prot_set.c`),因此"模拟器里越界被拒"与"真机上越界被拒"是同一个判定,不是两份规矩。
|
||||
> 固定帧的 `cal_block` 显式填 1:**真机现在就是**未标定态(`rt.eng` 也按同一条口径钉在引脚档),
|
||||
> 屏上的话必须与本机一致,模拟器才配当像素基准。
|
||||
> 6. 落定与"页面自己发现数据变了"的分工:`sim_param_pump()` 在无头模式排在注键之后、截图之前
|
||||
> 同步排空(真机由每 10 ms 的 tick 排);整定视图另有一条 100 ms 的 LVGL 轮询定时器,
|
||||
> 负责把"主站写进来的新值 / 标定完成 / 落定但不改校验和的 ERR"推上屏。**这条定时器在无头
|
||||
> 回归里一次都不会触发**——`run_headless()` 严禁调 `lv_timer_handler()`(I1+FULL 全屏刷新会
|
||||
> 进渲染死循环),所以黄金像素与它无关;两次连跑逐字节一致即为证据(`simulator/README.md`
|
||||
> 的同条注)。窗口模式才用它。
|
||||
|
||||
### 9.3 与"虚拟注入"的关系
|
||||
|
||||
@@ -469,18 +561,29 @@ typedef struct {
|
||||
|
||||
## 10. 内存与性能
|
||||
|
||||
| 项 | 量 | 说明 |
|
||||
| 项 | 量(`.map` 实测) | 说明 |
|
||||
|---|---|---|
|
||||
| `ui_data` rt 快照 | ≈ 132 B | 9×8 + 位图 + 时钟串 |
|
||||
| `ui_data` par 快照 | ≈ 180 B | 40×4 + 头部 |
|
||||
| 状态/队列 | ≈ 60 B | 请求环 4 条 + 状态字 |
|
||||
| **合计(RW_IRAM1)** | **≈ 380 B** | 当前 RW_IRAM1 余 57,504 B → 无压力 |
|
||||
| 页面行缓存 | 每页 ≈ 132 B(仅数据页) | 页面私有静态,随页数线性;若 6 个数据页 ≈ 800 B |
|
||||
| LVGL 池 | **0** | 快照不进池(不是 LVGL 对象) |
|
||||
| 每 200 ms 拷贝 | ≈ 132 B | 一次 memcpy,微秒级 |
|
||||
| 每帧(10 ms) | 0 次拷贝(比 seq 后只在变化时拷) | 见 §5.4 |
|
||||
| `rt` 帧 | 208 B | 遥测 9 路 + 相量槽带 + 位图 + 时钟串;三帧串行组装用的 `s_scr` 是联合体,**取最大者 = rt** |
|
||||
| `par` 帧 | 192 B | 40×4 Q10 + 头部 + `cal_block`(v2);追加这 8 B 由 rt 兜住,**净增 0**(尺寸关系由主机单测钉住,见 §4.2 落地注) |
|
||||
| `info` 帧 | 随字符串字段定长 | 与 rt 同节拍发布,同样落在联合体里,不额外占 |
|
||||
| `ui_data.o` 全部静态 | `.bss` 1,168 B | 含三帧存储、`s_scr` 联合体、**定值请求队列 4×168 = 672 B + 控制字 16 B** |
|
||||
| `ui_w_setview.o` 静态 | `.bss` 352 B / `.data` 48 B | 四视图行缓存 + **整定编辑叠层 190 B**(`int32_t[40]` + 三张位图 + 锁存态) |
|
||||
| 页面行缓存 | 每页 ≈ 52 B(`ui_page_view_*.o` 各 `.bss` 52 B) | 定值相关页共用一份视图缓存(同屏只开一页),所以四张整定页**没有**再各存一份 192 B |
|
||||
| LVGL 池 | 快照 0 B;轮询定时器一只 | 快照不进池(不是 LVGL 对象);定时器是 LVGL 池对象,只建一次不删 |
|
||||
| 每 200 ms 拷贝 | 一帧(≤208 B),且只在变化时 | 一次 memcpy,微秒级 |
|
||||
| 每轮(10 ms) | 0 次拷贝 | 比 seq / 比三个变化源;见 §5.2、§5.4 |
|
||||
|
||||
**结论**:代价是"几百字节 + 每 200 ms 一次 memcpy",换来的是整个 UI 层的数据可及性。
|
||||
栈侧(同一份 `.map` 的调用图,改前改后各跑一次 `scripts/stack_report.py`):uiTask 静态最坏链
|
||||
**1336 B / 配置 16 KB(余 15,048 B,91%)在接入写路径前后逐字节不变**——新链路自身都不深
|
||||
(`sv_commit` 帧 192 B 含 168 B 请求体、链深 208 B;「未标定」徽标帧 160 B、链深 536 B;按键链
|
||||
`pg_key` 起算 480 B),最坏链仍是 LVGL 建树那条,**没有换人**。这是"页面 commit 会在 uiTask 栈上
|
||||
放 168 B 局部量"这类担心的实测答案,也是本表值得写实测数而不写"约几百字节"的理由。
|
||||
|
||||
**结论**:代价 = 约 1.5 KB 静态 + 每 200 ms 一次 memcpy,换来整个 UI 层的数据可及性与一条
|
||||
可测的写路径。区域水位(RW_IRAM1 / RW_CCM / Flash)**不在本文复制**——那是受管快照,
|
||||
由 `scripts/ram_baseline.py` 从每次 `@build` 的 `.map` 生成,第二处写死就是一份会过期的
|
||||
真源(本仓已有一条门禁在钉这个,口径见 `docs/内存管理/RAM使用分析与优化方案.md` §2.1 与
|
||||
`docs/内存管理/RAM预算台账.md`)。本次新增约 1.5 KB 静态 + 一条 LVGL 池定时器,量级无压力。
|
||||
|
||||
---
|
||||
|
||||
@@ -492,7 +595,7 @@ typedef struct {
|
||||
| **步骤2** | `App/app/ui_data_feed.c` + `ui_task.c` 接线(每轮调 tick) | 串口打印首帧(临时锚点);`@build` 0/0 | 步骤1 |
|
||||
| **步骤3** | 交流量 / 直流量回炉(接快照 + 行表加 `src/dot/unit`) | 模拟器固定帧 golden 更新;台架看真实码值变化 | 步骤2 |
|
||||
| **步骤4** | 遥信量 + 定值/控制字/软压板查看(4 页) | 主站改定值 → 页面 200 ms 内变 | 步骤3 |
|
||||
| **步骤5** | 写路径(请求/状态)+ 整定 4 页 | 就地改定值 → 保存 → 掉电重启 → 值保持 | 步骤4 |
|
||||
| **步骤5** | 写路径(请求/状态)+ 整定 4 页 | 就地改定值 → 保存 → 掉电重启 → 值保持 | 步骤4。**已落**(道F task-1 落本层队列与状态机、task-2 落整定 4 页与模拟器侧,2026-10-26);「掉电重启值保持」只能上板收口,见 §12 判据 6 |
|
||||
| **步骤6** | 记录 reader 注入 + 删 3 条 layer-deps 基线 | `check_layer_deps.py` 基线由 6 → 3 | 可与步骤3 并行 |
|
||||
|
||||
**步骤1~步骤2 是关键路径**:它们不产出任何用户可见效果,但决定后面所有页面是否可信。**不要跳过步骤1 直接改页面**——那会退化成"页面里塞假数据 2.0"。
|
||||
@@ -533,6 +636,26 @@ typedef struct {
|
||||
> 「当前是否已过密码」既不真实也不该被推断。判据侧的过法只有一套(查看密码 = 出厂 000 /
|
||||
> 修改密码 = 123,键序与黄金回归的网口设置编辑态完全一致),口径与纪律见
|
||||
> `docs/UI设计/UI_自动化测试流程.md` §7.5;本节只登记这条事实,避免两份文档各说各话。
|
||||
>
|
||||
> 【落地注·六(写路径消费者已上屏,2026-10-26,道F task-2)】整定 4 页(参数 / 定值 /
|
||||
> 控制字 / 软压板)绑到与查看列**同一批行表、同一个 `ui_w_setlist` 组件**:同一份页面
|
||||
> 描述符按 `ui_content_page_is_editable()` 分派只读 / 可写两态,两态的差别只有绑不绑
|
||||
> `set/commit/state` 三个回调。于是本节契约里原本只是示意图的那条链路第一次真的被按到:
|
||||
> **越界值不落盘**(提交只带改动位图)、**闭锁禁编辑**(`state()` 回 LOCK 或帧里
|
||||
> `locked=1` 就直接挡住 ENTER,而不是只换个屏上词)、**落定要自己上屏**(`ERR_RANGE` 与
|
||||
> `ERR_LOCKED` 都不改校验和,页面收不到新帧,靠的是整定视图那条 100 ms 轮询,见 §9.2 注 6)。
|
||||
>
|
||||
> 这一批还暴露了两条「只有第一个消费者才会暴露」的缺陷,都已修(为什么必现写在
|
||||
> `docs/UI设计/页面扩充详细方案.md` §3.2 落地注):二态行 ENTER 不回绕(写出 opts 之外的
|
||||
> 下标:屏上看着没变、叠层里已是越界值,保存必被预检挡回);「参数帧已就位」原先只在
|
||||
> **绘制路径**里被置位,而按键门读它 → 无头回归里开页后的第一个 ENTER 永远被挡(真机开页
|
||||
> 10 ms 内就重绘,症状只在测试侧出现)。第二条的修法是把「数据就位」与「画过没有」解耦:
|
||||
> 判就位之前先按 seq 做一次 sync(帧没变时只是一次 uint32 比较)。
|
||||
>
|
||||
> 另记一条同批暴露、与本契约无关的事实:菜单树重跑时 `ui_menu_tab.h` 有一行注释
|
||||
> 跟着生成器的模板文字变了 —— 说明 HEAD 上「生成器模板」与「已提交生成物」本就不同步
|
||||
> (改模板那笔没重跑生成器)。生成物以生成器输出为准即可,但这条滞后该补一条门禁,
|
||||
> 已登记为待办「生成器幂等性检查」。
|
||||
|
||||
---
|
||||
|
||||
@@ -545,8 +668,9 @@ typedef struct {
|
||||
| 3 | 交流量页显示真实相量(非写死值) | 台架:注入已知信号 → 页面值随信号变 |
|
||||
| 4 | 未标定时页面明确标识(不把码值当伏特) | 目检 + 文本断言(`eng=0` 时带标记) |
|
||||
| 5 | 首帧前显示 `--` 而非 `0.000` | 模拟器:不 publish 直接开页,断言文本 |
|
||||
| 6 | 定值整定全链闭环 | 就地改 → 保存 → 重启 → 值保持 + 校验和自检通过 + 主站 C_RD 一致 |
|
||||
| 7 | 像素回归稳定 | `ui-golden` 连续两次运行零差异 |
|
||||
| 6 | 定值整定全链闭环 | 就地改 → 保存 → 重启 → 值保持 + 校验和自检通过 + 主站 C_RD 一致。**半收口(2026-10-26)**:模拟器侧「改 → 保存 → OK → 回首屏新值已在帧里」已由像素 + 文本断言覆盖(黄金用例 `sparam_save` / `sparam_landed`,越界侧 `sparam_oob`);**重启保持与主站 C_RD 一致只能上板**——模拟器没有 NAND,`apply()` 到 `prot_set_set_sealed()` 就是终点,这条差距已登记在 `docs/测试说明/模拟器差距清单.md`;另注意 40 项里有 13 项 `comm_addr=0`(纯本地项),主站侧根本读不到,不能拿 C_RD 当它们的判据 |
|
||||
| 7 | 像素回归稳定 | `ui-golden` 连续两次运行零差异。**已验(2026-10-26)**:27 页连跑两次逐字节一致(含整定 9 态);轮询定时器不影响——无头模式不调 `lv_timer_handler()`(§9.2 注 6) |
|
||||
| 9 | 「写不进去」与「改了不动作」两条事实互不冒充 | 反向文本断言(`check_content_pages.py` 的 `ABSENT`:查看页与只读态不得出现「未标定」徽标与「保存」行)+ 台架:制造一次 `locked`(改坏校验和档)目检保存行 `LOCK` 且 ENTER 无反应 |
|
||||
| 8 | 主机单测覆盖数据模块 | `test/host/ui_data` 全绿并挂 pre-commit 钩子 |
|
||||
|
||||
---
|
||||
|
||||
+53
-10
@@ -203,6 +203,12 @@ V0.1 **无直流采样通道**(`ADC_SEM` 只有 8 路电气 + 温度),点
|
||||
|
||||
8 个叶子(整定 4 + 查看 4)共用**一份** `ui_w_setlist` 组件 + **4 张视图表**。查看与整定的差别只是 `edit_capable` 与权限。
|
||||
|
||||
> 【2026-10-26 落地】查看 4 页(道E task-15 批 3)与**整定 4 页(道F task-2)都已落**:
|
||||
八个叶子绑的是**同一批模块**(`view_param` / `view_set` / `view_word` / `view_plate`),
|
||||
两态共用**同一份页面描述符**——差别不写在描述符里,而在 `pg_init` 按 `ui_content_page_is_editable()`
|
||||
选 `ui_w_setview_attach()`(只读)还是 `ui_w_setview_attach_edit()`(可写)。
|
||||
下方 §3.2.6 那张按两种描述符写的表已照此更正。
|
||||
|
||||
#### 3.2.1 40 项四视图划分
|
||||
|
||||
| 视图 | 项数 | 判据 | 页数(8 行/页) |
|
||||
@@ -294,23 +300,48 @@ V0.1 **无直流采样通道**(`ADC_SEM` 只有 8 路电气 + 温度),点
|
||||
|
||||
| 项 | 查看(4 页) | 整定(4 页) |
|
||||
|---|---|---|
|
||||
| 权限 | 无密码 → `UI_PERM_VIEW` | 密码 → 按所输密码授 `VIEW`/`MODIFY` |
|
||||
| `edit_capable` | 0 | 1 |
|
||||
| 页脚 | 只显示页码 | 额外一行"保存"(第 8 行固定为保存行) |
|
||||
| 提交 | — | 整表 `prot_set_set*` + `prot_set_seal()` + 置脏,走请求/状态交接 |
|
||||
| 权限 | 无密码 → `UI_PERM_VIEW` | 密码 → 按所输密码授 `VIEW`/`MODIFY`(123 = 修改、000 = 查看) |
|
||||
| `edit_capable` | 描述符置 1,但 `is_editable()` 为假 → 走只读 attach | 同一份描述符,`is_editable()` 为真 → 走 `attach_edit` |
|
||||
| 组件回调 | `commit = state = NULL` → **不画保存行**、ENTER 不消费(绑了 `key_cb` 也等价于没绑) | 绑 `set/commit/state` → 画保存行、放开编辑 |
|
||||
| 页脚 | 只显示页码 | 额外一行"保存"(挂在**末行之后**,不是固定第 8 行) |
|
||||
| 提交 | — | 只提交**改动位图**内的 id(一次封存、一次校验和),经 `ui_data_param_request()` 投递,App 侧 `drain` 落定 → 见 `docs/UI设计/UI_数据供给设计说明.md` §6.2 |
|
||||
| 越界反显来源 | `par.bad_mask`(装载侧,且 `bad_valid=1` 才参与) | 同上 **+** `ui_data_param_err_mask()`(本次提交被整批预检挡下的项) |
|
||||
| 闭锁 | 无写通路,不需要表达 | `par.locked=1` 或投递回 `ERR_LOCKED` → 保存行显 `LOCK` **且 ENTER 一律不消费**(禁编辑,不是只换个词) |
|
||||
| 未标定 | 不画(查看页像素恒零变化) | 标题右侧「未标定」徽标(`par.cal_block`);与 `LOCK` 是两件事,两源两通道两词 |
|
||||
|
||||
> 保存行占掉一个可见行 → 整定页行数 = 视图项数 + 1,分页随之增加:参数 9 行 2 页、定值 19 行 3 页、控制字 5 行 1 页、软压板 11 行 2 页(合计 8 页)。这是"保存必须整表封签"带来的必然代价,不是设计缺陷。
|
||||
|
||||
> 【2026-10-26 落地实测 + 两条必修缺陷】上述分页与黄金参考逐页对上(`sparam_p2` /
|
||||
> `sset` / `sword_cycle` / `splate` 四条用例的页脚分别是
|
||||
> 「共 2 页 / 共 3 页 / 共 1 页 / 共 2 页」)。**「整表封签」指的是一次保存只封存一次、
|
||||
> 只重算一次校验和,不是「把 40 项全写一遍」**——实际只写用户改过的项:Q10 存不下 0.95 这类
|
||||
> 返回系数(真存 972/1024),从显示值回提全表会把用户没动的项顺手改掉。
|
||||
>
|
||||
> 这一批暴露了两条「只有第一个消费者才会暴露」的缺陷(都已修;写路径口径见
|
||||
> `docs/UI设计/UI_数据供给设计说明.md` §6.2/§6.3):
|
||||
> ① `ui_w_setlist` 的二态行 ENTER **不回绕**——落在最后一项时写出 opts 之外的
|
||||
> 下标,屏上看着「按了没反应」,叠层里却已是越界值,随保存投出去必被预检判 `ERR_RANGE`;
|
||||
> 控制字 4 行与软压板 10 行的出厂值**全是**最后一项(跳闸 / 投入),所以是必现而不是边角。
|
||||
> 本方案落地之前,没有任何页面按过这条键(查看页 `commit=NULL` 走不到)。
|
||||
> ② 编辑态的「参数帧已就位」原先只在**绘制路径**里被置位,而按键门读它 → 无头回归里开页后
|
||||
> 第一个 ENTER 永远被挡(真机开页 10 ms 内就重绘,症状只在测试侧出现)。修法是让
|
||||
> 「数据就位」与「画过没有」解耦:判就位之前先按 seq 做一次 sync(帧未变时只是一次比较)。
|
||||
>
|
||||
> 另记一条生成器侧的事实(不在本道修,已登记为待办「生成器幂等性检查」):菜单树重跑时
|
||||
> `ui_menu_tab.h` 有一行注释跟着变了 —— 说明 HEAD 上生成器的模板文字与已提交的
|
||||
> 生成物本来就**不同步**(改模板那笔没重跑生成器)。生成物是产物,以生成器输出为准即可,
|
||||
> 但这条滞后不补,任何人都可能在不知情的情况下把它 revert 掉。
|
||||
#### 3.2.7 编辑交互(复用网口设置页的模型)
|
||||
|
||||
| 键 | 正常模式 | 编辑模式 |
|
||||
|---|---|---|
|
||||
| ↑↓ | 移选(框架内置) | 当前位 +1 / −1(按项步进) |
|
||||
| ←→ | ← 返回上级(框架) | 移动数字位光标 |
|
||||
| ENTER | 进编辑 / 触发保存行 | 确认本项(写 RAM 副本) |
|
||||
| ENTER | 进编辑 / 触发保存行(**闭锁态 `LOCK` 下一律不消费**) | 确认本项(越界则拒退并打提示,值不进叠层) |
|
||||
| ESC | 关页(框架恒可拦截除外) | 关页(未保存的调整留在 RAM,不落盘) |
|
||||
|
||||
- 权限不足时 ENTER 一律忽略(`ui_content_page_is_editable()` 判)。
|
||||
- 权限不足时 ENTER 一律忽略(`ui_content_page_is_editable()` 判);**权限够但装置写不进去时同样忽略**(`state()` 回 `LOCK` 或帧里 `locked=1`)——前者是「这人不能写」,后者是「这台装置现在写不进」,两件事都必须落在键路上,只改屏上词等于让用户白改一轮。
|
||||
- 编辑中「←」是**移动光标**而不是关页:描述符要置 `key_intercept = 1u`,框架才先把 LEFT 交页面(`simulator/tests/` 的 `sparam_edit` / `sparam_oob` 两条用例走的就是这条路)。
|
||||
- 越界项在保存失败后由 `bad_mask` 高亮(`draw_cb` 画反显)。
|
||||
- 规模:`ui_w_setlist` **M**(约 250 行),`ui_w_editor` **M**(约 180 行)。
|
||||
|
||||
@@ -411,6 +442,12 @@ int run_par_set_deadband(uint8_t yc_idx, float v); /* 范围校验 */
|
||||
| 操作记录 | 定值保存成功 / 强制复归 / 时钟设置 / 清除记录(4 处) | 动作成功后写一条 |
|
||||
| 遥控记录 | `iec104_pts_yk_exec()` 返回处 | 每次执行结果(成功/失败/选择态) |
|
||||
|
||||
> **本表只是页面侧的需求清单**(记录页要消费哪几类),**不是记录层的登记表**:
|
||||
> 5 个 ID 的定名、记录格式、条数与字节上限、写入时机、以及是否真的落地,一律以
|
||||
> `App/log` 侧为准,权威口径 `docs/子系统设计/运行与自检记录设计说明.md`;
|
||||
> 实施进度由记录侧任务回报后回填本文(本道不代记,免得同一件事在两份文档里各说一遍——
|
||||
> 同一事实只在一处详述,见 `docs/文档规范.md`)。
|
||||
|
||||
**`ui_w_soe` 要点**:
|
||||
|
||||
```c
|
||||
@@ -505,6 +542,12 @@ void ui_w_soe_refresh(ui_w_soe_t *inst, uint16_t row, char *name, char *valu
|
||||
|
||||
**本批不改菜单树**——27 个空叶子全在现有 42 节点里,`#page=` 注记逐个补上即可(改 `ui_menu_tree.txt` 后重跑 `python tools/menu_gen/gen_menu.py`)。
|
||||
|
||||
> 【2026-10-26 已改一笔】整定 4 叶(参数 / 定值 / 控制字 / 软压板)已补 `#page=`,
|
||||
> 绑的是与查看列**同一批模块**(一个模块被两个叶子绑定是生成器允许的,`validate_pages()` 按
|
||||
> 集合去重判孤儿页)。重跑生成器后 `UI_PAGE_MAP_COUNT` 12 → 16,
|
||||
> `uvprojx` 与 `CMakeLists.txt` 未变(没新增文件)。
|
||||
> 生成器幂等性的一条滞后见 §3.2.6 落地注末段。
|
||||
|
||||
**唯一建议改动**(可选,需产品拍板):补参考项目的"厂家设置"分支。参考侧该分支有 18 个叶子(元件配置、恢复默认 ×5、交流显示、装置调试 ×8、版本信息)。若采纳,精简为 3 个叶子 + 2 个分组:
|
||||
|
||||
| 建议叶子 | 理由 |
|
||||
@@ -579,10 +622,10 @@ SNTP 客户端 ──────→ 批4 SNTP设置
|
||||
| 2 | 3 个静态页全部接真数据 | 改变输入信号 → 页面值变 |
|
||||
| 3 | 未标定态可见 | `eng=0` 时交流量第 7 行显示"未标定" |
|
||||
| 4 | 无源点显示 `--` | 遥信量无效点、直流量直流通道 |
|
||||
| 5 | 定值整定闭环 | 改 → 保存 → 重启 → 值保持 + 校验和自检通过 + 主站 C_RD 一致 |
|
||||
| 6 | 分层门禁无新增 | `check_layer_deps.py` 退出码 0,基线 6 → 3 |
|
||||
| 7 | 像素回归稳定 | `ui-golden` 连续两次零差异 |
|
||||
| 8 | 每个新页有 golden 参考 | `simulator/tests/ref/` 新增对应 `.bin` |
|
||||
| 5 | 定值整定闭环 | 改 → 保存 → 重启 → 值保持 + 校验和自检通过 + 主站 C_RD 一致。**2026-10-26 半收口**:模拟器侧「改 → 保存 → `OK` → 回首屏新值已在帧里」与「越界被拒、值不落盘」已由像素 + 文本断言覆盖(`sparam_save` / `sparam_landed` / `sparam_oob`);**重启保持与 C_RD 一致只能上板**(模拟器无 NAND)。另注意 40 项里 **13 项 `comm_addr = 0`**(投退、返回系数、过频/低频段、全局软压板)主站不可寻址,不能拿 C_RD 当它们的判据 |
|
||||
| 6 | 分层门禁无新增 | `check_layer_deps.py` 退出码 0,基线 6 → 3。**现状:3**(记录 reader 已削 3 条;写路径走注入式 ops 函数表,未新增 include,见 `docs/UI设计/UI_数据供给设计说明.md` §6.2) |
|
||||
| 7 | 像素回归稳定 | `ui-golden` 连续两次零差异。**已验(2026-10-26)**:27 页连跑两次逐字节一致 |
|
||||
| 8 | 每个新页有 golden 参考 | `simulator/tests/ref/` 新增对应 `.bin`。**整定 4 页已有 9 张**(含保存/越界/落定/回绕各态);参考图入库,改它 = 改判据,须逐页过目 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -50,10 +50,10 @@
|
||||
|
||||
| 页面 | 现有数据源 | 还缺什么 |
|
||||
|---|---|---|
|
||||
| 整定 4 页 | `prot_set` 31 项、`ui_w_editor` | 第 1 项写路径 |
|
||||
| 整定 4 页 | `prot_set` 全部定值项(`App/algo/prot_set.h` 的 `PS_ID_COUNT`,现 40 项)、`ui_w_editor` | 写路径已落,缺可编辑页面 |
|
||||
| 时钟设置 | `sys_clock_set_unix()` | 页面与回读校验 |
|
||||
| 强制复归 | `prot_elm_request_reset()`、`ui_page_action` | 页面与 SOE 留痕 |
|
||||
| 清除记录 | `sys_log` | 删除 API、确认交互 |
|
||||
| 清除记录 | `sys_log` | 页面与确认交互(删除 API 已落) |
|
||||
| 遥测死区 / 系数 | `run_par`、`adc_cal` | 可编辑页面 |
|
||||
| 通讯设置 | 网口配置保存范式可复用 | `[comm]` 段及 104 消费 |
|
||||
| 记录 5 页 | SOE、自检、运行记录已有 reader 先例 | 五类写入方与记录类型 |
|
||||
|
||||
@@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1`
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| ER_IROM1 | 249,500 B (243.65 KB) | 1,048,576 B (1024 KB) | 23.8% |
|
||||
| RW_IRAM1 | 79,008 B (77.16 KB) | 131,072 B (128 KB) | 60.3% |
|
||||
| ER_IROM1 | 253,220 B (247.29 KB) | 1,048,576 B (1024 KB) | 24.1% |
|
||||
| RW_IRAM1 | 79,992 B (78.12 KB) | 131,072 B (128 KB) | 61.0% |
|
||||
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
|
||||
| **Flash (ROM 合计)** | **250,832 B (245.0 KB)** | 1,048,576 B (1024 KB) | **23.9%** |
|
||||
| **Flash (ROM 合计)** | **254,660 B (248.7 KB)** | 1,048,576 B (1024 KB) | **24.3%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
|
||||
@@ -15,19 +15,19 @@
|
||||
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|
||||
|---|---|---|---|---|
|
||||
| ER_IROM1 (Flash) | 249,500 B | 1,048,576 B | 23.8% | 参考 |
|
||||
| ER_IROM1 (Flash) | 253,220 B | 1,048,576 B | 24.1% | 参考 |
|
||||
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
|
||||
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
|
||||
| RW_IRAM1 | 79,008 B | 131,072 B | 60.3% | PASS |
|
||||
| RW_IRAM1 | 79,992 B | 131,072 B | 61.0% | PASS |
|
||||
|
||||
| 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 网络 | 13,702 B | 12 KB | ⚠ 超 1.4 KB |
|
||||
| 存储 | 7,056 B | 12 KB | OK |
|
||||
| UI | 32,335 B | 16 KB | ⚠ 超 15.6 KB |
|
||||
| UI | 33,247 B | 16 KB | ⚠ 超 16.5 KB |
|
||||
| RTOS 堆 | 57,376 B | 56 KB | 堆=配额 |
|
||||
| 应用 | 15,422 B | 8 KB | ⚠ 超 7.1 KB |
|
||||
| 其它 | 10,499 B | — | 参考 |
|
||||
| 应用 | 15,481 B | 8 KB | ⚠ 超 7.1 KB |
|
||||
| 其它 | 10,503 B | — | 参考 |
|
||||
<!-- MEM_BUDGET:END -->
|
||||
|
||||
- **门禁**:pre-commit `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)——
|
||||
|
||||
+36
-11
@@ -1,7 +1,7 @@
|
||||
# 运行/自检记录与待机显示设计说明
|
||||
|
||||
> **版本**:v1.1(已实施)
|
||||
> **日期**:2026-09-14
|
||||
> **版本**:v1.2(§4.3 已随七类记录更新;v1.1 的原始设计口径不变)
|
||||
> **日期**:2026-09-14(v1.2 更新 2026-10-26)
|
||||
> **状态**:步骤 1–9 全部完成——双端构建 0/0、`--logtest` PASS、ui-verify 五门禁全绿;真机 @test 待上电回归(§8.2)
|
||||
> **关联文档**:`项目架构总览.md` · `STM32F4-Base存储架构说明.md` · `系统墙钟时间维护说明.md` · `UI设计/内容页架构.md`(如存在)· `docs/UI设计/mockups/`(待机显示页面.jpg / 运行记录页.jpg / 自检记录页.jpg / 密码页面实际机器的示例图片.jpg)
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
|---|---|------|
|
||||
| `App/log/sys_log.h` | 共享 | 公共 API + 记录格式常量 + 行拆分内联函数(无 OS 依赖) |
|
||||
| `App/log/sys_log_mcu.c` | MCU | FatFS 后端:待发队列(启动前静态数组 + FreeRTOS 消息队列)、defaultTask 落盘、容量截断、上电/重启分类 |
|
||||
| `sim/sim_syslog.c` | 模拟器 | 宿主文件后端(CWD 下 `sim_run.log` / `sim_selftest.log`),直接追加,`time()` 时间戳 |
|
||||
| `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` | 共享 | 自检记录内容页描述符(同上) |
|
||||
@@ -105,14 +105,39 @@
|
||||
|
||||
### 4.3 文件与容量
|
||||
|
||||
| 文件 | 内容 | 上限 | 满时策略 |
|
||||
|---|------|------|---------|
|
||||
| `/log/run.log` | 上电/重启记录 | **1024 条(64KB)** | 删除重建(整卷清空,串口告警);DTU 上电频率低,容量充裕 |
|
||||
| `/log/selftest.log` | 自检故障记录 | **512 条(32KB)** | 同上 |
|
||||
**记录类型 = 日志 ID 本身,一类记录 = `/log` 下一个文件**(记录体内没有类型字段位,
|
||||
理由与逐类判据、写入点清单见 `App/log/README.md`「五类记录与『记录类型 = 日志 ID』」——
|
||||
本节只登记"有哪些文件、各多大"这一层,判据表**不在这里抄第二份**)。
|
||||
|
||||
- 目录 `/log/` 首次落盘前 `f_mkdir`(已存在则忽略);文件名均 8.3 合规。
|
||||
- FTP 视角:卷根下多一个 `LOG` 目录,两个 `.log` 文本文件可直接下载(R5)。
|
||||
- 记录**只增不改不删**(除满时重建与未来"清除记录"功能),保证 FTP 端所见即历史。
|
||||
| 文件 | 记录类型(= `syslog_id_t`) | 上限 | 满时策略 |
|
||||
|---|------|------|---------|
|
||||
| `/log/run.log` | `SYSLOG_RUN` 上电/重启与运行事件 | **1024 条(64KB)** | 删除重建(整卷清空,串口告警);DTU 上电频率低,容量充裕 |
|
||||
| `/log/selftest.log` | `SYSLOG_SELFTEST` 自检故障(有问题才记) | **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 写路径(单写者,调研结论落地)
|
||||
|
||||
@@ -283,7 +308,7 @@ Forbidden:`FAIL`/`ERROR`/`HardFault`。
|
||||
|---|----|------|
|
||||
| O1 | 断电 <10min 的"上电/重启"歧义 | §5.3 阈值折中,文档声明;如需精确可后续加掉电标志(PVD) |
|
||||
| O2 | 故障"已复归"/持续监测(mockup 语义) | v2:周期自检 + 活动故障状态机 + 恢复记录 |
|
||||
| O3 | "清除记录"菜单项(node 27 #P,现为空框) | v2:UI 动作 = 删除/重建 log 文件(与满时策略同路径) |
|
||||
| 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) |
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
| 渲染链路 | LVGL I1 渲染 → SDL 窗口(MCU 端为帧缓冲 → RemoDispBus) |
|
||||
| 配置文件引擎 | `App/cfg/cfg_file.c` 共享核心 + `cfg_file_store_sim.c`(宿主文件 `sim_config.ini`) |
|
||||
| `net_*` BSD API 之上的协议 | `ui_net_sink.c`(RemoDispBus 服务端,TCP 7003)与 `net_socket_sim.c`(Winsock 后端) |
|
||||
| 日志记录格式 | `sys_log` 定长记录编解码(`sim_syslog.c` 写宿主文件) |
|
||||
| 日志记录格式 | `sys_log` 定长记录编解码 + 记录删除 + **七类记录**(run / selftest / accid / alarm / start / oper / yk 各一份宿主文件,道F task-10;口径 = `App/log/README.md`「记录类型 = 日志 ID」)。`sim_syslog.c` 写/删宿主文件;删除与投递的**时序**与真机不同,见 §2.2 |
|
||||
|
||||
**不覆盖**:CH395F 真实网络栈、GD5F NAND、SD2506 RTC、TPAFE5160 ADC、RS-485、IWDG、中断/时序相关行为。
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
| **控制台单写者无对应实现** | `simulator/dbg_log.h` 把 `DBG_*` 映射到 `printf`;**不编译** `console_log.c` | 单写者环形缓冲 + defaultTask 唯一发送 | `console_log` 的环形缓冲溢出/丢弃计数逻辑**模拟器测不到**;共享代码若误调 `console_log.h` 声明,模拟器链接失败(已被此约束捕获) | `@test` + 板端串口观测;`docs/子系统设计/控制台单写者日志设计说明.md` §4.1 |
|
||||
| **mtime 粒度不同** | 宿主文件 mtime(**秒级**) | FAT 打包时间(`fdate<<16\|ftime`,**分钟级**) | `cfg_file_poll` 的 `(size, mtime)` 变更令牌在两端触发时机不同;分钟级粒度下的"同分钟内两次改动"在真机可能被判为无变化 | 板端 STORAGE 套件;`配置文件设计说明.md` §变更检测 |
|
||||
| **无应用回调(空操作)** | 模拟器不注册段回调,`cfg_file_poll` 重载后回调为空 | 各段回调真正执行(如网络重配置) | "配置变更 → 触发回调 → 副作用"链路在模拟器上**只验证到写盘**,回调逻辑不覆盖 | `@test`(配置保存 → UI 生效) |
|
||||
| **「清除记录」两端时序不同** | `syslog_request_clear()` 在调用内**同步**删除宿主文件(`sim_syslog.c` 单线程,没有卷锁、没有队列),返回后状态立刻是 `OK`/`FAIL`,`SYSLOG_CLEAR_WAIT` 永不出现 | 非写者上下文只置标志,真正的 `f_unlink` 排在 defaultTask 的 `syslog_drain()` 尾部(卷锁 try:拿不到锁就顺延,见 `App/log/sys_log_mcu.c`) | 「按了按钮 → 下一拍才见结果」这条异步链、卷占用顺延、以及删除与写者重建的交错,在模拟器上**结构上不存在**;UI 页面若把 WAIT 当失败,模拟器上看不出来 | 主机单测 `test/host/sys_log_mcu/`(TC-SYSLOG-110..122:请求侧零盘触点 / 落地只在 drain / 卷占用顺延 / 幂等 / 不误删;TC-SYSLOG-133/133b:清除成功投一条操作记录、清 oper 自己时不投)+ 台架装置维护页目检 |
|
||||
| **两条投递口在宿主端合一** | `syslog_post_quiet()` = `syslog_post()` 的别名(直写宿主文件,没有队列/待发数组),`syslog_post_dropped()` **恒返 0** | 两个口分开且判据不同:quiet 的调用链上不许内联日志行缓冲(protectTask / iec104Task 的静态最坏链硬线),丢弃只累加计数、由 defaultTask 在 `syslog_drain()` 里按差值补留痕 | 五类记录(事故 / 告警 / 启动 / 操作 / 遥控)在模拟器上**永不丢弃**,"队列满丢记录"、"丢了几条"、"投递侧不许打日志"这三件事在模拟器上结构上不可见;UI 若把"投了"当成"落盘了",模拟器看不出来 | 主机单测 `test/host/sys_log_mcu/`(TC-SYSLOG-132 / 134 / 135:队列侧与启动前数组侧各自的计数与零留痕)+ `scripts/stack_report.py` 静态最坏链(真机侧唯一手段) |
|
||||
| **堆后端不同** | `cfg_store_alloc` = 宿主 `malloc`(堆余量以 MB 计) | FreeRTOS 堆(`configTOTAL_HEAP_SIZE` 48 KB,实测最低水位 ~4.8 KB) | **分配失败路径在模拟器上永不触发**——"KB 级分配必然返回 NULL"这类缺陷模拟器照不出来(`AGENTS.md` 已列为铁律) | 内存预算门禁 + 真机 `[HEAP]` 水位告警(S11) |
|
||||
|
||||
### 2.3 硬件专属(模拟器完全无对应)
|
||||
|
||||
@@ -8,4 +8,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
|
||||
2026-10-23 213676 18860 1188 131500 232536 22.2 75264 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736
|
||||
2026-10-24 213780 18860 1204 131852 232640 22.2 75632 57.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
|
||||
2026-10-25 224860 24640 1332 135100 249500 23.8 79008 60.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
|
||||
2026-10-26 224860 24640 1332 135100 249500 23.8 79008 60.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
|
||||
2026-10-26 228260 24960 1440 135976 253220 24.1 79992 61.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=4003;default_task.o=2736
|
||||
|
||||
|
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"built_at": "2026-10-26T12:21:03+08:00",
|
||||
"fingerprint": "27086d0cd632d2443ff06d519183e4b9f748d08582c7e4f61b26b3d81ee0d312",
|
||||
"built_at": "2026-10-26T15:42:13+08:00",
|
||||
"fingerprint": "9aa277983cb6290c1c0aa5d3d0e2261e440f96141f88827575449c4bdc82b303",
|
||||
"fingerprint_files": 1422,
|
||||
"fingerprint_source": "git-index",
|
||||
"gated_regions": [
|
||||
"RW_IRAM1",
|
||||
"RW_CCM"
|
||||
],
|
||||
"git_head": "7635480e06f853fd27f1e48e059d71e59dca2165",
|
||||
"git_head": "0f7b5a87fb701e676fe747b96f0a2d4adeed26f8",
|
||||
"map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map",
|
||||
"map_mtime": 1792982281,
|
||||
"map_mtime": 1793000530,
|
||||
"pct": {
|
||||
"ER_IROM1": 23.79,
|
||||
"ER_IROM1": 24.15,
|
||||
"RW_CCM": 87.89,
|
||||
"RW_IRAM1": 60.28,
|
||||
"RW_IRAM1": 61.03,
|
||||
"UNINIT_CCM": 18.75
|
||||
},
|
||||
"regions": {
|
||||
"ER_IROM1": {
|
||||
"max": 1048576,
|
||||
"size": 249500
|
||||
"size": 253220
|
||||
},
|
||||
"RW_CCM": {
|
||||
"max": 65280,
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"RW_IRAM1": {
|
||||
"max": 131072,
|
||||
"size": 79008
|
||||
"size": 79992
|
||||
},
|
||||
"UNINIT_CCM": {
|
||||
"max": 256,
|
||||
@@ -37,10 +37,10 @@
|
||||
"schema": 2,
|
||||
"scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring",
|
||||
"sizes": {
|
||||
"code": 224860,
|
||||
"ro_data": 24640,
|
||||
"rw_data": 1332,
|
||||
"zi_data": 135100
|
||||
"code": 228260,
|
||||
"ro_data": 24960,
|
||||
"rw_data": 1440,
|
||||
"zi_data": 135976
|
||||
},
|
||||
"threshold_pct": 92.0,
|
||||
"worktree_dirt_at_stamp": 0,
|
||||
|
||||
+17
-5
@@ -26,7 +26,7 @@ simulator/
|
||||
│ ├── check_content_pages.py # 文本断言 + 内容页黄金参考回归 + 往返池门禁(--update 刷参考)
|
||||
│ ├── check_remo_sink.py # RemoDispBus 远端推流协议回归(起窗口模拟器 + TCP 取帧 + 参考比对)
|
||||
│ ├── regen_ref_png.py # 由 ref/*.bin 批量重生成目检 PNG(--scale 6 --invert)
|
||||
│ └── ref/ # 黄金参考 shot_*.bin + 目检 *.png(**本地生成物,不入库**)
|
||||
│ └── ref/ # 黄金参考 shot_*.bin(**入库**,判据本体)+ 目检 *.png(本地派生物,不入库)
|
||||
├── dbg_log.h # 调试日志 stub:DBG_* 映射到 printf(覆盖 MCU 版头文件)
|
||||
├── SDL2/
|
||||
│ └── SDL2-2.32.10/ # SDL2 预编译 SDK(随仓库提供,无需安装)
|
||||
@@ -53,7 +53,7 @@ simulator/
|
||||
| `build_sim.bat` / `build_sim.ps1` | 一键构建入口:vswhere 探测本机 VS 大版本(16=2019 / 17=2022 / 18=2026,取已装最高)→ 映射 CMake 生成器;`build/` 缓存生成器与本机不符(换机/升级 VS)时自动删除重建(带重试,抗文件锁)。**构建入口一律走它,文档/命令中勿写死生成器版本**;`.bat` 仅负责绕过 PowerShell 执行策略并转交 `.ps1` |
|
||||
| `CMakeLists.txt` | 收集 LVGL 源码(`../Lib/lvgl`)+ 共享 UI 源码(`../Drivers/BSP/UI`)+ 通用配置文件引擎(`../App/cfg_file.c`、`../Drivers/BSP/NETCFG` 的 net_cfg;`net_cfg_legacy` 已于 2026-10-10 删除)+ `sim/` 平台层,链接预编译 SDL2,产出 `lvgl_sim.exe` |
|
||||
| `i1_to_png.py` | 把 `--screenshot` 产出的 I1(1bpp) 原始位图转成 PNG,便于离线比对设计稿。详见 §3.5 |
|
||||
| `tests/` | 无头回归脚本(菜单 8 状态像素回归、内容页文本断言 + 黄金参考回归);`ref/` 为**本地生成物、不入库**(首次 `--update` 生成)。详见 §3.6 |
|
||||
| `tests/` | 无头回归脚本(菜单 8 状态像素回归 + 内容页 27 页:文本断言 / 反向断言 / 黄金参考逐字节比对);`ref/shot_*.bin` **随仓库入库**(判据本体,改动等于改判据,评审须逐页过目),`ref/*.png` 是本地派生的目检图(`.gitignore` 挡掉)。详见 §3.6 |
|
||||
|
||||
> **共享代码不复制**:`ui_menu.c` / `ui_font_cn_12.c` / `ui_net_sink.c` 直接引用 `Drivers/BSP/UI/` 下的原件,
|
||||
> MCU 与 PC 两端永远同步。**不要**在本目录内复制 UI 源码。
|
||||
@@ -116,6 +116,8 @@ LVGL 场景(同一份 ui_menu.c)
|
||||
| 日志 | `Drivers/BSP/dbg_log.h`(USART1 + RTC 时间戳) | `simulator/dbg_log.h`(printf stub) |
|
||||
| 业务任务 | netTask / ftpTask / timeSyncTask 等 | 无(仅编译纯软件部分的配置文件引擎 net_cfg/cfg_file,可离线验证配置加载/保存) |
|
||||
| 时钟 | `lv_tick_inc` 由 ui_task 手动喂(10ms 周期) | SDL 驱动内部 `lv_tick_set_cb(SDL_GetTicks)`,**不可再手动喂** |
|
||||
| UI 数据来源 | `App/app/ui_data_feed.c` 每轮从 phasor / prot_set / 104 点表…装配并发布三帧 | `main.c` 一次性发布**固定帧**(定值不手抄数字,取链进来的 `App/algo/prot_set.c` 出厂默认);口径 `docs/UI设计/UI_数据供给设计说明.md` §9.2 |
|
||||
| 定值写路径(保存) | `ui_data_feed_tick()` 里 `ui_data_param_drain()` 落定 → 置脏 → defaultTask 写 `/prot_set.bin` | `sim_param_pump()` 用**同一份** `g_prot_set_meta` 上下限预检后落值到 RAM,**到此为止**:模拟器无 NAND,"重启值保持 / 主站 C_RD 读到同值"只能上板验(差距登记 `docs/测试说明/模拟器差距清单.md`) |
|
||||
|
||||
### 2.4 关键设计决策
|
||||
|
||||
@@ -220,13 +222,23 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化
|
||||
| 脚本 | 原理 | 失败输出 |
|
||||
|------|------|----------|
|
||||
| `check_menu_rows.py` | 键序注入(u/d/e/s)跑 8 个菜单状态,断言选中行白带、非选行黑底白字、框体边框、往返状态逐像素一致;MENU-STATE-00 池水位须等于脚本内 `MENU_POOL_BASELINE` | 未过断言清单 + 保留临时截图 |
|
||||
| `check_content_pages.py` | ① 文本断言(解析字体字形,在截图内精确匹配期望串,不依赖 `ref/`)② 11 内容页无头截图与 `tests/ref/shot_*.bin` **逐字节**比对;另校验开页-关页往返池水位 = `POOL_AFTER_ROUNDTRIP` | 文本未命中项 / 差异像素数 + 双方 ink runs 对照 |
|
||||
| `check_content_pages.py` | ① 文本断言(解析字体字形,在截图 y 带内精确匹配期望串,不依赖 `ref/`)② **反向断言**(`ABSENT` 表:某串**不该**出现,如查看页/只读态不得出现「未标定」徽标与「保存」行——像素参考会被 `--update` 刷成"自证正确",反向断言不依赖参考图)③ **27 页**无头截图与 `tests/ref/shot_*.bin` **逐字节**比对(含 2026-10-26 新增的整定 9 态:进页 / 保存行 / 只读态 / 编辑态 / 越界 / 保存 OK / 落定后可读 / 二态回绕);另校验开页-关页往返池水位 = `POOL_AFTER_ROUNDTRIP` | 文本未命中项 / 反向断言被命中 / 差异像素数 + 双方 ink runs 对照 |
|
||||
| `check_remo_sink.py` | 启动**窗口**模拟器(非无头,`CREATE_NEW_PROCESS_GROUP` 防管道阻塞),经 TCP 7003 发 LCDMEM 取 1bpp 帧;断言首帧=LCDMEM(无 INIT 污染)/位图 3200 字节/非平凡;复用 `i1_to_png` 出目检 PNG;与无头参考(`lvgl_sim --screenshot`)逐位比对 <2% 判 MATCH。守护"INIT 污染 / 快照卡死"两个历史缺陷 | 各判据失败原因 + `sim_err.txt` 尾部 + 保留 PNG(默认不删,`--clean` 才删) |
|
||||
|
||||
- **黄金参考与目检 PNG**(`ref/` 不入库):首次/换机后先生成一次——
|
||||
- **黄金参考与目检 PNG**(`ref/*.bin` **入库**、`ref/*.png` 本地派生不入库):换机或想目检时——
|
||||
`.venv\Scripts\python.exe simulator/tests/check_content_pages.py --update` 出 `ref/*.bin`,
|
||||
`.venv\Scripts\python.exe simulator/tests/regen_ref_png.py` 出目检 `ref/*.png`(默认 `--scale 6 --invert`)。
|
||||
有意改视觉时先肉眼确认新效果正确再 `--update`(会先跑文本断言防误刷)。
|
||||
有意改视觉时先肉眼确认新效果正确再 `--update`(会先跑文本 + 反向断言防误刷),
|
||||
并且**改完必须** `git diff --stat simulator/tests/ref` 逐页过目:出现在这份清单里的每一张
|
||||
都是"判据被改了",正常应当只有本次有意改动的那几张;`--update` 一把全刷等于把别的意外
|
||||
一起签进仓库(`ref/*.bin` 是入库的,刷错就是提交了错判据)。
|
||||
- **回归为什么是确定的(无头不跑 LVGL 定时器)**:`run_headless()` 只在注入键值时调
|
||||
`lv_group_send_data()`,**从不调 `lv_timer_handler()`/`lv_refr_now()`**(I1 + FULL 刷新模式在
|
||||
离屏渲染上会进死循环,见 §四 已知坑与 `main.c` 里 `shot_delay_cb` 的注释)。推论有两条:
|
||||
① 界面侧任何**周期性 LVGL 定时器**(如定值视图为"落定/主站写值/标定完成"上屏而建的 100 ms
|
||||
轮询)在回归里一次都不会触发 → 像素与它无关,只有窗口模式才用它;② 两次注键之间不发生
|
||||
重绘,所以"数据就位"不能依赖"这一页画过没有"——判据连跑两次逐字节一致即为证据,设计口径见
|
||||
`docs/UI设计/UI_数据供给设计说明.md` §9.2 注 6。
|
||||
- **池水位常量漂移**:菜单/页面结构变更导致门禁失败时,确认新值合理后再更新脚本内常量并登记流程文档变更记录;无法解释的漂移 = 内存回归 bug。
|
||||
- 退出码均为 0=PASS / 1=断言失败 / 2=环境错误(exe 缺失等)。
|
||||
|
||||
|
||||
+217
-11
@@ -37,6 +37,7 @@
|
||||
* lvgl_sim.exe --logtest
|
||||
* 验证链:post N 条 → count/get 回读断言 + 宿主文件字节级校验
|
||||
* (64B 定长、ISO 时间戳、epoch 字段、NUL 填充)
|
||||
* → 「清除记录」:计数归零 + 文件真没了 + 只动自己那份 + 幂等 + 删后还能继续写
|
||||
*
|
||||
* 待机屏截图模式(无窗口,强制进入待机后截图):
|
||||
* lvgl_sim.exe --standby shot.bin
|
||||
@@ -232,9 +233,53 @@ static const ui_soe_reader_t s_soe_reader = { sim_soe_count, sim_soe_get };
|
||||
* 3. 定值取 prot_set 出厂默认(App/algo/prot_set.c 是纯算法,双端可编),
|
||||
* 不手抄 40 个数字——抄一次就是一份会漂移的第二真相
|
||||
*/
|
||||
/* 最近一次发布的参数帧校验和(sim_param_pump 的"值变了才重发"门,与真机同纪律) */
|
||||
static uint32_t s_par_pub_sum;
|
||||
|
||||
/*
|
||||
* 函数功能:按 prot_set 现状填一帧参数快照(模拟器版 feed_fill_par)
|
||||
* 入口参数:p_par - 目标帧(调用方已清零;本函数只填有真源的几个字段)
|
||||
* 返回值:无
|
||||
* 限定条件:prot_set_init() 已跑过(值区有效)
|
||||
* 函数说明:40 项值全部现取 prot_set,不手抄数字(抄一次就是一份会漂移的第二真相)。
|
||||
* cal_block 固定给 1:**真机现在就是**"UA/UN/IA 未标定 → 判据停走"
|
||||
* (产品构建下 [adc_cal] 的 valid 位没写,用户裁定保持闭锁),模拟器要
|
||||
* 与本机一致才配当像素基准——rt.eng 也按同一条口径钉在引脚档。
|
||||
* 标定链在模拟器里没有数据源,所以这里给常量而不是"猜一个会变的值";
|
||||
* 要拍"已标定"态的图,改这一处即可(黄金参考随之要一起刷)
|
||||
*/
|
||||
static void sim_fill_par_frame(ui_data_par_t *p_par) {
|
||||
uint8_t i;
|
||||
|
||||
p_par->valid = 1u;
|
||||
p_par->checksum = prot_set_checksum();
|
||||
p_par->locked = prot_set_locked();
|
||||
p_par->bad_mask = 0u; /* 装载成功:无越界项 */
|
||||
p_par->cal_block = 1u; /* 与真机同口径:未标定(判据停走) */
|
||||
for (i = 0u; i < UI_DATA_SET_N; i++) {
|
||||
p_par->q10[i] = prot_set_get((prot_set_id_t)i);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:发布一帧参数快照并记下其校验和(写路径落定后重发用)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:prot_set_init() 之后
|
||||
* 函数说明:与 App/app/ui_data_feed.c 同一条纪律——值变了才重发(这里比校验和,
|
||||
* 模拟器没有自检闭锁与标定链,那两个源恒不变);页面按发布序号失效缓存
|
||||
*/
|
||||
static void sim_publish_par_frame(void) {
|
||||
ui_data_par_t par;
|
||||
|
||||
(void)memset(&par, 0, sizeof(par));
|
||||
sim_fill_par_frame(&par);
|
||||
(void)ui_data_publish_par(&par);
|
||||
s_par_pub_sum = par.checksum;
|
||||
}
|
||||
|
||||
static void sim_publish_data_frames(void) {
|
||||
ui_data_rt_t rt;
|
||||
ui_data_par_t par;
|
||||
ui_data_info_t info;
|
||||
uint8_t i;
|
||||
|
||||
@@ -286,16 +331,9 @@ static void sim_publish_data_frames(void) {
|
||||
(void)snprintf(rt.clock, sizeof(rt.clock), "%s", "2026-10-25 08:00:00");
|
||||
(void)ui_data_publish_rt(&rt);
|
||||
|
||||
(void)memset(&par, 0, sizeof(par));
|
||||
(void)prot_set_init(); /* 出厂默认 + 封存(幂等) */
|
||||
par.valid = 1u;
|
||||
par.checksum = prot_set_checksum();
|
||||
par.locked = prot_set_locked();
|
||||
par.bad_mask = 0u; /* 装载成功:无越界项 */
|
||||
for (i = 0u; i < UI_DATA_SET_N; i++) {
|
||||
par.q10[i] = prot_set_get((prot_set_id_t)i);
|
||||
}
|
||||
(void)ui_data_publish_par(&par);
|
||||
/* 定值帧:出厂默认 + 封存(幂等),再按 prot_set 现状发一帧(见上面两个函数) */
|
||||
(void)prot_set_init();
|
||||
sim_publish_par_frame();
|
||||
|
||||
(void)memset(&info, 0, sizeof(info));
|
||||
info.valid = 1u;
|
||||
@@ -311,6 +349,86 @@ static void sim_publish_data_frames(void) {
|
||||
(void)ui_data_publish_info(&info);
|
||||
}
|
||||
|
||||
/* ── 定值写路径镜像(道F task-2:整定 4 页的"保存"落定)──────────────────── */
|
||||
|
||||
/*
|
||||
* 函数功能:写路径判定"定值闭锁中吗"(模拟器版,镜像 App/app/ui_data_feed.c)
|
||||
* 入口参数:无
|
||||
* 返回值:prot_set 的闭锁态(模拟器里恒 0:不跑 L5-1 自检) uint8_t
|
||||
* 限定条件:主循环 / 无头注键链(单线程)
|
||||
*/
|
||||
static uint8_t sim_param_locked(void) {
|
||||
return prot_set_locked();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写路径整批预检——单项是否落在 meta 上下限内
|
||||
* 入口参数:a_id - 定值项编号;a_q10 - Q10 定点值
|
||||
* 返回值:0 - 合法;-1 - 编号越界或值越界 int
|
||||
* 限定条件:只读 g_prot_set_meta
|
||||
* 函数说明:与真机走的是**同一份**上下限真源(App/algo/prot_set.c),
|
||||
* 所以"模拟器里越界被拒"与"真机上越界被拒"是同一个判定,不是两份规矩
|
||||
*/
|
||||
static int sim_param_check(uint8_t a_id, int32_t a_q10) {
|
||||
if (a_id >= (uint8_t)PS_ID_COUNT) {
|
||||
return -1;
|
||||
}
|
||||
if ((a_q10 < g_prot_set_meta[a_id].min) || (a_q10 > g_prot_set_meta[a_id].max)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写路径落值——单项原子写入 + 重封 + 置脏(模拟器无文件系统,落盘到此为止)
|
||||
* 入口参数:a_id - 定值项编号;a_q10 - Q10 定点值
|
||||
* 返回值:0 - 已写入;-1 - 拒写 int
|
||||
* 限定条件:BSP 侧已整批预检通过
|
||||
* 函数说明:真机在这里之后还有 defaultTask 扫脏写 /prot_set.bin 一段——模拟器没有
|
||||
* NAND,跨复位保持这条只能在台架验(差距已登记在案,别把模拟器跑绿
|
||||
* 当成"保存能落盘"的证据 → docs/测试说明/模拟器差距清单.md)
|
||||
*/
|
||||
static int sim_param_apply(uint8_t a_id, int32_t a_q10) {
|
||||
if (a_id >= (uint8_t)PS_ID_COUNT) {
|
||||
return -1;
|
||||
}
|
||||
return prot_set_set_sealed((prot_set_id_t)a_id, a_q10);
|
||||
}
|
||||
|
||||
/* 注入给 ui_data_param_drain() 的执行侧实现(与真机 s_param_ops 同构) */
|
||||
static const ui_data_param_ops_t s_param_ops = {
|
||||
sim_param_locked,
|
||||
sim_param_check,
|
||||
sim_param_apply
|
||||
};
|
||||
|
||||
/*
|
||||
* 函数功能:落定页面投递的定值整定请求(模拟器版装配节拍)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:主循环每轮一次(窗口模式)/ 注键结束后同步一次(无头模式)——
|
||||
* 两处都必须排在按键之后、渲染/截图之前,否则保存行停在 WAIT
|
||||
* 函数说明:真机这段在 App/app/ui_data_feed.c 的 tick 里(模拟器不链接装配器),
|
||||
* 这里只做同样的三件事:出队落定 → 值变了就重发参数帧 → 让页面看见。
|
||||
* drain 每轮最多处理一条,故用 while 把在队的清空(无头模式后面没有
|
||||
* 下一个 tick,必须当场排空才能拍到终态)。
|
||||
* 16 次上界是纯防御:队列深度只有 4,正常永不触及
|
||||
*/
|
||||
static void sim_param_pump(void) {
|
||||
uint32_t guard = 0u;
|
||||
|
||||
while (ui_data_param_drain(&s_param_ops) == 1) {
|
||||
if (prot_set_checksum() != s_par_pub_sum) {
|
||||
sim_publish_par_frame();
|
||||
ui_content_page_invalidate(); /* 让页面重绘:保存行 OK + 新值同帧 */
|
||||
}
|
||||
guard++;
|
||||
if (guard >= 16u) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:无窗口离屏渲染(可注入键序列)并导出末帧像素
|
||||
* 入口参数:keys - 键序列(u=上 d=下 l=左 r=右 e=确认 s=返回;空 = 不注键) const char *
|
||||
@@ -382,6 +500,11 @@ static int run_headless(const char * keys, const char * path, int standby) {
|
||||
net_cfg_save_run();
|
||||
}
|
||||
|
||||
/* 同上理由落定"整定保存"请求:真机由 ui_data_feed_tick() 每 10 ms 排一条,
|
||||
* 无头模式没有那个节拍,不在此当场排空,截图就会停在 WAIT(终态拍不到,
|
||||
* OK/ERR/LOCK 三条判据的像素证据也就全都不成立) */
|
||||
sim_param_pump();
|
||||
|
||||
/* --standby:注键后强制进入待机屏(截图前) */
|
||||
if (standby != 0) {
|
||||
ui_standby_force_enter();
|
||||
@@ -588,6 +711,10 @@ static int run_cfgtest(void) {
|
||||
* 返回值:0 - PASS;非 0 - FAIL int
|
||||
* 说明:验证链 清理旧档 → post N 条 → count/get 回读断言 + 宿主文件字节级校验
|
||||
* (64B 定长、ISO 时间戳格式、epoch 字段 ≈ now、NUL 填充)
|
||||
* → 「清除记录」(syslog_request_clear / syslog_clear_state)。宿主后端与 MCU 端
|
||||
* 判据相同、时序不同:模拟器单线程,请求在调用内就落地,所以本端断言的是
|
||||
* 「立刻 OK」——读到 WAIT 就说明两端语义漂了。完整分支矩阵(卷占用、unlink
|
||||
* 错误码、幂等、读窗口失效)在 test/host/sys_log_mcu/ 那侧用假卷注入去测。
|
||||
*/
|
||||
static int run_logtest(void) {
|
||||
uint32_t ep;
|
||||
@@ -675,6 +802,81 @@ static int run_logtest(void) {
|
||||
}
|
||||
|
||||
printf("[logtest] OK count/get/epoch/bytes\n");
|
||||
/* ── 「清除记录」:宿主后端必须与 MCU 端共用同一组判据 ──
|
||||
* 分支矩阵(卷占用 / unlink 错误码 / 幂等 / 读窗口失效)由假卷那边测,
|
||||
* 这里只测「同一份头契约在宿主上真对得上」——宿主没有卷锁,也就没有顺延。 */
|
||||
|
||||
/* 从未请求过的 id 必须是 NONE:UI 据此把「没操作过」和「已清除」分开显示 */
|
||||
if (syslog_clear_state(SYSLOG_SELFTEST) != SYSLOG_CLEAR_NONE) {
|
||||
printf("[logtest] FAIL selftest clear state = %d (expect NONE before any ask)\n",
|
||||
syslog_clear_state(SYSLOG_SELFTEST));
|
||||
return 1;
|
||||
}
|
||||
/* 越界 id:请求被拒、状态仍 NONE,且不得牵连合法 id */
|
||||
if ((syslog_request_clear((syslog_id_t)SYSLOG_COUNT) == 0)
|
||||
|| (syslog_clear_state((syslog_id_t)SYSLOG_COUNT) != SYSLOG_CLEAR_NONE)
|
||||
|| (syslog_clear_state(SYSLOG_SELFTEST) != SYSLOG_CLEAR_NONE)) {
|
||||
printf("[logtest] FAIL out-of-range clear was accepted\n");
|
||||
return 1;
|
||||
}
|
||||
if (syslog_request_clear(SYSLOG_RUN) != 0) {
|
||||
printf("[logtest] FAIL request_clear(run)\n");
|
||||
return 1;
|
||||
}
|
||||
if (syslog_count(SYSLOG_RUN) != 0u) {
|
||||
printf("[logtest] FAIL run count after clear = %u (expect 0)\n",
|
||||
(unsigned)syslog_count(SYSLOG_RUN));
|
||||
return 1;
|
||||
}
|
||||
/* 同步落地的终态;本端读到 WAIT 就是两端语义漂移 */
|
||||
if (syslog_clear_state(SYSLOG_RUN) != SYSLOG_CLEAR_OK) {
|
||||
printf("[logtest] FAIL run clear state = %d (expect OK on host)\n",
|
||||
syslog_clear_state(SYSLOG_RUN));
|
||||
return 1;
|
||||
}
|
||||
/* 文件必须真没了(不是「截成 0 字节」),而且不许牵连邻居 */
|
||||
{
|
||||
FILE *gone = fopen("sim_run.log", "rb");
|
||||
FILE *neigh = fopen("sim_selftest.log", "rb");
|
||||
|
||||
if (gone != NULL) {
|
||||
(void)fclose(gone);
|
||||
printf("[logtest] FAIL sim_run.log still there after clear\n");
|
||||
return 1;
|
||||
}
|
||||
if (neigh == NULL) {
|
||||
printf("[logtest] FAIL sim_selftest.log lost: clear touched the wrong file\n");
|
||||
return 1;
|
||||
}
|
||||
(void)fclose(neigh);
|
||||
}
|
||||
/* 幂等:文件已不在,再清一次仍算成功(UI 上重复点击不该报故障) */
|
||||
if ((syslog_request_clear(SYSLOG_RUN) != 0)
|
||||
|| (syslog_clear_state(SYSLOG_RUN) != SYSLOG_CLEAR_OK)) {
|
||||
printf("[logtest] FAIL second clear is not idempotent\n");
|
||||
return 1;
|
||||
}
|
||||
/* 删后重建可继续写:清除不能把日志通道本身弄坏(MCU 端那条 FA_WRITE 陷阱由
|
||||
* test/host/sys_log_mcu/ 的 TC-SYSLOG-112 钉,本端同判据同断言形状) */
|
||||
syslog_post(SYSLOG_RUN, "after clear");
|
||||
if (syslog_count(SYSLOG_RUN) != 1u) {
|
||||
printf("[logtest] FAIL run count after re-post = %u (expect 1)\n",
|
||||
(unsigned)syslog_count(SYSLOG_RUN));
|
||||
return 1;
|
||||
}
|
||||
if ((syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) != 0)
|
||||
|| (strcmp(desc, "after clear") != 0)) {
|
||||
printf("[logtest] FAIL get_record after clear: desc mismatch\n");
|
||||
return 1;
|
||||
}
|
||||
/* 两份记录各清各的:清 selftest 不得带走 run 里刚写的那一条 */
|
||||
if ((syslog_request_clear(SYSLOG_SELFTEST) != 0)
|
||||
|| (syslog_count(SYSLOG_SELFTEST) != 0u)
|
||||
|| (syslog_count(SYSLOG_RUN) != 1u)) {
|
||||
printf("[logtest] FAIL selftest clear (run must keep its 1 record)\n");
|
||||
return 1;
|
||||
}
|
||||
printf("[logtest] OK clear: zeroed + file gone + isolated + idempotent + rebuild\n");
|
||||
printf("[logtest] PASS\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -797,6 +999,10 @@ int main(int argc, char **argv) {
|
||||
* 无请求 = 空操作);"立即应用"回调未注册 → 空操作 */
|
||||
net_cfg_save_run();
|
||||
|
||||
/* 落定定值整定请求(镜像真机 ui_data_feed_tick 里的 ui_data_param_drain:
|
||||
* 出队 → prot_set 落值 → 重发参数帧);队列空时只是一次计数判断 */
|
||||
sim_param_pump();
|
||||
|
||||
/* 配置文件轮询(2s 周期:外部文件变更 4s 沉降后自动重载) */
|
||||
uint32_t now_ms = (uint32_t)SDL_GetTicks();
|
||||
if ((uint32_t)(now_ms - cfg_last_poll) >= 2000u) {
|
||||
|
||||
+147
-4
@@ -1,12 +1,30 @@
|
||||
/*
|
||||
* 模块名称:System Log (模拟器后端) — 运行/自检记录落盘
|
||||
* 模块功能:宿主文件 sim_run.log / sim_selftest.log(工作目录),与 MCU 端同格式
|
||||
* (定长 64B 记录);单线程直写,无队列
|
||||
* 模块功能:宿主文件七份(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 并逐项写出,
|
||||
* 越界这条随扩容一起消失。
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -19,8 +37,24 @@
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
static const char s_paths[SYSLOG_COUNT][16] = { "sim_run.log", "sim_selftest.log" };
|
||||
static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_MAX };
|
||||
/* 行宽 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];
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
@@ -126,6 +160,115 @@ 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
|
||||
|
||||
@@ -54,9 +54,30 @@ PAGES = [
|
||||
("vset", "ddedede"),
|
||||
("vword", "ddededde"),
|
||||
("vplate", "ddededdde"),
|
||||
# ── 道F task-2:整定 4 页(参数定值 → 整定 → *,#P 叶,123=修改密码可编辑)──
|
||||
# 导航公共前缀:dd 进"参数定值"→ e 进(选中"整定")→ e 进整定子框(选中"参数")
|
||||
# → e 进密码页;密码 123 = "uruuruuue"(三框各 +1、两次右移、确认,与 netcfg 同串法);
|
||||
# 000(直接 e)= 查看密码 → 只读。定值/控制字/软压板 在整定子框里多按 d 选到该行再 e。
|
||||
# 保存行在第 (行数) 行:参数 9 行 = 2 页 → 8×d 落到保存行;定值 19 行 = 3 页。
|
||||
("sparam_p2", "ddeee uruuruuue dddddddd"), # 参数整定第 2 页:保存行 ENTER
|
||||
("sparam_ro", "ddeee e"), # 000 只读:无保存行、无徽标
|
||||
("sparam_edit", "ddeee uruuruuue d e"), # PT二次 进编辑(值列是编辑串,无单位)
|
||||
("sparam_oob", "ddeee uruuruuue d e l l d e"), # 编到 0(< min=1)→ ENTER 拒退 + 越界
|
||||
("sparam_save", "ddeee uruuruuue e u e dddddddd e"), # PT一次 +1 → 保存行 ENTER → OK
|
||||
("sparam_landed", "ddeee uruuruuue e u e dddddddd e u"), # 保存后回首屏:新值 10001V 已在帧里
|
||||
("sset", "ddeed e uruuruuue"), # 定值整定首屏(18 项 + 保存 = 3 页)
|
||||
("sword_cycle", "ddeedde uruuruuue e"), # 控制字 二态行 ENTER:跳闸 → 告警
|
||||
("splate", "ddeeddde uruuruuue"), # 软压板整定首屏(10 项 + 保存 = 2 页)
|
||||
("pwd", "ddeee"), # 密码页初始态:3 框 000、光标首框
|
||||
("pwe", "ddeee uru"), # 密码页调值+移光标:110、光标第 2 框
|
||||
("pwk", "ddeee uruuruuue"), # 密码页输 123:u / r / u,u / r / u,u,u / e → 关页进空白终点框(菜单屏)
|
||||
# 密码页输 123:u / r / u,u / r / u,u,u / e → 关页并按授权"续做进入"。
|
||||
# ⚠ **道F task-2 起这张图的落点变了**(黄金参考已 --update 刷新,属有意改判据):
|
||||
# 整定 4 叶绑定内容页之前,123 确认后走 menu_password_ok 的"无内容页 → 压空白
|
||||
# 终点框"分支(屏上是 4 框菜单,整定叶本身是个死叶子);绑定之后走
|
||||
# "有内容页 → ui_content_page_open(page, UI_PERM_MODIFY)"分支,落在**整定-参数页**。
|
||||
# 也就是说这张图现在同时钉住两件事:密码页授权链正确 + 123 真的能开可编辑整定页。
|
||||
# 旧图并非"错",是那条分支从此不再可达——留档见 2026-10-26 提交正文。
|
||||
("pwk", "ddeee uruuruuue"),
|
||||
("pwc", "ddeee ue"), # 密码页 100 不一致 → 清空重输(回到 000 初始态像素)
|
||||
# 网口设置(node 30,通讯参数下第 2 项,需密码):
|
||||
# dddddd = 根框下移 5 至通讯参数;e 进框;d 选中网口设置;e 进密码页
|
||||
@@ -272,6 +293,79 @@ TEXTS = {
|
||||
("r1名", "子网掩码", (38, 52), False),
|
||||
("r4名", "保存", (80, 95), False),
|
||||
],
|
||||
# ── 道F task-2:整定 4 页文本级断言 ─────────────────────────────────────────
|
||||
# 值全部来自 simulator/main.c 的固定帧(prot_set 出厂默认 + cal_block 恒 1),
|
||||
# 每条断言都是"某个具体事实":徽标在不在、保存行在第几页、越界提示、改值后落没落。
|
||||
# ⚠ 编辑态的值列**没有单位后缀**(ui_w_setlist_refresh 编辑分支走
|
||||
# ui_w_editor_format,非编辑分支才拼 unit)——所以 sparam_edit 断 "100" 而不是
|
||||
# "100V":这条差异本身就是"这一行正在编辑"的像素证据,别把它"顺手统一"掉。
|
||||
"pwk": [
|
||||
("标题", "参数", (0, 24), False),
|
||||
("徽标", "未标定", (0, 24), False), # cal_block=1:整定页必须看得见
|
||||
("r0名", "PT一次", (24, 38), True), ("r0值", "10000V", (24, 38), True),
|
||||
("r4名", "过量返回", (80, 94), False), ("r4值", "0.95", (80, 94), False),
|
||||
("r7名", "低频返回", (122, 136), False), ("r7值", "1.01", (122, 136), False),
|
||||
("页脚", "第 1 页,共 2 页", (140, 160), False), # 8 项 + 保存行 = 9 行 → 2 页
|
||||
],
|
||||
"sparam_p2": [
|
||||
("徽标", "未标定", (0, 24), False),
|
||||
("保存名", "保存", (24, 38), True),
|
||||
("保存值", "ENTER", (24, 38), True), # 从未投递 → IDLE
|
||||
("页脚", "第 2 页,共 2 页", (140, 160), False),
|
||||
],
|
||||
"sparam_ro": [
|
||||
# 000 查看密码:同一份描述符的只读分支——有行、无保存行、无徽标(见 ABSENT)
|
||||
("标题", "参数", (0, 24), False),
|
||||
("r0名", "PT一次", (24, 38), True), ("r0值", "10000V", (24, 38), True),
|
||||
("r7名", "低频返回", (122, 136), False),
|
||||
("页脚", "第 1 页,共 1 页", (140, 160), False), # 无保存行 → 8 行正好 1 页
|
||||
],
|
||||
"sparam_edit": [
|
||||
# 编辑态的可见特征:选中带反显(白底黑字)+ 值列**没有单位后缀**(见 ABSENT)
|
||||
("r1名", "PT二次", (38, 52), True),
|
||||
("r0名", "PT一次", (24, 38), False), ("r0值", "10000V", (24, 38), False),
|
||||
("r3名", "CT二次", (66, 80), False), ("r3值", "5A", (66, 80), False),
|
||||
],
|
||||
"sparam_oob": [
|
||||
# PT二次 下限 1:把百位从 1 调到 0 → 0 < min → ENTER 被拒、留在编辑态并打"越界"
|
||||
("越界提示", "越界", (38, 52), True),
|
||||
("r1名", "PT二次", (38, 52), True),
|
||||
("r0值未变", "10000V", (24, 38), False), # 别的行不受影响(编辑只作用选中行)
|
||||
],
|
||||
"sparam_save": [
|
||||
("保存名", "保存", (24, 38), True),
|
||||
("保存值", "OK", (24, 38), True), # 投递 → drain 落定 → 终态 OK
|
||||
("页脚", "第 2 页,共 2 页", (140, 160), False),
|
||||
],
|
||||
"sparam_landed": [
|
||||
# 保存后再按一次上移回到首屏:新值必须已在参数帧里(写路径的完整闭环)
|
||||
("r0值新", "10001V", (24, 38), False),
|
||||
("r1值旧", "100V", (38, 52), False), # 只提交改动行:没动的行还是旧值
|
||||
("页脚", "第 1 页,共 2 页", (140, 160), False),
|
||||
],
|
||||
"sset": [
|
||||
("标题", "定值", (0, 24), False),
|
||||
("徽标", "未标定", (0, 24), False),
|
||||
("r0名", "Ⅰ段电流", (24, 38), True), ("r0值", "10.00A", (24, 38), True),
|
||||
("r7名", "过负荷延时", (122, 136), False), ("r7值", "10000ms", (122, 136), False),
|
||||
("页脚", "第 1 页,共 3 页", (140, 160), False), # 18 项 + 保存行 = 19 行 → 3 页
|
||||
],
|
||||
"sword_cycle": [
|
||||
# 二态行 ENTER:跳闸(1) → **回绕到 告警(0)**;未动的行保持 跳闸(只提交改动 id)
|
||||
("标题", "控制字", (0, 24), False),
|
||||
("r0名", "Ⅰ段跳闸", (24, 38), True), ("r0值", "告警", (24, 38), True),
|
||||
("r1名", "Ⅱ段跳闸", (38, 52), False), ("r1值", "跳闸", (38, 52), False),
|
||||
("r3名", "过负荷跳闸", (66, 80), False), ("r3值", "告警", (66, 80), False),
|
||||
("保存名", "保存", (80, 95), False), ("保存值", "ENTER", (80, 95), False),
|
||||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||||
],
|
||||
"splate": [
|
||||
("标题", "软压板", (0, 24), False),
|
||||
("徽标", "未标定", (0, 24), False),
|
||||
("r0名", "Ⅰ段投入", (24, 38), True), ("r0值", "投入", (24, 38), True),
|
||||
("r7名", "过频投入", (122, 136), False), ("r7值", "投入", (122, 136), False),
|
||||
("页脚", "第 1 页,共 2 页", (140, 160), False), # 10 项 + 保存行 = 11 行 → 2 页
|
||||
],
|
||||
"netcfg_save": [
|
||||
("标题", "网口设置", (0, 24), False),
|
||||
("r0名", "IP地址", (24, 38), False), ("r0值", "192.168.001.100", (24, 38), False),
|
||||
@@ -280,6 +374,36 @@ TEXTS = {
|
||||
],
|
||||
}
|
||||
|
||||
# 反向文本断言表:页名 -> [(名称, 期望**不出现**的串, y 带, 是否反显态)]
|
||||
# ⚠ inv 必须与"这一行本来会怎么画"一致,否则反向断言恒真、形同没写:
|
||||
# 选中/编辑行是白底黑字 → inv=True;普通行黑底白字 → inv=False。
|
||||
# 为什么需要"不该出现"这一侧:像素参考能钉住整张图,但参考图是按当前实现刷出来的
|
||||
# ——如果"未标定"徽标哪天在两处都画了(整定 attach 判据失效 + 顺手 --update 刷参考),
|
||||
# 像素比对会"一致"而事实已经错了。反向断言不依赖参考图,钉的是"这条规矩本身":
|
||||
# · 徽标只属于整定态(可改值但判据停走),不属于只读态,也不属于四个查看页;
|
||||
# · 只读态没有"保存"这件事(无 commit → 组件不画保存行)。
|
||||
ABSENT = {
|
||||
"sparam_ro": [
|
||||
("只读页无未标定徽标", "未标定", (0, 24), False),
|
||||
("只读页无保存行", "保存", (0, 140), False),
|
||||
],
|
||||
# 查看 4 页:task-2 给同一批描述符绑了 key_cb / edit_capable,像素必须零变化;
|
||||
# 徽标与保存行都不该出现在它们身上(出现 = 整定 attach 漏判了权限)
|
||||
"vparam": [("查看页无未标定徽标", "未标定", (0, 24), False),
|
||||
("查看页无保存行", "保存", (0, 140), False)],
|
||||
"vset": [("查看页无未标定徽标", "未标定", (0, 24), False),
|
||||
("查看页无保存行", "保存", (0, 140), False)],
|
||||
"vword": [("查看页无未标定徽标", "未标定", (0, 24), False),
|
||||
("查看页无保存行", "保存", (0, 140), False)],
|
||||
"vplate": [("查看页无未标定徽标", "未标定", (0, 24), False),
|
||||
("查看页无保存行", "保存", (0, 140), False)],
|
||||
# 编辑态的值列由 ui_w_editor_format 生成(**不拼单位**),非编辑态才拼:
|
||||
# 所以"100V 不在这一行"就是"这一行确实在编辑中"的证据。
|
||||
# 反过来正向断言"100"是没有意义的——它是"100V"甚至"10000V"的子串,
|
||||
# 字形匹配器只保证 bbox 内干净、不检查词边界(实测在上一行的 10000V 上命中)
|
||||
"sparam_edit": [("编辑态不打单位后缀", "100V", (38, 52), True)],
|
||||
}
|
||||
|
||||
W = H = 160
|
||||
STRIDE = W // 8 # I1:20 字节/行
|
||||
|
||||
@@ -468,16 +592,26 @@ def main():
|
||||
run_sim(seq, shot, tmp)
|
||||
shots[name] = open(shot, "rb").read()
|
||||
|
||||
n_pos = 0
|
||||
for name, _seq in PAGES:
|
||||
for tname, text, band, inv in TEXTS.get(name, []):
|
||||
n_pos += 1
|
||||
if not _match_text(shots[name], text, band, inv):
|
||||
ok = False
|
||||
print("[%s] FAIL 文本断言未命中:%s = %r(y 带 %s 反显=%s)"
|
||||
% (name, tname, text, band, inv))
|
||||
n_abs = 0
|
||||
for name, _seq in PAGES:
|
||||
for tname, text, band, inv in ABSENT.get(name, []):
|
||||
n_abs += 1
|
||||
if _match_text(shots[name], text, band, inv):
|
||||
ok = False
|
||||
print("[%s] FAIL 反向断言被命中:%s 不该出现却出现了(y 带 %s)"
|
||||
% (name, tname, band))
|
||||
if not ok:
|
||||
print("FAIL: 文本级断言未通过(缺字/错标签);中止像素比对(现场保留在 %s)" % tmp)
|
||||
return 1
|
||||
print("文本级断言:%d 条全部命中" % sum(len(TEXTS.get(n, [])) for n, _ in PAGES))
|
||||
print("文本级断言:%d 条命中 + %d 条反向(不该出现)全部通过" % (n_pos, n_abs))
|
||||
|
||||
# 2) 像素黄金参考比对 / 刷新
|
||||
for name, seq in PAGES:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -24,6 +24,11 @@
|
||||
* TC-IEC104-011 t2 到期发 S
|
||||
* TC-IEC104-012 t3 空闲发 TESTFR act;未答 t1 超时 → IDLE;con 清挂起不误杀
|
||||
* TC-IEC104-013 遥控两段式:选择/执行成功;失配 PN;拒绝类选择段即 PN
|
||||
* TC-IEC104-030 遥控 / 操作记录投递(道F task-10 的 104 侧写入点):未知 IOA、
|
||||
* YK11 选择与执行、YK12 选择 / 执行 / 非法 DPC 各投哪一类哪一句,外加
|
||||
* "拒绝不投操作记录""记录与压板实际状态一致""文案不超 28 字节"三条会
|
||||
* 静默变坏但台架看不出的点(YK12 的 set_sealed 失败腿不可达,理由写在
|
||||
* 该用例注释里,没有为它伪造定值范围)
|
||||
* TC-IEC104-014 定值两段写(真实 prot_set):越界 PN;执行后 get_f + selfcheck
|
||||
* TC-IEC104-015 C_RD:YC 命中 / 定值命中 / 未知 IOA COT14+EVI
|
||||
* TC-IEC104-016 C_CS:clock_sync 收正确 epoch;IV → PN;clock_sync 失败 → PN
|
||||
@@ -148,6 +153,65 @@ void syslog_post(syslog_id_t id, const char *desc) {
|
||||
rp_s_syslog_count++;
|
||||
}
|
||||
|
||||
/* ---- syslog_post_quiet 桩:记下投了什么(task-10 遥控 / 操作记录用) ----
|
||||
* 真机上 quiet 与 post 的差别只在"投递侧不打日志",收/丢判据逐条相同;
|
||||
* 本套件是协议栈单测,不验投递本身(那归 test/host/sys_log_mcu),只验
|
||||
* **板端源在每条返回路径上投了哪一类、哪一句**——所以这里存成环形日志。 */
|
||||
#define RP_REC_MAX 16u
|
||||
static uint8_t rp_s_rec_id[RP_REC_MAX];
|
||||
static char rp_s_rec_desc[RP_REC_MAX][SYSLOG_DESC_MAX + 1u];
|
||||
static uint32_t rp_s_rec_n; /* 累计条数(不回绕,供"一条都没多投"断言) */
|
||||
static uint32_t rp_s_drop_quiet; /* 本桩不模拟丢弃,恒 0 */
|
||||
|
||||
void syslog_post_quiet(syslog_id_t id, const char *desc) {
|
||||
uint32_t slot;
|
||||
size_t dl = 0u;
|
||||
|
||||
if (desc == NULL) {
|
||||
return;
|
||||
}
|
||||
slot = rp_s_rec_n % RP_REC_MAX;
|
||||
rp_s_rec_id[slot] = (uint8_t)id;
|
||||
while ((dl < SYSLOG_DESC_MAX) && (desc[dl] != '\0')) {
|
||||
dl++;
|
||||
}
|
||||
(void)memcpy(rp_s_rec_desc[slot], desc, dl);
|
||||
rp_s_rec_desc[slot][dl] = '\0';
|
||||
rp_s_rec_n++;
|
||||
}
|
||||
|
||||
uint32_t syslog_post_dropped(void) { return rp_s_drop_quiet; }
|
||||
|
||||
/* 第 k 条(0 起)记录;越界返回 -1 */
|
||||
static int rp_rec_at(uint32_t k, syslog_id_t id, const char *desc)
|
||||
{
|
||||
if (k >= rp_s_rec_n) {
|
||||
return -1;
|
||||
}
|
||||
return (rp_s_rec_id[k % RP_REC_MAX] == (uint8_t)id)
|
||||
&& (strcmp(rp_s_rec_desc[k % RP_REC_MAX], desc) == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* 某类记录一共投了几条 */
|
||||
static uint32_t rp_rec_cnt(syslog_id_t id)
|
||||
{
|
||||
uint32_t i, n = 0u;
|
||||
|
||||
for (i = 0u; (i < rp_s_rec_n) && (i < RP_REC_MAX); i++) {
|
||||
if (rp_s_rec_id[i] == (uint8_t)id) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void rp_rec_clear(void)
|
||||
{
|
||||
rp_s_rec_n = 0u;
|
||||
(void)memset(rp_s_rec_id, 0, sizeof(rp_s_rec_id));
|
||||
(void)memset(rp_s_rec_desc, 0, sizeof(rp_s_rec_desc));
|
||||
}
|
||||
|
||||
/* ---- adc_input_desc 桩:V0.1 权威映射(docs/硬件平台/ADC通道映射说明.md) ---- */
|
||||
#include "adc_input.h"
|
||||
|
||||
@@ -809,6 +873,93 @@ static void tc_dc_yk12(void)
|
||||
"TC-IEC104-024 an invalid DPC returns PN in the select phase");
|
||||
}
|
||||
|
||||
/* TC-IEC104-030(道F task-10):遥控记录——板端默认源 s_src_yk_exec() 的三条返回
|
||||
* 路径各投哪一类记录、投几句、是否顺手投了操作记录。
|
||||
* 为什么直接调 s_src_yk_exec 而不是发 ASDU:本套件绑定的是**假源** f_yk,
|
||||
* YK11 / 未知 IOA 在 f_yk 里就被短路(只有 YK12 转发到真板端源,见 f_yk),
|
||||
* 走 ASDU 反而测不到要验的那两条腿;"ASDU -> link -> pts -> 源"这条转发链
|
||||
* 已由 TC-IEC104-013/024 覆盖,不重复。**投递本身**(quiet 通道的收/丢)归
|
||||
* test/host/sys_log_mcu,本用例只钉"哪条路径投了哪一条"。
|
||||
* 覆盖不到的一条腿(登记,不假装):YK12 的 rc!=0 分支要 prot_set_set_sealed
|
||||
* 返 -1 才进得去,而 PS_GLOBAL_EN 的 min/max 恰好就是本函数会写的 0/1 两值
|
||||
* → 该产品路径上不可达,是防御性分支;不为它伪造定值范围(那是改判据)。 */
|
||||
static void tc_yk_records(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
|
||||
reset_all(now);
|
||||
rp_rec_clear();
|
||||
|
||||
/* 1) 未知 IOA:拒绝,只投一条遥控记录;拒绝没改动任何东西 -> 不该有操作记录 */
|
||||
CHECK(s_src_yk_exec(0x6000u, (uint8_t)IEC104_DPC_ON, 1u) == -1,
|
||||
"TC-IEC104-030 an IOA without output hardware is still rejected");
|
||||
CHECK((rp_s_rec_n == 1u) && (rp_rec_at(0u, SYSLOG_YK, "YK unknown ioa") == 1),
|
||||
"TC-IEC104-030 the reject path posts exactly one telecontrol record");
|
||||
CHECK(rp_rec_cnt(SYSLOG_OPER) == 0u,
|
||||
"TC-IEC104-030 a rejected command posts no operation record");
|
||||
|
||||
/* 2) YK11 选择段:受理但未动作 -> 只有遥控记录 */
|
||||
rp_rec_clear();
|
||||
CHECK((s_src_yk_exec(0x600Au, (uint8_t)IEC104_DPC_ON, 1u) == 0)
|
||||
&& (rp_s_rec_n == 1u) && (rp_rec_at(0u, SYSLOG_YK, "YK11 select") == 1),
|
||||
"TC-IEC104-030 the YK11 select phase posts a select record and acts on nothing");
|
||||
CHECK(rp_rec_cnt(SYSLOG_OPER) == 0u,
|
||||
"TC-IEC104-030 the select phase is not an operation yet");
|
||||
|
||||
/* 3) YK11 执行段:遥控记录 + 操作记录各一条,顺序 = 先命令后动作 */
|
||||
rp_rec_clear();
|
||||
CHECK((s_src_yk_exec(0x600Au, (uint8_t)IEC104_DPC_ON, 0u) == 0)
|
||||
&& (rp_s_rec_n == 2u)
|
||||
&& (rp_rec_at(0u, SYSLOG_YK, "YK11 exec") == 1)
|
||||
&& (rp_rec_at(1u, SYSLOG_OPER, "force reset") == 1),
|
||||
"TC-IEC104-030 the YK11 execute phase posts one telecontrol plus one operation record");
|
||||
|
||||
/* 4) YK12 非法双点值:拒绝 + 一条带原因的遥控记录,压板不动 */
|
||||
rp_rec_clear();
|
||||
CHECK((s_src_yk_exec(0x600Bu, 0u, 0u) == -1) && (rp_s_rec_n == 1u)
|
||||
&& (rp_rec_at(0u, SYSLOG_YK, "YK12 bad dpc") == 1),
|
||||
"TC-IEC104-030 an invalid DPC posts the reason and changes nothing");
|
||||
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u,
|
||||
"TC-IEC104-030 the invalid DPC leaves the soft plate engaged");
|
||||
|
||||
/* 5) YK12 执行 OFF:遥控 off + 操作 plate out,且压板真的退出了(记录与事实同步) */
|
||||
rp_rec_clear();
|
||||
CHECK((s_src_yk_exec(0x600Bu, (uint8_t)IEC104_DPC_OFF, 0u) == 0)
|
||||
&& (rp_s_rec_n == 2u)
|
||||
&& (rp_rec_at(0u, SYSLOG_YK, "YK12 exec off") == 1)
|
||||
&& (rp_rec_at(1u, SYSLOG_OPER, "plate out") == 1),
|
||||
"TC-IEC104-030 releasing the soft plate records both the command and the operation");
|
||||
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u,
|
||||
"TC-IEC104-030 the record agrees with the plate state the settings layer holds");
|
||||
|
||||
/* 6) YK12 执行 ON:对称的另一半(只测 OFF 的话,两条 desc 写反也发现不了) */
|
||||
rp_rec_clear();
|
||||
CHECK((s_src_yk_exec(0x600Bu, (uint8_t)IEC104_DPC_ON, 0u) == 0)
|
||||
&& (rp_rec_at(0u, SYSLOG_YK, "YK12 exec on") == 1)
|
||||
&& (rp_rec_at(1u, SYSLOG_OPER, "plate in") == 1),
|
||||
"TC-IEC104-030 engaging the soft plate records the matching pair");
|
||||
|
||||
/* 7) 选择段不落地:YK12 select 只投一条,压板状态不变 */
|
||||
rp_rec_clear();
|
||||
CHECK((s_src_yk_exec(0x600Bu, (uint8_t)IEC104_DPC_OFF, 1u) == 0)
|
||||
&& (rp_s_rec_n == 1u) && (rp_rec_at(0u, SYSLOG_YK, "YK12 select") == 1)
|
||||
&& (prot_set_get_ctl(PS_GLOBAL_EN) == 1u),
|
||||
"TC-IEC104-030 the YK12 select phase records the intent without changing the plate");
|
||||
|
||||
/* 8) 文案长度契约:desc 全是 ROM 常量,任何一条超过 SYSLOG_DESC_MAX 都会在
|
||||
* 投递侧被静默截断(屏上少几个字,没人会去数)-> 在单测里钉住。 */
|
||||
CHECK((strlen(YK_REC_UNKNOWN) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_YK11_EXEC) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_YK12_OFF) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_YK12_FAIL) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_DPC_BAD) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_YK12_SEL) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_YK11_SEL) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_YK12_ON) <= SYSLOG_DESC_MAX),
|
||||
"TC-IEC104-030 every telecontrol description fits the 28-byte record field");
|
||||
rp_rec_clear();
|
||||
}
|
||||
|
||||
static void tc_se(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
@@ -1283,6 +1434,7 @@ int main(void)
|
||||
tc_t3_t1();
|
||||
tc_dc();
|
||||
tc_dc_yk12();
|
||||
tc_yk_records();
|
||||
tc_se();
|
||||
tc_rd();
|
||||
tc_cs();
|
||||
|
||||
@@ -28,10 +28,17 @@
|
||||
* 否则对端会以为设备时间是 1970-01-01。
|
||||
* 4. 活性分频(每 32 轮才探一次)是 CPU 预算的取舍:写错成每轮都探,就把 100ms
|
||||
* 轮询变成了每轮两次系统调用;写错成分频相位,TESTFR 会迟迟不发。
|
||||
* 5. 操作记录是**审计面**(道F task-19):只要"投了"而不管"改成功了没有",屏上的
|
||||
* 操作记录页就会出现从没发生过的对时;越界拒绝与 RTC 写失败这两条腿必须
|
||||
* 一条都不投,而这只有能注入 sys_clock_set_unix 的返回值才测得到。
|
||||
* 同时钉住"用 quiet 口不用 post 口"——post 的两个满态留痕分支各带约 216 B
|
||||
* 内联行缓冲,iec104Task 的静态最坏链放不下它(台架与单测都看不出这个差别,
|
||||
* 只有 stack_report 会响,所以先在源码侧钉住)。
|
||||
*
|
||||
* 覆盖点:TC-I104SRV-001..004 建监听;010..013 发送与连续短写判死;020..023 时钟
|
||||
* 桥接;030..033 代次重建与 accept 门控;034..036 活性分频与对端 FIN;037..041
|
||||
* 错误丢弃与协议掉线重生。
|
||||
* 错误丢弃与协议掉线重生;050..053 对时的操作记录(道F task-19:成功才记、
|
||||
* 越界拒绝与写钟失败两条腿都不记、每次对时各记一条、且只走 quiet 投递口)。
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
@@ -194,6 +201,61 @@ static int g_wd_beats;
|
||||
static int g_wd_last_id;
|
||||
static uint32_t g_now;
|
||||
|
||||
/* ===================== sys_log 桩(task-19 操作记录) =====================
|
||||
* 两个投递口都抓下来:quiet 是 iec104Task 该用的那个(本任务栈深,不许在调用链上
|
||||
* 内联日志行缓冲),post 一旦在这里被调到就是"绕回旧口",053 会红。
|
||||
* 投递本身(收/丢/计数)归 test/host/sys_log_mcu,本套只钉"哪条腿投了哪一句"。 */
|
||||
#include "sys_log.h"
|
||||
|
||||
#define RP_I4_REC_MAX 8u
|
||||
static uint8_t rp_i4_id[RP_I4_REC_MAX];
|
||||
static char rp_i4_desc[RP_I4_REC_MAX][SYSLOG_DESC_MAX + 1u];
|
||||
static uint32_t rp_i4_n; /* quiet 通道累计(不回绕,供"恰好几条"断言) */
|
||||
static uint32_t rp_i4_post_n; /* 误用 syslog_post 的次数,期望恒 0 */
|
||||
|
||||
static void rp_i4_rec_clear(void)
|
||||
{
|
||||
rp_i4_n = 0u;
|
||||
rp_i4_post_n = 0u;
|
||||
(void)memset(rp_i4_id, 0, sizeof(rp_i4_id));
|
||||
(void)memset(rp_i4_desc, 0, sizeof(rp_i4_desc));
|
||||
}
|
||||
|
||||
void syslog_post_quiet(syslog_id_t id, const char *desc)
|
||||
{
|
||||
uint32_t slot = rp_i4_n % RP_I4_REC_MAX;
|
||||
size_t dl = 0u;
|
||||
|
||||
if (desc == NULL) {
|
||||
return;
|
||||
}
|
||||
while ((dl < SYSLOG_DESC_MAX) && (desc[dl] != '\0')) {
|
||||
dl++;
|
||||
}
|
||||
(void)memcpy(rp_i4_desc[slot], desc, dl);
|
||||
rp_i4_desc[slot][dl] = '\0';
|
||||
rp_i4_id[slot] = (uint8_t)id;
|
||||
rp_i4_n++;
|
||||
}
|
||||
|
||||
void syslog_post(syslog_id_t id, const char *desc)
|
||||
{
|
||||
(void)id;
|
||||
(void)desc;
|
||||
rp_i4_post_n++;
|
||||
}
|
||||
|
||||
uint32_t syslog_post_dropped(void) { return 0u; }
|
||||
|
||||
static int rp_i4_at(uint32_t k, syslog_id_t id, const char *desc)
|
||||
{
|
||||
if (k >= rp_i4_n) {
|
||||
return 0;
|
||||
}
|
||||
return (rp_i4_id[k % RP_I4_REC_MAX] == (uint8_t)id)
|
||||
&& (strcmp(rp_i4_desc[k % RP_I4_REC_MAX], desc) == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
uint32_t sys_clock_get(void) { return g_clock_val; }
|
||||
int sys_clock_set_unix(uint32_t unix_secs)
|
||||
{
|
||||
@@ -250,6 +312,7 @@ static void reset_all(void)
|
||||
g_lk_state = IEC104_ST_RUN;
|
||||
g_pts_init = 0;
|
||||
g_clock_val = 1700000000u;
|
||||
rp_i4_rec_clear();
|
||||
g_clock_set_rc = 0;
|
||||
g_clock_last_secs = 0u;
|
||||
g_clock_set_calls = 0;
|
||||
@@ -367,6 +430,38 @@ static void tc_clock(void)
|
||||
reset_all();
|
||||
g_clock_val = 1700000123u;
|
||||
CHECK(s_epoch_ms(NULL) == 1700000123000ULL, "TC-I104SRV-023 with a synced clock the answer is in milliseconds");
|
||||
|
||||
/* ---- task-19:对时 = 操作记录(审计面只记真发生过的动作) ---- */
|
||||
reset_all();
|
||||
(void)s_clock_sync(NULL, 1700000123000ULL);
|
||||
CHECK((rp_i4_n == 1u) && (rp_i4_at(0u, SYSLOG_OPER, "clock set by 104") == 1),
|
||||
"TC-I104SRV-050 a successful sync books exactly one operation record naming the source");
|
||||
|
||||
reset_all();
|
||||
(void)s_clock_sync(NULL, 0x100000000ULL * 1000ULL);
|
||||
CHECK((rp_i4_n == 0u) && (g_clock_set_calls == 0),
|
||||
"TC-I104SRV-050b the out-of-range rejection books nothing (an audit record for something that never happened is worse than none)");
|
||||
|
||||
reset_all();
|
||||
g_clock_set_rc = -1; /* 写钟失败(RTC 回写失败那一类) */
|
||||
CHECK(s_clock_sync(NULL, 1700000123000ULL) == -1,
|
||||
"TC-I104SRV-051 a failed clock write still answers the link layer with -1");
|
||||
CHECK(rp_i4_n == 0u,
|
||||
"TC-I104SRV-051b a failed clock write books no operation record");
|
||||
|
||||
reset_all();
|
||||
(void)s_clock_sync(NULL, 1700000123000ULL);
|
||||
(void)s_clock_sync(NULL, 1700000456000ULL);
|
||||
CHECK((rp_i4_n == 2u) && (rp_i4_at(1u, SYSLOG_OPER, "clock set by 104") == 1),
|
||||
"TC-I104SRV-052 every re-sync is its own operation record (no dedup, no latching)");
|
||||
|
||||
/* 053:用错投递口的形状——iec104Task 是深链任务,只许用 quiet。
|
||||
* 顺带量文案长度:超过 SYSLOG_DESC_MAX 会在投递侧被静默截断。 */
|
||||
reset_all();
|
||||
(void)s_clock_sync(NULL, 1700000123000ULL);
|
||||
CHECK((rp_i4_post_n == 0u) && (rp_i4_n == 1u)
|
||||
&& (strlen("clock set by 104") <= SYSLOG_DESC_MAX),
|
||||
"TC-I104SRV-053 the record goes out over the stack-safe channel only, and its text fits the record field");
|
||||
}
|
||||
|
||||
/* ===================== 代次重建与 accept 门控 ===================== */
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
* TC-PROTELEM-012 低频 UF(B8/L3-5):欠量方向 + RET_UF 滞回 + 方向隔离
|
||||
* TC-PROTELEM-013 run() 接线(B8):测频驱动 OF 动作;幅值门失效 OF 复位且
|
||||
* 相量类元件仍被评估(不整拍跳过)
|
||||
* TC-PROTELEM-014 启动位图与动作位图分离(道F task-10「保护启动」记录的前置):
|
||||
* 延时段已启动未到点时两枚位图必须不同 + 不变式 asserted ⊆ picked
|
||||
* TC-PROTELEM-015 动作边沿的记录分类(道F task-10「事故 / 保护告警」):
|
||||
* 逐项独立读 PS_*_TRIP、改定值即改类别、无该投退项的五项恒告警
|
||||
* (同时挡住 trip_ctl=0xFF 的越界读)、越界编号不落记录
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 创建日期:2026-10-11(B5);2026-10-12 扩 OF/UF(B8)
|
||||
@@ -442,6 +447,101 @@ static void tc_run_freq_wiring(void) {
|
||||
"TC-PROTELEM-013 run(): UV still evaluated with zero-amplitude (no whole-beat skip)");
|
||||
}
|
||||
|
||||
/* TC-PROTELEM-014(道F task-10):启动位图与动作位图**分离**。
|
||||
* 「保护启动」记录的边沿检测靠 pick_mask,若它等于 active_mask(或干脆没实现,
|
||||
* 让 protect_task 去轮询 state),"启动了但延时段还没到点"这一整段在记录里就是
|
||||
* 空的——而那恰恰是这一页唯一比"动作记录"多出来的信息。所以这里把两者的
|
||||
* **不等**关系钉死,并钉住不变式 asserted ⊆ picked。 */
|
||||
static void tc_pick_mask(void) {
|
||||
float m[PE_COUNT];
|
||||
|
||||
prot_set_init();
|
||||
prot_elm_init();
|
||||
(void)prot_set_set(PS_OC1_DLY, PROT_SET_Q10(100.0)); /* 延时段 */
|
||||
(void)prot_set_set(PS_OC3_DLY, PROT_SET_Q10(0.0)); /* 瞬时段 */
|
||||
mk_base(m);
|
||||
m[PE_OC1] = 10.1f; /* 越限,但 100 ms 未到 */
|
||||
m[PE_OC3] = 2.1f; /* 越限,dly=0 同拍动作 */
|
||||
|
||||
prot_elm_eval(6000u, m);
|
||||
CHECK(prot_elm_pick_mask() == ((1uL << PE_OC1) | (1uL << PE_OC3)),
|
||||
"TC-PROTELEM-014 pick mask carries the delayed element that has not tripped yet");
|
||||
CHECK(prot_elm_active_mask() == (1uL << PE_OC3),
|
||||
"TC-PROTELEM-014 active mask still excludes it (the two masks are genuinely different)");
|
||||
CHECK((prot_elm_active_mask() & ~prot_elm_pick_mask()) == 0u,
|
||||
"TC-PROTELEM-014 invariant: asserted is a subset of picked");
|
||||
|
||||
prot_elm_eval(6100u, m); /* OC1 定时限到点 */
|
||||
CHECK(prot_elm_active_mask() == ((1uL << PE_OC1) | (1uL << PE_OC3)),
|
||||
"TC-PROTELEM-014 after the delay the active mask catches up");
|
||||
CHECK(prot_elm_pick_mask() == ((1uL << PE_OC1) | (1uL << PE_OC3)),
|
||||
"TC-PROTELEM-014 pick mask unchanged while the level holds");
|
||||
|
||||
m[PE_OC1] = 9.4f; /* 跌破返回门槛 */
|
||||
m[PE_OC3] = 1.0f;
|
||||
prot_elm_eval(6200u, m);
|
||||
CHECK((prot_elm_pick_mask() == 0u) && (prot_elm_active_mask() == 0u),
|
||||
"TC-PROTELEM-014 both masks clear together below the return threshold");
|
||||
}
|
||||
|
||||
/* TC-PROTELEM-015(道F task-10):动作上升沿该归"事故"还是"保护告警"。
|
||||
* 判据的唯一来源是定值表里的跳闸投退项 PS_*_TRIP(枚举注释即"0 告警 / 1 跳闸"),
|
||||
* 元件->投退项的对应关系在 k_pe[] 的 trip_ctl 列。这里要钉的是三件容易做错的事:
|
||||
* a) **逐项独立**:OC1 投入、OC2 退出必须给出不同类别(做成一个全局开关、
|
||||
* 或在本函数里另写一份"哪些元件算跳闸"的名单,都会在这里翻红——而那份
|
||||
* 名单一旦与 g_prot_set_meta / k_pe[] 分叉,就是"整定视图与记录分类打架",
|
||||
* setview-check 也只认既有表);
|
||||
* b) **改定值即改类别**:同一元件同一状态,写 1 变事故、写 0 变告警,
|
||||
* 证明它是查表而不是编译期常量;
|
||||
* c) **没有该投退项的五项恒告警**,且**不会串到别人的字**:把全部 TRIP 项
|
||||
* 写成 1,OV/UV/3U0/OF/UF 仍然是告警——这条同时是 trip_ctl=0xFF 那格的
|
||||
* 越界读防线(若实现把 0xFF 直接喂给 prot_set_get_ctl(),读到的是定值区
|
||||
* 之外,类别会随内存内容漂,这条必红)。
|
||||
* 越界编号返回"不落任何记录"。 */
|
||||
static void tc_record_kind(void) {
|
||||
prot_set_init();
|
||||
prot_elm_init();
|
||||
|
||||
/* a) 逐项独立 */
|
||||
(void)prot_set_set(PS_OC1_TRIP, PROT_SET_Q10(1));
|
||||
(void)prot_set_set(PS_OC2_TRIP, PROT_SET_Q10(0));
|
||||
CHECK((prot_elm_record_kind(PE_OC1) == PROT_REC_TRIP)
|
||||
&& (prot_elm_record_kind(PE_OC2) == PROT_REC_ALARM),
|
||||
"TC-PROTELEM-015 the kind is read per element, not from one global switch");
|
||||
|
||||
/* b) 改定值即改类别(同元件、同状态) */
|
||||
(void)prot_set_set(PS_OC1_TRIP, PROT_SET_Q10(0));
|
||||
CHECK(prot_elm_record_kind(PE_OC1) == PROT_REC_ALARM,
|
||||
"TC-PROTELEM-015 releasing the trip word turns the same element into an alarm");
|
||||
(void)prot_set_set(PS_OC1_TRIP, PROT_SET_Q10(1));
|
||||
CHECK(prot_elm_record_kind(PE_OC1) == PROT_REC_TRIP,
|
||||
"TC-PROTELEM-015 re-engaging it turns it back into an accident (no latching)");
|
||||
(void)prot_set_set(PS_OC3_TRIP, PROT_SET_Q10(1));
|
||||
(void)prot_set_set(PS_OL_TRIP, PROT_SET_Q10(1));
|
||||
CHECK((prot_elm_record_kind(PE_OC3) == PROT_REC_TRIP)
|
||||
&& (prot_elm_record_kind(PE_OL) == PROT_REC_TRIP),
|
||||
"TC-PROTELEM-015 OC3/OL follow their own trip words too");
|
||||
|
||||
/* c) 无跳闸投退项的五项:全表写 1 之后仍恒告警(也挡住 0xFF 越界读) */
|
||||
(void)prot_set_set(PS_OC1_TRIP, PROT_SET_Q10(1));
|
||||
(void)prot_set_set(PS_OC2_TRIP, PROT_SET_Q10(1));
|
||||
(void)prot_set_set(PS_OC3_TRIP, PROT_SET_Q10(1));
|
||||
(void)prot_set_set(PS_OL_TRIP, PROT_SET_Q10(1));
|
||||
CHECK((prot_elm_record_kind(PE_OV) == PROT_REC_ALARM)
|
||||
&& (prot_elm_record_kind(PE_UV) == PROT_REC_ALARM)
|
||||
&& (prot_elm_record_kind(PE_3U0) == PROT_REC_ALARM)
|
||||
&& (prot_elm_record_kind(PE_OF) == PROT_REC_ALARM)
|
||||
&& (prot_elm_record_kind(PE_UF) == PROT_REC_ALARM),
|
||||
"TC-PROTELEM-015 alarm-only elements stay alarms with every trip word engaged");
|
||||
|
||||
/* 守卫:越界编号 = 一条都不许落 */
|
||||
CHECK(prot_elm_record_kind(PE_COUNT) == PROT_REC_NONE,
|
||||
"TC-PROTELEM-015 out-of-range id -> no record at all");
|
||||
/* 只读性:判定不改任何状态(protectTask 每拍都会问,问完还要跑判据) */
|
||||
CHECK(prot_elm_pick_mask() == 0u,
|
||||
"TC-PROTELEM-015 the classifier leaves the element states untouched");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== Prot Elm Tests ===\n");
|
||||
tc_init_clear();
|
||||
@@ -457,6 +557,8 @@ int main(void) {
|
||||
tc_of_hysteresis();
|
||||
tc_uf_direction();
|
||||
tc_run_freq_wiring();
|
||||
tc_pick_mask();
|
||||
tc_record_kind();
|
||||
|
||||
printf("\n=== Prot Elm Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
|
||||
@@ -7,7 +7,12 @@ r"""运行/自检记录落盘后端(sys_log_mcu)的主机侧单元测试运
|
||||
App/log/sys_log_mcu.c,并纳入共享假卷 test/host/common/fatfs_fake.c),在 PC 上离线
|
||||
跑 64B 定长记录格式、单条/批量两条写路径、syslog_drain 的 preboot+队列排空与
|
||||
"失败回投不丢条目"、日志读写放大收口的 8 条滑动读窗口 + 写者代数失效、挂载幂等与
|
||||
上电/重启分类、sys_log_evidence 的 wdev.log 取证通道,失败即非 0 退出。
|
||||
上电/重启分类、sys_log_evidence 的 wdev.log 取证通道、「清除记录」的请求/落地分离
|
||||
与四条判据(请求侧零盘触点、drain 内恰好一次 unlink、删后重建可继续写、
|
||||
FR_NO_FILE 幂等返 0、卷占用/真失败返 -1 且不误删原文件)、以及七类记录(道F
|
||||
task-10 新增 accid/alarm/start/oper/yk 五类)的存储面——逐 id 验"哪类落哪个
|
||||
文件、各用各的容量上限、名字过严格 8.3",以及 quiet 投递通道的丢弃计数与
|
||||
"投递侧零留痕",失败即非 0 退出。
|
||||
工具链与判定委托 test/host/common/unit_test_harness.py。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
@@ -20,7 +25,11 @@ r"""运行/自检记录落盘后端(sys_log_mcu)的主机侧单元测试运
|
||||
3. **挂载幂等**:syslog_mcu_mounted 曾在建 /log 失败后永久 no-op(运行期日志与自检
|
||||
全部滞留),改成幂等 + 重试后又必须防"每 2s 刷一条同样错误" → 要注入 mkdir
|
||||
失败才测得到,且必须能数出"到底留痕几次"。
|
||||
4. **读窗口 try 语义 / 日志读写放大收口的读侧**:窗口"命中即零 I/O"与"写者一落地就失效"是一枚硬币两面,
|
||||
4. **「清除记录」的请求 / 落地分离(2026-10-26 加)**:在 UI 按键回调里直接 f_unlink
|
||||
也能删掉文件、台架当场看不出毛病,代价是把 FAT 目录项回写挂进 uiTask 的
|
||||
100 ms 喂狗节拍、并与唯一写者自己的"容量满 unlink 重建"交错。这条边界只能靠
|
||||
数调用钉住:请求侧零 FatFs / 零卷锁、删除只在 syslog_drain 内且恰好一次 unlink。
|
||||
5. **读窗口 try 语义 / 日志读写放大收口的读侧**:窗口"命中即零 I/O"与"写者一落地就失效"是一枚硬币两面,
|
||||
只做前者会把陈旧读当正确、只做后者会退回逐条 open/seek/close 的旧实现。
|
||||
|
||||
桩的边界:ff.h / sys_log.h / soe_store.h / sys_clock.h / vol_lock.h 用**真实头**;
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
* 失败回投不丢条目、-3 退回慢路径)
|
||||
* D 读侧(日志读写放大收口的 8 条滑动窗口 + 写者代数失效 + 越界/忙/短读处置)
|
||||
* 外加 syslog_mcu_mounted 的 挂载幂等与上电/重启分类、sys_log_evidence
|
||||
* 的 wdev.log 取证通道(8KB 护栏 + 第二条写必须在释放卷锁之后)。
|
||||
* 的 wdev.log 取证通道(8KB 护栏 + 第二条写必须在释放卷锁之后),
|
||||
* 以及「清除记录」的请求 / 落地分离与四条判据(K 族,2026-10-26)。
|
||||
*
|
||||
* 被测对象:真实源文件 App/log/sys_log_mcu.c(#include 原样纳入,不改生产代码)。
|
||||
* 假件与桩:
|
||||
@@ -28,6 +29,20 @@
|
||||
* 4. **读窗口 try 语义 / 日志读写放大收口的读侧**:读窗口"命中即零 I/O"与"写者一落地就必须失效重填"是同一
|
||||
* 枚硬币的两面——只做前者会把陈旧读当正确,只做后者会退化成逐条 open/seek/close
|
||||
* 的旧实现(UI 翻 20 条 = 20 次 FAT 往返)。两者都靠假卷的调用计数才断言得了。
|
||||
* 5. **「清除记录」的请求 / 落地分离**(2026-10-26 加,K 族):最顺手的写法是在 UI
|
||||
* 按键回调里直接 f_unlink —— 文件确实删了,台架当场也看不出毛病,代价是把 FAT
|
||||
* 目录项回写挂进 uiTask 每 100 ms 一次的喂狗节拍,并与唯一写者自己的"容量满
|
||||
* unlink 重建"交错。这条"谁在什么上下文碰盘"的边界,只有离线数调用才钉得住:
|
||||
* 请求侧必须零 FatFs / 零卷锁(TC-SYSLOG-110),删除必须恰好一次 unlink 且
|
||||
* 发生在排空之后(TC-SYSLOG-111/117)。真机上这条写错的表现为偶发丢一段日志
|
||||
* 或一次误复位,串口里没有任何一行会指着这处代码。
|
||||
* 6. **一张表的下标序 = 枚举序**(2026-10-26 加,L 族):记录类型就是"哪个文件",
|
||||
* 于是 s_paths[]/s_caps[] 与 syslog_id_t 的**逐项同序**成了正确性本身。少写一项
|
||||
* 编译期就报,写错序却什么都不报错——事故记录会安静地写进告警文件,屏上一切
|
||||
* 正常,只有查记录的人被误导。这类"顺序即语义"的耦合只能靠逐 id 断言"我投的
|
||||
* 这句出现在哪个路径、别的文件里没有"来钉(TC-SYSLOG-130),容量同理(131)。
|
||||
* quiet 投递通道也在这一族:它存在的唯一理由是"投递侧不许带日志行缓冲",
|
||||
* 所以必须断言**丢弃时零留痕**(132/135),否则退化回 syslog_post 也没人发现。
|
||||
*
|
||||
* 覆盖点(TC 号族 SYSLOG,形如 TC-SYSLOG-001,族内按功能分档;缺口在小节末尾如实登记):
|
||||
* —— A 记录格式 ——
|
||||
@@ -129,11 +144,69 @@
|
||||
* TC-SYSLOG-081 生产路径全过严格 8.3:/log /run.log /selftest.log /wdev.log /soe.log
|
||||
* TC-SYSLOG-082 FR_INVALID_NAME 反复失败 20 轮:unlink 次数有界、/log 目录与
|
||||
* 邻居文件一根毛没动(自愈只针对自己的文件)
|
||||
* —— K 记录删除(「清除记录」前置,2026-10-26)——
|
||||
* TC-SYSLOG-110 请求侧只登记:零卷锁、零 FatFs、记录还在盘上(uiTask 唯一被
|
||||
* 允许做的事就是发这个请求)
|
||||
* TC-SYSLOG-111 落地在 syslog_drain(defaultTask)内:恰好一次 unlink、零 open、
|
||||
* count 归零、卷锁配对、邻居文件不动、零留痕
|
||||
* TC-SYSLOG-112 删后重建可继续写:清除后下一条记录把文件建回来、打开模式带写位、
|
||||
* 64/64 写全、读侧从 idx 0 读到新内容
|
||||
* TC-SYSLOG-113 FR_NO_FILE 幂等返 0:无留痕、卷锁配对、读侧代数照常自增
|
||||
* TC-SYSLOG-114 卷占用返 -1:零盘触点、不 release(没拿到锁)、不计故障、原文件一字节不少
|
||||
* TC-SYSLOG-115 请求路径遇卷忙 = 静默顺延(WAIT 不是 FAIL、请求挂回、零留痕),
|
||||
* 下一轮卷空闲自己落地(用户不必重按)
|
||||
* TC-SYSLOG-116 unlink 真失败 -> -1 + 留痕一次 + 文件完好;请求侧判 FAIL 且
|
||||
* **绝不自动重试**(反复删自己的日志 = 写侧刚修掉的坑,别再造一遍)
|
||||
* TC-SYSLOG-117 drain 内顺序本身也是判据:本轮排空出来的旧记录一起被清掉,
|
||||
* 删除之后新投的条目仍落盘(清除不等于把日志关掉)
|
||||
* TC-SYSLOG-118 同 id 重复请求合并为一次 unlink(请求是标志,不是计数器)
|
||||
* TC-SYSLOG-119 只删自己:清 RUN 不碰 SELFTEST,也不碰取证文件 wdev.log
|
||||
* TC-SYSLOG-120 三处入口的入参守卫都在取锁之前;坏 id 不污染合法 id 的状态
|
||||
* TC-SYSLOG-121 删除成功 -> 读侧代数自增(屏上"已清除"而窗口还装着旧记录 = 错数据);
|
||||
* 删除失败 -> 代数不变、缓存记录仍可读(盘上什么都没动)
|
||||
* TC-SYSLOG-122 /log 目录不在 -> FR_NO_PATH **不算**幂等成功:-1 + 一次留痕 +
|
||||
* 零 f_mkdir(名字/参数类码不得升级成卷级动作);目录回来后 FAIL 不粘
|
||||
*
|
||||
* 覆盖率台账(--module sys_log_mcu 自量):App/log/sys_log_mcu.c 310/313 行 =
|
||||
* 99.0%、函数 12/12。未覆盖 3 行:L628(下面第三条死分支)、L727~728
|
||||
* (sys_log_evidence 里 snprintf 返回 <=0 的分支——宿主 snprintf 不可能失败,
|
||||
* 假件也不伪造返回值;为刷数字去伪造等于把断言写在假件规则上,宁可留空)。
|
||||
* —— L 五类记录与 quiet 投递通道(道F task-10,2026-10-26)——
|
||||
* TC-SYSLOG-130 七类记录各进各的文件、各带各的描述:**id -> 文件的表序就是语义**
|
||||
* TC-SYSLOG-130b 卷里恰好七个记录文件,没有第八个拼错名字被顺手建出来的
|
||||
* TC-SYSLOG-130c 七条生产路径逐条过严格 8.3(主名<=8 / 扩展=3 / 不以点开头)
|
||||
* TC-SYSLOG-131 accid 到自己那格(512)轮转,邻居 alarm 一字节没动
|
||||
* TC-SYSLOG-131b yk 用的是自己那格(256)——三档上限互不相同,能照出 s_caps[] 串格
|
||||
* TC-SYSLOG-132 quiet 通道队列满:计数前进、投递侧**零留痕**、drain 侧按差值报一次
|
||||
* TC-SYSLOG-133 清除记录 = 一次运维动作 -> 落一条操作记录点名被清的那份;被清那份仍归零
|
||||
* TC-SYSLOG-133b 清 oper.log 自己时**不投**操作记录(否则刚删空又变 1 条,砸掉归零判据)
|
||||
* TC-SYSLOG-134 quiet 的入参守卫:坏 id / NULL 既不投也不计丢弃(参数错 != 容量故障)
|
||||
* TC-SYSLOG-135 启动前数组满同样计数、同样静默(另一条腿,与 F 项的两个通道分区一致)
|
||||
* TC-SYSLOG-136 同内容在七类之间**逐字节相同**:记录体不编码类型 = "文件即类型"的机器表达
|
||||
*
|
||||
* 覆盖率台账(`scripts/check_test_coverage.py --module sys_log_mcu` 本机实跑):
|
||||
* App/log/sys_log_mcu.c **403/406 行 = 99.3%、函数 22/22**。
|
||||
* 上一轮(K 族)是 377/380 行、函数 18/18 —— L 族新增 4 枚函数
|
||||
* (s_entry_fill / s_put / syslog_post_quiet / syslog_post_dropped)与 26 行
|
||||
* 可执行代码**全部**有断言跑到:覆盖行 +26、函数 +4,未覆盖仍是同一批 3 行老账
|
||||
* (一条死分支 + sys_log_evidence 里 snprintf<=0),**一条没涨**。
|
||||
* ⚠ 全量门禁(40 套并集 + 基线只增不减)由提交前那一次统一跑,本行只是自量记录。
|
||||
* L 族变异实测(2026-10-26,三处同时注入后逐字节还原):
|
||||
* ① s_paths[] 里 accid 与 alarm 两格**对调**(就是"表序错"那个静默错法);
|
||||
* ② 给 quiet 的丢弃分支补一条 `DBG_ERROR`(= 悄悄退化成 syslog_post);
|
||||
* ③ 去掉"清 oper.log 自己不投操作记录"那半枚守卫。
|
||||
* → 137 掉到 132:**TC-SYSLOG-130 / 131**(①)、**132 / 135**(②)、
|
||||
* **133b**(③)各自转红且互不牵连——每条新纪律都被单独钉住,不是靠一锅大断言。
|
||||
* App/log/sys_log_mcu.c **377/380 行 = 99.2%、函数 18/18**。
|
||||
* 本轮(K 族)之前是 328/330 行、函数 13/13 —— 新增 5 个函数
|
||||
* (s_clear_try / s_clear_service / syslog_clear_apply / syslog_request_clear /
|
||||
* syslog_clear_state)与 50 行新增可执行代码(已覆盖 +49):新增行**全部**有断言
|
||||
* 跑到,未覆盖仍是 3 行,一条没涨。
|
||||
* 仍未覆盖的 3 行是同一批老账:一条死分支 + sys_log_evidence 里 snprintf 返回
|
||||
* <=0 的分支——宿主 snprintf 不可能失败,假件也不伪造返回值;为刷数字去伪造
|
||||
* 等于把断言写在假件规则上,宁可留空。
|
||||
* 变异实测("断言有牙齿"的证据,2026-10-26,改完已逐字节还原):
|
||||
* ① 在排空循环**之前**多调一次 `s_clear_service()`(净效果 = 删除抢在条目落盘之前
|
||||
* 执行,尾部那次因请求已销账而空转);② 把 `FR_NO_PATH` 也算成幂等成功。
|
||||
* 两处一起改 → TC-SYSLOG-115 / 115b / 117 / 117b / 122 **五条转红**(126 → 121),
|
||||
* 还原后复绿 126/126。也就是说"删除必须排在条目排空之后落地"与"名字/参数类码
|
||||
* 不算幂等成功"这两条本来只写在注释里的纪律,现在真的会被测试拦住。
|
||||
*
|
||||
* 本套照出的四条现状 —— 已在 task-4(2026-10-24)全部处置,相应断言已翻成正向:
|
||||
*
|
||||
@@ -172,6 +245,18 @@
|
||||
* (TC-SOESTORE-022 / 022b),两侧的差异是刻意的,注释也互指。
|
||||
*
|
||||
* 故意没测(写在这里,不留"看起来全覆盖"的错觉):
|
||||
* - 真机上 uiTask 发请求与 defaultTask 落地的并发:主机单线程编不出真竞态,本套只
|
||||
* 钉"请求侧零盘触点 + 落地只在 drain 里"这两条形状;两个 volatile 单字节交接的
|
||||
* 最坏竞态(读到上一次的结果 / 失败被新请求清掉)写在 sys_log.h 的函数头里。
|
||||
* - quiet 通道与 post 通道在**同一轮**里交错时的抑制位归属(quiet 成功不复位 F 项
|
||||
* 那两个位):这是刻意的设计(那两个位描述 post 自己的历史),TC-SYSLOG-107c 已
|
||||
* 钉住两位互不干扰;"quiet 投成功把 post 的故障静默位清了"这种错向,本套没有
|
||||
* 单独一条——要钉它得先给两个通道各造一套满态时序,收益不抵这套的复杂度。
|
||||
* - 五类记录各自的**业务触发时序**(哪个元件边沿算事故、哪条 104 命令算操作)不在
|
||||
* 本套:那是判据与协议侧的事,分别由 test/host/prot_elm 的 TC-PROTELEM-015 与
|
||||
* test/host/iec104 的 TC-IEC104-030 钉;本套只管"投进来以后存储对不对"。
|
||||
* - 删除之后"下一次上电会不会多写一条 power on":那是 syslog_mcu_mounted() 的
|
||||
* 上电分类逻辑(E 族已测),与删除路径无关,两边各测各的。
|
||||
* - 真跨任务并发:读窗口用"对齐 32 位读写在 ARM 上原子"免锁,主机 x64 + 单线程
|
||||
* 编不出真竞态;本套只断言单写者语义与代数失效逻辑本身。
|
||||
* - f_close 内部 sync_fs -> disk_ioctl(CTRL_SYNC) -> ftl_flush_cache() 的掉电落盘
|
||||
@@ -218,6 +303,12 @@ static int s_failed;
|
||||
#define P_RUN "/log/run.log"
|
||||
#define P_SELFTEST "/log/selftest.log"
|
||||
#define P_WDEV "/log/wdev.log"
|
||||
/* 五类新记录(task-10,L 族):生产用的就是这七条路径,改名必须同步这里 */
|
||||
#define P_ACCID "/log/accid.log"
|
||||
#define P_ALARM "/log/alarm.log"
|
||||
#define P_START "/log/start.log"
|
||||
#define P_OPER "/log/oper.log"
|
||||
#define P_YK "/log/yk.log"
|
||||
|
||||
/* ===================== 假件与生产源码纳入 ===================== */
|
||||
|
||||
@@ -494,18 +585,28 @@ static void reset_all(void)
|
||||
s_queue = NULL;
|
||||
s_fs_ready = 0u;
|
||||
s_mount_fail_reported = 0u;
|
||||
/* B / F 项整改新增的留痕节流位:跨场景必须显式清(否则第二个故障场景看不到留痕) */
|
||||
s_requeue_reported[SYSLOG_RUN] = 0u;
|
||||
s_requeue_reported[SYSLOG_SELFTEST] = 0u;
|
||||
/* B / F 项整改新增的留痕节流位:跨场景必须显式清(否则第二个故障场景看不到留痕)。
|
||||
* ⚠ 按 SYSLOG_COUNT 循环清,不要逐 id 手写——task-10 把 id 从 2 扩到 7 之后,
|
||||
* 任何"逐枚点名"的清理都会静默漏掉新增的那几格,症状是"单独跑绿、连跑偶红"。 */
|
||||
(void)memset(s_requeue_reported, 0, sizeof(s_requeue_reported));
|
||||
s_post_qfull_reported = 0u;
|
||||
s_post_pboot_full_reported = 0u;
|
||||
/* quiet 投递通道(L 族):丢弃计数与 drain 侧已上报水位都要归零,
|
||||
* 否则上一个场景的丢弃会让本场景第一条留痕被当成"已经报过" */
|
||||
s_quiet_drop = 0u;
|
||||
s_quiet_reported = 0u;
|
||||
(void)memset(s_rd_win, 0, sizeof(s_rd_win));
|
||||
s_rd_win_id = -1;
|
||||
s_rd_win_base = 0u;
|
||||
s_rd_win_cnt = 0u;
|
||||
s_rd_win_gen = 0u;
|
||||
s_rd_gen[SYSLOG_RUN] = 0u;
|
||||
s_rd_gen[SYSLOG_SELFTEST] = 0u;
|
||||
(void)memset(s_rd_gen, 0, sizeof(s_rd_gen)); /* 同上:按数组清,扩 id 不会漏 */
|
||||
/* 「清除记录」的请求 / 结果交接(本套新增,K 族):两枚都必须显式清,
|
||||
* 否则上一个场景留下的 WAIT/FAIL 会让下一个场景第一眼就读到错状态 */
|
||||
/* 「清除记录」的请求 / 结果交接(K 族):两枚按数组清,理由同 s_rd_gen——
|
||||
* 五类新记录每一类都可被清除,漏清一格就是上一场景的 WAIT/FAIL 咬下一场景。 */
|
||||
(void)memset(s_clear_req, 0, sizeof(s_clear_req));
|
||||
(void)memset(s_clear_st, 0, sizeof(s_clear_st)); /* 0 == SYSLOG_CLEAR_NONE */
|
||||
/* 卷状态:/log 由 defaultTask 挂载时建好;本套多数场景直接假定已建,
|
||||
* "没建 -> 补建" 那条路径单独用 TC-SYSLOG-060/061 测 */
|
||||
(void)ff_fake_mkdir_direct("/log");
|
||||
@@ -1838,6 +1939,469 @@ static void tc_post_throttle(void)
|
||||
"TC-SYSLOG-107c the two channels keep independent throttle bits");
|
||||
}
|
||||
|
||||
/* ===================== K 记录删除(「清除记录」前置) ===================== */
|
||||
|
||||
/*
|
||||
* 这一族钉的是"谁在什么上下文把文件删掉",加上删除本身的四条判据:
|
||||
* 计数归零(111)、文件不在时幂等返 0(113)、卷占用/真失败返 -1 且不误删原文件
|
||||
* (114/116)、删后重建可继续写(112)。
|
||||
* 为什么值得离线测:最顺手的写法是在 UI 按键回调里直接 f_unlink —— 台架上当场看不出
|
||||
* 毛病,代价是把 FAT 目录项回写挂进 uiTask 每 100 ms 一次的喂狗节拍,并与唯一写者
|
||||
* 自己的"容量满 unlink 重建"交错。请求/落地分开之后,这条边界只能靠"数请求侧的盘
|
||||
* 触点"和"数删除发生在哪一轮"钉住(110/111/117 干的就是这件事)。
|
||||
* 一处限度说清楚:本目录的卷锁假件不记录超时入参,所以"绝不阻塞"只能断言成
|
||||
* "try 失败 = 零盘触点 + 不 release + 不计故障"(114),测不到传进去的超时值到底
|
||||
* 是不是 0;要测那一层得给共用的卷锁假件加一列观测,不属本套的范围。
|
||||
*/
|
||||
static void tc_clear(void)
|
||||
{
|
||||
char ts[SYSLOG_TS_LEN + 1u];
|
||||
char desc[SYSLOG_DESC_MAX + 1u];
|
||||
uint8_t rec[SYSLOG_REC_SIZE];
|
||||
uint32_t i;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-SYSLOG-110..121 record clear (request / land split) --\n");
|
||||
|
||||
/* 110 请求侧:登记就是登记 —— 一把锁都不取、一个 FatFs 调用都没有、记录还在盘上。
|
||||
* 这是"UI 只能发请求"的全部技术含义:按键回调在 uiTask,那侧不许碰存储。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)s_append(SYSLOG_RUN, "old1");
|
||||
(void)s_append(SYSLOG_RUN, "old2");
|
||||
{
|
||||
int open_base = g_ff_obs.calls[FF_FAKE_OP_OPEN];
|
||||
int unlink_base = g_ff_obs.calls[FF_FAKE_OP_UNLINK];
|
||||
int lock_base = s_lock_waits;
|
||||
int rel_base = s_lock_releases;
|
||||
ok = (syslog_request_clear(SYSLOG_RUN) == 0)
|
||||
&& (syslog_count(SYSLOG_RUN) == 2u)
|
||||
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_WAIT)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == open_base)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlink_base)
|
||||
&& (s_lock_waits == lock_base)
|
||||
&& (s_lock_releases == rel_base);
|
||||
CHECK(ok, "TC-SYSLOG-110 request only books the ask: zero volume lock, zero FatFs, records still on the volume");
|
||||
}
|
||||
|
||||
/* 111 落地发生在 drain(defaultTask)里:恰好一次 unlink(不 open)、计数归零、
|
||||
* 锁成对、零留痕,且邻居文件 selftest.log 一字节没动 */
|
||||
(void)s_append(SYSLOG_SELFTEST, "neighbour");
|
||||
{
|
||||
int open_base = g_ff_obs.calls[FF_FAKE_OP_OPEN];
|
||||
syslog_drain();
|
||||
ok = (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK)
|
||||
&& (syslog_count(SYSLOG_RUN) == 0u)
|
||||
&& (ff_fake_exists(P_RUN) == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||||
&& (strcmp(g_ff_obs.last_unlink_path, "/LOG/RUN.LOG") == 0) /* 观测位存的是归一化后的 8.3 大写名 */
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == open_base)
|
||||
&& (s_lock_seq_err == 0)
|
||||
&& (g_sys_err_count == 0)
|
||||
&& (ff_fake_exists(P_SELFTEST) == 1)
|
||||
&& (ff_fake_size(P_SELFTEST) == SYSLOG_REC_SIZE);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-111 drain lands it: one unlink and zero open, count 0, lock paired, no trace, neighbour intact");
|
||||
|
||||
/* 112 判据"删后重建可继续写":文件被删之后,下一条记录必须把文件建回来并写全。
|
||||
* 陷阱一在这里才有牙齿 —— 只有"文件不存在"这一条路才走 f_open 的创建分支,
|
||||
* 少了 FA_WRITE 位,平时写得好好的,清除之后从此每条静默写不进去。 */
|
||||
ok = (s_append(SYSLOG_RUN, "after-clear") == 0)
|
||||
&& (ff_fake_exists(P_RUN) == 1)
|
||||
&& (syslog_count(SYSLOG_RUN) == 1u)
|
||||
&& (g_ff_obs.last_open_mode == (BYTE)(FA_WRITE | FA_OPEN_APPEND))
|
||||
&& (g_ff_obs.last_write_bw == SYSLOG_REC_SIZE);
|
||||
CHECK(ok, "TC-SYSLOG-112 after a clear the next record recreates the file with the write bit set and lands 64/64 bytes");
|
||||
ok = ok && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
|
||||
&& (strcmp(desc, "after-clear") == 0);
|
||||
CHECK(ok, "TC-SYSLOG-112b the recreated file is readable from index 0 (cleared history never comes back)");
|
||||
|
||||
/* 113 幂等:文件本来就不存在 -> 返 0、只留一次 unlink、零留痕、卷锁照样配对 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
{
|
||||
int rel_base = s_lock_releases;
|
||||
uint32_t gen = s_rd_gen[SYSLOG_RUN];
|
||||
ok = (ff_fake_exists(P_RUN) == 0)
|
||||
&& (syslog_clear_apply(SYSLOG_RUN) == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||||
&& (g_sys_err_count == 0)
|
||||
&& (s_lock_releases == rel_base + 1)
|
||||
&& (s_lock_seq_err == 0)
|
||||
&& (s_rd_gen[SYSLOG_RUN] == gen + 1u);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-113 FR_NO_FILE is an idempotent success: 0 back, no trace, lock released, window bumped");
|
||||
|
||||
/* 114 卷占用:公开 API 返 -1,一个盘操作都没有、不 release(没拿到锁)、不计故障、
|
||||
* 原文件一字节不少。"try 失败不许 release"是漏锁 = FTP 永久等不到卷的老账。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)s_append(SYSLOG_RUN, "keepme");
|
||||
{
|
||||
int open_base = g_ff_obs.calls[FF_FAKE_OP_OPEN];
|
||||
int unlink_base = g_ff_obs.calls[FF_FAKE_OP_UNLINK];
|
||||
int rel_base = s_lock_releases;
|
||||
lock_busy_next(1);
|
||||
ok = (syslog_clear_apply(SYSLOG_RUN) == -1)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == open_base)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlink_base)
|
||||
&& (s_lock_releases == rel_base)
|
||||
&& (s_lock_seq_err == 0)
|
||||
&& (g_sys_err_count == 0)
|
||||
&& (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE)
|
||||
&& (syslog_count(SYSLOG_RUN) == 1u);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-114 busy volume -> -1 with zero disk touch, no release, no fault counted, original file intact");
|
||||
|
||||
/* 115 走请求路径时卷占用 = 静默顺延,不是失败:状态仍是 WAIT、请求挂回、零留痕;
|
||||
* 下一轮卷空闲后同一个请求自己落地(用户不必重按一次按钮)。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
syslog_drain(); /* 建队列,本轮空转 */
|
||||
(void)s_append(SYSLOG_RUN, "keepme");
|
||||
(void)syslog_request_clear(SYSLOG_RUN);
|
||||
lock_busy_next(1);
|
||||
g_sys_err_count = 0;
|
||||
syslog_drain(); /* 这一轮的清除拿锁失败 */
|
||||
ok = (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_WAIT)
|
||||
&& (ff_fake_exists(P_RUN) == 1)
|
||||
&& (g_sys_err_count == 0)
|
||||
&& (s_lock_seq_err == 0);
|
||||
CHECK(ok, "TC-SYSLOG-115 busy while landing defers: ask kept, state WAIT (not FAIL), file untouched, silent");
|
||||
syslog_drain(); /* 卷空闲 -> 落地 */
|
||||
ok = ok && (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK)
|
||||
&& (syslog_count(SYSLOG_RUN) == 0u);
|
||||
CHECK(ok, "TC-SYSLOG-115b the deferred ask lands on the next round without a second press");
|
||||
|
||||
/* 116 真失败(非 FR_NO_FILE):apply 返 -1 + 留痕一次;请求侧判 FAIL 且绝不自动
|
||||
* 重试 —— 反复 unlink 自己的日志文件是本项目在写侧刚修掉的坑(名字/参数类
|
||||
* 错误码同名同因,重试只会每轮删一次历史),删除路径不许再造一遍。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)s_append(SYSLOG_RUN, "precious");
|
||||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_DISK_ERR, 4);
|
||||
ok = (syslog_clear_apply(SYSLOG_RUN) == -1)
|
||||
&& (g_sys_err_count == 1)
|
||||
&& (ff_fake_exists(P_RUN) == 1)
|
||||
&& (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE);
|
||||
CHECK(ok, "TC-SYSLOG-116 a real unlink error -> -1, one trace, the log file itself is still there");
|
||||
reset_all();
|
||||
ready_only();
|
||||
syslog_drain();
|
||||
(void)s_append(SYSLOG_RUN, "precious");
|
||||
(void)syslog_request_clear(SYSLOG_RUN);
|
||||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_DISK_ERR, 4);
|
||||
syslog_drain();
|
||||
{
|
||||
int unlink_base = g_ff_obs.calls[FF_FAKE_OP_UNLINK];
|
||||
ok = (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_FAIL)
|
||||
&& (ff_fake_exists(P_RUN) == 1);
|
||||
syslog_drain(); /* 不许自己再删一次 */
|
||||
ok = ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlink_base)
|
||||
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_FAIL);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-116b a failed clear latches FAIL and is never retried on its own (re-press is the retry)");
|
||||
|
||||
/* 117 drain 内的顺序是判据的一部分:本轮排空出来的旧记录必须一起被清掉
|
||||
* (先删后写 = 白清,count 不归零);而删除**之后**投的条目要留下
|
||||
* (否则"清除记录"变成了"关掉日志")。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain(); /* 建队列 */
|
||||
for (i = 0u; i < 3u; i++) {
|
||||
syslog_post(SYSLOG_RUN, "old");
|
||||
}
|
||||
(void)syslog_request_clear(SYSLOG_RUN);
|
||||
syslog_drain(); /* 排空 3 条 -> 然后删除 */
|
||||
ok = (syslog_count(SYSLOG_RUN) == 0u) && (ff_fake_exists(P_RUN) == 0)
|
||||
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK);
|
||||
CHECK(ok, "TC-SYSLOG-117 the clear runs after the entry drain: the queued backlog does not survive it");
|
||||
syslog_post(SYSLOG_RUN, "new");
|
||||
syslog_drain();
|
||||
ok = ok && (syslog_count(SYSLOG_RUN) == 1u)
|
||||
&& (raw_at(P_RUN, 0u, rec) != 0)
|
||||
&& (memcmp(rec + SYSLOG_DESC_OFFS, "new", 3u) == 0);
|
||||
CHECK(ok, "TC-SYSLOG-117b a record posted after the clear still lands: clearing is not disabling logging");
|
||||
|
||||
/* 118 同一 id 连点两次只登记一次(合并成一次删除,不是计数器) */
|
||||
reset_all();
|
||||
ready_only();
|
||||
ok = (syslog_request_clear(SYSLOG_RUN) == 0) && (syslog_request_clear(SYSLOG_RUN) == 0)
|
||||
&& (s_clear_req[SYSLOG_RUN] == 1u);
|
||||
syslog_drain();
|
||||
ok = ok && (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||||
&& (s_clear_req[SYSLOG_RUN] == 0u);
|
||||
CHECK(ok, "TC-SYSLOG-118 duplicate asks merge into exactly one unlink (the ask is a flag, not a counter)");
|
||||
|
||||
/* 119 只删自己:清 RUN 不许碰 SELFTEST,也不许碰取证文件 wdev.log ——
|
||||
* 三个名字都是严格 8.3 短名,逐个对应,不存在"通配删除"。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)s_append(SYSLOG_RUN, "r");
|
||||
(void)s_append(SYSLOG_SELFTEST, "s");
|
||||
(void)ff_fake_mkfile_size(P_WDEV, 128u);
|
||||
ok = (syslog_clear_apply(SYSLOG_RUN) == 0)
|
||||
&& (ff_fake_exists(P_RUN) == 0)
|
||||
&& (ff_fake_exists(P_SELFTEST) == 1)
|
||||
&& (ff_fake_exists(P_WDEV) == 1)
|
||||
&& (strcmp(g_ff_obs.last_unlink_path, "/LOG/RUN.LOG") == 0);
|
||||
CHECK(ok, "TC-SYSLOG-119 clearing one id never touches the other log or the watchdog evidence file");
|
||||
|
||||
/* 120 入参守卫:三处公开入口都必须在取锁之前挡住,并且不污染合法 id 的状态 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
ok = (syslog_request_clear((syslog_id_t)SYSLOG_COUNT) == -1)
|
||||
&& (syslog_clear_apply((syslog_id_t)SYSLOG_COUNT) == -1)
|
||||
&& (syslog_clear_state((syslog_id_t)99u) == SYSLOG_CLEAR_NONE)
|
||||
&& (s_lock_waits == 0)
|
||||
&& (s_lock_releases == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0);
|
||||
CHECK(ok, "TC-SYSLOG-120 out-of-range id is refused before the lock and the disk (a guard behind the lock occupies the volume for nothing)");
|
||||
reset_all();
|
||||
ready_only();
|
||||
ok = (syslog_request_clear(SYSLOG_RUN) == 0)
|
||||
&& (syslog_request_clear((syslog_id_t)SYSLOG_COUNT) == -1)
|
||||
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_WAIT)
|
||||
&& (syslog_clear_state((syslog_id_t)SYSLOG_COUNT) == SYSLOG_CLEAR_NONE);
|
||||
CHECK(ok, "TC-SYSLOG-120b a rejected request leaves a valid id's pending ask and state untouched");
|
||||
|
||||
/* 121 读侧:删除成功必须让读者的 8 条窗口作废(否则屏上显示"已清除"而记录还在);
|
||||
* 失败路径不许自增 —— 盘上什么都没动,窗口里仍是真数据。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
for (i = 0u; i < 3u; i++) {
|
||||
(void)s_append(SYSLOG_RUN, "w");
|
||||
}
|
||||
{
|
||||
uint32_t gen = 0u;
|
||||
ok = (syslog_get_record(SYSLOG_RUN, 2u, ts, sizeof(ts), desc, sizeof(desc)) == 0);
|
||||
gen = s_rd_gen[SYSLOG_RUN];
|
||||
ok = ok && (syslog_clear_apply(SYSLOG_RUN) == 0)
|
||||
&& (s_rd_gen[SYSLOG_RUN] == gen + 1u)
|
||||
&& (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == -1);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-121 a successful clear invalidates the reader window (no records left to show)");
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)s_append(SYSLOG_RUN, "keep");
|
||||
ok = (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0);
|
||||
{
|
||||
uint32_t gen = s_rd_gen[SYSLOG_RUN];
|
||||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_DISK_ERR, 1);
|
||||
ok = ok && (syslog_clear_apply(SYSLOG_RUN) == -1)
|
||||
&& (s_rd_gen[SYSLOG_RUN] == gen)
|
||||
&& (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
|
||||
&& (strcmp(desc, "keep") == 0);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-121b a failed clear neither bumps the generation nor loses the cached valid record");
|
||||
|
||||
/* 122 /log 目录本身不在:假件与真实 ff.c 一样落 FR_NO_PATH。这条刻意**不算**幂等
|
||||
* 成功(记录读不到是真的,但成因是目录没了而不是"被清空",把卷异常洗成
|
||||
* "清除成功"是另一种撒谎),而且本函数一个卷级动作都不做:不补目录、
|
||||
* 不重开、不碰别的对象——补目录归 syslog_mcu_mounted() 每 2s 的重试。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
ok = (f_unlink("/log") == FR_OK) && (ff_fake_exists("/log") == 0);
|
||||
{
|
||||
int mkdir_base = g_ff_obs.calls[FF_FAKE_OP_MKDIR];
|
||||
ok = ok && (syslog_clear_apply(SYSLOG_RUN) == -1)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == mkdir_base)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == 0)
|
||||
&& (ff_fake_exists("/log") == 0)
|
||||
&& (g_sys_err_count == 1);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-122 FR_NO_PATH is not counted as an idempotent success: -1, one trace, and zero f_mkdir (no volume-level action on a name/param code)");
|
||||
(void)ff_fake_mkdir_direct("/log"); /* 目录回来了(模拟挂载门补建成功) */
|
||||
ok = (syslog_request_clear(SYSLOG_RUN) == 0)
|
||||
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_WAIT);
|
||||
syslog_drain();
|
||||
ok = ok && (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK)
|
||||
&& (syslog_count(SYSLOG_RUN) == 0u);
|
||||
CHECK(ok, "TC-SYSLOG-122b FAIL is not sticky: once the directory is back a new request clears normally");
|
||||
}
|
||||
|
||||
/* ===================== L 五类记录与 quiet 投递通道(task-10) ===================== */
|
||||
|
||||
/* 七类记录一枚描述、一枚路径:描述自带类别号,用来证明"id -> 文件"的对应关系
|
||||
* 与两端表序一致(表序错 = 事故安静地写进告警文件,屏上看不出任何毛病)。 */
|
||||
static const char *const s_kind_desc[SYSLOG_COUNT] = {
|
||||
"k0run", "k1selftest", "k2accid", "k3alarm", "k4start", "k5oper", "k6yk"
|
||||
};
|
||||
static const char *const s_kind_path[SYSLOG_COUNT] = {
|
||||
P_RUN, P_SELFTEST, P_ACCID, P_ALARM, P_START, P_OPER, P_YK
|
||||
};
|
||||
|
||||
static void tc_kinds(void)
|
||||
{
|
||||
uint8_t raw[SYSLOG_REC_SIZE];
|
||||
int ok;
|
||||
uint32_t i;
|
||||
int base_entries;
|
||||
|
||||
printf("-- TC-SYSLOG-130..136 five record kinds + quiet posting channel --\n");
|
||||
|
||||
/* 130 一类一个文件:每类只进自己那份,且没有第八个文件被顺手建出来 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
base_entries = ff_fake_count_entries();
|
||||
for (i = 0u; i < (uint32_t)SYSLOG_COUNT; i++) {
|
||||
(void)s_append((syslog_id_t)i, s_kind_desc[i]);
|
||||
}
|
||||
ok = 1;
|
||||
for (i = 0u; i < (uint32_t)SYSLOG_COUNT; i++) {
|
||||
if (ff_fake_size(s_kind_path[i]) != SYSLOG_REC_SIZE) { ok = 0; break; }
|
||||
if (!raw_at(s_kind_path[i], 0u, raw)) { ok = 0; break; }
|
||||
if (memcmp(raw + SYSLOG_DESC_OFFS, s_kind_desc[i], strlen(s_kind_desc[i])) != 0) { ok = 0; break; }
|
||||
if (raw[SYSLOG_DESC_OFFS + strlen(s_kind_desc[i])] != 0u) { ok = 0; break; }
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-130 each kind lands in its own file and carries its own description (id-to-path table order is semantics)");
|
||||
CHECK(ff_fake_count_entries() == (base_entries + (int)SYSLOG_COUNT),
|
||||
"TC-SYSLOG-130b exactly seven record files exist, none created under a mistyped 8.3 name");
|
||||
|
||||
/* 130c 五类新路径都过严格 8.3(081 只覆盖老三类 + wdev;扩 id 必须一起扩这条) */
|
||||
ok = 1;
|
||||
for (i = 0u; i < (uint32_t)SYSLOG_COUNT; i++) {
|
||||
/* 上面已经真打开写成功过;这里再核一次名字本身:主名 <=8、扩展 = log、
|
||||
* 不以点开头、不含非法字符 */
|
||||
const char *p = s_kind_path[i];
|
||||
const char *slash = strrchr(p, '/');
|
||||
size_t body = 0u, ext = 0u;
|
||||
const char *q;
|
||||
|
||||
if ((slash == NULL) || (slash[1] == '.')) { ok = 0; break; }
|
||||
for (q = slash + 1; (*q != '\0') && (*q != '.'); q++) { body++; }
|
||||
if (*q == '.') { for (q++; *q != '\0'; q++) { ext++; } }
|
||||
if ((body == 0u) || (body > 8u) || (ext != 3u)) { ok = 0; break; }
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-130c all seven production paths are strict 8.3 short names (FF_USE_LFN=0)");
|
||||
|
||||
/* 131 各用各的容量上限:accid 到自己那格(512)轮转,且不动邻居 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)ff_fake_mkfile_size(P_ACCID, (SYSLOG_ACCIDENT_MAX - 1u) * SYSLOG_REC_SIZE);
|
||||
(void)ff_fake_mkfile_size(P_ALARM, (SYSLOG_ALARM_MAX - 1u) * SYSLOG_REC_SIZE);
|
||||
(void)put_rec(SYSLOG_ACCIDENT, "cap-a1");
|
||||
ok = (syslog_count(SYSLOG_ACCIDENT) == SYSLOG_ACCIDENT_MAX)
|
||||
&& (ff_fake_size(P_ACCID) == (SYSLOG_ACCIDENT_MAX * SYSLOG_REC_SIZE));
|
||||
(void)put_rec(SYSLOG_ACCIDENT, "cap-a2"); /* 到顶 -> 删除重建,只剩本次一条 */
|
||||
ok = ok && (syslog_count(SYSLOG_ACCIDENT) == 1u)
|
||||
&& (ff_fake_size(P_ACCID) == SYSLOG_REC_SIZE);
|
||||
CHECK(ok && (ff_fake_size(P_ALARM) == ((SYSLOG_ALARM_MAX - 1u) * SYSLOG_REC_SIZE)),
|
||||
"TC-SYSLOG-131 the accident log rotates on its own cap and leaves the alarm log alone");
|
||||
|
||||
/* 131b yk 用的是自己那格(256),不是抄来的 512/1024 —— 三档上限互不相同,
|
||||
* 所以这一条能区分出"s_caps[] 串格"这种安静错误 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)ff_fake_mkfile_size(P_YK, (SYSLOG_YK_MAX - 1u) * SYSLOG_REC_SIZE);
|
||||
(void)put_rec(SYSLOG_YK, "cap-y1");
|
||||
ok = (syslog_count(SYSLOG_YK) == SYSLOG_YK_MAX);
|
||||
(void)put_rec(SYSLOG_YK, "cap-y2");
|
||||
ok = ok && (syslog_count(SYSLOG_YK) == 1u) && (ff_fake_size(P_YK) == SYSLOG_REC_SIZE);
|
||||
CHECK(ok, "TC-SYSLOG-131b the telecontrol log rotates on its own 256-record cap (per-id caps are not shared)");
|
||||
|
||||
/* 132 quiet 通道(队列侧):丢弃要计数,但投递侧一条日志都不许打
|
||||
* ——这正是它存在的唯一理由,退化成 syslog_post 的别名必须被照出来 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain(); /* 建队列 */
|
||||
g_sys_err_count = 0;
|
||||
for (i = 0u; i < (SYSLOG_QUEUE_MAX + 4u); i++) {
|
||||
syslog_post_quiet(SYSLOG_ACCIDENT, "q");
|
||||
}
|
||||
ok = (s_q_cnt == SYSLOG_QUEUE_MAX) && (syslog_post_dropped() == 4u)
|
||||
&& (g_sys_err_count == 0);
|
||||
syslog_drain(); /* 落盘 + drain 侧按差值补一条 */
|
||||
ok = ok && (syslog_count(SYSLOG_ACCIDENT) == SYSLOG_QUEUE_MAX)
|
||||
&& (g_sys_err_count == 1) && (syslog_post_dropped() == 4u);
|
||||
syslog_drain(); /* 计数没前进 -> 不得重复报 */
|
||||
CHECK(ok && (g_sys_err_count == 1),
|
||||
"TC-SYSLOG-132 quiet drops are counted and reported once from the writer side, never at the posting site");
|
||||
|
||||
/* 133 清除记录 = 一次真实运维动作 -> 落操作记录;且不影响被清那份的归零判据 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain();
|
||||
syslog_post(SYSLOG_RUN, "before clear");
|
||||
syslog_drain();
|
||||
ok = (syslog_count(SYSLOG_RUN) == 1u);
|
||||
ok = ok && (syslog_request_clear(SYSLOG_RUN) == 0);
|
||||
syslog_drain(); /* 删除落地 + 投递那条操作记录 */
|
||||
ok = ok && (syslog_count(SYSLOG_RUN) == 0u)
|
||||
&& (syslog_clear_state(SYSLOG_RUN) == (int)SYSLOG_CLEAR_OK);
|
||||
syslog_drain(); /* 操作记录这才落盘 */
|
||||
ok = ok && (syslog_count(SYSLOG_OPER) == 1u) && raw_at(P_OPER, 0u, raw)
|
||||
&& (memcmp(raw + SYSLOG_DESC_OFFS, "clear run.log", 13u) == 0);
|
||||
CHECK(ok, "TC-SYSLOG-133 clearing a log writes one operation record naming the file, while the cleared log stays at zero");
|
||||
|
||||
/* 133b 反过来:清 oper.log 自己时不得投递操作记录,否则"清除成功"这一条
|
||||
* 会把刚删空的文件又写成 1 条,直接砸掉 task-4 的「count 归零」判据 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain();
|
||||
syslog_post(SYSLOG_OPER, "an operation");
|
||||
syslog_drain();
|
||||
ok = (syslog_count(SYSLOG_OPER) == 1u);
|
||||
ok = ok && (syslog_request_clear(SYSLOG_OPER) == 0);
|
||||
syslog_drain();
|
||||
ok = ok && (syslog_clear_state(SYSLOG_OPER) == (int)SYSLOG_CLEAR_OK)
|
||||
&& (syslog_count(SYSLOG_OPER) == 0u);
|
||||
syslog_drain(); /* 再排一轮:真投了自我记录,这轮就写回去 */
|
||||
CHECK(ok && (syslog_count(SYSLOG_OPER) == 0u) && (syslog_post_dropped() == 0u),
|
||||
"TC-SYSLOG-133b clearing the operation log itself posts nothing, so the zeroed-count criterion survives");
|
||||
|
||||
/* 134 守卫:越界 id / NULL 描述在 quiet 通道上既不投递也不计丢弃
|
||||
* ("被守卫挡下"与"满了被丢掉"是两件事,混起来就等于把参数错报成容量故障) */
|
||||
reset_all();
|
||||
ready_only();
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain();
|
||||
g_sys_err_count = 0;
|
||||
syslog_post_quiet((syslog_id_t)SYSLOG_COUNT, "bad-id");
|
||||
syslog_post_quiet(SYSLOG_RUN, NULL);
|
||||
ok = (syslog_post_dropped() == 0u) && (s_q_cnt == 0u) && (s_preboot_cnt == 0u)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (g_sys_err_count == 0);
|
||||
CHECK(ok, "TC-SYSLOG-134 quiet channel guards bad id and NULL desc without counting them as drops and without touching the volume");
|
||||
|
||||
/* 135 启动前数组侧同样计数、同样静默(两个通道各自独立,与 F 项那两个抑制位
|
||||
* 的分区方式一致;漏掉这一侧就会"运行期有账、启动期没账") */
|
||||
reset_all();
|
||||
ready_only(); /* 内核未跑 -> 走 s_preboot */
|
||||
g_sys_err_count = 0;
|
||||
for (i = 0u; i < (SYSLOG_QUEUE_MAX + 3u); i++) {
|
||||
syslog_post_quiet(SYSLOG_START, "p");
|
||||
}
|
||||
ok = (s_preboot_cnt == SYSLOG_QUEUE_MAX) && (syslog_post_dropped() == 3u)
|
||||
&& (g_sys_err_count == 0);
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain();
|
||||
CHECK(ok && (syslog_count(SYSLOG_START) == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1),
|
||||
"TC-SYSLOG-135 pre-boot array overflow is counted and traced from the writer side too (8 slots kept, 3 lost)");
|
||||
|
||||
/* 136 五类记录与老两类**逐字节同构**:同一枚 s_format_rec,记录体里没有类型字段
|
||||
* ——这就是"文件即类型"这条决策的机器表达(task-11 页面因此只需一套解析) */
|
||||
reset_all();
|
||||
{
|
||||
uint8_t a[SYSLOG_REC_SIZE];
|
||||
uint8_t b[SYSLOG_REC_SIZE];
|
||||
s_now = 1760234567u;
|
||||
s_clock_ok = 1;
|
||||
s_format_rec(SYSLOG_RUN, "same text", a);
|
||||
s_format_rec(SYSLOG_YK, "same text", b);
|
||||
ok = (memcmp(a, b, SYSLOG_REC_SIZE) == 0);
|
||||
s_format_rec(SYSLOG_ACCIDENT, "same text", b);
|
||||
ok = ok && (memcmp(a, b, SYSLOG_REC_SIZE) == 0);
|
||||
s_format_rec(SYSLOG_OPER, "same text", b);
|
||||
ok = ok && (memcmp(a, b, SYSLOG_REC_SIZE) == 0);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-136 a record body does not encode its kind: same content is byte-identical across all seven kinds");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
int main(void)
|
||||
@@ -1855,6 +2419,8 @@ int main(void)
|
||||
tc_extra_branches();
|
||||
tc_selfheal_codes();
|
||||
tc_post_throttle();
|
||||
tc_clear();
|
||||
tc_kinds();
|
||||
|
||||
printf("\n=== Sys Log Mcu Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
|
||||
@@ -41,6 +41,25 @@
|
||||
* PE_COUNT == 表长);元件名不超名列宽、非元件/越界回占位
|
||||
* TC-UIDATA-014 注入 reader 后的两行渲染:时标行/内容行分工、单条读盘缓存
|
||||
* (同记录两行只读一次)、IV 时标抑制、读失败退化与不绑空态
|
||||
* ── 以下 6 条 = 定值写路径(道F task-1,docs/任务规划/UI补齐.md §2 第 1 行)──
|
||||
* TC-UIDATA-015 队列深度 4:四格都进得去、第五格满返 -1 且状态转 ERR_BUSY;
|
||||
* FIFO 顺序(最后投的一条最后落定)、空队列 drain 返 0、
|
||||
* drain(NULL) 返 -1 且不动队列、投递成功即转 WAIT
|
||||
* TC-UIDATA-016 请求表示法:NULL/带非法位 mask 返 -2 且状态与队列都不动;
|
||||
* mask=0 空请求返 0 且直接落 OK 不占槽;请求尺寸与队列 .bss
|
||||
* 预算钉死(168 B/格 × 4 格)——加深或加宽必须先过这条
|
||||
* TC-UIDATA-017 正常往返:请求 → WAIT → drain → OK → 新值可读;**只提交 mask
|
||||
* 内的 id**(未提交的槽一位都不动,这是"从显示值回提全表会
|
||||
* 顺手改掉返回系数"那条坑的机器防线)
|
||||
* TC-UIDATA-018 越界不改值:整批预检任一项越界 → ERR_RANGE 且**一字不写**
|
||||
* (含同批里合法的那几项)+ err_mask 指认越界项 + 下一次成功
|
||||
* 落定后 err_mask 自动清零
|
||||
* TC-UIDATA-019 闭锁拒写:locked 置位 → ERR_LOCKED、丢弃本条(不排队等解锁)、
|
||||
* 值区一字不动;解闭锁后重新提交可成功
|
||||
* TC-UIDATA-020 apply 中途失败(预检与执行不一致的防御分支):如实 ERR_RANGE,
|
||||
* 已生效的前几项不回滚(prot_set 无回滚接口,谎称整批未写更坏)
|
||||
* TC-UIDATA-021 v2 帧扩展:UI_DATA_VER == 2、既有字段偏移逐字节见证(只允许尾
|
||||
* 追加)、cal_block 两种态(未标定/已标定)整帧往返
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
@@ -49,6 +68,7 @@
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stddef.h> /* offsetof:v2 帧布局见证(只许尾追加) */
|
||||
#include <string.h>
|
||||
|
||||
/* 生产源文件原样纳入(纯内存叶子,零平台依赖,直接编) */
|
||||
@@ -521,6 +541,351 @@ static void tc_soe_render(void)
|
||||
ui_w_soe_bind(NULL);
|
||||
}
|
||||
|
||||
/* ====== TC-UIDATA-015~020:定值写路径(队列 + 状态机 + 注入执行侧) ====== */
|
||||
|
||||
/*
|
||||
* 假执行侧(替掉 App/algo/prot_set):一张 40 项值区 + 一张上下限表。
|
||||
* ⚠ 本单测不链 App 侧任何 .c(prot_set 依赖 Flash/校验和编排,且写路径的**契约**
|
||||
* 本来就只是"三个函数的返回值"),所以测的是 ui_data.c 里那份真实的队列 +
|
||||
* 状态机代码:越界不改值、闭锁拒写、只提交改动 id 三条判据都落在生产代码上,
|
||||
* 不是测试里的副本。真实 App 实现的等价性由 ui_data_feed.c 的三个包函数
|
||||
* (feed_param_locked/check/apply)在代码审查与台架侧保证。
|
||||
*/
|
||||
#define FAKE_LO_ALL (-100000) /* 默认宽松下限:单条 TC 里再逐行收紧 */
|
||||
#define FAKE_HI_ALL (100000)
|
||||
|
||||
static int32_t s_fake_val[UI_DATA_SET_N];
|
||||
static int32_t s_fake_lo[UI_DATA_SET_N];
|
||||
static int32_t s_fake_hi[UI_DATA_SET_N];
|
||||
static uint8_t s_fake_locked;
|
||||
static uint8_t s_fake_poison; /* 0xFF = 不投毒;否则该 id 的 apply 必失败 */
|
||||
static uint32_t s_fake_apply_calls;
|
||||
|
||||
static uint8_t fake_locked(void)
|
||||
{
|
||||
return s_fake_locked;
|
||||
}
|
||||
|
||||
static int fake_check(uint8_t a_id, int32_t a_q10)
|
||||
{
|
||||
if (a_id >= (uint8_t)UI_DATA_SET_N) {
|
||||
return -1;
|
||||
}
|
||||
if ((a_q10 < s_fake_lo[a_id]) || (a_q10 > s_fake_hi[a_id])) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int fake_apply(uint8_t a_id, int32_t a_q10)
|
||||
{
|
||||
if (fake_check(a_id, a_q10) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if ((s_fake_poison != 0xFFu) && (a_id == s_fake_poison)) {
|
||||
return -1; /* 投毒:模拟"预检过了但执行侧仍拒"(理论不可达的防御分支) */
|
||||
}
|
||||
s_fake_val[a_id] = a_q10;
|
||||
s_fake_apply_calls++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const ui_data_param_ops_t s_fake_ops = { fake_locked, fake_check, fake_apply };
|
||||
|
||||
/* 每个 TC 开头调一次:值区填可辨识的旧值 + 快照与队列复位 */
|
||||
static void fake_reset(void)
|
||||
{
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
|
||||
s_fake_val[i] = (int32_t)i * 1000;
|
||||
s_fake_lo[i] = FAKE_LO_ALL;
|
||||
s_fake_hi[i] = FAKE_HI_ALL;
|
||||
}
|
||||
s_fake_locked = 0u;
|
||||
s_fake_poison = 0xFFu;
|
||||
s_fake_apply_calls = 0u;
|
||||
ui_data_init();
|
||||
}
|
||||
|
||||
/* 投一条只改若干项的请求;未列出的 id 不进 mask(= 本次不提交) */
|
||||
static int fake_submit(const uint8_t *a_ids, const int32_t *a_vals, uint8_t a_n)
|
||||
{
|
||||
ui_data_param_req_t req;
|
||||
uint8_t i;
|
||||
|
||||
(void)memset(&req, 0, sizeof(req));
|
||||
for (i = 0u; i < a_n; i++) {
|
||||
req.mask |= ((uint64_t)1u << a_ids[i]);
|
||||
req.q10[a_ids[i]] = a_vals[i];
|
||||
}
|
||||
return ui_data_param_request(&req);
|
||||
}
|
||||
|
||||
static int32_t fake_val(uint8_t a_id)
|
||||
{
|
||||
return s_fake_val[a_id];
|
||||
}
|
||||
|
||||
static void tc_param_queue(void)
|
||||
{
|
||||
ui_data_param_req_t req;
|
||||
uint8_t k;
|
||||
int accepted = 1;
|
||||
int was_wait = 1;
|
||||
|
||||
fake_reset();
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_IDLE,
|
||||
"TC-UIDATA-015a state IDLE after init (never submitted)");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-015b drain on empty queue == 0");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_IDLE,
|
||||
"TC-UIDATA-015c empty drain leaves state alone (not WAIT)");
|
||||
CHECK(ui_data_param_err_mask() == 0u, "TC-UIDATA-015d err_mask clean at idle");
|
||||
|
||||
/* 四格全进得去;每投一条状态就该是 WAIT */
|
||||
(void)memset(&req, 0, sizeof(req));
|
||||
req.mask = 1u; /* 只改 id0 */
|
||||
for (k = 0u; k < UI_DATA_PARAM_QD; k++) {
|
||||
req.q10[0] = (int32_t)(1000 + (int32_t)k);
|
||||
if (ui_data_param_request(&req) != 0) {
|
||||
accepted = 0;
|
||||
}
|
||||
if (ui_data_param_state() != UI_DATA_PARAM_ST_WAIT) {
|
||||
was_wait = 0;
|
||||
}
|
||||
}
|
||||
CHECK(UI_DATA_PARAM_QD == 4u, "TC-UIDATA-015e queue depth is 4 (contract witness)");
|
||||
CHECK(accepted == 1, "TC-UIDATA-015f four requests all accepted");
|
||||
CHECK(was_wait == 1, "TC-UIDATA-015g state WAIT while queued");
|
||||
|
||||
/* 第五格:满 → 返 -1 + 状态 ERR_BUSY(在队四条一条不掉) */
|
||||
req.q10[0] = 9999;
|
||||
CHECK(ui_data_param_request(&req) == -1, "TC-UIDATA-015h fifth request rejected (-1)");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_BUSY,
|
||||
"TC-UIDATA-015i rejected request surfaces ERR_BUSY");
|
||||
|
||||
/* FIFO:前三条落定后仍是 WAIT,第四条才落终态;值 = 最后投的那条(1003) */
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-015j drain processes one");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_WAIT,
|
||||
"TC-UIDATA-015k still WAIT with 3 requests left in queue");
|
||||
(void)ui_data_param_drain(&s_fake_ops);
|
||||
(void)ui_data_param_drain(&s_fake_ops);
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_WAIT,
|
||||
"TC-UIDATA-015l WAIT until the last queued request settles");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-015m fourth request drained");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-015n terminal OK when drained");
|
||||
CHECK((fake_val(0) == 1003) && (s_fake_apply_calls == 4u),
|
||||
"TC-UIDATA-015o FIFO: last queued value lands, all four applied");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-015p queue empty again");
|
||||
|
||||
/* ops 无效:不动队列也不动状态 */
|
||||
fake_reset();
|
||||
CHECK(fake_submit(NULL, NULL, 0u) == 0, "TC-UIDATA-015q empty request accepted (see 016)");
|
||||
CHECK(ui_data_param_drain(NULL) == -1, "TC-UIDATA-015r drain(NULL) == -1");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK,
|
||||
"TC-UIDATA-015s failed drain does not rewrite state");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-015t failed drain does not consume");
|
||||
}
|
||||
|
||||
static void tc_param_request_shape(void)
|
||||
{
|
||||
ui_data_param_req_t req;
|
||||
|
||||
fake_reset();
|
||||
CHECK(ui_data_param_request(NULL) == -2, "TC-UIDATA-016a request(NULL) == -2");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_IDLE,
|
||||
"TC-UIDATA-016b invalid request leaves state IDLE (page must not show ERR for a bug)");
|
||||
|
||||
(void)memset(&req, 0, sizeof(req));
|
||||
req.mask = ((uint64_t)1u << 63u); /* 第 63 项不存在 */
|
||||
CHECK(ui_data_param_request(&req) == -2, "TC-UIDATA-016c mask with bit63 == -2");
|
||||
req.mask = ((uint64_t)1u << (UI_DATA_SET_N - 1u)); /* 第 39 项 = 最后一项,合法 */
|
||||
CHECK(ui_data_param_request(&req) == 0, "TC-UIDATA-016d last legal bit accepted");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-016e drained last-bit request");
|
||||
CHECK(s_fake_apply_calls == 1u, "TC-UIDATA-016f item 39 really reached the executor");
|
||||
|
||||
/* 空请求:不占槽、直接 OK("没改东西却按了保存"是正常操作) */
|
||||
fake_reset();
|
||||
req.mask = 0u;
|
||||
CHECK(ui_data_param_request(&req) == 0, "TC-UIDATA-016g empty request returns 0");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-016h empty request settles OK");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-016i empty request consumed no slot");
|
||||
|
||||
/* 内存预算护栏:请求是 .bss 里的定长槽,168 B/格 × 4 格 = 672 B
|
||||
* (台账 docs/内存管理/RAM使用分析与优化方案.md;扩深度/加宽必须先过这条) */
|
||||
CHECK(sizeof(ui_data_param_req_t) == 168u, "TC-UIDATA-016j request is 168 bytes");
|
||||
CHECK(sizeof(s_pq) == (672u), "TC-UIDATA-016k queue .bss is 4 x 168 = 672 bytes");
|
||||
CHECK(sizeof(s_pq) <= 768u, "TC-UIDATA-016l queue .bss within budget (<=768B)");
|
||||
CHECK((uint32_t)UI_DATA_SET_N <= 64u, "TC-UIDATA-016m item count fits the 64-bit mask");
|
||||
}
|
||||
|
||||
static void tc_param_roundtrip(void)
|
||||
{
|
||||
const uint8_t ids[3] = { 2u, 3u, 7u };
|
||||
const int32_t vals[3] = { 12345, 60000, 7 };
|
||||
|
||||
fake_reset();
|
||||
CHECK(fake_submit(ids, vals, 3u) == 0, "TC-UIDATA-017a batch of three submitted");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_WAIT, "TC-UIDATA-017b WAIT before drain");
|
||||
CHECK(fake_val(2) == 2000, "TC-UIDATA-017c old value still in effect before drain");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-017d drained one request");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-017e OK after drain");
|
||||
CHECK((fake_val(2) == 12345) && (fake_val(3) == 60000) && (fake_val(7) == 7),
|
||||
"TC-UIDATA-017f new values readable after roundtrip");
|
||||
/* 只提交改动项:mask 外的槽一位都不动(Q10 往返精度坑的机器防线) */
|
||||
CHECK((fake_val(0) == 0) && (fake_val(1) == 1000) && (fake_val(4) == 4000)
|
||||
&& (fake_val(6) == 6000) && (fake_val(8) == 8000)
|
||||
&& (fake_val(UI_DATA_SET_N - 1u) == (int32_t)(UI_DATA_SET_N - 1u) * 1000),
|
||||
"TC-UIDATA-017g untouched items are byte-identical (mask-only commit)");
|
||||
CHECK(s_fake_apply_calls == 3u, "TC-UIDATA-017h exactly the masked items were applied");
|
||||
|
||||
/* 回环的后半段:App 落值后重发参数帧 → 页面按新 seq 读到新值 */
|
||||
{
|
||||
ui_data_par_t frame;
|
||||
ui_data_par_t got;
|
||||
uint8_t i;
|
||||
|
||||
(void)memset(&frame, 0, sizeof(frame));
|
||||
frame.valid = 1u;
|
||||
frame.checksum = 0x1234u;
|
||||
frame.cal_block = 0u;
|
||||
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
|
||||
frame.q10[i] = s_fake_val[i];
|
||||
}
|
||||
CHECK(ui_data_publish_par(&frame) == 0, "TC-UIDATA-017i par frame republished");
|
||||
(void)memset(&got, 0, sizeof(got));
|
||||
CHECK((ui_data_par(&got) == 0) && (got.q10[2] == 12345) && (got.q10[7] == 7),
|
||||
"TC-UIDATA-017j page reads back the committed values");
|
||||
}
|
||||
}
|
||||
|
||||
static void tc_param_range_atomic(void)
|
||||
{
|
||||
/* 非 const:本 TC 复用同一组数组跑三种越界组合(改了再投) */
|
||||
uint8_t ids[2] = { 2u, 5u };
|
||||
int32_t vals[2] = { 12345, 99999 };
|
||||
|
||||
fake_reset();
|
||||
s_fake_hi[5] = 100; /* 第 5 项上限 100 → 99999 越界;第 2 项本身合法 */
|
||||
CHECK(fake_submit(ids, vals, 2u) == 0, "TC-UIDATA-018a out-of-range batch accepted for queue");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-018b drained");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_RANGE, "TC-UIDATA-018c state ERR_RANGE");
|
||||
CHECK(ui_data_param_err_mask() == ((uint64_t)1u << 5u),
|
||||
"TC-UIDATA-018d err_mask points at the offending item");
|
||||
CHECK((fake_val(2) == 2000) && (fake_val(5) == 5000),
|
||||
"TC-UIDATA-018e all-or-nothing: the legal item in the same batch NOT written");
|
||||
CHECK(s_fake_apply_calls == 0u, "TC-UIDATA-018f precheck blocks every apply call");
|
||||
|
||||
/* 多项同时越界:err_mask 全给(页面逐行反显,不是只报第一行) */
|
||||
fake_reset();
|
||||
s_fake_lo[6] = 500;
|
||||
s_fake_hi[5] = 100;
|
||||
ids[0] = 5u;
|
||||
ids[1] = 6u;
|
||||
vals[0] = 99999; /* 超上限 */
|
||||
vals[1] = 60; /* 低于下限 */
|
||||
CHECK(fake_submit(ids, vals, 2u) == 0, "TC-UIDATA-018g two-item bad batch queued");
|
||||
(void)ui_data_param_drain(&s_fake_ops);
|
||||
CHECK(ui_data_param_err_mask() == (((uint64_t)1u << 5u) | ((uint64_t)1u << 6u)),
|
||||
"TC-UIDATA-018h err_mask lists every offending item (both bounds)");
|
||||
|
||||
/* 下一次成功落定 → err_mask 自动清零(否则页面把上次越界显示成这次) */
|
||||
ids[0] = 2u;
|
||||
vals[0] = 12345;
|
||||
CHECK(fake_submit(ids, vals, 1u) == 0, "TC-UIDATA-018i good request queued");
|
||||
(void)ui_data_param_drain(&s_fake_ops);
|
||||
CHECK((ui_data_param_state() == UI_DATA_PARAM_ST_OK) && (ui_data_param_err_mask() == 0u),
|
||||
"TC-UIDATA-018j err_mask cleared on next successful commit");
|
||||
}
|
||||
|
||||
static void tc_param_locked(void)
|
||||
{
|
||||
const uint8_t ids[1] = { 2u };
|
||||
const int32_t vals[1] = { 12345 };
|
||||
|
||||
fake_reset();
|
||||
s_fake_locked = 1u;
|
||||
CHECK(fake_submit(ids, vals, 1u) == 0, "TC-UIDATA-019a request queued while locked");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-019b drained under lock");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_LOCKED, "TC-UIDATA-019c state ERR_LOCKED");
|
||||
CHECK((fake_val(2) == 2000) && (s_fake_apply_calls == 0u),
|
||||
"TC-UIDATA-019d locked commit writes nothing (no apply call)");
|
||||
CHECK(ui_data_param_err_mask() == 0u, "TC-UIDATA-019e lock failure is not a range failure");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 0,
|
||||
"TC-UIDATA-019f locked request is dropped, not queued waiting for unlock");
|
||||
|
||||
/* 解闭锁后重新提交 → OK(闭锁丢弃的是那一条,不是永久拉黑写路径) */
|
||||
s_fake_locked = 0u;
|
||||
CHECK(fake_submit(ids, vals, 1u) == 0, "TC-UIDATA-019g resubmit after unlock accepted");
|
||||
(void)ui_data_param_drain(&s_fake_ops);
|
||||
CHECK((ui_data_param_state() == UI_DATA_PARAM_ST_OK) && (fake_val(2) == 12345),
|
||||
"TC-UIDATA-019h write path healthy again after unlock");
|
||||
}
|
||||
|
||||
static void tc_param_apply_fail(void)
|
||||
{
|
||||
const uint8_t ids[2] = { 3u, 7u };
|
||||
const int32_t vals[2] = { 33333, 77777 };
|
||||
|
||||
fake_reset();
|
||||
s_fake_poison = 7u; /* 预检全过、执行侧仍拒第 7 项(两真源不一致的防御分支) */
|
||||
CHECK(fake_submit(ids, vals, 2u) == 0, "TC-UIDATA-020a request with poisoned item queued");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-020b drained");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_RANGE,
|
||||
"TC-UIDATA-020c apply failure reported as ERR_RANGE (not silent OK)");
|
||||
CHECK(ui_data_param_err_mask() == ((uint64_t)1u << 7u), "TC-UIDATA-020d failing item named");
|
||||
CHECK(fake_val(3) == 33333, "TC-UIDATA-020e earlier item already applied is NOT rolled back");
|
||||
CHECK(fake_val(7) == 7000, "TC-UIDATA-020f failing item itself unchanged");
|
||||
}
|
||||
|
||||
static void tc_param_frame_v2(void)
|
||||
{
|
||||
ui_data_par_t src;
|
||||
ui_data_par_t got;
|
||||
|
||||
/* 版本号是页面与写侧唯一对齐凭据;v2 = 尾追加 cal_block */
|
||||
CHECK(UI_DATA_VER == 2u, "TC-UIDATA-021a contract version is 2");
|
||||
/* v1 布局逐字节见证:字段只允许尾追加,任何插入/重排都会让下面四条红
|
||||
* (页面按名字取字段,错位是静默的——只有把偏移钉死才照得出来) */
|
||||
CHECK(offsetof(ui_data_par_t, seq) == 0u, "TC-UIDATA-021b seq offset unchanged");
|
||||
CHECK(offsetof(ui_data_par_t, bad_mask) == 16u, "TC-UIDATA-021c bad_mask offset unchanged");
|
||||
CHECK(offsetof(ui_data_par_t, q10) == 24u, "TC-UIDATA-021d q10 offset unchanged");
|
||||
CHECK(offsetof(ui_data_par_t, cal_block) == 184u,
|
||||
"TC-UIDATA-021e cal_block is the appended tail field (offset 184)");
|
||||
CHECK(sizeof(ui_data_par_t) == 192u, "TC-UIDATA-021f par frame is 192 B (+8 for v2)");
|
||||
/* 写侧组装缓冲是 rt/par/info 三帧共用的 union(App/app/ui_data_feed.c 的 s_scr):
|
||||
* rt 恒大于 par,所以给 par 追加字段**不会**连带抬升那块 .bss——加字段之前
|
||||
* 先确认这条,否则每次尾追加都悄悄多啃一份 RAM(台账只记总量,看不出是谁长的) */
|
||||
CHECK(sizeof(ui_data_rt_t) == 208u, "TC-UIDATA-021g rt frame is 208 B (union stays put)");
|
||||
CHECK(sizeof(ui_data_par_t) < sizeof(ui_data_rt_t),
|
||||
"TC-UIDATA-021h par still smaller than rt (shared scratch unchanged)");
|
||||
|
||||
/* 闭锁态:1 = 未标定(写侧按 UA/UN/IA 三路 VALID 现算,本层只存真值) */
|
||||
ui_data_init();
|
||||
(void)memset(&src, 0, sizeof(src));
|
||||
src.valid = 1u;
|
||||
src.locked = 0u;
|
||||
src.cal_block = 1u;
|
||||
CHECK(ui_data_publish_par(&src) == 0, "TC-UIDATA-021g publish blocked frame");
|
||||
(void)memset(&got, 0, sizeof(got));
|
||||
CHECK((ui_data_par(&got) == 0) && (got.cal_block == 1u) && (got.locked == 0u),
|
||||
"TC-UIDATA-021h cal_block=1 roundtrip, distinct from locked");
|
||||
|
||||
/* 解卡态:写侧填 0 后覆盖发布(同一份存储不会粘住旧值) */
|
||||
src.cal_block = 0u;
|
||||
(void)ui_data_publish_par(&src);
|
||||
(void)memset(&got, 0xAA, sizeof(got));
|
||||
CHECK((ui_data_par(&got) == 0) && (got.cal_block == 0u),
|
||||
"TC-UIDATA-021i cal_block=0 roundtrip (no stale block flag)");
|
||||
|
||||
/* 写侧默认值纪律:调用方清零后不填本字段 = 0(未闭锁),首帧不会假报徽标 */
|
||||
(void)memset(&src, 0, sizeof(src));
|
||||
src.valid = 1u;
|
||||
(void)ui_data_publish_par(&src);
|
||||
(void)ui_data_par(&got);
|
||||
CHECK(got.cal_block == 0u, "TC-UIDATA-021j zero-initialized frame means not blocked");
|
||||
}
|
||||
|
||||
/* ===================== main ===================== */
|
||||
|
||||
int main(void)
|
||||
@@ -551,6 +916,20 @@ int main(void)
|
||||
tc_soe_elms();
|
||||
printf("-- soe render --\n");
|
||||
tc_soe_render();
|
||||
printf("-- param write path: queue --\n");
|
||||
tc_param_queue();
|
||||
printf("-- param write path: request shape --\n");
|
||||
tc_param_request_shape();
|
||||
printf("-- param write path: roundtrip --\n");
|
||||
tc_param_roundtrip();
|
||||
printf("-- param write path: range atomic --\n");
|
||||
tc_param_range_atomic();
|
||||
printf("-- param write path: locked --\n");
|
||||
tc_param_locked();
|
||||
printf("-- param write path: apply failure --\n");
|
||||
tc_param_apply_fail();
|
||||
printf("-- param frame v2 (cal_block) --\n");
|
||||
tc_param_frame_v2();
|
||||
|
||||
printf("\n=== UI Data Snapshot Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
|
||||
+4
-4
@@ -24,10 +24,10 @@
|
||||
遥信量 #page=yx
|
||||
参数定值
|
||||
整定
|
||||
参数 #P
|
||||
定值 #P
|
||||
控制字 #P
|
||||
软压板 #P
|
||||
参数 #P #page=view_param
|
||||
定值 #P #page=view_set
|
||||
控制字 #P #page=view_word
|
||||
软压板 #P #page=view_plate
|
||||
查看
|
||||
参数 #page=view_param
|
||||
定值 #page=view_set
|
||||
|
||||
Reference in New Issue
Block a user