fix(cfg): 配置热重载吞段缺陷修复 + 两点标定工具链与台架实测落档

修复运行期改配置被打回出厂值的缺陷:cfg_file_poll() 按段循环复用同一
份读缓冲,而 s_parse() 通过 s_split_line()/s_key_value() 就地改写缓冲
(把换行与等号换成 NUL)。第一个段解析完,缓冲里已没有键值行,后续 7
个段静默保持出厂值;随后 run_par_store_save() 又把这份出厂值写回
config.ini,用户的标定值被永久覆盖。开机路径 cfg_file_load() 只解析一
轮,所以该缺陷长期不可见,只在运行中改配置时发作。

- s_parse() 改为非破坏性:入参 const char *,逐行 memcpy 到栈缓冲
  (CFG_LINE_TEST_MAX + 1)后再交给会破坏缓冲的解析子函数;超长行跳
  过(保存侧已拒绝落盘超长受管键行)。
- 新增 TC-CONFIG-063:同时改两段内容,断言第二个注册段也重载成功、
  含负值 INT 键存活,把「热重载吞段」钉成机器面。

新增两点标定台架工具链(test/host/):
- cal_two_point.py:整数算术镜像 adc_cal_two_point(),支持单路模式与
  从执行卡 CALDATA 表批量求解(--from-doc),带合法带与零点自检拦截。
- cal_write.py:复用 iec104_master 作库,经 104 定值段写 r 后写 b 并
  读回校验,是所有写入的唯一入口。

adc_task 的 meas: 心跳行精度提到 4 位小数,标定读数需要。

文档:新增《采样数据校正全流程》(端到端导航)与《ADC 两点标定执行
流程》(台架执行卡,含 CALDATA 可填表);ADC通道映射说明新增 §7.3 台
架实测结果(UA 554.687914、IA 29.634259,出厂默认值保持不变);配置
文件设计说明新增解析器契约 v1.19;采集与算法层设计说明 §13 系列与
README 索引同步。

台架实测:UA r=554.687914(出厂 1000.0 偏大 1.80 倍,根因是接线分压
使 PT 一次侧只拿到一半电压,实测与参数算定 606.06 只差 8.5%)、IA
r=29.634259(与出厂 29.4118 只差 0.76%,反证链路未算错)。UN 按用户
裁定本轮不标定,保护闭锁(vl=0x11)是预期行为,未伪造 VALID。

同时入库 .dsh/roles 角色定义(原被 .gitignore 挡住)。
This commit is contained in:
2026-11-13 01:35:58 +08:00
parent 18c087500a
commit 1355dbe1b1
29 changed files with 2470 additions and 201 deletions
+62 -13
View File
@@ -40,6 +40,13 @@
* 台架根因为卷层 dhara 旧视图回滚(复现多形态:空读、
* 短读、同尺寸全 0 块),引擎侧无法分辨"一致但陈旧"的
* 整卷回滚——该形态重启必收敛,见设计说明
* v1.7 2026-11-12 DTU 项目组 s_parse 改为**非破坏性**(只读入参 + 逐行栈
* 缓冲)——热重载对同一份读缓冲按段各解析一轮,旧实现
* 在原地把 '=' / '\n' 写成 NUL,第一个段解析完后面
* 所有段再无键值行 → 静默全体回出厂默认,且下次落盘
* 把出厂值写回文件、永久覆盖用户整定(台架实证:
* 104 写入 r/b 当场读得回,约 8 s 后打回出厂值,而
* 开机 load 始终正确,因 load 只解析一轮)
* 函数说明:引擎状态非重入——调用方须收敛同一任务(defaultTask:poll/save;
* 启动期 load 先于任务竞争);FS 卷并发由 FatFS REENTRANT 卷锁保证
* (见《配置文件设计说明》D15 A+ 方案)
@@ -705,30 +712,72 @@ static int s_write_template(void) {
}
/*
* 函数功能:解析 INI 文本,将有效键写入段 RAM 副本
* 入口参数:p_buf - 文件文本(可修改,NUL 结尾) char *
* 函数功能:解析 INI 文本,将有效键写入段 RAM 副本(**只读入参,不修改文件文本**)
* 入口参数:p_buf - 文件文本(NUL 结尾,只读) const char *
* only_sec - 仅解析该段下标;-1 = 全部段 int
* 返回值:无
* 限定条件:调用前对应段须已复位工厂默认;无效键 = 留工厂默认并留痕
* 函数说明:**必须保持非破坏性**——热重载(cfg_file_poll)对同一份读缓冲
* 按段各解析一轮,历史实现在原地把 '=' 与 '\n' 改写成 NUL(行切分
* 的常规做法),于是第一个段解析完,后面所有段拿到的文本里再无键值
* 行 → 静默全体回出厂默认;且 RAM 被改写后紧接的落盘会把出厂值
* 写回文件,把用户整定永久覆盖(台架 2026-11-12 实证:104 写入
* r/b 当场读得回,约 8 s 后被打回出厂值,开机 load 却始终正确)。
* 故此处逐行拷到本函数栈缓冲再交给 s_split_line/s_key_value 破坏。
* 行过长(> CFG_LINE_TEST_MAX)只可能是手工编辑的未知内容或超长
* 受管键行——后者在保存侧已被 v1.3 规则拒绝落盘,读侧按"非受管行"
* 跳过(与保存侧同一前提)。
*/
static void s_parse(char *p_buf, int only_sec) {
char *p = p_buf;
static void s_parse(const char *p_buf, int only_sec) {
const char *p = p_buf;
char p_name[CFG_NAME_MAX];
char line[CFG_LINE_TEST_MAX + 1u];
int sec_idx = -1;
while (*p != '\0') {
char *p_line = s_split_line(&p);
const char *p_begin = p;
const char *p_nl = strchr(p, '\n');
size_t l;
char *p_line = line;
char *p_ws;
if (p_line[0] == '\0' || p_line[0] == ';' || p_line[0] == '#') {
if (p_nl != NULL) {
l = (size_t)(p_nl - p);
p = p_nl + 1;
}
else {
l = strlen(p);
p = p + l; /* 指向结尾 NUL:本轮结束后 while 退出 */
}
if (l > CFG_LINE_TEST_MAX) {
continue; /* 超长行:受管键/段头都不可能这么长 */
}
(void)memcpy(line, p_begin, l);
line[l] = '\0';
if (l > 0u && line[l - 1u] == '\r') {
line[l - 1u] = '\0';
}
p_ws = line;
while (*p_ws == ' ' || *p_ws == '\t') {
p_ws++;
}
if (p_ws != line) {
(void)memmove(line, p_ws, strlen(p_ws) + 1u);
}
if (line[0] == '\0' || line[0] == ';' || line[0] == '#') {
continue;
}
if (s_match_section(p_line, p_name, sizeof(p_name)) != 0) {
int i;
if (s_match_section(line, p_name, sizeof(p_name)) != 0) {
size_t i;
sec_idx = -1;
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
for (i = 0; i < (size_t)CFG_FILE_MAX_SECTIONS; i++) {
if (s_sections[i].used != 0 && s_str_ci_eq(p_name, s_sections[i].name) != 0) {
sec_idx = i;
sec_idx = (int)i;
break;
}
}
@@ -1227,7 +1276,7 @@ int cfg_file_load(void) {
syslog_post(SYSLOG_SELFTEST, "cfg load read fail");
return -1;
}
s_parse((char *)p_buf, -1);
s_parse((const char *)p_buf, -1);
cfg_store_free(p_buf);
DBG_INFO("load: OK (%luB, heap free %uB)",
(unsigned long)st.size, (unsigned)cfg_store_heap_free());
@@ -1487,7 +1536,7 @@ void cfg_file_poll(void) {
if (p_sec->p_cb != NULL) {
(void)memcpy(s_snap, p_sec->p_data, p_sec->data_size);
(void)memcpy(p_sec->p_data, p_sec->p_defaults, p_sec->data_size);
s_parse((char *)p_buf, i);
s_parse((const char *)p_buf, i);
if (memcmp(s_snap, p_sec->p_data, p_sec->data_size) != 0) {
DBG_INFO("poll: section '%s' changed, reload", p_sec->name);
p_sec->p_cb();
@@ -1495,7 +1544,7 @@ void cfg_file_poll(void) {
}
else {
(void)memcpy(p_sec->p_data, p_sec->p_defaults, p_sec->data_size);
s_parse((char *)p_buf, i);
s_parse((const char *)p_buf, i);
}
}
cfg_store_free(p_buf);