/** * @file BS_ocpp_mv_offline_bridge.c * @brief OCPP 离线 MeterValues 桥接实现:落盘(未连接/未登录)+ 未结订单联网后顺序补发 * * @details **产品需求(明确)**\n * 1. **离线周期存储**:TCP 未连接或已连接但未登录 CSMS 时,充电过程中按配置周期将当前测量快照写入 SPI Flash 池,\n * 并在 FRAM 建立「本笔充电」与序号区间的映射。\n * 2. **离线订单上送**:未结订单在联网并完成 **Authorize → StartTransaction** 取得平台 **transactionId(流水号)** 后,\n * 在发送 `StopTransaction` 之前,须检查 Flash 中是否存有 **本次充电** 的离线 MeterValues。\n * 3. **如何认定「本次充电」的 MV**:落盘与补发使用 **同一规则** —— 以订单日志中的 **开始充电时间** `StartChargeTime`(`Comm_Time`)\n * 经 `xDate2Seconds` 得到 **32 位**开始充电秒 `u32_charge_start_sec`,并与 **枪号** 一起在 FRAM 映射中查找(落盘 API 第二参为兼容保留,恒传 0)。\n * 未结内存中的 `S_LOG_DATA` 与离线采样时 `v_meterlog_put_char_log_data` 取到的订单为同一会话时,**开始时间一致**则键一致,\n * `u8_ocpp_mv_offline_map_get` 命中的区间即为本笔充电的 MV,按序号逐条补发后再 `StopTransaction`。\n * * @details **实现划分**\n * 1. **键空间**:见上;离线时设备流水号常为空,故不以设备单号做键。\n * 2. **负载内容**:`S_OCPP_MV_OFFLINE_MV` 与 `BS_ocpp_ctrl.c` 中 `u8_get_ocpp_measurand_data` 口径对齐,补发由 `s32_ocpp_send_meter_values_replay` 组 JSON。\n * 3. **模块上下文**:文件内单一静态结构体 `s_ocpp_mv_bridge_ctx` 统一管理「每枪落盘节流时间戳、未结补发状态机」。\n * `v_ocpp_mv_offline_init` 在 **`v_ocpp_init_data`**(`BS_ocpp_ctrl.c`)中调用以装载 FRAM 管理块;`v_ocpp_mv_offline_bridge_private_logic` 不触发 Flash 层 init。\n * 释放未结订单内存前须 `v_ocpp_mv_offline_unsettled_abort()`,避免状态机与已释放 `S_LOG_DATA` 不一致。\n * 4. **两类时间源**:`get_current_seconds()` 为 **上电起单调秒**(`public_func.c`),只宜做间隔/节流;**OCPP 报文与 Flash 内 `u32_timestamp_unix`** 须用 **`GetCurrentTime` + `xDate2Seconds`**,与在线 `MeterValues_mag` 及 `StartChargeTime` 映射键同一套日历 epoch。 */ #include "BS_ocpp_mv_offline_bridge.h" #if BS_OCPP_EN #include #include #include #include "BS_ocpp_ctrl.h" #include "publicdata/public_define.h" #include "publicdata/publicdata.h" #include "plat_comm/impl/bs_public_impl.h" #include "flash_file_mgr/ocpp_mv_offline_flash_impl.h" #include "plat_comm/plat_comm_task.h" #include "BS_ocpp_configuration.h" #include "app_rtc/app_rtc.h" #include "bs_ocpp_json_data.h" #define OCPP_OFFLINE_LOG(fmt, ...) \ MYLOG_MSG(OCPP_PRINT_FLAG,fmt, ##__VA_ARGS__) //任务打印 extern int32_t s32_ocpp_send_meter_values_replay(int gunNo, int transactionId, const S_OCPP_MV_OFFLINE_MV *mv); /* 与 BS_ocpp_ctrl.c 中 `v_ocpp_timestamp_to_str` 口径一致(离线 MV 时间戳由 unix 秒经本文件内 helper 转换) */ static void v_mv_offline_timestamp_to_str(Comm_Time *ctTime, char *time_str) { Comm_Time time; if (ctTime == NULL) { GetCurrentTime(&time); ctTime = &time; } #if (OCPP_TIMESTAMP_UTC_OUTPUT) else { memcpy(&time, ctTime, sizeof(Comm_Time)); ctTime = &time; } v_adjust_time_with_timezone((Comm_Time *)ctTime, -8, 0); (void)snprintf(time_str, (size_t)OCPP_DATE_LEN, "%04u-%02u-%02uT%02u:%02u:%02uZ", (unsigned)ctTime->iYear, (unsigned)ctTime->ucMonth, (unsigned)ctTime->ucDay, (unsigned)ctTime->ucHour, (unsigned)ctTime->ucMin, (unsigned)ctTime->ucSec); #else (void)snprintf(time_str, (size_t)OCPP_DATE_LEN, "%04u-%02u-%02uT%02u:%02u:%02u+08:00", (unsigned)ctTime->iYear, (unsigned)ctTime->ucMonth, (unsigned)ctTime->ucDay, (unsigned)ctTime->ucHour, (unsigned)ctTime->ucMin, (unsigned)ctTime->ucSec); #endif } static void v_mv_offline_timestamp_unix_to_str(U32_T unix_sec, char *time_str) { Comm_Time ct; xSeconds2Date((unsigned long)unix_sec, &ct); v_mv_offline_timestamp_to_str(&ct, time_str); } /** 将离线 32B 快照填到单个 SampledValue(与 `u8_get_ocpp_measurand_data` 口径一致) */ static U8_T u8_offline_mv_to_sampled(SampledValue *sv, OCPP_MEASURAND_ID e_type, const S_OCPP_MV_OFFLINE_MV *mv) { char data_str[24] = {0}; if (sv == NULL || mv == NULL) { return 0u; } switch (e_type) { case ENUM_EnergyActiveImportRegister: (void)snprintf(data_str, sizeof(data_str), "%lu", (unsigned long)mv->u32_meter_import_wh); memcpy(sv->value, data_str, strlen(data_str)); memcpy(sv->context, OCPP_STR_SamplePeriodic, strlen(OCPP_STR_SamplePeriodic)); memcpy(sv->format, OCPP_STR_Raw, strlen(OCPP_STR_Raw)); memcpy(sv->measurand, OCPP_STR_EnergyActiveImportRegister, strlen(OCPP_STR_EnergyActiveImportRegister)); memcpy(sv->location, OCPP_STR_Outlet, strlen(OCPP_STR_Outlet)); memcpy(sv->unit, OCPP_STR_Wh, strlen(OCPP_STR_Wh)); return 1u; case ENUM_EnergyActiveImportInterval: (void)snprintf(data_str, sizeof(data_str), "%lu", (unsigned long)mv->u32_energy_interval_wh); memcpy(sv->value, data_str, strlen(data_str)); memcpy(sv->context, OCPP_STR_SamplePeriodic, strlen(OCPP_STR_SamplePeriodic)); memcpy(sv->format, OCPP_STR_Raw, strlen(OCPP_STR_Raw)); memcpy(sv->measurand, OCPP_STR_EnergyActiveImportInterval, strlen(OCPP_STR_EnergyActiveImportInterval)); memcpy(sv->location, OCPP_STR_Outlet, strlen(OCPP_STR_Outlet)); memcpy(sv->unit, OCPP_STR_Wh, strlen(OCPP_STR_Wh)); return 1u; case ENUM_PowerActiveImport: (void)snprintf(data_str, sizeof(data_str), "%.3f", (F32_T)mv->u16_power_active_w); memcpy(sv->value, data_str, strlen(data_str)); memcpy(sv->context, OCPP_STR_SamplePeriodic, strlen(OCPP_STR_SamplePeriodic)); memcpy(sv->format, OCPP_STR_Raw, strlen(OCPP_STR_Raw)); memcpy(sv->measurand, OCPP_STR_PowerActiveImport, strlen(OCPP_STR_PowerActiveImport)); memcpy(sv->location, OCPP_STR_Outlet, strlen(OCPP_STR_Outlet)); memcpy(sv->unit, OCPP_STR_W, strlen(OCPP_STR_W)); return 1u; case ENUM_PowerOffered: (void)snprintf(data_str, sizeof(data_str), "%.3f", (F32_T)mv->u16_power_offered_01kw * 100.0f); memcpy(sv->value, data_str, strlen(data_str)); memcpy(sv->context, OCPP_STR_SamplePeriodic, strlen(OCPP_STR_SamplePeriodic)); memcpy(sv->format, OCPP_STR_Raw, strlen(OCPP_STR_Raw)); memcpy(sv->measurand, OCPP_STR_PowerOffered, strlen(OCPP_STR_PowerOffered)); memcpy(sv->location, OCPP_STR_Outlet, strlen(OCPP_STR_Outlet)); memcpy(sv->unit, OCPP_STR_W, strlen(OCPP_STR_W)); return 1u; case ENUM_CurrentImport: (void)snprintf(data_str, sizeof(data_str), "%.3f", (F32_T)mv->u16_current_import_a_x100 / 100.0f); memcpy(sv->value, data_str, strlen(data_str)); memcpy(sv->context, OCPP_STR_SamplePeriodic, strlen(OCPP_STR_SamplePeriodic)); memcpy(sv->format, OCPP_STR_Raw, strlen(OCPP_STR_Raw)); memcpy(sv->measurand, OCPP_STR_CurrentImport, strlen(OCPP_STR_CurrentImport)); memcpy(sv->location, OCPP_STR_Outlet, strlen(OCPP_STR_Outlet)); memcpy(sv->unit, OCPP_STR_A, strlen(OCPP_STR_A)); return 1u; case ENUM_CurrentOffered: (void)snprintf(data_str, sizeof(data_str), "%.3f", (F32_T)mv->u16_current_offered_a_x100 / 100.0f); memcpy(sv->value, data_str, strlen(data_str)); memcpy(sv->context, OCPP_STR_SamplePeriodic, strlen(OCPP_STR_SamplePeriodic)); memcpy(sv->format, OCPP_STR_Raw, strlen(OCPP_STR_Raw)); memcpy(sv->measurand, OCPP_STR_CurrentOffered, strlen(OCPP_STR_CurrentOffered)); memcpy(sv->location, OCPP_STR_Outlet, strlen(OCPP_STR_Outlet)); memcpy(sv->unit, OCPP_STR_A, strlen(OCPP_STR_A)); return 1u; case ENUM_Voltage: (void)snprintf(data_str, sizeof(data_str), "%.3f", (F32_T)mv->u16_voltage_v_x10 / 10.0f); memcpy(sv->value, data_str, strlen(data_str)); memcpy(sv->context, OCPP_STR_SamplePeriodic, strlen(OCPP_STR_SamplePeriodic)); memcpy(sv->format, OCPP_STR_Raw, strlen(OCPP_STR_Raw)); memcpy(sv->measurand, OCPP_STR_Voltage, strlen(OCPP_STR_Voltage)); memcpy(sv->location, OCPP_STR_Outlet, strlen(OCPP_STR_Outlet)); memcpy(sv->unit, OCPP_STR_V, strlen(OCPP_STR_V)); return 1u; case ENUM_SoC: (void)snprintf(data_str, sizeof(data_str), "%u", (unsigned)mv->u8_soc); memcpy(sv->value, data_str, strlen(data_str)); memcpy(sv->format, OCPP_STR_Raw, strlen(OCPP_STR_Raw)); memcpy(sv->measurand, OCPP_STR_SOC, strlen(OCPP_STR_SOC)); memcpy(sv->location, OCPP_STR_EV, strlen(OCPP_STR_EV)); memcpy(sv->unit, OCPP_STR_Percent, strlen(OCPP_STR_Percent)); if (mv->u8_soc_data_type == 1u) { memcpy(sv->context, OCPP_STR_TransactionBegin, strlen(OCPP_STR_TransactionBegin)); } else if (mv->u8_soc_data_type == 2u) { memcpy(sv->context, OCPP_STR_TransactionEnd, strlen(OCPP_STR_TransactionEnd)); } else { memcpy(sv->context, OCPP_STR_SamplePeriodic, strlen(OCPP_STR_SamplePeriodic)); } return 1u; default: return 0u; } } cJSON *MeterValues_mag_offline_replay(int gunNo, int transactionId, const S_OCPP_MV_OFFLINE_MV *mv) { MeterValuesRequest json_data; char time_str[30] = {0}; U8_T i; U8_T u8_data_num = 0; if (mv == NULL) { return NULL; } (void)memset(&json_data, 0, sizeof(json_data)); json_data.connectorId = gunNo + 1; json_data.transactionId = transactionId; v_mv_offline_timestamp_unix_to_str(mv->u32_timestamp_unix, time_str); (void)memcpy(json_data.meterValue[0].timestamp, time_str, strlen(time_str)); for (i = 0; i < (U8_T)ENUM_measurand_Num; i++) { if ((s_ocpp_flash_cfg_data.s_ocpp_config_data.u8_MeterValuesMeasurandNeed[i] & E_CFG_MVMN_MeterValuesSampledData) == E_CFG_MVMN_MeterValuesSampledData) { if (u8_offline_mv_to_sampled(&json_data.meterValue[0].sampledValue[u8_data_num], (OCPP_MEASURAND_ID)i, mv) != 0u) { if (++u8_data_num >= MeterValuesRequest_SV_NUM) { break; } } } } if (u8_data_num == 0u) { return NULL; } return j_get_ocpp_json(OCPP_MeterValues, &json_data); } /** * @brief 离线 MV 桥接模块级 RAM 上下文(本文件内静态单例) * @details 集中存放:\n * - `last_mv_save_sec[]`:每枪上次**成功**落盘的单调秒,用于 `MeterValueSampleInterval` 节流;\n * - `unsettled`:未结订单补发 MV 时的状态机(与 `s_unsettled_order_data` 生命周期独立,释放订单前须 abort)。 */ /** 离线 MV 顺序补发状态(未结补单 / 在线会话内补发共用) */ typedef struct { U8_T active; /**< 非 0:正处于某笔单的补发区间 `[seq_next, seq_end]` 内 */ U8_T gun; /**< 当前补发枪号 */ U32_T u32_charge_start_sec; /**< `xDate2Seconds(StartChargeTime)`,与 FRAM `u32_tx_sn_lo` 一致 */ int i32_txid; /**< 本笔 OCPP transactionId(会话补发在读订单失败时沿用) */ U32_T seq_start; U32_T seq_next; U32_T seq_end; } S_OCPP_MV_REPLAY_CTX; typedef struct { U32_T last_mv_save_sec[GUN_MAX_CNT]; /**< 每枪上次成功落盘时的 `get_current_seconds()`(单调秒),仅用于采样周期节流 */ S_OCPP_MV_REPLAY_CTX unsettled; /**< 未结订单 StopTx 前补发 */ S_OCPP_MV_REPLAY_CTX session; /**< 在线起充、中途离线再上线:用已有 transactionId 补发 */ } S_OCPP_MV_OFFLINE_BRIDGE_CTX; /** 模块唯一静态上下文;静态存储期整体零初始化 */ static S_OCPP_MV_OFFLINE_BRIDGE_CTX s_ocpp_mv_bridge_ctx; /** 每枪当前是否已为本笔充电完成起充准备(flow 离开充电后清零) */ static U8_T s_u8_mv_charge_session_armed[GUN_MAX_CNT]; /** 上一周期 BMS flow,用于停充边沿补写一条 MV */ static U8_T s_u8_mv_prev_bs_flow[GUN_MAX_CNT]; /** map full 日志节流(单调秒) */ static U32_T s_u32_mv_map_full_log_sec[GUN_MAX_CNT]; /** * @fn v_ocpp_mv_charge_start_sec_from_time * @brief 用订单「开始充电时间」得到与 FRAM 映射一致的 **单路** 32 位键(`xDate2Seconds`) * @param[in] ct 指向 `S_LOG_DATA.StartChargeTime` 的日历时间(与未结订单 `log` 中字段同源) * @param[out] out_sec 成功时为 `xDate2Seconds(ct)`;无效日历或未写入时为 0 * @details 双枪在同一秒启动时 `*out_sec` 可能相同,依赖 `S_OCPP_MV_OFFLINE_MAP_ROW.u8_gun_no` 区分;\n * 调用 `u8_ocpp_mv_offline_mv_save` / `map_get` 时第二参固定传 0(与 FRAM 行 `u32_tx_sn_hi` 约定一致,非 64 位拆分)。\n * 对 `ct` 做简单范围检查(年月日非全零、月 1–12、日 1–31),避免未刷新的日志参与映射。 */ static void v_ocpp_mv_charge_start_sec_from_time(const Comm_Time *ct, U32_T *out_sec) { unsigned int sec; if (out_sec != NULL) { *out_sec = 0u; } if (ct == NULL || out_sec == NULL) { return; } if (ct->iYear == 0u && ct->ucMonth == 0u && ct->ucDay == 0u) { return; } if (ct->ucMonth == 0u || ct->ucMonth > 12u) { return; } if (ct->ucDay == 0u || ct->ucDay > 31u) { return; } sec = xDate2Seconds((Comm_Time *)ct); *out_sec = (U32_T)sec; } /** * @fn u8_ocpp_mv_fill_snapshot * @brief 从实时系统数据填充一条待写入 Flash 的 `S_OCPP_MV_OFFLINE_MV` 快照 * @param[in] gun 枪索引 `0..GUN_MAX_CNT-1`,与 `u8_ocpp_mv_offline_mv_save` 的 `u8_gun_no` 一致 * @param[out] out 输出快照;成功时魔数、版本、时间戳及测量字段均写入 * @return 1 成功;0 参数非法或枪号越界 * @details 各字段数据源与 `u8_get_ocpp_measurand_data` 对齐说明:\n * - `u32_timestamp_unix`:`GetCurrentTime` + `xDate2Seconds`,与在线 `MeterValues_mag` 的日历时间同源,供补发时 `v_mv_offline_timestamp_unix_to_str` 组 ISO8601(**勿用** `get_current_seconds()`,其为上电起算单调秒)。\n * - `u32_meter_import_wh`:电表累计 Wh(`E_BS_GET_SYS_DATA_MEMTER_ELECT`/10 截断 U32)\n * - `u32_energy_interval_wh`:本事务段增量 Wh(当前累计 − `E_BS_GET_SYS_DATA_LOG_ENERGY` type=1)\n * - `u16_voltage_v_x10` / 电流:`METER_VOL`、`METER_CUR`(电流存 0.01A:原 0.1A×10)\n * - `u16_power_active_w`:由本快照已写入的 `u16_voltage_v_x10`(0.1V)与 `u16_current_import_a_x100`(0.01A)整数估算:`W = vol×cur / 1000`,饱和 65535\n * - `u16_power_offered_01kw` / `u16_current_offered_a_x100`:桩额定功率 0.1kW 单位、桩最大输出电流\n * - SOC:`E_BS_GET_SYS_DATA_SOC_DATA` 当前值,`u8_soc_data_type` 固定 0(周期采样) */ static U8_T u8_ocpp_mv_fill_snapshot(U8_T gun, S_OCPP_MV_OFFLINE_MV *out) { U64_T u64_wh; /**< 电表累计能量 Wh(64 位中间量,避免乘除溢出) */ U32_T start_wh; /**< 事务起始电表累计 Wh,来自日志能量 type=1 */ U64_T delta; /**< 事务段内增量 Wh = 当前累计 − 起始 */ Comm_Time now_ct; /**< 当前 RTC 日历(东八区),与 `BS_ocpp_ctrl.c` 中 `v_ocpp_timestamp_to_str(NULL,…)` 一致 */ if (out == NULL || gun >= GUN_MAX_CNT) { return 0u; } (void)memset(out, 0, sizeof(*out)); //清空快照 out->u32_magic = OCPP_MV_OFFLINE_FLASH_MAGIC; //魔数 out->u16_version = (U16_T)OCPP_MV_OFFLINE_FLASH_VER; //版本 GetCurrentTime(&now_ct); out->u32_timestamp_unix = (U32_T)xDate2Seconds(&now_ct); /* 日历 epoch 秒,非 get_current_seconds 上电秒 */ u64_wh = u64_bs_get_sys_data(gun, E_BS_GET_SYS_DATA_MEMTER_ELECT) / 10u; //电表累计能量 Wh out->u32_meter_import_wh = (U32_T)((u64_wh > 0xFFFFFFFFULL) ? 0xFFFFFFFFu : u64_wh); //电表累计能量 Wh 截断 start_wh = u32_bs_get_sys_data(gun, E_BS_GET_SYS_DATA_LOG_ENERGY, 1u); //事务起始电表累计 Wh delta = (out->u32_meter_import_wh > start_wh) ? ((U64_T)out->u32_meter_import_wh - (U64_T)start_wh) : 0u; //事务段内增量 Wh out->u32_energy_interval_wh = (U32_T)((delta > 0xFFFFFFFFULL) ? 0xFFFFFFFFu : delta); //事务段内增量 Wh 截断 out->u16_voltage_v_x10 = u32_bs_get_sys_data(gun, E_BS_GET_SYS_DATA_METER_VOL, 0u); //输出电压 0.1V out->u16_current_import_a_x100 = (U16_T)(u32_bs_get_sys_data(gun, E_BS_GET_SYS_DATA_METER_CUR, 0u) * 10u); //输出电流 0.01A { U32_T p_w; /**< `W = (0.1V×值)×(0.01A×值) = u16_voltage_v_x10 * u16_current_import_a_x100 / 1000` */ p_w = (U32_T)out->u16_voltage_v_x10 * (U32_T)out->u16_current_import_a_x100 / 1000u; if (p_w > 65535u) { p_w = 65535u; } out->u16_power_active_w = (U16_T)p_w; } out->u16_power_offered_01kw = (U16_T)u32_bs_get_sys_data(gun, E_BS_GET_SYS_DATA_RATED_POWER, 0u); //额定功率 0.1kW out->u16_current_offered_a_x100 = (U16_T)u32_bs_get_sys_data(gun, E_BS_GET_SYS_DATA_PILE_PUT_PARA, 1u); //最大输出电流 0.01A out->u8_soc = (U8_T)u32_bs_get_sys_data(gun, E_BS_GET_SYS_DATA_SOC_DATA, 0u); //SOC% out->u8_soc_data_type = 0u; //SOC 上下文:0 当前 / 1 Transaction.Begin / 2 Transaction.End out->u8_connector_id = (U8_T)(gun + 1u); //连接器号 out->u8_evse_id = gun; //EVSE/枪逻辑号 return 1u; } /** * @fn v_ocpp_mv_offline_bridge_private_logic * @brief 参见头文件声明;按枪轮询落盘离线 MV */ static U8_T u8_mv_pre_trade_no_pending(const U8_T *p) { if (p == NULL || p[0] == '\0' || p[0] == (U8_T)0xFF) { return 1u; } return 0u; } /** * @brief 将当前枪订单快照写入 Flash(不检查 flow,供停充边沿补一条) * @return 1 成功;0 跳过或失败 */ static U8_T u8_ocpp_mv_offline_persist_snapshot(U8_T gun, U32_T *seq_out) { S_LOG_DATA s_log; U32_T charge_start_sec; S_OCPP_MV_OFFLINE_MV mv; U32_T seq_local; U8_T sav_ret; if (gun >= (U8_T)GUN_MAX_CNT) { return 0u; } if (seq_out == NULL) { seq_out = &seq_local; } (void)memset(&s_log, 0, sizeof(s_log)); v_meterlog_put_char_log_data(gun, &s_log); v_ocpp_mv_charge_start_sec_from_time(&s_log.StartChargeTime, &charge_start_sec); if (charge_start_sec == 0u) { return 0u; } if (u8_ocpp_mv_fill_snapshot(gun, &mv) == 0u) { return 0u; } sav_ret = u8_ocpp_mv_offline_mv_save(gun, charge_start_sec, 0u, &mv, seq_out); if (sav_ret == 0u) { s_ocpp_mv_bridge_ctx.last_mv_save_sec[gun] = (U32_T)get_current_seconds(); return 1u; } return 0u; } void v_ocpp_mv_offline_on_charge_start(U8_T gun) { if (gun >= (U8_T)GUN_MAX_CNT) { return; } if (s_ocpp_mv_bridge_ctx.session.active != 0u && s_ocpp_mv_bridge_ctx.session.gun == gun) { (void)memset(&s_ocpp_mv_bridge_ctx.session, 0, sizeof(s_ocpp_mv_bridge_ctx.session)); } /* 勿清 FRAM 映射:同枪连续多笔离线充(A→B→A)须各自保留 StartChargeTime 键,上线后逐笔补 MV */ s_ocpp_mv_bridge_ctx.last_mv_save_sec[gun] = 0u; s_u32_mv_map_full_log_sec[gun] = 0u; s_u8_mv_charge_session_armed[gun] = 1u; OCPP_OFFLINE_LOG("MV offline: charge start gun=%u (keep pending maps)\r\n", (unsigned)gun); } void v_ocpp_mv_offline_bridge_private_logic(U8_T bs_id) { U8_T i; /**< 枪循环变量 `0..GUN_MAX_CNT-1` */ U16_T interval_s; /**< 采样周期(秒),来自 OCPP 配置 `MeterValueSampleInterval` */ U32_T now; /**< `get_current_seconds()`:上电起单调秒,仅用于与 `MeterValueSampleInterval` 比较节流 */ (void)bs_id; interval_s = (U16_T)60; /* 配置为 0 或未加载时的保守默认,避免除零或过频写 Flash */ if (s_ocpp_flash_cfg_data.s_ocpp_config_data.u16_MeterValueSampleInterval != 0u) { interval_s = s_ocpp_flash_cfg_data.s_ocpp_config_data.u16_MeterValueSampleInterval; } now = (U32_T)get_current_seconds(); for (i = 0u; i < (U8_T)GUN_MAX_CNT; i++) { S_LOG_DATA s_log; /**< 当前枪订单日志快照,含 `StartChargeTime` */ U32_T charge_start_sec; /**< `xDate2Seconds(StartChargeTime)`,与 FRAM `u32_tx_sn_lo` 一致 */ S_OCPP_MV_OFFLINE_MV mv; /**< 单条待写入 Flash 的 32B 负载 + 帧头由底层封装 */ U32_T seq_out; /**< 本笔分配到的该枪单调序号(调试用,此处未回传上层) */ U8_T sav_ret; /**< `u8_ocpp_mv_offline_mv_save` 返回值:0 成功,2 映射满,3 Flash,4 FRAM,5 序号不连续等 */ { U8_T bs_flow = u8_bs_get_flow_state(i); /* 停充边沿:补写结束时刻 MV,避免最后一采样周期未落盘 */ if (s_u8_mv_prev_bs_flow[i] == 2u && bs_flow != 2u) { U32_T seq_end = 0u; if (u8_ocpp_mv_offline_persist_snapshot(i, &seq_end) != 0u) { OCPP_OFFLINE_LOG("MV offline: stop snap gun=%u seq=%lu\r\n", (unsigned)i, (unsigned long)seq_end); } } s_u8_mv_prev_bs_flow[i] = bs_flow; if (bs_flow != 2u) { //枪未充电 跳过 s_u8_mv_charge_session_armed[i] = 0u; continue; } } if (s_u8_mv_charge_session_armed[i] == 0u) { U32_T resume_key = 0u; U32_T resume_seq = 0u; U16_T resume_cnt = 0u; (void)memset(&s_log, 0, sizeof(s_log)); v_meterlog_put_char_log_data(i, &s_log); v_ocpp_mv_charge_start_sec_from_time(&s_log.StartChargeTime, &resume_key); if (resume_key != 0u && u8_ocpp_mv_offline_map_get(i, resume_key, 0u, &resume_seq, &resume_cnt) == 0u && resume_cnt > 0u) { /* 链路抖动/TCP 重连:同一笔充电勿清 map,避免补发从 seq 头反复上送 */ s_u8_mv_charge_session_armed[i] = 1u; OCPP_OFFLINE_LOG("MV offline: gun=%u link resume, keep map (%u条) key=0x%08lX\r\n", (unsigned)i, (unsigned)resume_cnt, (unsigned long)resume_key); } else { v_ocpp_mv_offline_on_charge_start(i); } } if (interval_s != 0u && s_ocpp_mv_bridge_ctx.last_mv_save_sec[i] != 0u && (now - s_ocpp_mv_bridge_ctx.last_mv_save_sec[i]) < (U32_T)interval_s) { //时间间隔未到 跳过 continue; } (void)memset(&s_log, 0, sizeof(s_log)); v_meterlog_put_char_log_data(i, &s_log); //获取订单日志 v_ocpp_mv_charge_start_sec_from_time(&s_log.StartChargeTime, &charge_start_sec); if (charge_start_sec == 0u) { /* 开始充电时间无效或未写入,无法建立稳定键 */ continue; } if (u8_ocpp_mv_fill_snapshot(i, &mv) == 0u) { //填充快照失败 跳过 continue; } sav_ret = u8_ocpp_mv_offline_mv_save(i, charge_start_sec, 0u, &mv, &seq_out); if (sav_ret == 0u) { s_ocpp_mv_bridge_ctx.last_mv_save_sec[i] = now; OCPP_OFFLINE_LOG("MV offline: write ok gun=%u seq=%lu key_sec=0x%08lX unix_ts=%lu Wh=%lu Ival=%lu\r\n", (unsigned)i, (unsigned long)seq_out, (unsigned long)charge_start_sec, (unsigned long)mv.u32_timestamp_unix, (unsigned long)mv.u32_meter_import_wh, (unsigned long)mv.u32_energy_interval_wh); } else if (sav_ret == 2u) { U32_T now_log = (U32_T)get_current_seconds(); if (s_u32_mv_map_full_log_sec[i] == 0u || (now_log - s_u32_mv_map_full_log_sec[i]) >= 30u) { s_u32_mv_map_full_log_sec[i] = now_log; OCPP_OFFLINE_LOG("MV offline: map full gun=%u (FRAM rows=%u)\r\n", (unsigned)i, (unsigned)OCPP_MV_OFFLINE_MAP_ROWS); } } else if (sav_ret != 5u) { OCPP_OFFLINE_LOG("MV offline: save err=%u gun=%u\r\n", (unsigned)sav_ret, (unsigned)i); } } } void v_ocpp_mv_offline_snap_gun_now(U8_T gun) { U32_T seq_out = 0u; if (gun >= (U8_T)GUN_MAX_CNT) { return; } if (u8_bs_get_flow_state(gun) != 2u) { return; } if (s_u8_mv_charge_session_armed[gun] == 0u) { v_ocpp_mv_offline_on_charge_start(gun); } if (u8_ocpp_mv_offline_persist_snapshot(gun, &seq_out) != 0u) { OCPP_OFFLINE_LOG("MV offline: ws-fail snap gun=%u seq=%lu\r\n", (unsigned)gun, (unsigned long)seq_out); } } /** * @fn v_ocpp_mv_offline_unsettled_abort * @brief 参见头文件;将 `s_ocpp_mv_bridge_ctx.unsettled` 整块清零 */ void v_ocpp_mv_offline_unsettled_abort(void) { (void)memset(&s_ocpp_mv_bridge_ctx.unsettled, 0, sizeof(s_ocpp_mv_bridge_ctx.unsettled)); } U8_T u8_ocpp_mv_offline_map_pending_for_log(const S_LOG_DATA *log) { U32_T charge_start_sec; U32_T seq_start; U16_T count; U8_T gun; if (log == NULL) { return 0u; } gun = log->u8_gunNo; if (gun >= (U8_T)GUN_MAX_CNT) { gun = 0u; } v_ocpp_mv_charge_start_sec_from_time(&log->StartChargeTime, &charge_start_sec); if (charge_start_sec == 0u) { return 0u; } if (u8_ocpp_mv_offline_map_get(gun, charge_start_sec, 0u, &seq_start, &count) != 0u || count == 0u) { return 0u; } return 1u; } void v_ocpp_mv_offline_session_replay_abort(void) { (void)memset(&s_ocpp_mv_bridge_ctx.session, 0, sizeof(s_ocpp_mv_bridge_ctx.session)); } /** * @brief 单步推进指定补发上下文,最多发送一条 MeterValues * @return 1 本周期已发或需重试;0 无映射/已全部完成 */ static U8_T u8_ocpp_mv_replay_step_ctx(S_OCPP_MV_REPLAY_CTX *rp, U8_T gun, U32_T charge_start_sec, int txid, const char *tag) { U32_T seq_start; U16_T count; S_OCPP_MV_OFFLINE_MV mv; if (rp == NULL || charge_start_sec == 0u || txid < 0) { return 0u; } if (rp->active == 0u) { if (u8_ocpp_mv_offline_map_get(gun, charge_start_sec, 0u, &seq_start, &count) != 0u || count == 0u) { return 0u; } rp->active = 1u; rp->gun = gun; rp->u32_charge_start_sec = charge_start_sec; rp->i32_txid = txid; rp->seq_start = seq_start; rp->seq_next = seq_start; rp->seq_end = seq_start + (U32_T)count - 1u; OCPP_OFFLINE_LOG("MV offline: %s queue gun=%u txId=%d key_sec=0x%08lX seq %lu-%lu (%u条)\r\n", tag, (unsigned)gun, txid, (unsigned long)charge_start_sec, (unsigned long)seq_start, (unsigned long)rp->seq_end, (unsigned)count); } else if (rp->gun != gun || rp->u32_charge_start_sec != charge_start_sec) { (void)memset(rp, 0, sizeof(*rp)); return 0u; } if (rp->seq_next > rp->seq_end) { (void)u8_ocpp_mv_offline_map_remove(rp->gun, rp->u32_charge_start_sec, 0u); (void)memset(rp, 0, sizeof(*rp)); return 0u; } if (u8_ocpp_mv_offline_mv_read_by_seq(rp->gun, rp->seq_next, &mv) != 0u) { OCPP_OFFLINE_LOG("MV offline: %s read fail gun=%u seq=%lu slot=%lu\r\n", tag, (unsigned)rp->gun, (unsigned long)rp->seq_next, (unsigned long)(rp->seq_next % (U32_T)OCPP_MV_OFFLINE_POOL_MAX_PER_GUN)); rp->seq_next++; return 1u; } if (u8_ocpp_mv_offline_mv_is_valid(&mv) == 0u) { OCPP_OFFLINE_LOG("MV offline: %s read skip invalid gun=%u seq=%lu\r\n", tag, (unsigned)rp->gun, (unsigned long)rp->seq_next); rp->seq_next++; return 1u; } if (s32_ocpp_send_meter_values_replay((int)rp->gun, txid, &mv) <= 0) { OCPP_OFFLINE_LOG("MV offline: %s send fail gun=%u seq=%lu\r\n", tag, (unsigned)rp->gun, (unsigned long)rp->seq_next); return 1u; } { U32_T total = rp->seq_end - rp->seq_start + 1u; U32_T done; rp->seq_next++; done = rp->seq_next - rp->seq_start; OCPP_OFFLINE_LOG("MV offline: %s send ok gun=%u %lu/%lu seq=%lu ts=%lu Wh=%lu\r\n", tag, (unsigned)rp->gun, (unsigned long)done, (unsigned long)total, (unsigned long)(rp->seq_next - 1u), (unsigned long)mv.u32_timestamp_unix, (unsigned long)mv.u32_meter_import_wh); if (rp->seq_next > rp->seq_end) { OCPP_OFFLINE_LOG("MV offline: %s replay done gun=%u total=%lu\r\n", tag, (unsigned)rp->gun, (unsigned long)total); (void)u8_ocpp_mv_offline_map_remove(rp->gun, rp->u32_charge_start_sec, 0u); (void)memset(rp, 0, sizeof(*rp)); return 0u; } } return 1u; } U8_T u8_ocpp_mv_offline_session_replay_pending(U8_T gun) { S_LOG_DATA log; U32_T charge_start_sec; U32_T seq_start; U16_T count; if (gun >= (U8_T)GUN_MAX_CNT) { return 0u; } if (s_ocpp_mv_bridge_ctx.session.active != 0u) { return (gun == s_ocpp_mv_bridge_ctx.session.gun) ? 1u : 0u; } if (s_ocpp_mv_bridge_ctx.unsettled.active != 0u) { return 0u; } (void)memset(&log, 0, sizeof(log)); if (u8_plat_get_real_order(&log, gun) == 0u) { return 0u; } if (u8_mv_pre_trade_no_pending(log.u8_preTradeNo) != 0u) { return 0u; } v_ocpp_mv_charge_start_sec_from_time(&log.StartChargeTime, &charge_start_sec); if (charge_start_sec == 0u) { return 0u; } if (u8_ocpp_mv_offline_map_get(gun, charge_start_sec, 0u, &seq_start, &count) != 0u || count == 0u) { return 0u; } return 1u; } U8_T u8_ocpp_mv_offline_session_replay_step(U8_T gun) { S_LOG_DATA log; U32_T charge_start_sec; U8_T flow; int txid; if (gun >= (U8_T)GUN_MAX_CNT) { return 0u; } if (s_ocpp_mv_bridge_ctx.unsettled.active != 0u) { return 0u; } /* 已在补发:仅在本枪推进;其它枪轮询项直接跳过,避免 gun0 空订单 pending 占满发送队列(session_0013) */ if (s_ocpp_mv_bridge_ctx.session.active != 0u) { if (gun != s_ocpp_mv_bridge_ctx.session.gun) { return 0u; } txid = s_ocpp_mv_bridge_ctx.session.i32_txid; (void)memset(&log, 0, sizeof(log)); if (u8_plat_get_real_order(&log, gun) != 0u) { int live_tx = atoi((const char *)log.u8_preTradeNo); if (live_tx > 0) { s_ocpp_mv_bridge_ctx.session.i32_txid = live_tx; txid = live_tx; } } return u8_ocpp_mv_replay_step_ctx(&s_ocpp_mv_bridge_ctx.session, s_ocpp_mv_bridge_ctx.session.gun, s_ocpp_mv_bridge_ctx.session.u32_charge_start_sec, txid, "session"); } (void)memset(&log, 0, sizeof(log)); if (u8_plat_get_real_order(&log, gun) == 0u) { return 0u; } if (u8_mv_pre_trade_no_pending(log.u8_preTradeNo) != 0u) { return 0u; } v_ocpp_mv_charge_start_sec_from_time(&log.StartChargeTime, &charge_start_sec); if (charge_start_sec == 0u) { return 0u; } txid = atoi((const char *)log.u8_preTradeNo); flow = u8_bs_get_flow_state(gun); if (flow != 2u && flow != 9u && flow != 10u) { return 0u; } return u8_ocpp_mv_replay_step_ctx(&s_ocpp_mv_bridge_ctx.session, gun, charge_start_sec, txid, "session"); } /** * @fn u8_ocpp_mv_offline_unsettled_step * @brief 参见头文件;单步推进补发状态机并最多发送一条 WS MeterValues */ U8_T u8_ocpp_mv_offline_unsettled_step(S_LOG_DATA *log) { U32_T charge_start_sec; U8_T gun; int txid; if (log == NULL) { return 0u; } if (u8_ocpp_unsettled_is_live_charging_session(log) != 0u) { return 0u; } gun = log->u8_gunNo; if (gun >= GUN_MAX_CNT) { gun = 0u; } v_ocpp_mv_charge_start_sec_from_time(&log->StartChargeTime, &charge_start_sec); if (charge_start_sec == 0u) { return 0u; } if (u8_mv_pre_trade_no_pending(log->u8_preTradeNo) != 0u) { return 0u; } txid = atoi((const char *)log->u8_preTradeNo); if (txid < 0) { return 0u; } if (u8_ocpp_mv_offline_map_pending_for_log(log) == 0u) { OCPP_OFFLINE_LOG("MV offline: unsettled no map gun=%u key_sec=0x%08lX txId=%d\r\n", (unsigned)gun, (unsigned long)charge_start_sec, txid); return 0u; } if (s_ocpp_mv_bridge_ctx.session.active != 0u && s_ocpp_mv_bridge_ctx.session.gun == gun) { /* 同枪在线会话 MV 补发中:勿 abort session 走未结 StopTx(session_0012) */ if (u8_ocpp_unsettled_is_live_charging_session(log) != 0u) { return 0u; } v_ocpp_mv_offline_session_replay_abort(); } return u8_ocpp_mv_replay_step_ctx(&s_ocpp_mv_bridge_ctx.unsettled, gun, charge_start_sec, txid, "unsettled"); } #endif /* BS_OCPP_EN */