Repository: matth-x/MicroOcpp Branch: main Commit: ee1271b7e8d9 Files: 276 Total size: 2.2 MB Directory structure: gitextract_ls9b0kd7/ ├── .github/ │ └── workflows/ │ ├── documentation.yml │ ├── esp-idf.yml │ ├── pio.yml │ ├── platformless.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── SConscript.py ├── docs/ │ ├── benchmarks.md │ ├── index.md │ ├── intro-tech.md │ ├── migration.md │ ├── modules.md │ ├── prerequisites.md │ ├── security.md │ └── stylesheets/ │ └── extra.css ├── examples/ │ ├── ESP/ │ │ └── main.cpp │ ├── ESP-IDF/ │ │ ├── CMakeLists.txt │ │ ├── Makefile │ │ ├── README.md │ │ ├── components/ │ │ │ ├── ArduinoJson/ │ │ │ │ └── .gitkeep │ │ │ ├── ArduinoOcpp/ │ │ │ │ └── .gitkeep │ │ │ ├── ArduinoOcppMongoose/ │ │ │ │ └── .gitkeep │ │ │ ├── README.md │ │ │ └── mongoose/ │ │ │ └── .gitkeep │ │ ├── main/ │ │ │ ├── CMakeLists.txt │ │ │ ├── Kconfig.projbuild │ │ │ ├── component.mk │ │ │ └── main.c │ │ ├── partitions.csv │ │ └── sdkconfig │ └── ESP-TLS/ │ └── main.cpp ├── library.json ├── library.properties ├── mkdocs.yml ├── platformio.ini ├── src/ │ ├── MicroOcpp/ │ │ ├── Core/ │ │ │ ├── Configuration.cpp │ │ │ ├── Configuration.h │ │ │ ├── ConfigurationContainer.cpp │ │ │ ├── ConfigurationContainer.h │ │ │ ├── ConfigurationContainerFlash.cpp │ │ │ ├── ConfigurationContainerFlash.h │ │ │ ├── ConfigurationKeyValue.cpp │ │ │ ├── ConfigurationKeyValue.h │ │ │ ├── ConfigurationOptions.h │ │ │ ├── Configuration_c.cpp │ │ │ ├── Configuration_c.h │ │ │ ├── Connection.cpp │ │ │ ├── Connection.h │ │ │ ├── Context.cpp │ │ │ ├── Context.h │ │ │ ├── FilesystemAdapter.cpp │ │ │ ├── FilesystemAdapter.h │ │ │ ├── FilesystemUtils.cpp │ │ │ ├── FilesystemUtils.h │ │ │ ├── Ftp.h │ │ │ ├── FtpMbedTLS.cpp │ │ │ ├── FtpMbedTLS.h │ │ │ ├── Memory.cpp │ │ │ ├── Memory.h │ │ │ ├── OcppError.h │ │ │ ├── Operation.cpp │ │ │ ├── Operation.h │ │ │ ├── OperationRegistry.cpp │ │ │ ├── OperationRegistry.h │ │ │ ├── Request.cpp │ │ │ ├── Request.h │ │ │ ├── RequestCallbacks.h │ │ │ ├── RequestQueue.cpp │ │ │ ├── RequestQueue.h │ │ │ ├── Time.cpp │ │ │ ├── Time.h │ │ │ ├── UuidUtils.cpp │ │ │ └── UuidUtils.h │ │ ├── Debug.cpp │ │ ├── Debug.h │ │ ├── Model/ │ │ │ ├── Authorization/ │ │ │ │ ├── AuthorizationData.cpp │ │ │ │ ├── AuthorizationData.h │ │ │ │ ├── AuthorizationList.cpp │ │ │ │ ├── AuthorizationList.h │ │ │ │ ├── AuthorizationService.cpp │ │ │ │ ├── AuthorizationService.h │ │ │ │ ├── IdToken.cpp │ │ │ │ └── IdToken.h │ │ │ ├── Availability/ │ │ │ │ ├── AvailabilityService.cpp │ │ │ │ ├── AvailabilityService.h │ │ │ │ └── ChangeAvailabilityStatus.h │ │ │ ├── Boot/ │ │ │ │ ├── BootService.cpp │ │ │ │ └── BootService.h │ │ │ ├── Certificates/ │ │ │ │ ├── Certificate.cpp │ │ │ │ ├── Certificate.h │ │ │ │ ├── CertificateMbedTLS.cpp │ │ │ │ ├── CertificateMbedTLS.h │ │ │ │ ├── CertificateService.cpp │ │ │ │ ├── CertificateService.h │ │ │ │ ├── Certificate_c.cpp │ │ │ │ └── Certificate_c.h │ │ │ ├── ConnectorBase/ │ │ │ │ ├── ChargePointErrorData.h │ │ │ │ ├── ChargePointStatus.h │ │ │ │ ├── Connector.cpp │ │ │ │ ├── Connector.h │ │ │ │ ├── ConnectorsCommon.cpp │ │ │ │ ├── ConnectorsCommon.h │ │ │ │ ├── EvseId.h │ │ │ │ └── UnlockConnectorResult.h │ │ │ ├── Diagnostics/ │ │ │ │ ├── DiagnosticsService.cpp │ │ │ │ ├── DiagnosticsService.h │ │ │ │ └── DiagnosticsStatus.h │ │ │ ├── FirmwareManagement/ │ │ │ │ ├── FirmwareService.cpp │ │ │ │ ├── FirmwareService.h │ │ │ │ └── FirmwareStatus.h │ │ │ ├── Heartbeat/ │ │ │ │ ├── HeartbeatService.cpp │ │ │ │ └── HeartbeatService.h │ │ │ ├── Metering/ │ │ │ │ ├── MeterStore.cpp │ │ │ │ ├── MeterStore.h │ │ │ │ ├── MeterValue.cpp │ │ │ │ ├── MeterValue.h │ │ │ │ ├── MeterValuesV201.cpp │ │ │ │ ├── MeterValuesV201.h │ │ │ │ ├── MeteringConnector.cpp │ │ │ │ ├── MeteringConnector.h │ │ │ │ ├── MeteringService.cpp │ │ │ │ ├── MeteringService.h │ │ │ │ ├── ReadingContext.cpp │ │ │ │ ├── ReadingContext.h │ │ │ │ ├── SampledValue.cpp │ │ │ │ └── SampledValue.h │ │ │ ├── Model.cpp │ │ │ ├── Model.h │ │ │ ├── RemoteControl/ │ │ │ │ ├── RemoteControlDefs.h │ │ │ │ ├── RemoteControlService.cpp │ │ │ │ └── RemoteControlService.h │ │ │ ├── Reservation/ │ │ │ │ ├── Reservation.cpp │ │ │ │ ├── Reservation.h │ │ │ │ ├── ReservationService.cpp │ │ │ │ └── ReservationService.h │ │ │ ├── Reset/ │ │ │ │ ├── ResetDefs.h │ │ │ │ ├── ResetService.cpp │ │ │ │ └── ResetService.h │ │ │ ├── SmartCharging/ │ │ │ │ ├── SmartChargingModel.cpp │ │ │ │ ├── SmartChargingModel.h │ │ │ │ ├── SmartChargingService.cpp │ │ │ │ └── SmartChargingService.h │ │ │ ├── Transactions/ │ │ │ │ ├── Transaction.cpp │ │ │ │ ├── Transaction.h │ │ │ │ ├── TransactionDefs.h │ │ │ │ ├── TransactionDeserialize.cpp │ │ │ │ ├── TransactionDeserialize.h │ │ │ │ ├── TransactionService.cpp │ │ │ │ ├── TransactionService.h │ │ │ │ ├── TransactionStore.cpp │ │ │ │ └── TransactionStore.h │ │ │ └── Variables/ │ │ │ ├── Variable.cpp │ │ │ ├── Variable.h │ │ │ ├── VariableContainer.cpp │ │ │ ├── VariableContainer.h │ │ │ ├── VariableService.cpp │ │ │ └── VariableService.h │ │ ├── Operations/ │ │ │ ├── Authorize.cpp │ │ │ ├── Authorize.h │ │ │ ├── BootNotification.cpp │ │ │ ├── BootNotification.h │ │ │ ├── CancelReservation.cpp │ │ │ ├── CancelReservation.h │ │ │ ├── ChangeAvailability.cpp │ │ │ ├── ChangeAvailability.h │ │ │ ├── ChangeConfiguration.cpp │ │ │ ├── ChangeConfiguration.h │ │ │ ├── CiStrings.h │ │ │ ├── ClearCache.cpp │ │ │ ├── ClearCache.h │ │ │ ├── ClearChargingProfile.cpp │ │ │ ├── ClearChargingProfile.h │ │ │ ├── CustomOperation.cpp │ │ │ ├── CustomOperation.h │ │ │ ├── DataTransfer.cpp │ │ │ ├── DataTransfer.h │ │ │ ├── DeleteCertificate.cpp │ │ │ ├── DeleteCertificate.h │ │ │ ├── DiagnosticsStatusNotification.cpp │ │ │ ├── DiagnosticsStatusNotification.h │ │ │ ├── FirmwareStatusNotification.cpp │ │ │ ├── FirmwareStatusNotification.h │ │ │ ├── GetBaseReport.cpp │ │ │ ├── GetBaseReport.h │ │ │ ├── GetCompositeSchedule.cpp │ │ │ ├── GetCompositeSchedule.h │ │ │ ├── GetConfiguration.cpp │ │ │ ├── GetConfiguration.h │ │ │ ├── GetDiagnostics.cpp │ │ │ ├── GetDiagnostics.h │ │ │ ├── GetInstalledCertificateIds.cpp │ │ │ ├── GetInstalledCertificateIds.h │ │ │ ├── GetLocalListVersion.cpp │ │ │ ├── GetLocalListVersion.h │ │ │ ├── GetVariables.cpp │ │ │ ├── GetVariables.h │ │ │ ├── Heartbeat.cpp │ │ │ ├── Heartbeat.h │ │ │ ├── InstallCertificate.cpp │ │ │ ├── InstallCertificate.h │ │ │ ├── MeterValues.cpp │ │ │ ├── MeterValues.h │ │ │ ├── NotifyReport.cpp │ │ │ ├── NotifyReport.h │ │ │ ├── RemoteStartTransaction.cpp │ │ │ ├── RemoteStartTransaction.h │ │ │ ├── RemoteStopTransaction.cpp │ │ │ ├── RemoteStopTransaction.h │ │ │ ├── RequestStartTransaction.cpp │ │ │ ├── RequestStartTransaction.h │ │ │ ├── RequestStopTransaction.cpp │ │ │ ├── RequestStopTransaction.h │ │ │ ├── ReserveNow.cpp │ │ │ ├── ReserveNow.h │ │ │ ├── Reset.cpp │ │ │ ├── Reset.h │ │ │ ├── SecurityEventNotification.cpp │ │ │ ├── SecurityEventNotification.h │ │ │ ├── SendLocalList.cpp │ │ │ ├── SendLocalList.h │ │ │ ├── SetChargingProfile.cpp │ │ │ ├── SetChargingProfile.h │ │ │ ├── SetVariables.cpp │ │ │ ├── SetVariables.h │ │ │ ├── StartTransaction.cpp │ │ │ ├── StartTransaction.h │ │ │ ├── StatusNotification.cpp │ │ │ ├── StatusNotification.h │ │ │ ├── StopTransaction.cpp │ │ │ ├── StopTransaction.h │ │ │ ├── TransactionEvent.cpp │ │ │ ├── TransactionEvent.h │ │ │ ├── TriggerMessage.cpp │ │ │ ├── TriggerMessage.h │ │ │ ├── UnlockConnector.cpp │ │ │ ├── UnlockConnector.h │ │ │ ├── UpdateFirmware.cpp │ │ │ └── UpdateFirmware.h │ │ ├── Platform.cpp │ │ ├── Platform.h │ │ └── Version.h │ ├── MicroOcpp.cpp │ ├── MicroOcpp.h │ ├── MicroOcpp_c.cpp │ └── MicroOcpp_c.h └── tests/ ├── Api.cpp ├── Boot.cpp ├── Certificates.cpp ├── ChargePointError.cpp ├── ChargingSessions.cpp ├── Configuration.cpp ├── ConfigurationBehavior.cpp ├── FirmwareManagement.cpp ├── LocalAuthList.cpp ├── Metering.cpp ├── RemoteStartTransaction.cpp ├── Reservation.cpp ├── Reset.cpp ├── Security.cpp ├── SmartCharging.cpp ├── TransactionSafety.cpp ├── Transactions.cpp ├── Variables.cpp ├── benchmarks/ │ ├── firmware_size/ │ │ ├── main.cpp │ │ └── platformio.ini │ └── scripts/ │ ├── eval_firmware_size.py │ └── measure_heap.py ├── catch2/ │ ├── catch.hpp │ └── catchMain.cpp ├── helpers/ │ ├── testHelper.cpp │ └── testHelper.h └── ocppEngineLifecycle.cpp ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/documentation.yml ================================================ # matth-x/MicroOcpp # Copyright Matthias Akstaller 2019 - 2024 # MIT License name: Documentation on: push: branches: - main pull_request: permissions: contents: write jobs: build_simulator: name: Build Simulator runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.x - uses: actions/cache@v4 with: key: ${{ github.ref }} path: .cache - name: Get build tools run: | sudo apt update sudo apt install cmake libssl-dev build-essential - name: Checkout Simulator uses: actions/checkout@v3 with: repository: matth-x/MicroOcppSimulator path: MicroOcppSimulator ref: 2cb07cdbe53954a694a29336ab31eac2d2b48673 submodules: 'recursive' - name: Clean MicroOcpp submodule run: | rm -rf MicroOcppSimulator/lib/MicroOcpp - name: Checkout MicroOcpp submodule uses: actions/checkout@v3 with: path: MicroOcppSimulator/lib/MicroOcpp - name: Generate CMake files run: cmake -S ./MicroOcppSimulator -B ./MicroOcppSimulator/build -DCMAKE_CXX_FLAGS="-DMO_OVERRIDE_ALLOCATION=1 -DMO_ENABLE_HEAP_PROFILER=1" - name: Compile run: cmake --build ./MicroOcppSimulator/build -j 32 --target mo_simulator - name: Upload Simulator executable uses: actions/upload-artifact@v4 with: name: Simulator executable path: | MicroOcppSimulator/build/mo_simulator MicroOcppSimulator/public/bundle.html.gz if-no-files-found: error retention-days: 1 measure_heap: needs: build_simulator name: Heap measurements runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.x - name: Install Python dependencies run: pip install requests paramiko pandas - name: Get Simulator uses: actions/download-artifact@v4 with: name: Simulator executable path: MicroOcppSimulator - name: Measure heap and create reports run: | mkdir -p docs/assets/tables python tests/benchmarks/scripts/measure_heap.py env: TEST_DRIVER_URL: ${{ secrets.TEST_DRIVER_URL }} TEST_DRIVER_CONFIG: ${{ secrets.TEST_DRIVER_CONFIG }} TEST_DRIVER_KEY: ${{ secrets.TEST_DRIVER_KEY }} MO_SIM_CONFIG: ${{ secrets.MO_SIM_CONFIG }} MO_SIM_OCPP_SERVER: ${{ secrets.MO_SIM_OCPP_SERVER }} MO_SIM_API_CERT: ${{ secrets.MO_SIM_API_CERT }} MO_SIM_API_KEY: ${{ secrets.MO_SIM_API_KEY }} MO_SIM_API_CONFIG: ${{ secrets.MO_SIM_API_CONFIG }} SSH_LOCAL_PRIV: ${{ secrets.SSH_LOCAL_PRIV }} SSH_HOST_PUB: ${{ secrets.SSH_HOST_PUB }} - name: Upload reports uses: actions/upload-artifact@v4 with: name: Memory usage reports CSV path: docs/assets/tables if-no-files-found: error build_firmware_size: name: Build firmware runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Cache pip uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: Cache PlatformIO uses: actions/cache@v4 with: path: ~/.platformio key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} - name: Set up Python uses: actions/setup-python@v4 - name: Install PlatformIO run: | python -m pip install --upgrade pip pip install --upgrade platformio - name: Run PlatformIO run: pio ci --lib="." --build-dir="${{ github.workspace }}/../build" --keep-build-dir --project-conf="./tests/benchmarks/firmware_size/platformio.ini" ./tests/benchmarks/firmware_size/main.cpp - name: Move firmware files # change path to location without parent dir ('..') statement (to make upload-artifact happy) run: | mkdir firmware mv "${{ github.workspace }}/../build/.pio/build/v16/firmware.elf" firmware/firmware_v16.elf mv "${{ github.workspace }}/../build/.pio/build/v201/firmware.elf" firmware/firmware_v201.elf - name: Upload firmware linker files uses: actions/upload-artifact@v4 with: name: Firmware linker files path: firmware if-no-files-found: error retention-days: 1 evaluate_firmware: needs: build_firmware_size name: Static firmware analysis runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.x - uses: actions/cache@v4 with: key: ${{ github.ref }} path: .cache - name: Install Python dependencies run: pip install pandas - name: Get build tools run: | sudo apt update sudo apt install build-essential cmake ninja-build sudo apt -y install gcc-9 g++-9 g++ --version - name: Check out bloaty uses: actions/checkout@v3 with: repository: google/bloaty ref: e1155149d54bb09b81e86f0e4e5cb7fbd2a318eb path: tools/bloaty submodules: recursive - name: Install bloaty run: | cmake -B tools/bloaty/build -G Ninja -S tools/bloaty cmake --build tools/bloaty/build -j 32 - name: Get firmware linker files uses: actions/download-artifact@v4 with: name: Firmware linker files path: firmware - name: Run bloaty run: | mkdir -p docs/assets/tables tools/bloaty/build/bloaty firmware/firmware_v16.elf -d compileunits --csv -n 0 > docs/assets/tables/bloaty_v16.csv tools/bloaty/build/bloaty firmware/firmware_v201.elf -d compileunits --csv -n 0 > docs/assets/tables/bloaty_v201.csv - name: Evaluate and create reports run: python tests/benchmarks/scripts/eval_firmware_size.py - name: Upload reports uses: actions/upload-artifact@v4 with: name: Firmware size reports CSV path: docs/assets/tables if-no-files-found: error deploy: needs: [evaluate_firmware, measure_heap] name: Deploy docs runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.x - uses: actions/cache@v4 with: key: ${{ github.ref }} path: .cache - name: Install Python dependencies run: pip install pandas mkdocs-material mkdocs-table-reader-plugin - name: Get firmware size reports uses: actions/download-artifact@v4 with: name: Firmware size reports CSV path: docs/assets/tables - name: Get memory occupation reports uses: actions/download-artifact@v4 with: name: Memory usage reports CSV path: docs/assets/tables - name: Run mkdocs run: mkdocs gh-deploy --force ================================================ FILE: .github/workflows/esp-idf.yml ================================================ # matth-x/MicroOcpp # Copyright Matthias Akstaller 2019 - 2024 # MIT License name: ESP-IDF CI on: push: branches: - main pull_request: jobs: build: runs-on: ubuntu-latest steps: - name: Checkout ESP-IDF example folder structure uses: actions/checkout@v3 with: sparse-checkout: examples/ESP-IDF - name: Clean sumodules folders template run: rm -r ./examples/ESP-IDF/components/* - name: Checkout main repo uses: actions/checkout@v3 with: path: examples/ESP-IDF/components/MicroOcpp - name: Checkout Mongoose uses: actions/checkout@v3 with: repository: cesanta/mongoose-esp-idf path: examples/ESP-IDF/components/mongoose submodules: 'recursive' - name: Checkout Mongoose WS adapter uses: actions/checkout@v3 with: repository: matth-x/MicroOcppMongoose ref: v1.2.0 path: examples/ESP-IDF/components/MicroOcppMongoose - name: Checkout ArduinoJson uses: actions/checkout@v3 with: repository: bblanchon/ArduinoJson ref: 3e1be980d93e47b2a0073efeeb9a9396fd7a83be path: examples/ESP-IDF/components/ArduinoJson - name: esp-idf build uses: espressif/esp-idf-ci-action@v1 with: esp_idf_version: v4.4 target: esp32 path: './examples/ESP-IDF' ================================================ FILE: .github/workflows/pio.yml ================================================ # matth-x/MicroOcpp # Copyright Matthias Akstaller 2019 - 2024 # MIT License name: PlatformIO CI on: push: branches: - main pull_request: jobs: build: runs-on: ubuntu-latest strategy: matrix: example: [examples/ESP/main.cpp, examples/ESP-TLS/main.cpp] steps: - uses: actions/checkout@v4 - name: Cache pip uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: Cache PlatformIO uses: actions/cache@v4 with: path: ~/.platformio key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} - name: Set up Python uses: actions/setup-python@v4 - name: Install PlatformIO run: | python -m pip install --upgrade pip pip install --upgrade platformio - name: Install library dependencies run: pio pkg install - name: Run PlatformIO run: pio ci --lib="." --project-conf=platformio.ini ${{ matrix.dashboard-extra }} env: PLATFORMIO_CI_SRC: ${{ matrix.example }} ================================================ FILE: .github/workflows/platformless.yml ================================================ # matth-x/MicroOcpp # Copyright Matthias Akstaller 2019 - 2024 # MIT License name: Default Compilation on: push: branches: - main pull_request: jobs: compile-platform: name: Compile (no linking) runs-on: ubuntu-latest steps: - name: Check out repository code uses: actions/checkout@v3 - name: Get gcc compiler run: | sudo apt update sudo apt install build-essential sudo apt -y install gcc-9 g++-9 g++ --version echo "g++ version must be 9.4.0" - name: Get ArduinoJson run: wget -Uri https://github.com/bblanchon/ArduinoJson/releases/download/v6.19.4/ArduinoJson-v6.19.4.h -O ./src/ArduinoJson.h - name: Compile run: g++ -c -std=c++11 -I ./src $(find ./src -type f -iregex ".*\.cpp") -DMO_PLATFORM=MO_PLATFORM_NONE -Wall -Wextra -Wno-unused-parameter -Wno-redundant-move -Werror ================================================ FILE: .github/workflows/tests.yml ================================================ # matth-x/MicroOcpp # Copyright Matthias Akstaller 2019 - 2024 # MIT License name: Unit tests on: push: branches: - main pull_request: jobs: compile-and-run: name: Automated Tests runs-on: ubuntu-latest steps: - name: Check out repository code uses: actions/checkout@v3 - name: Check out MbedTLS uses: actions/checkout@v3 with: repository: Mbed-TLS/mbedtls ref: v2.28.10 path: lib/mbedtls - name: Get build tools run: | sudo apt update sudo apt install build-essential cmake lcov valgrind sudo apt -y install gcc-9 g++-9 g++ --version echo "g++ version must be 9.4.0" - name: Get ArduinoJson run: wget -Uri https://github.com/bblanchon/ArduinoJson/releases/download/v6.21.3/ArduinoJson-v6.21.3.h -O ./src/ArduinoJson.h - name: Generate CMake build files run: cmake -S . -B ./build -DMO_BUILD_UNIT_MBEDTLS=True - name: Compile run: cmake --build ./build -j 32 --target mo_unit_tests - name: Configure FS run: mkdir mo_store - name: Run tests (valgrind) run: valgrind --error-exitcode=1 --leak-check=full ./build/mo_unit_tests --abort - name: Generate CMake build files (AddressSanitizer, UndefinedBehaviorSanitizer) run: | rm -r ./build cmake -S . -B ./build -DCMAKE_CXX_FLAGS="-fsanitize=address -fsanitize=undefined" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address -fsanitize=undefined" -DMO_BUILD_UNIT_MBEDTLS=True - name: Compile (ASan, UBSan) run: cmake --build ./build -j 32 --target mo_unit_tests - name: Run tests (ASan, UBSan) run: ./build/mo_unit_tests --abort - name: Create coverage report run: | lcov --directory . --capture --output-file coverage.info --ignore-errors mismatch lcov --remove coverage.info '/usr/*' '*/tests/*' '*/ArduinoJson.h' --output-file coverage.info lcov --list coverage.info - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .gitignore ================================================ .pio .vscode build lib mo_store src/ArduinoJson* src/main.cpp tests/helpers/ArduinoJson* coverage.info docs/assets ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## Unreleased ### Changed - Change `MicroOcpp::TxNotification` into C-style enum, replace `OCPP_TxNotication` ([#386](https://github.com/matth-x/MicroOcpp/pull/386)) - Improved UUID generation ([#383](https://github.com/matth-x/MicroOcpp/pull/383)) - `beginTransaction()` returns bool for better v2.0.1 interop ([#386](https://github.com/matth-x/MicroOcpp/pull/386)) - Configurations C-API updates ([#400](https://github.com/matth-x/MicroOcpp/pull/400)) - Platform integrations C-API upates ([#400](https://github.com/matth-x/MicroOcpp/pull/400)) ### Added - `getTransactionV201()` exposes v201 Tx in API ([#386](https://github.com/matth-x/MicroOcpp/pull/386)) - v201 support in Transaction.h C-API ([#386](https://github.com/matth-x/MicroOcpp/pull/386)) - Write-only Configurations ([#400](https://github.com/matth-x/MicroOcpp/pull/400)) ### Fixed - Timing issues for OCTT test cases ([#383](https://github.com/matth-x/MicroOcpp/pull/383)) - Misleading Reset failure dbg msg ([#388](https://github.com/matth-x/MicroOcpp/pull/388)) - Reject negative ints in ChangeConfig ([#388](https://github.com/matth-x/MicroOcpp/pull/388)) - Revised SCons integration ([#400](https://github.com/matth-x/MicroOcpp/pull/400)) ## [1.2.0] - 2024-11-03 ### Changed - Change `MicroOcpp::ChargePointStatus` into C-style enum ([#309](https://github.com/matth-x/MicroOcpp/pull/309)) - Connector lock disabled by default per `MO_ENABLE_CONNECTOR_LOCK` ([#312](https://github.com/matth-x/MicroOcpp/pull/312)) - Relaxed temporal order of non-tx-related operations ([#345](https://github.com/matth-x/MicroOcpp/pull/345)) - Use pseudo-GUIDs as messageId ([#345](https://github.com/matth-x/MicroOcpp/pull/345)) - ISO 8601 milliseconds omitted by default ([352](https://github.com/matth-x/MicroOcpp/pull/352)) - Rename `MO_NUM_EVSE` into `MO_NUM_EVSEID` (v2.0.1) ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) - Change `MicroOcpp::ReadingContext` into C-style struct ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) - Refactor RequestStartTransaction (v2.0.1) ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) ### Added - Provide ChargePointStatus in API ([#309](https://github.com/matth-x/MicroOcpp/pull/309)) - Built-in OTA over FTP ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) - Built-in Diagnostics over FTP ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) - Error `severity` mechanism ([#331](https://github.com/matth-x/MicroOcpp/pull/331)) - Build flag `MO_REPORT_NOERROR` to report error recovery ([#331](https://github.com/matth-x/MicroOcpp/pull/331)) - Support for `parentIdTag` ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) - Input validation for unsigned int Configs ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) - Support for TransactionMessageAttempts/-RetryInterval ([#345](https://github.com/matth-x/MicroOcpp/pull/345), [#380](https://github.com/matth-x/MicroOcpp/pull/380)) - Heap profiler and custom allocator support ([#350](https://github.com/matth-x/MicroOcpp/pull/350)) - Migration of persistent storage ([#355](https://github.com/matth-x/MicroOcpp/pull/355)) - Benchmarks pipeline ([#369](https://github.com/matth-x/MicroOcpp/pull/369), [#376](https://github.com/matth-x/MicroOcpp/pull/376)) - MeterValues port for OCPP 2.0.1 ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) - UnlockConnector port for OCPP 2.0.1 ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) - More APIs ported to OCPP 2.0.1 ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) - Support for AuthorizeRemoteTxRequests ([#373](https://github.com/matth-x/MicroOcpp/pull/373)) - Persistent Variable and Tx store for OCPP 2.0.1 ([#379](https://github.com/matth-x/MicroOcpp/pull/379)) ### Removed - ESP32 built-in HTTP OTA ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) - Operation store (files op-*.jsn and opstore.jsn) ([#345](https://github.com/matth-x/MicroOcpp/pull/345)) - Explicit tracking of txNr (file txstore.jsn) ([#345](https://github.com/matth-x/MicroOcpp/pull/345)) - SimpleRequestFactory ([#351](https://github.com/matth-x/MicroOcpp/pull/351)) ### Fixed - Skip Unix files . and .. in ftw_root ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) - Skip clock-aligned measurements when time not set - Hold back error StatusNotifs when time not set ([#311](https://github.com/matth-x/MicroOcpp/issues/311)) - Don't send Available when tx occupies connector ([#315](https://github.com/matth-x/MicroOcpp/issues/315)) - Make ChargingScheduleAllowedChargingRateUnit read-only ([#328](https://github.com/matth-x/MicroOcpp/issues/328)) - ~Don't send StatusNotifs while offline ([#344](https://github.com/matth-x/MicroOcpp/pull/344))~ (see ([#371](https://github.com/matth-x/MicroOcpp/pull/371))) - Don't change into Unavailable upon Reset ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) - Reject DataTransfer by default ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) - UnlockConnector NotSupported if connectorId invalid ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) - Fix regression bug of [#345](https://github.com/matth-x/MicroOcpp/pull/345) ([#353](https://github.com/matth-x/MicroOcpp/pull/353), [#356](https://github.com/matth-x/MicroOcpp/pull/356)) - Correct MeterValue PreBoot timestamp ([#354](https://github.com/matth-x/MicroOcpp/pull/354)) - Send errorCode in triggered StatusNotif ([#359](https://github.com/matth-x/MicroOcpp/pull/359)) - Remove int to bool conversion in ChangeConfig ([#362](https://github.com/matth-x/MicroOcpp/pull/362)) - Multiple fixes of the OCPP 2.0.1 extension ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) ## [1.1.0] - 2024-05-21 ### Changed - Replace `PollResult` with enum `UnlockConnectorResult` ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) - Rename master branch into main - Tx logic directly checks if WebSocket is offline ([#282](https://github.com/matth-x/MicroOcpp/pull/282)) - `ocppPermitsCharge` ignores Faulted state ([#279](https://github.com/matth-x/MicroOcpp/pull/279)) - `setEnergyMeterInput` expects `int` input ([#301](https://github.com/matth-x/MicroOcpp/pull/301)) ### Added - File index ([#270](https://github.com/matth-x/MicroOcpp/pull/270)) - Config `Cst_TxStartOnPowerPathClosed` to put back TxStartPoint ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) - Build flag `MO_ENABLE_RESERVATION=0` disables Reservation module ([#302](https://github.com/matth-x/MicroOcpp/pull/302)) - Build flag `MO_ENABLE_LOCAL_AUTH=0` disables LocalAuthList module ([#303](https://github.com/matth-x/MicroOcpp/pull/303)) - Function `bool isConnected()` in `Connection` interface ([#282](https://github.com/matth-x/MicroOcpp/pull/282)) - Build flags for customizing memory limits of SmartCharging ([#260](https://github.com/matth-x/MicroOcpp/pull/260)) - SConscript ([#287](https://github.com/matth-x/MicroOcpp/pull/287)) - C-API for custom Configs store ([297](https://github.com/matth-x/MicroOcpp/pull/297)) - Certificate Management, UCs M03 - M05 ([#262](https://github.com/matth-x/MicroOcpp/pull/262), [#274](https://github.com/matth-x/MicroOcpp/pull/274), [#292](https://github.com/matth-x/MicroOcpp/pull/292)) - FTP Client ([#291](https://github.com/matth-x/MicroOcpp/pull/291)) - `ProtocolVersion` selects v1.6 or v2.0.1 ([#247](https://github.com/matth-x/MicroOcpp/pull/247)) - Build flag `MO_ENABLE_V201=1` enables OCPP 2.0.1 features ([#247](https://github.com/matth-x/MicroOcpp/pull/247)) - Variables (non-persistent), UCs B05 - B07 ([#247](https://github.com/matth-x/MicroOcpp/pull/247), [#284](https://github.com/matth-x/MicroOcpp/pull/284)) - Transactions (preview only), UCs E01 - E12 ([#247](https://github.com/matth-x/MicroOcpp/pull/247)) - StatusNotification compatibility ([#247](https://github.com/matth-x/MicroOcpp/pull/247)) - ChangeAvailability compatibility ([#285](https://github.com/matth-x/MicroOcpp/pull/285)) - Reset compatibility, UCs B11 - B12 ([#286](https://github.com/matth-x/MicroOcpp/pull/286)) - RequestStart-/StopTransaction, UCs F01 - F02 ([#289](https://github.com/matth-x/MicroOcpp/pull/289)) ### Fixed - Fix defect idTag check in `endTransaction` ([#275](https://github.com/matth-x/MicroOcpp/pull/275)) - Make field localAuthorizationList in SendLocalList optional - Update charging profiles when flash disabled (relates to [#260](https://github.com/matth-x/MicroOcpp/pull/260)) - Ignore UnlockConnector when handler not set ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) - Reject ChargingProfile if unit not supported ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) - Fix building with debug level warn and error - Reduce debug output FW size overhead ([#304](https://github.com/matth-x/MicroOcpp/pull/304)) - Fix transaction freeze in offline mode ([#279](https://github.com/matth-x/MicroOcpp/pull/279), [#287](https://github.com/matth-x/MicroOcpp/pull/287)) - Fix compilation error caused by `PRId32` ([#279](https://github.com/matth-x/MicroOcpp/pull/279)) - Don't load FW-mngt. module when no handlers set ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) - Change arduinoWebSockets URL param to path ([#278](https://github.com/matth-x/MicroOcpp/issues/278)) - Avoid creating conf when operation fails ([#290](https://github.com/matth-x/MicroOcpp/pull/290)) - Fix whitespaces in MeterValues ([#301](https://github.com/matth-x/MicroOcpp/pull/301)) - Make SmartChargingProfile txId field optional ([#348](https://github.com/matth-x/MicroOcpp/pull/348)) ## [1.0.3] - 2024-04-06 ### Fixed - Fix nullptr access in endTransaction ([#275](https://github.com/matth-x/MicroOcpp/pull/275)) - Backport: Fix building with debug level warn and error ## [1.0.2] - 2024-03-24 ### Fixed - Correct MO version numbers in code (they were still `1.0.0`) ## [1.0.1] - 2024-02-27 ### Fixed - Allow `nullptr` as parameter for `mocpp_set_console_out` ([#224](https://github.com/matth-x/MicroOcpp/issues/224)) - Fix `mocpp_tick_ms()` on esp-idf roll-over after 12 hours - Pin ArduinoJson to v6.21 ([#245](https://github.com/matth-x/MicroOcpp/issues/245)) - Fix bounds checking in SmartCharging module ([#260](https://github.com/matth-x/MicroOcpp/pull/260)) ## [1.0.0] - 2023-10-22 _First release_ ### Changed - `mocpp_initialize` takes OCPP URL without explicit host, port ([#220](https://github.com/matth-x/MicroOcpp/pull/220)) - `endTransaction` checks authorization of `idTag` - Update configurations API ([#195](https://github.com/matth-x/MicroOcpp/pull/195)) - Update Firmware- and DiagnosticsService API ([#207](https://github.com/matth-x/MicroOcpp/pull/207)) - Update Connection interface - Update Authorization module functions ([#213](https://github.com/matth-x/MicroOcpp/pull/213)) - Reflect changes in C-API - Change build flag prefix from `MOCPP_` to `MO_` - Change `mo_set_console_out` to `mocpp_set_console_out` - Revise README.md - Revise misleading debug messages - Update Arduino IDE manifest ([#206](https://github.com/matth-x/MicroOcpp/issues/206)) ### Added - Auto-recovery switch in `mocpp_initialize` params - WebAssembly port - Configurable `MO_PARTITION_LABEL` for the esp-idf SPIFFS integration ([#218](https://github.com/matth-x/MicroOcpp/pull/218)) - `MO_TX_CLEAN_ABORTED=0` keeps aborted txs in journal - `MO_VERSION` specifier - `MO_PLATFORM_NONE` for compilation on custom platforms - `endTransaction_authorized` enforces the tx end - Add valgrind, ASan, UBSan CI/CD steps ([#189](https://github.com/matth-x/MicroOcpp/pull/189)) ### Fixed - Reservation ([#196](https://github.com/matth-x/MicroOcpp/pull/196)) - Fix immediate FW-update Download phase abort ([#216](https://github.com/matth-x/MicroOcpp/pull/216)) - `stat` usage on arduino-esp32 LittleFS - SetChargingProfile JSON capacity calculation - Set correct idTag when Reset triggers StopTx - Execute operations only once despite multiple .conf send attempts ([#207](https://github.com/matth-x/MicroOcpp/pull/207)) - ConnectionTimeOut only applies when connector is still unplugged - Fix valgrind warnings ## [1eff6e5] - 23-08-23 _Previous point with breaking changes on master_ Renaming to MicroOcpp is completed since this commit. See the [migration guide](https://matth-x.github.io/MicroOcpp/migration/) for more details on what's changed. Changelogs and semantic versioning are adopted starting with v1.0.0 ## [0.3.0] - 23-08-19 _Last version under the project name ArduinoOcpp_ ================================================ FILE: CMakeLists.txt ================================================ # matth-x/MicroOcpp # Copyright Matthias Akstaller 2019 - 2024 # MIT License cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) set(MO_SRC src/MicroOcpp/Core/Configuration_c.cpp src/MicroOcpp/Core/Configuration.cpp src/MicroOcpp/Core/ConfigurationContainer.cpp src/MicroOcpp/Core/ConfigurationContainerFlash.cpp src/MicroOcpp/Core/ConfigurationKeyValue.cpp src/MicroOcpp/Core/FilesystemAdapter.cpp src/MicroOcpp/Core/FilesystemUtils.cpp src/MicroOcpp/Core/FtpMbedTLS.cpp src/MicroOcpp/Core/Memory.cpp src/MicroOcpp/Core/RequestQueue.cpp src/MicroOcpp/Core/Context.cpp src/MicroOcpp/Core/Operation.cpp src/MicroOcpp/Model/Model.cpp src/MicroOcpp/Core/Request.cpp src/MicroOcpp/Core/Connection.cpp src/MicroOcpp/Core/Time.cpp src/MicroOcpp/Core/UuidUtils.cpp src/MicroOcpp/Operations/Authorize.cpp src/MicroOcpp/Operations/BootNotification.cpp src/MicroOcpp/Operations/CancelReservation.cpp src/MicroOcpp/Operations/ChangeAvailability.cpp src/MicroOcpp/Operations/ChangeConfiguration.cpp src/MicroOcpp/Operations/ClearCache.cpp src/MicroOcpp/Operations/ClearChargingProfile.cpp src/MicroOcpp/Operations/CustomOperation.cpp src/MicroOcpp/Operations/DataTransfer.cpp src/MicroOcpp/Operations/DeleteCertificate.cpp src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp src/MicroOcpp/Operations/FirmwareStatusNotification.cpp src/MicroOcpp/Operations/GetBaseReport.cpp src/MicroOcpp/Operations/GetCompositeSchedule.cpp src/MicroOcpp/Operations/GetConfiguration.cpp src/MicroOcpp/Operations/GetDiagnostics.cpp src/MicroOcpp/Operations/GetInstalledCertificateIds.cpp src/MicroOcpp/Operations/GetLocalListVersion.cpp src/MicroOcpp/Operations/GetVariables.cpp src/MicroOcpp/Operations/Heartbeat.cpp src/MicroOcpp/Operations/MeterValues.cpp src/MicroOcpp/Operations/NotifyReport.cpp src/MicroOcpp/Operations/RemoteStartTransaction.cpp src/MicroOcpp/Operations/RemoteStopTransaction.cpp src/MicroOcpp/Operations/RequestStartTransaction.cpp src/MicroOcpp/Operations/RequestStopTransaction.cpp src/MicroOcpp/Operations/ReserveNow.cpp src/MicroOcpp/Operations/Reset.cpp src/MicroOcpp/Operations/SecurityEventNotification.cpp src/MicroOcpp/Operations/SendLocalList.cpp src/MicroOcpp/Operations/SetChargingProfile.cpp src/MicroOcpp/Operations/SetVariables.cpp src/MicroOcpp/Operations/StartTransaction.cpp src/MicroOcpp/Operations/StatusNotification.cpp src/MicroOcpp/Operations/StopTransaction.cpp src/MicroOcpp/Operations/TransactionEvent.cpp src/MicroOcpp/Operations/TriggerMessage.cpp src/MicroOcpp/Operations/InstallCertificate.cpp src/MicroOcpp/Operations/UnlockConnector.cpp src/MicroOcpp/Operations/UpdateFirmware.cpp src/MicroOcpp/Debug.cpp src/MicroOcpp/Platform.cpp src/MicroOcpp/Core/OperationRegistry.cpp src/MicroOcpp/Model/Availability/AvailabilityService.cpp src/MicroOcpp/Model/Authorization/AuthorizationData.cpp src/MicroOcpp/Model/Authorization/AuthorizationList.cpp src/MicroOcpp/Model/Authorization/AuthorizationService.cpp src/MicroOcpp/Model/Authorization/IdToken.cpp src/MicroOcpp/Model/Boot/BootService.cpp src/MicroOcpp/Model/Certificates/Certificate.cpp src/MicroOcpp/Model/Certificates/Certificate_c.cpp src/MicroOcpp/Model/Certificates/CertificateMbedTLS.cpp src/MicroOcpp/Model/Certificates/CertificateService.cpp src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp src/MicroOcpp/Model/ConnectorBase/Connector.cpp src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp src/MicroOcpp/Model/Metering/MeteringConnector.cpp src/MicroOcpp/Model/Metering/MeteringService.cpp src/MicroOcpp/Model/Metering/MeterStore.cpp src/MicroOcpp/Model/Metering/MeterValue.cpp src/MicroOcpp/Model/Metering/MeterValuesV201.cpp src/MicroOcpp/Model/Metering/ReadingContext.cpp src/MicroOcpp/Model/Metering/SampledValue.cpp src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp src/MicroOcpp/Model/Reservation/Reservation.cpp src/MicroOcpp/Model/Reservation/ReservationService.cpp src/MicroOcpp/Model/Reset/ResetService.cpp src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp src/MicroOcpp/Model/Transactions/Transaction.cpp src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp src/MicroOcpp/Model/Transactions/TransactionService.cpp src/MicroOcpp/Model/Transactions/TransactionStore.cpp src/MicroOcpp/Model/Variables/Variable.cpp src/MicroOcpp/Model/Variables/VariableContainer.cpp src/MicroOcpp/Model/Variables/VariableService.cpp src/MicroOcpp.cpp src/MicroOcpp_c.cpp ) if(ESP_PLATFORM) idf_component_register(SRCS ${MO_SRC} INCLUDE_DIRS "./src" "../ArduinoJson/src" PRIV_REQUIRES spiffs ) target_compile_options(${COMPONENT_TARGET} PUBLIC -DMO_PLATFORM=MO_PLATFORM_ESPIDF ) return() endif() project(MicroOcpp VERSION 1.2.0) add_library(MicroOcpp ${MO_SRC}) target_include_directories(MicroOcpp PUBLIC "./src" "../ArduinoJson/src" ) target_compile_definitions(MicroOcpp PUBLIC MO_PLATFORM=MO_PLATFORM_UNIX ) # Unit tests set(MO_SRC_UNIT tests/helpers/testHelper.cpp tests/ocppEngineLifecycle.cpp tests/TransactionSafety.cpp tests/ChargingSessions.cpp tests/ConfigurationBehavior.cpp tests/SmartCharging.cpp tests/Api.cpp tests/Metering.cpp tests/Configuration.cpp tests/Reservation.cpp tests/Reset.cpp tests/LocalAuthList.cpp tests/Variables.cpp tests/Transactions.cpp tests/RemoteStartTransaction.cpp tests/Certificates.cpp tests/FirmwareManagement.cpp tests/ChargePointError.cpp tests/Boot.cpp tests/Security.cpp ) add_executable(mo_unit_tests ${MO_SRC} ${MO_SRC_UNIT} ./tests/catch2/catchMain.cpp ) if (MO_BUILD_UNIT_MBEDTLS) add_subdirectory(lib/mbedtls) target_link_libraries(mo_unit_tests PUBLIC mbedtls mbedcrypto mbedx509 ) target_compile_definitions(mo_unit_tests PUBLIC MO_ENABLE_MBEDTLS=1 ) endif() target_include_directories(mo_unit_tests PUBLIC "./tests" "./tests/helpers" "./src" ) target_compile_definitions(mo_unit_tests PUBLIC MO_PLATFORM=MO_PLATFORM_UNIX MO_NUMCONNECTORS=3 MO_CUSTOM_TIMER MO_DBG_LEVEL=MO_DL_INFO MO_TRAFFIC_OUT MO_FILENAME_PREFIX="./mo_store/" MO_LocalAuthListMaxLength=8 MO_SendLocalListMaxLength=4 MO_ENABLE_FILE_INDEX=1 MO_ChargeProfileMaxStackLevel=2 MO_ChargingScheduleMaxPeriods=4 MO_MaxChargingProfilesInstalled=3 MO_ENABLE_CERT_MGMT=1 MO_ENABLE_CONNECTOR_LOCK=1 MO_REPORT_NOERROR=1 MO_ENABLE_V201=1 MO_OVERRIDE_ALLOCATION=1 MO_ENABLE_HEAP_PROFILER=1 MO_HEAP_PROFILER_EXTERNAL_CONTROL=1 CATCH_CONFIG_EXTERNAL_INTERFACES ) target_compile_options(mo_unit_tests PUBLIC -Wall -O0 -g --coverage ) target_link_options(mo_unit_tests PUBLIC --coverage ) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 - 2024 Matthias Akstaller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Icon   MicroOCPP [![Build Status]( https://github.com/matth-x/MicroOcpp/workflows/PlatformIO%20CI/badge.svg)](https://github.com/matth-x/MicroOcpp/actions) [![Unit tests]( https://github.com/matth-x/MicroOcpp/workflows/Unit%20tests/badge.svg)](https://github.com/matth-x/MicroOcpp/actions) [![codecov](https://codecov.io/github/matth-x/ArduinoOcpp/branch/develop/graph/badge.svg?token=UN6LO96HM7)](https://codecov.io/github/matth-x/ArduinoOcpp) OCPP 1.6 / 2.0.1 client for microcontrollers. Portable C/C++. Compatible with Espressif, Arduino, NXP, STM, Linux and more. :heavy_check_mark: Works with [15+ commercial Central Systems](https://www.micro-ocpp.com/#h.314525e8447cc93c_81) :heavy_check_mark: Eligible for public chargers (Eichrecht-compliant) :heavy_check_mark: Supports all OCPP 1.6 feature profiles and the [basic OCPP 2.0.1 UCs](https://github.com/matth-x/MicroOcpp/tree/feature/prepare-release?tab=readme-ov-file#ocpp-201-and-iso-15118) Reference usage: [OpenEVSE](https://github.com/OpenEVSE/ESP32_WiFi_V4.x/blob/master/src/ocpp.cpp) | Technical introduction: [Docs](https://matth-x.github.io/MicroOcpp/intro-tech) | Website: [www.micro-ocpp.com](https://www.micro-ocpp.com) ## AI-friendly code AI models perform extremely well with the MicroOCPP codebase. The upcoming new release of MO (v2.0) will further optimize the code for more reliable results with AI models (preview to be found in the `develop/remodel-api` branch). The hope is to allow integrating MO into an existing EV charger project with only a few queries. Currently, the `develop/remodel-api` branch is not stable yet, but recommended for new developments. To get started, load `MicroOcpp.h` (now unified for C and C++) into the context window and ask what the AI model needs to know to integrate it into your codebase. If your tools have issues with something in MicroOCPP, please open an issue on GitHub. Any feedback on how to further optimize the codebase is also highly appreciated. ## Tester / Demo App *Main repository: [MicroOcppSimulator](https://github.com/matth-x/MicroOcppSimulator)* The Simulator is a demo & development tool for MicroOCPP which allows to quickly assess the compatibility with different OCPP backends. It simulates a full charging station, adds a GUI and a mocked hardware binding to MicroOCPP and runs in the browser (using WebAssembly): [Try it](https://demo.micro-ocpp.com/)
Screenshot
#### Usage **OCPP server setup**: Navigate to "Control Center". In the WebSocket options, add the OCPP backend URL, charge box ID and authorization key if existent. Press "Update WebSocket" to save. The Simulator should connect to the OCPP server. To check the connection status, it could be helpful to open the developer tools of the browser. If you don't have an OCPP server at hand, leave the charge box ID blank and enter the following backend address: `wss://echo.websocket.events/` (this server is sponsored by Lob.com) **RFID authentication**: Go to "Control Center" > "Connectors" > "Transaction" and update the idTag with the desired value. ## Benchmarks *Full report: [MicroOCPP benchmarks](https://matth-x.github.io/MicroOcpp/benchmarks/)* The following measurements were taken on the ESP32 @ 160MHz and represent the optimistic best case scenario for a charger with two physical connectors (i.e. compiled with `-Os`, disabled debug output and logs). | Description | Value | | :--- | ---: | | Flash size (minimal) | 121,170 B | | Heap occupation (idle) | 12,308 B | | Heap occupation (peak) | 21,916 B | | Initailization | 21 ms | | `loop()` call (idle) | 0.05 ms | | Large message sent | 5 ms | In practical setups, the execution time is largely determined by IO delays and the heap occupation is significantly influenced by the configuration with reservation, local authorization and charging profile lists. ## Developers guide PlatformIO package: [MicroOcpp](https://registry.platformio.org/libraries/matth-x/MicroOcpp) MicroOCPP is an implementation of the OCPP communication behavior. It automatically initiates the corresponding OCPP operations once the hardware status changes or the RFID input is updated with a new value. Conversely it processes new data from the server, stores it locally and updates the hardware controls when applicable. Please take `examples/ESP/main.cpp` as the starting point for the first project. It is a minimal example which shows how to establish an OCPP connection and how to start and stop charging sessions. The API documentation can be found in [`MicroOcpp.h`](https://github.com/matth-x/MicroOcpp/blob/main/src/MicroOcpp.h). Also check out the [Docs](https://matth-x.github.io/MicroOcpp). ### Dependencies Mandatory: - [bblanchon/ArduinoJSON](https://github.com/bblanchon/ArduinoJson) (version `6.21`) If compiled with the Arduino integration: - [Links2004/arduinoWebSockets](https://github.com/Links2004/arduinoWebSockets) (version `2.4.1`) If using the built-in certificate store (to enable, set build flag `MO_ENABLE_MBEDTLS=1`): - [Mbed-TLS/mbedtls](https://github.com/Mbed-TLS/mbedtls) (version `2.28.1`) In case you use PlatformIO, you can copy all dependencies from `platformio.ini` into your own configuration file. Alternatively, you can install the full library with dependencies by adding `matth-x/MicroOcpp@1.2.0` in the PIO library manager. ## OCPP 2.0.1 and ISO 15118 The following OCPP 2.0.1 use cases are implemented: | UC | Description | Note | | :--- | :--- | :--- | | B01 - B04
B11 - B12 | Provisioning | Ported from OCPP 1.6 | | B05 - B07 | Variables | | | C01 - C06 | Authorization options | | | C15 | Offline Authorization | | | E01 - E12 | Transactions | | | F01 - F03
F05 - F06 | RemoteControl | | | G01 - G04 | Availability | | | J02 | Tx-related MeterValues | persistency not supported yet | | M03 - M05 | Certificate management | Enable Mbed-TLS to use the built-in certificate store | | P01 - P02 | Data transfer | | | - | Protocol negotiation | The charger can select the OCPP version at runtime | The OCPP 2.0.1 features are in an alpha development stage. By default, they are disabled and excluded from the build, so they have no impact on the firmware size. To enable, set the build flag `MO_ENABLE_V201=1` and initialize the library with the ProtocolVersion parameter `2.0.1` (see [this example](https://github.com/matth-x/MicroOcppSimulator/blob/657e606c3b178d3add242935d413c72624130ff3/src/main.cpp#L43-L47) in the Simulator). An integration of the library for OCPP 1.6 will also be functional with the 2.0.1 upgrade. It works with the same API in MicroOcpp.h. ISO 15118 defines some use cases which include a message exchange between the charger and server. This library facilitates the integration of ISO 15118 by handling its OCPP-side communication. ## Contact If you have any questions, or found a potential bug, feel free to open an issue. This type of interaction is highly appreciated, because it shows problems in the codebase and helps improve the project for clarity. For further questions which shouldn't stand in public, you can reach me via LinkedIn or the following email address: :envelope: : matthias [A⊤] micro-ocpp [DО⊤] com ================================================ FILE: SConscript.py ================================================ # matth-x/MicroOcpp # Copyright Matthias Akstaller 2019 - 2024 # MIT License # NOTE: This SConscript is still WIP. It has thankfully been contributed from a project using SCons, # not necessarily considering full reusability in other projects though. # Use this file as a starting point for writing your own SCons integration. And as always, any # contributions are highly welcome! Import("env") import os, pathlib def getAllDirs(root_dir): dir_list = [] for root, subfolders, files in os.walk(root_dir.abspath): dir_list.append(Dir(root)) return dir_list SOURCE_DIR = Dir(".").srcnode().Dir("src") source_dirs = getAllDirs(SOURCE_DIR) source_files = [] for folder in source_dirs: source_files += folder.glob("*.c") source_files += folder.glob("*.cpp") compiled_objects = [] for source_file in source_files: obj = env.Object( target = pathlib.Path(source_file.path).stem + ".o", source=source_file, ) compiled_objects.append(obj) libmicroocpp = env.StaticLibrary( target='libmicroocpp', source=sorted(compiled_objects) ) exports = { 'library': libmicroocpp, 'CPPPATH': SOURCE_DIR } Return("exports") ================================================ FILE: docs/benchmarks.md ================================================ # Benchmarks Microcontrollers have tight hardware constraints which affect how much resources the firmware can demand. It is important to make sure that the available resources are not depleted to allow for robust operation and that there is sufficient flash head room to allow for future software upgrades. In general, microcontrollers have three relevant hardware constraints: - Limited processing speed - Limited memory size - Limited flash size For OCPP, the relevant bottlenecks are especially the memory and flash size. The processing speed is no concern, since OCPP is not computationally complex and does not include any extensive planning algorithms on the charger size. A previous [benchmark on the ESP-IDF](https://github.com/matth-x/MicroOcpp-benchmark) showed that the processing times are in the lower milliseconds range and are probably outweighed by IO times and network round trip times. However, the memory and flash requirements are important figures, because the device model of OCPP has a significant size. The microcontroller needs to keep the model data in the heap memory for the largest part and the firmware which covers the corresponding processing routines needs to have sufficient space on flash. This chapter presents benchmarks of the memory and flash requirements. They should help to determine the required microcontroller capabilities, or to give general insights for taking further action on optimizing the firmware. ## Firmware size When compiling a firmware with MicroOCPP, the resulting binary will contain functionality which is not related to OCPP, like hardware drivers, modules which are shared, like MbedTLS and the actual MicroOCPP object files. The size of the latter is the final flash requirement of MicroOCPP. For the flash benchmark, the profiler compiles a [dummy OCPP firmware](https://github.com/matth-x/MicroOcpp/tree/main/tests/benchmarks/firmware_size/main.cpp), analyzes the size of the compilation units using [bloaty](https://github.com/google/bloaty) and evaluates the bloaty report using a [Python script](https://github.com/matth-x/MicroOcpp/tree/main/tests/benchmarks/scripts/eval_firmware_size.py). To give realistic results, the firwmare is compiled with `-Os`, no RTTI or exceptions and newlib as the standard C library. The following tables show the results. ### OCPP 1.6 The following table shows the cumulated size of the objects files per module. The Module category consists of the OCPP 2.0.1 functional blocks, OCPP 1.6 feature profiles and general functionality which is shared accross the library. If a feature of the implementation falls under both an OCPP 2.0.1 functional block and OCPP 1.6 feature profile definition, it is preferrably assigned to the OCPP 2.0.1 category. This allows for better comparability between both OCPP versions. **Table 1: Firmware size per Module** {{ read_csv('modules_v16.csv') }} ### OCPP 2.0.1 **Table 2: Firmware size per Module** {{ read_csv('modules_v201.csv') }} ## Memory usage MicroOCPP uses the heap memory to process incoming messages, maintain the device model and create outgoing OCPP messages. The total heap usage should remain low enough to not risk a heap depletion which would not only affect the OCPP module, but the whole controller, because heap memory is typically shared on microcontrollers. To assess the heap usage of MicroOCPP, a test suite runs a variety of simulated charger use cases and measures the maximum occupied memory. Then, the maximum observed value is considered as the memory requirement of MicroOCPP. Another important figure is the base level which is much closer to the average heap usage. The total heap usage consists of a base level and a dynamic part. Some memory objects are only initialized once during startup or as the device model is populated (e.g. Charging Schedules) and therefore belong to the base which changes only slowly over time. In contrast, objects for the JSON parsing and serialization and the internal execution of the operations are highly dynamic as they are instantiated for one operation and freed again after completion of the action. If the firmware contains multiple components besides MicroOCPP with this usage pattern, then the average total memory occupation of the device RAM is even closer to the base levels of the individual components. The following table shows the dynamic heap usage for a variety of test cases, followed by the base level and resulting maximum memory occupation of MicroOCPP. At the time being, the measurements are limited to only OCPP 2.0.1 and a narrow set of test cases. They will be gradually extended over time. **Table 3: Memory usage per use case and total** {{ read_csv('heap_v201.csv') }} ## Full data sets This section contains the raw data which is the basis for the evaluations above. **Table 4: All compilation units for OCPP 1.6 firmware** {{ read_csv('compile_units_v16.csv') }} **Table 5: All compilation units for OCPP 2.0.1 firmware** {{ read_csv('compile_units_v201.csv') }} ================================================ FILE: docs/index.md ================================================ MicroOCPP is an OCPP client which runs on microcontrollers and enables EVSEs to participate in OCPP charging networks. As a software library, it can be added to the firmware of the EVSE and will become a new part of it. If the EVSE has already an internet controller, then most likely, no extra hardware is required. [Technical introduction](intro-tech) [Migrating to v1.0](migration) [Modules](modules) [Development tools and basic prerequisites](prerequisites) [Security whitepaper](security) *Documentation WIP. See the [GitHub Readme](https://github.com/matth-x/MicroOcpp) or the [API description](https://github.com/matth-x/MicroOcpp/blob/main/src/MicroOcpp.h) as reference.* ================================================ FILE: docs/intro-tech.md ================================================ # Technical introduction This chapter covers the technical concepts of MicroOCPP. ## Scope of MicroOCPP The OCPP specification defines a charger data model, operations on the data model and the resulting physical behavior on the charger side. MicroOCPP implements the full scope of OCPP, i.e. a minimalistic data store for the data model, the OCPP operations and an interface to the surrounding firmware. Another part of OCPP is its messaging mechanism, the so-called Remote Procedure Calls (RPC) framework. MicroOCPP also implements the specified RPC framework with the required guarantees of message delivery or the corresponding error handling. At the lowest layer, OCPP relies on standard WebSockets. MicroOCPP works with any WebSocket library and has a lean interface to integrate them. The high-level API in `MicroOcpp.h` bundles all touch points of the EVSE firmware with the OCPP library.


Overview of the architecture

## High-level OCPP support Being a full implementation of OCPP, MicroOCPP handles the OCPP communication, i.e. it sends OCPP requests and processes incoming OCPP requests autonomously. The messages are triggered by the internal data model and by input from the high-level API. Incoming OCPP requests are used to update the internal data model and if an action on the charger is required, the library signals that to the main firmware through the high-level API. In consequence, the high-level API decouples the main firmware from the OCPP communication and hides the operations. This has the following good reasons: - The high-level API guarantees correctnes of the OCPP integration. As soon as the charger adopts it properly, it is fully OCPP-compliant - The hardware-near design decreases the integration effort into the firmware hugely - The API won't change substantially for the OCPP 2.0.1 upgrade. The EVSE will get OCPP 2.0.1 support on the fly by a later firmware update ## Customizability One core principle of the architecture of MicroOCPP is the customizability and the selective usage of its components. Selective usage of components means that the EVSE firmware can use parts of MicroOCPP and work with its own implementation for the rest. In that case only the selected parts of MicroOCPP will be compiled into the firmware. For example, the main firmware can use the RPC framework and build a custom implementation of the OCPP logic on top of it. This could be necessary if the OCPP behavior should be tightly coupled to other modules of the firmware. In a different scenario, the EVSE firmware could already contain an extensive RPC framework and the OCPP client should reuse it. Then, only the business logic and high-level API are of interest.


Selective usage of MicroOCPP

Customizations of the library allow to integrate use cases for which the high-level API is too restrictive. The high-level API is designed to provide a facade for the expected usage of the library, but since the charging sector is driven by innovation, new use cases for OCPP emerge every day. If a custom use case cannot be integrated on the API level, the main firmware can access the internal data structures of MicroOCPP and complement the required functionality or replace parts of the internal behavior with custom implementations which fits the concrete scenarios better. ## Main-loop paradigm MicroOCPP works with the common main-loop execution model of microcontrollers. After initialization, the EVSE firmware most likely enters a main-loop and repeats it infinitely. To run MicroOCPP, a call to its loop function must be placed into the main loop of the firmware. Then at each main-loop iteration, MicroOCPP executes its internal routines, i.e. it processes input data, updates its data model, executes operations and creates new output data. The MicroOCPP loop function does not block the main loop but executes immediately. This library does not contain any delay functions. Some activities of the library spread over many loop iterations like the start of a charging session which needs to await the approval of an NFC card and a hardware diagnosis of the high power electronics for example. All activities in MicroOCPP support the distribution over many loop calls, leading to a pseudo-parallel execution behavior. No separate RTOS task is needed and MicroOCPP does not have an internal mechanism for multi-task synchronization. However, it is of course possible to create a dedicated OCPP task, as long as extra care is taken of the synchronization. ## How the API works The high-level API consists of four parts: - **Library lifecycle**: The library has initialize functions with a few initialization options. Dynamic system components like the WebSocket adapter need to be set at initialization time. The deinitialize function reverts the library into an unitialized state. That's useful for memory inspection tools like valgrind or to disable the OCPP communication. The loop function also counts as part of the lifecycle management. - **Sensor Inputs**: EVSEs are mechanical systems with a variety of sensor information. OCPP is used to send parts of the sensor readings to the server. The other part of the sensor data flows into the local charger model of MicroOCPP where it is further processed. To update MicroOCPP with the input data from the sensors, the firmware needs to bind the sensors to the library. An *Input-binding*, or in short *Input*, is a function which transfers the current sensor value to MicroOCPP. Inputs are callback functions which read a specific sensor value and pass the value in the return statement. The firmware defines those callback functions for each sensor and adds them to MicroOCPP during initialization. After initialization, MicroOCPP uses the callbacks and executes them to fetch the most recent sensor values.
This concept is reused for the data *Outputs* of the library to the firmware, where the callback applies output data from MicroOCPP to the firmware. - **Transaction management**: OCPP considers EVSEs as vending machines. To enable payment processing and the billing of the EVSE usage, all charging activity is assigned to transactions. A big portion of OCPP is about transactions, their prerequisites, runtime and their termination scenarios. The MicroOCPP API breaks transactions down into an initiation and termination function and gives a transparent view on the current process status, authorization result and offline behavior strategy. For non-commercial setups, the transaction mechanism is the same but has only informational purposes. - **Device management**: MicroOCPP implements the OCPP side of the device management operations. For the actual execution, the firmware needs to provide the charger-side implementations of the operations to MicroOCPP by passing handler functions to the API. For example, the OCPP server can restart the charger. Upon receipt of the request, MicroOCPP terminates the transactions and eventually triggers the system restart using the handler function which the firmware has provided through the high-level API. ## Transaction safety Software in EVSEs needs to withstand hazardous operating conditions. EVSEs are located on the street or in garages where the WiFi or LTE signal strength is often weak, leading to long offline periods or where random power cuts can occur. In addition to that, the lack of process virtualization on microcontrollers means that a malfunction in one part of the firmware leads to the crash of all other parts. The transaction process of MicroOCPP is robust against random failures or resets. A minimal transaction log on the flash storage ensures that each operation on a transaction is fully executed. It will always result in a consistent state between the EVSE and the OCPP server, even over resets of the microcontroller. The RPC queue facilitates this by tracking the delivery status of relevant messages. If the microcontroller is reset while the delivery status of a message is unknown, MicroOCPP takes up the message delivery again at the next start up and completes it. A requirement for the transaction safety feature is the availability of a journaling file system. Examples include LittleFS, SPIFFS and the POSIX file API, but some microcontroller platforms don't support this natively, so an extension would be required. ## Unit testing MicroOCPP includes a number of unit tests based on the [Catch2](https://github.com/catchorg/Catch2) framework. A [GitHub Action](https://github.com/matth-x/MicroOcpp/actions) runs the unit tests against each new commit in the MicroOCPP repository, which ensures that new features don't break old code. The scope of the unit tests is to to ensure a correct implementation of OCPP and to validate the high-level API against its definition. For that, it is not necessary to establish an actual test connection to an OCPP server. In fact, real-world communication would disturb the tests and make them undeterministic. That's why the test suite is fully based on an integrated, tiny OCPP test server which the OCPP client reaches over a loopback connection. The test suite does not access the WebSocket library. When making the unit tests of the main firmware, it is not necessary to check the full OCPP communication, but only to validate correct usage of the high-level API. An example of how the library can be initialized with a loopback connection can be found in its test suite. ## Microcontroller optimization As a library for microcontrollers, the design of MicroOCPP considers the strict memory limits and complies with the best practices of embedded software development. Also, a few measures were taken to optimize the memory usage which include the spare inclusion of external libraries, an optimization of the internal data structures and the exclusion of C++ run-time type information (RTTI) and exceptions. Features of C++ which may have a larger footprint are carefully used such as the standard template library (STL) and lambda functions. The STL increases the robustness of the code and lambdas prove to be a powerful tool to deal with the complexity of asynchronous data processing in embedded systems. That's also why the high-level API has many functional parameters. Because of the high importance of C in the embedded world, MicroOCPP provides its high-level API in C too. It is typically simple to instruct the compiler to compile and link the C++-based library in a C-based firmware development. In case that the firmware requires custom features which are not part of the C-API, then the firmware can implement it in a new C++ source file, export the new functions to the C namespace and use it normally in the main source. While memory constraints are of concern, the execution time generally is not. OCPP is rather uncomplex on the algorithmic side for clients, since there is no need for elaborate planning algorithms or complex data transformations. Low resource requirements also allow new usage areas on top of EV charging. For example, MicroOCPP has been ported to ordinary IoT equipment such as Wi-Fi sockets to integrate further electric devices into OCPP networks. Although MicroOCPP is optimized for the usage on microcontrollers, it is also suitable for embedded Linux systems. With more memory available, the upper limits of the internal data structures can be increased, leading to a more versatile support of charging use cases. Also, the separation of the charger firmware into multiple processes can lead to more robustness. MicroOCPP can be extended by an inter-process communication (IPC) interface to run in a separate process. ================================================ FILE: docs/migration.md ================================================ # Migrating to v1.1 As a new minor version, all features should work the same as in v1.0 and existing integrations are mostly backwards compatible. However, some fixes / cleanup steps in MicroOCPP require syntactic changes or special consideration when upgrading from v1.0 to v1.1. The known pitfalls are as follows: - The default branch has been renamed from `master` into `main` - Need to include extra headers: the transitive includes have been cleaned a bit. Probably it's necessary to add more includes next to `#include `. E.g.
`#include `
`#include ` - `ocppPermitsCharge()` does not consider failures reported by the charger anymore. Before v1.1 it was possible to report failures to MicroOCPP using ErrorCodeInputs and then to rely on `ocppPermitsCharge()` becoming false when a failure occurs. For backwards compatibility, complement any occurence to `ocppPermitsCharge() && !isFaulted()` - `setEnergyMeterInput` changed the expected return type of the callback function from `float` to `int` (see [#301](https://github.com/matth-x/MicroOcpp/pull/301)) - The return type of the UnlockConnector handler also changed from `PollResult` to enum `UnlockConnectorResult` (see [#271](https://github.com/matth-x/MicroOcpp/pull/271)) If upgrading MicroOcppMongoose at the same time, then the following changes are very important to consider: - Certificates are no longer copied into heap memory, but the MO-Mongoose class takes the passed certificate pointer as a zero-copy parameter. The string behind the passed pointer must outlive the MO-Mongoose class (see [#10](https://github.com/matth-x/MicroOcppMongoose/pull/10)) - WebSocket authorization keys are no longer stored as c-strings, but as `unsigned char` buffers. For backwards compatibility, a null-byte is still appended and the buffer can be accessed as c-string, but this should be tested in existing deployments. Furtermore, MicroOCPP only accepts hex-encoded keys coming via ChangeConfiguration which is mandated by the standard. This also may break existing deployments (see [#4](https://github.com/matth-x/MicroOcppMongoose/pull/4)). If accessing the MicroOCPP modules directly (i.e. not over `MicroOcpp.h` or `MicroOcpp_c.h`) then there are likely some more modifications to be done. See the history of pull requests where each change to the code is documented. However, if the existing integration compiles under the new MO version, then there shouldn't be too many unexpected incompatibilities. ## Migrating to v1.0 The API has been continously improved to best suit the common use cases for MicroOCPP. Moreover, the project has been given a new name to prevent confusion with the relation to the Arduino platform and to reflect the project goals properly. With the new project name, the API has been frozen for the v1.0 release. ### Adopting the new project name in existing projects Find and replace the keywords in the following. If using the C-facade (skip if you don't use anything from *ArduinoOcpp_c.h*): - `AO_Connection` to `OCPP_Connection` - `AO_Transaction` to `OCPP_Transaction` - `AO_FilesystemOpt` to `OCPP_FilesystemOpt` - `AO_TxNotification` to `OCPP_TxNotification` - `ao_set_console_out_c` to `ocpp_set_console_out_c` Change this in any case: - `ArduinoOcpp` to `MicroOcpp` - `"AO_` to `"Cst_` (define build flag `MO_CONFIG_EXT_PREFIX="AO_"` to keep old config keys) - `AO_` to `MO_` - `ocpp_` to `mocpp_` Change this if used anywhere: - `ao_set_console_out` to `mocpp_set_console_out` - `ao_tick_ms` to `mocpp_tick_ms` If using the C-facade, change this as the final step: - `ao_` to `ocpp_` ### Further API changes to consider In addition to the new project name, the API has also been reworked for more consistency. After renaming the existing project as described above, also take a look at the [changelogs](https://github.com/matth-x/MicroOcpp/blob/1.0.x/CHANGELOG.md) (see Section Changed for v1.0.0). **If something is missing in this guide, please share the issue here:** [https://github.com/matth-x/MicroOcpp/issues/176](https://github.com/matth-x/MicroOcpp/issues/176) ================================================ FILE: docs/modules.md ================================================ # Modules This chapter gives an overview of the class structure of MicroOCPP. ## Context The *Context* contains all runtime data of MicroOCPP. Every data object which this library creates is stored in the Context instance, except only the Configuration. So it is the basic entry point to the internals of the library. The structure of the context follows the main architecture as described in [this introduction](intro-tech) and consists of the Request queue and message deserializer for the RPC framework and the Model object for the OCPP model and behavior (see below). When the library is initialized, `getOcppContext()` returns the current Context object. ## Model The *Model* represents the OCPP device model and behavior. OCPP defines a rough charger model, i.e. the hardware parts of the charger and their basic functionality in relation to the OCPP operations. Furthermore, OCPP specifies a few only software related features like the reservation of the charger. This charger model is implemented as straightforward C++ data structures and corresponding algorithms. The implementation of the Model is structured into a top-level Model class and the subordinate Service classes. Each Service class represents a functional block of the OCPP specification and implements the corresponding data structures and functionality. The definition of the functional blocks in MicroOCPP is very similar to the feature profiles in OCPP. Only the Core profile is split into multiple functional blocks to keep a smaller module scope. The following list contains the resulting functional blocks: - **Authorization**: local information of user identifiers and their authorization status - **Boot**: implementation of the *preboot* behavior, i.e. sending and processing the BootNotification message - **ChargingSession**: management of charging sessions and control of the high power charging hardware - **Diagnostics**: GetDiagnostics upload routine - **FirmwareManagement**: UpdateFirmware download routine - **Heartbeat**: periodic OCPP Heartbeats (not including WebSocket ping-pongs) - **Metering**: periodic MeterValue messages and local caching - **Reservation**: management of Reservation lists and their effect on the authorization routine - **Reset**: execution of OCPP Reset message - **Transactions**: transaction journal behind StartTransaction and StopTransaction messages and *Transaction* class for extensions of the transaction mechanism ## Requests The *Request* class and all similarly named classes implement the Remote Procdure Call (RPC) framework of OCPP. A request executes an operation on the remote end of an OCPP connection. If a charger sends a request to a server, then the server will update its data base with the payload and vice versa. After receiving a request, each node replies with a confirmation, acknowledging the successful execution of the operation or notifying about an error. When being offline, outgoing requests must be queued before sending which is implemented in *RequestQueue*. Queueing is especially challenging for longer offline periods when the number of cached messages exceeds the memory limit. To address this, messages are swapped to the flash memory when the queue limit is reached as implemented in the *RequestStore* and *RequestQueueStorageStrategy* class. Incoming messages can be processed directly and don't have an extensive queueing mechanism. ## Operations Every OCPP operation (e.g. Heartbeat, BootNotification) has a dedicated class for creating outgoing messages, interpreting incoming messages, executing the specified OCPP action and handling responses. Operations work on the data structures of the Model layer. To send operations to the OCPP server, they must be wrapped into a Request object. The RPC framework and operations are separated modules. While the RPC framework (including the Request class) deals with the messaging mechanism and transfering data to the other OCPP device, operations define the effect on the OCPP model and data structure and execute the desired action. The operation classes inherit from *Operation* which is the interface visible to the Request class. Incoming messages are unmarshalled using the *OperationRegistry*. During the initialization phase of the library, the Model classes register all supported operations with their name and an instantiator. The instantiator, when executed, provides the Request interpreter with an instance of the corresponding Operation subclasses. It is possible to extend MicroOCPP by adding new Operation instantiators to the registry, or to modify the behavior by overriding the default Operation implementations. In addition to that, event handlers can be set which the RPC queue will notify with the payload once operations are sent or received. ## Configuration Configurations like the HeartbeatInterval are managed by the *Configuration* module which consists of - *AbstractConfiguration*: a single configuration as a key-value pair without value type - *Configuration*: a concrete configuration with a value type like `bool` or `const char*`. Inherits from AbstractConfiguration - *ConfigurationContainer*: a collection of AbstractConfigurations and an optional storage implementation. Multiple containers can be set for a separation of the configurations and different storage strategies. Each container has a unique file name - *ConfigurationContainerVolatile*: no persistency and access to the file system - *ConfigurationContainerFlash*: persistency by storing JSON files on the flash If another storage implementation is required (e.g. for syncing with an external configuration manager), then it's possible to add a custom ConfigurationContainer. In the initialization phase, MicroOCPP loads the built-in Configurations with hard-coded factory defaults and a default storage structure. To customize the factory defaults or which ConfigurationContainers will be used, the Configuration module must be initialized before loading the library. To do so, call `configuration_init(...)`. Then the factory defaults can be applied by calling `declareConfiguration(...)` with the desired default value. To use a custom ConfigurationContainer, call `addConfigurationContainer(...)` with the custom implementation. When the library is loaded afterwards, it will use the previously provided Configurations / Containers and create only the data structure which hasn't been set already. ================================================ FILE: docs/prerequisites.md ================================================ # Development tools and basic prerequisites This page explains how to work with this library using the appropriate development tools. Skip it if your IDE is set up and you already have an OCPP test server. ## Development tools prerequisites Throughout these document pages, it is assumed that you already have set up your development environment and that you are familiar with the corresponding building, flashing and (basic) debugging routines. MicroOCPP runs in many environments (from the Arduino-IDE to proprietary microcontroller IDEs like the Code Composer Studio). If you do not have any preferences yet, it is highly recommended to get started with VSCode + the PlatformIO add-on, since it is the favorite setup of the community and therefore you find the most related information in the Issues pages of the main repository. There are many high-quality tutorials for out there for setting up VSCode + PIO. The following site covers everything you need to know: [https://randomnerdtutorials.com/vs-code-platformio-ide-esp32-esp8266-arduino/](https://randomnerdtutorials.com/vs-code-platformio-ide-esp32-esp8266-arduino/) Once that's done, adding MicroOCPP is no big deal anymore. However, let's discuss another very important tool for your project first. ## OCPP Server prerequisites MicroOCPP is just a client, but all the magic of OCPP lives in the communication between a client and a server. Although it *is* possible to run MicroOCPP without a real server for testing purposes, the best approach for getting started is to get the hands on a real server. So you can always use the client in a practical setup, see immediate results and simplify development a lot. Perhaps you were already given access to an OCPP server for your project. Then you can use that, it should work fine. If you don't have a server already, it is highly recommended to get SteVe ([https://github.com/steve-community/steve](https://github.com/steve-community/steve)). It allows to control every detail of the OCPP operations and shows detail-rich information about the results. And again, it is the favorite test server of the community, so you will find the most related information on the Web. For the installation instructions, please refer to the [SteVe docs](https://github.com/steve-community/steve#configuration-and-installation). In case you can't wait to get started, you can make the first connection test with a WebSocket echo server as a fake OCPP service. MicroOCPP supports that: it can send all messages to an echo server which reflects all traffic. MicroOCPP gets back its own messages and replies to itself with mocked responses. Complicated, but it does work and the console will show a valid OCPP communication. An example echo server is given in the following section. For the further development though, you will definitely need a real OCPP server. ## Project structure MicroOCPP is a library, i.e. it is not a full firmware, but just solves one specific task in your project which is the OCPP connectivity. The project structure should reflect this: typically you download MicroOCPP into a libraries or dependencies subfolder, while the main part of the development takes place in a main source folder. All dependencies of MicroOCPP (i.e. ArduinoJson, see the dependencies sections) should be located in the same libraries or dependencies folder. When the include paths are correctly set up, you should be able `#include ` at the top of your own source files. This setup keeps the OCPP library source separate from your integration and gives the project a clear structure. ## Dependency managers Currently, the PlatformIO dependency manager is supported. In the `platformio.ini` manifest, you can add `matth-x/MicroOcpp` to the `lib_deps` section. ================================================ FILE: docs/security.md ================================================ # Security MicroOCPP is designed to be compatible with IoT devices which leads to special considerations regarding cyber security. This section describes the challenges and security concepts of MicroOCPP. ## Challenges of using microcontrollers in safety-critical environments The two challenges are as follows: 1. Lack of process virtualization in RTOS operating systems 2. Less attention for potential vulnerabilities in the used libraries In a general purpose OS like Linux, the internet communication modules of an application typically run in a different process than the data base or the hardware supervision / control function. In contrast, on a typical RTOS, all modules are compiled into the same binary, sharing the same address space and lifecycle when being executed. This means that once the network stack crashes, all software on the chip is reset and a vulnerability on the network stack could be exploited to read or manipulate the data of the full runtime environment. Challenge 2) is due to the fact that OCPP uses standard web technology (WebSocket over TLS), but microcontrollers are missing out the most widespread networking software like OpenSSL or the networking libraries of Linux. The available networking libraries for microcontrollers are also audited well (e.g. lwIP, mbedTLS), but in general there is more attention on potential vulnerabilites in the Linux world, because a huge share of commercial IT systems is based on Linux. On the upside, an advantage of microcontrollers is their single purpose usage and thus, reduced complexity. Many security breaches are caused by misconfigured and often even superflous software components (e.g. due to overlooked open ports) which are not a regular part of a microcontroller firmware. ## Security measures of MicroOCPP To address the challenges, the following measures were taken: - Input sanitazion: MicroOCPP only accepts the JSON format for all input. It is validated by ArduinoJson. Every JSON value is checked against the expected format and for conformity with the OCPP specification before using it. The JSON object is discarded immediately after interpretation - Transaction safety: to address crashes and random reboots of the microcontroller during operation, all activities of the OCPP library are programmed so that they will either be resumed or fully reverted after reboots, preventing inconsistent states. See also [Transaction safety](../intro-tech/#transaction-safety) - Careful choice of the dependencies: the mandatory dependency, [ArduinoJson](https://github.com/bblanchon/ArduinoJson), has a test coverage of nearly 100% and is fuzzed. The same goes for the recommended WebSocket library, [Mongoose](https://github.com/cesanta/mongoose). Both projects are very relevant in their field with over 6k and 9k stars on GitHub Two further measures would be beneficial and could be requested via support request: - Precautious memory allocation: migrating memory management to the stack and where possible would simplify code analysis and reduce the potential of vulnerabilities - OCPP fuzzer: as a stateful application protocol, there are specific challenges of developing a fuzzer. An open source fuzzing framework for OCPP could reveal vulnerabilities and be of use for other OCPP projects as well. MicroOCPP is a good foundation for trying new fuzzing approaches. The exposure of the main-loop function and the clock allow a fine-grained access to the program flow and facilitating random alterations of the environment conditions. Furthermore, all persistent data is stored in the JSON format and it is possible to develop a grammatic which contains both a device status and incoming OCPP messages. The Configuration interface could be reused for further status variables which don't need to be persistent in practice, but would improve fuzzing performance when being accessible by the fuzzer. - Memory pool: object orientation is a very helpful programming paradigm for OCPP. The standard contains a lot of polymorphic entities and optional or variable length data fields. MicroOCPP makes use of the heap and allocates new chunks of memory as the device model is populated with data. On the upside this allows to save a lot of memory during normal operation, but it also entails the risk of memory depletion of the whole controller. A fixed memory pool for OCPP would encapsulate the heap usage to a certain address space and set a hard limit for the memory consumption and avoid polluting the shared heap area by heap fragmentation. To realize the memory pool, it would be necessary to make the allocate and deallocate functions configurable by the client code. Then appropriate (de)allocators can be injected limiting the memory use to a restricted address area. As a consequence, a more thorough allocation error handling in the MicroOCPP code is required and test cases which randomly suppress allocations to test if the library always reverts to a consistent state. A less invasive alternative to memory pools is to inject measured (de)allocators which just prevent the allocation of new memory chunks after a certain threshold has been exceeded. This programming technique would also allow to create much more fine-grained benchmarks of the library. ## Measures to be taken by the EVSE vendor As a general rule, the communication controller which is exposed to the internet shouldn't be used for safety-critical tasks on the charging hardware. That's because the networking stack is a very complex piece of software which very likely still has open bugs which can crash the controller despite all the effort to improve it. Safety-critical tasks on the charging hardware shouldn't rely on a controller which could crash at any time because of incoming network traffic. To mitigate this, either the OCPP library and internet functionality should be placed onto a separate chip, or the most vital safety functionality should get a dedicated controller. The recommended [Mongoose WebSocket adapter for MicroOCPP](https://github.com/matth-x/MicroOcppMongoose) supports the OCPP Security Profile 2 (TLS with Basic Authentication) and needs to be provided with the necessary TLS certificate. Most IoT-controllers have built-in mechanisms to ensure the authenticity of their firmware. For example, the Espressif32 supports [Secure Boot](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/secure-boot-v2.html) which is a signature verification of the installed firmware before that firmware is executed. Many platforms also have a built-in signature verification for incoming OTA firmware updates. To prove the authenticity of the charger to the OCPP server, it is also important to keep the WebSocket key secret by [encrypting the flash memory](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/flash-encryption.html). These security mechanisms heavily depend on the host controller which runs MicroOCPP. It is the responsibility of the main firmware to make proper use of them. ## OCPP Security Whitepaper and ISO 15118 With MicroOCPP, the recommended way of handling certificates on microcontrollers is to compile them into the firmware binary and to rely on the built-in firmware signature checks of the host microcontroller platform. This lean approach results in a smaller attack vector compared to establishing a separate infrastructure for the server- and firmware certificate. It can be assumed that the OTA functionality of the microcontrollers is thoroughly tested and consequently, reaching a comparable level of robustness would require much effort. In case the certificate handling mechanism of the Security Whitepaper is preferred, then the EVSE vendor needs to implement it via a custom extension. Unfortunately, this mechanism hasn't been requested yet and is not natively supported by MicroOCPP yet. The new custom operations can be implemented by extending the class `Operation`. A handler for incoming messages can be registered via `OperationRegistry::registerOperation(...)`. To send custom messages to the server, use `Context::initiateRequest(...)`. A further challenge for microcontrollers is the relatively low processor speed which becomes relevant for a potential ISO 15118 integration. Some incoming message types (`AuthorizationReq` and `MeteringReceiptReq`) include a signature which needs to be verified on the communications controller of the EVSE. Moreover, messages in the ISO 15118 V2G protocol have a maximum round trip time (which is 2 seconds for the message types in question) and so the signature verification is time-contrained. [These benchmarks](https://web.archive.org/web/20230724184529/https://www.oryx-embedded.com/benchmark/espressif/crypto-esp32.html) for the Espressif32 show that for some signature algorithms, the verification time can get close or exceed the timing requirements of ISO 15118 if done on the processor only. As a consequence, hardware acceleration by the crypto-core is mandatory to ensure a robust communication between the EVSE and EV. Before making a communications controller with ISO 15118 support, the performance of the host controller should be benchmarked and checked against the requirements. *Disclaimer: the outlined risks in this section are not a complete list. Also, every system has unique security challenges which require individual attention. In doubt, please consult an IT-security specialist.* ================================================ FILE: docs/stylesheets/extra.css ================================================ :root { --md-primary-fg-color: #2984C7; } ================================================ FILE: examples/ESP/main.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if defined(ESP8266) #include #include ESP8266WiFiMulti WiFiMulti; #elif defined(ESP32) #include #else #error only ESP32 or ESP8266 supported at the moment #endif #include #define STASSID "YOUR_WIFI_SSID" #define STAPSK "YOUR_WIFI_PW" #define OCPP_BACKEND_URL "ws://echo.websocket.events" #define OCPP_CHARGE_BOX_ID "" // // Settings which worked for my SteVe instance: // //#define OCPP_BACKEND_URL "ws://192.168.178.100:8180/steve/websocket/CentralSystemService" //#define OCPP_CHARGE_BOX_ID "esp-charger" void setup() { /* * Initialize Serial and WiFi */ Serial.begin(115200); Serial.print(F("[main] Wait for WiFi: ")); #if defined(ESP8266) WiFiMulti.addAP(STASSID, STAPSK); while (WiFiMulti.run() != WL_CONNECTED) { Serial.print('.'); delay(1000); } #elif defined(ESP32) WiFi.begin(STASSID, STAPSK); while (!WiFi.isConnected()) { Serial.print('.'); delay(1000); } #else #error only ESP32 or ESP8266 supported at the moment #endif Serial.println(F(" connected!")); /* * Initialize the OCPP library */ mocpp_initialize(OCPP_BACKEND_URL, OCPP_CHARGE_BOX_ID, "My Charging Station", "My company name"); /* * Integrate OCPP functionality. You can leave out the following part if your EVSE doesn't need it. */ setEnergyMeterInput([]() { //take the energy register of the main electricity meter and return the value in watt-hours return 0.f; }); setSmartChargingCurrentOutput([](float limit) { //set the SAE J1772 Control Pilot value here Serial.printf("[main] Smart Charging allows maximum charge rate: %.0f\n", limit); }); setConnectorPluggedInput([]() { //return true if an EV is plugged to this EVSE return false; }); //... see MicroOcpp.h for more settings } void loop() { /* * Do all OCPP stuff (process WebSocket input, send recorded meter values to Central System, etc.) */ mocpp_loop(); /* * Energize EV plug if OCPP transaction is up and running */ if (ocppPermitsCharge()) { //OCPP set up and transaction running. Energize the EV plug here } else { //No transaction running at the moment. De-energize EV plug } /* * Use NFC reader to start and stop transactions */ if (/* RFID chip detected? */ false) { String idTag = "0123456789ABCD"; //e.g. idTag = RFID.readIdTag(); if (!getTransaction()) { //no transaction running or preparing. Begin a new transaction Serial.printf("[main] Begin Transaction with idTag %s\n", idTag.c_str()); /* * Begin Transaction. The OCPP lib will prepare transaction by checking the Authorization * and listen to the ConnectorPlugged Input. When the Authorization succeeds and an EV * is plugged, the OCPP lib will send the StartTransaction */ auto ret = beginTransaction(idTag.c_str()); if (ret) { Serial.println(F("[main] Transaction initiated. OCPP lib will send a StartTransaction when" \ "ConnectorPlugged Input becomes true and if the Authorization succeeds")); } else { Serial.println(F("[main] No transaction initiated")); } } else { //Transaction already initiated. Check if to stop current Tx by RFID card if (idTag.equals(getTransactionIdTag())) { //card matches -> user can stop Tx Serial.println(F("[main] End transaction by RFID card")); endTransaction(idTag.c_str()); } else { Serial.println(F("[main] Cannot end transaction by RFID card (different card?)")); } } } //... see MicroOcpp.h for more possibilities } ================================================ FILE: examples/ESP-IDF/CMakeLists.txt ================================================ # The following five lines of boilerplate have to be in your project's # CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.5) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(mo_example) idf_build_set_property(COMPILE_OPTIONS -DMO_TRAFFIC_OUT APPEND) idf_build_set_property(COMPILE_OPTIONS -DMO_FILENAME_PREFIX="/mo_store/" APPEND) ================================================ FILE: examples/ESP-IDF/Makefile ================================================ # # This is a project Makefile. It is assumed the directory this Makefile resides in is a # project subdirectory. # PROJECT_NAME := mo_example include $(IDF_PATH)/make/project.mk ================================================ FILE: examples/ESP-IDF/README.md ================================================ # ESP-IDF integration example To run MicroOcpp on the ESP-IDF platform, please take this example as the starting point. It is widely based on the [Wi-Fi Station Example](https://github.com/espressif/esp-idf/tree/release/v4.4/examples/wifi/getting_started/station) of Espressif. This example works with the ESP-IDF version `4.4`. For a general guide how to setup and use the ESP-IDF, please refer to the documentation of Espressif. ## Setup guide ### Dependencies Please clone the following repositories into the respective components-directories: - [MicroOcpp](https://github.com/matth-x/MicroOcpp) into `components/MicroOcpp` - [Mongoose (ESP-IDF integration)](https://github.com/cesanta/mongoose-esp-idf) into `components/mongoose` - [Mongoose adapter for MicroOcpp](https://github.com/matth-x/MicroOcppMongoose) into `components/MicroOcppMongoose` - [ArduinoJson (v6.21)](https://github.com/bblanchon/ArduinoJson) into `components/ArduinoJson` For setup, the following commands could come handy (change to the root directory of the ESP-IDF project first): ``` rm components/mongoose/.gitkeep rm components/MicroOcpp/.gitkeep rm components/MicroOcppMongoose/.gitkeep rm components/ArduinoJson/.gitkeep git clone https://github.com/matth-x/MicroOcpp components/MicroOcpp git clone --recurse-submodules https://github.com/cesanta/mongoose-esp-idf.git components/mongoose git clone https://github.com/matth-x/MicroOcppMongoose components/MicroOcppMongoose git clone https://github.com/bblanchon/ArduinoJson components/ArduinoJson cd components/ArduinoJson git checkout 3e1be980d93e47b2a0073efeeb9a9396fd7a83be ``` The setup is done if the following include statements work: ```cpp #include #include #include #include ``` ### Configure the project Open the project configuration menu (`idf.py menuconfig`). In the `Example Configuration` menu: * Set the Wi-Fi configuration. * Set `WiFi SSID`. * Set `WiFi Password`. * Set `OCPP backend URL`. (e.g. `ws://ocpp.example.com/steve/websocket/CentralSystemService`) * Set `ChargeBoxId`. (e.g. `my-charger` - last part of the WebSocket URL * Set `AuthorizationKey`, or leave empty if not necessary Optional: If you need, change the other options according to your requirements. ================================================ FILE: examples/ESP-IDF/components/ArduinoJson/.gitkeep ================================================ ================================================ FILE: examples/ESP-IDF/components/ArduinoOcpp/.gitkeep ================================================ ================================================ FILE: examples/ESP-IDF/components/ArduinoOcppMongoose/.gitkeep ================================================ ================================================ FILE: examples/ESP-IDF/components/README.md ================================================ ## Components folder structure The ESP-IDF integration requires at least the following components: - [MicroOcpp](https://github.com/matth-x/MicroOcpp) - [Mongoose (ESP-IDF integration)](https://github.com/cesanta/mongoose-esp-idf) - [Mongoose adapter for MicroOcpp](https://github.com/matth-x/MicroOcppMongoose) - [ArduinoJson (v6.21)](https://github.com/bblanchon/ArduinoJson) This example only provides the folder structure. You need to clone every project into it in order to run the example. ================================================ FILE: examples/ESP-IDF/components/mongoose/.gitkeep ================================================ ================================================ FILE: examples/ESP-IDF/main/CMakeLists.txt ================================================ idf_component_register(SRCS "main.c" INCLUDE_DIRS ".") ================================================ FILE: examples/ESP-IDF/main/Kconfig.projbuild ================================================ menu "Example Configuration" config ESP_WIFI_SSID string "WiFi SSID" default "myssid" help SSID (network name) for the example to connect to. config ESP_WIFI_PASSWORD string "WiFi Password" default "mypassword" help WiFi password (WPA or WPA2) for the example to use. config ESP_MAXIMUM_RETRY int "Maximum retry" default 5 help Set the Maximum retry to avoid station reconnecting to the AP unlimited when the AP is really inexistent. config MO_OCPP_BACKEND string "OCPP backend URL" default "ws://echo.websocket.events/" help URL of the OCPP backend config MO_CHARGEBOXID string "ChargeBoxId" default "" help ChargeBoxId as it appears in the WebSocket connection URL config MO_AUTHORIZATIONKEY string "Authorization Key" default "" help Passphrase for connecting to the OCPP server endmenu ================================================ FILE: examples/ESP-IDF/main/component.mk ================================================ # # Main component makefile. # # This Makefile can be left empty. By default, it will take the sources in the # src/ directory, compile them and link them into lib(subdirectory_name).a # in the build directory. This behaviour is entirely configurable, # please read the ESP-IDF documents if you need to do this. # ================================================ FILE: examples/ESP-IDF/main/main.c ================================================ /* Based on the ESP-IDF WiFi station Example (see https://github.com/espressif/esp-idf/tree/release/v4.4/examples/wifi/getting_started/station/main) This example code extends the WiFi example with the necessary calls to establish an OCPP connection on the ESP-IDF. */ #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" #include "esp_system.h" #include "esp_wifi.h" #include "esp_event.h" #include "esp_log.h" #include "nvs_flash.h" #include "lwip/err.h" #include "lwip/sys.h" /* MicroOcpp includes */ #include #include //C-facade of MicroOcpp #include //WebSocket integration for ESP-IDF /* The examples use WiFi configuration that you can set via project configuration menu If you'd rather not, just change the below entries to strings with the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid" */ #define EXAMPLE_ESP_WIFI_SSID CONFIG_ESP_WIFI_SSID #define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD #define EXAMPLE_ESP_MAXIMUM_RETRY CONFIG_ESP_MAXIMUM_RETRY #define EXAMPLE_MO_OCPP_BACKEND CONFIG_MO_OCPP_BACKEND #define EXAMPLE_MO_CHARGEBOXID CONFIG_MO_CHARGEBOXID #define EXAMPLE_MO_AUTHORIZATIONKEY CONFIG_MO_AUTHORIZATIONKEY /* FreeRTOS event group to signal when we are connected*/ static EventGroupHandle_t s_wifi_event_group; /* The event group allows multiple bits for each event, but we only care about two events: * - we are connected to the AP with an IP * - we failed to connect after the maximum amount of retries */ #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 static const char *TAG = "wifi station"; static int s_retry_num = 0; static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { esp_wifi_connect(); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) { esp_wifi_connect(); s_retry_num++; ESP_LOGI(TAG, "retry to connect to the AP"); } else { xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); } ESP_LOGI(TAG,"connect to the AP fail"); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip)); s_retry_num = 0; xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); } } void wifi_init_sta(void) { s_wifi_event_group = xEventGroupCreate(); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); esp_event_handler_instance_t instance_any_id; esp_event_handler_instance_t instance_got_ip; ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id)); ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip)); wifi_config_t wifi_config = { .sta = { .ssid = EXAMPLE_ESP_WIFI_SSID, .password = EXAMPLE_ESP_WIFI_PASS, /* Setting a password implies station will connect to all security modes including WEP/WPA. * However these modes are deprecated and not advisable to be used. Incase your Access point * doesn't support WPA2, these mode can be enabled by commenting below line */ .threshold.authmode = WIFI_AUTH_WPA2_PSK, }, }; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) ); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) ); ESP_ERROR_CHECK(esp_wifi_start() ); ESP_LOGI(TAG, "wifi_init_sta finished."); /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */ EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY); /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually * happened. */ if (bits & WIFI_CONNECTED_BIT) { ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS); } else if (bits & WIFI_FAIL_BIT) { ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s", EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS); } else { ESP_LOGE(TAG, "UNEXPECTED EVENT"); } /* The event will not be processed after unregister */ ESP_ERROR_CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip)); ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id)); vEventGroupDelete(s_wifi_event_group); } void app_main(void) { //Initialize NVS esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK(ret); ESP_LOGI(TAG, "ESP_WIFI_MODE_STA"); wifi_init_sta(); /* Initialize Mongoose (necessary for MicroOcpp)*/ struct mg_mgr mgr; // Event manager mg_mgr_init(&mgr); // Initialise event manager mg_log_set(MG_LL_DEBUG); // Set log level /* Initialize MicroOcpp */ struct OCPP_FilesystemOpt fsopt = { .use = true, .mount = true, .formatFsOnFail = true}; OCPP_Connection *osock = ocpp_makeConnection(&mgr, EXAMPLE_MO_OCPP_BACKEND, EXAMPLE_MO_CHARGEBOXID, EXAMPLE_MO_AUTHORIZATIONKEY, "", fsopt); ocpp_initialize(osock, "ESP-IDF charger", "Your brand name here", fsopt, false, false); /* Enter infinite loop */ while (1) { mg_mgr_poll(&mgr, 10); ocpp_loop(); } /* Deallocate ressources */ ocpp_deinitialize(); ocpp_deinitConnection(osock); mg_mgr_free(&mgr); return; } ================================================ FILE: examples/ESP-IDF/partitions.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, , 0x4000, otadata, data, ota, , 0x2000, phy_init, data, phy, , 0x1000, ota_0, app, ota_0, , 0x190000, ota_1, app, ota_1, , 0x190000, mo, data, spiffs, , 0xD0000, ================================================ FILE: examples/ESP-IDF/sdkconfig ================================================ # # Automatically generated file. DO NOT EDIT. # Espressif IoT Development Framework (ESP-IDF) Project Configuration # CONFIG_IDF_CMAKE=y CONFIG_IDF_TARGET_ARCH_XTENSA=y CONFIG_IDF_TARGET="esp32" CONFIG_IDF_TARGET_ESP32=y CONFIG_IDF_FIRMWARE_CHIP_ID=0x0000 # # SDK tool configuration # CONFIG_SDK_TOOLPREFIX="xtensa-esp32-elf-" # CONFIG_SDK_TOOLCHAIN_SUPPORTS_TIME_WIDE_64_BITS is not set # end of SDK tool configuration # # Build type # CONFIG_APP_BUILD_TYPE_APP_2NDBOOT=y # CONFIG_APP_BUILD_TYPE_ELF_RAM is not set CONFIG_APP_BUILD_GENERATE_BINARIES=y CONFIG_APP_BUILD_BOOTLOADER=y CONFIG_APP_BUILD_USE_FLASH_SECTIONS=y # end of Build type # # Application manager # CONFIG_APP_COMPILE_TIME_DATE=y # CONFIG_APP_EXCLUDE_PROJECT_VER_VAR is not set # CONFIG_APP_EXCLUDE_PROJECT_NAME_VAR is not set # CONFIG_APP_PROJECT_VER_FROM_CONFIG is not set CONFIG_APP_RETRIEVE_LEN_ELF_SHA=16 # end of Application manager # # Bootloader config # CONFIG_BOOTLOADER_OFFSET_IN_FLASH=0x1000 CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y # CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_DEBUG is not set # CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF is not set # CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_NONE is not set # CONFIG_BOOTLOADER_LOG_LEVEL_NONE is not set # CONFIG_BOOTLOADER_LOG_LEVEL_ERROR is not set # CONFIG_BOOTLOADER_LOG_LEVEL_WARN is not set CONFIG_BOOTLOADER_LOG_LEVEL_INFO=y # CONFIG_BOOTLOADER_LOG_LEVEL_DEBUG is not set # CONFIG_BOOTLOADER_LOG_LEVEL_VERBOSE is not set CONFIG_BOOTLOADER_LOG_LEVEL=3 # CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_8V is not set CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_9V=y # CONFIG_BOOTLOADER_FACTORY_RESET is not set # CONFIG_BOOTLOADER_APP_TEST is not set CONFIG_BOOTLOADER_WDT_ENABLE=y # CONFIG_BOOTLOADER_WDT_DISABLE_IN_USER_CODE is not set CONFIG_BOOTLOADER_WDT_TIME_MS=9000 # CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is not set # CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP is not set # CONFIG_BOOTLOADER_SKIP_VALIDATE_ON_POWER_ON is not set # CONFIG_BOOTLOADER_SKIP_VALIDATE_ALWAYS is not set CONFIG_BOOTLOADER_RESERVE_RTC_SIZE=0 # CONFIG_BOOTLOADER_CUSTOM_RESERVE_RTC is not set CONFIG_BOOTLOADER_FLASH_XMC_SUPPORT=y # end of Bootloader config # # Security features # # CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT is not set # CONFIG_SECURE_BOOT is not set # CONFIG_SECURE_FLASH_ENC_ENABLED is not set # end of Security features # # Serial flasher config # CONFIG_ESPTOOLPY_BAUD_OTHER_VAL=115200 # CONFIG_ESPTOOLPY_NO_STUB is not set # CONFIG_ESPTOOLPY_FLASHMODE_QIO is not set # CONFIG_ESPTOOLPY_FLASHMODE_QOUT is not set CONFIG_ESPTOOLPY_FLASHMODE_DIO=y # CONFIG_ESPTOOLPY_FLASHMODE_DOUT is not set CONFIG_ESPTOOLPY_FLASH_SAMPLE_MODE_STR=y CONFIG_ESPTOOLPY_FLASHMODE="dio" # CONFIG_ESPTOOLPY_FLASHFREQ_80M is not set CONFIG_ESPTOOLPY_FLASHFREQ_40M=y # CONFIG_ESPTOOLPY_FLASHFREQ_26M is not set # CONFIG_ESPTOOLPY_FLASHFREQ_20M is not set CONFIG_ESPTOOLPY_FLASHFREQ="40m" # CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set # CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y # CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set # CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set # CONFIG_ESPTOOLPY_FLASHSIZE_32MB is not set # CONFIG_ESPTOOLPY_FLASHSIZE_64MB is not set # CONFIG_ESPTOOLPY_FLASHSIZE_128MB is not set CONFIG_ESPTOOLPY_FLASHSIZE="4MB" CONFIG_ESPTOOLPY_FLASHSIZE_DETECT=y CONFIG_ESPTOOLPY_BEFORE_RESET=y # CONFIG_ESPTOOLPY_BEFORE_NORESET is not set CONFIG_ESPTOOLPY_BEFORE="default_reset" CONFIG_ESPTOOLPY_AFTER_RESET=y # CONFIG_ESPTOOLPY_AFTER_NORESET is not set CONFIG_ESPTOOLPY_AFTER="hard_reset" # CONFIG_ESPTOOLPY_MONITOR_BAUD_CONSOLE is not set # CONFIG_ESPTOOLPY_MONITOR_BAUD_9600B is not set # CONFIG_ESPTOOLPY_MONITOR_BAUD_57600B is not set CONFIG_ESPTOOLPY_MONITOR_BAUD_115200B=y # CONFIG_ESPTOOLPY_MONITOR_BAUD_230400B is not set # CONFIG_ESPTOOLPY_MONITOR_BAUD_921600B is not set # CONFIG_ESPTOOLPY_MONITOR_BAUD_2MB is not set # CONFIG_ESPTOOLPY_MONITOR_BAUD_OTHER is not set CONFIG_ESPTOOLPY_MONITOR_BAUD_OTHER_VAL=115200 CONFIG_ESPTOOLPY_MONITOR_BAUD=115200 # end of Serial flasher config # # Partition Table # # CONFIG_PARTITION_TABLE_SINGLE_APP is not set # CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set # CONFIG_PARTITION_TABLE_TWO_OTA is not set CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_OFFSET=0x8000 CONFIG_PARTITION_TABLE_MD5=y # end of Partition Table # # Example Configuration # CONFIG_ESP_WIFI_SSID="myssid" CONFIG_ESP_WIFI_PASSWORD="mypassword" CONFIG_ESP_MAXIMUM_RETRY=5 CONFIG_MO_OCPP_BACKEND="ws://echo.websocket.events/" CONFIG_MO_CHARGEBOXID="" CONFIG_MO_AUTHORIZATIONKEY="" # end of Example Configuration # # Compiler options # CONFIG_COMPILER_OPTIMIZATION_DEFAULT=y # CONFIG_COMPILER_OPTIMIZATION_SIZE is not set # CONFIG_COMPILER_OPTIMIZATION_PERF is not set # CONFIG_COMPILER_OPTIMIZATION_NONE is not set CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE=y # CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT is not set # CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE is not set CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL=2 # CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT is not set CONFIG_COMPILER_HIDE_PATHS_MACROS=y # CONFIG_COMPILER_CXX_EXCEPTIONS is not set # CONFIG_COMPILER_CXX_RTTI is not set CONFIG_COMPILER_STACK_CHECK_MODE_NONE=y # CONFIG_COMPILER_STACK_CHECK_MODE_NORM is not set # CONFIG_COMPILER_STACK_CHECK_MODE_STRONG is not set # CONFIG_COMPILER_STACK_CHECK_MODE_ALL is not set # CONFIG_COMPILER_WARN_WRITE_STRINGS is not set # CONFIG_COMPILER_DISABLE_GCC8_WARNINGS is not set # CONFIG_COMPILER_DUMP_RTL_FILES is not set # end of Compiler options # # Component config # # # Application Level Tracing # # CONFIG_APPTRACE_DEST_JTAG is not set CONFIG_APPTRACE_DEST_NONE=y CONFIG_APPTRACE_LOCK_ENABLE=y # end of Application Level Tracing # # ESP-ASIO # # CONFIG_ASIO_SSL_SUPPORT is not set # end of ESP-ASIO # # Bluetooth # # CONFIG_BT_ENABLED is not set # end of Bluetooth # # CoAP Configuration # CONFIG_COAP_MBEDTLS_PSK=y # CONFIG_COAP_MBEDTLS_PKI is not set # CONFIG_COAP_MBEDTLS_DEBUG is not set CONFIG_COAP_LOG_DEFAULT_LEVEL=0 # end of CoAP Configuration # # Driver configurations # # # ADC configuration # # CONFIG_ADC_FORCE_XPD_FSM is not set CONFIG_ADC_DISABLE_DAC=y # end of ADC configuration # # MCPWM configuration # # CONFIG_MCPWM_ISR_IN_IRAM is not set # end of MCPWM configuration # # SPI configuration # # CONFIG_SPI_MASTER_IN_IRAM is not set CONFIG_SPI_MASTER_ISR_IN_IRAM=y # CONFIG_SPI_SLAVE_IN_IRAM is not set CONFIG_SPI_SLAVE_ISR_IN_IRAM=y # end of SPI configuration # # TWAI configuration # # CONFIG_TWAI_ISR_IN_IRAM is not set # CONFIG_TWAI_ERRATA_FIX_BUS_OFF_REC is not set # CONFIG_TWAI_ERRATA_FIX_TX_INTR_LOST is not set # CONFIG_TWAI_ERRATA_FIX_RX_FRAME_INVALID is not set # CONFIG_TWAI_ERRATA_FIX_RX_FIFO_CORRUPT is not set # end of TWAI configuration # # UART configuration # # CONFIG_UART_ISR_IN_IRAM is not set # end of UART configuration # # RTCIO configuration # # CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC is not set # end of RTCIO configuration # # GPIO Configuration # # CONFIG_GPIO_ESP32_SUPPORT_SWITCH_SLP_PULL is not set # end of GPIO Configuration # # GDMA Configuration # # CONFIG_GDMA_CTRL_FUNC_IN_IRAM is not set # CONFIG_GDMA_ISR_IRAM_SAFE is not set # end of GDMA Configuration # end of Driver configurations # # eFuse Bit Manager # # CONFIG_EFUSE_CUSTOM_TABLE is not set # CONFIG_EFUSE_VIRTUAL is not set # CONFIG_EFUSE_CODE_SCHEME_COMPAT_NONE is not set CONFIG_EFUSE_CODE_SCHEME_COMPAT_3_4=y # CONFIG_EFUSE_CODE_SCHEME_COMPAT_REPEAT is not set CONFIG_EFUSE_MAX_BLK_LEN=192 # end of eFuse Bit Manager # # ESP-TLS # CONFIG_ESP_TLS_USING_MBEDTLS=y # CONFIG_ESP_TLS_USE_SECURE_ELEMENT is not set # CONFIG_ESP_TLS_CLIENT_SESSION_TICKETS is not set # CONFIG_ESP_TLS_SERVER is not set # CONFIG_ESP_TLS_PSK_VERIFICATION is not set # CONFIG_ESP_TLS_INSECURE is not set # end of ESP-TLS # # ESP32-specific # CONFIG_ESP32_REV_MIN_0=y # CONFIG_ESP32_REV_MIN_1 is not set # CONFIG_ESP32_REV_MIN_2 is not set # CONFIG_ESP32_REV_MIN_3 is not set CONFIG_ESP32_REV_MIN=0 CONFIG_ESP32_DPORT_WORKAROUND=y # CONFIG_ESP32_DEFAULT_CPU_FREQ_80 is not set CONFIG_ESP32_DEFAULT_CPU_FREQ_160=y # CONFIG_ESP32_DEFAULT_CPU_FREQ_240 is not set CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ=160 # CONFIG_ESP32_SPIRAM_SUPPORT is not set # CONFIG_ESP32_TRAX is not set CONFIG_ESP32_TRACEMEM_RESERVE_DRAM=0x0 # CONFIG_ESP32_ULP_COPROC_ENABLED is not set CONFIG_ESP32_ULP_COPROC_RESERVE_MEM=0 CONFIG_ESP32_DEBUG_OCDAWARE=y CONFIG_ESP32_BROWNOUT_DET=y CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_0=y # CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_1 is not set # CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_2 is not set # CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_3 is not set # CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_4 is not set # CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_5 is not set # CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_6 is not set # CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_7 is not set CONFIG_ESP32_BROWNOUT_DET_LVL=0 CONFIG_ESP32_TIME_SYSCALL_USE_RTC_FRC1=y # CONFIG_ESP32_TIME_SYSCALL_USE_RTC is not set # CONFIG_ESP32_TIME_SYSCALL_USE_FRC1 is not set # CONFIG_ESP32_TIME_SYSCALL_USE_NONE is not set CONFIG_ESP32_RTC_CLK_SRC_INT_RC=y # CONFIG_ESP32_RTC_CLK_SRC_EXT_CRYS is not set # CONFIG_ESP32_RTC_CLK_SRC_EXT_OSC is not set # CONFIG_ESP32_RTC_CLK_SRC_INT_8MD256 is not set CONFIG_ESP32_RTC_CLK_CAL_CYCLES=1024 CONFIG_ESP32_DEEP_SLEEP_WAKEUP_DELAY=2000 CONFIG_ESP32_XTAL_FREQ_40=y # CONFIG_ESP32_XTAL_FREQ_26 is not set # CONFIG_ESP32_XTAL_FREQ_AUTO is not set CONFIG_ESP32_XTAL_FREQ=40 # CONFIG_ESP32_DISABLE_BASIC_ROM_CONSOLE is not set # CONFIG_ESP32_NO_BLOBS is not set # CONFIG_ESP32_COMPATIBLE_PRE_V2_1_BOOTLOADERS is not set # CONFIG_ESP32_COMPATIBLE_PRE_V3_1_BOOTLOADERS is not set # CONFIG_ESP32_USE_FIXED_STATIC_RAM_SIZE is not set CONFIG_ESP32_DPORT_DIS_INTERRUPT_LVL=5 # end of ESP32-specific # # ADC-Calibration # CONFIG_ADC_CAL_EFUSE_TP_ENABLE=y CONFIG_ADC_CAL_EFUSE_VREF_ENABLE=y CONFIG_ADC_CAL_LUT_ENABLE=y # end of ADC-Calibration # # Common ESP-related # CONFIG_ESP_ERR_TO_NAME_LOOKUP=y # end of Common ESP-related # # Ethernet # CONFIG_ETH_ENABLED=y CONFIG_ETH_USE_ESP32_EMAC=y CONFIG_ETH_PHY_INTERFACE_RMII=y CONFIG_ETH_RMII_CLK_INPUT=y # CONFIG_ETH_RMII_CLK_OUTPUT is not set CONFIG_ETH_RMII_CLK_IN_GPIO=0 CONFIG_ETH_DMA_BUFFER_SIZE=512 CONFIG_ETH_DMA_RX_BUFFER_NUM=10 CONFIG_ETH_DMA_TX_BUFFER_NUM=10 CONFIG_ETH_USE_SPI_ETHERNET=y # CONFIG_ETH_SPI_ETHERNET_DM9051 is not set # CONFIG_ETH_SPI_ETHERNET_W5500 is not set # CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL is not set # CONFIG_ETH_USE_OPENETH is not set # end of Ethernet # # Event Loop Library # # CONFIG_ESP_EVENT_LOOP_PROFILING is not set CONFIG_ESP_EVENT_POST_FROM_ISR=y CONFIG_ESP_EVENT_POST_FROM_IRAM_ISR=y # end of Event Loop Library # # GDB Stub # # end of GDB Stub # # ESP HTTP client # CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y # CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH is not set CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH=y # end of ESP HTTP client # # HTTP Server # CONFIG_HTTPD_MAX_REQ_HDR_LEN=512 CONFIG_HTTPD_MAX_URI_LEN=512 CONFIG_HTTPD_ERR_RESP_NO_DELAY=y CONFIG_HTTPD_PURGE_BUF_LEN=32 # CONFIG_HTTPD_LOG_PURGE_DATA is not set # CONFIG_HTTPD_WS_SUPPORT is not set # end of HTTP Server # # ESP HTTPS OTA # # CONFIG_OTA_ALLOW_HTTP is not set # end of ESP HTTPS OTA # # ESP HTTPS server # # CONFIG_ESP_HTTPS_SERVER_ENABLE is not set # end of ESP HTTPS server # # Hardware Settings # # # MAC Config # CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_STA=y CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_AP=y CONFIG_ESP_MAC_ADDR_UNIVERSE_BT=y CONFIG_ESP_MAC_ADDR_UNIVERSE_ETH=y # CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES_TWO is not set CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES_FOUR=y CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES=4 # end of MAC Config # # Sleep Config # CONFIG_ESP_SLEEP_POWER_DOWN_FLASH=y CONFIG_ESP_SLEEP_RTC_BUS_ISO_WORKAROUND=y # CONFIG_ESP_SLEEP_GPIO_RESET_WORKAROUND is not set # CONFIG_ESP_SLEEP_FLASH_LEAKAGE_WORKAROUND is not set # end of Sleep Config # # RTC Clock Config # # end of RTC Clock Config # end of Hardware Settings # # IPC (Inter-Processor Call) # CONFIG_ESP_IPC_TASK_STACK_SIZE=1536 CONFIG_ESP_IPC_USES_CALLERS_PRIORITY=y CONFIG_ESP_IPC_ISR_ENABLE=y # end of IPC (Inter-Processor Call) # # LCD and Touch Panel # # # LCD Peripheral Configuration # CONFIG_LCD_PANEL_IO_FORMAT_BUF_SIZE=32 # end of LCD Peripheral Configuration # end of LCD and Touch Panel # # ESP NETIF Adapter # CONFIG_ESP_NETIF_IP_LOST_TIMER_INTERVAL=120 CONFIG_ESP_NETIF_TCPIP_LWIP=y # CONFIG_ESP_NETIF_LOOPBACK is not set CONFIG_ESP_NETIF_TCPIP_ADAPTER_COMPATIBLE_LAYER=y # end of ESP NETIF Adapter # # PHY # CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE=y # CONFIG_ESP_PHY_INIT_DATA_IN_PARTITION is not set CONFIG_ESP_PHY_MAX_WIFI_TX_POWER=20 CONFIG_ESP_PHY_MAX_TX_POWER=20 CONFIG_ESP_PHY_REDUCE_TX_POWER=y # end of PHY # # Power Management # # CONFIG_PM_ENABLE is not set # end of Power Management # # ESP System Settings # # CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT is not set CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT=y # CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT is not set # CONFIG_ESP_SYSTEM_PANIC_GDBSTUB is not set # CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME is not set # # Memory protection # # end of Memory protection CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32 CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304 CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y # CONFIG_ESP_MAIN_TASK_AFFINITY_CPU1 is not set # CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set CONFIG_ESP_MAIN_TASK_AFFINITY=0x0 CONFIG_ESP_MINIMAL_SHARED_STACK_SIZE=2048 CONFIG_ESP_CONSOLE_UART_DEFAULT=y # CONFIG_ESP_CONSOLE_UART_CUSTOM is not set # CONFIG_ESP_CONSOLE_NONE is not set CONFIG_ESP_CONSOLE_UART=y CONFIG_ESP_CONSOLE_MULTIPLE_UART=y CONFIG_ESP_CONSOLE_UART_NUM=0 CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200 CONFIG_ESP_INT_WDT=y CONFIG_ESP_INT_WDT_TIMEOUT_MS=300 CONFIG_ESP_INT_WDT_CHECK_CPU1=y CONFIG_ESP_TASK_WDT=y # CONFIG_ESP_TASK_WDT_PANIC is not set CONFIG_ESP_TASK_WDT_TIMEOUT_S=5 CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=y # CONFIG_ESP_PANIC_HANDLER_IRAM is not set # CONFIG_ESP_DEBUG_STUBS_ENABLE is not set # CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5 is not set CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_4=y # end of ESP System Settings # # High resolution timer (esp_timer) # # CONFIG_ESP_TIMER_PROFILING is not set CONFIG_ESP_TIME_FUNCS_USE_RTC_TIMER=y CONFIG_ESP_TIME_FUNCS_USE_ESP_TIMER=y CONFIG_ESP_TIMER_TASK_STACK_SIZE=3584 CONFIG_ESP_TIMER_INTERRUPT_LEVEL=1 # CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD is not set # CONFIG_ESP_TIMER_IMPL_FRC2 is not set CONFIG_ESP_TIMER_IMPL_TG0_LAC=y # end of High resolution timer (esp_timer) # # Wi-Fi # CONFIG_ESP32_WIFI_ENABLED=y CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10 CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32 # CONFIG_ESP32_WIFI_STATIC_TX_BUFFER is not set CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER=y CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1 CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32 # CONFIG_ESP32_WIFI_CSI_ENABLED is not set CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y CONFIG_ESP32_WIFI_TX_BA_WIN=6 CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y CONFIG_ESP32_WIFI_RX_BA_WIN=6 CONFIG_ESP32_WIFI_NVS_ENABLED=y CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0=y # CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1 is not set CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752 CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32 CONFIG_ESP32_WIFI_IRAM_OPT=y CONFIG_ESP32_WIFI_RX_IRAM_OPT=y CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y # CONFIG_ESP_WIFI_SLP_IRAM_OPT is not set # CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE is not set # CONFIG_ESP_WIFI_GMAC_SUPPORT is not set CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y # end of Wi-Fi # # Core dump # # CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH is not set # CONFIG_ESP_COREDUMP_ENABLE_TO_UART is not set CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y # end of Core dump # # FAT Filesystem support # # CONFIG_FATFS_CODEPAGE_DYNAMIC is not set CONFIG_FATFS_CODEPAGE_437=y # CONFIG_FATFS_CODEPAGE_720 is not set # CONFIG_FATFS_CODEPAGE_737 is not set # CONFIG_FATFS_CODEPAGE_771 is not set # CONFIG_FATFS_CODEPAGE_775 is not set # CONFIG_FATFS_CODEPAGE_850 is not set # CONFIG_FATFS_CODEPAGE_852 is not set # CONFIG_FATFS_CODEPAGE_855 is not set # CONFIG_FATFS_CODEPAGE_857 is not set # CONFIG_FATFS_CODEPAGE_860 is not set # CONFIG_FATFS_CODEPAGE_861 is not set # CONFIG_FATFS_CODEPAGE_862 is not set # CONFIG_FATFS_CODEPAGE_863 is not set # CONFIG_FATFS_CODEPAGE_864 is not set # CONFIG_FATFS_CODEPAGE_865 is not set # CONFIG_FATFS_CODEPAGE_866 is not set # CONFIG_FATFS_CODEPAGE_869 is not set # CONFIG_FATFS_CODEPAGE_932 is not set # CONFIG_FATFS_CODEPAGE_936 is not set # CONFIG_FATFS_CODEPAGE_949 is not set # CONFIG_FATFS_CODEPAGE_950 is not set CONFIG_FATFS_CODEPAGE=437 CONFIG_FATFS_LFN_NONE=y # CONFIG_FATFS_LFN_HEAP is not set # CONFIG_FATFS_LFN_STACK is not set CONFIG_FATFS_FS_LOCK=0 CONFIG_FATFS_TIMEOUT_MS=10000 CONFIG_FATFS_PER_FILE_CACHE=y # CONFIG_FATFS_USE_FASTSEEK is not set # end of FAT Filesystem support # # Modbus configuration # CONFIG_FMB_COMM_MODE_TCP_EN=y CONFIG_FMB_TCP_PORT_DEFAULT=502 CONFIG_FMB_TCP_PORT_MAX_CONN=5 CONFIG_FMB_TCP_CONNECTION_TOUT_SEC=20 CONFIG_FMB_COMM_MODE_RTU_EN=y CONFIG_FMB_COMM_MODE_ASCII_EN=y CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=150 CONFIG_FMB_MASTER_DELAY_MS_CONVERT=200 CONFIG_FMB_QUEUE_LENGTH=20 CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 CONFIG_FMB_SERIAL_BUF_SIZE=256 CONFIG_FMB_SERIAL_ASCII_BITS_PER_SYMB=8 CONFIG_FMB_SERIAL_ASCII_TIMEOUT_RESPOND_MS=1000 CONFIG_FMB_PORT_TASK_PRIO=10 # CONFIG_FMB_PORT_TASK_AFFINITY_NO_AFFINITY is not set CONFIG_FMB_PORT_TASK_AFFINITY_CPU0=y # CONFIG_FMB_PORT_TASK_AFFINITY_CPU1 is not set CONFIG_FMB_PORT_TASK_AFFINITY=0x0 CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT=y CONFIG_FMB_CONTROLLER_SLAVE_ID=0x00112233 CONFIG_FMB_CONTROLLER_NOTIFY_TIMEOUT=20 CONFIG_FMB_CONTROLLER_NOTIFY_QUEUE_SIZE=20 CONFIG_FMB_CONTROLLER_STACK_SIZE=4096 CONFIG_FMB_EVENT_QUEUE_TIMEOUT=20 # CONFIG_FMB_TIMER_PORT_ENABLED is not set CONFIG_FMB_TIMER_GROUP=0 CONFIG_FMB_TIMER_INDEX=0 CONFIG_FMB_MASTER_TIMER_GROUP=0 CONFIG_FMB_MASTER_TIMER_INDEX=0 # CONFIG_FMB_TIMER_ISR_IN_IRAM is not set # end of Modbus configuration # # FreeRTOS # # CONFIG_FREERTOS_UNICORE is not set CONFIG_FREERTOS_NO_AFFINITY=0x7FFFFFFF CONFIG_FREERTOS_TICK_SUPPORT_CORETIMER=y CONFIG_FREERTOS_CORETIMER_0=y # CONFIG_FREERTOS_CORETIMER_1 is not set CONFIG_FREERTOS_SYSTICK_USES_CCOUNT=y CONFIG_FREERTOS_HZ=100 CONFIG_FREERTOS_ASSERT_ON_UNTESTED_FUNCTION=y # CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE is not set # CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL is not set CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y # CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK is not set CONFIG_FREERTOS_INTERRUPT_BACKTRACE=y CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=1 CONFIG_FREERTOS_ASSERT_FAIL_ABORT=y # CONFIG_FREERTOS_ASSERT_FAIL_PRINT_CONTINUE is not set # CONFIG_FREERTOS_ASSERT_DISABLE is not set CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1536 CONFIG_FREERTOS_ISR_STACKSIZE=1536 # CONFIG_FREERTOS_LEGACY_HOOKS is not set CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16 CONFIG_FREERTOS_SUPPORT_STATIC_ALLOCATION=y # CONFIG_FREERTOS_ENABLE_STATIC_TASK_CLEAN_UP is not set CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2048 CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=10 CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 # CONFIG_FREERTOS_USE_TRACE_FACILITY is not set # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set CONFIG_FREERTOS_TASK_FUNCTION_WRAPPER=y CONFIG_FREERTOS_CHECK_MUTEX_GIVEN_BY_OWNER=y # CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set # CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH is not set CONFIG_FREERTOS_DEBUG_OCDAWARE=y # CONFIG_FREERTOS_FPU_IN_ISR is not set CONFIG_FREERTOS_ENABLE_TASK_SNAPSHOT=y # CONFIG_FREERTOS_PLACE_SNAPSHOT_FUNS_INTO_FLASH is not set # end of FreeRTOS # # Hardware Abstraction Layer (HAL) and Low Level (LL) # CONFIG_HAL_ASSERTION_EQUALS_SYSTEM=y # CONFIG_HAL_ASSERTION_DISABLE is not set # CONFIG_HAL_ASSERTION_SILIENT is not set # CONFIG_HAL_ASSERTION_ENABLE is not set CONFIG_HAL_DEFAULT_ASSERTION_LEVEL=2 # end of Hardware Abstraction Layer (HAL) and Low Level (LL) # # Heap memory debugging # CONFIG_HEAP_POISONING_DISABLED=y # CONFIG_HEAP_POISONING_LIGHT is not set # CONFIG_HEAP_POISONING_COMPREHENSIVE is not set CONFIG_HEAP_TRACING_OFF=y # CONFIG_HEAP_TRACING_STANDALONE is not set # CONFIG_HEAP_TRACING_TOHOST is not set # CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS is not set # end of Heap memory debugging # # jsmn # # CONFIG_JSMN_PARENT_LINKS is not set # CONFIG_JSMN_STRICT is not set # end of jsmn # # libsodium # # end of libsodium # # Log output # # CONFIG_LOG_DEFAULT_LEVEL_NONE is not set # CONFIG_LOG_DEFAULT_LEVEL_ERROR is not set # CONFIG_LOG_DEFAULT_LEVEL_WARN is not set CONFIG_LOG_DEFAULT_LEVEL_INFO=y # CONFIG_LOG_DEFAULT_LEVEL_DEBUG is not set # CONFIG_LOG_DEFAULT_LEVEL_VERBOSE is not set CONFIG_LOG_DEFAULT_LEVEL=3 CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT=y # CONFIG_LOG_MAXIMUM_LEVEL_DEBUG is not set # CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE is not set CONFIG_LOG_MAXIMUM_LEVEL=3 CONFIG_LOG_COLORS=y CONFIG_LOG_TIMESTAMP_SOURCE_RTOS=y # CONFIG_LOG_TIMESTAMP_SOURCE_SYSTEM is not set # end of Log output # # LWIP # CONFIG_LWIP_LOCAL_HOSTNAME="espressif" # CONFIG_LWIP_NETIF_API is not set # CONFIG_LWIP_TCPIP_CORE_LOCKING is not set CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES=y # CONFIG_LWIP_L2_TO_L3_COPY is not set # CONFIG_LWIP_IRAM_OPTIMIZATION is not set CONFIG_LWIP_TIMERS_ONDEMAND=y CONFIG_LWIP_MAX_SOCKETS=10 # CONFIG_LWIP_USE_ONLY_LWIP_SELECT is not set # CONFIG_LWIP_SO_LINGER is not set CONFIG_LWIP_SO_REUSE=y CONFIG_LWIP_SO_REUSE_RXTOALL=y # CONFIG_LWIP_SO_RCVBUF is not set # CONFIG_LWIP_NETBUF_RECVINFO is not set CONFIG_LWIP_IP4_FRAG=y CONFIG_LWIP_IP6_FRAG=y # CONFIG_LWIP_IP4_REASSEMBLY is not set # CONFIG_LWIP_IP6_REASSEMBLY is not set # CONFIG_LWIP_IP_FORWARD is not set # CONFIG_LWIP_STATS is not set # CONFIG_LWIP_ETHARP_TRUST_IP_MAC is not set CONFIG_LWIP_ESP_GRATUITOUS_ARP=y CONFIG_LWIP_GARP_TMR_INTERVAL=60 CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32 CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y # CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y # CONFIG_LWIP_DHCP_RESTORE_LAST_IP is not set CONFIG_LWIP_DHCP_OPTIONS_LEN=68 # # DHCP server # CONFIG_LWIP_DHCPS=y CONFIG_LWIP_DHCPS_LEASE_UNIT=60 CONFIG_LWIP_DHCPS_MAX_STATION_NUM=8 # end of DHCP server # CONFIG_LWIP_AUTOIP is not set CONFIG_LWIP_IPV6=y # CONFIG_LWIP_IPV6_AUTOCONFIG is not set CONFIG_LWIP_IPV6_NUM_ADDRESSES=3 # CONFIG_LWIP_IPV6_FORWARD is not set # CONFIG_LWIP_NETIF_STATUS_CALLBACK is not set CONFIG_LWIP_NETIF_LOOPBACK=y CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 # # TCP # CONFIG_LWIP_MAX_ACTIVE_TCP=16 CONFIG_LWIP_MAX_LISTENING_TCP=16 CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y CONFIG_LWIP_TCP_MAXRTX=12 CONFIG_LWIP_TCP_SYNMAXRTX=12 CONFIG_LWIP_TCP_MSS=1440 CONFIG_LWIP_TCP_TMR_INTERVAL=250 CONFIG_LWIP_TCP_MSL=60000 CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5744 CONFIG_LWIP_TCP_WND_DEFAULT=5744 CONFIG_LWIP_TCP_RECVMBOX_SIZE=6 CONFIG_LWIP_TCP_QUEUE_OOSEQ=y # CONFIG_LWIP_TCP_SACK_OUT is not set # CONFIG_LWIP_TCP_KEEP_CONNECTION_WHEN_IP_CHANGES is not set CONFIG_LWIP_TCP_OVERSIZE_MSS=y # CONFIG_LWIP_TCP_OVERSIZE_QUARTER_MSS is not set # CONFIG_LWIP_TCP_OVERSIZE_DISABLE is not set CONFIG_LWIP_TCP_RTO_TIME=1500 # end of TCP # # UDP # CONFIG_LWIP_MAX_UDP_PCBS=16 CONFIG_LWIP_UDP_RECVMBOX_SIZE=6 # end of UDP # # Checksums # # CONFIG_LWIP_CHECKSUM_CHECK_IP is not set # CONFIG_LWIP_CHECKSUM_CHECK_UDP is not set CONFIG_LWIP_CHECKSUM_CHECK_ICMP=y # end of Checksums CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=3072 CONFIG_LWIP_TCPIP_TASK_AFFINITY_NO_AFFINITY=y # CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU0 is not set # CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU1 is not set CONFIG_LWIP_TCPIP_TASK_AFFINITY=0x7FFFFFFF # CONFIG_LWIP_PPP_SUPPORT is not set CONFIG_LWIP_IPV6_MEMP_NUM_ND6_QUEUE=3 CONFIG_LWIP_IPV6_ND6_NUM_NEIGHBORS=5 # CONFIG_LWIP_SLIP_SUPPORT is not set # # ICMP # CONFIG_LWIP_ICMP=y # CONFIG_LWIP_MULTICAST_PING is not set # CONFIG_LWIP_BROADCAST_PING is not set # end of ICMP # # LWIP RAW API # CONFIG_LWIP_MAX_RAW_PCBS=16 # end of LWIP RAW API # # SNTP # CONFIG_LWIP_SNTP_MAX_SERVERS=1 # CONFIG_LWIP_DHCP_GET_NTP_SRV is not set CONFIG_LWIP_SNTP_UPDATE_DELAY=3600000 # end of SNTP CONFIG_LWIP_ESP_LWIP_ASSERT=y # # Hooks # # CONFIG_LWIP_HOOK_TCP_ISN_NONE is not set CONFIG_LWIP_HOOK_TCP_ISN_DEFAULT=y # CONFIG_LWIP_HOOK_TCP_ISN_CUSTOM is not set CONFIG_LWIP_HOOK_IP6_ROUTE_NONE=y # CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT is not set # CONFIG_LWIP_HOOK_IP6_ROUTE_CUSTOM is not set CONFIG_LWIP_HOOK_ND6_GET_GW_NONE=y # CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT is not set # CONFIG_LWIP_HOOK_ND6_GET_GW_CUSTOM is not set CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_NONE=y # CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_DEFAULT is not set # CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_CUSTOM is not set # end of Hooks # CONFIG_LWIP_DEBUG is not set # end of LWIP # # mbedTLS # CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y # CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set # CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 # CONFIG_MBEDTLS_DYNAMIC_BUFFER is not set # CONFIG_MBEDTLS_DEBUG is not set # # mbedTLS v2.28.x related # # CONFIG_MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH is not set # CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set # CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=y # end of mbedTLS v2.28.x related # # Certificate Bundle # CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y # CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN is not set # CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE is not set # CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE is not set # end of Certificate Bundle # CONFIG_MBEDTLS_ECP_RESTARTABLE is not set # CONFIG_MBEDTLS_CMAC_C is not set CONFIG_MBEDTLS_HARDWARE_AES=y CONFIG_MBEDTLS_HARDWARE_MPI=y CONFIG_MBEDTLS_HARDWARE_SHA=y CONFIG_MBEDTLS_ROM_MD5=y # CONFIG_MBEDTLS_ATCA_HW_ECDSA_SIGN is not set # CONFIG_MBEDTLS_ATCA_HW_ECDSA_VERIFY is not set CONFIG_MBEDTLS_HAVE_TIME=y # CONFIG_MBEDTLS_HAVE_TIME_DATE is not set CONFIG_MBEDTLS_ECDSA_DETERMINISTIC=y CONFIG_MBEDTLS_SHA512_C=y CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT=y # CONFIG_MBEDTLS_TLS_SERVER_ONLY is not set # CONFIG_MBEDTLS_TLS_CLIENT_ONLY is not set # CONFIG_MBEDTLS_TLS_DISABLED is not set CONFIG_MBEDTLS_TLS_SERVER=y CONFIG_MBEDTLS_TLS_CLIENT=y CONFIG_MBEDTLS_TLS_ENABLED=y # # TLS Key Exchange Methods # # CONFIG_MBEDTLS_PSK_MODES is not set CONFIG_MBEDTLS_KEY_EXCHANGE_RSA=y CONFIG_MBEDTLS_KEY_EXCHANGE_DHE_RSA=y CONFIG_MBEDTLS_KEY_EXCHANGE_ELLIPTIC_CURVE=y CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA=y CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA=y CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA=y CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA=y # end of TLS Key Exchange Methods CONFIG_MBEDTLS_SSL_RENEGOTIATION=y # CONFIG_MBEDTLS_SSL_PROTO_SSL3 is not set CONFIG_MBEDTLS_SSL_PROTO_TLS1=y CONFIG_MBEDTLS_SSL_PROTO_TLS1_1=y CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y # CONFIG_MBEDTLS_SSL_PROTO_GMTSSL1_1 is not set # CONFIG_MBEDTLS_SSL_PROTO_DTLS is not set CONFIG_MBEDTLS_SSL_ALPN=y CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS=y CONFIG_MBEDTLS_X509_CHECK_KEY_USAGE=y CONFIG_MBEDTLS_X509_CHECK_EXTENDED_KEY_USAGE=y CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS=y # # Symmetric Ciphers # CONFIG_MBEDTLS_AES_C=y # CONFIG_MBEDTLS_CAMELLIA_C is not set # CONFIG_MBEDTLS_DES_C is not set CONFIG_MBEDTLS_RC4_DISABLED=y # CONFIG_MBEDTLS_RC4_ENABLED_NO_DEFAULT is not set # CONFIG_MBEDTLS_RC4_ENABLED is not set # CONFIG_MBEDTLS_BLOWFISH_C is not set # CONFIG_MBEDTLS_XTEA_C is not set CONFIG_MBEDTLS_CCM_C=y CONFIG_MBEDTLS_GCM_C=y # CONFIG_MBEDTLS_NIST_KW_C is not set # end of Symmetric Ciphers # CONFIG_MBEDTLS_RIPEMD160_C is not set # # Certificates # CONFIG_MBEDTLS_PEM_PARSE_C=y CONFIG_MBEDTLS_PEM_WRITE_C=y CONFIG_MBEDTLS_X509_CRL_PARSE_C=y CONFIG_MBEDTLS_X509_CSR_PARSE_C=y # end of Certificates CONFIG_MBEDTLS_ECP_C=y CONFIG_MBEDTLS_ECDH_C=y CONFIG_MBEDTLS_ECDSA_C=y # CONFIG_MBEDTLS_ECJPAKE_C is not set CONFIG_MBEDTLS_ECP_DP_SECP192R1_ENABLED=y CONFIG_MBEDTLS_ECP_DP_SECP224R1_ENABLED=y CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=y CONFIG_MBEDTLS_ECP_DP_SECP521R1_ENABLED=y CONFIG_MBEDTLS_ECP_DP_SECP192K1_ENABLED=y CONFIG_MBEDTLS_ECP_DP_SECP224K1_ENABLED=y CONFIG_MBEDTLS_ECP_DP_SECP256K1_ENABLED=y CONFIG_MBEDTLS_ECP_DP_BP256R1_ENABLED=y CONFIG_MBEDTLS_ECP_DP_BP384R1_ENABLED=y CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=y CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y CONFIG_MBEDTLS_ECP_NIST_OPTIM=y # CONFIG_MBEDTLS_POLY1305_C is not set # CONFIG_MBEDTLS_CHACHA20_C is not set # CONFIG_MBEDTLS_HKDF_C is not set # CONFIG_MBEDTLS_THREADING_C is not set # CONFIG_MBEDTLS_LARGE_KEY_SOFTWARE_MPI is not set # CONFIG_MBEDTLS_SECURITY_RISKS is not set # end of mbedTLS # # mDNS # CONFIG_MDNS_MAX_SERVICES=10 CONFIG_MDNS_TASK_PRIORITY=1 CONFIG_MDNS_TASK_STACK_SIZE=4096 # CONFIG_MDNS_TASK_AFFINITY_NO_AFFINITY is not set CONFIG_MDNS_TASK_AFFINITY_CPU0=y # CONFIG_MDNS_TASK_AFFINITY_CPU1 is not set CONFIG_MDNS_TASK_AFFINITY=0x0 CONFIG_MDNS_SERVICE_ADD_TIMEOUT_MS=2000 # CONFIG_MDNS_STRICT_MODE is not set CONFIG_MDNS_TIMER_PERIOD_MS=100 # CONFIG_MDNS_NETWORKING_SOCKET is not set CONFIG_MDNS_MULTIPLE_INSTANCE=y # end of mDNS # # ESP-MQTT Configurations # CONFIG_MQTT_PROTOCOL_311=y CONFIG_MQTT_TRANSPORT_SSL=y CONFIG_MQTT_TRANSPORT_WEBSOCKET=y CONFIG_MQTT_TRANSPORT_WEBSOCKET_SECURE=y # CONFIG_MQTT_MSG_ID_INCREMENTAL is not set # CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED is not set # CONFIG_MQTT_REPORT_DELETED_MESSAGES is not set # CONFIG_MQTT_USE_CUSTOM_CONFIG is not set # CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED is not set # CONFIG_MQTT_CUSTOM_OUTBOX is not set # end of ESP-MQTT Configurations # # Newlib # CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y # CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set # CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set # CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set # CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y # CONFIG_NEWLIB_NANO_FORMAT is not set # end of Newlib # # NVS # # end of NVS # # OpenSSL # # CONFIG_OPENSSL_DEBUG is not set CONFIG_OPENSSL_ERROR_STACK=y # CONFIG_OPENSSL_ASSERT_DO_NOTHING is not set CONFIG_OPENSSL_ASSERT_EXIT=y # end of OpenSSL # # OpenThread # # CONFIG_OPENTHREAD_ENABLED is not set # end of OpenThread # # PThreads # CONFIG_PTHREAD_TASK_PRIO_DEFAULT=5 CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 CONFIG_PTHREAD_STACK_MIN=768 CONFIG_PTHREAD_DEFAULT_CORE_NO_AFFINITY=y # CONFIG_PTHREAD_DEFAULT_CORE_0 is not set # CONFIG_PTHREAD_DEFAULT_CORE_1 is not set CONFIG_PTHREAD_TASK_CORE_DEFAULT=-1 CONFIG_PTHREAD_TASK_NAME_DEFAULT="pthread" # end of PThreads # # SPI Flash driver # # CONFIG_SPI_FLASH_VERIFY_WRITE is not set # CONFIG_SPI_FLASH_ENABLE_COUNTERS is not set CONFIG_SPI_FLASH_ROM_DRIVER_PATCH=y CONFIG_SPI_FLASH_DANGEROUS_WRITE_ABORTS=y # CONFIG_SPI_FLASH_DANGEROUS_WRITE_FAILS is not set # CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED is not set # CONFIG_SPI_FLASH_USE_LEGACY_IMPL is not set # CONFIG_SPI_FLASH_SHARE_SPI1_BUS is not set # CONFIG_SPI_FLASH_BYPASS_BLOCK_ERASE is not set CONFIG_SPI_FLASH_YIELD_DURING_ERASE=y CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS=20 CONFIG_SPI_FLASH_ERASE_YIELD_TICKS=1 CONFIG_SPI_FLASH_WRITE_CHUNK_SIZE=8192 # CONFIG_SPI_FLASH_SIZE_OVERRIDE is not set # CONFIG_SPI_FLASH_CHECK_ERASE_TIMEOUT_DISABLED is not set # CONFIG_SPI_FLASH_OVERRIDE_CHIP_DRIVER_LIST is not set # # Auto-detect flash chips # CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP=y CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP=y CONFIG_SPI_FLASH_SUPPORT_GD_CHIP=y CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP=y # CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP is not set # CONFIG_SPI_FLASH_SUPPORT_TH_CHIP is not set # end of Auto-detect flash chips CONFIG_SPI_FLASH_ENABLE_ENCRYPTED_READ_WRITE=y # end of SPI Flash driver # # SPIFFS Configuration # CONFIG_SPIFFS_MAX_PARTITIONS=3 # # SPIFFS Cache Configuration # CONFIG_SPIFFS_CACHE=y CONFIG_SPIFFS_CACHE_WR=y # CONFIG_SPIFFS_CACHE_STATS is not set # end of SPIFFS Cache Configuration CONFIG_SPIFFS_PAGE_CHECK=y CONFIG_SPIFFS_GC_MAX_RUNS=10 # CONFIG_SPIFFS_GC_STATS is not set CONFIG_SPIFFS_PAGE_SIZE=256 CONFIG_SPIFFS_OBJ_NAME_LEN=32 # CONFIG_SPIFFS_FOLLOW_SYMLINKS is not set CONFIG_SPIFFS_USE_MAGIC=y CONFIG_SPIFFS_USE_MAGIC_LENGTH=y CONFIG_SPIFFS_META_LENGTH=4 CONFIG_SPIFFS_USE_MTIME=y # # Debug Configuration # # CONFIG_SPIFFS_DBG is not set # CONFIG_SPIFFS_API_DBG is not set # CONFIG_SPIFFS_GC_DBG is not set # CONFIG_SPIFFS_CACHE_DBG is not set # CONFIG_SPIFFS_CHECK_DBG is not set # CONFIG_SPIFFS_TEST_VISUALISATION is not set # end of Debug Configuration # end of SPIFFS Configuration # # TCP Transport # # # Websocket # CONFIG_WS_TRANSPORT=y CONFIG_WS_BUFFER_SIZE=1024 # end of Websocket # end of TCP Transport # # Unity unit testing library # CONFIG_UNITY_ENABLE_FLOAT=y CONFIG_UNITY_ENABLE_DOUBLE=y # CONFIG_UNITY_ENABLE_64BIT is not set # CONFIG_UNITY_ENABLE_COLOR is not set CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y # CONFIG_UNITY_ENABLE_FIXTURE is not set # CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL is not set # end of Unity unit testing library # # Virtual file system # CONFIG_VFS_SUPPORT_IO=y CONFIG_VFS_SUPPORT_DIR=y CONFIG_VFS_SUPPORT_SELECT=y CONFIG_VFS_SUPPRESS_SELECT_DEBUG_OUTPUT=y CONFIG_VFS_SUPPORT_TERMIOS=y # # Host File System I/O (Semihosting) # CONFIG_VFS_SEMIHOSTFS_MAX_MOUNT_POINTS=1 CONFIG_VFS_SEMIHOSTFS_HOST_PATH_MAX_LEN=128 # end of Host File System I/O (Semihosting) # end of Virtual file system # # Wear Levelling # # CONFIG_WL_SECTOR_SIZE_512 is not set CONFIG_WL_SECTOR_SIZE_4096=y CONFIG_WL_SECTOR_SIZE=4096 # end of Wear Levelling # # Wi-Fi Provisioning Manager # CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16 CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30 # end of Wi-Fi Provisioning Manager # # Supplicant # CONFIG_WPA_MBEDTLS_CRYPTO=y # CONFIG_WPA_WAPI_PSK is not set # CONFIG_WPA_SUITE_B_192 is not set # CONFIG_WPA_DEBUG_PRINT is not set # CONFIG_WPA_TESTING_OPTIONS is not set # CONFIG_WPA_WPS_STRICT is not set # CONFIG_WPA_11KV_SUPPORT is not set # CONFIG_WPA_MBO_SUPPORT is not set # CONFIG_WPA_DPP_SUPPORT is not set # end of Supplicant # end of Component config # # Compatibility options # # CONFIG_LEGACY_INCLUDE_COMMON_HEADERS is not set # end of Compatibility options # Deprecated options for backward compatibility CONFIG_TOOLPREFIX="xtensa-esp32-elf-" # CONFIG_LOG_BOOTLOADER_LEVEL_NONE is not set # CONFIG_LOG_BOOTLOADER_LEVEL_ERROR is not set # CONFIG_LOG_BOOTLOADER_LEVEL_WARN is not set CONFIG_LOG_BOOTLOADER_LEVEL_INFO=y # CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG is not set # CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set CONFIG_LOG_BOOTLOADER_LEVEL=3 # CONFIG_APP_ROLLBACK_ENABLE is not set # CONFIG_FLASH_ENCRYPTION_ENABLED is not set # CONFIG_FLASHMODE_QIO is not set # CONFIG_FLASHMODE_QOUT is not set CONFIG_FLASHMODE_DIO=y # CONFIG_FLASHMODE_DOUT is not set # CONFIG_MONITOR_BAUD_9600B is not set # CONFIG_MONITOR_BAUD_57600B is not set CONFIG_MONITOR_BAUD_115200B=y # CONFIG_MONITOR_BAUD_230400B is not set # CONFIG_MONITOR_BAUD_921600B is not set # CONFIG_MONITOR_BAUD_2MB is not set # CONFIG_MONITOR_BAUD_OTHER is not set CONFIG_MONITOR_BAUD_OTHER_VAL=115200 CONFIG_MONITOR_BAUD=115200 CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG=y # CONFIG_COMPILER_OPTIMIZATION_LEVEL_RELEASE is not set CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED=y # CONFIG_OPTIMIZATION_ASSERTIONS_SILENT is not set # CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED is not set CONFIG_OPTIMIZATION_ASSERTION_LEVEL=2 # CONFIG_CXX_EXCEPTIONS is not set CONFIG_STACK_CHECK_NONE=y # CONFIG_STACK_CHECK_NORM is not set # CONFIG_STACK_CHECK_STRONG is not set # CONFIG_STACK_CHECK_ALL is not set # CONFIG_WARN_WRITE_STRINGS is not set # CONFIG_DISABLE_GCC8_WARNINGS is not set # CONFIG_ESP32_APPTRACE_DEST_TRAX is not set CONFIG_ESP32_APPTRACE_DEST_NONE=y CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y CONFIG_ADC2_DISABLE_DAC=y # CONFIG_SPIRAM_SUPPORT is not set CONFIG_TRACEMEM_RESERVE_DRAM=0x0 # CONFIG_ULP_COPROC_ENABLED is not set CONFIG_ULP_COPROC_RESERVE_MEM=0 CONFIG_BROWNOUT_DET=y CONFIG_BROWNOUT_DET_LVL_SEL_0=y # CONFIG_BROWNOUT_DET_LVL_SEL_1 is not set # CONFIG_BROWNOUT_DET_LVL_SEL_2 is not set # CONFIG_BROWNOUT_DET_LVL_SEL_3 is not set # CONFIG_BROWNOUT_DET_LVL_SEL_4 is not set # CONFIG_BROWNOUT_DET_LVL_SEL_5 is not set # CONFIG_BROWNOUT_DET_LVL_SEL_6 is not set # CONFIG_BROWNOUT_DET_LVL_SEL_7 is not set CONFIG_BROWNOUT_DET_LVL=0 CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_RC=y # CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_CRYSTAL is not set # CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_OSC is not set # CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_8MD256 is not set # CONFIG_DISABLE_BASIC_ROM_CONSOLE is not set # CONFIG_NO_BLOBS is not set # CONFIG_COMPATIBLE_PRE_V2_1_BOOTLOADERS is not set # CONFIG_EVENT_LOOP_PROFILING is not set CONFIG_POST_EVENTS_FROM_ISR=y CONFIG_POST_EVENTS_FROM_IRAM_ISR=y # CONFIG_TWO_UNIVERSAL_MAC_ADDRESS is not set CONFIG_FOUR_UNIVERSAL_MAC_ADDRESS=y CONFIG_NUMBER_OF_UNIVERSAL_MAC_ADDRESS=4 CONFIG_ESP_SYSTEM_PD_FLASH=y # CONFIG_ESP32C3_LIGHTSLEEP_GPIO_RESET_WORKAROUND is not set CONFIG_IPC_TASK_STACK_SIZE=1536 CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y # CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION is not set CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20 CONFIG_ESP32_PHY_MAX_TX_POWER=20 CONFIG_ESP32_REDUCE_PHY_TX_POWER=y # CONFIG_ESP32S2_PANIC_PRINT_HALT is not set CONFIG_ESP32S2_PANIC_PRINT_REBOOT=y # CONFIG_ESP32S2_PANIC_SILENT_REBOOT is not set # CONFIG_ESP32S2_PANIC_GDBSTUB is not set CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32 CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304 CONFIG_MAIN_TASK_STACK_SIZE=8192 CONFIG_CONSOLE_UART_DEFAULT=y # CONFIG_CONSOLE_UART_CUSTOM is not set # CONFIG_ESP_CONSOLE_UART_NONE is not set CONFIG_CONSOLE_UART=y CONFIG_CONSOLE_UART_NUM=0 CONFIG_CONSOLE_UART_BAUDRATE=115200 CONFIG_INT_WDT=y CONFIG_INT_WDT_TIMEOUT_MS=300 CONFIG_INT_WDT_CHECK_CPU1=y CONFIG_TASK_WDT=y # CONFIG_TASK_WDT_PANIC is not set CONFIG_TASK_WDT_TIMEOUT_S=5 CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU1=y # CONFIG_ESP32_DEBUG_STUBS_ENABLE is not set CONFIG_TIMER_TASK_STACK_SIZE=3584 # CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH is not set # CONFIG_ESP32_ENABLE_COREDUMP_TO_UART is not set CONFIG_ESP32_ENABLE_COREDUMP_TO_NONE=y CONFIG_MB_MASTER_TIMEOUT_MS_RESPOND=150 CONFIG_MB_MASTER_DELAY_MS_CONVERT=200 CONFIG_MB_QUEUE_LENGTH=20 CONFIG_MB_SERIAL_TASK_STACK_SIZE=4096 CONFIG_MB_SERIAL_BUF_SIZE=256 CONFIG_MB_SERIAL_TASK_PRIO=10 CONFIG_MB_CONTROLLER_SLAVE_ID_SUPPORT=y CONFIG_MB_CONTROLLER_SLAVE_ID=0x00112233 CONFIG_MB_CONTROLLER_NOTIFY_TIMEOUT=20 CONFIG_MB_CONTROLLER_NOTIFY_QUEUE_SIZE=20 CONFIG_MB_CONTROLLER_STACK_SIZE=4096 CONFIG_MB_EVENT_QUEUE_TIMEOUT=20 # CONFIG_MB_TIMER_PORT_ENABLED is not set CONFIG_MB_TIMER_GROUP=0 CONFIG_MB_TIMER_INDEX=0 # CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set CONFIG_TIMER_TASK_PRIORITY=1 CONFIG_TIMER_TASK_STACK_DEPTH=2048 CONFIG_TIMER_QUEUE_LENGTH=10 # CONFIG_L2_TO_L3_COPY is not set # CONFIG_USE_ONLY_LWIP_SELECT is not set CONFIG_ESP_GRATUITOUS_ARP=y CONFIG_GARP_TMR_INTERVAL=60 CONFIG_TCPIP_RECVMBOX_SIZE=32 CONFIG_TCP_MAXRTX=12 CONFIG_TCP_SYNMAXRTX=12 CONFIG_TCP_MSS=1440 CONFIG_TCP_MSL=60000 CONFIG_TCP_SND_BUF_DEFAULT=5744 CONFIG_TCP_WND_DEFAULT=5744 CONFIG_TCP_RECVMBOX_SIZE=6 CONFIG_TCP_QUEUE_OOSEQ=y # CONFIG_ESP_TCP_KEEP_CONNECTION_WHEN_IP_CHANGES is not set CONFIG_TCP_OVERSIZE_MSS=y # CONFIG_TCP_OVERSIZE_QUARTER_MSS is not set # CONFIG_TCP_OVERSIZE_DISABLE is not set CONFIG_UDP_RECVMBOX_SIZE=6 CONFIG_TCPIP_TASK_STACK_SIZE=3072 CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y # CONFIG_TCPIP_TASK_AFFINITY_CPU0 is not set # CONFIG_TCPIP_TASK_AFFINITY_CPU1 is not set CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF # CONFIG_PPP_SUPPORT is not set CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT=5 CONFIG_ESP32_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 CONFIG_ESP32_PTHREAD_STACK_MIN=768 CONFIG_ESP32_DEFAULT_PTHREAD_CORE_NO_AFFINITY=y # CONFIG_ESP32_DEFAULT_PTHREAD_CORE_0 is not set # CONFIG_ESP32_DEFAULT_PTHREAD_CORE_1 is not set CONFIG_ESP32_PTHREAD_TASK_CORE_DEFAULT=-1 CONFIG_ESP32_PTHREAD_TASK_NAME_DEFAULT="pthread" CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y # CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS is not set # CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED is not set CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y CONFIG_SUPPORT_TERMIOS=y CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1 CONFIG_SEMIHOSTFS_HOST_PATH_MAX_LEN=128 # End of deprecated options ================================================ FILE: examples/ESP-TLS/main.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if defined(ESP8266) #include #include ESP8266WiFiMulti WiFiMulti; #elif defined(ESP32) #include #else #error only ESP32 or ESP8266 supported at the moment #endif #include #define STASSID "YOUR_WIFI_SSID" #define STAPSK "YOUR_WIFI_PW" #define OCPP_BACKEND_URL "wss://echo.websocket.events" #define OCPP_CHARGE_BOX_ID "" #define OCPP_AUTH_KEY "SecureAuthKey" // OCPP Security Profile 2: TLS with Basic Authentication /* * ISRG ROOT X1 */ const char ca_cert[] PROGMEM = R"EOF(-----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= -----END CERTIFICATE----- )EOF"; void setup() { /* * Initialize Serial and WiFi */ Serial.begin(115200); Serial.print(F("[main] Wait for WiFi: ")); #if defined(ESP8266) WiFiMulti.addAP(STASSID, STAPSK); while (WiFiMulti.run() != WL_CONNECTED) { Serial.print('.'); delay(1000); } #elif defined(ESP32) WiFi.begin(STASSID, STAPSK); while (!WiFi.isConnected()) { Serial.print('.'); delay(1000); } #else #error only ESP32 or ESP8266 supported at the moment #endif Serial.println(F(" connected!")); /* * Set system time (required for Certificate validation) */ configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov"); //alternatively: settimeofday(&de, &tz); Serial.print(F("[main] Wait for NTP time (used for certificate validation) ")); time_t now = time(nullptr); while (now < 8 * 3600 * 2) { delay(1000); Serial.print('.'); now = time(nullptr); } Serial.printf(" finished. Unix timestamp is %lu\n", now); /* * Initialize the OCPP library (using OCPP Security Profile 2: TLS with Basic Authentication) */ mocpp_initialize( OCPP_BACKEND_URL, OCPP_CHARGE_BOX_ID, "My Charging Station", "My company name", MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail, OCPP_AUTH_KEY, ca_cert); /* * ... see MicroOcpp.h for how to integrate the EVSE hardware. * * This example only showcases the TLS connection. For examples about the HW integration, * see the other examples */ } void loop() { /* * Execute all charge point routines and handle WebSocket */ mocpp_loop(); //... see MicroOcpp.h and the other examples for how to integrate the EVSE hardware. } ================================================ FILE: library.json ================================================ { "name": "MicroOcpp", "version": "1.2.0", "description": "OCPP 1.6 / 2.0.1 Client for microcontrollers", "keywords": "OCPP, 1.6, OCPP 1.6, OCPP 2.0.1, Smart Energy, Smart Charging, client, ESP8266, ESP32, Arduino, esp-idf, EVSE, Charge Point", "repository": { "type": "git", "url": "https://github.com/matth-x/MicroOcpp/" }, "authors": [ { "name": "Matthias Akstaller", "url": "https://www.micro-ocpp.com", "maintainer": true } ], "license": "MIT", "homepage": "https://www.micro-ocpp.com", "dependencies": [ { "owner": "bblanchon", "name": "ArduinoJson", "version": "6.20.1" }, { "owner": "links2004", "name": "WebSockets", "version": "2.4.1" } ], "frameworks": "arduino,espidf", "platforms": "espressif8266, espressif32", "export": { "include": [ "docs/*", "examples/*", "src/*", "CHANGELOG.md", "CMakeLists.txt", "library.json", "library.properties", "LICENSE", "mkdocs.yml", "platformio.ini", "README.md" ] }, "examples": [ { "name": "Basic OCPP connection", "base": "examples/ESP", "files": [ "main.cpp" ] }, { "name": "OCPP Security Profile 2", "base": "examples/ESP-TLS", "files": [ "main.cpp" ] }, { "name": "ESP-IDF integration", "base": "examples/ESP-IDF", "files": [ "main/main.c" ] } ] } ================================================ FILE: library.properties ================================================ name=MicroOcpp version=1.2.0 author=Matthias Akstaller maintainer=Matthias Akstaller sentence=OCPP 1.6 Client for microcontrollers paragraph= category=Communication url=https://github.com/matth-x/MicroOcpp/ architectures=* depends=ArduinoJson, WebSockets ================================================ FILE: mkdocs.yml ================================================ site_name: MicroOCPP docs theme: name: material features: - announce.dismiss - content.action.edit - content.action.view - content.code.annotate - content.code.copy # - content.tabs.link - content.tooltips # - header.autohide # - navigation.expand - navigation.footer - navigation.indexes # - navigation.instant # - navigation.prune - navigation.sections - navigation.tabs # - navigation.tabs.sticky - navigation.top - navigation.tracking - search.highlight - search.share - search.suggest - toc.follow # - toc.integrate palette: - scheme: default primary: custom accent: custom toggle: icon: material/brightness-7 name: Switch to dark mode - scheme: slate primary: custom accent: custom toggle: icon: material/brightness-4 name: Switch to light mode font: text: Roboto code: Roboto Mono favicon: img/favicon.ico icon: logo: logo extra_css: - stylesheets/extra.css plugins: - search - table-reader: data_path: "docs/assets/tables" ================================================ FILE: platformio.ini ================================================ ; matth-x/MicroOcpp ; Copyright Matthias Akstaller 2019 - 2024 ; MIT License [platformio] default_envs = esp32-development-board [common] framework = arduino lib_deps = bblanchon/ArduinoJson@6.20.1 links2004/WebSockets@2.4.1 monitor_speed = 115200 [env:nodemcuv2] platform = espressif8266@2.6.3 board = nodemcuv2 framework = ${common.framework} lib_deps = ${common.lib_deps} monitor_speed = ${common.monitor_speed} build_flags = -D MO_DBG_LEVEL=MO_DL_INFO ; flood the serial monitor with information about the internal state -DMO_TRAFFIC_OUT ; print the OCPP communication to the serial monitor -D ARDUINOJSON_ENABLE_STD_STRING=1 [env:esp32-development-board] platform = espressif32@6.0.1 board = esp-wrover-kit framework = ${common.framework} lib_deps = ${common.lib_deps} monitor_speed = ${common.monitor_speed} build_flags = -D MO_DBG_LEVEL=MO_DL_INFO ; flood the serial monitor with information about the internal state -DMO_TRAFFIC_OUT ; print the OCPP communication to the serial monitor board_build.partitions = min_spiffs.csv upload_speed = 921600 monitor_filters = esp32_exception_decoder ================================================ FILE: src/MicroOcpp/Core/Configuration.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include namespace MicroOcpp { struct Validator { const char *key = nullptr; std::function checkValue; Validator(const char *key, std::function checkValue) : key(key), checkValue(checkValue) { } }; namespace ConfigurationLocal { std::shared_ptr filesystem; auto configurationContainers = makeVector>("v16.Configuration.Containers"); auto validators = makeVector("v16.Configuration.Validators"); } using namespace ConfigurationLocal; std::unique_ptr createConfigurationContainer(const char *filename, bool accessible) { //create non-persistent Configuration store (i.e. lives only in RAM) if // - Flash FS usage is switched off OR // - Filename starts with "/volatile" if (!filesystem || !strncmp(filename, CONFIGURATION_VOLATILE, strlen(CONFIGURATION_VOLATILE))) { return makeConfigurationContainerVolatile(filename, accessible); } else { //create persistent Configuration store. This is the normal case return makeConfigurationContainerFlash(filesystem, filename, accessible); } } void addConfigurationContainer(std::shared_ptr container) { configurationContainers.push_back(container); } std::shared_ptr getContainer(const char *filename) { auto container = std::find_if(configurationContainers.begin(), configurationContainers.end(), [filename](decltype(configurationContainers)::value_type &elem) { return !strcmp(elem->getFilename(), filename); }); if (container != configurationContainers.end()) { return *container; } else { return nullptr; } } ConfigurationContainer *declareContainer(const char *filename, bool accessible) { auto container = getContainer(filename); if (!container) { MO_DBG_DEBUG("init new configurations container: %s", filename); container = createConfigurationContainer(filename, accessible); if (!container) { MO_DBG_ERR("OOM"); return nullptr; } configurationContainers.push_back(container); } if (container->isAccessible() != accessible) { MO_DBG_ERR("%s: conflicting accessibility declarations (expect %s)", filename, container->isAccessible() ? "accessible" : "inaccessible"); } return container.get(); } std::shared_ptr loadConfiguration(TConfig type, const char *key, bool accessible) { for (auto& container : configurationContainers) { if (auto config = container->getConfiguration(key)) { if (config->getType() != type) { MO_DBG_ERR("conflicting type for %s - remove old config", key); container->remove(config.get()); continue; } if (container->isAccessible() != accessible) { MO_DBG_ERR("conflicting accessibility for %s", key); } container->loadStaticKey(*config.get(), key); return config; } } return nullptr; } template bool loadFactoryDefault(Configuration& config, T loadFactoryDefault); template<> bool loadFactoryDefault(Configuration& config, int factoryDef) { config.setInt(factoryDef); return true; } template<> bool loadFactoryDefault(Configuration& config, bool factoryDef) { config.setBool(factoryDef); return true; } template<> bool loadFactoryDefault(Configuration& config, const char *factoryDef) { return config.setString(factoryDef); } void loadPermissions(Configuration& config, bool readonly, bool rebootRequired) { if (readonly) { config.setReadOnly(); } if (rebootRequired) { config.setRebootRequired(); } } template std::shared_ptr declareConfiguration(const char *key, T factoryDef, const char *filename, bool readonly, bool rebootRequired, bool accessible) { std::shared_ptr res = loadConfiguration(convertType(), key, accessible); if (!res) { auto container = declareContainer(filename, accessible); if (!container) { return nullptr; } res = container->createConfiguration(convertType(), key); if (!res) { return nullptr; } if (!loadFactoryDefault(*res.get(), factoryDef)) { container->remove(res.get()); return nullptr; } } loadPermissions(*res.get(), readonly, rebootRequired); return res; } template std::shared_ptr declareConfiguration(const char *key, int factoryDef, const char *filename, bool readonly, bool rebootRequired, bool accessible); template std::shared_ptr declareConfiguration(const char *key, bool factoryDef, const char *filename, bool readonly, bool rebootRequired, bool accessible); template std::shared_ptr declareConfiguration(const char *key, const char *factoryDef, const char *filename, bool readonly, bool rebootRequired, bool accessible); std::function *getConfigurationValidator(const char *key) { for (auto& v : validators) { if (!strcmp(v.key, key)) { return &v.checkValue; } } return nullptr; } void registerConfigurationValidator(const char *key, std::function validator) { for (auto& v : validators) { if (!strcmp(v.key, key)) { v.checkValue = validator; return; } } validators.push_back(Validator{key, validator}); } Configuration *getConfigurationPublic(const char *key) { for (auto& container : configurationContainers) { if (container->isAccessible()) { if (auto res = container->getConfiguration(key)) { return res.get(); } } } return nullptr; } Vector getConfigurationContainersPublic() { auto res = makeVector("v16.Configuration.Containers"); for (auto& container : configurationContainers) { if (container->isAccessible()) { res.push_back(container.get()); } } return res; } bool configuration_init(std::shared_ptr _filesystem) { filesystem = _filesystem; return true; } void configuration_deinit() { makeVector("v16.Configuration.Containers").swap(configurationContainers); //release allocated memory (see https://cplusplus.com/reference/vector/vector/clear/) makeVector("v16.Configuration.Validators").swap(validators); filesystem.reset(); } bool configuration_load(const char *filename) { bool success = true; for (auto& container : configurationContainers) { if ((!filename || !strcmp(filename, container->getFilename())) && !container->load()) { success = false; } } return success; } bool configuration_save() { bool success = true; for (auto& container : configurationContainers) { if (!container->save()) { success = false; } } return success; } bool configuration_clean_unused() { for (auto& container : configurationContainers) { container->removeUnused(); } return configuration_save(); } bool VALIDATE_UNSIGNED_INT(const char *value) { for(size_t i = 0; value[i] != '\0'; i++) { if (value[i] < '0' || value[i] > '9') { return false; } } return true; } } //end namespace MicroOcpp ================================================ FILE: src/MicroOcpp/Core/Configuration.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CONFIGURATION_H #define MO_CONFIGURATION_H #include #include #include #include #include #define CONFIGURATION_FN (MO_FILENAME_PREFIX "ocpp-config.jsn") #define CONFIGURATION_VOLATILE "/volatile" #define MO_KEYVALUE_FN (MO_FILENAME_PREFIX "client-state.jsn") namespace MicroOcpp { template std::shared_ptr declareConfiguration(const char *key, T factoryDefault, const char *filename = CONFIGURATION_FN, bool readonly = false, bool rebootRequired = false, bool accessible = true); std::function *getConfigurationValidator(const char *key); void registerConfigurationValidator(const char *key, std::function validator); void addConfigurationContainer(std::shared_ptr container); Configuration *getConfigurationPublic(const char *key); Vector getConfigurationContainersPublic(); bool configuration_init(std::shared_ptr filesytem); void configuration_deinit(); bool configuration_load(const char *filename = nullptr); bool configuration_save(); bool configuration_clean_unused(); //remove configs which haven't been accessed //default implementation for common validator bool VALIDATE_UNSIGNED_INT(const char*); } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Core/ConfigurationContainer.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include using namespace MicroOcpp; ConfigurationContainer::~ConfigurationContainer() { } ConfigurationContainerVolatile::ConfigurationContainerVolatile(const char *filename, bool accessible) : ConfigurationContainer(filename, accessible), MemoryManaged("v16.Configuration.ContainerVoltaile.", filename), configurations(makeVector>(getMemoryTag())) { } bool ConfigurationContainerVolatile::load() { return true; } bool ConfigurationContainerVolatile::save() { return true; } std::shared_ptr ConfigurationContainerVolatile::createConfiguration(TConfig type, const char *key) { auto res = std::shared_ptr(makeConfiguration(type, key).release(), std::default_delete(), makeAllocator("v16.Configuration.", key)); if (!res) { //allocation failure - OOM MO_DBG_ERR("OOM"); return nullptr; } configurations.push_back(res); return res; } void ConfigurationContainerVolatile::remove(Configuration *config) { for (auto entry = configurations.begin(); entry != configurations.end();) { if (entry->get() == config) { entry = configurations.erase(entry); } else { entry++; } } } size_t ConfigurationContainerVolatile::size() { return configurations.size(); } Configuration *ConfigurationContainerVolatile::getConfiguration(size_t i) { return configurations[i].get(); } std::shared_ptr ConfigurationContainerVolatile::getConfiguration(const char *key) { for (auto& entry : configurations) { if (entry->getKey() && !strcmp(entry->getKey(), key)) { return entry; } } return nullptr; } void ConfigurationContainerVolatile::add(std::shared_ptr c) { configurations.push_back(std::move(c)); } namespace MicroOcpp { std::unique_ptr makeConfigurationContainerVolatile(const char *filename, bool accessible) { return std::unique_ptr(new ConfigurationContainerVolatile(filename, accessible)); } } //end namespace MicroOcpp ================================================ FILE: src/MicroOcpp/Core/ConfigurationContainer.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CONFIGURATIONCONTAINER_H #define MO_CONFIGURATIONCONTAINER_H #include #include #include namespace MicroOcpp { class ConfigurationContainer { private: const char *filename; bool accessible; public: ConfigurationContainer(const char *filename, bool accessible) : filename(filename), accessible(accessible) { } virtual ~ConfigurationContainer(); const char *getFilename() {return filename;} bool isAccessible() {return accessible;} virtual bool load() = 0; //called at the end of mocpp_intialize, to load the configurations with the stored value virtual bool save() = 0; virtual std::shared_ptr createConfiguration(TConfig type, const char *key) = 0; virtual void remove(Configuration *config) = 0; virtual size_t size() = 0; virtual Configuration *getConfiguration(size_t i) = 0; virtual std::shared_ptr getConfiguration(const char *key) = 0; virtual void loadStaticKey(Configuration& config, const char *key) { } //possible optimization: can replace internal key with passed static key virtual void removeUnused() { } //remove configs which haven't been accessed (optional and only if known) }; class ConfigurationContainerVolatile : public ConfigurationContainer, public MemoryManaged { private: Vector> configurations; public: ConfigurationContainerVolatile(const char *filename, bool accessible); //ConfigurationContainer definitions bool load() override; bool save() override; std::shared_ptr createConfiguration(TConfig type, const char *key) override; void remove(Configuration *config) override; size_t size() override; Configuration *getConfiguration(size_t i) override; std::shared_ptr getConfiguration(const char *key) override; //add custom Configuration object void add(std::shared_ptr c); }; std::unique_ptr makeConfigurationContainerVolatile(const char *filename, bool accessible); } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Core/ConfigurationContainerFlash.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #define MAX_CONFIGURATIONS 50 namespace MicroOcpp { class ConfigurationContainerFlash : public ConfigurationContainer, public MemoryManaged { private: Vector> configurations; std::shared_ptr filesystem; uint16_t revisionSum = 0; bool loaded = false; Vector keyPool; void clearKeyPool(const char *key) { auto it = keyPool.begin(); while (it != keyPool.end()) { if (!strcmp(*it, key)) { MO_DBG_VERBOSE("clear key %s", key); MO_FREE(*it); it = keyPool.erase(it); } else { ++it; } } } bool configurationsUpdated() { auto revisionSum_old = revisionSum; revisionSum = 0; for (auto& config : configurations) { revisionSum += config->getValueRevision(); } return revisionSum != revisionSum_old; } public: ConfigurationContainerFlash(std::shared_ptr filesystem, const char *filename, bool accessible) : ConfigurationContainer(filename, accessible), MemoryManaged("v16.Configuration.ContainerFlash.", filename), configurations(makeVector>(getMemoryTag())), filesystem(filesystem), keyPool(makeVector(getMemoryTag())) { } ~ConfigurationContainerFlash() { auto it = keyPool.begin(); while (it != keyPool.end()) { MO_FREE(*it); it = keyPool.erase(it); } } bool load() override { if (loaded) { return true; } if (!filesystem) { return false; } size_t file_size = 0; if (filesystem->stat(getFilename(), &file_size) != 0 // file does not exist || file_size == 0) { // file exists, but empty MO_DBG_DEBUG("Populate FS: create configuration file"); return save(); } auto doc = FilesystemUtils::loadJson(filesystem, getFilename(), getMemoryTag()); if (!doc) { MO_DBG_ERR("failed to load %s", getFilename()); return false; } JsonObject root = doc->as(); JsonObject configHeader = root["head"]; if (strcmp(configHeader["content-type"] | "Invalid", "ocpp_config_file") && strcmp(configHeader["content-type"] | "Invalid", "ao_configuration_file")) { //backwards-compatibility MO_DBG_ERR("Unable to initialize: unrecognized configuration file format"); return false; } if (strcmp(configHeader["version"] | "Invalid", "2.0") && strcmp(configHeader["version"] | "Invalid", "1.1")) { //backwards-compatibility MO_DBG_ERR("Unable to initialize: unsupported version"); return false; } JsonArray configurationsArray = root["configurations"]; if (configurationsArray.size() > MAX_CONFIGURATIONS) { MO_DBG_ERR("Unable to initialize: configurations_len is too big (=%zu)", configurationsArray.size()); return false; } for (JsonObject stored : configurationsArray) { TConfig type; if (!deserializeTConfig(stored["type"] | "_Undefined", type)) { MO_DBG_ERR("corrupt config"); continue; } const char *key = stored["key"] | ""; if (!*key) { MO_DBG_ERR("corrupt config"); continue; } if (!stored.containsKey("value")) { MO_DBG_ERR("corrupt config"); continue; } char *key_pooled = nullptr; auto config = getConfiguration(key).get(); if (config && config->getType() != type) { MO_DBG_ERR("conflicting type for %s - remove old config", key); remove(config); config = nullptr; } if (!config) { #if MO_ENABLE_HEAP_PROFILER char memoryTag [64]; snprintf(memoryTag, sizeof(memoryTag), "%s%s", "v16.Configuration.", key); #else const char *memoryTag = nullptr; (void)memoryTag; #endif key_pooled = static_cast(MO_MALLOC(memoryTag, strlen(key) + 1)); if (!key_pooled) { MO_DBG_ERR("OOM: %s", key); return false; } strcpy(key_pooled, key); } switch (type) { case TConfig::Int: { if (!stored["value"].is()) { MO_DBG_ERR("corrupt config"); MO_FREE(key_pooled); continue; } int value = stored["value"] | 0; if (!config) { //create new config config = createConfiguration(TConfig::Int, key_pooled).get(); } if (config) { config->setInt(value); } break; } case TConfig::Bool: { if (!stored["value"].is()) { MO_DBG_ERR("corrupt config"); MO_FREE(key_pooled); continue; } bool value = stored["value"] | false; if (!config) { //create new config config = createConfiguration(TConfig::Bool, key_pooled).get(); } if (config) { config->setBool(value); } break; } case TConfig::String: { if (!stored["value"].is()) { MO_DBG_ERR("corrupt config"); MO_FREE(key_pooled); continue; } const char *value = stored["value"] | ""; if (!config) { //create new config config = createConfiguration(TConfig::String, key_pooled).get(); } if (config) { config->setString(value); } break; } } if (config) { //success if (key_pooled) { //allocated key, need to store keyPool.push_back(std::move(key_pooled)); } } else { MO_DBG_ERR("OOM: %s", key); MO_FREE(key_pooled); } } configurationsUpdated(); MO_DBG_DEBUG("Initialization finished"); loaded = true; return true; } bool save() override { if (!filesystem) { return false; } if (!configurationsUpdated()) { return true; //nothing to be done } //during mocpp_deinitialize(), key owners are destructed. Don't store if this container is affected for (auto& config : configurations) { if (!config->getKey()) { MO_DBG_DEBUG("don't write back container with destructed key(s)"); return false; } } size_t jsonCapacity = 2 * JSON_OBJECT_SIZE(2); //head + configurations + head payload jsonCapacity += JSON_ARRAY_SIZE(configurations.size()); //configurations array jsonCapacity += configurations.size() * JSON_OBJECT_SIZE(3); //config entries in array if (jsonCapacity > MO_MAX_JSON_CAPACITY) { MO_DBG_ERR("configs JSON exceeds maximum capacity (%s, %zu entries). Crop configs file (by FCFS)", getFilename(), configurations.size()); jsonCapacity = MO_MAX_JSON_CAPACITY; } auto doc = initJsonDoc(getMemoryTag(), jsonCapacity); JsonObject head = doc.createNestedObject("head"); head["content-type"] = "ocpp_config_file"; head["version"] = "2.0"; JsonArray configurationsArray = doc.createNestedArray("configurations"); size_t trackCapacity = 0; for (size_t i = 0; i < configurations.size(); i++) { auto& config = *configurations[i]; size_t entryCapacity = JSON_OBJECT_SIZE(3) + (JSON_ARRAY_SIZE(2) - JSON_ARRAY_SIZE(1)); if (trackCapacity + entryCapacity > MO_MAX_JSON_CAPACITY) { break; } trackCapacity += entryCapacity; auto stored = configurationsArray.createNestedObject(); stored["type"] = serializeTConfig(config.getType()); stored["key"] = config.getKey(); switch (config.getType()) { case TConfig::Int: stored["value"] = config.getInt(); break; case TConfig::Bool: stored["value"] = config.getBool(); break; case TConfig::String: stored["value"] = config.getString(); break; } } bool success = FilesystemUtils::storeJson(filesystem, getFilename(), doc); if (success) { MO_DBG_DEBUG("Saving configurations finished"); } else { MO_DBG_ERR("could not save configs file: %s", getFilename()); } return success; } std::shared_ptr createConfiguration(TConfig type, const char *key) override { auto res = std::shared_ptr(makeConfiguration(type, key).release(), std::default_delete(), makeAllocator("v16.Configuration.", key)); if (!res) { //allocation failure - OOM MO_DBG_ERR("OOM"); return nullptr; } configurations.push_back(res); return res; } void remove(Configuration *config) override { const char *key = config->getKey(); configurations.erase(std::remove_if(configurations.begin(), configurations.end(), [config] (std::shared_ptr& entry) { return entry.get() == config; }), configurations.end()); if (key) { clearKeyPool(key); } } size_t size() override { return configurations.size(); } Configuration *getConfiguration(size_t i) override { return configurations[i].get(); } std::shared_ptr getConfiguration(const char *key) override { for (auto& entry : configurations) { if (entry->getKey() && !strcmp(entry->getKey(), key)) { return entry; } } return nullptr; } void loadStaticKey(Configuration& config, const char *key) override { config.setKey(key); clearKeyPool(key); } void removeUnused() override { //if a config's key is still in the keyPool, we know it's unused because it has never been declared in FW (originates from an older FW version) auto key = keyPool.begin(); while (key != keyPool.end()) { for (auto config = configurations.begin(); config != configurations.end(); ++config) { if ((*config)->getKey() == *key) { MO_DBG_DEBUG("remove unused config %s", (*config)->getKey()); configurations.erase(config); break; } } MO_FREE(*key); key = keyPool.erase(key); } } }; std::unique_ptr makeConfigurationContainerFlash(std::shared_ptr filesystem, const char *filename, bool accessible) { return std::unique_ptr(new ConfigurationContainerFlash(filesystem, filename, accessible)); } } //end namespace MicroOcpp ================================================ FILE: src/MicroOcpp/Core/ConfigurationContainerFlash.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CONFIGURATIONCONTAINERFLASH_H #define MO_CONFIGURATIONCONTAINERFLASH_H #include #include namespace MicroOcpp { std::unique_ptr makeConfigurationContainerFlash(std::shared_ptr filesystem, const char *filename, bool accessible); } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Core/ConfigurationKeyValue.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #define KEY_MAXLEN 60 #define STRING_VAL_MAXLEN 512 namespace MicroOcpp { template<> TConfig convertType() {return TConfig::Int;} template<> TConfig convertType() {return TConfig::Bool;} template<> TConfig convertType() {return TConfig::String;} Configuration::~Configuration() { } void Configuration::setInt(int) { #if MO_CONFIG_TYPECHECK MO_DBG_ERR("type err"); #endif } void Configuration::setBool(bool) { #if MO_CONFIG_TYPECHECK MO_DBG_ERR("type err"); #endif } bool Configuration::setString(const char*) { #if MO_CONFIG_TYPECHECK MO_DBG_ERR("type err"); #endif return false; } int Configuration::getInt() { #if MO_CONFIG_TYPECHECK MO_DBG_ERR("type err"); #endif return 0; } bool Configuration::getBool() { #if MO_CONFIG_TYPECHECK MO_DBG_ERR("type err"); #endif return false; } const char *Configuration::getString() { #if MO_CONFIG_TYPECHECK MO_DBG_ERR("type err"); #endif return ""; } revision_t Configuration::getValueRevision() { return value_revision; } void Configuration::setRebootRequired() { rebootRequired = true; } bool Configuration::isRebootRequired() { return rebootRequired; } void Configuration::setReadOnly() { if (mutability == Mutability::ReadWrite) { mutability = Mutability::ReadOnly; } else { mutability = Mutability::None; } } bool Configuration::isReadOnly() { return mutability == Mutability::ReadOnly; } bool Configuration::isReadable() { return mutability == Mutability::ReadWrite || mutability == Mutability::ReadOnly; } void Configuration::setWriteOnly() { if (mutability == Mutability::ReadWrite) { mutability = Mutability::WriteOnly; } else { mutability = Mutability::None; } } /* * Default implementations of the Configuration interface. * * How to use custom implementations: for each OCPP config, pass a config instance to the OCPP lib * before its initialization stage. Then the library won't create new config objects but */ class ConfigInt : public Configuration, public MemoryManaged { private: const char *key = nullptr; int val = 0; public: ~ConfigInt() = default; bool setKey(const char *key) override { this->key = key; updateMemoryTag("v16.Configuration.", key); return true; } const char *getKey() override { return key; } TConfig getType() override { return TConfig::Int; } void setInt(int val) override { this->val = val; value_revision++; } int getInt() override { return val; } }; class ConfigBool : public Configuration, public MemoryManaged { private: const char *key = nullptr; bool val = false; public: ~ConfigBool() = default; bool setKey(const char *key) override { this->key = key; updateMemoryTag("v16.Configuration.", key); return true; } const char *getKey() override { return key; } TConfig getType() override { return TConfig::Bool; } void setBool(bool val) override { this->val = val; value_revision++; } bool getBool() override { return val; } }; class ConfigString : public Configuration, public MemoryManaged { private: const char *key = nullptr; char *val = nullptr; public: ConfigString() = default; ConfigString(const ConfigString&) = delete; ConfigString(ConfigString&&) = delete; ConfigString& operator=(const ConfigString&) = delete; ~ConfigString() { MO_FREE(val); } bool setKey(const char *key) override { this->key = key; updateMemoryTag("v16.Configuration.", key); if (val) { MO_MEM_SET_TAG(val, getMemoryTag()); } return true; } const char *getKey() override { return key; } TConfig getType() override { return TConfig::String; } bool setString(const char *src) override { bool src_empty = !src || !*src; if (!val && src_empty) { return true; } if (this->val && src && !strcmp(this->val, src)) { return true; } size_t size = 0; if (!src_empty) { size = strlen(src) + 1; } if (size > MO_CONFIG_MAX_VALSTRSIZE) { return false; } value_revision++; if (this->val) { MO_FREE(this->val); this->val = nullptr; } if (!src_empty) { this->val = (char*) MO_MALLOC(getMemoryTag(), size); if (!this->val) { return false; } strcpy(this->val, src); } return true; } const char *getString() override { if (!val) { return ""; } return val; } }; std::unique_ptr makeConfiguration(TConfig type, const char *key) { std::unique_ptr res; switch (type) { case TConfig::Int: res.reset(new ConfigInt()); break; case TConfig::Bool: res.reset(new ConfigBool()); break; case TConfig::String: res.reset(new ConfigString()); break; } if (!res) { MO_DBG_ERR("OOM"); return nullptr; } res->setKey(key); return res; } bool deserializeTConfig(const char *serialized, TConfig& out) { if (!strcmp(serialized, "int")) { out = TConfig::Int; return true; } else if (!strcmp(serialized, "bool")) { out = TConfig::Bool; return true; } else if (!strcmp(serialized, "string")) { out = TConfig::String; return true; } else { MO_DBG_WARN("config type error"); return false; } } const char *serializeTConfig(TConfig type) { switch (type) { case TConfig::Int: return "int"; case TConfig::Bool: return "bool"; case TConfig::String: return "string"; } return "_Undefined"; } } //end namespace MicroOcpp ================================================ FILE: src/MicroOcpp/Core/ConfigurationKeyValue.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef CONFIGURATIONKEYVALUE_H #define CONFIGURATIONKEYVALUE_H #include #include #define MO_CONFIG_MAX_VALSTRSIZE 128 #ifndef MO_CONFIG_EXT_PREFIX #define MO_CONFIG_EXT_PREFIX "Cst_" #endif #ifndef MO_CONFIG_TYPECHECK #define MO_CONFIG_TYPECHECK 1 //enable this for debugging #endif namespace MicroOcpp { using revision_t = uint16_t; enum class TConfig : uint8_t { Int, Bool, String }; template TConfig convertType(); class Configuration { protected: revision_t value_revision = 0; //write access counter; used to check if this config has been changed private: bool rebootRequired = false; enum class Mutability : uint8_t { ReadWrite, ReadOnly, WriteOnly, None }; Mutability mutability = Mutability::ReadWrite; public: virtual ~Configuration(); virtual bool setKey(const char *key) = 0; virtual const char *getKey() = 0; virtual void setInt(int); virtual void setBool(bool); virtual bool setString(const char*); virtual int getInt(); virtual bool getBool(); virtual const char *getString(); //always returns c-string (empty if undefined) virtual TConfig getType() = 0; virtual revision_t getValueRevision(); void setRebootRequired(); bool isRebootRequired(); void setReadOnly(); bool isReadOnly(); bool isReadable(); void setWriteOnly(); }; /* * Default implementations of the Configuration interface. * * How to use custom implementations: for each OCPP config, pass a config instance to the OCPP lib * before its initialization stage. Then the library won't create new config objects but */ std::unique_ptr makeConfiguration(TConfig type, const char *key); const char *serializeTConfig(TConfig type); bool deserializeTConfig(const char *serialized, TConfig& out); } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Core/ConfigurationOptions.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CONFIGURATIONOPTIONS_H #define MO_CONFIGURATIONOPTIONS_H #include #ifdef __cplusplus extern "C" { #endif struct OCPP_FilesystemOpt { bool use; bool mount; bool formatFsOnFail; }; #ifdef __cplusplus } namespace MicroOcpp { class FilesystemOpt{ private: bool use = false; bool mount = false; bool formatFsOnFail = false; public: enum Mode : uint8_t {Deactivate, Use, Use_Mount, Use_Mount_FormatOnFail}; FilesystemOpt() = default; FilesystemOpt(Mode mode) { switch (mode) { case (FilesystemOpt::Use_Mount_FormatOnFail): formatFsOnFail = true; //fallthrough case (FilesystemOpt::Use_Mount): mount = true; //fallthrough case (FilesystemOpt::Use): use = true; break; default: break; } } FilesystemOpt(struct OCPP_FilesystemOpt fsopt) { this->use = fsopt.use; this->mount = fsopt.mount; this->formatFsOnFail = fsopt.formatFsOnFail; } bool accessAllowed() {return use;} bool mustMount() {return mount;} bool formatOnFail() {return formatFsOnFail;} }; } //end namespace MicroOcpp #endif //__cplusplus #endif ================================================ FILE: src/MicroOcpp/Core/Configuration_c.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using namespace MicroOcpp; class ConfigurationC : public Configuration, public MemoryManaged { private: ocpp_configuration *config; public: ConfigurationC(ocpp_configuration *config) : config(config) { if (config->read_only) { setReadOnly(); } if (config->write_only) { setWriteOnly(); } if (config->reboot_required) { setRebootRequired(); } } bool setKey(const char *key) override { updateMemoryTag("v16.Configuration.", key); return config->set_key(config->user_data, key); } const char *getKey() override { return config->get_key(config->user_data); } void setInt(int val) override { #if MO_CONFIG_TYPECHECK if (config->get_type(config->user_data) != ENUM_CDT_INT) { MO_DBG_ERR("type err"); return; } #endif config->set_int(config->user_data, val); } void setBool(bool val) override { #if MO_CONFIG_TYPECHECK if (config->get_type(config->user_data) != ENUM_CDT_BOOL) { MO_DBG_ERR("type err"); return; } #endif config->set_bool(config->user_data, val); } bool setString(const char *val) override { #if MO_CONFIG_TYPECHECK if (config->get_type(config->user_data) != ENUM_CDT_STRING) { MO_DBG_ERR("type err"); return false; } #endif return config->set_string(config->user_data, val); } int getInt() override { #if MO_CONFIG_TYPECHECK if (config->get_type(config->user_data) != ENUM_CDT_INT) { MO_DBG_ERR("type err"); return 0; } #endif return config->get_int(config->user_data); } bool getBool() override { #if MO_CONFIG_TYPECHECK if (config->get_type(config->user_data) != ENUM_CDT_BOOL) { MO_DBG_ERR("type err"); return false; } #endif return config->get_bool(config->user_data); } const char *getString() override { #if MO_CONFIG_TYPECHECK if (config->get_type(config->user_data) != ENUM_CDT_STRING) { MO_DBG_ERR("type err"); return ""; } #endif return config->get_string(config->user_data); } TConfig getType() override { TConfig res = TConfig::Int; switch (config->get_type(config->user_data)) { case ENUM_CDT_INT: res = TConfig::Int; break; case ENUM_CDT_BOOL: res = TConfig::Bool; break; case ENUM_CDT_STRING: res = TConfig::String; break; default: MO_DBG_ERR("type conversion"); break; } return res; } uint16_t getValueRevision() override { return config->get_write_count(config->user_data); } ocpp_configuration *getConfiguration() { return config; } }; namespace MicroOcpp { ConfigurationC *getConfigurationC(ocpp_configuration *config) { if (!config->mo_data) { return nullptr; } return reinterpret_cast*>(config->mo_data)->get(); } } using namespace MicroOcpp; void ocpp_setRebootRequired(ocpp_configuration *config) { if (auto c = getConfigurationC(config)) { c->setRebootRequired(); } config->reboot_required = true; } bool ocpp_isRebootRequired(ocpp_configuration *config) { if (auto c = getConfigurationC(config)) { return c->isRebootRequired(); } return config->reboot_required; } void ocpp_setReadOnly(ocpp_configuration *config) { if (auto c = getConfigurationC(config)) { c->setReadOnly(); } config->read_only = true; } bool ocpp_isReadOnly(ocpp_configuration *config) { if (auto c = getConfigurationC(config)) { return c->isReadOnly(); } return config->read_only; } bool ocpp_isReadable(ocpp_configuration *config) { if (auto c = getConfigurationC(config)) { return c->isReadable(); } return !config->write_only; } void ocpp_setWriteOnly(ocpp_configuration *config) { if (auto c = getConfigurationC(config)) { c->setWriteOnly(); } config->write_only = true; } class ConfigurationContainerC : public ConfigurationContainer, public MemoryManaged { private: ocpp_configuration_container *container; public: ConfigurationContainerC(ocpp_configuration_container *container, const char *filename, bool accessible) : ConfigurationContainer(filename, accessible), MemoryManaged("v16.Configuration.ContainerC.", filename), container(container) { } ~ConfigurationContainerC() { for (size_t i = 0; i < container->size(container->user_data); i++) { if (auto config = container->get_configuration(container->user_data, i)) { if (config->mo_data) { delete reinterpret_cast*>(config->mo_data); config->mo_data = nullptr; } } } } bool load() override { if (container->load) { return container->load(container->user_data); } else { return true; } } bool save() override { if (container->save) { return container->save(container->user_data); } else { return true; } } std::shared_ptr createConfiguration(TConfig type, const char *key) override { auto result = std::shared_ptr(nullptr, std::default_delete(), makeAllocator(getMemoryTag())); if (!container->create_configuration) { return result; } ocpp_config_datatype dt; switch (type) { case TConfig::Int: dt = ENUM_CDT_INT; break; case TConfig::Bool: dt = ENUM_CDT_BOOL; break; case TConfig::String: dt = ENUM_CDT_STRING; break; default: MO_DBG_ERR("internal error"); return result; } ocpp_configuration *config = container->create_configuration(container->user_data, dt, key); if (!config) { return result; } result.reset(new ConfigurationC(config)); if (result) { auto captureConfigC = new std::shared_ptr(result); config->mo_data = reinterpret_cast(captureConfigC); } else { MO_DBG_ERR("could not create config: %s", key); if (container->remove) { container->remove(container->user_data, key); } } return result; } void remove(Configuration *config) override { if (!container->remove) { return; } if (auto c = container->get_configuration_by_key(container->user_data, config->getKey())) { delete reinterpret_cast*>(c->mo_data); c->mo_data = nullptr; } container->remove(container->user_data, config->getKey()); } size_t size() override { return container->size(container->user_data); } Configuration *getConfiguration(size_t i) override { auto config = container->get_configuration(container->user_data, i); if (config) { if (!config->mo_data) { auto c = new ConfigurationC(config); if (c) { config->mo_data = reinterpret_cast(new std::shared_ptr(c, std::default_delete(), makeAllocator(getMemoryTag()))); } } return static_cast(config->mo_data ? reinterpret_cast*>(config->mo_data)->get() : nullptr); } else { return nullptr; } } std::shared_ptr getConfiguration(const char *key) override { auto config = container->get_configuration_by_key(container->user_data, key); if (config) { if (!config->mo_data) { auto c = new ConfigurationC(config); if (c) { config->mo_data = reinterpret_cast(new std::shared_ptr(c, std::default_delete(), makeAllocator(getMemoryTag()))); } } return config->mo_data ? *reinterpret_cast*>(config->mo_data) : nullptr; } else { return nullptr; } } void loadStaticKey(Configuration& config, const char *key) override { if (container->load_static_key) { container->load_static_key(container->user_data, key); } } }; void ocpp_configuration_container_add(ocpp_configuration_container *container, const char *container_path, bool accessible) { addConfigurationContainer(std::allocate_shared(makeAllocator("v16.Configuration.ContainerC.", container_path), container, container_path, accessible)); } ================================================ FILE: src/MicroOcpp/Core/Configuration_c.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CONFIGURATION_C_H #define MO_CONFIGURATION_C_H #include #include #ifdef __cplusplus extern "C" { #endif typedef enum ocpp_config_datatype { ENUM_CDT_INT, ENUM_CDT_BOOL, ENUM_CDT_STRING } ocpp_config_datatype; typedef struct ocpp_configuration { void *user_data; // Set this at your choice. MO passes it back to the functions below bool (*set_key) (void *user_data, const char *key); // Optional. MO may provide a static key value which you can use to replace a possibly malloc'd key buffer const char* (*get_key) (void *user_data); // Return Configuration key ocpp_config_datatype (*get_type) (void *user_data); // Return internal data type of config (determines which of the following getX()/setX() pairs are valid) // Set value of Config union { void (*set_int) (void *user_data, int val); void (*set_bool) (void *user_data, bool val); bool (*set_string) (void *user_data, const char *val); }; // Get value of Config union { int (*get_int) (void *user_data); bool (*get_bool) (void *user_data); const char* (*get_string) (void *user_data); }; uint16_t (*get_write_count) (void *user_data); // Return number of changes of the value. MO uses this to detect if the firmware has updated the config bool read_only; bool write_only; bool reboot_required; void *mo_data; // Reserved for MO } ocpp_configuration; void ocpp_setRebootRequired(ocpp_configuration *config); bool ocpp_isRebootRequired(ocpp_configuration *config); void ocpp_setReadOnly(ocpp_configuration *config); bool ocpp_isReadOnly(ocpp_configuration *config); bool ocpp_isReadable(ocpp_configuration *config); void ocpp_setWriteOnly(ocpp_configuration *config); typedef struct ocpp_configuration_container { void *user_data; //set this at your choice. MO passes it back to the functions below bool (*load) (void *user_data); // Called after declaring Configurations, to load them with their values bool (*save) (void *user_data); // Commit all Configurations to memory ocpp_configuration* (*create_configuration) (void *user_data, ocpp_config_datatype dt, const char *key); // Called to get a reference to a Configuration managed by this container (create new or return existing) void (*remove) (void *user_data, const char *key); // Remove this config from the container. Do not free the config here, the config must outlive the MO lifecycle size_t (*size) (void *user_data); // Number of Configurations currently managed by this container ocpp_configuration* (*get_configuration) (void *user_data, size_t i); // Return config at container position i ocpp_configuration* (*get_configuration_by_key) (void *user_data, const char *key); // Return config for given key void (*load_static_key) (void *user_data, const char *key); // Optional. MO may provide a static key value which you can use to replace a possibly malloc'd key buffer } ocpp_configuration_container; // Add custom Configuration container. Add one container per container_path before mocpp_initialize(...) void ocpp_configuration_container_add(ocpp_configuration_container *container, const char *container_path, bool accessible); #ifdef __cplusplus } // extern "C" #endif #endif ================================================ FILE: src/MicroOcpp/Core/Connection.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include using namespace MicroOcpp; LoopbackConnection::LoopbackConnection() : MemoryManaged("WebSocketLoopback") { } void LoopbackConnection::loop() { } bool LoopbackConnection::sendTXT(const char *msg, size_t length) { if (!connected || !online) { return false; } if (receiveTXT) { lastRecv = mocpp_tick_ms(); return receiveTXT(msg, length); } else { return false; } } void LoopbackConnection::setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT) { this->receiveTXT = receiveTXT; } unsigned long LoopbackConnection::getLastRecv() { return lastRecv; } unsigned long LoopbackConnection::getLastConnected() { return lastConn; } void LoopbackConnection::setOnline(bool online) { if (online) { lastConn = mocpp_tick_ms(); } this->online = online; } void LoopbackConnection::setConnected(bool connected) { if (connected) { lastConn = mocpp_tick_ms(); } this->connected = connected; } #ifndef MO_CUSTOM_WS using namespace MicroOcpp::EspWiFi; WSClient::WSClient(WebSocketsClient *wsock) : MemoryManaged("WebSocketsClient"), wsock(wsock) { } void WSClient::loop() { wsock->loop(); } bool WSClient::sendTXT(const char *msg, size_t length) { return wsock->sendTXT(msg, length); } void WSClient::setReceiveTXTcallback(ReceiveTXTcallback &callback) { auto& captureLastRecv = lastRecv; auto& captureLastConnected = lastConnected; wsock->onEvent([callback, &captureLastRecv, &captureLastConnected](WStype_t type, uint8_t * payload, size_t length) { switch (type) { case WStype_DISCONNECTED: MO_DBG_INFO("Disconnected"); break; case WStype_CONNECTED: MO_DBG_INFO("Connected (path: %s)", payload); captureLastRecv = mocpp_tick_ms(); captureLastConnected = mocpp_tick_ms(); break; case WStype_TEXT: if (callback((const char *) payload, length)) { //forward message to RequestQueue captureLastRecv = mocpp_tick_ms(); } else { MO_DBG_WARN("Processing WebSocket input event failed"); } break; case WStype_BIN: MO_DBG_WARN("Binary data stream not supported"); break; case WStype_PING: // pong will be send automatically MO_DBG_TRAFFIC_IN(8, "WS ping"); captureLastRecv = mocpp_tick_ms(); break; case WStype_PONG: // answer to a ping we send MO_DBG_TRAFFIC_IN(8, "WS pong"); captureLastRecv = mocpp_tick_ms(); break; case WStype_FRAGMENT_TEXT_START: //fragments are not supported default: MO_DBG_WARN("Unsupported WebSocket event type"); break; } }); } unsigned long WSClient::getLastRecv() { return lastRecv; } unsigned long WSClient::getLastConnected() { return lastConnected; } bool WSClient::isConnected() { return wsock->isConnected(); } #endif ================================================ FILE: src/MicroOcpp/Core/Connection.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CONNECTION_H #define MO_CONNECTION_H #include #include #include #include //On all platforms other than Arduino, the integrated WS lib (links2004/arduinoWebSockets) cannot be //used. On Arduino its usage is optional. #ifndef MO_CUSTOM_WS #if MO_PLATFORM != MO_PLATFORM_ARDUINO #define MO_CUSTOM_WS #endif #endif //ndef MO_CUSTOM_WS namespace MicroOcpp { using ReceiveTXTcallback = std::function; class Connection { public: Connection() = default; virtual ~Connection() = default; /* * The OCPP library will call this function frequently. If you need to execute regular routines, like * calling the loop-function of the WebSocket library, implement them here */ virtual void loop() = 0; /* * The OCPP library calls this function for sending out OCPP messages to the server */ virtual bool sendTXT(const char *msg, size_t length) = 0; /* * The OCPP library calls this function once during initialization. It passes a callback function to * the socket. The socket should forward any incoming payload from the OCPP server to the receiveTXT callback */ virtual void setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT) = 0; /* * Returns the timestamp of the last incoming message. Use mocpp_tick_ms() for creating the correct timestamp * * DEPRECATED: this function is superseded by isConnected(). Will be removed in MO v2.0 */ virtual unsigned long getLastRecv() {return 0;} /* * Returns the timestamp of the last time a connection got successfully established. Use mocpp_tick_ms() for creating the correct timestamp */ virtual unsigned long getLastConnected() = 0; /* * NEW IN v1.1 * * Returns true if the connection is open; false if the charger is known to be offline. * * This function determines if MO is in "offline mode". In offline mode, MO doesn't wait for Authorize responses * before performing fully local authorization. If the connection is disrupted but isConnected is still true, then * MO will first wait for a timeout to expire (20 seconds) before going into offline mode. * * Returning true will have no further effects other than using the timeout-then-offline mechanism. If the * connection status is uncertain, it's best to return true by default. */ virtual bool isConnected() {return true;} //MO ignores true. This default implementation keeps backwards-compatibility }; class LoopbackConnection : public Connection, public MemoryManaged { private: ReceiveTXTcallback receiveTXT; //for simulating connection losses bool online = true; bool connected = true; unsigned long lastRecv = 0; unsigned long lastConn = 0; public: LoopbackConnection(); void loop() override; bool sendTXT(const char *msg, size_t length) override; void setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT) override; unsigned long getLastRecv() override; unsigned long getLastConnected() override; void setOnline(bool online); //"online": sent messages are going through bool isOnline() {return online;} void setConnected(bool connected); //"connected": connection has been established, but messages may not go through (e.g. weak connection) bool isConnected() override {return connected;} }; } //end namespace MicroOcpp #ifndef MO_CUSTOM_WS #include namespace MicroOcpp { namespace EspWiFi { class WSClient : public Connection, public MemoryManaged { private: WebSocketsClient *wsock; unsigned long lastRecv = 0, lastConnected = 0; public: WSClient(WebSocketsClient *wsock); void loop(); bool sendTXT(const char *msg, size_t length); void setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT); unsigned long getLastRecv() override; //get time of last successful receive in millis unsigned long getLastConnected() override; //get last connection creation in millis bool isConnected() override; }; } //end namespace EspWiFi } //end namespace MicroOcpp #endif //ndef MO_CUSTOM_WS #endif ================================================ FILE: src/MicroOcpp/Core/Context.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include using namespace MicroOcpp; Context::Context(Connection& connection, std::shared_ptr filesystem, uint16_t bootNr, ProtocolVersion version) : MemoryManaged("Context"), connection(connection), model{version, bootNr}, reqQueue{connection, operationRegistry} { } Context::~Context() { } void Context::loop() { connection.loop(); reqQueue.loop(); model.loop(); } void Context::initiateRequest(std::unique_ptr op) { if (!op) { MO_DBG_ERR("invalid arg"); return; } reqQueue.sendRequest(std::move(op)); } Model& Context::getModel() { return model; } OperationRegistry& Context::getOperationRegistry() { return operationRegistry; } const ProtocolVersion& Context::getVersion() { return model.getVersion(); } Connection& Context::getConnection() { return connection; } RequestQueue& Context::getRequestQueue() { return reqQueue; } void Context::setFtpClient(std::unique_ptr ftpClient) { this->ftpClient = std::move(ftpClient); } FtpClient *Context::getFtpClient() { return ftpClient.get(); } ================================================ FILE: src/MicroOcpp/Core/Context.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CONTEXT_H #define MO_CONTEXT_H #include #include #include #include #include #include #include namespace MicroOcpp { class Connection; class FilesystemAdapter; class Context : public MemoryManaged { private: Connection& connection; OperationRegistry operationRegistry; Model model; RequestQueue reqQueue; std::unique_ptr ftpClient; public: Context(Connection& connection, std::shared_ptr filesystem, uint16_t bootNr, ProtocolVersion version); ~Context(); void loop(); void initiateRequest(std::unique_ptr op); Model& getModel(); OperationRegistry& getOperationRegistry(); const ProtocolVersion& getVersion(); Connection& getConnection(); RequestQueue& getRequestQueue(); void setFtpClient(std::unique_ptr ftpClient); FtpClient *getFtpClient(); }; } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Core/FilesystemAdapter.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include //FilesystemOpt #include #include #include /* * Platform specific implementations. Currently supported: * - Arduino LittleFs * - Arduino SPIFFS * - ESP-IDF SPIFFS * - POSIX-like API (tested on Ubuntu 20.04) * Plus a filesystem index decorator working with any of the above * * You can add support for other file systems by passing a custom adapter to mocpp_initialize(...) */ #if MO_ENABLE_FILE_INDEX #include namespace MicroOcpp { class FilesystemAdapterIndex; class IndexedFileAdapter : public FileAdapter, public MemoryManaged { private: FilesystemAdapterIndex& index; char fn [MO_MAX_PATH_SIZE]; std::unique_ptr file; size_t written = 0; public: IndexedFileAdapter(FilesystemAdapterIndex& index, const char *fn, std::unique_ptr file) : MemoryManaged("FilesystemIndex"), index(index), file(std::move(file)) { snprintf(this->fn, sizeof(this->fn), "%s", fn); } ~IndexedFileAdapter(); // destructor updates file index with written size size_t read(char *buf, size_t len) override { return file->read(buf, len); } size_t write(const char *buf, size_t len) override { auto ret = file->write(buf, len); written += ret; return ret; } size_t seek(size_t offset) override { auto ret = file->seek(offset); written = ret; return ret; } int read() override { return file->read(); } }; class FilesystemAdapterIndex : public FilesystemAdapter, public MemoryManaged { private: std::shared_ptr filesystem; struct IndexEntry { String fname; size_t size; IndexEntry(const char *fname, size_t size) : fname(makeString("FilesystemIndex", fname)), size(size) { } }; Vector index; IndexEntry *getEntryByFname(const char *fn) { auto entry = std::find_if(index.begin(), index.end(), [fn] (const IndexEntry& el) -> bool { return el.fname.compare(fn) == 0; }); if (entry != index.end()) { return &(*entry); } else { return nullptr; } } IndexEntry *getEntryByPath(const char *path) { if (strlen(path) < sizeof(MO_FILENAME_PREFIX) - 1) { MO_DBG_ERR("invalid fn"); return nullptr; } const char *fn = path + sizeof(MO_FILENAME_PREFIX) - 1; return getEntryByFname(fn); } void (*onDestruct)(void*) = nullptr; public: FilesystemAdapterIndex(std::shared_ptr filesystem, void (*onDestruct)(void*) = nullptr) : MemoryManaged("FilesystemIndex"), filesystem(std::move(filesystem)), index(makeVector("FilesystemIndex")), onDestruct(onDestruct) { } ~FilesystemAdapterIndex() { if (onDestruct) { onDestruct(this); } } int stat(const char *path, size_t *size) override { if (auto file = getEntryByPath(path)) { *size = file->size; return 0; } else { return -1; } } std::unique_ptr open(const char *path, const char *mode) { if (!strcmp(mode, "r")) { return filesystem->open(path, "r"); } else if (!strcmp(mode, "w")) { if (strlen(path) < sizeof(MO_FILENAME_PREFIX) - 1) { MO_DBG_ERR("invalid fn"); return nullptr; } const char *fn = path + sizeof(MO_FILENAME_PREFIX) - 1; auto file = filesystem->open(path, "w"); if (!file) { return nullptr; } IndexEntry *entry = nullptr; if (!(entry = getEntryByFname(fn))) { index.emplace_back(fn, 0); entry = &index.back(); } if (!entry) { MO_DBG_ERR("internal error"); return nullptr; } entry->size = 0; //write always empties the file return std::unique_ptr(new IndexedFileAdapter(*this, entry->fname.c_str(), std::move(file))); } else { MO_DBG_ERR("only support r or w"); return nullptr; } } bool remove(const char *path) override { if (strlen(path) >= sizeof(MO_FILENAME_PREFIX) - 1) { //valid path const char *fn = path + sizeof(MO_FILENAME_PREFIX) - 1; index.erase(std::remove_if(index.begin(), index.end(), [fn] (const IndexEntry& el) -> bool { return el.fname.compare(fn) == 0; }), index.end()); } return filesystem->remove(path); } int ftw_root(std::function fn) { // allow fn to remove elements for (size_t it = 0; it < index.size();) { auto size_before = index.size(); auto err = fn(index[it].fname.c_str()); if (err) { return err; } if (index.size() + 1 == size_before) { // element removed continue; } // normal execution it++; } return 0; } bool createIndex() { if (!index.empty()) { return false; } auto ret = filesystem->ftw_root([this] (const char *fn) -> int { int ret; char path [MO_MAX_PATH_SIZE]; ret = snprintf(path, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "%s", fn); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return 0; //ignore this entry and continue ftw } size_t size; ret = filesystem->stat(path, &size); if (ret == 0) { //add fn and size to index MO_DBG_DEBUG("add file to index: %s (%zuB)", fn, size); index.emplace_back(fn, size); return 0; //successfully added filename to index } else { MO_DBG_ERR("unexpected entry: %s", fn); return 0; //ignore this entry and continue ftw } }); MO_DBG_DEBUG("create fs index: %s, %zu entries", ret == 0 ? "success" : "failure", index.size()); return ret == 0; } void updateFilesize(const char *fn, size_t size) { if (auto entry = getEntryByFname(fn)) { entry->size = size; MO_DBG_DEBUG("update index: %s (%zuB)", entry->fname.c_str(), entry->size); } } }; IndexedFileAdapter::~IndexedFileAdapter() { index.updateFilesize(fn, written); } std::shared_ptr decorateIndex(std::shared_ptr filesystem, void (*onDestruct)(void*) = nullptr) { auto fsIndex = std::allocate_shared(makeAllocator("FilesystemIndex"), std::move(filesystem), onDestruct); if (!fsIndex) { MO_DBG_ERR("OOM"); return nullptr; } if (!fsIndex->createIndex()) { MO_DBG_ERR("createIndex err"); return nullptr; } return fsIndex; } } // namespace MicroOcpp #endif //MO_ENABLE_FILE_INDEX #if MO_USE_FILEAPI == ARDUINO_LITTLEFS || MO_USE_FILEAPI == ARDUINO_SPIFFS #if MO_USE_FILEAPI == ARDUINO_LITTLEFS #include #include #define USE_FS LittleFS #elif MO_USE_FILEAPI == ARDUINO_SPIFFS #include #define USE_FS SPIFFS #endif namespace MicroOcpp { class ArduinoFileAdapter : public FileAdapter, public MemoryManaged { File file; public: ArduinoFileAdapter(File&& file) : MemoryManaged("Filesystem"), file(file) {} ~ArduinoFileAdapter() { if (file) { file.close(); } } int read() override { return file.read(); }; size_t read(char *buf, size_t len) override { return file.readBytes(buf, len); } size_t write(const char *buf, size_t len) override { return file.printf("%.*s", len, buf); } size_t seek(size_t offset) override { return file.seek(offset); } }; class ArduinoFilesystemAdapter : public FilesystemAdapter, public MemoryManaged { private: bool valid = false; FilesystemOpt config; void (* onDestruct)(void*) = nullptr; public: ArduinoFilesystemAdapter(FilesystemOpt config, void (*onDestruct)(void*) = nullptr) : MemoryManaged("Filesystem"), config(config), onDestruct(onDestruct) { valid = true; if (config.mustMount()) { #if MO_USE_FILEAPI == ARDUINO_LITTLEFS if(!USE_FS.begin(config.formatOnFail())) { MO_DBG_ERR("Error while mounting LITTLEFS"); valid = false; } else { MO_DBG_DEBUG("LittleFS mount success"); } #elif MO_USE_FILEAPI == ARDUINO_SPIFFS //ESP8266 SPIFFSConfig cfg; cfg.setAutoFormat(config.formatOnFail()); SPIFFS.setConfig(cfg); if (!SPIFFS.begin()) { MO_DBG_ERR("Unable to initialize: unable to mount SPIFFS"); valid = false; } #else #error #endif } //end if mustMount() } ~ArduinoFilesystemAdapter() { if (config.mustMount()) { USE_FS.end(); } if (onDestruct) { onDestruct(this); } } operator bool() {return valid;} int stat(const char *path, size_t *size) override { #if MO_USE_FILEAPI == ARDUINO_LITTLEFS char partition_path [MO_MAX_PATH_SIZE]; auto ret = snprintf(partition_path, MO_MAX_PATH_SIZE, "/littlefs%s", path); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return -1; } struct ::stat st; auto status = ::stat(partition_path, &st); if (status == 0) { *size = st.st_size; } return status; #elif MO_USE_FILEAPI == ARDUINO_SPIFFS if (!USE_FS.exists(path)) { return -1; } File f = USE_FS.open(path, "r"); if (!f) { return -1; } int status = -1; if (!f.isDirectory()) { *size = f.size(); status = 0; } else { //fetch more information for directory when MicroOcpp also uses them //status = 0; } f.close(); return status; #else #error #endif } //end stat std::unique_ptr open(const char *fn, const char *mode) override { File file = USE_FS.open(fn, mode); if (file && !file.isDirectory()) { MO_DBG_DEBUG("File open successful: %s", fn); return std::unique_ptr(new ArduinoFileAdapter(std::move(file))); } else { return nullptr; } } bool remove(const char *fn) override { return USE_FS.remove(fn); }; int ftw_root(std::function fn) override { #if MO_USE_FILEAPI == ARDUINO_LITTLEFS auto dir = USE_FS.open(MO_FILENAME_PREFIX); if (!dir) { MO_DBG_ERR("cannot open root directory: " MO_FILENAME_PREFIX); return -1; } int err = 0; while (auto entry = dir.openNextFile()) { char fname [MO_MAX_PATH_SIZE]; auto ret = snprintf(fname, MO_MAX_PATH_SIZE, "%s", entry.name()); entry.close(); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return -1; } err = fn(fname); if (err) { break; } } return err; #elif MO_USE_FILEAPI == ARDUINO_SPIFFS auto dir = USE_FS.openDir(MO_FILENAME_PREFIX); int err = 0; while (dir.next()) { auto fname = dir.fileName(); if (fname.c_str()) { err = fn(fname.c_str() + strlen(MO_FILENAME_PREFIX)); } else { MO_DBG_ERR("fs error"); err = -1; } if (err) { break; } } return err; #else #error #endif } }; std::weak_ptr filesystemCache; void resetFilesystemCache(void*) { filesystemCache.reset(); } std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { if (auto cached = filesystemCache.lock()) { return cached; } if (!config.accessAllowed()) { MO_DBG_DEBUG("Access to Arduino FS not allowed by config"); return nullptr; } auto fs_concrete = new ArduinoFilesystemAdapter(config, resetFilesystemCache); auto fs = std::shared_ptr(fs_concrete, std::default_delete(), makeAllocator("Filesystem")); #if MO_ENABLE_FILE_INDEX fs = decorateIndex(fs, resetFilesystemCache); #endif // MO_ENABLE_FILE_INDEX filesystemCache = fs; if (*fs_concrete) { return fs; } else { return nullptr; } } } //end namespace MicroOcpp #elif MO_USE_FILEAPI == ESPIDF_SPIFFS #include #include #include "esp_spiffs.h" #ifndef MO_PARTITION_LABEL #define MO_PARTITION_LABEL "mo" #endif namespace MicroOcpp { class EspIdfFileAdapter : public FileAdapter, public MemoryManaged { FILE *file {nullptr}; public: EspIdfFileAdapter(FILE *file) : MemoryManaged("Filesystem"), file(file) {} ~EspIdfFileAdapter() { fclose(file); } size_t read(char *buf, size_t len) override { return fread(buf, 1, len, file); } size_t write(const char *buf, size_t len) override { return fwrite(buf, 1, len, file); } size_t seek(size_t offset) override { return fseek(file, offset, SEEK_SET); } int read() override { return fgetc(file); } }; class EspIdfFilesystemAdapter : public FilesystemAdapter, public MemoryManaged { public: FilesystemOpt config; void (* onDestruct)(void*) = nullptr; public: EspIdfFilesystemAdapter(FilesystemOpt config, void (* onDestruct)(void*) = nullptr) : MemoryManaged("Filesystem"), config(config), onDestruct(onDestruct) { } ~EspIdfFilesystemAdapter() { if (config.mustMount()) { esp_vfs_spiffs_unregister(MO_PARTITION_LABEL); MO_DBG_DEBUG("SPIFFS unmounted"); } if (onDestruct) { onDestruct(this); } } int stat(const char *path, size_t *size) override { struct ::stat st; auto ret = ::stat(path, &st); if (ret == 0) { *size = st.st_size; } return ret; } std::unique_ptr open(const char *fn, const char *mode) override { auto file = fopen(fn, mode); if (file) { return std::unique_ptr(new EspIdfFileAdapter(std::move(file))); } else { MO_DBG_DEBUG("Failed to open file path %s", fn); return nullptr; } } bool remove(const char *fn) override { return unlink(fn) == 0; } int ftw_root(std::function fn) override { //open MO root directory char dname [MO_MAX_PATH_SIZE]; auto dlen = snprintf(dname, MO_MAX_PATH_SIZE, "%s", MO_FILENAME_PREFIX); if (dlen < 0 || dlen >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", dlen); return -1; } // trim trailing '/' if not root directory if (dlen >= 2 && dname[dlen - 1] == '/') { dname[dlen - 1] = '\0'; } auto dir = opendir(dname); if (!dir) { MO_DBG_ERR("cannot open root directory: %s", dname); return -1; } int err = 0; while (auto entry = readdir(dir)) { err = fn(entry->d_name); if (err) { break; } } closedir(dir); return err; } }; std::weak_ptr filesystemCache; void resetFilesystemCache(void*) { filesystemCache.reset(); } std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { if (auto cached = filesystemCache.lock()) { return cached; } if (!config.accessAllowed()) { MO_DBG_DEBUG("Access to ESP-IDF SPIFFS not allowed by config"); return nullptr; } bool mounted = true; if (config.mustMount()) { mounted = false; char fnpref [MO_MAX_PATH_SIZE]; auto fnpref_len = snprintf(fnpref, MO_MAX_PATH_SIZE, "%s", MO_FILENAME_PREFIX); if (fnpref_len < 0 || fnpref_len >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("MO_FILENAME_PREFIX error %i", fnpref_len); return nullptr; } else if (fnpref_len <= 2) { //shortest possible prefix: "/p/", i.e. length = 3 MO_DBG_ERR("MO_FILENAME_PREFIX cannot be root on ESP-IDF (working example: \"/mo_store/\")"); return nullptr; } // trim trailing '/' if (fnpref[fnpref_len - 1] == '/') { fnpref[fnpref_len - 1] = '\0'; } esp_vfs_spiffs_conf_t conf = { .base_path = fnpref, .partition_label = MO_PARTITION_LABEL, .max_files = 5, .format_if_mount_failed = config.formatOnFail() }; esp_err_t ret = esp_vfs_spiffs_register(&conf); if (ret == ESP_OK) { mounted = true; MO_DBG_DEBUG("SPIFFS mounted"); } else { if (ret == ESP_FAIL) { MO_DBG_ERR("Failed to mount or format filesystem"); } else if (ret == ESP_ERR_NOT_FOUND) { MO_DBG_ERR("Failed to find SPIFFS partition"); } else { MO_DBG_ERR("Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); } } } if (mounted) { auto fs = std::shared_ptr(new EspIdfFilesystemAdapter(config, resetFilesystemCache), std::default_delete(), makeAllocator("Filesystem")); #if MO_ENABLE_FILE_INDEX fs = decorateIndex(fs, resetFilesystemCache); #endif // MO_ENABLE_FILE_INDEX filesystemCache = fs; return fs; } else { return nullptr; } } } //end namespace MicroOcpp #elif MO_USE_FILEAPI == POSIX_FILEAPI #include #include #include namespace MicroOcpp { class PosixFileAdapter : public FileAdapter, public MemoryManaged { FILE *file {nullptr}; public: PosixFileAdapter(FILE *file) : MemoryManaged("Filesystem"), file(file) {} ~PosixFileAdapter() { fclose(file); } size_t read(char *buf, size_t len) override { return fread(buf, 1, len, file); } size_t write(const char *buf, size_t len) override { return fwrite(buf, 1, len, file); } size_t seek(size_t offset) override { return fseek(file, offset, SEEK_SET); } int read() override { return fgetc(file); } }; class PosixFilesystemAdapter : public FilesystemAdapter, public MemoryManaged { public: FilesystemOpt config; void (* onDestruct)(void*) = nullptr; public: PosixFilesystemAdapter(FilesystemOpt config, void (* onDestruct)(void*) = nullptr) : MemoryManaged("Filesystem"), config(config), onDestruct(onDestruct) { } ~PosixFilesystemAdapter() { if (onDestruct) { onDestruct(this); } } int stat(const char *path, size_t *size) override { struct ::stat st; auto ret = ::stat(path, &st); if (ret == 0) { *size = st.st_size; } return ret; } std::unique_ptr open(const char *fn, const char *mode) override { auto file = fopen(fn, mode); if (file) { return std::unique_ptr(new PosixFileAdapter(std::move(file))); } else { MO_DBG_DEBUG("Failed to open file path %s", fn); return nullptr; } } bool remove(const char *fn) override { return ::remove(fn) == 0; } int ftw_root(std::function fn) override { auto dir = opendir(MO_FILENAME_PREFIX); // use c_str() to convert the path string to a C-style string if (!dir) { MO_DBG_ERR("cannot open root directory: " MO_FILENAME_PREFIX); return -1; } int err = 0; while (auto entry = readdir(dir)) { if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) { continue; //files . and .. are specific to desktop systems and rarely appear on microcontroller filesystems. Filter them } err = fn(entry->d_name); if (err) { break; } } closedir(dir); return err; } }; std::weak_ptr filesystemCache; void resetFilesystemCache(void*) { filesystemCache.reset(); } std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { if (auto cached = filesystemCache.lock()) { return cached; } if (!config.accessAllowed()) { MO_DBG_DEBUG("Access to FS not allowed by config"); return nullptr; } if (config.mustMount()) { MO_DBG_DEBUG("Skip mounting on UNIX host"); } auto fs = std::shared_ptr(new PosixFilesystemAdapter(config, resetFilesystemCache), std::default_delete(), makeAllocator("Filesystem")); #if MO_ENABLE_FILE_INDEX fs = decorateIndex(fs, resetFilesystemCache); #endif // MO_ENABLE_FILE_INDEX filesystemCache = fs; return fs; } } //end namespace MicroOcpp #else //filesystem disabled namespace MicroOcpp { std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { return nullptr; } } //end namespace MicroOcpp #endif //switch-case MO_USE_FILEAPI ================================================ FILE: src/MicroOcpp/Core/FilesystemAdapter.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_FILESYSTEMADAPTER_H #define MO_FILESYSTEMADAPTER_H #include #include #include #include #define DISABLE_FS 0 #define ARDUINO_LITTLEFS 1 #define ARDUINO_SPIFFS 2 #define ESPIDF_SPIFFS 3 #define POSIX_FILEAPI 4 // choose FileAPI if not given by build flag; assume usage with Arduino if no build flags are present #ifndef MO_USE_FILEAPI #if MO_PLATFORM == MO_PLATFORM_ARDUINO #if defined(ESP32) #define MO_USE_FILEAPI ARDUINO_LITTLEFS #else #define MO_USE_FILEAPI ARDUINO_SPIFFS #endif #elif MO_PLATFORM == MO_PLATFORM_ESPIDF #define MO_USE_FILEAPI ESPIDF_SPIFFS #elif MO_PLATFORM == MO_PLATFORM_UNIX #define MO_USE_FILEAPI POSIX_FILEAPI #else #define MO_USE_FILEAPI DISABLE_FS #endif //switch-case MO_PLATFORM #endif //ndef MO_USE_FILEAPI #ifndef MO_FILENAME_PREFIX #if MO_USE_FILEAPI == ESPIDF_SPIFFS #define MO_FILENAME_PREFIX "/mo_store/" #else #define MO_FILENAME_PREFIX "/" #endif #endif // set default max path size parameters #ifndef MO_MAX_PATH_SIZE #if MO_USE_FILEAPI == POSIX_FILEAPI #define MO_MAX_PATH_SIZE 128 #else #define MO_MAX_PATH_SIZE 30 #endif #endif #ifndef MO_ENABLE_FILE_INDEX #define MO_ENABLE_FILE_INDEX 0 #endif namespace MicroOcpp { class FileAdapter { public: virtual ~FileAdapter() = default; virtual size_t read(char *buf, size_t len) = 0; virtual size_t write(const char *buf, size_t len) = 0; virtual size_t seek(size_t offset) = 0; virtual int read() = 0; }; class FilesystemAdapter { public: virtual ~FilesystemAdapter() = default; virtual int stat(const char *path, size_t *size) = 0; virtual std::unique_ptr open(const char *fn, const char *mode) = 0; virtual bool remove(const char *fn) = 0; virtual int ftw_root(std::function fn) = 0; //enumerate the files in the mo_store root folder }; /* * Platform specific implementation. Currently supported: * - Arduino LittleFs * - Arduino SPIFFS * - ESP-IDF SPIFFS * - POSIX-like API (tested on Ubuntu 20.04) * * You can add support for other file systems by passing a custom adapter to mocpp_initialize(...) * * Returns null if platform is not supported or Filesystem is disabled */ std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config); } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Core/FilesystemUtils.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include //FilesystemOpt #include using namespace MicroOcpp; std::unique_ptr FilesystemUtils::loadJson(std::shared_ptr filesystem, const char *fn, const char *memoryTag) { if (!filesystem || !fn || *fn == '\0') { MO_DBG_ERR("Format error"); return nullptr; } if (strnlen(fn, MO_MAX_PATH_SIZE) >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("Fn too long: %.*s", MO_MAX_PATH_SIZE, fn); return nullptr; } size_t fsize = 0; if (filesystem->stat(fn, &fsize) != 0) { MO_DBG_DEBUG("File does not exist: %s", fn); return nullptr; } if (fsize < 2) { MO_DBG_ERR("File too small for JSON, collect %s", fn); filesystem->remove(fn); return nullptr; } auto file = filesystem->open(fn, "r"); if (!file) { MO_DBG_ERR("Could not open file %s", fn); return nullptr; } size_t capacity_init = (3 * fsize) / 2; //capacity = ceil capacity_init to the next power of two; should be at least 128 size_t capacity = 128; while (capacity < capacity_init && capacity < MO_MAX_JSON_CAPACITY) { capacity *= 2; } if (capacity > MO_MAX_JSON_CAPACITY) { capacity = MO_MAX_JSON_CAPACITY; } std::unique_ptr doc; DeserializationError err = DeserializationError::NoMemory; ArduinoJsonFileAdapter fileReader {file.get()}; while (err == DeserializationError::NoMemory && capacity <= MO_MAX_JSON_CAPACITY) { doc = makeJsonDoc(memoryTag, capacity); err = deserializeJson(*doc, fileReader); capacity *= 2; file->seek(0); //rewind file to beginning } if (err) { MO_DBG_ERR("Error deserializing file %s: %s", fn, err.c_str()); //skip this file return nullptr; } MO_DBG_DEBUG("Loaded JSON file: %s", fn); return doc; } bool FilesystemUtils::storeJson(std::shared_ptr filesystem, const char *fn, const JsonDoc& doc) { if (!filesystem || !fn || *fn == '\0') { MO_DBG_ERR("Format error"); return false; } if (strnlen(fn, MO_MAX_PATH_SIZE) >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("Fn too long: %.*s", MO_MAX_PATH_SIZE, fn); return false; } if (doc.isNull() || doc.overflowed()) { MO_DBG_ERR("Invalid JSON %s", fn); return false; } auto file = filesystem->open(fn, "w"); if (!file) { MO_DBG_ERR("Could not open file %s", fn); return false; } ArduinoJsonFileAdapter fileWriter {file.get()}; size_t written = serializeJson(doc, fileWriter); if (written < 2) { MO_DBG_ERR("Error writing file %s", fn); size_t file_size = 0; if (filesystem->stat(fn, &file_size) == 0) { MO_DBG_DEBUG("Collect invalid file %s", fn); filesystem->remove(fn); } return false; } MO_DBG_DEBUG("Wrote JSON file: %s", fn); return true; } bool FilesystemUtils::remove_if(std::shared_ptr filesystem, std::function pred) { auto ret = filesystem->ftw_root([filesystem, pred] (const char *fpath) { if (pred(fpath)) { char fn [MO_MAX_PATH_SIZE] = {'\0'}; auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "%s", fpath); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return -1; } filesystem->remove(fn); //no error handling - just skip failed file } return 0; }); if (ret != 0) { MO_DBG_ERR("ftw_root: %i", ret); } return ret == 0; } ================================================ FILE: src/MicroOcpp/Core/FilesystemUtils.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_FILESYSTEMUTILS_H #define MO_FILESYSTEMUTILS_H #include #include #include #include namespace MicroOcpp { class ArduinoJsonFileAdapter { private: FileAdapter *file; public: ArduinoJsonFileAdapter(FileAdapter *file) : file(file) { } size_t readBytes(char *buf, size_t len) { return file->read(buf, len); } int read() { return file->read(); } size_t write(const uint8_t *buf, size_t len) { return file->write((const char*) buf, len); } size_t write(uint8_t c) { return file->write((const char*) &c, 1); } }; namespace FilesystemUtils { std::unique_ptr loadJson(std::shared_ptr filesystem, const char *fn, const char *memoryTag = nullptr); bool storeJson(std::shared_ptr filesystem, const char *fn, const JsonDoc& doc); bool remove_if(std::shared_ptr filesystem, std::function pred); } } #endif ================================================ FILE: src/MicroOcpp/Core/Ftp.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_FTP_H #define MO_FTP_H #include #ifdef __cplusplus extern "C" { #endif typedef enum { MO_FtpCloseReason_Undefined, MO_FtpCloseReason_Success, MO_FtpCloseReason_Failure } MO_FtpCloseReason; typedef struct ocpp_ftp_download { void *user_data; //set this at your choice. MO passes it back to the functions below void (*loop)(void *user_data); void (*is_active)(void *user_data); } ocpp_ftp_download; typedef struct ocpp_ftp_upload { void *user_data; //set this at your choice. MO passes it back to the functions below void (*loop)(void *user_data); void (*is_active)(void *user_data); } ocpp_ftp_upload; typedef struct ocpp_ftp_client { void *user_data; //set this at your choice. MO passes it back to the functions below void (*loop)(void *user_data); ocpp_ftp_download* (*get_file)(void *user_data, const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename size_t (*file_writer)(void *mo_data, unsigned char *data, size_t len), void (*on_close)(void *mo_data, MO_FtpCloseReason reason), void *mo_data, const char *ca_cert); // nullptr to disable cert check; will be ignored for non-TLS connections void (*get_file_free)(void *user_data, ocpp_ftp_download*); ocpp_ftp_upload* (*post_file)(void *user_data, const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename size_t (*file_reader)(void *mo_data, unsigned char *buf, size_t bufsize), void (*on_close)(void *mo_data, MO_FtpCloseReason reason), void *mo_data, const char *ca_cert); // nullptr to disable cert check; will be ignored for non-TLS connections void (*post_file_free)(void *user_data, ocpp_ftp_upload*); } ocpp_ftp_client; #ifdef __cplusplus } //extern "C" #include #include namespace MicroOcpp { class FtpDownload { public: virtual ~FtpDownload() = default; virtual void loop() = 0; virtual bool isActive() = 0; }; class FtpUpload { public: virtual ~FtpUpload() = default; virtual void loop() = 0; virtual bool isActive() = 0; }; class FtpClient { public: virtual ~FtpClient() = default; virtual std::unique_ptr getFile( const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename std::function fileWriter, std::function onClose, const char *ca_cert = nullptr) = 0; // nullptr to disable cert check; will be ignored for non-TLS connections virtual std::unique_ptr postFile( const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename std::function fileReader, //write at most buffsize bytes into out-buffer. Return number of bytes written std::function onClose, const char *ca_cert = nullptr) = 0; // nullptr to disable cert check; will be ignored for non-TLS connections }; } // namespace MicroOcpp #endif //def __cplusplus #endif ================================================ FILE: src/MicroOcpp/Core/FtpMbedTLS.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_MBEDTLS #include #include #include "mbedtls/net_sockets.h" #include "mbedtls/ssl.h" #include "mbedtls/entropy.h" #include "mbedtls/ctr_drbg.h" #include "mbedtls/x509.h" #include "mbedtls/error.h" #include #include namespace MicroOcpp { class FtpTransferMbedTLS : public FtpUpload, public FtpDownload, public MemoryManaged { private: //MbedTLS common mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; mbedtls_ssl_config conf; mbedtls_x509_crt cacert; mbedtls_x509_crt clicert; mbedtls_pk_context pkey; const char *ca_cert = nullptr; const char *client_cert = nullptr; const char *client_key = nullptr; bool isSecure = false; //tls policy //control connection specific mbedtls_net_context ctrl_fd; mbedtls_ssl_context ctrl_ssl; bool ctrl_opened = false; bool ctrl_ssl_established = false; //data connection specific mbedtls_net_context data_fd; mbedtls_ssl_context data_ssl; bool data_opened = false; bool data_ssl_established = false; bool data_conn_accepted = false; //Server sent okay to upload / download data //FTP URL String user; String pass; String ctrl_host; String ctrl_port; String dir; String fname; String data_host; String data_port; bool read_url_ctrl(const char *ftp_url); bool read_url_data(const char *data_url); std::function fileWriter; std::function fileReader; std::function onClose; enum class Method { Retrieve, //download file Store, //upload file UNDEFINED }; Method method = Method::UNDEFINED; int setup_tls(); int connect(mbedtls_net_context& fd, mbedtls_ssl_context& ssl, const char *server_name, const char *server_port); int connect_ctrl(); int connect_data(); void close_ctrl(); void close_data(MO_FtpCloseReason reason); int handshake_tls(); void send_cmd(const char *cmd, const char *arg = nullptr, bool disable_tls_policy = false); void process_ctrl(); void process_data(); unsigned char *data_buf = nullptr; size_t data_buf_size = 4096; size_t data_buf_avail = 0; size_t data_buf_offs = 0; public: FtpTransferMbedTLS(bool tls_only = false, const char *client_cert = nullptr, const char *client_key = nullptr); ~FtpTransferMbedTLS(); void loop() override; bool isActive() override; bool getFile(const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename std::function fileWriter, std::function onClose, const char *ca_cert = nullptr); // nullptr to disable cert check; will be ignored for non-TLS connections bool postFile(const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename std::function fileReader, //write at most buffsize bytes into out-buffer. Return number of bytes written std::function onClose, const char *ca_cert = nullptr); // nullptr to disable cert check; will be ignored for non-TLS connections }; class FtpClientMbedTLS : public FtpClient, public MemoryManaged { private: const char *client_cert = nullptr; const char *client_key = nullptr; bool tls_only = false; //tls policy public: FtpClientMbedTLS(bool tls_only = false, const char *client_cert = nullptr, const char *client_key = nullptr); std::unique_ptr getFile(const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename std::function fileWriter, std::function onClose, const char *ca_cert = nullptr) override; // nullptr to disable cert check; will be ignored for non-TLS connections std::unique_ptr postFile(const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename std::function fileReader, //write at most buffsize bytes into out-buffer. Return number of bytes written std::function onClose, const char *ca_cert = nullptr) override; // nullptr to disable cert check; will be ignored for non-TLS connections }; std::unique_ptr makeFtpClientMbedTLS(bool tls_only, const char *client_cert, const char *client_key) { return std::unique_ptr(new FtpClientMbedTLS(tls_only, client_cert, client_key)); } void mo_mbedtls_log(void *user, int level, const char *file, int line, const char *str) { /* * MbedTLS debug level documented in mbedtls/debug.h: * - 0 No debug * - 1 Error * - 2 State change * - 3 Informational * - 4 Verbose * * To change the debug level, use the build flag MO_DBG_LEVEL_MBEDTLS accordingly */ const char *lstr = ""; if (level <= 1) { lstr = "ERROR"; } else if (level <= 3) { lstr = "debug"; } else { lstr = "verbose"; } MO_CONSOLE_PRINTF("[MO] %s (%s:%i): %s\n", lstr, file, line, str); } /* * FTP implementation */ FtpTransferMbedTLS::FtpTransferMbedTLS(bool tls_only, const char *client_cert, const char *client_key) : MemoryManaged("FTP.TransferMbedTLS"), client_cert(client_cert), client_key(client_key), isSecure(tls_only), user(makeString(getMemoryTag())), pass(makeString(getMemoryTag())), ctrl_host(makeString(getMemoryTag())), ctrl_port(makeString(getMemoryTag())), dir(makeString(getMemoryTag())), fname(makeString(getMemoryTag())), data_host(makeString(getMemoryTag())), data_port(makeString(getMemoryTag())) { mbedtls_net_init(&ctrl_fd); mbedtls_ssl_init(&ctrl_ssl); mbedtls_net_init(&data_fd); mbedtls_ssl_init(&data_ssl); mbedtls_ssl_config_init(&conf); mbedtls_x509_crt_init(&cacert); mbedtls_x509_crt_init(&clicert); mbedtls_pk_init(&pkey); mbedtls_ctr_drbg_init(&ctr_drbg); mbedtls_entropy_init(&entropy); } FtpTransferMbedTLS::~FtpTransferMbedTLS() { if (onClose) { onClose(MO_FtpCloseReason_Failure); //data connection not closed properly onClose = nullptr; } MO_FREE(data_buf); mbedtls_x509_crt_free(&clicert); mbedtls_x509_crt_free(&cacert); mbedtls_pk_free(&pkey); mbedtls_ssl_config_free(&conf); mbedtls_ctr_drbg_free(&ctr_drbg); mbedtls_entropy_free(&entropy); mbedtls_net_free(&ctrl_fd); mbedtls_ssl_free(&ctrl_ssl); mbedtls_net_free(&data_fd); mbedtls_ssl_free(&data_ssl); } int FtpTransferMbedTLS::setup_tls() { if (auto ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char*) __FILE__, strlen(__FILE__)) != 0) { MO_DBG_ERR("mbedtls_ctr_drbg_seed: %i", ret); return ret; } if (ca_cert) { if (auto ret = mbedtls_x509_crt_parse(&cacert, (const unsigned char *) ca_cert, strlen(ca_cert)) < 0) { MO_DBG_ERR("mbedtls_x509_crt_parse(ca_cert): %i", ret); return ret; } } if (client_cert) { if (auto ret = mbedtls_x509_crt_parse(&clicert, (const unsigned char *) client_cert, strlen(client_cert))) { MO_DBG_ERR("mbedtls_x509_crt_parse(client_cert): %i", ret); return ret; } } if (client_key) { if (auto ret = mbedtls_pk_parse_key(&pkey, (const unsigned char *) client_key, strlen(client_key), NULL, 0)) { MO_DBG_ERR("mbedtls_pk_parse_key: %i", ret); return ret; } } if (auto ret = mbedtls_ssl_config_defaults(&conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) != 0) { MO_DBG_ERR("mbedtls_ssl_config_defaults: %i", ret); return ret; } mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_OPTIONAL); //certificate check result manually handled for now mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &ctr_drbg); mbedtls_ssl_conf_dbg(&conf, mo_mbedtls_log, NULL); if (ca_cert) { mbedtls_ssl_conf_ca_chain(&conf, &cacert, NULL); } if (client_cert || client_key) { if (auto ret = mbedtls_ssl_conf_own_cert(&conf, &clicert, &pkey) != 0) { MO_DBG_ERR("mbedtls_ssl_conf_own_cert: %i", ret); return ret; } } return 0; //success } int FtpTransferMbedTLS::connect(mbedtls_net_context& fd, mbedtls_ssl_context& ssl, const char *server_name, const char *server_port) { if (auto ret = mbedtls_net_connect(&fd, server_name, server_port, MBEDTLS_NET_PROTO_TCP) != 0) { MO_DBG_ERR("mbedtls_net_connect: %i", ret); return ret; } if (auto ret = mbedtls_net_set_nonblock(&fd)) { MO_DBG_ERR("mbedtls_net_set_nonblock: %i", ret); return ret; } if (auto ret = mbedtls_ssl_setup(&ssl, &conf) != 0) { MO_DBG_ERR("mbedtls_ssl_setup: %i", ret); return ret; } if (auto ret = mbedtls_ssl_set_hostname(&ssl, server_name) != 0) { MO_DBG_ERR("mbedtls_ssl_set_hostname: %i", ret); return ret; } mbedtls_ssl_set_bio(&ssl, &fd, mbedtls_net_send, mbedtls_net_recv, NULL); return 0; //success } int FtpTransferMbedTLS::connect_ctrl() { if (auto ret = connect(ctrl_fd, ctrl_ssl, ctrl_host.c_str(), ctrl_port.c_str())) { MO_DBG_ERR("connect: %i", ret); return ret; } ctrl_opened = true; //handshake will be done later during STARTTLS procedure return 0; //success } int FtpTransferMbedTLS::connect_data() { if (auto ret = connect(data_fd, data_ssl, data_host.c_str(), data_port.c_str())) { MO_DBG_ERR("connect: %i", ret); return ret; } data_opened = true; if (isSecure) { //reuse SSL session of ctrl conn if (auto ret = mbedtls_ssl_set_session(&data_ssl, mbedtls_ssl_get_session_pointer(&ctrl_ssl))) { MO_DBG_ERR("session reuse failure: %i", ret); return ret; } data_ssl_established = true; } if (!data_buf) { data_buf = static_cast(MO_MALLOC(getMemoryTag(), data_buf_size)); if (!data_buf) { MO_DBG_ERR("OOM"); return -1; } memset(data_buf, 0, data_buf_size); } return 0; //success } void FtpTransferMbedTLS::close_ctrl() { if (!ctrl_opened) { return; } if (ctrl_ssl_established) { mbedtls_ssl_close_notify(&ctrl_ssl); ctrl_ssl_established = false; } mbedtls_net_free(&ctrl_fd); ctrl_opened = false; if (onClose && !data_opened) { onClose(MO_FtpCloseReason_Failure); //data connection has never been opened --> failure onClose = nullptr; } } void FtpTransferMbedTLS::close_data(MO_FtpCloseReason reason) { if (!data_opened) { return; } MO_DBG_DEBUG("closing data conn"); if (data_ssl_established) { MO_DBG_DEBUG("TLS shutdown"); mbedtls_ssl_close_notify(&data_ssl); data_ssl_established = false; } mbedtls_net_free(&data_fd); data_opened = false; data_conn_accepted = false; if (onClose) { onClose(reason); onClose = nullptr; } } int FtpTransferMbedTLS::handshake_tls() { int ret; while ((ret = mbedtls_ssl_handshake(&ctrl_ssl)) != 0) { if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE && ret != 1) { char buf [1024]; mbedtls_strerror(ret, (char *) buf, 1024); MO_DBG_ERR("mbedtls_ssl_handshake: %i, %s", ret, buf); return ret; } } if (ca_cert) { //certificate validation enabled if ((ret = mbedtls_ssl_get_verify_result(&ctrl_ssl)) != 0) { char vrfy_buf[512]; mbedtls_x509_crt_verify_info(vrfy_buf, sizeof(vrfy_buf), " > ", ret); MO_DBG_ERR("mbedtls_ssl_get_verify_result: %i, %s", ret, vrfy_buf); return ret; } } ctrl_ssl_established = true; return 0; //success } void FtpTransferMbedTLS::send_cmd(const char *cmd, const char *arg, bool disable_tls_policy) { const size_t MSG_SIZE = 128; unsigned char msg [MSG_SIZE]; auto len = snprintf((char*) msg, MSG_SIZE, "%s%s%s\r\n", cmd, //cmd mandatory (e.g. "USER") arg ? " " : "", //line spacing if arg is provided arg ? arg : ""); //arg optional (e.g. "anonymous") if (len < 0 || (size_t)len >= MSG_SIZE) { MO_DBG_ERR("could not write cmd, send QUIT instead"); len = sprintf((char*) msg, "QUIT\r\n"); } else { //show outgoing traffic for debug, but shadow PASS MO_DBG_DEBUG("SEND: %s %s", cmd, !strncmp((char*) cmd, "PASS", strlen("PASS")) ? "***" : arg ? (char*) arg : ""); } int ret = -1; if (ctrl_ssl_established) { ret = mbedtls_ssl_write(&ctrl_ssl, (unsigned char*) msg, len); } else if (!isSecure || disable_tls_policy) { ret = mbedtls_net_send(&ctrl_fd, (unsigned char*) msg, len); } else { MO_DBG_ERR("TLS policy failure"); len = strlen("QUIT\r\n"); ret = mbedtls_net_send(&ctrl_fd, (unsigned char*) "QUIT\r\n", len); } if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE || ret <= 0 || ret < (int) len) { char buf [1024]; mbedtls_strerror(ret, (char *) buf, 1024); MO_DBG_ERR("fatal - message on ctrl channel lost: %i, %s", ret, buf); close_ctrl(); return; } } bool FtpTransferMbedTLS::getFile(const char *ftp_url_raw, std::function fileWriter, std::function onClose, const char *ca_cert) { if (method != Method::UNDEFINED) { MO_DBG_ERR("FTP Client reuse not supported"); return false; } if (!ftp_url_raw || !fileWriter) { MO_DBG_ERR("invalid args"); return false; } this->ca_cert = ca_cert; this->method = Method::Retrieve; this->fileWriter = fileWriter; this->onClose = onClose; if (!read_url_ctrl(ftp_url_raw)) { MO_DBG_ERR("could not parse URL"); return false; } MO_DBG_DEBUG("init download from %s: %s", ctrl_host.c_str(), fname.c_str()); if (auto ret = setup_tls()) { MO_DBG_ERR("could not setup MbedTLS: %i", ret); return false; } if (auto ret = connect_ctrl()) { MO_DBG_ERR("could not establish connection to FTP server: %i", ret); return false; } return true; } bool FtpTransferMbedTLS::postFile(const char *ftp_url_raw, std::function fileReader, std::function onClose, const char *ca_cert) { if (method != Method::UNDEFINED) { MO_DBG_ERR("FTP Client reuse not supported"); return false; } if (!ftp_url_raw || !fileReader) { MO_DBG_ERR("invalid args"); return false; } MO_DBG_DEBUG("init upload %s", ftp_url_raw); this->ca_cert = ca_cert; this->method = Method::Store; this->fileReader = fileReader; this->onClose = onClose; if (!read_url_ctrl(ftp_url_raw)) { MO_DBG_ERR("could not parse URL"); return false; } if (auto ret = setup_tls()) { MO_DBG_ERR("could not setup MbedTLS: %i", ret); return false; } if (auto ret = connect_ctrl()) { MO_DBG_ERR("could not establish connection to FTP server: %i", ret); return false; } return true; } void FtpTransferMbedTLS::process_ctrl() { // read input (if available) const size_t INBUF_SIZE = 128; unsigned char inbuf [INBUF_SIZE]; memset(inbuf, 0, INBUF_SIZE); int ret = -1; if (ctrl_ssl_established) { ret = mbedtls_ssl_read(&ctrl_ssl, inbuf, INBUF_SIZE - 1); } else { ret = mbedtls_net_recv(&ctrl_fd, inbuf, INBUF_SIZE - 1); } if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { //no new input data to be processed return; } else if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || ret == 0) { MO_DBG_ERR("FTP transfer aborted"); close_ctrl(); return; } else if (ret < 0) { MO_DBG_ERR("mbedtls_net_recv: %i", ret); send_cmd("QUIT"); close_ctrl(); return; } size_t inbuf_len = ret; // read multi-line command char *line_next = (char*) inbuf; while (line_next < (char*) inbuf + inbuf_len) { // take current line char *line = line_next; // null-terminate current line and find begin of next line while (line_next + 1 < (char*) inbuf + inbuf_len && *line_next != '\n') { line_next++; } *line_next = '\0'; line_next++; MO_DBG_DEBUG("RECV: %s", line); if (isSecure && !ctrl_ssl_established) { //tls not established yet, set up according to RFC 4217 if (!strncmp("220", line, 3)) { MO_DBG_DEBUG("start TLS negotiation"); send_cmd("AUTH TLS", nullptr, true); return; } else if (!strncmp("234", line, 3)) { // Proceed with TLS negotiation MO_DBG_DEBUG("upgrade to TLS"); if (auto ret = handshake_tls()) { MO_DBG_ERR("handshake: %i", ret); send_cmd("QUIT", nullptr, true); return; } } else { MO_DBG_ERR("cannot proceed without TLS"); send_cmd("QUIT", nullptr, true); return; } } if (isSecure && !ctrl_ssl_established) { //failure to establish security policy MO_DBG_ERR("internal error"); send_cmd("QUIT", nullptr, true); return; } //security policy met if (!strncmp("530", line, 3) // Not logged in || !strncmp("220", line, 3) // Service ready for new user || !strncmp("234", line, 3)) { // Just completed AUTH TLS handshake MO_DBG_DEBUG("select user %s", user.empty() ? "anonymous" : user.c_str()); send_cmd("USER", user.empty() ? "anonymous" : user.c_str()); } else if (!strncmp("331", line, 3)) { // User name okay, need password MO_DBG_DEBUG("enter pass %.2s***", pass.empty() ? "-" : pass.c_str()); send_cmd("PASS", pass.c_str()); } else if (!strncmp("230", line, 3)) { // User logged in, proceed MO_DBG_DEBUG("select directory %s", dir.empty() ? "/" : dir.c_str()); send_cmd("CWD", dir.empty() ? "/" : dir.c_str()); } else if (!strncmp("250", line, 3)) { // Requested file action okay, completed MO_DBG_DEBUG("enter passive mode"); if (isSecure) { send_cmd("PBSZ 0\r\n" "PROT P\r\n" //RFC 4217: set FTP session Private "PASV"); } else { send_cmd("PASV"); } } else if (!strncmp("227", line, 3)) { // Entering Passive Mode (h1,h2,h3,h4,p1,p2) if (!read_url_data(line + 3)) { //trim leading response code MO_DBG_ERR("could not process data url. Expect format: (h1,h2,h3,h4,p1,p2)"); send_cmd("QUIT"); return; } if (auto ret = connect_data()) { MO_DBG_ERR("data connection failure: %i", ret); send_cmd("QUIT"); return; } if (method == Method::Retrieve) { MO_DBG_DEBUG("request download for %s", fname.c_str()); send_cmd("RETR", fname.c_str()); } else if (method == Method::Store) { MO_DBG_DEBUG("request upload for %s", fname.c_str()); send_cmd("STOR", fname.c_str()); } else { MO_DBG_ERR("internal error"); send_cmd("QUIT"); return; } } else if (!strncmp("150", line, 3) // File status okay; about to open data connection || !strncmp("125", line, 3)) { // Data connection already open MO_DBG_DEBUG("data connection accepted"); data_conn_accepted = true; } else if (!strncmp("226", line, 3)) { // Closing data connection. Requested file action successful (for example, file transfer or file abort) MO_DBG_INFO("FTP success: %s", line); send_cmd("QUIT"); return; } else if (!strncmp("55", line, 2)) { // Requested action not taken / aborted MO_DBG_WARN("FTP failure: %s", line); send_cmd("QUIT"); return; } else if (!strncmp("200", line, 3)) { //PBSZ -> 0 and PROT -> P accepted MO_DBG_INFO("PBSZ/PROT success: %s", line); } else if (!strncmp("221", line, 3)) { // Server Goodbye MO_DBG_DEBUG("closing ctrl connection"); close_ctrl(); return; } else { MO_DBG_WARN("unkown commad (close connection): %s", line); send_cmd("QUIT"); return; } } } void FtpTransferMbedTLS::process_data() { if (!data_conn_accepted) { return; } if (isSecure && !data_ssl_established) { //failure to establish security policy MO_DBG_ERR("internal error"); close_data(MO_FtpCloseReason_Failure); send_cmd("QUIT", nullptr, true); return; } if (method == Method::Retrieve) { if (data_buf_avail == 0) { //load new data from socket data_buf_offs = 0; int ret = -1; if (data_ssl_established) { ret = mbedtls_ssl_read(&data_ssl, data_buf, data_buf_size - 1); } else { ret = mbedtls_net_recv(&data_fd, data_buf, data_buf_size - 1); } if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { //no new input data to be processed return; } else if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || ret == 0) { //download finished close_data(MO_FtpCloseReason_Success); return; } else if (ret < 0) { MO_DBG_ERR("mbedtls_net_recv: %i", ret); close_data(MO_FtpCloseReason_Failure); send_cmd("QUIT"); return; } data_buf_avail = ret; } auto ret = fileWriter(data_buf + data_buf_offs, data_buf_avail); if (ret == 0) { MO_DBG_ERR("fileWriter aborted download"); close_data(MO_FtpCloseReason_Failure); send_cmd("QUIT"); return; } else if (ret <= data_buf_avail) { data_buf_avail -= ret; data_buf_offs += ret; } else { MO_DBG_ERR("write error"); close_data(MO_FtpCloseReason_Failure); send_cmd("QUIT"); return; } //success } else if (method == Method::Store) { if (data_buf_avail == 0) { //load new data from file to write on socket data_buf_offs = 0; data_buf_avail = fileReader(data_buf, data_buf_size); } if (data_buf_avail > 0) { int ret = -1; if (data_ssl_established) { ret = mbedtls_ssl_write(&data_ssl, data_buf + data_buf_offs, data_buf_avail); } else { ret = mbedtls_net_send(&data_fd, data_buf + data_buf_offs, data_buf_avail); } if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { //no data sent, wait return; } else if (ret <= 0) { MO_DBG_ERR("mbedtls_ssl_write: %i", ret); close_data(MO_FtpCloseReason_Failure); send_cmd("QUIT"); return; } //successful write data_buf_avail -= ret; data_buf_offs += ret; } else { //no data in fileReader anymore MO_DBG_DEBUG("finished file reading"); close_data(MO_FtpCloseReason_Success); } } } void FtpTransferMbedTLS::loop() { if (ctrl_opened) { process_ctrl(); } if (data_opened) { process_data(); } } bool FtpTransferMbedTLS::isActive() { return ctrl_opened || data_opened; } bool FtpTransferMbedTLS::read_url_ctrl(const char *ftp_url_raw) { String ftp_url = makeString(getMemoryTag(), ftp_url_raw); //copy input ftp_url //tolower protocol specifier for (auto c = ftp_url.begin(); *c != ':' && c != ftp_url.end(); c++) { *c = tolower(*c); } //parse FTP URL: protocol specifier String proto = makeString(getMemoryTag()); if (!strncmp(ftp_url.c_str(), "ftps://", strlen("ftps://"))) { //FTP over TLS (RFC 4217) proto = "ftps://"; isSecure = true; //TLS policy } else if (!strncmp(ftp_url.c_str(), "ftp://", strlen("ftp://"))) { //FTP without security policies (RFC 959) proto = "ftp://"; } else { MO_DBG_ERR("protocol not supported. Please use ftps:// or ftp://"); return false; } //parse FTP URL: dir and fname auto dir_pos = ftp_url.find_first_of('/', proto.length()); if (dir_pos != String::npos) { auto fname_pos = ftp_url.find_last_of('/'); dir = ftp_url.substr(dir_pos, fname_pos - dir_pos); fname = ftp_url.substr(fname_pos + 1); } if (fname.empty()) { MO_DBG_ERR("missing filename"); return false; } MO_DBG_DEBUG("parsed dir: %s; fname: %s", dir.c_str(), fname.c_str()); //parse FTP URL: user, pass, host, port String user_pass_host_port = ftp_url.substr(proto.length(), dir_pos - proto.length()); String user_pass = makeString(getMemoryTag()); String host_port = makeString(getMemoryTag()); auto user_pass_delim = user_pass_host_port.find_first_of('@'); if (user_pass_delim != String::npos) { host_port = user_pass_host_port.substr(user_pass_delim + 1); user_pass = user_pass_host_port.substr(0, user_pass_delim); } else { host_port = user_pass_host_port; } if (!user_pass.empty()) { auto user_delim = user_pass.find_first_of(':'); if (user_delim != String::npos) { user = user_pass.substr(0, user_delim); pass = user_pass.substr(user_delim + 1); } else { user = user_pass; } } MO_DBG_DEBUG("parsed user: %s; pass: %.2s***", user.c_str(), pass.empty() ? "-" : pass.c_str()); if (host_port.empty()) { MO_DBG_ERR("missing hostname"); return false; } auto host_port_delim = host_port.find(':'); if (host_port_delim != String::npos) { ctrl_host = host_port.substr(0, host_port_delim); ctrl_port = host_port.substr(host_port_delim + 1); } else { //use default port number ctrl_host = host_port; ctrl_port = "21"; } MO_DBG_DEBUG("parsed host: %s; port: %s", ctrl_host.c_str(), ctrl_port.c_str()); return true; } bool FtpTransferMbedTLS::read_url_data(const char *data_url_raw) { String data_url = makeString(getMemoryTag(), data_url_raw); //format like " Entering Passive Mode (h1,h2,h3,h4,p1,p2)" // parse address field. Replace all non-digits by delimiter character ' ' for (char& c : data_url) { if (c < '0' || c > '9') { c = (unsigned char) ' '; } } unsigned int h1 = 0, h2 = 0, h3 = 0, h4 = 0, p1 = 0, p2 = 0; auto ntokens = sscanf(data_url.c_str(), "%u %u %u %u %u %u", &h1, &h2, &h3, &h4, &p1, &p2); if (ntokens != 6) { MO_DBG_ERR("could not process data url. Expect format: (h1,h2,h3,h4,p1,p2)"); return false; } unsigned int port = 256U * p1 + p2; char buf [64] = {'\0'}; auto ret = snprintf(buf, 64, "%u.%u.%u.%u", h1, h2, h3, h4); if (ret < 0 || ret >= 64) { MO_DBG_ERR("data url format failure"); return false; } data_host = buf; ret = snprintf(buf, 64, "%u", port); if (ret < 0 || ret >= 64) { MO_DBG_ERR("data url format failure"); return false; } data_port = buf; return true; } FtpClientMbedTLS::FtpClientMbedTLS(bool tls_only, const char *client_cert, const char *client_key) : MemoryManaged("FTP.ClientMbedTLS"), client_cert(client_cert), client_key(client_key), tls_only(tls_only) { } std::unique_ptr FtpClientMbedTLS::getFile(const char *ftp_url_raw, std::function fileWriter, std::function onClose, const char *ca_cert) { auto ftp_handle = std::unique_ptr(new FtpTransferMbedTLS(tls_only, client_cert, client_key)); if (!ftp_handle) { MO_DBG_ERR("OOM"); return nullptr; } bool success = ftp_handle->getFile(ftp_url_raw, fileWriter, onClose, ca_cert); if (success) { return ftp_handle; } else { return nullptr; } } std::unique_ptr FtpClientMbedTLS::postFile(const char *ftp_url_raw, std::function fileReader, std::function onClose, const char *ca_cert) { auto ftp_handle = std::unique_ptr(new FtpTransferMbedTLS(tls_only, client_cert, client_key)); if (!ftp_handle) { MO_DBG_ERR("OOM"); return nullptr; } bool success = ftp_handle->postFile(ftp_url_raw, fileReader, onClose, ca_cert); if (success) { return ftp_handle; } else { return nullptr; } } } //namespace MicroOcpp #endif //MO_ENABLE_MBEDTLS ================================================ FILE: src/MicroOcpp/Core/FtpMbedTLS.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_FTP_MBEDTLS_H #define MO_FTP_MBEDTLS_H /* * Built-in FTP client (depends on MbedTLS) * * Moved from https://github.com/matth-x/MicroFtp * * Currently, the compatibility with the following FTP servers has been tested: * * | Server | FTP | FTPS | * | --------------------------------------------------------------------- | --- | ---- | * | [vsftp](https://security.appspot.com/vsftpd.html) | | x | * | [Rebex](https://www.rebex.net/) | x | x | * | [Windows Server 2022](https://www.microsoft.com/en-us/windows-server) | x | x | * | [SFTPGo](https://github.com/drakkan/sftpgo) | x | | * */ #include #if MO_ENABLE_MBEDTLS #include #include namespace MicroOcpp { std::unique_ptr makeFtpClientMbedTLS(bool tls_only = false, const char *client_cert = nullptr, const char *client_key = nullptr); } //namespace MicroOcpp #endif //MO_ENABLE_MBEDTLS #endif ================================================ FILE: src/MicroOcpp/Core/Memory.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #if MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER #include namespace MicroOcpp { namespace Memory { struct MemBlockInfo { void* tagger_ptr = nullptr; std::string tag; size_t size = 0; MemBlockInfo(void* ptr, const char *tag, size_t size) : size{size} { updateTag(ptr, tag); } void updateTag(void* ptr, const char *tag); }; std::map memBlocks; //key: memory address of malloc'd block struct MemTagInfo { size_t current_size = 0; size_t max_size = 0; MemTagInfo(size_t size) { operator+=(size); } void operator+=(size_t size) { current_size += size; max_size = std::max(max_size, current_size); } void operator-=(size_t size) { if (size > current_size) { MO_DBG_ERR("captured size does not fit"); //return; let it happen for now } current_size -= size; } void reset() { max_size = current_size; } }; std::map memTags; size_t memTotal, memTotalMax; void MemBlockInfo::updateTag(void* ptr, const char *tag) { if (!tag) { return; } if (tagger_ptr == nullptr || ptr < tagger_ptr) { MO_DBG_VERBOSE("update tag from %s to %s, ptr from %p to %p", this->tag.c_str(), tag, tagger_ptr, ptr); auto tagInfo = memTags.find(this->tag); if (tagInfo != memTags.end()) { tagInfo->second -= size; } tagInfo = memTags.find(tag); if (tagInfo != memTags.end()) { tagInfo->second += size; } else { memTags.emplace(tag, size); } tagger_ptr = ptr; this->tag = tag; } } } //namespace Memory } //namespace MicroOcpp #endif //MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER #if MO_OVERRIDE_ALLOCATION namespace MicroOcpp { namespace Memory { void* (*malloc_override)(size_t); void (*free_override)(void*); } } using namespace MicroOcpp::Memory; void mo_mem_set_malloc_free(void* (*malloc_override)(size_t), void (*free_override)(void*)) { MicroOcpp::Memory::malloc_override = malloc_override; MicroOcpp::Memory::free_override = free_override; } void *mo_mem_malloc(const char *tag, size_t size) { MO_DBG_VERBOSE("malloc %zu B (%s)", size, tag ? tag : "unspecified"); void *ptr; if (malloc_override) { ptr = malloc_override(size); } else { ptr = malloc(size); } #if MO_ENABLE_HEAP_PROFILER if (ptr) { memBlocks.emplace(ptr, MemBlockInfo(ptr, tag, size)); memTotal += size; memTotalMax = std::max(memTotalMax, memTotal); } #endif return ptr; } void mo_mem_free(void* ptr) { MO_DBG_VERBOSE("free"); #if MO_ENABLE_HEAP_PROFILER if (ptr) { auto blockInfo = memBlocks.find(ptr); if (blockInfo != memBlocks.end()) { auto tagInfo = memTags.find(blockInfo->second.tag); if (tagInfo != memTags.end()) { tagInfo->second -= blockInfo->second.size; } memTotal -= blockInfo->second.size; } if (blockInfo != memBlocks.end()) { memBlocks.erase(blockInfo); } } #endif if (free_override) { free_override(ptr); } else { free(ptr); } } #endif //MO_OVERRIDE_ALLOCATION #if MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER void mo_mem_deinit() { memBlocks.clear(); memTags.clear(); } void mo_mem_reset() { MO_DBG_DEBUG("Reset all maximum values to current values"); for (auto tagInfo = (memTags).begin(); tagInfo != memTags.end(); ++tagInfo) { tagInfo->second.reset(); } memTotalMax = memTotal; } void mo_mem_set_tag(void *ptr, const char *tag) { MO_DBG_VERBOSE("set tag (%s)", tag ? tag : "unspecified"); if (!tag) { return; } bool hasTagged = false; if (tag) { auto foundBlock = memBlocks.upper_bound(ptr); if (foundBlock != memBlocks.begin()) { --foundBlock; } if (foundBlock != memBlocks.end() && (unsigned char*)ptr - (unsigned char*)foundBlock->first < (std::ptrdiff_t)foundBlock->second.size) { foundBlock->second.updateTag(ptr, tag); hasTagged = true; } } if (!hasTagged) { MO_DBG_VERBOSE("memory area doesn't apply"); } } void mo_mem_print_stats() { MO_CONSOLE_PRINTF("\n *** Heap usage statistics ***\n"); size_t size = 0; size_t untagged = 0, untagged_size = 0; for (const auto& heapEntry : memBlocks) { size += heapEntry.second.size; #if MO_DBG_LEVEL >= MO_DL_VERBOSE { MO_CONSOLE_PRINTF("@%p - %zu B (%s)\n", heapEntry.first, heapEntry.second.size, heapEntry.second.tag.c_str()); } #endif if (heapEntry.second.tag.empty()) { untagged ++; untagged_size += heapEntry.second.size; } } std::map tags; for (const auto& heapEntry : memBlocks) { auto foundTag = tags.find(heapEntry.second.tag); if (foundTag != tags.end()) { foundTag->second += heapEntry.second.size; } else { tags.emplace(heapEntry.second.tag, heapEntry.second.size); } } size_t size_control = 0; for (const auto& tag : tags) { size_control += tag.second; #if MO_DBG_LEVEL >= MO_DL_VERBOSE { MO_CONSOLE_PRINTF("%s - %zu B\n", tag.first.c_str(), tag.second); } #endif } size_t size_control2 = 0; for (const auto& tag : memTags) { size_control2 += tag.second.current_size; MO_CONSOLE_PRINTF("%s - %zu B (max. %zu B)\n", tag.first.c_str(), tag.second.current_size, tag.second.max_size); } MO_CONSOLE_PRINTF(" *** Summary ***\nBlocks: %zu\nTags: %zu\nCurrent usage: %zu B\nMaximum usage: %zu B\n", memBlocks.size(), memTags.size(), memTotal, memTotalMax); #if MO_DBG_LEVEL >= MO_DL_DEBUG { MO_CONSOLE_PRINTF(" *** Debug information ***\nTotal blocks (control value 1): %zu B\nTags (control value): %zu\nTotal tagged (control value 2): %zu B\nTotal tagged (control value 3): %zu B\nUntagged: %zu\nTotal untagged: %zu B\n", size, tags.size(), size_control, size_control2, untagged, untagged_size); } #endif } int mo_mem_write_stats_json(char *buf, size_t size) { DynamicJsonDocument doc {size * 2}; doc["total_current"] = memTotal; doc["total_max"] = memTotalMax; doc["total_blocks"] = memBlocks.size(); JsonArray by_tag = doc.createNestedArray("by_tag"); for (const auto& tag : memTags) { JsonObject entry = by_tag.createNestedObject(); entry["tag"] = tag.first.c_str(); entry["current"] = tag.second.current_size; entry["max"] = tag.second.max_size; } size_t untagged = 0, untagged_size = 0; for (const auto& heapEntry : memBlocks) { if (heapEntry.second.tag.empty()) { untagged ++; untagged_size += heapEntry.second.size; } } doc["untagged_blocks"] = untagged; doc["untagged_size"] = untagged_size; if (doc.overflowed()) { MO_DBG_ERR("exceeded JSON capacity"); return -1; } return (int)serializeJson(doc, buf, size); } #endif //MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER namespace MicroOcpp { String makeString(const char *tag, const char *val) { #if MO_OVERRIDE_ALLOCATION if (val) { return String(val, Allocator(tag)); } else { return String(Allocator(tag)); } #else if (val) { return String(val); } else { return String(); } #endif } JsonDoc initJsonDoc(const char *tag, size_t capacity) { #if MO_OVERRIDE_ALLOCATION return JsonDoc(capacity, ArduinoJsonAllocator(tag)); #else return JsonDoc(capacity); #endif } std::unique_ptr makeJsonDoc(const char *tag, size_t capacity) { #if MO_OVERRIDE_ALLOCATION return std::unique_ptr(new JsonDoc(capacity, ArduinoJsonAllocator(tag))); #else return std::unique_ptr(new JsonDoc(capacity)); #endif } } ================================================ FILE: src/MicroOcpp/Core/Memory.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_MEMORY_H #define MO_MEMORY_H #include #ifndef MO_OVERRIDE_ALLOCATION #define MO_OVERRIDE_ALLOCATION 0 #endif #ifndef MO_ENABLE_EXTERNAL_RAM #define MO_ENABLE_EXTERNAL_RAM 0 #endif #ifndef MO_ENABLE_HEAP_PROFILER #define MO_ENABLE_HEAP_PROFILER 0 #endif #ifdef __cplusplus extern "C" { #endif #if MO_OVERRIDE_ALLOCATION void mo_mem_set_malloc_free(void* (*malloc_override)(size_t), void (*free_override)(void*)); //pass custom malloc and free function to be used with the OCPP lib. If not set or NULL, defaults to standard malloc void *mo_mem_malloc(const char *tag, size_t size); void mo_mem_free(void* ptr); #define MO_MALLOC mo_mem_malloc #define MO_FREE mo_mem_free #else #define MO_MALLOC(TAG, SIZE) malloc(SIZE) //default malloc provided by host system #define MO_FREE(PTR) free(PTR) //default free provided by host system #endif //MO_OVERRIDE_ALLOCATION #if MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER void mo_mem_deinit(); //release allocated memory and deinit void mo_mem_reset(); //reset maximum heap occuption void mo_mem_set_tag(void *ptr, const char *tag); void mo_mem_get_current_heap(const char *tag); void mo_mem_get_maximum_heap(const char *tag); void mo_mem_get_current_heap_by_tag(const char *tag); void mo_mem_get_maximum_heap_by_tag(const char *tag); int mo_mem_write_stats_json(char *buf, size_t size); void mo_mem_print_stats(); #define MO_MEM_DEINIT mo_mem_deinit #define MO_MEM_RESET mo_mem_reset #define MO_MEM_SET_TAG mo_mem_set_tag #define MO_MEM_PRINT_STATS mo_mem_print_stats #else #define MO_MEM_DEINIT(...) (void)0 #define MO_MEM_RESET(...) (void)0 #define MO_MEM_SET_TAG(...) (void)0 #define MO_MEM_PRINT_STATS(...) (void)0 #endif //MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER #if MO_ENABLE_EXTERNAL_RAM void mo_mem_set_malloc_free_ext(void* (*malloc_override)(size_t), void (*free_override)(void*)); //pass malloc and free function to external RAM to be used with the OCPP lib. If not set or NULL, defaults to standard malloc void *mo_mem_malloc_ext(const char *tag, size_t size); void mo_mem_free_ext(void* ptr); #define MO_MALLOC_EXT(TAG, SIZE) mo_mem_malloc_ext(TAG, SIZE) #define MO_FREE_EXT(PTR) mo_mem_free_ext(PTR) #else #define MO_MALLOC_EXT MO_MALLOC #define MO_FREE_EXT MO_FREE #endif //MO_ENABLE_EXTERNAL_RAM #ifdef __cplusplus } #include #include #include #include #if MO_OVERRIDE_ALLOCATION #include namespace MicroOcpp { class MemoryManaged { private: #if MO_ENABLE_HEAP_PROFILER char *tag = nullptr; #endif protected: void updateMemoryTag(const char *src1, const char *src2 = nullptr) { #if MO_ENABLE_HEAP_PROFILER if (!src1 && !src2) { //empty source does not update tag return; } char src [64]; snprintf(src, sizeof(src), "%s%s", src1 ? src1 : "", src2 ? src2 : ""); if (tag) { if (!strcmp(src, tag)) { //nothing to do return; } MO_FREE(tag); tag = nullptr; } size_t size = strlen(src) + 1; tag = static_cast(malloc(size)); //heap profiler bypasses custom malloc to not count into the statistics memset(tag, 0, size); snprintf(tag, size, "%s", src); mo_mem_set_tag(this, tag); #else (void)src1; (void)src2; #endif } const char *getMemoryTag() const { #if MO_ENABLE_HEAP_PROFILER return tag; #else return nullptr; #endif } public: void *operator new(size_t size) { return MO_MALLOC(nullptr, size); } void operator delete(void * ptr) { MO_FREE(ptr); } MemoryManaged(const char *tag = nullptr, const char *tag_suffix = nullptr) { #if MO_ENABLE_HEAP_PROFILER updateMemoryTag(tag, tag_suffix); #endif } MemoryManaged(MemoryManaged&& other) { #if MO_ENABLE_HEAP_PROFILER tag = other.tag; other.tag = nullptr; #endif } ~MemoryManaged() { #if MO_ENABLE_HEAP_PROFILER MO_FREE(tag); tag = nullptr; #endif } void operator=(const MemoryManaged& other) { #if MO_ENABLE_HEAP_PROFILER updateMemoryTag(other.tag); #endif } }; template struct Allocator { Allocator(const char *tag = nullptr, const char *tag_suffix = nullptr) { #if MO_ENABLE_HEAP_PROFILER updateMemoryTag(tag, tag_suffix); #endif } template Allocator(const Allocator& other) { #if MO_ENABLE_HEAP_PROFILER updateMemoryTag(other.tag); #endif } Allocator(const Allocator& other) { #if MO_ENABLE_HEAP_PROFILER updateMemoryTag(other.tag); #endif } //template //Allocator(Allocator&& other) { Allocator(Allocator&& other) { #if MO_ENABLE_HEAP_PROFILER updateMemoryTag(other.tag); //ignore move semantics for allocators as it simplifies moving std::vector>. This is okay because the Allocator's state is only the memory tag which is not exclusively owned #endif } ~Allocator() { #if MO_ENABLE_HEAP_PROFILER if (tag) { //MO_FREE(tag); free(tag); tag = nullptr; } #endif } T *allocate(size_t count) { #if MO_ENABLE_HEAP_PROFILER return static_cast(MO_MALLOC(tag, sizeof(T) * count)); #else return static_cast(MO_MALLOC(nullptr, sizeof(T) * count)); #endif } void deallocate(T *ptr, size_t count) { MO_FREE(ptr); } bool operator==(const Allocator& other) { #if MO_ENABLE_HEAP_PROFILER if (!tag && !other.tag) { return true; } else if (tag && other.tag) { return !strcmp(tag, other.tag); } else { return false; } #else return true; #endif } bool operator!=(const Allocator& other) { return !operator==(other); } typedef T value_type; #if MO_ENABLE_HEAP_PROFILER char *tag = nullptr; void updateMemoryTag(const char *src1, const char *src2 = nullptr) { if (!src1 && !src2) { //empty source does not update tag return; } char src [64]; snprintf(src, sizeof(src), "%s%s", src1 ? src1 : "", src2 ? src2 : ""); if (tag) { if (!strcmp(src, tag)) { //nothing to do return; } //MO_FREE(tag); free(tag); tag = nullptr; } size_t size = strlen(src) + 1; tag = static_cast(malloc(size)); memset(tag, 0, size); snprintf(tag, size, "%s", src); } #endif }; template Allocator makeAllocator(const char *tag, const char *tag_suffix = nullptr) { return Allocator(tag, tag_suffix); } using String = std::basic_string, MicroOcpp::Allocator>; template using Vector = std::vector>; template Vector makeVector(const char *tag) { return Vector(Allocator(tag)); } class ArduinoJsonAllocator { private: #if MO_ENABLE_HEAP_PROFILER char *tag = nullptr; void updateMemoryTag(const char *src1, const char *src2 = nullptr) { if (!src1 && !src2) { //empty source does not update tag return; } char src [64]; snprintf(src, sizeof(src), "%s%s", src1 ? src1 : "", src2 ? src2 : ""); if (tag) { if (!strcmp(src, tag)) { //nothing to do return; } MO_FREE(tag); tag = nullptr; } size_t size = strlen(src) + 1; //tag = static_cast(MO_MALLOC("HeapProfilerInternal", size)); tag = static_cast(malloc(size)); memset(tag, 0, size); snprintf(tag, size, "%s", src); } #endif public: ArduinoJsonAllocator(const char *tag = nullptr, const char *tag_suffix = nullptr) { #if MO_ENABLE_HEAP_PROFILER updateMemoryTag(tag, tag_suffix); #endif } ArduinoJsonAllocator(const ArduinoJsonAllocator& other) { #if MO_ENABLE_HEAP_PROFILER updateMemoryTag(other.tag); #endif } ArduinoJsonAllocator(ArduinoJsonAllocator&& other) { #if MO_ENABLE_HEAP_PROFILER tag = other.tag; other.tag = nullptr; #endif } ~ArduinoJsonAllocator() { #if MO_ENABLE_HEAP_PROFILER if (tag) { MO_FREE(tag); tag = nullptr; } #endif } void *allocate(size_t size) { #if MO_ENABLE_HEAP_PROFILER return MO_MALLOC(tag, size); #else return MO_MALLOC(nullptr, size); #endif } void deallocate(void *ptr) { MO_FREE(ptr); } }; using JsonDoc = BasicJsonDocument; template T *mo_mem_new(const char *tag, Args&& ...args) { if (auto ptr = MO_MALLOC(tag, sizeof(T))) { return new(ptr) T(std::forward(args)...); } return nullptr; //OOM } template void mo_mem_delete(T *ptr) { if (ptr) { ptr->~T(); MO_FREE(ptr); } } } //namespace MicroOcpp #else #include namespace MicroOcpp { class MemoryManaged { protected: const char *getMemoryTag() const {return nullptr;} void updateMemoryTag(const char*,const char*) { } public: MemoryManaged() { } MemoryManaged(const char*) { } MemoryManaged(const char*,const char*) { } }; template using Allocator = ::std::allocator; template Allocator makeAllocator(const char *, const char *unused = nullptr) { (void)unused; return Allocator(); } using String = std::string; template using Vector = std::vector; template Vector makeVector(const char *tag) { return Vector(); } using JsonDoc = DynamicJsonDocument; template T *mo_mem_new(Args&& ...args) { return new T(std::forward(args)...); } template void mo_mem_delete(T *ptr) { delete ptr; } } //namespace MicroOcpp #endif //MO_OVERRIDE_ALLOCATION namespace MicroOcpp { String makeString(const char *tag, const char *val = nullptr); JsonDoc initJsonDoc(const char *tag, size_t capacity = 0); std::unique_ptr makeJsonDoc(const char *tag, size_t capacity = 0); } #endif //__cplusplus #endif ================================================ FILE: src/MicroOcpp/Core/OcppError.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_OCPPERROR_H #define MO_OCPPERROR_H #include #include namespace MicroOcpp { class NotImplemented : public Operation, public MemoryManaged { public: NotImplemented() : MemoryManaged("v16.CallError.", "NotImplemented") { } const char *getErrorCode() override { return "NotImplemented"; } }; class MsgBufferExceeded : public Operation, public MemoryManaged { private: size_t maxCapacity; size_t msgLen; public: MsgBufferExceeded(size_t maxCapacity, size_t msgLen) : MemoryManaged("v16.CallError.", "GenericError"), maxCapacity(maxCapacity), msgLen(msgLen) { } const char *getErrorCode() override { return "GenericError"; } const char *getErrorDescription() override { return "JSON too long or too many fields. Cannot deserialize"; } std::unique_ptr getErrorDetails() override { auto errDoc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2)); JsonObject err = errDoc->to(); err["max_capacity"] = maxCapacity; err["msg_length"] = msgLen; return errDoc; } }; } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Core/Operation.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include using namespace MicroOcpp; Operation::Operation() {} Operation::~Operation() {} const char* Operation::getOperationType(){ MO_DBG_ERR("Unsupported operation: getOperationType() is not implemented"); return "CustomOperation"; } std::unique_ptr Operation::createReq() { MO_DBG_ERR("Unsupported operation: createReq() is not implemented"); return createEmptyDocument(); } void Operation::processConf(JsonObject payload) { MO_DBG_ERR("Unsupported operation: processConf() is not implemented"); } void Operation::processReq(JsonObject payload) { MO_DBG_ERR("Unsupported operation: processReq() is not implemented"); } std::unique_ptr Operation::createConf() { MO_DBG_ERR("Unsupported operation: createConf() is not implemented"); return createEmptyDocument(); } std::unique_ptr MicroOcpp::createEmptyDocument() { auto emptyDoc = makeJsonDoc("EmptyJsonDoc", 0); emptyDoc->to(); return emptyDoc; } ================================================ FILE: src/MicroOcpp/Core/Operation.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License /** * This framework considers OCPP operations to be a combination of two things: first, ensure that the message reaches its * destination properly (the "Remote procedure call" header, e.g. message Id). Second, transmit the application data * as specified in the OCPP 1.6 document. * * The remote procedure call (RPC) part is implemented by the class Request. The application data part is implemented by * the respective Operation subclasses, e.g. BootNotification, StartTransaction, ect. * * The resulting structure is that the RPC header (=instance of Request) holds a reference to the payload * message creator (=instance of BootNotification, StartTransaction, ...). Both objects working together give the complete * OCPP operation. */ #ifndef MO_OPERATION_H #define MO_OPERATION_H #include #include #include namespace MicroOcpp { std::unique_ptr createEmptyDocument(); class Operation { public: Operation(); virtual ~Operation(); virtual const char* getOperationType(); /** * Create the payload for the respective OCPP message * * For instance operation Authorize: creates Authorize.req(idTag) * * This function is usually called multiple times by the Arduino loop(). On first call, the request is initially sent. In the * succeeding calls, the implementers decide to either recreate the request, or do nothing as the operation is still pending. */ virtual std::unique_ptr createReq(); virtual void processConf(JsonObject payload); /* * returns if the operation must be aborted */ virtual bool processErr(const char *code, const char *description, JsonObject details) { return true;} /** * Processes the request in the JSON document. */ virtual void processReq(JsonObject payload); /** * After successfully processing a request sent by the communication counterpart, this function creates the payload for a confirmation * message. */ virtual std::unique_ptr createConf(); virtual const char *getErrorCode() {return nullptr;} //nullptr means no error virtual const char *getErrorDescription() {return "";} virtual std::unique_ptr getErrorDetails() {return createEmptyDocument();} }; } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Core/OperationRegistry.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include using namespace MicroOcpp; OperationRegistry::OperationRegistry() : registry(makeVector("OperationRegistry")) { } OperationCreator *OperationRegistry::findCreator(const char *operationType) { for (auto it = registry.begin(); it != registry.end(); ++it) { if (!strcmp(it->operationType, operationType)) { return &*it; } } return nullptr; } void OperationRegistry::registerOperation(const char *operationType, std::function creator) { registry.erase(std::remove_if(registry.begin(), registry.end(), [operationType] (const OperationCreator& el) { return !strcmp(operationType, el.operationType); }), registry.end()); OperationCreator entry; entry.operationType = operationType; entry.creator = creator; registry.push_back(entry); MO_DBG_DEBUG("registered operation %s", operationType); } void OperationRegistry::setOnRequest(const char *operationType, OnReceiveReqListener onRequest) { if (auto entry = findCreator(operationType)) { entry->onRequest = onRequest; } else { MO_DBG_ERR("%s not registered", operationType); } } void OperationRegistry::setOnResponse(const char *operationType, OnSendConfListener onResponse) { if (auto entry = findCreator(operationType)) { entry->onResponse = onResponse; } else { MO_DBG_ERR("%s not registered", operationType); } } std::unique_ptr OperationRegistry::deserializeOperation(const char *operationType) { if (auto entry = findCreator(operationType)) { auto payload = entry->creator(); if (payload) { auto result = std::unique_ptr(new Request( std::unique_ptr(payload))); result->setOnReceiveReqListener(entry->onRequest); result->setOnSendConfListener(entry->onResponse); return result; } } return std::unique_ptr(new Request( std::unique_ptr(new NotImplemented()))); } void OperationRegistry::debugPrint() { for (auto& creator : registry) { MO_CONSOLE_PRINTF("[OCPP] > %s\n", creator.operationType); } } ================================================ FILE: src/MicroOcpp/Core/OperationRegistry.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_OPERATIONREGISTRY_H #define MO_OPERATIONREGISTRY_H #include #include #include #include namespace MicroOcpp { class Operation; class Request; struct OperationCreator { const char *operationType {nullptr}; std::function creator {nullptr}; OnReceiveReqListener onRequest {nullptr}; OnSendConfListener onResponse {nullptr}; }; class OperationRegistry { private: Vector registry; OperationCreator *findCreator(const char *operationType); public: OperationRegistry(); void registerOperation(const char *operationType, std::function creator); void setOnRequest(const char *operationType, OnReceiveReqListener onRequest); void setOnResponse(const char *operationType, OnSendConfListener onResponse); std::unique_ptr deserializeOperation(const char *operationType); void debugPrint(); }; } #endif ================================================ FILE: src/MicroOcpp/Core/Request.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include namespace MicroOcpp { unsigned int g_randSeed = 1394827383; void writeRandomNonsecure(unsigned char *buf, size_t len) { g_randSeed += mocpp_tick_ms(); const unsigned int a = 16807; const unsigned int m = 2147483647; for (size_t i = 0; i < len; i++) { g_randSeed = (a * g_randSeed) % m; buf[i] = g_randSeed; } } } using namespace MicroOcpp; Request::Request(std::unique_ptr msg) : MemoryManaged("Request.", msg->getOperationType()), messageID(makeString(getMemoryTag())), operation(std::move(msg)) { timeout_start = mocpp_tick_ms(); debugRequest_start = mocpp_tick_ms(); } Request::~Request(){ } Operation *Request::getOperation(){ return operation.get(); } void Request::setTimeout(unsigned long timeout) { this->timeout_period = timeout; } bool Request::isTimeoutExceeded() { return timed_out || (timeout_period && mocpp_tick_ms() - timeout_start >= timeout_period); } void Request::executeTimeout() { if (!timed_out) { onTimeoutListener(); onAbortListener(); } timed_out = true; } void Request::setMessageID(const char *id){ if (!messageID.empty()){ MO_DBG_ERR("messageID already defined"); } messageID = id; } Request::CreateRequestResult Request::createRequest(JsonDoc& requestJson) { if (messageID.empty()) { char uuid [37] = {'\0'}; generateUUID(uuid, 37); messageID = uuid; } /* * Create the OCPP message */ auto requestPayload = operation->createReq(); if (!requestPayload) { return CreateRequestResult::Failure; } /* * Create OCPP-J Remote Procedure Call header */ size_t json_buffsize = JSON_ARRAY_SIZE(4) + (messageID.length() + 1) + requestPayload->capacity(); requestJson = initJsonDoc(getMemoryTag(), json_buffsize); requestJson.add(MESSAGE_TYPE_CALL); //MessageType requestJson.add(messageID); //Unique message ID requestJson.add(operation->getOperationType()); //Action requestJson.add(*requestPayload); //Payload if (MO_DBG_LEVEL >= MO_DL_DEBUG && mocpp_tick_ms() - debugRequest_start >= 10000) { //print contents on the console debugRequest_start = mocpp_tick_ms(); char *buf = new char[1024]; size_t len = 0; if (buf) { len = serializeJson(requestJson, buf, 1024); } if (!buf || len < 1) { MO_DBG_DEBUG("Try to send request: %s", operation->getOperationType()); } else { MO_DBG_DEBUG("Try to send request: %.*s (...)", 128, buf); } delete[] buf; } return CreateRequestResult::Success; } bool Request::receiveResponse(JsonArray response){ /* * check if messageIDs match. If yes, continue with this function. If not, return false for message not consumed */ if (messageID.compare(response[1].as())){ return false; } int messageTypeId = response[0] | -1; if (messageTypeId == MESSAGE_TYPE_CALLRESULT) { /* * Hand the payload over to the Operation object */ JsonObject payload = response[2]; operation->processConf(payload); /* * Hand the payload over to the onReceiveConf Callback */ onReceiveConfListener(payload); /* * return true as this message has been consumed */ return true; } else if (messageTypeId == MESSAGE_TYPE_CALLERROR) { /* * Hand the error over to the Operation object */ const char *errorCode = response[2]; const char *errorDescription = response[3]; JsonObject errorDetails = response[4]; bool abortOperation = operation->processErr(errorCode, errorDescription, errorDetails); if (abortOperation) { onReceiveErrorListener(errorCode, errorDescription, errorDetails); onAbortListener(); } return abortOperation; } else { MO_DBG_WARN("invalid response"); return false; //don't discard this message but retry sending it } } bool Request::receiveRequest(JsonArray request) { if (!request[1].is()) { MO_DBG_ERR("malformatted msgId"); return false; } setMessageID(request[1].as()); /* * Hand the payload over to the Request object */ JsonObject payload = request[3]; operation->processReq(payload); /* * Hand the payload over to the first Callback. It is a callback that notifies the client that request has been processed in the OCPP-library */ onReceiveReqListener(payload); return true; //success } Request::CreateResponseResult Request::createResponse(JsonDoc& response) { bool operationFailure = operation->getErrorCode() != nullptr; if (!operationFailure) { std::unique_ptr payload = operation->createConf(); if (!payload) { return CreateResponseResult::Pending; //confirmation message still pending } /* * Create OCPP-J Remote Procedure Call header */ size_t json_buffsize = JSON_ARRAY_SIZE(3) + payload->capacity(); response = initJsonDoc(getMemoryTag(), json_buffsize); response.add(MESSAGE_TYPE_CALLRESULT); //MessageType response.add(messageID.c_str()); //Unique message ID response.add(*payload); //Payload if (onSendConfListener) { onSendConfListener(payload->as()); } } else { //operation failure. Send error message instead const char *errorCode = operation->getErrorCode(); const char *errorDescription = operation->getErrorDescription(); std::unique_ptr errorDetails = operation->getErrorDetails(); /* * Create OCPP-J Remote Procedure Call header */ size_t json_buffsize = JSON_ARRAY_SIZE(5) + errorDetails->capacity(); response = initJsonDoc(getMemoryTag(), json_buffsize); response.add(MESSAGE_TYPE_CALLERROR); //MessageType response.add(messageID.c_str()); //Unique message ID response.add(errorCode); response.add(errorDescription); response.add(*errorDetails); //Error description } return CreateResponseResult::Success; } void Request::setOnReceiveConfListener(OnReceiveConfListener onReceiveConf){ if (onReceiveConf) onReceiveConfListener = onReceiveConf; } /** * Sets a Listener that is called after this machine processed a request by the communication counterpart */ void Request::setOnReceiveReqListener(OnReceiveReqListener onReceiveReq){ if (onReceiveReq) onReceiveReqListener = onReceiveReq; } void Request::setOnSendConfListener(OnSendConfListener onSendConf){ if (onSendConf) onSendConfListener = onSendConf; } void Request::setOnTimeoutListener(OnTimeoutListener onTimeout) { if (onTimeout) onTimeoutListener = onTimeout; } void Request::setOnReceiveErrorListener(OnReceiveErrorListener onReceiveError) { if (onReceiveError) onReceiveErrorListener = onReceiveError; } void Request::setOnAbortListener(OnAbortListener onAbort) { if (onAbort) onAbortListener = onAbort; } const char *Request::getOperationType() { return operation ? operation->getOperationType() : "UNDEFINED"; } void Request::setRequestSent() { requestSent = true; } bool Request::isRequestSent() { return requestSent; } namespace MicroOcpp { std::unique_ptr makeRequest(std::unique_ptr operation){ if (operation == nullptr) { return nullptr; } return std::unique_ptr(new Request(std::move(operation))); } std::unique_ptr makeRequest(Operation *operation) { return makeRequest(std::unique_ptr(operation)); } } //end namespace MicroOcpp ================================================ FILE: src/MicroOcpp/Core/Request.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_REQUEST_H #define MO_REQUEST_H #define MESSAGE_TYPE_CALL 2 #define MESSAGE_TYPE_CALLRESULT 3 #define MESSAGE_TYPE_CALLERROR 4 #include #include #include namespace MicroOcpp { class Operation; class Model; class Request : public MemoryManaged { private: String messageID; std::unique_ptr operation; void setMessageID(const char *id); OnReceiveConfListener onReceiveConfListener = [] (JsonObject payload) {}; OnReceiveReqListener onReceiveReqListener = [] (JsonObject payload) {}; OnSendConfListener onSendConfListener = [] (JsonObject payload) {}; OnTimeoutListener onTimeoutListener = [] () {}; OnReceiveErrorListener onReceiveErrorListener = [] (const char *code, const char *description, JsonObject details) {}; OnAbortListener onAbortListener = [] () {}; unsigned long timeout_start = 0; unsigned long timeout_period = 40000; bool timed_out = false; unsigned long debugRequest_start = 0; bool requestSent = false; public: Request(std::unique_ptr msg); ~Request(); Operation *getOperation(); void setTimeout(unsigned long timeout); //0 = disable timeout bool isTimeoutExceeded(); void executeTimeout(); //call Timeout Listener void setOnTimeoutListener(OnTimeoutListener onTimeout); /** * Sends the message(s) that belong to the OCPP Operation. This function puts a JSON message on the lower protocol layer. * * For instance operation Authorize: sends Authorize.req(idTag) * * This function is usually called multiple times by the Arduino loop(). On first call, the request is initially sent. In the * succeeding calls, the implementers decide to either resend the request, or do nothing as the operation is still pending. */ enum class CreateRequestResult { Success, Failure }; CreateRequestResult createRequest(JsonDoc& out); /** * Decides if message belongs to this operation instance and if yes, proccesses it. Receives both Confirmations and Errors * * Returns true if JSON object has been consumed, false otherwise. */ bool receiveResponse(JsonArray json); /** * Processes the request in the JSON document. Returns true on success, false on error. * * Returns false if the request doesn't belong to the corresponding operation instance */ bool receiveRequest(JsonArray json); /** * After processing a request sent by the communication counterpart, this function sends a confirmation * message. Returns true on success, false otherwise. Returns also true if a CallError has successfully * been sent */ enum class CreateResponseResult { Success, Pending, Failure }; CreateResponseResult createResponse(JsonDoc& out); void setOnReceiveConfListener(OnReceiveConfListener onReceiveConf); //listener executed when we received the .conf() to a .req() we sent void setOnReceiveReqListener(OnReceiveReqListener onReceiveReq); //listener executed when we receive a .req() void setOnSendConfListener(OnSendConfListener onSendConf); //listener executed when we send a .conf() to a .req() we received void setOnReceiveErrorListener(OnReceiveErrorListener onReceiveError); /** * The listener onAbort will be called whenever the engine stops trying to execute an operation normally which were initiated * on this device. This includes timeouts or if the ocpp counterpart sends an error (then it will be called in addition to * onTimeout or onReceiveError, respectively). Causes for onAbort: * * - Cannot create OCPP payload * - Timeout * - Receives error msg instead of confirmation msg * * The engine uses this listener in both modes: EVSE mode and Central system mode */ void setOnAbortListener(OnAbortListener onAbort); const char *getOperationType(); void setRequestSent(); bool isRequestSent(); }; /* * Simple factory functions */ std::unique_ptr makeRequest(std::unique_ptr op); std::unique_ptr makeRequest(Operation *op); //takes ownership of op } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Core/RequestCallbacks.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_REQUESTCALLBACKS_H #define MO_REQUESTCALLBACKS_H #include #include namespace MicroOcpp { using OnReceiveConfListener = std::function; using OnReceiveReqListener = std::function; using OnSendConfListener = std::function; using OnTimeoutListener = std::function; using OnReceiveErrorListener = std::function; //will be called if OCPP communication partner returns error code using OnAbortListener = std::function; //will be called whenever the engine will stop trying to execute the operation normallythere is a timeout or error (onAbort = onTimeout || onReceiveError) } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Core/RequestQueue.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include size_t removePayload(const char *src, size_t src_size, char *dst, size_t dst_size); using namespace MicroOcpp; VolatileRequestQueue::VolatileRequestQueue() : MemoryManaged("VolatileRequestQueue") { } VolatileRequestQueue::~VolatileRequestQueue() = default; void VolatileRequestQueue::loop() { /* * Drop timed out operations */ size_t i = 0; while (i < len) { size_t index = (front + i) % MO_REQUEST_CACHE_MAXSIZE; auto& request = requests[index]; if (request->isTimeoutExceeded()) { MO_DBG_INFO("operation timeout: %s", request->getOperationType()); request->executeTimeout(); if (index == front) { requests[front].reset(); front = (front + 1) % MO_REQUEST_CACHE_MAXSIZE; len--; } else { requests[index].reset(); for (size_t i = (index + MO_REQUEST_CACHE_MAXSIZE - front) % MO_REQUEST_CACHE_MAXSIZE; i < len - 1; i++) { requests[(front + i) % MO_REQUEST_CACHE_MAXSIZE] = std::move(requests[(front + i + 1) % MO_REQUEST_CACHE_MAXSIZE]); } len--; } } else { i++; } } } unsigned int VolatileRequestQueue::getFrontRequestOpNr() { if (len == 0) { return NoOperation; } return 1; //return OpNr 1 to grant PreBoot queue higher priority (=0), but send messages before tx-msg queue (starting with 10) } std::unique_ptr VolatileRequestQueue::fetchFrontRequest() { if (len == 0) { return nullptr; } std::unique_ptr result = std::move(requests[front]); front = (front + 1) % MO_REQUEST_CACHE_MAXSIZE; len--; MO_DBG_VERBOSE("front %zu len %zu", front, len); return result; } bool VolatileRequestQueue::pushRequestBack(std::unique_ptr request) { // Don't queue up multiple StatusNotification messages for the same connectorId #if 0 // Leads to ASAN failure when executed by Unit test suite (CustomOperation is casted to StatusNotification) if (strcmp(request->getOperationType(), "StatusNotification") == 0) { size_t i = 0; while (i < len) { size_t index = (front + i) % MO_REQUEST_CACHE_MAXSIZE; if (strcmp(requests[index]->getOperationType(), "StatusNotification")!= 0) { i++; continue; } auto new_status_notification = static_cast(request->getOperation()); auto old_status_notification = static_cast(requests[index]->getOperation()); if (old_status_notification->getConnectorId() == new_status_notification->getConnectorId()) { requests[index].reset(); for (size_t i = (index + MO_REQUEST_CACHE_MAXSIZE - front) % MO_REQUEST_CACHE_MAXSIZE; i < len - 1; i++) { requests[(front + i) % MO_REQUEST_CACHE_MAXSIZE] = std::move(requests[(front + i + 1) % MO_REQUEST_CACHE_MAXSIZE]); } len--; } else { i++; } } } #endif if (len >= MO_REQUEST_CACHE_MAXSIZE) { MO_DBG_INFO("Drop cached operation (cache full): %s", requests[front]->getOperationType()); requests[front]->executeTimeout(); requests[front].reset(); front = (front + 1) % MO_REQUEST_CACHE_MAXSIZE; len--; } requests[(front + len) % MO_REQUEST_CACHE_MAXSIZE] = std::move(request); len++; return true; } RequestQueue::RequestQueue(Connection& connection, OperationRegistry& operationRegistry) : MemoryManaged("RequestQueue"), connection(connection), operationRegistry(operationRegistry) { ReceiveTXTcallback callback = [this] (const char *payload, size_t length) { return this->receiveMessage(payload, length); }; connection.setReceiveTXTcallback(callback); memset(sendQueues, 0, sizeof(sendQueues)); addSendQueue(&defaultSendQueue); } void RequestQueue::loop() { /* * Check if front request timed out */ if (sendReqFront && sendReqFront->isTimeoutExceeded()) { MO_DBG_INFO("operation timeout: %s", sendReqFront->getOperationType()); sendReqFront->executeTimeout(); sendReqFront.reset(); } if (recvReqFront && recvReqFront->isTimeoutExceeded()) { MO_DBG_INFO("operation timeout: %s", recvReqFront->getOperationType()); recvReqFront->executeTimeout(); recvReqFront.reset(); } defaultSendQueue.loop(); if (!connection.isConnected()) { return; } /** * Send and dequeue a pending confirmation message, if existing * * If a message has been sent, terminate this loop() function. */ if (!recvReqFront) { recvReqFront = recvQueue.fetchFrontRequest(); } if (recvReqFront) { auto response = initJsonDoc(getMemoryTag()); auto ret = recvReqFront->createResponse(response); if (ret == Request::CreateResponseResult::Success) { auto out = makeString(getMemoryTag()); serializeJson(response, out); bool success = connection.sendTXT(out.c_str(), out.length()); if (success) { MO_DBG_TRAFFIC_OUT(out.c_str()); recvReqFront.reset(); } return; } //else: There will be another attempt to send this conf message in a future loop call } /** * Send pending req message */ if (!sendReqFront) { unsigned int minOpNr = RequestEmitter::NoOperation; size_t index = MO_NUM_REQUEST_QUEUES; for (size_t i = 0; i < MO_NUM_REQUEST_QUEUES && sendQueues[i]; i++) { auto opNr = sendQueues[i]->getFrontRequestOpNr(); if (opNr < minOpNr) { minOpNr = opNr; index = i; } } if (index < MO_NUM_REQUEST_QUEUES) { sendReqFront = sendQueues[index]->fetchFrontRequest(); } } if (sendReqFront && !sendReqFront->isRequestSent()) { auto request = initJsonDoc(getMemoryTag()); auto ret = sendReqFront->createRequest(request); if (ret == Request::CreateRequestResult::Success) { //send request auto out = makeString(getMemoryTag()); serializeJson(request, out); bool success = connection.sendTXT(out.c_str(), out.length()); if (success) { MO_DBG_TRAFFIC_OUT(out.c_str()); sendReqFront->setRequestSent(); //mask as sent and wait for response / timeout } return; } } } void RequestQueue::sendRequest(std::unique_ptr op){ defaultSendQueue.pushRequestBack(std::move(op)); } void RequestQueue::sendRequestPreBoot(std::unique_ptr op){ if (!preBootSendQueue) { MO_DBG_ERR("did not set PreBoot queue"); return; } preBootSendQueue->pushRequestBack(std::move(op)); } void RequestQueue::addSendQueue(RequestEmitter* sendQueue) { for (size_t i = 0; i < MO_NUM_REQUEST_QUEUES; i++) { if (!sendQueues[i]) { sendQueues[i] = sendQueue; return; } } MO_DBG_ERR("exceeded sendQueue capacity"); } void RequestQueue::setPreBootSendQueue(VolatileRequestQueue *preBootQueue) { this->preBootSendQueue = preBootQueue; addSendQueue(preBootQueue); } unsigned int RequestQueue::getNextOpNr() { return nextOpNr++; } bool RequestQueue::receiveMessage(const char* payload, size_t length) { MO_DBG_TRAFFIC_IN((int) length, payload); size_t capacity_init = (3 * length) / 2; //capacity = ceil capacity_init to the next power of two; should be at least 128 size_t capacity = 128; while (capacity < capacity_init && capacity < MO_MAX_JSON_CAPACITY) { capacity *= 2; } if (capacity > MO_MAX_JSON_CAPACITY) { capacity = MO_MAX_JSON_CAPACITY; } auto doc = initJsonDoc(getMemoryTag()); DeserializationError err = DeserializationError::NoMemory; while (err == DeserializationError::NoMemory && capacity <= MO_MAX_JSON_CAPACITY) { doc = initJsonDoc(getMemoryTag(), capacity); err = deserializeJson(doc, payload, length); capacity *= 2; } bool success = false; switch (err.code()) { case DeserializationError::Ok: { int messageTypeId = doc[0] | -1; if (messageTypeId == MESSAGE_TYPE_CALL) { receiveRequest(doc.as()); success = true; } else if (messageTypeId == MESSAGE_TYPE_CALLRESULT || messageTypeId == MESSAGE_TYPE_CALLERROR) { receiveResponse(doc.as()); success = true; } else { MO_DBG_WARN("Invalid OCPP message! (though JSON has successfully been deserialized)"); } break; } case DeserializationError::InvalidInput: MO_DBG_WARN("Invalid input! Not a JSON"); break; case DeserializationError::NoMemory: { MO_DBG_WARN("incoming operation exceeds buffer capacity. Input length = %zu, max capacity = %d", length, MO_MAX_JSON_CAPACITY); /* * If websocket input is of message type MESSAGE_TYPE_CALL, send back a message of type MESSAGE_TYPE_CALLERROR. * Then the communication counterpart knows that this operation failed. * If the input type is MESSAGE_TYPE_CALLRESULT, then abort the operation to avoid getting stalled. */ doc = initJsonDoc(getMemoryTag(), 200); char onlyRpcHeader[200]; size_t onlyRpcHeader_len = removePayload(payload, length, onlyRpcHeader, sizeof(onlyRpcHeader)); DeserializationError err2 = deserializeJson(doc, onlyRpcHeader, onlyRpcHeader_len); if (err2.code() == DeserializationError::Ok) { int messageTypeId = doc[0] | -1; if (messageTypeId == MESSAGE_TYPE_CALL) { success = true; auto op = makeRequest(new MsgBufferExceeded(MO_MAX_JSON_CAPACITY, length)); receiveRequest(doc.as(), std::move(op)); } else if (messageTypeId == MESSAGE_TYPE_CALLRESULT || messageTypeId == MESSAGE_TYPE_CALLERROR) { success = true; MO_DBG_WARN("crop incoming response"); receiveResponse(doc.as()); } } break; } default: MO_DBG_WARN("Deserialization failed: %s", err.c_str()); break; } return success; } /** * call conf() on each element of the queue. Start with first element. On successful message delivery, * delete the element from the list. Try all the pending OCPP Operations until the right one is found. * * This function could result in improper behavior in Charging Stations, because messages are not * guaranteed to be received and therefore processed in the right order. */ void RequestQueue::receiveResponse(JsonArray json) { if (!sendReqFront || !sendReqFront->receiveResponse(json)) { MO_DBG_WARN("Received response doesn't match pending operation"); } sendReqFront.reset(); } void RequestQueue::receiveRequest(JsonArray json) { auto op = operationRegistry.deserializeOperation(json[2] | "UNDEFINED"); if (op == nullptr) { MO_DBG_WARN("OOM"); return; } receiveRequest(json, std::move(op)); } void RequestQueue::receiveRequest(JsonArray json, std::unique_ptr op) { op->receiveRequest(json); //execute the operation recvQueue.pushRequestBack(std::move(op)); //enqueue so loop() plans conf sending } /* * Tries to recover the Ocpp-Operation header from a broken message. * * Example input: * [2, "75705e50-682d-404e-b400-1bca33d41e19", "ChangeConfiguration", {"key":"now the message breaks... * * The Json library returns an error code when trying to deserialize that broken message. This * function searches for the first occurence of the character '{' and writes "}]" after it. * * Example output: * [2, "75705e50-682d-404e-b400-1bca33d41e19", "ChangeConfiguration", {}] * */ size_t removePayload(const char *src, size_t src_size, char *dst, size_t dst_size) { size_t res_len = 0; for (size_t i = 0; i < src_size && i < dst_size-3; i++) { if (src[i] == '\0'){ //no payload found within specified range. Cancel execution break; } dst[i] = src[i]; if (src[i] == '{') { dst[i+1] = '}'; dst[i+2] = ']'; res_len = i+3; break; } } dst[res_len] = '\0'; res_len++; return res_len; } ================================================ FILE: src/MicroOcpp/Core/RequestQueue.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_REQUESTQUEUE_H #define MO_REQUESTQUEUE_H #include #include #include #include #include #ifndef MO_REQUEST_CACHE_MAXSIZE #define MO_REQUEST_CACHE_MAXSIZE 10 #endif #ifndef MO_NUM_REQUEST_QUEUES #define MO_NUM_REQUEST_QUEUES 10 #endif namespace MicroOcpp { class Connection; class OperationRegistry; class Request; class RequestEmitter { public: static const unsigned int NoOperation = std::numeric_limits::max(); virtual unsigned int getFrontRequestOpNr() = 0; //return OpNr of front request or NoOperation if queue is empty virtual std::unique_ptr fetchFrontRequest() = 0; }; class VolatileRequestQueue : public RequestEmitter, public MemoryManaged { private: std::unique_ptr requests [MO_REQUEST_CACHE_MAXSIZE]; size_t front = 0, len = 0; public: VolatileRequestQueue(); ~VolatileRequestQueue(); void loop(); unsigned int getFrontRequestOpNr() override; std::unique_ptr fetchFrontRequest() override; bool pushRequestBack(std::unique_ptr request); }; class RequestQueue : public MemoryManaged { private: Connection& connection; OperationRegistry& operationRegistry; RequestEmitter* sendQueues [MO_NUM_REQUEST_QUEUES]; VolatileRequestQueue defaultSendQueue; VolatileRequestQueue *preBootSendQueue = nullptr; std::unique_ptr sendReqFront; VolatileRequestQueue recvQueue; std::unique_ptr recvReqFront; bool receiveMessage(const char* payload, size_t length); //receive from server: either a request or response void receiveRequest(JsonArray json); void receiveRequest(JsonArray json, std::unique_ptr op); void receiveResponse(JsonArray json); unsigned long sockTrackLastConnected = 0; unsigned int nextOpNr = 10; //Nr 0 - 9 reservered for internal purposes public: RequestQueue() = delete; RequestQueue(const RequestQueue&) = delete; RequestQueue(const RequestQueue&&) = delete; RequestQueue(Connection& connection, OperationRegistry& operationRegistry); void loop(); //polls all reqQueues and decides which request to send (if any) void sendRequest(std::unique_ptr request); //send an OCPP operation request to the server; adds request to default queue void sendRequestPreBoot(std::unique_ptr request); //send an OCPP operation request to the server; adds request to preBootQueue void addSendQueue(RequestEmitter* sendQueue); void setPreBootSendQueue(VolatileRequestQueue *preBootQueue); unsigned int getNextOpNr(); }; } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Core/Time.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include namespace MicroOcpp { const Timestamp MIN_TIME = Timestamp(2010, 0, 0, 0, 0, 0); const Timestamp MAX_TIME = Timestamp(2037, 0, 0, 0, 0, 0); Timestamp::Timestamp() : MemoryManaged("Timestamp") { } Timestamp::Timestamp(const Timestamp& other) : MemoryManaged("Timestamp") { *this = other; } #if MO_ENABLE_TIMESTAMP_MILLISECONDS Timestamp::Timestamp(int16_t year, int16_t month, int16_t day, int32_t hour, int32_t minute, int32_t second, int32_t ms) : MemoryManaged("Timestamp"), year(year), month(month), day(day), hour(hour), minute(minute), second(second), ms(ms) { } #else Timestamp::Timestamp(int16_t year, int16_t month, int16_t day, int32_t hour, int32_t minute, int32_t second) : MemoryManaged("Timestamp"), year(year), month(month), day(day), hour(hour), minute(minute), second(second) { } #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS int noDays(int month, int year) { return (month == 0 || month == 2 || month == 4 || month == 6 || month == 7 || month == 9 || month == 11) ? 31 : ((month == 3 || month == 5 || month == 8 || month == 10) ? 30 : ((year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) ? 29 : 28)); } bool Timestamp::setTime(const char *jsonDateString) { const int JSONDATE_MINLENGTH = 19; if (strlen(jsonDateString) < JSONDATE_MINLENGTH){ return false; } if (!isdigit(jsonDateString[0]) || //2 !isdigit(jsonDateString[1]) || //0 !isdigit(jsonDateString[2]) || //1 !isdigit(jsonDateString[3]) || //3 jsonDateString[4] != '-' || //- !isdigit(jsonDateString[5]) || //0 !isdigit(jsonDateString[6]) || //2 jsonDateString[7] != '-' || //- !isdigit(jsonDateString[8]) || //0 !isdigit(jsonDateString[9]) || //1 jsonDateString[10] != 'T' || //T !isdigit(jsonDateString[11]) || //2 !isdigit(jsonDateString[12]) || //0 jsonDateString[13] != ':' || //: !isdigit(jsonDateString[14]) || //5 !isdigit(jsonDateString[15]) || //3 jsonDateString[16] != ':' || //: !isdigit(jsonDateString[17]) || //3 !isdigit(jsonDateString[18])) { //2 //ignore subsequent characters return false; } int year = (jsonDateString[0] - '0') * 1000 + (jsonDateString[1] - '0') * 100 + (jsonDateString[2] - '0') * 10 + (jsonDateString[3] - '0'); int month = (jsonDateString[5] - '0') * 10 + (jsonDateString[6] - '0') - 1; int day = (jsonDateString[8] - '0') * 10 + (jsonDateString[9] - '0') - 1; int hour = (jsonDateString[11] - '0') * 10 + (jsonDateString[12] - '0'); int minute = (jsonDateString[14] - '0') * 10 + (jsonDateString[15] - '0'); int second = (jsonDateString[17] - '0') * 10 + (jsonDateString[18] - '0'); //optional fractals int ms = 0; if (jsonDateString[19] == '.') { if (isdigit(jsonDateString[20]) || //1 isdigit(jsonDateString[21]) || //2 isdigit(jsonDateString[22])) { ms = (jsonDateString[20] - '0') * 100 + (jsonDateString[21] - '0') * 10 + (jsonDateString[22] - '0'); } else { return false; } } if (year < 1970 || year >= 2038 || month < 0 || month >= 12 || day < 0 || day >= noDays(month, year) || hour < 0 || hour >= 24 || minute < 0 || minute >= 60 || second < 0 || second > 60 || //tolerate leap seconds -- (23:59:60) can be a valid time ms < 0 || ms >= 1000) { return false; } this->year = year; this->month = month; this->day = day; this->hour = hour; this->minute = minute; this->second = second; #if MO_ENABLE_TIMESTAMP_MILLISECONDS this->ms = ms; #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS return true; } bool Timestamp::toJsonString(char *jsonDateString, size_t buffsize) const { if (buffsize < JSONDATE_LENGTH + 1) return false; jsonDateString[0] = ((char) ((year / 1000) % 10)) + '0'; jsonDateString[1] = ((char) ((year / 100) % 10)) + '0'; jsonDateString[2] = ((char) ((year / 10) % 10)) + '0'; jsonDateString[3] = ((char) ((year / 1) % 10)) + '0'; jsonDateString[4] = '-'; jsonDateString[5] = ((char) (((month + 1) / 10) % 10)) + '0'; jsonDateString[6] = ((char) (((month + 1) / 1) % 10)) + '0'; jsonDateString[7] = '-'; jsonDateString[8] = ((char) (((day + 1) / 10) % 10)) + '0'; jsonDateString[9] = ((char) (((day + 1) / 1) % 10)) + '0'; jsonDateString[10] = 'T'; jsonDateString[11] = ((char) ((hour / 10) % 10)) + '0'; jsonDateString[12] = ((char) ((hour / 1) % 10)) + '0'; jsonDateString[13] = ':'; jsonDateString[14] = ((char) ((minute / 10) % 10)) + '0'; jsonDateString[15] = ((char) ((minute / 1) % 10)) + '0'; jsonDateString[16] = ':'; jsonDateString[17] = ((char) ((second / 10) % 10)) + '0'; jsonDateString[18] = ((char) ((second / 1) % 10)) + '0'; #if MO_ENABLE_TIMESTAMP_MILLISECONDS jsonDateString[19] = '.'; jsonDateString[20] = ((char) ((ms / 100) % 10)) + '0'; jsonDateString[21] = ((char) ((ms / 10) % 10)) + '0'; jsonDateString[22] = ((char) ((ms / 1) % 10)) + '0'; jsonDateString[23] = 'Z'; jsonDateString[24] = '\0'; #else jsonDateString[19] = 'Z'; jsonDateString[20] = '\0'; #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS return true; } Timestamp &Timestamp::operator+=(int secs) { second += secs; if (second >= 0 && second < 60) return *this; minute += second / 60; second %= 60; if (second < 0) { minute--; second += 60; } if (minute >= 0 && minute < 60) return *this; hour += minute / 60; minute %= 60; if (minute < 0) { hour--; minute += 60; } if (hour >= 0 && hour < 24) return *this; day += hour / 24; hour %= 24; if (hour < 0) { day--; hour += 24; } while (day >= noDays(month, year)) { day -= noDays(month, year); month++; if (month >= 12) { month -= 12; year++; } } while (day < 0) { month--; if (month < 0) { month += 12; year--; } day += noDays(month, year); } return *this; } #if MO_ENABLE_TIMESTAMP_MILLISECONDS Timestamp &Timestamp::addMilliseconds(int val) { ms += val; if (ms >= 0 && ms < 1000) return *this; auto dsecond = ms / 1000; ms %= 1000; if (ms < 0) { dsecond--; ms += 1000; } return this->operator+=(dsecond); } #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS Timestamp &Timestamp::operator-=(int secs) { return operator+=(-secs); } int Timestamp::operator-(const Timestamp &rhs) const { //dt = rhs - mocpp_base int16_t year_base, year_end; if (year <= rhs.year) { year_base = year; year_end = rhs.year; } else { year_base = rhs.year; year_end = year; } int16_t lhsDays = day; int16_t rhsDays = rhs.day; for (int16_t iy = year_base; iy <= year_end; iy++) { for (int16_t im = 0; im < 12; im++) { if (year > iy || (year == iy && month > im)) { lhsDays += noDays(im, iy); } if (rhs.year > iy || (rhs.year == iy && rhs.month > im)) { rhsDays += noDays(im, iy); } } } int dt = (lhsDays - rhsDays) * (24 * 3600) + (hour - rhs.hour) * 3600 + (minute - rhs.minute) * 60 + second - rhs.second; #if MO_ENABLE_TIMESTAMP_MILLISECONDS // Make it so that we round the difference to the nearest second, instead of being up to almost a whole second off if ((ms - rhs.ms) > 500) dt++; if ((ms - rhs.ms) < -500) dt--; #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS return dt; } Timestamp &Timestamp::operator=(const Timestamp &rhs) { year = rhs.year; month = rhs.month; day = rhs.day; hour = rhs.hour; minute = rhs.minute; second = rhs.second; #if MO_ENABLE_TIMESTAMP_MILLISECONDS ms = rhs.ms; #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS return *this; } Timestamp operator+(const Timestamp &lhs, int secs) { Timestamp res = lhs; res += secs; return res; } Timestamp operator-(const Timestamp &lhs, int secs) { return operator+(lhs, -secs); } bool operator==(const Timestamp &lhs, const Timestamp &rhs) { return lhs.year == rhs.year && lhs.month == rhs.month && lhs.day == rhs.day && lhs.hour == rhs.hour && lhs.minute == rhs.minute && lhs.second == rhs.second #if MO_ENABLE_TIMESTAMP_MILLISECONDS && lhs.ms == rhs.ms #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS ; } bool operator!=(const Timestamp &lhs, const Timestamp &rhs) { return !(lhs == rhs); } bool operator<(const Timestamp &lhs, const Timestamp &rhs) { if (lhs.year != rhs.year) return lhs.year < rhs.year; if (lhs.month != rhs.month) return lhs.month < rhs.month; if (lhs.day != rhs.day) return lhs.day < rhs.day; if (lhs.hour != rhs.hour) return lhs.hour < rhs.hour; if (lhs.minute != rhs.minute) return lhs.minute < rhs.minute; if (lhs.second != rhs.second) return lhs.second < rhs.second; #if MO_ENABLE_TIMESTAMP_MILLISECONDS if (lhs.ms != rhs.ms) return lhs.ms < rhs.ms; #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS return false; } bool operator<=(const Timestamp &lhs, const Timestamp &rhs) { return lhs < rhs || lhs == rhs; } bool operator>(const Timestamp &lhs, const Timestamp &rhs) { return rhs < lhs; } bool operator>=(const Timestamp &lhs, const Timestamp &rhs) { return rhs <= lhs; } Clock::Clock() { } bool Clock::setTime(const char* jsonDateString) { Timestamp timestamp = Timestamp(); if (!timestamp.setTime(jsonDateString)) { return false; } system_basetime = mocpp_tick_ms(); mocpp_basetime = timestamp; currentTime = mocpp_basetime; lastUpdate = system_basetime; return true; } const Timestamp &Clock::now() { auto tReading = mocpp_tick_ms(); auto delta = tReading - lastUpdate; #if MO_ENABLE_TIMESTAMP_MILLISECONDS currentTime.addMilliseconds(delta); lastUpdate = tReading; #else auto deltaSecs = delta / 1000; currentTime += deltaSecs; lastUpdate += deltaSecs * 1000; #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS return currentTime; } Timestamp Clock::adjustPrebootTimestamp(const Timestamp& t) { auto systemtime_in = t - Timestamp(); if (systemtime_in > (int) system_basetime / 1000) { return mocpp_basetime; } return mocpp_basetime - ((int) (system_basetime / 1000) - systemtime_in); } } ================================================ FILE: src/MicroOcpp/Core/Time.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_TIME_H #define MO_TIME_H #include #include #include #include #include #ifndef MO_ENABLE_TIMESTAMP_MILLISECONDS #define MO_ENABLE_TIMESTAMP_MILLISECONDS 0 #endif #if MO_ENABLE_TIMESTAMP_MILLISECONDS #define JSONDATE_LENGTH 24 //max. ISO 8601 date length, excluding the terminating zero #else #define JSONDATE_LENGTH 20 //ISO 8601 date length, excluding the terminating zero #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS namespace MicroOcpp { class Timestamp : public MemoryManaged { private: /* * Internal representation of the current time. The initial values correspond to UNIX-time 0. January * corresponds to month 0 and the first day in the month is day 0. */ int16_t year = 1970; int16_t month = 0; int16_t day = 0; int32_t hour = 0; int32_t minute = 0; int32_t second = 0; #if MO_ENABLE_TIMESTAMP_MILLISECONDS int32_t ms = 0; #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS public: Timestamp(); Timestamp(const Timestamp& other); #if MO_ENABLE_TIMESTAMP_MILLISECONDS Timestamp(int16_t year, int16_t month, int16_t day, int32_t hour, int32_t minute, int32_t second, int32_t ms = 0); #else Timestamp(int16_t year, int16_t month, int16_t day, int32_t hour, int32_t minute, int32_t second); #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS /** * Expects a date string like * 2020-10-01T20:53:32.486Z * * as generated in JavaScript by calling toJSON() on a Date object * * Only processes the first 19 characters. The subsequent are ignored until terminating 0. * * Has a semi-sophisticated type check included. Will return true on successful time set and false if * the given string is not a JSON Date string. * * jsonDateString: 0-terminated string */ bool setTime(const char* jsonDateString); bool toJsonString(char *out, size_t buffsize) const; Timestamp &operator=(const Timestamp &rhs); #if MO_ENABLE_TIMESTAMP_MILLISECONDS Timestamp &addMilliseconds(int ms); #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS /* * Time periods are given in seconds for all of the following arithmetic operations */ Timestamp &operator+=(int secs); Timestamp &operator-=(int secs); int operator-(const Timestamp &rhs) const; friend Timestamp operator+(const Timestamp &lhs, int secs); friend Timestamp operator-(const Timestamp &lhs, int secs); friend bool operator==(const Timestamp &lhs, const Timestamp &rhs); friend bool operator!=(const Timestamp &lhs, const Timestamp &rhs); friend bool operator<(const Timestamp &lhs, const Timestamp &rhs); friend bool operator<=(const Timestamp &lhs, const Timestamp &rhs); friend bool operator>(const Timestamp &lhs, const Timestamp &rhs); friend bool operator>=(const Timestamp &lhs, const Timestamp &rhs); }; extern const Timestamp MIN_TIME; extern const Timestamp MAX_TIME; class Clock { private: Timestamp mocpp_basetime = Timestamp(); decltype(mocpp_tick_ms()) system_basetime = 0; //the value of mocpp_tick_ms() when OCPP server's time was taken decltype(mocpp_tick_ms()) lastUpdate = 0; Timestamp currentTime = Timestamp(); public: Clock(); Clock(const Clock&) = delete; Clock(const Clock&&) = delete; Clock& operator=(const Clock&) = delete; const Timestamp &now(); /** * Expects a date string like * 2020-10-01T20:53:32.486Z * * as generated in JavaScript by calling toJSON() on a Date object * * Only processes the first 23 characters. The subsequent are ignored * * Has a semi-sophisticated type check included. Will return true on successful time set and false if * the given string is not a JSON Date string. * * jsonDateString: 0-terminated string */ bool setTime(const char* jsonDateString); /* * Timestamps which were taken before the Clock was initially set can be adjusted retrospectively. Two * conditions must be true: the Clock was set in the meantime and the Timestamp was taken at the same * run of this library. The caller must check this */ Timestamp adjustPrebootTimestamp(const Timestamp& t); }; } #endif ================================================ FILE: src/MicroOcpp/Core/UuidUtils.cpp ================================================ #include #include #include namespace MicroOcpp { #define UUID_STR_LEN 36 bool generateUUID(char *uuidBuffer, size_t len) { if (len < UUID_STR_LEN + 1) { return false; } uint32_t ar[4]; for (uint8_t i = 0; i < 4; i++) { ar[i] = mocpp_rng(); } // Conforming to RFC 4122 Specification // - byte 7: four most significant bits ==> 0100 --> always 4 // - byte 9: two most significant bits ==> 10 --> always {8, 9, A, B}. // // patch bits for version 1 and variant 4 here ar[1] &= 0xFFF0FFFF; // remove 4 bits. ar[1] |= 0x00040000; // variant 4 ar[2] &= 0xFFFFFFF3; // remove 2 bits ar[2] |= 0x00000008; // version 1 // loop through the random 16 byte array for (uint8_t i = 0, j = 0; i < 16; i++) { // multiples of 4 between 8 and 20 get a -. // note we are processing 2 digits in one loop. if ((i & 0x1) == 0) { if ((4 <= i) && (i <= 10)) { uuidBuffer[j++] = '-'; } } // encode the byte as two hex characters uint8_t nr = i / 4; uint8_t xx = ar[nr]; uint8_t ch = xx & 0x0F; uuidBuffer[j++] = (ch < 10)? '0' + ch : ('a' - 10) + ch; ch = (xx >> 4) & 0x0F; ar[nr] >>= 8; uuidBuffer[j++] = (ch < 10)? '0' + ch : ('a' - 10) + ch; } uuidBuffer[UUID_STR_LEN] = 0; return true; } } ================================================ FILE: src/MicroOcpp/Core/UuidUtils.h ================================================ #ifndef MO_UUIDUTILS_H #define MO_UUIDUTILS_H #include namespace MicroOcpp { // Generates a UUID (Universally Unique Identifier) and writes it into a given buffer // Returns false if the generation failed // The buffer must be at least 37 bytes long (36 characters + zero termination) bool generateUUID(char *uuidBuffer, size_t len); } #endif ================================================ FILE: src/MicroOcpp/Debug.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include const char *level_label [] = { "", //MO_DL_NONE 0x00 "ERROR", //MO_DL_ERROR 0x01 "warning", //MO_DL_WARN 0x02 "info", //MO_DL_INFO 0x03 "debug", //MO_DL_DEBUG 0x04 "verbose" //MO_DL_VERBOSE 0x05 }; #if MO_DBG_FORMAT == MO_DF_MINIMAL void mo_dbg_print_prefix(int level, const char *fn, int line) { (void)0; } #elif MO_DBG_FORMAT == MO_DF_COMPACT void mo_dbg_print_prefix(int level, const char *fn, int line) { size_t l = strlen(fn); size_t r = l; while (l > 0 && fn[l-1] != '/' && fn[l-1] != '\\') { l--; if (fn[l] == '.') r = l; } MO_CONSOLE_PRINTF("%.*s:%i ", (int) (r - l), fn + l, line); } #elif MO_DBG_FORMAT == MO_DF_FILE_LINE void mo_dbg_print_prefix(int level, const char *fn, int line) { size_t l = strlen(fn); while (l > 0 && fn[l-1] != '/' && fn[l-1] != '\\') { l--; } MO_CONSOLE_PRINTF("[MO] %s (%s:%i): ", level_label[level], fn + l, line); } #elif MO_DBG_FORMAT == MO_DF_FULL void mo_dbg_print_prefix(int level, const char *fn, int line) { MO_CONSOLE_PRINTF("[MO] %s (%s:%i): ", level_label[level], fn, line); } #else #error invalid MO_DBG_FORMAT definition #endif void mo_dbg_print_suffix() { MO_CONSOLE_PRINTF("\n"); } ================================================ FILE: src/MicroOcpp/Debug.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_DEBUG_H #define MO_DEBUG_H #include #define MO_DL_NONE 0x00 //suppress all output to the console #define MO_DL_ERROR 0x01 //report failures #define MO_DL_WARN 0x02 //report observed or assumed inconsistent state #define MO_DL_INFO 0x03 //inform about internal state changes #define MO_DL_DEBUG 0x04 //relevant info for debugging #define MO_DL_VERBOSE 0x05 //all output #ifndef MO_DBG_LEVEL #define MO_DBG_LEVEL MO_DL_INFO //default #endif //MbedTLS debug level documented in mbedtls/debug.h: #ifndef MO_DBG_LEVEL_MBEDTLS #define MO_DBG_LEVEL_MBEDTLS 1 #endif #define MO_DF_MINIMAL 0x00 //don't reveal origin of a debug message #define MO_DF_COMPACT 0x01 //print module by file name and line number #define MO_DF_FILE_LINE 0x02 //print file and line number #define MO_DF_FULL 0x03 //print path and file and line numbr #ifndef MO_DBG_FORMAT #define MO_DBG_FORMAT MO_DF_FILE_LINE //default #endif #ifdef __cplusplus extern "C" { #endif void mo_dbg_print_prefix(int level, const char *fn, int line); void mo_dbg_print_suffix(); #ifdef __cplusplus } #endif #define MO_DBG(level, X) \ do { \ mo_dbg_print_prefix(level, __FILE__, __LINE__); \ MO_CONSOLE_PRINTF X; \ mo_dbg_print_suffix(); \ } while (0) #if MO_DBG_LEVEL >= MO_DL_ERROR #define MO_DBG_ERR(...) MO_DBG(MO_DL_ERROR,(__VA_ARGS__)) #else #define MO_DBG_ERR(...) ((void)0) #endif #if MO_DBG_LEVEL >= MO_DL_WARN #define MO_DBG_WARN(...) MO_DBG(MO_DL_WARN,(__VA_ARGS__)) #else #define MO_DBG_WARN(...) ((void)0) #endif #if MO_DBG_LEVEL >= MO_DL_INFO #define MO_DBG_INFO(...) MO_DBG(MO_DL_INFO,(__VA_ARGS__)) #else #define MO_DBG_INFO(...) ((void)0) #endif #if MO_DBG_LEVEL >= MO_DL_DEBUG #define MO_DBG_DEBUG(...) MO_DBG(MO_DL_DEBUG,(__VA_ARGS__)) #else #define MO_DBG_DEBUG(...) ((void)0) #endif #if MO_DBG_LEVEL >= MO_DL_VERBOSE #define MO_DBG_VERBOSE(...) MO_DBG(MO_DL_VERBOSE,(__VA_ARGS__)) #else #define MO_DBG_VERBOSE(...) ((void)0) #endif #ifdef MO_TRAFFIC_OUT #define MO_DBG_TRAFFIC_OUT(...) \ do { \ MO_CONSOLE_PRINTF("[MO] Send: %s",__VA_ARGS__); \ MO_CONSOLE_PRINTF("\n"); \ } while (0) #define MO_DBG_TRAFFIC_IN(...) \ do { \ MO_CONSOLE_PRINTF("[MO] Recv: %.*s",__VA_ARGS__); \ MO_CONSOLE_PRINTF("\n"); \ } while (0) #else #define MO_DBG_TRAFFIC_OUT(...) ((void)0) #define MO_DBG_TRAFFIC_IN(...) ((void)0) #endif #endif ================================================ FILE: src/MicroOcpp/Model/Authorization/AuthorizationData.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_LOCAL_AUTH #include #include using namespace MicroOcpp; AuthorizationData::AuthorizationData() : MemoryManaged("v16.Authorization.AuthorizationData") { } AuthorizationData::AuthorizationData(AuthorizationData&& other) : MemoryManaged("v16.Authorization.AuthorizationData") { operator=(std::move(other)); } AuthorizationData::~AuthorizationData() { MO_FREE(parentIdTag); parentIdTag = nullptr; } AuthorizationData& AuthorizationData::operator=(AuthorizationData&& other) { parentIdTag = other.parentIdTag; other.parentIdTag = nullptr; expiryDate = std::move(other.expiryDate); strncpy(idTag, other.idTag, IDTAG_LEN_MAX + 1); idTag[IDTAG_LEN_MAX] = '\0'; status = other.status; return *this; } void AuthorizationData::readJson(JsonObject entry, bool compact) { if (entry.containsKey(AUTHDATA_KEY_IDTAG(compact))) { strncpy(idTag, entry[AUTHDATA_KEY_IDTAG(compact)], IDTAG_LEN_MAX + 1); idTag[IDTAG_LEN_MAX] = '\0'; } else { idTag[0] = '\0'; } JsonObject idTagInfo; if (compact){ idTagInfo = entry; } else { idTagInfo = entry[AUTHDATA_KEY_IDTAGINFO]; } if (idTagInfo.containsKey(AUTHDATA_KEY_EXPIRYDATE(compact))) { expiryDate = std::unique_ptr(new Timestamp()); if (!expiryDate->setTime(idTagInfo[AUTHDATA_KEY_EXPIRYDATE(compact)])) { expiryDate.reset(); } } else { expiryDate.reset(); } if (idTagInfo.containsKey(AUTHDATA_KEY_PARENTIDTAG(compact))) { MO_FREE(parentIdTag); parentIdTag = nullptr; parentIdTag = static_cast(MO_MALLOC(getMemoryTag(), IDTAG_LEN_MAX + 1)); if (parentIdTag) { strncpy(parentIdTag, idTagInfo[AUTHDATA_KEY_PARENTIDTAG(compact)], IDTAG_LEN_MAX + 1); parentIdTag[IDTAG_LEN_MAX] = '\0'; } else { MO_DBG_ERR("OOM"); } } else { MO_FREE(parentIdTag); parentIdTag = nullptr; } if (idTagInfo.containsKey(AUTHDATA_KEY_STATUS(compact))) { status = deserializeAuthorizationStatus(idTagInfo[AUTHDATA_KEY_STATUS(compact)]); } else { if (compact) { status = AuthorizationStatus::Accepted; } else { status = AuthorizationStatus::UNDEFINED; } } } size_t AuthorizationData::getJsonCapacity() const { return JSON_OBJECT_SIZE(2) + (idTag[0] != '\0' ? JSON_OBJECT_SIZE(1) : 0) + (expiryDate ? JSON_OBJECT_SIZE(1) + JSONDATE_LENGTH + 1 : 0) + (parentIdTag ? JSON_OBJECT_SIZE(1) : 0) + (status != AuthorizationStatus::UNDEFINED ? JSON_OBJECT_SIZE(1) : 0); } void AuthorizationData::writeJson(JsonObject& entry, bool compact) { if (idTag[0] != '\0') { entry[AUTHDATA_KEY_IDTAG(compact)] = (const char*) idTag; } JsonObject idTagInfo; if (compact) { idTagInfo = entry; } else { idTagInfo = entry.createNestedObject(AUTHDATA_KEY_IDTAGINFO); } if (expiryDate) { char buf [JSONDATE_LENGTH + 1]; if (expiryDate->toJsonString(buf, JSONDATE_LENGTH + 1)) { idTagInfo[AUTHDATA_KEY_EXPIRYDATE(compact)] = buf; } } if (parentIdTag) { idTagInfo[AUTHDATA_KEY_PARENTIDTAG(compact)] = (const char *) parentIdTag; } if (status != AuthorizationStatus::Accepted) { idTagInfo[AUTHDATA_KEY_STATUS(compact)] = serializeAuthorizationStatus(status); } else if (!compact) { idTagInfo[AUTHDATA_KEY_STATUS(compact)] = serializeAuthorizationStatus(AuthorizationStatus::Invalid); } } const char *AuthorizationData::getIdTag() const { return idTag; } Timestamp *AuthorizationData::getExpiryDate() const { return expiryDate.get(); } const char *AuthorizationData::getParentIdTag() const { return parentIdTag; } AuthorizationStatus AuthorizationData::getAuthorizationStatus() const { return status; } void AuthorizationData::reset() { idTag[0] = '\0'; } const char *MicroOcpp::serializeAuthorizationStatus(AuthorizationStatus status) { switch (status) { case (AuthorizationStatus::Accepted): return "Accepted"; case (AuthorizationStatus::Blocked): return "Blocked"; case (AuthorizationStatus::Expired): return "Expired"; case (AuthorizationStatus::Invalid): return "Invalid"; case (AuthorizationStatus::ConcurrentTx): return "ConcurrentTx"; default: return "UNDEFINED"; } } MicroOcpp::AuthorizationStatus MicroOcpp::deserializeAuthorizationStatus(const char *cstr) { if (!cstr) { return AuthorizationStatus::UNDEFINED; } if (!strcmp(cstr, "Accepted")) { return AuthorizationStatus::Accepted; } else if (!strcmp(cstr, "Blocked")) { return AuthorizationStatus::Blocked; } else if (!strcmp(cstr, "Expired")) { return AuthorizationStatus::Expired; } else if (!strcmp(cstr, "Invalid")) { return AuthorizationStatus::Invalid; } else if (!strcmp(cstr, "ConcurrentTx")) { return AuthorizationStatus::ConcurrentTx; } else { return AuthorizationStatus::UNDEFINED; } } #endif //MO_ENABLE_LOCAL_AUTH ================================================ FILE: src/MicroOcpp/Model/Authorization/AuthorizationData.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_AUTHORIZATIONDATA_H #define MO_AUTHORIZATIONDATA_H #include #if MO_ENABLE_LOCAL_AUTH #include #include #include #include #include namespace MicroOcpp { enum class AuthorizationStatus : uint8_t { Accepted, Blocked, Expired, Invalid, ConcurrentTx, UNDEFINED //not part of OCPP 1.6 }; #define AUTHDATA_KEY_IDTAG(COMPACT) (COMPACT ? "it" : "idTag") #define AUTHDATA_KEY_IDTAGINFO "idTagInfo" #define AUTHDATA_KEY_EXPIRYDATE(COMPACT) (COMPACT ? "ed" : "expiryDate") #define AUTHDATA_KEY_PARENTIDTAG(COMPACT) (COMPACT ? "pi" : "parentIdTag") #define AUTHDATA_KEY_STATUS(COMPACT) (COMPACT ? "st" : "status") #define AUTHORIZATIONSTATUS_LEN_MAX (sizeof("ConcurrentTx") - 1) //max length of serialized AuthStatus const char *serializeAuthorizationStatus(AuthorizationStatus status); AuthorizationStatus deserializeAuthorizationStatus(const char *cstr); class AuthorizationData : public MemoryManaged { private: //data structure optimized for memory consumption char *parentIdTag = nullptr; std::unique_ptr expiryDate; char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; AuthorizationStatus status = AuthorizationStatus::UNDEFINED; public: AuthorizationData(); AuthorizationData(AuthorizationData&& other); ~AuthorizationData(); AuthorizationData& operator=(AuthorizationData&& other); void readJson(JsonObject entry, bool compact = false); //compact: compressed representation for flash storage size_t getJsonCapacity() const; void writeJson(JsonObject& entry, bool compact = false); //compact: compressed representation for flash storage const char *getIdTag() const; Timestamp *getExpiryDate() const; const char *getParentIdTag() const; AuthorizationStatus getAuthorizationStatus() const; void reset(); }; } #endif //MO_ENABLE_LOCAL_AUTH #endif ================================================ FILE: src/MicroOcpp/Model/Authorization/AuthorizationList.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_LOCAL_AUTH #include #include #include #include using namespace MicroOcpp; AuthorizationList::AuthorizationList() : MemoryManaged("v16.Authorization.AuthorizationList"), localAuthorizationList(makeVector(getMemoryTag())) { } AuthorizationList::~AuthorizationList() { } MicroOcpp::AuthorizationData *AuthorizationList::get(const char *idTag) { //binary search if (!idTag) { return nullptr; } int l = 0; int r = ((int) localAuthorizationList.size()) - 1; while (l <= r) { auto m = (r + l) / 2; auto diff = strcmp(localAuthorizationList[m].getIdTag(), idTag); if (diff < 0) { l = m + 1; } else if (diff > 0) { r = m - 1; } else { return &localAuthorizationList[m]; } } return nullptr; } bool AuthorizationList::readJson(JsonArray authlistJson, int listVersion, bool differential, bool compact) { if (compact) { //compact representations don't contain remove commands differential = false; } for (size_t i = 0; i < authlistJson.size(); i++) { //check if JSON object is valid if (!authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAG(compact))) { return false; } } auto authlist_index = makeVector(getMemoryTag()); auto remove_list = makeVector(getMemoryTag()); unsigned int resultingListLength = 0; if (!differential) { //every entry will insert an idTag resultingListLength = authlistJson.size(); } else { //update type is differential; only unkown entries will insert an idTag resultingListLength = localAuthorizationList.size(); //also, build index here authlist_index.resize(authlistJson.size(), -1); for (size_t i = 0; i < authlistJson.size(); i++) { //check if locally stored auth info is present; if yes, apply it to the index AuthorizationData *found = get(authlistJson[i][AUTHDATA_KEY_IDTAG(compact)]); if (found) { authlist_index[i] = (int) (found - localAuthorizationList.data()); //remove or update? if (!authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAGINFO)) { //this entry should be removed found->reset(); //mark for deletion remove_list.push_back((int) (found - localAuthorizationList.data())); resultingListLength--; } //else: this entry should be updated } else { //insert or ignore? if (authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAGINFO)) { //add resultingListLength++; } //else: ignore } } } if (resultingListLength > MO_LocalAuthListMaxLength) { MO_DBG_WARN("localAuthList capacity exceeded"); return false; } //apply new list if (compact) { localAuthorizationList.clear(); for (size_t i = 0; i < authlistJson.size(); i++) { localAuthorizationList.emplace_back(); localAuthorizationList.back().readJson(authlistJson[i], compact); } } else if (differential) { for (size_t i = 0; i < authlistJson.size(); i++) { //is entry a remove command? if (!authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAGINFO)) { continue; //yes, remove command, will be deleted afterwards } //update, or insert if (authlist_index[i] < 0) { //auth list does not contain idTag yet -> insert new entry //reuse removed AuthData object? if (!remove_list.empty()) { //yes, reuse authlist_index[i] = remove_list.back(); remove_list.pop_back(); } else { //no, create new authlist_index[i] = localAuthorizationList.size(); localAuthorizationList.emplace_back(); } } localAuthorizationList[authlist_index[i]].readJson(authlistJson[i], compact); } } else { localAuthorizationList.clear(); for (size_t i = 0; i < authlistJson.size(); i++) { if (authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAGINFO)) { localAuthorizationList.emplace_back(); localAuthorizationList.back().readJson(authlistJson[i], compact); } } } localAuthorizationList.erase(std::remove_if(localAuthorizationList.begin(), localAuthorizationList.end(), [] (const AuthorizationData& elem) { return elem.getIdTag()[0] == '\0'; //"" means no idTag --> marked for removal }), localAuthorizationList.end()); std::sort(localAuthorizationList.begin(), localAuthorizationList.end(), [] (const AuthorizationData& lhs, const AuthorizationData& rhs) { return strcmp(lhs.getIdTag(), rhs.getIdTag()) < 0; }); this->listVersion = listVersion; if (localAuthorizationList.empty()) { this->listVersion = 0; } return true; } void AuthorizationList::clear() { localAuthorizationList.clear(); listVersion = 0; } size_t AuthorizationList::getJsonCapacity() { size_t res = JSON_ARRAY_SIZE(localAuthorizationList.size()); for (auto& entry : localAuthorizationList) { res += entry.getJsonCapacity(); } return res; } void AuthorizationList::writeJson(JsonArray authListOut, bool compact) { for (auto& entry : localAuthorizationList) { JsonObject entryJson = authListOut.createNestedObject(); entry.writeJson(entryJson, compact); } } size_t AuthorizationList::size() { return localAuthorizationList.size(); } #endif //MO_ENABLE_LOCAL_AUTH ================================================ FILE: src/MicroOcpp/Model/Authorization/AuthorizationList.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_AUTHORIZATIONLIST_H #define MO_AUTHORIZATIONLIST_H #include #if MO_ENABLE_LOCAL_AUTH #include #include #ifndef MO_LocalAuthListMaxLength #define MO_LocalAuthListMaxLength 48 #endif #ifndef MO_SendLocalListMaxLength #define MO_SendLocalListMaxLength MO_LocalAuthListMaxLength #endif namespace MicroOcpp { class AuthorizationList : public MemoryManaged { private: int listVersion = 0; Vector localAuthorizationList; //sorted list public: AuthorizationList(); ~AuthorizationList(); AuthorizationData *get(const char *idTag); bool readJson(JsonArray localAuthorizationList, int listVersion, bool differential = false, bool compact = false); //compact: if true, then use compact non-ocpp representation void clear(); size_t getJsonCapacity(); void writeJson(JsonArray authListOut, bool compact = false); int getListVersion() {return listVersion;} size_t size(); //used in unit tests }; } #endif //MO_ENABLE_LOCAL_AUTH #endif ================================================ FILE: src/MicroOcpp/Model/Authorization/AuthorizationService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_LOCAL_AUTH #include #include #include #include #include #include #include #include #include #include #include #define MO_LOCALAUTHORIZATIONLIST_FN (MO_FILENAME_PREFIX "localauth.jsn") using namespace MicroOcpp; AuthorizationService::AuthorizationService(Context& context, std::shared_ptr filesystem) : MemoryManaged("v16.Authorization.AuthorizationService"), context(context), filesystem(filesystem) { localAuthListEnabledBool = declareConfiguration("LocalAuthListEnabled", true, CONFIGURATION_FN, false, true); declareConfiguration("LocalAuthListMaxLength", MO_LocalAuthListMaxLength, CONFIGURATION_VOLATILE, true); declareConfiguration("SendLocalListMaxLength", MO_SendLocalListMaxLength, CONFIGURATION_VOLATILE, true); if (!localAuthListEnabledBool) { MO_DBG_ERR("initialization error"); } context.getOperationRegistry().registerOperation("GetLocalListVersion", [&context] () { return new Ocpp16::GetLocalListVersion(context.getModel());}); context.getOperationRegistry().registerOperation("SendLocalList", [this] () { return new Ocpp16::SendLocalList(*this);}); loadLists(); } AuthorizationService::~AuthorizationService() { } bool AuthorizationService::loadLists() { if (!filesystem) { MO_DBG_WARN("no fs access"); return true; } size_t msize = 0; if (filesystem->stat(MO_LOCALAUTHORIZATIONLIST_FN, &msize) != 0) { MO_DBG_DEBUG("no local authorization list stored already"); return true; } auto doc = FilesystemUtils::loadJson(filesystem, MO_LOCALAUTHORIZATIONLIST_FN, getMemoryTag()); if (!doc) { MO_DBG_ERR("failed to load %s", MO_LOCALAUTHORIZATIONLIST_FN); return false; } JsonObject root = doc->as(); int listVersion = root["listVersion"] | 0; if (!localAuthorizationList.readJson(root["localAuthorizationList"].as(), listVersion, false, true)) { MO_DBG_ERR("list read failure"); return false; } return true; } AuthorizationData *AuthorizationService::getLocalAuthorization(const char *idTag) { if (!localAuthListEnabled()) { return nullptr; //auth cache will follow } auto authData = localAuthorizationList.get(idTag); if (!authData) { return nullptr; } //check status if (authData->getAuthorizationStatus() != AuthorizationStatus::Accepted) { MO_DBG_DEBUG("idTag %s local auth status %s", idTag, serializeAuthorizationStatus(authData->getAuthorizationStatus())); return authData; } return authData; } int AuthorizationService::getLocalListVersion() { return localAuthorizationList.getListVersion(); } size_t AuthorizationService::getLocalListSize() { return localAuthorizationList.size(); } bool AuthorizationService::localAuthListEnabled() const { return localAuthListEnabledBool && localAuthListEnabledBool->getBool(); } bool AuthorizationService::updateLocalList(JsonArray localAuthorizationListJson, int listVersion, bool differential) { //TC_043_3_CS-Send Local Authorization List - Failed //return false; bool success = localAuthorizationList.readJson(localAuthorizationListJson, listVersion, differential, false); if (success) { auto doc = initJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(3) + localAuthorizationList.getJsonCapacity()); JsonObject root = doc.to(); root["listVersion"] = listVersion; JsonArray authListCompact = root.createNestedArray("localAuthorizationList"); localAuthorizationList.writeJson(authListCompact, true); success = FilesystemUtils::storeJson(filesystem, MO_LOCALAUTHORIZATIONLIST_FN, doc); if (!success) { loadLists(); } } return success; } void AuthorizationService::notifyAuthorization(const char *idTag, JsonObject idTagInfo) { //check local list conflicts. In future: also update authorization cache if (!localAuthListEnabled()) { return; //auth cache will follow } if (!idTagInfo.containsKey("status")) { return; //empty idTagInfo } auto localInfo = localAuthorizationList.get(idTag); if (!localInfo) { return; } //check for conflicts auto incomingStatus = deserializeAuthorizationStatus(idTagInfo["status"]); auto localStatus = localInfo->getAuthorizationStatus(); if (incomingStatus == AuthorizationStatus::UNDEFINED) { //ignore invalid messages (handled elsewhere) return; } if (incomingStatus == AuthorizationStatus::ConcurrentTx) { //incoming status ConcurrentTx is equivalent to local Accepted incomingStatus = AuthorizationStatus::Accepted; } if (localStatus == AuthorizationStatus::Accepted && localInfo->getExpiryDate()) { //check for expiry auto& t_now = context.getModel().getClock().now(); if (t_now > *localInfo->getExpiryDate()) { MO_DBG_DEBUG("local auth expired"); localStatus = AuthorizationStatus::Expired; } } bool equivalent = true; if (incomingStatus != localStatus) { MO_DBG_WARN("local auth list status conflict"); equivalent = false; } //check if parentIdTag definitions mismatch if (equivalent && strcmp(localInfo->getParentIdTag() ? localInfo->getParentIdTag() : "", idTagInfo["parentIdTag"] | "")) { MO_DBG_WARN("local auth list parentIdTag conflict"); equivalent = false; } MO_DBG_DEBUG("idTag %s fully evaluated: %s conflict", idTag, equivalent ? "no" : "contains"); if (!equivalent) { //send error code "LocalListConflict" to server ChargePointStatus cpStatus = ChargePointStatus_UNDEFINED; if (context.getModel().getNumConnectors() > 0) { cpStatus = context.getModel().getConnector(0)->getStatus(); } auto statusNotification = makeRequest(new Ocpp16::StatusNotification( 0, cpStatus, //will be determined in StatusNotification::initiate context.getModel().getClock().now(), "LocalListConflict")); statusNotification->setTimeout(60000); context.initiateRequest(std::move(statusNotification)); } } #endif //MO_ENABLE_LOCAL_AUTH ================================================ FILE: src/MicroOcpp/Model/Authorization/AuthorizationService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_AUTHORIZATIONSERVICE_H #define MO_AUTHORIZATIONSERVICE_H #include #if MO_ENABLE_LOCAL_AUTH #include #include #include #include namespace MicroOcpp { class Context; class AuthorizationService : public MemoryManaged { private: Context& context; std::shared_ptr filesystem; AuthorizationList localAuthorizationList; std::shared_ptr localAuthListEnabledBool; public: AuthorizationService(Context& context, std::shared_ptr filesystem); ~AuthorizationService(); bool loadLists(); AuthorizationData *getLocalAuthorization(const char *idTag); int getLocalListVersion(); bool localAuthListEnabled() const; size_t getLocalListSize(); //number of entries in current localAuthList; used in unit tests bool updateLocalList(JsonArray localAuthorizationListJson, int listVersion, bool differential); void notifyAuthorization(const char *idTag, JsonObject idTagInfo); }; } #endif //MO_ENABLE_LOCAL_AUTH #endif ================================================ FILE: src/MicroOcpp/Model/Authorization/IdToken.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include #include using namespace MicroOcpp; IdToken::IdToken(const char *token, Type type, const char *memoryTag) : MemoryManaged(memoryTag ? memoryTag : "v201.Authorization.IdToken"), type(type) { if (token) { auto ret = snprintf(idToken, MO_IDTOKEN_LEN_MAX + 1, "%s", token); if (ret < 0 || ret >= MO_IDTOKEN_LEN_MAX + 1) { MO_DBG_ERR("invalid token"); *idToken = '\0'; } } else { *idToken = '\0'; } } IdToken::IdToken(const IdToken& other, const char *memoryTag) : IdToken(other.idToken, other.type, memoryTag ? memoryTag : other.getMemoryTag()) { } bool IdToken::parseCstr(const char *token, const char *typeCstr) { if (!token || !typeCstr) { return false; } if (!strcmp(typeCstr, "Central")) { type = Type::Central; } else if (!strcmp(typeCstr, "eMAID")) { type = Type::eMAID; } else if (!strcmp(typeCstr, "ISO14443")) { type = Type::ISO14443; } else if (!strcmp(typeCstr, "ISO15693")) { type = Type::ISO15693; } else if (!strcmp(typeCstr, "KeyCode")) { type = Type::KeyCode; } else if (!strcmp(typeCstr, "Local")) { type = Type::Local; } else if (!strcmp(typeCstr, "MacAddress")) { type = Type::MacAddress; } else if (!strcmp(typeCstr, "NoAuthorization")) { type = Type::NoAuthorization; } else { return false; } auto ret = snprintf(idToken, sizeof(idToken), "%s", token); if (ret < 0 || (size_t)ret >= sizeof(idToken)) { return false; } return true; } const char *IdToken::get() const { return idToken; } const char *IdToken::getTypeCstr() const { const char *res = ""; switch (type) { case Type::UNDEFINED: MO_DBG_ERR("internal error"); break; case Type::Central: res = "Central"; break; case Type::eMAID: res = "eMAID"; break; case Type::ISO14443: res = "ISO14443"; break; case Type::ISO15693: res = "ISO15693"; break; case Type::KeyCode: res = "KeyCode"; break; case Type::Local: res = "Local"; break; case Type::MacAddress: res = "MacAddress"; break; case Type::NoAuthorization: res = "NoAuthorization"; break; } return res; } bool IdToken::equals(const IdToken& other) { return type == other.type && !strcmp(idToken, other.idToken); } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Model/Authorization/IdToken.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_IDTOKEN_H #define MO_IDTOKEN_H #include #if MO_ENABLE_V201 #include #include #define MO_IDTOKEN_LEN_MAX 36 namespace MicroOcpp { // IdTokenType (2.28) class IdToken : public MemoryManaged { public: // IdTokenEnumType (3.43) enum class Type : uint8_t { Central, eMAID, ISO14443, ISO15693, KeyCode, Local, MacAddress, NoAuthorization, UNDEFINED }; private: char idToken [MO_IDTOKEN_LEN_MAX + 1]; Type type = Type::UNDEFINED; public: IdToken(const char *token = nullptr, Type type = Type::ISO14443, const char *memoryTag = nullptr); IdToken(const IdToken& other, const char *memoryTag = nullptr); bool parseCstr(const char *token, const char *typeCstr); const char *get() const; const char *getTypeCstr() const; bool equals(const IdToken& other); }; } // namespace MicroOcpp #endif // MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Model/Availability/AvailabilityService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include #include #include #include #include #include using namespace MicroOcpp; AvailabilityServiceEvse::AvailabilityServiceEvse(Context& context, AvailabilityService& availabilityService, unsigned int evseId) : MemoryManaged("v201.Availability.AvailabilityServiceEvse"), context(context), availabilityService(availabilityService), evseId(evseId) { } void AvailabilityServiceEvse::loop() { if (evseId >= 1) { auto status = getStatus(); if (status != reportedStatus && context.getModel().getClock().now() >= MIN_TIME) { auto statusNotification = makeRequest(new Ocpp201::StatusNotification(evseId, status, context.getModel().getClock().now())); statusNotification->setTimeout(0); context.initiateRequest(std::move(statusNotification)); reportedStatus = status; return; } } } void AvailabilityServiceEvse::setConnectorPluggedInput(std::function connectorPluggedInput) { this->connectorPluggedInput = connectorPluggedInput; } void AvailabilityServiceEvse::setOccupiedInput(std::function occupiedInput) { this->occupiedInput = occupiedInput; } ChargePointStatus AvailabilityServiceEvse::getStatus() { ChargePointStatus res = ChargePointStatus_UNDEFINED; if (isFaulted()) { res = ChargePointStatus_Faulted; } else if (!isAvailable()) { res = ChargePointStatus_Unavailable; } #if MO_ENABLE_RESERVATION else if (context.getModel().getReservationService() && context.getModel().getReservationService()->getReservation(evseId)) { res = ChargePointStatus_Reserved; } #endif else if ((!connectorPluggedInput || !connectorPluggedInput()) && //no vehicle plugged (!occupiedInput || !occupiedInput())) { //occupied override clear res = ChargePointStatus_Available; } else { res = ChargePointStatus_Occupied; } return res; } void AvailabilityServiceEvse::setUnavailable(void *requesterId) { for (size_t i = 0; i < MO_INOPERATIVE_REQUESTERS_MAX; i++) { if (!unavailableRequesters[i]) { unavailableRequesters[i] = requesterId; return; } } MO_DBG_ERR("exceeded max. unavailable requesters"); } void AvailabilityServiceEvse::setAvailable(void *requesterId) { for (size_t i = 0; i < MO_INOPERATIVE_REQUESTERS_MAX; i++) { if (unavailableRequesters[i] == requesterId) { unavailableRequesters[i] = nullptr; return; } } MO_DBG_ERR("could not find unavailable requester"); } ChangeAvailabilityStatus AvailabilityServiceEvse::changeAvailability(bool operative) { if (operative) { setAvailable(this); } else { setUnavailable(this); } if (!operative) { if (isAvailable()) { return ChangeAvailabilityStatus::Scheduled; } if (evseId == 0) { for (unsigned int id = 1; id < MO_NUM_EVSEID; id++) { if (availabilityService.getEvse(id) && availabilityService.getEvse(id)->isAvailable()) { return ChangeAvailabilityStatus::Scheduled; } } } } return ChangeAvailabilityStatus::Accepted; } void AvailabilityServiceEvse::setFaulted(void *requesterId) { for (size_t i = 0; i < MO_FAULTED_REQUESTERS_MAX; i++) { if (!faultedRequesters[i]) { faultedRequesters[i] = requesterId; return; } } MO_DBG_ERR("exceeded max. faulted requesters"); } void AvailabilityServiceEvse::resetFaulted(void *requesterId) { for (size_t i = 0; i < MO_FAULTED_REQUESTERS_MAX; i++) { if (faultedRequesters[i] == requesterId) { faultedRequesters[i] = nullptr; return; } } MO_DBG_ERR("could not find faulted requester"); } bool AvailabilityServiceEvse::isAvailable() { auto txService = context.getModel().getTransactionService(); auto txEvse = txService ? txService->getEvse(evseId) : nullptr; if (txEvse) { if (txEvse->getTransaction() && txEvse->getTransaction()->started && !txEvse->getTransaction()->stopped) { return true; } } if (evseId > 0) { if (availabilityService.getEvse(0) && !availabilityService.getEvse(0)->isAvailable()) { return false; } } for (size_t i = 0; i < MO_INOPERATIVE_REQUESTERS_MAX; i++) { if (unavailableRequesters[i]) { return false; } } return true; } bool AvailabilityServiceEvse::isFaulted() { for (size_t i = 0; i < MO_FAULTED_REQUESTERS_MAX; i++) { if (faultedRequesters[i]) { return true; } } return false; } AvailabilityService::AvailabilityService(Context& context, size_t numEvses) : MemoryManaged("v201.Availability.AvailabilityService"), context(context) { for (size_t i = 0; i < numEvses && i < MO_NUM_EVSEID; i++) { evses[i] = new AvailabilityServiceEvse(context, *this, (unsigned int)i); } context.getOperationRegistry().registerOperation("StatusNotification", [&context] () { return new Ocpp16::StatusNotification(-1, ChargePointStatus_UNDEFINED, Timestamp());}); context.getOperationRegistry().registerOperation("ChangeAvailability", [this] () { return new Ocpp201::ChangeAvailability(*this);}); } AvailabilityService::~AvailabilityService() { for (size_t i = 0; i < MO_NUM_EVSEID && evses[i]; i++) { delete evses[i]; } } void AvailabilityService::loop() { for (size_t i = 0; i < MO_NUM_EVSEID && evses[i]; i++) { evses[i]->loop(); } } AvailabilityServiceEvse *AvailabilityService::getEvse(unsigned int evseId) { if (evseId >= MO_NUM_EVSEID) { MO_DBG_ERR("invalid arg"); return nullptr; } return evses[evseId]; } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Model/Availability/AvailabilityService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License /* * Implementation of the UCs G01, G03, G04. * * G02 (Heartbeat) is implemented in the HeartbeatService */ #ifndef MO_AVAILABILITYSERVICE_H #define MO_AVAILABILITYSERVICE_H #include #if MO_ENABLE_V201 #include #include #include #include #include #ifndef MO_INOPERATIVE_REQUESTERS_MAX #define MO_INOPERATIVE_REQUESTERS_MAX 3 #endif #ifndef MO_FAULTED_REQUESTERS_MAX #define MO_FAULTED_REQUESTERS_MAX 3 #endif namespace MicroOcpp { class Context; class AvailabilityService; class AvailabilityServiceEvse : public MemoryManaged { private: Context& context; AvailabilityService& availabilityService; const unsigned int evseId; std::function connectorPluggedInput; std::function occupiedInput; //instead of Available, go into Occupied void *unavailableRequesters [MO_INOPERATIVE_REQUESTERS_MAX] = {nullptr}; void *faultedRequesters [MO_FAULTED_REQUESTERS_MAX] = {nullptr}; ChargePointStatus reportedStatus = ChargePointStatus_UNDEFINED; public: AvailabilityServiceEvse(Context& context, AvailabilityService& availabilityService, unsigned int evseId); void loop(); void setConnectorPluggedInput(std::function connectorPluggedInput); void setOccupiedInput(std::function occupiedInput); ChargePointStatus getStatus(); void setUnavailable(void *requesterId); void setAvailable(void *requesterId); ChangeAvailabilityStatus changeAvailability(bool operative); void setFaulted(void *requesterId); void resetFaulted(void *requesterId); bool isAvailable(); bool isFaulted(); }; class AvailabilityService : public MemoryManaged { private: Context& context; AvailabilityServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; public: AvailabilityService(Context& context, size_t numEvses); ~AvailabilityService(); void loop(); AvailabilityServiceEvse *getEvse(unsigned int evseId); }; } // namespace MicroOcpp #endif // MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Model/Availability/ChangeAvailabilityStatus.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CHANGEAVAILABILITYSTATUS_H #define MO_CHANGEAVAILABILITYSTATUS_H #include #if MO_ENABLE_V201 #include namespace MicroOcpp { enum class ChangeAvailabilityStatus : uint8_t { Accepted, Rejected, Scheduled }; } //namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Model/Boot/BootService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include using namespace MicroOcpp; unsigned int PreBootQueue::getFrontRequestOpNr() { if (!activatedPostBootCommunication) { return 0; } return VolatileRequestQueue::getFrontRequestOpNr(); } void PreBootQueue::activatePostBootCommunication() { activatedPostBootCommunication = true; } RegistrationStatus MicroOcpp::deserializeRegistrationStatus(const char *serialized) { if (!strcmp(serialized, "Accepted")) { return RegistrationStatus::Accepted; } else if (!strcmp(serialized, "Pending")) { return RegistrationStatus::Pending; } else if (!strcmp(serialized, "Rejected")) { return RegistrationStatus::Rejected; } else { MO_DBG_ERR("deserialization error"); return RegistrationStatus::UNDEFINED; } } BootService::BootService(Context& context, std::shared_ptr filesystem) : MemoryManaged("v16.Boot.BootService"), context(context), filesystem(filesystem), cpCredentials{makeString(getMemoryTag())} { context.getRequestQueue().setPreBootSendQueue(&preBootQueue); //register PreBootQueue in RequestQueue module //if transactions can start before the BootNotification succeeds preBootTransactionsBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", false); if (!preBootTransactionsBool) { MO_DBG_ERR("initialization error"); } //Register message handler for TriggerMessage operation context.getOperationRegistry().registerOperation("BootNotification", [this] () { return new Ocpp16::BootNotification(this->context.getModel(), getChargePointCredentials());}); } void BootService::loop() { if (!executedFirstTime) { executedFirstTime = true; firstExecutionTimestamp = mocpp_tick_ms(); } if (!executedLongTime && mocpp_tick_ms() - firstExecutionTimestamp >= MO_BOOTSTATS_LONGTIME_MS) { executedLongTime = true; MO_DBG_DEBUG("boot success timer reached"); configuration_clean_unused(); BootStats bootstats; loadBootStats(filesystem, bootstats); bootstats.lastBootSuccess = bootstats.bootNr; storeBootStats(filesystem, bootstats); } preBootQueue.loop(); if (!activatedPostBootCommunication && status == RegistrationStatus::Accepted) { preBootQueue.activatePostBootCommunication(); activatedPostBootCommunication = true; } if (!activatedModel && (status == RegistrationStatus::Accepted || preBootTransactionsBool->getBool())) { context.getModel().activateTasks(); activatedModel = true; } if (status == RegistrationStatus::Accepted) { return; } if (mocpp_tick_ms() - lastBootNotification < (interval_s * 1000UL)) { return; } /* * Create BootNotification. The BootNotifaction object will fetch its paremeters from * this class and notify this class about the response */ auto bootNotification = makeRequest(new Ocpp16::BootNotification(context.getModel(), getChargePointCredentials())); bootNotification->setTimeout(interval_s * 1000UL); context.getRequestQueue().sendRequestPreBoot(std::move(bootNotification)); lastBootNotification = mocpp_tick_ms(); } void BootService::setChargePointCredentials(JsonObject credentials) { auto written = serializeJson(credentials, cpCredentials); if (written < 2) { MO_DBG_ERR("serialization error"); cpCredentials = "{}"; } } void BootService::setChargePointCredentials(const char *credentials) { cpCredentials = credentials; if (cpCredentials.size() < 2) { cpCredentials = "{}"; } } std::unique_ptr BootService::getChargePointCredentials() { if (cpCredentials.size() <= 2) { return createEmptyDocument(); } std::unique_ptr doc; size_t capacity = JSON_OBJECT_SIZE(9) + cpCredentials.size(); DeserializationError err = DeserializationError::NoMemory; while (err == DeserializationError::NoMemory && capacity <= MO_MAX_JSON_CAPACITY) { doc = makeJsonDoc(getMemoryTag(), capacity); err = deserializeJson(*doc, cpCredentials); capacity *= 2; } if (!err) { return doc; } else { MO_DBG_ERR("could not parse stored credentials: %s", err.c_str()); return nullptr; } } void BootService::notifyRegistrationStatus(RegistrationStatus status) { this->status = status; lastBootNotification = mocpp_tick_ms(); } void BootService::setRetryInterval(unsigned long interval_s) { if (interval_s == 0) { this->interval_s = MO_BOOT_INTERVAL_DEFAULT; } else { this->interval_s = interval_s; } lastBootNotification = mocpp_tick_ms(); } bool BootService::loadBootStats(std::shared_ptr filesystem, BootStats& bstats) { if (!filesystem) { return false; } size_t msize = 0; if (filesystem->stat(MO_FILENAME_PREFIX "bootstats.jsn", &msize) == 0) { bool success = true; auto json = FilesystemUtils::loadJson(filesystem, MO_FILENAME_PREFIX "bootstats.jsn", "v16.Boot.BootService"); if (json) { int bootNrIn = (*json)["bootNr"] | -1; if (bootNrIn >= 0 && bootNrIn <= std::numeric_limits::max()) { bstats.bootNr = (uint16_t) bootNrIn; } else { success = false; } int lastSuccessIn = (*json)["lastSuccess"] | -1; if (lastSuccessIn >= 0 && lastSuccessIn <= std::numeric_limits::max()) { bstats.lastBootSuccess = (uint16_t) lastSuccessIn; } else { success = false; } const char *microOcppVersionIn = (*json)["MicroOcppVersion"] | (const char*)nullptr; if (microOcppVersionIn) { auto ret = snprintf(bstats.microOcppVersion, sizeof(bstats.microOcppVersion), "%s", microOcppVersionIn); if (ret < 0 || (size_t)ret >= sizeof(bstats.microOcppVersion)) { success = false; } } //else: version specifier can be missing after upgrade from pre 1.2.0 version } else { success = false; } if (!success) { MO_DBG_ERR("bootstats corrupted"); filesystem->remove(MO_FILENAME_PREFIX "bootstats.jsn"); bstats = BootStats(); } return success; } else { return false; } } bool BootService::storeBootStats(std::shared_ptr filesystem, BootStats& bstats) { if (!filesystem) { return false; } auto json = initJsonDoc("v16.Boot.BootService", JSON_OBJECT_SIZE(3)); json["bootNr"] = bstats.bootNr; json["lastSuccess"] = bstats.lastBootSuccess; json["MicroOcppVersion"] = (const char*)bstats.microOcppVersion; return FilesystemUtils::storeJson(filesystem, MO_FILENAME_PREFIX "bootstats.jsn", json); } bool BootService::recover(std::shared_ptr filesystem, BootStats& bstats) { if (!filesystem) { return false; } bool success = FilesystemUtils::remove_if(filesystem, [] (const char *fname) -> bool { return !strncmp(fname, "sd", strlen("sd")) || !strncmp(fname, "tx", strlen("tx")) || !strncmp(fname, "sc-", strlen("sc-")) || !strncmp(fname, "reservation", strlen("reservation")) || !strncmp(fname, "client-state", strlen("client-state")); }); MO_DBG_ERR("clear local state files (recovery): %s", success ? "success" : "not completed"); return success; } bool BootService::migrate(std::shared_ptr filesystem, BootStats& bstats) { if (!filesystem) { return false; } bool success = true; if (strcmp(bstats.microOcppVersion, MO_VERSION)) { MO_DBG_INFO("migrate persistent storage to MO v" MO_VERSION); success = FilesystemUtils::remove_if(filesystem, [] (const char *fname) -> bool { return !strncmp(fname, "sd", strlen("sd")) || !strncmp(fname, "tx", strlen("tx")) || !strncmp(fname, "op", strlen("op")) || !strncmp(fname, "sc-", strlen("sc-")) || !strcmp(fname, "client-state.cnf") || !strcmp(fname, "arduino-ocpp.cnf") || !strcmp(fname, "ocpp-creds.jsn"); }); snprintf(bstats.microOcppVersion, sizeof(bstats.microOcppVersion), "%s", MO_VERSION); MO_DBG_DEBUG("clear local state files (migration): %s", success ? "success" : "not completed"); } return success; } ================================================ FILE: src/MicroOcpp/Model/Boot/BootService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_BOOTSERVICE_H #define MO_BOOTSERVICE_H #include #include #include #include #include #define MO_BOOT_INTERVAL_DEFAULT 60 #ifndef MO_BOOTSTATS_LONGTIME_MS #define MO_BOOTSTATS_LONGTIME_MS 180 * 1000 #endif namespace MicroOcpp { #define MO_BOOTSTATS_VERSION_SIZE 10 struct BootStats { uint16_t bootNr = 0; uint16_t lastBootSuccess = 0; uint16_t getBootFailureCount() { return bootNr - lastBootSuccess; } char microOcppVersion [MO_BOOTSTATS_VERSION_SIZE] = {'\0'}; }; enum class RegistrationStatus { Accepted, Pending, Rejected, UNDEFINED }; RegistrationStatus deserializeRegistrationStatus(const char *serialized); class PreBootQueue : public VolatileRequestQueue { private: bool activatedPostBootCommunication = false; public: unsigned int getFrontRequestOpNr() override; //override FrontRequestOpNr behavior: in PreBoot mode, always return 0 to avoid other RequestEmitters from sending msgs void activatePostBootCommunication(); //end PreBoot mode, now send Requests normally }; class Context; class BootService : public MemoryManaged { private: Context& context; std::shared_ptr filesystem; PreBootQueue preBootQueue; unsigned long interval_s = MO_BOOT_INTERVAL_DEFAULT; unsigned long lastBootNotification = -1UL / 2; RegistrationStatus status = RegistrationStatus::Pending; String cpCredentials; std::shared_ptr preBootTransactionsBool; bool activatedModel = false; bool activatedPostBootCommunication = false; unsigned long firstExecutionTimestamp = 0; bool executedFirstTime = false; bool executedLongTime = false; public: BootService(Context& context, std::shared_ptr filesystem); void loop(); void setChargePointCredentials(JsonObject credentials); void setChargePointCredentials(const char *credentials); //credentials: serialized BootNotification payload std::unique_ptr getChargePointCredentials(); void notifyRegistrationStatus(RegistrationStatus status); void setRetryInterval(unsigned long interval); static bool loadBootStats(std::shared_ptr filesystem, BootStats& bstats); static bool storeBootStats(std::shared_ptr filesystem, BootStats& bstats); static bool recover(std::shared_ptr filesystem, BootStats& bstats); //delete all persistent files which could lead to a crash static bool migrate(std::shared_ptr filesystem, BootStats& bstats); //migrate persistent storage if running on a new MO version }; } #endif ================================================ FILE: src/MicroOcpp/Model/Certificates/Certificate.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_CERT_MGMT #include #include bool ocpp_cert_equals(const ocpp_cert_hash *h1, const ocpp_cert_hash *h2) { return h1->hashAlgorithm == h2->hashAlgorithm && h1->serialNumberLen == h2->serialNumberLen && !memcmp(h1->serialNumber, h2->serialNumber, h1->serialNumberLen) && !memcmp(h1->issuerNameHash, h2->issuerNameHash, HashAlgorithmSize(h1->hashAlgorithm)) && !memcmp(h1->issuerKeyHash, h2->issuerKeyHash, HashAlgorithmSize(h1->hashAlgorithm)); } int ocpp_cert_bytes_to_hex(char *dst, size_t dst_size, const unsigned char *src, size_t src_len) { if (!dst || !dst_size || !src) { return -1; } dst[0] = '\0'; size_t hexLen = 2 * src_len; // hex-encoding needs two characters per byte if (dst_size < hexLen + 1) { // buf will hold hex-encoding + terminating null return -1; } for (size_t i = 0; i < src_len; i++) { snprintf(dst, 3, "%02X", src[i]); dst += 2; } return (int)hexLen; } int ocpp_cert_print_issuerNameHash(const ocpp_cert_hash *src, char *buf, size_t size) { return ocpp_cert_bytes_to_hex(buf, size, src->issuerNameHash, HashAlgorithmSize(src->hashAlgorithm)); } int ocpp_cert_print_issuerKeyHash(const ocpp_cert_hash *src, char *buf, size_t size) { return ocpp_cert_bytes_to_hex(buf, size, src->issuerKeyHash, HashAlgorithmSize(src->hashAlgorithm)); } int ocpp_cert_print_serialNumber(const ocpp_cert_hash *src, char *buf, size_t size) { if (!buf || !size) { return -1; } buf[0] = '\0'; if (!src->serialNumberLen) { return 0; } int hexLen = snprintf(buf, size, "%X", src->serialNumber[0]); if (hexLen < 0 || (size_t)hexLen >= size) { return -1; } if (src->serialNumberLen > 1) { auto ret = ocpp_cert_bytes_to_hex(buf + (size_t)hexLen, size - (size_t)hexLen, src->serialNumber + 1, src->serialNumberLen - 1); if (ret < 0) { return -1; } hexLen += ret; } return hexLen; } int ocpp_cert_hex_to_bytes(unsigned char *dst, size_t dst_size, const char *hex_src) { if (!dst || !dst_size || !hex_src) { return -1; } dst[0] = '\0'; size_t hex_len = strlen(hex_src); size_t write_len = (hex_len + 1) / 2; if (dst_size < write_len) { return -1; } for (size_t i = 0; i < write_len; i++) { char octet [2]; if (i == 0 && hex_len % 2) { octet[0] = '0'; octet[1] = hex_src[2*i]; } else { octet[0] = hex_src[2*i]; octet[1] = hex_src[2*i + 1]; } unsigned char val = 0; for (size_t j = 0; j < 2; j++) { char c = octet[j]; if (c >= '0' && c <= '9') { val += c - '0'; } else if (c >= 'A' && c <= 'F') { val += (c - 'A') + 0xA; } else if (c >= 'a' && c <= 'f') { val += (c - 'a') + 0xA; } else { return -1; } if (j == 0) { val *= 0x10; } } dst[i] = val; } return (int)write_len; } int ocpp_cert_set_issuerNameHash(ocpp_cert_hash *dst, const char *hex_src, HashAlgorithmType hash_algorithm) { auto ret = ocpp_cert_hex_to_bytes(dst->issuerNameHash, sizeof(dst->issuerNameHash), hex_src); if (ret < 0) { return ret; } if (ret != HashAlgorithmSize(hash_algorithm)) { return -1; } return ret; } int ocpp_cert_set_issuerKeyHash(ocpp_cert_hash *dst, const char *hex_src, HashAlgorithmType hash_algorithm) { auto ret = ocpp_cert_hex_to_bytes(dst->issuerKeyHash, sizeof(dst->issuerNameHash), hex_src); if (ret < 0) { return ret; } if (ret != HashAlgorithmSize(hash_algorithm)) { return -1; } return ret; } int ocpp_cert_set_serialNumber(ocpp_cert_hash *dst, const char *hex_src) { auto ret = ocpp_cert_hex_to_bytes(dst->serialNumber, sizeof(dst->serialNumber), hex_src); if (ret < 0) { return ret; } dst->serialNumberLen = (size_t)ret; return ret; } #endif //MO_ENABLE_CERT_MGMT ================================================ FILE: src/MicroOcpp/Model/Certificates/Certificate.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CERTIFICATE_H #define MO_CERTIFICATE_H #include #if MO_ENABLE_CERT_MGMT #include #ifdef __cplusplus extern "C" { #endif #define MO_MAX_CERT_SIZE 5500 //limit of field `certificate` in InstallCertificateRequest, not counting terminating '\0'. See OCPP 2.0.1 part 2 Data Type 1.30.1 /* * See OCPP 2.0.1 part 2 Data Type 3.36 */ typedef enum GetCertificateIdType { GetCertificateIdType_V2GRootCertificate, GetCertificateIdType_MORootCertificate, GetCertificateIdType_CSMSRootCertificate, GetCertificateIdType_V2GCertificateChain, GetCertificateIdType_ManufacturerRootCertificate } GetCertificateIdType; /* * See OCPP 2.0.1 part 2 Data Type 3.40 */ typedef enum GetInstalledCertificateStatus { GetInstalledCertificateStatus_Accepted, GetInstalledCertificateStatus_NotFound } GetInstalledCertificateStatus; /* * See OCPP 2.0.1 part 2 Data Type 3.45 */ typedef enum InstallCertificateType { InstallCertificateType_V2GRootCertificate, InstallCertificateType_MORootCertificate, InstallCertificateType_CSMSRootCertificate, InstallCertificateType_ManufacturerRootCertificate } InstallCertificateType; /* * See OCPP 2.0.1 part 2 Data Type 3.28 */ typedef enum InstallCertificateStatus { InstallCertificateStatus_Accepted, InstallCertificateStatus_Rejected, InstallCertificateStatus_Failed } InstallCertificateStatus; /* * See OCPP 2.0.1 part 2 Data Type 3.28 */ typedef enum DeleteCertificateStatus { DeleteCertificateStatus_Accepted, DeleteCertificateStatus_Failed, DeleteCertificateStatus_NotFound } DeleteCertificateStatus; /* * See OCPP 2.0.1 part 2 Data Type 3.42 */ typedef enum HashAlgorithmType { HashAlgorithmType_SHA256, HashAlgorithmType_SHA384, HashAlgorithmType_SHA512 } HashAlgorithmType; // Convert HashAlgorithmType into string #define HashAlgorithmLabel(alg) (alg == HashAlgorithmType_SHA256 ? "SHA256" : \ alg == HashAlgorithmType_SHA384 ? "SHA384" : \ alg == HashAlgorithmType_SHA512 ? "SHA512" : "_Undefined") // Convert HashAlgorithmType into hash size in bytes (e.g. SHA256 -> 32) #define HashAlgorithmSize(alg) (alg == HashAlgorithmType_SHA256 ? 32 : \ alg == HashAlgorithmType_SHA384 ? 48 : \ alg == HashAlgorithmType_SHA512 ? 64 : 0) typedef struct ocpp_cert_hash { enum HashAlgorithmType hashAlgorithm; unsigned char issuerNameHash [64]; // hash buf can hold 64 bytes (SHA512). Actual hash size is determined by hash algorithm unsigned char issuerKeyHash [64]; unsigned char serialNumber [20]; size_t serialNumberLen; // length of serial number in bytes } ocpp_cert_hash; bool ocpp_cert_equals(const ocpp_cert_hash *h1, const ocpp_cert_hash *h2); // Max size of hex-encoded cert hash components #define MO_CERT_HASH_ISSUER_NAME_KEY_SIZE (128 + 1) // hex-encoding needs two characters per byte + terminating null-byte #define MO_CERT_HASH_SERIAL_NUMBER_SIZE (40 + 1) /* * Print the issuerNameHash of ocpp_cert_hash as hex-encoded string (e.g. "0123AB") into buf. Bufsize MO_CERT_HASH_ISSUER_NAME_KEY_SIZE is always enough * * Returns the length not counting the terminating 0 on success, -1 on failure */ int ocpp_cert_print_issuerNameHash(const ocpp_cert_hash *src, char *buf, size_t size); /* * Print the issuerKeyHash of ocpp_cert_hash as hex-encoded string (e.g. "0123AB") into buf. Bufsize MO_CERT_HASH_ISSUER_NAME_KEY_SIZE is always enough * * Returns the length not counting the terminating 0 on success, -1 on failure */ int ocpp_cert_print_issuerKeyHash(const ocpp_cert_hash *src, char *buf, size_t size); /* * Print the serialNumber of ocpp_cert_hash as hex-encoded string without leading 0s (e.g. "123AB") into buf. Bufsize MO_CERT_HASH_SERIAL_NUMBER_SIZE is always enough * * Returns the length not counting the terminating 0 on success, -1 on failure */ int ocpp_cert_print_serialNumber(const ocpp_cert_hash *src, char *buf, size_t size); int ocpp_cert_set_issuerNameHash(ocpp_cert_hash *dst, const char *hex_src, HashAlgorithmType hash_algorithm); int ocpp_cert_set_issuerKeyHash(ocpp_cert_hash *dst, const char *hex_src, HashAlgorithmType hash_algorithm); int ocpp_cert_set_serialNumber(ocpp_cert_hash *dst, const char *hex_src); #ifdef __cplusplus } //extern "C" #include namespace MicroOcpp { using CertificateHash = ocpp_cert_hash; /* * See OCPP 2.0.1 part 2 Data Type 2.5 */ struct CertificateChainHash : public MemoryManaged { GetCertificateIdType certificateType; CertificateHash certificateHashData; Vector childCertificateHashData; CertificateChainHash() : MemoryManaged("v2.0.1.Certificates.CertificateChainHash"), childCertificateHashData(makeVector(getMemoryTag())) { } }; /* * Interface which allows MicroOcpp to interact with the certificates managed by the local TLS library */ class CertificateStore { public: virtual ~CertificateStore() = default; virtual GetInstalledCertificateStatus getCertificateIds(const Vector& certificateType, Vector& out) = 0; virtual DeleteCertificateStatus deleteCertificate(const CertificateHash& hash) = 0; virtual InstallCertificateStatus installCertificate(InstallCertificateType certificateType, const char *certificate) = 0; }; } //namespace MicroOcpp #endif //__cplusplus #endif //MO_ENABLE_CERT_MGMT #endif ================================================ FILE: src/MicroOcpp/Model/Certificates/CertificateMbedTLS.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS #include #include #include #include #include #include bool ocpp_get_cert_hash(mbedtls_x509_crt& cacert, HashAlgorithmType hashAlg, ocpp_cert_hash *out) { if (cacert.next) { MO_DBG_ERR("only sole root certs supported"); return false; } out->hashAlgorithm = hashAlg; mbedtls_md_type_t hash_alg_mbed; switch (hashAlg) { case HashAlgorithmType_SHA256: hash_alg_mbed = MBEDTLS_MD_SHA256; break; case HashAlgorithmType_SHA384: hash_alg_mbed = MBEDTLS_MD_SHA384; break; case HashAlgorithmType_SHA512: hash_alg_mbed = MBEDTLS_MD_SHA512; break; default: MO_DBG_ERR("internal error"); return false; } const mbedtls_md_info_t *md_info; md_info = mbedtls_md_info_from_type(hash_alg_mbed); if (!md_info) { MO_DBG_ERR("hash algorithmus not supported"); return false; } size_t hash_size = mbedtls_md_get_size(md_info); if (hash_size > sizeof(out->issuerNameHash)) { MO_DBG_ERR("internal error"); return false; } if (!cacert.issuer_raw.p) { MO_DBG_ERR("missing issuer name"); return false; } int ret; if ((ret = mbedtls_md(md_info, cacert.issuer_raw.p, cacert.issuer_raw.len, out->issuerNameHash))) { MO_DBG_ERR("mbedtls_md: %i", ret); return false; } // copy public key into pk_buf to create issuerKeyHash size_t pk_size = cacert.pk_raw.len; unsigned char *pk_buf = static_cast(MO_MALLOC("v201.Certificates.CertificateStoreMbedTLS", pk_size)); if (!pk_buf) { MO_DBG_ERR("OOM (alloc size %zu)", pk_size); return false; } int pk_len = 0; unsigned char *pk_p = pk_buf + pk_size; bool pk_err = false; if ((pk_len = mbedtls_pk_write_pubkey(&pk_p, pk_buf, &cacert.pk)) <= 0) { pk_err = true; char err [100]; mbedtls_strerror(ret, err, 100); MO_DBG_ERR("mbedtls_pk_write_pubkey_pem: %i -- %s", pk_len, err); // return after pk_buf has been freed } if (!pk_err) { if ((ret = mbedtls_md(md_info, pk_p, pk_len, out->issuerKeyHash))) { pk_err = true; MO_DBG_ERR("mbedtls_md: %i", ret); } } MO_FREE(pk_buf); if (pk_err) { return false; } size_t serial_begin = 0; //trunicate leftmost 0x00 bytes for (; serial_begin < cacert.serial.len - 1; serial_begin++) { //keep at least 1 byte, even if 0x00 if (cacert.serial.p[serial_begin] != 0) { break; } } out->serialNumberLen = std::min(cacert.serial.len - serial_begin, sizeof(out->serialNumber)); memcpy(out->serialNumber, cacert.serial.p + serial_begin, out->serialNumberLen); return true; } bool ocpp_get_cert_hash(const unsigned char *buf, size_t len, HashAlgorithmType hashAlg, ocpp_cert_hash *out) { mbedtls_x509_crt cacert; mbedtls_x509_crt_init(&cacert); bool success = false; int ret; if((ret = mbedtls_x509_crt_parse(&cacert, buf, len + 1)) >= 0) { success = ocpp_get_cert_hash(cacert, hashAlg, out); } else { char err [100]; mbedtls_strerror(ret, err, 100); MO_DBG_ERR("mbedtls_x509_crt_parse: %i -- %s", ret, err); } mbedtls_x509_crt_free(&cacert); return success; } namespace MicroOcpp { class CertificateStoreMbedTLS : public CertificateStore, public MemoryManaged { private: std::shared_ptr filesystem; bool getCertHash(const char *fn, HashAlgorithmType hashAlg, CertificateHash& out) { size_t fsize; if (filesystem->stat(fn, &fsize) != 0) { MO_DBG_ERR("certificate does not exist: %s", fn); return false; } if (fsize >= MO_MAX_CERT_SIZE) { MO_DBG_ERR("cert file exceeds limit: %s, %zuB", fn, fsize); return false; } auto file = filesystem->open(fn, "r"); if (!file) { MO_DBG_ERR("could not open file: %s", fn); return false; } unsigned char *buf = static_cast(MO_MALLOC(getMemoryTag(), fsize + 1)); if (!buf) { MO_DBG_ERR("OOM"); return false; } bool success = true; size_t ret; if ((ret = file->read((char*) buf, fsize)) != fsize) { MO_DBG_ERR("read error: %zu (expect %zu)", ret, fsize); success = false; } buf[fsize] = '\0'; if (success) { success &= ocpp_get_cert_hash(buf, fsize, hashAlg, &out); } if (!success) { MO_DBG_ERR("could not read cert: %s", fn); } MO_FREE(buf); return success; } public: CertificateStoreMbedTLS(std::shared_ptr filesystem) : MemoryManaged("v201.Certificates.CertificateStoreMbedTLS"), filesystem(filesystem) { } GetInstalledCertificateStatus getCertificateIds(const Vector& certificateType, Vector& out) override { out.clear(); for (auto certType : certificateType) { const char *certTypeFnStr = nullptr; switch (certType) { case GetCertificateIdType_CSMSRootCertificate: certTypeFnStr = MO_CERT_FN_CSMS_ROOT; break; case GetCertificateIdType_ManufacturerRootCertificate: certTypeFnStr = MO_CERT_FN_MANUFACTURER_ROOT; break; default: MO_DBG_ERR("only CSMS / Manufacturer root supported"); break; } if (!certTypeFnStr) { continue; } for (size_t i = 0; i < MO_CERT_STORE_SIZE; i++) { char fn [MO_MAX_PATH_SIZE]; if (!printCertFn(certTypeFnStr, i, fn, MO_MAX_PATH_SIZE)) { MO_DBG_ERR("internal error"); out.clear(); break; } size_t msize; if (filesystem->stat(fn, &msize) != 0) { continue; //no cert installed at this slot } out.emplace_back(); CertificateChainHash& rootCert = out.back(); rootCert.certificateType = certType; if (!getCertHash(fn, HashAlgorithmType_SHA256, rootCert.certificateHashData)) { MO_DBG_ERR("could not create hash: %s", fn); out.pop_back(); continue; } } } return out.empty() ? GetInstalledCertificateStatus_NotFound : GetInstalledCertificateStatus_Accepted; } DeleteCertificateStatus deleteCertificate(const CertificateHash& hash) override { bool err = false; //enumerate all certs possibly installed by this CertStore implementation for (const char *certTypeFnStr : {MO_CERT_FN_CSMS_ROOT, MO_CERT_FN_MANUFACTURER_ROOT}) { for (size_t i = 0; i < MO_CERT_STORE_SIZE; i++) { char fn [MO_MAX_PATH_SIZE] = {'\0'}; //cert fn on flash storage if (!printCertFn(certTypeFnStr, i, fn, MO_MAX_PATH_SIZE)) { MO_DBG_ERR("internal error"); return DeleteCertificateStatus_Failed; } size_t msize; if (filesystem->stat(fn, &msize) != 0) { continue; //no cert installed at this slot } CertificateHash probe; if (!getCertHash(fn, hash.hashAlgorithm, probe)) { MO_DBG_ERR("could not create hash: %s", fn); err = true; continue; } if (ocpp_cert_equals(&probe, &hash)) { //found, delete bool success = filesystem->remove(fn); return success ? DeleteCertificateStatus_Accepted : DeleteCertificateStatus_Failed; } } } return err ? DeleteCertificateStatus_Failed : DeleteCertificateStatus_NotFound; } InstallCertificateStatus installCertificate(InstallCertificateType certificateType, const char *certificate) override { const char *certTypeFnStr; GetCertificateIdType certTypeGetType; switch (certificateType) { case InstallCertificateType_CSMSRootCertificate: certTypeFnStr = MO_CERT_FN_CSMS_ROOT; certTypeGetType = GetCertificateIdType_CSMSRootCertificate; break; case InstallCertificateType_ManufacturerRootCertificate: certTypeFnStr = MO_CERT_FN_MANUFACTURER_ROOT; certTypeGetType = GetCertificateIdType_ManufacturerRootCertificate; break; default: MO_DBG_ERR("only CSMS / Manufacturer root supported"); return InstallCertificateStatus_Failed; } //check if this implementation is able to parse incoming cert CertificateHash certId; if (!ocpp_get_cert_hash((const unsigned char*)certificate, strlen(certificate), HashAlgorithmType_SHA256, &certId)) { MO_DBG_ERR("unable to parse cert"); return InstallCertificateStatus_Rejected; } #if MO_DBG_LEVEL >= MO_DL_DEBUG { MO_DBG_DEBUG("Cert ID:"); MO_DBG_DEBUG("hashAlgorithm: %s", HashAlgorithmLabel(certId.hashAlgorithm)); char buf [MO_CERT_HASH_ISSUER_NAME_KEY_SIZE]; ocpp_cert_print_issuerNameHash(&certId, buf, sizeof(buf)); MO_DBG_DEBUG("issuerNameHash: %s", buf); ocpp_cert_print_issuerKeyHash(&certId, buf, sizeof(buf)); MO_DBG_DEBUG("issuerKeyHash: %s", buf); ocpp_cert_print_serialNumber(&certId, buf, sizeof(buf)); MO_DBG_DEBUG("serialNumber: %s", buf); } #endif // MO_DBG_LEVEL >= MO_DL_DEBUG //check if cert is already stored on flash auto installedCerts = makeVector(getMemoryTag()); auto ret = getCertificateIds({certTypeGetType}, installedCerts); if (ret == GetInstalledCertificateStatus_Accepted) { for (auto &installedCert : installedCerts) { if (ocpp_cert_equals(&installedCert.certificateHashData, &certId)) { MO_DBG_INFO("certificate already installed"); return InstallCertificateStatus_Accepted; } for (auto& installedChild : installedCert.childCertificateHashData) { if (ocpp_cert_equals(&installedChild, &certId)) { MO_DBG_INFO("certificate already installed"); return InstallCertificateStatus_Accepted; } } } } char fn [MO_MAX_PATH_SIZE] = {'\0'}; //cert fn on flash storage //check for free cert slot for (size_t i = 0; i < MO_CERT_STORE_SIZE; i++) { if (!printCertFn(certTypeFnStr, i, fn, MO_MAX_PATH_SIZE)) { MO_DBG_ERR("invalid cert fn"); return InstallCertificateStatus_Failed; } size_t msize; if (filesystem->stat(fn, &msize) != 0) { //found free slot; fn contains result break; } else { //this slot is already occupied; invalidate fn and try next fn[0] = '\0'; } } if (fn[0] == '\0') { MO_DBG_ERR("exceed maximum number of certs; must delete before"); return InstallCertificateStatus_Rejected; } auto file = filesystem->open(fn, "w"); if (!file) { MO_DBG_ERR("could not open file"); return InstallCertificateStatus_Failed; } size_t cert_len = strlen(certificate); auto written = file->write(certificate, cert_len); if (written < cert_len) { MO_DBG_ERR("file write error"); file.reset(); filesystem->remove(fn); return InstallCertificateStatus_Failed; } MO_DBG_INFO("installed certificate: %s", fn); return InstallCertificateStatus_Accepted; } }; std::unique_ptr makeCertificateStoreMbedTLS(std::shared_ptr filesystem) { if (!filesystem) { MO_DBG_WARN("default Certificate Store requires FS"); return nullptr; } return std::unique_ptr(new CertificateStoreMbedTLS(filesystem)); } bool printCertFn(const char *certType, size_t index, char *buf, size_t bufsize) { if (!certType || !*certType || index >= MO_CERT_STORE_SIZE || !buf) { MO_DBG_ERR("invalid args"); return false; } auto ret = snprintf(buf, bufsize, MO_FILENAME_PREFIX MO_CERT_FN_PREFIX "%s" "-%zu" MO_CERT_FN_SUFFIX, certType, index); if (ret < 0 || ret >= (int)bufsize) { MO_DBG_ERR("fn error: %i", ret); return false; } return true; } } //namespace MicroOcpp #endif //MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS ================================================ FILE: src/MicroOcpp/Model/Certificates/CertificateMbedTLS.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CERTIFICATE_MBEDTLS_H #define MO_CERTIFICATE_MBEDTLS_H /* * Built-in implementation of the Certificate interface for MbedTLS */ #include #include #ifndef MO_ENABLE_CERT_STORE_MBEDTLS #define MO_ENABLE_CERT_STORE_MBEDTLS MO_ENABLE_MBEDTLS #endif #if MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS /* * Provide certificate interpreter to facilitate cert store in C. A full implementation is only available for C++ */ #include #ifdef __cplusplus extern "C" { #endif bool ocpp_get_cert_hash(const unsigned char *cert, size_t len, enum HashAlgorithmType hashAlg, ocpp_cert_hash *out); #ifdef __cplusplus } //extern "C" #include #include #ifndef MO_CERT_FN_PREFIX #define MO_CERT_FN_PREFIX "cert-" #endif #ifndef MO_CERT_FN_SUFFIX #define MO_CERT_FN_SUFFIX ".pem" #endif #ifndef MO_CERT_FN_CSMS_ROOT #define MO_CERT_FN_CSMS_ROOT "csms" #endif #ifndef MO_CERT_FN_MANUFACTURER_ROOT #define MO_CERT_FN_MANUFACTURER_ROOT "mfact" #endif #ifndef MO_CERT_STORE_SIZE #define MO_CERT_STORE_SIZE 3 //max number of certs per certificate type (e.g. CSMS root CA, Manufacturer root CA) #endif namespace MicroOcpp { std::unique_ptr makeCertificateStoreMbedTLS(std::shared_ptr filesystem); bool printCertFn(const char *certType, size_t index, char *buf, size_t bufsize); } //namespace MicroOcpp #endif //def __cplusplus #endif //MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS #endif ================================================ FILE: src/MicroOcpp/Model/Certificates/CertificateService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_CERT_MGMT #include #include #include #include using namespace MicroOcpp; CertificateService::CertificateService(Context& context) : MemoryManaged("v201.Certificates.CertificateService"), context(context) { context.getOperationRegistry().registerOperation("DeleteCertificate", [this] () { return new Ocpp201::DeleteCertificate(*this);}); context.getOperationRegistry().registerOperation("GetInstalledCertificateIds", [this] () { return new Ocpp201::GetInstalledCertificateIds(*this);}); context.getOperationRegistry().registerOperation("InstallCertificate", [this] () { return new Ocpp201::InstallCertificate(*this);}); } void CertificateService::setCertificateStore(std::unique_ptr certStore) { this->certStore = std::move(certStore); } CertificateStore *CertificateService::getCertificateStore() { return certStore.get(); } #endif //MO_ENABLE_CERT_MGMT ================================================ FILE: src/MicroOcpp/Model/Certificates/CertificateService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License /* * Functional Block M: ISO 15118 Certificate Management * * Implementation of UC: * - M03 * - M04 * - M05 */ #ifndef MO_CERTIFICATESERVICE_H #define MO_CERTIFICATESERVICE_H #include #if MO_ENABLE_CERT_MGMT #include #include #include #include namespace MicroOcpp { class Context; class CertificateService : public MemoryManaged { private: Context& context; std::unique_ptr certStore; public: CertificateService(Context& context); void setCertificateStore(std::unique_ptr certStore); CertificateStore *getCertificateStore(); }; } #endif //MO_ENABLE_CERT_MGMT #endif ================================================ FILE: src/MicroOcpp/Model/Certificates/Certificate_c.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_CERT_MGMT #include #include #include namespace MicroOcpp { /* * C++ wrapper for the C-style certificate interface */ class CertificateStoreC : public CertificateStore, public MemoryManaged { private: ocpp_cert_store *certstore = nullptr; public: CertificateStoreC(ocpp_cert_store *certstore) : MemoryManaged("v201.Certificates.CertificateStoreC"), certstore(certstore) { } ~CertificateStoreC() = default; GetInstalledCertificateStatus getCertificateIds(const Vector& certificateType, Vector& out) override { out.clear(); ocpp_cert_chain_hash *cch; auto ret = certstore->getCertificateIds(certstore->user_data, &certificateType[0], certificateType.size(), &cch); if (ret == GetInstalledCertificateStatus_NotFound || !cch) { return GetInstalledCertificateStatus_NotFound; } bool err = false; for (ocpp_cert_chain_hash *it = cch; it && !err; it = it->next) { out.emplace_back(); auto &chd_el = out.back(); chd_el.certificateType = it->certType; memcpy(&chd_el.certificateHashData, &it->certHashData, sizeof(ocpp_cert_hash)); } while (cch) { ocpp_cert_chain_hash *el = cch; cch = cch->next; el->invalidate(el); } if (err) { out.clear(); } return out.empty() ? GetInstalledCertificateStatus_NotFound : GetInstalledCertificateStatus_Accepted; } DeleteCertificateStatus deleteCertificate(const CertificateHash& hash) override { return certstore->deleteCertificate(certstore->user_data, &hash); } InstallCertificateStatus installCertificate(InstallCertificateType certificateType, const char *certificate) override { return certstore->installCertificate(certstore->user_data, certificateType, certificate); } }; std::unique_ptr makeCertificateStoreCwrapper(ocpp_cert_store *certstore) { return std::unique_ptr(new CertificateStoreC(certstore)); } } //namespace MicroOcpp #endif //MO_ENABLE_CERT_MGMT ================================================ FILE: src/MicroOcpp/Model/Certificates/Certificate_c.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CERTIFICATE_C_H #define MO_CERTIFICATE_C_H #include #if MO_ENABLE_CERT_MGMT #include #include #ifdef __cplusplus extern "C" { #endif typedef struct ocpp_cert_chain_hash { void *user_data; //set this at your choice. MO passes it back to the functions below enum GetCertificateIdType certType; ocpp_cert_hash certHashData; //ocpp_cert_hash *childCertificateHashData; struct ocpp_cert_chain_hash *next; //link to next list element if result of getCertificateIds void (*invalidate)(void *user_data); //free resources here. Guaranteed to be called } ocpp_cert_chain_hash; typedef struct ocpp_cert_store { void *user_data; //set this at your choice. MO passes it back to the functions below enum GetInstalledCertificateStatus (*getCertificateIds)(void *user_data, const enum GetCertificateIdType certType [], size_t certTypeLen, ocpp_cert_chain_hash **out); enum DeleteCertificateStatus (*deleteCertificate)(void *user_data, const ocpp_cert_hash *hash); enum InstallCertificateStatus (*installCertificate)(void *user_data, enum InstallCertificateType certType, const char *cert); } ocpp_cert_store; #ifdef __cplusplus } //extern "C" #include namespace MicroOcpp { std::unique_ptr makeCertificateStoreCwrapper(ocpp_cert_store *certstore); } //namespace MicroOcpp #endif //__cplusplus #endif //MO_ENABLE_CERT_MGMT #endif ================================================ FILE: src/MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CHARGEPOINTERRORCODE_H #define MO_CHARGEPOINTERRORCODE_H #include namespace MicroOcpp { struct ErrorData { bool isError = false; //if any error information is set bool isFaulted = false; //if this is a severe error and the EVSE should go into the faulted state uint8_t severity = 1; //severity: don't send less severe errors during highly severe error condition const char *errorCode = nullptr; //see ChargePointErrorCode (p. 76/77) for possible values const char *info = nullptr; //Additional free format information related to the error const char *vendorId = nullptr; //vendor-specific implementation identifier const char *vendorErrorCode = nullptr; //vendor-specific error code ErrorData() = default; ErrorData(const char *errorCode = nullptr) : errorCode(errorCode) { if (errorCode) { isError = true; isFaulted = true; } } }; } #endif ================================================ FILE: src/MicroOcpp/Model/ConnectorBase/ChargePointStatus.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CHARGEPOINTSTATUS_H #define MO_CHARGEPOINTSTATUS_H #include #ifdef __cplusplus extern "C" { #endif typedef enum ChargePointStatus { ChargePointStatus_UNDEFINED, //internal use only - no OCPP standard value ChargePointStatus_Available, ChargePointStatus_Preparing, ChargePointStatus_Charging, ChargePointStatus_SuspendedEVSE, ChargePointStatus_SuspendedEV, ChargePointStatus_Finishing, ChargePointStatus_Reserved, ChargePointStatus_Unavailable, ChargePointStatus_Faulted #if MO_ENABLE_V201 ,ChargePointStatus_Occupied #endif } ChargePointStatus; #ifdef __cplusplus } #endif #endif ================================================ FILE: src/MicroOcpp/Model/ConnectorBase/Connector.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef MO_TX_CLEAN_ABORTED #define MO_TX_CLEAN_ABORTED 1 #endif using namespace MicroOcpp; Connector::Connector(Context& context, std::shared_ptr filesystem, unsigned int connectorId) : MemoryManaged("v16.ConnectorBase.Connector"), context(context), model(context.getModel()), filesystem(filesystem), connectorId(connectorId), errorDataInputs(makeVector>(getMemoryTag())), trackErrorDataInputs(makeVector(getMemoryTag())) { context.getRequestQueue().addSendQueue(this); //register at RequestQueue as Request emitter snprintf(availabilityBoolKey, sizeof(availabilityBoolKey), MO_CONFIG_EXT_PREFIX "AVAIL_CONN_%d", connectorId); availabilityBool = declareConfiguration(availabilityBoolKey, true, MO_KEYVALUE_FN, false, false, false); #if MO_ENABLE_CONNECTOR_LOCK declareConfiguration("UnlockConnectorOnEVSideDisconnect", true); //read-write #else declareConfiguration("UnlockConnectorOnEVSideDisconnect", false, CONFIGURATION_VOLATILE, true); //read-only because there is no connector lock #endif //MO_ENABLE_CONNECTOR_LOCK connectionTimeOutInt = declareConfiguration("ConnectionTimeOut", 30); registerConfigurationValidator("ConnectionTimeOut", VALIDATE_UNSIGNED_INT); minimumStatusDurationInt = declareConfiguration("MinimumStatusDuration", 0); registerConfigurationValidator("MinimumStatusDuration", VALIDATE_UNSIGNED_INT); stopTransactionOnInvalidIdBool = declareConfiguration("StopTransactionOnInvalidId", true); stopTransactionOnEVSideDisconnectBool = declareConfiguration("StopTransactionOnEVSideDisconnect", true); localPreAuthorizeBool = declareConfiguration("LocalPreAuthorize", false); localAuthorizeOfflineBool = declareConfiguration("LocalAuthorizeOffline", true); allowOfflineTxForUnknownIdBool = MicroOcpp::declareConfiguration("AllowOfflineTxForUnknownId", false); //if the EVSE goes offline, can it continue to charge without sending StartTx / StopTx to the server when going online again? silentOfflineTransactionsBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "SilentOfflineTransactions", false); //how long the EVSE tries the Authorize request before it enters offline mode authorizationTimeoutInt = MicroOcpp::declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", 20); registerConfigurationValidator(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", VALIDATE_UNSIGNED_INT); //FreeVend mode freeVendActiveBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "FreeVendActive", false); freeVendIdTagString = declareConfiguration(MO_CONFIG_EXT_PREFIX "FreeVendIdTag", ""); txStartOnPowerPathClosedBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "TxStartOnPowerPathClosed", false); transactionMessageAttemptsInt = declareConfiguration("TransactionMessageAttempts", 3); registerConfigurationValidator("TransactionMessageAttempts", VALIDATE_UNSIGNED_INT); transactionMessageRetryIntervalInt = declareConfiguration("TransactionMessageRetryInterval", 60); registerConfigurationValidator("TransactionMessageRetryInterval", VALIDATE_UNSIGNED_INT); if (!availabilityBool) { MO_DBG_ERR("Cannot declare availabilityBool"); } char txFnamePrefix [30]; snprintf(txFnamePrefix, sizeof(txFnamePrefix), "tx-%u-", connectorId); size_t txFnamePrefixLen = strlen(txFnamePrefix); unsigned int txNrPivot = std::numeric_limits::max(); if (filesystem) { filesystem->ftw_root([this, txFnamePrefix, txFnamePrefixLen, &txNrPivot] (const char *fname) { if (!strncmp(fname, txFnamePrefix, txFnamePrefixLen)) { unsigned int parsedTxNr = 0; for (size_t i = txFnamePrefixLen; fname[i] >= '0' && fname[i] <= '9'; i++) { parsedTxNr *= 10; parsedTxNr += fname[i] - '0'; } if (txNrPivot == std::numeric_limits::max()) { txNrPivot = parsedTxNr; txNrBegin = parsedTxNr; txNrEnd = (parsedTxNr + 1) % MAX_TX_CNT; return 0; } if ((parsedTxNr + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT < MAX_TX_CNT / 2) { //parsedTxNr is after pivot point if ((parsedTxNr + 1 + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT > (txNrEnd + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT) { txNrEnd = (parsedTxNr + 1) % MAX_TX_CNT; } } else if ((txNrPivot + MAX_TX_CNT - parsedTxNr) % MAX_TX_CNT < MAX_TX_CNT / 2) { //parsedTxNr is before pivot point if ((txNrPivot + MAX_TX_CNT - parsedTxNr) % MAX_TX_CNT > (txNrPivot + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT) { txNrBegin = parsedTxNr; } } MO_DBG_DEBUG("found %s%u.jsn - Internal range from %u to %u (exclusive)", txFnamePrefix, parsedTxNr, txNrBegin, txNrEnd); } return 0; }); } MO_DBG_DEBUG("found %u transactions for connector %u. Internal range from %u to %u (exclusive)", (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT, connectorId, txNrBegin, txNrEnd); txNrFront = txNrBegin; if (model.getTransactionStore()) { unsigned int txNrLatest = (txNrEnd + MAX_TX_CNT - 1) % MAX_TX_CNT; //txNr of the most recent tx on flash transaction = model.getTransactionStore()->getTransaction(connectorId, txNrLatest); //returns nullptr if txNrLatest does not exist on flash } else { MO_DBG_ERR("must initialize TxStore before Connector"); } } Connector::~Connector() { if (availabilityBool->getKey() == availabilityBoolKey) { availabilityBool->setKey(nullptr); } } ChargePointStatus Connector::getStatus() { ChargePointStatus res = ChargePointStatus_UNDEFINED; /* * Handle special case: This is the Connector for the whole CP (i.e. connectorId=0) --> only states Available, Unavailable, Faulted are possible */ if (connectorId == 0) { if (isFaulted()) { res = ChargePointStatus_Faulted; } else if (!isOperative()) { res = ChargePointStatus_Unavailable; } else { res = ChargePointStatus_Available; } return res; } if (isFaulted()) { res = ChargePointStatus_Faulted; } else if (!isOperative()) { res = ChargePointStatus_Unavailable; } else if (transaction && transaction->isRunning()) { //Transaction is currently running if (connectorPluggedInput && !connectorPluggedInput()) { //special case when StopTransactionOnEVSideDisconnect is false res = ChargePointStatus_SuspendedEV; } else if (!ocppPermitsCharge() || (evseReadyInput && !evseReadyInput())) { res = ChargePointStatus_SuspendedEVSE; } else if (evReadyInput && !evReadyInput()) { res = ChargePointStatus_SuspendedEV; } else { res = ChargePointStatus_Charging; } } #if MO_ENABLE_RESERVATION else if (model.getReservationService() && model.getReservationService()->getReservation(connectorId)) { res = ChargePointStatus_Reserved; } #endif else if ((!transaction) && //no transaction process occupying the connector (!connectorPluggedInput || !connectorPluggedInput()) && //no vehicle plugged (!occupiedInput || !occupiedInput())) { //occupied override clear res = ChargePointStatus_Available; } else { /* * Either in Preparing or Finishing state. Only way to know is from previous state */ const auto previous = currentStatus; if (previous == ChargePointStatus_Finishing || previous == ChargePointStatus_Charging || previous == ChargePointStatus_SuspendedEV || previous == ChargePointStatus_SuspendedEVSE || (transaction && transaction->getStartSync().isRequested())) { //transaction process still occupying the connector res = ChargePointStatus_Finishing; } else { res = ChargePointStatus_Preparing; } } #if MO_ENABLE_V201 if (model.getVersion().major == 2) { //OCPP 2.0.1: map v1.6 status onto v2.0.1 if (res == ChargePointStatus_Preparing || res == ChargePointStatus_Charging || res == ChargePointStatus_SuspendedEV || res == ChargePointStatus_SuspendedEVSE || res == ChargePointStatus_Finishing) { res = ChargePointStatus_Occupied; } } #endif if (res == ChargePointStatus_UNDEFINED) { MO_DBG_DEBUG("status undefined"); return ChargePointStatus_Faulted; //internal error } return res; } bool Connector::ocppPermitsCharge() { if (connectorId == 0) { MO_DBG_WARN("not supported for connectorId == 0"); return false; } bool suspendDeAuthorizedIdTag = transaction && transaction->isIdTagDeauthorized(); //if idTag status is "DeAuthorized" and if charging should stop //check special case for DeAuthorized idTags: FreeVend mode if (suspendDeAuthorizedIdTag && freeVendActiveBool && freeVendActiveBool->getBool()) { suspendDeAuthorizedIdTag = false; } // check charge permission depending on TxStartPoint if (txStartOnPowerPathClosedBool && txStartOnPowerPathClosedBool->getBool()) { // tx starts when the power path is closed. Advertise charging before transaction return transaction && transaction->isActive() && transaction->isAuthorized() && !suspendDeAuthorizedIdTag; } else { // tx must be started before the power path can be closed return transaction && transaction->isRunning() && transaction->isActive() && !suspendDeAuthorizedIdTag; } } void Connector::loop() { if (!trackLoopExecute) { trackLoopExecute = true; if (connectorPluggedInput) { freeVendTrackPlugged = connectorPluggedInput(); } } if (transaction && ((transaction->isAborted() && MO_TX_CLEAN_ABORTED) || (transaction->isSilent() && transaction->getStopSync().isRequested()))) { //If the transaction is aborted (invalidated before started) or is silent and has stopped. Delete all artifacts from flash //This is an optimization. The memory management will attempt to remove those files again later bool removed = true; if (auto mService = model.getMeteringService()) { mService->abortTxMeterData(connectorId); removed &= mService->removeTxMeterData(connectorId, transaction->getTxNr()); } if (removed) { removed &= model.getTransactionStore()->remove(connectorId, transaction->getTxNr()); } if (removed) { if (txNrFront == txNrEnd) { txNrFront = transaction->getTxNr(); } txNrEnd = transaction->getTxNr(); //roll back creation of last tx entry } MO_DBG_DEBUG("collect aborted or silent transaction %u-%u %s", connectorId, transaction->getTxNr(), removed ? "" : "failure"); MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); transaction = nullptr; } if (transaction && transaction->isAborted()) { MO_DBG_DEBUG("collect aborted transaction %u-%u", connectorId, transaction->getTxNr()); MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); transaction = nullptr; } if (transaction && transaction->getStopSync().isRequested()) { MO_DBG_DEBUG("collect obsolete transaction %u-%u", connectorId, transaction->getTxNr()); MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); transaction = nullptr; } if (transaction) { //begin exclusively transaction-related operations if (connectorPluggedInput) { if (transaction->isRunning() && transaction->isActive() && !connectorPluggedInput()) { if (!stopTransactionOnEVSideDisconnectBool || stopTransactionOnEVSideDisconnectBool->getBool()) { MO_DBG_DEBUG("Stop Tx due to EV disconnect"); transaction->setStopReason("EVDisconnected"); transaction->setInactive(); transaction->commit(); } } if (transaction->isActive() && !transaction->getStartSync().isRequested() && transaction->getBeginTimestamp() > MIN_TIME && connectionTimeOutInt && connectionTimeOutInt->getInt() > 0 && !connectorPluggedInput() && model.getClock().now() - transaction->getBeginTimestamp() > connectionTimeOutInt->getInt()) { MO_DBG_INFO("Session mngt: timeout"); transaction->setInactive(); transaction->commit(); updateTxNotification(TxNotification_ConnectionTimeout); } } if (transaction->isActive() && transaction->isIdTagDeauthorized() && ( //transaction has been deAuthorized !transaction->isRunning() || //if transaction hasn't started yet, always end !stopTransactionOnInvalidIdBool || stopTransactionOnInvalidIdBool->getBool())) { //if transaction is running, behavior depends on StopTransactionOnInvalidId MO_DBG_DEBUG("DeAuthorize session"); transaction->setStopReason("DeAuthorized"); transaction->setInactive(); transaction->commit(); } /* * Check conditions for start or stop transaction */ if (!transaction->isRunning()) { //start tx? if (transaction->isActive() && transaction->isAuthorized() && //tx must be authorized (!connectorPluggedInput || connectorPluggedInput()) && //if applicable, connector must be plugged isOperative() && //only start tx if charger is free of error conditions (!txStartOnPowerPathClosedBool || !txStartOnPowerPathClosedBool->getBool() || !evReadyInput || evReadyInput()) && //if applicable, postpone tx start point to PowerPathClosed (!startTxReadyInput || startTxReadyInput())) { //if defined, user Input for allowing StartTx must be true //start Transaction MO_DBG_INFO("Session mngt: trigger StartTransaction"); auto meteringService = model.getMeteringService(); if (transaction->getMeterStart() < 0 && meteringService) { auto meterStart = meteringService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext_TransactionBegin); if (meterStart && *meterStart) { transaction->setMeterStart(meterStart->toInteger()); } else { MO_DBG_ERR("MeterStart undefined"); } } if (transaction->getStartTimestamp() <= MIN_TIME) { transaction->setStartTimestamp(model.getClock().now()); transaction->setStartBootNr(model.getBootNr()); } transaction->getStartSync().setRequested(); transaction->getStartSync().setOpNr(context.getRequestQueue().getNextOpNr()); if (transaction->isSilent()) { MO_DBG_INFO("silent Transaction: omit StartTx"); transaction->getStartSync().confirm(); } else { //normal transaction, record txMeterData if (model.getMeteringService()) { model.getMeteringService()->beginTxMeterData(transaction.get()); } } transaction->commit(); updateTxNotification(TxNotification_StartTx); //fetchFrontRequest will create the StartTransaction and pass it to the message sender return; } } else { //stop tx? if (!transaction->isActive() && (!stopTxReadyInput || stopTxReadyInput())) { //stop transaction MO_DBG_INFO("Session mngt: trigger StopTransaction"); auto meteringService = model.getMeteringService(); if (transaction->getMeterStop() < 0 && meteringService) { auto meterStop = meteringService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext_TransactionEnd); if (meterStop && *meterStop) { transaction->setMeterStop(meterStop->toInteger()); } else { MO_DBG_ERR("MeterStop undefined"); } } if (transaction->getStopTimestamp() <= MIN_TIME) { transaction->setStopTimestamp(model.getClock().now()); transaction->setStopBootNr(model.getBootNr()); } transaction->getStopSync().setRequested(); transaction->getStopSync().setOpNr(context.getRequestQueue().getNextOpNr()); if (transaction->isSilent()) { MO_DBG_INFO("silent Transaction: omit StopTx"); transaction->getStopSync().confirm(); } else { //normal transaction, record txMeterData if (model.getMeteringService()) { model.getMeteringService()->endTxMeterData(transaction.get()); } } transaction->commit(); updateTxNotification(TxNotification_StopTx); //fetchFrontRequest will create the StopTransaction and pass it to the message sender return; } } } //end transaction-related operations //handle FreeVend mode if (freeVendActiveBool && freeVendActiveBool->getBool() && connectorPluggedInput) { if (!freeVendTrackPlugged && connectorPluggedInput() && !transaction) { const char *idTag = freeVendIdTagString ? freeVendIdTagString->getString() : ""; if (!idTag || *idTag == '\0') { idTag = "A0000000"; } MO_DBG_INFO("begin FreeVend Tx using idTag %s", idTag); beginTransaction_authorized(idTag); if (!transaction) { MO_DBG_ERR("could not begin FreeVend Tx"); } } freeVendTrackPlugged = connectorPluggedInput(); } ErrorData errorData {nullptr}; errorData.severity = 0; int errorDataIndex = -1; if (model.getVersion().major == 1 && model.getClock().now() >= MIN_TIME) { //OCPP 1.6: use StatusNotification to send error codes if (reportedErrorIndex >= 0) { auto error = errorDataInputs[reportedErrorIndex].operator()(); if (error.isError) { errorData = error; errorDataIndex = reportedErrorIndex; } } for (auto i = std::min(errorDataInputs.size(), trackErrorDataInputs.size()); i >= 1; i--) { auto index = i - 1; ErrorData error {nullptr}; if ((int)index != errorDataIndex) { error = errorDataInputs[index].operator()(); } else { error = errorData; } if (error.isError && !trackErrorDataInputs[index] && error.severity >= errorData.severity) { //new error errorData = error; errorDataIndex = index; } else if (error.isError && error.severity > errorData.severity) { errorData = error; errorDataIndex = index; } else if (!error.isError && trackErrorDataInputs[index]) { //reset error trackErrorDataInputs[index] = false; } } if (errorDataIndex != reportedErrorIndex) { if (errorDataIndex >= 0 || MO_REPORT_NOERROR) { reportedStatus = ChargePointStatus_UNDEFINED; //trigger sending currentStatus again with code NoError } else { reportedErrorIndex = -1; } } } //if (model.getVersion().major == 1) auto status = getStatus(); if (status != currentStatus) { MO_DBG_DEBUG("Status changed %s -> %s %s", currentStatus == ChargePointStatus_UNDEFINED ? "" : cstrFromOcppEveState(currentStatus), cstrFromOcppEveState(status), minimumStatusDurationInt->getInt() ? " (will report delayed)" : ""); currentStatus = status; t_statusTransition = mocpp_tick_ms(); } if (reportedStatus != currentStatus && model.getClock().now() >= MIN_TIME && (minimumStatusDurationInt->getInt() <= 0 || //MinimumStatusDuration disabled mocpp_tick_ms() - t_statusTransition >= ((unsigned long) minimumStatusDurationInt->getInt()) * 1000UL)) { reportedStatus = currentStatus; reportedErrorIndex = errorDataIndex; if (errorDataIndex >= 0) { trackErrorDataInputs[errorDataIndex] = true; } Timestamp reportedTimestamp = model.getClock().now(); reportedTimestamp -= (mocpp_tick_ms() - t_statusTransition) / 1000UL; auto statusNotification = #if MO_ENABLE_V201 model.getVersion().major == 2 ? makeRequest( new Ocpp201::StatusNotification(connectorId, reportedStatus, reportedTimestamp)) : #endif //MO_ENABLE_V201 makeRequest( new Ocpp16::StatusNotification(connectorId, reportedStatus, reportedTimestamp, errorData)); statusNotification->setTimeout(0); context.initiateRequest(std::move(statusNotification)); return; } return; } bool Connector::isFaulted() { //for (auto i = errorDataInputs.begin(); i != errorDataInputs.end(); ++i) { for (size_t i = 0; i < errorDataInputs.size(); i++) { if (errorDataInputs[i].operator()().isFaulted) { return true; } } return false; } const char *Connector::getErrorCode() { if (reportedErrorIndex >= 0) { auto error = errorDataInputs[reportedErrorIndex].operator()(); if (error.isError && error.errorCode) { return error.errorCode; } } return nullptr; } std::shared_ptr Connector::allocateTransaction() { std::shared_ptr tx; //clean possible aborted tx unsigned int txr = txNrEnd; unsigned int txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; for (unsigned int i = 0; i < txSize; i++) { txr = (txr + MAX_TX_CNT - 1) % MAX_TX_CNT; //decrement by 1 auto tx = model.getTransactionStore()->getTransaction(connectorId, txr); //check if dangling silent tx, aborted tx, or corrupted entry (tx == null) if (!tx || tx->isSilent() || (tx->isAborted() && MO_TX_CLEAN_ABORTED)) { //yes, remove bool removed = true; if (auto mService = model.getMeteringService()) { removed &= mService->removeTxMeterData(connectorId, txr); } if (removed) { removed &= model.getTransactionStore()->remove(connectorId, txr); } if (removed) { if (txNrFront == txNrEnd) { txNrFront = txr; } txNrEnd = txr; MO_DBG_WARN("deleted dangling silent or aborted tx for new transaction"); } else { MO_DBG_ERR("memory corruption"); break; } } else { //no, tx record trimmed, end break; } } txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; //refresh after cleaning txs //try to create new transaction if (txSize < MO_TXRECORD_SIZE) { tx = model.getTransactionStore()->createTransaction(connectorId, txNrEnd); } if (!tx) { //could not create transaction - now, try to replace tx history entry unsigned int txl = txNrBegin; txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; for (unsigned int i = 0; i < txSize; i++) { if (tx) { //success, finished here break; } //no transaction allocated, delete history entry to make space auto txhist = model.getTransactionStore()->getTransaction(connectorId, txl); //oldest entry, now check if it's history and can be removed or corrupted entry if (!txhist || txhist->isCompleted() || txhist->isAborted() || (txhist->isSilent() && txhist->getStopSync().isRequested())) { //yes, remove bool removed = true; if (auto mService = model.getMeteringService()) { removed &= mService->removeTxMeterData(connectorId, txl); } if (removed) { removed &= model.getTransactionStore()->remove(connectorId, txl); } if (removed) { txNrBegin = (txl + 1) % MAX_TX_CNT; if (txNrFront == txl) { txNrFront = txNrBegin; } MO_DBG_DEBUG("deleted tx history entry for new transaction"); MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); tx = model.getTransactionStore()->createTransaction(connectorId, txNrEnd); } else { MO_DBG_ERR("memory corruption"); break; } } else { //no, end of history reached, don't delete further tx MO_DBG_DEBUG("cannot delete more tx"); break; } txl++; txl %= MAX_TX_CNT; } } if (!tx) { //couldn't create normal transaction -> check if to start charging without real transaction if (silentOfflineTransactionsBool && silentOfflineTransactionsBool->getBool()) { //try to handle charging session without sending StartTx or StopTx to the server tx = model.getTransactionStore()->createTransaction(connectorId, txNrEnd, true); if (tx) { MO_DBG_DEBUG("created silent transaction"); } } } if (tx) { //clean meter data which could still be here from a rolled-back transaction if (auto mService = model.getMeteringService()) { if (!mService->removeTxMeterData(connectorId, tx->getTxNr())) { MO_DBG_ERR("memory corruption"); } } } if (tx) { txNrEnd = (txNrEnd + 1) % MAX_TX_CNT; MO_DBG_DEBUG("advance txNrEnd %u-%u", connectorId, txNrEnd); MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); } return tx; } std::shared_ptr Connector::beginTransaction(const char *idTag) { if (transaction) { MO_DBG_WARN("tx process still running. Please call endTransaction(...) before"); return nullptr; } MO_DBG_DEBUG("Begin transaction process (%s), prepare", idTag != nullptr ? idTag : ""); bool localAuthFound = false; const char *parentIdTag = nullptr; //locally stored parentIdTag bool offlineBlockedAuth = false; //if offline authorization will be blocked by local auth list entry //check local OCPP whitelist #if MO_ENABLE_LOCAL_AUTH if (auto authService = model.getAuthorizationService()) { auto localAuth = authService->getLocalAuthorization(idTag); //check authorization status if (localAuth && localAuth->getAuthorizationStatus() != AuthorizationStatus::Accepted) { MO_DBG_DEBUG("local auth denied (%s)", idTag); offlineBlockedAuth = true; localAuth = nullptr; } //check expiry if (localAuth && localAuth->getExpiryDate() && *localAuth->getExpiryDate() < model.getClock().now()) { MO_DBG_DEBUG("idTag %s local auth entry expired", idTag); offlineBlockedAuth = true; localAuth = nullptr; } if (localAuth) { localAuthFound = true; parentIdTag = localAuth->getParentIdTag(); } } #endif //MO_ENABLE_LOCAL_AUTH int reservationId = -1; bool offlineBlockedResv = false; //if offline authorization will be blocked by reservation //check if blocked by reservation #if MO_ENABLE_RESERVATION if (model.getReservationService()) { auto reservation = model.getReservationService()->getReservation( connectorId, idTag, parentIdTag); if (reservation) { reservationId = reservation->getReservationId(); } if (reservation && !reservation->matches( idTag, parentIdTag)) { //reservation blocks connector offlineBlockedResv = true; //when offline, tx is always blocked //if parentIdTag is known, abort this tx immediately, otherwise wait for Authorize.conf to decide if (parentIdTag) { //parentIdTag known MO_DBG_INFO("connector %u reserved - abort transaction", connectorId); updateTxNotification(TxNotification_ReservationConflict); return nullptr; } else { //parentIdTag unkown but local authorization failed in any case MO_DBG_INFO("connector %u reserved - no local auth", connectorId); localAuthFound = false; } } } #endif //MO_ENABLE_RESERVATION transaction = allocateTransaction(); if (!transaction) { MO_DBG_ERR("could not allocate Tx"); return nullptr; } if (!idTag || *idTag == '\0') { //input string is empty transaction->setIdTag(""); } else { transaction->setIdTag(idTag); } if (parentIdTag) { transaction->setParentIdTag(parentIdTag); } transaction->setBeginTimestamp(model.getClock().now()); //check for local preauthorization if (localAuthFound && localPreAuthorizeBool && localPreAuthorizeBool->getBool()) { MO_DBG_DEBUG("Begin transaction process (%s), preauthorized locally", idTag != nullptr ? idTag : ""); if (reservationId >= 0) { transaction->setReservationId(reservationId); } transaction->setAuthorized(); updateTxNotification(TxNotification_Authorized); } transaction->commit(); auto authorize = makeRequest(new Ocpp16::Authorize(context.getModel(), idTag)); authorize->setTimeout(authorizationTimeoutInt && authorizationTimeoutInt->getInt() > 0 ? authorizationTimeoutInt->getInt() * 1000UL : 20UL * 1000UL); if (!context.getConnection().isConnected()) { //WebSockt unconnected. Enter offline mode immediately authorize->setTimeout(1); } auto tx = transaction; authorize->setOnReceiveConfListener([this, tx] (JsonObject response) { JsonObject idTagInfo = response["idTagInfo"]; if (strcmp("Accepted", idTagInfo["status"] | "UNDEFINED")) { //Authorization rejected, abort transaction MO_DBG_DEBUG("Authorize rejected (%s), abort tx process", tx->getIdTag()); tx->setIdTagDeauthorized(); tx->commit(); updateTxNotification(TxNotification_AuthorizationRejected); return; } #if MO_ENABLE_RESERVATION if (model.getReservationService()) { auto reservation = model.getReservationService()->getReservation( connectorId, tx->getIdTag(), idTagInfo["parentIdTag"] | (const char*) nullptr); if (reservation) { //reservation found for connector if (reservation->matches( tx->getIdTag(), idTagInfo["parentIdTag"] | (const char*) nullptr)) { MO_DBG_INFO("connector %u matches reservationId %i", connectorId, reservation->getReservationId()); tx->setReservationId(reservation->getReservationId()); } else { //reservation found for connector but does not match idTag or parentIdTag MO_DBG_INFO("connector %u reserved - abort transaction", connectorId); tx->setInactive(); tx->commit(); updateTxNotification(TxNotification_ReservationConflict); return; } } } #endif //MO_ENABLE_RESERVATION if (idTagInfo.containsKey("parentIdTag")) { tx->setParentIdTag(idTagInfo["parentIdTag"] | ""); } MO_DBG_DEBUG("Authorized transaction process (%s)", tx->getIdTag()); tx->setAuthorized(); tx->commit(); updateTxNotification(TxNotification_Authorized); }); //capture local auth and reservation check in for timeout handler authorize->setOnTimeoutListener([this, tx, offlineBlockedAuth, offlineBlockedResv, localAuthFound, reservationId] () { if (offlineBlockedAuth) { //local auth entry exists, but is expired -> avoid offline tx MO_DBG_DEBUG("Abort transaction process (%s), timeout, expired local auth", tx->getIdTag()); tx->setInactive(); tx->commit(); updateTxNotification(TxNotification_AuthorizationTimeout); return; } if (offlineBlockedResv) { //reservation found for connector but does not match idTag or parentIdTag MO_DBG_INFO("connector %u reserved (offline) - abort transaction", connectorId); tx->setInactive(); tx->commit(); updateTxNotification(TxNotification_ReservationConflict); return; } if (localAuthFound && localAuthorizeOfflineBool && localAuthorizeOfflineBool->getBool()) { MO_DBG_DEBUG("Offline transaction process (%s), locally authorized", tx->getIdTag()); if (reservationId >= 0) { tx->setReservationId(reservationId); } tx->setAuthorized(); tx->commit(); updateTxNotification(TxNotification_Authorized); return; } if (allowOfflineTxForUnknownIdBool && allowOfflineTxForUnknownIdBool->getBool()) { MO_DBG_DEBUG("Offline transaction process (%s), allow unknown ID", tx->getIdTag()); if (reservationId >= 0) { tx->setReservationId(reservationId); } tx->setAuthorized(); tx->commit(); updateTxNotification(TxNotification_Authorized); return; } MO_DBG_DEBUG("Abort transaction process (%s): timeout", tx->getIdTag()); tx->setInactive(); tx->commit(); updateTxNotification(TxNotification_AuthorizationTimeout); return; //offline tx disabled }); context.initiateRequest(std::move(authorize)); return transaction; } std::shared_ptr Connector::beginTransaction_authorized(const char *idTag, const char *parentIdTag) { if (transaction) { MO_DBG_WARN("tx process still running. Please call endTransaction(...) before"); return nullptr; } transaction = allocateTransaction(); if (!transaction) { MO_DBG_ERR("could not allocate Tx"); return nullptr; } if (!idTag || *idTag == '\0') { //input string is empty transaction->setIdTag(""); } else { transaction->setIdTag(idTag); } if (parentIdTag) { transaction->setParentIdTag(parentIdTag); } transaction->setBeginTimestamp(model.getClock().now()); MO_DBG_DEBUG("Begin transaction process (%s), already authorized", idTag != nullptr ? idTag : ""); transaction->setAuthorized(); #if MO_ENABLE_RESERVATION if (model.getReservationService()) { if (auto reservation = model.getReservationService()->getReservation(connectorId, idTag, parentIdTag)) { if (reservation->matches(idTag, parentIdTag)) { transaction->setReservationId(reservation->getReservationId()); } } } #endif //MO_ENABLE_RESERVATION transaction->commit(); return transaction; } void Connector::endTransaction(const char *idTag, const char *reason) { if (!transaction || !transaction->isActive()) { //transaction already ended / not active anymore return; } MO_DBG_DEBUG("End session started by idTag %s", transaction->getIdTag()); if (idTag && *idTag != '\0') { transaction->setStopIdTag(idTag); } if (reason) { transaction->setStopReason(reason); } transaction->setInactive(); transaction->commit(); } std::shared_ptr& Connector::getTransaction() { return transaction; } bool Connector::isOperative() { if (isFaulted()) { return false; } if (!trackLoopExecute) { return false; } //check for running transaction(s) - if yes then the connector is always operative if (connectorId == 0) { for (unsigned int cId = 1; cId < model.getNumConnectors(); cId++) { if (model.getConnector(cId)->getTransaction() && model.getConnector(cId)->getTransaction()->isRunning()) { return true; } } } else { if (transaction && transaction->isRunning()) { return true; } } #if MO_ENABLE_V201 if (model.getVersion().major == 2 && model.getTransactionService()) { auto txService = model.getTransactionService(); if (connectorId == 0) { for (unsigned int cId = 1; cId < model.getNumConnectors(); cId++) { if (txService->getEvse(cId)->getTransaction() && txService->getEvse(cId)->getTransaction()->started && !txService->getEvse(cId)->getTransaction()->stopped) { return true; } } } else { if (txService->getEvse(connectorId)->getTransaction() && txService->getEvse(connectorId)->getTransaction()->started && !txService->getEvse(connectorId)->getTransaction()->stopped) { return true; } } } #endif //MO_ENABLE_V201 return availabilityVolatile && availabilityBool->getBool(); } void Connector::setAvailability(bool available) { availabilityBool->setBool(available); configuration_save(); } void Connector::setAvailabilityVolatile(bool available) { availabilityVolatile = available; } void Connector::setConnectorPluggedInput(std::function connectorPlugged) { this->connectorPluggedInput = connectorPlugged; } void Connector::setEvReadyInput(std::function evRequestsEnergy) { this->evReadyInput = evRequestsEnergy; } void Connector::setEvseReadyInput(std::function connectorEnergized) { this->evseReadyInput = connectorEnergized; } void Connector::addErrorCodeInput(std::function connectorErrorCode) { addErrorDataInput([connectorErrorCode] () -> ErrorData { return ErrorData(connectorErrorCode()); }); } void Connector::addErrorDataInput(std::function errorDataInput) { this->errorDataInputs.push_back(errorDataInput); this->trackErrorDataInputs.push_back(false); } #if MO_ENABLE_CONNECTOR_LOCK void Connector::setOnUnlockConnector(std::function unlockConnector) { this->onUnlockConnector = unlockConnector; } std::function Connector::getOnUnlockConnector() { return this->onUnlockConnector; } #endif //MO_ENABLE_CONNECTOR_LOCK void Connector::setStartTxReadyInput(std::function startTxReady) { this->startTxReadyInput = startTxReady; } void Connector::setStopTxReadyInput(std::function stopTxReady) { this->stopTxReadyInput = stopTxReady; } void Connector::setOccupiedInput(std::function occupied) { this->occupiedInput = occupied; } void Connector::setTxNotificationOutput(std::function txNotificationOutput) { this->txNotificationOutput = txNotificationOutput; } void Connector::updateTxNotification(TxNotification event) { if (txNotificationOutput) { txNotificationOutput(transaction.get(), event); } } unsigned int Connector::getFrontRequestOpNr() { /* * Advance front transaction? */ unsigned int txSize = (txNrEnd + MAX_TX_CNT - txNrFront) % MAX_TX_CNT; if (transactionFront && txSize == 0) { //catch edge case where txBack has been rolled back and txFront was equal to txBack MO_DBG_DEBUG("collect front transaction %u-%u after tx rollback", connectorId, transactionFront->getTxNr()); MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); transactionFront = nullptr; } for (unsigned int i = 0; i < txSize; i++) { if (!transactionFront) { transactionFront = model.getTransactionStore()->getTransaction(connectorId, txNrFront); #if MO_DBG_LEVEL >= MO_DL_VERBOSE if (transactionFront) { MO_DBG_VERBOSE("load front transaction %u-%u", connectorId, transactionFront->getTxNr()); } #endif } if (transactionFront && (transactionFront->isAborted() || transactionFront->isCompleted() || transactionFront->isSilent())) { //advance front MO_DBG_DEBUG("collect front transaction %u-%u", connectorId, transactionFront->getTxNr()); transactionFront = nullptr; txNrFront = (txNrFront + 1) % MAX_TX_CNT; MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); } else { //front is accurate. Done here break; } } if (transactionFront) { if (transactionFront->getStartSync().isRequested() && !transactionFront->getStartSync().isConfirmed()) { return transactionFront->getStartSync().getOpNr(); } if (transactionFront->getStopSync().isRequested() && !transactionFront->getStopSync().isConfirmed()) { return transactionFront->getStopSync().getOpNr(); } } return NoOperation; } std::unique_ptr Connector::fetchFrontRequest() { if (transactionFront && !transactionFront->isSilent()) { if (transactionFront->getStartSync().isRequested() && !transactionFront->getStartSync().isConfirmed()) { //send StartTx? bool cancelStartTx = false; if (transactionFront->getStartTimestamp() < MIN_TIME && transactionFront->getStartBootNr() != model.getBootNr()) { //time not set, cannot be restored anymore -> invalid tx MO_DBG_ERR("cannot recover tx from previus run"); cancelStartTx = true; } if ((int)transactionFront->getStartSync().getAttemptNr() >= transactionMessageAttemptsInt->getInt()) { MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard transaction"); cancelStartTx = true; } if (cancelStartTx) { transactionFront->setSilent(); transactionFront->setInactive(); transactionFront->commit(); //clean up possible tx records if (auto mSerivce = model.getMeteringService()) { mSerivce->removeTxMeterData(connectorId, transactionFront->getTxNr()); } //next getFrontRequestOpNr() call will collect transactionFront return nullptr; } Timestamp nextAttempt = transactionFront->getStartSync().getAttemptTime() + transactionFront->getStartSync().getAttemptNr() * std::max(0, transactionMessageRetryIntervalInt->getInt()); if (nextAttempt > model.getClock().now()) { return nullptr; } transactionFront->getStartSync().advanceAttemptNr(); transactionFront->getStartSync().setAttemptTime(model.getClock().now()); transactionFront->commit(); auto startTx = makeRequest(new Ocpp16::StartTransaction(model, transactionFront)); startTx->setOnReceiveConfListener([this] (JsonObject response) { //fetch authorization status from StartTransaction.conf() for user notification const char* idTagInfoStatus = response["idTagInfo"]["status"] | "_Undefined"; if (strcmp(idTagInfoStatus, "Accepted")) { updateTxNotification(TxNotification_DeAuthorized); } }); auto transactionFront_capture = transactionFront; startTx->setOnAbortListener([this, transactionFront_capture] () { //shortcut to the attemptNr check above. Relevant if other operations block the queue while this StartTx is timing out if (transactionFront_capture && (int)transactionFront_capture->getStartSync().getAttemptNr() >= transactionMessageAttemptsInt->getInt()) { MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard transaction"); transactionFront_capture->setSilent(); transactionFront_capture->setInactive(); transactionFront_capture->commit(); //clean up possible tx records if (auto mSerivce = model.getMeteringService()) { mSerivce->removeTxMeterData(connectorId, transactionFront_capture->getTxNr()); } //next getFrontRequestOpNr() call will collect transactionFront } }); return startTx; } if (transactionFront->getStopSync().isRequested() && !transactionFront->getStopSync().isConfirmed()) { //send StopTx? if ((int)transactionFront->getStopSync().getAttemptNr() >= transactionMessageAttemptsInt->getInt()) { MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard transaction"); transactionFront->setSilent(); //clean up possible tx records if (auto mSerivce = model.getMeteringService()) { mSerivce->removeTxMeterData(connectorId, transactionFront->getTxNr()); } //next getFrontRequestOpNr() call will collect transactionFront return nullptr; } Timestamp nextAttempt = transactionFront->getStopSync().getAttemptTime() + transactionFront->getStopSync().getAttemptNr() * std::max(0, transactionMessageRetryIntervalInt->getInt()); if (nextAttempt > model.getClock().now()) { return nullptr; } transactionFront->getStopSync().advanceAttemptNr(); transactionFront->getStopSync().setAttemptTime(model.getClock().now()); transactionFront->commit(); std::shared_ptr stopTxData; if (auto meteringService = model.getMeteringService()) { stopTxData = meteringService->getStopTxMeterData(transactionFront.get()); } std::unique_ptr stopTx; if (stopTxData) { stopTx = makeRequest(new Ocpp16::StopTransaction(model, transactionFront, stopTxData->retrieveStopTxData())); } else { stopTx = makeRequest(new Ocpp16::StopTransaction(model, transactionFront)); } auto transactionFront_capture = transactionFront; stopTx->setOnAbortListener([this, transactionFront_capture] () { //shortcut to the attemptNr check above. Relevant if other operations block the queue while this StopTx is timing out if ((int)transactionFront_capture->getStopSync().getAttemptNr() >= transactionMessageAttemptsInt->getInt()) { MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard transaction"); transactionFront_capture->setSilent(); transactionFront_capture->setInactive(); transactionFront_capture->commit(); //clean up possible tx records if (auto mSerivce = model.getMeteringService()) { mSerivce->removeTxMeterData(connectorId, transactionFront_capture->getTxNr()); } //next getFrontRequestOpNr() call will collect transactionFront } }); return stopTx; } } return nullptr; } bool Connector::triggerStatusNotification() { ErrorData errorData {nullptr}; errorData.severity = 0; if (reportedErrorIndex >= 0) { errorData = errorDataInputs[reportedErrorIndex].operator()(); } else { //find errorData with maximum severity for (auto i = errorDataInputs.size(); i >= 1; i--) { auto index = i - 1; ErrorData error = errorDataInputs[index].operator()(); if (error.isError && error.severity >= errorData.severity) { errorData = error; } } } auto statusNotification = makeRequest(new Ocpp16::StatusNotification( connectorId, getStatus(), context.getModel().getClock().now(), errorData)); statusNotification->setTimeout(60000); context.getRequestQueue().sendRequestPreBoot(std::move(statusNotification)); return true; } unsigned int Connector::getTxNrBeginHistory() { return txNrBegin; } unsigned int Connector::getTxNrFront() { return txNrFront; } unsigned int Connector::getTxNrEnd() { return txNrEnd; } ================================================ FILE: src/MicroOcpp/Model/ConnectorBase/Connector.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CONNECTOR_H #define MO_CONNECTOR_H #include #include #include #include #include #include #include #include #include #include #include #ifndef MO_TXRECORD_SIZE #define MO_TXRECORD_SIZE 4 //no. of tx to hold on flash storage #endif #ifndef MO_REPORT_NOERROR #define MO_REPORT_NOERROR 0 #endif namespace MicroOcpp { class Context; class Model; class Operation; class Connector : public RequestEmitter, public MemoryManaged { private: Context& context; Model& model; std::shared_ptr filesystem; const unsigned int connectorId; std::shared_ptr transaction; std::shared_ptr availabilityBool; char availabilityBoolKey [sizeof(MO_CONFIG_EXT_PREFIX "AVAIL_CONN_xxxx") + 1]; bool availabilityVolatile = true; std::function connectorPluggedInput; std::function evReadyInput; std::function evseReadyInput; Vector> errorDataInputs; Vector trackErrorDataInputs; int reportedErrorIndex = -1; //last reported error bool isFaulted(); const char *getErrorCode(); ChargePointStatus currentStatus = ChargePointStatus_UNDEFINED; std::shared_ptr minimumStatusDurationInt; //in seconds ChargePointStatus reportedStatus = ChargePointStatus_UNDEFINED; unsigned long t_statusTransition = 0; #if MO_ENABLE_CONNECTOR_LOCK std::function onUnlockConnector; #endif //MO_ENABLE_CONNECTOR_LOCK std::function startTxReadyInput; //the StartTx request will be delayed while this Input is false std::function stopTxReadyInput; //the StopTx request will be delayed while this Input is false std::function occupiedInput; //instead of Available, go into Preparing / Finishing state std::function txNotificationOutput; std::shared_ptr connectionTimeOutInt; //in seconds std::shared_ptr stopTransactionOnInvalidIdBool; std::shared_ptr stopTransactionOnEVSideDisconnectBool; std::shared_ptr localPreAuthorizeBool; std::shared_ptr localAuthorizeOfflineBool; std::shared_ptr allowOfflineTxForUnknownIdBool; std::shared_ptr silentOfflineTransactionsBool; std::shared_ptr authorizationTimeoutInt; //in seconds std::shared_ptr freeVendActiveBool; std::shared_ptr freeVendIdTagString; bool freeVendTrackPlugged = false; std::shared_ptr txStartOnPowerPathClosedBool; // this postpones the tx start point to when evReadyInput becomes true std::shared_ptr transactionMessageAttemptsInt; std::shared_ptr transactionMessageRetryIntervalInt; bool trackLoopExecute = false; //if loop has been executed once unsigned int txNrBegin = 0; //oldest (historical) transaction on flash. Has no function, but is useful for error diagnosis unsigned int txNrFront = 0; //oldest transaction which is still queued to be sent to the server unsigned int txNrEnd = 0; //one position behind newest transaction std::shared_ptr transactionFront; public: Connector(Context& context, std::shared_ptr filesystem, unsigned int connectorId); Connector(const Connector&) = delete; Connector(Connector&&) = delete; Connector& operator=(const Connector&) = delete; ~Connector(); /* * beginTransaction begins the transaction process which eventually leads to a StartTransaction * request in the normal case. * * If the transaction process begins successfully, a Transaction object is returned * If no transaction process begins due to this call, nullptr is returned (e.g. memory allocation failed) */ std::shared_ptr beginTransaction(const char *idTag); std::shared_ptr beginTransaction_authorized(const char *idTag, const char *parentIdTag = nullptr); /* * End the current transaction process, if existing and not ended yet. This eventually leads to * a StopTransaction request, if the transaction process has actually ended due to this call. It * is safe to call this function at any time even if no transaction is running */ void endTransaction(const char *idTag = nullptr, const char *reason = nullptr); std::shared_ptr& getTransaction(); //create detached transaction - won't have any side-effects with the transaction handling of this lib std::shared_ptr allocateTransaction(); bool isOperative(); void setAvailability(bool available); void setAvailabilityVolatile(bool available); //set inoperative state but keep only until reboot at most void setConnectorPluggedInput(std::function connectorPlugged); void setEvReadyInput(std::function evRequestsEnergy); void setEvseReadyInput(std::function connectorEnergized); void addErrorCodeInput(std::function connectorErrorCode); void addErrorDataInput(std::function errorCodeInput); void loop(); ChargePointStatus getStatus(); bool ocppPermitsCharge(); #if MO_ENABLE_CONNECTOR_LOCK void setOnUnlockConnector(std::function unlockConnector); std::function getOnUnlockConnector(); #endif //MO_ENABLE_CONNECTOR_LOCK void setStartTxReadyInput(std::function startTxReady); void setStopTxReadyInput(std::function stopTxReady); void setOccupiedInput(std::function occupied); void setTxNotificationOutput(std::function txNotificationOutput); void updateTxNotification(TxNotification event); unsigned int getFrontRequestOpNr() override; std::unique_ptr fetchFrontRequest() override; bool triggerStatusNotification(); unsigned int getTxNrBeginHistory(); //if getTxNrBeginHistory() != getTxNrFront(), then return value is the txNr of the oldest tx history entry. If equal to getTxNrFront(), then the history is empty unsigned int getTxNrFront(); //if getTxNrEnd() != getTxNrFront(), then return value is the txNr of the oldest transaction queued to be sent to the server. If equal to getTxNrEnd(), then there is no tx to be sent to the server unsigned int getTxNrEnd(); //upper limit for the range of valid txNrs }; } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MicroOcpp; ConnectorsCommon::ConnectorsCommon(Context& context, unsigned int numConn, std::shared_ptr filesystem) : MemoryManaged("v16.ConnectorBase.ConnectorsCommon"), context(context) { declareConfiguration("NumberOfConnectors", numConn >= 1 ? numConn - 1 : 0, CONFIGURATION_VOLATILE, true); /* * Further configuration keys which correspond to the Core profile */ declareConfiguration("AuthorizeRemoteTxRequests", false); declareConfiguration("GetConfigurationMaxKeys", 30, CONFIGURATION_VOLATILE, true); context.getOperationRegistry().registerOperation("ChangeAvailability", [&context] () { return new Ocpp16::ChangeAvailability(context.getModel());}); context.getOperationRegistry().registerOperation("ChangeConfiguration", [] () { return new Ocpp16::ChangeConfiguration();}); context.getOperationRegistry().registerOperation("ClearCache", [filesystem] () { return new Ocpp16::ClearCache(filesystem);}); context.getOperationRegistry().registerOperation("DataTransfer", [] () { return new Ocpp16::DataTransfer();}); context.getOperationRegistry().registerOperation("GetConfiguration", [] () { return new Ocpp16::GetConfiguration();}); context.getOperationRegistry().registerOperation("RemoteStartTransaction", [&context] () { return new Ocpp16::RemoteStartTransaction(context.getModel());}); context.getOperationRegistry().registerOperation("RemoteStopTransaction", [&context] () { return new Ocpp16::RemoteStopTransaction(context.getModel());}); context.getOperationRegistry().registerOperation("Reset", [&context] () { return new Ocpp16::Reset(context.getModel());}); context.getOperationRegistry().registerOperation("TriggerMessage", [&context] () { return new Ocpp16::TriggerMessage(context);}); context.getOperationRegistry().registerOperation("UnlockConnector", [&context] () { return new Ocpp16::UnlockConnector(context.getModel());}); /* * Register further message handlers to support echo mode: when this library * is connected with a WebSocket echo server, let it reply to its own requests. * Mocking an OCPP Server on the same device makes running (unit) tests easier. */ #if MO_ENABLE_V201 if (context.getVersion().major == 2) { // OCPP 2.0.1 compliant echo messages context.getOperationRegistry().registerOperation("Authorize", [&context] () { return new Ocpp201::Authorize(context.getModel(), "");}); context.getOperationRegistry().registerOperation("TransactionEvent", [&context] () { return new Ocpp201::TransactionEvent(context.getModel(), nullptr);}); } else #endif //MO_ENABLE_V201 { // OCPP 1.6 compliant echo messages context.getOperationRegistry().registerOperation("Authorize", [&context] () { return new Ocpp16::Authorize(context.getModel(), "");}); context.getOperationRegistry().registerOperation("StartTransaction", [&context] () { return new Ocpp16::StartTransaction(context.getModel(), nullptr);}); context.getOperationRegistry().registerOperation("StopTransaction", [&context] () { return new Ocpp16::StopTransaction(context.getModel(), nullptr);}); } // OCPP 1.6 + 2.0.1 compliant echo messages context.getOperationRegistry().registerOperation("StatusNotification", [&context] () { return new Ocpp16::StatusNotification(-1, ChargePointStatus_UNDEFINED, Timestamp());}); } void ConnectorsCommon::loop() { //do nothing } ================================================ FILE: src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CHARGECONTROLCOMMON_H #define MO_CHARGECONTROLCOMMON_H #include #include namespace MicroOcpp { class Context; class ConnectorsCommon : public MemoryManaged { private: Context& context; public: ConnectorsCommon(Context& context, unsigned int numConnectors, std::shared_ptr filesystem); void loop(); }; } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Model/ConnectorBase/EvseId.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_EVSEID_H #define MO_EVSEID_H #include #if MO_ENABLE_V201 // number of EVSE IDs (including 0). Defaults to MO_NUMCONNECTORS if defined, otherwise to 2 #ifndef MO_NUM_EVSEID #if defined(MO_NUMCONNECTORS) #define MO_NUM_EVSEID MO_NUMCONNECTORS #else #define MO_NUM_EVSEID 2 #endif #endif // MO_NUM_EVSEID namespace MicroOcpp { // EVSEType (2.23) struct EvseId { int id; int connectorId = -1; //optional EvseId(int id) : id(id) { } EvseId(int id, int connectorId) : id(id), connectorId(connectorId) { } }; } #endif // MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Model/ConnectorBase/UnlockConnectorResult.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_UNLOCKCONNECTORRESULT_H #define MO_UNLOCKCONNECTORRESULT_H #include // Connector-lock related behavior (i.e. if UnlockConnectorOnEVSideDisconnect is RW; enable HW binding for UnlockConnector) #ifndef MO_ENABLE_CONNECTOR_LOCK #define MO_ENABLE_CONNECTOR_LOCK 0 #endif #if MO_ENABLE_CONNECTOR_LOCK #ifdef __cplusplus extern "C" { #endif // __cplusplus #ifndef MO_UNLOCK_TIMEOUT #define MO_UNLOCK_TIMEOUT 10000 // if Result is Pending, wait at most this period (in ms) until sending UnlockFailed #endif typedef enum { UnlockConnectorResult_UnlockFailed, UnlockConnectorResult_Unlocked, UnlockConnectorResult_Pending // unlock action not finished yet, result still unknown (MO will check again later) } UnlockConnectorResult; #ifdef __cplusplus } #endif // __cplusplus #endif // MO_ENABLE_CONNECTOR_LOCK #endif // MO_UNLOCKCONNECTORRESULT_H ================================================ FILE: src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include //Fetch relevant data from other modules for diagnostics #include #include //for serializing ChargePointStatus #include #include #include //for MO_ENABLE_V201 #include //for MO_ENABLE_CONNECTOR_LOCK using MicroOcpp::DiagnosticsService; using MicroOcpp::Ocpp16::DiagnosticsStatus; using MicroOcpp::Request; DiagnosticsService::DiagnosticsService(Context& context) : MemoryManaged("v16.Diagnostics.DiagnosticsService"), context(context), location(makeString(getMemoryTag())), diagFileList(makeVector(getMemoryTag())) { context.getOperationRegistry().registerOperation("GetDiagnostics", [this] () { return new Ocpp16::GetDiagnostics(*this);}); //Register message handler for TriggerMessage operation context.getOperationRegistry().registerOperation("DiagnosticsStatusNotification", [this] () { return new Ocpp16::DiagnosticsStatusNotification(getDiagnosticsStatus());}); } DiagnosticsService::~DiagnosticsService() { MO_FREE(diagPreamble); MO_FREE(diagPostamble); } void DiagnosticsService::loop() { if (ftpUpload && ftpUpload->isActive()) { ftpUpload->loop(); } if (ftpUpload) { if (ftpUpload->isActive()) { ftpUpload->loop(); } else { MO_DBG_DEBUG("Deinit FTP upload"); ftpUpload.reset(); } } auto notification = getDiagnosticsStatusNotification(); if (notification) { context.initiateRequest(std::move(notification)); } const auto& timestampNow = context.getModel().getClock().now(); if (retries > 0 && timestampNow >= nextTry) { if (!uploadIssued) { if (onUpload != nullptr) { MO_DBG_DEBUG("Call onUpload"); onUpload(location.c_str(), startTime, stopTime); uploadIssued = true; uploadFailure = false; } else { MO_DBG_ERR("onUpload must be set! (see setOnUpload). Will abort"); retries = 0; uploadIssued = false; uploadFailure = true; } } if (uploadIssued) { if (uploadStatusInput != nullptr && uploadStatusInput() == UploadStatus::Uploaded) { //success! MO_DBG_DEBUG("end upload routine (by status)"); uploadIssued = false; retries = 0; } //check if maximum time elapsed or failed const int UPLOAD_TIMEOUT = 60; if (timestampNow - nextTry >= UPLOAD_TIMEOUT || (uploadStatusInput != nullptr && uploadStatusInput() == UploadStatus::UploadFailed)) { //maximum upload time elapsed or failed if (uploadStatusInput == nullptr) { //No way to find out if failed. But maximum time has elapsed. Assume success MO_DBG_DEBUG("end upload routine (by timer)"); uploadIssued = false; retries = 0; } else { //either we have UploadFailed status or (NotUploaded + timeout) here MO_DBG_WARN("Upload timeout or failed"); ftpUpload.reset(); const int TRANSITION_DELAY = 10; if (retryInterval <= UPLOAD_TIMEOUT + TRANSITION_DELAY) { nextTry = timestampNow; nextTry += TRANSITION_DELAY; //wait for another 10 seconds } else { nextTry += retryInterval; } retries--; if (retries == 0) { MO_DBG_DEBUG("end upload routine (no more retry)"); uploadFailure = true; } } } } //end if (uploadIssued) } //end try upload } //timestamps before year 2021 will be treated as "undefined" MicroOcpp::String DiagnosticsService::requestDiagnosticsUpload(const char *location, unsigned int retries, unsigned int retryInterval, Timestamp startTime, Timestamp stopTime) { if (onUpload == nullptr) { return makeString(getMemoryTag()); } String fileName; if (refreshFilename) { fileName = refreshFilename().c_str(); } else { fileName = "diagnostics.log"; } this->location.reserve(strlen(location) + 1 + fileName.size()); this->location = location; if (!this->location.empty() && this->location.back() != '/') { this->location.append("/"); } this->location.append(fileName.c_str()); this->retries = retries; this->retryInterval = retryInterval; this->startTime = startTime; Timestamp stopMin = Timestamp(2021,0,0,0,0,0); if (stopTime >= stopMin) { this->stopTime = stopTime; } else { auto newStop = context.getModel().getClock().now(); newStop += 3600 * 24 * 365; //set new stop time one year in future this->stopTime = newStop; } #if MO_DBG_LEVEL >= MO_DL_INFO { char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; char dbuf2 [JSONDATE_LENGTH + 1] = {'\0'}; this->startTime.toJsonString(dbuf, JSONDATE_LENGTH + 1); this->stopTime.toJsonString(dbuf2, JSONDATE_LENGTH + 1); MO_DBG_INFO("Scheduled Diagnostics upload!\n" \ " location = %s\n" \ " retries = %i" \ ", retryInterval = %u" \ " startTime = %s\n" \ " stopTime = %s", this->location.c_str(), this->retries, this->retryInterval, dbuf, dbuf2); } #endif nextTry = context.getModel().getClock().now(); nextTry += 5; //wait for 5s before upload uploadIssued = false; #if MO_DBG_LEVEL >= MO_DL_DEBUG { char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; nextTry.toJsonString(dbuf, JSONDATE_LENGTH + 1); MO_DBG_DEBUG("Initial try at %s", dbuf); } #endif return fileName; } DiagnosticsStatus DiagnosticsService::getDiagnosticsStatus() { if (uploadFailure) { return DiagnosticsStatus::UploadFailed; } if (uploadIssued) { if (uploadStatusInput != nullptr) { switch (uploadStatusInput()) { case UploadStatus::NotUploaded: return DiagnosticsStatus::Uploading; case UploadStatus::Uploaded: return DiagnosticsStatus::Uploaded; case UploadStatus::UploadFailed: return DiagnosticsStatus::UploadFailed; } } return DiagnosticsStatus::Uploading; } return DiagnosticsStatus::Idle; } std::unique_ptr DiagnosticsService::getDiagnosticsStatusNotification() { if (getDiagnosticsStatus() != lastReportedStatus) { lastReportedStatus = getDiagnosticsStatus(); if (lastReportedStatus != DiagnosticsStatus::Idle) { Operation *diagNotificationMsg = new Ocpp16::DiagnosticsStatusNotification(lastReportedStatus); auto diagNotification = makeRequest(diagNotificationMsg); return diagNotification; } } return nullptr; } void DiagnosticsService::setRefreshFilename(std::function refreshFn) { this->refreshFilename = refreshFn; } void DiagnosticsService::setOnUpload(std::function onUpload) { this->onUpload = onUpload; } void DiagnosticsService::setOnUploadStatusInput(std::function uploadStatusInput) { this->uploadStatusInput = uploadStatusInput; } void DiagnosticsService::setDiagnosticsReader(std::function diagnosticsReader, std::function onClose, std::shared_ptr filesystem) { this->onUpload = [this, diagnosticsReader, onClose, filesystem] (const char *location, Timestamp &startTime, Timestamp &stopTime) -> bool { auto ftpClient = context.getFtpClient(); if (!ftpClient) { MO_DBG_ERR("FTP client not set"); this->ftpUploadStatus = UploadStatus::UploadFailed; return false; } const size_t diagPreambleSize = 128; diagPreamble = static_cast(MO_MALLOC(getMemoryTag(), diagPreambleSize)); if (!diagPreamble) { MO_DBG_ERR("OOM"); this->ftpUploadStatus = UploadStatus::UploadFailed; return false; } diagPreambleLen = 0; diagPreambleTransferred = 0; diagReaderHasData = diagnosticsReader ? true : false; const size_t diagPostambleSize = 1024; diagPostamble = static_cast(MO_MALLOC(getMemoryTag(), diagPostambleSize)); if (!diagPostamble) { MO_DBG_ERR("OOM"); this->ftpUploadStatus = UploadStatus::UploadFailed; MO_FREE(diagPreamble); return false; } diagPostambleLen = 0; diagPostambleTransferred = 0; diagFilesBackTransferred = 0; auto& model = context.getModel(); auto cpVendor = makeString(getMemoryTag()); auto cpModel = makeString(getMemoryTag()); auto fwVersion = makeString(getMemoryTag()); if (auto bootService = model.getBootService()) { if (auto cpCreds = bootService->getChargePointCredentials()) { cpVendor = (*cpCreds)["chargePointVendor"] | "Vendor"; cpModel = (*cpCreds)["chargePointModel"] | "Charger"; fwVersion = (*cpCreds)["firmwareVersion"] | ""; } } char jsonDate [JSONDATE_LENGTH + 1]; model.getClock().now().toJsonString(jsonDate, sizeof(jsonDate)); int ret; ret = snprintf(diagPreamble, diagPreambleSize, "### %s %s - Hardware Diagnostics%s%s\n%s\n", cpVendor.c_str(), cpModel.c_str(), fwVersion.empty() ? "" : " - v. ", fwVersion.c_str(), jsonDate); if (ret < 0 || (size_t)ret >= diagPreambleSize) { MO_DBG_ERR("snprintf: %i", ret); this->ftpUploadStatus = UploadStatus::UploadFailed; MO_FREE(diagPreamble); MO_FREE(diagPostamble); return false; } diagPreambleLen += (size_t)ret; Connector *connector0 = model.getConnector(0); Connector *connector1 = model.getConnector(1); Transaction *connector1Tx = connector1 ? connector1->getTransaction().get() : nullptr; Connector *connector2 = model.getNumConnectors() > 2 ? model.getConnector(2) : nullptr; Transaction *connector2Tx = connector2 ? connector2->getTransaction().get() : nullptr; ret = 0; if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { diagPostambleLen += (size_t)ret; ret = snprintf(diagPostamble + diagPostambleLen, diagPostambleSize - diagPostambleLen, "\n# OCPP" "\nclient_version=%s" "\nuptime=%lus" "%s%s" "%s%s" "%s%s" "\nws_status=%s" "\nws_last_conn=%lus" "\nws_last_recv=%lus" "%s%s" "%s%s" "%s%s" "%s%s" "%s%s" "%s%s" "%s%s" "%s%s" "\nENABLE_CONNECTOR_LOCK=%i" "\nENABLE_FILE_INDEX=%i" "\nENABLE_V201=%i" "\n", MO_VERSION, mocpp_tick_ms() / 1000UL, connector0 ? "\nocpp_status_cId0=" : "", connector0 ? cstrFromOcppEveState(connector0->getStatus()) : "", connector1 ? "\nocpp_status_cId1=" : "", connector1 ? cstrFromOcppEveState(connector1->getStatus()) : "", connector2 ? "\nocpp_status_cId2=" : "", connector2 ? cstrFromOcppEveState(connector2->getStatus()) : "", context.getConnection().isConnected() ? "connected" : "unconnected", context.getConnection().getLastConnected() / 1000UL, context.getConnection().getLastRecv() / 1000UL, connector1 ? "\ncId1_hasTx=" : "", connector1 ? (connector1Tx ? "1" : "0") : "", connector1Tx ? "\ncId1_txActive=" : "", connector1Tx ? (connector1Tx->isActive() ? "1" : "0") : "", connector1Tx ? "\ncId1_txHasStarted=" : "", connector1Tx ? (connector1Tx->getStartSync().isRequested() ? "1" : "0") : "", connector1Tx ? "\ncId1_txHasStopped=" : "", connector1Tx ? (connector1Tx->getStopSync().isRequested() ? "1" : "0") : "", connector2 ? "\ncId2_hasTx=" : "", connector2 ? (connector2Tx ? "1" : "0") : "", connector2Tx ? "\ncId2_txActive=" : "", connector2Tx ? (connector2Tx->isActive() ? "1" : "0") : "", connector2Tx ? "\ncId2_txHasStarted=" : "", connector2Tx ? (connector2Tx->getStartSync().isRequested() ? "1" : "0") : "", connector2Tx ? "\ncId2_txHasStopped=" : "", connector2Tx ? (connector2Tx->getStopSync().isRequested() ? "1" : "0") : "", MO_ENABLE_CONNECTOR_LOCK, MO_ENABLE_FILE_INDEX, MO_ENABLE_V201 ); } if (filesystem) { if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { diagPostambleLen += (size_t)ret; ret = snprintf(diagPostamble + diagPostambleLen, diagPostambleSize - diagPostambleLen, "\n# Filesystem\n"); } filesystem->ftw_root([this, &ret] (const char *fname) -> int { if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { diagPostambleLen += (size_t)ret; ret = snprintf(diagPostamble + diagPostambleLen, diagPostambleSize - diagPostambleLen, "%s\n", fname); } diagFileList.emplace_back(fname); return 0; }); MO_DBG_DEBUG("discovered %zu files", diagFileList.size()); } if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { diagPostambleLen += (size_t)ret; } else { char errMsg [64]; auto errLen = snprintf(errMsg, sizeof(errMsg), "\n[Diagnostics cut]\n"); size_t ellipseStart = std::min(diagPostambleSize - (size_t)errLen - 1, diagPostambleLen); auto ret2 = snprintf(diagPostamble + ellipseStart, diagPostambleSize - ellipseStart, "%s", errMsg); diagPostambleLen += (size_t)ret2; } this->ftpUpload = ftpClient->postFile(location, [this, diagnosticsReader, filesystem] (unsigned char *buf, size_t size) -> size_t { size_t written = 0; if (written < size && diagPreambleTransferred < diagPreambleLen) { size_t writeLen = std::min(size - written, diagPreambleLen - diagPreambleTransferred); memcpy(buf + written, diagPreamble + diagPreambleTransferred, writeLen); diagPreambleTransferred += writeLen; written += writeLen; } while (written < size && diagReaderHasData && diagnosticsReader) { size_t writeLen = diagnosticsReader((char*)buf + written, size - written); if (writeLen == 0) { diagReaderHasData = false; } written += writeLen; } if (written < size && diagPostambleTransferred < diagPostambleLen) { size_t writeLen = std::min(size - written, diagPostambleLen - diagPostambleTransferred); memcpy(buf + written, diagPostamble + diagPostambleTransferred, writeLen); diagPostambleTransferred += writeLen; written += writeLen; } while (written < size && !diagFileList.empty() && filesystem) { char fpath [MO_MAX_PATH_SIZE]; auto ret = snprintf(fpath, sizeof(fpath), "%s%s", MO_FILENAME_PREFIX, diagFileList.back().c_str()); if (ret < 0 || (size_t)ret >= sizeof(fpath)) { MO_DBG_ERR("fn error: %i", ret); diagFileList.pop_back(); // next file starts from offset 0 diagFilesBackTransferred = 0; continue; } if (auto file = filesystem->open(fpath, "r")) { if (diagFilesBackTransferred == 0) { char fileHeading [30 + MO_MAX_PATH_SIZE]; auto writeLen = snprintf(fileHeading, sizeof(fileHeading), "\n\n# File %s:\n", diagFileList.back().c_str()); if (writeLen < 0 || (size_t)writeLen >= sizeof(fileHeading)) { MO_DBG_ERR("fn error: %i", ret); diagFileList.pop_back(); diagFilesBackTransferred = 0; continue; } if (writeLen + written > size || //heading doesn't fit anymore, return with a bit unused buffer space and print heading the next time writeLen + written == size) { //filling the buffer up exactly would mean that no file payload is written and this head gets printed again MO_DBG_DEBUG("upload diag chunk (%zuB)", written); return written; } memcpy(buf + written, fileHeading, (size_t)writeLen); written += (size_t)writeLen; } file->seek(diagFilesBackTransferred); size_t writeLen = file->read((char*)buf + written, size - written); // advance per-file offset diagFilesBackTransferred += writeLen; if (writeLen < size - written) { // EOF for this file; move to next and reset offset MO_DBG_DEBUG("upload diag chunk %zu (done)", diagFilesBackTransferred); diagFileList.pop_back(); diagFilesBackTransferred = 0; } written += writeLen; } else { MO_DBG_ERR("could not open file: %s", fpath); diagFileList.pop_back(); diagFilesBackTransferred = 0; } } MO_DBG_DEBUG("upload diag chunk (%zuB)", written); return written; }, [this, onClose] (MO_FtpCloseReason reason) -> void { if (reason == MO_FtpCloseReason_Success) { MO_DBG_INFO("FTP upload success"); this->ftpUploadStatus = UploadStatus::Uploaded; } else { MO_DBG_INFO("FTP upload failure (%i)", reason); this->ftpUploadStatus = UploadStatus::UploadFailed; } MO_FREE(diagPreamble); MO_FREE(diagPostamble); diagFileList.clear(); diagFilesBackTransferred = 0; //reset offset for future uploads if (onClose) { onClose(); } }); if (this->ftpUpload) { this->ftpUploadStatus = UploadStatus::NotUploaded; return true; } else { this->ftpUploadStatus = UploadStatus::UploadFailed; return false; } }; this->uploadStatusInput = [this] () { return this->ftpUploadStatus; }; } void DiagnosticsService::setFtpServerCert(const char *cert) { this->ftpServerCert = cert; } #if !defined(MO_CUSTOM_DIAGNOSTICS) #if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS #include "esp_heap_caps.h" #include bool g_diagsSent = false; std::unique_ptr MicroOcpp::makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem) { std::unique_ptr diagService = std::unique_ptr(new DiagnosticsService(context)); diagService->setDiagnosticsReader( [] (char *buf, size_t size) -> size_t { if (!g_diagsSent) { g_diagsSent = true; int ret = snprintf(buf, size, "\n# Memory\n" "freeHeap=%zu\n" "minHeap=%zu\n" "maxAllocHeap=%zu\n" "LittleFS_used=%zu\n" "LittleFS_total=%zu\n", heap_caps_get_free_size(MALLOC_CAP_DEFAULT), heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT), heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT), LittleFS.usedBytes(), LittleFS.totalBytes() ); if (ret < 0 || (size_t)ret >= size) { MO_DBG_ERR("snprintf: %i", ret); return 0; } return (size_t)ret; } return 0; }, [] () { g_diagsSent = false; }, filesystem); return diagService; } #elif MO_ENABLE_MBEDTLS std::unique_ptr MicroOcpp::makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem) { std::unique_ptr diagService = std::unique_ptr(new DiagnosticsService(context)); diagService->setDiagnosticsReader(nullptr, nullptr, filesystem); //report the built-in MO defaults return diagService; } #endif //MO_PLATFORM #endif //!defined(MO_CUSTOM_DIAGNOSTICS) ================================================ FILE: src/MicroOcpp/Model/Diagnostics/DiagnosticsService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef DIAGNOSTICSSERVICE_H #define DIAGNOSTICSSERVICE_H #include #include #include #include #include #include namespace MicroOcpp { enum class UploadStatus { NotUploaded, Uploaded, UploadFailed }; class Context; class Request; class FilesystemAdapter; class DiagnosticsService : public MemoryManaged { private: Context& context; String location; unsigned int retries = 0; unsigned int retryInterval = 0; Timestamp startTime; Timestamp stopTime; Timestamp nextTry; std::function refreshFilename; std::function onUpload; std::function uploadStatusInput; bool uploadIssued = false; bool uploadFailure = false; std::unique_ptr ftpUpload; UploadStatus ftpUploadStatus = UploadStatus::NotUploaded; const char *ftpServerCert = nullptr; char *diagPreamble = nullptr; size_t diagPreambleLen = 0; size_t diagPreambleTransferred = 0; bool diagReaderHasData = false; char *diagPostamble = nullptr; size_t diagPostambleLen = 0; size_t diagPostambleTransferred = 0; Vector diagFileList; size_t diagFilesBackTransferred = 0; std::unique_ptr getDiagnosticsStatusNotification(); Ocpp16::DiagnosticsStatus lastReportedStatus = Ocpp16::DiagnosticsStatus::Idle; public: DiagnosticsService(Context& context); ~DiagnosticsService(); void loop(); //timestamps before year 2021 will be treated as "undefined" //returns empty std::string if onUpload is missing or upload cannot be scheduled for another reason //returns fileName of diagnostics file to be uploaded if upload has been scheduled String requestDiagnosticsUpload(const char *location, unsigned int retries = 1, unsigned int retryInterval = 0, Timestamp startTime = Timestamp(), Timestamp stopTime = Timestamp()); Ocpp16::DiagnosticsStatus getDiagnosticsStatus(); void setRefreshFilename(std::function refreshFn); //refresh a new filename which will be used for the subsequent upload tries /* * Sets the diagnostics data reader. When the server sends a GetDiagnostics operation, then MO will open an FTP * connection to the FTP server and upload a diagnostics file. MO automatically creates a small report about * the OCPP-related status data + it uploads the contents of the OCPP directory. In addition to the automatic * report, MO also sends all data provided by the custom diagnosticsReader. Use the diagnosticsReader to add * all data which could be helpful for troubleshooting, i.e. * - internal status variables, or state machine states * - error trip counters * - current sensor readings and all GPIO values * - Heap statistics, flash memory statistics * - and more. The more the better * * MO calls the diagnosticsReader output buffer `buf` and the bufsize `size`. Write at most `size` bytes and * return the number of bytes actually written (without terminating zero-byte). It's not necessary to append * a terminating zero, MO will ignore any data after the string. To end the reading process, return 0. * * Note that this function only works if MO_ENABLE_MBEDTLS=1, or MO has been configured with a custom FTP client */ void setDiagnosticsReader(std::function diagnosticsReader, std::function onClose, std::shared_ptr filesystem); void setFtpServerCert(const char *cert); //zero-copy mode, i.e. cert must outlive MO void setOnUpload(std::function onUpload); void setOnUploadStatusInput(std::function uploadStatusInput); }; } //end namespace MicroOcpp #if !defined(MO_CUSTOM_DIAGNOSTICS) #if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS namespace MicroOcpp { std::unique_ptr makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem); } #elif MO_ENABLE_MBEDTLS namespace MicroOcpp { std::unique_ptr makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem); } #endif //MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS #endif //!defined(MO_CUSTOM_DIAGNOSTICS) #endif ================================================ FILE: src/MicroOcpp/Model/Diagnostics/DiagnosticsStatus.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_DIAGNOSTICS_STATUS #define MO_DIAGNOSTICS_STATUS namespace MicroOcpp { namespace Ocpp16 { enum class DiagnosticsStatus { Idle, Uploaded, UploadFailed, Uploading }; } } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include #include #include //debug option: update immediately and don't wait for the retreive date #ifndef MO_IGNORE_FW_RETR_DATE #define MO_IGNORE_FW_RETR_DATE 0 #endif using MicroOcpp::FirmwareService; using MicroOcpp::Ocpp16::FirmwareStatus; using MicroOcpp::Request; FirmwareService::FirmwareService(Context& context) : MemoryManaged("v16.Firmware.FirmwareService"), context(context), buildNumber(makeString(getMemoryTag())), location(makeString(getMemoryTag())) { context.getOperationRegistry().registerOperation("UpdateFirmware", [this] () { return new Ocpp16::UpdateFirmware(*this);}); //Register message handler for TriggerMessage operation context.getOperationRegistry().registerOperation("FirmwareStatusNotification", [this] () { return new Ocpp16::FirmwareStatusNotification(getFirmwareStatus());}); } void FirmwareService::setBuildNumber(const char *buildNumber) { if (buildNumber == nullptr) return; this->buildNumber = buildNumber; previousBuildNumberString = declareConfiguration("BUILD_NUMBER", this->buildNumber.c_str(), MO_KEYVALUE_FN, false, false, false); checkedSuccessfulFwUpdate = false; //--> CS will be notified } void FirmwareService::loop() { if (ftpDownload && ftpDownload->isActive()) { ftpDownload->loop(); } if (ftpDownload) { if (ftpDownload->isActive()) { ftpDownload->loop(); } else { MO_DBG_DEBUG("Deinit FTP download"); ftpDownload.reset(); } } auto notification = getFirmwareStatusNotification(); if (notification) { context.initiateRequest(std::move(notification)); } if (mocpp_tick_ms() - timestampTransition < delayTransition) { return; } auto& timestampNow = context.getModel().getClock().now(); if (retries > 0 && timestampNow >= retreiveDate) { if (stage == UpdateStage::Idle) { MO_DBG_INFO("Start update"); if (context.getModel().getNumConnectors() > 0) { auto cp = context.getModel().getConnector(0); cp->setAvailabilityVolatile(false); } if (onDownload == nullptr) { stage = UpdateStage::AfterDownload; } else { downloadIssued = true; stage = UpdateStage::AwaitDownload; timestampTransition = mocpp_tick_ms(); delayTransition = 2000; //delay between state "Downloading" and actually starting the download return; } } if (stage == UpdateStage::AwaitDownload) { MO_DBG_INFO("Start download"); stage = UpdateStage::Downloading; if (onDownload != nullptr) { onDownload(location.c_str()); timestampTransition = mocpp_tick_ms(); delayTransition = downloadStatusInput ? 1000 : 30000; //give the download at least 30s return; } } if (stage == UpdateStage::Downloading) { if (downloadStatusInput) { //check if client reports download to be finished if (downloadStatusInput() == DownloadStatus::Downloaded) { //passed download stage stage = UpdateStage::AfterDownload; } else if (downloadStatusInput() == DownloadStatus::DownloadFailed) { MO_DBG_INFO("Download timeout or failed"); retreiveDate = timestampNow; retreiveDate += retryInterval; retries--; resetStage(); timestampTransition = mocpp_tick_ms(); delayTransition = 10000; } return; } else { //if client doesn't report download state, assume download to be finished (at least 30s download time have passed until here) stage = UpdateStage::AfterDownload; } } if (stage == UpdateStage::AfterDownload) { bool ongoingTx = false; for (unsigned int cId = 0; cId < context.getModel().getNumConnectors(); cId++) { auto connector = context.getModel().getConnector(cId); if (connector && connector->getTransaction() && connector->getTransaction()->isRunning()) { ongoingTx = true; break; } } if (!ongoingTx) { if (onInstall == nullptr) { stage = UpdateStage::Installing; } else { stage = UpdateStage::AwaitInstallation; } timestampTransition = mocpp_tick_ms(); delayTransition = 2000; installationIssued = true; } return; } if (stage == UpdateStage::AwaitInstallation) { MO_DBG_INFO("Installing"); stage = UpdateStage::Installing; if (onInstall) { onInstall(location.c_str()); //may restart the device on success timestampTransition = mocpp_tick_ms(); delayTransition = installationStatusInput ? 1000 : 120 * 1000; } return; } if (stage == UpdateStage::Installing) { if (installationStatusInput) { if (installationStatusInput() == InstallationStatus::Installed) { MO_DBG_INFO("FW update finished"); //Charger may reboot during onInstall. If it doesn't, server will send Reset request resetStage(); retries = 0; //End of update routine stage = UpdateStage::Installed; location.clear(); } else if (installationStatusInput() == InstallationStatus::InstallationFailed) { MO_DBG_INFO("Installation timeout or failed! Retry"); retreiveDate = timestampNow; retreiveDate += retryInterval; retries--; resetStage(); timestampTransition = mocpp_tick_ms(); delayTransition = 10000; } return; } else { MO_DBG_INFO("FW update finished"); //Charger may reboot during onInstall. If it doesn't, server will send Reset request resetStage(); stage = UpdateStage::Installed; retries = 0; //End of update routine location.clear(); return; } } //should never reach this code MO_DBG_ERR("Firmware update failed"); retries = 0; resetStage(); stage = UpdateStage::InternalError; location.clear(); } } void FirmwareService::scheduleFirmwareUpdate(const char *location, Timestamp retreiveDate, unsigned int retries, unsigned int retryInterval) { if (!onDownload && !onInstall) { MO_DBG_ERR("FW service not configured"); stage = UpdateStage::InternalError; //will send "InstallationFailed" and not proceed with update return; } this->location = location; this->retreiveDate = retreiveDate; this->retries = retries; this->retryInterval = retryInterval; if (MO_IGNORE_FW_RETR_DATE) { MO_DBG_DEBUG("ignore FW update retreive date"); this->retreiveDate = context.getModel().getClock().now(); } char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; this->retreiveDate.toJsonString(dbuf, JSONDATE_LENGTH + 1); MO_DBG_INFO("Scheduled FW update!\n" \ " location = %s\n" \ " retrieveDate = %s\n" \ " retries = %u" \ ", retryInterval = %u", this->location.c_str(), dbuf, this->retries, this->retryInterval); timestampTransition = mocpp_tick_ms(); delayTransition = 1000; resetStage(); } FirmwareStatus FirmwareService::getFirmwareStatus() { if (stage == UpdateStage::Installed) { return FirmwareStatus::Installed; } else if (stage == UpdateStage::InternalError) { return FirmwareStatus::InstallationFailed; } if (installationIssued) { if (installationStatusInput != nullptr) { if (installationStatusInput() == InstallationStatus::Installed) { return FirmwareStatus::Installed; } else if (installationStatusInput() == InstallationStatus::InstallationFailed) { return FirmwareStatus::InstallationFailed; } } if (onInstall != nullptr) return FirmwareStatus::Installing; } if (downloadIssued) { if (downloadStatusInput != nullptr) { if (downloadStatusInput() == DownloadStatus::Downloaded) { return FirmwareStatus::Downloaded; } else if (downloadStatusInput() == DownloadStatus::DownloadFailed) { return FirmwareStatus::DownloadFailed; } } if (onDownload != nullptr) return FirmwareStatus::Downloading; } return FirmwareStatus::Idle; } std::unique_ptr FirmwareService::getFirmwareStatusNotification() { /* * Check if FW has been updated previously, but only once */ if (!checkedSuccessfulFwUpdate && !buildNumber.empty() && previousBuildNumberString != nullptr) { checkedSuccessfulFwUpdate = true; MO_DBG_DEBUG("Previous build number: %s, new build number: %s", previousBuildNumberString->getString(), buildNumber.c_str()); if (buildNumber.compare(previousBuildNumberString->getString())) { //new FW previousBuildNumberString->setString(buildNumber.c_str()); configuration_save(); buildNumber.clear(); lastReportedStatus = FirmwareStatus::Installed; auto fwNotificationMsg = new Ocpp16::FirmwareStatusNotification(lastReportedStatus); auto fwNotification = makeRequest(fwNotificationMsg); return fwNotification; } } if (getFirmwareStatus() != lastReportedStatus) { lastReportedStatus = getFirmwareStatus(); if (lastReportedStatus != FirmwareStatus::Idle) { auto fwNotificationMsg = new Ocpp16::FirmwareStatusNotification(lastReportedStatus); auto fwNotification = makeRequest(fwNotificationMsg); return fwNotification; } } return nullptr; } void FirmwareService::setOnDownload(std::function onDownload) { this->onDownload = onDownload; } void FirmwareService::setDownloadStatusInput(std::function downloadStatusInput) { this->downloadStatusInput = downloadStatusInput; } void FirmwareService::setOnInstall(std::function onInstall) { this->onInstall = onInstall; } void FirmwareService::setInstallationStatusInput(std::function installationStatusInput) { this->installationStatusInput = installationStatusInput; } void FirmwareService::resetStage() { stage = UpdateStage::Idle; downloadIssued = false; installationIssued = false; } void FirmwareService::setDownloadFileWriter(std::function firmwareWriter, std::function onClose) { this->onDownload = [this, firmwareWriter, onClose] (const char *location) -> bool { auto ftpClient = context.getFtpClient(); if (!ftpClient) { MO_DBG_ERR("FTP client not set"); this->ftpDownloadStatus = DownloadStatus::DownloadFailed; return false; } this->ftpDownload = ftpClient->getFile(location, firmwareWriter, [this, onClose] (MO_FtpCloseReason reason) -> void { if (reason == MO_FtpCloseReason_Success) { MO_DBG_INFO("FTP download success"); this->ftpDownloadStatus = DownloadStatus::Downloaded; } else { MO_DBG_INFO("FTP download failure (%i)", reason); this->ftpDownloadStatus = DownloadStatus::DownloadFailed; } onClose(reason); }); if (this->ftpDownload) { this->ftpDownloadStatus = DownloadStatus::NotDownloaded; return true; } else { this->ftpDownloadStatus = DownloadStatus::DownloadFailed; return false; } }; this->downloadStatusInput = [this] () { return this->ftpDownloadStatus; }; } void FirmwareService::setFtpServerCert(const char *cert) { this->ftpServerCert = cert; } #if !defined(MO_CUSTOM_UPDATER) #if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS #include std::unique_ptr MicroOcpp::makeDefaultFirmwareService(Context& context) { std::unique_ptr fwService = std::unique_ptr(new FirmwareService(context)); auto ftServicePtr = fwService.get(); fwService->setDownloadFileWriter( [ftServicePtr] (const unsigned char *data, size_t size) -> size_t { if (!Update.isRunning()) { MO_DBG_DEBUG("start writing FW"); MO_DBG_WARN("Built-in updater for ESP32 is only intended for demonstration purposes"); ftServicePtr->setInstallationStatusInput([](){return InstallationStatus::NotInstalled;}); auto ret = Update.begin(); if (!ret) { MO_DBG_ERR("cannot start update: %i", ret); return 0; } } size_t written = Update.write((uint8_t*) data, size); #if MO_DBG_LEVEL >= MO_DL_INFO { size_t progress = Update.progress(); bool printProgress = false; if (progress <= 10000) { size_t p1k = progress / 1000; printProgress = progress < p1k * 1000 + written && progress >= p1k * 1000; } else if (progress <= 100000) { size_t p10k = progress / 10000; printProgress = progress < p10k * 10000 + written && progress >= p10k * 10000; } else { size_t p100k = progress / 100000; printProgress = progress < p100k * 100000 + written && progress >= p100k * 100000; } if (printProgress) { MO_DBG_INFO("update progress: %zu kB", progress / 1000); } } #endif //MO_DBG_LEVEL >= MO_DL_DEBUG return written; }, [] (MO_FtpCloseReason reason) { if (reason != MO_FtpCloseReason_Success) { Update.abort(); } }); fwService->setOnInstall([ftServicePtr] (const char *location) { if (Update.isRunning() && Update.end(true)) { MO_DBG_DEBUG("update success"); ftServicePtr->setInstallationStatusInput([](){return InstallationStatus::Installed;}); ESP.restart(); } else { MO_DBG_ERR("update failed"); ftServicePtr->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); } return true; }); fwService->setInstallationStatusInput([] () { return InstallationStatus::NotInstalled; }); return fwService; } #elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP8266) #include std::unique_ptr MicroOcpp::makeDefaultFirmwareService(Context& context) { std::unique_ptr fwService = std::unique_ptr(new FirmwareService(context)); auto fwServicePtr = fwService.get(); fwService->setOnInstall([fwServicePtr] (const char *location) { MO_DBG_WARN("Built-in updater for ESP8266 is only intended for demonstration purposes. HTTP support only"); WiFiClient client; //WiFiClientSecure client; //client.setCACert(rootCACertificate); client.setTimeout(60); //in seconds //ESPhttpUpdate.setLedPin(downloadStatusLedPin); HTTPUpdateResult ret = ESPhttpUpdate.update(client, location); switch (ret) { case HTTP_UPDATE_FAILED: fwServicePtr->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); MO_DBG_WARN("HTTP_UPDATE_FAILED Error (%d): %s\n", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); break; case HTTP_UPDATE_NO_UPDATES: fwServicePtr->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); MO_DBG_WARN("HTTP_UPDATE_NO_UPDATES"); break; case HTTP_UPDATE_OK: fwServicePtr->setInstallationStatusInput([](){return InstallationStatus::Installed;}); MO_DBG_INFO("HTTP_UPDATE_OK"); ESP.restart(); break; } return true; }); fwService->setInstallationStatusInput([] () { return InstallationStatus::NotInstalled; }); return fwService; } #endif //MO_PLATFORM #endif //!defined(MO_CUSTOM_UPDATER) ================================================ FILE: src/MicroOcpp/Model/FirmwareManagement/FirmwareService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef FIRMWARESERVICE_H #define FIRMWARESERVICE_H #include #include #include #include #include #include #include namespace MicroOcpp { enum class DownloadStatus { NotDownloaded, // == before download or during download Downloaded, DownloadFailed }; enum class InstallationStatus { NotInstalled, // == before installation or during installation Installed, InstallationFailed }; class Context; class Request; class FirmwareService : public MemoryManaged { private: Context& context; std::shared_ptr previousBuildNumberString; String buildNumber; std::function downloadStatusInput; bool downloadIssued = false; std::unique_ptr ftpDownload; DownloadStatus ftpDownloadStatus = DownloadStatus::NotDownloaded; const char *ftpServerCert = nullptr; std::function installationStatusInput; bool installationIssued = false; Ocpp16::FirmwareStatus lastReportedStatus = Ocpp16::FirmwareStatus::Idle; bool checkedSuccessfulFwUpdate = false; String location; Timestamp retreiveDate; unsigned int retries = 0; unsigned int retryInterval = 0; std::function onDownload; std::function onInstall; unsigned long delayTransition = 0; unsigned long timestampTransition = 0; enum class UpdateStage { Idle, AwaitDownload, Downloading, AfterDownload, AwaitInstallation, Installing, Installed, InternalError } stage = UpdateStage::Idle; void resetStage(); std::unique_ptr getFirmwareStatusNotification(); public: FirmwareService(Context& context); void setBuildNumber(const char *buildNumber); void loop(); void scheduleFirmwareUpdate(const char *location, Timestamp retreiveDate, unsigned int retries = 1, unsigned int retryInterval = 0); Ocpp16::FirmwareStatus getFirmwareStatus(); /* * Sets the firmware writer. During the UpdateFirmware process, MO will use an FTP client to download the firmware and forward * the binary data to `firmwareWriter`. The binary data comes in chunks. MO executes `firmwareWriter` with `buf` containing the * next chunk of FW data and `size` being the chunk size. `firmwareWriter` must return the number of bytes written, whereas * the result can be between 1 and `size`, and 0 aborts the download. MO executes `onClose` with the reason why the connection * has been closed. If the download hasn't been successful, MO will abort the update routine in any case. * * Note that this function only works if MO_ENABLE_MBEDTLS=1, or MO has been configured with a custom FTP client */ void setDownloadFileWriter(std::function firmwareWriter, std::function onClose); void setFtpServerCert(const char *cert); //zero-copy mode, i.e. cert must outlive MO /* * Manual alternative for FTP download handler `setDownloadFileWriter` */ void setOnDownload(std::function onDownload); void setDownloadStatusInput(std::function downloadStatusInput); void setOnInstall(std::function onInstall); void setInstallationStatusInput(std::function installationStatusInput); }; } //endif namespace MicroOcpp #if !defined(MO_CUSTOM_UPDATER) #if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS namespace MicroOcpp { std::unique_ptr makeDefaultFirmwareService(Context& context); } #elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP8266) namespace MicroOcpp { std::unique_ptr makeDefaultFirmwareService(Context& context); } #endif //MO_PLATFORM #endif //!defined(MO_CUSTOM_UPDATER) #endif ================================================ FILE: src/MicroOcpp/Model/FirmwareManagement/FirmwareStatus.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_FIRMWARE_STATUS #define MO_FIRMWARE_STATUS namespace MicroOcpp { namespace Ocpp16 { enum class FirmwareStatus { Downloaded, DownloadFailed, Downloading, Idle, InstallationFailed, Installing, Installed }; } } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include using namespace MicroOcpp; HeartbeatService::HeartbeatService(Context& context) : MemoryManaged("v16.Heartbeat.HeartbeatService"), context(context) { heartbeatIntervalInt = declareConfiguration("HeartbeatInterval", 86400); registerConfigurationValidator("HeartbeatInterval", VALIDATE_UNSIGNED_INT); lastHeartbeat = mocpp_tick_ms(); //Register message handler for TriggerMessage operation context.getOperationRegistry().registerOperation("Heartbeat", [&context] () { return new Ocpp16::Heartbeat(context.getModel());}); } void HeartbeatService::loop() { unsigned long hbInterval = heartbeatIntervalInt->getInt(); hbInterval *= 1000UL; //conversion s -> ms unsigned long now = mocpp_tick_ms(); if (now - lastHeartbeat >= hbInterval) { lastHeartbeat = now; auto heartbeat = makeRequest(new Ocpp16::Heartbeat(context.getModel())); // Heartbeats can not deviate more than 4s from the configured interval heartbeat->setTimeout(std::min(4000UL, hbInterval)); context.initiateRequest(std::move(heartbeat)); } } ================================================ FILE: src/MicroOcpp/Model/Heartbeat/HeartbeatService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_HEARTBEATSERVICE_H #define MO_HEARTBEATSERVICE_H #include #include #include namespace MicroOcpp { class Context; class HeartbeatService : public MemoryManaged { private: Context& context; unsigned long lastHeartbeat; std::shared_ptr heartbeatIntervalInt; public: HeartbeatService(Context& context); void loop(); }; } #endif ================================================ FILE: src/MicroOcpp/Model/Metering/MeterStore.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #ifndef MO_MAX_STOPTXDATA_LEN #define MO_MAX_STOPTXDATA_LEN 4 #endif using namespace MicroOcpp; TransactionMeterData::TransactionMeterData(unsigned int connectorId, unsigned int txNr, std::shared_ptr filesystem) : MemoryManaged("v16.Metering.TransactionMeterData"), connectorId(connectorId), txNr(txNr), filesystem{filesystem}, txData{makeVector>(getMemoryTag())} { if (!filesystem) { MO_DBG_DEBUG("volatile mode"); } } bool TransactionMeterData::addTxData(std::unique_ptr mv) { if (isFinalized()) { MO_DBG_ERR("immutable"); return false; } if (!mv) { MO_DBG_ERR("null"); return false; } if (MO_MAX_STOPTXDATA_LEN <= 0) { //txData off return true; } bool replaceLast = mvCount >= MO_MAX_STOPTXDATA_LEN; //txData size exceeded? overwrite last entry instead of appending if (filesystem) { unsigned int mvIndex = 0; if (replaceLast) { mvIndex = mvCount - 1; } else { mvIndex = mvCount ; } char fn [MO_MAX_PATH_SIZE] = {'\0'}; auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "sd" "-%u-%u-%u.jsn", connectorId, txNr, mvIndex); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return false; } auto mvDoc = mv->toJson(); if (!mvDoc) { MO_DBG_ERR("MV not ready yet"); return false; } if (!FilesystemUtils::storeJson(filesystem, fn, *mvDoc)) { MO_DBG_ERR("FS error"); return false; } if (!replaceLast) { mvCount++; } } if (replaceLast) { txData.back() = std::move(mv); MO_DBG_DEBUG("updated latest sd"); } else { txData.push_back(std::move(mv)); MO_DBG_DEBUG("added sd"); } return true; } Vector> TransactionMeterData::retrieveStopTxData() { if (isFinalized()) { MO_DBG_ERR("Can only retrieve once"); return makeVector>(getMemoryTag()); } finalize(); MO_DBG_DEBUG("creating sd"); return std::move(txData); } bool TransactionMeterData::restore(MeterValueBuilder& mvBuilder) { if (!filesystem) { MO_DBG_DEBUG("No FS - nothing to restore"); return true; } const unsigned int MISSES_LIMIT = 3; unsigned int i = 0; unsigned int misses = 0; while (misses < MISSES_LIMIT) { //search until region without mvs found char fn [MO_MAX_PATH_SIZE] = {'\0'}; auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "sd" "-%u-%u-%u.jsn", connectorId, txNr, i); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return false; //all files have same length } auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); if (!doc) { misses++; i++; continue; } JsonObject mvJson = doc->as(); std::unique_ptr mv = mvBuilder.deserializeSample(mvJson); if (!mv) { MO_DBG_ERR("Deserialization error"); misses++; i++; continue; } if (txData.size() >= MO_MAX_STOPTXDATA_LEN) { MO_DBG_ERR("corrupted memory"); return false; } txData.push_back(std::move(mv)); i++; mvCount = i; misses = 0; } MO_DBG_DEBUG("Restored %zu meter values from sd-%u-%u-0 to %u (exclusive)", txData.size(), connectorId, txNr, mvCount); return true; } MeterStore::MeterStore(std::shared_ptr filesystem) : MemoryManaged("v16.Metering.MeterStore"), filesystem {filesystem}, txMeterData{makeVector>(getMemoryTag())} { if (!filesystem) { MO_DBG_DEBUG("volatile mode"); } } std::shared_ptr MeterStore::getTxMeterData(MeterValueBuilder& mvBuilder, Transaction *transaction) { if (!transaction || transaction->isSilent()) { //no tx assignment -> don't store txData //tx is silent -> no StopTx will be sent and don't store txData return nullptr; } auto connectorId = transaction->getConnectorId(); auto txNr = transaction->getTxNr(); auto cached = std::find_if(txMeterData.begin(), txMeterData.end(), [connectorId, txNr] (std::weak_ptr& txm) { if (auto txml = txm.lock()) { return txml->getConnectorId() == connectorId && txml->getTxNr() == txNr; } else { return false; } }); if (cached != txMeterData.end()) { if (auto cachedl = cached->lock()) { return cachedl; } } //clean outdated pointers before creating new object txMeterData.erase(std::remove_if(txMeterData.begin(), txMeterData.end(), [] (std::weak_ptr& txm) { return txm.expired(); }), txMeterData.end()); //create new object and cache weak pointer auto tx = std::allocate_shared(makeAllocator(getMemoryTag()), connectorId, txNr, filesystem); if (filesystem) { char fn [MO_MAX_PATH_SIZE] = {'\0'}; auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "sd" "-%u-%u-%u.jsn", connectorId, txNr, 0); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return nullptr; //cannot store } size_t size = 0; bool exists = filesystem->stat(fn, &size) == 0; if (exists) { if (!tx->restore(mvBuilder)) { remove(connectorId, txNr); MO_DBG_ERR("removed corrupted tx entries"); } } } txMeterData.push_back(tx); MO_DBG_DEBUG("Added txNr %u, now holding %zu sds", txNr, txMeterData.size()); return tx; } bool MeterStore::remove(unsigned int connectorId, unsigned int txNr) { unsigned int mvCount = 0; auto cached = std::find_if(txMeterData.begin(), txMeterData.end(), [connectorId, txNr] (std::weak_ptr& txm) { if (auto txml = txm.lock()) { return txml->getConnectorId() == connectorId && txml->getTxNr() == txNr; } else { return false; } }); if (cached != txMeterData.end()) { if (auto cachedl = cached->lock()) { mvCount = cachedl->getPathsCount(); cachedl->finalize(); } } bool success = true; if (filesystem) { if (mvCount == 0) { const unsigned int MISSES_LIMIT = 3; unsigned int misses = 0; unsigned int i = 0; while (misses < MISSES_LIMIT) { //search until region without mvs found char fn [MO_MAX_PATH_SIZE] = {'\0'}; auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "sd" "-%u-%u-%u.jsn", connectorId, txNr, i); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return false; //all files have same length } size_t nsize = 0; if (filesystem->stat(fn, &nsize) != 0) { misses++; i++; continue; } i++; mvCount = i; misses = 0; } } MO_DBG_DEBUG("remove %u mvs for txNr %u", mvCount, txNr); for (unsigned int i = 0; i < mvCount; i++) { unsigned int sd = mvCount - 1U - i; char fn [MO_MAX_PATH_SIZE] = {'\0'}; auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "sd" "-%u-%u-%u.jsn", connectorId, txNr, sd); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return false; } success &= filesystem->remove(fn); } } //clean outdated pointers txMeterData.erase(std::remove_if(txMeterData.begin(), txMeterData.end(), [] (std::weak_ptr& txm) { return txm.expired(); }), txMeterData.end()); if (success) { MO_DBG_DEBUG("Removed meter values for cId %u, txNr %u", connectorId, txNr); } else { MO_DBG_DEBUG("corrupted fs"); } return success; } ================================================ FILE: src/MicroOcpp/Model/Metering/MeterStore.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_METERSTORE_H #define MO_METERSTORE_H #include #include #include #include namespace MicroOcpp { class TransactionMeterData : public MemoryManaged { private: const unsigned int connectorId; //assignment to Transaction object const unsigned int txNr; //assignment to Transaction object unsigned int mvCount = 0; //nr of saved meter values, including gaps bool finalized = false; //if true, this is read-only std::shared_ptr filesystem; Vector> txData; public: TransactionMeterData(unsigned int connectorId, unsigned int txNr, std::shared_ptr filesystem); bool addTxData(std::unique_ptr mv); Vector> retrieveStopTxData(); //will invalidate internal cache bool restore(MeterValueBuilder& mvBuilder); //load record from memory; true if record found, false if nothing loaded unsigned int getConnectorId() {return connectorId;} unsigned int getTxNr() {return txNr;} unsigned int getPathsCount() {return mvCount;} //size of spanned path indexes void finalize() {finalized = true;} bool isFinalized() {return finalized;} }; class MeterStore : public MemoryManaged { private: std::shared_ptr filesystem; Vector> txMeterData; public: MeterStore() = delete; MeterStore(MeterStore&) = delete; MeterStore(std::shared_ptr filesystem); std::shared_ptr getTxMeterData(MeterValueBuilder& mvBuilder, Transaction *transaction); bool remove(unsigned int connectorId, unsigned int txNr); }; } #endif ================================================ FILE: src/MicroOcpp/Model/Metering/MeterValue.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using namespace MicroOcpp; MeterValue::MeterValue(const Timestamp& timestamp) : MemoryManaged("v16.Metering.MeterValue"), timestamp(timestamp), sampledValue(makeVector>(getMemoryTag())) { } void MeterValue::addSampledValue(std::unique_ptr sample) { sampledValue.push_back(std::move(sample)); } std::unique_ptr MeterValue::toJson() { size_t capacity = 0; auto entries = makeVector>(getMemoryTag()); for (auto sample = sampledValue.begin(); sample != sampledValue.end(); sample++) { auto json = (*sample)->toJson(); if (!json) { return nullptr; } capacity += json->capacity(); entries.push_back(std::move(json)); } capacity += JSON_ARRAY_SIZE(entries.size()); capacity += JSONDATE_LENGTH + 1; capacity += JSON_OBJECT_SIZE(2); auto result = makeJsonDoc(getMemoryTag(), capacity); auto jsonPayload = result->to(); char timestampStr [JSONDATE_LENGTH + 1] = {'\0'}; if (timestamp.toJsonString(timestampStr, JSONDATE_LENGTH + 1)) { jsonPayload["timestamp"] = timestampStr; } auto jsonMeterValue = jsonPayload.createNestedArray("sampledValue"); for (auto entry = entries.begin(); entry != entries.end(); entry++) { jsonMeterValue.add(**entry); } return result; } const Timestamp& MeterValue::getTimestamp() { return timestamp; } void MeterValue::setTimestamp(Timestamp timestamp) { this->timestamp = timestamp; } ReadingContext MeterValue::getReadingContext() { //all sampledValues have the same ReadingContext. Just get the first result for (auto sample = sampledValue.begin(); sample != sampledValue.end(); sample++) { if ((*sample)->getReadingContext() != ReadingContext_UNDEFINED) { return (*sample)->getReadingContext(); } } return ReadingContext_UNDEFINED; } void MeterValue::setTxNr(unsigned int txNr) { if (txNr > (unsigned int)std::numeric_limits::max()) { MO_DBG_ERR("invalid arg"); return; } this->txNr = (int)txNr; } int MeterValue::getTxNr() { return txNr; } void MeterValue::setOpNr(unsigned int opNr) { this->opNr = opNr; } unsigned int MeterValue::getOpNr() { return opNr; } void MeterValue::advanceAttemptNr() { attemptNr++; } unsigned int MeterValue::getAttemptNr() { return attemptNr; } unsigned long MeterValue::getAttemptTime() { return attemptTime; } void MeterValue::setAttemptTime(unsigned long timestamp) { this->attemptTime = timestamp; } MeterValueBuilder::MeterValueBuilder(const Vector> &samplers, std::shared_ptr samplersSelectStr) : MemoryManaged("v16.Metering.MeterValueBuilder"), samplers(samplers), selectString(samplersSelectStr), select_mask(makeVector(getMemoryTag())) { updateObservedSamplers(); select_observe = selectString->getValueRevision(); } void MeterValueBuilder::updateObservedSamplers() { if (select_mask.size() != samplers.size()) { select_mask.resize(samplers.size(), false); select_n = 0; } for (size_t i = 0; i < select_mask.size(); i++) { select_mask[i] = false; } auto sstring = selectString->getString(); auto ssize = strlen(sstring) + 1; size_t sl = 0, sr = 0; while (sstring && sl < ssize) { while (sr < ssize) { if (sstring[sr] == ',') { break; } sr++; } if (sr != sl + 1) { for (size_t i = 0; i < samplers.size(); i++) { if (!strncmp(samplers[i]->getProperties().getMeasurand(), sstring + sl, sr - sl)) { select_mask[i] = true; select_n++; } } } sr++; sl = sr; } } std::unique_ptr MeterValueBuilder::takeSample(const Timestamp& timestamp, const ReadingContext& context) { if (select_observe != selectString->getValueRevision() || //OCPP server has changed configuration about which measurands to take samplers.size() != select_mask.size()) { //Client has added another Measurand; synchronize lists MO_DBG_DEBUG("Updating observed samplers due to config change or samplers added"); updateObservedSamplers(); select_observe = selectString->getValueRevision(); } if (select_n == 0) { return nullptr; } auto sample = std::unique_ptr(new MeterValue(timestamp)); for (size_t i = 0; i < select_mask.size(); i++) { if (select_mask[i]) { sample->addSampledValue(samplers[i]->takeValue(context)); } } return sample; } std::unique_ptr MeterValueBuilder::deserializeSample(const JsonObject mvJson) { Timestamp timestamp; bool ret = timestamp.setTime(mvJson["timestamp"] | "Invalid"); if (!ret) { MO_DBG_ERR("invalid timestamp"); return nullptr; } auto sample = std::unique_ptr(new MeterValue(timestamp)); JsonArray sampledValue = mvJson["sampledValue"]; for (JsonObject svJson : sampledValue) { //for each sampled value, search sampler with matching measurand type for (auto& sampler : samplers) { auto& properties = sampler->getProperties(); if (!strcmp(properties.getMeasurand(), svJson["measurand"] | "") && !strcmp(properties.getFormat(), svJson["format"] | "") && !strcmp(properties.getPhase(), svJson["phase"] | "") && !strcmp(properties.getLocation(), svJson["location"] | "") && !strcmp(properties.getUnit(), svJson["unit"] | "")) { //found correct sampler auto dVal = sampler->deserializeValue(svJson); if (dVal) { sample->addSampledValue(std::move(dVal)); } else { MO_DBG_ERR("deserialization error"); } break; } } } MO_DBG_VERBOSE("deserialized MV"); return sample; } ================================================ FILE: src/MicroOcpp/Model/Metering/MeterValue.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_METERVALUE_H #define MO_METERVALUE_H #include #include #include #include #include #include namespace MicroOcpp { class MeterValue : public MemoryManaged { private: Timestamp timestamp; Vector> sampledValue; int txNr = -1; unsigned int opNr = 1; unsigned int attemptNr = 0; unsigned long attemptTime = 0; public: MeterValue(const Timestamp& timestamp); MeterValue(const MeterValue& other) = delete; void addSampledValue(std::unique_ptr sample); std::unique_ptr toJson(); const Timestamp& getTimestamp(); void setTimestamp(Timestamp timestamp); ReadingContext getReadingContext(); void setTxNr(unsigned int txNr); int getTxNr(); void setOpNr(unsigned int opNr); unsigned int getOpNr(); void advanceAttemptNr(); unsigned int getAttemptNr(); unsigned long getAttemptTime(); void setAttemptTime(unsigned long timestamp); }; class MeterValueBuilder : public MemoryManaged { private: const Vector> &samplers; std::shared_ptr selectString; Vector select_mask; unsigned int select_n {0}; decltype(selectString->getValueRevision()) select_observe; void updateObservedSamplers(); public: MeterValueBuilder(const Vector> &samplers, std::shared_ptr samplersSelectStr); std::unique_ptr takeSample(const Timestamp& timestamp, const ReadingContext& context); std::unique_ptr deserializeSample(const JsonObject mvJson); }; } #endif ================================================ FILE: src/MicroOcpp/Model/Metering/MeterValuesV201.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include #include #include //helper function namespace MicroOcpp { bool csvContains(const char *csv, const char *elem) { if (!csv || !elem) { return false; } size_t elemLen = strlen(elem); size_t sl = 0, sr = 0; while (csv[sr]) { while (csv[sr]) { if (csv[sr] == ',') { break; } sr++; } //csv[sr] is either ',' or '\0' if (sr - sl == elemLen && !strncmp(csv + sl, elem, sr - sl)) { return true; } if (csv[sr]) { sr++; } sl = sr; } return false; } } //namespace MicroOcpp using namespace MicroOcpp::Ocpp201; SampledValueProperties::SampledValueProperties() : MemoryManaged("v201.MeterValues.SampledValueProperties") { } SampledValueProperties::SampledValueProperties(const SampledValueProperties& other) : MemoryManaged(other.getMemoryTag()), format(other.format), measurand(other.measurand), phase(other.phase), location(other.location), unitOfMeasureUnit(other.unitOfMeasureUnit), unitOfMeasureMultiplier(other.unitOfMeasureMultiplier) { } void SampledValueProperties::setFormat(const char *format) {this->format = format;} const char *SampledValueProperties::getFormat() const {return format;} void SampledValueProperties::setMeasurand(const char *measurand) {this->measurand = measurand;} const char *SampledValueProperties::getMeasurand() const {return measurand;} void SampledValueProperties::setPhase(const char *phase) {this->phase = phase;} const char *SampledValueProperties::getPhase() const {return phase;} void SampledValueProperties::setLocation(const char *location) {this->location = location;} const char *SampledValueProperties::getLocation() const {return location;} void SampledValueProperties::setUnitOfMeasureUnit(const char *unitOfMeasureUnit) {this->unitOfMeasureUnit = unitOfMeasureUnit;} const char *SampledValueProperties::getUnitOfMeasureUnit() const {return unitOfMeasureUnit;} void SampledValueProperties::setUnitOfMeasureMultiplier(int unitOfMeasureMultiplier) {this->unitOfMeasureMultiplier = unitOfMeasureMultiplier;} int SampledValueProperties::getUnitOfMeasureMultiplier() const {return unitOfMeasureMultiplier;} SampledValue::SampledValue(double value, ReadingContext readingContext, SampledValueProperties& properties) : MemoryManaged("v201.MeterValues.SampledValue"), value(value), readingContext(readingContext), properties(properties) { } bool SampledValue::toJson(JsonDoc& out) { size_t unitOfMeasureElements = (properties.getUnitOfMeasureUnit() ? 1 : 0) + (properties.getUnitOfMeasureMultiplier() ? 1 : 0); out = initJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE( 1 + //value (readingContext != ReadingContext_SamplePeriodic ? 1 : 0) + (properties.getMeasurand() ? 1 : 0) + (properties.getPhase() ? 1 : 0) + (properties.getLocation() ? 1 : 0) + (unitOfMeasureElements ? 1 : 0) ) + (unitOfMeasureElements ? JSON_OBJECT_SIZE(unitOfMeasureElements) : 0) ); out["value"] = value; if (readingContext != ReadingContext_SamplePeriodic) out["context"] = serializeReadingContext(readingContext); if (properties.getMeasurand()) out["measurand"] = properties.getMeasurand(); if (properties.getPhase()) out["phase"] = properties.getPhase(); if (properties.getLocation()) out["location"] = properties.getLocation(); if (properties.getUnitOfMeasureUnit()) out["unitOfMeasure"]["unit"] = properties.getUnitOfMeasureUnit(); if (properties.getUnitOfMeasureMultiplier()) out["unitOfMeasure"]["multiplier"] = properties.getUnitOfMeasureMultiplier(); return true; } SampledValueInput::SampledValueInput(std::function valueInput, const SampledValueProperties& properties) : MemoryManaged("v201.MeterValues.SampledValueInput"), valueInput(valueInput), properties(properties) { } SampledValue *SampledValueInput::takeSampledValue(ReadingContext readingContext) { return new SampledValue(valueInput(readingContext), readingContext, properties); } const SampledValueProperties& SampledValueInput::getProperties() { return properties; } uint8_t& SampledValueInput::getMeasurandTypeFlags() { return measurandTypeFlags; } MeterValue::MeterValue(const Timestamp& timestamp, SampledValue **sampledValue, size_t sampledValueSize) : MemoryManaged("v201.MeterValues.MeterValue"), timestamp(timestamp), sampledValue(sampledValue), sampledValueSize(sampledValueSize) { } MeterValue::~MeterValue() { for (size_t i = 0; i < sampledValueSize; i++) { delete sampledValue[i]; } MO_FREE(sampledValue); } bool MeterValue::toJson(JsonDoc& out) { size_t capacity = 0; for (size_t i = 0; i < sampledValueSize; i++) { //just measure, discard sampledValueJson afterwards JsonDoc sampledValueJson = initJsonDoc(getMemoryTag()); sampledValue[i]->toJson(sampledValueJson); capacity += sampledValueJson.capacity(); } capacity += JSON_OBJECT_SIZE(2) + JSONDATE_LENGTH + 1 + JSON_ARRAY_SIZE(sampledValueSize); out = initJsonDoc("v201.MeterValues.MeterValue", capacity); char timestampStr [JSONDATE_LENGTH + 1]; timestamp.toJsonString(timestampStr, sizeof(timestampStr)); out["timestamp"] = timestampStr; JsonArray sampledValueArray = out.createNestedArray("sampledValue"); for (size_t i = 0; i < sampledValueSize; i++) { JsonDoc sampledValueJson = initJsonDoc(getMemoryTag()); sampledValue[i]->toJson(sampledValueJson); sampledValueArray.add(sampledValueJson); } return true; } const MicroOcpp::Timestamp& MeterValue::getTimestamp() { return timestamp; } MeteringServiceEvse::MeteringServiceEvse(Model& model, unsigned int evseId) : MemoryManaged("v201.MeterValues.MeteringServiceEvse"), model(model), evseId(evseId), sampledValueInputs(makeVector(getMemoryTag())) { auto varService = model.getVariableService(); sampledDataTxStartedMeasurands = varService->declareVariable("SampledDataCtrlr", "TxStartedMeasurands", ""); sampledDataTxUpdatedMeasurands = varService->declareVariable("SampledDataCtrlr", "TxUpdatedMeasurands", ""); sampledDataTxEndedMeasurands = varService->declareVariable("SampledDataCtrlr", "TxEndedMeasurands", ""); alignedDataMeasurands = varService->declareVariable("AlignedDataCtrlr", "AlignedDataMeasurands", ""); } void MeteringServiceEvse::addMeterValueInput(std::function valueInput, const SampledValueProperties& properties) { sampledValueInputs.emplace_back(valueInput, properties); } std::unique_ptr MeteringServiceEvse::takeMeterValue(Variable *measurands, uint16_t& trackMeasurandsWriteCount, size_t& trackInputsSize, uint8_t measurandsMask, ReadingContext readingContext) { if (measurands->getWriteCount() != trackMeasurandsWriteCount || sampledValueInputs.size() != trackInputsSize) { MO_DBG_DEBUG("Updating observed samplers due to config change or samplers added"); for (size_t i = 0; i < sampledValueInputs.size(); i++) { if (csvContains(measurands->getString(), sampledValueInputs[i].getProperties().getMeasurand())) { sampledValueInputs[i].getMeasurandTypeFlags() |= measurandsMask; } else { sampledValueInputs[i].getMeasurandTypeFlags() &= ~measurandsMask; } } trackMeasurandsWriteCount = measurands->getWriteCount(); trackInputsSize = sampledValueInputs.size(); } size_t samplesSize = 0; for (size_t i = 0; i < sampledValueInputs.size(); i++) { if (sampledValueInputs[i].getMeasurandTypeFlags() & measurandsMask) { samplesSize++; } } if (samplesSize == 0) { return nullptr; } SampledValue **sampledValue = static_cast(MO_MALLOC(getMemoryTag(), samplesSize * sizeof(SampledValue*))); if (!sampledValue) { MO_DBG_ERR("OOM"); return nullptr; } size_t samplesWritten = 0; bool memoryErr = false; for (size_t i = 0; i < sampledValueInputs.size(); i++) { if (sampledValueInputs[i].getMeasurandTypeFlags() & measurandsMask) { auto sample = sampledValueInputs[i].takeSampledValue(readingContext); if (!sample) { MO_DBG_ERR("OOM"); memoryErr = true; break; } sampledValue[samplesWritten++] = sample; } } std::unique_ptr meterValue = std::unique_ptr(new MeterValue(model.getClock().now(), sampledValue, samplesWritten)); if (!meterValue) { MO_DBG_ERR("OOM"); memoryErr = true; } if (memoryErr) { if (!meterValue) { //meterValue did not take ownership, so clean resources manually for (size_t i = 0; i < samplesWritten; i++) { delete sampledValue[i]; } delete sampledValue; } return nullptr; } return meterValue; } std::unique_ptr MeteringServiceEvse::takeTxStartedMeterValue(ReadingContext readingContext) { return takeMeterValue(sampledDataTxStartedMeasurands, trackSampledDataTxStartedMeasurandsWriteCount, trackSampledValueInputsSizeTxStarted, MO_MEASURAND_TYPE_TXSTARTED, readingContext); } std::unique_ptr MeteringServiceEvse::takeTxUpdatedMeterValue(ReadingContext readingContext) { return takeMeterValue(sampledDataTxUpdatedMeasurands, trackSampledDataTxUpdatedMeasurandsWriteCount, trackSampledValueInputsSizeTxUpdated, MO_MEASURAND_TYPE_TXUPDATED, readingContext); } std::unique_ptr MeteringServiceEvse::takeTxEndedMeterValue(ReadingContext readingContext) { return takeMeterValue(sampledDataTxEndedMeasurands, trackSampledDataTxEndedMeasurandsWriteCount, trackSampledValueInputsSizeTxEnded, MO_MEASURAND_TYPE_TXENDED, readingContext); } std::unique_ptr MeteringServiceEvse::takeTriggeredMeterValues() { return takeMeterValue(alignedDataMeasurands, trackAlignedDataMeasurandsWriteCount, trackSampledValueInputsSizeAligned, MO_MEASURAND_TYPE_ALIGNED, ReadingContext_Trigger); } bool MeteringServiceEvse::existsMeasurand(const char *measurand, size_t len) { for (size_t i = 0; i < sampledValueInputs.size(); i++) { const char *sviMeasurand = sampledValueInputs[i].getProperties().getMeasurand(); if (sviMeasurand && len == strlen(sviMeasurand) && !strncmp(sviMeasurand, measurand, len)) { return true; } } return false; } namespace MicroOcpp { namespace Ocpp201 { bool validateSelectString(const char *csl, void *userPtr) { auto mService = static_cast(userPtr); bool isValid = true; const char *l = csl; //the beginning of an entry of the comma-separated list const char *r = l; //one place after the last character of the entry beginning with l while (*l) { if (*l == ',') { l++; continue; } r = l + 1; while (*r != '\0' && *r != ',') { r++; } bool found = false; for (size_t evseId = 0; evseId < MO_NUM_EVSEID && mService->getEvse(evseId); evseId++) { if (mService->getEvse(evseId)->existsMeasurand(l, (size_t) (r - l))) { found = true; break; } } if (!found) { isValid = false; MO_DBG_WARN("could not find metering device for %.*s", (int) (r - l), l); break; } l = r; } return isValid; } } //namespace Ocpp201 } //namespace MicroOcpp using namespace MicroOcpp::Ocpp201; MeteringService::MeteringService(Model& model, size_t numEvses) { auto varService = model.getVariableService(); //define factory defaults varService->declareVariable("SampledDataCtrlr", "TxStartedMeasurands", ""); varService->declareVariable("SampledDataCtrlr", "TxUpdatedMeasurands", ""); varService->declareVariable("SampledDataCtrlr", "TxEndedMeasurands", ""); varService->declareVariable("AlignedDataCtrlr", "AlignedDataMeasurands", ""); varService->registerValidator("SampledDataCtrlr", "TxStartedMeasurands", validateSelectString, this); varService->registerValidator("SampledDataCtrlr", "TxUpdatedMeasurands", validateSelectString, this); varService->registerValidator("SampledDataCtrlr", "TxEndedMeasurands", validateSelectString, this); varService->registerValidator("AlignedDataCtrlr", "AlignedDataMeasurands", validateSelectString, this); for (size_t evseId = 0; evseId < std::min(numEvses, (size_t)MO_NUM_EVSEID); evseId++) { evses[evseId] = new MeteringServiceEvse(model, evseId); } } MeteringService::~MeteringService() { for (size_t evseId = 0; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { delete evses[evseId]; } } MeteringServiceEvse *MeteringService::getEvse(unsigned int evseId) { return evses[evseId]; } #endif ================================================ FILE: src/MicroOcpp/Model/Metering/MeterValuesV201.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License /* * Implementation of the UCs E01 - E12 */ #ifndef MO_METERVALUESV201_H #define MO_METERVALUESV201_H #include #if MO_ENABLE_V201 #include #include #include #include #include namespace MicroOcpp { class Model; class Variable; namespace Ocpp201 { class SampledValueProperties : public MemoryManaged { private: const char *format = nullptr; const char *measurand = nullptr; const char *phase = nullptr; const char *location = nullptr; const char *unitOfMeasureUnit = nullptr; int unitOfMeasureMultiplier = 0; public: SampledValueProperties(); SampledValueProperties(const SampledValueProperties&); void setFormat(const char *format); //zero-copy const char *getFormat() const; void setMeasurand(const char *measurand); //zero-copy const char *getMeasurand() const; void setPhase(const char *phase); //zero-copy const char *getPhase() const; void setLocation(const char *location); //zero-copy const char *getLocation() const; void setUnitOfMeasureUnit(const char *unitOfMeasureUnit); //zero-copy const char *getUnitOfMeasureUnit() const; void setUnitOfMeasureMultiplier(int unitOfMeasureMultiplier); int getUnitOfMeasureMultiplier() const; }; class SampledValue : public MemoryManaged { private: double value = 0.; ReadingContext readingContext; SampledValueProperties& properties; //std::unique_ptr ... this could be an abstract type public: SampledValue(double value, ReadingContext readingContext, SampledValueProperties& properties); bool toJson(JsonDoc& out); }; #define MO_MEASURAND_TYPE_TXSTARTED (1 << 0) #define MO_MEASURAND_TYPE_TXUPDATED (1 << 1) #define MO_MEASURAND_TYPE_TXENDED (1 << 2) #define MO_MEASURAND_TYPE_ALIGNED (1 << 3) class SampledValueInput : public MemoryManaged { private: std::function valueInput; SampledValueProperties properties; uint8_t measurandTypeFlags = 0; public: SampledValueInput(std::function valueInput, const SampledValueProperties& properties); SampledValue *takeSampledValue(ReadingContext readingContext); const SampledValueProperties& getProperties(); uint8_t& getMeasurandTypeFlags(); }; class MeterValue : public MemoryManaged { private: Timestamp timestamp; SampledValue **sampledValue = nullptr; size_t sampledValueSize = 0; public: MeterValue(const Timestamp& timestamp, SampledValue **sampledValue, size_t sampledValueSize); ~MeterValue(); bool toJson(JsonDoc& out); const Timestamp& getTimestamp(); }; class MeteringServiceEvse : public MemoryManaged { private: Model& model; const unsigned int evseId; Vector sampledValueInputs; Variable *sampledDataTxStartedMeasurands = nullptr; Variable *sampledDataTxUpdatedMeasurands = nullptr; Variable *sampledDataTxEndedMeasurands = nullptr; Variable *alignedDataMeasurands = nullptr; size_t trackSampledValueInputsSizeTxStarted = 0; size_t trackSampledValueInputsSizeTxUpdated = 0; size_t trackSampledValueInputsSizeTxEnded = 0; size_t trackSampledValueInputsSizeAligned = 0; uint16_t trackSampledDataTxStartedMeasurandsWriteCount = -1; uint16_t trackSampledDataTxUpdatedMeasurandsWriteCount = -1; uint16_t trackSampledDataTxEndedMeasurandsWriteCount = -1; uint16_t trackAlignedDataMeasurandsWriteCount = -1; std::unique_ptr takeMeterValue(Variable *measurands, uint16_t& trackMeasurandsWriteCount, size_t& trackInputsSize, uint8_t measurandsMask, ReadingContext context); public: MeteringServiceEvse(Model& model, unsigned int evseId); void addMeterValueInput(std::function valueInput, const SampledValueProperties& properties); std::unique_ptr takeTxStartedMeterValue(ReadingContext context = ReadingContext_TransactionBegin); std::unique_ptr takeTxUpdatedMeterValue(ReadingContext context = ReadingContext_SamplePeriodic); std::unique_ptr takeTxEndedMeterValue(ReadingContext context); std::unique_ptr takeTriggeredMeterValues(); bool existsMeasurand(const char *measurand, size_t len); }; class MeteringService : public MemoryManaged { private: MeteringServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; public: MeteringService(Model& model, size_t numEvses); ~MeteringService(); MeteringServiceEvse *getEvse(unsigned int evseId); }; } } #endif #endif ================================================ FILE: src/MicroOcpp/Model/Metering/MeteringConnector.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MicroOcpp; using namespace MicroOcpp::Ocpp16; MeteringConnector::MeteringConnector(Context& context, int connectorId, MeterStore& meterStore) : MemoryManaged("v16.Metering.MeteringConnector"), context(context), model(context.getModel()), connectorId{connectorId}, meterStore(meterStore), meterData(makeVector>(getMemoryTag())), samplers(makeVector>(getMemoryTag())) { context.getRequestQueue().addSendQueue(this); auto meterValuesSampledDataString = declareConfiguration("MeterValuesSampledData", ""); declareConfiguration("MeterValuesSampledDataMaxLength", 8, CONFIGURATION_VOLATILE, true); meterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval", 60); registerConfigurationValidator("MeterValueSampleInterval", VALIDATE_UNSIGNED_INT); auto stopTxnSampledDataString = declareConfiguration("StopTxnSampledData", ""); declareConfiguration("StopTxnSampledDataMaxLength", 8, CONFIGURATION_VOLATILE, true); auto meterValuesAlignedDataString = declareConfiguration("MeterValuesAlignedData", ""); declareConfiguration("MeterValuesAlignedDataMaxLength", 8, CONFIGURATION_VOLATILE, true); clockAlignedDataIntervalInt = declareConfiguration("ClockAlignedDataInterval", 0); registerConfigurationValidator("ClockAlignedDataInterval", VALIDATE_UNSIGNED_INT); auto stopTxnAlignedDataString = declareConfiguration("StopTxnAlignedData", ""); meterValuesInTxOnlyBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "MeterValuesInTxOnly", true); stopTxnDataCapturePeriodicBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "StopTxnDataCapturePeriodic", false); transactionMessageAttemptsInt = declareConfiguration("TransactionMessageAttempts", 3); transactionMessageRetryIntervalInt = declareConfiguration("TransactionMessageRetryInterval", 60); sampledDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, meterValuesSampledDataString)); alignedDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, meterValuesAlignedDataString)); stopTxnSampledDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, stopTxnSampledDataString)); stopTxnAlignedDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, stopTxnAlignedDataString)); } void MeteringConnector::loop() { bool txBreak = false; if (model.getConnector(connectorId)) { auto &curTx = model.getConnector(connectorId)->getTransaction(); txBreak = (curTx && curTx->isRunning()) != trackTxRunning; trackTxRunning = (curTx && curTx->isRunning()); } if (txBreak) { lastSampleTime = mocpp_tick_ms(); } if (model.getConnector(connectorId)) { if (transaction != model.getConnector(connectorId)->getTransaction()) { transaction = model.getConnector(connectorId)->getTransaction(); } if (transaction && transaction->isRunning() && !transaction->isSilent()) { //check during transaction if (!stopTxnData || stopTxnData->getTxNr() != transaction->getTxNr()) { MO_DBG_WARN("reload stopTxnData, %s, for tx-%u-%u", stopTxnData ? "replace" : "first time", connectorId, transaction->getTxNr()); //reload (e.g. after power cut during transaction) stopTxnData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction.get()); } } else { //check outside of transaction if (connectorId != 0 && meterValuesInTxOnlyBool->getBool()) { //don't take any MeterValues outside of transactions on connectorIds other than 0 return; } } } if (clockAlignedDataIntervalInt->getInt() >= 1 && model.getClock().now() >= MIN_TIME) { auto& timestampNow = model.getClock().now(); auto dt = nextAlignedTime - timestampNow; if (dt < 0 || //normal case: interval elapsed dt > clockAlignedDataIntervalInt->getInt()) { //special case: clock has been adjusted or first run MO_DBG_DEBUG("Clock aligned measurement %ds: %s", dt, abs(dt) <= 60 ? "in time (tolerance <= 60s)" : "off, e.g. because of first run. Ignore"); if (abs(dt) <= 60) { //is measurement still "clock-aligned"? if (auto alignedMeterValue = alignedDataBuilder->takeSample(model.getClock().now(), ReadingContext_SampleClock)) { if (meterData.size() >= MO_METERVALUES_CACHE_MAXSIZE) { MO_DBG_INFO("MeterValue cache full. Drop old MV"); meterData.erase(meterData.begin()); } alignedMeterValue->setOpNr(context.getRequestQueue().getNextOpNr()); if (transaction) { alignedMeterValue->setTxNr(transaction->getTxNr()); } meterData.push_back(std::move(alignedMeterValue)); } if (stopTxnData) { auto alignedStopTx = stopTxnAlignedDataBuilder->takeSample(model.getClock().now(), ReadingContext_SampleClock); if (alignedStopTx) { stopTxnData->addTxData(std::move(alignedStopTx)); } } } Timestamp midnightBase = Timestamp(2010,0,0,0,0,0); auto intervall = timestampNow - midnightBase; intervall %= 3600 * 24; Timestamp midnight = timestampNow - intervall; intervall += clockAlignedDataIntervalInt->getInt(); if (intervall >= 3600 * 24) { //next measurement is tomorrow; set to precisely 00:00 nextAlignedTime = midnight; nextAlignedTime += 3600 * 24; } else { intervall /= clockAlignedDataIntervalInt->getInt(); nextAlignedTime = midnight + (intervall * clockAlignedDataIntervalInt->getInt()); } } } if (meterValueSampleIntervalInt->getInt() >= 1) { //record periodic tx data if (mocpp_tick_ms() - lastSampleTime >= (unsigned long) (meterValueSampleIntervalInt->getInt() * 1000)) { if (auto sampledMeterValue = sampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_SamplePeriodic)) { if (meterData.size() >= MO_METERVALUES_CACHE_MAXSIZE) { MO_DBG_INFO("MeterValue cache full. Drop old MV"); meterData.erase(meterData.begin()); } sampledMeterValue->setOpNr(context.getRequestQueue().getNextOpNr()); if (transaction) { sampledMeterValue->setTxNr(transaction->getTxNr()); } meterData.push_back(std::move(sampledMeterValue)); } if (stopTxnData && stopTxnDataCapturePeriodicBool->getBool()) { auto sampleStopTx = stopTxnSampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_SamplePeriodic); if (sampleStopTx) { stopTxnData->addTxData(std::move(sampleStopTx)); } } lastSampleTime = mocpp_tick_ms(); } } } std::unique_ptr MeteringConnector::takeTriggeredMeterValues() { auto sample = sampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_Trigger); if (!sample) { return nullptr; } std::shared_ptr transaction = nullptr; if (model.getConnector(connectorId)) { transaction = model.getConnector(connectorId)->getTransaction(); } return std::unique_ptr(new MeterValues(model, std::move(sample), connectorId, transaction)); } void MeteringConnector::addMeterValueSampler(std::unique_ptr meterValueSampler) { if (!strcmp(meterValueSampler->getProperties().getMeasurand(), "Energy.Active.Import.Register")) { energySamplerIndex = samplers.size(); } samplers.push_back(std::move(meterValueSampler)); } std::unique_ptr MeteringConnector::readTxEnergyMeter(ReadingContext model) { if (energySamplerIndex >= 0 && (size_t) energySamplerIndex < samplers.size()) { return samplers[energySamplerIndex]->takeValue(model); } else { MO_DBG_DEBUG("Called readTxEnergyMeter(), but no energySampler or handling strategy set"); return nullptr; } } void MeteringConnector::beginTxMeterData(Transaction *transaction) { if (!stopTxnData || stopTxnData->getTxNr() != transaction->getTxNr()) { stopTxnData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction); } if (stopTxnData) { auto sampleTxBegin = stopTxnSampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_TransactionBegin); if (sampleTxBegin) { stopTxnData->addTxData(std::move(sampleTxBegin)); } } } std::shared_ptr MeteringConnector::endTxMeterData(Transaction *transaction) { if (!stopTxnData || stopTxnData->getTxNr() != transaction->getTxNr()) { stopTxnData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction); } if (stopTxnData) { auto sampleTxEnd = stopTxnSampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_TransactionEnd); if (sampleTxEnd) { stopTxnData->addTxData(std::move(sampleTxEnd)); } } return std::move(stopTxnData); } void MeteringConnector::abortTxMeterData() { stopTxnData.reset(); } std::shared_ptr MeteringConnector::getStopTxMeterData(Transaction *transaction) { auto txData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction); if (!txData) { MO_DBG_ERR("could not create TxData"); return nullptr; } return txData; } bool MeteringConnector::existsSampler(const char *measurand, size_t len) { for (size_t i = 0; i < samplers.size(); i++) { if (strlen(samplers[i]->getProperties().getMeasurand()) == len && !strncmp(measurand, samplers[i]->getProperties().getMeasurand(), len)) { return true; } } return false; } unsigned int MeteringConnector::getFrontRequestOpNr() { if (!meterDataFront && !meterData.empty()) { MO_DBG_DEBUG("advance MV front"); meterDataFront = std::move(meterData.front()); meterData.erase(meterData.begin()); } if (meterDataFront) { return meterDataFront->getOpNr(); } return NoOperation; } std::unique_ptr MeteringConnector::fetchFrontRequest() { if (!meterDataFront) { return nullptr; } if ((int)meterDataFront->getAttemptNr() >= transactionMessageAttemptsInt->getInt()) { MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard MeterValue"); meterDataFront.reset(); return nullptr; } if (mocpp_tick_ms() - meterDataFront->getAttemptTime() < meterDataFront->getAttemptNr() * (unsigned long)(std::max(0, transactionMessageRetryIntervalInt->getInt())) * 1000UL) { return nullptr; } meterDataFront->advanceAttemptNr(); meterDataFront->setAttemptTime(mocpp_tick_ms()); //fetch tx for meterValue std::shared_ptr tx; if (meterDataFront->getTxNr() >= 0) { tx = model.getTransactionStore()->getTransaction(connectorId, meterDataFront->getTxNr()); } //discard MV if it belongs to silent tx if (tx && tx->isSilent()) { MO_DBG_DEBUG("Drop MeterValue belonging to silent tx"); meterDataFront.reset(); return nullptr; } auto meterValues = makeRequest(new MeterValues(model, meterDataFront.get(), connectorId, tx)); meterValues->setOnReceiveConfListener([this] (JsonObject) { //operation success MO_DBG_DEBUG("drop MV front"); meterDataFront.reset(); }); return meterValues; } ================================================ FILE: src/MicroOcpp/Model/Metering/MeteringConnector.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_METERING_CONNECTOR_H #define MO_METERING_CONNECTOR_H #include #include #include #include #include #include #include #include #ifndef MO_METERVALUES_CACHE_MAXSIZE #define MO_METERVALUES_CACHE_MAXSIZE MO_REQUEST_CACHE_MAXSIZE #endif namespace MicroOcpp { class Context; class Model; class Operation; class MeterStore; class MeteringConnector : public MemoryManaged, public RequestEmitter { private: Context& context; Model& model; const int connectorId; MeterStore& meterStore; Vector> meterData; std::unique_ptr meterDataFront; std::shared_ptr stopTxnData; std::unique_ptr sampledDataBuilder; std::unique_ptr alignedDataBuilder; std::unique_ptr stopTxnSampledDataBuilder; std::unique_ptr stopTxnAlignedDataBuilder; std::shared_ptr sampledDataSelectString; std::shared_ptr alignedDataSelectString; std::shared_ptr stopTxnSampledDataSelectString; std::shared_ptr stopTxnAlignedDataSelectString; unsigned long lastSampleTime = 0; //0 means not charging right now Timestamp nextAlignedTime; std::shared_ptr transaction; bool trackTxRunning = false; Vector> samplers; int energySamplerIndex {-1}; std::shared_ptr meterValueSampleIntervalInt; std::shared_ptr clockAlignedDataIntervalInt; std::shared_ptr meterValuesInTxOnlyBool; std::shared_ptr stopTxnDataCapturePeriodicBool; std::shared_ptr transactionMessageAttemptsInt; std::shared_ptr transactionMessageRetryIntervalInt; public: MeteringConnector(Context& context, int connectorId, MeterStore& meterStore); void loop(); void addMeterValueSampler(std::unique_ptr meterValueSampler); std::unique_ptr readTxEnergyMeter(ReadingContext model); std::unique_ptr takeTriggeredMeterValues(); void beginTxMeterData(Transaction *transaction); std::shared_ptr endTxMeterData(Transaction *transaction); void abortTxMeterData(); std::shared_ptr getStopTxMeterData(Transaction *transaction); bool existsSampler(const char *measurand, size_t len); //RequestEmitter implementation unsigned int getFrontRequestOpNr() override; std::unique_ptr fetchFrontRequest() override; }; } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Model/Metering/MeteringService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include using namespace MicroOcpp; MeteringService::MeteringService(Context& context, int numConn, std::shared_ptr filesystem) : MemoryManaged("v16.Metering.MeteringService"), context(context), meterStore(filesystem), connectors(makeVector>(getMemoryTag())) { //set factory defaults for Metering-related config keys declareConfiguration("MeterValuesSampledData", "Energy.Active.Import.Register,Power.Active.Import"); declareConfiguration("StopTxnSampledData", ""); declareConfiguration("MeterValuesAlignedData", "Energy.Active.Import.Register,Power.Active.Import"); declareConfiguration("StopTxnAlignedData", ""); connectors.reserve(numConn); for (int i = 0; i < numConn; i++) { connectors.emplace_back(new MeteringConnector(context, i, meterStore)); } std::function validateSelectString = [this] (const char *csl) { bool isValid = true; const char *l = csl; //the beginning of an entry of the comma-separated list const char *r = l; //one place after the last character of the entry beginning with l while (*l) { if (*l == ',') { l++; continue; } r = l + 1; while (*r != '\0' && *r != ',') { r++; } bool found = false; for (size_t cId = 0; cId < connectors.size(); cId++) { if (connectors[cId]->existsSampler(l, (size_t) (r - l))) { found = true; break; } } if (!found) { isValid = false; MO_DBG_WARN("could not find metering device for %.*s", (int) (r - l), l); break; } l = r; } return isValid; }; registerConfigurationValidator("MeterValuesSampledData", validateSelectString); registerConfigurationValidator("StopTxnSampledData", validateSelectString); registerConfigurationValidator("MeterValuesAlignedData", validateSelectString); registerConfigurationValidator("StopTxnAlignedData", validateSelectString); registerConfigurationValidator("MeterValueSampleInterval", VALIDATE_UNSIGNED_INT); registerConfigurationValidator("ClockAlignedDataInterval", VALIDATE_UNSIGNED_INT); /* * Register further message handlers to support echo mode: when this library * is connected with a WebSocket echo server, let it reply to its own requests. * Mocking an OCPP Server on the same device makes running (unit) tests easier. */ context.getOperationRegistry().registerOperation("MeterValues", [this] () { return new Ocpp16::MeterValues(this->context.getModel());}); } void MeteringService::loop(){ for (unsigned int i = 0; i < connectors.size(); i++){ connectors[i]->loop(); } } void MeteringService::addMeterValueSampler(int connectorId, std::unique_ptr meterValueSampler) { if (connectorId < 0 || connectorId >= (int) connectors.size()) { MO_DBG_ERR("connectorId is out of bounds"); return; } connectors[connectorId]->addMeterValueSampler(std::move(meterValueSampler)); } std::unique_ptr MeteringService::readTxEnergyMeter(int connectorId, ReadingContext context) { if (connectorId < 0 || (size_t) connectorId >= connectors.size()) { MO_DBG_ERR("connectorId is out of bounds"); return nullptr; } return connectors[connectorId]->readTxEnergyMeter(context); } std::unique_ptr MeteringService::takeTriggeredMeterValues(int connectorId) { if (connectorId < 0 || connectorId >= (int) connectors.size()) { MO_DBG_ERR("connectorId out of bounds. Ignore"); return nullptr; } auto msg = connectors[connectorId]->takeTriggeredMeterValues(); if (msg) { auto meterValues = makeRequest(std::move(msg)); meterValues->setTimeout(120000); return meterValues; } MO_DBG_DEBUG("Did not take any samples for connectorId %d", connectorId); return nullptr; } void MeteringService::beginTxMeterData(Transaction *transaction) { if (!transaction) { MO_DBG_ERR("invalid argument"); return; } auto connectorId = transaction->getConnectorId(); if (connectorId >= connectors.size()) { MO_DBG_ERR("connectorId is out of bounds"); return; } connectors[connectorId]->beginTxMeterData(transaction); } std::shared_ptr MeteringService::endTxMeterData(Transaction *transaction) { if (!transaction) { MO_DBG_ERR("invalid argument"); return nullptr; } auto connectorId = transaction->getConnectorId(); if (connectorId >= connectors.size()) { MO_DBG_ERR("connectorId is out of bounds"); return nullptr; } return connectors[connectorId]->endTxMeterData(transaction); } void MeteringService::abortTxMeterData(unsigned int connectorId) { if (connectorId >= connectors.size()) { MO_DBG_ERR("connectorId is out of bounds"); return; } connectors[connectorId]->abortTxMeterData(); } std::shared_ptr MeteringService::getStopTxMeterData(Transaction *transaction) { if (!transaction) { MO_DBG_ERR("invalid argument"); return nullptr; } auto connectorId = transaction->getConnectorId(); if (connectorId >= connectors.size()) { MO_DBG_ERR("connectorId is out of bounds"); return nullptr; } return connectors[connectorId]->getStopTxMeterData(transaction); } bool MeteringService::removeTxMeterData(unsigned int connectorId, unsigned int txNr) { return meterStore.remove(connectorId, txNr); } ================================================ FILE: src/MicroOcpp/Model/Metering/MeteringService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_METERINGSERVICE_H #define MO_METERINGSERVICE_H #include #include #include #include #include #include namespace MicroOcpp { class Context; class Request; class FilesystemAdapter; class MeteringService : public MemoryManaged { private: Context& context; MeterStore meterStore; Vector> connectors; public: MeteringService(Context& context, int numConnectors, std::shared_ptr filesystem); void loop(); void addMeterValueSampler(int connectorId, std::unique_ptr meterValueSampler); std::unique_ptr readTxEnergyMeter(int connectorId, ReadingContext reason); std::unique_ptr takeTriggeredMeterValues(int connectorId); //snapshot of all meters now void beginTxMeterData(Transaction *transaction); std::shared_ptr endTxMeterData(Transaction *transaction); //use return value to keep data in cache void abortTxMeterData(unsigned int connectorId); //call this to free resources if txMeterData record is not ended normally. Does not remove files std::shared_ptr getStopTxMeterData(Transaction *transaction); //prefer endTxMeterData when possible bool removeTxMeterData(unsigned int connectorId, unsigned int txNr); int getNumConnectors() {return connectors.size();} }; } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Model/Metering/ReadingContext.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include namespace MicroOcpp { const char *serializeReadingContext(ReadingContext context) { switch (context) { case (ReadingContext_InterruptionBegin): return "Interruption.Begin"; case (ReadingContext_InterruptionEnd): return "Interruption.End"; case (ReadingContext_Other): return "Other"; case (ReadingContext_SampleClock): return "Sample.Clock"; case (ReadingContext_SamplePeriodic): return "Sample.Periodic"; case (ReadingContext_TransactionBegin): return "Transaction.Begin"; case (ReadingContext_TransactionEnd): return "Transaction.End"; case (ReadingContext_Trigger): return "Trigger"; default: MO_DBG_ERR("ReadingContext not specified"); /* fall through */ case (ReadingContext_UNDEFINED): return ""; } } ReadingContext deserializeReadingContext(const char *context) { if (!context) { MO_DBG_ERR("Invalid argument"); return ReadingContext_UNDEFINED; } if (!strcmp(context, "Sample.Periodic")) { return ReadingContext_SamplePeriodic; } else if (!strcmp(context, "Sample.Clock")) { return ReadingContext_SampleClock; } else if (!strcmp(context, "Transaction.Begin")) { return ReadingContext_TransactionBegin; } else if (!strcmp(context, "Transaction.End")) { return ReadingContext_TransactionEnd; } else if (!strcmp(context, "Other")) { return ReadingContext_Other; } else if (!strcmp(context, "Interruption.Begin")) { return ReadingContext_InterruptionBegin; } else if (!strcmp(context, "Interruption.End")) { return ReadingContext_InterruptionEnd; } else if (!strcmp(context, "Trigger")) { return ReadingContext_Trigger; } MO_DBG_ERR("ReadingContext not specified %.10s", context); return ReadingContext_UNDEFINED; } } //namespace MicroOcpp ================================================ FILE: src/MicroOcpp/Model/Metering/ReadingContext.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_READINGCONTEXT_H #define MO_READINGCONTEXT_H typedef enum { ReadingContext_UNDEFINED, ReadingContext_InterruptionBegin, ReadingContext_InterruptionEnd, ReadingContext_Other, ReadingContext_SampleClock, ReadingContext_SamplePeriodic, ReadingContext_TransactionBegin, ReadingContext_TransactionEnd, ReadingContext_Trigger } ReadingContext; #ifdef __cplusplus namespace MicroOcpp { const char *serializeReadingContext(ReadingContext context); ReadingContext deserializeReadingContext(const char *serialized); } #endif #endif ================================================ FILE: src/MicroOcpp/Model/Metering/SampledValue.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #ifndef MO_SAMPLEDVALUE_FLOAT_FORMAT #define MO_SAMPLEDVALUE_FLOAT_FORMAT "%.2f" #endif using namespace MicroOcpp; int32_t SampledValueDeSerializer::deserialize(const char *str) { return strtol(str, nullptr, 10); } MicroOcpp::String SampledValueDeSerializer::serialize(int32_t& val) { char str [12] = {'\0'}; snprintf(str, 12, "%" PRId32, val); return makeString("v16.Metering.SampledValueDeSerializer", str); } MicroOcpp::String SampledValueDeSerializer::serialize(float& val) { char str [20]; str[0] = '\0'; snprintf(str, 20, MO_SAMPLEDVALUE_FLOAT_FORMAT, val); return makeString("v16.Metering.SampledValueDeSerializer", str); } std::unique_ptr SampledValue::toJson() { auto value = serializeValue(); if (value.empty()) { return nullptr; } size_t capacity = 0; capacity += JSON_OBJECT_SIZE(8); capacity += value.length() + 1; auto result = makeJsonDoc("v16.Metering.SampledValue", capacity); auto payload = result->to(); payload["value"] = value; auto context_cstr = serializeReadingContext(context); if (context_cstr) payload["context"] = context_cstr; if (*properties.getFormat()) payload["format"] = properties.getFormat(); if (*properties.getMeasurand()) payload["measurand"] = properties.getMeasurand(); if (*properties.getPhase()) payload["phase"] = properties.getPhase(); if (*properties.getLocation()) payload["location"] = properties.getLocation(); if (*properties.getUnit()) payload["unit"] = properties.getUnit(); return result; } ReadingContext SampledValue::getReadingContext() { return context; } ================================================ FILE: src/MicroOcpp/Model/Metering/SampledValue.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef SAMPLEDVALUE_H #define SAMPLEDVALUE_H #include #include #include #include #include #include namespace MicroOcpp { template class SampledValueDeSerializer { public: static T deserialize(const char *str); static bool ready(T& val); static String serialize(T& val); static int32_t toInteger(T& val); }; template <> class SampledValueDeSerializer { // example class public: static int32_t deserialize(const char *str); static bool ready(int32_t& val) {return true;} //int32_t is always valid static String serialize(int32_t& val); static int32_t toInteger(int32_t& val) {return val;} //no conversion required }; template <> class SampledValueDeSerializer { // Used in meterValues public: static float deserialize(const char *str) {return atof(str);} static bool ready(float& val) {return true;} //float is always valid static String serialize(float& val); static int32_t toInteger(float& val) {return (int32_t) val;} }; class SampledValueProperties { private: String format; String measurand; String phase; String location; String unit; public: SampledValueProperties() : format(makeString("v16.Metering.SampledValueProperties")), measurand(makeString("v16.Metering.SampledValueProperties")), phase(makeString("v16.Metering.SampledValueProperties")), location(makeString("v16.Metering.SampledValueProperties")), unit(makeString("v16.Metering.SampledValueProperties")) { } SampledValueProperties(const SampledValueProperties& other) : format(other.format), measurand(other.measurand), phase(other.phase), location(other.location), unit(other.unit) { } ~SampledValueProperties() = default; void setFormat(const char *format) {this->format = format;} const char *getFormat() const {return format.c_str();} void setMeasurand(const char *measurand) {this->measurand = measurand;} const char *getMeasurand() const {return measurand.c_str();} void setPhase(const char *phase) {this->phase = phase;} const char *getPhase() const {return phase.c_str();} void setLocation(const char *location) {this->location = location;} const char *getLocation() const {return location.c_str();} void setUnit(const char *unit) {this->unit = unit;} const char *getUnit() const {return unit.c_str();} }; class SampledValue { protected: const SampledValueProperties& properties; const ReadingContext context; virtual String serializeValue() = 0; public: SampledValue(const SampledValueProperties& properties, ReadingContext context) : properties(properties), context(context) { } SampledValue(const SampledValue& other) : properties(other.properties), context(other.context) { } virtual ~SampledValue() = default; std::unique_ptr toJson(); virtual operator bool() = 0; virtual int32_t toInteger() = 0; ReadingContext getReadingContext(); }; template class SampledValueConcrete : public SampledValue, public MemoryManaged { private: T value; public: SampledValueConcrete(const SampledValueProperties& properties, ReadingContext context, const T&& value) : SampledValue(properties, context), MemoryManaged("v16.Metering.SampledValueConcrete"), value(value) { } SampledValueConcrete(const SampledValueConcrete& other) : SampledValue(other), MemoryManaged(other), value(other.value) { } ~SampledValueConcrete() = default; operator bool() override {return DeSerializer::ready(value);} String serializeValue() override {return DeSerializer::serialize(value);} int32_t toInteger() override { return DeSerializer::toInteger(value);} }; class SampledValueSampler { protected: SampledValueProperties properties; public: SampledValueSampler(SampledValueProperties properties) : properties(properties) { } virtual ~SampledValueSampler() = default; virtual std::unique_ptr takeValue(ReadingContext context) = 0; virtual std::unique_ptr deserializeValue(JsonObject svJson) = 0; const SampledValueProperties& getProperties() {return properties;}; }; template class SampledValueSamplerConcrete : public SampledValueSampler, public MemoryManaged { private: std::function sampler; public: SampledValueSamplerConcrete(SampledValueProperties properties, std::function sampler) : SampledValueSampler(properties), MemoryManaged("v16.Metering.SampledValueSamplerConcrete"), sampler(sampler) { } std::unique_ptr takeValue(ReadingContext context) override { return std::unique_ptr>(new SampledValueConcrete( properties, context, sampler(context))); } std::unique_ptr deserializeValue(JsonObject svJson) override { return std::unique_ptr>(new SampledValueConcrete( properties, deserializeReadingContext(svJson["context"] | "NOT_SET"), DeSerializer::deserialize(svJson["value"] | ""))); } }; } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Model/Model.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MicroOcpp; Model::Model(ProtocolVersion version, uint16_t bootNr) : MemoryManaged("Model"), connectors(makeVector>(getMemoryTag())), version(version), bootNr(bootNr) { } Model::~Model() = default; void Model::loop() { if (bootService) { bootService->loop(); } if (capabilitiesUpdated) { updateSupportedStandardProfiles(); capabilitiesUpdated = false; } if (!runTasks) { return; } for (auto& connector : connectors) { connector->loop(); } if (chargeControlCommon) chargeControlCommon->loop(); if (smartChargingService) smartChargingService->loop(); if (heartbeatService) heartbeatService->loop(); if (meteringService) meteringService->loop(); if (diagnosticsService) diagnosticsService->loop(); if (firmwareService) firmwareService->loop(); #if MO_ENABLE_RESERVATION if (reservationService) reservationService->loop(); #endif //MO_ENABLE_RESERVATION if (resetService) resetService->loop(); #if MO_ENABLE_V201 if (availabilityService) availabilityService->loop(); if (transactionService) transactionService->loop(); if (resetServiceV201) resetServiceV201->loop(); #endif } void Model::setTransactionStore(std::unique_ptr ts) { transactionStore = std::move(ts); capabilitiesUpdated = true; } TransactionStore *Model::getTransactionStore() { return transactionStore.get(); } void Model::setSmartChargingService(std::unique_ptr scs) { smartChargingService = std::move(scs); capabilitiesUpdated = true; } SmartChargingService* Model::getSmartChargingService() const { return smartChargingService.get(); } void Model::setConnectorsCommon(std::unique_ptr ccs) { chargeControlCommon = std::move(ccs); capabilitiesUpdated = true; } ConnectorsCommon *Model::getConnectorsCommon() { return chargeControlCommon.get(); } void Model::setConnectors(Vector>&& connectors) { this->connectors = std::move(connectors); capabilitiesUpdated = true; } unsigned int Model::getNumConnectors() const { return connectors.size(); } Connector *Model::getConnector(unsigned int connectorId) { if (connectorId >= connectors.size()) { MO_DBG_ERR("connector with connectorId %u does not exist", connectorId); return nullptr; } return connectors[connectorId].get(); } void Model::setMeteringSerivce(std::unique_ptr ms) { meteringService = std::move(ms); capabilitiesUpdated = true; } MeteringService* Model::getMeteringService() const { return meteringService.get(); } void Model::setFirmwareService(std::unique_ptr fws) { firmwareService = std::move(fws); capabilitiesUpdated = true; } FirmwareService *Model::getFirmwareService() const { return firmwareService.get(); } void Model::setDiagnosticsService(std::unique_ptr ds) { diagnosticsService = std::move(ds); capabilitiesUpdated = true; } DiagnosticsService *Model::getDiagnosticsService() const { return diagnosticsService.get(); } void Model::setHeartbeatService(std::unique_ptr hs) { heartbeatService = std::move(hs); capabilitiesUpdated = true; } #if MO_ENABLE_LOCAL_AUTH void Model::setAuthorizationService(std::unique_ptr as) { authorizationService = std::move(as); capabilitiesUpdated = true; } AuthorizationService *Model::getAuthorizationService() { return authorizationService.get(); } #endif //MO_ENABLE_LOCAL_AUTH #if MO_ENABLE_RESERVATION void Model::setReservationService(std::unique_ptr rs) { reservationService = std::move(rs); capabilitiesUpdated = true; } ReservationService *Model::getReservationService() { return reservationService.get(); } #endif //MO_ENABLE_RESERVATION void Model::setBootService(std::unique_ptr bs){ bootService = std::move(bs); capabilitiesUpdated = true; } BootService *Model::getBootService() const { return bootService.get(); } void Model::setResetService(std::unique_ptr rs) { this->resetService = std::move(rs); capabilitiesUpdated = true; } ResetService *Model::getResetService() const { return resetService.get(); } #if MO_ENABLE_CERT_MGMT void Model::setCertificateService(std::unique_ptr cs) { this->certService = std::move(cs); capabilitiesUpdated = true; } CertificateService *Model::getCertificateService() const { return certService.get(); } #endif //MO_ENABLE_CERT_MGMT #if MO_ENABLE_V201 void Model::setAvailabilityService(std::unique_ptr as) { this->availabilityService = std::move(as); capabilitiesUpdated = true; } AvailabilityService *Model::getAvailabilityService() const { return availabilityService.get(); } void Model::setVariableService(std::unique_ptr vs) { this->variableService = std::move(vs); capabilitiesUpdated = true; } VariableService *Model::getVariableService() const { return variableService.get(); } void Model::setTransactionService(std::unique_ptr ts) { this->transactionService = std::move(ts); capabilitiesUpdated = true; } TransactionService *Model::getTransactionService() const { return transactionService.get(); } void Model::setResetServiceV201(std::unique_ptr rs) { this->resetServiceV201 = std::move(rs); capabilitiesUpdated = true; } Ocpp201::ResetService *Model::getResetServiceV201() const { return resetServiceV201.get(); } void Model::setMeteringServiceV201(std::unique_ptr rs) { this->meteringServiceV201 = std::move(rs); capabilitiesUpdated = true; } Ocpp201::MeteringService *Model::getMeteringServiceV201() const { return meteringServiceV201.get(); } void Model::setRemoteControlService(std::unique_ptr rs) { remoteControlService = std::move(rs); capabilitiesUpdated = true; } RemoteControlService *Model::getRemoteControlService() const { return remoteControlService.get(); } #endif Clock& Model::getClock() { return clock; } const ProtocolVersion& Model::getVersion() const { return version; } uint16_t Model::getBootNr() { return bootNr; } void Model::updateSupportedStandardProfiles() { auto supportedFeatureProfilesString = declareConfiguration("SupportedFeatureProfiles", "", CONFIGURATION_VOLATILE, true); if (!supportedFeatureProfilesString) { MO_DBG_ERR("OOM"); return; } auto buf = makeString(getMemoryTag(), supportedFeatureProfilesString->getString()); if (chargeControlCommon && heartbeatService && bootService) { if (!strstr(supportedFeatureProfilesString->getString(), "Core")) { if (!buf.empty()) buf += ','; buf += "Core"; } } if (firmwareService || diagnosticsService) { if (!strstr(supportedFeatureProfilesString->getString(), "FirmwareManagement")) { if (!buf.empty()) buf += ','; buf += "FirmwareManagement"; } } #if MO_ENABLE_LOCAL_AUTH if (authorizationService && authorizationService->localAuthListEnabled()) { if (!strstr(supportedFeatureProfilesString->getString(), "LocalAuthListManagement")) { if (!buf.empty()) buf += ','; buf += "LocalAuthListManagement"; } } #endif //MO_ENABLE_LOCAL_AUTH #if MO_ENABLE_RESERVATION if (reservationService) { if (!strstr(supportedFeatureProfilesString->getString(), "Reservation")) { if (!buf.empty()) buf += ','; buf += "Reservation"; } } #endif //MO_ENABLE_RESERVATION if (smartChargingService) { if (!strstr(supportedFeatureProfilesString->getString(), "SmartCharging")) { if (!buf.empty()) buf += ','; buf += "SmartCharging"; } } if (!strstr(supportedFeatureProfilesString->getString(), "RemoteTrigger")) { if (!buf.empty()) buf += ','; buf += "RemoteTrigger"; } supportedFeatureProfilesString->setString(buf.c_str()); MO_DBG_DEBUG("supported feature profiles: %s", buf.c_str()); } ================================================ FILE: src/MicroOcpp/Model/Model.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_MODEL_H #define MO_MODEL_H #include #include #include #include #include namespace MicroOcpp { class TransactionStore; class SmartChargingService; class ConnectorsCommon; class MeteringService; class FirmwareService; class DiagnosticsService; class HeartbeatService; class BootService; class ResetService; #if MO_ENABLE_LOCAL_AUTH class AuthorizationService; #endif //MO_ENABLE_LOCAL_AUTH #if MO_ENABLE_RESERVATION class ReservationService; #endif //MO_ENABLE_RESERVATION #if MO_ENABLE_CERT_MGMT class CertificateService; #endif //MO_ENABLE_CERT_MGMT #if MO_ENABLE_V201 class AvailabilityService; class VariableService; class TransactionService; class RemoteControlService; namespace Ocpp201 { class ResetService; class MeteringService; } #endif //MO_ENABLE_V201 class Model : public MemoryManaged { private: Vector> connectors; std::unique_ptr transactionStore; std::unique_ptr smartChargingService; std::unique_ptr chargeControlCommon; std::unique_ptr meteringService; std::unique_ptr firmwareService; std::unique_ptr diagnosticsService; std::unique_ptr heartbeatService; std::unique_ptr bootService; std::unique_ptr resetService; #if MO_ENABLE_LOCAL_AUTH std::unique_ptr authorizationService; #endif //MO_ENABLE_LOCAL_AUTH #if MO_ENABLE_RESERVATION std::unique_ptr reservationService; #endif //MO_ENABLE_RESERVATION #if MO_ENABLE_CERT_MGMT std::unique_ptr certService; #endif //MO_ENABLE_CERT_MGMT #if MO_ENABLE_V201 std::unique_ptr availabilityService; std::unique_ptr variableService; std::unique_ptr transactionService; std::unique_ptr resetServiceV201; std::unique_ptr meteringServiceV201; std::unique_ptr remoteControlService; #endif Clock clock; ProtocolVersion version; bool capabilitiesUpdated = true; void updateSupportedStandardProfiles(); bool runTasks = false; const uint16_t bootNr = 0; //each boot of this lib has a unique number public: Model(ProtocolVersion version = ProtocolVersion(1,6), uint16_t bootNr = 0); Model(const Model& rhs) = delete; ~Model(); void loop(); void activateTasks() {runTasks = true;} void setTransactionStore(std::unique_ptr transactionStore); TransactionStore *getTransactionStore(); void setSmartChargingService(std::unique_ptr scs); SmartChargingService* getSmartChargingService() const; void setConnectorsCommon(std::unique_ptr ccs); ConnectorsCommon *getConnectorsCommon(); void setConnectors(Vector>&& connectors); unsigned int getNumConnectors() const; Connector *getConnector(unsigned int connectorId); void setMeteringSerivce(std::unique_ptr meteringService); MeteringService* getMeteringService() const; void setFirmwareService(std::unique_ptr firmwareService); FirmwareService *getFirmwareService() const; void setDiagnosticsService(std::unique_ptr diagnosticsService); DiagnosticsService *getDiagnosticsService() const; void setHeartbeatService(std::unique_ptr heartbeatService); #if MO_ENABLE_LOCAL_AUTH void setAuthorizationService(std::unique_ptr authorizationService); AuthorizationService *getAuthorizationService(); #endif //MO_ENABLE_LOCAL_AUTH #if MO_ENABLE_RESERVATION void setReservationService(std::unique_ptr reservationService); ReservationService *getReservationService(); #endif //MO_ENABLE_RESERVATION void setBootService(std::unique_ptr bs); BootService *getBootService() const; void setResetService(std::unique_ptr rs); ResetService *getResetService() const; #if MO_ENABLE_CERT_MGMT void setCertificateService(std::unique_ptr cs); CertificateService *getCertificateService() const; #endif //MO_ENABLE_CERT_MGMT #if MO_ENABLE_V201 void setAvailabilityService(std::unique_ptr as); AvailabilityService *getAvailabilityService() const; void setVariableService(std::unique_ptr vs); VariableService *getVariableService() const; void setTransactionService(std::unique_ptr ts); TransactionService *getTransactionService() const; void setResetServiceV201(std::unique_ptr rs); Ocpp201::ResetService *getResetServiceV201() const; void setMeteringServiceV201(std::unique_ptr ms); Ocpp201::MeteringService *getMeteringServiceV201() const; void setRemoteControlService(std::unique_ptr rs); RemoteControlService *getRemoteControlService() const; #endif Clock &getClock(); const ProtocolVersion& getVersion() const; uint16_t getBootNr(); }; } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Model/RemoteControl/RemoteControlDefs.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_UNLOCKCONNECTOR_H #define MO_UNLOCKCONNECTOR_H #include #if MO_ENABLE_V201 #include #include typedef enum { RequestStartStopStatus_Accepted, RequestStartStopStatus_Rejected } RequestStartStopStatus; #if MO_ENABLE_CONNECTOR_LOCK typedef enum { UnlockStatus_Unlocked, UnlockStatus_UnlockFailed, UnlockStatus_OngoingAuthorizedTransaction, UnlockStatus_UnknownConnector, UnlockStatus_PENDING // unlock action not finished yet, result still unknown (MO will check again later) } UnlockStatus; #endif // MO_ENABLE_CONNECTOR_LOCK #endif // MO_ENABLE_V201 #endif // MO_UNLOCKCONNECTOR_H ================================================ FILE: src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include #include #include #include #include #include #include using namespace MicroOcpp; RemoteControlServiceEvse::RemoteControlServiceEvse(Context& context, unsigned int evseId) : MemoryManaged("v201.RemoteControl.RemoteControlServiceEvse"), context(context), evseId(evseId) { } #if MO_ENABLE_CONNECTOR_LOCK void RemoteControlServiceEvse::setOnUnlockConnector(UnlockConnectorResult (*onUnlockConnector)(unsigned int evseId, void *userData), void *userData) { this->onUnlockConnector = onUnlockConnector; this->onUnlockConnectorUserData = userData; } UnlockStatus RemoteControlServiceEvse::unlockConnector() { if (!onUnlockConnector) { return UnlockStatus_UnlockFailed; } if (auto txService = context.getModel().getTransactionService()) { if (auto evse = txService->getEvse(evseId)) { if (auto tx = evse->getTransaction()) { if (tx->started && !tx->stopped && tx->isAuthorized) { return UnlockStatus_OngoingAuthorizedTransaction; } else { evse->abortTransaction(Ocpp201::Transaction::StoppedReason::Other,Ocpp201::TransactionEventTriggerReason::UnlockCommand); } } } } auto status = onUnlockConnector(evseId, onUnlockConnectorUserData); switch (status) { case UnlockConnectorResult_Pending: return UnlockStatus_PENDING; case UnlockConnectorResult_Unlocked: return UnlockStatus_Unlocked; case UnlockConnectorResult_UnlockFailed: return UnlockStatus_UnlockFailed; } MO_DBG_ERR("invalid onUnlockConnector result code"); return UnlockStatus_UnlockFailed; } #endif RemoteControlService::RemoteControlService(Context& context, size_t numEvses) : MemoryManaged("v201.RemoteControl.RemoteControlService"), context(context) { for (size_t i = 0; i < numEvses && i < MO_NUM_EVSEID; i++) { evses[i] = new RemoteControlServiceEvse(context, (unsigned int)i); } auto varService = context.getModel().getVariableService(); authorizeRemoteStart = varService->declareVariable("AuthCtrlr", "AuthorizeRemoteStart", false); context.getOperationRegistry().registerOperation("RequestStartTransaction", [this] () -> Operation* { if (!this->context.getModel().getTransactionService()) { return nullptr; //-> NotSupported } return new Ocpp201::RequestStartTransaction(*this);}); context.getOperationRegistry().registerOperation("RequestStopTransaction", [this] () -> Operation* { if (!this->context.getModel().getTransactionService()) { return nullptr; //-> NotSupported } return new Ocpp201::RequestStopTransaction(*this);}); #if MO_ENABLE_CONNECTOR_LOCK context.getOperationRegistry().registerOperation("UnlockConnector", [this] () { return new Ocpp201::UnlockConnector(*this);}); #endif context.getOperationRegistry().registerOperation("TriggerMessage", [&context] () { return new Ocpp16::TriggerMessage(context);}); } RemoteControlService::~RemoteControlService() { for (size_t i = 0; i < MO_NUM_EVSEID && evses[i]; i++) { delete evses[i]; } } RemoteControlServiceEvse *RemoteControlService::getEvse(unsigned int evseId) { if (evseId >= MO_NUM_EVSEID) { MO_DBG_ERR("invalid arg"); return nullptr; } return evses[evseId]; } RequestStartStopStatus RemoteControlService::requestStartTransaction(unsigned int evseId, unsigned int remoteStartId, IdToken idToken, char *transactionIdOut, size_t transactionIdBufSize) { TransactionService *txService = context.getModel().getTransactionService(); if (!txService) { MO_DBG_ERR("TxService uninitialized"); return RequestStartStopStatus_Rejected; } auto evse = txService->getEvse(evseId); if (!evse) { MO_DBG_ERR("EVSE not found"); return RequestStartStopStatus_Rejected; } if (!evse->beginAuthorization(idToken, authorizeRemoteStart->getBool())) { MO_DBG_INFO("EVSE still occupied with pending tx"); if (auto tx = evse->getTransaction()) { auto ret = snprintf(transactionIdOut, transactionIdBufSize, "%s", tx->transactionId); if (ret < 0 || (size_t)ret >= transactionIdBufSize) { MO_DBG_ERR("internal error"); return RequestStartStopStatus_Rejected; } } return RequestStartStopStatus_Rejected; } auto tx = evse->getTransaction(); if (!tx) { MO_DBG_ERR("internal error"); return RequestStartStopStatus_Rejected; } auto ret = snprintf(transactionIdOut, transactionIdBufSize, "%s", tx->transactionId); if (ret < 0 || (size_t)ret >= transactionIdBufSize) { MO_DBG_ERR("internal error"); return RequestStartStopStatus_Rejected; } tx->remoteStartId = remoteStartId; tx->notifyRemoteStartId = true; return RequestStartStopStatus_Accepted; } RequestStartStopStatus RemoteControlService::requestStopTransaction(const char *transactionId) { TransactionService *txService = context.getModel().getTransactionService(); if (!txService) { MO_DBG_ERR("TxService uninitialized"); return RequestStartStopStatus_Rejected; } bool success = false; for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID; evseId++) { if (auto evse = txService->getEvse(evseId)) { if (evse->getTransaction() && !strcmp(evse->getTransaction()->transactionId, transactionId)) { success = evse->abortTransaction(Ocpp201::Transaction::StoppedReason::Remote, Ocpp201::TransactionEventTriggerReason::RemoteStop); break; } } } return success ? RequestStartStopStatus_Accepted : RequestStartStopStatus_Rejected; } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Model/RemoteControl/RemoteControlService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_REMOTECONTROLSERVICE_H #define MO_REMOTECONTROLSERVICE_H #include #if MO_ENABLE_V201 #include #include #include #include namespace MicroOcpp { class Context; class Variable; class RemoteControlServiceEvse : public MemoryManaged { private: Context& context; const unsigned int evseId; #if MO_ENABLE_CONNECTOR_LOCK UnlockConnectorResult (*onUnlockConnector)(unsigned int evseId, void *user) = nullptr; void *onUnlockConnectorUserData = nullptr; #endif public: RemoteControlServiceEvse(Context& context, unsigned int evseId); #if MO_ENABLE_CONNECTOR_LOCK void setOnUnlockConnector(UnlockConnectorResult (*onUnlockConnector)(unsigned int evseId, void *userData), void *userData); UnlockStatus unlockConnector(); #endif }; class RemoteControlService : public MemoryManaged { private: Context& context; RemoteControlServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; Variable *authorizeRemoteStart = nullptr; public: RemoteControlService(Context& context, size_t numEvses); ~RemoteControlService(); RemoteControlServiceEvse *getEvse(unsigned int evseId); RequestStartStopStatus requestStartTransaction(unsigned int evseId, unsigned int remoteStartId, IdToken idToken, char *transactionIdOut, size_t transactionIdBufSize); //ChargingProfile, GroupIdToken not supported yet RequestStartStopStatus requestStopTransaction(const char *transactionId); }; } // namespace MicroOcpp #endif // MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Model/Reservation/Reservation.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_RESERVATION #include #include #include using namespace MicroOcpp; Reservation::Reservation(Model& model, unsigned int slot) : MemoryManaged("v16.Reservation.Reservation"), model(model), slot(slot) { snprintf(connectorIdKey, sizeof(connectorIdKey), MO_RESERVATION_CID_KEY "%u", slot); connectorIdInt = declareConfiguration(connectorIdKey, -1, RESERVATION_FN, false, false, false); snprintf(expiryDateRawKey, sizeof(expiryDateRawKey), MO_RESERVATION_EXPDATE_KEY "%u", slot); expiryDateRawString = declareConfiguration(expiryDateRawKey, "", RESERVATION_FN, false, false, false); snprintf(idTagKey, sizeof(idTagKey), MO_RESERVATION_IDTAG_KEY "%u", slot); idTagString = declareConfiguration(idTagKey, "", RESERVATION_FN, false, false, false); snprintf(reservationIdKey, sizeof(reservationIdKey), MO_RESERVATION_RESID_KEY "%u", slot); reservationIdInt = declareConfiguration(reservationIdKey, -1, RESERVATION_FN, false, false, false); snprintf(parentIdTagKey, sizeof(parentIdTagKey), MO_RESERVATION_PARENTID_KEY "%u", slot); parentIdTagString = declareConfiguration(parentIdTagKey, "", RESERVATION_FN, false, false, false); if (!connectorIdInt || !expiryDateRawString || !idTagString || !reservationIdInt || !parentIdTagString) { MO_DBG_ERR("initialization failure"); } } Reservation::~Reservation() { if (connectorIdInt->getKey() == connectorIdKey) { connectorIdInt->setKey(nullptr); } if (expiryDateRawString->getKey() == expiryDateRawKey) { expiryDateRawString->setKey(nullptr); } if (idTagString->getKey() == idTagKey) { idTagString->setKey(nullptr); } if (reservationIdInt->getKey() == reservationIdKey) { reservationIdInt->setKey(nullptr); } if (parentIdTagString->getKey() == parentIdTagKey) { parentIdTagString->setKey(nullptr); } } bool Reservation::isActive() { if (connectorIdInt->getInt() < 0) { //reservation invalidated return false; } if (model.getClock().now() > getExpiryDate()) { //reservation expired return false; } return true; } bool Reservation::matches(unsigned int connectorId) { return (int) connectorId == connectorIdInt->getInt(); } bool Reservation::matches(const char *idTag, const char *parentIdTag) { if (idTag == nullptr && parentIdTag == nullptr) { return true; } if (idTag && !strcmp(idTag, idTagString->getString())) { return true; } if (parentIdTag && !strcmp(parentIdTag, parentIdTagString->getString())) { return true; } return false; } int Reservation::getConnectorId() { return connectorIdInt->getInt(); } Timestamp& Reservation::getExpiryDate() { if (expiryDate == MIN_TIME && *expiryDateRawString->getString()) { expiryDate.setTime(expiryDateRawString->getString()); } return expiryDate; } const char *Reservation::getIdTag() { return idTagString->getString(); } int Reservation::getReservationId() { return reservationIdInt->getInt(); } const char *Reservation::getParentIdTag() { return parentIdTagString->getString(); } void Reservation::update(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag) { reservationIdInt->setInt(reservationId); connectorIdInt->setInt((int) connectorId); this->expiryDate = expiryDate; char expiryDate_cstr [JSONDATE_LENGTH + 1]; if (this->expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1)) { expiryDateRawString->setString(expiryDate_cstr); } idTagString->setString(idTag); parentIdTagString->setString(parentIdTag); configuration_save(); } void Reservation::clear() { connectorIdInt->setInt(-1); expiryDate = MIN_TIME; expiryDateRawString->setString(""); idTagString->setString(""); reservationIdInt->setInt(-1); parentIdTagString->setString(""); configuration_save(); } #endif //MO_ENABLE_RESERVATION ================================================ FILE: src/MicroOcpp/Model/Reservation/Reservation.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_RESERVATION_H #define MO_RESERVATION_H #include #if MO_ENABLE_RESERVATION #include #include #include #ifndef RESERVATION_FN #define RESERVATION_FN (MO_FILENAME_PREFIX "reservations.jsn") #endif #define MO_RESERVATION_CID_KEY "cid_" #define MO_RESERVATION_EXPDATE_KEY "expdt_" #define MO_RESERVATION_IDTAG_KEY "idt_" #define MO_RESERVATION_RESID_KEY "rsvid_" #define MO_RESERVATION_PARENTID_KEY "pidt_" namespace MicroOcpp { class Model; class Reservation : public MemoryManaged { private: Model& model; const unsigned int slot; std::shared_ptr connectorIdInt; char connectorIdKey [sizeof(MO_RESERVATION_CID_KEY "xxx") + 1]; //"xxx" = placeholder for digits std::shared_ptr expiryDateRawString; char expiryDateRawKey [sizeof(MO_RESERVATION_EXPDATE_KEY "xxx") + 1]; Timestamp expiryDate = MIN_TIME; std::shared_ptr idTagString; char idTagKey [sizeof(MO_RESERVATION_IDTAG_KEY "xxx") + 1]; std::shared_ptr reservationIdInt; char reservationIdKey [sizeof(MO_RESERVATION_RESID_KEY "xxx") + 1]; std::shared_ptr parentIdTagString; char parentIdTagKey [sizeof(MO_RESERVATION_PARENTID_KEY "xxx") + 1]; public: Reservation(Model& model, unsigned int slot); Reservation(const Reservation&) = delete; Reservation(Reservation&&) = delete; Reservation& operator=(const Reservation&) = delete; ~Reservation(); bool isActive(); //if this object contains a valid, unexpired reservation bool matches(unsigned int connectorId); bool matches(const char *idTag, const char *parentIdTag = nullptr); //idTag == parentIdTag == nullptr -> return True int getConnectorId(); Timestamp& getExpiryDate(); const char *getIdTag(); int getReservationId(); const char *getParentIdTag(); void update(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag = nullptr); void clear(); }; } #endif //MO_ENABLE_RESERVATION #endif ================================================ FILE: src/MicroOcpp/Model/Reservation/ReservationService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_RESERVATION #include #include #include #include #include #include #include #include using namespace MicroOcpp; ReservationService::ReservationService(Context& context, unsigned int numConnectors) : MemoryManaged("v16.Reservation.ReservationService"), context(context), maxReservations((int) numConnectors - 1), reservations(makeVector>(getMemoryTag())) { if (maxReservations > 0) { reservations.reserve((size_t) maxReservations); for (int i = 0; i < maxReservations; i++) { reservations.emplace_back(new Reservation(context.getModel(), i)); } } reserveConnectorZeroSupportedBool = declareConfiguration("ReserveConnectorZeroSupported", true, CONFIGURATION_VOLATILE, true); context.getOperationRegistry().registerOperation("CancelReservation", [this] () { return new Ocpp16::CancelReservation(*this);}); context.getOperationRegistry().registerOperation("ReserveNow", [&context] () { return new Ocpp16::ReserveNow(context.getModel());}); } void ReservationService::loop() { //check if to end reservations for (auto& reservation : reservations) { if (!reservation->isActive()) { continue; } if (auto connector = context.getModel().getConnector(reservation->getConnectorId())) { //check if connector went inoperative auto cStatus = connector->getStatus(); if (cStatus == ChargePointStatus_Faulted || cStatus == ChargePointStatus_Unavailable) { reservation->clear(); continue; } //check if other tx started at this connector (e.g. due to RemoteStartTransaction) if (connector->getTransaction() && connector->getTransaction()->isAuthorized()) { reservation->clear(); continue; } } //check if tx with same idTag or reservationId has started for (unsigned int cId = 1; cId < context.getModel().getNumConnectors(); cId++) { auto& transaction = context.getModel().getConnector(cId)->getTransaction(); if (transaction && transaction->isAuthorized()) { const char *cIdTag = transaction->getIdTag(); if (transaction->getReservationId() == reservation->getReservationId() || (cIdTag && !strcmp(cIdTag, reservation->getIdTag()))) { reservation->clear(); break; } } } } } Reservation *ReservationService::getReservation(unsigned int connectorId) { if (connectorId == 0) { MO_DBG_DEBUG("tried to fetch connectorId 0"); return nullptr; //cannot fetch for connectorId 0 because multiple reservations are possible at a time } for (auto& reservation : reservations) { if (reservation->isActive() && reservation->matches(connectorId)) { return reservation.get(); } } return nullptr; } Reservation *ReservationService::getReservation(const char *idTag, const char *parentIdTag) { if (idTag == nullptr) { MO_DBG_ERR("invalid input"); return nullptr; } Reservation *connectorReservation = nullptr; for (auto& reservation : reservations) { if (!reservation->isActive()) { continue; } //TODO check for parentIdTag if (reservation->matches(idTag, parentIdTag)) { if (reservation->getConnectorId() == 0) { return reservation.get(); //reservation at connectorId 0 has higher priority } else { connectorReservation = reservation.get(); } } } return connectorReservation; } Reservation *ReservationService::getReservation(unsigned int connectorId, const char *idTag, const char *parentIdTag) { //is connector blocked by a reservation? if (auto reservation = getReservation(connectorId)) { //connector has reservation -> will always be the prevailing reservation return reservation; } //is there any reservation at this charge point for idTag? if (idTag) { if (auto reservation = getReservation(idTag, parentIdTag)) { //yes, can use reservation with different connectorId return reservation; } } if (reserveConnectorZeroSupportedBool && !reserveConnectorZeroSupportedBool->getBool()) { //no connectorZero check - all done MO_DBG_DEBUG("no reservation"); return nullptr; } //connectorZero check Reservation *blockingReservation = nullptr; //any reservation which blocks this connector now //Check if there are enough free connectors to satisfy all reservations at connectorId 0 unsigned int unspecifiedReservations = 0; for (auto& reservation : reservations) { if (reservation->isActive() && reservation->getConnectorId() == 0) { unspecifiedReservations++; blockingReservation = reservation.get(); } } unsigned int availableCount = 0; for (unsigned int cId = 1; cId < context.getModel().getNumConnectors(); cId++) { if (cId == connectorId) { //don't count this connector continue; } if (auto connector = context.getModel().getConnector(cId)) { if (connector->getStatus() == ChargePointStatus_Available) { availableCount++; } } } if (availableCount >= unspecifiedReservations) { //enough other connectors available to satisfy all reservations return nullptr; } else { //not sufficient connectors for all reservations after this action return blockingReservation; } } Reservation *ReservationService::getReservationById(int reservationId) { for (auto& reservation : reservations) { if (reservation->isActive() && reservation->getReservationId() == reservationId) { return reservation.get(); } } return nullptr; } bool ReservationService::updateReservation(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag) { if (auto reservation = getReservationById(reservationId)) { if (getReservation(connectorId) && getReservation(connectorId) != reservation && getReservation(connectorId)->isActive()) { MO_DBG_DEBUG("found blocking reservation at connectorId %u", connectorId); return false; //cannot transfer reservation to other connector with existing reservation } reservation->update(reservationId, connectorId, expiryDate, idTag, parentIdTag); return true; } // Alternative condition: avoids that one idTag can make two reservations at a time. The specification doesn't // mention that double-reservations should be possible but it seems to mean it. if (auto reservation = getReservation(connectorId, nullptr, nullptr)) { // payload["idTag"], // payload.containsKey("parentIdTag") ? payload["parentIdTag"] : nullptr)) { // if (auto reservation = getReservation(payload["connectorId"].as())) { MO_DBG_DEBUG("found blocking reservation at connectorId %u", reservation->getConnectorId()); (void)reservation; return false; } //update free reservation slot for (auto& reservation : reservations) { if (!reservation->isActive()) { reservation->update(reservationId, connectorId, expiryDate, idTag, parentIdTag); return true; } } MO_DBG_ERR("error finding blocking reservation"); return false; } #endif //MO_ENABLE_RESERVATION ================================================ FILE: src/MicroOcpp/Model/Reservation/ReservationService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_RESERVATIONSERVICE_H #define MO_RESERVATIONSERVICE_H #include #if MO_ENABLE_RESERVATION #include #include #include namespace MicroOcpp { class Context; class ReservationService : public MemoryManaged { private: Context& context; const int maxReservations; // = number of physical connectors Vector> reservations; std::shared_ptr reserveConnectorZeroSupportedBool; public: ReservationService(Context& context, unsigned int numConnectors); void loop(); Reservation *getReservation(unsigned int connectorId); //by connectorId Reservation *getReservation(const char *idTag, const char *parentIdTag = nullptr); //by idTag /* * Get prevailing reservation for a charging session authorized by idTag/parentIdTag at connectorId. * returns nullptr if there is no reservation in question * returns a reservation if applicable. Caller must check if idTag/parentIdTag match before starting a transaction */ Reservation *getReservation(unsigned int connectorId, const char *idTag, const char *parentIdtag = nullptr); Reservation *getReservationById(int reservationId); bool updateReservation(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag = nullptr); }; } #endif //MO_ENABLE_RESERVATION #endif ================================================ FILE: src/MicroOcpp/Model/Reset/ResetDefs.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_RESETDEFS_H #define MO_RESETDEFS_H #include #if MO_ENABLE_V201 typedef enum ResetType { ResetType_Immediate, ResetType_OnIdle } ResetType; typedef enum ResetStatus { ResetStatus_Accepted, ResetStatus_Rejected, ResetStatus_Scheduled } ResetStatus; #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Model/Reset/ResetService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef MO_RESET_DELAY #define MO_RESET_DELAY 10000 #endif using namespace MicroOcpp; ResetService::ResetService(Context& context) : MemoryManaged("v16.Reset.ResetService"), context(context) { resetRetriesInt = declareConfiguration("ResetRetries", 2); registerConfigurationValidator("ResetRetries", VALIDATE_UNSIGNED_INT); context.getOperationRegistry().registerOperation("Reset", [&context] () { return new Ocpp16::Reset(context.getModel());}); } ResetService::~ResetService() { } void ResetService::loop() { if (outstandingResetRetries > 0 && mocpp_tick_ms() - t_resetRetry >= MO_RESET_DELAY) { t_resetRetry = mocpp_tick_ms(); outstandingResetRetries--; if (executeReset) { MO_DBG_INFO("Reset device"); executeReset(isHardReset); } else { MO_DBG_ERR("No Reset function set! Abort"); outstandingResetRetries = 0; } if (outstandingResetRetries <= 0) { MO_DBG_ERR("Reset device failure. Abort"); ChargePointStatus cpStatus = ChargePointStatus_UNDEFINED; if (context.getModel().getNumConnectors() > 0) { cpStatus = context.getModel().getConnector(0)->getStatus(); } auto statusNotification = makeRequest(new Ocpp16::StatusNotification( 0, cpStatus, //will be determined in StatusNotification::initiate context.getModel().getClock().now(), "ResetFailure")); statusNotification->setTimeout(60000); context.initiateRequest(std::move(statusNotification)); } } } void ResetService::setPreReset(std::function preReset) { this->preReset = preReset; } std::function ResetService::getPreReset() { return this->preReset; } void ResetService::setExecuteReset(std::function executeReset) { this->executeReset = executeReset; } std::function ResetService::getExecuteReset() { return this->executeReset; } void ResetService::initiateReset(bool isHard) { isHardReset = isHard; outstandingResetRetries = 1 + resetRetriesInt->getInt(); //one initial try + no. of retries if (outstandingResetRetries > 5) { MO_DBG_ERR("no. of reset trials exceeds 5"); outstandingResetRetries = 5; } t_resetRetry = mocpp_tick_ms(); } #if MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) std::function MicroOcpp::makeDefaultResetFn() { return [] (bool isHard) { MO_DBG_DEBUG("Perform ESP reset"); ESP.restart(); }; } #endif //MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) #if MO_ENABLE_V201 namespace MicroOcpp { namespace Ocpp201 { ResetService::ResetService(Context& context) : MemoryManaged("v201.Reset.ResetService"), context(context), evses(makeVector(getMemoryTag())) { auto varService = context.getModel().getVariableService(); resetRetriesInt = varService->declareVariable("OCPPCommCtrlr", "ResetRetries", 0); context.getOperationRegistry().registerOperation("Reset", [this] () { return new Ocpp201::Reset(*this);}); } ResetService::~ResetService() { } ResetService::Evse::Evse(Context& context, ResetService& resetService, unsigned int evseId) : context(context), resetService(resetService), evseId(evseId) { auto varService = context.getModel().getVariableService(); varService->declareVariable(ComponentId("EVSE", evseId >= 1 ? evseId : -1), "AllowReset", true, Variable::Mutability::ReadOnly, false); } void ResetService::Evse::loop() { if (outstandingResetRetries && awaitTxStop) { for (unsigned int eId = std::max(1U, evseId); eId < (evseId == 0 ? MO_NUM_EVSEID : evseId + 1); eId++) { //If evseId > 0, execute this block one time for evseId. If evseId == 0, then iterate over all evseIds > 0 auto txService = context.getModel().getTransactionService(); if (txService && txService->getEvse(eId) && txService->getEvse(eId)->getTransaction()) { auto tx = txService->getEvse(eId)->getTransaction(); if (!tx->stopped) { // wait until tx stopped return; } } } awaitTxStop = false; MO_DBG_INFO("Reset - tx stopped"); t_resetRetry = mocpp_tick_ms(); // wait for some more time until final reset } if (outstandingResetRetries && mocpp_tick_ms() - t_resetRetry >= MO_RESET_DELAY) { t_resetRetry = mocpp_tick_ms(); outstandingResetRetries--; MO_DBG_INFO("Reset device"); bool success = executeReset(); if (success) { outstandingResetRetries = 0; if (evseId != 0) { //Set this EVSE Available again if (auto connector = context.getModel().getConnector(evseId)) { connector->setAvailabilityVolatile(true); } } } else if (!outstandingResetRetries) { MO_DBG_ERR("Reset device failure"); if (evseId == 0) { //Set all EVSEs Available again for (unsigned int cId = 0; cId < context.getModel().getNumConnectors(); cId++) { auto connector = context.getModel().getConnector(cId); connector->setAvailabilityVolatile(true); } } else { //Set only this EVSE Available if (auto connector = context.getModel().getConnector(evseId)) { connector->setAvailabilityVolatile(true); } } } } } ResetService::Evse *ResetService::getEvse(unsigned int evseId) { for (size_t i = 0; i < evses.size(); i++) { if (evses[i].evseId == evseId) { return &evses[i]; } } return nullptr; } ResetService::Evse *ResetService::getOrCreateEvse(unsigned int evseId) { if (auto evse = getEvse(evseId)) { return evse; } if (evseId >= MO_NUM_EVSEID) { MO_DBG_ERR("evseId out of bound"); return nullptr; } evses.emplace_back(context, *this, evseId); return &evses.back(); } void ResetService::loop() { for (Evse& evse : evses) { evse.loop(); } } void ResetService::setNotifyReset(std::function notifyReset, unsigned int evseId) { Evse *evse = getOrCreateEvse(evseId); if (!evse) { MO_DBG_ERR("evseId not found"); return; } evse->notifyReset = notifyReset; } std::function ResetService::getNotifyReset(unsigned int evseId) { Evse *evse = getOrCreateEvse(evseId); if (!evse) { MO_DBG_ERR("evseId not found"); return nullptr; } return evse->notifyReset; } void ResetService::setExecuteReset(std::function executeReset, unsigned int evseId) { Evse *evse = getOrCreateEvse(evseId); if (!evse) { MO_DBG_ERR("evseId not found"); return; } evse->executeReset = executeReset; } std::function ResetService::getExecuteReset(unsigned int evseId) { Evse *evse = getOrCreateEvse(evseId); if (!evse) { MO_DBG_ERR("evseId not found"); return nullptr; } return evse->executeReset; } ResetStatus ResetService::initiateReset(ResetType type, unsigned int evseId) { auto evse = getEvse(evseId); if (!evse) { MO_DBG_ERR("evseId not found"); return ResetStatus_Rejected; } if (!evse->executeReset) { MO_DBG_INFO("EVSE %u does not support Reset", evseId); return ResetStatus_Rejected; } //Check if EVSEs are ready for Reset for (unsigned int eId = evseId; eId < (evseId == 0 ? MO_NUM_EVSEID : evseId + 1); eId++) { //If evseId > 0, execute this block one time for evseId. If evseId == 0, then iterate over all evseIds if (auto it = getEvse(eId)) { if (it->notifyReset && !it->notifyReset(type)) { MO_DBG_INFO("EVSE %u not able to Reset", evseId); return ResetStatus_Rejected; } } } //Set EVSEs Unavailable if (evseId == 0) { //Set all EVSEs Unavailable for (unsigned int cId = 0; cId < context.getModel().getNumConnectors(); cId++) { auto connector = context.getModel().getConnector(cId); connector->setAvailabilityVolatile(false); } } else { //Set this EVSE Unavailable if (auto connector = context.getModel().getConnector(evseId)) { connector->setAvailabilityVolatile(false); } } bool scheduled = false; //Tx-related behavior: if immediate Reset, stop txs; otherwise schedule Reset for (unsigned int eId = std::max(1U, evseId); eId < (evseId == 0 ? MO_NUM_EVSEID : evseId + 1); eId++) { //If evseId > 0, execute this block one time for evseId. If evseId == 0, then iterate over all evseIds > 0 auto txService = context.getModel().getTransactionService(); if (txService && txService->getEvse(eId) && txService->getEvse(eId)->getTransaction()) { auto tx = txService->getEvse(eId)->getTransaction(); if (tx->active) { //Tx in progress. Check behavior if (type == ResetType_Immediate) { txService->getEvse(eId)->abortTransaction(Transaction::StoppedReason::ImmediateReset, TransactionEventTriggerReason::ResetCommand); } else { scheduled = true; break; } } } } //Actually engage Reset if (resetRetriesInt->getInt() >= 5) { MO_DBG_ERR("no. of reset trials exceeds 5"); evse->outstandingResetRetries = 5; } else { evse->outstandingResetRetries = 1 + resetRetriesInt->getInt(); //one initial try + no. of retries } evse->t_resetRetry = mocpp_tick_ms(); evse->awaitTxStop = scheduled; return scheduled ? ResetStatus_Scheduled : ResetStatus_Accepted; } } //namespace MicroOcpp } //namespace Ocpp201 #endif //MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Model/Reset/ResetService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_RESETSERVICE_H #define MO_RESETSERVICE_H #include #include #include #include #include namespace MicroOcpp { class Context; class ResetService : public MemoryManaged { private: Context& context; std::function preReset; //true: reset is possible; false: reject reset; Await: need more time to determine std::function executeReset; //please disconnect WebSocket (MO remains initialized), shut down device and restart with normal initialization routine; on failure reconnect WebSocket unsigned int outstandingResetRetries = 0; //0 = do not reset device bool isHardReset = false; unsigned long t_resetRetry; std::shared_ptr resetRetriesInt; public: ResetService(Context& context); ~ResetService(); void loop(); void setPreReset(std::function preReset); std::function getPreReset(); void setExecuteReset(std::function executeReset); std::function getExecuteReset(); void initiateReset(bool isHard); }; } //end namespace MicroOcpp #if MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) namespace MicroOcpp { std::function makeDefaultResetFn(); } #endif //MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) #if MO_ENABLE_V201 namespace MicroOcpp { class Variable; namespace Ocpp201 { class ResetService : public MemoryManaged { private: Context& context; struct Evse { Context& context; ResetService& resetService; const unsigned int evseId; std::function notifyReset; //notify firmware about a Reset command. Return true if Reset is okay; false if Reset cannot be executed std::function executeReset; //execute Reset of connector. Return true if Reset will be executed; false if there is a failure to Reset unsigned int outstandingResetRetries = 0; //0 = do not reset device unsigned long t_resetRetry; bool awaitTxStop = false; Evse(Context& context, ResetService& resetService, unsigned int evseId); void loop(); }; Vector evses; Evse *getEvse(unsigned int connectorId); Evse *getOrCreateEvse(unsigned int connectorId); Variable *resetRetriesInt = nullptr; public: ResetService(Context& context); ~ResetService(); void loop(); void setNotifyReset(std::function notifyReset, unsigned int evseId = 0); std::function getNotifyReset(unsigned int evseId = 0); void setExecuteReset(std::function executeReset, unsigned int evseId = 0); std::function getExecuteReset(unsigned int evseId = 0); ResetStatus initiateReset(ResetType type, unsigned int evseId = 0); }; } //namespace Ocpp201 } //namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using namespace MicroOcpp; ChargeRate MicroOcpp::chargeRate_min(const ChargeRate& a, const ChargeRate& b) { ChargeRate res; res.power = std::min(a.power, b.power); res.current = std::min(a.current, b.current); res.nphases = std::min(a.nphases, b.nphases); return res; } ChargingSchedule::ChargingSchedule() : MemoryManaged("v16.SmartCharging.SmartChargingModel"), chargingSchedulePeriod{makeVector(getMemoryTag())} { } bool ChargingSchedule::calculateLimit(const Timestamp &t, const Timestamp &startOfCharging, ChargeRate& limit, Timestamp& nextChange) { Timestamp basis = Timestamp(); //point in time to which schedule-related times are relative switch (chargingProfileKind) { case (ChargingProfileKindType::Absolute): //check if schedule is not valid yet but begins in future if (startSchedule > t) { //not valid YET nextChange = std::min(nextChange, startSchedule); return false; } //If charging profile is absolute, prefer startSchedule as basis. If absent, use chargingStart instead. If absent, no //behaviour is defined if (startSchedule > MIN_TIME) { basis = startSchedule; } else if (startOfCharging > MIN_TIME && startOfCharging < t) { basis = startOfCharging; } else { MO_DBG_ERR("Absolute profile, but neither startSchedule, nor start of charging are set. Undefined behavior, abort"); return false; } break; case (ChargingProfileKindType::Recurring): if (recurrencyKind == RecurrencyKindType::Daily) { basis = t - ((t - startSchedule) % (24 * 3600)); nextChange = std::min(nextChange, basis + (24 * 3600)); //constrain nextChange to basis + one day } else if (recurrencyKind == RecurrencyKindType::Weekly) { basis = t - ((t - startSchedule) % (7 * 24 * 3600)); nextChange = std::min(nextChange, basis + (7 * 24 * 3600)); } else { MO_DBG_ERR("Recurring ChargingProfile but no RecurrencyKindType set. Undefined behavior, assume 'Daily'"); basis = t - ((t - startSchedule) % (24 * 3600)); nextChange = std::min(nextChange, basis + (24 * 3600)); } break; case (ChargingProfileKindType::Relative): //assumed, that it is relative to start of charging //start of charging must be before t or equal to t if (startOfCharging > t) { //Relative charging profiles only work with a currently active charging session which is not the case here return false; } basis = startOfCharging; break; } if (t < basis) { //check for error MO_DBG_ERR("time basis is smaller than t, but t must be >= basis"); return false; } int t_toBasis = t - basis; if (duration > 0){ //duration is set //check if duration is exceeded and if yes, abort limit algorithm //if no, the duration is an upper limit for the validity of the schedule if (t_toBasis >= duration) { //"duration" is given relative to basis return false; } else { nextChange = std::min(nextChange, basis + duration); } } /* * Work through the ChargingProfilePeriods here. If the right period was found, assign the limit parameter from it * and make nextChange equal the beginning of the following period. If the right period is the last one, nextChange * will remain the time determined before. */ float limit_res = -1.0f; //If limit_res is still -1 after the loop, the limit algorithm failed int nphases_res = -1; for (auto period = chargingSchedulePeriod.begin(); period != chargingSchedulePeriod.end(); period++) { if (period->startPeriod > t_toBasis) { // found the first period that comes after t_toBasis. nextChange = basis + period->startPeriod; nextChange = std::min(nextChange, basis + period->startPeriod); break; //The currently valid limit was set the iteration before } limit_res = period->limit; nphases_res = period->numberPhases; } if (limit_res >= 0.0f) { limit_res = std::max(limit_res, minChargingRate); if (chargingRateUnit == ChargingRateUnitType::Amp) { limit.current = limit_res; } else { limit.power = limit_res; } limit.nphases = nphases_res; return true; } else { return false; //No limit was found. Either there is no ChargingProfilePeriod, or each period begins after t_toBasis } } bool ChargingSchedule::toJson(JsonDoc& doc) { size_t capacity = 0; capacity += JSON_OBJECT_SIZE(5); //no of fields of ChargingSchedule capacity += JSONDATE_LENGTH + 1; //startSchedule capacity += JSON_ARRAY_SIZE(chargingSchedulePeriod.size()) + chargingSchedulePeriod.size() * JSON_OBJECT_SIZE(3); doc = initJsonDoc("v16.SmartCharging.ChargingSchedule", capacity); if (duration >= 0) { doc["duration"] = duration; } char startScheduleJson [JSONDATE_LENGTH + 1] = {'\0'}; startSchedule.toJsonString(startScheduleJson, JSONDATE_LENGTH + 1); doc["startSchedule"] = startScheduleJson; doc["chargingRateUnit"] = chargingRateUnit == (ChargingRateUnitType::Amp) ? "A" : "W"; JsonArray periodArray = doc.createNestedArray("chargingSchedulePeriod"); for (auto period = chargingSchedulePeriod.begin(); period != chargingSchedulePeriod.end(); period++) { JsonObject entry = periodArray.createNestedObject(); entry["startPeriod"] = period->startPeriod; entry["limit"] = period->limit; if (period->numberPhases != 3) { entry["numberPhases"] = period->numberPhases; } } if (minChargingRate >= 0) { doc["minChargeRate"] = minChargingRate; } return true; } void ChargingSchedule::printSchedule(){ char tmp[JSONDATE_LENGTH + 1] = {'\0'}; startSchedule.toJsonString(tmp, JSONDATE_LENGTH + 1); MO_CONSOLE_PRINTF(" > CHARGING SCHEDULE:\n" \ " > duration: %i\n" \ " > startSchedule: %s\n" \ " > chargingRateUnit: %s\n" \ " > minChargingRate: %f\n", duration, tmp, chargingRateUnit == (ChargingRateUnitType::Amp) ? "A" : chargingRateUnit == (ChargingRateUnitType::Watt) ? "W" : "Error", minChargingRate); for (auto period = chargingSchedulePeriod.begin(); period != chargingSchedulePeriod.end(); period++) { MO_CONSOLE_PRINTF(" > CHARGING SCHEDULE PERIOD:\n" \ " > startPeriod: %i\n" \ " > limit: %f\n" \ " > numberPhases: %i\n", period->startPeriod, period->limit, period->numberPhases); } } ChargingProfile::ChargingProfile() : MemoryManaged("v16.SmartCharging.ChargingProfile") { } bool ChargingProfile::calculateLimit(const Timestamp &t, const Timestamp &startOfCharging, ChargeRate& limit, Timestamp& nextChange){ if (t > validTo && validTo > MIN_TIME) { return false; //no limit defined } if (t < validFrom) { nextChange = std::min(nextChange, validFrom); return false; //no limit defined } return chargingSchedule.calculateLimit(t, startOfCharging, limit, nextChange); } bool ChargingProfile::calculateLimit(const Timestamp &t, ChargeRate& limit, Timestamp& nextChange){ return calculateLimit(t, MIN_TIME, limit, nextChange); } int ChargingProfile::getChargingProfileId() { return chargingProfileId; } int ChargingProfile::getTransactionId() { return transactionId; } int ChargingProfile::getStackLevel(){ return stackLevel; } ChargingProfilePurposeType ChargingProfile::getChargingProfilePurpose(){ return chargingProfilePurpose; } bool ChargingProfile::toJson(JsonDoc& doc) { auto chargingScheduleDoc = initJsonDoc("v16.SmartCharging.ChargingSchedule"); if (!chargingSchedule.toJson(chargingScheduleDoc)) { return false; } doc = initJsonDoc("v16.SmartCharging.ChargingProfile", JSON_OBJECT_SIZE(9) + //no. of fields in ChargingProfile 2 * (JSONDATE_LENGTH + 1) + //validFrom and validTo chargingScheduleDoc.memoryUsage()); //nested JSON object doc["chargingProfileId"] = chargingProfileId; if (transactionId >= 0) { doc["transactionId"] = transactionId; } doc["stackLevel"] = stackLevel; switch (chargingProfilePurpose) { case (ChargingProfilePurposeType::ChargePointMaxProfile): doc["chargingProfilePurpose"] = "ChargePointMaxProfile"; break; case (ChargingProfilePurposeType::TxDefaultProfile): doc["chargingProfilePurpose"] = "TxDefaultProfile"; break; case (ChargingProfilePurposeType::TxProfile): doc["chargingProfilePurpose"] = "TxProfile"; break; } switch (chargingProfileKind) { case (ChargingProfileKindType::Absolute): doc["chargingProfileKind"] = "Absolute"; break; case (ChargingProfileKindType::Recurring): doc["chargingProfileKind"] = "Recurring"; break; case (ChargingProfileKindType::Relative): doc["chargingProfileKind"] = "Relative"; break; } switch (recurrencyKind) { case (RecurrencyKindType::Daily): doc["recurrencyKind"] = "Daily"; break; case (RecurrencyKindType::Weekly): doc["recurrencyKind"] = "Weekly"; break; default: break; } char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; if (validFrom > MIN_TIME) { if (!validFrom.toJsonString(timeStr, JSONDATE_LENGTH + 1)) { MO_DBG_ERR("serialization error"); return false; } doc["validFrom"] = timeStr; } if (validTo > MIN_TIME) { if (!validTo.toJsonString(timeStr, JSONDATE_LENGTH + 1)) { MO_DBG_ERR("serialization error"); return false; } doc["validTo"] = timeStr; } doc["chargingSchedule"] = chargingScheduleDoc; return true; } void ChargingProfile::printProfile(){ char tmp[JSONDATE_LENGTH + 1] = {'\0'}; validFrom.toJsonString(tmp, JSONDATE_LENGTH + 1); char tmp2[JSONDATE_LENGTH + 1] = {'\0'}; validTo.toJsonString(tmp2, JSONDATE_LENGTH + 1); MO_CONSOLE_PRINTF(" > CHARGING PROFILE:\n" \ " > chargingProfileId: %i\n" \ " > transactionId: %i\n" \ " > stackLevel: %i\n" \ " > chargingProfilePurpose: %s\n", chargingProfileId, transactionId, stackLevel, chargingProfilePurpose == (ChargingProfilePurposeType::ChargePointMaxProfile) ? "ChargePointMaxProfile" : chargingProfilePurpose == (ChargingProfilePurposeType::TxDefaultProfile) ? "TxDefaultProfile" : chargingProfilePurpose == (ChargingProfilePurposeType::TxProfile) ? "TxProfile" : "Error" ); MO_CONSOLE_PRINTF( " > chargingProfileKind: %s\n" \ " > recurrencyKind: %s\n" \ " > validFrom: %s\n" \ " > validTo: %s\n", chargingProfileKind == (ChargingProfileKindType::Absolute) ? "Absolute" : chargingProfileKind == (ChargingProfileKindType::Recurring) ? "Recurring" : chargingProfileKind == (ChargingProfileKindType::Relative) ? "Relative" : "Error", recurrencyKind == (RecurrencyKindType::Daily) ? "Daily" : recurrencyKind == (RecurrencyKindType::Weekly) ? "Weekly" : recurrencyKind == (RecurrencyKindType::NOT_SET) ? "NOT_SET" : "Error", tmp, tmp2 ); chargingSchedule.printSchedule(); } namespace MicroOcpp { bool loadChargingSchedulePeriod(JsonObject& json, ChargingSchedulePeriod& out) { int startPeriod = json["startPeriod"] | -1; if (startPeriod >= 0) { out.startPeriod = startPeriod; } else { MO_DBG_WARN("format violation"); return false; } float limit = json["limit"] | -1.f; if (limit >= 0.f) { out.limit = limit; } else { MO_DBG_WARN("format violation"); return false; } if (json.containsKey("numberPhases")) { int numberPhases = json["numberPhases"]; if (numberPhases >= 0 && numberPhases <= 3) { out.numberPhases = numberPhases; } else { MO_DBG_WARN("format violation"); return false; } } return true; } } //end namespace MicroOcpp std::unique_ptr MicroOcpp::loadChargingProfile(JsonObject& json) { auto res = std::unique_ptr(new ChargingProfile()); int chargingProfileId = json["chargingProfileId"] | -1; if (chargingProfileId >= 0) { res->chargingProfileId = chargingProfileId; } else { MO_DBG_WARN("format violation"); return nullptr; } int transactionId = json["transactionId"] | -1; if (transactionId >= 0) { res->transactionId = transactionId; } int stackLevel = json["stackLevel"] | -1; if (stackLevel >= 0 && stackLevel <= MO_ChargeProfileMaxStackLevel) { res->stackLevel = stackLevel; } else { MO_DBG_WARN("format violation"); return nullptr; } const char *chargingProfilePurposeStr = json["chargingProfilePurpose"] | "Invalid"; if (!strcmp(chargingProfilePurposeStr, "ChargePointMaxProfile")) { res->chargingProfilePurpose = ChargingProfilePurposeType::ChargePointMaxProfile; } else if (!strcmp(chargingProfilePurposeStr, "TxDefaultProfile")) { res->chargingProfilePurpose = ChargingProfilePurposeType::TxDefaultProfile; } else if (!strcmp(chargingProfilePurposeStr, "TxProfile")) { res->chargingProfilePurpose = ChargingProfilePurposeType::TxProfile; } else { MO_DBG_WARN("format violation"); return nullptr; } const char *chargingProfileKindStr = json["chargingProfileKind"] | "Invalid"; if (!strcmp(chargingProfileKindStr, "Absolute")) { res->chargingProfileKind = ChargingProfileKindType::Absolute; } else if (!strcmp(chargingProfileKindStr, "Recurring")) { res->chargingProfileKind = ChargingProfileKindType::Recurring; } else if (!strcmp(chargingProfileKindStr, "Relative")) { res->chargingProfileKind = ChargingProfileKindType::Relative; } else { MO_DBG_WARN("format violation"); return nullptr; } const char *recurrencyKindStr = json["recurrencyKind"] | "Invalid"; if (!strcmp(recurrencyKindStr, "Daily")) { res->recurrencyKind = RecurrencyKindType::Daily; } else if (!strcmp(recurrencyKindStr, "Weekly")) { res->recurrencyKind = RecurrencyKindType::Weekly; } MO_DBG_DEBUG("Deserialize JSON: chargingProfileId=%i, chargingProfilePurpose=%s, recurrencyKind=%s", chargingProfileId, chargingProfilePurposeStr, recurrencyKindStr); if (json.containsKey("validFrom")) { if (!res->validFrom.setTime(json["validFrom"] | "Invalid")) { //non-success MO_DBG_WARN("datetime format violation, expect format like 2022-02-01T20:53:32.486Z"); return nullptr; } } else { res->validFrom = MIN_TIME; } if (json.containsKey("validTo")) { if (!res->validTo.setTime(json["validTo"] | "Invalid")) { //non-success MO_DBG_WARN("datetime format violation, expect format like 2022-02-01T20:53:32.486Z"); return nullptr; } } else { res->validTo = MIN_TIME; } JsonObject scheduleJson = json["chargingSchedule"]; ChargingSchedule& schedule = res->chargingSchedule; auto success = loadChargingSchedule(scheduleJson, schedule); if (!success) { return nullptr; } //duplicate some fields to chargingSchedule to simplify the max charge rate calculation schedule.chargingProfileKind = res->chargingProfileKind; schedule.recurrencyKind = res->recurrencyKind; return res; } bool MicroOcpp::loadChargingSchedule(JsonObject& json, ChargingSchedule& out) { if (json.containsKey("duration")) { int duration = json["duration"] | -1; if (duration >= 0) { out.duration = duration; } else { MO_DBG_WARN("format violation"); return false; } } if (json.containsKey("startSchedule")) { if (!out.startSchedule.setTime(json["startSchedule"] | "Invalid")) { //non-success MO_DBG_WARN("datetime format violation, expect format like 2022-02-01T20:53:32.486Z"); return false; } } else { out.startSchedule = MIN_TIME; } const char *unit = json["chargingRateUnit"] | "_Undefined"; if (unit[0] == 'a' || unit[0] == 'A') { out.chargingRateUnit = ChargingRateUnitType::Amp; } else if (unit[0] == 'w' || unit[0] == 'W') { out.chargingRateUnit = ChargingRateUnitType::Watt; } else { MO_DBG_WARN("format violation"); return false; } JsonArray periodJsonArray = json["chargingSchedulePeriod"]; if (periodJsonArray.size() < 1) { MO_DBG_WARN("format violation"); return false; } if (periodJsonArray.size() > MO_ChargingScheduleMaxPeriods) { MO_DBG_WARN("exceed ChargingScheduleMaxPeriods"); return false; } for (JsonObject periodJson : periodJsonArray) { out.chargingSchedulePeriod.emplace_back(); if (!loadChargingSchedulePeriod(periodJson, out.chargingSchedulePeriod.back())) { return false; } } if (json.containsKey("minChargingRate")) { float minChargingRate = json["minChargingRate"]; if (minChargingRate >= 0.f) { out.minChargingRate = minChargingRate; } else { MO_DBG_WARN("format violation"); return false; } } return true; } ================================================ FILE: src/MicroOcpp/Model/SmartCharging/SmartChargingModel.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef SMARTCHARGINGMODEL_H #define SMARTCHARGINGMODEL_H #ifndef MO_ChargeProfileMaxStackLevel #define MO_ChargeProfileMaxStackLevel 8 #endif #ifndef MO_ChargingScheduleMaxPeriods #define MO_ChargingScheduleMaxPeriods 24 #endif #ifndef MO_MaxChargingProfilesInstalled #define MO_MaxChargingProfilesInstalled 10 #endif #include #include #include #include #include namespace MicroOcpp { enum class ChargingProfilePurposeType { ChargePointMaxProfile, TxDefaultProfile, TxProfile }; enum class ChargingProfileKindType { Absolute, Recurring, Relative }; enum class RecurrencyKindType { NOT_SET, //not part of OCPP 1.6 Daily, Weekly }; enum class ChargingRateUnitType { Watt, Amp }; struct ChargeRate { float power = std::numeric_limits::max(); float current = std::numeric_limits::max(); int nphases = std::numeric_limits::max(); bool operator==(const ChargeRate& rhs) { return power == rhs.power && current == rhs.current && nphases == rhs.nphases; } bool operator!=(const ChargeRate& rhs) { return !(*this == rhs); } }; //returns a new vector with the minimum of each component ChargeRate chargeRate_min(const ChargeRate& a, const ChargeRate& b); class ChargingSchedulePeriod { public: int startPeriod; float limit; int numberPhases = 3; }; class ChargingSchedule : public MemoryManaged { public: int duration = -1; Timestamp startSchedule; ChargingRateUnitType chargingRateUnit; Vector chargingSchedulePeriod; float minChargingRate = -1.0f; ChargingProfileKindType chargingProfileKind; //copied from ChargingProfile to increase cohesion of limit algorithms RecurrencyKindType recurrencyKind = RecurrencyKindType::NOT_SET; //copied from ChargingProfile to increase cohesion of limit algorithms ChargingSchedule(); /** * limit: output parameter * nextChange: output parameter * * returns if charging profile defines a limit at time t * if true, limit and nextChange will be set according to this Schedule * if false, only nextChange will be set */ bool calculateLimit(const Timestamp &t, const Timestamp &startOfCharging, ChargeRate& limit, Timestamp& nextChange); bool toJson(JsonDoc& out); /* * print on console */ void printSchedule(); }; class ChargingProfile : public MemoryManaged { public: int chargingProfileId = -1; int transactionId = -1; int stackLevel = 0; ChargingProfilePurposeType chargingProfilePurpose {ChargingProfilePurposeType::TxProfile}; ChargingProfileKindType chargingProfileKind {ChargingProfileKindType::Relative}; //copied to ChargingSchedule to increase cohesion of limit algorithms RecurrencyKindType recurrencyKind {RecurrencyKindType::NOT_SET}; // copied to ChargingSchedule to increase cohesion Timestamp validFrom; Timestamp validTo; ChargingSchedule chargingSchedule; ChargingProfile(); /** * limit: output parameter * nextChange: output parameter * * returns if charging profile defines a limit at time t * if true, limit and nextChange will be set according to this Schedule * if false, only nextChange will be set */ bool calculateLimit(const Timestamp &t, const Timestamp &startOfCharging, ChargeRate& limit, Timestamp& nextChange); /* * Simpler function if startOfCharging is not available. Caution: This likely will differ from function with startOfCharging */ bool calculateLimit(const Timestamp &t, ChargeRate& limit, Timestamp& nextChange); int getChargingProfileId(); int getTransactionId(); int getStackLevel(); ChargingProfilePurposeType getChargingProfilePurpose(); bool toJson(JsonDoc& out); /* * print on console */ void printProfile(); }; std::unique_ptr loadChargingProfile(JsonObject& json); bool loadChargingSchedule(JsonObject& json, ChargingSchedule& out); } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include using namespace::MicroOcpp; SmartChargingConnector::SmartChargingConnector(Model& model, std::shared_ptr filesystem, unsigned int connectorId, ProfileStack& ChargePointMaxProfile, ProfileStack& ChargePointTxDefaultProfile) : MemoryManaged("v16.SmartCharging.SmartChargingConnector"), model(model), filesystem{filesystem}, connectorId{connectorId}, ChargePointMaxProfile(ChargePointMaxProfile), ChargePointTxDefaultProfile(ChargePointTxDefaultProfile) { } SmartChargingConnector::~SmartChargingConnector() { } /* * limitOut: the calculated maximum charge rate at the moment, or the default value if no limit is defined * validToOut: The begin of the next SmartCharging restriction after time t */ void SmartChargingConnector::calculateLimit(const Timestamp &t, ChargeRate& limitOut, Timestamp& validToOut) { //initialize output parameters with the default values limitOut = ChargeRate(); validToOut = MAX_TIME; bool txLimitDefined = false; ChargeRate txLimit; //first, check if TxProfile is defined and limits charging for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { if (TxProfile[i] && ((trackTxRmtProfileId >= 0 && trackTxRmtProfileId == TxProfile[i]->getChargingProfileId()) || TxProfile[i]->getTransactionId() < 0 || trackTxId == TxProfile[i]->getTransactionId())) { ChargeRate crOut; bool defined = TxProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); if (defined) { txLimitDefined = true; txLimit = crOut; break; } } } //if no TxProfile limits charging, check the TxDefaultProfiles for this connector if (!txLimitDefined && trackTxStart < MAX_TIME) { for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { if (TxDefaultProfile[i]) { ChargeRate crOut; bool defined = TxDefaultProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); if (defined) { txLimitDefined = true; txLimit = crOut; break; } } } } //if no appropriate TxDefaultProfile is set for this connector, search in the general TxDefaultProfiles if (!txLimitDefined && trackTxStart < MAX_TIME) { for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { if (ChargePointTxDefaultProfile[i]) { ChargeRate crOut; bool defined = ChargePointTxDefaultProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); if (defined) { txLimitDefined = true; txLimit = crOut; break; } } } } ChargeRate cpLimit; //the calculated maximum charge rate is also limited by the ChargePointMaxProfiles for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { if (ChargePointMaxProfile[i]) { ChargeRate crOut; bool defined = ChargePointMaxProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); if (defined) { cpLimit = crOut; break; } } } //apply ChargePointMaxProfile value to calculated limit limitOut = chargeRate_min(txLimit, cpLimit); } void SmartChargingConnector::trackTransaction() { Transaction *tx = nullptr; if (model.getConnector(connectorId)) { tx = model.getConnector(connectorId)->getTransaction().get(); } bool update = false; if (tx) { if (tx->getTxProfileId() != trackTxRmtProfileId) { update = true; trackTxRmtProfileId = tx->getTxProfileId(); } if (tx->getStartSync().isRequested() && tx->getStartTimestamp() != trackTxStart) { update = true; trackTxStart = tx->getStartTimestamp(); } if (tx->getStartSync().isConfirmed() && tx->getTransactionId() != trackTxId) { update = true; trackTxId = tx->getTransactionId(); } } else { //check if transaction has just been completed if (trackTxRmtProfileId >= 0 || trackTxStart < MAX_TIME || trackTxId >= 0) { //yes, clear data update = true; trackTxRmtProfileId = -1; trackTxStart = MAX_TIME; trackTxId = -1; clearChargingProfile([this] (int, int connectorId, ChargingProfilePurposeType purpose, int) { return purpose == ChargingProfilePurposeType::TxProfile && (int) this->connectorId == connectorId; }); } } if (update) { nextChange = model.getClock().now(); //will refresh limit calculation } } void SmartChargingConnector::loop(){ trackTransaction(); /** * check if to call onLimitChange */ auto& tnow = model.getClock().now(); if (tnow >= nextChange){ ChargeRate limit; nextChange = MAX_TIME; //reset nextChange to default value and refresh it calculateLimit(tnow, limit, nextChange); #if MO_DBG_LEVEL >= MO_DL_INFO { char timestamp1[JSONDATE_LENGTH + 1] = {'\0'}; tnow.toJsonString(timestamp1, JSONDATE_LENGTH + 1); char timestamp2[JSONDATE_LENGTH + 1] = {'\0'}; nextChange.toJsonString(timestamp2, JSONDATE_LENGTH + 1); MO_DBG_INFO("New limit for connector %u, scheduled at = %s, nextChange = %s, limit = {%.1f, %.1f, %i}", connectorId, timestamp1, timestamp2, limit.power != std::numeric_limits::max() ? limit.power : -1.f, limit.current != std::numeric_limits::max() ? limit.current : -1.f, limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); } #endif if (trackLimitOutput != limit) { if (limitOutput) { limitOutput( limit.power != std::numeric_limits::max() ? limit.power : -1.f, limit.current != std::numeric_limits::max() ? limit.current : -1.f, limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); trackLimitOutput = limit; } } } } void SmartChargingConnector::setSmartChargingOutput(std::function limitOutput) { if (this->limitOutput) { MO_DBG_WARN("replacing existing SmartChargingOutput"); } this->limitOutput = limitOutput; } ChargingProfile *SmartChargingConnector::updateProfiles(std::unique_ptr chargingProfile) { int stackLevel = chargingProfile->getStackLevel(); //already validated switch (chargingProfile->getChargingProfilePurpose()) { case (ChargingProfilePurposeType::ChargePointMaxProfile): break; case (ChargingProfilePurposeType::TxDefaultProfile): TxDefaultProfile[stackLevel] = std::move(chargingProfile); return TxDefaultProfile[stackLevel].get(); case (ChargingProfilePurposeType::TxProfile): TxProfile[stackLevel] = std::move(chargingProfile); return TxProfile[stackLevel].get(); } MO_DBG_ERR("invalid args"); return nullptr; } void SmartChargingConnector::notifyProfilesUpdated() { nextChange = model.getClock().now(); } bool SmartChargingConnector::clearChargingProfile(const std::function filter) { bool found = false; ProfileStack *profileStacks [] = {&TxProfile, &TxDefaultProfile}; for (auto stack : profileStacks) { for (size_t iLevel = 0; iLevel < stack->size(); iLevel++) { if (auto& profile = stack->at(iLevel)) { if (profile && filter(profile->getChargingProfileId(), connectorId, profile->getChargingProfilePurpose(), iLevel)) { found = true; SmartChargingServiceUtils::removeProfile(filesystem, connectorId, profile->getChargingProfilePurpose(), iLevel); profile.reset(); } } } } return found; } std::unique_ptr SmartChargingConnector::getCompositeSchedule(int duration, ChargingRateUnitType_Optional unit) { auto& startSchedule = model.getClock().now(); auto schedule = std::unique_ptr(new ChargingSchedule()); schedule->duration = duration; schedule->startSchedule = startSchedule; schedule->chargingProfileKind = ChargingProfileKindType::Absolute; schedule->recurrencyKind = RecurrencyKindType::NOT_SET; auto& periods = schedule->chargingSchedulePeriod; Timestamp periodBegin = Timestamp(startSchedule); Timestamp periodStop = Timestamp(startSchedule); while (periodBegin - startSchedule < duration && periods.size() < MO_ChargingScheduleMaxPeriods) { //calculate limit ChargeRate limit; calculateLimit(periodBegin, limit, periodStop); //if the unit is still unspecified, guess by taking the unit of the first limit if (unit == ChargingRateUnitType_Optional::None) { if (limit.power < limit.current) { unit = ChargingRateUnitType_Optional::Watt; } else { unit = ChargingRateUnitType_Optional::Amp; } } periods.emplace_back(); float limit_opt = unit == ChargingRateUnitType_Optional::Watt ? limit.power : limit.current; periods.back().limit = limit_opt != std::numeric_limits::max() ? limit_opt : -1.f, periods.back().numberPhases = limit.nphases != std::numeric_limits::max() ? limit.nphases : -1; periods.back().startPeriod = periodBegin - startSchedule; periodBegin = periodStop; } if (unit == ChargingRateUnitType_Optional::Watt) { schedule->chargingRateUnit = ChargingRateUnitType::Watt; } else { schedule->chargingRateUnit = ChargingRateUnitType::Amp; } return schedule; } size_t SmartChargingConnector::getChargingProfilesCount() { size_t chargingProfilesCount = 0; for (size_t i = 0; i < MO_ChargeProfileMaxStackLevel + 1; i++) { if (TxDefaultProfile[i]) { chargingProfilesCount++; } if (TxProfile[i]) { chargingProfilesCount++; } } return chargingProfilesCount; } SmartChargingConnector *SmartChargingService::getScConnectorById(unsigned int connectorId) { if (connectorId == 0) { return nullptr; } if (connectorId - 1 >= connectors.size()) { return nullptr; } return &connectors[connectorId-1]; } SmartChargingService::SmartChargingService(Context& context, std::shared_ptr filesystem, unsigned int numConnectors) : MemoryManaged("v16.SmartCharging.SmartChargingService"), context(context), filesystem{filesystem}, connectors{makeVector(getMemoryTag())}, numConnectors(numConnectors) { for (unsigned int cId = 1; cId < numConnectors; cId++) { connectors.emplace_back(context.getModel(), filesystem, cId, ChargePointMaxProfile, ChargePointTxDefaultProfile); } declareConfiguration("ChargeProfileMaxStackLevel", MO_ChargeProfileMaxStackLevel, CONFIGURATION_VOLATILE, true); declareConfiguration("ChargingScheduleAllowedChargingRateUnit", "", CONFIGURATION_VOLATILE, true); declareConfiguration("ChargingScheduleMaxPeriods", MO_ChargingScheduleMaxPeriods, CONFIGURATION_VOLATILE, true); declareConfiguration("MaxChargingProfilesInstalled", MO_MaxChargingProfilesInstalled, CONFIGURATION_VOLATILE, true); context.getOperationRegistry().registerOperation("ClearChargingProfile", [this] () { return new Ocpp16::ClearChargingProfile(*this);}); context.getOperationRegistry().registerOperation("GetCompositeSchedule", [&context, this] () { return new Ocpp16::GetCompositeSchedule(context.getModel(), *this);}); context.getOperationRegistry().registerOperation("SetChargingProfile", [&context, this] () { return new Ocpp16::SetChargingProfile(context.getModel(), *this);}); loadProfiles(); } SmartChargingService::~SmartChargingService() { } ChargingProfile *SmartChargingService::updateProfiles(unsigned int connectorId, std::unique_ptr chargingProfile){ if ((connectorId > 0 && !getScConnectorById(connectorId)) || !chargingProfile) { MO_DBG_ERR("invalid args"); return nullptr; } if (MO_DBG_LEVEL >= MO_DL_VERBOSE) { MO_DBG_VERBOSE("Charging Profile internal model:"); chargingProfile->printProfile(); } int stackLevel = chargingProfile->getStackLevel(); if (stackLevel< 0 || stackLevel >= MO_ChargeProfileMaxStackLevel + 1) { MO_DBG_ERR("input validation failed"); return nullptr; } size_t chargingProfilesCount = 0; for (size_t i = 0; i < MO_ChargeProfileMaxStackLevel + 1; i++) { if (ChargePointTxDefaultProfile[i]) { chargingProfilesCount++; } if (ChargePointMaxProfile[i]) { chargingProfilesCount++; } } for (size_t i = 0; i < connectors.size(); i++) { chargingProfilesCount += connectors[i].getChargingProfilesCount(); } if (chargingProfilesCount >= MO_MaxChargingProfilesInstalled) { MO_DBG_WARN("number of maximum charging profiles exceeded"); return nullptr; } ChargingProfile *res = nullptr; switch (chargingProfile->getChargingProfilePurpose()) { case (ChargingProfilePurposeType::ChargePointMaxProfile): if (connectorId != 0) { MO_DBG_WARN("invalid charging profile"); return nullptr; } ChargePointMaxProfile[stackLevel] = std::move(chargingProfile); res = ChargePointMaxProfile[stackLevel].get(); break; case (ChargingProfilePurposeType::TxDefaultProfile): if (connectorId == 0) { ChargePointTxDefaultProfile[stackLevel] = std::move(chargingProfile); res = ChargePointTxDefaultProfile[stackLevel].get(); } else { res = getScConnectorById(connectorId)->updateProfiles(std::move(chargingProfile)); } break; case (ChargingProfilePurposeType::TxProfile): if (connectorId == 0) { MO_DBG_WARN("invalid charging profile"); return nullptr; } else { res = getScConnectorById(connectorId)->updateProfiles(std::move(chargingProfile)); } break; } /** * Invalidate the last limit by setting the nextChange to now. By the next loop()-call, the limit * and nextChange will be recalculated and onLimitChanged will be called. */ if (res) { nextChange = context.getModel().getClock().now(); for (size_t i = 0; i < connectors.size(); i++) { connectors[i].notifyProfilesUpdated(); } } return res; } bool SmartChargingService::loadProfiles() { bool success = true; if (!filesystem) { MO_DBG_DEBUG("no filesystem"); return true; //not an error } ChargingProfilePurposeType purposes[] = {ChargingProfilePurposeType::ChargePointMaxProfile, ChargingProfilePurposeType::TxDefaultProfile, ChargingProfilePurposeType::TxProfile}; char fn [MO_MAX_PATH_SIZE] = {'\0'}; for (auto purpose : purposes) { for (unsigned int cId = 0; cId < numConnectors; cId++) { if (cId > 0 && purpose == ChargingProfilePurposeType::ChargePointMaxProfile) { continue; } for (unsigned int iLevel = 0; iLevel < MO_ChargeProfileMaxStackLevel; iLevel++) { if (!SmartChargingServiceUtils::printProfileFileName(fn, MO_MAX_PATH_SIZE, cId, purpose, iLevel)) { return false; } size_t msize = 0; if (filesystem->stat(fn, &msize) != 0) { continue; //There is not a profile on the stack iStack with stacklevel iLevel. Normal case, just continue. } auto profileDoc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); if (!profileDoc) { success = false; MO_DBG_ERR("profile corrupt: %s, remove", fn); filesystem->remove(fn); continue; } JsonObject profileJson = profileDoc->as(); auto chargingProfile = loadChargingProfile(profileJson); bool valid = false; if (chargingProfile) { valid = updateProfiles(cId, std::move(chargingProfile)); } if (!valid) { success = false; MO_DBG_ERR("profile corrupt: %s, remove", fn); filesystem->remove(fn); } } } } return success; } /* * limitOut: the calculated maximum charge rate at the moment, or the default value if no limit is defined * validToOut: The begin of the next SmartCharging restriction after time t */ void SmartChargingService::calculateLimit(const Timestamp &t, ChargeRate& limitOut, Timestamp& validToOut){ //initialize output parameters with the default values limitOut = ChargeRate(); validToOut = MAX_TIME; //get ChargePointMaxProfile with the highest stackLevel for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { if (ChargePointMaxProfile[i]) { ChargeRate crOut; bool defined = ChargePointMaxProfile[i]->calculateLimit(t, crOut, validToOut); if (defined) { limitOut = crOut; break; } } } } void SmartChargingService::loop(){ for (size_t i = 0; i < connectors.size(); i++) { connectors[i].loop(); } /** * check if to call onLimitChange */ auto& tnow = context.getModel().getClock().now(); if (tnow >= nextChange){ ChargeRate limit; nextChange = MAX_TIME; //reset nextChange to default value and refresh it calculateLimit(tnow, limit, nextChange); #if MO_DBG_LEVEL >= MO_DL_INFO { char timestamp1[JSONDATE_LENGTH + 1] = {'\0'}; tnow.toJsonString(timestamp1, JSONDATE_LENGTH + 1); char timestamp2[JSONDATE_LENGTH + 1] = {'\0'}; nextChange.toJsonString(timestamp2, JSONDATE_LENGTH + 1); MO_DBG_INFO("New limit for connector %u, scheduled at = %s, nextChange = %s, limit = {%.1f, %.1f, %i}", 0, timestamp1, timestamp2, limit.power != std::numeric_limits::max() ? limit.power : -1.f, limit.current != std::numeric_limits::max() ? limit.current : -1.f, limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); } #endif if (trackLimitOutput != limit) { if (limitOutput) { limitOutput( limit.power != std::numeric_limits::max() ? limit.power : -1.f, limit.current != std::numeric_limits::max() ? limit.current : -1.f, limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); trackLimitOutput = limit; } } } } void SmartChargingService::setSmartChargingOutput(unsigned int connectorId, std::function limitOutput) { if ((connectorId > 0 && !getScConnectorById(connectorId))) { MO_DBG_ERR("invalid args"); return; } if (connectorId == 0) { if (this->limitOutput) { MO_DBG_WARN("replacing existing SmartChargingOutput"); } this->limitOutput = limitOutput; } else { getScConnectorById(connectorId)->setSmartChargingOutput(limitOutput); } } void SmartChargingService::updateAllowedChargingRateUnit(bool powerSupported, bool currentSupported) { if ((powerSupported != this->powerSupported) || (currentSupported != this->currentSupported)) { auto chargingScheduleAllowedChargingRateUnitString = declareConfiguration("ChargingScheduleAllowedChargingRateUnit", "", CONFIGURATION_VOLATILE); if (chargingScheduleAllowedChargingRateUnitString) { if (powerSupported && currentSupported) { chargingScheduleAllowedChargingRateUnitString->setString("Current,Power"); } else if (powerSupported) { chargingScheduleAllowedChargingRateUnitString->setString("Power"); } else if (currentSupported) { chargingScheduleAllowedChargingRateUnitString->setString("Current"); } } this->powerSupported = powerSupported; this->currentSupported = currentSupported; } } bool SmartChargingService::setChargingProfile(unsigned int connectorId, std::unique_ptr chargingProfile) { if ((connectorId > 0 && !getScConnectorById(connectorId)) || !chargingProfile) { MO_DBG_ERR("invalid args"); return false; } if ((!currentSupported && chargingProfile->chargingSchedule.chargingRateUnit == ChargingRateUnitType::Amp) || (!powerSupported && chargingProfile->chargingSchedule.chargingRateUnit == ChargingRateUnitType::Watt)) { MO_DBG_WARN("unsupported charge rate unit"); return false; } int chargingProfileId = chargingProfile->getChargingProfileId(); clearChargingProfile([chargingProfileId] (int id, int, ChargingProfilePurposeType, int) { return id == chargingProfileId; }); bool success = false; auto profilePtr = updateProfiles(connectorId, std::move(chargingProfile)); if (profilePtr) { success = SmartChargingServiceUtils::storeProfile(filesystem, connectorId, profilePtr); if (!success) { clearChargingProfile([chargingProfileId] (int id, int, ChargingProfilePurposeType, int) { return id == chargingProfileId; }); } } return success; } bool SmartChargingService::clearChargingProfile(std::function filter) { bool found = false; for (size_t cId = 0; cId < connectors.size(); cId++) { found |= connectors[cId].clearChargingProfile(filter); } ProfileStack *profileStacks [] = {&ChargePointMaxProfile, &ChargePointTxDefaultProfile}; for (auto stack : profileStacks) { for (size_t iLevel = 0; iLevel < stack->size(); iLevel++) { if (auto& profile = stack->at(iLevel)) { if (filter(profile->getChargingProfileId(), 0, profile->getChargingProfilePurpose(), iLevel)) { found = true; SmartChargingServiceUtils::removeProfile(filesystem, 0, profile->getChargingProfilePurpose(), iLevel); profile.reset(); } } } } /** * Invalidate the last limit by setting the nextChange to now. By the next loop()-call, the limit * and nextChange will be recalculated and onLimitChanged will be called. */ nextChange = context.getModel().getClock().now(); for (size_t i = 0; i < connectors.size(); i++) { connectors[i].notifyProfilesUpdated(); } return found; } std::unique_ptr SmartChargingService::getCompositeSchedule(unsigned int connectorId, int duration, ChargingRateUnitType_Optional unit) { if (connectorId > 0 && !getScConnectorById(connectorId)) { MO_DBG_ERR("invalid args"); return nullptr; } if (unit == ChargingRateUnitType_Optional::None) { if (powerSupported && !currentSupported) { unit = ChargingRateUnitType_Optional::Watt; } else if (!powerSupported && currentSupported) { unit = ChargingRateUnitType_Optional::Amp; } } if (connectorId > 0) { return getScConnectorById(connectorId)->getCompositeSchedule(duration, unit); } auto& startSchedule = context.getModel().getClock().now(); auto schedule = std::unique_ptr(new ChargingSchedule()); schedule->duration = duration; schedule->startSchedule = startSchedule; schedule->chargingProfileKind = ChargingProfileKindType::Absolute; schedule->recurrencyKind = RecurrencyKindType::NOT_SET; auto& periods = schedule->chargingSchedulePeriod; Timestamp periodBegin = Timestamp(startSchedule); Timestamp periodStop = Timestamp(startSchedule); while (periodBegin - startSchedule < duration && periods.size() < MO_ChargingScheduleMaxPeriods) { //calculate limit ChargeRate limit; calculateLimit(periodBegin, limit, periodStop); //if the unit is still unspecified, guess by taking the unit of the first limit if (unit == ChargingRateUnitType_Optional::None) { if (limit.power < limit.current) { unit = ChargingRateUnitType_Optional::Watt; } else { unit = ChargingRateUnitType_Optional::Amp; } } periods.push_back(ChargingSchedulePeriod()); float limit_opt = unit == ChargingRateUnitType_Optional::Watt ? limit.power : limit.current; periods.back().limit = limit_opt != std::numeric_limits::max() ? limit_opt : -1.f; periods.back().numberPhases = limit.nphases != std::numeric_limits::max() ? limit.nphases : -1; periods.back().startPeriod = periodBegin - startSchedule; periodBegin = periodStop; } if (unit == ChargingRateUnitType_Optional::Watt) { schedule->chargingRateUnit = ChargingRateUnitType::Watt; } else { schedule->chargingRateUnit = ChargingRateUnitType::Amp; } return schedule; } bool SmartChargingServiceUtils::printProfileFileName(char *out, size_t bufsize, unsigned int connectorId, ChargingProfilePurposeType purpose, unsigned int stackLevel) { int pret = 0; switch (purpose) { case (ChargingProfilePurposeType::ChargePointMaxProfile): pret = snprintf(out, bufsize, MO_FILENAME_PREFIX "sc-cm-%u.jsn", stackLevel); break; case (ChargingProfilePurposeType::TxDefaultProfile): pret = snprintf(out, bufsize, MO_FILENAME_PREFIX "sc-td-%u-%u.jsn", connectorId, stackLevel); break; case (ChargingProfilePurposeType::TxProfile): pret = snprintf(out, bufsize, MO_FILENAME_PREFIX "sc-tx-%u-%u.jsn", connectorId, stackLevel); break; } if (pret < 0 || (size_t) pret >= bufsize) { MO_DBG_ERR("fn error: %i", pret); return false; } return true; } bool SmartChargingServiceUtils::storeProfile(std::shared_ptr filesystem, unsigned int connectorId, ChargingProfile *chargingProfile) { if (!filesystem) { MO_DBG_DEBUG("no filesystem"); return true; //not an error } auto chargingProfileJson = initJsonDoc("v16.SmartCharging.ChargingProfile"); if (!chargingProfile->toJson(chargingProfileJson)) { return false; } char fn [MO_MAX_PATH_SIZE] = {'\0'}; if (!printProfileFileName(fn, MO_MAX_PATH_SIZE, connectorId, chargingProfile->getChargingProfilePurpose(), chargingProfile->getStackLevel())) { return false; } return FilesystemUtils::storeJson(filesystem, fn, chargingProfileJson); } bool SmartChargingServiceUtils::removeProfile(std::shared_ptr filesystem, unsigned int connectorId, ChargingProfilePurposeType purpose, unsigned int stackLevel) { if (!filesystem) { return false; } char fn [MO_MAX_PATH_SIZE] = {'\0'}; if (!printProfileFileName(fn, MO_MAX_PATH_SIZE, connectorId, purpose, stackLevel)) { return false; } return filesystem->remove(fn); } ================================================ FILE: src/MicroOcpp/Model/SmartCharging/SmartChargingService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef SMARTCHARGINGSERVICE_H #define SMARTCHARGINGSERVICE_H #include #include #include #include #include #include #include namespace MicroOcpp { enum class ChargingRateUnitType_Optional { Watt, Amp, None }; class Context; class Model; using ProfileStack = std::array, MO_ChargeProfileMaxStackLevel + 1>; class SmartChargingConnector : public MemoryManaged { private: Model& model; std::shared_ptr filesystem; const unsigned int connectorId; ProfileStack& ChargePointMaxProfile; ProfileStack& ChargePointTxDefaultProfile; ProfileStack TxDefaultProfile; ProfileStack TxProfile; std::function limitOutput; int trackTxRmtProfileId = -1; //optional Charging Profile ID when tx is started via RemoteStartTx Timestamp trackTxStart = MAX_TIME; //time basis for relative profiles int trackTxId = -1; //transactionId assigned by OCPP server Timestamp nextChange = MIN_TIME; ChargeRate trackLimitOutput; void calculateLimit(const Timestamp &t, ChargeRate& limitOut, Timestamp& validToOut); void trackTransaction(); public: SmartChargingConnector(Model& model, std::shared_ptr filesystem, unsigned int connectorId, ProfileStack& ChargePointMaxProfile, ProfileStack& ChargePointTxDefaultProfile); SmartChargingConnector(SmartChargingConnector&&) = default; ~SmartChargingConnector(); void loop(); void setSmartChargingOutput(std::function limitOutput); //read maximum Watt x Amps x numberPhases ChargingProfile *updateProfiles(std::unique_ptr chargingProfile); void notifyProfilesUpdated(); bool clearChargingProfile(std::function filter); std::unique_ptr getCompositeSchedule(int duration, ChargingRateUnitType_Optional unit); size_t getChargingProfilesCount(); }; class SmartChargingService : public MemoryManaged { private: Context& context; std::shared_ptr filesystem; Vector connectors; //connectorId 0 excluded SmartChargingConnector *getScConnectorById(unsigned int connectorId); unsigned int numConnectors; //connectorId 0 included ProfileStack ChargePointMaxProfile; ProfileStack ChargePointTxDefaultProfile; std::function limitOutput; ChargeRate trackLimitOutput; bool powerSupported = false; bool currentSupported = false; Timestamp nextChange = MIN_TIME; ChargingProfile *updateProfiles(unsigned int connectorId, std::unique_ptr chargingProfile); bool loadProfiles(); void calculateLimit(const Timestamp &t, ChargeRate& limitOut, Timestamp& validToOut); public: SmartChargingService(Context& context, std::shared_ptr filesystem, unsigned int numConnectors); ~SmartChargingService(); void loop(); void setSmartChargingOutput(unsigned int connectorId, std::function limitOutput); //read maximum Watt x Amps x numberPhases void updateAllowedChargingRateUnit(bool powerSupported, bool currentSupported); //set supported measurand of SmartChargingOutput bool setChargingProfile(unsigned int connectorId, std::unique_ptr chargingProfile); bool clearChargingProfile(std::function filter); std::unique_ptr getCompositeSchedule(unsigned int connectorId, int duration, ChargingRateUnitType_Optional unit = ChargingRateUnitType_Optional::None); }; //filesystem-related helper functions namespace SmartChargingServiceUtils { bool printProfileFileName(char *out, size_t bufsize, unsigned int connectorId, ChargingProfilePurposeType purpose, unsigned int stackLevel); bool storeProfile(std::shared_ptr filesystem, unsigned int connectorId, ChargingProfile *chargingProfile); bool removeProfile(std::shared_ptr filesystem, unsigned int connectorId, ChargingProfilePurposeType purpose, unsigned int stackLevel); } } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Model/Transactions/Transaction.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include using namespace MicroOcpp; bool Transaction::setIdTag(const char *idTag) { auto ret = snprintf(this->idTag, IDTAG_LEN_MAX + 1, "%s", idTag); return ret >= 0 && ret < IDTAG_LEN_MAX + 1; } bool Transaction::setParentIdTag(const char *idTag) { auto ret = snprintf(this->parentIdTag, IDTAG_LEN_MAX + 1, "%s", idTag); return ret >= 0 && ret < IDTAG_LEN_MAX + 1; } bool Transaction::setStopIdTag(const char *idTag) { auto ret = snprintf(stop_idTag, IDTAG_LEN_MAX + 1, "%s", idTag); return ret >= 0 && ret < IDTAG_LEN_MAX + 1; } bool Transaction::setStopReason(const char *reason) { auto ret = snprintf(stop_reason, REASON_LEN_MAX + 1, "%s", reason); return ret >= 0 && ret < REASON_LEN_MAX + 1; } bool Transaction::commit() { return context.commit(this); } #if MO_ENABLE_V201 namespace MicroOcpp { namespace Ocpp201 { const char *serializeTransactionStoppedReason(Transaction::StoppedReason stoppedReason) { const char *stoppedReasonCstr = nullptr; switch (stoppedReason) { case Transaction::StoppedReason::UNDEFINED: // optional, okay break; case Transaction::StoppedReason::Local: stoppedReasonCstr = "Local"; break; case Transaction::StoppedReason::DeAuthorized: stoppedReasonCstr = "DeAuthorized"; break; case Transaction::StoppedReason::EmergencyStop: stoppedReasonCstr = "EmergencyStop"; break; case Transaction::StoppedReason::EnergyLimitReached: stoppedReasonCstr = "EnergyLimitReached"; break; case Transaction::StoppedReason::EVDisconnected: stoppedReasonCstr = "EVDisconnected"; break; case Transaction::StoppedReason::GroundFault: stoppedReasonCstr = "GroundFault"; break; case Transaction::StoppedReason::ImmediateReset: stoppedReasonCstr = "ImmediateReset"; break; case Transaction::StoppedReason::LocalOutOfCredit: stoppedReasonCstr = "LocalOutOfCredit"; break; case Transaction::StoppedReason::MasterPass: stoppedReasonCstr = "MasterPass"; break; case Transaction::StoppedReason::Other: stoppedReasonCstr = "Other"; break; case Transaction::StoppedReason::OvercurrentFault: stoppedReasonCstr = "OvercurrentFault"; break; case Transaction::StoppedReason::PowerLoss: stoppedReasonCstr = "PowerLoss"; break; case Transaction::StoppedReason::PowerQuality: stoppedReasonCstr = "PowerQuality"; break; case Transaction::StoppedReason::Reboot: stoppedReasonCstr = "Reboot"; break; case Transaction::StoppedReason::Remote: stoppedReasonCstr = "Remote"; break; case Transaction::StoppedReason::SOCLimitReached: stoppedReasonCstr = "SOCLimitReached"; break; case Transaction::StoppedReason::StoppedByEV: stoppedReasonCstr = "StoppedByEV"; break; case Transaction::StoppedReason::TimeLimitReached: stoppedReasonCstr = "TimeLimitReached"; break; case Transaction::StoppedReason::Timeout: stoppedReasonCstr = "Timeout"; break; } return stoppedReasonCstr; } bool deserializeTransactionStoppedReason(const char *stoppedReasonCstr, Transaction::StoppedReason& stoppedReasonOut) { if (!stoppedReasonCstr || !*stoppedReasonCstr) { stoppedReasonOut = Transaction::StoppedReason::UNDEFINED; } else if (!strcmp(stoppedReasonCstr, "DeAuthorized")) { stoppedReasonOut = Transaction::StoppedReason::DeAuthorized; } else if (!strcmp(stoppedReasonCstr, "EmergencyStop")) { stoppedReasonOut = Transaction::StoppedReason::EmergencyStop; } else if (!strcmp(stoppedReasonCstr, "EnergyLimitReached")) { stoppedReasonOut = Transaction::StoppedReason::EnergyLimitReached; } else if (!strcmp(stoppedReasonCstr, "EVDisconnected")) { stoppedReasonOut = Transaction::StoppedReason::EVDisconnected; } else if (!strcmp(stoppedReasonCstr, "GroundFault")) { stoppedReasonOut = Transaction::StoppedReason::GroundFault; } else if (!strcmp(stoppedReasonCstr, "ImmediateReset")) { stoppedReasonOut = Transaction::StoppedReason::ImmediateReset; } else if (!strcmp(stoppedReasonCstr, "Local")) { stoppedReasonOut = Transaction::StoppedReason::Local; } else if (!strcmp(stoppedReasonCstr, "LocalOutOfCredit")) { stoppedReasonOut = Transaction::StoppedReason::LocalOutOfCredit; } else if (!strcmp(stoppedReasonCstr, "MasterPass")) { stoppedReasonOut = Transaction::StoppedReason::MasterPass; } else if (!strcmp(stoppedReasonCstr, "Other")) { stoppedReasonOut = Transaction::StoppedReason::Other; } else if (!strcmp(stoppedReasonCstr, "OvercurrentFault")) { stoppedReasonOut = Transaction::StoppedReason::OvercurrentFault; } else if (!strcmp(stoppedReasonCstr, "PowerLoss")) { stoppedReasonOut = Transaction::StoppedReason::PowerLoss; } else if (!strcmp(stoppedReasonCstr, "PowerQuality")) { stoppedReasonOut = Transaction::StoppedReason::PowerQuality; } else if (!strcmp(stoppedReasonCstr, "Reboot")) { stoppedReasonOut = Transaction::StoppedReason::Reboot; } else if (!strcmp(stoppedReasonCstr, "Remote")) { stoppedReasonOut = Transaction::StoppedReason::Remote; } else if (!strcmp(stoppedReasonCstr, "SOCLimitReached")) { stoppedReasonOut = Transaction::StoppedReason::SOCLimitReached; } else if (!strcmp(stoppedReasonCstr, "StoppedByEV")) { stoppedReasonOut = Transaction::StoppedReason::StoppedByEV; } else if (!strcmp(stoppedReasonCstr, "TimeLimitReached")) { stoppedReasonOut = Transaction::StoppedReason::TimeLimitReached; } else if (!strcmp(stoppedReasonCstr, "Timeout")) { stoppedReasonOut = Transaction::StoppedReason::Timeout; } else { MO_DBG_ERR("deserialization error"); return false; } return true; } const char *serializeTransactionEventType(TransactionEventData::Type type) { const char *typeCstr = ""; switch (type) { case TransactionEventData::Type::Ended: typeCstr = "Ended"; break; case TransactionEventData::Type::Started: typeCstr = "Started"; break; case TransactionEventData::Type::Updated: typeCstr = "Updated"; break; } return typeCstr; } bool deserializeTransactionEventType(const char *typeCstr, TransactionEventData::Type& typeOut) { if (!strcmp(typeCstr, "Ended")) { typeOut = TransactionEventData::Type::Ended; } else if (!strcmp(typeCstr, "Started")) { typeOut = TransactionEventData::Type::Started; } else if (!strcmp(typeCstr, "Updated")) { typeOut = TransactionEventData::Type::Updated; } else { MO_DBG_ERR("deserialization error"); return false; } return true; } const char *serializeTransactionEventTriggerReason(TransactionEventTriggerReason triggerReason) { const char *triggerReasonCstr = nullptr; switch(triggerReason) { case TransactionEventTriggerReason::UNDEFINED: break; case TransactionEventTriggerReason::Authorized: triggerReasonCstr = "Authorized"; break; case TransactionEventTriggerReason::CablePluggedIn: triggerReasonCstr = "CablePluggedIn"; break; case TransactionEventTriggerReason::ChargingRateChanged: triggerReasonCstr = "ChargingRateChanged"; break; case TransactionEventTriggerReason::ChargingStateChanged: triggerReasonCstr = "ChargingStateChanged"; break; case TransactionEventTriggerReason::Deauthorized: triggerReasonCstr = "Deauthorized"; break; case TransactionEventTriggerReason::EnergyLimitReached: triggerReasonCstr = "EnergyLimitReached"; break; case TransactionEventTriggerReason::EVCommunicationLost: triggerReasonCstr = "EVCommunicationLost"; break; case TransactionEventTriggerReason::EVConnectTimeout: triggerReasonCstr = "EVConnectTimeout"; break; case TransactionEventTriggerReason::MeterValueClock: triggerReasonCstr = "MeterValueClock"; break; case TransactionEventTriggerReason::MeterValuePeriodic: triggerReasonCstr = "MeterValuePeriodic"; break; case TransactionEventTriggerReason::TimeLimitReached: triggerReasonCstr = "TimeLimitReached"; break; case TransactionEventTriggerReason::Trigger: triggerReasonCstr = "Trigger"; break; case TransactionEventTriggerReason::UnlockCommand: triggerReasonCstr = "UnlockCommand"; break; case TransactionEventTriggerReason::StopAuthorized: triggerReasonCstr = "StopAuthorized"; break; case TransactionEventTriggerReason::EVDeparted: triggerReasonCstr = "EVDeparted"; break; case TransactionEventTriggerReason::EVDetected: triggerReasonCstr = "EVDetected"; break; case TransactionEventTriggerReason::RemoteStop: triggerReasonCstr = "RemoteStop"; break; case TransactionEventTriggerReason::RemoteStart: triggerReasonCstr = "RemoteStart"; break; case TransactionEventTriggerReason::AbnormalCondition: triggerReasonCstr = "AbnormalCondition"; break; case TransactionEventTriggerReason::SignedDataReceived: triggerReasonCstr = "SignedDataReceived"; break; case TransactionEventTriggerReason::ResetCommand: triggerReasonCstr = "ResetCommand"; break; } return triggerReasonCstr; } bool deserializeTransactionEventTriggerReason(const char *triggerReasonCstr, TransactionEventTriggerReason& triggerReasonOut) { if (!triggerReasonCstr || !*triggerReasonCstr) { triggerReasonOut = TransactionEventTriggerReason::UNDEFINED; } else if (!strcmp(triggerReasonCstr, "Authorized")) { triggerReasonOut = TransactionEventTriggerReason::Authorized; } else if (!strcmp(triggerReasonCstr, "CablePluggedIn")) { triggerReasonOut = TransactionEventTriggerReason::CablePluggedIn; } else if (!strcmp(triggerReasonCstr, "ChargingRateChanged")) { triggerReasonOut = TransactionEventTriggerReason::ChargingRateChanged; } else if (!strcmp(triggerReasonCstr, "ChargingStateChanged")) { triggerReasonOut = TransactionEventTriggerReason::ChargingStateChanged; } else if (!strcmp(triggerReasonCstr, "Deauthorized")) { triggerReasonOut = TransactionEventTriggerReason::Deauthorized; } else if (!strcmp(triggerReasonCstr, "EnergyLimitReached")) { triggerReasonOut = TransactionEventTriggerReason::EnergyLimitReached; } else if (!strcmp(triggerReasonCstr, "EVCommunicationLost")) { triggerReasonOut = TransactionEventTriggerReason::EVCommunicationLost; } else if (!strcmp(triggerReasonCstr, "EVConnectTimeout")) { triggerReasonOut = TransactionEventTriggerReason::EVConnectTimeout; } else if (!strcmp(triggerReasonCstr, "MeterValueClock")) { triggerReasonOut = TransactionEventTriggerReason::MeterValueClock; } else if (!strcmp(triggerReasonCstr, "MeterValuePeriodic")) { triggerReasonOut = TransactionEventTriggerReason::MeterValuePeriodic; } else if (!strcmp(triggerReasonCstr, "TimeLimitReached")) { triggerReasonOut = TransactionEventTriggerReason::TimeLimitReached; } else if (!strcmp(triggerReasonCstr, "Trigger")) { triggerReasonOut = TransactionEventTriggerReason::Trigger; } else if (!strcmp(triggerReasonCstr, "UnlockCommand")) { triggerReasonOut = TransactionEventTriggerReason::UnlockCommand; } else if (!strcmp(triggerReasonCstr, "StopAuthorized")) { triggerReasonOut = TransactionEventTriggerReason::StopAuthorized; } else if (!strcmp(triggerReasonCstr, "EVDeparted")) { triggerReasonOut = TransactionEventTriggerReason::EVDeparted; } else if (!strcmp(triggerReasonCstr, "EVDetected")) { triggerReasonOut = TransactionEventTriggerReason::EVDetected; } else if (!strcmp(triggerReasonCstr, "RemoteStop")) { triggerReasonOut = TransactionEventTriggerReason::RemoteStop; } else if (!strcmp(triggerReasonCstr, "RemoteStart")) { triggerReasonOut = TransactionEventTriggerReason::RemoteStart; } else if (!strcmp(triggerReasonCstr, "AbnormalCondition")) { triggerReasonOut = TransactionEventTriggerReason::AbnormalCondition; } else if (!strcmp(triggerReasonCstr, "SignedDataReceived")) { triggerReasonOut = TransactionEventTriggerReason::SignedDataReceived; } else if (!strcmp(triggerReasonCstr, "ResetCommand")) { triggerReasonOut = TransactionEventTriggerReason::ResetCommand; } else { MO_DBG_ERR("deserialization error"); return false; } return true; } const char *serializeTransactionEventChargingState(TransactionEventData::ChargingState chargingState) { const char *chargingStateCstr = nullptr; switch (chargingState) { case TransactionEventData::ChargingState::UNDEFINED: // optional, okay break; case TransactionEventData::ChargingState::Charging: chargingStateCstr = "Charging"; break; case TransactionEventData::ChargingState::EVConnected: chargingStateCstr = "EVConnected"; break; case TransactionEventData::ChargingState::SuspendedEV: chargingStateCstr = "SuspendedEV"; break; case TransactionEventData::ChargingState::SuspendedEVSE: chargingStateCstr = "SuspendedEVSE"; break; case TransactionEventData::ChargingState::Idle: chargingStateCstr = "Idle"; break; } return chargingStateCstr; } bool deserializeTransactionEventChargingState(const char *chargingStateCstr, TransactionEventData::ChargingState& chargingStateOut) { if (!chargingStateCstr || !*chargingStateCstr) { chargingStateOut = TransactionEventData::ChargingState::UNDEFINED; } else if (!strcmp(chargingStateCstr, "Charging")) { chargingStateOut = TransactionEventData::ChargingState::Charging; } else if (!strcmp(chargingStateCstr, "EVConnected")) { chargingStateOut = TransactionEventData::ChargingState::EVConnected; } else if (!strcmp(chargingStateCstr, "SuspendedEV")) { chargingStateOut = TransactionEventData::ChargingState::SuspendedEV; } else if (!strcmp(chargingStateCstr, "SuspendedEVSE")) { chargingStateOut = TransactionEventData::ChargingState::SuspendedEVSE; } else if (!strcmp(chargingStateCstr, "Idle")) { chargingStateOut = TransactionEventData::ChargingState::Idle; } else { MO_DBG_ERR("deserialization error"); return false; } return true; } } //namespace Ocpp201 } //namespace MicroOcpp #endif //MO_ENABLE_V201 #if MO_ENABLE_V201 bool g_ocpp_tx_compat_v201; void ocpp_tx_compat_setV201(bool isV201) { g_ocpp_tx_compat_v201 = isV201; } #endif int ocpp_tx_getTransactionId(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { MO_DBG_ERR("only supported in v16"); return -1; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getTransactionId(); } #if MO_ENABLE_V201 const char *ocpp_tx_getTransactionIdV201(OCPP_Transaction *tx) { if (!g_ocpp_tx_compat_v201) { MO_DBG_ERR("only supported in v201"); return nullptr; } return reinterpret_cast(tx)->transactionId; } #endif //MO_ENABLE_V201 bool ocpp_tx_isAuthorized(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { return reinterpret_cast(tx)->isAuthorized; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->isAuthorized(); } bool ocpp_tx_isIdTagDeauthorized(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { return reinterpret_cast(tx)->isDeauthorized; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->isIdTagDeauthorized(); } bool ocpp_tx_isRunning(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { auto transaction = reinterpret_cast(tx); return transaction->started && !transaction->stopped; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->isRunning(); } bool ocpp_tx_isActive(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { return reinterpret_cast(tx)->active; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->isActive(); } bool ocpp_tx_isAborted(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { auto transaction = reinterpret_cast(tx); return !transaction->active && !transaction->started; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->isAborted(); } bool ocpp_tx_isCompleted(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { auto transaction = reinterpret_cast(tx); return transaction->stopped && transaction->seqNos.empty(); } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->isCompleted(); } const char *ocpp_tx_getIdTag(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { auto transaction = reinterpret_cast(tx); return transaction->idToken.get(); } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getIdTag(); } const char *ocpp_tx_getParentIdTag(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { MO_DBG_ERR("only supported in v16"); return nullptr; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getParentIdTag(); } bool ocpp_tx_getBeginTimestamp(OCPP_Transaction *tx, char *buf, size_t len) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { return reinterpret_cast(tx)->beginTimestamp.toJsonString(buf, len); } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getBeginTimestamp().toJsonString(buf, len); } int32_t ocpp_tx_getMeterStart(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { MO_DBG_ERR("only supported in v16"); return -1; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getMeterStart(); } bool ocpp_tx_getStartTimestamp(OCPP_Transaction *tx, char *buf, size_t len) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { MO_DBG_ERR("only supported in v16"); return -1; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getStartTimestamp().toJsonString(buf, len); } const char *ocpp_tx_getStopIdTag(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { auto transaction = reinterpret_cast(tx); return transaction->stopIdToken ? transaction->stopIdToken->get() : ""; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getStopIdTag(); } int32_t ocpp_tx_getMeterStop(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { MO_DBG_ERR("only supported in v16"); return -1; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getMeterStop(); } void ocpp_tx_setMeterStop(OCPP_Transaction* tx, int32_t meter) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { MO_DBG_ERR("only supported in v16"); return; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->setMeterStop(meter); } bool ocpp_tx_getStopTimestamp(OCPP_Transaction *tx, char *buf, size_t len) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { MO_DBG_ERR("only supported in v16"); return -1; } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getStopTimestamp().toJsonString(buf, len); } const char *ocpp_tx_getStopReason(OCPP_Transaction *tx) { #if MO_ENABLE_V201 if (g_ocpp_tx_compat_v201) { auto transaction = reinterpret_cast(tx); return serializeTransactionStoppedReason(transaction->stoppedReason); } #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getStopReason(); } ================================================ FILE: src/MicroOcpp/Model/Transactions/Transaction.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef TRANSACTION_H #define TRANSACTION_H #include /* General Tx defs */ #ifdef __cplusplus extern "C" { #endif //__cplusplus //TxNotification - event from MO to the main firmware to notify it about transaction state changes typedef enum { TxNotification_UNDEFINED, //Authorization events TxNotification_Authorized, //success TxNotification_AuthorizationRejected, //IdTag/token not authorized TxNotification_AuthorizationTimeout, //authorization failed - offline TxNotification_ReservationConflict, //connector/evse reserved for other IdTag TxNotification_ConnectionTimeout, //user took to long to plug vehicle after the authorization TxNotification_DeAuthorized, //server rejected StartTx/TxEvent TxNotification_RemoteStart, //authorized via RemoteStartTx/RequestStartTx TxNotification_RemoteStop, //stopped via RemoteStopTx/RequestStopTx //Tx lifecycle events TxNotification_StartTx, //entered running state (StartTx/TxEvent was initiated) TxNotification_StopTx, //left running state (StopTx/TxEvent was initiated) } TxNotification; #ifdef __cplusplus } #endif //__cplusplus #ifdef __cplusplus #include #include #include #define MAX_TX_CNT 100000U //upper limit of txNr (internal usage). Must be at least 2*MO_TXRECORD_SIZE+1 namespace MicroOcpp { /* * A transaction is initiated by the client (charging station) and processed by the server (central system). * The client side of a transaction is all data that is generated or collected at the charging station. The * server side is all transaction data that is assigned by the central system. * * See OCPP 1.6 Specification - Edition 2, sections 3.6, 4.8, 4.10 and 5.11. */ class ConnectorTransactionStore; class SendStatus { private: bool requested = false; bool confirmed = false; unsigned int opNr = 0; unsigned int attemptNr = 0; Timestamp attemptTime = MIN_TIME; public: void setRequested() {this->requested = true;} bool isRequested() {return requested;} void confirm() {confirmed = true;} bool isConfirmed() {return confirmed;} void setOpNr(unsigned int opNr) {this->opNr = opNr;} unsigned int getOpNr() {return opNr;} void advanceAttemptNr() {attemptNr++;} void setAttemptNr(unsigned int attemptNr) {this->attemptNr = attemptNr;} unsigned int getAttemptNr() {return attemptNr;} const Timestamp& getAttemptTime() {return attemptTime;} void setAttemptTime(const Timestamp& timestamp) {attemptTime = timestamp;} }; class Transaction : public MemoryManaged { private: ConnectorTransactionStore& context; bool active = true; //once active is false, the tx must stop (or cannot start at all) /* * Attributes existing before StartTransaction */ char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; char parentIdTag [IDTAG_LEN_MAX + 1] = {'\0'}; bool authorized = false; //if the given idTag was authorized bool deauthorized = false; //if the server revoked a local authorization Timestamp begin_timestamp = MIN_TIME; int reservationId = -1; int txProfileId = -1; /* * Attributes of StartTransaction */ SendStatus start_sync; int32_t start_meter = -1; //meterStart of StartTx Timestamp start_timestamp = MIN_TIME; //timestamp of StartTx; can be set before actually initiating uint16_t start_bootNr = 0; int transactionId = -1; //only valid if confirmed = true /* * Attributes of StopTransaction */ SendStatus stop_sync; char stop_idTag [IDTAG_LEN_MAX + 1] = {'\0'}; int32_t stop_meter = -1; Timestamp stop_timestamp = MIN_TIME; uint16_t stop_bootNr = 0; char stop_reason [REASON_LEN_MAX + 1] = {'\0'}; /* * General attributes */ unsigned int connectorId = 0; unsigned int txNr = 0; //client-side key of this tx object (!= transactionId) bool silent = false; //silent Tx: process tx locally, without reporting to the server public: Transaction(ConnectorTransactionStore& context, unsigned int connectorId, unsigned int txNr, bool silent = false) : MemoryManaged("v16.Transactions.Transaction"), context(context), connectorId(connectorId), txNr(txNr), silent(silent) {} /* * data assigned by OCPP server */ int getTransactionId() {return transactionId;} bool isAuthorized() {return authorized;} //Authorize has been accepted bool isIdTagDeauthorized() {return deauthorized;} //StartTransaction has been rejected /* * Transaction life cycle */ bool isRunning() {return start_sync.isRequested() && !stop_sync.isRequested();} //tx is running bool isActive() {return active;} //tx continues to run or is preparing bool isAborted() {return !start_sync.isRequested() && !active;} //tx ended before startTx was sent bool isCompleted() {return stop_sync.isConfirmed();} //tx ended and startTx and stopTx have been confirmed by server /* * After modifying a field of tx, commit to make the data persistent */ bool commit(); /* * Getters and setters for (mostly) internal use */ void setInactive() {active = false;} bool setIdTag(const char *idTag); const char *getIdTag() {return idTag;} bool setParentIdTag(const char *idTag); const char *getParentIdTag() {return parentIdTag;} void setAuthorized() {authorized = true;} void setIdTagDeauthorized() {deauthorized = true;} void setBeginTimestamp(Timestamp timestamp) {begin_timestamp = timestamp;} const Timestamp& getBeginTimestamp() {return begin_timestamp;} void setReservationId(int reservationId) {this->reservationId = reservationId;} int getReservationId() {return reservationId;} void setTxProfileId(int txProfileId) {this->txProfileId = txProfileId;} int getTxProfileId() {return txProfileId;} SendStatus& getStartSync() {return start_sync;} void setMeterStart(int32_t meter) {start_meter = meter;} bool isMeterStartDefined() {return start_meter >= 0;} int32_t getMeterStart() {return start_meter;} void setStartTimestamp(Timestamp timestamp) {start_timestamp = timestamp;} const Timestamp& getStartTimestamp() {return start_timestamp;} void setStartBootNr(uint16_t bootNr) {start_bootNr = bootNr;} uint16_t getStartBootNr() {return start_bootNr;} void setTransactionId(int transactionId) {this->transactionId = transactionId;} SendStatus& getStopSync() {return stop_sync;} bool setStopIdTag(const char *idTag); const char *getStopIdTag() {return stop_idTag;} void setMeterStop(int32_t meter) {stop_meter = meter;} bool isMeterStopDefined() {return stop_meter >= 0;} int32_t getMeterStop() {return stop_meter;} void setStopTimestamp(Timestamp timestamp) {stop_timestamp = timestamp;} const Timestamp& getStopTimestamp() {return stop_timestamp;} void setStopBootNr(uint16_t bootNr) {stop_bootNr = bootNr;} uint16_t getStopBootNr() {return stop_bootNr;} bool setStopReason(const char *reason); const char *getStopReason() {return stop_reason;} void setConnectorId(unsigned int connectorId) {this->connectorId = connectorId;} unsigned int getConnectorId() {return connectorId;} void setTxNr(unsigned int txNr) {this->txNr = txNr;} unsigned int getTxNr() {return txNr;} //internal primary key of this tx object void setSilent() {silent = true;} bool isSilent() {return silent;} //no data will be sent to server and server will not assign transactionId }; } // namespace MicroOcpp #if MO_ENABLE_V201 #include #include #include #include #include #include #ifndef MO_SAMPLEDDATATXENDED_SIZE_MAX #define MO_SAMPLEDDATATXENDED_SIZE_MAX 5 #endif namespace MicroOcpp { namespace Ocpp201 { // TriggerReasonEnumType (3.82) enum class TransactionEventTriggerReason : uint8_t { UNDEFINED, // not part of OCPP Authorized, CablePluggedIn, ChargingRateChanged, ChargingStateChanged, Deauthorized, EnergyLimitReached, EVCommunicationLost, EVConnectTimeout, MeterValueClock, MeterValuePeriodic, TimeLimitReached, Trigger, UnlockCommand, StopAuthorized, EVDeparted, EVDetected, RemoteStop, RemoteStart, AbnormalCondition, SignedDataReceived, ResetCommand }; class Transaction : public MemoryManaged { public: // ReasonEnumType (3.67) enum class StoppedReason : uint8_t { UNDEFINED, // not part of OCPP DeAuthorized, EmergencyStop, EnergyLimitReached, EVDisconnected, GroundFault, ImmediateReset, Local, LocalOutOfCredit, MasterPass, Other, OvercurrentFault, PowerLoss, PowerQuality, Reboot, Remote, SOCLimitReached, StoppedByEV, TimeLimitReached, Timeout }; //private: /* * Transaction substates. Notify server about any change when transaction is running */ //bool trackParkingBayOccupancy; // not supported bool trackEvConnected = false; bool trackAuthorized = false; bool trackDataSigned = false; bool trackPowerPathClosed = false; bool trackEnergyTransfer = false; /* * Transaction lifecycle */ bool active = true; //once active is false, the tx must stop (or cannot start at all) bool started = false; //if a TxEvent with event type TxStarted has been initiated bool stopped = false; //if a TxEvent with event type TxEnded has been initiated /* * Global transaction data */ bool isAuthorizationActive = false; //period between beginAuthorization and endAuthorization bool isAuthorized = false; //if the given idToken was authorized bool isDeauthorized = false; //if the server revoked a local authorization IdToken idToken; Timestamp beginTimestamp = MIN_TIME; char transactionId [MO_TXID_LEN_MAX + 1] = {'\0'}; int remoteStartId = -1; //if to fill next TxEvent with optional fields bool notifyEvseId = false; bool notifyIdToken = false; bool notifyStopIdToken = false; bool notifyReservationId = false; bool notifyChargingState = false; bool notifyRemoteStartId = false; bool evConnectionTimeoutListen = true; StoppedReason stoppedReason = StoppedReason::UNDEFINED; TransactionEventTriggerReason stopTrigger = TransactionEventTriggerReason::UNDEFINED; std::unique_ptr stopIdToken; // if null, then stopIdToken equals idToken /* * Tx-related metering */ Vector> sampledDataTxEnded; unsigned long lastSampleTimeTxUpdated = 0; //0 means not charging right now unsigned long lastSampleTimeTxEnded = 0; /* * Attributes for internal store */ unsigned int evseId = 0; unsigned int txNr = 0; //internal key attribute (!= transactionId); {evseId*txNr} is unique key unsigned int seqNoEnd = 0; // increment by 1 for each event Vector seqNos; //track stored txEvents bool silent = false; //silent Tx: process tx locally, without reporting to the server Transaction() : MemoryManaged("v201.Transactions.Transaction"), sampledDataTxEnded(makeVector>(getMemoryTag())), seqNos(makeVector(getMemoryTag())) { } void addSampledDataTxEnded(std::unique_ptr mv) { if (sampledDataTxEnded.size() >= MO_SAMPLEDDATATXENDED_SIZE_MAX) { int deltaMin = std::numeric_limits::max(); size_t indexMin = sampledDataTxEnded.size(); for (size_t i = 1; i + 1 <= sampledDataTxEnded.size(); i++) { size_t t0 = sampledDataTxEnded.size() - i - 1; size_t t1 = sampledDataTxEnded.size() - i; auto delta = sampledDataTxEnded[t1]->getTimestamp() - sampledDataTxEnded[t0]->getTimestamp(); if (delta < deltaMin) { deltaMin = delta; indexMin = t1; } } sampledDataTxEnded.erase(sampledDataTxEnded.begin() + indexMin); } sampledDataTxEnded.push_back(std::move(mv)); } }; // TransactionEventRequest (1.60.1) class TransactionEventData : public MemoryManaged { public: // TransactionEventEnumType (3.80) enum class Type : uint8_t { Ended, Started, Updated }; // ChargingStateEnumType (3.16) enum class ChargingState : uint8_t { UNDEFINED, // not part of OCPP Charging, EVConnected, SuspendedEV, SuspendedEVSE, Idle }; //private: Transaction *transaction; Type eventType; Timestamp timestamp; uint16_t bootNr = 0; TransactionEventTriggerReason triggerReason; const unsigned int seqNo; bool offline = false; int numberOfPhasesUsed = -1; int cableMaxCurrent = -1; int reservationId = -1; int remoteStartId = -1; // TransactionType (2.48) ChargingState chargingState = ChargingState::UNDEFINED; //int timeSpentCharging = 0; // not supported std::unique_ptr idToken; EvseId evse = -1; //meterValue not supported Vector> meterValue; unsigned int opNr = 0; unsigned int attemptNr = 0; Timestamp attemptTime = MIN_TIME; TransactionEventData(Transaction *transaction, unsigned int seqNo) : MemoryManaged("v201.Transactions.TransactionEventData"), transaction(transaction), seqNo(seqNo), meterValue(makeVector>(getMemoryTag())) { } }; const char *serializeTransactionStoppedReason(Transaction::StoppedReason stoppedReason); bool deserializeTransactionStoppedReason(const char *stoppedReasonCstr, Transaction::StoppedReason& stoppedReasonOut); const char *serializeTransactionEventType(TransactionEventData::Type type); bool deserializeTransactionEventType(const char *typeCstr, TransactionEventData::Type& typeOut); const char *serializeTransactionEventTriggerReason(TransactionEventTriggerReason triggerReason); bool deserializeTransactionEventTriggerReason(const char *triggerReasonCstr, TransactionEventTriggerReason& triggerReasonOut); const char *serializeTransactionEventChargingState(TransactionEventData::ChargingState chargingState); bool deserializeTransactionEventChargingState(const char *chargingStateCstr, TransactionEventData::ChargingState& chargingStateOut); } // namespace Ocpp201 } // namespace MicroOcpp #endif // MO_ENABLE_V201 extern "C" { #endif //__cplusplus struct OCPP_Transaction; typedef struct OCPP_Transaction OCPP_Transaction; /* * Compat mode for transactions. This means that all following C-wrapper functions will interprete the handle as v201 transactions */ #if MO_ENABLE_V201 void ocpp_tx_compat_setV201(bool isV201); //if set, all OCPP_Transaction* handles are treated as v201 transactions #endif int ocpp_tx_getTransactionId(OCPP_Transaction *tx); #if MO_ENABLE_V201 const char *ocpp_tx_getTransactionIdV201(OCPP_Transaction *tx); #endif bool ocpp_tx_isAuthorized(OCPP_Transaction *tx); bool ocpp_tx_isIdTagDeauthorized(OCPP_Transaction *tx); bool ocpp_tx_isRunning(OCPP_Transaction *tx); bool ocpp_tx_isActive(OCPP_Transaction *tx); bool ocpp_tx_isAborted(OCPP_Transaction *tx); bool ocpp_tx_isCompleted(OCPP_Transaction *tx); const char *ocpp_tx_getIdTag(OCPP_Transaction *tx); const char *ocpp_tx_getParentIdTag(OCPP_Transaction *tx); bool ocpp_tx_getBeginTimestamp(OCPP_Transaction *tx, char *buf, size_t len); int32_t ocpp_tx_getMeterStart(OCPP_Transaction *tx); bool ocpp_tx_getStartTimestamp(OCPP_Transaction *tx, char *buf, size_t len); const char *ocpp_tx_getStopIdTag(OCPP_Transaction *tx); int32_t ocpp_tx_getMeterStop(OCPP_Transaction *tx); void ocpp_tx_setMeterStop(OCPP_Transaction* tx, int32_t meter); bool ocpp_tx_getStopTimestamp(OCPP_Transaction *tx, char *buf, size_t len); const char *ocpp_tx_getStopReason(OCPP_Transaction *tx); #ifdef __cplusplus } //end extern "C" #endif #endif ================================================ FILE: src/MicroOcpp/Model/Transactions/TransactionDefs.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_TRANSACTIONDEFS_H #define MO_TRANSACTIONDEFS_H #include #if MO_ENABLE_V201 #define MO_TXID_LEN_MAX 36 #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include namespace MicroOcpp { bool serializeSendStatus(SendStatus& status, JsonObject out) { if (status.isRequested()) { out["requested"] = true; } if (status.isConfirmed()) { out["confirmed"] = true; } out["opNr"] = status.getOpNr(); if (status.getAttemptNr() != 0) { out["attemptNr"] = status.getAttemptNr(); } if (status.getAttemptTime() > MIN_TIME) { char attemptTime [JSONDATE_LENGTH + 1]; status.getAttemptTime().toJsonString(attemptTime, sizeof(attemptTime)); out["attemptTime"] = attemptTime; } return true; } bool deserializeSendStatus(SendStatus& status, JsonObject in) { if (in["requested"] | false) { status.setRequested(); } if (in["confirmed"] | false) { status.confirm(); } unsigned int opNr = in["opNr"] | (unsigned int)0; if (opNr >= 10) { //10 is first valid tx-related opNr status.setOpNr(opNr); } status.setAttemptNr(in["attemptNr"] | (unsigned int)0); if (in.containsKey("attemptTime")) { Timestamp attemptTime; if (!attemptTime.setTime(in["attemptTime"] | "_Invalid")) { MO_DBG_ERR("deserialization error"); return false; } status.setAttemptTime(attemptTime); } return true; } bool serializeTransaction(Transaction& tx, JsonDoc& out) { out = initJsonDoc("v16.Transactions.TransactionDeserialize", 1024); JsonObject state = out.to(); JsonObject sessionState = state.createNestedObject("session"); if (!tx.isActive()) { sessionState["active"] = false; } if (tx.getIdTag()[0] != '\0') { sessionState["idTag"] = tx.getIdTag(); } if (tx.getParentIdTag()[0] != '\0') { sessionState["parentIdTag"] = tx.getParentIdTag(); } if (tx.isAuthorized()) { sessionState["authorized"] = true; } if (tx.isIdTagDeauthorized()) { sessionState["deauthorized"] = true; } if (tx.getBeginTimestamp() > MIN_TIME) { char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; tx.getBeginTimestamp().toJsonString(timeStr, JSONDATE_LENGTH + 1); sessionState["timestamp"] = timeStr; } if (tx.getReservationId() >= 0) { sessionState["reservationId"] = tx.getReservationId(); } if (tx.getTxProfileId() >= 0) { sessionState["txProfileId"] = tx.getTxProfileId(); } JsonObject txStart = state.createNestedObject("start"); if (!serializeSendStatus(tx.getStartSync(), txStart)) { return false; } if (tx.isMeterStartDefined()) { txStart["meter"] = tx.getMeterStart(); } char startTimeStr [JSONDATE_LENGTH + 1] = {'\0'}; tx.getStartTimestamp().toJsonString(startTimeStr, JSONDATE_LENGTH + 1); txStart["timestamp"] = startTimeStr; txStart["bootNr"] = tx.getStartBootNr(); if (tx.getStartSync().isConfirmed()) { txStart["transactionId"] = tx.getTransactionId(); } JsonObject txStop = state.createNestedObject("stop"); if (!serializeSendStatus(tx.getStopSync(), txStop)) { return false; } if (tx.getStopIdTag()[0] != '\0') { txStop["idTag"] = tx.getStopIdTag(); } if (tx.isMeterStopDefined()) { txStop["meter"] = tx.getMeterStop(); } char stopTimeStr [JSONDATE_LENGTH + 1] = {'\0'}; tx.getStopTimestamp().toJsonString(stopTimeStr, JSONDATE_LENGTH + 1); txStop["timestamp"] = stopTimeStr; txStop["bootNr"] = tx.getStopBootNr(); if (tx.getStopReason()[0] != '\0') { txStop["reason"] = tx.getStopReason(); } if (tx.isSilent()) { state["silent"] = true; } if (out.overflowed()) { MO_DBG_ERR("JSON capacity exceeded"); return false; } return true; } bool deserializeTransaction(Transaction& tx, JsonObject state) { JsonObject sessionState = state["session"]; if (!(sessionState["active"] | true)) { tx.setInactive(); } if (sessionState.containsKey("idTag")) { if (!tx.setIdTag(sessionState["idTag"] | "")) { MO_DBG_ERR("read err"); return false; } } if (sessionState.containsKey("parentIdTag")) { if (!tx.setParentIdTag(sessionState["parentIdTag"] | "")) { MO_DBG_ERR("read err"); return false; } } if (sessionState["authorized"] | false) { tx.setAuthorized(); } if (sessionState["deauthorized"] | false) { tx.setIdTagDeauthorized(); } if (sessionState.containsKey("timestamp")) { Timestamp timestamp; if (!timestamp.setTime(sessionState["timestamp"] | "Invalid")) { MO_DBG_ERR("read err"); return false; } tx.setBeginTimestamp(timestamp); } if (sessionState.containsKey("reservationId")) { tx.setReservationId(sessionState["reservationId"] | -1); } if (sessionState.containsKey("txProfileId")) { tx.setTxProfileId(sessionState["txProfileId"] | -1); } JsonObject txStart = state["start"]; if (!deserializeSendStatus(tx.getStartSync(), txStart)) { return false; } if (txStart.containsKey("meter")) { tx.setMeterStart(txStart["meter"] | 0); } if (txStart.containsKey("timestamp")) { Timestamp timestamp; if (!timestamp.setTime(txStart["timestamp"] | "Invalid")) { MO_DBG_ERR("read err"); return false; } tx.setStartTimestamp(timestamp); } if (txStart.containsKey("bootNr")) { int bootNrIn = txStart["bootNr"]; if (bootNrIn >= 0 && bootNrIn <= std::numeric_limits::max()) { tx.setStartBootNr((uint16_t) bootNrIn); } else { MO_DBG_ERR("read err"); return false; } } if (txStart.containsKey("transactionId")) { tx.setTransactionId(txStart["transactionId"] | -1); } JsonObject txStop = state["stop"]; if (!deserializeSendStatus(tx.getStopSync(), txStop)) { return false; } if (txStop.containsKey("idTag")) { if (!tx.setStopIdTag(txStop["idTag"] | "")) { MO_DBG_ERR("read err"); return false; } } if (txStop.containsKey("meter")) { tx.setMeterStop(txStop["meter"] | 0); } if (txStop.containsKey("timestamp")) { Timestamp timestamp; if (!timestamp.setTime(txStop["timestamp"] | "Invalid")) { MO_DBG_ERR("read err"); return false; } tx.setStopTimestamp(timestamp); } if (txStop.containsKey("bootNr")) { int bootNrIn = txStop["bootNr"]; if (bootNrIn >= 0 && bootNrIn <= std::numeric_limits::max()) { tx.setStopBootNr((uint16_t) bootNrIn); } else { MO_DBG_ERR("read err"); return false; } } if (txStop.containsKey("reason")) { if (!tx.setStopReason(txStop["reason"] | "")) { MO_DBG_ERR("read err"); return false; } } if (state["silent"] | false) { tx.setSilent(); } MO_DBG_DEBUG("DUMP TX (%s)", tx.getIdTag() ? tx.getIdTag() : "idTag missing"); MO_DBG_DEBUG("Session | idTag %s, active: %i, authorized: %i, deauthorized: %i", tx.getIdTag(), tx.isActive(), tx.isAuthorized(), tx.isIdTagDeauthorized()); MO_DBG_DEBUG("Start RPC | req: %i, conf: %i", tx.getStartSync().isRequested(), tx.getStartSync().isConfirmed()); MO_DBG_DEBUG("Stop RPC | req: %i, conf: %i", tx.getStopSync().isRequested(), tx.getStopSync().isConfirmed()); if (tx.isSilent()) { MO_DBG_DEBUG(" | silent Tx"); } return true; } } ================================================ FILE: src/MicroOcpp/Model/Transactions/TransactionDeserialize.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_TRANSACTIONDESERIALIZE_H #define MO_TRANSACTIONDESERIALIZE_H #include #include #include namespace MicroOcpp { bool serializeTransaction(Transaction& tx, JsonDoc& out); bool deserializeTransaction(Transaction& tx, JsonObject in); } #endif ================================================ FILE: src/MicroOcpp/Model/Transactions/TransactionService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef MO_TX_CLEAN_ABORTED #define MO_TX_CLEAN_ABORTED 1 #endif using namespace MicroOcpp; using namespace MicroOcpp::Ocpp201; TransactionService::Evse::Evse(Context& context, TransactionService& txService, Ocpp201::TransactionStoreEvse& txStore, unsigned int evseId) : MemoryManaged("v201.Transactions.TransactionServiceEvse"), context(context), txService(txService), txStore(txStore), evseId(evseId) { context.getRequestQueue().addSendQueue(this); //register at RequestQueue as Request emitter txStore.discoverStoredTx(txNrBegin, txNrEnd); //initializes txNrBegin and txNrEnd txNrFront = txNrBegin; MO_DBG_DEBUG("found %u transactions for evseId %u. Internal range from %u to %u (exclusive)", (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT, evseId, txNrBegin, txNrEnd); unsigned int txNrLatest = (txNrEnd + MAX_TX_CNT - 1) % MAX_TX_CNT; //txNr of the most recent tx on flash transaction = txStore.loadTransaction(txNrLatest); //returns nullptr if txNrLatest does not exist on flash } TransactionService::Evse::~Evse() { } bool TransactionService::Evse::beginTransaction() { if (transaction) { MO_DBG_ERR("transaction still running"); return false; } std::unique_ptr tx; char txId [sizeof(Ocpp201::Transaction::transactionId)]; //simple clock-based hash int v = context.getModel().getClock().now() - Timestamp(2020,0,0,0,0,0); unsigned int h = v; h += mocpp_tick_ms(); h *= 749572633U; h %= 24593209U; for (size_t i = 0; i < sizeof(tx->transactionId) - 3; i += 2) { snprintf(txId + i, 3, "%02X", (uint8_t)h); h *= 749572633U; h %= 24593209U; } //clean possible aborted tx unsigned int txr = txNrEnd; unsigned int txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; for (unsigned int i = 0; i < txSize; i++) { txr = (txr + MAX_TX_CNT - 1) % MAX_TX_CNT; //decrement by 1 std::unique_ptr intermediateTx; Ocpp201::Transaction *txhist = nullptr; if (transaction && transaction->txNr == txr) { txhist = transaction.get(); } else if (txFront && txFront->txNr == txr) { txhist = txFront; } else { intermediateTx = txStore.loadTransaction(txr); txhist = intermediateTx.get(); } //check if dangling silent tx, aborted tx, or corrupted entry (txhist == null) if (!txhist || txhist->silent || (!txhist->active && !txhist->started && MO_TX_CLEAN_ABORTED)) { //yes, remove if (txStore.remove(txr)) { if (txNrFront == txNrEnd) { txNrFront = txr; } txNrEnd = txr; MO_DBG_WARN("deleted dangling silent or aborted tx for new transaction"); } else { MO_DBG_ERR("memory corruption"); break; } } else { //no, tx record trimmed, end break; } } txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; //refresh after cleaning txs //try to create new transaction if (txSize < MO_TXRECORD_SIZE) { tx = txStore.createTransaction(txNrEnd, txId); } if (!tx) { //could not create transaction - now, try to replace tx history entry unsigned int txl = txNrBegin; txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; for (unsigned int i = 0; i < txSize; i++) { if (tx) { //success, finished here break; } //no transaction allocated, delete history entry to make space std::unique_ptr intermediateTx; Ocpp201::Transaction *txhist = nullptr; if (transaction && transaction->txNr == txl) { txhist = transaction.get(); } else if (txFront && txFront->txNr == txl) { txhist = txFront; } else { intermediateTx = txStore.loadTransaction(txl); txhist = intermediateTx.get(); } //oldest entry, now check if it's history and can be removed or corrupted entry if (!txhist || (txhist->stopped && txhist->seqNos.empty()) || (!txhist->active && !txhist->started) || (txhist->silent && txhist->stopped)) { //yes, remove if (txStore.remove(txl)) { txNrBegin = (txl + 1) % MAX_TX_CNT; if (txNrFront == txl) { txNrFront = txNrBegin; } MO_DBG_DEBUG("deleted tx history entry for new transaction"); MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); tx = txStore.createTransaction(txNrEnd, txId); } else { MO_DBG_ERR("memory corruption"); break; } } else { //no, end of history reached, don't delete further tx MO_DBG_DEBUG("cannot delete more tx"); break; } txl++; txl %= MAX_TX_CNT; } } if (!tx) { //couldn't create normal transaction -> check if to start charging without real transaction if (txService.silentOfflineTransactionsBool && txService.silentOfflineTransactionsBool->getBool()) { //try to handle charging session without sending StartTx or StopTx to the server tx = txStore.createTransaction(txNrEnd, txId); if (tx) { tx->silent = true; MO_DBG_DEBUG("created silent transaction"); } } } if (!tx) { MO_DBG_ERR("transaction queue full"); return false; } tx->beginTimestamp = context.getModel().getClock().now(); if (!txStore.commit(tx.get())) { MO_DBG_ERR("fs error"); return false; } transaction = std::move(tx); txNrEnd = (txNrEnd + 1) % MAX_TX_CNT; MO_DBG_DEBUG("advance txNrEnd %u-%u", evseId, txNrEnd); MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); return true; } bool TransactionService::Evse::endTransaction(Ocpp201::Transaction::StoppedReason stoppedReason = Ocpp201::Transaction::StoppedReason::Other, Ocpp201::TransactionEventTriggerReason stopTrigger = Ocpp201::TransactionEventTriggerReason::AbnormalCondition) { if (!transaction || !transaction->active) { //transaction already ended / not active anymore return false; } MO_DBG_DEBUG("End transaction started by idTag %s", transaction->idToken.get()); transaction->active = false; transaction->stopTrigger = stopTrigger; transaction->stoppedReason = stoppedReason; txStore.commit(transaction.get()); return true; } void TransactionService::Evse::loop() { if (transaction && !transaction->active && !transaction->started) { MO_DBG_DEBUG("collect aborted transaction %u-%s", evseId, transaction->transactionId); if (txFront == transaction.get()) { MO_DBG_DEBUG("pass ownership from tx to txFront"); txFrontCache = std::move(transaction); } transaction = nullptr; } if (transaction && transaction->stopped) { MO_DBG_DEBUG("collect obsolete transaction %u-%s", evseId, transaction->transactionId); if (txFront == transaction.get()) { MO_DBG_DEBUG("pass ownership from tx to txFront"); txFrontCache = std::move(transaction); } transaction = nullptr; } // tx-related behavior if (transaction) { if (connectorPluggedInput) { if (connectorPluggedInput()) { // if cable has been plugged at least once, EVConnectionTimeout will never get triggered transaction->evConnectionTimeoutListen = false; } if (transaction->active && transaction->evConnectionTimeoutListen && transaction->beginTimestamp > MIN_TIME && txService.evConnectionTimeOutInt && txService.evConnectionTimeOutInt->getInt() > 0 && !connectorPluggedInput() && context.getModel().getClock().now() - transaction->beginTimestamp >= txService.evConnectionTimeOutInt->getInt()) { MO_DBG_INFO("Session mngt: timeout"); endTransaction(Ocpp201::Transaction::StoppedReason::Timeout, TransactionEventTriggerReason::EVConnectTimeout); updateTxNotification(TxNotification_ConnectionTimeout); } if (transaction->active && transaction->isDeauthorized && !transaction->started && (txService.isTxStartPoint(TxStartStopPoint::Authorized) || txService.isTxStartPoint(TxStartStopPoint::PowerPathClosed) || txService.isTxStopPoint(TxStartStopPoint::Authorized) || txService.isTxStopPoint(TxStartStopPoint::PowerPathClosed))) { MO_DBG_INFO("Session mngt: Deauthorized before start"); endTransaction(Ocpp201::Transaction::StoppedReason::DeAuthorized, TransactionEventTriggerReason::Deauthorized); } } } std::unique_ptr txEvent; bool txStopCondition = false; { // stop tx? TransactionEventTriggerReason triggerReason = TransactionEventTriggerReason::UNDEFINED; Ocpp201::Transaction::StoppedReason stoppedReason = Ocpp201::Transaction::StoppedReason::UNDEFINED; if (transaction && !transaction->active) { // tx ended via endTransaction txStopCondition = true; triggerReason = transaction->stopTrigger; stoppedReason = transaction->stoppedReason; } else if ((txService.isTxStopPoint(TxStartStopPoint::EVConnected) || txService.isTxStopPoint(TxStartStopPoint::PowerPathClosed)) && connectorPluggedInput && !connectorPluggedInput() && (txService.stopTxOnEVSideDisconnectBool->getBool() || !transaction || !transaction->started)) { txStopCondition = true; triggerReason = TransactionEventTriggerReason::EVCommunicationLost; stoppedReason = Ocpp201::Transaction::StoppedReason::EVDisconnected; } else if ((txService.isTxStopPoint(TxStartStopPoint::Authorized) || txService.isTxStopPoint(TxStartStopPoint::PowerPathClosed)) && (!transaction || !transaction->isAuthorizationActive)) { // user revoked authorization (or EV or any "local" entity) txStopCondition = true; triggerReason = TransactionEventTriggerReason::StopAuthorized; stoppedReason = Ocpp201::Transaction::StoppedReason::Local; } else if (txService.isTxStopPoint(TxStartStopPoint::EnergyTransfer) && evReadyInput && !evReadyInput()) { txStopCondition = true; triggerReason = TransactionEventTriggerReason::ChargingStateChanged; stoppedReason = Ocpp201::Transaction::StoppedReason::StoppedByEV; } else if (txService.isTxStopPoint(TxStartStopPoint::EnergyTransfer) && (evReadyInput || evseReadyInput) && // at least one of the two defined !(evReadyInput && evReadyInput()) && !(evseReadyInput && evseReadyInput())) { txStopCondition = true; triggerReason = TransactionEventTriggerReason::ChargingStateChanged; stoppedReason = Ocpp201::Transaction::StoppedReason::Other; } else if (txService.isTxStopPoint(TxStartStopPoint::Authorized) && transaction && transaction->isDeauthorized && txService.stopTxOnInvalidIdBool->getBool()) { // OCPP server rejected authorization txStopCondition = true; triggerReason = TransactionEventTriggerReason::Deauthorized; stoppedReason = Ocpp201::Transaction::StoppedReason::DeAuthorized; } if (txStopCondition && transaction && transaction->started && transaction->active) { MO_DBG_INFO("Session mngt: TxStopPoint reached"); endTransaction(stoppedReason, triggerReason); } if (transaction && transaction->started && !transaction->stopped && !transaction->active && (!stopTxReadyInput || stopTxReadyInput())) { // yes, stop running tx txEvent = txStore.createTransactionEvent(*transaction); if (!txEvent) { // OOM return; } transaction->stopTrigger = triggerReason; transaction->stoppedReason = stoppedReason; txEvent->eventType = TransactionEventData::Type::Ended; txEvent->triggerReason = triggerReason; } } if (!txStopCondition) { // start tx? bool txStartCondition = false; TransactionEventTriggerReason triggerReason = TransactionEventTriggerReason::UNDEFINED; // tx should be started? if (txService.isTxStartPoint(TxStartStopPoint::PowerPathClosed) && (!connectorPluggedInput || connectorPluggedInput()) && transaction && transaction->isAuthorizationActive && transaction->isAuthorized) { txStartCondition = true; if (transaction->remoteStartId >= 0) { triggerReason = TransactionEventTriggerReason::RemoteStart; } else { triggerReason = TransactionEventTriggerReason::CablePluggedIn; } } else if (txService.isTxStartPoint(TxStartStopPoint::Authorized) && transaction && transaction->isAuthorizationActive && transaction->isAuthorized) { txStartCondition = true; if (transaction->remoteStartId >= 0) { triggerReason = TransactionEventTriggerReason::RemoteStart; } else { triggerReason = TransactionEventTriggerReason::Authorized; } } else if (txService.isTxStartPoint(TxStartStopPoint::EVConnected) && connectorPluggedInput && connectorPluggedInput()) { txStartCondition = true; triggerReason = TransactionEventTriggerReason::CablePluggedIn; } else if (txService.isTxStartPoint(TxStartStopPoint::EnergyTransfer) && (evReadyInput || evseReadyInput) && // at least one of the two defined (!evReadyInput || evReadyInput()) && (!evseReadyInput || evseReadyInput())) { txStartCondition = true; triggerReason = TransactionEventTriggerReason::ChargingStateChanged; } if (txStartCondition && (!transaction || (transaction->active && !transaction->started)) && (!startTxReadyInput || startTxReadyInput())) { // start tx if (!transaction) { beginTransaction(); if (!transaction) { // OOM return; } if (evseId > 0) { transaction->notifyEvseId = true; } } txEvent = txStore.createTransactionEvent(*transaction); if (!txEvent) { // OOM return; } txEvent->eventType = TransactionEventData::Type::Started; txEvent->triggerReason = triggerReason; } } TransactionEventData::ChargingState chargingState = TransactionEventData::ChargingState::Idle; if (connectorPluggedInput && !connectorPluggedInput()) { chargingState = TransactionEventData::ChargingState::Idle; } else if (!transaction || !transaction->isAuthorizationActive || !transaction->isAuthorized) { chargingState = TransactionEventData::ChargingState::EVConnected; } else if (evseReadyInput && !evseReadyInput()) { chargingState = TransactionEventData::ChargingState::SuspendedEVSE; } else if (evReadyInput && !evReadyInput()) { chargingState = TransactionEventData::ChargingState::SuspendedEV; } else if (ocppPermitsCharge()) { chargingState = TransactionEventData::ChargingState::Charging; } //General Metering behavior. There is another section for TxStarted, Updated and TxEnded MeterValues std::unique_ptr mvTxUpdated; if (transaction) { if (txService.sampledDataTxUpdatedInterval && txService.sampledDataTxUpdatedInterval->getInt() > 0 && mocpp_tick_ms() - transaction->lastSampleTimeTxUpdated >= (unsigned long)txService.sampledDataTxUpdatedInterval->getInt() * 1000UL) { transaction->lastSampleTimeTxUpdated = mocpp_tick_ms(); auto meteringService = context.getModel().getMeteringServiceV201(); auto meteringEvse = meteringService ? meteringService->getEvse(evseId) : nullptr; mvTxUpdated = meteringEvse ? meteringEvse->takeTxUpdatedMeterValue() : nullptr; } if (transaction->started && !transaction->stopped && txService.sampledDataTxEndedInterval && txService.sampledDataTxEndedInterval->getInt() > 0 && mocpp_tick_ms() - transaction->lastSampleTimeTxEnded >= (unsigned long)txService.sampledDataTxEndedInterval->getInt() * 1000UL) { transaction->lastSampleTimeTxEnded = mocpp_tick_ms(); auto meteringService = context.getModel().getMeteringServiceV201(); auto meteringEvse = meteringService ? meteringService->getEvse(evseId) : nullptr; auto mvTxEnded = meteringEvse ? meteringEvse->takeTxEndedMeterValue(ReadingContext_SamplePeriodic) : nullptr; if (mvTxEnded) { transaction->addSampledDataTxEnded(std::move(mvTxEnded)); } } } if (transaction) { // update tx? bool txUpdateCondition = false; TransactionEventTriggerReason triggerReason = TransactionEventTriggerReason::UNDEFINED; if (chargingState != trackChargingState) { txUpdateCondition = true; triggerReason = TransactionEventTriggerReason::ChargingStateChanged; transaction->notifyChargingState = true; } trackChargingState = chargingState; if ((transaction->isAuthorizationActive && transaction->isAuthorized) && !transaction->trackAuthorized) { transaction->trackAuthorized = true; txUpdateCondition = true; if (transaction->remoteStartId >= 0) { triggerReason = TransactionEventTriggerReason::RemoteStart; } else { triggerReason = TransactionEventTriggerReason::Authorized; } } else if (connectorPluggedInput && connectorPluggedInput() && !transaction->trackEvConnected) { transaction->trackEvConnected = true; txUpdateCondition = true; triggerReason = TransactionEventTriggerReason::CablePluggedIn; } else if (connectorPluggedInput && !connectorPluggedInput() && transaction->trackEvConnected) { transaction->trackEvConnected = false; txUpdateCondition = true; triggerReason = TransactionEventTriggerReason::EVCommunicationLost; } else if (!(transaction->isAuthorizationActive && transaction->isAuthorized) && transaction->trackAuthorized) { transaction->trackAuthorized = false; txUpdateCondition = true; triggerReason = TransactionEventTriggerReason::StopAuthorized; } else if (mvTxUpdated) { txUpdateCondition = true; triggerReason = TransactionEventTriggerReason::MeterValuePeriodic; } else if (evReadyInput && evReadyInput() && !transaction->trackPowerPathClosed) { transaction->trackPowerPathClosed = true; } else if (evReadyInput && !evReadyInput() && transaction->trackPowerPathClosed) { transaction->trackPowerPathClosed = false; } if (txUpdateCondition && !txEvent && transaction->started && !transaction->stopped) { // yes, updated txEvent = txStore.createTransactionEvent(*transaction); if (!txEvent) { // OOM return; } txEvent->eventType = TransactionEventData::Type::Updated; txEvent->triggerReason = triggerReason; } } if (txEvent) { txEvent->timestamp = context.getModel().getClock().now(); if (transaction->notifyChargingState) { txEvent->chargingState = chargingState; transaction->notifyChargingState = false; } if (transaction->notifyEvseId) { txEvent->evse = EvseId(evseId, 1); transaction->notifyEvseId = false; } if (transaction->notifyRemoteStartId) { txEvent->remoteStartId = transaction->remoteStartId; transaction->notifyRemoteStartId = false; } if (txEvent->eventType == TransactionEventData::Type::Started) { auto meteringService = context.getModel().getMeteringServiceV201(); auto meteringEvse = meteringService ? meteringService->getEvse(evseId) : nullptr; auto mvTxStarted = meteringEvse ? meteringEvse->takeTxStartedMeterValue() : nullptr; if (mvTxStarted) { txEvent->meterValue.push_back(std::move(mvTxStarted)); } auto mvTxEnded = meteringEvse ? meteringEvse->takeTxEndedMeterValue(ReadingContext_TransactionBegin) : nullptr; if (mvTxEnded) { transaction->addSampledDataTxEnded(std::move(mvTxEnded)); } transaction->lastSampleTimeTxEnded = mocpp_tick_ms(); transaction->lastSampleTimeTxUpdated = mocpp_tick_ms(); } else if (txEvent->eventType == TransactionEventData::Type::Ended) { auto meteringService = context.getModel().getMeteringServiceV201(); auto meteringEvse = meteringService ? meteringService->getEvse(evseId) : nullptr; auto mvTxEnded = meteringEvse ? meteringEvse->takeTxEndedMeterValue(ReadingContext_TransactionEnd) : nullptr; if (mvTxEnded) { transaction->addSampledDataTxEnded(std::move(mvTxEnded)); } transaction->lastSampleTimeTxEnded = mocpp_tick_ms(); } if (mvTxUpdated) { txEvent->meterValue.push_back(std::move(mvTxUpdated)); } if (transaction->notifyStopIdToken && transaction->stopIdToken) { txEvent->idToken = std::unique_ptr(new IdToken(*transaction->stopIdToken.get(), getMemoryTag())); transaction->notifyStopIdToken = false; } else if (transaction->notifyIdToken) { txEvent->idToken = std::unique_ptr(new IdToken(transaction->idToken, getMemoryTag())); transaction->notifyIdToken = false; } } if (txEvent) { if (txEvent->eventType == TransactionEventData::Type::Started) { transaction->started = true; } else if (txEvent->eventType == TransactionEventData::Type::Ended) { transaction->stopped = true; } } if (txEvent) { txEvent->opNr = context.getRequestQueue().getNextOpNr(); MO_DBG_DEBUG("enqueueing new txEvent at opNr %u", txEvent->opNr); } if (txEvent) { txStore.commit(txEvent.get()); } if (txEvent) { if (txEvent->eventType == TransactionEventData::Type::Started) { updateTxNotification(TxNotification_StartTx); } else if (txEvent->eventType == TransactionEventData::Type::Ended) { updateTxNotification(TxNotification_StartTx); } } //try to pass ownership to front txEvent immediatley if (txEvent && !txEventFront && transaction->txNr == txNrFront && !transaction->seqNos.empty() && transaction->seqNos.front() == txEvent->seqNo) { //txFront set up? if (!txFront) { txFront = transaction.get(); } //keep txEvent loaded (otherwise ReqEmitter would load it again from flash) MO_DBG_DEBUG("new txEvent is front element"); txEventFront = std::move(txEvent); } } void TransactionService::Evse::setConnectorPluggedInput(std::function connectorPlugged) { this->connectorPluggedInput = connectorPlugged; } void TransactionService::Evse::setEvReadyInput(std::function evRequestsEnergy) { this->evReadyInput = evRequestsEnergy; } void TransactionService::Evse::setEvseReadyInput(std::function connectorEnergized) { this->evseReadyInput = connectorEnergized; } void TransactionService::Evse::setTxNotificationOutput(std::function txNotificationOutput) { this->txNotificationOutput = txNotificationOutput; } void TransactionService::Evse::updateTxNotification(TxNotification event) { if (txNotificationOutput) { txNotificationOutput(transaction.get(), event); } } bool TransactionService::Evse::beginAuthorization(IdToken idToken, bool validateIdToken) { MO_DBG_DEBUG("begin auth: %s", idToken.get()); if (transaction && transaction->isAuthorizationActive) { MO_DBG_WARN("tx process still running. Please call endTransaction(...) before"); return false; } if (!transaction) { beginTransaction(); if (!transaction) { MO_DBG_ERR("could not allocate Tx"); return false; } if (evseId > 0) { transaction->notifyEvseId = true; } } transaction->isAuthorizationActive = true; transaction->idToken = idToken; transaction->beginTimestamp = context.getModel().getClock().now(); if (validateIdToken) { auto authorize = makeRequest(new Authorize(context.getModel(), idToken)); if (!authorize) { // OOM abortTransaction(); return false; } char txId [sizeof(transaction->transactionId)]; //capture txId to check if transaction reference is still the same snprintf(txId, sizeof(txId), "%s", transaction->transactionId); authorize->setOnReceiveConfListener([this, txId] (JsonObject response) { auto tx = getTransaction(); if (!tx || strcmp(tx->transactionId, txId)) { MO_DBG_INFO("dangling Authorize -- discard"); return; } if (strcmp(response["idTokenInfo"]["status"] | "_Undefined", "Accepted")) { MO_DBG_DEBUG("Authorize rejected (%s), abort tx process", tx->idToken.get()); tx->isDeauthorized = true; txStore.commit(tx); updateTxNotification(TxNotification_AuthorizationRejected); return; } MO_DBG_DEBUG("Authorized tx with validation (%s)", tx->idToken.get()); tx->isAuthorized = true; tx->notifyIdToken = true; txStore.commit(tx); updateTxNotification(TxNotification_Authorized); }); authorize->setOnAbortListener([this, txId] () { auto tx = getTransaction(); if (!tx || strcmp(tx->transactionId, txId)) { MO_DBG_INFO("dangling Authorize -- discard"); return; } MO_DBG_DEBUG("Authorize timeout (%s)", tx->idToken.get()); tx->isDeauthorized = true; txStore.commit(tx); updateTxNotification(TxNotification_AuthorizationTimeout); }); authorize->setTimeout(20 * 1000); context.initiateRequest(std::move(authorize)); } else { MO_DBG_DEBUG("Authorized tx directly (%s)", transaction->idToken.get()); transaction->isAuthorized = true; transaction->notifyIdToken = true; txStore.commit(transaction.get()); updateTxNotification(TxNotification_Authorized); } return true; } bool TransactionService::Evse::endAuthorization(IdToken idToken, bool validateIdToken) { if (!transaction || !transaction->isAuthorizationActive) { //transaction already ended / not active anymore return false; } MO_DBG_DEBUG("End session started by idTag %s", transaction->idToken.get()); if (transaction->idToken.equals(idToken)) { // use same idToken like tx start transaction->isAuthorizationActive = false; transaction->notifyIdToken = true; txStore.commit(transaction.get()); updateTxNotification(TxNotification_Authorized); } else if (!validateIdToken) { transaction->stopIdToken = std::unique_ptr(new IdToken(idToken, getMemoryTag())); transaction->isAuthorizationActive = false; transaction->notifyStopIdToken = true; txStore.commit(transaction.get()); updateTxNotification(TxNotification_Authorized); } else { // use a different idToken for stopping the tx auto authorize = makeRequest(new Authorize(context.getModel(), idToken)); if (!authorize) { // OOM abortTransaction(); return false; } char txId [sizeof(transaction->transactionId)]; //capture txId to check if transaction reference is still the same snprintf(txId, sizeof(txId), "%s", transaction->transactionId); authorize->setOnReceiveConfListener([this, txId, idToken] (JsonObject response) { auto tx = getTransaction(); if (!tx || strcmp(tx->transactionId, txId)) { MO_DBG_INFO("dangling Authorize -- discard"); return; } if (strcmp(response["idTokenInfo"]["status"] | "_Undefined", "Accepted")) { MO_DBG_DEBUG("Authorize rejected (%s), don't stop tx", idToken.get()); updateTxNotification(TxNotification_AuthorizationRejected); return; } MO_DBG_DEBUG("Authorized transaction stop (%s)", idToken.get()); tx->stopIdToken = std::unique_ptr(new IdToken(idToken, getMemoryTag())); if (!tx->stopIdToken) { // OOM if (tx->active) { abortTransaction(); } return; } tx->isAuthorizationActive = false; tx->notifyStopIdToken = true; txStore.commit(tx); updateTxNotification(TxNotification_Authorized); }); authorize->setOnTimeoutListener([this, txId] () { auto tx = getTransaction(); if (!tx || strcmp(tx->transactionId, txId)) { MO_DBG_INFO("dangling Authorize -- discard"); return; } updateTxNotification(TxNotification_AuthorizationTimeout); }); authorize->setTimeout(20 * 1000); context.initiateRequest(std::move(authorize)); } return true; } bool TransactionService::Evse::abortTransaction(Ocpp201::Transaction::StoppedReason stoppedReason, TransactionEventTriggerReason stopTrigger) { return endTransaction(stoppedReason, stopTrigger); } MicroOcpp::Ocpp201::Transaction *TransactionService::Evse::getTransaction() { return transaction.get(); } bool TransactionService::Evse::ocppPermitsCharge() { return transaction && transaction->active && transaction->isAuthorizationActive && transaction->isAuthorized && !transaction->isDeauthorized; } unsigned int TransactionService::Evse::getFrontRequestOpNr() { if (txEventFront) { return txEventFront->opNr; } /* * Advance front transaction? */ unsigned int txSize = (txNrEnd + MAX_TX_CNT - txNrFront) % MAX_TX_CNT; if (txFront && txSize == 0) { //catch edge case where txBack has been rolled back and txFront was equal to txBack MO_DBG_DEBUG("collect front transaction %u-%u after tx rollback", evseId, txFront->txNr); MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); txEventFront = nullptr; txFrontCache = nullptr; txFront = nullptr; } for (unsigned int i = 0; i < txSize; i++) { if (!txFront) { if (transaction && transaction->txNr == txNrFront) { txFront = transaction.get(); } else { txFrontCache = txStore.loadTransaction(txNrFront); txFront = txFrontCache.get(); } if (txFront) { MO_DBG_DEBUG("load front transaction %u-%u", evseId, txFront->txNr); (void)0; } } if (!txFront || (txFront && ((!txFront->active && !txFront->started) || (txFront->stopped && txFront->seqNos.empty()) || txFront->silent))) { //advance front MO_DBG_DEBUG("collect front transaction %u-%u", evseId, txNrFront); txEventFront = nullptr; txFrontCache = nullptr; txFront = nullptr; txNrFront = (txNrFront + 1) % MAX_TX_CNT; MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); } else { //front is accurate. Done here break; } } if (txFront && !txFront->seqNos.empty()) { MO_DBG_DEBUG("load front txEvent %u-%u-%u from flash", evseId, txFront->txNr, txFront->seqNos.front()); txEventFront = txStore.loadTransactionEvent(*txFront, txFront->seqNos.front()); } if (txEventFront) { return txEventFront->opNr; } return NoOperation; } std::unique_ptr TransactionService::Evse::fetchFrontRequest() { if (!txEventFront) { return nullptr; } if (txFront && txFront->silent) { return nullptr; } if (txEventFront->seqNo == 0 && txEventFront->timestamp < MIN_TIME && txEventFront->bootNr != context.getModel().getBootNr()) { //time not set, cannot be restored anymore -> invalid tx MO_DBG_ERR("cannot recover tx from previous power cycle"); txFront->silent = true; txFront->active = false; txStore.commit(txFront); //clean txEvents early auto seqNos = txFront->seqNos; for (size_t i = 0; i < seqNos.size(); i++) { txStore.remove(*txFront, seqNos[i]); } //last remove should keep tx201 file with only tx record and without txEvent //next getFrontRequestOpNr() call will collect txFront return nullptr; } if ((int)txEventFront->attemptNr >= txService.messageAttemptsTransactionEventInt->getInt()) { MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard txEvent"); txStore.remove(*txFront, txEventFront->seqNo); txEventFront = nullptr; return nullptr; } Timestamp nextAttempt = txEventFront->attemptTime + txEventFront->attemptNr * std::max(0, txService.messageAttemptIntervalTransactionEventInt->getInt()); if (nextAttempt > context.getModel().getClock().now()) { return nullptr; } if (txEventFrontIsRequested) { //ensure that only one TransactionEvent request is being executed at the same time return nullptr; } txEventFront->attemptNr++; txEventFront->attemptTime = context.getModel().getClock().now(); txStore.commit(txEventFront.get()); auto txEventRequest = makeRequest(new TransactionEvent(context.getModel(), txEventFront.get())); txEventRequest->setOnReceiveConfListener([this] (JsonObject) { MO_DBG_DEBUG("completed front txEvent"); txStore.remove(*txFront, txEventFront->seqNo); txEventFront = nullptr; txEventFrontIsRequested = false; }); txEventRequest->setOnAbortListener([this] () { MO_DBG_DEBUG("unsuccessful front txEvent"); txEventFrontIsRequested = false; }); txEventRequest->setTimeout(std::min(20, std::max(5, txService.messageAttemptIntervalTransactionEventInt->getInt())) * 1000); txEventFrontIsRequested = true; return txEventRequest; } bool TransactionService::isTxStartPoint(TxStartStopPoint check) { for (auto& v : txStartPointParsed) { if (v == check) { return true; } } return false; } bool TransactionService::isTxStopPoint(TxStartStopPoint check) { for (auto& v : txStopPointParsed) { if (v == check) { return true; } } return false; } bool TransactionService::parseTxStartStopPoint(const char *csl, Vector& dst) { dst.clear(); while (*csl == ',') { csl++; } while (*csl) { if (!strncmp(csl, "ParkingBayOccupancy", sizeof("ParkingBayOccupancy") - 1) && (csl[sizeof("ParkingBayOccupancy") - 1] == '\0' || csl[sizeof("ParkingBayOccupancy") - 1] == ',')) { dst.push_back(TxStartStopPoint::ParkingBayOccupancy); csl += sizeof("ParkingBayOccupancy") - 1; } else if (!strncmp(csl, "EVConnected", sizeof("EVConnected") - 1) && (csl[sizeof("EVConnected") - 1] == '\0' || csl[sizeof("EVConnected") - 1] == ',')) { dst.push_back(TxStartStopPoint::EVConnected); csl += sizeof("EVConnected") - 1; } else if (!strncmp(csl, "Authorized", sizeof("Authorized") - 1) && (csl[sizeof("Authorized") - 1] == '\0' || csl[sizeof("Authorized") - 1] == ',')) { dst.push_back(TxStartStopPoint::Authorized); csl += sizeof("Authorized") - 1; } else if (!strncmp(csl, "DataSigned", sizeof("DataSigned") - 1) && (csl[sizeof("DataSigned") - 1] == '\0' || csl[sizeof("DataSigned") - 1] == ',')) { dst.push_back(TxStartStopPoint::DataSigned); csl += sizeof("DataSigned") - 1; } else if (!strncmp(csl, "PowerPathClosed", sizeof("PowerPathClosed") - 1) && (csl[sizeof("PowerPathClosed") - 1] == '\0' || csl[sizeof("PowerPathClosed") - 1] == ',')) { dst.push_back(TxStartStopPoint::PowerPathClosed); csl += sizeof("PowerPathClosed") - 1; } else if (!strncmp(csl, "EnergyTransfer", sizeof("EnergyTransfer") - 1) && (csl[sizeof("EnergyTransfer") - 1] == '\0' || csl[sizeof("EnergyTransfer") - 1] == ',')) { dst.push_back(TxStartStopPoint::EnergyTransfer); csl += sizeof("EnergyTransfer") - 1; } else { MO_DBG_ERR("unkown TxStartStopPoint"); dst.clear(); return false; } while (*csl == ',') { csl++; } } return true; } namespace MicroOcpp { bool validateTxStartStopPoint(const char *value, void *userPtr) { auto txService = static_cast(userPtr); auto validated = makeVector("v201.Transactions.TransactionService"); return txService->parseTxStartStopPoint(value, validated); } bool validateUnsignedInt(int val, void*) { return val >= 0; } } //namespace MicroOcpp using namespace MicroOcpp; TransactionService::TransactionService(Context& context, std::shared_ptr filesystem, unsigned int numEvseIds) : MemoryManaged("v201.Transactions.TransactionService"), context(context), txStore(filesystem, numEvseIds), txStartPointParsed(makeVector(getMemoryTag())), txStopPointParsed(makeVector(getMemoryTag())) { auto varService = context.getModel().getVariableService(); txStartPointString = varService->declareVariable("TxCtrlr", "TxStartPoint", "PowerPathClosed"); txStopPointString = varService->declareVariable("TxCtrlr", "TxStopPoint", "PowerPathClosed"); stopTxOnInvalidIdBool = varService->declareVariable("TxCtrlr", "StopTxOnInvalidId", true); stopTxOnEVSideDisconnectBool = varService->declareVariable("TxCtrlr", "StopTxOnEVSideDisconnect", true); evConnectionTimeOutInt = varService->declareVariable("TxCtrlr", "EVConnectionTimeOut", 30); sampledDataTxUpdatedInterval = varService->declareVariable("SampledDataCtrlr", "TxUpdatedInterval", 0); sampledDataTxEndedInterval = varService->declareVariable("SampledDataCtrlr", "TxEndedInterval", 0); messageAttemptsTransactionEventInt = varService->declareVariable("OCPPCommCtrlr", "MessageAttempts", 3); messageAttemptIntervalTransactionEventInt = varService->declareVariable("OCPPCommCtrlr", "MessageAttemptInterval", 60); silentOfflineTransactionsBool = varService->declareVariable("CustomizationCtrlr", "SilentOfflineTransactions", false); varService->declareVariable("AuthCtrlr", "AuthorizeRemoteStart", false, Variable::Mutability::ReadOnly, false); varService->registerValidator("TxCtrlr", "TxStartPoint", validateTxStartStopPoint, this); varService->registerValidator("TxCtrlr", "TxStopPoint", validateTxStartStopPoint, this); varService->registerValidator("SampledDataCtrlr", "TxUpdatedInterval", validateUnsignedInt); varService->registerValidator("SampledDataCtrlr", "TxEndedInterval", validateUnsignedInt); for (unsigned int evseId = 0; evseId < std::min(numEvseIds, (unsigned int)MO_NUM_EVSEID); evseId++) { if (!txStore.getEvse(evseId)) { MO_DBG_ERR("initialization error"); break; } evses[evseId] = new Evse(context, *this, *txStore.getEvse(evseId), evseId); } //make sure EVSE 0 will only trigger transactions if TxStartPoint is Authorized if (evses[0]) { evses[0]->connectorPluggedInput = [] () {return false;}; evses[0]->evReadyInput = [] () {return false;}; evses[0]->evseReadyInput = [] () {return false;}; } } TransactionService::~TransactionService() { for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { delete evses[evseId]; } } void TransactionService::loop() { for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { evses[evseId]->loop(); } if (txStartPointString->getWriteCount() != trackTxStartPoint) { parseTxStartStopPoint(txStartPointString->getString(), txStartPointParsed); } if (txStopPointString->getWriteCount() != trackTxStopPoint) { parseTxStartStopPoint(txStopPointString->getString(), txStopPointParsed); } // assign tx on evseId 0 to an EVSE if (evses[0]->transaction) { //pending tx on evseId 0 if (evses[0]->transaction->active) { for (unsigned int evseId = 1; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { if (!evses[evseId]->getTransaction() && (!evses[evseId]->connectorPluggedInput || evses[evseId]->connectorPluggedInput())) { MO_DBG_INFO("assign tx to evse %u", evseId); evses[0]->transaction->notifyEvseId = true; evses[0]->transaction->evseId = evseId; evses[evseId]->transaction = std::move(evses[0]->transaction); } } } } } TransactionService::Evse *TransactionService::getEvse(unsigned int evseId) { if (evseId >= MO_NUM_EVSEID) { return nullptr; } return evses[evseId]; } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Model/Transactions/TransactionService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License /* * Implementation of the UCs E01 - E12 */ #ifndef MO_TRANSACTIONSERVICE_H #define MO_TRANSACTIONSERVICE_H #include #if MO_ENABLE_V201 #include #include #include #include #include #include #include #ifndef MO_TXRECORD_SIZE_V201 #define MO_TXRECORD_SIZE_V201 4 //maximum number of tx to hold on flash storage #endif namespace MicroOcpp { class Context; class FilesystemAdapter; class Variable; class TransactionService : public MemoryManaged { public: class Evse : public RequestEmitter, public MemoryManaged { private: Context& context; TransactionService& txService; Ocpp201::TransactionStoreEvse& txStore; const unsigned int evseId; unsigned int txNrCounter = 0; std::unique_ptr transaction; Ocpp201::TransactionEventData::ChargingState trackChargingState = Ocpp201::TransactionEventData::ChargingState::UNDEFINED; std::function connectorPluggedInput; std::function evReadyInput; std::function evseReadyInput; std::function startTxReadyInput; std::function stopTxReadyInput; std::function txNotificationOutput; bool beginTransaction(); bool endTransaction(Ocpp201::Transaction::StoppedReason stoppedReason, Ocpp201::TransactionEventTriggerReason stopTrigger); unsigned int txNrBegin = 0; //oldest (historical) transaction on flash. Has no function, but is useful for error diagnosis unsigned int txNrFront = 0; //oldest transaction which is still queued to be sent to the server unsigned int txNrEnd = 0; //one position behind newest transaction Ocpp201::Transaction *txFront = nullptr; std::unique_ptr txFrontCache; //helper owner for txFront. Empty if txFront == transaction.get() std::unique_ptr txEventFront; bool txEventFrontIsRequested = false; public: Evse(Context& context, TransactionService& txService, Ocpp201::TransactionStoreEvse& txStore, unsigned int evseId); virtual ~Evse(); void loop(); void setConnectorPluggedInput(std::function connectorPlugged); void setEvReadyInput(std::function evRequestsEnergy); void setEvseReadyInput(std::function connectorEnergized); void setTxNotificationOutput(std::function txNotificationOutput); void updateTxNotification(TxNotification event); bool beginAuthorization(IdToken idToken, bool validateIdToken = true); // authorize by swipe RFID bool endAuthorization(IdToken idToken = IdToken(), bool validateIdToken = false); // stop authorization by swipe RFID // stop transaction, but neither upon user request nor OCPP server request (e.g. after PowerLoss) bool abortTransaction(Ocpp201::Transaction::StoppedReason stoppedReason = Ocpp201::Transaction::StoppedReason::Other, Ocpp201::TransactionEventTriggerReason stopTrigger = Ocpp201::TransactionEventTriggerReason::AbnormalCondition); Ocpp201::Transaction *getTransaction(); bool ocppPermitsCharge(); unsigned int getFrontRequestOpNr() override; std::unique_ptr fetchFrontRequest() override; friend TransactionService; }; // TxStartStopPoint (2.6.4.1) enum class TxStartStopPoint : uint8_t { ParkingBayOccupancy, EVConnected, Authorized, DataSigned, PowerPathClosed, EnergyTransfer }; private: Context& context; Ocpp201::TransactionStore txStore; Evse *evses [MO_NUM_EVSEID] = {nullptr}; Variable *txStartPointString = nullptr; Variable *txStopPointString = nullptr; Variable *stopTxOnInvalidIdBool = nullptr; Variable *stopTxOnEVSideDisconnectBool = nullptr; Variable *evConnectionTimeOutInt = nullptr; Variable *sampledDataTxUpdatedInterval = nullptr; Variable *sampledDataTxEndedInterval = nullptr; Variable *messageAttemptsTransactionEventInt = nullptr; Variable *messageAttemptIntervalTransactionEventInt = nullptr; Variable *silentOfflineTransactionsBool = nullptr; uint16_t trackTxStartPoint = -1; uint16_t trackTxStopPoint = -1; Vector txStartPointParsed; Vector txStopPointParsed; bool isTxStartPoint(TxStartStopPoint check); bool isTxStopPoint(TxStartStopPoint check); public: TransactionService(Context& context, std::shared_ptr filesystem, unsigned int numEvseIds); ~TransactionService(); void loop(); Evse *getEvse(unsigned int evseId); bool parseTxStartStopPoint(const char *src, Vector& dst); }; } // namespace MicroOcpp #endif // MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Model/Transactions/TransactionStore.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using namespace MicroOcpp; ConnectorTransactionStore::ConnectorTransactionStore(TransactionStore& context, unsigned int connectorId, std::shared_ptr filesystem) : MemoryManaged("v16.Transactions.TransactionStore"), context(context), connectorId(connectorId), filesystem(filesystem), transactions{makeVector>(getMemoryTag())} { } ConnectorTransactionStore::~ConnectorTransactionStore() { } std::shared_ptr ConnectorTransactionStore::getTransaction(unsigned int txNr) { //check for most recent element of cache first because of temporal locality if (!transactions.empty()) { if (auto cached = transactions.back().lock()) { if (cached->getTxNr() == txNr) { //cache hit return cached; } } } //check all other elements (and free up unused entries) auto cached = transactions.begin(); while (cached != transactions.end()) { if (auto tx = cached->lock()) { if (tx->getTxNr() == txNr) { //cache hit return tx; } cached++; } else { //collect outdated cache reference cached = transactions.erase(cached); } } //cache miss - load tx from flash if existent if (!filesystem) { MO_DBG_DEBUG("no FS adapter"); return nullptr; } char fn [MO_MAX_PATH_SIZE] = {'\0'}; auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.json", connectorId, txNr); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return nullptr; } size_t msize; if (filesystem->stat(fn, &msize) != 0) { MO_DBG_DEBUG("%u-%u does not exist", connectorId, txNr); return nullptr; } auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); if (!doc) { MO_DBG_ERR("memory corruption"); return nullptr; } auto transaction = std::allocate_shared(makeAllocator(getMemoryTag()), *this, connectorId, txNr); JsonObject txJson = doc->as(); if (!deserializeTransaction(*transaction, txJson)) { MO_DBG_ERR("deserialization error"); return nullptr; } //before adding new entry, clean cache cached = transactions.begin(); while (cached != transactions.end()) { if (cached->expired()) { //collect outdated cache reference cached = transactions.erase(cached); } else { cached++; } } transactions.push_back(transaction); return transaction; } std::shared_ptr ConnectorTransactionStore::createTransaction(unsigned int txNr, bool silent) { auto transaction = std::allocate_shared(makeAllocator(getMemoryTag()), *this, connectorId, txNr, silent); if (!commit(transaction.get())) { MO_DBG_ERR("FS error"); return nullptr; } //before adding new entry, clean cache auto cached = transactions.begin(); while (cached != transactions.end()) { if (cached->expired()) { //collect outdated cache reference cached = transactions.erase(cached); } else { cached++; } } transactions.push_back(transaction); return transaction; } bool ConnectorTransactionStore::commit(Transaction *transaction) { if (!filesystem) { MO_DBG_DEBUG("no FS: nothing to commit"); return true; } char fn [MO_MAX_PATH_SIZE] = {'\0'}; auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.json", connectorId, transaction->getTxNr()); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return false; } auto txDoc = initJsonDoc(getMemoryTag()); if (!serializeTransaction(*transaction, txDoc)) { MO_DBG_ERR("Serialization error"); return false; } if (!FilesystemUtils::storeJson(filesystem, fn, txDoc)) { MO_DBG_ERR("FS error"); return false; } //success return true; } bool ConnectorTransactionStore::remove(unsigned int txNr) { if (!filesystem) { MO_DBG_DEBUG("no FS: nothing to remove"); return true; } char fn [MO_MAX_PATH_SIZE] = {'\0'}; auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.json", connectorId, txNr); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return false; } size_t msize; if (filesystem->stat(fn, &msize) != 0) { MO_DBG_DEBUG("%s already removed", fn); return true; } MO_DBG_DEBUG("remove %s", fn); return filesystem->remove(fn); } TransactionStore::TransactionStore(unsigned int nConnectors, std::shared_ptr filesystem) : MemoryManaged{"v16.Transactions.TransactionStore"}, connectors{makeVector>(getMemoryTag())} { for (unsigned int i = 0; i < nConnectors; i++) { connectors.push_back(std::unique_ptr( new ConnectorTransactionStore(*this, i, filesystem))); } } bool TransactionStore::commit(Transaction *transaction) { if (!transaction) { MO_DBG_ERR("Invalid arg"); return false; } auto connectorId = transaction->getConnectorId(); if (connectorId >= connectors.size()) { MO_DBG_ERR("Invalid tx"); return false; } return connectors[connectorId]->commit(transaction); } std::shared_ptr TransactionStore::getTransaction(unsigned int connectorId, unsigned int txNr) { if (connectorId >= connectors.size()) { MO_DBG_ERR("Invalid connectorId"); return nullptr; } return connectors[connectorId]->getTransaction(txNr); } std::shared_ptr TransactionStore::createTransaction(unsigned int connectorId, unsigned int txNr, bool silent) { if (connectorId >= connectors.size()) { MO_DBG_ERR("Invalid connectorId"); return nullptr; } return connectors[connectorId]->createTransaction(txNr, silent); } bool TransactionStore::remove(unsigned int connectorId, unsigned int txNr) { if (connectorId >= connectors.size()) { MO_DBG_ERR("Invalid connectorId"); return false; } return connectors[connectorId]->remove(txNr); } #if MO_ENABLE_V201 #include namespace MicroOcpp { namespace Ocpp201 { bool TransactionStoreEvse::serializeTransaction(Transaction& tx, JsonObject txJson) { if (tx.trackEvConnected) { txJson["trackEvConnected"] = tx.trackEvConnected; } if (tx.trackAuthorized) { txJson["trackAuthorized"] = tx.trackAuthorized; } if (tx.trackDataSigned) { txJson["trackDataSigned"] = tx.trackDataSigned; } if (tx.trackPowerPathClosed) { txJson["trackPowerPathClosed"] = tx.trackPowerPathClosed; } if (tx.trackEnergyTransfer) { txJson["trackEnergyTransfer"] = tx.trackEnergyTransfer; } if (tx.active) { txJson["active"] = true; } if (tx.started) { txJson["started"] = true; } if (tx.stopped) { txJson["stopped"] = true; } if (tx.isAuthorizationActive) { txJson["isAuthorizationActive"] = true; } if (tx.isAuthorized) { txJson["isAuthorized"] = true; } if (tx.isDeauthorized) { txJson["isDeauthorized"] = true; } if (tx.idToken.get()) { txJson["idToken"]["idToken"] = tx.idToken.get(); txJson["idToken"]["type"] = tx.idToken.getTypeCstr(); } if (tx.beginTimestamp > MIN_TIME) { char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; tx.beginTimestamp.toJsonString(timeStr, JSONDATE_LENGTH + 1); txJson["beginTimestamp"] = timeStr; } if (tx.remoteStartId >= 0) { txJson["remoteStartId"] = tx.remoteStartId; } if (tx.evConnectionTimeoutListen) { txJson["evConnectionTimeoutListen"] = true; } if (serializeTransactionStoppedReason(tx.stoppedReason)) { // optional txJson["stoppedReason"] = serializeTransactionStoppedReason(tx.stoppedReason); } if (serializeTransactionEventTriggerReason(tx.stopTrigger)) { txJson["stopTrigger"] = serializeTransactionEventTriggerReason(tx.stopTrigger); } if (tx.stopIdToken) { JsonObject stopIdToken = txJson.createNestedObject("stopIdToken"); stopIdToken["idToken"] = tx.stopIdToken->get(); stopIdToken["type"] = tx.stopIdToken->getTypeCstr(); } //sampledDataTxEnded not supported yet if (tx.silent) { txJson["silent"] = true; } txJson["txId"] = (const char*)tx.transactionId; //force zero-copy return true; } bool TransactionStoreEvse::deserializeTransaction(Transaction& tx, JsonObject txJson) { if (txJson.containsKey("trackEvConnected") && !txJson["trackEvConnected"].is()) { return false; } tx.trackEvConnected = txJson["trackEvConnected"] | false; if (txJson.containsKey("trackAuthorized") && !txJson["trackAuthorized"].is()) { return false; } tx.trackAuthorized = txJson["trackAuthorized"] | false; if (txJson.containsKey("trackDataSigned") && !txJson["trackDataSigned"].is()) { return false; } tx.trackDataSigned = txJson["trackDataSigned"] | false; if (txJson.containsKey("trackPowerPathClosed") && !txJson["trackPowerPathClosed"].is()) { return false; } tx.trackPowerPathClosed = txJson["trackPowerPathClosed"] | false; if (txJson.containsKey("trackEnergyTransfer") && !txJson["trackEnergyTransfer"].is()) { return false; } tx.trackEnergyTransfer = txJson["trackEnergyTransfer"] | false; if (txJson.containsKey("active") && !txJson["active"].is()) { return false; } tx.active = txJson["active"] | false; if (txJson.containsKey("started") && !txJson["started"].is()) { return false; } tx.started = txJson["started"] | false; if (txJson.containsKey("stopped") && !txJson["stopped"].is()) { return false; } tx.stopped = txJson["stopped"] | false; if (txJson.containsKey("isAuthorizationActive") && !txJson["isAuthorizationActive"].is()) { return false; } tx.isAuthorizationActive = txJson["isAuthorizationActive"] | false; if (txJson.containsKey("isAuthorized") && !txJson["isAuthorized"].is()) { return false; } tx.isAuthorized = txJson["isAuthorized"] | false; if (txJson.containsKey("isDeauthorized") && !txJson["isDeauthorized"].is()) { return false; } tx.isDeauthorized = txJson["isDeauthorized"] | false; if (txJson.containsKey("idToken")) { IdToken idToken; if (!idToken.parseCstr( txJson["idToken"]["idToken"] | (const char*)nullptr, txJson["idToken"]["type"] | (const char*)nullptr)) { return false; } tx.idToken = idToken; } if (txJson.containsKey("beginTimestamp")) { if (!tx.beginTimestamp.setTime(txJson["beginTimestamp"] | "_Undefined")) { return false; } } if (txJson.containsKey("remoteStartId")) { int remoteStartIdIn = txJson["remoteStartId"] | -1; if (remoteStartIdIn < 0) { return false; } tx.remoteStartId = remoteStartIdIn; } if (txJson.containsKey("evConnectionTimeoutListen") && !txJson["evConnectionTimeoutListen"].is()) { return false; } tx.evConnectionTimeoutListen = txJson["evConnectionTimeoutListen"] | false; Transaction::StoppedReason stoppedReason; if (!deserializeTransactionStoppedReason(txJson["stoppedReason"] | (const char*)nullptr, stoppedReason)) { return false; } tx.stoppedReason = stoppedReason; TransactionEventTriggerReason stopTrigger; if (!deserializeTransactionEventTriggerReason(txJson["stopTrigger"] | (const char*)nullptr, stopTrigger)) { return false; } tx.stopTrigger = stopTrigger; if (txJson.containsKey("stopIdToken")) { auto stopIdToken = std::unique_ptr(new IdToken()); if (!stopIdToken) { MO_DBG_ERR("OOM"); return false; } if (!stopIdToken->parseCstr( txJson["stopIdToken"]["idToken"] | (const char*)nullptr, txJson["stopIdToken"]["type"] | (const char*)nullptr)) { return false; } tx.stopIdToken = std::move(stopIdToken); } //sampledDataTxEnded not supported yet if (auto txId = txJson["txId"] | (const char*)nullptr) { auto ret = snprintf(tx.transactionId, sizeof(tx.transactionId), "%s", txId); if (ret < 0 || (size_t)ret >= sizeof(tx.transactionId)) { return false; } } else { return false; } if (txJson.containsKey("silent") && !txJson["silent"].is()) { return false; } tx.silent = txJson["silent"] | false; return true; } bool TransactionStoreEvse::serializeTransactionEvent(TransactionEventData& txEvent, JsonObject txEventJson) { if (txEvent.eventType != TransactionEventData::Type::Updated) { txEventJson["eventType"] = serializeTransactionEventType(txEvent.eventType); } if (txEvent.timestamp > MIN_TIME) { char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; txEvent.timestamp.toJsonString(timeStr, JSONDATE_LENGTH + 1); txEventJson["timestamp"] = timeStr; } txEventJson["bootNr"] = txEvent.bootNr; if (serializeTransactionEventTriggerReason(txEvent.triggerReason)) { txEventJson["triggerReason"] = serializeTransactionEventTriggerReason(txEvent.triggerReason); } if (txEvent.offline) { txEventJson["offline"] = true; } if (txEvent.numberOfPhasesUsed >= 0) { txEventJson["numberOfPhasesUsed"] = txEvent.numberOfPhasesUsed; } if (txEvent.cableMaxCurrent >= 0) { txEventJson["cableMaxCurrent"] = txEvent.cableMaxCurrent; } if (txEvent.reservationId >= 0) { txEventJson["reservationId"] = txEvent.reservationId; } if (txEvent.remoteStartId >= 0) { txEventJson["remoteStartId"] = txEvent.remoteStartId; } if (serializeTransactionEventChargingState(txEvent.chargingState)) { // optional txEventJson["chargingState"] = serializeTransactionEventChargingState(txEvent.chargingState); } if (txEvent.idToken) { JsonObject idToken = txEventJson.createNestedObject("idToken"); idToken["idToken"] = txEvent.idToken->get(); idToken["type"] = txEvent.idToken->getTypeCstr(); } if (txEvent.evse.id >= 0) { JsonObject evse = txEventJson.createNestedObject("evse"); evse["id"] = txEvent.evse.id; if (txEvent.evse.connectorId >= 0) { evse["connectorId"] = txEvent.evse.connectorId; } } //meterValue not supported yet txEventJson["opNr"] = txEvent.opNr; txEventJson["attemptNr"] = txEvent.attemptNr; if (txEvent.attemptTime > MIN_TIME) { char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; txEvent.attemptTime.toJsonString(timeStr, JSONDATE_LENGTH + 1); txEventJson["attemptTime"] = timeStr; } return true; } bool TransactionStoreEvse::deserializeTransactionEvent(TransactionEventData& txEvent, JsonObject txEventJson) { TransactionEventData::Type eventType; if (!deserializeTransactionEventType(txEventJson["eventType"] | "Updated", eventType)) { return false; } txEvent.eventType = eventType; if (txEventJson.containsKey("timestamp")) { if (!txEvent.timestamp.setTime(txEventJson["timestamp"] | "_Undefined")) { return false; } } int bootNrIn = txEventJson["bootNr"] | -1; if (bootNrIn >= 0 && bootNrIn <= std::numeric_limits::max()) { txEvent.bootNr = (uint16_t)bootNrIn; } else { return false; } TransactionEventTriggerReason triggerReason; if (!deserializeTransactionEventTriggerReason(txEventJson["triggerReason"] | "_Undefined", triggerReason)) { return false; } txEvent.triggerReason = triggerReason; if (txEventJson.containsKey("offline") && !txEventJson["offline"].is()) { return false; } txEvent.offline = txEventJson["offline"] | false; if (txEventJson.containsKey("numberOfPhasesUsed")) { int numberOfPhasesUsedIn = txEventJson["numberOfPhasesUsed"] | -1; if (numberOfPhasesUsedIn < 0) { return false; } txEvent.numberOfPhasesUsed = numberOfPhasesUsedIn; } if (txEventJson.containsKey("cableMaxCurrent")) { int cableMaxCurrentIn = txEventJson["cableMaxCurrent"] | -1; if (cableMaxCurrentIn < 0) { return false; } txEvent.cableMaxCurrent = cableMaxCurrentIn; } if (txEventJson.containsKey("reservationId")) { int reservationIdIn = txEventJson["reservationId"] | -1; if (reservationIdIn < 0) { return false; } txEvent.reservationId = reservationIdIn; } if (txEventJson.containsKey("remoteStartId")) { int remoteStartIdIn = txEventJson["remoteStartId"] | -1; if (remoteStartIdIn < 0) { return false; } txEvent.remoteStartId = remoteStartIdIn; } TransactionEventData::ChargingState chargingState; if (!deserializeTransactionEventChargingState(txEventJson["chargingState"] | (const char*)nullptr, chargingState)) { return false; } txEvent.chargingState = chargingState; if (txEventJson.containsKey("idToken")) { auto idToken = std::unique_ptr(new IdToken()); if (!idToken) { MO_DBG_ERR("OOM"); return false; } if (!idToken->parseCstr( txEventJson["idToken"]["idToken"] | (const char*)nullptr, txEventJson["idToken"]["type"] | (const char*)nullptr)) { return false; } txEvent.idToken = std::move(idToken); } if (txEventJson.containsKey("evse")) { int evseId = txEventJson["evse"]["id"] | -1; if (evseId < 0) { return false; } if (txEventJson["evse"].containsKey("connectorId")) { int connectorId = txEventJson["evse"]["connectorId"] | -1; if (connectorId < 0) { return false; } txEvent.evse = EvseId(evseId, connectorId); } else { txEvent.evse = EvseId(evseId); } } //meterValue not supported yet int opNrIn = txEventJson["opNr"] | -1; if (opNrIn >= 0) { txEvent.opNr = (unsigned int)opNrIn; } else { return false; } int attemptNrIn = txEventJson["attemptNr"] | -1; if (attemptNrIn >= 0) { txEvent.attemptNr = (unsigned int)attemptNrIn; } else { return false; } if (txEventJson.containsKey("attemptTime")) { if (!txEvent.attemptTime.setTime(txEventJson["attemptTime"] | "_Undefined")) { return false; } } return true; } TransactionStoreEvse::TransactionStoreEvse(TransactionStore& txStore, unsigned int evseId, std::shared_ptr filesystem) : MemoryManaged("v201.Transactions.TransactionStore"), txStore(txStore), evseId(evseId), filesystem(filesystem) { } bool TransactionStoreEvse::discoverStoredTx(unsigned int& txNrBeginOut, unsigned int& txNrEndOut) { if (!filesystem) { MO_DBG_DEBUG("no FS adapter"); return true; } char fnPrefix [MO_MAX_PATH_SIZE]; snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%u-", evseId); size_t fnPrefixLen = strlen(fnPrefix); unsigned int txNrPivot = std::numeric_limits::max(); unsigned int txNrBegin = 0, txNrEnd = 0; auto ret = filesystem->ftw_root([fnPrefix, fnPrefixLen, &txNrPivot, &txNrBegin, &txNrEnd] (const char *fn) { if (!strncmp(fn, fnPrefix, fnPrefixLen)) { unsigned int parsedTxNr = 0; for (size_t i = fnPrefixLen; fn[i] >= '0' && fn[i] <= '9'; i++) { parsedTxNr *= 10; parsedTxNr += fn[i] - '0'; } if (txNrPivot == std::numeric_limits::max()) { txNrPivot = parsedTxNr; txNrBegin = parsedTxNr; txNrEnd = (parsedTxNr + 1) % MAX_TX_CNT; return 0; } if ((parsedTxNr + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT < MAX_TX_CNT / 2) { //parsedTxNr is after pivot point if ((parsedTxNr + 1 + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT > (txNrEnd + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT) { txNrEnd = (parsedTxNr + 1) % MAX_TX_CNT; } } else if ((txNrPivot + MAX_TX_CNT - parsedTxNr) % MAX_TX_CNT < MAX_TX_CNT / 2) { //parsedTxNr is before pivot point if ((txNrPivot + MAX_TX_CNT - parsedTxNr) % MAX_TX_CNT > (txNrPivot + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT) { txNrBegin = parsedTxNr; } } MO_DBG_DEBUG("found %s%u-*.json - Internal range from %u to %u (exclusive)", fnPrefix, parsedTxNr, txNrBegin, txNrEnd); } return 0; }); if (ret == 0) { txNrBeginOut = txNrBegin; txNrEndOut = txNrEnd; return true; } else { MO_DBG_ERR("fs error"); return false; } } std::unique_ptr TransactionStoreEvse::loadTransaction(unsigned int txNr) { if (!filesystem) { MO_DBG_DEBUG("no FS adapter"); return nullptr; } char fnPrefix [MO_MAX_PATH_SIZE]; auto ret= snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%u-%u-", evseId, txNr); if (ret < 0 || (size_t)ret >= sizeof(fnPrefix)) { MO_DBG_ERR("fn error"); return nullptr; } size_t fnPrefixLen = strlen(fnPrefix); Vector seqNos = makeVector(getMemoryTag()); filesystem->ftw_root([fnPrefix, fnPrefixLen, &seqNos] (const char *fn) { if (!strncmp(fn, fnPrefix, fnPrefixLen)) { unsigned int parsedSeqNo = 0; for (size_t i = fnPrefixLen; fn[i] >= '0' && fn[i] <= '9'; i++) { parsedSeqNo *= 10; parsedSeqNo += fn[i] - '0'; } seqNos.push_back(parsedSeqNo); } return 0; }); if (seqNos.empty()) { MO_DBG_DEBUG("no tx at tx201-%u-%u", evseId, txNr); return nullptr; } std::sort(seqNos.begin(), seqNos.end()); char fn [MO_MAX_PATH_SIZE] = {'\0'}; ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx201" "-%u-%u-%u.json", evseId, txNr, seqNos.back()); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return nullptr; } size_t msize; if (filesystem->stat(fn, &msize) != 0) { MO_DBG_ERR("tx201-%u-%u memory corruption", evseId, txNr); return nullptr; } auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); if (!doc) { MO_DBG_ERR("memory corruption"); return nullptr; } auto transaction = std::unique_ptr(new Transaction()); if (!transaction) { MO_DBG_ERR("OOM"); return nullptr; } transaction->evseId = evseId; transaction->txNr = txNr; transaction->seqNos = std::move(seqNos); JsonObject txJson = (*doc)["tx"]; if (!deserializeTransaction(*transaction, txJson)) { MO_DBG_ERR("deserialization error"); return nullptr; } //determine seqNoEnd and trim seqNos record if (doc->containsKey("txEvent")) { //last tx201 file contains txEvent -> txNoEnd is one place after tx201 file and seqNos is accurate transaction->seqNoEnd = transaction->seqNos.back() + 1; } else { //last tx201 file contains only tx status information, but no txEvent -> remove from seqNos record and set seqNoEnd to this transaction->seqNoEnd = transaction->seqNos.back(); transaction->seqNos.pop_back(); } MO_DBG_DEBUG("loaded tx %u-%u, seqNos.size()=%zu", evseId, txNr, transaction->seqNos.size()); return transaction; } std::unique_ptr TransactionStoreEvse::createTransaction(unsigned int txNr, const char *txId) { //clean data which could still be here from a rolled-back transaction if (!remove(txNr)) { MO_DBG_ERR("txNr not clean"); return nullptr; } auto transaction = std::unique_ptr(new Transaction()); if (!transaction) { MO_DBG_ERR("OOM"); return nullptr; } transaction->evseId = evseId; transaction->txNr = txNr; auto ret = snprintf(transaction->transactionId, sizeof(transaction->transactionId), "%s", txId); if (ret < 0 || (size_t)ret >= sizeof(transaction->transactionId)) { MO_DBG_ERR("invalid arg"); return nullptr; } if (!commit(transaction.get())) { MO_DBG_ERR("FS error"); return nullptr; } return transaction; } std::unique_ptr TransactionStoreEvse::createTransactionEvent(Transaction& tx) { auto txEvent = std::unique_ptr(new TransactionEventData(&tx, tx.seqNoEnd)); if (!txEvent) { MO_DBG_ERR("OOM"); return nullptr; } //success return txEvent; } std::unique_ptr TransactionStoreEvse::loadTransactionEvent(Transaction& tx, unsigned int seqNo) { if (!filesystem) { MO_DBG_DEBUG("no FS adapter"); return nullptr; } bool found = false; for (size_t i = 0; i < tx.seqNos.size(); i++) { if (tx.seqNos[i] == seqNo) { found = true; } } if (!found) { MO_DBG_DEBUG("%u-%u-%u does not exist", evseId, tx.txNr, seqNo); return nullptr; } char fn [MO_MAX_PATH_SIZE] = {'\0'}; auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx201" "-%u-%u-%u.json", evseId, tx.txNr, seqNo); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return nullptr; } size_t msize; if (filesystem->stat(fn, &msize) != 0) { MO_DBG_ERR("seqNos out of sync: could not find %u-%u-%u", evseId, tx.txNr, seqNo); return nullptr; } auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); if (!doc) { MO_DBG_ERR("memory corruption"); return nullptr; } if (!doc->containsKey("txEvent")) { MO_DBG_DEBUG("%u-%u-%u does not contain txEvent", evseId, tx.txNr, seqNo); return nullptr; } auto txEvent = std::unique_ptr(new TransactionEventData(&tx, seqNo)); if (!txEvent) { MO_DBG_ERR("OOM"); return nullptr; } if (!deserializeTransactionEvent(*txEvent, (*doc)["txEvent"])) { MO_DBG_ERR("deserialization error"); return nullptr; } return txEvent; } bool TransactionStoreEvse::commit(Transaction& tx, TransactionEventData *txEvent) { if (!filesystem) { MO_DBG_DEBUG("no FS: nothing to commit"); return true; } unsigned int seqNo = 0; if (txEvent) { seqNo = txEvent->seqNo; } else { //update tx state in new or reused tx201 file seqNo = tx.seqNoEnd; } size_t seqNosNewSize = tx.seqNos.size() + 1; for (size_t i = 0; i < tx.seqNos.size(); i++) { if (tx.seqNos[i] == seqNo) { seqNosNewSize -= 1; break; } } // Check if to delete intermediate offline txEvent if (seqNosNewSize > MO_TXEVENTRECORD_SIZE_V201) { auto deltaMin = std::numeric_limits::max(); size_t indexMin = tx.seqNos.size(); for (size_t i = 2; i + 1 <= tx.seqNos.size(); i++) { //always keep first and final txEvent size_t t0 = tx.seqNos.size() - i - 1; size_t t1 = tx.seqNos.size() - i; size_t t2 = tx.seqNos.size() - i + 1; auto delta = tx.seqNos[t2] - tx.seqNos[t0]; if (delta < deltaMin) { deltaMin = delta; indexMin = t1; } } if (indexMin < tx.seqNos.size()) { MO_DBG_DEBUG("delete intermediate txEvent %u-%u-%u - delta=%u", evseId, tx.txNr, tx.seqNos[indexMin], deltaMin); remove(tx, tx.seqNos[indexMin]); //remove can call commit() again. Ensure that remove is not executed for last element } else { MO_DBG_ERR("internal error"); return false; } } char fn [MO_MAX_PATH_SIZE] = {'\0'}; auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx201" "-%u-%u-%u.json", evseId, tx.txNr, seqNo); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return false; } auto txDoc = initJsonDoc("v201.Transactions.TransactionStoreEvse", 2048); if (!serializeTransaction(tx, txDoc.createNestedObject("tx"))) { MO_DBG_ERR("Serialization error"); return false; } if (txEvent && !serializeTransactionEvent(*txEvent, txDoc.createNestedObject("txEvent"))) { MO_DBG_ERR("Serialization error"); return false; } if (!FilesystemUtils::storeJson(filesystem, fn, txDoc)) { MO_DBG_ERR("FS error"); return false; } if (txEvent && seqNo == tx.seqNoEnd) { tx.seqNos.push_back(seqNo); tx.seqNoEnd++; } MO_DBG_DEBUG("comitted tx %u-%u-%u", evseId, tx.txNr, seqNo); //success return true; } bool TransactionStoreEvse::commit(Transaction *transaction) { return commit(*transaction, nullptr); } bool TransactionStoreEvse::commit(TransactionEventData *txEvent) { return commit(*txEvent->transaction, txEvent); } bool TransactionStoreEvse::remove(unsigned int txNr) { if (!filesystem) { MO_DBG_DEBUG("no FS: nothing to remove"); return true; } char fnPrefix [MO_MAX_PATH_SIZE]; auto ret= snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%u-%u-", evseId, txNr); if (ret < 0 || (size_t)ret >= sizeof(fnPrefix)) { MO_DBG_ERR("fn error"); return false; } size_t fnPrefixLen = strlen(fnPrefix); auto success = FilesystemUtils::remove_if(filesystem, [fnPrefix, fnPrefixLen] (const char *fn) { return !strncmp(fn, fnPrefix, fnPrefixLen); }); return success; } bool TransactionStoreEvse::remove(Transaction& tx, unsigned int seqNo) { if (tx.seqNos.empty()) { //nothing to do return true; } if (tx.seqNos.back() == seqNo) { //special case: deletion of last tx201 file could also delete information about tx. Make sure all tx-related //information is commited into tx201 file at seqNoEnd, then delete file at seqNo char fn [MO_MAX_PATH_SIZE]; auto ret = snprintf(fn, sizeof(fn), "%stx201-%u-%u-%u.json", MO_FILENAME_PREFIX, evseId, tx.txNr, tx.seqNoEnd); if (ret < 0 || (size_t)ret >= sizeof(fn)) { MO_DBG_ERR("fn error"); return false; } auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); if (!doc || !doc->containsKey("tx")) { //no valid tx201 file at seqNoEnd. Commit tx into file seqNoEnd, then remove file at seqNo if (!commit(tx, nullptr)) { MO_DBG_ERR("fs error"); return false; } } //seqNoEnd contains all tx data which should be persisted. Continue } bool found = false; for (size_t i = 0; i < tx.seqNos.size(); i++) { if (tx.seqNos[i] == seqNo) { found = true; } } if (!found) { MO_DBG_DEBUG("%u-%u-%u does not exist", evseId, tx.txNr, seqNo); return true; } bool success = true; if (filesystem) { char fn [MO_MAX_PATH_SIZE]; auto ret = snprintf(fn, sizeof(fn), "%stx201-%u-%u-%u.json", MO_FILENAME_PREFIX, evseId, tx.txNr, seqNo); if (ret < 0 || (size_t)ret >= sizeof(fn)) { MO_DBG_ERR("fn error"); return false; } size_t msize; if (filesystem->stat(fn, &msize) == 0) { success &= filesystem->remove(fn); } else { MO_DBG_ERR("internal error: seqNos out of sync"); (void)0; } } if (success) { auto it = tx.seqNos.begin(); while (it != tx.seqNos.end()) { if (*it == seqNo) { it = tx.seqNos.erase(it); } else { it++; } } } return success; } TransactionStore::TransactionStore(std::shared_ptr filesystem, size_t numEvses) : MemoryManaged{"v201.Transactions.TransactionStore"} { for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID && (size_t)evseId < numEvses; evseId++) { evses[evseId] = new TransactionStoreEvse(*this, evseId, filesystem); } } TransactionStore::~TransactionStore() { for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { delete evses[evseId]; } } TransactionStoreEvse *TransactionStore::getEvse(unsigned int evseId) { if (evseId >= MO_NUM_EVSEID) { return nullptr; } return evses[evseId]; } } //namespace Ocpp201 } //namespace MicroOcpp #endif //MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Model/Transactions/TransactionStore.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_TRANSACTIONSTORE_H #define MO_TRANSACTIONSTORE_H #include #include #include #include namespace MicroOcpp { class TransactionStore; class ConnectorTransactionStore : public MemoryManaged { private: TransactionStore& context; const unsigned int connectorId; std::shared_ptr filesystem; Vector> transactions; public: ConnectorTransactionStore(TransactionStore& context, unsigned int connectorId, std::shared_ptr filesystem); ConnectorTransactionStore(const ConnectorTransactionStore&) = delete; ConnectorTransactionStore(ConnectorTransactionStore&&) = delete; ConnectorTransactionStore& operator=(const ConnectorTransactionStore&) = delete; ~ConnectorTransactionStore(); bool commit(Transaction *transaction); std::shared_ptr getTransaction(unsigned int txNr); std::shared_ptr createTransaction(unsigned int txNr, bool silent = false); bool remove(unsigned int txNr); }; class TransactionStore : public MemoryManaged { private: Vector> connectors; public: TransactionStore(unsigned int nConnectors, std::shared_ptr filesystem); bool commit(Transaction *transaction); std::shared_ptr getTransaction(unsigned int connectorId, unsigned int txNr); std::shared_ptr createTransaction(unsigned int connectorId, unsigned int txNr, bool silent = false); bool remove(unsigned int connectorId, unsigned int txNr); }; } #if MO_ENABLE_V201 #ifndef MO_TXEVENTRECORD_SIZE_V201 #define MO_TXEVENTRECORD_SIZE_V201 10 //maximum number of of txEvents per tx to hold on flash storage #endif namespace MicroOcpp { namespace Ocpp201 { class TransactionStore; class TransactionStoreEvse : public MemoryManaged { private: TransactionStore& txStore; const unsigned int evseId; std::shared_ptr filesystem; bool serializeTransaction(Transaction& tx, JsonObject out); bool serializeTransactionEvent(TransactionEventData& txEvent, JsonObject out); bool deserializeTransaction(Transaction& tx, JsonObject in); bool deserializeTransactionEvent(TransactionEventData& txEvent, JsonObject in); bool commit(Transaction& transaction, TransactionEventData *transactionEvent); public: TransactionStoreEvse(TransactionStore& txStore, unsigned int evseId, std::shared_ptr filesystem); bool discoverStoredTx(unsigned int& txNrBeginOut, unsigned int& txNrEndOut); bool commit(Transaction *transaction); bool commit(TransactionEventData *transactionEvent); std::unique_ptr loadTransaction(unsigned int txNr); std::unique_ptr createTransaction(unsigned int txNr, const char *txId); std::unique_ptr createTransactionEvent(Transaction& tx); std::unique_ptr loadTransactionEvent(Transaction& tx, unsigned int seqNo); bool remove(unsigned int txNr); bool remove(Transaction& tx, unsigned int seqNo); }; class TransactionStore : public MemoryManaged { private: TransactionStoreEvse *evses [MO_NUM_EVSEID] = {nullptr}; public: TransactionStore(std::shared_ptr filesystem, size_t numEvses); ~TransactionStore(); TransactionStoreEvse *getEvse(unsigned int evseId); }; } //namespace Ocpp201 } //namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Model/Variables/Variable.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License /* * Implementation of the UCs B05 - B06 */ #include #if MO_ENABLE_V201 #include #include #include using namespace MicroOcpp; ComponentId::ComponentId(const char *name) : name(name) { } ComponentId::ComponentId(const char *name, EvseId evse) : name(name), evse(evse) { } bool ComponentId::equals(const ComponentId& other) const { return !strcmp(name, other.name) && ((evse.id < 0 && other.evse.id < 0) || (evse.id == other.evse.id)) && // evseId undefined or equal ((evse.connectorId < 0 && other.evse.connectorId < 0) || (evse.connectorId == other.evse.connectorId)); // connectorId undefined or equal } bool Variable::AttributeTypeSet::has(AttributeType type) { switch(type) { case AttributeType::Actual: return flag & (1 << 0); case AttributeType::Target: return flag & (1 << 1); case AttributeType::MinSet: return flag & (1 << 2); case AttributeType::MaxSet: return flag & (1 << 3); } MO_DBG_ERR("internal error"); return false; } Variable::AttributeTypeSet& Variable::AttributeTypeSet::set(AttributeType type) { switch(type) { case AttributeType::Actual: flag |= (1 << 0); break; case AttributeType::Target: flag |= (1 << 1); break; case AttributeType::MinSet: flag |= (1 << 2); break; case AttributeType::MaxSet: flag |= (1 << 3); break; default: MO_DBG_ERR("internal error"); break; } return *this; } size_t Variable::AttributeTypeSet::count() { return (flag & (1 << 0) ? 1 : 0) + (flag & (1 << 1) ? 1 : 0) + (flag & (1 << 2) ? 1 : 0) + (flag & (1 << 3) ? 1 : 0); } Variable::AttributeTypeSet::AttributeTypeSet(AttributeType attrType) { set(attrType); } Variable::Variable(AttributeTypeSet attributes) : attributes(attributes) { } Variable::~Variable() { } void Variable::setName(const char *name) { this->variableName = name; updateMemoryTag("v201.Variables.Variable.", name); } const char *Variable::getName() const { return variableName; } void Variable::setComponentId(const ComponentId& componentId) { this->component = componentId; } const ComponentId& Variable::getComponentId() const { return component; } void Variable::setInt(int val, AttributeType) { MO_DBG_ERR("type err"); } void Variable::setBool(bool val, AttributeType) { MO_DBG_ERR("type err"); } bool Variable::setString(const char *val, AttributeType) { MO_DBG_ERR("type err"); return false; } int Variable::getInt(AttributeType) { MO_DBG_ERR("type err"); return 0; } bool Variable::getBool(AttributeType) { MO_DBG_ERR("type err"); return false; } const char *Variable::getString(AttributeType) { MO_DBG_ERR("type err"); return nullptr; } bool Variable::hasAttribute(AttributeType attrType) { return attributes.has(attrType); } void Variable::setVariableDataType(VariableCharacteristics::DataType dataType) { this->dataType = dataType; } VariableCharacteristics::DataType Variable::getVariableDataType() { return dataType; } bool Variable::getSupportsMonitoring() { return supportsMonitoring; } void Variable::setSupportsMonitoring() { supportsMonitoring = true; } bool Variable::isRebootRequired() { return rebootRequired; } void Variable::setRebootRequired() { rebootRequired = true; } void Variable::setMutability(Mutability m) { this->mutability = m; } Variable::Mutability Variable::getMutability() { return mutability; } void Variable::setPersistent() { persistent = true; } bool Variable::isPersistent() { return persistent; } void Variable::setConstant() { constant = true; } bool Variable::isConstant() { return constant; } template struct VariableSingleData { T value = 0; T& get(Variable::AttributeType attribute) { return value; } }; template struct VariableFullData { T actual = 0; T target = 0; T minSet = 0; T maxSet = 0; T& get(Variable::AttributeType attribute) { switch(attribute) { case Variable::AttributeType::Actual: return actual; case Variable::AttributeType::Target: return target; case Variable::AttributeType::MinSet: return minSet; case Variable::AttributeType::MaxSet: return maxSet; } MO_DBG_ERR("internal error"); return actual; } }; template class VariableData> class VariableInt : public Variable { private: VariableData value; uint16_t writeCount = 0; #if MO_VARIABLE_TYPECHECK AttributeTypeSet attributes; #endif public: VariableInt(AttributeTypeSet attributes) : Variable(attributes) #if MO_VARIABLE_TYPECHECK , attributes(attributes) #endif { } void setInt(int val, AttributeType attrType) override { #if MO_VARIABLE_TYPECHECK if (!attributes.has(attrType)) { MO_DBG_ERR("type err"); return; } #endif value.get(attrType) = val; writeCount++; } int getInt(AttributeType attrType) override { #if MO_VARIABLE_TYPECHECK if (!attributes.has(attrType)) { MO_DBG_ERR("type err"); return 0; } #endif return value.get(attrType); } InternalDataType getInternalDataType() override { return InternalDataType::Int; } uint16_t getWriteCount() override { return writeCount; } }; template class VariableData> class VariableBool : public Variable { private: VariableData value; uint16_t writeCount = 0; #if MO_VARIABLE_TYPECHECK AttributeTypeSet attributes; #endif public: VariableBool(AttributeTypeSet attributes) : Variable(attributes) #if MO_VARIABLE_TYPECHECK , attributes(attributes) #endif { } void setBool(bool val, AttributeType attrType) override { #if MO_VARIABLE_TYPECHECK if (!attributes.has(attrType)) { MO_DBG_ERR("type err"); return; } #endif value.get(attrType) = val; writeCount++; } bool getBool(AttributeType attrType) override { #if MO_VARIABLE_TYPECHECK if (!attributes.has(attrType)) { MO_DBG_ERR("type err"); return 0; } #endif return value.get(attrType); } InternalDataType getInternalDataType() override { return InternalDataType::Bool; } uint16_t getWriteCount() override { return writeCount; } }; template class VariableData> class VariableString : public Variable { private: VariableData value; uint16_t writeCount = 0; #if MO_VARIABLE_TYPECHECK AttributeTypeSet attributes; #endif public: VariableString(AttributeTypeSet attributes) : Variable(attributes) #if MO_VARIABLE_TYPECHECK , attributes(attributes) #endif { } ~VariableString() { MO_FREE(value.get(AttributeType::Actual)); value.get(AttributeType::Actual) = nullptr; MO_FREE(value.get(AttributeType::Target)); value.get(AttributeType::Target) = nullptr; MO_FREE(value.get(AttributeType::MinSet)); value.get(AttributeType::MinSet) = nullptr; MO_FREE(value.get(AttributeType::MaxSet)); value.get(AttributeType::MaxSet) = nullptr; } bool setString(const char *val, AttributeType attrType) override { #if MO_VARIABLE_TYPECHECK if (!attributes.has(attrType)) { MO_DBG_ERR("type err"); return false; } #endif size_t len = strlen(val); char *valNew = nullptr; if (len != 0) { size_t size = len + 1; valNew = static_cast(MO_MALLOC(getMemoryTag(), size)); if (!valNew) { MO_DBG_ERR("OOM"); return false; } memcpy(valNew, val, size); } MO_FREE(value.get(attrType)); value.get(attrType) = valNew; writeCount++; return true; } const char *getString(AttributeType attrType) override { #if MO_VARIABLE_TYPECHECK if (!attributes.has(attrType)) { MO_DBG_ERR("type err"); return 0; } #endif return value.get(attrType) ? value.get(attrType) : ""; } InternalDataType getInternalDataType() override { return InternalDataType::String; } uint16_t getWriteCount() override { return writeCount; } }; std::unique_ptr MicroOcpp::makeVariable(Variable::InternalDataType dtype, Variable::AttributeTypeSet supportAttributes) { switch(dtype) { case Variable::InternalDataType::Int: if (supportAttributes.count() > 1) { return std::unique_ptr(new VariableInt(supportAttributes)); } else { return std::unique_ptr(new VariableInt(supportAttributes)); } case Variable::InternalDataType::Bool: if (supportAttributes.count() > 1) { return std::unique_ptr(new VariableBool(supportAttributes)); } else { return std::unique_ptr(new VariableBool(supportAttributes)); } case Variable::InternalDataType::String: if (supportAttributes.count() > 1) { return std::unique_ptr(new VariableString(supportAttributes)); } else { return std::unique_ptr(new VariableString(supportAttributes)); } } MO_DBG_ERR("internal error"); return nullptr; } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Model/Variables/Variable.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License /* * Implementation of the UCs B05 - B06 */ #ifndef MO_VARIABLE_H #define MO_VARIABLE_H #include #if MO_ENABLE_V201 #include #include #include #include #include #ifndef MO_VARIABLE_TYPECHECK #define MO_VARIABLE_TYPECHECK 1 #endif namespace MicroOcpp { // VariableCharacteristicsType (2.51) struct VariableCharacteristics : public MemoryManaged { // DataEnumType (3.26) enum class DataType : uint8_t { string, decimal, integer, dateTime, boolean, OptionList, SequenceList, MemberList }; const char *unit = nullptr; //no copy //DataType dataType; //stored in Variable int minLimit = std::numeric_limits::min(); int maxLimit = std::numeric_limits::max(); const char *valuesList = nullptr; //no copy //bool supportsMonitoring; //stored in Variable VariableCharacteristics() : MemoryManaged("v201.Variables.VariableCharacteristics") { } }; // SetVariableStatusEnumType (3.79) enum class SetVariableStatus : uint8_t { Accepted, Rejected, UnknownComponent, UnknownVariable, NotSupportedAttributeType, RebootRequired }; // GetVariableStatusEnumType (3.41) enum class GetVariableStatus : uint8_t { Accepted, Rejected, UnknownComponent, UnknownVariable, NotSupportedAttributeType }; // ReportBaseEnumType (3.70) typedef enum ReportBase { ReportBase_ConfigurationInventory, ReportBase_FullInventory, ReportBase_SummaryInventory } ReportBase; // GenericDeviceModelStatus (3.34) typedef enum GenericDeviceModelStatus { GenericDeviceModelStatus_Accepted, GenericDeviceModelStatus_Rejected, GenericDeviceModelStatus_NotSupported, GenericDeviceModelStatus_EmptyResultSet } GenericDeviceModelStatus; // VariableMonitoringType (2.52) class VariableMonitor { public: //MonitorEnumType (3.55) enum class Type { UpperThreshold, LowerThreshold, Delta, Periodic, PeriodicClockAligned }; private: int id; bool transaction; float value; Type type; int severity; public: VariableMonitor() = delete; VariableMonitor(int id, bool transaction, float value, Type type, int severity) : id(id), transaction(transaction), value(value), type(type), severity(severity) { } }; // ComponentType (2.16) struct ComponentId { const char *name; // zero copy //const char *instance; // not supported in this implementation EvseId evse {-1}; ComponentId(const char *name = nullptr); ComponentId(const char *name, EvseId evse); bool equals(const ComponentId& other) const; }; /* * Corresponds to VariableType (2.53) * * Template method pattern: this is a super-class which has hook-methods for storing and fetching * the value of the variable. To make it use the host system's key-value store, extend this class * with a custom implementation of the virtual methods and pass its instances to MO. */ class Variable : public MemoryManaged { public: //AttributeEnumType (3.2) enum class AttributeType : uint8_t { Actual, Target, MinSet, MaxSet }; struct AttributeTypeSet { uint8_t flag = 0; bool has(Variable::AttributeType type); AttributeTypeSet& set(Variable::AttributeType type); size_t count(); AttributeTypeSet(AttributeType attrType = AttributeType::Actual); }; //MutabilityEnumType (3.58) enum class Mutability : uint8_t { ReadOnly, WriteOnly, ReadWrite }; //MO-internal optimization: if value is only in int range, store it in more compact representation enum class InternalDataType : uint8_t { Int, Bool, String }; private: const char *variableName = nullptr; ComponentId component; // VariableCharacteristicsType (2.51) std::unique_ptr characteristics; //optional VariableCharacteristics VariableCharacteristics::DataType dataType; //mandatory bool supportsMonitoring = false; //mandatory bool rebootRequired = false; //MO-internal: if to respond status RebootRequired on SetVariables // VariableAttributeType (2.50) Mutability mutability = Mutability::ReadWrite; bool persistent = false; bool constant = false; AttributeTypeSet attributes; // VariableMonitoringType (2.52) //std::vector monitors; // uncomment when testing Monitors public: Variable(AttributeTypeSet attributes); virtual ~Variable(); void setName(const char *name); //zero-copy const char *getName() const; void setComponentId(const ComponentId& componentId); //zero-copy const ComponentId& getComponentId() const; // set Value of Variable virtual void setInt(int val, AttributeType attrType = AttributeType::Actual); virtual void setBool(bool val, AttributeType attrType = AttributeType::Actual); virtual bool setString(const char *val, AttributeType attrType = AttributeType::Actual); // get Value of Variable virtual int getInt(AttributeType attrType = AttributeType::Actual); virtual bool getBool(AttributeType attrType = AttributeType::Actual); virtual const char *getString(AttributeType attrType = AttributeType::Actual); //always returns c-string (empty if undefined) virtual InternalDataType getInternalDataType() = 0; //corresponds to MO internal value representation bool hasAttribute(AttributeType attrType); void setVariableDataType(VariableCharacteristics::DataType dataType); //corresponds to OCPP DataEnumType (3.26) VariableCharacteristics::DataType getVariableDataType(); //corresponds to OCPP DataEnumType (3.26) bool getSupportsMonitoring(); void setSupportsMonitoring(); bool isRebootRequired(); void setRebootRequired(); void setMutability(Mutability m); Mutability getMutability(); void setPersistent(); bool isPersistent(); void setConstant(); bool isConstant(); //bool addMonitor(int id, bool transaction, float value, VariableMonitor::Type type, int severity); virtual uint16_t getWriteCount() = 0; //get write count (use this as a pre-check if the value changed) }; std::unique_ptr makeVariable(Variable::InternalDataType dtype, Variable::AttributeTypeSet supportAttributes); } // namespace MicroOcpp #endif // MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Model/Variables/VariableContainer.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License /* * Implementation of the UCs B05 - B06 */ #include #if MO_ENABLE_V201 #include #include #include #include using namespace MicroOcpp; VariableContainer::~VariableContainer() { } bool VariableContainer::commit() { return true; } VariableContainerNonOwning::VariableContainerNonOwning() : VariableContainer(), MemoryManaged("v201.Variables.VariableContainerNonOwning"), variables(makeVector(getMemoryTag())) { } size_t VariableContainerNonOwning::size() { return variables.size(); } Variable *VariableContainerNonOwning::getVariable(size_t i) { return variables[i]; } Variable *VariableContainerNonOwning::getVariable(const ComponentId& component, const char *variableName) { for (size_t i = 0; i < variables.size(); i++) { auto& var = variables[i]; if (!strcmp(var->getName(), variableName) && var->getComponentId().equals(component)) { return var; } } return nullptr; } bool VariableContainerNonOwning::add(Variable *variable) { variables.push_back(variable); return true; } bool VariableContainerOwning::checkWriteCountUpdated() { decltype(trackWriteCount) writeCount = 0; for (size_t i = 0; i < variables.size(); i++) { writeCount += variables[i]->getWriteCount(); } bool updated = writeCount != trackWriteCount; trackWriteCount = writeCount; return updated; } VariableContainerOwning::VariableContainerOwning() : VariableContainer(), MemoryManaged("v201.Variables.VariableContainerOwning"), variables(makeVector>(getMemoryTag())) { } VariableContainerOwning::~VariableContainerOwning() { MO_FREE(filename); filename = nullptr; } size_t VariableContainerOwning::size() { return variables.size(); } Variable *VariableContainerOwning::getVariable(size_t i) { return variables[i].get(); } Variable *VariableContainerOwning::getVariable(const ComponentId& component, const char *variableName) { for (size_t i = 0; i < variables.size(); i++) { auto& var = variables[i]; if (!strcmp(var->getName(), variableName) && var->getComponentId().equals(component)) { return var.get(); } } return nullptr; } bool VariableContainerOwning::add(std::unique_ptr variable) { variables.push_back(std::move(variable)); return true; } bool VariableContainerOwning::enablePersistency(std::shared_ptr filesystem, const char *filename) { this->filesystem = filesystem; MO_FREE(this->filename); this->filename = nullptr; size_t fnsize = strlen(filename) + 1; this->filename = static_cast(MO_MALLOC(getMemoryTag(), fnsize)); if (!this->filename) { MO_DBG_ERR("OOM"); return false; } snprintf(this->filename, fnsize, "%s", filename); return true; } bool VariableContainerOwning::load() { if (loaded) { return true; } if (!filesystem || !filename) { return true; //persistency disabled - nothing to do } size_t file_size = 0; if (filesystem->stat(filename, &file_size) != 0 // file does not exist || file_size == 0) { // file exists, but empty MO_DBG_DEBUG("Populate FS: create variables file"); return commit(); } auto doc = FilesystemUtils::loadJson(filesystem, filename, getMemoryTag()); if (!doc) { MO_DBG_ERR("failed to load %s", filename); return false; } JsonArray variablesJson = (*doc)["variables"]; for (JsonObject stored : variablesJson) { const char *component = stored["component"] | (const char*)nullptr; int evseId = stored["evseId"] | -1; const char *name = stored["name"] | (const char*)nullptr; if (!component || !name) { MO_DBG_ERR("corrupt entry: %s", filename); continue; } auto variablePtr = getVariable(ComponentId(component, EvseId(evseId)), name); if (!variablePtr) { MO_DBG_ERR("loaded variable does not exist: %s, %s, %s", filename, component, name); continue; } auto& variable = *variablePtr; switch (variable.getInternalDataType()) { case Variable::InternalDataType::Int: if (variable.hasAttribute(Variable::AttributeType::Actual)) variable.setInt(stored["valActual"] | 0, Variable::AttributeType::Actual); if (variable.hasAttribute(Variable::AttributeType::Target)) variable.setInt(stored["valTarget"] | 0, Variable::AttributeType::Target); if (variable.hasAttribute(Variable::AttributeType::MinSet)) variable.setInt(stored["valMinSet"] | 0, Variable::AttributeType::MinSet); if (variable.hasAttribute(Variable::AttributeType::MaxSet)) variable.setInt(stored["valMaxSet"] | 0, Variable::AttributeType::MaxSet); break; case Variable::InternalDataType::Bool: if (variable.hasAttribute(Variable::AttributeType::Actual)) variable.setBool(stored["valActual"] | false, Variable::AttributeType::Actual); if (variable.hasAttribute(Variable::AttributeType::Target)) variable.setBool(stored["valTarget"] | false, Variable::AttributeType::Target); if (variable.hasAttribute(Variable::AttributeType::MinSet)) variable.setBool(stored["valMinSet"] | false, Variable::AttributeType::MinSet); if (variable.hasAttribute(Variable::AttributeType::MaxSet)) variable.setBool(stored["valMaxSet"] | false, Variable::AttributeType::MaxSet); break; case Variable::InternalDataType::String: bool success = true; if (variable.hasAttribute(Variable::AttributeType::Actual)) success &= variable.setString(stored["valActual"] | "", Variable::AttributeType::Actual); if (variable.hasAttribute(Variable::AttributeType::Target)) success &= variable.setString(stored["valTarget"] | "", Variable::AttributeType::Target); if (variable.hasAttribute(Variable::AttributeType::MinSet)) success &= variable.setString(stored["valMinSet"] | "", Variable::AttributeType::MinSet); if (variable.hasAttribute(Variable::AttributeType::MaxSet)) success &= variable.setString(stored["valMaxSet"] | "", Variable::AttributeType::MaxSet); if (!success) { MO_DBG_ERR("value error: %s, %s, %s", filename, component, name); continue; } break; } } checkWriteCountUpdated(); // update trackWriteCount after load is completed MO_DBG_DEBUG("Initialization finished"); loaded = true; return true; } bool VariableContainerOwning::commit() { if (!filesystem || !filename) { //persistency disabled - nothing to do return true; } if (!checkWriteCountUpdated()) { return true; //nothing to be done } size_t jsonCapacity = JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(0); size_t variableCapacity = 0; for (size_t i = 0; i < variables.size(); i++) { auto& variable = *variables[i]; if (!variable.isPersistent()) { continue; } size_t addedJsonCapacity = JSON_ARRAY_SIZE(variableCapacity + 1) - JSON_ARRAY_SIZE(variableCapacity); size_t storedEntities = 2; //component name, variable name will always be stored storedEntities += variable.getComponentId().evse.id >= 0 ? 1 : 0; storedEntities += variable.hasAttribute(Variable::AttributeType::Actual) ? 1 : 0; storedEntities += variable.hasAttribute(Variable::AttributeType::Target) ? 1 : 0; storedEntities += variable.hasAttribute(Variable::AttributeType::MinSet) ? 1 : 0; storedEntities += variable.hasAttribute(Variable::AttributeType::MaxSet) ? 1 : 0; addedJsonCapacity += JSON_OBJECT_SIZE(storedEntities); if (jsonCapacity + addedJsonCapacity <= MO_MAX_JSON_CAPACITY) { jsonCapacity += addedJsonCapacity; variableCapacity++; } else { MO_DBG_ERR("configs JSON exceeds maximum capacity (%s, %zu entries). Crop configs file (by FCFS)", filename, variables.size()); break; } } auto doc = initJsonDoc(getMemoryTag(), jsonCapacity); JsonArray variablesJson = doc.createNestedArray("variables"); for (size_t i = 0; i < variableCapacity; i++) { auto& variable = *variables[i]; if (!variable.isPersistent()) { continue; } auto stored = variablesJson.createNestedObject(); stored["component"] = variable.getComponentId().name; if (variable.getComponentId().evse.id >= 0) { stored["evseId"] = variable.getComponentId().evse.id; } stored["name"] = variable.getName(); switch (variable.getInternalDataType()) { case Variable::InternalDataType::Int: if (variable.hasAttribute(Variable::AttributeType::Actual)) stored["valActual"] = variable.getInt(Variable::AttributeType::Actual); if (variable.hasAttribute(Variable::AttributeType::Target)) stored["valTarget"] = variable.getInt(Variable::AttributeType::Target); if (variable.hasAttribute(Variable::AttributeType::MinSet)) stored["valMinSet"] = variable.getInt(Variable::AttributeType::MinSet); if (variable.hasAttribute(Variable::AttributeType::MaxSet)) stored["valMaxSet"] = variable.getInt(Variable::AttributeType::MaxSet); break; case Variable::InternalDataType::Bool: if (variable.hasAttribute(Variable::AttributeType::Actual)) stored["valActual"] = variable.getBool(Variable::AttributeType::Actual); if (variable.hasAttribute(Variable::AttributeType::Target)) stored["valTarget"] = variable.getBool(Variable::AttributeType::Target); if (variable.hasAttribute(Variable::AttributeType::MinSet)) stored["valMinSet"] = variable.getBool(Variable::AttributeType::MinSet); if (variable.hasAttribute(Variable::AttributeType::MaxSet)) stored["valMaxSet"] = variable.getBool(Variable::AttributeType::MaxSet); break; case Variable::InternalDataType::String: if (variable.hasAttribute(Variable::AttributeType::Actual)) stored["valActual"] = variable.getString(Variable::AttributeType::Actual); if (variable.hasAttribute(Variable::AttributeType::Target)) stored["valTarget"] = variable.getString(Variable::AttributeType::Target); if (variable.hasAttribute(Variable::AttributeType::MinSet)) stored["valMinSet"] = variable.getString(Variable::AttributeType::MinSet); if (variable.hasAttribute(Variable::AttributeType::MaxSet)) stored["valMaxSet"] = variable.getString(Variable::AttributeType::MaxSet); break; } } bool success = FilesystemUtils::storeJson(filesystem, filename, doc); if (success) { MO_DBG_DEBUG("Saving variables finished"); } else { MO_DBG_ERR("could not save variables file: %s", filename); } return success; } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Model/Variables/VariableContainer.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License /* * Implementation of the UCs B05 - B06 */ #ifndef MO_VARIABLECONTAINER_H #define MO_VARIABLECONTAINER_H #include #if MO_ENABLE_V201 #include #include #include #include namespace MicroOcpp { class VariableContainer { public: ~VariableContainer(); virtual size_t size() = 0; virtual Variable *getVariable(size_t i) = 0; virtual Variable *getVariable(const ComponentId& component, const char *variableName) = 0; virtual bool commit(); }; class VariableContainerNonOwning : public VariableContainer, public MemoryManaged { private: Vector variables; public: VariableContainerNonOwning(); size_t size() override; Variable *getVariable(size_t i) override; Variable *getVariable(const ComponentId& component, const char *variableName) override; bool add(Variable *variable); }; class VariableContainerOwning : public VariableContainer, public MemoryManaged { private: Vector> variables; std::shared_ptr filesystem; char *filename = nullptr; uint16_t trackWriteCount = 0; bool checkWriteCountUpdated(); bool loaded = false; public: VariableContainerOwning(); ~VariableContainerOwning(); size_t size() override; Variable *getVariable(size_t i) override; Variable *getVariable(const ComponentId& component, const char *variableName) override; bool add(std::unique_ptr variable); bool enablePersistency(std::shared_ptr filesystem, const char *filename); bool load(); //load variables from flash bool commit() override; }; } //end namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Model/Variables/VariableService.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License /* * Implementation of the UCs B05 - B06 */ #include #if MO_ENABLE_V201 #include #include #include #include #include #include #include #include #include #include namespace MicroOcpp { template VariableValidator::VariableValidator(const ComponentId& component, const char *name, bool (*validateFn)(T, void*), void *userPtr) : MemoryManaged("v201.Variables.VariableValidator.", name), component(component), name(name), userPtr(userPtr), validateFn(validateFn) { } template bool VariableValidator::validate(T v) { return validateFn(v, userPtr); } template VariableValidator *getVariableValidator(Vector>& collection, const ComponentId& component, const char *name) { for (size_t i = 0; i < collection.size(); i++) { auto& validator = collection[i]; if (!strcmp(name, validator.name) && component.equals(validator.component)) { return &validator; } } return nullptr; } VariableValidator *VariableService::getValidatorInt(const ComponentId& component, const char *name) { return getVariableValidator(validatorInt, component, name); } VariableValidator *VariableService::getValidatorBool(const ComponentId& component, const char *name) { return getVariableValidator(validatorBool, component, name); } VariableValidator *VariableService::getValidatorString(const ComponentId& component, const char *name) { return getVariableValidator(validatorString, component, name); } VariableContainerOwning& VariableService::getContainerInternalByVariable(const ComponentId& component, const char *name) { unsigned int hash = 0; for (size_t i = 0; i < strlen(component.name); i++) { hash += (unsigned int)component.name[i]; } if (component.evse.id >= 0) hash += (unsigned int)component.evse.id; if (component.evse.connectorId >= 0) hash += (unsigned int)component.evse.connectorId; for (size_t i = 0; i < strlen(name); i++) { hash += (unsigned int)name[i]; } return containersInternal[hash % MO_VARIABLESTORE_BUCKETS]; } void VariableService::addContainer(VariableContainer *container) { containers.push_back(container); } template bool registerVariableValidator(Vector>& collection, const ComponentId& component, const char *name, bool (*validate)(T, void*), void *userPtr) { for (auto it = collection.begin(); it != collection.end(); it++) { if (!strcmp(name, it->name) && component.equals(it->component)) { collection.erase(it); break; } } collection.emplace_back(component, name, validate, userPtr); return true; } template <> bool VariableService::registerValidator(const ComponentId& component, const char *name, bool (*validate)(int, void*), void *userPtr) { return registerVariableValidator(validatorInt, component, name, validate, userPtr); } template <> bool VariableService::registerValidator(const ComponentId& component, const char *name, bool (*validate)(bool, void*), void *userPtr) { return registerVariableValidator(validatorBool, component, name, validate, userPtr); } template <> bool VariableService::registerValidator(const ComponentId& component, const char *name, bool (*validate)(const char*, void*), void *userPtr) { return registerVariableValidator(validatorString, component, name, validate, userPtr); } Variable *VariableService::getVariable(const ComponentId& component, const char *name) { if (auto variable = getContainerInternalByVariable(component, name).getVariable(component, name)) { return variable; } for (size_t i = 0; i < containers.size(); i++) { auto container = containers[containers.size() - i - 1]; //search from back, because internal containers at front don't contain variable if (auto variable = container->getVariable(component, name)) { return variable; } } return nullptr; } VariableService::VariableService(Context& context, std::shared_ptr filesystem) : MemoryManaged("v201.Variables.VariableService"), context(context), filesystem(filesystem), containers(makeVector(getMemoryTag())), validatorInt(makeVector>(getMemoryTag())), validatorBool(makeVector>(getMemoryTag())), validatorString(makeVector>(getMemoryTag())) { containers.reserve(MO_VARIABLESTORE_BUCKETS + 1); for (unsigned int i = 0; i < MO_VARIABLESTORE_BUCKETS; i++) { char fn [MO_MAX_PATH_SIZE]; auto ret = snprintf(fn, sizeof(fn), "%s%02x%s", MO_VARIABLESTORE_FN_PREFIX, i, MO_VARIABLESTORE_FN_SUFFIX); if (ret < 0 || (size_t)ret >= sizeof(fn)) { MO_DBG_ERR("fn error"); continue; } containersInternal[i].enablePersistency(filesystem, fn); containers.push_back(&containersInternal[i]); } containers.push_back(&containerExternal); context.getOperationRegistry().registerOperation("SetVariables", [this] () { return new Ocpp201::SetVariables(*this);}); context.getOperationRegistry().registerOperation("GetVariables", [this] () { return new Ocpp201::GetVariables(*this);}); context.getOperationRegistry().registerOperation("GetBaseReport", [this] () { return new Ocpp201::GetBaseReport(*this);}); } template bool loadVariableFactoryDefault(Variable& variable, T factoryDef); template<> bool loadVariableFactoryDefault(Variable& variable, int factoryDef) { variable.setInt(factoryDef); return true; } template<> bool loadVariableFactoryDefault(Variable& variable, bool factoryDef) { variable.setBool(factoryDef); return true; } template<> bool loadVariableFactoryDefault(Variable& variable, const char *factoryDef) { return variable.setString(factoryDef); } void loadVariableCharacteristics(Variable& variable, Variable::Mutability mutability, bool persistent, bool rebootRequired, Variable::InternalDataType defaultDataType) { if (variable.getMutability() == Variable::Mutability::ReadWrite) { variable.setMutability(mutability); } if (persistent) { variable.setPersistent(); } if (rebootRequired) { variable.setRebootRequired(); } switch (defaultDataType) { case Variable::InternalDataType::Int: variable.setVariableDataType(MicroOcpp::VariableCharacteristics::DataType::integer); break; case Variable::InternalDataType::Bool: variable.setVariableDataType(MicroOcpp::VariableCharacteristics::DataType::boolean); break; case Variable::InternalDataType::String: variable.setVariableDataType(MicroOcpp::VariableCharacteristics::DataType::string); break; default: MO_DBG_ERR("internal error"); break; } } template Variable::InternalDataType getInternalDataType(); template<> Variable::InternalDataType getInternalDataType() {return Variable::InternalDataType::Int;} template<> Variable::InternalDataType getInternalDataType() {return Variable::InternalDataType::Bool;} template<> Variable::InternalDataType getInternalDataType() {return Variable::InternalDataType::String;} template Variable *VariableService::declareVariable(const ComponentId& component, const char *name, T factoryDefault, Variable::Mutability mutability, bool persistent, Variable::AttributeTypeSet attributes, bool rebootRequired) { auto res = getVariable(component, name); if (!res) { auto variable = makeVariable(getInternalDataType(), attributes); if (!variable) { MO_DBG_ERR("OOM"); return nullptr; } variable->setName(name); variable->setComponentId(component); if (!loadVariableFactoryDefault(*variable, factoryDefault)) { return nullptr; } res = variable.get(); if (!getContainerInternalByVariable(component, name).add(std::move(variable))) { return nullptr; } } loadVariableCharacteristics(*res, mutability, persistent, rebootRequired, getInternalDataType()); return res; } template Variable *VariableService::declareVariable( const ComponentId&, const char*, int, Variable::Mutability, bool, Variable::AttributeTypeSet, bool); template Variable *VariableService::declareVariable( const ComponentId&, const char*, bool, Variable::Mutability, bool, Variable::AttributeTypeSet, bool); template Variable *VariableService::declareVariable(const ComponentId&, const char*, const char*, Variable::Mutability, bool, Variable::AttributeTypeSet, bool); bool VariableService::addVariable(Variable *variable) { return containerExternal.add(variable); } bool VariableService::addVariable(std::unique_ptr variable) { return getContainerInternalByVariable(variable->getComponentId(), variable->getName()).add(std::move(variable)); } bool VariableService::load() { bool success = true; for (size_t i = 0; i < MO_VARIABLESTORE_BUCKETS; i++) { if (!containersInternal[i].load()) { success = false; } } return success; } bool VariableService::commit() { bool success = true; for (size_t i = 0; i < containers.size(); i++) { if (!containers[i]->commit()) { success = false; } } return success; } SetVariableStatus VariableService::setVariable(Variable::AttributeType attrType, const char *value, const ComponentId& component, const char *variableName) { Variable *variable = nullptr; bool foundComponent = false; for (size_t i = 0; i < containers.size(); i++) { auto container = containers[i]; for (size_t i = 0; i < container->size(); i++) { auto entry = container->getVariable(i); if (entry->getComponentId().equals(component)) { foundComponent = true; if (!strcmp(entry->getName(), variableName)) { // found variable. Search terminated in this block variable = entry; break; } } } if (variable) { // result found in inner for-loop break; } } if (!variable) { if (foundComponent) { return SetVariableStatus::UnknownVariable; } else { return SetVariableStatus::UnknownComponent; } } if (variable->getMutability() == Variable::Mutability::ReadOnly) { return SetVariableStatus::Rejected; } if (!variable->hasAttribute(attrType)) { return SetVariableStatus::NotSupportedAttributeType; } //write config /* * Try to interpret input as number */ bool convertibleInt = true; int numInt = 0; bool convertibleBool = true; bool numBool = false; int nDigits = 0, nNonDigits = 0, nDots = 0, nSign = 0; //"-1.234" has 4 digits, 0 nonDigits, 1 dot and 1 sign. Don't allow comma as seperator. Don't allow e-expressions (e.g. 1.23e-7) for (const char *c = value; *c; ++c) { if (*c >= '0' && *c <= '9') { //int interpretation if (nDots == 0) { //only append number if before floating point nDigits++; numInt *= 10; numInt += *c - '0'; } } else if (*c == '.') { nDots++; } else if (c == value && *c == '-') { nSign++; } else { nNonDigits++; } } if (nSign == 1) { numInt = -numInt; } int INT_MAXDIGITS; //plausibility check: this allows a numerical range of (-999,999,999 to 999,999,999), or (-9,999 to 9,999) respectively if (sizeof(int) >= 4UL) INT_MAXDIGITS = 9; else INT_MAXDIGITS = 4; if (nNonDigits > 0 || nDigits == 0 || nSign > 1 || nDots > 1) { convertibleInt = false; } if (nDigits > INT_MAXDIGITS) { MO_DBG_DEBUG("Possible integer overflow: key = %s, value = %s", variableName, value); convertibleInt = false; } if (tolower(value[0]) == 't' && tolower(value[1]) == 'r' && tolower(value[2]) == 'u' && tolower(value[3]) == 'e' && !value[4]) { numBool = true; } else if (tolower(value[0]) == 'f' && tolower(value[1]) == 'a' && tolower(value[2]) == 'l' && tolower(value[3]) == 's' && tolower(value[4]) == 'e' && !value[5]) { numBool = false; } else if (convertibleInt) { numBool = numInt != 0; } else { convertibleBool = false; } // validate and store (parsed) value to Config if (variable->getInternalDataType() == Variable::InternalDataType::Int && convertibleInt) { auto validator = getValidatorInt(component, variableName); if (validator && !validator->validate(numInt)) { MO_DBG_WARN("validation failed for variable=%s", variableName); return SetVariableStatus::Rejected; } variable->setInt(numInt); } else if (variable->getInternalDataType() == Variable::InternalDataType::Bool && convertibleBool) { auto validator = getValidatorBool(component, variableName); if (validator && !validator->validate(numBool)) { MO_DBG_WARN("validation failed for variable=%s", variableName); return SetVariableStatus::Rejected; } variable->setBool(numBool); } else if (variable->getInternalDataType() == Variable::InternalDataType::String) { auto validator = getValidatorString(component, variableName); if (validator && !validator->validate(value)) { MO_DBG_WARN("validation failed for variable=%s", variableName); return SetVariableStatus::Rejected; } variable->setString(value); } else { MO_DBG_WARN("Value has incompatible type"); return SetVariableStatus::Rejected; } if (variable->isRebootRequired()) { return SetVariableStatus::RebootRequired; } return SetVariableStatus::Accepted; } GetVariableStatus VariableService::getVariable(Variable::AttributeType attrType, const ComponentId& component, const char *variableName, Variable **result) { bool foundComponent = false; for (size_t i = 0; i < containers.size(); i++) { auto container = containers[i]; for (size_t i = 0; i < container->size(); i++) { auto variable = container->getVariable(i); if (variable->getComponentId().equals(component)) { foundComponent = true; if (!strcmp(variable->getName(), variableName)) { // found variable. Search terminated in this block if (variable->getMutability() == Variable::Mutability::WriteOnly) { return GetVariableStatus::Rejected; } if (variable->hasAttribute(attrType)) { *result = variable; return GetVariableStatus::Accepted; } else { return GetVariableStatus::NotSupportedAttributeType; } } } } } if (foundComponent) { return GetVariableStatus::UnknownVariable; } else { return GetVariableStatus::UnknownComponent; } } GenericDeviceModelStatus VariableService::getBaseReport(int requestId, ReportBase reportBase) { if (reportBase == ReportBase_SummaryInventory) { return GenericDeviceModelStatus_NotSupported; } Vector variables = makeVector(getMemoryTag()); for (size_t i = 0; i < containers.size(); i++) { auto container = containers[i]; for (size_t i = 0; i < container->size(); i++) { auto variable = container->getVariable(i); if (reportBase == ReportBase_ConfigurationInventory && variable->getMutability() == Variable::Mutability::ReadOnly) { continue; } variables.push_back(variable); } } if (variables.empty()) { return GenericDeviceModelStatus_EmptyResultSet; } auto notifyReport = makeRequest(new Ocpp201::NotifyReport( context.getModel(), requestId, context.getModel().getClock().now(), false, 0, variables)); context.initiateRequest(std::move(notifyReport)); return GenericDeviceModelStatus_Accepted; } } // namespace MicroOcpp #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Model/Variables/VariableService.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License /* * Implementation of the UCs B05 - B06 */ #ifndef MO_VARIABLESERVICE_H #define MO_VARIABLESERVICE_H #include #include #include #include #if MO_ENABLE_V201 #include #include #include #include #ifndef MO_VARIABLESTORE_FN_PREFIX #define MO_VARIABLESTORE_FN_PREFIX (MO_FILENAME_PREFIX "ocpp-vars-") #endif #ifndef MO_VARIABLESTORE_FN_SUFFIX #define MO_VARIABLESTORE_FN_SUFFIX ".jsn" #endif namespace MicroOcpp { template struct VariableValidator : public MemoryManaged { ComponentId component; const char *name; void *userPtr; bool (*validateFn)(T, void*); VariableValidator(const ComponentId& component, const char *name, bool (*validate)(T, void*), void *userPtr); bool validate(T); }; class Context; #ifndef MO_VARIABLESTORE_BUCKETS #define MO_VARIABLESTORE_BUCKETS 8 #endif class VariableService : public MemoryManaged { private: Context& context; std::shared_ptr filesystem; Vector containers; VariableContainerNonOwning containerExternal; VariableContainerOwning containersInternal [MO_VARIABLESTORE_BUCKETS]; VariableContainerOwning& getContainerInternalByVariable(const ComponentId& component, const char *name); Vector> validatorInt; Vector> validatorBool; Vector> validatorString; VariableValidator *getValidatorInt(const ComponentId& component, const char *name); VariableValidator *getValidatorBool(const ComponentId& component, const char *name); VariableValidator *getValidatorString(const ComponentId& component, const char *name); public: VariableService(Context& context, std::shared_ptr filesystem); //Get Variable. If not existent, create Variable owned by MO and return template Variable *declareVariable(const ComponentId& component, const char *name, T factoryDefault, Variable::Mutability mutability = Variable::Mutability::ReadWrite, bool persistent = true, Variable::AttributeTypeSet attributes = Variable::AttributeTypeSet(), bool rebootRequired = false); bool addVariable(Variable *variable); //Add Variable without transferring ownership bool addVariable(std::unique_ptr variable); //Add Variable and transfer ownership //Get Variable. If not existent, return nullptr Variable *getVariable(const ComponentId& component, const char *name); bool load(); bool commit(); void addContainer(VariableContainer *container); template bool registerValidator(const ComponentId& component, const char *name, bool (*validate)(T, void*), void *userPtr = nullptr); SetVariableStatus setVariable(Variable::AttributeType attrType, const char *attrVal, const ComponentId& component, const char *variableName); GetVariableStatus getVariable(Variable::AttributeType attrType, const ComponentId& component, const char *variableName, Variable **result); GenericDeviceModelStatus getBaseReport(int requestId, ReportBase reportBase); }; } // namespace MicroOcpp #endif // MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/Authorize.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using namespace MicroOcpp; namespace MicroOcpp { namespace Ocpp16 { Authorize::Authorize(Model& model, const char *idTagIn) : MemoryManaged("v16.Operation.", "Authorize"), model(model) { if (idTagIn && strnlen(idTagIn, IDTAG_LEN_MAX + 2) <= IDTAG_LEN_MAX) { snprintf(idTag, IDTAG_LEN_MAX + 1, "%s", idTagIn); } else { MO_DBG_WARN("Format violation of idTag. Discard idTag"); } } const char* Authorize::getOperationType(){ return "Authorize"; } std::unique_ptr Authorize::createReq() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + (IDTAG_LEN_MAX + 1)); JsonObject payload = doc->to(); payload["idTag"] = idTag; return doc; } void Authorize::processConf(JsonObject payload){ const char *idTagInfo = payload["idTagInfo"]["status"] | "not specified"; if (!strcmp(idTagInfo, "Accepted")) { MO_DBG_INFO("Request has been accepted"); } else { MO_DBG_INFO("Request has been denied. Reason: %s", idTagInfo); } #if MO_ENABLE_LOCAL_AUTH if (auto authService = model.getAuthorizationService()) { authService->notifyAuthorization(idTag, payload["idTagInfo"]); } #endif //MO_ENABLE_LOCAL_AUTH } void Authorize::processReq(JsonObject payload){ /* * Ignore Contents of this Req-message, because this is for debug purposes only */ } std::unique_ptr Authorize::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), 2 * JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); idTagInfo["status"] = "Accepted"; return doc; } } // namespace Ocpp16 } // namespace MicroOcpp #if MO_ENABLE_V201 namespace MicroOcpp { namespace Ocpp201 { Authorize::Authorize(Model& model, const IdToken& idToken) : MemoryManaged("v201.Operation.Authorize"), model(model) { this->idToken = idToken; } const char* Authorize::getOperationType(){ return "Authorize"; } std::unique_ptr Authorize::createReq() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); JsonObject payload = doc->to(); payload["idToken"]["idToken"] = idToken.get(); payload["idToken"]["type"] = idToken.getTypeCstr(); return doc; } void Authorize::processConf(JsonObject payload){ const char *idTagInfo = payload["idTokenInfo"]["status"] | "_Undefined"; if (!strcmp(idTagInfo, "Accepted")) { MO_DBG_INFO("Request has been accepted"); } else { MO_DBG_INFO("Request has been denied. Reason: %s", idTagInfo); } //if (model.getAuthorizationService()) { // model.getAuthorizationService()->notifyAuthorization(idTag, payload["idTagInfo"]); //} } void Authorize::processReq(JsonObject payload){ /* * Ignore Contents of this Req-message, because this is for debug purposes only */ } std::unique_ptr Authorize::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), 2 * JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); JsonObject idTagInfo = payload.createNestedObject("idTokenInfo"); idTagInfo["status"] = "Accepted"; return doc; } } // namespace Ocpp201 } // namespace MicroOcpp #endif //MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/Authorize.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef AUTHORIZE_H #define AUTHORIZE_H #include #include #include namespace MicroOcpp { class Model; namespace Ocpp16 { class Authorize : public Operation, public MemoryManaged { private: Model& model; char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; public: Authorize(Model& model, const char *idTag); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #if MO_ENABLE_V201 #include namespace MicroOcpp { namespace Ocpp201 { class Authorize : public Operation, public MemoryManaged { private: Model& model; IdToken idToken; public: Authorize(Model& model, const IdToken& idToken); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //end namespace Ocpp201 } //end namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/BootNotification.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include using MicroOcpp::Ocpp16::BootNotification; using MicroOcpp::JsonDoc; BootNotification::BootNotification(Model& model, std::unique_ptr payload) : MemoryManaged("v16.Operation.", "BootNotification"), model(model), credentials(std::move(payload)) { } const char* BootNotification::getOperationType(){ return "BootNotification"; } std::unique_ptr BootNotification::createReq() { if (credentials) { #if MO_ENABLE_V201 if (model.getVersion().major == 2) { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2) + credentials->memoryUsage()); JsonObject payload = doc->to(); payload["reason"] = "PowerUp"; payload["chargingStation"] = *credentials; return doc; } #endif return std::unique_ptr(new JsonDoc(*credentials)); } else { MO_DBG_ERR("payload undefined"); return createEmptyDocument(); } } void BootNotification::processConf(JsonObject payload) { const char* currentTime = payload["currentTime"] | "Invalid"; if (strcmp(currentTime, "Invalid")) { if (model.getClock().setTime(currentTime)) { //success } else { MO_DBG_ERR("Time string format violation. Expect format like 2022-02-01T20:53:32.486Z"); errorCode = "PropertyConstraintViolation"; return; } } else { MO_DBG_ERR("Missing attribute currentTime"); errorCode = "FormationViolation"; return; } int interval = payload["interval"] | -1; if (interval < 0) { errorCode = "FormationViolation"; return; } RegistrationStatus status = deserializeRegistrationStatus(payload["status"] | "Invalid"); if (status == RegistrationStatus::UNDEFINED) { errorCode = "FormationViolation"; return; } if (status == RegistrationStatus::Accepted) { //only write if in valid range if (interval >= 1) { auto heartbeatIntervalInt = declareConfiguration("HeartbeatInterval", 86400); if (heartbeatIntervalInt && interval != heartbeatIntervalInt->getInt()) { heartbeatIntervalInt->setInt(interval); configuration_save(); } } } if (auto bootService = model.getBootService()) { if (status != RegistrationStatus::Accepted) { bootService->setRetryInterval(interval); } bootService->notifyRegistrationStatus(status); } MO_DBG_INFO("request has been %s", status == RegistrationStatus::Accepted ? "Accepted" : status == RegistrationStatus::Pending ? "replied with Pending" : "Rejected"); } void BootNotification::processReq(JsonObject payload){ /* * Ignore Contents of this Req-message, because this is for debug purposes only */ } std::unique_ptr BootNotification::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(3) + (JSONDATE_LENGTH + 1)); JsonObject payload = doc->to(); //safety mechanism; in some test setups the library has to answer BootNotifications without valid system time Timestamp ocppTimeReference = Timestamp(2022,0,27,11,59,55); Timestamp ocppSelect = ocppTimeReference; auto& ocppTime = model.getClock(); Timestamp ocppNow = ocppTime.now(); if (ocppNow > ocppTimeReference) { //time has already been set ocppSelect = ocppNow; } char ocppNowJson [JSONDATE_LENGTH + 1] = {'\0'}; ocppSelect.toJsonString(ocppNowJson, JSONDATE_LENGTH + 1); payload["currentTime"] = ocppNowJson; payload["interval"] = 86400; //heartbeat send interval - not relevant for JSON variant of OCPP so send dummy value that likely won't break payload["status"] = "Accepted"; return doc; } ================================================ FILE: src/MicroOcpp/Operations/BootNotification.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_BOOTNOTIFICATION_H #define MO_BOOTNOTIFICATION_H #include #include #define CP_MODEL_LEN_MAX CiString20TypeLen #define CP_SERIALNUMBER_LEN_MAX CiString25TypeLen #define CP_VENDOR_LEN_MAX CiString20TypeLen #define FW_VERSION_LEN_MAX CiString50TypeLen namespace MicroOcpp { class Model; namespace Ocpp16 { class BootNotification : public Operation, public MemoryManaged { private: Model& model; std::unique_ptr credentials; const char *errorCode = nullptr; public: BootNotification(Model& model, std::unique_ptr payload); ~BootNotification() = default; const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/CancelReservation.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_RESERVATION #include #include #include using MicroOcpp::Ocpp16::CancelReservation; using MicroOcpp::JsonDoc; CancelReservation::CancelReservation(ReservationService& reservationService) : MemoryManaged("v16.Operation.", "CancelReservation"), reservationService(reservationService) { } const char* CancelReservation::getOperationType() { return "CancelReservation"; } void CancelReservation::processReq(JsonObject payload) { if (!payload.containsKey("reservationId")) { errorCode = "FormationViolation"; return; } if (auto reservation = reservationService.getReservationById(payload["reservationId"])) { found = true; reservation->clear(); } } std::unique_ptr CancelReservation::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (found) { payload["status"] = "Accepted"; } else { payload["status"] = "Rejected"; } return doc; } #endif //MO_ENABLE_RESERVATION ================================================ FILE: src/MicroOcpp/Operations/CancelReservation.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CANCELRESERVATION_H #define MO_CANCELRESERVATION_H #include #if MO_ENABLE_RESERVATION #include namespace MicroOcpp { class ReservationService; namespace Ocpp16 { class CancelReservation : public Operation, public MemoryManaged { private: ReservationService& reservationService; bool found = false; const char *errorCode = nullptr; public: CancelReservation(ReservationService& reservationService); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif //MO_ENABLE_RESERVATION #endif ================================================ FILE: src/MicroOcpp/Operations/ChangeAvailability.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include namespace MicroOcpp { namespace Ocpp16 { ChangeAvailability::ChangeAvailability(Model& model) : MemoryManaged("v16.Operation.", "ChangeAvailability"), model(model) { } const char* ChangeAvailability::getOperationType(){ return "ChangeAvailability"; } void ChangeAvailability::processReq(JsonObject payload) { int connectorIdRaw = payload["connectorId"] | -1; if (connectorIdRaw < 0) { errorCode = "FormationViolation"; return; } unsigned int connectorId = (unsigned int)connectorIdRaw; if (connectorId >= model.getNumConnectors()) { errorCode = "PropertyConstraintViolation"; return; } const char *type = payload["type"] | "INVALID"; bool available = false; if (!strcmp(type, "Operative")) { accepted = true; available = true; } else if (!strcmp(type, "Inoperative")) { accepted = true; available = false; } else { errorCode = "PropertyConstraintViolation"; return; } if (connectorId == 0) { for (unsigned int cId = 0; cId < model.getNumConnectors(); cId++) { auto connector = model.getConnector(cId); connector->setAvailability(available); if (connector->isOperative() && !available) { scheduled = true; } } } else { auto connector = model.getConnector(connectorId); connector->setAvailability(available); if (connector->isOperative() && !available) { scheduled = true; } } } std::unique_ptr ChangeAvailability::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (!accepted) { payload["status"] = "Rejected"; } else if (scheduled) { payload["status"] = "Scheduled"; } else { payload["status"] = "Accepted"; } return doc; } } // namespace Ocpp16 } // namespace MicroOcpp #if MO_ENABLE_V201 #include namespace MicroOcpp { namespace Ocpp201 { ChangeAvailability::ChangeAvailability(AvailabilityService& availabilityService) : MemoryManaged("v201.Operation.", "ChangeAvailability"), availabilityService(availabilityService) { } const char* ChangeAvailability::getOperationType(){ return "ChangeAvailability"; } void ChangeAvailability::processReq(JsonObject payload) { unsigned int evseId = 0; if (payload.containsKey("evse")) { int evseIdRaw = payload["evse"]["id"] | -1; if (evseIdRaw < 0) { errorCode = "FormationViolation"; return; } evseId = (unsigned int)evseIdRaw; if ((payload["evse"]["connectorId"] | 1) != 1) { errorCode = "PropertyConstraintViolation"; return; } } auto availabilityEvse = availabilityService.getEvse(evseId); if (!availabilityEvse) { errorCode = "PropertyConstraintViolation"; return; } const char *type = payload["operationalStatus"] | "_Undefined"; bool operative = false; if (!strcmp(type, "Operative")) { operative = true; } else if (!strcmp(type, "Inoperative")) { operative = false; } else { errorCode = "PropertyConstraintViolation"; return; } status = availabilityEvse->changeAvailability(operative); } std::unique_ptr ChangeAvailability::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); switch (status) { case ChangeAvailabilityStatus::Accepted: payload["status"] = "Accepted"; break; case ChangeAvailabilityStatus::Scheduled: payload["status"] = "Scheduled"; break; case ChangeAvailabilityStatus::Rejected: payload["status"] = "Rejected"; break; } return doc; } } // namespace Ocpp201 } // namespace MicroOcpp #endif //MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/ChangeAvailability.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CHANGEAVAILABILITY_H #define MO_CHANGEAVAILABILITY_H #include namespace MicroOcpp { class Model; namespace Ocpp16 { class ChangeAvailability : public Operation, public MemoryManaged { private: Model& model; bool scheduled = false; bool accepted = false; const char *errorCode {nullptr}; public: ChangeAvailability(Model& model); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #if MO_ENABLE_V201 #include #include namespace MicroOcpp { class AvailabilityService; namespace Ocpp201 { class ChangeAvailability : public Operation, public MemoryManaged { private: AvailabilityService& availabilityService; ChangeAvailabilityStatus status = ChangeAvailabilityStatus::Rejected; const char *errorCode {nullptr}; public: ChangeAvailability(AvailabilityService& availabilityService); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp201 } //end namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/ChangeConfiguration.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include //for tolower using MicroOcpp::Ocpp16::ChangeConfiguration; using MicroOcpp::JsonDoc; ChangeConfiguration::ChangeConfiguration() : MemoryManaged("v16.Operation.", "ChangeConfiguration") { } const char* ChangeConfiguration::getOperationType(){ return "ChangeConfiguration"; } void ChangeConfiguration::processReq(JsonObject payload) { const char *key = payload["key"] | ""; if (!*key) { errorCode = "FormationViolation"; MO_DBG_WARN("Could not read key"); return; } if (!payload["value"].is()) { errorCode = "FormationViolation"; MO_DBG_WARN("Message is lacking value"); return; } const char *value = payload["value"]; auto configuration = getConfigurationPublic(key); if (!configuration) { //configuration not found or hidden configuration notSupported = true; return; } if (configuration->isReadOnly()) { MO_DBG_WARN("Trying to override readonly value"); readOnly = true; return; } //write config /* * Try to interpret input as number */ bool convertibleInt = true; int numInt = 0; bool convertibleBool = true; bool numBool = false; int nDigits = 0, nNonDigits = 0, nDots = 0, nSign = 0; //"-1.234" has 4 digits, 0 nonDigits, 1 dot and 1 sign. Don't allow comma as seperator. Don't allow e-expressions (e.g. 1.23e-7) for (const char *c = value; *c; ++c) { if (*c >= '0' && *c <= '9') { //int interpretation if (nDots == 0) { //only append number if before floating point nDigits++; numInt *= 10; numInt += *c - '0'; } } else if (*c == '.') { nDots++; } else if (c == value && *c == '-') { nSign++; } else { nNonDigits++; } } if (nSign == 1) { numInt = -numInt; } int INT_MAXDIGITS; //plausibility check: this allows a numerical range of (-999,999,999 to 999,999,999), or (-9,999 to 9,999) respectively if (sizeof(int) >= 4UL) INT_MAXDIGITS = 9; else INT_MAXDIGITS = 4; if (nNonDigits > 0 || nDigits == 0 || nSign > 1 || nDots > 1) { convertibleInt = false; } if (nDigits > INT_MAXDIGITS) { MO_DBG_DEBUG("Possible integer overflow: key = %s, value = %s", key, value); convertibleInt = false; } if (tolower(value[0]) == 't' && tolower(value[1]) == 'r' && tolower(value[2]) == 'u' && tolower(value[3]) == 'e' && !value[4]) { numBool = true; } else if (tolower(value[0]) == 'f' && tolower(value[1]) == 'a' && tolower(value[2]) == 'l' && tolower(value[3]) == 's' && tolower(value[4]) == 'e' && !value[5]) { numBool = false; } else { convertibleBool = false; } //check against optional validator auto validator = getConfigurationValidator(key); if (validator && !(*validator)(value)) { //validator exists and validation fails reject = true; MO_DBG_WARN("validation failed for key=%s value=%s", key, value); return; } //Store (parsed) value to Config if (configuration->getType() == TConfig::Int && convertibleInt) { configuration->setInt(numInt); } else if (configuration->getType() == TConfig::Bool && convertibleBool) { configuration->setBool(numBool); } else if (configuration->getType() == TConfig::String) { configuration->setString(value); } else { reject = true; MO_DBG_WARN("Value has incompatible type"); return; } if (!configuration_save()) { MO_DBG_ERR("could not write changes to flash"); errorCode = "InternalError"; return; } if (configuration->isRebootRequired()) { rebootRequired = true; } } std::unique_ptr ChangeConfiguration::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (notSupported) { payload["status"] = "NotSupported"; } else if (reject || readOnly) { payload["status"] = "Rejected"; } else if (rebootRequired) { payload["status"] = "RebootRequired"; } else { payload["status"] = "Accepted"; } return doc; } ================================================ FILE: src/MicroOcpp/Operations/ChangeConfiguration.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CHANGECONFIGURATION_H #define MO_CHANGECONFIGURATION_H #include namespace MicroOcpp { namespace Ocpp16 { class ChangeConfiguration : public Operation, public MemoryManaged { private: bool reject = false; bool rebootRequired = false; bool readOnly = false; bool notSupported = false; const char *errorCode = nullptr; public: ChangeConfiguration(); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/CiStrings.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License /* * A collection of the fixed-length string types in the OCPP specification */ #ifndef MO_CI_STRINGS_H #define MO_CI_STRINGS_H #define CiString20TypeLen 20 #define CiString25TypeLen 25 #define CiString50TypeLen 50 #define CiString255TypeLen 255 #define CiString500TypeLen 500 //specified by OCPP #define IDTAG_LEN_MAX CiString20TypeLen #define CONF_KEYLEN_MAX CiString50TypeLen //not specified by OCPP #define REASON_LEN_MAX 15 #endif ================================================ FILE: src/MicroOcpp/Operations/ClearCache.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include using MicroOcpp::Ocpp16::ClearCache; using MicroOcpp::JsonDoc; ClearCache::ClearCache(std::shared_ptr filesystem) : MemoryManaged("v16.Operation.", "ClearCache"), filesystem(filesystem) { } const char* ClearCache::getOperationType(){ return "ClearCache"; } void ClearCache::processReq(JsonObject payload) { MO_DBG_WARN("Clear transaction log (Authorization Cache not supported)"); if (!filesystem) { //no persistency - nothing to do return; } success = FilesystemUtils::remove_if(filesystem, [] (const char *fname) -> bool { return !strncmp(fname, "sd", strlen("sd")) || !strncmp(fname, "tx", strlen("tx")) || !strncmp(fname, "op", strlen("op")); }); } std::unique_ptr ClearCache::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (success) { payload["status"] = "Accepted"; //"Accepted", because the intended postcondition is true } else { payload["status"] = "Rejected"; } return doc; } ================================================ FILE: src/MicroOcpp/Operations/ClearCache.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CLEARCACHE_H #define MO_CLEARCACHE_H #include #include namespace MicroOcpp { namespace Ocpp16 { class ClearCache : public Operation, public MemoryManaged { private: std::shared_ptr filesystem; bool success = true; public: ClearCache(std::shared_ptr filesystem); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/ClearChargingProfile.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using MicroOcpp::Ocpp16::ClearChargingProfile; using MicroOcpp::JsonDoc; ClearChargingProfile::ClearChargingProfile(SmartChargingService& scService) : MemoryManaged("v16.Operation.", "ClearChargingProfile"), scService(scService) { } const char* ClearChargingProfile::getOperationType(){ return "ClearChargingProfile"; } void ClearChargingProfile::processReq(JsonObject payload) { std::function filter = [payload] (int chargingProfileId, int connectorId, ChargingProfilePurposeType chargingProfilePurpose, int stackLevel) { if (payload.containsKey("id")) { if (chargingProfileId == (payload["id"] | -1)) { return true; } else { return false; } } if (payload.containsKey("connectorId")) { if (connectorId != (payload["connectorId"] | -1)) { return false; } } if (payload.containsKey("chargingProfilePurpose")) { switch (chargingProfilePurpose) { case ChargingProfilePurposeType::ChargePointMaxProfile: if (strcmp(payload["chargingProfilePurpose"] | "INVALID", "ChargePointMaxProfile")) { return false; } break; case ChargingProfilePurposeType::TxDefaultProfile: if (strcmp(payload["chargingProfilePurpose"] | "INVALID", "TxDefaultProfile")) { return false; } break; case ChargingProfilePurposeType::TxProfile: if (strcmp(payload["chargingProfilePurpose"] | "INVALID", "TxProfile")) { return false; } break; } } if (payload.containsKey("stackLevel")) { if (stackLevel != (payload["stackLevel"] | -1)) { return false; } } return true; }; matchingProfilesFound = scService.clearChargingProfile(filter); } std::unique_ptr ClearChargingProfile::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (matchingProfilesFound) payload["status"] = "Accepted"; else payload["status"] = "Unknown"; return doc; } ================================================ FILE: src/MicroOcpp/Operations/ClearChargingProfile.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CLEARCHARGINGPROFILE_H #define MO_CLEARCHARGINGPROFILE_H #include namespace MicroOcpp { class SmartChargingService; namespace Ocpp16 { class ClearChargingProfile : public Operation, public MemoryManaged { private: SmartChargingService& scService; bool matchingProfilesFound = false; public: ClearChargingProfile(SmartChargingService& scService); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/CustomOperation.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include using MicroOcpp::Ocpp16::CustomOperation; using MicroOcpp::JsonDoc; CustomOperation::CustomOperation(const char *operationType, std::function ()> fn_createReq, std::function fn_processConf, std::function fn_processErr) : MemoryManaged("Operation.Custom.", operationType), operationType{makeString(getMemoryTag(), operationType)}, fn_createReq{fn_createReq}, fn_processConf{fn_processConf}, fn_processErr{fn_processErr} { } CustomOperation::CustomOperation(const char *operationType, std::function fn_processReq, std::function ()> fn_createConf, std::function fn_getErrorCode, std::function fn_getErrorDescription, std::function ()> fn_getErrorDetails) : MemoryManaged("Operation.Custom.", operationType), operationType{makeString(getMemoryTag(), operationType)}, fn_processReq{fn_processReq}, fn_createConf{fn_createConf}, fn_getErrorCode{fn_getErrorCode}, fn_getErrorDescription{fn_getErrorDescription}, fn_getErrorDetails{fn_getErrorDetails} { } CustomOperation::~CustomOperation() { } const char* CustomOperation::getOperationType() { return operationType.c_str(); } std::unique_ptr CustomOperation::createReq() { return fn_createReq(); } void CustomOperation::processConf(JsonObject payload) { return fn_processConf(payload); } bool CustomOperation::processErr(const char *code, const char *description, JsonObject details) { if (fn_processErr) { return fn_processErr(code, description, details); } return true; } void CustomOperation::processReq(JsonObject payload) { return fn_processReq(payload); } std::unique_ptr CustomOperation::createConf() { return fn_createConf(); } const char *CustomOperation::getErrorCode() { if (fn_getErrorCode) { return fn_getErrorCode(); } else { return nullptr; } } const char *CustomOperation::getErrorDescription() { if (fn_getErrorDescription) { return fn_getErrorDescription(); } else { return ""; } } std::unique_ptr CustomOperation::getErrorDetails() { if (fn_getErrorDetails) { return fn_getErrorDetails(); } else { return createEmptyDocument(); } } ================================================ FILE: src/MicroOcpp/Operations/CustomOperation.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CUSTOMOPERATION_H #define MO_CUSTOMOPERATION_H #include #include namespace MicroOcpp { namespace Ocpp16 { class CustomOperation : public Operation, public MemoryManaged { private: String operationType; std::function ()> fn_createReq; std::function fn_processConf; std::function fn_processErr; //optional std::function fn_processReq; std::function ()> fn_createConf; std::function fn_getErrorCode; //optional std::function fn_getErrorDescription; //optional std::function ()> fn_getErrorDetails; //optional public: //for operations initiated at this device CustomOperation(const char *operationType, std::function ()> fn_createReq, std::function fn_processConf, std::function fn_processErr = nullptr); //for operations receied from remote CustomOperation(const char *operationType, std::function fn_processReq, std::function ()> fn_createConf, std::function fn_getErrorCode = nullptr, std::function fn_getErrorDescription = nullptr, std::function ()> fn_getErrorDetails = nullptr); ~CustomOperation(); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; bool processErr(const char *code, const char *description, JsonObject details) override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override; const char *getErrorDescription() override; std::unique_ptr getErrorDetails() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/DataTransfer.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include using MicroOcpp::Ocpp16::DataTransfer; using MicroOcpp::JsonDoc; DataTransfer::DataTransfer() : MemoryManaged("v16.Operation.", "DataTransfer") { } DataTransfer::DataTransfer(const String &msg) : MemoryManaged("v16.Operation.", "DataTransfer"), msg{makeString(getMemoryTag(), msg.c_str())} { } const char* DataTransfer::getOperationType(){ return "DataTransfer"; } std::unique_ptr DataTransfer::createReq() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2) + (msg.length() + 1)); JsonObject payload = doc->to(); payload["vendorId"] = "CustomVendor"; payload["data"] = msg; return doc; } void DataTransfer::processConf(JsonObject payload){ const char *status = payload["status"] | "Invalid"; if (!strcmp(status, "Accepted")) { MO_DBG_DEBUG("Request has been accepted"); } else { MO_DBG_INFO("Request has been denied"); } } void DataTransfer::processReq(JsonObject payload) { // Do nothing - we're just required to reject these DataTransfer requests } std::unique_ptr DataTransfer::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = "Rejected"; return doc; } ================================================ FILE: src/MicroOcpp/Operations/DataTransfer.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_DATATRANSFER_H #define MO_DATATRANSFER_H #include namespace MicroOcpp { namespace Ocpp16 { class DataTransfer : public Operation, public MemoryManaged { private: String msg; public: DataTransfer(); DataTransfer(const String &msg); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/DeleteCertificate.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_CERT_MGMT #include #include #include using MicroOcpp::Ocpp201::DeleteCertificate; using MicroOcpp::JsonDoc; DeleteCertificate::DeleteCertificate(CertificateService& certService) : MemoryManaged("v201.Operation.", "DeleteCertificate"), certService(certService) { } void DeleteCertificate::processReq(JsonObject payload) { JsonObject certIdJson = payload["certificateHashData"]; if (!certIdJson.containsKey("hashAlgorithm") || !certIdJson.containsKey("issuerNameHash") || !certIdJson.containsKey("issuerKeyHash") || !certIdJson.containsKey("serialNumber")) { errorCode = "FormationViolation"; return; } const char *hashAlgorithm = certIdJson["hashAlgorithm"] | "_Invalid"; if (!certIdJson["issuerNameHash"].is() || !certIdJson["issuerKeyHash"].is() || !certIdJson["serialNumber"].is()) { errorCode = "FormationViolation"; return; } CertificateHash cert; if (!strcmp(hashAlgorithm, "SHA256")) { cert.hashAlgorithm = HashAlgorithmType_SHA256; } else if (!strcmp(hashAlgorithm, "SHA384")) { cert.hashAlgorithm = HashAlgorithmType_SHA384; } else if (!strcmp(hashAlgorithm, "SHA512")) { cert.hashAlgorithm = HashAlgorithmType_SHA512; } else { errorCode = "FormationViolation"; return; } auto retIN = ocpp_cert_set_issuerNameHash(&cert, certIdJson["issuerNameHash"] | "_Invalid", cert.hashAlgorithm); auto retIK = ocpp_cert_set_issuerKeyHash(&cert, certIdJson["issuerKeyHash"] | "_Invalid", cert.hashAlgorithm); auto retSN = ocpp_cert_set_serialNumber(&cert, certIdJson["serialNumber"] | "_Invalid"); if (retIN < 0 || retIK < 0 || retSN < 0) { errorCode = "FormationViolation"; return; } auto certStore = certService.getCertificateStore(); if (!certStore) { errorCode = "NotSupported"; return; } auto status = certStore->deleteCertificate(cert); switch (status) { case DeleteCertificateStatus_Accepted: this->status = "Accepted"; break; case DeleteCertificateStatus_Failed: this->status = "Failed"; break; case DeleteCertificateStatus_NotFound: this->status = "NotFound"; break; default: MO_DBG_ERR("internal error"); errorCode = "InternalError"; return; } //operation executed successfully } std::unique_ptr DeleteCertificate::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = status; return doc; } #endif //MO_ENABLE_CERT_MGMT ================================================ FILE: src/MicroOcpp/Operations/DeleteCertificate.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_DELETECERTIFICATE_H #define MO_DELETECERTIFICATE_H #include #if MO_ENABLE_CERT_MGMT #include namespace MicroOcpp { class CertificateService; namespace Ocpp201 { class DeleteCertificate : public Operation, public MemoryManaged { private: CertificateService& certService; const char *status = nullptr; const char *errorCode = nullptr; public: DeleteCertificate(CertificateService& certService); const char* getOperationType() override {return "DeleteCertificate";} void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp201 } //end namespace MicroOcpp #endif //MO_ENABLE_CERT_MGMT #endif ================================================ FILE: src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using MicroOcpp::Ocpp16::DiagnosticsStatusNotification; using MicroOcpp::JsonDoc; DiagnosticsStatusNotification::DiagnosticsStatusNotification(DiagnosticsStatus status) : MemoryManaged("v16.Operation.", "DiagnosticsStatusNotification"), status(status) { } const char *DiagnosticsStatusNotification::cstrFromStatus(DiagnosticsStatus status) { switch (status) { case (DiagnosticsStatus::Idle): return "Idle"; break; case (DiagnosticsStatus::Uploaded): return "Uploaded"; break; case (DiagnosticsStatus::UploadFailed): return "UploadFailed"; break; case (DiagnosticsStatus::Uploading): return "Uploading"; break; } return nullptr; //cannot be reached } std::unique_ptr DiagnosticsStatusNotification::createReq() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = cstrFromStatus(status); return doc; } void DiagnosticsStatusNotification::processConf(JsonObject payload){ // no payload, nothing to do } ================================================ FILE: src/MicroOcpp/Operations/DiagnosticsStatusNotification.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #ifndef MO_DIAGNOSTICSSTATUSNOTIFICATION_H #define MO_DIAGNOSTICSSTATUSNOTIFICATION_H namespace MicroOcpp { namespace Ocpp16 { class DiagnosticsStatusNotification : public Operation, public MemoryManaged { private: DiagnosticsStatus status = DiagnosticsStatus::Idle; static const char *cstrFromStatus(DiagnosticsStatus status); public: DiagnosticsStatusNotification(DiagnosticsStatus status); const char* getOperationType() override {return "DiagnosticsStatusNotification"; } std::unique_ptr createReq() override; void processConf(JsonObject payload) override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/FirmwareStatusNotification.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using MicroOcpp::Ocpp16::FirmwareStatusNotification; using MicroOcpp::JsonDoc; FirmwareStatusNotification::FirmwareStatusNotification(FirmwareStatus status) : MemoryManaged("v16.Operation.", "FirmwareStatusNotification"), status{status} { } const char *FirmwareStatusNotification::cstrFromFwStatus(FirmwareStatus status) { switch (status) { case (FirmwareStatus::Downloaded): return "Downloaded"; break; case (FirmwareStatus::DownloadFailed): return "DownloadFailed"; break; case (FirmwareStatus::Downloading): return "Downloading"; break; case (FirmwareStatus::Idle): return "Idle"; break; case (FirmwareStatus::InstallationFailed): return "InstallationFailed"; break; case (FirmwareStatus::Installing): return "Installing"; break; case (FirmwareStatus::Installed): return "Installed"; break; } return NULL; //cannot be reached } std::unique_ptr FirmwareStatusNotification::createReq() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = cstrFromFwStatus(status); return doc; } void FirmwareStatusNotification::processConf(JsonObject payload){ // no payload, nothing to do } ================================================ FILE: src/MicroOcpp/Operations/FirmwareStatusNotification.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #ifndef MO_FIRMWARESTATUSNOTIFICATION_H #define MO_FIRMWARESTATUSNOTIFICATION_H namespace MicroOcpp { namespace Ocpp16 { class FirmwareStatusNotification : public Operation, public MemoryManaged { private: FirmwareStatus status = FirmwareStatus::Idle; static const char *cstrFromFwStatus(FirmwareStatus status); public: FirmwareStatusNotification(FirmwareStatus status); const char* getOperationType() override {return "FirmwareStatusNotification"; } std::unique_ptr createReq() override; void processConf(JsonObject payload) override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/GetBaseReport.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include using MicroOcpp::Ocpp201::GetBaseReport; using MicroOcpp::JsonDoc; GetBaseReport::GetBaseReport(VariableService& variableService) : MemoryManaged("v201.Operation.", "GetBaseReport"), variableService(variableService) { } const char* GetBaseReport::getOperationType(){ return "GetBaseReport"; } void GetBaseReport::processReq(JsonObject payload) { int requestId = payload["requestId"] | -1; if (requestId < 0) { errorCode = "FormationViolation"; MO_DBG_ERR("invalid requestId"); return; } ReportBase reportBase; const char *reportBaseCstr = payload["reportBase"] | ""; if (!strcmp(reportBaseCstr, "ConfigurationInventory")) { reportBase = ReportBase_ConfigurationInventory; } else if (!strcmp(reportBaseCstr, "FullInventory")) { reportBase = ReportBase_FullInventory; } else if (!strcmp(reportBaseCstr, "SummaryInventory")) { reportBase = ReportBase_SummaryInventory; } else { errorCode = "FormationViolation"; MO_DBG_ERR("invalid reportBase"); return; } status = variableService.getBaseReport(requestId, reportBase); } std::unique_ptr GetBaseReport::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); const char *statusCstr = ""; switch (status) { case GenericDeviceModelStatus_Accepted: statusCstr = "Accepted"; break; case GenericDeviceModelStatus_Rejected: statusCstr = "Rejected"; break; case GenericDeviceModelStatus_NotSupported: statusCstr = "NotSupported"; break; case GenericDeviceModelStatus_EmptyResultSet: statusCstr = "EmptyResultSet"; break; default: MO_DBG_ERR("internal error"); break; } payload["status"] = statusCstr; return doc; } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/GetBaseReport.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_GETBASEREPORT_H #define MO_GETBASEREPORT_H #include #if MO_ENABLE_V201 #include #include #include namespace MicroOcpp { class VariableService; namespace Ocpp201 { class GetBaseReport : public Operation, public MemoryManaged { private: VariableService& variableService; GenericDeviceModelStatus status; const char *errorCode = nullptr; public: GetBaseReport(VariableService& variableService); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //namespace Ocpp201 } //namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/GetCompositeSchedule.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using MicroOcpp::Ocpp16::GetCompositeSchedule; using MicroOcpp::JsonDoc; GetCompositeSchedule::GetCompositeSchedule(Model& model, SmartChargingService& scService) : MemoryManaged("v16.Operation.", "GetCompositeSchedule"), model(model), scService(scService) { } const char* GetCompositeSchedule::getOperationType() { return "GetCompositeSchedule"; } void GetCompositeSchedule::processReq(JsonObject payload) { connectorId = payload["connectorId"] | -1; duration = payload["duration"] | 0; if (connectorId < 0 || !payload.containsKey("duration")) { errorCode = "FormationViolation"; return; } if ((unsigned int) connectorId >= model.getNumConnectors()) { errorCode = "PropertyConstraintViolation"; } const char *unitStr = payload["chargingRateUnit"] | "_Undefined"; if (unitStr[0] == 'A' || unitStr[0] == 'a') { chargingRateUnit = ChargingRateUnitType_Optional::Amp; } else if (unitStr[0] == 'W' || unitStr[0] == 'w') { chargingRateUnit = ChargingRateUnitType_Optional::Watt; } } std::unique_ptr GetCompositeSchedule::createConf(){ bool success = false; auto chargingSchedule = scService.getCompositeSchedule((unsigned int) connectorId, duration, chargingRateUnit); JsonDoc chargingScheduleDoc {0}; if (chargingSchedule) { success = chargingSchedule->toJson(chargingScheduleDoc); } char scheduleStart_str [JSONDATE_LENGTH + 1] = {'\0'}; if (success && chargingSchedule) { success = chargingSchedule->startSchedule.toJsonString(scheduleStart_str, JSONDATE_LENGTH + 1); } if (success && chargingSchedule) { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(4) + chargingScheduleDoc.memoryUsage()); JsonObject payload = doc->to(); payload["status"] = "Accepted"; payload["connectorId"] = connectorId; payload["scheduleStart"] = scheduleStart_str; payload["chargingSchedule"] = chargingScheduleDoc; return doc; } else { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = "Rejected"; return doc; } } ================================================ FILE: src/MicroOcpp/Operations/GetCompositeSchedule.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_GETCOMPOSITESCHEDULE_H #define MO_GETCOMPOSITESCHEDULE_H #include #include #include namespace MicroOcpp { class Model; namespace Ocpp16 { class GetCompositeSchedule : public Operation, public MemoryManaged { private: Model& model; SmartChargingService& scService; int connectorId = -1; int duration = -1; ChargingRateUnitType_Optional chargingRateUnit = ChargingRateUnitType_Optional::None; const char *errorCode {nullptr}; public: GetCompositeSchedule(Model& model, SmartChargingService& scService); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/GetConfiguration.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include using MicroOcpp::Ocpp16::GetConfiguration; using MicroOcpp::JsonDoc; GetConfiguration::GetConfiguration() : MemoryManaged("v16.Operation.", "GetConfiguration"), keys{makeVector(getMemoryTag())} { } const char* GetConfiguration::getOperationType(){ return "GetConfiguration"; } void GetConfiguration::processReq(JsonObject payload) { JsonArray requestedKeys = payload["key"]; for (size_t i = 0; i < requestedKeys.size(); i++) { keys.push_back(makeString(getMemoryTag(), requestedKeys[i].as())); } } std::unique_ptr GetConfiguration::createConf(){ Vector configurations = makeVector(getMemoryTag()); Vector unknownKeys = makeVector(getMemoryTag()); auto containers = getConfigurationContainersPublic(); if (keys.empty()) { //return all existing keys for (auto container : containers) { for (size_t i = 0; i < container->size(); i++) { if (!container->getConfiguration(i)->getKey()) { MO_DBG_ERR("invalid config"); continue; } if (!container->getConfiguration(i)->isReadable()) { continue; } configurations.push_back(container->getConfiguration(i)); } } } else { //only return keys that were searched using the "key" parameter for (auto& key : keys) { Configuration *res = nullptr; for (auto container : containers) { if ((res = container->getConfiguration(key.c_str()).get())) { break; } } if (res && res->isReadable()) { configurations.push_back(res); } else { unknownKeys.push_back(key.c_str()); } } } #define VALUE_BUFSIZE 30 //capacity of the resulting document size_t jcapacity = JSON_OBJECT_SIZE(2); //document root: configurationKey, unknownKey jcapacity += JSON_ARRAY_SIZE(configurations.size()) + configurations.size() * JSON_OBJECT_SIZE(3); //configurationKey: [{"key":...},{"key":...}] for (auto config : configurations) { //need to store ints by copied string: measure necessary capacity if (config->getType() == TConfig::Int) { char vbuf [VALUE_BUFSIZE]; auto ret = snprintf(vbuf, VALUE_BUFSIZE, "%i", config->getInt()); if (ret < 0 || ret >= VALUE_BUFSIZE) { continue; } jcapacity += ret + 1; } } jcapacity += JSON_ARRAY_SIZE(unknownKeys.size()); MO_DBG_DEBUG("GetConfiguration capacity: %zu", jcapacity); std::unique_ptr doc; if (jcapacity <= MO_MAX_JSON_CAPACITY) { doc = makeJsonDoc(getMemoryTag(), jcapacity); } if (!doc || doc->capacity() < jcapacity) { if (doc) { MO_DBG_ERR("OOM"); } errorCode = "InternalError"; errorDescription = "Query too big. Try fewer keys"; return nullptr; } JsonObject payload = doc->to(); JsonArray jsonConfigurationKey = payload.createNestedArray("configurationKey"); for (auto config : configurations) { char vbuf [VALUE_BUFSIZE]; const char *v = ""; switch (config->getType()) { case TConfig::Int: { auto ret = snprintf(vbuf, VALUE_BUFSIZE, "%i", config->getInt()); if (ret < 0 || ret >= VALUE_BUFSIZE) { MO_DBG_ERR("value error"); continue; } v = vbuf; break; } case TConfig::Bool: v = config->getBool() ? "true" : "false"; break; case TConfig::String: v = config->getString(); break; } JsonObject jconfig = jsonConfigurationKey.createNestedObject(); jconfig["key"] = config->getKey(); jconfig["readonly"] = config->isReadOnly(); if (v == vbuf) { //value points to buffer on stack, needs to be copied into JSON memory pool jconfig["value"] = (char*) v; } else { //value is static, no-copy mode jconfig["value"] = v; } } if (!unknownKeys.empty()) { JsonArray jsonUnknownKey = payload.createNestedArray("unknownKey"); for (auto key : unknownKeys) { MO_DBG_DEBUG("Unknown key: %s", key); jsonUnknownKey.add(key); } } return doc; } ================================================ FILE: src/MicroOcpp/Operations/GetConfiguration.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_GETCONFIGURATION_H #define MO_GETCONFIGURATION_H #include #include namespace MicroOcpp { namespace Ocpp16 { class GetConfiguration : public Operation, public MemoryManaged { private: Vector keys; const char *errorCode {nullptr}; const char *errorDescription = ""; public: GetConfiguration(); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} const char *getErrorDescription() override {return errorDescription;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/GetDiagnostics.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using MicroOcpp::Ocpp16::GetDiagnostics; using MicroOcpp::JsonDoc; GetDiagnostics::GetDiagnostics(DiagnosticsService& diagService) : MemoryManaged("v16.Operation.", "GetDiagnostics"), diagService(diagService), fileName(makeString(getMemoryTag())) { } void GetDiagnostics::processReq(JsonObject payload) { const char *location = payload["location"] | ""; //check location URL. Maybe introduce Same-Origin-Policy? if (!*location) { errorCode = "FormationViolation"; return; } int retries = payload["retries"] | 1; int retryInterval = payload["retryInterval"] | 180; if (retries < 0 || retryInterval < 0) { errorCode = "PropertyConstraintViolation"; return; } Timestamp startTime; if (payload.containsKey("startTime")) { if (!startTime.setTime(payload["startTime"] | "Invalid")) { errorCode = "PropertyConstraintViolation"; MO_DBG_WARN("bad time format"); return; } } Timestamp stopTime; if (payload.containsKey("stopTime")) { if (!stopTime.setTime(payload["stopTime"] | "Invalid")) { errorCode = "PropertyConstraintViolation"; MO_DBG_WARN("bad time format"); return; } } fileName = diagService.requestDiagnosticsUpload(location, (unsigned int) retries, (unsigned int) retryInterval, startTime, stopTime); } std::unique_ptr GetDiagnostics::createConf(){ if (fileName.empty()) { return createEmptyDocument(); } else { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["fileName"] = fileName.c_str(); return doc; } } ================================================ FILE: src/MicroOcpp/Operations/GetDiagnostics.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_GETDIAGNOSTICS_H #define MO_GETDIAGNOSTICS_H #include #include namespace MicroOcpp { class DiagnosticsService; namespace Ocpp16 { class GetDiagnostics : public Operation, public MemoryManaged { private: DiagnosticsService& diagService; String fileName; const char *errorCode = nullptr; public: GetDiagnostics(DiagnosticsService& diagService); const char* getOperationType() override {return "GetDiagnostics";} void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/GetInstalledCertificateIds.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_CERT_MGMT #include #include #include using MicroOcpp::Ocpp201::GetInstalledCertificateIds; using MicroOcpp::JsonDoc; GetInstalledCertificateIds::GetInstalledCertificateIds(CertificateService& certService) : MemoryManaged("v201.Operation.", "GetInstalledCertificateIds"), certService(certService), certificateHashDataChain(makeVector(getMemoryTag())) { } void GetInstalledCertificateIds::processReq(JsonObject payload) { if (!payload.containsKey("certificateType")) { errorCode = "FormationViolation"; return; } auto certificateType = makeVector(getMemoryTag()); for (const char *certificateTypeCstr : payload["certificateType"].as()) { if (!strcmp(certificateTypeCstr, "V2GRootCertificate")) { certificateType.push_back(GetCertificateIdType_V2GRootCertificate); } else if (!strcmp(certificateTypeCstr, "MORootCertificate")) { certificateType.push_back(GetCertificateIdType_MORootCertificate); } else if (!strcmp(certificateTypeCstr, "CSMSRootCertificate")) { certificateType.push_back(GetCertificateIdType_CSMSRootCertificate); } else if (!strcmp(certificateTypeCstr, "V2GCertificateChain")) { certificateType.push_back(GetCertificateIdType_V2GCertificateChain); } else if (!strcmp(certificateTypeCstr, "ManufacturerRootCertificate")) { certificateType.push_back(GetCertificateIdType_ManufacturerRootCertificate); } else { errorCode = "FormationViolation"; return; } } auto certStore = certService.getCertificateStore(); if (!certStore) { errorCode = "NotSupported"; return; } auto status = certStore->getCertificateIds(certificateType, certificateHashDataChain); switch (status) { case GetInstalledCertificateStatus_Accepted: this->status = "Accepted"; break; case GetInstalledCertificateStatus_NotFound: this->status = "NotFound"; break; default: MO_DBG_ERR("internal error"); errorCode = "InternalError"; return; } //operation executed successfully } std::unique_ptr GetInstalledCertificateIds::createConf() { size_t capacity = JSON_OBJECT_SIZE(2) + //payload root JSON_ARRAY_SIZE(certificateHashDataChain.size()); //array for field certificateHashDataChain for (auto& cch : certificateHashDataChain) { capacity += JSON_OBJECT_SIZE(2) + //certificateHashDataChain root JSON_OBJECT_SIZE(4) + //certificateHashData (2 * HashAlgorithmSize(cch.certificateHashData.hashAlgorithm) + //issuerNameHash and issuerKeyHash cch.certificateHashData.serialNumberLen) * 2 + 3; //issuerNameHash, issuerKeyHash and serialNumber as hex-endoded cstring } auto doc = makeJsonDoc(getMemoryTag(), capacity); JsonObject payload = doc->to(); payload["status"] = status; for (auto& chainElem : certificateHashDataChain) { JsonObject certHashJson = payload["certificateHashDataChain"].createNestedObject(); const char *certificateTypeCstr = ""; switch (chainElem.certificateType) { case GetCertificateIdType_V2GRootCertificate: certificateTypeCstr = "V2GRootCertificate"; break; case GetCertificateIdType_MORootCertificate: certificateTypeCstr = "MORootCertificate"; break; case GetCertificateIdType_CSMSRootCertificate: certificateTypeCstr = "CSMSRootCertificate"; break; case GetCertificateIdType_V2GCertificateChain: certificateTypeCstr = "V2GCertificateChain"; break; case GetCertificateIdType_ManufacturerRootCertificate: certificateTypeCstr = "ManufacturerRootCertificate"; break; } certHashJson["certificateType"] = (const char*) certificateTypeCstr; //use JSON zero-copy mode certHashJson["certificateHashData"]["hashAlgorithm"] = HashAlgorithmLabel(chainElem.certificateHashData.hashAlgorithm); char buf [MO_CERT_HASH_ISSUER_NAME_KEY_SIZE]; ocpp_cert_print_issuerNameHash(&chainElem.certificateHashData, buf, sizeof(buf)); certHashJson["certificateHashData"]["issuerNameHash"] = buf; ocpp_cert_print_issuerKeyHash(&chainElem.certificateHashData, buf, sizeof(buf)); certHashJson["certificateHashData"]["issuerKeyHash"] = buf; ocpp_cert_print_serialNumber(&chainElem.certificateHashData, buf, sizeof(buf)); certHashJson["certificateHashData"]["serialNumber"] = buf; if (!chainElem.childCertificateHashData.empty()) { MO_DBG_ERR("only sole root certs supported"); } } return doc; } #endif //MO_ENABLE_CERT_MGMT ================================================ FILE: src/MicroOcpp/Operations/GetInstalledCertificateIds.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_GETINSTALLEDCERTIFICATEIDS_H #define MO_GETINSTALLEDCERTIFICATEIDS_H #include #if MO_ENABLE_CERT_MGMT #include #include namespace MicroOcpp { class CertificateService; namespace Ocpp201 { class GetInstalledCertificateIds : public Operation, public MemoryManaged { private: CertificateService& certService; Vector certificateHashDataChain; const char *status = nullptr; const char *errorCode = nullptr; public: GetInstalledCertificateIds(CertificateService& certService); const char* getOperationType() override {return "GetInstalledCertificateIds";} void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp201 } //end namespace MicroOcpp #endif //MO_ENABLE_CERT_MGMT #endif ================================================ FILE: src/MicroOcpp/Operations/GetLocalListVersion.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_LOCAL_AUTH #include #include #include #include using MicroOcpp::Ocpp16::GetLocalListVersion; using MicroOcpp::JsonDoc; GetLocalListVersion::GetLocalListVersion(Model& model) : MemoryManaged("v16.Operation.", "GetLocalListVersion"), model(model) { } const char* GetLocalListVersion::getOperationType(){ return "GetLocalListVersion"; } void GetLocalListVersion::processReq(JsonObject payload) { //empty payload } std::unique_ptr GetLocalListVersion::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); auto authService = model.getAuthorizationService(); if (authService && authService->localAuthListEnabled()) { payload["listVersion"] = authService->getLocalListVersion(); } else { //TC_042_1_CS Get Local List Version (not supported) payload["listVersion"] = -1; } return doc; } #endif //MO_ENABLE_LOCAL_AUTH ================================================ FILE: src/MicroOcpp/Operations/GetLocalListVersion.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_GETLOCALLISTVERSION_H #define MO_GETLOCALLISTVERSION_H #include #if MO_ENABLE_LOCAL_AUTH #include namespace MicroOcpp { class Model; namespace Ocpp16 { class GetLocalListVersion : public Operation, public MemoryManaged { private: Model& model; public: GetLocalListVersion(Model& model); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif //MO_ENABLE_LOCAL_AUTH #endif ================================================ FILE: src/MicroOcpp/Operations/GetVariables.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include #include //for tolower using MicroOcpp::Ocpp201::GetVariableData; using MicroOcpp::Ocpp201::GetVariables; using MicroOcpp::JsonDoc; GetVariableData::GetVariableData(const char *memory_tag) : componentName{makeString(memory_tag)}, variableName{makeString(memory_tag)} { } GetVariables::GetVariables(VariableService& variableService) : MemoryManaged("v201.Operation.", "GetVariables"), variableService(variableService), queries(makeVector(getMemoryTag())) { } const char* GetVariables::getOperationType(){ return "GetVariables"; } void GetVariables::processReq(JsonObject payload) { for (JsonObject getVariable : payload["getVariableData"].as()) { queries.emplace_back(getMemoryTag()); auto& data = queries.back(); if (getVariable.containsKey("attributeType")) { const char *attributeTypeCstr = getVariable["attributeType"] | "_Undefined"; if (!strcmp(attributeTypeCstr, "Actual")) { data.attributeType = Variable::AttributeType::Actual; } else if (!strcmp(attributeTypeCstr, "Target")) { data.attributeType = Variable::AttributeType::Target; } else if (!strcmp(attributeTypeCstr, "MinSet")) { data.attributeType = Variable::AttributeType::MinSet; } else if (!strcmp(attributeTypeCstr, "MaxSet")) { data.attributeType = Variable::AttributeType::MaxSet; } else { errorCode = "FormationViolation"; MO_DBG_ERR("invalid attributeType"); return; } } const char *componentNameCstr = getVariable["component"]["name"] | (const char*) nullptr; const char *variableNameCstr = getVariable["variable"]["name"] | (const char*) nullptr; if (!componentNameCstr || !variableNameCstr) { errorCode = "FormationViolation"; return; } data.componentName = componentNameCstr; data.variableName = variableNameCstr; // TODO check against ConfigurationValueSize data.componentEvseId = getVariable["component"]["evse"]["id"] | -1; data.componentEvseConnectorId = getVariable["component"]["evse"]["connectorId"] | -1; if (getVariable["component"].containsKey("evse") && data.componentEvseId < 0) { errorCode = "FormationViolation"; MO_DBG_ERR("malformatted / missing evseId"); return; } } if (queries.empty()) { errorCode = "FormationViolation"; return; } } std::unique_ptr GetVariables::createConf(){ // process GetVariables queries for (auto& query : queries) { query.attributeStatus = variableService.getVariable( query.attributeType, ComponentId(query.componentName.c_str(), EvseId(query.componentEvseId, query.componentEvseConnectorId)), query.variableName.c_str(), &query.variable); } #define VALUE_BUFSIZE 30 // for primitives (int) size_t capacity = JSON_ARRAY_SIZE(queries.size()); for (const auto& data : queries) { size_t valueCapacity = 0; if (data.variable) { switch (data.variable->getInternalDataType()) { case Variable::InternalDataType::Int: { // measure int size by printing to a dummy buf char valbuf [VALUE_BUFSIZE]; auto ret = snprintf(valbuf, VALUE_BUFSIZE, "%i", data.variable->getInt()); if (ret < 0 || ret >= VALUE_BUFSIZE) { continue; } valueCapacity = (size_t) ret + 1; break; } case Variable::InternalDataType::Bool: // bool will be stored in zero-copy mode (string literal "true" or "false") valueCapacity = 0; break; case Variable::InternalDataType::String: valueCapacity = strlen(data.variable->getString()); // TODO limit by ReportingValueSize break; default: MO_DBG_ERR("internal error"); break; } } capacity += JSON_OBJECT_SIZE(5) + // getVariableResult valueCapacity + // capacity needed for storing the value JSON_OBJECT_SIZE(2) + // component data.componentName.length() + 1 + JSON_OBJECT_SIZE(2) + // evse JSON_OBJECT_SIZE(2) + // variable data.variableName.length() + 1; } auto doc = makeJsonDoc(getMemoryTag(), capacity); JsonObject payload = doc->to(); JsonArray getVariableResult = payload.createNestedArray("getVariableResult"); for (const auto& data : queries) { JsonObject getVariable = getVariableResult.createNestedObject(); const char *attributeStatusCstr = "Rejected"; switch (data.attributeStatus) { case GetVariableStatus::Accepted: attributeStatusCstr = "Accepted"; break; case GetVariableStatus::Rejected: attributeStatusCstr = "Rejected"; break; case GetVariableStatus::UnknownComponent: attributeStatusCstr = "UnknownComponent"; break; case GetVariableStatus::UnknownVariable: attributeStatusCstr = "UnknownVariable"; break; case GetVariableStatus::NotSupportedAttributeType: attributeStatusCstr = "NotSupportedAttributeType"; break; default: MO_DBG_ERR("internal error"); break; } getVariable["attributeStatus"] = attributeStatusCstr; const char *attributeTypeCstr = nullptr; switch (data.attributeType) { case Variable::AttributeType::Actual: // leave blank when Actual break; case Variable::AttributeType::Target: attributeTypeCstr = "Target"; break; case Variable::AttributeType::MinSet: attributeTypeCstr = "MinSet"; break; case Variable::AttributeType::MaxSet: attributeTypeCstr = "MaxSet"; break; default: MO_DBG_ERR("internal error"); break; } if (attributeTypeCstr) { getVariable["attributeType"] = attributeTypeCstr; } if (data.variable) { switch (data.variable->getInternalDataType()) { case Variable::InternalDataType::Int: { char valbuf [VALUE_BUFSIZE]; auto ret = snprintf(valbuf, VALUE_BUFSIZE, "%i", data.variable->getInt()); if (ret < 0 || ret >= VALUE_BUFSIZE) { break; } getVariable["attributeValue"] = valbuf; break; } case Variable::InternalDataType::Bool: getVariable["attributeValue"] = data.variable->getBool() ? "true" : "false"; break; case Variable::InternalDataType::String: getVariable["attributeValue"] = (char*) data.variable->getString(); // force zero-copy mode break; default: MO_DBG_ERR("internal error"); break; } } getVariable["component"]["name"] = (char*) data.componentName.c_str(); // force copy-mode if (data.componentEvseId >= 0) { getVariable["component"]["evse"]["id"] = data.componentEvseId; } if (data.componentEvseConnectorId >= 0) { getVariable["component"]["evse"]["connectorId"] = data.componentEvseConnectorId; } getVariable["variable"]["name"] = (char*) data.variableName.c_str(); // force copy-mode } return doc; } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/GetVariables.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_GETVARIABLES_H #define MO_GETVARIABLES_H #include #if MO_ENABLE_V201 #include #include #include namespace MicroOcpp { class VariableService; namespace Ocpp201 { // GetVariableDataType (2.25) and // GetVariableResultType (2.26) struct GetVariableData { // GetVariableDataType Variable::AttributeType attributeType = Variable::AttributeType::Actual; String componentName; int componentEvseId = -1; int componentEvseConnectorId = -1; String variableName; // GetVariableResultType GetVariableStatus attributeStatus; Variable *variable = nullptr; GetVariableData(const char *memory_tag = nullptr); }; class GetVariables : public Operation, public MemoryManaged { private: VariableService& variableService; Vector queries; const char *errorCode = nullptr; public: GetVariables(VariableService& variableService); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //namespace Ocpp201 } //namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/Heartbeat.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using MicroOcpp::Ocpp16::Heartbeat; using MicroOcpp::JsonDoc; Heartbeat::Heartbeat(Model& model) : MemoryManaged("v16.Operation.", "Heartbeat"), model(model) { } const char* Heartbeat::getOperationType(){ return "Heartbeat"; } std::unique_ptr Heartbeat::createReq() { return createEmptyDocument(); } void Heartbeat::processConf(JsonObject payload) { const char* currentTime = payload["currentTime"] | "Invalid"; if (strcmp(currentTime, "Invalid")) { if (model.getClock().setTime(currentTime)) { //success MO_DBG_DEBUG("Request has been accepted"); } else { MO_DBG_WARN("Could not read time string. Expect format like 2020-02-01T20:53:32.486Z"); } } else { MO_DBG_WARN("Missing field currentTime. Expect format like 2020-02-01T20:53:32.486Z"); } } void Heartbeat::processReq(JsonObject payload) { /** * Ignore Contents of this Req-message, because this is for debug purposes only */ } std::unique_ptr Heartbeat::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + (JSONDATE_LENGTH + 1)); JsonObject payload = doc->to(); //safety mechanism; in some test setups the library could have to answer Heartbeats without valid system time Timestamp ocppTimeReference = Timestamp(2019,10,0,11,59,55); Timestamp ocppSelect = ocppTimeReference; auto& ocppNow = model.getClock().now(); if (ocppNow > ocppTimeReference) { //time has already been set ocppSelect = ocppNow; } char ocppNowJson [JSONDATE_LENGTH + 1] = {'\0'}; ocppSelect.toJsonString(ocppNowJson, JSONDATE_LENGTH + 1); payload["currentTime"] = ocppNowJson; return doc; } ================================================ FILE: src/MicroOcpp/Operations/Heartbeat.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_HEARTBEAT_H #define MO_HEARTBEAT_H #include namespace MicroOcpp { class Model; namespace Ocpp16 { class Heartbeat : public Operation, public MemoryManaged { private: Model& model; public: Heartbeat(Model& model); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/InstallCertificate.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_CERT_MGMT #include #include using MicroOcpp::Ocpp201::InstallCertificate; using MicroOcpp::JsonDoc; InstallCertificate::InstallCertificate(CertificateService& certService) : MemoryManaged("v201.Operation.", "InstallCertificate"), certService(certService) { } void InstallCertificate::processReq(JsonObject payload) { if (!payload.containsKey("certificateType") || !payload.containsKey("certificate")) { errorCode = "FormationViolation"; return; } InstallCertificateType certificateType; const char *certificateTypeCstr = payload["certificateType"] | "_Invalid"; if (!strcmp(certificateTypeCstr, "V2GRootCertificate")) { certificateType = InstallCertificateType_V2GRootCertificate; } else if (!strcmp(certificateTypeCstr, "MORootCertificate")) { certificateType = InstallCertificateType_MORootCertificate; } else if (!strcmp(certificateTypeCstr, "CSMSRootCertificate")) { certificateType = InstallCertificateType_CSMSRootCertificate; } else if (!strcmp(certificateTypeCstr, "ManufacturerRootCertificate")) { certificateType = InstallCertificateType_ManufacturerRootCertificate; } else { errorCode = "FormationViolation"; return; } if (!payload["certificate"].is()) { errorCode = "FormationViolation"; return; } const char *certificate = payload["certificate"]; auto certStore = certService.getCertificateStore(); if (!certStore) { errorCode = "NotSupported"; return; } auto status = certStore->installCertificate(certificateType, certificate); switch (status) { case InstallCertificateStatus_Accepted: this->status = "Accepted"; break; case InstallCertificateStatus_Rejected: this->status = "Rejected"; break; case InstallCertificateStatus_Failed: this->status = "Failed"; break; default: MO_DBG_ERR("internal error"); errorCode = "InternalError"; return; } //operation executed successfully } std::unique_ptr InstallCertificate::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = status; return doc; } #endif //MO_ENABLE_CERT_MGMT ================================================ FILE: src/MicroOcpp/Operations/InstallCertificate.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_INSTALLCERTIFICATE_H #define MO_INSTALLCERTIFICATE_H #include #if MO_ENABLE_CERT_MGMT #include namespace MicroOcpp { class CertificateService; namespace Ocpp201 { class InstallCertificate : public Operation, public MemoryManaged { private: CertificateService& certService; const char *status = nullptr; const char *errorCode = nullptr; public: InstallCertificate(CertificateService& certService); const char* getOperationType() override {return "InstallCertificate";} void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp201 } //end namespace MicroOcpp #endif //MO_ENABLE_CERT_MGMT #endif ================================================ FILE: src/MicroOcpp/Operations/MeterValues.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include using MicroOcpp::Ocpp16::MeterValues; using MicroOcpp::JsonDoc; //can only be used for echo server debugging MeterValues::MeterValues(Model& model) : MemoryManaged("v16.Operation.", "MeterValues"), model(model) { } MeterValues::MeterValues(Model& model, MeterValue *meterValue, unsigned int connectorId, std::shared_ptr transaction) : MemoryManaged("v16.Operation.", "MeterValues"), model(model), meterValue{meterValue}, connectorId{connectorId}, transaction{transaction} { } MeterValues::MeterValues(Model& model, std::unique_ptr meterValue, unsigned int connectorId, std::shared_ptr transaction) : MeterValues(model, meterValue.get(), connectorId, transaction) { this->meterValueOwnership = std::move(meterValue); } MeterValues::~MeterValues(){ } const char* MeterValues::getOperationType(){ return "MeterValues"; } std::unique_ptr MeterValues::createReq() { size_t capacity = 0; std::unique_ptr meterValueJson; if (meterValue) { if (meterValue->getTimestamp() < MIN_TIME) { MO_DBG_DEBUG("adjust preboot MeterValue timestamp"); Timestamp adjusted = model.getClock().adjustPrebootTimestamp(meterValue->getTimestamp()); meterValue->setTimestamp(adjusted); } meterValueJson = meterValue->toJson(); if (meterValueJson) { capacity += meterValueJson->capacity(); } else { MO_DBG_ERR("Energy meter reading not convertible to JSON"); } } capacity += JSON_OBJECT_SIZE(3); capacity += JSON_ARRAY_SIZE(1); auto doc = makeJsonDoc(getMemoryTag(), capacity); auto payload = doc->to(); payload["connectorId"] = connectorId; if (transaction && transaction->getTransactionId() > 0) { //add txId if MVs are assigned to a tx with txId payload["transactionId"] = transaction->getTransactionId(); } auto meterValueArray = payload.createNestedArray("meterValue"); if (meterValueJson) { meterValueArray.add(*meterValueJson); } return doc; } void MeterValues::processConf(JsonObject payload) { MO_DBG_DEBUG("Request has been confirmed"); } void MeterValues::processReq(JsonObject payload) { /** * Ignore Contents of this Req-message, because this is for debug purposes only */ } std::unique_ptr MeterValues::createConf(){ return createEmptyDocument(); } ================================================ FILE: src/MicroOcpp/Operations/MeterValues.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_METERVALUES_H #define MO_METERVALUES_H #include #include #include namespace MicroOcpp { class Model; class MeterValue; class Transaction; namespace Ocpp16 { class MeterValues : public Operation, public MemoryManaged { private: Model& model; //for adjusting the timestamp if MeterValue has been created before BootNotification MeterValue *meterValue = nullptr; std::unique_ptr meterValueOwnership; unsigned int connectorId = 0; std::shared_ptr transaction; public: MeterValues(Model& model, MeterValue *meterValue, unsigned int connectorId, std::shared_ptr transaction = nullptr); MeterValues(Model& model, std::unique_ptr meterValue, unsigned int connectorId, std::shared_ptr transaction = nullptr); MeterValues(Model& model); //for debugging only. Make this for the server pendant ~MeterValues(); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/NotifyReport.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include #include using namespace MicroOcpp::Ocpp201; using MicroOcpp::JsonDoc; NotifyReport::NotifyReport(Model& model, int requestId, const Timestamp& generatedAt, bool tbc, int seqNo, const Vector& reportData) : MemoryManaged("v201.Operation.", "NotifyReport"), model(model), requestId(requestId), generatedAt(generatedAt), tbc(tbc), seqNo(seqNo), reportData(reportData) { } const char* NotifyReport::getOperationType() { return "NotifyReport"; } std::unique_ptr NotifyReport::createReq() { #define VALUE_BUFSIZE 30 // for primitives (int) const Variable::AttributeType enumerateAttributeTypes [] = { Variable::AttributeType::Actual, Variable::AttributeType::Target, Variable::AttributeType::MinSet, Variable::AttributeType::MaxSet }; size_t capacity = JSON_OBJECT_SIZE(5) + //total of 5 fields JSONDATE_LENGTH + 1; //timestamp string capacity += JSON_ARRAY_SIZE(reportData.size()); for (auto variable : reportData) { capacity += JSON_OBJECT_SIZE(4); //total of 4 fields capacity += 2 * JSON_OBJECT_SIZE(2); //component composite capacity += JSON_OBJECT_SIZE(1); //variable composite size_t nAttributes = 0; size_t valueCapacity = 0; for (auto attributeType : enumerateAttributeTypes) { if (!variable->hasAttribute(attributeType)) { continue; } nAttributes++; switch (variable->getInternalDataType()) { case Variable::InternalDataType::Int: { // measure int size by printing to a dummy buf char valbuf [VALUE_BUFSIZE]; auto ret = snprintf(valbuf, VALUE_BUFSIZE, "%i", variable->getInt()); if (ret < 0 || ret >= VALUE_BUFSIZE) { continue; } valueCapacity = (size_t) ret + 1; break; } case Variable::InternalDataType::Bool: // bool will be stored in zero-copy mode (string literal "true" or "false") valueCapacity = 0; break; case Variable::InternalDataType::String: valueCapacity = strlen(variable->getString()); // TODO limit by ReportingValueSize break; default: MO_DBG_ERR("internal error"); break; } } capacity += nAttributes * JSON_OBJECT_SIZE(5); //variableAttribute composite capacity += valueCapacity; //variableAttribute value total size capacity += JSON_OBJECT_SIZE(2); //variableCharacteristics composite: only send two data fields } auto doc = makeJsonDoc(getMemoryTag(), capacity); JsonObject payload = doc->to(); payload["requestId"] = requestId; char generatedAtCstr [JSONDATE_LENGTH + 1]; generatedAt.toJsonString(generatedAtCstr, sizeof(generatedAtCstr)); payload["generatedAt"] = generatedAtCstr; if (tbc) { payload["tbc"] = true; } payload["seqNo"] = seqNo; JsonArray reportDataJsonArray = payload.createNestedArray("reportData"); for (auto variable : reportData) { JsonObject reportDataJson = reportDataJsonArray.createNestedObject(); reportDataJson["component"]["name"] = (char*) variable->getComponentId().name; // force copy-mode if (variable->getComponentId().evse.id >= 0) { reportDataJson["component"]["evse"]["id"] = variable->getComponentId().evse.id; } if (variable->getComponentId().evse.connectorId >= 0) { reportDataJson["component"]["evse"]["connectorId"] = variable->getComponentId().evse.connectorId; } reportDataJson["variable"]["name"] = (char*) variable->getName(); // force copy-mode JsonArray variableAttribute = reportDataJson.createNestedArray("variableAttribute"); for (auto attributeType : enumerateAttributeTypes) { if (!variable->hasAttribute(attributeType)) { continue; } JsonObject attribute = variableAttribute.createNestedObject(); const char *attributeTypeCstr = nullptr; switch (attributeType) { case Variable::AttributeType::Actual: // leave blank when Actual break; case Variable::AttributeType::Target: attributeTypeCstr = "Target"; break; case Variable::AttributeType::MinSet: attributeTypeCstr = "MinSet"; break; case Variable::AttributeType::MaxSet: attributeTypeCstr = "MaxSet"; break; default: MO_DBG_ERR("internal error"); break; } if (attributeTypeCstr) { attribute["type"] = attributeTypeCstr; } if (variable->getMutability() != Variable::Mutability::WriteOnly) { switch (variable->getInternalDataType()) { case Variable::InternalDataType::Int: { char valbuf [VALUE_BUFSIZE]; auto ret = snprintf(valbuf, VALUE_BUFSIZE, "%i", variable->getInt()); if (ret < 0 || ret >= VALUE_BUFSIZE) { break; } attribute["value"] = valbuf; break; } case Variable::InternalDataType::Bool: attribute["value"] = variable->getBool() ? "true" : "false"; break; case Variable::InternalDataType::String: attribute["value"] = (char*) variable->getString(); // force zero-copy mode break; default: MO_DBG_ERR("internal error"); break; } } const char *mutabilityCstr = nullptr; switch (variable->getMutability()) { case Variable::Mutability::ReadOnly: mutabilityCstr = "ReadOnly"; break; case Variable::Mutability::WriteOnly: mutabilityCstr = "WriteOnly"; break; case Variable::Mutability::ReadWrite: // leave blank when ReadWrite break; default: MO_DBG_ERR("internal error"); break; } if (mutabilityCstr) { attribute["mutability"] = mutabilityCstr; } if (variable->isPersistent()) { attribute["persistent"] = true; } if (variable->isConstant()) { attribute["constant"] = true; } } JsonObject variableCharacteristics = reportDataJson.createNestedObject("variableCharacteristics"); const char *dataTypeCstr = ""; switch (variable->getVariableDataType()) { case VariableCharacteristics::DataType::string: dataTypeCstr = "string"; break; case VariableCharacteristics::DataType::decimal: dataTypeCstr = "decimal"; break; case VariableCharacteristics::DataType::integer: dataTypeCstr = "integer"; break; case VariableCharacteristics::DataType::dateTime: dataTypeCstr = "dateTime"; break; case VariableCharacteristics::DataType::boolean: dataTypeCstr = "boolean"; break; case VariableCharacteristics::DataType::OptionList: dataTypeCstr = "OptionList"; break; case VariableCharacteristics::DataType::SequenceList: dataTypeCstr = "SequenceList"; break; case VariableCharacteristics::DataType::MemberList: dataTypeCstr = "MemberList"; break; default: MO_DBG_ERR("internal error"); break; } variableCharacteristics["dataType"] = dataTypeCstr; variableCharacteristics["supportsMonitoring"] = variable->getSupportsMonitoring(); } return doc; } void NotifyReport::processConf(JsonObject payload) { // empty payload } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/NotifyReport.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_TRANSACTIONEVENT_H #define MO_TRANSACTIONEVENT_H #include #if MO_ENABLE_V201 #include #include #include namespace MicroOcpp { class Model; class Variable; namespace Ocpp201 { class NotifyReport : public Operation, public MemoryManaged { private: Model& model; int requestId; Timestamp generatedAt; bool tbc; int seqNo; Vector reportData; public: NotifyReport(Model& model, int requestId, const Timestamp& generatedAt, bool tbc, int seqNo, const Vector& reportData); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; }; } //end namespace Ocpp201 } //end namespace MicroOcpp #endif // MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/RemoteStartTransaction.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include using MicroOcpp::Ocpp16::RemoteStartTransaction; using MicroOcpp::JsonDoc; RemoteStartTransaction::RemoteStartTransaction(Model& model) : MemoryManaged("v16.Operation.", "RemoteStartTransaction"), model(model) { } const char* RemoteStartTransaction::getOperationType() { return "RemoteStartTransaction"; } void RemoteStartTransaction::processReq(JsonObject payload) { int connectorId = payload["connectorId"] | -1; // OCPP 1.6 specification: connectorId SHALL be > 0 (TC_027_CS) if (connectorId == 0) { MO_DBG_INFO("RemoteStartTransaction rejected: connectorId SHALL not be 0"); accepted = false; return; } if (!payload.containsKey("idTag")) { errorCode = "FormationViolation"; return; } const char *idTag = payload["idTag"] | ""; size_t len = strnlen(idTag, IDTAG_LEN_MAX + 1); if (len == 0 || len > IDTAG_LEN_MAX) { errorCode = "PropertyConstraintViolation"; errorDescription = "idTag empty or too long"; return; } std::unique_ptr chargingProfile; if (payload.containsKey("chargingProfile") && model.getSmartChargingService()) { MO_DBG_INFO("Setting Charging profile via RemoteStartTransaction"); JsonObject chargingProfileJson = payload["chargingProfile"]; chargingProfile = loadChargingProfile(chargingProfileJson); if (!chargingProfile) { errorCode = "PropertyConstraintViolation"; errorDescription = "chargingProfile validation failed"; return; } if (chargingProfile->getChargingProfilePurpose() != ChargingProfilePurposeType::TxProfile) { errorCode = "PropertyConstraintViolation"; errorDescription = "Can only set TxProfile here"; return; } if (chargingProfile->getChargingProfileId() < 0) { errorCode = "PropertyConstraintViolation"; errorDescription = "RemoteStartTx profile requires non-negative chargingProfileId"; return; } } Connector *selectConnector = nullptr; if (connectorId >= 1) { //connectorId specified for given connector, try to start Transaction here if (auto connector = model.getConnector(connectorId)){ if (!connector->getTransaction() && connector->isOperative()) { selectConnector = connector; } } } else { //connectorId not specified. Find free connector for (unsigned int cid = 1; cid < model.getNumConnectors(); cid++) { auto connector = model.getConnector(cid); if (!connector->getTransaction() && connector->isOperative()) { selectConnector = connector; connectorId = cid; break; } } } if (selectConnector) { bool success = true; int chargingProfileId = -1; //keep Id after moving charging profile to SCService if (chargingProfile && model.getSmartChargingService()) { chargingProfileId = chargingProfile->getChargingProfileId(); success = model.getSmartChargingService()->setChargingProfile(connectorId, std::move(chargingProfile)); } if (success) { std::shared_ptr tx; auto authorizeRemoteTxRequests = declareConfiguration("AuthorizeRemoteTxRequests", false); if (authorizeRemoteTxRequests && authorizeRemoteTxRequests->getBool()) { tx = selectConnector->beginTransaction(idTag); } else { tx = selectConnector->beginTransaction_authorized(idTag); } selectConnector->updateTxNotification(TxNotification_RemoteStart); if (tx) { if (chargingProfileId >= 0) { tx->setTxProfileId(chargingProfileId); } } else { success = false; } } accepted = success; } else { MO_DBG_INFO("No connector to start transaction"); accepted = false; } } std::unique_ptr RemoteStartTransaction::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (accepted) { payload["status"] = "Accepted"; } else { payload["status"] = "Rejected"; } return doc; } std::unique_ptr RemoteStartTransaction::createReq() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["idTag"] = "A0000000"; return doc; } void RemoteStartTransaction::processConf(JsonObject payload){ } ================================================ FILE: src/MicroOcpp/Operations/RemoteStartTransaction.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_REMOTESTARTTRANSACTION_H #define MO_REMOTESTARTTRANSACTION_H #include #include namespace MicroOcpp { class Model; class ChargingProfile; namespace Ocpp16 { class RemoteStartTransaction : public Operation, public MemoryManaged { private: Model& model; bool accepted = false; const char *errorCode {nullptr}; const char *errorDescription = ""; public: RemoteStartTransaction(Model& model); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} const char *getErrorDescription() override {return errorDescription;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/RemoteStopTransaction.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using MicroOcpp::Ocpp16::RemoteStopTransaction; using MicroOcpp::JsonDoc; RemoteStopTransaction::RemoteStopTransaction(Model& model) : MemoryManaged("v16.Operation.", "RemoteStopTransaction"), model(model) { } const char* RemoteStopTransaction::getOperationType(){ return "RemoteStopTransaction"; } void RemoteStopTransaction::processReq(JsonObject payload) { if (!payload.containsKey("transactionId")) { errorCode = "FormationViolation"; } int transactionId = payload["transactionId"]; for (unsigned int cId = 0; cId < model.getNumConnectors(); cId++) { auto connector = model.getConnector(cId); if (connector->getTransaction() && connector->getTransaction()->getTransactionId() == transactionId) { connector->endTransaction(nullptr, "Remote"); accepted = true; } } } std::unique_ptr RemoteStopTransaction::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (accepted){ payload["status"] = "Accepted"; } else { payload["status"] = "Rejected"; } return doc; } ================================================ FILE: src/MicroOcpp/Operations/RemoteStopTransaction.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_REMOTESTOPTRANSACTION_H #define MO_REMOTESTOPTRANSACTION_H #include namespace MicroOcpp { class Model; namespace Ocpp16 { class RemoteStopTransaction : public Operation, public MemoryManaged { private: Model& model; bool accepted = false; const char *errorCode = nullptr; public: RemoteStopTransaction(Model& model); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/RequestStartTransaction.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include using MicroOcpp::Ocpp201::RequestStartTransaction; using MicroOcpp::JsonDoc; RequestStartTransaction::RequestStartTransaction(RemoteControlService& rcService) : MemoryManaged("v201.Operation.", "RequestStartTransaction"), rcService(rcService) { } const char* RequestStartTransaction::getOperationType(){ return "RequestStartTransaction"; } void RequestStartTransaction::processReq(JsonObject payload) { int evseId = payload["evseId"] | 0; if (evseId < 0 || evseId >= MO_NUM_EVSEID) { errorCode = "PropertyConstraintViolation"; return; } int remoteStartId = payload["remoteStartId"] | 0; if (remoteStartId < 0) { errorCode = "PropertyConstraintViolation"; MO_DBG_ERR("IDs must be >= 0"); return; } IdToken idToken; if (!idToken.parseCstr(payload["idToken"]["idToken"] | (const char*)nullptr, payload["idToken"]["type"] | (const char*)nullptr)) { //parseCstr rejects nullptr as argument MO_DBG_ERR("could not parse idToken"); errorCode = "FormationViolation"; return; } status = rcService.requestStartTransaction(evseId, remoteStartId, idToken, transactionId, sizeof(transactionId)); } std::unique_ptr RequestStartTransaction::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2)); JsonObject payload = doc->to(); const char *statusCstr = ""; switch (status) { case RequestStartStopStatus_Accepted: statusCstr = "Accepted"; break; case RequestStartStopStatus_Rejected: statusCstr = "Rejected"; break; default: MO_DBG_ERR("internal error"); break; } payload["status"] = statusCstr; if (transaction) { payload["transactionId"] = (const char*)transaction->transactionId; //force zero-copy mode } return doc; } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/RequestStartTransaction.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_REQUESTSTARTTRANSACTION_H #define MO_REQUESTSTARTTRANSACTION_H #include #if MO_ENABLE_V201 #include #include #include #include namespace MicroOcpp { class RemoteControlService; namespace Ocpp201 { class RequestStartTransaction : public Operation, public MemoryManaged { private: RemoteControlService& rcService; RequestStartStopStatus status; std::shared_ptr transaction; char transactionId [MO_TXID_LEN_MAX + 1] = {'\0'}; const char *errorCode = nullptr; public: RequestStartTransaction(RemoteControlService& rcService); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //namespace Ocpp201 } //namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/RequestStopTransaction.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include using MicroOcpp::Ocpp201::RequestStopTransaction; using MicroOcpp::JsonDoc; RequestStopTransaction::RequestStopTransaction(RemoteControlService& rcService) : MemoryManaged("v201.Operation.", "RequestStopTransaction"), rcService(rcService) { } const char* RequestStopTransaction::getOperationType(){ return "RequestStopTransaction"; } void RequestStopTransaction::processReq(JsonObject payload) { if (!payload.containsKey("transactionId") || !payload["transactionId"].is() || strlen(payload["transactionId"].as()) > MO_TXID_LEN_MAX) { errorCode = "FormationViolation"; return; } status = rcService.requestStopTransaction(payload["transactionId"].as()); } std::unique_ptr RequestStopTransaction::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); const char *statusCstr = ""; switch (status) { case RequestStartStopStatus_Accepted: statusCstr = "Accepted"; break; case RequestStartStopStatus_Rejected: statusCstr = "Rejected"; break; default: MO_DBG_ERR("internal error"); break; } payload["status"] = statusCstr; return doc; } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/RequestStopTransaction.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_REQUESTSTOPTRANSACTION_H #define MO_REQUESTSTOPTRANSACTION_H #include #if MO_ENABLE_V201 #include #include #include namespace MicroOcpp { class RemoteControlService; namespace Ocpp201 { class RequestStopTransaction : public Operation, public MemoryManaged { private: RemoteControlService& rcService; RequestStartStopStatus status; const char *errorCode = nullptr; public: RequestStopTransaction(RemoteControlService& rcService); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //namespace Ocpp201 } //namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/ReserveNow.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_RESERVATION #include #include #include #include #include #include using MicroOcpp::Ocpp16::ReserveNow; using MicroOcpp::JsonDoc; ReserveNow::ReserveNow(Model& model) : MemoryManaged("v16.Operation.", "ReserveNow"), model(model) { } ReserveNow::~ReserveNow() { } const char* ReserveNow::getOperationType(){ return "ReserveNow"; } void ReserveNow::processReq(JsonObject payload) { if (!payload.containsKey("connectorId") || payload["connectorId"] < 0 || !payload.containsKey("expiryDate") || !payload.containsKey("idTag") || //parentIdTag is optional !payload.containsKey("reservationId")) { errorCode = "FormationViolation"; return; } int connectorId = payload["connectorId"] | -1; if (connectorId < 0 || (unsigned int) connectorId >= model.getNumConnectors()) { errorCode = "PropertyConstraintViolation"; return; } Timestamp expiryDate; if (!expiryDate.setTime(payload["expiryDate"])) { MO_DBG_WARN("bad time format"); errorCode = "PropertyConstraintViolation"; return; } const char *idTag = payload["idTag"] | ""; if (!*idTag) { errorCode = "PropertyConstraintViolation"; return; } const char *parentIdTag = nullptr; if (payload.containsKey("parentIdTag")) { parentIdTag = payload["parentIdTag"]; } int reservationId = payload["reservationId"] | -1; if (model.getReservationService() && model.getNumConnectors() > 0) { auto rService = model.getReservationService(); auto chargePoint = model.getConnector(0); auto reserveConnectorZeroSupportedBool = declareConfiguration("ReserveConnectorZeroSupported", true, CONFIGURATION_VOLATILE); if (connectorId == 0 && (!reserveConnectorZeroSupportedBool || !reserveConnectorZeroSupportedBool->getBool())) { reservationStatus = "Rejected"; return; } if (auto reservation = rService->getReservationById(reservationId)) { reservation->update(reservationId, (unsigned int) connectorId, expiryDate, idTag, parentIdTag); reservationStatus = "Accepted"; return; } Connector *connector = nullptr; if (connectorId > 0) { connector = model.getConnector((unsigned int) connectorId); } if (chargePoint->getStatus() == ChargePointStatus_Faulted || (connector && connector->getStatus() == ChargePointStatus_Faulted)) { reservationStatus = "Faulted"; return; } if (chargePoint->getStatus() == ChargePointStatus_Unavailable || (connector && connector->getStatus() == ChargePointStatus_Unavailable)) { reservationStatus = "Unavailable"; return; } if (connector && connector->getStatus() != ChargePointStatus_Available) { reservationStatus = "Occupied"; return; } if (rService->updateReservation(reservationId, (unsigned int) connectorId, expiryDate, idTag, parentIdTag)) { reservationStatus = "Accepted"; } else { reservationStatus = "Occupied"; } } else { errorCode = "InternalError"; } } std::unique_ptr ReserveNow::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (reservationStatus) { payload["status"] = reservationStatus; } else { MO_DBG_ERR("didn't set reservationStatus"); payload["status"] = "Rejected"; } return doc; } #endif //MO_ENABLE_RESERVATION ================================================ FILE: src/MicroOcpp/Operations/ReserveNow.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_RESERVENOW_H #define MO_RESERVENOW_H #include #if MO_ENABLE_RESERVATION #include namespace MicroOcpp { class Model; namespace Ocpp16 { class ReserveNow : public Operation, public MemoryManaged { private: Model& model; const char *errorCode = nullptr; const char *reservationStatus = nullptr; public: ReserveNow(Model& model); ~ReserveNow(); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif //MO_ENABLE_RESERVATION #endif ================================================ FILE: src/MicroOcpp/Operations/Reset.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using MicroOcpp::Ocpp16::Reset; using MicroOcpp::JsonDoc; Reset::Reset(Model& model) : MemoryManaged("v16.Operation.", "Reset"), model(model) { } const char* Reset::getOperationType(){ return "Reset"; } void Reset::processReq(JsonObject payload) { /* * Process the application data here. Note: you have to implement the device reset procedure in your client code. You have to set * a onSendConfListener in which you initiate a reset (e.g. calling ESP.reset() ) */ bool isHard = !strcmp(payload["type"] | "undefined", "Hard"); if (auto rService = model.getResetService()) { if (!rService->getExecuteReset()) { MO_DBG_ERR("No reset handler set. Abort operation"); //resetAccepted remains false } else { if (!rService->getPreReset() || rService->getPreReset()(isHard) || isHard) { resetAccepted = true; rService->initiateReset(isHard); for (unsigned int cId = 0; cId < model.getNumConnectors(); cId++) { model.getConnector(cId)->endTransaction(nullptr, isHard ? "HardReset" : "SoftReset"); } } } } else { resetAccepted = true; //assume that onReceiveReset is set } } std::unique_ptr Reset::createConf() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = resetAccepted ? "Accepted" : "Rejected"; return doc; } #if MO_ENABLE_V201 #include namespace MicroOcpp { namespace Ocpp201 { Reset::Reset(ResetService& resetService) : MemoryManaged("v201.Operation.", "Reset"), resetService(resetService) { } const char* Reset::getOperationType(){ return "Reset"; } void Reset::processReq(JsonObject payload) { ResetType type; const char *typeCstr = payload["type"] | "_Undefined"; if (!strcmp(typeCstr, "Immediate")) { type = ResetType_Immediate; } else if (!strcmp(typeCstr, "OnIdle")) { type = ResetType_OnIdle; } else { errorCode = "FormationViolation"; return; } int evseIdRaw = payload["evseId"] | 0; if (evseIdRaw < 0 || evseIdRaw >= MO_NUM_EVSEID) { errorCode = "PropertyConstraintViolation"; return; } unsigned int evseId = (unsigned int) evseIdRaw; status = resetService.initiateReset(type, evseId); } std::unique_ptr Reset::createConf() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); const char *statusCstr = ""; switch (status) { case ResetStatus_Accepted: statusCstr = "Accepted"; break; case ResetStatus_Rejected: statusCstr = "Rejected"; break; case ResetStatus_Scheduled: statusCstr = "Scheduled"; break; default: MO_DBG_ERR("internal error"); break; } payload["status"] = statusCstr; return doc; } } //end namespace Ocpp201 } //end namespace MicroOcpp #endif //MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/Reset.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef RESET_H #define RESET_H #include #include #include namespace MicroOcpp { class Model; namespace Ocpp16 { class Reset : public Operation, public MemoryManaged { private: Model& model; bool resetAccepted {false}; public: Reset(Model& model); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #if MO_ENABLE_V201 namespace MicroOcpp { namespace Ocpp201 { class ResetService; class Reset : public Operation, public MemoryManaged { private: ResetService& resetService; ResetStatus status; const char *errorCode = nullptr; public: Reset(ResetService& resetService); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp201 } //end namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/SecurityEventNotification.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include using MicroOcpp::Ocpp201::SecurityEventNotification; using MicroOcpp::JsonDoc; SecurityEventNotification::SecurityEventNotification(const char *type, const Timestamp& timestamp) : MemoryManaged("v201.Operation.", "SecurityEventNotification"), type(makeString(getMemoryTag(), type ? type : "")), timestamp(timestamp) { } const char* SecurityEventNotification::getOperationType(){ return "SecurityEventNotification"; } std::unique_ptr SecurityEventNotification::createReq() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2) + JSONDATE_LENGTH + 1); JsonObject payload = doc->to(); payload["type"] = type.c_str(); char timestampStr [JSONDATE_LENGTH + 1]; timestamp.toJsonString(timestampStr, sizeof(timestampStr)); payload["timestamp"] = timestampStr; return doc; } void SecurityEventNotification::processConf(JsonObject) { //empty payload } void SecurityEventNotification::processReq(JsonObject payload) { /** * Ignore Contents of this Req-message, because this is for debug purposes only */ } std::unique_ptr SecurityEventNotification::createConf() { return createEmptyDocument(); } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/SecurityEventNotification.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_SECURITYEVENTNOTIFICATION_H #define MO_SECURITYEVENTNOTIFICATION_H #include #if MO_ENABLE_V201 #include #include #include namespace MicroOcpp { namespace Ocpp201 { class SecurityEventNotification : public Operation, public MemoryManaged { private: String type; Timestamp timestamp; const char *errorCode = nullptr; public: SecurityEventNotification(const char *type, const Timestamp& timestamp); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; const char *getErrorCode() override {return errorCode;} void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //namespace Ocpp201 } //namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/SendLocalList.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_LOCAL_AUTH #include #include #include using MicroOcpp::Ocpp16::SendLocalList; using MicroOcpp::JsonDoc; SendLocalList::SendLocalList(AuthorizationService& authService) : MemoryManaged("v16.Operation.", "SendLocalList"), authService(authService) { } SendLocalList::~SendLocalList() { } const char* SendLocalList::getOperationType(){ return "SendLocalList"; } void SendLocalList::processReq(JsonObject payload) { //TC_043_1_CS Send Local Authorization List - NotSupported if (!authService.localAuthListEnabled()) { errorCode = "NotSupported"; return; } if (!payload.containsKey("listVersion") || !payload.containsKey("updateType")) { errorCode = "FormationViolation"; return; } if (payload.containsKey("localAuthorizationList") && !payload["localAuthorizationList"].is()) { errorCode = "FormationViolation"; return; } JsonArray localAuthorizationList = payload["localAuthorizationList"]; if (localAuthorizationList.size() > MO_SendLocalListMaxLength) { errorCode = "OccurenceConstraintViolation"; return; } bool differential = !strcmp("Differential", payload["updateType"] | "Invalid"); //updateType Differential or Full int listVersion = payload["listVersion"]; if (differential && authService.getLocalListVersion() >= listVersion) { versionMismatch = true; return; } updateFailure = !authService.updateLocalList(localAuthorizationList, listVersion, differential); } std::unique_ptr SendLocalList::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (versionMismatch) { payload["status"] = "VersionMismatch"; } else if (updateFailure) { payload["status"] = "Failed"; } else { payload["status"] = "Accepted"; } return doc; } #endif //MO_ENABLE_LOCAL_AUTH ================================================ FILE: src/MicroOcpp/Operations/SendLocalList.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_SENDLOCALLIST_H #define MO_SENDLOCALLIST_H #include #if MO_ENABLE_LOCAL_AUTH #include namespace MicroOcpp { class AuthorizationService; namespace Ocpp16 { class SendLocalList : public Operation, public MemoryManaged { private: AuthorizationService& authService; const char *errorCode = nullptr; bool updateFailure = true; bool versionMismatch = false; public: SendLocalList(AuthorizationService& authService); ~SendLocalList(); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif //MO_ENABLE_LOCAL_AUTH #endif ================================================ FILE: src/MicroOcpp/Operations/SetChargingProfile.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include using MicroOcpp::Ocpp16::SetChargingProfile; using MicroOcpp::JsonDoc; SetChargingProfile::SetChargingProfile(Model& model, SmartChargingService& scService) : MemoryManaged("v16.Operation.", "SetChargingProfile"), model(model), scService(scService) { } SetChargingProfile::~SetChargingProfile() { } const char* SetChargingProfile::getOperationType(){ return "SetChargingProfile"; } void SetChargingProfile::processReq(JsonObject payload) { int connectorId = payload["connectorId"] | -1; if (connectorId < 0 || !payload.containsKey("csChargingProfiles")) { errorCode = "FormationViolation"; return; } if ((unsigned int) connectorId >= model.getNumConnectors()) { errorCode = "PropertyConstraintViolation"; return; } JsonObject csChargingProfiles = payload["csChargingProfiles"]; auto chargingProfile = loadChargingProfile(csChargingProfiles); if (!chargingProfile) { errorCode = "PropertyConstraintViolation"; errorDescription = "csChargingProfiles validation failed"; return; } if (chargingProfile->getChargingProfilePurpose() == ChargingProfilePurposeType::TxProfile) { // if TxProfile, check if a transaction is running if (connectorId == 0) { errorCode = "PropertyConstraintViolation"; errorDescription = "Cannot set TxProfile at connectorId 0"; return; } Connector *connector = model.getConnector(connectorId); if (!connector) { errorCode = "PropertyConstraintViolation"; return; } auto& transaction = connector->getTransaction(); if (!transaction || !connector->getTransaction()->isRunning()) { //no transaction running, reject profile accepted = false; return; } if (chargingProfile->getTransactionId() >= 0 && chargingProfile->getTransactionId() != transaction->getTransactionId()) { //transactionId undefined / mismatch accepted = false; return; } //seems good } else if (chargingProfile->getChargingProfilePurpose() == ChargingProfilePurposeType::ChargePointMaxProfile) { if (connectorId > 0) { errorCode = "PropertyConstraintViolation"; errorDescription = "Cannot set ChargePointMaxProfile at connectorId > 0"; return; } } accepted = scService.setChargingProfile(connectorId, std::move(chargingProfile)); } std::unique_ptr SetChargingProfile::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (accepted) { payload["status"] = "Accepted"; } else { payload["status"] = "Rejected"; } return doc; } ================================================ FILE: src/MicroOcpp/Operations/SetChargingProfile.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_SETCHARGINGPROFILE_H #define MO_SETCHARGINGPROFILE_H #include namespace MicroOcpp { class Model; class SmartChargingService; namespace Ocpp16 { class SetChargingProfile : public Operation, public MemoryManaged { private: Model& model; SmartChargingService& scService; bool accepted = false; const char *errorCode = nullptr; const char *errorDescription = ""; public: SetChargingProfile(Model& model, SmartChargingService& scService); ~SetChargingProfile(); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} const char *getErrorDescription() override {return errorDescription;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/SetVariables.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include using MicroOcpp::Ocpp201::SetVariableData; using MicroOcpp::Ocpp201::SetVariables; using MicroOcpp::JsonDoc; SetVariableData::SetVariableData(const char *memory_tag) : componentName{makeString(memory_tag)}, variableName{makeString(memory_tag)} { } SetVariables::SetVariables(VariableService& variableService) : MemoryManaged("v201.Operation.", "SetVariables"), variableService(variableService), queries(makeVector(getMemoryTag())) { } const char* SetVariables::getOperationType(){ return "SetVariables"; } void SetVariables::processReq(JsonObject payload) { for (JsonObject setVariable : payload["setVariableData"].as()) { queries.emplace_back(getMemoryTag()); auto& data = queries.back(); if (setVariable.containsKey("attributeType")) { const char *attributeTypeCstr = setVariable["attributeType"] | "_Undefined"; if (!strcmp(attributeTypeCstr, "Actual")) { data.attributeType = Variable::AttributeType::Actual; } else if (!strcmp(attributeTypeCstr, "Target")) { data.attributeType = Variable::AttributeType::Target; } else if (!strcmp(attributeTypeCstr, "MinSet")) { data.attributeType = Variable::AttributeType::MinSet; } else if (!strcmp(attributeTypeCstr, "MaxSet")) { data.attributeType = Variable::AttributeType::MaxSet; } else { errorCode = "FormationViolation"; MO_DBG_ERR("invalid attributeType"); return; } } const char *attributeValueCstr = setVariable["attributeValue"] | (const char*) nullptr; const char *componentNameCstr = setVariable["component"]["name"] | (const char*) nullptr; const char *variableNameCstr = setVariable["variable"]["name"] | (const char*) nullptr; if (!attributeValueCstr || !componentNameCstr || !variableNameCstr) { errorCode = "FormationViolation"; return; } data.attributeValue = attributeValueCstr; data.componentName = componentNameCstr; data.variableName = variableNameCstr; // TODO check against ConfigurationValueSize data.componentEvseId = setVariable["component"]["evse"]["id"] | -1; data.componentEvseConnectorId = setVariable["component"]["evse"]["connectorId"] | -1; if (setVariable["component"].containsKey("evse") && data.componentEvseId < 0) { errorCode = "FormationViolation"; MO_DBG_ERR("malformatted / missing evseId"); return; } } if (queries.empty()) { errorCode = "FormationViolation"; return; } MO_DBG_DEBUG("processing %zu setVariable queries", queries.size()); for (auto& query : queries) { query.attributeStatus = variableService.setVariable( query.attributeType, query.attributeValue, ComponentId(query.componentName.c_str(), EvseId(query.componentEvseId, query.componentEvseConnectorId)), query.variableName.c_str()); } if (!variableService.commit()) { errorCode = "InternalError"; MO_DBG_ERR("Variables could not be stored. Rollback not possible"); return; } } std::unique_ptr SetVariables::createConf(){ size_t capacity = JSON_ARRAY_SIZE(queries.size()); for (const auto& data : queries) { capacity += JSON_OBJECT_SIZE(5) + // setVariableResult JSON_OBJECT_SIZE(2) + // component data.componentName.length() + 1 + JSON_OBJECT_SIZE(2) + // evse JSON_OBJECT_SIZE(2) + // variable data.variableName.length() + 1; } auto doc = makeJsonDoc(getMemoryTag(), capacity); JsonObject payload = doc->to(); JsonArray setVariableResult = payload.createNestedArray("setVariableResult"); for (const auto& data : queries) { JsonObject setVariable = setVariableResult.createNestedObject(); const char *attributeTypeCstr = nullptr; switch (data.attributeType) { case Variable::AttributeType::Actual: // leave blank when Actual break; case Variable::AttributeType::Target: attributeTypeCstr = "Target"; break; case Variable::AttributeType::MinSet: attributeTypeCstr = "MinSet"; break; case Variable::AttributeType::MaxSet: attributeTypeCstr = "MaxSet"; break; default: MO_DBG_ERR("internal error"); break; } if (attributeTypeCstr) { setVariable["attributeType"] = attributeTypeCstr; } const char *attributeStatusCstr = "Rejected"; switch (data.attributeStatus) { case SetVariableStatus::Accepted: attributeStatusCstr = "Accepted"; break; case SetVariableStatus::Rejected: attributeStatusCstr = "Rejected"; break; case SetVariableStatus::UnknownComponent: attributeStatusCstr = "UnknownComponent"; break; case SetVariableStatus::UnknownVariable: attributeStatusCstr = "UnknownVariable"; break; case SetVariableStatus::NotSupportedAttributeType: attributeStatusCstr = "NotSupportedAttributeType"; break; case SetVariableStatus::RebootRequired: attributeStatusCstr = "RebootRequired"; break; default: MO_DBG_ERR("internal error"); break; } setVariable["attributeStatus"] = attributeStatusCstr; setVariable["component"]["name"] = (char*) data.componentName.c_str(); // force copy-mode if (data.componentEvseId >= 0) { setVariable["component"]["evse"]["id"] = data.componentEvseId; } if (data.componentEvseConnectorId >= 0) { setVariable["component"]["evse"]["connectorId"] = data.componentEvseConnectorId; } setVariable["variable"]["name"] = (char*) data.variableName.c_str(); // force copy-mode } return doc; } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/SetVariables.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_SETVARIABLES_H #define MO_SETVARIABLES_H #include #if MO_ENABLE_V201 #include #include #include namespace MicroOcpp { class VariableService; namespace Ocpp201 { // SetVariableDataType (2.44) and // SetVariableResultType (2.45) struct SetVariableData { // SetVariableDataType Variable::AttributeType attributeType = Variable::AttributeType::Actual; const char *attributeValue; // will become invalid after processReq String componentName; int componentEvseId = -1; int componentEvseConnectorId = -1; String variableName; // SetVariableResultType SetVariableStatus attributeStatus; SetVariableData(const char *memory_tag = nullptr); }; class SetVariables : public Operation, public MemoryManaged { private: VariableService& variableService; Vector queries; const char *errorCode = nullptr; public: SetVariables(VariableService& variableService); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //namespace Ocpp201 } //namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/StartTransaction.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include using MicroOcpp::Ocpp16::StartTransaction; using MicroOcpp::JsonDoc; StartTransaction::StartTransaction(Model& model, std::shared_ptr transaction) : MemoryManaged("v16.Operation.", "StartTransaction"), model(model), transaction(transaction) { } StartTransaction::~StartTransaction() { } const char* StartTransaction::getOperationType() { return "StartTransaction"; } std::unique_ptr StartTransaction::createReq() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(6) + (IDTAG_LEN_MAX + 1) + (JSONDATE_LENGTH + 1)); JsonObject payload = doc->to(); payload["connectorId"] = transaction->getConnectorId(); payload["idTag"] = (char*) transaction->getIdTag(); payload["meterStart"] = transaction->getMeterStart(); if (transaction->getReservationId() >= 0) { payload["reservationId"] = transaction->getReservationId(); } if (transaction->getStartTimestamp() < MIN_TIME && transaction->getStartBootNr() == model.getBootNr()) { MO_DBG_DEBUG("adjust preboot StartTx timestamp"); Timestamp adjusted = model.getClock().adjustPrebootTimestamp(transaction->getStartTimestamp()); transaction->setStartTimestamp(adjusted); } char timestamp[JSONDATE_LENGTH + 1] = {'\0'}; transaction->getStartTimestamp().toJsonString(timestamp, JSONDATE_LENGTH + 1); payload["timestamp"] = timestamp; return doc; } void StartTransaction::processConf(JsonObject payload) { const char* idTagInfoStatus = payload["idTagInfo"]["status"] | "not specified"; if (!strcmp(idTagInfoStatus, "Accepted")) { MO_DBG_INFO("Request has been accepted"); } else { MO_DBG_INFO("Request has been denied. Reason: %s", idTagInfoStatus); transaction->setIdTagDeauthorized(); } int transactionId = payload["transactionId"] | -1; transaction->setTransactionId(transactionId); if (payload["idTagInfo"].containsKey("parentIdTag")) { transaction->setParentIdTag(payload["idTagInfo"]["parentIdTag"]); } transaction->getStartSync().confirm(); transaction->commit(); #if MO_ENABLE_LOCAL_AUTH if (auto authService = model.getAuthorizationService()) { authService->notifyAuthorization(transaction->getIdTag(), payload["idTagInfo"]); } #endif //MO_ENABLE_LOCAL_AUTH } void StartTransaction::processReq(JsonObject payload) { /** * Ignore Contents of this Req-message, because this is for debug purposes only */ } std::unique_ptr StartTransaction::createConf() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); JsonObject payload = doc->to(); JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); idTagInfo["status"] = "Accepted"; static int uniqueTxId = 1000; payload["transactionId"] = uniqueTxId++; //sample data for debug purpose return doc; } ================================================ FILE: src/MicroOcpp/Operations/StartTransaction.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_STARTTRANSACTION_H #define MO_STARTTRANSACTION_H #include #include #include #include namespace MicroOcpp { class Model; class Transaction; namespace Ocpp16 { class StartTransaction : public Operation, public MemoryManaged { private: Model& model; std::shared_ptr transaction; public: StartTransaction(Model& model, std::shared_ptr transaction); ~StartTransaction(); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/StatusNotification.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include namespace MicroOcpp { //helper function const char *cstrFromOcppEveState(ChargePointStatus state) { switch (state) { case (ChargePointStatus_Available): return "Available"; case (ChargePointStatus_Preparing): return "Preparing"; case (ChargePointStatus_Charging): return "Charging"; case (ChargePointStatus_SuspendedEVSE): return "SuspendedEVSE"; case (ChargePointStatus_SuspendedEV): return "SuspendedEV"; case (ChargePointStatus_Finishing): return "Finishing"; case (ChargePointStatus_Reserved): return "Reserved"; case (ChargePointStatus_Unavailable): return "Unavailable"; case (ChargePointStatus_Faulted): return "Faulted"; #if MO_ENABLE_V201 case (ChargePointStatus_Occupied): return "Occupied"; #endif default: MO_DBG_ERR("ChargePointStatus not specified"); /* fall through */ case (ChargePointStatus_UNDEFINED): return "UNDEFINED"; } } namespace Ocpp16 { StatusNotification::StatusNotification(int connectorId, ChargePointStatus currentStatus, const Timestamp ×tamp, ErrorData errorData) : MemoryManaged("v16.Operation.", "StatusNotification"), connectorId(connectorId), currentStatus(currentStatus), timestamp(timestamp), errorData(errorData) { if (currentStatus != ChargePointStatus_UNDEFINED) { MO_DBG_INFO("New status: %s (connectorId %d)", cstrFromOcppEveState(currentStatus), connectorId); } } const char* StatusNotification::getOperationType(){ return "StatusNotification"; } std::unique_ptr StatusNotification::createReq() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(7) + (JSONDATE_LENGTH + 1)); JsonObject payload = doc->to(); payload["connectorId"] = connectorId; if (errorData.isError) { if (errorData.errorCode) { payload["errorCode"] = errorData.errorCode; } if (errorData.info) { payload["info"] = errorData.info; } if (errorData.vendorId) { payload["vendorId"] = errorData.vendorId; } if (errorData.vendorErrorCode) { payload["vendorErrorCode"] = errorData.vendorErrorCode; } } else if (currentStatus == ChargePointStatus_UNDEFINED) { MO_DBG_ERR("Reporting undefined status"); payload["errorCode"] = "InternalError"; } else { payload["errorCode"] = "NoError"; } payload["status"] = cstrFromOcppEveState(currentStatus); char timestamp_cstr[JSONDATE_LENGTH + 1] = {'\0'}; timestamp.toJsonString(timestamp_cstr, JSONDATE_LENGTH + 1); payload["timestamp"] = timestamp_cstr; return doc; } void StatusNotification::processConf(JsonObject payload) { /* * Empty payload */ } /* * For debugging only */ void StatusNotification::processReq(JsonObject payload) { } /* * For debugging only */ std::unique_ptr StatusNotification::createConf(){ return createEmptyDocument(); } } // namespace Ocpp16 } // namespace MicroOcpp #if MO_ENABLE_V201 namespace MicroOcpp { namespace Ocpp201 { StatusNotification::StatusNotification(EvseId evseId, ChargePointStatus currentStatus, const Timestamp ×tamp) : MemoryManaged("v201.Operation.", "StatusNotification"), evseId(evseId), timestamp(timestamp), currentStatus(currentStatus) { } const char* StatusNotification::getOperationType(){ return "StatusNotification"; } std::unique_ptr StatusNotification::createReq() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(4) + (JSONDATE_LENGTH + 1)); JsonObject payload = doc->to(); char timestamp_cstr[JSONDATE_LENGTH + 1] = {'\0'}; timestamp.toJsonString(timestamp_cstr, JSONDATE_LENGTH + 1); payload["timestamp"] = timestamp_cstr; payload["connectorStatus"] = cstrFromOcppEveState(currentStatus); payload["evseId"] = evseId.id; payload["connectorId"] = evseId.id == 0 ? 0 : evseId.connectorId >= 0 ? evseId.connectorId : 1; return doc; } void StatusNotification::processConf(JsonObject payload) { /* * Empty payload */ } } // namespace Ocpp201 } // namespace MicroOcpp #endif //MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/StatusNotification.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef STATUSNOTIFICATION_H #define STATUSNOTIFICATION_H #include #include #include #include #include namespace MicroOcpp { const char *cstrFromOcppEveState(ChargePointStatus state); namespace Ocpp16 { class StatusNotification : public Operation, public MemoryManaged { private: int connectorId = 1; ChargePointStatus currentStatus = ChargePointStatus_UNDEFINED; Timestamp timestamp; ErrorData errorData; public: StatusNotification(int connectorId, ChargePointStatus currentStatus, const Timestamp ×tamp, ErrorData errorData = nullptr); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; int getConnectorId() { return connectorId; } }; } // namespace Ocpp16 } // namespace MicroOcpp #if MO_ENABLE_V201 #include namespace MicroOcpp { namespace Ocpp201 { class StatusNotification : public Operation, public MemoryManaged { private: EvseId evseId; Timestamp timestamp; ChargePointStatus currentStatus = ChargePointStatus_UNDEFINED; public: StatusNotification(EvseId evseId, ChargePointStatus currentStatus, const Timestamp ×tamp); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; }; } // namespace Ocpp201 } // namespace MicroOcpp #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/StopTransaction.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include using MicroOcpp::Ocpp16::StopTransaction; using MicroOcpp::JsonDoc; StopTransaction::StopTransaction(Model& model, std::shared_ptr transaction) : MemoryManaged("v16.Operation.", "StopTransaction"), model(model), transaction(transaction) { } StopTransaction::StopTransaction(Model& model, std::shared_ptr transaction, Vector> transactionData) : MemoryManaged("v16.Operation.", "StopTransaction"), model(model), transaction(transaction), transactionData(std::move(transactionData)) { } const char* StopTransaction::getOperationType() { return "StopTransaction"; } std::unique_ptr StopTransaction::createReq() { /* * Adjust timestamps in case they were taken before initial Clock setting */ if (transaction->getStopTimestamp() < MIN_TIME) { //Timestamp taken before Clock value defined. Determine timestamp if (transaction->getStopBootNr() == model.getBootNr()) { //possible to calculate real timestamp Timestamp adjusted = model.getClock().adjustPrebootTimestamp(transaction->getStopTimestamp()); transaction->setStopTimestamp(adjusted); } else if (transaction->getStartTimestamp() >= MIN_TIME) { MO_DBG_WARN("set stopTime = startTime because correct time is not available"); transaction->setStopTimestamp(transaction->getStartTimestamp() + 1); //1s behind startTime to keep order in backend DB } else { MO_DBG_ERR("failed to determine StopTx timestamp"); //send invalid value } } // if StopTx timestamp is before StartTx timestamp, something probably went wrong. Restore reasonable temporal order if (transaction->getStopTimestamp() < transaction->getStartTimestamp()) { MO_DBG_WARN("set stopTime = startTime because stopTime was before startTime"); transaction->setStopTimestamp(transaction->getStartTimestamp() + 1); //1s behind startTime to keep order in backend DB } for (auto mv = transactionData.begin(); mv != transactionData.end(); mv++) { if ((*mv)->getTimestamp() < MIN_TIME) { //time off. Try to adjust, otherwise send invalid value if ((*mv)->getReadingContext() == ReadingContext_TransactionBegin) { (*mv)->setTimestamp(transaction->getStartTimestamp()); } else if ((*mv)->getReadingContext() == ReadingContext_TransactionEnd) { (*mv)->setTimestamp(transaction->getStopTimestamp()); } else { (*mv)->setTimestamp(transaction->getStartTimestamp() + 1); } } } auto txDataJson = makeVector>(getMemoryTag()); size_t txDataJson_size = 0; for (auto mv = transactionData.begin(); mv != transactionData.end(); mv++) { auto mvJson = (*mv)->toJson(); if (!mvJson) { return nullptr; } txDataJson_size += mvJson->capacity(); txDataJson.emplace_back(std::move(mvJson)); } auto txDataDoc = initJsonDoc(getMemoryTag(), JSON_ARRAY_SIZE(txDataJson.size()) + txDataJson_size); for (auto mvJson = txDataJson.begin(); mvJson != txDataJson.end(); mvJson++) { txDataDoc.add(**mvJson); } auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(6) + //total of 6 fields (IDTAG_LEN_MAX + 1) + //stop idTag (JSONDATE_LENGTH + 1) + //timestamp string (REASON_LEN_MAX + 1) + //reason string txDataDoc.capacity()); JsonObject payload = doc->to(); if (transaction->getStopIdTag() && *transaction->getStopIdTag()) { payload["idTag"] = (char*) transaction->getStopIdTag(); } payload["meterStop"] = transaction->getMeterStop(); char timestamp[JSONDATE_LENGTH + 1] = {'\0'}; transaction->getStopTimestamp().toJsonString(timestamp, JSONDATE_LENGTH + 1); payload["timestamp"] = timestamp; payload["transactionId"] = transaction->getTransactionId(); if (transaction->getStopReason() && *transaction->getStopReason()) { payload["reason"] = (char*) transaction->getStopReason(); } if (!transactionData.empty()) { payload["transactionData"] = txDataDoc; } return doc; } void StopTransaction::processConf(JsonObject payload) { if (transaction) { transaction->getStopSync().confirm(); transaction->commit(); } MO_DBG_INFO("Request has been accepted!"); #if MO_ENABLE_LOCAL_AUTH if (auto authService = model.getAuthorizationService()) { authService->notifyAuthorization(transaction->getIdTag(), payload["idTagInfo"]); } #endif //MO_ENABLE_LOCAL_AUTH } bool StopTransaction::processErr(const char *code, const char *description, JsonObject details) { if (transaction) { transaction->getStopSync().confirm(); //no retry behavior for now; consider data "arrived" at server transaction->commit(); } MO_DBG_ERR("Server error, data loss!"); return false; } void StopTransaction::processReq(JsonObject payload) { /** * Ignore Contents of this Req-message, because this is for debug purposes only */ } std::unique_ptr StopTransaction::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), 2 * JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); idTagInfo["status"] = "Accepted"; return doc; } ================================================ FILE: src/MicroOcpp/Operations/StopTransaction.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_STOPTRANSACTION_H #define MO_STOPTRANSACTION_H #include #include #include #include namespace MicroOcpp { class Model; class SampledValue; class MeterValue; class Transaction; namespace Ocpp16 { class StopTransaction : public Operation, public MemoryManaged { private: Model& model; std::shared_ptr transaction; Vector> transactionData; public: StopTransaction(Model& model, std::shared_ptr transaction); StopTransaction(Model& model, std::shared_ptr transaction, Vector> transactionData); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; bool processErr(const char *code, const char *description, JsonObject details) override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/TransactionEvent.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include #include using namespace MicroOcpp::Ocpp201; using MicroOcpp::JsonDoc; TransactionEvent::TransactionEvent(Model& model, TransactionEventData *txEvent) : MemoryManaged("v201.Operation.", "TransactionEvent"), model(model), txEvent(txEvent) { } const char* TransactionEvent::getOperationType() { return "TransactionEvent"; } std::unique_ptr TransactionEvent::createReq() { size_t capacity = 0; if (txEvent->eventType == TransactionEventData::Type::Ended) { for (size_t i = 0; i < txEvent->transaction->sampledDataTxEnded.size(); i++) { JsonDoc meterValueJson = initJsonDoc(getMemoryTag()); //just measure, create again for serialization later txEvent->transaction->sampledDataTxEnded[i]->toJson(meterValueJson); capacity += meterValueJson.capacity(); } } for (size_t i = 0; i < txEvent->meterValue.size(); i++) { JsonDoc meterValueJson = initJsonDoc(getMemoryTag()); //just measure, create again for serialization later txEvent->meterValue[i]->toJson(meterValueJson); capacity += meterValueJson.capacity(); } capacity += JSON_OBJECT_SIZE(12) + //total of 12 fields JSONDATE_LENGTH + 1 + //timestamp string JSON_OBJECT_SIZE(5) + //transactionInfo MO_TXID_LEN_MAX + 1 + //transactionId MO_IDTOKEN_LEN_MAX + 1; //idToken auto doc = makeJsonDoc(getMemoryTag(), capacity); JsonObject payload = doc->to(); payload["eventType"] = serializeTransactionEventType(txEvent->eventType); char timestamp [JSONDATE_LENGTH + 1]; txEvent->timestamp.toJsonString(timestamp, JSONDATE_LENGTH + 1); payload["timestamp"] = timestamp; if (serializeTransactionEventTriggerReason(txEvent->triggerReason)) { payload["triggerReason"] = serializeTransactionEventTriggerReason(txEvent->triggerReason); } else { MO_DBG_ERR("serialization error"); } payload["seqNo"] = txEvent->seqNo; if (txEvent->offline) { payload["offline"] = txEvent->offline; } if (txEvent->numberOfPhasesUsed >= 0) { payload["numberOfPhasesUsed"] = txEvent->numberOfPhasesUsed; } if (txEvent->cableMaxCurrent >= 0) { payload["cableMaxCurrent"] = txEvent->cableMaxCurrent; } if (txEvent->reservationId >= 0) { payload["reservationId"] = txEvent->reservationId; } JsonObject transactionInfo = payload.createNestedObject("transactionInfo"); transactionInfo["transactionId"] = txEvent->transaction->transactionId; if (serializeTransactionEventChargingState(txEvent->chargingState)) { // optional transactionInfo["chargingState"] = serializeTransactionEventChargingState(txEvent->chargingState); } if (txEvent->transaction->stoppedReason != Transaction::StoppedReason::Local && serializeTransactionStoppedReason(txEvent->transaction->stoppedReason)) { // optional transactionInfo["stoppedReason"] = serializeTransactionStoppedReason(txEvent->transaction->stoppedReason); } if (txEvent->remoteStartId >= 0) { transactionInfo["remoteStartId"] = txEvent->transaction->remoteStartId; } if (txEvent->idToken) { JsonObject idToken = payload.createNestedObject("idToken"); idToken["idToken"] = txEvent->idToken->get(); idToken["type"] = txEvent->idToken->getTypeCstr(); } if (txEvent->evse.id >= 0) { JsonObject evse = payload.createNestedObject("evse"); evse["id"] = txEvent->evse.id; if (txEvent->evse.connectorId >= 0) { evse["connectorId"] = txEvent->evse.connectorId; } } if (txEvent->eventType == TransactionEventData::Type::Ended) { for (size_t i = 0; i < txEvent->transaction->sampledDataTxEnded.size(); i++) { JsonDoc meterValueJson = initJsonDoc(getMemoryTag()); txEvent->transaction->sampledDataTxEnded[i]->toJson(meterValueJson); payload["meterValue"].add(meterValueJson); } } for (size_t i = 0; i < txEvent->meterValue.size(); i++) { JsonDoc meterValueJson = initJsonDoc(getMemoryTag()); txEvent->meterValue[i]->toJson(meterValueJson); payload["meterValue"].add(meterValueJson); } return doc; } void TransactionEvent::processConf(JsonObject payload) { if (payload.containsKey("idTokenInfo")) { if (strcmp(payload["idTokenInfo"]["status"], "Accepted")) { MO_DBG_INFO("transaction deAuthorized"); txEvent->transaction->active = false; txEvent->transaction->isDeauthorized = true; } } } void TransactionEvent::processReq(JsonObject payload) { /** * Ignore Contents of this Req-message, because this is for debug purposes only */ } std::unique_ptr TransactionEvent::createConf() { return createEmptyDocument(); } #endif // MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/TransactionEvent.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_TRANSACTIONEVENT_H #define MO_TRANSACTIONEVENT_H #include #if MO_ENABLE_V201 #include namespace MicroOcpp { class Model; namespace Ocpp201 { class TransactionEventData; class TransactionEvent : public Operation, public MemoryManaged { private: Model& model; TransactionEventData *txEvent; const char *errorCode = nullptr; public: TransactionEvent(Model& model, TransactionEventData *txEvent); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; const char *getErrorCode() override {return errorCode;} void processReq(JsonObject payload) override; std::unique_ptr createConf() override; }; } //end namespace Ocpp201 } //end namespace MicroOcpp #endif // MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/TriggerMessage.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include using MicroOcpp::Ocpp16::TriggerMessage; using MicroOcpp::JsonDoc; TriggerMessage::TriggerMessage(Context& context) : MemoryManaged("v16.Operation.", "TriggerMessage"), context(context) { } const char* TriggerMessage::getOperationType(){ return "TriggerMessage"; } void TriggerMessage::processReq(JsonObject payload) { const char *requestedMessage = payload["requestedMessage"] | "Invalid"; const int connectorId = payload["connectorId"] | -1; MO_DBG_INFO("Execute for message type %s, connectorId = %i", requestedMessage, connectorId); statusMessage = "Rejected"; if (!strcmp(requestedMessage, "MeterValues")) { if (auto mService = context.getModel().getMeteringService()) { if (connectorId < 0) { auto nConnectors = mService->getNumConnectors(); for (decltype(nConnectors) cId = 0; cId < nConnectors; cId++) { if (auto meterValues = mService->takeTriggeredMeterValues(cId)) { context.getRequestQueue().sendRequestPreBoot(std::move(meterValues)); statusMessage = "Accepted"; } } } else if (connectorId < mService->getNumConnectors()) { if (auto meterValues = mService->takeTriggeredMeterValues(connectorId)) { context.getRequestQueue().sendRequestPreBoot(std::move(meterValues)); statusMessage = "Accepted"; } } else { errorCode = "PropertyConstraintViolation"; } } } else if (!strcmp(requestedMessage, "StatusNotification")) { unsigned int cIdRangeBegin = 0, cIdRangeEnd = 0; if (connectorId < 0) { cIdRangeEnd = context.getModel().getNumConnectors(); } else if ((unsigned int) connectorId < context.getModel().getNumConnectors()) { cIdRangeBegin = connectorId; cIdRangeEnd = connectorId + 1; } else { errorCode = "PropertyConstraintViolation"; } for (auto i = cIdRangeBegin; i < cIdRangeEnd; i++) { auto connector = context.getModel().getConnector(i); if (connector->triggerStatusNotification()) { statusMessage = "Accepted"; } } } else { auto msg = context.getOperationRegistry().deserializeOperation(requestedMessage); if (msg) { context.getRequestQueue().sendRequestPreBoot(std::move(msg)); statusMessage = "Accepted"; } else { statusMessage = "NotImplemented"; } } } std::unique_ptr TriggerMessage::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = statusMessage; return doc; } ================================================ FILE: src/MicroOcpp/Operations/TriggerMessage.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_TRIGGERMESSAGE_H #define MO_TRIGGERMESSAGE_H #include #include namespace MicroOcpp { class Context; namespace Ocpp16 { class TriggerMessage : public Operation, public MemoryManaged { private: Context& context; const char *statusMessage = nullptr; const char *errorCode = nullptr; public: TriggerMessage(Context& context); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Operations/UnlockConnector.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include using MicroOcpp::Ocpp16::UnlockConnector; using MicroOcpp::JsonDoc; UnlockConnector::UnlockConnector(Model& model) : MemoryManaged("v16.Operation.", "UnlockConnector"), model(model) { } const char* UnlockConnector::getOperationType(){ return "UnlockConnector"; } void UnlockConnector::processReq(JsonObject payload) { #if MO_ENABLE_CONNECTOR_LOCK { auto connectorId = payload["connectorId"] | -1; auto connector = model.getConnector(connectorId); if (!connector) { // NotSupported return; } unlockConnector = connector->getOnUnlockConnector(); if (!unlockConnector) { // NotSupported return; } connector->endTransaction(nullptr, "UnlockCommand"); connector->updateTxNotification(TxNotification_RemoteStop); cbUnlockResult = unlockConnector(); timerStart = mocpp_tick_ms(); } #endif //MO_ENABLE_CONNECTOR_LOCK } std::unique_ptr UnlockConnector::createConf() { const char *status = "NotSupported"; #if MO_ENABLE_CONNECTOR_LOCK if (unlockConnector) { if (mocpp_tick_ms() - timerStart < MO_UNLOCK_TIMEOUT) { //do poll and if more time is needed, delay creation of conf msg if (cbUnlockResult == UnlockConnectorResult_Pending) { cbUnlockResult = unlockConnector(); if (cbUnlockResult == UnlockConnectorResult_Pending) { return nullptr; //no result yet - delay confirmation response } } } if (cbUnlockResult == UnlockConnectorResult_Unlocked) { status = "Unlocked"; } else { status = "UnlockFailed"; } } #endif //MO_ENABLE_CONNECTOR_LOCK auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = status; return doc; } #if MO_ENABLE_V201 #if MO_ENABLE_CONNECTOR_LOCK #include namespace MicroOcpp { namespace Ocpp201 { UnlockConnector::UnlockConnector(RemoteControlService& rcService) : MemoryManaged("v201.Operation.UnlockConnector"), rcService(rcService) { } const char* UnlockConnector::getOperationType(){ return "UnlockConnector"; } void UnlockConnector::processReq(JsonObject payload) { int evseId = payload["evseId"] | -1; int connectorId = payload["connectorId"] | -1; if (evseId < 1 || evseId >= MO_NUM_EVSEID || connectorId < 1) { errorCode = "PropertyConstraintViolation"; return; } if (connectorId != 1) { status = UnlockStatus_UnknownConnector; return; } rcEvse = rcService.getEvse(evseId); if (!rcEvse) { status = UnlockStatus_UnlockFailed; return; } status = rcEvse->unlockConnector(); timerStart = mocpp_tick_ms(); } std::unique_ptr UnlockConnector::createConf() { if (rcEvse && status == UnlockStatus_PENDING && mocpp_tick_ms() - timerStart < MO_UNLOCK_TIMEOUT) { status = rcEvse->unlockConnector(); if (status == UnlockStatus_PENDING) { return nullptr; //no result yet - delay confirmation response } } const char *statusStr = ""; switch (status) { case UnlockStatus_Unlocked: statusStr = "Unlocked"; break; case UnlockStatus_UnlockFailed: statusStr = "UnlockFailed"; break; case UnlockStatus_OngoingAuthorizedTransaction: statusStr = "OngoingAuthorizedTransaction"; break; case UnlockStatus_UnknownConnector: statusStr = "UnknownConnector"; break; case UnlockStatus_PENDING: MO_DBG_ERR("UnlockConnector timeout"); statusStr = "UnlockFailed"; break; } auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = statusStr; return doc; } } // namespace Ocpp201 } // namespace MicroOcpp #endif //MO_ENABLE_CONNECTOR_LOCK #endif //MO_ENABLE_V201 ================================================ FILE: src/MicroOcpp/Operations/UnlockConnector.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef UNLOCKCONNECTOR_H #define UNLOCKCONNECTOR_H #include #include #include #include namespace MicroOcpp { class Model; namespace Ocpp16 { class UnlockConnector : public Operation, public MemoryManaged { private: Model& model; #if MO_ENABLE_CONNECTOR_LOCK std::function unlockConnector; UnlockConnectorResult cbUnlockResult; unsigned long timerStart = 0; //for timeout #endif //MO_ENABLE_CONNECTOR_LOCK const char *errorCode = nullptr; public: UnlockConnector(Model& model); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #if MO_ENABLE_V201 #if MO_ENABLE_CONNECTOR_LOCK #include namespace MicroOcpp { class RemoteControlService; class RemoteControlServiceEvse; namespace Ocpp201 { class UnlockConnector : public Operation, public MemoryManaged { private: RemoteControlService& rcService; RemoteControlServiceEvse *rcEvse = nullptr; UnlockStatus status; unsigned long timerStart = 0; //for timeout const char *errorCode = nullptr; public: UnlockConnector(RemoteControlService& rcService); const char* getOperationType() override; void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp201 } //end namespace MicroOcpp #endif //MO_ENABLE_CONNECTOR_LOCK #endif //MO_ENABLE_V201 #endif ================================================ FILE: src/MicroOcpp/Operations/UpdateFirmware.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include using MicroOcpp::Ocpp16::UpdateFirmware; using MicroOcpp::JsonDoc; UpdateFirmware::UpdateFirmware(FirmwareService& fwService) : MemoryManaged("v16.Operation.", "UpdateFirmware"), fwService(fwService) { } void UpdateFirmware::processReq(JsonObject payload) { const char *location = payload["location"] | ""; //check location URL. Maybe introduce Same-Origin-Policy? if (!*location) { errorCode = "FormationViolation"; return; } int retries = payload["retries"] | 1; int retryInterval = payload["retryInterval"] | 180; if (retries < 0 || retryInterval < 0) { errorCode = "PropertyConstraintViolation"; return; } //check the integrity of retrieveDate if (!payload.containsKey("retrieveDate")) { errorCode = "FormationViolation"; return; } Timestamp retrieveDate; if (!retrieveDate.setTime(payload["retrieveDate"] | "Invalid")) { errorCode = "PropertyConstraintViolation"; MO_DBG_WARN("bad time format"); return; } fwService.scheduleFirmwareUpdate(location, retrieveDate, (unsigned int) retries, (unsigned int) retryInterval); } std::unique_ptr UpdateFirmware::createConf(){ return createEmptyDocument(); } ================================================ FILE: src/MicroOcpp/Operations/UpdateFirmware.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_UPDATEFIRMWARE_H #define MO_UPDATEFIRMWARE_H #include #include namespace MicroOcpp { class FirmwareService; namespace Ocpp16 { class UpdateFirmware : public Operation, public MemoryManaged { private: FirmwareService& fwService; const char *errorCode = nullptr; public: UpdateFirmware(FirmwareService& fwService); const char* getOperationType() override {return "UpdateFirmware";} void processReq(JsonObject payload) override; std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp #endif ================================================ FILE: src/MicroOcpp/Platform.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #ifdef MO_CUSTOM_CONSOLE char _mo_console_msg_buf [MO_CUSTOM_CONSOLE_MAXMSGSIZE]; namespace MicroOcpp { void (*mocpp_console_out_impl)(const char *msg) = nullptr; } void _mo_console_out(const char *msg) { if (MicroOcpp::mocpp_console_out_impl) { MicroOcpp::mocpp_console_out_impl(msg); } } void mocpp_set_console_out(void (*console_out)(const char *msg)) { MicroOcpp::mocpp_console_out_impl = console_out; if (console_out) { console_out("[OCPP] console initialized\n"); } } #endif #ifdef MO_CUSTOM_TIMER unsigned long (*mocpp_tick_ms_impl)() = nullptr; void mocpp_set_timer(unsigned long (*get_ms)()) { mocpp_tick_ms_impl = get_ms; } unsigned long mocpp_tick_ms_custom() { if (mocpp_tick_ms_impl) { return mocpp_tick_ms_impl(); } else { return 0; } } #else #if MO_PLATFORM == MO_PLATFORM_ESPIDF #include "freertos/FreeRTOS.h" #include "freertos/task.h" namespace MicroOcpp { decltype(xTaskGetTickCount()) mocpp_ticks_count = 0; unsigned long mocpp_millis_count = 0; } unsigned long mocpp_tick_ms_espidf() { auto ticks_now = xTaskGetTickCount(); MicroOcpp::mocpp_millis_count += ((ticks_now - MicroOcpp::mocpp_ticks_count) * 1000UL) / configTICK_RATE_HZ; MicroOcpp::mocpp_ticks_count = ticks_now; return MicroOcpp::mocpp_millis_count; } #elif MO_PLATFORM == MO_PLATFORM_UNIX #include namespace MicroOcpp { std::chrono::steady_clock::time_point clock_reference; bool clock_initialized = false; } unsigned long mocpp_tick_ms_unix() { if (!MicroOcpp::clock_initialized) { MicroOcpp::clock_reference = std::chrono::steady_clock::now(); MicroOcpp::clock_initialized = true; } std::chrono::milliseconds ms = std::chrono::duration_cast( std::chrono::steady_clock::now() - MicroOcpp::clock_reference); return (unsigned long) ms.count(); } #endif #endif #ifdef MO_CUSTOM_RNG uint32_t (*mocpp_rng_impl)() = nullptr; void mocpp_set_rng(uint32_t (*rng)()) { mocpp_rng_impl = rng; } uint32_t mocpp_rng_custom(void) { if (mocpp_rng_impl) { return mocpp_rng_impl(); } else { return 0; } } #else // Time-based Pseudo RNG. // Contains internal state which is mixed with the current timestamp // each time it is called. Then this is passed through a multiply-with-carry // PRNG operation to get a pseudo-random number. uint32_t mocpp_time_based_prng(void) { static uint32_t prng_state = 1; uint32_t entropy = mocpp_tick_ms(); prng_state = (prng_state ^ entropy)*1664525U + 1013904223U; // assuming complement-2 integers and non-signaling overflow return prng_state; } #endif ================================================ FILE: src/MicroOcpp/Platform.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_PLATFORM_H #define MO_PLATFORM_H #include #define MO_PLATFORM_NONE 0 #define MO_PLATFORM_ARDUINO 1 #define MO_PLATFORM_ESPIDF 2 #define MO_PLATFORM_UNIX 3 #ifndef MO_PLATFORM #define MO_PLATFORM MO_PLATFORM_ARDUINO #endif #ifdef __cplusplus #define MO_EXTERN_C extern "C" #else #define MO_EXTERN_C #endif #if MO_PLATFORM == MO_PLATFORM_NONE #ifndef MO_CUSTOM_CONSOLE #define MO_CUSTOM_CONSOLE #endif #ifndef MO_CUSTOM_TIMER #define MO_CUSTOM_TIMER #endif #endif #ifdef MO_CUSTOM_CONSOLE #include #ifndef MO_CUSTOM_CONSOLE_MAXMSGSIZE #define MO_CUSTOM_CONSOLE_MAXMSGSIZE 256 #endif extern char _mo_console_msg_buf [MO_CUSTOM_CONSOLE_MAXMSGSIZE]; //define msg_buf in data section to save memory (see https://github.com/matth-x/MicroOcpp/pull/304) MO_EXTERN_C void _mo_console_out(const char *msg); MO_EXTERN_C void mocpp_set_console_out(void (*console_out)(const char *msg)); #define MO_CONSOLE_PRINTF(X, ...) \ do { \ auto _mo_ret = snprintf(_mo_console_msg_buf, MO_CUSTOM_CONSOLE_MAXMSGSIZE, X, ##__VA_ARGS__); \ if (_mo_ret < 0 || _mo_ret >= MO_CUSTOM_CONSOLE_MAXMSGSIZE) { \ sprintf(_mo_console_msg_buf + MO_CUSTOM_CONSOLE_MAXMSGSIZE - 7, " [...]"); \ } \ _mo_console_out(_mo_console_msg_buf); \ } while (0) #else #define mocpp_set_console_out(X) \ do { \ X("[OCPP] CONSOLE ERROR: mocpp_set_console_out ignored if MO_CUSTOM_CONSOLE " \ "not defined\n"); \ char msg [196]; \ snprintf(msg, 196, " > see %s:%i",__FILE__,__LINE__); \ X(msg); \ X("\n > see MicroOcpp/Platform.h\n"); \ } while (0) #if MO_PLATFORM == MO_PLATFORM_ARDUINO #include #ifndef MO_USE_SERIAL #define MO_USE_SERIAL Serial #endif #define MO_CONSOLE_PRINTF(X, ...) MO_USE_SERIAL.printf_P(PSTR(X), ##__VA_ARGS__) #elif MO_PLATFORM == MO_PLATFORM_ESPIDF #include "esp_log.h" #define MO_CONSOLE_PRINTF(X, ...) esp_log_write(ESP_LOG_INFO, "MicroOcpp", X, ##__VA_ARGS__) #elif MO_PLATFORM == MO_PLATFORM_UNIX #include #define MO_CONSOLE_PRINTF(X, ...) printf(X, ##__VA_ARGS__) #endif #endif #ifdef MO_CUSTOM_TIMER MO_EXTERN_C void mocpp_set_timer(unsigned long (*get_ms)()); MO_EXTERN_C unsigned long mocpp_tick_ms_custom(); #define mocpp_tick_ms mocpp_tick_ms_custom #else #if MO_PLATFORM == MO_PLATFORM_ARDUINO #include #define mocpp_tick_ms millis #elif MO_PLATFORM == MO_PLATFORM_ESPIDF MO_EXTERN_C unsigned long mocpp_tick_ms_espidf(); #define mocpp_tick_ms mocpp_tick_ms_espidf #elif MO_PLATFORM == MO_PLATFORM_UNIX MO_EXTERN_C unsigned long mocpp_tick_ms_unix(); #define mocpp_tick_ms mocpp_tick_ms_unix #endif #endif #ifdef MO_CUSTOM_RNG MO_EXTERN_C void mocpp_set_rng(uint32_t (*rng)()); MO_EXTERN_C uint32_t mocpp_rng_custom(); #define mocpp_rng mocpp_rng_custom #else MO_EXTERN_C uint32_t mocpp_time_based_prng(void); #define mocpp_rng mocpp_time_based_prng #endif #ifndef MO_MAX_JSON_CAPACITY #if MO_PLATFORM == MO_PLATFORM_UNIX #define MO_MAX_JSON_CAPACITY 16384 #else #define MO_MAX_JSON_CAPACITY 4096 #endif #endif #ifndef MO_ENABLE_MBEDTLS #define MO_ENABLE_MBEDTLS 0 #endif #endif ================================================ FILE: src/MicroOcpp/Version.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_VERSION_H #define MO_VERSION_H /* * Version specification of MicroOcpp library (not related with the OCPP version) */ #define MO_VERSION "1.2.0" /* * Enable OCPP 2.0.1 support. If enabled, library can be initialized with both v1.6 and v2.0.1. The choice * of the protocol is done dynamically during initialization */ #ifndef MO_ENABLE_V201 #define MO_ENABLE_V201 0 #endif #ifdef __cplusplus namespace MicroOcpp { /* * OCPP version type, defined in Model */ struct ProtocolVersion { const int major, minor, patch; ProtocolVersion(int major = 1, int minor = 6, int patch = 0) : major(major), minor(minor), patch(patch) { } }; } #endif //__cplusplus // Certificate Management (UCs M03 - M05). Works with OCPP 1.6 and 2.0.1 #ifndef MO_ENABLE_CERT_MGMT #define MO_ENABLE_CERT_MGMT MO_ENABLE_V201 #endif // Reservations #ifndef MO_ENABLE_RESERVATION #define MO_ENABLE_RESERVATION 1 #endif // Local Authorization, i.e. feature profile LocalAuthListManagement #ifndef MO_ENABLE_LOCAL_AUTH #define MO_ENABLE_LOCAL_AUTH 1 #endif #endif ================================================ FILE: src/MicroOcpp.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include "MicroOcpp.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace MicroOcpp { namespace Facade { #ifndef MO_CUSTOM_WS WebSocketsClient *webSocket {nullptr}; Connection *connection {nullptr}; #endif Context *context {nullptr}; std::shared_ptr filesystem; #ifndef MO_NUMCONNECTORS #define MO_NUMCONNECTORS 2 #endif #define OCPP_ID_OF_CP 0 #define OCPP_ID_OF_CONNECTOR 1 } //end namespace MicroOcpp::Facade } //end namespace MicroOcpp #if MO_ENABLE_HEAP_PROFILER #ifndef MO_HEAP_PROFILER_EXTERNAL_CONTROL #define MO_HEAP_PROFILER_EXTERNAL_CONTROL 0 //enable if you want to manually reset the heap profiler (e.g. for keeping stats over multiple MO lifecycles) #endif #endif using namespace MicroOcpp; using namespace MicroOcpp::Facade; using namespace MicroOcpp::Ocpp16; #ifndef MO_CUSTOM_WS void mocpp_initialize(const char *backendUrl, const char *chargeBoxId, const char *chargePointModel, const char *chargePointVendor, FilesystemOpt fsOpt, const char *password, const char *CA_cert, bool autoRecover) { if (context) { MO_DBG_WARN("already initialized. To reinit, call mocpp_deinitialize() before"); return; } if (!backendUrl || !chargePointModel || !chargePointVendor) { MO_DBG_ERR("invalid args"); return; } if (!chargeBoxId) { chargeBoxId = ""; } /* * parse backendUrl so that it suits the links2004/arduinoWebSockets interface */ auto url = makeString("MicroOcpp.cpp", backendUrl); //tolower protocol specifier for (auto c = url.begin(); *c != ':' && c != url.end(); c++) { *c = tolower(*c); } bool isTLS = true; if (!strncmp(url.c_str(),"wss://",strlen("wss://"))) { isTLS = true; } else if (!strncmp(url.c_str(),"ws://",strlen("ws://"))) { isTLS = false; } else { MO_DBG_ERR("only ws:// and wss:// supported"); return; } //parse host, port auto host_port_path = url.substr(url.find_first_of("://") + strlen("://")); auto host_port = host_port_path.substr(0, host_port_path.find_first_of('/')); auto path = host_port_path.substr(host_port.length()); auto host = host_port.substr(0, host_port.find_first_of(':')); if (host.empty()) { MO_DBG_ERR("could not parse host: %s", url.c_str()); return; } uint16_t port = 0; auto port_str = host_port.substr(host.length()); if (port_str.empty()) { port = isTLS ? 443U : 80U; } else { //skip leading ':' port_str = port_str.substr(1); for (auto c = port_str.begin(); c != port_str.end(); c++) { if (*c < '0' || *c > '9') { MO_DBG_ERR("could not parse port: %s", url.c_str()); return; } auto p = port * 10U + (*c - '0'); if (p < port) { MO_DBG_ERR("could not parse port (overflow): %s", url.c_str()); return; } port = p; } } if (path.empty()) { path = "/"; } if ((!*chargeBoxId) == '\0') { if (path.back() != '/') { path += '/'; } path += chargeBoxId; } MO_DBG_INFO("connecting to %s -- (host: %s, port: %u, path: %s)", url.c_str(), host.c_str(), port, path.c_str()); if (!webSocket) webSocket = new WebSocketsClient(); if (isTLS) { // server address, port, path and TLS certificate webSocket->beginSslWithCA(host.c_str(), port, path.c_str(), CA_cert, "ocpp1.6"); } else { // server address, port, path webSocket->begin(host.c_str(), port, path.c_str(), "ocpp1.6"); } // try ever 5000 again if connection has failed webSocket->setReconnectInterval(5000); // start heartbeat (optional) // ping server every 15000 ms // expect pong from server within 3000 ms // consider connection disconnected if pong is not received 2 times webSocket->enableHeartbeat(15000, 3000, 2); //comment this one out to for specific OCPP servers // add authentication data (optional) if (password && strlen(password) + strlen(chargeBoxId) >= 4) { webSocket->setAuthorization(chargeBoxId, password); } delete connection; connection = new EspWiFi::WSClient(webSocket); mocpp_initialize(*connection, ChargerCredentials(chargePointModel, chargePointVendor), makeDefaultFilesystemAdapter(fsOpt), autoRecover); } #endif ChargerCredentials::ChargerCredentials(const char *cpModel, const char *cpVendor, const char *fWv, const char *cpSNr, const char *meterSNr, const char *meterType, const char *cbSNr, const char *iccid, const char *imsi) { StaticJsonDocument<512> creds; if (cbSNr) creds["chargeBoxSerialNumber"] = cbSNr; if (cpModel) creds["chargePointModel"] = cpModel; if (cpSNr) creds["chargePointSerialNumber"] = cpSNr; if (cpVendor) creds["chargePointVendor"] = cpVendor; if (fWv) creds["firmwareVersion"] = fWv; if (iccid) creds["iccid"] = iccid; if (imsi) creds["imsi"] = imsi; if (meterSNr) creds["meterSerialNumber"] = meterSNr; if (meterType) creds["meterType"] = meterType; if (creds.overflowed()) { MO_DBG_ERR("Charger Credentials too long"); } size_t written = serializeJson(creds, payload, 512); if (written < 2) { MO_DBG_ERR("Charger Credentials could not be written"); sprintf(payload, "{}"); } } ChargerCredentials ChargerCredentials::v201(const char *cpModel, const char *cpVendor, const char *fWv, const char *cpSNr, const char *meterSNr, const char *meterType, const char *cbSNr, const char *iccid, const char *imsi) { ChargerCredentials res; StaticJsonDocument<512> creds; if (cpSNr) creds["serialNumber"] = cpSNr; if (cpModel) creds["model"] = cpModel; if (cpVendor) creds["vendorName"] = cpVendor; if (fWv) creds["firmwareVersion"] = fWv; if (iccid) creds["modem"]["iccid"] = iccid; if (imsi) creds["modem"]["imsi"] = imsi; if (creds.overflowed()) { MO_DBG_ERR("Charger Credentials too long"); } size_t written = serializeJson(creds, res.payload, 512); if (written < 2) { MO_DBG_ERR("Charger Credentials could not be written"); sprintf(res.payload, "{}"); } return res; } void mocpp_initialize(Connection& connection, const char *bootNotificationCredentials, std::shared_ptr fs, bool autoRecover, MicroOcpp::ProtocolVersion version) { if (context) { MO_DBG_WARN("already initialized. To reinit, call mocpp_deinitialize() before"); return; } MO_DBG_DEBUG("initialize OCPP"); filesystem = fs; MO_DBG_DEBUG("filesystem %s", filesystem ? "loaded" : "deactivated"); BootStats bootstats; BootService::loadBootStats(filesystem, bootstats); if (autoRecover && bootstats.getBootFailureCount() > 3) { BootService::recover(filesystem, bootstats); bootstats = BootStats(); } BootService::migrate(filesystem, bootstats); bootstats.bootNr++; //assign new boot number to this run BootService::storeBootStats(filesystem, bootstats); configuration_init(filesystem); //call before each other library call context = new Context(connection, filesystem, bootstats.bootNr, version); #if MO_ENABLE_MBEDTLS context->setFtpClient(makeFtpClientMbedTLS()); #endif //MO_ENABLE_MBEDTLS auto& model = context->getModel(); model.setBootService(std::unique_ptr( new BootService(*context, filesystem))); #if MO_ENABLE_V201 if (version.major == 2) { model.setAvailabilityService(std::unique_ptr( new AvailabilityService(*context, MO_NUM_EVSEID))); model.setVariableService(std::unique_ptr( new VariableService(*context, filesystem))); model.setTransactionService(std::unique_ptr( new TransactionService(*context, filesystem, MO_NUM_EVSEID))); model.setRemoteControlService(std::unique_ptr( new RemoteControlService(*context, MO_NUM_EVSEID))); model.setResetServiceV201(std::unique_ptr( new Ocpp201::ResetService(*context))); } else #endif { model.setTransactionStore(std::unique_ptr( new TransactionStore(MO_NUMCONNECTORS, filesystem))); model.setConnectorsCommon(std::unique_ptr( new ConnectorsCommon(*context, MO_NUMCONNECTORS, filesystem))); auto connectors = makeVector>("v16.ConnectorBase.Connector"); for (unsigned int connectorId = 0; connectorId < MO_NUMCONNECTORS; connectorId++) { connectors.emplace_back(new Connector(*context, filesystem, connectorId)); } model.setConnectors(std::move(connectors)); #if MO_ENABLE_LOCAL_AUTH model.setAuthorizationService(std::unique_ptr( new AuthorizationService(*context, filesystem))); #endif //MO_ENABLE_LOCAL_AUTH #if MO_ENABLE_RESERVATION model.setReservationService(std::unique_ptr( new ReservationService(*context, MO_NUMCONNECTORS))); #endif model.setResetService(std::unique_ptr( new ResetService(*context))); } model.setHeartbeatService(std::unique_ptr( new HeartbeatService(*context))); #if MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS std::unique_ptr certStore = makeCertificateStoreMbedTLS(filesystem); if (certStore) { model.setCertificateService(std::unique_ptr( new CertificateService(*context))); } if (certStore && model.getCertificateService()) { model.getCertificateService()->setCertificateStore(std::move(certStore)); } #endif #if !defined(MO_CUSTOM_UPDATER) #if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS model.setFirmwareService( makeDefaultFirmwareService(*context)); //instantiate FW service + ESP installation routine #elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP8266) model.setFirmwareService( makeDefaultFirmwareService(*context)); //instantiate FW service + ESP installation routine #endif //MO_PLATFORM #endif //!defined(MO_CUSTOM_UPDATER) #if !defined(MO_CUSTOM_DIAGNOSTICS) #if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS model.setDiagnosticsService( makeDefaultDiagnosticsService(*context, filesystem)); //instantiate Diag service + ESP hardware diagnostics #elif MO_ENABLE_MBEDTLS model.setDiagnosticsService( makeDefaultDiagnosticsService(*context, filesystem)); //instantiate Diag service #endif //MO_PLATFORM #endif //!defined(MO_CUSTOM_DIAGNOSTICS) #if MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) setOnResetExecute(makeDefaultResetFn()); #endif model.getBootService()->setChargePointCredentials(bootNotificationCredentials); auto credsJson = model.getBootService()->getChargePointCredentials(); if (model.getFirmwareService() && credsJson && credsJson->containsKey("firmwareVersion")) { model.getFirmwareService()->setBuildNumber((*credsJson)["firmwareVersion"]); } credsJson.reset(); configuration_load(); #if MO_ENABLE_V201 if (version.major == 2) { model.getVariableService()->load(); } #endif //MO_ENABLE_V201 MO_DBG_INFO("initialized MicroOcpp v" MO_VERSION " running OCPP %i.%i.%i", version.major, version.minor, version.patch); } void mocpp_deinitialize() { if (context) { //release bootstats recovery mechanism BootStats bootstats; BootService::loadBootStats(filesystem, bootstats); if (bootstats.lastBootSuccess != bootstats.bootNr) { MO_DBG_DEBUG("boot success timer override"); bootstats.lastBootSuccess = bootstats.bootNr; BootService::storeBootStats(filesystem, bootstats); } } delete context; context = nullptr; #ifndef MO_CUSTOM_WS delete connection; connection = nullptr; delete webSocket; webSocket = nullptr; #endif filesystem.reset(); configuration_deinit(); #if !MO_HEAP_PROFILER_EXTERNAL_CONTROL MO_MEM_DEINIT(); #endif MO_DBG_DEBUG("deinitialized OCPP\n"); } void mocpp_loop() { if (!context) { MO_DBG_WARN("need to call mocpp_initialize before"); return; } context->loop(); } bool beginTransaction(const char *idTag, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { if (!idTag || strnlen(idTag, MO_IDTOKEN_LEN_MAX + 2) > MO_IDTOKEN_LEN_MAX) { MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", MO_IDTOKEN_LEN_MAX); return false; } TransactionService::Evse *evse = nullptr; if (auto txService = context->getModel().getTransactionService()) { evse = txService->getEvse(connectorId); } if (!evse) { MO_DBG_ERR("could not find EVSE"); return false; } return evse->beginAuthorization(idTag, true); } #endif if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX) { MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); return false; } auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return false; } return connector->beginTransaction(idTag) != nullptr; } bool beginTransaction_authorized(const char *idTag, const char *parentIdTag, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { if (!idTag || strnlen(idTag, MO_IDTOKEN_LEN_MAX + 2) > MO_IDTOKEN_LEN_MAX) { MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", MO_IDTOKEN_LEN_MAX); return false; } TransactionService::Evse *evse = nullptr; if (auto txService = context->getModel().getTransactionService()) { evse = txService->getEvse(connectorId); } if (!evse) { MO_DBG_ERR("could not find EVSE"); return false; } return evse->beginAuthorization(idTag, false); } #endif if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX || (parentIdTag && strnlen(parentIdTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX)) { MO_DBG_ERR("(parent)idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); return false; } auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return false; } return connector->beginTransaction_authorized(idTag, parentIdTag) != nullptr; } bool endTransaction(const char *idTag, const char *reason, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { if (!idTag || strnlen(idTag, MO_IDTOKEN_LEN_MAX + 2) > MO_IDTOKEN_LEN_MAX) { MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", MO_IDTOKEN_LEN_MAX); return false; } TransactionService::Evse *evse = nullptr; if (auto txService = context->getModel().getTransactionService()) { evse = txService->getEvse(connectorId); } if (!evse) { MO_DBG_ERR("could not find EVSE"); return false; } return evse->endAuthorization(idTag, true); } #endif bool res = false; if (isTransactionActive(connectorId) && getTransactionIdTag(connectorId)) { //end transaction now if either idTag is nullptr (i.e. force stop) or the idTag matches beginTransaction if (!idTag || !strcmp(idTag, getTransactionIdTag(connectorId))) { res = endTransaction_authorized(idTag, reason, connectorId); } else { auto tx = getTransaction(connectorId); const char *parentIdTag = tx->getParentIdTag(); if (strlen(parentIdTag) > 0) { // We have a parent ID tag, so we need to check if this new card also has one auto authorize = makeRequest(new Ocpp16::Authorize(context->getModel(), idTag)); auto idTag_capture = makeString("MicroOcpp.cpp", idTag); auto reason_capture = makeString("MicroOcpp.cpp", reason ? reason : ""); authorize->setOnReceiveConfListener([idTag_capture, reason_capture, connectorId, tx] (JsonObject response) { JsonObject idTagInfo = response["idTagInfo"]; if (strcmp("Accepted", idTagInfo["status"] | "UNDEFINED")) { //Authorization rejected, do nothing MO_DBG_DEBUG("Authorize rejected (%s), continue transaction", idTag_capture.c_str()); auto connector = context->getModel().getConnector(connectorId); if (connector) { connector->updateTxNotification(TxNotification_AuthorizationRejected); } return; } if (idTagInfo.containsKey("parentIdTag") && !strcmp(idTagInfo["parenIdTag"], tx->getParentIdTag())) { endTransaction_authorized(idTag_capture.c_str(), reason_capture.empty() ? (const char*)nullptr : reason_capture.c_str(), connectorId); } }); authorize->setOnTimeoutListener([idTag_capture, connectorId] () { //Authorization timed out, do nothing MO_DBG_DEBUG("Authorization timeout (%s), continue transaction", idTag_capture.c_str()); auto connector = context->getModel().getConnector(connectorId); if (connector) { connector->updateTxNotification(TxNotification_AuthorizationTimeout); } }); auto authorizationTimeoutInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", 20); authorize->setTimeout(authorizationTimeoutInt && authorizationTimeoutInt->getInt() > 0 ? authorizationTimeoutInt->getInt() * 1000UL : 20UL * 1000UL); context->initiateRequest(std::move(authorize)); res = true; } else { MO_DBG_INFO("endTransaction: idTag doesn't match"); (void)0; } } } return res; } bool endTransaction_authorized(const char *idTag, const char *reason, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { if (!idTag || strnlen(idTag, MO_IDTOKEN_LEN_MAX + 2) > MO_IDTOKEN_LEN_MAX) { MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", MO_IDTOKEN_LEN_MAX); return false; } TransactionService::Evse *evse = nullptr; if (auto txService = context->getModel().getTransactionService()) { evse = txService->getEvse(connectorId); } if (!evse) { MO_DBG_ERR("could not find EVSE"); return false; } return evse->endAuthorization(idTag, false); } #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return false; } auto res = isTransactionActive(connectorId); connector->endTransaction(idTag, reason); return res; } bool isTransactionActive(unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { TransactionService::Evse *evse = nullptr; if (auto txService = context->getModel().getTransactionService()) { evse = txService->getEvse(connectorId); } if (!evse) { MO_DBG_ERR("could not find EVSE"); return false; } return evse->getTransaction() && evse->getTransaction()->active; } #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return false; } auto& tx = connector->getTransaction(); return tx ? tx->isActive() : false; } bool isTransactionRunning(unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { TransactionService::Evse *evse = nullptr; if (auto txService = context->getModel().getTransactionService()) { evse = txService->getEvse(connectorId); } if (!evse) { MO_DBG_ERR("could not find EVSE"); return false; } return evse->getTransaction() && evse->getTransaction()->started && !evse->getTransaction()->stopped; } #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return false; } auto& tx = connector->getTransaction(); return tx ? tx->isRunning() : false; } const char *getTransactionIdTag(unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return nullptr; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { TransactionService::Evse *evse = nullptr; if (auto txService = context->getModel().getTransactionService()) { evse = txService->getEvse(connectorId); } if (!evse) { MO_DBG_ERR("could not find EVSE"); return nullptr; } return evse->getTransaction() ? evse->getTransaction()->idToken.get() : nullptr; } #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return nullptr; } auto& tx = connector->getTransaction(); return tx ? tx->getIdTag() : nullptr; } std::shared_ptr mocpp_undefinedTx; std::shared_ptr& getTransaction(unsigned int connectorId) { if (!context) { MO_DBG_WARN("OCPP uninitialized"); return mocpp_undefinedTx; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { MO_DBG_ERR("only supported in v16"); return mocpp_undefinedTx; } #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return mocpp_undefinedTx; } return connector->getTransaction(); } #if MO_ENABLE_V201 Ocpp201::Transaction *getTransactionV201(unsigned int evseId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return nullptr; } if (context->getVersion().major != 2) { MO_DBG_ERR("only supported in v201"); return nullptr; } TransactionService::Evse *evse = nullptr; if (auto txService = context->getModel().getTransactionService()) { evse = txService->getEvse(evseId); } if (!evse) { MO_DBG_ERR("could not find EVSE"); return nullptr; } return evse->getTransaction(); } #endif //MO_ENABLE_V201 bool ocppPermitsCharge(unsigned int connectorId) { if (!context) { MO_DBG_WARN("OCPP uninitialized"); return false; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { TransactionService::Evse *evse = nullptr; if (auto txService = context->getModel().getTransactionService()) { evse = txService->getEvse(connectorId); } if (!evse) { MO_DBG_ERR("could not find EVSE"); return false; } return evse->ocppPermitsCharge(); } #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return false; } return connector->ocppPermitsCharge(); } ChargePointStatus getChargePointStatus(unsigned int connectorId) { if (!context) { MO_DBG_WARN("OCPP uninitialized"); return ChargePointStatus_UNDEFINED; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { if (auto availabilityService = context->getModel().getAvailabilityService()) { if (auto evse = availabilityService->getEvse(connectorId)) { return evse->getStatus(); } } } #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return ChargePointStatus_UNDEFINED; } return connector->getStatus(); } void setConnectorPluggedInput(std::function pluggedInput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { if (auto availabilityService = context->getModel().getAvailabilityService()) { if (auto evse = availabilityService->getEvse(connectorId)) { evse->setConnectorPluggedInput(pluggedInput); } } if (auto txService = context->getModel().getTransactionService()) { if (auto evse = txService->getEvse(connectorId)) { evse->setConnectorPluggedInput(pluggedInput); } } return; } #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return; } connector->setConnectorPluggedInput(pluggedInput); } void setEnergyMeterInput(std::function energyInput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { addMeterValueInput([energyInput] () {return static_cast(energyInput());}, "Energy.Active.Import.Register", "Wh", nullptr, nullptr, connectorId); return; } #endif SampledValueProperties meterProperties; meterProperties.setMeasurand("Energy.Active.Import.Register"); meterProperties.setUnit("Wh"); auto mvs = std::unique_ptr>>( new SampledValueSamplerConcrete>( meterProperties, [energyInput] (ReadingContext) {return energyInput();} )); addMeterValueInput(std::move(mvs), connectorId); } void setPowerMeterInput(std::function powerInput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { addMeterValueInput([powerInput] () {return static_cast(powerInput());}, "Power.Active.Import", "W", nullptr, nullptr, connectorId); return; } #endif SampledValueProperties meterProperties; meterProperties.setMeasurand("Power.Active.Import"); meterProperties.setUnit("W"); auto mvs = std::unique_ptr>>( new SampledValueSamplerConcrete>( meterProperties, [powerInput] (ReadingContext) {return powerInput();} )); addMeterValueInput(std::move(mvs), connectorId); } void setSmartChargingPowerOutput(std::function chargingLimitOutput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } if (!context->getModel().getConnector(connectorId)) { MO_DBG_ERR("could not find connector"); return; } if (chargingLimitOutput) { setSmartChargingOutput([chargingLimitOutput] (float power, float current, int nphases) -> void { chargingLimitOutput(power); }, connectorId); } else { setSmartChargingOutput(nullptr, connectorId); } if (auto scService = context->getModel().getSmartChargingService()) { if (chargingLimitOutput) { scService->updateAllowedChargingRateUnit(true, false); } else { scService->updateAllowedChargingRateUnit(false, false); } } } void setSmartChargingCurrentOutput(std::function chargingLimitOutput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } if (!context->getModel().getConnector(connectorId)) { MO_DBG_ERR("could not find connector"); return; } if (chargingLimitOutput) { setSmartChargingOutput([chargingLimitOutput] (float power, float current, int nphases) -> void { chargingLimitOutput(current); }, connectorId); } else { setSmartChargingOutput(nullptr, connectorId); } if (auto scService = context->getModel().getSmartChargingService()) { if (chargingLimitOutput) { scService->updateAllowedChargingRateUnit(false, true); } else { scService->updateAllowedChargingRateUnit(false, false); } } } void setSmartChargingOutput(std::function chargingLimitOutput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } if (!context->getModel().getConnector(connectorId)) { MO_DBG_ERR("could not find connector"); return; } auto& model = context->getModel(); if (!model.getSmartChargingService() && chargingLimitOutput) { model.setSmartChargingService(std::unique_ptr( new SmartChargingService(*context, filesystem, MO_NUMCONNECTORS))); } if (auto scService = context->getModel().getSmartChargingService()) { scService->setSmartChargingOutput(connectorId, chargingLimitOutput); if (chargingLimitOutput) { scService->updateAllowedChargingRateUnit(true, true); } else { scService->updateAllowedChargingRateUnit(false, false); } } } void setEvReadyInput(std::function evReadyInput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { if (auto txService = context->getModel().getTransactionService()) { if (auto evse = txService->getEvse(connectorId)) { evse->setEvReadyInput(evReadyInput); } } return; } #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return; } connector->setEvReadyInput(evReadyInput); } void setEvseReadyInput(std::function evseReadyInput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { if (auto txService = context->getModel().getTransactionService()) { if (auto evse = txService->getEvse(connectorId)) { evse->setEvseReadyInput(evseReadyInput); } } return; } #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return; } connector->setEvseReadyInput(evseReadyInput); } void addErrorCodeInput(std::function errorCodeInput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return; } connector->addErrorCodeInput(errorCodeInput); } void addErrorDataInput(std::function errorDataInput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return; } connector->addErrorDataInput(errorDataInput); } void addMeterValueInput(std::function valueInput, const char *measurand, const char *unit, const char *location, const char *phase, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } if (!valueInput) { MO_DBG_ERR("value undefined"); return; } if (!measurand) { measurand = "Energy.Active.Import.Register"; MO_DBG_WARN("measurand unspecified; assume %s", measurand); } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { auto& model = context->getModel(); if (!model.getMeteringServiceV201()) { model.setMeteringServiceV201(std::unique_ptr( new Ocpp201::MeteringService(context->getModel(), MO_NUM_EVSEID))); } if (auto mEvse = model.getMeteringServiceV201()->getEvse(connectorId)) { Ocpp201::SampledValueProperties properties; properties.setMeasurand(measurand); //mandatory for MO if (unit) properties.setUnitOfMeasureUnit(unit); if (location) properties.setLocation(location); if (phase) properties.setPhase(phase); mEvse->addMeterValueInput([valueInput] (ReadingContext) {return static_cast(valueInput());}, properties); } else { MO_DBG_ERR("inalid arg"); } return; } #endif SampledValueProperties properties; properties.setMeasurand(measurand); //mandatory for MO if (unit) properties.setUnit(unit); if (location) properties.setLocation(location); if (phase) properties.setPhase(phase); auto valueSampler = std::unique_ptr>>( new MicroOcpp::SampledValueSamplerConcrete>( properties, [valueInput] (ReadingContext) {return valueInput();})); addMeterValueInput(std::move(valueSampler), connectorId); } void addMeterValueInput(std::unique_ptr valueInput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { MO_DBG_ERR("addMeterValueInput(std::unique_ptr...) not compatible with v201. Use addMeterValueInput(std::function...) instead"); return; } #endif auto& model = context->getModel(); if (!model.getMeteringService()) { model.setMeteringSerivce(std::unique_ptr( new MeteringService(*context, MO_NUMCONNECTORS, filesystem))); } model.getMeteringService()->addMeterValueSampler(connectorId, std::move(valueInput)); } void setOccupiedInput(std::function occupied, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { if (auto availabilityService = context->getModel().getAvailabilityService()) { if (auto evse = availabilityService->getEvse(connectorId)) { evse->setOccupiedInput(occupied); } } return; } #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return; } connector->setOccupiedInput(occupied); } void setStartTxReadyInput(std::function startTxReady, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return; } connector->setStartTxReadyInput(startTxReady); } void setStopTxReadyInput(std::function stopTxReady, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return; } connector->setStopTxReadyInput(stopTxReady); } void setTxNotificationOutput(std::function notificationOutput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { MO_DBG_ERR("only supported in v16"); return; } #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return; } connector->setTxNotificationOutput(notificationOutput); } #if MO_ENABLE_V201 void setTxNotificationOutputV201(std::function notificationOutput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } if (context->getVersion().major != 2) { MO_DBG_ERR("only supported in v201"); return; } TransactionService::Evse *evse = nullptr; if (auto txService = context->getModel().getTransactionService()) { evse = txService->getEvse(connectorId); } if (!evse) { MO_DBG_ERR("could not find EVSE"); return; } evse->setTxNotificationOutput(notificationOutput); } #endif //MO_ENABLE_V201 #if MO_ENABLE_CONNECTOR_LOCK void setOnUnlockConnectorInOut(std::function onUnlockConnectorInOut, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); return; } connector->setOnUnlockConnector(onUnlockConnectorInOut); } #endif //MO_ENABLE_CONNECTOR_LOCK bool isOperative(unsigned int connectorId) { if (!context) { MO_DBG_WARN("OCPP uninitialized"); return true; //assume "true" as default state } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { if (auto availabilityService = context->getModel().getAvailabilityService()) { auto chargePoint = availabilityService->getEvse(OCPP_ID_OF_CP); auto connector = availabilityService->getEvse(connectorId); if (!chargePoint || !connector) { MO_DBG_ERR("could not find connector"); return true; //assume "true" as default state } return chargePoint->isAvailable() && connector->isAvailable(); } } #endif auto& model = context->getModel(); auto chargePoint = model.getConnector(OCPP_ID_OF_CP); auto connector = model.getConnector(connectorId); if (!chargePoint || !connector) { MO_DBG_ERR("could not find connector"); return true; //assume "true" as default state } return chargePoint->isOperative() && connector->isOperative(); } void setOnResetNotify(std::function onResetNotify) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { if (auto rService = context->getModel().getResetServiceV201()) { rService->setNotifyReset([onResetNotify] (ResetType) {return onResetNotify(true);}); } return; } #endif if (auto rService = context->getModel().getResetService()) { rService->setPreReset(onResetNotify); } } void setOnResetExecute(std::function onResetExecute) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } #if MO_ENABLE_V201 if (context->getVersion().major == 2) { if (auto rService = context->getModel().getResetServiceV201()) { rService->setExecuteReset([onResetExecute] () {onResetExecute(true); return true;}); } return; } #endif if (auto rService = context->getModel().getResetService()) { rService->setExecuteReset(onResetExecute); } } FirmwareService *getFirmwareService() { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return nullptr; } auto& model = context->getModel(); if (!model.getFirmwareService()) { model.setFirmwareService(std::unique_ptr( new FirmwareService(*context))); } return model.getFirmwareService(); } DiagnosticsService *getDiagnosticsService() { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return nullptr; } auto& model = context->getModel(); if (!model.getDiagnosticsService()) { model.setDiagnosticsService(std::unique_ptr( new DiagnosticsService(*context))); } return model.getDiagnosticsService(); } #if MO_ENABLE_CERT_MGMT void setCertificateStore(std::unique_ptr certStore) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } auto& model = context->getModel(); if (!model.getCertificateService()) { model.setCertificateService(std::unique_ptr( new CertificateService(*context))); } if (auto certService = model.getCertificateService()) { certService->setCertificateStore(std::move(certStore)); } else { MO_DBG_ERR("OOM"); } } #endif //MO_ENABLE_CERT_MGMT Context *getOcppContext() { return context; } void setOnReceiveRequest(const char *operationType, OnReceiveReqListener onReceiveReq) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } if (!operationType) { MO_DBG_ERR("invalid args"); return; } context->getOperationRegistry().setOnRequest(operationType, onReceiveReq); } void setOnSendConf(const char *operationType, OnSendConfListener onSendConf) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } if (!operationType) { MO_DBG_ERR("invalid args"); return; } context->getOperationRegistry().setOnResponse(operationType, onSendConf); } void sendRequest(const char *operationType, std::function ()> fn_createReq, std::function fn_processConf) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } if (!operationType || !fn_createReq || !fn_processConf) { MO_DBG_ERR("invalid args"); return; } auto request = makeRequest(new CustomOperation(operationType, fn_createReq, fn_processConf)); context->initiateRequest(std::move(request)); } void setRequestHandler(const char *operationType, std::function fn_processReq, std::function ()> fn_createConf) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } if (!operationType || !fn_processReq || !fn_createConf) { MO_DBG_ERR("invalid args"); return; } auto captureOpType = makeString("MicroOcpp.cpp", operationType); context->getOperationRegistry().registerOperation(operationType, [captureOpType, fn_processReq, fn_createConf] () { return new CustomOperation(captureOpType.c_str(), fn_processReq, fn_createConf); }); } void authorize(const char *idTag, OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, unsigned int timeout) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX) { MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); return; } auto authorize = makeRequest( new Authorize(context->getModel(), idTag)); if (onConf) authorize->setOnReceiveConfListener(onConf); if (onAbort) authorize->setOnAbortListener(onAbort); if (onTimeout) authorize->setOnTimeoutListener(onTimeout); if (onError) authorize->setOnReceiveErrorListener(onError); if (timeout) authorize->setTimeout(timeout); else authorize->setTimeout(20000); context->initiateRequest(std::move(authorize)); } bool startTransaction(const char *idTag, OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, unsigned int timeout) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX) { MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); return false; } auto connector = context->getModel().getConnector(OCPP_ID_OF_CONNECTOR); if (!connector) { MO_DBG_ERR("could not find connector"); return false; } auto transaction = connector->getTransaction(); if (transaction) { if (transaction->getStartSync().isRequested()) { MO_DBG_ERR("transaction already in progress. Must call stopTransaction()"); return false; } transaction->setIdTag(idTag); } else { beginTransaction_authorized(idTag); //request new transaction object transaction = connector->getTransaction(); if (!transaction) { MO_DBG_WARN("transaction queue full"); return false; } } if (auto mService = context->getModel().getMeteringService()) { auto meterStart = mService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext_TransactionBegin); if (meterStart && *meterStart) { transaction->setMeterStart(meterStart->toInteger()); } else { MO_DBG_ERR("meterStart undefined"); } } transaction->setStartTimestamp(context->getModel().getClock().now()); transaction->commit(); auto startTransaction = makeRequest( new StartTransaction(context->getModel(), transaction)); if (onConf) startTransaction->setOnReceiveConfListener(onConf); if (onAbort) startTransaction->setOnAbortListener(onAbort); if (onTimeout) startTransaction->setOnTimeoutListener(onTimeout); if (onError) startTransaction->setOnReceiveErrorListener(onError); if (timeout) startTransaction->setTimeout(timeout); else startTransaction->setTimeout(0); context->initiateRequest(std::move(startTransaction)); return true; } bool stopTransaction(OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, unsigned int timeout) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } auto connector = context->getModel().getConnector(OCPP_ID_OF_CONNECTOR); if (!connector) { MO_DBG_ERR("could not find connector"); return false; } auto transaction = connector->getTransaction(); if (!transaction || !transaction->isRunning()) { MO_DBG_ERR("no running Tx to stop"); return false; } connector->endTransaction(transaction->getIdTag(), "Local"); const char *idTag = transaction->getIdTag(); if (idTag) { transaction->setStopIdTag(idTag); } if (auto mService = context->getModel().getMeteringService()) { auto meterStop = mService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext_TransactionEnd); if (meterStop && *meterStop) { transaction->setMeterStop(meterStop->toInteger()); } else { MO_DBG_ERR("meterStop undefined"); } } transaction->setStopTimestamp(context->getModel().getClock().now()); transaction->commit(); auto stopTransaction = makeRequest( new StopTransaction(context->getModel(), transaction)); if (onConf) stopTransaction->setOnReceiveConfListener(onConf); if (onAbort) stopTransaction->setOnAbortListener(onAbort); if (onTimeout) stopTransaction->setOnTimeoutListener(onTimeout); if (onError) stopTransaction->setOnReceiveErrorListener(onError); if (timeout) stopTransaction->setTimeout(timeout); else stopTransaction->setTimeout(0); context->initiateRequest(std::move(stopTransaction)); return true; } ================================================ FILE: src/MicroOcpp.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_MICROOCPP_H #define MO_MICROOCPP_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using MicroOcpp::OnReceiveConfListener; using MicroOcpp::OnReceiveReqListener; using MicroOcpp::OnSendConfListener; using MicroOcpp::OnAbortListener; using MicroOcpp::OnTimeoutListener; using MicroOcpp::OnReceiveErrorListener; #ifndef MO_CUSTOM_WS //use links2004/WebSockets library /* * Initialize the library with the OCPP URL, EVSE voltage and filesystem configuration. * * If the connections fails, please refer to * https://github.com/matth-x/MicroOcpp/issues/36#issuecomment-989716573 for recommendations on * how to track down the issue with the connection. * * This is a convenience function only available for Arduino. */ void mocpp_initialize( const char *backendUrl, //e.g. "wss://example.com:8443/steve/websocket/CentralSystemService" const char *chargeBoxId, //e.g. "charger001" const char *chargePointModel = "Demo Charger", //model name of this charger const char *chargePointVendor = "My Company Ltd.", //brand name MicroOcpp::FilesystemOpt fsOpt = MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail, //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h const char *password = nullptr, //password present in the websocket message header const char *CA_cert = nullptr, //TLS certificate bool autoRecover = false); //automatically sanitize the local data store when the lib detects recurring crashes. Not recommended during development #endif /* * Convenience initialization: use this for passing the BootNotification payload JSON to the mocpp_initialize(...) below * * Example usage: * * mocpp_initialize(osock, ChargerCredentials("Demo Charger", "My Company Ltd.")); * * For a description of the fields, refer to OCPP 1.6 Specification - Edition 2 p. 60 */ struct ChargerCredentials { ChargerCredentials( const char *chargePointModel = "Demo Charger", const char *chargePointVendor = "My Company Ltd.", const char *firmwareVersion = nullptr, const char *chargePointSerialNumber = nullptr, const char *meterSerialNumber = nullptr, const char *meterType = nullptr, const char *chargeBoxSerialNumber = nullptr, const char *iccid = nullptr, const char *imsi = nullptr); /* * OCPP 2.0.1 compatible charger credentials. Use this if initializing the library with ProtocolVersion(2,0,1) */ static ChargerCredentials v201( const char *chargePointModel = "Demo Charger", const char *chargePointVendor = "My Company Ltd.", const char *firmwareVersion = nullptr, const char *chargePointSerialNumber = nullptr, const char *meterSerialNumber = nullptr, const char *meterType = nullptr, const char *chargeBoxSerialNumber = nullptr, const char *iccid = nullptr, const char *imsi = nullptr); operator const char *() {return payload;} private: char payload [512] = {'{', '}', '\0'}; }; /* * Initialize the library with a WebSocket connection which is configured with protocol=ocpp1.6 * (=Connection), EVSE voltage and filesystem configuration. This library requires that you handle * establishing the connection and keeping it alive. Please refer to * https://github.com/matth-x/MicroOcpp/tree/main/examples/ESP-TLS for an example how to use it. * * This GitHub project also delivers an Connection implementation based on links2004/WebSockets. If * you need another WebSockets implementation, you can subclass the Connection class and pass it to * this initialize() function. Please refer to * https://github.com/OpenEVSE/ESP32_WiFi_V4.x/blob/master/src/MongooseConnectionClient.cpp for * an example. */ void mocpp_initialize( MicroOcpp::Connection& connection, //WebSocket adapter for MicroOcpp const char *bootNotificationCredentials = ChargerCredentials("Demo Charger", "My Company Ltd."), //e.g. '{"chargePointModel":"Demo Charger","chargePointVendor":"My Company Ltd."}' (refer to OCPP 1.6 Specification - Edition 2 p. 60) std::shared_ptr filesystem = MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail), //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h bool autoRecover = false, //automatically sanitize the local data store when the lib detects recurring crashes. Not recommended during development MicroOcpp::ProtocolVersion version = MicroOcpp::ProtocolVersion(1,6)); /* * Stop the OCPP library and release allocated resources. */ void mocpp_deinitialize(); /* * To be called in the main loop (e.g. place it inside loop()) */ void mocpp_loop(); /* * Transaction management. * * OCPP 1.6 (2.0.1 see below): * Begin the transaction process and prepare it. When all conditions for the transaction are true, * eventually send a StartTransaction request to the OCPP server. * Conditions: * 1) the connector is operative (no faults reported, not set "Unavailable" by the backend) * 2) no reservation blocks the connector * 3) the idTag is authorized for charging. The transaction process will send an Authorize message * to the server for approval, except if the charger is offline, then the Local Authorization * rules will apply as in the specification. * 4) the vehicle is already plugged or will be plugged soon (only applicable if the * ConnectorPlugged Input is set) * * See beginTransaction_authorized for skipping steps 1) to 3) * * Returns true if it was possible to create the transaction process. Returns * false if either another transaction process is still active or you need to try it again later. * * OCPP 2.0.1: * Authorize a transaction. Like the OCPP 1.6 behavior, this should be called when the user swipes the * card to start charging, but the semantic is slightly different. This function begins the authorized * phase, but a transaction may already have started due to an earlier transaction start point. */ bool beginTransaction(const char *idTag, unsigned int connectorId = 1); /* * Begin the transaction process and skip the OCPP-side authorization. See beginTransaction(...) for a * complete description */ bool beginTransaction_authorized(const char *idTag, const char *parentIdTag = nullptr, unsigned int connectorId = 1); /* * OCPP 1.6 (2.0.1 see below): * End the transaction process if idTag is authorized to stop the transaction. The OCPP lib sends * a StopTransaction request if the following conditions are true: * Conditions: * 1) Currently, a transaction is running which hasn't been terminated yet AND * 2) idTag is either * - nullptr OR * - matches the idTag of beginTransaction (or RemoteStartTransaction) OR * - [Planned, not released yet] is part of the current LocalList and the parentIdTag * matches with the parentIdTag of beginTransaction. * - [Planned, not released yet] If none of step 2) applies, then the OCPP lib will check * the authorization status via an Authorize request * * See endTransaction_authorized for skipping the authorization check, i.e. step 2) * * If the transaction is ended by swiping an RFID card, then idTag should contain its identifier. If * charging stops for a different reason than swiping the card, idTag should be null or empty. * * Please refer to OCPP 1.6 Specification - Edition 2 p. 90 for a list of valid reasons. `reason` * can also be nullptr. * * It is safe to call this function at any time, i.e. when no transaction runs or when the transaction * has already been ended. For example you can place * `endTransaction(nullptr, "Reboot");` * in the beginning of the program just to ensure that there is no transaction from a previous run. * * If called with idTag=nullptr, this is functionally equivalent to * `endTransaction_authorized(nullptr, reason);` * * Returns true if there is a transaction which could eventually be ended by this action * * OCPP 2.0.1: * End the user authorization. Like when running with OCPP 1.6, this should be called when the user * swipes the card to stop charging. The difference between the 1.6/2.0.1 behavior is that in 1.6, * endTransaction always sets the transaction inactive so that it wants to stop. In 2.0.1, this only * revokes the user authorization which may terminate the transaction but doesn't have to if the * transaction stop point is set to EvConnected. * * Note: the stop reason parameter is ignored when running with OCPP 2.0.1. It's always Local */ bool endTransaction(const char *idTag = nullptr, const char *reason = nullptr, unsigned int connectorId = 1); /* * End the transaction process definitely without authorization check. See endTransaction(...) for a * complete description. * * Use this function if you manage authorization on your own and want to bypass the Authorization * management of this lib. */ bool endTransaction_authorized(const char *idTag, const char *reason = nullptr, unsigned int connectorId = 1); /* * Get information about the current Transaction lifecycle. A transaction can enter the following * states: * - Idle: no transaction running or being started * - Preparing: before a potential transaction * - Aborted: transaction not started and never will be started * - Running: transaction started and running * - Running/StopTxAwait: transaction still running but will end at the next possible time * - Finished: transaction stopped * * isTransactionActive() and isTransactionRunning() give the status by combining them: * * State | isTransactionActive() | isTransactionRunning() * --------------------+-----------------------+----------------------- * Preparing | true | false * Running | true | true * Running/StopTxAwait | false | true * Finished / Aborted | | * / Idle | false | false */ bool isTransactionActive(unsigned int connectorId = 1); bool isTransactionRunning(unsigned int connectorId = 1); /* * Get the idTag which has been used to start the transaction. If no transaction process is * running, this function returns nullptr */ const char *getTransactionIdTag(unsigned int connectorId = 1); /* * Returns the current transaction process. Returns nullptr if no transaction is running, preparing or finishing * * See the class definition in MicroOcpp/Model/Transactions/Transaction.h for possible uses of this object * * Examples: * auto tx = getTransaction(); //fetch tx object * if (tx) { //check if tx object exists * bool active = tx->isActive(); //active tells if the transaction is preparing or continuing to run * //inactive means that the transaction is about to stop, stopped or won't be started anymore * int transactionId = tx->getTransactionId(); //the transactionId as assigned by the OCPP server * bool deauthorized = tx->isIdTagDeauthorized(); //if StartTransaction has been rejected * } */ std::shared_ptr& getTransaction(unsigned int connectorId = 1); #if MO_ENABLE_V201 /* * OCPP 2.0.1 version of getTransaction(). Note that the return transaction object is of another type * and unlike the 1.6 version, this function does not give ownership. */ MicroOcpp::Ocpp201::Transaction *getTransactionV201(unsigned int evseId = 1); #endif //MO_ENABLE_V201 /* * Returns if the OCPP library allows the EVSE to charge at the moment. * * If you integrate it into a J1772 charger, true means that the Control Pilot can send the PWM signal * and false means that the Control Pilot must be at a DC voltage. */ bool ocppPermitsCharge(unsigned int connectorId = 1); /* * Returns the latest ChargePointStatus as reported via StatusNotification (standard OCPP data type) */ ChargePointStatus getChargePointStatus(unsigned int connectorId = 1); /* * Define the Inputs and Outputs of this library. * * This library interacts with the hardware of your charger by Inputs and Outputs. Inputs and Outputs * are tiny function-objects which read information from the EVSE or control the behavior of the EVSE. * * An Input is a function which returns the current state of a variable of the EVSE. For example, if * the energy meter stores the energy register in the global variable `e_reg`, then you can allow * this library to read it by defining the Input * `[] () {return e_reg;}` * and passing it to the library. * * An Output is a function which gets a state value from the OCPP library and applies it to the EVSE. * For example, to let Smart Charging control the PWM signal of the Control Pilot, define the Output * `[] (float p_max) {pwm = p_max / PWM_FACTOR;}` (simplified example) * and pass it to the library. * * Configure the library with Inputs and Outputs once in the setup() function. */ void setConnectorPluggedInput(std::function pluggedInput, unsigned int connectorId = 1); //Input about if an EV is plugged to this EVSE void setEnergyMeterInput(std::function energyInput, unsigned int connectorId = 1); //Input of the electricity meter register in Wh void setPowerMeterInput(std::function powerInput, unsigned int connectorId = 1); //Input of the power meter reading in W //Smart Charging Output, alternative for Watts only, Current only, or Watts x Current x numberPhases. //Only one of the Smart Charging Outputs can be set at a time. //MO will execute the callback whenever the OCPP charging limit changes and will pass the limit for now //to the callback. If OCPP does not define a limit, then MO passes the value -1 for "undefined". void setSmartChargingPowerOutput(std::function chargingLimitOutput, unsigned int connectorId = 1); //Output (in Watts) for the Smart Charging limit void setSmartChargingCurrentOutput(std::function chargingLimitOutput, unsigned int connectorId = 1); //Output (in Amps) for the Smart Charging limit void setSmartChargingOutput(std::function chargingLimitOutput, unsigned int connectorId = 1); //Output (in Watts, Amps, numberPhases) for the Smart Charging limit /* * Define the Inputs and Outputs of this library. (Advanced) * * These Inputs and Outputs are optional depending on the use case of your charger. */ void setEvReadyInput(std::function evReadyInput, unsigned int connectorId = 1); //Input if EV is ready to charge (= J1772 State C) void setEvseReadyInput(std::function evseReadyInput, unsigned int connectorId = 1); //Input if EVSE allows charge (= PWM signal on) void addErrorCodeInput(std::function errorCodeInput, unsigned int connectorId = 1); //Input for Error codes (please refer to OCPP 1.6, Edit2, p. 71 and 72 for valid error codes) void addErrorDataInput(std::function errorDataInput, unsigned int connectorId = 1); void addMeterValueInput(std::function valueInput, const char *measurand = nullptr, const char *unit = nullptr, const char *location = nullptr, const char *phase = nullptr, unsigned int connectorId = 1); //integrate further metering Inputs void addMeterValueInput(std::unique_ptr valueInput, unsigned int connectorId = 1); //integrate further metering Inputs (more extensive alternative) void setOccupiedInput(std::function occupied, unsigned int connectorId = 1); //Input if instead of Available, send StatusNotification Preparing / Finishing void setStartTxReadyInput(std::function startTxReady, unsigned int connectorId = 1); //Input if the charger is ready for StartTransaction void setStopTxReadyInput(std::function stopTxReady, unsigned int connectorId = 1); //Input if charger is ready for StopTransaction void setTxNotificationOutput(std::function notificationOutput, unsigned int connectorId = 1); //called when transaction state changes (see TxNotification for possible events). Transaction can be null #if MO_ENABLE_V201 void setTxNotificationOutputV201(std::function notificationOutput, unsigned int connectorId = 1); #endif //MO_ENABLE_V201 #if MO_ENABLE_CONNECTOR_LOCK /* * Set an InputOutput (reads and sets information at the same time) for forcing to unlock the * connector. Called as part of the OCPP operation "UnlockConnector" * Return values: * - UnlockConnectorResult_Pending if action needs more time to complete (MO will call this cb again later or eventually time out) * - UnlockConnectorResult_Unlocked if successful * - UnlockConnectorResult_UnlockFailed if not successful (e.g. lock stuck) */ void setOnUnlockConnectorInOut(std::function onUnlockConnectorInOut, unsigned int connectorId = 1); #endif //MO_ENABLE_CONNECTOR_LOCK /* * Access further information about the internal state of the library */ bool isOperative(unsigned int connectorId = 1); //if the charge point is operative (see OCPP1.6 Edit2, p. 45) and ready for transactions /* * Configure the device management */ void setOnResetNotify(std::function onResetNotify); //call onResetNotify(isHard) before Reset. If you return false, Reset will be aborted. Optional void setOnResetExecute(std::function onResetExecute); //reset handler. This function should reboot this controller immediately. Already defined for the ESP32 on Arduino namespace MicroOcpp { class FirmwareService; class DiagnosticsService; } /* * You need to configure this object if FW updates are relevant for you. This project already * brings a simple configuration for the ESP32 and ESP8266 for prototyping purposes, however * for the productive system you will have to develop a configuration targeting the specific * OCPP backend. * See MicroOcpp/Model/FirmwareManagement/FirmwareService.h * * Lazy initialization: The FW Service will be created at the first call to this function * * To use, add `#include ` */ MicroOcpp::FirmwareService *getFirmwareService(); /* * This library implements the OCPP messaging side of Diagnostics, but no logging or the * log upload to your backend. * To integrate Diagnostics, see MicroOcpp/Model/Diagnostics/DiagnosticsService.h * * Lazy initialization: The Diag Service will be created at the first call to this function * * To use, add `#include ` */ MicroOcpp::DiagnosticsService *getDiagnosticsService(); #if MO_ENABLE_CERT_MGMT /* * Set a custom Certificate Store which implements certificate updates on the host system. * MicroOcpp will forward OCPP-side update requests to the certificate store, as well as * query the certificate store upon server request. * * To enable OCPP-side certificate updates (UCs M03 - M05), set the build flag * MO_ENABLE_CERT_MGMT=1 so that this function becomes accessible. * * To use the built-in certificate store (depends on MbedTLS), set the build flag * MO_ENABLE_MBEDTLS=1. To not use the built-in implementation, but still enable MbedTLS, * additionally set MO_ENABLE_CERT_STORE_MBEDTLS=0. */ void setCertificateStore(std::unique_ptr certStore); #endif //MO_ENABLE_CERT_MGMT /* * Add features and customize the behavior of the OCPP client */ namespace MicroOcpp { class Context; } //Get access to internal functions and data structures. The returned Context object allows //you to bypass the facade functions of this header and implement custom functionality. //To use, add `#include ` MicroOcpp::Context *getOcppContext(); /* * Set a listener which is notified when the OCPP lib processes an incoming operation of type * operationType. After the operation has been interpreted, onReceiveReq will be called with * the original message from the OCPP server. * * Example usage: * * setOnReceiveRequest("SetChargingProfile", [] (JsonObject payload) { * Serial.print("[main] received charging profile for connector: "; //Arduino print function * Serial.printf("update connector %i with chargingProfileId %i\n", * payload["connectorId"], //ArduinoJson object access * payload["csChargingProfiles"]["chargingProfileId"]); * }); */ void setOnReceiveRequest(const char *operationType, OnReceiveReqListener onReceiveReq); /* * Set a listener which is notified when the OCPP lib sends the confirmation to an incoming * operation of type operation type. onSendConf will be passed the original output of the * OCPP lib. * * Example usage: * * setOnSendConf("RemoteStopTransaction", [] (JsonObject payload) -> void { * if (!strcmp(payload["status"], "Rejected")) { * //the OCPP lib rejected the RemoteStopTransaction command. In this example, the customer * //wishes to stop the running transaction in any case and to log this case * endTransaction(nullptr, "Remote"); //end transaction and send StopTransaction * Serial.println("[main] override rejected RemoteStopTransaction"); //Arduino print function * } * }); * */ void setOnSendConf(const char *operationType, OnSendConfListener onSendConf); /* * Create and send an operation without using the built-in Operation class. This function bypasses * the business logic which comes with this library. E.g. you can send unknown operations, extend * OCPP or replace parts of the business logic with custom behavior. * * Use case 1, extend the library by sending additional operations. E.g. DataTransfer: * * sendRequest("DataTransfer", [] () -> std::unique_ptr { * //will be called to create the request once this operation is being sent out * size_t capacity = JSON_OBJECT_SIZE(3) + * JSON_OBJECT_SIZE(2); //for calculating the required capacity, see https://arduinojson.org/v6/assistant/ * auto res = std::unique_ptr(new MicroOcpp::JsonDoc(capacity)); * JsonObject request = *res; * request["vendorId"] = "My company Ltd."; * request["messageId"] = "TargetValues"; * request["data"]["battery_capacity"] = 89; * request["data"]["battery_soc"] = 34; * return res; * }, [] (JsonObject response) -> void { * //will be called with the confirmation response of the server * if (!strcmp(response["status"], "Accepted")) { * //DataTransfer has been accepted * int max_energy = response["data"]["max_energy"]; * } * }); * * Use case 2, bypass the business logic of this library for custom behavior. E.g. StartTransaction: * * sendRequest("StartTransaction", [] () -> std::unique_ptr { * //will be called to create the request once this operation is being sent out * size_t capacity = JSON_OBJECT_SIZE(4); //for calculating the required capacity, see https://arduinojson.org/v6/assistant/ * auto res = std::unique_ptr(new MicroOcpp::JsonDoc(capacity)); * JsonObject request = res->to(); * request["connectorId"] = 1; * request["idTag"] = "A9C3CE1D7B71EA"; * request["meterStart"] = 1234; * request["timestamp"] = "2023-06-01T11:07:43Z"; //e.g. some historic transaction * return res; * }, [] (JsonObject response) -> void { * //will be called with the confirmation response of the server * const char *status = response["idTagInfo"]["status"]; * int transactionId = response["transactionId"]; * }); * * In Use case 2, the library won't send any further StatusNotification or StopTransaction on * its own. */ void sendRequest(const char *operationType, std::function ()> fn_createReq, std::function fn_processConf); /* * Set a custom handler for an incoming operation type. This will update the core Operation registry * of the library, potentially replacing the built-in Operation handler and bypassing the * business logic of this library. * * Note that when replacing an operation handler, the attached listeners will be reset. * * Example usage: * * setRequestHandler("DataTransfer", [] (JsonObject request) -> void { * //will be called with the request message from the server * const char *vendorId = request["vendorId"]; * const char *messageId = request["messageId"]; * int battery_capacity = request["data"]["battery_capacity"]; * int battery_soc = request["data"]["battery_soc"]; * }, [] () -> std::unique_ptr { * //will be called to create the response once this operation is being sent out * size_t capacity = JSON_OBJECT_SIZE(2) + * JSON_OBJECT_SIZE(1); //for calculating the required capacity, see https://arduinojson.org/v6/assistant/ * auto res = std::unique_ptr(new MicroOcpp::JsonDoc(capacity)); * JsonObject response = res->to(); * response["status"] = "Accepted"; * response["data"]["max_energy"] = 59; * return res; * }); */ void setRequestHandler(const char *operationType, std::function fn_processReq, std::function ()> fn_createConf); /* * Send OCPP operations manually not bypassing the internal business logic * * On receipt of the .conf() response the library calls the callback function * "OnReceiveConfListener onConf" and passes the OCPP payload to it. * * For your first EVSE integration, the `onReceiveConfListener` is probably sufficient. For * advanced EVSE projects, the other listeners likely become relevant: * - `onAbortListener`: will be called whenever the engine stops trying to finish an operation * normally which was initiated by this device. * - `onTimeoutListener`: will be executed when the operation is not answered until the timeout * expires. Note that timeouts also trigger the `onAbortListener`. * - `onReceiveErrorListener`: will be called when the Central System returns a CallError. * Again, each error also triggers the `onAbortListener`. * * The functions for sending OCPP operations are non-blocking. The program will resume immediately * with the code after with the subsequent code in any case. */ void authorize( const char *idTag, //RFID tag (e.g. ISO 14443 UID tag with 4 or 7 bytes) OnReceiveConfListener onConf = nullptr, //callback (confirmation received) OnAbortListener onAbort = nullptr, //callback (confirmation not received), optional OnTimeoutListener onTimeout = nullptr, //callback (timeout expired), optional OnReceiveErrorListener onError = nullptr, //callback (error code received), optional unsigned int timeout = 0); //custom timeout behavior, optional bool startTransaction( const char *idTag, OnReceiveConfListener onConf = nullptr, OnAbortListener onAbort = nullptr, OnTimeoutListener onTimeout = nullptr, OnReceiveErrorListener onError = nullptr, unsigned int timeout = 0); bool stopTransaction( OnReceiveConfListener onConf = nullptr, OnAbortListener onAbort = nullptr, OnTimeoutListener onTimeout = nullptr, OnReceiveErrorListener onError = nullptr, unsigned int timeout = 0); #endif ================================================ FILE: src/MicroOcpp_c.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include "MicroOcpp_c.h" #include "MicroOcpp.h" #include #include #include #include #include #include #include MicroOcpp::Connection *ocppSocket = nullptr; void ocpp_initialize(OCPP_Connection *conn, const char *chargePointModel, const char *chargePointVendor, struct OCPP_FilesystemOpt fsopt, bool autoRecover, bool ocpp201) { ocpp_initialize_full(conn, ocpp201 ? ChargerCredentials::v201(chargePointModel, chargePointVendor) : ChargerCredentials(chargePointModel, chargePointVendor), fsopt, autoRecover, ocpp201); } void ocpp_initialize_full(OCPP_Connection *conn, const char *bootNotificationCredentials, struct OCPP_FilesystemOpt fsopt, bool autoRecover, bool ocpp201) { if (!conn) { MO_DBG_ERR("conn is null"); } ocppSocket = reinterpret_cast(conn); MicroOcpp::FilesystemOpt adaptFsopt = fsopt; mocpp_initialize(*ocppSocket, bootNotificationCredentials, MicroOcpp::makeDefaultFilesystemAdapter(adaptFsopt), autoRecover, ocpp201 ? MicroOcpp::ProtocolVersion(2,0,1) : MicroOcpp::ProtocolVersion(1,6)); } void ocpp_initialize_full2(OCPP_Connection *conn, const char *bootNotificationCredentials, FilesystemAdapterC *filesystem, bool autoRecover, bool ocpp201) { if (!conn) { MO_DBG_ERR("conn is null"); } ocppSocket = reinterpret_cast(conn); mocpp_initialize(*ocppSocket, bootNotificationCredentials, *reinterpret_cast*>(filesystem), autoRecover, ocpp201 ? MicroOcpp::ProtocolVersion(2,0,1) : MicroOcpp::ProtocolVersion(1,6)); } void ocpp_deinitialize() { mocpp_deinitialize(); } bool ocpp_is_initialized() { return getOcppContext() != nullptr; } void ocpp_loop() { mocpp_loop(); } /* * Helper functions for transforming callback functions from C-style to C++style */ std::function adaptFn(InputBool fn) { return fn; } std::function adaptFn(unsigned int connectorId, InputBool_m fn) { return [fn, connectorId] () {return fn(connectorId);}; } std::function adaptFn(InputString fn) { return fn; } std::function adaptFn(unsigned int connectorId, InputString_m fn) { return [fn, connectorId] () {return fn(connectorId);}; } std::function adaptFn(InputFloat fn) { return fn; } std::function adaptFn(unsigned int connectorId, InputFloat_m fn) { return [fn, connectorId] () {return fn(connectorId);}; } std::function adaptFn(InputInt fn) { return fn; } std::function adaptFn(unsigned int connectorId, InputInt_m fn) { return [fn, connectorId] () {return fn(connectorId);}; } std::function adaptFn(OutputFloat fn) { return fn; } std::function adaptFn(OutputSmartCharging fn) { return fn; } std::function adaptFn(unsigned int connectorId, OutputSmartCharging_m fn) { return [fn, connectorId] (float power, float current, int nphases) {fn(connectorId, power, current, nphases);}; } std::function adaptFn(unsigned int connectorId, OutputFloat_m fn) { return [fn, connectorId] (float value) {return fn(connectorId, value);}; } std::function adaptFn(void (*fn)(void)) { return fn; } #ifndef MO_RECEIVE_PAYLOAD_BUFSIZE #define MO_RECEIVE_PAYLOAD_BUFSIZE 512 #endif char ocpp_recv_payload_buff [MO_RECEIVE_PAYLOAD_BUFSIZE] = {'\0'}; std::function adaptFn(OnMessage fn) { if (!fn) return nullptr; return [fn] (JsonObject payload) { auto len = serializeJson(payload, ocpp_recv_payload_buff, MO_RECEIVE_PAYLOAD_BUFSIZE); if (len <= 0) { MO_DBG_WARN("Received payload buffer exceeded. Continue without payload"); } fn(len > 0 ? ocpp_recv_payload_buff : "", len); }; } MicroOcpp::OnReceiveErrorListener adaptFn(OnCallError fn) { if (!fn) return nullptr; return [fn] (const char *code, const char *description, JsonObject details) { auto len = serializeJson(details, ocpp_recv_payload_buff, MO_RECEIVE_PAYLOAD_BUFSIZE); if (len <= 0) { MO_DBG_WARN("Received payload buffer exceeded. Continue without payload"); } fn(code, description, len > 0 ? ocpp_recv_payload_buff : "", len); }; } #if MO_ENABLE_CONNECTOR_LOCK std::function adaptFn(PollUnlockResult fn) { return [fn] () {return fn();}; } std::function adaptFn(unsigned int connectorId, PollUnlockResult_m fn) { return [fn, connectorId] () {return fn(connectorId);}; } #endif //MO_ENABLE_CONNECTOR_LOCK bool ocpp_beginTransaction(const char *idTag) { return beginTransaction(idTag); } bool ocpp_beginTransaction_m(unsigned int connectorId, const char *idTag) { return beginTransaction(idTag, connectorId); } bool ocpp_beginTransaction_authorized(const char *idTag, const char *parentIdTag) { return beginTransaction_authorized(idTag, parentIdTag); } bool ocpp_beginTransaction_authorized_m(unsigned int connectorId, const char *idTag, const char *parentIdTag) { return beginTransaction_authorized(idTag, parentIdTag, connectorId); } bool ocpp_endTransaction(const char *idTag, const char *reason) { return endTransaction(idTag, reason); } bool ocpp_endTransaction_m(unsigned int connectorId, const char *idTag, const char *reason) { return endTransaction(idTag, reason, connectorId); } bool ocpp_endTransaction_authorized(const char *idTag, const char *reason) { return endTransaction_authorized(idTag, reason); } bool ocpp_endTransaction_authorized_m(unsigned int connectorId, const char *idTag, const char *reason) { return endTransaction_authorized(idTag, reason, connectorId); } bool ocpp_isTransactionActive() { return isTransactionActive(); } bool ocpp_isTransactionActive_m(unsigned int connectorId) { return isTransactionActive(connectorId); } bool ocpp_isTransactionRunning() { return isTransactionRunning(); } bool ocpp_isTransactionRunning_m(unsigned int connectorId) { return isTransactionRunning(connectorId); } const char *ocpp_getTransactionIdTag() { return getTransactionIdTag(); } const char *ocpp_getTransactionIdTag_m(unsigned int connectorId) { return getTransactionIdTag(connectorId); } OCPP_Transaction *ocpp_getTransaction() { return ocpp_getTransaction_m(1); } OCPP_Transaction *ocpp_getTransaction_m(unsigned int connectorId) { #if MO_ENABLE_V201 { if (!getOcppContext()) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return nullptr; } if (getOcppContext()->getModel().getVersion().major == 2) { ocpp_tx_compat_setV201(true); //set the ocpp_tx C-API into v201 mode globally if (getTransactionV201(connectorId)) { return reinterpret_cast(getTransactionV201(connectorId)); } else { return nullptr; } } else { ocpp_tx_compat_setV201(false); //set the ocpp_tx C-API into v16 mode globally //continue with V16 implementation } } #endif //MO_ENABLE_V201 if (getTransaction(connectorId)) { return reinterpret_cast(getTransaction(connectorId).get()); } else { return nullptr; } } bool ocpp_ocppPermitsCharge() { return ocppPermitsCharge(); } bool ocpp_ocppPermitsCharge_m(unsigned int connectorId) { return ocppPermitsCharge(connectorId); } ChargePointStatus ocpp_getChargePointStatus() { return getChargePointStatus(); } ChargePointStatus ocpp_getChargePointStatus_m(unsigned int connectorId) { return getChargePointStatus(connectorId); } void ocpp_setConnectorPluggedInput(InputBool pluggedInput) { setConnectorPluggedInput(adaptFn(pluggedInput)); } void ocpp_setConnectorPluggedInput_m(unsigned int connectorId, InputBool_m pluggedInput) { setConnectorPluggedInput(adaptFn(connectorId, pluggedInput), connectorId); } void ocpp_setEnergyMeterInput(InputInt energyInput) { setEnergyMeterInput(adaptFn(energyInput)); } void ocpp_setEnergyMeterInput_m(unsigned int connectorId, InputInt_m energyInput) { setEnergyMeterInput(adaptFn(connectorId, energyInput), connectorId); } void ocpp_setPowerMeterInput(InputFloat powerInput) { setPowerMeterInput(adaptFn(powerInput)); } void ocpp_setPowerMeterInput_m(unsigned int connectorId, InputFloat_m powerInput) { setPowerMeterInput(adaptFn(connectorId, powerInput), connectorId); } void ocpp_setSmartChargingPowerOutput(OutputFloat maxPowerOutput) { setSmartChargingPowerOutput(adaptFn(maxPowerOutput)); } void ocpp_setSmartChargingPowerOutput_m(unsigned int connectorId, OutputFloat_m maxPowerOutput) { setSmartChargingPowerOutput(adaptFn(connectorId, maxPowerOutput), connectorId); } void ocpp_setSmartChargingCurrentOutput(OutputFloat maxCurrentOutput) { setSmartChargingCurrentOutput(adaptFn(maxCurrentOutput)); } void ocpp_setSmartChargingCurrentOutput_m(unsigned int connectorId, OutputFloat_m maxCurrentOutput) { setSmartChargingCurrentOutput(adaptFn(connectorId, maxCurrentOutput), connectorId); } void ocpp_setSmartChargingOutput(OutputSmartCharging chargingLimitOutput) { setSmartChargingOutput(adaptFn(chargingLimitOutput)); } void ocpp_setSmartChargingOutput_m(unsigned int connectorId, OutputSmartCharging_m chargingLimitOutput) { setSmartChargingOutput(adaptFn(connectorId, chargingLimitOutput), connectorId); } void ocpp_setEvReadyInput(InputBool evReadyInput) { setEvReadyInput(adaptFn(evReadyInput)); } void ocpp_setEvReadyInput_m(unsigned int connectorId, InputBool_m evReadyInput) { setEvReadyInput(adaptFn(connectorId, evReadyInput), connectorId); } void ocpp_setEvseReadyInput(InputBool evseReadyInput) { setEvseReadyInput(adaptFn(evseReadyInput)); } void ocpp_setEvseReadyInput_m(unsigned int connectorId, InputBool_m evseReadyInput) { setEvseReadyInput(adaptFn(connectorId, evseReadyInput), connectorId); } void ocpp_addErrorCodeInput(InputString errorCodeInput) { addErrorCodeInput(adaptFn(errorCodeInput)); } void ocpp_addErrorCodeInput_m(unsigned int connectorId, InputString_m errorCodeInput) { addErrorCodeInput(adaptFn(connectorId, errorCodeInput), connectorId); } void ocpp_addMeterValueInputFloat(InputFloat valueInput, const char *measurand, const char *unit, const char *location, const char *phase) { addMeterValueInput(adaptFn(valueInput), measurand, unit, location, phase, 1); } void ocpp_addMeterValueInputFloat_m(unsigned int connectorId, InputFloat_m valueInput, const char *measurand, const char *unit, const char *location, const char *phase) { addMeterValueInput(adaptFn(connectorId, valueInput), measurand, unit, location, phase, connectorId); } void ocpp_addMeterValueInputIntTx(int (*valueInput)(ReadingContext), const char *measurand, const char *unit, const char *location, const char *phase) { MicroOcpp::SampledValueProperties props; props.setMeasurand(measurand); props.setUnit(unit); props.setLocation(location); props.setPhase(phase); auto mvs = std::unique_ptr>>( new MicroOcpp::SampledValueSamplerConcrete>( props, [valueInput] (ReadingContext readingContext) {return valueInput(readingContext);} )); addMeterValueInput(std::move(mvs)); } void ocpp_addMeterValueInputIntTx_m(unsigned int connectorId, int (*valueInput)(unsigned int cId, ReadingContext), const char *measurand, const char *unit, const char *location, const char *phase) { MicroOcpp::SampledValueProperties props; props.setMeasurand(measurand); props.setUnit(unit); props.setLocation(location); props.setPhase(phase); auto mvs = std::unique_ptr>>( new MicroOcpp::SampledValueSamplerConcrete>( props, [valueInput, connectorId] (ReadingContext readingContext) {return valueInput(connectorId, readingContext);} )); addMeterValueInput(std::move(mvs), connectorId); } void ocpp_addMeterValueInput(MeterValueInput *meterValueInput) { ocpp_addMeterValueInput_m(1, meterValueInput); } void ocpp_addMeterValueInput_m(unsigned int connectorId, MeterValueInput *meterValueInput) { auto svs = std::unique_ptr( reinterpret_cast(meterValueInput)); addMeterValueInput(std::move(svs), connectorId); } #if MO_ENABLE_CONNECTOR_LOCK void ocpp_setOnUnlockConnectorInOut(PollUnlockResult onUnlockConnectorInOut) { setOnUnlockConnectorInOut(adaptFn(onUnlockConnectorInOut)); } void ocpp_setOnUnlockConnectorInOut_m(unsigned int connectorId, PollUnlockResult_m onUnlockConnectorInOut) { setOnUnlockConnectorInOut(adaptFn(connectorId, onUnlockConnectorInOut), connectorId); } #endif //MO_ENABLE_CONNECTOR_LOCK void ocpp_setStartTxReadyInput(InputBool startTxReady) { setStartTxReadyInput(adaptFn(startTxReady)); } void ocpp_setStartTxReadyInput_m(unsigned int connectorId, InputBool_m startTxReady) { setStartTxReadyInput(adaptFn(connectorId, startTxReady), connectorId); } void ocpp_setStopTxReadyInput(InputBool stopTxReady) { setStopTxReadyInput(adaptFn(stopTxReady)); } void ocpp_setStopTxReadyInput_m(unsigned int connectorId, InputBool_m stopTxReady) { setStopTxReadyInput(adaptFn(connectorId, stopTxReady), connectorId); } void ocpp_setTxNotificationOutput(void (*notificationOutput)(OCPP_Transaction*, TxNotification)) { setTxNotificationOutput([notificationOutput] (MicroOcpp::Transaction *tx, TxNotification notification) { notificationOutput(reinterpret_cast(tx), notification); }); } void ocpp_setTxNotificationOutput_m(unsigned int connectorId, void (*notificationOutput)(unsigned int, OCPP_Transaction*, TxNotification)) { setTxNotificationOutput([notificationOutput, connectorId] (MicroOcpp::Transaction *tx, TxNotification notification) { notificationOutput(connectorId, reinterpret_cast(tx), notification); }, connectorId); } void ocpp_setOccupiedInput(InputBool occupied) { setOccupiedInput(adaptFn(occupied)); } void ocpp_setOccupiedInput_m(unsigned int connectorId, InputBool_m occupied) { setOccupiedInput(adaptFn(connectorId, occupied), connectorId); } bool ocpp_isOperative() { return isOperative(); } bool ocpp_isOperative_m(unsigned int connectorId) { return isOperative(connectorId); } void ocpp_setOnResetNotify(bool (*onResetNotify)(bool)) { setOnResetNotify([onResetNotify] (bool isHard) {return onResetNotify(isHard);}); } void ocpp_setOnResetExecute(void (*onResetExecute)(bool)) { setOnResetExecute([onResetExecute] (bool isHard) {onResetExecute(isHard);}); } #if MO_ENABLE_CERT_MGMT void ocpp_setCertificateStore(ocpp_cert_store *certs) { std::unique_ptr certsCwrapper; if (certs) { certsCwrapper = MicroOcpp::makeCertificateStoreCwrapper(certs); } setCertificateStore(std::move(certsCwrapper)); } #endif //MO_ENABLE_CERT_MGMT void ocpp_setOnReceiveRequest(const char *operationType, OnMessage onRequest) { setOnReceiveRequest(operationType, adaptFn(onRequest)); } void ocpp_setOnSendConf(const char *operationType, OnMessage onConfirmation) { setOnSendConf(operationType, adaptFn(onConfirmation)); } void ocpp_authorize(const char *idTag, AuthorizeConfCallback onConfirmation, AuthorizeAbortCallback onAbort, AuthorizeTimeoutCallback onTimeout, AuthorizeErrorCallback onError, void *user_data) { auto idTag_capture = MicroOcpp::makeString("MicroOcpp_c.cpp", idTag); authorize(idTag, onConfirmation ? [onConfirmation, idTag_capture, user_data] (JsonObject payload) { auto len = serializeJson(payload, ocpp_recv_payload_buff, MO_RECEIVE_PAYLOAD_BUFSIZE); if (len <= 0) {MO_DBG_WARN("Received payload buffer exceeded. Continue without payload");} onConfirmation(idTag_capture.c_str(), len > 0 ? ocpp_recv_payload_buff : "", len, user_data); } : OnReceiveConfListener(nullptr), onAbort ? [onAbort, idTag_capture, user_data] () -> void { onAbort(idTag_capture.c_str(), user_data); } : OnAbortListener(nullptr), onTimeout ? [onTimeout, idTag_capture, user_data] () { onTimeout(idTag_capture.c_str(), user_data); } : OnTimeoutListener(nullptr), onError ? [onError, idTag_capture, user_data] (const char *code, const char *description, JsonObject details) { auto len = serializeJson(details, ocpp_recv_payload_buff, MO_RECEIVE_PAYLOAD_BUFSIZE); if (len <= 0) {MO_DBG_WARN("Received payload buffer exceeded. Continue without payload");} onError(idTag_capture.c_str(), code, description, len > 0 ? ocpp_recv_payload_buff : "", len, user_data); } : OnReceiveErrorListener(nullptr)); } void ocpp_startTransaction(const char *idTag, OnMessage onConfirmation, OnAbort onAbort, OnTimeout onTimeout, OnCallError onError) { startTransaction(idTag, adaptFn(onConfirmation), adaptFn(onAbort), adaptFn(onTimeout), adaptFn(onError)); } void ocpp_stopTransaction(OnMessage onConfirmation, OnAbort onAbort, OnTimeout onTimeout, OnCallError onError) { stopTransaction(adaptFn(onConfirmation), adaptFn(onAbort), adaptFn(onTimeout), adaptFn(onError)); } ================================================ FILE: src/MicroOcpp_c.h ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_MICROOCPP_C_H #define MO_MICROOCPP_C_H #include #include #include #include #include #include #include struct OCPP_Connection; typedef struct OCPP_Connection OCPP_Connection; struct MeterValueInput; typedef struct MeterValueInput MeterValueInput; struct FilesystemAdapterC; typedef struct FilesystemAdapterC FilesystemAdapterC; typedef void (*OnMessage) (const char *payload, size_t len); typedef void (*OnAbort) (); typedef void (*OnTimeout) (); typedef void (*OnCallError) (const char *code, const char *description, const char *details_json, size_t details_len); typedef void (*AuthorizeConfCallback) (const char *idTag, const char *payload, size_t len, void *user_data); typedef void (*AuthorizeAbortCallback) (const char *idTag, void* user_data); typedef void (*AuthorizeTimeoutCallback) (const char *idTag, void* user_data); typedef void (*AuthorizeErrorCallback) (const char *idTag, const char *code, const char *description, const char *details_json, size_t details_len, void* user_data); typedef float (*InputFloat)(); typedef float (*InputFloat_m)(unsigned int connectorId); //multiple connectors version typedef int (*InputInt)(); typedef int (*InputInt_m)(unsigned int connectorId); typedef bool (*InputBool)(); typedef bool (*InputBool_m)(unsigned int connectorId); typedef const char* (*InputString)(); typedef const char* (*InputString_m)(unsigned int connectorId); typedef void (*OutputFloat)(float limit); typedef void (*OutputFloat_m)(unsigned int connectorId, float limit); typedef void (*OutputSmartCharging)(float power, float current, int nphases); typedef void (*OutputSmartCharging_m)(unsigned int connectorId, float power, float current, int nphases); #if MO_ENABLE_CONNECTOR_LOCK typedef UnlockConnectorResult (*PollUnlockResult)(); typedef UnlockConnectorResult (*PollUnlockResult_m)(unsigned int connectorId); #endif //MO_ENABLE_CONNECTOR_LOCK #ifdef __cplusplus extern "C" { #endif /* * Please refer to MicroOcpp.h for the documentation */ void ocpp_initialize( OCPP_Connection *conn, //WebSocket adapter for MicroOcpp const char *chargePointModel, //model name of this charger (e.g. "My Charger") const char *chargePointVendor, //brand name (e.g. "My Company Ltd.") struct OCPP_FilesystemOpt fsopt, //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h bool autoRecover, //automatically sanitize the local data store when the lib detects recurring crashes. During development, `false` is recommended bool ocpp201); //true to select OCPP 2.0.1, false for OCPP 1.6 //same as above, but more fields for the BootNotification void ocpp_initialize_full( OCPP_Connection *conn, //WebSocket adapter for MicroOcpp const char *bootNotificationCredentials, //e.g. '{"chargePointModel":"Demo Charger","chargePointVendor":"My Company Ltd."}' (refer to OCPP 1.6 Specification - Edition 2 p. 60) struct OCPP_FilesystemOpt fsopt, //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h bool autoRecover, //automatically sanitize the local data store when the lib detects recurring crashes. During development, `false` is recommended bool ocpp201); //true to select OCPP 2.0.1, false for OCPP 1.6 //same as above, but pass FS handle instead of FS options void ocpp_initialize_full2( OCPP_Connection *conn, //WebSocket adapter for MicroOcpp const char *bootNotificationCredentials, //e.g. '{"chargePointModel":"Demo Charger","chargePointVendor":"My Company Ltd."}' (refer to OCPP 1.6 Specification - Edition 2 p. 60) FilesystemAdapterC *filesystem, //FilesystemAdapter handle initialized by client. MO takes ownership and deletes it during deinitialization bool autoRecover, //automatically sanitize the local data store when the lib detects recurring crashes. During development, `false` is recommended bool ocpp201); //true to select OCPP 2.0.1, false for OCPP 1.6 void ocpp_deinitialize(); bool ocpp_is_initialized(); void ocpp_loop(); /* * Charging session management */ bool ocpp_beginTransaction(const char *idTag); bool ocpp_beginTransaction_m(unsigned int connectorId, const char *idTag); //multiple connectors version bool ocpp_beginTransaction_authorized(const char *idTag, const char *parentIdTag); bool ocpp_beginTransaction_authorized_m(unsigned int connectorId, const char *idTag, const char *parentIdTag); bool ocpp_endTransaction(const char *idTag, const char *reason); //idTag, reason can be NULL bool ocpp_endTransaction_m(unsigned int connectorId, const char *idTag, const char *reason); //idTag, reason can be NULL bool ocpp_endTransaction_authorized(const char *idTag, const char *reason); //idTag, reason can be NULL bool ocpp_endTransaction_authorized_m(unsigned int connectorId, const char *idTag, const char *reason); //idTag, reason can be NULL bool ocpp_isTransactionActive(); bool ocpp_isTransactionActive_m(unsigned int connectorId); bool ocpp_isTransactionRunning(); bool ocpp_isTransactionRunning_m(unsigned int connectorId); const char *ocpp_getTransactionIdTag(); const char *ocpp_getTransactionIdTag_m(unsigned int connectorId); OCPP_Transaction *ocpp_getTransaction(); OCPP_Transaction *ocpp_getTransaction_m(unsigned int connectorId); bool ocpp_ocppPermitsCharge(); bool ocpp_ocppPermitsCharge_m(unsigned int connectorId); ChargePointStatus ocpp_getChargePointStatus(); ChargePointStatus ocpp_getChargePointStatus_m(unsigned int connectorId); /* * Define the Inputs and Outputs of this library. */ void ocpp_setConnectorPluggedInput(InputBool pluggedInput); void ocpp_setConnectorPluggedInput_m(unsigned int connectorId, InputBool_m pluggedInput); void ocpp_setEnergyMeterInput(InputInt energyInput); void ocpp_setEnergyMeterInput_m(unsigned int connectorId, InputInt_m energyInput); void ocpp_setPowerMeterInput(InputFloat powerInput); void ocpp_setPowerMeterInput_m(unsigned int connectorId, InputFloat_m powerInput); void ocpp_setSmartChargingPowerOutput(OutputFloat maxPowerOutput); void ocpp_setSmartChargingPowerOutput_m(unsigned int connectorId, OutputFloat_m maxPowerOutput); void ocpp_setSmartChargingCurrentOutput(OutputFloat maxCurrentOutput); void ocpp_setSmartChargingCurrentOutput_m(unsigned int connectorId, OutputFloat_m maxCurrentOutput); void ocpp_setSmartChargingOutput(OutputSmartCharging chargingLimitOutput); void ocpp_setSmartChargingOutput_m(unsigned int connectorId, OutputSmartCharging_m chargingLimitOutput); /* * Define the Inputs and Outputs of this library. (Advanced) */ void ocpp_setEvReadyInput(InputBool evReadyInput); void ocpp_setEvReadyInput_m(unsigned int connectorId, InputBool_m evReadyInput); void ocpp_setEvseReadyInput(InputBool evseReadyInput); void ocpp_setEvseReadyInput_m(unsigned int connectorId, InputBool_m evseReadyInput); void ocpp_addErrorCodeInput(InputString errorCodeInput); void ocpp_addErrorCodeInput_m(unsigned int connectorId, InputString_m errorCodeInput); void ocpp_addMeterValueInputFloat(InputFloat valueInput, const char *measurand, const char *unit, const char *location, const char *phase); //measurand, unit, location and phase can be NULL void ocpp_addMeterValueInputFloat_m(unsigned int connectorId, InputFloat_m valueInput, const char *measurand, const char *unit, const char *location, const char *phase); //measurand, unit, location and phase can be NULL void ocpp_addMeterValueInputIntTx(int (*valueInput)(ReadingContext), const char *measurand, const char *unit, const char *location, const char *phase); //measurand, unit, location and phase can be NULL void ocpp_addMeterValueInputIntTx_m(unsigned int connectorId, int (*valueInput)(unsigned int cId, ReadingContext), const char *measurand, const char *unit, const char *location, const char *phase); //measurand, unit, location and phase can be NULL void ocpp_addMeterValueInput(MeterValueInput *meterValueInput); //takes ownership of meterValueInput void ocpp_addMeterValueInput_m(unsigned int connectorId, MeterValueInput *meterValueInput); //takes ownership of meterValueInput void ocpp_setOccupiedInput(InputBool occupied); void ocpp_setOccupiedInput_m(unsigned int connectorId, InputBool_m occupied); void ocpp_setStartTxReadyInput(InputBool startTxReady); void ocpp_setStartTxReadyInput_m(unsigned int connectorId, InputBool_m startTxReady); void ocpp_setStopTxReadyInput(InputBool stopTxReady); void ocpp_setStopTxReadyInput_m(unsigned int connectorId, InputBool_m stopTxReady); void ocpp_setTxNotificationOutput(void (*notificationOutput)(OCPP_Transaction*, TxNotification)); void ocpp_setTxNotificationOutput_m(unsigned int connectorId, void (*notificationOutput)(unsigned int, OCPP_Transaction*, TxNotification)); #if MO_ENABLE_CONNECTOR_LOCK void ocpp_setOnUnlockConnectorInOut(PollUnlockResult onUnlockConnectorInOut); void ocpp_setOnUnlockConnectorInOut_m(unsigned int connectorId, PollUnlockResult_m onUnlockConnectorInOut); #endif //MO_ENABLE_CONNECTOR_LOCK /* * Access further information about the internal state of the library */ bool ocpp_isOperative(); bool ocpp_isOperative_m(unsigned int connectorId); void ocpp_setOnResetNotify(bool (*onResetNotify)(bool)); void ocpp_setOnResetExecute(void (*onResetExecute)(bool)); #if MO_ENABLE_CERT_MGMT void ocpp_setCertificateStore(ocpp_cert_store *certs); #endif //MO_ENABLE_CERT_MGMT void ocpp_setOnReceiveRequest(const char *operationType, OnMessage onRequest); void ocpp_setOnSendConf(const char *operationType, OnMessage onConfirmation); /* * Send OCPP operations */ void ocpp_authorize(const char *idTag, AuthorizeConfCallback onConfirmation, AuthorizeAbortCallback onAbort, AuthorizeTimeoutCallback onTimeout, AuthorizeErrorCallback onError, void *user_data); void ocpp_startTransaction(const char *idTag, OnMessage onConfirmation, OnAbort onAbort, OnTimeout onTimeout, OnCallError onError); void ocpp_stopTransaction(OnMessage onConfirmation, OnAbort onAbort, OnTimeout onTimeout, OnCallError onError); #ifdef __cplusplus } #endif #endif ================================================ FILE: tests/Api.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include "./helpers/testHelper.h" #include #define BASE_TIME "2023-01-01T00:00:00.000Z" #define SCPROFILE "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":0,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"chargingSchedule\":{\"duration\":1000000,\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"W\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3}]}}}]" TEST_CASE( "C++ API test" ) { printf("\nRun %s\n", "C++ API test"); //initialize Context with dummy socket MicroOcpp::LoopbackConnection loopback; mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); auto context = getOcppContext(); auto& model = context->getModel(); mocpp_set_timer(custom_timer_cb); model.getClock().setTime(BASE_TIME); endTransaction(); SECTION("Run all functions") { //Set all possible Inputs and outputs std::array checkpoints {false}; size_t ncheck = 0; setConnectorPluggedInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return true;}); setEnergyMeterInput([c = &checkpoints[ncheck++]] () -> float {*c = true; return 0.f;}); setPowerMeterInput([c = &checkpoints[ncheck++]] () -> float {*c = true; return 0.f;}); setSmartChargingPowerOutput([] (float) {}); //overridden by CurrentOutput setSmartChargingCurrentOutput([] (float) {}); //overridden by generic SmartChargingOutput setSmartChargingOutput([c = &checkpoints[ncheck++]] (float, float, int) {*c = true;}); setEvReadyInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return true;}); setEvseReadyInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return true;}); addErrorCodeInput([c = &checkpoints[ncheck++]] () -> const char* {*c = true; return nullptr;}); addErrorDataInput([c = &checkpoints[ncheck++]] () -> MicroOcpp::ErrorData {*c = true; return nullptr;}); addMeterValueInput([c = &checkpoints[ncheck++]] () -> float {*c = true; return 0.f;}, "Current.Import"); MicroOcpp::SampledValueProperties svprops; svprops.setMeasurand("Current.Offered"); auto valueSampler = std::unique_ptr>>( new MicroOcpp::SampledValueSamplerConcrete>( svprops, [c = &checkpoints[ncheck++]] (ReadingContext) -> int32_t {*c = true; return 0;})); addMeterValueInput(std::move(valueSampler)); setOccupiedInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return false;}); setStartTxReadyInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return true;}); setStopTxReadyInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return true;}); setTxNotificationOutput([c = &checkpoints[ncheck++]] (MicroOcpp::Transaction*, TxNotification) {*c = true;}); #if MO_ENABLE_CONNECTOR_LOCK setOnUnlockConnectorInOut([c = &checkpoints[ncheck++]] () -> UnlockConnectorResult {*c = true; return UnlockConnectorResult_Unlocked;}); #endif //MO_ENABLE_CONNECTOR_LOCK setOnResetNotify([c = &checkpoints[ncheck++]] (bool) -> bool {*c = true; return true;}); setOnResetExecute([c = &checkpoints[ncheck++]] (bool) {*c = true;}); REQUIRE( getFirmwareService() != nullptr ); REQUIRE( getDiagnosticsService() != nullptr ); REQUIRE( getOcppContext() != nullptr ); setOnReceiveRequest("StatusNotification", [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); setOnSendConf("StatusNotification", [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); sendRequest("DataTransfer", [c = &checkpoints[ncheck++]] () { *c = true; auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); doc->to(); return doc; }, [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); setRequestHandler("DataTransfer", [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}, [c = &checkpoints[ncheck++]] () { *c = true; auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); doc->to(); return doc; }); //set configuration which uses all Inputs and Outputs auto MeterValuesSampledDataString = MicroOcpp::declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); MeterValuesSampledDataString->setString("Energy.Active.Import.Register,Power.Active.Import,Current.Import,Current.Offered"); loopback.sendTXT(SCPROFILE, strlen(SCPROFILE)); //run tx management mocpp_loop(); loop(); beginTransaction("mIdTag"); loop(); REQUIRE(isTransactionActive()); REQUIRE(isTransactionRunning()); REQUIRE(getTransactionIdTag() != nullptr); REQUIRE(getTransaction() != nullptr); REQUIRE(ocppPermitsCharge()); endTransaction(); loop(); beginTransaction_authorized("mIdTag"); loop(); mtime += 3600 * 1000; loop(); endTransaction(); loop(); authorize("mIdTag", [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); startTransaction("mIdTag", [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); loop(); stopTransaction([c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); //occupied Input will be validated when vehiclePlugged is false or undefined setConnectorPluggedInput(nullptr); loop(); //run device management REQUIRE(isOperative()); sendRequest("UnlockConnector", [] () { auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); (*doc)["connectorId"] = 1; return doc; }, [] (JsonObject) {}); sendRequest("Reset", [] () { auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); (*doc)["type"] = "Hard"; return doc; }, [] (JsonObject) {}); loop(); mtime += 3600 * 1000; loop(); MO_DBG_DEBUG("added %zu checkpoints", ncheck); bool checkpointsPassed = true; for (unsigned int i = 0; i < ncheck; i++) { if (!checkpoints[i]) { MO_DBG_ERR("missed checkpoint %u", i); checkpointsPassed = false; } } REQUIRE(checkpointsPassed); } mocpp_deinitialize(); REQUIRE(!getOcppContext()); } #include #include std::array checkpointsc {false}; size_t ncheckc = 0; TEST_CASE( "C API test" ) { //initialize Context with dummy socket struct OCPP_FilesystemOpt fsopt; fsopt.use = true; fsopt.mount = true; fsopt.formatFsOnFail = true; MicroOcpp::LoopbackConnection loopback; ocpp_initialize(reinterpret_cast(&loopback), "test-runner1234", "vendor", fsopt, false, false); auto context = getOcppContext(); auto& model = context->getModel(); mocpp_set_timer(custom_timer_cb); model.getClock().setTime(BASE_TIME); ocpp_endTransaction(NULL, NULL); SECTION("Run all functions") { ocpp_setConnectorPluggedInput([] () -> bool {checkpointsc[0] = true; return true;}); ncheckc++; ocpp_setConnectorPluggedInput_m(2, [] (unsigned int) -> bool {checkpointsc[1] = true; return true;}); ncheckc++; ocpp_setEnergyMeterInput([] () -> int {checkpointsc[2] = true; return 0;}); ncheckc++; ocpp_setEnergyMeterInput_m(2, [] (unsigned int) -> int {checkpointsc[3] = true; return 0;}); ncheckc++; ocpp_setPowerMeterInput([] () -> float {checkpointsc[4] = true; return 0.f;}); ncheckc++; ocpp_setPowerMeterInput_m(2, [] (unsigned int) -> float {checkpointsc[5] = true; return 0.f;}); ncheckc++; ocpp_setSmartChargingPowerOutput([] (float) {}); //overridden by CurrentOutput ocpp_setSmartChargingPowerOutput_m(2, [] (unsigned int, float) {}); //overridden by CurrentOutput ocpp_setSmartChargingCurrentOutput([] (float) {}); //overridden by generic SmartChargingOutput ocpp_setSmartChargingCurrentOutput_m(2, [] (unsigned int, float) {}); //overridden by generic SmartChargingOutput ocpp_setSmartChargingOutput([] (float, float, int) {checkpointsc[6] = true;}); ncheckc++; ocpp_setSmartChargingOutput_m(2, [] (unsigned int, float, float, int) {checkpointsc[7] = true;}); ncheckc++; ocpp_setEvReadyInput([] () -> bool {checkpointsc[8] = true; return true;}); ncheckc++; ocpp_setEvReadyInput_m(2, [] (unsigned int) -> bool {checkpointsc[9] = true; return true;}); ncheckc++; ocpp_setEvseReadyInput([] () -> bool {checkpointsc[10] = true; return true;}); ncheckc++; ocpp_setEvseReadyInput_m(2, [] (unsigned int) -> bool {checkpointsc[11] = true; return true;}); ncheckc++; ocpp_addErrorCodeInput([] () -> const char* {checkpointsc[12] = true; return nullptr;}); ncheckc++; ocpp_addErrorCodeInput_m(2, [] (unsigned int) -> const char* {checkpointsc[13] = true; return nullptr;}); ncheckc++; ocpp_addMeterValueInputFloat([] () -> float {checkpointsc[14] = true; return 0.f;}, "Current.Import", "A", NULL, NULL); ncheckc++; ocpp_addMeterValueInputFloat_m(2, [] (unsigned int) -> float {checkpointsc[15] = true; return 0.f;}, "Current.Import", "A", NULL, NULL); ncheckc++; MicroOcpp::SampledValueProperties svprops; svprops.setMeasurand("Current.Offered"); auto valueSampler = std::unique_ptr>>( new MicroOcpp::SampledValueSamplerConcrete>( svprops, [] (ReadingContext) -> int32_t {checkpointsc[16] = true; return 0;})); ncheckc++; ocpp_addMeterValueInput(reinterpret_cast(valueSampler.release())); valueSampler = std::unique_ptr>>( new MicroOcpp::SampledValueSamplerConcrete>( svprops, [] (ReadingContext) -> int32_t {checkpointsc[17] = true; return 0;})); ncheckc++; ocpp_addMeterValueInput_m(2, reinterpret_cast(valueSampler.release())); ocpp_setOccupiedInput([] () -> bool {checkpointsc[18] = true; return true;}); ncheckc++; ocpp_setOccupiedInput_m(2, [] (unsigned int) -> bool {checkpointsc[19] = true; return true;}); ncheckc++; ocpp_setStartTxReadyInput([] () -> bool {checkpointsc[20] = true; return true;}); ncheckc++; ocpp_setStartTxReadyInput_m(2, [] (unsigned int) -> bool {checkpointsc[21] = true; return true;}); ncheckc++; ocpp_setStopTxReadyInput([] () -> bool {checkpointsc[22] = true; return true;}); ncheckc++; ocpp_setStopTxReadyInput_m(2, [] (unsigned int) -> bool {checkpointsc[23] = true; return true;}); ncheckc++; ocpp_setTxNotificationOutput([] (OCPP_Transaction*, TxNotification) {checkpointsc[24] = true;}); ncheckc++; ocpp_setTxNotificationOutput_m(2, [] (unsigned int, OCPP_Transaction*, TxNotification) {checkpointsc[25] = true;}); ncheckc++; #if MO_ENABLE_CONNECTOR_LOCK ocpp_setOnUnlockConnectorInOut([] () -> UnlockConnectorResult {checkpointsc[26] = true; return UnlockConnectorResult_Unlocked;}); ncheckc++; ocpp_setOnUnlockConnectorInOut_m(2, [] (unsigned int) -> UnlockConnectorResult {checkpointsc[27] = true; return UnlockConnectorResult_Unlocked;}); ncheckc++; #else checkpointsc[26] = true; checkpointsc[27] = true; #endif //MO_ENABLE_CONNECTOR_LOCK ocpp_setOnResetNotify([] (bool) -> bool {checkpointsc[28] = true; return true;}); ncheckc++; ocpp_setOnResetExecute([] (bool) {checkpointsc[29] = true;}); ncheckc++; ocpp_setOnReceiveRequest("StatusNotification", [] (const char*,size_t) {checkpointsc[30] = true;}); ncheckc++; ocpp_setOnSendConf("StatusNotification", [] (const char*,size_t) {checkpointsc[31] = true;}); ncheckc++; //set configuration which uses all Inputs and Outputs auto MeterValuesSampledDataString = MicroOcpp::declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); MeterValuesSampledDataString->setString("Energy.Active.Import.Register,Power.Active.Import,Current.Import,Current.Offered"); loopback.sendTXT(SCPROFILE, strlen(SCPROFILE)); //run tx management ocpp_loop(); loop(); ocpp_beginTransaction("mIdTag"); ocpp_beginTransaction_m(2, "mIdTag"); loop(); REQUIRE(ocpp_isTransactionActive()); REQUIRE(ocpp_isTransactionActive_m(2)); REQUIRE(ocpp_isTransactionRunning()); REQUIRE(ocpp_isTransactionRunning_m(2)); REQUIRE(ocpp_getTransactionIdTag() != nullptr); REQUIRE(ocpp_getTransactionIdTag_m(2) != nullptr); REQUIRE(ocpp_getTransaction() != nullptr); REQUIRE(ocpp_getTransaction_m(2) != nullptr); REQUIRE(ocpp_ocppPermitsCharge()); REQUIRE(ocpp_ocppPermitsCharge_m(2)); ocpp_endTransaction("mIdTag", NULL); ocpp_endTransaction_m(2, "mIdTag", NULL); loop(); ocpp_beginTransaction_authorized("mIdTag", NULL); ocpp_beginTransaction_authorized_m(2, "mIdTag", NULL); loop(); mtime += 3600 * 1000; loop(); ocpp_endTransaction_authorized(NULL, NULL); ocpp_endTransaction_authorized_m(2, NULL, NULL); loop(); ocpp_authorize("mIdTag", [] (const char*,const char*,size_t,void*) {checkpointsc[32] = true;}, NULL, NULL, NULL, NULL); ncheckc++; ocpp_startTransaction("mIdTag", [] (const char*,size_t) {checkpointsc[33] = true;}, NULL, NULL, NULL); ncheckc++; loop(); ocpp_stopTransaction([] (const char*,size_t) {checkpointsc[34] = true;}, NULL, NULL, NULL); ncheckc++; //occupied Input will be validated when vehiclePlugged is false or undefined ocpp_setConnectorPluggedInput([] () {return false;}); ocpp_setConnectorPluggedInput_m(2,[] (unsigned int) {return false;}); loop(); //run device management REQUIRE(ocpp_isOperative()); REQUIRE(ocpp_isOperative_m(2)); sendRequest("UnlockConnector", [] () { auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); (*doc)["connectorId"] = 1; return doc; }, [] (JsonObject) {}); sendRequest("UnlockConnector", [] () { auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); (*doc)["connectorId"] = 2; return doc; }, [] (JsonObject) {}); sendRequest("Reset", [] () { auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); (*doc)["type"] = "Hard"; return doc; }, [] (JsonObject) {}); loop(); mtime += 3600 * 1000; loop(); MO_DBG_DEBUG("added %zu checkpoints", ncheckc); bool checkpointsPassed = true; for (unsigned int i = 0; i < ncheckc; i++) { if (!checkpointsc[i]) { MO_DBG_ERR("missed checkpoint %u", i); checkpointsPassed = false; } } REQUIRE(checkpointsPassed); } ocpp_deinitialize(); REQUIRE(!getOcppContext()); } ================================================ FILE: tests/Boot.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "./helpers/testHelper.h" #define CHARGEPOINTMODEL "Test model" #define CHARGEPOINTVENDOR "Test vendor" #define BASE_TIME "2023-01-01T00:00:00.000Z" #define GET_CONFIGURATION "[2,\"msgId01\",\"GetConfiguration\",{\"key\":[]}]" #define TRIGGER_MESSAGE "[2,\"msgId02\",\"TriggerMessage\",{\"requestedMessage\":\"TriggeredOperation\"}]" using namespace MicroOcpp; //dummy operation type to test TriggerMessage class TriggeredOperation : public Operation { private: bool& checkExecuted; public: TriggeredOperation(bool& checkExecuted) : checkExecuted(checkExecuted) { } const char* getOperationType() override {return "TriggeredOperation";} std::unique_ptr createReq() override { checkExecuted = true; return createEmptyDocument(); } void processConf(JsonObject) override {} void processReq(JsonObject) override {} std::unique_ptr createConf() override {return createEmptyDocument();} }; TEST_CASE( "Boot Behavior" ) { printf("\nRun %s\n", "Boot Behavior"); //clean state auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_initialize(loopback, ChargerCredentials(CHARGEPOINTMODEL, CHARGEPOINTVENDOR), filesystem); mocpp_set_timer(custom_timer_cb); SECTION("BootNotification - Accepted") { bool checkProcessed = false; getOcppContext()->getOperationRegistry().registerOperation("BootNotification", [&checkProcessed] () { return new Ocpp16::CustomOperation("BootNotification", [ &checkProcessed] (JsonObject payload) { //process req checkProcessed = true; REQUIRE( !strcmp(payload["chargePointModel"] | "_Undefined", CHARGEPOINTMODEL) ); REQUIRE( !strcmp(payload["chargePointVendor"] | "_Undefined", CHARGEPOINTVENDOR) ); }, [] () { //create conf auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024); (*conf)["currentTime"] = BASE_TIME; (*conf)["interval"] = 3600; (*conf)["status"] = "Accepted"; return conf; }); }); loop(); REQUIRE(checkProcessed); REQUIRE(getOcppContext()->getModel().getClock().now() >= MIN_TIME); } SECTION("BootNotification - Pending") { MO_DBG_INFO("Queue messages before BootNotification to see if they come through"); loop(); //normal BootNotification run REQUIRE( isOperative() ); //normal BN succeeded loopback.setConnected( false ); beginTransaction_authorized("mIdTag"); loop(); endTransaction(); mocpp_deinitialize(); loopback.setConnected( true ); MO_DBG_INFO("Start charger again with queued transaction messages, also init non-tx-related msg, but now delay BN procedure"); mocpp_initialize(loopback, ChargerCredentials()); getOcppContext()->getOperationRegistry().registerOperation("BootNotification", [] () { return new Ocpp16::CustomOperation("BootNotification", [] (JsonObject payload) { //ignore req }, [] () { //create conf auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024); (*conf)["currentTime"] = BASE_TIME; (*conf)["interval"] = 3600; (*conf)["status"] = "Pending"; return conf; }); }); bool sentTxMsg = false; getOcppContext()->getOperationRegistry().setOnRequest("StartTransaction", [&sentTxMsg] (JsonObject) { sentTxMsg = true; }); getOcppContext()->getOperationRegistry().setOnRequest("StopTransaction", [&sentTxMsg] (JsonObject) { sentTxMsg = true; }); bool checkProcessedHeartbeat = false; auto heartbeat = makeRequest(new Ocpp16::CustomOperation( "Heartbeat", [] () { //create req return createEmptyDocument();}, [&checkProcessedHeartbeat] (JsonObject) { //process conf checkProcessedHeartbeat = true; })); heartbeat->setTimeout(0); //disable timeout and check if message will be sent later getOcppContext()->initiateRequest(std::move(heartbeat)); bool sentNonTxMsg = false; getOcppContext()->getOperationRegistry().setOnRequest("Heartbeat", [&sentNonTxMsg] (JsonObject) { sentNonTxMsg = true; }); loop(); REQUIRE( !sentTxMsg ); REQUIRE( !sentNonTxMsg ); REQUIRE( !checkProcessedHeartbeat ); MO_DBG_INFO("Check if charger still responds to server-side messages and executes TriggerMessages"); bool reactedToServerMsg = false; getOcppContext()->getOperationRegistry().setOnRequest("GetConfiguration", [&reactedToServerMsg] (JsonObject) { reactedToServerMsg = true; }); loopback.sendTXT(GET_CONFIGURATION, sizeof(GET_CONFIGURATION) - 1); loop(); REQUIRE( reactedToServerMsg ); bool executedTriggerMessage = false; getOcppContext()->getOperationRegistry().registerOperation("TriggeredOperation", [&executedTriggerMessage] () {return new TriggeredOperation(executedTriggerMessage);}); loopback.sendTXT(TRIGGER_MESSAGE, sizeof(TRIGGER_MESSAGE) - 1); loop(); REQUIRE( executedTriggerMessage ); //other messages still didn't get through? REQUIRE( !sentTxMsg ); REQUIRE( !sentNonTxMsg ); REQUIRE( !checkProcessedHeartbeat ); MO_DBG_INFO("Now, accept BN and check if all queued messages finally arrive"); getOcppContext()->getOperationRegistry().registerOperation("BootNotification", [] () { return new Ocpp16::CustomOperation("BootNotification", [] (JsonObject payload) { //ignore req }, [] () { //create conf auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024); (*conf)["currentTime"] = BASE_TIME; (*conf)["interval"] = 3600; (*conf)["status"] = "Accepted"; return conf; }); }); mtime += 3600 * 1000; loop(); REQUIRE( sentTxMsg ); REQUIRE( sentNonTxMsg ); REQUIRE( checkProcessedHeartbeat ); } SECTION("PreBoot transactions") { declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true)->setBool(true); declareConfiguration("AllowOfflineTxForUnknownId", true)->setBool(true); unsigned int startTxCount = 0; getOcppContext()->getOperationRegistry().setOnRequest("StartTransaction", [&startTxCount] (JsonObject) { startTxCount++; }); //start one transaction in full offline mode loopback.setConnected( false ); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); beginTransaction("mIdTag"); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); endTransaction("mIdTag"); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); //start another transaction while BN is pending getOcppContext()->getOperationRegistry().registerOperation("BootNotification", [] () { return new Ocpp16::CustomOperation("BootNotification", [] (JsonObject payload) { //ignore req }, [] () { //create conf auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024); (*conf)["currentTime"] = BASE_TIME; (*conf)["interval"] = 3600; (*conf)["status"] = "Pending"; return conf; }); }); loopback.setConnected( true ); loop(); REQUIRE( startTxCount == 0 ); beginTransaction("mIdTag2"); mtime += 20 * 1000; loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); endTransaction(); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); REQUIRE( startTxCount == 0 ); //Now, accept BN and check again getOcppContext()->getOperationRegistry().registerOperation("BootNotification", [] () { return new Ocpp16::CustomOperation("BootNotification", [] (JsonObject payload) { //ignore req }, [] () { //create conf auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024); (*conf)["currentTime"] = BASE_TIME; (*conf)["interval"] = 3600; (*conf)["status"] = "Accepted"; return conf; }); }); mtime += 3600 * 1000; loop(); REQUIRE( startTxCount == 2 ); } SECTION("Auto recovery") { //start transaction which will persist a few boot cycles, but then will be wiped by auto recovery loop(); beginTransaction("mIdTag"); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); declareConfiguration("keepConfigOverRecovery", "originalVal"); configuration_save(); mocpp_deinitialize(); //MO has 2 unexpected power cycles. Probably just back luck - keep the local state and configuration //Increase the power cycle counter manually because it's not possible to interrupt the MO lifecycle during unit tests BootStats bootstats; BootService::loadBootStats(filesystem, bootstats); bootstats.bootNr += 2; BootService::storeBootStats(filesystem, bootstats); mocpp_initialize(loopback, ChargerCredentials(), filesystem, /*enable auto recovery*/ true); BootService::loadBootStats(filesystem, bootstats); REQUIRE( bootstats.getBootFailureCount() == 2 + 1 ); //two boot failures have been measured, +1 because each power cycle is counted as potentially failing until reaching the long runtime barrier loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); REQUIRE( !strcmp(declareConfiguration("keepConfigOverRecovery", "otherVal")->getString(), "originalVal") ); //check that the power cycle counter has been updated properly after the controller has been running stable over a long time mtime += MO_BOOTSTATS_LONGTIME_MS; loop(); BootService::loadBootStats(filesystem, bootstats); REQUIRE( bootstats.getBootFailureCount() == 0 ); mocpp_deinitialize(); //MO has 10 power cycles without running for at least 3 minutes and wipes the local state, but keeps the configuration BootStats bootstats2; BootService::loadBootStats(filesystem, bootstats2); bootstats2.bootNr += 10; BootService::storeBootStats(filesystem, bootstats2); mocpp_initialize(loopback, ChargerCredentials(), filesystem, /*enable auto recovery*/ true); REQUIRE( !strcmp(declareConfiguration("keepConfigOverRecovery", "otherVal")->getString(), "originalVal") ); BootStats bootstats3; BootService::loadBootStats(filesystem, bootstats3); REQUIRE( bootstats3.getBootFailureCount() == 0 + 1 ); //failure count is reset, but +1 because each power cycle is counted as potentially failing until reaching the long runtime barrier loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); } SECTION("Migration") { //migration removes files from previous MO versions which were running on the controller. This includes the //transaction cache, but configs are preserved auto old_opstore = filesystem->open(MO_FILENAME_PREFIX "opstore.jsn", "w"); //the opstore has been removed in MO v1.2.0 old_opstore->write("example content", sizeof("example content") - 1); old_opstore.reset(); //flushes the file loop(); beginTransaction("mIdTag"); //tx store will also be removed auto tx = getTransaction(); auto txNr = tx->getTxNr(); //remember this for later usage tx.reset(); //reset this smart pointer loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); endTransaction(); loop(); REQUIRE( getOcppContext()->getModel().getTransactionStore()->getTransaction(1, txNr) != nullptr ); //tx exists on flash declareConfiguration("keepConfigOverMigration", "originalVal"); //migration keeps configs configuration_save(); mocpp_deinitialize(); //After a FW update, the tracked version number has changed BootStats bootstats; BootService::loadBootStats(filesystem, bootstats); snprintf(bootstats.microOcppVersion, sizeof(bootstats.microOcppVersion), "oldFwVers"); BootService::storeBootStats(filesystem, bootstats); mocpp_initialize(loopback, ChargerCredentials(), filesystem); //MO migrates here size_t msize = 0; REQUIRE( filesystem->stat(MO_FILENAME_PREFIX "opstore.jsn", &msize) != 0 ); //opstore has been removed REQUIRE( getOcppContext()->getModel().getTransactionStore()->getTransaction(1, txNr) == nullptr ); //tx history entry has been removed REQUIRE( !strcmp(declareConfiguration("keepConfigOverMigration", "otherVal")->getString(), "originalVal") ); //config has been preserved } SECTION("Clean unused configs") { declareConfiguration("neverDeclaredInsideMO", "originalVal"); //unused configs will be cleared automatically after the controller has been running for a long time configuration_save(); mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials(), filesystem); //all configs are loaded here, including the test config of this section loop(); //unused configs will be cleared automatically after long time mtime += MO_BOOTSTATS_LONGTIME_MS; loop(); REQUIRE( !strcmp(declareConfiguration("neverDeclaredInsideMO", "newVal")->getString(), "newVal") ); //config has been removed } SECTION("Boot with v201") { mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials::v201(CHARGEPOINTMODEL, CHARGEPOINTVENDOR), filesystem, false, ProtocolVersion(2,0,1)); bool checkProcessed = false; getOcppContext()->getOperationRegistry().registerOperation("BootNotification", [&checkProcessed] () { return new Ocpp16::CustomOperation("BootNotification", [ &checkProcessed] (JsonObject payload) { //process req checkProcessed = true; REQUIRE( !strcmp(payload["reason"] | "_Undefined", "PowerUp") ); REQUIRE( !strcmp(payload["chargingStation"]["model"] | "_Undefined", CHARGEPOINTMODEL) ); REQUIRE( !strcmp(payload["chargingStation"]["vendorName"] | "_Undefined", CHARGEPOINTVENDOR) ); }, [] () { //create conf auto conf = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(3)); (*conf)["currentTime"] = BASE_TIME; (*conf)["interval"] = 3600; (*conf)["status"] = "Accepted"; return conf; }); }); MO_MEM_RESET(); loop(); REQUIRE(checkProcessed); REQUIRE(getOcppContext()->getModel().getClock().now() >= MIN_TIME); MO_MEM_PRINT_STATS(); MO_MEM_RESET(); mtime += 3600 * 1000; loop(); MO_DBG_INFO("Memory requirements UC G02:"); MO_MEM_PRINT_STATS(); } mocpp_deinitialize(); } ================================================ FILE: tests/Certificates.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_MBEDTLS #include #include #include #include "./helpers/testHelper.h" #include #include #include #include #include #include #include #include #define BASE_TIME "2023-01-01T00:00:00.000Z" //ISRG Root X1 const char *root_cert = R"(-----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= -----END CERTIFICATE----- )"; //precomputed identifiers of root cert above, based on Open Certificate Status Protocol (OCSP) const char *root_cert_hash_algorithm = "SHA256"; //algorithm used for the following hashes const char *root_cert_hash_issuer_name = "F6DB2FBD9DD85D9259DDB3C6DE7D7B2FEC3F3E0CEF1761BCBF3320571E2D30F8"; const char *root_cert_hash_issuer_key = "F4593A1E07CC9CCEFFBED9C11DC5218356F7814D9B22949DE745E629990C6C60"; const char *root_cert_hash_serial_number = "8210CFB0D240E3594463E0BB63828B00"; using namespace MicroOcpp; TEST_CASE( "M - Certificates" ) { printf("\nRun %s\n", "M - Certificates"); //clean state auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_set_timer(custom_timer_cb); mocpp_initialize(loopback, ChargerCredentials("test-runner")); auto& model = getOcppContext()->getModel(); auto certService = model.getCertificateService(); SECTION("CertificateService initialized") { REQUIRE(certService != nullptr); } auto certs = certService->getCertificateStore(); SECTION("CertificateStore initialized") { REQUIRE(certs != nullptr); } auto connector = model.getConnector(1); model.getClock().setTime(BASE_TIME); loop(); SECTION("M05 Install CA cert -- sent cert is valid") { auto ret = certs->installCertificate(InstallCertificateType_CSMSRootCertificate, root_cert); REQUIRE(ret == InstallCertificateStatus_Accepted); size_t msize; char fn [MO_MAX_PATH_SIZE]; printCertFn(MO_CERT_FN_CSMS_ROOT, 0, fn, MO_MAX_PATH_SIZE); REQUIRE(filesystem->stat(fn, &msize) == 0); REQUIRE(msize == strlen(root_cert)); } SECTION("M03 Retrieve list of available certs -- one cert available") { auto ret1 = certs->installCertificate(InstallCertificateType_CSMSRootCertificate, root_cert); REQUIRE(ret1 == InstallCertificateStatus_Accepted); auto chain = makeVector("UnitTests"); auto ret2 = certs->getCertificateIds({GetCertificateIdType_CSMSRootCertificate}, chain); REQUIRE(ret2 == GetInstalledCertificateStatus_Accepted); REQUIRE(chain.size() == 1); auto& chainElem = chain.front(); REQUIRE(chainElem.certificateType == GetCertificateIdType_CSMSRootCertificate); auto& certHash = chainElem.certificateHashData; REQUIRE(!strcmp(HashAlgorithmLabel(certHash.hashAlgorithm), root_cert_hash_algorithm)); //if this fails, please update the precomputed test hashes char buf [MO_CERT_HASH_ISSUER_NAME_KEY_SIZE]; ocpp_cert_print_issuerNameHash(&certHash, buf, sizeof(buf)); REQUIRE(!strcmp(buf, root_cert_hash_issuer_name)); ocpp_cert_print_issuerKeyHash(&certHash, buf, sizeof(buf)); REQUIRE(!strcmp(buf, root_cert_hash_issuer_key)); ocpp_cert_print_serialNumber(&certHash, buf, sizeof(buf)); REQUIRE(!strcmp(buf, root_cert_hash_serial_number)); REQUIRE(chainElem.childCertificateHashData.empty()); //no sub certs sent } SECTION("M04 Delete a specific cert -- specified cert exists") { auto ret1 = certs->installCertificate(InstallCertificateType_CSMSRootCertificate, root_cert); REQUIRE(ret1 == InstallCertificateStatus_Accepted); auto chain = makeVector("UnitTests"); auto ret2 = certs->getCertificateIds({GetCertificateIdType_CSMSRootCertificate}, chain); REQUIRE(ret2 == GetInstalledCertificateStatus_Accepted); REQUIRE(chain.size() == 1); auto ret3 = certs->deleteCertificate(chain.front().certificateHashData); REQUIRE(ret3 == DeleteCertificateStatus_Accepted); ret2 = certs->getCertificateIds({GetCertificateIdType_CSMSRootCertificate}, chain); REQUIRE(ret2 == GetInstalledCertificateStatus_NotFound); REQUIRE(chain.size() == 0); size_t msize; char fn [MO_MAX_PATH_SIZE]; printCertFn(MO_CERT_FN_CSMS_ROOT, 0, fn, MO_MAX_PATH_SIZE); REQUIRE(filesystem->stat(fn, &msize) != 0); } SECTION("M05 InstallCertificate operation") { bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "InstallCertificate", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["certificateType"] = "CSMSRootCertificate"; //of InstallCertificateTypeEnumType payload["certificate"] = root_cert; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); size_t msize; char fn [MO_MAX_PATH_SIZE]; printCertFn(MO_CERT_FN_CSMS_ROOT, 0, fn, MO_MAX_PATH_SIZE); REQUIRE(filesystem->stat(fn, &msize) == 0); REQUIRE(msize == strlen(root_cert)); } SECTION("M04 DeleteCertificate operation") { auto ret = certs->installCertificate(InstallCertificateType_CSMSRootCertificate, root_cert); REQUIRE(ret == InstallCertificateStatus_Accepted); bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "DeleteCertificate", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(4)); auto payload = doc->to(); payload["certificateHashData"]["hashAlgorithm"] = root_cert_hash_algorithm; //of HashAlgorithmType payload["certificateHashData"]["issuerNameHash"] = root_cert_hash_issuer_name; payload["certificateHashData"]["issuerKeyHash"] = root_cert_hash_issuer_key; payload["certificateHashData"]["serialNumber"] = root_cert_hash_serial_number; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); } SECTION("M03 GetInstalledCertificateIds operation") { auto ret = certs->installCertificate(InstallCertificateType_CSMSRootCertificate, root_cert); REQUIRE(ret == InstallCertificateStatus_Accepted); bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "GetInstalledCertificateIds", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(1)); auto payload = doc->to(); payload["certificateType"][0] = "CSMSRootCertificate"; //of GetCertificateIdTypeEnumType return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); REQUIRE( payload["certificateHashDataChain"].size() == 1 ); JsonObject certificateHashDataChain = payload["certificateHashDataChain"][0]; REQUIRE( !strcmp(certificateHashDataChain["certificateType"] | "_Undefined", "CSMSRootCertificate") ); JsonObject certificateHashData = certificateHashDataChain["certificateHashData"]; REQUIRE( !strcmp(certificateHashData["hashAlgorithm"] | "_Undefined", root_cert_hash_algorithm) ); //if this fails, please update the precomputed test hashes REQUIRE( !strcmp(certificateHashData["issuerNameHash"] | "_Undefined", root_cert_hash_issuer_name) ); REQUIRE( !strcmp(certificateHashData["issuerKeyHash"] | "_Undefined", root_cert_hash_issuer_key) ); REQUIRE( !strcmp(certificateHashData["serialNumber"] | "_Undefined", root_cert_hash_serial_number) ); REQUIRE( !certificateHashDataChain.containsKey("childCertificateHashData") ); } ))); loop(); REQUIRE( checkProcessed ); } mocpp_deinitialize(); } #else #warning Certificates unit tests depend on MbedTLS #endif //MO_ENABLE_MBEDTLS ================================================ FILE: tests/ChargePointError.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include "./helpers/testHelper.h" #include #include #include #include #include #include #include #include #define BASE_TIME "2023-01-01T00:00:00.000Z" #define BASE_TIME_1H "2023-01-01T01:00:00.000Z" #define FTP_URL "ftps://localhost/firmware.bin" #define ERROR_INFO_EXAMPLE "error description" #define ERROR_INFO_LOW_1 "low severity 1" #define ERROR_INFO_LOW_2 "low severity 2" #define ERROR_INFO_HIGH "high severity" #define ERROR_VENDOR_ID "mVendorId" #define ERROR_VENDOR_CODE "mVendorErrorCode" using namespace MicroOcpp; TEST_CASE( "ChargePointError" ) { printf("\nRun %s\n", "ChargePointError"); //clean state auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_set_timer(custom_timer_cb); mocpp_initialize(loopback, ChargerCredentials("test-runner")); auto& model = getOcppContext()->getModel(); auto fwService = getFirmwareService(); SECTION("FirmwareService initialized") { REQUIRE(fwService != nullptr); } model.getClock().setTime(BASE_TIME); loop(); SECTION("Err and resolve (soft error)") { bool errorCondition = false; addErrorDataInput([&errorCondition] () -> ErrorData { if (errorCondition) { ErrorData error = "OtherError"; error.isFaulted = false; error.info = ERROR_INFO_EXAMPLE; error.vendorId = ERROR_VENDOR_ID; error.vendorErrorCode = ERROR_VENDOR_CODE; return error; } return nullptr; }); //test error condition during transaction to check if status remains unchanged beginTransaction("mIdTag"); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); REQUIRE( isOperative() ); bool checkProcessed = false; getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", [&checkProcessed] () { return new Ocpp16::CustomOperation("StatusNotification", [ &checkProcessed] (JsonObject payload) { //process req checkProcessed = true; REQUIRE( !strcmp(payload["errorCode"] | "_Undefined", "OtherError") ); REQUIRE( !strcmp(payload["info"] | "_Undefined", ERROR_INFO_EXAMPLE) ); REQUIRE( !strcmp(payload["status"] | "_Undefined", "Charging") ); REQUIRE( !strcmp(payload["vendorId"] | "_Undefined", ERROR_VENDOR_ID) ); REQUIRE( !strcmp(payload["vendorErrorCode"] | "_Undefined", ERROR_VENDOR_CODE) ); }, [] () { //create conf return createEmptyDocument(); }); }); errorCondition = true; loop(); REQUIRE( checkProcessed ); REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); REQUIRE( isOperative() ); #if MO_REPORT_NOERROR checkProcessed = false; getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", [&checkProcessed] () { return new Ocpp16::CustomOperation("StatusNotification", [ &checkProcessed] (JsonObject payload) { //process req checkProcessed = true; REQUIRE( !strcmp(payload["errorCode"] | "_Undefined", "NoError") ); REQUIRE( !payload.containsKey("info") ); REQUIRE( !strcmp(payload["status"] | "_Undefined", "Charging") ); }, [] () { //create conf return createEmptyDocument(); }); }); #else checkProcessed = true; #endif //MO_REPORT_NOERROR errorCondition = false; loop(); REQUIRE( checkProcessed ); REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); REQUIRE( isOperative() ); } SECTION("Err and resolve (fatal)") { bool errorCondition = false; addErrorCodeInput([&errorCondition] () { return errorCondition ? "OtherError" : nullptr; }); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); REQUIRE( isOperative() ); errorCondition = true; loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Faulted ); REQUIRE( !isOperative() ); errorCondition = false; loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); REQUIRE( isOperative() ); } SECTION("Error severity") { bool errorConditionLow1 = false; bool errorConditionLow2 = false; bool errorConditionHigh = false; addErrorDataInput([&errorConditionLow1] () -> ErrorData { if (errorConditionLow1) { ErrorData error = "OtherError"; error.severity = 1; error.info = ERROR_INFO_LOW_1; return error; } return nullptr; }); addErrorDataInput([&errorConditionLow2] () -> ErrorData { if (errorConditionLow2) { ErrorData error = "OtherError"; error.severity = 1; error.info = ERROR_INFO_LOW_2; return error; } return nullptr; }); addErrorDataInput([&errorConditionHigh] () -> ErrorData { if (errorConditionHigh) { ErrorData error = "OtherError"; error.severity = 2; error.info = ERROR_INFO_HIGH; return error; } return nullptr; }); const char *errorCode = "*"; bool checkErrorCode = false; const char *errorInfo = "*"; bool checkErrorInfo = false; getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", [&checkErrorCode, &checkErrorInfo, &errorInfo, &errorCode] () { return new Ocpp16::CustomOperation("StatusNotification", [&checkErrorCode, &checkErrorInfo, &errorInfo, &errorCode] (JsonObject payload) { //process req if (strcmp(errorInfo, "*")) { MO_DBG_DEBUG("expect \"%s\", got \"%s\"", errorInfo, payload["info"] | "_Undefined"); REQUIRE( !strcmp(payload["info"] | "_Undefined", errorInfo) ); checkErrorInfo = true; } if (strcmp(errorCode, "*")) { MO_DBG_DEBUG("expect \"%s\", got \"%s\"", errorCode, payload["errorCode"] | "_Undefined"); REQUIRE( !strcmp(payload["errorCode"] | "_Undefined", errorCode) ); checkErrorCode = true; } }, [] () { //create conf return createEmptyDocument(); }); }); //sequence: low-level error 1, low-level error 2, then severe error -- all errors should go through MO_DBG_INFO("test sequence: low-level error 1, low-level error 2, then severe error"); errorConditionLow1 = true; errorInfo = ERROR_INFO_LOW_1; checkErrorInfo = false; loop(); REQUIRE( checkErrorInfo ); errorConditionLow2 = true; errorInfo = ERROR_INFO_LOW_2; checkErrorInfo = false; loop(); REQUIRE( checkErrorInfo ); errorConditionHigh = true; errorInfo = ERROR_INFO_HIGH; checkErrorInfo = false; loop(); REQUIRE( checkErrorInfo ); errorConditionLow1 = false; errorConditionLow2 = false; errorConditionHigh = false; errorInfo = "*"; loop(); //sequence: low-level error 1, severe error, then low-level error 2 -- last error gets muted until severe error is resolved MO_DBG_INFO("test sequence: low-level error 1, severe error, then low-level error 2"); errorConditionLow1 = true; errorInfo = ERROR_INFO_LOW_1; checkErrorInfo = false; loop(); REQUIRE( checkErrorInfo ); errorConditionHigh = true; errorInfo = ERROR_INFO_HIGH; checkErrorInfo = false; loop(); REQUIRE( checkErrorInfo ); errorConditionLow2 = true; checkErrorInfo = false; loop(); REQUIRE( !checkErrorInfo ); errorConditionHigh = false; errorInfo = ERROR_INFO_LOW_2; checkErrorInfo = false; loop(); REQUIRE( checkErrorInfo ); errorConditionLow1 = false; errorConditionLow2 = false; errorConditionHigh = false; errorInfo = "*"; loop(); //sequence: low-level error 1, severe error, then severe error gets resolved -- low-level error is reported again MO_DBG_INFO("test sequence: low-level error 1, severe error, then severe error gets resolved"); errorConditionLow1 = true; errorInfo = ERROR_INFO_LOW_1; checkErrorInfo = false; loop(); REQUIRE( checkErrorInfo ); errorConditionHigh = true; errorInfo = ERROR_INFO_HIGH; checkErrorInfo = false; loop(); REQUIRE( checkErrorInfo ); errorConditionHigh = false; errorInfo = ERROR_INFO_LOW_1; checkErrorInfo = false; loop(); REQUIRE( checkErrorInfo ); errorConditionLow1 = false; errorConditionLow2 = false; errorConditionHigh = false; errorInfo = "*"; loop(); //sequence: error, then error gets resolved -- report NoError MO_DBG_INFO("test sequence: error, then error gets resolved"); errorConditionLow1 = true; errorInfo = ERROR_INFO_LOW_1; checkErrorInfo = false; loop(); REQUIRE( checkErrorInfo ); errorConditionLow1 = false; errorInfo = "*"; errorCode = "NoError"; checkErrorCode = false; loop(); REQUIRE( checkErrorCode ); } endTransaction(); mocpp_deinitialize(); } ================================================ FILE: tests/ChargingSessions.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include #include #include #include "./helpers/testHelper.h" #include #define BASE_TIME "2023-01-01T00:00:00.000Z" using namespace MicroOcpp; TEST_CASE( "Charging sessions" ) { printf("\nRun %s\n", "Charging sessions"); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); auto engine = getOcppContext(); auto& checkMsg = engine->getOperationRegistry(); mocpp_set_timer(custom_timer_cb); auto connectionTimeOutInt = declareConfiguration("ConnectionTimeOut", 30, CONFIGURATION_FN); connectionTimeOutInt->setInt(30); auto minimumStatusDurationInt = declareConfiguration("MinimumStatusDuration", 0, CONFIGURATION_FN); minimumStatusDurationInt->setInt(0); std::array expectedSN {"Available", "Available"}; std::array checkedSN {false, false}; checkMsg.registerOperation("StatusNotification", [] () -> Operation* {return new Ocpp16::StatusNotification(0, ChargePointStatus_UNDEFINED, MIN_TIME);}); checkMsg.setOnRequest("StatusNotification", [&checkedSN, &expectedSN] (JsonObject request) { int connectorId = request["connectorId"] | -1; if (connectorId == 0 || connectorId == 1) { //only test single connector case here checkedSN[connectorId] = !strcmp(request["status"] | "Invalid", expectedSN[connectorId]); } }); SECTION("Check idle state"){ bool checkedBN = false; checkMsg.registerOperation("BootNotification", [engine] () -> Operation* {return new Ocpp16::BootNotification(engine->getModel(), makeJsonDoc("UnitTests"));}); checkMsg.setOnRequest("BootNotification", [&checkedBN] (JsonObject request) { checkedBN = !strcmp(request["chargePointModel"] | "Invalid", "test-runner1234"); }); REQUIRE( !isOperative() ); //not operative before reaching loop stage loop(); loop(); REQUIRE( checkedBN ); REQUIRE( checkedSN[0] ); REQUIRE( checkedSN[1] ); REQUIRE( isOperative() ); REQUIRE( !getTransaction() ); REQUIRE( !ocppPermitsCharge() ); } loop(); SECTION("StartTx") { SECTION("StartTx directly"){ startTransaction("mIdTag"); loop(); REQUIRE(ocppPermitsCharge()); } SECTION("StartTx via session management - plug in first") { expectedSN[1] = "Preparing"; setConnectorPluggedInput([] () {return true;}); loop(); REQUIRE(checkedSN[1]); checkedSN[1] = false; expectedSN[1] = "Charging"; beginTransaction("mIdTag"); loop(); REQUIRE(checkedSN[1]); REQUIRE(ocppPermitsCharge()); } SECTION("StartTx via session management - authorization first") { expectedSN[1] = "Preparing"; setConnectorPluggedInput([] () {return false;}); beginTransaction("mIdTag"); loop(); REQUIRE(checkedSN[1]); checkedSN[1] = false; expectedSN[1] = "Charging"; setConnectorPluggedInput([] () {return true;}); loop(); REQUIRE(checkedSN[1]); REQUIRE(ocppPermitsCharge()); } SECTION("StartTx via session management - no plug") { expectedSN[1] = "Charging"; beginTransaction("mIdTag"); loop(); REQUIRE(checkedSN[1]); } SECTION("StartTx via session management - ConnectionTimeOut") { expectedSN[1] = "Preparing"; setConnectorPluggedInput([] () {return false;}); beginTransaction("mIdTag"); loop(); REQUIRE(checkedSN[1]); checkedSN[1] = false; expectedSN[1] = "Available"; mtime += connectionTimeOutInt->getInt() * 1000; loop(); REQUIRE(checkedSN[1]); } loop(); if (ocppPermitsCharge()) { stopTransaction(); } loop(); } SECTION("StopTx") { startTransaction("mIdTag"); loop(); expectedSN[1] = "Available"; SECTION("directly") { stopTransaction(); loop(); REQUIRE(checkedSN[1]); REQUIRE(!ocppPermitsCharge()); } SECTION("via session management - deauthorize") { endTransaction(); loop(); REQUIRE(checkedSN[1]); REQUIRE(!ocppPermitsCharge()); } SECTION("via session management - deauthorize first") { expectedSN[1] = "Finishing"; setConnectorPluggedInput([] () {return true;}); endTransaction(); loop(); REQUIRE(checkedSN[1]); REQUIRE(!ocppPermitsCharge()); checkedSN[1] = false; expectedSN[1] = "Available"; setConnectorPluggedInput([] () {return false;}); loop(); REQUIRE(checkedSN[1]); REQUIRE(!ocppPermitsCharge()); } SECTION("via session management - plug out") { setConnectorPluggedInput([] () {return false;}); loop(); REQUIRE(checkedSN[1]); REQUIRE(!ocppPermitsCharge()); } if (ocppPermitsCharge()) { stopTransaction(); } loop(); } SECTION("Preboot transactions - tx before BootNotification") { mocpp_deinitialize(); loopback.setOnline(false); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); configuration_save(); loop(); beginTransaction_authorized("mIdTag"); loop(); REQUIRE(isTransactionRunning()); mtime += 3600 * 1000; //transaction duration ~1h endTransaction(); loop(); mtime += 3600 * 1000; //set base time one hour later bool checkStartProcessed = false; getOcppContext()->getModel().getClock().setTime(BASE_TIME); Timestamp basetime = Timestamp(); basetime.setTime(BASE_TIME); getOcppContext()->getOperationRegistry().setOnRequest("StartTransaction", [&checkStartProcessed, basetime] (JsonObject payload) { checkStartProcessed = true; Timestamp timestamp; timestamp.setTime(payload["timestamp"].as()); auto adjustmentDelay = basetime - timestamp; REQUIRE((adjustmentDelay > 2 * 3600 - 10 && adjustmentDelay < 2 * 3600 + 10)); }); bool checkStopProcessed = false; getOcppContext()->getOperationRegistry().setOnRequest("StopTransaction", [&checkStopProcessed, basetime] (JsonObject payload) { checkStopProcessed = true; Timestamp timestamp; timestamp.setTime(payload["timestamp"].as()); auto adjustmentDelay = basetime - timestamp; REQUIRE((adjustmentDelay > 3600 - 10 && adjustmentDelay < 3600 + 10)); }); loopback.setOnline(true); loop(); REQUIRE(checkStartProcessed); REQUIRE(checkStopProcessed); } SECTION("Preboot transactions - lose StartTx timestamp") { mocpp_deinitialize(); loopback.setOnline(false); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); configuration_save(); loop(); beginTransaction_authorized("mIdTag"); loop(); REQUIRE(isTransactionRunning()); mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); configuration_save(); bool checkProcessed = false; getOcppContext()->getOperationRegistry().setOnRequest("StartTransaction", [&checkProcessed] (JsonObject) { checkProcessed = true; }); getOcppContext()->getOperationRegistry().setOnRequest("StopTransaction", [&checkProcessed] (JsonObject) { checkProcessed = true; }); loopback.setOnline(true); loop(); REQUIRE(!isTransactionRunning()); REQUIRE(!checkProcessed); } SECTION("Preboot transactions - lose StopTx timestamp") { const char *starTxTimestampStr = "2023-02-01T00:00:00.000Z"; getOcppContext()->getModel().getClock().setTime(starTxTimestampStr); beginTransaction_authorized("mIdTag"); loop(); REQUIRE(isTransactionRunning()); mocpp_deinitialize(); loopback.setOnline(false); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); configuration_save(); loop(); REQUIRE(isTransactionRunning()); endTransaction(); loop(); REQUIRE(!isTransactionRunning()); mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); configuration_save(); bool checkProcessed = false; getOcppContext()->getOperationRegistry().setOnRequest("StopTransaction", [&checkProcessed, starTxTimestampStr] (JsonObject payload) { checkProcessed = true; Timestamp timestamp; timestamp.setTime(payload["timestamp"].as()); Timestamp starTxTimestamp = Timestamp(); starTxTimestamp.setTime(starTxTimestampStr); auto adjustmentDelay = timestamp - starTxTimestamp; REQUIRE(adjustmentDelay == 1); }); loopback.setOnline(true); loop(); REQUIRE(checkProcessed); } SECTION("Preboot transactions - reject tx if limit exceeded") { mocpp_deinitialize(); loopback.setConnected(false); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); declareConfiguration(MO_CONFIG_EXT_PREFIX "SilentOfflineTransactions", false, CONFIGURATION_FN)->setBool(false); // do not start more txs if tx journal is full configuration_save(); loop(); for (size_t i = 0; i < MO_TXRECORD_SIZE; i++) { beginTransaction_authorized("mIdTag"); loop(); REQUIRE(isTransactionRunning()); endTransaction(); loop(); REQUIRE(!isTransactionRunning()); } // now, tx journal is full. Block any further charging session auto tx_success = beginTransaction_authorized("mIdTag"); REQUIRE( !tx_success ); loop(); REQUIRE(!isTransactionRunning()); REQUIRE(!ocppPermitsCharge()); // Check if all 4 cached transctions are transmitted after going online const int txId_base = 10000; int txId_generate = txId_base; int txId_confirm = txId_base; getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&txId_generate] () { return new Ocpp16::CustomOperation("StartTransaction", [] (JsonObject payload) {}, //ignore req [&txId_generate] () { //create conf auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); JsonObject payload = doc->to(); JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); idTagInfo["status"] = "Accepted"; txId_generate++; payload["transactionId"] = txId_generate; return doc; });}); getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&txId_generate, &txId_confirm] () { return new Ocpp16::CustomOperation("StopTransaction", [&txId_generate, &txId_confirm] (JsonObject payload) { //receive req REQUIRE( payload["transactionId"].as() == txId_generate ); REQUIRE( payload["transactionId"].as() == txId_confirm + 1 ); txId_confirm = payload["transactionId"].as(); }, [] () { //create conf return createEmptyDocument(); });}); loopback.setConnected(true); loop(); REQUIRE( txId_confirm == txId_base + MO_TXRECORD_SIZE ); } SECTION("Preboot transactions - charge without tx if limit exceeded") { mocpp_deinitialize(); loopback.setConnected(false); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); declareConfiguration(MO_CONFIG_EXT_PREFIX "SilentOfflineTransactions", false, CONFIGURATION_FN)->setBool(true); // don't report further transactions to server but charge anyway configuration_save(); loop(); for (size_t i = 0; i < MO_TXRECORD_SIZE; i++) { beginTransaction_authorized("mIdTag"); loop(); REQUIRE(isTransactionRunning()); endTransaction(); loop(); REQUIRE(!isTransactionRunning()); } // now, tx journal is full. Block any further charging session auto tx_success = beginTransaction_authorized("mIdTag"); REQUIRE( tx_success ); loop(); REQUIRE(isTransactionRunning()); REQUIRE(ocppPermitsCharge()); endTransaction(); loop(); // Check if all 4 cached transctions are transmitted after going online const int txId_base = 10000; int txId_generate = txId_base; int txId_confirm = txId_base; getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&txId_generate] () { return new Ocpp16::CustomOperation("StartTransaction", [] (JsonObject payload) {}, //ignore req [&txId_generate] () { //create conf auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); JsonObject payload = doc->to(); JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); idTagInfo["status"] = "Accepted"; txId_generate++; payload["transactionId"] = txId_generate; return doc; });}); getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&txId_generate, &txId_confirm] () { return new Ocpp16::CustomOperation("StopTransaction", [&txId_generate, &txId_confirm] (JsonObject payload) { //receive req REQUIRE( payload["transactionId"].as() == txId_generate ); REQUIRE( payload["transactionId"].as() == txId_confirm + 1 ); txId_confirm = payload["transactionId"].as(); }, [] () { //create conf return createEmptyDocument(); });}); loopback.setConnected(true); loop(); REQUIRE( txId_confirm == txId_base + MO_TXRECORD_SIZE ); } SECTION("Preboot transactions - mix PreBoot with Offline tx") { /* * The charger boots and connects to the OCPP server normally. It looses connection and then starts * transaction #1 which is persisted on flash. Then a power loss occurs, but the charger doesn't reconnect. * Start transaction #2 in PreBoot mode. Trigger another power loss, start transaction #3 while still * being offline and then, after reconnection to the server, transaction #4. * * Tx #1 can be fully restored. The timestamp information for Tx #2 is missing, so it is discarded. Tx #3 is * missing absolute timestamps at first, but after reconnection with the server, the timestamps get updated * with absolute values from the server. Tx #4 is the standard case for transactions and should start normally. */ // use idTags to identify the transactions const char *tx1_idTag = "Tx#1"; const char *tx2_idTag = "Tx#2"; const char *tx3_idTag = "Tx#3"; const char *tx4_idTag = "Tx#4"; declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true)->setBool(true); declareConfiguration("AllowOfflineTxForUnknownId", true)->setBool(true); configuration_save(); loop(); // start Tx #1 (offline tx) loopback.setConnected(false); MO_DBG_DEBUG("begin tx (%s)", tx1_idTag); beginTransaction(tx1_idTag); loop(); REQUIRE(isTransactionRunning()); endTransaction(); loop(); REQUIRE(!isTransactionRunning()); // first power cycle mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); loop(); // start Tx #2 (PreBoot tx, won't get timestamp) MO_DBG_DEBUG("begin tx (%s)", tx2_idTag); beginTransaction(tx2_idTag); loop(); REQUIRE(isTransactionRunning()); endTransaction(); loop(); REQUIRE(!isTransactionRunning()); // second power cycle mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); loop(); // start Tx #3 (PreBoot tx, will eventually get timestamp) MO_DBG_DEBUG("begin tx (%s)", tx3_idTag); beginTransaction(tx3_idTag); loop(); REQUIRE(isTransactionRunning()); endTransaction(); loop(); REQUIRE(!isTransactionRunning()); // set up checks before getting online and starting Tx #4 bool check_1 = false, check_2 = false, check_3 = false, check_4 = false; getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&check_1, &check_2, &check_3, &check_4, tx1_idTag, tx2_idTag, tx3_idTag, tx4_idTag] () { return new Ocpp16::CustomOperation("StartTransaction", [&check_1, &check_2, &check_3, &check_4, tx1_idTag, tx2_idTag, tx3_idTag, tx4_idTag] (JsonObject payload) { //process req const char *idTag = payload["idTag"] | "_Undefined"; if (!strcmp(idTag, tx1_idTag )) { check_1 = true; } else if (!strcmp(idTag, tx2_idTag )) { check_2 = true; } else if (!strcmp(idTag, tx3_idTag )) { check_3 = true; } else if (!strcmp(idTag, tx4_idTag )) { check_4 = true; } }, [] () { //create conf auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); JsonObject payload = doc->to(); JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); idTagInfo["status"] = "Accepted"; static int uniqueTxId = 1000; payload["transactionId"] = uniqueTxId++; //sample data for debug purpose return doc; });}); // get online loopback.setConnected(true); loop(); // start Tx #4 MO_DBG_DEBUG("begin tx (%s)", tx4_idTag); beginTransaction(tx4_idTag); loop(); REQUIRE(isTransactionRunning()); endTransaction(); loop(); REQUIRE(!isTransactionRunning()); // evaluate results REQUIRE( check_1 ); REQUIRE( !check_2 ); // critical data for Tx #2 got lost so it must be discarded REQUIRE( check_3 ); REQUIRE( check_4 ); } SECTION("Set Unavaible"){ beginTransaction("mIdTag"); loop(); auto connector = getOcppContext()->getModel().getConnector(1); REQUIRE(connector->getStatus() == ChargePointStatus_Charging); REQUIRE(isOperative()); bool checkProcessed = false; auto changeAvailability = makeRequest(new Ocpp16::CustomOperation( "ChangeAvailability", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["connectorId"] = 1; payload["type"] = "Inoperative"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE(!strcmp(payload["status"], "Scheduled"));})); getOcppContext()->initiateRequest(std::move(changeAvailability)); loop(); REQUIRE(checkProcessed); REQUIRE(connector->getStatus() == ChargePointStatus_Charging); REQUIRE(isOperative()); mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); connector = getOcppContext()->getModel().getConnector(1); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Charging); REQUIRE(isOperative()); endTransaction(); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Unavailable); REQUIRE(!isOperative()); connector->setAvailability(true); REQUIRE(connector->getStatus() == ChargePointStatus_Available); REQUIRE(isOperative()); } SECTION("UnlockConnector") { // UnlockConnector handler beginTransaction_authorized("mIdTag"); loop(); REQUIRE( isTransactionRunning() ); bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest( new MicroOcpp::Ocpp16::CustomOperation("UnlockConnector", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["connectorId"] = 1; return doc; }, [&checkProcessed] (JsonObject payload) { //process conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "NotSupported") ); }))); loop(); REQUIRE( checkProcessed ); REQUIRE( isTransactionRunning() ); // NotSupported doesn't lead to transaction stop #if MO_ENABLE_CONNECTOR_LOCK setOnUnlockConnectorInOut([] () -> UnlockConnectorResult { // connector lock fails return UnlockConnectorResult_UnlockFailed; }); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest( new MicroOcpp::Ocpp16::CustomOperation("UnlockConnector", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["connectorId"] = 1; return doc; }, [&checkProcessed] (JsonObject payload) { //process conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "UnlockFailed") ); }))); loop(); REQUIRE( checkProcessed ); REQUIRE( !isTransactionRunning() ); // Stop tx when UnlockConnector generally supported setOnUnlockConnectorInOut([] () -> UnlockConnectorResult { // connector lock times out return UnlockConnectorResult_Pending; }); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest( new MicroOcpp::Ocpp16::CustomOperation("UnlockConnector", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["connectorId"] = 1; return doc; }, [&checkProcessed] (JsonObject payload) { //process conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "UnlockFailed") ); }))); loop(); mtime += MO_UNLOCK_TIMEOUT; // increment clock so that MO_UNLOCK_TIMEOUT expires loop(); REQUIRE( checkProcessed ); #else endTransaction(); loop(); #endif //MO_ENABLE_CONNECTOR_LOCK } SECTION("TxStartPoint - PowerPathClosed") { declareConfiguration(MO_CONFIG_EXT_PREFIX "TxStartOnPowerPathClosed", true)->setBool(true); // precondition: charge not allowed REQUIRE( !ocppPermitsCharge() ); REQUIRE( !isTransactionRunning() ); setConnectorPluggedInput([] () {return false;}); // TxStartOnPowerPathClosed removes ConnectorPlugged as a prerequisite of transactions setEvReadyInput([] () {return false;}); // TxStartOnPowerPathClosed puts EvReady in the role of ConnectorPlugged in conventional transactions beginTransaction("mIdTag"); loop(); // in contrast to conventional tx mode, charge permission is granted before transaction. PowerPathClosed is a prerequisite of transactions REQUIRE( ocppPermitsCharge() ); REQUIRE( !isTransactionRunning() ); setConnectorPluggedInput([] () {return true;}); // ConnectorPlugged not sufficient to start tx loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( !isTransactionRunning() ); setEvReadyInput([] () {return true;}); // now, close PowerPath. Transaction will start now loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( isTransactionRunning() ); endTransaction(); loop(); } SECTION("TransactionMessageAttempts-/RetryInterval") { /* * Scenarios: * - final failure to send txMsg after tx terminated * - normal communication restored after a final failure * - StartTx fails finally during tx * - StartTx works but StopTx fails finally after tx terminated * - sends attempts fail until final attempt succeeds * - after reboot, continue attempting */ declareConfiguration("TransactionMessageAttempts", 1)->setInt(1); bool checkProcessedStartTx = false; bool checkProcessedStopTx = false; unsigned int txId = 1000; /* * - final failure to send txMsg after tx terminated */ getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&checkProcessedStartTx, &txId] () { return new Ocpp16::CustomOperation("StartTransaction", [&checkProcessedStartTx] (JsonObject payload) { //receive req checkProcessedStartTx = true; }, [&txId] () { //create conf auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); JsonObject payload = doc->to(); JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); idTagInfo["status"] = "Accepted"; payload["transactionId"] = txId++; return doc; });}); getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&checkProcessedStopTx] () { return new Ocpp16::CustomOperation("StopTransaction", [&checkProcessedStopTx] (JsonObject payload) { //receive req checkProcessedStopTx = true; }, [] () { //create conf return createEmptyDocument(); });}); loopback.setOnline(false); REQUIRE( !ocppPermitsCharge() ); beginTransaction_authorized("mIdTag"); loop(); REQUIRE( ocppPermitsCharge() ); endTransaction(); loop(); REQUIRE( !ocppPermitsCharge() ); mtime += 10 * 60 * 1000; //jump 10 minutes into future loopback.setOnline(true); loop(); REQUIRE( !checkProcessedStartTx ); REQUIRE( !checkProcessedStopTx ); /* * - normal communication restored after a final failure */ checkProcessedStartTx = false; checkProcessedStopTx = false; beginTransaction_authorized("mIdTag"); loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( checkProcessedStartTx ); endTransaction(); loop(); REQUIRE( !ocppPermitsCharge() ); REQUIRE( checkProcessedStopTx ); /* * - StartTx fails finally during tx */ checkProcessedStartTx = false; checkProcessedStopTx = false; loopback.setOnline(false); REQUIRE( !ocppPermitsCharge() ); beginTransaction_authorized("mIdTag"); loop(); REQUIRE( ocppPermitsCharge() ); mtime += 10 * 60 * 1000; //jump 10 minutes into future loop(); REQUIRE( !ocppPermitsCharge() ); loopback.setOnline(true); loop(); REQUIRE( !checkProcessedStartTx ); REQUIRE( !checkProcessedStopTx ); /* * - StartTx works but StopTx fails finally after tx terminated */ checkProcessedStartTx = false; checkProcessedStopTx = false; beginTransaction_authorized("mIdTag"); loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( checkProcessedStartTx ); loopback.setOnline(false); endTransaction(); loop(); mtime += 10 * 60 * 1000; //jump 10 minutes into future loopback.setOnline(true); loop(); REQUIRE( !checkProcessedStopTx ); /* * - sends attempts fail until final attempt succeeds */ const size_t NUM_ATTEMPTS = 3; const int RETRY_INTERVAL_SECS = 3600; declareConfiguration("TransactionMessageAttempts", 0)->setInt(NUM_ATTEMPTS); declareConfiguration("TransactionMessageRetryInterval", 0)->setInt(RETRY_INTERVAL_SECS); configuration_save(); checkProcessedStartTx = false; checkProcessedStopTx = false; unsigned int attemptNr = 0; getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&checkProcessedStartTx, &txId, &attemptNr] () { return new Ocpp16::CustomOperation("StartTransaction", [&attemptNr] (JsonObject payload) { //receive req attemptNr++; }, [&txId] () { //create conf auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); JsonObject payload = doc->to(); JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); idTagInfo["status"] = "Accepted"; payload["transactionId"] = txId++; return doc; }, [&attemptNr] () { //ErrorCode for CALLERROR return attemptNr < NUM_ATTEMPTS ? "InternalError" : (const char*)nullptr; });}); beginTransaction_authorized("mIdTag"); loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( attemptNr == 1 ); mtime += (unsigned long)RETRY_INTERVAL_SECS * 1000; loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( attemptNr == 2 ); mtime += 2 * (unsigned long)RETRY_INTERVAL_SECS * 1000; loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( attemptNr == 3 ); mtime += 100 * (unsigned long)RETRY_INTERVAL_SECS * 1000; loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( attemptNr == 3 ); //no further retry after third and successful attempt endTransaction(); loop(); REQUIRE( !ocppPermitsCharge() ); REQUIRE( attemptNr == 3 ); REQUIRE( checkProcessedStopTx ); /* * - after reboot, continue attempting */ getOcppContext()->getModel().getClock().setTime(BASE_TIME); //reset system time to have roughly the same time after reboot checkProcessedStartTx = false; checkProcessedStopTx = false; attemptNr = 0; beginTransaction_authorized("mIdTag"); loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( attemptNr == 1 ); mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); getOcppContext()->getModel().getClock().setTime(BASE_TIME); getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&checkProcessedStartTx, &txId, &attemptNr] () { return new Ocpp16::CustomOperation("StartTransaction", [&attemptNr] (JsonObject payload) { //receive req attemptNr++; }, [&txId] () { //create conf auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); JsonObject payload = doc->to(); JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); idTagInfo["status"] = "Accepted"; payload["transactionId"] = txId++; return doc; }, [&attemptNr] () { //ErrorCode for CALLERROR return attemptNr < NUM_ATTEMPTS ? "InternalError" : (const char*)nullptr; });}); getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&checkProcessedStopTx] () { return new Ocpp16::CustomOperation("StopTransaction", [&checkProcessedStopTx] (JsonObject payload) { //receive req checkProcessedStopTx = true; }, [] () { //create conf return createEmptyDocument(); });}); loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( attemptNr == 1 ); mtime += (unsigned long)RETRY_INTERVAL_SECS * 1000; loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( attemptNr == 2 ); mtime += 2 * (unsigned long)RETRY_INTERVAL_SECS * 1000; loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( attemptNr == 3 ); mtime += 100 * (unsigned long)RETRY_INTERVAL_SECS * 1000; loop(); REQUIRE( ocppPermitsCharge() ); REQUIRE( attemptNr == 3 ); //no further retry after third and successful attempt endTransaction(); loop(); REQUIRE( !ocppPermitsCharge() ); REQUIRE( attemptNr == 3 ); REQUIRE( checkProcessedStopTx ); } SECTION("StatusNotification") { mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials()); bool checkProcessed = false; const char *checkStatus = ""; getOcppContext()->getOperationRegistry().setOnRequest("StatusNotification", [&checkProcessed, &checkStatus] (JsonObject payload) { //process req if (payload["connectorId"].as() == 1) { checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", checkStatus) ); } }); checkStatus = "Available"; loop(); REQUIRE( checkProcessed ); checkStatus = "Preparing"; checkProcessed = false; setConnectorPluggedInput([] () {return true;}); loop(); REQUIRE( checkProcessed ); checkStatus = "Available"; checkProcessed = false; setConnectorPluggedInput([] () {return false;}); loop(); REQUIRE( checkProcessed ); checkStatus = "Preparing"; checkProcessed = false; beginTransaction("mIdTag"); loop(); REQUIRE( checkProcessed ); checkStatus = "Available"; checkProcessed = false; endTransaction("mIdTag"); loop(); REQUIRE( checkProcessed ); checkStatus = "Preparing"; beginTransaction("mIdTag"); loop(); checkProcessed = false; checkStatus = "Charging"; checkProcessed = false; setConnectorPluggedInput([] () {return true;}); loop(); REQUIRE( checkProcessed ); checkStatus = "SuspendedEV"; checkProcessed = false; setEvReadyInput([] () {return false;}); loop(); REQUIRE( checkProcessed ); checkStatus = "SuspendedEVSE"; checkProcessed = false; setEvReadyInput([] () {return true;}); setEvseReadyInput([] () {return false;}); loop(); REQUIRE( checkProcessed ); checkStatus = "Charging"; checkProcessed = false; setEvReadyInput([] () {return true;}); setEvseReadyInput([] () {return true;}); loop(); REQUIRE( checkProcessed ); checkStatus = "Finishing"; checkProcessed = false; endTransaction(); loop(); REQUIRE( checkProcessed ); checkStatus = "Available"; checkProcessed = false; setConnectorPluggedInput([] () {return false;}); loop(); REQUIRE( checkProcessed ); checkStatus = "Available"; const char *checkStatus2 = checkStatus; checkProcessed = false; mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); getOcppContext()->getOperationRegistry().setOnRequest("StatusNotification", [&checkProcessed, &checkStatus, &checkStatus2] (JsonObject payload) { //process req if (payload["connectorId"].as() == 1) { checkProcessed = true; REQUIRE( (!strcmp(payload["status"] | "_Undefined", checkStatus) || !strcmp(payload["status"] | "_Undefined", checkStatus2)) ); } }); loop(); REQUIRE( checkProcessed ); checkStatus = "Charging"; checkStatus2 = "Preparing"; checkProcessed = false; beginTransaction("mIdTag"); loop(); REQUIRE( checkProcessed ); checkStatus = "Charging"; checkStatus2 = checkStatus; checkProcessed = false; mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); getOcppContext()->getOperationRegistry().setOnRequest("StatusNotification", [&checkProcessed, &checkStatus] (JsonObject payload) { //process req if (payload["connectorId"].as() == 1) { checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", checkStatus) ); } }); loop(); REQUIRE( checkProcessed ); checkStatus = "Available"; checkStatus2 = checkStatus; checkProcessed = false; endTransaction(); mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); getOcppContext()->getOperationRegistry().setOnRequest("StatusNotification", [&checkProcessed, &checkStatus] (JsonObject payload) { //process req if (payload["connectorId"].as() == 1) { checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", checkStatus) ); } }); loop(); REQUIRE( checkProcessed ); } SECTION("No filesystem access behavior") { //re-init without filesystem access mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials(), MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Deactivate)); mocpp_set_timer(custom_timer_cb); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); REQUIRE( !ocppPermitsCharge() ); for (size_t i = 0; i < 3; i++) { beginTransaction("mIdTag"); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); REQUIRE( ocppPermitsCharge() ); endTransaction(); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); REQUIRE( !ocppPermitsCharge() ); } //Tx status will be lost over reboot beginTransaction("mIdTag"); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); REQUIRE( ocppPermitsCharge() ); mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials(), MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Deactivate)); mocpp_set_timer(custom_timer_cb); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); REQUIRE( !ocppPermitsCharge() ); //Note: queueing offline transactions without FS is currently not implemented } mocpp_deinitialize(); } ================================================ FILE: tests/Configuration.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include "./helpers/testHelper.h" #include #include #include #include #include #include #include #include #include #include using namespace MicroOcpp; #define GET_CONFIG_ALL "[2,\"test-msg\",\"GetConfiguration\",{}]" #define KNOWN_KEY "__ExistingKey" #define UNKOWN_KEY "__UnknownKey" #define GET_CONFIG_KNOWN_UNKOWN "[2,\"test-mst\",\"GetConfiguration\",{\"key\":[\"" KNOWN_KEY "\",\"" UNKOWN_KEY "\"]}]" // some globals for the C-API tests bool g_checkProcessed [10]; ocpp_configuration g_configs [2]; int g_config_values [2]; uint16_t g_config_write_count[2]; TEST_CASE( "Configuration" ) { printf("\nRun %s\n", "Configuration"); //clean state auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); LoopbackConnection loopback; //initialize Context with dummy socket mocpp_set_timer(custom_timer_cb); SECTION("Basic container operations"){ std::unique_ptr container; SECTION("Volatile storage") { container = makeConfigurationContainerVolatile(CONFIGURATION_VOLATILE "/volatile1", true); } SECTION("Persistent storage") { container = makeConfigurationContainerFlash(filesystem, MO_FILENAME_PREFIX "persistent1.jsn", true); } //check emptyness REQUIRE( container->size() == 0 ); //add first config, fetch by index auto configFirst = container->createConfiguration(TConfig::Int, "cFirst"); REQUIRE( container->size() == 1 ); REQUIRE( container->getConfiguration((size_t) 0) == configFirst.get()); //add one config of each type auto cInt = container->createConfiguration(TConfig::Int, "cInt"); auto cBool = container->createConfiguration(TConfig::Bool, "cBool"); auto cString = container->createConfiguration(TConfig::String, "cString"); REQUIRE( container->size() == 4 ); //fetch config by key REQUIRE( container->getConfiguration(cBool->getKey()) == cBool); //remove config container->remove(cBool.get()); REQUIRE( container->size() == 3 ); REQUIRE( container->getConfiguration(cBool->getKey()) == nullptr); //clean container container->remove(container->getConfiguration((size_t) 0)); container->remove(container->getConfiguration((size_t) 0)); container->remove(container->getConfiguration((size_t) 0)); REQUIRE( container->size() == 0 ); } SECTION("Persistency on filesystem") { auto container = makeConfigurationContainerFlash(filesystem, MO_FILENAME_PREFIX "persistent1.jsn", true); //trivial load call REQUIRE( container->load() ); REQUIRE( container->size() == 0 ); //add config, store, load again auto cString = container->createConfiguration(TConfig::String, "cString"); cString->setString("mValue"); REQUIRE( container->save() ); //store container.reset(); //destroy //...load again auto container2 = makeConfigurationContainerFlash(filesystem, MO_FILENAME_PREFIX "persistent1.jsn", true); REQUIRE( container2->size() == 0 ); REQUIRE( container2->load() ); REQUIRE( container2->size() == 1 ); auto cString2 = container2->getConfiguration("cString"); REQUIRE( cString2 != nullptr ); REQUIRE( !strcmp(cString2->getString(), "mValue") ); } SECTION("Configuration API") { //declare configs REQUIRE( configuration_init(filesystem) ); auto cInt = declareConfiguration("cInt", 42); REQUIRE( cInt != nullptr ); declareConfiguration("cBool", true); declareConfiguration("cString", "mValue"); //fetch config REQUIRE( getConfigurationPublic("cInt")->getInt() == 42 ); //store, destroy, reload REQUIRE( configuration_save() ); cInt.reset(); configuration_deinit(); REQUIRE( getConfigurationPublic("cInt") == nullptr); REQUIRE( configuration_init(filesystem) ); //reload //fetch configs (declare with different factory default - should remain at original value) auto cInt2 = declareConfiguration("cInt", -1); auto cBool2 = declareConfiguration("cBool", false); auto cString2 = declareConfiguration("cString", "no effect"); REQUIRE( configuration_load() ); //load config objects with stored values //check load result REQUIRE( cInt2->getInt() == 42 ); REQUIRE( cBool2->getBool() == true ); REQUIRE( !strcmp(cString2->getString(), "mValue") ); //declare config twice auto cInt3 = declareConfiguration("cInt", -1); REQUIRE( cInt3 == cInt2 ); //declare config twice but in different container auto cInt4 = declareConfiguration("cInt", -1, CONFIGURATION_VOLATILE); REQUIRE( cInt4 == cInt2 ); //declare config twice but with conflicting type (will supersede old type because to simplify FW upgrades) auto cNewType = declareConfiguration("cInt", "mValue2"); REQUIRE( cNewType != cInt2 ); REQUIRE( !strcmp(cNewType->getString(), "mValue2") ); //store, destroy, reload REQUIRE( configuration_save() ); configuration_deinit(); REQUIRE( getConfigurationPublic("cInt") == nullptr); REQUIRE( configuration_init(filesystem) ); //reload auto cNewType2 = declareConfiguration("cInt", "no effect"); REQUIRE( configuration_load() ); REQUIRE( !strcmp(cNewType2->getString(), "mValue2") ); //get config before declared (container needs to be declared already at this point) auto cString3 = getConfigurationPublic("cString"); REQUIRE( !strcmp(cString3->getString(), "mValue") ); configuration_deinit(); //value needs to outlive container configuration_init(filesystem); auto cString4 = declareConfiguration("cString2", "mValue3"); configuration_deinit(); REQUIRE( !strcmp(cString4->getString(), "mValue3") ); FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); //config accessibility / permissions configuration_init(filesystem); bool readonly = false; bool rebootRequired = false; bool isPublic = true; auto cInt6 = declareConfiguration("cInt", 42, CONFIGURATION_FN, readonly, rebootRequired, isPublic); REQUIRE( !cInt6->isReadOnly() ); REQUIRE( !cInt6->isRebootRequired() ); REQUIRE( getConfigurationPublic("cInt") ); //revoke permissions readonly = true; rebootRequired = true; declareConfiguration("cInt", 42, CONFIGURATION_FN, readonly, rebootRequired, isPublic); REQUIRE( cInt6->isReadOnly() ); REQUIRE( cInt6->isRebootRequired() ); //revoked permissions cannot be reverted readonly = false; rebootRequired = false; auto cInt7 = declareConfiguration("cInt", 42, CONFIGURATION_FN, readonly, rebootRequired, isPublic); REQUIRE( cInt7->isReadOnly() ); REQUIRE( cInt7->isRebootRequired() ); //accessibility cannot be changed after first initialization isPublic = false; declareConfiguration("cInt", 42, CONFIGURATION_FN, false, rebootRequired, isPublic); declareConfiguration("cInt2", 42, CONFIGURATION_FN, false, rebootRequired, isPublic); REQUIRE( getConfigurationPublic("cInt") ); REQUIRE( getConfigurationPublic("cInt2") ); //create config in hidden container isPublic = false; auto cHidden = declareConfiguration("cHidden", 42, MO_FILENAME_PREFIX "hidden.jsn", false, false, isPublic); REQUIRE( !getConfigurationPublic("cHidden") ); //hidden container cannot be fetched auto configsPublic = getConfigurationContainersPublic(); REQUIRE( configsPublic.size() == 1 ); configuration_deinit(); } SECTION("ContainerFlash memory optimization") { //key storage optimization: the static key provided by declareConfiguration is preferred. If //no static key is available for the config (if the config is loaded from flash without being //declared before), then a key on the heap is allocated. If the config is later allocated, //the heap-key is replaced by the static key const char *key_static = "cInt"; configuration_init(filesystem); auto cInt = declareConfiguration(key_static, 42); configuration_save(); configuration_deinit(); configuration_init(filesystem); declareConfiguration("dummy", 23); //dummy config to declare CONFIGURATION_FN configuration_load(); const char *key_heap = getConfigurationPublic(key_static)->getKey(); REQUIRE( key_heap != key_static ); declareConfiguration(key_static, 42); //replace heap key with static key here const char *key_replaced = getConfigurationPublic(key_static)->getKey(); REQUIRE( key_replaced == key_static ); configuration_deinit(); } SECTION("Main lib integration") { //basic lifecycle mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); REQUIRE( getConfigurationPublic("ConnectionTimeOut") ); REQUIRE( !getConfigurationContainersPublic().empty() ); mocpp_deinitialize(); REQUIRE( !getConfigurationPublic("ConnectionTimeOut") ); REQUIRE( getConfigurationContainersPublic().empty() ); //modify standard config ConnectionTimeOut. This config is not modified by the main lib during normal initialization / deinitialization mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); auto config = getConfigurationPublic("ConnectionTimeOut"); config->setInt(1234); //update configuration_save(); //write back mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); REQUIRE( getConfigurationPublic("ConnectionTimeOut")->getInt() == 1234 ); mocpp_deinitialize(); } SECTION("GetConfiguration") { mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); loop(); declareConfiguration(KNOWN_KEY, 1234, MO_FILENAME_PREFIX "persistent1.jsn", false); bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "GetConfiguration", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); doc->to(); return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; JsonArray configurationKey = payload["configurationKey"]; bool foundCustomConfig = false; bool foundStandardConfig = false; for (JsonObject keyvalue : configurationKey) { MO_DBG_DEBUG("key %s", keyvalue["key"] | "_Undefined"); if (!strcmp(keyvalue["key"] | "_Undefined", KNOWN_KEY)) { foundCustomConfig = true; REQUIRE( (keyvalue["readonly"] | true) == false ); REQUIRE( !strcmp(keyvalue["value"] | "_Undefined", "1234") ); } else if (!strcmp(keyvalue["key"] | "_Undefined", "ConnectionTimeOut")) { foundStandardConfig = true; } } REQUIRE( foundCustomConfig ); REQUIRE( foundStandardConfig ); } ))); loop(); REQUIRE(checkProcessed); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "GetConfiguration", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(2)); auto payload = doc->to(); auto key = payload.createNestedArray("key"); key.add(KNOWN_KEY); key.add(UNKOWN_KEY); return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; JsonArray configurationKey = payload["configurationKey"]; bool foundCustomConfig = false; for (JsonObject keyvalue : configurationKey) { if (!strcmp(keyvalue["key"] | "_Undefined", KNOWN_KEY)) { foundCustomConfig = true; break; } } REQUIRE( foundCustomConfig ); JsonArray unknownKey = payload["unknownKey"]; bool foundUnkownKey = false; for (const char *key : unknownKey) { if (!strcmp(key, UNKOWN_KEY)) { foundUnkownKey = true; } } REQUIRE( foundUnkownKey ); } ))); loop(); REQUIRE(checkProcessed); mocpp_deinitialize(); } SECTION("ChangeConfiguration") { mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); loop(); declareConfiguration(KNOWN_KEY, 0, MO_FILENAME_PREFIX "persistent1.jsn", false); //update existing config bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ChangeConfiguration", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = KNOWN_KEY; payload["value"] = "1234"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE(checkProcessed); REQUIRE( getConfigurationPublic(KNOWN_KEY)->getInt() == 1234 ); //try to update not existing key checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ChangeConfiguration", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = UNKOWN_KEY; payload["value"] = "no effect"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "NotSupported") ); } ))); loop(); REQUIRE( checkProcessed ); //try to update config with malformatted value checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ChangeConfiguration", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = KNOWN_KEY; payload["value"] = "not convertible to int"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Rejected") ); } ))); loop(); REQUIRE( checkProcessed ); //try to update config with value validation //value is valid if it begins with 1 registerConfigurationValidator(KNOWN_KEY, [] (const char *v) { return v[0] == '1'; }); //validation success checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ChangeConfiguration", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = KNOWN_KEY; payload["value"] = "100234"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); REQUIRE( getConfigurationPublic(KNOWN_KEY)->getInt() == 100234 ); //validation failure checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ChangeConfiguration", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = KNOWN_KEY; payload["value"] = "4321"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Rejected") ); } ))); loop(); REQUIRE( checkProcessed ); REQUIRE( getConfigurationPublic(KNOWN_KEY)->getInt() == 100234 ); //keep old value mocpp_deinitialize(); } SECTION("Define factory defaults for standard configs") { //set factory default for standard config ConnectionTimeOut configuration_init(filesystem); auto factoryConnectionTimeOut = declareConfiguration("ConnectionTimeOut", 1234, MO_FILENAME_PREFIX "factory.jsn"); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); auto connectionTimeout2 = declareConfiguration("ConnectionTimeOut", 4321); REQUIRE( connectionTimeout2->getInt() == 1234 ); REQUIRE( connectionTimeout2 == factoryConnectionTimeOut ); configuration_save(); mocpp_deinitialize(); //this time, factory default is not given (will lead to duplicates, should be considered in sanitization) mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); REQUIRE( getConfigurationPublic("ConnectionTimeOut")->getInt() != 1234 ); mocpp_deinitialize(); //provide factory default again configuration_init(filesystem); declareConfiguration("ConnectionTimeOut", 4321, MO_FILENAME_PREFIX "factory.jsn"); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); REQUIRE( getConfigurationPublic("ConnectionTimeOut")->getInt() == 1234 ); mocpp_deinitialize(); } SECTION("C-API") { ocpp_configuration_container container; memset(&container, 0, sizeof(container)); bool check_load = false; container.load = [] (void *user_data) { g_checkProcessed[0] = true; return true; }; container.save = [] (void *user_data) { g_checkProcessed[1] = true; return true; }; ocpp_configuration *config_predefined = &g_configs[0]; config_predefined->get_key = [] (void *user_data) -> const char* {return "ConnectionTimeOut";}; // existing OCPP key to use custom config store config_predefined->get_type = [] (void *user_data) -> ocpp_config_datatype {return ENUM_CDT_INT;}; config_predefined->set_int = [] (void *user_data, int val) -> void {g_config_values[0] = val;}; config_predefined->get_int = [] (void *user_data) -> int {return g_config_values[0];}; config_predefined->get_write_count = [] (void *user_data) -> uint16_t {return g_config_write_count[0];}; container.create_configuration = [] (void *user_data, ocpp_config_datatype dt, const char *key) -> ocpp_configuration* { ocpp_configuration *config_created = &g_configs[1]; config_created->get_key = [] (void *user_data) -> const char* {return "MCreatedConfig";}; // non-existing key to test create_configuration function config_created->get_type = [] (void *user_data) -> ocpp_config_datatype {return ENUM_CDT_INT;}; config_created->set_int = [] (void *user_data, int val) -> void {g_config_values[1] = val;}; config_created->get_int = [] (void *user_data) -> int {return g_config_values[1];}; config_created->get_write_count = [] (void *user_data) -> uint16_t {return g_config_write_count[1];}; return config_created; }; container.remove = [] (void *user_data, const char *key) -> void { g_checkProcessed[2] = true; }; container.size = [] (void *user_data) { return sizeof(g_configs) / sizeof(g_configs[0]); }; container.get_configuration = [] (void *user_data, size_t i) -> ocpp_configuration* { return &g_configs[i]; }; container.get_configuration_by_key = [] (void *user_data, const char *key) -> ocpp_configuration* { if (!strcmp(key, "ConnectionTimeOut")) { return &g_configs[0]; } else if (!strcmp(key, "MCreatedConfig")) { return g_configs[1].get_key ? &g_configs[1] : // createConfig has already been called nullptr; // config hasn't been created yet } return nullptr; }; ocpp_configuration_container_add(&container, "MContainerPath", true); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); loop(); REQUIRE( g_checkProcessed[0] ); auto test_predefined = declareConfiguration("ConnectionTimeOut", 0); test_predefined->setInt(12345); REQUIRE( test_predefined->getInt() == 12345 ); REQUIRE( config_predefined->get_int(config_predefined->user_data) == 12345 ); g_checkProcessed[1] = false; // check if store is executed test_predefined->setInt(555); g_config_write_count[0]; configuration_save(); REQUIRE( g_checkProcessed[1] ); // test if declaring new configs is handled auto test_created = declareConfiguration("MCreatedConfig", 123, "MContainerPath"); REQUIRE( test_created != nullptr ); REQUIRE( test_created->getInt() == 123 ); ocpp_configuration *config_created = &g_configs[1]; REQUIRE( config_created->get_int(config_created->user_data) == 123 ); mocpp_deinitialize(); } } ================================================ FILE: tests/ConfigurationBehavior.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include #include #include #include "./helpers/testHelper.h" using namespace MicroOcpp; class CustomAuthorize : public Operation { private: const char *status; public: CustomAuthorize(const char *status) : status(status) { }; const char *getOperationType() override {return "Authorize";} void processReq(JsonObject payload) override { //ignore payload - result is determined at construction time } std::unique_ptr createConf() override { auto res = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = res->to(); payload["idTagInfo"]["status"] = status; return res; } }; class CustomStartTransaction : public Operation { private: const char *status; public: CustomStartTransaction(const char *status) : status(status) { }; const char *getOperationType() override {return "StartTransaction";} void processReq(JsonObject payload) override { //ignore payload - result is determined at construction time } std::unique_ptr createConf() override { auto res = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(1)); auto payload = res->to(); payload["idTagInfo"]["status"] = status; payload["transactionId"] = 1000; return res; } }; TEST_CASE( "Configuration Behavior" ) { printf("\nRun %s\n", "Configuration Behavior"); //clean state auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); MO_DBG_DEBUG("remove all"); FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); auto context = getOcppContext(); auto& checkMsg = context->getOperationRegistry(); auto connector = context->getModel().getConnector(1); mocpp_set_timer(custom_timer_cb); loop(); SECTION("StopTransactionOnEVSideDisconnect") { setConnectorPluggedInput([] () {return true;}); startTransaction("mIdTag"); loop(); auto configBool = declareConfiguration("StopTransactionOnEVSideDisconnect", true); SECTION("set true") { configBool->setBool(true); setConnectorPluggedInput([] () {return false;}); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Available); } SECTION("set false") { configBool->setBool(false); setConnectorPluggedInput([] () {return false;}); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_SuspendedEV); endTransaction(); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Available); } } SECTION("StopTransactionOnInvalidId") { auto configBool = declareConfiguration("StopTransactionOnInvalidId", true); checkMsg.registerOperation("Authorize", [] () {return new CustomAuthorize("Invalid");}); checkMsg.registerOperation("StartTransaction", [] () {return new CustomStartTransaction("Invalid");}); SECTION("set true") { configBool->setBool(true); beginTransaction("mIdTag_invalid"); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Available); beginTransaction_authorized("mIdTag_invalid2"); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Available); } SECTION("set false") { configBool->setBool(false); beginTransaction("mIdTag"); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Available); beginTransaction_authorized("mIdTag"); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_SuspendedEVSE); endTransaction(); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Available); } } SECTION("AllowOfflineTxForUnknownId") { auto configBool = declareConfiguration("AllowOfflineTxForUnknownId", true); auto authorizationTimeoutInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", 1); authorizationTimeoutInt->setInt(1); //try normal Authorize for 1s, then enter offline mode loopback.setOnline(false); //connection loss SECTION("set true") { configBool->setBool(true); beginTransaction("mIdTag"); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Charging); endTransaction(); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Available); } SECTION("set false") { configBool->setBool(false); beginTransaction("mIdTag"); REQUIRE(connector->getStatus() == ChargePointStatus_Preparing); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Available); } endTransaction(); loopback.setOnline(true); } #if MO_ENABLE_LOCAL_AUTH SECTION("LocalPreAuthorize") { auto configBool = declareConfiguration("LocalPreAuthorize", true); auto authorizationTimeoutInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", 20); authorizationTimeoutInt->setInt(300); //try normal Authorize for 5 minutes declareConfiguration("LocalAuthorizeOffline", true)->setBool(true); declareConfiguration("LocalAuthListEnabled", true)->setBool(true); //define local auth list with entry local-idtag const char *localListMsg = "[2,\"testmsg-01\",\"SendLocalList\",{\"listVersion\":1,\"localAuthorizationList\":[{\"idTag\":\"local-idtag\",\"idTagInfo\":{\"status\":\"Accepted\"}}],\"updateType\":\"Full\"}]"; loopback.sendTXT(localListMsg, strlen(localListMsg)); loop(); loopback.setOnline(false); //connection loss SECTION("set true - accepted idtag") { configBool->setBool(true); beginTransaction("local-idtag"); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Charging); } SECTION("set false") { configBool->setBool(false); beginTransaction("local-idtag"); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Preparing); loopback.setOnline(true); mtime += 20000; //Authorize will be retried after a few seconds loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Charging); } endTransaction(); loopback.setOnline(true); } #endif //MO_ENABLE_LOCAL_AUTH SECTION("AuthorizeRemoteTxRequests") { auto configBool = declareConfiguration("AuthorizeRemoteTxRequests", false); bool receivedAuthorize = false; setOnReceiveRequest("Authorize", [&receivedAuthorize] (JsonObject payload) { receivedAuthorize = true; REQUIRE( !strcmp(payload["idTag"] | "_Undefined", "mIdTag") ); }); SECTION("set true") { configBool->setBool(true); context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "RemoteStartTransaction", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTag"] = "mIdTag"; return doc;}, [] (JsonObject) { //ignore conf } ))); loop(); REQUIRE(receivedAuthorize); REQUIRE(connector->getStatus() == ChargePointStatus_Charging); } SECTION("set false") { configBool->setBool(false); context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "RemoteStartTransaction", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTag"] = "mIdTag"; return doc;}, [] (JsonObject) { //ignore conf } ))); loop(); REQUIRE(!receivedAuthorize); REQUIRE(connector->getStatus() == ChargePointStatus_Charging); } endTransaction(); loop(); } mocpp_deinitialize(); } ================================================ FILE: tests/FirmwareManagement.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include "./helpers/testHelper.h" #include #include #include #include #include #include #include #define BASE_TIME "2023-01-01T00:00:00.000Z" #define BASE_TIME_1H "2023-01-01T01:00:00.000Z" #define FTP_URL "ftps://localhost/firmware.bin" using namespace MicroOcpp; TEST_CASE( "FirmwareManagement" ) { printf("\nRun %s\n", "FirmwareManagement"); //clean state auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_set_timer(custom_timer_cb); mocpp_initialize(loopback, ChargerCredentials("test-runner")); auto& model = getOcppContext()->getModel(); auto fwService = getFirmwareService(); SECTION("FirmwareService initialized") { REQUIRE(fwService != nullptr); } model.getClock().setTime(BASE_TIME); loop(); SECTION("Unconfigured FW service") { bool checkProcessed = false; getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { return new Ocpp16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req checkProcessed = true; REQUIRE(( !strcmp(payload["status"] | "_Undefined", "DownloadFailed") || !strcmp(payload["status"] | "_Undefined", "InstallationFailed") )); }, [] () { //create conf return createEmptyDocument(); }); }); getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "UpdateFirmware", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); auto payload = doc->to(); payload["location"] = FTP_URL; payload["retries"] = 1; payload["retrieveDate"] = BASE_TIME; payload["retryInterval"] = 1; return doc;}, [] (JsonObject) { } //ignore conf ))); loop(); REQUIRE(checkProcessed); } SECTION("Download phase only") { int checkProcessed = 0; getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { return new Ocpp16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req if (checkProcessed == 0) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloading") ); checkProcessed++; } else if (checkProcessed == 1) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloaded") ); checkProcessed++; } else if (checkProcessed == 2) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installed") ); checkProcessed++; } }, [] () { //create conf return createEmptyDocument(); }); }); bool checkProcessedOnDownload = false; fwService->setOnDownload([&checkProcessedOnDownload] (const char *location) { checkProcessedOnDownload = true; return true; }); int checkProcessedOnDownloadStatus = 0; fwService->setDownloadStatusInput([&checkProcessed, &checkProcessedOnDownloadStatus] () { if (checkProcessed == 0) { if (checkProcessedOnDownloadStatus == 0) checkProcessedOnDownloadStatus = 1; return DownloadStatus::NotDownloaded; } else { if (checkProcessedOnDownloadStatus == 1) checkProcessedOnDownloadStatus = 2; return DownloadStatus::Downloaded; } }); getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "UpdateFirmware", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); auto payload = doc->to(); payload["location"] = FTP_URL; payload["retries"] = 1; payload["retrieveDate"] = BASE_TIME; payload["retryInterval"] = 1; return doc;}, [] (JsonObject) { } //ignore conf ))); for (unsigned int i = 0; i < 10; i++) { loop(); mtime += 5000; } REQUIRE( checkProcessed == 3 ); //all FirmwareStatusNotification messages have been received REQUIRE( checkProcessedOnDownload ); REQUIRE( checkProcessedOnDownloadStatus == 2 ); } SECTION("Installation phase only") { int checkProcessed = 0; getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { return new Ocpp16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req if (checkProcessed == 0) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installing") ); checkProcessed++; } else if (checkProcessed == 1) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installed") ); checkProcessed++; } }, [] () { //create conf return createEmptyDocument(); }); }); bool checkProcessedOnInstall = false; fwService->setOnInstall([&checkProcessedOnInstall] (const char *location) { checkProcessedOnInstall = true; REQUIRE( !strcmp(location, FTP_URL) ); return true; }); int checkProcessedOnInstallStatus = 0; fwService->setInstallationStatusInput([&checkProcessed, &checkProcessedOnInstallStatus] () { if (checkProcessed == 0) { if (checkProcessedOnInstallStatus == 0) checkProcessedOnInstallStatus = 1; return InstallationStatus::NotInstalled; } else { if (checkProcessedOnInstallStatus == 1) checkProcessedOnInstallStatus = 2; return InstallationStatus::Installed; } }); getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "UpdateFirmware", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); auto payload = doc->to(); payload["location"] = FTP_URL; payload["retries"] = 1; payload["retrieveDate"] = BASE_TIME; payload["retryInterval"] = 1; return doc;}, [] (JsonObject) { } //ignore conf ))); for (unsigned int i = 0; i < 10; i++) { loop(); mtime += 5000; } REQUIRE( checkProcessed == 2 ); //all FirmwareStatusNotification messages have been received REQUIRE( checkProcessedOnInstall ); REQUIRE( checkProcessedOnInstallStatus == 2 ); } SECTION("Download and install") { int checkProcessed = 0; getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { return new Ocpp16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req if (checkProcessed == 0) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloading") ); checkProcessed++; } else if (checkProcessed == 1) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloaded") ); checkProcessed++; } else if (checkProcessed == 2) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installing") ); checkProcessed++; } else if (checkProcessed == 3) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installed") ); checkProcessed++; } }, [] () { //create conf return createEmptyDocument(); }); }); bool checkProcessedOnDownload = false; fwService->setOnDownload([&checkProcessedOnDownload] (const char *location) { checkProcessedOnDownload = true; return true; }); int checkProcessedOnDownloadStatus = 0; fwService->setDownloadStatusInput([&checkProcessed, &checkProcessedOnDownloadStatus] () { if (checkProcessed == 0) { if (checkProcessedOnDownloadStatus == 0) checkProcessedOnDownloadStatus = 1; return DownloadStatus::NotDownloaded; } else { if (checkProcessedOnDownloadStatus == 1) checkProcessedOnDownloadStatus = 2; return DownloadStatus::Downloaded; } }); bool checkProcessedOnInstall = false; fwService->setOnInstall([&checkProcessedOnInstall] (const char *location) { checkProcessedOnInstall = true; return true; }); int checkProcessedOnInstallStatus = 0; fwService->setInstallationStatusInput([&checkProcessed, &checkProcessedOnInstallStatus] () { if (checkProcessed <= 2) { if (checkProcessedOnInstallStatus == 0) checkProcessedOnInstallStatus = 1; return InstallationStatus::NotInstalled; } else { if (checkProcessedOnInstallStatus == 1) checkProcessedOnInstallStatus = 2; return InstallationStatus::Installed; } }); getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "UpdateFirmware", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); auto payload = doc->to(); payload["location"] = FTP_URL; payload["retries"] = 1; payload["retrieveDate"] = BASE_TIME; payload["retryInterval"] = 1; return doc;}, [] (JsonObject) { } //ignore conf ))); for (unsigned int i = 0; i < 10; i++) { loop(); mtime += 5000; } REQUIRE( checkProcessed == 4 ); //all FirmwareStatusNotification messages have been received REQUIRE( checkProcessedOnDownload ); REQUIRE( checkProcessedOnDownloadStatus == 2 ); REQUIRE( checkProcessedOnInstall ); REQUIRE( checkProcessedOnInstallStatus == 2 ); } SECTION("Download failure (try 2 times)") { int checkProcessed = 0; getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { return new Ocpp16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req if (checkProcessed == 0) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloading") ); checkProcessed++; } else if (checkProcessed == 1) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "DownloadFailed") ); checkProcessed++; } else if (checkProcessed == 2) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloading") ); checkProcessed++; } else if (checkProcessed == 3) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "DownloadFailed") ); checkProcessed++; } }, [] () { //create conf return createEmptyDocument(); }); }); int checkProcessedOnDownload = 0; fwService->setOnDownload([&checkProcessedOnDownload] (const char *location) { checkProcessedOnDownload++; return true; }); int checkProcessedOnDownloadStatus = 0; fwService->setDownloadStatusInput([&checkProcessed, &checkProcessedOnDownloadStatus] () { if (checkProcessed == 0) { if (checkProcessedOnDownloadStatus == 0) checkProcessedOnDownloadStatus = 1; return DownloadStatus::NotDownloaded; } else if (checkProcessed == 1) { if (checkProcessedOnDownloadStatus == 1) checkProcessedOnDownloadStatus = 2; return DownloadStatus::DownloadFailed; } else if (checkProcessed == 2) { if (checkProcessedOnDownloadStatus == 2) checkProcessedOnDownloadStatus = 3; return DownloadStatus::NotDownloaded; } else { if (checkProcessedOnDownloadStatus == 3) checkProcessedOnDownloadStatus = 4; return DownloadStatus::DownloadFailed; } }); bool checkProcessedOnInstall = false; // must not be executed fwService->setOnInstall([&checkProcessedOnInstall] (const char *location) { checkProcessedOnInstall = true; return true; }); getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "UpdateFirmware", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); auto payload = doc->to(); payload["location"] = FTP_URL; payload["retries"] = 2; payload["retrieveDate"] = BASE_TIME; payload["retryInterval"] = 10; return doc;}, [] (JsonObject) { } //ignore conf ))); for (unsigned int i = 0; i < 20; i++) { loop(); mtime += 5000; } REQUIRE( checkProcessed == 4 ); //all FirmwareStatusNotification messages have been received REQUIRE( checkProcessedOnDownload == 2 ); REQUIRE( checkProcessedOnDownloadStatus == 4 ); REQUIRE( !checkProcessedOnInstall ); } SECTION("Installation failure (try 2 times)") { int checkProcessed = 0; getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { return new Ocpp16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req if (checkProcessed == 0) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installing") ); checkProcessed++; } else if (checkProcessed == 1) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "InstallationFailed") ); checkProcessed++; } else if (checkProcessed == 2) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installing") ); checkProcessed++; } else if (checkProcessed == 3) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "InstallationFailed") ); checkProcessed++; } }, [] () { //create conf return createEmptyDocument(); }); }); int checkProcessedOnInstall = 0; fwService->setOnInstall([&checkProcessedOnInstall] (const char *location) { checkProcessedOnInstall++; return true; }); int checkProcessedOnInstallStatus = 0; fwService->setInstallationStatusInput([&checkProcessed, &checkProcessedOnInstallStatus] () { if (checkProcessed == 0) { if (checkProcessedOnInstallStatus == 0) checkProcessedOnInstallStatus = 1; return InstallationStatus::NotInstalled; } else if (checkProcessed == 1) { if (checkProcessedOnInstallStatus == 1) checkProcessedOnInstallStatus = 2; return InstallationStatus::InstallationFailed; } else if (checkProcessed == 2) { if (checkProcessedOnInstallStatus == 2) checkProcessedOnInstallStatus = 3; return InstallationStatus::NotInstalled; } else { if (checkProcessedOnInstallStatus == 3) checkProcessedOnInstallStatus = 4; return InstallationStatus::InstallationFailed; } }); getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "UpdateFirmware", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); auto payload = doc->to(); payload["location"] = FTP_URL; payload["retries"] = 2; payload["retrieveDate"] = BASE_TIME; payload["retryInterval"] = 10; return doc;}, [] (JsonObject) { } //ignore conf ))); for (unsigned int i = 0; i < 10; i++) { loop(); mtime += 5000; } REQUIRE( checkProcessed == 4 ); //all FirmwareStatusNotification messages have been received REQUIRE( checkProcessedOnInstall == 2 ); REQUIRE( checkProcessedOnInstallStatus == 4 ); } SECTION("Wait for retreiveDate and charging sessions end") { beginTransaction("mIdTag"); int checkProcessed = 0; getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { return new Ocpp16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req if (checkProcessed == 0) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloading") ); checkProcessed++; } else if (checkProcessed == 1) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloaded") ); checkProcessed++; } else if (checkProcessed == 2) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installing") ); checkProcessed++; } else if (checkProcessed == 3) { REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installed") ); checkProcessed++; } }, [] () { //create conf return createEmptyDocument(); }); }); bool checkProcessedOnDownload = false; fwService->setOnDownload([&checkProcessedOnDownload] (const char *location) { checkProcessedOnDownload = true; return true; }); int checkProcessedOnDownloadStatus = 0; fwService->setDownloadStatusInput([&checkProcessed, &checkProcessedOnDownloadStatus] () { if (checkProcessed == 0) { if (checkProcessedOnDownloadStatus == 0) checkProcessedOnDownloadStatus = 1; return DownloadStatus::NotDownloaded; } else { if (checkProcessedOnDownloadStatus == 1) checkProcessedOnDownloadStatus = 2; return DownloadStatus::Downloaded; } }); bool checkProcessedOnInstall = false; fwService->setOnInstall([&checkProcessedOnInstall] (const char *location) { checkProcessedOnInstall = true; return true; }); int checkProcessedOnInstallStatus = 0; fwService->setInstallationStatusInput([&checkProcessed, &checkProcessedOnInstallStatus] () { if (checkProcessed <= 2) { if (checkProcessedOnInstallStatus == 0) checkProcessedOnInstallStatus = 1; return InstallationStatus::NotInstalled; } else { if (checkProcessedOnInstallStatus == 1) checkProcessedOnInstallStatus = 2; return InstallationStatus::Installed; } }); getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "UpdateFirmware", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); auto payload = doc->to(); payload["location"] = FTP_URL; payload["retries"] = 1; payload["retrieveDate"] = BASE_TIME_1H; payload["retryInterval"] = 1; return doc;}, [] (JsonObject) { } //ignore conf ))); for (unsigned int i = 0; i < 10; i++) { loop(); mtime += 5000; } //retreiveDate not reached yet REQUIRE( checkProcessed == 0 ); REQUIRE( !checkProcessedOnDownload ); REQUIRE( checkProcessedOnDownloadStatus == 0 ); REQUIRE( !checkProcessedOnInstall ); REQUIRE( checkProcessedOnInstallStatus == 0 ); getOcppContext()->getModel().getClock().setTime(BASE_TIME_1H); for (unsigned int i = 0; i < 10; i++) { loop(); mtime += 5000; } //download-related FirmwareStatusNotification messages have been received REQUIRE( checkProcessed == 2 ); REQUIRE( checkProcessedOnDownload ); REQUIRE( checkProcessedOnDownloadStatus == 2 ); REQUIRE( !checkProcessedOnInstall ); REQUIRE( checkProcessedOnInstallStatus == 0 ); endTransaction(); for (unsigned int i = 0; i < 10; i++) { loop(); mtime += 5000; } //all FirmwareStatusNotification messages have been received REQUIRE( checkProcessed == 4 ); REQUIRE( checkProcessedOnDownload ); REQUIRE( checkProcessedOnDownloadStatus == 2 ); REQUIRE( checkProcessedOnInstall ); REQUIRE( checkProcessedOnInstallStatus == 2 ); } mocpp_deinitialize(); } ================================================ FILE: tests/LocalAuthList.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_LOCAL_AUTH #include #include #include #include "./helpers/testHelper.h" #include #include #include #include #include #include #include #define BASE_TIME "2023-01-01T00:00:00.000Z" using namespace MicroOcpp; void generateAuthList(JsonArray out, size_t size, bool compact) { for (size_t i = 0; i < size; i++) { JsonObject authData = out.createNestedObject(); JsonObject idTagInfo; if (compact) { //flat structure idTagInfo = authData; } else { //nested idTagInfo idTagInfo = authData["idTagInfo"].to(); } char buf [IDTAG_LEN_MAX + 1]; sprintf(buf, "mIdTag%zu", i); authData[AUTHDATA_KEY_IDTAG(compact)] = buf; idTagInfo[AUTHDATA_KEY_STATUS(compact)] = "Accepted"; } } TEST_CASE( "LocalAuth" ) { printf("\nRun %s\n", "LocalAuth"); //clean state auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_set_timer(custom_timer_cb); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); auto& model = getOcppContext()->getModel(); auto authService = model.getAuthorizationService(); auto connector = model.getConnector(1); model.getClock().setTime(BASE_TIME); loop(); //enable local auth declareConfiguration("LocalAuthListEnabled", true)->setBool(true); //set Authorize timeout after which the charger is considered offline const unsigned long AUTH_TIMEOUT_MS = 60000; MicroOcpp::declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", -1)->setInt(AUTH_TIMEOUT_MS / 1000); //fetch local auth configs auto localAuthorizeOffline = declareConfiguration("LocalAuthorizeOffline", false); auto localPreAuthorize = declareConfiguration("LocalPreAuthorize", false); SECTION("Basic local auth - LocalPreAuthorize") { localAuthorizeOffline->setBool(false); localPreAuthorize->setBool(true); //set local list StaticJsonDocument<256> localAuthList; localAuthList[0]["idTag"] = "mIdTag"; localAuthList[0]["idTagInfo"]["status"] = "Accepted"; REQUIRE( authService->updateLocalList(localAuthList.as(), 1, false) ); REQUIRE( authService->getLocalListSize() == 1 ); REQUIRE( authService->getLocalAuthorization("mIdTag") != nullptr ); REQUIRE( authService->getLocalAuthorization("mIdTag")->getAuthorizationStatus() == AuthorizationStatus::Accepted ); //check TX notification bool checkTxAuthorized = false; setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { if (txNotification == TxNotification_Authorized) { checkTxAuthorized = true; } }); //begin transaction and delay Authorize request - tx should start immediately loopback.setOnline(false); //Authorize delayed by short offline period REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("mIdTag"); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( checkTxAuthorized ); loopback.setOnline(true); endTransaction(); loop(); //begin transaction delay Authorize request, but idTag doesn't match local list - tx should start when online again checkTxAuthorized = false; loopback.setOnline(false); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("wrong idTag"); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); REQUIRE( !checkTxAuthorized ); loopback.setOnline(true); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( checkTxAuthorized ); endTransaction(); loop(); } SECTION("Basic local auth - LocalAuthorizeOffline") { localAuthorizeOffline->setBool(true); localPreAuthorize->setBool(false); //set local list StaticJsonDocument<256> localAuthList; localAuthList[0]["idTag"] = "mIdTag"; localAuthList[0]["idTagInfo"]["status"] = "Accepted"; authService->updateLocalList(localAuthList.as(), 1, false); //check TX notification bool checkTxAuthorized = false; setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { if (txNotification == TxNotification_Authorized) { checkTxAuthorized = true; } }); //make charger offline and begin tx - tx should begin after Authorize timeout loopback.setOnline(false); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); unsigned long t_before = mocpp_tick_ms(); beginTransaction("mIdTag"); loop(); REQUIRE( mocpp_tick_ms() - t_before < AUTH_TIMEOUT_MS ); //if this fails, increase AUTH_TIMEOUT_MS REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); REQUIRE( !checkTxAuthorized ); mtime += AUTH_TIMEOUT_MS - (mocpp_tick_ms() - t_before); //increment clock so that auth timeout is exceeded loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( checkTxAuthorized ); loopback.setOnline(true); endTransaction(); loop(); //make charger offline and begin tx, but idTag doesn't match - tx should be aborted bool checkTxTimeout = false; setTxNotificationOutput([&checkTxTimeout] (Transaction*, TxNotification txNotification) { if (txNotification == TxNotification_AuthorizationTimeout) { checkTxTimeout = true; } }); loopback.setOnline(false); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); t_before = mocpp_tick_ms(); beginTransaction("wrong idTag"); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); REQUIRE( !checkTxTimeout ); mtime += AUTH_TIMEOUT_MS - (mocpp_tick_ms() - t_before); //increment clock so that auth timeout is exceeded loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); REQUIRE( checkTxTimeout ); loopback.setOnline(true); loop(); } SECTION("Basic local auth - AllowOfflineTxForUnknownId") { localAuthorizeOffline->setBool(false); localPreAuthorize->setBool(false); MicroOcpp::declareConfiguration("AllowOfflineTxForUnknownId", true)->setBool(true); //check TX notification bool checkTxAuthorized = false; setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { if (txNotification == TxNotification_Authorized) { checkTxAuthorized = true; } }); //make charger offline and begin tx - tx should begin after Authorize timeout loopback.setOnline(false); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); unsigned long t_before = mocpp_tick_ms(); beginTransaction("unknownIdTag"); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); REQUIRE( !checkTxAuthorized ); mtime += AUTH_TIMEOUT_MS - (mocpp_tick_ms() - t_before); //increment clock so that auth timeout is exceeded loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( checkTxAuthorized ); loopback.setOnline(true); endTransaction(); loop(); } SECTION("Local auth - check WS online status") { localAuthorizeOffline->setBool(false); localPreAuthorize->setBool(false); MicroOcpp::declareConfiguration("AllowOfflineTxForUnknownId", true)->setBool(true); //check TX notification bool checkTxAuthorized = false; setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { if (txNotification == TxNotification_Authorized) { checkTxAuthorized = true; } }); //disconnect WS and begin tx - charger should enter offline mode immediately loopback.setConnected(false); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("unknownIdTag"); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( checkTxAuthorized ); loopback.setConnected(true); endTransaction(); loop(); } SECTION("Local auth list entry expired / unauthorized") { localPreAuthorize->setBool(true); //set local list with expired / unauthorized entry StaticJsonDocument<512> localAuthList; localAuthList[0]["idTag"] = "mIdTagExpired"; localAuthList[0]["idTagInfo"]["status"] = "Accepted"; localAuthList[0]["idTagInfo"]["expiryDate"] = BASE_TIME; //is in past localAuthList[1]["idTag"] = "mIdTagUnauthorized"; localAuthList[1]["idTagInfo"]["status"] = "Blocked"; authService->updateLocalList(localAuthList.as(), 1, false); REQUIRE( authService->getLocalAuthorization("mIdTagExpired") ); REQUIRE( authService->getLocalAuthorization("mIdTagUnauthorized") ); REQUIRE( authService->getLocalAuthorization("mIdTagExpired") ); //begin transaction and delay Authorize request - cannot PreAuthorize because entry is expired loopback.setOnline(false); //Authorize delayed by short offline period REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("mIdTagExpired"); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); loopback.setOnline(true); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); endTransaction(); loop(); //begin transaction and delay Authorize request - cannot PreAuthorize because entry is unauthorized loopback.setOnline(false); //Authorize delayed by short offline period REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("mIdTagUnauthorized"); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); loopback.setOnline(true); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); endTransaction(); loop(); } SECTION("Mix local authorization modes") { localAuthorizeOffline->setBool(true); localPreAuthorize->setBool(true); MicroOcpp::declareConfiguration("AllowOfflineTxForUnknownId", true)->setBool(true); //set local list with accepted and accepted entry StaticJsonDocument<512> localAuthList; localAuthList[0]["idTag"] = "mIdTagExpired"; localAuthList[0]["idTagInfo"]["status"] = "Accepted"; localAuthList[0]["idTagInfo"]["expiryDate"] = BASE_TIME; //is in past localAuthList[1]["idTag"] = "mIdTagAccepted"; localAuthList[1]["idTagInfo"]["status"] = "Accepted"; authService->updateLocalList(localAuthList.as(), 1, false); REQUIRE( authService->getLocalAuthorization("mIdTagExpired") ); REQUIRE( authService->getLocalAuthorization("mIdTagAccepted") ); //begin transaction and delay Authorize request - tx should start immediately loopback.setOnline(false); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("mIdTagAccepted"); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); loopback.setOnline(true); endTransaction(); loop(); //begin transaction, but idTag is expired - AllowOfflineTxForUnknownId must not apply loopback.setOnline(false); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); unsigned long t_before = mocpp_tick_ms(); beginTransaction("mIdTagExpired"); loop(); REQUIRE( mocpp_tick_ms() - t_before < AUTH_TIMEOUT_MS ); //if this fails, increase AUTH_TIMEOUT_MS REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); mtime += AUTH_TIMEOUT_MS - (mocpp_tick_ms() - t_before); //increment clock so that auth timeout is exceeded loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); loopback.setOnline(true); loop(); } SECTION("DeAuthorize locally authorized tx") { localAuthorizeOffline->setBool(false); localPreAuthorize->setBool(true); //check TX notification bool checkTxAuthorized = false; setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { if (txNotification == TxNotification_Authorized) { checkTxAuthorized = true; } }); //set local list StaticJsonDocument<256> localAuthList; localAuthList[0]["idTag"] = "mIdTag"; localAuthList[0]["idTagInfo"]["status"] = "Accepted"; authService->updateLocalList(localAuthList.as(), 1, false); //patch Authorize so it will reject all idTags getOcppContext()->getOperationRegistry().registerOperation("Authorize", [] () { return new Ocpp16::CustomOperation("Authorize", [] (JsonObject) {}, //ignore req [] () { //create conf auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTagInfo"]["status"] = "Blocked"; return doc; });}); //begin transaction and delay Authorize request - tx should start immediately loopback.setOnline(false); //Authorize delayed by short offline period REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("mIdTag"); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( checkTxAuthorized ); //check TX notification bool checkTxRejected = false; setTxNotificationOutput([&checkTxRejected] (Transaction*, TxNotification txNotification) { if (txNotification == TxNotification_AuthorizationRejected) { checkTxRejected = true; } }); loopback.setOnline(true); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); REQUIRE( checkTxRejected ); loop(); } SECTION("LocalListConflict") { //patch Authorize so it will reject all idTags bool checkAuthorize = false; getOcppContext()->getOperationRegistry().registerOperation("Authorize", [&checkAuthorize] () { return new Ocpp16::CustomOperation("Authorize", [&checkAuthorize] (JsonObject) { checkAuthorize = true; }, [] () { //create conf auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTagInfo"]["status"] = "Blocked"; return doc; });}); //patch StartTransaction so it will DeAuthorize all txs bool checkStartTx = false; getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&checkStartTx] () { return new Ocpp16::CustomOperation("StartTransaction", [&checkStartTx] (JsonObject) { checkStartTx = true; }, [] () { //create conf auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTagInfo"]["status"] = "Blocked"; payload["transactionId"] = 1000; return doc; });}); //check resulting StatusNotification message bool checkLocalListConflict = false; getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", [&checkLocalListConflict] () { return new Ocpp16::CustomOperation("StatusNotification", [&checkLocalListConflict] (JsonObject payload) { if (payload["connectorId"] == 0 && !strcmp(payload["errorCode"] | "_Undefined", "LocalListConflict")) { checkLocalListConflict = true; } }, [] () { //create conf return createEmptyDocument(); });}); //set local list StaticJsonDocument<256> localAuthList; localAuthList[0]["idTag"] = "mIdTag"; localAuthList[0]["idTagInfo"]["status"] = "Accepted"; authService->updateLocalList(localAuthList.as(), 1, false); //send Authorize and StartTx and check if they trigger LocalListConflict beginTransaction("mIdTag"); loop(); REQUIRE( checkLocalListConflict ); REQUIRE( checkAuthorize ); REQUIRE( !checkStartTx ); checkLocalListConflict = false; checkAuthorize = false; beginTransaction_authorized("mIdTag"); loop(); REQUIRE( checkLocalListConflict ); REQUIRE( !checkAuthorize ); REQUIRE( checkStartTx ); } SECTION("Update local list") { REQUIRE( authService->getLocalListSize() == 0 ); //idle, empty local list //set local list int localListVersion = 42; StaticJsonDocument<256> localAuthList; localAuthList[0]["idTag"] = "mIdTag"; localAuthList[0]["idTagInfo"]["status"] = "Accepted"; authService->updateLocalList(localAuthList.as(), localListVersion, false); REQUIRE( authService->getLocalListVersion() == localListVersion ); REQUIRE( authService->getLocalListSize() == 1 ); REQUIRE( authService->getLocalAuthorization("mIdTag") != nullptr ); //overwrite list localListVersion++; localAuthList.clear(); localAuthList[0]["idTag"] = "mIdTag2"; localAuthList[0]["idTagInfo"]["status"] = "Accepted"; authService->updateLocalList(localAuthList.as(), localListVersion, false); REQUIRE( authService->getLocalListVersion() == localListVersion ); REQUIRE( authService->getLocalListSize() == 1 ); REQUIRE( authService->getLocalAuthorization("mIdTag") == nullptr ); REQUIRE( authService->getLocalAuthorization("mIdTag2") != nullptr ); //reset list localListVersion++; localAuthList.clear(); localAuthList.to(); authService->updateLocalList(localAuthList.as(), localListVersion, false); REQUIRE( authService->getLocalListVersion() == 0 ); //localListVersion is ignored - empty list always resets version REQUIRE( authService->getLocalListSize() == 0 ); //consecutive updates in Differential mode localListVersion = 1; localAuthList.clear(); localAuthList[0]["idTag"] = "mIdTag"; localAuthList[0]["idTagInfo"]["status"] = "Accepted"; authService->updateLocalList(localAuthList.as(), localListVersion, true); REQUIRE( authService->getLocalListVersion() == localListVersion ); REQUIRE( authService->getLocalListSize() == 1 ); //append further entry localListVersion++; localAuthList.clear(); localAuthList[0]["idTag"] = "mIdTag2"; localAuthList[0]["idTagInfo"]["status"] = "Accepted"; authService->updateLocalList(localAuthList.as(), localListVersion, true); REQUIRE( authService->getLocalListVersion() == localListVersion ); REQUIRE( authService->getLocalListSize() == 2 ); //overwrite previous entry REQUIRE( authService->getLocalAuthorization("mIdTag")->getAuthorizationStatus() == AuthorizationStatus::Accepted ); localListVersion++; localAuthList.clear(); localAuthList[0]["idTag"] = "mIdTag"; localAuthList[0]["idTagInfo"]["status"] = "Blocked"; authService->updateLocalList(localAuthList.as(), localListVersion, true); REQUIRE( authService->getLocalListVersion() == localListVersion ); REQUIRE( authService->getLocalListSize() == 2 ); REQUIRE( authService->getLocalAuthorization("mIdTag")->getAuthorizationStatus() == AuthorizationStatus::Blocked ); //empty update keeps previous entries localListVersion++; localAuthList.clear(); localAuthList.to(); authService->updateLocalList(localAuthList.as(), localListVersion, true); REQUIRE( authService->getLocalListVersion() == localListVersion ); REQUIRE( authService->getLocalListSize() == 2 ); //delete entries localListVersion++; localAuthList.clear(); localAuthList[0]["idTag"] = "mIdTag"; authService->updateLocalList(localAuthList.as(), localListVersion, true); REQUIRE( authService->getLocalListVersion() == localListVersion ); REQUIRE( authService->getLocalListSize() == 1 ); localListVersion++; localAuthList.clear(); localAuthList[0]["idTag"] = "mIdTag2"; authService->updateLocalList(localAuthList.as(), localListVersion, true); REQUIRE( authService->getLocalListVersion() == 0 ); REQUIRE( authService->getLocalListSize() == 0 ); } SECTION("LocalList persistency") { int listVersion = 42; StaticJsonDocument<512> localAuthList; localAuthList[0]["idTag"] = "mIdTagMinimal"; localAuthList[0]["idTagInfo"]["status"] = "Accepted"; localAuthList[1]["idTag"] = "mIdTagFull"; localAuthList[1]["idTagInfo"]["expiryDate"] = BASE_TIME; localAuthList[1]["idTagInfo"]["parentIdTag"] = "mParentIdTag"; localAuthList[1]["idTagInfo"]["status"] = "Blocked"; authService->updateLocalList(localAuthList.as(), listVersion, false); mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); authService = getOcppContext()->getModel().getAuthorizationService(); REQUIRE( authService->getLocalListVersion() == listVersion ); REQUIRE( authService->getLocalListSize() == 2 ); auto auth0 = authService->getLocalAuthorization("mIdTagMinimal"); REQUIRE( auth0 != nullptr ); REQUIRE( !strcmp(auth0->getIdTag(), "mIdTagMinimal") ); REQUIRE( auth0->getExpiryDate() == nullptr ); REQUIRE( auth0->getParentIdTag() == nullptr ); REQUIRE( auth0->getAuthorizationStatus() == AuthorizationStatus::Accepted ); auto auth1 = authService->getLocalAuthorization("mIdTagFull"); REQUIRE( auth1 != nullptr ); REQUIRE( !strcmp(auth1->getIdTag(), "mIdTagFull") ); REQUIRE( auth1->getExpiryDate() != nullptr ); Timestamp baseTimeParsed; baseTimeParsed.setTime(BASE_TIME); REQUIRE( *auth1->getExpiryDate() == baseTimeParsed ); REQUIRE( !strcmp(auth1->getParentIdTag(), "mParentIdTag") ); REQUIRE( auth1->getAuthorizationStatus() == AuthorizationStatus::Blocked ); } SECTION("SendLocalList") { int listVersion = 42; size_t listSize = 2; auto populatedEntryIdTag = makeString("UnitTests"); //local auth list entry to be fully populated //Full update - happy path bool checkAccepted = false; getOcppContext()->initiateRequest(makeRequest( new Ocpp16::CustomOperation("SendLocalList", [&listVersion, &listSize, &populatedEntryIdTag] () { //create req auto doc = makeJsonDoc("UnitTests", 4096); auto payload = doc->to(); payload["listVersion"] = listVersion; generateAuthList(payload["localAuthorizationList"].to(), listSize, false); //fully populate first entry populatedEntryIdTag = payload["localAuthorizationList"][0]["idTag"] | "_Undefined"; payload["localAuthorizationList"][0]["idTagInfo"]["expiryDate"] = BASE_TIME; payload["localAuthorizationList"][0]["idTagInfo"]["parentIdTag"] = "mParentIdTag"; payload["localAuthorizationList"][0]["idTagInfo"]["status"] = "Blocked"; payload["updateType"] = "Full"; return doc; }, [&checkAccepted] (JsonObject payload) { //process conf checkAccepted = !strcmp(payload["status"] | "_Undefined", "Accepted"); }))); loop(); REQUIRE( authService->getLocalListVersion() == listVersion ); REQUIRE( authService->getLocalListSize() == listSize ); REQUIRE( !populatedEntryIdTag.empty() ); auto localAuth = authService->getLocalAuthorization(populatedEntryIdTag.c_str()); REQUIRE( localAuth != nullptr ); Timestamp baseTimeParsed; baseTimeParsed.setTime(BASE_TIME); REQUIRE( localAuth->getExpiryDate() ); REQUIRE( *localAuth->getExpiryDate() == baseTimeParsed ); REQUIRE( !strcmp(localAuth->getIdTag(), populatedEntryIdTag.c_str()) ); REQUIRE( !strcmp(localAuth->getParentIdTag(), "mParentIdTag") ); REQUIRE( localAuth->getAuthorizationStatus() == AuthorizationStatus::Blocked ); REQUIRE( checkAccepted ); //Differential update - happy path listVersion++; listSize++; //add one extra entry and update all others checkAccepted = false; getOcppContext()->initiateRequest(makeRequest( new Ocpp16::CustomOperation("SendLocalList", [&listVersion, &listSize] () { //create req auto doc = makeJsonDoc("UnitTests", 1024); auto payload = doc->to(); payload["listVersion"] = listVersion; generateAuthList(payload["localAuthorizationList"].to(), listSize, false); payload["updateType"] = "Differential"; return doc; }, [&checkAccepted] (JsonObject payload) { //process conf checkAccepted = !strcmp(payload["status"] | "_Undefined", "Accepted"); }))); loop(); REQUIRE( authService->getLocalListVersion() == listVersion ); REQUIRE( authService->getLocalListSize() == listSize ); REQUIRE( checkAccepted ); //Differential update - version mismatch size_t listSizeInvalid = listSize + 1; bool checkVersionMismatch = false; getOcppContext()->initiateRequest(makeRequest( new Ocpp16::CustomOperation("SendLocalList", [&listVersion, &listSizeInvalid] () { //create req auto doc = makeJsonDoc("UnitTests", 1024); auto payload = doc->to(); payload["listVersion"] = listVersion; generateAuthList(payload["localAuthorizationList"].to(), listSizeInvalid, false); payload["updateType"] = "Differential"; return doc; }, [&checkVersionMismatch] (JsonObject payload) { //process conf checkVersionMismatch = !strcmp(payload["status"] | "_Undefined", "VersionMismatch"); }))); loop(); REQUIRE( authService->getLocalListVersion() == listVersion ); REQUIRE( authService->getLocalListSize() == listSize ); REQUIRE( checkVersionMismatch ); //Boundary check - maximum entries per SendLocalList listVersion = 42; listSize = (size_t) declareConfiguration("SendLocalListMaxLength", -1)->getInt(); checkAccepted = false; getOcppContext()->initiateRequest(makeRequest( new Ocpp16::CustomOperation("SendLocalList", [&listVersion, &listSize] () { //create req auto doc = makeJsonDoc("UnitTests", 4096); auto payload = doc->to(); payload["listVersion"] = listVersion; generateAuthList(payload["localAuthorizationList"].to(), listSize, false); payload["updateType"] = "Full"; return doc; }, [&checkAccepted] (JsonObject payload) { //process conf checkAccepted = !strcmp(payload["status"] | "_Undefined", "Accepted"); }))); loop(); REQUIRE( authService->getLocalListVersion() == listVersion ); REQUIRE( authService->getLocalListSize() == listSize ); REQUIRE( checkAccepted ); //Boundary check - maximum entries per SendLocalList in Differential mode listVersion++; checkAccepted = false; getOcppContext()->initiateRequest(makeRequest( new Ocpp16::CustomOperation("SendLocalList", [&listVersion, &listSize] () { //create req auto doc = makeJsonDoc("UnitTests", 4096); auto payload = doc->to(); payload["listVersion"] = listVersion; generateAuthList(payload["localAuthorizationList"].to(), listSize, false); payload["updateType"] = "Differential"; return doc; }, [&checkAccepted] (JsonObject payload) { //process conf checkAccepted = !strcmp(payload["status"] | "_Undefined", "Accepted"); }))); loop(); REQUIRE( authService->getLocalListVersion() == listVersion ); REQUIRE( authService->getLocalListSize() == listSize ); REQUIRE( checkAccepted ); //Boundary check - exceed maximum entries per SendLocalList int listVersionInvalid = listVersion + 1; listSizeInvalid = listSize + 1; bool errOccurence = false; getOcppContext()->initiateRequest(makeRequest( new Ocpp16::CustomOperation("SendLocalList", [&listVersionInvalid, &listSizeInvalid] () { //create req auto doc = makeJsonDoc("UnitTests", 4096); auto payload = doc->to(); payload["listVersion"] = listVersionInvalid; generateAuthList(payload["localAuthorizationList"].to(), listSizeInvalid, false); payload["updateType"] = "Full"; return doc; }, [] (JsonObject) { }, //ignore conf [&errOccurence] (const char *errorCode, const char*, JsonObject) { errOccurence = !strcmp(errorCode, "OccurenceConstraintViolation"); return true; }))); loop(); REQUIRE( authService->getLocalListVersion() == listVersion ); REQUIRE( authService->getLocalListSize() == listSize ); REQUIRE( errOccurence ); //Boundary check - exceed maximum local list size by multiple Differerntial updates //clear local list StaticJsonDocument<64> emptyDoc; emptyDoc.to(); authService->updateLocalList(emptyDoc.as(), 1, false); size_t localAuthListMaxLength = (size_t) declareConfiguration("LocalAuthListMaxLength", -1)->getInt(); //send Differential lists with 2 entries: update an existing entry and add a new entry for (size_t i = 1; i < localAuthListMaxLength; i++) { //Full update - happy path bool checkAccepted = false; getOcppContext()->initiateRequest(makeRequest( new Ocpp16::CustomOperation("SendLocalList", [&i] () { //create req auto doc = makeJsonDoc("UnitTests", 1024); auto payload = doc->to(); payload["listVersion"] = (int) i; char buf [IDTAG_LEN_MAX + 1]; sprintf(buf, "mIdTag%zu", i-1); payload["localAuthorizationList"][0]["idTag"] = buf; payload["localAuthorizationList"][0]["idTagInfo"]["status"] = "Accepted"; sprintf(buf, "mIdTag%zu", i); payload["localAuthorizationList"][1]["idTag"] = buf; payload["localAuthorizationList"][1]["idTagInfo"]["status"] = "Accepted"; payload["updateType"] = "Differential"; return doc; }, [&checkAccepted] (JsonObject payload) { //process conf checkAccepted = !strcmp(payload["status"] | "_Undefined", "Accepted"); }))); loop(); REQUIRE( authService->getLocalListVersion() == (int)i ); REQUIRE( authService->getLocalListSize() == i + 1 ); REQUIRE( checkAccepted ); } //now exceed local list by sending overflow entry listVersion = authService->getLocalListVersion(); listVersionInvalid = listVersion + 1; listSize = authService->getLocalListSize(); bool checkFailed = false; getOcppContext()->initiateRequest(makeRequest( new Ocpp16::CustomOperation("SendLocalList", [&listVersionInvalid] () { //create req auto doc = makeJsonDoc("UnitTests", 1024); auto payload = doc->to(); payload["listVersion"] = listVersionInvalid; //update already existing entry char buf [IDTAG_LEN_MAX + 1]; sprintf(buf, "mIdTag%zu", 0UL); payload["localAuthorizationList"][0]["idTag"] = buf; payload["localAuthorizationList"][0]["idTagInfo"]["status"] = "Accepted"; //additional overflowing entry payload["localAuthorizationList"][1]["idTag"] = "overflow idTag"; payload["localAuthorizationList"][1]["idTagInfo"]["status"] = "Accepted"; payload["updateType"] = "Differential"; return doc; }, [&checkFailed] (JsonObject payload) { //process conf checkFailed = !strcmp(payload["status"] | "_Undefined", "Failed"); }))); loop(); REQUIRE( authService->getLocalListVersion() == listVersion ); REQUIRE( authService->getLocalListSize() == listSize ); REQUIRE( checkFailed ); } SECTION("GetLocalListVersion") { int localListVersion = 42; StaticJsonDocument<256> localAuthList; localAuthList[0]["idTag"] = "mIdTag"; localAuthList[0]["idTagInfo"]["status"] = "Accepted"; authService->updateLocalList(localAuthList.as(), localListVersion, false); int checkListVerion = -1; getOcppContext()->initiateRequest(makeRequest( new Ocpp16::CustomOperation("GetLocalListVersion", [] () { //create req return createEmptyDocument(); }, [&checkListVerion] (JsonObject payload) { //process conf checkListVerion = payload["listVersion"] | -1; }))); loop(); REQUIRE( checkListVerion == localListVersion ); } mocpp_deinitialize(); } #endif //MO_ENABLE_LOCAL_AUTH ================================================ FILE: tests/Metering.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include "./helpers/testHelper.h" #define BASE_TIME "2023-01-01T00:00:00.000Z" #define TRIGGER_METERVALUES "[2,\"msgId01\",\"TriggerMessage\",{\"requestedMessage\":\"MeterValues\"}]" using namespace MicroOcpp; TEST_CASE("Metering") { printf("\nRun %s\n", "Metering"); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); auto context = getOcppContext(); auto& model = context->getModel(); mocpp_set_timer(custom_timer_cb); model.getClock().setTime(BASE_TIME); endTransaction(); SECTION("Configure Metering Service") { addMeterValueInput([] () { return 0; }, "Energy.Active.Import.Register"); addMeterValueInput([] () { return 0; }, "Voltage"); auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData",""); MeterValuesSampledDataString->setString(""); bool checkProcessed = false; //set up measurands and check validation sendRequest("ChangeConfiguration", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = "MeterValuesSampledData"; payload["value"] = "Energy.Active.Import.Register,INVALID,Voltage"; //invalid request return doc; }, [&checkProcessed] (JsonObject payload) { checkProcessed = true; REQUIRE(!strcmp(payload["status"], "Rejected")); }); loop(); REQUIRE(checkProcessed); REQUIRE(!strcmp(MeterValuesSampledDataString->getString(), "")); checkProcessed = false; sendRequest("ChangeConfiguration", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = "MeterValuesSampledData"; payload["value"] = "Voltage,Energy.Active.Import.Register"; //valid request return doc; }, [&checkProcessed, &model] (JsonObject payload) { checkProcessed = true; REQUIRE(!strcmp(payload["status"], "Accepted")); }); loop(); REQUIRE(checkProcessed); REQUIRE(!strcmp(MeterValuesSampledDataString->getString(), "Voltage,Energy.Active.Import.Register")); } SECTION("Capture Periodic data") { Timestamp base; base.setTime(BASE_TIME); addMeterValueInput([base] () { //simulate 3600W consumption return getOcppContext()->getModel().getClock().now() - base; }, "Energy.Active.Import.Register"); auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(10); bool checkProcessed = false; setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; Timestamp t0; t0.setTime(payload["meterValue"][0]["timestamp"] | ""); REQUIRE((t0 - base >= 10 && t0 - base <= 11)); REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["context"] | "", "Sample.Periodic")); }); loop(); model.getClock().setTime(BASE_TIME); auto trackMtime = mtime; beginTransaction_authorized("mIdTag"); loop(); mtime = trackMtime + 10 * 1000; loop(); endTransaction(); loop(); REQUIRE(checkProcessed); } SECTION("Capture Clock-aligned data") { Timestamp base; base.setTime(BASE_TIME); addMeterValueInput([base] () { //simulate 3600W consumption return getOcppContext()->getModel().getClock().now() - base; }, "Energy.Active.Import.Register"); //disablee sampled data auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(0); auto ClockAlignedDataIntervalInt = declareConfiguration("ClockAlignedDataInterval", 0, CONFIGURATION_FN); ClockAlignedDataIntervalInt->setInt(900); auto MeterValuesAlignedDataString = declareConfiguration("MeterValuesAlignedData", "", CONFIGURATION_FN); MeterValuesAlignedDataString->setString("Energy.Active.Import.Register"); bool checkProcessed = false; setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; Timestamp t0; t0.setTime(payload["meterValue"][0]["timestamp"] | ""); REQUIRE((t0 - base >= 900 && t0 - base <= 901)); REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["context"] | "", "Sample.Clock")); }); model.getClock().setTime("2023-01-01T00:00:10Z"); loop(); beginTransaction_authorized("mIdTag"); loop(); model.getClock().setTime("2023-01-01T00:10:00Z"); loop(); model.getClock().setTime("2023-01-01T00:15:00Z"); loop(); model.getClock().setTime("2023-01-01T00:29:50Z"); endTransaction(); loop(); REQUIRE(checkProcessed); } SECTION("Capture transaction-aligned data") { Timestamp base; base.setTime(BASE_TIME); addMeterValueInput([base] () { //simulate 3600W consumption return getOcppContext()->getModel().getClock().now() - base; }, "Energy.Active.Import.Register"); auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(10); auto StopTxnSampledDataString = declareConfiguration("StopTxnSampledData", "", CONFIGURATION_FN); StopTxnSampledDataString->setString("Energy.Active.Import.Register"); configuration_save(); loop(); model.getClock().setTime(BASE_TIME); beginTransaction_authorized("mIdTag"); loop(); mocpp_deinitialize(); //check if StopData is stored over reboots mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); addMeterValueInput([base] () { //simulate 3600W consumption return getOcppContext()->getModel().getClock().now() - base; }, "Energy.Active.Import.Register"); bool checkProcessed = false; setOnReceiveRequest("StopTransaction", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; REQUIRE(payload["transactionData"].size() >= 2); Timestamp t0, t1; t0.setTime(payload["transactionData"][0]["timestamp"] | ""); t1.setTime(payload["transactionData"][1]["timestamp"] | ""); REQUIRE((t0 - base >= 0 && t0 - base <= 1)); REQUIRE((t1 - base >= 3600 && t1 - base <= 3601)); REQUIRE(!strcmp(payload["transactionData"][0]["sampledValue"][0]["context"] | "", "Transaction.Begin")); REQUIRE(!strcmp(payload["transactionData"][1]["sampledValue"][0]["context"] | "", "Transaction.End")); }); loop(); getOcppContext()->getModel().getClock().setTime("2023-01-01T01:00:00Z"); endTransaction(); loop(); REQUIRE(checkProcessed); } SECTION("Capture measurements at connectorId 0") { Timestamp base; base.setTime(BASE_TIME); const unsigned int connectorId = 0; addMeterValueInput([base] () { //simulate 3600W consumption return 3600; }, "Power.Active.Import", nullptr, nullptr, nullptr, connectorId); auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); MeterValuesSampledDataString->setString("Power.Active.Import"); auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(10); bool checkProcessed = false; setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; REQUIRE( !strncmp(payload["meterValue"][0]["sampledValue"][0]["value"] | "", "3600", strlen("3600")) ); }); loop(); mtime += 10 * 1000; loop(); REQUIRE(checkProcessed); } SECTION("Change measurands live") { Timestamp base; base.setTime(BASE_TIME); addMeterValueInput([base] () { //simulate 3600W consumption return getOcppContext()->getModel().getClock().now() - base; }, "Energy.Active.Import.Register"); addMeterValueInput([] () { return 3600; }, "Power.Active.Import"); auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(10); bool checkProcessed = false; setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; Timestamp t0; t0.setTime(payload["meterValue"][0]["timestamp"] | ""); REQUIRE((t0 - base >= 10 && t0 - base <= 11)); REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["measurand"] | "", "Power.Active.Import")); }); loop(); model.getClock().setTime(BASE_TIME); auto trackMtime = mtime; beginTransaction_authorized("mIdTag"); MeterValuesSampledDataString->setString("Power.Active.Import"); loop(); mtime = trackMtime + 10 * 1000; loop(); endTransaction(); loop(); REQUIRE(checkProcessed); } SECTION("Preserve order of tx-related msgs") { loopback.setConnected(false); declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true)->setBool(true); Timestamp base; base.setTime(BASE_TIME); addMeterValueInput([base] () { //simulate 3600W consumption return getOcppContext()->getModel().getClock().now() - base; }, "Energy.Active.Import.Register"); auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(10); configuration_save(); unsigned int countProcessed = 0; setOnReceiveRequest("StartTransaction", [&countProcessed] (JsonObject) { REQUIRE(countProcessed == 0); countProcessed++; }); int assignedTxId = -1; setOnSendConf("StartTransaction", [&assignedTxId] (JsonObject conf) { assignedTxId = conf["transactionId"]; }); setOnReceiveRequest("MeterValues", [&countProcessed, &assignedTxId] (JsonObject req) { REQUIRE(countProcessed == 1); countProcessed++; int transactionId = req["transactionId"] | -1000; REQUIRE(assignedTxId == transactionId); }); setOnReceiveRequest("StopTransaction", [&countProcessed] (JsonObject) { REQUIRE(countProcessed == 2); countProcessed++; }); loop(); auto trackMtime = mtime; beginTransaction_authorized("mIdTag"); loop(); mtime = trackMtime + 10 * 1000; loop(); endTransaction(); loop(); loopback.setConnected(true); loop(); REQUIRE(countProcessed == 3); /* * Combine test case with power loss. Start tx before power loss, then enqueue 1 MV, then StopTx */ countProcessed = 0; beginTransaction("mIdTag"); loop(); mocpp_deinitialize(); loopback.setConnected(false); mocpp_initialize(loopback, ChargerCredentials()); getOcppContext()->getModel().getClock().setTime(BASE_TIME); base.setTime(BASE_TIME); addMeterValueInput([base] () { //simulate 3600W consumption return getOcppContext()->getModel().getClock().now() - base; }, "Energy.Active.Import.Register"); setOnReceiveRequest("MeterValues", [&countProcessed, &assignedTxId] (JsonObject req) { REQUIRE(countProcessed == 1); countProcessed++; int transactionId = req["transactionId"] | -1000; REQUIRE(assignedTxId == transactionId); }); setOnReceiveRequest("StopTransaction", [&countProcessed] (JsonObject) { REQUIRE(countProcessed == 2); countProcessed++; }); trackMtime = mtime; loop(); mtime = trackMtime + 10 * 1000; loop(); endTransaction(); loop(); loopback.setConnected(true); loop(); REQUIRE(countProcessed == 3); } SECTION("Queue multiple MeterValues") { Timestamp base; base.setTime(BASE_TIME); model.getClock().setTime(BASE_TIME); addMeterValueInput([base] () { //simulate 3600W consumption return getOcppContext()->getModel().getClock().now() - base; }, "Energy.Active.Import.Register"); auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(10); unsigned int nrInitiated = 0; unsigned int countProcessed = 0; setOnReceiveRequest("MeterValues", [&base, &nrInitiated, &countProcessed] (JsonObject payload) { countProcessed++; Timestamp t0; t0.setTime(payload["meterValue"][0]["timestamp"] | ""); REQUIRE((t0 - base >= 10 * ((int)nrInitiated - (MO_METERVALUES_CACHE_MAXSIZE - (int)countProcessed)) && t0 - base <= 1 + 10 * ((int)nrInitiated - (MO_METERVALUES_CACHE_MAXSIZE - (int)countProcessed)))); }); loop(); beginTransaction_authorized("mIdTag"); base = model.getClock().now(); auto trackMtime = mtime; loop(); loopback.setConnected(false); //initiate 10 more MeterValues than can be cached for (unsigned long i = 1; i <= 10 + MO_METERVALUES_CACHE_MAXSIZE; i++) { mtime = trackMtime + i * 10 * 1000; loop(); nrInitiated++; } loopback.setConnected(true); loop(); REQUIRE(countProcessed == MO_METERVALUES_CACHE_MAXSIZE); endTransaction(); loop(); } SECTION("Drop MeterValues for silent tx") { loopback.setConnected(false); declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true)->setBool(true); Timestamp base; base.setTime(BASE_TIME); addMeterValueInput([base] () { //simulate 3600W consumption return getOcppContext()->getModel().getClock().now() - base; }, "Energy.Active.Import.Register"); auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(10); configuration_save(); unsigned int countProcessed = 0; setOnReceiveRequest("StartTransaction", [&countProcessed] (JsonObject) { countProcessed++; }); int assignedTxId = -1; setOnSendConf("StartTransaction", [&assignedTxId] (JsonObject conf) { assignedTxId = conf["transactionId"]; }); setOnReceiveRequest("MeterValues", [&countProcessed, &assignedTxId] (JsonObject req) { countProcessed++; }); setOnReceiveRequest("StopTransaction", [&countProcessed] (JsonObject) { REQUIRE(countProcessed == 2); }); loop(); auto trackMtime = mtime; beginTransaction_authorized("mIdTag"); auto tx = getTransaction(); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); mtime = trackMtime + 10 * 1000; loop(); endTransaction(); loop(); tx->setSilent(); tx->commit(); loopback.setConnected(true); loop(); REQUIRE(countProcessed == 0); } SECTION("TxMsg retry behavior") { Timestamp base; addMeterValueInput([&base] () { //simulate 3600W consumption return getOcppContext()->getModel().getClock().now() - base; }, "Energy.Active.Import.Register"); auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(10); configuration_save(); const size_t NUM_ATTEMPTS = 3; const int RETRY_INTERVAL_SECS = 3600; declareConfiguration("TransactionMessageAttempts", 0)->setInt(NUM_ATTEMPTS); declareConfiguration("TransactionMessageRetryInterval", 0)->setInt(RETRY_INTERVAL_SECS); unsigned int attemptNr = 0; getOcppContext()->getOperationRegistry().registerOperation("MeterValues", [&attemptNr] () { return new Ocpp16::CustomOperation("MeterValues", [&attemptNr] (JsonObject payload) { //receive req attemptNr++; }, [] () { //create conf return createEmptyDocument(); }, [] () { //ErrorCode for CALLERROR return "InternalError"; });}); loop(); auto trackMtime = mtime; base = model.getClock().now(); beginTransaction("mIdTag"); loop(); mtime = trackMtime + 10 * 1000; loop(); REQUIRE(attemptNr == 1); endTransaction(); mtime = trackMtime + 20 * 1000; loop(); REQUIRE(attemptNr == 1); mtime = trackMtime + 10 * 1000 + RETRY_INTERVAL_SECS * 1000; loop(); REQUIRE(attemptNr == 2); mtime = trackMtime + 10 * 1000 + 2 * RETRY_INTERVAL_SECS * 1000; loop(); REQUIRE(attemptNr == 2); mtime = trackMtime + 10 * 1000 + 3 * RETRY_INTERVAL_SECS * 1000; loop(); REQUIRE(attemptNr == 3); mtime = trackMtime + 10 * 1000 + 7 * RETRY_INTERVAL_SECS * 1000; loop(); REQUIRE(attemptNr == 3); } SECTION("TriggerMessage") { addMeterValueInput([] () { return 12345; }, "Energy.Active.Import.Register"); auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); Timestamp base; bool checkProcessed = false; setOnReceiveRequest("MeterValues", [&base, &checkProcessed] (JsonObject payload) { int connectorId = payload["connectorId"] | -1; if (connectorId != 1) { return; } checkProcessed = true; Timestamp t0; t0.setTime(payload["meterValue"][0]["timestamp"] | ""); REQUIRE( std::abs(t0 - base) <= 1 ); REQUIRE( !strncmp(payload["meterValue"][0]["sampledValue"][0]["value"] | "", "12345", strlen("12345")) ); }); loop(); base = model.getClock().now(); loopback.sendTXT(TRIGGER_METERVALUES, sizeof(TRIGGER_METERVALUES) - 1); loop(); REQUIRE(checkProcessed); } mocpp_deinitialize(); } ================================================ FILE: tests/RemoteStartTransaction.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include #include #include #include "./helpers/testHelper.h" using namespace MicroOcpp; TEST_CASE("RemoteStartTransaction") { printf("\nRun %s\n", "RemoteStartTransaction"); LoopbackConnection loopback; mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); mocpp_set_timer(custom_timer_cb); loop(); auto context = getOcppContext(); auto connector = context->getModel().getConnector(1); SECTION("Basic remote start accepted") { // Ensure connector idle REQUIRE(connector->getStatus() == ChargePointStatus_Available); context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "RemoteStartTransaction", [] () { auto doc = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["idTag"] = "mIdTag"; return doc;}, [] (JsonObject) {} ))); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Charging); endTransaction(); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Available); } SECTION("Same connectorId rejected when transaction active") { // Start with connector 1 busy so remote start with connectorId=1 should not auto-assign beginTransaction("anotherId"); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Charging); bool checkProcessed = false; context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "RemoteStartTransaction", [] () { auto doc = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(3)); auto payload = doc->to(); payload["idTag"] = "mIdTag"; payload["connectorId"] = 1; // the same connector already in use return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); } ))); loop(); // Transaction should still be the original one only REQUIRE(checkProcessed); REQUIRE(connector->getTransaction()); REQUIRE(strcmp(connector->getTransaction()->getIdTag(), "anotherId") == 0); REQUIRE(connector->getStatus() == ChargePointStatus_Charging); endTransaction(); loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Available); } SECTION("ConnectorId 0 rejected per spec") { // RemoteStartTransaction response status is Rejected when connectorId == 0 REQUIRE(connector->getStatus() == ChargePointStatus_Available); bool checkProcessed = false; context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "RemoteStartTransaction", [] () { auto doc = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(3)); auto payload = doc->to(); payload["idTag"] = "mIdTag"; payload["connectorId"] = 0; // invalid per spec return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); } ))); loop(); REQUIRE(checkProcessed); REQUIRE(connector->getStatus() == ChargePointStatus_Available); } SECTION("No free connector so rejected") { // Occupy all connectors (limit defined by MO_NUMCONNECTORS) for (unsigned cId = 1; cId < context->getModel().getNumConnectors(); cId++) { auto c = context->getModel().getConnector(cId); if (c) { c->beginTransaction_authorized("busyId"); } } loop(); bool checkProcessed = false; auto freeFound = false; for (unsigned cId = 1; cId < context->getModel().getNumConnectors(); cId++) { auto c = context->getModel().getConnector(cId); if (c && !c->getTransaction()) freeFound = true; } REQUIRE(!freeFound); // ensure all are busy context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "RemoteStartTransaction", [] () { auto doc = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["idTag"] = "mIdTag"; return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); } ))); loop(); REQUIRE(checkProcessed); // No new transaction should be created; keep statuses int activeTx = 0; for (unsigned cId = 1; cId < context->getModel().getNumConnectors(); cId++) { auto c = context->getModel().getConnector(cId); if (c && c->getTransaction()) activeTx++; } REQUIRE(activeTx == (int)context->getModel().getNumConnectors() - 1); // all occupied // cleanup for (unsigned cId = 1; cId < context->getModel().getNumConnectors(); cId++) { auto c = context->getModel().getConnector(cId); if (c && c->getTransaction()) { c->endTransaction(); } } loop(); REQUIRE(connector->getStatus() == ChargePointStatus_Available); } mocpp_deinitialize(); } ================================================ FILE: tests/Reservation.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_RESERVATION #include #include #include #include "./helpers/testHelper.h" #include #include #include #include #include #include #include #define BASE_TIME "2023-01-01T00:00:00.000Z" using namespace MicroOcpp; TEST_CASE( "Reservation" ) { printf("\nRun %s\n", "Reservation"); //clean state auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_set_timer(custom_timer_cb); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); auto& model = getOcppContext()->getModel(); auto rService = model.getReservationService(); auto connector = model.getConnector(1); model.getClock().setTime(BASE_TIME); loop(); SECTION("Basic reservation") { REQUIRE( connector->getStatus() == ChargePointStatus_Available ); REQUIRE( rService ); //set reservation int reservationId = 123; unsigned int connectorId = 1; Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future const char *idTag = "mIdTag"; const char *parentIdTag = nullptr; rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); //transaction blocked by reservation bool checkTxRejected = false; setTxNotificationOutput([&checkTxRejected] (Transaction*, TxNotification txNotification) { if (txNotification == TxNotification_ReservationConflict) { checkTxRejected = true; } }); beginTransaction("wrong idTag"); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); REQUIRE( checkTxRejected ); //idTag matches reservation beginTransaction("mIdTag"); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( connector->getTransaction()->getReservationId() == reservationId ); //reservation is reset after tx endTransaction(); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //RemoteStartTx - idTag doesn't match. The tx will start anyway assuming some start trigger in the backend prevails over reservations in the backend implementation rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "RemoteStartTransaction", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTag"] = "wrong idTag"; return doc;}, [] (JsonObject) { } //ignore conf ))); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( connector->getTransaction()->getReservationId() != reservationId ); //reservation is reset after tx endTransaction(); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //RemoteStartTx - idTag does match rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "RemoteStartTransaction", [idTag] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTag"] = idTag; return doc;}, [] (JsonObject) { } //ignore conf ))); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( connector->getTransaction()->getReservationId() == reservationId ); //reservation is reset after tx endTransaction(); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); } SECTION("Tx on other connector") { REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //set reservation int reservationId = 123; unsigned int connectorIdResvd = 1; //reserve connector 1 unsigned int connectorIdOther = 2; //start charging on other connector Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future const char *idTag = "mIdTag"; const char *parentIdTag = nullptr; rService->updateReservation(reservationId, connectorIdResvd, expiryDate, idTag, parentIdTag); REQUIRE( model.getConnector(connectorIdResvd)->getStatus() == ChargePointStatus_Reserved ); beginTransaction(idTag, connectorIdOther); loop(); REQUIRE( model.getConnector(connectorIdResvd)->getStatus() == ChargePointStatus_Available ); //reservation on first connector withdrawed REQUIRE( model.getConnector(connectorIdOther)->getStatus() == ChargePointStatus_Charging ); REQUIRE( getTransaction(connectorIdOther)->getReservationId() == reservationId ); //reservation transferred to other connector endTransaction(nullptr, nullptr, connectorIdOther); loop(); REQUIRE( model.getConnector(connectorIdOther)->getStatus() == ChargePointStatus_Available ); } SECTION("parentIdTag") { REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //set reservation int reservationId = 123; unsigned int connectorId = 1; Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future const char *idTag = "mIdTag"; const char *parentIdTag = "mParentIdTag"; rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); bool checkProcessed = false; getOcppContext()->getOperationRegistry().registerOperation("Authorize", [parentIdTag, &checkProcessed] () { return new Ocpp16::CustomOperation("Authorize", [] (JsonObject) {}, //ignore req payload [parentIdTag, &checkProcessed] () { //create conf checkProcessed = true; auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + //payload root JSON_OBJECT_SIZE(3)); //idTagInfo auto payload = doc->to(); payload["idTagInfo"]["parentIdTag"] = parentIdTag; payload["idTagInfo"]["status"] = "Accepted"; return doc;}); }); beginTransaction("other idTag"); loop(); REQUIRE( checkProcessed ); REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( connector->getTransaction()->getReservationId() == reservationId ); //reset tx endTransaction(); loop(); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); } SECTION("ConnectorZero") { REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //set reservation Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future const char *idTag = "mIdTag"; const char *parentIdTag = nullptr; //if connector 0 is reserved, accept at most one further reservation REQUIRE( rService->updateReservation(1000, 0, expiryDate, idTag, parentIdTag) ); REQUIRE( rService->updateReservation(1001, 1, expiryDate, idTag, parentIdTag) ); REQUIRE( !rService->updateReservation(1002, 2, expiryDate, idTag, parentIdTag) ); REQUIRE( model.getConnector(2)->getStatus() == ChargePointStatus_Available ); //reset reservations rService->getReservationById(1000)->clear(); rService->getReservationById(1001)->clear(); REQUIRE( model.getConnector(1)->getStatus() == ChargePointStatus_Available ); //if connector 0 is reserved, ensure that at least one physical connector remains available for the idTag of the reservation REQUIRE( rService->updateReservation(1000, 0, expiryDate, idTag, parentIdTag) ); beginTransaction("other idTag", 1); loop(); REQUIRE( model.getConnector(1)->getStatus() == ChargePointStatus_Charging ); bool checkTxRejected = false; setTxNotificationOutput([&checkTxRejected] (Transaction*, TxNotification txNotification) { if (txNotification == TxNotification_ReservationConflict) { checkTxRejected = true; } }, 2); beginTransaction("other idTag 2", 2); loop(); REQUIRE( checkTxRejected ); REQUIRE( model.getConnector(2)->getStatus() == ChargePointStatus_Available ); endTransaction(nullptr, nullptr, 1); loop(); } SECTION("Expiry date") { REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //set reservation int reservationId = 123; unsigned int connectorId = 1; Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future const char *idTag = "mIdTag"; const char *parentIdTag = nullptr; rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); Timestamp expired = expiryDate + 1; char expired_cstr [JSONDATE_LENGTH + 1]; expired.toJsonString(expired_cstr, JSONDATE_LENGTH + 1); model.getClock().setTime(expired_cstr); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); } SECTION("Reservation persistency") { unsigned int connectorId = 1; REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus_Available ); //set reservation int reservationId = 123; Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future const char *idTag = "mIdTag"; const char *parentIdTag = "mParentIdTag"; getOcppContext()->getModel().getReservationService()->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus_Reserved ); mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); getOcppContext()->getModel().getClock().setTime(BASE_TIME); loop(); REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus_Reserved ); auto reservation = getOcppContext()->getModel().getReservationService()->getReservationById(reservationId); REQUIRE( reservation->getReservationId() == reservationId ); REQUIRE( reservation->getConnectorId() == (int)connectorId ); REQUIRE( reservation->getExpiryDate() == expiryDate ); REQUIRE( !strcmp(reservation->getIdTag(), idTag) ); REQUIRE( !strcmp(reservation->getParentIdTag(), parentIdTag) ); reservation->clear(); mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); getOcppContext()->getModel().getClock().setTime(BASE_TIME); loop(); REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus_Available ); } SECTION("ReserveNow") { REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //set reservation int reservationId = 123; unsigned int connectorId = 1; Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future const char *idTag = "mIdTag"; const char *parentIdTag = nullptr; //simple reservation bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ReserveNow", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(5) + JSONDATE_LENGTH + 1); auto payload = doc->to(); payload["connectorId"] = connectorId; char expiryDate_cstr [JSONDATE_LENGTH + 1]; expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1); payload["expiryDate"] = expiryDate_cstr; payload["idTag"] = idTag; payload["parentIdTag"] = parentIdTag; payload["reservationId"] = reservationId; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); model.getReservationService()->getReservationById(reservationId)->clear(); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //reserve while charger is in Faulted state const char *errorCode = "OtherError"; addErrorCodeInput([&errorCode] () {return errorCode;}); REQUIRE( connector->getStatus() == ChargePointStatus_Faulted ); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ReserveNow", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(5) + JSONDATE_LENGTH + 1); auto payload = doc->to(); payload["connectorId"] = connectorId; char expiryDate_cstr [JSONDATE_LENGTH + 1]; expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1); payload["expiryDate"] = expiryDate_cstr; payload["idTag"] = idTag; payload["parentIdTag"] = parentIdTag; payload["reservationId"] = reservationId; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Faulted") ); } ))); loop(); REQUIRE( checkProcessed ); REQUIRE( connector->getStatus() == ChargePointStatus_Faulted ); errorCode = nullptr; //reset error REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //reserve while connector is already occupied setConnectorPluggedInput([] {return true;}); //plug EV REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ReserveNow", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(5) + JSONDATE_LENGTH + 1); auto payload = doc->to(); payload["connectorId"] = connectorId; char expiryDate_cstr [JSONDATE_LENGTH + 1]; expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1); payload["expiryDate"] = expiryDate_cstr; payload["idTag"] = idTag; payload["parentIdTag"] = parentIdTag; payload["reservationId"] = reservationId; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Occupied") ); } ))); loop(); REQUIRE( checkProcessed ); REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); setConnectorPluggedInput(nullptr); //reset ConnectorPluggedInput REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //Rejected ReserveNow status not possible //reserve while connector is inoperative connector->setAvailabilityVolatile(false); REQUIRE( connector->getStatus() == ChargePointStatus_Unavailable ); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ReserveNow", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(5) + JSONDATE_LENGTH + 1); auto payload = doc->to(); payload["connectorId"] = connectorId; char expiryDate_cstr [JSONDATE_LENGTH + 1]; expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1); payload["expiryDate"] = expiryDate_cstr; payload["idTag"] = idTag; payload["parentIdTag"] = parentIdTag; payload["reservationId"] = reservationId; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Unavailable") ); } ))); loop(); REQUIRE( checkProcessed ); REQUIRE( connector->getStatus() == ChargePointStatus_Unavailable ); connector->setAvailabilityVolatile(true); //revert Unavailable status REQUIRE( connector->getStatus() == ChargePointStatus_Available ); } SECTION("CancelReservation") { REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //set reservation int reservationId = 123; unsigned int connectorId = 1; Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future const char *idTag = "mIdTag"; const char *parentIdTag = nullptr; rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); //CancelReservation successfully bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "CancelReservation", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["reservationId"] = reservationId; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //CancelReservation while no reservation exists checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "CancelReservation", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["reservationId"] = reservationId; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Rejected") ); } ))); loop(); REQUIRE( checkProcessed ); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); } mocpp_deinitialize(); } #endif //MO_ENABLE_RESERVATION ================================================ FILE: tests/Reset.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include #include #include #include #include #include #include #include #include #include "./helpers/testHelper.h" #define BASE_TIME "2023-01-01T00:00:00.000Z" using namespace MicroOcpp; TEST_CASE( "Reset" ) { printf("\nRun %s\n", "Reset"); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_initialize(loopback, ChargerCredentials("test-runner1234"), makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail), false, ProtocolVersion(2,0,1)); auto context = getOcppContext(); mocpp_set_timer(custom_timer_cb); getOcppContext()->getOperationRegistry().registerOperation("Authorize", [] () { return new Ocpp16::CustomOperation("Authorize", [] (JsonObject) {}, //ignore req [] () { //create conf auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTokenInfo"]["status"] = "Accepted"; return doc; });}); getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [] () { return new Ocpp16::CustomOperation("TransactionEvent", [] (JsonObject) {}, //ignore req [] () { //create conf auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTokenInfo"]["status"] = "Accepted"; return doc; });}); // Register Reset handlers bool checkNotified [MO_NUM_EVSEID] = {false}; bool checkExecuted [MO_NUM_EVSEID] = {false}; setOnResetNotify([&checkNotified] (bool) { MO_DBG_DEBUG("Notify"); checkNotified[0] = true; return true; }); context->getModel().getResetServiceV201()->setExecuteReset([&checkExecuted] () { MO_DBG_DEBUG("Execute"); checkExecuted[0] = true; return false; // Reset fails because we're not actually exiting the process }); for (size_t i = 1; i < MO_NUM_EVSEID; i++) { context->getModel().getResetServiceV201()->setNotifyReset([&checkNotified, i] (ResetType) { MO_DBG_DEBUG("Notify %zu", i); checkNotified[i] = true; return true; }, i); context->getModel().getResetServiceV201()->setExecuteReset([&checkExecuted, i] () { MO_DBG_DEBUG("Execute %zu", i); checkExecuted[i] = true; return true; }, i); } loop(); SECTION("B11 - Reset - Without ongoing transaction") { MO_MEM_RESET(); bool checkProcessed = false; auto resetRequest = makeRequest(new Ocpp16::CustomOperation( "Reset", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["type"] = "OnIdle"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE(!strcmp(payload["status"], "Accepted")); } )); context->initiateRequest(std::move(resetRequest)); loop(); mtime += 30000; // Reset has some delays to ensure that the WS is not cut off immediately loop(); REQUIRE(checkProcessed); for (size_t i = 0; i < MO_NUM_EVSEID; i++) { REQUIRE( checkNotified[i] ); } MO_MEM_PRINT_STATS(); } SECTION("Schedule full charger Reset") { REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); REQUIRE( context->getModel().getTransactionService()->getEvse(2)->getTransaction() == nullptr ); context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken"); setConnectorPluggedInput([] () {return true;}, 1); setEvReadyInput([] () {return true;}, 1); setEvseReadyInput([] () {return true;}, 1); context->getModel().getTransactionService()->getEvse(2)->beginAuthorization("mIdToken2"); setConnectorPluggedInput([] () {return true;}, 2); setEvReadyInput([] () {return true;}, 2); setEvseReadyInput([] () {return true;}, 2); loop(); REQUIRE( ocppPermitsCharge(1) ); REQUIRE( ocppPermitsCharge(2) ); bool checkProcessed = false; auto resetRequest = makeRequest(new Ocpp16::CustomOperation( "Reset", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["type"] = "OnIdle"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE(!strcmp(payload["status"], "Scheduled")); } )); context->initiateRequest(std::move(resetRequest)); loop(); mtime += 30000; // Reset has some delays to ensure that the WS is not cut off immediately loop(); REQUIRE(checkProcessed); for (size_t i = 0; i < MO_NUM_EVSEID; i++) { REQUIRE( checkNotified[i] ); } // Still scheduled REQUIRE( ocppPermitsCharge(1) ); REQUIRE( ocppPermitsCharge(2) ); context->getModel().getTransactionService()->getEvse(1)->endAuthorization("mIdToken"); setConnectorPluggedInput([] () {return false;}, 1); setEvReadyInput([] () {return false;}, 1); setEvseReadyInput([] () {return false;}, 1); loop(); // Still scheduled REQUIRE( !ocppPermitsCharge(1) ); REQUIRE( ocppPermitsCharge(2) ); //REQUIRE( getChargePointStatus(1) == ChargePointStatus_Unavailable ); //change: Reset doesn't lead to Unavailable state context->getModel().getTransactionService()->getEvse(2)->endAuthorization("mIdToken"); setConnectorPluggedInput([] () {return false;}, 2); setEvReadyInput([] () {return false;}, 2); setEvseReadyInput([] () {return false;}, 2); loop(); mtime += 30000; // Reset has some delays to ensure that the WS is not cut off immediately loop(); // Not scheduled anymore; execute Reset REQUIRE( !ocppPermitsCharge(1) ); REQUIRE( !ocppPermitsCharge(2) ); REQUIRE( checkExecuted[0] ); // Technically, Reset failed at this point, because the program is still running. Check if connectors are Available agin REQUIRE( getChargePointStatus(1) == ChargePointStatus_Available ); REQUIRE( getChargePointStatus(2) == ChargePointStatus_Available ); } SECTION("Immediate full charger Reset") { context->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); REQUIRE( context->getModel().getTransactionService()->getEvse(2)->getTransaction() == nullptr ); context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken"); context->getModel().getTransactionService()->getEvse(2)->beginAuthorization("mIdToken2"); loop(); MO_MEM_RESET(); REQUIRE( ocppPermitsCharge(1) ); REQUIRE( ocppPermitsCharge(2) ); bool checkProcessedTx = false; getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkProcessedTx] () { return new Ocpp16::CustomOperation("TransactionEvent", [&checkProcessedTx] (JsonObject payload) { //process req checkProcessedTx = true; REQUIRE(!strcmp(payload["eventType"], "Ended")); REQUIRE(!strcmp(payload["triggerReason"], "ResetCommand")); REQUIRE(!strcmp(payload["transactionInfo"]["stoppedReason"], "ImmediateReset")); }, [] () { //create conf return createEmptyDocument(); });}); bool checkProcessed = false; auto resetRequest = makeRequest(new Ocpp16::CustomOperation( "Reset", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["type"] = "Immediate"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE(!strcmp(payload["status"], "Accepted")); } )); context->initiateRequest(std::move(resetRequest)); loop(); mtime += 30000; // Reset has some delays to ensure that the WS is not cut off immediately loop(); REQUIRE(checkProcessed); REQUIRE(checkProcessedTx); for (size_t i = 0; i < MO_NUM_EVSEID; i++) { REQUIRE( checkNotified[i] ); } // Stopped Tx REQUIRE( !ocppPermitsCharge(1) ); REQUIRE( !ocppPermitsCharge(2) ); REQUIRE( checkExecuted[0] ); MO_MEM_PRINT_STATS(); loop(); // Technically, Reset failed at this point, because the program is still running. Check if connectors are Available agin REQUIRE( getChargePointStatus(1) == ChargePointStatus_Available ); REQUIRE( getChargePointStatus(2) == ChargePointStatus_Available ); } SECTION("Reject Reset") { context->getModel().getResetServiceV201()->setNotifyReset([&checkNotified] (ResetType) { MO_DBG_DEBUG("Reject Reset"); checkNotified[2] = true; return false; }, 2); bool checkProcessed = false; auto resetRequest = makeRequest(new Ocpp16::CustomOperation( "Reset", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["type"] = "Immediate"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE(!strcmp(payload["status"], "Rejected")); } )); context->initiateRequest(std::move(resetRequest)); loop(); REQUIRE(checkProcessed); REQUIRE(checkNotified[2]); REQUIRE( getChargePointStatus(0) == ChargePointStatus_Available ); REQUIRE( getChargePointStatus(1) == ChargePointStatus_Available ); REQUIRE( getChargePointStatus(2) == ChargePointStatus_Available ); } SECTION("Reset single EVSE") { bool checkProcessed = false; auto resetRequest = makeRequest(new Ocpp16::CustomOperation( "Reset", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["type"] = "OnIdle"; payload["evseId"] = 1; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE(!strcmp(payload["status"], "Accepted")); } )); context->initiateRequest(std::move(resetRequest)); loop(); REQUIRE(checkProcessed); REQUIRE(checkNotified[1]); //REQUIRE( getChargePointStatus(1) == ChargePointStatus_Unavailable ); //change: Reset doesn't lead to Unavailable state REQUIRE( getChargePointStatus(2) == ChargePointStatus_Available ); mtime += 30000; // Reset has some delays to ensure that the WS is not cut off immediately loop(); REQUIRE(checkExecuted[1]); REQUIRE( getChargePointStatus(1) == ChargePointStatus_Available ); } mocpp_deinitialize(); } #endif // MO_ENABLE_V201 ================================================ FILE: tests/Security.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include #include #include #include #include #include #include #include #include "./helpers/testHelper.h" #define BASE_TIME "2023-01-01T00:00:00.000Z" using namespace MicroOcpp; TEST_CASE( "Security" ) { printf("\nRun %s\n", "Security"); mocpp_set_timer(custom_timer_cb); //initialize Context with dummy socket LoopbackConnection loopback; auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); SECTION("Manual SecurityEventNotification") { loop(); MO_MEM_RESET(); getOcppContext()->initiateRequest(makeRequest(new Ocpp201::SecurityEventNotification( "ReconfigurationOfSecurityParameters", getOcppContext()->getModel().getClock().now()))); loop(); MO_MEM_PRINT_STATS(); } mocpp_deinitialize(); } #endif // MO_ENABLE_V201 ================================================ FILE: tests/SmartCharging.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include "./helpers/testHelper.h" #define BASE_TIME "2023-01-01T00:00:00.000Z" #define SCPROFILE_0 "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":1,\"csChargingProfiles\":{\"chargingProfileId\":0,\"stackLevel\":0,\"chargingProfilePurpose\":\"TxDefaultProfile\",\"chargingProfileKind\":\"Recurring\",\"recurrencyKind\":\"Daily\",\"validFrom\":\"2022-06-12T00:00:00.000Z\",\"validTo\":\"2023-06-21T00:00:00.000Z\",\"chargingSchedule\":{\"duration\":1000000,\"startSchedule\":\"2023-06-18T00:00:00.000Z\",\"chargingRateUnit\":\"W\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3},{\"startPeriod\":18000,\"limit\":32,\"numberPhases\":3}],\"minChargingRate\":6}}}]" #define SCPROFILE_0_ALT_SAME_ID "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":1,\"csChargingProfiles\":{\"chargingProfileId\":0,\"stackLevel\":1,\"chargingProfilePurpose\":\"TxDefaultProfile\",\"chargingProfileKind\":\"Recurring\",\"recurrencyKind\":\"Daily\",\"validFrom\":\"2022-06-12T00:00:00.000Z\",\"validTo\":\"2023-06-21T00:00:00.000Z\",\"chargingSchedule\":{\"duration\":1000000,\"startSchedule\":\"2023-06-18T00:00:00.000Z\",\"chargingRateUnit\":\"W\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3},{\"startPeriod\":18000,\"limit\":32,\"numberPhases\":3}],\"minChargingRate\":6}}}]" #define SCPROFILE_0_ALT_SAME_STACKEVEL_PURPOSE "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":1,\"csChargingProfiles\":{\"chargingProfileId\":1,\"stackLevel\":0,\"chargingProfilePurpose\":\"TxDefaultProfile\",\"chargingProfileKind\":\"Recurring\",\"recurrencyKind\":\"Daily\",\"validFrom\":\"2022-06-12T00:00:00.000Z\",\"validTo\":\"2023-06-21T00:00:00.000Z\",\"chargingSchedule\":{\"duration\":1000000,\"startSchedule\":\"2023-06-18T00:00:00.000Z\",\"chargingRateUnit\":\"W\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3},{\"startPeriod\":18000,\"limit\":32,\"numberPhases\":3}],\"minChargingRate\":6}}}]" #define SCPROFILE_1_ABSOLUTE_LIMIT_16A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":1,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"chargingSchedule\":{\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3}]}}}]" #define SCPROFILE_2_RELATIVE_TXDEF_24A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":1,\"csChargingProfiles\":{\"chargingProfileId\":2,\"stackLevel\":0,\"chargingProfilePurpose\":\"TxDefaultProfile\",\"chargingProfileKind\":\"Relative\",\"chargingSchedule\":{\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":24,\"numberPhases\":3}]}}}]" #define SCPROFILE_3_TXPROF_TXID123_20A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":1,\"csChargingProfiles\":{\"chargingProfileId\":3,\"transactionId\":123,\"stackLevel\":0,\"chargingProfilePurpose\":\"TxProfile\",\"chargingProfileKind\":\"Relative\",\"chargingSchedule\":{\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":20,\"numberPhases\":3}]}}}]" #define SCPROFILE_4_VALID_FROM_2024_16A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":4,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"validFrom\":\"2024-01-01T00:00:00.000Z\",\"chargingSchedule\":{\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3}]}}}]" #define SCPROFILE_5_VALID_UNTIL_2022_16A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":5,\"stackLevel\":1,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"validTo\": \"2022-01-01T00:00:00.000Z\",\"chargingSchedule\":{\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3}]}}}]" #define SCPROFILE_6_MULTIPLE_PERIODS_16A_20A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":6,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"chargingSchedule\":{\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3},{\"startPeriod\":3600,\"limit\":20,\"numberPhases\":3}]}}}]" #define SCPROFILE_7_RECURRING_DAY_2H_16A_20A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":7,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Recurring\",\"recurrencyKind\":\"Daily\", \"chargingSchedule\":{\"duration\":7200,\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3},{\"startPeriod\":3600,\"limit\":20,\"numberPhases\":3}]}}}]" #define SCPROFILE_8_RECURRING_WEEK_2H_10A_12A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":8,\"stackLevel\":1,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Recurring\",\"recurrencyKind\":\"Weekly\",\"chargingSchedule\":{\"duration\":7200,\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":10,\"numberPhases\":3},{\"startPeriod\":3600,\"limit\":12,\"numberPhases\":3}]}}}]" #define SCPROFILE_9_VIA_RMTSTARTTX_20A "[2,\"testmsg\",\"RemoteStartTransaction\",{\"connectorId\":1,\"idTag\":\"mIdTag\",\"chargingProfile\":{\"chargingProfileId\":9,\"stackLevel\":0,\"chargingProfilePurpose\":\"TxProfile\",\"chargingProfileKind\":\"Relative\",\"chargingSchedule\":{\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":20,\"numberPhases\":1}]}}}]" #define SCPROFILE_10_ABSOLUTE_LIMIT_5KW "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":10,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"chargingSchedule\":{\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"W\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":5000,\"numberPhases\":3}]}}}]" using namespace MicroOcpp; TEST_CASE( "SmartCharging" ) { printf("\nRun %s\n", "SmartCharging"); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); auto context = getOcppContext(); auto& model = context->getModel(); mocpp_set_timer(custom_timer_cb); model.getClock().setTime(BASE_TIME); endTransaction(); SECTION("Load Smart Charging Service"){ REQUIRE(!model.getSmartChargingService()); setSmartChargingOutput([] (float, float, int) {}); REQUIRE(model.getSmartChargingService()); } setSmartChargingOutput([] (float, float, int) {}); auto scService = model.getSmartChargingService(); scService->clearChargingProfile([] (int, int, ChargingProfilePurposeType, int) { return true; }); SECTION("Set Charging Profile and clear") { unsigned int count = 0; scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { count++; return true; }); REQUIRE(count == 0); loopback.sendTXT(SCPROFILE_0, strlen(SCPROFILE_0)); //check if filter works by comparing the outcome of returning false and true and repeating the test count = 0; scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { count++; return false; }); REQUIRE(count == 1); count = 0; scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { count++; return true; }); REQUIRE(count == 1); count = 0; scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { count++; return true; }); REQUIRE(count == 0); } SECTION("Charging Profiles persistency over reboots") { loopback.sendTXT(SCPROFILE_0, strlen(SCPROFILE_0)); mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); setSmartChargingOutput([] (float, float, int) {}); scService = getOcppContext()->getModel().getSmartChargingService(); unsigned int count = 0; scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { count++; return true; }); REQUIRE (count == 1); } SECTION("Set conflicting profile") { loopback.sendTXT(SCPROFILE_0, strlen(SCPROFILE_0)); loopback.sendTXT(SCPROFILE_0_ALT_SAME_ID, strlen(SCPROFILE_0_ALT_SAME_ID)); unsigned int count = 0; scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { count++; return true; }); REQUIRE(count == 1); loopback.sendTXT(SCPROFILE_0, strlen(SCPROFILE_0)); loopback.sendTXT(SCPROFILE_0_ALT_SAME_STACKEVEL_PURPOSE, strlen(SCPROFILE_0_ALT_SAME_STACKEVEL_PURPOSE)); count = 0; scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { count++; return true; }); REQUIRE(count == 1); } SECTION("Set charging profile via RmtStartTx") { float current = -1.f; setSmartChargingOutput([¤t] (float, float limit_current, int) { current = limit_current; }); loop(); loopback.sendTXT(SCPROFILE_9_VIA_RMTSTARTTX_20A, strlen(SCPROFILE_9_VIA_RMTSTARTTX_20A)); loop(); REQUIRE((current > 19.99f && current < 20.01f)); endTransaction(); loop(); } SECTION("Set ChargePointMaxProfile - Absolute") { float current = -1.f; setSmartChargingOutput([¤t] (float, float limit_current, int) { current = limit_current; }); loopback.sendTXT(SCPROFILE_1_ABSOLUTE_LIMIT_16A, strlen(SCPROFILE_1_ABSOLUTE_LIMIT_16A)); loop(); REQUIRE((current > 15.99f && current < 16.01f)); } SECTION("Set TxDefaultProfile - Relative") { float current = -1.f; setSmartChargingOutput([¤t] (float, float limit_current, int) { current = limit_current; }); loopback.sendTXT(SCPROFILE_2_RELATIVE_TXDEF_24A, strlen(SCPROFILE_2_RELATIVE_TXDEF_24A)); loop(); REQUIRE(current < 0.f); beginTransaction_authorized("mIdTag"); loop(); REQUIRE((current > 23.99f && current < 24.01f)); endTransaction(); loop(); REQUIRE(current < 0.f); } SECTION("Set TxProfile - tx fit and mismatch") { float current = -1.f; setSmartChargingOutput([¤t] (float, float limit_current, int) { current = limit_current; }); //send before transaction - expect rejection loopback.sendTXT(SCPROFILE_3_TXPROF_TXID123_20A, strlen(SCPROFILE_3_TXPROF_TXID123_20A)); unsigned int count = 0; scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { count++; return true; }); REQUIRE(count == 0); loop(); beginTransaction_authorized("mIdTag"); //send during transaction but wrong txId - expect rejection loopback.sendTXT(SCPROFILE_3_TXPROF_TXID123_20A, strlen(SCPROFILE_3_TXPROF_TXID123_20A)); loop(); count = 0; scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { count++; return true; }); REQUIRE(count == 0); getTransaction()->setTransactionId(123); //send during tx with matchin txId - accept loopback.sendTXT(SCPROFILE_3_TXPROF_TXID123_20A, strlen(SCPROFILE_3_TXPROF_TXID123_20A)); loop(); REQUIRE((current > 19.99f && current < 20.01f)); endTransaction(); loop(); //check if SCService deleted TxProfiles after tx count = 0; scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { count++; return true; }); REQUIRE(count == 0); } SECTION("Time validity check") { float current = -1.f; setSmartChargingOutput([¤t] (float, float limit_current, int) { current = limit_current; }); loopback.sendTXT(SCPROFILE_4_VALID_FROM_2024_16A, strlen(SCPROFILE_4_VALID_FROM_2024_16A)); loopback.sendTXT(SCPROFILE_5_VALID_UNTIL_2022_16A, strlen(SCPROFILE_5_VALID_UNTIL_2022_16A)); loop(); REQUIRE(current < 0.f); //now reach validity period of future profile model.getClock().setTime("2024-01-01T00:00:00.000Z"); loop(); REQUIRE((current > 15.99f && current < 16.01f)); } SECTION("Multiple periods") { float current = -1.f; setSmartChargingOutput([¤t] (float, float limit_current, int) { current = limit_current; }); loopback.sendTXT(SCPROFILE_6_MULTIPLE_PERIODS_16A_20A, strlen(SCPROFILE_6_MULTIPLE_PERIODS_16A_20A)); loop(); REQUIRE((current > 15.99f && current < 16.01f)); //now reach next period model.getClock().setTime("2023-01-01T01:00:00.000Z"); loop(); REQUIRE((current > 19.99f && current < 20.01f)); } SECTION("Recurring profiles - Daily") { float current = -1.f; setSmartChargingOutput([¤t] (float, float limit_current, int) { current = limit_current; }); loopback.sendTXT(SCPROFILE_7_RECURRING_DAY_2H_16A_20A, strlen(SCPROFILE_7_RECURRING_DAY_2H_16A_20A)); loop(); REQUIRE((current > 15.99f && current < 16.01f)); //now exceed duration model.getClock().setTime("2023-01-01T02:00:00.000Z"); loop(); REQUIRE(current < 0.f); //check second period three days afterwards model.getClock().setTime("2023-01-04T01:00:00.000Z"); loop(); REQUIRE((current > 19.99f && current < 20.01f)); } SECTION("Recurring profiles - Weekly") { float current = -1.f; setSmartChargingOutput([¤t] (float, float limit_current, int) { current = limit_current; }); loopback.sendTXT(SCPROFILE_8_RECURRING_WEEK_2H_10A_12A, strlen(SCPROFILE_8_RECURRING_WEEK_2H_10A_12A)); loop(); REQUIRE((current > 9.99f && current < 10.01f)); //now exceed duration model.getClock().setTime("2023-01-01T02:00:00.000Z"); loop(); REQUIRE(current < 0.f); //check second period three weeks afterwards model.getClock().setTime("2023-01-22T01:00:00.000Z"); loop(); REQUIRE((current > 11.99f && current < 12.01f)); } SECTION("Stacking recurring profiles") { float current = -1.f; setSmartChargingOutput([¤t] (float, float limit_current, int) { current = limit_current; }); loopback.sendTXT(SCPROFILE_7_RECURRING_DAY_2H_16A_20A, strlen(SCPROFILE_7_RECURRING_DAY_2H_16A_20A)); //stackLevel: 0 loopback.sendTXT(SCPROFILE_8_RECURRING_WEEK_2H_10A_12A, strlen(SCPROFILE_8_RECURRING_WEEK_2H_10A_12A)); //stackLevel: 1 loop(); REQUIRE((current > 9.99f && current < 10.01f)); //Weekly schedule prevails //check again during the week model.getClock().setTime("2023-01-03T00:00:00.000Z"); loop(); REQUIRE((current > 15.99f && current < 16.01f)); //Weekly schedule out of duration, only Daily defined //check again three weeks later model.getClock().setTime("2023-01-22T00:00:00.000Z"); loop(); REQUIRE((current > 9.99f && current < 10.01f)); //Weekly schedule prevails again //check again during the week model.getClock().setTime("2023-01-23T00:00:00.000Z"); loop(); REQUIRE((current > 15.99f && current < 16.01f)); //Weekly schedule out of duration again, only Daily defined again } SECTION("TxProfile capped by ChargePointMaxProfile") { float current = -1.f; int numberPhases = -1; setSmartChargingOutput([¤t, &numberPhases] (float, float limit_current, int limit_numberPhases) { current = limit_current; numberPhases = limit_numberPhases; }); loop(); loopback.sendTXT(SCPROFILE_9_VIA_RMTSTARTTX_20A, strlen(SCPROFILE_9_VIA_RMTSTARTTX_20A)); loop(); loopback.sendTXT(SCPROFILE_1_ABSOLUTE_LIMIT_16A, strlen(SCPROFILE_1_ABSOLUTE_LIMIT_16A)); loop(); REQUIRE((current > 15.99f && current < 16.01f)); //current limited by ChargePointMaxProfile REQUIRE(numberPhases == 1); //numberPhases limited by TxProfile endTransaction(); loop(); } SECTION("TxProfile and ChargePointMaxProfile with mixed units") { float power = -1.f; float current = -1.f; setSmartChargingOutput([&power, ¤t] (float limit_power, float limit_current, int) { power = limit_power; current = limit_current; }); loop(); loopback.sendTXT(SCPROFILE_9_VIA_RMTSTARTTX_20A, strlen(SCPROFILE_9_VIA_RMTSTARTTX_20A)); loop(); loopback.sendTXT(SCPROFILE_10_ABSOLUTE_LIMIT_5KW, strlen(SCPROFILE_10_ABSOLUTE_LIMIT_5KW)); loop(); REQUIRE((power > 4999.f && power < 5001.f)); //ChargePointMaxProfile defines power REQUIRE((current > 19.99f && current < 20.01f)); //TxProfile defines current endTransaction(); loop(); } SECTION("Get composite schedule") { loopback.sendTXT(SCPROFILE_6_MULTIPLE_PERIODS_16A_20A, strlen(SCPROFILE_6_MULTIPLE_PERIODS_16A_20A)); bool checkProcessed = false; auto getCompositeSchedule = makeRequest(new Ocpp16::CustomOperation( "GetCompositeSchedule", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(3)); auto payload = doc->to(); payload["connectorId"] = 1; payload["duration"] = 86400; payload["chargingRateUnit"] = "A"; return doc;}, [&checkProcessed, &model] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE(!strcmp(payload["status"], "Accepted")); REQUIRE(payload["connectorId"] == 1); char checkScheduleStart [JSONDATE_LENGTH + 1]; model.getClock().now().toJsonString(checkScheduleStart, JSONDATE_LENGTH + 1); REQUIRE(!strcmp(payload["scheduleStart"], checkScheduleStart)); JsonObject chargingScheduleJson = payload["chargingSchedule"]; ChargingSchedule schedule; bool success = loadChargingSchedule(chargingScheduleJson, schedule); REQUIRE(success); REQUIRE(schedule.chargingSchedulePeriod.size() == 2); REQUIRE((schedule.chargingSchedulePeriod[0].limit > 15.99f && schedule.chargingSchedulePeriod[0].limit < 16.01f)); REQUIRE(schedule.chargingSchedulePeriod[0].startPeriod == 0); REQUIRE((schedule.chargingSchedulePeriod[1].limit > 19.99f && schedule.chargingSchedulePeriod[1].limit < 20.01f)); REQUIRE(schedule.chargingSchedulePeriod[1].startPeriod == 3600); } )); context->initiateRequest(std::move(getCompositeSchedule)); loop(); REQUIRE(checkProcessed); } SECTION("Get composite schedule with definition gap") { loopback.sendTXT(SCPROFILE_7_RECURRING_DAY_2H_16A_20A, strlen(SCPROFILE_7_RECURRING_DAY_2H_16A_20A)); auto schedule = scService->getCompositeSchedule(1, 86401); REQUIRE(schedule != nullptr); REQUIRE(schedule->duration == 86401); REQUIRE(schedule->chargingSchedulePeriod.size() == 4); REQUIRE((schedule->chargingSchedulePeriod[0].limit > 15.99f && schedule->chargingSchedulePeriod[0].limit < 16.01f)); REQUIRE(schedule->chargingSchedulePeriod[0].startPeriod == 0); REQUIRE((schedule->chargingSchedulePeriod[1].limit > 19.99f && schedule->chargingSchedulePeriod[1].limit < 20.01f)); REQUIRE(schedule->chargingSchedulePeriod[1].startPeriod == 3600); REQUIRE(schedule->chargingSchedulePeriod[2].limit < 0.f); //undefined during this period REQUIRE(schedule->chargingSchedulePeriod[2].startPeriod == 2 * 3600); REQUIRE((schedule->chargingSchedulePeriod[3].limit > 15.99f && schedule->chargingSchedulePeriod[3].limit < 16.01f)); REQUIRE(schedule->chargingSchedulePeriod[3].startPeriod == 86400); } SECTION("SmartCharging memory limits - MaxChargingProfilesInstalled") { loop(); bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_0_ALT_SAME_ID); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; (*doc)["connectorId"] = 2; return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_1_ABSOLUTE_LIMIT_16A); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); // 3 distinct ChargingProfiles installed. Check if further Profiles are rejected correctly for (size_t i = 0; i < 2; i++) { // replace existing profile - OK checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_1_ABSOLUTE_LIMIT_16A); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); } for (size_t i = 0; i < 2; i++) { // try to install additional profile - not okay checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_5_VALID_UNTIL_2022_16A); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); } ))); loop(); REQUIRE( checkProcessed ); } } SECTION("SmartCharging memory limits - ChargeProfileMaxStackLevel") { loop(); bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; (*doc)["csChargingProfiles"]["stackLevel"] = MO_ChargeProfileMaxStackLevel; return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; (*doc)["csChargingProfiles"]["stackLevel"] = MO_ChargeProfileMaxStackLevel + 1; return doc;}, [] (JsonObject) { }, //ignore conf [&checkProcessed] (const char*, const char*, JsonObject) { // process error checkProcessed = true; return true; } ))); loop(); REQUIRE( checkProcessed ); } SECTION("SmartCharging memory limits - ChargingScheduleMaxPeriods") { loop(); bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; JsonArray chargingSchedulePeriod = (*doc)["csChargingProfiles"]["chargingSchedule"]["chargingSchedulePeriod"]; chargingSchedulePeriod.clear(); for (size_t i = 0; i < MO_ChargingScheduleMaxPeriods; i++) { auto period = chargingSchedulePeriod.createNestedObject(); period["startPeriod"] = i; period["limit"] = 16; } return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; JsonArray chargingSchedulePeriod = (*doc)["csChargingProfiles"]["chargingSchedule"]["chargingSchedulePeriod"]; chargingSchedulePeriod.clear(); for (size_t i = 0; i < MO_ChargingScheduleMaxPeriods + 1; i++) { auto period = chargingSchedulePeriod.createNestedObject(); period["startPeriod"] = i; period["limit"] = 16; } return doc;}, [] (JsonObject) { }, //ignore conf [&checkProcessed] (const char*, const char*, JsonObject) { // process error checkProcessed = true; return true; } ))); loop(); REQUIRE( checkProcessed ); } SECTION("ChargingScheduleAllowedChargingRateUnit") { setSmartChargingOutput(nullptr); loop(); // accept power, reject current setSmartChargingPowerOutput([] (float) { }); bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_0); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_1_ABSOLUTE_LIMIT_16A); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); } ))); loop(); REQUIRE( checkProcessed ); // reject power, accept current setSmartChargingPowerOutput(nullptr); setSmartChargingCurrentOutput([] (float) { }); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_0); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); } ))); loop(); REQUIRE( checkProcessed ); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "SetChargingProfile", [] () { //create req StaticJsonDocument<2048> raw; deserializeJson(raw, SCPROFILE_1_ABSOLUTE_LIMIT_16A); auto doc = makeJsonDoc("UnitTests", 2048); *doc = raw[3]; return doc;}, [&checkProcessed] (JsonObject response) { checkProcessed = true; REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); } scService->clearChargingProfile([] (int, int, ChargingProfilePurposeType, int) { return true; }); mocpp_deinitialize(); } ================================================ FILE: tests/TransactionSafety.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include #include #include #include #include #include #include "./helpers/testHelper.h" using namespace MicroOcpp; TEST_CASE( "Transaction safety" ) { printf("\nRun %s\n", "Transaction safety"); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_initialize(loopback); mocpp_set_timer(custom_timer_cb); declareConfiguration("ConnectionTimeOut", 30)->setInt(30); SECTION("Basic transaction") { MO_DBG_DEBUG("Basic transaction"); loop(); startTransaction("mIdTag"); loop(); REQUIRE(ocppPermitsCharge()); stopTransaction(); loop(); REQUIRE(!ocppPermitsCharge()); mocpp_deinitialize(); } SECTION("Managed transaction") { MO_DBG_DEBUG("Managed transaction"); loop(); setConnectorPluggedInput([] () {return true;}); beginTransaction("mIdTag"); loop(); REQUIRE(ocppPermitsCharge()); endTransaction(); loop(); REQUIRE(!ocppPermitsCharge()); mocpp_deinitialize(); } SECTION("Reset during transaction 01 - interrupt initiation") { MO_DBG_DEBUG("Reset during transaction 01 - interrupt initiation"); setConnectorPluggedInput([] () {return false;}); loop(); beginTransaction("mIdTag"); loop(); mocpp_deinitialize(); //reset and jump to next section } SECTION("Reset during transaction 02 - interrupt initiation second time") { MO_DBG_DEBUG("Reset during transaction 02 - interrupt initiation second time"); setConnectorPluggedInput([] () {return false;}); loop(); REQUIRE(!ocppPermitsCharge()); mocpp_deinitialize(); } SECTION("Reset during transaction 03 - interrupt running tx") { MO_DBG_DEBUG("Reset during transaction 03 - interrupt running tx"); setConnectorPluggedInput([] () {return true;}); loop(); REQUIRE(ocppPermitsCharge()); mocpp_deinitialize(); } SECTION("Reset during transaction 04 - interrupt stopping tx") { MO_DBG_DEBUG("Reset during transaction 04 - interrupt stopping tx"); setConnectorPluggedInput([] () {return true;}); loop(); REQUIRE(ocppPermitsCharge()); endTransaction(); mocpp_deinitialize(); } SECTION("Reset during transaction 06 - check tx finished") { MO_DBG_DEBUG("Reset during transaction 06 - check tx finished"); setConnectorPluggedInput([] () {return true;}); loop(); REQUIRE(!ocppPermitsCharge()); mocpp_deinitialize(); } } ================================================ FILE: tests/Transactions.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include #include #include #include #include #include #include #include #include "./helpers/testHelper.h" #define BASE_TIME "2023-01-01T00:00:00.000Z" using namespace MicroOcpp; TEST_CASE( "Transactions" ) { printf("\nRun %s\n", "Transactions"); //initialize Context with dummy socket LoopbackConnection loopback; mocpp_initialize(loopback, ChargerCredentials("test-runner1234"), makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail), false, ProtocolVersion(2,0,1)); auto context = getOcppContext(); mocpp_set_timer(custom_timer_cb); getOcppContext()->getOperationRegistry().registerOperation("Authorize", [] () { return new Ocpp16::CustomOperation("Authorize", [] (JsonObject) {}, //ignore req [] () { //create conf auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTokenInfo"]["status"] = "Accepted"; return doc; });}); getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [] () { return new Ocpp16::CustomOperation("TransactionEvent", [] (JsonObject) {}, //ignore req [] () { //create conf auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTokenInfo"]["status"] = "Accepted"; return doc; });}); loop(); SECTION("Basic transaction") { REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); MO_DBG_DEBUG("plug EV"); setConnectorPluggedInput([] () {return true;}); loop(); MO_DBG_DEBUG("authorize"); context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken"); loop(); MO_DBG_DEBUG("EV requests charge"); setEvReadyInput([] () {return true;}); loop(); MO_DBG_DEBUG("power circuit closed"); setEvseReadyInput([] () {return true;}); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction()->started ); REQUIRE( !context->getModel().getTransactionService()->getEvse(1)->getTransaction()->stopped ); MO_DBG_DEBUG("EV idle"); setEvReadyInput([] () {return false;}); loop(); MO_DBG_DEBUG("power circuit opened"); setEvseReadyInput([] () {return false;}); loop(); MO_DBG_DEBUG("deauthorize"); context->getModel().getTransactionService()->getEvse(1)->endAuthorization("mIdToken"); loop(); MO_DBG_DEBUG("unplug EV"); setConnectorPluggedInput([] () {return false;}); loop(); REQUIRE( (context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr || context->getModel().getTransactionService()->getEvse(1)->getTransaction()->stopped)); } SECTION("UC C01-04") { //scenario preparation REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("PowerPathClosed"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("PowerPathClosed"); setConnectorPluggedInput([] () {return false;}); loop(); MO_MEM_RESET(); context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken"); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() != nullptr ); REQUIRE( !context->getModel().getTransactionService()->getEvse(1)->getTransaction()->started ); REQUIRE( !context->getModel().getTransactionService()->getEvse(1)->getTransaction()->stopped ); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); MO_DBG_INFO("Memory requirements UC C01-04:"); MO_MEM_PRINT_STATS(); context->getModel().getTransactionService()->getEvse(1)->abortTransaction(); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); } SECTION("UC E01 - S5 / E06") { //scenario preparation REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("PowerPathClosed"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("PowerPathClosed"); setConnectorPluggedInput([] () {return false;}); loop(); MO_MEM_RESET(); //run scenario setConnectorPluggedInput([] () {return true;}); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); REQUIRE( getChargePointStatus() == ChargePointStatus_Occupied ); context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken"); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() != nullptr ); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction()->started ); REQUIRE( !context->getModel().getTransactionService()->getEvse(1)->getTransaction()->stopped ); REQUIRE( getChargePointStatus() == ChargePointStatus_Occupied ); MO_DBG_INFO("Memory requirements UC E01 - S5:"); MO_MEM_PRINT_STATS(); MO_MEM_RESET(); setConnectorPluggedInput([] () {return false;}); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); MO_DBG_INFO("Memory requirements UC E06:"); MO_MEM_PRINT_STATS(); } SECTION("UC G01") { setConnectorPluggedInput([] () {return false;}); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); MO_MEM_RESET(); setConnectorPluggedInput([] () {return true;}); loop(); REQUIRE( getChargePointStatus() == ChargePointStatus_Occupied ); MO_DBG_INFO("Memory requirements UC G01:"); MO_MEM_PRINT_STATS(); } SECTION("UC J02") { //scenario preparation REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("PowerPathClosed"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("PowerPathClosed"); getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "TxStartedMeasurands", "")->setString("Energy.Active.Import.Register"); getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "TxUpdatedMeasurands", "")->setString("Power.Active.Import"); getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "TxUpdatedInterval", 0)->setInt(60); getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "TxEndedMeasurands", "")->setString("Current.Import"); getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "TxEndededInterval", 0)->setInt(100); setConnectorPluggedInput([] () {return false;}); setEnergyMeterInput([] () {return 100;}); setPowerMeterInput([] () {return 200;}); addMeterValueInput([] () {return 30;}, "Current.Import", "A"); Timestamp tStart, tUpdated, tEnded; setOnReceiveRequest("TransactionEvent", [&tStart, &tUpdated, &tEnded] (JsonObject request) { const char *eventType = request["eventType"] | (const char*)nullptr; bool eventTypeError = false; if (!strcmp(eventType, "Started")) { tStart = getOcppContext()->getModel().getClock().now(); REQUIRE( request["meterValue"].as().size() >= 1 ); Timestamp tMv; tMv.setTime(request["meterValue"][0]["timestamp"]); REQUIRE( std::abs(tStart - tMv) <= 1); REQUIRE( request["meterValue"][0]["sampledValue"].as().size() >= 1 ); REQUIRE( !strcmp(request["meterValue"][0]["sampledValue"][0]["measurand"] | "_Undefined", "Energy.Active.Import.Register") ); REQUIRE( !strcmp(request["meterValue"][0]["sampledValue"][0]["measurand"] | "_Undefined", "Energy.Active.Import.Register") ); } else if (!strcmp(eventType, "Updated")) { tUpdated = getOcppContext()->getModel().getClock().now(); } else if (!strcmp(eventType, "Ended")) { tEnded = getOcppContext()->getModel().getClock().now(); } else { eventTypeError = true; } REQUIRE( !eventTypeError ); }); loop(); MO_MEM_RESET(); //run scenario setConnectorPluggedInput([] () {return true;}); context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken"); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() != nullptr ); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction()->started ); REQUIRE( !context->getModel().getTransactionService()->getEvse(1)->getTransaction()->stopped ); REQUIRE( getChargePointStatus() == ChargePointStatus_Occupied ); MO_DBG_INFO("Memory requirements UC E01 - S5:"); MO_MEM_PRINT_STATS(); context->getModel().getTransactionService()->getEvse(1)->endAuthorization(); loop(); REQUIRE( (tStart > MIN_TIME) ); //REQUIRE( (tUpdated > MIN_TIME) ); REQUIRE( (tEnded > MIN_TIME) ); } SECTION("TxEvents queue") { getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); bool checkReceivedStarted = false, checkReceivedEnded = false; getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded] () { return new Ocpp16::CustomOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded] (JsonObject request) { //process req const char *eventType = request["eventType"] | (const char*)nullptr; if (!strcmp(eventType, "Started")) { checkReceivedStarted = true; } else if (!strcmp(eventType, "Ended")) { checkReceivedEnded = true; } }, [] () { //create conf auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTokenInfo"]["status"] = "Accepted"; return doc; });}); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); loopback.setConnected(false); context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken", false); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() != nullptr ); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction()->started ); context->getModel().getTransactionService()->getEvse(1)->endAuthorization(); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); loopback.setConnected(true); loop(); REQUIRE( checkReceivedStarted ); REQUIRE( checkReceivedEnded ); } SECTION("TxEvents queue size limit") { getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); bool checkReceivedStarted = false, checkReceivedEnded = false; size_t checkSeqNosSize = 0; getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded, &checkSeqNosSize] () { return new Ocpp16::CustomOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded, &checkSeqNosSize] (JsonObject request) { //process req const char *eventType = request["eventType"] | (const char*)nullptr; if (!strcmp(eventType, "Started")) { checkReceivedStarted = true; } else if (!strcmp(eventType, "Ended")) { checkReceivedEnded = true; } checkSeqNosSize++; }, [] () { //create conf auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTokenInfo"]["status"] = "Accepted"; return doc; });}); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); loopback.setConnected(false); context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken", false); loop(); auto tx = context->getModel().getTransactionService()->getEvse(1)->getTransaction(); REQUIRE( tx != nullptr ); for (size_t i = 0; i < MO_TXEVENTRECORD_SIZE_V201 * 2; i++) { setEvReadyInput([] () {return false;}); loop(); setEvReadyInput([] () {return true;}); loop(); setEvReadyInput([] () {return false;}); loop(); } REQUIRE( tx->seqNos.size() == MO_TXEVENTRECORD_SIZE_V201 ); for (auto seqNo : tx->seqNos) { MO_DBG_DEBUG("stored seqNo %u", seqNo); (void)seqNo; } for (size_t i = 1; i < tx->seqNos.size(); i++) { auto delta = tx->seqNos[i] - tx->seqNos[i-1]; REQUIRE(delta <= 2 * tx->seqNos.back() / MO_TXEVENTRECORD_SIZE_V201 ); } context->getModel().getTransactionService()->getEvse(1)->endAuthorization(); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); loopback.setConnected(true); loop(); REQUIRE( checkReceivedStarted ); REQUIRE( checkReceivedEnded ); REQUIRE( checkSeqNosSize == MO_TXEVENTRECORD_SIZE_V201 ); } SECTION("Tx queue") { getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); std::map> txEventRequests; getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&txEventRequests] () { return new Ocpp16::CustomOperation("TransactionEvent", [&txEventRequests] (JsonObject request) { //process req const char *eventType = request["eventType"] | (const char*)nullptr; if (!strcmp(eventType, "Started")) { std::get<0>(txEventRequests[request["transactionInfo"]["transactionId"] | "_Undefined"]) = true; } else if (!strcmp(eventType, "Ended")) { std::get<1>(txEventRequests[request["transactionInfo"]["transactionId"] | "_Undefined"]) = true; } }, [] () { //create conf auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTokenInfo"]["status"] = "Accepted"; return doc; });}); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); loopback.setConnected(false); for (size_t i = 0; i < MO_TXRECORD_SIZE_V201; i++) { char idTokenBuf [MO_IDTOKEN_LEN_MAX + 1]; snprintf(idTokenBuf, sizeof(idTokenBuf), "mIdToken-%zu", i); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->beginAuthorization(idTokenBuf, false) ); loop(); auto tx = context->getModel().getTransactionService()->getEvse(1)->getTransaction(); REQUIRE( tx != nullptr ); REQUIRE( tx->started ); txEventRequests[tx->transactionId] = {false, false}; context->getModel().getTransactionService()->getEvse(1)->endAuthorization(); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); } REQUIRE( !context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken", false) ); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); loopback.setConnected(true); loop(); for (const auto& txReq : txEventRequests) { MO_DBG_DEBUG("check txId %s", txReq.first.c_str()); REQUIRE( std::get<0>(txReq.second) ); REQUIRE( std::get<1>(txReq.second) ); } REQUIRE( txEventRequests.size() == MO_TXRECORD_SIZE_V201 ); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken", false) ); loop(); auto tx = context->getModel().getTransactionService()->getEvse(1)->getTransaction(); REQUIRE( tx != nullptr ); REQUIRE( tx->started ); context->getModel().getTransactionService()->getEvse(1)->endAuthorization(); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); } SECTION("Power loss during running transaction") { getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); REQUIRE( getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); const char *idTag = "example123"; getOcppContext()->getModel().getTransactionService()->getEvse(1)->beginAuthorization(idTag, false); loop(); auto tx = getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction(); REQUIRE( tx != nullptr ); REQUIRE( tx->started ); auto txNr = tx->txNr; std::string txId = tx->transactionId; //power cut mocpp_deinitialize(); //power restored mocpp_initialize(loopback, ChargerCredentials(), makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail), false, ProtocolVersion(2,0,1)); mocpp_set_timer(custom_timer_cb); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); bool checkProcessed = false; getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkProcessed, txId] () { return new Ocpp16::CustomOperation("TransactionEvent", [&checkProcessed, txId] (JsonObject request) { //process req const char *eventType = request["eventType"] | (const char*)nullptr; REQUIRE( strcmp(eventType, "Started") ); if (!strcmp(eventType, "Ended")) { checkProcessed = true; } REQUIRE( !txId.compare(request["transactionInfo"]["transactionId"] | "_Undefined") ); }, [] () { //create conf auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTokenInfo"]["status"] = "Accepted"; return doc; });}); loop(); //let MO spin up and reconnect tx = getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction(); REQUIRE( tx != nullptr ); REQUIRE( tx->started ); REQUIRE( !tx->stopped ); REQUIRE( tx->txNr == txNr ); REQUIRE( !txId.compare(tx->transactionId) ); REQUIRE( !strcmp(tx->idToken.get(), idTag) ); getOcppContext()->getModel().getTransactionService()->getEvse(1)->endAuthorization(); loop(); tx = getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction(); REQUIRE( tx == nullptr ); REQUIRE( checkProcessed ); //txEvent was sent } SECTION("Power loss with enqueued txEvents") { getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); REQUIRE( getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); loopback.setConnected(false); const char *idTag = "example123"; getOcppContext()->getModel().getTransactionService()->getEvse(1)->beginAuthorization(idTag, false); loop(); auto tx = getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction(); REQUIRE( tx != nullptr ); REQUIRE( tx->started ); auto txNr = tx->txNr; std::string txId = tx->transactionId; setEvReadyInput([] () {return false;}); loop(); setEvReadyInput([] () {return true;}); loop(); setEvReadyInput([] () {return false;}); loop(); size_t seqNosSize = tx->seqNos.size(); size_t checkSeqNosSize = 0; //power cut mocpp_deinitialize(); //power restored mocpp_initialize(loopback, ChargerCredentials(), makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail), false, ProtocolVersion(2,0,1)); mocpp_set_timer(custom_timer_cb); loopback.setConnected(true); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); bool checkReceivedStarted = false, checkReceivedEnded = false; getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded, txId, &checkSeqNosSize] () { return new Ocpp16::CustomOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded, txId, &checkSeqNosSize] (JsonObject request) { //process req const char *eventType = request["eventType"] | (const char*)nullptr; if (!strcmp(eventType, "Started")) { checkReceivedStarted = true; } else if (!strcmp(eventType, "Ended")) { checkReceivedEnded = true; } REQUIRE( !txId.compare(request["transactionInfo"]["transactionId"] | "_Undefined") ); checkSeqNosSize++; }, [] () { //create conf auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTokenInfo"]["status"] = "Accepted"; return doc; });}); loop(); //let MO spin up and reconnect REQUIRE( checkReceivedStarted ); REQUIRE( (seqNosSize == checkSeqNosSize || seqNosSize + 1 == checkSeqNosSize) ); tx = getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction(); REQUIRE( tx != nullptr ); REQUIRE( tx->started ); REQUIRE( !tx->stopped ); REQUIRE( tx->txNr == txNr ); REQUIRE( !txId.compare(tx->transactionId) ); REQUIRE( !strcmp(tx->idToken.get(), idTag) ); getOcppContext()->getModel().getTransactionService()->getEvse(1)->endAuthorization(); loop(); tx = getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction(); REQUIRE( tx == nullptr ); REQUIRE( checkReceivedEnded ); //txEvent was sent } SECTION("Power loss with enqueued transactions") { getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); std::map> txEventRequests; REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); loopback.setConnected(false); for (size_t i = 0; i < MO_TXRECORD_SIZE_V201; i++) { char idTokenBuf [MO_IDTOKEN_LEN_MAX + 1]; snprintf(idTokenBuf, sizeof(idTokenBuf), "mIdToken-%zu", i); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->beginAuthorization(idTokenBuf, false) ); loop(); auto tx = context->getModel().getTransactionService()->getEvse(1)->getTransaction(); REQUIRE( tx != nullptr ); REQUIRE( tx->started ); txEventRequests[tx->transactionId] = {false, false}; context->getModel().getTransactionService()->getEvse(1)->endAuthorization(); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); } //power cut mocpp_deinitialize(); //power restored mocpp_initialize(loopback, ChargerCredentials(), makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail), false, ProtocolVersion(2,0,1)); mocpp_set_timer(custom_timer_cb); loopback.setConnected(true); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&txEventRequests] () { return new Ocpp16::CustomOperation("TransactionEvent", [&txEventRequests] (JsonObject request) { //process req const char *eventType = request["eventType"] | (const char*)nullptr; if (!strcmp(eventType, "Started")) { std::get<0>(txEventRequests[request["transactionInfo"]["transactionId"] | "_Undefined"]) = true; } else if (!strcmp(eventType, "Ended")) { std::get<1>(txEventRequests[request["transactionInfo"]["transactionId"] | "_Undefined"]) = true; } }, [] () { //create conf auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTokenInfo"]["status"] = "Accepted"; return doc; });}); loopback.setConnected(true); loop(); for (const auto& txReq : txEventRequests) { MO_DBG_DEBUG("check txId %s", txReq.first.c_str()); REQUIRE( std::get<0>(txReq.second) ); REQUIRE( std::get<1>(txReq.second) ); } REQUIRE( txEventRequests.size() == MO_TXRECORD_SIZE_V201 ); REQUIRE( getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); } mocpp_deinitialize(); } #endif // MO_ENABLE_V201 ================================================ FILE: tests/Variables.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #if MO_ENABLE_V201 #include #include #include #include "./helpers/testHelper.h" #include #include #include #include #include #include using namespace MicroOcpp; #define GET_CONFIG_ALL "[2,\"test-msg\",\"GetVariable\",{}]" #define KNOWN_KEY "__ExistingKey" #define UNKOWN_KEY "__UnknownKey" #define GET_CONFIG_KNOWN_UNKOWN "[2,\"test-mst\",\"GetVariable\",{\"key\":[\"" KNOWN_KEY "\",\"" UNKOWN_KEY "\"]}]" TEST_CASE( "Variable" ) { printf("\nRun %s\n", "Variable"); //clean state auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); SECTION("Basic container operations"){ auto container = std::unique_ptr(new VariableContainerOwning()); //check emptyness REQUIRE( container->size() == 0 ); //add first config, fetch by index Variable::AttributeTypeSet attrs = Variable::AttributeType::Actual; auto configFirst = makeVariable(Variable::InternalDataType::Int, attrs); configFirst->setName("cFirst"); configFirst->setComponentId("mComponent"); auto configFirstRaw = configFirst.get(); REQUIRE( container->size() == 0 ); REQUIRE( container->add(std::move(configFirst)) ); REQUIRE( container->size() == 1 ); REQUIRE( container->getVariable((size_t) 0) == configFirstRaw); //add one config of each type auto cInt = makeVariable(Variable::InternalDataType::Int, attrs); cInt->setName("cInt"); cInt->setComponentId("mComponent"); auto cBool = makeVariable(Variable::InternalDataType::Bool, attrs); cBool->setName("cBool"); cBool->setComponentId("mComponent"); auto cBoolRaw = cBool.get(); auto cString = makeVariable(Variable::InternalDataType::String, attrs); cString->setName("cString"); cString->setComponentId("mComponent"); container->add(std::move(cInt)); container->add(std::move(cBool)); container->add(std::move(cString)); REQUIRE( container->size() == 4 ); //fetch config by key REQUIRE( container->getVariable(cBoolRaw->getComponentId(), cBoolRaw->getName()) == cBoolRaw); } SECTION("Persistency on filesystem") { auto container = std::unique_ptr(new VariableContainerOwning()); container->enablePersistency(filesystem, MO_FILENAME_PREFIX "persistent1.jsn"); //trivial load call REQUIRE( container->load() ); REQUIRE( container->size() == 0 ); //add config, store, load again auto cString = makeVariable(Variable::InternalDataType::String, Variable::AttributeType::Actual); cString->setName("cString"); cString->setComponentId("mComponent"); cString->setString("mValue"); container->add(std::move(cString)); REQUIRE( container->size() == 1 ); REQUIRE( container->commit() ); //store container.reset(); //destroy //...load again auto container2 = std::unique_ptr(new VariableContainerOwning()); container2->enablePersistency(filesystem, MO_FILENAME_PREFIX "persistent1.jsn"); REQUIRE( container2->size() == 0 ); auto cString2 = makeVariable(Variable::InternalDataType::String, Variable::AttributeType::Actual); cString2->setName("cString"); cString2->setComponentId("mComponent"); cString2->setString("mValue"); container2->add(std::move(cString2)); REQUIRE( container2->size() == 1 ); REQUIRE( container2->load() ); REQUIRE( container2->size() == 1 ); auto cString3 = container2->getVariable("mComponent", "cString"); REQUIRE( cString3 != nullptr ); REQUIRE( !strcmp(cString3->getString(), "mValue") ); } LoopbackConnection loopback; //initialize Context with dummy socket mocpp_set_timer(custom_timer_cb); SECTION("Variable API") { //declare configs mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); auto vs = getOcppContext()->getModel().getVariableService(); auto cInt = vs->declareVariable("mComponent", "cInt", 42); REQUIRE( cInt != nullptr ); vs->declareVariable("mComponent", "cBool", true); vs->declareVariable("mComponent", "cString", "mValue"); //fetch config REQUIRE( vs->declareVariable("mComponent", "cInt", -1)->getInt() == 42 ); #if 0 //store, destroy, reload REQUIRE( configuration_save() ); cInt.reset(); configuration_deinit(); REQUIRE( getVariablePublic("cInt") == nullptr); REQUIRE( configuration_init(filesystem) ); //reload //fetch configs (declare with different factory default - should remain at original value) auto cInt2 = vs->declareVariable("cInt", -1); auto cBool2 = vs->declareVariable("cBool", false); auto cString2 = vs->declareVariable("cString", "no effect"); REQUIRE( configuration_load() ); //load config objects with stored values //check load result REQUIRE( cInt2->getInt() == 42 ); REQUIRE( cBool2->getBool() == true ); REQUIRE( !strcmp(cString2->getString(), "mValue") ); #else auto cInt2 = cInt; #endif //declare config twice auto cInt3 = vs->declareVariable("mComponent", "cInt", -1); REQUIRE( cInt3 == cInt2 ); #if 0 //store, destroy, reload REQUIRE( configuration_save() ); configuration_deinit(); REQUIRE( getVariablePublic("cInt") == nullptr); REQUIRE( configuration_init(filesystem) ); //reload auto cNewType2 = vs->declareVariable("cInt", "no effect"); REQUIRE( configuration_load() ); REQUIRE( !strcmp(cNewType2->getString(), "mValue2") ); //get config before declared (container needs to be declared already at this point) auto cString3 = getVariablePublic("cString"); REQUIRE( !strcmp(cString3->getString(), "mValue") ); configuration_deinit(); //value needs to outlive container configuration_init(filesystem); auto cString4 = vs->declareVariable("cString2", "mValue3"); configuration_deinit(); REQUIRE( !strcmp(cString4->getString(), "mValue3") ); FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); #else mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); #endif //config accessibility / permissions vs = getOcppContext()->getModel().getVariableService(); Variable::Mutability mutability = Variable::Mutability::ReadWrite; bool persistent = false; Variable::AttributeTypeSet attrs = Variable::AttributeType::Actual; bool rebootRequired = false; auto cInt6 = vs->declareVariable("mComponent", "cInt", 42, mutability, persistent, attrs, rebootRequired); REQUIRE( cInt6->getMutability() == Variable::Mutability::ReadWrite ); REQUIRE( !cInt6->isPersistent() ); REQUIRE( !cInt6->isRebootRequired() ); REQUIRE( vs->declareVariable("mComponent", "cInt", 42) ); //revoke permissions mutability = Variable::Mutability::ReadOnly; persistent = true; rebootRequired = true; vs->declareVariable("mComponent", "cInt", 42, mutability, persistent, attrs, rebootRequired); REQUIRE( cInt6->getMutability() == mutability ); REQUIRE( cInt6->isPersistent() ); REQUIRE( cInt6->isRebootRequired() ); //revoked permissions cannot be reverted mutability = Variable::Mutability::ReadWrite; persistent = false; rebootRequired = false; auto cInt7 = vs->declareVariable("mComponent", "cInt", 42, mutability, persistent, attrs, rebootRequired); REQUIRE( cInt7->getMutability() == Variable::Mutability::ReadOnly ); REQUIRE( cInt6->isPersistent() ); REQUIRE( cInt7->isRebootRequired() ); } #if 0 SECTION("Main lib integration") { //basic lifecycle mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); REQUIRE( getVariablePublic("ConnectionTimeOut") ); REQUIRE( !getVariableContainersPublic().empty() ); mocpp_deinitialize(); REQUIRE( !getVariablePublic("ConnectionTimeOut") ); REQUIRE( getVariableContainersPublic().empty() ); //modify standard config ConnectionTimeOut. This config is not modified by the main lib during normal initialization / deinitialization mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); auto config = getVariablePublic("ConnectionTimeOut"); config->setInt(1234); //update configuration_save(); //write back mocpp_deinitialize(); mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); REQUIRE( getVariablePublic("ConnectionTimeOut")->getInt() == 1234 ); mocpp_deinitialize(); } #endif #if 0 SECTION("GetVariables") { mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); loop(); vs->declareVariable(KNOWN_KEY, 1234, MO_FILENAME_PREFIX "persistent1.jsn", false); bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "GetVariables", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; JsonArray configurationKey = payload["configurationKey"]; bool foundCustomConfig = false; bool foundStandardConfig = false; for (JsonObject keyvalue : configurationKey) { MO_DBG_DEBUG("key %s", keyvalue["key"] | "_Undefined"); if (!strcmp(keyvalue["key"] | "_Undefined", KNOWN_KEY)) { foundCustomConfig = true; REQUIRE( (keyvalue["readonly"] | true) == false ); REQUIRE( !strcmp(keyvalue["value"] | "_Undefined", "1234") ); } else if (!strcmp(keyvalue["key"] | "_Undefined", "ConnectionTimeOut")) { foundStandardConfig = true; } } REQUIRE( foundCustomConfig ); REQUIRE( foundStandardConfig ); } ))); loop(); REQUIRE(checkProcessed); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "GetVariable", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(2)); auto payload = doc->to(); auto key = payload.createNestedArray("key"); key.add(KNOWN_KEY); key.add(UNKOWN_KEY); return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; JsonArray configurationKey = payload["configurationKey"]; bool foundCustomConfig = false; for (JsonObject keyvalue : configurationKey) { if (!strcmp(keyvalue["key"] | "_Undefined", KNOWN_KEY)) { foundCustomConfig = true; break; } } REQUIRE( foundCustomConfig ); JsonArray unknownKey = payload["unknownKey"]; bool foundUnkownKey = false; for (const char *key : unknownKey) { if (!strcmp(key, UNKOWN_KEY)) { foundUnkownKey = true; } } REQUIRE( foundUnkownKey ); } ))); loop(); REQUIRE(checkProcessed); mocpp_deinitialize(); } SECTION("ChangeVariable") { mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); loop(); vs->declareVariable(KNOWN_KEY, 0, MO_FILENAME_PREFIX "persistent1.jsn", false); //update existing config bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ChangeVariable", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = KNOWN_KEY; payload["value"] = "1234"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE(checkProcessed); REQUIRE( getVariablePublic(KNOWN_KEY)->getInt() == 1234 ); //try to update not existing key checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ChangeVariable", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = UNKOWN_KEY; payload["value"] = "no effect"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "NotSupported") ); } ))); loop(); REQUIRE( checkProcessed ); //try to update config with malformatted value checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ChangeVariable", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = KNOWN_KEY; payload["value"] = "not convertible to int"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Rejected") ); } ))); loop(); REQUIRE( checkProcessed ); //try to update config with value validation //value is valid if it begins with 1 registerVariableValidator(KNOWN_KEY, [] (const char *v) { return v[0] == '1'; }); //validation success checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ChangeVariable", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = KNOWN_KEY; payload["value"] = "100234"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); } ))); loop(); REQUIRE( checkProcessed ); REQUIRE( getVariablePublic(KNOWN_KEY)->getInt() == 100234 ); //validation failure checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ChangeVariable", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = KNOWN_KEY; payload["value"] = "4321"; return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf checkProcessed = true; REQUIRE( !strcmp(payload["status"] | "_Undefined", "Rejected") ); } ))); loop(); REQUIRE( checkProcessed ); REQUIRE( getVariablePublic(KNOWN_KEY)->getInt() == 100234 ); //keep old value mocpp_deinitialize(); } SECTION("Define factory defaults for standard configs") { //set factory default for standard config ConnectionTimeOut configuration_init(filesystem); auto factoryConnectionTimeOut = vs->declareVariable("ConnectionTimeOut", 1234, MO_FILENAME_PREFIX "factory.jsn"); mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); auto connectionTimeout2 = vs->declareVariable("ConnectionTimeOut", 4321); REQUIRE( connectionTimeout2->getInt() == 1234 ); REQUIRE( connectionTimeout2 == factoryConnectionTimeOut ); configuration_save(); mocpp_deinitialize(); //this time, factory default is not given (will lead to duplicates, should be considered in sanitization) mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); REQUIRE( getVariablePublic("ConnectionTimeOut")->getInt() != 1234 ); mocpp_deinitialize(); //provide factory default again configuration_init(filesystem); vs->declareVariable("ConnectionTimeOut", 4321, MO_FILENAME_PREFIX "factory.jsn"); mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); REQUIRE( getVariablePublic("ConnectionTimeOut")->getInt() == 1234 ); mocpp_deinitialize(); } #endif SECTION("GetVariables request") { mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); auto vs = getOcppContext()->getModel().getVariableService(); auto varString = vs->declareVariable("mComponent", "mString", "mValue"); REQUIRE( varString != nullptr ); REQUIRE( !strcmp(varString->getString(), "mValue") ); loop(); MO_MEM_RESET(); bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest( new Ocpp16::CustomOperation("GetVariables", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(1)); auto payload = doc->to(); auto getVariableData = payload.createNestedArray("getVariableData"); getVariableData[0]["component"]["name"] = "mComponent"; getVariableData[0]["variable"]["name"] = "mString"; return doc; }, [&checkProcessed] (JsonObject payload) { //process conf JsonArray getVariableResult = payload["getVariableResult"]; REQUIRE( !strcmp(getVariableResult[0]["attributeStatus"] | "_Undefined", "Accepted") ); REQUIRE( !strcmp(getVariableResult[0]["component"]["name"] | "_Undefined", "mComponent") ); REQUIRE( !strcmp(getVariableResult[0]["variable"]["name"] | "_Undefined", "mString") ); REQUIRE( !strcmp(getVariableResult[0]["attributeValue"] | "_Undefined", "mValue") ); checkProcessed = true; }))); loop(); REQUIRE( checkProcessed ); MO_MEM_PRINT_STATS(); } SECTION("SetVariables request") { mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); auto vs = getOcppContext()->getModel().getVariableService(); auto varString = vs->declareVariable("mComponent", "mString", ""); REQUIRE( varString != nullptr ); REQUIRE( !strcmp(varString->getString(), "") ); loop(); MO_MEM_RESET(); bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest( new Ocpp16::CustomOperation("SetVariables", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(1)); auto payload = doc->to(); auto setVariableData = payload.createNestedArray("setVariableData"); setVariableData[0]["component"]["name"] = "mComponent"; setVariableData[0]["variable"]["name"] = "mString"; setVariableData[0]["attributeValue"] = "mValue"; return doc; }, [&checkProcessed] (JsonObject payload) { //process conf JsonArray setVariableResult = payload["setVariableResult"]; REQUIRE( !strcmp(setVariableResult[0]["attributeStatus"] | "_Undefined", "Accepted") ); REQUIRE( !strcmp(setVariableResult[0]["component"]["name"] | "_Undefined", "mComponent") ); REQUIRE( !strcmp(setVariableResult[0]["variable"]["name"] | "_Undefined", "mString") ); checkProcessed = true; }))); loop(); REQUIRE( checkProcessed ); MO_MEM_PRINT_STATS(); } SECTION("GetBaseReport request") { mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); auto vs = getOcppContext()->getModel().getVariableService(); auto varString = vs->declareVariable("mComponent", "mString", ""); REQUIRE( varString != nullptr ); REQUIRE( !strcmp(varString->getString(), "") ); loop(); MO_MEM_RESET(); bool checkProcessedNotification = false; Timestamp checkTimestamp; getOcppContext()->getOperationRegistry().registerOperation("NotifyReport", [&checkProcessedNotification, &checkTimestamp] () { return new Ocpp16::CustomOperation("NotifyReport", [ &checkProcessedNotification, &checkTimestamp] (JsonObject payload) { //process req checkProcessedNotification = true; REQUIRE( (payload["requestId"] | -1) == 1); checkTimestamp.setTime(payload["generatedAt"] | "_Undefined"); REQUIRE( (payload["seqNo"] | -1) == 0); bool foundVar = false; for (auto reportData : payload["reportData"].as()) { if (!strcmp(reportData["component"]["name"] | "_Undefined", "mComponent") && !strcmp(reportData["variable"]["name"] | "_Undefined", "mString")) { foundVar = true; } } REQUIRE( foundVar ); }, [] () { //create conf return createEmptyDocument(); }); }); bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest( new Ocpp16::CustomOperation("GetBaseReport", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["requestId"] = 1; payload["reportBase"] = "FullInventory"; return doc; }, [&checkProcessed] (JsonObject payload) { //process conf REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); checkProcessed = true; }))); loop(); REQUIRE( checkProcessed ); REQUIRE( checkProcessedNotification ); REQUIRE( std::abs(getOcppContext()->getModel().getClock().now() - checkTimestamp) <= 10 ); MO_MEM_PRINT_STATS(); } mocpp_deinitialize(); } #endif // MO_ENABLE_V201 ================================================ FILE: tests/benchmarks/firmware_size/main.cpp ================================================ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include MicroOcpp::LoopbackConnection g_loopback; void setup() { ocpp_deinitialize(); #if MO_ENABLE_V201 mocpp_initialize(g_loopback, ChargerCredentials::v201(),MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail),true,MicroOcpp::ProtocolVersion(2,0,1)); #else mocpp_initialize(g_loopback, ChargerCredentials()); #endif ocpp_beginTransaction(""); ocpp_beginTransaction_authorized("",""); ocpp_endTransaction("",""); ocpp_endTransaction_authorized("",""); ocpp_isTransactionActive(); ocpp_isTransactionRunning(); ocpp_getTransactionIdTag(); ocpp_getTransaction(); ocpp_ocppPermitsCharge(); ocpp_getChargePointStatus(); ocpp_setConnectorPluggedInput([] () {return false;}); ocpp_setEnergyMeterInput([] () {return 0;}); ocpp_setPowerMeterInput([] () {return 0.f;}); ocpp_setSmartChargingPowerOutput([] (float) {}); ocpp_setSmartChargingCurrentOutput([] (float) {}); ocpp_setSmartChargingOutput([] (float,float,int) {}); ocpp_setEvReadyInput([] () {return false;}); ocpp_setEvseReadyInput([] () {return false;}); ocpp_addErrorCodeInput([] () {return (const char*)nullptr;}); addErrorDataInput([] () {return MicroOcpp::ErrorData("");}); ocpp_addMeterValueInputFloat([] () {return 0.f;},"","","",""); ocpp_setOccupiedInput([] () {return false;}); ocpp_setStartTxReadyInput([] () {return false;}); ocpp_setStopTxReadyInput([] () {return false;}); ocpp_setTxNotificationOutput([] (OCPP_Transaction*, TxNotification) {}); #if MO_ENABLE_CONNECTOR_LOCK ocpp_setOnUnlockConnectorInOut([] () {return UnlockConnectorResult_UnlockFailed;}); #endif isOperative(); setOnResetNotify([] (bool) {return false;}); setOnResetExecute([] (bool) {return false;}); getFirmwareService()->getFirmwareStatus(); getDiagnosticsService()->getDiagnosticsStatus(); #if MO_ENABLE_CERT_MGMT setCertificateStore(nullptr); #endif getOcppContext(); } void loop() { mocpp_loop(); } ================================================ FILE: tests/benchmarks/firmware_size/platformio.ini ================================================ ; matth-x/MicroOcpp ; Copyright Matthias Akstaller 2019 - 2024 ; MIT License [common] platform = espressif32@6.8.1 board = esp-wrover-kit framework = arduino lib_deps = bblanchon/ArduinoJson@6.20.1 build_flags= -D MO_DBG_LEVEL=MO_DL_NONE ; don't take debug messages into account -D MO_CUSTOM_WS [env:v16] platform = ${common.platform} board = ${common.board} framework = ${common.framework} lib_deps = ${common.lib_deps} build_flags = ${common.build_flags} -D MO_ENABLE_MBEDTLS=1 -D MO_ENABLE_CERT_MGMT=1 -D MO_ENABLE_RESERVATION=1 -D MO_ENABLE_LOCAL_AUTH=1 -D MO_REPORT_NOERROR=1 -D MO_ENABLE_CONNECTOR_LOCK=1 [env:v201] platform = ${common.platform} board = ${common.board} framework = ${common.framework} lib_deps = ${common.lib_deps} build_flags = ${common.build_flags} -D MO_ENABLE_V201=1 -D MO_ENABLE_MBEDTLS=1 -D MO_ENABLE_CERT_MGMT=1 ================================================ FILE: tests/benchmarks/scripts/eval_firmware_size.py ================================================ import sys import numpy as np import pandas as pd # load data COLUMN_BINSIZE = 'Binary size (Bytes)' def load_compilation_units(fn): df = pd.read_csv(fn, index_col="compileunits").filter(like="lib/MicroOcpp/src/MicroOcpp", axis=0).filter(['Module','v16','v201','vmsize'], axis=1).sort_index() df.index.names = ['Compile Unit'] df.index = df.index.map(lambda s: s[len("lib/MicroOcpp/src/"):] if s.startswith("lib/MicroOcpp/src/") else s) df.index = df.index.map(lambda s: s[len("MicroOcpp/"):] if s.startswith("MicroOcpp/") else s) df.rename(columns={'vmsize': COLUMN_BINSIZE}, inplace=True) return df cunits_v16 = load_compilation_units('docs/assets/tables/bloaty_v16.csv') cunits_v201 = load_compilation_units('docs/assets/tables/bloaty_v201.csv') # categorize data def categorize_table(df): df["v16"] = ' ' df["v201"] = ' ' df["Module"] = '' TICK = 'x' MODULE_GENERAL = 'General' MODULE_HAL = 'General - Hardware Abstraction Layer' MODULE_RPC = 'General - RPC framework' MODULE_API = 'General - API' MODULE_CORE = 'Core' MODULE_CONFIGURATION = 'Configuration' MODULE_FW_MNGT = 'Firmware Management' MODULE_TRIGGERMESSAGE = 'TriggerMessage' MODULE_SECURITY = 'A - Security' MODULE_PROVISIONING = 'B - Provisioning' MODULE_PROVISIONING_VARS = 'B - Provisioning - Variables' MODULE_AUTHORIZATION = 'C - Authorization' MODULE_LOCALAUTH = 'D - Local Authorization List Management' MODULE_TX = 'E - Transactions' MODULE_REMOTECONTROL = 'F - RemoteControl' MODULE_AVAILABILITY = 'G - Availability' MODULE_RESERVATION = 'H - Reservation' MODULE_METERVALUES = 'J - MeterValues' MODULE_SMARTCHARGING = 'K - SmartCharging' MODULE_CERTS = 'M - Certificate Management' df.at['MicroOcpp.cpp', 'v16'] = TICK df.at['MicroOcpp.cpp', 'v201'] = TICK df.at['MicroOcpp.cpp', 'Module'] = MODULE_API df.at['Core/Configuration.cpp', 'v16'] = TICK df.at['Core/Configuration.cpp', 'v201'] = TICK df.at['Core/Configuration.cpp', 'Module'] = MODULE_CONFIGURATION if 'Core/Configuration_c.cpp' in df.index: df.at['Core/Configuration_c.cpp', 'v16'] = TICK df.at['Core/Configuration_c.cpp', 'v201'] = TICK df.at['Core/Configuration_c.cpp', 'Module'] = MODULE_CONFIGURATION df.at['Core/ConfigurationContainer.cpp', 'v16'] = TICK df.at['Core/ConfigurationContainer.cpp', 'v201'] = TICK df.at['Core/ConfigurationContainer.cpp', 'Module'] = MODULE_CONFIGURATION df.at['Core/ConfigurationContainerFlash.cpp', 'v16'] = TICK df.at['Core/ConfigurationContainerFlash.cpp', 'v201'] = TICK df.at['Core/ConfigurationContainerFlash.cpp', 'Module'] = MODULE_CONFIGURATION df.at['Core/ConfigurationKeyValue.cpp', 'v16'] = TICK df.at['Core/ConfigurationKeyValue.cpp', 'v201'] = TICK df.at['Core/ConfigurationKeyValue.cpp', 'Module'] = MODULE_CONFIGURATION df.at['Core/Connection.cpp', 'v16'] = TICK df.at['Core/Connection.cpp', 'v201'] = TICK df.at['Core/Connection.cpp', 'Module'] = MODULE_HAL df.at['Core/Context.cpp', 'v16'] = TICK df.at['Core/Context.cpp', 'v201'] = TICK df.at['Core/Context.cpp', 'Module'] = MODULE_GENERAL df.at['Core/FilesystemAdapter.cpp', 'v16'] = TICK df.at['Core/FilesystemAdapter.cpp', 'v201'] = TICK df.at['Core/FilesystemAdapter.cpp', 'Module'] = MODULE_HAL df.at['Core/FilesystemUtils.cpp', 'v16'] = TICK df.at['Core/FilesystemUtils.cpp', 'v201'] = TICK df.at['Core/FilesystemUtils.cpp', 'Module'] = MODULE_GENERAL df.at['Core/FtpMbedTLS.cpp', 'v16'] = TICK df.at['Core/FtpMbedTLS.cpp', 'v201'] = TICK df.at['Core/FtpMbedTLS.cpp', 'Module'] = MODULE_GENERAL df.at['Core/Memory.cpp', 'v16'] = TICK df.at['Core/Memory.cpp', 'v201'] = TICK df.at['Core/Memory.cpp', 'Module'] = MODULE_GENERAL df.at['Core/Operation.cpp', 'v16'] = TICK df.at['Core/Operation.cpp', 'v201'] = TICK df.at['Core/Operation.cpp', 'Module'] = MODULE_RPC df.at['Core/OperationRegistry.cpp', 'v16'] = TICK df.at['Core/OperationRegistry.cpp', 'v201'] = TICK df.at['Core/OperationRegistry.cpp', 'Module'] = MODULE_RPC df.at['Core/Request.cpp', 'v16'] = TICK df.at['Core/Request.cpp', 'v201'] = TICK df.at['Core/Request.cpp', 'Module'] = MODULE_RPC df.at['Core/RequestQueue.cpp', 'v16'] = TICK df.at['Core/RequestQueue.cpp', 'v201'] = TICK df.at['Core/RequestQueue.cpp', 'Module'] = MODULE_RPC df.at['Core/Time.cpp', 'v16'] = TICK df.at['Core/Time.cpp', 'v201'] = TICK df.at['Core/Time.cpp', 'Module'] = MODULE_GENERAL df.at['Core/UuidUtils.cpp', 'v16'] = TICK df.at['Core/UuidUtils.cpp', 'v201'] = TICK df.at['Core/UuidUtils.cpp', 'Module'] = MODULE_GENERAL if 'Debug.cpp' in df.index: df.at['Debug.cpp', 'v16'] = TICK df.at['Debug.cpp', 'v201'] = TICK df.at['Debug.cpp', 'Module'] = MODULE_HAL if 'Platform.cpp' in df.index: df.at['Platform.cpp', 'v16'] = TICK df.at['Platform.cpp', 'v201'] = TICK df.at['Platform.cpp', 'Module'] = MODULE_HAL df.at['Model/Authorization/AuthorizationData.cpp', 'v16'] = TICK df.at['Model/Authorization/AuthorizationData.cpp', 'Module'] = MODULE_LOCALAUTH df.at['Model/Authorization/AuthorizationList.cpp', 'v16'] = TICK df.at['Model/Authorization/AuthorizationList.cpp', 'Module'] = MODULE_LOCALAUTH df.at['Model/Authorization/AuthorizationService.cpp', 'v16'] = TICK df.at['Model/Authorization/AuthorizationService.cpp', 'Module'] = MODULE_LOCALAUTH if 'Model/Authorization/IdToken.cpp' in df.index: df.at['Model/Authorization/IdToken.cpp', 'v201'] = TICK df.at['Model/Authorization/IdToken.cpp', 'Module'] = MODULE_AUTHORIZATION if 'Model/Availability/AvailabilityService.cpp' in df.index: df.at['Model/Availability/AvailabilityService.cpp', 'v16'] = TICK df.at['Model/Availability/AvailabilityService.cpp', 'v201'] = TICK df.at['Model/Availability/AvailabilityService.cpp', 'Module'] = MODULE_AVAILABILITY df.at['Model/Boot/BootService.cpp', 'v16'] = TICK df.at['Model/Boot/BootService.cpp', 'v201'] = TICK df.at['Model/Boot/BootService.cpp', 'Module'] = MODULE_PROVISIONING df.at['Model/Certificates/Certificate.cpp', 'v16'] = TICK df.at['Model/Certificates/Certificate.cpp', 'v201'] = TICK df.at['Model/Certificates/Certificate.cpp', 'Module'] = MODULE_CERTS df.at['Model/Certificates/CertificateMbedTLS.cpp', 'v16'] = TICK df.at['Model/Certificates/CertificateMbedTLS.cpp', 'v201'] = TICK df.at['Model/Certificates/CertificateMbedTLS.cpp', 'Module'] = MODULE_CERTS if 'Model/Certificates/Certificate_c.cpp' in df.index: df.at['Model/Certificates/Certificate_c.cpp', 'v16'] = TICK df.at['Model/Certificates/Certificate_c.cpp', 'v201'] = TICK df.at['Model/Certificates/Certificate_c.cpp', 'Module'] = MODULE_CERTS df.at['Model/Certificates/CertificateService.cpp', 'v16'] = TICK df.at['Model/Certificates/CertificateService.cpp', 'v201'] = TICK df.at['Model/Certificates/CertificateService.cpp', 'Module'] = MODULE_CERTS df.at['Model/ConnectorBase/Connector.cpp', 'v16'] = TICK df.at['Model/ConnectorBase/Connector.cpp', 'Module'] = MODULE_CORE df.at['Model/ConnectorBase/ConnectorsCommon.cpp', 'v16'] = TICK df.at['Model/ConnectorBase/ConnectorsCommon.cpp', 'Module'] = MODULE_CORE df.at['Model/Diagnostics/DiagnosticsService.cpp', 'v16'] = TICK df.at['Model/Diagnostics/DiagnosticsService.cpp', 'Module'] = MODULE_FW_MNGT df.at['Model/FirmwareManagement/FirmwareService.cpp', 'v16'] = TICK df.at['Model/FirmwareManagement/FirmwareService.cpp', 'Module'] = MODULE_FW_MNGT df.at['Model/Heartbeat/HeartbeatService.cpp', 'v16'] = TICK df.at['Model/Heartbeat/HeartbeatService.cpp', 'v201'] = TICK df.at['Model/Heartbeat/HeartbeatService.cpp', 'Module'] = MODULE_AVAILABILITY df.at['Model/Metering/MeteringConnector.cpp', 'v16'] = TICK df.at['Model/Metering/MeteringConnector.cpp', 'Module'] = MODULE_METERVALUES df.at['Model/Metering/MeteringService.cpp', 'v16'] = TICK df.at['Model/Metering/MeteringService.cpp', 'Module'] = MODULE_METERVALUES df.at['Model/Metering/MeterStore.cpp', 'v16'] = TICK df.at['Model/Metering/MeterStore.cpp', 'Module'] = MODULE_METERVALUES df.at['Model/Metering/MeterValue.cpp', 'v16'] = TICK df.at['Model/Metering/MeterValue.cpp', 'Module'] = MODULE_METERVALUES if 'Model/Metering/MeterValuesV201.cpp' in df.index: df.at['Model/Metering/MeterValuesV201.cpp', 'v201'] = TICK df.at['Model/Metering/MeterValuesV201.cpp', 'Module'] = MODULE_METERVALUES if 'Model/Metering/ReadingContext.cpp' in df.index: df.at['Model/Metering/ReadingContext.cpp', 'v201'] = TICK df.at['Model/Metering/ReadingContext.cpp', 'Module'] = MODULE_METERVALUES df.at['Model/Metering/SampledValue.cpp', 'v16'] = TICK df.at['Model/Metering/SampledValue.cpp', 'Module'] = MODULE_METERVALUES if 'Model/RemoteControl/RemoteControlService.cpp' in df.index: df.at['Model/RemoteControl/RemoteControlService.cpp', 'v201'] = TICK df.at['Model/RemoteControl/RemoteControlService.cpp', 'Module'] = MODULE_REMOTECONTROL df.at['Model/Model.cpp', 'v16'] = TICK df.at['Model/Model.cpp', 'v201'] = TICK df.at['Model/Model.cpp', 'Module'] = MODULE_GENERAL df.at['Model/Reservation/Reservation.cpp', 'v16'] = TICK df.at['Model/Reservation/Reservation.cpp', 'Module'] = MODULE_RESERVATION df.at['Model/Reservation/ReservationService.cpp', 'v16'] = TICK df.at['Model/Reservation/ReservationService.cpp', 'Module'] = MODULE_RESERVATION df.at['Model/Reset/ResetService.cpp', 'v16'] = TICK df.at['Model/Reset/ResetService.cpp', 'v201'] = TICK df.at['Model/Reset/ResetService.cpp', 'Module'] = MODULE_PROVISIONING df.at['Model/SmartCharging/SmartChargingModel.cpp', 'v16'] = TICK df.at['Model/SmartCharging/SmartChargingModel.cpp', 'Module'] = MODULE_SMARTCHARGING df.at['Model/SmartCharging/SmartChargingService.cpp', 'v16'] = TICK df.at['Model/SmartCharging/SmartChargingService.cpp', 'Module'] = MODULE_SMARTCHARGING df.at['Model/Transactions/Transaction.cpp', 'v16'] = TICK df.at['Model/Transactions/Transaction.cpp', 'v201'] = TICK df.at['Model/Transactions/Transaction.cpp', 'Module'] = MODULE_TX df.at['Model/Transactions/TransactionDeserialize.cpp', 'v16'] = TICK df.at['Model/Transactions/TransactionDeserialize.cpp', 'Module'] = MODULE_TX if 'Model/Transactions/TransactionService.cpp' in df.index: df.at['Model/Transactions/TransactionService.cpp', 'v201'] = TICK df.at['Model/Transactions/TransactionService.cpp', 'Module'] = MODULE_TX df.at['Model/Transactions/TransactionStore.cpp', 'v16'] = TICK df.at['Model/Transactions/TransactionStore.cpp', 'Module'] = MODULE_TX if 'Model/Variables/Variable.cpp' in df.index: df.at['Model/Variables/Variable.cpp', 'v201'] = TICK df.at['Model/Variables/Variable.cpp', 'Module'] = MODULE_PROVISIONING_VARS if 'Model/Variables/VariableContainer.cpp' in df.index: df.at['Model/Variables/VariableContainer.cpp', 'v201'] = TICK df.at['Model/Variables/VariableContainer.cpp', 'Module'] = MODULE_PROVISIONING_VARS if 'Model/Variables/VariableService.cpp' in df.index: df.at['Model/Variables/VariableService.cpp', 'v201'] = TICK df.at['Model/Variables/VariableService.cpp', 'Module'] = MODULE_PROVISIONING_VARS df.at['Operations/Authorize.cpp', 'v16'] = TICK df.at['Operations/Authorize.cpp', 'v201'] = TICK df.at['Operations/Authorize.cpp', 'Module'] = MODULE_AUTHORIZATION df.at['Operations/BootNotification.cpp', 'v16'] = TICK df.at['Operations/BootNotification.cpp', 'v201'] = TICK df.at['Operations/BootNotification.cpp', 'Module'] = MODULE_PROVISIONING df.at['Operations/CancelReservation.cpp', 'v16'] = TICK df.at['Operations/CancelReservation.cpp', 'Module'] = MODULE_RESERVATION df.at['Operations/ChangeAvailability.cpp', 'v16'] = TICK df.at['Operations/ChangeAvailability.cpp', 'Module'] = MODULE_AVAILABILITY df.at['Operations/ChangeConfiguration.cpp', 'v16'] = TICK df.at['Operations/ChangeConfiguration.cpp', 'Module'] = MODULE_CONFIGURATION df.at['Operations/ClearCache.cpp', 'v16'] = TICK df.at['Operations/ClearCache.cpp', 'Module'] = MODULE_CORE df.at['Operations/ClearChargingProfile.cpp', 'v16'] = TICK df.at['Operations/ClearChargingProfile.cpp', 'Module'] = MODULE_SMARTCHARGING df.at['Operations/CustomOperation.cpp', 'v16'] = TICK df.at['Operations/CustomOperation.cpp', 'v201'] = TICK df.at['Operations/CustomOperation.cpp', 'Module'] = MODULE_RPC df.at['Operations/DataTransfer.cpp', 'v16'] = TICK df.at['Operations/DataTransfer.cpp', 'Module'] = MODULE_CORE df.at['Operations/DeleteCertificate.cpp', 'v16'] = TICK df.at['Operations/DeleteCertificate.cpp', 'v201'] = TICK df.at['Operations/DeleteCertificate.cpp', 'Module'] = MODULE_CERTS df.at['Operations/DiagnosticsStatusNotification.cpp', 'v16'] = TICK df.at['Operations/DiagnosticsStatusNotification.cpp', 'Module'] = MODULE_FW_MNGT df.at['Operations/FirmwareStatusNotification.cpp', 'v16'] = TICK df.at['Operations/FirmwareStatusNotification.cpp', 'Module'] = MODULE_FW_MNGT if 'Operations/GetBaseReport.cpp' in df.index: df.at['Operations/GetBaseReport.cpp', 'v201'] = TICK df.at['Operations/GetBaseReport.cpp', 'Module'] = MODULE_PROVISIONING_VARS df.at['Operations/GetCompositeSchedule.cpp', 'v16'] = TICK df.at['Operations/GetCompositeSchedule.cpp', 'Module'] = MODULE_SMARTCHARGING df.at['Operations/GetConfiguration.cpp', 'v16'] = TICK df.at['Operations/GetConfiguration.cpp', 'v201'] = TICK df.at['Operations/GetConfiguration.cpp', 'Module'] = MODULE_CONFIGURATION df.at['Operations/GetDiagnostics.cpp', 'v16'] = TICK df.at['Operations/GetDiagnostics.cpp', 'Module'] = MODULE_FW_MNGT df.at['Operations/GetInstalledCertificateIds.cpp', 'v16'] = TICK df.at['Operations/GetInstalledCertificateIds.cpp', 'Module'] = MODULE_SMARTCHARGING df.at['Operations/GetLocalListVersion.cpp', 'v16'] = TICK df.at['Operations/GetLocalListVersion.cpp', 'Module'] = MODULE_LOCALAUTH if 'Operations/GetVariables.cpp' in df.index: df.at['Operations/GetVariables.cpp', 'v201'] = TICK df.at['Operations/GetVariables.cpp', 'Module'] = MODULE_PROVISIONING_VARS df.at['Operations/Heartbeat.cpp', 'v16'] = TICK df.at['Operations/Heartbeat.cpp', 'v201'] = TICK df.at['Operations/Heartbeat.cpp', 'Module'] = MODULE_AVAILABILITY df.at['Operations/InstallCertificate.cpp', 'v16'] = TICK df.at['Operations/InstallCertificate.cpp', 'v201'] = TICK df.at['Operations/InstallCertificate.cpp', 'Module'] = MODULE_CERTS df.at['Operations/MeterValues.cpp', 'v16'] = TICK df.at['Operations/MeterValues.cpp', 'Module'] = MODULE_METERVALUES if 'Operations/NotifyReport.cpp' in df.index: df.at['Operations/NotifyReport.cpp', 'v201'] = TICK df.at['Operations/NotifyReport.cpp', 'Module'] = MODULE_PROVISIONING_VARS df.at['Operations/RemoteStartTransaction.cpp', 'v16'] = TICK df.at['Operations/RemoteStartTransaction.cpp', 'Module'] = MODULE_TX df.at['Operations/RemoteStopTransaction.cpp', 'v16'] = TICK df.at['Operations/RemoteStopTransaction.cpp', 'Module'] = MODULE_TX if 'Operations/RequestStartTransaction.cpp' in df.index: df.at['Operations/RequestStartTransaction.cpp', 'v201'] = TICK df.at['Operations/RequestStartTransaction.cpp', 'Module'] = MODULE_TX if 'Operations/RequestStopTransaction.cpp' in df.index: df.at['Operations/RequestStopTransaction.cpp', 'v201'] = TICK df.at['Operations/RequestStopTransaction.cpp', 'Module'] = MODULE_TX df.at['Operations/ReserveNow.cpp', 'v16'] = TICK df.at['Operations/ReserveNow.cpp', 'Module'] = MODULE_RESERVATION df.at['Operations/Reset.cpp', 'v16'] = TICK df.at['Operations/Reset.cpp', 'v201'] = TICK df.at['Operations/Reset.cpp', 'Module'] = MODULE_PROVISIONING if 'Operations/SecurityEventNotification.cpp' in df.index: df.at['Operations/SecurityEventNotification.cpp', 'v201'] = TICK df.at['Operations/SecurityEventNotification.cpp', 'Module'] = MODULE_SECURITY df.at['Operations/SendLocalList.cpp', 'v16'] = TICK df.at['Operations/SendLocalList.cpp', 'Module'] = MODULE_LOCALAUTH df.at['Operations/SetChargingProfile.cpp', 'v16'] = TICK df.at['Operations/SetChargingProfile.cpp', 'Module'] = MODULE_SMARTCHARGING if 'Operations/SetVariables.cpp' in df.index: df.at['Operations/SetVariables.cpp', 'v201'] = TICK df.at['Operations/SetVariables.cpp', 'Module'] = MODULE_PROVISIONING_VARS df.at['Operations/StartTransaction.cpp', 'v16'] = TICK df.at['Operations/StartTransaction.cpp', 'Module'] = MODULE_TX df.at['Operations/StatusNotification.cpp', 'v16'] = TICK df.at['Operations/StatusNotification.cpp', 'v201'] = TICK df.at['Operations/StatusNotification.cpp', 'Module'] = MODULE_AVAILABILITY df.at['Operations/StopTransaction.cpp', 'v16'] = TICK df.at['Operations/StopTransaction.cpp', 'Module'] = MODULE_TX if 'Operations/TransactionEvent.cpp' in df.index: df.at['Operations/TransactionEvent.cpp', 'v201'] = TICK df.at['Operations/TransactionEvent.cpp', 'Module'] = MODULE_TX df.at['Operations/TriggerMessage.cpp', 'v16'] = TICK df.at['Operations/TriggerMessage.cpp', 'Module'] = MODULE_TRIGGERMESSAGE df.at['Operations/UnlockConnector.cpp', 'v16'] = TICK df.at['Operations/UnlockConnector.cpp', 'Module'] = MODULE_CORE df.at['Operations/UpdateFirmware.cpp', 'v16'] = TICK df.at['Operations/UpdateFirmware.cpp', 'Module'] = MODULE_FW_MNGT if 'MicroOcpp_c.cpp' in df.index: df.at['MicroOcpp_c.cpp', 'v16'] = TICK df.at['MicroOcpp_c.cpp', 'v201'] = TICK df.at['MicroOcpp_c.cpp', 'Module'] = MODULE_API print(df) categorize_table(cunits_v16) categorize_table(cunits_v201) categorize_success = True if cunits_v16[COLUMN_BINSIZE].isnull().any(): print('Error: categorized the following compilation units erroneously (v16):\n') print(cunits_v16.loc[cunits_v16[COLUMN_BINSIZE].isnull()]) categorize_success = False if cunits_v201[COLUMN_BINSIZE].isnull().any(): print('Error: categorized the following compilation units erroneously (v201):\n') print(cunits_v201.loc[cunits_v201[COLUMN_BINSIZE].isnull()]) categorize_success = False if (cunits_v16['Module'].values == '').sum() > 0: print('Error: did not categorize the following compilation units (v16):\n') print(cunits_v16.loc[cunits_v16['Module'].values == '']) categorize_success = False if (cunits_v201['Module'].values == '').sum() > 0: print('Error: did not categorize the following compilation units (v201):\n') print(cunits_v201.loc[cunits_v201['Module'].values == '']) categorize_success = False if not categorize_success: sys.exit('\nError categorizing compilation units') # store csv with all details print('Uncategorized compile units (v16): ', (cunits_v16['Module'].values == '').sum()) print('Uncategorized compile units (v201): ', (cunits_v201['Module'].values == '').sum()) cunits_v16.to_csv("docs/assets/tables/compile_units_v16.csv") cunits_v201.to_csv("docs/assets/tables/compile_units_v201.csv") # store csv with size by Module for v16 modules_v16 = cunits_v16.loc[cunits_v16['v16'].values == 'x'].sort_index() modules_v16_by_module = modules_v16[['Module', COLUMN_BINSIZE]].groupby('Module').sum() modules_v16_by_module.loc['**Total**'] = [modules_v16_by_module[COLUMN_BINSIZE].sum()] print(modules_v16_by_module) modules_v16_by_module.to_csv('docs/assets/tables/modules_v16.csv') # store csv with size by Module for v201 modules_v201 = cunits_v201.loc[cunits_v201['v201'].values == 'x'].sort_index() modules_v201_by_module = modules_v201[['Module', COLUMN_BINSIZE]].groupby('Module').sum() modules_v201_by_module.loc['**Total**'] = [modules_v201_by_module[COLUMN_BINSIZE].sum()] print(modules_v201_by_module) modules_v201_by_module.to_csv('docs/assets/tables/modules_v201.csv') ================================================ FILE: tests/benchmarks/scripts/measure_heap.py ================================================ import os import sys import requests import paramiko import base64 import traceback import io import json import time import pandas as pd requests.packages.urllib3.disable_warnings() # avoid the URL to be printed to console # Test case selection (commented out a few to simplify testing for now) testcase_name_list = [ 'TC_B_06_CS', 'TC_B_07_CS', 'TC_B_09_CS', 'TC_B_10_CS', 'TC_B_11_CS', 'TC_B_12_CS', 'TC_B_13_CS', 'TC_B_32_CS', 'TC_B_34_CS', 'TC_B_35_CS', 'TC_B_36_CS', 'TC_B_37_CS', 'TC_B_39_CS', 'TC_C_02_CS', 'TC_C_04_CS', 'TC_C_06_CS', 'TC_C_07_CS', 'TC_C_49_CS', 'TC_E_01_CS', 'TC_E_02_CS', 'TC_E_03_CS', 'TC_E_04_CS', 'TC_E_05_CS', 'TC_E_06_CS', 'TC_E_07_CS', 'TC_E_09_CS', 'TC_E_13_CS', 'TC_E_15_CS', 'TC_E_17_CS', 'TC_E_20_CS', 'TC_E_21_CS', 'TC_E_24_CS', 'TC_E_25_CS', 'TC_E_28_CS', 'TC_E_29_CS', 'TC_E_30_CS', 'TC_E_31_CS', 'TC_E_32_CS', 'TC_E_33_CS', 'TC_E_34_CS', 'TC_E_35_CS', 'TC_E_39_CS', 'TC_F_01_CS', 'TC_F_02_CS', 'TC_F_03_CS', 'TC_F_04_CS', 'TC_F_05_CS', 'TC_F_06_CS', 'TC_F_07_CS', 'TC_F_08_CS', 'TC_F_09_CS', 'TC_F_10_CS', 'TC_F_11_CS', 'TC_F_12_CS', 'TC_F_13_CS', 'TC_F_14_CS', 'TC_F_20_CS', 'TC_F_23_CS', 'TC_F_24_CS', 'TC_F_26_CS', 'TC_F_27_CS', 'TC_G_01_CS', 'TC_G_02_CS', 'TC_G_03_CS', 'TC_G_04_CS', 'TC_G_05_CS', 'TC_G_06_CS', 'TC_G_07_CS', 'TC_G_08_CS', 'TC_G_09_CS', 'TC_G_10_CS', 'TC_G_11_CS', 'TC_G_12_CS', 'TC_G_13_CS', 'TC_G_14_CS', 'TC_G_15_CS', 'TC_G_16_CS', 'TC_G_17_CS', 'TC_J_07_CS', 'TC_J_08_CS', 'TC_J_09_CS', 'TC_J_10_CS', ] # Result data set df = pd.DataFrame(columns=['FN_BLOCK', 'Testcase', 'Pass', 'Heap usage (Bytes)']) df.index.names = ['TC_ID'] max_memory_total = 0 min_memory_base = 1000 * 1000 * 1000 def connect_ssh(): if not os.path.isfile(os.path.join('tests', 'benchmarks', 'scripts', 'id_ed25519')): file = open(os.path.join('tests', 'benchmarks', 'scripts', 'id_ed25519'), 'w') file.write(os.environ['SSH_LOCAL_PRIV']) file.close() print('SSH ID written to file') client = paramiko.SSHClient() client.get_host_keys().add('cicd.micro-ocpp.com', 'ssh-ed25519', paramiko.pkey.PKey.from_type_string('ssh-ed25519', base64.b64decode(os.environ['SSH_HOST_PUB']))) client.connect('cicd.micro-ocpp.com', username='ocpp', key_filename=os.path.join('tests', 'benchmarks', 'scripts', 'id_ed25519'), look_for_keys=False) return client def close_ssh(client: paramiko.SSHClient): client.close() def deploy_simulator(): print('Deploy Simulator') client = connect_ssh() print(' - stop Simulator, if still running') stdin, stdout, stderr = client.exec_command('killall -s SIGINT mo_simulator') print(' - clean previous deployment') stdin, stdout, stderr = client.exec_command('rm -rf ' + os.path.join('MicroOcppSimulator')) print(' - init folder structure') sftp = client.open_sftp() sftp.mkdir(os.path.join('MicroOcppSimulator')) sftp.mkdir(os.path.join('MicroOcppSimulator', 'build')) sftp.mkdir(os.path.join('MicroOcppSimulator', 'public')) sftp.mkdir(os.path.join('MicroOcppSimulator', 'mo_store')) print(' - upload files') sftp.put( os.path.join('MicroOcppSimulator', 'build', 'mo_simulator'), os.path.join('MicroOcppSimulator', 'build', 'mo_simulator')) sftp.chmod(os.path.join('MicroOcppSimulator', 'build', 'mo_simulator'), 0O777) sftp.put( os.path.join('MicroOcppSimulator', 'public', 'bundle.html.gz'), os.path.join('MicroOcppSimulator', 'public', 'bundle.html.gz')) sftp.close() close_ssh(client) print(' - done') def cleanup_simulator(): print('Clean up Simulator') client = connect_ssh() print(' - stop Simulator, if still running') stdin, stdout, stderr = client.exec_command('killall -s SIGINT mo_simulator') print(' - clean deployment') stdin, stdout, stderr = client.exec_command('rm -rf ' + os.path.join('MicroOcppSimulator')) close_ssh(client) print(' - done') def setup_simulator(): print('Setup Simulator') client = connect_ssh() print(' - stop Simulator, if still running') stdin, stdout, stderr = client.exec_command('killall -s SIGINT mo_simulator') print(' - clean state') stdin, stdout, stderr = client.exec_command('rm -rf ' + os.path.join('MicroOcppSimulator', 'mo_store', '*')) print(' - upload credentials') sftp = client.open_sftp() sftp.putfo(io.StringIO(os.environ['MO_SIM_CONFIG']), os.path.join('MicroOcppSimulator', 'mo_store', 'simulator.jsn')) sftp.putfo(io.StringIO(os.environ['MO_SIM_OCPP_SERVER']),os.path.join('MicroOcppSimulator', 'mo_store', 'ws-conn-v201.jsn')) sftp.putfo(io.StringIO(os.environ['MO_SIM_API_CERT']), os.path.join('MicroOcppSimulator', 'mo_store', 'api_cert.pem')) sftp.putfo(io.StringIO(os.environ['MO_SIM_API_KEY']), os.path.join('MicroOcppSimulator', 'mo_store', 'api_key.pem')) sftp.putfo(io.StringIO(os.environ['MO_SIM_API_CONFIG']), os.path.join('MicroOcppSimulator', 'mo_store', 'api.jsn')) sftp.close() print(' - start Simulator') stdin, stdout, stderr = client.exec_command('mkdir -p logs && cd ' + os.path.join('MicroOcppSimulator') + ' && ./build/mo_simulator > ~/logs/sim_"$(date +%Y-%m-%d_%H-%M-%S.log)"') close_ssh(client) print(' - done') def run_measurements(): global max_memory_total global min_memory_base print("Fetch TCs from Test Driver") response = requests.get(os.environ['TEST_DRIVER_URL'] + '/ocpp2.0.1/CS/testcases/' + os.environ['TEST_DRIVER_CONFIG'], headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}, verify=False) #print(json.dumps(response.json(), indent=4)) testcases = [] for i in response.json()['data']['testcasesData']: for j in i['data']: is_core = False for k in j['certification_profiles']: if k == 'Core': is_core = True break select = False for k in testcase_name_list: if j['testcase_name'] == k: select = True break if select: print(i['header'] + ' --- ' + j['functional_block'] + ' --- ' + j['description']) testcases.append(j) deploy_simulator() print('Get Simulator base memory data') setup_simulator() response = requests.post('https://cicd.micro-ocpp.com:8443/api/memory/reset', auth=(json.loads(os.environ['MO_SIM_API_CONFIG'])['user'], json.loads(os.environ['MO_SIM_API_CONFIG'])['pass'])) print(f'Simulator API /memory/reset:\n > {response.status_code}') response = requests.get('https://cicd.micro-ocpp.com:8443/api/memory/info', auth=(json.loads(os.environ['MO_SIM_API_CONFIG'])['user'], json.loads(os.environ['MO_SIM_API_CONFIG'])['pass'])) print(f'Simulator API /memory/info:\n > {response.status_code}, current heap={response.json()["total_current"]}, max heap={response.json()["total_max"]}') base_memory_level = response.json()["total_max"] min_memory_base = min(min_memory_base, response.json()["total_max"]) print("Start Test Driver") response = requests.post(os.environ['TEST_DRIVER_URL'] + '/ocpp2.0.1/CS/session/start/' + os.environ['TEST_DRIVER_CONFIG'], headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}, verify=False) print(f'Test Driver /*/*/session/start/*:\n > {response.status_code}') #print(json.dumps(response.json(), indent=4)) for testcase in testcases: print('\nRun ' + testcase['functional_block'] + ' > ' + testcase['description'] + ' (' + testcase['testcase_name'] + ')') if testcase['testcase_name'] in df.index: print('Test case already executed - skip') continue setup_simulator() time.sleep(1) # Check connection simulator_connected = False for i in range(5): response = requests.get(os.environ['TEST_DRIVER_URL'] + '/sut_connection_status', headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}, verify=False) print(f'Test Driver /sut_connection_status:\n > {response.status_code}') #print(json.dumps(response.json(), indent=4)) if response.status_code == 200: simulator_connected = True break else: print(f'Waiting for the Simulator to connect ({i}) ...') time.sleep(3) if not simulator_connected: print('Simulator could not connect to Test Driver') raise Exception() response = requests.post('https://cicd.micro-ocpp.com:8443/api/memory/reset', auth=(json.loads(os.environ['MO_SIM_API_CONFIG'])['user'], json.loads(os.environ['MO_SIM_API_CONFIG'])['pass'])) print(f'Simulator API /memory/reset:\n > {response.status_code}') test_response = requests.post(os.environ['TEST_DRIVER_URL'] + '/testcases/' + testcase['testcase_name'] + '/execute', headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}, verify=False) print(f'Test Driver /testcases/{testcase["testcase_name"]}/execute:\n > {test_response.status_code}') #try: # print(json.dumps(test_response.json(), indent=4)) #except: # print(' > No JSON') sim_response = requests.get('https://cicd.micro-ocpp.com:8443/api/memory/info', auth=(json.loads(os.environ['MO_SIM_API_CONFIG'])['user'], json.loads(os.environ['MO_SIM_API_CONFIG'])['pass'])) print(f'Simulator API /memory/info:\n > {sim_response.status_code}, current heap={sim_response.json()["total_current"]}, max heap={sim_response.json()["total_max"]}') df.loc[testcase['testcase_name']] = [testcase['functional_block'], testcase['description'], 'x' if test_response.status_code == 200 and test_response.json()['data'][0]['verdict'] == "pass" else '-', str(sim_response.json()["total_max"] - min(base_memory_level, sim_response.json()["total_current"]))] max_memory_total = max(max_memory_total, sim_response.json()["total_max"]) min_memory_base = min(min_memory_base, sim_response.json()["total_current"]) print("Stop Test Driver") response = requests.post(os.environ['TEST_DRIVER_URL'] + '/session/stop', headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}, verify=False) print(f'Test Driver /session/stop:\n > {response.status_code}') #print(json.dumps(response.json(), indent=4)) cleanup_simulator() print('Store test results') # Add some meta information max_memory = 0 for index, row in df.iterrows(): memory = row['Heap usage (Bytes)'] if memory.isdigit(): max_memory = max(max_memory, int(memory)) functional_blocks = set() for index, row in df.iterrows(): functional_blocks.add(row['FN_BLOCK']) print(functional_blocks) for i in functional_blocks: df.loc[f'TC_{i[0]}'] = [i, f'**{i}**', ' ', ' '] df.loc['|MO_SIM_000'] = ['-', '**Simulator stats**', ' ', ' '] df.loc['|MO_SIM_010'] = ['-', 'Base memory occupation', ' ', str(min_memory_base)] df.loc['|MO_SIM_020'] = ['-', 'Test case maximum', ' ', str(max_memory)] df.loc['|MO_SIM_030'] = ['-', 'Total memory maximum', ' ', str(max_memory_total)] df.sort_index(inplace=True) print(df) df.to_csv('docs/assets/tables/heap_v201.csv',index=False,columns=['Testcase','Pass','Heap usage (Bytes)']) print('Stored test results to CSV') def run_measurements_and_retry(): if ( 'TEST_DRIVER_URL' not in os.environ or 'TEST_DRIVER_CONFIG' not in os.environ or 'TEST_DRIVER_KEY' not in os.environ or 'MO_SIM_CONFIG' not in os.environ or 'MO_SIM_OCPP_SERVER' not in os.environ or 'MO_SIM_API_CERT' not in os.environ or 'MO_SIM_API_KEY' not in os.environ or 'MO_SIM_API_CONFIG' not in os.environ or 'SSH_LOCAL_PRIV' not in os.environ or 'SSH_HOST_PUB' not in os.environ): sys.exit('\nCould not read environment variables') n_tries = 3 for i in range(n_tries): try: run_measurements() print('\n **Test cases executed successfully**') break except: print(f'Error detected ({i+1})') traceback.print_exc() print("Stop Test Driver") response = requests.post(os.environ['TEST_DRIVER_URL'] + '/session/stop', headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}, verify=False) print(f'Test Driver /session/stop:\n > {response.status_code}') #print(json.dumps(response.json(), indent=4)) cleanup_simulator() if i + 1 < n_tries: print('Retry test cases') else: print('\n **Test case execution aborted**') sys.exit('\nError running test cases') run_measurements_and_retry() ================================================ FILE: tests/catch2/catch.hpp ================================================ /* * Catch v2.13.9 * Generated: 2022-04-12 22:37:23.260201 * ---------------------------------------------------------- * This file has been merged from multiple headers. Please don't edit it directly * Copyright (c) 2022 Two Blue Cubes Ltd. All rights reserved. * * Distributed under the Boost Software License, Version 1.0. (See accompanying * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) */ #ifndef TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED #define TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED // start catch.hpp #define CATCH_VERSION_MAJOR 2 #define CATCH_VERSION_MINOR 13 #define CATCH_VERSION_PATCH 9 #ifdef __clang__ # pragma clang system_header #elif defined __GNUC__ # pragma GCC system_header #endif // start catch_suppress_warnings.h #ifdef __clang__ # ifdef __ICC // icpc defines the __clang__ macro # pragma warning(push) # pragma warning(disable: 161 1682) # else // __ICC # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wpadded" # pragma clang diagnostic ignored "-Wswitch-enum" # pragma clang diagnostic ignored "-Wcovered-switch-default" # endif #elif defined __GNUC__ // Because REQUIREs trigger GCC's -Wparentheses, and because still // supported version of g++ have only buggy support for _Pragmas, // Wparentheses have to be suppressed globally. # pragma GCC diagnostic ignored "-Wparentheses" // See #674 for details # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-variable" # pragma GCC diagnostic ignored "-Wpadded" #endif // end catch_suppress_warnings.h #if defined(CATCH_CONFIG_MAIN) || defined(CATCH_CONFIG_RUNNER) # define CATCH_IMPL # define CATCH_CONFIG_ALL_PARTS #endif // In the impl file, we want to have access to all parts of the headers // Can also be used to sanely support PCHs #if defined(CATCH_CONFIG_ALL_PARTS) # define CATCH_CONFIG_EXTERNAL_INTERFACES # if defined(CATCH_CONFIG_DISABLE_MATCHERS) # undef CATCH_CONFIG_DISABLE_MATCHERS # endif # if !defined(CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER) # define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER # endif #endif #if !defined(CATCH_CONFIG_IMPL_ONLY) // start catch_platform.h // See e.g.: // https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-18.1/TargetConditionals.h.auto.html #ifdef __APPLE__ # include # if (defined(TARGET_OS_OSX) && TARGET_OS_OSX == 1) || \ (defined(TARGET_OS_MAC) && TARGET_OS_MAC == 1) # define CATCH_PLATFORM_MAC # elif (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) # define CATCH_PLATFORM_IPHONE # endif #elif defined(linux) || defined(__linux) || defined(__linux__) # define CATCH_PLATFORM_LINUX #elif defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER) || defined(__MINGW32__) # define CATCH_PLATFORM_WINDOWS #endif // end catch_platform.h #ifdef CATCH_IMPL # ifndef CLARA_CONFIG_MAIN # define CLARA_CONFIG_MAIN_NOT_DEFINED # define CLARA_CONFIG_MAIN # endif #endif // start catch_user_interfaces.h namespace Catch { unsigned int rngSeed(); } // end catch_user_interfaces.h // start catch_tag_alias_autoregistrar.h // start catch_common.h // start catch_compiler_capabilities.h // Detect a number of compiler features - by compiler // The following features are defined: // // CATCH_CONFIG_COUNTER : is the __COUNTER__ macro supported? // CATCH_CONFIG_WINDOWS_SEH : is Windows SEH supported? // CATCH_CONFIG_POSIX_SIGNALS : are POSIX signals supported? // CATCH_CONFIG_DISABLE_EXCEPTIONS : Are exceptions enabled? // **************** // Note to maintainers: if new toggles are added please document them // in configuration.md, too // **************** // In general each macro has a _NO_ form // (e.g. CATCH_CONFIG_NO_POSIX_SIGNALS) which disables the feature. // Many features, at point of detection, define an _INTERNAL_ macro, so they // can be combined, en-mass, with the _NO_ forms later. #ifdef __cplusplus # if (__cplusplus >= 201402L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201402L) # define CATCH_CPP14_OR_GREATER # endif # if (__cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) # define CATCH_CPP17_OR_GREATER # endif #endif // Only GCC compiler should be used in this block, so other compilers trying to // mask themselves as GCC should be ignored. #if defined(__GNUC__) && !defined(__clang__) && !defined(__ICC) && !defined(__CUDACC__) && !defined(__LCC__) # define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION _Pragma( "GCC diagnostic push" ) # define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION _Pragma( "GCC diagnostic pop" ) # define CATCH_INTERNAL_IGNORE_BUT_WARN(...) (void)__builtin_constant_p(__VA_ARGS__) #endif #if defined(__clang__) # define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION _Pragma( "clang diagnostic push" ) # define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION _Pragma( "clang diagnostic pop" ) // As of this writing, IBM XL's implementation of __builtin_constant_p has a bug // which results in calls to destructors being emitted for each temporary, // without a matching initialization. In practice, this can result in something // like `std::string::~string` being called on an uninitialized value. // // For example, this code will likely segfault under IBM XL: // ``` // REQUIRE(std::string("12") + "34" == "1234") // ``` // // Therefore, `CATCH_INTERNAL_IGNORE_BUT_WARN` is not implemented. # if !defined(__ibmxl__) && !defined(__CUDACC__) # define CATCH_INTERNAL_IGNORE_BUT_WARN(...) (void)__builtin_constant_p(__VA_ARGS__) /* NOLINT(cppcoreguidelines-pro-type-vararg, hicpp-vararg) */ # endif # define CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS \ _Pragma( "clang diagnostic ignored \"-Wexit-time-destructors\"" ) \ _Pragma( "clang diagnostic ignored \"-Wglobal-constructors\"") # define CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS \ _Pragma( "clang diagnostic ignored \"-Wparentheses\"" ) # define CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS \ _Pragma( "clang diagnostic ignored \"-Wunused-variable\"" ) # define CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS \ _Pragma( "clang diagnostic ignored \"-Wgnu-zero-variadic-macro-arguments\"" ) # define CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS \ _Pragma( "clang diagnostic ignored \"-Wunused-template\"" ) #endif // __clang__ //////////////////////////////////////////////////////////////////////////////// // Assume that non-Windows platforms support posix signals by default #if !defined(CATCH_PLATFORM_WINDOWS) #define CATCH_INTERNAL_CONFIG_POSIX_SIGNALS #endif //////////////////////////////////////////////////////////////////////////////// // We know some environments not to support full POSIX signals #if defined(__CYGWIN__) || defined(__QNX__) || defined(__EMSCRIPTEN__) || defined(__DJGPP__) #define CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS #endif #ifdef __OS400__ # define CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS # define CATCH_CONFIG_COLOUR_NONE #endif //////////////////////////////////////////////////////////////////////////////// // Android somehow still does not support std::to_string #if defined(__ANDROID__) # define CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING # define CATCH_INTERNAL_CONFIG_ANDROID_LOGWRITE #endif //////////////////////////////////////////////////////////////////////////////// // Not all Windows environments support SEH properly #if defined(__MINGW32__) # define CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH #endif //////////////////////////////////////////////////////////////////////////////// // PS4 #if defined(__ORBIS__) # define CATCH_INTERNAL_CONFIG_NO_NEW_CAPTURE #endif //////////////////////////////////////////////////////////////////////////////// // Cygwin #ifdef __CYGWIN__ // Required for some versions of Cygwin to declare gettimeofday // see: http://stackoverflow.com/questions/36901803/gettimeofday-not-declared-in-this-scope-cygwin # define _BSD_SOURCE // some versions of cygwin (most) do not support std::to_string. Use the libstd check. // https://gcc.gnu.org/onlinedocs/gcc-4.8.2/libstdc++/api/a01053_source.html line 2812-2813 # if !((__cplusplus >= 201103L) && defined(_GLIBCXX_USE_C99) \ && !defined(_GLIBCXX_HAVE_BROKEN_VSWPRINTF)) # define CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING # endif #endif // __CYGWIN__ //////////////////////////////////////////////////////////////////////////////// // Visual C++ #if defined(_MSC_VER) // Universal Windows platform does not support SEH // Or console colours (or console at all...) # if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) # define CATCH_CONFIG_COLOUR_NONE # else # define CATCH_INTERNAL_CONFIG_WINDOWS_SEH # endif # if !defined(__clang__) // Handle Clang masquerading for msvc // MSVC traditional preprocessor needs some workaround for __VA_ARGS__ // _MSVC_TRADITIONAL == 0 means new conformant preprocessor // _MSVC_TRADITIONAL == 1 means old traditional non-conformant preprocessor # if !defined(_MSVC_TRADITIONAL) || (defined(_MSVC_TRADITIONAL) && _MSVC_TRADITIONAL) # define CATCH_INTERNAL_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR # endif // MSVC_TRADITIONAL // Only do this if we're not using clang on Windows, which uses `diagnostic push` & `diagnostic pop` # define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION __pragma( warning(push) ) # define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION __pragma( warning(pop) ) # endif // __clang__ #endif // _MSC_VER #if defined(_REENTRANT) || defined(_MSC_VER) // Enable async processing, as -pthread is specified or no additional linking is required # define CATCH_INTERNAL_CONFIG_USE_ASYNC #endif // _MSC_VER //////////////////////////////////////////////////////////////////////////////// // Check if we are compiled with -fno-exceptions or equivalent #if defined(__EXCEPTIONS) || defined(__cpp_exceptions) || defined(_CPPUNWIND) # define CATCH_INTERNAL_CONFIG_EXCEPTIONS_ENABLED #endif //////////////////////////////////////////////////////////////////////////////// // DJGPP #ifdef __DJGPP__ # define CATCH_INTERNAL_CONFIG_NO_WCHAR #endif // __DJGPP__ //////////////////////////////////////////////////////////////////////////////// // Embarcadero C++Build #if defined(__BORLANDC__) #define CATCH_INTERNAL_CONFIG_POLYFILL_ISNAN #endif //////////////////////////////////////////////////////////////////////////////// // Use of __COUNTER__ is suppressed during code analysis in // CLion/AppCode 2017.2.x and former, because __COUNTER__ is not properly // handled by it. // Otherwise all supported compilers support COUNTER macro, // but user still might want to turn it off #if ( !defined(__JETBRAINS_IDE__) || __JETBRAINS_IDE__ >= 20170300L ) #define CATCH_INTERNAL_CONFIG_COUNTER #endif //////////////////////////////////////////////////////////////////////////////// // RTX is a special version of Windows that is real time. // This means that it is detected as Windows, but does not provide // the same set of capabilities as real Windows does. #if defined(UNDER_RTSS) || defined(RTX64_BUILD) #define CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH #define CATCH_INTERNAL_CONFIG_NO_ASYNC #define CATCH_CONFIG_COLOUR_NONE #endif #if !defined(_GLIBCXX_USE_C99_MATH_TR1) #define CATCH_INTERNAL_CONFIG_GLOBAL_NEXTAFTER #endif // Various stdlib support checks that require __has_include #if defined(__has_include) // Check if string_view is available and usable #if __has_include() && defined(CATCH_CPP17_OR_GREATER) # define CATCH_INTERNAL_CONFIG_CPP17_STRING_VIEW #endif // Check if optional is available and usable # if __has_include() && defined(CATCH_CPP17_OR_GREATER) # define CATCH_INTERNAL_CONFIG_CPP17_OPTIONAL # endif // __has_include() && defined(CATCH_CPP17_OR_GREATER) // Check if byte is available and usable # if __has_include() && defined(CATCH_CPP17_OR_GREATER) # include # if defined(__cpp_lib_byte) && (__cpp_lib_byte > 0) # define CATCH_INTERNAL_CONFIG_CPP17_BYTE # endif # endif // __has_include() && defined(CATCH_CPP17_OR_GREATER) // Check if variant is available and usable # if __has_include() && defined(CATCH_CPP17_OR_GREATER) # if defined(__clang__) && (__clang_major__ < 8) // work around clang bug with libstdc++ https://bugs.llvm.org/show_bug.cgi?id=31852 // fix should be in clang 8, workaround in libstdc++ 8.2 # include # if defined(__GLIBCXX__) && defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE < 9) # define CATCH_CONFIG_NO_CPP17_VARIANT # else # define CATCH_INTERNAL_CONFIG_CPP17_VARIANT # endif // defined(__GLIBCXX__) && defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE < 9) # else # define CATCH_INTERNAL_CONFIG_CPP17_VARIANT # endif // defined(__clang__) && (__clang_major__ < 8) # endif // __has_include() && defined(CATCH_CPP17_OR_GREATER) #endif // defined(__has_include) #if defined(CATCH_INTERNAL_CONFIG_COUNTER) && !defined(CATCH_CONFIG_NO_COUNTER) && !defined(CATCH_CONFIG_COUNTER) # define CATCH_CONFIG_COUNTER #endif #if defined(CATCH_INTERNAL_CONFIG_WINDOWS_SEH) && !defined(CATCH_CONFIG_NO_WINDOWS_SEH) && !defined(CATCH_CONFIG_WINDOWS_SEH) && !defined(CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH) # define CATCH_CONFIG_WINDOWS_SEH #endif // This is set by default, because we assume that unix compilers are posix-signal-compatible by default. #if defined(CATCH_INTERNAL_CONFIG_POSIX_SIGNALS) && !defined(CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS) && !defined(CATCH_CONFIG_NO_POSIX_SIGNALS) && !defined(CATCH_CONFIG_POSIX_SIGNALS) # define CATCH_CONFIG_POSIX_SIGNALS #endif // This is set by default, because we assume that compilers with no wchar_t support are just rare exceptions. #if !defined(CATCH_INTERNAL_CONFIG_NO_WCHAR) && !defined(CATCH_CONFIG_NO_WCHAR) && !defined(CATCH_CONFIG_WCHAR) # define CATCH_CONFIG_WCHAR #endif #if !defined(CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING) && !defined(CATCH_CONFIG_NO_CPP11_TO_STRING) && !defined(CATCH_CONFIG_CPP11_TO_STRING) # define CATCH_CONFIG_CPP11_TO_STRING #endif #if defined(CATCH_INTERNAL_CONFIG_CPP17_OPTIONAL) && !defined(CATCH_CONFIG_NO_CPP17_OPTIONAL) && !defined(CATCH_CONFIG_CPP17_OPTIONAL) # define CATCH_CONFIG_CPP17_OPTIONAL #endif #if defined(CATCH_INTERNAL_CONFIG_CPP17_STRING_VIEW) && !defined(CATCH_CONFIG_NO_CPP17_STRING_VIEW) && !defined(CATCH_CONFIG_CPP17_STRING_VIEW) # define CATCH_CONFIG_CPP17_STRING_VIEW #endif #if defined(CATCH_INTERNAL_CONFIG_CPP17_VARIANT) && !defined(CATCH_CONFIG_NO_CPP17_VARIANT) && !defined(CATCH_CONFIG_CPP17_VARIANT) # define CATCH_CONFIG_CPP17_VARIANT #endif #if defined(CATCH_INTERNAL_CONFIG_CPP17_BYTE) && !defined(CATCH_CONFIG_NO_CPP17_BYTE) && !defined(CATCH_CONFIG_CPP17_BYTE) # define CATCH_CONFIG_CPP17_BYTE #endif #if defined(CATCH_CONFIG_EXPERIMENTAL_REDIRECT) # define CATCH_INTERNAL_CONFIG_NEW_CAPTURE #endif #if defined(CATCH_INTERNAL_CONFIG_NEW_CAPTURE) && !defined(CATCH_INTERNAL_CONFIG_NO_NEW_CAPTURE) && !defined(CATCH_CONFIG_NO_NEW_CAPTURE) && !defined(CATCH_CONFIG_NEW_CAPTURE) # define CATCH_CONFIG_NEW_CAPTURE #endif #if !defined(CATCH_INTERNAL_CONFIG_EXCEPTIONS_ENABLED) && !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS) # define CATCH_CONFIG_DISABLE_EXCEPTIONS #endif #if defined(CATCH_INTERNAL_CONFIG_POLYFILL_ISNAN) && !defined(CATCH_CONFIG_NO_POLYFILL_ISNAN) && !defined(CATCH_CONFIG_POLYFILL_ISNAN) # define CATCH_CONFIG_POLYFILL_ISNAN #endif #if defined(CATCH_INTERNAL_CONFIG_USE_ASYNC) && !defined(CATCH_INTERNAL_CONFIG_NO_ASYNC) && !defined(CATCH_CONFIG_NO_USE_ASYNC) && !defined(CATCH_CONFIG_USE_ASYNC) # define CATCH_CONFIG_USE_ASYNC #endif #if defined(CATCH_INTERNAL_CONFIG_ANDROID_LOGWRITE) && !defined(CATCH_CONFIG_NO_ANDROID_LOGWRITE) && !defined(CATCH_CONFIG_ANDROID_LOGWRITE) # define CATCH_CONFIG_ANDROID_LOGWRITE #endif #if defined(CATCH_INTERNAL_CONFIG_GLOBAL_NEXTAFTER) && !defined(CATCH_CONFIG_NO_GLOBAL_NEXTAFTER) && !defined(CATCH_CONFIG_GLOBAL_NEXTAFTER) # define CATCH_CONFIG_GLOBAL_NEXTAFTER #endif // Even if we do not think the compiler has that warning, we still have // to provide a macro that can be used by the code. #if !defined(CATCH_INTERNAL_START_WARNINGS_SUPPRESSION) # define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION #endif #if !defined(CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION) # define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION #endif #if !defined(CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS) # define CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS #endif #if !defined(CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS) # define CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS #endif #if !defined(CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS) # define CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS #endif #if !defined(CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS) # define CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS #endif // The goal of this macro is to avoid evaluation of the arguments, but // still have the compiler warn on problems inside... #if !defined(CATCH_INTERNAL_IGNORE_BUT_WARN) # define CATCH_INTERNAL_IGNORE_BUT_WARN(...) #endif #if defined(__APPLE__) && defined(__apple_build_version__) && (__clang_major__ < 10) # undef CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS #elif defined(__clang__) && (__clang_major__ < 5) # undef CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS #endif #if !defined(CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS) # define CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS #endif #if defined(CATCH_CONFIG_DISABLE_EXCEPTIONS) #define CATCH_TRY if ((true)) #define CATCH_CATCH_ALL if ((false)) #define CATCH_CATCH_ANON(type) if ((false)) #else #define CATCH_TRY try #define CATCH_CATCH_ALL catch (...) #define CATCH_CATCH_ANON(type) catch (type) #endif #if defined(CATCH_INTERNAL_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR) && !defined(CATCH_CONFIG_NO_TRADITIONAL_MSVC_PREPROCESSOR) && !defined(CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR) #define CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR #endif // end catch_compiler_capabilities.h #define INTERNAL_CATCH_UNIQUE_NAME_LINE2( name, line ) name##line #define INTERNAL_CATCH_UNIQUE_NAME_LINE( name, line ) INTERNAL_CATCH_UNIQUE_NAME_LINE2( name, line ) #ifdef CATCH_CONFIG_COUNTER # define INTERNAL_CATCH_UNIQUE_NAME( name ) INTERNAL_CATCH_UNIQUE_NAME_LINE( name, __COUNTER__ ) #else # define INTERNAL_CATCH_UNIQUE_NAME( name ) INTERNAL_CATCH_UNIQUE_NAME_LINE( name, __LINE__ ) #endif #include #include #include // We need a dummy global operator<< so we can bring it into Catch namespace later struct Catch_global_namespace_dummy {}; std::ostream& operator<<(std::ostream&, Catch_global_namespace_dummy); namespace Catch { struct CaseSensitive { enum Choice { Yes, No }; }; class NonCopyable { NonCopyable( NonCopyable const& ) = delete; NonCopyable( NonCopyable && ) = delete; NonCopyable& operator = ( NonCopyable const& ) = delete; NonCopyable& operator = ( NonCopyable && ) = delete; protected: NonCopyable(); virtual ~NonCopyable(); }; struct SourceLineInfo { SourceLineInfo() = delete; SourceLineInfo( char const* _file, std::size_t _line ) noexcept : file( _file ), line( _line ) {} SourceLineInfo( SourceLineInfo const& other ) = default; SourceLineInfo& operator = ( SourceLineInfo const& ) = default; SourceLineInfo( SourceLineInfo&& ) noexcept = default; SourceLineInfo& operator = ( SourceLineInfo&& ) noexcept = default; bool empty() const noexcept { return file[0] == '\0'; } bool operator == ( SourceLineInfo const& other ) const noexcept; bool operator < ( SourceLineInfo const& other ) const noexcept; char const* file; std::size_t line; }; std::ostream& operator << ( std::ostream& os, SourceLineInfo const& info ); // Bring in operator<< from global namespace into Catch namespace // This is necessary because the overload of operator<< above makes // lookup stop at namespace Catch using ::operator<<; // Use this in variadic streaming macros to allow // >> +StreamEndStop // as well as // >> stuff +StreamEndStop struct StreamEndStop { std::string operator+() const; }; template T const& operator + ( T const& value, StreamEndStop ) { return value; } } #define CATCH_INTERNAL_LINEINFO \ ::Catch::SourceLineInfo( __FILE__, static_cast( __LINE__ ) ) // end catch_common.h namespace Catch { struct RegistrarForTagAliases { RegistrarForTagAliases( char const* alias, char const* tag, SourceLineInfo const& lineInfo ); }; } // end namespace Catch #define CATCH_REGISTER_TAG_ALIAS( alias, spec ) \ CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS \ namespace{ Catch::RegistrarForTagAliases INTERNAL_CATCH_UNIQUE_NAME( AutoRegisterTagAlias )( alias, spec, CATCH_INTERNAL_LINEINFO ); } \ CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION // end catch_tag_alias_autoregistrar.h // start catch_test_registry.h // start catch_interfaces_testcase.h #include namespace Catch { class TestSpec; struct ITestInvoker { virtual void invoke () const = 0; virtual ~ITestInvoker(); }; class TestCase; struct IConfig; struct ITestCaseRegistry { virtual ~ITestCaseRegistry(); virtual std::vector const& getAllTests() const = 0; virtual std::vector const& getAllTestsSorted( IConfig const& config ) const = 0; }; bool isThrowSafe( TestCase const& testCase, IConfig const& config ); bool matchTest( TestCase const& testCase, TestSpec const& testSpec, IConfig const& config ); std::vector filterTests( std::vector const& testCases, TestSpec const& testSpec, IConfig const& config ); std::vector const& getAllTestCasesSorted( IConfig const& config ); } // end catch_interfaces_testcase.h // start catch_stringref.h #include #include #include #include namespace Catch { /// A non-owning string class (similar to the forthcoming std::string_view) /// Note that, because a StringRef may be a substring of another string, /// it may not be null terminated. class StringRef { public: using size_type = std::size_t; using const_iterator = const char*; private: static constexpr char const* const s_empty = ""; char const* m_start = s_empty; size_type m_size = 0; public: // construction constexpr StringRef() noexcept = default; StringRef( char const* rawChars ) noexcept; constexpr StringRef( char const* rawChars, size_type size ) noexcept : m_start( rawChars ), m_size( size ) {} StringRef( std::string const& stdString ) noexcept : m_start( stdString.c_str() ), m_size( stdString.size() ) {} explicit operator std::string() const { return std::string(m_start, m_size); } public: // operators auto operator == ( StringRef const& other ) const noexcept -> bool; auto operator != (StringRef const& other) const noexcept -> bool { return !(*this == other); } auto operator[] ( size_type index ) const noexcept -> char { assert(index < m_size); return m_start[index]; } public: // named queries constexpr auto empty() const noexcept -> bool { return m_size == 0; } constexpr auto size() const noexcept -> size_type { return m_size; } // Returns the current start pointer. If the StringRef is not // null-terminated, throws std::domain_exception auto c_str() const -> char const*; public: // substrings and searches // Returns a substring of [start, start + length). // If start + length > size(), then the substring is [start, size()). // If start > size(), then the substring is empty. auto substr( size_type start, size_type length ) const noexcept -> StringRef; // Returns the current start pointer. May not be null-terminated. auto data() const noexcept -> char const*; constexpr auto isNullTerminated() const noexcept -> bool { return m_start[m_size] == '\0'; } public: // iterators constexpr const_iterator begin() const { return m_start; } constexpr const_iterator end() const { return m_start + m_size; } }; auto operator += ( std::string& lhs, StringRef const& sr ) -> std::string&; auto operator << ( std::ostream& os, StringRef const& sr ) -> std::ostream&; constexpr auto operator "" _sr( char const* rawChars, std::size_t size ) noexcept -> StringRef { return StringRef( rawChars, size ); } } // namespace Catch constexpr auto operator "" _catch_sr( char const* rawChars, std::size_t size ) noexcept -> Catch::StringRef { return Catch::StringRef( rawChars, size ); } // end catch_stringref.h // start catch_preprocessor.hpp #define CATCH_RECURSION_LEVEL0(...) __VA_ARGS__ #define CATCH_RECURSION_LEVEL1(...) CATCH_RECURSION_LEVEL0(CATCH_RECURSION_LEVEL0(CATCH_RECURSION_LEVEL0(__VA_ARGS__))) #define CATCH_RECURSION_LEVEL2(...) CATCH_RECURSION_LEVEL1(CATCH_RECURSION_LEVEL1(CATCH_RECURSION_LEVEL1(__VA_ARGS__))) #define CATCH_RECURSION_LEVEL3(...) CATCH_RECURSION_LEVEL2(CATCH_RECURSION_LEVEL2(CATCH_RECURSION_LEVEL2(__VA_ARGS__))) #define CATCH_RECURSION_LEVEL4(...) CATCH_RECURSION_LEVEL3(CATCH_RECURSION_LEVEL3(CATCH_RECURSION_LEVEL3(__VA_ARGS__))) #define CATCH_RECURSION_LEVEL5(...) CATCH_RECURSION_LEVEL4(CATCH_RECURSION_LEVEL4(CATCH_RECURSION_LEVEL4(__VA_ARGS__))) #ifdef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR #define INTERNAL_CATCH_EXPAND_VARGS(...) __VA_ARGS__ // MSVC needs more evaluations #define CATCH_RECURSION_LEVEL6(...) CATCH_RECURSION_LEVEL5(CATCH_RECURSION_LEVEL5(CATCH_RECURSION_LEVEL5(__VA_ARGS__))) #define CATCH_RECURSE(...) CATCH_RECURSION_LEVEL6(CATCH_RECURSION_LEVEL6(__VA_ARGS__)) #else #define CATCH_RECURSE(...) CATCH_RECURSION_LEVEL5(__VA_ARGS__) #endif #define CATCH_REC_END(...) #define CATCH_REC_OUT #define CATCH_EMPTY() #define CATCH_DEFER(id) id CATCH_EMPTY() #define CATCH_REC_GET_END2() 0, CATCH_REC_END #define CATCH_REC_GET_END1(...) CATCH_REC_GET_END2 #define CATCH_REC_GET_END(...) CATCH_REC_GET_END1 #define CATCH_REC_NEXT0(test, next, ...) next CATCH_REC_OUT #define CATCH_REC_NEXT1(test, next) CATCH_DEFER ( CATCH_REC_NEXT0 ) ( test, next, 0) #define CATCH_REC_NEXT(test, next) CATCH_REC_NEXT1(CATCH_REC_GET_END test, next) #define CATCH_REC_LIST0(f, x, peek, ...) , f(x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST1) ) ( f, peek, __VA_ARGS__ ) #define CATCH_REC_LIST1(f, x, peek, ...) , f(x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST0) ) ( f, peek, __VA_ARGS__ ) #define CATCH_REC_LIST2(f, x, peek, ...) f(x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST1) ) ( f, peek, __VA_ARGS__ ) #define CATCH_REC_LIST0_UD(f, userdata, x, peek, ...) , f(userdata, x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST1_UD) ) ( f, userdata, peek, __VA_ARGS__ ) #define CATCH_REC_LIST1_UD(f, userdata, x, peek, ...) , f(userdata, x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST0_UD) ) ( f, userdata, peek, __VA_ARGS__ ) #define CATCH_REC_LIST2_UD(f, userdata, x, peek, ...) f(userdata, x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST1_UD) ) ( f, userdata, peek, __VA_ARGS__ ) // Applies the function macro `f` to each of the remaining parameters, inserts commas between the results, // and passes userdata as the first parameter to each invocation, // e.g. CATCH_REC_LIST_UD(f, x, a, b, c) evaluates to f(x, a), f(x, b), f(x, c) #define CATCH_REC_LIST_UD(f, userdata, ...) CATCH_RECURSE(CATCH_REC_LIST2_UD(f, userdata, __VA_ARGS__, ()()(), ()()(), ()()(), 0)) #define CATCH_REC_LIST(f, ...) CATCH_RECURSE(CATCH_REC_LIST2(f, __VA_ARGS__, ()()(), ()()(), ()()(), 0)) #define INTERNAL_CATCH_EXPAND1(param) INTERNAL_CATCH_EXPAND2(param) #define INTERNAL_CATCH_EXPAND2(...) INTERNAL_CATCH_NO## __VA_ARGS__ #define INTERNAL_CATCH_DEF(...) INTERNAL_CATCH_DEF __VA_ARGS__ #define INTERNAL_CATCH_NOINTERNAL_CATCH_DEF #define INTERNAL_CATCH_STRINGIZE(...) INTERNAL_CATCH_STRINGIZE2(__VA_ARGS__) #ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR #define INTERNAL_CATCH_STRINGIZE2(...) #__VA_ARGS__ #define INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS(param) INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_REMOVE_PARENS(param)) #else // MSVC is adding extra space and needs another indirection to expand INTERNAL_CATCH_NOINTERNAL_CATCH_DEF #define INTERNAL_CATCH_STRINGIZE2(...) INTERNAL_CATCH_STRINGIZE3(__VA_ARGS__) #define INTERNAL_CATCH_STRINGIZE3(...) #__VA_ARGS__ #define INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS(param) (INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_REMOVE_PARENS(param)) + 1) #endif #define INTERNAL_CATCH_MAKE_NAMESPACE2(...) ns_##__VA_ARGS__ #define INTERNAL_CATCH_MAKE_NAMESPACE(name) INTERNAL_CATCH_MAKE_NAMESPACE2(name) #define INTERNAL_CATCH_REMOVE_PARENS(...) INTERNAL_CATCH_EXPAND1(INTERNAL_CATCH_DEF __VA_ARGS__) #ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR #define INTERNAL_CATCH_MAKE_TYPE_LIST2(...) decltype(get_wrapper()) #define INTERNAL_CATCH_MAKE_TYPE_LIST(...) INTERNAL_CATCH_MAKE_TYPE_LIST2(INTERNAL_CATCH_REMOVE_PARENS(__VA_ARGS__)) #else #define INTERNAL_CATCH_MAKE_TYPE_LIST2(...) INTERNAL_CATCH_EXPAND_VARGS(decltype(get_wrapper())) #define INTERNAL_CATCH_MAKE_TYPE_LIST(...) INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_MAKE_TYPE_LIST2(INTERNAL_CATCH_REMOVE_PARENS(__VA_ARGS__))) #endif #define INTERNAL_CATCH_MAKE_TYPE_LISTS_FROM_TYPES(...)\ CATCH_REC_LIST(INTERNAL_CATCH_MAKE_TYPE_LIST,__VA_ARGS__) #define INTERNAL_CATCH_REMOVE_PARENS_1_ARG(_0) INTERNAL_CATCH_REMOVE_PARENS(_0) #define INTERNAL_CATCH_REMOVE_PARENS_2_ARG(_0, _1) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_1_ARG(_1) #define INTERNAL_CATCH_REMOVE_PARENS_3_ARG(_0, _1, _2) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_2_ARG(_1, _2) #define INTERNAL_CATCH_REMOVE_PARENS_4_ARG(_0, _1, _2, _3) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_3_ARG(_1, _2, _3) #define INTERNAL_CATCH_REMOVE_PARENS_5_ARG(_0, _1, _2, _3, _4) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_4_ARG(_1, _2, _3, _4) #define INTERNAL_CATCH_REMOVE_PARENS_6_ARG(_0, _1, _2, _3, _4, _5) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_5_ARG(_1, _2, _3, _4, _5) #define INTERNAL_CATCH_REMOVE_PARENS_7_ARG(_0, _1, _2, _3, _4, _5, _6) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_6_ARG(_1, _2, _3, _4, _5, _6) #define INTERNAL_CATCH_REMOVE_PARENS_8_ARG(_0, _1, _2, _3, _4, _5, _6, _7) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_7_ARG(_1, _2, _3, _4, _5, _6, _7) #define INTERNAL_CATCH_REMOVE_PARENS_9_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_8_ARG(_1, _2, _3, _4, _5, _6, _7, _8) #define INTERNAL_CATCH_REMOVE_PARENS_10_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_9_ARG(_1, _2, _3, _4, _5, _6, _7, _8, _9) #define INTERNAL_CATCH_REMOVE_PARENS_11_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_10_ARG(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10) #define INTERNAL_CATCH_VA_NARGS_IMPL(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N #define INTERNAL_CATCH_TYPE_GEN\ template struct TypeList {};\ template\ constexpr auto get_wrapper() noexcept -> TypeList { return {}; }\ template class...> struct TemplateTypeList{};\ template class...Cs>\ constexpr auto get_wrapper() noexcept -> TemplateTypeList { return {}; }\ template\ struct append;\ template\ struct rewrap;\ template class, typename...>\ struct create;\ template class, typename>\ struct convert;\ \ template \ struct append { using type = T; };\ template< template class L1, typename...E1, template class L2, typename...E2, typename...Rest>\ struct append, L2, Rest...> { using type = typename append, Rest...>::type; };\ template< template class L1, typename...E1, typename...Rest>\ struct append, TypeList, Rest...> { using type = L1; };\ \ template< template class Container, template class List, typename...elems>\ struct rewrap, List> { using type = TypeList>; };\ template< template class Container, template class List, class...Elems, typename...Elements>\ struct rewrap, List, Elements...> { using type = typename append>, typename rewrap, Elements...>::type>::type; };\ \ template