/****************************************************************************** * @file app_light_life.c * @brief 灯泡剩余寿命(规格书 13.2 节):实际吸合扣秒 / APP 写 DP3 / * 到期告警 / 60s 节流上报 / 边沿立刻落盘 * @author cyWu <1917507415@qq.com> * @date 2026.09.08 * @version V1.0.1 * * @note 扣秒与运行记录同口径:只看 app_relay_get_actual()==1(过零后已吸合)。 * 上下文分工(避免 ISR 写 VM / 切 BLE): * - tick(20ms ISR):只折秒到 s_life.remain,标记 need_sync / * need_edge(到期),不碰 gDevData / flash。 * - loop(500ms 任务):60s 节流同步 gDevData.lighe_remain_time; * 边沿立刻同步 + 推导 gDevData.alarm_code + 立刻落盘。 * BOARD_LIGHT_LIFE_ENABLE=0:不扣秒、不置到期告警;DP3 走 VM/默认 * 值原样上报(规格书 13.2 / 第 0 章第 8 条)。 ******************************************************************************/ #include #include "system/includes.h" /* local_irq_disable / local_irq_enable */ #include "app_light_life.h" #include "app_relay.h" #include "app_nvm.h" #include "jb_product.h" #include "board_pin.h" #define LOG_TAG_CONST APP #define LOG_TAG "[LIGHT]" #define LOG_INFO_ENABLE #include "debug.h" /** 剩余寿命运行上下文 */ typedef struct { volatile uint32_t remain; /**< 权威剩余寿命秒(ISR 扣减,任务读) */ volatile uint16_t ms; /**< 不足 1 秒的毫秒零头(仅 ISR) */ volatile uint16_t since_sync; /**< 距上次同步 gDevData 的秒数(60s 节流) */ volatile uint32_t since_save; /**< 距上次落盘的累计运行秒(600s 节奏) */ volatile uint8_t need_sync; /**< 1=ISR 要求任务同步 gDevData(60s 到点) */ volatile uint8_t need_edge; /**< 1=边沿(到期)需立刻同步 + 落盘 */ } LightLifeCtx_t; static LightLifeCtx_t s_life; #if BOARD_LIGHT_LIFE_ENABLE /** * @brief 按当前 remain 推导告警并同步 gDevData(任务上下文) * @param immediate 1=边沿立刻落盘;0=仅同步(60s 节流点,落盘随 600s) * @return 无 */ static void life_sync_gdata(uint8_t immediate) { gDevData.lighe_remain_time = s_life.remain; gDevData.alarm_code = (s_life.remain == 0) ? 1 : 0; if (immediate) { local_irq_disable(); s_life.since_save = 0; local_irq_enable(); app_nvm_save_runrec(); /* 寿命随 runrec item 落盘(规格书 13.2 边沿) */ } } #endif /** * @brief 初始化剩余寿命:默认额定值(VM 有效时 app_nvm_load 会覆盖) * @return 无 */ void app_light_life_init(void) { #if !BOARD_LIGHT_LIFE_ENABLE log_info("light life disabled\n"); return; #else memset(&s_life, 0, sizeof(s_life)); s_life.remain = BOARD_LIGHT_RATED_SEC; /* 首上电(VM 无效)DP3 即额定值、DP25=0(规格书 17 章 P1 末条); * VM 有效时 app_nvm_load 会再调 set_remain 覆盖 */ gDevData.lighe_remain_time = BOARD_LIGHT_RATED_SEC; gDevData.alarm_code = 0; log_info("light life init, remain=%lu\n", (unsigned long)s_life.remain); #endif } /** * @brief 20ms 业务节拍:实际吸合才扣秒(与运行记录同口径) * @param dt 距上次调用的毫秒数 * @return 无 */ void app_light_life_tick(uint16_t dt) { #if !BOARD_LIGHT_LIFE_ENABLE (void)dt; return; #else if (!app_relay_get_actual()) { s_life.ms = 0; /* 没在点灯:丢弃不足 1 秒的零头(13.2 同口径) */ return; } if (s_life.remain == 0) { return; /* 已到期:停在 0,不回绕 */ } s_life.ms = (uint16_t)(s_life.ms + dt); while (s_life.ms >= 1000u) { s_life.ms = (uint16_t)(s_life.ms - 1000u); s_life.remain--; s_life.since_sync++; s_life.since_save++; if (s_life.remain == 0) { s_life.need_edge = 1; /* 到期边沿:任务立刻同步 + 落盘 + 上报 */ } } /* 60s 节流:点灯且未到期时满 60s 才允许同步一次 DP3(规格书 13.2) */ if ((s_life.since_sync >= BOARD_LIGHT_REPORT_MS / 1000u) && !s_life.need_edge) { s_life.since_sync = 0; s_life.need_sync = 1; } #endif } /** * @brief 500ms 任务上下文:节流同步 / 边沿立刻同步 + 落盘 * @return 无 */ void app_light_life_loop(void) { #if !BOARD_LIGHT_LIFE_ENABLE return; #else uint8_t edge; uint8_t sync; local_irq_disable(); edge = s_life.need_edge; sync = s_life.need_sync; s_life.need_edge = 0; s_life.need_sync = 0; local_irq_enable(); if (edge) { life_sync_gdata(1); log_info("life expired, remain=0 alarm=1\n"); return; } if (sync) { life_sync_gdata(0); /* 60s 节流点:只同步,flash 随 600s */ } /* 平时落盘节奏:累计点灯满 600s 写一次(与运行记录同次可接受误差) */ local_irq_disable(); if (s_life.since_save >= BOARD_RUN_RECORD_SAVE_SEC) { s_life.since_save = 0; local_irq_enable(); app_nvm_save_runrec(); } else { local_irq_enable(); } #endif } /** * @brief 设置剩余寿命(APP 写 DP3 / 出厂复位,任务上下文) * @param remain 剩余寿命秒,0~0xFFFFFFFF 不封顶 * @return 无 */ void app_light_life_set_remain(uint32_t remain) { #if BOARD_LIGHT_LIFE_ENABLE local_irq_disable(); s_life.remain = remain; s_life.since_sync = 0; s_life.since_save = 0; s_life.ms = 0; local_irq_enable(); /* 边沿立刻生效:同步 gDevData + 推导告警 + 立刻落盘 */ gDevData.lighe_remain_time = remain; gDevData.alarm_code = (remain == 0) ? 1 : 0; app_nvm_save_runrec(); log_info("life set remain=%lu alarm=%d\n", (unsigned long)remain, gDevData.alarm_code); #else /* 模块关闭:只回填 DP3,不因寿命置告警(规格书 13.2) */ gDevData.lighe_remain_time = remain; #endif } /** * @brief 上电从 VM 灌回剩余寿命(只改 RAM,不写 flash) * @param remain VM 中的剩余寿命秒 * @return 无 */ void app_light_life_restore(uint32_t remain) { #if BOARD_LIGHT_LIFE_ENABLE local_irq_disable(); s_life.remain = remain; s_life.since_sync = 0; s_life.since_save = 0; s_life.ms = 0; s_life.need_sync = 0; s_life.need_edge = 0; local_irq_enable(); gDevData.lighe_remain_time = remain; gDevData.alarm_code = (remain == 0) ? 1 : 0; log_info("life restore remain=%lu alarm=%d\n", (unsigned long)remain, gDevData.alarm_code); #else gDevData.lighe_remain_time = remain; #endif } /** * @brief 寿命回到额定值(仅出厂复位 DP26 调用;APP 换灯请写 DP3) * @return 无 */ void app_light_life_reset(void) { app_light_life_set_remain(BOARD_LIGHT_RATED_SEC); log_info("light reset to rated %lu\n", (unsigned long)BOARD_LIGHT_RATED_SEC); } /** * @brief 查询剩余寿命 * @return 剩余寿命秒 */ uint32_t app_light_life_get_remain(void) { #if !BOARD_LIGHT_LIFE_ENABLE return gDevData.lighe_remain_time; /* 关闭时按上报值原样返回 */ #else return s_life.remain; #endif }