9ceb218f80
Co-authored-by: Cursor <cursoragent@cursor.com>
765 lines
33 KiB
C
765 lines
33 KiB
C
/**
|
||
* @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 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 */
|