充电桩OCPP1.6J协议适配


前言

OCPP(开放充电点协议)是充电桩与中央管理系统之间实现标准化通信的“通用语言”,确保不同品牌设备能互联互通、远程管理并满足国际准入要求‌。因此充电桩适配OCPP协议是走向海外市场的基本条件。本文介绍了一种快速适配OCPP协议的方法,就是通过移植开源MicroOcpp库,能减少繁琐工作,快速完成协议功能适配。


一、OCPP协议是什么

        OCPP是一种用于电动汽车充电桩与CSMS之间数据交互的通信协议,支持多种充电设备与管理系统之间的互操作性,是一种上层应用层协议,传输层仍是TCP/IP协议。

        OCPP协议可以被视为全球标准。它是由荷兰“充电设备操作系统开发者联盟”(OCA)于2009年开发,逐渐发展成为全球范围内用于电动车充电设备(EVSE)商业应用通信的业界通用标准,是一个全球开放性的通讯协议,用于解决私营充电网络间通讯的各种困难,支持充电站点与各供应商中央管理系统间的无缝通讯管理,已在全球多个国家推广使用。

        最新正式版为OCPP2.0.1,且不向下兼容。只有OCPP1.x是向低版本兼容, OCPP1.6兼容OCPP1.5, OCPP1.5兼容OCPP1.2。OCPP2.0.1不兼容OCPP1.6,OCPP2.0.1中虽然有些内容在OCPP1.6也有, 但是发送的数据帧格式已经完全不同。相较于OCPP1.6等更早的版本,OCPP2.0. 1在以下几个方面有较大的改进:

        提高安全性:OCPP2.0.1通过引入基于安全套接层的HTTPS连接以及新的证书管理方案进行安全加固,以确保通信的安全性。

        增加新功能:OCPP2.0.1增加了许多新功能,包括智能充电管理,以及更详尽的故障报告和分析等。

        设计更灵活:OCPP2.0.1的设计更加灵活,能够满足更加复杂和多样化的应用需求。

        代码简化:OCPP2.0.1简化了代码,使得软件实现更加容易。

目前市场上还是以OCPP1.6J协议应用为主。

二、MicroOcpp移植的前置条件

1.OCPP1.6J协议文档

OCPP1.6J是基于TCP/IP传输协议,使用 WebSocket 建立长连接,支持双向实时通信,协议格式采用JSON编码。

协议文档官方下载链接为:https://www.openchargealliance.org/protocols/ocpp-16/

参考奥升新能源的协议解读博客也是一样的,为以下链接:

https://blog.csdn.net/flainsky/article/details/156346918?ops_request_misc=elastic_search_misc&request_id=e11c51aaf982f1f27f79ee7acad7c800&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~ElasticSearch~search_v2-1-156346918-null-null.142^v102^control&utm_term=%E5%A5%A5%E5%8D%87%E5%85%85%E7%94%B5%E5%B9%B3%E5%8F%B0OCPP%E5%8D%8F%E8%AE%AE%E8%A7%A3%E6%9E%90&spm=1018.2226.3001.4187

我这里搜集到的英文原版,和中文翻译版也已打包上传,需要的自取:

https://download.csdn.net/download/x150061/92777359

2.MicroOcpp是什么

‌        MicroOcpp是一个专为微控制器(MCU)和嵌入式系统优化的轻量级OCPP 1.6/2.0.1客户端开源库,用C/C++编写,可在资源受限的设备上实现充电桩与中央管理系统的标准化通信‌。它正被广泛应用于ESP32、STM32、Arduino等主流嵌入式平台,助力开发者低成本、高效率地构建符合国际标准的智能充电站 。

        MicroOcpp完全开源,托管于GitHub等公共代码平台,允许开发者自由使用、修改和部署,广泛应用于个人开发者、初创企业及中小厂商的智能充电桩研发中 。gitee上的下载链接为:

https://gitcode.com/gh_mirrors/mi/MicroOcpp/?utm_source=artical_gitcode&index=top&type=card&&uuid_tt_dd=10_28833067900-1750224379163-831369&isLogin=1&from_id=144211606&from_link=d4a228b6fc877a98617bef11b0f2b993

从MicroOcpp的README可以看到,需要移植前置ArduinoJSONarduinoWebSockets。我这里的开发平台是STM32F407系列芯片,网络用的SIM通信,串口转4G通信模组,采用EC800E通信模组,使用AT指令控制。因此移植MicroOcpp,除了业务层接口适配,还需要修改arduinoWebSockets中的网络通信接口。

三、MicroOcpp的移植

1.MicroOcpp库框架介绍

MicroOcpp系统组件架构图展示各个模块的交互关系,项目采用分层架构设计,主要包含以下核心模块:

核心层(Core): 提供基础通信框架和配置管理 

模型层(Model): 实现充电业务逻辑和状态管理
操作层(Operations:  处理具体的OCPP协议操作

交易管理模块
位于 src/MicroOcpp/Model/Transactions/ 的交易处理系统负责管理充电会话的完整生命周期,包括开始、停止和状态跟踪。

计量数据模块
src/MicroOcpp/Model/Metering/ 目录下的计量功能实时收集和上报充电过程中的能量消耗数据。

授权与安全模块
授权服务位于 src/MicroOcpp/Model/Authorization/,确保只有合法用户能够启动充电过程。
内存管理策略
利用 src/MicroOcpp/Core/Memory.h 中提供的内存管理工具,优化资源使用。

2.OCPP协议中充电流程介绍

从充电流程中可以看出,充电桩请求充电发StartTransaction帧,运营平台回复conf,并带有transactionId,OCPP协议中没有下发计费费率的部分,充电过程中充电桩隔固定周期上传电表示数,由运营平台去计费。充电桩侧停止充电时会发StopTransaction帧。

3.MicroOcpp业务接口适配

所有需要对接的API接口都在MicroOcpp.h头文件中,可根据需要适配对应的接口。

// matth-x/MicroOcpp
// Copyright Matthias Akstaller 2019 - 2024
// MIT License

#ifndef MO_MICROOCPP_H
#define MO_MICROOCPP_H

#include <ArduinoJson.h>
#include <memory>
#include <functional>

#include <MicroOcpp/Core/ConfigurationOptions.h>
#include <MicroOcpp/Core/FilesystemAdapter.h>
#include <MicroOcpp/Core/RequestCallbacks.h>
#include <MicroOcpp/Core/Connection.h>
#include <MicroOcpp/Core/Memory.h>
#include <MicroOcpp/Model/Metering/SampledValue.h>
#include <MicroOcpp/Model/Transactions/Transaction.h>
#include <MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h>
#include <MicroOcpp/Model/ConnectorBase/ChargePointStatus.h>
#include <MicroOcpp/Model/ConnectorBase/UnlockConnectorResult.h>
#include <MicroOcpp/Version.h>
#include <MicroOcpp/Model/Certificates/Certificate.h>

using MicroOcpp::OnReceiveConfListener;
using MicroOcpp::OnReceiveReqListener;
using MicroOcpp::OnSendConfListener;
using MicroOcpp::OnAbortListener;
using MicroOcpp::OnTimeoutListener;
using MicroOcpp::OnReceiveErrorListener;


//use links2004/WebSockets library

/*
 * Initialize the library with the OCPP URL, EVSE voltage and filesystem configuration.
 * 
 * If the connections fails, please refer to 
 * https://github.com/matth-x/MicroOcpp/issues/36#issuecomment-989716573 for recommendations on
 * how to track down the issue with the connection.
 * 
 * This is a convenience function only available for Arduino.
 */
void mocpp_initialize(
            const char *backendUrl,    //e.g. "wss://example.com:8443/steve/websocket/CentralSystemService"
            const char *chargeBoxId,   //e.g. "charger001"
            const char *chargePointModel = "Demo Charger",     //model name of this charger
            const char *chargePointVendor = "My Company Ltd.", //brand name
            MicroOcpp::FilesystemOpt fsOpt = MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail, //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h
            const char *password = nullptr, //password present in the websocket message header
            const char *CA_cert = nullptr, //TLS certificate
            bool autoRecover = false); //automatically sanitize the local data store when the lib detects recurring crashes. Not recommended during development


/*
 * Convenience initialization: use this for passing the BootNotification payload JSON to the mocpp_initialize(...) below
 * 方便初始化:使用此函数将BootNotification有效容器JSON传递给下面的mocpp_initialize(…)
 * Example usage:
 * 
 *     mocpp_initialize(osock, ChargerCredentials("Demo Charger", "My Company Ltd."));
 * 
 * For a description of the fields, refer to OCPP 1.6 Specification - Edition 2 p. 60
 */
struct ChargerCredentials {
    ChargerCredentials(
            const char *chargePointModel = "Demo Charger",          //充电桩的型号(必需)
            const char *chargePointVendor = "My Company Ltd.",      //供应商名(必需)
            const char *firmwareVersion = nullptr,                  //固件版本号
            const char *chargePointSerialNumber = nullptr,          //充电桩的序列号
            const char *meterSerialNumber = nullptr,                //主电表的序列号
            const char *meterType = nullptr,                        //主电表的类型
            const char *chargeBoxSerialNumber = nullptr,            //充电箱的序列号
            const char *iccid = nullptr,                            //SIM卡的ICCID
            const char *imsi = nullptr);                            //SIM卡的IMSI
    
    /*
     * OCPP 2.0.1 compatible charger credentials. Use this if initializing the library with ProtocolVersion(2,0,1)
     */
    static ChargerCredentials v201(
            const char *chargePointModel = "Demo Charger",
            const char *chargePointVendor = "My Company Ltd.",
            const char *firmwareVersion = nullptr,
            const char *chargePointSerialNumber = nullptr,
            const char *meterSerialNumber = nullptr,
            const char *meterType = nullptr,
            const char *chargeBoxSerialNumber = nullptr,
            const char *iccid = nullptr,
            const char *imsi = nullptr);

    operator const char *() {return payload;}

private:
    char payload [512] = {'{', '}', '\0'};
};

/*
 * Initialize the library with a WebSocket connection which is configured with protocol=ocpp1.6
 * (=Connection), EVSE voltage and filesystem configuration. This library requires that you handle
 * establishing the connection and keeping it alive. Please refer to
 * https://github.com/matth-x/MicroOcpp/tree/main/examples/ESP-TLS for an example how to use it.
 * This GitHub project also delivers an Connection implementation based on links2004/WebSockets. If
 * you need another WebSockets implementation, you can subclass the Connection class and pass it to
 * this initialize() function. Please refer to
 * https://github.com/OpenEVSE/ESP32_WiFi_V4.x/blob/master/src/MongooseConnectionClient.cpp for
 * an example.
 */
/*
 * 使用WebSocket连接初始化库,该连接配置了协议=ocpp1.6(=连接)、EVSE电压和文件系统配置。此库要求您建立连接并保持其活动状态。
 * 请参考https://github.com/matth-x/MicroOcpp/tree/main/examples/ESP-TLS。
 * 这个GitHub项目还提供了一个基于links2004/Webockets的Connection实现,如果你需要自实现WebSockets,
 * 你可以子类化Connection类并将其传递给这个initialize()函数,
 * 请参考https://github.com/OpenEVSE/ESP32_WiFi_V4.x/blob/master/src/MongooseConnectionClient.cpp。
 */
void mocpp_initialize(
            MicroOcpp::Connection& connection, //WebSocket adapter for MicroOcpp
            const char *bootNotificationCredentials = ChargerCredentials("Demo Charger", "My Company Ltd."), //e.g. '{"chargePointModel":"Demo Charger","chargePointVendor":"My Company Ltd."}' (refer to OCPP 1.6 Specification - Edition 2 p. 60)
            std::shared_ptr<MicroOcpp::FilesystemAdapter> filesystem =
                MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail), //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h
            bool autoRecover = false, //automatically sanitize the local data store when the lib detects recurring crashes. Not recommended during development
            MicroOcpp::ProtocolVersion version = MicroOcpp::ProtocolVersion(1,6));

/*
 * Stop the OCPP library and release allocated resources.
 * 停止OCPP库,并释放分配的资源。
 */
void mocpp_deinitialize();

/*
 * To be called in the main loop (e.g. place it inside loop())
 * 在主循环中调用(例如将其放入loop())
 */
void mocpp_loop();

/*
 * 交易管理 
 * 
 * OCPP 1.6 (2.0.1 see below):
 * 开始交易流程并做好准备。当以下所有条件都为真时,向OCPP服务器发送StartTransaction请求:
 * 1) 连接正常(未报告故障,后端未设置“不可用”)
 * 2) 没有预留阻止连接器
 * 3) idTag被授权充电。交易过程将向服务器发送授权消息以供批准,除非充电器离线,否则将按照规范中的本地授权规则应用。
 * 4) 车辆已经插入或即将插入(仅适用于设置了连接器插入输入的情况)
 * 请参阅beginTransaction_authorized以跳过步骤1)至3)
 * Begin the transaction process and prepare it. When all conditions for the transaction are true,
 * eventually send a StartTransaction request to the OCPP server.
 * Conditions:
 *     1) the connector is operative (no faults reported, not set "Unavailable" by the backend)
 *     2) no reservation blocks the connector
 *     3) the idTag is authorized for charging. The transaction process will send an Authorize message
 *        to the server for approval, except if the charger is offline, then the Local Authorization
 *        rules will apply as in the specification.
 *     4) the vehicle is already plugged or will be plugged soon (only applicable if the
 *        ConnectorPlugged Input is set)
 * 
 * See beginTransaction_authorized for skipping steps 1) to 3)
 * 
 * Returns true if it was possible to create the transaction process. Returns
 * false if either another transaction process is still active or you need to try it again later.
 * 
 * OCPP 2.0.1:
 * Authorize a transaction. Like the OCPP 1.6 behavior, this should be called when the user swipes the
 * card to start charging, but the semantic is slightly different. This function begins the authorized
 * phase, but a transaction may already have started due to an earlier transaction start point.
 */
bool beginTransaction(const char *idTag, unsigned int connectorId = 1);

/*
 * Begin the transaction process and skip the OCPP-side authorization. See beginTransaction(...) for a
 * complete description
 */
bool beginTransaction_authorized(const char *idTag, const char *parentIdTag = nullptr, unsigned int connectorId = 1);

/*
 * OCPP 1.6 (2.0.1 see below):
 * End the transaction process if idTag is authorized to stop the transaction. The OCPP lib sends
 * a StopTransaction request if the following conditions are true:
 * Conditions:
 *     1) Currently, a transaction is running which hasn't been terminated yet AND
 *     2) idTag is either
 *         - nullptr OR
 *         - matches the idTag of beginTransaction (or RemoteStartTransaction) OR
 *         - [Planned, not released yet] is part of the current LocalList and the parentIdTag
 *           matches with the parentIdTag of beginTransaction.
 *         - [Planned, not released yet] If none of step 2) applies, then the OCPP lib will check
 *           the authorization status via an Authorize request
 * 
 * See endTransaction_authorized for skipping the authorization check, i.e. step 2)
 * 
 * If the transaction is ended by swiping an RFID card, then idTag should contain its identifier. If
 * charging stops for a different reason than swiping the card, idTag should be null or empty.
 * 
 * Please refer to OCPP 1.6 Specification - Edition 2 p. 90 for a list of valid reasons. `reason`
 * can also be nullptr.
 * 
 * It is safe to call this function at any time, i.e. when no transaction runs or when the transaction
 * has already been ended. For example you can place
 *     `endTransaction(nullptr, "Reboot");`
 * in the beginning of the program just to ensure that there is no transaction from a previous run.
 * 
 * If called with idTag=nullptr, this is functionally equivalent to
 *     `endTransaction_authorized(nullptr, reason);`
 * 
 * Returns true if there is a transaction which could eventually be ended by this action
 * 
 * OCPP 2.0.1:
 * End the user authorization. Like when running with OCPP 1.6, this should be called when the user
 * swipes the card to stop charging. The difference between the 1.6/2.0.1 behavior is that in 1.6,
 * endTransaction always sets the transaction inactive so that it wants to stop. In 2.0.1, this only
 * revokes the user authorization which may terminate the transaction but doesn't have to if the
 * transaction stop point is set to EvConnected.
 * 
 * Note: the stop reason parameter is ignored when running with OCPP 2.0.1. It's always Local
 */
bool endTransaction(const char *idTag = nullptr, const char *reason = nullptr, unsigned int connectorId = 1);

/*
 * End the transaction process definitely without authorization check. See endTransaction(...) for a
 * complete description.
 * 
 * Use this function if you manage authorization on your own and want to bypass the Authorization
 * management of this lib.
 */
bool endTransaction_authorized(const char *idTag, const char *reason = nullptr, unsigned int connectorId = 1);

/*
 * Get information about the current Transaction lifecycle. A transaction can enter the following
 * states:
 *     - Idle: no transaction running or being started
 *     - Preparing: before a potential transaction
 *     - Aborted: transaction not started and never will be started
 *     - Running: transaction started and running
 *     - Running/StopTxAwait: transaction still running but will end at the next possible time
 *     - Finished: transaction stopped
 * 
 * isTransactionActive() and isTransactionRunning() give the status by combining them:
 * 
 *     State               | isTransactionActive() | isTransactionRunning()
 *     --------------------+-----------------------+-----------------------
 *     Preparing           | true                  | false
 *     Running             | true                  | true
 *     Running/StopTxAwait | false                 | true
 *     Finished / Aborted  |                       |
 *                  / Idle | false                 | false
 */
bool isTransactionActive(unsigned int connectorId = 1);
bool isTransactionRunning(unsigned int connectorId = 1);

/*
 * Get the idTag which has been used to start the transaction. If no transaction process is
 * running, this function returns nullptr
 */
const char *getTransactionIdTag(unsigned int connectorId = 1);

/*
 * Returns the current transaction process. Returns nullptr if no transaction is running, preparing or finishing
 *
 * See the class definition in MicroOcpp/Model/Transactions/Transaction.h for possible uses of this object
 * 
 * Examples:
 * auto tx = getTransaction(); //fetch tx object
 * if (tx) { //check if tx object exists
 *     bool active = tx->isActive(); //active tells if the transaction is preparing or continuing to run
 *                                   //inactive means that the transaction is about to stop, stopped or won't be started anymore
 *     int transactionId = tx->getTransactionId(); //the transactionId as assigned by the OCPP server
 *     bool deauthorized = tx->isIdTagDeauthorized(); //if StartTransaction has been rejected
 * }
 */
std::shared_ptr<MicroOcpp::Transaction>& getTransaction(unsigned int connectorId = 1);

#if MO_ENABLE_V201
/*
 * OCPP 2.0.1 version of getTransaction(). Note that the return transaction object is of another type
 * and unlike the 1.6 version, this function does not give ownership.
 */
MicroOcpp::Ocpp201::Transaction *getTransactionV201(unsigned int evseId = 1);
#endif //MO_ENABLE_V201

/* 
 * Returns if the OCPP library allows the EVSE to charge at the moment.
 *
 * If you integrate it into a J1772 charger, true means that the Control Pilot can send the PWM signal
 * and false means that the Control Pilot must be at a DC voltage.
 */
bool ocppPermitsCharge(unsigned int connectorId = 1);

/*
 * Returns the latest ChargePointStatus as reported via StatusNotification (standard OCPP data type)
 */
ChargePointStatus getChargePointStatus(unsigned int connectorId = 1);

/*
 * Define the Inputs and Outputs of this library.
 * 
 * This library interacts with the hardware of your charger by Inputs and Outputs. Inputs and Outputs
 * are tiny function-objects which read information from the EVSE or control the behavior of the EVSE.
 * 
 * An Input is a function which returns the current state of a variable of the EVSE. For example, if
 * the energy meter stores the energy register in the global variable `e_reg`, then you can allow
 * this library to read it by defining the Input 
 *     `[] () {return e_reg;}`
 * and passing it to the library.
 * 
 * An Output is a function which gets a state value from the OCPP library and applies it to the EVSE.
 * For example, to let Smart Charging control the PWM signal of the Control Pilot, define the Output
 *     `[] (float p_max) {pwm = p_max / PWM_FACTOR;}` (simplified example)
 * and pass it to the library.
 * 
 * Configure the library with Inputs and Outputs once in the setup() function.
 */

void setConnectorPluggedInput(std::function<bool()> pluggedInput, unsigned int connectorId = 1); //Input about if an EV is plugged to this EVSE

void setEnergyMeterInput(std::function<int()> energyInput, unsigned int connectorId = 1); //Input of the electricity meter register in Wh

void setPowerMeterInput(std::function<float()> powerInput, unsigned int connectorId = 1); //Input of the power meter reading in W

//Smart Charging Output, alternative for Watts only, Current only, or Watts x Current x numberPhases.
//Only one of the Smart Charging Outputs can be set at a time.
//MO will execute the callback whenever the OCPP charging limit changes and will pass the limit for now
//to the callback. If OCPP does not define a limit, then MO passes the value -1 for "undefined".
void setSmartChargingPowerOutput(std::function<void(float)> chargingLimitOutput, unsigned int connectorId = 1); //Output (in Watts) for the Smart Charging limit
void setSmartChargingCurrentOutput(std::function<void(float)> chargingLimitOutput, unsigned int connectorId = 1); //Output (in Amps) for the Smart Charging limit
void setSmartChargingOutput(std::function<void(float,float,int)> chargingLimitOutput, unsigned int connectorId = 1); //Output (in Watts, Amps, numberPhases) for the Smart Charging limit

/*
 * Define the Inputs and Outputs of this library. (Advanced)
 * 
 * These Inputs and Outputs are optional depending on the use case of your charger.
 */

void setEvReadyInput(std::function<bool()> evReadyInput, unsigned int connectorId = 1); //Input if EV is ready to charge (= J1772 State C)

void setEvseReadyInput(std::function<bool()> evseReadyInput, unsigned int connectorId = 1); //Input if EVSE allows charge (= PWM signal on)

void addErrorCodeInput(std::function<const char*()> errorCodeInput, unsigned int connectorId = 1); //Input for Error codes (please refer to OCPP 1.6, Edit2, p. 71 and 72 for valid error codes)
void addErrorDataInput(std::function<MicroOcpp::ErrorData()> errorDataInput, unsigned int connectorId = 1);

void addMeterValueInput(std::function<float ()> valueInput, const char *measurand = nullptr, const char *unit = nullptr, const char *location = nullptr, const char *phase = nullptr, unsigned int connectorId = 1); //integrate further metering Inputs

void addMeterValueInput(std::unique_ptr<MicroOcpp::SampledValueSampler> valueInput, unsigned int connectorId = 1); //integrate further metering Inputs (more extensive alternative)

void setOccupiedInput(std::function<bool()> occupied, unsigned int connectorId = 1); //Input if instead of Available, send StatusNotification Preparing / Finishing

void setStartTxReadyInput(std::function<bool()> startTxReady, unsigned int connectorId = 1); //Input if the charger is ready for StartTransaction

void setStopTxReadyInput(std::function<bool()> stopTxReady, unsigned int connectorId = 1); //Input if charger is ready for StopTransaction

void setTxNotificationOutput(std::function<void(MicroOcpp::Transaction*,TxNotification)> notificationOutput, unsigned int connectorId = 1); //called when transaction state changes (see TxNotification for possible events). Transaction can be null

#if MO_ENABLE_V201
void setTxNotificationOutputV201(std::function<void(MicroOcpp::Ocpp201::Transaction*,TxNotification)> notificationOutput, unsigned int connectorId = 1);
#endif //MO_ENABLE_V201

#if MO_ENABLE_CONNECTOR_LOCK
/*
 * Set an InputOutput (reads and sets information at the same time) for forcing to unlock the
 * connector. Called as part of the OCPP operation "UnlockConnector"
 * Return values:
 *     - UnlockConnectorResult_Pending if action needs more time to complete (MO will call this cb again later or eventually time out)
 *     - UnlockConnectorResult_Unlocked if successful
 *     - UnlockConnectorResult_UnlockFailed if not successful (e.g. lock stuck)
 */
void setOnUnlockConnectorInOut(std::function<UnlockConnectorResult()> onUnlockConnectorInOut, unsigned int connectorId = 1);
#endif //MO_ENABLE_CONNECTOR_LOCK

/*
 * Access further information about the internal state of the library
 */

bool isOperative(unsigned int connectorId = 1); //if the charge point is operative (see OCPP1.6 Edit2, p. 45) and ready for transactions

/*
 * Configure the device management
 */

void setOnResetNotify(std::function<bool(bool)> onResetNotify); //call onResetNotify(isHard) before Reset. If you return false, Reset will be aborted. Optional

void setOnResetExecute(std::function<void(bool)> onResetExecute); //reset handler. This function should reboot this controller immediately. Already defined for the ESP32 on Arduino


namespace MicroOcpp {
class FirmwareService;
class DiagnosticsService;
}

/*
 * You need to configure this object if FW updates are relevant for you. This project already
 * brings a simple configuration for the ESP32 and ESP8266 for prototyping purposes, however
 * for the productive system you will have to develop a configuration targeting the specific
 * OCPP backend.
 * See MicroOcpp/Model/FirmwareManagement/FirmwareService.h
 * 
 * Lazy initialization: The FW Service will be created at the first call to this function
 * 
 * To use, add `#include <MicroOcpp/Model/FirmwareManagement/FirmwareService.h>`
 */
MicroOcpp::FirmwareService *getFirmwareService();

/*
 * This library implements the OCPP messaging side of Diagnostics, but no logging or the
 * log upload to your backend.
 * To integrate Diagnostics, see MicroOcpp/Model/Diagnostics/DiagnosticsService.h
 * 
 * Lazy initialization: The Diag Service will be created at the first call to this function
 * 
 * To use, add `#include <MicroOcpp/Model/Diagnostics/DiagnosticsService.h>`
 */
MicroOcpp::DiagnosticsService *getDiagnosticsService();

#if MO_ENABLE_CERT_MGMT
/*
 * Set a custom Certificate Store which implements certificate updates on the host system. 
 * MicroOcpp will forward OCPP-side update requests to the certificate store, as well as
 * query the certificate store upon server request.
 *
 * To enable OCPP-side certificate updates (UCs M03 - M05), set the build flag
 * MO_ENABLE_CERT_MGMT=1 so that this function becomes accessible.
 * 
 * To use the built-in certificate store (depends on MbedTLS), set the build flag
 * MO_ENABLE_MBEDTLS=1. To not use the built-in implementation, but still enable MbedTLS,
 * additionally set MO_ENABLE_CERT_STORE_MBEDTLS=0.
 */
void setCertificateStore(std::unique_ptr<MicroOcpp::CertificateStore> certStore);
#endif //MO_ENABLE_CERT_MGMT

/*
 * Add features and customize the behavior of the OCPP client
 */

namespace MicroOcpp {
class Context;
}

//Get access to internal functions and data structures. The returned Context object allows
//you to bypass the facade functions of this header and implement custom functionality.
//To use, add `#include <MicroOcpp/Core/Context.h>`
MicroOcpp::Context *getOcppContext();

/*
 * Set a listener which is notified when the OCPP lib processes an incoming operation of type
 * operationType. After the operation has been interpreted, onReceiveReq will be called with
 * the original message from the OCPP server.
 * 
 * Example usage:
 * 
 * setOnReceiveRequest("SetChargingProfile", [] (JsonObject payload) {
 *     Serial.print("[main] received charging profile for connector: "; //Arduino print function
 *     Serial.printf("update connector %i with chargingProfileId %i\n",
 *             payload["connectorId"],                                  //ArduinoJson object access
 *             payload["csChargingProfiles"]["chargingProfileId"]);
 * });
 */
void setOnReceiveRequest(const char *operationType, OnReceiveReqListener onReceiveReq);

/*
 * Set a listener which is notified when the OCPP lib sends the confirmation to an incoming
 * operation of type operation type. onSendConf will be passed the original output of the
 * OCPP lib.
 * 
 * Example usage:
 * 
 * setOnSendConf("RemoteStopTransaction", [] (JsonObject payload) -> void {
 *     if (!strcmp(payload["status"], "Rejected")) {
 *         //the OCPP lib rejected the RemoteStopTransaction command. In this example, the customer
 *         //wishes to stop the running transaction in any case and to log this case
 *         endTransaction(nullptr, "Remote"); //end transaction and send StopTransaction
 *         Serial.println("[main] override rejected RemoteStopTransaction"); //Arduino print function
 *     }
 * });
 * 
 */
void setOnSendConf(const char *operationType, OnSendConfListener onSendConf);

/*
 * Create and send an operation without using the built-in Operation class. This function bypasses
 * the business logic which comes with this library. E.g. you can send unknown operations, extend
 * OCPP or replace parts of the business logic with custom behavior.
 * 
 * Use case 1, extend the library by sending additional operations. E.g. DataTransfer:  
 * 用例1,通过发送其他操作来扩展库。例如,数据传输:
 * sendRequest("DataTransfer", [] () -> std::unique_ptr<MicroOcpp::JsonDoc> {
 *     //will be called to create the request once this operation is being sent out
 *     size_t capacity = JSON_OBJECT_SIZE(3) +
 *                       JSON_OBJECT_SIZE(2); //for calculating the required capacity, see https://arduinojson.org/v6/assistant/
 *     auto res = std::unique_ptr<MicroOcpp::JsonDoc>(new MicroOcpp::JsonDoc(capacity)); 
 *     JsonObject request = *res;
 *     request["vendorId"] = "My company Ltd.";
 *     request["messageId"] = "TargetValues";
 *     request["data"]["battery_capacity"] = 89;
 *     request["data"]["battery_soc"] = 34;
 *     return res;
 * }, [] (JsonObject response) -> void {
 *     //will be called with the confirmation response of the server
 *     if (!strcmp(response["status"], "Accepted")) {
 *         //DataTransfer has been accepted
 *         int max_energy = response["data"]["max_energy"];
 *     }
 * });
 * 
 * Use case 2, bypass the business logic of this library for custom behavior. E.g. StartTransaction:
 * 用例2,绕过此库的业务逻辑以实现自定义行为。例如,启动交易:
 * sendRequest("StartTransaction", [] () -> std::unique_ptr<MicroOcpp::JsonDoc> {
 *     //will be called to create the request once this operation is being sent out
 *     size_t capacity = JSON_OBJECT_SIZE(4); //for calculating the required capacity, see https://arduinojson.org/v6/assistant/
 *     auto res = std::unique_ptr<MicroOcpp::JsonDoc>(new MicroOcpp::JsonDoc(capacity)); 
 *     JsonObject request = res->to<JsonObject>();
 *     request["connectorId"] = 1;
 *     request["idTag"] = "A9C3CE1D7B71EA";
 *     request["meterStart"] = 1234;
 *     request["timestamp"] = "2023-06-01T11:07:43Z"; //e.g. some historic transaction
 *     return res;
 * }, [] (JsonObject response) -> void {
 *     //will be called with the confirmation response of the server
 *     const char *status = response["idTagInfo"]["status"];
 *     int transactionId = response["transactionId"];
 * });
 * 
 * In Use case 2, the library won't send any further StatusNotification or StopTransaction on
 * its own.
 */
void sendRequest(const char *operationType,
            std::function<std::unique_ptr<MicroOcpp::JsonDoc> ()> fn_createReq,
            std::function<void (JsonObject)> fn_processConf);

/*
 * Set a custom handler for an incoming operation type. This will update the core Operation registry
 * of the library, potentially replacing the built-in Operation handler and bypassing the
 * business logic of this library.
 * 
 * Note that when replacing an operation handler, the attached listeners will be reset.
 * 
 * Example usage:
 * 
 * setRequestHandler("DataTransfer", [] (JsonObject request) -> void {
 *     //will be called with the request message from the server
 *     const char *vendorId = request["vendorId"];
 *     const char *messageId = request["messageId"];
 *     int battery_capacity = request["data"]["battery_capacity"];
 *     int battery_soc = request["data"]["battery_soc"];
 * }, [] () -> std::unique_ptr<MicroOcpp::JsonDoc> {
 *     //will be called  to create the response once this operation is being sent out
 *     size_t capacity = JSON_OBJECT_SIZE(2) +
 *                       JSON_OBJECT_SIZE(1); //for calculating the required capacity, see https://arduinojson.org/v6/assistant/
 *     auto res = std::unique_ptr<MicroOcpp::JsonDoc>(new MicroOcpp::JsonDoc(capacity)); 
 *     JsonObject response = res->to<JsonObject>();
 *     response["status"] = "Accepted";
 *     response["data"]["max_energy"] = 59;
 *     return res;
 * });
 */
void setRequestHandler(const char *operationType,
            std::function<void (JsonObject)> fn_processReq,
            std::function<std::unique_ptr<MicroOcpp::JsonDoc> ()> fn_createConf);

/*
 * Send OCPP operations manually not bypassing the internal business logic
 * 
 * On receipt of the .conf() response the library calls the callback function
 * "OnReceiveConfListener onConf" and passes the OCPP payload to it.
 * 
 * For your first EVSE integration, the `onReceiveConfListener` is probably sufficient. For
 * advanced EVSE projects, the other listeners likely become relevant:
 * - `onAbortListener`: will be called whenever the engine stops trying to finish an operation
 *           normally which was initiated by this device.
 * - `onTimeoutListener`: will be executed when the operation is not answered until the timeout
 *           expires. Note that timeouts also trigger the `onAbortListener`.
 * - `onReceiveErrorListener`: will be called when the Central System returns a CallError.
 *           Again, each error also triggers the `onAbortListener`.
 * 
 * The functions for sending OCPP operations are non-blocking. The program will resume immediately
 * with the code after with the subsequent code in any case.
 */

void authorize(
            const char *idTag,                           //RFID tag (e.g. ISO 14443 UID tag with 4 or 7 bytes)
            OnReceiveConfListener onConf = nullptr,      //callback (confirmation received)
            OnAbortListener onAbort = nullptr,           //callback (confirmation not received), optional
            OnTimeoutListener onTimeout = nullptr,       //callback (timeout expired), optional
            OnReceiveErrorListener onError = nullptr,    //callback (error code received), optional
            unsigned int timeout = 0); //custom timeout behavior, optional

bool startTransaction(
            const char *idTag,
            OnReceiveConfListener onConf = nullptr,
            OnAbortListener onAbort = nullptr,
            OnTimeoutListener onTimeout = nullptr,
            OnReceiveErrorListener onError = nullptr,
            unsigned int timeout = 0);

bool stopTransaction(
            OnReceiveConfListener onConf = nullptr,
            OnAbortListener onAbort = nullptr,
            OnTimeoutListener onTimeout = nullptr,
            OnReceiveErrorListener onError = nullptr,
            unsigned int timeout = 0);

uint8_t get_wsIsConnected();

#endif

此处我适配的接口在user_plat_ocpp.cpp文件中,具体为:

/********************************************************************************
 * Copyright (c) 
 *
 * 文件名称:user_plat_ocpp.cpp
 *
 * 描    述:登录支持OCPP 1.6J协议的运营平台
 *
 * 修改记录:
 *******************************************************************************/
#include "user_plat.h"
#include "user_SerialLCD.h"


#if(OPERAT_PLAT_TYPE == OPERAT_PLAT_TYPE_OCPP)

#include <MicroOcpp.h>
typedef enum
{
    OCPP_PLAT_STEP_INIT = 0,        //等待初始化
	OCPP_PLAT_STEP_OFFLINE,		    //未连接上
	OCPP_PLAT_STEP_ONLINE,			//在线待机状态
}OCPP_PLAT_STEP_ENUM;

typedef enum
{
    OCPP_ALL  = 0,      //表示整机   
    OCPP_GUNA = 1,      //表示A枪
    OCPP_GUNB,          
}OCPP_GUN_NUM_ENUM;

#define CHECK_TIME_CNT      (150)    //15s左右连不上平台报警
OCPP_PLAT_STEP_ENUM step_plat = OCPP_PLAT_STEP_INIT;    //登录平台状态

//
//  Settings which worked for my SteVe instance:
//
#define OCPP_BACKEND_URL   "ws://111.231.69.220:8080/steve/websocket/CentralSystemService"
#define OCPP_CHARGE_BOX_ID "TestChargeT"
/********************************************************************
* 功能:获取一个有效错误码
* 输入:无
* 输出:
* 说明:
* 修改记录:
********************************************************************/
void get_One_Err(ERR_GUN_INDEX_ENUM gunIndex, char* outStr, uint8_t size)
{
   uint8_t index_base = 0, index = 0;
   uint16_t alarmCnt = 0;
   uint32_t alarmCode = 0;
   uint16_t rst = 0;
   if(outStr == RT_NULL || size == 0){
        return;
   }
   for (uint8_t i = 0; i < ERR_GROUP_COUNT; i++)
   {
        index_base = i * 32;
        alarmCode = user_GetErrValueSafe(i);
        if (alarmCode == 0) // 此组无报警快速跳过
            continue;
        for (uint8_t j = 0; j < 32; j++)
        {
            index = index_base + j;
            if ((alarmCode & ErrTable[index].ErrCode) && (ErrTable[index].ErrLevel) && (ErrInfoTable[index].ErrGunA_B == 3 || ErrInfoTable[index].ErrGunA_B == gunIndex))
            { // 报警码有效
                alarmCnt++;
                rst = err_localToPlat(index);
                if(rst > 0){
                    switch(rst){
                        case OCPP_ERR_COMM_BMS:
                            rt_snprintf(outStr, size, "EVCommunicationError");
                            break;
                        case OCPP_ERR_PILE:
                            rt_snprintf(outStr, size, "InternalError");
                            break;
                        case OCPP_ERR_METER:
                            rt_snprintf(outStr, size, "PowerMeterFailure");
                            break;
                        case OCPP_ERR_HIGH_TEMP:
                            rt_snprintf(outStr, size, "HighTemperature");
                            break;
                        case OCPP_ERR_OVER_VOL:
                            rt_snprintf(outStr, size, "OverVoltage");
                            break;
                        case OCPP_ERR_UNDER_VOL:
                            rt_snprintf(outStr, size, "UnderVoltage");
                            break;
                        default:
                            break;
                    }
                    return; 
                }
            }
        }
	}
    if(alarmCnt > 0){
        rt_snprintf(outStr, size, "OtherError");
    }
    else{
        rt_snprintf(outStr, size, "NoError");
    }
}

unsigned long get_millisecond()
{
	return (unsigned long)rt_tick_get_millisecond();
}
/********************************************************************
* 功能:MicroOcpp初始化操作
* 输入:无
* 输出:
* 说明:
* 修改记录:
********************************************************************/
int8_t setup() 
{
    /*
     * Initialize the OCPP library
     */
    mocpp_initialize(OCPP_BACKEND_URL, OCPP_CHARGE_BOX_ID, CCU_DEV_TYPE, CCU_MANUFACTURE);

    //设置控制台打印回调函数
    mocpp_set_console_out([](const char* msg){
        rt_kprintf("%s",msg);
    });
    //设置毫秒级时间戳回调函数
    mocpp_set_timer(get_millisecond);
    //设置获取枪连接状态回调函数
    setConnectorPluggedInput([]() {
        bool gunState = false;
        gunState = (Charge_task_st.CharA_Flag == CHAR_NO_GUN)? false : true; 
        return gunState;
    },OCPP_GUNA);
    setConnectorPluggedInput([]() {
        bool gunState = false;
        gunState = (Charge_task_st.CharB_Flag == CHAR_NO_GUN)? false : true; 
        return gunState;
    }, OCPP_GUNB);
    //设置获取直流电表读数回调函数, 单位Wh
    setEnergyMeterInput([](){
        int32_t energyData = 0;
        energyData = EnergyDataA.Energy * 1000;    //转化为WH 
        return energyData;
    }, OCPP_GUNA);
    setEnergyMeterInput([](){
        int32_t energyData = 0;
        energyData = EnergyDataB.Energy * 1000;    //转化为WH 
        return energyData;
    }, OCPP_GUNB);
    //设置获取直流电表当前功率回调函数  //单位W
    setPowerMeterInput([]{
       float powerData = 0.0f; 
       powerData = EnergyDataA.Voltage * EnergyDataA.Current;  
       return powerData; 
    }, OCPP_GUNA);
    setPowerMeterInput([]{
       float powerData = 0.0f; 
       powerData = EnergyDataB.Voltage * EnergyDataB.Current;  
       return powerData; 
    }, OCPP_GUNB);
    //设置电子锁解锁返回回调函数
    // setOnUnlockConnectorInOut([]{

    // },OCPP_GUNA);
    //充电过程中车暂停充电
    // setEvReadyInput([]{
    //    return false;
    // }, OCPP_GUNA);
    //充电过程中桩暂停充电
    // setEvseReadyInput([](){
    //    return false;
    // }, OCPP_GUNA);
    //设置故障码获取的回调函数,有效码参考 OCPP 1.6, Edit2中的StatusNotification协议帧
    addErrorCodeInput([]{   //表示整机,只有可用、不可用、故障三个状态
        uint8_t rst = 0;
        static char errCode[12] = {0}; 
        rst = get_errCnt(ERR_GUN_INDEX_ALL, 1);
        if(rst == 0){   //可用
            return (char*)nullptr;
//            rt_snprintf(errCode, sizeof(errCode), "Available");
        }
        else{           //故障
            rt_snprintf(errCode, sizeof(errCode), "Faulted");
        }
        return errCode;
    }, 0);
    addErrorCodeInput([]{   //A枪
        static char errCodeA[24] = {0}; 
        get_One_Err(ERR_GUN_INDEX_A, errCodeA, sizeof(errCodeA));
        if(rt_strncmp(errCodeA,"NoError", 7) == 0){
            return (char*)nullptr;     
        }
        return errCodeA;
    }, OCPP_GUNA);
    addErrorCodeInput([]{   //B枪
        static char errCodeB[24] = {0}; 
        get_One_Err(ERR_GUN_INDEX_B, errCodeB, sizeof(errCodeB));
        if(rt_strncmp(errCodeB,"NoError", 7) == 0){
            return (char*)nullptr;     
        }
        return errCodeB;
    }, OCPP_GUNB);
    //错误数据类
    addErrorDataInput([](){
        char errStr[24] = {0};
        char* errStr_t = nullptr;
        get_One_Err(ERR_GUN_INDEX_ALL, errStr, sizeof(errStr));
        if(rt_strncmp(errStr,"NoError", 7) != 0){
            errStr_t = errStr;     
        }
        MicroOcpp::ErrorData errData(errStr_t);
        return errData;
    }, OCPP_ALL);
    addErrorDataInput([](){
        char errStrA[24] = {0};
        char* errStrA_t = nullptr;
        get_One_Err(ERR_GUN_INDEX_A, errStrA, sizeof(errStrA));
        if(rt_strncmp(errStrA,"NoError", 7) != 0){
            errStrA_t = errStrA;     
        }
        MicroOcpp::ErrorData errData(errStrA_t);
        return errData;
    }, OCPP_GUNA);
    addErrorDataInput([](){
        char errStrB[24] = {0};
        char* errStrB_t = nullptr;
        get_One_Err(ERR_GUN_INDEX_B, errStrB, sizeof(errStrB));
        if(rt_strncmp(errStrB,"NoError", 7) != 0){
            errStrB_t = errStrB;    
        }
        MicroOcpp::ErrorData errData(errStrB_t);
        return errData;
    }, OCPP_GUNB);

    //接收到ocpp服务器消息的通知回调
    setTxNotificationOutput([](MicroOcpp::Transaction* trans,TxNotification type){
        MO_CONSOLE_PRINTF("deal:%d", type);
        switch(type){
            case TxNotification_Authorized:     //授权成功准备充电,本地请求还有远程启动最终成功都走这里

                break;
            case TxNotification_AuthorizationRejected: //IdTag/令牌未授权

                break;
            case TxNotification_AuthorizationTimeout:  //授权超时

                break;
            case TxNotification_ReservationConflict:   //预订冲突

                break;
            case TxNotification_RemoteStart:           //远程启动

                break;
            case TxNotification_RemoteStop:           //远程停止

                break;
            default:
                break;
        }
    });
    //ocpp服务端下发reset重启时,先调用此回调查询是否允许重启(注意若下发的硬重启,即使返回FALSE,也会强制重启)
    setOnResetNotify([](bool type){ //0:软重启 1:硬重启
        bool rst = false;
        //A、B枪都未插枪时才允许重启
        if(Charge_task_st.CharA_Flag == CHAR_NO_GUN &&  Charge_task_st.CharB_Flag == CHAR_NO_GUN){
            rst = true;
        }
        return rst;
    });
    //ocpp服务端下发reset重启的回调函数
    setOnResetExecute([](bool type){ //0:软重启 1:硬重启
        set_lcdReboot();    //重启屏幕
		rt_hw_cpu_reset();  //重启系统
    });

    //设置接收监听,ocpp客户端接收到operationType对应消息时,触发此函数
    // setOnReceiveRequest("SetChargingProfile", [] (JsonObject payload) {
    //     MO_CONSOLE_PRINTF("update connector %i with chargingProfileId %i\n",
    //                        payload["connectorId"], payload["csChargingProfiles"]["chargingProfileId"]);
                                                                      
    // });
    //设置发送监听,ocpp客户端发送operationType对应消息时,触发此函数
    // setOnSendConf("RemoteStopTransaction", [] (JsonObject payload) -> void {
    //     if (!strcmp(payload["status"], "Rejected")) {   //当ocpp客户端拒绝了远程停止充电,但想能够实际停止充电时
    //        endTransaction(nullptr, "Remote");           //结束交易并发送StopTransaction
    //        MO_CONSOLE_PRINTF("[main] override rejected RemoteStopTransaction");
    //     }
    // });
    return 1;
    //... see MicroOcpp.h for more settings
}

void loop() 
{
    static uint8_t permitCharg_last[GUNLIMT_CS] = {0}; 
    static uint8_t permitCharg_curr[GUNLIMT_CS] = {0}; 
    /*
     * Do all OCPP stuff (process WebSocket input, send recorded meter values to Central System, etc.)
     */
    mocpp_loop();

    /*
     * Energize EV plug if OCPP transaction is up and running
     */
    for(uint8_t i = 0; i < GUNLIMT_CS; i++){
        permitCharg_last[i] = permitCharg_curr[i];
        permitCharg_curr[i] = ocppPermitsCharge(i+1);
        if(permitCharg_last[i] != permitCharg_curr[i]){
            if(permitCharg_curr[i]){    //充电开始
                Ocpp_set_chargStart(getTransaction(i+1)->getTransactionId(), GUN_NUM_ENUM(i));   
            }
            else{   //充电结束
                if(get_chargFlag(GUN_NUM_ENUM(i))){
                    Ocpp_set_chargStop(GUN_NUM_ENUM(i));
                }
            }
        } 
    }
    //... see MicroOcpp.h for more possibilities
}
/********************************************************************
 * 功能:云平台交互线程入口
 * 输入:无
 * 输出:无
 * 返回:无
 * 说明:
 * 修改记录:
 ********************************************************************/
static void Task_ocpp_plat_thread_entry(void *parameter)
{
    int8_t rst = 0;
    uint8_t disConnectCnt = 0;
    rst = setup();    //ocpp初始化 

    while(1){
        loop();
        if(get_wsIsConnected()){
            step_plat = OCPP_PLAT_STEP_ONLINE;
            disConnectCnt = 0;     
        }
        else{
            if(disConnectCnt <= CHECK_TIME_CNT)
                disConnectCnt++;
            else
                step_plat = OCPP_PLAT_STEP_OFFLINE; 
        }
        rt_thread_mdelay(100);
    }
}
/********************************************************************
* 功能:云平台交互线程初始化
* 输入:无
* 输出:
* 说明:
* 修改记录:
********************************************************************/
static int Task_ocpp_Plat_thread(void)
{
	rt_thread_t thread;
	rt_err_t ret = RT_EOK;
	
	thread = rt_thread_create("task_ocpp_plat", Task_ocpp_plat_thread_entry, RT_NULL,
							 TASK_PLAT_THREAD_STACK_SIZE, TASK_PLAT_THREAD_PRIORITY, TASK_PLAT_THREAD_TICK);
	if(thread != RT_NULL)
	{
		rt_thread_startup(thread);
	}
	else
		ret = RT_ERROR;
	
	return ret;
}
INIT_APP_EXPORT(Task_ocpp_Plat_thread);

/********************************************************************
* 功能:获取桩登录运营平台状态
* 输入:无
* 输出:1:登录成功;
* 说明:
* 修改记录:
********************************************************************/
uint8_t get_platLogState(void)
{
	uint8_t rst = 0;
	if(step_plat == OCPP_PLAT_STEP_ONLINE)
	    rst = 1;
	return rst;
}
/********************************************************************
* 功能:检查桩登录运营平台状态
* 输入:无
* 输出:无
* 说明:循环调用,登录失败置位报警
* 修改记录:
********************************************************************/
void user_chaeckPlatCon(void)
{
    if(step_plat >= OCPP_PLAT_STEP_ONLINE){	//已连接清除
        if (user_GetErrFlagSafe(E064_095, E066)){
            user_ClearErrFlagSafe(E064_095, E066);
            ErrInfoTable[66].ErrGunA_B = 0;
        }
    }
    else if(step_plat == OCPP_PLAT_STEP_OFFLINE){
        if (!user_GetErrFlagSafe(E064_095, E066)){
            user_SetErrFlagSafe(E064_095, E066);
            ErrInfoTable[66].ErrGunA_B |= 0x03;
        }	
    }
}
/********************************************************************
* 功能:向运营平台请求充电开始
* 输入:idTag:鉴权信息; gunNum:枪号
* 输出:
* 说明:
* 修改记录:
********************************************************************/
uint8_t ocpp_set_beginTransactionReq(const char *idTag, GUN_NUM_ENUM gunNum)
{
    if(idTag == RT_NULL || gunNum >= GUNLIMT_CS){
        rt_kprintf("ocpp_set_beginTransactionReq fail\n");
        return 0;
    }
    return beginTransaction(idTag, gunNum + 1);
}
/********************************************************************
* 功能:通知ocpp库停止充电
* 输入:idTag:鉴权信息; gunNum:枪号
* 输出:
* 说明:
* 修改记录:
********************************************************************/
uint8_t ocpp_set_endTransaction(const char *idTag, GUN_NUM_ENUM gunNum)
{
    if(idTag == RT_NULL || gunNum >= GUNLIMT_CS){
        rt_kprintf("ocpp_set_endTransaction fail\n");
        return 0;
    }
    //ocpp库充电运行时,停止充电有效
    if(isTransactionActive(gunNum + 1) && isTransactionRunning(gunNum + 1)){
        return endTransaction(idTag,"Local",gunNum + 1);
    }
    else{
        isTransactionRunning(gunNum + 1));
    }
    return 0;
}
/********************************************************************
* 功能:向OCPP库获取交易ID
* 输入:gunNum:枪号
* 输出:
* 说明:
* 修改记录:
********************************************************************/
const char* ocpp_get_idTag(GUN_NUM_ENUM gunNum)
{
    if(gunNum >= GUNLIMT_CS){
        rt_kprintf("ocpp_get_idTag fail\n");
        return 0;
    }
    return getTransactionIdTag(gunNum + 1); 
}
#endi

此文件中的启动充电和停止充电等接口是向其它业务线程提供使用。

4.WebSocket通信接口适配

ArduinoJSON直接移植就可以,ArduinoWebSocket则需要修改底层通信接口,可只保留WebSocket客户端部分,服务器部分用不着。在WebSockets.h头文件中,令WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32,此处我是封装了一个Ec800eClient类适配通信接口,其它类声明注释掉即可,如下:

#elif(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32)

// #include <WiFi.h>
// #include <WiFiClientSecure.h>
//#define SSL_AXTLS
#define WEBSOCKETS_NETWORK_CLASS  Ec800eClient                  //ec800Client
//#define WEBSOCKETS_NETWORK_SSL_CLASS WiFiClientSecure
//#define WEBSOCKETS_NETWORK_SERVER_CLASS     WiFiServer        

需要注意的是WebSockets.h头文件中用到了String类,本来是Arduino平台中实现的,此处可用MicroOcpp库中的内存优化,其中就包括了Sting类的实现,因此只需要WebSockets.h头文件中包含#include <MicroOcpp/Core/Memory.h>即可。

此处封装了一个类然后提供  void ws_recv(uint8_t* data, uint16_t len);接口由EC800处理模块调用,将串口读取的数据分类后调用此接口放到缓存区。然后此文件中调用tcp_send_data接口将数据通过EC800处理模块发送出去。

头文件Ec800eAdapta.h为:

#ifndef EC800EADAPTA_H_
#define EC800EADAPTA_H_

#include "user_ec800.h"
#define WSRECV_MAX  (1024)

struct wsRecv_STR
{
  uint16_t wr_index;
  uint16_t rd_index;
  char recv_buf[WSRECV_MAX];
};

unsigned long millis();
size_t get_free_heap_size(void);
void myTrim(char* str);

class Ec800eClient {
public:
  Ec800eClient();
  ~Ec800eClient();

  uint8_t connected(); 
  int available();
  int read();
  int read(char *buf, size_t size);
  size_t write(const char *buf, size_t size);
  int connect(const char *host, uint16_t port);
  void flush();
  void stop();
  uint8_t readStringUntil(char terminator, char* outdata, uint8_t len);
  size_t readBytes(char *buffer, size_t length);
  void setTimeout(unsigned long timeout);
  void ws_recv(uint8_t* data, uint16_t len);
private:
  int _timeout;
  bool _connected = false;
  int _lastWriteTimeout = 0;
  int _lastReadTimeout = 0;
  wsRecv_STR wsRecv_str; 
};
#endif

Ec800eAdapta.cpp为:

/********************************************************************************
 * Copyright (c) 
 *
 * 文件名称:Ec800eAdapta.cpp
 *
 * 描    述:Ec800e通信模组适配ArduinoWebSockets 库底层通信相关接口
 *
 * 修改记录:
 *******************************************************************************/
#include "Ec800eAdapta.h"
#include "user_timer.h"

#define CLIENT_DEF_CONN_TIMEOUT_MS (3000)
#define myIsspace(c)           (c == ' ' || c == '\f' || c == '\n' || c == '\r' || c == '\t' || c == '\v')

rt_mutex_t mutex_ocpp_ws = RT_NULL;
Ec800eClient* ec800Clent_t = NULL;

/********************************************************************
 * 功能:适配ArduinoWebSockets,获取程序运行后的毫秒数
 * 输入:无
 * 输出:无
 * 返回:程序运行后的毫秒数
 * 说明:
 * 修改记录:
 ********************************************************************/
unsigned long millis(){
    return (unsigned long)get_timer_ms();
}
/********************************************************************
 * 功能:获取当前堆剩余大小
 * 输入:无
 * 输出:无
 * 返回:剩余堆大小
 * 说明:
 * 修改记录:
 ********************************************************************/
size_t get_free_heap_size(void)
{
    rt_size_t total = 0, used = 0, max_used = 0;
    
    // 使用rt_memory_info函数获取内存使用信息
    rt_memory_info(&total, &used, &max_used);
    
    // 返回可用堆内存大小
    return (total - used);
}
/********************************************************************
 * 功能:ocpp发送回调
 * 输入:无
 * 输出:无
 * 返回:无
 * 说明:
 * 修改记录:
 ********************************************************************/
static void ocpp_send_callback(uint8_t fram_type,uint8_t* data_domain,eTcpSendStatus send_status)
{
  if(send_status != TCP_SEND_STATUS_SUCCESS){
    rt_kprintf("ocpp send fail\n");
  }
}
/********************************************************************
 * 功能:ocpp接收C语言回调适配接口
 * 输入:无
 * 输出:无
 * 返回:无
 * 说明:
 * 修改记录:
 ********************************************************************/
void ocpp_recv_callBack(uint8_t* data, uint16_t len)
{
  if(ec800Clent_t)
    ec800Clent_t->ws_recv(data, len);
}

void myTrim(char* str) 
{
    if (str == NULL) return;

    // 找到第一个非空白字符的位置
    char* start = str;
    while (myIsspace((unsigned char)*start)) {
        start++;
    }

    // 找到最后一个非空白字符的位置
    char* end = str + strlen(str) - 1;
    while (end > start && myIsspace((unsigned char)*end)) {
        end--;
    }

    // 在最后一个非空白字符后添加字符串结束符
    *(end + 1) = '\0';

    // 如果开头有空白字符,则将有效部分移到字符串开头
    if (start != str) {
        memmove(str, start, end - start + 1);
        str[end - start + 1] = '\0';
    }
}

Ec800eClient::Ec800eClient() 
{
  _timeout = CLIENT_DEF_CONN_TIMEOUT_MS;
  /* 创建互斥量 */
	mutex_ocpp_ws = rt_mutex_create("mutex_ocpp_ws",RT_IPC_FLAG_FIFO);
	if(mutex_ocpp_ws == RT_NULL)
	{
		rt_kprintf("create mutex_ocpp_ws failed!\r\n");
	}
  memset(&wsRecv_str, 0, sizeof(wsRecv_str));
  ec800Clent_t = this;
  set_tcp_recv_callback(ocpp_recv_callBack);
}

Ec800eClient::~Ec800eClient()
{

}

void Ec800eClient::ws_recv(uint8_t* data, uint16_t len)
{
  if(data == RT_NULL || len == 0){
    return;
  }
  rt_mutex_take(mutex_ocpp_ws,RT_WAITING_FOREVER); 
  /* 数据存到环形缓存区 */
  for(uint16_t i = 0; i < len; i++){
    wsRecv_str.recv_buf[wsRecv_str.wr_index] = data[i];
    wsRecv_str.wr_index = (wsRecv_str.wr_index + 1) & (WSRECV_MAX - 1);
  }
	if(wsRecv_str.wr_index == wsRecv_str.rd_index)
	{
		wsRecv_str.rd_index = (wsRecv_str.rd_index + 1) & (WSRECV_MAX - 1);
	}
  rt_mutex_release(mutex_ocpp_ws);
}
/********************************************************************
 * 功能:返回ec800模组TCP连接状态
 * 输入:无
 * 输出:无
 * 返回:TCP连接状态
 * 说明:
 * 修改记录:
 ********************************************************************/
uint8_t Ec800eClient::connected() 
{
  uint8_t ret = 0;
  ret = get_net_status();
  return ((ret & STATUS_4G_TCP_SUCCESS) && (ret & STATUS_4G_INIT_SUCCESS)  && _connected) ?1:0;
}
/********************************************************************
 * 功能:获取当前通信接收缓存区中有多少数据可读取
 * 输入:无
 * 输出:无
 * 返回:当前接收缓存区中的字节个数
 * 说明:
 * 修改记录:
 ********************************************************************/
int Ec800eClient::available() {
  int cnt = 0;
  if (!connected()) {
    return 0;
  }
  cnt = (WSRECV_MAX + wsRecv_str.wr_index - wsRecv_str.rd_index) & (WSRECV_MAX - 1);
  return cnt;
}
/********************************************************************
 * 功能:从当前接收缓存区读出1个数据
 * 输入:无
 * 输出:无
 * 返回:当前接收缓存区中的1个数据
 * 说明:
 * 修改记录:
 ********************************************************************/
int Ec800eClient::read() {
  char data = 0;
  int res = read(&data, 1);
  if (res < 0) {
    return res;
  }
  if (res == 0) {  //  No data available.
    return -1;
  }
  return data;
}
/********************************************************************
 * 功能:从当前接收缓存区读出size个数据
 * 输入:
 * 输出:无
 * 返回:实际读出数据个数,ArduinoWebSockets库中调用时已处理未读取够size情况
 * 说明:
 * 修改记录:
 ********************************************************************/
int Ec800eClient::read(char *buf, size_t size) {
  uint16_t len = 0;
  uint16_t len_min = 0;
  if(!connected() || buf == nullptr || size == 0){
    return 0; 
  }
  rt_mutex_take(mutex_ocpp_ws,RT_WAITING_FOREVER);
	if((len = (WSRECV_MAX + wsRecv_str.wr_index - wsRecv_str.rd_index) & (WSRECV_MAX-1)) > 0)
	{
    len_min = (len >= size)? size : len;  
		if(WSRECV_MAX - wsRecv_str.rd_index >= len_min)
		{
			memcpy(buf,&wsRecv_str.recv_buf[wsRecv_str.rd_index],len_min);
		}
		else
		{
			memcpy(buf,&wsRecv_str.recv_buf[wsRecv_str.rd_index], WSRECV_MAX - wsRecv_str.rd_index);
			memcpy(&buf[WSRECV_MAX - wsRecv_str.rd_index], &wsRecv_str.recv_buf[0], len_min - (WSRECV_MAX - wsRecv_str.rd_index));
		}
		wsRecv_str.rd_index = (wsRecv_str.rd_index + len_min) & (WSRECV_MAX - 1);
	}
	rt_mutex_release(mutex_ocpp_ws);
  return len_min;
}
/********************************************************************
 * 功能:从接收缓存区读字符串直到遇到terminator
 * 输入:无
 * 输出:无
 * 返回:无
 * 说明:内部有超时时间
 * 修改记录:
 ********************************************************************/
uint8_t Ec800eClient::readStringUntil(char terminator, char* outdata, uint8_t len) {
  char c = 0;
  uint8_t byteCnt = 0;
  if(terminator == 0 || len == 0 || outdata == nullptr)
    return 0;
  uint64_t timeCnt = millis();
   
  do{
    c = read();
    outdata[byteCnt++] = c;
  }while(c >= 0 && (char)c != terminator && millis() - timeCnt < _timeout && byteCnt < len - 1);
 
  return byteCnt;
}
/********************************************************************
 * 功能:从接收缓存区读数据
 * 输入:无
 * 输出:无
 * 返回:无
 * 说明:函数内部直到读取length为止,超时时间到日志打印超时错误并返回读取到的数据
 * 修改记录:
 ********************************************************************/
size_t Ec800eClient::readBytes(char *buffer, size_t length) {
  uint64_t time_out = millis();
  size_t readCnt = 0;
  do{
    readCnt += read(buffer + readCnt, length - readCnt);
    if(readCnt >= length){
      return length;
    }
    rt_thread_yield();
  }while(millis() - time_out < _timeout);

  rt_kprintf("[readBytes] fail size is:%d\n", readCnt);
  return readCnt;
}
/********************************************************************
 * 功能:将要发送的数据写入到发送缓冲区中
 * 输入:无
 * 输出:无
 * 返回:写入成功的字节数
 * 说明:
 * 修改记录:
 ********************************************************************/
size_t Ec800eClient::write(const char *buf, size_t size) {
  //判断连接状态
  if(!connected() || buf == nullptr || size == 0){
    return 0;
  }
  tcp_send_data((uint8_t *)buf, size, ocpp_send_callback);
  return size; 
}
/********************************************************************
 * 功能:arduinoWebSockets库调用连接TCP
 * 输入:IP和端口号
 * 输出:无
 * 返回:连接成功与否
 * 说明:实际在user_ec800.c文件中的线程去连接,此处值获取连接结果
 * 修改记录:
 ********************************************************************/
int Ec800eClient::connect(const char *host, uint16_t port) 
{
  _connected = true;
  return connected();
}
/********************************************************************
 * 功能:arduinoWebSockets库调用清除接收缓存区
 * 输入:无
 * 输出:无
 * 返回:无
 * 说明:无
 * 修改记录:
 ********************************************************************/
void Ec800eClient::flush() 
{
  rt_mutex_take(mutex_ocpp_ws,RT_WAITING_FOREVER);
  wsRecv_str.rd_index = wsRecv_str.wr_index; 
  rt_mutex_release(mutex_ocpp_ws);
}
/********************************************************************
 * 功能:arduinoWebSockets库调用断开TCP
 * 输入:无
 * 输出:无
 * 返回:无
 * 说明:实际ec800e模组不需要断开TCP连接
 * 修改记录:
 ********************************************************************/
void Ec800eClient::stop() {
  _connected = false;
  _lastReadTimeout = 0;
  _lastWriteTimeout = 0;
}

void Ec800eClient::setTimeout(unsigned long timeout)  // sets the maximum number of milliseconds to wait
{
  _timeout = timeout;
}

5.项目工程(C/C++)编译链接通过

用vscode加GCC编译的话,根据错误提示解决错误就可。在keil下用ARMCC编译时,需要切到ARMCC6编译器,C++11标准,优化等级切到O1。

特别注意:如果用了嵌入式实时系统的话,因为默认C++构造函数先执行,而MicroOcpp库类的构造函数中有申请动态内存的,但如果嵌入式实时系统还没初始化完堆管理池的话,就会进入断言错误,陷入死循环,我这里用的RT_Thread系统。此时需要在嵌入式实时系统初始化函数加一个如下声明int rtthread_startup(void) __attribute__((constructor));在main函数之前定义初始化函数,确保优先于C++全局变量初始化。

四、MicroOcpp移植后如何测试

1.部署开源OCPP协议服务器

验证环节可以找一些开源的OCPP协议服务器,例如SteVe,下载链接为https://gitcode.com/gh_mirrors/st/steve?source_module=search_result_repo

可参考以上链接中的README,去部署。需要一台连入公网独立IP的电脑,可以考虑去试用各运营商的云服务器,推荐腾讯,新用户试用第一个月免费,一个月后删除关闭就可。

2.开源工具测试OCPP协议服务器

部署好OCPP协议服务器后,可以先用开源的模仿充电桩固件的在线网站去连接OCPP协议服务器,测试部署的是否有问题,有利于调试自己开发的充电桩固件程序,能排查问题。以下链接为充电桩模拟软件:

OCPP1.6模拟器 devTest.run


总结

本文介绍了充电桩固件上快速适配OCPP协议的方法,就是移植MicroOcpp库,讲明了业务层,及网络通信层如何适配接口,并提出如何自测试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值