feat(firmware): add Mongoose client implementation for OCPP communication

This commit is contained in:
2026-03-17 01:25:41 +08:00
parent e1fb43d57b
commit 8371b2a76b
8 changed files with 1168 additions and 12 deletions

View File

@@ -0,0 +1,37 @@
{
"name": "MicroOcppMongoose",
"version": "1.2.0",
"description": "Mongoose Adapter for the MicroOCPP Client",
"keywords": "OCPP, 1.6, OCPP 1.6, OCPP 2.0.1, Smart Energy, Smart Charging, client, ESP8266, ESP32, Arduino, EVSE, Charge Point, Mongoose",
"repository":
{
"type": "git",
"url": "https://github.com/matth-x/MicroOcppMongoose/"
},
"authors":
[
{
"name": "Matthias Akstaller",
"url": "https://www.micro-ocpp.com",
"maintainer": true
}
],
"license": "GPL-3.0",
"homepage": "https://www.micro-ocpp.com",
"frameworks": "arduino,espidf",
"platforms": "espressif8266, espressif32",
"export": {
"include":
[
"src/MicroOcppMongooseClient_c.cpp",
"src/MicroOcppMongooseClient_c.h",
"src/MicroOcppMongooseClient.cpp",
"src/MicroOcppMongooseClient.h",
"CHANGELOG.md",
"CMakeLists.txt",
"library.json",
"LICENSE",
"README.md"
]
}
}

View File

@@ -0,0 +1,754 @@
// matth-x/MicroOcppMongoose
// Copyright Matthias Akstaller 2019 - 2024
// GPL-3.0 License (see LICENSE)
#include "MicroOcppMongooseClient.h"
#include <MicroOcpp/Core/Configuration.h>
#include <MicroOcpp/Debug.h>
#if MO_ENABLE_V201
#include <MicroOcpp/Model/Variables/VariableContainer.h>
#endif
#define DEBUG_MSG_INTERVAL 5000UL
#define WS_UNRESPONSIVE_THRESHOLD_MS 15000UL
#define MO_MG_V614 614
#define MO_MG_V708 708
#define MO_MG_V713 713
#define MO_MG_V714 714
#define MO_MG_V715 715
#ifndef MO_MG_USE_VERSION
#if defined(MO_MG_VERSION_614)
#define MO_MG_USE_VERSION MO_MG_V614
#else
#define MO_MG_USE_VERSION MO_MG_V708
#endif
#endif
#if MO_MG_USE_VERSION == MO_MG_V614
#define MO_MG_F_IS_MOcppMongooseClient MG_F_USER_2
#endif
namespace MicroOcpp {
bool validateAuthorizationKeyHex(const char *auth_key_hex);
}
using namespace MicroOcpp;
#if MO_MG_USE_VERSION <= MO_MG_V708
void ws_cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data);
#else
void ws_cb(struct mg_connection *c, int ev, void *ev_data);
#endif
MOcppMongooseClient::MOcppMongooseClient(struct mg_mgr *mgr,
const char *backend_url_factory,
const char *charge_box_id_factory,
unsigned char *auth_key_factory, size_t auth_key_factory_len,
const char *ca_certificate,
std::shared_ptr<FilesystemAdapter> filesystem,
ProtocolVersion protocolVersion) : mgr(mgr), protocolVersion(protocolVersion) {
bool readonly;
if (filesystem) {
configuration_init(filesystem);
//all credentials are persistent over reboots
readonly = false;
} else {
//make the credentials non-persistent
MO_DBG_WARN("Credentials non-persistent. Use MicroOcpp::makeDefaultFilesystemAdapter(...) for persistency");
readonly = true;
}
if (auth_key_factory_len > MO_AUTHKEY_LEN_MAX) {
MO_DBG_WARN("auth_key_factory too long - will be cropped");
auth_key_factory_len = MO_AUTHKEY_LEN_MAX;
}
#if MO_ENABLE_V201
if (protocolVersion.major == 2) {
websocketSettings = std::unique_ptr<VariableContainerOwning>(new VariableContainerOwning());
if (filesystem) {
websocketSettings->enablePersistency(filesystem, MO_WSCONN_FN_V201);
}
auto csmsUrl = makeVariable(Variable::InternalDataType::String, Variable::AttributeType::Actual);
csmsUrl->setComponentId("SecurityCtrlr");
csmsUrl->setName("CsmsUrl");
csmsUrl->setString(backend_url_factory ? backend_url_factory : "");
csmsUrl->setPersistent();
v201csmsUrlString = csmsUrl.get();
websocketSettings->add(std::move(csmsUrl));
auto identity = makeVariable(Variable::InternalDataType::String, Variable::AttributeType::Actual);
identity->setComponentId("SecurityCtrlr");
identity->setName("Identity");
identity->setString(charge_box_id_factory ? charge_box_id_factory : "");
identity->setPersistent();
v201identityString = identity.get();
websocketSettings->add(std::move(identity));
auto basicAuthPassword = makeVariable(Variable::InternalDataType::String, Variable::AttributeType::Actual);
basicAuthPassword->setComponentId("SecurityCtrlr");
basicAuthPassword->setName("BasicAuthPassword");
char basicAuthPasswordVal [MO_AUTHKEY_LEN_MAX + 1];
snprintf(basicAuthPasswordVal, sizeof(basicAuthPasswordVal), "%.*s", (int)auth_key_factory_len, auth_key_factory ? (const char*)auth_key_factory : "");
basicAuthPassword->setString(basicAuthPasswordVal);
basicAuthPassword->setPersistent();
v201basicAuthPasswordString = basicAuthPassword.get();
websocketSettings->add(std::move(basicAuthPassword));
websocketSettings->load(); //if settings on flash already exist, this overwrites factory defaults
} else
#endif
{
setting_backend_url_str = declareConfiguration<const char*>(
MO_CONFIG_EXT_PREFIX "BackendUrl", backend_url_factory, MO_WSCONN_FN, readonly, true);
setting_cb_id_str = declareConfiguration<const char*>(
MO_CONFIG_EXT_PREFIX "ChargeBoxId", charge_box_id_factory, MO_WSCONN_FN, readonly, true);
char auth_key_hex [2 * MO_AUTHKEY_LEN_MAX + 1];
auth_key_hex[0] = '\0';
if (auth_key_factory) {
for (size_t i = 0; i < auth_key_factory_len; i++) {
snprintf(auth_key_hex + 2 * i, 3, "%02X", auth_key_factory[i]);
}
}
setting_auth_key_hex_str = declareConfiguration<const char*>(
"AuthorizationKey", auth_key_hex, MO_WSCONN_FN, readonly, true);
registerConfigurationValidator("AuthorizationKey", validateAuthorizationKeyHex);
}
ws_ping_interval_int = declareConfiguration<int>(
"WebSocketPingInterval", 5, MO_WSCONN_FN);
reconnect_interval_int = declareConfiguration<int>(
MO_CONFIG_EXT_PREFIX "ReconnectInterval", 10, MO_WSCONN_FN);
stale_timeout_int = declareConfiguration<int>(
MO_CONFIG_EXT_PREFIX "StaleTimeout", 300, MO_WSCONN_FN);
configuration_load(MO_WSCONN_FN); //load configs with values stored on flash
ca_cert = ca_certificate;
reloadConfigs(); //load WS creds with configs values
#if MO_MG_USE_VERSION == MO_MG_V614
MO_DBG_DEBUG("use MG version %s (tested with 6.14)", MG_VERSION);
#elif MO_MG_USE_VERSION == MO_MG_V708
MO_DBG_DEBUG("use MG version %s (tested with 7.8)", MG_VERSION);
#elif MO_MG_USE_VERSION == MO_MG_V713
MO_DBG_DEBUG("use MG version %s (tested with 7.13)", MG_VERSION);
#endif
maintainWsConn();
}
MOcppMongooseClient::MOcppMongooseClient(struct mg_mgr *mgr,
const char *backend_url_factory,
const char *charge_box_id_factory,
const char *auth_key_factory,
const char *ca_certificate,
std::shared_ptr<FilesystemAdapter> filesystem,
ProtocolVersion protocolVersion) :
MOcppMongooseClient(mgr,
backend_url_factory,
charge_box_id_factory,
(unsigned char *)auth_key_factory, auth_key_factory ? strlen(auth_key_factory) : 0,
ca_certificate,
filesystem,
protocolVersion) {
}
MOcppMongooseClient::~MOcppMongooseClient() {
MO_DBG_DEBUG("destruct MOcppMongooseClient");
if (websocket) {
reconnect(); //close WS connection, won't be reopened
#if MO_MG_USE_VERSION == MO_MG_V614
websocket->flags &= ~MO_MG_F_IS_MOcppMongooseClient;
websocket->user_data = nullptr;
#else
websocket->fn_data = nullptr;
#endif
}
}
void MOcppMongooseClient::loop() {
maintainWsConn();
}
bool MOcppMongooseClient::sendTXT(const char *msg, size_t length) {
if (!websocket || !isConnectionOpen()) {
return false;
}
size_t sent;
#if MO_MG_USE_VERSION == MO_MG_V614
if (websocket->send_mbuf.len > 0) {
sent = 0;
return false;
} else {
mg_send_websocket_frame(websocket, WEBSOCKET_OP_TEXT, msg, length);
sent = length;
}
#else
sent = mg_ws_send(websocket, msg, length, WEBSOCKET_OP_TEXT);
#endif
if (sent < length) {
MO_DBG_WARN("mg_ws_send did only accept %zu out of %zu bytes", sent, length);
//flush broken package and wait for next retry
(void)0;
}
return true;
}
void MOcppMongooseClient::maintainWsConn() {
if (mocpp_tick_ms() - last_status_dbg_msg >= DEBUG_MSG_INTERVAL) {
last_status_dbg_msg = mocpp_tick_ms();
//WS successfully connected?
if (!isConnectionOpen()) {
MO_DBG_DEBUG("WS unconnected");
} else if (mocpp_tick_ms() - last_recv >= (ws_ping_interval_int && ws_ping_interval_int->getInt() > 0 ? (ws_ping_interval_int->getInt() * 1000UL) : 0UL) + WS_UNRESPONSIVE_THRESHOLD_MS) {
//WS connected but unresponsive
MO_DBG_DEBUG("WS unresponsive");
}
}
if (websocket && isConnectionOpen() &&
stale_timeout_int && stale_timeout_int->getInt() > 0 && mocpp_tick_ms() - last_recv >= (stale_timeout_int->getInt() * 1000UL)) {
MO_DBG_INFO("connection %s -- stale, reconnect", url.c_str());
reconnect();
return;
}
if (websocket && isConnectionOpen() &&
ws_ping_interval_int && ws_ping_interval_int->getInt() > 0 && mocpp_tick_ms() - last_hb >= (ws_ping_interval_int->getInt() * 1000UL)) {
last_hb = mocpp_tick_ms();
#if MO_MG_USE_VERSION == MO_MG_V614
mg_send_websocket_frame(websocket, WEBSOCKET_OP_PING, "", 0);
#else
mg_ws_send(websocket, "", 0, WEBSOCKET_OP_PING);
#endif
}
if (websocket != nullptr) { //connection pointer != nullptr means that the socket is still open
return;
}
if (url.empty()) {
//cannot open OCPP connection: credentials missing
return;
}
if (reconnect_interval_int && reconnect_interval_int->getInt() > 0 && mocpp_tick_ms() - last_reconnection_attempt < (reconnect_interval_int->getInt() * 1000UL)) {
return;
}
MO_DBG_DEBUG("(re-)connect to %s", url.c_str());
last_reconnection_attempt = mocpp_tick_ms();
/*
* determine auth token
*/
std::string basic_auth64;
if (auth_key_len > 0) {
#if MO_DBG_LEVEL >= MO_DL_DEBUG
{
char auth_key_hex [2 * MO_AUTHKEY_LEN_MAX + 1];
auth_key_hex[0] = '\0';
for (size_t i = 0; i < auth_key_len; i++) {
snprintf(auth_key_hex + 2 * i, 3, "%02X", auth_key[i]);
}
MO_DBG_DEBUG("auth Token=%s:%s (key will be converted to non-hex)", cb_id.c_str(), auth_key_hex);
}
#endif //MO_DBG_LEVEL >= MO_DL_DEBUG
unsigned char *token = new unsigned char[cb_id.length() + 1 + auth_key_len]; //cb_id:auth_key
if (!token) {
//OOM
return;
}
size_t len = 0;
memcpy(token, cb_id.c_str(), cb_id.length());
len += cb_id.length();
token[len++] = (unsigned char) ':';
memcpy(token + len, auth_key, auth_key_len);
len += auth_key_len;
int base64_length = ((len + 2) / 3) * 4; //3 bytes base256 get encoded into 4 bytes base64. --> base64_len = ceil(len/3) * 4
char *base64 = new char[base64_length + 1];
if (!base64) {
//OOM
delete[] token;
return;
}
// mg_base64_encode() places a null terminator automatically, because the output is a c-string
#if MO_MG_USE_VERSION <= MO_MG_V708
mg_base64_encode(token, len, base64);
#else
mg_base64_encode(token, len, base64, base64_length + 1);
#endif
delete[] token;
MO_DBG_DEBUG("auth64 len=%u, auth64 Token=%s", base64_length, base64);
basic_auth64 = &base64[0];
delete[] base64;
} else {
MO_DBG_DEBUG("no authentication");
(void) 0;
}
#if MO_MG_USE_VERSION == MO_MG_V614
struct mg_connect_opts opts;
memset(&opts, 0, sizeof(opts));
const char *ca_string = ca_cert ? ca_cert : "*"; //"*" enables TLS but disables CA verification
//Check if SSL is disabled, i.e. if URL starts with "ws:"
if (url.length() >= strlen("ws:") &&
tolower(url.c_str()[0]) == 'w' &&
tolower(url.c_str()[1]) == 's' &&
url.c_str()[2] == ':') {
//yes, disable SSL
ca_string = nullptr;
MO_DBG_WARN("Insecure connection (WS)");
}
opts.ssl_ca_cert = ca_string;
char extra_headers [128] = {'\0'};
if (!basic_auth64.empty()) {
auto ret = snprintf(extra_headers, 128, "Authorization: Basic %s\r\n", basic_auth64.c_str());
if (ret < 0 || ret >= 128) {
MO_DBG_ERR("Basic Authentication failed: %d", ret);
(void)0;
}
}
websocket = mg_connect_ws_opt(
mgr,
ws_cb,
this,
opts,
url.c_str(),
protocolVersion.major == 2 ? "ocpp2.0.1" : "ocpp1.6",
*extra_headers ? extra_headers : nullptr);
if (websocket) {
websocket->flags |= MO_MG_F_IS_MOcppMongooseClient;
}
#else
websocket = mg_ws_connect(
mgr,
url.c_str(),
ws_cb,
this,
"Sec-WebSocket-Protocol: %s%s%s\r\n",
protocolVersion.major == 2 ? "ocpp2.0.1" : "ocpp1.6",
basic_auth64.empty() ? "" : "\r\nAuthorization: Basic ",
basic_auth64.empty() ? "" : basic_auth64.c_str()); // Create client
#endif
}
void MOcppMongooseClient::reconnect() {
if (!websocket) {
return;
}
#if MO_MG_USE_VERSION == MO_MG_V614
if (!connection_closing) {
const char *msg = "socket closed by client";
mg_send_websocket_frame(websocket, WEBSOCKET_OP_CLOSE, msg, strlen(msg));
}
#else
websocket->is_closing = 1; //Mongoose will close the socket and the following maintainWsConn() call will open it again
#endif
setConnectionOpen(false);
}
void MOcppMongooseClient::setBackendUrl(const char *backend_url_cstr) {
if (!backend_url_cstr) {
MO_DBG_ERR("invalid argument");
return;
}
#if MO_ENABLE_V201
if (protocolVersion.major == 2) {
if (v201csmsUrlString) {
v201csmsUrlString->setString(backend_url_cstr);
websocketSettings->commit();
}
} else
#endif
{
if (setting_backend_url_str) {
setting_backend_url_str->setString(backend_url_cstr);
configuration_save();
}
}
}
void MOcppMongooseClient::setChargeBoxId(const char *cb_id_cstr) {
if (!cb_id_cstr) {
MO_DBG_ERR("invalid argument");
return;
}
#if MO_ENABLE_V201
if (protocolVersion.major == 2) {
if (v201identityString) {
v201identityString->setString(cb_id_cstr);
websocketSettings->commit();
}
} else
#endif
{
if (setting_cb_id_str) {
setting_cb_id_str->setString(cb_id_cstr);
configuration_save();
}
}
}
void MOcppMongooseClient::setAuthKey(const char *auth_key_cstr) {
if (!auth_key_cstr) {
MO_DBG_ERR("invalid argument");
return;
}
return setAuthKey((const unsigned char*)auth_key_cstr, strlen(auth_key_cstr));
}
void MOcppMongooseClient::setAuthKey(const unsigned char *auth_key, size_t len) {
if (!auth_key || len > MO_AUTHKEY_LEN_MAX) {
MO_DBG_ERR("invalid argument");
return;
}
#if MO_ENABLE_V201
if (protocolVersion.major == 2) {
char basicAuthPassword [MO_AUTHKEY_LEN_MAX + 1];
snprintf(basicAuthPassword, sizeof(basicAuthPassword), "%.*s", (int)len, auth_key ? (const char*)auth_key : "");
if (v201basicAuthPasswordString) {
v201basicAuthPasswordString->setString(basicAuthPassword);
}
} else
#endif
{
char auth_key_hex [2 * MO_AUTHKEY_LEN_MAX + 1];
auth_key_hex[0] = '\0';
for (size_t i = 0; i < len; i++) {
snprintf(auth_key_hex + 2 * i, 3, "%02X", auth_key[i]);
}
if (setting_auth_key_hex_str) {
setting_auth_key_hex_str->setString(auth_key_hex);
configuration_save();
}
}
}
void MOcppMongooseClient::setCaCert(const char *ca_cert_cstr) {
ca_cert = ca_cert_cstr; //updated ca_cert takes immediate effect
}
void MOcppMongooseClient::reloadConfigs() {
reconnect(); //closes WS connection; will be reopened in next maintainWsConn execution
/*
* reload WS credentials from configs
*/
#if MO_ENABLE_V201
if (protocolVersion.major == 2) {
if (v201csmsUrlString) {
backend_url = v201csmsUrlString->getString();
}
if (v201identityString) {
cb_id = v201identityString->getString();
}
if (v201basicAuthPasswordString) {
snprintf((char*)auth_key, sizeof(auth_key), "%s", v201basicAuthPasswordString->getString());
auth_key_len = strlen((char*)auth_key);
}
} else
#endif
{
if (setting_backend_url_str) {
backend_url = setting_backend_url_str->getString();
}
if (setting_cb_id_str) {
cb_id = setting_cb_id_str->getString();
}
if (setting_auth_key_hex_str) {
auto auth_key_hex = setting_auth_key_hex_str->getString();
auto auth_key_hex_len = strlen(setting_auth_key_hex_str->getString());
if (!validateAuthorizationKeyHex(auth_key_hex)) {
MO_DBG_ERR("AuthorizationKey stored with format error. Disable Basic Auth");
auth_key_hex_len = 0;
}
auth_key_len = auth_key_hex_len / 2;
#if MO_MG_VERSION_614
cs_from_hex((char*)auth_key, auth_key_hex, auth_key_hex_len);
#elif MO_MG_USE_VERSION <= MO_MG_V713
mg_unhex(auth_key_hex, auth_key_hex_len, auth_key);
#else
for (size_t i = 0; i < auth_key_len; i++) {
mg_str_to_num(mg_str_n(auth_key_hex + 2*i, 2), 16, auth_key + i, sizeof(uint8_t));
}
#endif
auth_key[auth_key_len] = '\0'; //need null-termination as long as deprecated `const char *getAuthKey()` exists
}
}
/*
* determine new URL with updated WS credentials
*/
url.clear();
if (backend_url.empty()) {
MO_DBG_DEBUG("empty URL closes connection");
return;
}
url = backend_url;
if (url.back() != '/' && !cb_id.empty()) {
url.append("/");
}
url.append(cb_id);
}
int MOcppMongooseClient::printAuthKey(unsigned char *buf, size_t size) {
if (!buf || size < auth_key_len) {
MO_DBG_ERR("invalid argument");
return -1;
}
memcpy(buf, auth_key, auth_key_len);
return (int)auth_key_len;
}
void MOcppMongooseClient::setConnectionOpen(bool open) {
if (open) {
connection_established = true;
last_connection_established = mocpp_tick_ms();
} else {
connection_closing = true;
}
}
void MOcppMongooseClient::cleanConnection() {
connection_established = false;
connection_closing = false;
websocket = nullptr;
}
void MOcppMongooseClient::updateRcvTimer() {
last_recv = mocpp_tick_ms();
}
unsigned long MOcppMongooseClient::getLastRecv() {
return last_recv;
}
unsigned long MOcppMongooseClient::getLastConnected() {
return last_connection_established;
}
#if MO_ENABLE_V201
VariableContainer *MOcppMongooseClient::getVariableContainer() {
return websocketSettings.get();
}
#endif
#if MO_MG_USE_VERSION == MO_MG_V614
void ws_cb(struct mg_connection *nc, int ev, void *ev_data, void *user_data) {
MOcppMongooseClient *osock = nullptr;
if (user_data && nc->flags & MG_F_IS_WEBSOCKET && nc->flags & MO_MG_F_IS_MOcppMongooseClient) {
osock = reinterpret_cast<MOcppMongooseClient*>(user_data);
} else {
return;
}
switch (ev) {
case MG_EV_CONNECT: {
int status = *((int *) ev_data);
if (status != 0) {
MO_DBG_WARN("connection %s -- error %d", osock->getUrl(), status);
(void)0;
}
break;
}
case MG_EV_WEBSOCKET_HANDSHAKE_DONE: {
struct http_message *hm = (struct http_message *) ev_data;
if (hm->resp_code == 101) {
MO_DBG_INFO("connection %s -- connected!", osock->getUrl());
osock->setConnectionOpen(true);
} else {
MO_DBG_WARN("connection %s -- HTTP error %d", osock->getUrl(), hm->resp_code);
(void)0;
/* Connection will be closed after this. */
}
osock->updateRcvTimer();
break;
}
case MG_EV_POLL: {
/* Nothing to do here. OCPP engine has own loop-function */
break;
}
case MG_EV_WEBSOCKET_FRAME: {
struct websocket_message *wm = (struct websocket_message *) ev_data;
if (!osock->getReceiveTXTcallback()((const char *) wm->data, wm->size)) { //forward message to Context
MO_DBG_ERR("processing WS input failed");
(void)0;
}
osock->updateRcvTimer();
break;
}
case MG_EV_WEBSOCKET_CONTROL_FRAME: {
osock->updateRcvTimer();
break;
}
case MG_EV_CLOSE: {
MO_DBG_INFO("connection %s -- closed", osock->getUrl());
osock->cleanConnection();
break;
}
}
}
#else
#if MO_MG_USE_VERSION <= MO_MG_V708
void ws_cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
#else
void ws_cb(struct mg_connection *c, int ev, void *ev_data) {
void *fn_data = c->fn_data;
#endif
if (ev != 2) {
MO_DBG_VERBOSE("Cb fn with event: %d\n", ev);
(void)0;
}
MOcppMongooseClient *osock = reinterpret_cast<MOcppMongooseClient*>(fn_data);
if (!osock) {
if (ev == MG_EV_ERROR || ev == MG_EV_CLOSE) {
MO_DBG_INFO("connection %s", ev == MG_EV_CLOSE ? "closed" : "error");
(void)0;
} else {
MO_DBG_ERR("invalid state %d", ev);
(void)0;
}
return;
}
if (ev == MG_EV_ERROR) {
// On error, log error message
MG_ERROR(("%p %s", c->fd, (char *) ev_data));
} else if (ev == MG_EV_CONNECT) {
// If target URL is SSL/TLS, command client connection to use TLS
if (mg_url_is_ssl(osock->getUrl())) {
const char *ca_string = osock->getCaCert();
if (ca_string && *ca_string == '\0') { //check if certificate verification is disabled (cert string is empty)
//yes, disabled
ca_string = nullptr;
}
struct mg_tls_opts opts;
memset(&opts, 0, sizeof(struct mg_tls_opts));
#if MO_MG_USE_VERSION <= MO_MG_V708
opts.ca = ca_string;
opts.srvname = mg_url_host(osock->getUrl());
#else
opts.ca = mg_str(ca_string);
opts.name = mg_url_host(osock->getUrl());
#endif
mg_tls_init(c, &opts);
} else {
MO_DBG_WARN("Insecure connection (WS)");
}
} else if (ev == MG_EV_WS_OPEN) {
// WS connection established. Perform MQTT login
MO_DBG_INFO("connection %s -- connected!", osock->getUrl());
osock->setConnectionOpen(true);
osock->updateRcvTimer();
} else if (ev == MG_EV_WS_MSG) {
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
#if MO_MG_USE_VERSION <= MO_MG_V713
if (!osock->getReceiveTXTcallback()((const char*) wm->data.ptr, wm->data.len)) {
#else
if (!osock->getReceiveTXTcallback()((const char*) wm->data.buf, wm->data.len)) {
#endif
MO_DBG_WARN("processing input message failed");
}
osock->updateRcvTimer();
} else if (ev == MG_EV_WS_CTL) {
osock->updateRcvTimer();
}
if (ev == MG_EV_ERROR || ev == MG_EV_CLOSE) {
MO_DBG_INFO("connection %s -- %s", osock->getUrl(), ev == MG_EV_CLOSE ? "closed" : "error");
osock->cleanConnection();
}
}
#endif
bool MicroOcpp::validateAuthorizationKeyHex(const char *auth_key_hex) {
if (!auth_key_hex) {
return true; //nullptr (or "") means disable Auth
}
bool valid = true;
size_t i = 0;
while (i <= 2 * MO_AUTHKEY_LEN_MAX && auth_key_hex[i] != '\0') {
//check if character is in 0-9, a-f, or A-F
if ( (auth_key_hex[i] >= '0' && auth_key_hex[i] <= '9') ||
(auth_key_hex[i] >= 'a' && auth_key_hex[i] <= 'f') ||
(auth_key_hex[i] >= 'A' && auth_key_hex[i] <= 'F')) {
//yes, it is
i++;
} else {
//no, it isn't
valid = false;
break;
}
}
valid &= auth_key_hex[i] == '\0';
valid &= (i % 2) == 0;
if (!valid) {
MO_DBG_ERR("AuthorizationKey must be hex with at most 20 octets");
(void)0;
}
return valid;
}

View File

@@ -0,0 +1,141 @@
// matth-x/MicroOcppMongoose
// Copyright Matthias Akstaller 2019 - 2024
// GPL-3.0 License (see LICENSE)
#ifndef MO_MONGOOSECLIENT_H
#define MO_MONGOOSECLIENT_H
#if defined(ARDUINO) //fix for conflicting definitions of IPAddress on Arduino
#include <Arduino.h>
#include <IPAddress.h>
#endif
#include "mongoose.h"
#include <MicroOcpp/Core/Connection.h>
#include <MicroOcpp/Version.h>
#include <string>
#include <memory>
#ifndef MO_WSCONN_FN
#define MO_WSCONN_FN (MO_FILENAME_PREFIX "ws-conn.jsn")
#define MO_WSCONN_FN_V201 (MO_FILENAME_PREFIX "ws-conn-v201.jsn")
#endif
#define MO_AUTHKEY_LEN_MAX 63 // Basic Auth password length for both OCPP 1.6 and 2.0.1
namespace MicroOcpp {
class FilesystemAdapter;
class Configuration;
#if MO_ENABLE_V201
class Variable;
class VariableContainer;
class VariableContainerOwning;
#endif
class MOcppMongooseClient : public MicroOcpp::Connection {
private:
struct mg_mgr *mgr {nullptr};
struct mg_connection *websocket {nullptr};
std::string backend_url;
std::string cb_id;
std::string url; //url = backend_url + '/' + cb_id
unsigned char auth_key [MO_AUTHKEY_LEN_MAX + 1]; // Stores the raw Basic Auth password bytes. Appends a terminating '\0' for legacy accessors.
size_t auth_key_len;
const char *ca_cert; //zero-copy. The host system must ensure that this pointer remains valid during the lifetime of this class
std::shared_ptr<Configuration> setting_backend_url_str;
std::shared_ptr<Configuration> setting_cb_id_str;
std::shared_ptr<Configuration> setting_auth_key_hex_str;
unsigned long last_status_dbg_msg {0}, last_recv {0};
std::shared_ptr<Configuration> reconnect_interval_int; //minimum time between two connect trials in s
unsigned long last_reconnection_attempt {-1UL / 2UL};
std::shared_ptr<Configuration> stale_timeout_int; //inactivity period after which the connection will be closed
std::shared_ptr<Configuration> ws_ping_interval_int; //heartbeat intervall in s. 0 sets hb off
unsigned long last_hb {0};
#if MO_ENABLE_V201
std::unique_ptr<VariableContainerOwning> websocketSettings;
Variable *v201csmsUrlString = nullptr;
Variable *v201identityString = nullptr;
Variable *v201basicAuthPasswordString = nullptr;
#endif
bool connection_established {false};
unsigned long last_connection_established {-1UL / 2UL};
bool connection_closing {false};
ReceiveTXTcallback receiveTXTcallback = [] (const char *, size_t) {return false;};
ProtocolVersion protocolVersion;
void reconnect();
void maintainWsConn();
public:
MOcppMongooseClient(struct mg_mgr *mgr,
const char *backend_url_factory,
const char *charge_box_id_factory,
unsigned char *auth_key_factory, size_t auth_key_factory_len,
const char *ca_cert = nullptr, //zero-copy, the string must outlive this class and mg_mgr. Forwards this string to Mongoose as ssl_ca_cert (see https://github.com/cesanta/mongoose/blob/ab650ec5c99ceb52bb9dc59e8e8ec92a2724932b/mongoose.h#L4192)
std::shared_ptr<MicroOcpp::FilesystemAdapter> filesystem = nullptr,
ProtocolVersion protocolVersion = ProtocolVersion(1,6));
//DEPRECATED: will be removed in a future release
MOcppMongooseClient(struct mg_mgr *mgr,
const char *backend_url_factory = nullptr,
const char *charge_box_id_factory = nullptr,
const char *auth_key_factory = nullptr,
const char *ca_cert = nullptr, //zero-copy, the string must outlive this class and mg_mgr. Forwards this string to Mongoose as ssl_ca_cert (see https://github.com/cesanta/mongoose/blob/ab650ec5c99ceb52bb9dc59e8e8ec92a2724932b/mongoose.h#L4192)
std::shared_ptr<MicroOcpp::FilesystemAdapter> filesystem = nullptr,
ProtocolVersion protocolVersion = ProtocolVersion(1,6));
~MOcppMongooseClient();
void loop() override;
bool sendTXT(const char *msg, size_t length) override;
void setReceiveTXTcallback(MicroOcpp::ReceiveTXTcallback &receiveTXT) override {
this->receiveTXTcallback = receiveTXT;
}
MicroOcpp::ReceiveTXTcallback &getReceiveTXTcallback() {
return receiveTXTcallback;
}
//update WS configs. To apply the updates, call `reloadConfigs()` afterwards
void setBackendUrl(const char *backend_url);
void setChargeBoxId(const char *cb_id);
void setAuthKey(const char *auth_key); //DEPRECATED: will be removed in a future release
void setAuthKey(const unsigned char *auth_key, size_t len); //set the auth key in bytes-encoded format
void setCaCert(const char *ca_cert); //forwards this string to Mongoose as ssl_ca_cert (see https://github.com/cesanta/mongoose/blob/ab650ec5c99ceb52bb9dc59e8e8ec92a2724932b/mongoose.h#L4192)
void reloadConfigs();
const char *getBackendUrl() {return backend_url.c_str();}
const char *getChargeBoxId() {return cb_id.c_str();}
const char *getAuthKey() {return (const char*)auth_key;} //DEPRECATED: will be removed in a future release
int printAuthKey(unsigned char *buf, size_t size);
const char *getCaCert() {return ca_cert ? ca_cert : "";}
const char *getUrl() {return url.c_str();}
void setConnectionOpen(bool open);
bool isConnectionOpen() {return connection_established && !connection_closing;}
bool isConnected() {return isConnectionOpen();}
void cleanConnection();
void updateRcvTimer();
unsigned long getLastRecv(); //get time of last successful receive in millis
unsigned long getLastConnected(); //get time of last connection establish
#if MO_ENABLE_V201
//WS client creates and manages its own Variables. This getter function is a temporary solution, in future
//the WS client will be initialized with a Context reference for registering the Variables directly
VariableContainer *getVariableContainer();
#endif
};
}
#endif

View File

@@ -0,0 +1,129 @@
// matth-x/MicroOcppMongoose
// Copyright Matthias Akstaller 2019 - 2024
// GPL-3.0 License (see LICENSE)
#include "MicroOcppMongooseClient_c.h"
#include "MicroOcppMongooseClient.h"
#include <MicroOcpp/Core/FilesystemAdapter.h>
#include <MicroOcpp/Debug.h>
using namespace MicroOcpp;
OCPP_Connection *ocpp_makeConnection(struct mg_mgr *mgr,
const char *backend_url_default,
const char *charge_box_id_default,
const char *auth_key_default,
const char *CA_cert_default,
OCPP_FilesystemOpt fsopt) {
std::shared_ptr<MicroOcpp::FilesystemAdapter> filesystem;
#ifndef MO_DEACTIVATE_FLASH
filesystem = makeDefaultFilesystemAdapter(fsopt);
#endif
auto sock = new MOcppMongooseClient(mgr,
backend_url_default,
charge_box_id_default,
auth_key_default,
CA_cert_default,
filesystem);
return reinterpret_cast<OCPP_Connection*>(sock);;
}
void ocpp_deinitConnection(OCPP_Connection *sock) {
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
delete mgsock;
}
void ocpp_setBackendUrl(OCPP_Connection *sock, const char *backend_url) {
if (!sock) {
MO_DBG_ERR("invalid argument");
return;
}
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
mgsock->setBackendUrl(backend_url);
}
void ocpp_setChargeBoxId(OCPP_Connection *sock, const char *cb_id) {
if (!sock) {
MO_DBG_ERR("invalid argument");
return;
}
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
mgsock->setChargeBoxId(cb_id);
}
void ocpp_setAuthKey(OCPP_Connection *sock, const char *auth_key) {
if (!sock) {
MO_DBG_ERR("invalid argument");
return;
}
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
mgsock->setAuthKey(auth_key);
}
void ocpp_setCaCert(OCPP_Connection *sock, const char *ca_cert) {
if (!sock) {
MO_DBG_ERR("invalid argument");
return;
}
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
mgsock->setCaCert(ca_cert);
}
void ocpp_reloadConfigs(OCPP_Connection *sock) {
if (!sock) {
MO_DBG_ERR("invalid argument");
return;
}
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
mgsock->reloadConfigs();
}
const char *ocpp_getBackendUrl(OCPP_Connection *sock) {
if (!sock) {
MO_DBG_ERR("invalid argument");
return nullptr;
}
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
return mgsock->getBackendUrl();
}
const char *ocpp_getChargeBoxId(OCPP_Connection *sock) {
if (!sock) {
MO_DBG_ERR("invalid argument");
return nullptr;
}
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
return mgsock->getChargeBoxId();
}
const char *ocpp_getAuthKey(OCPP_Connection *sock) {
if (!sock) {
MO_DBG_ERR("invalid argument");
return nullptr;
}
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
return mgsock->getAuthKey();
}
const char *ocpp_getCaCert(OCPP_Connection *sock) {
if (!sock) {
MO_DBG_ERR("invalid argument");
return nullptr;
}
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
return mgsock->getCaCert();
}
bool ocpp_isConnectionOpen(OCPP_Connection *sock) {
if (!sock) {
MO_DBG_ERR("invalid argument");
return false;
}
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
return mgsock->isConnectionOpen();
}

View File

@@ -0,0 +1,51 @@
// matth-x/MicroOcppMongoose
// Copyright Matthias Akstaller 2019 - 2024
// GPL-3.0 License (see LICENSE)
#ifndef MO_MONGOOSECLIENT_C_H
#define MO_MONGOOSECLIENT_C_H
#if defined(__cplusplus) && defined(ARDUINO) //fix for conflicting defitions of IPAddress on Arduino
#include <Arduino.h>
#include <IPAddress.h>
#endif
#include "mongoose.h"
#include <MicroOcpp/Core/ConfigurationOptions.h>
#ifdef __cplusplus
extern "C" {
#endif
struct OCPP_Connection;
typedef struct OCPP_Connection OCPP_Connection;
OCPP_Connection *ocpp_makeConnection(struct mg_mgr *mgr,
const char *backend_url_default, //all cstrings can be NULL
const char *charge_box_id_default,
const char *auth_key_default,
const char *CA_cert_default,
struct OCPP_FilesystemOpt fsopt);
void ocpp_deinitConnection(OCPP_Connection *sock);
//update WS configs. To apply the updates, call `ocpp_reloadConfigs()` afterwards
void ocpp_setBackendUrl(OCPP_Connection *sock, const char *backend_url);
void ocpp_setChargeBoxId(OCPP_Connection *sock, const char *cb_id);
void ocpp_setAuthKey(OCPP_Connection *sock, const char *auth_key);
void ocpp_setCaCert(OCPP_Connection *sock, const char *ca_cert);
void ocpp_reloadConfigs(OCPP_Connection *sock);
const char *ocpp_getBackendUrl(OCPP_Connection *sock);
const char *ocpp_getChargeBoxId(OCPP_Connection *sock);
const char *ocpp_getAuthKey(OCPP_Connection *sock);
const char *ocpp_getCaCert(OCPP_Connection *sock);
bool ocpp_isConnectionOpen(OCPP_Connection *sock);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -14,7 +14,6 @@ board = rymcu-esp32-devkitc
framework = arduino framework = arduino
lib_deps = lib_deps =
matth-x/MicroOcpp@^1.2.0 matth-x/MicroOcpp@^1.2.0
matth-x/MicroOcppMongoose@^1.2.0
roboticsbrno/SmartLeds@^3.1.5 roboticsbrno/SmartLeds@^3.1.5
miguelbalboa/MFRC522@^1.4.12 miguelbalboa/MFRC522@^1.4.12
tzapu/WiFiManager@^2.0.17 tzapu/WiFiManager@^2.0.17

View File

@@ -4,12 +4,12 @@
// For production: ws://csms.helios.bh8.ga:8180/steve/websocket/CentralSystemService // For production: ws://csms.helios.bh8.ga:8180/steve/websocket/CentralSystemService
#define CFG_OCPP_BACKEND "wss://csms-server.uniiem.com/ocpp" #define CFG_OCPP_BACKEND "wss://csms-server.uniiem.com/ocpp"
// #define CFG_CP_IDENTIFIER "CQWU_HHB_0001" // #define CFG_CP_IDENTIFIER "CQWU_HHB_0001"
#define CFG_CP_IDENTIFIER "ICP_906A28" #define CFG_CP_IDENTIFIER ""
#define CFG_CB_SERIAL "REDAone_prototype00" #define CFG_CB_SERIAL "REDAone_prototype00"
#define CFG_CP_FW_VERSION "1.0.0" #define CFG_CP_FW_VERSION "1.0.0"
#define CFG_CP_MODAL "Helios DA One" #define CFG_CP_MODAL "Helios DA One"
#define CFG_CP_VENDOR "RayineElec" #define CFG_CP_VENDOR "RayineElec"
// OCPP Security Profile 1: Basic Auth password (username = chargePointIdentifier) // OCPP Security Profile 1: sent as Authorization: Basic base64(<chargePointIdentifier>:<password>)
// Set to nullptr to disable authentication // Set to nullptr to disable authentication
// #define CFG_OCPP_PASSWORD "my_password" // #define CFG_OCPP_PASSWORD "my_password"
#define CFG_OCPP_PASSWORD nullptr #define CFG_OCPP_PASSWORD nullptr

View File

@@ -145,8 +145,16 @@ void setup()
"%02X%02X%02X%02X%02X%02X", "%02X%02X%02X%02X%02X%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
// Auto-generate Charge Point Identifier based on MAC (e.g. HLCP_A1B2C3) if (strlen(CFG_CP_IDENTIFIER) > 0)
snprintf(cp_identifier, sizeof(cp_identifier), "HLCP_%s", cpSerial + 6); {
strncpy(cp_identifier, CFG_CP_IDENTIFIER, sizeof(cp_identifier) - 1);
cp_identifier[sizeof(cp_identifier) - 1] = '\0';
}
else
{
// Auto-generate Charge Point Identifier based on MAC (e.g. HLCP_A1B2C3)
snprintf(cp_identifier, sizeof(cp_identifier), "HLCP_%s", cpSerial + 6);
}
// reset LED // reset LED
leds[0] = Rgb{0, 0, 0}; leds[0] = Rgb{0, 0, 0};
@@ -155,6 +163,7 @@ void setup()
Serial.begin(115200); Serial.begin(115200);
delay(1000); delay(1000);
Serial.printf("\n\n%s(%s) made by %s\n", CFG_CP_MODAL, cpSerial, CFG_CP_VENDOR); Serial.printf("\n\n%s(%s) made by %s\n", CFG_CP_MODAL, cpSerial, CFG_CP_VENDOR);
Serial.printf("Charge Point Identifier: %s\n", cp_identifier);
Serial.println("Initializing firmware...\n"); Serial.println("Initializing firmware...\n");
// Initialize LED // Initialize LED
@@ -174,8 +183,10 @@ void setup()
Serial.printf("\n[OCPP] Loaded Backend URL: %s\n", b.c_str()); Serial.printf("\n[OCPP] Loaded Backend URL: %s\n", b.c_str());
Serial.printf("[OCPP] Loaded Password length: %d\n", p.length()); Serial.printf("[OCPP] Loaded Password length: %d\n", p.length());
strncpy(ocpp_backend, b.c_str(), sizeof(ocpp_backend)); strncpy(ocpp_backend, b.c_str(), sizeof(ocpp_backend) - 1);
strncpy(ocpp_password, p.c_str(), sizeof(ocpp_password)); ocpp_backend[sizeof(ocpp_backend) - 1] = '\0';
strncpy(ocpp_password, p.c_str(), sizeof(ocpp_password) - 1);
ocpp_password[sizeof(ocpp_password) - 1] = '\0';
WiFiManager wm; WiFiManager wm;
wm.setSaveConfigCallback(saveConfigCallback); wm.setSaveConfigCallback(saveConfigCallback);
@@ -184,7 +195,7 @@ void setup()
// Use autocomplete=off to prevent browsers from autofilling old URLs after a reset // Use autocomplete=off to prevent browsers from autofilling old URLs after a reset
WiFiManagerParameter custom_ocpp_backend("backend", "OCPP Backend URL", ocpp_backend, 128, "autocomplete=\"off\""); WiFiManagerParameter custom_ocpp_backend("backend", "OCPP Backend URL", ocpp_backend, 128, "autocomplete=\"off\"");
WiFiManagerParameter custom_ocpp_password("ocpp_password", "OCPP 连接密码 (Basic Auth, 没有请留空)", ocpp_password, 64, "autocomplete=\"off\" type=\"password\""); WiFiManagerParameter custom_ocpp_password("ocpp_password", "OCPP Basic Auth 密码 (Authorization 头, 没有请留空)", ocpp_password, 64, "autocomplete=\"off\" type=\"password\"");
wm.addParameter(&custom_ocpp_backend); wm.addParameter(&custom_ocpp_backend);
wm.addParameter(&custom_ocpp_password); wm.addParameter(&custom_ocpp_password);
@@ -334,8 +345,10 @@ void setup()
if (shouldSaveConfig) if (shouldSaveConfig)
{ {
strncpy(ocpp_backend, custom_ocpp_backend.getValue(), sizeof(ocpp_backend)); strncpy(ocpp_backend, custom_ocpp_backend.getValue(), sizeof(ocpp_backend) - 1);
strncpy(ocpp_password, custom_ocpp_password.getValue(), sizeof(ocpp_password)); ocpp_backend[sizeof(ocpp_backend) - 1] = '\0';
strncpy(ocpp_password, custom_ocpp_password.getValue(), sizeof(ocpp_password) - 1);
ocpp_password[sizeof(ocpp_password) - 1] = '\0';
preferences.putString("backend", ocpp_backend); preferences.putString("backend", ocpp_backend);
preferences.putString("ocpp_password", ocpp_password); preferences.putString("ocpp_password", ocpp_password);
@@ -354,8 +367,40 @@ void setup()
s_led_state = LED_WIFI_CONNECTED; s_led_state = LED_WIFI_CONNECTED;
mg_mgr_init(&mgr); mg_mgr_init(&mgr);
const char *final_ocpp_password = (strlen(ocpp_password) > 0) ? ocpp_password : nullptr; const char *basic_auth_password = (strlen(ocpp_password) > 0) ? ocpp_password : nullptr;
MicroOcpp::MOcppMongooseClient *client = new MicroOcpp::MOcppMongooseClient(&mgr, ocpp_backend, cp_identifier, final_ocpp_password, "", MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail), MicroOcpp::ProtocolVersion(1, 6)); unsigned char *basic_auth_password_bytes = nullptr;
size_t basic_auth_password_len = 0;
if (basic_auth_password)
{
basic_auth_password_bytes = reinterpret_cast<unsigned char *>(const_cast<char *>(basic_auth_password));
basic_auth_password_len = strlen(basic_auth_password);
}
MicroOcpp::MOcppMongooseClient *client = new MicroOcpp::MOcppMongooseClient(
&mgr,
ocpp_backend,
cp_identifier,
nullptr,
0,
"",
MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail),
MicroOcpp::ProtocolVersion(1, 6));
// Preferences and firmware config are the source of truth. Override any stale
// values cached in MicroOcpp's ws-conn storage before the first reconnect cycle.
client->setBackendUrl(ocpp_backend);
client->setChargeBoxId(cp_identifier);
if (basic_auth_password_bytes)
{
client->setAuthKey(basic_auth_password_bytes, basic_auth_password_len);
}
else
{
client->setAuthKey(reinterpret_cast<const unsigned char *>(""), 0);
}
client->reloadConfigs();
mocpp_initialize(*client, ChargerCredentials(CFG_CP_MODAL, CFG_CP_VENDOR, CFG_CP_FW_VERSION, cpSerial, nullptr, nullptr, CFG_CB_SERIAL, nullptr, nullptr), MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail)); mocpp_initialize(*client, ChargerCredentials(CFG_CP_MODAL, CFG_CP_VENDOR, CFG_CP_FW_VERSION, cpSerial, nullptr, nullptr, CFG_CB_SERIAL, nullptr, nullptr), MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail));
// For development/recovery: Set up BOOT button (GPIO 0) // For development/recovery: Set up BOOT button (GPIO 0)