/* * 模块名称:UI Page RunReport(运行报告内容页) * 模块功能:运行报告叶子节点(node 41,父节点「记录查询」)的内容页数据——13 行只读统计, * 分 2 页(UI_LIST_ROWS=8 行/页 → 8 + 5);数据全部经 ui_data 装置信息帧下发 * (写侧 App/app/ui_data_feed.c 搬 boot_stats_get() 与 iec104_serv_get_stats(), * 本文件零 App include,分层方向由 layer-deps 门禁守护) * 口径纪律(判据 docs/任务规划/UI补齐.md §2「运行报告」行 + App/util/boot_stats.h 四数台账): * ① **四个数逐个挂范围标签**,不许含糊——上电次数=自 RTC 电池周期(换电池即清零, * **禁写终身次数**,固件里压根没有跨开机计数器)、复位原因/运行时长/104 统计 * =本次开机以来(104 四枚跨重连不清零,但**断电即归零**); * ② boot_ready==0(账本未初始化)或 seq_state==0(UNKNOWN)时上电次数显示 "--" * 而不是 0——把"没有数"洗成"0 次"是最坏的一种误导; * ③ write_failed==1 单独占一行("未写回"):后果是账本**少计**,绝不会重复计数 * (掉电保持数 +1 后先写 SRAM 再发布,写失败仍按内存值显示); * ④ 运行时长是 tick 回绕口径(32 位 ms ≈ 49.7 天、uptime_s ≈ 136 年), * 显示按"天 + 时时:分分"退化(与装置信息页同一 ASCII 记法,见 di_fmt_uptime), * **不做跨开机累计**(用户裁定 2026-10-27) * 几何余量(2026-10-28 黄金目检):「自电池周期」5 字 x12px 从 x=101 起、右缘 161,**余量 0**—— * 值列再加字或换字号必切边;要扩文案必须先减字或改标签列宽度,别直接加字。 * 台架待补(不在本批,已登记 docs/任务规划/登记簿.md):真机上电一次看 boot_count +1、 * 拔电换电池后 seq_state 落 RESTARTED、IWDG 复位后 rst_cause 落 IWDG、 * 主站连一次看 conn_up +1 —— 模拟器无 boot_stats(依赖 SD2506 I2C 与 RCC_CSR) * 与 104 协议栈,黄金参考拍的是固定假数据 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:DTU 项目组 * 创建日期:2026-10-28 * 修改记录: * v1.0 2026-10-28 DTU 项目组 创建(形态照 ui_page_device_info.c 的行表 + 值格式化, * 分页与选中带照 ui_page_deadband.c 的列表页几何) */ /* 头文件包含区 */ #include #include "ui_page_runreport.h" #include "ui_data.h" #include "ui_tokens.h" /* 私有宏定义区 */ #define RR_ROW_N 13u /* 行数(2 页:8 + 5) */ #define RR_INVALID "--" #define RR_SEC_PER_DAY 86400u /* 复位原因编码(与 App/util/boot_stats.h 的 BOOT_STATS_CAUSE_* 一一对应; * 这里刻意写成数字常量并在注释里钉死映射——BSP 不得 include App 头, * 编码变更由 test/host/ui_data 的搬运断言与 App/log/README 口径联动发现 */ #define RR_CAUSE_UNKNOWN 0u #define RR_CAUSE_IWDG 1u #define RR_CAUSE_WWDG 2u #define RR_CAUSE_SFT 3u #define RR_CAUSE_POR 4u #define RR_CAUSE_PIN 5u #define RR_CAUSE_BOR 6u /* 上电次数可信度编码(BOOT_STATS_SEQ_*) */ #define RR_SEQ_UNKNOWN 0u #define RR_SEQ_FRESH 1u #define RR_SEQ_CONTINUED 2u #define RR_SEQ_RESTARTED 3u /* 静态变量定义区 */ /* 行名表(Flash;四数各自挂范围行,口径不许含糊) */ static const char *const s_names[RR_ROW_N] = { "上电次数", "计数范围", "计数可信", "计数写回", "复位原因", "复位范围", "运行时长", "时长范围", "主站接入", "发送帧数", "发送失败", "接收字节", "统计范围", }; /* 帧缓存:发布序号变化才整帧拷一次(同装置信息页口径) */ static ui_data_info_t s_info; static uint32_t s_seq; /* 函数定义区 */ /* * 函数功能:取用最新装置信息帧(只在发布序号变化时整帧拷) * 入口参数:无 * 返 回 值:1 - 有可用帧;0 - 首帧未到或本帧无效 uint8_t * 限定条件:uiTask 绘制上下文 */ static uint8_t rr_frame_ready(void) { uint32_t now = ui_data_info_seq(); if (now != s_seq) { s_seq = now; if (ui_data_info(&s_info) != 0) { (void)memset(&s_info, 0, sizeof(s_info)); return 0u; } } return (s_info.valid != 0u) ? 1u : 0u; } /* * 函数功能:开页复位缓存(强制下一次绘制重取最新帧,避免停在上一帧的计数上) * 入口参数:无 * 返 回 值:无 void * 限定条件:开页时由框架调用 */ static void rr_init(void) { s_seq = 0u; (void)memset(&s_info, 0, sizeof(s_info)); } /* * 函数功能:复位原因编码 → 屏上中文(5 字以内,避免挤压值列) * 入口参数:cause - 复位原因编码(RR_CAUSE_*) uint8_t * 返 回 值:中文标签(Flash 字面量,恒非 NULL) const char * * 限定条件:编码越界(含 SD2506 诊断混进来的位)一律落"未知",不猜 */ static const char *rr_cause_str(uint8_t cause) { const char *p; switch (cause) { case RR_CAUSE_IWDG: p = "独立看门狗"; break; case RR_CAUSE_WWDG: p = "窗口看门狗"; break; case RR_CAUSE_SFT: p = "软件复位"; break; case RR_CAUSE_POR: p = "上电复位"; break; case RR_CAUSE_PIN: p = "引脚复位"; break; case RR_CAUSE_BOR: p = "掉电复位"; break; default: p = "未知"; break; } return p; } /* * 函数功能:上电次数可信度编码 → 屏上中文 * 入口参数:state - 可信度编码(RR_SEQ_*) uint8_t * 返 回 值:中文标签(Flash 字面量,恒非 NULL) const char * * 限定条件:编码越界一律"未知" * 函数说明:RESTARTED 的屏上话是"掉电重开"——它指的是 RTC 电池域掉过电 * (计数值本身还在,但跨不掉电那段已经不可信),不是"整机重启过" */ static const char *rr_seq_str(uint8_t state) { const char *p; switch (state) { case RR_SEQ_FRESH: p = "首次启用"; break; case RR_SEQ_CONTINUED: p = "延续"; break; case RR_SEQ_RESTARTED: p = "掉电重开"; break; default: p = "未知"; break; } return p; } /* * 函数功能:格式化运行时长(本次开机以来,秒) * 入口参数:sec - 秒数 uint32_t * a_buf - 输出缓冲 char * * a_cap - 缓冲容量 uint32_t * 返 回 值:无 void * 限定条件:a_buf/a_cap 有效 * 函数说明:>= 1 天走 "%ud%02u:%02u"(ASCII 小写 d,避免为"天"字扩字模); * < 1 天走 "%02u:%02u:%02u"。与装置信息页同一记法,两页不许各写一套 */ static void rr_fmt_uptime(uint32_t sec, char *a_buf, uint32_t a_cap) { uint32_t day = sec / RR_SEC_PER_DAY; uint32_t rem = sec % RR_SEC_PER_DAY; if (day > 0u) { (void)lv_snprintf(a_buf, a_cap, "%luud%02lu:%02lu", (unsigned long)day, (unsigned long)(rem / 3600u), (unsigned long)((rem % 3600u) / 60u)); } else { (void)lv_snprintf(a_buf, a_cap, "%02lu:%02lu:%02lu", (unsigned long)(rem / 3600u), (unsigned long)((rem % 3600u) / 60u), (unsigned long)(rem % 60u)); } } /* * 函数功能:总行数(固定 12 行,分 2 页) * 入口参数:无 * 返 回 值:行数 uint16_t * 限定条件:uiTask 绘制上下文(经框架回调) */ static uint16_t rr_count(void) { return RR_ROW_N; } /* * 函数功能:逐行取名/值(13 行,见 s_names 表) * 入口参数:row - 行下标 uint16_t * name - 名输出缓冲 char * * value - 值输出缓冲 char * * 返 回 值:无 void * 限定条件:uiTask 绘制上下文;首帧未到时数值行一律 "--"(不显示 0 冒充有数) */ static void rr_refresh(uint16_t row, char *name, char *value) { const uint32_t cap = (uint32_t)UI_BUF_STRLEN; uint8_t ready; if (row >= RR_ROW_N) { value[0] = (char)0; return; } (void)lv_snprintf(name, cap, "%s", s_names[row]); ready = rr_frame_ready(); if (ready == 0u) { (void)lv_snprintf(value, cap, "%s", RR_INVALID); return; } switch (row) { case 0u: /* 上电次数 */ if ((s_info.boot_ready == 0u) || (s_info.boot_seq_state == RR_SEQ_UNKNOWN)) { (void)lv_snprintf(value, cap, "%s", RR_INVALID); } else { (void)lv_snprintf(value, cap, "%lu", (unsigned long)s_info.boot_count); } break; case 1u: /* 计数范围 */ (void)lv_snprintf(value, cap, "%s", (s_info.boot_ready == 0u) ? "未知" : "自电池周期"); break; case 2u: /* 计数可信(四态;未初始化落"未知") */ (void)lv_snprintf(value, cap, "%s", rr_seq_str(s_info.boot_seq_state)); break; case 3u: /* 计数写回 */ (void)lv_snprintf(value, cap, "%s", (s_info.boot_write_failed != 0u) ? "未写回" : "正常"); break; case 4u: /* 复位原因 */ (void)lv_snprintf(value, cap, "%s", rr_cause_str(s_info.rst_cause)); break; case 5u: /* 复位范围 */ (void)lv_snprintf(value, cap, "%s", "本次开机"); break; case 6u: /* 运行时长 */ rr_fmt_uptime(s_info.uptime_s, value, cap); break; case 7u: /* 时长范围 */ (void)lv_snprintf(value, cap, "%s", "本次开机"); break; case 8u: (void)lv_snprintf(value, cap, "%lu", (unsigned long)s_info.iec104_conn_up); break; case 9u: (void)lv_snprintf(value, cap, "%lu", (unsigned long)s_info.iec104_apdu_tx); break; case 10u: (void)lv_snprintf(value, cap, "%lu", (unsigned long)s_info.iec104_tx_fail); break; case 11u: (void)lv_snprintf(value, cap, "%lu", (unsigned long)s_info.iec104_rx_bytes); break; default: /* 统计范围 */ (void)lv_snprintf(value, cap, "%s", "本次开机"); break; } } /* * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=runreport 绑定引用): * 只读列表页——selectable=1 走真分页(13 行 / UI_LIST_ROWS 行 = 2 页,页脚真实页码); * band_h=UI_ROW_BAND_H(选中行黑字须配白带);无写入口,故无保存态/无三态 */ const content_page_def_t ui_page_runreport_desc = { .id = "runreport", .title = "运行报告", .selectable = 1u, .top_y = UI_LIST_TOP, .pitch = UI_ROW_PITCH, .band_h = UI_ROW_BAND_H, .text_dy = 0, .left_x = UI_LIST_LEFT_X, .right_x = UI_LIST_RIGHT_X, .rows_per_page = UI_LIST_ROWS, .count_cb = rr_count, .init_cb = rr_init, .refresh = rr_refresh, };