Files
wandering1 b623a406ad fix(栈·存储·UI 收尾): 断掉临界区↔断言幻影环回到 1,464,标定门控单点化,卷锁五态可观测
这四笔在主树里已经互相纠缠(同一次 @build 编译、同一份 .htm 度量),拆成四笔就不
存在任何一个被真编过的中间态,所以合一笔入库;下面按模块分段陈述,作者与出证归属
逐段写明。

—— 一、崩溃证据路径的整行独占收尾(log-maint-writer,task-26)——
上午那笔 1d4b6a17 之后,stack_report 报 iec104Task 静态最坏链 1,544 / margin 1,016,
低于 1,024 的 ISR 底线。真因不是新增局部缓冲,而是本模块与内核临界区之间新长出
一条环:taskEXIT_CRITICAL ⇒ vPortExitCritical ⇒ configASSERT ⇒ vApplicationAssertFailed
⇒ dbg_console_write_raw ⇒ 回到本模块。armlink 把这一整坨计入**每一个含临界区的
任务链**(vApplicationAssertFailed 的 Max Depth 由约 40 B 涨到 128 B + In Cycle),
所以九条链同涨约 80 B——同涨本身就排除了任何单个调用方。
改法两处:① 本模块三处窗口(取凭证 / 还凭证 / 整行入环)改用 CON_CRIT_ENTER/EXIT
(PRIMASK 保存恢复到局部变量;注释写全为什么不能直接 __enable_irq()——嵌套时会
替别人打开任务级临界区——以及屏蔽面变宽是有意为之);② 异常/终点钩子里那对
自证空转的 begin/end 摘掉(异常态 begin 放行但不持有、end 是幂等空操作,调它只有
成本没有收益),任务态 STALL 行保留 begin/end,这个区分写进两处注释与文档 §4.4.1。
实测四棵树逐个 .htm 解析(身份钉 tree + Linker run 时刻 + 产物来源):A=77779d49
链 1,456;B=1d4b6a17 链 1,464 / margin 1,096 PASS;C=含未提交钩子接线的工作树
1,544 / 1,016 FAIL;D=本轮修复后(Linker run 22:52)回到 **1,464 / 1,096 PASS**,
九条链齐降 80 B。断环图证:vPortExitCritical 由 136 B + In Cycle 变 8 B 且不再
In Cycle,con_tx_release 子树 152→8;Error_Handler 回到 280 + In Cycle = A 树同值
(既有发送环无人拆动)。顺带删掉 Src/freertos.c 崩溃路径里最后一个 HAL_Delay(200)
(该文件真实调用现为 0)。
测试:console_log 152→**153** 条(只增不减);换钉不删防——015 现在钉「PRIMASK
窗口 2 次、深度 1、内核临界区 0 次」,新钉 015c 绝不等待,160 注入点搬到
con_pm_hook,176 强化到未调度路径连 PRIMASK 窗口都不开;红线水位
con_pm_at_tx_max 与 con_uart_crit_at_tx_max 双 0;判据⑦(延后的行与当场直发逐
字节相同)170b/171/178 仍全绿。作者自报并纠正一处:换钉时曾把 TC-CONLOG-015b 改
编号,而 015b 在生产注释与文档里都被引用——已恢复「编号不退役、语义搬过去」。
覆盖率 App/log/console_log.c 199/200 → 256/257 行、21/21 函数(只增)。

—— 二、未标定闭锁判据收成单点访问器(ui-param-writer,task-16)——
「UA/UN/IA 三相 VALID 全置位才允许跑判据」这条门控原先在两处各自镜像实现
(protect_task.c 的 s_cal_locked 与 ui_data_feed.c 的 feed_cal_block),改一处另一处
静默失效且无门禁可照。现新增 adc_cal_prot_blocked() 落在 App/algo/adc_cal,DTU_BENCH
特例收进一处;两个消费者同批改调、本地副本删除。作者还查实并纠正了我先前一句
错判:全仓 App/app→App/task 的 include 实为 0 条,故访问器不能落在 protect_task。
新增 TC-ADCCAL-018 八条(含冷启动即闭锁),adc_cal 套件 99/99。

—— 三、共享卷锁五态可观测性(ui-param-writer,task-17)——
原状比登记簿写的更糟:五个可区分态(未调用 / try 超时 / 分片超时 / fail-open /
未持锁却 release)生产侧零留痕,且 vol_lock.c 的返回值注释与代码相反——
fail-open(锁没建成、互斥已失效却在读写盘)被返 1 **谎报成取到锁**,而那正是
vol_lock.h 记录的台架实证故障(并发写坏 FTL → 喂狗点饥饿 → IWDG 误复位)的前置。
锁层加四个 volatile 计数(16 B 静态,零新局部、零新 callee、零日志、零 FatFs)与
不取锁的 vol_lock_get_stats();13 处 try 调用点一字未动。**刻意不在锁层记时刻**:
记时刻要读 tick,等于给 defaultTask 取锁链添帧——诊断不值那几个字节;消费方若
自盖时间戳,那只能是被观测到的时刻而非发生时刻,.h 里已写明不得写成首次/末次。
四套本地假件各加 s_last_wait_to(字段名逐字一致),把「喂狗点取锁一律 timeout=0」
与「ftpTask 分片一律 100 ms」从注释纪律升级成会 FAIL 的断言;红-绿反证:临时把
sys_log_mcu.c 的 0u 改 50u → TC-SYSLOG-010/012 双红,把 lftpd_io.c 的 100u 改 0u →
TC-LFTPIO-034 红,两处按原串改回并用 SHA-256 证明逐字节还原。TC-VOLLOCK-015~019
共 45/45(无新桩)。

—— 四、UI 死接口清理(ui-maint-writer,task-6 收尾)——
删掉 ui_data_maint_ready():全仓零消费者、只有它自己的用例在读。**更值得记的是
它为什么能活过一整轮评审**:头注释写的是「页面用它决定这页能不能操作」,而页面
从来没用过它——注释陈述的是意图,评审读的是意图,门禁读的是可达路径,两者都不
反驳「意图未实现」。作者对「连用例一起删」做了显式抗命:TC-UIDATA-025 的 13 条里
只有 4 条读那个死口,另 9 条钉的是真防线(六口未注入安全返回 / 残表整表拒绝 /
被拒不覆盖已生效的好表),故删冗余的 025a、把 025i/025k/025m 换成用 reset_req()
的可观察返回值判定,性质不丢。ui_data 套件 244 → 243(净减 1),黄金 40 页与
池 5448 B 未变(该刀零像素影响,参考图无 diff)。

—— 门禁与未跑项(本笔如实留账)——
cubemx-survival 基线已 --write(漂移 6 项全部是上述意图内区域:断言/栈溢出钩子
实现、看门狗模块行等),基线文件同笔入库,随后 --check 通过。
本笔未跑的钩子只有两条:mem-numbers 与 mem-budget——磁盘 .map 出自 22:52 那次
构建、含尚未入库的 UI 与工具改动,与 HEAD 台账必然不同码;按仓库既有约定允许
--no-verify + 正文留证,回填在紧随的台账一笔统一做(含 ram_baseline --update 与
覆盖率基线两条 [NOTE]:ui_data.c 65→316、iec104_comm.c 36/36)。
两条已挂着的账:① id-check 不可判定引用 1,162 / 上限 1,166,**余量只剩 4**,
主要吃进者是我今天推进的 UI 那笔(68 条文本断言的注释引用),账在 Lead;
② 崩溃注入腿(故意复位)台架复验未跑,需先由代码道提交触发器,且判据只判原子性
不判 [ERR]=0——IPSR 路径的主张归主机侧套件,硬件照不出来。

产物身份(按 D 表要求钉死,避免数字与树脱钩):本笔包含的固件树就是那张 D 表对应的
那一棵——MDK-ARM/.../STM32F407-Demo.htm 的 mtime = 2026-10-26 22:52:12(.htm/.map 按
约定不入库,故把时刻写进正文)。提交前在本树复跑 stack_report.py:exit 0,
iec104Task 1,464 / margin 1,096(42%)PASS、defaultTask 3,040 / 2,080(40%)PASS,
与 D 表逐项一致;本次运行不含任何未提交固件源码之外的新改动。
2026-10-26 23:31:05 +08:00

882 lines
38 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:UI 数据快照存储(ui_data)
* 模块功能:三通道快照(实时量 rt / 参数 par / 装置信息 info)的存储、发布盖章与
* 只读访问——本模块是纯内存叶子:不取数、不格式化、不判断业务含义、
* 不碰 LVGL、不 include 任何 App 头(职责边界与依赖方向 =
* Drivers/BSP/UI/ui_data.h 文件头与
* docs/UI设计/UI_数据供给设计说明.md §3.2)
* 并发模型:单写者同上下文——唯一发布者 App/app/ui_data_feed.c 与全部页面钩子都在
* uiTask 内串行执行,故无锁、无双缓冲、无 seqlock;将来真出现第二个
* 发布者,保护收在本模块内部(双槽 + 每槽 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_maint_*:墙钟设置 / 强制复归 / 清除记录)
* ——常驻注入的 ui_data_maint_ops_t 转发,本层不解释业务含义;
* 为什么这里常驻注入而 ① 每次传表,见 ui_data.h 通道四的函数说明;
* ③ 时钟字段换算(ui_data_clock_*:本地域、无时区偏移、纯整数零依赖)
* ——「无效值拒收」的唯一判据处,也是唯一能在主机单测里跑全表的
* 日历判据(页面 .c 依赖 lvgl.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 原样入存(本层不解释含义)
* v1.2 2026-10-26 DTU 项目组 道F task-3(装置维护 3 页的前置通道):新增维护
* 写通道 ui_data_maint_*(常驻注入 ui_data_maint_ops_t:
* clock_set / reset_req / active_count / oper_note /
* clear_req / clear_state 六口,未注入一律安全返回)与
* 时钟字段换算 ui_data_clock_*(本地域无时区偏移;
* to_epoch 同时是"无效值拒收"的唯一判据处)。
* 三通道快照结构一个字节都没动 ⇒ UI_DATA_VER 保持 2
* (版本号是页面与写侧唯一的对齐凭据,无字段变更却升版
* = 让老页面以为新帧换了布局)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_data.h"
/* 静态变量定义区 */
/* 三通道存储(上电全零 = "尚无首帧",与 ui_data_init() 后状态一致) */
static ui_data_rt_t s_rt;
static ui_data_par_t s_par;
static ui_data_info_t s_info;
/*
* 发布计数:seq 由本层盖章,与入帧携带的 seq 无关——入帧的该字段被覆盖。
* 读侧"有无首帧"的判据是计数为 0,不是帧内 valid:valid 是"这一帧有没有有效
* 数据"(写侧可能发布一帧全无效的真值),两者语义不同,混用会让"首帧之前"
* 与"首帧但无数据"在页面上显示成同一种态。
*/
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)+ 写路径队列复位,
* 页面即按"无首帧/未投递"渲染
* 入口参数:无
* 返回值:无
* 限定条件:内核启动前可调(纯内存操作,零 OS 依赖);可重复调用(幂等)
* 函数说明:静态存储上电本已清零,本函数的意义是运行期复位(例如"恢复出厂后
* 重开 UI"这类后续路径),以及让宿主侧测试能随时造出"首帧之前"的状态。
* 写路径一并丢弃在队请求是**有意**的:旧请求带的是旧配置下的编辑叠层,
* 复位后还把它落进 prot_set,等于拿陈旧草稿覆盖新装载的定值
*/
void ui_data_init(void) {
(void)memset(&s_rt, 0, sizeof(s_rt));
(void)memset(&s_par, 0, sizeof(s_par));
(void)memset(&s_info, 0, sizeof(s_info));
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;
}
/*
* 函数功能:发布一帧实时量快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧(本层整帧拷入自有存储,调用方可复用其栈/静态空间)
* const ui_data_rt_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:唯一发布者 = App/app/ui_data_feed.c,uiTask 上下文
* 函数说明:seq 由本层盖章(入帧 seq 被忽略);其余字段原样入存,本层不做任何
* 业务判断与格式化
*/
int ui_data_publish_rt(const ui_data_rt_t *a_snap) {
if (a_snap == NULL) {
return -1;
}
(void)memcpy(&s_rt, a_snap, sizeof(s_rt));
s_rt_cnt += 1u;
s_rt.seq = s_rt_cnt;
return 0;
}
/*
* 函数功能:发布一帧参数快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧 const ui_data_par_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:同上;发布节拍 = 定值校验和变化才发
* (docs/UI设计/UI_数据供给设计说明.md §5.2)
*/
int ui_data_publish_par(const ui_data_par_t *a_snap) {
if (a_snap == NULL) {
return -1;
}
(void)memcpy(&s_par, a_snap, sizeof(s_par));
s_par_cnt += 1u;
s_par.seq = s_par_cnt;
return 0;
}
/*
* 函数功能:发布一帧装置信息快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧 const ui_data_info_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:同上
*/
int ui_data_publish_info(const ui_data_info_t *a_snap) {
if (a_snap == NULL) {
return -1;
}
(void)memcpy(&s_info, a_snap, sizeof(s_info));
s_info_cnt += 1u;
s_info.seq = s_info_cnt;
return 0;
}
/*
* 函数功能:读最新一帧实时量快照(整帧拷给页面私有缓存)
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_rt_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动,页面显示 "--");
* -2 - 参数无效(NULL) int
* 限定条件:页面钩子上下文(uiTask);勿在逐行 refresh 里无条件整帧拷贝,
* 先比 ui_data_rt_seq()(用法见 Drivers/BSP/UI/ui_data.h 文件头)
*/
int ui_data_rt(ui_data_rt_t *a_out) {
if (a_out == NULL) {
return -2;
}
if (s_rt_cnt == 0u) {
return -1;
}
(void)memcpy(a_out, &s_rt, sizeof(s_rt));
return 0;
}
/*
* 函数功能:读最新一帧参数快照
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_par_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动);-2 - 参数无效 int
* 限定条件:同上;定值页同样用 ui_data_par_seq() 门控页面私有缓存
*/
int ui_data_par(ui_data_par_t *a_out) {
if (a_out == NULL) {
return -2;
}
if (s_par_cnt == 0u) {
return -1;
}
(void)memcpy(a_out, &s_par, sizeof(s_par));
return 0;
}
/*
* 函数功能:读最新一帧装置信息快照
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_info_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动);-2 - 参数无效 int
* 限定条件:同上
*/
int ui_data_info(ui_data_info_t *a_out) {
if (a_out == NULL) {
return -2;
}
if (s_info_cnt == 0u) {
return -1;
}
(void)memcpy(a_out, &s_info, sizeof(s_info));
return 0;
}
/*
* 函数功能:查实时帧发布序号(页面缓存失效判据)
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无(只读单字,任意上下文安全)
* 函数说明:轻量查询——只读一个 uint32,可逐行调用;序号只在发布时 +1,页面
* 据此把"整帧拷贝"从每帧每行降到只在换帧时一次
*/
uint32_t ui_data_rt_seq(void) {
return s_rt.seq;
}
/*
* 函数功能:查参数帧发布序号
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无
*/
uint32_t ui_data_par_seq(void) {
return s_par.seq;
}
/*
* 函数功能:查装置信息帧发布序号
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无
*/
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;
}
/* ── 通道四:装置维护写请求(时钟 / 强制复归 / 清除记录)──────────────────── */
/*
* 注入的维护执行侧函数表(NULL = 未注入)。
* 只存指针不拷内容:注入方(App/task/ui_task.c 与 simulator/main.c)的两端实现
* 都是文件级 static const 表,生命周期 = 整个运行期,与 ui_w_records_bind() 存的
* reader 指针同一条约定。撤销只可能出现在宿主测试里。
*/
static const ui_data_maint_ops_t *s_maint;
/*
* 函数功能:注入装置维护执行侧函数表(启动期一次)
* 入口参数:ops - 函数表;六个成员指针都不得为 NULL;NULL = 撤销注入
* const ui_data_maint_ops_t *
* 返回值:0 - 已注入(或已撤销);-1 - ops 非空但某成员为 NULL(存量不动) int
* 限定条件:uiTask 启动期,先于 ui_menu_init()(与 ui_w_records_bind 同一处);
* uiTask 之外的上下文不得调用(本层无锁,与快照层同一条并发前提)
* 函数说明:成员逐条校验而不只看表指针——半途接错一条(例如忘了注 clear_state)
* 会让清除记录页在"按下才崩"的位置崩,生成期/启动期就拒掉整表才是省事
*/
int ui_data_maint_bind(const ui_data_maint_ops_t *a_ops) {
if (a_ops == NULL) {
s_maint = NULL; /* 撤销注入:页面随即按"能力不可用"渲染 */
return 0;
}
if ((a_ops->clock_set == NULL) || (a_ops->reset_req == NULL) ||
(a_ops->active_count == NULL) || (a_ops->oper_note == NULL) ||
(a_ops->clear_req == NULL) || (a_ops->clear_state == NULL)) {
return -1;
}
s_maint = a_ops;
return 0;
}
/*
* 函数功能:请求设置墙钟(时钟设置页"保存"行的唯一出口)
* 入口参数:unix_epoch - 已整组校验过的秒(见 ui_data_clock_to_epoch) uint32_t
* 返回值:0 - 执行侧受理;-1 - 未注入或执行侧失败;-2 - 参数无效(0 = 本层保留的
* "未初始化"哨兵,与 sys_clock 的 UINT32_MAX 哨兵不同族,
* 这里只挡 0 这一个明显不该写进 RTC 的值) int
* 限定条件:uiTask 上下文(页面按键钩子)
* 函数说明:本层不判日期合法性——判据只有一份,在 ui_data_clock_to_epoch();
* 也不做回读,回读属执行侧(它才知道 sys_clock_get() 的真值)
*/
int ui_data_maint_clock_set(uint32_t a_unix_epoch) {
if (s_maint == NULL) {
return -1;
}
if (a_unix_epoch == 0u) {
return -2;
}
return s_maint->clock_set(a_unix_epoch);
}
/*
* 函数功能:请求复归全部保护元件(强制复归页的执行出口)
* 入口参数:无
* 返回值:0 - 已受理;-1 - 未注入或执行侧拒收 int
* 限定条件:uiTask 上下文
* 函数说明:本函数**不投**操作记录,页面在返 0 之后自己投("本地面板发起"这个
* 事实只有页面知道;远端 104 那条另有投递点,两边各一条、不得互替);
* 返 -1 时页面必须一条都不投——给一次没发生的操作留审计记录,比少一条
* 记录坏得多
*/
int ui_data_maint_reset_req(void) {
if (s_maint == NULL) {
return -1;
}
return s_maint->reset_req();
}
/*
* 函数功能:取当前动作元件数(复归页"保护动作 N"行的数据源)
* 入口参数:无
* 返回值:元件数;未注入返回 0 uint32_t
* 限定条件:页面绘制路径可直调(一次函数指针转发,零 I/O、零锁)
* 函数说明:返回类型取 uint32_t 与执行侧位图同宽,不在本层压成 uint8_t——
* 元件数将来超 8 时,窄化会在页面里静默截成 0("明明动作了屏上说没有")
*/
uint32_t ui_data_maint_active_count(void) {
if (s_maint == NULL) {
return 0u;
}
return s_maint->active_count();
}
/*
* 函数功能:投一条操作记录留痕
* 入口参数:desc - 描述串(ASCII 常量;NULL = 什么都不做) const char *
* 返回值:0 - 已投递;-1 - 未注入或 desc 为 NULL int
* 限定条件:uiTask 上下文
* 函数说明:投递可能被队列丢弃(执行侧口径)——本层不因此把屏上的"操作成功"
* 翻成失败:记录通道故障不等于操作没发生,混起来只会让现场误判装置
*/
int ui_data_maint_oper_note(const char *a_desc) {
if ((s_maint == NULL) || (a_desc == NULL)) {
return -1;
}
s_maint->oper_note(a_desc);
return 0;
}
/*
* 函数功能:请求清除某类记录(清除记录页的执行出口)
* 入口参数:log_id - UI 侧记录源编号(UI_W_REC_LOG_*) uint8_t
* 返回值:0 - 已受理;-1 - 未注入或执行侧拒绝 int
* 限定条件:uiTask 上下文;"受理"≠"已清除",终态读 ui_data_maint_clear_state()
* 函数说明:不在本层判 log_id 上界——上界的真源是记录侧有几个文件(映射表在
* 适配层),这里再抄一份长度就成了第二真源,记录加一类时必然漏改
*/
int ui_data_maint_clear_req(uint8_t a_log_id) {
if (s_maint == NULL) {
return -1;
}
return s_maint->clear_req(a_log_id);
}
/*
* 函数功能:查某类记录的清除状态
* 入口参数:log_id - 记录源编号 uint8_t
* 返回值:UI_DATA_CLEAR_ST_NONE / _OK / _WAIT / _FAIL;未注入返回 NONE int
* 限定条件:页面绘制路径可直调
* 函数说明:返回值直通执行侧、不做二次加工(与 ui_data_param_state() 同理:
* 两处解释同一件事迟早漂移);WAIT 是正常态,页面不得画成失败
*/
int ui_data_maint_clear_state(uint8_t a_log_id) {
if (s_maint == NULL) {
return UI_DATA_CLEAR_ST_NONE;
}
return s_maint->clear_state(a_log_id);
}
/* ── 时钟字段换算(本地域,无时区偏移)──────────────────────────────────── */
/*
* 私有宏定义区(本小节自带,不与其它小节共用):
* 算法 = Howard Hinnant 的 days_from_civil / civil_from_days(以 0000-03-01
* 为纪元、400 年一周期),与 App/protocol/iec104/iec104_asdu.c 的正反两套实现
* 同族;本工程年域 2000..2038,全程非负,用 uint32 就够,不引 int64 也不引 time.h
*/
#define CL_SECONDS_PER_DAY 86400u
#define CL_SECONDS_OF_DAY_MAX 11647u /* 0x7FFFFFFF % 86400 = 2038-01-19 的 03:14:07 */
#define CL_DAYS_EPOCH_MAX 24855u /* 0x7FFFFFFF / 86400 = 2038-01-19(距 1970-01-01 天数) */
#define CL_DAYS_TO_0000 719468u /* 1970-01-01 距 0000-03-01 纪元的天数(Hinnant 偏移量) */
#define CL_DAYS_PER_ERA 146097u /* 400 年一周期的天数 */
/*
* 函数功能:公历日期 → 距 1970-01-01 的天数(调用方保证字段已判过合法性)
* 入口参数:year - 年(>= 1) uint16_t month - 月 1..12 uint8_t day - 日 1..31 uint8_t
* 返回值:天数(本工程年域内恒非负) uint32_t
* 限定条件:月/日未做合法性判定——**合法性判定在 to_epoch(),不在这里**,
* 换算函数保持"给什么算什么"才能与反向函数严格互逆
* 函数说明:先做 1/2 月归到上一年的 11/12 月(y 相应减一),使 3 月成为岁首、
* 闰日落在周期末端,之后每月天数变成 31 天/30.6 天的规整式子
*/
static uint32_t ui_data_clock_days_from_civil(uint16_t a_year, uint8_t a_month, uint8_t a_day) {
uint32_t y = (a_month <= 2u) ? ((uint32_t)a_year - 1u) : (uint32_t)a_year;
uint32_t era = y / 400u;
uint32_t yoe = y - (era * 400u); /* [0, 399] */
uint32_t mp = (a_month > 2u) ? ((uint32_t)a_month - 3u) : ((uint32_t)a_month + 9u);
uint32_t doy = ((153u * mp + 2u) / 5u) + (uint32_t)a_day - 1u; /* [0, 365] */
uint32_t doe = (yoe * 365u) + (yoe / 4u) - (yoe / 100u) + doy; /* [0, 146096] */
return (era * CL_DAYS_PER_ERA) + doe - CL_DAYS_TO_0000;
}
/*
* 函数功能:判断是否闰年(完整格里历规则)
* 入口参数:year - 年 uint16_t
* 返回值:1 - 闰年;0 - 平年 uint8_t
* 限定条件:与 App/util/sys_clock.c 的 unix_to_sd2506() 内联判据逐字同一条规则
* (4 年 && 非 100 年 || 400 年);2100 不在允许年域内,无需讨论该例外
* 函数说明:取 uint8_t 而不是 bool——本工程布尔判定允许 bool,但本函数参与
* 天数计算,返回宽度明确的整型省掉一次隐式转换(同 ui_w_editor_key()
* 把"分发标志"写成 uint8_t 的口径)
*/
static uint8_t ui_data_clock_is_leap(uint16_t a_year) {
if ((((uint32_t)a_year % 4u) == 0u) && (((uint32_t)a_year % 100u) != 0u)) {
return 1u;
}
if (((uint32_t)a_year % 400u) == 0u) {
return 1u;
}
return 0u;
}
/*
* 函数功能:取某年某月的天数(含闰年 2 月)
* 入口参数:year - 年 uint16_t month - 月 1..12 uint8_t
* 返回值:28 / 29 / 30 / 31;月份为 0 或 > 12 时返回 0(调用方以 0 判非法,
* 不开第二条错误码) uint8_t
* 限定条件:无(纯查表 + 闰年判定,零依赖)
* 函数说明:表序 = 1 月起(下标 0 是 1 月),与 sys_clock.c:145 那份 mdays[]
* 的"0 月起"排法**故意不同**——本表按屏上月的直觉排(下标 = 月 - 1),
* 少一次心算 -1 就少一次错位;两侧同一张表的等价性由主机单测逐月核对
*/
uint8_t ui_data_clock_days_in_month(uint16_t a_year, uint8_t a_month) {
static const uint8_t k_mdays[12] = {
31u, 28u, 31u, 30u, 31u, 30u, 31u, 31u, 30u, 31u, 30u, 31u
};
if ((a_month < 1u) || (a_month > 12u)) {
return 0u;
}
if ((a_month == 2u) && (ui_data_clock_is_leap(a_year) != 0u)) {
return 29u;
}
return k_mdays[a_month - 1u];
}
/*
* 函数功能:日历字段 → 本地域 epoch 秒,同时充当这组字段的合法性判据
* 入口参数:p_in - 六个字段 const ui_data_clock_t *
* p_out - 秒 uint32_t *
* 返回值:0 - 合法并已输出;-1 - 参数为 NULL;-2 - 字段组合非法(任一段越界,
* 含 2 月 30 日、非闰年 2 月 29 日、月 13、时 24、分/秒 60);
* -3 - 字段各自合法但换算结果越界(> UI_DATA_CLOCK_EPOCH_MAX) int
* 限定条件:年段先判(2000..2038),因此天数计算全程在非负 uint32 域内
* 函数说明:三段边界分开返码,是为了让页面能给出现场听得懂的话:"2 月 30 日"
* 是输入错(-2,光标回到出问题那一行),而"2038 年之后"是能力边界
* (-3,说这一版装置的墙钟就到这儿)——两种情况混成一个 ERR,
* 用户会反复试一个永远试不出的日期
* ⚠ 全程不含时区偏移(口径见 ui_data.h 本节注释与
* App/task/ui_task.c 的 SOE 时标注记):加 28800 的后果是"屏上输
* 08:00、存进去 16:00",且模拟器里往返照样自洽、照不出来
*/
int ui_data_clock_to_epoch(const ui_data_clock_t *p_in, uint32_t *p_out) {
uint32_t days;
uint32_t sod;
uint32_t dmax;
if ((p_in == NULL) || (p_out == NULL)) {
return -1;
}
if ((p_in->year < (uint16_t)UI_DATA_CLOCK_YEAR_MIN) ||
(p_in->year > (uint16_t)UI_DATA_CLOCK_YEAR_MAX)) {
return -2;
}
if ((p_in->month < 1u) || (p_in->month > 12u)) {
return -2;
}
dmax = (uint32_t)ui_data_clock_days_in_month(p_in->year, p_in->month);
if ((p_in->day < 1u) || ((uint32_t)p_in->day > dmax)) {
return -2;
}
if (p_in->hour > 23u) {
return -2;
}
if ((p_in->minute > 59u) || (p_in->second > 59u)) {
return -2;
}
days = ui_data_clock_days_from_civil(p_in->year, p_in->month, p_in->day);
sod = ((uint32_t)p_in->hour * 3600u) + ((uint32_t)p_in->minute * 60u) +
(uint32_t)p_in->second;
if ((days > CL_DAYS_EPOCH_MAX) || ((days == CL_DAYS_EPOCH_MAX) && (sod > CL_SECONDS_OF_DAY_MAX))) {
return -3; /* 2038-01-19 03:14:07 之后:uint32 秒域到头,不是用户输错 */
}
*p_out = (days * CL_SECONDS_PER_DAY) + sod;
return 0;
}
/*
* 函数功能:本地域 epoch 秒 → 日历字段(ui_data_clock_to_epoch 的反函数)
* 入口参数:unix_epoch - 秒 uint32_t p_out - 输出 ui_data_clock_t *
* 返回值:0 - 已输出;-1 - p_out 为 NULL;-2 - 超出允许域(0 哨兵、年 < 2000、
* 或 > UI_DATA_CLOCK_EPOCH_MAX) int
* 限定条件:与 to_epoch 严格互逆(主机单测按往返相等钉,见 test/host/ui_data);
* 刻意把 0 也判越界:0 = 1970-01-01,落在 RTC 年域(2000..2099)之外,
* 而它又恰好是"墙钟无效"在记录侧的占位起点,绝不允许被当真实时间显示
*/
int ui_data_clock_from_epoch(uint32_t a_unix_epoch, ui_data_clock_t *p_out) {
uint32_t days;
uint32_t sod;
uint32_t z;
uint32_t era;
uint32_t doe;
uint32_t yoe;
uint32_t doy;
uint32_t mp;
uint32_t y;
uint32_t m;
if (p_out == NULL) {
return -1;
}
if ((a_unix_epoch == 0u) || (a_unix_epoch > UI_DATA_CLOCK_EPOCH_MAX)) {
return -2;
}
days = a_unix_epoch / CL_SECONDS_PER_DAY;
sod = a_unix_epoch % CL_SECONDS_PER_DAY;
z = days + CL_DAYS_TO_0000; /* 移到 0000-03-01 纪元 */
era = z / CL_DAYS_PER_ERA;
doe = z - (era * CL_DAYS_PER_ERA); /* [0, 146096] */
yoe = (doe - (doe / 1460u) + (doe / 36524u) - (doe / 146096u)) / 365u; /* [0, 399] */
doy = doe - ((365u * yoe) + (yoe / 4u) - (yoe / 100u));
mp = (5u * doy + 2u) / 153u;
/* 3 月纪元月序转公历月序:mp < 10 落在 3..12 月,否则落在 1/2 月并归上一年 */
y = yoe + (era * 400u);
if (mp < 10u) {
m = mp + 3u;
} else {
m = mp - 9u;
y += 1u; /* 1/2 月属于上一年 */
}
p_out->year = (uint16_t)y;
p_out->month = (uint8_t)m;
p_out->day = (uint8_t)(doy - ((153u * mp + 2u) / 5u) + 1u);
p_out->hour = (uint8_t)(sod / 3600u);
p_out->minute = (uint8_t)((sod / 60u) % 60u);
p_out->second = (uint8_t)(sod % 60u);
if ((p_out->year < (uint16_t)UI_DATA_CLOCK_YEAR_MIN) ||
(p_out->year > (uint16_t)UI_DATA_CLOCK_YEAR_MAX)) {
return -2;
}
return 0;
}
/*
* 函数功能:解析定长时标串为日历字段
* 入口参数:ts - "YYYY-MM-DD HH:MM:SS"(快照 rt.clock 的形状) const char *
* p_out - 输出 ui_data_clock_t *
* 返回值:0 - 成功;-1 - 参数为 NULL;-2 - 串型不对(不是 19 字符、分隔位不是
* '-'/' '/':'、出现非数字)**或**型对但值非法
* (组合再过 ui_data_clock_to_epoch() 把关) int
* 限定条件:空串(墙钟无效时快照里的取值,ui_data.h:149)走 -2;
* 本函数**不猜**:"1970-01-01 08:00:00"这类记录侧占位串在这里同样返 -2
* (年 1970 出域),页面据此显示"无时钟"而不是把占位串画成真实时间
* 函数说明:手写十进制累加而不用 sscanf——本模块是零 LVGL、零 App 的纯内存叶子,
* 拉进 stdio 就等于给主机单测加依赖、给 MCU 端加一份 printf 家族
*/
int ui_data_clock_parse(const char *a_ts, ui_data_clock_t *p_out) {
ui_data_clock_t t;
uint32_t i;
uint32_t acc;
const char *p;
if ((a_ts == NULL) || (p_out == NULL)) {
return -1;
}
(void)memset(&t, 0, sizeof(t));
/* 五段数字:年 4 位、月/日/时/分/秒各 2 位;段间分隔位逐位核对 */
p = a_ts;
acc = 0u;
for (i = 0u; i < 4u; i++) {
if ((p[i] < '0') || (p[i] > '9')) {
return -2;
}
acc = (acc * 10u) + (uint32_t)(p[i] - '0');
}
t.year = (uint16_t)acc;
if (p[4] != '-') {
return -2;
}
p += 5u;
{
uint8_t *const fields[5] = { &t.month, &t.day, &t.hour, &t.minute, &t.second };
const char seps[4] = { '-', ' ', ':', ':' };
for (i = 0u; i < 5u; i++) {
if (((p[0] < '0') || (p[0] > '9')) || ((p[1] < '0') || (p[1] > '9'))) {
return -2;
}
*fields[i] = (uint8_t)(((uint32_t)(p[0] - '0') * 10u) + (uint32_t)(p[1] - '0'));
p += 2u;
if (i < 4u) {
if (*p != seps[i]) {
return -2;
}
p++;
}
}
}
if (*p != '\0') {
return -2; /* 型对但尾部还有字符:不是本工程认的时标串 */
}
{
uint32_t probe;
if (ui_data_clock_to_epoch(&t, &probe) != 0) {
return -2;
}
}
*p_out = t;
return 0;
}
/*
* 函数功能:日历字段 → 定长时标串(与 ui_data_clock_parse 同串型)
* 入口参数:p_in - 字段 const ui_data_clock_t *
* buf - 输出缓冲 char *
* len - 缓冲容量(含 NUL,至少 UI_DATA_CLOCK_TS_LEN + 1 = 20) uint32_t
* 返回值:0 - 已写出;-1 - 参数无效或容量不足(**不写半个串**,避免调用方
* 把截断串当完整时标显示) int
* 限定条件:不判字段合法性(调用方刚从 parse/to_epoch 那条链出来,再判一遍
* 就是第二条判据)
*/
int ui_data_clock_format(const ui_data_clock_t *p_in, char *a_buf, uint32_t a_len) {
uint8_t i;
char *p;
uint32_t v;
if ((p_in == NULL) || (a_buf == NULL) ||
(a_len < (UI_DATA_CLOCK_TS_LEN + 1u))) {
return -1;
}
p = a_buf;
v = (uint32_t)p_in->year;
for (i = 0u; i < 4u; i++) { /* 年份 4 位补零 */
p[3u - i] = (char)('0' + (uint32_t)(v % 10u));
v /= 10u;
}
p[4] = '-';
p += 5;
{
const uint8_t fields[5] = { p_in->month, p_in->day, p_in->hour,
p_in->minute, p_in->second };
const char seps[4] = { '-', ' ', ':', ':' };
for (i = 0u; i < 5u; i++) {
p[0] = (char)('0' + ((uint32_t)fields[i] / 10u));
p[1] = (char)('0' + ((uint32_t)fields[i] % 10u));
p += 2;
if (i < 4u) {
*p = seps[i];
p++;
}
}
}
*p = '\0';
return 0;
}