Files
CCU621M/app/plat_comm/ocpp/BS_ocpp_mv_offline_bridge.c

765 lines
33 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.
/**
* @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 <string.h>
#include <stdio.h>
#include <stdlib.h>
#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 FRAM5 序号不连续等 */
{
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 走未结 StopTxsession_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 */