diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..378eac2
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+build
diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml
new file mode 100644
index 0000000..5827bab
--- /dev/null
+++ b/.github/workflows/build_docker.yml
@@ -0,0 +1,25 @@
+name: Docker
+
+on:
+ push:
+ branches:
+ - main
+
+ 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..f7fa298
--- /dev/null
+++ b/.github/workflows/build_ubuntu.yml
@@ -0,0 +1,27 @@
+name: Ubuntu
+
+on:
+ push:
+ branches:
+ - main
+
+ 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..b66fa97
--- /dev/null
+++ b/.github/workflows/build_wasm.yml
@@ -0,0 +1,27 @@
+name: WebAssembly
+
+on:
+ push:
+ branches:
+ - main
+
+ 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/.gitignore b/.gitignore
index 05ea832..cffd789 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
*.exe
*.obj
build_and_run.sh
-ao_store
+mo_store/*
+!mo_store/README.md
build
diff --git a/.gitmodules b/.gitmodules
index 1df87c2..ceb826b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,15 +1,18 @@
[submodule "lib/ArduinoJson"]
path = lib/ArduinoJson
url = https://github.com/bblanchon/ArduinoJson.git
-[submodule "lib/ArduinoOcpp"]
- path = lib/ArduinoOcpp
- url = https://github.com/matth-x/ArduinoOcpp
-[submodule "lib/ArduinoOcppMongoose"]
- path = lib/ArduinoOcppMongoose
- url = https://github.com/matth-x/ArduinoOcppMongoose
+[submodule "lib/MicroOcpp"]
+ path = lib/MicroOcpp
+ url = https://github.com/matth-x/MicroOcpp
+[submodule "lib/MicroOcppMongoose"]
+ path = lib/MicroOcppMongoose
+ url = https://github.com/matth-x/MicroOcppMongoose
[submodule "lib/mongoose"]
path = lib/mongoose
url = https://github.com/cesanta/mongoose
[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 98b09b8..31f02a5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,41 +1,140 @@
-# matth-x/ArduinoOcpp
-# 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(AO_SM_SRC
+set(CMAKE_CXX_STANDARD 11)
+
+set(MO_SIM_SRC
src/evse.cpp
src/main.cpp
- src/webserver.cpp
+ src/api.cpp
+)
+
+set(MO_SIM_MG_SRC
+ src/net_mongoose.cpp
lib/mongoose/mongoose.c
)
-project(ArduinoOcppSimulator
+set(MO_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(ao_simulator ${AO_SM_SRC})
+add_compile_definitions(
+ MO_PLATFORM=MO_PLATFORM_UNIX
+ MO_NUMCONNECTORS=3
+ MO_TRAFFIC_OUT
+ MO_DBG_LEVEL=MO_DL_INFO
+ MO_FILENAME_PREFIX="./mo_store/"
+ MO_ENABLE_V201=1
+ MO_ENABLE_MBEDTLS=1
+ MO_ENABLE_TIMESTAMP_MILLISECONDS=1
+ MO_MG_USE_VERSION=MO_MG_V715
+)
-target_include_directories(ao_simulator PUBLIC
- "${PROJECT_DIR}/lib/ArduinoJson/src"
- "${PROJECT_DIR}/lib/mongoose"
- )
+add_executable(mo_simulator ${MO_SIM_SRC} ${MO_SIM_MG_SRC})
-add_compile_definitions(
- MG_ENABLE_OPENSSL=1
- AO_NUMCONNECTORS=3
- AO_TRAFFIC_OUT
- AO_DBG_LEVEL=AO_DL_INFO
- AO_FILENAME_PREFIX="./ao_store/"
+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
+ MO_NETLIB=MO_NETLIB_MONGOOSE
+)
+
+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_TLS=MG_TLS_MBED
+ )
+
+endif()
+
+add_subdirectory(lib/MicroOcppMongoose)
+target_link_libraries(mo_simulator PUBLIC MicroOcppMongoose)
+
+# experimental WebAssembly port
+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"
+ "${CMAKE_CURRENT_SOURCE_DIR}/lib/MicroOcppMongoose/src"
+)
+
+target_compile_definitions(mo_simulator_wasm PUBLIC
+ MO_NETLIB=MO_NETLIB_WASM
+)
+
+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
+ -Os
+)
-add_subdirectory(lib/ArduinoOcpp)
-target_link_libraries(ao_simulator PUBLIC ArduinoOcpp)
+target_link_libraries(mo_simulator_wasm PUBLIC MicroOcpp)
-add_subdirectory(lib/ArduinoOcppMongoose)
-target_link_libraries(ao_simulator PUBLIC ArduinoOcppMongoose)
+if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
+ set(CMAKE_EXECUTABLE_SUFFIX ".mjs")
+endif()
-target_link_libraries(ao_simulator PUBLIC ssl crypto)
+if(WIN32)
+ target_link_libraries(mo_simulator PUBLIC wsock32 ws2_32)
+ set(CMAKE_EXE_LINKER_FLAGS "-static-libgcc -static-libstdc++ -static")
+endif()
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..d90093e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,29 @@
+# Use Alpine Linux as the base image
+FROM alpine:latest
+
+# Update package lists and install necessary dependencies
+RUN apk update && \
+ apk add --no-cache \
+ git \
+ cmake \
+ openssl-dev \
+ build-base
+
+# Set the working directory inside the container
+WORKDIR /MicroOcppSimulator
+
+# Copy your application files to the container's working directory
+COPY . .
+
+RUN git submodule init && git submodule update
+RUN cmake -S . -B ./build
+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
+
+# Expose port 8000
+EXPOSE 8000
+
+# Run the shell script inside the container
+CMD ["./build/mo_simulator"]
diff --git a/README.md b/README.md
index f96a892..6ccc07f 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,48 @@
-#
ArduinoOcppSimulator
+#
MicroOcppSimulator
-Tester / Demo App for the [ArduinoOcpp](https://github.com/matth-x/ArduinoOcpp) Client, running on native Ubuntu or the WSL.
+[]((https://github.com/matth-x/MicroOcppSimulator/actions))
+[]((https://github.com/matth-x/MicroOcppSimulator/actions))
+[]((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/)
+
+[](https://demo.micro-ocpp.com/)
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).
-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
-## Installation
+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.
-On Windows, get the Windows Subsystem for Linux (WSL): [https://ubuntu.com/wsl](https://ubuntu.com/wsl)
+Firstly, build the image:
+
+```shell
+docker build -t matthx/microocppsimulator:latest .
+```
+
+Then run the image:
+
+```shell
+docker run -p 8000:8000 matthx/microocppsimulator:latest
+```
+
+The Simulator should be up and running now on [localhost:8000](http://localhost:8000).
+
+## Installation (Ubuntu or 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.
-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
@@ -25,26 +51,26 @@ 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:
+Navigate to the copy of the Simulator and build:
```shell
-cd ArduinoOcppSimulator
-mkdir build
-mkdir ao_store
+cd MicroOcppSimulator
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.
+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 +78,46 @@ 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.
+## 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/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/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/ArduinoOcpp b/lib/ArduinoOcpp
deleted file mode 160000
index 894a207..0000000
--- a/lib/ArduinoOcpp
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 894a2071bd0eec96c26647e6927ccc851b4c8052
diff --git a/lib/ArduinoOcppMongoose b/lib/ArduinoOcppMongoose
deleted file mode 160000
index 85d6f27..0000000
--- a/lib/ArduinoOcppMongoose
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 85d6f27c17b87ae626e47599b21ac3bfdb319903
diff --git a/lib/MicroOcpp b/lib/MicroOcpp
new file mode 160000
index 0000000..4b89f46
--- /dev/null
+++ b/lib/MicroOcpp
@@ -0,0 +1 @@
+Subproject commit 4b89f4649d65c83cc559e4cc7c65f430db482217
diff --git a/lib/MicroOcppMongoose b/lib/MicroOcppMongoose
new file mode 160000
index 0000000..e228297
--- /dev/null
+++ b/lib/MicroOcppMongoose
@@ -0,0 +1 @@
+Subproject commit e2282976eed4b98e200e5987f2dded5313902cab
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/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/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.
diff --git a/public/bundle.html.gz b/public/bundle.html.gz
index 1011336..d419b54 100644
Binary files a/public/bundle.html.gz and b/public/bundle.html.gz differ
diff --git a/src/api.cpp b/src/api.cpp
new file mode 100644
index 0000000..054f37c
--- /dev/null
+++ b/src/api.cpp
@@ -0,0 +1,349 @@
+// matth-x/MicroOcppSimulator
+// Copyright Matthias Akstaller 2022 - 2024
+// GPL-3.0 License
+
+#include "api.h"
+#include "mongoose.h"
+
+#include
+#include
+#include
+#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 (pattern[pi]) {
+ if (query[qi] && 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) {
+
+ MO_DBG_VERBOSE("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) {
+ MO_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;
+ }
+ }
+
+ MO_DBG_VERBOSE("connectorId = %u", connectorId);
+
+ Evse *evse = nullptr;
+ if (connectorId >= 1 && connectorId < MO_NUMCONNECTORS) {
+ evse = &connectors[connectorId-1];
+ }
+
+ //start different api endpoints
+ if(str_match(endpoint, "/connectors")) {
+ MO_DBG_VERBOSE("query connectors");
+ response.add("1");
+ response.add("2");
+ status = 200;
+ } else if(str_match(endpoint, "/connector/*/evse")){
+ MO_DBG_VERBOSE("query evse");
+ if (!evse) {
+ return 404;
+ }
+
+ if (method == MicroOcpp::Method::POST) {
+ if (request.containsKey("evPlugged")) {
+ evse->setEvPlugged(request["evPlugged"]);
+ }
+ if (request.containsKey("evsePlugged")) {
+ evse->setEvsePlugged(request["evsePlugged"]);
+ }
+ if (request.containsKey("evReady")) {
+ evse->setEvReady(request["evReady"]);
+ }
+ if (request.containsKey("evseReady")) {
+ evse->setEvseReady(request["evseReady"]);
+ }
+ }
+
+ response["evPlugged"] = evse->getEvPlugged();
+ response["evsePlugged"] = evse->getEvsePlugged();
+ response["evReady"] = evse->getEvReady();
+ response["evseReady"] = evse->getEvseReady();
+ response["chargePointStatus"] = evse->getOcppStatus();
+ status = 200;
+ } else if(str_match(endpoint, "/connector/*/meter")){
+ MO_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")){
+ MO_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")){
+ MO_DBG_VERBOSE("query smartcharging");
+ if (!evse) {
+ return 404;
+ }
+
+ response["maxPower"] = evse->getSmartChargingMaxPower();
+ response["maxCurrent"] = evse->getSmartChargingMaxCurrent();
+ status = 200;
+ } else {
+ return 404;
+ }
+
+ 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;
+}
+
+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
new file mode 100644
index 0000000..a2de1f8
--- /dev/null
+++ b/src/api.h
@@ -0,0 +1,24 @@
+// matth-x/MicroOcppSimulator
+// Copyright Matthias Akstaller 2022 - 2024
+// GPL-3.0 License
+
+#ifndef MO_SIM_API_H
+#define MO_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);
+
+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 68fbe2a..a0261d6 100644
--- a/src/evse.cpp
+++ b/src/evse.cpp
@@ -1,59 +1,72 @@
+// matth-x/MicroOcppSimulator
+// Copyright Matthias Akstaller 2022 - 2024
+// GPL-3.0 License
+
#include "evse.h"
-#include
-#include
-#include
-#include
-#include
-#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
#include
#include
-#define SIMULATOR_FN AO_FILENAME_PREFIX "simulator.jsn"
-
Evse::Evse(unsigned int connectorId) : connectorId{connectorId} {
}
-ArduinoOcpp::Connector *getConnector(unsigned int connectorId) {
- if (!getOcppContext()) {
- AO_DBG_ERR("unitialized");
- return nullptr;
- }
- return getOcppContext()->getModel().getConnector(connectorId);
-}
-
void Evse::setup() {
- auto connector = getConnector(connectorId);
- if (!connector) {
- AO_DBG_ERR("invalid state");
- return;
+
+#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, MicroOcpp::Variable::Mutability::ReadOnly, false);
+ }
+ }
}
+#endif
char key [30] = {'\0'};
snprintf(key, 30, "evPlugged_cId_%u", connectorId);
- trackEvPlugged = ArduinoOcpp::declareConfiguration(key, false, SIMULATOR_FN, false, false);
+ 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);
- trackEvReady = ArduinoOcpp::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 = ArduinoOcpp::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
- });
+ MicroOcpp::configuration_load(SIMULATOR_FN);
- connector->setEvReadyInput([this] () -> bool {
- return *trackEvReady; //return if J1772 is in State C
- });
+ setConnectorPluggedInput([this] () -> bool {
+ return trackEvPluggedBool->getBool(); //return if J1772 is in State B or C
+ }, connectorId);
- connector->setEvseReadyInput([this] () -> bool {
- return *trackEvseReady;
- });
+ setEvReadyInput([this] () -> bool {
+ return trackEvReadyBool->getBool(); //return if J1772 is in State C
+ }, connectorId);
+
+ 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;
@@ -95,119 +108,145 @@ void Evse::setup() {
});
setSmartChargingPowerOutput([this] (float limit) {
- AO_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);
}
void Evse::loop() {
- if (auto connector = getConnector(connectorId)) {
- auto curStatus = connector->getStatus();
- if (status.compare(ArduinoOcpp::Ocpp16::cstrFromOcppEveState(curStatus))) {
- status = ArduinoOcpp::Ocpp16::cstrFromOcppEveState(curStatus);
- }
+ auto curStatus = getChargePointStatus(connectorId);
+
+ if (status.compare(MicroOcpp::cstrFromOcppEveState(curStatus))) {
+ status = MicroOcpp::cstrFromOcppEveState(curStatus);
}
+ bool simulate_isCharging = ocppPermitsCharge(connectorId) && trackEvPluggedBool->getBool() && trackEvsePluggedBool->getBool() && trackEvReadyBool->getBool() && trackEvseReadyBool->getBool();
- 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) (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;
}
}
-void Evse::presentNfcTag(const char *uid_cstr) {
- if (!uid_cstr) {
- AO_DBG_ERR("invalid argument");
+void Evse::presentNfcTag(const char *uid) {
+ if (!uid) {
+ MO_DBG_ERR("invalid argument");
return;
}
- std::string uid = uid_cstr;
- auto connector = getConnector(connectorId);
- if (!connector) {
- AO_DBG_ERR("invalid state");
- return;
+
+#if MO_ENABLE_V201
+ if (auto context = getOcppContext()) {
+ if (context->getVersion().major == 2) {
+ presentNfcTag(uid, "ISO14443");
+ return;
+ }
}
+#endif
- if (connector->getTransaction() && connector->getTransaction()->isActive()) {
- if (!uid.compare(connector->getTransaction()->getIdTag())) {
- connector->endTransaction(uid.c_str());
+ if (isTransactionActive(connectorId)) {
+ if (!strcmp(uid, getTransactionIdTag(connectorId))) {
+ endTransaction(uid, "Local", connectorId);
} else {
- AO_DBG_INFO("RFID card denied");
+ MO_DBG_INFO("RFID card denied");
}
} else {
- connector->beginTransaction(uid.c_str());
+ 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 (!trackEvPlugged) return;
- *trackEvPlugged = plugged;
- ArduinoOcpp::configuration_save();
+ 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::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 (!trackEvReady) return;
- *trackEvReady = ready;
- ArduinoOcpp::configuration_save();
+ 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;
- ArduinoOcpp::configuration_save();
+ 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() {
- auto connector = getConnector(connectorId);
- if (!connector) {
- AO_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) {
- AO_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) {
- AO_DBG_ERR("invalid state");
- return false;
- }
- return connector->ocppPermitsCharge();
+ return ocppPermitsCharge(connectorId);
}
int Evse::getPower() {
@@ -216,7 +255,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..eaf37dc 100644
--- a/src/evse.h
+++ b/src/evse.h
@@ -1,24 +1,35 @@
+// matth-x/MicroOcppSimulator
+// Copyright Matthias Akstaller 2022 - 2024
+// GPL-3.0 License
+
#ifndef EVSE_H
#define EVSE_H
#include
#include
-#include
+#include
+#include
+#define SIMULATOR_FN MO_FILENAME_PREFIX "simulator.jsn"
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 trackEvsePluggedBool;
+ std::string trackEvsePluggedKey;
+ 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;
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;
@@ -31,10 +42,18 @@ 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();
+ void setEvsePlugged(bool plugged);
+
+ bool getEvsePlugged();
+
void setEvReady(bool ready);
bool getEvReady();
@@ -48,7 +67,7 @@ class Evse {
bool chargingPermitted();
bool isCharging() {
- return chargingPermitted() && trackEvReady;
+ return chargingPermitted() && trackEvReadyBool->getBool();
}
const char *getOcppStatus() {
@@ -89,6 +108,6 @@ class Evse {
};
-extern std::array connectors;
+extern std::array connectors;
#endif
diff --git a/src/main.cpp b/src/main.cpp
index a2721ea..0e945fd 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,54 +1,233 @@
+// matth-x/MicroOcppSimulator
+// Copyright Matthias Akstaller 2022 - 2024
+// GPL-3.0 License
+
#include
-#include "mongoose.h"
-#include
-#include
+#include
+
+#include
+#include
+#include
+#include
#include "evse.h"
-#include "webserver.h"
+#include "api.h"
+
+#include
+
+#if MO_NUMCONNECTORS == 3
+std::array connectors {{1,2}};
+#else
+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
+#if MO_NETLIB == MO_NETLIB_MONGOOSE
+#include "mongoose.h"
+#include
+
+#include "net_mongoose.h"
+
struct mg_mgr mgr;
-ArduinoOcpp::AOcppMongooseClient *osock;
+MicroOcpp::MOcppMongooseClient *osock;
+
+#elif MO_NETLIB == MO_NETLIB_WASM
+#include
+
+#include
+
+#include "net_wasm.h"
+
+MicroOcpp::Connection *conn = nullptr;
-#if AO_NUMCONNECTORS == 3
-std::array connectors {{1,2}};
#else
-std::array connectors {{1}};
+#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
+ */
+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,
+ 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();
+ }
+}
+
+/*
+ * Execute one loop iteration
+ */
+void app_loop() {
+ mocpp_loop();
+ for (unsigned int i = 0; i < connectors.size(); i++) {
+ connectors[i].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
+ 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);
- 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);
- auto filesystem = ArduinoOcpp::makeDefaultFilesystemAdapter(ArduinoOcpp::FilesystemOpt::Use_Mount_FormatOnFail);
+ 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();
- osock = new ArduinoOcpp::AOcppMongooseClient(&mgr,
+ 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",
"",
"",
- filesystem);
-
- server_initialize(osock);
- ocpp_initialize(*osock,
- ChargerCredentials("Demo Charger", "My Company Ltd."),
- filesystem);
+ filesystem,
+ g_isOcpp201 ?
+ MicroOcpp::ProtocolVersion{2,0,1} :
+ MicroOcpp::ProtocolVersion{1,6}
+ );
- for (unsigned int i = 0; i < connectors.size(); i++) {
- connectors[i].setup();
- }
+ 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) {
+ g_runSimulator = false;
+ });
- for (;;) { // Block forever
+ while (g_runSimulator) { //Run Simulator until OCPP Reset is executed or user presses Ctrl+C
mg_mgr_poll(&mgr, 100);
- ocpp_loop();
- for (unsigned int i = 0; i < connectors.size(); i++) {
- connectors[i].loop();
+ 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);
+ free(api_cert.buf);
+ free(api_key.buf);
return 0;
}
+
+#elif MO_NETLIB == MO_NETLIB_WASM
+
+int main() {
+
+ printf("[WASM] start\n");
+
+ auto filesystem = MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Deactivate);
+
+ conn = wasm_ocpp_connection_init(nullptr, 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_mongoose.cpp b/src/net_mongoose.cpp
new file mode 100644
index 0000000..e203279
--- /dev/null
+++ b/src/net_mongoose.cpp
@@ -0,0 +1,160 @@
+// matth-x/MicroOcppSimulator
+// Copyright Matthias Akstaller 2022 - 2024
+// GPL-3.0 License
+
+#include "net_mongoose.h"
+#include "evse.h"
+#include "api.h"
+#include
+#include
+#include
+#include
+#include
+
+//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"
+
+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, 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;
+}
+
+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_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);
+
+ MicroOcpp::Method method = MicroOcpp::Method::UNDEFINED;
+
+ if (!mg_strcasecmp(message_data->method, mg_str("POST"))) {
+ method = MicroOcpp::Method::POST;
+ MO_DBG_VERBOSE("POST");
+ } else if (!mg_strcasecmp(message_data->method, mg_str("GET"))) {
+ method = MicroOcpp::Method::GET;
+ MO_DBG_VERBOSE("GET");
+ }
+
+ //start different api endpoints
+ 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);
+
+ if (method == MicroOcpp::Method::POST) {
+ if (auto val = mg_json_get_str(json, "$.backendUrl")) {
+ ao_sock->setBackendUrl(val);
+ }
+ if (auto val = mg_json_get_str(json, "$.chargeBoxId")) {
+ ao_sock->setChargeBoxId(val);
+ }
+ 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) {
+ webSocketPingIntervalInt->setInt(val);
+ }
+ }
+ {
+ auto val = mg_json_get_long(json, "$.reconnectInterval", -1);
+ if (val > 0) {
+ reconnectIntervalInt->setInt(val);
+ }
+ }
+ if (auto val = mg_json_get_str(json, "$.dnsUrl")) {
+ MO_DBG_WARN("dnsUrl not implemented");
+ (void)val;
+ }
+ MicroOcpp::configuration_save();
+ }
+ StaticJsonDocument<256> doc;
+ doc["backendUrl"] = ao_sock->getBackendUrl();
+ doc["chargeBoxId"] = ao_sock->getChargeBoxId();
+ doc["authorizationKey"] = ao_sock->getAuthKey();
+ doc["pingInterval"] = webSocketPingIntervalInt->getInt();
+ doc["reconnectInterval"] = reconnectIntervalInt->getInt();
+ std::string serialized;
+ serializeJson(doc, serialized);
+ 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 8192
+ char resp_buf [RESP_BUF_SIZE];
+
+ //replace endpoint-body separator by null
+ if (char *c = strchr((char*) message_data->uri.buf, ' ')) {
+ *c = '\0';
+ }
+
+ 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;
+ 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, "API endpoint not found");
+ }
+ }
+}
diff --git a/src/net_mongoose.h b/src/net_mongoose.h
new file mode 100644
index 0000000..aa88717
--- /dev/null
+++ b/src/net_mongoose.h
@@ -0,0 +1,22 @@
+// matth-x/MicroOcppSimulator
+// Copyright Matthias Akstaller 2022 - 2024
+// GPL-3.0 License
+
+#ifndef MO_NET_MONGOOSE_H
+#define MO_NET_MONGOOSE_H
+
+#if MO_NETLIB == MO_NETLIB_MONGOOSE
+
+#include "mongoose.h"
+
+namespace MicroOcpp {
+class MOcppMongooseClient;
+}
+
+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);
+
+#endif //MO_NETLIB == MO_NETLIB_MONGOOSE
+
+#endif
diff --git a/src/net_wasm.cpp b/src/net_wasm.cpp
new file mode 100644
index 0000000..99ac1f2
--- /dev/null
+++ b/src/net_wasm.cpp
@@ -0,0 +1,439 @@
+// matth-x/MicroOcppSimulator
+// Copyright Matthias Akstaller 2022 - 2024
+// GPL-3.0 License
+
+#include "net_wasm.h"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#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::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_int; //minimum time between two connect trials in s
+ unsigned long last_reconnection_attempt {-1UL / 2UL};
+ 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;};
+
+ 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()) {
+ MO_DBG_DEBUG("WS unconnected");
+ } else if (mocpp_tick_ms() - last_recv >= (ws_ping_interval_int && ws_ping_interval_int->getInt() > 0 ? (ws_ping_interval_int->getInt() * 1000UL) : 0UL) + WS_UNRESPONSIVE_THRESHOLD_MS) {
+ //WS connected but unresponsive
+ MO_DBG_DEBUG("WS unresponsive");
+ }
+ }
+
+ if (websocket && isConnectionOpen() &&
+ stale_timeout_int && stale_timeout_int->getInt() > 0 && mocpp_tick_ms() - last_recv >= (stale_timeout_int->getInt() * 1000UL)) {
+ MO_DBG_INFO("connection %s -- stale, reconnect", url.c_str());
+ reconnect();
+ return;
+ }
+
+ if (websocket && isConnectionOpen() &&
+ ws_ping_interval_int && ws_ping_interval_int->getInt() > 0 && mocpp_tick_ms() - last_hb >= (ws_ping_interval_int->getInt() * 1000UL)) {
+ last_hb = mocpp_tick_ms();
+ MO_DBG_VERBOSE("omit heartbeat");
+ }
+
+ if (websocket != NULL) { //connection pointer != NULL means that the socket is still open
+ return;
+ }
+
+ if (url.empty()) {
+ //cannot open OCPP connection: credentials missing
+ return;
+ }
+
+ if (reconnect_interval_int && reconnect_interval_int->getInt() > 0 && mocpp_tick_ms() - last_reconnection_attempt < (reconnect_interval_int->getInt() * 1000UL)) {
+ return;
+ }
+
+ MO_DBG_DEBUG("(re-)connect to %s", url.c_str());
+
+ last_reconnection_attempt = mocpp_tick_ms();
+
+ 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) {
+ MO_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);
+ 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) {
+ MO_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);
+ 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)) {
+ MO_DBG_WARN("processing input message failed");
+ }
+ conn->updateRcvTimer();
+ return true;
+ });
+ if (ret_message < 0) {
+ MO_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);
+ 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) {
+ MO_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);
+ 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) {
+ MO_DBG_ERR("emscripten_websocket_set_onopen_callback: %i", ret_open);
+ }
+ }
+
+ 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_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_int = declareConfiguration(
+ MO_CONFIG_EXT_PREFIX "ReconnectInterval", 10, CONFIGURATION_VOLATILE);
+ stale_timeout_int = declareConfiguration(
+ MO_CONFIG_EXT_PREFIX "StaleTimeout", 300, CONFIGURATION_VOLATILE);
+
+ reloadConfigs(); //load WS creds with configs values
+
+ MO_DBG_DEBUG("connection initialized");
+
+ maintainWsConn();
+ }
+
+ ~WasmOcppConnection() {
+ if (websocket) {
+ emscripten_websocket_delete(websocket);
+ websocket = NULL;
+ }
+ }
+
+ void loop() override {
+ maintainWsConn();
+ }
+
+ bool sendTXT(const char *msg, size_t length) override {
+ if (!websocket || !isConnectionOpen()) {
+ return false;
+ }
+
+ if (auto ret = emscripten_websocket_send_utf8_text(websocket, msg) < 0) {
+ MO_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) {
+ MO_DBG_ERR("invalid argument");
+ return;
+ }
+
+ if (setting_backend_url_str) {
+ setting_backend_url_str->setString(backend_url_cstr);
+ configuration_save();
+ }
+ }
+
+ void setChargeBoxId(const char *cb_id_cstr) {
+ if (!cb_id_cstr) {
+ MO_DBG_ERR("invalid argument");
+ return;
+ }
+
+ if (setting_cb_id_str) {
+ setting_cb_id_str->setString(cb_id_cstr);
+ configuration_save();
+ }
+ }
+
+ void setAuthKey(const char *auth_key_cstr) {
+ if (!auth_key_cstr) {
+ MO_DBG_ERR("invalid argument");
+ return;
+ }
+
+ if (setting_auth_key_str) {
+ setting_auth_key_str->setString(auth_key_cstr);
+ configuration_save();
+ }
+ }
+
+ void reloadConfigs() {
+
+ 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);
+ }
+
+ if (!auth_key.empty()) {
+ MO_DBG_WARN("WASM app does not support Securiy Profile 2 yet");
+ } else {
+ MO_DBG_DEBUG("no authentication");
+ (void) 0;
+ }
+ }
+
+ 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 *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 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) {
+ MO_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) {
+ MO_DBG_ERR("no websocket instance");
+ return nullptr;
+ }
+ StaticJsonDocument<512> request;
+ if (*body) {
+ auto err = deserializeJson(request, body);
+ if (err) {
+ MO_DBG_WARN("malformatted body: %s", err.c_str());
+ return nullptr;
+ }
+ }
+
+ auto webSocketPingInterval = declareConfiguration("WebSocketPingInterval", 5, CONFIGURATION_VOLATILE);
+ auto reconnectInterval = declareConfiguration(MO_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"] | "");
+ }
+ wasm_ocpp_connection_instance->reloadConfigs();
+ if (request.containsKey("pingInterval")) {
+ webSocketPingInterval->setInt(request["pingInterval"] | 0);
+ }
+ if (request.containsKey("reconnectInterval")) {
+ reconnectInterval->setInt(request["reconnectInterval"] | 0);
+ }
+ if (request.containsKey("dnsUrl")) {
+ MO_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 ? webSocketPingInterval->getInt() : 0;
+ response["reconnectInterval"] = reconnectInterval ? reconnectInterval->getInt() : 0;
+ serializeJson(response, wasm_resp_buf, MO_WASM_RESP_BUF_SIZE);
+ return wasm_resp_buf;
+ }
+
+ //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) {
+ //200: HTTP status code Success
+ MO_DBG_DEBUG("API resp: %s", wasm_resp_buf);
+ return wasm_resp_buf;
+ } else {
+ MO_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..bca1b5e
--- /dev/null
+++ b/src/net_wasm.h
@@ -0,0 +1,16 @@
+// matth-x/MicroOcppSimulator
+// Copyright Matthias Akstaller 2022 - 2024
+// GPL-3.0 License
+
+#ifndef MO_NET_WASM_H
+#define MO_NET_WASM_H
+
+#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 //MO_NETLIB == MO_NETLIB_WASM
+
+#endif
diff --git a/src/webserver.cpp b/src/webserver.cpp
deleted file mode 100644
index e47fd32..0000000
--- a/src/webserver.cpp
+++ /dev/null
@@ -1,213 +0,0 @@
-#include "webserver.h"
-#include "evse.h"
-#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"
-
-//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;
-
-void server_initialize(ArduinoOcpp::AOcppMongooseClient *osock) {
- ao_sock = osock;
-}
-
-char* toStringPtr(std::string cppString){
- char *cstr = new char[cppString.length() + 1];
- strcpy(cstr, cppString.c_str());
- 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;
- 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;
-
- AO_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");
- } else if (!mg_vcasecmp(&message_data->method, "GET")) {
- method = Method::GET;
- AO_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;
- }
- }
-
- AO_DBG_VERBOSE("connectorId = %u", connectorId);
-
- Evse *evse = nullptr;
- if (connectorId >= 1 && connectorId < AO_NUMCONNECTORS) {
- evse = &connectors[connectorId-1];
- }
-
- //start different api endpoints
- if(mg_http_match_uri(message_data, "/api/connectors")) {
- AO_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")){
- 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);
-
- if (method == Method::POST) {
- if (auto val = mg_json_get_str(json, "$.backendUrl")) {
- ao_sock->setBackendUrl(val);
- }
- if (auto val = mg_json_get_str(json, "$.chargeBoxId")) {
- ao_sock->setChargeBoxId(val);
- }
- if (auto val = mg_json_get_str(json, "$.authorizationKey")) {
- ao_sock->setAuthKey(val);
- }
- {
- auto val = mg_json_get_long(json, "$.pingInterval", -1);
- if (val > 0) {
- *webSocketPingInterval = (int) val;
- }
- }
- {
- auto val = mg_json_get_long(json, "$.reconnectInterval", -1);
- if (val > 0) {
- *reconnectInterval = (int) val;
- }
- }
- if (auto val = mg_json_get_str(json, "$.dnsUrl")) {
- AO_DBG_WARN("dnsUrl not implemented");
- (void)val;
- }
- ArduinoOcpp::configuration_save();
- }
- StaticJsonDocument<256> doc;
- doc["backendUrl"] = ao_sock->getBackendUrl();
- doc["chargeBoxId"] = ao_sock->getChargeBoxId();
- doc["authorizationKey"] = ao_sock->getAuthKey();
- doc["pingInterval"] = (int) *webSocketPingInterval;
- doc["reconnectInterval"] = (int) *reconnectInterval;
- 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/*/evse")){
- AO_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")){
- AO_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")){
- AO_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")){
- AO_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;
- }
- //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");
- }
- }
-}
diff --git a/src/webserver.h b/src/webserver.h
deleted file mode 100644
index 629ec86..0000000
--- a/src/webserver.h
+++ /dev/null
@@ -1,9 +0,0 @@
-#include "mongoose.h"
-
-namespace ArduinoOcpp {
-class AOcppMongooseClient;
-}
-
-void server_initialize(ArduinoOcpp::AOcppMongooseClient *osock);
-
-void http_serve(struct mg_connection *c, int ev, void *ev_data, void *fn_data);
diff --git a/webapp-src b/webapp-src
index f7a9c94..015a0e8 160000
--- a/webapp-src
+++ b/webapp-src
@@ -1 +1 @@
-Subproject commit f7a9c9428f54397259006deb3c99b5e9fa161e94
+Subproject commit 015a0e816ca4fc617e67acdbe7d067688d6b3939