From fad86c942ae14823cd4fdd5f624b8d443192c6bc Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 3 May 2023 12:04:53 +0800 Subject: [PATCH 01/41] evse: rename `ulong` -> `unsigned long` The type `ulong` does not exist under mingw2. Signed-off-by: Sean Cross --- src/evse.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/evse.h b/src/evse.h index e469d5c..35dbdea 100644 --- a/src/evse.h +++ b/src/evse.h @@ -18,7 +18,7 @@ class Evse { float simulate_power = 0; float limit_power = 11000.f; const float SIMULATE_ENERGY_DELTA_MS = SIMULATE_POWER_CONST / (3600.f * 1000.f); - ulong simulate_energy_track_time = 0; + unsigned long simulate_energy_track_time = 0; float simulate_energy = 0; std::string status; From 3698ef67130af4a275fba7228e873f4c757c4df6 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 3 May 2023 12:09:15 +0800 Subject: [PATCH 02/41] cmake: link winsock on Windows Windows requires winsock in order to run. Signed-off-by: Sean Cross --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index e2ea642..968fe92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,3 +39,7 @@ add_subdirectory(lib/ArduinoOcppMongoose) target_link_libraries(ao_simulator PUBLIC ArduinoOcppMongoose) target_link_libraries(ao_simulator PUBLIC ssl crypto) + +if(WIN32) + target_link_libraries(ao_simulator PUBLIC wsock32 ws2_32) +endif() From 341262af6b687d9396750bf6f3f3d22df1ce419c Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 3 May 2023 12:20:26 +0800 Subject: [PATCH 03/41] cmake: use static linking on Windows By statically linking, the resulting .exe can be used outside of the mingw environment under normal Windows. Signed-off-by: Sean Cross --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 968fe92..1277497 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,4 +42,5 @@ target_link_libraries(ao_simulator PUBLIC ssl crypto) if(WIN32) target_link_libraries(ao_simulator PUBLIC wsock32 ws2_32) + set(CMAKE_EXE_LINKER_FLAGS "-static-libgcc -static-libstdc++ -static") endif() From 1eb4c44212d69367df13b61f53ea2b6d94919f67 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 3 May 2023 12:22:49 +0800 Subject: [PATCH 04/41] README: add a note about MSYS2 Signed-off-by: Sean Cross --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f12056..11ddba8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Icon   ArduinoOcppSimulator -Tester / Demo App for the [ArduinoOcpp](https://github.com/matth-x/ArduinoOcpp) Client, running on native Ubuntu or the WSL. +Tester / Demo App for the [ArduinoOcpp](https://github.com/matth-x/ArduinoOcpp) Client, running on native Ubuntu, WSL, or MSYS2. ![Screenshot](https://github.com/agruenb/arduino-ocpp-dashboard/blob/master/docs/img/status_page.png) @@ -12,7 +12,7 @@ That means that the Simulator runs on your computer and connects to an OCPP serv ## Installation -On Windows, get the Windows Subsystem for Linux (WSL): [https://ubuntu.com/wsl](https://ubuntu.com/wsl) +On Windows, get the Windows Subsystem for Linux (WSL): [https://ubuntu.com/wsl](https://ubuntu.com/wsl) or [MSYS2](https://www.msys2.org/). Then follow the same steps like for Ubuntu. From 9042b3961e43f8579f220c81b5a917718eaecebd Mon Sep 17 00:00:00 2001 From: Blaz Dular Date: Wed, 21 Jun 2023 12:19:29 +0200 Subject: [PATCH 05/41] Dockerized the simulator --- .dockerignore | 2 ++ Dockerfile | 30 +++++++++++++++++++++++ README.md | 68 ++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4ee3e87 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +ao_store +build \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..300bcec --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# Use Ubuntu as the base image +FROM ubuntu:latest + +# Update package lists and install necessary dependencies +RUN apt-get update && apt-get install -y \ + git \ + cmake \ + libssl-dev \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Set the working directory inside the container +WORKDIR /ArduinoOcppSimulator + +# Copy your application files to the container's working directory +COPY . . + +RUN git clone --recurse-submodules https://github.com/matth-x/ArduinoOcppSimulator +RUN cd ArduinoOcppSimulator && mkdir build && mkdir ao_store +RUN cmake -S . -B ./build +RUN cmake --build ./build -j 16 + +# Grant execute permissions to the shell script +RUN chmod +x /ArduinoOcppSimulator/build/ao_simulator + +# Expose port 8000 +EXPOSE 8000 + +# Run the shell script inside the container +CMD ["./build/ao_simulator"] \ No newline at end of file diff --git a/README.md b/README.md index 9f12056..9a1bcdf 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,39 @@ # Icon   ArduinoOcppSimulator -Tester / Demo App for the [ArduinoOcpp](https://github.com/matth-x/ArduinoOcpp) Client, running on native Ubuntu or the WSL. +Tester / Demo App for the [ArduinoOcpp](https://github.com/matth-x/ArduinoOcpp) Client, running on native Ubuntu or the +WSL. ![Screenshot](https://github.com/agruenb/arduino-ocpp-dashboard/blob/master/docs/img/status_page.png) The Simulator has two purposes: -- As a development tool, it allows to run ArduinoOcpp directly on the host computer and simplifies the development (no flashing of the microcontroller required) -- As a demonstration tool, it allows backend operators to test and use ArduinoOcpp without the need to set up an actual microcontroller or to buy an actual charger with ArduinoOcpp. -That means that the Simulator runs on your computer and connects to an OCPP server using the same software like a microcontroller. It provides a Graphical User Interface to show the connection status and to trigger simulated charging sessions (and further simulated actions). +- As a development tool, it allows to run ArduinoOcpp directly on the host computer and simplifies the development (no + flashing of the microcontroller required) +- As a demonstration tool, it allows backend operators to test and use ArduinoOcpp without the need to set up an actual + microcontroller or to buy an actual charger with ArduinoOcpp. + +That means that the Simulator runs on your computer and connects to an OCPP server using the same software like a +microcontroller. It provides a Graphical User Interface to show the connection status and to trigger simulated charging +sessions (and further simulated actions). + +## Running on Docker + +The Simulator can be run on Docker. This is the easiest way to get it up and running. The Docker image is based on +Ubuntu 20.04 and contains all necessary dependencies. + +Firstly, build the image: + +```shell +docker build -t matthx/arduinoocppsimulator:latest . +``` + +Then run the image: + +```shell +docker run -p 8000:8000 matthx/arduinoocppsimulator:latest +``` + +The Simulator should be up and running now on [localhost:8000](http://localhost:8000). ## Installation @@ -16,7 +41,8 @@ On Windows, get the Windows Subsystem for Linux (WSL): [https://ubuntu.com/wsl]( Then follow the same steps like for Ubuntu. -On Ubuntu (other distros probably work as well, tested on Ubuntu 20.04 and 22.04), install cmake, OpenSSL and the C++ compiler: +On Ubuntu (other distros probably work as well, tested on Ubuntu 20.04 and 22.04), install cmake, OpenSSL and the C++ +compiler: ```shell sudo apt install cmake libssl-dev build-essential @@ -44,7 +70,9 @@ The installation is complete! To run the Simulator, type: ./build/ao_simulator ``` -This will open [localhost:8000](http://localhost:8000). You can access the Graphical User Interface by entering that address into a browser running on the same computer. Make sure that the firewall settings allow the Simulator to connect and to be reached. +This will open [localhost:8000](http://localhost:8000). You can access the Graphical User Interface by entering that +address into a browser running on the same computer. Make sure that the firewall settings allow the Simulator to connect +and to be reached. The Simulator should be up and running now! @@ -52,20 +80,26 @@ The Simulator should be up and running now! The webapp is registered as a git submodule in *webapp-src*. -Before you can build the webapp, you have to create a *.env.production* file in the *webapp-src* folder. If you just want to try out the build process, you can simply duplicate the *.env.development* file and rename it. +Before you can build the webapp, you have to create a *.env.production* file in the *webapp-src* folder. If you just +want to try out the build process, you can simply duplicate the *.env.development* file and rename it. -After that, to build it for deployment, all you have to do is run `./build-webapp/build_webapp.ps1` (Windows) from the root directory. -For this to work NodeJS, npm and git have to be installed on your machine. The called script automatically performs the following tasks: +After that, to build it for deployment, all you have to do is run `./build-webapp/build_webapp.ps1` (Windows) from the +root directory. +For this to work NodeJS, npm and git have to be installed on your machine. The called script automatically performs the +following tasks: - - pull the newest version of the the [arduino-ocpp-dashboard](https://github.com/agruenb/arduino-ocpp-dashboard) - - check if you have added a *.env.production* file - - install webapp dependencies - - build the webapp - - compress the webapp - - move the g-zipped bundle file into the public folder +- pull the newest version of the the [arduino-ocpp-dashboard](https://github.com/agruenb/arduino-ocpp-dashboard) +- check if you have added a *.env.production* file +- install webapp dependencies +- build the webapp +- compress the webapp +- move the g-zipped bundle file into the public folder -During the process there might be some warnings displayed. Als long as the script exits without an error everything worked fine. An up-to-date version of the webapp should be placed in the *public* folder. +During the process there might be some warnings displayed. Als long as the script exits without an error everything +worked fine. An up-to-date version of the webapp should be placed in the *public* folder. ## License -This project is licensed under the GPL as it uses the [Mongoose Embedded Networking Library](https://github.com/cesanta/mongoose). If you have a proprietary license of Mongoose, then the [MIT License](https://github.com/matth-x/ArduinoOcpp/blob/master/LICENSE) applies. +This project is licensed under the GPL as it uses +the [Mongoose Embedded Networking Library](https://github.com/cesanta/mongoose). If you have a proprietary license of +Mongoose, then the [MIT License](https://github.com/matth-x/ArduinoOcpp/blob/master/LICENSE) applies. From 7b2fd548f7e26c1cc1c56f38a01b72f7c90c20fa Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Sun, 20 Aug 2023 23:02:39 +0200 Subject: [PATCH 06/41] change naming for release --- .gitignore | 2 +- CMakeLists.txt | 24 ++++++++--------- README.md | 20 +++++++------- lib/ArduinoOcpp | 2 +- lib/ArduinoOcppMongoose | 2 +- src/evse.cpp | 58 ++++++++++++++++++++--------------------- src/evse.h | 10 +++---- src/main.cpp | 20 +++++++------- src/webserver.cpp | 42 ++++++++++++++--------------- src/webserver.h | 6 ++--- 10 files changed, 93 insertions(+), 93 deletions(-) diff --git a/.gitignore b/.gitignore index 05ea832..7987594 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ *.exe *.obj build_and_run.sh -ao_store +mo_store build diff --git a/CMakeLists.txt b/CMakeLists.txt index 98b09b8..992b76d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,41 +1,41 @@ -# matth-x/ArduinoOcpp +# matth-x/MicroOcpp # Copyright Matthias Akstaller 2019 - 2023 # MIT License cmake_minimum_required(VERSION 3.13) -set(AO_SM_SRC +set(MOCPP_SM_SRC src/evse.cpp src/main.cpp src/webserver.cpp lib/mongoose/mongoose.c ) -project(ArduinoOcppSimulator +project(MicroOcppSimulator VERSION 0.0.1) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED True) -add_executable(ao_simulator ${AO_SM_SRC}) +add_executable(mo_simulator ${MOCPP_SM_SRC}) -target_include_directories(ao_simulator PUBLIC +target_include_directories(mo_simulator PUBLIC "${PROJECT_DIR}/lib/ArduinoJson/src" "${PROJECT_DIR}/lib/mongoose" ) add_compile_definitions( MG_ENABLE_OPENSSL=1 - AO_NUMCONNECTORS=3 - AO_TRAFFIC_OUT - AO_DBG_LEVEL=AO_DL_INFO - AO_FILENAME_PREFIX="./ao_store/" + MOCPP_NUMCONNECTORS=3 + MOCPP_TRAFFIC_OUT + MOCPP_DBG_LEVEL=MOCPP_DL_INFO + MOCPP_FILENAME_PREFIX="./mo_store/" ) add_subdirectory(lib/ArduinoOcpp) -target_link_libraries(ao_simulator PUBLIC ArduinoOcpp) +target_link_libraries(mo_simulator PUBLIC MicroOcpp) add_subdirectory(lib/ArduinoOcppMongoose) -target_link_libraries(ao_simulator PUBLIC ArduinoOcppMongoose) +target_link_libraries(mo_simulator PUBLIC MicroOcppMongoose) -target_link_libraries(ao_simulator PUBLIC ssl crypto) +target_link_libraries(mo_simulator PUBLIC ssl crypto) diff --git a/README.md b/README.md index f96a892..52db5d3 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# Icon   ArduinoOcppSimulator +# Icon   MicroOcppSimulator -Tester / Demo App for the [ArduinoOcpp](https://github.com/matth-x/ArduinoOcpp) Client, running on native Ubuntu or the WSL. +Tester / Demo App for the [MicroOcpp](https://github.com/matth-x/MicroOcpp) Client, running on native Ubuntu or the WSL. ![Screenshot](https://github.com/agruenb/arduino-ocpp-dashboard/blob/master/docs/img/status_page.png) The Simulator has two purposes: -- As a development tool, it allows to run ArduinoOcpp directly on the host computer and simplifies the development (no flashing of the microcontroller required) -- As a demonstration tool, it allows backend operators to test and use ArduinoOcpp without the need to set up an actual microcontroller or to buy an actual charger with ArduinoOcpp. +- As a development tool, it allows to run MicroOcpp directly on the host computer and simplifies the development (no flashing of the microcontroller required) +- As a demonstration tool, it allows backend operators to test and use MicroOcpp without the need to set up an actual microcontroller or to buy an actual charger with MicroOcpp. That means that the Simulator runs on your computer and connects to an OCPP server using the same software like a microcontroller. It provides a Graphical User Interface to show the connection status and to trigger simulated charging sessions (and further simulated actions). @@ -25,23 +25,23 @@ sudo apt install cmake libssl-dev build-essential Navigate to the preferred installation directory or just to the home folder. Clone the Simulator and all submodules: ```shell -git clone --recurse-submodules https://github.com/matth-x/ArduinoOcppSimulator +git clone --recurse-submodules https://github.com/matth-x/MicroOcppSimulator ``` Navigate to the copy of the Simulator, prepare some necessary local folders and build: ```shell -cd ArduinoOcppSimulator +cd MicroOcppSimulator mkdir build -mkdir ao_store +mkdir mo_store cmake -S . -B ./build -cmake --build ./build -j 16 --target ao_simulator +cmake --build ./build -j 16 --target mo_simulator ``` The installation is complete! To run the Simulator, type: ```shell -./build/ao_simulator +./build/mo_simulator ``` This will open [localhost:8000](http://localhost:8000). You can access the Graphical User Interface by entering that address into a browser running on the same computer. Make sure that the firewall settings allow the Simulator to connect and to be reached. @@ -68,4 +68,4 @@ During the process there might be some warnings displayed. Als long as the scrip ## License -This project is licensed under the GPL as it uses the [Mongoose Embedded Networking Library](https://github.com/cesanta/mongoose). If you have a proprietary license of Mongoose, then the [MIT License](https://github.com/matth-x/ArduinoOcpp/blob/master/LICENSE) applies. +This project is licensed under the GPL as it uses the [Mongoose Embedded Networking Library](https://github.com/cesanta/mongoose). If you have a proprietary license of Mongoose, then the [MIT License](https://github.com/matth-x/MicroOcpp/blob/master/LICENSE) applies. diff --git a/lib/ArduinoOcpp b/lib/ArduinoOcpp index 894a207..9d63ce2 160000 --- a/lib/ArduinoOcpp +++ b/lib/ArduinoOcpp @@ -1 +1 @@ -Subproject commit 894a2071bd0eec96c26647e6927ccc851b4c8052 +Subproject commit 9d63ce28eab3c8c983e4aa8f9f088fdce6535ee0 diff --git a/lib/ArduinoOcppMongoose b/lib/ArduinoOcppMongoose index 85d6f27..de87719 160000 --- a/lib/ArduinoOcppMongoose +++ b/lib/ArduinoOcppMongoose @@ -1 +1 @@ -Subproject commit 85d6f27c17b87ae626e47599b21ac3bfdb319903 +Subproject commit de8771915b3f0f569535a6aaea011cddb74262ff diff --git a/src/evse.cpp b/src/evse.cpp index 68fbe2a..0c0411a 100644 --- a/src/evse.cpp +++ b/src/evse.cpp @@ -1,22 +1,22 @@ #include "evse.h" -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include -#define SIMULATOR_FN AO_FILENAME_PREFIX "simulator.jsn" +#define SIMULATOR_FN MOCPP_FILENAME_PREFIX "simulator.jsn" Evse::Evse(unsigned int connectorId) : connectorId{connectorId} { } -ArduinoOcpp::Connector *getConnector(unsigned int connectorId) { +MicroOcpp::Connector *getConnector(unsigned int connectorId) { if (!getOcppContext()) { - AO_DBG_ERR("unitialized"); + MOCPP_DBG_ERR("unitialized"); return nullptr; } return getOcppContext()->getModel().getConnector(connectorId); @@ -25,18 +25,18 @@ ArduinoOcpp::Connector *getConnector(unsigned int connectorId) { void Evse::setup() { auto connector = getConnector(connectorId); if (!connector) { - AO_DBG_ERR("invalid state"); + MOCPP_DBG_ERR("invalid state"); return; } char key [30] = {'\0'}; snprintf(key, 30, "evPlugged_cId_%u", connectorId); - trackEvPlugged = ArduinoOcpp::declareConfiguration(key, false, SIMULATOR_FN, false, false); + trackEvPlugged = MicroOcpp::declareConfiguration(key, false, SIMULATOR_FN, false, false); snprintf(key, 30, "evReady_cId_%u", connectorId); - trackEvReady = ArduinoOcpp::declareConfiguration(key, false, SIMULATOR_FN, false, false); + trackEvReady = MicroOcpp::declareConfiguration(key, false, SIMULATOR_FN, false, false); snprintf(key, 30, "evseReady_cId_%u", connectorId); - trackEvseReady = ArduinoOcpp::declareConfiguration(key, false, SIMULATOR_FN, false, false); + trackEvseReady = MicroOcpp::declareConfiguration(key, false, SIMULATOR_FN, false, false); connector->setConnectorPluggedInput([this] () -> bool { return *trackEvPlugged; //return if J1772 is in State B or C @@ -95,7 +95,7 @@ void Evse::setup() { }); setSmartChargingPowerOutput([this] (float limit) { - AO_DBG_DEBUG("set limit: %f", limit); + MOCPP_DBG_DEBUG("set limit: %f", limit); this->limit_power = limit; }, connectorId); } @@ -104,8 +104,8 @@ void Evse::loop() { if (auto connector = getConnector(connectorId)) { auto curStatus = connector->getStatus(); - if (status.compare(ArduinoOcpp::Ocpp16::cstrFromOcppEveState(curStatus))) { - status = ArduinoOcpp::Ocpp16::cstrFromOcppEveState(curStatus); + if (status.compare(MicroOcpp::Ocpp16::cstrFromOcppEveState(curStatus))) { + status = MicroOcpp::Ocpp16::cstrFromOcppEveState(curStatus); } } @@ -114,13 +114,13 @@ void Evse::loop() { if (simulate_isCharging) { if (simulate_power >= 1.f) { - simulate_energy += (float) (ao_tick_ms() - simulate_energy_track_time) * simulate_power * (0.001f / 3600.f); + simulate_energy += (float) (mocpp_tick_ms() - simulate_energy_track_time) * simulate_power * (0.001f / 3600.f); } simulate_power = SIMULATE_POWER_CONST; simulate_power = std::min(simulate_power, limit_power); - simulate_power += (((ao_tick_ms() / 5000) * 3483947) % 20000) * 0.001f - 10.f; - simulate_energy_track_time = ao_tick_ms(); + simulate_power += (((mocpp_tick_ms() / 5000) * 3483947) % 20000) * 0.001f - 10.f; + simulate_energy_track_time = mocpp_tick_ms(); } else { simulate_power = 0.f; } @@ -129,13 +129,13 @@ void Evse::loop() { void Evse::presentNfcTag(const char *uid_cstr) { if (!uid_cstr) { - AO_DBG_ERR("invalid argument"); + MOCPP_DBG_ERR("invalid argument"); return; } std::string uid = uid_cstr; auto connector = getConnector(connectorId); if (!connector) { - AO_DBG_ERR("invalid state"); + MOCPP_DBG_ERR("invalid state"); return; } @@ -143,7 +143,7 @@ void Evse::presentNfcTag(const char *uid_cstr) { if (!uid.compare(connector->getTransaction()->getIdTag())) { connector->endTransaction(uid.c_str()); } else { - AO_DBG_INFO("RFID card denied"); + MOCPP_DBG_INFO("RFID card denied"); } } else { connector->beginTransaction(uid.c_str()); @@ -153,7 +153,7 @@ void Evse::presentNfcTag(const char *uid_cstr) { void Evse::setEvPlugged(bool plugged) { if (!trackEvPlugged) return; *trackEvPlugged = plugged; - ArduinoOcpp::configuration_save(); + MicroOcpp::configuration_save(); } bool Evse::getEvPlugged() { @@ -164,7 +164,7 @@ bool Evse::getEvPlugged() { void Evse::setEvReady(bool ready) { if (!trackEvReady) return; *trackEvReady = ready; - ArduinoOcpp::configuration_save(); + MicroOcpp::configuration_save(); } bool Evse::getEvReady() { @@ -175,7 +175,7 @@ bool Evse::getEvReady() { void Evse::setEvseReady(bool ready) { if (!trackEvseReady) return; *trackEvseReady = ready; - ArduinoOcpp::configuration_save(); + MicroOcpp::configuration_save(); } bool Evse::getEvseReady() { @@ -186,7 +186,7 @@ bool Evse::getEvseReady() { const char *Evse::getSessionIdTag() { auto connector = getConnector(connectorId); if (!connector) { - AO_DBG_ERR("invalid state"); + MOCPP_DBG_ERR("invalid state"); return nullptr; } return connector->getTransaction() ? connector->getTransaction()->getIdTag() : nullptr; @@ -195,7 +195,7 @@ const char *Evse::getSessionIdTag() { int Evse::getTransactionId() { auto connector = getConnector(connectorId); if (!connector) { - AO_DBG_ERR("invalid state"); + MOCPP_DBG_ERR("invalid state"); return -1; } return connector->getTransaction() ? connector->getTransaction()->getTransactionId() : -1; @@ -204,7 +204,7 @@ int Evse::getTransactionId() { bool Evse::chargingPermitted() { auto connector = getConnector(connectorId); if (!connector) { - AO_DBG_ERR("invalid state"); + MOCPP_DBG_ERR("invalid state"); return false; } return connector->ocppPermitsCharge(); @@ -216,7 +216,7 @@ int Evse::getPower() { float Evse::getVoltage() { if (getPower() > 1.f) { - return 228.f + (((ao_tick_ms() / 5000) * 7484311) % 4000) * 0.001f; + return 228.f + (((mocpp_tick_ms() / 5000) * 7484311) % 4000) * 0.001f; } else { return 0.f; } diff --git a/src/evse.h b/src/evse.h index e469d5c..80934ae 100644 --- a/src/evse.h +++ b/src/evse.h @@ -3,16 +3,16 @@ #include #include -#include +#include class Evse { private: const unsigned int connectorId; - std::shared_ptr> trackEvPlugged; - std::shared_ptr> trackEvReady; - std::shared_ptr> trackEvseReady; + std::shared_ptr> trackEvPlugged; + std::shared_ptr> trackEvReady; + std::shared_ptr> trackEvseReady; const float SIMULATE_POWER_CONST = 11000.f; float simulate_power = 0; @@ -89,6 +89,6 @@ class Evse { }; -extern std::array connectors; +extern std::array connectors; #endif diff --git a/src/main.cpp b/src/main.cpp index a2721ea..3fc17a4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,19 +1,19 @@ #include #include "mongoose.h" -#include -#include +#include +#include #include "evse.h" #include "webserver.h" struct mg_mgr mgr; -ArduinoOcpp::AOcppMongooseClient *osock; +MicroOcpp::MOcppMongooseClient *osock; -#if AO_NUMCONNECTORS == 3 -std::array connectors {{1,2}}; +#if MOCPP_NUMCONNECTORS == 3 +std::array connectors {{1,2}}; #else -std::array connectors {{1}}; +std::array connectors {{1}}; #endif int main() { @@ -22,9 +22,9 @@ int main() { mg_http_listen(&mgr, "0.0.0.0:8000", http_serve, NULL); // Create listening connection - auto filesystem = ArduinoOcpp::makeDefaultFilesystemAdapter(ArduinoOcpp::FilesystemOpt::Use_Mount_FormatOnFail); + auto filesystem = MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail); - osock = new ArduinoOcpp::AOcppMongooseClient(&mgr, + osock = new MicroOcpp::MOcppMongooseClient(&mgr, "ws://echo.websocket.events", "charger-01", "", @@ -32,7 +32,7 @@ int main() { filesystem); server_initialize(osock); - ocpp_initialize(*osock, + mocpp_initialize(*osock, ChargerCredentials("Demo Charger", "My Company Ltd."), filesystem); @@ -42,7 +42,7 @@ int main() { for (;;) { // Block forever mg_mgr_poll(&mgr, 100); - ocpp_loop(); + mocpp_loop(); for (unsigned int i = 0; i < connectors.size(); i++) { connectors[i].loop(); } diff --git a/src/webserver.cpp b/src/webserver.cpp index e47fd32..f2ff8fa 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -1,23 +1,23 @@ #include "webserver.h" #include "evse.h" -#include +#include #include #include -#include -#include +#include +#include static const char *s_http_addr = "http://localhost:8000"; // HTTP port static const char *s_root_dir = "web_root"; -#define OCPP_CREDENTIALS_FN AO_FILENAME_PREFIX "ocpp-creds.jsn" +#define OCPP_CREDENTIALS_FN MOCPP_FILENAME_PREFIX "ws-conn.jsn" //cors_headers allow the browser to make requests from any domain, allowing all headers and all methods #define DEFAULT_HEADER "Content-Type: application/json\r\n" #define CORS_HEADERS "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Headers:Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers\r\nAccess-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,PUT\r\n" -ArduinoOcpp::AOcppMongooseClient *ao_sock = nullptr; +MicroOcpp::MOcppMongooseClient *ao_sock = nullptr; -void server_initialize(ArduinoOcpp::AOcppMongooseClient *osock) { +void server_initialize(MicroOcpp::MOcppMongooseClient *osock) { ao_sock = osock; } @@ -40,16 +40,16 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { const char *final_headers = DEFAULT_HEADER CORS_HEADERS; struct mg_str json = message_data->body; - AO_DBG_VERBOSE("%.*s", 20, message_data->uri.ptr); + MOCPP_DBG_VERBOSE("%.*s", 20, message_data->uri.ptr); Method method = Method::UNDEFINED; if (!mg_vcasecmp(&message_data->method, "POST")) { method = Method::POST; - AO_DBG_VERBOSE("POST"); + MOCPP_DBG_VERBOSE("POST"); } else if (!mg_vcasecmp(&message_data->method, "GET")) { method = Method::GET; - AO_DBG_VERBOSE("GET"); + MOCPP_DBG_VERBOSE("GET"); } unsigned int connectorId = 0; @@ -65,16 +65,16 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { } } - AO_DBG_VERBOSE("connectorId = %u", connectorId); + MOCPP_DBG_VERBOSE("connectorId = %u", connectorId); Evse *evse = nullptr; - if (connectorId >= 1 && connectorId < AO_NUMCONNECTORS) { + if (connectorId >= 1 && connectorId < MOCPP_NUMCONNECTORS) { evse = &connectors[connectorId-1]; } //start different api endpoints if(mg_http_match_uri(message_data, "/api/connectors")) { - AO_DBG_VERBOSE("query connectors"); + MOCPP_DBG_VERBOSE("query connectors"); StaticJsonDocument<256> doc; doc.add("1"); doc.add("2"); @@ -83,9 +83,9 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { mg_http_reply(c, 200, final_headers, serialized.c_str()); return; } else if(mg_http_match_uri(message_data, "/api/websocket")){ - AO_DBG_VERBOSE("query websocket"); - auto webSocketPingInterval = ArduinoOcpp::declareConfiguration("WebSocketPingInterval", (int) 10, OCPP_CREDENTIALS_FN); - auto reconnectInterval = ArduinoOcpp::declareConfiguration("AO_ReconnectInterval", (int) 30, OCPP_CREDENTIALS_FN); + MOCPP_DBG_VERBOSE("query websocket"); + auto webSocketPingInterval = MicroOcpp::declareConfiguration("WebSocketPingInterval", (int) 10, OCPP_CREDENTIALS_FN); + auto reconnectInterval = MicroOcpp::declareConfiguration("MOCPP_ReconnectInterval", (int) 30, OCPP_CREDENTIALS_FN); if (method == Method::POST) { if (auto val = mg_json_get_str(json, "$.backendUrl")) { @@ -110,10 +110,10 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { } } if (auto val = mg_json_get_str(json, "$.dnsUrl")) { - AO_DBG_WARN("dnsUrl not implemented"); + MOCPP_DBG_WARN("dnsUrl not implemented"); (void)val; } - ArduinoOcpp::configuration_save(); + MicroOcpp::configuration_save(); } StaticJsonDocument<256> doc; doc["backendUrl"] = ao_sock->getBackendUrl(); @@ -126,7 +126,7 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { mg_http_reply(c, 200, final_headers, serialized.c_str()); return; } else if(mg_http_match_uri(message_data, "/api/connector/*/evse")){ - AO_DBG_VERBOSE("query evse"); + MOCPP_DBG_VERBOSE("query evse"); if (!evse) { mg_http_reply(c, 404, final_headers, "connector does not exist"); return; @@ -153,7 +153,7 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { mg_http_reply(c, 200, final_headers, serialized.c_str()); return; } else if(mg_http_match_uri(message_data, "/api/connector/*/meter")){ - AO_DBG_VERBOSE("query meter"); + MOCPP_DBG_VERBOSE("query meter"); if (!evse) { mg_http_reply(c, 404, final_headers, "connector does not exist"); return; @@ -169,7 +169,7 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { return; } else if(mg_http_match_uri(message_data, "/api/connector/*/transaction")){ - AO_DBG_VERBOSE("query transaction"); + MOCPP_DBG_VERBOSE("query transaction"); if (!evse) { mg_http_reply(c, 404, final_headers, "connector does not exist"); return; @@ -188,7 +188,7 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { mg_http_reply(c, 200, final_headers, serialized.c_str()); return; } else if(mg_http_match_uri(message_data, "/api/connector/*/smartcharging")){ - AO_DBG_VERBOSE("query smartcharging"); + MOCPP_DBG_VERBOSE("query smartcharging"); if (!evse) { mg_http_reply(c, 404, final_headers, "connector does not exist"); return; diff --git a/src/webserver.h b/src/webserver.h index 629ec86..3007cc7 100644 --- a/src/webserver.h +++ b/src/webserver.h @@ -1,9 +1,9 @@ #include "mongoose.h" -namespace ArduinoOcpp { -class AOcppMongooseClient; +namespace MicroOcpp { +class MOcppMongooseClient; } -void server_initialize(ArduinoOcpp::AOcppMongooseClient *osock); +void server_initialize(MicroOcpp::MOcppMongooseClient *osock); void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data); From e6f43d951979276809a865cfcf1619be7cd0d410 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Sun, 20 Aug 2023 23:03:52 +0200 Subject: [PATCH 07/41] change naming for release II --- .gitmodules | 4 ++-- lib/{ArduinoOcpp => MicroOcpp} | 0 lib/{ArduinoOcppMongoose => MicroOcppMongoose} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename lib/{ArduinoOcpp => MicroOcpp} (100%) rename lib/{ArduinoOcppMongoose => MicroOcppMongoose} (100%) diff --git a/.gitmodules b/.gitmodules index 1df87c2..52a0af0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,10 +2,10 @@ path = lib/ArduinoJson url = https://github.com/bblanchon/ArduinoJson.git [submodule "lib/ArduinoOcpp"] - path = lib/ArduinoOcpp + path = lib/MicroOcpp url = https://github.com/matth-x/ArduinoOcpp [submodule "lib/ArduinoOcppMongoose"] - path = lib/ArduinoOcppMongoose + path = lib/MicroOcppMongoose url = https://github.com/matth-x/ArduinoOcppMongoose [submodule "lib/mongoose"] path = lib/mongoose diff --git a/lib/ArduinoOcpp b/lib/MicroOcpp similarity index 100% rename from lib/ArduinoOcpp rename to lib/MicroOcpp diff --git a/lib/ArduinoOcppMongoose b/lib/MicroOcppMongoose similarity index 100% rename from lib/ArduinoOcppMongoose rename to lib/MicroOcppMongoose From 0c349a7543b4be0f7d126756330b20c8a474884f Mon Sep 17 00:00:00 2001 From: Matthias Akstaller <63792403+matth-x@users.noreply.github.com> Date: Mon, 21 Aug 2023 01:12:01 +0200 Subject: [PATCH 08/41] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 52db5d3..ee365f3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Icon   MicroOcppSimulator +**Formerly ArduinoOcppSimulator**: *see the statement on the former ArduinoOcpp [project site](https://github.com/matth-x/MicroOcpp) for more details.* + Tester / Demo App for the [MicroOcpp](https://github.com/matth-x/MicroOcpp) Client, running on native Ubuntu or the WSL. ![Screenshot](https://github.com/agruenb/arduino-ocpp-dashboard/blob/master/docs/img/status_page.png) From 8b25288a62a2d6a275bbb847cf5a63b1b4ffb895 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Thu, 24 Aug 2023 16:56:14 +0200 Subject: [PATCH 09/41] update dependencies --- .gitmodules | 8 ++++---- CMakeLists.txt | 4 ++-- lib/MicroOcpp | 2 +- lib/MicroOcppMongoose | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitmodules b/.gitmodules index 52a0af0..8d9990d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,12 @@ [submodule "lib/ArduinoJson"] path = lib/ArduinoJson url = https://github.com/bblanchon/ArduinoJson.git -[submodule "lib/ArduinoOcpp"] +[submodule "lib/MicroOcpp"] path = lib/MicroOcpp - url = https://github.com/matth-x/ArduinoOcpp -[submodule "lib/ArduinoOcppMongoose"] + url = https://github.com/matth-x/MicroOcpp +[submodule "lib/MicroOcppMongoose"] path = lib/MicroOcppMongoose - url = https://github.com/matth-x/ArduinoOcppMongoose + url = https://github.com/matth-x/MicroOcppMongoose [submodule "lib/mongoose"] path = lib/mongoose url = https://github.com/cesanta/mongoose diff --git a/CMakeLists.txt b/CMakeLists.txt index 992b76d..d427465 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,10 +32,10 @@ add_compile_definitions( MOCPP_FILENAME_PREFIX="./mo_store/" ) -add_subdirectory(lib/ArduinoOcpp) +add_subdirectory(lib/MicroOcpp) target_link_libraries(mo_simulator PUBLIC MicroOcpp) -add_subdirectory(lib/ArduinoOcppMongoose) +add_subdirectory(lib/MicroOcppMongoose) target_link_libraries(mo_simulator PUBLIC MicroOcppMongoose) target_link_libraries(mo_simulator PUBLIC ssl crypto) diff --git a/lib/MicroOcpp b/lib/MicroOcpp index 9d63ce2..1eff6e5 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit 9d63ce28eab3c8c983e4aa8f9f088fdce6535ee0 +Subproject commit 1eff6e5dd9cc7b8676febe740136c8a7ecfdbe09 diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose index de87719..d761742 160000 --- a/lib/MicroOcppMongoose +++ b/lib/MicroOcppMongoose @@ -1 +1 @@ -Subproject commit de8771915b3f0f569535a6aaea011cddb74262ff +Subproject commit d76174250cbc20174f3d84e1d0fd998b076df824 From 3c823a81d781ab97547bd7b48c161549a36352b4 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Tue, 29 Aug 2023 20:36:46 +0200 Subject: [PATCH 10/41] draft WebAssembly integration --- CMakeLists.txt | 53 +++++- lib/MicroOcpp | 2 +- src/api.cpp | 147 +++++++++++++++ src/api.h | 18 ++ src/evse.h | 2 +- src/main.cpp | 90 +++++++-- src/net_wasm.cpp | 458 ++++++++++++++++++++++++++++++++++++++++++++++ src/net_wasm.h | 13 ++ src/webserver.cpp | 112 +----------- src/webserver.h | 4 + webapp-src | 2 +- 11 files changed, 769 insertions(+), 132 deletions(-) create mode 100644 src/api.cpp create mode 100644 src/api.h create mode 100644 src/net_wasm.cpp create mode 100644 src/net_wasm.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d427465..bd73ade 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,33 +4,46 @@ cmake_minimum_required(VERSION 3.13) -set(MOCPP_SM_SRC +set(MOCPP_SIM_SRC src/evse.cpp src/main.cpp + src/api.cpp +) + +set(MOCPP_SIM_MG_SRC src/webserver.cpp lib/mongoose/mongoose.c ) +set(MOCPP_SIM_WASM_SRC + src/net_wasm.cpp +) + project(MicroOcppSimulator VERSION 0.0.1) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED True) -add_executable(mo_simulator ${MOCPP_SM_SRC}) +add_executable(mo_simulator ${MOCPP_SIM_SRC} ${MOCPP_SIM_MG_SRC}) target_include_directories(mo_simulator PUBLIC - "${PROJECT_DIR}/lib/ArduinoJson/src" - "${PROJECT_DIR}/lib/mongoose" - ) + "${CMAKE_CURRENT_SOURCE_DIR}/lib/ArduinoJson/src" + "${CMAKE_CURRENT_SOURCE_DIR}/lib/mongoose" +) add_compile_definitions( - MG_ENABLE_OPENSSL=1 + MOCPP_PLATFORM=MOCPP_PLATFORM_UNIX MOCPP_NUMCONNECTORS=3 MOCPP_TRAFFIC_OUT MOCPP_DBG_LEVEL=MOCPP_DL_INFO MOCPP_FILENAME_PREFIX="./mo_store/" - ) +) + +target_compile_definitions(mo_simulator PUBLIC + MOCPP_NETLIB=MOCPP_NETLIB_MONGOOSE + MG_ENABLE_OPENSSL=1 +) add_subdirectory(lib/MicroOcpp) target_link_libraries(mo_simulator PUBLIC MicroOcpp) @@ -39,3 +52,29 @@ add_subdirectory(lib/MicroOcppMongoose) target_link_libraries(mo_simulator PUBLIC MicroOcppMongoose) target_link_libraries(mo_simulator PUBLIC ssl crypto) + +# experimental WebAssembly port +add_executable(mo_simulator_for_wasm ${MOCPP_SIM_SRC} ${MOCPP_SIM_WASM_SRC}) + +target_include_directories(mo_simulator_for_wasm PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/lib/ArduinoJson/src" + "${CMAKE_CURRENT_SOURCE_DIR}/lib/MicroOcppMongoose/src" +) + +target_compile_definitions(mo_simulator_for_wasm PUBLIC + MOCPP_NETLIB=MOCPP_NETLIB_WASM +) + +target_link_options(mo_simulator_for_wasm PUBLIC + -lwebsocket.js + -sENVIRONMENT=web + -sEXPORT_NAME=createModule + -sUSE_ES6_IMPORT_META=0 + -sEXPORTED_FUNCTIONS=_main,_mocpp_wasm_api_call + -sEXPORTED_RUNTIME_METHODS=ccall,cwrap + -O3 +) + +target_link_libraries(mo_simulator_for_wasm PUBLIC MicroOcpp) + +set(CMAKE_EXECUTABLE_SUFFIX ".mjs") diff --git a/lib/MicroOcpp b/lib/MicroOcpp index 1eff6e5..3b48781 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit 1eff6e5dd9cc7b8676febe740136c8a7ecfdbe09 +Subproject commit 3b487817d9f741ca3d1903fc2263463b7bc03625 diff --git a/src/api.cpp b/src/api.cpp new file mode 100644 index 0000000..05e65df --- /dev/null +++ b/src/api.cpp @@ -0,0 +1,147 @@ +#include "api.h" + +#include + +#include "evse.h" + +//simple matching function; takes * as a wildcard +bool str_match(const char *query, const char *pattern) { + size_t qi = 0, pi = 0; + + while (query[qi] && pattern[pi]) { + if (query[qi] == pattern[pi]) { + qi++; + pi++; + } else if (pattern[pi] == '*') { + while (pattern[pi] == '*') pi++; + while (query[qi] != pattern[pi]) qi++; + } else { + break; + } + } + + return !query[qi] && !pattern[pi]; +} + +int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *body, char *resp_body, size_t resp_body_size) { + + MOCPP_DBG_DEBUG("process %s, %s: %s", + endpoint, + method == MicroOcpp::Method::GET ? "GET" : + method == MicroOcpp::Method::POST ? "POST" : "error", + body); + + int status = 500; + StaticJsonDocument<512> response; + if (resp_body_size >= sizeof("{}")) { + sprintf(resp_body, "%s", "{}"); + } + + StaticJsonDocument<512> request; + if (*body) { + auto err = deserializeJson(request, body); + if (err) { + MOCPP_DBG_WARN("malformatted body: %s", err.c_str()); + return 400; + } + } + + unsigned int connectorId = 0; + + if (strlen(endpoint) >= 11) { + if (endpoint[11] == '1') { + connectorId = 1; + } else if (endpoint[11] == '2') { + connectorId = 2; + } + } + + MOCPP_DBG_VERBOSE("connectorId = %u", connectorId); + + Evse *evse = nullptr; + if (connectorId >= 1 && connectorId < MOCPP_NUMCONNECTORS) { + evse = &connectors[connectorId-1]; + } + + //start different api endpoints + if(str_match(endpoint, "/connectors")) { + MOCPP_DBG_VERBOSE("query connectors"); + response.add("1"); + response.add("2"); + status = 200; + } else if(str_match(endpoint, "/connector/*/evse")){ + MOCPP_DBG_VERBOSE("query evse"); + if (!evse) { + return 404; + } + + if (method == MicroOcpp::Method::POST) { + if (request.containsKey("evPlugged")) { + evse->setEvPlugged(request["evPlugged"]); + } + if (request.containsKey("evReady")) { + evse->setEvReady(request["evReady"]); + } + if (request.containsKey("evseReady")) { + evse->setEvseReady(request["evseReady"]); + } + } + + response["evPlugged"] = evse->getEvPlugged(); + response["evReady"] = evse->getEvReady(); + response["evseReady"] = evse->getEvseReady(); + response["chargePointStatus"] = evse->getOcppStatus(); + status = 200; + } else if(str_match(endpoint, "/connector/*/meter")){ + MOCPP_DBG_VERBOSE("query meter"); + if (!evse) { + return 404; + } + + response["energy"] = evse->getEnergy(); + response["power"] = evse->getPower(); + response["current"] = evse->getCurrent(); + response["voltage"] = evse->getVoltage(); + status = 200; + } else if(str_match(endpoint, "/connector/*/transaction")){ + MOCPP_DBG_VERBOSE("query transaction"); + if (!evse) { + return 404; + } + + if (method == MicroOcpp::Method::POST) { + if (request.containsKey("idTag")) { + evse->presentNfcTag(request["idTag"] | ""); + } + } + response["idTag"] = evse->getSessionIdTag(); + response["transactionId"] = evse->getTransactionId(); + response["authorizationStatus"] = ""; + status = 200; + } else if(str_match(endpoint, "/connector/*/smartcharging")){ + MOCPP_DBG_VERBOSE("query smartcharging"); + if (!evse) { + return 404; + } + + response["maxPower"] = evse->getSmartChargingMaxPower(); + response["maxCurrent"] = evse->getSmartChargingMaxCurrent(); + status = 200; + } + + if (response.overflowed()) { + return 500; + } + + std::string out; + serializeJson(response, out); + if (out.length() >= resp_body_size) { + return 500; + } + + if (!out.empty()) { + sprintf(resp_body, "%s", out.c_str()); + } + + return status; +} diff --git a/src/api.h b/src/api.h new file mode 100644 index 0000000..0b8e5a7 --- /dev/null +++ b/src/api.h @@ -0,0 +1,18 @@ +#ifndef MOCPP_SIM_API_H +#define MOCPP_SIM_API_H + +#include + +namespace MicroOcpp { + +enum class Method { + GET, + POST, + UNDEFINED +}; + +} + +int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *body, char *resp_body, size_t resp_body_size); + +#endif diff --git a/src/evse.h b/src/evse.h index 80934ae..dd292da 100644 --- a/src/evse.h +++ b/src/evse.h @@ -18,7 +18,7 @@ class Evse { float simulate_power = 0; float limit_power = 11000.f; const float SIMULATE_ENERGY_DELTA_MS = SIMULATE_POWER_CONST / (3600.f * 1000.f); - ulong simulate_energy_track_time = 0; + unsigned long simulate_energy_track_time = 0; float simulate_energy = 0; std::string status; diff --git a/src/main.cpp b/src/main.cpp index 3fc17a4..2ec7d6b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,21 +1,65 @@ #include -#include "mongoose.h" + #include +#include "evse.h" +#include "api.h" + +#if MOCPP_NUMCONNECTORS == 3 +std::array connectors {{1,2}}; +#else +std::array connectors {{1}}; +#endif + +//#if MOCPP_NETLIB == MOCPP_NETLIB_MONGOOSE +#if 0 +#include "mongoose.h" #include -#include "evse.h" #include "webserver.h" - struct mg_mgr mgr; MicroOcpp::MOcppMongooseClient *osock; -#if MOCPP_NUMCONNECTORS == 3 -std::array connectors {{1,2}}; +//#elif MOCPP_NETLIB == MOCPP_NETLIB_WASM +#elif 1 +#include + +#include + +#include "net_wasm.h" + +MicroOcpp::Connection *conn = nullptr; + #else -std::array connectors {{1}}; +#error Please ensure that build flag MOCPP_NETLIB is set as MOCPP_NETLIB_MONGOOSE or MOCPP_NETLIB_WASM #endif +/* + * Setup MicroOcpp and API + */ +void app_setup(MicroOcpp::Connection& connection, std::shared_ptr filesystem) { + mocpp_initialize(connection, + ChargerCredentials("Demo Charger", "My Company Ltd."), + filesystem); + + for (unsigned int i = 0; i < connectors.size(); i++) { + connectors[i].setup(); + } +} + +/* + * Execute one loop iteration + */ +void app_loop() { + mocpp_loop(); + for (unsigned int i = 0; i < connectors.size(); i++) { + connectors[i].loop(); + } +} + +//#if MOCPP_NETLIB == MOCPP_NETLIB_MONGOOSE +#if 0 + int main() { mg_log_set(MG_LL_INFO); mg_mgr_init(&mgr); @@ -32,23 +76,35 @@ int main() { filesystem); server_initialize(osock); - mocpp_initialize(*osock, - ChargerCredentials("Demo Charger", "My Company Ltd."), - filesystem); - - for (unsigned int i = 0; i < connectors.size(); i++) { - connectors[i].setup(); - } + app_setup(*osock, filesystem); for (;;) { // Block forever mg_mgr_poll(&mgr, 100); - mocpp_loop(); - for (unsigned int i = 0; i < connectors.size(); i++) { - connectors[i].loop(); - } + app_loop(); } delete osock; mg_mgr_free(&mgr); return 0; } + +//#elif MOCPP_NETLIB == MOCPP_NETLIB_WASM +#elif 1 + +int main() { + + printf("[WASM] start\n"); + + auto filesystem = MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Deactivate); + + conn = wasm_ocpp_connection_init("wss://echo.websocket.events/", nullptr, nullptr); + + app_setup(*conn, filesystem); + + const int LOOP_FREQ = 10; //called 10 times per second + const int BLOCK_INFINITELY = 0; //0 for non-blocking execution, 1 for blocking infinitely + emscripten_set_main_loop(app_loop, LOOP_FREQ, BLOCK_INFINITELY); + + printf("[WASM] setup complete\n"); +} +#endif diff --git a/src/net_wasm.cpp b/src/net_wasm.cpp new file mode 100644 index 0000000..b784b37 --- /dev/null +++ b/src/net_wasm.cpp @@ -0,0 +1,458 @@ +#include "net_wasm.h" + +#include +#include +#include + +#include +#include +#include +#include "base64.hpp" + +#include "api.h" + +#define DEBUG_MSG_INTERVAL 5000UL +#define WS_UNRESPONSIVE_THRESHOLD_MS 15000UL + +using namespace MicroOcpp; + +class WasmOcppConnection : public Connection { +private: + EMSCRIPTEN_WEBSOCKET_T websocket; + std::string backend_url; + std::string cb_id; + std::string url; //url = backend_url + '/' + cb_id + std::string auth_key; + std::string basic_auth64; + std::string ca_cert; + std::shared_ptr> setting_backend_url; + std::shared_ptr> setting_cb_id; + std::shared_ptr> setting_auth_key; +#if !MOCPP_CA_CERT_LOCAL + std::shared_ptr> setting_ca_cert; +#endif + unsigned long last_status_dbg_msg {0}, last_recv {0}; + std::shared_ptr> reconnect_interval; //minimum time between two connect trials in s + unsigned long last_reconnection_attempt {-1UL / 2UL}; + std::shared_ptr> stale_timeout; //inactivity period after which the connection will be closed + std::shared_ptr> ws_ping_interval; //heartbeat intervall in s. 0 sets hb off + unsigned long last_hb {0}; + bool connection_established {false}; + unsigned long last_connection_established {-1UL / 2UL}; + bool connection_closing {false}; + ReceiveTXTcallback receiveTXTcallback = [] (const char *, size_t) {return false;}; + + bool credentials_changed {true}; //set credentials to be reloaded + void reload_credentials() { + url.clear(); + basic_auth64.clear(); + + if (backend_url.empty()) { + MOCPP_DBG_DEBUG("empty URL closes connection"); + return; + } else { + url = backend_url; + + if (url.back() != '/' && !cb_id.empty()) { + url.append("/"); + } + + url.append(cb_id); + } + + if (!auth_key.empty()) { + std::string token = cb_id + ":" + auth_key; + + MOCPP_DBG_DEBUG("auth Token=%s", token.c_str()); + + unsigned int base64_length = encode_base64_length(token.length()); + std::vector base64 (base64_length + 1); + + // encode_base64() places a null terminator automatically, because the output is a string + base64_length = encode_base64((const unsigned char*) token.c_str(), token.length(), &base64[0]); + + MOCPP_DBG_DEBUG("auth64 len=%u, auth64 Token=%s", base64_length, &base64[0]); + + basic_auth64 = (const char*) &base64[0]; + } else { + MOCPP_DBG_DEBUG("no authentication"); + (void) 0; + } + } + + void maintainWsConn() { + if (mocpp_tick_ms() - last_status_dbg_msg >= DEBUG_MSG_INTERVAL) { + last_status_dbg_msg = mocpp_tick_ms(); + + //WS successfully connected? + if (!isConnectionOpen()) { + MOCPP_DBG_DEBUG("WS unconnected"); + } else if (mocpp_tick_ms() - last_recv >= (ws_ping_interval && *ws_ping_interval > 0 ? (*ws_ping_interval * 1000UL) : 0UL) + WS_UNRESPONSIVE_THRESHOLD_MS) { + //WS connected but unresponsive + MOCPP_DBG_DEBUG("WS unresponsive"); + } + } + + if (websocket && isConnectionOpen() && + stale_timeout && *stale_timeout > 0 && mocpp_tick_ms() - last_recv >= (*stale_timeout * 1000UL)) { + MOCPP_DBG_INFO("connection %s -- stale, reconnect", url.c_str()); + reconnect(); + return; + } + + if (websocket && isConnectionOpen() && + ws_ping_interval && *ws_ping_interval > 0 && mocpp_tick_ms() - last_hb >= (*ws_ping_interval * 1000UL)) { + last_hb = mocpp_tick_ms(); + MOCPP_DBG_VERBOSE("omit heartbeat"); + } + + if (websocket != NULL) { //connection pointer != NULL means that the socket is still open + return; + } + + if (credentials_changed) { + reload_credentials(); + credentials_changed = false; + } + + if (url.empty()) { + //cannot open OCPP connection: credentials missing + return; + } + + if (reconnect_interval && *reconnect_interval > 0 && mocpp_tick_ms() - last_reconnection_attempt < (*reconnect_interval * 1000UL)) { + return; + } + + MOCPP_DBG_DEBUG("(re-)connect to %s", url.c_str()); + + last_reconnection_attempt = mocpp_tick_ms(); + + EmscriptenWebSocketCreateAttributes attr; + emscripten_websocket_init_create_attributes(&attr); + + attr.url = url.c_str(); + attr.protocols = "ocpp1.6"; + attr.createOnMainThread = true; + + websocket = emscripten_websocket_new(&attr); + if (websocket < 0) { + MOCPP_DBG_ERR("emscripten_websocket_new: %i", websocket); + websocket = 0; + } + + if (websocket <= 0) { + return; + } + + auto ret_open = emscripten_websocket_set_onopen_callback( + websocket, + this, + [] (int eventType, const EmscriptenWebSocketOpenEvent *websocketEvent, void *userData) -> EM_BOOL { + WasmOcppConnection *conn = reinterpret_cast(userData); + MOCPP_DBG_DEBUG("on open eventType: %i", eventType); + MOCPP_DBG_INFO("connection %s -- connected!", conn->getUrl()); + conn->setConnectionOpen(true); + conn->updateRcvTimer(); + return true; + }); + if (ret_open < 0) { + MOCPP_DBG_ERR("emscripten_websocket_set_onopen_callback: %i", ret_open); + } + + auto ret_message = emscripten_websocket_set_onmessage_callback( + websocket, + this, + [] (int eventType, const EmscriptenWebSocketMessageEvent *websocketEvent, void *userData) -> EM_BOOL { + WasmOcppConnection *conn = reinterpret_cast(userData); + MOCPP_DBG_DEBUG("evenType: %i", eventType); + MOCPP_DBG_DEBUG("txt: %s", websocketEvent->data ? (const char*) websocketEvent->data : "undefined"); + if (!conn->getReceiveTXTcallback()((const char*) websocketEvent->data, websocketEvent->numBytes)) { + MOCPP_DBG_WARN("processing input message failed"); + } + conn->updateRcvTimer(); + return true; + }); + if (ret_message < 0) { + MOCPP_DBG_ERR("emscripten_websocket_set_onmessage_callback: %i", ret_message); + } + + auto ret_err = emscripten_websocket_set_onerror_callback( + websocket, + this, + [] (int eventType, const EmscriptenWebSocketErrorEvent *websocketEvent, void *userData) -> EM_BOOL { + WasmOcppConnection *conn = reinterpret_cast(userData); + MOCPP_DBG_DEBUG("on error eventType: %i", eventType); + MOCPP_DBG_INFO("connection %s -- %s", conn->getUrl(), "error"); + conn->cleanConnection(); + return true; + }); + if (ret_open < 0) { + MOCPP_DBG_ERR("emscripten_websocket_set_onopen_callback: %i", ret_open); + } + + auto ret_close = emscripten_websocket_set_onclose_callback( + websocket, + this, + [] (int eventType, const EmscriptenWebSocketCloseEvent *websocketEvent, void *userData) -> EM_BOOL { + WasmOcppConnection *conn = reinterpret_cast(userData); + MOCPP_DBG_DEBUG("on close eventType: %i", eventType); + MOCPP_DBG_INFO("connection %s -- %s", conn->getUrl(), "closed"); + conn->cleanConnection(); + return true; + }); + if (ret_open < 0) { + MOCPP_DBG_ERR("emscripten_websocket_set_onopen_callback: %i", ret_open); + } + } + +public: + WasmOcppConnection( + const char *backend_url_default, + const char *charge_box_id_default, + const char *auth_key_default) { + + setting_backend_url = declareConfiguration( + MOCPP_CONFIG_EXT_PREFIX "BackendUrl", backend_url_default, CONFIGURATION_VOLATILE, true, true, true); + setting_cb_id = declareConfiguration( + MOCPP_CONFIG_EXT_PREFIX "ChargeBoxId", charge_box_id_default, CONFIGURATION_VOLATILE, true, true, true); + setting_auth_key = declareConfiguration( + "AuthorizationKey", auth_key_default, CONFIGURATION_VOLATILE, true, true, true); + ws_ping_interval = declareConfiguration( + "WebSocketPingInterval", 5, CONFIGURATION_VOLATILE); + reconnect_interval = declareConfiguration( + MOCPP_CONFIG_EXT_PREFIX "ReconnectInterval", 10, CONFIGURATION_VOLATILE); + stale_timeout = declareConfiguration( + MOCPP_CONFIG_EXT_PREFIX "StaleTimeout", 300, CONFIGURATION_VOLATILE); + + configuration_save(); + + backend_url = setting_backend_url && *setting_backend_url ? *setting_backend_url : + (backend_url_default ? backend_url_default : ""); + cb_id = setting_cb_id && *setting_cb_id ? *setting_cb_id : + (charge_box_id_default ? charge_box_id_default : ""); + auth_key = setting_auth_key && *setting_auth_key ? *setting_auth_key : + (auth_key_default ? auth_key_default : ""); + + MOCPP_DBG_DEBUG("connection initialized"); + + maintainWsConn(); + } + + ~WasmOcppConnection() { + if (websocket) { + emscripten_websocket_delete(websocket); + websocket = NULL; + } + } + + void loop() override { + maintainWsConn(); + } + + bool sendTXT(std::string &out) override { + if (!websocket || !isConnectionOpen()) { + return false; + } + + if (auto ret = emscripten_websocket_send_utf8_text(websocket, out.c_str()) < 0) { + MOCPP_DBG_ERR("emscripten_websocket_send_utf8_text: %i", ret); + return false; + } + + return true; + } + + void setReceiveTXTcallback(MicroOcpp::ReceiveTXTcallback &receiveTXT) override { + this->receiveTXTcallback = receiveTXT; + } + + MicroOcpp::ReceiveTXTcallback &getReceiveTXTcallback() { + return receiveTXTcallback; + } + + void setBackendUrl(const char *backend_url_cstr) { + if (!backend_url_cstr) { + MOCPP_DBG_ERR("invalid argument"); + return; + } + backend_url = backend_url_cstr; + + if (setting_backend_url) { + *setting_backend_url = backend_url_cstr; + configuration_save(); + } + + credentials_changed = true; //reload composed credentials when reconnecting the next time + + reconnect(); + } + + void setChargeBoxId(const char *cb_id_cstr) { + if (!cb_id_cstr) { + MOCPP_DBG_ERR("invalid argument"); + return; + } + cb_id = cb_id_cstr; + + if (setting_cb_id) { + *setting_cb_id = cb_id_cstr; + configuration_save(); + } + + credentials_changed = true; //reload composed credentials when reconnecting the next time + + reconnect(); + } + + void setAuthKey(const char *auth_key_cstr) { + if (!auth_key_cstr) { + MOCPP_DBG_ERR("invalid argument"); + return; + } + auth_key = auth_key_cstr; + + if (setting_auth_key) { + *setting_auth_key = auth_key_cstr; + configuration_save(); + } + + credentials_changed = true; //reload composed credentials when reconnecting the next time + + reconnect(); + } + + void reconnect() { + if (!websocket) { + return; + } + auto ret = emscripten_websocket_close(websocket, 1000, "reconnect"); + if (ret < 0) { + MOCPP_DBG_ERR("emscripten_websocket_close: %i", ret); + } + setConnectionOpen(false); + } + + const char *getBackendUrl() {return backend_url.c_str();} + const char *getChargeBoxId() {return cb_id.c_str();} + const char *getAuthKey() {return auth_key.c_str();} + const char *getCaCert() {return ca_cert.c_str();} + + const char *getUrl() {return url.c_str();} + + void setConnectionOpen(bool open) { + if (open) { + connection_established = true; + last_connection_established = mocpp_tick_ms(); + } else { + connection_closing = true; + } + } + bool isConnectionOpen() {return connection_established && !connection_closing;} + void cleanConnection() { + connection_established = false; + connection_closing = false; + websocket = NULL; + } + + void updateRcvTimer() { + last_recv = mocpp_tick_ms(); + } + unsigned long getLastRecv() override { + return last_recv; + } + unsigned long getLastConnected() override { + return last_connection_established; + } + +}; + +WasmOcppConnection *wasm_ocpp_connection_instance = nullptr; + +MicroOcpp::Connection *wasm_ocpp_connection_init(const char *backend_url_default, const char *charge_box_id_default, const char *auth_key_default) { + if (!wasm_ocpp_connection_instance) { + wasm_ocpp_connection_instance = new WasmOcppConnection(backend_url_default, charge_box_id_default, auth_key_default); + } + + return wasm_ocpp_connection_instance; +} + +#define MOCPP_WASM_RESP_BUF_SIZE 1024 +char wasm_resp_buf [MOCPP_WASM_RESP_BUF_SIZE] = {'\0'}; + +//exported to WebAssembly +extern "C" char* mocpp_wasm_api_call(const char *endpoint, const char *method, const char *body) { + MOCPP_DBG_DEBUG("API call: %s, %s, %s", endpoint, method, body); + + auto method_parsed = Method::UNDEFINED; + if (!strcmp(method, "GET")) { + method_parsed = Method::GET; + } else if (!strcmp(method, "POST")) { + method_parsed = Method::POST; + } + + //handle endpoint /websocket + if (!strcmp(endpoint, "/websocket")) { + sprintf(wasm_resp_buf, "%s", "{}"); + if (!wasm_ocpp_connection_instance) { + MOCPP_DBG_ERR("no websocket instance"); + return nullptr; + } + StaticJsonDocument<512> request; + if (*body) { + auto err = deserializeJson(request, body); + if (err) { + MOCPP_DBG_WARN("malformatted body: %s", err.c_str()); + return nullptr; + } + } + + auto webSocketPingInterval = declareConfiguration("WebSocketPingInterval", 5, CONFIGURATION_VOLATILE); + auto reconnectInterval = declareConfiguration(MOCPP_CONFIG_EXT_PREFIX "ReconnectInterval", 10, CONFIGURATION_VOLATILE); + + if (method_parsed == Method::POST) { + if (request.containsKey("backendUrl")) { + wasm_ocpp_connection_instance->setBackendUrl(request["backendUrl"] | ""); + } + if (request.containsKey("chargeBoxId")) { + wasm_ocpp_connection_instance->setChargeBoxId(request["chargeBoxId"] | ""); + } + if (request.containsKey("authorizationKey")) { + wasm_ocpp_connection_instance->setAuthKey(request["authorizationKey"] | ""); + } + if (request.containsKey("pingInterval")) { + *webSocketPingInterval = request["pingInterval"] | 0; + } + if (request.containsKey("reconnectInterval")) { + *reconnectInterval = request["reconnectInterval"] | 0; + } + if (request.containsKey("dnsUrl")) { + MOCPP_DBG_WARN("dnsUrl not implemented"); + (void)0; + } + MicroOcpp::configuration_save(); + } + + StaticJsonDocument<512> response; + response["backendUrl"] = wasm_ocpp_connection_instance->getBackendUrl(); + response["chargeBoxId"] = wasm_ocpp_connection_instance->getChargeBoxId(); + response["authorizationKey"] = wasm_ocpp_connection_instance->getAuthKey(); + + response["pingInterval"] = webSocketPingInterval ? (int) *webSocketPingInterval : 0; + response["reconnectInterval"] = reconnectInterval ? (int) *reconnectInterval : 0; + serializeJson(response, wasm_resp_buf, MOCPP_WASM_RESP_BUF_SIZE); + return wasm_resp_buf; + } + + //all other endpoints + int status = mocpp_api_call(endpoint, method_parsed, body, wasm_resp_buf, MOCPP_WASM_RESP_BUF_SIZE); + + if (status == 200) { + //200: HTTP status code Success + MOCPP_DBG_DEBUG("API resp: %s", wasm_resp_buf); + return wasm_resp_buf; + } else { + MOCPP_DBG_DEBUG("API err: %i", status); + return nullptr; + } +} diff --git a/src/net_wasm.h b/src/net_wasm.h new file mode 100644 index 0000000..daf2319 --- /dev/null +++ b/src/net_wasm.h @@ -0,0 +1,13 @@ +#ifndef MOCPP_NET_WASM_H +#define MOCPP_NET_WASM_H + +//#if MOCPP_NETLIB == MOCPP_NETLIB_WASM +#if 1 + +#include + +MicroOcpp::Connection *wasm_ocpp_connection_init(const char *backend_url_default, const char *charge_box_id_default, const char *auth_key_default); + +#endif //MOCPP_NETLIB == MOCPP_NETLIB_WASM + +#endif diff --git a/src/webserver.cpp b/src/webserver.cpp index f2ff8fa..99134b7 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -1,3 +1,5 @@ +#if 0 + #include "webserver.h" #include "evse.h" #include @@ -52,37 +54,8 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { MOCPP_DBG_VERBOSE("GET"); } - unsigned int connectorId = 0; - - char parseConn [20] = {'\0'}; - snprintf(parseConn, 20, "%s", message_data->uri.ptr); - std::string parseConnString = parseConn; - if (parseConnString.length() >= 15) { - if (parseConn[15] == '1') { - connectorId = 1; - } else if (parseConn[15] == '2') { - connectorId = 2; - } - } - - MOCPP_DBG_VERBOSE("connectorId = %u", connectorId); - - Evse *evse = nullptr; - if (connectorId >= 1 && connectorId < MOCPP_NUMCONNECTORS) { - evse = &connectors[connectorId-1]; - } - //start different api endpoints - if(mg_http_match_uri(message_data, "/api/connectors")) { - MOCPP_DBG_VERBOSE("query connectors"); - StaticJsonDocument<256> doc; - doc.add("1"); - doc.add("2"); - std::string serialized; - serializeJson(doc, serialized); - mg_http_reply(c, 200, final_headers, serialized.c_str()); - return; - } else if(mg_http_match_uri(message_data, "/api/websocket")){ + if(mg_http_match_uri(message_data, "/api/websocket")){ MOCPP_DBG_VERBOSE("query websocket"); auto webSocketPingInterval = MicroOcpp::declareConfiguration("WebSocketPingInterval", (int) 10, OCPP_CREDENTIALS_FN); auto reconnectInterval = MicroOcpp::declareConfiguration("MOCPP_ReconnectInterval", (int) 30, OCPP_CREDENTIALS_FN); @@ -125,81 +98,8 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { serializeJson(doc, serialized); mg_http_reply(c, 200, final_headers, serialized.c_str()); return; - } else if(mg_http_match_uri(message_data, "/api/connector/*/evse")){ - MOCPP_DBG_VERBOSE("query evse"); - if (!evse) { - mg_http_reply(c, 404, final_headers, "connector does not exist"); - return; - } - if (method == Method::POST) { - bool val = false; - if (mg_json_get_bool(json, "$.evPlugged", &val)) { - evse->setEvPlugged(val); - } - if (mg_json_get_bool(json, "$.evReady", &val)) { - evse->setEvReady(val); - } - if (mg_json_get_bool(json, "$.evseReady", &val)) { - evse->setEvseReady(val); - } - } - StaticJsonDocument<256> doc; - doc["evPlugged"] = evse->getEvPlugged(); - doc["evReady"] = evse->getEvReady(); - doc["evseReady"] = evse->getEvseReady(); - doc["chargePointStatus"] = evse->getOcppStatus(); - std::string serialized; - serializeJson(doc, serialized); - mg_http_reply(c, 200, final_headers, serialized.c_str()); - return; - } else if(mg_http_match_uri(message_data, "/api/connector/*/meter")){ - MOCPP_DBG_VERBOSE("query meter"); - if (!evse) { - mg_http_reply(c, 404, final_headers, "connector does not exist"); - return; - } - StaticJsonDocument<256> doc; - doc["energy"] = evse->getEnergy(); - doc["power"] = evse->getPower(); - doc["current"] = evse->getCurrent(); - doc["voltage"] = evse->getVoltage(); - std::string serialized; - serializeJson(doc, serialized); - mg_http_reply(c, 200, final_headers, serialized.c_str()); - return; - - } else if(mg_http_match_uri(message_data, "/api/connector/*/transaction")){ - MOCPP_DBG_VERBOSE("query transaction"); - if (!evse) { - mg_http_reply(c, 404, final_headers, "connector does not exist"); - return; - } - if (method == Method::POST) { - if (auto val = mg_json_get_str(json, "$.idTag")) { - evse->presentNfcTag(val); - } - } - StaticJsonDocument<256> doc; - doc["idTag"] = evse->getSessionIdTag(); - doc["transactionId"] = evse->getTransactionId(); - doc["authorizationStatus"] = ""; - std::string serialized; - serializeJson(doc, serialized); - mg_http_reply(c, 200, final_headers, serialized.c_str()); - return; - } else if(mg_http_match_uri(message_data, "/api/connector/*/smartcharging")){ - MOCPP_DBG_VERBOSE("query smartcharging"); - if (!evse) { - mg_http_reply(c, 404, final_headers, "connector does not exist"); - return; - } - StaticJsonDocument<256> doc; - doc["maxPower"] = evse->getSmartChargingMaxPower(); - doc["maxCurrent"] = evse->getSmartChargingMaxCurrent(); - std::string serialized; - serializeJson(doc, serialized); - mg_http_reply(c, 200, final_headers, serialized.c_str()); - return; + } else if(mg_http_match_uri(message_data, "/api/*")){ + } //if no specific path is given serve dashboard application file else if (mg_http_match_uri(message_data, "/")) { @@ -211,3 +111,5 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { } } } + +#endif diff --git a/src/webserver.h b/src/webserver.h index 3007cc7..cfd5914 100644 --- a/src/webserver.h +++ b/src/webserver.h @@ -1,3 +1,5 @@ +#if 0 + #include "mongoose.h" namespace MicroOcpp { @@ -7,3 +9,5 @@ class MOcppMongooseClient; void server_initialize(MicroOcpp::MOcppMongooseClient *osock); void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data); + +#endif diff --git a/webapp-src b/webapp-src index f7a9c94..75de77c 160000 --- a/webapp-src +++ b/webapp-src @@ -1 +1 @@ -Subproject commit f7a9c9428f54397259006deb3c99b5e9fa161e94 +Subproject commit 75de77c6312e4e89d44a293f0e8de2a7b26ab983 From b0c18610acf7a917078a64507f90c312778a3c39 Mon Sep 17 00:00:00 2001 From: Matthias Akstaller <63792403+matth-x@users.noreply.github.com> Date: Tue, 29 Aug 2023 23:15:34 +0200 Subject: [PATCH 11/41] add WASM preview --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee365f3..4c98e82 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ **Formerly ArduinoOcppSimulator**: *see the statement on the former ArduinoOcpp [project site](https://github.com/matth-x/MicroOcpp) for more details.* -Tester / Demo App for the [MicroOcpp](https://github.com/matth-x/MicroOcpp) Client, running on native Ubuntu or the WSL. +Tester / Demo App for the [MicroOcpp](https://github.com/matth-x/MicroOcpp) Client, running on native Ubuntu or the WSL. **New** (August 2023): the Simulator has been ported to WebAssembly and runs natively in the browser too: [Try it](https://demo.micro-ocpp.com/) -![Screenshot](https://github.com/agruenb/arduino-ocpp-dashboard/blob/master/docs/img/status_page.png) +[![Screenshot](https://github.com/agruenb/arduino-ocpp-dashboard/blob/master/docs/img/status_page.png)](https://demo.micro-ocpp.com/) The Simulator has two purposes: - As a development tool, it allows to run MicroOcpp directly on the host computer and simplifies the development (no flashing of the microcontroller required) From aeb8e389743965857395413e2c2945b9ca7676ec Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Sat, 9 Sep 2023 16:49:41 +0200 Subject: [PATCH 12/41] restore mongoose integration and build system --- CMakeLists.txt | 34 ++++++++-------- README.md | 23 +++++++++++ build-webapp/install_webassembly.sh | 14 +++++++ lib/MicroOcpp | 2 +- src/api.cpp | 2 + src/main.cpp | 20 +++++----- src/{webserver.cpp => net_mongoose.cpp} | 53 ++++++++++++++----------- src/{webserver.h => net_mongoose.h} | 7 +++- src/net_wasm.h | 3 +- webapp-src | 2 +- 10 files changed, 105 insertions(+), 55 deletions(-) create mode 100644 build-webapp/install_webassembly.sh rename src/{webserver.cpp => net_mongoose.cpp} (74%) rename src/{webserver.h => net_mongoose.h} (60%) diff --git a/CMakeLists.txt b/CMakeLists.txt index bd73ade..b282f78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ set(MOCPP_SIM_SRC ) set(MOCPP_SIM_MG_SRC - src/webserver.cpp + src/net_mongoose.cpp lib/mongoose/mongoose.c ) @@ -25,21 +25,21 @@ project(MicroOcppSimulator set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED True) -add_executable(mo_simulator ${MOCPP_SIM_SRC} ${MOCPP_SIM_MG_SRC}) - -target_include_directories(mo_simulator PUBLIC - "${CMAKE_CURRENT_SOURCE_DIR}/lib/ArduinoJson/src" - "${CMAKE_CURRENT_SOURCE_DIR}/lib/mongoose" -) - add_compile_definitions( MOCPP_PLATFORM=MOCPP_PLATFORM_UNIX MOCPP_NUMCONNECTORS=3 MOCPP_TRAFFIC_OUT - MOCPP_DBG_LEVEL=MOCPP_DL_INFO + MOCPP_DBG_LEVEL=MOCPP_DL_VERBOSE MOCPP_FILENAME_PREFIX="./mo_store/" ) +add_executable(mo_simulator ${MOCPP_SIM_SRC} ${MOCPP_SIM_MG_SRC}) + +target_include_directories(mo_simulator PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/lib/ArduinoJson/src" + "${CMAKE_CURRENT_SOURCE_DIR}/lib/mongoose" +) + target_compile_definitions(mo_simulator PUBLIC MOCPP_NETLIB=MOCPP_NETLIB_MONGOOSE MG_ENABLE_OPENSSL=1 @@ -54,27 +54,29 @@ target_link_libraries(mo_simulator PUBLIC MicroOcppMongoose) target_link_libraries(mo_simulator PUBLIC ssl crypto) # experimental WebAssembly port -add_executable(mo_simulator_for_wasm ${MOCPP_SIM_SRC} ${MOCPP_SIM_WASM_SRC}) +add_executable(mo_simulator_wasm ${MOCPP_SIM_SRC} ${MOCPP_SIM_WASM_SRC}) -target_include_directories(mo_simulator_for_wasm PUBLIC +target_include_directories(mo_simulator_wasm PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/lib/ArduinoJson/src" "${CMAKE_CURRENT_SOURCE_DIR}/lib/MicroOcppMongoose/src" ) -target_compile_definitions(mo_simulator_for_wasm PUBLIC +target_compile_definitions(mo_simulator_wasm PUBLIC MOCPP_NETLIB=MOCPP_NETLIB_WASM ) -target_link_options(mo_simulator_for_wasm PUBLIC +target_link_options(mo_simulator_wasm PUBLIC -lwebsocket.js -sENVIRONMENT=web -sEXPORT_NAME=createModule -sUSE_ES6_IMPORT_META=0 -sEXPORTED_FUNCTIONS=_main,_mocpp_wasm_api_call -sEXPORTED_RUNTIME_METHODS=ccall,cwrap - -O3 + -Os ) -target_link_libraries(mo_simulator_for_wasm PUBLIC MicroOcpp) +target_link_libraries(mo_simulator_wasm PUBLIC MicroOcpp) -set(CMAKE_EXECUTABLE_SUFFIX ".mjs") +if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") + set(CMAKE_EXECUTABLE_SUFFIX ".mjs") +endif() diff --git a/README.md b/README.md index ee365f3..df48b84 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,29 @@ For this to work NodeJS, npm and git have to be installed on your machine. The c During the process there might be some warnings displayed. Als long as the script exits without an error everything worked fine. An up-to-date version of the webapp should be placed in the *public* folder. +## Porting to WebAssembly (Developers) + +If you want to run the Simulator in the browser instead of a Linux host, you can port the code for WebAssembly. + +Make sure that emscripten is installed and on the path (see [https://emscripten.org/docs/getting_started/downloads.html#installation-instructions-using-the-emsdk-recommended](https://emscripten.org/docs/getting_started/downloads.html#installation-instructions-using-the-emsdk-recommended)). + +Then, create the CMake build files with the corresponding emscripten tool and change the target: + +```shell +emcmake cmake -S . -B ./build +cmake --build ./build -j 16 --target mo_simulator_wasm +``` + +The compiler toolchain should emit the WebAssembly binary and a JavaScript wrapper into the build folder. They need to be built into the preact webapp. Instead of making XHR requests to the server, the webapp will call the API of the WebAssembly JS wrapper then. The `install_webassembly.sh` script patches the webapp sources with the WASM binary and JS wrapper: + +```shell +./build-webapp/install_webassembly.sh +``` + +Now, the GUI can be developed or built as described in the [webapp repository](https://github.com/agruenb/arduino-ocpp-dashboard). + +After building the GUI, the emited files contain the full Simulator functionality. To run the Simualtor, start an HTTP file server in the dist folder and access it with your browser. + ## License This project is licensed under the GPL as it uses the [Mongoose Embedded Networking Library](https://github.com/cesanta/mongoose). If you have a proprietary license of Mongoose, then the [MIT License](https://github.com/matth-x/MicroOcpp/blob/master/LICENSE) applies. diff --git a/build-webapp/install_webassembly.sh b/build-webapp/install_webassembly.sh new file mode 100644 index 0000000..4cbcc4d --- /dev/null +++ b/build-webapp/install_webassembly.sh @@ -0,0 +1,14 @@ +# execute after building cmake Simulator WebAssembly target: +# move created WebAssembly files into webapp folder and patch webapp + +echo "installing WebAssembly files" + +cp ./build/mo_simulator_wasm.mjs ./webapp-src/src/ +cp ./build/mo_simulator_wasm.wasm ./webapp-src/public/ + +if [ -e ./webapp-src/src/DataService_wasm.js.template ] +then + echo "patch DataService" + mv ./webapp-src/src/DataService.js ./webapp-src/src/DataService_http.js.template + mv ./webapp-src/src/DataService_wasm.js.template ./webapp-src/src/DataService.js +fi diff --git a/lib/MicroOcpp b/lib/MicroOcpp index 3b48781..76a2dfe 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit 3b487817d9f741ca3d1903fc2263463b7bc03625 +Subproject commit 76a2dfe2703a07a279b0253d658c2e9f8f66f6a1 diff --git a/src/api.cpp b/src/api.cpp index 05e65df..22f65ad 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -127,6 +127,8 @@ int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *b response["maxPower"] = evse->getSmartChargingMaxPower(); response["maxCurrent"] = evse->getSmartChargingMaxCurrent(); status = 200; + } else { + return 404; } if (response.overflowed()) { diff --git a/src/main.cpp b/src/main.cpp index 2ec7d6b..d2d956a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,18 +10,20 @@ std::array connectors {{1,2}}; std::array connectors {{1}}; #endif -//#if MOCPP_NETLIB == MOCPP_NETLIB_MONGOOSE -#if 0 +#define MOCPP_NETLIB_MONGOOSE 1 +#define MOCPP_NETLIB_WASM 2 + + +#if MOCPP_NETLIB == MOCPP_NETLIB_MONGOOSE #include "mongoose.h" #include -#include "webserver.h" +#include "net_mongoose.h" struct mg_mgr mgr; MicroOcpp::MOcppMongooseClient *osock; -//#elif MOCPP_NETLIB == MOCPP_NETLIB_WASM -#elif 1 +#elif MOCPP_NETLIB == MOCPP_NETLIB_WASM #include #include @@ -57,8 +59,7 @@ void app_loop() { } } -//#if MOCPP_NETLIB == MOCPP_NETLIB_MONGOOSE -#if 0 +#if MOCPP_NETLIB == MOCPP_NETLIB_MONGOOSE int main() { mg_log_set(MG_LL_INFO); @@ -88,8 +89,7 @@ int main() { return 0; } -//#elif MOCPP_NETLIB == MOCPP_NETLIB_WASM -#elif 1 +#elif MOCPP_NETLIB == MOCPP_NETLIB_WASM int main() { @@ -97,7 +97,7 @@ int main() { auto filesystem = MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Deactivate); - conn = wasm_ocpp_connection_init("wss://echo.websocket.events/", nullptr, nullptr); + conn = wasm_ocpp_connection_init(nullptr, nullptr, nullptr); app_setup(*conn, filesystem); diff --git a/src/webserver.cpp b/src/net_mongoose.cpp similarity index 74% rename from src/webserver.cpp rename to src/net_mongoose.cpp index 99134b7..2a13d05 100644 --- a/src/webserver.cpp +++ b/src/net_mongoose.cpp @@ -1,7 +1,6 @@ -#if 0 - -#include "webserver.h" +#include "net_mongoose.h" #include "evse.h" +#include "api.h" #include #include #include @@ -29,12 +28,6 @@ char* toStringPtr(std::string cppString){ return cstr; } -enum class Method { - GET, - POST, - UNDEFINED -}; - void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { if (ev == MG_EV_HTTP_MSG) { //struct mg_http_message *message_data = (struct mg_http_message *) ev_data; @@ -44,13 +37,13 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { MOCPP_DBG_VERBOSE("%.*s", 20, message_data->uri.ptr); - Method method = Method::UNDEFINED; + MicroOcpp::Method method = MicroOcpp::Method::UNDEFINED; if (!mg_vcasecmp(&message_data->method, "POST")) { - method = Method::POST; + method = MicroOcpp::Method::POST; MOCPP_DBG_VERBOSE("POST"); } else if (!mg_vcasecmp(&message_data->method, "GET")) { - method = Method::GET; + method = MicroOcpp::Method::GET; MOCPP_DBG_VERBOSE("GET"); } @@ -60,7 +53,7 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { auto webSocketPingInterval = MicroOcpp::declareConfiguration("WebSocketPingInterval", (int) 10, OCPP_CREDENTIALS_FN); auto reconnectInterval = MicroOcpp::declareConfiguration("MOCPP_ReconnectInterval", (int) 30, OCPP_CREDENTIALS_FN); - if (method == Method::POST) { + if (method == MicroOcpp::Method::POST) { if (auto val = mg_json_get_str(json, "$.backendUrl")) { ao_sock->setBackendUrl(val); } @@ -98,18 +91,30 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { serializeJson(doc, serialized); mg_http_reply(c, 200, final_headers, serialized.c_str()); return; - } else if(mg_http_match_uri(message_data, "/api/*")){ + } else if(strncmp(message_data->uri.ptr, "/api", strlen("api")) == 0) { + #define RESP_BUF_SIZE 1024 + char resp_buf [RESP_BUF_SIZE]; + + //replace endpoint-body separator by null + if (char *c = strchr((char*) message_data->uri.ptr, ' ')) { + *c = '\0'; + } + + int status = mocpp_api_call( + message_data->uri.ptr + strlen("/api"), + method, + message_data->body.ptr, + resp_buf, RESP_BUF_SIZE); + mg_http_reply(c, status, final_headers, resp_buf); + } + //if no specific path is given serve dashboard application file + else if (mg_http_match_uri(message_data, "/")) { + struct mg_http_serve_opts opts = { .root_dir = "./public" }; + opts.extra_headers = "Content-Type: text/html\r\nContent-Encoding: gzip\r\n"; + mg_http_serve_file(c, message_data, "public/bundle.html.gz", &opts); + } else { + mg_http_reply(c, 404, final_headers, "The required parameters are not given"); } - //if no specific path is given serve dashboard application file - else if (mg_http_match_uri(message_data, "/")) { - struct mg_http_serve_opts opts = { .root_dir = "./public" }; - opts.extra_headers = "Content-Type: text/html\r\nContent-Encoding: gzip\r\n"; - mg_http_serve_file(c, message_data, "public/bundle.html.gz", &opts); - } else { - mg_http_reply(c, 404, final_headers, "The required parameters are not given"); } - } } - -#endif diff --git a/src/webserver.h b/src/net_mongoose.h similarity index 60% rename from src/webserver.h rename to src/net_mongoose.h index cfd5914..6020a46 100644 --- a/src/webserver.h +++ b/src/net_mongoose.h @@ -1,4 +1,7 @@ -#if 0 +#ifndef MOCPP_NET_MONGOOSE_H +#define MOCPP_NET_MONGOOSE_H + +#if MOCPP_NETLIB == MOCPP_NETLIB_MONGOOSE #include "mongoose.h" @@ -10,4 +13,6 @@ void server_initialize(MicroOcpp::MOcppMongooseClient *osock); void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data); +#endif //MOCPP_NETLIB == MOCPP_NETLIB_MONGOOSE + #endif diff --git a/src/net_wasm.h b/src/net_wasm.h index daf2319..613f590 100644 --- a/src/net_wasm.h +++ b/src/net_wasm.h @@ -1,8 +1,7 @@ #ifndef MOCPP_NET_WASM_H #define MOCPP_NET_WASM_H -//#if MOCPP_NETLIB == MOCPP_NETLIB_WASM -#if 1 +#if MOCPP_NETLIB == MOCPP_NETLIB_WASM #include diff --git a/webapp-src b/webapp-src index 75de77c..cb3973b 160000 --- a/webapp-src +++ b/webapp-src @@ -1 +1 @@ -Subproject commit 75de77c6312e4e89d44a293f0e8de2a7b26ab983 +Subproject commit cb3973b17708419390bf911af9be255ecaf988c8 From 77ac74e191872b1af9f20fe616e6f54d13d168cb Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Sat, 9 Sep 2023 17:11:09 +0200 Subject: [PATCH 13/41] revert debug level --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b282f78..31d9846 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ add_compile_definitions( MOCPP_PLATFORM=MOCPP_PLATFORM_UNIX MOCPP_NUMCONNECTORS=3 MOCPP_TRAFFIC_OUT - MOCPP_DBG_LEVEL=MOCPP_DL_VERBOSE + MOCPP_DBG_LEVEL=MOCPP_DL_INFO MOCPP_FILENAME_PREFIX="./mo_store/" ) From 007ee90482b5ea0fa472a0301705270cd0a32590 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Wed, 13 Sep 2023 13:57:06 +0200 Subject: [PATCH 14/41] set current to 0 if Smart Charging < 6A --- src/evse.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/evse.cpp b/src/evse.cpp index 0c0411a..9a6e1c7 100644 --- a/src/evse.cpp +++ b/src/evse.cpp @@ -112,6 +112,8 @@ void Evse::loop() { bool simulate_isCharging = ocppPermitsCharge(connectorId) && *trackEvPlugged && *trackEvReady && *trackEvseReady; + simulate_isCharging &= limit_power >= 720.f; //minimum charging current is 6A (720W for 120V grids) according to J1772 + if (simulate_isCharging) { if (simulate_power >= 1.f) { simulate_energy += (float) (mocpp_tick_ms() - simulate_energy_track_time) * simulate_power * (0.001f / 3600.f); From 8160be80f5350a6971684afb25d2451c868a1b00 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Thu, 21 Sep 2023 17:20:34 +0200 Subject: [PATCH 15/41] adopt configs update --- lib/MicroOcpp | 2 +- lib/MicroOcppMongoose | 2 +- src/evse.cpp | 41 ++++++++++++++++++++++------------------- src/evse.h | 11 +++++++---- src/net_mongoose.cpp | 14 ++++++-------- 5 files changed, 37 insertions(+), 33 deletions(-) diff --git a/lib/MicroOcpp b/lib/MicroOcpp index 76a2dfe..f2ef4a3 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit 76a2dfe2703a07a279b0253d658c2e9f8f66f6a1 +Subproject commit f2ef4a3fbfe9f36f844b70c0a5ba3e298b771592 diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose index d761742..642006f 160000 --- a/lib/MicroOcppMongoose +++ b/lib/MicroOcppMongoose @@ -1 +1 @@ -Subproject commit d76174250cbc20174f3d84e1d0fd998b076df824 +Subproject commit 642006f0629b715dce44a913a5c4b77f3f8953bb diff --git a/src/evse.cpp b/src/evse.cpp index 9a6e1c7..94597d0 100644 --- a/src/evse.cpp +++ b/src/evse.cpp @@ -32,22 +32,25 @@ void Evse::setup() { char key [30] = {'\0'}; snprintf(key, 30, "evPlugged_cId_%u", connectorId); - trackEvPlugged = MicroOcpp::declareConfiguration(key, false, SIMULATOR_FN, false, false); + trackEvPluggedKey = key; + trackEvPluggedBool = MicroOcpp::declareConfiguration(trackEvPluggedKey.c_str(), false, SIMULATOR_FN, false, false, false); snprintf(key, 30, "evReady_cId_%u", connectorId); - trackEvReady = MicroOcpp::declareConfiguration(key, false, SIMULATOR_FN, false, false); + trackEvReadyKey = key; + trackEvReadyBool = MicroOcpp::declareConfiguration(trackEvReadyKey.c_str(), false, SIMULATOR_FN, false, false, false); snprintf(key, 30, "evseReady_cId_%u", connectorId); - trackEvseReady = MicroOcpp::declareConfiguration(key, false, SIMULATOR_FN, false, false); + trackEvseReadyKey = key; + trackEvseReadyBool = MicroOcpp::declareConfiguration(trackEvseReadyKey.c_str(), false, SIMULATOR_FN, false, false, false); connector->setConnectorPluggedInput([this] () -> bool { - return *trackEvPlugged; //return if J1772 is in State B or C + return trackEvPluggedBool->getBool(); //return if J1772 is in State B or C }); connector->setEvReadyInput([this] () -> bool { - return *trackEvReady; //return if J1772 is in State C + return trackEvReadyBool->getBool(); //return if J1772 is in State C }); connector->setEvseReadyInput([this] () -> bool { - return *trackEvseReady; + return trackEvseReadyBool->getBool(); }); connector->addErrorCodeInput([this] () -> const char* { @@ -110,7 +113,7 @@ void Evse::loop() { } - bool simulate_isCharging = ocppPermitsCharge(connectorId) && *trackEvPlugged && *trackEvReady && *trackEvseReady; + bool simulate_isCharging = ocppPermitsCharge(connectorId) && trackEvPluggedBool->getBool() && trackEvReadyBool->getBool() && trackEvseReadyBool->getBool(); simulate_isCharging &= limit_power >= 720.f; //minimum charging current is 6A (720W for 120V grids) according to J1772 @@ -153,36 +156,36 @@ void Evse::presentNfcTag(const char *uid_cstr) { } void Evse::setEvPlugged(bool plugged) { - if (!trackEvPlugged) return; - *trackEvPlugged = plugged; + if (!trackEvPluggedBool) return; + trackEvPluggedBool->setBool(plugged); MicroOcpp::configuration_save(); } bool Evse::getEvPlugged() { - if (!trackEvPlugged) return false; - return *trackEvPlugged; + if (!trackEvPluggedBool) return false; + return trackEvPluggedBool->getBool(); } void Evse::setEvReady(bool ready) { - if (!trackEvReady) return; - *trackEvReady = ready; + if (!trackEvReadyBool) return; + trackEvReadyBool->setBool(ready); MicroOcpp::configuration_save(); } bool Evse::getEvReady() { - if (!trackEvReady) return false; - return *trackEvReady; + if (!trackEvReadyBool) return false; + return trackEvReadyBool->getBool(); } void Evse::setEvseReady(bool ready) { - if (!trackEvseReady) return; - *trackEvseReady = ready; + if (!trackEvseReadyBool) return; + trackEvseReadyBool->setBool(ready); MicroOcpp::configuration_save(); } bool Evse::getEvseReady() { - if (!trackEvseReady) return false; - return *trackEvseReady; + if (!trackEvseReadyBool) return false; + return trackEvseReadyBool->getBool(); } const char *Evse::getSessionIdTag() { diff --git a/src/evse.h b/src/evse.h index dd292da..08bcd25 100644 --- a/src/evse.h +++ b/src/evse.h @@ -10,9 +10,12 @@ class Evse { private: const unsigned int connectorId; - std::shared_ptr> trackEvPlugged; - std::shared_ptr> trackEvReady; - std::shared_ptr> trackEvseReady; + std::shared_ptr trackEvPluggedBool; + std::string trackEvPluggedKey; + std::shared_ptr trackEvReadyBool; + std::string trackEvReadyKey; + std::shared_ptr trackEvseReadyBool; + std::string trackEvseReadyKey; const float SIMULATE_POWER_CONST = 11000.f; float simulate_power = 0; @@ -48,7 +51,7 @@ class Evse { bool chargingPermitted(); bool isCharging() { - return chargingPermitted() && trackEvReady; + return chargingPermitted() && trackEvReadyBool->getBool(); } const char *getOcppStatus() { diff --git a/src/net_mongoose.cpp b/src/net_mongoose.cpp index 2a13d05..8b7bda2 100644 --- a/src/net_mongoose.cpp +++ b/src/net_mongoose.cpp @@ -10,8 +10,6 @@ static const char *s_http_addr = "http://localhost:8000"; // HTTP port static const char *s_root_dir = "web_root"; -#define OCPP_CREDENTIALS_FN MOCPP_FILENAME_PREFIX "ws-conn.jsn" - //cors_headers allow the browser to make requests from any domain, allowing all headers and all methods #define DEFAULT_HEADER "Content-Type: application/json\r\n" #define CORS_HEADERS "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Headers:Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers\r\nAccess-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,PUT\r\n" @@ -50,8 +48,8 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { //start different api endpoints if(mg_http_match_uri(message_data, "/api/websocket")){ MOCPP_DBG_VERBOSE("query websocket"); - auto webSocketPingInterval = MicroOcpp::declareConfiguration("WebSocketPingInterval", (int) 10, OCPP_CREDENTIALS_FN); - auto reconnectInterval = MicroOcpp::declareConfiguration("MOCPP_ReconnectInterval", (int) 30, OCPP_CREDENTIALS_FN); + auto webSocketPingIntervalInt = MicroOcpp::declareConfiguration("WebSocketPingInterval", 10, MOCPP_WSCONN_FN); + auto reconnectIntervalInt = MicroOcpp::declareConfiguration(MOCPP_CONFIG_EXT_PREFIX "ReconnectInterval", 30, MOCPP_WSCONN_FN); if (method == MicroOcpp::Method::POST) { if (auto val = mg_json_get_str(json, "$.backendUrl")) { @@ -66,13 +64,13 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { { auto val = mg_json_get_long(json, "$.pingInterval", -1); if (val > 0) { - *webSocketPingInterval = (int) val; + webSocketPingIntervalInt->setInt(val); } } { auto val = mg_json_get_long(json, "$.reconnectInterval", -1); if (val > 0) { - *reconnectInterval = (int) val; + reconnectIntervalInt->setInt(val); } } if (auto val = mg_json_get_str(json, "$.dnsUrl")) { @@ -85,8 +83,8 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { doc["backendUrl"] = ao_sock->getBackendUrl(); doc["chargeBoxId"] = ao_sock->getChargeBoxId(); doc["authorizationKey"] = ao_sock->getAuthKey(); - doc["pingInterval"] = (int) *webSocketPingInterval; - doc["reconnectInterval"] = (int) *reconnectInterval; + doc["pingInterval"] = webSocketPingIntervalInt->getInt(); + doc["reconnectInterval"] = reconnectIntervalInt->getInt(); std::string serialized; serializeJson(doc, serialized); mg_http_reply(c, 200, final_headers, serialized.c_str()); From 2d8ba421ee6cc99cf14fde53b202a77e1fab454e Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Sat, 23 Sep 2023 15:38:16 +0200 Subject: [PATCH 16/41] update dependencies --- lib/MicroOcpp | 2 +- lib/MicroOcppMongoose | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/MicroOcpp b/lib/MicroOcpp index f2ef4a3..0699d97 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit f2ef4a3fbfe9f36f844b70c0a5ba3e298b771592 +Subproject commit 0699d97f83472fb4677b753a07231e692d1a1a53 diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose index 642006f..e3bf1e0 160000 --- a/lib/MicroOcppMongoose +++ b/lib/MicroOcppMongoose @@ -1 +1 @@ -Subproject commit 642006f0629b715dce44a913a5c4b77f3f8953bb +Subproject commit e3bf1e0a0ca87ab333f5d6faf445e9eafdc83d65 From de5ee0509cda66fe53fa28366e18916523e88984 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:22:05 +0200 Subject: [PATCH 17/41] update dependencies --- lib/MicroOcpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/MicroOcpp b/lib/MicroOcpp index 0699d97..f993ff4 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit 0699d97f83472fb4677b753a07231e692d1a1a53 +Subproject commit f993ff4c3a37bd83075170e08a1ac04a4cd16c42 From f9364b0fb516fcfe0ddd98e77f78ccd08c6f9fd3 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:30:43 +0200 Subject: [PATCH 18/41] update dashboard webapp dependency --- public/bundle.html.gz | Bin 38996 -> 36613 bytes webapp-src | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/public/bundle.html.gz b/public/bundle.html.gz index 1011336e37a7cec67bce28513ee9d024970ccaaf..d419b54b2ec28252c7c8204f9fe51e6a6dfe9ea8 100644 GIT binary patch literal 36613 zcmYhCb8u!~@aALNwrwX98*gS}+qP}ndSly~I1^`L+qRS0e0R5gySMs2{i%NXoT^iG zt8V{uh@)XZ{`UiS;rs5s-rjI?{hV{{pU_ex$CXmpMo1fzZ8m?^>gk%i*?N9{E+*4F zoI$0UbZ1QYdA&UeLULQkPMb{@VQE8$4^`^Ep? zBSC&Fk2KS#Sq2AbF{pnY^MJwh_UStxL(qr~s)?t@%hdIc&sx_Fjy-IN2AfcSZ3%9_ zd*+&J{Z_q_<%xobQpXjW$+=<}bm@;v>mND#VPuVV1L)WJ(Sf-#{cmmGEql9%)x#JM7t49 z4O%ADl=WYnvB{~A`S_rlC{_)VYt75Ki*{J=4(smPt-m&h+7g(ZgPbKd7m{^PkFwKn zPEy^|=#TV&zK|hZQ@zY*Non%X7joc=rB$7aj~66v>(nxX7}M;QIS9biQ7?Be>C9_Atl#M|NA*hZKT5WDiRy(EZ{^PO8}i6p6EO%|!fDN`EF^W^F(U@=6aV)WnR zxZ`Q;nBYzxD?7xmY>Pvc%p*I1RW<-X$qMFLTNX*Vm}W#`hT}wB zXvqz4a)mn-AO39L#BR1Rt;Xp|f!QiPx^DIE10g6AY7LDsu?KKsa$-E11+6DD0+%`|BvshLPzJam7tH+e$HpGYVjPwDc>4%?{EajC9C`SRAW*9efT!D##s@DOJ z-0@vJ)zNW@4HKohl?6o-x!mp8`B^D((!(WD)T6}#OK-k{d%Sx>w>b@A&U&*te-Z4^nN z?({(EzIUh~|Oo zKi4R@XermFM9QIrb!)`lYZkne&4yzclSFoUX`&(&vdN3&J6Y#`?oW!-{vwE-X z-ie$q{)l99_V0Ik<_sU8P+ic4iD#CpQ*;-^c4I}h`T|M+;N=pebigkh z4_w+y3tAP+_-y|%k5DC-K0-X^Yd4PCez9nadj{7pjZBP8m5%ZPA7Akb7^ipD*g7 zYD_PY;2i^U-?0eXA9}(o$5hlZ!iew9UII62J*o7mO;syoi|oEI8bQS<4?Z~`Q`nd` z^?xo(Ov!{~?fiA0DrG^}jV?8M+C%!0My-Y5ld?&UYZP2S zUT1(UZ+cSqo&>#7Deo!WiL+4-V-i z(`eSh#ld3mXy>c~`k=ZZ20ZKniQ^@}ux7IiliG)8pPr-vw^21VM#R$2xnB#N(&i*c zMHC|0rDv%>`Pjz zqDZu0yD-NUrC8-VZCq+x@TjuqUVGY3yrT2kPQ0T7+i<+1|7wjuz~YD3d>fw_^?6o^~2F?1$`;^F86xqnLw z{CObj%z>1%QDD}RQ;mfq*|NcxFT)LFl9b=z;CaxhLOZ=+FW{<5C;Mm8g>9UvKcH&4 zKxDVB3U6FQaZiFCB!-hZ^$#iEl}}-shlcL>)OG+UKnWg^PQJ*{>4a7MwPZ>vQK|WG zz@VXCJ0TXQ%6>YLS)JCxo1$!W2TC>CYb-|-T2&FIkh!pI*w`2pi@~z=UK1cSWlO@* zdUfOB=h%#hkv^(!xPp%muARKj87;#;tpau0V3z`F48q~Ejlpw^H;lyw9^@Tng}P4&^a-|JDZm%(0(}Pr6dHnuqz@Dn3C2M<033{fY$pLE9D;|Uj}#OM z)W90F_l1BLm#%NAn>dLSP#UO zDu}LA?GcXPH5)VtOXgvi9qLQ#E46h;K^lo0N%Cdfh*BnUbAKPHvN#?b&^ zYMCR4eS*aN?Tvlc`8b6o=xnh{s(m4Wxqh)7m3HX9UnVK>t6V)6xU;D>a8n|%K|9t8 zIjrPrg(zg%Kx?ij8x++h={MVw1xP)r_Q38-x!^~MRw1YQq^z3dpQJs$sn(!W@eS4$ z)I-3O1P=4%mkb&5tcflce5+B>=4dNNEMC8u1sQyFK~+PfxWX*2@C*p! z%~lHvh&(Iuq>d*AE-0}9N}W!0DLev)L5#J5kr#tuxtYG;)1tZl3*e3jur=Q@P8^k2 zrAU(c86pr;G9@N{B5#LX5Hjz89Y+d2`KLHyntT+C@6T4BF`lek8=D(^Be-*Y!%F#T zeHuBpAag*I^Wk8^1$V`Iq3NXah@sLMt*YnGlg=<|0NEH!+GfaZe-vj`t?cy^3Vk&n zzy+CGT$!`yt_IaZ26KCv&;jGnS_@@h;C2{(TsU2{r=f^$u97nrj!0?4c;mNzDe zDD&kG>Lhzip_~30qAJ6Dq}KqzbRK7ck`0TBS~h3XPX7xb7|XO=>d_XIl%jWTqa#6^ z%yY>W2Z7dlu63u|BqN17A+Ty4ZLckyU`hoJ=p)w|W+R)OGYAWLd|~-l0y_-1INRFi zc+nO`8CO$+ugHav^uPt5ruVo;b7ZZF&;SV16;^CU>%WLv;P%q3<3I80=|`=ELCANd zL z0DRkfAdA2sj=$MYsfYu|+pBb|Hw%qR%UAK`%c_TWa~zMFP$@@nC^~t3D9&FxRN(ao zna~=RlES(~vxc$4#pcY>E%K#~f}bzg=A^~D5?NR&=-!$%11d{FI8BN2Me;O>M9s6o z2E{D9EDp-tA6m0q(M;#|@%4qu$)!$D<*2Gtj{r_tgfr^$1M!3L5d1F^+14X2pS-<2>?h6+2Rd9Fr*)&E@{5XR3BK9!uMIVVA6R>rT^^C{% zYkwB%6a)-PwTV^HYt(@e8XdXt|F&tygSlQqdx(MPfks1!g&53*eUpU!+@ySX&tr5k zFqyaCr(4+c#9euEH|A%lC%hPOv!KAaQeCBgY6^K6WRFA8P9ZO{gU{?H zFcpbpGwFj%q_wZN4ow?m)LjXss^SMW*ubhL{N+V|OkdL`#+7bXqQMvM2|+Hk*+^Nc zuSQc=pbAc~X&JxO{!mdB&1=^bjEEL?kT?i<&wUs}8Doe~$OC|}toP|o6?zgX}* zbmjuxH&kCv_KY5EEXGRWWPd&sl{c&Na@i%#s+AAT{}88CX3{seSd~<5da^S@cw~rH zV;Kyogg5_PA=$d%j5;~5tp+iuy#4RSMne&YX^n*a} z!QDZ>lP+O$AQG7VdWi0b=~y80_XjIB1b%b?pGe(wS@g6osn@tbH;h_?ZK5S_a=ch= z$(U7hcV6#VVej*D#r+b68FizPYFn@)HXd@C?)sh@#!aw2bVXY3`CZ5Qy(G(uy9nwT zqHFw9FbpiwwQ6p&y6c>lO$3)(&O;}tBBC}AOk0Y>J8ee^r1tjG#qZ&v zl@q#^?~=uTO8(!gU|FpS{&|`zBX65p?b@@Vnx+J7+!`0@l!@ae8yq{dqPt~gDx`%w zH%-%=mq=`1I=ip%OSbFWVJK%4q3nr-?Q~Xg!EU;G!Mj1^JF10JrU*pB&3bU0>)nB2 zq2*^CgY^R-=(Eb^=F{%0zAjH$wPEY_rkvh-0GunzJ$#KN&XX46=kXc!n@@MjQtytG zh;e{Fg9QQy2T2vPI9MC~AD6>9`RuG30Lp!~)oVs{OaSF5bDzC@l^z3|+Bz{GQScZG zJhr6(Gi7&2Yss0-{t_qKtdnZOBL6A}Z#rb(pU#*SSX{9aCJ-@y%jD9mR9D2Vm~ybbft({mJ%s zu35U0cdjDzZZ@LZit@c?Q$Nac_NHD_U>VsbSI>Pm&+$Vws0ec&bFk6h_K3X|d!zNQ zM)!?dybFErn+c)=mgj7H{>vi08T{sW?@#U;#jg2K_qT20=9T4Jivv&<_lNyQ)EFJH zy89zdNazL_Y_YW>KV&xi&p-D9R_^g}9(hui<_t=8>tYsQcBsu+wBSQzxCU}THbTC) z1d%s;>b3G4vSzcZ=$EKSG8@zsfu^J!$aYT$_HP7WOa^9*US5b#KF!4?PhR*xC&mRR zZV}G=bvO+)Z;|_by-zIPXXsY>8cs?}h(q_qi5vpa-)S5xGr{ZkO?cMA;pmjX_7QS% z0=%Vc`ZzuOI;_L#G{(%;M$fcW?pg1J#U^wrkxZ)GDMu#eFvib;P-YBnMWT6>q4+4T>=rZrc@wN-t^f#7_#T_@|dVauK zk0D&rd3gY|lr5d&YJhD{w}FY;wPyXGR*gTu#ikJ1V&2-k#iTf&Q*l&tV@X6aS%jxc z5qN|z{G1*e2f_peOn#{UFK+3v;zr0=l*4WI(d1^*_jZolcAy`umnQCo>DIlnx z)Pe&_OaoW>maD#`$<%WaGqf*{F`+JDzMJ;KMjD=HFhX%P@vqbJT&2ZgX`4xCS6-&A zb(pjDatw>SM=03OGHWGJE1?OsUr-q^7W~xJ_C7ll z5T5grmXm3DQ)4`?D6DuI8mN(Y%OG-bDshJmDtwnqPT+NjP zZ;EgC2zPtg=9(;axU^lS@b2X!5lmhqLfib4|9pIzbYauCbfa9!zHZ1tV;MTrp~(h$ zSqZ@?A?T-LMEF?Aa3PtZ1pb=y2s+7oJ$9r7F{k2jL>?AUz_!4SgdpG zbjqTClR!)%24(w6nCypis=8xEeq5NRAF3tmj>*krqyI{6D^x4-{F{R_-lc~$g}W?| zf&a>GU~1mGVn~5kJ91r2%)$QR9mkkBTd7ZI`2=TtnVE#nAFN}!yh=BCVCBA9WcDbx zMnmr8i;;j#^z_sHN@3mZ?Y=ka4qECbycFxcb3}5wnI->ns{qH|3S09N9QeTa->I{g zMqS)!XKMEYhIrK_i3Zo~vc-M=N!AS>hM1b)EYD=P$B_sJuFL%x5S#|h-Yp=%P8Uk> z^|DjOOw;uCctZy~&@dK`%X+vFI6C~CDk*-OzQMw2Pe*#z^Dc>sU--wxhCa4DB6En# z9Iq|rP8sc=>-tr{t>HkQIs}aq9^|(a0L`Ftx>q(gwwdxtXZtW&IE=V2CO$5!uv0Lp zOjAOP%K|m`Ng{G0yC9g08cw|SYSeolC)z|_xfr%3JL5tJ(WBuX5Hkc;+;SGS zS>x(;Ue)B9+SIbVOYRydCN@X;*l5;MYF&SAgLj=gAbMII;VCm%%gcE8^-5~K=Bly27Bo&}rp6}Pj?%>Kc+FvxkvF5Yhf!xE5K98yd2B#-cfjueTZN1tRgAauXQi=Bplz$i~bOaj`?k-so~>!mG-$fTT`h%h;dd= zr5`BU>ex%^_)BdVNNqSsZJY#JS*>W1dLfHG5QpFhz_9wD7!~I!(VjY(4hX*9$>CfZ zehmb>{PR3CzCtc4$Sqi}a{f}G1>=ew{xGO5#j)g~c&@$yN^SWw#ckD0X;_@L+{O#7 zUiMd~7;km<#X?akOu6NoTjk6bql&D9HT`j~~r4D*7@%X3`EPEH)}H*({I z;2%Rp(ZfwYu9oGt4~Oa-=7?^{%u3jV!< z{ivFEOx=?YsI@k!EHY4_C^BVKn|>_g=(m--ZtTrthP&6i*IYp;0? z91Lf{Y_{x0*%wL>jHhy{K<;+tj7|8hQ@BWQ65lW(oDt>Xv@%1*2q+sK zB&h&H+CtkUK#A?9DvCu?mQ1q9<`Ds@h6+#x<87BOp#mye7Wd6&K~S*7K;T#@ki7Mz z1wk)UHmHe*EK>&cT9m%{SweCfqSnux$|9EOI?+(JCI+Ay6ma%(A`5ZIb&G+99NTcl zn$=jo{K7DsP3~61SN9hq9=hirU%)$WND!RIZ|iRa$#vxAKqtgx@l~$}Svi57OVCL8 z6SQsCT9W<+w2YJ@0FWvO&`_hNx86&*U{7;Wzc{*{@@{JXLoeWgK2J9T<4HSUFnX4e zCbbdyO)$U}_=mJ4Sj(+OR+TQgj}l`K)7SjaTgmHM?i&2mm!>EFAu_BHWcY`5ZQT2v z40J{BRc)DqGV)cdKS%JoX zxY$d&&4VEC@eik0TDxH*ICN!MVg*~Yg4T+z%qjKd&8Y%NuBYul=&U)A%BUTG13>Gl zpgwA=oA*S4zlMLOd6q68{cFqXV2ujp931qh{!3Y^IK(3#D#oiBXY3c7vEkZJ5vYK5 zRoQDWY~MM{Tlv*;(W_TUx^)8@lM=z@ z0CeXTglzeA#|6d8)3fg4Cw)&|Rr8k}FFH4*LWA@UM-QdcTtBSHoFvENEBujmfyzS{ zIpt0cHMO9O!)5z_4Z?z}#>??Gc=u_8xquJ1c!C<+bs+T6#z8^T@Sf=-AOFI|MeQkG zYVS|CQ6?YP*4EHphsrpqR(2J#MHpQqGfagPbT3-xqtxC$QJum5Q~P}4rW!~dBX0sh zzy(}{TUjAZ7pf)U5R;R0!0Dk&6a-(>m=H5vl{Bg zW-%0853|QoBNveJ`;9@QYZhA|95;S}t}s2*`2j}g+irFe#}Dx(=-sa##!vDh((D2K zJy8D@o=+bp7JT!vw)2W~(#h?{Gt2W4^q6Y&)0U1)XrFfx#^<2AIS!w)4RHkz{uWJV zLB@#p0U&NZUUfOg4>|VqvLfGPZpM8%q)Y>)1pE)5GK(6yq`p_wX6~~+vMSYVFx*$i z_>B|=N-Uk>@%z%sq}-eqwD2Ye-ll1=`iJ~Q_{r*AOs`3_=zpj!F@>~o>Cr7pWGww; zP&M>+QDs8$WZ(#n#cdLg>R;d9NGrGmxai-DTU};=uP`B@=_c3CKdi_suta1B^d}S;zGiFLRFe*L)BAym00qU z7cVW~q|5aiF@~nJRl$pJUw?a|FMVm*Y>mQ)w{6`>1-4>zz*)yvd8)ng!2mjBE6q1{ zquW@<&OJ}MT*3*E8P$I*YzV2w9aC<(b{)HU##F7&{T}R*EIiug4Kwp!X z%1XZz-Bu^3E}n{|m}Kv*Pgiub-c8WZOIb@K^D?!FkJiN!q8WsieYw>VdToTeR>O5& z%;_XQ3{&g4nAUniA7@p)6>Ry?3l&xxF3>leRcAd>$9YgifWm|V#|#b>2f+*$3GEji zHqbC+QDMbV!$D9*ki>){#|%~u?U%L=rkiTWg5o?#S|MaqBy&$c`Gg5=H6a70YkJDW zFKEcdFIkmHog}o7Tgyg8Ppeo7C0A+Q0B5Cg5>`Aeid9n zg0;W1<#%@f&Y`hOTs*55ipfBQw3AtJ4Or(g>E1QxEy|ZXIGY|rJ`RsY}?b!wFfdlSA9__&e z?|~EVK>_c<_5T3^2Ofev9)invVAbCQCsFOiGl_q;*({gWiWf90*}u}}?6-}UCJ-w@q90^{u$ zR$`e&LuiH=^$P0rwIUN)?yIOGUtDZR3JIS&DB&Pyj1@O{^#Cooy<9)ZK7TDuAp7w@V1~|a`C2NTyl9o zVOk1#uV8s9_O@hmySpfvT}i$wS^iJ9G?K5%4*#Qzl@3GZ+|fK2=EeU6@lTe^4~akD z+4pO&MZ~QC-oMWZA#fz%G?(`sU%;CP*zfH4tA<24EU;4 zb{2bEsJR}jiuG5@b%y7urn?}!R#BY_X{qbZ38vKX*{z5{SA9ElYTr(5^|uqH`RzP3 zWfi}dTe`|T&n}&HZV1V0>n^VZh#3=z}c zj!OEdGZt87OB&Vk5rB=ANw4hRkrA#uc6%#|WsR7Uq1OCO&p~ zmTcs~kVal6d7A9Dfsn>78U_cowvZAiHT4k4=`+9jK22RDs+`R!gTSUvEC!C&PW0~v z?4WOti)Hts77RXTQP5yuRTm74rwYGsKvividw`kqs5ch;5~~+N2ELOa8I~`C*S|n4 z109MlgjcLUY^4+e0cQduAh2fm(7sPY=L0HcxTJ1yMd#%kOzmf9_6oTT7??c4NA`l2 z4Q-g*zrpA~aQy~RZl1wCHtw~}Gcs=8KS!wP1uJhxq5BF_7~p4?D1vg5;D}}hfxJrw zaf~dwJ@vIU9Ffm8Ok{EHQc@d|L3wdrGhyCJQoZJby>-UhZHC*+4cF`S#3w7rHY>;o z1OJ1r7vWqN;e1bG_)p}7?}>=-iE*EQ#<{)pMq9fN{0yIcVUXB&WP7F6A;Esu0piB? zQ3X-gexW0XzX&7#z}g+P@@ShZuR2^HtSPC9kX_AfbU@tH)AUiI|E*3oJI`{Gj`P5Y z@J0#cix?~t*)Ke306_guj96w_aMExPI1$1qp~Mh_l_UG52Mtt=SmxFXC-i1b?Dz$Y<2nNma@8C zzbvA2)vv=^aeh7annw@+ymn~CCFk#+cmrGRdy!9<;V;%5e9H zvJJaOx~`w{+uB8K&HT&X-N$9^6@I*IrgDArv}SWoNTin?*)v#W-PG*v;baT-jC5VI zc(rxd(2}*?mD8+m6uPVsk1uJk>C_wX9p7Hpn*f zzKm(H>{*@$WJ~&7#xz?_EKdt(Q~6%TG+C}LPqSo0`CY~|S{^S?lVl%#V}s@6@-#~J zi2r3w{V+7(2e^$RUYUs95H;U zoziNhcA8&#bPkI;6r|;iu33816ew}7)`Vrt!_w?A9}fh_xk4cPF zBxh>xr6D%yiJpB*52e6VuGC6!1=t^eomkx>1!zWR52y3 zt#T^dQyJOik|ixiTN&{mJol8=vZUh19IQ(d|ZnK%{COyH7bv&wT+4vjia zW%}^gjv%6%74B-h)&MfcOCa2j+1+@~m#i*Y*_YZlfcl_xI{I@`PJ%kpxQwJ%QdHwz zlt{-Gl&-hel(*KRE3YH-7_4V#LC#RQpMIU6epN3M^%W&AX&p?;_rZuy7Ktqumk)<} zD5wuG1Dw!`-oSg_z-!(*bK27nLb~^qWD6%Ey=4^t(0?9$A5qHdQA+Fa|88oUpEGpe z)01LcgQ2UqG2tYtIYxco_`SU;FR*&&7vEG7wGqa!*rL+Y+z%U>CxDpE*wd`!w#9x+uYVc3=$|m&0Usb_dRy-?5UdRwF#9WLKsU&>MQu4*=lXAM%&nqD3 zBESs!u!8E4h)+dfTu_D9`+Hcg%u4a5QR9@a5BF0|c0%T?`nK271P_H|)xUdOn7?~e z>c4v|j5vL5h(oJOn=Z=I7ax_Yyd*2kEhp__sdpE5L|m>kF{|O(8BJ%BCXJ3QE1=R; zu&kBjTy11b8(y1Lbn&Cq=9QFq8Y!D`1Nap9xC&P$g$04JOlrmeB@6P{MT!NqC)=S0 zyf80Oe8=h|;MN1zo<^*=ATZU^rnE3`R+6Bqznqd_Ey9=+|1so0Pl|E(&vzBJg1Q-X z)JpoycU-M-F=xSBYBa5Jq>`hvn6qZH)#n0qwNZYbtwFMRvE15gyK<&j&wF08UMSv6 zRnflUP2lX2FZkv$^J4A}VXU1zn~*M_l-fPPE&R(|dWvxM0sw`G+xcD20+K49H?5XX z;oZ4Paq+uIimhGkbQ8iAlX|K(%P~67ZxRs}c>D11crRP~npZ>R)4>40We~Jll zAz_EVaTOSccjptHu>#D_n)8*y-ma*~YV=E=TrFv}l2Vwj;r!T9?uMYe0qdUKgCp|r zYj;Vb8(OPtTZ?PrU|i})n5k8b%P)afjgs&nRHLTYzisWW*02sguf0Qw4~QGo>}NLA z5_i5#xOfs6*!N+-H{&4MP2<*T4lfBpg22GWmkF&t6FHLV;w~W9#TI!-KCN>Ry4q5lh=46-t6gqTg@>l$bAo6Pth=Fz#bAOC z`dgX|%=A{M#JzSMuE8FzwbdY0=@a+YRT!jF#+hJW?N&JsF>f+g5nXtPJDMcZaikD76+;Fl8cCxq>8@D#fGw~R6vZCDySAqwo_*psr^Cm2m;fYZ+j zUZ$T3>X3D}(>G^7cs%0bOr#|baoU|2cqk0+VI^QoT^1t3sbuligfXJO&XLEpx)X|z ztvOY?x$Zf8XV}Azmy?G5e5@qzvd2P5-$GH?53iFUZzGh7W|7*!tAQ$UZ-XR^^UF57 zU*dodu44UcWdXr)=+L33%wC>IdF<(cv@;CR&t4Ha6Xm`a^MCAoeDY>AQ9sAS;Q?B-$1l};*wR=bsgo^Ivn7h+s#xsl z=VdP>HSbC$v6XRV?(1pZDb6eqJL6+48~?#{Cbrkv@t<=@Yedtv-XcM<3OF^tfQ*dN zcQWqUS2YKyyM=M*x^A9(ZHg?PBgU=R)*@(h_6h$$d-t#iuB_#q-SwvQaJ71Wsbcy% zjmB|14QOevc_@?LyC+jDL{GXEzP0=1yimi zWC{2rg5u_ssRX>F5rgw=N6*;=uSbwXkIHOB=3ATKJ~}WJ&XH&}uzU z_>$zLg~k(aJRrj)<*&tv5{^Ydvl_V+yBPO(!8&iB3la65jj932Zf;kBkM%18(g?ET z5sHlbA5+oq-?t7v3LpoN?Q?mgka){x)y{|iz1Kl~cz$6dMI?Y8<(sm>ra%XV#H9^Km;Dy9_+XmCbA;fS5bLHSnOZRgi#3hGcIUohu_0l)obi00 zYOB-x{r=L4D6swVIRDRyVA;RHt`?>8Pod`D91ueM(BFPb=dth+^lQ6>dVY!T*i|8R zn9gwSvBE%qFRn3BftlZ^t$#rQVVuO~gftuzby9Uk6F8JrA=U%&(bs?s~84%0hJWy${ zIDq&8c`)3(mVK5E1}tVi>Zvfa-8fK*Jfb;KgVdSU3XCZs=sWX9*Ucw$7V#Xj`AsiF z{-i%AlzK-cdIz=Hynky?--kchcO~k3@g* z7oQM`C2Squ*ZK>%1Iook(hW=;B?}q75$6^aloehur?vX?Zp6R^*5p$pkj_ zCfN<_Vn+f6jBxqU2rg1*)b%IyLPZA9NvpZ2W7+4e8FYkbZG5+zwZ~pZN@gMJ&@(!` zHSHGeA@`pmP8Om8w?*m^E*_zb9!S?PAXE9@Hh#ad`NMVW&fk-5f`0R&{@>g|Qlo?Z z&Jg@P{`L^>Qt}k3;GGH5h)FU=F47-r*idKF6y8Kpq-4RQV3NQa4t{VSVeP0smbn{x+S;&MPb3$K}sfj|X)9wpk*v+pL2Y8Sx zMB#t99tj7-s_cDfH>Z5!#$dgorSh0Vc^!34jP0>*lj(13?$K%)Dr;^bf$5UWd)V8- z7E`1&kl0bHvB3w2XUO{<8VuTTZzNst#FStIZ;K^uTV#D{``AE1<7pKWB^(Fj0fwTn zGy+qHF~a8sDE)+Xiys}!kfEe^!HgE5gNYl0Kxpl;gnBe~iQE7zhR24RW*+ov7A5m4 zYlGUSZ8bx;e~BM^fU*Gzz24X)Pp|c_qxp62?Jp$zH!s3)7$1*}0MkTlP!i-bOTr9o zljqszWG;7FKgMytrUCl* zs-02CV|7s@kQiGqcqSU0a4w%%mm|~wr(32M+6+GuI5C#pzAG5G7g&65k#0ErS*M3b z`VHI+%->c4vC!rFQ}?J_wNQY+-J3&tuuvKqllqK>9#=c2rh0Y!UbA>~u>9ct>M%(> z3Zl)LY!ohG^&Xl51ISBu8G`|U=HR>eQ06dViDEeha8WTMD9`5gP&fqqCK(Tp^0Y$^ z^n_4D^dYbVw+EZ{BH3A(DNJc95;vZ@3p8b^Ez9K?|IwtWwnvS8)qz&@u=MhEDT+uE z0I9U`0hEs~NrR^bt5*~3T_GBsqM9+Kns7-?2F&R&nq@0^{2>-GFjwu#azK2E0<%JO zK1@ox2A_yJJ2^otsBsZ}HF3DSU{tg@N^~bAvZolW1D&O>7>gf;k&E$X;;KKjlK-b_ zk$eM0^!@ww){C$+&SM&l?;qQn2j@I0bOK!|@VX)5dq*{yjF}+QlK>RCtm zy;4}NePW?r|79~b8y81pE%=YXtgkm(+!Na~$o!xQr9Dw$YVbiW240`&4W75_qYklc zghxRQ(OQ`c8JNopBCFEqNBbiRVXzg<40d>^Xv>7j zFhR=*d#Yh0JxTdbc7sQmu z_}fRj4G%!T4aItKnV-z`K!OEexLiU5!d@}#c{W4br^k4lNqtICm6Eq z=hl6kb0@iE9J@ddl+2@>rj#r@U-LP#?a6Wq{qQ{H{+XshK3~$5nJ*4Gj|7x#OrqCQ;|@P^l>xanf*)u%WkkVr~WR#4GhNf$Cap zKwO6Ql0OLs6MKnqg(zpdXuRa>JqRC7uCX)Vu-wJIe983V0b_2xu5e=VOZyt|TneZr z9Wmj|2Hhq%(KH#mRlHFohQL`>4W&Ewo4cX4Cd+FYFe0U&4+_c8V0s*4X=35+BQs;r zku5kBmFbD4wq9QsGd^68kN0>9-FLV#^6y#AoA2*4VYKV9*~S_%D~&K`r!r(ZUk=8S zZ3xnH)56tEI%Eol8+uNMGds4bD@Rn^m{Jvf4(GF{kzbQ8PB06+kx(K*=xX%qrQap) zk{sB9Lk_|Y4Gazvzql`!%Je;B1HO`Li1uT9W|m|-H$r%0QqG-C-+_;Eb(@;VCB zV55lIQkZZ0#|(Ef3S*fgkm*}1`%G80yG?$ksslVtHD6ebdlD#!;7OvBRn!wEa+$09 z)+Q6q3Nc5&FPNjxiyE6?#|2Ph_Co@=Fs$UI-Dm zNaMyq_IVOH&6*;0k7bQM=|>q4NxUkQg#$1ioUHmV7LWh{^Q+>UU0+mPj5vNrNIy~t zw!)NNUuHDs1`o|wMPU~w(pAK-ki2AcP9c&?T-kLP|D2V4eiDE1bm31B|9qDo`+hYz z5LWplk3;EX!QC&8u1eXBLnmX$$f-%0#VOo%ii#nGT;9< zG@_TP!9h6?M?vKpY|@{3^L@D}yPt{@J>UK@JVr86KMcK2Sh$G|k=?>8w?Z_ZpC zkDAUzq+#@Dho&sEMhtLNHxJOjrly@egJ?%bav($pSNI`A)nT(G)0KbzIlZpH7A8FH z-`c#L>u@2pI;#mqz|*&*L`mx1mlO7ftGw@VmdRb|G7*K%BeB`V4!MN=Y&7epxWW}% zLh5~k4V8~X_+^mvJx72YH~s7Hco6cu{v1wT-qKzc$NJyBGYCZcbIlcOn46AbXyvJ#abTiP6VRr?tWOqzR_N<$ zWQ>XVwX-O*3eP(A0ny3R1DHC_>AT{&v0sNpU?eWGjG?Cm`-k+$7haG+?}w-yq@JNF zBpMxxjO43GnTsYpQ%H`46=)I5ftVF~N*l*1=F9RlFW}4im_X~P;M^-;T*`4G5A#iZ zwn0x3t7a+WnJy&gfd8gqN26kIidrZq9VXhL<0%3;44ti(BIE~LcH2TmPYNPE2{yk) zh~!MDbe1>?xUgAi*DF56tlP#msmK}Jg8eC z+hM{5D4beJB+?JEoRxDnTxMX+@+GC^^)Dpi?JVtZXg?BXRN!5v+Byr^sY#5$O0(3% zEbCW6x=L;aG*|~%WYy1@IsD&)tl|4*usbPFo04Bbw^wb4eBKDTSbqI*#dY&HG7t*Kl+?IxIR--hzTkwX z3Mts67V5Kef5#?nsR79&klgI&AXKKN$0AO+W~X%V*FVCB+hF^E$Q_Yz^sE+VbgI3B zk9*VH*|!6sd`ww(n5n>;I};=vq5t1udI z&1;e5h&MUxp0?$u0~Ps_Q_}DtV>_d>!?#W?-viy$^AKonTLjA?LVJY-a0JZ@?vFkHBRnr=HNLDEEeXO|CI}$JBRkIymxo zjaoYlb5wehp3g22%ja6xS@4DYq~CoDdLdLq#{0C%Q1#aSZ%zXaN3fD$vHga2OW9h;h1Nr-KQOF=M(748!P#?k4VA70c>^3T-l{{l}yu)nDB7x43X z>@-V{RTG%$<>BE=$%mDZy>`NqLKfjIG|-jz8B`!S?3}{9g-kFoA{IQo|Gn#dJUsl^ z&R0`sfpE;|2dxKWR8YV;>F_s_1&h)T@YzEw6F>EC2w${8DQH2)Nc%jW!i&p?0E|G7 zH9dWvqtJR^wlBltYcM`lV_$(A(E5tnG!Ash4_EuBw3L*)X-CmIc* zD@}!odx5I^$ps%{N_WbId-7>~`svIv>;lS_vr!%nYdGDz<#P8m5r)~w(hNQmo|u8d z{S*g&>~cFd^aEbR!NJOlPvg=P9Jt)v_K*uhrXh&c9;H`XmK-OJ-tDg_LH%4z-A+7KB7qMc$G)Bs;fT*(>nI(OrwBQZ+}V3aG=n z!?aXb&bdP6T|zC%0=hx-$3KVmiqMhli3TJS^zwNu=y1uL+Ca+q^b+mHrmSqD)VT1W zkujrl;Kw+H+Mr^NnUDKEaw^V9kd6e^@-IBhhgg7nyrroGX=?XeXn1uKq*G`KKRU6m zxp?={X)cOlIez-|_V%`Y+i$1Y<vnZ`zJvxSZ1BJ7n(wgw9q* z>>fKJ8Ti0mK@qS|hks0~;X6iN=ItA@PKv1KxaiC&n`yIIx$PRe2=uIJZQMan5=^w* zJ{uLJ=?6}y(+-nlglv>UvzI~oPrUXu<2~H1J9|?$0zY==WI+e@$e9rQa~R@ zL7;TxOTX&Jl`|7NAgCD*b?8X|?Zj&e#DW2)G`Jx7l(7Laz$MKo(I-#?(7w#VJm*5> z0H*=iLJl5+;P++f*HB+x2pD`kocymPU2TQ)IG1G?if^8uNJ%;J56rV%3n}>&zZ2pf z<+5$Q@T73iTh*SeGISUEi7AUS*{c3pWXR{wC$wT@m0U9ozkJ2Tl@2-&*;Ij(iV|=F ziNB&R6U*3azH?H}#pU#Xlk&+(<{r^BOI8S3sP-twHmJDUlqmx#DTi#22^o-#{@69S zqnY>!oz0r38Qsi>hhIA>alMzx6G;c^EUY`pgkHp}xnPs3E*0um^PysXpd$14nlD&P zOAE7>)u!-s=E%b3GC19?%N1#u$3b%vk`6qfku9b78BU-gIfS(jl};6pgRrt9n>j{H z6sdV(c%)FL9*!K|F%f?*nmJX}`&Zaxj9c_jWJFruTICQ|D}PX3OL>dqRI?v$**eN% zyT#?(-npdU3skkWPi%HR;p!(*oD6yKhU|*3q&1(2(6M-7W_kr-oyK9?n+m2+gEC*O zJpj@`qmHI)O&$wo-ENzrBIlE(Ml26ffn63(MrZtPS z&H@M0X)6?OTaQmbgRat{MPGmR6Q_Ul%=P|88wmdFdha0Q=-JTqexQ(duJ>CA8IAzv zM+*6^>wQBZ0P{Nv`Ns8r0U^Ea$n}1rkYBjo?-Ax0QvH=ee(!pJgpg4OVE&*iRr{0V zIsB82LLcw1r)i7|CB5hoZCWV>9q+`pMT&FLc5I83hkK*ALh*|Eei?os-mA7c5x9-Z z0=+*e%c%aMt=0niC)LbNB9?+@eC<}dTD#`UAL#7$GQ{j6x?Hh%;K*uR&S*yOHj4@# z2UcRu3OVjE1>Q84G&a*xjlgP@)5}GyVe^(rqKJOLdg!^4Rf?PfW=v+bs|sxw1#7z9np zN>FO*o}^iTs5cNE4ZPW>Mpmz7USLlc+v zH!+&+F7bbmVC6O5{!b9`H^+7U*#6P`0|<@wkB5iw!wkk;erGHQqg>&opjQ`OTqx4} zz+`KSaxX?}uqv%A_i+XNr4imOQH!O8ZOC6*mk}Mt`!jbSa^3YfbejWd%>^2WeW$S(UE8c zJWfKzD9+&tJX{ne?=L{_8&I)Sbp=M-ts`x2DwnkR&;v3ORB5?Es$fICJUmPa-$Wtw zsuimV_gob|t?G{NDUS-kvI$A7F3060Tu73*3SH}3tgw`w>TFmITXTK0Ayp!8TnG2; zOc$XhQnAS06)RY`OUXNk3aP;F#5N{gDptC&2y)zC5QB0vZ?qaY_BF>< zAh3PqVv8*aav~qRxRoLiSLX;AR6KABc~=q;%Sx43L6W2el?fes76*CW2y&%w^P*#1 z{LTh6WQNj`gnpFxr)ym8iw>|4L9u9hd}>cY3Pl7>hJMW!8`H+FB)DnM;xrFGvI9rn z(h)Ib6wlD9l;Cy-8X#@bVMP+kMuCDZ&3sB{YBj_GXw4@^UR-BqAz0`VN<3*c5PFP9 z6NsXiZfma`R}LT}(QG$PGXSZCQ|sgAS51!)Wu^eVLLJlO6_{pMRaPN-k42qBZ0)TF z`OKpm$cbfuHR3zSnr0*(Wnl@%Q`I22TH$=!IdcuD;;vAEx@Tx|RNr`-gC$WT4`*t^ z9wY?Z&+s^czc{k^PwIquxJ#JFTj$*MW(Df;1sWDB`ppYAPwib4eCB3Zlzwe*&H`uN z0$;e(q3S$01=}4MvdM9JI54t;c>&TsVa4%X$Qf!6-)uGxdqGQj4CUhr_0zLzKV|WP z-0$U#0ez6Y1POi^uL`INhT(3$Y9+m_?m(GcPv)#@TNmJ)6i=RbJrd4Y8*D`HAY&eP zwxsPeIYAa0zb}NM8%h$s+zElO?@s7fILdINZ~_suvCf^~`kI92`VO*XNO#Z9*ercz z$m8g;<)J_%z^7=hT(K-nuLo^jqzr(D)u6|K%Lt^|MHJ)CPp3wYn?bQOLgC_y?A``P z3&P*w;K#qwC0dCukD?fnP0h1G1mpJ?zh^$tHX6?vNJl4-Ee@7u|(V`<)j79Hx?&OC=ej(l&I9KG{Dvts|{G^ zA%uVMR|W4ZMO$##mewkV28lw0JS7f@Gj2O505z-~6Z%R8mr%ha?V;tPGuCU8uu0T! z_Xk#~w2ToMf#e&j$S-8%5;j)z) z-|Qe�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 literal 38996 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 HaO(m9O;)2$ diff --git a/webapp-src b/webapp-src index cb3973b..015a0e8 160000 --- a/webapp-src +++ b/webapp-src @@ -1 +1 @@ -Subproject commit cb3973b17708419390bf911af9be255ecaf988c8 +Subproject commit 015a0e816ca4fc617e67acdbe7d067688d6b3939 From b1fa29d301896ef5118a1017f620b11840ab8a0c Mon Sep 17 00:00:00 2001 From: Matt Magoffin Date: Sat, 14 Oct 2023 16:16:08 +1300 Subject: [PATCH 19/41] Tweak to work with Homebrew OpenSSL on macOS. --- CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 428a6f4..0c4d1f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,9 +35,12 @@ add_compile_definitions( add_executable(mo_simulator ${MOCPP_SIM_SRC} ${MOCPP_SIM_MG_SRC}) +find_package(OpenSSL REQUIRED) + target_include_directories(mo_simulator PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/lib/ArduinoJson/src" "${CMAKE_CURRENT_SOURCE_DIR}/lib/mongoose" + "${OPENSSL_INCLUDE_DIR}" ) target_compile_definitions(mo_simulator PUBLIC @@ -51,7 +54,7 @@ target_link_libraries(mo_simulator PUBLIC MicroOcpp) add_subdirectory(lib/MicroOcppMongoose) target_link_libraries(mo_simulator PUBLIC MicroOcppMongoose) -target_link_libraries(mo_simulator PUBLIC ssl crypto) +target_link_libraries(mo_simulator PUBLIC ${OPENSSL_LIBRARIES}) # experimental WebAssembly port add_executable(mo_simulator_wasm ${MOCPP_SIM_SRC} ${MOCPP_SIM_WASM_SRC}) From 5c420a76601bc739e24408a2a13060b99a9df155 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Mon, 23 Oct 2023 16:32:29 +0200 Subject: [PATCH 20/41] update dependencies --- CMakeLists.txt | 24 ++++++------ lib/MicroOcpp | 2 +- lib/MicroOcppMongoose | 2 +- src/api.cpp | 18 ++++----- src/api.h | 4 +- src/evse.cpp | 22 ++++++----- src/evse.h | 2 +- src/main.cpp | 20 +++++----- src/net_mongoose.cpp | 15 ++++---- src/net_mongoose.h | 8 ++-- src/net_wasm.cpp | 90 +++++++++++++++++++++---------------------- src/net_wasm.h | 8 ++-- 12 files changed, 109 insertions(+), 106 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 428a6f4..50bde60 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,18 +4,18 @@ cmake_minimum_required(VERSION 3.13) -set(MOCPP_SIM_SRC +set(MO_SIM_SRC src/evse.cpp src/main.cpp src/api.cpp ) -set(MOCPP_SIM_MG_SRC +set(MO_SIM_MG_SRC src/net_mongoose.cpp lib/mongoose/mongoose.c ) -set(MOCPP_SIM_WASM_SRC +set(MO_SIM_WASM_SRC src/net_wasm.cpp ) @@ -26,14 +26,14 @@ set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED True) add_compile_definitions( - MOCPP_PLATFORM=MOCPP_PLATFORM_UNIX - MOCPP_NUMCONNECTORS=3 - MOCPP_TRAFFIC_OUT - MOCPP_DBG_LEVEL=MOCPP_DL_INFO - MOCPP_FILENAME_PREFIX="./mo_store/" + MO_PLATFORM=MO_PLATFORM_UNIX + MO_NUMCONNECTORS=3 + MO_TRAFFIC_OUT + MO_DBG_LEVEL=MO_DL_INFO + MO_FILENAME_PREFIX="./mo_store/" ) -add_executable(mo_simulator ${MOCPP_SIM_SRC} ${MOCPP_SIM_MG_SRC}) +add_executable(mo_simulator ${MO_SIM_SRC} ${MO_SIM_MG_SRC}) target_include_directories(mo_simulator PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/lib/ArduinoJson/src" @@ -41,7 +41,7 @@ target_include_directories(mo_simulator PUBLIC ) target_compile_definitions(mo_simulator PUBLIC - MOCPP_NETLIB=MOCPP_NETLIB_MONGOOSE + MO_NETLIB=MO_NETLIB_MONGOOSE MG_ENABLE_OPENSSL=1 ) @@ -54,7 +54,7 @@ target_link_libraries(mo_simulator PUBLIC MicroOcppMongoose) target_link_libraries(mo_simulator PUBLIC ssl crypto) # experimental WebAssembly port -add_executable(mo_simulator_wasm ${MOCPP_SIM_SRC} ${MOCPP_SIM_WASM_SRC}) +add_executable(mo_simulator_wasm ${MO_SIM_SRC} ${MO_SIM_WASM_SRC}) target_include_directories(mo_simulator_wasm PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/lib/ArduinoJson/src" @@ -62,7 +62,7 @@ target_include_directories(mo_simulator_wasm PUBLIC ) target_compile_definitions(mo_simulator_wasm PUBLIC - MOCPP_NETLIB=MOCPP_NETLIB_WASM + MO_NETLIB=MO_NETLIB_WASM ) target_link_options(mo_simulator_wasm PUBLIC diff --git a/lib/MicroOcpp b/lib/MicroOcpp index f993ff4..cf532c4 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit f993ff4c3a37bd83075170e08a1ac04a4cd16c42 +Subproject commit cf532c41d541efdde1b9f4a95963f2661b49ea70 diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose index e3bf1e0..92ebea9 160000 --- a/lib/MicroOcppMongoose +++ b/lib/MicroOcppMongoose @@ -1 +1 @@ -Subproject commit e3bf1e0a0ca87ab333f5d6faf445e9eafdc83d65 +Subproject commit 92ebea9fe8554999b4d34c6a03fa12c0d9faec48 diff --git a/src/api.cpp b/src/api.cpp index 22f65ad..93e7d53 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -25,7 +25,7 @@ bool str_match(const char *query, const char *pattern) { int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *body, char *resp_body, size_t resp_body_size) { - MOCPP_DBG_DEBUG("process %s, %s: %s", + MO_DBG_DEBUG("process %s, %s: %s", endpoint, method == MicroOcpp::Method::GET ? "GET" : method == MicroOcpp::Method::POST ? "POST" : "error", @@ -41,7 +41,7 @@ int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *b if (*body) { auto err = deserializeJson(request, body); if (err) { - MOCPP_DBG_WARN("malformatted body: %s", err.c_str()); + MO_DBG_WARN("malformatted body: %s", err.c_str()); return 400; } } @@ -56,21 +56,21 @@ int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *b } } - MOCPP_DBG_VERBOSE("connectorId = %u", connectorId); + MO_DBG_VERBOSE("connectorId = %u", connectorId); Evse *evse = nullptr; - if (connectorId >= 1 && connectorId < MOCPP_NUMCONNECTORS) { + if (connectorId >= 1 && connectorId < MO_NUMCONNECTORS) { evse = &connectors[connectorId-1]; } //start different api endpoints if(str_match(endpoint, "/connectors")) { - MOCPP_DBG_VERBOSE("query connectors"); + MO_DBG_VERBOSE("query connectors"); response.add("1"); response.add("2"); status = 200; } else if(str_match(endpoint, "/connector/*/evse")){ - MOCPP_DBG_VERBOSE("query evse"); + MO_DBG_VERBOSE("query evse"); if (!evse) { return 404; } @@ -93,7 +93,7 @@ int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *b response["chargePointStatus"] = evse->getOcppStatus(); status = 200; } else if(str_match(endpoint, "/connector/*/meter")){ - MOCPP_DBG_VERBOSE("query meter"); + MO_DBG_VERBOSE("query meter"); if (!evse) { return 404; } @@ -104,7 +104,7 @@ int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *b response["voltage"] = evse->getVoltage(); status = 200; } else if(str_match(endpoint, "/connector/*/transaction")){ - MOCPP_DBG_VERBOSE("query transaction"); + MO_DBG_VERBOSE("query transaction"); if (!evse) { return 404; } @@ -119,7 +119,7 @@ int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *b response["authorizationStatus"] = ""; status = 200; } else if(str_match(endpoint, "/connector/*/smartcharging")){ - MOCPP_DBG_VERBOSE("query smartcharging"); + MO_DBG_VERBOSE("query smartcharging"); if (!evse) { return 404; } diff --git a/src/api.h b/src/api.h index 0b8e5a7..9c42e5b 100644 --- a/src/api.h +++ b/src/api.h @@ -1,5 +1,5 @@ -#ifndef MOCPP_SIM_API_H -#define MOCPP_SIM_API_H +#ifndef MO_SIM_API_H +#define MO_SIM_API_H #include diff --git a/src/evse.cpp b/src/evse.cpp index 94597d0..bebfed6 100644 --- a/src/evse.cpp +++ b/src/evse.cpp @@ -8,7 +8,7 @@ #include #include -#define SIMULATOR_FN MOCPP_FILENAME_PREFIX "simulator.jsn" +#define SIMULATOR_FN MO_FILENAME_PREFIX "simulator.jsn" Evse::Evse(unsigned int connectorId) : connectorId{connectorId} { @@ -16,7 +16,7 @@ Evse::Evse(unsigned int connectorId) : connectorId{connectorId} { MicroOcpp::Connector *getConnector(unsigned int connectorId) { if (!getOcppContext()) { - MOCPP_DBG_ERR("unitialized"); + MO_DBG_ERR("unitialized"); return nullptr; } return getOcppContext()->getModel().getConnector(connectorId); @@ -25,7 +25,7 @@ MicroOcpp::Connector *getConnector(unsigned int connectorId) { void Evse::setup() { auto connector = getConnector(connectorId); if (!connector) { - MOCPP_DBG_ERR("invalid state"); + MO_DBG_ERR("invalid state"); return; } @@ -41,6 +41,8 @@ void Evse::setup() { trackEvseReadyKey = key; trackEvseReadyBool = MicroOcpp::declareConfiguration(trackEvseReadyKey.c_str(), false, SIMULATOR_FN, false, false, false); + MicroOcpp::configuration_load(SIMULATOR_FN); + connector->setConnectorPluggedInput([this] () -> bool { return trackEvPluggedBool->getBool(); //return if J1772 is in State B or C }); @@ -98,7 +100,7 @@ void Evse::setup() { }); setSmartChargingPowerOutput([this] (float limit) { - MOCPP_DBG_DEBUG("set limit: %f", limit); + MO_DBG_DEBUG("set limit: %f", limit); this->limit_power = limit; }, connectorId); } @@ -134,13 +136,13 @@ void Evse::loop() { void Evse::presentNfcTag(const char *uid_cstr) { if (!uid_cstr) { - MOCPP_DBG_ERR("invalid argument"); + MO_DBG_ERR("invalid argument"); return; } std::string uid = uid_cstr; auto connector = getConnector(connectorId); if (!connector) { - MOCPP_DBG_ERR("invalid state"); + MO_DBG_ERR("invalid state"); return; } @@ -148,7 +150,7 @@ void Evse::presentNfcTag(const char *uid_cstr) { if (!uid.compare(connector->getTransaction()->getIdTag())) { connector->endTransaction(uid.c_str()); } else { - MOCPP_DBG_INFO("RFID card denied"); + MO_DBG_INFO("RFID card denied"); } } else { connector->beginTransaction(uid.c_str()); @@ -191,7 +193,7 @@ bool Evse::getEvseReady() { const char *Evse::getSessionIdTag() { auto connector = getConnector(connectorId); if (!connector) { - MOCPP_DBG_ERR("invalid state"); + MO_DBG_ERR("invalid state"); return nullptr; } return connector->getTransaction() ? connector->getTransaction()->getIdTag() : nullptr; @@ -200,7 +202,7 @@ const char *Evse::getSessionIdTag() { int Evse::getTransactionId() { auto connector = getConnector(connectorId); if (!connector) { - MOCPP_DBG_ERR("invalid state"); + MO_DBG_ERR("invalid state"); return -1; } return connector->getTransaction() ? connector->getTransaction()->getTransactionId() : -1; @@ -209,7 +211,7 @@ int Evse::getTransactionId() { bool Evse::chargingPermitted() { auto connector = getConnector(connectorId); if (!connector) { - MOCPP_DBG_ERR("invalid state"); + MO_DBG_ERR("invalid state"); return false; } return connector->ocppPermitsCharge(); diff --git a/src/evse.h b/src/evse.h index 08bcd25..d01c8f4 100644 --- a/src/evse.h +++ b/src/evse.h @@ -92,6 +92,6 @@ class Evse { }; -extern std::array connectors; +extern std::array connectors; #endif diff --git a/src/main.cpp b/src/main.cpp index d2d956a..370e272 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,17 +4,17 @@ #include "evse.h" #include "api.h" -#if MOCPP_NUMCONNECTORS == 3 -std::array connectors {{1,2}}; +#if MO_NUMCONNECTORS == 3 +std::array connectors {{1,2}}; #else -std::array connectors {{1}}; +std::array connectors {{1}}; #endif -#define MOCPP_NETLIB_MONGOOSE 1 -#define MOCPP_NETLIB_WASM 2 +#define MO_NETLIB_MONGOOSE 1 +#define MO_NETLIB_WASM 2 -#if MOCPP_NETLIB == MOCPP_NETLIB_MONGOOSE +#if MO_NETLIB == MO_NETLIB_MONGOOSE #include "mongoose.h" #include @@ -23,7 +23,7 @@ std::array connectors {{1}}; struct mg_mgr mgr; MicroOcpp::MOcppMongooseClient *osock; -#elif MOCPP_NETLIB == MOCPP_NETLIB_WASM +#elif MO_NETLIB == MO_NETLIB_WASM #include #include @@ -33,7 +33,7 @@ MicroOcpp::MOcppMongooseClient *osock; MicroOcpp::Connection *conn = nullptr; #else -#error Please ensure that build flag MOCPP_NETLIB is set as MOCPP_NETLIB_MONGOOSE or MOCPP_NETLIB_WASM +#error Please ensure that build flag MO_NETLIB is set as MO_NETLIB_MONGOOSE or MO_NETLIB_WASM #endif /* @@ -59,7 +59,7 @@ void app_loop() { } } -#if MOCPP_NETLIB == MOCPP_NETLIB_MONGOOSE +#if MO_NETLIB == MO_NETLIB_MONGOOSE int main() { mg_log_set(MG_LL_INFO); @@ -89,7 +89,7 @@ int main() { return 0; } -#elif MOCPP_NETLIB == MOCPP_NETLIB_WASM +#elif MO_NETLIB == MO_NETLIB_WASM int main() { diff --git a/src/net_mongoose.cpp b/src/net_mongoose.cpp index 8b7bda2..a956675 100644 --- a/src/net_mongoose.cpp +++ b/src/net_mongoose.cpp @@ -33,23 +33,23 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { const char *final_headers = DEFAULT_HEADER CORS_HEADERS; struct mg_str json = message_data->body; - MOCPP_DBG_VERBOSE("%.*s", 20, message_data->uri.ptr); + MO_DBG_VERBOSE("%.*s", 20, message_data->uri.ptr); MicroOcpp::Method method = MicroOcpp::Method::UNDEFINED; if (!mg_vcasecmp(&message_data->method, "POST")) { method = MicroOcpp::Method::POST; - MOCPP_DBG_VERBOSE("POST"); + MO_DBG_VERBOSE("POST"); } else if (!mg_vcasecmp(&message_data->method, "GET")) { method = MicroOcpp::Method::GET; - MOCPP_DBG_VERBOSE("GET"); + MO_DBG_VERBOSE("GET"); } //start different api endpoints if(mg_http_match_uri(message_data, "/api/websocket")){ - MOCPP_DBG_VERBOSE("query websocket"); - auto webSocketPingIntervalInt = MicroOcpp::declareConfiguration("WebSocketPingInterval", 10, MOCPP_WSCONN_FN); - auto reconnectIntervalInt = MicroOcpp::declareConfiguration(MOCPP_CONFIG_EXT_PREFIX "ReconnectInterval", 30, MOCPP_WSCONN_FN); + MO_DBG_VERBOSE("query websocket"); + auto webSocketPingIntervalInt = MicroOcpp::declareConfiguration("WebSocketPingInterval", 10, MO_WSCONN_FN); + auto reconnectIntervalInt = MicroOcpp::declareConfiguration(MO_CONFIG_EXT_PREFIX "ReconnectInterval", 30, MO_WSCONN_FN); if (method == MicroOcpp::Method::POST) { if (auto val = mg_json_get_str(json, "$.backendUrl")) { @@ -61,6 +61,7 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { if (auto val = mg_json_get_str(json, "$.authorizationKey")) { ao_sock->setAuthKey(val); } + ao_sock->reloadConfigs(); { auto val = mg_json_get_long(json, "$.pingInterval", -1); if (val > 0) { @@ -74,7 +75,7 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { } } if (auto val = mg_json_get_str(json, "$.dnsUrl")) { - MOCPP_DBG_WARN("dnsUrl not implemented"); + MO_DBG_WARN("dnsUrl not implemented"); (void)val; } MicroOcpp::configuration_save(); diff --git a/src/net_mongoose.h b/src/net_mongoose.h index 6020a46..3a5f095 100644 --- a/src/net_mongoose.h +++ b/src/net_mongoose.h @@ -1,7 +1,7 @@ -#ifndef MOCPP_NET_MONGOOSE_H -#define MOCPP_NET_MONGOOSE_H +#ifndef MO_NET_MONGOOSE_H +#define MO_NET_MONGOOSE_H -#if MOCPP_NETLIB == MOCPP_NETLIB_MONGOOSE +#if MO_NETLIB == MO_NETLIB_MONGOOSE #include "mongoose.h" @@ -13,6 +13,6 @@ void server_initialize(MicroOcpp::MOcppMongooseClient *osock); void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data); -#endif //MOCPP_NETLIB == MOCPP_NETLIB_MONGOOSE +#endif //MO_NETLIB == MO_NETLIB_MONGOOSE #endif diff --git a/src/net_wasm.cpp b/src/net_wasm.cpp index b784b37..f5d40f6 100644 --- a/src/net_wasm.cpp +++ b/src/net_wasm.cpp @@ -28,7 +28,7 @@ class WasmOcppConnection : public Connection { std::shared_ptr> setting_backend_url; std::shared_ptr> setting_cb_id; std::shared_ptr> setting_auth_key; -#if !MOCPP_CA_CERT_LOCAL +#if !MO_CA_CERT_LOCAL std::shared_ptr> setting_ca_cert; #endif unsigned long last_status_dbg_msg {0}, last_recv {0}; @@ -48,7 +48,7 @@ class WasmOcppConnection : public Connection { basic_auth64.clear(); if (backend_url.empty()) { - MOCPP_DBG_DEBUG("empty URL closes connection"); + MO_DBG_DEBUG("empty URL closes connection"); return; } else { url = backend_url; @@ -63,7 +63,7 @@ class WasmOcppConnection : public Connection { if (!auth_key.empty()) { std::string token = cb_id + ":" + auth_key; - MOCPP_DBG_DEBUG("auth Token=%s", token.c_str()); + MO_DBG_DEBUG("auth Token=%s", token.c_str()); unsigned int base64_length = encode_base64_length(token.length()); std::vector base64 (base64_length + 1); @@ -71,11 +71,11 @@ class WasmOcppConnection : public Connection { // encode_base64() places a null terminator automatically, because the output is a string base64_length = encode_base64((const unsigned char*) token.c_str(), token.length(), &base64[0]); - MOCPP_DBG_DEBUG("auth64 len=%u, auth64 Token=%s", base64_length, &base64[0]); + MO_DBG_DEBUG("auth64 len=%u, auth64 Token=%s", base64_length, &base64[0]); basic_auth64 = (const char*) &base64[0]; } else { - MOCPP_DBG_DEBUG("no authentication"); + MO_DBG_DEBUG("no authentication"); (void) 0; } } @@ -86,16 +86,16 @@ class WasmOcppConnection : public Connection { //WS successfully connected? if (!isConnectionOpen()) { - MOCPP_DBG_DEBUG("WS unconnected"); + MO_DBG_DEBUG("WS unconnected"); } else if (mocpp_tick_ms() - last_recv >= (ws_ping_interval && *ws_ping_interval > 0 ? (*ws_ping_interval * 1000UL) : 0UL) + WS_UNRESPONSIVE_THRESHOLD_MS) { //WS connected but unresponsive - MOCPP_DBG_DEBUG("WS unresponsive"); + MO_DBG_DEBUG("WS unresponsive"); } } if (websocket && isConnectionOpen() && stale_timeout && *stale_timeout > 0 && mocpp_tick_ms() - last_recv >= (*stale_timeout * 1000UL)) { - MOCPP_DBG_INFO("connection %s -- stale, reconnect", url.c_str()); + MO_DBG_INFO("connection %s -- stale, reconnect", url.c_str()); reconnect(); return; } @@ -103,7 +103,7 @@ class WasmOcppConnection : public Connection { if (websocket && isConnectionOpen() && ws_ping_interval && *ws_ping_interval > 0 && mocpp_tick_ms() - last_hb >= (*ws_ping_interval * 1000UL)) { last_hb = mocpp_tick_ms(); - MOCPP_DBG_VERBOSE("omit heartbeat"); + MO_DBG_VERBOSE("omit heartbeat"); } if (websocket != NULL) { //connection pointer != NULL means that the socket is still open @@ -124,7 +124,7 @@ class WasmOcppConnection : public Connection { return; } - MOCPP_DBG_DEBUG("(re-)connect to %s", url.c_str()); + MO_DBG_DEBUG("(re-)connect to %s", url.c_str()); last_reconnection_attempt = mocpp_tick_ms(); @@ -137,7 +137,7 @@ class WasmOcppConnection : public Connection { websocket = emscripten_websocket_new(&attr); if (websocket < 0) { - MOCPP_DBG_ERR("emscripten_websocket_new: %i", websocket); + MO_DBG_ERR("emscripten_websocket_new: %i", websocket); websocket = 0; } @@ -150,14 +150,14 @@ class WasmOcppConnection : public Connection { this, [] (int eventType, const EmscriptenWebSocketOpenEvent *websocketEvent, void *userData) -> EM_BOOL { WasmOcppConnection *conn = reinterpret_cast(userData); - MOCPP_DBG_DEBUG("on open eventType: %i", eventType); - MOCPP_DBG_INFO("connection %s -- connected!", conn->getUrl()); + MO_DBG_DEBUG("on open eventType: %i", eventType); + MO_DBG_INFO("connection %s -- connected!", conn->getUrl()); conn->setConnectionOpen(true); conn->updateRcvTimer(); return true; }); if (ret_open < 0) { - MOCPP_DBG_ERR("emscripten_websocket_set_onopen_callback: %i", ret_open); + MO_DBG_ERR("emscripten_websocket_set_onopen_callback: %i", ret_open); } auto ret_message = emscripten_websocket_set_onmessage_callback( @@ -165,16 +165,16 @@ class WasmOcppConnection : public Connection { this, [] (int eventType, const EmscriptenWebSocketMessageEvent *websocketEvent, void *userData) -> EM_BOOL { WasmOcppConnection *conn = reinterpret_cast(userData); - MOCPP_DBG_DEBUG("evenType: %i", eventType); - MOCPP_DBG_DEBUG("txt: %s", websocketEvent->data ? (const char*) websocketEvent->data : "undefined"); + MO_DBG_DEBUG("evenType: %i", eventType); + MO_DBG_DEBUG("txt: %s", websocketEvent->data ? (const char*) websocketEvent->data : "undefined"); if (!conn->getReceiveTXTcallback()((const char*) websocketEvent->data, websocketEvent->numBytes)) { - MOCPP_DBG_WARN("processing input message failed"); + MO_DBG_WARN("processing input message failed"); } conn->updateRcvTimer(); return true; }); if (ret_message < 0) { - MOCPP_DBG_ERR("emscripten_websocket_set_onmessage_callback: %i", ret_message); + MO_DBG_ERR("emscripten_websocket_set_onmessage_callback: %i", ret_message); } auto ret_err = emscripten_websocket_set_onerror_callback( @@ -182,13 +182,13 @@ class WasmOcppConnection : public Connection { this, [] (int eventType, const EmscriptenWebSocketErrorEvent *websocketEvent, void *userData) -> EM_BOOL { WasmOcppConnection *conn = reinterpret_cast(userData); - MOCPP_DBG_DEBUG("on error eventType: %i", eventType); - MOCPP_DBG_INFO("connection %s -- %s", conn->getUrl(), "error"); + MO_DBG_DEBUG("on error eventType: %i", eventType); + MO_DBG_INFO("connection %s -- %s", conn->getUrl(), "error"); conn->cleanConnection(); return true; }); if (ret_open < 0) { - MOCPP_DBG_ERR("emscripten_websocket_set_onopen_callback: %i", ret_open); + MO_DBG_ERR("emscripten_websocket_set_onopen_callback: %i", ret_open); } auto ret_close = emscripten_websocket_set_onclose_callback( @@ -196,13 +196,13 @@ class WasmOcppConnection : public Connection { this, [] (int eventType, const EmscriptenWebSocketCloseEvent *websocketEvent, void *userData) -> EM_BOOL { WasmOcppConnection *conn = reinterpret_cast(userData); - MOCPP_DBG_DEBUG("on close eventType: %i", eventType); - MOCPP_DBG_INFO("connection %s -- %s", conn->getUrl(), "closed"); + MO_DBG_DEBUG("on close eventType: %i", eventType); + MO_DBG_INFO("connection %s -- %s", conn->getUrl(), "closed"); conn->cleanConnection(); return true; }); if (ret_open < 0) { - MOCPP_DBG_ERR("emscripten_websocket_set_onopen_callback: %i", ret_open); + MO_DBG_ERR("emscripten_websocket_set_onopen_callback: %i", ret_open); } } @@ -213,17 +213,17 @@ class WasmOcppConnection : public Connection { const char *auth_key_default) { setting_backend_url = declareConfiguration( - MOCPP_CONFIG_EXT_PREFIX "BackendUrl", backend_url_default, CONFIGURATION_VOLATILE, true, true, true); + MO_CONFIG_EXT_PREFIX "BackendUrl", backend_url_default, CONFIGURATION_VOLATILE, true, true, true); setting_cb_id = declareConfiguration( - MOCPP_CONFIG_EXT_PREFIX "ChargeBoxId", charge_box_id_default, CONFIGURATION_VOLATILE, true, true, true); + MO_CONFIG_EXT_PREFIX "ChargeBoxId", charge_box_id_default, CONFIGURATION_VOLATILE, true, true, true); setting_auth_key = declareConfiguration( "AuthorizationKey", auth_key_default, CONFIGURATION_VOLATILE, true, true, true); ws_ping_interval = declareConfiguration( "WebSocketPingInterval", 5, CONFIGURATION_VOLATILE); reconnect_interval = declareConfiguration( - MOCPP_CONFIG_EXT_PREFIX "ReconnectInterval", 10, CONFIGURATION_VOLATILE); + MO_CONFIG_EXT_PREFIX "ReconnectInterval", 10, CONFIGURATION_VOLATILE); stale_timeout = declareConfiguration( - MOCPP_CONFIG_EXT_PREFIX "StaleTimeout", 300, CONFIGURATION_VOLATILE); + MO_CONFIG_EXT_PREFIX "StaleTimeout", 300, CONFIGURATION_VOLATILE); configuration_save(); @@ -234,7 +234,7 @@ class WasmOcppConnection : public Connection { auth_key = setting_auth_key && *setting_auth_key ? *setting_auth_key : (auth_key_default ? auth_key_default : ""); - MOCPP_DBG_DEBUG("connection initialized"); + MO_DBG_DEBUG("connection initialized"); maintainWsConn(); } @@ -256,7 +256,7 @@ class WasmOcppConnection : public Connection { } if (auto ret = emscripten_websocket_send_utf8_text(websocket, out.c_str()) < 0) { - MOCPP_DBG_ERR("emscripten_websocket_send_utf8_text: %i", ret); + MO_DBG_ERR("emscripten_websocket_send_utf8_text: %i", ret); return false; } @@ -273,7 +273,7 @@ class WasmOcppConnection : public Connection { void setBackendUrl(const char *backend_url_cstr) { if (!backend_url_cstr) { - MOCPP_DBG_ERR("invalid argument"); + MO_DBG_ERR("invalid argument"); return; } backend_url = backend_url_cstr; @@ -290,7 +290,7 @@ class WasmOcppConnection : public Connection { void setChargeBoxId(const char *cb_id_cstr) { if (!cb_id_cstr) { - MOCPP_DBG_ERR("invalid argument"); + MO_DBG_ERR("invalid argument"); return; } cb_id = cb_id_cstr; @@ -307,7 +307,7 @@ class WasmOcppConnection : public Connection { void setAuthKey(const char *auth_key_cstr) { if (!auth_key_cstr) { - MOCPP_DBG_ERR("invalid argument"); + MO_DBG_ERR("invalid argument"); return; } auth_key = auth_key_cstr; @@ -328,7 +328,7 @@ class WasmOcppConnection : public Connection { } auto ret = emscripten_websocket_close(websocket, 1000, "reconnect"); if (ret < 0) { - MOCPP_DBG_ERR("emscripten_websocket_close: %i", ret); + MO_DBG_ERR("emscripten_websocket_close: %i", ret); } setConnectionOpen(false); } @@ -377,12 +377,12 @@ MicroOcpp::Connection *wasm_ocpp_connection_init(const char *backend_url_default return wasm_ocpp_connection_instance; } -#define MOCPP_WASM_RESP_BUF_SIZE 1024 -char wasm_resp_buf [MOCPP_WASM_RESP_BUF_SIZE] = {'\0'}; +#define MO_WASM_RESP_BUF_SIZE 1024 +char wasm_resp_buf [MO_WASM_RESP_BUF_SIZE] = {'\0'}; //exported to WebAssembly extern "C" char* mocpp_wasm_api_call(const char *endpoint, const char *method, const char *body) { - MOCPP_DBG_DEBUG("API call: %s, %s, %s", endpoint, method, body); + MO_DBG_DEBUG("API call: %s, %s, %s", endpoint, method, body); auto method_parsed = Method::UNDEFINED; if (!strcmp(method, "GET")) { @@ -395,20 +395,20 @@ extern "C" char* mocpp_wasm_api_call(const char *endpoint, const char *method, c if (!strcmp(endpoint, "/websocket")) { sprintf(wasm_resp_buf, "%s", "{}"); if (!wasm_ocpp_connection_instance) { - MOCPP_DBG_ERR("no websocket instance"); + MO_DBG_ERR("no websocket instance"); return nullptr; } StaticJsonDocument<512> request; if (*body) { auto err = deserializeJson(request, body); if (err) { - MOCPP_DBG_WARN("malformatted body: %s", err.c_str()); + MO_DBG_WARN("malformatted body: %s", err.c_str()); return nullptr; } } auto webSocketPingInterval = declareConfiguration("WebSocketPingInterval", 5, CONFIGURATION_VOLATILE); - auto reconnectInterval = declareConfiguration(MOCPP_CONFIG_EXT_PREFIX "ReconnectInterval", 10, CONFIGURATION_VOLATILE); + auto reconnectInterval = declareConfiguration(MO_CONFIG_EXT_PREFIX "ReconnectInterval", 10, CONFIGURATION_VOLATILE); if (method_parsed == Method::POST) { if (request.containsKey("backendUrl")) { @@ -427,7 +427,7 @@ extern "C" char* mocpp_wasm_api_call(const char *endpoint, const char *method, c *reconnectInterval = request["reconnectInterval"] | 0; } if (request.containsKey("dnsUrl")) { - MOCPP_DBG_WARN("dnsUrl not implemented"); + MO_DBG_WARN("dnsUrl not implemented"); (void)0; } MicroOcpp::configuration_save(); @@ -440,19 +440,19 @@ extern "C" char* mocpp_wasm_api_call(const char *endpoint, const char *method, c response["pingInterval"] = webSocketPingInterval ? (int) *webSocketPingInterval : 0; response["reconnectInterval"] = reconnectInterval ? (int) *reconnectInterval : 0; - serializeJson(response, wasm_resp_buf, MOCPP_WASM_RESP_BUF_SIZE); + serializeJson(response, wasm_resp_buf, MO_WASM_RESP_BUF_SIZE); return wasm_resp_buf; } //all other endpoints - int status = mocpp_api_call(endpoint, method_parsed, body, wasm_resp_buf, MOCPP_WASM_RESP_BUF_SIZE); + int status = mocpp_api_call(endpoint, method_parsed, body, wasm_resp_buf, MO_WASM_RESP_BUF_SIZE); if (status == 200) { //200: HTTP status code Success - MOCPP_DBG_DEBUG("API resp: %s", wasm_resp_buf); + MO_DBG_DEBUG("API resp: %s", wasm_resp_buf); return wasm_resp_buf; } else { - MOCPP_DBG_DEBUG("API err: %i", status); + MO_DBG_DEBUG("API err: %i", status); return nullptr; } } diff --git a/src/net_wasm.h b/src/net_wasm.h index 613f590..4184359 100644 --- a/src/net_wasm.h +++ b/src/net_wasm.h @@ -1,12 +1,12 @@ -#ifndef MOCPP_NET_WASM_H -#define MOCPP_NET_WASM_H +#ifndef MO_NET_WASM_H +#define MO_NET_WASM_H -#if MOCPP_NETLIB == MOCPP_NETLIB_WASM +#if MO_NETLIB == MO_NETLIB_WASM #include MicroOcpp::Connection *wasm_ocpp_connection_init(const char *backend_url_default, const char *charge_box_id_default, const char *auth_key_default); -#endif //MOCPP_NETLIB == MOCPP_NETLIB_WASM +#endif //MO_NETLIB == MO_NETLIB_WASM #endif From 55476326fdba53476434bf47cdaa8e4c20a65c98 Mon Sep 17 00:00:00 2001 From: Matthias Akstaller <63792403+matth-x@users.noreply.github.com> Date: Sun, 5 Nov 2023 12:58:28 +0100 Subject: [PATCH 21/41] Add build checks (#10) * build on ubuntu * add submodules statement * add Docker build * add WebAssembly build * fix Action step * fix WASM integration * fix WASM integration Configs * update WASM integration Connection interface * update README.md * more concise workflow names --- .github/workflows/build_docker.yml | 26 ++++ .github/workflows/build_ubuntu.yml | 28 ++++ .github/workflows/build_wasm.yml | 28 ++++ README.md | 4 + src/net_wasm.cpp | 216 ++++++++++++++--------------- 5 files changed, 189 insertions(+), 113 deletions(-) create mode 100644 .github/workflows/build_docker.yml create mode 100644 .github/workflows/build_ubuntu.yml create mode 100644 .github/workflows/build_wasm.yml diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml new file mode 100644 index 0000000..fe91653 --- /dev/null +++ b/.github/workflows/build_docker.yml @@ -0,0 +1,26 @@ +name: Docker + +on: + push: + branches: + - master + - develop + + pull_request: + +jobs: + + compile-ubuntu-docker: + name: Build Docker image + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Get Docker + run: | + sudo apt update + sudo apt install docker + - name: Build Docker image + run: docker build -t matthx/microocppsimulator:latest . diff --git a/.github/workflows/build_ubuntu.yml b/.github/workflows/build_ubuntu.yml new file mode 100644 index 0000000..6d23281 --- /dev/null +++ b/.github/workflows/build_ubuntu.yml @@ -0,0 +1,28 @@ +name: Ubuntu + +on: + push: + branches: + - master + - develop + + pull_request: + +jobs: + + compile-ubuntu-native: + name: Compile for Ubuntu + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Get compiler toolchain + run: | + sudo apt update + sudo apt install cmake libssl-dev build-essential + - name: Generate CMake files + run: cmake -S . -B ./build + - name: Compile + run: cmake --build ./build -j 16 --target mo_simulator diff --git a/.github/workflows/build_wasm.yml b/.github/workflows/build_wasm.yml new file mode 100644 index 0000000..ec04559 --- /dev/null +++ b/.github/workflows/build_wasm.yml @@ -0,0 +1,28 @@ +name: WebAssembly + +on: + push: + branches: + - master + - develop + + pull_request: + +jobs: + + compile-esmscripten: + name: Compile for WebAssembly + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Set up emscripten + uses: mymindstorm/setup-emsdk@v11 + - name: Verify + run: emcc -v + - name: Generate CMake files + run: emcmake cmake -S . -B ./build + - name: Compile + run: cmake --build ./build -j 16 --target mo_simulator_wasm diff --git a/README.md b/README.md index a74b47b..8a5a365 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Icon   MicroOcppSimulator +[![Build (Ubuntu)](https://github.com/matth-x/MicroOcppSimulator/workflows/Ubuntu/badge.svg)]((https://github.com/matth-x/MicroOcppSimulator/actions)) +[![Build (Docker)](https://github.com/matth-x/MicroOcppSimulator/workflows/Docker/badge.svg)]((https://github.com/matth-x/MicroOcppSimulator/actions)) +[![Build (WebAssembly)](https://github.com/matth-x/MicroOcppSimulator/workflows/WebAssembly/badge.svg)]((https://github.com/matth-x/MicroOcppSimulator/actions)) + **Formerly ArduinoOcppSimulator**: *see the statement on the former ArduinoOcpp [project site](https://github.com/matth-x/MicroOcpp) for more details.* Tester / Demo App for the [MicroOcpp](https://github.com/matth-x/MicroOcpp) Client, running on native Ubuntu, WSL, or MSYS2. **New** (August 2023): the Simulator has been ported to WebAssembly and runs natively in the browser too: [Try it](https://demo.micro-ocpp.com/) diff --git a/src/net_wasm.cpp b/src/net_wasm.cpp index f5d40f6..83a7507 100644 --- a/src/net_wasm.cpp +++ b/src/net_wasm.cpp @@ -24,62 +24,20 @@ class WasmOcppConnection : public Connection { std::string url; //url = backend_url + '/' + cb_id std::string auth_key; std::string basic_auth64; - std::string ca_cert; - std::shared_ptr> setting_backend_url; - std::shared_ptr> setting_cb_id; - std::shared_ptr> setting_auth_key; -#if !MO_CA_CERT_LOCAL - std::shared_ptr> setting_ca_cert; -#endif + std::shared_ptr setting_backend_url_str; + std::shared_ptr setting_cb_id_str; + std::shared_ptr setting_auth_key_str; unsigned long last_status_dbg_msg {0}, last_recv {0}; - std::shared_ptr> reconnect_interval; //minimum time between two connect trials in s + std::shared_ptr reconnect_interval_int; //minimum time between two connect trials in s unsigned long last_reconnection_attempt {-1UL / 2UL}; - std::shared_ptr> stale_timeout; //inactivity period after which the connection will be closed - std::shared_ptr> ws_ping_interval; //heartbeat intervall in s. 0 sets hb off + std::shared_ptr stale_timeout_int; //inactivity period after which the connection will be closed + std::shared_ptr ws_ping_interval_int; //heartbeat intervall in s. 0 sets hb off unsigned long last_hb {0}; bool connection_established {false}; unsigned long last_connection_established {-1UL / 2UL}; bool connection_closing {false}; ReceiveTXTcallback receiveTXTcallback = [] (const char *, size_t) {return false;}; - bool credentials_changed {true}; //set credentials to be reloaded - void reload_credentials() { - url.clear(); - basic_auth64.clear(); - - if (backend_url.empty()) { - MO_DBG_DEBUG("empty URL closes connection"); - return; - } else { - url = backend_url; - - if (url.back() != '/' && !cb_id.empty()) { - url.append("/"); - } - - url.append(cb_id); - } - - if (!auth_key.empty()) { - std::string token = cb_id + ":" + auth_key; - - MO_DBG_DEBUG("auth Token=%s", token.c_str()); - - unsigned int base64_length = encode_base64_length(token.length()); - std::vector base64 (base64_length + 1); - - // encode_base64() places a null terminator automatically, because the output is a string - base64_length = encode_base64((const unsigned char*) token.c_str(), token.length(), &base64[0]); - - MO_DBG_DEBUG("auth64 len=%u, auth64 Token=%s", base64_length, &base64[0]); - - basic_auth64 = (const char*) &base64[0]; - } else { - MO_DBG_DEBUG("no authentication"); - (void) 0; - } - } - void maintainWsConn() { if (mocpp_tick_ms() - last_status_dbg_msg >= DEBUG_MSG_INTERVAL) { last_status_dbg_msg = mocpp_tick_ms(); @@ -87,21 +45,21 @@ class WasmOcppConnection : public Connection { //WS successfully connected? if (!isConnectionOpen()) { MO_DBG_DEBUG("WS unconnected"); - } else if (mocpp_tick_ms() - last_recv >= (ws_ping_interval && *ws_ping_interval > 0 ? (*ws_ping_interval * 1000UL) : 0UL) + WS_UNRESPONSIVE_THRESHOLD_MS) { + } 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 && *stale_timeout > 0 && mocpp_tick_ms() - last_recv >= (*stale_timeout * 1000UL)) { + 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 && *ws_ping_interval > 0 && mocpp_tick_ms() - last_hb >= (*ws_ping_interval * 1000UL)) { + 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(); MO_DBG_VERBOSE("omit heartbeat"); } @@ -110,17 +68,12 @@ class WasmOcppConnection : public Connection { return; } - if (credentials_changed) { - reload_credentials(); - credentials_changed = false; - } - if (url.empty()) { //cannot open OCPP connection: credentials missing return; } - if (reconnect_interval && *reconnect_interval > 0 && mocpp_tick_ms() - last_reconnection_attempt < (*reconnect_interval * 1000UL)) { + if (reconnect_interval_int && reconnect_interval_int->getInt() > 0 && mocpp_tick_ms() - last_reconnection_attempt < (reconnect_interval_int->getInt() * 1000UL)) { return; } @@ -206,33 +159,37 @@ class WasmOcppConnection : public Connection { } } + void reconnect() { + if (!websocket) { + return; + } + auto ret = emscripten_websocket_close(websocket, 1000, "reconnect"); + if (ret < 0) { + MO_DBG_ERR("emscripten_websocket_close: %i", ret); + } + setConnectionOpen(false); + } + public: WasmOcppConnection( - const char *backend_url_default, - const char *charge_box_id_default, - const char *auth_key_default) { - - setting_backend_url = declareConfiguration( - MO_CONFIG_EXT_PREFIX "BackendUrl", backend_url_default, CONFIGURATION_VOLATILE, true, true, true); - setting_cb_id = declareConfiguration( - MO_CONFIG_EXT_PREFIX "ChargeBoxId", charge_box_id_default, CONFIGURATION_VOLATILE, true, true, true); - setting_auth_key = declareConfiguration( - "AuthorizationKey", auth_key_default, CONFIGURATION_VOLATILE, true, true, true); - ws_ping_interval = declareConfiguration( + const char *backend_url_factory, + const char *charge_box_id_factory, + const char *auth_key_factory) { + + setting_backend_url_str = declareConfiguration( + MO_CONFIG_EXT_PREFIX "BackendUrl", backend_url_factory, CONFIGURATION_VOLATILE, true, true); + setting_cb_id_str = declareConfiguration( + MO_CONFIG_EXT_PREFIX "ChargeBoxId", charge_box_id_factory, CONFIGURATION_VOLATILE, true, true); + setting_auth_key_str = declareConfiguration( + "AuthorizationKey", auth_key_factory, CONFIGURATION_VOLATILE, true, true); + ws_ping_interval_int = declareConfiguration( "WebSocketPingInterval", 5, CONFIGURATION_VOLATILE); - reconnect_interval = declareConfiguration( + reconnect_interval_int = declareConfiguration( MO_CONFIG_EXT_PREFIX "ReconnectInterval", 10, CONFIGURATION_VOLATILE); - stale_timeout = declareConfiguration( + stale_timeout_int = declareConfiguration( MO_CONFIG_EXT_PREFIX "StaleTimeout", 300, CONFIGURATION_VOLATILE); - configuration_save(); - - backend_url = setting_backend_url && *setting_backend_url ? *setting_backend_url : - (backend_url_default ? backend_url_default : ""); - cb_id = setting_cb_id && *setting_cb_id ? *setting_cb_id : - (charge_box_id_default ? charge_box_id_default : ""); - auth_key = setting_auth_key && *setting_auth_key ? *setting_auth_key : - (auth_key_default ? auth_key_default : ""); + reloadConfigs(); //load WS creds with configs values MO_DBG_DEBUG("connection initialized"); @@ -250,12 +207,12 @@ class WasmOcppConnection : public Connection { maintainWsConn(); } - bool sendTXT(std::string &out) override { + bool sendTXT(const char *msg, size_t length) override { if (!websocket || !isConnectionOpen()) { return false; } - if (auto ret = emscripten_websocket_send_utf8_text(websocket, out.c_str()) < 0) { + if (auto ret = emscripten_websocket_send_utf8_text(websocket, msg) < 0) { MO_DBG_ERR("emscripten_websocket_send_utf8_text: %i", ret); return false; } @@ -276,16 +233,11 @@ class WasmOcppConnection : public Connection { MO_DBG_ERR("invalid argument"); return; } - backend_url = backend_url_cstr; - if (setting_backend_url) { - *setting_backend_url = backend_url_cstr; + if (setting_backend_url_str) { + setting_backend_url_str->setString(backend_url_cstr); configuration_save(); } - - credentials_changed = true; //reload composed credentials when reconnecting the next time - - reconnect(); } void setChargeBoxId(const char *cb_id_cstr) { @@ -293,16 +245,11 @@ class WasmOcppConnection : public Connection { MO_DBG_ERR("invalid argument"); return; } - cb_id = cb_id_cstr; - if (setting_cb_id) { - *setting_cb_id = cb_id_cstr; + if (setting_cb_id_str) { + setting_cb_id_str->setString(cb_id_cstr); configuration_save(); } - - credentials_changed = true; //reload composed credentials when reconnecting the next time - - reconnect(); } void setAuthKey(const char *auth_key_cstr) { @@ -310,33 +257,75 @@ class WasmOcppConnection : public Connection { MO_DBG_ERR("invalid argument"); return; } - auth_key = auth_key_cstr; - if (setting_auth_key) { - *setting_auth_key = auth_key_cstr; + if (setting_auth_key_str) { + setting_auth_key_str->setString(auth_key_cstr); configuration_save(); } + } - credentials_changed = true; //reload composed credentials when reconnecting the next time + void reloadConfigs() { - reconnect(); - } - - void reconnect() { - if (!websocket) { + reconnect(); //closes WS connection; will be reopened in next maintainWsConn execution + + /* + * reload WS credentials from configs + */ + 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_str) { + auth_key = setting_auth_key_str->getString(); + } + + /* + * determine new URL and auth token with updated WS credentials + */ + + url.clear(); + basic_auth64.clear(); + + if (backend_url.empty()) { + MO_DBG_DEBUG("empty URL closes connection"); return; + } else { + url = backend_url; + + if (url.back() != '/' && !cb_id.empty()) { + url.append("/"); + } + + url.append(cb_id); } - auto ret = emscripten_websocket_close(websocket, 1000, "reconnect"); - if (ret < 0) { - MO_DBG_ERR("emscripten_websocket_close: %i", ret); + + if (!auth_key.empty()) { + std::string token = cb_id + ":" + auth_key; + + MO_DBG_DEBUG("auth Token=%s", token.c_str()); + + unsigned int base64_length = encode_base64_length(token.length()); + std::vector base64 (base64_length + 1); + + // encode_base64() places a null terminator automatically, because the output is a string + base64_length = encode_base64((const unsigned char*) token.c_str(), token.length(), &base64[0]); + + MO_DBG_DEBUG("auth64 len=%u, auth64 Token=%s", base64_length, &base64[0]); + + basic_auth64 = (const char*) &base64[0]; + } else { + MO_DBG_DEBUG("no authentication"); + (void) 0; } - setConnectionOpen(false); } const char *getBackendUrl() {return backend_url.c_str();} const char *getChargeBoxId() {return cb_id.c_str();} const char *getAuthKey() {return auth_key.c_str();} - const char *getCaCert() {return ca_cert.c_str();} const char *getUrl() {return url.c_str();} @@ -420,11 +409,12 @@ extern "C" char* mocpp_wasm_api_call(const char *endpoint, const char *method, c if (request.containsKey("authorizationKey")) { wasm_ocpp_connection_instance->setAuthKey(request["authorizationKey"] | ""); } + wasm_ocpp_connection_instance->reloadConfigs(); if (request.containsKey("pingInterval")) { - *webSocketPingInterval = request["pingInterval"] | 0; + webSocketPingInterval->setInt(request["pingInterval"] | 0); } if (request.containsKey("reconnectInterval")) { - *reconnectInterval = request["reconnectInterval"] | 0; + reconnectInterval->setInt(request["reconnectInterval"] | 0); } if (request.containsKey("dnsUrl")) { MO_DBG_WARN("dnsUrl not implemented"); @@ -438,13 +428,13 @@ extern "C" char* mocpp_wasm_api_call(const char *endpoint, const char *method, c response["chargeBoxId"] = wasm_ocpp_connection_instance->getChargeBoxId(); response["authorizationKey"] = wasm_ocpp_connection_instance->getAuthKey(); - response["pingInterval"] = webSocketPingInterval ? (int) *webSocketPingInterval : 0; - response["reconnectInterval"] = reconnectInterval ? (int) *reconnectInterval : 0; + response["pingInterval"] = webSocketPingInterval ? webSocketPingInterval->getInt() : 0; + response["reconnectInterval"] = reconnectInterval ? reconnectInterval->getInt() : 0; serializeJson(response, wasm_resp_buf, MO_WASM_RESP_BUF_SIZE); return wasm_resp_buf; } - //all other endpoints + //forward all other endpoints to main API int status = mocpp_api_call(endpoint, method_parsed, body, wasm_resp_buf, MO_WASM_RESP_BUF_SIZE); if (status == 200) { From 09851a08727089b1ed383d7c481ccd48eece5ca0 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Mon, 11 Dec 2023 12:00:55 +0100 Subject: [PATCH 22/41] change mo_store dir in Docker (fixes #12) --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 26afd91..0ac4d67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,10 +15,10 @@ WORKDIR /MicroOcppSimulator # Copy your application files to the container's working directory COPY . . -RUN git clone --recurse-submodules https://github.com/matth-x/MicroOcppSimulator -RUN cd MicroOcppSimulator && mkdir build && mkdir mo_store +RUN git submodule init && git submodule update +RUN mkdir mo_store RUN cmake -S . -B ./build -RUN cmake --build ./build -j 16 --target mo_simulator +RUN cmake --build ./build -j 16 --target mo_simulator -j 16 # Grant execute permissions to the shell script RUN chmod +x /MicroOcppSimulator/build/mo_simulator @@ -27,4 +27,4 @@ RUN chmod +x /MicroOcppSimulator/build/mo_simulator EXPOSE 8000 # Run the shell script inside the container -CMD ["./build/mo_simulator"] \ No newline at end of file +CMD ["./build/mo_simulator"] From c7420ff7d415018aededf04edeebe6f2411eed1c Mon Sep 17 00:00:00 2001 From: Matthias Akstaller <63792403+matth-x@users.noreply.github.com> Date: Fri, 1 Mar 2024 14:17:43 +0100 Subject: [PATCH 23/41] Update README.md --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8a5a365..d2d8748 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ -# Icon   MicroOcppSimulator +# Icon   MicroOcppSimulator [![Build (Ubuntu)](https://github.com/matth-x/MicroOcppSimulator/workflows/Ubuntu/badge.svg)]((https://github.com/matth-x/MicroOcppSimulator/actions)) [![Build (Docker)](https://github.com/matth-x/MicroOcppSimulator/workflows/Docker/badge.svg)]((https://github.com/matth-x/MicroOcppSimulator/actions)) [![Build (WebAssembly)](https://github.com/matth-x/MicroOcppSimulator/workflows/WebAssembly/badge.svg)]((https://github.com/matth-x/MicroOcppSimulator/actions)) -**Formerly ArduinoOcppSimulator**: *see the statement on the former ArduinoOcpp [project site](https://github.com/matth-x/MicroOcpp) for more details.* - -Tester / Demo App for the [MicroOcpp](https://github.com/matth-x/MicroOcpp) Client, running on native Ubuntu, WSL, or MSYS2. **New** (August 2023): the Simulator has been ported to WebAssembly and runs natively in the browser too: [Try it](https://demo.micro-ocpp.com/) +Tester / Demo App for the [MicroOcpp](https://github.com/matth-x/MicroOcpp) Client, running on native Ubuntu, WSL, WebAssembly or MSYS2. Online demo: [Try it](https://demo.micro-ocpp.com/) [![Screenshot](https://github.com/agruenb/arduino-ocpp-dashboard/blob/master/docs/img/status_page.png)](https://demo.micro-ocpp.com/) @@ -60,7 +58,6 @@ Navigate to the copy of the Simulator, prepare some necessary local folders and ```shell cd MicroOcppSimulator -mkdir build mkdir mo_store cmake -S . -B ./build cmake --build ./build -j 16 --target mo_simulator From 374e9e4bc1c01299affe79923f4fda057e0cd26e Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Tue, 5 Mar 2024 16:55:46 +0100 Subject: [PATCH 24/41] include default mo_store folder in repo --- .gitignore | 3 ++- Dockerfile | 1 - README.md | 3 +-- mo_store/README.md | 5 +++++ 4 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 mo_store/README.md diff --git a/.gitignore b/.gitignore index 7987594..cffd789 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ *.exe *.obj build_and_run.sh -mo_store +mo_store/* +!mo_store/README.md build diff --git a/Dockerfile b/Dockerfile index 0ac4d67..d7660f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,6 @@ WORKDIR /MicroOcppSimulator COPY . . RUN git submodule init && git submodule update -RUN mkdir mo_store RUN cmake -S . -B ./build RUN cmake --build ./build -j 16 --target mo_simulator -j 16 diff --git a/README.md b/README.md index d2d8748..f300808 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,10 @@ Navigate to the preferred installation directory or just to the home folder. Clo git clone --recurse-submodules https://github.com/matth-x/MicroOcppSimulator ``` -Navigate to the copy of the Simulator, prepare some necessary local folders and build: +Navigate to the copy of the Simulator and build: ```shell cd MicroOcppSimulator -mkdir mo_store cmake -S . -B ./build cmake --build ./build -j 16 --target mo_simulator ``` diff --git a/mo_store/README.md b/mo_store/README.md new file mode 100644 index 0000000..9af6d10 --- /dev/null +++ b/mo_store/README.md @@ -0,0 +1,5 @@ +### JSON key-value store + +MicroOcpp has a local storage for the persistency of the OCPP configurations, transactions and more. As MicroOcpp is initialized for the first time, this folder is populated with all stored objects. The storage format is JSON with mostly human-readable keys. Feel free to open the stored objects with your favorite JSON viewer and inspect them to learn more about MicroOcpp. + +To change the local storage folder in a productive environment, see the build flag `MO_FILENAME_PREFIX` in the CMakeLists.txt. The folder must already exist, MicroOcpp won't create a new folder at the specified location. From 66dee99f5052c3d4d4913a02370d189214896c53 Mon Sep 17 00:00:00 2001 From: Matthias Akstaller <63792403+matth-x@users.noreply.github.com> Date: Sun, 21 Apr 2024 21:25:44 +0200 Subject: [PATCH 25/41] add OCPP 2.0.1 preview (#15) * enable basic OCPP 2.0.1 * update v2.0.1 Tx * update v2.0.1 integration * change config name to OcppVersion --- CMakeLists.txt | 1 + lib/MicroOcpp | 2 +- lib/MicroOcppMongoose | 2 +- src/api.cpp | 2 +- src/evse.cpp | 62 ++++++++++++++++++++++++++++++++++--------- src/evse.h | 5 ++++ src/main.cpp | 46 +++++++++++++++++++++++++++++--- 7 files changed, 100 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dcee410..2d2b5f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,7 @@ add_compile_definitions( MO_TRAFFIC_OUT MO_DBG_LEVEL=MO_DL_INFO MO_FILENAME_PREFIX="./mo_store/" + MO_ENABLE_V201=1 ) add_executable(mo_simulator ${MO_SIM_SRC} ${MO_SIM_MG_SRC}) diff --git a/lib/MicroOcpp b/lib/MicroOcpp index cf532c4..76bc5b3 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit cf532c41d541efdde1b9f4a95963f2661b49ea70 +Subproject commit 76bc5b3be1e62fe9765a1c0243e9cb8c4987a47b diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose index 92ebea9..d9bda4a 160000 --- a/lib/MicroOcppMongoose +++ b/lib/MicroOcppMongoose @@ -1 +1 @@ -Subproject commit 92ebea9fe8554999b4d34c6a03fa12c0d9faec48 +Subproject commit d9bda4a38c59d3adeece054b1838d7d49c820e76 diff --git a/src/api.cpp b/src/api.cpp index 93e7d53..29b555d 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -25,7 +25,7 @@ bool str_match(const char *query, const char *pattern) { int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *body, char *resp_body, size_t resp_body_size) { - MO_DBG_DEBUG("process %s, %s: %s", + MO_DBG_VERBOSE("process %s, %s: %s", endpoint, method == MicroOcpp::Method::GET ? "GET" : method == MicroOcpp::Method::POST ? "POST" : "error", diff --git a/src/evse.cpp b/src/evse.cpp index bebfed6..e331961 100644 --- a/src/evse.cpp +++ b/src/evse.cpp @@ -1,15 +1,20 @@ +// matth-x/MicroOcppSimulator +// Copyright Matthias Akstaller 2022 - 2024 +// MIT License + #include "evse.h" #include #include #include #include +#include +#include #include +#include #include #include #include -#define SIMULATOR_FN MO_FILENAME_PREFIX "simulator.jsn" - Evse::Evse(unsigned int connectorId) : connectorId{connectorId} { } @@ -23,6 +28,21 @@ MicroOcpp::Connector *getConnector(unsigned int connectorId) { } void Evse::setup() { + +#if MO_ENABLE_V201 + if (auto context = getOcppContext()) { + if (context->getVersion().major == 2) { + //load some example variables for testing + + if (auto varService = context->getModel().getVariableService()) { + varService->declareVariable("AuthCtrlr", "LocalAuthorizeOffline", false, MO_VARIABLE_VOLATILE); + varService->declareVariable("OCPPCommCtrlr", "OfflineThreshold", false, MO_VARIABLE_VOLATILE); + varService->declareVariable("TxCtrlr", "StopTxOnInvalidId", false, MO_VARIABLE_VOLATILE); + } + } + } +#endif + auto connector = getConnector(connectorId); if (!connector) { MO_DBG_ERR("invalid state"); @@ -43,22 +63,22 @@ void Evse::setup() { MicroOcpp::configuration_load(SIMULATOR_FN); - connector->setConnectorPluggedInput([this] () -> bool { + setConnectorPluggedInput([this] () -> bool { return trackEvPluggedBool->getBool(); //return if J1772 is in State B or C - }); + }, connectorId); - connector->setEvReadyInput([this] () -> bool { + setEvReadyInput([this] () -> bool { return trackEvReadyBool->getBool(); //return if J1772 is in State C - }); + }, connectorId); - connector->setEvseReadyInput([this] () -> bool { + setEvseReadyInput([this] () -> bool { return trackEvseReadyBool->getBool(); - }); + }, connectorId); - connector->addErrorCodeInput([this] () -> const char* { + addErrorCodeInput([this] () -> const char* { const char *errorCode = nullptr; //if error is present, point to error code; any number of error code samplers can be added in this project return errorCode; - }); + }, connectorId); setEnergyMeterInput([this] () -> float { return simulate_energy; @@ -109,12 +129,11 @@ void Evse::loop() { if (auto connector = getConnector(connectorId)) { auto curStatus = connector->getStatus(); - if (status.compare(MicroOcpp::Ocpp16::cstrFromOcppEveState(curStatus))) { - status = MicroOcpp::Ocpp16::cstrFromOcppEveState(curStatus); + if (status.compare(MicroOcpp::cstrFromOcppEveState(curStatus))) { + status = MicroOcpp::cstrFromOcppEveState(curStatus); } } - bool simulate_isCharging = ocppPermitsCharge(connectorId) && trackEvPluggedBool->getBool() && trackEvReadyBool->getBool() && trackEvseReadyBool->getBool(); simulate_isCharging &= limit_power >= 720.f; //minimum charging current is 6A (720W for 120V grids) according to J1772 @@ -146,6 +165,23 @@ void Evse::presentNfcTag(const char *uid_cstr) { return; } +#if MO_ENABLE_V201 + if (auto context = getOcppContext()) { + if (context->getVersion().major == 2) { + if (auto txService = context->getModel().getTransactionService()) { + if (auto evse = txService->getEvse(connectorId)) { + if (evse->getTransaction() && evse->getTransaction()->isAuthorized) { + evse->endAuthorization(uid_cstr); + } else { + evse->beginAuthorization(uid_cstr); + } + return; + } + } + } + } +#endif + if (connector->getTransaction() && connector->getTransaction()->isActive()) { if (!uid.compare(connector->getTransaction()->getIdTag())) { connector->endTransaction(uid.c_str()); diff --git a/src/evse.h b/src/evse.h index d01c8f4..f0f3951 100644 --- a/src/evse.h +++ b/src/evse.h @@ -1,3 +1,7 @@ +// matth-x/MicroOcppSimulator +// Copyright Matthias Akstaller 2022 - 2024 +// MIT License + #ifndef EVSE_H #define EVSE_H @@ -5,6 +9,7 @@ #include #include +#define SIMULATOR_FN MO_FILENAME_PREFIX "simulator.jsn" class Evse { private: diff --git a/src/main.cpp b/src/main.cpp index 370e272..d501d20 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,7 @@ +// matth-x/MicroOcppSimulator +// Copyright Matthias Akstaller 2022 - 2024 +// MIT License + #include #include @@ -10,6 +14,8 @@ std::array connectors {{1,2}}; std::array connectors {{1}}; #endif +bool g_isOcpp201 = false; + #define MO_NETLIB_MONGOOSE 1 #define MO_NETLIB_WASM 2 @@ -39,10 +45,36 @@ MicroOcpp::Connection *conn = nullptr; /* * Setup MicroOcpp and API */ +void load_ocpp_version(std::shared_ptr filesystem) { + + MicroOcpp::configuration_init(filesystem); + + #if MO_ENABLE_V201 + { + auto protocolVersion_stored = MicroOcpp::declareConfiguration("OcppVersion", "1.6", SIMULATOR_FN, false, false, false); + MicroOcpp::configuration_load(SIMULATOR_FN); + if (!strcmp(protocolVersion_stored->getString(), "2.0.1")) { + //select OCPP 2.0.1 + g_isOcpp201 = true; + return; + } + } + #endif //MO_ENABLE_V201 + + g_isOcpp201 = false; +} + void app_setup(MicroOcpp::Connection& connection, std::shared_ptr filesystem) { mocpp_initialize(connection, - ChargerCredentials("Demo Charger", "My Company Ltd."), - filesystem); + g_isOcpp201 ? + ChargerCredentials::v201("MicroOcpp Simulator", "MicroOcpp") : + ChargerCredentials("MicroOcpp Simulator", "MicroOcpp"), + filesystem, + false, + g_isOcpp201 ? + MicroOcpp::ProtocolVersion{2,0,1} : + MicroOcpp::ProtocolVersion{1,6} + ); for (unsigned int i = 0; i < connectors.size(); i++) { connectors[i].setup(); @@ -69,13 +101,19 @@ int main() { auto filesystem = MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail); + load_ocpp_version(filesystem); + osock = new MicroOcpp::MOcppMongooseClient(&mgr, "ws://echo.websocket.events", "charger-01", "", "", - filesystem); - + filesystem, + g_isOcpp201 ? + MicroOcpp::ProtocolVersion{2,0,1} : + MicroOcpp::ProtocolVersion{1,6} + ); + server_initialize(osock); app_setup(*osock, filesystem); From edb62c228d1a23d32eb1c8b94f0a6df169f0a439 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:42:43 +0200 Subject: [PATCH 26/41] update dependencies --- lib/MicroOcpp | 2 +- lib/MicroOcppMongoose | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/MicroOcpp b/lib/MicroOcpp index 76bc5b3..80da31d 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit 76bc5b3be1e62fe9765a1c0243e9cb8c4987a47b +Subproject commit 80da31d92f39e666193c14ffbe11d1a01b84d8bd diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose index d9bda4a..2e5aa6a 160000 --- a/lib/MicroOcppMongoose +++ b/lib/MicroOcppMongoose @@ -1 +1 @@ -Subproject commit d9bda4a38c59d3adeece054b1838d7d49c820e76 +Subproject commit 2e5aa6a087f4af66077d4d10216583c7163051a2 From 93128ee22ac0073622b31f02e04ff12b17c8b1ca Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Wed, 1 May 2024 21:25:57 +0200 Subject: [PATCH 27/41] include mo_store in docker image (#16) --- .dockerignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index 96832fa..378eac2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1 @@ -mo_store -build \ No newline at end of file +build From ebf38f57715d6384ccd3f24b204ee648740d5a80 Mon Sep 17 00:00:00 2001 From: Albert Bru Corts Date: Fri, 3 May 2024 13:11:26 +0200 Subject: [PATCH 28/41] Updated Dockerfile base image --- Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index d7660f1..d90093e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,13 @@ -# Use Ubuntu as the base image -FROM ubuntu:latest +# Use Alpine Linux as the base image +FROM alpine:latest # Update package lists and install necessary dependencies -RUN apt-get update && apt-get install -y \ +RUN apk update && \ + apk add --no-cache \ git \ cmake \ - libssl-dev \ - build-essential \ - && rm -rf /var/lib/apt/lists/* + openssl-dev \ + build-base # Set the working directory inside the container WORKDIR /MicroOcppSimulator From 558d14f9a3057398145af1405af6264beb98560d Mon Sep 17 00:00:00 2001 From: Matthias Akstaller <63792403+matth-x@users.noreply.github.com> Date: Thu, 23 May 2024 13:05:46 +0200 Subject: [PATCH 29/41] integrate MbedTLS (#22) * use MbedTLS as the default TLS lib * update MO, clean CMakeLists * enable workflows * fix compiler warnings * update copyright notice * update dependencies * disable MbedTLS test suites * fix WASM compilation errors --- .github/workflows/build_docker.yml | 3 +- .github/workflows/build_ubuntu.yml | 3 +- .github/workflows/build_wasm.yml | 3 +- .gitmodules | 3 ++ CMakeLists.txt | 68 ++++++++++++++++++++++++------ lib/MicroOcpp | 2 +- lib/MicroOcppMongoose | 2 +- lib/mbedtls | 1 + src/api.cpp | 4 ++ src/api.h | 4 ++ src/evse.cpp | 2 +- src/evse.h | 2 +- src/main.cpp | 2 +- src/net_mongoose.cpp | 21 ++++----- src/net_mongoose.h | 4 ++ src/net_wasm.cpp | 19 +++------ src/net_wasm.h | 4 ++ 17 files changed, 100 insertions(+), 47 deletions(-) create mode 160000 lib/mbedtls diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index fe91653..5827bab 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -3,8 +3,7 @@ name: Docker on: push: branches: - - master - - develop + - main pull_request: diff --git a/.github/workflows/build_ubuntu.yml b/.github/workflows/build_ubuntu.yml index 6d23281..f7fa298 100644 --- a/.github/workflows/build_ubuntu.yml +++ b/.github/workflows/build_ubuntu.yml @@ -3,8 +3,7 @@ name: Ubuntu on: push: branches: - - master - - develop + - main pull_request: diff --git a/.github/workflows/build_wasm.yml b/.github/workflows/build_wasm.yml index ec04559..b66fa97 100644 --- a/.github/workflows/build_wasm.yml +++ b/.github/workflows/build_wasm.yml @@ -3,8 +3,7 @@ name: WebAssembly on: push: branches: - - master - - develop + - main pull_request: diff --git a/.gitmodules b/.gitmodules index 8d9990d..ceb826b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "webapp-src"] path = webapp-src url = https://github.com/agruenb/arduino-ocpp-dashboard.git +[submodule "lib/mbedtls"] + path = lib/mbedtls + url = https://github.com/Mbed-TLS/mbedtls diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d2b5f4..a05245f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,11 @@ -# matth-x/MicroOcpp -# Copyright Matthias Akstaller 2019 - 2023 -# MIT License +# matth-x/MicroOcppSimulator +# Copyright Matthias Akstaller 2022 - 2024 +# GPL-3.0 License cmake_minimum_required(VERSION 3.13) +set(CMAKE_CXX_STANDARD 11) + set(MO_SIM_SRC src/evse.cpp src/main.cpp @@ -32,34 +34,76 @@ add_compile_definitions( MO_DBG_LEVEL=MO_DL_INFO MO_FILENAME_PREFIX="./mo_store/" MO_ENABLE_V201=1 + MO_ENABLE_MBEDTLS=1 ) add_executable(mo_simulator ${MO_SIM_SRC} ${MO_SIM_MG_SRC}) -# find OpenSSL (skip for WebAssembly) -if (NOT ${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") - find_package(OpenSSL REQUIRED) -endif() - target_include_directories(mo_simulator PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/lib/ArduinoJson/src" "${CMAKE_CURRENT_SOURCE_DIR}/lib/mongoose" - "${OPENSSL_INCLUDE_DIR}" ) target_compile_definitions(mo_simulator PUBLIC MO_NETLIB=MO_NETLIB_MONGOOSE - MG_ENABLE_OPENSSL=1 ) add_subdirectory(lib/MicroOcpp) target_link_libraries(mo_simulator PUBLIC MicroOcpp) +# disable some warnings for MbedTLS which cause compilation errors on WASM +if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") + add_compile_options( + -Wno-unused-but-set-variable + -Wno-documentation + ) +endif() + +# disable MbedTLS unit tests and test suites (not needed for the Simualtor) +option(ENABLE_TESTING "Build mbed TLS tests." OFF) +option(ENABLE_PROGRAMS "Build mbed TLS programs." OFF) + +add_subdirectory(lib/mbedtls) +target_link_libraries(MicroOcpp PUBLIC + mbedtls + mbedcrypto + mbedx509 +) + +if (MO_SIM_BUILD_USE_OPENSSL) + + message("Using OpenSSL for WebSocket") + + # find OpenSSL + find_package(OpenSSL REQUIRED) + target_include_directories(mo_simulator PUBLIC + "${OPENSSL_INCLUDE_DIR}" + ) + target_link_libraries(mo_simulator PUBLIC + ${OPENSSL_LIBRARIES} + ) + target_compile_definitions(mo_simulator PUBLIC + MG_ENABLE_OPENSSL=1 + ) + +else() + + message("Using MbedTLS for WebSocket") + + target_link_libraries(mo_simulator PUBLIC + mbedtls + mbedcrypto + mbedx509 + ) + target_compile_definitions(mo_simulator PUBLIC + MG_ENABLE_MBEDTLS=1 + ) + +endif() + add_subdirectory(lib/MicroOcppMongoose) target_link_libraries(mo_simulator PUBLIC MicroOcppMongoose) -target_link_libraries(mo_simulator PUBLIC ${OPENSSL_LIBRARIES}) - # experimental WebAssembly port add_executable(mo_simulator_wasm ${MO_SIM_SRC} ${MO_SIM_WASM_SRC}) diff --git a/lib/MicroOcpp b/lib/MicroOcpp index 80da31d..e879c0e 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit 80da31d92f39e666193c14ffbe11d1a01b84d8bd +Subproject commit e879c0e6988d7946959ce8cf18e05c4b623d21ba diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose index 2e5aa6a..95f4748 160000 --- a/lib/MicroOcppMongoose +++ b/lib/MicroOcppMongoose @@ -1 +1 @@ -Subproject commit 2e5aa6a087f4af66077d4d10216583c7163051a2 +Subproject commit 95f4748f0863ab276f2b5481d29a3281a38a0a36 diff --git a/lib/mbedtls b/lib/mbedtls new file mode 160000 index 0000000..dd79db1 --- /dev/null +++ b/lib/mbedtls @@ -0,0 +1 @@ +Subproject commit dd79db10014d85b26d11fe57218431f2e5ede6f2 diff --git a/src/api.cpp b/src/api.cpp index 29b555d..f07d225 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -1,3 +1,7 @@ +// matth-x/MicroOcppSimulator +// Copyright Matthias Akstaller 2022 - 2024 +// GPL-3.0 License + #include "api.h" #include diff --git a/src/api.h b/src/api.h index 9c42e5b..fd25663 100644 --- a/src/api.h +++ b/src/api.h @@ -1,3 +1,7 @@ +// matth-x/MicroOcppSimulator +// Copyright Matthias Akstaller 2022 - 2024 +// GPL-3.0 License + #ifndef MO_SIM_API_H #define MO_SIM_API_H diff --git a/src/evse.cpp b/src/evse.cpp index e331961..62c1a03 100644 --- a/src/evse.cpp +++ b/src/evse.cpp @@ -1,6 +1,6 @@ // matth-x/MicroOcppSimulator // Copyright Matthias Akstaller 2022 - 2024 -// MIT License +// GPL-3.0 License #include "evse.h" #include diff --git a/src/evse.h b/src/evse.h index f0f3951..920ecc5 100644 --- a/src/evse.h +++ b/src/evse.h @@ -1,6 +1,6 @@ // matth-x/MicroOcppSimulator // Copyright Matthias Akstaller 2022 - 2024 -// MIT License +// GPL-3.0 License #ifndef EVSE_H #define EVSE_H diff --git a/src/main.cpp b/src/main.cpp index d501d20..8d8ee8d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,6 @@ // matth-x/MicroOcppSimulator // Copyright Matthias Akstaller 2022 - 2024 -// MIT License +// GPL-3.0 License #include diff --git a/src/net_mongoose.cpp b/src/net_mongoose.cpp index a956675..544dfc0 100644 --- a/src/net_mongoose.cpp +++ b/src/net_mongoose.cpp @@ -1,3 +1,7 @@ +// matth-x/MicroOcppSimulator +// Copyright Matthias Akstaller 2022 - 2024 +// GPL-3.0 License + #include "net_mongoose.h" #include "evse.h" #include "api.h" @@ -7,9 +11,6 @@ #include #include -static const char *s_http_addr = "http://localhost:8000"; // HTTP port -static const char *s_root_dir = "web_root"; - //cors_headers allow the browser to make requests from any domain, allowing all headers and all methods #define DEFAULT_HEADER "Content-Type: application/json\r\n" #define CORS_HEADERS "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Headers:Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers\r\nAccess-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,PUT\r\n" @@ -90,7 +91,7 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { serializeJson(doc, serialized); mg_http_reply(c, 200, final_headers, serialized.c_str()); return; - } else if(strncmp(message_data->uri.ptr, "/api", strlen("api")) == 0) { + } else if (strncmp(message_data->uri.ptr, "/api", strlen("api")) == 0) { #define RESP_BUF_SIZE 1024 char resp_buf [RESP_BUF_SIZE]; @@ -106,12 +107,12 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { resp_buf, RESP_BUF_SIZE); mg_http_reply(c, status, final_headers, resp_buf); - } - //if no specific path is given serve dashboard application file - else if (mg_http_match_uri(message_data, "/")) { - struct mg_http_serve_opts opts = { .root_dir = "./public" }; - opts.extra_headers = "Content-Type: text/html\r\nContent-Encoding: gzip\r\n"; - mg_http_serve_file(c, message_data, "public/bundle.html.gz", &opts); + } else if (mg_http_match_uri(message_data, "/")) { //if no specific path is given serve dashboard application file + struct mg_http_serve_opts opts; + memset(&opts, 0, sizeof(opts)); + opts.root_dir = "./public"; + opts.extra_headers = "Content-Type: text/html\r\nContent-Encoding: gzip\r\n"; + mg_http_serve_file(c, message_data, "public/bundle.html.gz", &opts); } else { mg_http_reply(c, 404, final_headers, "The required parameters are not given"); } diff --git a/src/net_mongoose.h b/src/net_mongoose.h index 3a5f095..9ff3ff2 100644 --- a/src/net_mongoose.h +++ b/src/net_mongoose.h @@ -1,3 +1,7 @@ +// matth-x/MicroOcppSimulator +// Copyright Matthias Akstaller 2022 - 2024 +// GPL-3.0 License + #ifndef MO_NET_MONGOOSE_H #define MO_NET_MONGOOSE_H diff --git a/src/net_wasm.cpp b/src/net_wasm.cpp index 83a7507..99ac1f2 100644 --- a/src/net_wasm.cpp +++ b/src/net_wasm.cpp @@ -1,3 +1,7 @@ +// matth-x/MicroOcppSimulator +// Copyright Matthias Akstaller 2022 - 2024 +// GPL-3.0 License + #include "net_wasm.h" #include @@ -7,7 +11,6 @@ #include #include #include -#include "base64.hpp" #include "api.h" @@ -304,19 +307,7 @@ class WasmOcppConnection : public Connection { } if (!auth_key.empty()) { - std::string token = cb_id + ":" + auth_key; - - MO_DBG_DEBUG("auth Token=%s", token.c_str()); - - unsigned int base64_length = encode_base64_length(token.length()); - std::vector base64 (base64_length + 1); - - // encode_base64() places a null terminator automatically, because the output is a string - base64_length = encode_base64((const unsigned char*) token.c_str(), token.length(), &base64[0]); - - MO_DBG_DEBUG("auth64 len=%u, auth64 Token=%s", base64_length, &base64[0]); - - basic_auth64 = (const char*) &base64[0]; + MO_DBG_WARN("WASM app does not support Securiy Profile 2 yet"); } else { MO_DBG_DEBUG("no authentication"); (void) 0; diff --git a/src/net_wasm.h b/src/net_wasm.h index 4184359..bca1b5e 100644 --- a/src/net_wasm.h +++ b/src/net_wasm.h @@ -1,3 +1,7 @@ +// matth-x/MicroOcppSimulator +// Copyright Matthias Akstaller 2022 - 2024 +// GPL-3.0 License + #ifndef MO_NET_WASM_H #define MO_NET_WASM_H From b2b8741baa91b63ee16de2de05c01bbc54b8606a Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:55:40 +0200 Subject: [PATCH 30/41] opt out charging limit if neg. (matth-x/MicroOcpp#310) --- src/evse.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/evse.cpp b/src/evse.cpp index 62c1a03..59269e8 100644 --- a/src/evse.cpp +++ b/src/evse.cpp @@ -120,8 +120,13 @@ void Evse::setup() { }); setSmartChargingPowerOutput([this] (float limit) { - MO_DBG_DEBUG("set limit: %f", limit); - this->limit_power = limit; + if (limit >= 0.f) { + MO_DBG_DEBUG("set limit: %f", limit); + this->limit_power = limit; + } else { + // negative value means no limit defined + this->limit_power = SIMULATE_POWER_CONST; + } }, connectorId); } From 9cc3276e80aa8cd4ee58581007ec6e173ec8cf97 Mon Sep 17 00:00:00 2001 From: Bradley Neild <46658771+BradleyNeild@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:08:41 +0200 Subject: [PATCH 31/41] Added EVSE-side plug API Will allow us to add an EVSE-side plug button to the dashboard in the future --- src/api.cpp | 4 ++++ src/evse.cpp | 16 +++++++++++++++- src/evse.h | 6 ++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/api.cpp b/src/api.cpp index f07d225..955415f 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -83,6 +83,9 @@ int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *b if (request.containsKey("evPlugged")) { evse->setEvPlugged(request["evPlugged"]); } + if (request.containsKey("evsePlugged")) { + evse->setEvsePlugged(request["evsePlugged"]); + } if (request.containsKey("evReady")) { evse->setEvReady(request["evReady"]); } @@ -92,6 +95,7 @@ int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *b } response["evPlugged"] = evse->getEvPlugged(); + response["evsePlugged"] = evse->getEvsePlugged(); response["evReady"] = evse->getEvReady(); response["evseReady"] = evse->getEvseReady(); response["chargePointStatus"] = evse->getOcppStatus(); diff --git a/src/evse.cpp b/src/evse.cpp index 59269e8..ff85000 100644 --- a/src/evse.cpp +++ b/src/evse.cpp @@ -54,6 +54,9 @@ void Evse::setup() { snprintf(key, 30, "evPlugged_cId_%u", connectorId); trackEvPluggedKey = key; trackEvPluggedBool = MicroOcpp::declareConfiguration(trackEvPluggedKey.c_str(), false, SIMULATOR_FN, false, false, false); + snprintf(key, 30, "evsePlugged_cId_%u", connectorId); + trackEvsePluggedKey = key; + trackEvsePluggedBool = MicroOcpp::declareConfiguration(trackEvsePluggedKey.c_str(), false, SIMULATOR_FN, false, false, false); snprintf(key, 30, "evReady_cId_%u", connectorId); trackEvReadyKey = key; trackEvReadyBool = MicroOcpp::declareConfiguration(trackEvReadyKey.c_str(), false, SIMULATOR_FN, false, false, false); @@ -139,7 +142,7 @@ void Evse::loop() { } } - bool simulate_isCharging = ocppPermitsCharge(connectorId) && trackEvPluggedBool->getBool() && trackEvReadyBool->getBool() && trackEvseReadyBool->getBool(); + bool simulate_isCharging = ocppPermitsCharge(connectorId) && trackEvPluggedBool->getBool() && trackEvsePluggedBool->getBool() && trackEvReadyBool->getBool() && trackEvseReadyBool->getBool(); simulate_isCharging &= limit_power >= 720.f; //minimum charging current is 6A (720W for 120V grids) according to J1772 @@ -209,6 +212,17 @@ bool Evse::getEvPlugged() { return trackEvPluggedBool->getBool(); } +void Evse::setEvsePlugged(bool plugged) { + if (!trackEvsePluggedBool) return; + trackEvsePluggedBool->setBool(plugged); + MicroOcpp::configuration_save(); +} + +bool Evse::getEvsePlugged() { + if (!trackEvsePluggedBool) return false; + return trackEvsePluggedBool->getBool(); +} + void Evse::setEvReady(bool ready) { if (!trackEvReadyBool) return; trackEvReadyBool->setBool(ready); diff --git a/src/evse.h b/src/evse.h index 920ecc5..399652d 100644 --- a/src/evse.h +++ b/src/evse.h @@ -17,6 +17,8 @@ class Evse { std::shared_ptr trackEvPluggedBool; std::string trackEvPluggedKey; + std::shared_ptr trackEvsePluggedBool; + std::string trackEvsePluggedKey; std::shared_ptr trackEvReadyBool; std::string trackEvReadyKey; std::shared_ptr trackEvseReadyBool; @@ -43,6 +45,10 @@ class Evse { bool getEvPlugged(); + void setEvsePlugged(bool plugged); + + bool getEvsePlugged(); + void setEvReady(bool ready); bool getEvReady(); From baec7ed185d419afbfb22fda65ff1327e0ee0adf Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Sun, 15 Sep 2024 17:56:54 +0200 Subject: [PATCH 32/41] integrate new MO features --- CMakeLists.txt | 1 + lib/MicroOcpp | 2 +- lib/MicroOcppMongoose | 2 +- src/evse.cpp | 59 ++++++++---------------------------- src/main.cpp | 69 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 83 insertions(+), 50 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a05245f..afd3d39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ add_compile_definitions( MO_FILENAME_PREFIX="./mo_store/" MO_ENABLE_V201=1 MO_ENABLE_MBEDTLS=1 + MO_ENABLE_TIMESTAMP_MILLISECONDS=1 ) add_executable(mo_simulator ${MO_SIM_SRC} ${MO_SIM_MG_SRC}) diff --git a/lib/MicroOcpp b/lib/MicroOcpp index e879c0e..6ec363d 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit e879c0e6988d7946959ce8cf18e05c4b623d21ba +Subproject commit 6ec363d8ba7871c8f52bfc8a33e4d56827c99732 diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose index 95f4748..9f19677 160000 --- a/lib/MicroOcppMongoose +++ b/lib/MicroOcppMongoose @@ -1 +1 @@ -Subproject commit 95f4748f0863ab276f2b5481d29a3281a38a0a36 +Subproject commit 9f19677e4da0b7303c7dacbac518a0b2fc9d1207 diff --git a/src/evse.cpp b/src/evse.cpp index 59269e8..b7e9531 100644 --- a/src/evse.cpp +++ b/src/evse.cpp @@ -19,14 +19,6 @@ Evse::Evse(unsigned int connectorId) : connectorId{connectorId} { } -MicroOcpp::Connector *getConnector(unsigned int connectorId) { - if (!getOcppContext()) { - MO_DBG_ERR("unitialized"); - return nullptr; - } - return getOcppContext()->getModel().getConnector(connectorId); -} - void Evse::setup() { #if MO_ENABLE_V201 @@ -43,12 +35,6 @@ void Evse::setup() { } #endif - auto connector = getConnector(connectorId); - if (!connector) { - MO_DBG_ERR("invalid state"); - return; - } - char key [30] = {'\0'}; snprintf(key, 30, "evPlugged_cId_%u", connectorId); @@ -131,12 +117,11 @@ void Evse::setup() { } void Evse::loop() { - if (auto connector = getConnector(connectorId)) { - auto curStatus = connector->getStatus(); - if (status.compare(MicroOcpp::cstrFromOcppEveState(curStatus))) { - status = MicroOcpp::cstrFromOcppEveState(curStatus); - } + auto curStatus = getChargePointStatus(connectorId); + + if (status.compare(MicroOcpp::cstrFromOcppEveState(curStatus))) { + status = MicroOcpp::cstrFromOcppEveState(curStatus); } bool simulate_isCharging = ocppPermitsCharge(connectorId) && trackEvPluggedBool->getBool() && trackEvReadyBool->getBool() && trackEvseReadyBool->getBool(); @@ -164,11 +149,6 @@ void Evse::presentNfcTag(const char *uid_cstr) { return; } std::string uid = uid_cstr; - auto connector = getConnector(connectorId); - if (!connector) { - MO_DBG_ERR("invalid state"); - return; - } #if MO_ENABLE_V201 if (auto context = getOcppContext()) { @@ -178,7 +158,7 @@ void Evse::presentNfcTag(const char *uid_cstr) { if (evse->getTransaction() && evse->getTransaction()->isAuthorized) { evse->endAuthorization(uid_cstr); } else { - evse->beginAuthorization(uid_cstr); + evse->beginAuthorization(MicroOcpp::IdToken(uid_cstr, MicroOcpp::IdToken::Type::KeyCode)); } return; } @@ -187,14 +167,14 @@ void Evse::presentNfcTag(const char *uid_cstr) { } #endif - if (connector->getTransaction() && connector->getTransaction()->isActive()) { - if (!uid.compare(connector->getTransaction()->getIdTag())) { - connector->endTransaction(uid.c_str()); + if (isTransactionActive(connectorId)) { + if (!uid.compare(getTransactionIdTag(connectorId))) { + endTransaction(uid.c_str(), "Local", connectorId); } else { MO_DBG_INFO("RFID card denied"); } } else { - connector->beginTransaction(uid.c_str()); + beginTransaction(uid.c_str(), connectorId); } } @@ -232,30 +212,15 @@ bool Evse::getEvseReady() { } const char *Evse::getSessionIdTag() { - auto connector = getConnector(connectorId); - if (!connector) { - MO_DBG_ERR("invalid state"); - return nullptr; - } - return connector->getTransaction() ? connector->getTransaction()->getIdTag() : nullptr; + return getTransactionIdTag(connectorId) ? getTransactionIdTag(connectorId) : ""; } int Evse::getTransactionId() { - auto connector = getConnector(connectorId); - if (!connector) { - MO_DBG_ERR("invalid state"); - return -1; - } - return connector->getTransaction() ? connector->getTransaction()->getTransactionId() : -1; + return getTransaction(connectorId) ? getTransaction(connectorId)->getTransactionId() : -1; } bool Evse::chargingPermitted() { - auto connector = getConnector(connectorId); - if (!connector) { - MO_DBG_ERR("invalid state"); - return false; - } - return connector->ocppPermitsCharge(); + return ocppPermitsCharge(connectorId); } int Evse::getPower() { diff --git a/src/main.cpp b/src/main.cpp index 8d8ee8d..ac7d6b4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,11 +3,17 @@ // GPL-3.0 License #include +#include + +#include #include +#include #include "evse.h" #include "api.h" +#include + #if MO_NUMCONNECTORS == 3 std::array connectors {{1,2}}; #else @@ -15,6 +21,10 @@ std::array connectors {{1}}; #endif bool g_isOcpp201 = false; +bool g_runSimulator = true; + +bool g_isUpAndRunning = false; //if the initial BootNotification and StatusNotifications got through + 1s delay +unsigned int g_bootNotificationTime = 0; #define MO_NETLIB_MONGOOSE 1 #define MO_NETLIB_WASM 2 @@ -42,6 +52,31 @@ MicroOcpp::Connection *conn = nullptr; #error Please ensure that build flag MO_NETLIB is set as MO_NETLIB_MONGOOSE or MO_NETLIB_WASM #endif +#if MBEDTLS_PLATFORM_MEMORY //configure MbedTLS with allocation hook functions + +void *mo_mem_mbedtls_calloc( size_t n, size_t count ) { + size_t size = n * count; + auto ptr = MO_MALLOC("MbedTLS", size); + if (ptr) { + memset(ptr, 0, size); + } + return ptr; +} +void mo_mem_mbedtls_free( void *ptr ) { + MO_FREE(ptr); +} + +#endif //MBEDTLS_PLATFORM_MEMORY + +void mo_sim_sig_handler(int s){ + + if (!g_runSimulator) { //already tried to shut down, now force stop + exit(EXIT_FAILURE); + } + + g_runSimulator = false; //shut down simulator gracefully +} + /* * Setup MicroOcpp and API */ @@ -94,6 +129,17 @@ void app_loop() { #if MO_NETLIB == MO_NETLIB_MONGOOSE int main() { + +#if MBEDTLS_PLATFORM_MEMORY + mbedtls_platform_set_calloc_free(mo_mem_mbedtls_calloc, mo_mem_mbedtls_free); +#endif //MBEDTLS_PLATFORM_MEMORY + + struct sigaction sigIntHandler; + sigIntHandler.sa_handler = mo_sim_sig_handler; + sigemptyset(&sigIntHandler.sa_mask); + sigIntHandler.sa_flags = 0; + sigaction(SIGINT, &sigIntHandler, NULL); + mg_log_set(MG_LL_INFO); mg_mgr_init(&mgr); @@ -117,11 +163,32 @@ int main() { server_initialize(osock); app_setup(*osock, filesystem); - for (;;) { // Block forever + setOnResetExecute([] (bool isHard) { + g_runSimulator = false; + }); + + while (g_runSimulator) { //Run Simulator until OCPP Reset is executed or user presses Ctrl+C mg_mgr_poll(&mgr, 100); app_loop(); + + if (!g_bootNotificationTime && getOcppContext()->getModel().getClock().now() >= MicroOcpp::MIN_TIME) { + //time has been set, BootNotification succeeded + g_bootNotificationTime = mocpp_tick_ms(); + } + + if (!g_isUpAndRunning && g_bootNotificationTime && mocpp_tick_ms() - g_bootNotificationTime >= 1000) { + printf("[Sim] Resetting maximum heap usage after boot success\n"); + g_isUpAndRunning = true; + MO_MEM_RESET(); + } } + printf("[Sim] Shutting down Simulator\n"); + + MO_MEM_PRINT_STATS(); + + mocpp_deinitialize(); + delete osock; mg_mgr_free(&mgr); return 0; From bb8b664030c82a7199fb441cc4c70ba5db7d092b Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:55:43 +0200 Subject: [PATCH 33/41] flag RFID tags as KeyCode to make OCTT happy --- src/evse.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/evse.cpp b/src/evse.cpp index b7e9531..3a7e49c 100644 --- a/src/evse.cpp +++ b/src/evse.cpp @@ -155,8 +155,8 @@ void Evse::presentNfcTag(const char *uid_cstr) { if (context->getVersion().major == 2) { if (auto txService = context->getModel().getTransactionService()) { if (auto evse = txService->getEvse(connectorId)) { - if (evse->getTransaction() && evse->getTransaction()->isAuthorized) { - evse->endAuthorization(uid_cstr); + if (evse->getTransaction() && evse->getTransaction()->isAuthorizationActive) { + evse->endAuthorization(MicroOcpp::IdToken(uid_cstr, MicroOcpp::IdToken::Type::KeyCode)); } else { evse->beginAuthorization(MicroOcpp::IdToken(uid_cstr, MicroOcpp::IdToken::Type::KeyCode)); } From 449a0bc821a6d937dc9b6b4d057f5b973837e38e Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:13:14 +0200 Subject: [PATCH 34/41] update dependencies --- lib/MicroOcpp | 2 +- lib/MicroOcppMongoose | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/MicroOcpp b/lib/MicroOcpp index 6ec363d..4ea7db2 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit 6ec363d8ba7871c8f52bfc8a33e4d56827c99732 +Subproject commit 4ea7db2daeefb94bef42d4fa2d4ae4db9fab5646 diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose index 9f19677..dbd8786 160000 --- a/lib/MicroOcppMongoose +++ b/lib/MicroOcppMongoose @@ -1 +1 @@ -Subproject commit 9f19677e4da0b7303c7dacbac518a0b2fc9d1207 +Subproject commit dbd878688534086e74810d9aafa37886b84c3c06 From 1ed8e2054fc92dd758a15607412e3595b8573b05 Mon Sep 17 00:00:00 2001 From: Matthias Akstaller <63792403+matth-x@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:08:15 +0200 Subject: [PATCH 35/41] Upgrade mongoose (#30) * upgrade mongoose * upgrade MO-Mongoose --- CMakeLists.txt | 1 + lib/MicroOcppMongoose | 2 +- lib/mongoose | 2 +- src/net_mongoose.cpp | 20 ++++++++++---------- src/net_mongoose.h | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index afd3d39..5632e24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ add_compile_definitions( MO_ENABLE_V201=1 MO_ENABLE_MBEDTLS=1 MO_ENABLE_TIMESTAMP_MILLISECONDS=1 + MO_MG_USE_VERSION=MO_MG_V715 ) add_executable(mo_simulator ${MO_SIM_SRC} ${MO_SIM_MG_SRC}) diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose index dbd8786..01c5903 160000 --- a/lib/MicroOcppMongoose +++ b/lib/MicroOcppMongoose @@ -1 +1 @@ -Subproject commit dbd878688534086e74810d9aafa37886b84c3c06 +Subproject commit 01c5903618de930e26e10d0752119bec78dd8c51 diff --git a/lib/mongoose b/lib/mongoose index 987f8bc..9729693 160000 --- a/lib/mongoose +++ b/lib/mongoose @@ -1 +1 @@ -Subproject commit 987f8bc9490c86a395e6ea4a96fcf544f6b9247d +Subproject commit 97296934ab594241ba2489f018a21779882a7936 diff --git a/src/net_mongoose.cpp b/src/net_mongoose.cpp index 544dfc0..3bcd6c9 100644 --- a/src/net_mongoose.cpp +++ b/src/net_mongoose.cpp @@ -27,27 +27,27 @@ char* toStringPtr(std::string cppString){ return cstr; } -void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { +void http_serve(struct mg_connection *c, int ev, void *ev_data) { if (ev == MG_EV_HTTP_MSG) { //struct mg_http_message *message_data = (struct mg_http_message *) ev_data; struct mg_http_message *message_data = reinterpret_cast(ev_data); const char *final_headers = DEFAULT_HEADER CORS_HEADERS; struct mg_str json = message_data->body; - MO_DBG_VERBOSE("%.*s", 20, message_data->uri.ptr); + MO_DBG_VERBOSE("%.*s", 20, message_data->uri.buf); MicroOcpp::Method method = MicroOcpp::Method::UNDEFINED; - if (!mg_vcasecmp(&message_data->method, "POST")) { + if (!mg_strcasecmp(message_data->method, mg_str("POST"))) { method = MicroOcpp::Method::POST; MO_DBG_VERBOSE("POST"); - } else if (!mg_vcasecmp(&message_data->method, "GET")) { + } else if (!mg_strcasecmp(message_data->method, mg_str("GET"))) { method = MicroOcpp::Method::GET; MO_DBG_VERBOSE("GET"); } //start different api endpoints - if(mg_http_match_uri(message_data, "/api/websocket")){ + if(mg_match(message_data->uri, mg_str("/api/websocket"), NULL)){ MO_DBG_VERBOSE("query websocket"); auto webSocketPingIntervalInt = MicroOcpp::declareConfiguration("WebSocketPingInterval", 10, MO_WSCONN_FN); auto reconnectIntervalInt = MicroOcpp::declareConfiguration(MO_CONFIG_EXT_PREFIX "ReconnectInterval", 30, MO_WSCONN_FN); @@ -91,23 +91,23 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { serializeJson(doc, serialized); mg_http_reply(c, 200, final_headers, serialized.c_str()); return; - } else if (strncmp(message_data->uri.ptr, "/api", strlen("api")) == 0) { + } else if (strncmp(message_data->uri.buf, "/api", strlen("api")) == 0) { #define RESP_BUF_SIZE 1024 char resp_buf [RESP_BUF_SIZE]; //replace endpoint-body separator by null - if (char *c = strchr((char*) message_data->uri.ptr, ' ')) { + if (char *c = strchr((char*) message_data->uri.buf, ' ')) { *c = '\0'; } int status = mocpp_api_call( - message_data->uri.ptr + strlen("/api"), + message_data->uri.buf + strlen("/api"), method, - message_data->body.ptr, + message_data->body.buf, resp_buf, RESP_BUF_SIZE); mg_http_reply(c, status, final_headers, resp_buf); - } else if (mg_http_match_uri(message_data, "/")) { //if no specific path is given serve dashboard application file + } else if (mg_match(message_data->uri, mg_str("/"), NULL)) { //if no specific path is given serve dashboard application file struct mg_http_serve_opts opts; memset(&opts, 0, sizeof(opts)); opts.root_dir = "./public"; diff --git a/src/net_mongoose.h b/src/net_mongoose.h index 9ff3ff2..46e0890 100644 --- a/src/net_mongoose.h +++ b/src/net_mongoose.h @@ -15,7 +15,7 @@ class MOcppMongooseClient; void server_initialize(MicroOcpp::MOcppMongooseClient *osock); -void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data); +void http_serve(struct mg_connection *c, int ev, void *ev_data); #endif //MO_NETLIB == MO_NETLIB_MONGOOSE From a1368ed18308e8b40986c24eca331b67bcdb1549 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Thu, 10 Oct 2024 06:52:00 +0000 Subject: [PATCH 36/41] fix enable TLS build flag (fixes #31) --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5632e24..31f02a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,7 +98,7 @@ else() mbedx509 ) target_compile_definitions(mo_simulator PUBLIC - MG_ENABLE_MBEDTLS=1 + MG_TLS=MG_TLS_MBED ) endif() From 67615d8fd1f8c4c2f4467caa5148ecc33e9ba222 Mon Sep 17 00:00:00 2001 From: Matthias Akstaller <63792403+matth-x@users.noreply.github.com> Date: Sun, 13 Oct 2024 08:12:40 +0200 Subject: [PATCH 37/41] Enable TLS for HTTP API and add endpoints (#32) * add security policies for public access to API * load certificates from .pem files * add HTTP API extension * add memory endpoint * fix /memory/reset status code * update dependencies --- lib/MicroOcpp | 2 +- src/api.cpp | 196 ++++++++++++++++++++++++++++++++++++++++++- src/api.h | 2 + src/evse.cpp | 46 ++++++---- src/evse.h | 5 ++ src/main.cpp | 24 +++++- src/net_mongoose.cpp | 70 ++++++++++++---- src/net_mongoose.h | 2 +- 8 files changed, 309 insertions(+), 38 deletions(-) diff --git a/lib/MicroOcpp b/lib/MicroOcpp index 4ea7db2..942fb88 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit 4ea7db2daeefb94bef42d4fa2d4ae4db9fab5646 +Subproject commit 942fb88aa736a5f99d1a24c11fe657c49af2c7ea diff --git a/src/api.cpp b/src/api.cpp index 955415f..054f37c 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -3,8 +3,12 @@ // GPL-3.0 License #include "api.h" +#include "mongoose.h" #include +#include +#include +#include #include "evse.h" @@ -12,8 +16,8 @@ bool str_match(const char *query, const char *pattern) { size_t qi = 0, pi = 0; - while (query[qi] && pattern[pi]) { - if (query[qi] == pattern[pi]) { + while (pattern[pi]) { + if (query[qi] && query[qi] == pattern[pi]) { qi++; pi++; } else if (pattern[pi] == '*') { @@ -155,3 +159,191 @@ int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *b return status; } + +int mocpp_api2_call(const char *uri_raw, size_t uri_raw_len, MicroOcpp::Method method, const char *query_raw, size_t query_raw_len, char *resp_body, size_t resp_body_size) { + + snprintf(resp_body, resp_body_size, "%s", ""); + + struct mg_str uri = mg_str_n(uri_raw, uri_raw_len); + struct mg_str query = mg_str_n(query_raw, query_raw_len); + + int evse_id = -1; + int connector_id = -1; + + unsigned int num; + struct mg_str evse_id_str = mg_http_var(query, mg_str("evse_id")); + if (evse_id_str.buf) { + if (!mg_str_to_num(evse_id_str, 10, &num, sizeof(num)) || num < 1 || num >= MO_NUM_EVSEID) { + snprintf(resp_body, resp_body_size, "invalid connector_id"); + return 400; + } + evse_id = (int)num; + } + + struct mg_str connector_id_str = mg_http_var(query, mg_str("connector_id")); + if (connector_id_str.buf) { + if (!mg_str_to_num(connector_id_str, 10, &num, sizeof(num)) || num != 1) { + snprintf(resp_body, resp_body_size, "invalid connector_id"); + return 400; + } + connector_id = (int)num; + } + + if (mg_match(uri, mg_str("/plugin"), NULL)) { + if (method != MicroOcpp::Method::POST) { + return 405; + } + if (evse_id < 0) { + snprintf(resp_body, resp_body_size, "no action taken"); + return 200; + } else { + snprintf(resp_body, resp_body_size, "%s", connectors[evse_id-1].getEvPlugged() ? "EV already plugged" : "plugged in EV"); + connectors[evse_id-1].setEvPlugged(true); + connectors[evse_id-1].setEvReady(true); + connectors[evse_id-1].setEvseReady(true); + return 200; + } + } else if (mg_match(uri, mg_str("/plugout"), NULL)) { + if (method != MicroOcpp::Method::POST) { + return 405; + } + if (evse_id < 0) { + snprintf(resp_body, resp_body_size, "no action taken"); + return 200; + } else { + snprintf(resp_body, resp_body_size, "%s", connectors[evse_id-1].getEvPlugged() ? "EV already unplugged" : "unplug EV"); + connectors[evse_id-1].setEvPlugged(false); + connectors[evse_id-1].setEvReady(false); + connectors[evse_id-1].setEvseReady(false); + return 200; + } + } else if (mg_match(uri, mg_str("/end"), NULL)) { + if (method != MicroOcpp::Method::POST) { + return 405; + } + bool trackEvReady = false; + for (size_t i = 0; i < connectors.size(); i++) { + trackEvReady |= connectors[i].getEvReady(); + connectors[i].setEvReady(false); + } + snprintf(resp_body, resp_body_size, "%s", trackEvReady ? "suspended EV" : "EV already suspended"); + return 200; + } else if (mg_match(uri, mg_str("/state"), NULL)) { + if (method != MicroOcpp::Method::POST) { + return 405; + } + struct mg_str ready_str = mg_http_var(query, mg_str("ready")); + bool ready = true; + if (ready_str.buf) { + if (mg_match(ready_str, mg_str("true"), NULL)) { + ready = true; + } else if (mg_match(ready_str, mg_str("false"), NULL)) { + ready = false; + } else { + snprintf(resp_body, resp_body_size, "invalid ready"); + return 400; + } + } + bool trackEvReady = false; + for (size_t i = 0; i < connectors.size(); i++) { + if (connectors[i].getEvPlugged()) { + bool trackEvReady = connectors[i].getEvReady(); + connectors[i].setEvReady(ready); + snprintf(resp_body, resp_body_size, "%s, %s", ready ? "EV suspended" : "EV not suspended", trackEvReady ? "suspended before" : "not suspended before"); + return 200; + } + } + snprintf(resp_body, resp_body_size, "no action taken - EV not plugged"); + return 200; + } else if (mg_match(uri, mg_str("/authorize"), NULL)) { + if (method != MicroOcpp::Method::POST) { + return 405; + } + struct mg_str id = mg_http_var(query, mg_str("id")); + if (!id.buf) { + snprintf(resp_body, resp_body_size, "missing id"); + return 400; + } + struct mg_str type = mg_http_var(query, mg_str("type")); + if (!id.buf) { + snprintf(resp_body, resp_body_size, "missing type"); + return 400; + } + + int ret; + char id_buf [MO_IDTOKEN_LEN_MAX + 1]; + ret = snprintf(id_buf, sizeof(id_buf), "%.*s", (int)id.len, id.buf); + if (ret < 0 || ret >= sizeof(id_buf)) { + snprintf(resp_body, resp_body_size, "invalid id"); + return 400; + } + char type_buf [128]; + ret = snprintf(type_buf, sizeof(type_buf), "%.*s", (int)type.len, type.buf); + if (ret < 0 || ret >= sizeof(type_buf)) { + snprintf(resp_body, resp_body_size, "invalid type"); + return 400; + } + + if (evse_id <= 0) { + snprintf(resp_body, resp_body_size, "invalid evse_id"); + return 400; + } + + bool trackAuthActive = connectors[evse_id-1].getSessionIdTag(); + + if (!connectors[evse_id-1].presentNfcTag(id_buf, type_buf)) { + snprintf(resp_body, resp_body_size, "invalid id and / or type"); + return 400; + } + + bool authActive = connectors[evse_id-1].getSessionIdTag(); + + snprintf(resp_body, resp_body_size, "%s", + !trackAuthActive && authActive ? "authorize in progress" : + trackAuthActive && !authActive ? "unauthorize in progress" : + trackAuthActive && authActive ? "no action taken (EVSE still authorized)" : + "no action taken (EVSE not authorized)"); + + return 200; + } else if (mg_match(uri, mg_str("/memory/info"), NULL)) { + #if MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER + { + if (method != MicroOcpp::Method::GET) { + return 405; + } + + int ret = mo_mem_write_stats_json(resp_body, resp_body_size); + if (ret < 0 || ret >= resp_body_size) { + snprintf(resp_body, resp_body_size, "internal error"); + return 500; + } + + return 200; + } + #else + { + snprintf(resp_body, resp_body_size, "memory profiler disabled"); + return 404; + } + #endif + } else if (mg_match(uri, mg_str("/memory/reset"), NULL)) { + #if MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER + { + if (method != MicroOcpp::Method::POST) { + return 405; + } + + MO_MEM_RESET(); + return 200; + } + #else + { + snprintf(resp_body, resp_body_size, "memory profiler disabled"); + return 404; + } + #endif + + } + + return 404; +} diff --git a/src/api.h b/src/api.h index fd25663..a2de1f8 100644 --- a/src/api.h +++ b/src/api.h @@ -19,4 +19,6 @@ enum class Method { int mocpp_api_call(const char *endpoint, MicroOcpp::Method method, const char *body, char *resp_body, size_t resp_body_size); +int mocpp_api2_call(const char *endpoint, size_t endpoint_len, MicroOcpp::Method method, const char *query, size_t query_len, char *resp_body, size_t resp_body_size); + #endif diff --git a/src/evse.cpp b/src/evse.cpp index b8b7d4a..a5f4275 100644 --- a/src/evse.cpp +++ b/src/evse.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -146,41 +147,54 @@ void Evse::loop() { } -void Evse::presentNfcTag(const char *uid_cstr) { - if (!uid_cstr) { +void Evse::presentNfcTag(const char *uid) { + if (!uid) { MO_DBG_ERR("invalid argument"); return; } - std::string uid = uid_cstr; #if MO_ENABLE_V201 if (auto context = getOcppContext()) { if (context->getVersion().major == 2) { - if (auto txService = context->getModel().getTransactionService()) { - if (auto evse = txService->getEvse(connectorId)) { - if (evse->getTransaction() && evse->getTransaction()->isAuthorizationActive) { - evse->endAuthorization(MicroOcpp::IdToken(uid_cstr, MicroOcpp::IdToken::Type::KeyCode)); - } else { - evse->beginAuthorization(MicroOcpp::IdToken(uid_cstr, MicroOcpp::IdToken::Type::KeyCode)); - } - return; - } - } + presentNfcTag(uid, "ISO14443"); + return; } } #endif if (isTransactionActive(connectorId)) { - if (!uid.compare(getTransactionIdTag(connectorId))) { - endTransaction(uid.c_str(), "Local", connectorId); + if (!strcmp(uid, getTransactionIdTag(connectorId))) { + endTransaction(uid, "Local", connectorId); } else { MO_DBG_INFO("RFID card denied"); } } else { - beginTransaction(uid.c_str(), connectorId); + beginTransaction(uid, connectorId); } } +#if MO_ENABLE_V201 +bool Evse::presentNfcTag(const char *uid, const char *type) { + + MicroOcpp::IdToken idToken {nullptr, MicroOcpp::IdToken::Type::UNDEFINED, "Simulator"}; + if (!idToken.parseCstr(uid, type)) { + return false; + } + + if (auto txService = getOcppContext()->getModel().getTransactionService()) { + if (auto evse = txService->getEvse(connectorId)) { + if (evse->getTransaction() && evse->getTransaction()->isAuthorizationActive) { + evse->endAuthorization(idToken); + } else { + evse->beginAuthorization(idToken); + } + return true; + } + } + return false; +} +#endif + void Evse::setEvPlugged(bool plugged) { if (!trackEvPluggedBool) return; trackEvPluggedBool->setBool(plugged); diff --git a/src/evse.h b/src/evse.h index 399652d..eaf37dc 100644 --- a/src/evse.h +++ b/src/evse.h @@ -8,6 +8,7 @@ #include #include #include +#include #define SIMULATOR_FN MO_FILENAME_PREFIX "simulator.jsn" @@ -41,6 +42,10 @@ class Evse { void presentNfcTag(const char *uid); +#if MO_ENABLE_V201 + bool presentNfcTag(const char *uid, const char *type); +#endif //MO_ENABLE_V201 + void setEvPlugged(bool plugged); bool getEvPlugged(); diff --git a/src/main.cpp b/src/main.cpp index ac7d6b4..0e945fd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,7 @@ #include #include +#include #include "evse.h" #include "api.h" @@ -128,6 +129,10 @@ void app_loop() { #if MO_NETLIB == MO_NETLIB_MONGOOSE +#ifndef MO_SIM_ENDPOINT_URL +#define MO_SIM_ENDPOINT_URL "http://0.0.0.0:8000" //URL to forward to mg_http_listen(). Will be ignored if the URL field exists in api.jsn +#endif + int main() { #if MBEDTLS_PLATFORM_MEMORY @@ -143,12 +148,23 @@ int main() { mg_log_set(MG_LL_INFO); mg_mgr_init(&mgr); - mg_http_listen(&mgr, "0.0.0.0:8000", http_serve, NULL); // Create listening connection - auto filesystem = MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail); load_ocpp_version(filesystem); + struct mg_str api_cert = mg_file_read(&mg_fs_posix, MO_FILENAME_PREFIX "api_cert.pem"); + struct mg_str api_key = mg_file_read(&mg_fs_posix, MO_FILENAME_PREFIX "api_key.pem"); + + auto api_settings_doc = MicroOcpp::FilesystemUtils::loadJson(filesystem, MO_FILENAME_PREFIX "api.jsn", "Simulator"); + if (!api_settings_doc) { + api_settings_doc = MicroOcpp::makeJsonDoc("Simulator", 0); + } + JsonObject api_settings = api_settings_doc->as(); + + const char *api_url = api_settings["url"] | MO_SIM_ENDPOINT_URL; + + mg_http_listen(&mgr, api_url, http_serve, (void*)api_url); // Create listening connection + osock = new MicroOcpp::MOcppMongooseClient(&mgr, "ws://echo.websocket.events", "charger-01", @@ -160,7 +176,7 @@ int main() { MicroOcpp::ProtocolVersion{1,6} ); - server_initialize(osock); + server_initialize(osock, api_cert.buf ? api_cert.buf : "", api_key.buf ? api_key.buf : "", api_settings["user"] | "", api_settings["pass"] | ""); app_setup(*osock, filesystem); setOnResetExecute([] (bool isHard) { @@ -191,6 +207,8 @@ int main() { delete osock; mg_mgr_free(&mgr); + free(api_cert.buf); + free(api_key.buf); return 0; } diff --git a/src/net_mongoose.cpp b/src/net_mongoose.cpp index 3bcd6c9..e203279 100644 --- a/src/net_mongoose.cpp +++ b/src/net_mongoose.cpp @@ -16,22 +16,50 @@ #define CORS_HEADERS "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Headers:Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers\r\nAccess-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,PUT\r\n" MicroOcpp::MOcppMongooseClient *ao_sock = nullptr; +const char *api_cert = ""; +const char *api_key = ""; +const char *api_user = ""; +const char *api_pass = ""; -void server_initialize(MicroOcpp::MOcppMongooseClient *osock) { - ao_sock = osock; +void server_initialize(MicroOcpp::MOcppMongooseClient *osock, const char *cert, const char *key, const char *user, const char *pass) { + ao_sock = osock; + api_cert = cert; + api_key = key; + api_user = user; + api_pass = pass; } -char* toStringPtr(std::string cppString){ - char *cstr = new char[cppString.length() + 1]; - strcpy(cstr, cppString.c_str()); - return cstr; +bool api_check_basic_auth(const char *user, const char *pass) { + if (strcmp(api_user, user)) { + return false; + } + if (strcmp(api_pass, pass)) { + return false; + } + return true; } void http_serve(struct mg_connection *c, int ev, void *ev_data) { - if (ev == MG_EV_HTTP_MSG) { + if (ev == MG_EV_ACCEPT) { + if (mg_url_is_ssl((const char*)c->fn_data)) { // TLS listener! + MO_DBG_VERBOSE("API TLS setup"); + struct mg_tls_opts opts = {0}; + opts.cert = mg_str(api_cert); + opts.key = mg_str(api_key); + mg_tls_init(c, &opts); + } + } else if (ev == MG_EV_HTTP_MSG) { //struct mg_http_message *message_data = (struct mg_http_message *) ev_data; struct mg_http_message *message_data = reinterpret_cast(ev_data); const char *final_headers = DEFAULT_HEADER CORS_HEADERS; + + char user[64], pass[64]; + mg_http_creds(message_data, user, sizeof(user), pass, sizeof(pass)); + if (!api_check_basic_auth(user, pass)) { + mg_http_reply(c, 401, final_headers, "Unauthorized. Expect Basic Auth user and / or password\n"); + return; + } + struct mg_str json = message_data->body; MO_DBG_VERBOSE("%.*s", 20, message_data->uri.buf); @@ -92,7 +120,7 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data) { mg_http_reply(c, 200, final_headers, serialized.c_str()); return; } else if (strncmp(message_data->uri.buf, "/api", strlen("api")) == 0) { - #define RESP_BUF_SIZE 1024 + #define RESP_BUF_SIZE 8192 char resp_buf [RESP_BUF_SIZE]; //replace endpoint-body separator by null @@ -100,12 +128,24 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data) { *c = '\0'; } - int status = mocpp_api_call( - message_data->uri.buf + strlen("/api"), - method, - message_data->body.buf, - resp_buf, RESP_BUF_SIZE); - + int status = 404; + if (status == 404) { + status = mocpp_api2_call( + message_data->uri.buf + strlen("/api"), + message_data->uri.len - strlen("/api"), + method, + message_data->query.buf, + message_data->query.len, + resp_buf, RESP_BUF_SIZE); + } + if (status == 404) { + status = mocpp_api_call( + message_data->uri.buf + strlen("/api"), + method, + message_data->body.buf, + resp_buf, RESP_BUF_SIZE); + } + mg_http_reply(c, status, final_headers, resp_buf); } else if (mg_match(message_data->uri, mg_str("/"), NULL)) { //if no specific path is given serve dashboard application file struct mg_http_serve_opts opts; @@ -114,7 +154,7 @@ void http_serve(struct mg_connection *c, int ev, void *ev_data) { opts.extra_headers = "Content-Type: text/html\r\nContent-Encoding: gzip\r\n"; mg_http_serve_file(c, message_data, "public/bundle.html.gz", &opts); } else { - mg_http_reply(c, 404, final_headers, "The required parameters are not given"); + mg_http_reply(c, 404, final_headers, "API endpoint not found"); } } } diff --git a/src/net_mongoose.h b/src/net_mongoose.h index 46e0890..aa88717 100644 --- a/src/net_mongoose.h +++ b/src/net_mongoose.h @@ -13,7 +13,7 @@ namespace MicroOcpp { class MOcppMongooseClient; } -void server_initialize(MicroOcpp::MOcppMongooseClient *osock); +void server_initialize(MicroOcpp::MOcppMongooseClient *osock, const char *cert = "", const char *key = "", const char *user = "", const char *pass = ""); void http_serve(struct mg_connection *c, int ev, void *ev_data); From 065418ca7ea5ac4ecfc006974da223705472d06e Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:03:44 +0100 Subject: [PATCH 38/41] upgrade dependencies --- lib/MicroOcpp | 2 +- lib/MicroOcppMongoose | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/MicroOcpp b/lib/MicroOcpp index 942fb88..fecac09 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit 942fb88aa736a5f99d1a24c11fe657c49af2c7ea +Subproject commit fecac0978b5ab29548f97e6384b4b63964ed0e32 diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose index 01c5903..fb1292e 160000 --- a/lib/MicroOcppMongoose +++ b/lib/MicroOcppMongoose @@ -1 +1 @@ -Subproject commit 01c5903618de930e26e10d0752119bec78dd8c51 +Subproject commit fb1292ed84fbab18c965a131f8704b61ced0f1b7 From 7890a3c46be299df91ab0256285135380a45d51f Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Sun, 3 Nov 2024 11:25:59 +0100 Subject: [PATCH 39/41] update MO Variables API --- lib/MicroOcppMongoose | 2 +- src/evse.cpp | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose index fb1292e..e228297 160000 --- a/lib/MicroOcppMongoose +++ b/lib/MicroOcppMongoose @@ -1 +1 @@ -Subproject commit fb1292ed84fbab18c965a131f8704b61ced0f1b7 +Subproject commit e2282976eed4b98e200e5987f2dded5313902cab diff --git a/src/evse.cpp b/src/evse.cpp index a5f4275..a0261d6 100644 --- a/src/evse.cpp +++ b/src/evse.cpp @@ -28,9 +28,7 @@ void Evse::setup() { //load some example variables for testing if (auto varService = context->getModel().getVariableService()) { - varService->declareVariable("AuthCtrlr", "LocalAuthorizeOffline", false, MO_VARIABLE_VOLATILE); - varService->declareVariable("OCPPCommCtrlr", "OfflineThreshold", false, MO_VARIABLE_VOLATILE); - varService->declareVariable("TxCtrlr", "StopTxOnInvalidId", false, MO_VARIABLE_VOLATILE); + varService->declareVariable("AuthCtrlr", "LocalAuthorizeOffline", false, MicroOcpp::Variable::Mutability::ReadOnly, false); } } } From 20e8da7b76a3f5f3e50facab018a457ecdbcca59 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Sun, 3 Nov 2024 11:26:14 +0100 Subject: [PATCH 40/41] update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f300808..6ccc07f 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ [![Build (Docker)](https://github.com/matth-x/MicroOcppSimulator/workflows/Docker/badge.svg)]((https://github.com/matth-x/MicroOcppSimulator/actions)) [![Build (WebAssembly)](https://github.com/matth-x/MicroOcppSimulator/workflows/WebAssembly/badge.svg)]((https://github.com/matth-x/MicroOcppSimulator/actions)) -Tester / Demo App for the [MicroOcpp](https://github.com/matth-x/MicroOcpp) Client, running on native Ubuntu, WSL, WebAssembly or MSYS2. Online demo: [Try it](https://demo.micro-ocpp.com/) +Tester / Demo App for the [MicroOCPP](https://github.com/matth-x/MicroOcpp) Client, running on native Ubuntu, WSL, WebAssembly or MSYS2. Online demo: [Try it](https://demo.micro-ocpp.com/) [![Screenshot](https://github.com/agruenb/arduino-ocpp-dashboard/blob/master/docs/img/status_page.png)](https://demo.micro-ocpp.com/) The Simulator has two purposes: -- As a development tool, it allows to run MicroOcpp directly on the host computer and simplifies the development (no flashing of the microcontroller required) -- As a demonstration tool, it allows backend operators to test and use MicroOcpp without the need to set up an actual microcontroller or to buy an actual charger with MicroOcpp. +- As a development tool, it allows to run MicroOCPP directly on the host computer and simplifies the development (no flashing of the microcontroller required) +- As a demonstration tool, it allows backend operators to test and use MicroOCPP without the need to set up an actual microcontroller or to buy an actual charger with MicroOCPP. That means that the Simulator runs on your computer and connects to an OCPP server using the same software like a microcontroller. It provides a Graphical User Interface to show the connection status and to trigger simulated charging From 2cb07cdbe53954a694a29336ab31eac2d2b48673 Mon Sep 17 00:00:00 2001 From: matth-x <63792403+matth-x@users.noreply.github.com> Date: Sun, 3 Nov 2024 11:49:54 +0100 Subject: [PATCH 41/41] update dependencies --- lib/MicroOcpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/MicroOcpp b/lib/MicroOcpp index fecac09..4b89f46 160000 --- a/lib/MicroOcpp +++ b/lib/MicroOcpp @@ -1 +1 @@ -Subproject commit fecac0978b5ab29548f97e6384b4b63964ed0e32 +Subproject commit 4b89f4649d65c83cc559e4cc7c65f430db482217