前言
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://download.csdn.net/download/x150061/92777359
2.MicroOcpp是什么
MicroOcpp是一个专为微控制器(MCU)和嵌入式系统优化的轻量级OCPP 1.6/2.0.1客户端开源库,用C/C++编写,可在资源受限的设备上实现充电桩与中央管理系统的标准化通信。它正被广泛应用于ESP32、STM32、Arduino等主流嵌入式平台,助力开发者低成本、高效率地构建符合国际标准的智能充电站 。
MicroOcpp完全开源,托管于GitHub等公共代码平台,允许开发者自由使用、修改和部署,广泛应用于个人开发者、初创企业及中小厂商的智能充电桩研发中 。gitee上的下载链接为:
从MicroOcpp的README可以看到,需要移植前置ArduinoJSON和arduinoWebSockets。我这里的开发平台是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协议服务器,测试部署的是否有问题,有利于调试自己开发的充电桩固件程序,能排查问题。以下链接为充电桩模拟软件:
总结
本文介绍了充电桩固件上快速适配OCPP协议的方法,就是移植MicroOcpp库,讲明了业务层,及网络通信层如何适配接口,并提出如何自测试。
562

被折叠的 条评论
为什么被折叠?



