diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 7fc6acd6..54440598 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -13,15 +13,15 @@ permissions: contents: write jobs: - build_simulator: - name: Build Simulator + measure_heap: + name: Heap measurements runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.x - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: key: ${{ github.ref }} path: .cache @@ -34,7 +34,7 @@ jobs: with: repository: matth-x/MicroOcppSimulator path: MicroOcppSimulator - ref: 2cb07cdbe53954a694a29336ab31eac2d2b48673 + ref: 741733550b801d29261f81c3b7fb74bfc0b6faec submodules: 'recursive' - name: Clean MicroOcpp submodule run: | @@ -47,32 +47,8 @@ jobs: 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 @@ -81,13 +57,12 @@ jobs: TEST_DRIVER_URL: ${{ secrets.TEST_DRIVER_URL }} TEST_DRIVER_CONFIG: ${{ secrets.TEST_DRIVER_CONFIG }} TEST_DRIVER_KEY: ${{ secrets.TEST_DRIVER_KEY }} + TEST_DRIVER_CERT: ${{ secrets.TEST_DRIVER_CERT }} 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 }} + MO_SIM_OCPP_CERT: ${{ secrets.MO_SIM_OCPP_CERT }} + MO_SIM_RMT_CTRL_CONFIG: ${{ secrets.MO_SIM_RMT_CTRL_CONFIG }} + MO_SIM_RMT_CTRL_CERT: ${{ secrets.MO_SIM_RMT_CTRL_CERT }} - name: Upload reports uses: actions/upload-artifact@v4 with: @@ -95,20 +70,23 @@ jobs: path: docs/assets/tables if-no-files-found: error - build_firmware_size: + build_firmware: name: Build firmware runs-on: ubuntu-latest + strategy: + matrix: + part: [v16, v201, v16_v201] steps: - uses: actions/checkout@v3 - name: Cache pip - uses: actions/cache@v2 + 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@v2 + uses: actions/cache@v4 with: path: ~/.platformio key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} @@ -119,35 +97,28 @@ jobs: 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: pio ci --lib="." --build-dir="${{ github.workspace }}/../build" --keep-build-dir --project-conf="./tests/benchmarks/firmware_size/platformio.ini" -e ${{ matrix.part }} ./tests/benchmarks/firmware_size/main.cpp + - name: Move firmware file # 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 + mv "${{ github.workspace }}/../build/.pio/build/${{ matrix.part }}/firmware.elf" "firmware/firmware_${{ matrix.part }}.elf" + - name: Upload firmware linker file uses: actions/upload-artifact@v4 with: - name: Firmware linker files + name: Firmware linker file ${{ matrix.part }} path: firmware if-no-files-found: error retention-days: 1 - evaluate_firmware: - needs: build_firmware_size - name: Static firmware analysis + build_bloaty: + name: Build Google bloaty runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: 3.x - - uses: actions/cache@v2 + - 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 @@ -165,16 +136,55 @@ jobs: run: | cmake -B tools/bloaty/build -G Ninja -S tools/bloaty cmake --build tools/bloaty/build -j 32 - - name: Get firmware linker files + - name: Upload bloaty executable + uses: actions/upload-artifact@v4 + with: + name: Google bloaty + path: tools/bloaty/build/bloaty + if-no-files-found: error + retention-days: 1 + + evaluate_firmware: + needs: [build_firmware, build_bloaty] + name: Static firmware analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/cache@v4 + with: + key: ${{ github.ref }} + path: .cache + - name: Set up Python + uses: actions/setup-python@v4 + - name: Install Python dependencies + run: pip install pandas + - name: Get firmware linker file + uses: actions/download-artifact@v4 + with: + name: Firmware linker file v16 + path: firmware + - name: Get firmware linker file + uses: actions/download-artifact@v4 + with: + name: Firmware linker file v201 + path: firmware + - name: Get firmware linker file uses: actions/download-artifact@v4 with: - name: Firmware linker files + name: Firmware linker file v16_v201 path: firmware + - name: Get bloaty executable + uses: actions/download-artifact@v4 + with: + name: Google bloaty + path: tools/bloaty/build - name: Run bloaty run: | mkdir -p docs/assets/tables + chmod +x tools/bloaty/build/bloaty 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 + tools/bloaty/build/bloaty firmware/firmware_v16_v201.elf -d compileunits --csv -n 0 > docs/assets/tables/bloaty_v16_v201.csv - name: Evaluate and create reports run: python tests/benchmarks/scripts/eval_firmware_size.py - name: Upload reports @@ -194,7 +204,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: 3.x - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: key: ${{ github.ref }} path: .cache diff --git a/.github/workflows/pio.yml b/.github/workflows/pio.yml index ec777853..90983518 100644 --- a/.github/workflows/pio.yml +++ b/.github/workflows/pio.yml @@ -18,18 +18,19 @@ jobs: strategy: matrix: example: [examples/ESP/main.cpp, examples/ESP-TLS/main.cpp] + pio_platform: [nodemcuv2, esp32-development-board] steps: - uses: actions/checkout@v2 - name: Cache pip - uses: actions/cache@v2 + 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@v2 + uses: actions/cache@v4 with: path: ~/.platformio key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} @@ -42,6 +43,6 @@ jobs: - name: Install library dependencies run: pio pkg install - name: Run PlatformIO - run: pio ci --lib="." --project-conf=platformio.ini ${{ matrix.dashboard-extra }} + run: pio ci --lib="." --project-conf=platformio.ini -e ${{ matrix.pio_platform }} ${{ matrix.dashboard-extra }} env: PLATFORMIO_CI_SRC: ${{ matrix.example }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 963c1d8b..6436d964 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ # matth-x/MicroOcpp -# Copyright Matthias Akstaller 2019 - 2024 +# Copyright Matthias Akstaller 2019 - 2025 # MIT License cmake_minimum_required(VERSION 3.15) @@ -7,103 +7,100 @@ 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/FilesystemAdapter.cpp + src/MicroOcpp/Core/PersistencyUtils.cpp src/MicroOcpp/Core/Request.cpp + src/MicroOcpp/Core/FilesystemUtils.cpp + src/MicroOcpp/Core/RequestQueue.cpp + src/MicroOcpp/Core/MessageService.cpp + src/MicroOcpp/Core/FtpMbedTLS.cpp + src/MicroOcpp/Core/UuidUtils.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/Context.cpp + src/MicroOcpp/Operations/GetVariables.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/UpdateFirmware.cpp src/MicroOcpp/Operations/MeterValues.cpp - src/MicroOcpp/Operations/NotifyReport.cpp + src/MicroOcpp/Operations/DataTransfer.cpp + src/MicroOcpp/Operations/GetCompositeSchedule.cpp src/MicroOcpp/Operations/RemoteStartTransaction.cpp + src/MicroOcpp/Operations/CustomOperation.cpp + src/MicroOcpp/Operations/TransactionEvent.cpp + src/MicroOcpp/Operations/ChangeAvailability.cpp src/MicroOcpp/Operations/RemoteStopTransaction.cpp + src/MicroOcpp/Operations/ReserveNow.cpp + src/MicroOcpp/Operations/CancelReservation.cpp + src/MicroOcpp/Operations/TriggerMessage.cpp + src/MicroOcpp/Operations/GetDiagnostics.cpp + src/MicroOcpp/Operations/GetLocalListVersion.cpp src/MicroOcpp/Operations/RequestStartTransaction.cpp + src/MicroOcpp/Operations/DeleteCertificate.cpp + src/MicroOcpp/Operations/LogStatusNotification.cpp + src/MicroOcpp/Operations/Heartbeat.cpp + src/MicroOcpp/Operations/GetLog.cpp + src/MicroOcpp/Operations/GetConfiguration.cpp + src/MicroOcpp/Operations/ClearCache.cpp + src/MicroOcpp/Operations/SetChargingProfile.cpp + src/MicroOcpp/Operations/Authorize.cpp + src/MicroOcpp/Operations/SecurityEventNotification.cpp src/MicroOcpp/Operations/RequestStopTransaction.cpp - src/MicroOcpp/Operations/ReserveNow.cpp + src/MicroOcpp/Operations/StatusNotification.cpp + src/MicroOcpp/Operations/StopTransaction.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/DiagnosticsStatusNotification.cpp + src/MicroOcpp/Operations/NotifyReport.cpp + src/MicroOcpp/Operations/GetBaseReport.cpp + src/MicroOcpp/Operations/ChangeConfiguration.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/BootNotification.cpp + src/MicroOcpp/Operations/SetVariables.cpp + src/MicroOcpp/Operations/ClearChargingProfile.cpp src/MicroOcpp/Operations/UnlockConnector.cpp - src/MicroOcpp/Operations/UpdateFirmware.cpp + src/MicroOcpp/Operations/InstallCertificate.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/Configuration/ConfigurationContainer.cpp + src/MicroOcpp/Model/Configuration/Configuration.cpp + src/MicroOcpp/Model/Configuration/ConfigurationService.cpp src/MicroOcpp/Model/Boot/BootService.cpp - src/MicroOcpp/Model/Certificates/Certificate.cpp - src/MicroOcpp/Model/Certificates/Certificate_c.cpp + src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp + src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp + src/MicroOcpp/Model/Reset/ResetService.cpp + src/MicroOcpp/Model/Reservation/ReservationService.cpp + src/MicroOcpp/Model/Reservation/Reservation.cpp + src/MicroOcpp/Model/SecurityEvent/SecurityEventService.cpp src/MicroOcpp/Model/Certificates/CertificateMbedTLS.cpp + src/MicroOcpp/Model/Certificates/Certificate.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/Transactions/TransactionStore.cpp + src/MicroOcpp/Model/Transactions/TransactionService201.cpp + src/MicroOcpp/Model/Transactions/TransactionService16.cpp + src/MicroOcpp/Model/Transactions/Transaction.cpp + src/MicroOcpp/Model/Variables/VariableContainer.cpp + src/MicroOcpp/Model/Variables/VariableService.cpp + src/MicroOcpp/Model/Variables/Variable.cpp + src/MicroOcpp/Model/Diagnostics/Diagnostics.cpp + src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp + src/MicroOcpp/Model/Model.cpp + src/MicroOcpp/Model/Availability/AvailabilityDefs.cpp + src/MicroOcpp/Model/Availability/AvailabilityService.cpp src/MicroOcpp/Model/Metering/MeteringService.cpp + src/MicroOcpp/Model/Metering/ReadingContext.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/Model/Authorization/AuthorizationService.cpp + src/MicroOcpp/Model/Authorization/AuthorizationData.cpp + src/MicroOcpp/Model/Authorization/AuthorizationList.cpp + src/MicroOcpp/Model/Authorization/IdToken.cpp + src/MicroOcpp/Platform.cpp src/MicroOcpp.cpp - src/MicroOcpp_c.cpp ) if(ESP_PLATFORM) @@ -138,23 +135,24 @@ target_compile_definitions(MicroOcpp PUBLIC set(MO_SRC_UNIT tests/helpers/testHelper.cpp tests/ocppEngineLifecycle.cpp + tests/Core.cpp tests/TransactionSafety.cpp tests/ChargingSessions.cpp - tests/ConfigurationBehavior.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/Certificates.cpp - tests/FirmwareManagement.cpp - tests/ChargePointError.cpp - tests/Boot.cpp - tests/Security.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/Certificates.cpp + #tests/FirmwareManagement.cpp + #tests/ChargePointError.cpp + #tests/Boot.cpp + #tests/Security.cpp ) add_executable(mo_unit_tests @@ -184,14 +182,15 @@ target_include_directories(mo_unit_tests PUBLIC target_compile_definitions(mo_unit_tests PUBLIC MO_PLATFORM=MO_PLATFORM_UNIX - MO_NUMCONNECTORS=3 + MO_NUM_EVSEID=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_ENABLE_FILE_INDEX=0 + MO_ENABLE_TIMESTAMP_MILLISECONDS=1 MO_ChargeProfileMaxStackLevel=2 MO_ChargingScheduleMaxPeriods=4 MO_MaxChargingProfilesInstalled=3 diff --git a/README.md b/README.md index 9434aec7..65e3d0fa 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,19 @@ OCPP 1.6 / 2.0.1 client for microcontrollers. Portable C/C++. Compatible with Es 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 + +In 2025, the code base was largely restructured to work better with AI models. Chat bots can explain large parts of the code, write code snippets and help with the integration into the main charger firmware. Use your favorite AI tools with MicroOCPP and save hours of development time. + +The best practices for AI-friendly code are not fully clear yet. The following was considered a good start: + +- Consistency in architecture, variable names, algorithmic approaches +- More granular functions to make complexities explicit on the caller end +- Break down complexity and prefer duplicate code over deep abstraction +- Strictly define resource ownership in comments + +If your tools have issues working with something in MicroOCPP, please open an issue. Any ideas how to further optimize the code base are also highly appreciated. + ## Tester / Demo App *Main repository: [MicroOcppSimulator](https://github.com/matth-x/MicroOcppSimulator)* diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 665930c6..3112c02e 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -8,7 +8,7 @@ In general, microcontrollers have three relevant hardware constraints: - 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. +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 side. 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. @@ -20,19 +20,11 @@ When compiling a firmware with MicroOCPP, the resulting binary will contain func 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. +The following table shows the firmware size of each functional block. Their scope is defined in OCPP 2.0.1 and the 1.6 implementation follows the same scope. Since some code is reused between 1.6 and 2.0.1, the firmware size was taken in three configurations, one to only support 1.6, one for 2.0.1 only and one to enable both OCPP versions (the firmware can select the OCPP version during runtime). These configurations are shown per separate columns. **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') }} +{{ read_csv('modules.csv') }} ## Memory usage @@ -42,7 +34,7 @@ Another important figure is the base level which is much closer to the average h 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** +**Table 2: Memory usage per use case and total** {{ read_csv('heap_v201.csv') }} @@ -50,10 +42,6 @@ The following table shows the dynamic heap usage for a variety of test cases, fo 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** +**Table 3: All compilation units for OCPP 1.6 firmware** -{{ read_csv('compile_units_v201.csv') }} +{{ read_csv('compile_units.csv') }} diff --git a/examples/ESP/main.cpp b/examples/ESP/main.cpp index 306e700b..294e59e8 100644 --- a/examples/ESP/main.cpp +++ b/examples/ESP/main.cpp @@ -24,7 +24,7 @@ ESP8266WiFiMulti WiFiMulti; // // Settings which worked for my SteVe instance: // -//#define OCPP_BACKEND_URL "ws://192.168.178.100:8180/steve/websocket/CentralSystemService" +//#define OCPP_BACKEND_URL "ws://192.168.178.100:8080/steve/websocket/CentralSystemService" //#define OCPP_CHARGE_BOX_ID "esp-charger" void setup() { @@ -58,22 +58,32 @@ void setup() { /* * Initialize the OCPP library */ - mocpp_initialize(OCPP_BACKEND_URL, OCPP_CHARGE_BOX_ID, "My Charging Station", "My company name"); + mo_initialize(); + + mo_setWebsocketUrl( + OCPP_BACKEND_URL, //OCPP backend URL + OCPP_CHARGE_BOX_ID, //ChargeBoxId, assigned by OCPP backend + nullptr, //Authorization key (`nullptr` disables Basic Auth) + nullptr); //OCPP backend TLS certificate (`nullptr` disables certificate check) + + mo_setBootNotificationData("My Charging Station", "My company name"); + + mo_setOcppVersion(MO_OCPP_V16); //Run OCPP 1.6. For v2.0.1, pass argument `MO_OCPP_V201` /* * Integrate OCPP functionality. You can leave out the following part if your EVSE doesn't need it. */ - setEnergyMeterInput([]() { + mo_setEnergyMeterInput([]() { //take the energy register of the main electricity meter and return the value in watt-hours - return 0.f; + return 0; }); - setSmartChargingCurrentOutput([](float limit) { + mo_setSmartChargingCurrentOutput([](float limit) { //set the SAE J1772 Control Pilot value here Serial.printf("[main] Smart Charging allows maximum charge rate: %.0f\n", limit); }); - setConnectorPluggedInput([]() { + mo_setConnectorPluggedInput([]() { //return true if an EV is plugged to this EVSE return false; }); @@ -86,12 +96,12 @@ void loop() { /* * Do all OCPP stuff (process WebSocket input, send recorded meter values to Central System, etc.) */ - mocpp_loop(); + mo_loop(); /* * Energize EV plug if OCPP transaction is up and running */ - if (ocppPermitsCharge()) { + if (mo_ocppPermitsCharge()) { //OCPP set up and transaction running. Energize the EV plug here } else { //No transaction running at the moment. De-energize EV plug @@ -103,7 +113,7 @@ void loop() { if (/* RFID chip detected? */ false) { String idTag = "0123456789ABCD"; //e.g. idTag = RFID.readIdTag(); - if (!getTransaction()) { + if (!mo_getTransactionIdTag()) { //no transaction running or preparing. Begin a new transaction Serial.printf("[main] Begin Transaction with idTag %s\n", idTag.c_str()); @@ -112,7 +122,7 @@ void loop() { * 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()); + auto ret = mo_beginTransaction(idTag.c_str()); if (ret) { Serial.println(F("[main] Transaction initiated. OCPP lib will send a StartTransaction when" \ @@ -122,15 +132,10 @@ void loop() { } } 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?)")); - } + //Transaction already initiated. Attempt to stop current Tx by RFID card. If idTag is the same, + //or in the same group, then MO will stop the transaction and `mo_ocppPermitsCharge()` becomes + //false + mo_endTransaction(idTag.c_str(), nullptr); //`idTag` and `reason` can be nullptr } } diff --git a/src/MicroOcpp.cpp b/src/MicroOcpp.cpp index 5dbd3714..3a802e48 100644 --- a/src/MicroOcpp.cpp +++ b/src/MicroOcpp.cpp @@ -4,27 +4,29 @@ #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 #include @@ -37,26 +39,18 @@ #include -namespace MicroOcpp { -namespace Facade { +namespace MO_Facade { -#ifndef MO_CUSTOM_WS -WebSocketsClient *webSocket {nullptr}; -Connection *connection {nullptr}; -#endif +MicroOcpp::Context *g_context = nullptr; -Context *context {nullptr}; -std::shared_ptr filesystem; +} //namespace MO_Facade -#ifndef MO_NUMCONNECTORS -#define MO_NUMCONNECTORS 2 -#endif +using namespace MO_Facade; -#define OCPP_ID_OF_CP 0 -#define OCPP_ID_OF_CONNECTOR 1 +#define EVSE_ID_0 0 +#define EVSE_ID_1 1 -} //end namespace MicroOcpp::Facade -} //end namespace MicroOcpp +#define CONFIGS_FN_FACADE "user-config.jsn" #if MO_ENABLE_HEAP_PROFILER #ifndef MO_HEAP_PROFILER_EXTERNAL_CONTROL @@ -64,1470 +58,2923 @@ std::shared_ptr filesystem; #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; +//Begin the lifecycle of MO. Now, it is possible to set up the library. After configuring MO, +//call mo_setup() to finalize the setup +bool mo_initialize() { + if (g_context) { + MO_DBG_INFO("already initialized"); + return true; } - if (!backendUrl || !chargePointModel || !chargePointVendor) { - MO_DBG_ERR("invalid args"); - return; + g_context = new MicroOcpp::Context(); + if (!g_context) { + MO_DBG_ERR("OOM"); + return false; } + return true; +} - if (!chargeBoxId) { - chargeBoxId = ""; - } +//End the lifecycle of MO and free all resources +void mo_deinitialize() { + delete g_context; + g_context = nullptr; +} - /* - * parse backendUrl so that it suits the links2004/arduinoWebSockets interface - */ - auto url = makeString("MicroOcpp.cpp", backendUrl); +//Returns if library is initialized +bool mo_isInitialized() { + return g_context != nullptr; +} - //tolower protocol specifier - for (auto c = url.begin(); *c != ':' && c != url.end(); c++) { - *c = tolower(*c); - } +MO_Context *mo_getApiContext() { + return reinterpret_cast(g_context); +} - 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; - } +#if MO_USE_FILEAPI != MO_CUSTOM_FS +//Set if MO can use the filesystem and if it needs to mount it +void mo_setFilesystemConfig(MO_FilesystemOpt opt) { + mo_setFilesystemConfig2(mo_getApiContext(), opt, MO_FILENAME_PREFIX); +} - //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()); +void mo_setFilesystemConfig2(MO_Context *ctx, MO_FilesystemOpt opt, const char *pathPrefix) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before 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; - } - } + auto context = mo_getContext2(ctx); - if (path.empty()) { - path = "/"; - } + MO_FilesystemConfig filesystemConfig; + mo_filesystemConfig_init(&filesystemConfig); + filesystemConfig.opt = opt; + filesystemConfig.path_prefix = pathPrefix; - if ((!*chargeBoxId) == '\0') { - if (path.back() != '/') { - path += '/'; - } + context->setFilesystemConfig(filesystemConfig); +} +#endif // MO_USE_FILEAPI != MO_CUSTOM_FS - path += chargeBoxId; +#if MO_WS_USE == MO_WS_ARDUINO +//Setup MO with links2004/WebSockets library +bool mo_setWebsocketUrl(const char *backendUrl, const char *chargeBoxId, const char *authorizationKey, const char *CA_cert) { + if (!g_context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; } - 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"); + if (!backendUrl) { + MO_DBG_ERR("invalid args"); + return false; } - // try ever 5000 again if connection has failed - webSocket->setReconnectInterval(5000); + MO_ConnectionConfig config; + mo_connectionConfig_init(&config); + config.backendUrl = backendUrl; + config.chargeBoxId = chargeBoxId; + config.authorizationKey = authorizationKey; + config.CA_cert = CA_cert; - // 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 + g_context->setConnectionConfig(config); + return true; +} +#endif - // add authentication data (optional) - if (password && strlen(password) + strlen(chargeBoxId) >= 4) { - webSocket->setAuthorization(chargeBoxId, password); - } +#if __cplusplus +// Set a WebSocket Client +void mo_setConnection(MicroOcpp::Connection *connection) { + mo_setConnection2(mo_getApiContext(), connection); +} - delete connection; - connection = new EspWiFi::WSClient(webSocket); +void mo_setConnection2(MO_Context *ctx, MicroOcpp::Connection *connection) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + auto context = mo_getContext2(ctx); - mocpp_initialize(*connection, ChargerCredentials(chargePointModel, chargePointVendor), makeDefaultFilesystemAdapter(fsOpt), autoRecover); + context->setConnection(connection); } #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) { +//Set the OCPP version +void mo_setOcppVersion(int ocppVersion) { + mo_setOcppVersion2(mo_getApiContext(), ocppVersion); +} - 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"); +void mo_setOcppVersion2(MO_Context *ctx, int ocppVersion) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; } + auto context = mo_getContext2(ctx); - size_t written = serializeJson(creds, payload, 512); - - if (written < 2) { - MO_DBG_ERR("Charger Credentials could not be written"); - sprintf(payload, "{}"); - } + context->setOcppVersion(ocppVersion); } -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) { +//Set BootNotification fields +bool mo_setBootNotificationData(const char *chargePointModel, const char *chargePointVendor) { + MO_BootNotificationData bnData; + mo_bootNotificationData_init(&bnData); + bnData.chargePointModel = chargePointModel; + bnData.chargePointVendor = chargePointVendor; + + return mo_setBootNotificationData2(mo_getApiContext(), bnData); +} - ChargerCredentials res; +bool mo_setBootNotificationData2(MO_Context *ctx, MO_BootNotificationData bnData) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); - 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 MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { - if (creds.overflowed()) { - MO_DBG_ERR("Charger Credentials too long"); } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { - size_t written = serializeJson(creds, res.payload, 512); + } + #endif - if (written < 2) { - MO_DBG_ERR("Charger Credentials could not be written"); - sprintf(res.payload, "{}"); + MicroOcpp::BootService *bootService = context->getModelCommon().getBootService(); + if (!bootService) { + MO_DBG_ERR("OOM"); + return false; } - return res; + return bootService->setBootNotificationData(bnData); +} + +//Input about if an EV is plugged to this EVSE +void mo_setConnectorPluggedInput(bool (*connectorPlugged)()) { + //convert and forward callback to `mo_setConnectorPluggedInput2()` + mo_setConnectorPluggedInput2(mo_getApiContext(), EVSE_ID_1, [] (unsigned int, void *userData) { + auto connectorPlugged = reinterpret_cast(userData); + return connectorPlugged(); + }, reinterpret_cast(connectorPlugged)); } -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"); +void mo_setConnectorPluggedInput2(MO_Context *ctx, unsigned int evseId, bool (*connectorPlugged2)(unsigned int, void*), void *userData) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } + auto context = mo_getContext2(ctx); - MO_DBG_DEBUG("initialize OCPP"); - - filesystem = fs; - MO_DBG_DEBUG("filesystem %s", filesystem ? "loaded" : "deactivated"); + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto availSvc = context->getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + availSvcEvse->setConnectorPluggedInput(connectorPlugged2, userData); - BootStats bootstats; - BootService::loadBootStats(filesystem, bootstats); + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + txSvcEvse->setConnectorPluggedInput(connectorPlugged2, userData); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto availSvc = context->getModel201().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + availSvcEvse->setConnectorPluggedInput(connectorPlugged2, userData); - if (autoRecover && bootstats.getBootFailureCount() > 3) { - BootService::recover(filesystem, bootstats); - bootstats = BootStats(); + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + txSvcEvse->setConnectorPluggedInput(connectorPlugged2, userData); } + #endif +} - BootService::migrate(filesystem, bootstats); +//Input of the electricity meter register in Wh +bool mo_setEnergyMeterInput(int32_t (*energyInput)()) { + MO_MeterInput mInput; + mo_meterInput_init(&mInput, MO_MeterInputType_Int); + mInput.getInt = energyInput; + mInput.measurand = "Energy.Active.Import.Register"; + mInput.unit = "Wh"; + return mo_addMeterValueInput(mo_getApiContext(), EVSE_ID_1, mInput); +} - bootstats.bootNr++; //assign new boot number to this run - BootService::storeBootStats(filesystem, bootstats); +bool mo_setEnergyMeterInput2(MO_Context *ctx, unsigned int evseId, int32_t (*energyInput2)(MO_ReadingContext, unsigned int, void*), void *userData) { + MO_MeterInput mInput; + mo_meterInput_init(&mInput, MO_MeterInputType_IntWithArgs); + mInput.getInt2 = energyInput2; + mInput.measurand = "Energy.Active.Import.Register"; + mInput.unit = "Wh"; + mInput.user_data = userData; + return mo_addMeterValueInput(ctx, evseId, mInput); +} - configuration_init(filesystem); //call before each other library call +//Input of the power meter reading in W +bool mo_setPowerMeterInput(float (*powerInput)()) { + MO_MeterInput mInput; + mo_meterInput_init(&mInput, MO_MeterInputType_Float); + mInput.getFloat = powerInput; + mInput.measurand = "Power.Active.Import"; + mInput.unit = "W"; + return mo_addMeterValueInput(mo_getApiContext(), EVSE_ID_1, mInput); +} - context = new Context(connection, filesystem, bootstats.bootNr, version); +bool mo_setPowerMeterInput2(MO_Context *ctx, unsigned int evseId, float (*powerInput2)(MO_ReadingContext, unsigned int, void*), void *userData) { + MO_MeterInput mInput; + mo_meterInput_init(&mInput, MO_MeterInputType_Float); + mInput.getFloat2 = powerInput2; + mInput.measurand = "Power.Active.Import"; + mInput.unit = "W"; + mInput.user_data = userData; + return mo_addMeterValueInput(ctx, evseId, mInput); +} -#if MO_ENABLE_MBEDTLS - context->setFtpClient(makeFtpClientMbedTLS()); -#endif //MO_ENABLE_MBEDTLS +//Smart Charging Output +bool mo_setSmartChargingPowerOutput(void (*powerLimitOutput)(float)) { + //convert and forward callback to `mo_setSmartChargingOutput()` + return mo_setSmartChargingOutput(mo_getApiContext(), EVSE_ID_1, [] (MO_ChargeRate limit, unsigned int, void *userData) { + auto powerLimitOutput = reinterpret_cast(userData); - auto& model = context->getModel(); + powerLimitOutput(limit.power); + }, /*powerSupported*/ true, false, false, false, reinterpret_cast(powerLimitOutput)); +} - model.setBootService(std::unique_ptr( - new BootService(*context, filesystem))); +bool mo_setSmartChargingCurrentOutput(void (*currentLimitOutput)(float)) { + //convert and forward callback to `mo_setSmartChargingOutput()` + return mo_setSmartChargingOutput(mo_getApiContext(), EVSE_ID_1, [] (MO_ChargeRate limit, unsigned int, void *userData) { + auto currentLimitOutput = reinterpret_cast(userData); -#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 + currentLimitOutput(limit.current); + }, false, /*currentSupported*/ true, false, false, reinterpret_cast(currentLimitOutput)); +} - model.setResetService(std::unique_ptr( - new ResetService(*context))); +bool mo_setSmartChargingOutput(MO_Context *ctx, unsigned int evseId, void (*chargingLimitOutput)(MO_ChargeRate, unsigned int, void*), bool powerSupported, bool currentSupported, bool phases3to1Supported, bool phaseSwitchingSupported, void *userData) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; } + auto context = mo_getContext2(ctx); - 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)); + MicroOcpp::SmartChargingService *scService = context->getModelCommon().getSmartChargingService(); + if (!scService) { + MO_DBG_ERR("OOM"); + return false; } -#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); + return scService->setSmartChargingOutput(evseId, chargingLimitOutput, powerSupported, currentSupported, phases3to1Supported, phaseSwitchingSupported, userData); +} - auto credsJson = model.getBootService()->getChargePointCredentials(); - if (model.getFirmwareService() && credsJson && credsJson->containsKey("firmwareVersion")) { - model.getFirmwareService()->setBuildNumber((*credsJson)["firmwareVersion"]); +//Finalize setup. After this, the library is ready to be used and mo_loop() can be run +bool mo_setup() { + if (!g_context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; } - credsJson.reset(); - configuration_load(); + return g_context->setup(); +} -#if MO_ENABLE_V201 - if (version.major == 2) { - model.getVariableService()->load(); +//Run library routines. To be called in the main loop (e.g. place it inside `loop()`) +void mo_loop() { + if (!g_context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; } -#endif //MO_ENABLE_V201 + g_context->loop(); +} - MO_DBG_INFO("initialized MicroOcpp v" MO_VERSION " running OCPP %i.%i.%i", version.major, version.minor, version.patch); +//Transaction management. +bool mo_beginTransaction(const char *idTag) { + return mo_beginTransaction2(mo_getApiContext(), EVSE_ID_1, idTag); } -void mocpp_deinitialize() { +bool mo_beginTransaction2(MO_Context *ctx, unsigned int evseId, const char *idTag) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = false; - 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); + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("OOM"); + return false; } + success = txSvcEvse->beginTransaction(idTag); } - - delete context; - context = nullptr; - -#ifndef MO_CUSTOM_WS - delete connection; - connection = nullptr; - delete webSocket; - webSocket = nullptr; -#endif + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + success = mo_authorizeTransaction2(ctx, evseId, idTag, MO_IdTokenType_ISO14443); + } + #endif - filesystem.reset(); + return success; +} - configuration_deinit(); +//Same as `mo_beginTransaction()`, but skip authorization. `parentIdTag` can be NULL +bool mo_beginTransaction_authorized(const char *idTag, const char *parentIdTag) { + return mo_beginTransaction_authorized2(mo_getApiContext(), EVSE_ID_1, idTag, parentIdTag); +} -#if !MO_HEAP_PROFILER_EXTERNAL_CONTROL - MO_MEM_DEINIT(); -#endif +bool mo_beginTransaction_authorized2(MO_Context *ctx, unsigned int evseId, const char *idTag, const char *parentIdTag) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); - MO_DBG_DEBUG("deinitialized OCPP\n"); -} + bool success = false; -void mocpp_loop() { - if (!context) { - MO_DBG_WARN("need to call mocpp_initialize before"); - return; + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("OOM"); + return false; + } + success = txSvcEvse->beginTransaction_authorized(idTag, parentIdTag); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + success = mo_authorizeTransaction3(ctx, evseId, idTag, MO_IdTokenType_ISO14443, false, parentIdTag); } + #endif - context->loop(); + return success; } -bool beginTransaction(const char *idTag, unsigned int connectorId) { - if (!context) { +#if MO_ENABLE_V201 +bool mo_authorizeTransaction(const char *idToken) { + return mo_authorizeTransaction2(mo_getApiContext(), EVSE_ID_1, idToken, MO_IdTokenType_ISO14443); +} + +bool mo_authorizeTransaction2(MO_Context *ctx, unsigned int evseId, const char *idToken, MO_IdTokenType type) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } + auto context = mo_getContext2(ctx); + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + success = mo_beginTransaction2(ctx, evseId, idToken); + } + #endif #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"); + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("OOM"); return false; } - return evse->beginAuthorization(idTag, true); + success = txSvcEvse->beginAuthorization(MicroOcpp::v201::IdToken(idToken, type), /*validateIdToken*/ 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; + return success; } -bool beginTransaction_authorized(const char *idTag, const char *parentIdTag, unsigned int connectorId) { - if (!context) { +bool mo_authorizeTransaction3(MO_Context *ctx, unsigned int evseId, const char *idToken, MO_IdTokenType type, bool validateIdToken, const char *groupIdToken) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } + auto context = mo_getContext2(ctx); - #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); + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + if (validateIdToken) { + success = mo_beginTransaction2(ctx, evseId, idToken); + } else { + success = mo_beginTransaction_authorized2(ctx, evseId, idToken, nullptr); } - if (!evse) { - MO_DBG_ERR("could not find EVSE"); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("OOM"); return false; } - return evse->beginAuthorization(idTag, false); + success = txSvcEvse->beginAuthorization(MicroOcpp::v201::IdToken(idToken, type), validateIdToken, groupIdToken); } #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; + return success; } +#endif //MO_ENABLE_V201 -bool endTransaction(const char *idTag, const char *reason, unsigned int connectorId) { - if (!context) { +//End the transaction process if idTag is authorized +bool mo_endTransaction(const char *idTag, const char *reason) { + return mo_endTransaction2(mo_getApiContext(), EVSE_ID_1, idTag, reason); +} + +bool mo_endTransaction2(MO_Context *ctx, unsigned int evseId, const char *idTag, const char *reason) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } + auto context = mo_getContext2(ctx); - #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"); + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("OOM"); return false; } - return evse->endAuthorization(idTag, true); + success = txSvcEvse->endTransaction(idTag, reason); } #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); + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + if (!reason || !strcmp(reason, "Local")) { + success = mo_deauthorizeTransaction2(ctx, evseId, idTag, MO_IdTokenType_ISO14443); + } else if (!strcmp(reason, "PowerLoss")) { + success = mo_abortTransaction2(ctx, evseId, MO_TxStoppedReason_PowerLoss, MO_TxEventTriggerReason_AbnormalCondition); + } else if (!strcmp(reason, "Reboot")) { + success = mo_abortTransaction2(ctx, evseId, MO_TxStoppedReason_Reboot, MO_TxEventTriggerReason_ResetCommand); + } else if (idTag) { + success = mo_deauthorizeTransaction2(ctx, evseId, idTag, MO_IdTokenType_ISO14443); } 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; - } + success = mo_abortTransaction2(ctx, evseId, MO_TxStoppedReason_Other, MO_TxEventTriggerReason_AbnormalCondition); } } - return res; + #endif + + return success; } -bool endTransaction_authorized(const char *idTag, const char *reason, unsigned int connectorId) { - if (!context) { +//Same as `endTransaction()`, but skip authorization +bool mo_endTransaction_authorized(const char *idTag, const char *reason) { + return mo_endTransaction_authorized2(mo_getApiContext(), EVSE_ID_1, idTag, reason); +} + +bool mo_endTransaction_authorized2(MO_Context *ctx, unsigned int evseId, const char *idTag, const char *reason) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } + auto context = mo_getContext2(ctx); - #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); + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("OOM"); 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; + success = txSvcEvse->endTransaction_authorized(idTag, reason); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + if (!reason || !strcmp(reason, "Local")) { + success = mo_deauthorizeTransaction3(ctx, evseId, idTag, MO_IdTokenType_ISO14443, false, NULL); + } else if (!strcmp(reason, "PowerLoss")) { + success = mo_abortTransaction2(ctx, evseId, MO_TxStoppedReason_PowerLoss, MO_TxEventTriggerReason_AbnormalCondition); + } else if (!strcmp(reason, "Reboot")) { + success = mo_abortTransaction2(ctx, evseId, MO_TxStoppedReason_Reboot, MO_TxEventTriggerReason_ResetCommand); + } else if (idTag) { + success = mo_deauthorizeTransaction3(ctx, evseId, idTag, MO_IdTokenType_ISO14443, false, NULL); + } else { + success = mo_abortTransaction2(ctx, evseId, MO_TxStoppedReason_Other, MO_TxEventTriggerReason_AbnormalCondition); } - 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; + return success; } -bool isTransactionActive(unsigned int connectorId) { - if (!context) { +#if MO_ENABLE_V201 +//Same as mo_endTransaction +bool mo_deauthorizeTransaction(const char *idToken) { + return mo_deauthorizeTransaction2(mo_getApiContext(), EVSE_ID_1, idToken, MO_IdTokenType_ISO14443); +} + +bool mo_deauthorizeTransaction2(MO_Context *ctx, unsigned int evseId, const char *idToken, MO_IdTokenType type) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } + auto context = mo_getContext2(ctx); + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + success = mo_endTransaction2(ctx, evseId, idToken, "Local"); + } + #endif #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"); + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("OOM"); return false; } - return evse->getTransaction() && evse->getTransaction()->active; + success = txSvcEvse->endAuthorization(MicroOcpp::v201::IdToken(idToken, type), /*validateIdToken*/ true, nullptr); } #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; + return success; } -bool isTransactionRunning(unsigned int connectorId) { - if (!context) { +bool mo_deauthorizeTransaction3(MO_Context *ctx, unsigned int evseId, const char *idToken, MO_IdTokenType type, bool validateIdToken, const char *groupIdToken) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } + auto context = mo_getContext2(ctx); - #if MO_ENABLE_V201 - if (context->getVersion().major == 2) { - TransactionService::Evse *evse = nullptr; - if (auto txService = context->getModel().getTransactionService()) { - evse = txService->getEvse(connectorId); + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + if (validateIdToken) { + success = mo_endTransaction2(ctx, evseId, idToken, "Local"); + } else { + success = mo_endTransaction_authorized2(ctx, evseId, idToken, "Local"); } - if (!evse) { - MO_DBG_ERR("could not find EVSE"); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("OOM"); return false; } - return evse->getTransaction() && evse->getTransaction()->started && !evse->getTransaction()->stopped; + success = txSvcEvse->endAuthorization(MicroOcpp::v201::IdToken(idToken, type), validateIdToken, groupIdToken); } #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; + return success; } -const char *getTransactionIdTag(unsigned int connectorId) { - if (!context) { +//Force transaction stop +bool mo_abortTransaction(MO_TxStoppedReason stoppedReason, MO_TxEventTriggerReason stopTrigger) { + return mo_abortTransaction2(mo_getApiContext(), EVSE_ID_1, stoppedReason, stopTrigger); +} + +bool mo_abortTransaction2(MO_Context *ctx, unsigned int evseId, MO_TxStoppedReason stoppedReason, MO_TxEventTriggerReason stopTrigger) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return nullptr; + 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); + auto context = mo_getContext2(ctx); + + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + const char *reason = ""; + switch (stoppedReason) { + case MO_TxStoppedReason_EmergencyStop: + reason = "EmergencyStop"; + break; + case MO_TxStoppedReason_PowerLoss: + reason = "PowerLoss"; + break; + case MO_TxStoppedReason_Reboot: + reason = "Reboot"; + break; + default: + reason = "Other"; + break; } - if (!evse) { - MO_DBG_ERR("could not find EVSE"); - return nullptr; + success = mo_endTransaction_authorized2(ctx, evseId, NULL, reason); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("OOM"); + return false; } - return evse->getTransaction() ? evse->getTransaction()->idToken.get() : nullptr; + success = txSvcEvse->abortTransaction(stoppedReason, stopTrigger); } #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; + return success; } +#endif //MO_ENABLE_V201 -std::shared_ptr mocpp_undefinedTx; +//Returns if according to OCPP, the EVSE is allowed to close provide charge now +bool mo_ocppPermitsCharge() { + return mo_ocppPermitsCharge2(mo_getApiContext(), EVSE_ID_1); +} -std::shared_ptr& getTransaction(unsigned int connectorId) { - if (!context) { - MO_DBG_WARN("OCPP uninitialized"); - return mocpp_undefinedTx; +bool mo_ocppPermitsCharge2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; } - #if MO_ENABLE_V201 - if (context->getVersion().major == 2) { - MO_DBG_ERR("only supported in v16"); - return mocpp_undefinedTx; + auto context = mo_getContext2(ctx); + + bool res = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return false; + } + res = txSvcEvse->ocppPermitsCharge(); } #endif - auto connector = context->getModel().getConnector(connectorId); - if (!connector) { - MO_DBG_ERR("could not find connector"); - return mocpp_undefinedTx; + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return false; + } + res = txSvcEvse->ocppPermitsCharge(); } - return connector->getTransaction(); + #endif + + return res; } -#if MO_ENABLE_V201 -Ocpp201::Transaction *getTransactionV201(unsigned int evseId) { - if (!context) { +//Get information about the current Transaction lifecycle +bool mo_isTransactionActive() { + return mo_isTransactionActive2(mo_getApiContext(), EVSE_ID_1); +} + +bool mo_isTransactionActive2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return nullptr; + return false; } + auto context = mo_getContext2(ctx); - if (context->getVersion().major != 2) { - MO_DBG_ERR("only supported in v201"); - return nullptr; - } + bool res = false; - TransactionService::Evse *evse = nullptr; - if (auto txService = context->getModel().getTransactionService()) { - evse = txService->getEvse(evseId); + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return false; + } + auto tx = txSvcEvse->getTransaction(); + res = (tx && tx->isActive()); } - if (!evse) { - MO_DBG_ERR("could not find EVSE"); - return nullptr; + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return false; + } + auto tx = txSvcEvse->getTransaction(); + res = (tx && tx->active); } - return evse->getTransaction(); + #endif + + return res; } -#endif //MO_ENABLE_V201 -bool ocppPermitsCharge(unsigned int connectorId) { - if (!context) { - MO_DBG_WARN("OCPP uninitialized"); +bool mo_isTransactionRunning() { + return mo_isTransactionRunning2(mo_getApiContext(), EVSE_ID_1); +} + +bool mo_isTransactionRunning2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { + 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"); + auto context = mo_getContext2(ctx); + + bool res = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); return false; } - return evse->ocppPermitsCharge(); + auto tx = txSvcEvse->getTransaction(); + res = (tx && tx->isRunning()); } -#endif - auto connector = context->getModel().getConnector(connectorId); - if (!connector) { - MO_DBG_ERR("could not find connector"); - return false; + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return false; + } + auto tx = txSvcEvse->getTransaction(); + res = (tx && tx->started && !tx->stopped); } - return connector->ocppPermitsCharge(); + #endif + + return res; } -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(); +/* + * Get the idTag which has been used to start the transaction. If no transaction process is + * running, this function returns nullptr + */ +const char *mo_getTransactionIdTag() { + return mo_getTransactionIdTag2(mo_getApiContext(), EVSE_ID_1); } -void setConnectorPluggedInput(std::function pluggedInput, unsigned int connectorId) { - if (!context) { +const char *mo_getTransactionIdTag2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return; + return nullptr; } -#if MO_ENABLE_V201 - if (context->getVersion().major == 2) { - if (auto availabilityService = context->getModel().getAvailabilityService()) { - if (auto evse = availabilityService->getEvse(connectorId)) { - evse->setConnectorPluggedInput(pluggedInput); - } + auto context = mo_getContext2(ctx); + + const char *res = nullptr; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return nullptr; } - if (auto txService = context->getModel().getTransactionService()) { - if (auto evse = txService->getEvse(connectorId)) { - evse->setConnectorPluggedInput(pluggedInput); - } + if (auto tx = txSvcEvse->getTransaction()) { + res = tx->getIdTag(); } - return; } -#endif - auto connector = context->getModel().getConnector(connectorId); - if (!connector) { - MO_DBG_ERR("could not find connector"); - return; + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return nullptr; + } + if (auto tx = txSvcEvse->getTransaction()) { + res = tx->idToken.get(); + } } - connector->setConnectorPluggedInput(pluggedInput); + #endif + + return res && *res ? res : nullptr; } -void setEnergyMeterInput(std::function energyInput, unsigned int connectorId) { - if (!context) { +#if MO_ENABLE_V201 +const char *mo_getTransactionId() { + return mo_getTransactionId2(mo_getApiContext(), EVSE_ID_1); +} + +const char *mo_getTransactionId2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return; + return nullptr; } + auto context = mo_getContext2(ctx); + + const char *res = nullptr; + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return nullptr; + } + if (auto tx = txSvcEvse->getTransaction()) { + res = tx->getTransactionIdCompat(); + } + } + #endif #if MO_ENABLE_V201 - if (context->getVersion().major == 2) { - addMeterValueInput([energyInput] () {return static_cast(energyInput());}, "Energy.Active.Import.Register", "Wh", nullptr, nullptr, connectorId); - return; + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return nullptr; + } + if (auto tx = txSvcEvse->getTransaction()) { + res = tx->transactionId; + } } #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); + return res && *res ? res : nullptr; } +#endif //MO_ENABLE_V201 -void setPowerMeterInput(std::function powerInput, unsigned int connectorId) { - if (!context) { +#if MO_ENABLE_V16 +int mo_v16_getTransactionId() { + return mo_v16_getTransactionId2(mo_getApiContext(), EVSE_ID_1); +} + +int mo_v16_getTransactionId2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return; + return -1; } + auto context = mo_getContext2(ctx); + int res = -1; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return -1; + } + if (auto tx = txSvcEvse->getTransaction()) { + res = tx->getTransactionId(); + } + } + #endif #if MO_ENABLE_V201 - if (context->getVersion().major == 2) { - addMeterValueInput([powerInput] () {return static_cast(powerInput());}, "Power.Active.Import", "W", nullptr, nullptr, connectorId); - return; + if (context->getOcppVersion() == MO_OCPP_V201) { + MO_DBG_ERR("mo_v16_getTransactionId not supported with OCPP 2.0.1"); } #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); + return res; } +#endif //MO_ENABLE_V16 -void setSmartChargingPowerOutput(std::function chargingLimitOutput, unsigned int connectorId) { - if (!context) { +MO_ChargePointStatus mo_getChargePointStatus() { + return mo_getChargePointStatus2(mo_getApiContext(), EVSE_ID_1); +} + +MO_ChargePointStatus mo_getChargePointStatus2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { 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; + return MO_ChargePointStatus_UNDEFINED; } + auto context = mo_getContext2(ctx); - if (chargingLimitOutput) { - setSmartChargingOutput([chargingLimitOutput] (float power, float current, int nphases) -> void { - chargingLimitOutput(power); - }, connectorId); - } else { - setSmartChargingOutput(nullptr, connectorId); - } + MO_ChargePointStatus res = MO_ChargePointStatus_UNDEFINED; - if (auto scService = context->getModel().getSmartChargingService()) { - if (chargingLimitOutput) { - scService->updateAllowedChargingRateUnit(true, false); - } else { - scService->updateAllowedChargingRateUnit(false, false); + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto availSvc = context->getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return MO_ChargePointStatus_UNDEFINED; + } + res = availSvcEvse->getStatus(); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto availSvc = context->getModel201().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return MO_ChargePointStatus_UNDEFINED; } + res = availSvcEvse->getStatus(); } + #endif + + return res; } -void setSmartChargingCurrentOutput(std::function chargingLimitOutput, unsigned int connectorId) { - if (!context) { +//Input if EV is ready to charge (= J1772 State C) +void mo_setEvReadyInput(bool (*evReadyInput)()) { + //convert and forward callback to `mo_setEvReadyInput2()` + mo_setEvReadyInput2(mo_getApiContext(), EVSE_ID_1, [] (unsigned int, void *userData) { + auto evReadyInput = reinterpret_cast(userData); + return evReadyInput(); + }, reinterpret_cast(evReadyInput)); +} + +void mo_setEvReadyInput2(MO_Context *ctx, unsigned int evseId, bool (*evReadyInput2)(unsigned int, void*), void *userData) { + if (!ctx) { 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 context = mo_getContext2(ctx); - if (chargingLimitOutput) { - setSmartChargingOutput([chargingLimitOutput] (float power, float current, int nphases) -> void { - chargingLimitOutput(current); - }, connectorId); - } else { - setSmartChargingOutput(nullptr, connectorId); - } + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto availSvc = context->getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + availSvcEvse->setEvReadyInput(evReadyInput2, userData); - if (auto scService = context->getModel().getSmartChargingService()) { - if (chargingLimitOutput) { - scService->updateAllowedChargingRateUnit(false, true); - } else { - scService->updateAllowedChargingRateUnit(false, false); + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + txSvcEvse->setEvReadyInput(evReadyInput2, userData); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; } + txSvcEvse->setEvReadyInput(evReadyInput2, userData); } + #endif } -void setSmartChargingOutput(std::function chargingLimitOutput, unsigned int connectorId) { - if (!context) { +//Input if EVSE allows charge (= PWM signal on) +void mo_setEvseReadyInput(bool (*evseReadyInput)()) { + //convert and forward callback to `mo_setEvseReadyInput2()` + mo_setEvseReadyInput2(mo_getApiContext(), EVSE_ID_1, [] (unsigned int, void *userData) { + auto evseReadyInput = reinterpret_cast(userData); + return evseReadyInput(); + }, reinterpret_cast(evseReadyInput)); +} + +void mo_setEvseReadyInput2(MO_Context *ctx, unsigned int evseId, bool (*evseReadyInput2)(unsigned int, void*), void *userData) { + if (!ctx) { 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 context = mo_getContext2(ctx); - auto& model = context->getModel(); - if (!model.getSmartChargingService() && chargingLimitOutput) { - model.setSmartChargingService(std::unique_ptr( - new SmartChargingService(*context, filesystem, MO_NUMCONNECTORS))); + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto availSvc = context->getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + availSvcEvse->setEvseReadyInput(evseReadyInput2, userData); } - - if (auto scService = context->getModel().getSmartChargingService()) { - scService->setSmartChargingOutput(connectorId, chargingLimitOutput); - if (chargingLimitOutput) { - scService->updateAllowedChargingRateUnit(true, true); - } else { - scService->updateAllowedChargingRateUnit(false, false); + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; } + txSvcEvse->setEvseReadyInput(evseReadyInput2, userData); } + #endif } -void setEvReadyInput(std::function evReadyInput, unsigned int connectorId) { - if (!context) { +//Input for Error codes (please refer to OCPP 1.6, Edit2, p. 71 and 72 for valid error codes) +bool mo_addErrorCodeInput(const char* (*errorCodeInput)()) { + if (!g_context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return; + return false; } -#if MO_ENABLE_V201 - if (context->getVersion().major == 2) { - if (auto txService = context->getModel().getTransactionService()) { - if (auto evse = txService->getEvse(connectorId)) { - evse->setEvReadyInput(evReadyInput); - } + auto context = g_context; + + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto availSvc = context->getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(EVSE_ID_1) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return false; } - return; + + MO_ErrorDataInput errorDataInput; + mo_errorDataInput_init(&errorDataInput); + errorDataInput.userData = reinterpret_cast(errorCodeInput); + errorDataInput.getErrorData = [] (unsigned int, void *userData) { + auto errorCodeInput = reinterpret_cast(userData); + + MO_ErrorData errorData; + mo_errorData_init(&errorData); + mo_errorData_setErrorCode(&errorData, errorCodeInput()); + return errorData; + }; + + success = availSvcEvse->addErrorDataInput(errorDataInput); } -#endif - auto connector = context->getModel().getConnector(connectorId); - if (!connector) { - MO_DBG_ERR("could not find connector"); - return; + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto availSvc = context->getModel201().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(EVSE_ID_1) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return false; + } + + //error codes not really supported in OCPP 2.0.1, just Faulted status + MicroOcpp::v201::FaultedInput faultedInput; + faultedInput.userData = reinterpret_cast(errorCodeInput); + faultedInput.isFaulted = [] (unsigned int evseId, void *userData) { + auto errorCodeInput = reinterpret_cast(userData); + + return errorCodeInput() != nullptr; + }; + + success = availSvcEvse->addFaultedInput(faultedInput); } - connector->setEvReadyInput(evReadyInput); + #endif + + return success; } -void setEvseReadyInput(std::function evseReadyInput, unsigned int connectorId) { - if (!context) { +#if MO_ENABLE_V16 +//Input for Error codes + additional error data. See "Availability.h" for more docs +bool mo_v16_addErrorDataInput(MO_Context *ctx, unsigned int evseId, MO_ErrorData (*errorData)(unsigned int, void*), void *userData) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return; + return false; } -#if MO_ENABLE_V201 - if (context->getVersion().major == 2) { - if (auto txService = context->getModel().getTransactionService()) { - if (auto evse = txService->getEvse(connectorId)) { - evse->setEvseReadyInput(evseReadyInput); - } + auto context = mo_getContext2(ctx); + + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto availSvc = context->getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return false; } - return; + + MO_ErrorDataInput errorDataInput; + mo_errorDataInput_init(&errorDataInput); + errorDataInput.getErrorData = errorData; + errorDataInput.userData = userData; + + success = availSvcEvse->addErrorDataInput(errorDataInput); } -#endif - auto connector = context->getModel().getConnector(connectorId); - if (!connector) { - MO_DBG_ERR("could not find connector"); - return; + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + MO_DBG_ERR("mo_v16_addErrorDataInput not supported with OCPP 2.0.1"); } - connector->setEvseReadyInput(evseReadyInput); + #endif + + return success; } +#endif //MO_ENABLE_V16 -void addErrorCodeInput(std::function errorCodeInput, unsigned int connectorId) { - if (!context) { +#if MO_ENABLE_V201 +//Input to set EVSE into Faulted state +bool mo_v201_addFaultedInput(MO_Context *ctx, unsigned int evseId, bool (*faulted)(unsigned int, void*), void *userData) { + if (!ctx) { 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; + return false; } - connector->addErrorCodeInput(errorCodeInput); -} + auto context = mo_getContext2(ctx); -void addErrorDataInput(std::function errorDataInput, unsigned int connectorId) { - if (!context) { - MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return; + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + MO_DBG_ERR("mo_v201_addFaultedInput not supported with OCPP 1.6"); } - auto connector = context->getModel().getConnector(connectorId); - if (!connector) { - MO_DBG_ERR("could not find connector"); - return; + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto availSvc = context->getModel201().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return false; + } + + //error codes not really supported in OCPP 2.0.1, just Faulted status + MicroOcpp::v201::FaultedInput faultedInput; + faultedInput.isFaulted = faulted; + faultedInput.userData = userData; + + success = availSvcEvse->addFaultedInput(faultedInput); } - connector->addErrorDataInput(errorDataInput); + #endif + + return success; } +#endif //MO_ENABLE_V201 -void addMeterValueInput(std::function valueInput, const char *measurand, const char *unit, const char *location, const char *phase, unsigned int connectorId) { - if (!context) { +bool mo_addMeterValueInputInt(int32_t (*meterInput)(), const char *measurand, const char *unit, const char *location, const char *phase) { + MO_MeterInput mInput; + mo_meterInput_init(&mInput, MO_MeterInputType_Int); + mInput.getInt = meterInput; + mInput.measurand = measurand; + mInput.unit = unit; + mInput.location = location; + mInput.phase = phase; + return mo_addMeterValueInput(mo_getApiContext(), EVSE_ID_1, mInput); +} + +bool mo_addMeterValueInputInt2(MO_Context *ctx, unsigned int evseId, int32_t (*meterInput)(MO_ReadingContext, unsigned int, void*), const char *measurand, const char *unit, const char *location, const char *phase, void *userData) { + MO_MeterInput mInput; + mo_meterInput_init(&mInput, MO_MeterInputType_IntWithArgs); + mInput.getInt2 = meterInput; + mInput.measurand = measurand; + mInput.unit = unit; + mInput.location = location; + mInput.phase = phase; + mInput.user_data = userData; + return mo_addMeterValueInput(ctx, evseId, mInput); +} + +bool mo_addMeterValueInputFloat(float (*meterInput)(), const char *measurand, const char *unit, const char *location, const char *phase) { + MO_MeterInput mInput; + mo_meterInput_init(&mInput, MO_MeterInputType_Float); + mInput.getFloat = meterInput; + mInput.measurand = measurand; + mInput.unit = unit; + mInput.location = location; + mInput.phase = phase; + return mo_addMeterValueInput(mo_getApiContext(), EVSE_ID_1, mInput); +} + +bool mo_addMeterValueInputFloat2(MO_Context *ctx, unsigned int evseId, float (*meterInput)(MO_ReadingContext, unsigned int, void*), const char *measurand, const char *unit, const char *location, const char *phase, void *userData) { + MO_MeterInput mInput; + mo_meterInput_init(&mInput, MO_MeterInputType_FloatWithArgs); + mInput.getFloat2 = meterInput; + mInput.measurand = measurand; + mInput.unit = unit; + mInput.location = location; + mInput.phase = phase; + mInput.user_data = userData; + return mo_addMeterValueInput(ctx, evseId, mInput); +} + +bool mo_addMeterValueInputString(int (*meterInput)(char *buf, size_t size), const char *measurand, const char *unit, const char *location, const char *phase) { + MO_MeterInput mInput; + mo_meterInput_init(&mInput, MO_MeterInputType_String); + mInput.getString = meterInput; + mInput.measurand = measurand; + mInput.unit = unit; + mInput.location = location; + mInput.phase = phase; + return mo_addMeterValueInput(mo_getApiContext(), EVSE_ID_1, mInput); +} +bool mo_addMeterValueInputString2(MO_Context *ctx, unsigned int evseId, int (*meterInput)(char *buf, size_t size, MO_ReadingContext, unsigned int, void*), const char *measurand, const char *unit, const char *location, const char *phase, void *userData) { + MO_MeterInput mInput; + mo_meterInput_init(&mInput, MO_MeterInputType_StringWithArgs); + mInput.getString2 = meterInput; + mInput.measurand = measurand; + mInput.unit = unit; + mInput.location = location; + mInput.phase = phase; + mInput.user_data = userData; + return mo_addMeterValueInput(ctx, evseId, mInput); +} + +//Input for further MeterValue types with more options. See "MeterValue.h" for how to use it +bool mo_addMeterValueInput(MO_Context *ctx, unsigned int evseId, MO_MeterInput mInput) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return; + return false; } + auto context = mo_getContext2(ctx); - if (!valueInput) { - MO_DBG_ERR("value undefined"); - return; - } + bool success = false; - if (!measurand) { - measurand = "Energy.Active.Import.Register"; - MO_DBG_WARN("measurand unspecified; assume %s", measurand); + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto meterSvc = context->getModel16().getMeteringService(); + auto meterSvcEvse = meterSvc ? meterSvc->getEvse(evseId) : nullptr; + if (!meterSvcEvse) { + MO_DBG_ERR("init failure"); + return false; + } + success = meterSvcEvse->addMeterInput(mInput); } - + #endif #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"); + if (context->getOcppVersion() == MO_OCPP_V201) { + auto meterSvc = context->getModel201().getMeteringService(); + auto meterSvcEvse = meterSvc ? meterSvc->getEvse(evseId) : nullptr; + if (!meterSvcEvse) { + MO_DBG_ERR("init failure"); + return false; } - return; + success = meterSvcEvse->addMeterInput(mInput); } #endif - SampledValueProperties properties; - properties.setMeasurand(measurand); //mandatory for MO - - if (unit) - properties.setUnit(unit); - if (location) - properties.setLocation(location); - if (phase) - properties.setPhase(phase); + return success; +} - auto valueSampler = std::unique_ptr>>( - new MicroOcpp::SampledValueSamplerConcrete>( - properties, - [valueInput] (ReadingContext) {return valueInput();})); - addMeterValueInput(std::move(valueSampler), connectorId); +void mo_setOccupiedInput(bool (*occupied)()) { + //convert and forward callback to `mo_setOccupiedInput2()` + mo_setOccupiedInput2(mo_getApiContext(), EVSE_ID_1, [] (unsigned int, void *userData) { + auto occupied = reinterpret_cast(userData); + return occupied(); + }, reinterpret_cast(occupied)); } -void addMeterValueInput(std::unique_ptr valueInput, unsigned int connectorId) { - if (!context) { +void mo_setOccupiedInput2(MO_Context *ctx, unsigned int evseId, bool (*occupied2)(unsigned int, void*), void *userData) { + if (!ctx) { 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; + auto context = mo_getContext2(ctx); + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto availSvc = context->getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + availSvcEvse->setOccupiedInput(occupied2, userData); } #endif - auto& model = context->getModel(); - if (!model.getMeteringService()) { - model.setMeteringSerivce(std::unique_ptr( - new MeteringService(*context, MO_NUMCONNECTORS, filesystem))); + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto availSvc = context->getModel201().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + availSvcEvse->setOccupiedInput(occupied2, userData); } - model.getMeteringService()->addMeterValueSampler(connectorId, std::move(valueInput)); + #endif } -void setOccupiedInput(std::function occupied, unsigned int connectorId) { - if (!context) { +//Input if the charger is ready for StartTransaction +void mo_setStartTxReadyInput(bool (*startTxReady)()) { + //convert and forward callback to `mo_setStartTxReadyInput2()` + mo_setStartTxReadyInput2(mo_getApiContext(), EVSE_ID_1, [] (unsigned int, void *userData) { + auto startTxReady = reinterpret_cast(userData); + return startTxReady(); + }, reinterpret_cast(startTxReady)); +} + +void mo_setStartTxReadyInput2(MO_Context *ctx, unsigned int evseId, bool (*startTxReady2)(unsigned int, void*), void *userData) { + if (!ctx) { 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); - } + auto context = mo_getContext2(ctx); + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; } - return; + txSvcEvse->setStartTxReadyInput(startTxReady2, userData); } -#endif - auto connector = context->getModel().getConnector(connectorId); - if (!connector) { - MO_DBG_ERR("could not find connector"); - return; + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + txSvcEvse->setStartTxReadyInput(startTxReady2, userData); } - connector->setOccupiedInput(occupied); + #endif } -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); +//Input if the charger is ready for StopTransaction +void mo_setStopTxReadyInput(bool (*stopTxReady)()) { + //convert and forward callback to `mo_setStopTxReadyInput2()` + mo_setStopTxReadyInput2(mo_getApiContext(), EVSE_ID_1, [] (unsigned int, void *userData) { + auto stopTxReady = reinterpret_cast(userData); + return stopTxReady(); + }, reinterpret_cast(stopTxReady)); } -void setStopTxReadyInput(std::function stopTxReady, unsigned int connectorId) { - if (!context) { +void mo_setStopTxReadyInput2(MO_Context *ctx, unsigned int evseId, bool (*stopTxReady2)(unsigned int, void*), void *userData) { + if (!ctx) { 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); -} + auto context = mo_getContext2(ctx); -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_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + txSvcEvse->setStopTxReadyInput(stopTxReady2, userData); } + #endif #if MO_ENABLE_V201 - if (context->getVersion().major == 2) { - MO_DBG_ERR("only supported in v16"); - return; + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + txSvcEvse->setStopTxReadyInput(stopTxReady2, userData); } #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; - } +//Called when transaction state changes (see "TransactionDefs.h" for possible events). Transaction can be null +void mo_setTxNotificationOutput(void (*txNotificationOutput)(MO_TxNotification)) { + //convert and forward callback to `mo_setTxNotificationOutput2()` + mo_setTxNotificationOutput2(mo_getApiContext(), EVSE_ID_1, [] (MO_TxNotification txNotification, unsigned int, void *userData) { + auto txNotificationOutput = reinterpret_cast(userData); + txNotificationOutput(txNotification); + }, reinterpret_cast(txNotificationOutput)); +} - if (context->getVersion().major != 2) { - MO_DBG_ERR("only supported in v201"); +void mo_setTxNotificationOutput2(MO_Context *ctx, unsigned int evseId, void (*txNotificationOutput2)(MO_TxNotification, unsigned int, void*), void *userData) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } + auto context = mo_getContext2(ctx); - TransactionService::Evse *evse = nullptr; - if (auto txService = context->getModel().getTransactionService()) { - evse = txService->getEvse(connectorId); + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + txSvcEvse->setTxNotificationOutput(txNotificationOutput2, userData); } - if (!evse) { - MO_DBG_ERR("could not find EVSE"); - return; + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + txSvcEvse->setTxNotificationOutput(txNotificationOutput2, userData); } - evse->setTxNotificationOutput(notificationOutput); + #endif } -#endif //MO_ENABLE_V201 #if MO_ENABLE_CONNECTOR_LOCK -void setOnUnlockConnectorInOut(std::function onUnlockConnectorInOut, unsigned int connectorId) { - if (!context) { +void mo_setOnUnlockConnector(MO_UnlockConnectorResult (*onUnlockConnector)()) { + //convert and forward callback to `mo_setOnUnlockConnector2()` + mo_setOnUnlockConnector2(mo_getApiContext(), EVSE_ID_1, [] (unsigned int, void *userData) { + auto onUnlockConnector = reinterpret_cast(userData); + return onUnlockConnector(); + }, reinterpret_cast(onUnlockConnector)); +} + +void mo_setOnUnlockConnector2(MO_Context *ctx, unsigned int evseId, MO_UnlockConnectorResult (*onUnlockConnector2)(unsigned int, void*), void *userData) { + if (!ctx) { 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"); + auto context = mo_getContext2(ctx); + + auto rmtSvc = context->getModelCommon().getRemoteControlService(); + auto rmtSvcEvse = rmtSvc ? rmtSvc->getEvse(evseId) : nullptr; + if (!rmtSvcEvse) { + MO_DBG_ERR("init failure"); return; } - connector->setOnUnlockConnector(onUnlockConnectorInOut); + rmtSvcEvse->setOnUnlockConnector(onUnlockConnector2, userData); } #endif //MO_ENABLE_CONNECTOR_LOCK -bool isOperative(unsigned int connectorId) { - if (!context) { - MO_DBG_WARN("OCPP uninitialized"); - return true; //assume "true" as default state +bool mo_isOperative() { + return mo_isOperative2(mo_getApiContext(), EVSE_ID_1); +} + +bool mo_isOperative2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; } -#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(); + auto context = mo_getContext2(ctx); + + bool result = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto availSvc = context->getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return false; } + result = availSvcEvse->isOperative(); } -#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 + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto availSvc = context->getModel201().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (!availSvcEvse) { + MO_DBG_ERR("init failure"); + return false; + } + result = availSvcEvse->isOperative(); } - return chargePoint->isOperative() && connector->isOperative(); + #endif + + return result; } -void setOnResetNotify(std::function onResetNotify) { - if (!context) { +bool mo_isConnected() { + return mo_isConnected2(mo_getApiContext()); +} +bool mo_isConnected2(MO_Context *ctx) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return; + return false; } + auto context = mo_getContext2(ctx); -#if MO_ENABLE_V201 - if (context->getVersion().major == 2) { - if (auto rService = context->getModel().getResetServiceV201()) { - rService->setNotifyReset([onResetNotify] (ResetType) {return onResetNotify(true);}); - } - return; + if (auto connection = context->getConnection()) { + return connection->isConnected(); + } else { + MO_DBG_ERR("WebSocket not set up"); + return false; } -#endif +} + +bool mo_isAcceptedByCsms() { + return mo_isAcceptedByCsms2(mo_getApiContext()); +} - if (auto rService = context->getModel().getResetService()) { - rService->setPreReset(onResetNotify); +bool mo_isAcceptedByCsms2(MO_Context *ctx) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + MicroOcpp::BootService *bootService = context->getModelCommon().getBootService(); + if (!bootService) { + MO_DBG_ERR("OOM"); + return false; } + + return (bootService->getRegistrationStatus() == MicroOcpp::RegistrationStatus::Accepted); } -void setOnResetExecute(std::function onResetExecute) { - if (!context) { +int32_t mo_getUnixTime() { + return mo_getUnixTime2(mo_getApiContext()); +} + +int32_t mo_getUnixTime2(MO_Context *ctx) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return; + return 0; } + auto context = mo_getContext2(ctx); + auto& clock = context->getClock(); -#if MO_ENABLE_V201 - if (context->getVersion().major == 2) { - if (auto rService = context->getModel().getResetServiceV201()) { - rService->setExecuteReset([onResetExecute] () {onResetExecute(true); return true;}); - } - return; + int32_t unixTime = 0; + if (!clock.toUnixTime(clock.now(), unixTime)) { + unixTime = 0; } -#endif - if (auto rService = context->getModel().getResetService()) { - rService->setExecuteReset(onResetExecute); + return unixTime; +} + +bool mo_setUnixTime(int32_t unixTime) { + return mo_setUnixTime2(mo_getApiContext(), unixTime); +} + +bool mo_setUnixTime2(MO_Context *ctx, int32_t unixTime) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; } + auto context = mo_getContext2(ctx); + + return context->getClock().setTime(unixTime); } -FirmwareService *getFirmwareService() { - if (!context) { +int32_t mo_getUptime() { + return mo_getUptime2(mo_getApiContext()); +} + +int32_t mo_getUptime2(MO_Context *ctx) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return nullptr; + return false; } + auto context = mo_getContext2(ctx); + + return context->getClock().getUptimeInt(); +} + +int mo_getOcppVersion() { + return mo_getOcppVersion2(mo_getApiContext()); +} - auto& model = context->getModel(); - if (!model.getFirmwareService()) { - model.setFirmwareService(std::unique_ptr( - new FirmwareService(*context))); +int mo_getOcppVersion2(MO_Context *ctx) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return -1; } + auto context = mo_getContext2(ctx); - return model.getFirmwareService(); + return context->getOcppVersion(); } -DiagnosticsService *getDiagnosticsService() { - if (!context) { +void mo_setOnResetExecute(void (*onResetExecute)()) { + if (!g_context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return nullptr; + return; } + auto context = g_context; - auto& model = context->getModel(); - if (!model.getDiagnosticsService()) { - model.setDiagnosticsService(std::unique_ptr( - new DiagnosticsService(*context))); + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto rstSvc = context->getModel16().getResetService(); + if (!rstSvc) { + MO_DBG_ERR("init failure"); + return; + } + rstSvc->setExecuteReset([] (bool, void *userData) { + auto onResetExecute = reinterpret_cast(userData); + onResetExecute(); + return true; + }, reinterpret_cast(onResetExecute)); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto rstSvc = context->getModel201().getResetService(); + if (!rstSvc) { + MO_DBG_ERR("init failure"); + return; + } + rstSvc->setExecuteReset(EVSE_ID_0, [] (unsigned int, void *userData) { + auto onResetExecute = reinterpret_cast(userData); + onResetExecute(); + return true; + }, reinterpret_cast(onResetExecute)); } + #endif +} - return model.getDiagnosticsService(); +#if MO_ENABLE_V16 +void mo_v16_setOnResetNotify(bool (*onResetNotify)(bool)) { + mo_v16_setOnResetNotify2(mo_getApiContext(), [] (bool isHard, void *userData) { + auto onResetNotify = reinterpret_cast(userData); + return onResetNotify(isHard); + }, reinterpret_cast(onResetNotify)); } -#if MO_ENABLE_CERT_MGMT +void mo_v16_setOnResetNotify2(MO_Context *ctx, bool (*onResetNotify2)(bool, void*), void *userData) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + auto context = mo_getContext2(ctx); -void setCertificateStore(std::unique_ptr certStore) { - if (!context) { + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto rstSvc = context->getModel16().getResetService(); + if (!rstSvc) { + MO_DBG_ERR("init failure"); + return; + } + rstSvc->setNotifyReset(onResetNotify2, userData); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + MO_DBG_ERR("mo_v16_setOnResetNotify not supported with OCPP 2.0.1"); + } + #endif +} + +void mo_v16_setOnResetExecute(void (*onResetExecute)(bool)) { + mo_v16_setOnResetExecute2(mo_getApiContext(), [] (bool isHard, void *userData) { + auto onResetExecute = reinterpret_cast(userData); + return onResetExecute(isHard); + }, reinterpret_cast(onResetExecute)); +} +void mo_v16_setOnResetExecute2(MO_Context *ctx, bool (*onResetExecute2)(bool, void*), void *userData) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } + auto context = mo_getContext2(ctx); - auto& model = context->getModel(); - if (!model.getCertificateService()) { - model.setCertificateService(std::unique_ptr( - new CertificateService(*context))); + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto rstSvc = context->getModel16().getResetService(); + if (!rstSvc) { + MO_DBG_ERR("init failure"); + return; + } + rstSvc->setExecuteReset(onResetExecute2, userData); } - if (auto certService = model.getCertificateService()) { - certService->setCertificateStore(std::move(certStore)); - } else { - MO_DBG_ERR("OOM"); + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + MO_DBG_ERR("mo_v16_setOnResetNotify not supported with OCPP 2.0.1"); } + #endif } -#endif //MO_ENABLE_CERT_MGMT +#endif //MO_ENABLE_V16 -Context *getOcppContext() { - return context; +#if MO_ENABLE_V201 +void mo_v201_setOnResetNotify(bool (*onResetNotify)(MO_ResetType)) { + //convert and forward callback to `mo_v201_setOnResetNotify2()` + mo_v201_setOnResetNotify2(mo_getApiContext(), EVSE_ID_0, [] (MO_ResetType resetType, unsigned int, void *userData) { + auto onResetNotify = reinterpret_cast(userData); + return onResetNotify(resetType); + }, reinterpret_cast(onResetNotify)); } -void setOnReceiveRequest(const char *operationType, OnReceiveReqListener onReceiveReq) { - if (!context) { +void mo_v201_setOnResetNotify2(MO_Context *ctx, unsigned int evseId, bool (*onResetNotify2)(MO_ResetType, unsigned int, void*), void *userData) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } - if (!operationType) { - MO_DBG_ERR("invalid args"); + auto context = mo_getContext2(ctx); + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + MO_DBG_ERR("mo_v201_setOnResetNotify not supported with OCPP 1.6"); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto rstSvc = context->getModel201().getResetService(); + if (!rstSvc) { + MO_DBG_ERR("init failure"); + return; + } + rstSvc->setNotifyReset(evseId, onResetNotify2, userData); + } + #endif +} + +void mo_v201_setOnResetExecute(void (*onResetExecute)()) { + //convert and forward callback to `mo_v201_setOnResetExecute2()` + mo_v201_setOnResetExecute2(mo_getApiContext(), EVSE_ID_0, [] (unsigned int, void *userData) { + auto onResetExecute = reinterpret_cast(userData); + onResetExecute(); + return false; + }, reinterpret_cast(onResetExecute)); +} + +void mo_v201_setOnResetExecute2(MO_Context *ctx, unsigned int evseId, bool (*onResetExecute2)(unsigned int, void*), void *userData) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } - context->getOperationRegistry().setOnRequest(operationType, onReceiveReq); + auto context = mo_getContext2(ctx); + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + MO_DBG_ERR("mo_v201_setOnResetExecute not supported with OCPP 1.6"); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto rstSvc = context->getModel201().getResetService(); + if (!rstSvc) { + MO_DBG_ERR("init failure"); + return; + } + rstSvc->setExecuteReset(evseId, onResetExecute2, userData); + } + #endif } +#endif //MO_ENABLE_V201 -void setOnSendConf(const char *operationType, OnSendConfListener onSendConf) { - if (!context) { +void mo_setDebugCb(void (*debugCb)(const char *msg)) { + if (!g_context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } - if (!operationType) { - MO_DBG_ERR("invalid args"); + auto context = g_context; + + context->setDebugCb(debugCb); +} + +void mo_setDebugCb2(MO_Context *ctx, void (*debugCb2)(int lvl, const char *fn, int line, const char *msg)) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } - context->getOperationRegistry().setOnResponse(operationType, onSendConf); + auto context = mo_getContext2(ctx); + + context->setDebugCb2(debugCb2); } -void sendRequest(const char *operationType, - std::function ()> fn_createReq, - std::function fn_processConf) { +void mo_setTicksCb(uint32_t (*ticksCb)()) { + mo_setTicksCb2(mo_getApiContext(), ticksCb); +} - if (!context) { +void mo_setTicksCb2(MO_Context *ctx, uint32_t (*ticksCb)()) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } - if (!operationType || !fn_createReq || !fn_processConf) { - MO_DBG_ERR("invalid args"); + auto context = mo_getContext2(ctx); + + context->setTicksCb(ticksCb); +} + +void mo_setRngCb(uint32_t (*rngCb)()) { + mo_setRngCb2(mo_getApiContext(), rngCb); +} + +void mo_setRngCb2(MO_Context *ctx, uint32_t (*rngCb)()) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } + auto context = mo_getContext2(ctx); - auto request = makeRequest(new CustomOperation(operationType, fn_createReq, fn_processConf)); - context->initiateRequest(std::move(request)); + context->setRngCb(rngCb); } -void setRequestHandler(const char *operationType, - std::function fn_processReq, - std::function ()> fn_createConf) { +MO_FilesystemAdapter *mo_getFilesystem() { + return mo_getFilesystem2(mo_getApiContext()); +} - if (!context) { +MO_FilesystemAdapter *mo_getFilesystem2(MO_Context *ctx) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return nullptr; + } + auto context = mo_getContext2(ctx); + + return context->getFilesystem(); +} + +#if MO_ENABLE_MBEDTLS +void mo_setFtpConfig2(MO_Context *ctx, MO_FTPConfig ftpConfig) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } - if (!operationType || !fn_processReq || !fn_createConf) { - MO_DBG_ERR("invalid args"); + auto context = mo_getContext2(ctx); + + context->setFtpConfig(ftpConfig); +} + +#if MO_ENABLE_DIAGNOSTICS +void mo_setDiagnosticsReader(MO_Context *ctx, size_t (*readBytes)(char*, size_t, void*), void(*onClose)(void*), void *userData) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } + auto context = mo_getContext2(ctx); - auto captureOpType = makeString("MicroOcpp.cpp", operationType); + MicroOcpp::DiagnosticsService *diagSvc = context->getModelCommon().getDiagnosticsService(); + if (!diagSvc) { + MO_DBG_ERR("init failure"); + return; + } - context->getOperationRegistry().registerOperation(operationType, [captureOpType, fn_processReq, fn_createConf] () { - return new CustomOperation(captureOpType.c_str(), fn_processReq, fn_createConf); - }); + diagSvc->setDiagnosticsReader(readBytes, onClose, userData); } -void authorize(const char *idTag, OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, unsigned int timeout) { - if (!context) { +void mo_setDiagnosticsFtpServerCert(MO_Context *ctx, const char *cert) { + if (!ctx) { 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); + auto context = mo_getContext2(ctx); + + MicroOcpp::DiagnosticsService *diagSvc = context->getModelCommon().getDiagnosticsService(); + if (!diagSvc) { + MO_DBG_ERR("init failure"); 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) { + + diagSvc->setFtpServerCert(cert); +} +#endif //MO_ENABLE_DIAGNOSTICS + +#if MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT +//void mo_setFirmwareFtpServerCert(const char *cert); //zero-copy mode, i.e. cert must outlive MO +#endif //MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT + +#endif //MO_ENABLE_MBEDTLS + +/* + * Transaction management (Advanced) + */ + +bool mo_isTransactionAuthorized() { + return mo_isTransactionAuthorized2(mo_getApiContext(), EVSE_ID_1); +} + +bool mo_isTransactionAuthorized2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { 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 context = mo_getContext2(ctx); + + bool res = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return false; + } + auto tx = txSvcEvse->getTransaction(); + res = (tx && tx->isAuthorized()); } - auto connector = context->getModel().getConnector(OCPP_ID_OF_CONNECTOR); - if (!connector) { - MO_DBG_ERR("could not find connector"); + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return false; + } + auto tx = txSvcEvse->getTransaction(); + res = (tx && tx->isAuthorized); + } + #endif + + return res; +} + +//Returns if server has rejected transaction in StartTx or TransactionEvent response +bool mo_isTransactionDeauthorized() { + return mo_isTransactionDeauthorized2(mo_getApiContext(), EVSE_ID_1); +} + +bool mo_isTransactionDeauthorized2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } - auto transaction = connector->getTransaction(); - if (transaction) { - if (transaction->getStartSync().isRequested()) { - MO_DBG_ERR("transaction already in progress. Must call stopTransaction()"); + auto context = mo_getContext2(ctx); + + bool res = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); return false; } - transaction->setIdTag(idTag); - } else { - beginTransaction_authorized(idTag); //request new transaction object - transaction = connector->getTransaction(); - if (!transaction) { - MO_DBG_WARN("transaction queue full"); + auto tx = txSvcEvse->getTransaction(); + res = (tx && tx->isIdTagDeauthorized()); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); return false; } + auto tx = txSvcEvse->getTransaction(); + res = (tx && tx->isDeauthorized); } + #endif - 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 res; +} - return true; +//Returns start unix time of transaction. Returns 0 if unix time is not known +int32_t mo_getTransactionStartUnixTime() { + return mo_getTransactionStartUnixTime2(mo_getApiContext(), EVSE_ID_1); } -bool stopTransaction(OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, unsigned int timeout) { - if (!context) { +int32_t mo_getTransactionStartUnixTime2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return false; + return 0; } - auto connector = context->getModel().getConnector(OCPP_ID_OF_CONNECTOR); - if (!connector) { - MO_DBG_ERR("could not find connector"); - return false; + auto context = mo_getContext2(ctx); + + MicroOcpp::Timestamp startTime; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return 0; + } + if (auto tx = txSvcEvse->getTransaction()) { + startTime = tx->getStartTimestamp(); + } } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + auto txSvc = context->getModel201().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return 0; + } + if (auto tx = txSvcEvse->getTransaction()) { + startTime = tx->startTimestamp; + } + } + #endif - auto transaction = connector->getTransaction(); - if (!transaction || !transaction->isRunning()) { - MO_DBG_ERR("no running Tx to stop"); - return false; + int32_t res = 0; + + if (!context->getClock().toUnixTime(startTime, res)) { + MO_DBG_ERR("cannot determine transaction start unix time"); + res = 0; } - connector->endTransaction(transaction->getIdTag(), "Local"); + return res; +} + +#if MO_ENABLE_V16 +//Overrides start unix time of transaction +void mo_setTransactionStartUnixTime(int32_t unixTime) { + mo_setTransactionStartUnixTime2(mo_getApiContext(), EVSE_ID_1, unixTime); +} - const char *idTag = transaction->getIdTag(); - if (idTag) { - transaction->setStopIdTag(idTag); +void mo_setTransactionStartUnixTime2(MO_Context *ctx, unsigned int evseId, int32_t unixTime) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; } + auto context = mo_getContext2(ctx); - 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"); + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + if (auto tx = txSvcEvse->getTransaction()) { + + MicroOcpp::Timestamp t; + if (!context->getClock().fromUnixTime(t, unixTime)) { + MO_DBG_ERR("invalid unix time"); + return; + } + tx->setStartTimestamp(t); } } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + MO_DBG_ERR("mo_setTransactionStartUnixTime not supported with OCPP 2.0.1"); + } + #endif +} - transaction->setStopTimestamp(context->getModel().getClock().now()); +int32_t mo_getTransactionStopUnixTime() { + return mo_getTransactionStopUnixTime2(mo_getApiContext(), EVSE_ID_1); +} - transaction->commit(); +int32_t mo_getTransactionStopUnixTime2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return 0; + } + auto context = mo_getContext2(ctx); - 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)); + int32_t res = 0; - return true; + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return 0; + } + if (auto tx = txSvcEvse->getTransaction()) { + if (!context->getClock().toUnixTime(tx->getStopTimestamp(), res)) { + MO_DBG_ERR("cannot determine transaction stop unix time"); + res = 0; + } + } + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + MO_DBG_ERR("mo_setTransactionStartUnixTime not supported with OCPP 2.0.1"); + } + #endif + + return res; +} + +//Overrides stop unix time of transaction +void mo_setTransactionStopUnixTime(int32_t unixTime) { + mo_setTransactionStopUnixTime2(mo_getApiContext(), EVSE_ID_1, unixTime); +} + +void mo_setTransactionStopUnixTime2(MO_Context *ctx, unsigned int evseId, int32_t unixTime) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + auto context = mo_getContext2(ctx); + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + if (auto tx = txSvcEvse->getTransaction()) { + + MicroOcpp::Timestamp t; + if (!context->getClock().fromUnixTime(t, unixTime)) { + MO_DBG_ERR("invalid unix time"); + return; + } + tx->setStopTimestamp(t); + } + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + MO_DBG_ERR("mo_setTransactionStopUnixTime not supported with OCPP 2.0.1"); + } + #endif +} + +int32_t mo_getTransactionMeterStart() { + return mo_getTransactionMeterStart2(mo_getApiContext(), EVSE_ID_1); +} + +int32_t mo_getTransactionMeterStart2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return -1; + } + auto context = mo_getContext2(ctx); + + int32_t res = -1; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return -1; + } + if (auto tx = txSvcEvse->getTransaction()) { + res = tx->getMeterStart(); + } + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + MO_DBG_ERR("mo_getTransactionMeterStart not supported with OCPP 2.0.1"); + } + #endif + + return res; +} + +//Overrides meterStart of transaction +void mo_setTransactionMeterStart(int32_t meterStart) { + mo_setTransactionMeterStart2(mo_getApiContext(), EVSE_ID_1, meterStart); +} + +void mo_setTransactionMeterStart2(MO_Context *ctx, unsigned int evseId, int32_t meterStart) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + auto context = mo_getContext2(ctx); + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + if (auto tx = txSvcEvse->getTransaction()) { + tx->setMeterStart(meterStart); + } + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + MO_DBG_ERR("mo_setTransactionMeterStart not supported with OCPP 2.0.1"); + } + #endif +} + +int32_t mo_getTransactionMeterStop() { + return mo_getTransactionMeterStop2(mo_getApiContext(), EVSE_ID_1); +} + +int32_t mo_getTransactionMeterStop2(MO_Context *ctx, unsigned int evseId) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return -1; + } + auto context = mo_getContext2(ctx); + + int32_t res = -1; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return -1; + } + if (auto tx = txSvcEvse->getTransaction()) { + res = tx->getMeterStop(); + } + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + MO_DBG_ERR("mo_getTransactionMeterStop not supported with OCPP 2.0.1"); + } + #endif + + return res; +} + +//Overrides meterStop of transaction +void mo_setTransactionMeterStop(int32_t meterStop) { + mo_setTransactionMeterStop2(mo_getApiContext(), EVSE_ID_1, meterStop); +} + +void mo_setTransactionMeterStop2(MO_Context *ctx, unsigned int evseId, int32_t meterStop) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + auto context = mo_getContext2(ctx); + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + auto txSvc = context->getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("init failure"); + return; + } + if (auto tx = txSvcEvse->getTransaction()) { + tx->setMeterStop(meterStop); + } + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + MO_DBG_ERR("mo_setTransactionMeterStop not supported with OCPP 2.0.1"); + } + #endif +} +#endif //MO_ENABLE_V16 + +//helper function +namespace MO_Facade { +MicroOcpp::Mutability convertMutability(MO_Mutability mutability) { + auto res = MicroOcpp::Mutability::None; + switch (mutability) { + case MO_Mutability_ReadWrite: + res = MicroOcpp::Mutability::ReadWrite; + break; + case MO_Mutability_ReadOnly: + res = MicroOcpp::Mutability::ReadOnly; + break; + case MO_Mutability_WriteOnly: + res = MicroOcpp::Mutability::WriteOnly; + break; + case MO_Mutability_None: + res = MicroOcpp::Mutability::None; + break; + } + return res; +} +} //namespace MO_Facade + +using namespace MO_Facade; + +#if MO_ENABLE_V16 +bool mo_declareConfigurationInt(const char *key, int factoryDefault, MO_Mutability mutability, bool persistent, bool rebootRequired) { + return mo_declareVarConfigInt(mo_getApiContext(), NULL, NULL, key, factoryDefault, mutability, persistent, rebootRequired); +} + +bool mo_declareConfigurationBool(const char *key, bool factoryDefault, MO_Mutability mutability, bool persistent, bool rebootRequired) { + return mo_declareVarConfigBool(mo_getApiContext(), NULL, NULL, key, factoryDefault, mutability, persistent, rebootRequired); +} + +bool mo_declareConfigurationString(const char *key, const char *factoryDefault, MO_Mutability mutability, bool persistent, bool rebootRequired) { + return mo_declareVarConfigString(mo_getApiContext(), NULL, NULL, key, factoryDefault, mutability, persistent, rebootRequired); +} + +//Set Config value. Set after `mo_setup()` +bool mo_setConfigurationInt(const char *key, int value) { + return mo_setVarConfigInt(mo_getApiContext(), NULL, NULL, key, value); +} +bool mo_setConfigurationBool(const char *key, bool value) { + return mo_setVarConfigBool(mo_getApiContext(), NULL, NULL, key, value); +} +bool mo_setConfigurationString(const char *key, const char *value) { + return mo_setVarConfigString(mo_getApiContext(), NULL, NULL, key, value); +} + +//Get Config value. MO writes the value into `valueOut`. Call after `mo_setup()` +bool mo_getConfigurationInt(const char *key, int *valueOut) { + return mo_getVarConfigInt(mo_getApiContext(), NULL, NULL, key, valueOut); +} +bool mo_getConfigurationBool(const char *key, bool *valueOut) { + return mo_getVarConfigBool(mo_getApiContext(), NULL, NULL, key, valueOut); +} +bool mo_getConfigurationString(const char *key, const char **valueOut) { + return mo_getVarConfigString(mo_getApiContext(), NULL, NULL, key, valueOut); +} +#endif //MO_ENABLE_V16 + +bool mo_declareVarConfigInt(MO_Context *ctx, const char *component201, const char *name201, const char *key16, int factoryDefault, MO_Mutability mutability, bool persistent, bool rebootRequired) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + if (!key16) { + //disabled OCPP 1.6 compatibility + return true; + } + auto configSvc = context->getModel16().getConfigurationService(); + if (!configSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + + const char *containerId = persistent ? CONFIGS_FN_FACADE : MO_CONFIGURATION_VOLATILE; + success = configSvc->declareConfiguration(key16, factoryDefault, containerId, convertMutability(mutability), rebootRequired); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + if (!component201 || !name201) { + //disabled OCPP 2.0.1 compatibility + return true; + } + auto varSvc = context->getModel201().getVariableService(); + if (!varSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + + success = varSvc->declareVariable(component201, name201, factoryDefault, convertMutability(mutability), persistent, MicroOcpp::v201::Variable::AttributeTypeSet(), rebootRequired); + } + #endif + + return success; +} +bool mo_declareVarConfigBool(MO_Context *ctx, const char *component201, const char *name201, const char *key16, bool factoryDefault, MO_Mutability mutability, bool persistent, bool rebootRequired) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + if (!key16) { + //disabled OCPP 1.6 compatibility + return true; + } + auto configSvc = context->getModel16().getConfigurationService(); + if (!configSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + + const char *containerId = persistent ? CONFIGS_FN_FACADE : MO_CONFIGURATION_VOLATILE; + success = configSvc->declareConfiguration(key16, factoryDefault, containerId, convertMutability(mutability), rebootRequired); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + if (!component201 || !name201) { + //disabled OCPP 2.0.1 compatibility + return true; + } + auto varSvc = context->getModel201().getVariableService(); + if (!varSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + + success = varSvc->declareVariable(component201, name201, factoryDefault, convertMutability(mutability), persistent, MicroOcpp::v201::Variable::AttributeTypeSet(), rebootRequired); + } + #endif + + return success; +} +bool mo_declareVarConfigString(MO_Context *ctx, const char *component201, const char *name201, const char *key16, const char *factoryDefault, MO_Mutability mutability, bool persistent, bool rebootRequired) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + if (!key16) { + //disabled OCPP 1.6 compatibility + return true; + } + auto configSvc = context->getModel16().getConfigurationService(); + if (!configSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + + const char *containerId = persistent ? CONFIGS_FN_FACADE : MO_CONFIGURATION_VOLATILE; + success = configSvc->declareConfiguration(key16, factoryDefault, containerId, convertMutability(mutability), rebootRequired); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + if (!component201 || !name201) { + //disabled OCPP 2.0.1 compatibility + return true; + } + auto varSvc = context->getModel201().getVariableService(); + if (!varSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + + success = varSvc->declareVariable(component201, name201, factoryDefault, convertMutability(mutability), persistent, MicroOcpp::v201::Variable::AttributeTypeSet(), rebootRequired); + } + #endif + + return success; +} + +//Set Variable or Config value. Set after `mo_setup()`. If running OCPP 2.0.1, `key16` can be NULL +bool mo_setVarConfigInt(MO_Context *ctx, const char *component201, const char *name201, const char *key16, int value) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + if (!key16) { + //disabled OCPP 1.6 compatibility + return true; + } + auto configSvc = context->getModel16().getConfigurationService(); + if (!configSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + auto config = configSvc->getConfiguration(key16); + if (!config) { + MO_DBG_ERR("could not find config %s", key16); + return false; + } + config->setInt(value); + success = configSvc->commit(); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + if (!component201 || !name201) { + //disabled OCPP 2.0.1 compatibility + return true; + } + auto varSvc = context->getModel201().getVariableService(); + if (!varSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + auto variable = varSvc->getVariable(component201, name201); + if (!variable) { + MO_DBG_ERR("could not find variable %s %s", component201, name201); + return false; + } + variable->setInt(value); + success = varSvc->commit(); + } + #endif + + return success; +} +bool mo_setVarConfigBool(MO_Context *ctx, const char *component201, const char *name201, const char *key16, bool value) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + if (!key16) { + //disabled OCPP 1.6 compatibility + return true; + } + + auto configSvc = context->getModel16().getConfigurationService(); + if (!configSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + auto config = configSvc->getConfiguration(key16); + if (!config) { + MO_DBG_ERR("could not find config %s", key16); + return false; + } + config->setBool(value); + success = configSvc->commit(); + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + if (!component201 || !name201) { + //disabled OCPP 2.0.1 compatibility + return true; + } + auto varSvc = context->getModel201().getVariableService(); + if (!varSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + auto variable = varSvc->getVariable(component201, name201); + if (!variable) { + MO_DBG_ERR("could not find variable %s %s", component201, name201); + return false; + } + variable->setBool(value); + success = varSvc->commit(); + } + #endif + + return success; +} + +bool mo_setVarConfigString(MO_Context *ctx, const char *component201, const char *name201, const char *key16, const char *value) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + if (!key16) { + //disabled OCPP 1.6 compatibility + return true; + } + auto configSvc = context->getModel16().getConfigurationService(); + if (!configSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + auto config = configSvc->getConfiguration(key16); + if (!config) { + MO_DBG_ERR("could not find config %s", key16); + return false; + } + success = config->setString(value); + if (success) { + success = configSvc->commit(); + } + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + if (!component201 || !name201) { + //disabled OCPP 2.0.1 compatibility + return true; + } + auto varSvc = context->getModel201().getVariableService(); + if (!varSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + auto variable = varSvc->getVariable(component201, name201); + if (!variable) { + MO_DBG_ERR("could not find variable %s %s", component201, name201); + return false; + } + success = variable->setString(value); + if (success) { + success = varSvc->commit(); + } + } + #endif + + return success; +} + +bool mo_getVarConfigInt(MO_Context *ctx, const char *component201, const char *name201, const char *key16, int *valueOut) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + if (!key16) { + //disabled OCPP 1.6 compatibility + return true; + } + auto configSvc = context->getModel16().getConfigurationService(); + if (!configSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + auto config = configSvc->getConfiguration(key16); + if (!config) { + MO_DBG_ERR("could not find config %s", key16); + return false; + } + *valueOut = config->getInt(); + success = true; + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + if (!component201 || !name201) { + //disabled OCPP 2.0.1 compatibility + return true; + } + auto varSvc = context->getModel201().getVariableService(); + if (!varSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + auto variable = varSvc->getVariable(component201, name201); + if (!variable) { + MO_DBG_ERR("could not find variable %s %s", component201, name201); + return false; + } + *valueOut = variable->getInt(); + success = true; + } + #endif + + return success; +} +bool mo_getVarConfigBool(MO_Context *ctx, const char *component201, const char *name201, const char *key16, bool *valueOut) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + if (!key16) { + //disabled OCPP 1.6 compatibility + return true; + } + auto configSvc = context->getModel16().getConfigurationService(); + if (!configSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + auto config = configSvc->getConfiguration(key16); + if (!config) { + MO_DBG_ERR("could not find config %s", key16); + return false; + } + *valueOut = config->getBool(); + success = true; + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + if (!component201 || !name201) { + //disabled OCPP 2.0.1 compatibility + return true; + } + auto varSvc = context->getModel201().getVariableService(); + if (!varSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + auto variable = varSvc->getVariable(component201, name201); + if (!variable) { + MO_DBG_ERR("could not find variable %s %s", component201, name201); + return false; + } + *valueOut = variable->getBool(); + success = true; + } + #endif + + return success; +} +bool mo_getVarConfigString(MO_Context *ctx, const char *component201, const char *name201, const char *key16, const char **valueOut) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = false; + + #if MO_ENABLE_V16 + if (context->getOcppVersion() == MO_OCPP_V16) { + if (!key16) { + //disabled OCPP 1.6 compatibility + return true; + } + auto configSvc = context->getModel16().getConfigurationService(); + if (!configSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + auto config = configSvc->getConfiguration(key16); + if (!config) { + MO_DBG_ERR("could not find config %s", key16); + return false; + } + *valueOut = config->getString(); + success = true; + } + #endif + #if MO_ENABLE_V201 + if (context->getOcppVersion() == MO_OCPP_V201) { + if (!component201 || !name201) { + //disabled OCPP 2.0.1 compatibility + return true; + } + auto varSvc = context->getModel201().getVariableService(); + if (!varSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + auto variable = varSvc->getVariable(component201, name201); + if (!variable) { + MO_DBG_ERR("could not find variable %s %s", component201, name201); + return false; + } + *valueOut = variable->getString(); + success = true; + } + #endif + + return success; +} + +//bool mo_addCustomVarConfigInt(const char *component201, const char *name201, const char *key16, int (*getValue)(const char*, const char*, const char*, void*), void (*setValue)(int, const char*, const char*, const char*, void*), void *userData); +//bool mo_addCustomVarConfigBool(const char *component201, const char *name201, const char *key16, bool (*getValue)(const char*, const char*, const char*, void*), void (*setValue)(bool, const char*, const char*, const char*, void*), void *userData); +//bool mo_addCustomVarConfigString(const char *component201, const char *name201, const char *key16, const char* (*getValue)(const char*, const char*, const char*, void*), bool (*setValue)(const char*, const char*, const char*, const char*, void*), void *userData); + +MicroOcpp::Context *mo_getContext() { + return g_context; +} + +MicroOcpp::Context *mo_getContext2(MO_Context *ctx) { + return reinterpret_cast(ctx); +} + +MO_Context *mo_initialize2() { + auto context = new MicroOcpp::Context(); + if (!context) { + MO_DBG_ERR("OOM"); + return nullptr; + } + return reinterpret_cast(context); +} + +bool mo_setup2(MO_Context *ctx) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + return context->setup(); +} + +void mo_deinitialize2(MO_Context *ctx) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + auto context = mo_getContext2(ctx); + + delete context; +} + +void mo_loop2(MO_Context *ctx) { + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + auto context = mo_getContext2(ctx); + + context->loop(); +} + +//Send operation manually and bypass MO business logic +bool mo_sendRequest(MO_Context *ctx, const char *operationType, + const char *payloadJson, + void (*onResponse)(const char *payloadJson, void *userData), + void (*onAbort)(void *userData), + void *userData) { + + MicroOcpp::Operation *operation = nullptr; + std::unique_ptr request; + + MicroOcpp::Context *context = nullptr; + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + goto fail; + } + context = mo_getContext2(ctx); + + if (auto customOperation = new MicroOcpp::CustomOperation()) { + + if (!customOperation->setupEvseInitiated(operationType, payloadJson, onResponse, userData)) { + MO_DBG_ERR("create operation failure"); + delete customOperation; + goto fail; + } + + operation = static_cast(customOperation); + } else { + MO_DBG_ERR("OOM"); + goto fail; + } + + request = MicroOcpp::makeRequest(*context, operation); + if (!request) { + MO_DBG_ERR("OOM"); + goto fail; + } + + request->setOnAbort([onAbort, userData] () { + onAbort(userData); + }); + + request->setTimeout(20); + + context->getMessageService().sendRequest(std::move(request)); + return true; +fail: + delete operation; + if (onAbort) { + //onAbort execution is guaranteed to allow client to free up resources here + onAbort(userData); + } + return false; +} + +//Set custom operation handler for incoming reqeusts and bypass MO business logic +bool mo_setRequestHandler(MO_Context *ctx, const char *operationType, + void (*onRequest)(const char *operationType, const char *payloadJson, void **userStatus, void *userData), + int (*writeResponse)(const char *operationType, char *buf, size_t size, void *userStatus, void *userData), + void (*finally)(const char *operationType, void *userStatus, void *userData), + void *userData) { + + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = context->getMessageService().registerOperation(operationType, onRequest, writeResponse, finally, userData); + if (!success) { + MO_DBG_ERR("could not register operation %s", operationType); + } + return success; +} + +//Sniff incoming requests without control over the response +bool mo_setOnReceiveRequest(MO_Context *ctx, const char *operationType, + void (*onRequest)(const char *operationType, const char *payloadJson, void *userData), + void *userData) { + + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = context->getMessageService().setOnReceiveRequest(operationType, onRequest, userData); + if (!success) { + MO_DBG_ERR("could not set onRequest %s", operationType); + } + return success; +} + +//Sniff outgoing responses without control over the response +bool mo_setOnSendConf(MO_Context *ctx, const char *operationType, + void (*onSendConf)(const char *operationType, const char *payloadJson, void *userData), + void *userData) { + + if (!ctx) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto context = mo_getContext2(ctx); + + bool success = context->getMessageService().setOnSendConf(operationType, onSendConf, userData); + if (!success) { + MO_DBG_ERR("could not set onRequest %s", operationType); + } + return success; } diff --git a/src/MicroOcpp.h b/src/MicroOcpp.h index f15c13cc..1e19359e 100644 --- a/src/MicroOcpp.h +++ b/src/MicroOcpp.h @@ -5,127 +5,156 @@ #ifndef MO_MICROOCPP_H #define MO_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 -using MicroOcpp::OnReceiveConfListener; -using MicroOcpp::OnReceiveReqListener; -using MicroOcpp::OnSendConfListener; -using MicroOcpp::OnAbortListener; -using MicroOcpp::OnTimeoutListener; -using MicroOcpp::OnReceiveErrorListener; +/* + * Basic integration + */ + +//Begin the lifecycle of MO. Now, it is possible to set up the library. After configuring MO, +//call mo_setup() to finalize the setup +bool mo_initialize(); + +//End the lifecycle of MO and free all resources +void mo_deinitialize(); -#ifndef MO_CUSTOM_WS -//use links2004/WebSockets library +//Returns if library is initialized +bool mo_isInitialized(); +//Returns Context handle to pass around API functions +MO_Context *mo_getApiContext(); + +#if MO_USE_FILEAPI != MO_CUSTOM_FS +//Set if MO can use the filesystem and if it needs to mount it. See "FilesystemAdapter.h" for all options +void mo_setFilesystemConfig(MO_FilesystemOpt opt); +void mo_setFilesystemConfig2(MO_Context *ctx, MO_FilesystemOpt opt, const char *pathPrefix); +#endif //#if MO_USE_FILEAPI != MO_CUSTOM_FS + +#if MO_WS_USE == MO_WS_ARDUINO /* - * Initialize the library with the OCPP URL, EVSE voltage and filesystem configuration. - * - * If the connections fails, please refer to + * Setup MO with links2004/WebSockets library. Only available on Arduino, for other platforms set custom + * WebSockets adapter (see examples folder). `CA_cert` is zero-copy and must outlive the MO lifecycle. + * + * 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 +bool mo_setWebsocketUrl( + const char *backendUrl, //e.g. "wss://example.com:8443/steve/websocket/CentralSystemService". Must be defined + const char *chargeBoxId, //e.g. "charger001". Can be NULL + const char *authorizationKey, //authorizationKey present in the websocket message header. Can be NULL. Set this to enable OCPP Security Profile 2 + const char *CA_cert); //TLS certificate. Can be NULL. Zero-copy, must outlive MO. Set this to enable OCPP Security Profile 2 #endif -/* - * Convenience initialization: use this for passing the BootNotification payload JSON to the mocpp_initialize(...) below +#if __cplusplus +/* Set a WebSocket Client. Required if ArduinoWebsockets is not supported. This library requires + * that you handle establishing the connection and keeping it alive. MO does not take ownership + * of the passed `connection` object, i.e. it must be destroyed after `mo_deinitialize()` Please + * refer to https://github.com/matth-x/MicroOcpp/tree/main/examples/ESP-TLS for an example how to + * use it. * - * 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)); +void mo_setConnection(MicroOcpp::Connection *connection); +void mo_setConnection2(MO_Context *ctx, MicroOcpp::Connection *connection); +#endif + +//Set the OCPP version `MO_OCPP_V16` or `MO_OCPP_V201`. Works only if build flags `MO_ENABLE_V16` or +//`MO_ENABLE_V201` are set to 1 +void mo_setOcppVersion(int ocppVersion); +void mo_setOcppVersion2(MO_Context *ctx, int ocppVersion); + +//Set BootNotification fields (for more options, use `mo_setBootNotificationData2()`). For a description of +//the fields, refer to OCPP 1.6 or 2.0.1 specification +bool mo_setBootNotificationData( + const char *chargePointModel, //model name of this charger, e.g. "Demo Charger" + const char *chargePointVendor); //brand name, e.g. "My Company Ltd." + +bool mo_setBootNotificationData2(MO_Context *ctx, MO_BootNotificationData bnData); //see "BootNotificationData.h" for example usage -/* - * Stop the OCPP library and release allocated resources. - */ -void mocpp_deinitialize(); /* - * To be called in the main loop (e.g. place it inside loop()) + * 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 callbacks 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 following Input and passing it to the library. + * ``` + * mo_setEnergyMeterInput([] () { + * return e_reg; + * }); + * ``` + * + * An Output is a callback which gets 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 + * following Output and pass it to the library. + * ``` + * mo_setSmartChargingPowerOutput([] (float p_max) { + * pwm = p_max / PWM_FACTOR; //(simplified example) + * }); + * ``` + * + * Configure the library with Inputs and Outputs after mo_initialize() and before mo_setup(). */ -void mocpp_loop(); + +void mo_setConnectorPluggedInput(bool (*connectorPlugged)()); //Input about if an EV is plugged to this EVSE +void mo_setConnectorPluggedInput2(MO_Context *ctx, unsigned int evseId, bool (*connectorPlugged2)(unsigned int, void*), void *userData); //alternative with more args + +bool mo_setEnergyMeterInput(int32_t (*energyInput)()); //Input of the electricity meter register in Wh +bool mo_setEnergyMeterInput2(MO_Context *ctx, unsigned int evseId, int32_t (*energyInput2)(MO_ReadingContext, unsigned int, void*), void *userData); //alternative with more args + +bool mo_setPowerMeterInput(float (*powerInput)()); //Input of the power meter reading in W +bool mo_setPowerMeterInput2(MO_Context *ctx, unsigned int evseId, float (*powerInput2)(MO_ReadingContext, unsigned int, void*), void *userData); //alternative with more args + +#if MO_ENABLE_SMARTCHARGING +//Smart Charging Output, alternative for Watts only, Current only, or Watts x Current x numberPhases (x phaseToUse) +//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". +bool mo_setSmartChargingPowerOutput(void (*powerLimitOutput)(float)); //Output (in Watts) for the Smart Charging limit +bool mo_setSmartChargingCurrentOutput(void (*currentLimitOutput)(float)); //Output (in Amps) for the Smart Charging limit +bool mo_setSmartChargingOutput( + MO_Context *ctx, + unsigned int evseId, + void (*chargingLimitOutput)(MO_ChargeRate, unsigned int, void*), + bool powerSupported, //charge limit defined as power supported + bool currentSupported, //charge limit defined as current supported + bool phases3to1Supported, //charger supports switching from 3 to 1 phase during charge + bool phaseSwitchingSupported, //charger supports selecting the phase by server command. Ignored if OCPP 1.6 + void *userData); //Output (in Watts, Amps, numberPhases, phaseToUse) for the Smart Charging limit +#endif //MO_ENABLE_SMARTCHARGING + +//See section with advanced Inputs and Outputs further below + +//Finalize setup. After this, the library is ready to be used and mo_loop() can be run +bool mo_setup(); + +//Run library routines. To be called in the main loop (e.g. place it inside `loop()`) +void mo_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. @@ -137,24 +166,33 @@ void mocpp_loop(); * 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); +bool mo_beginTransaction(const char *idTag); +bool mo_beginTransaction2(MO_Context *ctx, unsigned int evseId, const char *idTag); -/* - * 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); +//Same as `mo_beginTransaction()`, but skip authorization. `parentIdTag` can be NULL +bool mo_beginTransaction_authorized(const char *idTag, const char *parentIdTag); +bool mo_beginTransaction_authorized2(MO_Context *ctx, unsigned int evseId, const char *idTag, const char *parentIdTag); //alternative with more args + +#if MO_ENABLE_V201 +//Same as mo_beginTransaction, but function name fits better for OCPP 2.0.1 and more options for 2.0.1. +//Attempt to authorize a pending transaction, or create new transaction to authorize. If local +//authorization is enabled, will search the local whitelist if server response takes too long. +//Backwards-compatible: if MO is initialized with 1.6, then this is the same as `mo_beginTransaction()` +bool mo_authorizeTransaction(const char *idToken); //idTokenType ISO14443 is assumed +bool mo_authorizeTransaction2(MO_Context *ctx, unsigned int evseId, const char *idToken, MO_IdTokenType type); +bool mo_authorizeTransaction3(MO_Context *ctx, unsigned int evseId, const char *idToken, MO_IdTokenType type, bool validateIdToken, const char *groupIdToken); +#endif //MO_ENABLE_V201 /* * OCPP 1.6 (2.0.1 see below): @@ -169,44 +207,63 @@ bool beginTransaction_authorized(const char *idTag, const char *parentIdTag = nu * 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); +bool mo_endTransaction(const char *idTag, const char *reason); //idTag and reason can be NULL +bool mo_endTransaction2(MO_Context *ctx, unsigned int evseId, const char *idTag, const char *reason); + +//Same as `endTransaction()`, but skip authorization +bool mo_endTransaction_authorized(const char *idTag, const char *reason); //idTag and reason can be NULL +bool mo_endTransaction_authorized2(MO_Context *ctx, unsigned int evseId, const char *idTag, const char *reason); + +#if MO_ENABLE_V201 +//Same as mo_endTransaction, but function name fits better for OCPP 2.0.1 and more options for 2.0.1. +//Backwards-compatible: if MO is initialized with 1.6, then this is the same as `mo_endTransaction()` +bool mo_deauthorizeTransaction(const char *idToken); //idTokenType ISO14443 is assumed +bool mo_deauthorizeTransaction2(MO_Context *ctx, unsigned int evseId, const char *idToken, MO_IdTokenType type); +bool mo_deauthorizeTransaction3(MO_Context *ctx, unsigned int evseId, const char *idToken, MO_IdTokenType type, bool validateIdToken, const char *groupIdToken); + +//Force transaction stop and set stoppedReason and stopTrigger correspondingly +//Backwards-compatible: if MO is initialized with 1.6, then this is the same as `mo_endTransaction()` +//and attempts to map 2.0.1 `stoppedReason` to 1.6 `reason` +bool mo_abortTransaction(MO_TxStoppedReason stoppedReason, MO_TxEventTriggerReason stopTrigger); +bool mo_abortTransaction2(MO_Context *ctx, unsigned int evseId, MO_TxStoppedReason stoppedReason, MO_TxEventTriggerReason stopTrigger); +#endif //MO_ENABLE_V201 /* - * 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. + * Returns if according to OCPP, the EVSE is allowed to close provide charge now. + * + * 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 endTransaction_authorized(const char *idTag, const char *reason = nullptr, unsigned int connectorId = 1); +bool mo_ocppPermitsCharge(); +bool mo_ocppPermitsCharge2(MO_Context *ctx, unsigned int evseId); /* * Get information about the current Transaction lifecycle. A transaction can enter the following @@ -217,9 +274,9 @@ bool endTransaction_authorized(const char *idTag, const char *reason = nullptr, * - 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 @@ -228,356 +285,373 @@ bool endTransaction_authorized(const char *idTag, const char *reason = nullptr, * Finished / Aborted | | * / Idle | false | false */ -bool isTransactionActive(unsigned int connectorId = 1); -bool isTransactionRunning(unsigned int connectorId = 1); +bool mo_isTransactionActive(); +bool mo_isTransactionActive2(MO_Context *ctx, unsigned int evseId); +bool mo_isTransactionRunning(); +bool mo_isTransactionRunning2(MO_Context *ctx, unsigned int evseId); /* * 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); +const char *mo_getTransactionIdTag(); +const char *mo_getTransactionIdTag2(MO_Context *ctx, unsigned int evseId); #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); +//Get the transactionId of the currently pending transaction, formatted as c-string. Returns NULL +//if no transaction is running. The returned string must be copied into own buffer, as it may be +//invalidated after call. +//Backwards-compatible: if initialized with OCPP 1.6, returns txId formatted as c-string +const char *mo_getTransactionId(); +const char *mo_getTransactionId2(MO_Context *ctx, unsigned int evseId); #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); +#if MO_ENABLE_V16 +//Get the transactionId once the StartTransaction.conf from the server has arrived. Otherwise, +//returns -1 +int mo_v16_getTransactionId(); +int mo_v16_getTransactionId2(MO_Context *ctx, unsigned int evseId); +#endif //MO_ENABLE_V16 /* - * Returns the latest ChargePointStatus as reported via StatusNotification (standard OCPP data type) + * Returns the latest MO_ChargePointStatus as reported via StatusNotification (standard OCPP data type) */ -ChargePointStatus getChargePointStatus(unsigned int connectorId = 1); +MO_ChargePointStatus mo_getChargePointStatus(); +MO_ChargePointStatus mo_getChargePointStatus2(MO_Context *ctx, unsigned int evseId); + /* - * 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. + * Define the Inputs and Outputs of this library. (Advanced) + * + * These Inputs and Outputs are optional depending on the use case of your charger. Set before `mo_setup()` */ -void setConnectorPluggedInput(std::function pluggedInput, unsigned int connectorId = 1); //Input about if an EV is plugged to this EVSE +void mo_setEvReadyInput(bool (*evReadyInput)()); //Input if EV is ready to charge (= J1772 State C) +void mo_setEvReadyInput2(MO_Context *ctx, unsigned int evseId, bool (*evReadyInput2)(unsigned int, void*), void *userData); //alternative with more args -void setEnergyMeterInput(std::function energyInput, unsigned int connectorId = 1); //Input of the electricity meter register in Wh +void mo_setEvseReadyInput(bool (*evseReadyInput)()); //Input if EVSE allows charge (= PWM signal on) +void mo_setEvseReadyInput2(MO_Context *ctx, unsigned int evseId, bool (*evseReadyInput2)(unsigned int, void*), void *userData); //alternative with more args -void setPowerMeterInput(std::function powerInput, unsigned int connectorId = 1); //Input of the power meter reading in W +bool mo_addErrorCodeInput(const char* (*errorCodeInput)()); //Input for Error codes (please refer to OCPP 1.6, Edit2, p. 71 and 72 for valid error codes) -//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 +#if MO_ENABLE_V16 +//Input for Error codes + additional error data. See "Availability.h" for more docs +bool mo_v16_addErrorDataInput(MO_Context *ctx, unsigned int evseId, MO_ErrorData (*errorData)(unsigned int, void*), void *userData); +#endif //MO_ENABLE_V16 -/* - * 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) +#if MO_ENABLE_V201 +//Input to set EVSE into Faulted state +bool mo_v201_addFaultedInput(MO_Context *ctx, unsigned int evseId, bool (*faulted)(unsigned int, void*), void *userData); +#endif //MO_ENABLE_V201 -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); +//Input for further integer MeterValue types +bool mo_addMeterValueInputInt(int32_t (*meterInput)(), const char *measurand, const char *unit, const char *location, const char *phase); +bool mo_addMeterValueInputInt2(MO_Context *ctx, unsigned int evseId, int32_t (*meterInput)(MO_ReadingContext, unsigned int, void*), const char *measurand, const char *unit, const char *location, const char *phase, void *userData); -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 +//Input for further float MeterValue types +bool mo_addMeterValueInputFloat(float (*meterInput)(), const char *measurand, const char *unit, const char *location, const char *phase); +bool mo_addMeterValueInputFloat2(MO_Context *ctx, unsigned int evseId, float (*meterInput)(MO_ReadingContext, unsigned int, void*), const char *measurand, const char *unit, const char *location, const char *phase, void *userData); -void addMeterValueInput(std::unique_ptr valueInput, unsigned int connectorId = 1); //integrate further metering Inputs (more extensive alternative) +//Input for further string MeterValue types. `meterInput` shall write the value into `buf` (which is `size` large) and return the number +//of characters written (not including the terminating 0). See "MeterValue.h" for a full description +bool mo_addMeterValueInputString(int (*meterInput)(char *buf, size_t size), const char *measurand, const char *unit, const char *location, const char *phase); +bool mo_addMeterValueInputString2(MO_Context *ctx, unsigned int evseId, int (*meterInput)(char *buf, size_t size, MO_ReadingContext, unsigned int, void*), const char *measurand, const char *unit, const char *location, const char *phase, void *userData); -void setOccupiedInput(std::function occupied, unsigned int connectorId = 1); //Input if instead of Available, send StatusNotification Preparing / Finishing +//Input for further MeterValue types with more options. See "MeterValue.h" for how to use it +bool mo_addMeterValueInput(MO_Context *ctx, unsigned int evseId, MO_MeterInput meterValueInput); -void setStartTxReadyInput(std::function startTxReady, unsigned int connectorId = 1); //Input if the charger is ready for StartTransaction +//Input if instead of Available, send StatusNotification Preparing / Finishing (OCPP 1.6) or Occupied (OCPP 2.0.1) +void mo_setOccupiedInput(bool (*occupied)()); +void mo_setOccupiedInput2(MO_Context *ctx, unsigned int evseId, bool (*occupied2)(unsigned int, void*), void *userData); -void setStopTxReadyInput(std::function stopTxReady, unsigned int connectorId = 1); //Input if charger is ready for StopTransaction +void mo_setStartTxReadyInput(bool (*startTxReady)()); //Input if the charger is ready for StartTransaction +void mo_setStartTxReadyInput2(MO_Context *ctx, unsigned int evseId, bool (*startTxReady2)(unsigned int, void*), void *userData); -void setTxNotificationOutput(std::function notificationOutput, unsigned int connectorId = 1); //called when transaction state changes (see TxNotification for possible events). Transaction can be null +void mo_setStopTxReadyInput(bool (*stopTxReady)()); //Input if charger is ready for StopTransaction +void mo_setStopTxReadyInput2(MO_Context *ctx, unsigned int evseId, bool (*stopTxReady2)(unsigned int, void*), void *userData); -#if MO_ENABLE_V201 -void setTxNotificationOutputV201(std::function notificationOutput, unsigned int connectorId = 1); -#endif //MO_ENABLE_V201 +//Called when transaction state changes (see "TransactionDefs.h" for possible events). Transaction can be null +void mo_setTxNotificationOutput(void (*txNotificationOutput)(MO_TxNotification)); +void mo_setTxNotificationOutput2(MO_Context *ctx, unsigned int evseId, void (*txNotificationOutput2)(MO_TxNotification, unsigned int, void*), void *userData); #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) + * - MO_UnlockConnectorResult_Pending if action needs more time to complete (MO will call this cb again later or eventually time out) + * - MO_UnlockConnectorResult_Unlocked if successful + * - MO_UnlockConnectorResult_UnlockFailed if not successful (e.g. lock stuck) */ -void setOnUnlockConnectorInOut(std::function onUnlockConnectorInOut, unsigned int connectorId = 1); +void mo_setOnUnlockConnector(MO_UnlockConnectorResult (*onUnlockConnector)()); +void mo_setOnUnlockConnector2(MO_Context *ctx, unsigned int evseId, MO_UnlockConnectorResult (*onUnlockConnector2)(unsigned int, void*), void *userData); #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 +bool mo_isOperative(); //if the charge point is operative (see OCPP1.6 Edit2, p. 45) and ready for transactions +bool mo_isOperative2(MO_Context *ctx, unsigned int evseId); -/* - * Configure the device management - */ +bool mo_isConnected(); //Returns WebSocket connection state +bool mo_isConnected2(MO_Context *ctx); -void setOnResetNotify(std::function onResetNotify); //call onResetNotify(isHard) before Reset. If you return false, Reset will be aborted. Optional +bool mo_isAcceptedByCsms(); //Returns if BootNotification has succeeded +bool mo_isAcceptedByCsms2(MO_Context *ctx); -void setOnResetExecute(std::function onResetExecute); //reset handler. This function should reboot this controller immediately. Already defined for the ESP32 on Arduino +int32_t mo_getUnixTime(); //Returns unix time (seconds since 1970-01-01 if known, 0 if unknown) +int32_t mo_getUnixTime2(MO_Context *ctx); +bool mo_setUnixTime(int32_t unixTime); //If the charger has an RTC, pre-init MO with the RTC time. On first connection, MO will sync with the server +bool mo_setUnixTime2(MO_Context *ctx, int32_t unixTime); +int32_t mo_getUptime(); //Returns seconds since boot +int32_t mo_getUptime2(MO_Context *ctx); -namespace MicroOcpp { -class FirmwareService; -class DiagnosticsService; -} +int mo_getOcppVersion(); //Returns either `MO_OCPP_V16` or `MO_OCPP_V201` +int mo_getOcppVersion2(MO_Context *ctx); /* - * 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 ` + * Configure the device management. Set before `mo_setup()` */ -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(); +//Reset handler. `onResetExecute` should reboot the controller. Already implemented for +//the ESP32 on Arduino +//Simpler reset handler version which supports OCPP 1.6 and 2.0.1 +void mo_setOnResetExecute(void (*onResetExecute)()); + +#if MO_ENABLE_V16 +//MO calls onResetNotify(isHard) before Reset. If you return false, Reset will be aborted. Optional for Reset feature +void mo_v16_setOnResetNotify(bool (*onResetNotify)(bool)); +void mo_v16_setOnResetNotify2(MO_Context *ctx, bool (*onResetNotify2)(bool, void*), void *userData); + +//Reset handler. This function should reboot this controller immediately. Already defined for the ESP32 on Arduino +void mo_v16_setOnResetExecute(void (*onResetExecute)(bool)); +void mo_v16_setOnResetExecute2(MO_Context *ctx, bool (*onResetExecute2)(bool, void*), void *userData); +#endif //MO_ENABLE_V16 + +#if MO_ENABLE_V201 +//MO calls onResetNotify(isHard) before Reset. If you return false, Reset will be aborted. Optional for Reset feature +void mo_v201_setOnResetNotify(bool (*onResetNotify)(MO_ResetType)); +void mo_v201_setOnResetNotify2(MO_Context *ctx, unsigned int evseId, bool (*onResetNotify2)(MO_ResetType, unsigned int, void*), void *userData); + +//Reset handler. This function should reboot this controller immediately. Already defined for the ESP32 on Arduino +void mo_v201_setOnResetExecute(void (*onResetExecute)()); //identical to `mo_setOnResetExecute` +void mo_v201_setOnResetExecute2(MO_Context *ctx, unsigned int evseId, bool (*onResetExecute2)(unsigned int, void*), void *userData); +#endif //MO_ENABLE_V201 -#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. + * Further platform configurations (Advanced) */ -void setCertificateStore(std::unique_ptr certStore); -#endif //MO_ENABLE_CERT_MGMT + +//Set custom debug function +//`setDebugCb`: MO calls `debugCb` with full lines of formatted debug messages +//`setDebugCb2`: MO calls `debugCb2` with the raw debug message, the file and line where it originated and debug level +void mo_setDebugCb(void (*debugCb)(const char *msg)); +void mo_setDebugCb2(MO_Context *ctx, void (*debugCb2)(int lvl, const char *fn, int line, const char *msg)); + +//Set custom timer function which returns the number of milliseconds elapsed since boot. Note: the return value +//of this function must roll over from (2^32 - 1) to 0. It cannot roll over from an earlier value, otherwise it +//would break the unsigned integer arithmetics which MO relies on. This is a common pitfall if the platform +//tick frequency is not 1 tick per ms. Then a helper function is needed which counts the ticks up to 2^32 before +//rolling over. +void mo_setTicksCb(uint32_t (*ticksCb)()); +void mo_setTicksCb2(MO_Context *ctx, uint32_t (*ticksCb)()); + +//Set custom random number generator function. By default, MO uses a pseudo-RNG which with the system time as +//the only entropy source. Using a true RNG greatly improves security. +void mo_setRngCb(uint32_t (*rngCb)()); +void mo_setRngCb2(MO_Context *ctx, uint32_t (*rngCb)()); + +//Returns the internal filesystem adapter or NULL if the initialization failed. Need to set configs using +//via `mo_setFilesystemConfig` before accessing the FS adpater. The FS adapter can be passed to other +//MO utils functions or be used for other purposes than OCPP, as a file abstraction layer +MO_FilesystemAdapter *mo_getFilesystem(); +MO_FilesystemAdapter *mo_getFilesystem2(MO_Context *ctx); + +#if MO_ENABLE_MBEDTLS +//Set FTP security parameters (e.g. client cert). See "FtpMbedTLS.h" for all options +//To use a custom FTP client, subclass `FtpClient` (see "Ftp.h") and pass to C++ `Context` object +void mo_setFtpConfig2(MO_Context *ctx, MO_FTPConfig ftpConfig); + +#if MO_ENABLE_DIAGNOSTICS +//Provide MO with diagnostics data for the "GetDiagnostics" operation. Should contain human-readable text which +//is useful for troubleshooting, e.g. heap occupation, all sensor readings, any device state and config which +//isn't directly exposed via OCPP operation. +//For more documentation, see "DiagnosticsService.h" +void mo_setDiagnosticsReader(MO_Context *ctx, + size_t (*readBytes)(char *buf, size_t size, void *user_data), //reads diags into `buf`, having `size` bytes + void(*onClose)(void *user_data), //MO calls this when diags upload is terminated + void *userData); //implementer-defined pointer + +void mo_setDiagnosticsFtpServerCert(MO_Context *ctx, const char *cert); //zero-copy, i.e. cert must outlive MO +#endif //MO_ENABLE_DIAGNOSTICS + +#if MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT +//Implement custom Firmware upgrade. MO downloads the firmware binary via FTP and passes it chunk by chunk to +//the `writeBytes` callback. After the FTP download, MO terminates the firmware upgrade by calling `onClose`. +//For more documentation, see "FirmwareService.h" +//void mo_setFirmwareDownloadWriter( +// size_t (*writeBytes)(const unsigned char *buf, size_t size, void *userData), //writes `buf`, having `size` bytes to OTA partition +// void (*onClose)(MO_FtpCloseReason, void *userData), //MO calls this when the FTP was closed +// void *userData); //implementer-defined pointer +// +//void mo_setFirmwareFtpServerCert(const char *cert); //zero-copy, i.e. cert must outlive MO +#endif //MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT + +#endif //MO_ENABLE_MBEDTLS /* - * Add features and customize the behavior of the OCPP client + * Transaction management (Advanced) */ -namespace MicroOcpp { -class Context; -} +bool mo_isTransactionAuthorized(); +bool mo_isTransactionAuthorized2(MO_Context *ctx, unsigned int evseId); -//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(); +//Returns if server has rejected transaction in StartTx or TransactionEvent response +bool mo_isTransactionDeauthorized(); +bool mo_isTransactionDeauthorized2(MO_Context *ctx, unsigned int evseId); + +//Returns start unix time of transaction. Returns 0 if unix time is not known +int32_t mo_getTransactionStartUnixTime(); +int32_t mo_getTransactionStartUnixTime2(MO_Context *ctx, unsigned int evseId); + +#if MO_ENABLE_V16 +//Overrides start unix time of transaction +void mo_setTransactionStartUnixTime(int32_t unixTime); +void mo_setTransactionStartUnixTime2(MO_Context *ctx, unsigned int evseId, int32_t unixTime); +//Returns stop unix time of transaction. Returns 0 if unix time is not known +int32_t mo_getTransactionStopUnixTime(); +int32_t mo_getTransactionStopUnixTime2(MO_Context *ctx, unsigned int evseId); + +//Overrides stop unix time of transaction +void mo_setTransactionStopUnixTime(int32_t unixTime); +void mo_setTransactionStopUnixTime2(MO_Context *ctx, unsigned int evseId, int32_t unixTime); + +//Returns meterStart of transaction. Returns -1 if meterStart is undefined +int32_t mo_getTransactionMeterStart(); +int32_t mo_getTransactionMeterStart2(MO_Context *ctx, unsigned int evseId); + +//Overrides meterStart of transaction +void mo_setTransactionMeterStart(int32_t meterStart); +void mo_setTransactionMeterStart2(MO_Context *ctx, unsigned int evseId, int32_t meterStart); + +//Returns meterStop of transaction. Returns -1 if meterStop is undefined +int32_t mo_getTransactionMeterStop(); +int32_t mo_getTransactionMeterStop2(MO_Context *ctx, unsigned int evseId); + +//Overrides meterStop of transaction +void mo_setTransactionMeterStop(int32_t meterStop); +void mo_setTransactionMeterStop2(MO_Context *ctx, unsigned int evseId, int32_t meterStop); +#endif //MO_ENABLE_V16 + + +#if MO_ENABLE_V16 /* - * 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"]); - * }); + * Access OCPP Configurations (OCPP 1.6) */ -void setOnReceiveRequest(const char *operationType, OnReceiveReqListener onReceiveReq); +//Add new Config. `key` is zero-copy, i.e. must outlive the MO lifecycle. Add before `mo_setup()`. +//At first boot, MO uses `factoryDefault`. At subsequent boots, it loads the values from flash +//during `mo_setup()` +bool mo_declareConfigurationInt(const char *key, int factoryDefault, MO_Mutability mutability, bool persistent, bool rebootRequired); +bool mo_declareConfigurationBool(const char *key, bool factoryDefault, MO_Mutability mutability, bool persistent, bool rebootRequired); +bool mo_declareConfigurationString(const char *key, const char *factoryDefault, MO_Mutability mutability, bool persistent, bool rebootRequired); + +//Set Config value. Set after `mo_setup()` +bool mo_setConfigurationInt(const char *key, int value); +bool mo_setConfigurationBool(const char *key, bool value); +bool mo_setConfigurationString(const char *key, const char *value); + +//Get Config value. MO writes the value into `valueOut`. Call after `mo_setup()` +bool mo_getConfigurationInt(const char *key, int *valueOut); +bool mo_getConfigurationBool(const char *key, bool *valueOut); +bool mo_getConfigurationString(const char *key, const char **valueOut); //need to copy `valueOut` into own buffer. `valueOut` may be invalidated after call +#endif //MO_ENABLE_V16 /* - * 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 - * } - * }); - * + * Access OCPP Configurations (OCPP 1.6) or Variables (OCPP 2.0.1) + * Backwards-compatible: if MO is initialized with OCPP 1.6, then these functions access Configurations */ -void setOnSendConf(const char *operationType, OnSendConfListener onSendConf); +//Add new Variable or Config. If running OCPP 2.0.1, `key16` can be NULL. If running OCPP 1.6, +//`component201` and `name201` can be NULL. `component201`, `name201` and `key16` are zero-copy, +//i.e. must outlive the MO lifecycle. Add before `mo_setup()`. At first boot, MO uses `factoryDefault`. +//At subsequent boots, it loads the values from flash during `mo_setup()` +bool mo_declareVarConfigInt(MO_Context *ctx, const char *component201, const char *name201, const char *key16, int factoryDefault, MO_Mutability mutability, bool persistent, bool rebootRequired); +bool mo_declareVarConfigBool(MO_Context *ctx, const char *component201, const char *name201, const char *key16, bool factoryDefault, MO_Mutability mutability, bool persistent, bool rebootRequired); +bool mo_declareVarConfigString(MO_Context *ctx, const char *component201, const char *name201, const char *key16, const char *factoryDefault, MO_Mutability mutability, bool persistent, bool rebootRequired); + +//Set Variable or Config value. Set after `mo_setup()`. If running OCPP 2.0.1, `key16` can be NULL +bool mo_setVarConfigInt(MO_Context *ctx, const char *component201, const char *name201, const char *key16, int value); +bool mo_setVarConfigBool(MO_Context *ctx, const char *component201, const char *name201, const char *key16, bool value); +bool mo_setVarConfigString(MO_Context *ctx, const char *component201, const char *name201, const char *key16, const char *value); + +//Get Config value. MO writes the value into `valueOut`. Call after `mo_setup()`. If running +//OCPP 2.0.1, `key16` can be NULL +bool mo_getVarConfigInt(MO_Context *ctx, const char *component201, const char *name201, const char *key16, int *valueOut); +bool mo_getVarConfigBool(MO_Context *ctx, const char *component201, const char *name201, const char *key16, bool *valueOut); +bool mo_getVarConfigString(MO_Context *ctx, const char *component201, const char *name201, const char *key16, const char **valueOut); //need to copy `valueOut` into own buffer. `valueOut` may be invalidated after call /* - * 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. + * Callback-based Config or Variable integration (shared API between OCPP 1.6 and 2.0.1). This + * allows to implement custom key-value stores and bypass the built-in implementation of MO. + * MO uses `getValue` to read the Config/Variable value and `setValue` to update the value after + * receiving a ChangeConfiguration or SetVariables request. */ -void sendRequest(const char *operationType, - std::function ()> fn_createReq, - std::function fn_processConf); +//bool mo_addCustomVarConfigInt(const char *component201, const char *name201, const char *key16, int (*getValue)(const char*, const char*, const char*, void*), void (*setValue)(int, const char*, const char*, const char*, void*), void *userData); +//bool mo_addCustomVarConfigBool(const char *component201, const char *name201, const char *key16, bool (*getValue)(const char*, const char*, const char*, void*), void (*setValue)(bool, const char*, const char*, const char*, void*), void *userData); +//bool mo_addCustomVarConfigString(const char *component201, const char *name201, const char *key16, const char* (*getValue)(const char*, const char*, const char*, void*), bool (*setValue)(const char*, const char*, const char*, const char*, void*), void *userData); /* - * 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; - * }); + * Add features and customize the behavior of the OCPP client */ -void setRequestHandler(const char *operationType, - std::function fn_processReq, - std::function ()> fn_createConf); + +#if __cplusplus +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 *mo_getContext(); +MicroOcpp::Context *mo_getContext2(MO_Context *ctx); +#endif //__cplusplus /* - * 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. + * Explicit context lifecycle. Avoids the internal `g_context` variable. Use if: + * - Explicit lifecycle is preferred (may fit the architecture better) + * - To run multiple instances of MO in parallel */ - -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); +MO_Context *mo_initialize2(); +bool mo_setup2(MO_Context *ctx); +void mo_deinitialize2(MO_Context *ctx); +void mo_loop2(MO_Context *ctx); + +//Send operation manually and bypass MO business logic +bool mo_sendRequest(MO_Context *ctx, + const char *operationType, //zero-copy, i.e. must outlive MO + const char *payloadJson, //copied, i.e. can be invalidated + void (*onResponse)(const char *payloadJson, void *userData), + void (*onAbort)(void *userData), + void *userData); + +//Set custom operation handler for incoming reqeusts and bypass MO business logic +bool mo_setRequestHandler(MO_Context *ctx, const char *operationType, + void (*onRequest)(const char *operationType, const char *payloadJson, void **userStatus, void *userData), + int (*writeResponse)(const char *operationType, char *buf, size_t size, void *userStatus, void *userData), + void (*finally)(const char *operationType, void *userStatus, void *userData), + void *userData); + +//Sniff incoming requests without control over the response +bool mo_setOnReceiveRequest(MO_Context *ctx, const char *operationType, + void (*onRequest)(const char *operationType, const char *payloadJson, void *userData), + void *userData); + +//Sniff outgoing responses generated by MO +bool mo_setOnSendConf(MO_Context *ctx, const char *operationType, + void (*onSendConf)(const char *operationType, const char *payloadJson, void *userData), + void *userData); #endif diff --git a/src/MicroOcpp/Context.cpp b/src/MicroOcpp/Context.cpp new file mode 100644 index 00000000..f6890930 --- /dev/null +++ b/src/MicroOcpp/Context.cpp @@ -0,0 +1,400 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace MicroOcpp; + +Context::Context() : MemoryManaged("Context") { + + (void)debug.setup(); //initialize with default debug function. Can replace later + +#if MO_USE_FILEAPI != MO_CUSTOM_FS + memset(&filesystemConfig, 0, sizeof(filesystemConfig)); +#endif //MO_USE_FILEAPI != MO_CUSTOM_FS + +#if MO_WS_USE != MO_WS_CUSTOM + mo_connectionConfig_init(&connectionConfig); +#endif //MO_WS_USE != MO_WS_CUSTOM + +#if MO_ENABLE_MBEDTLS + memset(&ftpConfig, 0, sizeof(ftpConfig)); +#endif //MO_ENABLE_MBEDTLS +} + +Context::~Context() { + // reset resources (frees resources if owning them) + setFilesystem(nullptr); + setConnection(nullptr); + setFtpClient(nullptr); + setCertificateStore(nullptr); + +#if MO_USE_FILEAPI != MO_CUSTOM_FS + mo_filesystemConfig_deinit(&filesystemConfig); + filesystemConfigDefined = false; +#endif //MO_USE_FILEAPI != MO_CUSTOM_FS + +#if MO_WS_USE != MO_WS_CUSTOM + mo_connectionConfig_deinit(&connectionConfig); +#endif //MO_WS_USE != MO_WS_CUSTOM + + MO_DBG_INFO("MicroOCPP deinitialized\n"); +} + +void Context::setDebugCb(void (*debugCb)(const char *msg)) { + debug.setDebugCb(debugCb); // `debug` globally declared in Debug.h +} + +void Context::setDebugCb2(void (*debugCb2)(int lvl, const char *fn, int line, const char *msg)) { + debug.setDebugCb2(debugCb2); // `debug` globally declared in Debug.h +} + +void Context::setTicksCb(uint32_t (*ticksCb)()) { + this->ticksCb = ticksCb; +} + +uint32_t Context::getTicksMs() { + if (this->ticksCb) { + return this->ticksCb(); + } else { + MO_DBG_ERR("ticksCb not set yet"); + return 0; + } +} + +void Context::setRngCb(uint32_t (*rngCb)()) { + this->rngCb = rngCb; +} + +uint32_t (*Context::getRngCb())() { + if (!rngCb) { + rngCb = getDefaultRngCb(); + } + return rngCb; +} + +#if MO_USE_FILEAPI != MO_CUSTOM_FS +bool Context::setFilesystemConfig(MO_FilesystemConfig filesystemConfig) { + if (!mo_filesystemConfig_copy(&this->filesystemConfig, &filesystemConfig)) { + MO_DBG_ERR("OOM"); + return false; + } + filesystemConfigDefined = true; + return true; +} +#endif //MO_USE_FILEAPI != MO_CUSTOM_FS + +void Context::setFilesystem(MO_FilesystemAdapter *filesystem) { +#if MO_USE_FILEAPI != MO_CUSTOM_FS + if (this->filesystem && isFilesystemOwner) { + mo_freeDefaultFilesystemAdapter(this->filesystem); + this->filesystem = nullptr; + isFilesystemOwner = false; + } +#endif //MO_USE_FILEAPI != MO_CUSTOM_FS + this->filesystem = filesystem; +} + +MO_FilesystemAdapter *Context::getFilesystem() { + #if MO_USE_FILEAPI != MO_CUSTOM_FS + if (!filesystem && filesystemConfigDefined) { + // init default FS implementaiton + filesystem = mo_makeDefaultFilesystemAdapter(filesystemConfig); + if (!filesystem) { + //nullptr if either FS is disabled or OOM. If OOM, then err msg is already printed + return nullptr; + } + isFilesystemOwner = true; + mo_filesystemConfig_deinit(&filesystemConfig); + filesystemConfigDefined = false; + } + #endif //MO_USE_FILEAPI != MO_CUSTOM_FS + + return filesystem; +} + +#if MO_WS_USE != MO_WS_CUSTOM +bool Context::setConnectionConfig(MO_ConnectionConfig connectionConfig) { + if (!mo_connectionConfig_copy(&this->connectionConfig, &connectionConfig)) { + MO_DBG_ERR("OOM"); + return false; + } + connectionConfigDefined = true; + return true; +} +#endif //MO_WS_USE != MO_WS_CUSTOM + +void Context::setConnection(Connection *connection) { + if (this->connection) { + this->connection->setContext(nullptr); + } +#if MO_WS_USE != MO_WS_CUSTOM + if (this->connection && isConnectionOwner) { + freeDefaultConnection(this->connection); + this->connection = nullptr; + isConnectionOwner = false; + } +#endif //MO_WS_USE != MO_WS_CUSTOM + this->connection = connection; + if (this->connection) { + this->connection->setContext(this); + } +} + +Connection *Context::getConnection() { + #if MO_WS_USE != MO_WS_CUSTOM + if (!connection && connectionConfigDefined) { + // init default Connection implementation + connection = static_cast(makeDefaultConnection(connectionConfig, ocppVersion)); + if (!connection) { + MO_DBG_ERR("OOM"); + return nullptr; + } + isConnectionOwner = true; + mo_connectionConfig_deinit(&connectionConfig); + } + #endif //MO_WS_USE != MO_WS_CUSTOM + return connection; +} + +#if MO_ENABLE_MBEDTLS +void Context::setFtpConfig(MO_FTPConfig ftpConfig) { + this->ftpConfig = ftpConfig; + ftpConfigDefined = true; +} +#endif //MO_ENABLE_MBEDTLS + +void Context::setFtpClient(FtpClient *ftpClient) { + #if MO_ENABLE_MBEDTLS + if (this->ftpClient && isFtpClientOwner) { + delete this->ftpClient; + this->ftpClient = nullptr; + isFtpClientOwner = false; + } + #endif //MO_ENABLE_MBEDTLS + this->ftpClient = ftpClient; +} + +FtpClient *Context::getFtpClient() { + #if MO_ENABLE_MBEDTLS + if (!ftpClient && ftpConfigDefined) { + ftpClient = makeFtpClientMbedTLS(ftpConfig).release(); + if (!ftpClient) { + MO_DBG_ERR("OOM"); + return nullptr; + } + isFtpClientOwner = true; + } + #endif //MO_ENABLE_MBEDTLS + return ftpClient; +} + +void Context::setCertificateStore(CertificateStore *certStore) { + if (this->certStore && isCertStoreOwner) { + delete this->certStore; + this->certStore = nullptr; + isCertStoreOwner = false; + } + this->certStore = certStore; +} + +CertificateStore *Context::getCertificateStore() { + #if MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS + if (!certStore && filesystem) { + certStore = makeCertificateStoreMbedTLS(filesystem).release(); + if (!certStore) { + MO_DBG_ERR("OOM"); + return nullptr; + } + isCertStoreOwner = true; + } + #endif //MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS + return certStore; +} + +Clock& Context::getClock() { + return clock; +} + +MessageService& Context::getMessageService() { + return msgService; +} + +bool Context::setOcppVersion(int ocppVersion) { + if (ocppVersion == MO_OCPP_V16 && MO_ENABLE_V16) { + this->ocppVersion = ocppVersion; + return true; + } else if (ocppVersion == MO_OCPP_V201 && MO_ENABLE_V201) { + this->ocppVersion = ocppVersion; + return true; + } else { + MO_DBG_ERR("unsupported OCPP version"); + return false; + } +} + +int Context::getOcppVersion() { + return ocppVersion; +} + +#if MO_ENABLE_V16 +v16::Model& Context::getModel16() { + return modelV16; +} +#endif + +#if MO_ENABLE_V201 +v201::Model& Context::getModel201() { + return modelV201; +} +#endif + +#if MO_ENABLE_V16 || MO_ENABLE_V201 +ModelCommon& Context::getModelCommon() { + #if MO_ENABLE_V16 && MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V16) { + return static_cast(modelV16); + } else if (ocppVersion == MO_OCPP_V201) { + return static_cast(modelV201); + } else { + MO_DBG_ERR("OCPP version undefined"); + return static_cast(modelV16); + } + #elif MO_ENABLE_V16 + { + return static_cast(modelV16); + } + #elif MO_ENABLE_V201 + { + return static_cast(modelV201); + } + #endif +} +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 + +#if MO_ENABLE_V16 && !MO_ENABLE_V201 +Model& Context::getModel() { + return getModel16(); +} +#elif !MO_ENABLE_V16 && MO_ENABLE_V201 +Model& Context::getModel() { + return getModel201(); +} +#endif + +bool Context::setup() { + + if (!debug.setup()) { + MO_DBG_ERR("must set debugCb before"); //message won't show up on console + return false; + } + + if (!ticksCb) { + ticksCb = getDefaultTickCb(); + + if (!ticksCb) { + MO_DBG_ERR("must set TicksCb connection before setup. See the examples in this repository"); + return false; + } + } + + if (!getRngCb()) { + MO_DBG_ERR("random number generator cannot be found"); + return false; + } + + #if MO_USE_FILEAPI != MO_CUSTOM_FS + if (!filesystemConfigDefined) { + //set defaults + mo_filesystemConfig_init(&filesystemConfig); + filesystemConfig.opt = MO_FS_OPT_USE_MOUNT; + filesystemConfig.path_prefix = MO_FILENAME_PREFIX; + filesystemConfigDefined = true; + } + #endif //MO_USE_FILEAPI != MO_CUSTOM_FS + + if (!getFilesystem()) { + MO_DBG_DEBUG("initialize MO without filesystem access"); + } + + if (!getConnection()) { + MO_DBG_ERR("must set WebSocket connection before setup. See the examples in this repository"); + return false; + } + + #if MO_ENABLE_MBEDTLS + if (!ftpConfigDefined) { + //set defaults + ftpConfigDefined = true; + } + #endif //MO_ENABLE_MBEDTLS + + if (!getFtpClient()) { + MO_DBG_DEBUG("initialize MO without FTP client"); + } + + if (!getCertificateStore() && MO_ENABLE_CERT_MGMT) { + if (MO_ENABLE_CERT_MGMT && !certStore) { + MO_DBG_DEBUG("initialize MO without certificate store"); + } + } + + if (!clock.setup()) { + return false; + } + + if (!msgService.setup()) { + return false; + } + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + if (!modelV16.setup()) { + MO_DBG_ERR("setup failure"); + return false; + } + } + #endif + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + if (!modelV201.setup()) { + MO_DBG_ERR("setup failure"); + return false; + } + } + #endif + + MO_DBG_INFO("MicroOCPP setup complete. Run %s (MO version %s)", + ocppVersion == MO_OCPP_V16 ? "OCPP 1.6" : ocppVersion == MO_OCPP_V201 ? "OCPP 2.0.1" : "(OCPP version error)", + MO_VERSION); + + return true; +} + +void Context::loop() { + if (connection) { + connection->loop(); + } + clock.loop(); + msgService.loop(); + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + modelV16.loop(); + } + #endif + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + modelV201.loop(); + } + #endif +} diff --git a/src/MicroOcpp/Context.h b/src/MicroOcpp/Context.h new file mode 100644 index 00000000..ea2f9f5a --- /dev/null +++ b/src/MicroOcpp/Context.h @@ -0,0 +1,141 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#ifndef MO_CONTEXT_H +#define MO_CONTEXT_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +//Anonymous handle to pass Context object around API functions +struct MO_Context; +typedef struct MO_Context MO_Context; + +#ifdef __cplusplus +} //extern "C" { + +namespace MicroOcpp { + +class CertificateStore; + +class Context : public MemoryManaged { +private: + uint32_t (*ticksCb)() = nullptr; + uint32_t (*rngCb)() = nullptr; + +#if MO_USE_FILEAPI != MO_CUSTOM_FS + MO_FilesystemConfig filesystemConfig; + bool filesystemConfigDefined = false; + bool isFilesystemOwner = false; +#endif //MO_USE_FILEAPI != MO_CUSTOM_FS + MO_FilesystemAdapter *filesystem = nullptr; + +#if MO_WS_USE != MO_WS_CUSTOM + MO_ConnectionConfig connectionConfig; + bool connectionConfigDefined = false; + bool isConnectionOwner = false; +#endif //MO_WS_USE != MO_WS_CUSTOM + Connection *connection = nullptr; + +#if MO_ENABLE_MBEDTLS + MO_FTPConfig ftpConfig; + bool ftpConfigDefined = false; + bool isFtpClientOwner = false; +#endif //MO_ENABLE_MBEDTLS + FtpClient *ftpClient = nullptr; + + CertificateStore *certStore = nullptr; + bool isCertStoreOwner = false; + + Clock clock {*this}; + MessageService msgService {*this}; + +#if MO_ENABLE_V16 + v16::Model modelV16 {*this}; +#endif +#if MO_ENABLE_V201 + v201::Model modelV201 {*this}; +#endif + int ocppVersion = MO_ENABLE_V16 ? MO_OCPP_V16 : MO_ENABLE_V201 ? MO_OCPP_V201 : -1; + +public: + Context(); + ~Context(); + + void setDebugCb(void (*debugCb)(const char *msg)); + void setDebugCb2(void (*debugCb2)(int lvl, const char *fn, int line, const char *msg)); + + void setTicksCb(uint32_t (*ticksCb)()); + uint32_t getTicksMs(); + + void setRngCb(uint32_t (*rngCb)()); + uint32_t (*getRngCb())(); + +#if MO_USE_FILEAPI != MO_CUSTOM_FS + bool setFilesystemConfig(MO_FilesystemConfig filesystemConfig); +#endif //MO_USE_FILEAPI != MO_CUSTOM_FS + void setFilesystem(MO_FilesystemAdapter *filesystem); + MO_FilesystemAdapter *getFilesystem(); + +#if MO_WS_USE != MO_WS_CUSTOM + bool setConnectionConfig(MO_ConnectionConfig connectionConfig); +#endif //MO_WS_USE != MO_WS_CUSTOM + void setConnection(Connection *connection); + Connection *getConnection(); + +#if MO_ENABLE_MBEDTLS + void setFtpConfig(MO_FTPConfig ftpConfig); +#endif //MO_ENABLE_MBEDTLS + void setFtpClient(FtpClient *ftpClient); + FtpClient *getFtpClient(); + + void setCertificateStore(CertificateStore *certStore); + CertificateStore *getCertificateStore(); + + Clock& getClock(); + MessageService& getMessageService(); + + bool setOcppVersion(int ocppVersion); + int getOcppVersion(); + +#if MO_ENABLE_V16 + v16::Model& getModel16(); +#endif + +#if MO_ENABLE_V201 + v201::Model& getModel201(); +#endif + +#if MO_ENABLE_V16 || MO_ENABLE_V201 + ModelCommon& getModelCommon(); //subset of Model16 / Model201 with modules which are shared between v16 and v201 implementation +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 + + // eliminate OCPP version specifiers if charger supports just one version anyway +#if MO_ENABLE_V16 && !MO_ENABLE_V201 + Model& getModel(); +#elif !MO_ENABLE_V16 && MO_ENABLE_V201 + Model& getModel(); +#endif + + bool setup(); + + void loop(); +}; + +} //namespace MicroOcpp + +#endif // __cplusplus +#endif diff --git a/src/MicroOcpp/Core/Configuration.cpp b/src/MicroOcpp/Core/Configuration.cpp deleted file mode 100644 index cb0a8ff5..00000000 --- a/src/MicroOcpp/Core/Configuration.cpp +++ /dev/null @@ -1,258 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Core/Configuration.h b/src/MicroOcpp/Core/Configuration.h deleted file mode 100644 index 486b9c69..00000000 --- a/src/MicroOcpp/Core/Configuration.h +++ /dev/null @@ -1,45 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Core/ConfigurationContainer.cpp b/src/MicroOcpp/Core/ConfigurationContainer.cpp deleted file mode 100644 index af0cba2f..00000000 --- a/src/MicroOcpp/Core/ConfigurationContainer.cpp +++ /dev/null @@ -1,76 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Core/ConfigurationContainer.h b/src/MicroOcpp/Core/ConfigurationContainer.h deleted file mode 100644 index 9a3ff3ae..00000000 --- a/src/MicroOcpp/Core/ConfigurationContainer.h +++ /dev/null @@ -1,65 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Core/ConfigurationContainerFlash.cpp b/src/MicroOcpp/Core/ConfigurationContainerFlash.cpp deleted file mode 100644 index b32f1af8..00000000 --- a/src/MicroOcpp/Core/ConfigurationContainerFlash.cpp +++ /dev/null @@ -1,362 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Core/ConfigurationContainerFlash.h b/src/MicroOcpp/Core/ConfigurationContainerFlash.h deleted file mode 100644 index 950f3cd7..00000000 --- a/src/MicroOcpp/Core/ConfigurationContainerFlash.h +++ /dev/null @@ -1,17 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Core/ConfigurationKeyValue.cpp b/src/MicroOcpp/Core/ConfigurationKeyValue.cpp deleted file mode 100644 index 77e8ad85..00000000 --- a/src/MicroOcpp/Core/ConfigurationKeyValue.cpp +++ /dev/null @@ -1,298 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Core/ConfigurationKeyValue.h b/src/MicroOcpp/Core/ConfigurationKeyValue.h deleted file mode 100644 index 3e631c1e..00000000 --- a/src/MicroOcpp/Core/ConfigurationKeyValue.h +++ /dev/null @@ -1,89 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Core/ConfigurationOptions.h b/src/MicroOcpp/Core/ConfigurationOptions.h deleted file mode 100644 index 0e9e227f..00000000 --- a/src/MicroOcpp/Core/ConfigurationOptions.h +++ /dev/null @@ -1,64 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Core/Configuration_c.cpp b/src/MicroOcpp/Core/Configuration_c.cpp deleted file mode 100644 index 6b6998e1..00000000 --- a/src/MicroOcpp/Core/Configuration_c.cpp +++ /dev/null @@ -1,315 +0,0 @@ -// 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)); -} diff --git a/src/MicroOcpp/Core/Configuration_c.h b/src/MicroOcpp/Core/Configuration_c.h deleted file mode 100644 index 94172d07..00000000 --- a/src/MicroOcpp/Core/Configuration_c.h +++ /dev/null @@ -1,84 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Core/Connection.cpp b/src/MicroOcpp/Core/Connection.cpp index ae2ee2fb..233e81de 100644 --- a/src/MicroOcpp/Core/Connection.cpp +++ b/src/MicroOcpp/Core/Connection.cpp @@ -3,9 +3,26 @@ // MIT License #include +#include #include -using namespace MicroOcpp; +namespace MicroOcpp { + +Connection::Connection() { + +} + +void Connection::setContext(Context *context) { + this->context = context; +} + +bool Connection::receiveTXT(const char *msg, size_t len) { + if (!context) { + MO_DBG_WARN("must assign Connection to a Context"); + return false; + } + return context->getMessageService().receiveMessage(msg, len); +} LoopbackConnection::LoopbackConnection() : MemoryManaged("WebSocketLoopback") { } @@ -15,107 +32,309 @@ 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; - } + return receiveTXT(msg, length); //`Connection::receiveTXT()` passes message back to MicroOcpp } -void LoopbackConnection::setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT) { - this->receiveTXT = receiveTXT; +void LoopbackConnection::setOnline(bool online) { + this->online = online; } -unsigned long LoopbackConnection::getLastRecv() { - return lastRecv; +bool LoopbackConnection::isOnline() { + return online; } -unsigned long LoopbackConnection::getLastConnected() { - return lastConn; +void LoopbackConnection::setConnected(bool connected) { + this->connected = connected; } -void LoopbackConnection::setOnline(bool online) { - if (online) { - lastConn = mocpp_tick_ms(); - } - this->online = online; +bool LoopbackConnection::isConnected() { + return connected; } -void LoopbackConnection::setConnected(bool connected) { - if (connected) { - lastConn = mocpp_tick_ms(); - } - this->connected = connected; +} //namespace MicroOcpp + +#if MO_WS_USE == MO_WS_ARDUINO + +void mo_connectionConfig_init(MO_ConnectionConfig *config) { + memset(config, 0, sizeof(*config)); } -#ifndef MO_CUSTOM_WS +bool mo_connectionConfig_copy(MO_ConnectionConfig *dst, MO_ConnectionConfig *src) { + + size_t size = 0; + size += src->backendUrl ? strlen(src->backendUrl) + 1 : 0; + size += src->chargeBoxId ? strlen(src->chargeBoxId) + 1 : 0; + size += src->authorizationKey ? strlen(src->authorizationKey) + 1 : 0; -using namespace MicroOcpp::EspWiFi; + char *buf = static_cast(MO_MALLOC("WebSocketsClient", size)); + if (!buf) { + MO_DBG_ERR("OOM"); + return false; + } -WSClient::WSClient(WebSocketsClient *wsock) : MemoryManaged("WebSocketsClient"), wsock(wsock) { + mo_connectionConfig_init(dst); -} + size_t written = 0; + if (src->backendUrl) { + dst->backendUrl = buf + written; + auto ret = snprintf(buf + written, size - written, "%s", src->backendUrl); + if (ret < 0 || (size_t)ret >= size - written) { + goto fail; + } + written += ret + 1; + } + + if (src->chargeBoxId) { + dst->chargeBoxId = buf + written; + auto ret = snprintf(buf + written, size - written, "%s", src->chargeBoxId); + if (ret < 0 || (size_t)ret >= size - written) { + goto fail; + } + written += ret + 1; + } -void WSClient::loop() { - wsock->loop(); + if (src->authorizationKey) { + dst->authorizationKey = buf + written; + auto ret = snprintf(buf + written, size - written, "%s", src->authorizationKey); + if (ret < 0 || (size_t)ret >= size - written) { + goto fail; + } + written += ret + 1; + } + + if (written != size) { + MO_DBG_ERR("internal error"); + goto fail; + } + + dst->internalBuf = buf; + return true; +fail: + MO_DBG_ERR("copy failed"); + MO_FREE(buf); + return false; } -bool WSClient::sendTXT(const char *msg, size_t length) { - return wsock->sendTXT(msg, length); +void mo_connectionConfig_deinit(MO_ConnectionConfig *config) { + MO_FREE(config->internalBuf); + memset(config, 0, sizeof(*config)); } -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; +#include + +namespace MicroOcpp { + +class ArduinoWSClient : public Connection, public MemoryManaged { +private: + WebSocketsClient *wsock = nullptr; + bool isWSockOwner = false; +public: + + ArduinoWSClient(WebSocketsClient *wsock, bool transferOwnership) : MemoryManaged("WebSocketsClient"), wsock(wsock), isWSockOwner(transferOwnership) { + wsock->onEvent([this](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); + break; + case WStype_TEXT: + if (!receiveTXT((const char *) payload, length)) { //`Connection::receiveTXT()` forwards message to MicroOcpp + 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_VERBOSE("Recv WS ping"); + break; + case WStype_PONG: + // answer to a ping we send + MO_DBG_VERBOSE("Recv WS pong"); + break; + case WStype_FRAGMENT_TEXT_START: //fragments are not supported + default: + MO_DBG_WARN("Unsupported WebSocket event type"); + break; + } + }); + } + + ~ArduinoWSClient() { + if (isWSockOwner) { + delete wsock; + wsock = nullptr; + isWSockOwner = false; } - }); + } + + void loop() override { + wsock->loop(); + } + + bool sendTXT(const char *msg, size_t length) override { + return wsock->sendTXT(msg, length); + } + + bool isConnected() override { + return wsock->isConnected(); + } +}; + +Connection *makeArduinoWSClient(WebSocketsClient& arduinoWebsockets) { + return static_cast(new ArduinoWSClient(&arduinoWebsockets, false)); } -unsigned long WSClient::getLastRecv() { - return lastRecv; +void freeArduinoWSClient(Connection *connection) { + delete connection; } -unsigned long WSClient::getLastConnected() { - return lastConnected; +Connection *makeDefaultConnection(MO_ConnectionConfig config, int ocppVersion) { + + Connection *connection = nullptr; + WebSocketsClient *wsock = nullptr; + + if (!config.backendUrl) { + MO_DBG_ERR("invalid args"); + return nullptr; + } + + wsock = new WebSocketsClient(); + if (!wsock) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + /* + * parse backendUrl so that it suits the links2004/arduinoWebSockets interface + */ + auto url = makeString("WebSocketsClient", config.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 nullptr; + } + + //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 nullptr; + } + 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 nullptr; + } + auto p = port * 10U + (*c - '0'); + if (p < port) { + MO_DBG_ERR("could not parse port (overflow): %s", url.c_str()); + return nullptr; + } + port = p; + } + } + + if (path.empty()) { + path = "/"; + } + + if (config.chargeBoxId) { + if (path.back() != '/') { + path += '/'; + } + + path += config.chargeBoxId; + } + + const char *ocppVersionStr = "ocpp1.6"; + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + ocppVersionStr = "ocpp1.6"; + } + #endif + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + ocppVersionStr = "ocpp2.0.1"; + } + #endif + + MO_DBG_INFO("connecting to %s -- (host: %s, port: %u, path: %s)", url.c_str(), host.c_str(), port, path.c_str()); + + wsock = new WebSocketsClient(); + if (!wsock) { + MO_DBG_ERR("OOM"); + goto fail; + } + + if (isTLS) { + // server address, port, path and TLS certificate + wsock->beginSslWithCA(host.c_str(), port, path.c_str(), config.CA_cert, ocppVersionStr); + } else { + // server address, port, path + wsock->begin(host.c_str(), port, path.c_str(), ocppVersionStr); + } + + // try ever 5000 again if connection has failed + wsock->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 + wsock->enableHeartbeat(15000, 3000, 2); //comment this one out to for specific OCPP servers + + + // add authentication data (optional) + { + size_t chargeBoxIdLen = config.chargeBoxId ? strlen(config.chargeBoxId) : 0; + size_t authorizationKeyLen = config.authorizationKey ? strlen(config.authorizationKey) : 0; + + if (config.authorizationKey && (authorizationKeyLen + chargeBoxIdLen >= 4)) { + wsock->setAuthorization(config.chargeBoxId ? config.chargeBoxId : "", config.authorizationKey); + } + } + + connection = new ArduinoWSClient(wsock, true); + if (!connection) { + MO_DBG_ERR("OOM"); + goto fail; + } + + //success + return static_cast(connection); +fail: + delete connection; + delete wsock; + return nullptr; } -bool WSClient::isConnected() { - return wsock->isConnected(); +void freeDefaultConnection(Connection *connection) { + delete connection; } -#endif +} //namespace MicroOcpp +#endif //MO_WS_USE diff --git a/src/MicroOcpp/Core/Connection.h b/src/MicroOcpp/Core/Connection.h index 2c2f7d5c..abc52c8e 100644 --- a/src/MicroOcpp/Core/Connection.h +++ b/src/MicroOcpp/Core/Connection.h @@ -5,29 +5,37 @@ #ifndef MO_CONNECTION_H #define MO_CONNECTION_H -#include -#include - #include #include +#define MO_WS_CUSTOM 0 +#define MO_WS_ARDUINO 1 + //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 +#ifndef MO_WS_USE +#if MO_PLATFORM == MO_PLATFORM_ARDUINO +#define MO_WS_USE MO_WS_ARDUINO +#else +#define MO_WS_USE MO_WS_CUSTOM #endif -#endif //ndef MO_CUSTOM_WS +#endif //MO_WS_USE + +#ifdef __cplusplus namespace MicroOcpp { -using ReceiveTXTcallback = std::function; +class Context; class Connection { +protected: + Context *context = nullptr; public: - Connection() = default; + Connection(); virtual ~Connection() = default; + void setContext(Context *context); // MO will set this during `setConnection()` + /* * 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 @@ -40,32 +48,21 @@ class Connection { virtual bool sendTXT(const char *msg, size_t length) = 0; /* + * Pass incoming data from the server to this function. The OCPP library will consume it * 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; + bool receiveTXT(const char *msg, size_t len); /* * 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. */ @@ -74,58 +71,60 @@ class Connection { 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;} + bool isOnline(); void setConnected(bool connected); //"connected": connection has been established, but messages may not go through (e.g. weak connection) - bool isConnected() override {return connected;} + bool isConnected() override; }; -} //end namespace MicroOcpp +} //namespace MicroOcpp -#ifndef MO_CUSTOM_WS +#endif //__cplusplus -#include +#if MO_WS_USE == MO_WS_ARDUINO -namespace MicroOcpp { -namespace EspWiFi { +#ifdef __cplusplus +extern "C" { +#endif -class WSClient : public Connection, public MemoryManaged { -private: - WebSocketsClient *wsock; - unsigned long lastRecv = 0, lastConnected = 0; -public: - WSClient(WebSocketsClient *wsock); +typedef struct { + const char *backendUrl; //e.g. "wss://example.com:8443/steve/websocket/CentralSystemService". Must be defined + const char *chargeBoxId; //e.g. "charger001". Can be NULL + const char *authorizationKey; //authorizationKey present in the websocket message header. Can be NULL. Set this to enable OCPP Security Profile 2 + const char *CA_cert; //TLS certificate. Can be NULL. Set this to enable OCPP Security Profile 2 - void loop(); + char *internalBuf; //used by MO internally +} MO_ConnectionConfig; - bool sendTXT(const char *msg, size_t length); +void mo_connectionConfig_init(MO_ConnectionConfig *config); - void setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT); +bool mo_connectionConfig_copy(MO_ConnectionConfig *dst, MO_ConnectionConfig *src); - unsigned long getLastRecv() override; //get time of last successful receive in millis +void mo_connectionConfig_deinit(MO_ConnectionConfig *config); - unsigned long getLastConnected() override; //get last connection creation in millis +#ifdef __cplusplus +} //extern "C" - bool isConnected() override; -}; +class WebSocketsClient; + +namespace MicroOcpp { + +Connection *makeArduinoWSClient(WebSocketsClient& arduinoWebsockets); //does not take ownership of arduinoWebsockets +void freeArduinoWSClient(Connection *connection); + +Connection *makeDefaultConnection(MO_ConnectionConfig config, int ocppVersion); +void freeDefaultConnection(Connection *connection); -} //end namespace EspWiFi -} //end namespace MicroOcpp -#endif //ndef MO_CUSTOM_WS +} //namespace MicroOcpp +#endif //__cplusplus +#endif //MO_WS_USE #endif diff --git a/src/MicroOcpp/Core/Context.cpp b/src/MicroOcpp/Core/Context.cpp deleted file mode 100644 index 1ab65fcb..00000000 --- a/src/MicroOcpp/Core/Context.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// 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(); -} diff --git a/src/MicroOcpp/Core/Context.h b/src/MicroOcpp/Core/Context.h deleted file mode 100644 index 2df691d1..00000000 --- a/src/MicroOcpp/Core/Context.h +++ /dev/null @@ -1,55 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Core/FilesystemAdapter.cpp b/src/MicroOcpp/Core/FilesystemAdapter.cpp index b442e583..ad5bfadb 100644 --- a/src/MicroOcpp/Core/FilesystemAdapter.cpp +++ b/src/MicroOcpp/Core/FilesystemAdapter.cpp @@ -1,13 +1,12 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #include -#include //FilesystemOpt #include #include -#include +#include /* * Platform specific implementations. Currently supported: @@ -25,453 +24,689 @@ #include namespace MicroOcpp { +namespace IndexedFS { -class FilesystemAdapterIndex; +struct FilesystemAdapterIndex; -class IndexedFileAdapter : public FileAdapter, public MemoryManaged { -private: +struct IndexedFileAdapter : public MemoryManaged { FilesystemAdapterIndex& index; - char fn [MO_MAX_PATH_SIZE]; - std::unique_ptr file; - + MO_File *file = nullptr; //takes ownership + char *fname = nullptr; //no ownership size_t written = 0; + + IndexedFileAdapter(FilesystemAdapterIndex& index) : MemoryManaged("FilesystemIndex"), index(index) { } +}; + +struct FilesystemAdapterIndex : public MemoryManaged { 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); - } + MO_FilesystemAdapter *filesystem = nullptr; //takes ownership - ~IndexedFileAdapter(); // destructor updates file index with written size + struct IndexEntry { + char *fname = nullptr; //takes ownership + size_t size; + IndexEntry(const char *fname, size_t size) : fname(fname), size(size) { } + ~IndexEntry() { + MO_FREE(fname); + fname = nullptr; + } + }; - size_t read(char *buf, size_t len) override { - return file->read(buf, len); + Vector fileEntries; + + IndexEntry *getEntryByPath(const char *path) { + const char *prefix = filesystem->path_prefix ? filesystem->path_prefix : ""; + size_t prefix_len = strlen(prefix); + + if (strncmp(path, prefix, prefix_len)) { + return nullptr; + } + + const char *fname = path + prefix_len; + + for (size_t i = 0; i < fileEntries.size(); i++) { + if (!strcmp(fileEntries[i].fname, fname)) { + return &fileEntries[i]; + } + } + return nullptr; } +}; - size_t write(const char *buf, size_t len) override { - auto ret = file->write(buf, len); - written += ret; - return ret; +Vector indexedFilesystems; +FilesystemAdapterIndex *getFilesystemAdapterIndexByPath(const char *path) { + if (!path) { + path = ""; + } + for (size_t i = 0; i < indexedFilesystems.size(); i++) { + auto index = &indexedFilesystems[i]; + const char *prefix = index->filesystem->path_prefix ? index->filesystem->path_prefix : ""; + size_t prefix_len = strlen(prefix); + if (!*path || !*prefix) { + //filesystem adapters are not distinguishable by a path + return index; + } + if (!strncmp(path, index->filesystem->path_prefix, prefix_len)) { + return index; + } } + return nullptr; +} - size_t seek(size_t offset) override { - auto ret = file->seek(offset); - written = ret; - return ret; +int stat(const char *path, size_t *size) { + auto index = getFilesystemAdapterIndexByPath(path); + if (!index) { + MO_DBG_ERR("path not recognized"); + return -1; + } + auto entry = index->getEntryByPath(path); + if (entry) { + *size = entry->size; + } else { + return -1; } +} - int read() override { - return file->read(); +bool remove(const char *path) { + auto index = getFilesystemAdapterIndexByPath(path); + if (!index) { + MO_DBG_ERR("path not recognized"); + return false; } -}; + auto entry = index.getEntryByPath(path); + if (!entry) { + return true; + } + bool removed = index.filesystem->remove(path); + if (removed) { + index.fileEntries.erase(index.fileEntries.begin() + (entry - &index.fileEntries[0])); + } + return removed; +} -class FilesystemAdapterIndex : public FilesystemAdapter, public MemoryManaged { -private: - std::shared_ptr filesystem; +int ftw(const char *path_prefix, int(*fn)(const char *fname, void *mo_user_data), void *mo_user_data) { + auto index = getFilesystemAdapterIndexByPath(path_prefix); + if (!index) { + MO_DBG_ERR("path not recognized"); + return -1; + } + for (size_t it = 0; it < index.size();) { + auto size_before = index.size(); + auto err = fn(index[it].fname); + if (err) { + return err; + } + if (index.size() + 1 == size_before) { + // element removed + continue; + } + // normal execution + it++; + } - struct IndexEntry { - String fname; - size_t size; + return 0; +} - IndexEntry(const char *fname, size_t size) : fname(makeString("FilesystemIndex", fname)), size(size) { } - }; +MO_File* open(const char *path, const char *mode) { + auto index = getFilesystemAdapterIndexByPath(path); + if (!index) { + MO_DBG_ERR("path not recognized"); + return nullptr; + } - Vector index; + IndexEntry *entry = nullptr; + const char *fnCopy = nullptr; + bool created = false; + if (!(entry = getEntryByPath(path))) { - 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; - }); + size_t capacity = index->fileEntries.size() + 1; + index->fileEntries.reserve(capacity); + if (index->fileEntries.capacity() < size) { + MO_DBG_ERR("OOM"); + return nullptr; + } - if (entry != index.end()) { - return &(*entry); - } else { + const char *prefix = index.path_prefix ? index.path_prefix : ""; + size_t prefix_len = strlen(prefix); + if (!strncmp(path, prefix, prefix_len)) { + MO_DBG_ERR("internal error"); return nullptr; } - } - IndexEntry *getEntryByPath(const char *path) { - if (strlen(path) < sizeof(MO_FILENAME_PREFIX) - 1) { - MO_DBG_ERR("invalid fn"); + char *fn = path + prefix_len; + size_t fn_size = strlen(path) - prefix_len + 1; + + char *fnCopy = static_cast(MO_MALLOC("FilesystemIndex", fn_size)); + if (!fnCopy) { + MO_DBG_ERR("OOM"); return nullptr; } - const char *fn = path + sizeof(MO_FILENAME_PREFIX) - 1; - return getEntryByFname(fn); + (void)snprintf(fnCopy, fn_size, "%s", fn); + created = true; } - - 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); - } + auto indexedFile = new IndexedFileAdapter(*index); + if (!indexedFile) { + MO_DBG_ERR("OOM"); + MO_FREE(fnCopy); + fnCopy = nullptr + return nullptr } - int stat(const char *path, size_t *size) override { - if (auto file = getEntryByPath(path)) { - *size = file->size; - return 0; + auto file = filesystem->open(path, mode); + if (!file) { + MO_FREE(fnCopy); + fnCopy = nullptr; + delete indexedFile; + indexedFile = nullptr; + return nullptr; + } + + if (created) { + index.emplace_back(fnCopy, 0); //transfer ownership of fnCopy + entry = &index.back(); + } + + if (!strcmp(mode, "w")) { + entry->size = 0; //write always empties the file + } + + indexedFile->file = file; //transfer ownership + indexedFile->fname = entry->fname; //no ownership + + return reinterpret_cast(indexedFile); +} + +bool close(MO_File *fileHandle) { + auto indexedFile = reinterpret_cast(fileHandle); + auto index = indexedFile->index; + auto file = indexedFile->file; + auto filesystem = index->filesystem; + bool success = filesystem->close(file); + if (success) { + auto entry = index->getEntryByFname(indexedFile->fname); + if (entry) { + entry->size = indexedFile->written; + MO_DBG_DEBUG("update index: %s (%zuB)", entry->fname, entry->size); } else { - return -1; + MO_DBG_ERR("index consistency failure"); + //don't return false, the contents have been written and the file operation was successful } } + delete indexedFile; + indexedFile = nullptr; + return success; +} - std::unique_ptr open(const char *path, const char *mode) { - if (!strcmp(mode, "r")) { - return filesystem->open(path, "r"); - } else if (!strcmp(mode, "w")) { +size_t read(MO_File *file, char *buf, size_t len) { + auto indexedFile = reinterpret_cast(fileHandle); + auto file = indexedFile->file; + auto filesystem = indexedFile->index->filesystem; + return filesystem->read(file, buf, len); +} - if (strlen(path) < sizeof(MO_FILENAME_PREFIX) - 1) { - MO_DBG_ERR("invalid fn"); - return nullptr; - } +int getc(MO_File *file) { + auto indexedFile = reinterpret_cast(fileHandle); + auto file = indexedFile->file; + auto filesystem = indexedFile->index->filesystem; + return filesystem->getc(file); +} - const char *fn = path + sizeof(MO_FILENAME_PREFIX) - 1; +size_t write(MO_File *file, const char *buf, size_t len) { + auto indexedFile = reinterpret_cast(fileHandle); + auto file = indexedFile->file; + auto filesystem = indexedFile->index->filesystem; - auto file = filesystem->open(path, "w"); - if (!file) { - return nullptr; - } + auto ret = filesystem->write(file, buf, len); + indexedFile->written += ret; + return ret; +} - IndexEntry *entry = nullptr; - if (!(entry = getEntryByFname(fn))) { - index.emplace_back(fn, 0); - entry = &index.back(); - } +int seek(MO_File *file, size_t fpos) { + auto indexedFile = reinterpret_cast(fileHandle); + auto file = indexedFile->file; + auto filesystem = indexedFile->index->filesystem; - if (!entry) { - MO_DBG_ERR("internal error"); - return nullptr; - } + auto ret = filesystem->seek(file, fpos); + if (ret >= 0) { + indexedFile->written = (size_t)ret; + } + return ret; +} - entry->size = 0; //write always empties the file +int loadIndexEntry(const char *fname, void *user_data) { + auto index = reinterpret_cast(user_data); + int ret; + char path [MO_MAX_PATH_SIZE]; - 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; - } + const char *prefix = index.path_prefix ? index.path_prefix : ""; + + ret = snprintf(path, sizeof(path), "%s%s", prefix, fname); + if (ret < 0 || ret >= sizeof(path),) { + MO_DBG_ERR("snprintf: %i", ret); + return -1; } - 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()); - } + auto filesystem = index->filesystem; - return filesystem->remove(path); - } + 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)", fname, size); - 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++; + size_t fnameSize = strlen(fname) + 1; + char *fnameCopy = static_cast(MO_MALLOC("FilesystemIndex", fnameSize)); + if (!fnameCopy) { + MO_DBG_ERR("OOM"); + return -1; + } + + size_t capacity = index->fileEntries.size() + 1; + index->fileEntries.reserve(capacity); + if (index->fileEntries.capacity() < size) { + MO_DBG_ERR("OOM"); + MO_FREE(fnameCopy); + fnameCopy = nullptr + return -1; } - return 0; + index.emplace_back(fnameCopy, size); + return 0; //successfully added filename to index + } else { + MO_DBG_ERR("unexpected entry: %s", fname); + return 0; //ignore this entry and continue ftw } +} - 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]; +MO_FilesystemAdapter *decorateIndex(MO_FilesystemAdapter *filesystem) { + if (getFilesystemAdapterIndexByPath(filesystem->path_prefix)) { + MO_DBG_ERR("Multiple filesystem indexes only supported with unique MO folder paths"); + goto fail; + } - 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 - } + auto index = new FilesystemAdapterIndex(); + if (!index) { + MO_DBG_ERR("OOM"); + goto fail; + } - 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 - } - }); + auto decorator = static_cast(MO_MALLOC("Filesystem", sizeof(MO_FilesystemAdapter))); + if (!decorator) { + MO_DBG_ERR("OOM"); + goto fail; + } + memset(decorator, 0, sizeof(MO_FilesystemAdapter)); + + decorator->path_prefix = filesystem->path_prefix; + decorator->stat = stat; + decorator->remove = remove; + decorator->ftw = ftw; + decorator->open = open; + decorator->close = close; + decorator->read = read; + decorator->getc = getc; + decorator->write = write; + decorator->seek = seek; + + size_t capacity = indexedFilesystems.size() + 1; + indexedFilesystems.reserve(capacity); + if (indexedFilesystems.capacity() < size) { + MO_DBG_ERR("OOM"); + goto fail; + } - MO_DBG_DEBUG("create fs index: %s, %zu entries", ret == 0 ? "success" : "failure", index.size()); + index->filesystem = filesystem; //transfer ownership - return ret == 0; + //enumerate files in MO folder and load each file into the index structure + auto ret = filesystem->ftw(filesystem->path_prefix, loadIndexEntry, reinterpret_cast(index)); + if (ret == 0) { + MO_DBG_DEBUG("create fs index: found %zu entries", index->fileEntries.size()); + } else { + MO_DBG_ERR("error loading fs index"); + goto fail; } - 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); - } + indexedFilesystems.push_back(index); + return decorator; +fail: + index->filesystem = nullptr; //revoke ownership + delete index; + index = nullptr; + MO_FREE(decorator); + decorator = nullptr; + return filesystem; +} + +MO_FilesystemAdapter *resetIndex(MO_FilesystemAdapter *decorator) { + auto index = getFilesystemAdapterIndexByPath(decorator->path_prefix); + if (!index) { + MO_DBG_ERR("Index not found"); + return decorator; } -}; + auto filesystem = index->filesystem; + indexedFilesystems.erase(indexedFilesystems.begin() + (index - &indexedFilesystems[0])); + delete index; + index = nullptr; + MO_FREE(decorator); + decorator = nullptr; + return filesystem; +} -IndexedFileAdapter::~IndexedFileAdapter() { - index.updateFilesize(fn, written); +} //namespace IndexedFS +} //namespace MicroOcpp + +#endif //MO_ENABLE_FILE_INDEX + +#if MO_USE_FILEAPI != MO_CUSTOM_FS + +void mo_filesystemConfig_init(MO_FilesystemConfig *config) { + memset(config, 0, sizeof(*config)); } -std::shared_ptr decorateIndex(std::shared_ptr filesystem, void (*onDestruct)(void*) = nullptr) { +bool mo_filesystemConfig_copy(MO_FilesystemConfig *dst, MO_FilesystemConfig *src) { - auto fsIndex = std::allocate_shared(makeAllocator("FilesystemIndex"), std::move(filesystem), onDestruct); - if (!fsIndex) { - MO_DBG_ERR("OOM"); - return nullptr; - } + char *prefix_copy = nullptr; - if (!fsIndex->createIndex()) { - MO_DBG_ERR("createIndex err"); - return nullptr; + const char *prefix = src->path_prefix; + size_t prefix_len = prefix ? strlen(prefix) : 0; + if (prefix_len > 0) { + size_t prefix_size = prefix_len + 1; + prefix_copy = static_cast(MO_MALLOC("Filesystem", prefix_size)); + if (!prefix_copy) { + MO_DBG_ERR("OOM"); + return false; + } + (void)snprintf(prefix_copy, prefix_size, "%s", prefix); } - return fsIndex; + dst->internalBuf = prefix_copy; + dst->path_prefix = dst->internalBuf; + dst->opt = src->opt; + return true; } -} // namespace MicroOcpp +void mo_filesystemConfig_deinit(MO_FilesystemConfig *config) { + MO_FREE(config->internalBuf); + memset(config, 0, sizeof(*config)); +} -#endif //MO_ENABLE_FILE_INDEX +#endif //MO_USE_FILEAPI != MO_CUSTOM_FS -#if MO_USE_FILEAPI == ARDUINO_LITTLEFS || MO_USE_FILEAPI == ARDUINO_SPIFFS +#if MO_USE_FILEAPI == MO_ARDUINO_LITTLEFS || MO_USE_FILEAPI == MO_ARDUINO_SPIFFS -#if MO_USE_FILEAPI == ARDUINO_LITTLEFS +#if MO_USE_FILEAPI == MO_ARDUINO_LITTLEFS #include #include #define USE_FS LittleFS -#elif MO_USE_FILEAPI == ARDUINO_SPIFFS +#elif MO_USE_FILEAPI == MO_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(); - } +namespace ArduinoFS { + +int stat(const char *path, size_t *size) { +#if MO_USE_FILEAPI == MO_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; } - - int read() override { - return file.read(); - }; - size_t read(char *buf, size_t len) override { - return file.readBytes(buf, len); + struct ::stat st; + auto status = ::stat(partition_path, &st); + if (status == 0) { + *size = st.st_size; } - size_t write(const char *buf, size_t len) override { - return file.printf("%.*s", len, buf); + return status; +#elif MO_USE_FILEAPI == MO_ARDUINO_SPIFFS + if (!USE_FS.exists(path)) { + return -1; } - size_t seek(size_t offset) override { - return file.seek(offset); + File f = USE_FS.open(path, "r"); + if (!f) { + return -1; } -}; - -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); + int status = -1; + if (!f.isDirectory()) { + *size = f.size(); + status = 0; + } - if (!SPIFFS.begin()) { - MO_DBG_ERR("Unable to initialize: unable to mount SPIFFS"); - valid = false; - } + f.close(); + return status; #else #error #endif - } //end if mustMount() - - } +} //end stat - ~ArduinoFilesystemAdapter() { - if (config.mustMount()) { - USE_FS.end(); - } +bool remove(const char *path) { + return USE_FS.remove(path); +} - if (onDestruct) { - onDestruct(this); - } +int ftw(const char *path_prefix, int(*fn)(const char *fname, void *mo_user_data), void *mo_user_data) { + const char *prefix = path_prefix ? path_prefix : ""; +#if MO_USE_FILEAPI == MO_ARDUINO_LITTLEFS + auto dir = USE_FS.open(prefix); + if (!dir) { + MO_DBG_ERR("cannot open root directory: %s", prefix); + return -1; } - operator bool() {return valid;} + 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(); - 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; + err = fn(fname, mo_user_data); + if (err) { + break; + } + } + return err; +#elif MO_USE_FILEAPI == MO_ARDUINO_SPIFFS + size_t prefix_len = strlen(prefix); + auto dir = USE_FS.openDir(prefix); + int err = 0; + while (dir.next()) { + auto path = dir.fileName(); + if (path.c_str()) { + const char *fname = path.c_str() + prefix_len; + err = fn(fname, mo_user_data); } else { - //fetch more information for directory when MicroOcpp also uses them - //status = 0; + MO_DBG_ERR("fs error"); + err = -1; } - - f.close(); - return status; + if (err) { + break; + } + } + return err; #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; - } +MO_File* open(const char *path, const char *mode) { + auto *file = new File(); + if (!file) { + MO_DBG_ERR("OOM"); + 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; - } + *file = USE_FS.open(path, mode); + if (!*file) { + MO_DBG_ERR("open file failure"); + goto fail; + } + if (file->isDirectory()) { + MO_DBG_ERR("file failure"); + goto fail; + } + MO_DBG_DEBUG("File open successful: %s", path); + return reinterpret_cast(file); +fail: + if (file) { + file->close(); + } + delete file; + file = nullptr; + return nullptr; +} - int err = 0; - while (auto entry = dir.openNextFile()) { +bool close(MO_File *fileHandle) { + auto file = reinterpret_cast(fileHandle); + file->close(); + delete file; + return true; +} - char fname [MO_MAX_PATH_SIZE]; - auto ret = snprintf(fname, MO_MAX_PATH_SIZE, "%s", entry.name()); - entry.close(); +size_t read(MO_File *fileHandle, char *buf, size_t len) { + auto file = reinterpret_cast(fileHandle); + return file->readBytes(buf, len); +} - if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { - MO_DBG_ERR("fn error: %i", ret); - return -1; - } +int getc(MO_File *fileHandle) { + auto file = reinterpret_cast(fileHandle); + return file->read(); +} - err = fn(fname); - if (err) { - break; - } +size_t write(MO_File *fileHandle, const char *buf, size_t len) { + auto file = reinterpret_cast(fileHandle); + return file->printf("%.*s", len, buf); +} + +int seek(MO_File *fileHandle, size_t fpos) { + auto file = reinterpret_cast(fileHandle); + return file->seek(fpos); +} + +int useCount; +MO_FilesystemOpt opt = MO_FS_OPT_DISABLE; + +} //namespace ArduinoFS +} //namespace MicroOcpp + +MO_FilesystemAdapter *mo_makeDefaultFilesystemAdapter(MO_FilesystemConfig config) { + + if (config.opt == MO_FS_OPT_DISABLE) { + MO_DBG_DEBUG("Access to filesystem not allowed by opt"); + return nullptr; + } + + char *prefix_copy = nullptr; + MO_FilesystemAdapter *filesystem = nullptr; + + const char *prefix = config.path_prefix; + size_t prefix_len = prefix ? strlen(prefix) : 0; + if (prefix_len > 0) { + size_t prefix_size = prefix_len + 1; + prefix_copy = static_cast(MO_MALLOC("Filesystem", prefix_size)); + if (!prefix_copy) { + MO_DBG_ERR("OOM"); + goto fail; } - 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)); + (void)snprintf(prefix_copy, prefix_size, "%s", prefix); + } + + filesystem = static_cast(MO_MALLOC("Filesystem", sizeof(MO_FilesystemAdapter))); + if (!filesystem) { + MO_DBG_ERR("OOM"); + goto fail; + } + memset(filesystem, 0, sizeof(MO_FilesystemAdapter)); + + filesystem->path_prefix = prefix_copy; + filesystem->stat = MicroOcpp::ArduinoFS::stat; + filesystem->remove = MicroOcpp::ArduinoFS::remove; + filesystem->ftw = MicroOcpp::ArduinoFS::ftw; + filesystem->open = MicroOcpp::ArduinoFS::open; + filesystem->close = MicroOcpp::ArduinoFS::close; + filesystem->read = MicroOcpp::ArduinoFS::read; + filesystem->getc = MicroOcpp::ArduinoFS::getc; + filesystem->write = MicroOcpp::ArduinoFS::write; + filesystem->seek = MicroOcpp::ArduinoFS::seek; + + if (config.opt >= MO_FS_OPT_USE_MOUNT && MicroOcpp::ArduinoFS::opt < MO_FS_OPT_USE_MOUNT) { + #if MO_USE_FILEAPI == MO_ARDUINO_LITTLEFS + { + if(!USE_FS.begin(config.opt == MO_FS_OPT_USE_MOUNT_FORMAT_ON_FAIL)) { + MO_DBG_ERR("Error while mounting LITTLEFS"); + goto fail; } else { - MO_DBG_ERR("fs error"); - err = -1; + MO_DBG_DEBUG("LittleFS mount success"); } - if (err) { - break; + } + #elif MO_USE_FILEAPI == MO_ARDUINO_SPIFFS + { + //ESP8266 + SPIFFSConfig cfg; + cfg.setAutoFormat(config.opt == MO_FS_OPT_USE_MOUNT_FORMAT_ON_FAIL); + SPIFFS.setConfig(cfg); + + if (!SPIFFS.begin()) { + MO_DBG_ERR("Unable to initialize: unable to mount SPIFFS"); + goto fail; } } - return err; -#else -#error -#endif - } -}; - -std::weak_ptr filesystemCache; + #else + #error + #endif -void resetFilesystemCache(void*) { - filesystemCache.reset(); -} + //mounted successfully -std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { + } //if MO_FS_OPT_USE_MOUNT - if (auto cached = filesystemCache.lock()) { - return cached; + if (filesystem) { + MicroOcpp::ArduinoFS::useCount++; + if (config.opt > MicroOcpp::ArduinoFS::opt) { + MicroOcpp::ArduinoFS::opt = config.opt; + } } - if (!config.accessAllowed()) { - MO_DBG_DEBUG("Access to Arduino FS not allowed by config"); - return nullptr; - } +#if MO_ENABLE_FILE_INDEX + filesystem = MicroOcpp::IndexedFS::decorateIndex(filesystem); +#endif //MO_ENABLE_FILE_INDEX + + return filesystem; +fail: + MO_FREE(prefix_copy); + MO_FREE(filesystem); + return nullptr; +} - auto fs_concrete = new ArduinoFilesystemAdapter(config, resetFilesystemCache); - auto fs = std::shared_ptr(fs_concrete, std::default_delete(), makeAllocator("Filesystem")); +void mo_freeDefaultFilesystemAdapter(MO_FilesystemAdapter *filesystem) { + if (!filesystem) { + return; //noop + } #if MO_ENABLE_FILE_INDEX - fs = decorateIndex(fs, resetFilesystemCache); -#endif // MO_ENABLE_FILE_INDEX - - filesystemCache = fs; + filesystem = MicroOcpp::IndexedFS::resetIndex(filesystem); +#endif //MO_ENABLE_FILE_INDEX - if (*fs_concrete) { - return fs; - } else { - return nullptr; + MicroOcpp::ArduinoFS::useCount--; + if (MicroOcpp::ArduinoFS::useCount <= 0 && MicroOcpp::ArduinoFS::opt >= MO_FS_OPT_USE_MOUNT) { + USE_FS.end(); + MicroOcpp::ArduinoFS::opt = MO_FS_OPT_DISABLE; } -} -} //end namespace MicroOcpp + //the default FS adapter owns the path_prefix buf and needs to free it + char *path_prefix_buf = const_cast(filesystem->path_prefix); + MO_FREE(path_prefix_buf); + + MO_FREE(filesystem); +} -#elif MO_USE_FILEAPI == ESPIDF_SPIFFS +#elif MO_USE_FILEAPI == MO_ESPIDF_SPIFFS #include #include @@ -482,156 +717,152 @@ std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt co #endif namespace MicroOcpp { +namespace EspIdfFS { -class EspIdfFileAdapter : public FileAdapter, public MemoryManaged { - FILE *file {nullptr}; -public: - EspIdfFileAdapter(FILE *file) : MemoryManaged("Filesystem"), file(file) {} - - ~EspIdfFileAdapter() { - fclose(file); +int stat(const char *path, size_t *size) { + struct ::stat st; + auto ret = ::stat(path, &st); + if (ret == 0) { + *size = st.st_size; } + return ret; +} - size_t read(char *buf, size_t len) override { - return fread(buf, 1, len, file); - } +bool remove(const char *path) { + return unlink(path) == 0; +} - size_t write(const char *buf, size_t len) override { - return fwrite(buf, 1, len, file); +int ftw(const char *path_prefix, int(*fn)(const char *fname, void *mo_user_data), void *mo_user_data) { + //open MO root directory + const char *prefix = path_prefix ? path_prefix : ""; + char dpath [MO_MAX_PATH_SIZE]; + auto path_len = snprintf(dpath, MO_MAX_PATH_SIZE, "%s", prefix); + if (path_len < 0 || path_len >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", path_len); + return -1; } - size_t seek(size_t offset) override { - return fseek(file, offset, SEEK_SET); + // trim trailing '/' if not root directory + if (path_len >= 2 && dpath[path_len - 1] == '/') { + dpath[path_len - 1] = '\0'; } - int read() override { - return fgetc(file); + auto dir = opendir(dpath); + if (!dir) { + MO_DBG_ERR("cannot open root directory: %s", dpath); + return -1; } -}; - -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 err = 0; + while (auto entry = readdir(dir)) { + err = fn(entry->d_name, mo_user_data); + if (err) { + break; } } - 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; - } + closedir(dir); + return err; +} - 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; - } - } +MO_File* open(const char *path, const char *mode) { + return reinterpret_cast(fopen(path, mode)); +} - bool remove(const char *fn) override { - return unlink(fn) == 0; - } +bool close(MO_File *file) { + return fclose(reinterpret_cast(file)) == 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; - } +size_t read(MO_File *file, char *buf, size_t len) { + return fread(buf, 1, len, reinterpret_cast(file)); +} - // trim trailing '/' if not root directory - if (dlen >= 2 && dname[dlen - 1] == '/') { - dname[dlen - 1] = '\0'; - } +int getc(MO_File *file) { + return fgetc(reinterpret_cast(file)); +} - auto dir = opendir(dname); - if (!dir) { - MO_DBG_ERR("cannot open root directory: %s", dname); - return -1; - } +size_t write(MO_File *file, const char *buf, size_t len) { + return fwrite(buf, 1, len, reinterpret_cast(file)); +} - int err = 0; - while (auto entry = readdir(dir)) { - err = fn(entry->d_name); - if (err) { - break; - } - } +int seek(MO_File *file, size_t fpos) { + return fseek(file, fpos, SEEK_SET); +} - closedir(dir); - return err; - } -}; +int useCount; +MO_FilesystemOpt opt = MO_FS_OPT_DISABLE; -std::weak_ptr filesystemCache; +} //namespace EspIdfFS +} //namespace MicroOcpp -void resetFilesystemCache(void*) { - filesystemCache.reset(); -} +using namespace MicroOcpp; -std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { +MO_FilesystemAdapter *mo_makeDefaultFilesystemAdapter(MO_FilesystemConfig config) { - if (auto cached = filesystemCache.lock()) { - return cached; - } + char *prefix_copy = nullptr; + MO_FilesystemAdapter *filesystem = nullptr; - if (!config.accessAllowed()) { - MO_DBG_DEBUG("Access to ESP-IDF SPIFFS not allowed by config"); - return nullptr; + if (config.opt == MO_FS_OPT_DISABLE) { + MO_DBG_DEBUG("Access to filesystem not allowed by opt"); + goto fail; + } + + const char *prefix = config.path_prefix; + size_t prefix_len = prefix ? strlen(prefix) : 0; + if (prefix_len > 0) { + size_t prefix_size = prefix_len + 1; + prefix_copy = static_cast(MO_MALLOC("Filesystem", prefix_size)); + if (!prefix_copy) { + MO_DBG_ERR("OOM"); + goto fail; + } + (void)snprintf(prefix_copy, prefix_size, "%s", prefix); } - 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; + auto filesystem = static_cast(MO_MALLOC("Filesystem", sizeof(MO_FilesystemAdapter))); + if (!filesystem) { + MO_DBG_ERR("OOM"); + goto fail; + } + memset(filesystem, 0, sizeof(MO_FilesystemAdapter)); + + filesystem->path_prefix = prefix_copy; + filesystem->stat = EspIdfFS::stat; + filesystem->remove = EspIdfFS::remove; + filesystem->ftw = EspIdfFS::ftw; + filesystem->open = EspIdfFS::open; + filesystem->close = EspIdfFS::close; + filesystem->read = EspIdfFS::read; + filesystem->getc = EspIdfFS::getc; + filesystem->write = EspIdfFS::write; + filesystem->seek = EspIdfFS::seek; + + if (config.opt >= MO_FS_OPT_USE_MOUNT && EspIdfFS::opt < MO_FS_OPT_USE_MOUNT) { + char prefix [MO_MAX_PATH_SIZE]; + auto prefix_len = snprintf(prefix, MO_MAX_PATH_SIZE, "%s", prefix ? prefix : ""); + if (prefix_len < 0 || prefix_len >= sizeof(prefix)) { + MO_DBG_ERR("path_prefix error %i", prefix_len); + goto fail; + } else if (prefix_len <= 2) { //shortest possible prefix: "/p/", i.e. length = 3 + MO_DBG_ERR("path_prefix cannot be root on ESP-IDF (working example: \"/mo_store/\")"); + goto fail; } // trim trailing '/' - if (fnpref[fnpref_len - 1] == '/') { - fnpref[fnpref_len - 1] = '\0'; + if (prefix[prefix_len - 1] == '/') { + prefix[prefix_len - 1] = '\0'; } esp_vfs_spiffs_conf_t conf = { - .base_path = fnpref, + .base_path = prefix, .partition_label = MO_PARTITION_LABEL, .max_files = 5, - .format_if_mount_failed = config.formatOnFail() + .format_if_mount_failed = (config.opt == MO_FS_OPT_USE_MOUNT_FORMAT_ON_FAIL) }; 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) { @@ -641,160 +872,207 @@ std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt co } else { MO_DBG_ERR("Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); } + goto fail; + } + + //mounted successfully + + } //if MO_FS_OPT_USE_MOUNT + + if (filesystem) { + EspIdfFS::useCount++; + if (config.opt > EspIdfFS::opt) { + EspIdfFS::opt = config.opt; } } - if (mounted) { - auto fs = std::shared_ptr(new EspIdfFilesystemAdapter(config, resetFilesystemCache), std::default_delete(), makeAllocator("Filesystem")); +#if MO_ENABLE_FILE_INDEX + filesystem = decorateIndex(filesystem); +#endif //MO_ENABLE_FILE_INDEX + + return filesystem; +fail: + MO_FREE(prefix_copy); + MO_FREE(filesystem); + return nullptr; +} + +void mo_freeDefaultFilesystemAdapter(MO_FilesystemAdapter *filesystem) { + if (!filesystem) { + return; //noop + } #if MO_ENABLE_FILE_INDEX - fs = decorateIndex(fs, resetFilesystemCache); -#endif // MO_ENABLE_FILE_INDEX + filesystem = resetIndex(filesystem); +#endif //MO_ENABLE_FILE_INDEX - filesystemCache = fs; - return fs; - } else { - return nullptr; + EspIdfFS::useCount--; + if (EspIdfFS::useCount <= 0 && EspIdfFS::opt >= MO_FS_OPT_USE_MOUNT) { + esp_vfs_spiffs_unregister(MO_PARTITION_LABEL); + MO_DBG_DEBUG("SPIFFS unmounted"); + EspIdfFS::opt = MO_FS_OPT_DISABLE; } + MO_FREE(filesystem->path_prefix); + MO_FREE(filesystem); } -} //end namespace MicroOcpp - -#elif MO_USE_FILEAPI == POSIX_FILEAPI +#elif MO_USE_FILEAPI == MO_POSIX_FILEAPI #include #include #include namespace MicroOcpp { +namespace PosixFS { -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); +int stat(const char *path, size_t *size) { + struct ::stat st; + auto ret = ::stat(path, &st); + if (ret == 0) { + *size = st.st_size; } + return ret; +} - size_t seek(size_t offset) override { - return fseek(file, offset, SEEK_SET); - } +bool remove(const char *path) { + return ::remove(path) == 0; +} - int read() override { - return fgetc(file); +int ftw(const char *path_prefix, int(*fn)(const char *fname, void *mo_user_data), void *mo_user_data) { + auto dir = opendir(path_prefix); + if (!dir) { + MO_DBG_ERR("cannot open root directory: %s", path_prefix); + return -1; } -}; - -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 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 } - } - - int stat(const char *path, size_t *size) override { - struct ::stat st; - auto ret = ::stat(path, &st); - if (ret == 0) { - *size = st.st_size; + err = fn(entry->d_name, mo_user_data); + if (err) { + break; } - 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; - } - } + closedir(dir); + return err; +} - bool remove(const char *fn) override { - return ::remove(fn) == 0; - } +MO_File* open(const char *path, const char *mode) { + return reinterpret_cast(fopen(path, mode)); +} - 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; - } +bool close(MO_File *file) { + return fclose(reinterpret_cast(file)) == 0; +} - 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; - } - } +size_t read(MO_File *file, char *buf, size_t len) { + return fread(buf, 1, len, reinterpret_cast(file)); +} - closedir(dir); - return err; - } -}; +int getc(MO_File *file) { + return fgetc(reinterpret_cast(file)); +} -std::weak_ptr filesystemCache; +size_t write(MO_File *file, const char *buf, size_t len) { + return fwrite(buf, 1, len, reinterpret_cast(file)); +} -void resetFilesystemCache(void*) { - filesystemCache.reset(); +int seek(MO_File *file, size_t fpos) { + return fseek(reinterpret_cast(file), fpos, SEEK_SET); } -std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { +} //namespace PosixFS +} //namespace MicroOcpp - if (auto cached = filesystemCache.lock()) { - return cached; - } +using namespace MicroOcpp; - if (!config.accessAllowed()) { - MO_DBG_DEBUG("Access to FS not allowed by config"); +MO_FilesystemAdapter *mo_makeDefaultFilesystemAdapter(MO_FilesystemConfig config) { + + if (config.opt == MO_FS_OPT_DISABLE) { + MO_DBG_DEBUG("Access to filesystem not allowed by opt"); return nullptr; } - if (config.mustMount()) { - MO_DBG_DEBUG("Skip mounting on UNIX host"); + char *prefix_copy = nullptr; + MO_FilesystemAdapter *filesystem = nullptr; + + const char *prefix = config.path_prefix; + size_t prefix_len = prefix ? strlen(prefix) : 0; + if (prefix_len > 0) { + size_t prefix_size = prefix_len + 1; + prefix_copy = static_cast(MO_MALLOC("Filesystem", prefix_size)); + if (!prefix_copy) { + MO_DBG_ERR("OOM"); + goto fail; + } + (void)snprintf(prefix_copy, prefix_size, "%s", prefix); } - auto fs = std::shared_ptr(new PosixFilesystemAdapter(config, resetFilesystemCache), std::default_delete(), makeAllocator("Filesystem")); + filesystem = static_cast(MO_MALLOC("Filesystem", sizeof(MO_FilesystemAdapter))); + if (!filesystem) { + MO_DBG_ERR("OOM"); + goto fail; + } + memset(filesystem, 0, sizeof(MO_FilesystemAdapter)); + + filesystem->path_prefix = prefix_copy; + filesystem->stat = PosixFS::stat; + filesystem->remove = PosixFS::remove; + filesystem->ftw = PosixFS::ftw; + filesystem->open = PosixFS::open; + filesystem->close = PosixFS::close; + filesystem->read = PosixFS::read; + filesystem->getc = PosixFS::getc; + filesystem->write = PosixFS::write; + filesystem->seek = PosixFS::seek; + + // Create MO folder if not exists + struct ::stat st; + int ret; + ret = ::stat(filesystem->path_prefix, &st); + if (ret != 0) { + mkdir(filesystem->path_prefix, 0755); + + auto ret = ::stat(filesystem->path_prefix, &st); + if (ret == 0) { + MO_DBG_INFO("created MO folder at %s", filesystem->path_prefix); + } else { + MO_DBG_ERR("MO folder not found at %s",filesystem->path_prefix); + goto fail; + } + } #if MO_ENABLE_FILE_INDEX - fs = decorateIndex(fs, resetFilesystemCache); -#endif // MO_ENABLE_FILE_INDEX + filesystem = decorateIndex(filesystem); +#endif //MO_ENABLE_FILE_INDEX - filesystemCache = fs; - return fs; + return filesystem; +fail: + MO_FREE(prefix_copy); + MO_FREE(filesystem); + return nullptr; } -} //end namespace MicroOcpp +void mo_freeDefaultFilesystemAdapter(MO_FilesystemAdapter *filesystem) { -#else //filesystem disabled + if (!filesystem) { + return; //noop + } -namespace MicroOcpp { +#if MO_ENABLE_FILE_INDEX + filesystem = resetIndex(filesystem); +#endif //MO_ENABLE_FILE_INDEX -std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { - return nullptr; + //the default FS adapter owns the path_prefix buf and needs to free it + char *path_prefix_buf = const_cast(filesystem->path_prefix); + MO_FREE(path_prefix_buf); + + MO_FREE(filesystem); } -} //end namespace MicroOcpp +#else //filesystem disabled #endif //switch-case MO_USE_FILEAPI diff --git a/src/MicroOcpp/Core/FilesystemAdapter.h b/src/MicroOcpp/Core/FilesystemAdapter.h index 7e92e7cf..86078475 100644 --- a/src/MicroOcpp/Core/FilesystemAdapter.h +++ b/src/MicroOcpp/Core/FilesystemAdapter.h @@ -1,42 +1,41 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_FILESYSTEMADAPTER_H #define MO_FILESYSTEMADAPTER_H -#include -#include - +#include #include -#include -#define DISABLE_FS 0 -#define ARDUINO_LITTLEFS 1 -#define ARDUINO_SPIFFS 2 -#define ESPIDF_SPIFFS 3 -#define POSIX_FILEAPI 4 +#define MO_CUSTOM_FS 0 +#define MO_ARDUINO_LITTLEFS 1 +#define MO_ARDUINO_SPIFFS 2 +#define MO_ESPIDF_SPIFFS 3 +#define MO_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 +#define MO_USE_FILEAPI MO_ARDUINO_LITTLEFS #else -#define MO_USE_FILEAPI ARDUINO_SPIFFS +#define MO_USE_FILEAPI MO_ARDUINO_SPIFFS #endif #elif MO_PLATFORM == MO_PLATFORM_ESPIDF -#define MO_USE_FILEAPI ESPIDF_SPIFFS +#define MO_USE_FILEAPI MO_ESPIDF_SPIFFS #elif MO_PLATFORM == MO_PLATFORM_UNIX -#define MO_USE_FILEAPI POSIX_FILEAPI +#define MO_USE_FILEAPI MO_POSIX_FILEAPI #else -#define MO_USE_FILEAPI DISABLE_FS +#define MO_USE_FILEAPI MO_CUSTOM_FS #endif //switch-case MO_PLATFORM #endif //ndef MO_USE_FILEAPI #ifndef MO_FILENAME_PREFIX -#if MO_USE_FILEAPI == ESPIDF_SPIFFS +#if MO_USE_FILEAPI == MO_ESPIDF_SPIFFS #define MO_FILENAME_PREFIX "/mo_store/" +#elif MO_PLATFORM == MO_PLATFORM_UNIX +#define MO_FILENAME_PREFIX "./mo_store/" #else #define MO_FILENAME_PREFIX "/" #endif @@ -44,7 +43,7 @@ // set default max path size parameters #ifndef MO_MAX_PATH_SIZE -#if MO_USE_FILEAPI == POSIX_FILEAPI +#if MO_USE_FILEAPI == MO_POSIX_FILEAPI #define MO_MAX_PATH_SIZE 128 #else #define MO_MAX_PATH_SIZE 30 @@ -55,26 +54,75 @@ #define MO_ENABLE_FILE_INDEX 0 #endif -namespace MicroOcpp { +#ifdef __cplusplus +extern "C" { +#endif + +struct MO_File; +typedef struct MO_File MO_File; + +typedef struct { + /* Path of the MO root folder, plus trailing '/'. MO will create the path for accessing the filesystem by appending + * a filename to the end of path_prefix. Can be empty if the filesystem implementation accepts the MO filenames as + * path directly. MO doesn't use sub-directories. */ + const char *path_prefix; + + /* Checks if a file at `path` exists and if yes, writes the size of the file content into `size` and returns 0. Returns + * a non-zero code if the file doesn't exist, is a directory or the operation was unsuccessful. + * `path`: path to the file, determined by path_prefix and the MO file name + * `size`: output param for the file content size */ + int (*stat)(const char *path, size_t *size); + + /* Removes the file at `path`. Returns true if after this operation, no file at this path exists anymore, i.e. either + * the file was removed or there was no file. Returns false if the file couldn't be removed. + * `path`: path to the file, determined by path_prefix and the MO file name */ + bool (*remove)(const char *path); + + /* Enumerates all files in the folder at `path` and executes the callback function `fn()` for each file. Passes two + * arguments to `fn()`, the name of the enumerated file (!= path) and the `mo_user_data` pointer which is provided + * to `ftw()`. `ftw()` returns 0 if all `fn()` executions were successful, or the non-zero error code of the failing + * `fn()` execution. + * `path_prefix`: path to the directory to enumerate. `path` may contain a superfluous "/" at the end + * `fn`: callback function to execute for each file. Returns 0 on success and a non-zero value to abort `ftw()` + * `mo_user_data`: pointer to pass through to `fn()` */ + int (*ftw)(const char *path_prefix, int(*fn)(const char *fname, void *mo_user_data), void *mo_user_data); + + /* Opens the file at `path`. The `mode` is either "w" (write) or "r" (read). Returns an implementation-specific file + * handle on success, or NULL on failure. + * `path`: path to the file, determined by path_prefix and the MO file name + * `mode`: "w" (write) or "r" (read) mode */ + MO_File* (*open)(const char *path, const char *mode); + + /* Closes a file which has been opened with `open`. Returns `true` on success. For files in the write mode the success + * code also indicates if the file writes were successful. Deallocates the resources regargless of the error code. + * `file`: file handle */ + bool (*close)(MO_File *file); + + /* Reads `file` contents into `buf` with `len` bytes bufsize. Returns the number of read bytes, or 0 on error or if end + * of file has been reached. If the return value is equal to `len`, MO will exeucte `read` again. + * `file`: file handle + * `buf`: read buffer + * `len`: bufsize in bytes */ + size_t (*read)(MO_File *file, char *buf, size_t len); -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; + int (*getc)(MO_File *file); - virtual int read() = 0; -}; + /* Writes `buf` (with `len` bytes) into `file`. Returns the number of bytes actually written. If the return value is + * less than `len`, that indicates an error and MO will stop writing to this file. + * `file`: file handle + * `buf`: write buffer + * `len`: bufsize in bytes */ + size_t (*write)(MO_File *file, const char *buf, size_t len); -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 -}; + /* Sets the file read- or write-position to `fpos`. For example, if `file` has been opened in read mode and `fpos` is + * 5, then the next `read` operation will continue from the 5th byte of the file. Returns 0 on success and a non-zero + * error code on failue. + * `file`: file handle + * `fpos`: file position */ + int (*seek)(MO_File *file, size_t fpos); + + void *mo_data; //reserved for MO internal usage +} MO_FilesystemAdapter; /* * Platform specific implementation. Currently supported: @@ -82,13 +130,43 @@ class FilesystemAdapter { * - 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 +#if MO_USE_FILEAPI != MO_CUSTOM_FS + +typedef enum { + MO_FS_OPT_DISABLE, + MO_FS_OPT_USE, + MO_FS_OPT_USE_MOUNT, + MO_FS_OPT_USE_MOUNT_FORMAT_ON_FAIL +} MO_FilesystemOpt; + +typedef struct { + MO_FilesystemOpt opt; //FS access level and mounting strategy + + /* Path of the MO root folder, plus trailing '/'. MO will create the path for accessing the filesystem by appending + * a filename to the end of path_prefix. Can be empty if the filesystem implementation accepts the MO filenames as + * path directly. MO doesn't use sub-directories. */ + const char *path_prefix; + + char *internalBuf; //used by MO internally +} MO_FilesystemConfig; + +void mo_filesystemConfig_init(MO_FilesystemConfig *config); +bool mo_filesystemConfig_copy(MO_FilesystemConfig *dst, MO_FilesystemConfig *src); +void mo_filesystemConfig_deinit(MO_FilesystemConfig *config); + +MO_FilesystemAdapter *mo_makeDefaultFilesystemAdapter(MO_FilesystemConfig config); +void mo_freeDefaultFilesystemAdapter(MO_FilesystemAdapter *filesystem); + +#endif //MO_USE_FILEAPI != MO_CUSTOM_FS + +#ifdef __cplusplus +} +#endif #endif diff --git a/src/MicroOcpp/Core/FilesystemUtils.cpp b/src/MicroOcpp/Core/FilesystemUtils.cpp index de6a034d..e10f8eca 100644 --- a/src/MicroOcpp/Core/FilesystemUtils.cpp +++ b/src/MicroOcpp/Core/FilesystemUtils.cpp @@ -2,40 +2,58 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + #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; +bool FilesystemUtils::printPath(MO_FilesystemAdapter *filesystem, char *path, size_t size, const char *fname, ...) { + + size_t written = 0; + int ret; + + // write path_prefix + ret = snprintf(path, size, "%s", filesystem->path_prefix ? filesystem->path_prefix : ""); + if (ret < 0 || written + (size_t)ret >= size) { + MO_DBG_ERR("path buffer insufficient"); + return false; } + written += (size_t)ret; - 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; + // write fname + va_list args; + va_start(args, fname); + ret = vsnprintf(path + written, size - written, fname, args); + va_end(args); + if (ret < 0 || written + (size_t)ret >= size) { + MO_DBG_ERR("path buffer insufficient, fname too long or fname error"); + return false; } - - size_t fsize = 0; - if (filesystem->stat(fn, &fsize) != 0) { - MO_DBG_DEBUG("File does not exist: %s", fn); - return nullptr; + written += (size_t)ret; + + return true; +} + +FilesystemUtils::LoadStatus FilesystemUtils::loadJson(MO_FilesystemAdapter *filesystem, const char *fname, JsonDoc& doc, const char *memoryTag) { + + char path [MO_MAX_PATH_SIZE]; + if (!printPath(filesystem, path, sizeof(path), "%s", fname)) { + MO_DBG_ERR("fname too long: %s", fname); + return FilesystemUtils::LoadStatus::ErrOther; } - if (fsize < 2) { - MO_DBG_ERR("File too small for JSON, collect %s", fn); - filesystem->remove(fn); - return nullptr; + size_t fsize = 0; + if (filesystem->stat(path, &fsize) != 0) { + return FilesystemUtils::LoadStatus::FileNotFound; } - auto file = filesystem->open(fn, "r"); + auto file = filesystem->open(path, "r"); if (!file) { - MO_DBG_ERR("Could not open file %s", fn); - return nullptr; + MO_DBG_ERR("Could not open file %s", fname); + return FilesystemUtils::LoadStatus::ErrFileCorruption; } size_t capacity_init = (3 * fsize) / 2; @@ -49,92 +67,196 @@ std::unique_ptr FilesystemUtils::loadJson(std::shared_ptr MO_MAX_JSON_CAPACITY) { capacity = MO_MAX_JSON_CAPACITY; } - - std::unique_ptr doc; + DeserializationError err = DeserializationError::NoMemory; - ArduinoJsonFileAdapter fileReader {file.get()}; + ArduinoJsonFileAdapter fileReader {filesystem, file}; while (err == DeserializationError::NoMemory && capacity <= MO_MAX_JSON_CAPACITY) { - doc = makeJsonDoc(memoryTag, capacity); - err = deserializeJson(*doc, fileReader); + doc = std::move(initJsonDoc(memoryTag, capacity)); + if (doc.capacity() < capacity) { + MO_DBG_ERR("OOM"); + (void)filesystem->close(file); + return FilesystemUtils::LoadStatus::ErrOOM; + } + + err = deserializeJson(doc, fileReader); capacity *= 2; - file->seek(0); //rewind file to beginning + filesystem->seek(file, 0); //rewind file to beginning } + (void)filesystem->close(file); + if (err) { - MO_DBG_ERR("Error deserializing file %s: %s", fn, err.c_str()); + MO_DBG_ERR("Error deserializing file %s: %s", fname, err.c_str()); //skip this file - return nullptr; + return FilesystemUtils::LoadStatus::ErrFileCorruption; } - MO_DBG_DEBUG("Loaded JSON file: %s", fn); + MO_DBG_DEBUG("Loaded JSON file: %s", fname); - return doc; + return FilesystemUtils::LoadStatus::Success;; } -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; - } +FilesystemUtils::StoreStatus FilesystemUtils::storeJson(MO_FilesystemAdapter *filesystem, const char *fname, const JsonDoc& doc) { - 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; + char path [MO_MAX_PATH_SIZE]; + if (!printPath(filesystem, path, sizeof(path), "%s", fname)) { + MO_DBG_ERR("fname too long: %s", fname); + return FilesystemUtils::StoreStatus::ErrOther; } if (doc.isNull() || doc.overflowed()) { - MO_DBG_ERR("Invalid JSON %s", fn); - return false; + MO_DBG_ERR("Invalid JSON %s", fname); + return FilesystemUtils::StoreStatus::ErrOther; } - auto file = filesystem->open(fn, "w"); + auto file = filesystem->open(path, "w"); if (!file) { - MO_DBG_ERR("Could not open file %s", fn); - return false; + MO_DBG_ERR("Could not open file %s", fname); + return FilesystemUtils::StoreStatus::ErrFileWrite; } - ArduinoJsonFileAdapter fileWriter {file.get()}; + ArduinoJsonFileAdapter fileWriter (filesystem, file); - size_t written = serializeJson(doc, fileWriter); + size_t written; + if (MO_PLATFORM == MO_PLATFORM_UNIX) { + //when debugging on Linux, add line breaks and white spaces for better readibility + written = serializeJsonPretty(doc, fileWriter); + } else { + 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_ERR("Error writing file %s", fname); + (void)filesystem->close(file); + return FilesystemUtils::StoreStatus::ErrFileWrite; } - MO_DBG_DEBUG("Wrote JSON file: %s", fn); - return true; + bool ret = filesystem->close(file); + if (!ret) { + MO_DBG_ERR("Error writing file %s", fname); + return FilesystemUtils::StoreStatus::ErrFileWrite; + } + + MO_DBG_DEBUG("Wrote JSON file: %s", fname); + + return FilesystemUtils::StoreStatus::Success; } -bool FilesystemUtils::remove_if(std::shared_ptr filesystem, std::function pred) { - auto ret = filesystem->ftw_root([filesystem, pred] (const char *fpath) { - if (pred(fpath)) { +namespace MicroOcpp { +namespace FilesystemUtils { - 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; +struct RemoveByPrefixData { + MO_FilesystemAdapter *filesystem; + const char *fnamePrefix; +}; + +} //namespace FilesystemUtils +} //namespace MicroOcpp + +bool FilesystemUtils::removeByPrefix(MO_FilesystemAdapter *filesystem, const char *fnamePrefix) { + + RemoveByPrefixData data; + data.filesystem = filesystem; + data.fnamePrefix = fnamePrefix; + + auto ret = filesystem->ftw(filesystem->path_prefix, [] (const char *fname, void* user_data) -> int { + auto data = reinterpret_cast(user_data); + auto filesystem = data->filesystem; + auto fnamePrefix = data->fnamePrefix; + + //check if fname starts with fnamePrefix + for (size_t i = 0; fnamePrefix[i] != '\0'; i++) { + if (fname[i] != fnamePrefix[i]) { + //not a prefix of fname - don't do anything on this file + return 0; } + } - filesystem->remove(fn); - //no error handling - just skip failed file + char path [MO_MAX_PATH_SIZE]; + if (!printPath(filesystem, path, sizeof(path), "%s", fname)) { + MO_DBG_ERR("fname too long: %s", fname); + return -1; } + + (void)filesystem->remove(path); //no error handling - just skip failed file return 0; - }); + }, reinterpret_cast(&data)); + + return ret == 0; +} + +namespace MicroOcpp { +namespace FilesystemUtils { + +struct RingIndexData { + const char *fnamePrefix; + unsigned int MAX_INDEX; + unsigned int *indexBegin; + unsigned int *indexEnd; + + size_t fnamePrefixLen; + unsigned int indexPivot; +}; + +int updateRingIndex(const char *fname, void* user_data) { + FilesystemUtils::RingIndexData& data = *reinterpret_cast(user_data); + const char *fnamePrefix = data.fnamePrefix; + unsigned int MAX_INDEX = data.MAX_INDEX; + unsigned int& indexBegin = *data.indexBegin; + unsigned int& indexEnd = *data.indexEnd; + size_t fnamePrefixLen = data.fnamePrefixLen; + unsigned int& indexPivot = data.indexPivot; - if (ret != 0) { - MO_DBG_ERR("ftw_root: %i", ret); + if (!strncmp(fname, fnamePrefix, fnamePrefixLen)) { + unsigned int parsedIndex = 0; + for (size_t i = fnamePrefixLen; fname[i] >= '0' && fname[i] <= '9'; i++) { + parsedIndex *= 10; + parsedIndex += fname[i] - '0'; + } + + if (indexPivot == MAX_INDEX) { + indexPivot = parsedIndex; + indexBegin = parsedIndex; + indexEnd = (parsedIndex + 1) % MAX_INDEX; + MO_DBG_DEBUG("found %s%u.jsn - Internal range from %u to %u (exclusive)", fnamePrefix, parsedIndex, indexBegin, indexEnd); + return 0; + } + + if ((parsedIndex + MAX_INDEX - indexPivot) % MAX_INDEX < MAX_INDEX / 2) { + //parsedIndex is after pivot point + if ((parsedIndex + 1 + MAX_INDEX - indexPivot) % MAX_INDEX > (indexEnd + MAX_INDEX - indexPivot) % MAX_INDEX) { + indexEnd = (parsedIndex + 1) % MAX_INDEX; + } + } else if ((indexPivot + MAX_INDEX - parsedIndex) % MAX_INDEX < MAX_INDEX / 2) { + //parsedIndex is before pivot point + if ((indexPivot + MAX_INDEX - parsedIndex) % MAX_INDEX > (indexPivot + MAX_INDEX - indexBegin) % MAX_INDEX) { + indexBegin = parsedIndex; + } + } + + MO_DBG_DEBUG("found %s%u.jsn - Internal range from %u to %u (exclusive)", fnamePrefix, parsedIndex, indexBegin, indexEnd); } + return 0; +} + +} //namespace FilesystemUtils +} //namespace MicroOcpp + +bool FilesystemUtils::loadRingIndex(MO_FilesystemAdapter *filesystem, const char *fnamePrefix, unsigned int MAX_INDEX, unsigned int *indexBegin, unsigned int *indexEnd) { + + FilesystemUtils::RingIndexData data; + data.fnamePrefix = fnamePrefix; + data.MAX_INDEX = MAX_INDEX; + data.indexBegin = indexBegin; + data.indexEnd = indexEnd; + data.fnamePrefixLen = strlen(fnamePrefix); + data.indexPivot = MAX_INDEX; + + auto ret = filesystem->ftw(filesystem->path_prefix, updateRingIndex, reinterpret_cast(&data)); return ret == 0; } diff --git a/src/MicroOcpp/Core/FilesystemUtils.h b/src/MicroOcpp/Core/FilesystemUtils.h index 31a36a73..2073bc37 100644 --- a/src/MicroOcpp/Core/FilesystemUtils.h +++ b/src/MicroOcpp/Core/FilesystemUtils.h @@ -8,42 +8,80 @@ #include #include #include -#include namespace MicroOcpp { class ArduinoJsonFileAdapter { private: - FileAdapter *file; + MO_FilesystemAdapter *filesystem = nullptr; + MO_File *file = nullptr; public: - ArduinoJsonFileAdapter(FileAdapter *file) : file(file) { } + ArduinoJsonFileAdapter(MO_FilesystemAdapter *filesystem, MO_File *file) : filesystem(filesystem), file(file) { } size_t readBytes(char *buf, size_t len) { - return file->read(buf, len); + return filesystem->read(file, buf, len); } int read() { - return file->read(); + return filesystem->getc(file); } size_t write(const uint8_t *buf, size_t len) { - return file->write((const char*) buf, len); + return filesystem->write(file, (const char*) buf, len); } size_t write(uint8_t c) { - return file->write((const char*) &c, 1); + return filesystem->write(file, (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); +/* Determines path of a file given the filename and the path_prefix defined in the filesystem adapter. + * `path`: array which should have at least MO_MAX_PATH_SIZE bytes (including the terminating zero) + * `size`: number of bytes of `path` */ +bool printPath(MO_FilesystemAdapter *filesystem, char *path, size_t size, const char *fname, ...); -bool remove_if(std::shared_ptr filesystem, std::function pred); +enum class LoadStatus : uint8_t { + Success, //file operation successful + FileNotFound, + ErrOOM, + ErrFileCorruption, + ErrOther, //file operation is not possible with the given parameters +}; +LoadStatus loadJson(MO_FilesystemAdapter *filesystem, const char *fname, JsonDoc& doc, const char *memoryTag); + +enum class StoreStatus : uint8_t { + Success, //file operation successful + ErrFileWrite, + ErrJsonCorruption, + ErrOther, //file operation is not possible with the given parameters +}; +StoreStatus storeJson(MO_FilesystemAdapter *filesystem, const char *fname, const JsonDoc& doc); + +/* + * Removes files in the MO folder whose file names start with `fnamePrefix`. + */ +bool removeByPrefix(MO_FilesystemAdapter *filesystem, const char *fnamePrefix); -} +/* + * Loads index for data queue on flash which is organized as a ring buffer. For example, Security + * events are stored on flash like this: + * sloc-4.jsn + * sloc-5.jsn + * sloc-6.jsn + * Then the `indexBegin` is 4 and `indexEnd` is 7 (one after last element). The size of the queue is + * `indexEnd - indexBegin = 7 - 4 = 3`. If the queue is empty, then `indexEnd - indexBegin = 0`. + * Parameters (and example values): + * `filesystem`: filesystem adapter + * `fnamePrefix`: common prefix of the filenames (e.g. "sloc-") + * `MAX_INDEX`: range end of the index (e.g. if the index ranges from 0 - 999, then `MAX_INDEX = 1000`) + * `*indexBegin`: output parameter for the first file in the queue (e.g. 4) + * `*indexEnd`: output parameter for the end of the queue (e.g. 7) + */ +bool loadRingIndex(MO_FilesystemAdapter *filesystem, const char *fnamePrefix, unsigned int MAX_INDEX, unsigned int *indexBegin, unsigned int *indexEnd); -} +} //namespace FilesystemUtils +} //namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Core/Ftp.h b/src/MicroOcpp/Core/Ftp.h index 053f9e01..31243e14 100644 --- a/src/MicroOcpp/Core/Ftp.h +++ b/src/MicroOcpp/Core/Ftp.h @@ -17,43 +17,43 @@ typedef enum { MO_FtpCloseReason_Failure } MO_FtpCloseReason; -typedef struct ocpp_ftp_download { +typedef struct mo_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; +} mo_ftp_download; -typedef struct ocpp_ftp_upload { +typedef struct mo_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; +} mo_ftp_upload; -typedef struct ocpp_ftp_client { +typedef struct mo_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, + mo_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*); + void (*get_file_free)(void *user_data, mo_ftp_download*); - ocpp_ftp_upload* (*post_file)(void *user_data, + mo_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; + void (*post_file_free)(void *user_data, mo_ftp_upload*); +} mo_ftp_client; #ifdef __cplusplus } //extern "C" @@ -94,6 +94,6 @@ class FtpClient { const char *ca_cert = nullptr) = 0; // nullptr to disable cert check; will be ignored for non-TLS connections }; -} // namespace MicroOcpp +} //namespace MicroOcpp #endif //def __cplusplus #endif diff --git a/src/MicroOcpp/Core/FtpMbedTLS.cpp b/src/MicroOcpp/Core/FtpMbedTLS.cpp index b40015b9..bc6f1b9a 100644 --- a/src/MicroOcpp/Core/FtpMbedTLS.cpp +++ b/src/MicroOcpp/Core/FtpMbedTLS.cpp @@ -61,7 +61,7 @@ class FtpTransferMbedTLS : public FtpUpload, public FtpDownload, public MemoryMa 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; @@ -97,7 +97,7 @@ class FtpTransferMbedTLS : public FtpUpload, public FtpDownload, public MemoryMa ~FtpTransferMbedTLS(); void loop() override; - + bool isActive() override; bool getFile(const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename @@ -131,8 +131,8 @@ class FtpClientMbedTLS : public FtpClient, public MemoryManaged { 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)); +std::unique_ptr makeFtpClientMbedTLS(MO_FTPConfig config) { + return std::unique_ptr(new FtpClientMbedTLS(config.tls_only, config.client_cert, config.client_key)); } void mo_mbedtls_log(void *user, int level, const char *file, int line, const char *str) { @@ -144,26 +144,32 @@ void mo_mbedtls_log(void *user, int level, const char *file, int line, const cha * - 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); + int mo_level = MO_DL_VERBOSE; + switch (level) { + case 1: + mo_level = MO_DL_ERROR; + break; + case 2: + case 3: + mo_level = MO_DL_DEBUG; + break; + case 4: + mo_level = MO_DL_VERBOSE; + break; + } + + MicroOcpp::debug(mo_level, file, line, "%s", str); } /* * FTP implementation */ -FtpTransferMbedTLS::FtpTransferMbedTLS(bool tls_only, const char *client_cert, const char *client_key) : +FtpTransferMbedTLS::FtpTransferMbedTLS(bool tls_only, const char *client_cert, const char *client_key) : MemoryManaged("FTP.TransferMbedTLS"), client_cert(client_cert), client_key(client_key), @@ -321,7 +327,7 @@ int FtpTransferMbedTLS::connect_data() { if (isSecure) { //reuse SSL session of ctrl conn - if (auto ret = mbedtls_ssl_set_session(&data_ssl, + 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; @@ -415,7 +421,7 @@ void FtpTransferMbedTLS::send_cmd(const char *cmd, const char *arg, bool disable const size_t MSG_SIZE = 128; unsigned char msg [MSG_SIZE]; - auto len = snprintf((char*) msg, MSG_SIZE, "%s%s%s\r\n", + 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") @@ -424,7 +430,7 @@ void FtpTransferMbedTLS::send_cmd(const char *cmd, const char *arg, bool disable len = sprintf((char*) msg, "QUIT\r\n"); } else { //show outgoing traffic for debug, but shadow PASS - MO_DBG_DEBUG("SEND: %s %s", + MO_DBG_DEBUG("SEND: %s %s", cmd, !strncmp((char*) cmd, "PASS", strlen("PASS")) ? "***" : arg ? (char*) arg : ""); } @@ -458,7 +464,7 @@ bool FtpTransferMbedTLS::getFile(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; @@ -601,7 +607,7 @@ void FtpTransferMbedTLS::process_ctrl() { } //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 @@ -935,7 +941,7 @@ std::unique_ptr FtpClientMbedTLS::getFile(const char *ftp_url_raw, } 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"); diff --git a/src/MicroOcpp/Core/FtpMbedTLS.h b/src/MicroOcpp/Core/FtpMbedTLS.h index 799b3905..d7572b1b 100644 --- a/src/MicroOcpp/Core/FtpMbedTLS.h +++ b/src/MicroOcpp/Core/FtpMbedTLS.h @@ -9,16 +9,16 @@ * 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 @@ -29,9 +29,19 @@ #include +extern "C" { + +typedef struct { + bool tls_only; + const char *client_cert; //zero-copy. client_cert must outlive MO lifecycle. Can be NULL + const char *client_key; //zero-copy. client_key must outlive MO lifecycle. Can be NULL +} MO_FTPConfig; + +} //extern "C" + namespace MicroOcpp { -std::unique_ptr makeFtpClientMbedTLS(bool tls_only = false, const char *client_cert = nullptr, const char *client_key = nullptr); +std::unique_ptr makeFtpClientMbedTLS(MO_FTPConfig config); } //namespace MicroOcpp diff --git a/src/MicroOcpp/Core/Memory.cpp b/src/MicroOcpp/Core/Memory.cpp index a352e9ac..a89203b8 100644 --- a/src/MicroOcpp/Core/Memory.cpp +++ b/src/MicroOcpp/Core/Memory.cpp @@ -199,7 +199,7 @@ void mo_mem_set_tag(void *ptr, const char *tag) { void mo_mem_print_stats() { - MO_CONSOLE_PRINTF("\n *** Heap usage statistics ***\n"); + MO_DBG_INFO("\n\n *** Heap usage statistics ***"); size_t size = 0; @@ -209,7 +209,7 @@ void mo_mem_print_stats() { 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()); + MO_DBG_INFO("@%p - %zu B (%s)\n", heapEntry.first, heapEntry.second.size, heapEntry.second.tag.c_str()); } #endif @@ -235,7 +235,7 @@ void mo_mem_print_stats() { size_control += tag.second; #if MO_DBG_LEVEL >= MO_DL_VERBOSE { - MO_CONSOLE_PRINTF("%s - %zu B\n", tag.first.c_str(), tag.second); + MO_DBG_INFO("%s - %zu B\n", tag.first.c_str(), tag.second); } #endif } @@ -243,13 +243,13 @@ void mo_mem_print_stats() { 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_DBG_INFO("%s - %zu B (max. %zu B)", 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); + MO_DBG_INFO("\n *** 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); + MO_DBG_INFO("\n *** 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 } @@ -319,7 +319,12 @@ JsonDoc initJsonDoc(const char *tag, size_t capacity) { std::unique_ptr makeJsonDoc(const char *tag, size_t capacity) { #if MO_OVERRIDE_ALLOCATION - return std::unique_ptr(new JsonDoc(capacity, ArduinoJsonAllocator(tag))); + auto doc = std::unique_ptr(new JsonDoc(capacity, ArduinoJsonAllocator(tag))); + if (!doc || doc->capacity() < capacity) { + MO_DBG_ERR("OOM"); + return nullptr; + } + return doc; #else return std::unique_ptr(new JsonDoc(capacity)); #endif diff --git a/src/MicroOcpp/Core/Memory.h b/src/MicroOcpp/Core/Memory.h index bca25a84..7647c2a7 100644 --- a/src/MicroOcpp/Core/Memory.h +++ b/src/MicroOcpp/Core/Memory.h @@ -389,7 +389,7 @@ namespace MicroOcpp { class MemoryManaged { protected: const char *getMemoryTag() const {return nullptr;} - void updateMemoryTag(const char*,const char*) { } + void updateMemoryTag(const char*,const char*) { } public: MemoryManaged() { } MemoryManaged(const char*) { } diff --git a/src/MicroOcpp/Core/MessageService.cpp b/src/MicroOcpp/Core/MessageService.cpp new file mode 100644 index 00000000..c9bffacf --- /dev/null +++ b/src/MicroOcpp/Core/MessageService.cpp @@ -0,0 +1,498 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace MicroOcpp { +size_t removePayload(const char *src, size_t src_size, char *dst, size_t dst_size); +} + +using namespace MicroOcpp; + +MessageService::MessageService(Context& context) : + MemoryManaged("MessageService"), + context(context), + operationRegistry(makeVector(getMemoryTag())), + operationRegistry2(makeVector(getMemoryTag())), + receiveRequestListeners(makeVector(getMemoryTag())), + sendConfListeners(makeVector(getMemoryTag())) { + + addSendQueue(&defaultSendQueue); +} + +bool MessageService::setup() { + connection = context.getConnection(); + if (!connection) { + MO_DBG_ERR("need to set Connection before MessageService setup"); + return false; + } + return true; +} + +void MessageService::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) { + #ifdef MO_TRAFFIC_OUT + MO_DBG_INFO("Send %s", out.c_str()); + #endif + 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 = RequestQueue::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) { + #ifdef MO_TRAFFIC_OUT + MO_DBG_INFO("Send %s", out.c_str()); + #endif + sendReqFront->setRequestSent(); //mask as sent and wait for response / timeout + } + + return; + } + } +} + +bool MessageService::sendRequest(std::unique_ptr op){ + return defaultSendQueue.pushRequestBack(std::move(op)); +} + +bool MessageService::sendRequestPreBoot(std::unique_ptr op){ + if (!preBootSendQueue) { + MO_DBG_ERR("did not set PreBoot queue"); + return false; + } + return preBootSendQueue->pushRequestBack(std::move(op)); +} + +void MessageService::addSendQueue(RequestQueue* 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 MessageService::setPreBootSendQueue(VolatileRequestQueue *preBootQueue) { + this->preBootSendQueue = preBootQueue; + addSendQueue(preBootQueue); +} + +unsigned int MessageService::getNextOpNr() { + return nextOpNr++; +} + +namespace MicroOcpp { + +template +T *getEntry(Vector& listeners, const char *operationType) { + for (size_t i = 0; i < listeners.size(); i++) { + if (!strcmp(listeners[i].operationType, operationType)) { + return &listeners[i]; + } + } + return nullptr; +} + +template +void removeEntry(Vector& listeners, const char *operationType) { + for (auto it = listeners.begin(); it != listeners.end();) { + if (!strcmp(operationType, it->operationType)) { + it = listeners.erase(it); + } else { + it++; + } + } +} + +template +bool appendEntry(Vector& listeners, const T& el) { + bool capacity = listeners.size() + 1; + listeners.reserve(capacity); + if (listeners.capacity() < capacity) { + MO_DBG_ERR("OOM"); + return false; + } + + listeners.push_back(el); + return true; +} + +} //namespace MicroOcpp + +bool MessageService::registerOperation(const char *operationType, Operation* (*createOperationCb)(Context& context)) { + + bool exists = false; + + exists |= (getEntry(operationRegistry, operationType) != nullptr); + exists |= (getEntry(operationRegistry2, operationType) != nullptr); + + if (exists) { + MO_DBG_DEBUG("operation creator %s already exists - do not replace", operationType); + return true; + } + + OperationCreator entry; + entry.operationType = operationType; + entry.create = createOperationCb; + + MO_DBG_DEBUG("registered operation %s", operationType); + return appendEntry(operationRegistry, entry); +} + +bool MessageService::registerOperation(const char *operationType, void (*onRequest)(const char *operationType, const char *payloadJson, void **userStatus, void *userData), int (*writeResponse)(const char *operationType, char *buf, size_t size, void *userStatus, void *userData), void (*finally)(const char *operationType, void *userStatus, void *userData), void *userData) { + + bool exists = false; + + exists |= (getEntry(operationRegistry, operationType) != nullptr); + exists |= (getEntry(operationRegistry2, operationType) != nullptr); + + if (exists) { + MO_DBG_DEBUG("operation creator %s already exists - do not replace", operationType); + return true; + } + + CustomOperationCreator entry; + entry.operationType = operationType; + entry.userData = userData; + entry.onRequest = onRequest; + entry.writeResponse = writeResponse; + entry.finally = finally; + + MO_DBG_DEBUG("registered operation %s", operationType); + return appendEntry(operationRegistry2, entry); +} + +bool MessageService::setOnReceiveRequest(const char *operationType, void (*onRequest)(const char *operationType, const char *payloadJson, void *userData), void *userData) { + bool exists = (getEntry(receiveRequestListeners, operationType)) != nullptr; + if (exists) { + MO_DBG_DEBUG("operation onRequest %s already exists - do not replace", operationType); + return true; + } + MO_DBG_DEBUG("set onRequest %s", operationType); + OperationListener listener; + listener.operationType = operationType; + listener.onEvent = onRequest; + listener.userData = userData; + return appendEntry(receiveRequestListeners, listener); +} + +bool MessageService::setOnSendConf(const char *operationType, void (*onConfirmation)(const char *operationType, const char *payloadJson, void *userData), void *userData) { + bool exists = (getEntry(sendConfListeners, operationType)) != nullptr; + if (exists) { + MO_DBG_DEBUG("operation onConfirmation %s already exists - do not replace", operationType); + return true; + } + MO_DBG_DEBUG("set onConfirmation %s", operationType); + OperationListener listener; + listener.operationType = operationType; + listener.onEvent = onConfirmation; + listener.userData = userData; + return appendEntry(sendConfListeners, listener); +} + +std::unique_ptr MessageService::createRequest(const char *operationType) { + + Operation *operation = nullptr; + if (auto entry = getEntry(operationRegistry, operationType)) { + operation = entry->create(context); + if (!operation) { + MO_DBG_ERR("create operation failure"); + return nullptr; + } + } + + if (!operation) { + if (auto entry = getEntry(operationRegistry2, operationType)) { + auto customOperation = new CustomOperation(); + if (!customOperation) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + if (!customOperation->setupCsmsInitiated( + entry->operationType, + entry->onRequest, + entry->writeResponse, + entry->finally, + entry->userData)) { + MO_DBG_ERR("create operation failure"); + delete customOperation; + return nullptr; + } + + operation = static_cast(customOperation); + } + } + + if (!operation) { + auto notImplemented = new NotImplemented(); + if (!notImplemented) { + MO_DBG_ERR("OOM"); + return nullptr; + } + auto request = makeRequest(context, notImplemented); + if (!request) { + MO_DBG_ERR("OOM"); + return nullptr; + } + return request; + } + + auto request = makeRequest(context, operation); + if (!request) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + if (auto entry = getEntry(receiveRequestListeners, operationType)) { + request->setOnReceiveRequest(entry->onEvent, entry->userData); + } + + if (auto entry = getEntry(sendConfListeners, operationType)) { + request->setOnSendConf(entry->onEvent, entry->userData); + } + + return request; +} + +bool MessageService::receiveMessage(const char* payload, size_t length) { + + #ifdef MO_TRAFFIC_OUT + MO_DBG_INFO("Recv %.*s", (int)length, payload); + #endif + + 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(context, 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 MessageService::receiveResponse(JsonArray json) { + + if (!sendReqFront || !sendReqFront->receiveResponse(json)) { + MO_DBG_WARN("Received response doesn't match pending operation"); + } + + sendReqFront.reset(); +} + +void MessageService::receiveRequest(JsonArray json) { + if (json.size() <= 2 || !json[2].is()) { + MO_DBG_ERR("malformatted request"); + return; + } + auto request = createRequest(json[2]); + if (request == nullptr) { + MO_DBG_WARN("OOM"); + return; + } + receiveRequest(json, std::move(request)); +} + +void MessageService::receiveRequest(JsonArray json, std::unique_ptr request) { + request->receiveRequest(json); //execute the operation + recvQueue.pushRequestBack(std::move(request)); //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 MicroOcpp::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; +} diff --git a/src/MicroOcpp/Core/MessageService.h b/src/MicroOcpp/Core/MessageService.h new file mode 100644 index 00000000..a34a92af --- /dev/null +++ b/src/MicroOcpp/Core/MessageService.h @@ -0,0 +1,100 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#ifndef MO_MESSAGESERVICE_H +#define MO_MESSAGESERVICE_H + +#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 Context; +class Operation; + +struct OperationCreator { + const char *operationType = nullptr; + Operation* (*create)(Context& context) = nullptr; +}; + +struct CustomOperationCreator { + const char *operationType = nullptr; + void *userData = nullptr; + void (*onRequest)(const char *operationType, const char *payloadJson, void **userStatus, void *userData) = nullptr; + int (*writeResponse)(const char *operationType, char *buf, size_t size, void *userStatus, void *userData) = nullptr; + void (*finally)(const char *operationType, void *userStatus, void *userData) = nullptr; +}; + +struct OperationListener { + const char *operationType = nullptr; + void *userData = nullptr; + void (*onEvent)(const char *operationType, const char *payloadJson, void *userData) = nullptr; +}; + +class Connection; + +class MessageService : public MemoryManaged { +private: + Context& context; + Connection *connection = nullptr; + + RequestQueue* sendQueues [MO_NUM_REQUEST_QUEUES] {nullptr}; + VolatileRequestQueue defaultSendQueue; + VolatileRequestQueue *preBootSendQueue = nullptr; + std::unique_ptr sendReqFront; + + VolatileRequestQueue recvQueue; + std::unique_ptr recvReqFront; + + Vector operationRegistry; + Vector operationRegistry2; + Vector receiveRequestListeners; + Vector sendConfListeners; + + void receiveRequest(JsonArray json); + void receiveRequest(JsonArray json, std::unique_ptr op); + void receiveResponse(JsonArray json); + std::unique_ptr createRequest(const char *operationType); + + unsigned int nextOpNr = 10; //Nr 0 - 9 reservered for internal purposes +public: + MessageService(Context& context); + + bool setup(); + + void loop(); //polls all reqQueues and decides which request to send (if any) + + // send a Request to the OCPP server + bool sendRequest(std::unique_ptr request); //send an OCPP operation request to the server; adds request to default queue + bool sendRequestPreBoot(std::unique_ptr request); //send an OCPP operation request to the server; adds request to preBootQueue + + // handle Requests from the OCPP Server + bool registerOperation(const char *operationType, Operation* (*createOperationCb)(Context& context)); + bool registerOperation(const char *operationType, void (*onRequest)(const char *operationType, const char *payloadJson, void **userStatus, void *userData), int (*writeResponse)(const char *operationType, char *buf, size_t size, void *userStatus, void *userData), void (*finally)(const char *operationType, void *userStatus, void *userData) = nullptr, void *userData = nullptr); + bool setOnReceiveRequest(const char *operationType, void (*onRequest)(const char *operationType, const char *payloadJson, void *userData), void *userData = nullptr); + bool setOnSendConf(const char *operationType, void (*onConfirmation)(const char *operationType, const char *payloadJson, void *userData), void *userData = nullptr); + + // process message from server ("message" = serialized Request or Confirmation) + bool receiveMessage(const char* payload, size_t length); + + // Outgoing Requests are handled in separate queues. + void addSendQueue(RequestQueue* sendQueue); + void setPreBootSendQueue(VolatileRequestQueue *preBootQueue); + + unsigned int getNextOpNr(); +}; + +} //namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Core/OcppError.h b/src/MicroOcpp/Core/OcppError.h index 826c3215..06ec6a9a 100644 --- a/src/MicroOcpp/Core/OcppError.h +++ b/src/MicroOcpp/Core/OcppError.h @@ -40,5 +40,5 @@ class MsgBufferExceeded : public Operation, public MemoryManaged { } }; -} //end namespace MicroOcpp +} //namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Core/Operation.cpp b/src/MicroOcpp/Core/Operation.cpp index 1289381b..8c618090 100644 --- a/src/MicroOcpp/Core/Operation.cpp +++ b/src/MicroOcpp/Core/Operation.cpp @@ -11,7 +11,7 @@ using namespace MicroOcpp; Operation::Operation() {} Operation::~Operation() {} - + const char* Operation::getOperationType(){ MO_DBG_ERR("Unsupported operation: getOperationType() is not implemented"); return "CustomOperation"; diff --git a/src/MicroOcpp/Core/Operation.h b/src/MicroOcpp/Core/Operation.h index 9c1756a1..8dbe8f8c 100644 --- a/src/MicroOcpp/Core/Operation.h +++ b/src/MicroOcpp/Core/Operation.h @@ -6,10 +6,10 @@ * 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. @@ -31,14 +31,14 @@ class Operation { 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. */ @@ -46,14 +46,11 @@ class Operation { 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;} + + virtual void processErr(const char *code, const char *description, JsonObject details) {} /** - * Processes the request in the JSON document. + * Processes the request in the JSON document. */ virtual void processReq(JsonObject payload); @@ -68,5 +65,5 @@ class Operation { virtual std::unique_ptr getErrorDetails() {return createEmptyDocument();} }; -} //end namespace MicroOcpp +} //namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Core/OperationRegistry.cpp b/src/MicroOcpp/Core/OperationRegistry.cpp deleted file mode 100644 index 0bd17d45..00000000 --- a/src/MicroOcpp/Core/OperationRegistry.cpp +++ /dev/null @@ -1,80 +0,0 @@ -// 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); - } -} diff --git a/src/MicroOcpp/Core/OperationRegistry.h b/src/MicroOcpp/Core/OperationRegistry.h deleted file mode 100644 index 0d0d44b5..00000000 --- a/src/MicroOcpp/Core/OperationRegistry.h +++ /dev/null @@ -1,44 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Core/PersistencyUtils.cpp b/src/MicroOcpp/Core/PersistencyUtils.cpp new file mode 100644 index 00000000..494a3bc7 --- /dev/null +++ b/src/MicroOcpp/Core/PersistencyUtils.cpp @@ -0,0 +1,154 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#include + +#include +#include +#include + +using namespace MicroOcpp; + +bool PersistencyUtils::loadBootStats(MO_FilesystemAdapter *filesystem, BootStats& bstats) { + + JsonDoc json {0}; + bool success = false; + + auto ret = FilesystemUtils::loadJson(filesystem, "bootstats.jsn", json, "PersistencyUtils"); + switch (ret) { + case FilesystemUtils::LoadStatus::Success: { + success = true; + + int bootNrIn = json["bootNr"] | 0; + if (bootNrIn >= 0 && bootNrIn <= std::numeric_limits::max()) { + bstats.bootNr = (uint16_t)bootNrIn; + } else { + success = false; + } + + int attemptsIn = json["attempts"] | -1; + if (attemptsIn >= 0 && attemptsIn <= std::numeric_limits::max()) { + bstats.attempts = (uint16_t) attemptsIn; + } //else: attempts counter can be missing after upgrade from pre 2.0.0 version + + 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 + + if (!success) { + MO_DBG_ERR("bootstats invalid data"); + bstats = BootStats(); + } + break; } + case FilesystemUtils::LoadStatus::FileNotFound: + MO_DBG_DEBUG("populate bootstats"); + success = true; + break; + case FilesystemUtils::LoadStatus::ErrOOM: + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("error loading bootstats %i", (int)ret); + success = false; + break; + } + + return success; +} + +bool PersistencyUtils::storeBootStats(MO_FilesystemAdapter *filesystem, BootStats& bstats) { + + auto json = initJsonDoc("PersistencyUtils", JSON_OBJECT_SIZE(3)); + + json["bootNr"] = bstats.bootNr; + json["attempts"] = bstats.attempts; + if (*bstats.microOcppVersion) { + json["MicroOcppVersion"] = (const char*)bstats.microOcppVersion; + } + + auto ret = FilesystemUtils::storeJson(filesystem, "bootstats.jsn", json); + bool success = (ret == FilesystemUtils::StoreStatus::Success); + + if (!success) { + MO_DBG_ERR("error storing bootstats %i", (int)ret); + } + + return success; +} + +bool PersistencyUtils::migrate(MO_FilesystemAdapter *filesystem) { + + BootStats bstats; + if (!loadBootStats(filesystem, bstats)) { + return false; + } + + bool success = true; + + if (strcmp(bstats.microOcppVersion, MO_VERSION)) { + MO_DBG_INFO("migrate persistent storage to MO v" MO_VERSION); + success &= FilesystemUtils::removeByPrefix(filesystem, "sd"); + success &= FilesystemUtils::removeByPrefix(filesystem, "tx"); + success &= FilesystemUtils::removeByPrefix(filesystem, "op"); + success &= FilesystemUtils::removeByPrefix(filesystem, "sc-"); + success &= FilesystemUtils::removeByPrefix(filesystem, "client-state.cnf"); + success &= FilesystemUtils::removeByPrefix(filesystem, "arduino-ocpp.cnf"); + success &= FilesystemUtils::removeByPrefix(filesystem, "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"); + + success &= storeBootStats(filesystem, bstats); + } + return success; +} + +bool PersistencyUtils::autoRecovery(MO_FilesystemAdapter *filesystem) { + + BootStats bstats; + if (!loadBootStats(filesystem, bstats)) { + return false; + } + + bstats.attempts++; + + bool success = storeBootStats(filesystem, bstats); + + if (bstats.attempts <= 3) { + //no boot loop - just increment attempts and we're good + return success; + } + + bool ret = true; + ret &= FilesystemUtils::removeByPrefix(filesystem, "sd"); + ret &= FilesystemUtils::removeByPrefix(filesystem, "tx"); + ret &= FilesystemUtils::removeByPrefix(filesystem, "sc-"); + ret &= FilesystemUtils::removeByPrefix(filesystem, "reservation"); + ret &= FilesystemUtils::removeByPrefix(filesystem, "client-state"); + + MO_DBG_ERR("clear local state files (recovery): %s", ret ? "success" : "not completed"); + success &= ret; + + return success; +} + +bool PersistencyUtils::setBootSuccess(MO_FilesystemAdapter *filesystem) { + + BootStats bstats; + if (!loadBootStats(filesystem, bstats)) { + return false; + } + + if (bstats.attempts == 0) { + // attempts already 0 - no need to write back file + return true; + } + + bstats.attempts = 0; + + return storeBootStats(filesystem, bstats); +} diff --git a/src/MicroOcpp/Core/PersistencyUtils.h b/src/MicroOcpp/Core/PersistencyUtils.h new file mode 100644 index 00000000..a1e1c47c --- /dev/null +++ b/src/MicroOcpp/Core/PersistencyUtils.h @@ -0,0 +1,36 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#ifndef MO_PERSISTENCY_UTILS_H +#define MO_PERSISTENCY_UTILS_H + +#include +#include + +#define MO_BOOTSTATS_VERSION_SIZE 10 + +namespace MicroOcpp { +namespace PersistencyUtils { + +struct BootStats { + uint16_t bootNr = 0; + uint16_t attempts = 0; //incremented by 1 for each attempt to initialize the library, reset to 0 after successful initialization + + char microOcppVersion [MO_BOOTSTATS_VERSION_SIZE] = {'\0'}; +}; + +bool loadBootStats(MO_FilesystemAdapter *filesystem, BootStats& bstats); +bool storeBootStats(MO_FilesystemAdapter *filesystem, BootStats& bstats); + +bool migrate(MO_FilesystemAdapter *filesystem); //migrate persistent storage if running on a new MO version + +//check for multiple boot failures in a row and if detected, delete all persistent files which could cause a crash. Call +//before MO is initialized / set up / as early as possible +bool autoRecovery(MO_FilesystemAdapter *filesystem); + +bool setBootSuccess(MO_FilesystemAdapter *filesystem); //reset boot failure counter after successful boot + +} //namespace PersistencyUtils +} //namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Core/Request.cpp b/src/MicroOcpp/Core/Request.cpp index af6e2885..6c34822b 100644 --- a/src/MicroOcpp/Core/Request.cpp +++ b/src/MicroOcpp/Core/Request.cpp @@ -1,8 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #include +#include #include #include #include @@ -14,64 +15,84 @@ #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(Context& context, std::unique_ptr operation) : MemoryManaged("Request.", operation->getOperationType()), context(context), operation(std::move(operation)) { + timeoutStart = context.getClock().getUptime(); + debugRequestTime = context.getClock().getUptime(); } Request::~Request(){ - + if (onAbortListener) { + onAbortListener(); + } + setMessageID(nullptr); } Operation *Request::getOperation(){ return operation.get(); } -void Request::setTimeout(unsigned long timeout) { - this->timeout_period = timeout; +void Request::setTimeout(int32_t timeout) { + this->timeoutPeriod = timeout; } bool Request::isTimeoutExceeded() { - return timed_out || (timeout_period && mocpp_tick_ms() - timeout_start >= timeout_period); + auto& clock = context.getClock(); + int32_t dt; + clock.delta(clock.getUptime(), timeoutStart, dt); + return timedOut || (timeoutPeriod > 0 && dt >= timeoutPeriod); } void Request::executeTimeout() { - if (!timed_out) { - onTimeoutListener(); - onAbortListener(); + if (!timedOut) { + if (onTimeoutListener) { + onTimeoutListener(); + onTimeoutListener = nullptr; + } + if (onAbortListener) { + onAbortListener(); + onAbortListener = nullptr; + } } - timed_out = true; + timedOut = true; } -void Request::setMessageID(const char *id){ - if (!messageID.empty()){ - MO_DBG_ERR("messageID already defined"); +bool Request::setMessageID(const char *id){ + MO_FREE(messageID); + messageID = nullptr; + + if (id) { + size_t size = strlen(id) + 1; + messageID = static_cast(MO_MALLOC(getMemoryTag(), size)); + if (!messageID) { + MO_DBG_ERR("OOM"); + return false; + } + memset(messageID, 0, size); + auto ret = snprintf(messageID, size, "%s", id); + if (ret < 0 || (size_t)ret >= size) { + MO_DBG_ERR("snprintf: %i", ret); + MO_FREE(messageID); + messageID = nullptr; + return false; + } } - messageID = id; + + //success + return true; } Request::CreateRequestResult Request::createRequest(JsonDoc& requestJson) { - if (messageID.empty()) { - char uuid [37] = {'\0'}; - generateUUID(uuid, 37); - messageID = uuid; + if (!messageID) { + char uuid [MO_UUID_STR_SIZE] = {'\0'}; + UuidUtils::generateUUID(context.getRngCb(), uuid, sizeof(uuid)); + setMessageID(uuid); + } + + if (!messageID) { + return CreateRequestResult::Failure; } /* @@ -85,40 +106,47 @@ Request::CreateRequestResult Request::createRequest(JsonDoc& requestJson) { /* * Create OCPP-J Remote Procedure Call header */ - size_t json_buffsize = JSON_ARRAY_SIZE(4) + (messageID.length() + 1) + requestPayload->capacity(); + size_t json_buffsize = JSON_ARRAY_SIZE(4) + requestPayload->capacity(); requestJson = initJsonDoc(getMemoryTag(), json_buffsize); - requestJson.add(MESSAGE_TYPE_CALL); //MessageType - requestJson.add(messageID); //Unique message ID + requestJson.add(MESSAGE_TYPE_CALL); //MessageType + requestJson.add((const char*)messageID); //Unique message ID (force zero-copy) 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); + requestJson.add(*requestPayload); //Payload + + if (MO_DBG_LEVEL >= MO_DL_DEBUG) { + auto& clock = context.getClock(); + int32_t dt; + clock.delta(clock.now(), debugRequestTime, dt); + if (dt >= 10) { + debugRequestTime = clock.now(); + + size_t size = measureJson(requestJson) + 1; + size = std::min(size, (size_t)256); + + char *buf = static_cast(MO_MALLOC(getMemoryTag(), size)); + size_t len = 0; + if (buf) { + len = serializeJson(requestJson, buf, size); + } + + if (!buf || len < 1 || (size_t)len >= size) { + MO_DBG_DEBUG("Try to send request: %s", operation->getOperationType()); + } else { + MO_DBG_DEBUG("Try to send request: %.*s (...)", 128, buf); + } + + MO_FREE(buf); } - - 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())){ + + //check if messageIDs match + if (!messageID || strcmp(messageID, response[1].as())) { return false; } @@ -135,12 +163,12 @@ bool Request::receiveResponse(JsonArray response){ /* * Hand the payload over to the onReceiveConf Callback */ - onReceiveConfListener(payload); + if (onReceiveConfListener) { + onReceiveConfListener(payload); + } + + onAbortListener = nullptr; //ensure onAbort is not called during destruction - /* - * return true as this message has been consumed - */ - return true; } else if (messageTypeId == MESSAGE_TYPE_CALLERROR) { /* @@ -149,19 +177,22 @@ bool Request::receiveResponse(JsonArray response){ const char *errorCode = response[2]; const char *errorDescription = response[3]; JsonObject errorDetails = response[4]; - bool abortOperation = operation->processErr(errorCode, errorDescription, errorDetails); + operation->processErr(errorCode, errorDescription, errorDetails); - if (abortOperation) { + if (onReceiveErrorListener) { onReceiveErrorListener(errorCode, errorDescription, errorDetails); + } + if (onAbortListener) { onAbortListener(); + onAbortListener = nullptr; //ensure onAbort is called only once } - return abortOperation; } else { MO_DBG_WARN("invalid response"); return false; //don't discard this message but retry sending it } + return true; } bool Request::receiveRequest(JsonArray request) { @@ -170,25 +201,44 @@ bool Request::receiveRequest(JsonArray request) { 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); + if (onReceiveReqListener) { + size_t bufsize = measureJson(payload) + 1; + char *buf = static_cast(MO_MALLOC(getMemoryTag(), bufsize)); + if (buf) { + auto written = serializeJson(payload, buf, bufsize); + if (written >= 2 && written < bufsize) { + //success + onReceiveReqListener(operation->getOperationType(), buf, onReceiveReqListenerUserData); + } else { + MO_DBG_ERR("onReceiveReqListener supports only %zu charactes", bufsize); + } + } else { + MO_DBG_ERR("OOM"); + } + MO_FREE(buf); + } return true; //success } Request::CreateResponseResult Request::createResponse(JsonDoc& response) { + if (!messageID) { + return CreateResponseResult::Failure; + } + bool operationFailure = operation->getErrorCode() != nullptr; if (!operationFailure) { @@ -206,11 +256,24 @@ Request::CreateResponseResult Request::createResponse(JsonDoc& response) { response = initJsonDoc(getMemoryTag(), json_buffsize); response.add(MESSAGE_TYPE_CALLRESULT); //MessageType - response.add(messageID.c_str()); //Unique message ID - response.add(*payload); //Payload + response.add((const char*)messageID); //Unique message ID (force zero-copy) + response.add(*payload); //Payload if (onSendConfListener) { - onSendConfListener(payload->as()); + size_t bufsize = 1000; //fixed for now, may change to direct pass-through of the input message + char *buf = static_cast(MO_MALLOC(getMemoryTag(), bufsize)); + if (buf) { + auto written = serializeJson(payload->as(), buf, bufsize); + if (written >= 2 && written < bufsize) { + //success + onSendConfListener(operation->getOperationType(), buf, onSendConfListenerUserData); + } else { + MO_DBG_ERR("onSendConfListener supports only %zu charactes", bufsize); + } + } else { + MO_DBG_ERR("OOM"); + } + MO_FREE(buf); } } else { //operation failure. Send error message instead @@ -227,46 +290,44 @@ Request::CreateResponseResult Request::createResponse(JsonDoc& response) { response = initJsonDoc(getMemoryTag(), json_buffsize); response.add(MESSAGE_TYPE_CALLERROR); //MessageType - response.add(messageID.c_str()); //Unique message ID + response.add((const char*)messageID); //Unique message ID (force zero-copy) response.add(errorCode); response.add(errorDescription); - response.add(*errorDetails); //Error description + response.add(*errorDetails); //Error description } + onAbortListener = nullptr; //ensure onAbort is not called during destruction + return CreateResponseResult::Success; } -void Request::setOnReceiveConfListener(OnReceiveConfListener onReceiveConf){ - if (onReceiveConf) - onReceiveConfListener = onReceiveConf; +void Request::setOnReceiveConf(ReceiveConfListener 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::setOnReceiveRequest(void (*onReceiveReq)(const char *operationType, const char *payloadJson, void *userData), void *userData){ + this->onReceiveReqListener = onReceiveReq; + this->onReceiveReqListenerUserData = userData; } -void Request::setOnSendConfListener(OnSendConfListener onSendConf){ - if (onSendConf) - onSendConfListener = onSendConf; +void Request::setOnSendConf(void (*onSendConf)(const char *operationType, const char *payloadJson, void *userData), void *userData){ + this->onSendConfListener = onSendConf; + this->onSendConfListenerUserData = userData; } -void Request::setOnTimeoutListener(OnTimeoutListener onTimeout) { - if (onTimeout) - onTimeoutListener = onTimeout; +void Request::setOnTimeout(TimeoutListener onTimeout) { + onTimeoutListener = onTimeout; } -void Request::setOnReceiveErrorListener(OnReceiveErrorListener onReceiveError) { - if (onReceiveError) - onReceiveErrorListener = onReceiveError; +void Request::setReceiveErrorListener(ReceiveErrorListener onReceiveError) { + onReceiveErrorListener = onReceiveError; } -void Request::setOnAbortListener(OnAbortListener onAbort) { - if (onAbort) - onAbortListener = onAbort; +void Request::setOnAbort(AbortListener onAbort) { + onAbortListener = onAbort; } const char *Request::getOperationType() { @@ -283,15 +344,15 @@ bool Request::isRequestSent() { namespace MicroOcpp { -std::unique_ptr makeRequest(std::unique_ptr operation){ +std::unique_ptr makeRequest(Context& context, std::unique_ptr operation){ if (operation == nullptr) { return nullptr; } - return std::unique_ptr(new Request(std::move(operation))); + return std::unique_ptr(new Request(context, std::move(operation))); } -std::unique_ptr makeRequest(Operation *operation) { - return makeRequest(std::unique_ptr(operation)); +std::unique_ptr makeRequest(Context& context, Operation *operation) { + return makeRequest(context, std::unique_ptr(operation)); } -} //end namespace MicroOcpp +} //namespace MicroOcpp diff --git a/src/MicroOcpp/Core/Request.h b/src/MicroOcpp/Core/Request.h index 62e42967..78597611 100644 --- a/src/MicroOcpp/Core/Request.h +++ b/src/MicroOcpp/Core/Request.h @@ -12,51 +12,54 @@ #include #include - +#include #include namespace MicroOcpp { +class Context; class Operation; -class Model; class Request : public MemoryManaged { private: - String messageID; + Context& context; + char *messageID = nullptr; 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 setMessageID(const char *id); + ReceiveConfListener onReceiveConfListener; + void (*onReceiveReqListener)(const char *operationType, const char *payloadJson, void *userData) = nullptr; + void *onReceiveReqListenerUserData = nullptr; + void (*onSendConfListener)(const char *operationType, const char *payloadJson, void *userData) = nullptr; + void *onSendConfListenerUserData = nullptr; + TimeoutListener onTimeoutListener; + ReceiveErrorListener onReceiveErrorListener; + AbortListener onAbortListener; + + Timestamp timeoutStart; + int32_t timeoutPeriod = 40; //in seconds + bool timedOut = false; + + Timestamp debugRequestTime; bool requestSent = false; public: - Request(std::unique_ptr msg); + Request(Context& context, std::unique_ptr msg); ~Request(); Operation *getOperation(); - void setTimeout(unsigned long timeout); //0 = disable timeout + void setTimeout(int32_t timeout); //if positive, sets timeout period in secs. If 0 or negative value, disables timeout bool isTimeoutExceeded(); void executeTimeout(); //call Timeout Listener - void setOnTimeoutListener(OnTimeoutListener onTimeout); + void setOnTimeout(TimeoutListener 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. */ @@ -67,15 +70,13 @@ class Request : public MemoryManaged { 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. + * Processes Server response (=Confirmations and Errors). Returns true on success, 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); @@ -93,24 +94,24 @@ class Request : public MemoryManaged { 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 setOnReceiveConf(ReceiveConfListener onReceiveConf); //listener executed when we received the .conf() to a .req() we sent + void setOnReceiveRequest(void (*onReceiveReq)(const char *operationType, const char *payloadJson, void *userData), void *userData); //listener executed when we receive a .req() + void setOnSendConf(void (*onSendConf)(const char *operationType, const char *payloadJson, void *userData), void *userData); //listener executed when we send a .conf() to a .req() we received - void setOnReceiveErrorListener(OnReceiveErrorListener onReceiveError); + void setReceiveErrorListener(ReceiveErrorListener 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); + void setOnAbort(AbortListener onAbort); const char *getOperationType(); @@ -121,9 +122,9 @@ class Request : public MemoryManaged { /* * Simple factory functions */ -std::unique_ptr makeRequest(std::unique_ptr op); -std::unique_ptr makeRequest(Operation *op); //takes ownership of op +std::unique_ptr makeRequest(Context& context, std::unique_ptr op); +std::unique_ptr makeRequest(Context& context, Operation *op); //takes ownership of op -} //end namespace MicroOcpp +} //namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Core/RequestCallbacks.h b/src/MicroOcpp/Core/RequestCallbacks.h index 4959a3bc..859d60e5 100644 --- a/src/MicroOcpp/Core/RequestCallbacks.h +++ b/src/MicroOcpp/Core/RequestCallbacks.h @@ -10,12 +10,10 @@ 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) +using ReceiveConfListener = std::function; +using TimeoutListener = std::function; +using ReceiveErrorListener = std::function; //will be called if OCPP communication partner returns error code +using AbortListener = 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 +} //namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Core/RequestQueue.cpp b/src/MicroOcpp/Core/RequestQueue.cpp index 104a2e97..ab3df59b 100644 --- a/src/MicroOcpp/Core/RequestQueue.cpp +++ b/src/MicroOcpp/Core/RequestQueue.cpp @@ -1,20 +1,11 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // 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") { @@ -91,8 +82,8 @@ bool VolatileRequestQueue::pushRequestBack(std::unique_ptr request) { i++; continue; } - auto new_status_notification = static_cast(request->getOperation()); - auto old_status_notification = static_cast(requests[index]->getOperation()); + 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++) { @@ -118,291 +109,3 @@ bool VolatileRequestQueue::pushRequestBack(std::unique_ptr 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; -} diff --git a/src/MicroOcpp/Core/RequestQueue.h b/src/MicroOcpp/Core/RequestQueue.h index 0cdfe7bc..5145a0cd 100644 --- a/src/MicroOcpp/Core/RequestQueue.h +++ b/src/MicroOcpp/Core/RequestQueue.h @@ -1,16 +1,15 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_REQUESTQUEUE_H #define MO_REQUESTQUEUE_H #include +#include -#include #include -#include #include #ifndef MO_REQUEST_CACHE_MAXSIZE @@ -27,7 +26,7 @@ class Connection; class OperationRegistry; class Request; -class RequestEmitter { +class RequestQueue { public: static const unsigned int NoOperation = std::numeric_limits::max(); @@ -35,7 +34,7 @@ class RequestEmitter { virtual std::unique_ptr fetchFrontRequest() = 0; }; -class VolatileRequestQueue : public RequestEmitter, public MemoryManaged { +class VolatileRequestQueue : public RequestQueue, public MemoryManaged { private: std::unique_ptr requests [MO_REQUEST_CACHE_MAXSIZE]; size_t front = 0, len = 0; @@ -50,44 +49,5 @@ class VolatileRequestQueue : public RequestEmitter, public MemoryManaged { 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 +} //namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Core/Time.cpp b/src/MicroOcpp/Core/Time.cpp index 1bcd0caa..1d5eaee4 100644 --- a/src/MicroOcpp/Core/Time.cpp +++ b/src/MicroOcpp/Core/Time.cpp @@ -1,382 +1,615 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #include + #include -#include +#include + +#include +#include +#include + +#define MO_MAX_UPTIME (MO_MIN_TIME - 1) + +// It's a common mistake to roll-over the mocpp_tick_ms callback too early which breaks +// the unsigned integer arithmetics in this library. The ms counter should use the full +// value range up to (32^2 - 1) ms before resetting to 0. Clock checks for this issue +// and skips one time increment if it is detected. This can be disabled by defining +// MO_CLOCK_ROLLOVER_PROTECTION=0 in the build system +#ifndef MO_CLOCK_ROLLOVER_PROTECTION +#define MO_CLOCK_ROLLOVER_PROTECTION 1 +#endif namespace MicroOcpp { +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)); +} +} //namespace MicroOcpp -const Timestamp MIN_TIME = Timestamp(2010, 0, 0, 0, 0, 0); -const Timestamp MAX_TIME = Timestamp(2037, 0, 0, 0, 0, 0); +using namespace MicroOcpp; Timestamp::Timestamp() : MemoryManaged("Timestamp") { - + } -Timestamp::Timestamp(const Timestamp& other) : MemoryManaged("Timestamp") { +Timestamp::Timestamp(const Timestamp& other) : MemoryManaged(other.getMemoryTag()) { *this = other; } +bool Timestamp::isUnixTime() const { + return time >= MO_MIN_TIME && time <= MO_MAX_TIME; +} + +bool Timestamp::isUptime() const { + return time <= MO_MAX_UPTIME; +} + +bool Timestamp::isDefined() const { + return bootNr != 0; +} + +Clock::Clock(Context& context) : context(context) { + +} + +bool Clock::setup() { + + uint16_t bootNr = 1; //0 is reserved for undefined + + if (auto filesystem = context.getFilesystem()) { + // restore bootNr from bootstats + + PersistencyUtils::BootStats bootstats; + if (!PersistencyUtils::loadBootStats(filesystem, bootstats)) { + MO_DBG_ERR("cannot load bootstats"); + return false; + } + + bootstats.bootNr++; //assign new boot number to this run + if (bootstats.bootNr == 0) { //handle roll-over + bootstats.bootNr = 1; + } + + bootNr = bootstats.bootNr; + + if (!PersistencyUtils::storeBootStats(filesystem, bootstats)) { + MO_DBG_ERR("cannot update bootstats"); + return false; + } + } + + uptime.bootNr = bootNr; + unixTime.bootNr = bootNr; + + return true; +} + +void Clock::loop() { + + auto platformTimeMs = context.getTicksMs(); + auto platformDeltaMs = platformTimeMs - lastIncrement; + + /* Roll-over protection: if the platform implementation of mocpp_tick_ms does not + * fill up the full 32 bits before rolling over, the unsigned integer arithmetics + * would break. Handle this case here, because it is a common mistake */ + if (MO_CLOCK_ROLLOVER_PROTECTION && platformTimeMs < lastIncrement) { + // rolled over + if (platformDeltaMs > 3600 * 1000) { + // timer rolled over and delta is above 1 hour - that's suspicious + MO_DBG_ERR("getTickMs roll-over error. Must increment milliseconds up to (32^2 - 1) before resetting to 0"); + platformDeltaMs = 0; + } + } + #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) { } + { + if (platformDeltaMs > (uint32_t)std::numeric_limits::max()) { + MO_DBG_ERR("integer overflow"); + platformDeltaMs = 0; + } + addMs(unixTime, (int32_t)platformDeltaMs); + addMs(uptime, (int32_t)platformDeltaMs); + lastIncrement = platformTimeMs; + } +#else + { + auto platformDeltaS = platformDeltaMs / (uint32_t)1000; + add(unixTime, platformDeltaS); + add(uptime, platformDeltaS); + lastIncrement += platformDeltaS * 1000; + } #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)); +const Timestamp &Clock::now() { + if (unixTime.isUnixTime()) { + return unixTime; + } else { + return uptime; + } } -bool Timestamp::setTime(const char *jsonDateString) { +const Timestamp &Clock::getUptime() { + return uptime; +} + +int32_t Clock::getUptimeInt() { + return uptime.time; +} + +bool Clock::setTime(const char* jsonDateString) { + + Timestamp t; + if (!parseString(jsonDateString, t)) { + return false; + } + + unixTime = t; + return true; +} - const int JSONDATE_MINLENGTH = 19; +bool Clock::setTime(int32_t unixTimeInt) { - if (strlen(jsonDateString) < JSONDATE_MINLENGTH){ + Timestamp t; + if (!fromUnixTime(t, unixTimeInt)) { 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 + unixTime = t; + return true; +} + +bool Clock::delta(const Timestamp& t2, const Timestamp& t1, int32_t& dt) const { + //dt = t2 - t1 + if (!t1.isDefined() || !t2.isDefined()) { 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'); + + if (t1.isUnixTime() && t2.isUnixTime()) { + dt = t2.time - t1.time; + return true; + } else if (t1.isUptime() && t2.isUptime() && t1.bootNr == t2.bootNr) { + + if ((t1.time > 0 && t2.time - t1.time > t2.time) || + (t1.time < 0 && t2.time - t1.time < t2.time)) { + MO_DBG_ERR("integer overflow"); + return false; + } + + dt = t2.time - t1.time; + return true; + } else { + Timestamp t1Unix, t2Unix; + if (toUnixTime(t1, t1Unix) && toUnixTime(t2, t2Unix)) { + dt = t2Unix.time - t1Unix.time; + return true; } else { + MO_DBG_ERR("cannot get delta: time captured before initial clock sync"); 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) { +bool Clock::add(Timestamp& t, int32_t secs) const { + + if (!t.isDefined()) { 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 - + if ((secs > 0 && t.time + secs < t.time) || + (secs < 0 && t.time + secs > t.time)) { + MO_DBG_ERR("integer overflow"); + return false; + } + + if (t.isUnixTime() && (t.time + secs < MO_MIN_TIME || t.time + secs > MO_MAX_TIME)) { + MO_DBG_ERR("exceed valid unix time range"); + return false; + } + + if (t.isUptime() && (t.time + secs > MO_MAX_UPTIME)) { + MO_DBG_ERR("exceed valid uptime time range"); + return false; + } + + t.time += secs; + 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 +bool Clock::deltaMs(const Timestamp& t2, const Timestamp& t1, int32_t& dtMs) const { + + if (t1.ms < 0 || t1.ms >= 1000 || t2.ms < 0 || t2.ms >= 1000) { + MO_DBG_ERR("invalid timestamp"); + return false; + } + + auto fullSec2 = t2; + fullSec2.ms = 0; + auto fullSec1 = t1; + fullSec1.ms = 0; + + int32_t dtS; + if (!delta(fullSec2, fullSec1, dtS)) { + return false; + } + + if ((dtS > 0 && dtS * 1000 + 1000 < dtS) || + (dtS < 0 && dtS * 1000 - 1000 > dtS)) { + MO_DBG_ERR("integer overflow"); + return false; + } + + dtMs = dtS * 1000 + (int32_t)(t2.ms - t1.ms); return true; } -Timestamp &Timestamp::operator+=(int secs) { +bool Clock::addMs(Timestamp& t, int32_t ms) const { - second += secs; + if (t.ms < 0 || t.ms >= 1000) { + MO_DBG_ERR("invalid timestamp"); + return false; + } + + if (ms + t.ms < ms) { + MO_DBG_ERR("integer overflow"); + return false; + } + ms += t.ms; - if (second >= 0 && second < 60) return *this; + int32_t s = 0; + if (ms >= 0) { + //positive, addition + s = ms / 1000; + ms %= 1000; + } else { + //negative, substraction + s = ms / 1000; + ms -= s * 1000; + if (ms != 0) { + s -= 1; + ms += 1000; + } + } - minute += second / 60; - second %= 60; - if (second < 0) { - minute--; - second += 60; + if (!add(t, s)) { + return false; } - if (minute >= 0 && minute < 60) return *this; + t.ms = ms; + return true; +} +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + +bool Clock::toJsonString(const Timestamp& src, char *dst, size_t size) const { - hour += minute / 60; - minute %= 60; - if (minute < 0) { - hour--; - minute += 60; + if (!src.isDefined()) { + return false; } - if (hour >= 0 && hour < 24) return *this; + if (size < MO_JSONDATE_SIZE) { + return false; + } - day += hour / 24; - hour %= 24; - if (hour < 0) { - day--; - hour += 24; + Timestamp t; + if (!toUnixTime(src, t)) { + MO_DBG_ERR("timestamp not a unix time"); + return false; } - while (day >= noDays(month, year)) { - day -= noDays(month, year); + int32_t time = t.time; + + int year = 1970; + int month = 0; + while (time - (noDays(month, year) * 24 * 3600) >= 0) { + time -= noDays(month, year) * 24 * 3600; month++; - if (month >= 12) { - month -= 12; year++; + month = 0; } } - while (day < 0) { - month--; - if (month < 0) { - month += 12; - year--; - } - day += noDays(month, year); + int day = time / (24 * 3600); + time %= 24 * 3600; + int hour = time / 3600; + time %= 3600; + int minute = time / 60; + time %= 60; + int second = time; + + // first month of the year is '1' and first day of the month is '1' + month++; + day++; + + int ret; +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + ret = snprintf(dst, size, "%04i-%02i-%02iT%02i:%02i:%02i.%03uZ", + year, month, day, hour, minute, second, (int)(t.ms >= 0 && t.ms <= 999 ? t.ms : 0)); +#else + ret = snprintf(dst, size, "%04i-%02i-%02iT%02i:%02i:%02iZ", + year, month, day, hour, minute, second); +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + + if (ret < 0 || (size_t)ret >= size) { + return false; } - return *this; + //success + return true; } -#if MO_ENABLE_TIMESTAMP_MILLISECONDS -Timestamp &Timestamp::addMilliseconds(int val) { +bool Clock::toInternalString(const Timestamp& t, char *dst, size_t size) const { - ms += val; + if (!t.isDefined()) { + return false; + } - if (ms >= 0 && ms < 1000) return *this; - - auto dsecond = ms / 1000; - ms %= 1000; - if (ms < 0) { - dsecond--; - ms += 1000; + if (size < MO_INTERNALTIME_SIZE) { + return false; } - return this->operator+=(dsecond); -} + + int ret; +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + ret = snprintf(dst, size, "i%ut%lim%u", (unsigned int)t.bootNr, (long int)t.time, (int)(t.ms >= 0 && t.ms <= 999 ? t.ms : 0)); +#else + ret = snprintf(dst, size, "i%ut%li", (unsigned int)t.bootNr, (long int)t.time); #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS -Timestamp &Timestamp::operator-=(int secs) { - return operator+=(-secs); + if (ret < 0 || (size_t)ret >= size) { + return false; + } + + return true; } -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; - } +bool Clock::parseString(const char *src, Timestamp& dst) const { + + if (src[0] == 'i') { //this is the internal representation, consisting of an optional bootNr and a time value + size_t i = 1; + uint16_t bootNr = 0, bootNr2 = 0; + for (; isdigit(src[i]); i++) { + bootNr *= 10; + bootNr += src[i] - '0'; + if (bootNr < bootNr2) { + //overflow + MO_DBG_ERR("invalid bootNr"); + return false; + } + bootNr2 = bootNr; + } + + if (src[i++] != 't') { + MO_DBG_ERR("invalid time string"); + return false; + } - int16_t lhsDays = day; - int16_t rhsDays = rhs.day; + bool positive = true; + if (src[i] == '-') { + positive = false; + i++; + } - 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); + int32_t time = 0, time2 = 0; + for (; isdigit(src[i]); i++) { + time *= 10; + if (positive) { + time += src[i] - '0'; + } else { + time -= src[i] - '0'; + } + if ((positive && time < time2) || (!positive && time > time2)) { + //overflow + MO_DBG_ERR("invalid time"); + return false; } - if (rhs.year > iy || (rhs.year == iy && rhs.month > im)) { - rhsDays += noDays(im, iy); + if (time >= MO_MAX_TIME) { + //exceed numeric limit + MO_DBG_ERR("invalid time"); + return false; } + if (time < MO_MIN_TIME && time > MO_MAX_UPTIME) { + MO_DBG_ERR("invalid time"); + return false; + } + time2 = time; } - } - int dt = (lhsDays - rhsDays) * (24 * 3600) + (hour - rhs.hour) * 3600 + (minute - rhs.minute) * 60 + second - rhs.second; + // Optional ms + int16_t ms = 0; + if (src[i] == 'm') { + i++; + for (; isdigit(src[i]); i++) { + ms *= 10; + ms += src[i] - '0'; + if (ms >= 1000) { + //overflow + MO_DBG_ERR("invalid ms"); + return false; + } + } + } + if (src[i] != '\0') { + MO_DBG_ERR("invalid time"); + return false; + } + + //success + dst.time = time; + dst.bootNr = bootNr; #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--; + dst.ms = ms; #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + return true; + } else { // this is a JSON time string - return dt; -} + const int JSONDATE_MINLENGTH = 19; -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 + if (strlen(src) < JSONDATE_MINLENGTH){ + return false; + } - return *this; -} + if (!isdigit(src[0]) || //2 + !isdigit(src[1]) || //0 + !isdigit(src[2]) || //1 + !isdigit(src[3]) || //3 + src[4] != '-' || //- + !isdigit(src[5]) || //0 + !isdigit(src[6]) || //2 + src[7] != '-' || //- + !isdigit(src[8]) || //0 + !isdigit(src[9]) || //1 + src[10] != 'T' || //T + !isdigit(src[11]) || //2 + !isdigit(src[12]) || //0 + src[13] != ':' || //: + !isdigit(src[14]) || //5 + !isdigit(src[15]) || //3 + src[16] != ':' || //: + !isdigit(src[17]) || //3 + !isdigit(src[18])) { //2 + //ignore subsequent characters + return false; + } -Timestamp operator+(const Timestamp &lhs, int secs) { - Timestamp res = lhs; - res += secs; - return res; -} + int year = (src[0] - '0') * 1000 + + (src[1] - '0') * 100 + + (src[2] - '0') * 10 + + (src[3] - '0'); + int month = (src[5] - '0') * 10 + + (src[6] - '0') - 1; + int day = (src[8] - '0') * 10 + + (src[9] - '0') - 1; + int hour = (src[11] - '0') * 10 + + (src[12] - '0'); + int minute = (src[14] - '0') * 10 + + (src[15] - '0'); + int second = (src[17] - '0') * 10 + + (src[18] - '0'); + + //optional fractals + int16_t ms = 0; + if (src[19] == '.') { + if (isdigit(src[20]) || //1 + isdigit(src[21]) || //2 + isdigit(src[22])) { + + ms = (src[20] - '0') * 100 + + (src[21] - '0') * 10 + + (src[22] - '0'); + } else { + return false; + } + } -Timestamp operator-(const Timestamp &lhs, int secs) { - return operator+(lhs, -secs); -} + 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; + } -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 - ; -} + int32_t time = 0; -bool operator!=(const Timestamp &lhs, const Timestamp &rhs) { - return !(lhs == rhs); -} + for (int y = 1970; y < year; y++) { + for (int m = 0; m < 12; m++) { + time += noDays(m, y) * 24 * 3600; + } + } -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; -} + for (int m = 0; m < month; m++) { + time += noDays(m, year) * 24 * 3600; + } -bool operator<=(const Timestamp &lhs, const Timestamp &rhs) { - return lhs < rhs || lhs == rhs; -} + time += day * 24 * 3600; + time += hour * 3600; + time += minute * 60; + time += second; -bool operator>(const Timestamp &lhs, const Timestamp &rhs) { - return rhs < lhs; -} + if (time < MO_MIN_TIME || time > MO_MAX_TIME) { + MO_DBG_ERR("only accept time range from year 2010 to 2037"); + return false; + } -bool operator>=(const Timestamp &lhs, const Timestamp &rhs) { - return rhs <= lhs; + //success + dst.time = time; + dst.bootNr = unixTime.bootNr; //set bootNr to a defined value +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + dst.ms = ms; +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + return true; + } } +bool Clock::toUnixTime(const Timestamp& src, Timestamp& dst) const { + if (src.isUnixTime()) { + dst = src; + return true; + } + if (!src.isUptime() || !src.isDefined()) { + return false; + } -Clock::Clock() { - -} + if (!unixTime.isUnixTime()) { + //clock doesn't know unix time yet - no conversion is possible + return false; + } -bool Clock::setTime(const char* jsonDateString) { + if (src.bootNr != uptime.bootNr) { + //src is from a previous power cycle - don't have a record of previous boot-time-unix-time offsets + return false; + } - Timestamp timestamp = Timestamp(); - - if (!timestamp.setTime(jsonDateString)) { + int32_t deltaUptime; + if (!delta(src, uptime, deltaUptime)) { return false; } - system_basetime = mocpp_tick_ms(); - mocpp_basetime = timestamp; + dst = unixTime; + if (!add(dst, deltaUptime)) { + return false; + } - currentTime = mocpp_basetime; - lastUpdate = system_basetime; + //success + return true; +} +bool Clock::toUnixTime(const Timestamp& src, int32_t& dst) const { + Timestamp t; + if (!toUnixTime(src, t)) { + return false; + } + dst = t.time; return true; } -const Timestamp &Clock::now() { - auto tReading = mocpp_tick_ms(); - auto delta = tReading - lastUpdate; +bool Clock::fromUnixTime(Timestamp& dst, int32_t unixTimeInt) const { + + Timestamp t = unixTime; + t.time = unixTimeInt; + + if (!t.isUnixTime()) { + return false; + } #if MO_ENABLE_TIMESTAMP_MILLISECONDS - currentTime.addMilliseconds(delta); - lastUpdate = tReading; -#else - auto deltaSecs = delta / 1000; - currentTime += deltaSecs; - lastUpdate += deltaSecs * 1000; + t.ms = 0; #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); + dst = t; + return true; } +uint16_t Clock::getBootNr() const { + return uptime.bootNr; } diff --git a/src/MicroOcpp/Core/Time.h b/src/MicroOcpp/Core/Time.h index 9a82753b..2124bbfe 100644 --- a/src/MicroOcpp/Core/Time.h +++ b/src/MicroOcpp/Core/Time.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_TIME_H @@ -22,122 +22,131 @@ #define JSONDATE_LENGTH 20 //ISO 8601 date length, excluding the terminating zero #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS +#define MO_JSONDATE_SIZE (JSONDATE_LENGTH + 1) +#define MO_INTERNALTIME_SIZE sizeof("i65535t-2147483648m999") //longest possible string: max bootNr, max negative time and milliseconds + +#define MO_MIN_TIME (((2010 - 1970) * 365 + 10) * 24 * 3600) // unix time for 2010-01-01Z00:00:00T (= 40 years + 10 leap days, in seconds) +#define MO_MAX_TIME (((2037 - 1970) * 365 + 17) * 24 * 3600) // unix time for 2037-01-01Z00:00:00T (= 67 years + 17 leap days, in seconds) + namespace MicroOcpp { +class Context; +class Clock; + 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; + int32_t time = 0; //Unix time (number of seconds since Jan 1, 1970 UTC, not counting leap seconds) + uint16_t bootNr = 0; //bootNr when timestamp was taken + #if MO_ENABLE_TIMESTAMP_MILLISECONDS - int32_t ms = 0; + int16_t ms = 0; //fractional ms of timestamp. Compound timestamp = time + ms. Range should be 0...999 #endif //MO_ENABLE_TIMESTAMP_MILLISECONDS public: Timestamp(); - Timestamp(const Timestamp& other); + MicroOcpp::Timestamp& operator=(const Timestamp& other) = default; -#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); + bool isUnixTime() const; + bool isUptime() const; + bool isDefined() const; - 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); +friend class Clock; }; -extern const Timestamp MIN_TIME; -extern const Timestamp MAX_TIME; - class Clock { private: + Context& context; - 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 unixTime; + Timestamp uptime; - Timestamp currentTime = Timestamp(); + uint32_t lastIncrement = 0; public: - Clock(); - Clock(const Clock&) = delete; - Clock(const Clock&&) = delete; - Clock& operator=(const Clock&) = delete; + Clock(Context& context); + + bool setup(); + + void loop(); const Timestamp &now(); + const Timestamp &getUptime(); + int32_t getUptimeInt(); + /** * 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 + * Same as `setTime(const char* jsonDateString)`, but expects Unix time as seconds since 1970-01-01 + */ + bool setTime(int32_t unixTimeInt); + + /* + * Time delta between two timestamps. Calculates t2 - t1, writes the result in seconds into dt and + * returns true, if successful and false otherwise. The delta between two timestamps is defined if + * t1 and t2 have a unix time, or if the clock can determine their unix time after having connected + * to the OCPP server, or if both have a processor uptime. */ - Timestamp adjustPrebootTimestamp(const Timestamp& t); + bool delta(const Timestamp& t2, const Timestamp& t1, int32_t& dt) const; + + /* + * Add secs seconds to `t`. After this operation, t will be later timestamp if secs is positive, or + * t will be an earlier timestamp if secs is negative. This operation is only valid if t remains in + * its numerical limits. I.e. if t is a processor uptime timestamp, then secs must not move it outside + * the uptime range and vice versa for unix timestamps. Returns true if successful, or false if + * operation is invalid or would overflow + */ + bool add(Timestamp& t, int32_t secs) const; + +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + bool deltaMs(const Timestamp& t2, const Timestamp& t1, int32_t& dtMs) const; + bool addMs(Timestamp& t, int32_t ms) const; +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + + /* + * Print the ISO8601 represenation of t into dst, having `size` bytes. Fails if the unix time is + * not defined and cannot be determined by the clock. `size` must be at least MO_JSONDATE_SIZE + */ + bool toJsonString(const Timestamp& t, char *dst, size_t size) const; + + /* + * Print an internal representation of t into dst, having `size` bytes. `size` must be at least + * MO_INTERNALTIME_SIZE + */ + bool toInternalString(const Timestamp& t, char *dst, size_t size) const; + + /* + * Parses the time either from the ISO8601 representation, or the internal representation. Returns + * true on success and false on failure. + */ + bool parseString(const char *src, Timestamp& dst) const; + + /* Converts src into a unix time and writes result to dst. dst will equal to src if src is already + * a unix time. Returns true if successful and false if the unix time cannot be determined */ + bool toUnixTime(const Timestamp& src, Timestamp& dst) const; + bool toUnixTime(const Timestamp& src, int32_t& dstInt) const; + + bool fromUnixTime(Timestamp& dst, int32_t unixTimeInt) const; + + uint16_t getBootNr() const; }; -} +} //namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Core/UuidUtils.cpp b/src/MicroOcpp/Core/UuidUtils.cpp index 7d3ddb15..a8ea0ce8 100644 --- a/src/MicroOcpp/Core/UuidUtils.cpp +++ b/src/MicroOcpp/Core/UuidUtils.cpp @@ -4,18 +4,21 @@ #include namespace MicroOcpp { +namespace UuidUtils { -#define UUID_STR_LEN 36 +bool generateUUID(uint32_t (*rng)(), char *uuidBuffer, size_t size) { + if (!rng) { + return false; + } -bool generateUUID(char *uuidBuffer, size_t len) { - if (len < UUID_STR_LEN + 1) + if (size < MO_UUID_STR_SIZE) { return false; } uint32_t ar[4]; for (uint8_t i = 0; i < 4; i++) { - ar[i] = mocpp_rng(); + ar[i] = rng(); } // Conforming to RFC 4122 Specification @@ -49,8 +52,9 @@ bool generateUUID(char *uuidBuffer, size_t len) { uuidBuffer[j++] = (ch < 10)? '0' + ch : ('a' - 10) + ch; } - uuidBuffer[UUID_STR_LEN] = 0; + uuidBuffer[MO_UUID_STR_SIZE - 1] = 0; return true; } -} +} //namespace UuidUtils +} //namespace MicroOcpp diff --git a/src/MicroOcpp/Core/UuidUtils.h b/src/MicroOcpp/Core/UuidUtils.h index 3516effe..4fb0d79b 100644 --- a/src/MicroOcpp/Core/UuidUtils.h +++ b/src/MicroOcpp/Core/UuidUtils.h @@ -1,14 +1,19 @@ #ifndef MO_UUIDUTILS_H #define MO_UUIDUTILS_H +#include #include + +#define MO_UUID_STR_SIZE (36 + 1) + namespace MicroOcpp { +namespace UuidUtils { // 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); - -} +bool generateUUID(uint32_t (*rng)(), char *uuidBuffer, size_t size); +} //namespace UuidUtils +} //namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Debug.cpp b/src/MicroOcpp/Debug.cpp index 71f42343..99f7f1fc 100644 --- a/src/MicroOcpp/Debug.cpp +++ b/src/MicroOcpp/Debug.cpp @@ -1,12 +1,15 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #include +#include +#include #include -const char *level_label [] = { +namespace MicroOcpp { +const char *dbgLevelLabel [] = { "", //MO_DL_NONE 0x00 "ERROR", //MO_DL_ERROR 0x01 "warning", //MO_DL_WARN 0x02 @@ -15,40 +18,68 @@ const char *level_label [] = { "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; +Debug debug; +} //namespace MicroOcpp + +using namespace MicroOcpp; + +void Debug::setDebugCb(void (*debugCb)(const char *msg)) { + this->debugCb = debugCb; } -#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); +void Debug::setDebugCb2(void (*debugCb2)(int lvl, const char *fn, int line, const char *msg)) { + this->debugCb2 = debugCb2; } -#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--; +void Debug::setDebugLevel(int dbgLevel) { + if (dbgLevel > MO_DBG_LEVEL) { + MO_DBG_ERR("Debug level limited to %s by build config", dbgLevelLabel[MO_DBG_LEVEL]); + dbgLevel = MO_DBG_LEVEL; } - MO_CONSOLE_PRINTF("[MO] %s (%s:%i): ", level_label[level], fn + l, line); + this->dbgLevel = dbgLevel; } -#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); +bool Debug::setup() { + if (!debugCb && !debugCb2) { + // default-initialize console + debugCb = getDefaultDebugCb(); //defaultDebugCb is null on unsupported platforms. Just inconvenient, not a failure + } + return true; } -#else -#error invalid MO_DBG_FORMAT definition -#endif +void Debug::operator()(int lvl, const char *fn, int line, const char *format, ...) { + if (debugCb2) { + va_list args; + va_start(args, format); + auto ret = vsnprintf(buf, sizeof(buf), format, args); + if (ret < 0 || (size_t)ret >= sizeof(buf)) { + snprintf(buf + sizeof(buf) - sizeof(" [...]"), sizeof(buf), " [...]"); + } + va_end(args); -void mo_dbg_print_suffix() { - MO_CONSOLE_PRINTF("\n"); + debugCb2(lvl, fn, line, buf); + } else if (debugCb) { + size_t l = strlen(fn); + while (l > 0 && fn[l-1] != '/' && fn[l-1] != '\\') { + l--; + } + + auto ret = snprintf(buf, sizeof(buf), "[MO] %s (%s:%i): ", dbgLevelLabel[lvl], fn + l, line); + if (ret < 0 || (size_t)ret >= sizeof(buf)) { + snprintf(buf + sizeof(buf) - sizeof(" : "), sizeof(buf), " : "); + } + + debugCb(buf); + + va_list args; + va_start(args, format); + ret = vsnprintf(buf, sizeof(buf), format, args); + if (ret < 0 || (size_t)ret >= sizeof(buf)) { + snprintf(buf + sizeof(buf) - sizeof(" [...]"), sizeof(buf), " [...]"); + } + va_end(args); + + debugCb(buf); + debugCb(MO_DBG_ENDL); + } } diff --git a/src/MicroOcpp/Debug.h b/src/MicroOcpp/Debug.h index 51e71216..10d32e23 100644 --- a/src/MicroOcpp/Debug.h +++ b/src/MicroOcpp/Debug.h @@ -20,83 +20,76 @@ //MbedTLS debug level documented in mbedtls/debug.h: #ifndef MO_DBG_LEVEL_MBEDTLS -#define MO_DBG_LEVEL_MBEDTLS 1 +#define MO_DBG_LEVEL_MBEDTLS 1 //Error #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_ENDL +#define MO_DBG_ENDL "\n" +#endif -#ifndef MO_DBG_FORMAT -#define MO_DBG_FORMAT MO_DF_FILE_LINE //default +#ifndef MO_DBG_MAXMSGSIZE +#ifdef MO_CUSTOM_CONSOLE_MAXMSGSIZE +#define MO_DBG_MAXMSGSIZE MO_CUSTOM_CONSOLE_MAXMSGSIZE +#else +#define MO_DBG_MAXMSGSIZE 256 #endif +#endif //MO_DBG_MAXMSGSIZE #ifdef __cplusplus -extern "C" { -#endif -void mo_dbg_print_prefix(int level, const char *fn, int line); -void mo_dbg_print_suffix(); +namespace MicroOcpp { +class Debug { +private: + void (*debugCb)(const char *msg) = nullptr; + void (*debugCb2)(int lvl, const char *fn, int line, const char *msg) = nullptr; -#ifdef __cplusplus -} -#endif + char buf [MO_DBG_MAXMSGSIZE] = {'\0'}; + int dbgLevel = MO_DBG_LEVEL; +public: + Debug() = default; + + void setDebugCb(void (*debugCb)(const char *msg)); + void setDebugCb2(void (*debugCb2)(int lvl, const char *fn, int line, const char *msg)); + + void setDebugLevel(int dbgLevel); + + bool setup(); -#define MO_DBG(level, X) \ - do { \ - mo_dbg_print_prefix(level, __FILE__, __LINE__); \ - MO_CONSOLE_PRINTF X; \ - mo_dbg_print_suffix(); \ - } while (0) + void operator()(int lvl, const char *fn, int line, const char *format, ...); +}; + +extern Debug debug; +} //namespace MicroOcpp +#endif //__cplusplus #if MO_DBG_LEVEL >= MO_DL_ERROR -#define MO_DBG_ERR(...) MO_DBG(MO_DL_ERROR,(__VA_ARGS__)) +#define MO_DBG_ERR(...) MicroOcpp::debug(MO_DL_ERROR, __FILE__, __LINE__, __VA_ARGS__) #else -#define MO_DBG_ERR(...) ((void)0) +#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__)) +#define MO_DBG_WARN(...) MicroOcpp::debug(MO_DL_WARN, __FILE__, __LINE__, __VA_ARGS__) #else -#define MO_DBG_WARN(...) ((void)0) +#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__)) +#define MO_DBG_INFO(...) MicroOcpp::debug(MO_DL_INFO, __FILE__, __LINE__, __VA_ARGS__) #else -#define MO_DBG_INFO(...) ((void)0) +#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__)) +#define MO_DBG_DEBUG(...) MicroOcpp::debug(MO_DL_DEBUG, __FILE__, __LINE__, __VA_ARGS__) #else -#define MO_DBG_DEBUG(...) ((void)0) +#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) - +#define MO_DBG_VERBOSE(...) MicroOcpp::debug(MO_DL_VERBOSE, __FILE__, __LINE__, __VA_ARGS__) #else -#define MO_DBG_TRAFFIC_OUT(...) ((void)0) -#define MO_DBG_TRAFFIC_IN(...) ((void)0) +#define MO_DBG_VERBOSE(...) (void)0 #endif #endif diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationData.cpp b/src/MicroOcpp/Model/Authorization/AuthorizationData.cpp index 08cbfbca..c58201a6 100644 --- a/src/MicroOcpp/Model/Authorization/AuthorizationData.cpp +++ b/src/MicroOcpp/Model/Authorization/AuthorizationData.cpp @@ -2,14 +2,13 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_LOCAL_AUTH - #include #include +#if MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH + using namespace MicroOcpp; +using namespace MicroOcpp::v16; AuthorizationData::AuthorizationData() : MemoryManaged("v16.Authorization.AuthorizationData") { @@ -20,109 +19,130 @@ AuthorizationData::AuthorizationData(AuthorizationData&& other) : MemoryManaged( } AuthorizationData::~AuthorizationData() { - MO_FREE(parentIdTag); - parentIdTag = nullptr; + reset(); } 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'; + expiryDate = other.expiryDate; + other.expiryDate = nullptr; + snprintf(idTag, sizeof(idTag), "%s", other.idTag); + memset(other.idTag, 0, sizeof(other.idTag)); 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'; +bool AuthorizationData::readJson(Clock& clock, JsonObject entry, bool internalFormat) { + if (entry.containsKey(AUTHDATA_KEY_IDTAG(internalFormat))) { + snprintf(idTag, sizeof(idTag), "%s", entry[AUTHDATA_KEY_IDTAG(internalFormat)].as()); } else { - idTag[0] = '\0'; + MO_DBG_ERR("format error"); + return false; } JsonObject idTagInfo; - if (compact){ + if (internalFormat){ 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(); + delete expiryDate; + expiryDate = nullptr; + + if (idTagInfo.containsKey(AUTHDATA_KEY_EXPIRYDATE(internalFormat))) { + expiryDate = new Timestamp(); + if (!expiryDate) { + MO_DBG_ERR("OOM"); + return false; + } + if (!clock.parseString(idTagInfo[AUTHDATA_KEY_EXPIRYDATE(internalFormat)], *expiryDate)) { + MO_DBG_ERR("format error"); + return false; } - } 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)); + MO_FREE(parentIdTag); + parentIdTag = nullptr; + + if (idTagInfo.containsKey(AUTHDATA_KEY_PARENTIDTAG(internalFormat))) { + parentIdTag = static_cast(MO_MALLOC(getMemoryTag(), MO_IDTAG_LEN_MAX + 1)); if (parentIdTag) { - strncpy(parentIdTag, idTagInfo[AUTHDATA_KEY_PARENTIDTAG(compact)], IDTAG_LEN_MAX + 1); - parentIdTag[IDTAG_LEN_MAX] = '\0'; + snprintf(parentIdTag, MO_IDTAG_LEN_MAX + 1, "%s", idTagInfo[AUTHDATA_KEY_PARENTIDTAG(internalFormat)].as()); } else { MO_DBG_ERR("OOM"); + return false; } - } else { - MO_FREE(parentIdTag); - parentIdTag = nullptr; } - if (idTagInfo.containsKey(AUTHDATA_KEY_STATUS(compact))) { - status = deserializeAuthorizationStatus(idTagInfo[AUTHDATA_KEY_STATUS(compact)]); + if (idTagInfo.containsKey(AUTHDATA_KEY_STATUS(internalFormat))) { + status = deserializeAuthorizationStatus(idTagInfo[AUTHDATA_KEY_STATUS(internalFormat)]); } else { - if (compact) { + if (internalFormat) { status = AuthorizationStatus::Accepted; } else { status = AuthorizationStatus::UNDEFINED; } } + + if (status == AuthorizationStatus::UNDEFINED) { + MO_DBG_ERR("format error"); + return false; + } + + return true; } -size_t AuthorizationData::getJsonCapacity() const { +size_t AuthorizationData::getJsonCapacity(bool internalFormat) const { return JSON_OBJECT_SIZE(2) + - (idTag[0] != '\0' ? + (idTag[0] != '\0' ? JSON_OBJECT_SIZE(1) : 0) + (expiryDate ? - JSON_OBJECT_SIZE(1) + JSONDATE_LENGTH + 1 : 0) + + JSON_OBJECT_SIZE(1) + + (internalFormat ? + MO_INTERNALTIME_SIZE : MO_JSONDATE_SIZE) + : 0) + (parentIdTag ? JSON_OBJECT_SIZE(1) : 0) + (status != AuthorizationStatus::UNDEFINED ? JSON_OBJECT_SIZE(1) : 0); } -void AuthorizationData::writeJson(JsonObject& entry, bool compact) { +void AuthorizationData::writeJson(Clock& clock, JsonObject& entry, bool internalFormat) { if (idTag[0] != '\0') { - entry[AUTHDATA_KEY_IDTAG(compact)] = (const char*) idTag; + entry[AUTHDATA_KEY_IDTAG(internalFormat)] = (const char*) idTag; } JsonObject idTagInfo; - if (compact) { + if (internalFormat) { 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 (internalFormat) { + char buf [MO_INTERNALTIME_SIZE]; + if (clock.toInternalString(*expiryDate, buf, sizeof(buf))) { + idTagInfo[AUTHDATA_KEY_EXPIRYDATE(internalFormat)] = buf; + } + } else { + char buf [MO_JSONDATE_SIZE]; + if (clock.toJsonString(*expiryDate, buf, sizeof(buf))) { + idTagInfo[AUTHDATA_KEY_EXPIRYDATE(internalFormat)] = buf; + } } } if (parentIdTag) { - idTagInfo[AUTHDATA_KEY_PARENTIDTAG(compact)] = (const char *) parentIdTag; + idTagInfo[AUTHDATA_KEY_PARENTIDTAG(internalFormat)] = (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); + idTagInfo[AUTHDATA_KEY_STATUS(internalFormat)] = serializeAuthorizationStatus(status); + } else if (!internalFormat) { + idTagInfo[AUTHDATA_KEY_STATUS(internalFormat)] = serializeAuthorizationStatus(AuthorizationStatus::Invalid); } } @@ -130,7 +150,7 @@ const char *AuthorizationData::getIdTag() const { return idTag; } Timestamp *AuthorizationData::getExpiryDate() const { - return expiryDate.get(); + return expiryDate; } const char *AuthorizationData::getParentIdTag() const { return parentIdTag; @@ -140,10 +160,17 @@ AuthorizationStatus AuthorizationData::getAuthorizationStatus() const { } void AuthorizationData::reset() { + delete expiryDate; + expiryDate = nullptr; + MO_FREE(parentIdTag); + parentIdTag = nullptr; idTag[0] = '\0'; } -const char *MicroOcpp::serializeAuthorizationStatus(AuthorizationStatus status) { +namespace MicroOcpp { +namespace v16 { + +const char *serializeAuthorizationStatus(AuthorizationStatus status) { switch (status) { case (AuthorizationStatus::Accepted): return "Accepted"; @@ -160,7 +187,7 @@ const char *MicroOcpp::serializeAuthorizationStatus(AuthorizationStatus status) } } -MicroOcpp::AuthorizationStatus MicroOcpp::deserializeAuthorizationStatus(const char *cstr) { +AuthorizationStatus deserializeAuthorizationStatus(const char *cstr) { if (!cstr) { return AuthorizationStatus::UNDEFINED; } @@ -180,4 +207,6 @@ MicroOcpp::AuthorizationStatus MicroOcpp::deserializeAuthorizationStatus(const c } } -#endif //MO_ENABLE_LOCAL_AUTH +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationData.h b/src/MicroOcpp/Model/Authorization/AuthorizationData.h index 7bcdccd7..8645f054 100644 --- a/src/MicroOcpp/Model/Authorization/AuthorizationData.h +++ b/src/MicroOcpp/Model/Authorization/AuthorizationData.h @@ -5,17 +5,18 @@ #ifndef MO_AUTHORIZATIONDATA_H #define MO_AUTHORIZATIONDATA_H -#include - -#if MO_ENABLE_LOCAL_AUTH - -#include #include #include +#include +#include + #include #include +#if MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH + namespace MicroOcpp { +namespace v16 { enum class AuthorizationStatus : uint8_t { Accepted, @@ -26,25 +27,21 @@ enum class AuthorizationStatus : uint8_t { 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 +#define AUTHDATA_KEY_IDTAG(INTERNAL_FORMAT) (INTERNAL_FORMAT ? "it" : "idTag") +#define AUTHDATA_KEY_IDTAGINFO "idTagInfo" +#define AUTHDATA_KEY_EXPIRYDATE(INTERNAL_FORMAT) (INTERNAL_FORMAT ? "ed" : "expiryDate") +#define AUTHDATA_KEY_PARENTIDTAG(INTERNAL_FORMAT) (INTERNAL_FORMAT ? "pi" : "parentIdTag") +#define AUTHDATA_KEY_STATUS(INTERNAL_FORMAT) (INTERNAL_FORMAT ? "st" : "status") 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; + Timestamp *expiryDate = nullptr; //has ownership - char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; + char idTag [MO_IDTAG_LEN_MAX + 1] = {'\0'}; AuthorizationStatus status = AuthorizationStatus::UNDEFINED; public: @@ -54,10 +51,10 @@ class AuthorizationData : public MemoryManaged { AuthorizationData& operator=(AuthorizationData&& other); - void readJson(JsonObject entry, bool compact = false); //compact: compressed representation for flash storage + bool readJson(Clock& clock, JsonObject entry, bool internalFormat); //internalFormat: compressed representation for flash storage - size_t getJsonCapacity() const; - void writeJson(JsonObject& entry, bool compact = false); //compact: compressed representation for flash storage + size_t getJsonCapacity(bool internalFormat) const; + void writeJson(Clock& clock, JsonObject& entry, bool internalFormat); //internalFormat: compressed representation for flash storage const char *getIdTag() const; Timestamp *getExpiryDate() const; @@ -67,7 +64,7 @@ class AuthorizationData : public MemoryManaged { void reset(); }; -} - -#endif //MO_ENABLE_LOCAL_AUTH +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH #endif diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationList.cpp b/src/MicroOcpp/Model/Authorization/AuthorizationList.cpp index dd438ff8..9a0c57d5 100644 --- a/src/MicroOcpp/Model/Authorization/AuthorizationList.cpp +++ b/src/MicroOcpp/Model/Authorization/AuthorizationList.cpp @@ -1,202 +1,315 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License -#include - -#if MO_ENABLE_LOCAL_AUTH - #include #include #include #include +#if MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH + using namespace MicroOcpp; +using namespace MicroOcpp::v16; -AuthorizationList::AuthorizationList() : MemoryManaged("v16.Authorization.AuthorizationList"), localAuthorizationList(makeVector(getMemoryTag())) { +AuthorizationList::AuthorizationList() : MemoryManaged("v16.Authorization.AuthorizationList") { } AuthorizationList::~AuthorizationList() { - + clear(); } -MicroOcpp::AuthorizationData *AuthorizationList::get(const char *idTag) { - //binary search +MicroOcpp::v16::AuthorizationData *AuthorizationList::get(const char *idTag) { if (!idTag) { return nullptr; } + //binary search int l = 0; - int r = ((int) localAuthorizationList.size()) - 1; + int r = ((int) localAuthorizationListSize) - 1; while (l <= r) { auto m = (r + l) / 2; - auto diff = strcmp(localAuthorizationList[m].getIdTag(), idTag); + 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 localAuthorizationList[m]; } } return nullptr; } -bool AuthorizationList::readJson(JsonArray authlistJson, int listVersion, bool differential, bool compact) { +bool AuthorizationList::readJson(Clock& clock, JsonArray authlistJson, int listVersion, bool differential, bool internalFormat) { - if (compact) { + if (internalFormat) { //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; + size_t resListSize = 0; + size_t updateSize = 0; if (!differential) { //every entry will insert an idTag - resultingListLength = authlistJson.size(); + resListSize = authlistJson.size(); + updateSize = 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); + resListSize = localAuthorizationListSize; 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)]); + AuthorizationData *found = get(authlistJson[i][AUTHDATA_KEY_IDTAG(internalFormat)]); 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 + resListSize--; + } else { + //this entry should be updated + updateSize++; + } } else { //insert or ignore? if (authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAGINFO)) { //add - resultingListLength++; - } //else: ignore + resListSize++; + updateSize++; + } else { + // invalid record + return false; + } } } } - if (resultingListLength > MO_LocalAuthListMaxLength) { + if (resListSize > MO_LocalAuthListMaxLength) { MO_DBG_WARN("localAuthList capacity exceeded"); return false; } - //apply new list - if (compact) { - localAuthorizationList.clear(); + AuthorizationData **updateList = nullptr; //list of newly allocated entries + AuthorizationData **resList = nullptr; //resulting list after list update. Contains pointers to old auth list and updateList - for (size_t i = 0; i < authlistJson.size(); i++) { - localAuthorizationList.emplace_back(); - localAuthorizationList.back().readJson(authlistJson[i], compact); - } - } else if (differential) { + size_t updateWritten = 0; + size_t resWritten = 0; - for (size_t i = 0; i < authlistJson.size(); i++) { + if (updateSize > 0) { + updateList = static_cast(MO_MALLOC(getMemoryTag(), sizeof(AuthorizationData*) * updateSize)); + if (!updateList) { + MO_DBG_ERR("OOM"); + goto fail; + } + memset(updateList, 0, sizeof(AuthorizationData*) * updateSize); + } - //is entry a remove command? - if (!authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAGINFO)) { - continue; //yes, remove command, will be deleted afterwards - } + if (resListSize > 0) { + resList = static_cast(MO_MALLOC(getMemoryTag(), sizeof(AuthorizationData*) * resListSize)); + if (!resList) { + MO_DBG_ERR("OOM"); + goto fail; + } + memset(resList, 0, sizeof(AuthorizationData*) * resListSize); + } - //update, or insert + // Keep, update, or remove old entries + if (differential) { + for (size_t i = 0; i < localAuthorizationListSize; i++) { - if (authlist_index[i] < 0) { - //auth list does not contain idTag yet -> insert new entry + bool remove = false; + bool update = false; - //reuse removed AuthData object? - if (!remove_list.empty()) { - //yes, reuse - authlist_index[i] = remove_list.back(); - remove_list.pop_back(); + for (size_t j = 0; j < authlistJson.size(); j++) { + //remove or update? + const char *key_i = localAuthorizationList[i]->getIdTag(); + const char *key_j = authlistJson[j][AUTHDATA_KEY_IDTAG(false)] | ""; + if (!*key_i || !*key_j || strcmp(key_i, key_j)) { + // Keys don't match + continue; + } + if (authlistJson[j].containsKey(AUTHDATA_KEY_IDTAGINFO)) { + //this entry should be updated + update = true; + break; } else { - //no, create new - authlist_index[i] = localAuthorizationList.size(); - localAuthorizationList.emplace_back(); + //this entry should be removed + remove = true; + break; } } - localAuthorizationList[authlist_index[i]].readJson(authlistJson[i], compact); + if (remove) { + // resList won't include this entry + (void)0; + } else if (update) { + updateList[updateWritten] = new AuthorizationData(); + if (!updateList[updateWritten]) { + MO_DBG_ERR("OOM"); + goto fail; + } + if (!updateList[updateWritten]->readJson(clock, authlistJson[i], internalFormat)) { + MO_DBG_ERR("format error"); + goto fail; + } + resList[resWritten] = updateList[updateWritten]; + updateWritten++; + resWritten++; + } else { + resList[resWritten] = localAuthorizationList[i]; + resWritten++; + } } + } - } else { - localAuthorizationList.clear(); + // Insert new entries + for (size_t i = 0; i < authlistJson.size(); i++) { - 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); + if (!internalFormat && !authlistJson[i].containsKey(AUTHDATA_KEY_IDTAGINFO)) { + // remove already handled above + continue; + } + + bool insert = true; + + const char *key_i = authlistJson[i][AUTHDATA_KEY_IDTAG(false)] | ""; + + for (size_t j = 0; j < resWritten; j++) { + //insert? + const char *key_j = resList[j]->getIdTag(); + if (*key_i && key_j && !strcmp(key_i, key_j)) { + // Keys match, this entry was updated above, and should not be inserted + insert = false; + break; } } + + if (insert) { + updateList[updateWritten] = new AuthorizationData(); + if (!updateList[updateWritten]) { + MO_DBG_ERR("OOM"); + goto fail; + } + if (!updateList[updateWritten]->readJson(clock, authlistJson[i], internalFormat)) { + MO_DBG_ERR("format error"); + goto fail; + } + resList[resWritten] = updateList[updateWritten]; + updateWritten++; + resWritten++; + } } - 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()); + // sanity check 1 + if (resWritten != resListSize) { + MO_DBG_ERR("internal error"); + goto fail; + } + + // sanity check 2 + for (size_t i = 0; i < resListSize; i++) { + if (!resList[i]) { + MO_DBG_ERR("internal error"); + goto fail; + } + } + + qsort(resList, resListSize, sizeof(resList[0]), + [] (const void* a,const void* b) -> int { + return strcmp( + (*reinterpret_cast(a))->getIdTag(), + (*reinterpret_cast(b))->getIdTag()); + }); + + // success - 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()) { + if (resListSize == 0) { this->listVersion = 0; } + for (size_t i = 0; i < localAuthorizationListSize; i++) { + bool found = false; + for (size_t j = 0; j < resListSize; j++) { + if (localAuthorizationList[i] == resList[j]) { + found = true; + break; + } + } + + if (!found) { + //entry not used anymore + delete localAuthorizationList[i]; + localAuthorizationList[i] = nullptr; + } + } + MO_FREE(localAuthorizationList); + localAuthorizationList = nullptr; + localAuthorizationListSize = 0; + + localAuthorizationList = resList; + localAuthorizationListSize = resListSize; + + MO_FREE(updateList); + updateList = nullptr; + updateSize = 0; + return true; +fail: + if (updateList) { + for (size_t i = 0; i < updateSize; i++) { + delete updateList[i]; + updateList[i] = nullptr; + } + } + MO_FREE(updateList); + updateList = nullptr; + + MO_FREE(resList); + resList = nullptr; + resListSize = 0; + + return false; } void AuthorizationList::clear() { - localAuthorizationList.clear(); + for (size_t i = 0; i < localAuthorizationListSize; i++) { + delete localAuthorizationList[i]; + localAuthorizationList[i] = nullptr; + } + MO_FREE(localAuthorizationList); + localAuthorizationList = nullptr; + localAuthorizationListSize = 0; listVersion = 0; } -size_t AuthorizationList::getJsonCapacity() { - size_t res = JSON_ARRAY_SIZE(localAuthorizationList.size()); - for (auto& entry : localAuthorizationList) { - res += entry.getJsonCapacity(); +size_t AuthorizationList::getJsonCapacity(bool internalFormat) { + size_t res = JSON_ARRAY_SIZE(localAuthorizationListSize); + for (size_t i = 0; i < localAuthorizationListSize; i++) { + res += localAuthorizationList[i]->getJsonCapacity(internalFormat); } return res; } -void AuthorizationList::writeJson(JsonArray authListOut, bool compact) { - for (auto& entry : localAuthorizationList) { +void AuthorizationList::writeJson(Clock& clock, JsonArray authListOut, bool internalFormat) { + for (size_t i = 0; i < localAuthorizationListSize; i++) { JsonObject entryJson = authListOut.createNestedObject(); - entry.writeJson(entryJson, compact); + localAuthorizationList[i]->writeJson(clock, entryJson, internalFormat); } } size_t AuthorizationList::size() { - return localAuthorizationList.size(); + return localAuthorizationListSize; } -#endif //MO_ENABLE_LOCAL_AUTH +#endif //MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationList.h b/src/MicroOcpp/Model/Authorization/AuthorizationList.h index 0a083777..bb13b95a 100644 --- a/src/MicroOcpp/Model/Authorization/AuthorizationList.h +++ b/src/MicroOcpp/Model/Authorization/AuthorizationList.h @@ -1,16 +1,15 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_AUTHORIZATIONLIST_H #define MO_AUTHORIZATIONLIST_H -#include - -#if MO_ENABLE_LOCAL_AUTH - #include #include +#include + +#if MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH #ifndef MO_LocalAuthListMaxLength #define MO_LocalAuthListMaxLength 48 @@ -22,28 +21,33 @@ namespace MicroOcpp { +class Clock; + +namespace v16 { + class AuthorizationList : public MemoryManaged { private: int listVersion = 0; - Vector localAuthorizationList; //sorted list + AuthorizationData **localAuthorizationList = nullptr; //sorted array + size_t localAuthorizationListSize = 0; 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 + bool readJson(Clock& clock, JsonArray localAuthorizationList, int listVersion, bool differential, bool internalFormat); //internalFormat: if true, then use compact non-ocpp representation void clear(); - size_t getJsonCapacity(); - void writeJson(JsonArray authListOut, bool compact = false); + size_t getJsonCapacity(bool internalFormat); + void writeJson(Clock& clock, JsonArray authListOut, bool internalFormat); int getListVersion() {return listVersion;} size_t size(); //used in unit tests }; -} - -#endif //MO_ENABLE_LOCAL_AUTH +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH #endif diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationService.cpp b/src/MicroOcpp/Model/Authorization/AuthorizationService.cpp index e9dc39d2..00e0ddf1 100644 --- a/src/MicroOcpp/Model/Authorization/AuthorizationService.cpp +++ b/src/MicroOcpp/Model/Authorization/AuthorizationService.cpp @@ -2,46 +2,54 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_LOCAL_AUTH - #include -#include +#include +#include #include -#include +#include #include -#include #include #include #include #include #include -#define MO_LOCALAUTHORIZATIONLIST_FN (MO_FILENAME_PREFIX "localauth.jsn") +#if MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH + +#define MO_LOCALAUTHORIZATIONLIST_FN "localauth.jsn" using namespace MicroOcpp; +using namespace MicroOcpp::v16; + +AuthorizationService::AuthorizationService(Context& context) : MemoryManaged("v16.Authorization.AuthorizationService"), context(context) { + +} + +bool AuthorizationService::setup() { -AuthorizationService::AuthorizationService(Context& context, std::shared_ptr filesystem) : MemoryManaged("v16.Authorization.AuthorizationService"), context(context), filesystem(filesystem) { + filesystem = context.getFilesystem(); - localAuthListEnabledBool = declareConfiguration("LocalAuthListEnabled", true); - declareConfiguration("LocalAuthListMaxLength", MO_LocalAuthListMaxLength, CONFIGURATION_VOLATILE, true); - declareConfiguration("SendLocalListMaxLength", MO_SendLocalListMaxLength, CONFIGURATION_VOLATILE, true); + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("initialization error"); + return false; + } + localAuthListEnabledBool = configService->declareConfiguration("LocalAuthListEnabled", true); if (!localAuthListEnabledBool) { MO_DBG_ERR("initialization error"); + return false; } - - context.getOperationRegistry().registerOperation("GetLocalListVersion", [&context] () { - return new Ocpp16::GetLocalListVersion(context.getModel());}); - context.getOperationRegistry().registerOperation("SendLocalList", [this] () { - return new Ocpp16::SendLocalList(*this);}); - loadLists(); -} + configService->declareConfiguration("LocalAuthListMaxLength", MO_LocalAuthListMaxLength, MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); + configService->declareConfiguration("SendLocalListMaxLength", MO_SendLocalListMaxLength, MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); + + context.getMessageService().registerOperation("GetLocalListVersion", [] (Context& context) -> Operation* { + return new GetLocalListVersion(context.getModel16());}); + context.getMessageService().registerOperation("SendLocalList", [] (Context& context) -> Operation* { + return new SendLocalList(*context.getModel16().getAuthorizationService());}); -AuthorizationService::~AuthorizationService() { - + return loadLists(); } bool AuthorizationService::loadLists() { @@ -50,23 +58,26 @@ bool AuthorizationService::loadLists() { 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; + JsonDoc doc (0); + auto ret = FilesystemUtils::loadJson(filesystem, MO_LOCALAUTHORIZATIONLIST_FN, doc, getMemoryTag()); + switch (ret) { + case FilesystemUtils::LoadStatus::Success: + break; //continue loading JSON + case FilesystemUtils::LoadStatus::FileNotFound: + MO_DBG_DEBUG("no local authorization list stored already"); + return true; + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + return false; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("failed to load %s", MO_LOCALAUTHORIZATIONLIST_FN); + return false; } - JsonObject root = doc->as(); + int listVersion = doc["listVersion"] | 0; - int listVersion = root["listVersion"] | 0; - - if (!localAuthorizationList.readJson(root["localAuthorizationList"].as(), listVersion, false, true)) { + if (!localAuthorizationList.readJson(context.getClock(), doc["localAuthorizationList"].as(), listVersion, /*differential*/ false, /*internalFormat*/ true)) { MO_DBG_ERR("list read failure"); return false; } @@ -102,19 +113,19 @@ size_t AuthorizationService::getLocalListSize() { } bool AuthorizationService::updateLocalList(JsonArray localAuthorizationListJson, int listVersion, bool differential) { - bool success = localAuthorizationList.readJson(localAuthorizationListJson, listVersion, differential, false); + bool success = localAuthorizationList.readJson(context.getClock(), localAuthorizationListJson, listVersion, differential, /*internalFormat*/ false); + + if (success && filesystem) { - if (success) { - auto doc = initJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(3) + - localAuthorizationList.getJsonCapacity()); + localAuthorizationList.getJsonCapacity(/*internalFormat*/ true)); JsonObject root = doc.to(); root["listVersion"] = listVersion; JsonArray authListCompact = root.createNestedArray("localAuthorizationList"); - localAuthorizationList.writeJson(authListCompact, true); - success = FilesystemUtils::storeJson(filesystem, MO_LOCALAUTHORIZATIONLIST_FN, doc); + localAuthorizationList.writeJson(context.getClock(), authListCompact, /*internalFormat*/ true); + success = (FilesystemUtils::storeJson(filesystem, MO_LOCALAUTHORIZATIONLIST_FN, doc) == FilesystemUtils::StoreStatus::Success); if (!success) { loadLists(); @@ -154,10 +165,15 @@ void AuthorizationService::notifyAuthorization(const char *idTag, JsonObject idT } 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; + int32_t dtExpiryDate; + if (context.getClock().delta(context.getClock().now(), *localInfo->getExpiryDate(), dtExpiryDate)) { + if (dtExpiryDate > 0) { + //now is past expiryDate + MO_DBG_DEBUG("local auth expired"); + localStatus = AuthorizationStatus::Expired; + } + } else { + MO_DBG_ERR("cannot determine local auth expiry"); } } @@ -180,21 +196,25 @@ void AuthorizationService::notifyAuthorization(const char *idTag, JsonObject idT if (!equivalent) { //send error code "LocalListConflict" to server - ChargePointStatus cpStatus = ChargePointStatus_UNDEFINED; - if (context.getModel().getNumConnectors() > 0) { - cpStatus = context.getModel().getConnector(0)->getStatus(); - } + auto availSvc = context.getModel16().getAvailabilityService(); + auto availSvcCp = availSvc ? availSvc->getEvse(0) : nullptr; + auto cpStatus = availSvcCp ? availSvcCp->getStatus() : MO_ChargePointStatus_UNDEFINED; + + MO_ErrorData errorCode; + mo_errorData_init(&errorCode); + mo_errorData_setErrorCode(&errorCode, "LocalListConflict"); - auto statusNotification = makeRequest(new Ocpp16::StatusNotification( + auto statusNotification = makeRequest(context, new StatusNotification( + context, 0, - cpStatus, //will be determined in StatusNotification::initiate - context.getModel().getClock().now(), - "LocalListConflict")); + cpStatus, + context.getClock().now(), + errorCode)); - statusNotification->setTimeout(60000); + statusNotification->setTimeout(60); - context.initiateRequest(std::move(statusNotification)); + context.getMessageService().sendRequest(std::move(statusNotification)); } } -#endif //MO_ENABLE_LOCAL_AUTH +#endif //MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationService.h b/src/MicroOcpp/Model/Authorization/AuthorizationService.h index 88ba7936..31146c1e 100644 --- a/src/MicroOcpp/Model/Authorization/AuthorizationService.h +++ b/src/MicroOcpp/Model/Authorization/AuthorizationService.h @@ -5,30 +5,33 @@ #ifndef MO_AUTHORIZATIONSERVICE_H #define MO_AUTHORIZATIONSERVICE_H -#include - -#if MO_ENABLE_LOCAL_AUTH - #include #include -#include #include +#include + +#if MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH namespace MicroOcpp { class Context; +namespace v16 { + +class Configuration; + class AuthorizationService : public MemoryManaged { private: Context& context; - std::shared_ptr filesystem; + MO_FilesystemAdapter *filesystem = nullptr; AuthorizationList localAuthorizationList; - std::shared_ptr localAuthListEnabledBool; + Configuration *localAuthListEnabledBool = nullptr; public: - AuthorizationService(Context& context, std::shared_ptr filesystem); - ~AuthorizationService(); + AuthorizationService(Context& context); + + bool setup(); bool loadLists(); @@ -42,7 +45,7 @@ class AuthorizationService : public MemoryManaged { void notifyAuthorization(const char *idTag, JsonObject idTagInfo); }; -} - -#endif //MO_ENABLE_LOCAL_AUTH +} //namespace MicroOcpp +} //namespace v16 +#endif //MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH #endif diff --git a/src/MicroOcpp/Model/Authorization/IdToken.cpp b/src/MicroOcpp/Model/Authorization/IdToken.cpp index e1637ef8..fa28718f 100644 --- a/src/MicroOcpp/Model/Authorization/IdToken.cpp +++ b/src/MicroOcpp/Model/Authorization/IdToken.cpp @@ -1,21 +1,18 @@ // matth-x/MicroOcpp // Copyright Matthias Akstaller 2019 - 2024 // MIT License - -#include - -#if MO_ENABLE_V201 - #include +#include #include #include -#include +#if MO_ENABLE_V201 using namespace MicroOcpp; +using namespace MicroOcpp::v201; -IdToken::IdToken(const char *token, Type type, const char *memoryTag) : MemoryManaged(memoryTag ? memoryTag : "v201.Authorization.IdToken"), type(type) { +IdToken::IdToken(const char *token, MO_IdTokenType 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) { @@ -27,8 +24,15 @@ IdToken::IdToken(const char *token, Type type, const char *memoryTag) : MemoryMa } } -IdToken::IdToken(const IdToken& other, const char *memoryTag) : IdToken(other.idToken, other.type, memoryTag ? memoryTag : other.getMemoryTag()) { +IdToken::IdToken(const IdToken& other, const char *memoryTag) : IdToken(other.idToken, other.type, memoryTag ? memoryTag : other.getMemoryTag()) { + +} +IdToken& IdToken::operator=(const IdToken& other) { + snprintf(this->idToken, sizeof(this->idToken), "%s", other.idToken); + this->type = other.type; + updateMemoryTag(other.getMemoryTag(), nullptr); + return *this; } bool IdToken::parseCstr(const char *token, const char *typeCstr) { @@ -37,21 +41,21 @@ bool IdToken::parseCstr(const char *token, const char *typeCstr) { } if (!strcmp(typeCstr, "Central")) { - type = Type::Central; + type = MO_IdTokenType_Central; } else if (!strcmp(typeCstr, "eMAID")) { - type = Type::eMAID; + type = MO_IdTokenType_eMAID; } else if (!strcmp(typeCstr, "ISO14443")) { - type = Type::ISO14443; + type = MO_IdTokenType_ISO14443; } else if (!strcmp(typeCstr, "ISO15693")) { - type = Type::ISO15693; + type = MO_IdTokenType_ISO15693; } else if (!strcmp(typeCstr, "KeyCode")) { - type = Type::KeyCode; + type = MO_IdTokenType_KeyCode; } else if (!strcmp(typeCstr, "Local")) { - type = Type::Local; + type = MO_IdTokenType_Local; } else if (!strcmp(typeCstr, "MacAddress")) { - type = Type::MacAddress; + type = MO_IdTokenType_MacAddress; } else if (!strcmp(typeCstr, "NoAuthorization")) { - type = Type::NoAuthorization; + type = MO_IdTokenType_NoAuthorization; } else { return false; } @@ -68,34 +72,38 @@ const char *IdToken::get() const { return idToken; } +MO_IdTokenType IdToken::getType() const { + return type; +} + const char *IdToken::getTypeCstr() const { const char *res = ""; switch (type) { - case Type::UNDEFINED: + case MO_IdTokenType_UNDEFINED: MO_DBG_ERR("internal error"); break; - case Type::Central: + case MO_IdTokenType_Central: res = "Central"; break; - case Type::eMAID: + case MO_IdTokenType_eMAID: res = "eMAID"; break; - case Type::ISO14443: + case MO_IdTokenType_ISO14443: res = "ISO14443"; break; - case Type::ISO15693: + case MO_IdTokenType_ISO15693: res = "ISO15693"; break; - case Type::KeyCode: + case MO_IdTokenType_KeyCode: res = "KeyCode"; break; - case Type::Local: + case MO_IdTokenType_Local: res = "Local"; break; - case Type::MacAddress: + case MO_IdTokenType_MacAddress: res = "MacAddress"; break; - case Type::NoAuthorization: + case MO_IdTokenType_NoAuthorization: res = "NoAuthorization"; break; } @@ -107,4 +115,4 @@ bool IdToken::equals(const IdToken& other) { return type == other.type && !strcmp(idToken, other.idToken); } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Authorization/IdToken.h b/src/MicroOcpp/Model/Authorization/IdToken.h index cb209872..a38ac8f2 100644 --- a/src/MicroOcpp/Model/Authorization/IdToken.h +++ b/src/MicroOcpp/Model/Authorization/IdToken.h @@ -5,52 +5,67 @@ #ifndef MO_IDTOKEN_H #define MO_IDTOKEN_H +#include + +#include #include +#if MO_ENABLE_V16 + +#define MO_IDTAG_LEN_MAX 20 + +#endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 -#include +#ifdef __cplusplus +extern "C" { +#endif -#include +// IdTokenEnumType (3.43) +typedef enum { + MO_IdTokenType_UNDEFINED, + MO_IdTokenType_Central, + MO_IdTokenType_eMAID, + MO_IdTokenType_ISO14443, + MO_IdTokenType_ISO15693, + MO_IdTokenType_KeyCode, + MO_IdTokenType_Local, + MO_IdTokenType_MacAddress, + MO_IdTokenType_NoAuthorization, +} MO_IdTokenType; + +#ifdef __cplusplus +} //extern "C" #define MO_IDTOKEN_LEN_MAX 36 namespace MicroOcpp { +namespace v201 { // 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; + MO_IdTokenType type = MO_IdTokenType_UNDEFINED; public: - IdToken(const char *token = nullptr, Type type = Type::ISO14443, const char *memoryTag = nullptr); + IdToken(const char *token = nullptr, MO_IdTokenType type = MO_IdTokenType_ISO14443, const char *memoryTag = nullptr); IdToken(const IdToken& other, const char *memoryTag = nullptr); + IdToken& operator=(const IdToken& other); + bool parseCstr(const char *token, const char *typeCstr); const char *get() const; + MO_IdTokenType getType() const; const char *getTypeCstr() const; bool equals(const IdToken& other); }; -} // namespace MicroOcpp - -#endif // MO_ENABLE_V201 +} //namespace MicroOcpp +} //namespace v201 +#endif //__cplusplus +#endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Availability/AvailabilityDefs.cpp b/src/MicroOcpp/Model/Availability/AvailabilityDefs.cpp new file mode 100644 index 00000000..001fb194 --- /dev/null +++ b/src/MicroOcpp/Model/Availability/AvailabilityDefs.cpp @@ -0,0 +1,74 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include + +#if MO_ENABLE_V16 + +void mo_errorData_init(MO_ErrorData *errorData) { + memset(errorData, 0, sizeof(*errorData)); + errorData->severity = 1; +} + +void mo_errorData_setErrorCode(MO_ErrorData *errorData, const char *errorCode) { + errorData->errorCode = errorCode; + if (errorCode) { + errorData->isError = true; + errorData->isFaulted = true; + } +} + +void mo_errorDataInput_init(MO_ErrorDataInput *errorDataInput) { + memset(errorDataInput, 0, sizeof(*errorDataInput)); +} + +#endif //MO_ENABLE_V16 + +#if MO_ENABLE_V16 || MO_ENABLE_V201 + +const char *mo_serializeChargePointStatus(MO_ChargePointStatus v) { + const char *res = ""; + switch (v) { + case (MO_ChargePointStatus_UNDEFINED): + res = ""; + break; + case (MO_ChargePointStatus_Available): + res = "Available"; + break; + case (MO_ChargePointStatus_Preparing): + res = "Preparing"; + break; + case (MO_ChargePointStatus_Charging): + res = "Charging"; + break; + case (MO_ChargePointStatus_SuspendedEVSE): + res = "SuspendedEVSE"; + break; + case (MO_ChargePointStatus_SuspendedEV): + res = "SuspendedEV"; + break; + case (MO_ChargePointStatus_Finishing): + res = "Finishing"; + break; + case (MO_ChargePointStatus_Reserved): + res = "Reserved"; + break; + case (MO_ChargePointStatus_Unavailable): + res = "Unavailable"; + break; + case (MO_ChargePointStatus_Faulted): + res = "Faulted"; + break; +#if MO_ENABLE_V201 + case (MO_ChargePointStatus_Occupied): + res = "Occupied"; + break; +#endif + } + return res; +} + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Availability/AvailabilityDefs.h b/src/MicroOcpp/Model/Availability/AvailabilityDefs.h new file mode 100644 index 00000000..65c0f2a8 --- /dev/null +++ b/src/MicroOcpp/Model/Availability/AvailabilityDefs.h @@ -0,0 +1,81 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_AVAILABILITYDEFS_H +#define MO_AVAILABILITYDEFS_H + +#include + +#include + +#if MO_ENABLE_V16 + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + bool isError; //if any error information is set + bool isFaulted; //if this is a severe error and the EVSE should go into the faulted state + uint8_t severity; //severity: don't send less severe errors during highly severe error condition + const char *errorCode; //see ChargePointErrorCode (p. 76/77) for possible values + const char *info; //Additional free format information related to the error + const char *vendorId; //vendor-specific implementation identifier + const char *vendorErrorCode; //vendor-specific error code +} MO_ErrorData; + +void mo_errorData_init(MO_ErrorData *errorData); +void mo_errorData_setErrorCode(MO_ErrorData *errorData, const char *errorCode); + +typedef struct { + MO_ErrorData (*getErrorData)(unsigned int evseId, void *userData); + void *userData; +} MO_ErrorDataInput; + +void mo_errorDataInput_init(MO_ErrorDataInput *errorDataInput); + +#ifdef __cplusplus +} //extern "C" +#endif + +#endif //MO_ENABLE_V16 + +#if MO_ENABLE_V16 || MO_ENABLE_V201 +#ifdef __cplusplus +extern "C" { +#endif //__cplusplus + +typedef enum MO_ChargePointStatus { + MO_ChargePointStatus_UNDEFINED, //internal use only - no OCPP standard value + MO_ChargePointStatus_Available, + MO_ChargePointStatus_Preparing, + MO_ChargePointStatus_Charging, + MO_ChargePointStatus_SuspendedEVSE, + MO_ChargePointStatus_SuspendedEV, + MO_ChargePointStatus_Finishing, + MO_ChargePointStatus_Reserved, + MO_ChargePointStatus_Unavailable, + MO_ChargePointStatus_Faulted, +#if MO_ENABLE_V201 + MO_ChargePointStatus_Occupied, +#endif +} MO_ChargePointStatus; + +const char *mo_serializeChargePointStatus(MO_ChargePointStatus v); + +#ifdef __cplusplus +} //extern "C" + +namespace MicroOcpp { + +enum class ChangeAvailabilityStatus : uint8_t { + Accepted, + Rejected, + Scheduled +}; + +} //namespace MicroOcpp +#endif //__cplusplus +#endif //MO_ENABLE_V201 +#endif diff --git a/src/MicroOcpp/Model/Availability/AvailabilityService.cpp b/src/MicroOcpp/Model/Availability/AvailabilityService.cpp index b1e59419..e5224579 100644 --- a/src/MicroOcpp/Model/Availability/AvailabilityService.cpp +++ b/src/MicroOcpp/Model/Availability/AvailabilityService.cpp @@ -2,74 +2,491 @@ // 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 +#if MO_ENABLE_V16 + +using namespace MicroOcpp; + +v16::AvailabilityServiceEvse::AvailabilityServiceEvse(Context& context, AvailabilityService& availService, unsigned int evseId) : MemoryManaged("v16.Availability.AvailabilityServiceEvse"), context(context), clock(context.getClock()), model(context.getModel16()), availService(availService), evseId(evseId) { + +} + +v16::AvailabilityServiceEvse::~AvailabilityServiceEvse() { + if (availabilityBool->getKey() == availabilityBoolKey) { + availabilityBool->setKey(nullptr); + } +} + + +bool v16::AvailabilityServiceEvse::setup() { + + connection = context.getConnection(); + if (!connection) { + MO_DBG_ERR("setup failure"); + return false; + } + + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("setup failure"); + return false; + } + + snprintf(availabilityBoolKey, sizeof(availabilityBoolKey), MO_CONFIG_EXT_PREFIX "AVAIL_CONN_%d", evseId); + availabilityBool = configService->declareConfiguration(availabilityBoolKey, true, MO_KEYVALUE_FN, Mutability::None); + if (!availabilityBool) { + MO_DBG_ERR("setup failure"); + return false; + } + + availabilityContainer = configService->findContainerOfConfiguration(availabilityBool); + if (!availabilityContainer) { + MO_DBG_ERR("setup failure"); + return false; + } + + auto txSvc = context.getModel16().getTransactionService(); + txServiceEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (!txServiceEvse) { + MO_DBG_ERR("setup failure"); + return false; + } + + return true; +} + +void v16::AvailabilityServiceEvse::loop() { + + if (!trackLoopExecute) { + trackLoopExecute = true; + } + + MO_ErrorData errorData; + mo_errorData_init(&errorData); + errorData.severity = 0; + int errorDataIndex = -1; + + if (clock.now().isUnixTime()) { + + if (reportedErrorIndex >= 0) { + auto error = errorDataInputs[reportedErrorIndex].getErrorData(evseId, errorDataInputs[reportedErrorIndex].userData); + if (error.isError) { + errorData = error; + errorDataIndex = reportedErrorIndex; + } + } + + for (auto i = std::min(errorDataInputs.size(), trackErrorDataInputs.size()); i >= 1; i--) { + auto index = i - 1; + MO_ErrorData error; + if ((int)index != errorDataIndex) { + error = errorDataInputs[index].getErrorData(evseId, errorDataInputs[index].userData); + } 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 = MO_ChargePointStatus_UNDEFINED; //trigger sending currentStatus again with code NoError + } else { + reportedErrorIndex = -1; + } + } + } + + auto status = getStatus(); + + if (status != currentStatus) { + MO_DBG_DEBUG("Status changed %s -> %s %s", + currentStatus == MO_ChargePointStatus_UNDEFINED ? "" : mo_serializeChargePointStatus(currentStatus), + mo_serializeChargePointStatus(status), + availService.minimumStatusDurationInt->getInt() ? " (will report delayed)" : ""); + currentStatus = status; + t_statusTransition = clock.now(); + } + + int32_t dtStatusTransition; + if (!t_statusTransition.isDefined() || !clock.delta(clock.now(), t_statusTransition, dtStatusTransition)) { + dtStatusTransition = 0; + } + + if (reportedStatus != currentStatus && + connection->isConnected() && + clock.now().isUnixTime() && + (availService.minimumStatusDurationInt->getInt() <= 0 || //MinimumStatusDuration disabled + dtStatusTransition >= availService.minimumStatusDurationInt->getInt())) { + reportedStatus = currentStatus; + reportedErrorIndex = errorDataIndex; + if (errorDataIndex >= 0) { + trackErrorDataInputs[errorDataIndex] = true; + } + + auto statusNotificationOp = new StatusNotification(context, evseId, reportedStatus, t_statusTransition, errorData); + if (!statusNotificationOp) { + MO_DBG_ERR("OOM"); + return; + } + auto statusNotification = makeRequest(context, statusNotificationOp); + if (!statusNotification) { + MO_DBG_ERR("OOM"); + return; + } + statusNotification->setTimeout(0); + context.getMessageService().sendRequest(std::move(statusNotification)); + return; + } +} + +void v16::AvailabilityServiceEvse::setConnectorPluggedInput(bool (*connectorPlugged)(unsigned int, void*), void *userData) { + this->connectorPluggedInput = connectorPlugged; + this->connectorPluggedInputUserData = userData; +} + +void v16::AvailabilityServiceEvse::setEvReadyInput(bool (*evReady)(unsigned int, void*), void *userData) { + this->evReadyInput = evReady; + this->evReadyInputUserData = userData; +} + +void v16::AvailabilityServiceEvse::setEvseReadyInput(bool (*evseReady)(unsigned int, void*), void *userData) { + this->evseReadyInput = evseReady; + this->evseReadyInputUserData = userData; +} + +void v16::AvailabilityServiceEvse::setOccupiedInput(bool (*occupied)(unsigned int, void*), void *userData) { + this->occupiedInput = occupied; + this->occupiedInputUserData = userData; +} + +bool v16::AvailabilityServiceEvse::addErrorDataInput(MO_ErrorDataInput errorDataInput) { + size_t capacity = errorDataInputs.size() + 1; + errorDataInputs.reserve(capacity); + trackErrorDataInputs.reserve(capacity); + if (errorDataInputs.capacity() < capacity || trackErrorDataInputs.capacity() < capacity) { + MO_DBG_ERR("OOM"); + return false; + } + + errorDataInputs.push_back(errorDataInput); + trackErrorDataInputs.push_back(false); + return true; +} + +void v16::AvailabilityServiceEvse::setAvailability(bool available) { + availabilityBool->setBool(available); + availabilityContainer->commit(); +} + +void v16::AvailabilityServiceEvse::setAvailabilityVolatile(bool available) { + availabilityVolatile = available; +} + +MO_ChargePointStatus v16::AvailabilityServiceEvse::getStatus() { + + MO_ChargePointStatus res = MO_ChargePointStatus_UNDEFINED; + + /* + * Handle special case: This is the Connector for the whole CP (i.e. evseId=0) --> only states Available, Unavailable, Faulted are possible + */ + if (evseId == 0) { + if (isFaulted()) { + res = MO_ChargePointStatus_Faulted; + } else if (!isOperative()) { + res = MO_ChargePointStatus_Unavailable; + } else { + res = MO_ChargePointStatus_Available; + } + return res; + } + + auto transaction = txServiceEvse->getTransaction(); + + if (isFaulted()) { + res = MO_ChargePointStatus_Faulted; + } else if (!isOperative()) { + res = MO_ChargePointStatus_Unavailable; + } else if (transaction && transaction->isRunning()) { + //Transaction is currently running + if (connectorPluggedInput && !connectorPluggedInput(evseId, connectorPluggedInputUserData)) { //special case when StopTransactionOnEVSideDisconnect is false + res = MO_ChargePointStatus_SuspendedEV; + } else if (!txServiceEvse->ocppPermitsCharge() || + (evseReadyInput && !evseReadyInput(evseId, evseReadyInputUserData))) { + res = MO_ChargePointStatus_SuspendedEVSE; + } else if (evReadyInput && !evReadyInput(evseId, evReadyInputUserData)) { + res = MO_ChargePointStatus_SuspendedEV; + } else { + res = MO_ChargePointStatus_Charging; + } + } + #if MO_ENABLE_RESERVATION + else if (model.getReservationService() && model.getReservationService()->getReservation(evseId)) { + res = MO_ChargePointStatus_Reserved; + } + #endif + else if ((!transaction) && //no transaction process occupying the connector + (!connectorPluggedInput || !connectorPluggedInput(evseId, connectorPluggedInputUserData)) && //no vehicle plugged + (!occupiedInput || !occupiedInput(evseId, occupiedInputUserData))) { //occupied override clear + res = MO_ChargePointStatus_Available; + } else { + /* + * Either in Preparing or Finishing state. Only way to know is from previous state + */ + const auto previous = currentStatus; + if (previous == MO_ChargePointStatus_Finishing || + previous == MO_ChargePointStatus_Charging || + previous == MO_ChargePointStatus_SuspendedEV || + previous == MO_ChargePointStatus_SuspendedEVSE || + (transaction && transaction->getStartSync().isRequested())) { //transaction process still occupying the connector + res = MO_ChargePointStatus_Finishing; + } else { + res = MO_ChargePointStatus_Preparing; + } + } + + if (res == MO_ChargePointStatus_UNDEFINED) { + MO_DBG_DEBUG("status undefined"); + return MO_ChargePointStatus_Faulted; //internal error + } + + return res; +} + +bool v16::AvailabilityServiceEvse::isFaulted() { + //for (auto i = errorDataInputs.begin(); i != errorDataInputs.end(); ++i) { + for (size_t i = 0; i < errorDataInputs.size(); i++) { + if (errorDataInputs[i].getErrorData(evseId, errorDataInputs[i].userData).isFaulted) { + return true; + } + } + return false; +} + +const char *v16::AvailabilityServiceEvse::getErrorCode() { + if (reportedErrorIndex >= 0) { + auto error = errorDataInputs[reportedErrorIndex].getErrorData(evseId, errorDataInputs[reportedErrorIndex].userData); + if (error.isError && error.errorCode) { + return error.errorCode; + } + } + return nullptr; +} + +bool v16::AvailabilityServiceEvse::isOperative() { + if (isFaulted()) { + return false; + } + + if (!trackLoopExecute) { + return false; + } + + //check for running transaction(s) - if yes then the connector is always operative + if (evseId == 0) { + for (unsigned int cId = 1; cId < model.getNumEvseId(); cId++) { + auto txService = model.getTransactionService(); + auto txServiceEvse = txService ? txService->getEvse(cId) : nullptr; + auto transaction = txServiceEvse ? txServiceEvse->getTransaction() : nullptr; + if (transaction && transaction->isRunning()) { + return true; + } + } + } else { + auto transaction = txServiceEvse->getTransaction(); + if (transaction && transaction->isRunning()) { + return true; + } + } + + return availabilityVolatile && availabilityBool->getBool(); +} + +Operation *v16::AvailabilityServiceEvse::createTriggeredStatusNotification() { + + MO_ErrorData errorData; + mo_errorData_init(&errorData); + errorData.severity = 0; + + if (reportedErrorIndex >= 0) { + errorData = errorDataInputs[reportedErrorIndex].getErrorData(evseId, errorDataInputs[reportedErrorIndex].userData); + } else { + //find errorData with maximum severity + for (auto i = errorDataInputs.size(); i >= 1; i--) { + auto index = i - 1; + MO_ErrorData error = errorDataInputs[index].getErrorData(evseId, errorDataInputs[index].userData); + if (error.isError && error.severity >= errorData.severity) { + errorData = error; + } + } + } + + return new StatusNotification( + context, + evseId, + getStatus(), + clock.now(), + errorData); +} + +v16::AvailabilityService::AvailabilityService(Context& context) : MemoryManaged("v16.Availability.AvailabilityService"), context(context) { + +} + +v16::AvailabilityService::~AvailabilityService() { + for (size_t i = 0; i < MO_NUM_EVSEID && evses[i]; i++) { + delete evses[i]; + evses[i] = nullptr; + } +} + +bool v16::AvailabilityService::setup() { + + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("setup failure"); + return false; + } + + minimumStatusDurationInt = configService->declareConfiguration("MinimumStatusDuration", 0); + if (!minimumStatusDurationInt) { + MO_DBG_ERR("setup failure"); + return false; + } + + context.getMessageService().registerOperation("ChangeAvailability", [] (Context& context) -> Operation* { + return new ChangeAvailability(context.getModel16());}); + + #if MO_ENABLE_MOCK_SERVER + context.getMessageService().registerOperation("StatusNotification", nullptr, nullptr); + #endif //MO_ENABLE_MOCK_SERVER + + auto rcService = context.getModel16().getRemoteControlService(); + if (!rcService) { + MO_DBG_ERR("initialization error"); + return false; + } + + rcService->addTriggerMessageHandler("StatusNotification", [] (Context& context, unsigned int evseId) { + auto availSvc = context.getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + return availSvcEvse ? availSvcEvse->createTriggeredStatusNotification() : nullptr; + }); + + numEvseId = context.getModel16().getNumEvseId(); + for (unsigned int i = 0; i < numEvseId; i++) { + if (!getEvse(i) || !getEvse(i)->setup()) { + MO_DBG_ERR("connector init failure"); + return false; + } + } + + return true; +} + +void v16::AvailabilityService::loop() { + for (size_t i = 0; i < numEvseId && evses[i]; i++) { + evses[i]->loop(); + } +} + +v16::AvailabilityServiceEvse *v16::AvailabilityService::getEvse(unsigned int evseId) { + if (evseId >= numEvseId) { + MO_DBG_ERR("evseId out of bound"); + return nullptr; + } + + if (!evses[evseId]) { + evses[evseId] = new AvailabilityServiceEvse(context, *this, evseId); + if (!evses[evseId]) { + MO_DBG_ERR("OOM"); + return nullptr; + } + } + + return evses[evseId]; +} + +#endif //MO_ENABLE_V16 + +#if MO_ENABLE_V201 + using namespace MicroOcpp; -AvailabilityServiceEvse::AvailabilityServiceEvse(Context& context, AvailabilityService& availabilityService, unsigned int evseId) : MemoryManaged("v201.Availability.AvailabilityServiceEvse"), context(context), availabilityService(availabilityService), evseId(evseId) { +v201::AvailabilityServiceEvse::AvailabilityServiceEvse(Context& context, AvailabilityService& availService, unsigned int evseId) : MemoryManaged("v201.Availability.AvailabilityServiceEvse"), context(context), availService(availService), evseId(evseId), faultedInputs(makeVector(getMemoryTag())) { } -void AvailabilityServiceEvse::loop() { +void v201::AvailabilityServiceEvse::loop() { + + if (!trackLoopExecute) { + trackLoopExecute = true; + } if (evseId >= 1) { auto status = getStatus(); if (status != reportedStatus && - context.getModel().getClock().now() >= MIN_TIME) { + context.getClock().now().isUnixTime()) { - auto statusNotification = makeRequest(new Ocpp201::StatusNotification(evseId, status, context.getModel().getClock().now())); + auto statusNotification = makeRequest(context, new StatusNotification(context, evseId, status, context.getClock().now())); statusNotification->setTimeout(0); - context.initiateRequest(std::move(statusNotification)); + context.getMessageService().sendRequest(std::move(statusNotification)); reportedStatus = status; return; } } } -void AvailabilityServiceEvse::setConnectorPluggedInput(std::function connectorPluggedInput) { - this->connectorPluggedInput = connectorPluggedInput; +void v201::AvailabilityServiceEvse::setConnectorPluggedInput(bool (*connectorPlugged)(unsigned int, void*), void *userData) { + this->connectorPluggedInput = connectorPlugged; + this->connectorPluggedInputUserData = userData; } -void AvailabilityServiceEvse::setOccupiedInput(std::function occupiedInput) { - this->occupiedInput = occupiedInput; +void v201::AvailabilityServiceEvse::setOccupiedInput(bool (*occupied)(unsigned int, void*), void *userData) { + this->occupiedInput = occupied; + this->occupiedInputUserData = userData; } -ChargePointStatus AvailabilityServiceEvse::getStatus() { - ChargePointStatus res = ChargePointStatus_UNDEFINED; +MO_ChargePointStatus v201::AvailabilityServiceEvse::getStatus() { + MO_ChargePointStatus res = MO_ChargePointStatus_UNDEFINED; if (isFaulted()) { - res = ChargePointStatus_Faulted; + res = MO_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; + res = MO_ChargePointStatus_Unavailable; + } else if ((!connectorPluggedInput || !connectorPluggedInput(evseId, connectorPluggedInputUserData)) && //no vehicle plugged + (!occupiedInput || !occupiedInput(evseId, occupiedInputUserData))) { //occupied override clear + res = MO_ChargePointStatus_Available; } else { - res = ChargePointStatus_Occupied; + res = MO_ChargePointStatus_Occupied; } return res; } -void AvailabilityServiceEvse::setUnavailable(void *requesterId) { +void v201::AvailabilityServiceEvse::setUnavailable(void *requesterId) { for (size_t i = 0; i < MO_INOPERATIVE_REQUESTERS_MAX; i++) { if (!unavailableRequesters[i]) { unavailableRequesters[i] = requesterId; @@ -79,17 +496,16 @@ void AvailabilityServiceEvse::setUnavailable(void *requesterId) { MO_DBG_ERR("exceeded max. unavailable requesters"); } -void AvailabilityServiceEvse::setAvailable(void *requesterId) { +void v201::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) { +ChangeAvailabilityStatus v201::AvailabilityServiceEvse::changeAvailability(bool operative) { if (operative) { setAvailable(this); } else { @@ -103,7 +519,7 @@ ChangeAvailabilityStatus AvailabilityServiceEvse::changeAvailability(bool operat if (evseId == 0) { for (unsigned int id = 1; id < MO_NUM_EVSEID; id++) { - if (availabilityService.getEvse(id) && availabilityService.getEvse(id)->isAvailable()) { + if (availService.getEvse(id) && availService.getEvse(id)->isAvailable()) { return ChangeAvailabilityStatus::Scheduled; } } @@ -113,29 +529,17 @@ ChangeAvailabilityStatus AvailabilityServiceEvse::changeAvailability(bool operat 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"); +Operation *v201::AvailabilityServiceEvse::createTriggeredStatusNotification() { + return new StatusNotification( + context, + evseId, + getStatus(), + context.getClock().now()); } -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() { +bool v201::AvailabilityServiceEvse::isAvailable() { - auto txService = context.getModel().getTransactionService(); + auto txService = context.getModel201().getTransactionService(); auto txEvse = txService ? txService->getEvse(evseId) : nullptr; if (txEvse) { if (txEvse->getTransaction() && @@ -146,7 +550,7 @@ bool AvailabilityServiceEvse::isAvailable() { } if (evseId > 0) { - if (availabilityService.getEvse(0) && !availabilityService.getEvse(0)->isAvailable()) { + if (availService.getEvse(0) && !availService.getEvse(0)->isAvailable()) { return false; } } @@ -159,45 +563,115 @@ bool AvailabilityServiceEvse::isAvailable() { return true; } -bool AvailabilityServiceEvse::isFaulted() { - for (size_t i = 0; i < MO_FAULTED_REQUESTERS_MAX; i++) { - if (faultedRequesters[i]) { +bool v201::AvailabilityServiceEvse::isOperative() { + if (isFaulted()) { + return false; + } + + if (!trackLoopExecute) { + return false; + } + + return isAvailable(); +} + +bool v201::AvailabilityServiceEvse::isFaulted() { + //for (auto i = errorDataInputs.begin(); i != errorDataInputs.end(); ++i) { + for (size_t i = 0; i < faultedInputs.size(); i++) { + if (faultedInputs[i].isFaulted(evseId, faultedInputs[i].userData)) { 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); +bool v201::AvailabilityServiceEvse::addFaultedInput(FaultedInput faultedInput) { + size_t capacity = faultedInputs.size() + 1; + faultedInputs.reserve(capacity); + if (faultedInputs.capacity() < capacity) { + MO_DBG_ERR("OOM"); + return false; } + faultedInputs.push_back(faultedInput); + return true; +} + +v201::AvailabilityService::AvailabilityService(Context& context) : MemoryManaged("v201.Availability.AvailabilityService"), context(context) { - 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() { +v201::AvailabilityService::~AvailabilityService() { for (size_t i = 0; i < MO_NUM_EVSEID && evses[i]; i++) { delete evses[i]; + evses[i] = nullptr; } } -void AvailabilityService::loop() { - for (size_t i = 0; i < MO_NUM_EVSEID && evses[i]; i++) { +bool v201::AvailabilityService::setup() { + + auto varSvc = context.getModel201().getVariableService(); + if (!varSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + + offlineThreshold = varSvc->declareVariable("OCPPCommCtrlr", "OfflineThreshold", 600); + if (!offlineThreshold) { + MO_DBG_ERR("setup failure"); + return false; + } + + context.getMessageService().registerOperation("ChangeAvailability", [] (Context& context) -> Operation* { + return new ChangeAvailability(*context.getModel201().getAvailabilityService());}); + + #if MO_ENABLE_MOCK_SERVER + context.getMessageService().registerOperation("StatusNotification", nullptr, nullptr); + #endif //MO_ENABLE_MOCK_SERVER + + auto rcService = context.getModel201().getRemoteControlService(); + if (!rcService) { + MO_DBG_ERR("initialization error"); + return false; + } + + rcService->addTriggerMessageHandler("StatusNotification", [] (Context& context, unsigned int evseId) { + auto availSvc = context.getModel201().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + return availSvcEvse ? availSvcEvse->createTriggeredStatusNotification() : nullptr; + }); + + numEvseId = context.getModel201().getNumEvseId(); + for (unsigned int i = 0; i < numEvseId; i++) { + if (!getEvse(i)) { + MO_DBG_ERR("connector init failure"); + return false; + } + } + + return true; +} + +void v201::AvailabilityService::loop() { + for (size_t i = 0; i < numEvseId && evses[i]; i++) { evses[i]->loop(); } } -AvailabilityServiceEvse *AvailabilityService::getEvse(unsigned int evseId) { - if (evseId >= MO_NUM_EVSEID) { - MO_DBG_ERR("invalid arg"); +v201::AvailabilityServiceEvse *v201::AvailabilityService::getEvse(unsigned int evseId) { + if (evseId >= numEvseId) { + MO_DBG_ERR("evseId out of bound"); return nullptr; } + + if (!evses[evseId]) { + evses[evseId] = new AvailabilityServiceEvse(context, *this, evseId); + if (!evses[evseId]) { + MO_DBG_ERR("OOM"); + return nullptr; + } + } + return evses[evseId]; } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Availability/AvailabilityService.h b/src/MicroOcpp/Model/Availability/AvailabilityService.h index d8b74d61..2c35c7e3 100644 --- a/src/MicroOcpp/Model/Availability/AvailabilityService.h +++ b/src/MicroOcpp/Model/Availability/AvailabilityService.h @@ -11,16 +11,122 @@ #ifndef MO_AVAILABILITYSERVICE_H #define MO_AVAILABILITYSERVICE_H +#include +#include +#include +#include +#include +#include #include -#if MO_ENABLE_V201 +#if MO_ENABLE_V16 -#include +namespace MicroOcpp { -#include -#include -#include -#include +class Context; +class Clock; +class Connection; + +namespace v16 { + +class Model; +class Configuration; +class ConfigurationContainer; +class AvailabilityService; +class TransactionServiceEvse; + +class AvailabilityServiceEvse : public MemoryManaged { +private: + Context& context; + Clock& clock; + Model& model; + AvailabilityService& availService; + const unsigned int evseId; + + Connection *connection = nullptr; + TransactionServiceEvse *txServiceEvse = nullptr; + + bool (*connectorPluggedInput)(unsigned int evseId, void *userData) = nullptr; + void *connectorPluggedInputUserData = nullptr; + bool (*evReadyInput)(unsigned int evseId, void *userData) = nullptr; + void *evReadyInputUserData = nullptr; + bool (*evseReadyInput)(unsigned int evseId, void *userData) = nullptr; + void *evseReadyInputUserData = nullptr; + bool (*occupiedInput)(unsigned int evseId, void *userData) = nullptr; //instead of Available, go into Preparing / Finishing state + void *occupiedInputUserData = nullptr; + + Configuration *availabilityBool = nullptr; + char availabilityBoolKey [sizeof(MO_CONFIG_EXT_PREFIX "AVAIL_CONN_xxxx") + 1]; + bool availabilityVolatile = true; + + ConfigurationContainer *availabilityContainer = nullptr; + + Vector errorDataInputs; + Vector trackErrorDataInputs; + int reportedErrorIndex = -1; //last reported error + const char *getErrorCode(); + + MO_ChargePointStatus currentStatus = MO_ChargePointStatus_UNDEFINED; + MO_ChargePointStatus reportedStatus = MO_ChargePointStatus_UNDEFINED; + Timestamp t_statusTransition; + + bool trackLoopExecute = false; //if loop has been executed once + +public: + AvailabilityServiceEvse(Context& context, AvailabilityService& availService, unsigned int evseId); + ~AvailabilityServiceEvse(); + + void setConnectorPluggedInput(bool (*connectorPlugged)(unsigned int, void*), void *userData); + void setEvReadyInput(bool (*evReady)(unsigned int, void*), void *userData); + void setEvseReadyInput(bool (*evseReady)(unsigned int, void*), void *userData); + void setOccupiedInput(bool (*occupied)(unsigned int, void*), void *userData); + + bool addErrorDataInput(MO_ErrorDataInput errorDataInput); + + void loop(); + + bool setup(); + + MO_ChargePointStatus getStatus(); + + void setAvailability(bool available); + void setAvailabilityVolatile(bool available); //set inoperative state but keep only until reboot at most + + ChangeAvailabilityStatus changeAvailability(bool operative); + + Operation *createTriggeredStatusNotification(); + + bool isOperative(); + bool isFaulted(); +}; + +class AvailabilityService : public MemoryManaged { +private: + Context& context; + + AvailabilityServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; + unsigned int numEvseId = MO_NUM_EVSEID; + + Configuration *minimumStatusDurationInt = nullptr; //in seconds + +public: + AvailabilityService(Context& context); + ~AvailabilityService(); + + bool setup(); + + void loop(); + + AvailabilityServiceEvse *getEvse(unsigned int evseId); + +friend class AvailabilityServiceEvse; +}; + +} //namespace MicroOcpp +} //namespace v16 +#endif //MO_ENABLE_V16 + +#if MO_ENABLE_V201 #ifndef MO_INOPERATIVE_REQUESTERS_MAX #define MO_INOPERATIVE_REQUESTERS_MAX 3 @@ -33,40 +139,55 @@ namespace MicroOcpp { class Context; + +namespace v201 { + class AvailabilityService; +class Variable; + +struct FaultedInput { + bool (*isFaulted)(unsigned int connectorId, void *userData) = nullptr; + void *userData = nullptr; +}; class AvailabilityServiceEvse : public MemoryManaged { private: Context& context; - AvailabilityService& availabilityService; + AvailabilityService& availService; const unsigned int evseId; - std::function connectorPluggedInput; - std::function occupiedInput; //instead of Available, go into Occupied + bool (*connectorPluggedInput)(unsigned int evseId, void *userData) = nullptr; + void *connectorPluggedInputUserData = nullptr; + bool (*occupiedInput)(unsigned int evseId, void *userData) = nullptr; //instead of Available, go into Occupied + void *occupiedInputUserData = nullptr; void *unavailableRequesters [MO_INOPERATIVE_REQUESTERS_MAX] = {nullptr}; - void *faultedRequesters [MO_FAULTED_REQUESTERS_MAX] = {nullptr}; + Vector faultedInputs; - ChargePointStatus reportedStatus = ChargePointStatus_UNDEFINED; + MO_ChargePointStatus reportedStatus = MO_ChargePointStatus_UNDEFINED; + + bool trackLoopExecute = false; //if loop has been executed once public: - AvailabilityServiceEvse(Context& context, AvailabilityService& availabilityService, unsigned int evseId); + AvailabilityServiceEvse(Context& context, AvailabilityService& availService, unsigned int evseId); - void loop(); + void setConnectorPluggedInput(bool (*connectorPlugged)(unsigned int, void*), void *userData); + void setOccupiedInput(bool (*occupied)(unsigned int, void*), void *userData); - void setConnectorPluggedInput(std::function connectorPluggedInput); - void setOccupiedInput(std::function occupiedInput); + bool addFaultedInput(FaultedInput faultedInput); + + void loop(); - ChargePointStatus getStatus(); + MO_ChargePointStatus getStatus(); void setUnavailable(void *requesterId); void setAvailable(void *requesterId); ChangeAvailabilityStatus changeAvailability(bool operative); - void setFaulted(void *requesterId); - void resetFaulted(void *requesterId); + Operation *createTriggeredStatusNotification(); bool isAvailable(); + bool isOperative(); bool isFaulted(); }; @@ -75,18 +196,22 @@ class AvailabilityService : public MemoryManaged { Context& context; AvailabilityServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; + unsigned int numEvseId = MO_NUM_EVSEID; + + Variable *offlineThreshold = nullptr; public: - AvailabilityService(Context& context, size_t numEvses); + AvailabilityService(Context& context); ~AvailabilityService(); + bool setup(); + void loop(); AvailabilityServiceEvse *getEvse(unsigned int evseId); }; -} // namespace MicroOcpp - -#endif // MO_ENABLE_V201 - +} //namespace MicroOcpp +} //namespace v201 +#endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Availability/ChangeAvailabilityStatus.h b/src/MicroOcpp/Model/Availability/ChangeAvailabilityStatus.h deleted file mode 100644 index 113a3768..00000000 --- a/src/MicroOcpp/Model/Availability/ChangeAvailabilityStatus.h +++ /dev/null @@ -1,25 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/Boot/BootNotificationData.h b/src/MicroOcpp/Model/Boot/BootNotificationData.h new file mode 100644 index 00000000..fff15184 --- /dev/null +++ b/src/MicroOcpp/Model/Boot/BootNotificationData.h @@ -0,0 +1,52 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#ifndef MO_BOOTNOTIFICATIONDATA_H +#define MO_BOOTNOTIFICATIONDATA_H + +#include + +#if MO_ENABLE_V16 || MO_ENABLE_V201 + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Data to be sent in BootNotification. + * + * Usage: + * ``` + * // init + * MO_BootNotificationData bnData; + * mo_bootNotificationData_init(&bnData); + * + * // set payload + * bnData.chargePointVendor = "My Company Ltd."; + * bnData.chargePointModel = "Demo Charger"; + * + * // pass data to MO + * mo_setBootNotificationData2(bnData); //copies data, i.e. bnData can be invalidated now + */ + +typedef struct { + const char *chargePointModel; + const char *chargePointVendor; + const char *firmwareVersion; + const char *chargePointSerialNumber; + const char *meterSerialNumber; //OCPP 1.6 only + const char *meterType; //OCPP 1.6 only + const char *chargeBoxSerialNumber; //OCPP 1.6 only + const char *iccid; + const char *imsi; +} MO_BootNotificationData; + +void mo_bootNotificationData_init(MO_BootNotificationData *bnData); + +#ifdef __cplusplus +} //extern "C" +#endif + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 +#endif diff --git a/src/MicroOcpp/Model/Boot/BootService.cpp b/src/MicroOcpp/Model/Boot/BootService.cpp index 1aabb200..f29bf47e 100644 --- a/src/MicroOcpp/Model/Boot/BootService.cpp +++ b/src/MicroOcpp/Model/Boot/BootService.cpp @@ -1,21 +1,31 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License +#include + #include -#include -#include +#include #include -#include +#include +#include +#include #include #include +#include #include #include #include +#if MO_ENABLE_V16 || MO_ENABLE_V201 + using namespace MicroOcpp; +void mo_bootNotificationData_init(MO_BootNotificationData *bnData) { + memset(bnData, 0, sizeof(*bnData)); +} + unsigned int PreBootQueue::getFrontRequestOpNr() { if (!activatedPostBootCommunication) { return 0; @@ -41,39 +51,131 @@ RegistrationStatus MicroOcpp::deserializeRegistrationStatus(const char *serializ } } -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"); +BootService::BootService(Context& context) : MemoryManaged("v16/v201.Boot.BootService"), context(context) { + mo_bootNotificationData_init(&bnData); +} + +BootService::~BootService() { + MO_FREE(bnDataBuf); + bnDataBuf = nullptr; +} + +bool BootService::setup() { + + context.getMessageService().setPreBootSendQueue(&preBootQueue); //register PreBootQueue in RequestQueue module + + filesystem = context.getFilesystem(); + + ocppVersion = context.getOcppVersion(); + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("initialization error"); + return false; + } + + //if transactions can start before the BootNotification succeeds + preBootTransactionsBoolV16 = configService->declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", false); + if (!preBootTransactionsBoolV16) { + MO_DBG_ERR("initialization error"); + return false; + } + + RemoteControlService *rcService = context.getModel16().getRemoteControlService(); + if (!rcService) { + MO_DBG_ERR("initialization error"); + return false; + } + + rcService->addTriggerMessageHandler("BootNotification", [] (Context& context) -> Operation* { + auto& model = context.getModel16(); + auto& bootSvc = *model.getBootService(); + return new BootNotification(context, bootSvc, model.getHeartbeatService(), bootSvc.getBootNotificationData(), nullptr); + }); } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + + auto varService = context.getModel201().getVariableService(); + if (!varService) { + MO_DBG_ERR("initialization error"); + return false; + } - //Register message handler for TriggerMessage operation - context.getOperationRegistry().registerOperation("BootNotification", [this] () { - return new Ocpp16::BootNotification(this->context.getModel(), getChargePointCredentials());}); + //if transactions can start before the BootNotification succeeds + preBootTransactionsBoolV201 = varService->declareVariable("CustomizationCtrlr", "PreBootTransactions", false); + if (!preBootTransactionsBoolV201) { + MO_DBG_ERR("initialization error"); + return false; + } + + RemoteControlService *rcService = context.getModel201().getRemoteControlService(); + if (!rcService) { + MO_DBG_ERR("initialization error"); + return false; + } + + rcService->addTriggerMessageHandler("BootNotification", [] (Context& context, int evseId) -> TriggerMessageStatus { + (void)evseId; + auto& model = context.getModel201(); + auto& bootSvc = *model.getBootService(); + + if (bootSvc.getRegistrationStatus() == RegistrationStatus::Accepted) { + //F06.FR.17: if previous BootNotification was accepted, don't allow triggering a new one + return TriggerMessageStatus::Rejected; + } else { + auto operation = new BootNotification(context, bootSvc, model.getHeartbeatService(), bootSvc.getBootNotificationData(), "Triggered"); + if (!operation) { + MO_DBG_ERR("OOM"); + return TriggerMessageStatus::ERR_INTERNAL; + } + auto request = makeRequest(context, operation); + if (!request) { + MO_DBG_ERR("OOM"); + return TriggerMessageStatus::ERR_INTERNAL; + } + if (!context.getMessageService().sendRequestPreBoot(std::move(request))) { + MO_DBG_ERR("OOM"); + return TriggerMessageStatus::ERR_INTERNAL; + } + return TriggerMessageStatus::Accepted; + } + }); + } + #endif //MO_ENABLE_V201 + + #if MO_ENABLE_MOCK_SERVER + context.getMessageService().registerOperation("BootNotification", nullptr, BootNotification::writeMockConf, nullptr, reinterpret_cast(&context)); + #endif //MO_ENABLE_MOCK_SERVER + + return true; } void BootService::loop() { + auto& clock = context.getClock(); + if (!executedFirstTime) { executedFirstTime = true; - firstExecutionTimestamp = mocpp_tick_ms(); + firstExecutionTimestamp = clock.getUptime(); } - if (!executedLongTime && mocpp_tick_ms() - firstExecutionTimestamp >= MO_BOOTSTATS_LONGTIME_MS) { + int32_t dtFirstExecution; + if (!clock.delta(clock.getUptime(), firstExecutionTimestamp, dtFirstExecution)) { + dtFirstExecution = 0; + } + + if (!executedLongTime && dtFirstExecution >= 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); + if (filesystem) { + PersistencyUtils::setBootSuccess(filesystem); + } } preBootQueue.loop(); @@ -83,16 +185,33 @@ void BootService::loop() { activatedPostBootCommunication = true; } - if (!activatedModel && (status == RegistrationStatus::Accepted || preBootTransactionsBool->getBool())) { - context.getModel().activateTasks(); + bool preBootTransactions = false; + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + preBootTransactions = preBootTransactionsBoolV16->getBool(); + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + preBootTransactions = preBootTransactionsBoolV201->getBool(); + } + #endif //MO_ENABLE_V201 + + if (!activatedModel && (status == RegistrationStatus::Accepted || preBootTransactions)) { + context.getModelCommon().activateTasks(); activatedModel = true; } if (status == RegistrationStatus::Accepted) { return; } - - if (mocpp_tick_ms() - lastBootNotification < (interval_s * 1000UL)) { + + int32_t dtLastBootNotification; + if (!clock.delta(clock.getUptime(), lastBootNotification, dtLastBootNotification)) { + dtLastBootNotification = 0; + } + + if (lastBootNotification.isDefined() && dtLastBootNotification < interval_s) { return; } @@ -100,166 +219,103 @@ void BootService::loop() { * 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)); + auto bootNotification = makeRequest(context, new BootNotification(context, *this, context.getModelCommon().getHeartbeatService(), getBootNotificationData(), nullptr)); + bootNotification->setTimeout(interval_s); + context.getMessageService().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 = "{}"; - } + lastBootNotification = clock.getUptime(); } -void BootService::setChargePointCredentials(const char *credentials) { - cpCredentials = credentials; - if (cpCredentials.size() < 2) { - cpCredentials = "{}"; - } +namespace MicroOcpp { +size_t measureDataEntry(const char *bnDataEntry) { + return bnDataEntry ? strlen(bnDataEntry) + 1 : 0; } -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; +bool setDataEntry(char *bnDataBuf, size_t bufsize, size_t& written, const char **bnDataEntryDest, const char *bnDataEntrySrc) { + if (!bnDataEntrySrc) { + bnDataEntryDest = nullptr; + return true; } - - if (!err) { - return doc; - } else { - MO_DBG_ERR("could not parse stored credentials: %s", err.c_str()); - return nullptr; + *bnDataEntryDest = bnDataBuf + written; + int ret = snprintf(bnDataBuf + written, bufsize - written, "%s", bnDataEntrySrc); + if (ret < 0 || (size_t)ret >= bufsize - written) { + return false; } + written += (size_t)ret + 1; + return true; } - -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) { +bool BootService::setBootNotificationData(MO_BootNotificationData bnData) { + size_t bufsize = 0; + bufsize += measureDataEntry(bnData.chargePointModel); + bufsize += measureDataEntry(bnData.chargePointVendor); + bufsize += measureDataEntry(bnData.firmwareVersion); + bufsize += measureDataEntry(bnData.chargePointSerialNumber); + bufsize += measureDataEntry(bnData.meterSerialNumber); + bufsize += measureDataEntry(bnData.meterType); + bufsize += measureDataEntry(bnData.chargeBoxSerialNumber); + bufsize += measureDataEntry(bnData.iccid); + bufsize += measureDataEntry(bnData.imsi); + + bnDataBuf = static_cast(MO_MALLOC(getMemoryTag(), bufsize)); + if (!bnDataBuf) { + MO_DBG_ERR("OOM"); return false; } - size_t msize = 0; - if (filesystem->stat(MO_FILENAME_PREFIX "bootstats.jsn", &msize) == 0) { - - bool success = true; + memset(&this->bnData, 0, sizeof(this->bnData)); - 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; + size_t written = 0; + bool success = true; + if (success) {success &= setDataEntry(bnDataBuf, bufsize, written, &this->bnData.chargePointModel, bnData.chargePointModel);} + if (success) {success &= setDataEntry(bnDataBuf, bufsize, written, &this->bnData.chargePointVendor, bnData.chargePointVendor);} + if (success) {success &= setDataEntry(bnDataBuf, bufsize, written, &this->bnData.firmwareVersion, bnData.firmwareVersion);} + if (success) {success &= setDataEntry(bnDataBuf, bufsize, written, &this->bnData.chargePointSerialNumber, bnData.chargePointSerialNumber);} + if (success) {success &= setDataEntry(bnDataBuf, bufsize, written, &this->bnData.meterSerialNumber, bnData.meterSerialNumber);} + if (success) {success &= setDataEntry(bnDataBuf, bufsize, written, &this->bnData.meterType, bnData.meterType);} + if (success) {success &= setDataEntry(bnDataBuf, bufsize, written, &this->bnData.chargeBoxSerialNumber, bnData.chargeBoxSerialNumber);} + if (success) {success &= setDataEntry(bnDataBuf, bufsize, written, &this->bnData.iccid, bnData.iccid);} + if (success) {success &= setDataEntry(bnDataBuf, bufsize, written, &this->bnData.imsi, bnData.imsi);} + + if (!success) { + MO_DBG_ERR("failure to set BootNotification data"); + goto fail; } -} -bool BootService::storeBootStats(std::shared_ptr filesystem, BootStats& bstats) { - if (!filesystem) { - return false; + if (written != bufsize) { + MO_DBG_ERR("internal error"); + goto fail; } - 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); + return true; +fail: + MO_FREE(bnDataBuf); + memset(&this->bnData, 0, sizeof(this->bnData)); + return false; } -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; +const MO_BootNotificationData& BootService::getBootNotificationData() { + return bnData; } -bool BootService::migrate(std::shared_ptr filesystem, BootStats& bstats) { - if (!filesystem) { - return false; - } - - bool success = true; +void BootService::notifyRegistrationStatus(RegistrationStatus status) { + this->status = status; + lastBootNotification = context.getClock().getUptime(); +} - 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"); - }); +RegistrationStatus BootService::getRegistrationStatus() { + return status; +} - snprintf(bstats.microOcppVersion, sizeof(bstats.microOcppVersion), "%s", MO_VERSION); - MO_DBG_DEBUG("clear local state files (migration): %s", success ? "success" : "not completed"); +void BootService::setRetryInterval(int interval_s) { + if (interval_s <= 0) { + this->interval_s = MO_BOOT_INTERVAL_DEFAULT; + } else { + this->interval_s = interval_s; + this->interval_s += 1; //TC_B_03_CS expects retry after at least interval_s secs and doesn't tolerate network jitter } - return success; + lastBootNotification = context.getClock().getUptime(); } + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Boot/BootService.h b/src/MicroOcpp/Model/Boot/BootService.h index 6091dc2c..cdc02027 100644 --- a/src/MicroOcpp/Model/Boot/BootService.h +++ b/src/MicroOcpp/Model/Boot/BootService.h @@ -1,16 +1,20 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_BOOTSERVICE_H #define MO_BOOTSERVICE_H -#include +#include #include #include #include +#include +#include #include +#if MO_ENABLE_V16 || MO_ENABLE_V201 + #define MO_BOOT_INTERVAL_DEFAULT 60 #ifndef MO_BOOTSTATS_LONGTIME_MS @@ -19,19 +23,6 @@ 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, @@ -45,56 +36,73 @@ 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 - + unsigned int getFrontRequestOpNr() override; //override FrontRequestOpNr behavior: in PreBoot mode, always return 0 to avoid other RequestQueues from sending msgs + void activatePostBootCommunication(); //end PreBoot mode, now send Requests normally }; class Context; +#if MO_ENABLE_V16 +namespace v16 { +class Configuration; +} +#endif +#if MO_ENABLE_V201 +namespace v201 { +class Variable; +} +#endif + class BootService : public MemoryManaged { private: Context& context; - std::shared_ptr filesystem; + MO_FilesystemAdapter *filesystem = nullptr;; PreBootQueue preBootQueue; - unsigned long interval_s = MO_BOOT_INTERVAL_DEFAULT; - unsigned long lastBootNotification = -1UL / 2; + int32_t interval_s = MO_BOOT_INTERVAL_DEFAULT; + Timestamp lastBootNotification; RegistrationStatus status = RegistrationStatus::Pending; - - String cpCredentials; - std::shared_ptr preBootTransactionsBool; + MO_BootNotificationData bnData; + char *bnDataBuf = nullptr; + + int ocppVersion = -1; + + #if MO_ENABLE_V16 + v16::Configuration *preBootTransactionsBoolV16 = nullptr; + #endif + #if MO_ENABLE_V201 + v201::Variable *preBootTransactionsBoolV201 = nullptr; + #endif bool activatedModel = false; bool activatedPostBootCommunication = false; - unsigned long firstExecutionTimestamp = 0; + Timestamp firstExecutionTimestamp; bool executedFirstTime = false; bool executedLongTime = false; public: - BootService(Context& context, std::shared_ptr filesystem); + BootService(Context& context); + ~BootService(); + + bool setup(); void loop(); - void setChargePointCredentials(JsonObject credentials); - void setChargePointCredentials(const char *credentials); //credentials: serialized BootNotification payload - std::unique_ptr getChargePointCredentials(); + bool setBootNotificationData(MO_BootNotificationData bnData); + const MO_BootNotificationData& getBootNotificationData(); 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 + RegistrationStatus getRegistrationStatus(); - static bool migrate(std::shared_ptr filesystem, BootStats& bstats); //migrate persistent storage if running on a new MO version + void setRetryInterval(int interval); }; } +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Certificates/Certificate.cpp b/src/MicroOcpp/Model/Certificates/Certificate.cpp index 1de32094..160f646b 100644 --- a/src/MicroOcpp/Model/Certificates/Certificate.cpp +++ b/src/MicroOcpp/Model/Certificates/Certificate.cpp @@ -4,19 +4,19 @@ #include -#if MO_ENABLE_CERT_MGMT +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT #include #include -bool ocpp_cert_equals(const ocpp_cert_hash *h1, const ocpp_cert_hash *h2) { +bool mo_cert_equals(const mo_cert_hash *h1, const mo_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) { +int mo_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; } @@ -37,15 +37,15 @@ int ocpp_cert_bytes_to_hex(char *dst, size_t dst_size, const unsigned char *src, 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 mo_cert_print_issuerNameHash(const mo_cert_hash *src, char *buf, size_t size) { + return mo_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 mo_cert_print_issuerKeyHash(const mo_cert_hash *src, char *buf, size_t size) { + return mo_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) { +int mo_cert_print_serialNumber(const mo_cert_hash *src, char *buf, size_t size) { if (!buf || !size) { return -1; @@ -63,7 +63,7 @@ int ocpp_cert_print_serialNumber(const ocpp_cert_hash *src, char *buf, size_t si } 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); + auto ret = mo_cert_bytes_to_hex(buf + (size_t)hexLen, size - (size_t)hexLen, src->serialNumber + 1, src->serialNumberLen - 1); if (ret < 0) { return -1; } @@ -73,7 +73,7 @@ int ocpp_cert_print_serialNumber(const ocpp_cert_hash *src, char *buf, size_t si return hexLen; } -int ocpp_cert_hex_to_bytes(unsigned char *dst, size_t dst_size, const char *hex_src) { +int mo_cert_hex_to_bytes(unsigned char *dst, size_t dst_size, const char *hex_src) { if (!dst || !dst_size || !hex_src) { return -1; } @@ -124,8 +124,8 @@ int ocpp_cert_hex_to_bytes(unsigned char *dst, size_t dst_size, const char *hex_ 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); +int mo_cert_set_issuerNameHash(mo_cert_hash *dst, const char *hex_src, HashAlgorithmType hash_algorithm) { + auto ret = mo_cert_hex_to_bytes(dst->issuerNameHash, sizeof(dst->issuerNameHash), hex_src); if (ret < 0) { return ret; @@ -138,8 +138,8 @@ int ocpp_cert_set_issuerNameHash(ocpp_cert_hash *dst, const char *hex_src, HashA 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); +int mo_cert_set_issuerKeyHash(mo_cert_hash *dst, const char *hex_src, HashAlgorithmType hash_algorithm) { + auto ret = mo_cert_hex_to_bytes(dst->issuerKeyHash, sizeof(dst->issuerNameHash), hex_src); if (ret < 0) { return ret; @@ -152,8 +152,8 @@ int ocpp_cert_set_issuerKeyHash(ocpp_cert_hash *dst, const char *hex_src, HashAl 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); +int mo_cert_set_serialNumber(mo_cert_hash *dst, const char *hex_src) { + auto ret = mo_cert_hex_to_bytes(dst->serialNumber, sizeof(dst->serialNumber), hex_src); if (ret < 0) { return ret; @@ -164,4 +164,4 @@ int ocpp_cert_set_serialNumber(ocpp_cert_hash *dst, const char *hex_src) { return ret; } -#endif //MO_ENABLE_CERT_MGMT +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT diff --git a/src/MicroOcpp/Model/Certificates/Certificate.h b/src/MicroOcpp/Model/Certificates/Certificate.h index 33574351..d39596dc 100644 --- a/src/MicroOcpp/Model/Certificates/Certificate.h +++ b/src/MicroOcpp/Model/Certificates/Certificate.h @@ -7,7 +7,7 @@ #include -#if MO_ENABLE_CERT_MGMT +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT #include @@ -83,47 +83,47 @@ typedef enum HashAlgorithmType { alg == HashAlgorithmType_SHA384 ? 48 : \ alg == HashAlgorithmType_SHA512 ? 64 : 0) -typedef struct ocpp_cert_hash { +typedef struct mo_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; +} mo_cert_hash; -bool ocpp_cert_equals(const ocpp_cert_hash *h1, const ocpp_cert_hash *h2); +bool mo_cert_equals(const mo_cert_hash *h1, const mo_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 + * Print the issuerNameHash of mo_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); +int mo_cert_print_issuerNameHash(const mo_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 + * Print the issuerKeyHash of mo_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); +int mo_cert_print_issuerKeyHash(const mo_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 + * Print the serialNumber of mo_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 mo_cert_print_serialNumber(const mo_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 mo_cert_set_issuerNameHash(mo_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 mo_cert_set_issuerKeyHash(mo_cert_hash *dst, const char *hex_src, HashAlgorithmType hash_algorithm); -int ocpp_cert_set_serialNumber(ocpp_cert_hash *dst, const char *hex_src); +int mo_cert_set_serialNumber(mo_cert_hash *dst, const char *hex_src); #ifdef __cplusplus } //extern "C" @@ -132,7 +132,7 @@ int ocpp_cert_set_serialNumber(ocpp_cert_hash *dst, const char *hex_src); namespace MicroOcpp { -using CertificateHash = ocpp_cert_hash; +using CertificateHash = mo_cert_hash; /* * See OCPP 2.0.1 part 2 Data Type 2.5 @@ -160,5 +160,5 @@ class CertificateStore { } //namespace MicroOcpp #endif //__cplusplus -#endif //MO_ENABLE_CERT_MGMT +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT #endif diff --git a/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.cpp b/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.cpp index 73314158..f55993e7 100644 --- a/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.cpp +++ b/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.cpp @@ -4,7 +4,7 @@ #include -#if MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS #include @@ -13,9 +13,10 @@ #include #include +#include #include -bool ocpp_get_cert_hash(mbedtls_x509_crt& cacert, HashAlgorithmType hashAlg, ocpp_cert_hash *out) { +bool mo_get_cert_hash(mbedtls_x509_crt& cacert, HashAlgorithmType hashAlg, mo_cert_hash *out) { if (cacert.next) { MO_DBG_ERR("only sole root certs supported"); @@ -112,7 +113,7 @@ bool ocpp_get_cert_hash(mbedtls_x509_crt& cacert, HashAlgorithmType hashAlg, ocp return true; } -bool ocpp_get_cert_hash(const unsigned char *buf, size_t len, HashAlgorithmType hashAlg, ocpp_cert_hash *out) { +bool mo_get_cert_hash(const unsigned char *buf, size_t len, HashAlgorithmType hashAlg, mo_cert_hash *out) { mbedtls_x509_crt cacert; mbedtls_x509_crt_init(&cacert); @@ -121,7 +122,7 @@ bool ocpp_get_cert_hash(const unsigned char *buf, size_t len, HashAlgorithmType int ret; if((ret = mbedtls_x509_crt_parse(&cacert, buf, len + 1)) >= 0) { - success = ocpp_get_cert_hash(cacert, hashAlg, out); + success = mo_get_cert_hash(cacert, hashAlg, out); } else { char err [100]; mbedtls_strerror(ret, err, 100); @@ -136,55 +137,62 @@ namespace MicroOcpp { class CertificateStoreMbedTLS : public CertificateStore, public MemoryManaged { private: - std::shared_ptr filesystem; + MO_FilesystemAdapter *filesystem = nullptr; + + bool getCertHash(const char *path, HashAlgorithmType hashAlg, CertificateHash& out) { + + MO_File *file = nullptr; + unsigned char *buf = nullptr; + bool success = false; - 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 (filesystem->stat(path, &fsize) != 0) { + MO_DBG_ERR("certificate does not exist: %s", path); + goto exit; } if (fsize >= MO_MAX_CERT_SIZE) { - MO_DBG_ERR("cert file exceeds limit: %s, %zuB", fn, fsize); - return false; + MO_DBG_ERR("cert file exceeds limit: %s, %zuB", path, fsize); + goto exit; } - auto file = filesystem->open(fn, "r"); + file = filesystem->open(path, "r"); if (!file) { - MO_DBG_ERR("could not open file: %s", fn); - return false; + MO_DBG_ERR("could not open file: %s", path); + goto exit; } - unsigned char *buf = static_cast(MO_MALLOC(getMemoryTag(), fsize + 1)); + buf = static_cast(MO_MALLOC(getMemoryTag(), fsize + 1)); if (!buf) { MO_DBG_ERR("OOM"); - return false; + goto exit; } - bool success = true; - size_t ret; - if ((ret = file->read((char*) buf, fsize)) != fsize) { + if ((ret = filesystem->read(file, (char*) buf, fsize)) != fsize) { MO_DBG_ERR("read error: %zu (expect %zu)", ret, fsize); - success = false; + goto exit; } buf[fsize] = '\0'; - if (success) { - success &= ocpp_get_cert_hash(buf, fsize, hashAlg, &out); + if (!mo_get_cert_hash(buf, fsize, hashAlg, &out)) { + MO_DBG_ERR("could not read cert: %s", path); + goto exit; } - if (!success) { - MO_DBG_ERR("could not read cert: %s", fn); - } + //success + success = true; + exit: + if (file) { + (void)filesystem->close(file); //read-only + } MO_FREE(buf); return success; } public: - CertificateStoreMbedTLS(std::shared_ptr filesystem) + CertificateStoreMbedTLS(MO_FilesystemAdapter *filesystem) : MemoryManaged("v201.Certificates.CertificateStoreMbedTLS"), filesystem(filesystem) { } @@ -210,16 +218,16 @@ class CertificateStoreMbedTLS : public CertificateStore, public MemoryManaged { 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)) { + for (unsigned int i = 0; i < MO_CERT_STORE_SIZE; i++) { + char path [MO_MAX_PATH_SIZE]; + if (!printCertPath(filesystem, path, sizeof(path), certTypeFnStr, i)) { MO_DBG_ERR("internal error"); out.clear(); break; } size_t msize; - if (filesystem->stat(fn, &msize) != 0) { + if (filesystem->stat(path, &msize) != 0) { continue; //no cert installed at this slot } @@ -228,8 +236,8 @@ class CertificateStoreMbedTLS : public CertificateStore, public MemoryManaged { rootCert.certificateType = certType; - if (!getCertHash(fn, HashAlgorithmType_SHA256, rootCert.certificateHashData)) { - MO_DBG_ERR("could not create hash: %s", fn); + if (!getCertHash(path, HashAlgorithmType_SHA256, rootCert.certificateHashData)) { + MO_DBG_ERR("could not create hash: %s", path); out.pop_back(); continue; } @@ -246,31 +254,31 @@ class CertificateStoreMbedTLS : public CertificateStore, public MemoryManaged { //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++) { + for (unsigned int i = 0; i < MO_CERT_STORE_SIZE; i++) { - char fn [MO_MAX_PATH_SIZE] = {'\0'}; //cert fn on flash storage + char path [MO_MAX_PATH_SIZE]; //cert path on flash storage - if (!printCertFn(certTypeFnStr, i, fn, MO_MAX_PATH_SIZE)) { + if (!printCertPath(filesystem, path, sizeof(path), certTypeFnStr, i)) { MO_DBG_ERR("internal error"); return DeleteCertificateStatus_Failed; } size_t msize; - if (filesystem->stat(fn, &msize) != 0) { + if (filesystem->stat(path, &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); + if (!getCertHash(path, hash.hashAlgorithm, probe)) { + MO_DBG_ERR("could not create hash: %s", path); err = true; continue; } - if (ocpp_cert_equals(&probe, &hash)) { + if (mo_cert_equals(&probe, &hash)) { //found, delete - bool success = filesystem->remove(fn); + bool success = filesystem->remove(path); return success ? DeleteCertificateStatus_Accepted : DeleteCertificateStatus_Failed; @@ -302,7 +310,7 @@ class CertificateStoreMbedTLS : public CertificateStore, public MemoryManaged { //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)) { + if (!mo_get_cert_hash((const unsigned char*)certificate, strlen(certificate), HashAlgorithmType_SHA256, &certId)) { MO_DBG_ERR("unable to parse cert"); return InstallCertificateStatus_Rejected; } @@ -313,28 +321,28 @@ class CertificateStoreMbedTLS : public CertificateStore, public MemoryManaged { 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_cert_print_issuerNameHash(&certId, buf, sizeof(buf)); MO_DBG_DEBUG("issuerNameHash: %s", buf); - ocpp_cert_print_issuerKeyHash(&certId, buf, sizeof(buf)); + mo_cert_print_issuerKeyHash(&certId, buf, sizeof(buf)); MO_DBG_DEBUG("issuerKeyHash: %s", buf); - ocpp_cert_print_serialNumber(&certId, buf, sizeof(buf)); + mo_cert_print_serialNumber(&certId, buf, sizeof(buf)); MO_DBG_DEBUG("serialNumber: %s", buf); } -#endif // MO_DBG_LEVEL >= MO_DL_DEBUG +#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)) { + if (mo_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)) { + if (mo_cert_equals(&installedChild, &certId)) { MO_DBG_INFO("certificate already installed"); return InstallCertificateStatus_Accepted; } @@ -342,51 +350,58 @@ class CertificateStoreMbedTLS : public CertificateStore, public MemoryManaged { } } - char fn [MO_MAX_PATH_SIZE] = {'\0'}; //cert fn on flash storage + char path [MO_MAX_PATH_SIZE] = {'\0'}; //cert path 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"); + for (unsigned int i = 0; i < MO_CERT_STORE_SIZE; i++) { + if (!printCertPath(filesystem, path, sizeof(path), certTypeFnStr, i)) { + MO_DBG_ERR("invalid cert path"); return InstallCertificateStatus_Failed; } size_t msize; - if (filesystem->stat(fn, &msize) != 0) { - //found free slot; fn contains result + if (filesystem->stat(path, &msize) != 0) { + //found free slot; path contains result break; } else { - //this slot is already occupied; invalidate fn and try next - fn[0] = '\0'; + //this slot is already occupied; invalidate path and try next + path[0] = '\0'; } } - if (fn[0] == '\0') { + if (path[0] == '\0') { MO_DBG_ERR("exceed maximum number of certs; must delete before"); return InstallCertificateStatus_Rejected; } - auto file = filesystem->open(fn, "w"); + auto file = filesystem->open(path, "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); + auto written = filesystem->write(file, certificate, cert_len); if (written < cert_len) { MO_DBG_ERR("file write error"); - file.reset(); - filesystem->remove(fn); + (void)filesystem->close(file); + filesystem->remove(path); return InstallCertificateStatus_Failed; } - MO_DBG_INFO("installed certificate: %s", fn); - return InstallCertificateStatus_Accepted; + bool closeSuccess = filesystem->close(file); + if (closeSuccess) { + MO_DBG_INFO("installed certificate: %s", path); + return InstallCertificateStatus_Accepted; + } else { + MO_DBG_ERR("Error writing file %s", path); + filesystem->remove(path); + return InstallCertificateStatus_Failed; + } } }; -std::unique_ptr makeCertificateStoreMbedTLS(std::shared_ptr filesystem) { +std::unique_ptr makeCertificateStoreMbedTLS(MO_FilesystemAdapter *filesystem) { if (!filesystem) { MO_DBG_WARN("default Certificate Store requires FS"); return nullptr; @@ -394,21 +409,26 @@ std::unique_ptr makeCertificateStoreMbedTLS(std::shared_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) { +bool printCertPath(MO_FilesystemAdapter *filesystem, char *path, size_t size, const char *certType, unsigned int index) { + if (!certType || !*certType || index >= MO_CERT_STORE_SIZE || !path) { 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); + char fname [MO_MAX_PATH_SIZE]; + auto ret = snprintf(fname, sizeof(fname), "%s-%.*u" MO_CERT_FN_SUFFIX, certType, MO_CERT_STORE_DIGITS, index); + if (ret < 0 || (size_t)ret >= sizeof(fname)) { + MO_DBG_ERR("fname error: %i", ret); return false; } - return true; + + auto ret2 = FilesystemUtils::printPath(filesystem, path, size, fname); + if (!ret2) { + MO_DBG_ERR("path error: %s", fname); + } + return ret2; } } //namespace MicroOcpp -#endif //MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS diff --git a/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.h b/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.h index e60a2734..80c30cbe 100644 --- a/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.h +++ b/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.h @@ -16,7 +16,7 @@ #define MO_ENABLE_CERT_STORE_MBEDTLS MO_ENABLE_MBEDTLS #endif -#if MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && 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++ @@ -27,7 +27,7 @@ extern "C" { #endif -bool ocpp_get_cert_hash(const unsigned char *cert, size_t len, enum HashAlgorithmType hashAlg, ocpp_cert_hash *out); +bool mo_get_cert_hash(const unsigned char *cert, size_t len, enum HashAlgorithmType hashAlg, mo_cert_hash *out); #ifdef __cplusplus } //extern "C" @@ -49,22 +49,26 @@ bool ocpp_get_cert_hash(const unsigned char *cert, size_t len, enum HashAlgorith #endif #ifndef MO_CERT_FN_MANUFACTURER_ROOT -#define MO_CERT_FN_MANUFACTURER_ROOT "mfact" +#define MO_CERT_FN_MANUFACTURER_ROOT "mfct" #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 +#ifndef MO_CERT_STORE_DIGITS +#define MO_CERT_STORE_DIGITS 1 //digits needed to print MO_CERT_STORE_SIZE-1 (="3", i.e. 1 digit) +#endif + namespace MicroOcpp { -std::unique_ptr makeCertificateStoreMbedTLS(std::shared_ptr filesystem); +std::unique_ptr makeCertificateStoreMbedTLS(MO_FilesystemAdapter *filesystem); -bool printCertFn(const char *certType, size_t index, char *buf, size_t bufsize); +bool printCertPath(MO_FilesystemAdapter *filesystem, char *path, size_t size, const char *certType, unsigned int index); } //namespace MicroOcpp #endif //def __cplusplus -#endif //MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS #endif diff --git a/src/MicroOcpp/Model/Certificates/CertificateService.cpp b/src/MicroOcpp/Model/Certificates/CertificateService.cpp index 22e5ab6d..c9b30774 100644 --- a/src/MicroOcpp/Model/Certificates/CertificateService.cpp +++ b/src/MicroOcpp/Model/Certificates/CertificateService.cpp @@ -4,32 +4,40 @@ #include -#if MO_ENABLE_CERT_MGMT - -#include +#include #include #include #include +#include + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT 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);}); + : MemoryManaged("v16/v201.Certificates.CertificateService"), context(context) { + } -void CertificateService::setCertificateStore(std::unique_ptr certStore) { - this->certStore = std::move(certStore); +bool CertificateService::setup() { + certStore = context.getCertificateStore(); + if (!certStore) { + MO_DBG_WARN("CertMgmt disabled, because CertificateStore implementation is missing"); + return true; // not a critical failure + } + + context.getMessageService().registerOperation("DeleteCertificate", [] (Context& context) -> Operation* { + return new DeleteCertificate(*context.getModelCommon().getCertificateService());}); + context.getMessageService().registerOperation("GetInstalledCertificateIds", [] (Context& context) -> Operation* { + return new GetInstalledCertificateIds(*context.getModelCommon().getCertificateService());}); + context.getMessageService().registerOperation("InstallCertificate", [] (Context& context) -> Operation* { + return new InstallCertificate(*context.getModelCommon().getCertificateService());}); + + return true; } CertificateStore *CertificateService::getCertificateStore() { - return certStore.get(); + return certStore; } -#endif //MO_ENABLE_CERT_MGMT +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT diff --git a/src/MicroOcpp/Model/Certificates/CertificateService.h b/src/MicroOcpp/Model/Certificates/CertificateService.h index 31d10048..ab09c120 100644 --- a/src/MicroOcpp/Model/Certificates/CertificateService.h +++ b/src/MicroOcpp/Model/Certificates/CertificateService.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License /* @@ -16,7 +16,7 @@ #include -#if MO_ENABLE_CERT_MGMT +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT #include #include @@ -31,15 +31,16 @@ class Context; class CertificateService : public MemoryManaged { private: Context& context; - std::unique_ptr certStore; + CertificateStore *certStore = nullptr; public: CertificateService(Context& context); - void setCertificateStore(std::unique_ptr certStore); + bool setup(); + CertificateStore *getCertificateStore(); }; } -#endif //MO_ENABLE_CERT_MGMT +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT #endif diff --git a/src/MicroOcpp/Model/Certificates/Certificate_c.cpp b/src/MicroOcpp/Model/Certificates/Certificate_c.cpp deleted file mode 100644 index d316140c..00000000 --- a/src/MicroOcpp/Model/Certificates/Certificate_c.cpp +++ /dev/null @@ -1,77 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/Certificates/Certificate_c.h b/src/MicroOcpp/Model/Certificates/Certificate_c.h deleted file mode 100644 index 3b0bc858..00000000 --- a/src/MicroOcpp/Model/Certificates/Certificate_c.h +++ /dev/null @@ -1,54 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/ConnectorBase/EvseId.h b/src/MicroOcpp/Model/Common/EvseId.h similarity index 58% rename from src/MicroOcpp/Model/ConnectorBase/EvseId.h rename to src/MicroOcpp/Model/Common/EvseId.h index 6ca5295a..900d987f 100644 --- a/src/MicroOcpp/Model/ConnectorBase/EvseId.h +++ b/src/MicroOcpp/Model/Common/EvseId.h @@ -7,16 +7,22 @@ #include -#if MO_ENABLE_V201 - -// number of EVSE IDs (including 0). Defaults to MO_NUMCONNECTORS if defined, otherwise to 2 +// number of EVSE IDs (including 0). On a charger with one physical connector, NUM_EVSEID is 2 #ifndef MO_NUM_EVSEID +// Use MO_NUMCONNECTORS if defined, for backwards compatibility #if defined(MO_NUMCONNECTORS) #define MO_NUM_EVSEID MO_NUMCONNECTORS #else #define MO_NUM_EVSEID 2 #endif -#endif // MO_NUM_EVSEID +#endif //MO_NUM_EVSEID + +#ifndef MO_NUM_EVSEID_DIGITS +#define MO_NUM_EVSEID_DIGITS 1 //digits needed to print MO_NUM_EVSEID-1 (="1", i.e. 1 digit) +#endif + +#if MO_ENABLE_V201 +#ifdef __cplusplus namespace MicroOcpp { @@ -31,5 +37,6 @@ struct EvseId { } -#endif // MO_ENABLE_V201 +#endif //__cplusplus +#endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Common/Mutability.h b/src/MicroOcpp/Model/Common/Mutability.h new file mode 100644 index 00000000..e563faf6 --- /dev/null +++ b/src/MicroOcpp/Model/Common/Mutability.h @@ -0,0 +1,41 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_MUTABILITY_H +#define MO_MUTABILITY_H + +#include + +#if MO_ENABLE_V16 || MO_ENABLE_V201 + +#ifdef __cplusplus +extern "C" { +#endif + +//MutabilityEnumType (3.58), shared between Configuration and Variable +typedef enum { + MO_Mutability_ReadWrite, + MO_Mutability_ReadOnly, + MO_Mutability_WriteOnly, + MO_Mutability_None +} MO_Mutability; + +#ifdef __cplusplus +} //extern "C" + +namespace MicroOcpp { + +//MutabilityEnumType (3.58), shared between Configuration and Variable +enum class Mutability : uint8_t { + ReadWrite, + ReadOnly, + WriteOnly, + None +}; + +} //namespace MicroOcpp + +#endif //__cplusplus +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 +#endif //MO_MUTABILITY_H diff --git a/src/MicroOcpp/Model/Configuration/Configuration.cpp b/src/MicroOcpp/Model/Configuration/Configuration.cpp new file mode 100644 index 00000000..41c213a1 --- /dev/null +++ b/src/MicroOcpp/Model/Configuration/Configuration.cpp @@ -0,0 +1,192 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#include + +#include +#include + +#if MO_ENABLE_V16 + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; + +Configuration::~Configuration() { + +} + +void Configuration::setKey(const char *name) { + this->key = name; + updateMemoryTag("v16.Configuration.", name); +} +const char *Configuration::getKey() const { + return key; +} + +void Configuration::setInt(int val) { + MO_DBG_ERR("type err"); +} +void Configuration::setBool(bool val) { + MO_DBG_ERR("type err"); +} +bool Configuration::setString(const char *val) { + MO_DBG_ERR("type err"); + return false; +} + +int Configuration::getInt() { + MO_DBG_ERR("type err"); + return 0; +} +bool Configuration::getBool() { + MO_DBG_ERR("type err"); + return false; +} +const char *Configuration::getString() { + MO_DBG_ERR("type err"); + return ""; +} + +uint16_t Configuration::getWriteCount() { + return 0; +} + +bool Configuration::isRebootRequired() { + return rebootRequired; +} +void Configuration::setRebootRequired() { + rebootRequired = true; +} + +void Configuration::setMutability(Mutability mutability) { + this->mutability = mutability; +} + +Mutability Configuration::getMutability() { + return mutability; +} + +class ConfigurationConcrete : public Configuration { +private: + uint16_t writeCount = 0; + + union { + int valueInt; + bool valueBool; + char *valueString = nullptr; + }; + Type type = Type::Int; + + bool resetString(const char *val) { + if (type != Type::String) { + return false; + } + + char *nextString = nullptr; + + if (val) { + size_t size = strlen(val) + 1; + nextString = static_cast(MO_MALLOC(getMemoryTag(), size)); + if (!nextString) { + MO_DBG_ERR("OOM"); + return false; + } + memset(nextString, 0, size); + auto ret = snprintf(nextString, size, "%s", val); + if (ret < 0 || (size_t)ret >= size) { + MO_DBG_ERR("snprintf: %i", ret); + MO_FREE(nextString); + nextString = nullptr; + return false; + } + } + + MO_FREE(valueString); + valueString = nextString; + return true; + } + +public: + ConfigurationConcrete(Type type) : type(type) { } + ~ConfigurationConcrete() { + (void)resetString(nullptr); + } + + void setInt(int val) override { + #if MO_CONFIG_TYPECHECK + if (type != Type::Int) { + MO_DBG_ERR("type err: %s is not int", getKey()); + return; + } + #endif //MO_CONFIG_TYPECHECK + valueInt = val; + writeCount++; + } + void setBool(bool val) override { + #if MO_CONFIG_TYPECHECK + if (type != Type::Bool) { + MO_DBG_ERR("type err: %s is not bool", getKey()); + return; + } + #endif //MO_CONFIG_TYPECHECK + valueBool = val; + writeCount++; + } + bool setString(const char *val) override { + #if MO_CONFIG_TYPECHECK + if (type != Type::String) { + MO_DBG_ERR("type err: %s is not string", getKey()); + return false; + } + #endif //MO_CONFIG_TYPECHECK + bool success = resetString(val); + if (success) { + writeCount++; + } + return success; + } + + // get Value of Configuration + int getInt() override { + #if MO_CONFIG_TYPECHECK + if (type != Type::Int) { + MO_DBG_ERR("type err: %s is not int", getKey()); + return 0; + } + #endif //MO_CONFIG_TYPECHECK + return valueInt; + } + bool getBool() override { + #if MO_CONFIG_TYPECHECK + if (type != Type::Bool) { + MO_DBG_ERR("type err: %s is not bool", getKey()); + return false; + } + #endif //MO_CONFIG_TYPECHECK + return valueBool; + } + const char *getString() override { + #if MO_CONFIG_TYPECHECK + if (type != Type::String) { + MO_DBG_ERR("type err: %s is not string", getKey()); + return ""; + } + #endif //MO_CONFIG_TYPECHECK + return valueString ? valueString : ""; + } + + Type getType() override { + return type; + } + + uint16_t getWriteCount() override { + return writeCount; + } +}; + +std::unique_ptr MicroOcpp::v16::makeConfiguration(Configuration::Type type) { + return std::unique_ptr(new ConfigurationConcrete(type)); +} + +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Model/Configuration/Configuration.h b/src/MicroOcpp/Model/Configuration/Configuration.h new file mode 100644 index 00000000..86f8c5ac --- /dev/null +++ b/src/MicroOcpp/Model/Configuration/Configuration.h @@ -0,0 +1,82 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#ifndef MO_CONFIGURATION_H +#define MO_CONFIGURATION_H + +#include +#include + +#include +#include +#include + +#if MO_ENABLE_V16 + +#ifndef MO_CONFIG_TYPECHECK +#define MO_CONFIG_TYPECHECK 1 +#endif + +namespace MicroOcpp { + +namespace v16 { + +// ConfigurationStatus (7.22) +enum class ConfigurationStatus : uint8_t { + Accepted, + Rejected, + RebootRequired, + NotSupported +}; + +/* + * Single Configuration Key-value pair + * + * Template method pattern: this is a super-class which has hook-methods for storing and fetching + * the value of the configuration. 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 Configuration : public MemoryManaged { +private: + const char *key = nullptr; + bool rebootRequired = false; + Mutability mutability = Mutability::ReadWrite; +public: + virtual ~Configuration(); + + void setKey(const char *key); //zero-copy + const char *getKey() const; + + // set Value of Configuration + virtual void setInt(int val); + virtual void setBool(bool val); + virtual bool setString(const char *val); + + // get Value of Configuration + virtual int getInt(); + virtual bool getBool(); + virtual const char *getString(); //always returns c-string (empty if undefined) + + enum class Type : uint8_t { + Int, + Bool, + String + }; + virtual Type getType() = 0; + + virtual uint16_t getWriteCount(); //get write count (implement this if MO should persist values on flash) + + void setRebootRequired(); + bool isRebootRequired(); + + void setMutability(Mutability mutability); //server-side mutability, i.e. how Get-/ChangeConfiguration can access this config + Mutability getMutability(); //server-side mutability, i.e. how Get-/ChangeConfiguration can access this config +}; + +std::unique_ptr makeConfiguration(Configuration::Type dtype); + +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 +#endif diff --git a/src/MicroOcpp/Model/Configuration/ConfigurationContainer.cpp b/src/MicroOcpp/Model/Configuration/ConfigurationContainer.cpp new file mode 100644 index 00000000..1e611426 --- /dev/null +++ b/src/MicroOcpp/Model/Configuration/ConfigurationContainer.cpp @@ -0,0 +1,258 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#include + +#include +#include +#include + +#if MO_ENABLE_V16 + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; + +ConfigurationContainer::~ConfigurationContainer() { + +} + +bool ConfigurationContainer::commit() { + return true; +} + +ConfigurationContainerNonOwning::ConfigurationContainerNonOwning() : + ConfigurationContainer(), MemoryManaged("v16.Configuration.ConfigurationContainerNonOwning"), configurations(makeVector(getMemoryTag())) { + +} + +size_t ConfigurationContainerNonOwning::size() { + return configurations.size(); +} + +Configuration *ConfigurationContainerNonOwning::getConfiguration(size_t i) { + return configurations[i]; +} + +Configuration *ConfigurationContainerNonOwning::getConfiguration(const char *key) { + for (size_t i = 0; i < configurations.size(); i++) { + auto& configuration = configurations[i]; + if (!strcmp(configuration->getKey(), key)) { + return configuration; + } + } + return nullptr; +} + +bool ConfigurationContainerNonOwning::add(Configuration *configuration) { + configurations.push_back(configuration); + return true; +} + +bool ConfigurationContainerOwning::checkWriteCountUpdated() { + + decltype(trackWriteCount) writeCount = 0; + + for (size_t i = 0; i < configurations.size(); i++) { + writeCount += configurations[i]->getWriteCount(); + } + + bool updated = writeCount != trackWriteCount; + + trackWriteCount = writeCount; + + return updated; +} + +ConfigurationContainerOwning::ConfigurationContainerOwning() : + ConfigurationContainer(), MemoryManaged("v16.Configuration.ConfigurationContainerOwning"), configurations(makeVector(getMemoryTag())) { + +} + +ConfigurationContainerOwning::~ConfigurationContainerOwning() { + MO_FREE(filename); + filename = nullptr; + + for (size_t i = 0; i < configurations.size(); i++) { + delete configurations[i]; + } +} + +size_t ConfigurationContainerOwning::size() { + return configurations.size(); +} + +Configuration *ConfigurationContainerOwning::getConfiguration(size_t i) { + return configurations[i]; +} + +Configuration *ConfigurationContainerOwning::getConfiguration(const char *key) { + for (size_t i = 0; i < configurations.size(); i++) { + auto& configuration = configurations[i]; + if (!strcmp(configuration->getKey(), key)) { + return configuration; + } + } + return nullptr; +} + +bool ConfigurationContainerOwning::add(std::unique_ptr configuration) { + configurations.push_back(configuration.release()); + return true; +} + +void ConfigurationContainerOwning::setFilesystem(MO_FilesystemAdapter *filesystem) { + this->filesystem = filesystem; +} + +bool ConfigurationContainerOwning::setFilename(const char *filename) { + MO_FREE(this->filename); + this->filename = nullptr; + + if (filename && *filename) { + 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; +} + +const char *ConfigurationContainerOwning::getFilename() { + return filename; +} + +bool ConfigurationContainerOwning::load() { + if (loaded) { + return true; + } + + if (!filesystem || !filename || !strncmp(filename, MO_CONFIGURATION_VOLATILE, sizeof(MO_CONFIGURATION_VOLATILE) - 1)) { + return true; //persistency disabled - nothing to do + } + + JsonDoc doc (0); + + auto ret = FilesystemUtils::loadJson(filesystem, filename, doc, getMemoryTag()); + switch (ret) { + case FilesystemUtils::LoadStatus::Success: + break; //continue loading JSON + case FilesystemUtils::LoadStatus::FileNotFound: + MO_DBG_DEBUG("Populate FS: create configurations file"); + return commit(); + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + return false; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("failed to load %s", filename); + return false; + } + + JsonArray configurationsJson = doc["configurations"]; + + for (JsonObject stored : configurationsJson) { + + const char *key = stored["key"] | ""; + if (!*key) { + MO_DBG_ERR("corrupt config"); + continue; + } + + if (!stored.containsKey("value")) { + MO_DBG_ERR("corrupt config"); + continue; + } + + auto configurationPtr = getConfiguration(key); + if (!configurationPtr) { + MO_DBG_ERR("loaded configuration does not exist: %s, %s", filename, key); + continue; + } + + auto& configuration = *configurationPtr; + + switch (configuration.getType()) { + case Configuration::Type::Int: + configuration.setInt(stored["value"] | 0); + break; + case Configuration::Type::Bool: + configuration.setBool(stored["value"] | false); + break; + case Configuration::Type::String: + if (!configuration.setString(stored["value"] | "")) { + MO_DBG_ERR("value error: %s, %s", filename, key); + continue; + } + break; + } + } + + checkWriteCountUpdated(); // update trackWriteCount after load is completed + + MO_DBG_DEBUG("Initialization finished"); + loaded = true; + return true; +} + +bool ConfigurationContainerOwning::commit() { + if (!filesystem || !filename || !strncmp(filename, MO_CONFIGURATION_VOLATILE, sizeof(MO_CONFIGURATION_VOLATILE) - 1)) { + //persistency disabled - nothing to do + return true; + } + + if (!checkWriteCountUpdated()) { + return true; //nothing to be done + } + + size_t 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); + + JsonArray configurationsJson = doc.createNestedArray("configurations"); + + for (size_t i = 0; i < configurations.size(); i++) { + auto& configuration = *configurations[i]; + + auto stored = configurationsJson.createNestedObject(); + + stored["key"] = configuration.getKey(); + + switch (configuration.getType()) { + case Configuration::Type::Int: + stored["value"] = configuration.getInt(); + break; + case Configuration::Type::Bool: + stored["value"] = configuration.getBool(); + break; + case Configuration::Type::String: + stored["value"] = configuration.getString(); + break; + } + } + + auto ret = FilesystemUtils::storeJson(filesystem, filename, doc); + bool success = (ret == FilesystemUtils::StoreStatus::Success); + + if (success) { + MO_DBG_DEBUG("Saving configurations finished"); + } else { + MO_DBG_ERR("could not save configurations file: %s %i", filename, (int)ret); + } + + return success; +} + +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Model/Configuration/ConfigurationContainer.h b/src/MicroOcpp/Model/Configuration/ConfigurationContainer.h new file mode 100644 index 00000000..be2dc3a3 --- /dev/null +++ b/src/MicroOcpp/Model/Configuration/ConfigurationContainer.h @@ -0,0 +1,78 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#ifndef MO_CONFIGURATIONCONTAINER_H +#define MO_CONFIGURATIONCONTAINER_H + +#include + +#include +#include +#include + +#if MO_ENABLE_V16 + +#define MO_CONFIGURATION_FN "ocpp-config.jsn" +#define MO_CONFIGURATION_VOLATILE "/volatile" +#define MO_KEYVALUE_FN "client-state.jsn" + +namespace MicroOcpp { +namespace v16 { + +class ConfigurationContainer { +public: + virtual ~ConfigurationContainer(); + virtual size_t size() = 0; + virtual Configuration *getConfiguration(size_t i) = 0; + virtual Configuration *getConfiguration(const char *key) = 0; + + virtual bool commit(); +}; + +class ConfigurationContainerNonOwning : public ConfigurationContainer, public MemoryManaged { +private: + Vector configurations; +public: + ConfigurationContainerNonOwning(); + + size_t size() override; + Configuration *getConfiguration(size_t i) override; + Configuration *getConfiguration(const char *key) override; + + bool add(Configuration *configuration); +}; + +class ConfigurationContainerOwning : public ConfigurationContainer, public MemoryManaged { +private: + MO_FilesystemAdapter *filesystem = nullptr; + char *filename = nullptr; + + Vector configurations; + + uint16_t trackWriteCount = 0; + bool checkWriteCountUpdated(); + + bool loaded = false; + +public: + ConfigurationContainerOwning(); + ~ConfigurationContainerOwning(); + + size_t size() override; + Configuration *getConfiguration(size_t i) override; + Configuration *getConfiguration(const char *key) override; + + bool add(std::unique_ptr configuration); + + void setFilesystem(MO_FilesystemAdapter *filesystem); + bool setFilename(const char *filename); + const char *getFilename(); + bool load(); //load configurations from flash + bool commit() override; +}; + +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 +#endif diff --git a/src/MicroOcpp/Model/Configuration/ConfigurationDefs.h b/src/MicroOcpp/Model/Configuration/ConfigurationDefs.h new file mode 100644 index 00000000..e0e65950 --- /dev/null +++ b/src/MicroOcpp/Model/Configuration/ConfigurationDefs.h @@ -0,0 +1,17 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#ifndef MO_CONFIGURATIONDEFS_H +#define MO_CONFIGURATIONDEFS_H + +#include + +#if MO_ENABLE_V16 + +#ifndef MO_CONFIG_EXT_PREFIX +#define MO_CONFIG_EXT_PREFIX "Cst_" +#endif + +#endif //MO_ENABLE_V16 +#endif diff --git a/src/MicroOcpp/Model/Configuration/ConfigurationService.cpp b/src/MicroOcpp/Model/Configuration/ConfigurationService.cpp new file mode 100644 index 00000000..9aaa3fe5 --- /dev/null +++ b/src/MicroOcpp/Model/Configuration/ConfigurationService.cpp @@ -0,0 +1,497 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +/* + * Implementation of the UCs B05 - B06 + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#if MO_ENABLE_V16 + +#ifndef MO_GETBASEREPORT_CHUNKSIZE +#define MO_GETBASEREPORT_CHUNKSIZE 4 +#endif + +namespace MicroOcpp { +namespace v16 { + +template +ConfigurationValidator::ConfigurationValidator(const char *key, bool (*validateFn)(T, void*), void *user_data) : + key(key), user_data(user_data), validateFn(validateFn) { + +} + +template +bool ConfigurationValidator::validate(T v) { + return validateFn(v, user_data); +} + +bool VALIDATE_UNSIGNED_INT(int v, void*) { + return v >= 0; +} + +template +ConfigurationValidator *getConfigurationValidator(Vector>& collection, const char *key) { + for (size_t i = 0; i < collection.size(); i++) { + auto& validator = collection[i]; + if (!strcmp(key, validator.key)) { + return &validator; + } + } + return nullptr; +} + +ConfigurationValidator *ConfigurationService::getValidatorInt(const char *key) { + return getConfigurationValidator(validatorInt, key); +} + +ConfigurationValidator *ConfigurationService::getValidatorBool(const char *key) { + return getConfigurationValidator(validatorBool, key); +} + +ConfigurationValidator *ConfigurationService::getValidatorString(const char *key) { + return getConfigurationValidator(validatorString, key); +} + +ConfigurationContainerOwning *ConfigurationService::getContainerInternal(const char *filename) { + for (size_t i = 0; i < containersInternal.size(); i++) { + auto container = containersInternal[i]; + if (container->getFilename() && !strcmp(filename, container->getFilename())) { + return container; + } + } + return nullptr; +} + +ConfigurationContainerOwning *ConfigurationService::declareContainer(const char *filename) { + + auto container = getContainerInternal(filename); + + if (!container) { + MO_DBG_DEBUG("init new configurations container: %s", filename); + + container = new ConfigurationContainerOwning(); + if (!container) { + MO_DBG_ERR("OOM"); + return nullptr; + } + if (!container->setFilename(filename)) { + MO_DBG_ERR("OOM"); + delete container; + container = nullptr; + return nullptr; + } + containersInternal.push_back(container); //transfer ownership + containers.push_back(container); //does not take ownership + } + + return container; +} + +bool ConfigurationService::addContainer(ConfigurationContainer *container) { + containers.push_back(container); //does not take ownership + return true; +} + +template +bool registerConfigurationValidator(Vector>& collection, const char *key, bool (*validate)(T, void*), void *user_data) { + for (auto it = collection.begin(); it != collection.end(); it++) { + if (!strcmp(key, it->key)) { + collection.erase(it); + break; + } + } + collection.emplace_back(key, validate, user_data); + return true; +} + +template <> +bool ConfigurationService::registerValidator(const char *key, bool (*validate)(int, void*), void *user_data) { + return registerConfigurationValidator(validatorInt, key, validate, user_data); +} + +template <> +bool ConfigurationService::registerValidator(const char *key, bool (*validate)(bool, void*), void *user_data) { + return registerConfigurationValidator(validatorBool, key, validate, user_data); +} + +template <> +bool ConfigurationService::registerValidator(const char *key, bool (*validate)(const char*, void*), void *user_data) { + return registerConfigurationValidator(validatorString, key, validate, user_data); +} + +Configuration *ConfigurationService::getConfiguration(const char *key) { + + for (size_t i = 0; i < containers.size(); i++) { + auto container = containers[i]; + if (auto config = container->getConfiguration(key)) { + return config; + } + } + + return nullptr; +} + +bool ConfigurationService::getConfiguration(const char **keys, size_t keysSize, Vector& configurations, Vector& unknownKeys) { + if (keysSize == 0) { + // query empty - dump all configs + + for (size_t i = 0; i < containers.size(); i++) { + auto container = containers[i]; + for (size_t j = 0; j < container->size(); j++) { + auto config = container->getConfiguration(j); + if (config->getMutability() == Mutability::ReadWrite || config->getMutability() == Mutability::ReadOnly) { + configurations.push_back(config); //does not take ownership + } + } + } + } else { + // query configs by keys + + for (size_t i = 0; i < keysSize; i++) { + const char *key = keys[i]; + + auto config = getConfiguration(key); + if (config && (config->getMutability() == Mutability::ReadWrite || config->getMutability() == Mutability::ReadOnly)) { + //found key + configurations.push_back(config); //does not take ownership + } else { + //unknown key + + size_t size = strlen(key) + 1; + char *unknownKey = static_cast(MO_MALLOC(getMemoryTag(), size)); + if (!unknownKey) { + MO_DBG_ERR("OOM"); + return false; + } + memset(unknownKey, 0, size); + auto ret = snprintf(unknownKey, size, "%s", key); + if (ret < 0 || (size_t)ret >= size) { + MO_DBG_ERR("snprintf: %i", ret); + MO_FREE(unknownKey); + unknownKey = nullptr; + return false; + } + + unknownKeys.push_back(unknownKey); //transfer ownership + } + } + } + + return true; +} + +ConfigurationStatus ConfigurationService::changeConfiguration(const char *key, const char *value) { + + ConfigurationContainer *container = nullptr; + Configuration *config = nullptr; + + for (size_t i = 0; i < containers.size(); i++) { + auto container_i = containers[i]; + for (size_t j = 0; j < container_i->size(); j++) { + auto config_ij = container_i->getConfiguration(j); + if (!strcmp(key, config_ij->getKey())) { + container = container_i; + config = config_ij; + } + } + } + + if (!config || config->getMutability() == Mutability::None) { + //config not found or hidden config + return ConfigurationStatus::NotSupported; + } + + if (config->getMutability() == Mutability::ReadOnly) { + MO_DBG_WARN("Trying to override readonly value"); + return ConfigurationStatus::Rejected; + } + + //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; + } + + //validate and store (parsed) value to Config + + if (config->getType() == Configuration::Type::Int && convertibleInt) { + auto validator = getValidatorInt(key); + if (validator && !validator->validate(numInt)) { + MO_DBG_WARN("validation failed for key=%s value=%i", key, numInt); + return ConfigurationStatus::Rejected; + } + config->setInt(numInt); + } else if (config->getType() == Configuration::Type::Bool && convertibleBool) { + auto validator = getValidatorBool(key); + if (validator && !validator->validate(numBool)) { + MO_DBG_WARN("validation failed for key=%s value=%s", key, numBool ? "true" : "false"); + return ConfigurationStatus::Rejected; + } + config->setBool(numBool); + } else if (config->getType() == Configuration::Type::String) { + auto validator = getValidatorString(key); + if (validator && !validator->validate(value)) { + MO_DBG_WARN("validation failed for key=%s value=%i", key, value); + return ConfigurationStatus::Rejected; + } + if (!config->setString(value)) { + MO_DBG_WARN("OOM"); + return ConfigurationStatus::Rejected; + } + } else { + MO_DBG_WARN("Value has incompatible type"); + return ConfigurationStatus::Rejected; + } + + if (!container->commit()) { + MO_DBG_ERR("could not write changes to flash"); + return ConfigurationStatus::Rejected; + } + + //success + + if (config->isRebootRequired()) { + return ConfigurationStatus::RebootRequired; + } else { + return ConfigurationStatus::Accepted; + } +} + +ConfigurationContainer *ConfigurationService::findContainerOfConfiguration(Configuration *whichConfig) { + for (size_t i = 0; i < containers.size(); i++) { + auto container = containers[i]; + for (size_t j = 0; j < container->size(); j++) { + if (whichConfig == container->getConfiguration(j)) { + return container; + } + } + } + return nullptr; +} + +ConfigurationService::ConfigurationService(Context& context) : + MemoryManaged("v16.Configuration.ConfigurationService"), + context(context), + containers(makeVector(getMemoryTag())), + validatorInt(makeVector>(getMemoryTag())), + validatorBool(makeVector>(getMemoryTag())), + validatorString(makeVector>(getMemoryTag())) { + +} + +ConfigurationService::~ConfigurationService() { + for (unsigned int i = 0; i < containersInternal.size(); i++) { + delete containersInternal[i]; + } +} + +bool ConfigurationService::init() { + containers.reserve(1); + if (containers.capacity() < 1) { + MO_DBG_ERR("OOM"); + return false; + } + + containers.push_back(&containerExternal); + return true; +} + +bool ConfigurationService::setup() { + + declareConfiguration("GetConfigurationMaxKeys", 30, MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); + + filesystem = context.getFilesystem(); + if (!filesystem) { + MO_DBG_DEBUG("no fs access"); + } + + for (unsigned int i = 0; i < containersInternal.size(); i++) { + containersInternal[i]->setFilesystem(filesystem); + if (!containersInternal[i]->load()) { + MO_DBG_ERR("failure to load %s", containersInternal[i]->getFilename()); + return false; + } + } + + context.getMessageService().registerOperation("ChangeConfiguration", [] (Context& context) -> Operation* { + return new ChangeConfiguration(*context.getModel16().getConfigurationService());}); + context.getMessageService().registerOperation("GetConfiguration", [] (Context& context) -> Operation* { + return new GetConfiguration(*context.getModel16().getConfigurationService());}); + + return true; +} + +template +bool loadConfigurationFactoryDefault(Configuration& config, T factoryDef); + +template<> +bool loadConfigurationFactoryDefault(Configuration& config, int factoryDef) { + config.setInt(factoryDef); + return true; +} + +template<> +bool loadConfigurationFactoryDefault(Configuration& config, bool factoryDef) { + config.setBool(factoryDef); + return true; +} + +template<> +bool loadConfigurationFactoryDefault(Configuration& config, const char *factoryDef) { + return config.setString(factoryDef); +} + +void loadConfigurationCharacteristics(Configuration& config, Mutability mutability, bool rebootRequired) { + if (config.getMutability() == Mutability::ReadWrite) { + config.setMutability(mutability); + } else if (mutability != config.getMutability() && mutability != Mutability::ReadWrite) { + config.setMutability(Mutability::None); + } + + if (rebootRequired) { + config.setRebootRequired(); + } +} + +template +Configuration::Type getType(); + +template<> Configuration::Type getType() {return Configuration::Type::Int;} +template<> Configuration::Type getType() {return Configuration::Type::Bool;} +template<> Configuration::Type getType() {return Configuration::Type::String;} + +template +Configuration *ConfigurationService::declareConfiguration(const char *key, T factoryDefault, const char *filename, Mutability mutability, bool rebootRequired) { + + auto res = getConfiguration(key); + if (!res) { + auto container = declareContainer(filename); + if (!container) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + auto config = makeConfiguration(getType()); + if (!config) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + config->setKey(key); + + if (!loadConfigurationFactoryDefault(*config, factoryDefault)) { + return nullptr; + } + + res = config.get(); + + if (!container->add(std::move(config))) { + return nullptr; + } + } + + loadConfigurationCharacteristics(*res, mutability, rebootRequired); + return res; +} + +template Configuration *ConfigurationService::declareConfiguration( const char*, int, const char*, Mutability, bool); +template Configuration *ConfigurationService::declareConfiguration( const char*, bool, const char*, Mutability, bool); +template Configuration *ConfigurationService::declareConfiguration(const char*, const char*, const char*, Mutability, bool); + +bool ConfigurationService::addConfiguration(Configuration *config) { + return containerExternal.add(config); +} + +bool ConfigurationService::addConfiguration(std::unique_ptr config, const char *filename) { + if (getConfiguration(config->getKey())) { + MO_DBG_ERR("config already exists"); + return false; + } + auto container = declareContainer(filename); + if (!container) { + MO_DBG_ERR("OOM"); + return false; + } + return container->add(std::move(config)); +} + +bool ConfigurationService::commit() { + bool success = true; + + for (size_t i = 0; i < containers.size(); i++) { + if (!containers[i]->commit()) { + success = false; + } + } + + return success; +} + +} //namespace v16 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Model/Configuration/ConfigurationService.h b/src/MicroOcpp/Model/Configuration/ConfigurationService.h new file mode 100644 index 00000000..a1c37a38 --- /dev/null +++ b/src/MicroOcpp/Model/Configuration/ConfigurationService.h @@ -0,0 +1,87 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#ifndef MO_CONFIGURATIONSERVICE_H +#define MO_CONFIGURATIONSERVICE_H + +#include +#include +#include +#include +#include + +#if MO_ENABLE_V16 + +namespace MicroOcpp { + +class Context; + +namespace v16 { + +template +struct ConfigurationValidator { + const char *key; + void *user_data; + bool (*validateFn)(T, void*); + ConfigurationValidator(const char *key, bool (*validate)(T, void*), void *user_data); + bool validate(T); +}; + +bool VALIDATE_UNSIGNED_INT(int v, void*); //default implementation for common validator. Returns true if v >= 0 + +class ConfigurationService : public MemoryManaged { +private: + Context& context; + + MO_FilesystemAdapter *filesystem = nullptr; + + Vector containers; //does not take ownership + ConfigurationContainerNonOwning containerExternal; + Vector containersInternal; //takes ownership + ConfigurationContainerOwning *getContainerInternal(const char *filename); + ConfigurationContainerOwning *declareContainer(const char *filename); + + Vector> validatorInt; + Vector> validatorBool; + Vector> validatorString; + + ConfigurationValidator *getValidatorInt(const char *key); + ConfigurationValidator *getValidatorBool(const char *key); + ConfigurationValidator *getValidatorString(const char *key); + +public: + ConfigurationService(Context& context); + ~ConfigurationService(); + + bool init(); + + //Get Configuration. If not existent, create Configuration owned by MO and return + template + Configuration *declareConfiguration(const char *key, T factoryDefault, const char *filename = MO_CONFIGURATION_FN, Mutability mutability = Mutability::ReadWrite, bool rebootRequired = false); + + bool addConfiguration(Configuration *configuration); //Add Configuration without transferring ownership + bool addConfiguration(std::unique_ptr configuration, const char *filename); //Add Configuration and transfer ownership + + bool addContainer(ConfigurationContainer *container); //Add Container without transferring ownership + + template + bool registerValidator(const char *key, bool (*validate)(T, void*), void *userPtr = nullptr); + + bool setup(); + + //Get Configuration. If not existent, return nullptr + Configuration *getConfiguration(const char *key); + + bool getConfiguration(const char **keys, size_t keysSize, Vector& configurations, Vector& unknownKeys); //unknownKeys entries are allocated on heap - need to MO_FREE each entry, even after failure + ConfigurationStatus changeConfiguration(const char *key, const char *value); + + ConfigurationContainer *findContainerOfConfiguration(Configuration *whichConfig); + + bool commit(); +}; + +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 +#endif diff --git a/src/MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h b/src/MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h deleted file mode 100644 index 6c7b04f2..00000000 --- a/src/MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h +++ /dev/null @@ -1,33 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/ConnectorBase/ChargePointStatus.h b/src/MicroOcpp/Model/ConnectorBase/ChargePointStatus.h deleted file mode 100644 index 019fce27..00000000 --- a/src/MicroOcpp/Model/ConnectorBase/ChargePointStatus.h +++ /dev/null @@ -1,36 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/ConnectorBase/Connector.cpp b/src/MicroOcpp/Model/ConnectorBase/Connector.cpp deleted file mode 100644 index 11142dc9..00000000 --- a/src/MicroOcpp/Model/ConnectorBase/Connector.cpp +++ /dev/null @@ -1,1322 +0,0 @@ -// 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; -} diff --git a/src/MicroOcpp/Model/ConnectorBase/Connector.h b/src/MicroOcpp/Model/ConnectorBase/Connector.h deleted file mode 100644 index d8d61639..00000000 --- a/src/MicroOcpp/Model/ConnectorBase/Connector.h +++ /dev/null @@ -1,166 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp b/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp deleted file mode 100644 index cfc32c82..00000000 --- a/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp +++ /dev/null @@ -1,92 +0,0 @@ -// 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 -} diff --git a/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.h b/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.h deleted file mode 100644 index 4bc9f78a..00000000 --- a/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.h +++ /dev/null @@ -1,26 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/ConnectorBase/UnlockConnectorResult.h b/src/MicroOcpp/Model/ConnectorBase/UnlockConnectorResult.h deleted file mode 100644 index 0d863f2d..00000000 --- a/src/MicroOcpp/Model/ConnectorBase/UnlockConnectorResult.h +++ /dev/null @@ -1,36 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/Diagnostics/Diagnostics.cpp b/src/MicroOcpp/Model/Diagnostics/Diagnostics.cpp new file mode 100644 index 00000000..af9e5038 --- /dev/null +++ b/src/MicroOcpp/Model/Diagnostics/Diagnostics.cpp @@ -0,0 +1,106 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include + +#include + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS + +const char *mo_serializeGetLogStatus(MO_GetLogStatus status) { + const char *statusCstr = ""; + switch (status) { + case MO_GetLogStatus_Accepted: + statusCstr = "Accepted"; + break; + case MO_GetLogStatus_Rejected: + statusCstr = "Rejected"; + break; + case MO_GetLogStatus_AcceptedCanceled: + statusCstr = "AcceptedCanceled"; + break; + case MO_GetLogStatus_UNDEFINED: + MO_DBG_ERR("serialize undefined"); + break; + } + return statusCstr; +} + +const char *mo_serializeUploadLogStatus(MO_UploadLogStatus status) { + const char *statusCstr = ""; + switch (status) { + case MO_UploadLogStatus_BadMessage: + statusCstr = "BadMessage"; + break; + case MO_UploadLogStatus_Idle: + statusCstr = "Idle"; + break; + case MO_UploadLogStatus_NotSupportedOperation: + statusCstr = "NotSupportedOperation"; + break; + case MO_UploadLogStatus_PermissionDenied: + statusCstr = "PermissionDenied"; + break; + case MO_UploadLogStatus_Uploaded: + statusCstr = "Uploaded"; + break; + case MO_UploadLogStatus_UploadFailure: + statusCstr = "UploadFailure"; + break; + case MO_UploadLogStatus_Uploading: + statusCstr = "Uploading"; + break; + case MO_UploadLogStatus_AcceptedCanceled: + statusCstr = "AcceptedCanceled"; + break; + } + return statusCstr; +} + +MO_LogType mo_deserializeLogType(const char *v) { + if (!v) { + return MO_LogType_UNDEFINED; + } + + MO_LogType res = MO_LogType_UNDEFINED; + + if (!strcmp(v, "DiagnosticsLog")) { + res = MO_LogType_DiagnosticsLog; + } else if (!strcmp(v, "SecurityLog")) { + res = MO_LogType_SecurityLog; + } + + return res; +} + +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS + +#if MO_ENABLE_V16 && MO_ENABLE_DIAGNOSTICS +namespace MicroOcpp { +namespace v16 { + +const char *serializeDiagnosticsStatus(DiagnosticsStatus status) { + const char *statusCstr = ""; + switch (status) { + case DiagnosticsStatus::Idle: + statusCstr = "Idle"; + break; + case DiagnosticsStatus::Uploaded: + statusCstr = "Uploaded"; + break; + case DiagnosticsStatus::UploadFailed: + statusCstr = "UploadFailed"; + break; + case DiagnosticsStatus::Uploading: + statusCstr = "Uploading"; + break; + } + return statusCstr; +} + +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_DIAGNOSTICS diff --git a/src/MicroOcpp/Model/Diagnostics/Diagnostics.h b/src/MicroOcpp/Model/Diagnostics/Diagnostics.h new file mode 100644 index 00000000..587b26c5 --- /dev/null +++ b/src/MicroOcpp/Model/Diagnostics/Diagnostics.h @@ -0,0 +1,78 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_DIAGNOSTICS_H +#define MO_DIAGNOSTICS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif //__cplusplus + +#ifndef MO_GETLOG_FNAME_SIZE +#define MO_GETLOG_FNAME_SIZE 30 +#endif + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS + +typedef enum { + MO_GetLogStatus_UNDEFINED, //MO-internal + MO_GetLogStatus_Accepted, + MO_GetLogStatus_Rejected, + MO_GetLogStatus_AcceptedCanceled, +} MO_GetLogStatus; +const char *mo_serializeGetLogStatus(MO_GetLogStatus status); + +// UploadLogStatusEnumType (6.17 in 1.6 Security whitepaper, 3.87 in 2.0.1 spec) +typedef enum { + MO_UploadLogStatus_BadMessage, + MO_UploadLogStatus_Idle, + MO_UploadLogStatus_NotSupportedOperation, + MO_UploadLogStatus_PermissionDenied, + MO_UploadLogStatus_Uploaded, + MO_UploadLogStatus_UploadFailure, + MO_UploadLogStatus_Uploading, + MO_UploadLogStatus_AcceptedCanceled, + +} MO_UploadLogStatus; +const char *mo_serializeUploadLogStatus(MO_UploadLogStatus status); + +typedef enum { + MO_UploadStatus_NotUploaded, + MO_UploadStatus_Uploaded, + MO_UploadStatus_UploadFailed +} MO_UploadStatus; + +typedef enum { + MO_LogType_UNDEFINED, + MO_LogType_DiagnosticsLog, + MO_LogType_SecurityLog, +} MO_LogType; +MO_LogType mo_deserializeLogType(const char *v); + +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS + +#ifdef __cplusplus +} //extern "C" + +#if MO_ENABLE_V16 && MO_ENABLE_DIAGNOSTICS + +namespace MicroOcpp { +namespace v16 { + +enum class DiagnosticsStatus { + Idle, + Uploaded, + UploadFailed, + Uploading +}; +const char *serializeDiagnosticsStatus(DiagnosticsStatus status); + +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_DIAGNOSTICS + +#endif //__cplusplus +#endif diff --git a/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp index 6257ad57..2579feb4 100644 --- a/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp +++ b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp @@ -3,47 +3,179 @@ // MIT License #include -#include +#include #include +#include +#include +#include +#include +#include #include #include #include #include +#include +#include //Fetch relevant data from other modules for diagnostics -#include -#include //for serializing ChargePointStatus +#include #include -#include +#include +#include #include //for MO_ENABLE_V201 -#include //for MO_ENABLE_CONNECTOR_LOCK +#include //for MO_ENABLE_CONNECTOR_LOCK + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS -using MicroOcpp::DiagnosticsService; -using MicroOcpp::Ocpp16::DiagnosticsStatus; -using MicroOcpp::Request; +#define MO_DIAG_PREAMBLE_SIZE 192U +#define MO_DIAG_POSTAMBLE_SIZE 1024U -DiagnosticsService::DiagnosticsService(Context& context) : MemoryManaged("v16.Diagnostics.DiagnosticsService"), context(context), location(makeString(getMemoryTag())), diagFileList(makeVector(getMemoryTag())) { +namespace MicroOcpp { + +#if MO_USE_DIAGNOSTICS == MO_DIAGNOSTICS_BUILTIN_MBEDTLS_ESP32 +size_t defaultDiagnosticsReader(char *buf, size_t size, void *user_data); +void defaultDiagnosticsOnClose(void *user_data); +#endif - context.getOperationRegistry().registerOperation("GetDiagnostics", [this] () { - return new Ocpp16::GetDiagnostics(*this);}); +DiagnosticsService::DiagnosticsService(Context& context) : MemoryManaged("v16/v201.Diagnostics.DiagnosticsService"), context(context), diagFileList(makeVector(getMemoryTag())) { - //Register message handler for TriggerMessage operation - context.getOperationRegistry().registerOperation("DiagnosticsStatusNotification", [this] () { - return new Ocpp16::DiagnosticsStatusNotification(getDiagnosticsStatus());}); } DiagnosticsService::~DiagnosticsService() { + MO_FREE(customProtocols); + customProtocols = nullptr; + MO_FREE(location); + location = nullptr; + MO_FREE(filename); + filename = nullptr; MO_FREE(diagPreamble); + diagPreamble = nullptr; MO_FREE(diagPostamble); + diagPostamble = nullptr; } -void DiagnosticsService::loop() { +bool DiagnosticsService::setup() { + + filesystem = context.getFilesystem(); + ftpClient = context.getFtpClient(); + +#if MO_USE_DIAGNOSTICS == MO_DIAGNOSTICS_CUSTOM + if (!onUpload || !onUploadStatusInput) { + MO_DBG_ERR("need to set onUpload cb and onUploadStatusInput cb"); + return false; + } +#else + if ((!onUpload || !onUploadStatusInput) && ftpClient) { + if (!filesystem) { + MO_DBG_WARN("Security Log upload disabled (volatile mode)"); + } + } else { + MO_DBG_ERR("depends on FTP client"); + return false; + } +#endif + +#if MO_USE_DIAGNOSTICS == MO_DIAGNOSTICS_BUILTIN_MBEDTLS_ESP32 + if (!diagnosticsReader) { + diagnosticsReader = defaultDiagnosticsReader; + diagnosticsOnClose = defaultDiagnosticsOnClose; + } +#endif //MO_USE_DIAGNOSTICS == MO_DIAGNOSTICS_BUILTIN_MBEDTLS_ESP32 + + char fileTransferProtocolsBuf [sizeof("FTP,FTPS,HTTP,HTTPS,SFTP") * 2]; //dimension to fit all allowed protocols (plus some safety space) + if (customProtocols) { + auto ret = snprintf(fileTransferProtocolsBuf, sizeof(fileTransferProtocolsBuf), "%s", customProtocols); + if (ret < 0 || (size_t)ret >= sizeof(fileTransferProtocolsBuf)) { + MO_DBG_ERR("custom protocols too long"); + return false; + } + MO_FREE(customProtocols); + customProtocols = nullptr; + } - if (ftpUpload && ftpUpload->isActive()) { - ftpUpload->loop(); + auto rcService = context.getModelCommon().getRemoteControlService(); + if (!rcService) { + MO_DBG_ERR("initialization error"); + return false; } + ocppVersion = context.getOcppVersion(); + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("initialization error"); + return false; + } + + auto fileTransferProtocols = configService->declareConfiguration("SupportedFileTransferProtocols", "", MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); + if (!fileTransferProtocols) { + MO_DBG_ERR("initialization error"); + return false; + } + + if (customProtocols) { + fileTransferProtocols->setString(customProtocols); + } else if (ftpClient) { + fileTransferProtocols->setString("FTP,FTPS"); + } + + context.getMessageService().registerOperation("GetDiagnostics", [] (Context& context) -> Operation* { + return new v16::GetDiagnostics(context, *context.getModel16().getDiagnosticsService());}); + + #if MO_ENABLE_MOCK_SERVER + context.getMessageService().registerOperation("DiagnosticsStatusNotification", nullptr, nullptr); + #endif //MO_ENABLE_MOCK_SERVER + + rcService->addTriggerMessageHandler("DiagnosticsStatusNotification", [] (Context& context) -> Operation* { + auto diagSvc = context.getModel16().getDiagnosticsService(); + return new v16::DiagnosticsStatusNotification(diagSvc->getUploadStatus16()); + }); + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + auto varService = context.getModel201().getVariableService(); + if (!varService) { + MO_DBG_ERR("initialization error"); + return false; + } + + //if transactions can start before the BootNotification succeeds + auto fileTransferProtocols = varService->declareVariable("OCPPCommCtrlr", "FileTransferProtocols", "", Mutability::ReadOnly, false); + if (!fileTransferProtocols) { + MO_DBG_ERR("initialization error"); + return false; + } + + if (customProtocols) { + fileTransferProtocols->setString(customProtocols); + } else if (ftpClient) { + fileTransferProtocols->setString("FTP,FTPS"); + } + } + #endif //MO_ENABLE_V201 + + context.getMessageService().registerOperation("GetLog", [] (Context& context) -> Operation* { + return new GetLog(context, *context.getModelCommon().getDiagnosticsService());}); + + rcService->addTriggerMessageHandler("LogStatusNotification", [] (Context& context) -> Operation* { + auto diagService = context.getModelCommon().getDiagnosticsService(); + return new LogStatusNotification(diagService->getUploadStatus(), diagService->getRequestId());}); + + #if MO_ENABLE_MOCK_SERVER + context.getMessageService().registerOperation("LogStatusNotification", nullptr, nullptr); + #endif //MO_ENABLE_MOCK_SERVER + + MO_FREE(customProtocols); //not needed anymore + customProtocols = nullptr; + + return true; +} + +void DiagnosticsService::loop() { + if (ftpUpload) { if (ftpUpload->isActive()) { ftpUpload->loop(); @@ -53,120 +185,283 @@ void DiagnosticsService::loop() { } } - auto notification = getDiagnosticsStatusNotification(); - if (notification) { - context.initiateRequest(std::move(notification)); + Operation *uploadStatusNotification = nullptr; + +#if MO_ENABLE_V16 + if (use16impl) { + if (getUploadStatus16() != lastReportedUploadStatus16) { + lastReportedUploadStatus16 = getUploadStatus16(); + if (lastReportedUploadStatus16 != v16::DiagnosticsStatus::Idle) { + uploadStatusNotification = new v16::DiagnosticsStatusNotification(lastReportedUploadStatus16); + if (!uploadStatusNotification) { + MO_DBG_ERR("OOM"); + } + } + } + } else +#endif + { + if (getUploadStatus() != lastReportedUploadStatus) { + lastReportedUploadStatus = getUploadStatus(); + if (lastReportedUploadStatus != MO_UploadLogStatus_Idle) { + uploadStatusNotification = new LogStatusNotification(lastReportedUploadStatus, requestId); + if (!uploadStatusNotification) { + MO_DBG_ERR("OOM"); + } + } + } + } + + if (uploadStatusNotification) { + auto request = makeRequest(context, uploadStatusNotification); + if (!request) { + MO_DBG_ERR("OOM"); + delete uploadStatusNotification; + } else { + context.getMessageService().sendRequest(std::move(request)); + } + uploadStatusNotification = nullptr; + } + + if (runCustomUpload) { + if (getUploadStatus() != MO_UploadLogStatus_Uploading) { + MO_DBG_INFO("custom upload finished"); + runCustomUpload = false; + } + return; + } + + auto& clock = context.getClock(); + + int32_t dtNextTry; + if (!clock.delta(clock.getUptime(), nextTry, dtNextTry)) { + dtNextTry = -1; } - const auto& timestampNow = context.getModel().getClock().now(); - if (retries > 0 && timestampNow >= nextTry) { + if (retries >= 0 && dtNextTry >= 0) { if (!uploadIssued) { - if (onUpload != nullptr) { - MO_DBG_DEBUG("Call onUpload"); - onUpload(location.c_str(), startTime, stopTime); + + bool success = false; + switch (logType) { + case MO_LogType_DiagnosticsLog: + success = uploadDiagnostics(); + break; + case MO_LogType_SecurityLog: + success = uploadSecurityLog(); + break; + case MO_LogType_UNDEFINED: + MO_DBG_ERR("undefined value"); + success = false; + break; + } + + if (success) { uploadIssued = true; uploadFailure = false; } else { - MO_DBG_ERR("onUpload must be set! (see setOnUpload). Will abort"); - retries = 0; + MO_DBG_ERR("cannot upload via FTP. Abort"); + retries = -1; uploadIssued = false; uploadFailure = true; + cleanUploadData(); + cleanGetLogData(); } } if (uploadIssued) { - if (uploadStatusInput != nullptr && uploadStatusInput() == UploadStatus::Uploaded) { + if (getUploadStatus() == MO_UploadLogStatus_Uploaded) { //success! MO_DBG_DEBUG("end upload routine (by status)"); uploadIssued = false; - retries = 0; + retries = -1; + cleanUploadData(); + cleanGetLogData(); } //check if maximum time elapsed or failed + bool isUploadFailure = false; + switch(getUploadStatus()) { + case MO_UploadLogStatus_BadMessage: + case MO_UploadLogStatus_NotSupportedOperation: + case MO_UploadLogStatus_PermissionDenied: + case MO_UploadLogStatus_UploadFailure: + isUploadFailure = true; + break; + case MO_UploadLogStatus_Uploading: + case MO_UploadLogStatus_Idle: + case MO_UploadLogStatus_Uploaded: + case MO_UploadLogStatus_AcceptedCanceled: + isUploadFailure = false; + break; + } + const int UPLOAD_TIMEOUT = 60; - if (timestampNow - nextTry >= UPLOAD_TIMEOUT - || (uploadStatusInput != nullptr && uploadStatusInput() == UploadStatus::UploadFailed)) { + if (isUploadFailure || dtNextTry >= UPLOAD_TIMEOUT) { //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--; + //either we have UploadFailed status or (NotUploaded + timeout) here + MO_DBG_WARN("Upload timeout or failed"); + cleanUploadData(); + uploadIssued = false; - if (retries == 0) { - MO_DBG_DEBUG("end upload routine (no more retry)"); - uploadFailure = true; - } + clock.add(nextTry, retryInterval); + retries--; + + if (retries < 0) { + MO_DBG_DEBUG("end upload routine (no more retry)"); + uploadFailure = true; + cleanGetLogData(); } } } //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()); +#if MO_ENABLE_V16 +bool DiagnosticsService::requestDiagnosticsUpload(const char *location, unsigned int retries, int retryInterval, Timestamp startTime, Timestamp stopTime, char filenameOut[MO_GETLOG_FNAME_SIZE]) { + + bool success = false; + + auto ret = getLog(MO_LogType_DiagnosticsLog, -1, retries, retryInterval, location, startTime, stopTime, filenameOut); + switch (ret) { + case MO_GetLogStatus_Accepted: + case MO_GetLogStatus_AcceptedCanceled: + this->use16impl = true; + success = true; + break; + case MO_GetLogStatus_Rejected: + filenameOut[0] = '\0'; //clear filename - that means that no upload will follow + success = true; + break; + case MO_GetLogStatus_UNDEFINED: + success = false; + break; } + return success; +} +#endif //MO_ENABLE_V16 - String fileName; - if (refreshFilename) { - fileName = refreshFilename().c_str(); - } else { - fileName = "diagnostics.log"; +MO_GetLogStatus DiagnosticsService::getLog(MO_LogType type, int requestId, int retries, int retryInterval, const char *location, Timestamp oldestTimestamp, Timestamp latestTimestamp, char filenameOut[MO_GETLOG_FNAME_SIZE]) { + + if (runCustomUpload || this->retries >= 0) { + MO_DBG_INFO("upload still running"); + return MO_GetLogStatus_Rejected; } - this->location.reserve(strlen(location) + 1 + fileName.size()); + //clean data which outlives last GetLog + ftpUploadStatus = MO_UploadLogStatus_Idle; + + #if MO_ENABLE_V16 + use16impl = false; //may be re-assigned in requestDiagnosticsUpload + #endif + + //set data which is needed for custom upload and built-in upload + this->requestId = requestId; + + auto& clock = context.getClock(); - this->location = location; + if (onUpload && onUploadStatusInput) { + //initiate custom upload - if (!this->location.empty() && this->location.back() != '/') { - this->location.append("/"); + char oldestTimestampStr [MO_JSONDATE_SIZE]; + if (oldestTimestamp.isDefined()) { + if (!clock.toJsonString(oldestTimestamp, oldestTimestampStr, sizeof(oldestTimestampStr))) { + MO_DBG_ERR("toJsonString"); + return MO_GetLogStatus_UNDEFINED; + } + } + + char latestTimestampStr [MO_JSONDATE_SIZE]; + if (latestTimestamp.isDefined()) { + if (!clock.toJsonString(latestTimestamp, latestTimestampStr, sizeof(latestTimestampStr))) { + MO_DBG_ERR("toJsonString"); + return MO_GetLogStatus_UNDEFINED; + } + } + + auto ret = onUpload( + type, + location, + oldestTimestamp.isDefined() ? oldestTimestampStr : nullptr, + latestTimestamp.isDefined() ? latestTimestampStr : nullptr, + filenameOut, + onUploadUserData); + if (ret == MO_GetLogStatus_Accepted || ret == MO_GetLogStatus_AcceptedCanceled) { + runCustomUpload = true; + } + return ret; } - 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; + //initiate built-in upload + + bool hasFilename = false; + size_t filenameSize = 0; + + MO_FREE(this->location); + this->location = nullptr; + size_t locationSize = strlen(location) + 1; + this->location = static_cast(MO_MALLOC(getMemoryTag(), locationSize)); + if (!this->location) { + MO_DBG_ERR("OOM"); + goto fail; + } + + { + int ret = snprintf(this->location, locationSize, "%s", location); + if (ret < 0 || (size_t)ret >= locationSize) { + MO_DBG_ERR("snprintf: %i", ret); + goto fail; + } + } + + MO_FREE(this->filename); + this->filename = nullptr; + + if (refreshFilename) { + hasFilename = refreshFilename(type, filenameOut, refreshFilenameUserData); + } + + if (!hasFilename) { + auto ret = snprintf(filenameOut, MO_GETLOG_FNAME_SIZE, "diagnostics.log"); + if (ret < 0 || (size_t)ret >= MO_GETLOG_FNAME_SIZE) { + MO_DBG_ERR("snprintf: %i", ret); + goto fail; + } + } + + filenameSize = strlen(filenameOut) + 1; + this->filename = static_cast(MO_MALLOC(getMemoryTag(), filenameSize)); + if (!this->filename) { + MO_DBG_ERR("OOM"); + goto fail; } - + snprintf(this->filename, filenameSize, "%s", filenameOut); + + this->logType = type; + this->retries = retries >= 0 ? retries : 0; + this->retryInterval = retryInterval >= 0 ? retryInterval : 30; + this->oldestTimestamp = oldestTimestamp; + this->latestTimestamp = latestTimestamp; + #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); + char dbuf [MO_JSONDATE_SIZE] = {'\0'}; + char dbuf2 [MO_JSONDATE_SIZE] = {'\0'}; + if (oldestTimestamp.isDefined()) { + clock.toJsonString(oldestTimestamp, dbuf, sizeof(dbuf)); + } + if (latestTimestamp.isDefined()) { + clock.toJsonString(latestTimestamp, dbuf2, sizeof(dbuf2)); + } MO_DBG_INFO("Scheduled Diagnostics upload!\n" \ " location = %s\n" \ " retries = %i" \ ", retryInterval = %u" \ - " startTime = %s\n" \ - " stopTime = %s", - this->location.c_str(), + " oldestTimestamp = %s\n" \ + " latestTimestamp = %s", + this->location, this->retries, this->retryInterval, dbuf, @@ -174,146 +469,197 @@ MicroOcpp::String DiagnosticsService::requestDiagnosticsUpload(const char *locat } #endif - nextTry = context.getModel().getClock().now(); - nextTry += 5; //wait for 5s before upload + nextTry = clock.now(); uploadIssued = false; + uploadFailure = false; #if MO_DBG_LEVEL >= MO_DL_DEBUG { - char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; - nextTry.toJsonString(dbuf, JSONDATE_LENGTH + 1); + char dbuf [MO_JSONDATE_SIZE] = {'\0'}; + if (nextTry.isDefined()) { + clock.toJsonString(nextTry, dbuf, sizeof(dbuf)); + } MO_DBG_DEBUG("Initial try at %s", dbuf); } #endif - return fileName; + return MO_GetLogStatus_Accepted; +fail: + cleanGetLogData(); + return MO_GetLogStatus_UNDEFINED; } -DiagnosticsStatus DiagnosticsService::getDiagnosticsStatus() { - if (uploadFailure) { - return DiagnosticsStatus::UploadFailed; +int DiagnosticsService::getRequestId() { + if (runCustomUpload || this->retries >= 0) { + return requestId; + } else { + return -1; } +} - 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; +MO_UploadLogStatus DiagnosticsService::getUploadStatus() { + + MO_UploadLogStatus status = MO_UploadLogStatus_Idle; + + if (runCustomUpload) { + status = onUploadStatusInput(onUploadUserData); + } else if (uploadFailure) { + status = MO_UploadLogStatus_UploadFailure; + } else { + status = ftpUploadStatus; } - return DiagnosticsStatus::Idle; + return status; } -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; - } +#if MO_ENABLE_V16 +v16::DiagnosticsStatus DiagnosticsService::getUploadStatus16() { + + MO_UploadLogStatus status = getUploadStatus(); + + auto res = v16::DiagnosticsStatus::Idle; + + switch(status) { + case MO_UploadLogStatus_Idle: + res = v16::DiagnosticsStatus::Idle; + break; + case MO_UploadLogStatus_Uploaded: + case MO_UploadLogStatus_AcceptedCanceled: + res = v16::DiagnosticsStatus::Uploaded; + break; + case MO_UploadLogStatus_BadMessage: + case MO_UploadLogStatus_NotSupportedOperation: + case MO_UploadLogStatus_PermissionDenied: + case MO_UploadLogStatus_UploadFailure: + res = v16::DiagnosticsStatus::UploadFailed; + break; + case MO_UploadLogStatus_Uploading: + res = v16::DiagnosticsStatus::Uploading; + break; } + return res; +} +#endif //MO_ENABLE_V16 - return nullptr; +void DiagnosticsService::setDiagnosticsReader(size_t (*diagnosticsReader)(char *buf, size_t size, void *user_data), void(*onClose)(void *user_data), void *user_data) { + this->diagnosticsReader = diagnosticsReader; + this->diagnosticsOnClose = onClose; + this->diagnosticsUserData = user_data; } -void DiagnosticsService::setRefreshFilename(std::function refreshFn) { - this->refreshFilename = refreshFn; +void DiagnosticsService::setRefreshFilename(bool (*refreshFilename)(MO_LogType type, char filenameOut[MO_GETLOG_FNAME_SIZE], void *user_data), void *user_data) { + this->refreshFilename = refreshFilename; + this->refreshFilenameUserData = user_data; } -void DiagnosticsService::setOnUpload(std::function onUpload) { +bool DiagnosticsService::setOnUpload( + MO_GetLogStatus (*onUpload)(MO_LogType type, const char *location, const char *oldestTimestamp, const char *latestTimestamp, char filenameOut[MO_GETLOG_FNAME_SIZE], void *user_data), + MO_UploadLogStatus (*onUploadStatusInput)(void *user_data), + const char *customProtocols, + void *user_data) { this->onUpload = onUpload; -} + this->onUploadStatusInput = onUploadStatusInput; + + MO_FREE(this->customProtocols); + this->customProtocols = nullptr; + if (customProtocols) { + size_t customProtocolsSize = strlen(customProtocols) + 1; + this->customProtocols = static_cast(MO_MALLOC(getMemoryTag(), customProtocolsSize)); + if (!this->customProtocols) { + MO_DBG_ERR("OOM"); + return false; + } + snprintf(this->customProtocols, customProtocolsSize, "%s", customProtocols); + } -void DiagnosticsService::setOnUploadStatusInput(std::function uploadStatusInput) { - this->uploadStatusInput = uploadStatusInput; + this->onUploadUserData = user_data; + return true; } -void DiagnosticsService::setDiagnosticsReader(std::function diagnosticsReader, std::function onClose, std::shared_ptr filesystem) { +struct DiagnosticsReaderFtwData { + DiagnosticsService *diagService; + int ret; +}; - this->onUpload = [this, diagnosticsReader, onClose, filesystem] (const char *location, Timestamp &startTime, Timestamp &stopTime) -> bool { +bool DiagnosticsService::uploadDiagnostics() { - auto ftpClient = context.getFtpClient(); - if (!ftpClient) { - MO_DBG_ERR("FTP client not set"); - this->ftpUploadStatus = UploadStatus::UploadFailed; - return false; - } + MO_FREE(diagPreamble); + diagPreamble = nullptr; - 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; + diagPreamble = static_cast(MO_MALLOC(getMemoryTag(), MO_DIAG_PREAMBLE_SIZE)); + if (!diagPreamble) { + MO_DBG_ERR("OOM"); + cleanUploadData(); + return false; + } + diagPreambleLen = 0; + diagPreambleTransferred = 0; - diagReaderHasData = diagnosticsReader ? true : false; + 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; + MO_FREE(diagPostamble); + diagPostamble = nullptr; - auto& model = context.getModel(); + diagPostamble = static_cast(MO_MALLOC(getMemoryTag(), MO_DIAG_POSTAMBLE_SIZE)); + if (!diagPostamble) { + MO_DBG_ERR("OOM"); + cleanUploadData(); + return false; + } + diagPostambleLen = 0; + diagPostambleTransferred = 0; - auto cpModel = makeString(getMemoryTag()); - auto fwVersion = makeString(getMemoryTag()); + const char *cpModel = nullptr; + const char *fwVersion = nullptr; - if (auto bootService = model.getBootService()) { - if (auto cpCreds = bootService->getChargePointCredentials()) { - cpModel = (*cpCreds)["chargePointModel"] | "Charger"; - fwVersion = (*cpCreds)["firmwareVersion"] | ""; - } - } + if (auto bootService = context.getModelCommon().getBootService()) { + auto bnData = bootService->getBootNotificationData(); + cpModel = bnData.chargePointModel; + fwVersion = bnData.firmwareVersion; + } - char jsonDate [JSONDATE_LENGTH + 1]; - model.getClock().now().toJsonString(jsonDate, sizeof(jsonDate)); + char jsonDate [MO_JSONDATE_SIZE]; + if (!context.getClock().toJsonString(context.getClock().now(), jsonDate, sizeof(jsonDate))) { + MO_DBG_ERR("internal error"); + jsonDate[0] = '\0'; + } - int ret; + int ret; - ret = snprintf(diagPreamble, diagPreambleSize, - "### %s Hardware Diagnostics%s%s\n%s\n", - cpModel.c_str(), - fwVersion.empty() ? "" : " - v. ", fwVersion.c_str(), - jsonDate); + ret = snprintf(diagPreamble, MO_DIAG_PREAMBLE_SIZE, + "### %s Hardware Diagnostics%s%s\n%s\n", + cpModel ? cpModel : "Charger", + fwVersion ? " - v. " : "", fwVersion ? fwVersion : "", + 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; - } + if (ret < 0 || (size_t)ret >= MO_DIAG_PREAMBLE_SIZE) { + MO_DBG_ERR("snprintf: %i", ret); + cleanUploadData(); + return false; + } + + diagPreambleLen += (size_t)ret; + ret = 0; - diagPreambleLen += (size_t)ret; + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + //OCPP 1.6 specific diagnostics - 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; + auto& model = context.getModel16(); - ret = 0; + auto txSvc = model.getTransactionService(); + auto txSvcEvse1 = txSvc ? txSvc->getEvse(1) : nullptr; + auto txSvcEvse2 = txSvc && model.getNumEvseId() > 2 ? txSvc->getEvse(2) : nullptr; + auto txSvcEvse1Tx = txSvcEvse1 ? txSvcEvse1->getTransaction() : nullptr; + auto txSvcEvse2Tx = txSvcEvse2 ? txSvcEvse2->getTransaction() : nullptr; - if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { + auto availSvc = model.getAvailabilityService(); + auto availSvcEvse0 = availSvc ? availSvc->getEvse(0) : nullptr; + auto availSvcEvse1 = availSvc ? availSvc->getEvse(1) : nullptr; + auto availSvcEvse2 = availSvc ? availSvc->getEvse(2) : nullptr; + + if (ret >= 0 && (size_t)ret + diagPostambleLen < MO_DIAG_POSTAMBLE_SIZE) { diagPostambleLen += (size_t)ret; - ret = snprintf(diagPostamble + diagPostambleLen, diagPostambleSize - diagPostambleLen, + ret = snprintf(diagPostamble + diagPostambleLen, MO_DIAG_POSTAMBLE_SIZE - diagPostambleLen, "\n# OCPP" "\nclient_version=%s" "\nuptime=%lus" @@ -321,8 +667,6 @@ void DiagnosticsService::setDiagnosticsReader(std::functiongetStatus()) : "", - 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") : "", + context.getTicksMs() / 1000UL, + availSvcEvse0 ? "\nocpp_status_cId0=" : "", availSvcEvse0 ? mo_serializeChargePointStatus(availSvcEvse0->getStatus()) : "", + availSvcEvse1 ? "\nocpp_status_cId1=" : "", availSvcEvse1 ? mo_serializeChargePointStatus(availSvcEvse1->getStatus()) : "", + availSvcEvse2 ? "\nocpp_status_cId2=" : "", availSvcEvse2 ? mo_serializeChargePointStatus(availSvcEvse2->getStatus()) : "", + context.getConnection() && context.getConnection()->isConnected() ? "connected" : "unconnected", + txSvcEvse1 ? "\ncId1_hasTx=" : "", txSvcEvse1 ? (txSvcEvse1Tx ? "1" : "0") : "", + txSvcEvse1Tx ? "\ncId1_txActive=" : "", txSvcEvse1Tx ? (txSvcEvse1Tx->isActive() ? "1" : "0") : "", + txSvcEvse1Tx ? "\ncId1_txHasStarted=" : "", txSvcEvse1Tx ? (txSvcEvse1Tx->getStartSync().isRequested() ? "1" : "0") : "", + txSvcEvse1Tx ? "\ncId1_txHasStopped=" : "", txSvcEvse1Tx ? (txSvcEvse1Tx->getStopSync().isRequested() ? "1" : "0") : "", + txSvcEvse2 ? "\ncId2_hasTx=" : "", txSvcEvse2 ? (txSvcEvse2Tx ? "1" : "0") : "", + txSvcEvse2Tx ? "\ncId2_txActive=" : "", txSvcEvse2Tx ? (txSvcEvse2Tx->isActive() ? "1" : "0") : "", + txSvcEvse2Tx ? "\ncId2_txHasStarted=" : "", txSvcEvse2Tx ? (txSvcEvse2Tx->getStartSync().isRequested() ? "1" : "0") : "", + txSvcEvse2Tx ? "\ncId2_txHasStopped=" : "", txSvcEvse2Tx ? (txSvcEvse2Tx->getStopSync().isRequested() ? "1" : "0") : "", MO_ENABLE_CONNECTOR_LOCK, MO_ENABLE_FILE_INDEX, MO_ENABLE_V201 ); } + } + #endif //MO_ENABLE_V16 + + if (filesystem) { - if (filesystem) { + if (ret >= 0 && (size_t)ret + diagPostambleLen < MO_DIAG_POSTAMBLE_SIZE) { + diagPostambleLen += (size_t)ret; + ret = snprintf(diagPostamble + diagPostambleLen, MO_DIAG_POSTAMBLE_SIZE - diagPostambleLen, "\n# Filesystem\n"); + } - if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { + DiagnosticsReaderFtwData data; + data.diagService = this; + data.ret = ret; + + diagFileList.clear(); + + filesystem->ftw(filesystem->path_prefix, [] (const char *fname, void *user_data) -> int { + auto& data = *reinterpret_cast(user_data); + auto& diagPostambleLen = data.diagService->diagPostambleLen; + auto& diagPostamble = data.diagService->diagPostamble; + auto& diagFileList = data.diagService->diagFileList; + auto& ret = data.ret; + + if (ret >= 0 && (size_t)ret + diagPostambleLen < MO_DIAG_POSTAMBLE_SIZE) { diagPostambleLen += (size_t)ret; - ret = snprintf(diagPostamble + diagPostambleLen, diagPostambleSize - diagPostambleLen, "\n# Filesystem\n"); + ret = snprintf(diagPostamble + diagPostambleLen, MO_DIAG_POSTAMBLE_SIZE - diagPostambleLen, "%s\n", fname); } + diagFileList.emplace_back(fname); + return 0; + }, reinterpret_cast(&data)); - 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; - }); + ret = data.ret; - MO_DBG_DEBUG("discovered %zu files", diagFileList.size()); - } + 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; + if (ret >= 0 && (size_t)ret + diagPostambleLen < MO_DIAG_POSTAMBLE_SIZE) { + diagPostambleLen += (size_t)ret; + } else { + char errMsg [64]; + auto errLen = snprintf(errMsg, sizeof(errMsg), "\n[Diagnostics cut]\n"); + size_t ellipseStart = std::min(MO_DIAG_POSTAMBLE_SIZE - (size_t)errLen - 1, diagPostambleLen); + auto ret2 = snprintf(diagPostamble + ellipseStart, MO_DIAG_POSTAMBLE_SIZE - ellipseStart, "%s", errMsg); + diagPostambleLen += (size_t)ret2; + } + + if (!startFtpUpload()) { + cleanUploadData(); + return false; + } + + return true; +} + +bool DiagnosticsService::uploadSecurityLog() { + + if (!filesystem) { + MO_DBG_ERR("depends on filesystem"); + cleanUploadData(); + return false; + } + + MO_FREE(diagPreamble); + diagPreamble = nullptr; + + diagPreamble = static_cast(MO_MALLOC(getMemoryTag(), MO_DIAG_PREAMBLE_SIZE)); + if (!diagPreamble) { + MO_DBG_ERR("OOM"); + cleanUploadData(); + return false; + } + diagPreambleLen = 0; + diagPreambleTransferred = 0; + + diagReaderHasData = false; + + const char *cpModel = nullptr; + const char *fwVersion = nullptr; + + if (auto bootService = context.getModelCommon().getBootService()) { + auto bnData = bootService->getBootNotificationData(); + cpModel = bnData.chargePointModel; + fwVersion = bnData.firmwareVersion; + } + + char jsonDate [MO_JSONDATE_SIZE]; + if (!context.getClock().toJsonString(context.getClock().now(), jsonDate, sizeof(jsonDate))) { + MO_DBG_ERR("internal error"); + jsonDate[0] = '\0'; + } + + int ret; + + ret = snprintf(diagPreamble, MO_DIAG_PREAMBLE_SIZE, + "### %s Security Log%s%s\n%s\n", + cpModel ? cpModel : "Charger", + fwVersion ? " - v. " : "", fwVersion ? fwVersion : "", + jsonDate); + + if (ret < 0 || (size_t)ret >= MO_DIAG_PREAMBLE_SIZE) { + MO_DBG_ERR("snprintf: %i", ret); + cleanUploadData(); + return false; + } + + diagPreambleLen += (size_t)ret; + + //load Security Log index structure from flash (fileNrBegin denotes first file, fileNrEnd denots index after last file) + + unsigned int fileNrBegin = 0, fileNrEnd = 0; + + if (!FilesystemUtils::loadRingIndex(filesystem, MO_SECLOG_FN_PREFIX, MO_SECLOG_INDEX_MAX, &fileNrBegin, &fileNrEnd)) { + MO_DBG_ERR("failed to load security event log"); + cleanUploadData(); + return false; + } + + DiagnosticsReaderFtwData data; + data.diagService = this; + + diagFileList.clear(); + + int ret_ftw = filesystem->ftw(filesystem->path_prefix, [] (const char *fname, void *user_data) -> int { + auto& data = *reinterpret_cast(user_data); + auto& diagFileList = data.diagService->diagFileList; + + if (!strncmp(fname, MO_SECLOG_FN_PREFIX, sizeof(MO_SECLOG_FN_PREFIX) - 1)) { + //fname starts with "slog-" + diagFileList.emplace_back(fname); } - 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; - } + return 0; + }, reinterpret_cast(&data)); - while (written < size && diagReaderHasData && diagnosticsReader) { - size_t writeLen = diagnosticsReader((char*)buf + written, size - written); - if (writeLen == 0) { - diagReaderHasData = false; - } - written += writeLen; - } + if (ret_ftw != 0) { + MO_DBG_ERR("ftw: %i", ret_ftw); + cleanUploadData(); + return false; + } - if (written < size && diagPostambleTransferred < diagPostambleLen) { - size_t writeLen = std::min(size - written, diagPostambleLen - diagPostambleTransferred); - memcpy(buf + written, diagPostamble + diagPostambleTransferred, writeLen); - diagPostambleTransferred += writeLen; - written += writeLen; + MO_DBG_DEBUG("discovered %zu files", diagFileList.size()); + + if (!startFtpUpload()) { + cleanUploadData(); + return false; + } + + return true; +} + +bool DiagnosticsService::startFtpUpload() { + + size_t locationLen = strlen(location); + size_t filenameLen = strlen(filename); + size_t urlSize = locationLen + + 1 + //separating '/' between location and filename + filenameLen + + 1; //terminating 0 + + char *url = static_cast(MO_MALLOC(getMemoryTag(), urlSize)); + if (!url) { + MO_DBG_ERR("OOM"); + return false; + } + + if (locationLen > 0 && filenameLen > 0 && location[locationLen - 1] != '/') { + snprintf(url, urlSize, "%s/%s", location, filename); + } else { + snprintf(url, urlSize, "%s%s", location, filename); + } + + this->ftpUpload = ftpClient->postFile(url, + [this] (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, diagnosticsUserData); + if (writeLen == 0) { + diagReaderHasData = false; } + written += writeLen; + } - while (written < size && !diagFileList.empty() && filesystem) { + if (written < size && diagPostambleTransferred < diagPostambleLen) { + size_t writeLen = std::min(size - written, diagPostambleLen - diagPostambleTransferred); + memcpy(buf + written, diagPostamble + diagPostambleTransferred, writeLen); + diagPostambleTransferred += writeLen; + written += writeLen; + } - 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(); - continue; - } + while (written < size && !diagFileList.empty() && filesystem) { - 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(); - 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; - } + char fpath [MO_MAX_PATH_SIZE]; + if (!FilesystemUtils::printPath(filesystem, fpath, sizeof(fpath), diagFileList.back().c_str())) { + MO_DBG_ERR("path error: %s", diagFileList.back().c_str()); + diagFileList.pop_back(); + continue; + } - file->seek(diagFilesBackTransferred); - size_t writeLen = file->read((char*)buf + written, size - written); + if (auto file = filesystem->open(fpath, "r")) { - if (writeLen < size - written) { + 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", writeLen); diagFileList.pop_back(); + filesystem->close(file); + continue; } - written += writeLen; - } else { - MO_DBG_ERR("could not open file: %s", fpath); - diagFileList.pop_back(); + 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); + filesystem->close(file); + return written; + } + + memcpy(buf + written, fileHeading, (size_t)writeLen); + written += (size_t)writeLen; } - } - 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; + filesystem->seek(file, diagFilesBackTransferred); + size_t writeLen = filesystem->read(file, (char*)buf + written, size - written); + + if (writeLen < size - written) { + diagFileList.pop_back(); + } + written += writeLen; } else { - MO_DBG_INFO("FTP upload failure (%i)", reason); - this->ftpUploadStatus = UploadStatus::UploadFailed; + MO_DBG_ERR("could not open file: %s", fpath); + diagFileList.pop_back(); } + } - MO_FREE(diagPreamble); - MO_FREE(diagPostamble); - diagFileList.clear(); + MO_DBG_DEBUG("upload diag chunk (%zuB)", written); + return written; + }, + [this] (MO_FtpCloseReason reason) -> void { + if (reason == MO_FtpCloseReason_Success) { + MO_DBG_INFO("FTP upload success"); + this->ftpUploadStatus = MO_UploadLogStatus_Uploaded; + } else { + MO_DBG_INFO("FTP upload failure (%i)", reason); + this->ftpUploadStatus = MO_UploadLogStatus_UploadFailure; + } - if (onClose) { - onClose(); - } - }); + if (diagnosticsOnClose) { + diagnosticsOnClose(diagnosticsUserData); + } + }); - if (this->ftpUpload) { - this->ftpUploadStatus = UploadStatus::NotUploaded; - return true; - } else { - this->ftpUploadStatus = UploadStatus::UploadFailed; - return false; - } - }; + MO_FREE(url); + url = nullptr; - this->uploadStatusInput = [this] () { - return this->ftpUploadStatus; - }; + if (!this->ftpUpload) { + MO_DBG_ERR("OOM"); + return false; + } + + this->ftpUploadStatus = MO_UploadLogStatus_Uploading; + return true; +} + +void DiagnosticsService::cleanUploadData() { + ftpUpload.reset(); + + MO_FREE(diagPreamble); + diagPreamble = nullptr; + diagPreambleLen = 0; + diagPreambleTransferred = 0; + + diagReaderHasData = false; + + MO_FREE(diagPostamble); + diagPostamble = nullptr; + diagPostambleLen = 0; + diagPostambleTransferred = 0; + + diagFileList.clear(); + diagFilesBackTransferred = 0; +} + +void DiagnosticsService::cleanGetLogData() { + logType = MO_LogType_UNDEFINED; + requestId = -1; + MO_FREE(location); + location = nullptr; + MO_FREE(filename); + filename = nullptr; + retries = -1; + retryInterval = 0; + oldestTimestamp = Timestamp(); + latestTimestamp = Timestamp(); } void DiagnosticsService::setFtpServerCert(const char *cert) { this->ftpServerCert = cert; } -#if !defined(MO_CUSTOM_DIAGNOSTICS) +} //namespace MicroOcpp + +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS -#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS +#if MO_USE_DIAGNOSTICS == MO_DIAGNOSTICS_BUILTIN_MBEDTLS_ESP32 #include "esp_heap_caps.h" #include +namespace MicroOcpp { + 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; - } +size_t defaultDiagnosticsReader(char *buf, size_t size, void*) { + 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; - }, [] () { - g_diagsSent = false; - }, - filesystem); - - return diagService; + } + return (size_t)ret; + } + return 0; } -#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)); +void defaultDiagnosticsOnClose(void*) { + g_diagsSent = false; +}; - diagService->setDiagnosticsReader(nullptr, nullptr, filesystem); //report the built-in MO defaults - - return diagService; -} +} //namespace MicroOcpp -#endif //MO_PLATFORM -#endif //!defined(MO_CUSTOM_DIAGNOSTICS) +#endif //MO_USE_DIAGNOSTICS diff --git a/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.h b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.h index ed3ae3c0..c566197b 100644 --- a/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.h +++ b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.h @@ -9,41 +9,81 @@ #include #include #include +#include #include -#include +#include +#include -namespace MicroOcpp { +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS -enum class UploadStatus { - NotUploaded, - Uploaded, - UploadFailed -}; +#define MO_DIAGNOSTICS_CUSTOM 0 +#define MO_DIAGNOSTICS_BUILTIN_MBEDTLS 1 +#define MO_DIAGNOSTICS_BUILTIN_MBEDTLS_ESP32 2 + +#ifndef MO_USE_DIAGNOSTICS +#ifdef MO_CUSTOM_DIAGNOSTICS +#define MO_USE_DIAGNOSTICS MO_DIAGNOSTICS_CUSTOM +#elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS +#define MO_USE_DIAGNOSTICS MO_DIAGNOSTICS_BUILTIN_MBEDTLS_ESP32 +#elif MO_ENABLE_MBEDTLS +#define MO_USE_DIAGNOSTICS MO_DIAGNOSTICS_BUILTIN_MBEDTLS +#else +#define MO_USE_DIAGNOSTICS MO_DIAGNOSTICS_CUSTOM +#endif +#endif //MO_USE_DIAGNOSTICS + +namespace MicroOcpp { class Context; class Request; -class FilesystemAdapter; +class FtpClient; class DiagnosticsService : public MemoryManaged { private: Context& context; - - String location; - unsigned int retries = 0; - unsigned int retryInterval = 0; - Timestamp startTime; - Timestamp stopTime; + //platform requirements for built-in logs upload + MO_FilesystemAdapter *filesystem = nullptr; + FtpClient *ftpClient = nullptr; + + //custom logs upload (bypasses built-in implementation) + MO_GetLogStatus (*onUpload)(MO_LogType type, const char *location, const char *oldestTimestamp, const char *latestTimestamp, char filenameOut[MO_GETLOG_FNAME_SIZE], void *onUploadUserData) = nullptr; + MO_UploadLogStatus (*onUploadStatusInput)(void *onUploadUserData) = nullptr; + void *onUploadUserData = nullptr; + char *customProtocols = nullptr; + + int ocppVersion = -1; + + size_t (*diagnosticsReader)(char *buf, size_t size, void *diagnosticsReaderUserData) = nullptr; + void(*diagnosticsOnClose)(void *diagnosticsReaderUserData) = nullptr; + void *diagnosticsUserData = nullptr; + + //override filename for built-in logs upload + bool (*refreshFilename)(MO_LogType type, char filenameOut[MO_GETLOG_FNAME_SIZE], void *refreshFilenameUserData) = nullptr; + void *refreshFilenameUserData = nullptr; + + //server request data + MO_LogType logType = MO_LogType_UNDEFINED; + int requestId = -1; + char *location = nullptr; + char *filename = nullptr; + int retries = -1; + int retryInterval = 0; + Timestamp oldestTimestamp; + Timestamp latestTimestamp; + + //internal status Timestamp nextTry; - - std::function refreshFilename; - std::function onUpload; - std::function uploadStatusInput; bool uploadIssued = false; bool uploadFailure = false; +#if MO_ENABLE_V16 + bool use16impl = false; //if to use original 1.6 implementation instead of Security Whitepaper +#endif //MO_ENABLE_V16 + bool runCustomUpload = false; + //built-in logs upload data std::unique_ptr ftpUpload; - UploadStatus ftpUploadStatus = UploadStatus::NotUploaded; + MO_UploadLogStatus ftpUploadStatus = MO_UploadLogStatus_Idle; const char *ftpServerCert = nullptr; char *diagPreamble = nullptr; size_t diagPreambleLen = 0; @@ -55,25 +95,21 @@ class DiagnosticsService : public MemoryManaged { Vector diagFileList; size_t diagFilesBackTransferred = 0; - std::unique_ptr getDiagnosticsStatusNotification(); + MO_UploadLogStatus lastReportedUploadStatus = MO_UploadLogStatus_Idle; + #if MO_ENABLE_V16 + v16::DiagnosticsStatus lastReportedUploadStatus16 = v16::DiagnosticsStatus::Idle; + #endif //MO_ENABLE_V16 - Ocpp16::DiagnosticsStatus lastReportedStatus = Ocpp16::DiagnosticsStatus::Idle; + bool uploadDiagnostics(); //prepare diags and start FTP upload + bool uploadSecurityLog(); //prepare sec logs and start FTP upload + bool startFtpUpload(); //do FTP upload + void cleanUploadData(); //clean data which is set per FTP upload attempt + void cleanGetLogData(); //clean data which is set per GetLog request 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 @@ -92,32 +128,45 @@ class DiagnosticsService : public MemoryManaged { * * 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 setDiagnosticsReader(size_t (*diagnosticsReader)(char *buf, size_t size, void *user_data), void(*onClose)(void *user_data), void *user_data); void setFtpServerCert(const char *cert); //zero-copy mode, i.e. cert must outlive MO - void setOnUpload(std::function onUpload); - - void setOnUploadStatusInput(std::function uploadStatusInput); -}; + //override default logs file name ("diagnostics.log"). Only applies to built-in diagnostics upload + void setRefreshFilename(bool (*refreshFilename)(MO_LogType type, char filenameOut[MO_GETLOG_FNAME_SIZE], void *user_data), void *user_data); -} //end namespace MicroOcpp + /* Bypass built-in upload + * `onUpload`: hook function for diagnostics upload + * `onUploadStatusInput`: status getter cb which MO will execute to control custom onUpload and notify server + * `protocols`: CSV list for supported protocols. Possible values: FTP, FTPS, HTTP, HTTPS, SFTP + * `user_data`: implementer-defined pointer */ + bool setOnUpload( + MO_GetLogStatus (*onUpload)(MO_LogType type, const char *location, const char *oldestTimestamp, const char *latestTimestamp, char filenameOut[MO_GETLOG_FNAME_SIZE], void *user_data), + MO_UploadLogStatus (*onUploadStatusInput)(void *user_data), + const char *protocols, + void *user_data); -#if !defined(MO_CUSTOM_DIAGNOSTICS) + bool setup(); -#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS - -namespace MicroOcpp { -std::unique_ptr makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem); -} + void loop(); -#elif MO_ENABLE_MBEDTLS +#if MO_ENABLE_V16 + //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 + bool requestDiagnosticsUpload(const char *location, unsigned int retries, int retryInterval, Timestamp startTime, Timestamp stopTime, char filenameOut[MO_GETLOG_FNAME_SIZE]); +#endif //MO_ENABLE_V16 -namespace MicroOcpp { -std::unique_ptr makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem); -} + MO_GetLogStatus getLog(MO_LogType type, int requestId, int retries, int retryInterval, const char *remoteLocation, Timestamp oldestTimestamp, Timestamp latestTimestamp, char filenameOut[MO_GETLOG_FNAME_SIZE]); -#endif //MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS -#endif //!defined(MO_CUSTOM_DIAGNOSTICS) + //for TriggerMessage + int getRequestId(); + MO_UploadLogStatus getUploadStatus(); + #if MO_ENABLE_V16 + v16::DiagnosticsStatus getUploadStatus16(); + #endif //MO_ENABLE_V16 +}; +} //namespace MicroOcpp +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS #endif diff --git a/src/MicroOcpp/Model/Diagnostics/DiagnosticsStatus.h b/src/MicroOcpp/Model/Diagnostics/DiagnosticsStatus.h deleted file mode 100644 index 632708a4..00000000 --- a/src/MicroOcpp/Model/Diagnostics/DiagnosticsStatus.h +++ /dev/null @@ -1,20 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp b/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp index 7eb006e1..fa8ff6f3 100644 --- a/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp +++ b/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp @@ -3,12 +3,13 @@ // MIT License #include -#include +#include #include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include @@ -17,31 +18,109 @@ #include #include +#if MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT + //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; +namespace MicroOcpp { +namespace v16 { + +#if MO_USE_FW_UPDATER != MO_FW_UPDATER_CUSTOM +bool setupDefaultFwUpdater(FirmwareService *fwService); +#endif -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);}); +FirmwareService::FirmwareService(Context& context) : MemoryManaged("v16.Firmware.FirmwareService"), context(context), clock(context.getClock()) { - //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 +FirmwareService::~FirmwareService() { + MO_FREE(buildNumber); + buildNumber = nullptr; + MO_FREE(location); + location = nullptr; +} + +bool FirmwareService::setup() { + + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("setup failure"); + return false; + } + + auto previousBuildNumberString = configService->declareConfiguration("BUILD_NUMBER", "", MO_KEYVALUE_FN, Mutability::None); + if (!previousBuildNumberString) { + MO_DBG_ERR("declareConfiguration"); + return false; + } + + auto bootSvc = context.getModel16().getBootService(); + if (!bootSvc) { + MO_DBG_ERR("setup failure"); + return false; + } + + MO_BootNotificationData bnData = bootSvc->getBootNotificationData(); + + const char *fwIdentifier = buildNumber ? buildNumber : bnData.firmwareVersion; + if (fwIdentifier && strcmp(fwIdentifier, previousBuildNumberString->getString())) { + + MO_DBG_INFO("Firmware updated. Previous build number: %s, new build number: %s", previousBuildNumberString->getString(), fwIdentifier); + notifyFirmwareUpdate = true; //will send this during `loop()` + } + MO_FREE(buildNumber); + buildNumber = nullptr; + + context.getMessageService().registerOperation("UpdateFirmware", [] (Context& context) -> Operation* { + return new v16::UpdateFirmware(context, *context.getModel16().getFirmwareService());}); + + #if MO_ENABLE_MOCK_SERVER + context.getMessageService().registerOperation("FirmwareStatusNotification", nullptr, nullptr, nullptr); + #endif //MO_ENABLE_MOCK_SERVER + + auto rcService = context.getModel16().getRemoteControlService(); + if (!rcService) { + MO_DBG_ERR("initialization error"); + return false; + } + + rcService->addTriggerMessageHandler("FirmwareStatusNotification", [] (Context& context) -> Operation* { + return new v16::FirmwareStatusNotification(context.getModel16().getFirmwareService()->getFirmwareStatus());}); + +#if MO_USE_FW_UPDATER != MO_FW_UPDATER_CUSTOM + if (!setupDefaultFwUpdater(this)) { + return false; + } +#endif //MO_CUSTOM_UPDATER + + return true; +} + +bool FirmwareService::setBuildNumber(const char *buildNumber) { + if (!buildNumber) { + MO_DBG_ERR("invalid arg"); + return false; + } + MO_FREE(this->buildNumber); + this->buildNumber = nullptr; + size_t size = strlen(buildNumber) + 1; + this->buildNumber = static_cast(MO_MALLOC(getMemoryTag(), size)); + if (!this->buildNumber) { + MO_DBG_ERR("OOM"); + return false; + } + auto ret = snprintf(this->buildNumber, size, "%s", buildNumber); + if (ret < 0 || (size_t)ret >= size) { + MO_DBG_ERR("snprintf: %i", ret); + MO_FREE(this->buildNumber); + this->buildNumber = nullptr; + return false; + } + + return true; //CS will be notified } void FirmwareService::loop() { @@ -61,30 +140,40 @@ void FirmwareService::loop() { auto notification = getFirmwareStatusNotification(); if (notification) { - context.initiateRequest(std::move(notification)); + context.getMessageService().sendRequest(std::move(notification)); } - if (mocpp_tick_ms() - timestampTransition < delayTransition) { + int32_t dtTransition; + if (!clock.delta(clock.getUptime(), timestampTransition, dtTransition)) { + dtTransition = -1; + } + + if (dtTransition < delayTransition) { return; } - auto& timestampNow = context.getModel().getClock().now(); - if (retries > 0 && timestampNow >= retreiveDate) { + int32_t dtRetreiveDate; + if (!clock.delta(clock.now(), retreiveDate, dtRetreiveDate)) { + dtRetreiveDate = 0; + } + + if (retries > 0 && dtRetreiveDate >= 0) { if (stage == UpdateStage::Idle) { MO_DBG_INFO("Start update"); - if (context.getModel().getNumConnectors() > 0) { - auto cp = context.getModel().getConnector(0); - cp->setAvailabilityVolatile(false); + auto availSvc = context.getModel16().getAvailabilityService(); + auto availSvcCp = availSvc ? availSvc->getEvse(0) : nullptr; + if (availSvcCp) { + availSvcCp->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 + timestampTransition = clock.getUptime(); + delayTransition = 2; //delay between state "Downloading" and actually starting the download return; } } @@ -93,9 +182,9 @@ void FirmwareService::loop() { 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 + onDownload(location ? location : ""); + timestampTransition = clock.getUptime(); + delayTransition = downloadStatusInput ? 1 : 30; //give the download at least 30s return; } } @@ -110,13 +199,13 @@ void FirmwareService::loop() { stage = UpdateStage::AfterDownload; } else if (downloadStatusInput() == DownloadStatus::DownloadFailed) { MO_DBG_INFO("Download timeout or failed"); - retreiveDate = timestampNow; - retreiveDate += retryInterval; + retreiveDate = clock.now(); + clock.add(retreiveDate, retryInterval); retries--; resetStage(); - timestampTransition = mocpp_tick_ms(); - delayTransition = 10000; + timestampTransition = clock.getUptime(); + delayTransition = 10; } return; } else { @@ -127,9 +216,10 @@ void FirmwareService::loop() { 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()) { + for (unsigned int evseId = 0; evseId < context.getModel16().getNumEvseId(); evseId++) { + auto txSvc = context.getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + if (txSvcEvse && txSvcEvse->getTransaction() && txSvcEvse->getTransaction()->isRunning()) { ongoingTx = true; break; } @@ -141,8 +231,8 @@ void FirmwareService::loop() { } else { stage = UpdateStage::AwaitInstallation; } - timestampTransition = mocpp_tick_ms(); - delayTransition = 2000; + timestampTransition = clock.getUptime(); + delayTransition = 2; installationIssued = true; } @@ -154,10 +244,10 @@ void FirmwareService::loop() { stage = UpdateStage::Installing; if (onInstall) { - onInstall(location.c_str()); //may restart the device on success + onInstall(location ? location : ""); //may restart the device on success - timestampTransition = mocpp_tick_ms(); - delayTransition = installationStatusInput ? 1000 : 120 * 1000; + timestampTransition = clock.getUptime(); + delayTransition = installationStatusInput ? 1 : 120; } return; } @@ -171,16 +261,17 @@ void FirmwareService::loop() { resetStage(); retries = 0; //End of update routine stage = UpdateStage::Installed; - location.clear(); + MO_FREE(location); + location = nullptr; } else if (installationStatusInput() == InstallationStatus::InstallationFailed) { MO_DBG_INFO("Installation timeout or failed! Retry"); - retreiveDate = timestampNow; - retreiveDate += retryInterval; + retreiveDate = clock.now(); + clock.add(retreiveDate, retryInterval); retries--; resetStage(); - timestampTransition = mocpp_tick_ms(); - delayTransition = 10000; + timestampTransition = clock.getUptime(); + delayTransition = 10; } return; } else { @@ -189,7 +280,8 @@ void FirmwareService::loop() { resetStage(); stage = UpdateStage::Installed; retries = 0; //End of update routine - location.clear(); + MO_FREE(location); + location = nullptr; return; } } @@ -199,45 +291,71 @@ void FirmwareService::loop() { retries = 0; resetStage(); stage = UpdateStage::InternalError; - location.clear(); + MO_FREE(location); + location = nullptr; } } -void FirmwareService::scheduleFirmwareUpdate(const char *location, Timestamp retreiveDate, unsigned int retries, unsigned int retryInterval) { +bool FirmwareService::scheduleFirmwareUpdate(const char *location, Timestamp retreiveDate, unsigned int retries, unsigned int retryInterval) { + + if (!location) { + MO_DBG_ERR("invalid arg"); + return false; + } if (!onDownload && !onInstall) { MO_DBG_ERR("FW service not configured"); stage = UpdateStage::InternalError; //will send "InstallationFailed" and not proceed with update - return; + return false; + } + + MO_FREE(this->location); + this->location = nullptr; + size_t len = strlen(location); + this->location = static_cast(MO_MALLOC(getMemoryTag(), len)); + if (!this->location) { + MO_DBG_ERR("OOM"); + return false; + } + auto ret = snprintf(this->location, len, "%s", location); + if (ret < 0 || (size_t)ret >= len) { + MO_DBG_ERR("snprintf: %i", ret); + MO_FREE(this->location); + this->location = nullptr; + return false; } - 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(); + this->retreiveDate = clock.now(); } - char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; - this->retreiveDate.toJsonString(dbuf, JSONDATE_LENGTH + 1); + char dbuf [MO_JSONDATE_SIZE] = {'\0'}; + if (!clock.toJsonString(this->retreiveDate, dbuf, sizeof(dbuf))) { + MO_DBG_ERR("internal error"); + dbuf[0] = '\0'; + } MO_DBG_INFO("Scheduled FW update!\n" \ " location = %s\n" \ " retrieveDate = %s\n" \ " retries = %u" \ ", retryInterval = %u", - this->location.c_str(), + location ? location : "", dbuf, this->retries, this->retryInterval); - timestampTransition = mocpp_tick_ms(); - delayTransition = 1000; + timestampTransition = clock.getUptime(); + delayTransition = 1; resetStage(); + + return true; } FirmwareStatus FirmwareService::getFirmwareStatus() { @@ -245,7 +363,7 @@ FirmwareStatus FirmwareService::getFirmwareStatus() { if (stage == UpdateStage::Installed) { return FirmwareStatus::Installed; } else if (stage == UpdateStage::InternalError) { - return FirmwareStatus::InstallationFailed; + return FirmwareStatus::InstallationFailed; } if (installationIssued) { @@ -259,7 +377,7 @@ FirmwareStatus FirmwareService::getFirmwareStatus() { if (onInstall != nullptr) return FirmwareStatus::Installing; } - + if (downloadIssued) { if (downloadStatusInput != nullptr) { if (downloadStatusInput() == DownloadStatus::Downloaded) { @@ -279,30 +397,20 @@ 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(); + if (notifyFirmwareUpdate) { + notifyFirmwareUpdate = false; - lastReportedStatus = FirmwareStatus::Installed; - auto fwNotificationMsg = new Ocpp16::FirmwareStatusNotification(lastReportedStatus); - auto fwNotification = makeRequest(fwNotificationMsg); - return fwNotification; - } + lastReportedStatus = FirmwareStatus::Installed; + auto fwNotificationMsg = new v16::FirmwareStatusNotification(lastReportedStatus); + auto fwNotification = makeRequest(context, fwNotificationMsg); + return fwNotification; } if (getFirmwareStatus() != lastReportedStatus) { lastReportedStatus = getFirmwareStatus(); if (lastReportedStatus != FirmwareStatus::Idle) { - auto fwNotificationMsg = new Ocpp16::FirmwareStatusNotification(lastReportedStatus); - auto fwNotification = makeRequest(fwNotificationMsg); + auto fwNotificationMsg = new v16::FirmwareStatusNotification(lastReportedStatus); + auto fwNotification = makeRequest(context, fwNotificationMsg); return fwNotification; } } @@ -374,21 +482,20 @@ 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 +} //namespace v16 +} //namespace MicroOcpp -#include +#if MO_USE_FW_UPDATER == MO_FW_UPDATER_BUILTIN_ESP32 -std::unique_ptr MicroOcpp::makeDefaultFirmwareService(Context& context) { - std::unique_ptr fwService = std::unique_ptr(new FirmwareService(context)); - auto ftServicePtr = fwService.get(); +#include +bool MicroOcpp::v16::setupDefaultFwUpdater(MicroOcpp::v16::FirmwareService *fwService) { fwService->setDownloadFileWriter( - [ftServicePtr] (const unsigned char *data, size_t size) -> size_t { + [fwService] (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;}); + fwService->setInstallationStatusInput([](){return InstallationStatus::NotInstalled;}); auto ret = Update.begin(); if (!ret) { @@ -429,16 +536,16 @@ std::unique_ptr MicroOcpp::makeDefaultFirmwareService(Context& } }); - fwService->setOnInstall([ftServicePtr] (const char *location) { + fwService->setOnInstall([fwService] (const char *location) { if (Update.isRunning() && Update.end(true)) { MO_DBG_DEBUG("update success"); - ftServicePtr->setInstallationStatusInput([](){return InstallationStatus::Installed;}); + fwService->setInstallationStatusInput([](){return InstallationStatus::Installed;}); ESP.restart(); } else { MO_DBG_ERR("update failed"); - ftServicePtr->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); + fwService->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); } return true; @@ -448,19 +555,17 @@ std::unique_ptr MicroOcpp::makeDefaultFirmwareService(Context& return InstallationStatus::NotInstalled; }); - return fwService; + return true; } -#elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP8266) +#elif MO_USE_FW_UPDATER == MO_FW_UPDATER_BUILTIN_ESP8266 #include -std::unique_ptr MicroOcpp::makeDefaultFirmwareService(Context& context) { - std::unique_ptr fwService = std::unique_ptr(new FirmwareService(context)); - auto fwServicePtr = fwService.get(); +bool MicroOcpp::v16::setupDefaultFwUpdater(MicroOcpp::v16::FirmwareService *fwService) { + + fwService->setOnInstall([fwService] (const char *location) { - 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; @@ -474,15 +579,15 @@ std::unique_ptr MicroOcpp::makeDefaultFirmwareService(Context& switch (ret) { case HTTP_UPDATE_FAILED: - fwServicePtr->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); + fwService->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;}); + fwService->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); MO_DBG_WARN("HTTP_UPDATE_NO_UPDATES"); break; case HTTP_UPDATE_OK: - fwServicePtr->setInstallationStatusInput([](){return InstallationStatus::Installed;}); + fwService->setInstallationStatusInput([](){return InstallationStatus::Installed;}); MO_DBG_INFO("HTTP_UPDATE_OK"); ESP.restart(); break; @@ -495,8 +600,9 @@ std::unique_ptr MicroOcpp::makeDefaultFirmwareService(Context& return InstallationStatus::NotInstalled; }); - return fwService; + return true; } -#endif //MO_PLATFORM -#endif //!defined(MO_CUSTOM_UPDATER) +#endif //MO_USE_FW_UPDATER + +#endif //MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT diff --git a/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.h b/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.h index 5f4671f2..fc1a2e96 100644 --- a/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.h +++ b/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.h @@ -8,14 +8,35 @@ #include #include -#include +#include #include #include #include #include +#include + +#define MO_FW_UPDATER_CUSTOM 0 +#define MO_FW_UPDATER_BUILTIN_ESP32 1 +#define MO_FW_UPDATER_BUILTIN_ESP8266 2 + +#ifndef MO_USE_FW_UPDATER +#ifdef MO_CUSTOM_UPDATER +#define MO_USE_FW_UPDATER MO_FW_UPDATER_CUSTOM +#elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS +#define MO_USE_FW_UPDATER MO_FW_UPDATER_BUILTIN_ESP32 +#elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP8266) +#define MO_USE_FW_UPDATER MO_FW_UPDATER_BUILTIN_ESP8266 +#endif +#endif //MO_USE_FW_UPDATER + +#if MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT namespace MicroOcpp { +class Context; +class Clock; +class Request; + enum class DownloadStatus { NotDownloaded, // == before download or during download Downloaded, @@ -28,15 +49,15 @@ enum class InstallationStatus { InstallationFailed }; -class Context; -class Request; +namespace v16 { class FirmwareService : public MemoryManaged { private: Context& context; - - std::shared_ptr previousBuildNumberString; - String buildNumber; + Clock& clock; + + char *buildNumber = nullptr; + bool notifyFirmwareUpdate = false; std::function downloadStatusInput; bool downloadIssued = false; @@ -48,10 +69,9 @@ class FirmwareService : public MemoryManaged { std::function installationStatusInput; bool installationIssued = false; - Ocpp16::FirmwareStatus lastReportedStatus = Ocpp16::FirmwareStatus::Idle; - bool checkedSuccessfulFwUpdate = false; + FirmwareStatus lastReportedStatus = FirmwareStatus::Idle; - String location; + char *location = nullptr; Timestamp retreiveDate; unsigned int retries = 0; unsigned int retryInterval = 0; @@ -59,8 +79,8 @@ class FirmwareService : public MemoryManaged { std::function onDownload; std::function onInstall; - unsigned long delayTransition = 0; - unsigned long timestampTransition = 0; + int32_t delayTransition = 0; + Timestamp timestampTransition; enum class UpdateStage { Idle, @@ -80,13 +100,9 @@ class FirmwareService : public MemoryManaged { public: FirmwareService(Context& context); - void setBuildNumber(const char *buildNumber); + ~FirmwareService(); - void loop(); - - void scheduleFirmwareUpdate(const char *location, Timestamp retreiveDate, unsigned int retries = 1, unsigned int retryInterval = 0); - - Ocpp16::FirmwareStatus getFirmwareStatus(); + bool setBuildNumber(const char *buildNumber); /* * Sets the firmware writer. During the UpdateFirmware process, MO will use an FTP client to download the firmware and forward @@ -94,7 +110,7 @@ class FirmwareService : public MemoryManaged { * 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); @@ -111,25 +127,17 @@ class FirmwareService : public MemoryManaged { void setOnInstall(std::function onInstall); void setInstallationStatusInput(std::function installationStatusInput); -}; -} //endif namespace MicroOcpp + bool setup(); -#if !defined(MO_CUSTOM_UPDATER) - -#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS - -namespace MicroOcpp { -std::unique_ptr makeDefaultFirmwareService(Context& context); -} + void loop(); -#elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP8266) + bool scheduleFirmwareUpdate(const char *location, Timestamp retreiveDate, unsigned int retries = 1, unsigned int retryInterval = 0); -namespace MicroOcpp { -std::unique_ptr makeDefaultFirmwareService(Context& context); -} - -#endif //MO_PLATFORM -#endif //!defined(MO_CUSTOM_UPDATER) + FirmwareStatus getFirmwareStatus(); +}; +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT #endif diff --git a/src/MicroOcpp/Model/FirmwareManagement/FirmwareStatus.h b/src/MicroOcpp/Model/FirmwareManagement/FirmwareStatus.h index 3c8c7c2c..725b8093 100644 --- a/src/MicroOcpp/Model/FirmwareManagement/FirmwareStatus.h +++ b/src/MicroOcpp/Model/FirmwareManagement/FirmwareStatus.h @@ -5,8 +5,12 @@ #ifndef MO_FIRMWARE_STATUS #define MO_FIRMWARE_STATUS +#include + +#if MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT + namespace MicroOcpp { -namespace Ocpp16 { +namespace v16 { enum class FirmwareStatus { Downloaded, @@ -18,6 +22,7 @@ enum class FirmwareStatus { Installed }; -} -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT #endif diff --git a/src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp b/src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp index 4ebab502..5254c253 100644 --- a/src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp +++ b/src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp @@ -1,37 +1,146 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #include -#include +#include #include -#include +#include +#include +#include #include #include +#include + +#if MO_ENABLE_V16 || MO_ENABLE_V201 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(); +HeartbeatService::HeartbeatService(Context& context) : MemoryManaged("v16/v201.Heartbeat.HeartbeatService"), context(context) { - //Register message handler for TriggerMessage operation - context.getOperationRegistry().registerOperation("Heartbeat", [&context] () { - return new Ocpp16::Heartbeat(context.getModel());}); +} + +bool HeartbeatService::setup() { + + ocppVersion = context.getOcppVersion(); + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + + configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("initialization error"); + return false; + } + + //if transactions can start before the BootNotification succeeds + heartbeatIntervalIntV16 = configService->declareConfiguration("HeartbeatInterval", 86400); + if (!heartbeatIntervalIntV16) { + MO_DBG_ERR("initialization error"); + return false; + } + + if (!configService->registerValidator("HeartbeatInterval", validateHeartbeatInterval)) { + MO_DBG_ERR("initialization error"); + return false; + } + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + + varService = context.getModel201().getVariableService(); + if (!varService) { + MO_DBG_ERR("initialization error"); + return false; + } + + //if transactions can start before the BootNotification succeeds + heartbeatIntervalIntV201 = varService->declareVariable("OCPPCommCtrlr", "HeartbeatInterval", 86400); + if (!heartbeatIntervalIntV201) { + MO_DBG_ERR("initialization error"); + return false; + } + + if (!varService->registerValidator("OCPPCommCtrlr", "HeartbeatInterval", validateHeartbeatInterval)) { + MO_DBG_ERR("initialization error"); + return false; + } + } + #endif //MO_ENABLE_V201 + + lastHeartbeat = context.getClock().getUptime(); + + #if MO_ENABLE_MOCK_SERVER + context.getMessageService().registerOperation("Heartbeat", nullptr, Heartbeat::writeMockConf, nullptr, reinterpret_cast(&context)); + #endif //MO_ENABLE_MOCK_SERVER + + auto rcService = context.getModelCommon().getRemoteControlService(); + if (!rcService) { + MO_DBG_ERR("initialization error"); + return false; + } + + rcService->addTriggerMessageHandler("Heartbeat", [] (Context& context) -> Operation* { + return new Heartbeat(context);}); + + return true; } 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; + int hbInterval = 86400; + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + hbInterval = heartbeatIntervalIntV16->getInt(); + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + hbInterval = heartbeatIntervalIntV201->getInt(); + } + #endif //MO_ENABLE_V201 + + int32_t dtLastHeartbeat; + if (!context.getClock().delta(context.getClock().getUptime(), lastHeartbeat, dtLastHeartbeat)) { + dtLastHeartbeat = hbInterval; //force send Heartbeat + } - auto heartbeat = makeRequest(new Ocpp16::Heartbeat(context.getModel())); + if (dtLastHeartbeat >= hbInterval && hbInterval > 0) { + lastHeartbeat = context.getClock().getUptime(); + + auto heartbeat = makeRequest(context, new Heartbeat(context)); // Heartbeats can not deviate more than 4s from the configured interval - heartbeat->setTimeout(std::min(4000UL, hbInterval)); - context.initiateRequest(std::move(heartbeat)); + heartbeat->setTimeout(std::min(4, hbInterval)); + context.getMessageService().sendRequest(std::move(heartbeat)); } } + +bool HeartbeatService::setHeartbeatInterval(int interval) { + if (!validateHeartbeatInterval(interval, nullptr)) { + return false; + } + + bool success = false; + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + heartbeatIntervalIntV16->setInt(interval); + success = configService->commit(); + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + heartbeatIntervalIntV201->setInt(interval); + success = varService->commit(); + } + #endif //MO_ENABLE_V201 + + return success; +} + +bool MicroOcpp::validateHeartbeatInterval(int v, void*) { + return v >= 1; +} + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Heartbeat/HeartbeatService.h b/src/MicroOcpp/Model/Heartbeat/HeartbeatService.h index ea632cc8..4c2a6904 100644 --- a/src/MicroOcpp/Model/Heartbeat/HeartbeatService.h +++ b/src/MicroOcpp/Model/Heartbeat/HeartbeatService.h @@ -1,32 +1,63 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_HEARTBEATSERVICE_H #define MO_HEARTBEATSERVICE_H -#include - -#include +#include #include +#include + +#if MO_ENABLE_V16 || MO_ENABLE_V201 namespace MicroOcpp { class Context; +#if MO_ENABLE_V16 +namespace v16 { +class ConfigurationService; +class Configuration; +} +#endif +#if MO_ENABLE_V201 +namespace v201 { +class VariableService; +class Variable; +} +#endif + class HeartbeatService : public MemoryManaged { private: Context& context; - unsigned long lastHeartbeat; - std::shared_ptr heartbeatIntervalInt; + int ocppVersion = -1; + + #if MO_ENABLE_V16 + v16::ConfigurationService *configService = nullptr; + v16::Configuration *heartbeatIntervalIntV16 = nullptr; + #endif + #if MO_ENABLE_V201 + v201::VariableService *varService = nullptr; + v201::Variable *heartbeatIntervalIntV201 = nullptr; + #endif + + Timestamp lastHeartbeat; public: HeartbeatService(Context& context); + bool setup(); + void loop(); + + bool setHeartbeatInterval(int interval); }; +bool validateHeartbeatInterval(int v, void*); + } +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Metering/MeterStore.cpp b/src/MicroOcpp/Model/Metering/MeterStore.cpp index e4bc9e85..dcf3d77b 100644 --- a/src/MicroOcpp/Model/Metering/MeterStore.cpp +++ b/src/MicroOcpp/Model/Metering/MeterStore.cpp @@ -3,297 +3,117 @@ // MIT License #include -#include - -#include #include -#ifndef MO_MAX_STOPTXDATA_LEN -#define MO_MAX_STOPTXDATA_LEN 4 -#endif +#include +#include +#include -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"); - } -} +#if MO_ENABLE_V16 -bool TransactionMeterData::addTxData(std::unique_ptr mv) { - if (isFinalized()) { - MO_DBG_ERR("immutable"); - return false; - } +using namespace MicroOcpp; +using namespace MicroOcpp::v16; - if (!mv) { - MO_DBG_ERR("null"); +bool MeterStore::printTxMeterValueFName(char *fname, size_t size, unsigned int evseId, unsigned int txNr, unsigned int mvIndex) { + auto ret = snprintf(fname, MO_MAX_PATH_SIZE, "sd-%.*u-%.*u-%.*u.jsn", + MO_NUM_EVSEID_DIGITS, evseId, MO_TXNR_DIGITS, txNr, MO_STOPTXDATA_DIGITS, mvIndex); + if (ret < 0 || (size_t)ret >= size) { + MO_DBG_ERR("fname error: %i", ret); 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); -} +FilesystemUtils::LoadStatus MeterStore::load(MO_FilesystemAdapter *filesystem, Context& context, Vector& meterInputs, unsigned int evseId, unsigned int txNr, Vector& txMeterValues) { -bool TransactionMeterData::restore(MeterValueBuilder& mvBuilder) { - if (!filesystem) { - MO_DBG_DEBUG("No FS - nothing to restore"); - return true; - } + auto& clock = context.getClock(); - const unsigned int MISSES_LIMIT = 3; - unsigned int i = 0; - unsigned int misses = 0; + for (unsigned int i = 0; i < MO_STOPTXDATA_MAX_SIZE; i++) { //search until region without mvs found - 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 + char fname [MO_MAX_PATH_SIZE] = {'\0'}; + if (!printTxMeterValueFName(fname, sizeof(fname), evseId, txNr, i)) { + MO_DBG_ERR("fname error %u %u", evseId, txNr); + return FilesystemUtils::LoadStatus::ErrOther; } - auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); + JsonDoc doc (0); - if (!doc) { - misses++; - i++; - continue; + auto ret = FilesystemUtils::loadJson(filesystem, fname, doc, "v16/v201.Metering.MeterStore"); + if (ret == FilesystemUtils::LoadStatus::FileNotFound) { + break; //done loading meterValues } - - JsonObject mvJson = doc->as(); - std::unique_ptr mv = mvBuilder.deserializeSample(mvJson); - - if (!mv) { - MO_DBG_ERR("Deserialization error"); - misses++; - i++; - continue; + if (ret != FilesystemUtils::LoadStatus::Success) { + return ret; } - if (txData.size() >= MO_MAX_STOPTXDATA_LEN) { - MO_DBG_ERR("corrupted memory"); - return false; + auto capacity = txMeterValues.size() + 1; + txMeterValues.reserve(capacity); + if (txMeterValues.capacity() < capacity) { + MO_DBG_ERR("OOM"); + return FilesystemUtils::LoadStatus::ErrOOM; } - 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"); - } -} + auto mv = new MeterValue(); + if (!mv) { + MO_DBG_ERR("OOM"); + return FilesystemUtils::LoadStatus::ErrOOM; + } + txMeterValues.push_back(mv); -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; + if (!mv->parseJson(clock, meterInputs, doc.as())) { + MO_DBG_ERR("deserialization error %s", fname); + return FilesystemUtils::LoadStatus::ErrFileCorruption; } } - //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 - } + //success - size_t size = 0; - bool exists = filesystem->stat(fn, &size) == 0; + MO_DBG_DEBUG("Restored tx %u-%u with %zu meter values", evseId, txNr, txMeterValues.size()); - if (exists) { - if (!tx->restore(mvBuilder)) { - remove(connectorId, txNr); - MO_DBG_ERR("removed corrupted tx entries"); - } - } + if (txMeterValues.empty()) { + return FilesystemUtils::LoadStatus::FileNotFound; } - txMeterData.push_back(tx); - - MO_DBG_DEBUG("Added txNr %u, now holding %zu sds", txNr, txMeterData.size()); - - return tx; + return FilesystemUtils::LoadStatus::Success; } -bool MeterStore::remove(unsigned int connectorId, unsigned int txNr) { - - unsigned int mvCount = 0; +FilesystemUtils::StoreStatus MeterStore::store(MO_FilesystemAdapter *filesystem, Context& context, unsigned int evseId, unsigned int txNr, unsigned int mvIndex, MeterValue& mv) { - 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(); - } + char fname [MO_MAX_PATH_SIZE]; + if (!printTxMeterValueFName(fname, sizeof(fname), evseId, txNr, mvIndex)) { + MO_DBG_ERR("fname error %u %u %u", evseId, txNr, mvIndex); + return FilesystemUtils::StoreStatus::ErrOther; } - 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; - } + auto& clock = context.getClock(); - 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; - } + JsonDoc doc = initJsonDoc("v16/v201.MeterStore", 1024); + if (!mv.toJson(clock, context.getOcppVersion(), /*internal format*/ true, doc.as())) { + MO_DBG_ERR("serialization error %s", fname); + return FilesystemUtils::StoreStatus::ErrJsonCorruption; + } - success &= filesystem->remove(fn); - } + auto ret = FilesystemUtils::storeJson(filesystem, fname, doc); + if (ret != FilesystemUtils::StoreStatus::Success) { + MO_DBG_ERR("fs error %s %i", fname, (int)ret); } - //clean outdated pointers + return ret; +} - txMeterData.erase(std::remove_if(txMeterData.begin(), txMeterData.end(), - [] (std::weak_ptr& txm) { - return txm.expired(); - }), - txMeterData.end()); +bool MeterStore::remove(MO_FilesystemAdapter *filesystem, unsigned int evseId, unsigned int txNr) { - if (success) { - MO_DBG_DEBUG("Removed meter values for cId %u, txNr %u", connectorId, txNr); - } else { - MO_DBG_DEBUG("corrupted fs"); + char fname [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fname, MO_MAX_PATH_SIZE, "sd-%.*u-%.*u-", + MO_NUM_EVSEID_DIGITS, evseId, MO_TXNR_DIGITS, txNr); + if (ret < 0 || (size_t)ret >= sizeof(fname)) { + MO_DBG_ERR("fname error: %i", ret); + return false; } - return success; + return FilesystemUtils::removeByPrefix(filesystem, fname); } + +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Model/Metering/MeterStore.h b/src/MicroOcpp/Model/Metering/MeterStore.h index bdb67441..85074f20 100644 --- a/src/MicroOcpp/Model/Metering/MeterStore.h +++ b/src/MicroOcpp/Model/Metering/MeterStore.h @@ -8,54 +8,35 @@ #include #include #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); +#if MO_ENABLE_V16 - 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 +#ifndef MO_STOPTXDATA_MAX_SIZE +#define MO_STOPTXDATA_MAX_SIZE 4 +#endif - 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;} -}; +#ifndef MO_STOPTXDATA_DIGITS +#define MO_STOPTXDATA_DIGITS 1 //digits needed to print MO_STOPTXDATA_MAX_SIZE-1 (="3", i.e. 1 digit) +#endif -class MeterStore : public MemoryManaged { -private: - std::shared_ptr filesystem; - - Vector> txMeterData; +namespace MicroOcpp { -public: - MeterStore() = delete; - MeterStore(MeterStore&) = delete; - MeterStore(std::shared_ptr filesystem); +class Context; - std::shared_ptr getTxMeterData(MeterValueBuilder& mvBuilder, Transaction *transaction); +namespace v16 { +namespace MeterStore { - bool remove(unsigned int connectorId, unsigned int txNr); -}; +bool printTxMeterValueFName(char *fname, size_t size, unsigned int evseId, unsigned int txNr, unsigned int mvIndex); -} +FilesystemUtils::LoadStatus load(MO_FilesystemAdapter *filesystem, Context& context, Vector& meterInputs, unsigned int evseId, unsigned int txNr, Vector& txMeterValue); +FilesystemUtils::StoreStatus store(MO_FilesystemAdapter *filesystem, Context& context, unsigned int evseId, unsigned int txNr, unsigned int mvIndex, MeterValue& mv); +bool remove(MO_FilesystemAdapter *filesystem, unsigned int evseId, unsigned int txNr); //removes tx meter values only +} //namespace MeterStore +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #endif diff --git a/src/MicroOcpp/Model/Metering/MeterValue.cpp b/src/MicroOcpp/Model/Metering/MeterValue.cpp index 740905da..72708166 100644 --- a/src/MicroOcpp/Model/Metering/MeterValue.cpp +++ b/src/MicroOcpp/Model/Metering/MeterValue.cpp @@ -2,211 +2,584 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - #include -#include +#include #include -using namespace MicroOcpp; - -MeterValue::MeterValue(const Timestamp& timestamp) : - MemoryManaged("v16.Metering.MeterValue"), - timestamp(timestamp), - sampledValue(makeVector>(getMemoryTag())) { +#if MO_ENABLE_V16 || MO_ENABLE_V201 +void mo_meterInput_init(MO_MeterInput *mInput, MO_MeterInputType type) { + memset(mInput, 0, sizeof(*mInput)); + mo_meterInput_setType(mInput, type); } -void MeterValue::addSampledValue(std::unique_ptr sample) { - sampledValue.push_back(std::move(sample)); +MO_MeterInputType mo_meterInput_getType(MO_MeterInput *mInput) { + return static_cast(mInput->type); +} +void mo_meterInput_setType(MO_MeterInput *mInput, MO_MeterInputType type) { + mInput->type = static_casttype)>(type); } -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)); - } +using namespace MicroOcpp; - capacity += JSON_ARRAY_SIZE(entries.size()); - capacity += JSONDATE_LENGTH + 1; - capacity += JSON_OBJECT_SIZE(2); - - auto result = makeJsonDoc(getMemoryTag(), capacity); - auto jsonPayload = result->to(); +SampledValue::SampledValue(MO_MeterInput& meterInput) : MemoryManaged("v16.Metering.MeterValue"), meterInput(meterInput) { - 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); +} + +SampledValue::~SampledValue() { + switch (getType()) { + case Type::Int: + case Type::Float: + case Type::UNDEFINED: + break; + case Type::String: + MO_FREE(valueString); + valueString = nullptr; + break; + #if MO_ENABLE_V201 + case Type::SignedValue: + valueSigned->onDestroy(valueSigned->user_data); + MO_FREE(valueSigned); + valueSigned = nullptr; + break; + #endif //MO_ENABLE_V201 } - return result; } -const Timestamp& MeterValue::getTimestamp() { - return timestamp; +SampledValue::Type SampledValue::getType() { + Type res = Type::UNDEFINED; + switch (meterInput.type) { + case MO_MeterInputType_Int: + case MO_MeterInputType_IntWithArgs: + res = Type::Int; + break; + case MO_MeterInputType_Float: + case MO_MeterInputType_FloatWithArgs: + res = Type::Float; + break; + case MO_MeterInputType_String: + case MO_MeterInputType_StringWithArgs: + res = Type::Float; + break; + #if MO_ENABLE_V201 + case MO_MeterInputType_SignedValueWithArgs: + res = Type::SignedValue; + break; + #endif //MO_ENABLE_V201 + case MO_MeterInputType_UNDEFINED: + res = Type::UNDEFINED; + break; + } + return res; } -void MeterValue::setTimestamp(Timestamp timestamp) { - this->timestamp = timestamp; +MeterValue::MeterValue() : + MemoryManaged("v16.Metering.MeterValue"), + sampledValue(makeVector(getMemoryTag())) { + } -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(); - } +MeterValue::~MeterValue() { + for (size_t i = 0; i < sampledValue.size(); i++) { + delete sampledValue[i]; } - return ReadingContext_UNDEFINED; + sampledValue.clear(); } -void MeterValue::setTxNr(unsigned int txNr) { - if (txNr > (unsigned int)std::numeric_limits::max()) { - MO_DBG_ERR("invalid arg"); - return; +#if MO_ENABLE_V201 + +namespace MicroOcpp { +bool loadSignedValue(MO_SignedMeterValue201* signedValue, const char *signedMeterData, const char *signingMethod, const char *encodingMethod, const char *publicKey, const char *memoryTag) { + + size_t signedMeterDataSize = signedMeterData ? strlen(signedMeterData) + 1 : 0; + size_t signingMethodSize = signingMethod ? strlen(signingMethod) + 1 : 0; + size_t encodingMethodSize = encodingMethod ? strlen(encodingMethod) + 1 : 0; + size_t publicKeySize = publicKey ? strlen(publicKey) + 1 : 0; + + size_t bufsize = + signedMeterDataSize + + signingMethodSize + + encodingMethodSize + + publicKeySize; + + char *buf = static_cast(MO_MALLOC(memoryTag, bufsize)); + if (!buf) { + MO_DBG_ERR("OOM"); + return false; } - this->txNr = (int)txNr; -} + memset(buf, 0, bufsize); + size_t written = 0; + + if (signedMeterDataSize > 0) { + signedValue->signedMeterData = buf + written; + auto ret = snprintf(const_cast(signedValue->signedMeterData), signedMeterDataSize, "%s", signedMeterData); + if (ret < 0 || (size_t)ret >= signedMeterDataSize) { + MO_DBG_ERR("snprintf: %i", ret); + goto fail; + } -int MeterValue::getTxNr() { - return txNr; -} + written += (size_t)ret + 1; + } -void MeterValue::setOpNr(unsigned int opNr) { - this->opNr = opNr; -} + if (signingMethodSize > 0) { + signedValue->signingMethod = buf + written; + auto ret = snprintf(const_cast(signedValue->signingMethod), signingMethodSize, "%s", signingMethod); + if (ret < 0 || (size_t)ret >= signingMethodSize) { + MO_DBG_ERR("snprintf: %i", ret); + goto fail; + } + written += (size_t)ret + 1; + } -unsigned int MeterValue::getOpNr() { - return opNr; -} + if (encodingMethodSize > 0) { + signedValue->encodingMethod = buf + written; + auto ret = snprintf(const_cast(signedValue->encodingMethod), encodingMethodSize, "%s", encodingMethod); + if (ret < 0 || (size_t)ret >= encodingMethodSize) { + MO_DBG_ERR("snprintf: %i", ret); + goto fail; + } + written += (size_t)ret + 1; + } -void MeterValue::advanceAttemptNr() { - attemptNr++; -} + if (publicKeySize > 0) { + signedValue->publicKey = buf + written; + auto ret = snprintf(const_cast(signedValue->publicKey), publicKeySize, "%s", publicKey); + if (ret < 0 || (size_t)ret >= publicKeySize) { + MO_DBG_ERR("snprintf: %i", ret); + goto fail; + } + written += (size_t)ret + 1; + } -unsigned int MeterValue::getAttemptNr() { - return attemptNr; -} + if (written != bufsize) { + MO_DBG_ERR("internal error"); + goto fail; + } -unsigned long MeterValue::getAttemptTime() { - return attemptTime; -} + signedValue->user_data = reinterpret_cast(buf); -void MeterValue::setAttemptTime(unsigned long timestamp) { - this->attemptTime = timestamp; -} + signedValue->onDestroy = [] (void *user_data) { + auto buf = reinterpret_cast(user_data); + MO_FREE(buf); + }; -MeterValueBuilder::MeterValueBuilder(const Vector> &samplers, - std::shared_ptr samplersSelectStr) : - MemoryManaged("v16.Metering.MeterValueBuilder"), - samplers(samplers), - selectString(samplersSelectStr), - select_mask(makeVector(getMemoryTag())) { + return true; - updateObservedSamplers(); - select_observe = selectString->getValueRevision(); +fail: + MO_FREE(buf); + return false; } +} //namespace MicroOcpp +#endif //MO_ENABLE_V201 -void MeterValueBuilder::updateObservedSamplers() { +bool MeterValue::parseJson(Clock& clock, Vector& meterInputs, JsonObject in) { - if (select_mask.size() != samplers.size()) { - select_mask.resize(samplers.size(), false); - select_n = 0; + if (!clock.parseString(in["timestamp"] | "_Invalid", timestamp)) { + return false; } - for (size_t i = 0; i < select_mask.size(); i++) { - select_mask[i] = false; + //MO stores the ReadingContext only once in the MeterValue object. Currently, there are no use cases in MO where + //different ReadingContexts could occur in the same MeterValue + if (in.containsKey("context")) { + readingContext = deserializeReadingContext(in["context"] | "_Invalid"); + if (readingContext == MO_ReadingContext_UNDEFINED) { + MO_DBG_ERR("deserialization error"); + return false; + } + } else { + readingContext = MO_ReadingContext_SamplePeriodic; } - - 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] == ',') { + + JsonArray sampledValueJson = in["sampledValue"]; + sampledValue.resize(sampledValueJson.size()); + if (sampledValue.capacity() < sampledValueJson.size()) { + MO_DBG_ERR("OOM"); + return false; + } + for (size_t i = 0; i < sampledValueJson.size(); i++) { //for each sampled value, search sampler with matching measurand type + JsonObject svJson = sampledValueJson[i]; + + auto format = svJson["format"] | (const char*)nullptr; + auto measurand = svJson["measurand"] | (const char*)nullptr; + auto phase = svJson["phase"] | (const char*)nullptr; + auto location = svJson["location"] | (const char*)nullptr; + + const char *unit = svJson["unit"] | (const char*)nullptr; + int8_t multiplier = 0; + + #if MO_ENABLE_V201 + { + int multiplierRaw = svJson["multiplier"] | 0; + multiplier = multiplierRaw; + if ((int)multiplier != multiplierRaw) { + MO_DBG_ERR("int overflow"); + return false; + } + } + #else + { + (void)multiplier; + } + #endif //MO_ENABLE_V201 + + MO_MeterInput *meterDevice = nullptr; + for (size_t i = 0; i < meterInputs.size(); i++) { + auto& mInput = meterInputs[i]; + if (!strcmp(mInput.measurand ? mInput.measurand : "Energy.Active.Import.Register", measurand ? measurand : "Energy.Active.Import.Register") && + !strcmp(mInput.unit ? mInput.unit : "Wh", unit ? unit : "Wh") && + #if MO_ENABLE_V201 + mInput.unitOfMeasureMultiplier == multiplier && + #endif //MO_ENABLE_V201 + !strcmp(mInput.location ? mInput.location : "Outlet", location ? location : "Outlet") && + !strcmp(mInput.phase ? mInput.phase : "", phase ? phase : "") && + !strcmp(mInput.format ? mInput.format : "Raw", format ? format : "Raw")) { + + //found + meterDevice = &mInput; 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++; + if (!meterDevice) { + MO_DBG_ERR("need to add MeterInputs before setup()"); + return false; + } + + auto sv = new SampledValue(*meterDevice); + if (!sv) { + return false; + } + sampledValue.push_back(sv); + + switch (sv->getType()) { //type defined via meterDevice + case SampledValue::Type::Int: + if (!svJson["value"].is()) { + MO_DBG_ERR("deserialization error"); + return false; } + sv->valueInt = svJson["value"]; + break; + case SampledValue::Type::Float: + if (!svJson["value"].is()) { + MO_DBG_ERR("deserialization error"); + return false; + } + sv->valueFloat = svJson["value"]; + break; + case SampledValue::Type::String: { + if (!svJson["value"].is()) { + MO_DBG_ERR("deserialization error"); + return false; + } + const char *value = svJson["value"]; + size_t valueLen = strlen(value); + char *valueCopy = nullptr; + if (valueLen > 0) { + size_t size = valueLen + 1; + valueCopy = static_cast(MO_MALLOC("v16.Transactions.TransactionStore", size)); + if (!valueCopy) { + MO_DBG_ERR("OOM"); + return false; + } + snprintf(valueCopy, size, "%s", value); + } + sv->valueString = valueCopy; + break; } - } + #if MO_ENABLE_V201 + case SampledValue::Type::SignedValue: + if (!svJson["value"].isvalueSigned->value)>() || + !svJson["signedMeterData"].is() || + !svJson["signingMethod"].is() || + !svJson["encodingMethod"].is() || + !svJson["publicKey"].is()) { + MO_DBG_ERR("deserialization error"); + return false; + } + sv->valueSigned->value = svJson["value"]; - sr++; - sl = sr; + sv->valueSigned = static_cast(MO_MALLOC(getMemoryTag(), sizeof(MO_SignedMeterValue201))); + if (!sv->valueSigned) { + MO_DBG_ERR("OOM"); + return false; + } + if (!loadSignedValue(sv->valueSigned, + svJson["signedMeterData"] | (const char*)nullptr, + svJson["signingMethod"] | (const char*)nullptr, + svJson["encodingMethod"] | (const char*)nullptr, + svJson["publicKey"] | (const char*)nullptr, + getMemoryTag())) { + MO_DBG_ERR("OOM"); + MO_FREE(sv->valueSigned); + return false; + } + break; + #endif //MO_ENABLE_V201 + case SampledValue::Type::UNDEFINED: + MO_DBG_ERR("deserialization error"); + return false; + } } + + MO_DBG_VERBOSE("deserialized MV"); + return true; } -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(); - } +int MeterValue::getJsonCapacity(int ocppVersion, bool internalFormat) { + + size_t capacity = 0; + + capacity += JSON_OBJECT_SIZE(2); //timestamp, sampledValue - if (select_n == 0) { - return nullptr; + if (internalFormat) { + capacity += MO_INTERNALTIME_SIZE; + if (readingContext != MO_ReadingContext_SamplePeriodic) { + capacity += JSON_OBJECT_SIZE(1); //readingContext + } + } else { + capacity += MO_JSONDATE_SIZE; } - auto sample = std::unique_ptr(new MeterValue(timestamp)); + capacity += JSON_ARRAY_SIZE(sampledValue.size()); + for (size_t j = 0; j < sampledValue.size(); j++) { + auto sv = sampledValue[j]; - for (size_t i = 0; i < select_mask.size(); i++) { - if (select_mask[i]) { - sample->addSampledValue(samplers[i]->takeValue(context)); + int valueLen = -1; + switch (sv->getType()) { + case SampledValue::Type::Int: + valueLen = snprintf(nullptr, 0, "%i", sv->valueInt); + break; + case SampledValue::Type::Float: + valueLen = snprintf(nullptr, 0, MO_SAMPLEDVALUE_FLOAT_FORMAT, sv->valueFloat); + break; + case SampledValue::Type::String: + valueLen = (int)strlen(sv->valueString ? sv->valueString : ""); + break; + #if MO_ENABLE_V201 + case SampledValue::Type::SignedValue: + valueLen = snprintf(nullptr, 0, MO_SAMPLEDVALUE_FLOAT_FORMAT, sv->valueSigned->value); + break; + #endif //MO_ENABLE_V201 + case SampledValue::Type::UNDEFINED: + MO_DBG_ERR("serialization error"); + break; + } + if (valueLen >= 0) { + capacity += (size_t)valueLen + 1; + } else { + MO_DBG_ERR("serialization error"); + return -1; + } + + capacity += JSON_OBJECT_SIZE(1) + valueLen + 1; //value + + if (!internalFormat && readingContext != MO_ReadingContext_SamplePeriodic) { + capacity += JSON_OBJECT_SIZE(1); //readingContext + } + + auto& meterDevice = sv->meterInput; + + if (meterDevice.format && strcmp(meterDevice.format, "Raw")) { + capacity += JSON_OBJECT_SIZE(1); //format } + if (meterDevice.measurand && strcmp(meterDevice.measurand, "Energy.Active.Import.Register")) { + capacity += JSON_OBJECT_SIZE(1); //measurand + } + if (meterDevice.phase && *meterDevice.phase) { + capacity += JSON_OBJECT_SIZE(1); //phase + } + if (meterDevice.location && strcmp(meterDevice.location, "Outlet")) { + capacity += JSON_OBJECT_SIZE(1); //location + } + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + //no signed meter value and simple unitOfMeasure + if (meterDevice.unit && strcmp(meterDevice.unit, "Wh")) { + capacity += JSON_OBJECT_SIZE(1); //unit + } + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + //complex unitOfMeasure and signedMeterValue + if (sv->getType() == SampledValue::Type::SignedValue) { + if (!internalFormat) { + capacity += JSON_OBJECT_SIZE(1); //stored as complex type + } + capacity += sv->valueSigned->signedMeterData ? JSON_OBJECT_SIZE(1) : 0; + capacity += sv->valueSigned->signingMethod ? JSON_OBJECT_SIZE(1): 0; + capacity += sv->valueSigned->encodingMethod ? JSON_OBJECT_SIZE(1) : 0; + capacity += sv->valueSigned->publicKey ? JSON_OBJECT_SIZE(1) : 0; + } + + bool hasUnitOfMeasure = false; + + if (meterDevice.unit && strcmp(meterDevice.unit, "Wh")) { + capacity += JSON_OBJECT_SIZE(1); //unit + hasUnitOfMeasure = true; + } + + if (meterDevice.unitOfMeasureMultiplier != 0) { + capacity += JSON_OBJECT_SIZE(1); //multiplier + hasUnitOfMeasure = true; + } + + if (hasUnitOfMeasure && !internalFormat) { + capacity += JSON_OBJECT_SIZE(1); + } + } + #endif //MO_ENABLE_V201 } - return sample; + return (size_t)capacity; } -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)); +bool MeterValue::toJson(Clock& clock, int ocppVersion, bool internalFormat, JsonObject out) { + + if (internalFormat) { + char timeStr [MO_INTERNALTIME_SIZE]; + if (!clock.toInternalString(timestamp, timeStr, sizeof(timeStr))) { + return false; + } + out["timestamp"] = timeStr; + } else { + char timeStr [MO_JSONDATE_SIZE]; + if (!clock.toJsonString(timestamp, timeStr, sizeof(timeStr))) { + return false; + } + out["timestamp"] = timeStr; + } + + if (readingContext == MO_ReadingContext_UNDEFINED) { + MO_DBG_ERR("serialization error"); + return false; + } + + if (internalFormat && readingContext != MO_ReadingContext_SamplePeriodic) { + out["context"] = serializeReadingContext(readingContext); + } + + JsonArray sampledValueJson = out.createNestedArray("sampledValue"); + + for (size_t i = 0; i < sampledValue.size(); i++) { + auto sv = sampledValue[i]; + + JsonObject svJson = sampledValueJson.createNestedObject(); + + char valueBuf [30]; //for int and float, serialized as string + + switch (sv->getType()) { + case SampledValue::Type::Int: + if (internalFormat) { + svJson["value"] = sv->valueInt; } else { - MO_DBG_ERR("deserialization error"); + int ret = snprintf(valueBuf, sizeof(valueBuf), "%i", sv->valueInt); + if (ret < 0 || (size_t)ret >= sizeof(valueBuf)) { + MO_DBG_ERR("serialization error"); + return false; + } + svJson["value"] = valueBuf; + } + break; + case SampledValue::Type::Float: + if (internalFormat) { + svJson["value"] = sv->valueFloat; + } else { + int ret = snprintf(valueBuf, sizeof(valueBuf), MO_SAMPLEDVALUE_FLOAT_FORMAT, sv->valueFloat); + if (ret < 0 || (size_t)ret >= sizeof(valueBuf)) { + MO_DBG_ERR("serialization error"); + return false; + } + svJson["value"] = valueBuf; + } + break; + case SampledValue::Type::String: + svJson["value"] = sv->valueString ? (const char*)sv->valueString : ""; //force zero-copy + break; + #if MO_ENABLE_V201 + case SampledValue::Type::SignedValue: { + JsonObject signedMeterValue; + if (internalFormat) { + svJson["value"] = sv->valueSigned->value; + signedMeterValue = svJson; //write signature data into same sv JSON object + } else { + int ret = snprintf(valueBuf, sizeof(valueBuf), MO_SAMPLEDVALUE_FLOAT_FORMAT, sv->valueFloat); + if (ret < 0 || (size_t)ret >= sizeof(valueBuf)) { + MO_DBG_ERR("serialization error"); + return false; + } + svJson["value"] = valueBuf; + signedMeterValue = svJson.createNestedObject("signedMeterValue"); + } + if (sv->valueSigned->signedMeterData) { + signedMeterValue["signedMeterData"] = (const char*)sv->valueSigned->signedMeterData; //force zero-copy + } + if (sv->valueSigned->signingMethod) { + signedMeterValue["signingMethod"] = (const char*)sv->valueSigned->signingMethod; //force zero-copy + } + if (sv->valueSigned->encodingMethod) { + signedMeterValue["encodingMethod"] = (const char*)sv->valueSigned->encodingMethod; //force zero-copy + } + if (sv->valueSigned->publicKey) { + signedMeterValue["publicKey"] = (const char*)sv->valueSigned->publicKey; //force zero-copy } break; } + #endif //MO_ENABLE_V201 + case SampledValue::Type::UNDEFINED: + MO_DBG_ERR("serialization error"); + return false; + } + + if (!internalFormat && readingContext != MO_ReadingContext_SamplePeriodic) { + svJson["context"] = serializeReadingContext(readingContext); } + + auto& meterDevice = sv->meterInput; + + if (meterDevice.format && strcmp(meterDevice.format, "Raw")) { + svJson["format"] = meterDevice.format; + } + if (meterDevice.measurand && strcmp(meterDevice.measurand, "Energy.Active.Import.Register")) { + svJson["measurand"] = meterDevice.measurand; + } + if (meterDevice.phase && *meterDevice.phase) { + svJson["phase"] = meterDevice.phase; //phase + } + if (meterDevice.location && strcmp(meterDevice.location, "Outlet")) { + svJson["location"] = meterDevice.location; //location + } + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + if (meterDevice.unit && strcmp(meterDevice.unit, "Wh")) { + svJson["unit"] = meterDevice.unit; //unit + } + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + if ((meterDevice.unit && strcmp(meterDevice.unit, "Wh")) || meterDevice.unitOfMeasureMultiplier != 0) { + JsonObject unitJson; + if (internalFormat) { + unitJson = svJson; + } else { + unitJson = svJson.createNestedObject("unitOfMeasure"); + } + + if (meterDevice.unit && strcmp(meterDevice.unit, "Wh")) { + unitJson["unit"] = meterDevice.unit; + } + if (meterDevice.unitOfMeasureMultiplier != 0) { + unitJson["multiplier"] = meterDevice.unitOfMeasureMultiplier; + } + } + } + #endif //MO_ENABLE_V201 } - MO_DBG_VERBOSE("deserialized MV"); - return sample; + return true; } + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Metering/MeterValue.h b/src/MicroOcpp/Model/Metering/MeterValue.h index 0637ad33..21571374 100644 --- a/src/MicroOcpp/Model/Metering/MeterValue.h +++ b/src/MicroOcpp/Model/Metering/MeterValue.h @@ -5,68 +5,161 @@ #ifndef MO_METERVALUE_H #define MO_METERVALUE_H +#include +#include +#include +#include + +#if MO_ENABLE_V16 || MO_ENABLE_V201 + +#ifndef MO_SAMPLEDVALUE_FLOAT_FORMAT +#define MO_SAMPLEDVALUE_FLOAT_FORMAT "%.2f" +#endif + +#ifndef MO_SAMPLEDVALUE_STRING_MAX_LEN +#define MO_SAMPLEDVALUE_STRING_MAX_LEN 1000 //maximum acceptable sampled value string length +#endif + +#ifdef __cplusplus +extern "C" { +#endif //__cplusplus + +#if MO_ENABLE_V201 + +/* Compound type for transmitting SignedMeterValues via OCPP. The firmware should generate signature + * data and keep it ready until MO queries it using the `getSignedValue` callback. Then, the firmware + * can fill this struct provided by MO. Probably the signature data needs to be duplicated into the + * heap per malloc. The firmware can set the `onDestroy` callback to free the memory again. */ +struct MO_SignedMeterValue201 { + double value; + + /* Signed data. If not null, MO will dereference these strings and send them to the server. When + * MO no longer needs these strings, it calls onDestroy (guaranteed execution). It's possible to + * malloc string buffers for these fields and to free them again in onDestroy */ + const char *signedMeterData; + const char *signingMethod; + const char *encodingMethod; + const char *publicKey; + + //set this as needed + void* user_data; //MO will forward this pointer to onDestroy + void (*onDestroy)(void *user_data); //MO calls it shortly before destruction of this struct +}; + +#endif //MO_ENABLE_V201 + +typedef enum { + MO_MeterInputType_UNDEFINED = 0, + MO_MeterInputType_Int, + MO_MeterInputType_Float, + MO_MeterInputType_String, + MO_MeterInputType_IntWithArgs, + MO_MeterInputType_FloatWithArgs, + MO_MeterInputType_StringWithArgs, + + #if MO_ENABLE_V201 + MO_MeterInputType_SignedValueWithArgs, + #endif //MO_ENABLE_V201 +} MO_MeterInputType; + +typedef struct { + + void* user_data; + union { + int32_t (*getInt)(); + float (*getFloat)(); + //if `size` is large enough, writes string to `buf` and returns written string length (not + //including terminating zero). If `size` is too short, returns string length which would have + //been written if `buf` was large enough. MO may retry with a larger buf. Adds a terminating 0. + //Note: `buf` will never be larger than (MO_SAMPLEDVALUE_STRING_MAX_LEN + 1) + int (*getString)(char *buf, size_t size); + + int32_t (*getInt2)(MO_ReadingContext readingContext, unsigned int evseId, void *user_data); + float (*getFloat2)(MO_ReadingContext readingContext, unsigned int evseId, void *user_data); + int (*getString2)(char *buf, size_t size, MO_ReadingContext readingContext, unsigned int evseId, void *user_data); //see `getString()` + + #if MO_ENABLE_V201 + //if signed value exists, fills `val` with the signature data and returns true. If no signed + //data exists, returns false. + bool (*getSignedValue2)(MO_SignedMeterValue201* val, MO_ReadingContext readingContext, unsigned int evseId, void *user_data); + #endif //MO_ENABLE_V201 + }; + + uint8_t type; //MO_MeterInputType in dense representation. Use `mo_meterInput_setType()` to define + uint8_t mo_flags; //flags which MO uses internally + + #if MO_ENABLE_V201 + int8_t unitOfMeasureMultiplier; + #endif //MO_ENABLE_V201 + + //OCPP-side attributes. Zero-copy strings + const char *format; + const char *measurand; + const char *phase; + const char *location; + const char *unit; +} MO_MeterInput; + +void mo_meterInput_init(MO_MeterInput *mInput, MO_MeterInputType type); +MO_MeterInputType mo_meterInput_getType(MO_MeterInput *mInput); +void mo_meterInput_setType(MO_MeterInput *mInput, MO_MeterInputType type); + +#ifdef __cplusplus +} //extern "C" + #include -#include -#include +#include #include -#include -#include namespace MicroOcpp { -class MeterValue : public MemoryManaged { -private: - Timestamp timestamp; - Vector> sampledValue; +class Clock; - 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); +struct SampledValue : public MemoryManaged { + MO_MeterInput& meterInput; - std::unique_ptr toJson(); + union { + int32_t valueInt; + float valueFloat; + char *valueString = nullptr; - const Timestamp& getTimestamp(); - void setTimestamp(Timestamp timestamp); + #if MO_ENABLE_V201 + MO_SignedMeterValue201 *valueSigned; + #endif //MO_ENABLE_V201 + }; - ReadingContext getReadingContext(); + SampledValue(MO_MeterInput& meterInput); + ~SampledValue(); - void setTxNr(unsigned int txNr); - int getTxNr(); + enum class Type : uint8_t { + UNDEFINED, + Int, + Float, + String, + SignedValue, + }; + Type getType(); +}; - void setOpNr(unsigned int opNr); - unsigned int getOpNr(); +struct MeterValue : public MemoryManaged { + Timestamp timestamp; + Vector sampledValue; + MO_ReadingContext readingContext = MO_ReadingContext_UNDEFINED; //all SampledValues in a MeterValue have the same MO_ReadingContext - void advanceAttemptNr(); - unsigned int getAttemptNr(); + int txNr = -1; + unsigned int opNr = 1; + unsigned int attemptNr = 0; + Timestamp attemptTime; - unsigned long getAttemptTime(); - void setAttemptTime(unsigned long timestamp); -}; + MeterValue(); + ~MeterValue(); -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); + bool parseJson(Clock& clock, Vector& meterInputs, JsonObject in); //only parses internal format + int getJsonCapacity(int ocppVersion, bool internalFormat); + bool toJson(Clock& clock, int ocppVersion, bool internalFormat, JsonObject out); }; -} - +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 +#endif //__cplusplus #endif diff --git a/src/MicroOcpp/Model/Metering/MeterValuesV201.cpp b/src/MicroOcpp/Model/Metering/MeterValuesV201.cpp deleted file mode 100644 index 97376774..00000000 --- a/src/MicroOcpp/Model/Metering/MeterValuesV201.cpp +++ /dev/null @@ -1,361 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/Metering/MeterValuesV201.h b/src/MicroOcpp/Model/Metering/MeterValuesV201.h deleted file mode 100644 index f35ec146..00000000 --- a/src/MicroOcpp/Model/Metering/MeterValuesV201.h +++ /dev/null @@ -1,152 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/Metering/MeteringConnector.cpp b/src/MicroOcpp/Model/Metering/MeteringConnector.cpp deleted file mode 100644 index 378232a5..00000000 --- a/src/MicroOcpp/Model/Metering/MeteringConnector.cpp +++ /dev/null @@ -1,304 +0,0 @@ -// 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; -} diff --git a/src/MicroOcpp/Model/Metering/MeteringConnector.h b/src/MicroOcpp/Model/Metering/MeteringConnector.h deleted file mode 100644 index 75873ff6..00000000 --- a/src/MicroOcpp/Model/Metering/MeteringConnector.h +++ /dev/null @@ -1,95 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/Metering/MeteringService.cpp b/src/MicroOcpp/Model/Metering/MeteringService.cpp index 73ab4deb..98934d6b 100644 --- a/src/MicroOcpp/Model/Metering/MeteringService.cpp +++ b/src/MicroOcpp/Model/Metering/MeteringService.cpp @@ -1,163 +1,968 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License +#include +#include + +#include +#include +#include #include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include #include -using namespace MicroOcpp; +#if MO_ENABLE_V16 || MO_ENABLE_V201 -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; +namespace MicroOcpp { + +MeterValue *takeMeterValue(Clock& clock, Vector& meterInputs, unsigned int evseId, MO_ReadingContext readingContext, uint8_t inputSelectFlag, const char *memoryTag) { + + size_t samplesSize = 0; + for (size_t i = 0; i < meterInputs.size(); i++) { + if (meterInputs[i].mo_flags & inputSelectFlag) { + samplesSize++; + } + } + + if (samplesSize == 0) { + MO_DBG_DEBUG("no meter inputs selected"); + return nullptr; + } + + auto meterValue = new MeterValue(); + if (!meterValue) { + MO_DBG_ERR("OOM"); + goto fail; + } + + meterValue->sampledValue.resize(samplesSize); + if (meterValue->sampledValue.capacity() < samplesSize) { + MO_DBG_ERR("OOM"); + goto fail; + } + + meterValue->timestamp = clock.now(); + meterValue->readingContext = readingContext; + + for (size_t i = 0; i < meterInputs.size(); i++) { + auto& mInput = meterInputs[i]; + if (mInput.mo_flags & inputSelectFlag) { + auto sv = new SampledValue(mInput); + if (!sv) { + MO_DBG_ERR("OOM"); + goto fail; + } + + switch (mInput.type) { + case MO_MeterInputType_Int: + sv->valueInt = mInput.getInt(); + break; + case MO_MeterInputType_IntWithArgs: + sv->valueInt = mInput.getInt2(readingContext, evseId, mInput.user_data); + break; + case MO_MeterInputType_Float: + sv->valueFloat = mInput.getFloat(); + break; + case MO_MeterInputType_FloatWithArgs: + sv->valueFloat = mInput.getFloat2(readingContext, evseId, mInput.user_data); + break; + case MO_MeterInputType_String: + case MO_MeterInputType_StringWithArgs: { + char bufStack [100]; + int ret = -1; + if (mInput.type == MO_MeterInputType_String) { + ret = mInput.getString(bufStack, sizeof(bufStack)); + } else { //MO_MeterInputType_StringWithArgs + ret = mInput.getString2(bufStack, sizeof(bufStack), readingContext, evseId, mInput.user_data); + } + if (ret < 0 || ret > MO_SAMPLEDVALUE_STRING_MAX_LEN) { + MO_DBG_ERR("meterInput getString cb: %i", ret); + delete sv; + sv = nullptr; + break; //failure, skip this measurand + } + + if (ret > 0) { + size_t size = ret + 1; + sv->valueString = static_cast(MO_MALLOC(memoryTag, size)); + if (!sv->valueString) { + MO_DBG_ERR("OOM"); + delete sv; + sv = nullptr; + break; //failure, skip this measurand + } + memset(sv->valueString, 0, size); + + if ((size_t)ret < sizeof(bufStack)) { + //bufStack contains value + snprintf(sv->valueString, size, "%s", bufStack); + } else { + //need to re-run getString with properly sized buffer + if (mInput.type == MO_MeterInputType_String) { + ret = mInput.getString(sv->valueString, size); + } else { //MO_MeterInputType_StringWithArgs + ret = mInput.getString2(sv->valueString, size, readingContext, evseId, mInput.user_data); + } + if (ret < 0 || (size_t)ret >= size) { + MO_DBG_ERR("meterInput getString cb: %i", ret); + MO_FREE(sv->valueString); + sv->valueString = nullptr; + delete sv; + sv = nullptr; + break; //failure, skip this measurand + } + } + } + + break; //success + } + #if MO_ENABLE_V201 + case MO_MeterInputType_SignedValueWithArgs: + sv->valueSigned = static_cast(MO_MALLOC(memoryTag, sizeof(MO_SignedMeterValue201))); + if (!sv->valueSigned) { + MO_DBG_ERR("OOM"); + delete sv; + sv = nullptr; + break; //failure, skip this measurand + } + memset(sv->valueSigned, 0, sizeof(*sv->valueSigned)); + + if (!mInput.getSignedValue2(sv->valueSigned, readingContext, evseId, mInput.user_data)) { + if (sv->valueSigned->onDestroy) { + sv->valueSigned->onDestroy(sv->valueSigned->user_data); + } + MO_FREE(sv->valueSigned); + delete sv; + sv = nullptr; + break; //undefined value skip this measurand + } + break; + #endif //MO_ENABLE_V201 + case MO_MeterInputType_UNDEFINED: + MO_DBG_ERR("need to set type of meter input"); + delete sv; + sv = nullptr; + break; //failure, skip this measurand } - r = l + 1; - while (*r != '\0' && *r != ',') { - r++; + + if (sv) { + meterValue->sampledValue.push_back(sv); } - bool found = false; - for (size_t cId = 0; cId < connectors.size(); cId++) { - if (connectors[cId]->existsSampler(l, (size_t) (r - l))) { + } + } + + return meterValue; +fail: + delete meterValue; + meterValue = nullptr; + return nullptr; +} + +void updateInputSelectFlag(const char *selectString, uint8_t flag, Vector& meterInputs) { + auto size = strlen(selectString) + 1; + size_t sl = 0, sr = 0; + while (selectString && sl < size) { + while (sr < size) { + if (selectString[sr] == ',') { + break; + } + sr++; + } + + if (sr != sl + 1) { + const char *csvMeasurand = selectString + sl; + size_t csvMeasurandLen = sr - sl; + + for (size_t i = 0; i < meterInputs.size(); i++) { + const char *measurand = meterInputs[i].measurand ? meterInputs[i].measurand : "Energy.Active.Import.Register"; + if (!strncmp(measurand, csvMeasurand, csvMeasurandLen) && strlen(measurand) == csvMeasurandLen) { + meterInputs[i].mo_flags |= flag; + } else { + meterInputs[i].mo_flags &= ~flag; + } + } + } + + sr++; + sl = sr; + } +} + +bool validateSelectString(const char *selectString, void *user_data) { + auto& context = *reinterpret_cast(user_data); + + bool isValid = true; + const char *l = selectString; //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++; + } + + const char *csvMeasurand = l; + size_t csvMeasurandLen = (size_t)(r - l); + + //check if measurand exists in MeterInputs. Search through all EVSEs + bool found = false; + for (unsigned int evseId = 0; evseId < context.getModelCommon().getNumEvseId(); evseId++) { + + Vector *meterInputsPtr = nullptr; + + #if MO_ENABLE_V16 + if (context.getOcppVersion() == MO_OCPP_V16) { + auto mService = context.getModel16().getMeteringService(); + auto evse = mService ? mService->getEvse(evseId) : nullptr; + meterInputsPtr = evse ? &evse->getMeterInputs() : nullptr; + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (context.getOcppVersion() == MO_OCPP_V201) { + auto mService = context.getModel201().getMeteringService(); + auto evse = mService ? mService->getEvse(evseId) : nullptr; + meterInputsPtr = evse ? &evse->getMeterInputs() : nullptr; + } + #endif //MO_ENABLE_V201 + + if (!meterInputsPtr) { + MO_DBG_ERR("internal error"); + continue; + } + + auto& meterInputs = *meterInputsPtr; + + for (size_t i = 0; i < meterInputs.size(); i++) { + const char *measurand = meterInputs[i].measurand ? meterInputs[i].measurand : "Energy.Active.Import.Register"; + if (!strncmp(measurand, csvMeasurand, csvMeasurandLen) && strlen(measurand) == csvMeasurandLen) { found = true; break; } } - if (!found) { - isValid = false; - MO_DBG_WARN("could not find metering device for %.*s", (int) (r - l), l); + + if (found) { break; } - l = r; } - return isValid; - }; + if (!found) { + isValid = false; + MO_DBG_WARN("could not find metering device for %.*s", (int)csvMeasurandLen, csvMeasurand); + break; + } + l = r; + } + return isValid; +} + +} //namespace MicroOcpp + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 + +#if MO_ENABLE_V16 + +#define MO_FLAG_MeterValuesSampledData (1 << 0) +#define MO_FLAG_MeterValuesAlignedData (1 << 1) +#define MO_FLAG_StopTxnSampledData (1 << 2) +#define MO_FLAG_StopTxnAlignedData (1 << 3) + +using namespace MicroOcpp; - registerConfigurationValidator("MeterValuesSampledData", validateSelectString); - registerConfigurationValidator("StopTxnSampledData", validateSelectString); - registerConfigurationValidator("MeterValuesAlignedData", validateSelectString); - registerConfigurationValidator("StopTxnAlignedData", validateSelectString); - registerConfigurationValidator("MeterValueSampleInterval", VALIDATE_UNSIGNED_INT); - registerConfigurationValidator("ClockAlignedDataInterval", VALIDATE_UNSIGNED_INT); +v16::MeteringServiceEvse::MeteringServiceEvse(Context& context, MeteringService& mService, unsigned int connectorId) + : MemoryManaged("v16.Metering.MeteringServiceEvse"), context(context), clock(context.getClock()), model(context.getModel16()), mService(mService), connectorId(connectorId), meterData(makeVector(getMemoryTag())), meterInputs(makeVector(getMemoryTag())) { - /* - * 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(); +v16::MeteringServiceEvse::~MeteringServiceEvse() { + for (size_t i = 0; i < meterData.size(); i++) { + delete meterData[i]; } + meterData.clear(); + delete meterDataFront; + meterDataFront = nullptr; } -void MeteringService::addMeterValueSampler(int connectorId, std::unique_ptr meterValueSampler) { - if (connectorId < 0 || connectorId >= (int) connectors.size()) { - MO_DBG_ERR("connectorId is out of bounds"); +bool v16::MeteringServiceEvse::addMeterInput(MO_MeterInput meterInput) { + auto capacity = meterInputs.size() + 1; + meterInputs.resize(capacity); + if (meterInputs.capacity() < capacity) { + MO_DBG_ERR("OOM"); + return false; + } + + meterInput.mo_flags = 0; + meterInputs.push_back(meterInput); + return true; +} + +Vector& v16::MeteringServiceEvse::getMeterInputs() { + return meterInputs; +} + +bool v16::MeteringServiceEvse::setTxEnergyMeterInput(int32_t (*getInt)()) { + txEnergyMeterInput = getInt; + return true; +} + +bool v16::MeteringServiceEvse::setTxEnergyMeterInput2(int32_t (*getInt2)(MO_ReadingContext readingContext, unsigned int evseId, void *user_data), void *user_data) { + txEnergyMeterInput2 = getInt2; + txEnergyMeterInput2_user_data = user_data; + return true; +} + +bool v16::MeteringServiceEvse::setup() { + + context.getMessageService().addSendQueue(this); + + lastAlignedTime = context.getClock().now(); + + filesystem = context.getFilesystem(); + if (!filesystem) { + MO_DBG_DEBUG("volatile mode"); + } + + auto txSvc = context.getModel16().getTransactionService(); + txSvcEvse = txSvc ? txSvc->getEvse(connectorId) : nullptr; + if (!txSvcEvse) { + MO_DBG_ERR("setup failure"); + return false; + } + + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("setup failure"); + return false; + } + + if (!txEnergyMeterInput && !txEnergyMeterInput2) { + //search default energy meter + bool found = false; + for (size_t i = 0; i < meterInputs.size(); i++) { + auto& mInput = meterInputs[i]; + if (mInput.measurand && !strcmp(mInput.measurand, "Energy.Active.Import.Register") && + (!mInput.phase || !*mInput.phase) && + (!mInput.location || !strcmp(mInput.location, "Outlet")) && + (!mInput.unit || !strcmp(mInput.unit, "Wh")) && + (mInput.type == MO_MeterInputType_Int || mInput.type == MO_MeterInputType_IntWithArgs)) { + + if (mInput.type == MO_MeterInputType_Int) { + txEnergyMeterInput = mInput.getInt; + } else { + txEnergyMeterInput2 = mInput.getInt2; + txEnergyMeterInput2_user_data = mInput.user_data; + } + found = true; + } + } + + if (found) { + MO_DBG_DEBUG("found energy input for Start- and StopTx values"); + } else { + MO_DBG_WARN("no energy meter set up. Start- and StopTx will report 0 Wh"); + } + } + + return true; +} + +MicroOcpp::MeterValue *v16::MeteringServiceEvse::takeMeterValue(MO_ReadingContext readingContext, uint8_t inputSelectFlag) { + return MicroOcpp::takeMeterValue(context.getClock(), meterInputs, connectorId, readingContext, inputSelectFlag, getMemoryTag()); +} + +void v16::MeteringServiceEvse::loop() { + + bool txBreak = false; + auto transaction = txSvcEvse->getTransaction(); + txBreak = (transaction && transaction->isRunning()) != trackTxRunning; + trackTxRunning = (transaction && transaction->isRunning()); + + if (txBreak) { + lastSampleTime = clock.getUptimeInt(); + } + + if (!transaction && connectorId != 0 && mService.meterValuesInTxOnlyBool->getBool()) { + //don't take any MeterValues outside of transactions on connectorIds other than 0 return; } - connectors[connectorId]->addMeterValueSampler(std::move(meterValueSampler)); + + updateInputSelectFlags(); + + if (mService.clockAlignedDataIntervalInt->getInt() >= 1 && clock.now().isUnixTime()) { + + bool shouldTakeMeasurement = false; + bool shouldUpdateLastTime = false; + + auto& timestampNow = clock.now(); + int32_t dt; + bool dt_defined = clock.delta(timestampNow, lastAlignedTime, dt); + if (!dt_defined) { + //lastAlignedTime not set yet. Do that now + shouldUpdateLastTime = true; + MO_DBG_DEBUG("Clock aligned measurement: initial setting"); + } else if (dt < 0) { + //timestampNow before lastAligned time - probably the clock has been adjusted + shouldUpdateLastTime = true; + MO_DBG_DEBUG("Clock aligned measurement: correct time after clock adjustment (%i)", (int)dt); + } else if (dt < mService.clockAlignedDataIntervalInt->getInt()) { + //dt is number of seconds elapsed since last measurements and seconds < interval, so do nothing + (void)0; + } else if (dt <= mService.clockAlignedDataIntervalInt->getInt() + 60) { + //elapsed seconds >= interval and still within tolerance band of 60 seconds, take measurement! + shouldTakeMeasurement = true; + shouldUpdateLastTime = true; + MO_DBG_DEBUG("Clock aligned measurement: take measurement (%i)", (int)dt); + } else { + //elapsed seconds is past the tolerance band. Measurement wouldn't be clock-aligned anymore, so skip + shouldUpdateLastTime = true; + MO_DBG_DEBUG("Clock aligned measurement: tolerance exceeded, skip measurement (%i)", (int)dt); + } + + if (shouldTakeMeasurement) { + + if (auto alignedMeterValue = takeMeterValue(MO_ReadingContext_SampleClock, MO_FLAG_MeterValuesAlignedData)) { + if (meterData.size() >= MO_MVRECORD_SIZE) { + MO_DBG_INFO("MeterValue cache full. Drop old MV"); + auto mv = meterData.begin(); + delete *mv; + meterData.erase(mv); + } + alignedMeterValue->opNr = context.getMessageService().getNextOpNr(); + if (transaction) { + alignedMeterValue->txNr = transaction->getTxNr(); + } + meterData.push_back(alignedMeterValue); + } + + if (transaction) { + if (auto alignedStopTx = takeMeterValue(MO_ReadingContext_SampleClock, MO_FLAG_StopTxnAlignedData)) { + addTxMeterData(*transaction, alignedStopTx); + } + } + } + + if (shouldUpdateLastTime) { + Timestamp midnightBase; + clock.parseString("2010-01-01T00:00:00Z", midnightBase); + int32_t dt; + clock.delta(timestampNow, midnightBase, dt); //dt is the number of seconds since Jan 1 2010 + dt %= (int32_t)(3600 * 24); //dt is the number of seconds since midnight + Timestamp midnight = timestampNow; + clock.add(midnight, -dt); //midnight is last midnight (i.e. today at 00:00) + if (dt < mService.clockAlignedDataIntervalInt->getInt()) { + //close to midnight, so align to midnight + lastAlignedTime = midnight; + } else { + //at least one period since midnight has elapsed, so align to last period + dt /= mService.clockAlignedDataIntervalInt->getInt(); //floor dt to the last full period + dt *= mService.clockAlignedDataIntervalInt->getInt(); + lastAlignedTime = midnight; + clock.add(lastAlignedTime, dt); + } + } + } + + if (mService.meterValueSampleIntervalInt->getInt() >= 1) { + //record periodic tx data + + if (clock.getUptimeInt() - lastSampleTime >= mService.meterValueSampleIntervalInt->getInt()) { + if (auto sampledMeterValue = takeMeterValue(MO_ReadingContext_SamplePeriodic, MO_FLAG_MeterValuesSampledData)) { + if (meterData.size() >= MO_MVRECORD_SIZE) { + auto mv = meterData.begin(); + delete *mv; + meterData.erase(mv); + } + sampledMeterValue->opNr = context.getMessageService().getNextOpNr(); + if (transaction) { + sampledMeterValue->txNr = transaction->getTxNr(); + } + meterData.push_back(sampledMeterValue); + } + + if (transaction && mService.stopTxnDataCapturePeriodicBool->getBool()) { + if (auto sampleStopTx = takeMeterValue(MO_ReadingContext_SamplePeriodic, MO_FLAG_StopTxnSampledData)) { + addTxMeterData(*transaction, sampleStopTx); + } + } + + lastSampleTime = clock.getUptimeInt(); + } + } } -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"); +Operation *v16::MeteringServiceEvse::createTriggeredMeterValues() { + + auto meterValue = takeMeterValue(MO_ReadingContext_Trigger, MO_FLAG_MeterValuesSampledData); + if (!meterValue) { + MO_DBG_ERR("OOM"); 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"); + int transactionId = -1; + if (auto tx = txSvcEvse->getTransaction()) { + transactionId = tx->getTransactionId(); + } + + auto operation = new MeterValues(context, connectorId, transactionId, meterValue, /*transferOwnership*/ true); + if (!operation) { + MO_DBG_ERR("OOM"); + delete meterValue; return nullptr; } - auto msg = connectors[connectorId]->takeTriggeredMeterValues(); - if (msg) { - auto meterValues = makeRequest(std::move(msg)); - meterValues->setTimeout(120000); - return meterValues; + return operation; +} + +int32_t v16::MeteringServiceEvse::readTxEnergyMeter(MO_ReadingContext readingContext) { + if (txEnergyMeterInput) { + return txEnergyMeterInput(); + } else if (txEnergyMeterInput2) { + return txEnergyMeterInput2(readingContext, connectorId, txEnergyMeterInput2_user_data); + } else { + return 0; } - 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; +bool v16::MeteringServiceEvse::addTxMeterData(Transaction& transaction, MeterValue *mv) { + + auto& txMeterValues = transaction.getTxMeterValues(); + + unsigned int mvIndex; + bool replaceEntry = false; + if (txMeterValues.size() >= MO_STOPTXDATA_MAX_SIZE) { + //replace last entry + mvIndex = txMeterValues.size() - 1; + replaceEntry = true; + } else { + //append + mvIndex = txMeterValues.size(); } - auto connectorId = transaction->getConnectorId(); - if (connectorId >= connectors.size()) { - MO_DBG_ERR("connectorId is out of bounds"); - return; + + size_t capacity = mvIndex + 1; + txMeterValues.resize(capacity); + if (txMeterValues.size() < capacity) { + MO_DBG_ERR("OOM"); + goto fail; + } + + if (filesystem) { + auto ret = MeterStore::store(filesystem, context, connectorId, transaction.getTxNr(), mvIndex, *mv); + if (ret != FilesystemUtils::StoreStatus::Success) { + MO_DBG_ERR("fs failure"); + goto fail; + } } - connectors[connectorId]->beginTxMeterData(transaction); + + if (replaceEntry) { + delete txMeterValues.back(); + txMeterValues.back() = mv; + MO_DBG_DEBUG("updated latest sd"); + } else { + txMeterValues.emplace_back(mv); + MO_DBG_DEBUG("added sd"); + } + return true; +fail: + delete mv; + return false; } -std::shared_ptr MeteringService::endTxMeterData(Transaction *transaction) { - if (!transaction) { - MO_DBG_ERR("invalid argument"); +bool v16::MeteringServiceEvse::beginTxMeterData(Transaction *transaction) { + + auto& txMeterValues = transaction->getTxMeterValues(); + + if (!txMeterValues.empty()) { + //first MV already taken + return true; + } + + auto sampleTxBegin = takeMeterValue(MO_ReadingContext_TransactionBegin, MO_FLAG_StopTxnSampledData); + if (!sampleTxBegin) { + return true; + } + + return addTxMeterData(*transaction, sampleTxBegin); +} + +bool v16::MeteringServiceEvse::endTxMeterData(Transaction *transaction) { + + auto& txMeterValues = transaction->getTxMeterValues(); + + if (!txMeterValues.empty() && txMeterValues.back()->readingContext == MO_ReadingContext_TransactionEnd) { + //final MV already taken + return true; + } + + auto sampleTxEnd = takeMeterValue(MO_ReadingContext_TransactionEnd, MO_FLAG_StopTxnSampledData); + if (!sampleTxEnd) { + return true; + } + + return addTxMeterData(*transaction, sampleTxEnd); +} + +void v16::MeteringServiceEvse::updateInputSelectFlags() { + uint16_t selectInputsWriteCount = 0; + selectInputsWriteCount += mService.meterValuesSampledDataString->getWriteCount(); + selectInputsWriteCount += mService.stopTxnSampledDataString->getWriteCount(); + selectInputsWriteCount += mService.meterValuesAlignedDataString->getWriteCount(); + selectInputsWriteCount += mService.stopTxnAlignedDataString->getWriteCount(); + selectInputsWriteCount += (uint16_t)meterInputs.size(); //also invalidate if new meterInputs were added + if (selectInputsWriteCount != trackSelectInputs) { + MO_DBG_DEBUG("Updating observed samplers after config change or meterInputs added"); + updateInputSelectFlag(mService.meterValuesSampledDataString->getString(), MO_FLAG_MeterValuesSampledData, meterInputs); + updateInputSelectFlag(mService.stopTxnSampledDataString->getString(), MO_FLAG_StopTxnSampledData, meterInputs); + updateInputSelectFlag(mService.meterValuesAlignedDataString->getString(), MO_FLAG_MeterValuesAlignedData, meterInputs); + updateInputSelectFlag(mService.stopTxnAlignedDataString->getString(), MO_FLAG_StopTxnAlignedData, meterInputs); + trackSelectInputs = selectInputsWriteCount; + } +} + +unsigned int v16::MeteringServiceEvse::getFrontRequestOpNr() { + if (!meterDataFront && !meterData.empty()) { + MO_DBG_DEBUG("advance MV front"); + meterDataFront = meterData.front(); //transfer ownership + meterData.erase(meterData.begin()); + } + if (meterDataFront) { + return meterDataFront->opNr; + } + return NoOperation; +} + +std::unique_ptr v16::MeteringServiceEvse::fetchFrontRequest() { + + if (!mService.connection->isConnected()) { + //offline behavior: pause sending messages and do not increment attempt counters return nullptr; } - auto connectorId = transaction->getConnectorId(); - if (connectorId >= connectors.size()) { - MO_DBG_ERR("connectorId is out of bounds"); + + if (!meterDataFront) { + return nullptr; + } + + if (meterValuesInProgress) { + //ensure that only one MeterValues request is being executed at the same time + return nullptr; + } + + if ((int)meterDataFront->attemptNr >= mService.transactionMessageAttemptsInt->getInt()) { + MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard MeterValue"); + delete meterDataFront; + meterDataFront = nullptr; + return nullptr; + } + + int32_t dtLastAttempt = MO_MAX_TIME; + if (meterDataFront->attemptTime.isDefined() && + !clock.delta(clock.getUptime(), meterDataFront->attemptTime, dtLastAttempt)) { + MO_DBG_ERR("internal error"); + } + + if (dtLastAttempt < (int)meterDataFront->attemptNr * std::max(0, mService.transactionMessageRetryIntervalInt->getInt())) { + return nullptr; + } + + meterDataFront->attemptNr++; + meterDataFront->attemptTime = clock.getUptime(); + + auto transaction = txSvcEvse->getTransactionFront(); + bool txNrMatch = (!transaction && meterDataFront->txNr < 0) || (transaction && meterDataFront->txNr >= 0 && (unsigned int)meterDataFront->txNr == transaction->getTxNr()); + if (!txNrMatch) { + MO_DBG_ERR("txNr mismatch"); //this could happen if tx-related MeterValues are sent shortly before StartTx or shortly after StopTx, or if txNr is mis-assigned during creation + transaction = nullptr; + } + + //discard MV if it belongs to silent tx + if (transaction && transaction->isSilent()) { + MO_DBG_DEBUG("Drop MeterValue belonging to silent tx"); + delete meterDataFront; + meterDataFront = nullptr; return nullptr; } - return connectors[connectorId]->endTxMeterData(transaction); + + int transactionId = -1; + if (transaction) { + transactionId = transaction->getTransactionId(); + } + + auto meterValues = makeRequest(context, new MeterValues(context, connectorId, transactionId, meterDataFront, /*transferOwnership*/ false)); + meterValues->setOnReceiveConf([this] (JsonObject) { + meterValuesInProgress = false; + + //operation success + MO_DBG_DEBUG("drop MV front"); + delete meterDataFront; + meterDataFront = nullptr; + }); + meterValues->setOnAbort([this] () { + meterValuesInProgress = false; + }); + + meterValuesInProgress = true; + + return meterValues; } -void MeteringService::abortTxMeterData(unsigned int connectorId) { - if (connectorId >= connectors.size()) { - MO_DBG_ERR("connectorId is out of bounds"); - return; +v16::MeteringService::MeteringService(Context& context) + : MemoryManaged("v16.Metering.MeteringService"), context(context) { + +} + +v16::MeteringService::~MeteringService() { + for (unsigned int i = 0; i < MO_NUM_EVSEID; i++) { + delete evses[i]; + evses[i] = nullptr; + } +} + +bool v16::MeteringService::setup() { + + connection = context.getConnection(); + if (!connection) { + MO_DBG_ERR("setup failure"); + return false; + } + + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("setup failure"); + return false; + } + + meterValuesSampledDataString = configService->declareConfiguration("MeterValuesSampledData", "Energy.Active.Import.Register,Power.Active.Import"); + stopTxnSampledDataString = configService->declareConfiguration("StopTxnSampledData", ""); + meterValueSampleIntervalInt = configService->declareConfiguration("MeterValueSampleInterval", 60); + + meterValuesAlignedDataString = configService->declareConfiguration("MeterValuesAlignedData", ""); + stopTxnAlignedDataString = configService->declareConfiguration("StopTxnAlignedData", ""); + clockAlignedDataIntervalInt = configService->declareConfiguration("ClockAlignedDataInterval", 0); + + meterValuesInTxOnlyBool = configService->declareConfiguration(MO_CONFIG_EXT_PREFIX "MeterValuesInTxOnly", true); + stopTxnDataCapturePeriodicBool = configService->declareConfiguration(MO_CONFIG_EXT_PREFIX "StopTxnDataCapturePeriodic", false); + + transactionMessageAttemptsInt = configService->declareConfiguration("TransactionMessageAttempts", 3); + transactionMessageRetryIntervalInt = configService->declareConfiguration("TransactionMessageRetryInterval", 60); + + if (!meterValuesSampledDataString || + !stopTxnSampledDataString || + !meterValueSampleIntervalInt || + !meterValuesAlignedDataString || + !stopTxnAlignedDataString || + !clockAlignedDataIntervalInt || + !meterValuesInTxOnlyBool || + !stopTxnDataCapturePeriodicBool || + !transactionMessageAttemptsInt || + !transactionMessageRetryIntervalInt) { + MO_DBG_ERR("declareConfiguration failed"); + return false; + } + + configService->registerValidator("MeterValuesSampledData", validateSelectString, reinterpret_cast(&context)); + configService->registerValidator("StopTxnSampledData", validateSelectString, reinterpret_cast(&context)); + configService->registerValidator("MeterValueSampleInterval", VALIDATE_UNSIGNED_INT); + + configService->registerValidator("MeterValuesAlignedData", validateSelectString, reinterpret_cast(&context)); + configService->registerValidator("StopTxnAlignedData", validateSelectString, reinterpret_cast(&context)); + configService->registerValidator("ClockAlignedDataInterval", VALIDATE_UNSIGNED_INT); + + auto rcService = context.getModel16().getRemoteControlService(); + if (!rcService) { + MO_DBG_ERR("initialization error"); + return false; + } + + rcService->addTriggerMessageHandler("MeterValues", [] (Context& context, unsigned int evseId) { + auto mvSvc = context.getModel16().getMeteringService(); + auto mvSvcEvse = mvSvc ? mvSvc->getEvse(evseId) : nullptr; + return mvSvcEvse ? mvSvcEvse->createTriggeredMeterValues() : nullptr; + }); + + #if MO_ENABLE_MOCK_SERVER + context.getMessageService().registerOperation("MeterValues", nullptr, nullptr); + #endif //MO_ENABLE_MOCK_SERVER + + numEvseId = context.getModel16().getNumEvseId(); + for (unsigned int i = 0; i < numEvseId; i++) { + if (!getEvse(i) || !getEvse(i)->setup()) { + MO_DBG_ERR("connector init failure"); + return false; + } + } + + return true; +} + +void v16::MeteringService::loop(){ + for (unsigned int i = 0; i < numEvseId; i++){ + evses[i]->loop(); } - connectors[connectorId]->abortTxMeterData(); } -std::shared_ptr MeteringService::getStopTxMeterData(Transaction *transaction) { - if (!transaction) { - MO_DBG_ERR("invalid argument"); +v16::MeteringServiceEvse *v16::MeteringService::getEvse(unsigned int evseId) { + if (evseId >= numEvseId) { + MO_DBG_ERR("evseId out of bound"); return nullptr; } - auto connectorId = transaction->getConnectorId(); - if (connectorId >= connectors.size()) { - MO_DBG_ERR("connectorId is out of bounds"); + + if (!evses[evseId]) { + evses[evseId] = new MeteringServiceEvse(context, *this, evseId); + if (!evses[evseId]) { + MO_DBG_ERR("OOM"); + return nullptr; + } + } + + return evses[evseId]; +} + +#endif //MO_ENABLE_V16 + +#if MO_ENABLE_V201 + +#define MO_FLAG_SampledDataTxStarted (1 << 0) +#define MO_FLAG_SampledDataTxUpdated (1 << 1) +#define MO_FLAG_SampledDataTxEnded (1 << 2) +#define MO_FLAG_AlignedData (1 << 3) + +using namespace MicroOcpp; + +v201::MeteringServiceEvse::MeteringServiceEvse(Context& context, MeteringService& mService, unsigned int evseId) + : MemoryManaged("v201.MeterValues.MeteringServiceEvse"), context(context), mService(mService), evseId(evseId), meterInputs(makeVector(getMemoryTag())) { + +} + +bool v201::MeteringServiceEvse::setup() { + return true; //nothing to be done here +} + +bool v201::MeteringServiceEvse::addMeterInput(MO_MeterInput meterInput) { + auto capacity = meterInputs.size() + 1; + meterInputs.resize(capacity); + if (meterInputs.capacity() < capacity) { + MO_DBG_ERR("OOM"); + return false; + } + + meterInput.mo_flags = 0; + meterInputs.push_back(meterInput); + return true; +} + +Vector& v201::MeteringServiceEvse::getMeterInputs() { + return meterInputs; +} + +void v201::MeteringServiceEvse::updateInputSelectFlags() { + uint16_t selectInputsWriteCount = 0; + selectInputsWriteCount += mService.sampledDataTxStartedMeasurands->getWriteCount(); + selectInputsWriteCount += mService.sampledDataTxUpdatedMeasurands->getWriteCount(); + selectInputsWriteCount += mService.sampledDataTxEndedMeasurands->getWriteCount(); + selectInputsWriteCount += mService.alignedDataMeasurands->getWriteCount(); + selectInputsWriteCount += (uint16_t)meterInputs.size(); //also invalidate if new meterInputs were added + if (selectInputsWriteCount != trackSelectInputs) { + MO_DBG_DEBUG("Updating observed samplers after variables change or meterInputs added"); + updateInputSelectFlag(mService.sampledDataTxStartedMeasurands->getString(), MO_FLAG_SampledDataTxStarted, meterInputs); + updateInputSelectFlag(mService.sampledDataTxUpdatedMeasurands->getString(), MO_FLAG_SampledDataTxUpdated, meterInputs); + updateInputSelectFlag(mService.sampledDataTxEndedMeasurands->getString(), MO_FLAG_SampledDataTxEnded, meterInputs); + updateInputSelectFlag(mService.alignedDataMeasurands->getString(), MO_FLAG_AlignedData, meterInputs); + trackSelectInputs = selectInputsWriteCount; + } +} + +std::unique_ptr v201::MeteringServiceEvse::takeTxStartedMeterValue(MO_ReadingContext readingContext) { + updateInputSelectFlags(); + return std::unique_ptr(MicroOcpp::takeMeterValue(context.getClock(), meterInputs, evseId, readingContext, MO_FLAG_SampledDataTxStarted, getMemoryTag())); +} +std::unique_ptr v201::MeteringServiceEvse::takeTxUpdatedMeterValue(MO_ReadingContext readingContext) { + updateInputSelectFlags(); + return std::unique_ptr(MicroOcpp::takeMeterValue(context.getClock(), meterInputs, evseId, readingContext, MO_FLAG_SampledDataTxUpdated, getMemoryTag())); +} +std::unique_ptr v201::MeteringServiceEvse::takeTxEndedMeterValue(MO_ReadingContext readingContext) { + updateInputSelectFlags(); + return std::unique_ptr(MicroOcpp::takeMeterValue(context.getClock(), meterInputs, evseId, readingContext, MO_FLAG_SampledDataTxEnded, getMemoryTag())); +} +std::unique_ptr v201::MeteringServiceEvse::takeTriggeredMeterValues() { + updateInputSelectFlags(); + return std::unique_ptr(MicroOcpp::takeMeterValue(context.getClock(), meterInputs, evseId, MO_ReadingContext_Trigger, MO_FLAG_AlignedData, getMemoryTag())); +} + +v201::MeteringService::MeteringService(Context& context) : MemoryManaged("v201.Metering.MeteringService"), context(context) { + +} + +v201::MeteringService::~MeteringService() { + for (size_t i = 0; i < MO_NUM_EVSEID; i++) { + delete evses[i]; + evses[i] = nullptr; + } +} + +v201::MeteringServiceEvse *v201::MeteringService::getEvse(unsigned int evseId) { + if (evseId >= numEvseId) { + MO_DBG_ERR("evseId out of bound"); return nullptr; } - return connectors[connectorId]->getStopTxMeterData(transaction); + + if (!evses[evseId]) { + evses[evseId] = new MeteringServiceEvse(context, *this, evseId); + if (!evses[evseId]) { + MO_DBG_ERR("OOM"); + return nullptr; + } + } + + return evses[evseId]; } -bool MeteringService::removeTxMeterData(unsigned int connectorId, unsigned int txNr) { - return meterStore.remove(connectorId, txNr); +bool v201::MeteringService::setup() { + + auto varService = context.getModel201().getVariableService(); + if (!varService) { + return false; + } + + sampledDataTxStartedMeasurands = varService->declareVariable("SampledDataCtrlr", "TxStartedMeasurands", ""); + sampledDataTxUpdatedMeasurands = varService->declareVariable("SampledDataCtrlr", "TxUpdatedMeasurands", ""); + sampledDataTxEndedMeasurands = varService->declareVariable("SampledDataCtrlr", "TxEndedMeasurands", ""); + alignedDataMeasurands = varService->declareVariable("AlignedDataCtrlr", "AlignedDataMeasurands", ""); + + if (!sampledDataTxStartedMeasurands || + !sampledDataTxUpdatedMeasurands || + !sampledDataTxEndedMeasurands || + !alignedDataMeasurands) { + MO_DBG_ERR("failure to declare variable"); + return false; + } + + //define factory defaults + varService->declareVariable("SampledDataCtrlr", "TxStartedMeasurands", ""); + varService->declareVariable("SampledDataCtrlr", "TxUpdatedMeasurands", ""); + varService->declareVariable("SampledDataCtrlr", "TxEndedMeasurands", ""); + varService->declareVariable("AlignedDataCtrlr", "AlignedDataMeasurands", ""); + + varService->registerValidator("SampledDataCtrlr", "TxStartedMeasurands", validateSelectString, reinterpret_cast(&context)); + varService->registerValidator("SampledDataCtrlr", "TxUpdatedMeasurands", validateSelectString, reinterpret_cast(&context)); + varService->registerValidator("SampledDataCtrlr", "TxEndedMeasurands", validateSelectString, reinterpret_cast(&context)); + varService->registerValidator("AlignedDataCtrlr", "AlignedDataMeasurands", validateSelectString, reinterpret_cast(&context)); + + numEvseId = context.getModel201().getNumEvseId(); + for (size_t i = 0; i < numEvseId; i++) { + if (!getEvse(i) || !getEvse(i)->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } + } + + return true; } + +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Metering/MeteringService.h b/src/MicroOcpp/Model/Metering/MeteringService.h index 748a0b4a..49da11ed 100644 --- a/src/MicroOcpp/Model/Metering/MeteringService.h +++ b/src/MicroOcpp/Model/Metering/MeteringService.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_METERINGSERVICE_H @@ -8,46 +8,192 @@ #include #include -#include -#include -#include +#include +#include +#include +#include #include +#include +#include + +#if MO_ENABLE_V16 + +#ifndef MO_MVRECORD_SIZE +#define MO_MVRECORD_SIZE 10 +#endif namespace MicroOcpp { class Context; +class Clock; +class Operation; class Request; -class FilesystemAdapter; + +namespace v16 { + +class Model; +class MeteringService; +class Configuration; +class TransactionServiceEvse; +class Transaction; + +class MeteringServiceEvse : public MemoryManaged, public RequestQueue { +private: + Context& context; + Clock& clock; + Model& model; + MeteringService& mService; + const unsigned int connectorId; + MO_FilesystemAdapter *filesystem = nullptr; + TransactionServiceEvse *txSvcEvse = nullptr; + + Vector meterData; //has ownership + MeterValue *meterDataFront = nullptr; //has ownership + bool meterValuesInProgress = false; + + Vector meterInputs; + int32_t (*txEnergyMeterInput)() = nullptr; + int32_t (*txEnergyMeterInput2)(MO_ReadingContext readingContext, unsigned int evseId, void *user_data) = nullptr; + void *txEnergyMeterInput2_user_data = nullptr; + + MeterValue *takeMeterValue(MO_ReadingContext readingContext, uint8_t inputSelectFlag); + + int32_t lastSampleTime = 0; //in secs since boot + Timestamp lastAlignedTime; //as unix time stamp + bool trackTxRunning = false; + + bool addTxMeterData(Transaction& transaction, MeterValue *mv); + + uint16_t trackSelectInputs = -1; + void updateInputSelectFlags(); + +public: + MeteringServiceEvse(Context& context, MeteringService& mService, unsigned int connectorId); + virtual ~MeteringServiceEvse(); + + bool addMeterInput(MO_MeterInput meterInput); + Vector& getMeterInputs(); + + /* Specify energy meter for Start- and StopTx (if not used, MO will select general-purpose energy meter) + * `setTxEnergyMeterInput(cb)`: set simple callback without args + * `setTxEnergyMeterInput2(cb)`: set complex callback. When MO executes the callback, it passes the ReadingContext (i.e. if measurement is + * taken at the start, middle or stop of a transactinon), the evseId and the user_data pointer which the caller can freely select */ + bool setTxEnergyMeterInput(int32_t (*getInt)()); + bool setTxEnergyMeterInput2(int32_t (*getInt2)(MO_ReadingContext readingContext, unsigned int evseId, void *user_data), void *user_data = nullptr); + + bool setup(); + + void loop(); + + int32_t readTxEnergyMeter(MO_ReadingContext readingContext); + + Operation *createTriggeredMeterValues(); + + bool beginTxMeterData(Transaction *transaction); + + bool endTxMeterData(Transaction *transaction); + + //RequestQueue implementation + unsigned int getFrontRequestOpNr() override; + std::unique_ptr fetchFrontRequest() override; +}; class MeteringService : public MemoryManaged { private: Context& context; - MeterStore meterStore; + Connection *connection = nullptr; - Vector> connectors; + MeteringServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; + unsigned int numEvseId = MO_NUM_EVSEID; + + Configuration *meterValuesSampledDataString = nullptr; + Configuration *stopTxnSampledDataString = nullptr; + Configuration *meterValueSampleIntervalInt = nullptr; + + Configuration *meterValuesAlignedDataString = nullptr; + Configuration *stopTxnAlignedDataString = nullptr; + Configuration *clockAlignedDataIntervalInt = nullptr; + + Configuration *meterValuesInTxOnlyBool = nullptr; + Configuration *stopTxnDataCapturePeriodicBool = nullptr; + + Configuration *transactionMessageAttemptsInt = nullptr; + Configuration *transactionMessageRetryIntervalInt = nullptr; public: - MeteringService(Context& context, int numConnectors, std::shared_ptr filesystem); + MeteringService(Context& context); + ~MeteringService(); + + bool setup(); void loop(); - void addMeterValueSampler(int connectorId, std::unique_ptr meterValueSampler); + MeteringServiceEvse *getEvse(unsigned int evseId); + +friend class MeteringServiceEvse; +}; + +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 - std::unique_ptr readTxEnergyMeter(int connectorId, ReadingContext reason); +#if MO_ENABLE_V201 - std::unique_ptr takeTriggeredMeterValues(int connectorId); //snapshot of all meters now +namespace MicroOcpp { - void beginTxMeterData(Transaction *transaction); +class Context; - std::shared_ptr endTxMeterData(Transaction *transaction); //use return value to keep data in cache +namespace v201 { - void abortTxMeterData(unsigned int connectorId); //call this to free resources if txMeterData record is not ended normally. Does not remove files +class MeteringService; +class Variable; - std::shared_ptr getStopTxMeterData(Transaction *transaction); //prefer endTxMeterData when possible +class MeteringServiceEvse : public MemoryManaged { +private: + Context& context; + MeteringService& mService; + const unsigned int evseId; + + Vector meterInputs; - bool removeTxMeterData(unsigned int connectorId, unsigned int txNr); + uint16_t trackSelectInputs = -1; + void updateInputSelectFlags(); +public: + MeteringServiceEvse(Context& context, MeteringService& mService, unsigned int evseId); - int getNumConnectors() {return connectors.size();} + bool addMeterInput(MO_MeterInput meterInput); + Vector& getMeterInputs(); + + bool setup(); + + std::unique_ptr takeTxStartedMeterValue(MO_ReadingContext context = MO_ReadingContext_TransactionBegin); + std::unique_ptr takeTxUpdatedMeterValue(MO_ReadingContext context = MO_ReadingContext_SamplePeriodic); + std::unique_ptr takeTxEndedMeterValue(MO_ReadingContext context); + std::unique_ptr takeTriggeredMeterValues(); }; -} //end namespace MicroOcpp +class MeteringService : public MemoryManaged { +private: + Context& context; + MeteringServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; + unsigned int numEvseId = MO_NUM_EVSEID; + + Variable *sampledDataTxStartedMeasurands = nullptr; + Variable *sampledDataTxUpdatedMeasurands = nullptr; + Variable *sampledDataTxEndedMeasurands = nullptr; + Variable *alignedDataMeasurands = nullptr; +public: + MeteringService(Context& context); + ~MeteringService(); + + MeteringServiceEvse *getEvse(unsigned int evseId); + + bool setup(); + +friend class MeteringServiceEvse; +}; + +} //namespace v201 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Metering/ReadingContext.cpp b/src/MicroOcpp/Model/Metering/ReadingContext.cpp index 2ea24886..639f878d 100644 --- a/src/MicroOcpp/Model/Metering/ReadingContext.cpp +++ b/src/MicroOcpp/Model/Metering/ReadingContext.cpp @@ -7,59 +7,62 @@ #include #include +#if MO_ENABLE_V16 || MO_ENABLE_V201 + namespace MicroOcpp { -const char *serializeReadingContext(ReadingContext context) { +const char *serializeReadingContext(MO_ReadingContext context) { switch (context) { - case (ReadingContext_InterruptionBegin): + case (MO_ReadingContext_InterruptionBegin): return "Interruption.Begin"; - case (ReadingContext_InterruptionEnd): + case (MO_ReadingContext_InterruptionEnd): return "Interruption.End"; - case (ReadingContext_Other): + case (MO_ReadingContext_Other): return "Other"; - case (ReadingContext_SampleClock): + case (MO_ReadingContext_SampleClock): return "Sample.Clock"; - case (ReadingContext_SamplePeriodic): + case (MO_ReadingContext_SamplePeriodic): return "Sample.Periodic"; - case (ReadingContext_TransactionBegin): + case (MO_ReadingContext_TransactionBegin): return "Transaction.Begin"; - case (ReadingContext_TransactionEnd): + case (MO_ReadingContext_TransactionEnd): return "Transaction.End"; - case (ReadingContext_Trigger): + case (MO_ReadingContext_Trigger): return "Trigger"; default: - MO_DBG_ERR("ReadingContext not specified"); + MO_DBG_ERR("MO_ReadingContext not specified"); /* fall through */ - case (ReadingContext_UNDEFINED): + case (MO_ReadingContext_UNDEFINED): return ""; } } -ReadingContext deserializeReadingContext(const char *context) { +MO_ReadingContext deserializeReadingContext(const char *context) { if (!context) { MO_DBG_ERR("Invalid argument"); - return ReadingContext_UNDEFINED; + return MO_ReadingContext_UNDEFINED; } if (!strcmp(context, "Sample.Periodic")) { - return ReadingContext_SamplePeriodic; + return MO_ReadingContext_SamplePeriodic; } else if (!strcmp(context, "Sample.Clock")) { - return ReadingContext_SampleClock; + return MO_ReadingContext_SampleClock; } else if (!strcmp(context, "Transaction.Begin")) { - return ReadingContext_TransactionBegin; + return MO_ReadingContext_TransactionBegin; } else if (!strcmp(context, "Transaction.End")) { - return ReadingContext_TransactionEnd; + return MO_ReadingContext_TransactionEnd; } else if (!strcmp(context, "Other")) { - return ReadingContext_Other; + return MO_ReadingContext_Other; } else if (!strcmp(context, "Interruption.Begin")) { - return ReadingContext_InterruptionBegin; + return MO_ReadingContext_InterruptionBegin; } else if (!strcmp(context, "Interruption.End")) { - return ReadingContext_InterruptionEnd; + return MO_ReadingContext_InterruptionEnd; } else if (!strcmp(context, "Trigger")) { - return ReadingContext_Trigger; + return MO_ReadingContext_Trigger; } MO_DBG_ERR("ReadingContext not specified %.10s", context); - return ReadingContext_UNDEFINED; + return MO_ReadingContext_UNDEFINED; } } //namespace MicroOcpp +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Metering/ReadingContext.h b/src/MicroOcpp/Model/Metering/ReadingContext.h index 592914e1..be38a4fe 100644 --- a/src/MicroOcpp/Model/Metering/ReadingContext.h +++ b/src/MicroOcpp/Model/Metering/ReadingContext.h @@ -5,24 +5,30 @@ #ifndef MO_READINGCONTEXT_H #define MO_READINGCONTEXT_H +#include + +#if MO_ENABLE_V16 || MO_ENABLE_V201 + typedef enum { - ReadingContext_UNDEFINED, - ReadingContext_InterruptionBegin, - ReadingContext_InterruptionEnd, - ReadingContext_Other, - ReadingContext_SampleClock, - ReadingContext_SamplePeriodic, - ReadingContext_TransactionBegin, - ReadingContext_TransactionEnd, - ReadingContext_Trigger -} ReadingContext; + MO_ReadingContext_UNDEFINED, + MO_ReadingContext_InterruptionBegin, + MO_ReadingContext_InterruptionEnd, + MO_ReadingContext_Other, + MO_ReadingContext_SampleClock, + MO_ReadingContext_SamplePeriodic, + MO_ReadingContext_TransactionBegin, + MO_ReadingContext_TransactionEnd, + MO_ReadingContext_Trigger +} MO_ReadingContext; #ifdef __cplusplus namespace MicroOcpp { -const char *serializeReadingContext(ReadingContext context); -ReadingContext deserializeReadingContext(const char *serialized); +const char *serializeReadingContext(MO_ReadingContext context); +MO_ReadingContext deserializeReadingContext(const char *serialized); } -#endif +#endif //__cplusplus + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Metering/SampledValue.cpp b/src/MicroOcpp/Model/Metering/SampledValue.cpp deleted file mode 100644 index 4168a048..00000000 --- a/src/MicroOcpp/Model/Metering/SampledValue.cpp +++ /dev/null @@ -1,61 +0,0 @@ -// 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; -} diff --git a/src/MicroOcpp/Model/Metering/SampledValue.h b/src/MicroOcpp/Model/Metering/SampledValue.h deleted file mode 100644 index 350990ae..00000000 --- a/src/MicroOcpp/Model/Metering/SampledValue.h +++ /dev/null @@ -1,147 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/Model.cpp b/src/MicroOcpp/Model/Model.cpp index f2e19c1e..55f50569 100644 --- a/src/MicroOcpp/Model/Model.cpp +++ b/src/MicroOcpp/Model/Model.cpp @@ -1,14 +1,15 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #include +#include +#include #include #include -#include #include -#include +#include #include #include #include @@ -17,336 +18,594 @@ #include #include #include -#include +#include +#include +#include #include #include #include - -#include - +#include +#include +#include #include +#if MO_ENABLE_V16 || MO_ENABLE_V201 + using namespace MicroOcpp; -Model::Model(ProtocolVersion version, uint16_t bootNr) : MemoryManaged("Model"), connectors(makeVector>(getMemoryTag())), version(version), bootNr(bootNr) { +ModelCommon::ModelCommon(Context& context) : context(context) { } -Model::~Model() = default; +ModelCommon::~ModelCommon() { + delete bootService; + bootService = nullptr; + delete heartbeatService; + heartbeatService = nullptr; + delete remoteControlService; + remoteControlService = nullptr; -void Model::loop() { +#if MO_ENABLE_DIAGNOSTICS + delete diagnosticsService; + diagnosticsService = nullptr; +#endif //MO_ENABLE_DIAGNOSTICS - if (bootService) { - bootService->loop(); +#if MO_ENABLE_SMARTCHARGING + delete smartChargingService; + smartChargingService = nullptr; +#endif //MO_ENABLE_SMARTCHARGING + +#if MO_ENABLE_CERT_MGMT + delete certService; + certService = nullptr; +#endif //MO_ENABLE_CERT_MGMT + +#if MO_ENABLE_SECURITY_EVENT + delete secEventService; + secEventService = nullptr; +#endif //MO_ENABLE_SECURITY_EVENT + +} + +bool ModelCommon::setupCommon() { + + if (!getBootService() || !getBootService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; } - if (capabilitiesUpdated) { - updateSupportedStandardProfiles(); - capabilitiesUpdated = false; + if (!getHeartbeatService() || !getHeartbeatService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; } - if (!runTasks) { - return; + if (!getRemoteControlService() || !getRemoteControlService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; } - for (auto& connector : connectors) { - connector->loop(); +#if MO_ENABLE_DIAGNOSTICS + if (!getDiagnosticsService() || !getDiagnosticsService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; } +#endif //MO_ENABLE_DIAGNOSTICS - if (chargeControlCommon) - chargeControlCommon->loop(); +#if MO_ENABLE_SMARTCHARGING + if (!getSmartChargingService() || !getSmartChargingService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } +#endif //MO_ENABLE_SMARTCHARGING - if (smartChargingService) - smartChargingService->loop(); +#if MO_ENABLE_CERT_MGMT + if (!getCertificateService() || !getCertificateService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } +#endif //MO_ENABLE_CERT_MGMT - if (heartbeatService) - heartbeatService->loop(); +#if MO_ENABLE_SECURITY_EVENT + if (!getSecurityEventService() || !getSecurityEventService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } +#endif //MO_ENABLE_SECURITY_EVENT - if (meteringService) - meteringService->loop(); + return true; +} - if (diagnosticsService) - diagnosticsService->loop(); +void ModelCommon::loopCommon() { - if (firmwareService) - firmwareService->loop(); + if (bootService) { + bootService->loop(); + } -#if MO_ENABLE_RESERVATION - if (reservationService) - reservationService->loop(); -#endif //MO_ENABLE_RESERVATION + if (!runTasks) { + return; + } - if (resetService) - resetService->loop(); + if (heartbeatService) { + heartbeatService->loop(); + } -#if MO_ENABLE_V201 - if (availabilityService) - availabilityService->loop(); +#if MO_ENABLE_DIAGNOSTICS + if (diagnosticsService) { + diagnosticsService->loop(); + } +#endif //MO_ENABLE_DIAGNOSTICS - if (transactionService) - transactionService->loop(); - - if (resetServiceV201) - resetServiceV201->loop(); -#endif +#if MO_ENABLE_SMARTCHARGING + if (smartChargingService) { + smartChargingService->loop(); + } +#endif //MO_ENABLE_SMARTCHARGING } -void Model::setTransactionStore(std::unique_ptr ts) { - transactionStore = std::move(ts); - capabilitiesUpdated = true; +void ModelCommon::setNumEvseId(unsigned int numEvseId) { + if (numEvseId >= MO_NUM_EVSEID) { + MO_DBG_ERR("invalid arg"); + return; + } + this->numEvseId = numEvseId; } -TransactionStore *Model::getTransactionStore() { - return transactionStore.get(); +unsigned int ModelCommon::getNumEvseId() { + return numEvseId; } -void Model::setSmartChargingService(std::unique_ptr scs) { - smartChargingService = std::move(scs); - capabilitiesUpdated = true; +BootService *ModelCommon::getBootService() { + if (!bootService) { + bootService = new BootService(context); + } + return bootService; } -SmartChargingService* Model::getSmartChargingService() const { - return smartChargingService.get(); +HeartbeatService *ModelCommon::getHeartbeatService() { + if (!heartbeatService) { + heartbeatService = new HeartbeatService(context); + } + return heartbeatService; } -void Model::setConnectorsCommon(std::unique_ptr ccs) { - chargeControlCommon = std::move(ccs); - capabilitiesUpdated = true; +RemoteControlService *ModelCommon::getRemoteControlService() { + if (!remoteControlService) { + remoteControlService = new RemoteControlService(context); + } + return remoteControlService; } -ConnectorsCommon *Model::getConnectorsCommon() { - return chargeControlCommon.get(); +#if MO_ENABLE_DIAGNOSTICS +DiagnosticsService *ModelCommon::getDiagnosticsService() { + if (!diagnosticsService) { + diagnosticsService = new DiagnosticsService(context); + } + return diagnosticsService; } +#endif //MO_ENABLE_DIAGNOSTICS -void Model::setConnectors(Vector>&& connectors) { - this->connectors = std::move(connectors); - capabilitiesUpdated = true; +#if MO_ENABLE_SMARTCHARGING +SmartChargingService* ModelCommon::getSmartChargingService() { + if (!smartChargingService) { + smartChargingService = new SmartChargingService(context); + } + return smartChargingService; } +#endif //MO_ENABLE_SMARTCHARGING -unsigned int Model::getNumConnectors() const { - return connectors.size(); +#if MO_ENABLE_CERT_MGMT +CertificateService *ModelCommon::getCertificateService() { + if (!certService) { + certService = new CertificateService(context); + } + return certService; } +#endif //MO_ENABLE_CERT_MGMT -Connector *Model::getConnector(unsigned int connectorId) { - if (connectorId >= connectors.size()) { - MO_DBG_ERR("connector with connectorId %u does not exist", connectorId); - return nullptr; +#if MO_ENABLE_SECURITY_EVENT +SecurityEventService *ModelCommon::getSecurityEventService() { + if (!secEventService) { + secEventService = new SecurityEventService(context); } + return secEventService; +} +#endif //MO_ENABLE_SECURITY_EVENT + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 + +#if MO_ENABLE_V16 + +v16::Model::Model(Context& context) : MemoryManaged("v16.Model"), ModelCommon(context), context(context) { - return connectors[connectorId].get(); } -void Model::setMeteringSerivce(std::unique_ptr ms) { - meteringService = std::move(ms); - capabilitiesUpdated = true; +v16::Model::~Model() { + delete transactionService; + transactionService = nullptr; + delete meteringService; + meteringService = nullptr; + delete resetService; + resetService = nullptr; + delete availabilityService; + availabilityService = nullptr; + +#if MO_ENABLE_FIRMWAREMANAGEMENT + delete firmwareService; + firmwareService = nullptr; +#endif //MO_ENABLE_FIRMWAREMANAGEMENT + +#if MO_ENABLE_LOCAL_AUTH + delete authorizationService; + authorizationService = nullptr; +#endif //MO_ENABLE_LOCAL_AUTH + +#if MO_ENABLE_RESERVATION + delete reservationService; + reservationService = nullptr; +#endif //MO_ENABLE_RESERVATION + + delete configurationService; + configurationService = nullptr; } -MeteringService* Model::getMeteringService() const { - return meteringService.get(); +void v16::Model::updateSupportedStandardProfiles() { + + auto supportedFeatureProfilesString = + configurationService->declareConfiguration("SupportedFeatureProfiles", "", MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); + + if (!supportedFeatureProfilesString) { + MO_DBG_ERR("OOM"); + return; + } + + auto buf = makeString(getMemoryTag(), supportedFeatureProfilesString->getString()); + + if (transactionService && + availabilityService && + getRemoteControlService() && + getHeartbeatService() && + getBootService()) { + if (!strstr(supportedFeatureProfilesString->getString(), "Core")) { + if (!buf.empty()) buf += ','; + buf += "Core"; + } + } + + if (firmwareService || + getDiagnosticsService()) { + if (!strstr(supportedFeatureProfilesString->getString(), "FirmwareManagement")) { + if (!buf.empty()) buf += ','; + buf += "FirmwareManagement"; + } + } + +#if MO_ENABLE_LOCAL_AUTH + if (authorizationService) { + 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 (getSmartChargingService()) { + 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()); } -void Model::setFirmwareService(std::unique_ptr fws) { - firmwareService = std::move(fws); - capabilitiesUpdated = true; +v16::ConfigurationService *v16::Model::getConfigurationService() { + if (!configurationService) { + configurationService = new v16::ConfigurationService(context); + + // need extra init step so that other modules can declare configs before setup() + if (configurationService && !configurationService->init()) { + // configurationService cannot be inited + delete configurationService; + configurationService = nullptr; + } + } + return configurationService; } -FirmwareService *Model::getFirmwareService() const { - return firmwareService.get(); +v16::TransactionService *v16::Model::getTransactionService() { + if (!transactionService) { + transactionService = new TransactionService(context); + } + return transactionService; } -void Model::setDiagnosticsService(std::unique_ptr ds) { - diagnosticsService = std::move(ds); - capabilitiesUpdated = true; +v16::MeteringService* v16::Model::getMeteringService() { + if (!meteringService) { + meteringService = new MeteringService(context); + } + return meteringService; } -DiagnosticsService *Model::getDiagnosticsService() const { - return diagnosticsService.get(); +v16::ResetService *v16::Model::getResetService() { + if (!resetService) { + resetService = new ResetService(context); + } + return resetService; } -void Model::setHeartbeatService(std::unique_ptr hs) { - heartbeatService = std::move(hs); - capabilitiesUpdated = true; +v16::AvailabilityService *v16::Model::getAvailabilityService() { + if (!availabilityService) { + availabilityService = new v16::AvailabilityService(context); + } + return availabilityService; } -#if MO_ENABLE_LOCAL_AUTH -void Model::setAuthorizationService(std::unique_ptr as) { - authorizationService = std::move(as); - capabilitiesUpdated = true; +#if MO_ENABLE_FIRMWAREMANAGEMENT +v16::FirmwareService *v16::Model::getFirmwareService() { + if (!firmwareService) { + firmwareService = new FirmwareService(context); + } + return firmwareService; } +#endif //MO_ENABLE_FIRMWAREMANAGEMENT -AuthorizationService *Model::getAuthorizationService() { - return authorizationService.get(); +#if MO_ENABLE_LOCAL_AUTH +v16::AuthorizationService *v16::Model::getAuthorizationService() { + if (!authorizationService) { + authorizationService = new AuthorizationService(context); + } + return authorizationService; } #endif //MO_ENABLE_LOCAL_AUTH #if MO_ENABLE_RESERVATION -void Model::setReservationService(std::unique_ptr rs) { - reservationService = std::move(rs); - capabilitiesUpdated = true; +v16::ReservationService *v16::Model::getReservationService() { + if (!reservationService) { + reservationService = new ReservationService(context); + } + return reservationService; } +#endif //MO_ENABLE_RESERVATION -ReservationService *Model::getReservationService() { - return reservationService.get(); -} +bool v16::Model::setup() { + + if (!setupCommon()) { + MO_DBG_ERR("setup failure"); + return false; + } + + if (!getTransactionService() || !getTransactionService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } + + if (!getMeteringService() || !getMeteringService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } + + if (!getResetService() || !getResetService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } + + if (!getAvailabilityService() || !getAvailabilityService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } + +#if MO_ENABLE_FIRMWAREMANAGEMENT + if (!getFirmwareService() || !getFirmwareService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } +#endif //MO_ENABLE_FIRMWAREMANAGEMENT + +#if MO_ENABLE_LOCAL_AUTH + if (!getAuthorizationService() || !getAuthorizationService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } +#endif //MO_ENABLE_LOCAL_AUTH + +#if MO_ENABLE_RESERVATION + if (!getReservationService() || !getReservationService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } #endif //MO_ENABLE_RESERVATION -void Model::setBootService(std::unique_ptr bs){ - bootService = std::move(bs); - capabilitiesUpdated = true; -} + // Ensure this is set up last. ConfigurationService::setup() loads the persistent config values from flash + if (!getConfigurationService() || !getConfigurationService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } -BootService *Model::getBootService() const { - return bootService.get(); -} + updateSupportedStandardProfiles(); -void Model::setResetService(std::unique_ptr rs) { - this->resetService = std::move(rs); - capabilitiesUpdated = true; -} + // Register remainder of operations which don't have dedicated service + context.getMessageService().registerOperation("DataTransfer", [] (Context&) -> Operation* { + return new v16::DataTransfer();}); -ResetService *Model::getResetService() const { - return resetService.get(); + return true; } -#if MO_ENABLE_CERT_MGMT -void Model::setCertificateService(std::unique_ptr cs) { - this->certService = std::move(cs); - capabilitiesUpdated = true; -} +void v16::Model::loop() { -CertificateService *Model::getCertificateService() const { - return certService.get(); -} -#endif //MO_ENABLE_CERT_MGMT + loopCommon(); -#if MO_ENABLE_V201 -void Model::setAvailabilityService(std::unique_ptr as) { - this->availabilityService = std::move(as); - capabilitiesUpdated = true; -} + if (!runTasks) { + return; + } -AvailabilityService *Model::getAvailabilityService() const { - return availabilityService.get(); -} + if (transactionService) { + transactionService->loop(); + } -void Model::setVariableService(std::unique_ptr vs) { - this->variableService = std::move(vs); - capabilitiesUpdated = true; -} + if (meteringService) { + meteringService->loop(); + } -VariableService *Model::getVariableService() const { - return variableService.get(); -} + if (resetService) { + resetService->loop(); + } -void Model::setTransactionService(std::unique_ptr ts) { - this->transactionService = std::move(ts); - capabilitiesUpdated = true; -} + if (availabilityService) { + availabilityService->loop(); + } -TransactionService *Model::getTransactionService() const { - return transactionService.get(); -} +#if MO_ENABLE_FIRMWAREMANAGEMENT + if (firmwareService) { + firmwareService->loop(); + } +#endif //MO_ENABLE_FIRMWAREMANAGEMENT -void Model::setResetServiceV201(std::unique_ptr rs) { - this->resetServiceV201 = std::move(rs); - capabilitiesUpdated = true; +#if MO_ENABLE_RESERVATION + if (reservationService) { + reservationService->loop(); + } +#endif //MO_ENABLE_RESERVATION } -Ocpp201::ResetService *Model::getResetServiceV201() const { - return resetServiceV201.get(); -} +#endif //MO_ENABLE_V16 + +#if MO_ENABLE_V201 + +using namespace MicroOcpp; + +v201::Model::Model(Context& context) : MemoryManaged("v201.Model"), ModelCommon(context), context(context) { -void Model::setMeteringServiceV201(std::unique_ptr rs) { - this->meteringServiceV201 = std::move(rs); - capabilitiesUpdated = true; } -Ocpp201::MeteringService *Model::getMeteringServiceV201() const { - return meteringServiceV201.get(); +v201::Model::~Model() { + delete transactionService; + transactionService = nullptr; + delete meteringService; + meteringService = nullptr; + delete resetService; + resetService = nullptr; + delete availabilityService; + availabilityService = nullptr; + delete variableService; + variableService = nullptr; } -void Model::setRemoteControlService(std::unique_ptr rs) { - remoteControlService = std::move(rs); - capabilitiesUpdated = true; +v201::VariableService *v201::Model::getVariableService() { + if (!variableService) { + variableService = new v201::VariableService(context); + + // need extra init step so that other modules can declare variables before setup() + if (variableService && !variableService->init()) { + // variableService cannot be inited + delete variableService; + variableService = nullptr; + } + } + return variableService; } -RemoteControlService *Model::getRemoteControlService() const { - return remoteControlService.get(); +v201::TransactionService *v201::Model::getTransactionService() { + if (!transactionService) { + transactionService = new TransactionService(context); + } + return transactionService; } -#endif -Clock& Model::getClock() { - return clock; +v201::MeteringService *v201::Model::getMeteringService() { + if (!meteringService) { + meteringService = new v201::MeteringService(context); + } + return meteringService; } -const ProtocolVersion& Model::getVersion() const { - return version; +v201::ResetService *v201::Model::getResetService() { + if (!resetService) { + resetService = new v201::ResetService(context); + } + return resetService; } -uint16_t Model::getBootNr() { - return bootNr; +v201::AvailabilityService *v201::Model::getAvailabilityService() { + if (!availabilityService) { + availabilityService = new v201::AvailabilityService(context); + } + return availabilityService; } -void Model::updateSupportedStandardProfiles() { +bool v201::Model::setup() { - auto supportedFeatureProfilesString = - declareConfiguration("SupportedFeatureProfiles", "", CONFIGURATION_VOLATILE, true); - - if (!supportedFeatureProfilesString) { - MO_DBG_ERR("OOM"); - return; + if (!setupCommon()) { + MO_DBG_ERR("setup failure"); + return false; } - auto buf = makeString(getMemoryTag(), supportedFeatureProfilesString->getString()); + if (!getTransactionService() || !getTransactionService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } - if (chargeControlCommon && - heartbeatService && - bootService) { - if (!strstr(supportedFeatureProfilesString->getString(), "Core")) { - if (!buf.empty()) buf += ','; - buf += "Core"; - } + if (!getMeteringService() || !getMeteringService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } + if (!getAvailabilityService() || !getAvailabilityService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; } - if (firmwareService || - diagnosticsService) { - if (!strstr(supportedFeatureProfilesString->getString(), "FirmwareManagement")) { - if (!buf.empty()) buf += ','; - buf += "FirmwareManagement"; - } + if (!getResetService() || !getResetService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; } -#if MO_ENABLE_LOCAL_AUTH - if (authorizationService) { - if (!strstr(supportedFeatureProfilesString->getString(), "LocalAuthListManagement")) { - if (!buf.empty()) buf += ','; - buf += "LocalAuthListManagement"; - } + // Ensure this is set up last. VariableService::setup() loads the persistent variable values from flash + if (!getVariableService() || !getVariableService()->setup()) { + MO_DBG_ERR("setup failure"); + return false; } -#endif //MO_ENABLE_LOCAL_AUTH -#if MO_ENABLE_RESERVATION - if (reservationService) { - if (!strstr(supportedFeatureProfilesString->getString(), "Reservation")) { - if (!buf.empty()) buf += ','; - buf += "Reservation"; - } + return true; +} + +void v201::Model::loop() { + + loopCommon(); + + if (variableService) { + variableService->loop(); } -#endif //MO_ENABLE_RESERVATION - if (smartChargingService) { - if (!strstr(supportedFeatureProfilesString->getString(), "SmartCharging")) { - if (!buf.empty()) buf += ','; - buf += "SmartCharging"; - } + if (!runTasks) { + return; } - if (!strstr(supportedFeatureProfilesString->getString(), "RemoteTrigger")) { - if (!buf.empty()) buf += ','; - buf += "RemoteTrigger"; + if (transactionService) { + transactionService->loop(); } - supportedFeatureProfilesString->setString(buf.c_str()); + if (resetService) { + resetService->loop(); + } - MO_DBG_DEBUG("supported feature profiles: %s", buf.c_str()); + if (availabilityService) { + availabilityService->loop(); + } } + +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Model.h b/src/MicroOcpp/Model/Model.h index ff31bdb2..e579e69a 100644 --- a/src/MicroOcpp/Model/Model.h +++ b/src/MicroOcpp/Model/Model.h @@ -1,179 +1,238 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_MODEL_H #define MO_MODEL_H -#include - -#include #include +#include #include -#include namespace MicroOcpp { -class TransactionStore; -class SmartChargingService; -class ConnectorsCommon; -class MeteringService; -class FirmwareService; -class DiagnosticsService; -class HeartbeatService; +class Context; + +} //namespace MicroOcpp + +#if MO_ENABLE_V16 || MO_ENABLE_V201 + +namespace MicroOcpp { + class BootService; -class ResetService; +class HeartbeatService; +class RemoteControlService; -#if MO_ENABLE_LOCAL_AUTH -class AuthorizationService; -#endif //MO_ENABLE_LOCAL_AUTH +#if MO_ENABLE_DIAGNOSTICS +class DiagnosticsService; +#endif //MO_ENABLE_DIAGNOSTICS -#if MO_ENABLE_RESERVATION -class ReservationService; -#endif //MO_ENABLE_RESERVATION +#if MO_ENABLE_SMARTCHARGING +class SmartChargingService; +#endif //MO_ENABLE_SMARTCHARGING #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 +#if MO_ENABLE_SECURITY_EVENT +class SecurityEventService; +#endif //MO_ENABLE_SECURITY_EVENT -class Model : public MemoryManaged { +//For Model modules which can be used with OCPP 1.6 and OCPP 2.0.1 +class ModelCommon { 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; + Context& context; -#if MO_ENABLE_LOCAL_AUTH - std::unique_ptr authorizationService; -#endif //MO_ENABLE_LOCAL_AUTH + BootService *bootService = nullptr; + HeartbeatService *heartbeatService = nullptr; + RemoteControlService *remoteControlService = nullptr; -#if MO_ENABLE_RESERVATION - std::unique_ptr reservationService; -#endif //MO_ENABLE_RESERVATION +#if MO_ENABLE_DIAGNOSTICS + DiagnosticsService *diagnosticsService = nullptr; +#endif //MO_ENABLE_DIAGNOSTICS + +#if MO_ENABLE_SMARTCHARGING + SmartChargingService *smartChargingService = nullptr; +#endif //MO_ENABLE_SMARTCHARGING #if MO_ENABLE_CERT_MGMT - std::unique_ptr certService; + CertificateService *certService = nullptr; #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; +#if MO_ENABLE_SECURITY_EVENT + SecurityEventService *secEventService = nullptr; +#endif //MO_ENABLE_SECURITY_EVENT - ProtocolVersion version; - - bool capabilitiesUpdated = true; - void updateSupportedStandardProfiles(); +protected: + unsigned int numEvseId = MO_NUM_EVSEID; bool runTasks = false; - const uint16_t bootNr = 0; //each boot of this lib has a unique number + ModelCommon(Context& context); + ~ModelCommon(); + + bool setupCommon(); + void loopCommon(); public: - Model(ProtocolVersion version = ProtocolVersion(1,6), uint16_t bootNr = 0); - Model(const Model& rhs) = delete; - ~Model(); - void loop(); + // Set number of EVSE IDs (including 0). On a charger with one physical connector, numEvseId is 2. Default value is MO_NUM_EVSEID + void setNumEvseId(unsigned int numEvseId); + unsigned int getNumEvseId(); + + BootService *getBootService(); + HeartbeatService *getHeartbeatService(); + RemoteControlService *getRemoteControlService(); + +#if MO_ENABLE_DIAGNOSTICS + DiagnosticsService *getDiagnosticsService(); +#endif //MO_ENABLE_DIAGNOSTICS + +#if MO_ENABLE_SMARTCHARGING + SmartChargingService *getSmartChargingService(); +#endif //MO_ENABLE_SMARTCHARGING + +#if MO_ENABLE_CERT_MGMT + CertificateService *getCertificateService(); +#endif //MO_ENABLE_CERT_MGMT + +#if MO_ENABLE_SECURITY_EVENT + SecurityEventService *getSecurityEventService(); +#endif //MO_ENABLE_SECURITY_EVENT void activateTasks() {runTasks = true;} - void setTransactionStore(std::unique_ptr transactionStore); - TransactionStore *getTransactionStore(); +}; - void setSmartChargingService(std::unique_ptr scs); - SmartChargingService* getSmartChargingService() const; +} //namespace MicroOcpp - void setConnectorsCommon(std::unique_ptr ccs); - ConnectorsCommon *getConnectorsCommon(); +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 - void setConnectors(Vector>&& connectors); - unsigned int getNumConnectors() const; - Connector *getConnector(unsigned int connectorId); +#if MO_ENABLE_V16 - void setMeteringSerivce(std::unique_ptr meteringService); - MeteringService* getMeteringService() const; +namespace MicroOcpp { +namespace v16 { - void setFirmwareService(std::unique_ptr firmwareService); - FirmwareService *getFirmwareService() const; +class ConfigurationService; +class TransactionService; +class MeteringService; +class ResetService; +class AvailabilityService; + +#if MO_ENABLE_FIRMWAREMANAGEMENT +class FirmwareService; +#endif //MO_ENABLE_FIRMWAREMANAGEMENT + +#if MO_ENABLE_LOCAL_AUTH +class AuthorizationService; +#endif //MO_ENABLE_LOCAL_AUTH - void setDiagnosticsService(std::unique_ptr diagnosticsService); - DiagnosticsService *getDiagnosticsService() const; +#if MO_ENABLE_RESERVATION +class ReservationService; +#endif //MO_ENABLE_RESERVATION - void setHeartbeatService(std::unique_ptr heartbeatService); +class Model : public MemoryManaged, public ModelCommon { +private: + Context& context; + + ConfigurationService *configurationService = nullptr; + TransactionService *transactionService = nullptr; + MeteringService *meteringService = nullptr; + ResetService *resetService = nullptr; + AvailabilityService *availabilityService = nullptr; + +#if MO_ENABLE_FIRMWAREMANAGEMENT + FirmwareService *firmwareService = nullptr; +#endif //MO_ENABLE_FIRMWAREMANAGEMENT + +#if MO_ENABLE_LOCAL_AUTH + AuthorizationService *authorizationService = nullptr; +#endif //MO_ENABLE_LOCAL_AUTH + +#if MO_ENABLE_RESERVATION + ReservationService *reservationService = nullptr; +#endif //MO_ENABLE_RESERVATION + + void updateSupportedStandardProfiles(); + +public: + Model(Context& context); + ~Model(); + + ConfigurationService *getConfigurationService(); + TransactionService *getTransactionService(); + MeteringService *getMeteringService(); + ResetService *getResetService(); + AvailabilityService *getAvailabilityService(); + +#if MO_ENABLE_FIRMWAREMANAGEMENT + FirmwareService *getFirmwareService(); +#endif //MO_ENABLE_FIRMWAREMANAGEMENT #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; + bool setup(); + void loop(); +}; -#if MO_ENABLE_CERT_MGMT - void setCertificateService(std::unique_ptr cs); - CertificateService *getCertificateService() const; -#endif //MO_ENABLE_CERT_MGMT +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #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; +namespace MicroOcpp { +namespace v201 { - void setResetServiceV201(std::unique_ptr rs); - Ocpp201::ResetService *getResetServiceV201() const; +class VariableService; +class TransactionService; +class MeteringService; +class ResetService; +class AvailabilityService; - void setMeteringServiceV201(std::unique_ptr ms); - Ocpp201::MeteringService *getMeteringServiceV201() const; +class Model : public MemoryManaged, public ModelCommon { +private: + Context& context; - void setRemoteControlService(std::unique_ptr rs); - RemoteControlService *getRemoteControlService() const; -#endif + VariableService *variableService = nullptr; + TransactionService *transactionService = nullptr; + MeteringService *meteringService = nullptr; + ResetService *resetService = nullptr; + AvailabilityService *availabilityService = nullptr; - Clock &getClock(); +public: + Model(Context& context); + ~Model(); - const ProtocolVersion& getVersion() const; + VariableService *getVariableService(); + TransactionService *getTransactionService(); + MeteringService *getMeteringService(); + ResetService *getResetService(); + AvailabilityService *getAvailabilityService(); - uint16_t getBootNr(); + bool setup(); + void loop(); }; -} //end namespace MicroOcpp +} //namespace v201 +} //namespace MicroOcpp +#endif //MO_ENABLE_V201 + +#if MO_ENABLE_V16 && !MO_ENABLE_V201 +namespace MicroOcpp { +using Model = v16::Model; +} +#elif !MO_ENABLE_V16 && MO_ENABLE_V201 +namespace MicroOcpp { +using Model = v201::Model; +} +#endif #endif diff --git a/src/MicroOcpp/Model/RemoteControl/RemoteControlDefs.h b/src/MicroOcpp/Model/RemoteControl/RemoteControlDefs.h index 2edc83e3..ad1fd7f3 100644 --- a/src/MicroOcpp/Model/RemoteControl/RemoteControlDefs.h +++ b/src/MicroOcpp/Model/RemoteControl/RemoteControlDefs.h @@ -5,30 +5,94 @@ #ifndef MO_UNLOCKCONNECTOR_H #define MO_UNLOCKCONNECTOR_H +#include #include -#if MO_ENABLE_V201 +// 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 1 +#endif -#include +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus -#include +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CONNECTOR_LOCK + +#ifndef MO_UNLOCK_TIMEOUT +#define MO_UNLOCK_TIMEOUT 10 // if Result is Pending, wait at most this period (in ms) until sending UnlockFailed +#endif typedef enum { - RequestStartStopStatus_Accepted, - RequestStartStopStatus_Rejected -} RequestStartStopStatus; + MO_UnlockConnectorResult_UnlockFailed, + MO_UnlockConnectorResult_Unlocked, + MO_UnlockConnectorResult_Pending // unlock action not finished yet, result still unknown (MO will check again later) +} MO_UnlockConnectorResult; -#if MO_ENABLE_CONNECTOR_LOCK +#ifdef __cplusplus +} +#endif // __cplusplus -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; +namespace MicroOcpp { + +enum class TriggerMessageStatus : uint8_t { + ERR_INTERNAL, + Accepted, + Rejected, + NotImplemented, +}; + +} //namespace MicroOcpp + +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CONNECTOR_LOCK + +#if MO_ENABLE_V16 +#ifdef __cplusplus + +namespace MicroOcpp { +namespace v16 { + +enum class RemoteStartStopStatus : uint8_t { + ERR_INTERNAL, + Accepted, + Rejected +}; + +enum class UnlockStatus : uint8_t { + Unlocked, + UnlockFailed, + NotSupported, + PENDING //MO-internal: unlock action not finished yet, result still unknown. Check later +}; + +} //namespace v16 +} //namespace MicroOcpp + +#endif //__cplusplus +#endif //MO_ENABLE_V16 + +#if MO_ENABLE_V201 +#ifdef __cplusplus + +namespace MicroOcpp { +namespace v201 { + +enum class RequestStartStopStatus : uint8_t { + Accepted, + Rejected +}; + +enum class UnlockStatus : uint8_t { + Unlocked, + UnlockFailed, + OngoingAuthorizedTransaction, + UnknownConnector, + PENDING //MO-internal: unlock action not finished yet, result still unknown. Check later +}; -#endif // MO_ENABLE_CONNECTOR_LOCK +} //namespace v201 +} //namespace MicroOcpp +#endif //__cplusplus +#endif //MO_ENABLE_V201 -#endif // MO_ENABLE_V201 -#endif // MO_UNLOCKCONNECTOR_H +#endif //MO_UNLOCKCONNECTOR_H diff --git a/src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp b/src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp index 74aefe0f..252acc89 100644 --- a/src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp +++ b/src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp @@ -2,172 +2,582 @@ // 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 +#include +#include +#include #include #include #include #include +#if MO_ENABLE_V16 || MO_ENABLE_V201 + using namespace MicroOcpp; -RemoteControlServiceEvse::RemoteControlServiceEvse(Context& context, unsigned int evseId) : MemoryManaged("v201.RemoteControl.RemoteControlServiceEvse"), context(context), evseId(evseId) { +RemoteControlServiceEvse::RemoteControlServiceEvse(Context& context, RemoteControlService& rcService, unsigned int evseId) : + MemoryManaged("v16/v201.RemoteControl.RemoteControlServiceEvse"), + context(context), + rcService(rcService), + evseId(evseId) { } +bool RemoteControlServiceEvse::setup() { + + #if MO_ENABLE_V16 + if (context.getOcppVersion() == MO_OCPP_V16) { + auto txServce = context.getModel16().getTransactionService(); + txServiceEvse16 = txServce ? txServce->getEvse(evseId) : nullptr; + if (!txServiceEvse16) { + MO_DBG_ERR("setup failure"); + return false; + } + } + #endif + #if MO_ENABLE_V201 + if (context.getOcppVersion() == MO_OCPP_V201) { + auto txServce = context.getModel201().getTransactionService(); + txServiceEvse201 = txServce ? txServce->getEvse(evseId) : nullptr; + if (!txServiceEvse201) { + MO_DBG_ERR("setup failure"); + return false; + } + } + #endif + + #if MO_ENABLE_CONNECTOR_LOCK + if (!onUnlockConnector) { + MO_DBG_WARN("need to set onUnlockConnector"); + } + #endif + + return true; +} + #if MO_ENABLE_CONNECTOR_LOCK -void RemoteControlServiceEvse::setOnUnlockConnector(UnlockConnectorResult (*onUnlockConnector)(unsigned int evseId, void *userData), void *userData) { +void RemoteControlServiceEvse::setOnUnlockConnector(MO_UnlockConnectorResult (*onUnlockConnector)(unsigned int evseId, void *userData), void *userData) { this->onUnlockConnector = onUnlockConnector; this->onUnlockConnectorUserData = userData; } -UnlockStatus RemoteControlServiceEvse::unlockConnector() { +#if MO_ENABLE_V16 +v16::UnlockStatus RemoteControlServiceEvse::unlockConnector16() { if (!onUnlockConnector) { - return UnlockStatus_UnlockFailed; + return v16::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 tx = txServiceEvse16->getTransaction(); + if (tx && tx->isActive()) { + txServiceEvse16->endTransaction(nullptr, "UnlockCommand"); + txServiceEvse16->updateTxNotification(MO_TxNotification_RemoteStop); } 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; + case MO_UnlockConnectorResult_Pending: + return v16::UnlockStatus::PENDING; + case MO_UnlockConnectorResult_Unlocked: + return v16::UnlockStatus::Unlocked; + case MO_UnlockConnectorResult_UnlockFailed: + return v16::UnlockStatus::UnlockFailed; } MO_DBG_ERR("invalid onUnlockConnector result code"); - return UnlockStatus_UnlockFailed; + return v16::UnlockStatus::UnlockFailed; } -#endif +#endif //MO_ENABLE_V16 + +#if MO_ENABLE_V201 +v201::UnlockStatus RemoteControlServiceEvse::unlockConnector201() { -RemoteControlService::RemoteControlService(Context& context, size_t numEvses) : MemoryManaged("v201.RemoteControl.RemoteControlService"), context(context) { + if (!onUnlockConnector) { + return v201::UnlockStatus::UnlockFailed; + } - for (size_t i = 0; i < numEvses && i < MO_NUM_EVSEID; i++) { - evses[i] = new RemoteControlServiceEvse(context, (unsigned int)i); + if (auto tx = txServiceEvse201->getTransaction()) { + if (tx->started && !tx->stopped && tx->isAuthorized) { + return v201::UnlockStatus::OngoingAuthorizedTransaction; + } else { + txServiceEvse201->abortTransaction(MO_TxStoppedReason_Other,MO_TxEventTriggerReason_UnlockCommand); + } } - auto varService = context.getModel().getVariableService(); - authorizeRemoteStart = varService->declareVariable("AuthCtrlr", "AuthorizeRemoteStart", false); + auto status = onUnlockConnector(evseId, onUnlockConnectorUserData); + switch (status) { + case MO_UnlockConnectorResult_Pending: + return v201::UnlockStatus::PENDING; + case MO_UnlockConnectorResult_Unlocked: + return v201::UnlockStatus::Unlocked; + case MO_UnlockConnectorResult_UnlockFailed: + return v201::UnlockStatus::UnlockFailed; + } + + MO_DBG_ERR("invalid onUnlockConnector result code"); + return v201::UnlockStatus::UnlockFailed; +} +#endif //MO_ENABLE_V201 + +#endif //MO_ENABLE_CONNECTOR_LOCK + +RemoteControlService::RemoteControlService(Context& context) : MemoryManaged("v16/v201.RemoteControl.RemoteControlService"), context(context) { - 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++) { + for (unsigned int i = 0; i < MO_NUM_EVSEID; i++) { delete evses[i]; + evses[i] = nullptr; } } RemoteControlServiceEvse *RemoteControlService::getEvse(unsigned int evseId) { - if (evseId >= MO_NUM_EVSEID) { - MO_DBG_ERR("invalid arg"); + if (evseId >= numEvseId) { + MO_DBG_ERR("evseId out of bound"); return nullptr; } + + if (!evses[evseId]) { + evses[evseId] = new RemoteControlServiceEvse(context, *this, evseId); + if (!evses[evseId]) { + MO_DBG_ERR("OOM"); + return nullptr; + } + } + return evses[evseId]; } -RequestStartStopStatus RemoteControlService::requestStartTransaction(unsigned int evseId, unsigned int remoteStartId, IdToken idToken, char *transactionIdOut, size_t transactionIdBufSize) { +bool RemoteControlService::setup() { + + #if MO_ENABLE_V16 + if (context.getOcppVersion() == MO_OCPP_V16) { + + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("setup failure"); + return false; + } + + configService->declareConfiguration("AuthorizeRemoteTxRequests", false); + + #if MO_ENABLE_CONNECTOR_LOCK + configService->declareConfiguration("UnlockConnectorOnEVSideDisconnect", true); //read-write + #else + configService->declareConfiguration("UnlockConnectorOnEVSideDisconnect", false, MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); //read-only because there is no connector lock + #endif //MO_ENABLE_CONNECTOR_LOCK + + txService16 = context.getModel16().getTransactionService(); + if (!txService16) { + MO_DBG_ERR("setup failure"); + return false; + } + + context.getMessageService().registerOperation("RemoteStartTransaction", [] (Context& context) -> Operation* { + return new v16::RemoteStartTransaction(context, *context.getModel16().getRemoteControlService());}); + context.getMessageService().registerOperation("RemoteStopTransaction", [] (Context& context) -> Operation* { + return new v16::RemoteStopTransaction(context, *context.getModel16().getRemoteControlService());}); + context.getMessageService().registerOperation("UnlockConnector", [] (Context& context) -> Operation* { + return new v16::UnlockConnector(context, *context.getModel16().getRemoteControlService());}); + } + #endif + #if MO_ENABLE_V201 + if (context.getOcppVersion() == MO_OCPP_V201) { + + auto varService = context.getModel201().getVariableService(); + if (!varService) { + return false; + } + + authorizeRemoteStart = varService->declareVariable("AuthCtrlr", "AuthorizeRemoteStart", false); + if (!authorizeRemoteStart) { + MO_DBG_ERR("failure to declare variable"); + return false; + } + + txService201 = context.getModel201().getTransactionService(); + if (!txService201) { + MO_DBG_ERR("setup failure"); + return false; + } + + context.getMessageService().registerOperation("RequestStartTransaction", [] (Context& context) -> Operation* { + return new v201::RequestStartTransaction(context, *context.getModel201().getRemoteControlService());}); + context.getMessageService().registerOperation("RequestStopTransaction", [] (Context& context) -> Operation* { + return new v201::RequestStopTransaction(*context.getModel201().getRemoteControlService());}); + + #if MO_ENABLE_CONNECTOR_LOCK + context.getMessageService().registerOperation("UnlockConnector", [] (Context& context) -> Operation* { + return new v201::UnlockConnector(context, *context.getModel201().getRemoteControlService());}); + #endif + } + #endif + + context.getMessageService().registerOperation("TriggerMessage", [] (Context& context) -> Operation* { + return new TriggerMessage(context, *context.getModelCommon().getRemoteControlService());}); + + numEvseId = context.getModelCommon().getNumEvseId(); + for (unsigned int i = 0; i < numEvseId; i++) { + if (!getEvse(i) || !getEvse(i)->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } + } + + return true; +} + +bool RemoteControlService::addTriggerMessageHandler(const char *operationType, Operation* (*createOperationCb)(Context& context)) { + bool capacity = triggerMessageHandlers.size() + 1; + triggerMessageHandlers.reserve(capacity); + if (triggerMessageHandlers.capacity() < capacity) { + MO_DBG_ERR("OOM"); + return false; + } + OperationCreator handler; + handler.operationType = operationType; + handler.create = createOperationCb; + triggerMessageHandlers.push_back(handler); + return true; +} + +bool RemoteControlService::addTriggerMessageHandler(const char *operationType, Operation* (*createOperationCb)(Context& context, unsigned int evseId)) { + bool capacity = triggerMessageHandlers.size() + 1; + triggerMessageHandlers.reserve(capacity); + if (triggerMessageHandlers.capacity() < capacity) { + MO_DBG_ERR("OOM"); + return false; + } + OperationCreator handler; + handler.operationType = operationType; + handler.create2 = createOperationCb; + triggerMessageHandlers.push_back(handler); + return true; +} + +bool RemoteControlService::addTriggerMessageHandler(const char *operationType, TriggerMessageStatus (*triggerMessageHandlerCb)(Context& context, int evseId)) { + bool capacity = triggerMessageHandlers.size() + 1; + triggerMessageHandlers.reserve(capacity); + if (triggerMessageHandlers.capacity() < capacity) { + MO_DBG_ERR("OOM"); + return false; + } + OperationCreator handler; + handler.operationType = operationType; + handler.handler3 = triggerMessageHandlerCb; + triggerMessageHandlers.push_back(handler); + return true; +} + +#if MO_ENABLE_V16 +v16::RemoteStartStopStatus RemoteControlService::remoteStartTransaction(int connectorId, const char *idTag, std::unique_ptr chargingProfile) { + + if (auto bSvc = context.getModel16().getBootService()) { + if (bSvc->getRegistrationStatus() != RegistrationStatus::Accepted) { + MO_DBG_WARN("BootNotofication not accepted"); + return v16::RemoteStartStopStatus::Rejected; + } + } - TransactionService *txService = context.getModel().getTransactionService(); - if (!txService) { + auto configService = context.getModel16().getConfigurationService(); + auto authorizeRemoteTxRequests = configService ? configService->declareConfiguration("AuthorizeRemoteTxRequests", false) : nullptr; + if (!authorizeRemoteTxRequests) { + MO_DBG_ERR("internal error"); + return v16::RemoteStartStopStatus::ERR_INTERNAL; + } + + auto status = v16::RemoteStartStopStatus::Rejected; + + v16::TransactionServiceEvse *selectEvse = nullptr; + if (connectorId >= 1) { + //connectorId specified for given connector, try to start Transaction here + auto txSvcEvse = txService16->getEvse(connectorId); + auto availSvc = context.getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(connectorId) : nullptr; + if (txSvcEvse && availSvcEvse){ + if (!txSvcEvse->getTransaction() && + availSvcEvse->isOperative()) { + selectEvse = txSvcEvse; + } + } + } else { + //connectorId not specified. Find free connector + for (unsigned int eId = 1; eId < numEvseId; eId++) { + auto txSvcEvse = txService16->getEvse(eId); + auto availSvc = context.getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(eId) : nullptr; + if (txSvcEvse && availSvcEvse) { + if (!txSvcEvse->getTransaction() && + availSvcEvse->isOperative()) { + selectEvse = txSvcEvse; + connectorId = eId; + break; + } + } + } + } + + if (selectEvse) { + + bool success = true; + + int chargingProfileId = -1; //keep Id after moving charging profile to SCService + + if (chargingProfile && context.getModel16().getSmartChargingService()) { + chargingProfileId = chargingProfile->chargingProfileId; + success = context.getModel16().getSmartChargingService()->setChargingProfile(connectorId, std::move(chargingProfile)); + } + + if (success) { + if (authorizeRemoteTxRequests->getBool()) { + success = selectEvse->beginTransaction(idTag); + } else { + success = selectEvse->beginTransaction_authorized(idTag); + } + } + if (success) { + if (auto transaction = selectEvse->getTransaction()) { + selectEvse->updateTxNotification(MO_TxNotification_RemoteStart); + + if (chargingProfileId >= 0) { + transaction->setTxProfileId(chargingProfileId); + } + } else { + MO_DBG_ERR("internal error"); + status = v16::RemoteStartStopStatus::ERR_INTERNAL; + success = false; + } + } + + if (!success && chargingProfile && context.getModel16().getSmartChargingService()) { + context.getModel16().getSmartChargingService()->clearChargingProfile(chargingProfile->chargingProfileId, connectorId, ChargingProfilePurposeType::UNDEFINED, -1); + } + + if (success) { + status = v16::RemoteStartStopStatus::Accepted; + } else { + status = v16::RemoteStartStopStatus::Rejected; + } + } else { + MO_DBG_INFO("No connector to start transaction"); + status = v16::RemoteStartStopStatus::Rejected; + } + + return status; +} + +v16::RemoteStartStopStatus RemoteControlService::remoteStopTransaction(int transactionId) { + + auto status = v16::RemoteStartStopStatus::Rejected; + + for (unsigned int eId = 0; eId < numEvseId; eId++) { + auto txSvcEvse = txService16->getEvse(eId); + auto transaction = txSvcEvse ? txSvcEvse->getTransaction() : nullptr; + if (transaction && + txSvcEvse->getTransaction()->getTransactionId() == transactionId) { + if (transaction->isActive()) { + txSvcEvse->updateTxNotification(MO_TxNotification_RemoteStop); + } + txSvcEvse->endTransaction(nullptr, "Remote"); + status = v16::RemoteStartStopStatus::Accepted; + } + } + + return status; +} +#endif //MO_ENABLE_V16 + +#if MO_ENABLE_V201 + +v201::RequestStartStopStatus RemoteControlService::requestStartTransaction(unsigned int evseId, unsigned int remoteStartId, v201::IdToken idToken, std::unique_ptr chargingProfile, char *transactionIdOut, size_t transactionIdBufSize) { + + if (auto bSvc = context.getModel201().getBootService()) { + if (bSvc->getRegistrationStatus() != RegistrationStatus::Accepted) { + MO_DBG_WARN("BootNotofication not accepted"); + return v201::RequestStartStopStatus::Rejected; + } + } + + if (!txService201) { MO_DBG_ERR("TxService uninitialized"); - return RequestStartStopStatus_Rejected; + return v201::RequestStartStopStatus::Rejected; } - - auto evse = txService->getEvse(evseId); + + auto evse = txService201->getEvse(evseId); if (!evse) { MO_DBG_ERR("EVSE not found"); - return RequestStartStopStatus_Rejected; + return v201::RequestStartStopStatus::Rejected; } - if (!evse->beginAuthorization(idToken, authorizeRemoteStart->getBool())) { + if (!evse->beginAuthorization(idToken, authorizeRemoteStart->getBool(), nullptr, /*commit*/ false)) { //only commit after storing the ChargingProfile 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 v201::RequestStartStopStatus::Rejected; } } - return RequestStartStopStatus_Rejected; + return v201::RequestStartStopStatus::Rejected; } - auto tx = evse->getTransaction(); + int ret = -1; + + v201::Transaction *tx = evse->getTransaction(); if (!tx) { - MO_DBG_ERR("internal error"); - return RequestStartStopStatus_Rejected; + goto fail; } - auto ret = snprintf(transactionIdOut, transactionIdBufSize, "%s", tx->transactionId); + #if MO_ENABLE_SMARTCHARGING + if (chargingProfile && context.getModel201().getSmartChargingService()) { + auto scService = context.getModel201().getSmartChargingService(); + + auto ret = snprintf(chargingProfile->transactionId201, sizeof(chargingProfile->transactionId201), "%s", tx->transactionId); + if (ret < 0 || (size_t)ret >= transactionIdBufSize) { + MO_DBG_ERR("internal error"); + goto fail; + } + + bool success = scService->setChargingProfile(evseId, std::move(chargingProfile)); + if (!success) { + MO_DBG_ERR("setChargingProfile"); + goto fail; + } + + } + #endif //MO_ENABLE_SMARTCHARGING + + ret = snprintf(transactionIdOut, transactionIdBufSize, "%s", tx->transactionId); if (ret < 0 || (size_t)ret >= transactionIdBufSize) { MO_DBG_ERR("internal error"); - return RequestStartStopStatus_Rejected; + goto fail; } tx->remoteStartId = remoteStartId; tx->notifyRemoteStartId = true; - return RequestStartStopStatus_Accepted; + if (!evse->commitTransaction()) { + MO_DBG_ERR("internal error"); + goto fail; + + } + + return v201::RequestStartStopStatus::Accepted; +fail: + evse->abortTransaction(); + #if MO_ENABLE_SMARTCHARGING + if (chargingProfile && context.getModel201().getSmartChargingService()) { + auto scService = context.getModel201().getSmartChargingService(); + scService->clearChargingProfile(chargingProfile->chargingProfileId,evseId,MicroOcpp::ChargingProfilePurposeType::UNDEFINED, -1); + } + #endif //MO_ENABLE_SMARTCHARGING + return v201::RequestStartStopStatus::Rejected; } -RequestStartStopStatus RemoteControlService::requestStopTransaction(const char *transactionId) { +v201::RequestStartStopStatus RemoteControlService::requestStopTransaction(const char *transactionId) { - TransactionService *txService = context.getModel().getTransactionService(); - if (!txService) { + if (!txService201) { MO_DBG_ERR("TxService uninitialized"); - return RequestStartStopStatus_Rejected; + return v201::RequestStartStopStatus::Rejected; } bool success = false; for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID; evseId++) { - if (auto evse = txService->getEvse(evseId)) { + if (auto evse = txService201->getEvse(evseId)) { if (evse->getTransaction() && !strcmp(evse->getTransaction()->transactionId, transactionId)) { - success = evse->abortTransaction(Ocpp201::Transaction::StoppedReason::Remote, Ocpp201::TransactionEventTriggerReason::RemoteStop); + success = evse->abortTransaction(MO_TxStoppedReason_Remote, MO_TxEventTriggerReason_RemoteStop); break; } } } return success ? - RequestStartStopStatus_Accepted : - RequestStartStopStatus_Rejected; + v201::RequestStartStopStatus::Accepted : + v201::RequestStartStopStatus::Rejected; +} + +#endif //MO_ENABLE_V201 + +TriggerMessageStatus RemoteControlService::triggerMessage(const char *requestedMessage, int evseId) { + + TriggerMessageStatus status = TriggerMessageStatus::NotImplemented; + + for (size_t i = 0; i < triggerMessageHandlers.size(); i++) { + + auto& handler = triggerMessageHandlers[i]; + + if (!strcmp(requestedMessage, handler.operationType)) { + + if (handler.handler3) { + // handler3 is a fully custom implementation. Just execute the handler with the parameters + // from TriggerMessage and we're done + status = handler.handler3(context, evseId); + break; + } + + unsigned int evseIdRangeBegin = 0, evseIdRangeEnd = 1; + if (handler.create2) { + // create2 is defined if multiple evseIds are supported. Then determine evseId range + if (evseId < 0) { + evseIdRangeBegin = 0; + evseIdRangeEnd = numEvseId; + } else if ((unsigned int)evseId < numEvseId) { + evseIdRangeBegin = (unsigned int)evseId; + evseIdRangeEnd = (unsigned int)evseId + 1; + } else { + return TriggerMessageStatus::ERR_INTERNAL; + } + } + + for (unsigned int eId = evseIdRangeBegin; eId < evseIdRangeEnd; eId++) { + + Operation *operation = nullptr; + + if (handler.create) { + operation = handler.create(context); + } else if (handler.create2) { + operation = handler.create2(context, eId); + } + + if (!operation) { + MO_DBG_ERR("OOM"); + return TriggerMessageStatus::ERR_INTERNAL; + } + + auto request = makeRequest(context, operation); + if (!request) { + MO_DBG_ERR("OOM"); + return TriggerMessageStatus::ERR_INTERNAL; + } + + bool success = context.getMessageService().sendRequest(std::move(request)); + if (success) { + status = TriggerMessageStatus::Accepted; + } else { + MO_DBG_ERR("OOM"); + return TriggerMessageStatus::ERR_INTERNAL; + } + } + + // handler found, done + break; + } + } + + return status; } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/RemoteControl/RemoteControlService.h b/src/MicroOcpp/Model/RemoteControl/RemoteControlService.h index 0a26567b..410720fd 100644 --- a/src/MicroOcpp/Model/RemoteControl/RemoteControlService.h +++ b/src/MicroOcpp/Model/RemoteControl/RemoteControlService.h @@ -5,61 +5,126 @@ #ifndef MO_REMOTECONTROLSERVICE_H #define MO_REMOTECONTROLSERVICE_H -#include - -#if MO_ENABLE_V201 - +#include #include +#include #include -#include +#include #include +#include + +#if MO_ENABLE_V16 || MO_ENABLE_V201 namespace MicroOcpp { class Context; +class RemoteControlService; + +#if MO_ENABLE_V16 +namespace v16 { +class Configuration; +class TransactionService; +class TransactionServiceEvse; +} //namespace v16 +#endif //MO_ENABLE_V16 + +#if MO_ENABLE_V201 +namespace v201 { class Variable; +class TransactionService; +class TransactionServiceEvse; +} //namespace v201 +#endif //MO_ENABLE_V201 class RemoteControlServiceEvse : public MemoryManaged { private: Context& context; + RemoteControlService& rcService; const unsigned int evseId; + #if MO_ENABLE_V16 + v16::TransactionServiceEvse *txServiceEvse16 = nullptr; + #endif + #if MO_ENABLE_V201 + v201::TransactionServiceEvse *txServiceEvse201 = nullptr; + #endif + #if MO_ENABLE_CONNECTOR_LOCK - UnlockConnectorResult (*onUnlockConnector)(unsigned int evseId, void *user) = nullptr; + MO_UnlockConnectorResult (*onUnlockConnector)(unsigned int evseId, void *user) = nullptr; void *onUnlockConnectorUserData = nullptr; -#endif +#endif //MO_ENABLE_CONNECTOR_LOCK public: - RemoteControlServiceEvse(Context& context, unsigned int evseId); + RemoteControlServiceEvse(Context& context, RemoteControlService& rcService, unsigned int evseId); #if MO_ENABLE_CONNECTOR_LOCK - void setOnUnlockConnector(UnlockConnectorResult (*onUnlockConnector)(unsigned int evseId, void *userData), void *userData); + void setOnUnlockConnector(MO_UnlockConnectorResult (*onUnlockConnector)(unsigned int evseId, void *userData), void *userData); +#endif //MO_ENABLE_CONNECTOR_LOCK - UnlockStatus unlockConnector(); -#endif + bool setup(); +#if MO_ENABLE_CONNECTOR_LOCK + #if MO_ENABLE_V16 + v16::UnlockStatus unlockConnector16(); + #endif + #if MO_ENABLE_V201 + v201::UnlockStatus unlockConnector201(); + #endif +#endif //MO_ENABLE_CONNECTOR_LOCK }; class RemoteControlService : public MemoryManaged { private: Context& context; RemoteControlServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; - - Variable *authorizeRemoteStart = nullptr; + unsigned int numEvseId = MO_NUM_EVSEID; + + #if MO_ENABLE_V16 + v16::TransactionService *txService16 = nullptr; + v16::Configuration *authorizeRemoteTxRequests = nullptr; + #endif + #if MO_ENABLE_V201 + v201::TransactionService *txService201 = nullptr; + v201::Variable *authorizeRemoteStart = nullptr; + #endif + + struct OperationCreator { + const char *operationType = nullptr; + Operation* (*create)(Context& context) = nullptr; + Operation* (*create2)(Context& context, unsigned int evseId) = nullptr; + TriggerMessageStatus (*handler3)(Context& context, int evseId) = nullptr; + }; + Vector triggerMessageHandlers; public: - RemoteControlService(Context& context, size_t numEvses); + RemoteControlService(Context& context); ~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 + bool addTriggerMessageHandler(const char *operationType, Operation* (*createOperationCb)(Context& context)); + bool addTriggerMessageHandler(const char *operationType, Operation* (*createOperationCb)(Context& context, unsigned int evseId)); + bool addTriggerMessageHandler(const char *operationType, TriggerMessageStatus (*triggerMessageHandlerCb)(Context& context, int evseId)); - RequestStartStopStatus requestStopTransaction(const char *transactionId); -}; + bool setup(); -} // namespace MicroOcpp + #if MO_ENABLE_V16 + v16::RemoteStartStopStatus remoteStartTransaction(int connectorId, const char *idTag, std::unique_ptr chargingProfile); -#endif // MO_ENABLE_V201 + v16::RemoteStartStopStatus remoteStopTransaction(int transactionId); + #endif //MO_ENABLE_V16 + + #if MO_ENABLE_V201 + v201::RequestStartStopStatus requestStartTransaction(unsigned int evseId, unsigned int remoteStartId, v201::IdToken idToken, std::unique_ptr chargingProfile, char *transactionIdOut, size_t transactionIdBufSize); //ChargingProfile, GroupIdToken not supported yet + + v201::RequestStartStopStatus requestStopTransaction(const char *transactionId); + #endif //MO_ENABLE_V201 + + TriggerMessageStatus triggerMessage(const char *requestedMessage, int evseId = -1); + +friend RemoteControlServiceEvse; +}; +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Reservation/Reservation.cpp b/src/MicroOcpp/Model/Reservation/Reservation.cpp index eb856b5d..82a9d826 100644 --- a/src/MicroOcpp/Model/Reservation/Reservation.cpp +++ b/src/MicroOcpp/Model/Reservation/Reservation.cpp @@ -2,36 +2,22 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include +#include -#if MO_ENABLE_RESERVATION +#include -#include +#include #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); +#if MO_ENABLE_V16 && MO_ENABLE_RESERVATION - snprintf(reservationIdKey, sizeof(reservationIdKey), MO_RESERVATION_RESID_KEY "%u", slot); - reservationIdInt = declareConfiguration(reservationIdKey, -1, RESERVATION_FN, false, false, false); +using namespace MicroOcpp; +using namespace MicroOcpp::v16; - snprintf(parentIdTagKey, sizeof(parentIdTagKey), MO_RESERVATION_PARENTID_KEY "%u", slot); - parentIdTagString = declareConfiguration(parentIdTagKey, "", RESERVATION_FN, false, false, false); +Reservation::Reservation(Context& context, unsigned int slot) : MemoryManaged("v16.Reservation.Reservation"), context(context), slot(slot) { - if (!connectorIdInt || !expiryDateRawString || !idTagString || !reservationIdInt || !parentIdTagString) { - MO_DBG_ERR("initialization failure"); - } } Reservation::~Reservation() { @@ -52,13 +38,73 @@ Reservation::~Reservation() { } } +bool Reservation::setup() { + + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("setup failure"); + return false; + } + + int ret; + + ret = snprintf(connectorIdKey, sizeof(connectorIdKey), MO_RESERVATION_CID_KEY "%u", slot); + if (ret < 0 || (size_t)ret >= sizeof(connectorIdKey)) { + return false; + } + connectorIdInt = configService->declareConfiguration(connectorIdKey, -1, RESERVATION_FN, Mutability::None); + + ret = snprintf(expiryDateRawKey, sizeof(expiryDateRawKey), MO_RESERVATION_EXPDATE_KEY "%u", slot); + if (ret < 0 || (size_t)ret >= sizeof(expiryDateRawKey)) { + return false; + } + expiryDateRawString = configService->declareConfiguration(expiryDateRawKey, "", RESERVATION_FN, Mutability::None); + + ret = snprintf(idTagKey, sizeof(idTagKey), MO_RESERVATION_IDTAG_KEY "%u", slot); + if (ret < 0 || (size_t)ret >= sizeof(idTagKey)) { + return false; + } + idTagString = configService->declareConfiguration(idTagKey, "", RESERVATION_FN, Mutability::None); + + ret = snprintf(reservationIdKey, sizeof(reservationIdKey), MO_RESERVATION_RESID_KEY "%u", slot); + if (ret < 0 || (size_t)ret >= sizeof(reservationIdKey)) { + return false; + } + reservationIdInt = configService->declareConfiguration(reservationIdKey, -1, RESERVATION_FN, Mutability::None); + + ret = snprintf(parentIdTagKey, sizeof(parentIdTagKey), MO_RESERVATION_PARENTID_KEY "%u", slot); + if (ret < 0 || (size_t)ret >= sizeof(parentIdTagKey)) { + return false; + } + parentIdTagString = configService->declareConfiguration(parentIdTagKey, "", RESERVATION_FN, Mutability::None); + + if (!connectorIdInt || !expiryDateRawString || !idTagString || !reservationIdInt || !parentIdTagString) { + MO_DBG_ERR("initialization failure"); + return false; + } + + reservations = configService->findContainerOfConfiguration(connectorIdInt); + if (!reservations) { + MO_DBG_ERR("setup failure"); + return false; + } + + return true; +} + bool Reservation::isActive() { if (connectorIdInt->getInt() < 0) { //reservation invalidated return false; } - if (model.getClock().now() > getExpiryDate()) { + int32_t dt; + if (!context.getClock().delta(context.getClock().now(), getExpiryDate(), dt)) { + //expiryDate undefined + return false; + } + + if (dt >= 0) { //reservation expired return false; } @@ -91,8 +137,11 @@ int Reservation::getConnectorId() { } Timestamp& Reservation::getExpiryDate() { - if (expiryDate == MIN_TIME && *expiryDateRawString->getString()) { - expiryDate.setTime(expiryDateRawString->getString()); + if (!expiryDate.isDefined() && *expiryDateRawString->getString()) { + if (!context.getClock().parseString(expiryDateRawString->getString(), expiryDate)) { + MO_DBG_ERR("internal error"); + expiryDate = Timestamp(); + } } return expiryDate; } @@ -109,29 +158,39 @@ const char *Reservation::getParentIdTag() { return parentIdTagString->getString(); } -void Reservation::update(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag) { +bool Reservation::update(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag) { + bool success = true; + 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); + char expiryDateStr [MO_INTERNALTIME_SIZE]; + if (context.getClock().toInternalString(this->expiryDate, expiryDateStr, sizeof(expiryDateStr))) { + success &= expiryDateRawString->setString(expiryDateStr); + } else { + success = false; } - idTagString->setString(idTag); - parentIdTagString->setString(parentIdTag); + success &= idTagString->setString(idTag); + success &= parentIdTagString->setString(parentIdTag); + + success &= reservations->commit(); - configuration_save(); + return success; //no roll-back mechanism implemented yet } -void Reservation::clear() { +bool Reservation::clear() { + bool success = true; + connectorIdInt->setInt(-1); - expiryDate = MIN_TIME; - expiryDateRawString->setString(""); - idTagString->setString(""); + expiryDate = Timestamp(); + success &= expiryDateRawString->setString(""); + success &= idTagString->setString(""); reservationIdInt->setInt(-1); - parentIdTagString->setString(""); + success &= parentIdTagString->setString(""); + + success &= reservations->commit(); - configuration_save(); + return success; //no roll-back mechanism implemented yet } -#endif //MO_ENABLE_RESERVATION +#endif //MO_ENABLE_V16 && MO_ENABLE_RESERVATION diff --git a/src/MicroOcpp/Model/Reservation/Reservation.h b/src/MicroOcpp/Model/Reservation/Reservation.h index daae197a..969a216a 100644 --- a/src/MicroOcpp/Model/Reservation/Reservation.h +++ b/src/MicroOcpp/Model/Reservation/Reservation.h @@ -5,16 +5,12 @@ #ifndef MO_RESERVATION_H #define MO_RESERVATION_H -#include - -#if MO_ENABLE_RESERVATION - -#include #include #include +#include #ifndef RESERVATION_FN -#define RESERVATION_FN (MO_FILENAME_PREFIX "reservations.jsn") +#define RESERVATION_FN "reservations.jsn" #endif #define MO_RESERVATION_CID_KEY "cid_" @@ -23,36 +19,47 @@ #define MO_RESERVATION_RESID_KEY "rsvid_" #define MO_RESERVATION_PARENTID_KEY "pidt_" +#if MO_ENABLE_V16 && MO_ENABLE_RESERVATION + namespace MicroOcpp { -class Model; +class Context; + +namespace v16 { + +class Configuration; +class ConfigurationContainer; class Reservation : public MemoryManaged { private: - Model& model; + Context& context; const unsigned int slot; - std::shared_ptr connectorIdInt; + ConfigurationContainer *reservations = nullptr; + + Configuration *connectorIdInt = nullptr; char connectorIdKey [sizeof(MO_RESERVATION_CID_KEY "xxx") + 1]; //"xxx" = placeholder for digits - std::shared_ptr expiryDateRawString; + Configuration *expiryDateRawString = nullptr; char expiryDateRawKey [sizeof(MO_RESERVATION_EXPDATE_KEY "xxx") + 1]; - Timestamp expiryDate = MIN_TIME; - std::shared_ptr idTagString; + Timestamp expiryDate; + Configuration *idTagString = nullptr; char idTagKey [sizeof(MO_RESERVATION_IDTAG_KEY "xxx") + 1]; - std::shared_ptr reservationIdInt; + Configuration *reservationIdInt = nullptr; char reservationIdKey [sizeof(MO_RESERVATION_RESID_KEY "xxx") + 1]; - std::shared_ptr parentIdTagString; + Configuration *parentIdTagString = nullptr; char parentIdTagKey [sizeof(MO_RESERVATION_PARENTID_KEY "xxx") + 1]; public: - Reservation(Model& model, unsigned int slot); + Reservation(Context& context, unsigned int slot); Reservation(const Reservation&) = delete; Reservation(Reservation&&) = delete; Reservation& operator=(const Reservation&) = delete; ~Reservation(); + bool setup(); + bool isActive(); //if this object contains a valid, unexpired reservation bool matches(unsigned int connectorId); @@ -64,11 +71,11 @@ class Reservation : public MemoryManaged { int getReservationId(); const char *getParentIdTag(); - void update(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag = nullptr); - void clear(); + bool update(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag = nullptr); + bool clear(); }; -} - -#endif //MO_ENABLE_RESERVATION +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_RESERVATION #endif diff --git a/src/MicroOcpp/Model/Reservation/ReservationService.cpp b/src/MicroOcpp/Model/Reservation/ReservationService.cpp index dde39aea..44029f60 100644 --- a/src/MicroOcpp/Model/Reservation/ReservationService.cpp +++ b/src/MicroOcpp/Model/Reservation/ReservationService.cpp @@ -2,71 +2,107 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_RESERVATION - #include -#include + +#include #include -#include -#include +#include +#include +#include +#include #include #include - #include +#if MO_ENABLE_V16 && MO_ENABLE_RESERVATION + using namespace MicroOcpp; +using namespace MicroOcpp::v16; + +ReservationService::ReservationService(Context& context) : MemoryManaged("v16.Reservation.ReservationService"), context(context) { + +} + +ReservationService::~ReservationService() { + for (size_t i = 0; i < sizeof(reservations) / sizeof(reservations[0]); i++) { + delete reservations[i]; + reservations[i] = nullptr; + } +} + +bool ReservationService::setup() { -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)); + numEvseId = context.getModel16().getNumEvseId(); + + maxReservations = numEvseId > 0 ? numEvseId - 1 : 0; // = number of physical connectors + for (size_t i = 0; i < maxReservations; i++) { + reservations[i] = new Reservation(context, i); + if (!reservations[i] || !reservations[i]->setup()) { + MO_DBG_ERR("OOM"); + return false; } } - 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());}); + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("setup failure"); + return false; + } + + reserveConnectorZeroSupportedBool = configService->declareConfiguration("ReserveConnectorZeroSupported", true, MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); + if (!reserveConnectorZeroSupportedBool) { + MO_DBG_ERR("declareConfiguration failed"); + return false; + } + + context.getMessageService().registerOperation("CancelReservation", [] (Context& context) -> Operation* { + return new CancelReservation(*context.getModel16().getReservationService());}); + context.getMessageService().registerOperation("ReserveNow", [] (Context& context) -> Operation* { + return new ReserveNow(context, *context.getModel16().getReservationService());}); + + return true; } void ReservationService::loop() { //check if to end reservations - for (auto& reservation : reservations) { - if (!reservation->isActive()) { + for (size_t i = 0; i < maxReservations; i++) { + if (!reservations[i]->isActive()) { continue; } - if (auto connector = context.getModel().getConnector(reservation->getConnectorId())) { - + auto availSvc = context.getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(reservations[i]->getConnectorId()) : nullptr; + if (availSvcEvse) { //check if connector went inoperative - auto cStatus = connector->getStatus(); - if (cStatus == ChargePointStatus_Faulted || cStatus == ChargePointStatus_Unavailable) { - reservation->clear(); + auto cStatus = availSvcEvse->getStatus(); + if (cStatus == MO_ChargePointStatus_Faulted || cStatus == MO_ChargePointStatus_Unavailable) { + reservations[i]->clear(); continue; } + } + auto txSvc = context.getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(reservations[i]->getConnectorId()) : nullptr; + if (txSvcEvse) { //check if other tx started at this connector (e.g. due to RemoteStartTransaction) - if (connector->getTransaction() && connector->getTransaction()->isAuthorized()) { - reservation->clear(); + if (txSvcEvse->getTransaction() && txSvcEvse->getTransaction()->isAuthorized()) { + reservations[i]->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(); + for (unsigned int evseId = 1; evseId < numEvseId; evseId++) { + auto txSvc = context.getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + auto transaction = txSvcEvse ? txSvcEvse->getTransaction() : nullptr; if (transaction && transaction->isAuthorized()) { - const char *cIdTag = transaction->getIdTag(); - if (transaction->getReservationId() == reservation->getReservationId() || - (cIdTag && !strcmp(cIdTag, reservation->getIdTag()))) { + const char *idTag = transaction->getIdTag(); + if (transaction->getReservationId() == reservations[i]->getReservationId() || + (idTag && !strcmp(idTag, reservations[i]->getIdTag()))) { - reservation->clear(); + reservations[i]->clear(); break; } } @@ -80,12 +116,12 @@ Reservation *ReservationService::getReservation(unsigned int connectorId) { 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(); + for (size_t i = 0; i < maxReservations; i++) { + if (reservations[i]->isActive() && reservations[i]->matches(connectorId)) { + return reservations[i]; } } - + return nullptr; } @@ -97,18 +133,18 @@ Reservation *ReservationService::getReservation(const char *idTag, const char *p Reservation *connectorReservation = nullptr; - for (auto& reservation : reservations) { - if (!reservation->isActive()) { + for (size_t i = 0; i < maxReservations; i++) { + if (!reservations[i]->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 + if (reservations[i]->matches(idTag, parentIdTag)) { + if (reservations[i]->getConnectorId() == 0) { + return reservations[i]; //reservation at connectorId 0 has higher priority } else { - connectorReservation = reservation.get(); + connectorReservation = reservations[i]; } } } @@ -132,7 +168,7 @@ Reservation *ReservationService::getReservation(unsigned int connectorId, const } } - if (reserveConnectorZeroSupportedBool && !reserveConnectorZeroSupportedBool->getBool()) { + if (!reserveConnectorZeroSupportedBool->getBool()) { //no connectorZero check - all done MO_DBG_DEBUG("no reservation"); return nullptr; @@ -143,23 +179,23 @@ Reservation *ReservationService::getReservation(unsigned int connectorId, const //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) { + for (size_t i = 0; i < maxReservations; i++) { + if (reservations[i]->isActive() && reservations[i]->getConnectorId() == 0) { unspecifiedReservations++; - blockingReservation = reservation.get(); + blockingReservation = reservations[i]; } } unsigned int availableCount = 0; - for (unsigned int cId = 1; cId < context.getModel().getNumConnectors(); cId++) { - if (cId == connectorId) { + for (unsigned int eId = 1; eId < numEvseId; eId++) { + if (eId == connectorId) { //don't count this connector continue; } - if (auto connector = context.getModel().getConnector(cId)) { - if (connector->getStatus() == ChargePointStatus_Available) { - availableCount++; - } + auto availSvc = context.getModel16().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(eId) : nullptr; + if (availSvcEvse && availSvcEvse->getStatus() == MO_ChargePointStatus_Available) { + availableCount++; } } @@ -173,9 +209,9 @@ Reservation *ReservationService::getReservation(unsigned int connectorId, const } Reservation *ReservationService::getReservationById(int reservationId) { - for (auto& reservation : reservations) { - if (reservation->isActive() && reservation->getReservationId() == reservationId) { - return reservation.get(); + for (size_t i = 0; i < maxReservations; i++) { + if (reservations[i]->isActive() && reservations[i]->getReservationId() == reservationId) { + return reservations[i]; } } @@ -193,7 +229,7 @@ bool ReservationService::updateReservation(int reservationId, unsigned int conne } // 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. +// 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)) { @@ -204,9 +240,9 @@ bool ReservationService::updateReservation(int reservationId, unsigned int conne } //update free reservation slot - for (auto& reservation : reservations) { - if (!reservation->isActive()) { - reservation->update(reservationId, connectorId, expiryDate, idTag, parentIdTag); + for (size_t i = 0; i < maxReservations; i++) { + if (!reservations[i]->isActive()) { + reservations[i]->update(reservationId, connectorId, expiryDate, idTag, parentIdTag); return true; } } @@ -215,4 +251,4 @@ bool ReservationService::updateReservation(int reservationId, unsigned int conne return false; } -#endif //MO_ENABLE_RESERVATION +#endif //MO_ENABLE_V16 && MO_ENABLE_RESERVATION diff --git a/src/MicroOcpp/Model/Reservation/ReservationService.h b/src/MicroOcpp/Model/Reservation/ReservationService.h index 367f1f59..5e67931f 100644 --- a/src/MicroOcpp/Model/Reservation/ReservationService.h +++ b/src/MicroOcpp/Model/Reservation/ReservationService.h @@ -5,30 +5,37 @@ #ifndef MO_RESERVATIONSERVICE_H #define MO_RESERVATIONSERVICE_H -#include - -#if MO_ENABLE_RESERVATION +#include #include +#include #include +#include -#include +#if MO_ENABLE_V16 && MO_ENABLE_RESERVATION namespace MicroOcpp { class Context; +namespace v16 { + class ReservationService : public MemoryManaged { private: Context& context; - const int maxReservations; // = number of physical connectors - Vector> reservations; + Reservation *reservations [MO_NUM_EVSEID - 1] = {nullptr}; // = number of physical connectors + size_t maxReservations = MO_NUM_EVSEID - 1; + + unsigned int numEvseId = MO_NUM_EVSEID; - std::shared_ptr reserveConnectorZeroSupportedBool; + Configuration *reserveConnectorZeroSupportedBool = nullptr; public: - ReservationService(Context& context, unsigned int numConnectors); + ReservationService(Context& context); + ~ReservationService(); + + bool setup(); void loop(); @@ -47,7 +54,7 @@ class ReservationService : public MemoryManaged { bool updateReservation(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag = nullptr); }; -} - -#endif //MO_ENABLE_RESERVATION +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_RESERVATION #endif diff --git a/src/MicroOcpp/Model/Reset/ResetDefs.h b/src/MicroOcpp/Model/Reset/ResetDefs.h index baa5048d..78026435 100644 --- a/src/MicroOcpp/Model/Reset/ResetDefs.h +++ b/src/MicroOcpp/Model/Reset/ResetDefs.h @@ -9,10 +9,10 @@ #if MO_ENABLE_V201 -typedef enum ResetType { - ResetType_Immediate, - ResetType_OnIdle -} ResetType; +typedef enum MO_ResetType { + MO_ResetType_Immediate, + MO_ResetType_OnIdle +} MO_ResetType; typedef enum ResetStatus { ResetStatus_Accepted, @@ -21,4 +21,4 @@ typedef enum ResetStatus { } ResetStatus; #endif //MO_ENABLE_V201 -#endif +#endif diff --git a/src/MicroOcpp/Model/Reset/ResetService.cpp b/src/MicroOcpp/Model/Reset/ResetService.cpp index 2eef02b8..ebde91be 100644 --- a/src/MicroOcpp/Model/Reset/ResetService.cpp +++ b/src/MicroOcpp/Model/Reset/ResetService.cpp @@ -3,50 +3,103 @@ // MIT License #include -#include + +#include #include -#include -#include #include - -#include -#include #include -#include - +#include +#include #include -#include - +#include +#include #include +#if MO_ENABLE_V16 || MO_ENABLE_V201 + #ifndef MO_RESET_DELAY -#define MO_RESET_DELAY 10000 +#define MO_RESET_DELAY 10 #endif +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) + +namespace MicroOcpp { +void defaultExecuteResetImpl() { + MO_DBG_DEBUG("Perform ESP reset"); + ESP.restart(); +} +} //namespace MicroOcpp + +#else + +namespace MicroOcpp { +void (*defaultExecuteResetImpl)() = nullptr; +} //namespace MicroOcpp + +#endif //MO_PLATFORM + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 + +#if MO_ENABLE_V16 + using namespace MicroOcpp; -ResetService::ResetService(Context& context) +v16::ResetService::ResetService(Context& context) : MemoryManaged("v16.Reset.ResetService"), context(context) { - resetRetriesInt = declareConfiguration("ResetRetries", 2); - registerConfigurationValidator("ResetRetries", VALIDATE_UNSIGNED_INT); +} + +v16::ResetService::~ResetService() { - context.getOperationRegistry().registerOperation("Reset", [&context] () { - return new Ocpp16::Reset(context.getModel());}); } -ResetService::~ResetService() { +bool v16::ResetService::setup() { + + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("setup failure"); + return false; + } + resetRetriesInt = configService->declareConfiguration("ResetRetries", 2); + if (!resetRetriesInt) { + MO_DBG_ERR("setup failure"); + return false; + } + if (!configService->registerValidator("ResetRetries", VALIDATE_UNSIGNED_INT)) { + MO_DBG_ERR("setup failure"); + return false; + } + + if (!executeResetCb && defaultExecuteResetImpl) { + MO_DBG_DEBUG("setup default reset function"); + executeResetCb = [] (bool, void*) { + defaultExecuteResetImpl(); + return false; + }; + } + + context.getMessageService().registerOperation("Reset", [] (Context& context) -> Operation* { + return new Reset(*context.getModel16().getResetService());}); + + return true; } -void ResetService::loop() { +void v16::ResetService::loop() { - if (outstandingResetRetries > 0 && mocpp_tick_ms() - t_resetRetry >= MO_RESET_DELAY) { - t_resetRetry = mocpp_tick_ms(); + auto& clock = context.getClock(); + + int32_t dtLastResetAttempt; + if (!clock.delta(clock.getUptime(), lastResetAttempt, dtLastResetAttempt)) { + dtLastResetAttempt = MO_RESET_DELAY; + } + + if (outstandingResetRetries > 0 && dtLastResetAttempt >= MO_RESET_DELAY) { + lastResetAttempt = clock.getUptime(); outstandingResetRetries--; - if (executeReset) { + if (executeResetCb) { MO_DBG_INFO("Reset device"); - executeReset(isHardReset); + executeResetCb(isHardReset, executeResetUserData); } else { MO_DBG_ERR("No Reset function set! Abort"); outstandingResetRetries = 0; @@ -56,88 +109,146 @@ void ResetService::loop() { MO_DBG_ERR("Reset device failure. Abort"); - ChargePointStatus cpStatus = ChargePointStatus_UNDEFINED; - if (context.getModel().getNumConnectors() > 0) { - cpStatus = context.getModel().getConnector(0)->getStatus(); - } + auto availSvc = context.getModel16().getAvailabilityService(); + auto availSvcCp = availSvc ? availSvc->getEvse(0) : nullptr; + auto cpStatus = availSvcCp ? availSvcCp->getStatus() : MO_ChargePointStatus_UNDEFINED; + + MO_ErrorData errorCode; + mo_errorData_init(&errorCode); + mo_errorData_setErrorCode(&errorCode, "ResetFailure"); - auto statusNotification = makeRequest(new Ocpp16::StatusNotification( + auto statusNotification = makeRequest(context, new StatusNotification( + context, 0, - cpStatus, //will be determined in StatusNotification::initiate - context.getModel().getClock().now(), - "ResetFailure")); - statusNotification->setTimeout(60000); - context.initiateRequest(std::move(statusNotification)); + cpStatus, + context.getClock().now(), + errorCode)); + statusNotification->setTimeout(60); + context.getMessageService().sendRequest(std::move(statusNotification)); } } } -void ResetService::setPreReset(std::function preReset) { - this->preReset = preReset; +void v16::ResetService::setNotifyReset(bool (*notifyResetCb)(bool isHard, void *userData), void *userData) { + this->notifyResetCb = notifyResetCb; + this->notifyResetUserData = userData; } -std::function ResetService::getPreReset() { - return this->preReset; +bool v16::ResetService::isPreResetDefined() { + return notifyResetCb != nullptr; } -void ResetService::setExecuteReset(std::function executeReset) { - this->executeReset = executeReset; +bool v16::ResetService::notifyReset(bool isHard) { + return notifyResetCb(isHard, notifyResetUserData); } -std::function ResetService::getExecuteReset() { - return this->executeReset; +void v16::ResetService::setExecuteReset(bool (*executeReset)(bool isHard, void *userData), void *userData) { + this->executeResetCb = executeReset; + this->executeResetUserData = userData; } -void ResetService::initiateReset(bool isHard) { +bool v16::ResetService::isExecuteResetDefined() { + return executeResetCb != nullptr; +} + +void v16::ResetService::initiateReset(bool isHard) { + + for (unsigned int eId = 0; eId < context.getModel16().getNumEvseId(); eId++) { + auto txSvc = context.getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(eId) : nullptr; + if (txSvcEvse) { + txSvcEvse->endTransaction(nullptr, isHard ? "HardReset" : "SoftReset"); + } + } + 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(); + lastResetAttempt = context.getClock().getUptime(); } -#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)) +#endif //MO_ENABLE_V16 #if MO_ENABLE_V201 namespace MicroOcpp { -namespace Ocpp201 { -ResetService::ResetService(Context& context) - : MemoryManaged("v201.Reset.ResetService"), context(context), evses(makeVector(getMemoryTag())) { +v201::ResetService::ResetService(Context& context) + : MemoryManaged("v201.Reset.ResetService"), context(context) { + +} - auto varService = context.getModel().getVariableService(); +v201::ResetService::~ResetService() { + for (unsigned int i = 0; i < numEvseId; i++) { + delete evses[i]; + evses[i] = 0; + } +} + +bool v201::ResetService::setup() { + auto varService = context.getModel201().getVariableService(); + if (!varService) { + return false; + } resetRetriesInt = varService->declareVariable("OCPPCommCtrlr", "ResetRetries", 0); + if (!resetRetriesInt) { + MO_DBG_ERR("setup failure"); + return false; + } - context.getOperationRegistry().registerOperation("Reset", [this] () { - return new Ocpp201::Reset(*this);}); + if (defaultExecuteResetImpl) { + if (!getEvse(0)) { + MO_DBG_ERR("setup failure"); + } + + if (!getEvse(0)->executeReset) { + MO_DBG_DEBUG("setup default reset function"); + getEvse(0)->executeReset = [] (unsigned int, void*) -> bool { + defaultExecuteResetImpl(); + return true; + }; + } + } + + numEvseId = context.getModel201().getNumEvseId(); + for (unsigned int i = 0; i < numEvseId; i++) { + if (!getEvse(i) || !getEvse(i)->setup()) { + MO_DBG_ERR("setup failure"); + return false; + } + } + + context.getMessageService().registerOperation("Reset", [] (Context& context) -> Operation* { + return new Reset(*context.getModel201().getResetService());}); + + return true; } -ResetService::~ResetService() { +v201::ResetService::Evse::Evse(Context& context, ResetService& resetService, unsigned int evseId) : MemoryManaged("v201.Reset.ResetService"), context(context), resetService(resetService), evseId(evseId) { } -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); +bool v201::ResetService::Evse::setup() { + auto varService = context.getModel201().getVariableService(); + if (!varService) { + return false; + } + varService->declareVariable(ComponentId("EVSE", evseId >= 1 ? evseId : -1), "AllowReset", true, Mutability::ReadOnly, false); + return true; } -void ResetService::Evse::loop() { +void v201::ResetService::Evse::loop() { + + auto& clock = context.getClock(); 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(); + auto txService = context.getModel201().getTransactionService(); if (txService && txService->getEvse(eId) && txService->getEvse(eId)->getTransaction()) { auto tx = txService->getEvse(eId)->getTransaction(); @@ -151,24 +262,32 @@ void ResetService::Evse::loop() { awaitTxStop = false; MO_DBG_INFO("Reset - tx stopped"); - t_resetRetry = mocpp_tick_ms(); // wait for some more time until final reset + lastResetAttempt = clock.getUptime(); // wait for some more time until final reset } - if (outstandingResetRetries && mocpp_tick_ms() - t_resetRetry >= MO_RESET_DELAY) { - t_resetRetry = mocpp_tick_ms(); + int32_t dtLastResetAttempt; + if (!clock.delta(clock.getUptime(), lastResetAttempt, dtLastResetAttempt)) { + dtLastResetAttempt = MO_RESET_DELAY; + } + + if (outstandingResetRetries && dtLastResetAttempt >= MO_RESET_DELAY) { + lastResetAttempt = clock.getUptime(); outstandingResetRetries--; MO_DBG_INFO("Reset device"); - bool success = executeReset(); + bool success = executeReset(evseId, executeResetUserData); if (success) { outstandingResetRetries = 0; if (evseId != 0) { //Set this EVSE Available again - if (auto connector = context.getModel().getConnector(evseId)) { - connector->setAvailabilityVolatile(true); + + auto availSvc = context.getModel201().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (availSvcEvse) { + availSvcEvse->setAvailable(this); } } } else if (!outstandingResetRetries) { @@ -176,86 +295,71 @@ void ResetService::Evse::loop() { 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); + for (unsigned int eId = 0; eId < resetService.numEvseId; eId++) { + auto availSvc = context.getModel201().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(eId) : nullptr; + if (availSvcEvse) { + availSvcEvse->setAvailable(this); + } } } else { //Set only this EVSE Available - if (auto connector = context.getModel().getConnector(evseId)) { - connector->setAvailabilityVolatile(true); + auto availSvc = context.getModel201().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (availSvcEvse) { + availSvcEvse->setAvailable(this); } } } } } -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) { +v201::ResetService::Evse *v201::ResetService::getEvse(unsigned int evseId) { + if (evseId >= numEvseId) { MO_DBG_ERR("evseId out of bound"); return nullptr; } - evses.emplace_back(context, *this, evseId); - return &evses.back(); + if (!evses[evseId]) { + evses[evseId] = new Evse(context, *this, evseId); + if (!evses[evseId]) { + MO_DBG_ERR("OOM"); + return nullptr; + } + } + + return evses[evseId]; } -void ResetService::loop() { - for (Evse& evse : evses) { - evse.loop(); +void v201::ResetService::loop() { + for (unsigned i = 0; i < numEvseId; i++) { + if (evses[i]) { + evses[i]->loop(); + } } } -void ResetService::setNotifyReset(std::function notifyReset, unsigned int evseId) { - Evse *evse = getOrCreateEvse(evseId); +void v201::ResetService::setNotifyReset(unsigned int evseId, bool (*notifyReset)(MO_ResetType, unsigned int evseId, void *userData), void *userData) { + Evse *evse = getEvse(evseId); if (!evse) { MO_DBG_ERR("evseId not found"); return; } evse->notifyReset = notifyReset; + evse->notifyResetUserData = userData; } -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); +void v201::ResetService::setExecuteReset(unsigned int evseId, bool (*executeReset)(unsigned int evseId, void *userData), void *userData) { + Evse *evse = getEvse(evseId); if (!evse) { MO_DBG_ERR("evseId not found"); return; } evse->executeReset = executeReset; + evse->executeResetUserData = userData; } -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) { +ResetStatus v201::ResetService::initiateReset(MO_ResetType type, unsigned int evseId) { auto evse = getEvse(evseId); if (!evse) { MO_DBG_ERR("evseId not found"); @@ -268,11 +372,11 @@ ResetStatus ResetService::initiateReset(ResetType type, unsigned int evseId) { } //Check if EVSEs are ready for Reset - for (unsigned int eId = evseId; eId < (evseId == 0 ? MO_NUM_EVSEID : evseId + 1); eId++) { + for (unsigned int eId = evseId; eId < (evseId == 0 ? numEvseId : 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)) { + if (it->notifyReset && !it->notifyReset(type, eId, it->notifyResetUserData)) { MO_DBG_INFO("EVSE %u not able to Reset", evseId); return ResetStatus_Rejected; } @@ -282,14 +386,19 @@ ResetStatus ResetService::initiateReset(ResetType type, unsigned int evseId) { //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); + for (unsigned int eId = 0; eId < numEvseId; eId++) { + auto availSvc = context.getModel201().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(eId) : nullptr; + if (availSvcEvse) { + availSvcEvse->setUnavailable(this); + } } } else { //Set this EVSE Unavailable - if (auto connector = context.getModel().getConnector(evseId)) { - connector->setAvailabilityVolatile(false); + auto availSvc = context.getModel201().getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(evseId) : nullptr; + if (availSvcEvse) { + availSvcEvse->setUnavailable(this); } } @@ -299,13 +408,13 @@ ResetStatus ResetService::initiateReset(ResetType type, unsigned int evseId) { 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(); + auto txService = context.getModel201().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); + if (type == MO_ResetType_Immediate) { + txService->getEvse(eId)->abortTransaction(MO_TxStoppedReason_ImmediateReset, MO_TxEventTriggerReason_ResetCommand); } else { scheduled = true; break; @@ -322,12 +431,11 @@ ResetStatus ResetService::initiateReset(ResetType type, unsigned int evseId) { } else { evse->outstandingResetRetries = 1 + resetRetriesInt->getInt(); //one initial try + no. of retries } - evse->t_resetRetry = mocpp_tick_ms(); + evse->lastResetAttempt = context.getClock().getUptime(); evse->awaitTxStop = scheduled; return scheduled ? ResetStatus_Scheduled : ResetStatus_Accepted; } } //namespace MicroOcpp -} //namespace Ocpp201 #endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Reset/ResetService.h b/src/MicroOcpp/Model/Reset/ResetService.h index a84e82d6..23f1de64 100644 --- a/src/MicroOcpp/Model/Reset/ResetService.h +++ b/src/MicroOcpp/Model/Reset/ResetService.h @@ -5,109 +5,117 @@ #ifndef MO_RESETSERVICE_H #define MO_RESETSERVICE_H -#include - #include -#include #include +#include #include +#include + +#if MO_ENABLE_V16 namespace MicroOcpp { class Context; +namespace v16 { + +class Configuration; + 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 + bool (*notifyResetCb)(bool isHard, void *userData) = nullptr; //true: reset is possible; false: reject reset; Await: need more time to determine + void *notifyResetUserData = nullptr; + bool (*executeResetCb)(bool isHard, void *userData) = nullptr;; //please disconnect WebSocket (MO remains initialized), shut down device and restart with normal initialization routine; on failure reconnect WebSocket + void *executeResetUserData = nullptr; unsigned int outstandingResetRetries = 0; //0 = do not reset device bool isHardReset = false; - unsigned long t_resetRetry; + Timestamp lastResetAttempt; - std::shared_ptr resetRetriesInt; + Configuration *resetRetriesInt = nullptr; public: ResetService(Context& context); ~ResetService(); - - void loop(); - void setPreReset(std::function preReset); - std::function getPreReset(); + void setNotifyReset(bool (*notifyReset)(bool isHard, void *userData), void *userData); - void setExecuteReset(std::function executeReset); - std::function getExecuteReset(); + void setExecuteReset(bool (*executeReset)(bool isHard, void *userData), void *userData); - void initiateReset(bool isHard); -}; - -} //end namespace MicroOcpp + bool setup(); -#if MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) + void loop(); -namespace MicroOcpp { + bool isPreResetDefined(); + bool notifyReset(bool isHard); -std::function makeDefaultResetFn(); + bool isExecuteResetDefined(); -} + void initiateReset(bool isHard); +}; -#endif //MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #if MO_ENABLE_V201 namespace MicroOcpp { -class Variable; +namespace v201 { -namespace Ocpp201 { +class Variable; class ResetService : public MemoryManaged { private: Context& context; - struct Evse { + struct Evse : public MemoryManaged { 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 + bool (*notifyReset)(MO_ResetType, unsigned int evseId, void *userData) = nullptr; //notify firmware about a Reset command. Return true if Reset is okay; false if Reset cannot be executed + void *notifyResetUserData = nullptr; + bool (*executeReset)(unsigned int evseId, void *userData) = nullptr; //execute Reset of connector. Return true if Reset will be executed; false if there is a failure to Reset + void *executeResetUserData = nullptr; unsigned int outstandingResetRetries = 0; //0 = do not reset device - unsigned long t_resetRetry; + Timestamp lastResetAttempt; bool awaitTxStop = false; Evse(Context& context, ResetService& resetService, unsigned int evseId); + bool setup(); + void loop(); }; - Vector evses; + Evse* evses [MO_NUM_EVSEID] = {nullptr}; + unsigned int numEvseId = MO_NUM_EVSEID; 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 setNotifyReset(unsigned int evseId, bool (*notifyReset)(MO_ResetType, unsigned int evseId, void *userData), void *userData); + + void setExecuteReset(unsigned int evseId, bool (*executeReset)(unsigned int evseId, void *userData), void *userData); - void setExecuteReset(std::function executeReset, unsigned int evseId = 0); - std::function getExecuteReset(unsigned int evseId = 0); + bool setup(); + + void loop(); - ResetStatus initiateReset(ResetType type, unsigned int evseId = 0); + ResetStatus initiateReset(MO_ResetType type, unsigned int evseId = 0); }; -} //namespace Ocpp201 +} //namespace v201 } //namespace MicroOcpp #endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/SecurityEvent/SecurityEvent.h b/src/MicroOcpp/Model/SecurityEvent/SecurityEvent.h new file mode 100644 index 00000000..fd36fd63 --- /dev/null +++ b/src/MicroOcpp/Model/SecurityEvent/SecurityEvent.h @@ -0,0 +1,16 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#ifndef MO_SECURITYEVENTSERVICE_H +#define MO_SECURITYEVENTSERVICE_H + +#include + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SECURITY_EVENT + +// Length of field `type` of a SecurityEventNotificationRequest (1.49.1) +#define MO_SECURITY_EVENT_TYPE_LENGTH 50 + +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SECURITY_EVENT +#endif diff --git a/src/MicroOcpp/Model/SecurityEvent/SecurityEventService.cpp b/src/MicroOcpp/Model/SecurityEvent/SecurityEventService.cpp new file mode 100644 index 00000000..3addbd25 --- /dev/null +++ b/src/MicroOcpp/Model/SecurityEvent/SecurityEventService.cpp @@ -0,0 +1,619 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SECURITY_EVENT + +/* Queue naming schema + * + * The SecurityEvent queue keeps historic events for the GetLogs operation / later troubleshooting and it keeps + * outstanding events in case the charger is offline. + * + * To store the queue on the file system, it is partitioned into fewer, but larger files. Most filesystems come with + * an overhead if storing small, but many files. So MO uses one file to store up to 20 entries. This results in a + * two-dimensional index to address a SecurityEvent: first the position of the file on the filesystem and second the + * position of the SecurityEvent in the file. So a SecurityEvent is addressed by fileNr x entryNr. + * + * The SecurityEvent queue the queue consists of two sections, the historic section, followed by the outstanding + * section. The first element of the historic section is the "begin", the first of the outstanding is the "front", + * and the outstanding section is delimited by the "end" index which is one address higher than the last element. + * This results in the following arithmetics: + * + * front - begin: number of historic elements + * front == begin: true if no historic elements exist + * end - front: number of outstanding elements to be sent + * end == front: true if no outstanding elements exist + * end - begin: number of total elements on flash + * end == front: true if there are no files on flash + * + * The fileNr is a ring index which rolls over at MO_SECLOG_INDEX_MAX. Going to the next file from index + * (MO_SECLOG_INDEX_MAX - 1) results in the index 0. This breaks the < and > comparators, because the previous + * logfile index may be numerically higher than the following logfile index. This is handled by some explicit + * modulus operations. The other index, entryNr, is an ordinary index though and does not need any of the ring + * index arithmetics. + */ + +using namespace MicroOcpp; + +SecurityEventService::SecurityEventService(Context& context) : MemoryManaged("v16/v201.Security.SecurityEventService"), context(context) { + +} + +bool SecurityEventService::setup() { + + ocppVersion = context.getOcppVersion(); + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("initialization error"); + return false; + } + + //if transactions can start before the BootNotification succeeds + timeAdjustmentReportingThresholdIntV16 = configService->declareConfiguration(MO_CONFIG_EXT_PREFIX "TimeAdjustmentReportingThreshold", 20); + if (!timeAdjustmentReportingThresholdIntV16) { + MO_DBG_ERR("initialization error"); + return false; + } + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + + auto varService = context.getModel201().getVariableService(); + if (!varService) { + MO_DBG_ERR("initialization error"); + return false; + } + + timeAdjustmentReportingThresholdIntV201 = varService->declareVariable("ClockCtrlr", "TimeAdjustmentReportingThreshold", 20); + if (!timeAdjustmentReportingThresholdIntV201) { + return false; + } + } + #endif //MO_ENABLE_V201 + + auto& clock = context.getClock(); + trackUnixTime = clock.now(); + trackUptime = clock.getUptime(); + + connection = context.getConnection(); + if (!connection) { + MO_DBG_ERR("setup failure"); + return false; + } + + filesystem = context.getFilesystem(); + if (!filesystem) { + MO_DBG_DEBUG("volatile mode"); + } + + if (filesystem) { + if (!FilesystemUtils::loadRingIndex(filesystem, MO_SECLOG_FN_PREFIX, MO_SECLOG_INDEX_MAX, &fileNrBegin, &fileNrEnd)) { + MO_DBG_ERR("failed to init security event log"); + return false; + } + + // find front file and front securityEvent inside that file + fileNrFront = fileNrEnd; + unsigned int fileNrSize = (fileNrEnd + MO_SECLOG_INDEX_MAX - fileNrBegin) % MO_SECLOG_INDEX_MAX; + + for (unsigned int i = 1; i <= fileNrSize; i++) { + + unsigned int fileNr = (fileNrEnd + MO_SECLOG_INDEX_MAX - i) % MO_SECLOG_INDEX_MAX; // iterate logFiles from back + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, sizeof(fn), MO_SECLOG_FN_PREFIX "%u.jsn", fileNr); + if (ret < 0 || (size_t)ret >= sizeof(fn)) { + MO_DBG_ERR("fn error: %i", ret); + return false; + } + + JsonDoc logFile (0); + auto status = FilesystemUtils::loadJson(filesystem, fn, logFile, getMemoryTag()); + switch (status) { + case FilesystemUtils::LoadStatus::Success: + break; //continue loading JSON + case FilesystemUtils::LoadStatus::FileNotFound: + break; //file gap - will skip this fileNr + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + return false; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("failed to load %s", fn); + break; + } + + if (status != FilesystemUtils::LoadStatus::Success) { + continue; + } + + unsigned int nextEntryNrFront = (unsigned int)logFile.size(); + unsigned int nextEntryNrEnd = (unsigned int)logFile.size(); + + for (size_t entryNr = 0; entryNr < logFile.size(); entryNr++) { + auto securityEvent = logFile[entryNr]; + if (securityEvent.containsKey("atnr")) { + // this is the first entry to be sent. Found front candidate + nextEntryNrFront = (unsigned int)entryNr; + break; + } + } + + if (nextEntryNrFront != nextEntryNrEnd) { + // This file contains outstanding SecurityEvents to be sent. These could be the front of the queue + fileNrFront = fileNr; + entryNrFront = nextEntryNrFront; + entryNrEnd = nextEntryNrEnd; + } else { + // This file does not contain any events to send. So this loop iteration already skipped over the front file + break; + } + + if (nextEntryNrFront > 0) { + // This file contains both historic and outstanding events. It is safe to say that this is the front and to stop the search + break; + } + + // continue: This file contains only events to send and no historic events. This could be the front file + // or in the middle of the queue. Took it as candidate, but continue searching at fileNr-1 + } + } + + return true; +} + +void SecurityEventService::loop() { + + auto& clock = context.getClock(); + Timestamp unixTime = clock.now(); + Timestamp uptime = clock.getUptime(); + + if (unixTime.isUnixTime() && trackUnixTime.isUnixTime()) { + // Got initial time - check if clock drift exceeds timeAdjustmentReportingThreshold + + int32_t deltaUnixTime = 0; //unix time jumps over clock adjustments + clock.delta(unixTime, trackUnixTime, deltaUnixTime); + + int32_t deltaUptime = 0; //uptime is steadily increasing without gaps + clock.delta(uptime, trackUptime, deltaUptime); + + int32_t timeAdjustment = deltaUnixTime - deltaUptime; //should be 0 if clock is not adjusted, otherwise the number of seconds adjusted + + int timeAdjustmentReportingThreshold = 0; + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + timeAdjustmentReportingThreshold = timeAdjustmentReportingThresholdIntV16->getInt(); + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + timeAdjustmentReportingThreshold = timeAdjustmentReportingThresholdIntV201->getInt(); + } + #endif //MO_ENABLE_V201 + + if (abs(timeAdjustment) >= timeAdjustmentReportingThreshold && timeAdjustmentReportingThreshold > 0) { + triggerSecurityEvent("SettingSystemTime"); + } + } + trackUnixTime = unixTime; + trackUptime = uptime; + + if (!filesystem) { + // Nothing else to do in volatile mode + return; + } + + if (isSecurityEventInProgress) { + //avoid sending multiple messages at the same time + return; + } + + if (!connection->isConnected()) { + //don't use up attempts while offline + return; + } + + int32_t dtAttemptTime; + if (!attemptTime.isDefined() || !context.getClock().delta(context.getClock().now(), attemptTime, dtAttemptTime)) { + dtAttemptTime = 0; + } + + if (entryNrFront != entryNrEnd && + dtAttemptTime >= (int32_t)attemptNr * MO_SECLOG_SEND_ATTEMPT_INTERVAL) { + + // Send SecurityEventNotification + + attemptTime = context.getClock().now(); + + char type [MO_SECURITY_EVENT_TYPE_LENGTH + 1]; + Timestamp timestamp; + unsigned int attemptNr; + + bool success = fetchSecurityEventFront(type, timestamp, attemptNr); + if (!success) { + return; //retry + } + + this->attemptNr = attemptNr; + + auto securityEventNotification = makeRequest(context, new SecurityEventNotification(context, type, timestamp)); + securityEventNotification->setOnReceiveConf([this] (JsonObject) { + isSecurityEventInProgress = false; + + MO_DBG_DEBUG("completed front securityEvent"); + advanceSecurityEventFront(); + }); + securityEventNotification->setOnAbort([this] () { + isSecurityEventInProgress = false; + + if (this->attemptNr >= MO_SECLOG_SEND_ATTEMPTS) { + MO_DBG_ERR("final SecurityEventNotification attempt failed"); + advanceSecurityEventFront(); + } else { + MO_DBG_DEBUG("retry SecurityEventNotification"); + } + }); + isSecurityEventInProgress = true; + + securityEventNotification->setTimeout(std::min(20, MO_SECLOG_SEND_ATTEMPT_INTERVAL)); + context.getMessageService().sendRequest(std::move(securityEventNotification)); + } +} + +bool SecurityEventService::fetchSecurityEventFront(char *type, Timestamp& timestamp, unsigned int& attemptNr) { + if (!filesystem) { + MO_DBG_ERR("invalid state"); + return false; + } + + if (fileNrFront == fileNrEnd || entryNrFront == entryNrEnd) { + MO_DBG_ERR("invalid state"); + return false; + } + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, sizeof(fn), MO_SECLOG_FN_PREFIX "%u.jsn", fileNrFront); + if (ret < 0 || (size_t)ret >= sizeof(fn)) { + MO_DBG_ERR("fn error: %i", ret); + return false; + } + + JsonDoc logFile (0); + auto status = FilesystemUtils::loadJson(filesystem, fn, logFile, getMemoryTag()); + switch (status) { + case FilesystemUtils::LoadStatus::Success: + break; //continue loading JSON + case FilesystemUtils::LoadStatus::FileNotFound: + advanceSecurityEventFront(); + break; //file gap - will skip this fileNr + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + break; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + MO_DBG_ERR("failed to load %s", fn); + advanceSecurityEventFront(); + break; + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("failed to load %s", fn); + break; + } + + if (status != FilesystemUtils::LoadStatus::Success) { + return false; + } + + if (logFile.size() != (size_t)entryNrEnd) { + MO_DBG_ERR("deserialization error: %s", fn); + advanceSecurityEventFront(); + return false; + } + + JsonObject securityEvent = logFile[entryNrFront]; + + const char *typeIn = securityEvent["type"] | (const char*)nullptr; + ret = -1; + if (typeIn) { + ret = snprintf(type, MO_SECURITY_EVENT_TYPE_LENGTH + 1, "%s", typeIn); + } + if (!typeIn || ret < 0 || ret >= MO_SECURITY_EVENT_TYPE_LENGTH) { + MO_DBG_ERR("deserialization error"); + advanceSecurityEventFront(); + return false; + } + + const char *time = securityEvent["time"] | (const char*)nullptr; + if (!time || !context.getClock().parseString(time, timestamp)) { + MO_DBG_ERR("deserialization error"); + advanceSecurityEventFront(); + return false; + } + + attemptNr = securityEvent["atnr"] | MO_SECLOG_SEND_ATTEMPTS; + if (attemptNr >= MO_SECLOG_SEND_ATTEMPTS) { + MO_DBG_ERR("invalid attemptNr"); + return false; + } + + attemptNr++; + + if (attemptNr < MO_SECLOG_SEND_ATTEMPTS) { + securityEvent["atnr"] = attemptNr; + } else { + securityEvent.remove("atnr"); + } + + if (FilesystemUtils::storeJson(filesystem, fn, logFile) != FilesystemUtils::StoreStatus::Success) { + MO_DBG_ERR("FS error: %s", fn); + return false; + } + + return true; +} + +bool SecurityEventService::advanceSecurityEventFront() { + + if (!filesystem) { + MO_DBG_ERR("invalid state"); + return false; + } + + // this invalidates attemptTime of previous securityEvent + attemptTime = Timestamp(); + attemptNr = 0; + + // Advance entryNrFront + entryNrFront++; + + if (entryNrFront != entryNrEnd) { + // This file has further securityEvents. Done here + return true; + } + + // Current front file done. Advance front file + fileNrFront = (fileNrFront + 1) % MO_SECLOG_INDEX_MAX; + entryNrFront = 0; + entryNrEnd = 0; + + if (fileNrFront == fileNrEnd) { + // All logs sent. Done here + return true; + } + + // Check if new front logfile already contains entries and if so, update entryNrEnd. Skip corrupt files if necessary + + for (; fileNrFront != fileNrEnd; fileNrFront = (fileNrFront + 1) % MO_SECLOG_INDEX_MAX) { + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, sizeof(fn), MO_SECLOG_FN_PREFIX "%u.jsn", fileNrFront); + if (ret < 0 || (size_t)ret >= sizeof(fn)) { + MO_DBG_ERR("fn error: %i", ret); + return false; + } + + JsonDoc logFile (0); + auto status = FilesystemUtils::loadJson(filesystem, fn, logFile, getMemoryTag()); + switch (status) { + case FilesystemUtils::LoadStatus::Success: + break; //continue loading JSON + case FilesystemUtils::LoadStatus::FileNotFound: + break; //file gap - will skip this fileNr + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + return false; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("failure to load %s. Skipping logfile", fn); + break; + } + + if (status != FilesystemUtils::LoadStatus::Success) { + continue; + } + + if (logFile.size() == 0) { + MO_DBG_ERR("invalid state: %s. Skpping logfile", fn); + continue; + } + + unsigned int nextEntryNrFront = (unsigned int)logFile.size(); + unsigned int nextEntryNrEnd = (unsigned int)logFile.size(); + + for (size_t entryNr = 0; entryNr < logFile.size(); entryNr++) { + auto securityEvent = logFile[entryNr]; + if (securityEvent.containsKey("atnr")) { + // this is the first entry to be sent. Found front + nextEntryNrFront = (unsigned int)entryNr; + break; + } + } + + if (nextEntryNrFront == nextEntryNrEnd) { + MO_DBG_ERR("invalid state: %s. Skpping logfile", fn); + continue; + } + + // logfile is valid. Apply Nrs and exit for-loop + entryNrFront = nextEntryNrFront; + entryNrEnd = nextEntryNrEnd; + break; + } + + // Advanced to the next front SecurityEvent + return true; +} + +bool SecurityEventService::triggerSecurityEvent(const char *eventType) { + + if (!filesystem) { + // Volatile mode. Just enqueue SecurityEventNotification to OCPP message queue and return + auto securityEventNotification = makeRequest(context, new SecurityEventNotification(context, eventType, context.getClock().now())); + context.getMessageService().sendRequest(std::move(securityEventNotification)); + return true; + } + + // Append SecurityEvent to queue + + unsigned int fileNrBack = fileNrEnd; + JsonDoc logBack (0); + bool logBackLoaded = false; + + if (fileNrBegin != fileNrEnd) { + + unsigned int fileNrBack = (fileNrEnd + MO_SECLOG_INDEX_MAX - 1) % MO_SECLOG_INDEX_MAX; + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, sizeof(fn), MO_SECLOG_FN_PREFIX "%u.jsn", fileNrBack); + if (ret < 0 || (size_t)ret >= sizeof(fn)) { + MO_DBG_ERR("fn error: %i", ret); + return false; + } + + auto status = FilesystemUtils::loadJson(filesystem, fn, logBack, getMemoryTag()); + switch (status) { + case FilesystemUtils::LoadStatus::Success: + logBackLoaded = true; + break; //continue loading JSON + case FilesystemUtils::LoadStatus::FileNotFound: + break; //file gap - will skip this fileNr + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + return false; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("failure to load %s", fn); + break; + } + + if (!logBackLoaded || logBack.size() == 0) { // Is logBack errorneous? (load failure or empty JSON) + MO_DBG_ERR("invalid state: %s. Skpping logfile", fn); + fileNrBack = fileNrEnd; + logBack.clear(); + logBackLoaded = false; + } else if (logBack.size() >= MO_SECLOG_MAX_FILE_ENTRIES) { // Is logBack aready full? + // Yes, start new logfile + fileNrBack = fileNrEnd; + logBack.clear(); + logBackLoaded = false; + } + } + + // Is security log full? + unsigned int outstandingFiles = (fileNrBack + MO_SECLOG_INDEX_MAX - fileNrFront) % MO_SECLOG_INDEX_MAX; + if (outstandingFiles > MO_SECLOG_MAX_FILES) { + MO_DBG_ERR("SecurityLog capacity exceeded"); + return false; + } + + // Are there historic logfiles which should be deleted? + unsigned int historicFiles = (fileNrFront + MO_SECLOG_INDEX_MAX - fileNrBegin) % MO_SECLOG_INDEX_MAX; + if (historicFiles + outstandingFiles > MO_SECLOG_MAX_FILES) { + MO_DBG_ERR("Cleaning historic logfile"); + + char fn [MO_MAX_PATH_SIZE]; + auto ret = snprintf(fn, sizeof(fn), MO_SECLOG_FN_PREFIX "%u.jsn", fileNrBegin); + if (ret < 0 || (size_t)ret >= sizeof(fn)) { + MO_DBG_ERR("fn error: %i", ret); + return false; + } + + char path [MO_MAX_PATH_SIZE]; + if (!FilesystemUtils::printPath(filesystem, path, sizeof(path), fn)) { + MO_DBG_ERR("fn error: %i", ret); + return false; + } + + if (!filesystem->remove(path)) { + MO_DBG_ERR("failure to remove %s", path); + return false; + } + + // Advance fileNrBegin + fileNrBegin = (fileNrBegin + 1) % MO_SECLOG_INDEX_MAX; + } + + // Determine JSON capacity + + size_t capacity = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(3); + + if (logBackLoaded) { + // Keep exisitng logfile contents - therefore need enough space for copying everything over to next write + capacity += JSON_ARRAY_SIZE(logBack.size()); + for (size_t entryNr = 0; entryNr < logBack.size(); entryNr++) { + auto securityEvent = logBack[entryNr]; + if (securityEvent.containsKey("atnr")) { + capacity += JSON_ARRAY_SIZE(3); + } else { + capacity += JSON_ARRAY_SIZE(2); + } + } + } + + JsonDoc nextLogBack = initJsonDoc(getMemoryTag(), capacity); + + if (logBackLoaded) { + // Copy old Json object to new + nextLogBack = logBack; + } + + // Append SecurityEvent + auto securityEvent = nextLogBack.createNestedObject(); + securityEvent["type"] = eventType; + + Timestamp time = context.getClock().now(); + char timeStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!context.getClock().toJsonString(time, timeStr, sizeof(timeStr))) { + MO_DBG_ERR("Serialization error"); + return false; + } + securityEvent["time"] = (const char*)timeStr; // force zero-copy mode + + securityEvent["atnr"] = 0; + + if (nextLogBack.overflowed()) { + MO_DBG_ERR("Serialization error"); + return false; + } + + // Update logfile on flash + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, sizeof(fn), MO_SECLOG_FN_PREFIX "%u.jsn", fileNrBack); + if (ret < 0 || (size_t)ret >= sizeof(fn)) { + MO_DBG_ERR("fn error: %i", ret); + return false; + } + + if (FilesystemUtils::storeJson(filesystem, fn, nextLogBack) != FilesystemUtils::StoreStatus::Success) { + MO_DBG_ERR("FS error: %s", fn); + return false; + } + + // Successfully updated end of queue on flash. Update fileNrEnd (remains unchanged when SecurityEvent was appended to existing file) + fileNrEnd = (fileNrBack + 1) % MO_SECLOG_INDEX_MAX; + + // If current front logfile was updated, then need to update cached SecurityEvent counter + if (fileNrBack == fileNrFront) { + entryNrEnd++; + } + + return true; +} + +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SECURITY_EVENT diff --git a/src/MicroOcpp/Model/SecurityEvent/SecurityEventService.h b/src/MicroOcpp/Model/SecurityEvent/SecurityEventService.h new file mode 100644 index 00000000..c088f8cf --- /dev/null +++ b/src/MicroOcpp/Model/SecurityEvent/SecurityEventService.h @@ -0,0 +1,109 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#ifndef MO_SECURITYEVENTSERVICE_H +#define MO_SECURITYEVENTSERVICE_H + +#include +#include +#include +#include +#include + +#define MO_SECLOG_FN_PREFIX "slog-" +#define MO_SECLOG_INDEX_MAX 10000 +#define MO_SECLOG_INDEX_DIGITS 4 //digits needed to print MAX_INDEX-1 (=9999, i.e. 4 digits) + +#ifndef MO_SECLOG_MAX_FILES +#define MO_SECLOG_MAX_FILES 10 // Number of locally stored event log files. Must be smaller than (MO_SECLOG_INDEX_MAX / 2) +#endif + +#if !(MO_SECLOG_MAX_FILES < MO_SECLOG_INDEX_MAX / 2) +#error MO_SECLOG_MAX_FILES must be smaller than (MO_SECLOG_INDEX_MAX / 2) to allow ring index initialization from file list +#endif + +#ifndef MO_SECLOG_MAX_FILE_ENTRIES +#define MO_SECLOG_MAX_FILE_ENTRIES 20 // Number of SecurityEvents per File +#endif + +#ifndef MO_SECLOG_SEND_ATTEMPTS +#define MO_SECLOG_SEND_ATTEMPTS 3 // Number of attempts to send a SecurityEventNotification +#endif + +#ifndef MO_SECLOG_SEND_ATTEMPTS +#define MO_SECLOG_SEND_ATTEMPTS 3 // Number of attempts to send a SecurityEventNotification +#endif + +#ifndef MO_SECLOG_SEND_ATTEMPT_INTERVAL +#define MO_SECLOG_SEND_ATTEMPT_INTERVAL 180 +#endif + +// Length of field `type` of a SecurityEventNotificationRequest (1.49.1) +#define MO_SECURITY_EVENT_TYPE_LENGTH 50 + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SECURITY_EVENT + +namespace MicroOcpp { + +class Context; +class Connection; + +#if MO_ENABLE_V16 +namespace v16 { +class Configuration; +} +#endif +#if MO_ENABLE_V201 +namespace v201 { +class Variable; +} +#endif + +class SecurityEventService : public MemoryManaged { +private: + Context& context; + Connection *connection = nullptr; + MO_FilesystemAdapter *filesystem = nullptr; + + int ocppVersion = -1; + + #if MO_ENABLE_V16 + v16::Configuration *timeAdjustmentReportingThresholdIntV16 = nullptr; + #endif + #if MO_ENABLE_V201 + v201::Variable *timeAdjustmentReportingThresholdIntV201 = nullptr; + #endif + + Timestamp trackUptime; + Timestamp trackUnixTime; + int32_t trackTimeOffset = 0; + + unsigned int fileNrBegin = 0; //oldest (historical) log file on flash. Already sent via SecurityEventNotification, can still be fetched via GetLog + unsigned int fileNrFront = 0; //oldest log which is still queued to be sent to the server + unsigned int fileNrEnd = 0; //one position behind newest log file + + // Front logfile entries. entryNr specifies the position of a SecurityEvent in the front logfile + unsigned int entryNrFront = 0; //same logic as fileNr. The "Begin" number is always 0 and is not needed explict here + unsigned int entryNrEnd = 0; + + Timestamp attemptTime; + unsigned int attemptNr = 0; + bool isSecurityEventInProgress = false; + + bool fetchSecurityEventFront(char *type, Timestamp& timestamp, unsigned int& attemptNr); //read current front event into output parameters + bool advanceSecurityEventFront(); + +public: + SecurityEventService(Context& context); + + bool setup(); + + void loop(); + + bool triggerSecurityEvent(const char *eventType); +}; + +} //namespace MicroOcpp +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SECURITY_EVENT +#endif diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp index 3c71d539..49f271d5 100644 --- a/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp @@ -3,107 +3,229 @@ // MIT License #include +#include #include #include #include +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING + 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); +void mo_chargeRate_init(MO_ChargeRate *limit) { + limit->power = -1.f; + limit->current = -1.f; + limit->numberPhases = -1; + #if MO_ENABLE_V201 + limit->phaseToUse = -1; + #endif +} + +bool mo_chargeRate_equals(const MO_ChargeRate *a, const MO_ChargeRate *b) { + return + ((a->power < 0 && b->power < 0) || a->power == b->power) && + ((a->current < 0 && b->current < 0) || a->current == b->current) && + ((a->numberPhases < 0 && b->numberPhases < 0) || a->numberPhases == b->numberPhases) + #if MO_ENABLE_V201 + && ((a->phaseToUse < 0 && b->phaseToUse < 0) || a->phaseToUse == b->phaseToUse) + #endif + ; +} + +const char *MicroOcpp::serializeChargingProfilePurposeType(ChargingProfilePurposeType v) { + const char *res = ""; + switch (v) { + case ChargingProfilePurposeType::UNDEFINED: + MO_DBG_WARN("serialization error"); + break; + case ChargingProfilePurposeType::ChargePointMaxProfile: + res = "ChargePointMaxProfile"; + break; + case ChargingProfilePurposeType::TxDefaultProfile: + res = "TxDefaultProfile"; + break; + case ChargingProfilePurposeType::TxProfile: + res = "TxProfile"; + break; + } + return res; +} + +ChargingProfilePurposeType MicroOcpp::deserializeChargingProfilePurposeType(const char *str) { + ChargingProfilePurposeType res = ChargingProfilePurposeType::UNDEFINED; + if (!strcmp(str, "ChargePointMaxProfile")) { + res = ChargingProfilePurposeType::ChargePointMaxProfile; + } else if (!strcmp(str, "TxDefaultProfile")) { + res = ChargingProfilePurposeType::TxDefaultProfile; + } else if (!strcmp(str, "TxProfile")) { + res = ChargingProfilePurposeType::TxProfile; + } + return res; +} + +const char *MicroOcpp::serializeChargingRateUnitType(ChargingRateUnitType v) { + const char *res = ""; + switch (v) { + case ChargingRateUnitType::UNDEFINED: + res = ""; + break; + case ChargingRateUnitType::Watt: + res = "W"; + break; + case ChargingRateUnitType::Amp: + res = "A"; + break; + } + return res; +} + +ChargingRateUnitType MicroOcpp::deserializeChargingRateUnitType(const char *str) { + ChargingRateUnitType res = ChargingRateUnitType::UNDEFINED; + if (!strcmp(str, "W")) { + res = ChargingRateUnitType::Watt; + } else if (!strcmp(str, "A")) { + res = ChargingRateUnitType::Amp; + } else { + MO_DBG_ERR("deserialization error"); + } return res; } -ChargingSchedule::ChargingSchedule() : MemoryManaged("v16.SmartCharging.SmartChargingModel"), chargingSchedulePeriod{makeVector(getMemoryTag())} { +ChargingSchedule::ChargingSchedule() : MemoryManaged("v16.SmartCharging.SmartChargingModel") { + +} + +ChargingSchedule::~ChargingSchedule() { + resizeChargingSchedulePeriod(0); // frees all resources if called with 0 +} + +bool ChargingSchedule::resizeChargingSchedulePeriod(size_t chargingSchedulePeriodSize) { + for (size_t i = 0; i < this->chargingSchedulePeriodSize; i++) { + delete chargingSchedulePeriod[i]; + chargingSchedulePeriod[i] = nullptr; + } + MO_FREE(chargingSchedulePeriod); + chargingSchedulePeriod = nullptr; + this->chargingSchedulePeriodSize = 0; + + if (chargingSchedulePeriodSize == 0) { + return true; + } + + chargingSchedulePeriod = static_cast(MO_MALLOC(getMemoryTag(), sizeof(ChargingSchedulePeriod*) * chargingSchedulePeriodSize)); + if (!chargingSchedulePeriod) { + MO_DBG_ERR("OOM"); + return false; + } + memset(chargingSchedulePeriod, 0, sizeof(ChargingSchedulePeriod*) * chargingSchedulePeriodSize); + for (size_t i = 0; i < chargingSchedulePeriodSize; i++) { + chargingSchedulePeriod[i] = new ChargingSchedulePeriod(); + if (!chargingSchedulePeriod[i]) { + MO_DBG_ERR("OOM"); + resizeChargingSchedulePeriod(0); + return false; + } + this->chargingSchedulePeriodSize = i; + } + this->chargingSchedulePeriodSize = chargingSchedulePeriodSize; + return true; } -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 +bool ChargingSchedule::calculateLimit(int32_t unixTime, int32_t sessionDurationSecs, MO_ChargeRate& limit, int32_t& nextChangeSecs) { + if (unixTime == 0 && sessionDurationSecs < 0) { + MO_DBG_WARN("Neither absolute time nor session duration known. Cannot determine limit"); + return false; + } + if (startSchedule == 0 && sessionDurationSecs < 0) { //both undefined + MO_DBG_WARN("startSchedule not set and session duration unkown. Cannot determine limit"); + return false; + } + + int32_t scheduleSecs = -1; //time passed since begin of this schedule switch (chargingProfileKind) { - case (ChargingProfileKindType::Absolute): + case ChargingProfileKindType::Absolute: + if (unixTime == 0) { + //cannot use Absolute profiles when + MO_DBG_WARN("Need to set clock before using absolute profiles"); + return false; + } //check if schedule is not valid yet but begins in future - if (startSchedule > t) { + if (startSchedule != 0 && startSchedule > unixTime) { //not valid YET - nextChange = std::min(nextChange, startSchedule); + nextChangeSecs = std::min(nextChangeSecs, startSchedule - unixTime); 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; + //If charging profile is absolute, prefer startSchedule as basis. If absent, use session duration instead + if (startSchedule != 0) { + scheduleSecs = unixTime - startSchedule; } else { - MO_DBG_ERR("Absolute profile, but neither startSchedule, nor start of charging are set. Undefined behavior, abort"); - return false; + scheduleSecs = sessionDurationSecs; //sessionDurationSecs valid per precondition } 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)); + case ChargingProfileKindType::Recurring: { + int32_t recurrencySecs = 0; + switch (recurrencyKind) { + case RecurrencyKindType::Daily: + recurrencySecs = 24 * 3600; + break; + case RecurrencyKindType::Weekly: + recurrencySecs = 7 * 24 * 3600; + break; + case RecurrencyKindType::UNDEFINED: + MO_DBG_ERR("Recurring ChargingProfile but no RecurrencyKindType set. Assume 'Daily'"); + recurrencySecs = 24 * 3600; + break; + } + if (unixTime != 0 && startSchedule != 0) { + scheduleSecs = (unixTime - startSchedule) % recurrencySecs; } 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)); + scheduleSecs = sessionDurationSecs % recurrencySecs; //sessionDurationSecs valid per precondition } + nextChangeSecs = std::min(nextChangeSecs, recurrencySecs - scheduleSecs); //constrain nextChange to basis + one day break; - case (ChargingProfileKindType::Relative): + } + 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) { + if (sessionDurationSecs < 0) { //Relative charging profiles only work with a currently active charging session which is not the case here return false; } - basis = startOfCharging; + scheduleSecs = sessionDurationSecs; 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 + if (scheduleSecs >= duration) { //"duration" is given relative to basis return false; } else { - nextChange = std::min(nextChange, basis + duration); + nextChangeSecs = std::min(nextChangeSecs, duration - scheduleSecs); } } /* * 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 + * and make nextChange equal the beginning of the following chargingSchedulePeriod[i]. 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) { + int numberPhases_res = -1; + for (size_t i = 0; i < chargingSchedulePeriodSize; i++) { + if (chargingSchedulePeriod[i]->startPeriod > scheduleSecs) { // found the first period that comes after t_toBasis. - nextChange = basis + period->startPeriod; - nextChange = std::min(nextChange, basis + period->startPeriod); + nextChangeSecs = std::min(nextChangeSecs, chargingSchedulePeriod[i]->startPeriod - scheduleSecs); break; //The currently valid limit was set the iteration before } - limit_res = period->limit; - nphases_res = period->numberPhases; + limit_res = chargingSchedulePeriod[i]->limit; + numberPhases_res = chargingSchedulePeriod[i]->numberPhases; } - + if (limit_res >= 0.0f) { limit_res = std::max(limit_res, minChargingRate); @@ -113,402 +235,510 @@ bool ChargingSchedule::calculateLimit(const Timestamp &t, const Timestamp &start limit.power = limit_res; } - limit.nphases = nphases_res; + limit.numberPhases = numberPhases_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); +bool ChargingSchedule::parseJson(Clock& clock, int ocppVersion, JsonObject json) { - 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 MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + id = json["id"] | -1; + if (id < 0) { + MO_DBG_WARN("format violation"); + return false; } } - if (minChargingRate >= 0) { - doc["minChargeRate"] = minChargingRate; - } - - return true; -} + #endif //MO_ENABLE_V201 -void ChargingSchedule::printSchedule(){ - - char tmp[JSONDATE_LENGTH + 1] = {'\0'}; - startSchedule.toJsonString(tmp, JSONDATE_LENGTH + 1); + if (json.containsKey("duration")) { + duration = json["duration"] | -1; + if (duration < 0) { + MO_DBG_WARN("format violation"); + return false; + } + } - 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); + if (json.containsKey("startSchedule")) { + Timestamp startSchedule2; + if (!clock.parseString(json["startSchedule"] | "_Invalid", startSchedule2)) { + //non-success + MO_DBG_WARN("datetime format violation, expect format like 2022-02-01T20:53:32.486Z"); + return false; + } + if (!clock.toUnixTime(startSchedule2, startSchedule)) { + MO_DBG_ERR("internal error"); + return false; + } + } - 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); + chargingRateUnit = deserializeChargingRateUnitType(json["chargingRateUnit"] | "_Undefined"); + if (chargingRateUnit == ChargingRateUnitType::UNDEFINED) { + MO_DBG_WARN("format violation"); + return false; } -} -ChargingProfile::ChargingProfile() : MemoryManaged("v16.SmartCharging.ChargingProfile") { + JsonArray periodJsonArray = json["chargingSchedulePeriod"]; + if (periodJsonArray.size() < 1) { + MO_DBG_WARN("format violation"); + return false; + } -} + size_t chargingSchedulePeriodSize = periodJsonArray.size(); -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 (chargingSchedulePeriodSize > MO_ChargingScheduleMaxPeriods) { + MO_DBG_WARN("exceed ChargingScheduleMaxPeriods"); + return false; } - if (t < validFrom) { - nextChange = std::min(nextChange, validFrom); - return false; //no limit defined + + if (!resizeChargingSchedulePeriod(chargingSchedulePeriodSize)) { + MO_DBG_ERR("OOM"); + return false; } - return chargingSchedule.calculateLimit(t, startOfCharging, limit, nextChange); -} + for (size_t i = 0; i < chargingSchedulePeriodSize; i++) { -bool ChargingProfile::calculateLimit(const Timestamp &t, ChargeRate& limit, Timestamp& nextChange){ - return calculateLimit(t, MIN_TIME, limit, nextChange); -} + JsonObject periodJson = periodJsonArray[i]; + auto& period = *chargingSchedulePeriod[i]; -int ChargingProfile::getChargingProfileId() { - return chargingProfileId; -} + period.startPeriod = periodJson["startPeriod"] | -1; + if (period.startPeriod < 0) { + MO_DBG_WARN("format violation"); + return false; + } -int ChargingProfile::getTransactionId() { - return transactionId; -} + period.limit = periodJson["limit"] | -1.f; + if (period.limit < 0.f) { + MO_DBG_WARN("format violation"); + return false; + } -int ChargingProfile::getStackLevel(){ - return stackLevel; -} - -ChargingProfilePurposeType ChargingProfile::getChargingProfilePurpose(){ - return chargingProfilePurpose; -} + if (json.containsKey("numberPhases")) { + period.numberPhases = periodJson["numberPhases"] | -1; + if (period.numberPhases < 0 || period.numberPhases > 3) { + MO_DBG_WARN("format violation"); + return false; + } + } -bool ChargingProfile::toJson(JsonDoc& doc) { - - auto chargingScheduleDoc = initJsonDoc("v16.SmartCharging.ChargingSchedule"); - if (!chargingSchedule.toJson(chargingScheduleDoc)) { - return false; + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + if (json.containsKey("phaseToUse")) { + period.phaseToUse = json["phaseToUse"] | -1; + if (period.phaseToUse < 0 || period.phaseToUse > 3) { + MO_DBG_WARN("format violation"); + return false; + } + } + } + #endif //MO_ENABLE_V201 } - 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; + if (json.containsKey("minChargingRate")) { + float minChargingRateIn = json["minChargingRate"]; + if (minChargingRateIn >= 0.f) { + minChargingRate = minChargingRateIn; + } else { + MO_DBG_WARN("format violation"); + return false; + } } - 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; - } + return true; +} - switch (chargingProfileKind) { - case (ChargingProfileKindType::Absolute): - doc["chargingProfileKind"] = "Absolute"; - break; - case (ChargingProfileKindType::Recurring): - doc["chargingProfileKind"] = "Recurring"; - break; - case (ChargingProfileKindType::Relative): - doc["chargingProfileKind"] = "Relative"; - break; - } +size_t ChargingSchedule::getJsonCapacity(int ocppVersion, bool compositeSchedule) { + size_t capacity = 0; - switch (recurrencyKind) { - case (RecurrencyKindType::Daily): - doc["recurrencyKind"] = "Daily"; - break; - case (RecurrencyKindType::Weekly): - doc["recurrencyKind"] = "Weekly"; - break; - default: - break; + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + capacity += JSON_OBJECT_SIZE(5); //no of fields of ChargingSchedule } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + if (compositeSchedule) { + capacity += JSON_OBJECT_SIZE(5); //no of fields of a compositie ChargingSchedule + } else { + capacity += JSON_OBJECT_SIZE(6); //no of fields of ChargingSchedule + } + } + #endif //MO_ENABLE_V201 + + capacity += MO_JSONDATE_SIZE; //startSchedule / scheduleStart + capacity += JSON_ARRAY_SIZE(chargingSchedulePeriodSize) + chargingSchedulePeriodSize * ( + ocppVersion == MO_OCPP_V16 ? + JSON_OBJECT_SIZE(3) : + JSON_OBJECT_SIZE(4) + ); - char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; + return capacity; +} + +bool ChargingSchedule::toJson(Clock& clock, int ocppVersion, bool compositeSchedule, JsonObject out, int evseId) { - if (validFrom > MIN_TIME) { - if (!validFrom.toJsonString(timeStr, JSONDATE_LENGTH + 1)) { + char startScheduleStr [MO_JSONDATE_SIZE] = {'\0'}; + if (startSchedule) { + Timestamp startSchedule2; + if (!clock.fromUnixTime(startSchedule2, startSchedule)) { MO_DBG_ERR("serialization error"); return false; } - doc["validFrom"] = timeStr; - } - - if (validTo > MIN_TIME) { - if (!validTo.toJsonString(timeStr, JSONDATE_LENGTH + 1)) { + if (!clock.toJsonString(startSchedule2, startScheduleStr, sizeof(startScheduleStr))) { MO_DBG_ERR("serialization error"); return false; } - doc["validTo"] = timeStr; } - doc["chargingSchedule"] = chargingScheduleDoc; + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + out["startSchedule"] = startScheduleStr; + if (minChargingRate >= 0) { + out["minChargeRate"] = minChargingRate; + } + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + if (compositeSchedule) { + out["evseId"] = evseId; + out["scheduleStart"] = startScheduleStr; + } else { + out["id"] = id; + out["startSchedule"] = startScheduleStr; + if (minChargingRate >= 0) { + out["minChargeRate"] = minChargingRate; + } + //salesTariff not supported + } + } + #endif //MO_ENABLE_V201 + + if (duration >= 0) { + out["duration"] = duration; + } + out["chargingRateUnit"] = serializeChargingRateUnitType(chargingRateUnit); + + JsonArray periodArray = out.createNestedArray("chargingSchedulePeriod"); + for (size_t i = 0; i < chargingSchedulePeriodSize; i++) { + JsonObject entry = periodArray.createNestedObject(); + entry["startPeriod"] = chargingSchedulePeriod[i]->startPeriod; + entry["limit"] = chargingSchedulePeriod[i]->limit; + if (chargingSchedulePeriod[i]->numberPhases != 3) { + entry["numberPhases"] = chargingSchedulePeriod[i]->numberPhases; + } + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + if (chargingSchedulePeriod[i]->phaseToUse >= 0) { + entry["numberPhases"] = chargingSchedulePeriod[i]->numberPhases; + } + } + #endif //MO_ENABLE_V201 + } return true; } -void ChargingProfile::printProfile(){ +void ChargingSchedule::printSchedule(Clock& clock){ - char tmp[JSONDATE_LENGTH + 1] = {'\0'}; - validFrom.toJsonString(tmp, JSONDATE_LENGTH + 1); - char tmp2[JSONDATE_LENGTH + 1] = {'\0'}; - validTo.toJsonString(tmp2, JSONDATE_LENGTH + 1); + char tmp[MO_JSONDATE_SIZE] = {'\0'}; + if (startSchedule) { + Timestamp startSchedule2; + clock.fromUnixTime(startSchedule2, startSchedule); + clock.toJsonString(startSchedule2, tmp, sizeof(tmp)); + } - 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", + MO_DBG_VERBOSE(" > CHARGING SCHEDULE:\n" \ + " > duration: %i\n" \ + " > startSchedule: %s\n" \ + " > chargingRateUnit: %s\n" \ + " > minChargingRate: %f\n", + duration, tmp, - tmp2 - ); + serializeChargingRateUnitType(chargingRateUnit), + minChargingRate); - chargingSchedule.printSchedule(); + for (size_t i = 0; i < chargingSchedulePeriodSize; i++) { + MO_DBG_VERBOSE(" > CHARGING SCHEDULE PERIOD:\n" \ + " > startPeriod: %i\n" \ + " > limit: %f\n" \ + " > numberPhases: %i\n", + chargingSchedulePeriod[i]->startPeriod, + chargingSchedulePeriod[i]->limit, + chargingSchedulePeriod[i]->numberPhases); + } } -namespace MicroOcpp { +ChargingProfile::ChargingProfile() : MemoryManaged("v16.SmartCharging.ChargingProfile") { -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; +bool ChargingProfile::calculateLimit(int32_t unixTime, int32_t sessionDurationSecs, MO_ChargeRate& limit, int32_t& nextChangeSecs){ + if (unixTime && validTo && unixTime > validTo) { + return false; //no limit defined } - - if (json.containsKey("numberPhases")) { - int numberPhases = json["numberPhases"]; - if (numberPhases >= 0 && numberPhases <= 3) { - out.numberPhases = numberPhases; - } else { - MO_DBG_WARN("format violation"); - return false; - } + if (unixTime && validFrom && unixTime < validFrom) { + nextChangeSecs = std::min(nextChangeSecs, validFrom - unixTime); + return false; //no limit defined } - return true; + return chargingSchedule.calculateLimit(unixTime, sessionDurationSecs, limit, nextChangeSecs); } -} //end namespace MicroOcpp +bool ChargingProfile::parseJson(Clock& clock, int ocppVersion, JsonObject json) { -std::unique_ptr MicroOcpp::loadChargingProfile(JsonObject& json) { - auto res = std::unique_ptr(new ChargingProfile()); + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + chargingProfileId = json["chargingProfileId"] | -1; + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + chargingProfileId = json["id"] | -1; + } + #endif //MO_ENABLE_V201 - int chargingProfileId = json["chargingProfileId"] | -1; - if (chargingProfileId >= 0) { - res->chargingProfileId = chargingProfileId; - } else { + if (chargingProfileId < 0) { MO_DBG_WARN("format violation"); - return nullptr; + return false; } - int transactionId = json["transactionId"] | -1; - if (transactionId >= 0) { - res->transactionId = transactionId; + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + int transactionId = json["transactionId"] | -1; + if (transactionId >= 0) { + transactionId16 = transactionId; + } + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + if (json.containsKey("transactionId")) { + auto ret = snprintf(transactionId201, sizeof(transactionId201), "%s", json["transactionId"] | ""); + if (ret < 0 || (size_t)ret >= sizeof(transactionId201)) { + MO_DBG_WARN("format violation"); + return false; + } + } } + #endif //MO_ENABLE_V201 - int stackLevel = json["stackLevel"] | -1; - if (stackLevel >= 0 && stackLevel <= MO_ChargeProfileMaxStackLevel) { - res->stackLevel = stackLevel; - } else { + stackLevel = json["stackLevel"] | -1; + if (stackLevel < 0 || stackLevel > MO_ChargeProfileMaxStackLevel) { MO_DBG_WARN("format violation"); - return nullptr; + return false; } - 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 { + chargingProfilePurpose = deserializeChargingProfilePurposeType(json["chargingProfilePurpose"] | "_Invalid"); + if (chargingProfilePurpose == ChargingProfilePurposeType::UNDEFINED) { MO_DBG_WARN("format violation"); - return nullptr; + return false; } - const char *chargingProfileKindStr = json["chargingProfileKind"] | "Invalid"; + const char *chargingProfileKindStr = json["chargingProfileKind"] | "_Invalid"; if (!strcmp(chargingProfileKindStr, "Absolute")) { - res->chargingProfileKind = ChargingProfileKindType::Absolute; + chargingProfileKind = ChargingProfileKindType::Absolute; } else if (!strcmp(chargingProfileKindStr, "Recurring")) { - res->chargingProfileKind = ChargingProfileKindType::Recurring; + chargingProfileKind = ChargingProfileKindType::Recurring; } else if (!strcmp(chargingProfileKindStr, "Relative")) { - res->chargingProfileKind = ChargingProfileKindType::Relative; + chargingProfileKind = ChargingProfileKindType::Relative; } else { MO_DBG_WARN("format violation"); - return nullptr; + return false; } - const char *recurrencyKindStr = json["recurrencyKind"] | "Invalid"; - if (!strcmp(recurrencyKindStr, "Daily")) { - res->recurrencyKind = RecurrencyKindType::Daily; - } else if (!strcmp(recurrencyKindStr, "Weekly")) { - res->recurrencyKind = RecurrencyKindType::Weekly; + if (json.containsKey("recurrencyKind")) { + const char *recurrencyKindStr = json["recurrencyKind"] | "_Invalid"; + if (!strcmp(recurrencyKindStr, "Daily")) { + recurrencyKind = RecurrencyKindType::Daily; + } else if (!strcmp(recurrencyKindStr, "Weekly")) { + recurrencyKind = RecurrencyKindType::Weekly; + } else { + MO_DBG_WARN("format violation"); + return false; + } } - MO_DBG_DEBUG("Deserialize JSON: chargingProfileId=%i, chargingProfilePurpose=%s, recurrencyKind=%s", chargingProfileId, chargingProfilePurposeStr, recurrencyKindStr); + MO_DBG_DEBUG("Deserialize JSON: chargingProfileId=%i, chargingProfilePurpose=%s", chargingProfileId, serializeChargingProfilePurposeType(chargingProfilePurpose)); if (json.containsKey("validFrom")) { - if (!res->validFrom.setTime(json["validFrom"] | "Invalid")) { + Timestamp validFrom2; + if (!clock.parseString(json["validFrom"] | "_Invalid", validFrom2)) { //non-success MO_DBG_WARN("datetime format violation, expect format like 2022-02-01T20:53:32.486Z"); - return nullptr; + return false; + } + if (!clock.toUnixTime(validFrom2, validFrom)) { + MO_DBG_ERR("internal error"); + return false; } - } else { - res->validFrom = MIN_TIME; } if (json.containsKey("validTo")) { - if (!res->validTo.setTime(json["validTo"] | "Invalid")) { + Timestamp validTo2; + if (!clock.parseString(json["validTo"] | "_Invalid", validTo2)) { //non-success MO_DBG_WARN("datetime format violation, expect format like 2022-02-01T20:53:32.486Z"); - return nullptr; + return false; + } + if (!clock.toUnixTime(validTo2, validTo)) { + MO_DBG_ERR("internal error"); + return false; } - } else { - res->validTo = MIN_TIME; } - JsonObject scheduleJson = json["chargingSchedule"]; - ChargingSchedule& schedule = res->chargingSchedule; - auto success = loadChargingSchedule(scheduleJson, schedule); + auto success = chargingSchedule.parseJson(clock, ocppVersion, json["chargingSchedule"]); if (!success) { - return nullptr; + return false; } //duplicate some fields to chargingSchedule to simplify the max charge rate calculation - schedule.chargingProfileKind = res->chargingProfileKind; - schedule.recurrencyKind = res->recurrencyKind; + chargingSchedule.chargingProfileKind = chargingProfileKind; + chargingSchedule.recurrencyKind = recurrencyKind; - return res; + return true; } -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; +size_t ChargingProfile::getJsonCapacity(int ocppVersion) { + size_t capacity = 0; + + capacity += chargingSchedule.getJsonCapacity(ocppVersion, /*compositeSchedule*/ false); + + capacity += JSON_OBJECT_SIZE(9) + //no. of fields in ChargingProfile + 2 * MO_JSONDATE_SIZE; //validFrom and validTo + + return capacity; +} + +bool ChargingProfile::toJson(Clock& clock, int ocppVersion, JsonObject out) { + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + out["chargingProfileId"] = chargingProfileId; + if (transactionId16 >= 0) { + out["transactionId"] = transactionId16; } } - - 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; + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + out["id"] = chargingProfileId; + if (*transactionId201) { + out["transactionId"] = (const char*)transactionId201; //force zero-copy } - } else { - out.startSchedule = MIN_TIME; } + #endif //MO_ENABLE_V201 - 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; + out["stackLevel"] = stackLevel; + + out["chargingProfilePurpose"] = serializeChargingProfilePurposeType(chargingProfilePurpose); + + switch (chargingProfileKind) { + case ChargingProfileKindType::Absolute: + out["chargingProfileKind"] = "Absolute"; + break; + case ChargingProfileKindType::Recurring: + out["chargingProfileKind"] = "Recurring"; + break; + case ChargingProfileKindType::Relative: + out["chargingProfileKind"] = "Relative"; + break; } - if (periodJsonArray.size() > MO_ChargingScheduleMaxPeriods) { - MO_DBG_WARN("exceed ChargingScheduleMaxPeriods"); - return false; + switch (recurrencyKind) { + case RecurrencyKindType::Daily: + out["recurrencyKind"] = "Daily"; + break; + case RecurrencyKindType::Weekly: + out["recurrencyKind"] = "Weekly"; + break; + default: + break; } - for (JsonObject periodJson : periodJsonArray) { - out.chargingSchedulePeriod.emplace_back(); - if (!loadChargingSchedulePeriod(periodJson, out.chargingSchedulePeriod.back())) { + if (validFrom) { + Timestamp validFrom2; + if (!clock.fromUnixTime(validFrom2, validFrom)) { + MO_DBG_ERR("serialization error"); return false; } + char validFromStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!clock.toJsonString(validFrom2, validFromStr, sizeof(validFromStr))) { + MO_DBG_ERR("serialization error"); + return false; + } + out["validFrom"] = validFromStr; } - - if (json.containsKey("minChargingRate")) { - float minChargingRate = json["minChargingRate"]; - if (minChargingRate >= 0.f) { - out.minChargingRate = minChargingRate; - } else { - MO_DBG_WARN("format violation"); + + if (validTo) { + Timestamp validTo2; + if (!clock.fromUnixTime(validTo2, validTo)) { + MO_DBG_ERR("serialization error"); + return false; + } + char validToStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!clock.toJsonString(validTo2, validToStr, sizeof(validToStr))) { + MO_DBG_ERR("serialization error"); return false; } + out["validTo"] = validToStr; + } + + JsonObject chargingScheduleJson = out.createNestedObject("chargingSchedule"); + if (!chargingSchedule.toJson(clock, ocppVersion, /*compositeSchedule*/ false, chargingScheduleJson)) { + return false; } return true; } + +void ChargingProfile::printProfile(Clock& clock){ + + char validFromStr [MO_JSONDATE_SIZE] = {'\0'}; + if (validFrom) { + Timestamp validFrom2; + clock.fromUnixTime(validFrom2, validFrom); + clock.toJsonString(validFrom2, validFromStr, sizeof(validFromStr)); + } + + char validToStr [MO_JSONDATE_SIZE] = {'\0'}; + if (validTo) { + Timestamp validTo2; + clock.fromUnixTime(validTo2, validTo); + clock.toJsonString(validTo2, validToStr, sizeof(validToStr)); + } + + MO_DBG_VERBOSE(" > CHARGING PROFILE:\n" \ + " > chargingProfileId: %i\n" \ + " > transactionId16: %i\n" \ + " > transactionId201: %i\n" \ + " > stackLevel: %i\n" \ + " > chargingProfilePurpose: %s\n", + chargingProfileId, + transactionId16, + transactionId201, + stackLevel, + serializeChargingProfilePurposeType(chargingProfilePurpose) + ); + MO_DBG_VERBOSE( + " > 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::UNDEFINED) ? "NOT_SET" : "Error", + validFrom, + validTo + ); + + chargingSchedule.printSchedule(clock); +} + +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.h b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.h index 825d6866..b7ac31e7 100644 --- a/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.h +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.h @@ -2,13 +2,21 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef SMARTCHARGINGMODEL_H -#define SMARTCHARGINGMODEL_H +#ifndef MO_SMARTCHARGINGMODEL_H +#define MO_SMARTCHARGINGMODEL_H #ifndef MO_ChargeProfileMaxStackLevel #define MO_ChargeProfileMaxStackLevel 8 #endif +#ifndef MO_ChargeProfileMaxStackLevel_digits +#define MO_ChargeProfileMaxStackLevel_digits 1 //digits needed to print MO_ChargeProfileMaxStackLevel (=8, i.e. 1 digit) +#endif + +#if MO_ChargeProfileMaxStackLevel >= 10 && MO_ChargeProfileMaxStackLevel_digits < 2 +#error Need to set build flag MO_ChargeProfileMaxStackLevel_digits +#endif + #ifndef MO_ChargingScheduleMaxPeriods #define MO_ChargingScheduleMaxPeriods 24 #endif @@ -24,98 +32,130 @@ #include #include +#include +#include + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + float power; //in W. Negative if undefined + float current; //in A. Negative if undefined + int numberPhases; //Negative if undefined + + #if MO_ENABLE_V201 + int phaseToUse; //select between L1, L2, L3 (negative if undefined) + #endif //MO_ENABLE_V201 +} MO_ChargeRate; + +void mo_chargeRate_init(MO_ChargeRate *limit); //initializes all fields to -1 +bool mo_chargeRate_equals(const MO_ChargeRate *a, const MO_ChargeRate *b); + +#ifdef __cplusplus +} //extern "C" namespace MicroOcpp { -enum class ChargingProfilePurposeType { +class Clock; + +enum class ChargingProfilePurposeType : uint8_t { + UNDEFINED, ChargePointMaxProfile, TxDefaultProfile, TxProfile }; +const char *serializeChargingProfilePurposeType(ChargingProfilePurposeType v); +ChargingProfilePurposeType deserializeChargingProfilePurposeType(const char *str); -enum class ChargingProfileKindType { +enum class ChargingProfileKindType : uint8_t { Absolute, Recurring, Relative }; -enum class RecurrencyKindType { - NOT_SET, //not part of OCPP 1.6 +enum class RecurrencyKindType : uint8_t { + UNDEFINED, //MO-internal Daily, Weekly }; -enum class ChargingRateUnitType { +enum class ChargingRateUnitType : uint8_t { + UNDEFINED, //MO-internal 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); +const char *serializeChargingRateUnitType(ChargingRateUnitType v); +ChargingRateUnitType deserializeChargingRateUnitType(const char *str); class ChargingSchedulePeriod { public: int startPeriod; float limit; int numberPhases = 3; + #if MO_ENABLE_V201 + int phaseToUse = -1; + #endif }; class ChargingSchedule : public MemoryManaged { public: + #if MO_ENABLE_V201 + int id = -1; //defined for ChargingScheduleType (2.12), not used for CompositeScheduleType (2.18) + #endif int duration = -1; - Timestamp startSchedule; + int32_t startSchedule = 0; //unix time. 0 if undefined ChargingRateUnitType chargingRateUnit; - Vector chargingSchedulePeriod; + ChargingSchedulePeriod **chargingSchedulePeriod = nullptr; + size_t chargingSchedulePeriodSize = 0; 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 + RecurrencyKindType recurrencyKind = RecurrencyKindType::UNDEFINED; //copied from ChargingProfile to increase cohesion of limit algorithms ChargingSchedule(); + ~ChargingSchedule(); + + bool resizeChargingSchedulePeriod(size_t chargingSchedulePeriodSize); + /** * 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 calculateLimit(int32_t unixTime, int32_t sessionDurationSecs, MO_ChargeRate& limit, int32_t& nextChangeSecs); - bool toJson(JsonDoc& out); + bool parseJson(Clock& clock, int ocppVersion, JsonObject json); + size_t getJsonCapacity(int ocppVersion, bool compositeSchedule); + bool toJson(Clock& clock, int ocppVersion, bool compositeSchedule, JsonObject out, int evseId = -1); /* * print on console */ - void printSchedule(); + void printSchedule(Clock& clock); }; class ChargingProfile : public MemoryManaged { public: int chargingProfileId = -1; - int transactionId = -1; + #if MO_ENABLE_V16 + int transactionId16 = -1; + #endif + #if MO_ENABLE_V201 + char transactionId201 [MO_TXID_SIZE] = {'\0'}; + #endif 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; + RecurrencyKindType recurrencyKind {RecurrencyKindType::UNDEFINED}; // copied to ChargingSchedule to increase cohesion + int32_t validFrom = 0; //unix time. 0 if undefined + int32_t validTo = 0; //unix time. 0 if undefined ChargingSchedule chargingSchedule; ChargingProfile(); @@ -123,36 +163,24 @@ class ChargingProfile : public MemoryManaged { /** * 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 calculateLimit(int32_t unixTime, int32_t sessionDurationSecs, MO_ChargeRate& limit, int32_t& nextChangeSecs); - bool toJson(JsonDoc& out); + bool parseJson(Clock& clock, int ocppVersion, JsonObject json); + size_t getJsonCapacity(int ocppVersion); + bool toJson(Clock& clock, int ocppVersion, JsonObject out); /* * print on console */ - void printProfile(); + void printProfile(Clock& clock); }; -std::unique_ptr loadChargingProfile(JsonObject& json); - -bool loadChargingSchedule(JsonObject& json, ChargingSchedule& out); - -} //end namespace MicroOcpp - +} //namespace MicroOcpp +#endif //__cplusplus +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING #endif diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp index 762bd6ac..e249e455 100644 --- a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp @@ -3,61 +3,109 @@ // MIT License #include -#include + +#include #include -#include -#include +#include +#include +#include +#include #include #include #include #include #include -using namespace::MicroOcpp; +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING -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) { - +namespace MicroOcpp { +template +const T& minIfDefined(const T& a, const T& b) { + return (a >= (T)0 && b >= (T)0) ? std::min(a, b) : std::max(a, b); } -SmartChargingConnector::~SmartChargingConnector() { +//filesystem-related helper functions +namespace SmartChargingServiceUtils { +bool printProfileFileName(char *out, size_t bufsize, unsigned int evseId, ChargingProfilePurposeType purpose, unsigned int stackLevel); +bool storeProfile(MO_FilesystemAdapter *filesystem, Clock& clock, int ocppVersion, unsigned int evseId, ChargingProfile *chargingProfile); +bool removeProfile(MO_FilesystemAdapter *filesystem, unsigned int evseId, ChargingProfilePurposeType purpose, unsigned int stackLevel); +} //namespace SmartChargingServiceUtils +} //namespace MicroOcpp + +using namespace::MicroOcpp; + +SmartChargingServiceEvse::SmartChargingServiceEvse(Context& context, SmartChargingService& scService, unsigned int evseId) : + MemoryManaged("v16/v201.SmartCharging.SmartChargingServiceEvse"), context(context), clock(context.getClock()), scService(scService), evseId(evseId) { + mo_chargeRate_init(&trackLimitOutput); +} + +SmartChargingServiceEvse::~SmartChargingServiceEvse() { + for (size_t i = 0; i < MO_CHARGEPROFILESTACK_SIZE; i++) { + delete txDefaultProfile[i]; + txDefaultProfile[i] = nullptr; + } + for (size_t i = 0; i < MO_CHARGEPROFILESTACK_SIZE; i++) { + delete txProfile[i]; + txProfile[i] = nullptr; + } } /* * 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) { +void SmartChargingServiceEvse::calculateLimit(int32_t unixTime, int32_t sessionDurationSecs, MO_ChargeRate& limitOut, int32_t& nextChangeSecsOut) { //initialize output parameters with the default values - limitOut = ChargeRate(); - validToOut = MAX_TIME; + mo_chargeRate_init(&limitOut); + nextChangeSecsOut = 365 * 24 * 3600; bool txLimitDefined = false; - ChargeRate txLimit; + MO_ChargeRate txLimit; + mo_chargeRate_init(&txLimit); - //first, check if TxProfile is defined and limits charging + //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 (txProfile[i]) { + + bool txMatch = false; + + #if MO_ENABLE_V16 + if (scService.ocppVersion == MO_OCPP_V16) { + txMatch = (trackTxRmtProfileId >= 0 && trackTxRmtProfileId == txProfile[i]->chargingProfileId) || + txProfile[i]->transactionId16 < 0 || + trackTransactionId16 == txProfile[i]->transactionId16; + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (scService.ocppVersion == MO_OCPP_V201) { + txMatch = (trackTxRmtProfileId >= 0 && trackTxRmtProfileId == txProfile[i]->chargingProfileId) || + !*txProfile[i]->transactionId201 || + !strcmp(trackTransactionId201, txProfile[i]->transactionId201); + } + #endif //MO_ENABLE_V201 + + if (txMatch) { + MO_ChargeRate crOut; + mo_chargeRate_init(&crOut); + bool defined = txProfile[i]->calculateLimit(unixTime, sessionDurationSecs, crOut, nextChangeSecsOut); + if (defined) { + txLimitDefined = true; + txLimit = crOut; + break; + } } } } - //if no TxProfile limits charging, check the TxDefaultProfiles for this connector - if (!txLimitDefined && trackTxStart < MAX_TIME) { + //if no txProfile limits charging, check the txDefaultProfiles for this connector + if (!txLimitDefined && sessionDurationSecs >= 0) { for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { - if (TxDefaultProfile[i]) { - ChargeRate crOut; - bool defined = TxDefaultProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); + if (txDefaultProfile[i]) { + MO_ChargeRate crOut; + mo_chargeRate_init(&crOut); + bool defined = txDefaultProfile[i]->calculateLimit(unixTime, sessionDurationSecs, crOut, nextChangeSecsOut); if (defined) { txLimitDefined = true; txLimit = crOut; @@ -67,12 +115,13 @@ void SmartChargingConnector::calculateLimit(const Timestamp &t, ChargeRate& limi } } - //if no appropriate TxDefaultProfile is set for this connector, search in the general TxDefaultProfiles - if (!txLimitDefined && trackTxStart < MAX_TIME) { + //if no appropriate txDefaultProfile is set for this connector, search in the general txDefaultProfiles + if (!txLimitDefined && sessionDurationSecs >= 0) { for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { - if (ChargePointTxDefaultProfile[i]) { - ChargeRate crOut; - bool defined = ChargePointTxDefaultProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); + if (scService.chargePointMaxProfile[i]) { + MO_ChargeRate crOut; + mo_chargeRate_init(&crOut); + bool defined = scService.chargePointMaxProfile[i]->calculateLimit(unixTime, sessionDurationSecs, crOut, nextChangeSecsOut); if (defined) { txLimitDefined = true; txLimit = crOut; @@ -82,13 +131,15 @@ void SmartChargingConnector::calculateLimit(const Timestamp &t, ChargeRate& limi } } - ChargeRate cpLimit; + MO_ChargeRate cpLimit; + mo_chargeRate_init(&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 (scService.chargePointMaxProfile[i]) { + MO_ChargeRate crOut; + mo_chargeRate_init(&crOut); + bool defined = scService.chargePointMaxProfile[i]->calculateLimit(unixTime, sessionDurationSecs, crOut, nextChangeSecsOut); if (defined) { cpLimit = crOut; break; @@ -96,323 +147,579 @@ void SmartChargingConnector::calculateLimit(const Timestamp &t, ChargeRate& limi } } - //apply ChargePointMaxProfile value to calculated limit - limitOut = chargeRate_min(txLimit, cpLimit); -} - -void SmartChargingConnector::trackTransaction() { + //apply ChargePointMaxProfile value to calculated limit: + //determine the minimum of both values if at least one is defined, otherwise assign -1 for undefined + limitOut.power = minIfDefined(txLimit.power, cpLimit.power); + limitOut.current = minIfDefined(txLimit.current, cpLimit.current); + limitOut.numberPhases = minIfDefined(txLimit.numberPhases, cpLimit.numberPhases); - Transaction *tx = nullptr; - if (model.getConnector(connectorId)) { - tx = model.getConnector(connectorId)->getTransaction().get(); + #if MO_ENABLE_V201 + if (cpLimit.phaseToUse >= 0) { + //ChargePoint Schedule always takes precedence for phaseToUse + limitOut.phaseToUse = cpLimit.phaseToUse; + } else { + limitOut.phaseToUse = txLimit.phaseToUse; //note: can be -1 for undefined } + #endif +} + +void SmartChargingServiceEvse::trackTransaction() { 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; + #if MO_ENABLE_V16 + if (scService.ocppVersion == MO_OCPP_V16) { + auto txSvc = context.getModel16().getTransactionService(); + auto txSvcEvse = txSvc ? txSvc->getEvse(evseId) : nullptr; + auto tx = txSvcEvse ? txSvcEvse->getTransaction() : nullptr; - clearChargingProfile([this] (int, int connectorId, ChargingProfilePurposeType purpose, int) { - return purpose == ChargingProfilePurposeType::TxProfile && - (int) this->connectorId == connectorId; - }); + if (tx) { + if (tx->getTxProfileId() != trackTxRmtProfileId) { + update = true; + trackTxRmtProfileId = tx->getTxProfileId(); + } + int32_t dtTxStart; + if (tx->getStartSync().isRequested() && (!trackTxStart.isDefined() || (clock.delta(tx->getStartTimestamp(), trackTxStart, dtTxStart) && dtTxStart != 0))) { + update = true; + trackTxStart = tx->getStartTimestamp(); + } + if (tx->getStartSync().isConfirmed() && tx->getTransactionId() != trackTransactionId16) { + update = true; + trackTransactionId16 = tx->getTransactionId(); + } + } else { + //check if transaction has just been completed + if (trackTxRmtProfileId >= 0 || trackTxStart.isDefined() || trackTransactionId16 >= 0) { + //yes, clear data + update = true; + trackTxRmtProfileId = -1; + trackTxStart = Timestamp(); + trackTransactionId16 = -1; + + clearChargingProfile(-1, ChargingProfilePurposeType::TxProfile, -1); + } } } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (scService.ocppVersion == MO_OCPP_V201) { + auto txService = context.getModel201().getTransactionService(); + auto txServiceEvse = txService ? txService->getEvse(evseId) : nullptr; + auto tx = txServiceEvse ? txServiceEvse->getTransaction() : nullptr; + + if (tx) { + if (tx->txProfileId != trackTxRmtProfileId) { + update = true; + trackTxRmtProfileId = tx->txProfileId; + } + if (tx->started) { + int32_t dt; + if (!clock.delta(tx->startTimestamp, trackTxStart, dt)) { + dt = -1; //force update + } + if (dt != 0) { + update = true; + trackTxStart = tx->startTimestamp; + } + } + if (strcmp(tx->transactionId, trackTransactionId201)) { + update = true; + auto ret = snprintf(trackTransactionId201, sizeof(trackTransactionId201), "%s", tx->transactionId); + if (ret < 0 || (size_t)ret >= sizeof(trackTransactionId201)) { + MO_DBG_ERR("snprintf: %i", ret); + memset(trackTransactionId201, 0, sizeof(trackTransactionId201)); + } + } + } else { + //check if transaction has just been completed + if (trackTxRmtProfileId >= 0 || trackTxStart.isDefined() || *trackTransactionId201) { + //yes, clear data + update = true; + trackTxRmtProfileId = -1; + trackTxStart = Timestamp(); + memset(trackTransactionId201, 0, sizeof(trackTransactionId201)); + + clearChargingProfile(-1, ChargingProfilePurposeType::TxProfile, -1); + } + } + } + #endif //MO_ENABLE_V201 if (update) { - nextChange = model.getClock().now(); //will refresh limit calculation + nextChange = clock.now(); //will refresh limit calculation } } -void SmartChargingConnector::loop(){ +void SmartChargingServiceEvse::loop(){ trackTransaction(); /** * check if to call onLimitChange */ - auto& tnow = model.getClock().now(); + auto& tnow = clock.now(); - if (tnow >= nextChange){ + int32_t dtNextChange; + if (!clock.delta(tnow, nextChange, dtNextChange)) { + dtNextChange = 0; + } - ChargeRate limit; - nextChange = MAX_TIME; //reset nextChange to default value and refresh it + if (dtNextChange >= 0) { - calculateLimit(tnow, limit, nextChange); + int32_t unixTime = -1; + if (!clock.toUnixTime(tnow, unixTime)) { + unixTime = 0; //undefined + } + + int32_t sessionDurationSecs = -1; + if (!trackTxStart.isDefined() || !clock.delta(tnow, trackTxStart, sessionDurationSecs)) { + sessionDurationSecs = -1; + } + + MO_ChargeRate limit; + int32_t nextChangeSecs; + + calculateLimit(unixTime, sessionDurationSecs, limit, nextChangeSecs); + + //reset nextChange with nextChangeSecs + nextChange = tnow; + clock.add(nextChange, nextChangeSecs); #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); + char timestamp1[MO_JSONDATE_SIZE] = {'\0'}; + if (!clock.toJsonString(tnow, timestamp1, sizeof(timestamp1))) { + timestamp1[0] = '\0'; + } + char timestamp2[MO_JSONDATE_SIZE] = {'\0'}; + if (!clock.toJsonString(nextChange, timestamp2, sizeof(timestamp2))) { + timestamp1[0] = '\0'; + } MO_DBG_INFO("New limit for connector %u, scheduled at = %s, nextChange = %s, limit = {%.1f, %.1f, %i}", - connectorId, + evseId, 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); + limit.power, + limit.current, + limit.numberPhases); } #endif - if (trackLimitOutput != limit) { + if (!mo_chargeRate_equals(&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); + limitOutput(limit, evseId, limitOutputUserData); trackLimitOutput = limit; } } } } -void SmartChargingConnector::setSmartChargingOutput(std::function limitOutput) { +void SmartChargingServiceEvse::setSmartChargingOutput(void (*limitOutput)(MO_ChargeRate limit, unsigned int evseId, void *userData), bool powerSupported, bool currentSupported, bool phases3to1Supported, bool phaseSwitchingSupported, void *userData) { if (this->limitOutput) { MO_DBG_WARN("replacing existing SmartChargingOutput"); } this->limitOutput = limitOutput; + this->limitOutputUserData = userData; + + scService.powerSupported |= powerSupported; + scService.currentSupported |= currentSupported; + scService.phases3to1Supported |= phases3to1Supported; + #if MO_ENABLE_V201 + scService.phaseSwitchingSupported |= phaseSwitchingSupported; + #endif //MO_ENABLE_V201 } -ChargingProfile *SmartChargingConnector::updateProfiles(std::unique_ptr chargingProfile) { - - int stackLevel = chargingProfile->getStackLevel(); //already validated - - switch (chargingProfile->getChargingProfilePurpose()) { - case (ChargingProfilePurposeType::ChargePointMaxProfile): +bool SmartChargingServiceEvse::updateProfile(std::unique_ptr chargingProfile, bool updateFile) { + + ChargingProfile **stack = nullptr; + + switch (chargingProfile->chargingProfilePurpose) { + case ChargingProfilePurposeType::TxDefaultProfile: + stack = txDefaultProfile; + break; + case ChargingProfilePurposeType::TxProfile: + stack = txProfile; 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(); + default: + break; + } + + if (!stack) { + MO_DBG_ERR("invalid args"); + return false; + } + + if (updateFile && scService.filesystem) { + if (!SmartChargingServiceUtils::storeProfile(scService.filesystem, clock, scService.ocppVersion, evseId, chargingProfile.get())) { + MO_DBG_ERR("fs error"); + return false; + } } - MO_DBG_ERR("invalid args"); - return nullptr; + + int stackLevel = chargingProfile->stackLevel; //already validated + + delete stack[stackLevel]; + stack[stackLevel] = nullptr; + stack[stackLevel] = chargingProfile.release(); + + return true; } -void SmartChargingConnector::notifyProfilesUpdated() { - nextChange = model.getClock().now(); +void SmartChargingServiceEvse::notifyProfilesUpdated() { + nextChange = clock.now(); } -bool SmartChargingConnector::clearChargingProfile(const std::function filter) { +bool SmartChargingServiceEvse::clearChargingProfile(int chargingProfileId, ChargingProfilePurposeType chargingProfilePurpose, int stackLevel) { bool found = false; - ProfileStack *profileStacks [] = {&TxProfile, &TxDefaultProfile}; + ChargingProfilePurposeType purposes[] = {ChargingProfilePurposeType::TxProfile, ChargingProfilePurposeType::TxDefaultProfile}; + for (unsigned int p = 0; p < sizeof(purposes) / sizeof(purposes[0]); p++) { + ChargingProfilePurposeType purpose = purposes[p]; + if (chargingProfilePurpose != ChargingProfilePurposeType::UNDEFINED && chargingProfilePurpose != purpose) { + continue; + } - 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(); - } + ChargingProfile **stack = nullptr; + if (purpose == ChargingProfilePurposeType::TxProfile) { + stack = txProfile; + } else if (purpose == ChargingProfilePurposeType::TxDefaultProfile) { + stack = txDefaultProfile; + } + + for (size_t sLvl = 0; sLvl < MO_CHARGEPROFILESTACK_SIZE; sLvl++) { + if (stackLevel >= 0 && (size_t)stackLevel != sLvl) { + continue; } + + if (!stack[sLvl]) { + // no profile installed at this stack and stackLevel + continue; + } + + if (chargingProfileId >= 0 && chargingProfileId != stack[sLvl]->chargingProfileId) { + continue; + } + + // this profile matches all filter criteria + if (scService.filesystem) { + SmartChargingServiceUtils::removeProfile(scService.filesystem, evseId, purpose, sLvl); + } + delete stack[sLvl]; + stack[sLvl] = nullptr; + found = true; } } return found; } -std::unique_ptr SmartChargingConnector::getCompositeSchedule(int duration, ChargingRateUnitType_Optional unit) { - - auto& startSchedule = model.getClock().now(); +std::unique_ptr SmartChargingServiceEvse::getCompositeSchedule(int duration, ChargingRateUnitType unit) { + + int32_t nowUnixTime; + if (!clock.toUnixTime(clock.now(), nowUnixTime)) { + MO_DBG_ERR("internal error"); + return nullptr; + } + + // dry run to measure Schedule size + size_t periodsSize = 0; + + int32_t periodBegin = nowUnixTime; + int32_t measuredDuration = 0; + while (measuredDuration < duration && periodsSize < MO_ChargingScheduleMaxPeriods) { + MO_ChargeRate limit; + int32_t nextChangeSecs; + + calculateLimit(periodBegin, -1, limit, nextChangeSecs); + + periodBegin += nextChangeSecs; + measuredDuration += nextChangeSecs; + periodsSize++; + } auto schedule = std::unique_ptr(new ChargingSchedule()); + if (!schedule || !schedule->resizeChargingSchedulePeriod(periodsSize)) { + MO_DBG_ERR("OOM"); + return nullptr; + } schedule->duration = duration; - schedule->startSchedule = startSchedule; + schedule->startSchedule = nowUnixTime; schedule->chargingProfileKind = ChargingProfileKindType::Absolute; - schedule->recurrencyKind = RecurrencyKindType::NOT_SET; + schedule->recurrencyKind = RecurrencyKindType::UNDEFINED; - auto& periods = schedule->chargingSchedulePeriod; + periodBegin = nowUnixTime; + measuredDuration = 0; - Timestamp periodBegin = Timestamp(startSchedule); - Timestamp periodStop = Timestamp(startSchedule); - - while (periodBegin - startSchedule < duration && periods.size() < MO_ChargingScheduleMaxPeriods) { + for (size_t i = 0; measuredDuration < duration && i < periodsSize; i++) { //calculate limit - ChargeRate limit; - calculateLimit(periodBegin, limit, periodStop); + MO_ChargeRate limit; + int32_t nextChangeSecs; + + calculateLimit(periodBegin, -1, limit, nextChangeSecs); //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; + if (unit == ChargingRateUnitType::UNDEFINED) { + if (limit.power > limit.current) { + unit = ChargingRateUnitType::Watt; } else { - unit = ChargingRateUnitType_Optional::Amp; + unit = ChargingRateUnitType::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; - } + schedule->chargingSchedulePeriod[i]->limit = (unit == ChargingRateUnitType::Watt) ? limit.power : limit.current, + schedule->chargingSchedulePeriod[i]->numberPhases = limit.numberPhases; + schedule->chargingSchedulePeriod[i]->startPeriod = measuredDuration; + #if MO_ENABLE_V201 + schedule->chargingSchedulePeriod[i]->phaseToUse = limit.phaseToUse; + #endif //MO_ENABLE_V201 - if (unit == ChargingRateUnitType_Optional::Watt) { - schedule->chargingRateUnit = ChargingRateUnitType::Watt; - } else { - schedule->chargingRateUnit = ChargingRateUnitType::Amp; + periodBegin += nextChangeSecs; + measuredDuration += nextChangeSecs; } + schedule->chargingRateUnit = unit; + return schedule; } -size_t SmartChargingConnector::getChargingProfilesCount() { +size_t SmartChargingServiceEvse::getChargingProfilesCount() { size_t chargingProfilesCount = 0; - for (size_t i = 0; i < MO_ChargeProfileMaxStackLevel + 1; i++) { - if (TxDefaultProfile[i]) { + for (size_t i = 0; i < MO_CHARGEPROFILESTACK_SIZE; i++) { + if (txDefaultProfile[i]) { chargingProfilesCount++; } - if (TxProfile[i]) { + if (txProfile[i]) { chargingProfilesCount++; } } return chargingProfilesCount; } -SmartChargingConnector *SmartChargingService::getScConnectorById(unsigned int connectorId) { - if (connectorId == 0) { - return nullptr; +SmartChargingService::SmartChargingService(Context& context) + : MemoryManaged("v16/v201.SmartCharging.SmartChargingService"), context(context), clock(context.getClock()) { + + mo_chargeRate_init(&trackLimitOutput); +} + +SmartChargingService::~SmartChargingService() { + for (unsigned int i = 0; i < MO_NUM_EVSEID; i++) { + delete evses[i]; + evses[i] = nullptr; + } + for (size_t i = 0; i < MO_CHARGEPROFILESTACK_SIZE; i++) { + delete chargePointMaxProfile[i]; + chargePointMaxProfile[i] = nullptr; } + for (size_t i = 0; i < MO_CHARGEPROFILESTACK_SIZE; i++) { + delete chargePointTxDefaultProfile[i]; + chargePointTxDefaultProfile[i] = nullptr; + } +} - if (connectorId - 1 >= connectors.size()) { - return nullptr; +bool SmartChargingService::setup() { + + filesystem = context.getFilesystem(); + if (!filesystem) { + MO_DBG_DEBUG("volatile mode"); } - return &connectors[connectorId-1]; -} + ocppVersion = context.getOcppVersion(); + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("setup failure"); + return false; + } -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); + const char *chargingScheduleAllowedChargingRateUnit = ""; + if (powerSupported && currentSupported) { + chargingScheduleAllowedChargingRateUnit = "Current,Power"; + } else if (powerSupported) { + chargingScheduleAllowedChargingRateUnit = "Power"; + } else if (currentSupported) { + chargingScheduleAllowedChargingRateUnit = "Current"; + } + configService->declareConfiguration("ChargingScheduleAllowedChargingRateUnit", chargingScheduleAllowedChargingRateUnit, MO_CONFIGURATION_VOLATILE); + + configService->declareConfiguration("ChargeProfileMaxStackLevel", MO_ChargeProfileMaxStackLevel, MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); + configService->declareConfiguration("ChargingScheduleAllowedChargingRateUnit", "", MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); + configService->declareConfiguration("ChargingScheduleMaxPeriods", MO_ChargingScheduleMaxPeriods, MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); + configService->declareConfiguration("MaxChargingProfilesInstalled", MO_MaxChargingProfilesInstalled, MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); + if (phases3to1Supported) { + configService->declareConfiguration("ConnectorSwitch3to1PhaseSupported", phases3to1Supported, MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); + } } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + + auto varService = context.getModel201().getVariableService(); + if (!varService) { + MO_DBG_ERR("initialization error"); + return false; + } - 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); + chargingProfileEntriesInt201 = varService->declareVariable("SmartChargingCtrlr", "ChargingProfileEntries", 0, Mutability::ReadOnly, false); + if (!chargingProfileEntriesInt201) { + MO_DBG_ERR("setup failure"); + return false; + } - 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);}); + varService->declareVariable("SmartChargingCtrlr", "SmartChargingEnabled", true, Mutability::ReadOnly, false); + if (phaseSwitchingSupported) { + varService->declareVariable("SmartChargingCtrlr", "ACPhaseSwitchingSupported", phaseSwitchingSupported, Mutability::ReadOnly, false); + } + varService->declareVariable("SmartChargingCtrlr", "ChargingProfileMaxStackLevel", MO_ChargeProfileMaxStackLevel, Mutability::ReadOnly, false); + + const char *chargingScheduleChargingRateUnit = ""; + if (powerSupported && currentSupported) { + chargingScheduleChargingRateUnit = "A,W"; + } else if (powerSupported) { + chargingScheduleChargingRateUnit = "W"; + } else if (currentSupported) { + chargingScheduleChargingRateUnit = "A"; + } + varService->declareVariable("SmartChargingCtrlr", "ChargingScheduleChargingRateUnit", chargingScheduleChargingRateUnit, Mutability::ReadOnly, false); - loadProfiles(); + varService->declareVariable("SmartChargingCtrlr", "PeriodsPerSchedule", MO_ChargingScheduleMaxPeriods, Mutability::ReadOnly, false); + if (phases3to1Supported) { + varService->declareVariable("SmartChargingCtrlr", "Phases3to1", phases3to1Supported, Mutability::ReadOnly, false); + } + varService->declareVariable("SmartChargingCtrlr", "SmartChargingAvailable", true, Mutability::ReadOnly, false); + } + #endif //MO_ENABLE_V201 + + context.getMessageService().registerOperation("ClearChargingProfile", [] (Context& context) -> Operation* { + return new ClearChargingProfile(*context.getModelCommon().getSmartChargingService(), MO_OCPP_V16);}); + context.getMessageService().registerOperation("GetCompositeSchedule", [] (Context& context) -> Operation* { + return new GetCompositeSchedule(context, *context.getModelCommon().getSmartChargingService());}); + context.getMessageService().registerOperation("SetChargingProfile", [] (Context& context) -> Operation* { + return new SetChargingProfile(context, *context.getModelCommon().getSmartChargingService());}); + + numEvseId = context.getModelCommon().getNumEvseId(); + for (unsigned int i = 1; i < numEvseId; i++) { //evseId 0 won't be populated + if (!getEvse(i)) { + MO_DBG_ERR("OOM"); + return false; + } + } + + if (!loadProfiles()) { + MO_DBG_ERR("loadProfiles"); + return false; + } + + return true; } -SmartChargingService::~SmartChargingService() { +SmartChargingServiceEvse *SmartChargingService::getEvse(unsigned int evseId) { + if (evseId == 0 || evseId >= numEvseId) { + MO_DBG_ERR("evseId out of bound"); + return nullptr; + } + + if (!evses[evseId]) { + evses[evseId] = new SmartChargingServiceEvse(context, *this, evseId); + if (!evses[evseId]) { + MO_DBG_ERR("OOM"); + return nullptr; + } + } + return evses[evseId]; } -ChargingProfile *SmartChargingService::updateProfiles(unsigned int connectorId, std::unique_ptr chargingProfile){ +bool SmartChargingService::updateProfile(unsigned int evseId, std::unique_ptr chargingProfile, bool updateFile){ - if ((connectorId > 0 && !getScConnectorById(connectorId)) || !chargingProfile) { + if (evseId >= numEvseId || !chargingProfile) { MO_DBG_ERR("invalid args"); - return nullptr; + return false; } if (MO_DBG_LEVEL >= MO_DL_VERBOSE) { MO_DBG_VERBOSE("Charging Profile internal model:"); - chargingProfile->printProfile(); + chargingProfile->printProfile(clock); } - int stackLevel = chargingProfile->getStackLevel(); - if (stackLevel< 0 || stackLevel >= MO_ChargeProfileMaxStackLevel + 1) { + int stackLevel = chargingProfile->stackLevel; + if (stackLevel < 0 || stackLevel >= MO_CHARGEPROFILESTACK_SIZE) { MO_DBG_ERR("input validation failed"); - return nullptr; + return false; } - 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(); - } + size_t chargingProfilesCount = getChargingProfilesCount(); if (chargingProfilesCount >= MO_MaxChargingProfilesInstalled) { MO_DBG_WARN("number of maximum charging profiles exceeded"); - return nullptr; + return false; } - ChargingProfile *res = nullptr; + bool success = false; - switch (chargingProfile->getChargingProfilePurpose()) { - case (ChargingProfilePurposeType::ChargePointMaxProfile): - if (connectorId != 0) { + switch (chargingProfile->chargingProfilePurpose) { + case ChargingProfilePurposeType::ChargePointMaxProfile: + if (evseId != 0) { MO_DBG_WARN("invalid charging profile"); - return nullptr; + return false; } - ChargePointMaxProfile[stackLevel] = std::move(chargingProfile); - res = ChargePointMaxProfile[stackLevel].get(); + if (updateFile && filesystem) { + if (!SmartChargingServiceUtils::storeProfile(filesystem, clock, ocppVersion, evseId, chargingProfile.get())) { + MO_DBG_ERR("fs error"); + return false; + } + } + chargePointMaxProfile[stackLevel] = chargingProfile.release(); + success = true; break; - case (ChargingProfilePurposeType::TxDefaultProfile): - if (connectorId == 0) { - ChargePointTxDefaultProfile[stackLevel] = std::move(chargingProfile); - res = ChargePointTxDefaultProfile[stackLevel].get(); + case ChargingProfilePurposeType::TxDefaultProfile: + if (evseId == 0) { + if (updateFile && filesystem) { + if (!SmartChargingServiceUtils::storeProfile(filesystem, clock, ocppVersion, evseId, chargingProfile.get())) { + MO_DBG_ERR("fs error"); + return false; + } + } + chargePointTxDefaultProfile[stackLevel] = chargingProfile.release(); + success = true; } else { - res = getScConnectorById(connectorId)->updateProfiles(std::move(chargingProfile)); + success = evses[evseId]->updateProfile(std::move(chargingProfile), updateFile); } break; - case (ChargingProfilePurposeType::TxProfile): - if (connectorId == 0) { + case ChargingProfilePurposeType::TxProfile: + if (evseId == 0) { MO_DBG_WARN("invalid charging profile"); - return nullptr; + return false; } else { - res = getScConnectorById(connectorId)->updateProfiles(std::move(chargingProfile)); + success = evses[evseId]->updateProfile(std::move(chargingProfile), updateFile); } break; + default: + MO_DBG_ERR("input validation failed"); + return false; } + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + chargingProfileEntriesInt201->setInt((int)getChargingProfilesCount()); + } + #endif //MO_ENABLE_V201 + /** * 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(); + if (success) { + nextChange = clock.now(); + for (unsigned int i = 1; i < numEvseId; i++) { + evses[i]->notifyProfilesUpdated(); } } - return res; + return success; } bool SmartChargingService::loadProfiles() { @@ -426,66 +733,111 @@ bool SmartChargingService::loadProfiles() { ChargingProfilePurposeType purposes[] = {ChargingProfilePurposeType::ChargePointMaxProfile, ChargingProfilePurposeType::TxDefaultProfile, ChargingProfilePurposeType::TxProfile}; - char fn [MO_MAX_PATH_SIZE] = {'\0'}; + char fname [MO_MAX_PATH_SIZE] = {'\0'}; + + for (unsigned int p = 0; p < sizeof(purposes) / sizeof(purposes[0]); p++) { + ChargingProfilePurposeType purpose = purposes[p]; - for (auto purpose : purposes) { - for (unsigned int cId = 0; cId < numConnectors; cId++) { + for (unsigned int cId = 0; cId < numEvseId; 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)) { + if (!SmartChargingServiceUtils::printProfileFileName(fname, sizeof(fname), 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. + char path [MO_MAX_PATH_SIZE]; + if (!FilesystemUtils::printPath(filesystem, path, sizeof(path), fname)) { + MO_DBG_ERR("fname error: %s", fname); + return false; } - auto profileDoc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); - if (!profileDoc) { - success = false; - MO_DBG_ERR("profile corrupt: %s, remove", fn); - filesystem->remove(fn); + JsonDoc doc (0); + auto ret = FilesystemUtils::loadJson(filesystem, fname, doc, getMemoryTag()); + switch (ret) { + case FilesystemUtils::LoadStatus::Success: + break; //continue loading JSON + case FilesystemUtils::LoadStatus::FileNotFound: + break; //There is not a profile on the stack iStack with stacklevel iLevel. Normal case, just continue. + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + return false; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: { + MO_DBG_ERR("profile corrupt: %s, remove", fname); + success = false; + filesystem->remove(path); + } + break; + } + + if (ret != FilesystemUtils::LoadStatus::Success) { continue; } - JsonObject profileJson = profileDoc->as(); - auto chargingProfile = loadChargingProfile(profileJson); + auto chargingProfile = std::unique_ptr(new ChargingProfile()); + if (!chargingProfile) { + MO_DBG_ERR("OOM"); + return false; + } - bool valid = false; - if (chargingProfile) { - valid = updateProfiles(cId, std::move(chargingProfile)); + bool valid = chargingProfile->parseJson(clock, ocppVersion, doc.as()); + if (valid) { + valid = updateProfile(cId, std::move(chargingProfile), false); } + if (!valid) { success = false; - MO_DBG_ERR("profile corrupt: %s, remove", fn); - filesystem->remove(fn); + MO_DBG_ERR("profile corrupt: %s, remove", fname); + filesystem->remove(path); } } } } + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + chargingProfileEntriesInt201->setInt((int)getChargingProfilesCount()); + } + #endif //MO_ENABLE_V201 + return success; } +size_t SmartChargingService::getChargingProfilesCount() { + size_t chargingProfilesCount = 0; + for (size_t i = 0; i < MO_CHARGEPROFILESTACK_SIZE; i++) { + if (chargePointTxDefaultProfile[i]) { + chargingProfilesCount++; + } + if (chargePointMaxProfile[i]) { + chargingProfilesCount++; + } + } + for (unsigned int i = 1; i < numEvseId; i++) { + chargingProfilesCount += evses[i]->getChargingProfilesCount(); + } + + return chargingProfilesCount; +} + /* * 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){ - +void SmartChargingService::calculateLimit(int32_t unixTime, MO_ChargeRate& limitOut, int32_t& nextChangeSecsOut){ + //initialize output parameters with the default values - limitOut = ChargeRate(); - validToOut = MAX_TIME; + mo_chargeRate_init(&limitOut); + nextChangeSecsOut = 365 * 24 * 3600; //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 (chargePointMaxProfile[i]) { + MO_ChargeRate crOut; + bool defined = chargePointMaxProfile[i]->calculateLimit(unixTime, -1, crOut, nextChangeSecsOut); if (defined) { limitOut = crOut; break; @@ -496,88 +848,94 @@ void SmartChargingService::calculateLimit(const Timestamp &t, ChargeRate& limitO void SmartChargingService::loop(){ - for (size_t i = 0; i < connectors.size(); i++) { - connectors[i].loop(); + for (unsigned int i = 1; i < numEvseId; i++) { + evses[i]->loop(); } /** * check if to call onLimitChange */ - auto& tnow = context.getModel().getClock().now(); + auto& tnow = clock.now(); - if (tnow >= nextChange){ - - ChargeRate limit; - nextChange = MAX_TIME; //reset nextChange to default value and refresh it + int32_t dtNextChange; + if (!clock.delta(tnow, nextChange, dtNextChange)) { + dtNextChange = 0; + } + + if (dtNextChange >= 0) { + + int32_t unixTime = -1; + if (!clock.toUnixTime(tnow, unixTime)) { + unixTime = 0; //undefined + } - calculateLimit(tnow, limit, nextChange); + MO_ChargeRate limit; + int32_t nextChangeSecs; + + calculateLimit(unixTime, limit, nextChangeSecs); + + //reset nextChange with nextChangeSecs + nextChange = tnow; + clock.add(nextChange, nextChangeSecs); #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); + char timestamp1[MO_JSONDATE_SIZE] = {'\0'}; + if (!clock.toJsonString(tnow, timestamp1, sizeof(timestamp1))) { + timestamp1[0] = '\0'; + } + char timestamp2[MO_JSONDATE_SIZE] = {'\0'}; + if (!clock.toJsonString(nextChange, timestamp2, sizeof(timestamp2))) { + timestamp1[0] = '\0'; + } 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); + limit.power, + limit.current, + limit.numberPhases); } #endif - if (trackLimitOutput != limit) { + if (!mo_chargeRate_equals(&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); + limitOutput(limit, 0, limitOutputUserData); trackLimitOutput = limit; } } } } -void SmartChargingService::setSmartChargingOutput(unsigned int connectorId, std::function limitOutput) { - if ((connectorId > 0 && !getScConnectorById(connectorId))) { +bool SmartChargingService::setSmartChargingOutput(unsigned int evseId, void (*limitOutput)(MO_ChargeRate limit, unsigned int evseId, void *userData), bool powerSupported, bool currentSupported, bool phases3to1Supported, bool phaseSwitchingSupported, void *userData) { + if (evseId >= numEvseId || (evseId > 0 && !getEvse(evseId))) { MO_DBG_ERR("invalid args"); - return; + return false; } - if (connectorId == 0) { + if (evseId == 0) { if (this->limitOutput) { MO_DBG_WARN("replacing existing SmartChargingOutput"); } this->limitOutput = limitOutput; + this->limitOutputUserData = userData; } else { - getScConnectorById(connectorId)->setSmartChargingOutput(limitOutput); + getEvse(evseId)->setSmartChargingOutput(limitOutput, powerSupported, currentSupported, phases3to1Supported, phaseSwitchingSupported, userData); } -} -void SmartChargingService::updateAllowedChargingRateUnit(bool powerSupported, bool currentSupported) { - if ((powerSupported != this->powerSupported) || (currentSupported != this->currentSupported)) { + this->powerSupported |= powerSupported; + this->currentSupported |= currentSupported; + this->phases3to1Supported |= phases3to1Supported; + #if MO_ENABLE_V201 + this->phaseSwitchingSupported |= phaseSwitchingSupported; + #endif //MO_ENABLE_V201 - 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; - } + return true; } -bool SmartChargingService::setChargingProfile(unsigned int connectorId, std::unique_ptr chargingProfile) { +bool SmartChargingService::setChargingProfile(unsigned int evseId, std::unique_ptr chargingProfile) { - if ((connectorId > 0 && !getScConnectorById(connectorId)) || !chargingProfile) { + if (evseId >= numEvseId || (evseId > 0 && !evses[evseId]) || !chargingProfile) { MO_DBG_ERR("invalid args"); return false; } @@ -588,183 +946,231 @@ bool SmartChargingService::setChargingProfile(unsigned int connectorId, std::uni return false; } - int chargingProfileId = chargingProfile->getChargingProfileId(); - clearChargingProfile([chargingProfileId] (int id, int, ChargingProfilePurposeType, int) { - return id == chargingProfileId; - }); + int chargingProfileId = chargingProfile->chargingProfileId; + clearChargingProfile(chargingProfileId, -1, ChargingProfilePurposeType::UNDEFINED, -1); - bool success = false; + return updateProfile(evseId, std::move(chargingProfile), true); +} - auto profilePtr = updateProfiles(connectorId, std::move(chargingProfile)); +bool SmartChargingService::clearChargingProfile(int chargingProfileId, int evseId, ChargingProfilePurposeType chargingProfilePurpose, int stackLevel) { + bool found = false; - if (profilePtr) { - success = SmartChargingServiceUtils::storeProfile(filesystem, connectorId, profilePtr); + // Enumerate all profiles, check for filter criteria and delete - if (!success) { - clearChargingProfile([chargingProfileId] (int id, int, ChargingProfilePurposeType, int) { - return id == chargingProfileId; - }); + for (unsigned int cId = 0; cId < numEvseId; cId++) { + if (evseId >= 0 && (unsigned int)evseId != cId) { + continue; } - } - - 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); - } + if (cId > 0) { + found |= evses[cId]->clearChargingProfile(chargingProfileId, chargingProfilePurpose, stackLevel); + } else { + ChargingProfilePurposeType purposes[] = {ChargingProfilePurposeType::ChargePointMaxProfile, ChargingProfilePurposeType::TxDefaultProfile}; + for (unsigned int p = 0; p < sizeof(purposes) / sizeof(purposes[0]); p++) { + ChargingProfilePurposeType purpose = purposes[p]; + if (chargingProfilePurpose != ChargingProfilePurposeType::UNDEFINED && chargingProfilePurpose != purpose) { + continue; + } - ProfileStack *profileStacks [] = {&ChargePointMaxProfile, &ChargePointTxDefaultProfile}; + ChargingProfile **stack = nullptr; + if (purpose == ChargingProfilePurposeType::ChargePointMaxProfile) { + stack = chargePointMaxProfile; + } else if (purpose == ChargingProfilePurposeType::TxDefaultProfile) { + stack = 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)) { + for (size_t sLvl = 0; sLvl < MO_CHARGEPROFILESTACK_SIZE; sLvl++) { + if (stackLevel >= 0 && (size_t)stackLevel != sLvl) { + continue; + } + + if (!stack[sLvl]) { + // no profile installed at this stack and stackLevel + continue; + } + + if (chargingProfileId >= 0 && chargingProfileId != stack[sLvl]->chargingProfileId) { + continue; + } + + // this profile matches all filter criteria + if (filesystem) { + SmartChargingServiceUtils::removeProfile(filesystem, cId, purpose, sLvl); + } + delete stack[sLvl]; + stack[sLvl] = nullptr; found = true; - SmartChargingServiceUtils::removeProfile(filesystem, 0, profile->getChargingProfilePurpose(), iLevel); - profile.reset(); } } } } + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + chargingProfileEntriesInt201->setInt((int)getChargingProfilesCount()); + } + #endif //MO_ENABLE_V201 + /** * 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(); + nextChange = clock.now(); + for (unsigned int i = 1; i < numEvseId; i++) { + evses[i]->notifyProfilesUpdated(); } return found; } -std::unique_ptr SmartChargingService::getCompositeSchedule(unsigned int connectorId, int duration, ChargingRateUnitType_Optional unit) { +std::unique_ptr SmartChargingService::getCompositeSchedule(unsigned int evseId, int duration, ChargingRateUnitType unit) { - if (connectorId > 0 && !getScConnectorById(connectorId)) { + if (evseId >= numEvseId || (evseId > 0 && !evses[evseId])) { MO_DBG_ERR("invalid args"); return nullptr; } - - if (unit == ChargingRateUnitType_Optional::None) { + + if (unit == ChargingRateUnitType::UNDEFINED) { if (powerSupported && !currentSupported) { - unit = ChargingRateUnitType_Optional::Watt; + unit = ChargingRateUnitType::Watt; } else if (!powerSupported && currentSupported) { - unit = ChargingRateUnitType_Optional::Amp; + unit = ChargingRateUnitType::Amp; } } - if (connectorId > 0) { - return getScConnectorById(connectorId)->getCompositeSchedule(duration, unit); + if (evseId > 0) { + return evses[evseId]->getCompositeSchedule(duration, unit); + } + + int32_t nowUnixTime; + if (!clock.toUnixTime(clock.now(), nowUnixTime)) { + MO_DBG_ERR("internal error"); + return nullptr; + } + + // dry run to measure Schedule size + size_t periodsSize = 0; + + int32_t periodBegin = nowUnixTime; + int32_t measuredDuration = 0; + while (measuredDuration < duration && periodsSize < MO_ChargingScheduleMaxPeriods) { + MO_ChargeRate limit; + int32_t nextChangeSecs; + + calculateLimit(periodBegin, limit, nextChangeSecs); + + periodBegin += nextChangeSecs; + measuredDuration += nextChangeSecs; + periodsSize++; } - - auto& startSchedule = context.getModel().getClock().now(); auto schedule = std::unique_ptr(new ChargingSchedule()); + if (!schedule || !schedule->resizeChargingSchedulePeriod(periodsSize)) { + MO_DBG_ERR("OOM"); + return nullptr; + } schedule->duration = duration; - schedule->startSchedule = startSchedule; + schedule->startSchedule = nowUnixTime; schedule->chargingProfileKind = ChargingProfileKindType::Absolute; - schedule->recurrencyKind = RecurrencyKindType::NOT_SET; - - auto& periods = schedule->chargingSchedulePeriod; + schedule->recurrencyKind = RecurrencyKindType::UNDEFINED; - Timestamp periodBegin = Timestamp(startSchedule); - Timestamp periodStop = Timestamp(startSchedule); + periodBegin = nowUnixTime; + measuredDuration = 0; - while (periodBegin - startSchedule < duration && periods.size() < MO_ChargingScheduleMaxPeriods) { + for (size_t i = 0; measuredDuration < duration && i < periodsSize; i++) { //calculate limit - ChargeRate limit; - calculateLimit(periodBegin, limit, periodStop); + MO_ChargeRate limit; + int32_t nextChangeSecs; + + calculateLimit(periodBegin, limit, nextChangeSecs); //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; + if (unit == ChargingRateUnitType::UNDEFINED) { + if (limit.power > limit.current) { + unit = ChargingRateUnitType::Watt; } else { - unit = ChargingRateUnitType_Optional::Amp; + unit = ChargingRateUnitType::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; + schedule->chargingSchedulePeriod[i]->limit = (unit == ChargingRateUnitType::Watt) ? limit.power : limit.current; + schedule->chargingSchedulePeriod[i]->numberPhases = limit.numberPhases; + schedule->chargingSchedulePeriod[i]->startPeriod = measuredDuration; + #if MO_ENABLE_V201 + schedule->chargingSchedulePeriod[i]->phaseToUse = limit.phaseToUse; + #endif //MO_ENABLE_V201 - periodBegin = periodStop; + periodBegin += nextChangeSecs; + measuredDuration += nextChangeSecs; } - if (unit == ChargingRateUnitType_Optional::Watt) { - schedule->chargingRateUnit = ChargingRateUnitType::Watt; - } else { - schedule->chargingRateUnit = ChargingRateUnitType::Amp; - } + schedule->chargingRateUnit = unit; return schedule; } -bool SmartChargingServiceUtils::printProfileFileName(char *out, size_t bufsize, unsigned int connectorId, ChargingProfilePurposeType purpose, unsigned int stackLevel) { - int pret = 0; +bool SmartChargingServiceUtils::printProfileFileName(char *out, size_t bufsize, unsigned int evseId, ChargingProfilePurposeType purpose, unsigned int stackLevel) { + int ret = 0; switch (purpose) { - case (ChargingProfilePurposeType::ChargePointMaxProfile): - pret = snprintf(out, bufsize, MO_FILENAME_PREFIX "sc-cm-%u.jsn", stackLevel); + case ChargingProfilePurposeType::ChargePointMaxProfile: + ret = snprintf(out, bufsize, "sc-cm-%.*u-%.*u.jsn", MO_NUM_EVSEID_DIGITS, 0, MO_ChargeProfileMaxStackLevel_digits, stackLevel); break; - case (ChargingProfilePurposeType::TxDefaultProfile): - pret = snprintf(out, bufsize, MO_FILENAME_PREFIX "sc-td-%u-%u.jsn", connectorId, stackLevel); + case ChargingProfilePurposeType::TxDefaultProfile: + ret = snprintf(out, bufsize, "sc-td-%.*u-%.*u.jsn", MO_NUM_EVSEID_DIGITS, evseId, MO_ChargeProfileMaxStackLevel_digits, stackLevel); break; - case (ChargingProfilePurposeType::TxProfile): - pret = snprintf(out, bufsize, MO_FILENAME_PREFIX "sc-tx-%u-%u.jsn", connectorId, stackLevel); + case ChargingProfilePurposeType::TxProfile: + ret = snprintf(out, bufsize, "sc-tx-%.*u-%.*u.jsn", MO_NUM_EVSEID_DIGITS, evseId, MO_ChargeProfileMaxStackLevel_digits, stackLevel); break; + case ChargingProfilePurposeType::UNDEFINED: + MO_DBG_ERR("invalid args"); + return false; } - if (pret < 0 || (size_t) pret >= bufsize) { - MO_DBG_ERR("fn error: %i", pret); + if (ret < 0 || (size_t) ret >= bufsize) { + MO_DBG_ERR("fname error: %i", ret); 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 - } +bool SmartChargingServiceUtils::storeProfile(MO_FilesystemAdapter *filesystem, Clock& clock, int ocppVersion, unsigned int evseId, ChargingProfile *chargingProfile) { - auto chargingProfileJson = initJsonDoc("v16.SmartCharging.ChargingProfile"); - if (!chargingProfile->toJson(chargingProfileJson)) { + auto capacity = chargingProfile->getJsonCapacity(ocppVersion); + auto chargingProfileJson = initJsonDoc("v16.SmartCharging.ChargingProfile", capacity); + if (!chargingProfile->toJson(clock, ocppVersion, chargingProfileJson.to())) { return false; } - char fn [MO_MAX_PATH_SIZE] = {'\0'}; + char fname [MO_MAX_PATH_SIZE] = {'\0'}; - if (!printProfileFileName(fn, MO_MAX_PATH_SIZE, connectorId, chargingProfile->getChargingProfilePurpose(), chargingProfile->getStackLevel())) { + if (!printProfileFileName(fname, MO_MAX_PATH_SIZE, evseId, chargingProfile->chargingProfilePurpose, chargingProfile->stackLevel)) { return false; } - return FilesystemUtils::storeJson(filesystem, fn, chargingProfileJson); + if (FilesystemUtils::storeJson(filesystem, fname, chargingProfileJson) != FilesystemUtils::StoreStatus::Success) { + MO_DBG_ERR("failed to store %s", fname); + } + + return true; } -bool SmartChargingServiceUtils::removeProfile(std::shared_ptr filesystem, unsigned int connectorId, ChargingProfilePurposeType purpose, unsigned int stackLevel) { +bool SmartChargingServiceUtils::removeProfile(MO_FilesystemAdapter *filesystem, unsigned int evseId, ChargingProfilePurposeType purpose, unsigned int stackLevel) { - if (!filesystem) { + char fname [MO_MAX_PATH_SIZE] = {'\0'}; + + if (!printProfileFileName(fname, MO_MAX_PATH_SIZE, evseId, purpose, stackLevel)) { return false; } - char fn [MO_MAX_PATH_SIZE] = {'\0'}; - - if (!printProfileFileName(fn, MO_MAX_PATH_SIZE, connectorId, purpose, stackLevel)) { + char path [MO_MAX_PATH_SIZE]; + if (!FilesystemUtils::printPath(filesystem, path, sizeof(path), fname)) { return false; } - return filesystem->remove(fn); + return filesystem->remove(path); } - +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h index 57952a72..f02adf3b 100644 --- a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h @@ -2,73 +2,78 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef SMARTCHARGINGSERVICE_H -#define SMARTCHARGINGSERVICE_H - -#include -#include - -#include +#ifndef MO_SMARTCHARGINGSERVICE_H +#define MO_SMARTCHARGINGSERVICE_H #include +#include +#include #include #include #include +#include -namespace MicroOcpp { +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING -enum class ChargingRateUnitType_Optional { - Watt, - Amp, - None -}; +#define MO_CHARGEPROFILESTACK_SIZE (MO_ChargeProfileMaxStackLevel + 1) + +namespace MicroOcpp { class Context; -class Model; -using ProfileStack = std::array, MO_ChargeProfileMaxStackLevel + 1>; +class SmartChargingService; -class SmartChargingConnector : public MemoryManaged { +#if MO_ENABLE_V201 +namespace v201 { +class Variable; +} +#endif //MO_ENABLE_V201 + +class SmartChargingServiceEvse : public MemoryManaged { private: - Model& model; - std::shared_ptr filesystem; - const unsigned int connectorId; + Context& context; + Clock& clock; + SmartChargingService& scService; + const unsigned int evseId; - ProfileStack& ChargePointMaxProfile; - ProfileStack& ChargePointTxDefaultProfile; - ProfileStack TxDefaultProfile; - ProfileStack TxProfile; + ChargingProfile *txDefaultProfile [MO_CHARGEPROFILESTACK_SIZE] = {nullptr}; + ChargingProfile *txProfile [MO_CHARGEPROFILESTACK_SIZE] = {nullptr}; - std::function limitOutput; + void (*limitOutput)(MO_ChargeRate limit, unsigned int evseId, void *userData) = nullptr; + void *limitOutputUserData = nullptr; + MO_ChargeRate trackLimitOutput; 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 trackTxStart; //time basis for relative profiles + #if MO_ENABLE_V16 + int trackTransactionId16 = -1; //transactionId + #endif + #if MO_ENABLE_V201 + char trackTransactionId201[MO_TXID_SIZE] = {'\0'}; //transactionId + #endif //MO_ENABLE_V201 - Timestamp nextChange = MIN_TIME; + Timestamp nextChange; - ChargeRate trackLimitOutput; - - void calculateLimit(const Timestamp &t, ChargeRate& limitOut, Timestamp& validToOut); + void calculateLimit(int32_t unixTime, int32_t sessionDurationSecs, MO_ChargeRate& limit, int32_t& nextChangeSecs); void trackTransaction(); public: - SmartChargingConnector(Model& model, std::shared_ptr filesystem, unsigned int connectorId, ProfileStack& ChargePointMaxProfile, ProfileStack& ChargePointTxDefaultProfile); - SmartChargingConnector(SmartChargingConnector&&) = default; - ~SmartChargingConnector(); + SmartChargingServiceEvse(Context& context, SmartChargingService& scService, unsigned int evseId); + SmartChargingServiceEvse(SmartChargingServiceEvse&&) = default; + ~SmartChargingServiceEvse(); void loop(); - void setSmartChargingOutput(std::function limitOutput); //read maximum Watt x Amps x numberPhases + void setSmartChargingOutput(void (*limitOutput)(MO_ChargeRate limit, unsigned int evseId, void *userData), bool powerSupported, bool currentSupported, bool phases3to1Supported, bool phaseSwitchingSupported, void *userData); - ChargingProfile *updateProfiles(std::unique_ptr chargingProfile); + bool updateProfile(std::unique_ptr chargingProfile, bool updateFile); void notifyProfilesUpdated(); - bool clearChargingProfile(std::function filter); + bool clearChargingProfile(int chargingProfileId, ChargingProfilePurposeType chargingProfilePurpose, int stackLevel); - std::unique_ptr getCompositeSchedule(int duration, ChargingRateUnitType_Optional unit); + std::unique_ptr getCompositeSchedule(int duration, ChargingRateUnitType unit); size_t getChargingProfilesCount(); }; @@ -76,49 +81,59 @@ class SmartChargingConnector : public MemoryManaged { 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; + Clock& clock; + MO_FilesystemAdapter *filesystem = nullptr; + SmartChargingServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; //evseId 0 won't be populated + unsigned int numEvseId = MO_NUM_EVSEID; + + ChargingProfile *chargePointMaxProfile [MO_CHARGEPROFILESTACK_SIZE] = {nullptr}; + ChargingProfile *chargePointTxDefaultProfile [MO_CHARGEPROFILESTACK_SIZE] = {nullptr}; + + void (*limitOutput)(MO_ChargeRate limit, unsigned int evseId, void *userData) = nullptr; + void *limitOutputUserData = nullptr; bool powerSupported = false; bool currentSupported = false; + bool phases3to1Supported = false; + #if MO_ENABLE_V201 + bool phaseSwitchingSupported = false; + #endif + MO_ChargeRate trackLimitOutput; + + Timestamp nextChange; + + int ocppVersion = -1; - Timestamp nextChange = MIN_TIME; + #if MO_ENABLE_V201 + v201::Variable *chargingProfileEntriesInt201 = nullptr; + #endif //MO_ENABLE_V201 - ChargingProfile *updateProfiles(unsigned int connectorId, std::unique_ptr chargingProfile); + SmartChargingServiceEvse *getEvse(unsigned int evseId); + bool updateProfile(unsigned int evseId, std::unique_ptr chargingProfile, bool updateFile); bool loadProfiles(); - void calculateLimit(const Timestamp &t, ChargeRate& limitOut, Timestamp& validToOut); - + void calculateLimit(int32_t unixTime, MO_ChargeRate& limit, int32_t& nextChangeSecs); + public: - SmartChargingService(Context& context, std::shared_ptr filesystem, unsigned int numConnectors); + SmartChargingService(Context& context); ~SmartChargingService(); - void loop(); + bool setSmartChargingOutput(unsigned int evseId, void (*limitOutput)(MO_ChargeRate limit, unsigned int evseId, void *userData), bool powerSupported, bool currentSupported, bool phases3to1Supported, bool phaseSwitchingSupported, void *userData); - 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 setup(); - bool setChargingProfile(unsigned int connectorId, std::unique_ptr chargingProfile); + void loop(); - bool clearChargingProfile(std::function filter); + bool setChargingProfile(unsigned int evseId, std::unique_ptr chargingProfile); - std::unique_ptr getCompositeSchedule(unsigned int connectorId, int duration, ChargingRateUnitType_Optional unit = ChargingRateUnitType_Optional::None); -}; + bool clearChargingProfile(int chargingProfileId, int evseId, ChargingProfilePurposeType chargingProfilePurpose, int stackLevel); -//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); -} + std::unique_ptr getCompositeSchedule(unsigned int evseId, int duration, ChargingRateUnitType unit = ChargingRateUnitType::UNDEFINED); -} //end namespace MicroOcpp + size_t getChargingProfilesCount(); + +friend class SmartChargingServiceEvse; +}; +} //namespace MicroOcpp +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING #endif diff --git a/src/MicroOcpp/Model/Transactions/Transaction.cpp b/src/MicroOcpp/Model/Transactions/Transaction.cpp index a53b68f1..5239bf3a 100644 --- a/src/MicroOcpp/Model/Transactions/Transaction.cpp +++ b/src/MicroOcpp/Model/Transactions/Transaction.cpp @@ -6,145 +6,170 @@ #include #include +#if MO_ENABLE_V16 + 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; +v16::Transaction::Transaction(unsigned int connectorId, unsigned int txNr, bool silent) : + MemoryManaged("v16.Transactions.Transaction"), + connectorId(connectorId), + txNr(txNr), + silent(silent), + meterValues(makeVector("v16.Transactions.TransactionMeterData")) { } + +v16::Transaction::~Transaction() { + for (size_t i = 0; i < meterValues.size(); i++) { + delete meterValues[i]; + } + meterValues.clear(); } -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; +void v16::Transaction::setTransactionId(int transactionId) { + this->transactionId = transactionId; + #if MO_ENABLE_V201 + if (transactionId > 0) { + snprintf(transactionIdCompat, sizeof(transactionIdCompat), "%d", transactionId); + } else { + transactionIdCompat[0] = '\0'; + } + #endif //MO_ENABLE_V201 } -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 v16::Transaction::setIdTag(const char *idTag) { + auto ret = snprintf(this->idTag, MO_IDTAG_LEN_MAX + 1, "%s", idTag); + return ret >= 0 && ret < MO_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 v16::Transaction::setParentIdTag(const char *idTag) { + auto ret = snprintf(this->parentIdTag, MO_IDTAG_LEN_MAX + 1, "%s", idTag); + return ret >= 0 && ret < MO_IDTAG_LEN_MAX + 1; } -bool Transaction::commit() { - return context.commit(this); +bool v16::Transaction::setStopIdTag(const char *idTag) { + auto ret = snprintf(stop_idTag, MO_IDTAG_LEN_MAX + 1, "%s", idTag); + return ret >= 0 && ret < MO_IDTAG_LEN_MAX + 1; } +bool v16::Transaction::setStopReason(const char *reason) { + auto ret = snprintf(stop_reason, sizeof(stop_reason) + 1, "%s", reason); + return ret >= 0 && (size_t)ret < sizeof(stop_reason) + 1; +} + +#endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 namespace MicroOcpp { -namespace Ocpp201 { +namespace v201 { -const char *serializeTransactionStoppedReason(Transaction::StoppedReason stoppedReason) { +const char *serializeTransactionStoppedReason(MO_TxStoppedReason stoppedReason) { const char *stoppedReasonCstr = nullptr; switch (stoppedReason) { - case Transaction::StoppedReason::UNDEFINED: + case MO_TxStoppedReason_UNDEFINED: // optional, okay break; - case Transaction::StoppedReason::Local: + case MO_TxStoppedReason_Local: stoppedReasonCstr = "Local"; break; - case Transaction::StoppedReason::DeAuthorized: + case MO_TxStoppedReason_DeAuthorized: stoppedReasonCstr = "DeAuthorized"; break; - case Transaction::StoppedReason::EmergencyStop: + case MO_TxStoppedReason_EmergencyStop: stoppedReasonCstr = "EmergencyStop"; break; - case Transaction::StoppedReason::EnergyLimitReached: + case MO_TxStoppedReason_EnergyLimitReached: stoppedReasonCstr = "EnergyLimitReached"; break; - case Transaction::StoppedReason::EVDisconnected: + case MO_TxStoppedReason_EVDisconnected: stoppedReasonCstr = "EVDisconnected"; break; - case Transaction::StoppedReason::GroundFault: + case MO_TxStoppedReason_GroundFault: stoppedReasonCstr = "GroundFault"; break; - case Transaction::StoppedReason::ImmediateReset: + case MO_TxStoppedReason_ImmediateReset: stoppedReasonCstr = "ImmediateReset"; break; - case Transaction::StoppedReason::LocalOutOfCredit: + case MO_TxStoppedReason_LocalOutOfCredit: stoppedReasonCstr = "LocalOutOfCredit"; break; - case Transaction::StoppedReason::MasterPass: + case MO_TxStoppedReason_MasterPass: stoppedReasonCstr = "MasterPass"; break; - case Transaction::StoppedReason::Other: + case MO_TxStoppedReason_Other: stoppedReasonCstr = "Other"; break; - case Transaction::StoppedReason::OvercurrentFault: + case MO_TxStoppedReason_OvercurrentFault: stoppedReasonCstr = "OvercurrentFault"; break; - case Transaction::StoppedReason::PowerLoss: + case MO_TxStoppedReason_PowerLoss: stoppedReasonCstr = "PowerLoss"; break; - case Transaction::StoppedReason::PowerQuality: + case MO_TxStoppedReason_PowerQuality: stoppedReasonCstr = "PowerQuality"; break; - case Transaction::StoppedReason::Reboot: + case MO_TxStoppedReason_Reboot: stoppedReasonCstr = "Reboot"; break; - case Transaction::StoppedReason::Remote: + case MO_TxStoppedReason_Remote: stoppedReasonCstr = "Remote"; break; - case Transaction::StoppedReason::SOCLimitReached: + case MO_TxStoppedReason_SOCLimitReached: stoppedReasonCstr = "SOCLimitReached"; break; - case Transaction::StoppedReason::StoppedByEV: + case MO_TxStoppedReason_StoppedByEV: stoppedReasonCstr = "StoppedByEV"; break; - case Transaction::StoppedReason::TimeLimitReached: + case MO_TxStoppedReason_TimeLimitReached: stoppedReasonCstr = "TimeLimitReached"; break; - case Transaction::StoppedReason::Timeout: + case MO_TxStoppedReason_Timeout: stoppedReasonCstr = "Timeout"; break; } return stoppedReasonCstr; } -bool deserializeTransactionStoppedReason(const char *stoppedReasonCstr, Transaction::StoppedReason& stoppedReasonOut) { +bool deserializeTransactionStoppedReason(const char *stoppedReasonCstr, MO_TxStoppedReason& stoppedReasonOut) { if (!stoppedReasonCstr || !*stoppedReasonCstr) { - stoppedReasonOut = Transaction::StoppedReason::UNDEFINED; + stoppedReasonOut = MO_TxStoppedReason_UNDEFINED; } else if (!strcmp(stoppedReasonCstr, "DeAuthorized")) { - stoppedReasonOut = Transaction::StoppedReason::DeAuthorized; + stoppedReasonOut = MO_TxStoppedReason_DeAuthorized; } else if (!strcmp(stoppedReasonCstr, "EmergencyStop")) { - stoppedReasonOut = Transaction::StoppedReason::EmergencyStop; + stoppedReasonOut = MO_TxStoppedReason_EmergencyStop; } else if (!strcmp(stoppedReasonCstr, "EnergyLimitReached")) { - stoppedReasonOut = Transaction::StoppedReason::EnergyLimitReached; + stoppedReasonOut = MO_TxStoppedReason_EnergyLimitReached; } else if (!strcmp(stoppedReasonCstr, "EVDisconnected")) { - stoppedReasonOut = Transaction::StoppedReason::EVDisconnected; + stoppedReasonOut = MO_TxStoppedReason_EVDisconnected; } else if (!strcmp(stoppedReasonCstr, "GroundFault")) { - stoppedReasonOut = Transaction::StoppedReason::GroundFault; + stoppedReasonOut = MO_TxStoppedReason_GroundFault; } else if (!strcmp(stoppedReasonCstr, "ImmediateReset")) { - stoppedReasonOut = Transaction::StoppedReason::ImmediateReset; + stoppedReasonOut = MO_TxStoppedReason_ImmediateReset; } else if (!strcmp(stoppedReasonCstr, "Local")) { - stoppedReasonOut = Transaction::StoppedReason::Local; + stoppedReasonOut = MO_TxStoppedReason_Local; } else if (!strcmp(stoppedReasonCstr, "LocalOutOfCredit")) { - stoppedReasonOut = Transaction::StoppedReason::LocalOutOfCredit; + stoppedReasonOut = MO_TxStoppedReason_LocalOutOfCredit; } else if (!strcmp(stoppedReasonCstr, "MasterPass")) { - stoppedReasonOut = Transaction::StoppedReason::MasterPass; + stoppedReasonOut = MO_TxStoppedReason_MasterPass; } else if (!strcmp(stoppedReasonCstr, "Other")) { - stoppedReasonOut = Transaction::StoppedReason::Other; + stoppedReasonOut = MO_TxStoppedReason_Other; } else if (!strcmp(stoppedReasonCstr, "OvercurrentFault")) { - stoppedReasonOut = Transaction::StoppedReason::OvercurrentFault; + stoppedReasonOut = MO_TxStoppedReason_OvercurrentFault; } else if (!strcmp(stoppedReasonCstr, "PowerLoss")) { - stoppedReasonOut = Transaction::StoppedReason::PowerLoss; + stoppedReasonOut = MO_TxStoppedReason_PowerLoss; } else if (!strcmp(stoppedReasonCstr, "PowerQuality")) { - stoppedReasonOut = Transaction::StoppedReason::PowerQuality; + stoppedReasonOut = MO_TxStoppedReason_PowerQuality; } else if (!strcmp(stoppedReasonCstr, "Reboot")) { - stoppedReasonOut = Transaction::StoppedReason::Reboot; + stoppedReasonOut = MO_TxStoppedReason_Reboot; } else if (!strcmp(stoppedReasonCstr, "Remote")) { - stoppedReasonOut = Transaction::StoppedReason::Remote; + stoppedReasonOut = MO_TxStoppedReason_Remote; } else if (!strcmp(stoppedReasonCstr, "SOCLimitReached")) { - stoppedReasonOut = Transaction::StoppedReason::SOCLimitReached; + stoppedReasonOut = MO_TxStoppedReason_SOCLimitReached; } else if (!strcmp(stoppedReasonCstr, "StoppedByEV")) { - stoppedReasonOut = Transaction::StoppedReason::StoppedByEV; + stoppedReasonOut = MO_TxStoppedReason_StoppedByEV; } else if (!strcmp(stoppedReasonCstr, "TimeLimitReached")) { - stoppedReasonOut = Transaction::StoppedReason::TimeLimitReached; + stoppedReasonOut = MO_TxStoppedReason_TimeLimitReached; } else if (!strcmp(stoppedReasonCstr, "Timeout")) { - stoppedReasonOut = Transaction::StoppedReason::Timeout; + stoppedReasonOut = MO_TxStoppedReason_Timeout; } else { MO_DBG_ERR("deserialization error"); return false; @@ -181,124 +206,124 @@ bool deserializeTransactionEventType(const char *typeCstr, TransactionEventData: return true; } -const char *serializeTransactionEventTriggerReason(TransactionEventTriggerReason triggerReason) { +const char *serializeTxEventTriggerReason(MO_TxEventTriggerReason triggerReason) { const char *triggerReasonCstr = nullptr; switch(triggerReason) { - case TransactionEventTriggerReason::UNDEFINED: + case MO_TxEventTriggerReason_UNDEFINED: break; - case TransactionEventTriggerReason::Authorized: + case MO_TxEventTriggerReason_Authorized: triggerReasonCstr = "Authorized"; break; - case TransactionEventTriggerReason::CablePluggedIn: + case MO_TxEventTriggerReason_CablePluggedIn: triggerReasonCstr = "CablePluggedIn"; break; - case TransactionEventTriggerReason::ChargingRateChanged: + case MO_TxEventTriggerReason_ChargingRateChanged: triggerReasonCstr = "ChargingRateChanged"; break; - case TransactionEventTriggerReason::ChargingStateChanged: + case MO_TxEventTriggerReason_ChargingStateChanged: triggerReasonCstr = "ChargingStateChanged"; break; - case TransactionEventTriggerReason::Deauthorized: + case MO_TxEventTriggerReason_Deauthorized: triggerReasonCstr = "Deauthorized"; break; - case TransactionEventTriggerReason::EnergyLimitReached: + case MO_TxEventTriggerReason_EnergyLimitReached: triggerReasonCstr = "EnergyLimitReached"; break; - case TransactionEventTriggerReason::EVCommunicationLost: + case MO_TxEventTriggerReason_EVCommunicationLost: triggerReasonCstr = "EVCommunicationLost"; break; - case TransactionEventTriggerReason::EVConnectTimeout: + case MO_TxEventTriggerReason_EVConnectTimeout: triggerReasonCstr = "EVConnectTimeout"; break; - case TransactionEventTriggerReason::MeterValueClock: + case MO_TxEventTriggerReason_MeterValueClock: triggerReasonCstr = "MeterValueClock"; break; - case TransactionEventTriggerReason::MeterValuePeriodic: + case MO_TxEventTriggerReason_MeterValuePeriodic: triggerReasonCstr = "MeterValuePeriodic"; break; - case TransactionEventTriggerReason::TimeLimitReached: + case MO_TxEventTriggerReason_TimeLimitReached: triggerReasonCstr = "TimeLimitReached"; break; - case TransactionEventTriggerReason::Trigger: + case MO_TxEventTriggerReason_Trigger: triggerReasonCstr = "Trigger"; break; - case TransactionEventTriggerReason::UnlockCommand: + case MO_TxEventTriggerReason_UnlockCommand: triggerReasonCstr = "UnlockCommand"; break; - case TransactionEventTriggerReason::StopAuthorized: + case MO_TxEventTriggerReason_StopAuthorized: triggerReasonCstr = "StopAuthorized"; break; - case TransactionEventTriggerReason::EVDeparted: + case MO_TxEventTriggerReason_EVDeparted: triggerReasonCstr = "EVDeparted"; break; - case TransactionEventTriggerReason::EVDetected: + case MO_TxEventTriggerReason_EVDetected: triggerReasonCstr = "EVDetected"; break; - case TransactionEventTriggerReason::RemoteStop: + case MO_TxEventTriggerReason_RemoteStop: triggerReasonCstr = "RemoteStop"; break; - case TransactionEventTriggerReason::RemoteStart: + case MO_TxEventTriggerReason_RemoteStart: triggerReasonCstr = "RemoteStart"; break; - case TransactionEventTriggerReason::AbnormalCondition: + case MO_TxEventTriggerReason_AbnormalCondition: triggerReasonCstr = "AbnormalCondition"; break; - case TransactionEventTriggerReason::SignedDataReceived: + case MO_TxEventTriggerReason_SignedDataReceived: triggerReasonCstr = "SignedDataReceived"; break; - case TransactionEventTriggerReason::ResetCommand: + case MO_TxEventTriggerReason_ResetCommand: triggerReasonCstr = "ResetCommand"; break; } return triggerReasonCstr; } -bool deserializeTransactionEventTriggerReason(const char *triggerReasonCstr, TransactionEventTriggerReason& triggerReasonOut) { +bool deserializeTxEventTriggerReason(const char *triggerReasonCstr, MO_TxEventTriggerReason& triggerReasonOut) { if (!triggerReasonCstr || !*triggerReasonCstr) { - triggerReasonOut = TransactionEventTriggerReason::UNDEFINED; + triggerReasonOut = MO_TxEventTriggerReason_UNDEFINED; } else if (!strcmp(triggerReasonCstr, "Authorized")) { - triggerReasonOut = TransactionEventTriggerReason::Authorized; + triggerReasonOut = MO_TxEventTriggerReason_Authorized; } else if (!strcmp(triggerReasonCstr, "CablePluggedIn")) { - triggerReasonOut = TransactionEventTriggerReason::CablePluggedIn; + triggerReasonOut = MO_TxEventTriggerReason_CablePluggedIn; } else if (!strcmp(triggerReasonCstr, "ChargingRateChanged")) { - triggerReasonOut = TransactionEventTriggerReason::ChargingRateChanged; + triggerReasonOut = MO_TxEventTriggerReason_ChargingRateChanged; } else if (!strcmp(triggerReasonCstr, "ChargingStateChanged")) { - triggerReasonOut = TransactionEventTriggerReason::ChargingStateChanged; + triggerReasonOut = MO_TxEventTriggerReason_ChargingStateChanged; } else if (!strcmp(triggerReasonCstr, "Deauthorized")) { - triggerReasonOut = TransactionEventTriggerReason::Deauthorized; + triggerReasonOut = MO_TxEventTriggerReason_Deauthorized; } else if (!strcmp(triggerReasonCstr, "EnergyLimitReached")) { - triggerReasonOut = TransactionEventTriggerReason::EnergyLimitReached; + triggerReasonOut = MO_TxEventTriggerReason_EnergyLimitReached; } else if (!strcmp(triggerReasonCstr, "EVCommunicationLost")) { - triggerReasonOut = TransactionEventTriggerReason::EVCommunicationLost; + triggerReasonOut = MO_TxEventTriggerReason_EVCommunicationLost; } else if (!strcmp(triggerReasonCstr, "EVConnectTimeout")) { - triggerReasonOut = TransactionEventTriggerReason::EVConnectTimeout; + triggerReasonOut = MO_TxEventTriggerReason_EVConnectTimeout; } else if (!strcmp(triggerReasonCstr, "MeterValueClock")) { - triggerReasonOut = TransactionEventTriggerReason::MeterValueClock; + triggerReasonOut = MO_TxEventTriggerReason_MeterValueClock; } else if (!strcmp(triggerReasonCstr, "MeterValuePeriodic")) { - triggerReasonOut = TransactionEventTriggerReason::MeterValuePeriodic; + triggerReasonOut = MO_TxEventTriggerReason_MeterValuePeriodic; } else if (!strcmp(triggerReasonCstr, "TimeLimitReached")) { - triggerReasonOut = TransactionEventTriggerReason::TimeLimitReached; + triggerReasonOut = MO_TxEventTriggerReason_TimeLimitReached; } else if (!strcmp(triggerReasonCstr, "Trigger")) { - triggerReasonOut = TransactionEventTriggerReason::Trigger; + triggerReasonOut = MO_TxEventTriggerReason_Trigger; } else if (!strcmp(triggerReasonCstr, "UnlockCommand")) { - triggerReasonOut = TransactionEventTriggerReason::UnlockCommand; + triggerReasonOut = MO_TxEventTriggerReason_UnlockCommand; } else if (!strcmp(triggerReasonCstr, "StopAuthorized")) { - triggerReasonOut = TransactionEventTriggerReason::StopAuthorized; + triggerReasonOut = MO_TxEventTriggerReason_StopAuthorized; } else if (!strcmp(triggerReasonCstr, "EVDeparted")) { - triggerReasonOut = TransactionEventTriggerReason::EVDeparted; + triggerReasonOut = MO_TxEventTriggerReason_EVDeparted; } else if (!strcmp(triggerReasonCstr, "EVDetected")) { - triggerReasonOut = TransactionEventTriggerReason::EVDetected; + triggerReasonOut = MO_TxEventTriggerReason_EVDetected; } else if (!strcmp(triggerReasonCstr, "RemoteStop")) { - triggerReasonOut = TransactionEventTriggerReason::RemoteStop; + triggerReasonOut = MO_TxEventTriggerReason_RemoteStop; } else if (!strcmp(triggerReasonCstr, "RemoteStart")) { - triggerReasonOut = TransactionEventTriggerReason::RemoteStart; + triggerReasonOut = MO_TxEventTriggerReason_RemoteStart; } else if (!strcmp(triggerReasonCstr, "AbnormalCondition")) { - triggerReasonOut = TransactionEventTriggerReason::AbnormalCondition; + triggerReasonOut = MO_TxEventTriggerReason_AbnormalCondition; } else if (!strcmp(triggerReasonCstr, "SignedDataReceived")) { - triggerReasonOut = TransactionEventTriggerReason::SignedDataReceived; + triggerReasonOut = MO_TxEventTriggerReason_SignedDataReceived; } else if (!strcmp(triggerReasonCstr, "ResetCommand")) { - triggerReasonOut = TransactionEventTriggerReason::ResetCommand; + triggerReasonOut = MO_TxEventTriggerReason_ResetCommand; } else { MO_DBG_ERR("deserialization error"); return false; @@ -350,185 +375,7 @@ bool deserializeTransactionEventChargingState(const char *chargingStateCstr, Tra return true; } -} //namespace Ocpp201 +} //namespace v201 } //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(); -} diff --git a/src/MicroOcpp/Model/Transactions/Transaction.h b/src/MicroOcpp/Model/Transactions/Transaction.h index 5dc3e869..ed92994e 100644 --- a/src/MicroOcpp/Model/Transactions/Transaction.h +++ b/src/MicroOcpp/Model/Transactions/Transaction.h @@ -2,68 +2,49 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef TRANSACTION_H -#define TRANSACTION_H +#ifndef MO_TRANSACTION_H +#define MO_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 #include -#include +#include +#include +#include +#include +#include +#include -#define MAX_TX_CNT 100000U //upper limit of txNr (internal usage). Must be at least 2*MO_TXRECORD_SIZE+1 +#define MO_TXNR_MAX 10000U //upper limit of txNr (internal usage). Must be at least 2*MO_TXRECORD_SIZE+1 +#define MO_TXNR_DIGITS 4 //digits needed to print MAX_INDEX-1 (=9999, i.e. 4 digits) + +#define MO_REASON_LEN_MAX 15 + +#if MO_ENABLE_V16 namespace MicroOcpp { +namespace v16 { /* * 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. + * + * See OCPP 1.6 Specification - Edition 2, sections 3.6, 4.8, 4.10 and 5.11. */ -class ConnectorTransactionStore; +class TransactionStoreEvse; class SendStatus { private: bool requested = false; bool confirmed = false; - + unsigned int opNr = 0; unsigned int attemptNr = 0; - Timestamp attemptTime = MIN_TIME; + Timestamp attemptTime; public: void setRequested() {this->requested = true;} bool isRequested() {return requested;} @@ -80,18 +61,16 @@ class SendStatus { 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'}; + char idTag [MO_IDTAG_LEN_MAX + 1] = {'\0'}; + char parentIdTag [MO_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; + Timestamp begin_timestamp; int reservationId = -1; int txProfileId = -1; @@ -100,19 +79,21 @@ class Transaction : public MemoryManaged { */ 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; + Timestamp start_timestamp; //timestamp of StartTx; can be set before actually initiating int transactionId = -1; //only valid if confirmed = true + #if MO_ENABLE_V201 + char transactionIdCompat [12] = {'\0'}; //v201 compat: provide txId as string too. Buffer dimensioned to hold max int value + #endif //MO_ENABLE_V201 + /* * Attributes of StopTransaction */ SendStatus stop_sync; - char stop_idTag [IDTAG_LEN_MAX + 1] = {'\0'}; + char stop_idTag [MO_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'}; + Timestamp stop_timestamp; + char stop_reason [MO_REASON_LEN_MAX + 1] = {'\0'}; /* * General attributes @@ -122,18 +103,20 @@ class Transaction : public MemoryManaged { bool silent = false; //silent Tx: process tx locally, without reporting to the server + Vector meterValues; + 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) {} + Transaction(unsigned int connectorId, unsigned int txNr, bool silent = false); + + ~Transaction(); /* * data assigned by OCPP server */ int getTransactionId() {return transactionId;} + #if MO_ENABLE_V201 + const char *getTransactionIdCompat() {return transactionIdCompat;} + #endif //MO_ENABLE_V201 bool isAuthorized() {return authorized;} //Authorize has been accepted bool isIdTagDeauthorized() {return deauthorized;} //StartTransaction has been rejected @@ -145,11 +128,6 @@ class Transaction : public MemoryManaged { 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 */ @@ -182,10 +160,7 @@ class Transaction : public MemoryManaged { 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;} + void setTransactionId(int transactionId); SendStatus& getStopSync() {return stop_sync;} @@ -199,9 +174,6 @@ class Transaction : public MemoryManaged { 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;} @@ -213,81 +185,32 @@ class Transaction : public MemoryManaged { void setSilent() {silent = true;} bool isSilent() {return silent;} //no data will be sent to server and server will not assign transactionId + + Vector& getTxMeterValues() {return meterValues;} }; -} // namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #if MO_ENABLE_V201 -#include -#include - -#include -#include -#include -#include +#define MO_TXID_SIZE MO_UUID_STR_SIZE #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: +class Clock; - // 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 - }; +namespace v201 { -//private: +class Transaction : public MemoryManaged { +private: + Clock& clock; +public: /* * Transaction substates. Notify server about any change when transaction is running */ @@ -312,9 +235,11 @@ class Transaction : public MemoryManaged { 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'}; + Timestamp beginTimestamp; + Timestamp startTimestamp; + char transactionId [MO_TXID_SIZE] = {'\0'}; int remoteStartId = -1; + int txProfileId = -1; //if to fill next TxEvent with optional fields bool notifyEvseId = false; @@ -326,18 +251,18 @@ class Transaction : public MemoryManaged { bool evConnectionTimeoutListen = true; - StoppedReason stoppedReason = StoppedReason::UNDEFINED; - TransactionEventTriggerReason stopTrigger = TransactionEventTriggerReason::UNDEFINED; + MO_TxStoppedReason stoppedReason = MO_TxStoppedReason_UNDEFINED; + MO_TxEventTriggerReason stopTrigger = MO_TxEventTriggerReason_UNDEFINED; std::unique_ptr stopIdToken; // if null, then stopIdToken equals idToken /* * Tx-related metering */ - Vector> sampledDataTxEnded; + Vector> sampledDataTxEnded; - unsigned long lastSampleTimeTxUpdated = 0; //0 means not charging right now - unsigned long lastSampleTimeTxEnded = 0; + Timestamp lastSampleTimeTxUpdated; + Timestamp lastSampleTimeTxEnded; /* * Attributes for internal store @@ -350,23 +275,27 @@ class Transaction : public MemoryManaged { bool silent = false; //silent Tx: process tx locally, without reporting to the server - Transaction() : + Transaction(Clock& clock) : MemoryManaged("v201.Transactions.Transaction"), - sampledDataTxEnded(makeVector>(getMemoryTag())), + clock(clock), + sampledDataTxEnded(makeVector>(getMemoryTag())), seqNos(makeVector(getMemoryTag())) { } - void addSampledDataTxEnded(std::unique_ptr mv) { + void addSampledDataTxEnded(std::unique_ptr mv) { if (sampledDataTxEnded.size() >= MO_SAMPLEDDATATXENDED_SIZE_MAX) { - int deltaMin = std::numeric_limits::max(); + int32_t 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(); + int32_t dt; + if (!clock.delta(sampledDataTxEnded[t1]->timestamp, sampledDataTxEnded[t0]->timestamp, dt)) { + dt = std::numeric_limits::max(); + } - if (delta < deltaMin) { - deltaMin = delta; + if (dt < deltaMin) { + deltaMin = dt; indexMin = t1; } } @@ -399,12 +328,10 @@ class TransactionEventData : public MemoryManaged { Idle }; -//private: Transaction *transaction; Type eventType; Timestamp timestamp; - uint16_t bootNr = 0; - TransactionEventTriggerReason triggerReason; + MO_TxEventTriggerReason triggerReason; const unsigned int seqNo; bool offline = false; int numberOfPhasesUsed = -1; @@ -417,81 +344,30 @@ class TransactionEventData : public MemoryManaged { //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; + Timestamp attemptTime; 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 *serializeTransactionStoppedReason(MO_TxStoppedReason stoppedReason); +bool deserializeTransactionStoppedReason(const char *stoppedReasonCstr, MO_TxStoppedReason& 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 *serializeTxEventTriggerReason(MO_TxEventTriggerReason triggerReason); +bool deserializeTxEventTriggerReason(const char *triggerReasonCstr, MO_TxEventTriggerReason& 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 +} //namespace v201 +} //namespace MicroOcpp +#endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Transactions/TransactionDefs.h b/src/MicroOcpp/Model/Transactions/TransactionDefs.h index 62ef657a..45d1024a 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionDefs.h +++ b/src/MicroOcpp/Model/Transactions/TransactionDefs.h @@ -7,9 +7,90 @@ #include +#ifdef __cplusplus +extern "C" { +#endif + +#if MO_ENABLE_V16 || MO_ENABLE_V201 + +//MO_TxNotification - event from MO to the main firmware to notify it about transaction state changes +typedef enum { + MO_TxNotification_UNDEFINED, + + //Authorization events + MO_TxNotification_Authorized, //success + MO_TxNotification_AuthorizationRejected, //IdTag/token not authorized + MO_TxNotification_AuthorizationTimeout, //authorization failed - offline + MO_TxNotification_ReservationConflict, //connector/evse reserved for other IdTag + + MO_TxNotification_ConnectionTimeout, //user took to long to plug vehicle after the authorization + MO_TxNotification_DeAuthorized, //server rejected StartTx/TxEvent + MO_TxNotification_RemoteStart, //authorized via RemoteStartTx/RequestStartTx + MO_TxNotification_RemoteStop, //stopped via RemoteStopTx/RequestStopTx + + //Tx lifecycle events + MO_TxNotification_StartTx, //entered running state (StartTx/TxEvent was initiated) + MO_TxNotification_StopTx, //left running state (StopTx/TxEvent was initiated) +} MO_TxNotification; + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 + #if MO_ENABLE_V201 -#define MO_TXID_LEN_MAX 36 +// TriggerReasonEnumType (3.82) +typedef enum { + MO_TxEventTriggerReason_UNDEFINED, // not part of OCPP + MO_TxEventTriggerReason_Authorized, + MO_TxEventTriggerReason_CablePluggedIn, + MO_TxEventTriggerReason_ChargingRateChanged, + MO_TxEventTriggerReason_ChargingStateChanged, + MO_TxEventTriggerReason_Deauthorized, + MO_TxEventTriggerReason_EnergyLimitReached, + MO_TxEventTriggerReason_EVCommunicationLost, + MO_TxEventTriggerReason_EVConnectTimeout, + MO_TxEventTriggerReason_MeterValueClock, + MO_TxEventTriggerReason_MeterValuePeriodic, + MO_TxEventTriggerReason_TimeLimitReached, + MO_TxEventTriggerReason_Trigger, + MO_TxEventTriggerReason_UnlockCommand, + MO_TxEventTriggerReason_StopAuthorized, + MO_TxEventTriggerReason_EVDeparted, + MO_TxEventTriggerReason_EVDetected, + MO_TxEventTriggerReason_RemoteStop, + MO_TxEventTriggerReason_RemoteStart, + MO_TxEventTriggerReason_AbnormalCondition, + MO_TxEventTriggerReason_SignedDataReceived, + MO_TxEventTriggerReason_ResetCommand +} MO_TxEventTriggerReason; + +// ReasonEnumType (3.67) +typedef enum { + MO_TxStoppedReason_UNDEFINED, // not part of OCPP + MO_TxStoppedReason_DeAuthorized, + MO_TxStoppedReason_EmergencyStop, + MO_TxStoppedReason_EnergyLimitReached, + MO_TxStoppedReason_EVDisconnected, + MO_TxStoppedReason_GroundFault, + MO_TxStoppedReason_ImmediateReset, + MO_TxStoppedReason_Local, + MO_TxStoppedReason_LocalOutOfCredit, + MO_TxStoppedReason_MasterPass, + MO_TxStoppedReason_Other, + MO_TxStoppedReason_OvercurrentFault, + MO_TxStoppedReason_PowerLoss, + MO_TxStoppedReason_PowerQuality, + MO_TxStoppedReason_Reboot, + MO_TxStoppedReason_Remote, + MO_TxStoppedReason_SOCLimitReached, + MO_TxStoppedReason_StoppedByEV, + MO_TxStoppedReason_TimeLimitReached, + MO_TxStoppedReason_Timeout +} MO_TxStoppedReason; #endif //MO_ENABLE_V201 + +#ifdef __cplusplus +} //extern "C" +#endif + #endif diff --git a/src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp b/src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp deleted file mode 100644 index e3c412f0..00000000 --- a/src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp +++ /dev/null @@ -1,280 +0,0 @@ -// 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; -} - -} diff --git a/src/MicroOcpp/Model/Transactions/TransactionDeserialize.h b/src/MicroOcpp/Model/Transactions/TransactionDeserialize.h deleted file mode 100644 index c8cfc427..00000000 --- a/src/MicroOcpp/Model/Transactions/TransactionDeserialize.h +++ /dev/null @@ -1,20 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/Transactions/TransactionService.h b/src/MicroOcpp/Model/Transactions/TransactionService.h deleted file mode 100644 index 36a0bc99..00000000 --- a/src/MicroOcpp/Model/Transactions/TransactionService.h +++ /dev/null @@ -1,144 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Model/Transactions/TransactionService16.cpp b/src/MicroOcpp/Model/Transactions/TransactionService16.cpp new file mode 100644 index 00000000..7dc80844 --- /dev/null +++ b/src/MicroOcpp/Model/Transactions/TransactionService16.cpp @@ -0,0 +1,1505 @@ +// 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 + +#if MO_ENABLE_V16 + +#ifndef MO_TX_CLEAN_ABORTED +#define MO_TX_CLEAN_ABORTED 1 +#endif + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; + +TransactionServiceEvse::TransactionServiceEvse(Context& context, TransactionService& cService, unsigned int evseId) + : MemoryManaged("v16.Transactions.TransactionServiceEvse"), context(context), clock(context.getClock()), model(context.getModel16()), cService(cService), evseId(evseId) { + +} + +TransactionServiceEvse::~TransactionServiceEvse() { + if (transaction != transactionFront) { + delete transaction; + } + transaction = nullptr; + delete transactionFront; + transactionFront = nullptr; +} + +bool TransactionServiceEvse::setup() { + + context.getMessageService().addSendQueue(this); //register at RequestQueue as Request emitter + + connection = context.getConnection(); + if (!connection) { + MO_DBG_ERR("setup failure"); + return false; + } + + meteringServiceEvse = model.getMeteringService() ? model.getMeteringService()->getEvse(evseId) : nullptr; + if (!meteringServiceEvse) { + MO_DBG_ERR("setup failure"); + return false; + } + + availServiceEvse = model.getAvailabilityService() ? model.getAvailabilityService()->getEvse(evseId) : nullptr; + if (!availServiceEvse) { + MO_DBG_ERR("setup failure"); + return false; + } + + filesystem = context.getFilesystem(); + if (!filesystem) { + MO_DBG_DEBUG("no FS access. Can enqueue only one transaction"); + } + + char txFnamePrefix [30]; + snprintf(txFnamePrefix, sizeof(txFnamePrefix), "tx-%.*u-", MO_NUM_EVSEID_DIGITS, evseId); + + if (filesystem) { + if (!FilesystemUtils::loadRingIndex(filesystem, txFnamePrefix, MO_TXNR_MAX, &txNrBegin, &txNrEnd)) { + MO_DBG_ERR("failed to load tx index"); + return false; + } + } + + MO_DBG_DEBUG("found %u transactions for connector %u. Internal range from %u to %u (exclusive)", (txNrEnd + MO_TXNR_MAX - txNrBegin) % MO_TXNR_MAX, evseId, txNrBegin, txNrEnd); + txNrFront = txNrBegin; + + if (filesystem) { + unsigned int txNrLatest = (txNrEnd + MO_TXNR_MAX - 1) % MO_TXNR_MAX; //txNr of the most recent tx on flash + auto txLoaded = new Transaction(evseId, txNrLatest); + if (!txLoaded) { + MO_DBG_ERR("OOM"); + return false; + } + + bool setupFailure = false; + + auto ret = TransactionStore::load(filesystem, context, evseId, txNrLatest, *txLoaded); + switch (ret) { + case FilesystemUtils::LoadStatus::Success: + //continue loading txMeterValues + break; + case FilesystemUtils::LoadStatus::FileNotFound: + break; + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + setupFailure = true; + break; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("tx load failure"); //leave the corrupt file on flash and wait for the queue front to reach and clean it + break; + } + + if (ret == FilesystemUtils::LoadStatus::Success) { + auto ret2 = MeterStore::load(filesystem, context, meteringServiceEvse->getMeterInputs(), evseId, txNrLatest, txLoaded->getTxMeterValues()); + switch (ret2) { + case FilesystemUtils::LoadStatus::Success: + case FilesystemUtils::LoadStatus::FileNotFound: + transaction = txLoaded; + txLoaded = nullptr; + break; + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + setupFailure = true; + break; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("tx load failure"); + MeterStore::remove(filesystem, evseId, txNrLatest); + transaction = txLoaded; + txLoaded = nullptr; + break; + } + } + + delete txLoaded; + txLoaded = nullptr; + + if (setupFailure) { + return false; + } + } + + return true; +} + +bool TransactionServiceEvse::ocppPermitsCharge() { + if (evseId == 0) { + MO_DBG_WARN("not supported for evseId == 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 && cService.freeVendActiveBool->getBool()) { + suspendDeAuthorizedIdTag = false; + } + + // check charge permission depending on TxStartPoint + if (cService.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 TransactionServiceEvse::loop() { + + if (!trackLoopExecute) { + trackLoopExecute = true; + if (connectorPluggedInput) { + freeVendTrackPlugged = connectorPluggedInput(evseId, connectorPluggedInputUserData); + } + } + + if (transaction && filesystem && ((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 = MeterStore::remove(filesystem, evseId, transaction->getTxNr()); + + if (removed) { + removed &= TransactionStore::remove(filesystem, evseId, 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", evseId, transaction->getTxNr(), removed ? "" : "failure"); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + if (transaction != transactionFront) { + delete transaction; + } + transaction = nullptr; + } + + if (transaction && transaction->isAborted()) { + MO_DBG_DEBUG("collect aborted transaction %u-%u", evseId, transaction->getTxNr()); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + if (transaction != transactionFront) { + delete transaction; + } + transaction = nullptr; + } + + if (transaction && transaction->getStopSync().isRequested()) { + MO_DBG_DEBUG("collect obsolete transaction %u-%u", evseId, transaction->getTxNr()); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + if (transaction != transactionFront) { + delete transaction; + } + transaction = nullptr; + } + + if (transaction) { //begin exclusively transaction-related operations + + if (connectorPluggedInput) { + if (transaction->isRunning() && transaction->isActive() && !connectorPluggedInput(evseId, connectorPluggedInputUserData)) { + if (cService.stopTransactionOnEVSideDisconnectBool->getBool()) { + MO_DBG_DEBUG("Stop Tx due to EV disconnect"); + transaction->setStopReason("EVDisconnected"); + transaction->setInactive(); + commitTransaction(transaction); + } + } + + int32_t runtimeSecs; + if (!clock.delta(clock.now(), transaction->getBeginTimestamp(), runtimeSecs)) { + runtimeSecs = 0; + } + + if (transaction->isActive() && + !transaction->getStartSync().isRequested() && + cService.connectionTimeOutInt->getInt() > 0 && + !connectorPluggedInput(evseId, connectorPluggedInputUserData) && + runtimeSecs >= cService.connectionTimeOutInt->getInt()) { + + MO_DBG_INFO("Session mngt: timeout"); + transaction->setInactive(); + commitTransaction(transaction); + + updateTxNotification(MO_TxNotification_ConnectionTimeout); + } + } + + if (transaction->isActive() && + transaction->isIdTagDeauthorized() && ( //transaction has been deAuthorized + !transaction->isRunning() || //if transaction hasn't started yet, always end + cService.stopTransactionOnInvalidIdBool->getBool())) { //if transaction is running, behavior depends on StopTransactionOnInvalidId + + MO_DBG_DEBUG("DeAuthorize session"); + transaction->setStopReason("DeAuthorized"); + transaction->setInactive(); + commitTransaction(transaction); + } + + /* + * Check conditions for start or stop transaction + */ + + if (!transaction->isRunning()) { + //start tx? + + if (transaction->isActive() && transaction->isAuthorized() && //tx must be authorized + (!connectorPluggedInput || connectorPluggedInput(evseId, connectorPluggedInputUserData)) && //if applicable, connector must be plugged + availServiceEvse->isOperative() && //only start tx if charger is free of error conditions + (!cService.txStartOnPowerPathClosedBool->getBool() || !evReadyInput || evReadyInput(evseId, evReadyInputUserData)) && //if applicable, postpone tx start point to PowerPathClosed + (!startTxReadyInput || startTxReadyInput(evseId, startTxReadyInputUserData))) { //if defined, user Input for allowing StartTx must be true + //start Transaction + + MO_DBG_INFO("Session mngt: trigger StartTransaction"); + + if (transaction->getMeterStart() < 0) { + auto meterStart = meteringServiceEvse->readTxEnergyMeter(MO_ReadingContext_TransactionBegin); + transaction->setMeterStart(meterStart); + } + + if (!transaction->getStartTimestamp().isDefined()) { + transaction->setStartTimestamp(clock.now()); + } + + transaction->getStartSync().setRequested(); + transaction->getStartSync().setOpNr(context.getMessageService().getNextOpNr()); + + if (transaction->isSilent()) { + MO_DBG_INFO("silent Transaction: omit StartTx"); + transaction->getStartSync().confirm(); + } else { + //normal transaction, record txMeterData + meteringServiceEvse->beginTxMeterData(transaction); + } + + commitTransaction(transaction); + + updateTxNotification(MO_TxNotification_StartTx); + + //fetchFrontRequest will create the StartTransaction and pass it to the message sender + return; + } + } else { + //stop tx? + + if (!transaction->isActive() && + (!stopTxReadyInput || stopTxReadyInput(evseId, stopTxReadyInputUserData))) { + //stop transaction + + MO_DBG_INFO("Session mngt: trigger StopTransaction"); + + if (transaction->getMeterStop() < 0) { + auto meterStop = meteringServiceEvse->readTxEnergyMeter(MO_ReadingContext_TransactionEnd); + transaction->setMeterStop(meterStop); + } + + if (!transaction->getStopTimestamp().isDefined()) { + transaction->setStopTimestamp(clock.now()); + } + + transaction->getStopSync().setRequested(); + transaction->getStopSync().setOpNr(context.getMessageService().getNextOpNr()); + + if (transaction->isSilent()) { + MO_DBG_INFO("silent Transaction: omit StopTx"); + transaction->getStopSync().confirm(); + } else { + //normal transaction, record txMeterData + meteringServiceEvse->endTxMeterData(transaction); + } + + commitTransaction(transaction); + + updateTxNotification(MO_TxNotification_StopTx); + + //fetchFrontRequest will create the StopTransaction and pass it to the message sender + return; + } + } + } //end transaction-related operations + + //handle FreeVend mode + if (cService.freeVendActiveBool->getBool() && connectorPluggedInput) { + if (!freeVendTrackPlugged && connectorPluggedInput(evseId, connectorPluggedInputUserData) && !transaction) { + const char *idTag = cService.freeVendIdTagString->getString(); + 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(evseId, connectorPluggedInputUserData); + } + + + return; +} + +bool TransactionServiceEvse::commitTransaction(Transaction *transaction) { + if (filesystem) { + auto ret = TransactionStore::store(filesystem, context, *transaction); + if (ret != FilesystemUtils::StoreStatus::Success) { + MO_DBG_ERR("tx commit failure"); + return false; + } + } + return true; +} + +Transaction *TransactionServiceEvse::allocateTransaction() { + + Transaction *tx = nullptr; + + if (!filesystem) { + //volatile mode shortcuts file restore and capacity logic + if (transactionFront) { + MO_DBG_WARN("volatile mode: cannot start tx while older is still in progress"); + return nullptr; + } + tx = new Transaction(evseId, txNrEnd); + if (!tx) { + MO_DBG_ERR("OOM"); + return nullptr; + } + txNrEnd = (txNrEnd + 1) % MO_TXNR_MAX; + MO_DBG_DEBUG("advance txNrEnd %u-%u", evseId, txNrEnd); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + return tx; + } + + //clean possible aborted tx + unsigned int txr = txNrEnd; + unsigned int txSize = (txNrEnd + MO_TXNR_MAX - txNrBegin) % MO_TXNR_MAX; + for (unsigned int i = 0; i < txSize; i++) { + txr = (txr + MO_TXNR_MAX - 1) % MO_TXNR_MAX; //decrement by 1 + + Transaction txLoaded {evseId, txr}; + + auto ret = TransactionStore::load(filesystem, context, evseId, txr, txLoaded); + if (ret == FilesystemUtils::LoadStatus::ErrOOM) { + MO_DBG_ERR("OOM"); + goto fail; + } + + bool found = (ret != FilesystemUtils::LoadStatus::Success); //possible to restore tx from this slot, i.e. exists and non-corrupt + + //check if dangling silent tx, aborted tx, or corrupted entry (tx == null) + if (!found || txLoaded.isSilent() || (txLoaded.isAborted() && MO_TX_CLEAN_ABORTED)) { + //yes, remove + bool removed = MeterStore::remove(filesystem, evseId, txr); + if (removed) { + removed &= TransactionStore::remove(filesystem, evseId, 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 + MO_TXNR_MAX - txNrBegin) % MO_TXNR_MAX; //refresh after cleaning txs + + //try to create new transaction + if (txSize < MO_TXRECORD_SIZE) { + tx = new Transaction(evseId, txNrEnd); + if (!tx) { + MO_DBG_ERR("OOM"); + goto fail; + } + } + + if (!tx) { + //could not create transaction - now, try to replace tx history entry + + unsigned int txl = txNrBegin; + txSize = (txNrEnd + MO_TXNR_MAX - txNrBegin) % MO_TXNR_MAX; + + for (unsigned int i = 0; i < txSize; i++) { + + if (tx) { + //success, finished here + break; + } + + //no transaction allocated, delete history entry to make space + + Transaction txhist {evseId, txl}; + + auto ret = TransactionStore::load(filesystem, context, evseId, txl, txhist); + if (ret == FilesystemUtils::LoadStatus::ErrOOM) { + MO_DBG_ERR("OOM"); + goto fail; + } + + bool found = (ret != FilesystemUtils::LoadStatus::Success); //possible to restore tx from this slot, i.e. exists and non-corrupt + + //oldest entry, now check if it's history and can be removed or corrupted entry + if (!found || txhist.isCompleted() || txhist.isAborted() || (txhist.isSilent() && txhist.getStopSync().isRequested())) { + //yes, remove + bool removed = MeterStore::remove(filesystem, evseId, txl); + if (removed) { + removed &= TransactionStore::remove(filesystem, evseId, txl); + } + if (removed) { + txNrBegin = (txl + 1) % MO_TXNR_MAX; + 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 = new Transaction(evseId, txNrEnd); + if (!tx) { + MO_DBG_ERR("OOM"); + goto fail; + } + } 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 %= MO_TXNR_MAX; + } + } + + if (!tx) { + //couldn't create normal transaction -> check if to start charging without real transaction + if (cService.silentOfflineTransactionsBool->getBool()) { + //run charging session without sending StartTx or StopTx to the server + tx = new Transaction(evseId, txNrEnd, true); + if (!tx) { + MO_DBG_ERR("OOM"); + goto fail; + } + MO_DBG_DEBUG("created silent transaction"); + } + } + + if (tx) { + //clean meter data which could still be here from a rolled-back transaction + if (!MeterStore::remove(filesystem, evseId, tx->getTxNr())) { + MO_DBG_ERR("memory corruption"); + } + } + + if (tx) { + txNrEnd = (txNrEnd + 1) % MO_TXNR_MAX; + MO_DBG_DEBUG("advance txNrEnd %u-%u", evseId, txNrEnd); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + } + + return tx; +fail: + delete tx; + return nullptr; +} + +bool TransactionServiceEvse::beginTransaction(const char *idTag) { + + if (transaction) { + MO_DBG_WARN("tx process still running. Please call endTransaction(...) before"); + return false; + } + + 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 + int32_t dtExpiryDate; + if (localAuth && localAuth->getExpiryDate() && clock.delta(clock.now(), *localAuth->getExpiryDate(), dtExpiryDate) && dtExpiryDate >= 0) { + 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( + evseId, + 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", evseId); + updateTxNotification(MO_TxNotification_ReservationConflict); + return false; + } else { + //parentIdTag unkown but local authorization failed in any case + MO_DBG_INFO("connector %u reserved - no local auth", evseId); + localAuthFound = false; + } + } + } + #endif //MO_ENABLE_RESERVATION + + transaction = allocateTransaction(); + + if (!transaction) { + MO_DBG_WARN("tx queue full - cannot start another tx"); + return false; + } + + if (!idTag || *idTag == '\0') { + //input string is empty + transaction->setIdTag(""); + } else { + transaction->setIdTag(idTag); + } + + if (parentIdTag) { + transaction->setParentIdTag(parentIdTag); + } + + transaction->setBeginTimestamp(clock.now()); + + //check for local preauthorization + if (localAuthFound && cService.localPreAuthorizeBool->getBool()) { + MO_DBG_DEBUG("Begin transaction process (%s), preauthorized locally", idTag != nullptr ? idTag : ""); + + if (reservationId >= 0) { + transaction->setReservationId(reservationId); + } + transaction->setAuthorized(); + + updateTxNotification(MO_TxNotification_Authorized); + } + + commitTransaction(transaction); + + auto authorize = makeRequest(context, new Authorize(model, idTag)); + authorize->setTimeout(cService.authorizationTimeoutInt->getInt() > 0 ? cService.authorizationTimeoutInt->getInt(): 20); + + if (!connection->isConnected()) { + //WebSockt unconnected. Enter offline mode immediately + authorize->setTimeout(1); + } + + //Sending Authorize, but operation could have become outdated when the response from the server arrives. Should discard + //it then. To match the server response with currently active tx object, capture tx identifiers + unsigned int txNr_capture = transaction->getTxNr(); + Timestamp beginTimestamp_capture = transaction->getBeginTimestamp(); + + authorize->setOnReceiveConf([this, txNr_capture, beginTimestamp_capture] (JsonObject response) { + + int32_t dtBeginTimestamp = 0; + if (!transaction || !context.getClock().delta(transaction->getBeginTimestamp(), beginTimestamp_capture, dtBeginTimestamp)) { + dtBeginTimestamp = 0; + } + + if (!transaction || transaction->getTxNr() != txNr_capture || dtBeginTimestamp != 0) { + MO_DBG_DEBUG("stale Authorize"); + return; + } + + JsonObject idTagInfo = response["idTagInfo"]; + + if (strcmp("Accepted", idTagInfo["status"] | "UNDEFINED")) { + //Authorization rejected, abort transaction + MO_DBG_DEBUG("Authorize rejected (%s), abort tx process", transaction->getIdTag()); + transaction->setIdTagDeauthorized(); + commitTransaction(transaction); + updateTxNotification(MO_TxNotification_AuthorizationRejected); + return; + } + + #if MO_ENABLE_RESERVATION + if (model.getReservationService()) { + auto reservation = model.getReservationService()->getReservation( + evseId, + transaction->getIdTag(), + idTagInfo["parentIdTag"] | (const char*) nullptr); + if (reservation) { + //reservation found for connector + if (reservation->matches( + transaction->getIdTag(), + idTagInfo["parentIdTag"] | (const char*) nullptr)) { + MO_DBG_INFO("connector %u matches reservationId %i", evseId, reservation->getReservationId()); + transaction->setReservationId(reservation->getReservationId()); + } else { + //reservation found for connector but does not match idTag or parentIdTag + MO_DBG_INFO("connector %u reserved - abort transaction", evseId); + transaction->setInactive(); + commitTransaction(transaction); + updateTxNotification(MO_TxNotification_ReservationConflict); + return; + } + } + } + #endif //MO_ENABLE_RESERVATION + + if (idTagInfo.containsKey("parentIdTag")) { + transaction->setParentIdTag(idTagInfo["parentIdTag"] | ""); + } + + MO_DBG_DEBUG("Authorized transaction process (%s)", transaction->getIdTag()); + transaction->setAuthorized(); + commitTransaction(transaction); + + updateTxNotification(MO_TxNotification_Authorized); + }); + + //capture local auth and reservation check in for timeout handler + authorize->setOnTimeout([this, txNr_capture, beginTimestamp_capture, + offlineBlockedAuth, + offlineBlockedResv, + localAuthFound, + reservationId] () { + + int32_t dtBeginTimestamp = 0; + if (!transaction || !context.getClock().delta(transaction->getBeginTimestamp(), beginTimestamp_capture, dtBeginTimestamp)) { + dtBeginTimestamp = 0; + } + + if (!transaction || transaction->getTxNr() != txNr_capture || dtBeginTimestamp != 0) { + MO_DBG_DEBUG("stale Authorize"); + return; + } + + if (offlineBlockedAuth) { + //local auth entry exists, but is expired -> avoid offline tx + MO_DBG_DEBUG("Abort transaction process (%s), timeout, expired local auth", transaction->getIdTag()); + transaction->setInactive(); + commitTransaction(transaction); + updateTxNotification(MO_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", evseId); + transaction->setInactive(); + commitTransaction(transaction); + updateTxNotification(MO_TxNotification_ReservationConflict); + return; + } + + if (localAuthFound && cService.localAuthorizeOfflineBool->getBool()) { + MO_DBG_DEBUG("Offline transaction process (%s), locally authorized", transaction->getIdTag()); + if (reservationId >= 0) { + transaction->setReservationId(reservationId); + } + transaction->setAuthorized(); + commitTransaction(transaction); + + updateTxNotification(MO_TxNotification_Authorized); + return; + } + + if (cService.allowOfflineTxForUnknownIdBool->getBool()) { + MO_DBG_DEBUG("Offline transaction process (%s), allow unknown ID", transaction->getIdTag()); + if (reservationId >= 0) { + transaction->setReservationId(reservationId); + } + transaction->setAuthorized(); + commitTransaction(transaction); + updateTxNotification(MO_TxNotification_Authorized); + return; + } + + MO_DBG_DEBUG("Abort transaction process (%s): timeout", transaction->getIdTag()); + transaction->setInactive(); + commitTransaction(transaction); + updateTxNotification(MO_TxNotification_AuthorizationTimeout); + return; //offline tx disabled + }); + context.getMessageService().sendRequest(std::move(authorize)); + + return transaction; +} + +bool TransactionServiceEvse::beginTransaction_authorized(const char *idTag, const char *parentIdTag) { + + if (transaction) { + MO_DBG_WARN("tx process still running. Please call endTransaction(...) before"); + return false; + } + + transaction = allocateTransaction(); + + if (!transaction) { + MO_DBG_ERR("could not allocate Tx"); + return false; + } + + if (!idTag || *idTag == '\0') { + //input string is empty + transaction->setIdTag(""); + } else { + transaction->setIdTag(idTag); + } + + if (parentIdTag) { + transaction->setParentIdTag(parentIdTag); + } + + transaction->setBeginTimestamp(clock.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(evseId, idTag, parentIdTag)) { + if (reservation->matches(idTag, parentIdTag)) { + transaction->setReservationId(reservation->getReservationId()); + } + } + } + #endif //MO_ENABLE_RESERVATION + + commitTransaction(transaction); + + return true; +} + +bool TransactionServiceEvse::endTransaction(const char *idTag, const char *reason) { + + if (!transaction || !transaction->isActive()) { + //transaction already ended / not active anymore + return false; + } + + MO_DBG_DEBUG("End session started by idTag %s", + transaction->getIdTag()); + + if (!idTag || !strcmp(idTag, transaction->getIdTag())) { + return endTransaction_authorized(idTag, reason); + } + + const char *parentIdTag = transaction->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(context, new Authorize(model, idTag)); + authorize->setTimeout(cService.authorizationTimeoutInt->getInt() > 0 ? cService.authorizationTimeoutInt->getInt(): 20); + + if (!connection->isConnected()) { + //WebSockt unconnected. Enter offline mode immediately + authorize->setTimeout(1); + } + + auto txNr_capture = transaction->getTxNr(); + auto beginTimestamp_capture = transaction->getBeginTimestamp(); + auto idTag_capture = makeString(getMemoryTag(), idTag); + auto reason_capture = makeString(getMemoryTag(), reason ? reason : ""); + authorize->setOnReceiveConf([this, txNr_capture, beginTimestamp_capture, idTag_capture, reason_capture] (JsonObject response) { + + int32_t dtBeginTimestamp = 0; + if (!transaction || !context.getClock().delta(transaction->getBeginTimestamp(), beginTimestamp_capture, dtBeginTimestamp)) { + dtBeginTimestamp = 0; + } + + if (!transaction || transaction->getTxNr() != txNr_capture || dtBeginTimestamp != 0) { + MO_DBG_DEBUG("stale Authorize"); + return; + } + + 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()); + updateTxNotification(MO_TxNotification_AuthorizationRejected); + return; + } + if (idTagInfo.containsKey("parentIdTag") && *transaction->getParentIdTag() && !strcmp(idTagInfo["parenIdTag"] | "", transaction->getParentIdTag())) + { + endTransaction_authorized(idTag_capture.c_str(), reason_capture.empty() ? (const char*)nullptr : reason_capture.c_str()); + } + }); + + authorize->setOnTimeout([this, txNr_capture, beginTimestamp_capture, idTag_capture, reason_capture] () { + //Authorization timed out, do nothing + int32_t dtBeginTimestamp = 0; + if (!transaction || !context.getClock().delta(transaction->getBeginTimestamp(), beginTimestamp_capture, dtBeginTimestamp)) { + dtBeginTimestamp = 0; + } + + if (!transaction || transaction->getTxNr() != txNr_capture || dtBeginTimestamp != 0) { + MO_DBG_DEBUG("stale Authorize"); + return; + } + + //check local OCPP whitelist + const char *parentIdTag = nullptr; //locally stored parentIdTag + + #if MO_ENABLE_LOCAL_AUTH + if (auto authService = model.getAuthorizationService()) { + auto localAuth = authService->getLocalAuthorization(idTag_capture.c_str()); + + //check authorization status + if (localAuth && localAuth->getAuthorizationStatus() != AuthorizationStatus::Accepted) { + MO_DBG_DEBUG("local auth denied (%s)", idTag_capture.c_str()); + localAuth = nullptr; + } + + //check expiry + int32_t dtExpiryDate; + if (localAuth && localAuth->getExpiryDate() && clock.delta(clock.now(), *localAuth->getExpiryDate(), dtExpiryDate) && dtExpiryDate >= 0) { + MO_DBG_DEBUG("idTag %s local auth entry expired", idTag_capture.c_str()); + localAuth = nullptr; + } + + if (localAuth) { + parentIdTag = localAuth->getParentIdTag(); + } + } + #endif //MO_ENABLE_LOCAL_AUTH + + if (parentIdTag && *parentIdTag && !strcmp(parentIdTag, transaction->getParentIdTag())) { + MO_DBG_DEBUG("IdTag locally authorized (%s)", idTag_capture.c_str()); + endTransaction_authorized(idTag_capture.c_str(), reason_capture.empty() ? (const char*)nullptr : reason_capture.c_str()); + return; + } + + MO_DBG_DEBUG("Authorization timeout (%s), continue transaction", idTag_capture.c_str()); + updateTxNotification(MO_TxNotification_AuthorizationTimeout); + }); + + context.getMessageService().sendRequest(std::move(authorize)); + return true; + } else { + MO_DBG_INFO("endTransaction: idTag doesn't match"); + return false; + } + + return false; +} + +bool TransactionServiceEvse::endTransaction_authorized(const char *idTag, const char *reason) { + + if (!transaction || !transaction->isActive()) { + //transaction already ended / not active anymore + return false; + } + + 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(); + commitTransaction(transaction); + return true; +} + +Transaction *TransactionServiceEvse::getTransaction() { + return transaction; +} + +void TransactionServiceEvse::setConnectorPluggedInput(bool (*connectorPlugged)(unsigned int, void*), void *userData) { + this->connectorPluggedInput = connectorPlugged; + this->connectorPluggedInputUserData = userData; +} + +void TransactionServiceEvse::setEvReadyInput(bool (*evReady)(unsigned int, void*), void *userData) { + this->evReadyInput = evReady; + this->evReadyInputUserData = userData; +} + +void TransactionServiceEvse::setStartTxReadyInput(bool (*startTxReady)(unsigned int, void*), void *userData) { + this->startTxReadyInput = startTxReady; + this->startTxReadyInputUserData = userData; +} + +void TransactionServiceEvse::setStopTxReadyInput(bool (*stopTxReady)(unsigned int, void*), void *userData) { + this->stopTxReadyInput = stopTxReady; + this->stopTxReadyInputUserData = userData; +} + +void TransactionServiceEvse::setTxNotificationOutput(void (*txNotificationOutput)(MO_TxNotification, unsigned int, void*), void *userData) { + this->txNotificationOutput = txNotificationOutput; + this->txNotificationOutputUserData = userData; +} + +void TransactionServiceEvse::updateTxNotification(MO_TxNotification event) { + if (txNotificationOutput) { + txNotificationOutput(event, evseId, txNotificationOutputUserData); + } +} + +unsigned int TransactionServiceEvse::getFrontRequestOpNr() { + + if (transactionFront && startTransactionInProgress) { + return transactionFront->getStartSync().getOpNr(); + } + + if (transactionFront && stopTransactionInProgress) { + return transactionFront->getStopSync().getOpNr(); + } + + /* + * Advance front transaction? + */ + unsigned int txSize = (txNrEnd + MO_TXNR_MAX - txNrFront) % MO_TXNR_MAX; + + 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", evseId, transactionFront->getTxNr()); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + if (transactionFront != transaction) { + delete transactionFront; + } + transactionFront = nullptr; + } + + for (unsigned int i = 0; i < txSize; i++) { + + if (!transactionFront) { + if (transaction && transaction->getTxNr() == txNrFront) { + //back == front + transactionFront = transaction; //share ownership + + } else if (filesystem) { + + auto txLoaded = new Transaction(evseId, txNrFront); + if (!txLoaded) { + MO_DBG_ERR("OOM"); + return NoOperation; + } + + bool hardFailure = false; + + auto ret = TransactionStore::load(filesystem, context, evseId, txNrFront, *txLoaded); + switch (ret) { + case FilesystemUtils::LoadStatus::Success: + //continue loading txMeterValues + break; + case FilesystemUtils::LoadStatus::FileNotFound: + break; + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + hardFailure = true; + break; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("tx load failure"); + break; + } + + if (ret == FilesystemUtils::LoadStatus::Success) { + auto ret2 = MeterStore::load(filesystem, context, meteringServiceEvse->getMeterInputs(), evseId, txNrFront, txLoaded->getTxMeterValues()); + switch (ret2) { + case FilesystemUtils::LoadStatus::Success: + case FilesystemUtils::LoadStatus::FileNotFound: + transactionFront = txLoaded; + txLoaded = nullptr; + break; + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + hardFailure = true; + break; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("tx load failure"); + MeterStore::remove(filesystem, evseId, txNrFront); + transactionFront = txLoaded; + txLoaded = nullptr; + break; + } + } + + delete txLoaded; + txLoaded = nullptr; + + if (hardFailure) { + return NoOperation; + } + + if (transactionFront) { + MO_DBG_VERBOSE("load front transaction %u-%u", evseId, transactionFront->getTxNr()); + } + } + } + + if (transactionFront && (transactionFront->isAborted() || transactionFront->isCompleted() || transactionFront->isSilent())) { + //advance front + MO_DBG_DEBUG("collect front transaction %u-%u", evseId, transactionFront->getTxNr()); + if (transactionFront != transaction) { + delete transactionFront; + } + transactionFront = nullptr; + txNrFront = (txNrFront + 1) % MO_TXNR_MAX; + 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 TransactionServiceEvse::fetchFrontRequest() { + + if (!connection->isConnected()) { + //offline behavior: pause sending messages and do not increment attempt counters + return nullptr; + } + + if (!clock.now().isUnixTime()) { + //before unix time is set, do not process send queue. The attempt timestamps require absolute time + return nullptr; + } + + if (transactionFront && !transactionFront->isSilent()) { + if (transactionFront->getStartSync().isRequested() && !transactionFront->getStartSync().isConfirmed()) { + //send StartTx? + + if (startTransactionInProgress) { + //ensure that only one StartTx request is being executed at the same time + return nullptr; + } + + bool cancelStartTx = false; + + //if not unix yet, convert (noop if unix time already) + auto convertTime = transactionFront->getStartTimestamp(); + if (!clock.toUnixTime(transactionFront->getStartTimestamp(), convertTime)) { + //time not set, cannot be restored anymore -> invalid tx + MO_DBG_ERR("cannot recover tx from previus run"); + + cancelStartTx = true; + } + transactionFront->setStartTimestamp(convertTime); + + if ((int)transactionFront->getStartSync().getAttemptNr() >= cService.transactionMessageAttemptsInt->getInt()) { + MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard transaction"); + + cancelStartTx = true; + } + + if (cancelStartTx) { + transactionFront->setSilent(); + transactionFront->setInactive(); + commitTransaction(transactionFront); + + //clean up possible tx records + if (filesystem) { + MeterStore::remove(filesystem, evseId, transactionFront->getTxNr()); + } + //next getFrontRequestOpNr() call will collect transactionFront + return nullptr; + } + + int32_t dtLastAttempt = MO_MAX_TIME; + if (transactionFront->getStartSync().getAttemptTime().isDefined() && + !clock.delta(clock.now(), transactionFront->getStartSync().getAttemptTime(), dtLastAttempt)) { + MO_DBG_ERR("internal error"); + } + + if (dtLastAttempt < (int)transactionFront->getStartSync().getAttemptNr() * std::max(0, cService.transactionMessageRetryIntervalInt->getInt())) { + return nullptr; + } + + transactionFront->getStartSync().advanceAttemptNr(); + transactionFront->getStartSync().setAttemptTime(clock.now()); + commitTransaction(transactionFront); + + auto startTxOp = new StartTransaction(context, transactionFront); + if (!startTxOp) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + auto startTx = makeRequest(context, startTxOp); + if (!startTx) { + MO_DBG_ERR("OOM"); + return nullptr; + } + startTx->setOnReceiveConf([this] (JsonObject response) { + //fetch authorization status from StartTransaction.conf() for user notification + commitTransaction(transactionFront); + startTransactionInProgress = false; + + const char* idTagInfoStatus = response["idTagInfo"]["status"] | "_Undefined"; + if (strcmp(idTagInfoStatus, "Accepted")) { + if (transactionFront == transaction) { //check if the front transaction is the currently active transaciton + updateTxNotification(MO_TxNotification_DeAuthorized); + } + } + }); + startTx->setOnAbort([this] () { + //shortcut to the attemptNr check above. Relevant if other operations block the queue while this StartTx is timing out + + startTransactionInProgress = false; + + if ((int)transactionFront->getStartSync().getAttemptNr() >= cService.transactionMessageAttemptsInt->getInt()) { + MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard transaction"); + + transactionFront->setSilent(); + transactionFront->setInactive(); + commitTransaction(transactionFront); + + //clean up possible tx records + if (filesystem) { + MeterStore::remove(filesystem, evseId, transactionFront->getTxNr()); + } + //next getFrontRequestOpNr() call will collect transactionFront + } + }); + + startTransactionInProgress = true; + + return startTx; + } + + if (transactionFront->getStopSync().isRequested() && !transactionFront->getStopSync().isConfirmed()) { + //send StopTx? + + if (stopTransactionInProgress) { + //ensure that only one TransactionEvent request is being executed at the same time + return nullptr; + } + + if ((int)transactionFront->getStopSync().getAttemptNr() >= cService.transactionMessageAttemptsInt->getInt()) { + MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard transaction"); + + transactionFront->setSilent(); + + //clean up possible tx records + if (filesystem) { + MeterStore::remove(filesystem, evseId, transactionFront->getTxNr()); + } + //next getFrontRequestOpNr() call will collect transactionFront + return nullptr; + } + + bool fallbackStopTime = false; + + //if not unix yet, convert (noop if unix time already) + auto convertTime = transactionFront->getStopTimestamp(); + if (!clock.toUnixTime(transactionFront->getStopTimestamp(), convertTime)) { + //fall back to start timestamp + MO_DBG_ERR("cannot recover tx from previus run"); + fallbackStopTime = true; + } + transactionFront->setStopTimestamp(convertTime); + + int32_t dt; + if (clock.delta(transactionFront->getStopTimestamp(), transactionFront->getStartTimestamp(), dt) && + dt < 0) { + MO_DBG_ERR("StopTx time is before StartTx time"); + fallbackStopTime = true; + } + + if (fallbackStopTime) { + transactionFront->setStopTimestamp(transactionFront->getStartTimestamp()); + auto timestamp = transactionFront->getStopTimestamp(); + clock.add(timestamp, 1); //set 1 second after start timestamp + transactionFront->setStopTimestamp(timestamp); + } + + //check timestamps in tx meter values + auto& txMeterValues = transactionFront->getTxMeterValues(); + for (size_t i = 0; i < txMeterValues.size(); i++) { + auto mv = txMeterValues[i]; + + char dummy [MO_JSONDATE_SIZE]; + if (!context.getClock().toJsonString(mv->timestamp, dummy, sizeof(dummy))) { + //try to fix timestamp + if (mv->readingContext == MO_ReadingContext_TransactionBegin) { + mv->timestamp = transaction->getStartTimestamp(); + } else if (mv->readingContext == MO_ReadingContext_TransactionEnd) { + mv->timestamp = transaction->getStopTimestamp(); + } else { + mv->timestamp = transaction->getStartTimestamp(); + clock.add(mv->timestamp, 1 + (int)i); + } + } + } + + int32_t dtLastAttempt = MO_MAX_TIME; + if (transactionFront->getStopSync().getAttemptTime().isDefined() && + !clock.delta(clock.now(), transactionFront->getStopSync().getAttemptTime(), dtLastAttempt)) { + MO_DBG_ERR("internal error"); + } + + if (dtLastAttempt < (int)transactionFront->getStopSync().getAttemptNr() * std::max(0, cService.transactionMessageRetryIntervalInt->getInt())) { + return nullptr; + } + + transactionFront->getStopSync().advanceAttemptNr(); + transactionFront->getStopSync().setAttemptTime(clock.now()); + commitTransaction(transactionFront); + + auto stopTxOp = new StopTransaction(context, transactionFront); + if (!stopTxOp) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + auto stopTx = makeRequest(context, stopTxOp); + if (!stopTx) { + MO_DBG_ERR("OOM"); + return nullptr; + } + stopTx->setOnReceiveConf([this] (JsonObject response) { + commitTransaction(transactionFront); + stopTransactionInProgress = false; + }); + stopTx->setOnAbort([this] () { + stopTransactionInProgress = false; + + //shortcut to the attemptNr check above. Relevant if other operations block the queue while this StopTx is timing out + if ((int)transactionFront->getStopSync().getAttemptNr() >= cService.transactionMessageAttemptsInt->getInt()) { + MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard transaction"); + + transactionFront->setSilent(); + transactionFront->setInactive(); + commitTransaction(transactionFront); + + //clean up possible tx records + if (filesystem) { + MeterStore::remove(filesystem, evseId, transactionFront->getTxNr()); + } + //next getFrontRequestOpNr() call will collect transactionFront + } + }); + + stopTransactionInProgress = true; + + return stopTx; + } + } + + return nullptr; +} + +unsigned int TransactionServiceEvse::getTxNrBeginHistory() { + return txNrBegin; +} + +unsigned int TransactionServiceEvse::getTxNrFront() { + return txNrFront; +} + +unsigned int TransactionServiceEvse::getTxNrEnd() { + return txNrEnd; +} + +Transaction *TransactionServiceEvse::getTransactionFront() { + return transactionFront; +} + +TransactionService::TransactionService(Context& context) : + MemoryManaged("v16.Transactions.TransactionService"), context(context) { + +} + +TransactionService::~TransactionService() { + for (unsigned int i = 0; i < MO_NUM_EVSEID; i++) { + delete evses[i]; + evses[i] = nullptr; + } +} + +bool TransactionService::setup() { + + auto configService = context.getModel16().getConfigurationService(); + if (!configService) { + MO_DBG_ERR("setup failure"); + return false; + } + + numEvseId = context.getModel16().getNumEvseId(); + + configService->declareConfiguration("NumberOfConnectors", numEvseId >= 1 ? numEvseId - 1 : 0, MO_CONFIGURATION_VOLATILE, Mutability::ReadOnly); + + connectionTimeOutInt = configService->declareConfiguration("ConnectionTimeOut", 30); + configService->registerValidator("ConnectionTimeOut", VALIDATE_UNSIGNED_INT); + configService->registerValidator("MinimumStatusDuration", VALIDATE_UNSIGNED_INT); + stopTransactionOnInvalidIdBool = configService->declareConfiguration("StopTransactionOnInvalidId", true); + stopTransactionOnEVSideDisconnectBool = configService->declareConfiguration("StopTransactionOnEVSideDisconnect", true); + localPreAuthorizeBool = configService->declareConfiguration("LocalPreAuthorize", false); + localAuthorizeOfflineBool = configService->declareConfiguration("LocalAuthorizeOffline", true); + allowOfflineTxForUnknownIdBool = configService->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 = configService->declareConfiguration(MO_CONFIG_EXT_PREFIX "SilentOfflineTransactions", false); + + //how long the EVSE tries the Authorize request before it enters offline mode + authorizationTimeoutInt = configService->declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", 20); + configService->registerValidator(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", VALIDATE_UNSIGNED_INT); + + //FreeVend mode + freeVendActiveBool = configService->declareConfiguration(MO_CONFIG_EXT_PREFIX "FreeVendActive", false); + freeVendIdTagString = configService->declareConfiguration(MO_CONFIG_EXT_PREFIX "FreeVendIdTag", ""); + + txStartOnPowerPathClosedBool = configService->declareConfiguration(MO_CONFIG_EXT_PREFIX "TxStartOnPowerPathClosed", false); + + transactionMessageAttemptsInt = configService->declareConfiguration("TransactionMessageAttempts", 3); + configService->registerValidator("TransactionMessageAttempts", VALIDATE_UNSIGNED_INT); + transactionMessageRetryIntervalInt = configService->declareConfiguration("TransactionMessageRetryInterval", 60); + configService->registerValidator("TransactionMessageRetryInterval", VALIDATE_UNSIGNED_INT); + + if (!connectionTimeOutInt || + !stopTransactionOnInvalidIdBool || + !stopTransactionOnEVSideDisconnectBool || + !localPreAuthorizeBool || + !localAuthorizeOfflineBool || + !allowOfflineTxForUnknownIdBool || + !silentOfflineTransactionsBool || + !authorizationTimeoutInt || + !freeVendActiveBool || + !freeVendIdTagString || + !txStartOnPowerPathClosedBool || + !transactionMessageAttemptsInt || + !transactionMessageRetryIntervalInt) { + + MO_DBG_ERR("declareConfiguration failed"); + return false; + } + + context.getMessageService().registerOperation("ClearCache", [] (Context& context) -> Operation* { + return new ClearCache(context.getFilesystem());}); + + #if MO_ENABLE_MOCK_SERVER + context.getMessageService().registerOperation("Authorize", nullptr, Authorize::writeMockConf); + context.getMessageService().registerOperation("StartTransaction", nullptr, StartTransaction::writeMockConf); + context.getMessageService().registerOperation("StopTransaction", nullptr, StopTransaction::writeMockConf); + #endif //MO_ENABLE_MOCK_SERVER + + for (unsigned int i = 0; i < numEvseId; i++) { + if (!getEvse(i) || !getEvse(i)->setup()) { + MO_DBG_ERR("connector init failure"); + return false; + } + } + + return true; +} + +void TransactionService::loop() { + for (unsigned int i = 0; i < numEvseId; i++) { + evses[i]->loop(); + } +} + +TransactionServiceEvse* TransactionService::getEvse(unsigned int evseId) { + if (evseId >= numEvseId) { + MO_DBG_ERR("evseId out of bound"); + return nullptr; + } + + if (!evses[evseId]) { + evses[evseId] = new TransactionServiceEvse(context, *this, evseId); + if (!evses[evseId]) { + MO_DBG_ERR("OOM"); + return nullptr; + } + } + + return evses[evseId]; +} + +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Model/Transactions/TransactionService16.h b/src/MicroOcpp/Model/Transactions/TransactionService16.h new file mode 100644 index 00000000..68b17672 --- /dev/null +++ b/src/MicroOcpp/Model/Transactions/TransactionService16.h @@ -0,0 +1,176 @@ +// 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 + +#if MO_ENABLE_V16 + +#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 Clock; +class Connection; +class Operation; + +namespace v16 { + +class Model; +class Configuration; +class MeteringServiceEvse; +class TransactionService; +class AvailabilityServiceEvse; + +class TransactionServiceEvse : public RequestQueue, public MemoryManaged { +private: + Context& context; + Clock& clock; + Model& model; + TransactionService& cService; + const unsigned int evseId; + + Connection *connection = nullptr; + MO_FilesystemAdapter *filesystem = nullptr; + MeteringServiceEvse *meteringServiceEvse = nullptr; + AvailabilityServiceEvse *availServiceEvse = nullptr; + + Transaction *transaction = nullptr; //shares ownership with transactionFront + + bool (*connectorPluggedInput)(unsigned int evseId, void *userData) = nullptr; + void *connectorPluggedInputUserData = nullptr; + bool (*evReadyInput)(unsigned int evseId, void *userData) = nullptr; + void *evReadyInputUserData = nullptr; + + bool (*startTxReadyInput)(unsigned int evseId, void *userData) = nullptr; //the StartTx request will be delayed while this Input is false + void *startTxReadyInputUserData = nullptr; + bool (*stopTxReadyInput)(unsigned int evseId, void *userData) = nullptr; //the StopTx request will be delayed while this Input is false + void *stopTxReadyInputUserData = nullptr; + + void (*txNotificationOutput)(MO_TxNotification, unsigned int evseId, void *userData) = nullptr; + void *txNotificationOutputUserData = nullptr; + + bool freeVendTrackPlugged = false; + + 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 + + Transaction *transactionFront = nullptr; //shares ownership with transaction + bool startTransactionInProgress = false; + bool stopTransactionInProgress = false; + + bool commitTransaction(Transaction *transaction); +public: + TransactionServiceEvse(Context& context, TransactionService& conService, unsigned int evseId); + virtual ~TransactionServiceEvse(); + + bool setup(); + + /* + * beginTransaction begins the transaction process which eventually leads to a StartTransaction + * request in the normal case. + * + * Returns true if the transaction process begins successfully + * Returns false if no transaction process begins due to this call (e.g. other transaction still running) + */ + bool beginTransaction(const char *idTag); + bool 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 + */ + bool endTransaction(const char *idTag = nullptr, const char *reason = nullptr); + bool endTransaction_authorized(const char *idTag = nullptr, const char *reason = nullptr); + + Transaction *getTransaction(); + + Transaction *allocateTransaction(); + + void setConnectorPluggedInput(bool (*connectorPlugged)(unsigned int, void*), void *userData); + void setEvReadyInput(bool (*evReady)(unsigned int, void*), void *userData); + void setEvseReadyInput(bool (*evseReady)(unsigned int, void*), void *userData); + + void loop(); + + bool ocppPermitsCharge(); + + void setStartTxReadyInput(bool (*startTxReady)(unsigned int, void*), void *userData); + void setStopTxReadyInput(bool (*stopTxReady)(unsigned int, void*), void *userData); + + void setTxNotificationOutput(void (*txNotificationOutput)(MO_TxNotification, unsigned int, void*), void *userData); + void updateTxNotification(MO_TxNotification event); + + unsigned int getFrontRequestOpNr() override; + std::unique_ptr fetchFrontRequest() override; + + 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 + + Transaction *getTransactionFront(); +}; + +class TransactionService : public MemoryManaged { +private: + Context& context; + + TransactionServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; + unsigned int numEvseId = MO_NUM_EVSEID; + + Configuration *connectionTimeOutInt = nullptr; //in seconds + Configuration *stopTransactionOnInvalidIdBool = nullptr; + Configuration *stopTransactionOnEVSideDisconnectBool = nullptr; + Configuration *localPreAuthorizeBool = nullptr; + Configuration *localAuthorizeOfflineBool = nullptr; + Configuration *allowOfflineTxForUnknownIdBool = nullptr; + + Configuration *silentOfflineTransactionsBool = nullptr; + Configuration *authorizationTimeoutInt = nullptr; //in seconds + Configuration *freeVendActiveBool = nullptr; + Configuration *freeVendIdTagString = nullptr; + + Configuration *txStartOnPowerPathClosedBool = nullptr; // this postpones the tx start point to when evReadyInput becomes true + + Configuration *transactionMessageAttemptsInt = nullptr; + Configuration *transactionMessageRetryIntervalInt = nullptr; +public: + TransactionService(Context& context); + + ~TransactionService(); + + bool setup(); + + void loop(); + + TransactionServiceEvse *getEvse(unsigned int evseId); + +friend class TransactionServiceEvse; +}; + +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 +#endif diff --git a/src/MicroOcpp/Model/Transactions/TransactionService.cpp b/src/MicroOcpp/Model/Transactions/TransactionService201.cpp similarity index 59% rename from src/MicroOcpp/Model/Transactions/TransactionService.cpp rename to src/MicroOcpp/Model/Transactions/TransactionService201.cpp index d0e58e1f..6537b851 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionService.cpp +++ b/src/MicroOcpp/Model/Transactions/TransactionService201.cpp @@ -2,19 +2,15 @@ // 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 @@ -23,66 +19,84 @@ #include #include +#if MO_ENABLE_V201 + #ifndef MO_TX_CLEAN_ABORTED #define MO_TX_CLEAN_ABORTED 1 #endif using namespace MicroOcpp; -using namespace MicroOcpp::Ocpp201; +using namespace MicroOcpp::v201; -TransactionService::Evse::Evse(Context& context, TransactionService& txService, Ocpp201::TransactionStoreEvse& txStore, unsigned int evseId) : +TransactionServiceEvse::TransactionServiceEvse(Context& context, TransactionService& txService, TransactionStoreEvse& txStore, unsigned int evseId) : MemoryManaged("v201.Transactions.TransactionServiceEvse"), context(context), + clock(context.getClock()), 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); +TransactionServiceEvse::~TransactionServiceEvse() { - 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 TransactionServiceEvse::setup() { + context.getMessageService().addSendQueue(this); //register at RequestQueue as Request emitter + + auto meteringService = context.getModel201().getMeteringService(); + meteringEvse = meteringService ? meteringService->getEvse(evseId) : nullptr; + if (!meteringEvse) { + MO_DBG_ERR("depends on MeteringService"); + return false; + } + + if (txService.filesystem) { + + char fnPrefix [MO_MAX_PATH_SIZE]; + snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%u-", evseId); + + if (!FilesystemUtils::loadRingIndex(txService.filesystem, fnPrefix, MO_TXNR_MAX, &txNrBegin, &txNrEnd)) { + MO_DBG_ERR("failure to init tx index"); + return false; + } + + txNrFront = txNrBegin; + MO_DBG_DEBUG("found %u transactions for evseId %u. Internal range from %u to %u (exclusive)", (txNrEnd + MO_TXNR_MAX - txNrBegin) % MO_TXNR_MAX, evseId, txNrBegin, txNrEnd); + + unsigned int txNrLatest = (txNrEnd + MO_TXNR_MAX - 1) % MO_TXNR_MAX; //txNr of the most recent tx on flash + transaction = txStore.loadTransaction(txNrLatest); //returns nullptr if txNrLatest does not exist on flash + } + + return true; } -bool TransactionService::Evse::beginTransaction() { +bool TransactionServiceEvse::beginTransaction() { if (transaction) { MO_DBG_ERR("transaction still running"); return false; } - std::unique_ptr tx; + std::unique_ptr tx; - char txId [sizeof(Ocpp201::Transaction::transactionId)]; + char txId [sizeof(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; + if (!UuidUtils::generateUUID(context.getRngCb(), txId, sizeof(txId))) { + MO_DBG_ERR("UUID failure"); + return false; } //clean possible aborted tx unsigned int txr = txNrEnd; - unsigned int txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; + unsigned int txSize = (txNrEnd + MO_TXNR_MAX - txNrBegin) % MO_TXNR_MAX; for (unsigned int i = 0; i < txSize; i++) { - txr = (txr + MAX_TX_CNT - 1) % MAX_TX_CNT; //decrement by 1 + txr = (txr + MO_TXNR_MAX - 1) % MO_TXNR_MAX; //decrement by 1 - std::unique_ptr intermediateTx; + std::unique_ptr intermediateTx; - Ocpp201::Transaction *txhist = nullptr; + Transaction *txhist = nullptr; if (transaction && transaction->txNr == txr) { txhist = transaction.get(); } else if (txFront && txFront->txNr == txr) { @@ -111,10 +125,10 @@ bool TransactionService::Evse::beginTransaction() { } } - txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; //refresh after cleaning txs + txSize = (txNrEnd + MO_TXNR_MAX - txNrBegin) % MO_TXNR_MAX; //refresh after cleaning txs //try to create new transaction - if (txSize < MO_TXRECORD_SIZE) { + if (txSize < MO_TXRECORD_SIZE_V201) { tx = txStore.createTransaction(txNrEnd, txId); } @@ -122,7 +136,7 @@ bool TransactionService::Evse::beginTransaction() { //could not create transaction - now, try to replace tx history entry unsigned int txl = txNrBegin; - txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; + txSize = (txNrEnd + MO_TXNR_MAX - txNrBegin) % MO_TXNR_MAX; for (unsigned int i = 0; i < txSize; i++) { @@ -132,9 +146,9 @@ bool TransactionService::Evse::beginTransaction() { } //no transaction allocated, delete history entry to make space - std::unique_ptr intermediateTx; + std::unique_ptr intermediateTx; - Ocpp201::Transaction *txhist = nullptr; + Transaction *txhist = nullptr; if (transaction && transaction->txNr == txl) { txhist = transaction.get(); } else if (txFront && txFront->txNr == txl) { @@ -149,7 +163,7 @@ bool TransactionService::Evse::beginTransaction() { //yes, remove if (txStore.remove(txl)) { - txNrBegin = (txl + 1) % MAX_TX_CNT; + txNrBegin = (txl + 1) % MO_TXNR_MAX; if (txNrFront == txl) { txNrFront = txNrBegin; } @@ -168,13 +182,13 @@ bool TransactionService::Evse::beginTransaction() { } txl++; - txl %= MAX_TX_CNT; + txl %= MO_TXNR_MAX; } } if (!tx) { //couldn't create normal transaction -> check if to start charging without real transaction - if (txService.silentOfflineTransactionsBool && txService.silentOfflineTransactionsBool->getBool()) { + if (txService.silentOfflineTransactionsBool->getBool()) { //try to handle charging session without sending StartTx or StopTx to the server tx = txStore.createTransaction(txNrEnd, txId); @@ -190,7 +204,7 @@ bool TransactionService::Evse::beginTransaction() { return false; } - tx->beginTimestamp = context.getModel().getClock().now(); + tx->beginTimestamp = clock.now(); if (!txStore.commit(tx.get())) { MO_DBG_ERR("fs error"); @@ -199,14 +213,14 @@ bool TransactionService::Evse::beginTransaction() { transaction = std::move(tx); - txNrEnd = (txNrEnd + 1) % MAX_TX_CNT; + txNrEnd = (txNrEnd + 1) % MO_TXNR_MAX; 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) { +bool TransactionServiceEvse::endTransaction(MO_TxStoppedReason stoppedReason = MO_TxStoppedReason_Other, MO_TxEventTriggerReason stopTrigger = MO_TxEventTriggerReason_AbnormalCondition) { if (!transaction || !transaction->active) { //transaction already ended / not active anymore @@ -224,7 +238,24 @@ bool TransactionService::Evse::endTransaction(Ocpp201::Transaction::StoppedReaso return true; } -void TransactionService::Evse::loop() { +TransactionEventData::ChargingState TransactionServiceEvse::getChargingState() { + auto res = TransactionEventData::ChargingState::Idle; + if (connectorPluggedInput && !connectorPluggedInput(evseId, connectorPluggedInputUserData)) { + res = TransactionEventData::ChargingState::Idle; + } else if (!transaction || !transaction->isAuthorizationActive || !transaction->isAuthorized || + (evseReadyInput && !evseReadyInput(evseId, evseReadyInputUserData))) { + res = TransactionEventData::ChargingState::EVConnected; + } else if (evReadyInput && !evReadyInput(evseId, evReadyInputUserData)) { + res = TransactionEventData::ChargingState::SuspendedEV; + } else if (ocppPermitsCharge()) { + res = TransactionEventData::ChargingState::Charging; + } else { + res = TransactionEventData::ChargingState::SuspendedEVSE; + } + return res; +} + +void TransactionServiceEvse::loop() { if (transaction && !transaction->active && !transaction->started) { MO_DBG_DEBUG("collect aborted transaction %u-%s", evseId, transaction->transactionId); @@ -247,22 +278,27 @@ void TransactionService::Evse::loop() { // tx-related behavior if (transaction) { if (connectorPluggedInput) { - if (connectorPluggedInput()) { + if (connectorPluggedInput(evseId, connectorPluggedInputUserData)) { // if cable has been plugged at least once, EVConnectionTimeout will never get triggered transaction->evConnectionTimeoutListen = false; } - if (transaction->active && + int32_t dtTxBegin; + if (!clock.delta(clock.now(), transaction->beginTimestamp, dtTxBegin)) { + dtTxBegin = txService.evConnectionTimeOutInt->getInt(); + } + + if (transaction->active && !transaction->started && transaction->evConnectionTimeoutListen && - transaction->beginTimestamp > MIN_TIME && - txService.evConnectionTimeOutInt && txService.evConnectionTimeOutInt->getInt() > 0 && - !connectorPluggedInput() && - context.getModel().getClock().now() - transaction->beginTimestamp >= txService.evConnectionTimeOutInt->getInt()) { + transaction->beginTimestamp.isDefined() && + txService.evConnectionTimeOutInt->getInt() > 0 && + !connectorPluggedInput(evseId, connectorPluggedInputUserData) && + dtTxBegin >= txService.evConnectionTimeOutInt->getInt()) { MO_DBG_INFO("Session mngt: timeout"); - endTransaction(Ocpp201::Transaction::StoppedReason::Timeout, TransactionEventTriggerReason::EVConnectTimeout); + endTransaction(MO_TxStoppedReason_Timeout, MO_TxEventTriggerReason_EVConnectTimeout); - updateTxNotification(TxNotification_ConnectionTimeout); + updateTxNotification(MO_TxNotification_ConnectionTimeout); } if (transaction->active && @@ -270,9 +306,9 @@ void TransactionService::Evse::loop() { !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); + endTransaction(MO_TxStoppedReason_DeAuthorized, MO_TxEventTriggerReason_Deauthorized); } } } @@ -284,8 +320,8 @@ void TransactionService::Evse::loop() { { // stop tx? - TransactionEventTriggerReason triggerReason = TransactionEventTriggerReason::UNDEFINED; - Ocpp201::Transaction::StoppedReason stoppedReason = Ocpp201::Transaction::StoppedReason::UNDEFINED; + MO_TxEventTriggerReason triggerReason = MO_TxEventTriggerReason_UNDEFINED; + MO_TxStoppedReason stoppedReason = MO_TxStoppedReason_UNDEFINED; if (transaction && !transaction->active) { // tx ended via endTransaction @@ -294,37 +330,37 @@ void TransactionService::Evse::loop() { stoppedReason = transaction->stoppedReason; } else if ((txService.isTxStopPoint(TxStartStopPoint::EVConnected) || txService.isTxStopPoint(TxStartStopPoint::PowerPathClosed)) && - connectorPluggedInput && !connectorPluggedInput() && + connectorPluggedInput && !connectorPluggedInput(evseId, connectorPluggedInputUserData) && (txService.stopTxOnEVSideDisconnectBool->getBool() || !transaction || !transaction->started)) { txStopCondition = true; - triggerReason = TransactionEventTriggerReason::EVCommunicationLost; - stoppedReason = Ocpp201::Transaction::StoppedReason::EVDisconnected; + triggerReason = MO_TxEventTriggerReason_EVCommunicationLost; + stoppedReason = MO_TxStoppedReason_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; + triggerReason = MO_TxEventTriggerReason_StopAuthorized; + stoppedReason = MO_TxStoppedReason_Local; } else if (txService.isTxStopPoint(TxStartStopPoint::EnergyTransfer) && - evReadyInput && !evReadyInput()) { + evReadyInput && !evReadyInput(evseId, evReadyInputUserData)) { txStopCondition = true; - triggerReason = TransactionEventTriggerReason::ChargingStateChanged; - stoppedReason = Ocpp201::Transaction::StoppedReason::StoppedByEV; + triggerReason = MO_TxEventTriggerReason_ChargingStateChanged; + stoppedReason = MO_TxStoppedReason_StoppedByEV; } else if (txService.isTxStopPoint(TxStartStopPoint::EnergyTransfer) && (evReadyInput || evseReadyInput) && // at least one of the two defined - !(evReadyInput && evReadyInput()) && - !(evseReadyInput && evseReadyInput())) { + !(evReadyInput && evReadyInput(evseId, evReadyInputUserData)) && + !(evseReadyInput && evseReadyInput(evseId, evseReadyInputUserData))) { txStopCondition = true; - triggerReason = TransactionEventTriggerReason::ChargingStateChanged; - stoppedReason = Ocpp201::Transaction::StoppedReason::Other; + triggerReason = MO_TxEventTriggerReason_ChargingStateChanged; + stoppedReason = MO_TxStoppedReason_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; + triggerReason = MO_TxEventTriggerReason_Deauthorized; + stoppedReason = MO_TxStoppedReason_DeAuthorized; } if (txStopCondition && @@ -336,7 +372,7 @@ void TransactionService::Evse::loop() { if (transaction && transaction->started && !transaction->stopped && !transaction->active && - (!stopTxReadyInput || stopTxReadyInput())) { + (!stopTxReadyInput || stopTxReadyInput(evseId, stopTxReadyInputUserData))) { // yes, stop running tx txEvent = txStore.createTransactionEvent(*transaction); @@ -351,48 +387,50 @@ void TransactionService::Evse::loop() { txEvent->eventType = TransactionEventData::Type::Ended; txEvent->triggerReason = triggerReason; } - } - + } + if (!txStopCondition) { // start tx? bool txStartCondition = false; - TransactionEventTriggerReason triggerReason = TransactionEventTriggerReason::UNDEFINED; + MO_TxEventTriggerReason triggerReason = MO_TxEventTriggerReason_UNDEFINED; // tx should be started? if (txService.isTxStartPoint(TxStartStopPoint::PowerPathClosed) && - (!connectorPluggedInput || connectorPluggedInput()) && + (!connectorPluggedInput || connectorPluggedInput(evseId, connectorPluggedInputUserData)) && transaction && transaction->isAuthorizationActive && transaction->isAuthorized) { txStartCondition = true; if (transaction->remoteStartId >= 0) { - triggerReason = TransactionEventTriggerReason::RemoteStart; + triggerReason = MO_TxEventTriggerReason_RemoteStart; + } else if (!transaction->trackAuthorized) { + triggerReason = MO_TxEventTriggerReason_Authorized; } else { - triggerReason = TransactionEventTriggerReason::CablePluggedIn; + triggerReason = MO_TxEventTriggerReason_CablePluggedIn; } } else if (txService.isTxStartPoint(TxStartStopPoint::Authorized) && transaction && transaction->isAuthorizationActive && transaction->isAuthorized) { txStartCondition = true; if (transaction->remoteStartId >= 0) { - triggerReason = TransactionEventTriggerReason::RemoteStart; + triggerReason = MO_TxEventTriggerReason_RemoteStart; } else { - triggerReason = TransactionEventTriggerReason::Authorized; + triggerReason = MO_TxEventTriggerReason_Authorized; } } else if (txService.isTxStartPoint(TxStartStopPoint::EVConnected) && - connectorPluggedInput && connectorPluggedInput()) { + connectorPluggedInput && connectorPluggedInput(evseId, connectorPluggedInputUserData)) { txStartCondition = true; - triggerReason = TransactionEventTriggerReason::CablePluggedIn; + triggerReason = MO_TxEventTriggerReason_CablePluggedIn; } else if (txService.isTxStartPoint(TxStartStopPoint::EnergyTransfer) && (evReadyInput || evseReadyInput) && // at least one of the two defined - (!evReadyInput || evReadyInput()) && - (!evseReadyInput || evseReadyInput())) { + (!evReadyInput || evReadyInput(evseId, evReadyInputUserData)) && + (!evseReadyInput || evseReadyInput(evseId, evseReadyInputUserData))) { txStartCondition = true; - triggerReason = TransactionEventTriggerReason::ChargingStateChanged; + triggerReason = MO_TxEventTriggerReason_ChargingStateChanged; } if (txStartCondition && (!transaction || (transaction->active && !transaction->started)) && - (!startTxReadyInput || startTxReadyInput())) { + (!startTxReadyInput || startTxReadyInput(evseId, startTxReadyInputUserData))) { // start tx if (!transaction) { @@ -417,38 +455,33 @@ void TransactionService::Evse::loop() { } } - 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; + 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; + int32_t dtLastSampleTimeTxUpdated; + if (!transaction->lastSampleTimeTxUpdated.isDefined() || !clock.delta(clock.getUptime(), transaction->lastSampleTimeTxUpdated, dtLastSampleTimeTxUpdated)) { + dtLastSampleTimeTxUpdated = 0; + transaction->lastSampleTimeTxUpdated = clock.getUptime(); + } + + if (txService.sampledDataTxUpdatedInterval->getInt() > 0 && dtLastSampleTimeTxUpdated >= txService.sampledDataTxUpdatedInterval->getInt()) { + transaction->lastSampleTimeTxUpdated = clock.getUptime(); + mvTxUpdated = meteringEvse->takeTxUpdatedMeterValue(); + } + + int32_t dtLastSampleTimeTxEnded; + if (!transaction->lastSampleTimeTxEnded.isDefined() || !clock.delta(clock.getUptime(), transaction->lastSampleTimeTxEnded, dtLastSampleTimeTxEnded)) { + dtLastSampleTimeTxEnded = 0; + transaction->lastSampleTimeTxEnded = clock.getUptime(); } 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; + txService.sampledDataTxEndedInterval->getInt() > 0 && + dtLastSampleTimeTxEnded >= txService.sampledDataTxEndedInterval->getInt()) { + transaction->lastSampleTimeTxEnded = clock.getUptime(); + auto mvTxEnded = meteringEvse->takeTxEndedMeterValue(MO_ReadingContext_SamplePeriodic); if (mvTxEnded) { transaction->addSampledDataTxEnded(std::move(mvTxEnded)); } @@ -460,11 +493,13 @@ void TransactionService::Evse::loop() { bool txUpdateCondition = false; - TransactionEventTriggerReason triggerReason = TransactionEventTriggerReason::UNDEFINED; + MO_TxEventTriggerReason triggerReason = MO_TxEventTriggerReason_UNDEFINED; + + auto chargingState = getChargingState(); if (chargingState != trackChargingState) { txUpdateCondition = true; - triggerReason = TransactionEventTriggerReason::ChargingStateChanged; + triggerReason = MO_TxEventTriggerReason_ChargingStateChanged; transaction->notifyChargingState = true; } trackChargingState = chargingState; @@ -473,28 +508,28 @@ void TransactionService::Evse::loop() { transaction->trackAuthorized = true; txUpdateCondition = true; if (transaction->remoteStartId >= 0) { - triggerReason = TransactionEventTriggerReason::RemoteStart; + triggerReason = MO_TxEventTriggerReason_RemoteStart; } else { - triggerReason = TransactionEventTriggerReason::Authorized; + triggerReason = MO_TxEventTriggerReason_Authorized; } - } else if (connectorPluggedInput && connectorPluggedInput() && !transaction->trackEvConnected) { + } else if (connectorPluggedInput && connectorPluggedInput(evseId, connectorPluggedInputUserData) && !transaction->trackEvConnected) { transaction->trackEvConnected = true; txUpdateCondition = true; - triggerReason = TransactionEventTriggerReason::CablePluggedIn; - } else if (connectorPluggedInput && !connectorPluggedInput() && transaction->trackEvConnected) { + triggerReason = MO_TxEventTriggerReason_CablePluggedIn; + } else if (connectorPluggedInput && !connectorPluggedInput(evseId, connectorPluggedInputUserData) && transaction->trackEvConnected) { transaction->trackEvConnected = false; txUpdateCondition = true; - triggerReason = TransactionEventTriggerReason::EVCommunicationLost; + triggerReason = MO_TxEventTriggerReason_EVCommunicationLost; } else if (!(transaction->isAuthorizationActive && transaction->isAuthorized) && transaction->trackAuthorized) { transaction->trackAuthorized = false; txUpdateCondition = true; - triggerReason = TransactionEventTriggerReason::StopAuthorized; + triggerReason = MO_TxEventTriggerReason_StopAuthorized; } else if (mvTxUpdated) { txUpdateCondition = true; - triggerReason = TransactionEventTriggerReason::MeterValuePeriodic; - } else if (evReadyInput && evReadyInput() && !transaction->trackPowerPathClosed) { + triggerReason = MO_TxEventTriggerReason_MeterValuePeriodic; + } else if (evReadyInput && evReadyInput(evseId, evReadyInputUserData) && !transaction->trackPowerPathClosed) { transaction->trackPowerPathClosed = true; - } else if (evReadyInput && !evReadyInput() && transaction->trackPowerPathClosed) { + } else if (evReadyInput && !evReadyInput(evseId, evReadyInputUserData) && transaction->trackPowerPathClosed) { transaction->trackPowerPathClosed = false; } @@ -513,9 +548,9 @@ void TransactionService::Evse::loop() { } if (txEvent) { - txEvent->timestamp = context.getModel().getClock().now(); + txEvent->timestamp = clock.now(); if (transaction->notifyChargingState) { - txEvent->chargingState = chargingState; + txEvent->chargingState = getChargingState(); transaction->notifyChargingState = false; } if (transaction->notifyEvseId) { @@ -527,26 +562,22 @@ void TransactionService::Evse::loop() { 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; + auto mvTxStarted = meteringEvse->takeTxStartedMeterValue(); if (mvTxStarted) { txEvent->meterValue.push_back(std::move(mvTxStarted)); } - auto mvTxEnded = meteringEvse ? meteringEvse->takeTxEndedMeterValue(ReadingContext_TransactionBegin) : nullptr; + auto mvTxEnded = meteringEvse->takeTxEndedMeterValue(MO_ReadingContext_TransactionBegin); if (mvTxEnded) { transaction->addSampledDataTxEnded(std::move(mvTxEnded)); } - transaction->lastSampleTimeTxEnded = mocpp_tick_ms(); - transaction->lastSampleTimeTxUpdated = mocpp_tick_ms(); + transaction->lastSampleTimeTxEnded = clock.getUptime(); + transaction->lastSampleTimeTxUpdated = clock.getUptime(); } 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; + auto mvTxEnded = meteringEvse->takeTxEndedMeterValue(MO_ReadingContext_TransactionEnd); if (mvTxEnded) { transaction->addSampledDataTxEnded(std::move(mvTxEnded)); } - transaction->lastSampleTimeTxEnded = mocpp_tick_ms(); + transaction->lastSampleTimeTxEnded = clock.getUptime(); } if (mvTxUpdated) { txEvent->meterValue.push_back(std::move(mvTxUpdated)); @@ -564,13 +595,14 @@ void TransactionService::Evse::loop() { if (txEvent) { if (txEvent->eventType == TransactionEventData::Type::Started) { transaction->started = true; + transaction->startTimestamp = txEvent->timestamp; } else if (txEvent->eventType == TransactionEventData::Type::Ended) { transaction->stopped = true; } } if (txEvent) { - txEvent->opNr = context.getRequestQueue().getNextOpNr(); + txEvent->opNr = context.getMessageService().getNextOpNr(); MO_DBG_DEBUG("enqueueing new txEvent at opNr %u", txEvent->opNr); } @@ -580,9 +612,9 @@ void TransactionService::Evse::loop() { if (txEvent) { if (txEvent->eventType == TransactionEventData::Type::Started) { - updateTxNotification(TxNotification_StartTx); + updateTxNotification(MO_TxNotification_StartTx); } else if (txEvent->eventType == TransactionEventData::Type::Ended) { - updateTxNotification(TxNotification_StartTx); + updateTxNotification(MO_TxNotification_StartTx); } } @@ -602,29 +634,43 @@ void TransactionService::Evse::loop() { } } -void TransactionService::Evse::setConnectorPluggedInput(std::function connectorPlugged) { +void TransactionServiceEvse::setConnectorPluggedInput(bool (*connectorPlugged)(unsigned int, void*), void *userData) { this->connectorPluggedInput = connectorPlugged; + this->connectorPluggedInputUserData = userData; } -void TransactionService::Evse::setEvReadyInput(std::function evRequestsEnergy) { - this->evReadyInput = evRequestsEnergy; +void TransactionServiceEvse::setEvReadyInput(bool (*evReady)(unsigned int, void*), void *userData) { + this->evReadyInput = evReady; + this->evReadyInputUserData = userData; } -void TransactionService::Evse::setEvseReadyInput(std::function connectorEnergized) { - this->evseReadyInput = connectorEnergized; +void TransactionServiceEvse::setEvseReadyInput(bool (*evseReady)(unsigned int, void*), void *userData) { + this->evseReadyInput = evseReady; + this->evseReadyInputUserData = userData; } -void TransactionService::Evse::setTxNotificationOutput(std::function txNotificationOutput) { +void TransactionServiceEvse::setStartTxReadyInput(bool (*startTxReady)(unsigned int, void*), void *userData) { + this->startTxReadyInput = startTxReady; + this->startTxReadyInputUserData = userData; +} + +void TransactionServiceEvse::setStopTxReadyInput(bool (*stopTxReady)(unsigned int, void*), void *userData) { + this->stopTxReadyInput = stopTxReady; + this->stopTxReadyInputUserData = userData; +} + +void TransactionServiceEvse::setTxNotificationOutput(void (*txNotificationOutput)(MO_TxNotification, unsigned int, void*), void *userData) { this->txNotificationOutput = txNotificationOutput; + this->txNotificationOutputUserData = userData; } -void TransactionService::Evse::updateTxNotification(TxNotification event) { +void TransactionServiceEvse::updateTxNotification(MO_TxNotification event) { if (txNotificationOutput) { - txNotificationOutput(transaction.get(), event); + txNotificationOutput(event, evseId, txNotificationOutputUserData); } } -bool TransactionService::Evse::beginAuthorization(IdToken idToken, bool validateIdToken) { +bool TransactionServiceEvse::beginAuthorization(IdToken idToken, bool validateIdToken, IdToken groupIdToken, bool commit) { MO_DBG_DEBUG("begin auth: %s", idToken.get()); if (transaction && transaction->isAuthorizationActive) { @@ -632,6 +678,10 @@ bool TransactionService::Evse::beginAuthorization(IdToken idToken, bool validate return false; } + if (*groupIdToken.get()) { + MO_DBG_WARN("groupIdToken not supported"); + } + if (!transaction) { beginTransaction(); if (!transaction) { @@ -645,10 +695,17 @@ bool TransactionService::Evse::beginAuthorization(IdToken idToken, bool validate transaction->isAuthorizationActive = true; transaction->idToken = idToken; - transaction->beginTimestamp = context.getModel().getClock().now(); + transaction->beginTimestamp = clock.now(); + + if (commit) { + if (!txStore.commit(transaction.get())) { + MO_DBG_ERR("fs error"); + return false; + } + } if (validateIdToken) { - auto authorize = makeRequest(new Authorize(context.getModel(), idToken)); + auto authorize = makeRequest(context, new Authorize(context.getModel201(), idToken)); if (!authorize) { // OOM abortTransaction(); @@ -658,7 +715,7 @@ bool TransactionService::Evse::beginAuthorization(IdToken idToken, bool validate 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) { + authorize->setOnReceiveConf([this, txId] (JsonObject response) { auto tx = getTransaction(); if (!tx || strcmp(tx->transactionId, txId)) { MO_DBG_INFO("dangling Authorize -- discard"); @@ -670,7 +727,7 @@ bool TransactionService::Evse::beginAuthorization(IdToken idToken, bool validate tx->isDeauthorized = true; txStore.commit(tx); - updateTxNotification(TxNotification_AuthorizationRejected); + updateTxNotification(MO_TxNotification_AuthorizationRejected); return; } @@ -679,9 +736,9 @@ bool TransactionService::Evse::beginAuthorization(IdToken idToken, bool validate tx->notifyIdToken = true; txStore.commit(tx); - updateTxNotification(TxNotification_Authorized); + updateTxNotification(MO_TxNotification_Authorized); }); - authorize->setOnAbortListener([this, txId] () { + authorize->setOnAbort([this, txId] () { auto tx = getTransaction(); if (!tx || strcmp(tx->transactionId, txId)) { MO_DBG_INFO("dangling Authorize -- discard"); @@ -692,49 +749,53 @@ bool TransactionService::Evse::beginAuthorization(IdToken idToken, bool validate tx->isDeauthorized = true; txStore.commit(tx); - updateTxNotification(TxNotification_AuthorizationTimeout); + updateTxNotification(MO_TxNotification_AuthorizationTimeout); }); - authorize->setTimeout(20 * 1000); - context.initiateRequest(std::move(authorize)); + authorize->setTimeout(20); + context.getMessageService().sendRequest(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); + updateTxNotification(MO_TxNotification_Authorized); } return true; } -bool TransactionService::Evse::endAuthorization(IdToken idToken, bool validateIdToken) { +bool TransactionServiceEvse::endAuthorization(IdToken idToken, bool validateIdToken, IdToken groupIdToken) { if (!transaction || !transaction->isAuthorizationActive) { //transaction already ended / not active anymore return false; } + if (*groupIdToken.get()) { + MO_DBG_WARN("groupIdToken not supported"); + } + 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); + updateTxNotification(MO_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); + updateTxNotification(MO_TxNotification_Authorized); } else { // use a different idToken for stopping the tx - auto authorize = makeRequest(new Authorize(context.getModel(), idToken)); + auto authorize = makeRequest(context, new Authorize(context.getModel201(), idToken)); if (!authorize) { // OOM abortTransaction(); @@ -744,7 +805,7 @@ bool TransactionService::Evse::endAuthorization(IdToken idToken, bool validateId 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) { + authorize->setOnReceiveConf([this, txId, idToken] (JsonObject response) { auto tx = getTransaction(); if (!tx || strcmp(tx->transactionId, txId)) { MO_DBG_INFO("dangling Authorize -- discard"); @@ -754,7 +815,7 @@ bool TransactionService::Evse::endAuthorization(IdToken idToken, bool validateId if (strcmp(response["idTokenInfo"]["status"] | "_Undefined", "Accepted")) { MO_DBG_DEBUG("Authorize rejected (%s), don't stop tx", idToken.get()); - updateTxNotification(TxNotification_AuthorizationRejected); + updateTxNotification(MO_TxNotification_AuthorizationRejected); return; } @@ -773,39 +834,103 @@ bool TransactionService::Evse::endAuthorization(IdToken idToken, bool validateId tx->notifyStopIdToken = true; txStore.commit(tx); - updateTxNotification(TxNotification_Authorized); + updateTxNotification(MO_TxNotification_Authorized); }); - authorize->setOnTimeoutListener([this, txId] () { + authorize->setOnTimeout([this, txId] () { auto tx = getTransaction(); if (!tx || strcmp(tx->transactionId, txId)) { MO_DBG_INFO("dangling Authorize -- discard"); return; } - updateTxNotification(TxNotification_AuthorizationTimeout); + updateTxNotification(MO_TxNotification_AuthorizationTimeout); }); - authorize->setTimeout(20 * 1000); - context.initiateRequest(std::move(authorize)); + authorize->setTimeout(20); + context.getMessageService().sendRequest(std::move(authorize)); } return true; } -bool TransactionService::Evse::abortTransaction(Ocpp201::Transaction::StoppedReason stoppedReason, TransactionEventTriggerReason stopTrigger) { + +bool TransactionServiceEvse::abortTransaction(MO_TxStoppedReason stoppedReason, MO_TxEventTriggerReason stopTrigger) { return endTransaction(stoppedReason, stopTrigger); } -MicroOcpp::Ocpp201::Transaction *TransactionService::Evse::getTransaction() { + +Transaction *TransactionServiceEvse::getTransaction() { return transaction.get(); } -bool TransactionService::Evse::ocppPermitsCharge() { +bool TransactionServiceEvse::commitTransaction() { + if (!transaction) { + MO_DBG_ERR("no tx to commit"); + return false; + } + return txStore.commit(transaction.get()); +} + +bool TransactionServiceEvse::ocppPermitsCharge() { return transaction && transaction->active && transaction->isAuthorizationActive && transaction->isAuthorized && + (!connectorPluggedInput || connectorPluggedInput(evseId, connectorPluggedInputUserData)) && !transaction->isDeauthorized; } -unsigned int TransactionService::Evse::getFrontRequestOpNr() { +TriggerMessageStatus TransactionServiceEvse::triggerTransactionEvent() { + //F06.FR.07: The Charging Station SHALL send a TransactionEventRequest to the CSMS with + //triggerReason = Trigger, transactionInfo with at least the chargingState, and meterValue + //with the most recent measurements for all measurands configured in Configuration Variable: + //SampledDataTxUpdatedMeasurands. + + if (!transaction) { + return TriggerMessageStatus::Rejected; + } + + auto txEvent = txStore.createTransactionEvent(*transaction); + if (!txEvent) { + MO_DBG_ERR("OOM"); + return TriggerMessageStatus::ERR_INTERNAL; + } + + txEvent->eventType = TransactionEventData::Type::Updated; + txEvent->timestamp = clock.now(); + txEvent->triggerReason = MO_TxEventTriggerReason_Trigger; + txEvent->chargingState = getChargingState(); + txEvent->evse = evseId; + + auto mvTriggerMessage = meteringEvse->takeTriggeredMeterValues(); + if (mvTriggerMessage) { + txEvent->meterValue.push_back(std::move(mvTriggerMessage)); + } + + txEvent->opNr = context.getMessageService().getNextOpNr(); + MO_DBG_DEBUG("enqueueing triggered txEvent at opNr %u", txEvent->opNr); + + if (!txStore.commit(txEvent.get())) { + MO_DBG_ERR("could not commit triggered txEvent"); + return TriggerMessageStatus::Rejected; + } + + //try to pass ownership to front txEvent immediatley + if (!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); + } + + return TriggerMessageStatus::Accepted; +} + +unsigned int TransactionServiceEvse::getFrontRequestOpNr() { if (txEventFront) { return txEventFront->opNr; @@ -815,7 +940,7 @@ unsigned int TransactionService::Evse::getFrontRequestOpNr() { * Advance front transaction? */ - unsigned int txSize = (txNrEnd + MAX_TX_CNT - txNrFront) % MAX_TX_CNT; + unsigned int txSize = (txNrEnd + MO_TXNR_MAX - txNrFront) % MO_TXNR_MAX; if (txFront && txSize == 0) { //catch edge case where txBack has been rolled back and txFront was equal to txBack @@ -848,7 +973,7 @@ unsigned int TransactionService::Evse::getFrontRequestOpNr() { txEventFront = nullptr; txFrontCache = nullptr; txFront = nullptr; - txNrFront = (txNrFront + 1) % MAX_TX_CNT; + txNrFront = (txNrFront + 1) % MO_TXNR_MAX; MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); } else { //front is accurate. Done here @@ -868,7 +993,7 @@ unsigned int TransactionService::Evse::getFrontRequestOpNr() { return NoOperation; } -std::unique_ptr TransactionService::Evse::fetchFrontRequest() { +std::unique_ptr TransactionServiceEvse::fetchFrontRequest() { if (!txEventFront) { return nullptr; @@ -878,9 +1003,10 @@ std::unique_ptr TransactionService::Evse::fetchFrontRequest() { return nullptr; } + char dummyTimestamp [MO_JSONDATE_SIZE]; if (txEventFront->seqNo == 0 && - txEventFront->timestamp < MIN_TIME && - txEventFront->bootNr != context.getModel().getBootNr()) { + !clock.toJsonString(txEventFront->timestamp, dummyTimestamp, sizeof(dummyTimestamp))) { + //time not set, cannot be restored anymore -> invalid tx MO_DBG_ERR("cannot recover tx from previous power cycle"); @@ -907,10 +1033,12 @@ std::unique_ptr TransactionService::Evse::fetchFrontRequest() { return nullptr; } - Timestamp nextAttempt = txEventFront->attemptTime + - txEventFront->attemptNr * std::max(0, txService.messageAttemptIntervalTransactionEventInt->getInt()); + int32_t dtLastAttempt = MO_MAX_TIME; + if (txEventFront->attemptTime.isDefined() && !clock.delta(clock.now(), txEventFront->attemptTime, dtLastAttempt)) { + MO_DBG_ERR("internal error"); + } - if (nextAttempt > context.getModel().getClock().now()) { + if (dtLastAttempt < (int)txEventFront->attemptNr * std::max(0, txService.messageAttemptIntervalTransactionEventInt->getInt())) { return nullptr; } @@ -920,21 +1048,21 @@ std::unique_ptr TransactionService::Evse::fetchFrontRequest() { } txEventFront->attemptNr++; - txEventFront->attemptTime = context.getModel().getClock().now(); + txEventFront->attemptTime = clock.now(); txStore.commit(txEventFront.get()); - auto txEventRequest = makeRequest(new TransactionEvent(context.getModel(), txEventFront.get())); - txEventRequest->setOnReceiveConfListener([this] (JsonObject) { + auto txEventRequest = makeRequest(context, new TransactionEvent(context, txEventFront.get())); + txEventRequest->setOnReceiveConf([this] (JsonObject) { MO_DBG_DEBUG("completed front txEvent"); txStore.remove(*txFront, txEventFront->seqNo); txEventFront = nullptr; txEventFrontIsRequested = false; }); - txEventRequest->setOnAbortListener([this] () { + txEventRequest->setOnAbort([this] () { MO_DBG_DEBUG("unsuccessful front txEvent"); txEventFrontIsRequested = false; }); - txEventRequest->setTimeout(std::min(20, std::max(5, txService.messageAttemptIntervalTransactionEventInt->getInt())) * 1000); + txEventRequest->setTimeout(std::min(20, std::max(5, txService.messageAttemptIntervalTransactionEventInt->getInt()))); txEventFrontIsRequested = true; @@ -1009,7 +1137,7 @@ namespace MicroOcpp { bool validateTxStartStopPoint(const char *value, void *userPtr) { auto txService = static_cast(userPtr); - auto validated = makeVector("v201.Transactions.TransactionService"); + auto validated = makeVector("v201.Transactions.TransactionService"); return txService->parseTxStartStopPoint(value, validated); } @@ -1017,17 +1145,50 @@ bool validateUnsignedInt(int val, void*) { return val >= 0; } -} //namespace MicroOcpp +bool disabledInput(unsigned int, void*) { + return false; +} -using namespace MicroOcpp; +} //namespace MicroOcpp -TransactionService::TransactionService(Context& context, std::shared_ptr filesystem, unsigned int numEvseIds) : +TransactionService::TransactionService(Context& context) : MemoryManaged("v201.Transactions.TransactionService"), context(context), - txStore(filesystem, numEvseIds), + txStore(context), txStartPointParsed(makeVector(getMemoryTag())), txStopPointParsed(makeVector(getMemoryTag())) { - auto varService = context.getModel().getVariableService(); + +} + +TransactionService::~TransactionService() { + for (unsigned int i = 0; i < MO_NUM_EVSEID; i++) { + delete evses[i]; + evses[i] = nullptr; + } +} + +bool TransactionService::setup() { + + filesystem = context.getFilesystem(); + if (!filesystem) { + MO_DBG_DEBUG("volatile mode"); + } + + rngCb = context.getRngCb(); + if (!rngCb) { + MO_DBG_ERR("setup failure"); + return false; + } + + if (!txStore.setup()) { + MO_DBG_ERR("setup failure"); + return false; + } + + auto varService = context.getModel201().getVariableService(); + if (!varService) { + return false; + } txStartPointString = varService->declareVariable("TxCtrlr", "TxStartPoint", "PowerPathClosed"); txStopPointString = varService->declareVariable("TxCtrlr", "TxStopPoint", "PowerPathClosed"); @@ -1040,33 +1201,101 @@ TransactionService::TransactionService(Context& context, std::shared_ptrdeclareVariable("OCPPCommCtrlr", "MessageAttemptInterval", 60); silentOfflineTransactionsBool = varService->declareVariable("CustomizationCtrlr", "SilentOfflineTransactions", false); - varService->declareVariable("AuthCtrlr", "AuthorizeRemoteStart", false, Variable::Mutability::ReadOnly, false); + if (!txStartPointString || + !txStopPointString || + !stopTxOnInvalidIdBool || + !stopTxOnEVSideDisconnectBool || + !evConnectionTimeOutInt || + !sampledDataTxUpdatedInterval || + !sampledDataTxEndedInterval || + !messageAttemptsTransactionEventInt || + !messageAttemptIntervalTransactionEventInt || + !silentOfflineTransactionsBool) { + MO_DBG_ERR("failure to declare variables"); + return false; + } + + varService->declareVariable("AuthCtrlr", "AuthorizeRemoteStart", false, Mutability::ReadOnly, false); + varService->declareVariable("AuthCtrlr", "LocalAuthorizeOffline", false, 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)) { + #if MO_ENABLE_MOCK_SERVER + context.getMessageService().registerOperation("Authorize", nullptr, Authorize::writeMockConf); + context.getMessageService().registerOperation("TransactionEvent", TransactionEvent::onRequestMock, TransactionEvent::writeMockConf); + #endif //MO_ENABLE_MOCK_SERVER + + auto rcService = context.getModel201().getRemoteControlService(); + if (!rcService) { + MO_DBG_ERR("initialization error"); + return false; + } + + rcService->addTriggerMessageHandler("TransactionEvent", [] (Context& context, int evseId) -> TriggerMessageStatus { + TriggerMessageStatus status = TriggerMessageStatus::Rejected; + auto txSvc = context.getModel201().getTransactionService(); + + unsigned int evseIdRangeBegin = 1; + unsigned int evseIdRangeEnd = context.getModel201().getNumEvseId(); + + if (evseId > 0) { + //send for one evseId only + evseIdRangeBegin = (unsigned int)evseId; + evseIdRangeEnd = evseIdRangeBegin + 1; + } + + for (unsigned i = evseIdRangeBegin; i < evseIdRangeEnd; i++) { + auto txSvcEvse = txSvc ? txSvc->getEvse(i) : nullptr; + if (!txSvcEvse) { + return TriggerMessageStatus::ERR_INTERNAL; + } + + auto ret = txSvcEvse->triggerTransactionEvent(); + + bool abortLoop = false; + + switch (ret) { + case TriggerMessageStatus::Accepted: + //Accepted takes precedence over Rejected + status = TriggerMessageStatus::Accepted; + break; + case TriggerMessageStatus::Rejected: + //if one EVSE rejects, but another accepts, respond with Accepted + break; + case TriggerMessageStatus::ERR_INTERNAL: + case TriggerMessageStatus::NotImplemented: + status = TriggerMessageStatus::ERR_INTERNAL; + abortLoop = true; + break; + } + + if (abortLoop) { + break; + } + } + + return status; + }); + + numEvseId = context.getModel201().getNumEvseId(); + for (unsigned int i = 0; i < numEvseId; i++) { + if (!getEvse(i) || !getEvse(i)->setup()) { MO_DBG_ERR("initialization error"); - break; + return false; } - 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;}; + evses[0]->connectorPluggedInput = disabledInput; + evses[0]->evReadyInput = disabledInput; + evses[0]->evseReadyInput = disabledInput; } -} -TransactionService::~TransactionService() { - for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { - delete evses[evseId]; - } + return true; } void TransactionService::loop() { @@ -1087,8 +1316,8 @@ void TransactionService::loop() { //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())) { + if (!evses[evseId]->getTransaction() && + (!evses[evseId]->connectorPluggedInput || evses[evseId]->connectorPluggedInput(evseId, evses[evseId]->connectorPluggedInputUserData))) { MO_DBG_INFO("assign tx to evse %u", evseId); evses[0]->transaction->notifyEvseId = true; evses[0]->transaction->evseId = evseId; @@ -1099,11 +1328,27 @@ void TransactionService::loop() { } } -TransactionService::Evse *TransactionService::getEvse(unsigned int evseId) { - if (evseId >= MO_NUM_EVSEID) { +TransactionServiceEvse *TransactionService::getEvse(unsigned int evseId) { + if (evseId >= numEvseId) { + MO_DBG_ERR("evseId out of bound"); + return nullptr; + } + + auto txStoreEvse = txStore.getEvse(evseId); + if (!txStoreEvse) { + MO_DBG_ERR("OOM"); return nullptr; } + + if (!evses[evseId]) { + evses[evseId] = new TransactionServiceEvse(context, *this, *txStoreEvse, evseId); + if (!evses[evseId]) { + MO_DBG_ERR("OOM"); + return nullptr; + } + } + return evses[evseId]; } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Transactions/TransactionService201.h b/src/MicroOcpp/Model/Transactions/TransactionService201.h new file mode 100644 index 00000000..af3a679b --- /dev/null +++ b/src/MicroOcpp/Model/Transactions/TransactionService201.h @@ -0,0 +1,174 @@ +// 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 +#include +#include +#include +#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 + +#if MO_ENABLE_V201 + +namespace MicroOcpp { + +class Context; +class Clock; + +namespace v201 { + +// TxStartStopPoint (2.6.4.1) +enum class TxStartStopPoint : uint8_t { + ParkingBayOccupancy, + EVConnected, + Authorized, + DataSigned, + PowerPathClosed, + EnergyTransfer +}; + +class Variable; +class TransactionService; +class MeteringServiceEvse; + +class TransactionServiceEvse : public RequestQueue, public MemoryManaged { +private: + Context& context; + Clock& clock; + TransactionService& txService; + TransactionStoreEvse& txStore; + MeteringServiceEvse *meteringEvse = nullptr; + const unsigned int evseId; + unsigned int txNrCounter = 0; + std::unique_ptr transaction; + TransactionEventData::ChargingState trackChargingState = TransactionEventData::ChargingState::UNDEFINED; + + bool (*connectorPluggedInput)(unsigned int evseId, void *userData) = nullptr; + void *connectorPluggedInputUserData = nullptr; + bool (*evReadyInput)(unsigned int evseId, void *userData) = nullptr; + void *evReadyInputUserData = nullptr; + bool (*evseReadyInput)(unsigned int evseId, void *userData) = nullptr; + void *evseReadyInputUserData = nullptr; + + bool (*startTxReadyInput)(unsigned int evseId, void *userData) = nullptr; //the StartTx request will be delayed while this Input is false + void *startTxReadyInputUserData = nullptr; + bool (*stopTxReadyInput)(unsigned int evseId, void *userData) = nullptr; //the StopTx request will be delayed while this Input is false + void *stopTxReadyInputUserData = nullptr; + + void (*txNotificationOutput)(MO_TxNotification, unsigned int evseId, void *userData) = nullptr; + void *txNotificationOutputUserData = nullptr; + + bool beginTransaction(); + bool endTransaction(MO_TxStoppedReason stoppedReason, MO_TxEventTriggerReason stopTrigger); + + TransactionEventData::ChargingState getChargingState(); + + 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 + + Transaction *txFront = nullptr; + std::unique_ptr txFrontCache; //helper owner for txFront. Empty if txFront == transaction.get() + std::unique_ptr txEventFront; + bool txEventFrontIsRequested = false; + +public: + TransactionServiceEvse(Context& context, TransactionService& txService, TransactionStoreEvse& txStore, unsigned int evseId); + virtual ~TransactionServiceEvse(); + + bool setup(); + + void loop(); + + void setConnectorPluggedInput(bool (*connectorPlugged)(unsigned int, void*), void *userData); + void setEvReadyInput(bool (*evReady)(unsigned int, void*), void *userData); + void setEvseReadyInput(bool (*evseReady)(unsigned int, void*), void *userData); + + void setStartTxReadyInput(bool (*startTxReady)(unsigned int, void*), void *userData); + void setStopTxReadyInput(bool (*stopTxReady)(unsigned int, void*), void *userData); + + void setTxNotificationOutput(void (*txNotificationOutput)(MO_TxNotification, unsigned int, void*), void *userData); + void updateTxNotification(MO_TxNotification event); + + bool beginAuthorization(IdToken idToken, bool validateIdToken = true, IdToken groupIdToken = nullptr, bool commit = true); // authorize by swipe RFID, groupIdToken ignored if validateIdToken = true, tx not stored on flash if commit = false + bool endAuthorization(IdToken idToken = IdToken(), bool validateIdToken = false, IdToken groupIdToken = nullptr); // stop authorization by swipe RFID, groupIdToken ignored if validateIdToken = true + + // stop transaction, but neither upon user request nor OCPP server request (e.g. after PowerLoss) + bool abortTransaction(MO_TxStoppedReason stoppedReason = MO_TxStoppedReason_Other, MO_TxEventTriggerReason stopTrigger = MO_TxEventTriggerReason_AbnormalCondition); + + Transaction *getTransaction(); + bool commitTransaction(); + + bool ocppPermitsCharge(); + + TriggerMessageStatus triggerTransactionEvent(); + + unsigned int getFrontRequestOpNr() override; + std::unique_ptr fetchFrontRequest() override; + + friend TransactionService; +}; + +class TransactionService : public MemoryManaged { +private: + Context& context; + TransactionStore txStore; + TransactionServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; + unsigned int numEvseId = MO_NUM_EVSEID; + + MO_FilesystemAdapter *filesystem = nullptr; + uint32_t (*rngCb)() = 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); + ~TransactionService(); + + bool setup(); + + void loop(); + + TransactionServiceEvse *getEvse(unsigned int evseId); + + bool parseTxStartStopPoint(const char *src, Vector& dst); + +friend class TransactionServiceEvse; +}; + +} //namespace v201 +} //namespace MicroOcpp +#endif //MO_ENABLE_V201 +#endif diff --git a/src/MicroOcpp/Model/Transactions/TransactionStore.cpp b/src/MicroOcpp/Model/Transactions/TransactionStore.cpp index 39c096ba..a79dc3e9 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionStore.cpp +++ b/src/MicroOcpp/Model/Transactions/TransactionStore.cpp @@ -3,233 +3,375 @@ // MIT License #include -#include +#include #include +#include +#include #include -using namespace MicroOcpp; +#include -ConnectorTransactionStore::ConnectorTransactionStore(TransactionStore& context, unsigned int connectorId, std::shared_ptr filesystem) : - MemoryManaged("v16.Transactions.TransactionStore"), - context(context), - connectorId(connectorId), - filesystem(filesystem), - transactions{makeVector>(getMemoryTag())} { +#if MO_ENABLE_V16 -} +namespace MicroOcpp { +namespace v16 { +namespace TransactionStore { -ConnectorTransactionStore::~ConnectorTransactionStore() { +bool serializeSendStatus(Clock& clock, SendStatus& status, JsonObject out); +bool deserializeSendStatus(Clock& clock, SendStatus& status, JsonObject in); -} +bool serializeTransaction(Clock& clock, Transaction& tx, JsonDoc& out); +bool deserializeTransaction(Clock& clock, Transaction& tx, JsonObject in); -std::shared_ptr ConnectorTransactionStore::getTransaction(unsigned int txNr) { +} //namespace TransactionStore +} //namespace v16 +} //namespace MicroOcpp - //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; - } - } - } +using namespace MicroOcpp; - //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); - } +bool v16::TransactionStore::printTxFname(char *fname, size_t size, unsigned int evseId, unsigned int txNr) { + auto ret = snprintf(fname, size, "tx-%.*u-%.*u.json", + MO_NUM_EVSEID_DIGITS, evseId, MO_TXNR_DIGITS, txNr); + if (ret < 0 || (size_t)ret >= size) { + MO_DBG_ERR("fn error: %i", ret); + return false; } + return true; +} - //cache miss - load tx from flash if existent - - if (!filesystem) { - MO_DBG_DEBUG("no FS adapter"); - return nullptr; +FilesystemUtils::LoadStatus v16::TransactionStore::load(MO_FilesystemAdapter *filesystem, Context& context, unsigned int evseId, unsigned int txNr, Transaction& transaction) { + + char fname [MO_MAX_PATH_SIZE]; + if (!printTxFname(fname, sizeof(fname), evseId, txNr)) { + MO_DBG_ERR("fname error %u %u", evseId, txNr); + return FilesystemUtils::LoadStatus::ErrOther; } - 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; + JsonDoc doc (0); + auto ret = FilesystemUtils::loadJson(filesystem, fname, doc, "v16.Transactions.TransactionStore"); + if (ret != FilesystemUtils::LoadStatus::Success) { + return ret; } - size_t msize; - if (filesystem->stat(fn, &msize) != 0) { - MO_DBG_DEBUG("%u-%u does not exist", connectorId, txNr); - return nullptr; + auto& clock = context.getClock(); + + if (!deserializeTransaction(clock, transaction, doc.as())) { + MO_DBG_ERR("deserialization error %s", fname); + return FilesystemUtils::LoadStatus::ErrFileCorruption; } - auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); + //success - if (!doc) { - MO_DBG_ERR("memory corruption"); - return nullptr; + MO_DBG_DEBUG("Restored tx %u-%u", evseId, txNr); + + return FilesystemUtils::LoadStatus::Success; +} + +FilesystemUtils::StoreStatus v16::TransactionStore::store(MO_FilesystemAdapter *filesystem, Context& context, Transaction& transaction) { + + char fname [MO_MAX_PATH_SIZE]; + if (!printTxFname(fname, sizeof(fname), transaction.getConnectorId(), transaction.getTxNr())) { + MO_DBG_ERR("fname error %u %u", transaction.getConnectorId(), transaction.getTxNr()); + return FilesystemUtils::StoreStatus::ErrOther; } - 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; + auto& clock = context.getClock(); + + JsonDoc doc (0); + if (!serializeTransaction(clock, transaction, doc)) { + MO_DBG_ERR("serialization error %s", fname); + return FilesystemUtils::StoreStatus::ErrJsonCorruption; } - //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++; - } + auto ret = FilesystemUtils::storeJson(filesystem, fname, doc); + if (ret != FilesystemUtils::StoreStatus::Success) { + MO_DBG_ERR("fs error %s %i", fname, (int)ret); } - transactions.push_back(transaction); - return transaction; + return ret; } -std::shared_ptr ConnectorTransactionStore::createTransaction(unsigned int txNr, bool silent) { +bool v16::TransactionStore::remove(MO_FilesystemAdapter *filesystem, unsigned int evseId, unsigned int txNr) { - auto transaction = std::allocate_shared(makeAllocator(getMemoryTag()), *this, connectorId, txNr, silent); + char fname [MO_MAX_PATH_SIZE]; + if (!printTxFname(fname, sizeof(fname), evseId, txNr)) { + MO_DBG_ERR("fname error %u %u", evseId, txNr); + return false; + } - if (!commit(transaction.get())) { - MO_DBG_ERR("FS error"); - return nullptr; + char path [MO_MAX_PATH_SIZE]; + if (!FilesystemUtils::printPath(filesystem, path, sizeof(path), "%s", fname)) { + MO_DBG_ERR("fname error %u %u", evseId, txNr); + return false; } - //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++; + return filesystem->remove(path); +} + +bool v16::TransactionStore::serializeSendStatus(Clock& clock, 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().isDefined()) { + char attemptTime [MO_JSONDATE_SIZE]; + if (!clock.toInternalString(status.getAttemptTime(), attemptTime, sizeof(attemptTime))) { + return false; } + out["attemptTime"] = attemptTime; } + return true; +} - transactions.push_back(transaction); - return transaction; +bool v16::TransactionStore::deserializeSendStatus(Clock& clock, 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 (!clock.parseString(in["attemptTime"] | "_Invalid", attemptTime)) { + MO_DBG_ERR("deserialization error"); + return false; + } + status.setAttemptTime(attemptTime); + } + return true; } -bool ConnectorTransactionStore::commit(Transaction *transaction) { +bool v16::TransactionStore::serializeTransaction(Clock& clock, Transaction& tx, JsonDoc& out) { + out = initJsonDoc("v16.Transactions.TransactionDeserialize", 1024); + JsonObject state = out.to(); - if (!filesystem) { - MO_DBG_DEBUG("no FS: nothing to commit"); - return true; + 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().isDefined()) { + char timeStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!clock.toInternalString(tx.getBeginTimestamp(), timeStr, sizeof(timeStr))) { + return false; + } + sessionState["timestamp"] = timeStr; + } + if (tx.getReservationId() >= 0) { + sessionState["reservationId"] = tx.getReservationId(); + } + if (tx.getTxProfileId() >= 0) { + sessionState["txProfileId"] = tx.getTxProfileId(); } - 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); + JsonObject txStart = state.createNestedObject("start"); + + if (!serializeSendStatus(clock, tx.getStartSync(), txStart)) { return false; } - - auto txDoc = initJsonDoc(getMemoryTag()); - if (!serializeTransaction(*transaction, txDoc)) { - MO_DBG_ERR("Serialization error"); + + if (tx.isMeterStartDefined()) { + txStart["meter"] = tx.getMeterStart(); + } + + if (tx.getStartTimestamp().isDefined()) { + char startTimeStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!clock.toInternalString(tx.getStartTimestamp(), startTimeStr, sizeof(startTimeStr))) { + return false; + } + txStart["timestamp"] = startTimeStr; + } + + if (tx.getStartSync().isConfirmed()) { + txStart["transactionId"] = tx.getTransactionId(); + } + + JsonObject txStop = state.createNestedObject("stop"); + + if (!serializeSendStatus(clock, tx.getStopSync(), txStop)) { return false; } - if (!FilesystemUtils::storeJson(filesystem, fn, txDoc)) { - MO_DBG_ERR("FS error"); + if (tx.getStopIdTag()[0] != '\0') { + txStop["idTag"] = tx.getStopIdTag(); + } + + if (tx.isMeterStopDefined()) { + txStop["meter"] = tx.getMeterStop(); + } + + if (tx.getStopTimestamp().isDefined()) { + char stopTimeStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!clock.toInternalString(tx.getStopTimestamp(), stopTimeStr, sizeof(stopTimeStr))) { + return false; + } + txStop["timestamp"] = stopTimeStr; + } + + 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; } - //success return true; } -bool ConnectorTransactionStore::remove(unsigned int txNr) { +bool v16::TransactionStore::deserializeTransaction(Clock& clock, Transaction& tx, JsonObject state) { - if (!filesystem) { - MO_DBG_DEBUG("no FS: nothing to remove"); - return true; + JsonObject sessionState = state["session"]; + + if (!(sessionState["active"] | true)) { + tx.setInactive(); } - 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; + if (sessionState.containsKey("idTag")) { + if (!tx.setIdTag(sessionState["idTag"] | "")) { + MO_DBG_ERR("read err"); + return false; + } } - size_t msize; - if (filesystem->stat(fn, &msize) != 0) { - MO_DBG_DEBUG("%s already removed", fn); - return true; + if (sessionState.containsKey("parentIdTag")) { + if (!tx.setParentIdTag(sessionState["parentIdTag"] | "")) { + MO_DBG_ERR("read err"); + return false; + } } - MO_DBG_DEBUG("remove %s", fn); - - return filesystem->remove(fn); -} + if (sessionState["authorized"] | false) { + tx.setAuthorized(); + } -TransactionStore::TransactionStore(unsigned int nConnectors, std::shared_ptr filesystem) : - MemoryManaged{"v16.Transactions.TransactionStore"}, connectors{makeVector>(getMemoryTag())} { + if (sessionState["deauthorized"] | false) { + tx.setIdTagDeauthorized(); + } - for (unsigned int i = 0; i < nConnectors; i++) { - connectors.push_back(std::unique_ptr( - new ConnectorTransactionStore(*this, i, filesystem))); + if (sessionState.containsKey("timestamp")) { + Timestamp timestamp; + if (!clock.parseString(sessionState["timestamp"] | "_Invalid", timestamp)) { + MO_DBG_ERR("read err"); + return false; + } + tx.setBeginTimestamp(timestamp); } -} -bool TransactionStore::commit(Transaction *transaction) { - if (!transaction) { - MO_DBG_ERR("Invalid arg"); - return false; + if (sessionState.containsKey("reservationId")) { + tx.setReservationId(sessionState["reservationId"] | -1); + } + + if (sessionState.containsKey("txProfileId")) { + tx.setTxProfileId(sessionState["txProfileId"] | -1); } - auto connectorId = transaction->getConnectorId(); - if (connectorId >= connectors.size()) { - MO_DBG_ERR("Invalid tx"); + + JsonObject txStart = state["start"]; + + if (!deserializeSendStatus(clock, tx.getStartSync(), txStart)) { 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; + if (txStart.containsKey("meter")) { + tx.setMeterStart(txStart["meter"] | 0); } - 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; + if (txStart.containsKey("timestamp")) { + Timestamp timestamp; + if (!clock.parseString(txStart["timestamp"] | "_Invalid", timestamp)) { + MO_DBG_ERR("deserialization error"); + return false; + } + tx.setStartTimestamp(timestamp); } - return connectors[connectorId]->createTransaction(txNr, silent); -} -bool TransactionStore::remove(unsigned int connectorId, unsigned int txNr) { - if (connectorId >= connectors.size()) { - MO_DBG_ERR("Invalid connectorId"); + if (txStart.containsKey("transactionId")) { + tx.setTransactionId(txStart["transactionId"] | -1); + } + + JsonObject txStop = state["stop"]; + + if (!deserializeSendStatus(clock, tx.getStopSync(), txStop)) { return false; } - return connectors[connectorId]->remove(txNr); + + 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 (!clock.parseString(txStop["timestamp"] | "_Invalid", timestamp)) { + MO_DBG_ERR("deserialization error"); + return false; + } + tx.setStopTimestamp(timestamp); + } + + 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; } -#if MO_ENABLE_V201 +#endif //MO_ENABLE_V16 -#include +#if MO_ENABLE_V201 -namespace MicroOcpp { -namespace Ocpp201 { +using namespace MicroOcpp; -bool TransactionStoreEvse::serializeTransaction(Transaction& tx, JsonObject txJson) { +bool v201::TransactionStoreEvse::serializeTransaction(Transaction& tx, JsonObject txJson) { if (tx.trackEvConnected) { txJson["trackEvConnected"] = tx.trackEvConnected; @@ -276,16 +418,32 @@ bool TransactionStoreEvse::serializeTransaction(Transaction& tx, JsonObject txJs txJson["idToken"]["type"] = tx.idToken.getTypeCstr(); } - if (tx.beginTimestamp > MIN_TIME) { - char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; - tx.beginTimestamp.toJsonString(timeStr, JSONDATE_LENGTH + 1); + if (tx.beginTimestamp.isDefined()) { + char timeStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!context.getClock().toInternalString(tx.beginTimestamp, timeStr, sizeof(timeStr))) { + MO_DBG_ERR("serialization error"); + return false; + } txJson["beginTimestamp"] = timeStr; } + if (tx.startTimestamp.isDefined()) { + char timeStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!context.getClock().toInternalString(tx.startTimestamp, timeStr, sizeof(timeStr))) { + MO_DBG_ERR("serialization error"); + return false; + } + txJson["startTimestamp"] = timeStr; + } + if (tx.remoteStartId >= 0) { txJson["remoteStartId"] = tx.remoteStartId; } + if (tx.txProfileId >= 0) { + txJson["txProfileId"] = tx.txProfileId; + } + if (tx.evConnectionTimeoutListen) { txJson["evConnectionTimeoutListen"] = true; } @@ -294,8 +452,8 @@ bool TransactionStoreEvse::serializeTransaction(Transaction& tx, JsonObject txJs txJson["stoppedReason"] = serializeTransactionStoppedReason(tx.stoppedReason); } - if (serializeTransactionEventTriggerReason(tx.stopTrigger)) { - txJson["stopTrigger"] = serializeTransactionEventTriggerReason(tx.stopTrigger); + if (serializeTxEventTriggerReason(tx.stopTrigger)) { + txJson["stopTrigger"] = serializeTxEventTriggerReason(tx.stopTrigger); } if (tx.stopIdToken) { @@ -315,7 +473,7 @@ bool TransactionStoreEvse::serializeTransaction(Transaction& tx, JsonObject txJs return true; } -bool TransactionStoreEvse::deserializeTransaction(Transaction& tx, JsonObject txJson) { +bool v201::TransactionStoreEvse::deserializeTransaction(Transaction& tx, JsonObject txJson) { if (txJson.containsKey("trackEvConnected") && !txJson["trackEvConnected"].is()) { return false; @@ -373,7 +531,7 @@ bool TransactionStoreEvse::deserializeTransaction(Transaction& tx, JsonObject tx if (txJson.containsKey("idToken")) { IdToken idToken; if (!idToken.parseCstr( - txJson["idToken"]["idToken"] | (const char*)nullptr, + txJson["idToken"]["idToken"] | (const char*)nullptr, txJson["idToken"]["type"] | (const char*)nullptr)) { return false; } @@ -381,7 +539,13 @@ bool TransactionStoreEvse::deserializeTransaction(Transaction& tx, JsonObject tx } if (txJson.containsKey("beginTimestamp")) { - if (!tx.beginTimestamp.setTime(txJson["beginTimestamp"] | "_Undefined")) { + if (!context.getClock().parseString(txJson["beginTimestamp"] | "_Invalid", tx.beginTimestamp)) { + return false; + } + } + + if (txJson.containsKey("startTimestamp")) { + if (!context.getClock().parseString(txJson["startTimestamp"] | "_Invalid", tx.startTimestamp)) { return false; } } @@ -394,19 +558,27 @@ bool TransactionStoreEvse::deserializeTransaction(Transaction& tx, JsonObject tx tx.remoteStartId = remoteStartIdIn; } + if (txJson.containsKey("txProfileId")) { + int txProfileIdIn = txJson["txProfileId"] | -1; + if (txProfileIdIn < 0) { + return false; + } + tx.txProfileId = txProfileIdIn; + } + if (txJson.containsKey("evConnectionTimeoutListen") && !txJson["evConnectionTimeoutListen"].is()) { return false; } tx.evConnectionTimeoutListen = txJson["evConnectionTimeoutListen"] | false; - Transaction::StoppedReason stoppedReason; + MO_TxStoppedReason stoppedReason; if (!deserializeTransactionStoppedReason(txJson["stoppedReason"] | (const char*)nullptr, stoppedReason)) { return false; } tx.stoppedReason = stoppedReason; - TransactionEventTriggerReason stopTrigger; - if (!deserializeTransactionEventTriggerReason(txJson["stopTrigger"] | (const char*)nullptr, stopTrigger)) { + MO_TxEventTriggerReason stopTrigger; + if (!deserializeTxEventTriggerReason(txJson["stopTrigger"] | (const char*)nullptr, stopTrigger)) { return false; } tx.stopTrigger = stopTrigger; @@ -418,7 +590,7 @@ bool TransactionStoreEvse::deserializeTransaction(Transaction& tx, JsonObject tx return false; } if (!stopIdToken->parseCstr( - txJson["stopIdToken"]["idToken"] | (const char*)nullptr, + txJson["stopIdToken"]["idToken"] | (const char*)nullptr, txJson["stopIdToken"]["type"] | (const char*)nullptr)) { return false; } @@ -444,24 +616,24 @@ bool TransactionStoreEvse::deserializeTransaction(Transaction& tx, JsonObject tx return true; } -bool TransactionStoreEvse::serializeTransactionEvent(TransactionEventData& txEvent, JsonObject txEventJson) { - +bool v201::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); + if (txEvent.timestamp.isDefined()) { + char timeStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!context.getClock().toInternalString(txEvent.timestamp, timeStr, sizeof(timeStr))) { + return false; + } txEventJson["timestamp"] = timeStr; } - txEventJson["bootNr"] = txEvent.bootNr; - - if (serializeTransactionEventTriggerReason(txEvent.triggerReason)) { - txEventJson["triggerReason"] = serializeTransactionEventTriggerReason(txEvent.triggerReason); + if (serializeTxEventTriggerReason(txEvent.triggerReason)) { + txEventJson["triggerReason"] = serializeTxEventTriggerReason(txEvent.triggerReason); } - + if (txEvent.offline) { txEventJson["offline"] = true; } @@ -504,17 +676,19 @@ bool TransactionStoreEvse::serializeTransactionEvent(TransactionEventData& txEve 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); + + if (txEvent.attemptTime.isDefined()) { + char timeStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!context.getClock().toInternalString(txEvent.attemptTime, timeStr, sizeof(timeStr))) { + return false; + } txEventJson["attemptTime"] = timeStr; } return true; } -bool TransactionStoreEvse::deserializeTransactionEvent(TransactionEventData& txEvent, JsonObject txEventJson) { +bool v201::TransactionStoreEvse::deserializeTransactionEvent(TransactionEventData& txEvent, JsonObject txEventJson) { TransactionEventData::Type eventType; if (!deserializeTransactionEventType(txEventJson["eventType"] | "Updated", eventType)) { @@ -523,24 +697,17 @@ bool TransactionStoreEvse::deserializeTransactionEvent(TransactionEventData& txE txEvent.eventType = eventType; if (txEventJson.containsKey("timestamp")) { - if (!txEvent.timestamp.setTime(txEventJson["timestamp"] | "_Undefined")) { + if (!context.getClock().parseString(txEventJson["timestamp"] | "_Undefined", txEvent.timestamp)) { 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)) { + MO_TxEventTriggerReason triggerReason; + if (!deserializeTxEventTriggerReason(txEventJson["triggerReason"] | "_Undefined", triggerReason)) { return false; } txEvent.triggerReason = triggerReason; - + if (txEventJson.containsKey("offline") && !txEventJson["offline"].is()) { return false; } @@ -591,7 +758,7 @@ bool TransactionStoreEvse::deserializeTransactionEvent(TransactionEventData& txE return false; } if (!idToken->parseCstr( - txEventJson["idToken"]["idToken"] | (const char*)nullptr, + txEventJson["idToken"]["idToken"] | (const char*)nullptr, txEventJson["idToken"]["type"] | (const char*)nullptr)) { return false; } @@ -631,7 +798,7 @@ bool TransactionStoreEvse::deserializeTransactionEvent(TransactionEventData& txE } if (txEventJson.containsKey("attemptTime")) { - if (!txEvent.attemptTime.setTime(txEventJson["attemptTime"] | "_Undefined")) { + if (!context.getClock().parseString(txEventJson["attemptTime"] | "_Undefined", txEvent.attemptTime)) { return false; } } @@ -639,71 +806,55 @@ bool TransactionStoreEvse::deserializeTransactionEvent(TransactionEventData& txE return true; } -TransactionStoreEvse::TransactionStoreEvse(TransactionStore& txStore, unsigned int evseId, std::shared_ptr filesystem) : +v201::TransactionStoreEvse::TransactionStoreEvse(Context& context, unsigned int evseId) : MemoryManaged("v201.Transactions.TransactionStore"), - txStore(txStore), - evseId(evseId), - filesystem(filesystem) { + context(context), + evseId(evseId) { } -bool TransactionStoreEvse::discoverStoredTx(unsigned int& txNrBeginOut, unsigned int& txNrEndOut) { - +bool v201::TransactionStoreEvse::setup() { + filesystem = context.getFilesystem(); if (!filesystem) { - MO_DBG_DEBUG("no FS adapter"); - return true; + MO_DBG_DEBUG("volatile mode"); } + 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; - } +bool v201::TransactionStoreEvse::printTxEventFname(char *fname, size_t size, unsigned int evseId, unsigned int txNr, unsigned int seqNo) { + auto ret = snprintf(fname, size, "tx201-%.*u-%.*u-%.*u.json", + MO_NUM_EVSEID_DIGITS, evseId, MO_TXNR_DIGITS, txNr, MO_TXEVENTRECORD_DIGITS, seqNo); + if (ret < 0 || (size_t)ret >= size) { + MO_DBG_ERR("fn error: %i", ret); + return false; + } + return true; +} - 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; - } - } +namespace MicroOcpp { - MO_DBG_DEBUG("found %s%u-*.json - Internal range from %u to %u (exclusive)", fnPrefix, parsedTxNr, txNrBegin, txNrEnd); +struct LoadSeqNosData { + const char *fnPrefix = nullptr; + size_t fnPrefixLen = 0; + Vector *seqNos = nullptr; +}; + +int loadSeqNoEntry(const char *fname, void* user_data) { + auto data = reinterpret_cast(user_data); + if (!strncmp(fname, data->fnPrefix, data->fnPrefixLen)) { + unsigned int parsedSeqNo = 0; + for (size_t i = data->fnPrefixLen; fname[i] >= '0' && fname[i] <= '9'; i++) { + parsedSeqNo *= 10; + parsedSeqNo += fname[i] - '0'; } - return 0; - }); - if (ret == 0) { - txNrBeginOut = txNrBegin; - txNrEndOut = txNrEnd; - return true; - } else { - MO_DBG_ERR("fs error"); - return false; + data->seqNos->push_back(parsedSeqNo); } + return 0; } +} //namespace MicroOcpp -std::unique_ptr TransactionStoreEvse::loadTransaction(unsigned int txNr) { +std::unique_ptr v201::TransactionStoreEvse::loadTransaction(unsigned int txNr) { if (!filesystem) { MO_DBG_DEBUG("no FS adapter"); @@ -711,7 +862,8 @@ std::unique_ptr TransactionStoreEvse::loadTransaction(unsigned int } char fnPrefix [MO_MAX_PATH_SIZE]; - auto ret= snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%u-%u-", evseId, txNr); + auto ret = snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%.*u-%.*u-", + MO_NUM_EVSEID_DIGITS, evseId, MO_TXNR_DIGITS, txNr); if (ret < 0 || (size_t)ret >= sizeof(fnPrefix)) { MO_DBG_ERR("fn error"); return nullptr; @@ -720,18 +872,12 @@ std::unique_ptr TransactionStoreEvse::loadTransaction(unsigned int 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'; - } + LoadSeqNosData data; + data.fnPrefix = fnPrefix; + data.fnPrefixLen = fnPrefixLen; + data.seqNos = &seqNos; - seqNos.push_back(parsedSeqNo); - } - return 0; - }); + filesystem->ftw(filesystem->path_prefix, loadSeqNoEntry, reinterpret_cast(&data)); if (seqNos.empty()) { MO_DBG_DEBUG("no tx at tx201-%u-%u", evseId, txNr); @@ -741,26 +887,29 @@ std::unique_ptr TransactionStoreEvse::loadTransaction(unsigned int 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) { + if (!printTxEventFname(fn, sizeof(fn), evseId, txNr, seqNos.back())) { 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; + JsonDoc doc (0); + auto status = FilesystemUtils::loadJson(filesystem, fn, doc, getMemoryTag()); + switch (status) { + case FilesystemUtils::LoadStatus::Success: + break; //continue loading JSON + case FilesystemUtils::LoadStatus::FileNotFound: + MO_DBG_ERR("memory corruption"); + return nullptr; + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + return nullptr; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("failed to load %s", fn); + return nullptr; } - auto transaction = std::unique_ptr(new Transaction()); + auto transaction = std::unique_ptr(new Transaction(context.getClock())); if (!transaction) { MO_DBG_ERR("OOM"); return nullptr; @@ -770,7 +919,7 @@ std::unique_ptr TransactionStoreEvse::loadTransaction(unsigned int transaction->txNr = txNr; transaction->seqNos = std::move(seqNos); - JsonObject txJson = (*doc)["tx"]; + JsonObject txJson = doc["tx"]; if (!deserializeTransaction(*transaction, txJson)) { MO_DBG_ERR("deserialization error"); @@ -778,7 +927,7 @@ std::unique_ptr TransactionStoreEvse::loadTransaction(unsigned int } //determine seqNoEnd and trim seqNos record - if (doc->containsKey("txEvent")) { + 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 { @@ -792,7 +941,7 @@ std::unique_ptr TransactionStoreEvse::loadTransaction(unsigned int return transaction; } -std::unique_ptr TransactionStoreEvse::createTransaction(unsigned int txNr, const char *txId) { +std::unique_ptr v201::TransactionStoreEvse::createTransaction(unsigned int txNr, const char *txId) { //clean data which could still be here from a rolled-back transaction if (!remove(txNr)) { @@ -800,7 +949,7 @@ std::unique_ptr TransactionStoreEvse::createTransaction(un return nullptr; } - auto transaction = std::unique_ptr(new Transaction()); + auto transaction = std::unique_ptr(new Transaction(context.getClock())); if (!transaction) { MO_DBG_ERR("OOM"); return nullptr; @@ -815,15 +964,10 @@ std::unique_ptr TransactionStoreEvse::createTransaction(un return nullptr; } - if (!commit(transaction.get())) { - MO_DBG_ERR("FS error"); - return nullptr; - } - return transaction; } -std::unique_ptr TransactionStoreEvse::createTransactionEvent(Transaction& tx) { +std::unique_ptr v201::TransactionStoreEvse::createTransactionEvent(Transaction& tx) { auto txEvent = std::unique_ptr(new TransactionEventData(&tx, tx.seqNoEnd)); if (!txEvent) { @@ -835,7 +979,7 @@ std::unique_ptr TransactionStoreEvse::createTransactionEve return txEvent; } -std::unique_ptr TransactionStoreEvse::loadTransactionEvent(Transaction& tx, unsigned int seqNo) { +std::unique_ptr v201::TransactionStoreEvse::loadTransactionEvent(Transaction& tx, unsigned int seqNo) { if (!filesystem) { MO_DBG_DEBUG("no FS adapter"); @@ -853,27 +997,30 @@ std::unique_ptr TransactionStoreEvse::loadTransactionEvent 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); + char fn [MO_MAX_PATH_SIZE]; + if (!printTxEventFname(fn, sizeof(fn), evseId, tx.txNr, seqNo)) { + MO_DBG_ERR("fn error: %s", fn); return nullptr; } - auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); - - if (!doc) { - MO_DBG_ERR("memory corruption"); - return nullptr; + JsonDoc doc (0); + auto ret = FilesystemUtils::loadJson(filesystem, fn, doc, getMemoryTag()); + switch (ret) { + case FilesystemUtils::LoadStatus::Success: + break; //continue loading JSON + case FilesystemUtils::LoadStatus::FileNotFound: + MO_DBG_ERR("seqNos out of sync: could not find %u-%u-%u", evseId, tx.txNr, seqNo); + return nullptr; + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + return nullptr; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("failed to load %s", fn); + return nullptr; } - if (!doc->containsKey("txEvent")) { + if (!doc.containsKey("txEvent")) { MO_DBG_DEBUG("%u-%u-%u does not contain txEvent", evseId, tx.txNr, seqNo); return nullptr; } @@ -884,7 +1031,7 @@ std::unique_ptr TransactionStoreEvse::loadTransactionEvent return nullptr; } - if (!deserializeTransactionEvent(*txEvent, (*doc)["txEvent"])) { + if (!deserializeTransactionEvent(*txEvent, doc["txEvent"])) { MO_DBG_ERR("deserialization error"); return nullptr; } @@ -892,7 +1039,7 @@ std::unique_ptr TransactionStoreEvse::loadTransactionEvent return txEvent; } -bool TransactionStoreEvse::commit(Transaction& tx, TransactionEventData *txEvent) { +bool v201::TransactionStoreEvse::commit(Transaction& tx, TransactionEventData *txEvent) { if (!filesystem) { MO_DBG_DEBUG("no FS: nothing to commit"); @@ -917,7 +1064,7 @@ bool TransactionStoreEvse::commit(Transaction& tx, TransactionEventData *txEvent } // Check if to delete intermediate offline txEvent - if (seqNosNewSize > MO_TXEVENTRECORD_SIZE_V201) { + if (seqNosNewSize > MO_TXEVENTRECORD_SIZE) { 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 @@ -942,10 +1089,9 @@ bool TransactionStoreEvse::commit(Transaction& tx, TransactionEventData *txEvent } } - 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); + char fn [MO_MAX_PATH_SIZE]; + if (!printTxEventFname(fn, sizeof(fn), evseId, tx.txNr, seqNo)) { + MO_DBG_ERR("fn error: %s", fn); return false; } @@ -961,7 +1107,7 @@ bool TransactionStoreEvse::commit(Transaction& tx, TransactionEventData *txEvent return false; } - if (!FilesystemUtils::storeJson(filesystem, fn, txDoc)) { + if (FilesystemUtils::storeJson(filesystem, fn, txDoc) != FilesystemUtils::StoreStatus::Success) { MO_DBG_ERR("FS error"); return false; } @@ -977,15 +1123,15 @@ bool TransactionStoreEvse::commit(Transaction& tx, TransactionEventData *txEvent return true; } -bool TransactionStoreEvse::commit(Transaction *transaction) { +bool v201::TransactionStoreEvse::commit(Transaction *transaction) { return commit(*transaction, nullptr); } -bool TransactionStoreEvse::commit(TransactionEventData *txEvent) { +bool v201::TransactionStoreEvse::commit(TransactionEventData *txEvent) { return commit(*txEvent->transaction, txEvent); } -bool TransactionStoreEvse::remove(unsigned int txNr) { +bool v201::TransactionStoreEvse::remove(unsigned int txNr) { if (!filesystem) { MO_DBG_DEBUG("no FS: nothing to remove"); @@ -993,21 +1139,17 @@ bool TransactionStoreEvse::remove(unsigned int txNr) { } char fnPrefix [MO_MAX_PATH_SIZE]; - auto ret= snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%u-%u-", evseId, txNr); + auto ret = snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%.*u-%.*u-", + MO_NUM_EVSEID_DIGITS, evseId, MO_TXNR_DIGITS, 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; + return FilesystemUtils::removeByPrefix(filesystem, fnPrefix); } -bool TransactionStoreEvse::remove(Transaction& tx, unsigned int seqNo) { +bool v201::TransactionStoreEvse::remove(Transaction& tx, unsigned int seqNo) { if (tx.seqNos.empty()) { //nothing to do @@ -1019,15 +1161,28 @@ bool TransactionStoreEvse::remove(Transaction& tx, unsigned int seqNo) { //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)) { + if (!printTxEventFname(fn, sizeof(fn), evseId, tx.txNr, tx.seqNoEnd)) { MO_DBG_ERR("fn error"); return false; } - auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); - - if (!doc || !doc->containsKey("tx")) { + JsonDoc doc (0); + auto ret = FilesystemUtils::loadJson(filesystem, fn, doc, getMemoryTag()); + switch (ret) { + case FilesystemUtils::LoadStatus::Success: + break; //continue loading JSON + case FilesystemUtils::LoadStatus::FileNotFound: + break; + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + return false; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("failed to load %s", fn); + break; + } + + if (ret != FilesystemUtils::LoadStatus::Success || !doc.containsKey("tx")) { //no valid tx201 file at seqNoEnd. Commit tx into file seqNoEnd, then remove file at seqNo if (!commit(tx, nullptr)) { @@ -1043,6 +1198,7 @@ bool TransactionStoreEvse::remove(Transaction& tx, unsigned int seqNo) { for (size_t i = 0; i < tx.seqNos.size(); i++) { if (tx.seqNos[i] == seqNo) { found = true; + break; } } if (!found) { @@ -1054,15 +1210,20 @@ bool TransactionStoreEvse::remove(Transaction& tx, unsigned int seqNo) { 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"); + if (!printTxEventFname(fn, sizeof(fn), evseId, tx.txNr, seqNo)) { + MO_DBG_ERR("fn error: %s", fn); + return false; + } + + char path [MO_MAX_PATH_SIZE]; + if (!FilesystemUtils::printPath(filesystem, path, sizeof(path), fn)) { + MO_DBG_ERR("path error: %s", fn); return false; } size_t msize; - if (filesystem->stat(fn, &msize) == 0) { - success &= filesystem->remove(fn); + if (filesystem->stat(path, &msize) == 0) { + success &= filesystem->remove(path); } else { MO_DBG_ERR("internal error: seqNos out of sync"); (void)0; @@ -1083,28 +1244,45 @@ bool TransactionStoreEvse::remove(Transaction& tx, unsigned int seqNo) { return success; } -TransactionStore::TransactionStore(std::shared_ptr filesystem, size_t numEvses) : - MemoryManaged{"v201.Transactions.TransactionStore"} { +v201::TransactionStore::TransactionStore(Context& context) : + MemoryManaged{"v201.Transactions.TransactionStore"}, context(context) { - 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++) { +v201::TransactionStore::~TransactionStore() { + for (unsigned int evseId = 0; evseId < numEvseId; evseId++) { delete evses[evseId]; } } -TransactionStoreEvse *TransactionStore::getEvse(unsigned int evseId) { - if (evseId >= MO_NUM_EVSEID) { +bool v201::TransactionStore::setup() { + + numEvseId = context.getModel201().getNumEvseId(); + for (unsigned int i = 0; i < numEvseId; i++) { + if (!getEvse(i) || !getEvse(i)->setup()) { + MO_DBG_ERR("initialization error"); + return false; + } + } + + return true; +} + +v201::TransactionStoreEvse *v201::TransactionStore::getEvse(unsigned int evseId) { + if (evseId >= numEvseId) { + MO_DBG_ERR("evseId out of bound"); return nullptr; } + + if (!evses[evseId]) { + evses[evseId] = new TransactionStoreEvse(context, evseId); + if (!evses[evseId]) { + MO_DBG_ERR("OOM"); + return nullptr; + } + } + return evses[evseId]; } -} //namespace Ocpp201 -} //namespace MicroOcpp - #endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Transactions/TransactionStore.h b/src/MicroOcpp/Model/Transactions/TransactionStore.h index c5d3421e..d156fb7b 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionStore.h +++ b/src/MicroOcpp/Model/Transactions/TransactionStore.h @@ -5,73 +5,69 @@ #ifndef MO_TRANSACTIONSTORE_H #define MO_TRANSACTIONSTORE_H -#include -#include #include +#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; +#if MO_OCPP_V16 -public: - ConnectorTransactionStore(TransactionStore& context, unsigned int connectorId, std::shared_ptr filesystem); - ConnectorTransactionStore(const ConnectorTransactionStore&) = delete; - ConnectorTransactionStore(ConnectorTransactionStore&&) = delete; - ConnectorTransactionStore& operator=(const ConnectorTransactionStore&) = delete; +#ifndef MO_STOPTXDATA_MAX_SIZE +#define MO_STOPTXDATA_MAX_SIZE 4 +#endif - ~ConnectorTransactionStore(); +#ifndef MO_STOPTXDATA_DIGITS +#define MO_STOPTXDATA_DIGITS 1 //digits needed to print MO_STOPTXDATA_MAX_SIZE-1 (="3", i.e. 1 digit) +#endif - bool commit(Transaction *transaction); +namespace MicroOcpp { - std::shared_ptr getTransaction(unsigned int txNr); - std::shared_ptr createTransaction(unsigned int txNr, bool silent = false); +class Context; - bool remove(unsigned int txNr); -}; +namespace v16 { -class TransactionStore : public MemoryManaged { -private: - Vector> connectors; -public: - TransactionStore(unsigned int nConnectors, std::shared_ptr filesystem); +class Transaction; - bool commit(Transaction *transaction); +namespace TransactionStore { - std::shared_ptr getTransaction(unsigned int connectorId, unsigned int txNr); - std::shared_ptr createTransaction(unsigned int connectorId, unsigned int txNr, bool silent = false); +bool printTxFname(char *fname, size_t size, unsigned int evseId, unsigned int txNr); - bool remove(unsigned int connectorId, unsigned int txNr); -}; +FilesystemUtils::LoadStatus load(MO_FilesystemAdapter *filesystem, Context& context, unsigned int evseId, unsigned int txNr, Transaction& transaction); +FilesystemUtils::StoreStatus store(MO_FilesystemAdapter *filesystem, Context& context, Transaction& transaction); +bool remove(MO_FilesystemAdapter *filesystem, unsigned int evseId, unsigned int txNr); -} +} //namespace TransactionStore +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_OCPP_V16 #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 +#ifndef MO_TXEVENTRECORD_SIZE +#define MO_TXEVENTRECORD_SIZE 10 //maximum number of of txEvents per tx to hold on flash storage +#endif + +#ifndef MO_TXEVENTRECORD_DIGITS +#define MO_TXEVENTRECORD_DIGITS 1 //digits needed to print MO_TXEVENTRECORD_SIZE-1 (="9", i.e. 1 digit) #endif namespace MicroOcpp { -namespace Ocpp201 { +class Context; + +namespace v201 { + +class Transaction; +class TransactionEventData; class TransactionStore; class TransactionStoreEvse : public MemoryManaged { private: - TransactionStore& txStore; + Context& context; const unsigned int evseId; - std::shared_ptr filesystem; + MO_FilesystemAdapter *filesystem = nullptr; bool serializeTransaction(Transaction& tx, JsonObject out); bool serializeTransactionEvent(TransactionEventData& txEvent, JsonObject out); @@ -80,10 +76,12 @@ class TransactionStoreEvse : public MemoryManaged { bool commit(Transaction& transaction, TransactionEventData *transactionEvent); + bool printTxEventFname(char *fname, size_t size, unsigned int evseId, unsigned int txNr, unsigned int seqNo); + public: - TransactionStoreEvse(TransactionStore& txStore, unsigned int evseId, std::shared_ptr filesystem); + TransactionStoreEvse(Context& context, unsigned int evseId); - bool discoverStoredTx(unsigned int& txNrBeginOut, unsigned int& txNrEndOut); + bool setup(); bool commit(Transaction *transaction); bool commit(TransactionEventData *transactionEvent); @@ -100,18 +98,21 @@ class TransactionStoreEvse : public MemoryManaged { class TransactionStore : public MemoryManaged { private: - TransactionStoreEvse *evses [MO_NUM_EVSEID] = {nullptr}; + Context& context; + TransactionStoreEvse* evses [MO_NUM_EVSEID] = {nullptr}; + unsigned int numEvseId = MO_NUM_EVSEID; public: - TransactionStore(std::shared_ptr filesystem, size_t numEvses); + TransactionStore(Context& context); ~TransactionStore(); + bool setup(); + TransactionStoreEvse *getEvse(unsigned int evseId); }; -} //namespace Ocpp201 +} //namespace v201 } //namespace MicroOcpp - #endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Variables/Variable.cpp b/src/MicroOcpp/Model/Variables/Variable.cpp index ee3f0229..fbfa91a2 100644 --- a/src/MicroOcpp/Model/Variables/Variable.cpp +++ b/src/MicroOcpp/Model/Variables/Variable.cpp @@ -6,17 +6,15 @@ * Implementation of the UCs B05 - B06 */ -#include - -#if MO_ENABLE_V201 - -#include - #include +#include #include +#if MO_ENABLE_V201 + using namespace MicroOcpp; +using namespace MicroOcpp::v201; ComponentId::ComponentId(const char *name) : name(name) { } ComponentId::ComponentId(const char *name, EvseId evse) : name(name), evse(evse) { } @@ -147,7 +145,7 @@ void Variable::setRebootRequired() { void Variable::setMutability(Mutability m) { this->mutability = m; } -Variable::Mutability Variable::getMutability() { +Mutability Variable::getMutability() { return mutability; } @@ -369,7 +367,7 @@ class VariableString : public Variable { } }; -std::unique_ptr MicroOcpp::makeVariable(Variable::InternalDataType dtype, Variable::AttributeTypeSet supportAttributes) { +std::unique_ptr MicroOcpp::v201::makeVariable(Variable::InternalDataType dtype, Variable::AttributeTypeSet supportAttributes) { switch(dtype) { case Variable::InternalDataType::Int: if (supportAttributes.count() > 1) { @@ -395,4 +393,4 @@ std::unique_ptr MicroOcpp::makeVariable(Variable::InternalDataType dty return nullptr; } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Variables/Variable.h b/src/MicroOcpp/Model/Variables/Variable.h index 79178303..1c5c576f 100644 --- a/src/MicroOcpp/Model/Variables/Variable.h +++ b/src/MicroOcpp/Model/Variables/Variable.h @@ -9,16 +9,16 @@ #ifndef MO_VARIABLE_H #define MO_VARIABLE_H -#include - -#if MO_ENABLE_V201 - #include #include #include -#include +#include +#include #include +#include + +#if MO_ENABLE_V201 #ifndef MO_VARIABLE_TYPECHECK #define MO_VARIABLE_TYPECHECK 1 @@ -26,6 +26,8 @@ namespace MicroOcpp { +namespace v201 { + // VariableCharacteristicsType (2.51) struct VariableCharacteristics : public MemoryManaged { @@ -147,13 +149,6 @@ class Variable : public MemoryManaged { 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, @@ -220,13 +215,13 @@ class Variable : public MemoryManaged { 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 +} //namespace v201 +} //namespace MicroOcpp +#endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Variables/VariableContainer.cpp b/src/MicroOcpp/Model/Variables/VariableContainer.cpp index 0d751d8f..7713fedf 100644 --- a/src/MicroOcpp/Model/Variables/VariableContainer.cpp +++ b/src/MicroOcpp/Model/Variables/VariableContainer.cpp @@ -6,18 +6,16 @@ * Implementation of the UCs B05 - B06 */ -#include - -#if MO_ENABLE_V201 +#include #include #include - -#include - #include +#if MO_ENABLE_V201 + using namespace MicroOcpp; +using namespace MicroOcpp::v201; VariableContainer::~VariableContainer() { @@ -105,7 +103,7 @@ bool VariableContainerOwning::add(std::unique_ptr variable) { return true; } -bool VariableContainerOwning::enablePersistency(std::shared_ptr filesystem, const char *filename) { +bool VariableContainerOwning::enablePersistency(MO_FilesystemAdapter *filesystem, const char *filename) { this->filesystem = filesystem; MO_FREE(this->filename); @@ -132,20 +130,24 @@ bool VariableContainerOwning::load() { 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(); + JsonDoc doc (0); + auto ret = FilesystemUtils::loadJson(filesystem, filename, doc, getMemoryTag()); + switch (ret) { + case FilesystemUtils::LoadStatus::Success: + break; //continue loading JSON + case FilesystemUtils::LoadStatus::FileNotFound: + MO_DBG_DEBUG("Populate FS: create variables file"); + return commit(); + case FilesystemUtils::LoadStatus::ErrOOM: + MO_DBG_ERR("OOM"); + return false; + case FilesystemUtils::LoadStatus::ErrFileCorruption: + case FilesystemUtils::LoadStatus::ErrOther: + MO_DBG_ERR("failed to load %s", filename); + return false; } - auto doc = FilesystemUtils::loadJson(filesystem, filename, getMemoryTag()); - if (!doc) { - MO_DBG_ERR("failed to load %s", filename); - return false; - } - - JsonArray variablesJson = (*doc)["variables"]; + JsonArray variablesJson = doc["variables"]; for (JsonObject stored : variablesJson) { @@ -243,7 +245,7 @@ bool VariableContainerOwning::commit() { JsonArray variablesJson = doc.createNestedArray("variables"); - for (size_t i = 0; i < variableCapacity; i++) { + for (size_t i = 0; i < variables.size(); i++) { auto& variable = *variables[i]; if (!variable.isPersistent()) { @@ -257,7 +259,7 @@ bool VariableContainerOwning::commit() { 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); @@ -280,8 +282,8 @@ bool VariableContainerOwning::commit() { } } - - bool success = FilesystemUtils::storeJson(filesystem, filename, doc); + auto ret = FilesystemUtils::storeJson(filesystem, filename, doc); + bool success = (ret == FilesystemUtils::StoreStatus::Success); if (success) { MO_DBG_DEBUG("Saving variables finished"); @@ -292,4 +294,4 @@ bool VariableContainerOwning::commit() { return success; } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Variables/VariableContainer.h b/src/MicroOcpp/Model/Variables/VariableContainer.h index d4dd0c69..9a5cd26c 100644 --- a/src/MicroOcpp/Model/Variables/VariableContainer.h +++ b/src/MicroOcpp/Model/Variables/VariableContainer.h @@ -9,17 +9,17 @@ #ifndef MO_VARIABLECONTAINER_H #define MO_VARIABLECONTAINER_H -#include - -#if MO_ENABLE_V201 - #include #include #include #include +#include + +#if MO_ENABLE_V201 namespace MicroOcpp { +namespace v201 { class VariableContainer { public: @@ -47,7 +47,7 @@ class VariableContainerNonOwning : public VariableContainer, public MemoryManage class VariableContainerOwning : public VariableContainer, public MemoryManaged { private: Vector> variables; - std::shared_ptr filesystem; + MO_FilesystemAdapter *filesystem = nullptr; char *filename = nullptr; uint16_t trackWriteCount = 0; @@ -65,12 +65,12 @@ class VariableContainerOwning : public VariableContainer, public MemoryManaged { bool add(std::unique_ptr variable); - bool enablePersistency(std::shared_ptr filesystem, const char *filename); + bool enablePersistency(MO_FilesystemAdapter *filesystem, const char *filename); bool load(); //load variables from flash bool commit() override; }; -} //end namespace MicroOcpp - +} //namespace v201 +} //namespace MicroOcpp #endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Variables/VariableService.cpp b/src/MicroOcpp/Model/Variables/VariableService.cpp index 9c0d6fde..77c07284 100644 --- a/src/MicroOcpp/Model/Variables/VariableService.cpp +++ b/src/MicroOcpp/Model/Variables/VariableService.cpp @@ -6,28 +6,30 @@ * Implementation of the UCs B05 - B06 */ -#include - -#if MO_ENABLE_V201 +#include +#include #include -#include +#include #include #include #include #include #include +#include -#include -#include +#if MO_ENABLE_V201 -#include +#ifndef MO_GETBASEREPORT_CHUNKSIZE +#define MO_GETBASEREPORT_CHUNKSIZE 4 +#endif namespace MicroOcpp { +namespace v201 { 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) { + component(component), name(name), userPtr(userPtr), validateFn(validateFn) { } @@ -121,34 +123,114 @@ Variable *VariableService::getVariable(const ComponentId& component, const char return nullptr; } -VariableService::VariableService(Context& context, std::shared_ptr filesystem) : +VariableService::VariableService(Context& context) : MemoryManaged("v201.Variables.VariableService"), - context(context), filesystem(filesystem), + context(context), containers(makeVector(getMemoryTag())), validatorInt(makeVector>(getMemoryTag())), validatorBool(makeVector>(getMemoryTag())), - validatorString(makeVector>(getMemoryTag())) { - + validatorString(makeVector>(getMemoryTag())), + getBaseReportVars(makeVector(getMemoryTag())) { + +} + +bool VariableService::init() { containers.reserve(MO_VARIABLESTORE_BUCKETS + 1); + if (containers.capacity() < MO_VARIABLESTORE_BUCKETS + 1) { + MO_DBG_ERR("OOM"); + return false; + } + + for (unsigned int i = 0; i < MO_VARIABLESTORE_BUCKETS; i++) { + containers.push_back(&containersInternal[i]); + } + containers.push_back(&containerExternal); + + return true; +} + +bool VariableService::setup() { + + filesystem = context.getFilesystem(); + if (!filesystem) { + MO_DBG_DEBUG("no fs access"); + } 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; + return false; } containersInternal[i].enablePersistency(filesystem, fn); - containers.push_back(&containersInternal[i]); + + if (!containersInternal[i].load()) { + MO_DBG_ERR("failure to load %"); + return false; + } } - 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);}); + context.getMessageService().registerOperation("SetVariables", [] (Context& context) -> Operation* { + return new SetVariables(*context.getModel201().getVariableService());}); + context.getMessageService().registerOperation("GetVariables", [] (Context& context) -> Operation* { + return new GetVariables(*context.getModel201().getVariableService());}); + context.getMessageService().registerOperation("GetBaseReport", [] (Context& context) -> Operation* { + return new GetBaseReport(*context.getModel201().getVariableService());}); + + return true; +} + +void VariableService::loop() { + + if (notifyReportInProgress) { + return; + } + + if (getBaseReportVars.empty()) { + // all done + return; + } + + auto variablesChunk = makeVector(getMemoryTag()); + variablesChunk.reserve(MO_GETBASEREPORT_CHUNKSIZE); + if (variablesChunk.capacity() < MO_GETBASEREPORT_CHUNKSIZE) { + MO_DBG_ERR("OOM"); + getBaseReportVars.clear(); + return; + } + + for (size_t i = 0; i < MO_GETBASEREPORT_CHUNKSIZE && !getBaseReportVars.empty(); i++) { + variablesChunk.push_back(getBaseReportVars.back()); + getBaseReportVars.pop_back(); + } + + auto notifyReport = makeRequest(context, new NotifyReport( + context, + getBaseReportRequestId, + context.getClock().now(), + !getBaseReportVars.empty(), // tbc: to be continued + getBaseReportSeqNo, + variablesChunk)); + + if (!notifyReport) { + MO_DBG_ERR("OOM"); + getBaseReportVars.clear(); + return; + } + + getBaseReportSeqNo++; + + notifyReport->setOnReceiveConf([this] (JsonObject) { + notifyReportInProgress = false; + }); + notifyReport->setOnAbort([this] () { + notifyReportInProgress = false; + }); + + notifyReportInProgress = true; + + context.getMessageService().sendRequestPreBoot(std::move(notifyReport)); } template @@ -171,8 +253,8 @@ bool loadVariableFactoryDefault(Variable& variable, const char *fac return variable.setString(factoryDef); } -void loadVariableCharacteristics(Variable& variable, Variable::Mutability mutability, bool persistent, bool rebootRequired, Variable::InternalDataType defaultDataType) { - if (variable.getMutability() == Variable::Mutability::ReadWrite) { +void loadVariableCharacteristics(Variable& variable, Mutability mutability, bool persistent, bool rebootRequired, Variable::InternalDataType defaultDataType) { + if (variable.getMutability() == Mutability::ReadWrite) { variable.setMutability(mutability); } @@ -186,13 +268,13 @@ void loadVariableCharacteristics(Variable& variable, Variable::Mutability mutabi switch (defaultDataType) { case Variable::InternalDataType::Int: - variable.setVariableDataType(MicroOcpp::VariableCharacteristics::DataType::integer); + variable.setVariableDataType(VariableCharacteristics::DataType::integer); break; case Variable::InternalDataType::Bool: - variable.setVariableDataType(MicroOcpp::VariableCharacteristics::DataType::boolean); + variable.setVariableDataType(VariableCharacteristics::DataType::boolean); break; case Variable::InternalDataType::String: - variable.setVariableDataType(MicroOcpp::VariableCharacteristics::DataType::string); + variable.setVariableDataType(VariableCharacteristics::DataType::string); break; default: MO_DBG_ERR("internal error"); @@ -208,7 +290,7 @@ template<> Variable::InternalDataType getInternalDataType() {return Variab 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) { +Variable *VariableService::declareVariable(const ComponentId& component, const char *name, T factoryDefault, Mutability mutability, bool persistent, Variable::AttributeTypeSet attributes, bool rebootRequired) { auto res = getVariable(component, name); if (!res) { @@ -236,9 +318,9 @@ Variable *VariableService::declareVariable(const ComponentId& component, const c 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); +template Variable *VariableService::declareVariable( const ComponentId&, const char*, int, Mutability, bool, Variable::AttributeTypeSet, bool); +template Variable *VariableService::declareVariable( const ComponentId&, const char*, bool, Mutability, bool, Variable::AttributeTypeSet, bool); +template Variable *VariableService::declareVariable(const ComponentId&, const char*, const char*, Mutability, bool, Variable::AttributeTypeSet, bool); bool VariableService::addVariable(Variable *variable) { return containerExternal.add(variable); @@ -248,18 +330,6 @@ 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; @@ -304,11 +374,11 @@ SetVariableStatus VariableService::setVariable(Variable::AttributeType attrType, if (foundComponent) { return SetVariableStatus::UnknownVariable; } else { - return SetVariableStatus::UnknownComponent; + return SetVariableStatus::UnknownComponent; } } - if (variable->getMutability() == Variable::Mutability::ReadOnly) { + if (variable->getMutability() == Mutability::ReadOnly) { return SetVariableStatus::Rejected; } @@ -424,7 +494,7 @@ GetVariableStatus VariableService::getVariable(Variable::AttributeType attrType, if (!strcmp(variable->getName(), variableName)) { // found variable. Search terminated in this block - if (variable->getMutability() == Variable::Mutability::WriteOnly) { + if (variable->getMutability() == Mutability::WriteOnly) { return GetVariableStatus::Rejected; } @@ -442,7 +512,7 @@ GetVariableStatus VariableService::getVariable(Variable::AttributeType attrType, if (foundComponent) { return GetVariableStatus::UnknownVariable; } else { - return GetVariableStatus::UnknownComponent; + return GetVariableStatus::UnknownComponent; } } @@ -452,7 +522,10 @@ GenericDeviceModelStatus VariableService::getBaseReport(int requestId, ReportBas return GenericDeviceModelStatus_NotSupported; } - Vector variables = makeVector(getMemoryTag()); + if (!getBaseReportVars.empty()) { + MO_DBG_ERR("request new base report while old is still pending"); + return GenericDeviceModelStatus_Rejected; + } for (size_t i = 0; i < containers.size(); i++) { auto container = containers[i]; @@ -460,31 +533,24 @@ GenericDeviceModelStatus VariableService::getBaseReport(int requestId, ReportBas for (size_t i = 0; i < container->size(); i++) { auto variable = container->getVariable(i); - if (reportBase == ReportBase_ConfigurationInventory && variable->getMutability() == Variable::Mutability::ReadOnly) { + if (reportBase == ReportBase_ConfigurationInventory && variable->getMutability() == Mutability::ReadOnly) { continue; } - variables.push_back(variable); + getBaseReportVars.push_back(variable); } } - if (variables.empty()) { + if (getBaseReportVars.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)); + getBaseReportRequestId = requestId; + getBaseReportSeqNo = 0; return GenericDeviceModelStatus_Accepted; } -} // namespace MicroOcpp - -#endif // MO_ENABLE_V201 +} //namespace v201 +} //namespace MicroOcpp +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Variables/VariableService.h b/src/MicroOcpp/Model/Variables/VariableService.h index afe19099..7ffe65ab 100644 --- a/src/MicroOcpp/Model/Variables/VariableService.h +++ b/src/MicroOcpp/Model/Variables/VariableService.h @@ -13,27 +13,34 @@ #include #include -#include - -#if MO_ENABLE_V201 - #include #include #include #include +#include + +#if MO_ENABLE_V201 #ifndef MO_VARIABLESTORE_FN_PREFIX -#define MO_VARIABLESTORE_FN_PREFIX (MO_FILENAME_PREFIX "ocpp-vars-") +#define MO_VARIABLESTORE_FN_PREFIX "ocpp-vars-" #endif #ifndef MO_VARIABLESTORE_FN_SUFFIX #define MO_VARIABLESTORE_FN_SUFFIX ".jsn" #endif +#ifndef MO_VARIABLESTORE_BUCKETS +#define MO_VARIABLESTORE_BUCKETS 8 +#endif + namespace MicroOcpp { +class Context; + +namespace v201 { + template -struct VariableValidator : public MemoryManaged { +struct VariableValidator { ComponentId component; const char *name; void *userPtr; @@ -42,16 +49,10 @@ struct VariableValidator : public MemoryManaged { 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; + MO_FilesystemAdapter *filesystem = nullptr; Vector containers; VariableContainerNonOwning containerExternal; VariableContainerOwning containersInternal [MO_VARIABLESTORE_BUCKETS]; @@ -64,12 +65,19 @@ class VariableService : public MemoryManaged { VariableValidator *getValidatorInt(const ComponentId& component, const char *name); VariableValidator *getValidatorBool(const ComponentId& component, const char *name); VariableValidator *getValidatorString(const ComponentId& component, const char *name); + + Vector getBaseReportVars; + int getBaseReportRequestId = -1; + unsigned int getBaseReportSeqNo = 0; + bool notifyReportInProgress = false; public: - VariableService(Context& context, std::shared_ptr filesystem); + VariableService(Context& context); + + bool init(); //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); + template + Variable *declareVariable(const ComponentId& component, const char *name, T factoryDefault, Mutability mutability = 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 @@ -77,14 +85,17 @@ class VariableService : public MemoryManaged { //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); + bool setup(); + + void loop(); + + bool commit(); + 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); @@ -92,8 +103,7 @@ class VariableService : public MemoryManaged { GenericDeviceModelStatus getBaseReport(int requestId, ReportBase reportBase); }; -} // namespace MicroOcpp - -#endif // MO_ENABLE_V201 - +} //namespace MicroOcpp +} //namespace v201 +#endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Operations/Authorize.cpp b/src/MicroOcpp/Operations/Authorize.cpp index e12050be..f71a601d 100644 --- a/src/MicroOcpp/Operations/Authorize.cpp +++ b/src/MicroOcpp/Operations/Authorize.cpp @@ -10,29 +10,28 @@ using namespace MicroOcpp; -namespace MicroOcpp { -namespace Ocpp16 { +#if MO_ENABLE_V16 -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); +v16::Authorize::Authorize(Model& model, const char *idTagIn) : MemoryManaged("v16.Operation.", "Authorize"), model(model) { + if (idTagIn && strnlen(idTagIn, MO_IDTAG_LEN_MAX + 2) <= MO_IDTAG_LEN_MAX) { + snprintf(idTag, MO_IDTAG_LEN_MAX + 1, "%s", idTagIn); } else { MO_DBG_WARN("Format violation of idTag. Discard idTag"); } } -const char* Authorize::getOperationType(){ +const char* v16::Authorize::getOperationType(){ return "Authorize"; } -std::unique_ptr Authorize::createReq() { - auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + (IDTAG_LEN_MAX + 1)); +std::unique_ptr v16::Authorize::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + (MO_IDTAG_LEN_MAX + 1)); JsonObject payload = doc->to(); payload["idTag"] = idTag; return doc; } -void Authorize::processConf(JsonObject payload){ +void v16::Authorize::processConf(JsonObject payload){ const char *idTagInfo = payload["idTagInfo"]["status"] | "not specified"; if (!strcmp(idTagInfo, "Accepted")) { @@ -48,37 +47,27 @@ void Authorize::processConf(JsonObject payload){ #endif //MO_ENABLE_LOCAL_AUTH } -void Authorize::processReq(JsonObject payload){ - /* - * Ignore Contents of this Req-message, because this is for debug purposes only - */ +#if MO_ENABLE_MOCK_SERVER +int v16::Authorize::writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData) { + (void)userStatus; + (void)userData; + return snprintf(buf, size, "{\"idTagInfo\":{\"status\":\"Accepted\"}}"); } +#endif //MO_ENABLE_MOCK_SERVER -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 +#endif //MO_ENABLE_V16 #if MO_ENABLE_V201 -namespace MicroOcpp { -namespace Ocpp201 { - -Authorize::Authorize(Model& model, const IdToken& idToken) : MemoryManaged("v201.Operation.Authorize"), model(model) { +v201::Authorize::Authorize(Model& model, const IdToken& idToken) : MemoryManaged("v201.Operation.Authorize"), model(model) { this->idToken = idToken; } -const char* Authorize::getOperationType(){ +const char* v201::Authorize::getOperationType(){ return "Authorize"; } -std::unique_ptr Authorize::createReq() { +std::unique_ptr v201::Authorize::createReq() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); @@ -88,7 +77,7 @@ std::unique_ptr Authorize::createReq() { return doc; } -void Authorize::processConf(JsonObject payload){ +void v201::Authorize::processConf(JsonObject payload){ const char *idTagInfo = payload["idTokenInfo"]["status"] | "_Undefined"; if (!strcmp(idTagInfo, "Accepted")) { @@ -102,21 +91,12 @@ void Authorize::processConf(JsonObject payload){ //} } -void Authorize::processReq(JsonObject payload){ - /* - * Ignore Contents of this Req-message, because this is for debug purposes only - */ +#if MO_ENABLE_MOCK_SERVER +int v201::Authorize::writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData) { + (void)userStatus; + (void)userData; + return snprintf(buf, size, "{\"idTokenInfo\":{\"status\":\"Accepted\"}}"); } - -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_MOCK_SERVER #endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/Authorize.h b/src/MicroOcpp/Operations/Authorize.h index b49226ef..86849140 100644 --- a/src/MicroOcpp/Operations/Authorize.h +++ b/src/MicroOcpp/Operations/Authorize.h @@ -2,23 +2,24 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef AUTHORIZE_H -#define AUTHORIZE_H +#ifndef MO_AUTHORIZE_H +#define MO_AUTHORIZE_H #include -#include +#include #include +#if MO_ENABLE_V16 + namespace MicroOcpp { +namespace v16 { class Model; -namespace Ocpp16 { - class Authorize : public Operation, public MemoryManaged { private: Model& model; - char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; + char idTag [MO_IDTAG_LEN_MAX + 1] = {'\0'}; public: Authorize(Model& model, const char *idTag); @@ -28,21 +29,25 @@ class Authorize : public Operation, public MemoryManaged { void processConf(JsonObject payload) override; - void processReq(JsonObject payload) override; - - std::unique_ptr createConf() override; +#if MO_ENABLE_MOCK_SERVER + static int writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData); +#endif }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V16 #if MO_ENABLE_V201 #include namespace MicroOcpp { -namespace Ocpp201 { +namespace v201 { + +class Model; class Authorize : public Operation, public MemoryManaged { private: @@ -57,14 +62,14 @@ class Authorize : public Operation, public MemoryManaged { void processConf(JsonObject payload) override; - void processReq(JsonObject payload) override; - - std::unique_ptr createConf() override; +#if MO_ENABLE_MOCK_SERVER + static int writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData); +#endif }; -} //end namespace Ocpp201 -} //end namespace MicroOcpp +} //namespace v201 +} //namespace MicroOcpp #endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/BootNotification.cpp b/src/MicroOcpp/Operations/BootNotification.cpp index 1f4b7c77..dcf69388 100644 --- a/src/MicroOcpp/Operations/BootNotification.cpp +++ b/src/MicroOcpp/Operations/BootNotification.cpp @@ -3,19 +3,22 @@ // MIT License #include + +#include #include #include -#include +#include #include #include #include -using MicroOcpp::Ocpp16::BootNotification; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 || MO_ENABLE_V201 + +using namespace MicroOcpp; + +BootNotification::BootNotification(Context& context, BootService& bootService, HeartbeatService *heartbeatService, const MO_BootNotificationData& bnData, const char *reason201) : MemoryManaged("v16/v201.Operation.", "BootNotification"), context(context), bootService(bootService), heartbeatService(heartbeatService), bnData(bnData), reason201(reason201), ocppVersion(context.getOcppVersion()) { -BootNotification::BootNotification(Model& model, std::unique_ptr payload) : MemoryManaged("v16.Operation.", "BootNotification"), model(model), credentials(std::move(payload)) { - } const char* BootNotification::getOperationType(){ @@ -23,27 +26,50 @@ const char* BootNotification::getOperationType(){ } 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; + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(9)); + JsonObject payload = doc->to(); + if (bnData.chargePointModel) {payload["chargePointModel"] = bnData.chargePointModel;} + if (bnData.chargePointVendor) {payload["chargePointVendor"] = bnData.chargePointVendor;} + if (bnData.firmwareVersion) {payload["firmwareVersion"] = bnData.firmwareVersion;} + if (bnData.chargePointSerialNumber) {payload["chargePointSerialNumber"] = bnData.chargePointSerialNumber;} + if (bnData.meterSerialNumber) {payload["meterSerialNumber"] = bnData.meterSerialNumber;} + if (bnData.meterType) {payload["meterType"] = bnData.meterType;} + if (bnData.chargeBoxSerialNumber) {payload["chargeBoxSerialNumber"] = bnData.chargeBoxSerialNumber;} + if (bnData.iccid) {payload["iccid"] = bnData.iccid;} + if (bnData.imsi) {payload["imsi"] = bnData.imsi;} + return doc; + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + payload["reason"] = reason201 ? reason201 : "Unknown"; + JsonObject chargingStation = payload.createNestedObject("chargingStation"); + if (bnData.chargePointSerialNumber) {chargingStation["serialNumber"] = bnData.chargePointSerialNumber;} + if (bnData.chargePointModel) {chargingStation["model"] = bnData.chargePointModel;} + if (bnData.chargePointVendor) {chargingStation["vendorName"] = bnData.chargePointVendor;} + if (bnData.firmwareVersion) {chargingStation["firmwareVersion"] = bnData.firmwareVersion;} + if (bnData.iccid || bnData.imsi) { + JsonObject modem = chargingStation.createNestedObject("modem"); + if (bnData.iccid) {modem["iccid"] = bnData.iccid;} + if (bnData.imsi) {modem["imsi"] = bnData.imsi;} } -#endif - return std::unique_ptr(new JsonDoc(*credentials)); - } else { - MO_DBG_ERR("payload undefined"); - return createEmptyDocument(); + return doc; } + #endif //MO_ENABLE_V201 + + MO_DBG_ERR("internal error"); + return createEmptyDocument(); } void BootNotification::processConf(JsonObject payload) { const char* currentTime = payload["currentTime"] | "Invalid"; if (strcmp(currentTime, "Invalid")) { - if (model.getClock().setTime(currentTime)) { + if (context.getClock().setTime(currentTime)) { //success } else { MO_DBG_ERR("Time string format violation. Expect format like 2022-02-01T20:53:32.486Z"); @@ -55,7 +81,7 @@ void BootNotification::processConf(JsonObject payload) { errorCode = "FormationViolation"; return; } - + int interval = payload["interval"] | -1; if (interval < 0) { errorCode = "FormationViolation"; @@ -63,62 +89,44 @@ void BootNotification::processConf(JsonObject payload) { } 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 (heartbeatService) { + heartbeatService->setHeartbeatInterval(interval); } } - if (auto bootService = model.getBootService()) { - - if (status != RegistrationStatus::Accepted) { - bootService->setRetryInterval(interval); - } - - bootService->notifyRegistrationStatus(status); + 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 - */ -} +#if MO_ENABLE_MOCK_SERVER +int BootNotification::writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData) { + (void)userStatus; -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; - } + auto& context = *reinterpret_cast(userData); - char ocppNowJson [JSONDATE_LENGTH + 1] = {'\0'}; - ocppSelect.toJsonString(ocppNowJson, JSONDATE_LENGTH + 1); - payload["currentTime"] = ocppNowJson; + char timeStr [MO_JSONDATE_SIZE]; - 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; + if (context.getClock().now().isUnixTime()) { + context.getClock().toJsonString(context.getClock().now(), timeStr, sizeof(timeStr)); + } else { + (void)snprintf(timeStr, sizeof(timeStr), "2025-05-18T18:55:13Z"); + } + + return snprintf(buf, size, "{\"currentTime\":\"%s\",\"interval\":60,\"status\":\"Accepted\"}", timeStr); } +#endif //MO_ENABLE_MOCK_SERVER + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/BootNotification.h b/src/MicroOcpp/Operations/BootNotification.h index c560e03f..774ae66f 100644 --- a/src/MicroOcpp/Operations/BootNotification.h +++ b/src/MicroOcpp/Operations/BootNotification.h @@ -5,27 +5,29 @@ #ifndef MO_BOOTNOTIFICATION_H #define MO_BOOTNOTIFICATION_H +#include #include -#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 +#if MO_ENABLE_V16 || MO_ENABLE_V201 namespace MicroOcpp { -class Model; - -namespace Ocpp16 { +class Context; +class BootService; +class HeartbeatService; class BootNotification : public Operation, public MemoryManaged { private: - Model& model; - std::unique_ptr credentials; + Context& context; + BootService& bootService; + HeartbeatService *heartbeatService = nullptr; + const MO_BootNotificationData& bnData; + const char *reason201 = nullptr; + int ocppVersion = -1; const char *errorCode = nullptr; public: - BootNotification(Model& model, std::unique_ptr payload); + BootNotification(Context& context, BootService& bootService, HeartbeatService *heartbeatService, const MO_BootNotificationData& bnData, const char *reason201); ~BootNotification() = default; @@ -35,14 +37,14 @@ class BootNotification : public Operation, public MemoryManaged { void processConf(JsonObject payload) override; - void processReq(JsonObject payload) override; - - std::unique_ptr createConf() override; - const char *getErrorCode() override {return errorCode;} + +#if MO_ENABLE_MOCK_SERVER + static int writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData); +#endif }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Operations/CancelReservation.cpp b/src/MicroOcpp/Operations/CancelReservation.cpp index 7741d9b9..6782ac79 100644 --- a/src/MicroOcpp/Operations/CancelReservation.cpp +++ b/src/MicroOcpp/Operations/CancelReservation.cpp @@ -4,17 +4,17 @@ #include -#if MO_ENABLE_RESERVATION +#if MO_ENABLE_V16 && MO_ENABLE_RESERVATION #include #include #include -using MicroOcpp::Ocpp16::CancelReservation; -using MicroOcpp::JsonDoc; +using namespace MicroOcpp; +using namespace MicroOcpp::v16; CancelReservation::CancelReservation(ReservationService& reservationService) : MemoryManaged("v16.Operation.", "CancelReservation"), reservationService(reservationService) { - + } const char* CancelReservation::getOperationType() { @@ -44,4 +44,4 @@ std::unique_ptr CancelReservation::createConf(){ return doc; } -#endif //MO_ENABLE_RESERVATION +#endif //MO_ENABLE_V16 && MO_ENABLE_RESERVATION diff --git a/src/MicroOcpp/Operations/CancelReservation.h b/src/MicroOcpp/Operations/CancelReservation.h index 0e39bb60..4610d8a9 100644 --- a/src/MicroOcpp/Operations/CancelReservation.h +++ b/src/MicroOcpp/Operations/CancelReservation.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_CANCELRESERVATION_H @@ -7,16 +7,15 @@ #include -#if MO_ENABLE_RESERVATION +#if MO_ENABLE_V16 && MO_ENABLE_RESERVATION #include namespace MicroOcpp { +namespace v16 { class ReservationService; -namespace Ocpp16 { - class CancelReservation : public Operation, public MemoryManaged { private: ReservationService& reservationService; @@ -34,8 +33,8 @@ class CancelReservation : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp -#endif //MO_ENABLE_RESERVATION +#endif //MO_ENABLE_V16 && MO_ENABLE_RESERVATION #endif diff --git a/src/MicroOcpp/Operations/ChangeAvailability.cpp b/src/MicroOcpp/Operations/ChangeAvailability.cpp index 7377657c..60e68835 100644 --- a/src/MicroOcpp/Operations/ChangeAvailability.cpp +++ b/src/MicroOcpp/Operations/ChangeAvailability.cpp @@ -3,24 +3,23 @@ // MIT License #include + +#include #include -#include -#include -#include +using namespace MicroOcpp; -namespace MicroOcpp { -namespace Ocpp16 { +#if MO_ENABLE_V16 -ChangeAvailability::ChangeAvailability(Model& model) : MemoryManaged("v16.Operation.", "ChangeAvailability"), model(model) { +v16::ChangeAvailability::ChangeAvailability(Model& model) : MemoryManaged("v16.Operation.", "ChangeAvailability"), model(model) { } -const char* ChangeAvailability::getOperationType(){ +const char* v16::ChangeAvailability::getOperationType(){ return "ChangeAvailability"; } -void ChangeAvailability::processReq(JsonObject payload) { +void v16::ChangeAvailability::processReq(JsonObject payload) { int connectorIdRaw = payload["connectorId"] | -1; if (connectorIdRaw < 0) { errorCode = "FormationViolation"; @@ -28,7 +27,7 @@ void ChangeAvailability::processReq(JsonObject payload) { } unsigned int connectorId = (unsigned int)connectorIdRaw; - if (connectorId >= model.getNumConnectors()) { + if (connectorId >= model.getNumEvseId()) { errorCode = "PropertyConstraintViolation"; return; } @@ -48,23 +47,29 @@ void ChangeAvailability::processReq(JsonObject payload) { } 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; + for (unsigned int eId = 0; eId < model.getNumEvseId(); eId++) { + auto availSvc = model.getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(eId) : nullptr; + if (availSvcEvse) { + availSvcEvse->setAvailability(available); + if (availSvcEvse->isOperative() && !available) { + scheduled = true; + } } } } else { - auto connector = model.getConnector(connectorId); - connector->setAvailability(available); - if (connector->isOperative() && !available) { - scheduled = true; + auto availSvc = model.getAvailabilityService(); + auto availSvcEvse = availSvc ? availSvc->getEvse(connectorId) : nullptr; + if (availSvcEvse) { + availSvcEvse->setAvailability(available); + if (availSvcEvse->isOperative() && !available) { + scheduled = true; + } } } } -std::unique_ptr ChangeAvailability::createConf(){ +std::unique_ptr v16::ChangeAvailability::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (!accepted) { @@ -74,32 +79,26 @@ std::unique_ptr ChangeAvailability::createConf(){ } else { payload["status"] = "Accepted"; } - + return doc; } -} // namespace Ocpp16 -} // namespace MicroOcpp +#endif //MO_ENABLE_V16 #if MO_ENABLE_V201 -#include - -namespace MicroOcpp { -namespace Ocpp201 { - -ChangeAvailability::ChangeAvailability(AvailabilityService& availabilityService) : MemoryManaged("v201.Operation.", "ChangeAvailability"), availabilityService(availabilityService) { +v201::ChangeAvailability::ChangeAvailability(AvailabilityService& availabilityService) : MemoryManaged("v201.Operation.", "ChangeAvailability"), availabilityService(availabilityService) { } -const char* ChangeAvailability::getOperationType(){ +const char* v201::ChangeAvailability::getOperationType(){ return "ChangeAvailability"; } -void ChangeAvailability::processReq(JsonObject payload) { +void v201::ChangeAvailability::processReq(JsonObject payload) { unsigned int evseId = 0; - + if (payload.containsKey("evse")) { int evseIdRaw = payload["evse"]["id"] | -1; if (evseIdRaw < 0) { @@ -136,7 +135,7 @@ void ChangeAvailability::processReq(JsonObject payload) { status = availabilityEvse->changeAvailability(operative); } -std::unique_ptr ChangeAvailability::createConf(){ +std::unique_ptr v201::ChangeAvailability::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); @@ -155,7 +154,4 @@ std::unique_ptr ChangeAvailability::createConf(){ return doc; } -} // namespace Ocpp201 -} // namespace MicroOcpp - #endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/ChangeAvailability.h b/src/MicroOcpp/Operations/ChangeAvailability.h index 08914e52..28c3e4a8 100644 --- a/src/MicroOcpp/Operations/ChangeAvailability.h +++ b/src/MicroOcpp/Operations/ChangeAvailability.h @@ -1,18 +1,20 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_CHANGEAVAILABILITY_H #define MO_CHANGEAVAILABILITY_H #include +#include + +#if MO_ENABLE_V16 namespace MicroOcpp { +namespace v16 { class Model; -namespace Ocpp16 { - class ChangeAvailability : public Operation, public MemoryManaged { private: Model& model; @@ -32,20 +34,21 @@ class ChangeAvailability : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V16 #if MO_ENABLE_V201 -#include +#include #include namespace MicroOcpp { +namespace v201 { class AvailabilityService; -namespace Ocpp201 { - class ChangeAvailability : public Operation, public MemoryManaged { private: AvailabilityService& availabilityService; @@ -64,8 +67,8 @@ class ChangeAvailability : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp201 -} //end namespace MicroOcpp +} //namespace v201 +} //namespace MicroOcpp #endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/ChangeConfiguration.cpp b/src/MicroOcpp/Operations/ChangeConfiguration.cpp index 79abaf29..8ab1034b 100644 --- a/src/MicroOcpp/Operations/ChangeConfiguration.cpp +++ b/src/MicroOcpp/Operations/ChangeConfiguration.cpp @@ -1,18 +1,20 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #include -#include +#include #include -#include //for tolower +#include //for tolower -using MicroOcpp::Ocpp16::ChangeConfiguration; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; + +ChangeConfiguration::ChangeConfiguration(ConfigurationService& configService) : MemoryManaged("v16.Operation.", "ChangeConfiguration"), configService(configService) { -ChangeConfiguration::ChangeConfiguration() : MemoryManaged("v16.Operation.", "ChangeConfiguration") { - } const char* ChangeConfiguration::getOperationType(){ @@ -35,122 +37,29 @@ void ChangeConfiguration::processReq(JsonObject payload) { 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; - } + status = configService.changeConfiguration(key, value); } 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"; + const char *statusStr = ""; + switch (status) { + case ConfigurationStatus::Accepted: + statusStr = "Accepted"; + break; + case ConfigurationStatus::Rejected: + statusStr = "Rejected"; + break; + case ConfigurationStatus::RebootRequired: + statusStr = "RebootRequired"; + break; + case ConfigurationStatus::NotSupported: + statusStr = "NotSupported"; + break; } + payload["status"] = statusStr; return doc; } + +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Operations/ChangeConfiguration.h b/src/MicroOcpp/Operations/ChangeConfiguration.h index 3699cf6d..6d14f90f 100644 --- a/src/MicroOcpp/Operations/ChangeConfiguration.h +++ b/src/MicroOcpp/Operations/ChangeConfiguration.h @@ -6,20 +6,24 @@ #define MO_CHANGECONFIGURATION_H #include +#include +#include + +#if MO_ENABLE_V16 namespace MicroOcpp { -namespace Ocpp16 { +namespace v16 { + +class ConfigurationService; class ChangeConfiguration : public Operation, public MemoryManaged { private: - bool reject = false; - bool rebootRequired = false; - bool readOnly = false; - bool notSupported = false; + ConfigurationService& configService; + ConfigurationStatus status = ConfigurationStatus::Rejected; const char *errorCode = nullptr; public: - ChangeConfiguration(); + ChangeConfiguration(ConfigurationService& configService); const char* getOperationType() override; @@ -31,6 +35,7 @@ class ChangeConfiguration : public Operation, public MemoryManaged { }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #endif diff --git a/src/MicroOcpp/Operations/CiStrings.h b/src/MicroOcpp/Operations/CiStrings.h deleted file mode 100644 index d52e176c..00000000 --- a/src/MicroOcpp/Operations/CiStrings.h +++ /dev/null @@ -1,25 +0,0 @@ -// 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 diff --git a/src/MicroOcpp/Operations/ClearCache.cpp b/src/MicroOcpp/Operations/ClearCache.cpp index 6666ae51..eac4257c 100644 --- a/src/MicroOcpp/Operations/ClearCache.cpp +++ b/src/MicroOcpp/Operations/ClearCache.cpp @@ -6,11 +6,13 @@ #include #include -using MicroOcpp::Ocpp16::ClearCache; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; + +ClearCache::ClearCache(MO_FilesystemAdapter *filesystem) : MemoryManaged("v16.Operation.", "ClearCache"), filesystem(filesystem) { -ClearCache::ClearCache(std::shared_ptr filesystem) : MemoryManaged("v16.Operation.", "ClearCache"), filesystem(filesystem) { - } const char* ClearCache::getOperationType(){ @@ -21,15 +23,16 @@ void ClearCache::processReq(JsonObject payload) { MO_DBG_WARN("Clear transaction log (Authorization Cache not supported)"); if (!filesystem) { - //no persistency - nothing to do + //no persistency - operation not actually supported, so reject + success = false; 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")); - }); + success = true; + + success &= FilesystemUtils::removeByPrefix(filesystem, "sd"); + success &= FilesystemUtils::removeByPrefix(filesystem, "tx"); + success &= FilesystemUtils::removeByPrefix(filesystem, "op"); } std::unique_ptr ClearCache::createConf(){ @@ -42,3 +45,5 @@ std::unique_ptr ClearCache::createConf(){ } return doc; } + +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Operations/ClearCache.h b/src/MicroOcpp/Operations/ClearCache.h index 110ab14a..2a03acac 100644 --- a/src/MicroOcpp/Operations/ClearCache.h +++ b/src/MicroOcpp/Operations/ClearCache.h @@ -7,16 +7,20 @@ #include #include +#include + +#if MO_ENABLE_V16 namespace MicroOcpp { -namespace Ocpp16 { + +namespace v16 { class ClearCache : public Operation, public MemoryManaged { private: - std::shared_ptr filesystem; - bool success = true; + MO_FilesystemAdapter *filesystem = nullptr; + bool success = false; public: - ClearCache(std::shared_ptr filesystem); + ClearCache(MO_FilesystemAdapter *filesystem); const char* getOperationType() override; @@ -25,6 +29,7 @@ class ClearCache : public Operation, public MemoryManaged { std::unique_ptr createConf() override; }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #endif diff --git a/src/MicroOcpp/Operations/ClearChargingProfile.cpp b/src/MicroOcpp/Operations/ClearChargingProfile.cpp index e99f9bd7..4b005a1b 100644 --- a/src/MicroOcpp/Operations/ClearChargingProfile.cpp +++ b/src/MicroOcpp/Operations/ClearChargingProfile.cpp @@ -1,17 +1,16 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #include #include #include -#include +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING -using MicroOcpp::Ocpp16::ClearChargingProfile; -using MicroOcpp::JsonDoc; +using namespace MicroOcpp; -ClearChargingProfile::ClearChargingProfile(SmartChargingService& scService) : MemoryManaged("v16.Operation.", "ClearChargingProfile"), scService(scService) { +ClearChargingProfile::ClearChargingProfile(SmartChargingService& scService, int ocppVersion) : MemoryManaged("v16.Operation.", "ClearChargingProfile"), scService(scService), ocppVersion(ocppVersion) { } @@ -21,62 +20,45 @@ const char* ClearChargingProfile::getOperationType(){ 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; - } - } + int chargingProfileId = -1; + int evseId = -1; + ChargingProfilePurposeType chargingProfilePurpose = ChargingProfilePurposeType::UNDEFINED; + int stackLevel = -1; - 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 MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + chargingProfileId = payload["id"] | -1; + evseId = payload["connectorId"] | -1; + chargingProfilePurpose = deserializeChargingProfilePurposeType(payload["chargingProfilePurpose"] | "_Undefined"); + stackLevel = payload["stackLevel"] | -1; + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + if (payload.containsKey("chargingProfileId")) { + chargingProfileId = payload["chargingProfileId"] | -1; + } else { + JsonObject chargingProfileCriteria = payload["chargingProfileCriteria"]; + evseId = chargingProfileCriteria["evseId"] | -1; + chargingProfilePurpose = deserializeChargingProfilePurposeType(chargingProfileCriteria["chargingProfilePurpose"] | "_Undefined"); + stackLevel = chargingProfileCriteria["stackLevel"] | -1; } + } + #endif //MO_ENABLE_V201 - if (payload.containsKey("stackLevel")) { - if (stackLevel != (payload["stackLevel"] | -1)) { - return false; - } - } - - return true; - }; - - matchingProfilesFound = scService.clearChargingProfile(filter); + bool matchingProfilesFound = scService.clearChargingProfile(chargingProfileId, evseId, chargingProfilePurpose, stackLevel); + if (matchingProfilesFound) { + status = "Accepted"; + } else { + status = "Unknown"; + } } 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"; - + payload["status"] = status; return doc; } + +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING diff --git a/src/MicroOcpp/Operations/ClearChargingProfile.h b/src/MicroOcpp/Operations/ClearChargingProfile.h index 40b7d210..cea9f755 100644 --- a/src/MicroOcpp/Operations/ClearChargingProfile.h +++ b/src/MicroOcpp/Operations/ClearChargingProfile.h @@ -6,28 +6,33 @@ #define MO_CLEARCHARGINGPROFILE_H #include +#include + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING namespace MicroOcpp { class SmartChargingService; -namespace Ocpp16 { - class ClearChargingProfile : public Operation, public MemoryManaged { private: SmartChargingService& scService; - bool matchingProfilesFound = false; + int ocppVersion = -1; + + const char *status = ""; + const char *errorCode = nullptr; public: - ClearChargingProfile(SmartChargingService& scService); + ClearChargingProfile(SmartChargingService& scService, int ocppVersion); 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 +} //namespace MicroOcpp +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING #endif diff --git a/src/MicroOcpp/Operations/CustomOperation.cpp b/src/MicroOcpp/Operations/CustomOperation.cpp index 317d01ef..c682ea1b 100644 --- a/src/MicroOcpp/Operations/CustomOperation.cpp +++ b/src/MicroOcpp/Operations/CustomOperation.cpp @@ -4,88 +4,195 @@ #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} { - -} +#include +#include + +using namespace MicroOcpp; + +namespace MicroOcpp { + +std::unique_ptr makeDeserializedJson(const char *memoryTag, const char *jsonString) { + std::unique_ptr doc; + + size_t capacity = MO_MAX_JSON_CAPACITY / 8; + DeserializationError err = DeserializationError::NoMemory; + + while (err == DeserializationError::NoMemory && capacity <= MO_MAX_JSON_CAPACITY) { + + doc = makeJsonDoc(memoryTag, capacity); + if (!doc) { + MO_DBG_ERR("OOM"); + return nullptr; + } + err = deserializeJson(*doc, jsonString); -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} { - + capacity *= 2; + } + + if (err) { + MO_DBG_ERR("JSON deserialization error: %s", err.c_str()); + return nullptr; + } + + return doc; } -CustomOperation::~CustomOperation() { +char *makeSerializedJsonString(const char *memoryTag, JsonObject json) { + char *str = nullptr; + size_t capacity = MO_MAX_JSON_CAPACITY / 8; + + while (!str && capacity <= MO_MAX_JSON_CAPACITY) { + str = static_cast(MO_MALLOC(memoryTag, capacity)); + if (!str) { + MO_DBG_ERR("OOM"); + break; + } + auto written = serializeJson(json, str, capacity); + + if (written >= 2 && written < capacity) { + //success + break; + } + + MO_FREE(str); + capacity *= 2; + + if (written <= 1) { + MO_DBG_ERR("serialization error"); + break; //don't retry + } + } + + if (!str) { + MO_DBG_ERR("could not serialize JSON"); + } + + return str; } -const char* CustomOperation::getOperationType() { - return operationType.c_str(); +void freeSerializedJsonString(char *str) { + MO_FREE(str); } -std::unique_ptr CustomOperation::createReq() { - return fn_createReq(); +} //namespace MicroOcpp + +CustomOperation::CustomOperation() : MemoryManaged("v16/v201.CustomOperation") { + } -void CustomOperation::processConf(JsonObject payload) { - return fn_processConf(payload); +CustomOperation::~CustomOperation() { + if (finally) { + finally(operationType, userStatus, userData); + finally = nullptr; + } + MO_FREE(request); + request = nullptr; } -bool CustomOperation::processErr(const char *code, const char *description, JsonObject details) { - if (fn_processErr) { - return fn_processErr(code, description, details); +bool CustomOperation::setupEvseInitiated(const char *operationType, const char *request, void (*onResponse)(const char *payloadJson, void *userData), void *userData) { + this->operationType = operationType; + MO_FREE(this->request); + this->request = nullptr; + if (request) { + size_t size = strlen(request) + 1; + this->request = static_cast(MO_MALLOC(getMemoryTag(), size)); + if (!this->request) { + MO_DBG_ERR("OOM"); + return false; + } + int ret = snprintf(this->request, size, "%s", request); + if (ret < 0 || (size_t)ret >= size) { + MO_DBG_ERR("snprintf: %i", ret); + MO_FREE(this->request); + return false; + } } + this->onResponse = onResponse; + this->userData = userData; + return true; } -void CustomOperation::processReq(JsonObject payload) { - return fn_processReq(payload); +//for operations receied from remote +bool CustomOperation::setupCsmsInitiated(const char *operationType, void (*onRequest)(const char *operationType, const char *payloadJson, void **userStatus, void *userData), int (*writeResponse)(const char *operationType, char *buf, size_t size, void *userStatus, void *userData), void (*finally)(const char *operationType, void *userStatus, void *userData), void *userData) { + this->operationType = operationType; + this->onRequest = onRequest; + this->writeResponse = writeResponse; + this->userData = userData; + this->finally = finally; + return true; } -std::unique_ptr CustomOperation::createConf() { - return fn_createConf(); +const char *CustomOperation::getOperationType() { + return operationType; } -const char *CustomOperation::getErrorCode() { - if (fn_getErrorCode) { - return fn_getErrorCode(); - } else { - return nullptr; +std::unique_ptr CustomOperation::createReq() { + return makeDeserializedJson(getMemoryTag(), request ? request : "{}"); +} + +void CustomOperation::processConf(JsonObject payload) { + if (!onResponse) { + return; } + char *jsonString = makeSerializedJsonString(getMemoryTag(), payload); + if (!jsonString) { + MO_DBG_ERR("serialization error"); + return; + } + onResponse(jsonString, userData); + + freeSerializedJsonString(jsonString); } -const char *CustomOperation::getErrorDescription() { - if (fn_getErrorDescription) { - return fn_getErrorDescription(); - } else { - return ""; +void CustomOperation::processReq(JsonObject payload) { + if (!onRequest) { + return; } + char *jsonString = makeSerializedJsonString(getMemoryTag(), payload); + if (!jsonString) { + MO_DBG_ERR("serialization error"); + return; + } + onRequest(operationType, jsonString, &userStatus, userData); + + freeSerializedJsonString(jsonString); } -std::unique_ptr CustomOperation::getErrorDetails() { - if (fn_getErrorDetails) { - return fn_getErrorDetails(); - } else { +std::unique_ptr CustomOperation::createConf() { + if (!writeResponse) { return createEmptyDocument(); } + + char *str = nullptr; + + size_t capacity = MO_MAX_JSON_CAPACITY / 8; + + while (!str && capacity <= MO_MAX_JSON_CAPACITY) { + str = static_cast(MO_MALLOC(getMemoryTag(), capacity)); + if (!str) { + MO_DBG_ERR("OOM"); + break; + } + + auto ret = writeResponse(operationType, str, capacity, userStatus, userData); + if (ret >= 0 && ret < 1) { + MO_DBG_ERR("cannot process empty string"); + } else if (ret >= 0 && (size_t)ret < capacity) { + //success + break; + } + + MO_FREE(str); + capacity *= 2; + } + + if (!str) { + MO_DBG_ERR("failure to create conf"); + } + + auto ret = makeDeserializedJson(getMemoryTag(), str); + freeSerializedJsonString(str); + return ret; } diff --git a/src/MicroOcpp/Operations/CustomOperation.h b/src/MicroOcpp/Operations/CustomOperation.h index 02f72e88..0f2272e9 100644 --- a/src/MicroOcpp/Operations/CustomOperation.h +++ b/src/MicroOcpp/Operations/CustomOperation.h @@ -7,40 +7,40 @@ #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 + const char *operationType = nullptr; + void *userData = nullptr; + void *userStatus = nullptr; //`onRequest` cb can write this number and pass status from `onRequest` to `writeResponse` + + //Operation initiated on this EVSE + char *request = nullptr; + void (*onResponse)(const char *payloadJson, void *userData) = nullptr; + + //Operation initiated by server + void (*onRequest)(const char *operationType, const char *payloadJson, void **userStatus, void *userData) = nullptr; + int (*writeResponse)(const char *operationType, char *buf, size_t size, void *userStatus, void *userData) = nullptr; + void (*finally)(const char *operationType, void *userStatus, void *userData) = nullptr; public: + CustomOperation(); + + ~CustomOperation(); + //for operations initiated at this device - CustomOperation(const char *operationType, - std::function ()> fn_createReq, - std::function fn_processConf, - std::function fn_processErr = nullptr); - + bool setupEvseInitiated(const char *operationType, + const char *request, + void (*onResponse)(const char *payloadJson, void *userData), + void *userData); + //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(); + bool setupCsmsInitiated(const char *operationType, + void (*onRequest)(const char *operationType, const char *payloadJson, void **userStatus, void *userData), + int (*writeResponse)(const char *operationType, char *buf, size_t size, void *userStatus, void *userData), + void (*finally)(const char *operationType, void *userStatus, void *userData), + void *userData); const char* getOperationType() override; @@ -48,16 +48,10 @@ class CustomOperation : public Operation, public MemoryManaged { 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 +} //namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Operations/DataTransfer.cpp b/src/MicroOcpp/Operations/DataTransfer.cpp index 5c605039..f4b5c3ad 100644 --- a/src/MicroOcpp/Operations/DataTransfer.cpp +++ b/src/MicroOcpp/Operations/DataTransfer.cpp @@ -5,15 +5,35 @@ #include #include -using MicroOcpp::Ocpp16::DataTransfer; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 -DataTransfer::DataTransfer() : MemoryManaged("v16.Operation.", "DataTransfer") { +using namespace MicroOcpp; +using namespace MicroOcpp::v16; +DataTransfer::DataTransfer(const char *msg) : MemoryManaged("v16.Operation.", "DataTransfer") { + if (msg) { + size_t size = strlen(msg); + this->msg = static_cast(MO_MALLOC(getMemoryTag(), size)); + if (this->msg) { + memset(this->msg, 0, size); + auto ret = snprintf(this->msg, size, "%s", msg); + if (ret < 0 || (size_t)ret >= size) { + MO_DBG_ERR("snprintf: %i", ret); + MO_FREE(this->msg); + this->msg = nullptr; + //send empty string + } + //success + } else { + MO_DBG_ERR("OOM"); + //send empty string + } + } } -DataTransfer::DataTransfer(const String &msg) : MemoryManaged("v16.Operation.", "DataTransfer"), msg{makeString(getMemoryTag(), msg.c_str())} { - +DataTransfer::~DataTransfer() { + MO_FREE(msg); + msg = nullptr; } const char* DataTransfer::getOperationType(){ @@ -21,10 +41,10 @@ const char* DataTransfer::getOperationType(){ } std::unique_ptr DataTransfer::createReq() { - auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2) + (msg.length() + 1)); + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2)); JsonObject payload = doc->to(); payload["vendorId"] = "CustomVendor"; - payload["data"] = msg; + payload["data"] = msg ? (const char*)msg : ""; return doc; } @@ -48,3 +68,5 @@ std::unique_ptr DataTransfer::createConf(){ payload["status"] = "Rejected"; return doc; } + +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Operations/DataTransfer.h b/src/MicroOcpp/Operations/DataTransfer.h index f7967202..3a64c4dc 100644 --- a/src/MicroOcpp/Operations/DataTransfer.h +++ b/src/MicroOcpp/Operations/DataTransfer.h @@ -6,16 +6,19 @@ #define MO_DATATRANSFER_H #include +#include + +#if MO_ENABLE_V16 namespace MicroOcpp { -namespace Ocpp16 { +namespace v16 { class DataTransfer : public Operation, public MemoryManaged { private: - String msg; + char *msg = nullptr; public: - DataTransfer(); - DataTransfer(const String &msg); + DataTransfer(const char *msg = nullptr); + ~DataTransfer(); const char* getOperationType() override; @@ -26,9 +29,10 @@ class DataTransfer : public Operation, public MemoryManaged { void processReq(JsonObject payload) override; std::unique_ptr createConf() override; - + }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #endif diff --git a/src/MicroOcpp/Operations/DeleteCertificate.cpp b/src/MicroOcpp/Operations/DeleteCertificate.cpp index b7a7267a..30f6da07 100644 --- a/src/MicroOcpp/Operations/DeleteCertificate.cpp +++ b/src/MicroOcpp/Operations/DeleteCertificate.cpp @@ -3,15 +3,13 @@ // MIT License #include - -#if MO_ENABLE_CERT_MGMT - #include #include #include -using MicroOcpp::Ocpp201::DeleteCertificate; -using MicroOcpp::JsonDoc; +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT + +using namespace MicroOcpp; DeleteCertificate::DeleteCertificate(CertificateService& certService) : MemoryManaged("v201.Operation.", "DeleteCertificate"), certService(certService) { @@ -51,9 +49,9 @@ void DeleteCertificate::processReq(JsonObject payload) { 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"); + auto retIN = mo_cert_set_issuerNameHash(&cert, certIdJson["issuerNameHash"] | "_Invalid", cert.hashAlgorithm); + auto retIK = mo_cert_set_issuerKeyHash(&cert, certIdJson["issuerKeyHash"] | "_Invalid", cert.hashAlgorithm); + auto retSN = mo_cert_set_serialNumber(&cert, certIdJson["serialNumber"] | "_Invalid"); if (retIN < 0 || retIK < 0 || retSN < 0) { errorCode = "FormationViolation"; return; @@ -93,4 +91,4 @@ std::unique_ptr DeleteCertificate::createConf(){ return doc; } -#endif //MO_ENABLE_CERT_MGMT +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT diff --git a/src/MicroOcpp/Operations/DeleteCertificate.h b/src/MicroOcpp/Operations/DeleteCertificate.h index 4c07014b..946f43ac 100644 --- a/src/MicroOcpp/Operations/DeleteCertificate.h +++ b/src/MicroOcpp/Operations/DeleteCertificate.h @@ -5,18 +5,15 @@ #ifndef MO_DELETECERTIFICATE_H #define MO_DELETECERTIFICATE_H +#include #include -#if MO_ENABLE_CERT_MGMT - -#include +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT namespace MicroOcpp { class CertificateService; -namespace Ocpp201 { - class DeleteCertificate : public Operation, public MemoryManaged { private: CertificateService& certService; @@ -34,8 +31,7 @@ class DeleteCertificate : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp201 -} //end namespace MicroOcpp +} //namespace MicroOcpp -#endif //MO_ENABLE_CERT_MGMT +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT #endif diff --git a/src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp b/src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp index 8fc99913..14ff2eda 100644 --- a/src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp +++ b/src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp @@ -3,42 +3,28 @@ // MIT License #include -#include +#include #include #include -using MicroOcpp::Ocpp16::DiagnosticsStatusNotification; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 && MO_ENABLE_DIAGNOSTICS + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; 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); + payload["status"] = serializeDiagnosticsStatus(status); return doc; } void DiagnosticsStatusNotification::processConf(JsonObject payload){ // no payload, nothing to do } + +#endif //MO_ENABLE_V16 && MO_ENABLE_DIAGNOSTICS diff --git a/src/MicroOcpp/Operations/DiagnosticsStatusNotification.h b/src/MicroOcpp/Operations/DiagnosticsStatusNotification.h index e21c8c07..21ccb6be 100644 --- a/src/MicroOcpp/Operations/DiagnosticsStatusNotification.h +++ b/src/MicroOcpp/Operations/DiagnosticsStatusNotification.h @@ -2,19 +2,21 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include -#include - #ifndef MO_DIAGNOSTICSSTATUSNOTIFICATION_H #define MO_DIAGNOSTICSSTATUSNOTIFICATION_H +#include +#include +#include + +#if MO_ENABLE_V16 && MO_ENABLE_DIAGNOSTICS + namespace MicroOcpp { -namespace Ocpp16 { +namespace v16 { class DiagnosticsStatusNotification : public Operation, public MemoryManaged { private: DiagnosticsStatus status = DiagnosticsStatus::Idle; - static const char *cstrFromStatus(DiagnosticsStatus status); public: DiagnosticsStatusNotification(DiagnosticsStatus status); @@ -23,10 +25,9 @@ class DiagnosticsStatusNotification : public Operation, public MemoryManaged { std::unique_ptr createReq() override; void processConf(JsonObject payload) override; - }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp - +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_DIAGNOSTICS #endif diff --git a/src/MicroOcpp/Operations/FirmwareStatusNotification.cpp b/src/MicroOcpp/Operations/FirmwareStatusNotification.cpp index 0b52c56f..dff71316 100644 --- a/src/MicroOcpp/Operations/FirmwareStatusNotification.cpp +++ b/src/MicroOcpp/Operations/FirmwareStatusNotification.cpp @@ -3,12 +3,14 @@ // MIT License #include -#include +#include #include #include -using MicroOcpp::Ocpp16::FirmwareStatusNotification; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; FirmwareStatusNotification::FirmwareStatusNotification(FirmwareStatus status) : MemoryManaged("v16.Operation.", "FirmwareStatusNotification"), status{status} { @@ -51,3 +53,5 @@ std::unique_ptr FirmwareStatusNotification::createReq() { void FirmwareStatusNotification::processConf(JsonObject payload){ // no payload, nothing to do } + +#endif //MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT diff --git a/src/MicroOcpp/Operations/FirmwareStatusNotification.h b/src/MicroOcpp/Operations/FirmwareStatusNotification.h index 026ea45b..eefac7fb 100644 --- a/src/MicroOcpp/Operations/FirmwareStatusNotification.h +++ b/src/MicroOcpp/Operations/FirmwareStatusNotification.h @@ -2,15 +2,17 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include +#ifndef MO_FIRMWARESTATUSNOTIFICATION_H +#define MO_FIRMWARESTATUSNOTIFICATION_H +#include #include +#include -#ifndef MO_FIRMWARESTATUSNOTIFICATION_H -#define MO_FIRMWARESTATUSNOTIFICATION_H +#if MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT namespace MicroOcpp { -namespace Ocpp16 { +namespace v16 { class FirmwareStatusNotification : public Operation, public MemoryManaged { private: @@ -24,10 +26,9 @@ class FirmwareStatusNotification : public Operation, public MemoryManaged { std::unique_ptr createReq() override; void processConf(JsonObject payload) override; - }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp - +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT #endif diff --git a/src/MicroOcpp/Operations/GetBaseReport.cpp b/src/MicroOcpp/Operations/GetBaseReport.cpp index 95414481..080cf34c 100644 --- a/src/MicroOcpp/Operations/GetBaseReport.cpp +++ b/src/MicroOcpp/Operations/GetBaseReport.cpp @@ -2,19 +2,17 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_V201 - #include #include #include -using MicroOcpp::Ocpp201::GetBaseReport; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V201 + +using namespace MicroOcpp; +using namespace MicroOcpp::v201; GetBaseReport::GetBaseReport(VariableService& variableService) : MemoryManaged("v201.Operation.", "GetBaseReport"), variableService(variableService) { - + } const char* GetBaseReport::getOperationType(){ @@ -78,4 +76,4 @@ std::unique_ptr GetBaseReport::createConf(){ return doc; } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/GetBaseReport.h b/src/MicroOcpp/Operations/GetBaseReport.h index f23b7e81..a284901c 100644 --- a/src/MicroOcpp/Operations/GetBaseReport.h +++ b/src/MicroOcpp/Operations/GetBaseReport.h @@ -5,20 +5,18 @@ #ifndef MO_GETBASEREPORT_H #define MO_GETBASEREPORT_H -#include - -#if MO_ENABLE_V201 - #include #include #include +#include + +#if MO_ENABLE_V201 namespace MicroOcpp { +namespace v201 { class VariableService; -namespace Ocpp201 { - class GetBaseReport : public Operation, public MemoryManaged { private: VariableService& variableService; @@ -39,9 +37,7 @@ class GetBaseReport : public Operation, public MemoryManaged { }; -} //namespace Ocpp201 +} //namespace v201 } //namespace MicroOcpp - #endif //MO_ENABLE_V201 - #endif diff --git a/src/MicroOcpp/Operations/GetCompositeSchedule.cpp b/src/MicroOcpp/Operations/GetCompositeSchedule.cpp index 760e0e2f..b12ed4b1 100644 --- a/src/MicroOcpp/Operations/GetCompositeSchedule.cpp +++ b/src/MicroOcpp/Operations/GetCompositeSchedule.cpp @@ -3,14 +3,17 @@ // MIT License #include + +#include #include #include #include -using MicroOcpp::Ocpp16::GetCompositeSchedule; -using MicroOcpp::JsonDoc; +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING + +using namespace MicroOcpp; -GetCompositeSchedule::GetCompositeSchedule(Model& model, SmartChargingService& scService) : MemoryManaged("v16.Operation.", "GetCompositeSchedule"), model(model), scService(scService) { +GetCompositeSchedule::GetCompositeSchedule(Context& context, SmartChargingService& scService) : MemoryManaged("v16.Operation.", "GetCompositeSchedule"), context(context), scService(scService), ocppVersion(context.getOcppVersion()) { } @@ -20,24 +23,34 @@ const char* GetCompositeSchedule::getOperationType() { void GetCompositeSchedule::processReq(JsonObject payload) { - connectorId = payload["connectorId"] | -1; - duration = payload["duration"] | 0; + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + evseId = payload["connectorId"] | -1; + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + evseId = payload["evseId"] | -1; + } + #endif //MO_ENABLE_V201 + + duration = payload["duration"] | -1; - if (connectorId < 0 || !payload.containsKey("duration")) { + if (evseId < 0 || duration < 0) { errorCode = "FormationViolation"; return; } - if ((unsigned int) connectorId >= model.getNumConnectors()) { + if ((unsigned int) evseId >= context.getModelCommon().getNumEvseId()) { 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; + if (payload.containsKey("chargingRateUnit")) { + chargingRateUnit = deserializeChargingRateUnitType(payload["chargingRateUnit"] | "_Invalid"); + if (chargingRateUnit == ChargingRateUnitType::UNDEFINED) { + errorCode = "FormationViolation"; + return; + } } } @@ -45,27 +58,46 @@ std::unique_ptr GetCompositeSchedule::createConf(){ bool success = false; - auto chargingSchedule = scService.getCompositeSchedule((unsigned int) connectorId, duration, chargingRateUnit); - JsonDoc chargingScheduleDoc {0}; + auto chargingSchedule = scService.getCompositeSchedule((unsigned int) evseId, duration, chargingRateUnit); + JsonDoc chargingScheduleDoc (0); if (chargingSchedule) { - success = chargingSchedule->toJson(chargingScheduleDoc); + auto capacity = chargingSchedule->getJsonCapacity(ocppVersion, /* compositeSchedule */ true); + chargingScheduleDoc = initJsonDoc(getMemoryTag(), capacity); + success = chargingSchedule->toJson(context.getClock(), ocppVersion, /* compositeSchedule */ true, chargingScheduleDoc.to()); } - char scheduleStart_str [JSONDATE_LENGTH + 1] = {'\0'}; - - if (success && chargingSchedule) { - success = chargingSchedule->startSchedule.toJsonString(scheduleStart_str, JSONDATE_LENGTH + 1); + char scheduleStartStr [MO_JSONDATE_SIZE] = {'\0'}; + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + if (success && chargingSchedule) { + Timestamp scheduleStart; + if (context.getClock().fromUnixTime(scheduleStart, chargingSchedule->startSchedule)) { + success = context.getClock().toJsonString(scheduleStart, scheduleStartStr, sizeof(scheduleStartStr)); + } else { + success = false; + } + } } + #endif //MO_ENABLE_V16 if (success && chargingSchedule) { auto doc = makeJsonDoc(getMemoryTag(), - JSON_OBJECT_SIZE(4) + + (ocppVersion == MO_OCPP_V16 ? + JSON_OBJECT_SIZE(4) + MO_JSONDATE_SIZE : + JSON_OBJECT_SIZE(2)) + chargingScheduleDoc.memoryUsage()); JsonObject payload = doc->to(); payload["status"] = "Accepted"; - payload["connectorId"] = connectorId; - payload["scheduleStart"] = scheduleStart_str; + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + payload["connectorId"] = evseId; + if (ocppVersion == MO_OCPP_V16) { + payload["scheduleStart"] = scheduleStartStr; + } + } + #endif //MO_ENABLE_V16 payload["chargingSchedule"] = chargingScheduleDoc; return doc; } else { @@ -75,3 +107,5 @@ std::unique_ptr GetCompositeSchedule::createConf(){ return doc; } } + +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING diff --git a/src/MicroOcpp/Operations/GetCompositeSchedule.h b/src/MicroOcpp/Operations/GetCompositeSchedule.h index 30e427c4..7f748e9b 100644 --- a/src/MicroOcpp/Operations/GetCompositeSchedule.h +++ b/src/MicroOcpp/Operations/GetCompositeSchedule.h @@ -6,26 +6,28 @@ #define MO_GETCOMPOSITESCHEDULE_H #include -#include -#include +#include +#include -namespace MicroOcpp { +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING -class Model; +namespace MicroOcpp { -namespace Ocpp16 { +class Context; +class SmartChargingService; class GetCompositeSchedule : public Operation, public MemoryManaged { private: - Model& model; + Context& context; SmartChargingService& scService; - int connectorId = -1; + int ocppVersion = -1; + int evseId = -1; int duration = -1; - ChargingRateUnitType_Optional chargingRateUnit = ChargingRateUnitType_Optional::None; + ChargingRateUnitType chargingRateUnit = ChargingRateUnitType::UNDEFINED; const char *errorCode {nullptr}; public: - GetCompositeSchedule(Model& model, SmartChargingService& scService); + GetCompositeSchedule(Context& context, SmartChargingService& scService); const char* getOperationType() override; @@ -36,6 +38,6 @@ class GetCompositeSchedule : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace MicroOcpp +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING #endif diff --git a/src/MicroOcpp/Operations/GetConfiguration.cpp b/src/MicroOcpp/Operations/GetConfiguration.cpp index 8146bba1..2cbfc64e 100644 --- a/src/MicroOcpp/Operations/GetConfiguration.cpp +++ b/src/MicroOcpp/Operations/GetConfiguration.cpp @@ -3,14 +3,27 @@ // MIT License #include -#include +#include #include -using MicroOcpp::Ocpp16::GetConfiguration; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 -GetConfiguration::GetConfiguration() : MemoryManaged("v16.Operation.", "GetConfiguration"), keys{makeVector(getMemoryTag())} { +using namespace MicroOcpp; +using namespace MicroOcpp::v16; +GetConfiguration::GetConfiguration(ConfigurationService& configService) : + MemoryManaged("v16.Operation.", "GetConfiguration"), + configService(configService), + configurations(makeVector(getMemoryTag())), + unknownKeys(makeVector(getMemoryTag())) { + +} + +GetConfiguration::~GetConfiguration() { + for (size_t i = 0; i < unknownKeys.size(); i++) { + MO_FREE(unknownKeys[i]); + unknownKeys[i] = nullptr; + } } const char* GetConfiguration::getOperationType(){ @@ -19,71 +32,66 @@ const char* GetConfiguration::getOperationType(){ 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())); + JsonArray keysArray = payload["key"]; + const char **keys = nullptr; + size_t keysSize = keysArray.size(); + if (keysSize > 0) { + keys = static_cast(MO_MALLOC(getMemoryTag(), sizeof(const char**) * keysSize)); + if (!keys) { + MO_DBG_ERR("OOM"); + errorCode = "InternalError"; + errorDescription = "Query too big. Try fewer keys"; + goto exit; + } + memset(keys, 0, sizeof(const char**) * keysSize); } -} -std::unique_ptr GetConfiguration::createConf(){ + for (size_t i = 0; i < keysArray.size(); i++) { + const char *key = keysArray[i].as(); + if (!key || !*key) { + errorCode = "FormationViolation"; + goto exit; + } - Vector configurations = makeVector(getMemoryTag()); - Vector unknownKeys = makeVector(getMemoryTag()); + keys[i] = key; + } - auto containers = getConfigurationContainersPublic(); + if (!configService.getConfiguration(keys, keysSize, configurations, unknownKeys)) { + errorCode = "InternalError"; + errorDescription = "Query too big. Try fewer keys"; + goto exit; + } - 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; - } - } +exit: + MO_FREE(keys); + keys = nullptr; + keysSize = 0; +} - if (res && res->isReadable()) { - configurations.push_back(res); - } else { - unknownKeys.push_back(key.c_str()); - } - } - } +std::unique_ptr GetConfiguration::createConf(){ - #define VALUE_BUFSIZE 30 + const size_t 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) { + for (size_t i = 0; i < configurations.size(); i++) { + auto config = configurations[i]; //need to store ints by copied string: measure necessary capacity - if (config->getType() == TConfig::Int) { + if (config->getType() == Configuration::Type::Int) { char vbuf [VALUE_BUFSIZE]; - auto ret = snprintf(vbuf, VALUE_BUFSIZE, "%i", config->getInt()); - if (ret < 0 || ret >= VALUE_BUFSIZE) { + auto ret = snprintf(vbuf, sizeof(vbuf), "%i", config->getInt()); + if (ret < 0 || (size_t)ret >= sizeof(vbuf)) { + MO_DBG_ERR("snprintf: %i", ret); continue; } - jcapacity += ret + 1; + jcapacity += (size_t)ret + 1; } } jcapacity += JSON_ARRAY_SIZE(unknownKeys.size()); - + MO_DBG_DEBUG("GetConfiguration capacity: %zu", jcapacity); std::unique_ptr doc; @@ -103,32 +111,33 @@ std::unique_ptr GetConfiguration::createConf(){ } JsonObject payload = doc->to(); - + JsonArray jsonConfigurationKey = payload.createNestedArray("configurationKey"); - for (auto config : configurations) { + for (size_t i = 0; i < configurations.size(); i++) { + auto config = configurations[i]; char vbuf [VALUE_BUFSIZE]; const char *v = ""; switch (config->getType()) { - case TConfig::Int: { + case Configuration::Type::Int: { auto ret = snprintf(vbuf, VALUE_BUFSIZE, "%i", config->getInt()); - if (ret < 0 || ret >= VALUE_BUFSIZE) { + if (ret < 0 || (size_t)ret >= VALUE_BUFSIZE) { MO_DBG_ERR("value error"); continue; } v = vbuf; break; } - case TConfig::Bool: + case Configuration::Type::Bool: v = config->getBool() ? "true" : "false"; break; - case TConfig::String: + case Configuration::Type::String: v = config->getString(); break; } JsonObject jconfig = jsonConfigurationKey.createNestedObject(); jconfig["key"] = config->getKey(); - jconfig["readonly"] = config->isReadOnly(); + jconfig["readonly"] = config->getMutability() == Mutability::ReadOnly; if (v == vbuf) { //value points to buffer on stack, needs to be copied into JSON memory pool jconfig["value"] = (char*) v; @@ -140,11 +149,14 @@ std::unique_ptr GetConfiguration::createConf(){ if (!unknownKeys.empty()) { JsonArray jsonUnknownKey = payload.createNestedArray("unknownKey"); - for (auto key : unknownKeys) { + for (size_t i = 0; i < unknownKeys.size(); i++) { + auto key = unknownKeys[i]; MO_DBG_DEBUG("Unknown key: %s", key); - jsonUnknownKey.add(key); + jsonUnknownKey.add((const char*)key); //force zero-copy mode } } return doc; } + +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Operations/GetConfiguration.h b/src/MicroOcpp/Operations/GetConfiguration.h index c6781119..c9b40917 100644 --- a/src/MicroOcpp/Operations/GetConfiguration.h +++ b/src/MicroOcpp/Operations/GetConfiguration.h @@ -7,18 +7,28 @@ #include #include +#include + +#if MO_ENABLE_V16 namespace MicroOcpp { -namespace Ocpp16 { +namespace v16 { + +class Configuration; +class ConfigurationService; class GetConfiguration : public Operation, public MemoryManaged { private: - Vector keys; + ConfigurationService& configService; + + Vector configurations; + Vector unknownKeys; const char *errorCode {nullptr}; const char *errorDescription = ""; public: - GetConfiguration(); + GetConfiguration(ConfigurationService& configService); + ~GetConfiguration(); const char* getOperationType() override; @@ -31,6 +41,7 @@ class GetConfiguration : public Operation, public MemoryManaged { }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #endif diff --git a/src/MicroOcpp/Operations/GetDiagnostics.cpp b/src/MicroOcpp/Operations/GetDiagnostics.cpp index e3bab7eb..ad3f25e7 100644 --- a/src/MicroOcpp/Operations/GetDiagnostics.cpp +++ b/src/MicroOcpp/Operations/GetDiagnostics.cpp @@ -3,14 +3,18 @@ // MIT License #include + +#include #include #include #include -using MicroOcpp::Ocpp16::GetDiagnostics; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 && MO_ENABLE_DIAGNOSTICS + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; -GetDiagnostics::GetDiagnostics(DiagnosticsService& diagService) : MemoryManaged("v16.Operation.", "GetDiagnostics"), diagService(diagService), fileName(makeString(getMemoryTag())) { +GetDiagnostics::GetDiagnostics(Context& context, DiagnosticsService& diagService) : MemoryManaged("v16.Operation.", "GetDiagnostics"), context(context), diagService(diagService) { } @@ -22,8 +26,8 @@ void GetDiagnostics::processReq(JsonObject payload) { errorCode = "FormationViolation"; return; } - - int retries = payload["retries"] | 1; + + int retries = payload["retries"] | 0; int retryInterval = payload["retryInterval"] | 180; if (retries < 0 || retryInterval < 0) { errorCode = "PropertyConstraintViolation"; @@ -32,8 +36,8 @@ void GetDiagnostics::processReq(JsonObject payload) { Timestamp startTime; if (payload.containsKey("startTime")) { - if (!startTime.setTime(payload["startTime"] | "Invalid")) { - errorCode = "PropertyConstraintViolation"; + if (!context.getClock().parseString(payload["startTime"] | "_Invalid", startTime)) { + errorCode = "FormationViolation"; MO_DBG_WARN("bad time format"); return; } @@ -41,23 +45,25 @@ void GetDiagnostics::processReq(JsonObject payload) { Timestamp stopTime; if (payload.containsKey("stopTime")) { - if (!stopTime.setTime(payload["stopTime"] | "Invalid")) { - errorCode = "PropertyConstraintViolation"; + if (!context.getClock().parseString(payload["stopTime"] | "_Invalid", stopTime)) { + errorCode = "FormationViolation"; MO_DBG_WARN("bad time format"); return; } } - fileName = diagService.requestDiagnosticsUpload(location, (unsigned int) retries, (unsigned int) retryInterval, startTime, stopTime); + (void)diagService.requestDiagnosticsUpload(location, (unsigned int) retries, retryInterval, startTime, stopTime, filename); } std::unique_ptr GetDiagnostics::createConf(){ - if (fileName.empty()) { + if (!*filename) { return createEmptyDocument(); } else { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); - payload["fileName"] = fileName.c_str(); + payload["fileName"] = (const char*)filename; //force zero-copy return doc; } } + +#endif //MO_ENABLE_V16 && MO_ENABLE_DIAGNOSTICS diff --git a/src/MicroOcpp/Operations/GetDiagnostics.h b/src/MicroOcpp/Operations/GetDiagnostics.h index 704f9199..86b96768 100644 --- a/src/MicroOcpp/Operations/GetDiagnostics.h +++ b/src/MicroOcpp/Operations/GetDiagnostics.h @@ -6,22 +6,27 @@ #define MO_GETDIAGNOSTICS_H #include -#include +#include +#include + +#if MO_ENABLE_V16 && MO_ENABLE_DIAGNOSTICS namespace MicroOcpp { +class Context; class DiagnosticsService; -namespace Ocpp16 { +namespace v16 { class GetDiagnostics : public Operation, public MemoryManaged { private: + Context& context; DiagnosticsService& diagService; - String fileName; + char filename [MO_GETLOG_FNAME_SIZE] = {'\0'}; const char *errorCode = nullptr; public: - GetDiagnostics(DiagnosticsService& diagService); + GetDiagnostics(Context& context, DiagnosticsService& diagService); const char* getOperationType() override {return "GetDiagnostics";} @@ -32,7 +37,7 @@ class GetDiagnostics : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp - +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_DIAGNOSTICS #endif diff --git a/src/MicroOcpp/Operations/GetInstalledCertificateIds.cpp b/src/MicroOcpp/Operations/GetInstalledCertificateIds.cpp index 87d4c2dd..c3ed29a3 100644 --- a/src/MicroOcpp/Operations/GetInstalledCertificateIds.cpp +++ b/src/MicroOcpp/Operations/GetInstalledCertificateIds.cpp @@ -3,17 +3,15 @@ // MIT License #include - -#if MO_ENABLE_CERT_MGMT - #include #include #include -using MicroOcpp::Ocpp201::GetInstalledCertificateIds; -using MicroOcpp::JsonDoc; +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT -GetInstalledCertificateIds::GetInstalledCertificateIds(CertificateService& certService) : MemoryManaged("v201.Operation.", "GetInstalledCertificateIds"), certService(certService), certificateHashDataChain(makeVector(getMemoryTag())) { +using namespace MicroOcpp; + +GetInstalledCertificateIds::GetInstalledCertificateIds(CertificateService& certService) : MemoryManaged("v16/v201.Operation.", "GetInstalledCertificateIds"), certService(certService), certificateHashDataChain(makeVector(getMemoryTag())) { } @@ -111,15 +109,15 @@ std::unique_ptr GetInstalledCertificateIds::createConf() { char buf [MO_CERT_HASH_ISSUER_NAME_KEY_SIZE]; - ocpp_cert_print_issuerNameHash(&chainElem.certificateHashData, buf, sizeof(buf)); + mo_cert_print_issuerNameHash(&chainElem.certificateHashData, buf, sizeof(buf)); certHashJson["certificateHashData"]["issuerNameHash"] = buf; - ocpp_cert_print_issuerKeyHash(&chainElem.certificateHashData, buf, sizeof(buf)); + mo_cert_print_issuerKeyHash(&chainElem.certificateHashData, buf, sizeof(buf)); certHashJson["certificateHashData"]["issuerKeyHash"] = buf; - ocpp_cert_print_serialNumber(&chainElem.certificateHashData, buf, sizeof(buf)); + mo_cert_print_serialNumber(&chainElem.certificateHashData, buf, sizeof(buf)); certHashJson["certificateHashData"]["serialNumber"] = buf; - + if (!chainElem.childCertificateHashData.empty()) { MO_DBG_ERR("only sole root certs supported"); } @@ -128,4 +126,4 @@ std::unique_ptr GetInstalledCertificateIds::createConf() { return doc; } -#endif //MO_ENABLE_CERT_MGMT +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT diff --git a/src/MicroOcpp/Operations/GetInstalledCertificateIds.h b/src/MicroOcpp/Operations/GetInstalledCertificateIds.h index 28a31c92..c5813adc 100644 --- a/src/MicroOcpp/Operations/GetInstalledCertificateIds.h +++ b/src/MicroOcpp/Operations/GetInstalledCertificateIds.h @@ -5,19 +5,16 @@ #ifndef MO_GETINSTALLEDCERTIFICATEIDS_H #define MO_GETINSTALLEDCERTIFICATEIDS_H -#include - -#if MO_ENABLE_CERT_MGMT - #include #include +#include + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT namespace MicroOcpp { class CertificateService; -namespace Ocpp201 { - class GetInstalledCertificateIds : public Operation, public MemoryManaged { private: CertificateService& certService; @@ -36,8 +33,6 @@ class GetInstalledCertificateIds : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp201 -} //end namespace MicroOcpp - -#endif //MO_ENABLE_CERT_MGMT +} //namespace MicroOcpp +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT #endif diff --git a/src/MicroOcpp/Operations/GetLocalListVersion.cpp b/src/MicroOcpp/Operations/GetLocalListVersion.cpp index f2c8a058..f9459a2e 100644 --- a/src/MicroOcpp/Operations/GetLocalListVersion.cpp +++ b/src/MicroOcpp/Operations/GetLocalListVersion.cpp @@ -2,20 +2,18 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_LOCAL_AUTH - #include #include #include #include -using MicroOcpp::Ocpp16::GetLocalListVersion; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; GetLocalListVersion::GetLocalListVersion(Model& model) : MemoryManaged("v16.Operation.", "GetLocalListVersion"), model(model) { - + } const char* GetLocalListVersion::getOperationType(){ @@ -38,4 +36,4 @@ std::unique_ptr GetLocalListVersion::createConf(){ return doc; } -#endif //MO_ENABLE_LOCAL_AUTH +#endif //MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Operations/GetLocalListVersion.h b/src/MicroOcpp/Operations/GetLocalListVersion.h index 554f7a6b..6fdd5dc4 100644 --- a/src/MicroOcpp/Operations/GetLocalListVersion.h +++ b/src/MicroOcpp/Operations/GetLocalListVersion.h @@ -5,18 +5,16 @@ #ifndef MO_GETLOCALLISTVERSION_H #define MO_GETLOCALLISTVERSION_H +#include #include -#if MO_ENABLE_LOCAL_AUTH - -#include +#if MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH namespace MicroOcpp { +namespace v16 { class Model; -namespace Ocpp16 { - class GetLocalListVersion : public Operation, public MemoryManaged { private: Model& model; @@ -30,8 +28,7 @@ class GetLocalListVersion : public Operation, public MemoryManaged { std::unique_ptr createConf() override; }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp - -#endif //MO_ENABLE_LOCAL_AUTH +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH #endif diff --git a/src/MicroOcpp/Operations/GetLog.cpp b/src/MicroOcpp/Operations/GetLog.cpp new file mode 100644 index 00000000..cd8a24c1 --- /dev/null +++ b/src/MicroOcpp/Operations/GetLog.cpp @@ -0,0 +1,77 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#include + +#include +#include +#include + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS + +using namespace MicroOcpp; + +GetLog::GetLog(Context& context, DiagnosticsService& diagService) : MemoryManaged("v16/v201.Operation.", "GetLog"), context(context), diagService(diagService) { + +} + +void GetLog::processReq(JsonObject payload) { + + auto logType = mo_deserializeLogType(payload["logType"] | "_Undefined"); + if (logType == MO_LogType_UNDEFINED) { + errorCode = "FormationViolation"; + return; + } + + int requestId = payload["requestId"] | -1; + if (requestId < 0) { + errorCode = "FormationViolation"; + return; + } + + int retries = payload["retries"] | 0; + int retryInterval = payload["retryInterval"] | 180; + if (retries < 0 || retryInterval < 0) { + errorCode = "PropertyConstraintViolation"; + return; + } + + JsonObject log = payload["log"]; + + const char *remoteLocation = log["remoteLocation"] | ""; + if (!*remoteLocation) { + errorCode = "FormationViolation"; + return; + } + + Timestamp oldestTimestamp; + if (payload.containsKey("oldestTimestamp")) { + if (!context.getClock().parseString(payload["oldestTimestamp"] | "_Invalid", oldestTimestamp)) { + errorCode = "FormationViolation"; + MO_DBG_WARN("bad time format"); + return; + } + } + + Timestamp latestTimestamp; + if (payload.containsKey("latestTimestamp")) { + if (!context.getClock().parseString(payload["latestTimestamp"] | "_Invalid", latestTimestamp)) { + errorCode = "FormationViolation"; + MO_DBG_WARN("bad time format"); + return; + } + } + + status = diagService.getLog(logType, requestId, retries, retryInterval, remoteLocation, oldestTimestamp, latestTimestamp, filename); +} + +std::unique_ptr GetLog::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + payload["status"] = mo_serializeGetLogStatus(status); + payload["filename"] = (const char*)filename; //force zero-copy + return doc; +} + +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS diff --git a/src/MicroOcpp/Operations/GetLog.h b/src/MicroOcpp/Operations/GetLog.h new file mode 100644 index 00000000..15c08750 --- /dev/null +++ b/src/MicroOcpp/Operations/GetLog.h @@ -0,0 +1,41 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#ifndef MO_GETLOG_H +#define MO_GETLOG_H + +#include +#include +#include + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS + +namespace MicroOcpp { + +class Context; +class DiagnosticsService; + +class GetLog : public Operation, public MemoryManaged { +private: + Context& context; + DiagnosticsService& diagService; + MO_GetLogStatus status = MO_GetLogStatus_UNDEFINED; + char filename [MO_GETLOG_FNAME_SIZE] = {'\0'}; + + const char *errorCode = nullptr; +public: + GetLog(Context& context, DiagnosticsService& diagService); + + const char* getOperationType() override {return "GetLog";} + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //namespace MicroOcpp +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS +#endif diff --git a/src/MicroOcpp/Operations/GetVariables.cpp b/src/MicroOcpp/Operations/GetVariables.cpp index 30f8dc7f..f7dbbf19 100644 --- a/src/MicroOcpp/Operations/GetVariables.cpp +++ b/src/MicroOcpp/Operations/GetVariables.cpp @@ -2,19 +2,16 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_V201 - #include #include #include -#include //for tolower +#include //for tolower + +#if MO_ENABLE_V201 -using MicroOcpp::Ocpp201::GetVariableData; -using MicroOcpp::Ocpp201::GetVariables; -using MicroOcpp::JsonDoc; +using namespace MicroOcpp; +using namespace MicroOcpp::v201; GetVariableData::GetVariableData(const char *memory_tag) : componentName{makeString(memory_tag)}, variableName{makeString(memory_tag)} { @@ -87,7 +84,7 @@ std::unique_ptr GetVariables::createConf(){ for (auto& query : queries) { query.attributeStatus = variableService.getVariable( query.attributeType, - ComponentId(query.componentName.c_str(), + ComponentId(query.componentName.c_str(), EvseId(query.componentEvseId, query.componentEvseConnectorId)), query.variableName.c_str(), &query.variable); @@ -123,7 +120,7 @@ std::unique_ptr GetVariables::createConf(){ } } - capacity += + capacity += JSON_OBJECT_SIZE(5) + // getVariableResult valueCapacity + // capacity needed for storing the value JSON_OBJECT_SIZE(2) + // component @@ -225,4 +222,4 @@ std::unique_ptr GetVariables::createConf(){ return doc; } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/GetVariables.h b/src/MicroOcpp/Operations/GetVariables.h index a0360e62..9e3e7cd4 100644 --- a/src/MicroOcpp/Operations/GetVariables.h +++ b/src/MicroOcpp/Operations/GetVariables.h @@ -5,20 +5,18 @@ #ifndef MO_GETVARIABLES_H #define MO_GETVARIABLES_H -#include - -#if MO_ENABLE_V201 - #include #include #include +#include + +#if MO_ENABLE_V201 namespace MicroOcpp { +namespace v201 { class VariableService; -namespace Ocpp201 { - // GetVariableDataType (2.25) and // GetVariableResultType (2.26) struct GetVariableData { @@ -52,12 +50,10 @@ class GetVariables : public Operation, public MemoryManaged { std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} - }; -} //namespace Ocpp201 +} //namespace v201 } //namespace MicroOcpp - #endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Operations/Heartbeat.cpp b/src/MicroOcpp/Operations/Heartbeat.cpp index 4dfd0439..e1956801 100644 --- a/src/MicroOcpp/Operations/Heartbeat.cpp +++ b/src/MicroOcpp/Operations/Heartbeat.cpp @@ -3,15 +3,16 @@ // MIT License #include -#include +#include #include #include -using MicroOcpp::Ocpp16::Heartbeat; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 || MO_ENABLE_V201 + +using namespace MicroOcpp; + +Heartbeat::Heartbeat(Context& context) : MemoryManaged("v16.Operation.", "Heartbeat"), context(context) { -Heartbeat::Heartbeat(Model& model) : MemoryManaged("v16.Operation.", "Heartbeat"), model(model) { - } const char* Heartbeat::getOperationType(){ @@ -23,10 +24,10 @@ std::unique_ptr Heartbeat::createReq() { } void Heartbeat::processConf(JsonObject payload) { - + const char* currentTime = payload["currentTime"] | "Invalid"; if (strcmp(currentTime, "Invalid")) { - if (model.getClock().setTime(currentTime)) { + if (context.getClock().setTime(currentTime)) { //success MO_DBG_DEBUG("Request has been accepted"); } else { @@ -37,30 +38,22 @@ void Heartbeat::processConf(JsonObject payload) { } } -void Heartbeat::processReq(JsonObject payload) { - - /** - * Ignore Contents of this Req-message, because this is for debug purposes only - */ +#if MO_ENABLE_MOCK_SERVER +int Heartbeat::writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData) { + (void)userStatus; -} + auto& context = *reinterpret_cast(userData); -std::unique_ptr Heartbeat::createConf(){ - auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + (JSONDATE_LENGTH + 1)); - JsonObject payload = doc->to(); + char timeStr [MO_JSONDATE_SIZE]; - //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; + if (context.getClock().now().isUnixTime()) { + context.getClock().toJsonString(context.getClock().now(), timeStr, sizeof(timeStr)); + } else { + (void)snprintf(timeStr, sizeof(timeStr), "2025-05-18T18:55:13Z"); } - char ocppNowJson [JSONDATE_LENGTH + 1] = {'\0'}; - ocppSelect.toJsonString(ocppNowJson, JSONDATE_LENGTH + 1); - payload["currentTime"] = ocppNowJson; - - return doc; + return snprintf(buf, size, "{\"currentTime\":\"%s\"}", timeStr); } +#endif //MO_ENABLE_MOCK_SERVER + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/Heartbeat.h b/src/MicroOcpp/Operations/Heartbeat.h index b3ce6a0e..30a7add3 100644 --- a/src/MicroOcpp/Operations/Heartbeat.h +++ b/src/MicroOcpp/Operations/Heartbeat.h @@ -1,23 +1,24 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_HEARTBEAT_H #define MO_HEARTBEAT_H #include +#include -namespace MicroOcpp { +#if MO_ENABLE_V16 || MO_ENABLE_V201 -class Model; +namespace MicroOcpp { -namespace Ocpp16 { +class Context; class Heartbeat : public Operation, public MemoryManaged { private: - Model& model; + Context& context; public: - Heartbeat(Model& model); + Heartbeat(Context& context); const char* getOperationType() override; @@ -25,11 +26,12 @@ class Heartbeat : public Operation, public MemoryManaged { void processConf(JsonObject payload) override; - void processReq(JsonObject payload) override; - - std::unique_ptr createConf() override; +#if MO_ENABLE_MOCK_SERVER + static int writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData); +#endif }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace MicroOcpp + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Operations/InstallCertificate.cpp b/src/MicroOcpp/Operations/InstallCertificate.cpp index b9859501..4c60fedb 100644 --- a/src/MicroOcpp/Operations/InstallCertificate.cpp +++ b/src/MicroOcpp/Operations/InstallCertificate.cpp @@ -3,14 +3,12 @@ // MIT License #include - -#if MO_ENABLE_CERT_MGMT - #include #include -using MicroOcpp::Ocpp201::InstallCertificate; -using MicroOcpp::JsonDoc; +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT + +using namespace MicroOcpp; InstallCertificate::InstallCertificate(CertificateService& certService) : MemoryManaged("v201.Operation.", "InstallCertificate"), certService(certService) { @@ -82,4 +80,4 @@ std::unique_ptr InstallCertificate::createConf(){ return doc; } -#endif //MO_ENABLE_CERT_MGMT +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT diff --git a/src/MicroOcpp/Operations/InstallCertificate.h b/src/MicroOcpp/Operations/InstallCertificate.h index e2c22b55..9287f242 100644 --- a/src/MicroOcpp/Operations/InstallCertificate.h +++ b/src/MicroOcpp/Operations/InstallCertificate.h @@ -5,18 +5,15 @@ #ifndef MO_INSTALLCERTIFICATE_H #define MO_INSTALLCERTIFICATE_H +#include #include -#if MO_ENABLE_CERT_MGMT - -#include +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT namespace MicroOcpp { class CertificateService; -namespace Ocpp201 { - class InstallCertificate : public Operation, public MemoryManaged { private: CertificateService& certService; @@ -34,8 +31,7 @@ class InstallCertificate : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp201 -} //end namespace MicroOcpp +} //namespace MicroOcpp -#endif //MO_ENABLE_CERT_MGMT +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_CERT_MGMT #endif diff --git a/src/MicroOcpp/Operations/LogStatusNotification.cpp b/src/MicroOcpp/Operations/LogStatusNotification.cpp new file mode 100644 index 00000000..6fe66453 --- /dev/null +++ b/src/MicroOcpp/Operations/LogStatusNotification.cpp @@ -0,0 +1,49 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#include +#include + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS + +using namespace MicroOcpp; + +LogStatusNotification::LogStatusNotification(MO_UploadLogStatus status, int requestId) + : MemoryManaged("v16/v201.Operation.", "LogStatusNotification"), status(status), requestId(requestId) { + +} + +const char* LogStatusNotification::getOperationType(){ + return "LogStatusNotification"; +} + +std::unique_ptr LogStatusNotification::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + payload["status"] = mo_serializeUploadLogStatus(status); + payload["requestId"] = requestId; + return doc; +} + +void LogStatusNotification::processConf(JsonObject payload) { + /* + * Empty payload + */ +} + +/* + * For debugging only + */ +void LogStatusNotification::processReq(JsonObject payload) { + +} + +/* + * For debugging only + */ +std::unique_ptr LogStatusNotification::createConf(){ + return createEmptyDocument(); +} + +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS diff --git a/src/MicroOcpp/Operations/LogStatusNotification.h b/src/MicroOcpp/Operations/LogStatusNotification.h new file mode 100644 index 00000000..f28b8021 --- /dev/null +++ b/src/MicroOcpp/Operations/LogStatusNotification.h @@ -0,0 +1,36 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2025 +// MIT License + +#ifndef MO_LOGSTATUSNOTIFICATION_H +#define MO_LOGSTATUSNOTIFICATION_H + +#include +#include +#include + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS + +namespace MicroOcpp { + +class LogStatusNotification : public Operation, public MemoryManaged { +private: + MO_UploadLogStatus status; + int requestId; +public: + LogStatusNotification(MO_UploadLogStatus status, int requestId); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; +}; + +} //namespace MicroOcpp +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_DIAGNOSTICS +#endif diff --git a/src/MicroOcpp/Operations/MeterValues.cpp b/src/MicroOcpp/Operations/MeterValues.cpp index ee2b4c7e..8b44427a 100644 --- a/src/MicroOcpp/Operations/MeterValues.cpp +++ b/src/MicroOcpp/Operations/MeterValues.cpp @@ -3,31 +3,38 @@ // MIT License #include + +#include #include #include -#include #include -using MicroOcpp::Ocpp16::MeterValues; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; //can only be used for echo server debugging -MeterValues::MeterValues(Model& model) : MemoryManaged("v16.Operation.", "MeterValues"), model(model) { - -} +MeterValues::MeterValues(Context& context) : MemoryManaged("v16.Operation.", "MeterValues"), context(context) { -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(Context& context, unsigned int connectorId, int transactionId, MeterValue *meterValue, bool transferOwnership) : + MemoryManaged("v16.Operation.", "MeterValues"), + context(context), + meterValue(meterValue), + isMeterValueOwner(transferOwnership), + connectorId(connectorId), + transactionId(transactionId) { + } MeterValues::~MeterValues(){ - + if (isMeterValueOwner) { + delete meterValue; + meterValue = nullptr; + isMeterValueOwner = false; + } } const char* MeterValues::getOperationType(){ @@ -37,39 +44,28 @@ const char* MeterValues::getOperationType(){ 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); + int ret = meterValue->getJsonCapacity(MO_OCPP_V16, /*internalFormat*/ false); + if (ret >= 0) { + capacity += (size_t)ret; + } else { + MO_DBG_ERR("serialization error"); + } + 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(); + if (transactionId >= 0) { + payload["transactionId"] = transactionId; } auto meterValueArray = payload.createNestedArray("meterValue"); - if (meterValueJson) { - meterValueArray.add(*meterValueJson); + auto meterValueJson = meterValueArray.createNestedObject(); + if (!meterValue->toJson(context.getClock(), MO_OCPP_V16, /*internalFormat*/ false, meterValueJson)) { + MO_DBG_ERR("serialization error"); } return doc; @@ -91,3 +87,5 @@ void MeterValues::processReq(JsonObject payload) { std::unique_ptr MeterValues::createConf(){ return createEmptyDocument(); } + +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Operations/MeterValues.h b/src/MicroOcpp/Operations/MeterValues.h index 774a538a..216bb74b 100644 --- a/src/MicroOcpp/Operations/MeterValues.h +++ b/src/MicroOcpp/Operations/MeterValues.h @@ -8,30 +8,30 @@ #include #include #include +#include + +#if MO_ENABLE_V16 namespace MicroOcpp { -class Model; -class MeterValue; -class Transaction; +class Context; +struct MeterValue; -namespace Ocpp16 { +namespace v16 { class MeterValues : public Operation, public MemoryManaged { private: - Model& model; //for adjusting the timestamp if MeterValue has been created before BootNotification + Context& context; //for adjusting the timestamp if MeterValue has been created before BootNotification MeterValue *meterValue = nullptr; - std::unique_ptr meterValueOwnership; + bool isMeterValueOwner = false; unsigned int connectorId = 0; - - std::shared_ptr transaction; + int transactionId = -1; 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(Context& context, unsigned int connectorId, int transactionId, MeterValue *meterValue, bool transferOwnership); //`transferOwnership`: if this object should take ownership of the passed `meterValue` - MeterValues(Model& model); //for debugging only. Make this for the server pendant + MeterValues(Context& context); //for debugging only. Make this for the server pendant ~MeterValues(); @@ -46,6 +46,7 @@ class MeterValues : public Operation, public MemoryManaged { std::unique_ptr createConf() override; }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #endif diff --git a/src/MicroOcpp/Operations/NotifyReport.cpp b/src/MicroOcpp/Operations/NotifyReport.cpp index 0528e452..e86baf32 100644 --- a/src/MicroOcpp/Operations/NotifyReport.cpp +++ b/src/MicroOcpp/Operations/NotifyReport.cpp @@ -2,20 +2,20 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_V201 - #include + +#include #include #include #include -using namespace MicroOcpp::Ocpp201; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V201 + +using namespace MicroOcpp; +using namespace MicroOcpp::v201; -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) { +NotifyReport::NotifyReport(Context& context, int requestId, const Timestamp& generatedAt, bool tbc, unsigned int seqNo, const Vector& reportData) + : MemoryManaged("v201.Operation.", "NotifyReport"), context(context), requestId(requestId), generatedAt(generatedAt), tbc(tbc), seqNo(seqNo), reportData(reportData) { } @@ -34,9 +34,9 @@ std::unique_ptr NotifyReport::createReq() { Variable::AttributeType::MaxSet }; - size_t capacity = + size_t capacity = JSON_OBJECT_SIZE(5) + //total of 5 fields - JSONDATE_LENGTH + 1; //timestamp string + MO_JSONDATE_SIZE; //timestamp string capacity += JSON_ARRAY_SIZE(reportData.size()); for (auto variable : reportData) { @@ -86,8 +86,11 @@ std::unique_ptr NotifyReport::createReq() { JsonObject payload = doc->to(); payload["requestId"] = requestId; - char generatedAtCstr [JSONDATE_LENGTH + 1]; - generatedAt.toJsonString(generatedAtCstr, sizeof(generatedAtCstr)); + char generatedAtCstr [MO_JSONDATE_SIZE]; + if (!context.getClock().toJsonString(generatedAt, generatedAtCstr, sizeof(generatedAtCstr))) { + MO_DBG_ERR("internal error"); + generatedAtCstr[0] = '\0'; + } payload["generatedAt"] = generatedAtCstr; if (tbc) { @@ -144,7 +147,7 @@ std::unique_ptr NotifyReport::createReq() { attribute["type"] = attributeTypeCstr; } - if (variable->getMutability() != Variable::Mutability::WriteOnly) { + if (variable->getMutability() != Mutability::WriteOnly) { switch (variable->getInternalDataType()) { case Variable::InternalDataType::Int: { char valbuf [VALUE_BUFSIZE]; @@ -169,13 +172,13 @@ std::unique_ptr NotifyReport::createReq() { const char *mutabilityCstr = nullptr; switch (variable->getMutability()) { - case Variable::Mutability::ReadOnly: + case Mutability::ReadOnly: mutabilityCstr = "ReadOnly"; break; - case Variable::Mutability::WriteOnly: + case Mutability::WriteOnly: mutabilityCstr = "WriteOnly"; break; - case Variable::Mutability::ReadWrite: + case Mutability::ReadWrite: // leave blank when ReadWrite break; default: @@ -225,7 +228,7 @@ std::unique_ptr NotifyReport::createReq() { break; default: MO_DBG_ERR("internal error"); - break; + break; } variableCharacteristics["dataType"] = dataTypeCstr; @@ -239,4 +242,4 @@ void NotifyReport::processConf(JsonObject payload) { // empty payload } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/NotifyReport.h b/src/MicroOcpp/Operations/NotifyReport.h index 1a092678..cd19fedf 100644 --- a/src/MicroOcpp/Operations/NotifyReport.h +++ b/src/MicroOcpp/Operations/NotifyReport.h @@ -5,33 +5,33 @@ #ifndef MO_TRANSACTIONEVENT_H #define MO_TRANSACTIONEVENT_H -#include - -#if MO_ENABLE_V201 - #include #include #include +#include + +#if MO_ENABLE_V201 namespace MicroOcpp { -class Model; -class Variable; +class Context; -namespace Ocpp201 { +namespace v201 { + +class Variable; class NotifyReport : public Operation, public MemoryManaged { private: - Model& model; + Context& context; int requestId; Timestamp generatedAt; bool tbc; - int seqNo; + unsigned int seqNo; Vector reportData; public: - NotifyReport(Model& model, int requestId, const Timestamp& generatedAt, bool tbc, int seqNo, const Vector& reportData); + NotifyReport(Context& context, int requestId, const Timestamp& generatedAt, bool tbc, unsigned int seqNo, const Vector& reportData); const char* getOperationType() override; @@ -40,7 +40,7 @@ class NotifyReport : public Operation, public MemoryManaged { void processConf(JsonObject payload) override; }; -} //end namespace Ocpp201 -} //end namespace MicroOcpp -#endif // MO_ENABLE_V201 +} //namespace v201 +} //namespace MicroOcpp +#endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Operations/RemoteStartTransaction.cpp b/src/MicroOcpp/Operations/RemoteStartTransaction.cpp index 77314f36..c2111a31 100644 --- a/src/MicroOcpp/Operations/RemoteStartTransaction.cpp +++ b/src/MicroOcpp/Operations/RemoteStartTransaction.cpp @@ -4,18 +4,23 @@ #include -#include + +#include #include -#include +#include +#include +#include #include -#include +#include #include -using MicroOcpp::Ocpp16::RemoteStartTransaction; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; + +RemoteStartTransaction::RemoteStartTransaction(Context& context, RemoteControlService& rcService) : MemoryManaged("v16.Operation.", "RemoteStartTransaction"), context(context), rcService(rcService) { -RemoteStartTransaction::RemoteStartTransaction(Model& model) : MemoryManaged("v16.Operation.", "RemoteStartTransaction"), model(model) { - } const char* RemoteStartTransaction::getOperationType() { @@ -31,8 +36,8 @@ void RemoteStartTransaction::processReq(JsonObject payload) { } const char *idTag = payload["idTag"] | ""; - size_t len = strnlen(idTag, IDTAG_LEN_MAX + 1); - if (len == 0 || len > IDTAG_LEN_MAX) { + size_t len = strnlen(idTag, MO_IDTAG_LEN_MAX + 1); + if (len == 0 || len > MO_IDTAG_LEN_MAX) { errorCode = "PropertyConstraintViolation"; errorDescription = "idTag empty or too long"; return; @@ -40,11 +45,22 @@ void RemoteStartTransaction::processReq(JsonObject payload) { std::unique_ptr chargingProfile; - if (payload.containsKey("chargingProfile") && model.getSmartChargingService()) { + if (payload.containsKey("chargingProfile") && context.getModel16().getSmartChargingService()) { MO_DBG_INFO("Setting Charging profile via RemoteStartTransaction"); - JsonObject chargingProfileJson = payload["chargingProfile"]; - chargingProfile = loadChargingProfile(chargingProfileJson); + chargingProfile = std::unique_ptr(new ChargingProfile()); + if (!chargingProfile) { + MO_DBG_ERR("OOM"); + errorCode = "InternalError"; + return; + } + + bool valid = chargingProfile->parseJson(context.getClock(), MO_OCPP_V16, payload["chargingProfile"]); + if (!valid) { + errorCode = "FormationViolation"; + errorDescription = "chargingProfile validation failed"; + return; + } if (!chargingProfile) { errorCode = "PropertyConstraintViolation"; @@ -52,81 +68,30 @@ void RemoteStartTransaction::processReq(JsonObject payload) { return; } - if (chargingProfile->getChargingProfilePurpose() != ChargingProfilePurposeType::TxProfile) { + if (chargingProfile->chargingProfilePurpose != ChargingProfilePurposeType::TxProfile) { errorCode = "PropertyConstraintViolation"; errorDescription = "Can only set TxProfile here"; return; } - if (chargingProfile->getChargingProfileId() < 0) { + if (chargingProfile->chargingProfileId < 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; + status = rcService.remoteStartTransaction(connectorId, idTag, std::move(chargingProfile)); + if (status == v16::RemoteStartStopStatus::ERR_INTERNAL) { + errorCode = "InternalError"; + return; } } std::unique_ptr RemoteStartTransaction::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); - if (accepted) { + if (status == v16::RemoteStartStopStatus::Accepted) { payload["status"] = "Accepted"; } else { payload["status"] = "Rejected"; @@ -134,15 +99,4 @@ std::unique_ptr RemoteStartTransaction::createConf(){ 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){ - -} +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Operations/RemoteStartTransaction.h b/src/MicroOcpp/Operations/RemoteStartTransaction.h index 411578bd..58ce3089 100644 --- a/src/MicroOcpp/Operations/RemoteStartTransaction.h +++ b/src/MicroOcpp/Operations/RemoteStartTransaction.h @@ -6,32 +6,33 @@ #define MO_REMOTESTARTTRANSACTION_H #include -#include +#include +#include + +#if MO_ENABLE_V16 namespace MicroOcpp { -class Model; +class Context; +class RemoteControlService; class ChargingProfile; -namespace Ocpp16 { +namespace v16 { class RemoteStartTransaction : public Operation, public MemoryManaged { private: - Model& model; + Context& context; + RemoteControlService& rcService; + + RemoteStartStopStatus status = RemoteStartStopStatus::Rejected; - bool accepted = false; - const char *errorCode {nullptr}; const char *errorDescription = ""; public: - RemoteStartTransaction(Model& model); + RemoteStartTransaction(Context& context, RemoteControlService& rcService); const char* getOperationType() override; - std::unique_ptr createReq() override; - - void processConf(JsonObject payload) override; - void processReq(JsonObject payload) override; std::unique_ptr createConf() override; @@ -40,6 +41,7 @@ class RemoteStartTransaction : public Operation, public MemoryManaged { const char *getErrorDescription() override {return errorDescription;} }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #endif diff --git a/src/MicroOcpp/Operations/RemoteStopTransaction.cpp b/src/MicroOcpp/Operations/RemoteStopTransaction.cpp index 8a36699a..c7e9bb42 100644 --- a/src/MicroOcpp/Operations/RemoteStopTransaction.cpp +++ b/src/MicroOcpp/Operations/RemoteStopTransaction.cpp @@ -3,45 +3,47 @@ // MIT License #include + #include -#include -#include +#include +#include + +#if MO_ENABLE_V16 -using MicroOcpp::Ocpp16::RemoteStopTransaction; -using MicroOcpp::JsonDoc; +using namespace MicroOcpp; +using namespace MicroOcpp::v16; + +RemoteStopTransaction::RemoteStopTransaction(Context& context, RemoteControlService& rcService) : MemoryManaged("v16.Operation.", "RemoteStopTransaction"), context(context), rcService(rcService) { -RemoteStopTransaction::RemoteStopTransaction(Model& model) : MemoryManaged("v16.Operation.", "RemoteStopTransaction"), model(model) { - } -const char* RemoteStopTransaction::getOperationType(){ +const char* RemoteStopTransaction::getOperationType() { return "RemoteStopTransaction"; } void RemoteStopTransaction::processReq(JsonObject payload) { - if (!payload.containsKey("transactionId")) { + if (!payload.containsKey("transactionId") || !payload["transactionId"].is()) { 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; - } + status = rcService.remoteStopTransaction(transactionId); + if (status == v16::RemoteStartStopStatus::ERR_INTERNAL) { + errorCode = "InternalError"; + return; } } -std::unique_ptr RemoteStopTransaction::createConf(){ +std::unique_ptr RemoteStopTransaction::createConf() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); - if (accepted){ + if (status == v16::RemoteStartStopStatus::Accepted) { payload["status"] = "Accepted"; } else { payload["status"] = "Rejected"; } return doc; } + +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Operations/RemoteStopTransaction.h b/src/MicroOcpp/Operations/RemoteStopTransaction.h index 2a42d695..55f20f1d 100644 --- a/src/MicroOcpp/Operations/RemoteStopTransaction.h +++ b/src/MicroOcpp/Operations/RemoteStopTransaction.h @@ -6,21 +6,28 @@ #define MO_REMOTESTOPTRANSACTION_H #include +#include +#include + +#if MO_ENABLE_V16 namespace MicroOcpp { -class Model; +class Context; +class RemoteControlService; -namespace Ocpp16 { +namespace v16 { class RemoteStopTransaction : public Operation, public MemoryManaged { private: - Model& model; - bool accepted = false; + Context& context; + RemoteControlService& rcService; + + RemoteStartStopStatus status = RemoteStartStopStatus::Rejected; const char *errorCode = nullptr; public: - RemoteStopTransaction(Model& model); + RemoteStopTransaction(Context& context, RemoteControlService& rcService); const char* getOperationType() override; @@ -31,6 +38,7 @@ class RemoteStopTransaction : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #endif diff --git a/src/MicroOcpp/Operations/RequestStartTransaction.cpp b/src/MicroOcpp/Operations/RequestStartTransaction.cpp index 1b37f051..95e009a6 100644 --- a/src/MicroOcpp/Operations/RequestStartTransaction.cpp +++ b/src/MicroOcpp/Operations/RequestStartTransaction.cpp @@ -2,19 +2,21 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_V201 - #include + +#include +#include #include +#include #include -using MicroOcpp::Ocpp201::RequestStartTransaction; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V201 + +using namespace MicroOcpp; +using namespace MicroOcpp::v201; + +RequestStartTransaction::RequestStartTransaction(Context& context, RemoteControlService& rcService) : MemoryManaged("v201.Operation.", "RequestStartTransaction"), context(context), rcService(rcService) { -RequestStartTransaction::RequestStartTransaction(RemoteControlService& rcService) : MemoryManaged("v201.Operation.", "RequestStartTransaction"), rcService(rcService) { - } const char* RequestStartTransaction::getOperationType(){ @@ -43,7 +45,31 @@ void RequestStartTransaction::processReq(JsonObject payload) { return; } - status = rcService.requestStartTransaction(evseId, remoteStartId, idToken, transactionId, sizeof(transactionId)); + std::unique_ptr chargingProfile; + if (payload.containsKey("chargingProfile")) { + #if MO_ENABLE_SMARTCHARGING + if (context.getModel201().getSmartChargingService()) { + JsonObject chargingProfileJson = payload["chargingProfile"]; + chargingProfile = std::unique_ptr(new ChargingProfile()); + if (!chargingProfile) { + MO_DBG_ERR("OOM"); + errorCode = "InternalError"; + return; + } + + bool valid = chargingProfile->parseJson(context.getClock(), MO_OCPP_V201, chargingProfileJson); + if (!valid) { + errorCode = "FormationViolation"; + return; + } + } else + #endif + { + MO_DBG_INFO("ignore ChargingProfile"); //see F01.FR.12 + } + } + + status = rcService.requestStartTransaction(evseId, remoteStartId, idToken, std::move(chargingProfile), transactionId, sizeof(transactionId)); } std::unique_ptr RequestStartTransaction::createConf(){ @@ -54,10 +80,10 @@ std::unique_ptr RequestStartTransaction::createConf(){ const char *statusCstr = ""; switch (status) { - case RequestStartStopStatus_Accepted: + case RequestStartStopStatus::Accepted: statusCstr = "Accepted"; break; - case RequestStartStopStatus_Rejected: + case RequestStartStopStatus::Rejected: statusCstr = "Rejected"; break; default: @@ -74,4 +100,4 @@ std::unique_ptr RequestStartTransaction::createConf(){ return doc; } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/RequestStartTransaction.h b/src/MicroOcpp/Operations/RequestStartTransaction.h index 4ee19761..3e89622f 100644 --- a/src/MicroOcpp/Operations/RequestStartTransaction.h +++ b/src/MicroOcpp/Operations/RequestStartTransaction.h @@ -5,32 +5,33 @@ #ifndef MO_REQUESTSTARTTRANSACTION_H #define MO_REQUESTSTARTTRANSACTION_H -#include - -#if MO_ENABLE_V201 - #include #include #include #include +#include + +#if MO_ENABLE_V201 namespace MicroOcpp { +class Context; class RemoteControlService; -namespace Ocpp201 { +namespace v201 { class RequestStartTransaction : public Operation, public MemoryManaged { private: + Context& context; RemoteControlService& rcService; RequestStartStopStatus status; - std::shared_ptr transaction; - char transactionId [MO_TXID_LEN_MAX + 1] = {'\0'}; + std::shared_ptr transaction; + char transactionId [MO_TXID_SIZE] = {'\0'}; const char *errorCode = nullptr; public: - RequestStartTransaction(RemoteControlService& rcService); + RequestStartTransaction(Context& context, RemoteControlService& rcService); const char* getOperationType() override; @@ -42,9 +43,7 @@ class RequestStartTransaction : public Operation, public MemoryManaged { }; -} //namespace Ocpp201 +} //namespace v201 } //namespace MicroOcpp - #endif //MO_ENABLE_V201 - #endif diff --git a/src/MicroOcpp/Operations/RequestStopTransaction.cpp b/src/MicroOcpp/Operations/RequestStopTransaction.cpp index a316188e..0b3229ed 100644 --- a/src/MicroOcpp/Operations/RequestStopTransaction.cpp +++ b/src/MicroOcpp/Operations/RequestStopTransaction.cpp @@ -2,19 +2,17 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_V201 - #include #include #include -using MicroOcpp::Ocpp201::RequestStopTransaction; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V201 + +using namespace MicroOcpp; +using namespace MicroOcpp::v201; RequestStopTransaction::RequestStopTransaction(RemoteControlService& rcService) : MemoryManaged("v201.Operation.", "RequestStopTransaction"), rcService(rcService) { - + } const char* RequestStopTransaction::getOperationType(){ @@ -23,9 +21,9 @@ const char* RequestStopTransaction::getOperationType(){ void RequestStopTransaction::processReq(JsonObject payload) { - if (!payload.containsKey("transactionId") || + if (!payload.containsKey("transactionId") || !payload["transactionId"].is() || - strlen(payload["transactionId"].as()) > MO_TXID_LEN_MAX) { + strlen(payload["transactionId"].as()) + 1 > MO_TXID_SIZE) { errorCode = "FormationViolation"; return; } @@ -41,10 +39,10 @@ std::unique_ptr RequestStopTransaction::createConf(){ const char *statusCstr = ""; switch (status) { - case RequestStartStopStatus_Accepted: + case RequestStartStopStatus::Accepted: statusCstr = "Accepted"; break; - case RequestStartStopStatus_Rejected: + case RequestStartStopStatus::Rejected: statusCstr = "Rejected"; break; default: @@ -57,4 +55,4 @@ std::unique_ptr RequestStopTransaction::createConf(){ return doc; } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/RequestStopTransaction.h b/src/MicroOcpp/Operations/RequestStopTransaction.h index 30664b67..815b752f 100644 --- a/src/MicroOcpp/Operations/RequestStopTransaction.h +++ b/src/MicroOcpp/Operations/RequestStopTransaction.h @@ -4,20 +4,18 @@ #ifndef MO_REQUESTSTOPTRANSACTION_H #define MO_REQUESTSTOPTRANSACTION_H - -#include - -#if MO_ENABLE_V201 - #include #include #include +#include + +#if MO_ENABLE_V201 namespace MicroOcpp { class RemoteControlService; -namespace Ocpp201 { +namespace v201 { class RequestStopTransaction : public Operation, public MemoryManaged { private: @@ -39,9 +37,7 @@ class RequestStopTransaction : public Operation, public MemoryManaged { }; -} //namespace Ocpp201 +} //namespace v201 } //namespace MicroOcpp - #endif //MO_ENABLE_V201 - #endif diff --git a/src/MicroOcpp/Operations/ReserveNow.cpp b/src/MicroOcpp/Operations/ReserveNow.cpp index 26857fdb..84203c51 100644 --- a/src/MicroOcpp/Operations/ReserveNow.cpp +++ b/src/MicroOcpp/Operations/ReserveNow.cpp @@ -2,26 +2,27 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_RESERVATION - #include + +#include #include +#include +#include #include -#include #include #include -using MicroOcpp::Ocpp16::ReserveNow; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 && MO_ENABLE_RESERVATION + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; + +ReserveNow::ReserveNow(Context& context, ReservationService& rService) : MemoryManaged("v16.Operation.", "ReserveNow"), context(context), rService(rService) { -ReserveNow::ReserveNow(Model& model) : MemoryManaged("v16.Operation.", "ReserveNow"), model(model) { - } ReserveNow::~ReserveNow() { - + } const char* ReserveNow::getOperationType(){ @@ -40,13 +41,13 @@ void ReserveNow::processReq(JsonObject payload) { } int connectorId = payload["connectorId"] | -1; - if (connectorId < 0 || (unsigned int) connectorId >= model.getNumConnectors()) { + if (connectorId < 0 || (unsigned int) connectorId >= context.getModel16().getNumEvseId()) { errorCode = "PropertyConstraintViolation"; return; } Timestamp expiryDate; - if (!expiryDate.setTime(payload["expiryDate"])) { + if (!context.getClock().parseString(payload["expiryDate"] | "_Undefined", expiryDate)) { MO_DBG_WARN("bad time format"); errorCode = "PropertyConstraintViolation"; return; @@ -65,53 +66,61 @@ void ReserveNow::processReq(JsonObject payload) { int reservationId = payload["reservationId"] | -1; - if (model.getReservationService() && - model.getNumConnectors() > 0) { - auto rService = model.getReservationService(); - auto chargePoint = model.getConnector(0); + auto availSvc = context.getModel16().getAvailabilityService(); + auto availSvcCp = availSvc ? availSvc->getEvse(0) : nullptr; + if (!availSvcCp) { + errorCode = "InternalError"; + return; + } - auto reserveConnectorZeroSupportedBool = declareConfiguration("ReserveConnectorZeroSupported", true, CONFIGURATION_VOLATILE); - if (connectorId == 0 && (!reserveConnectorZeroSupportedBool || !reserveConnectorZeroSupportedBool->getBool())) { - reservationStatus = "Rejected"; + AvailabilityServiceEvse *availSvcEvse = nullptr; + if (connectorId > 0) { + availSvcEvse = availSvc->getEvse(connectorId); + if (!availSvcEvse) { + errorCode = "InternalError"; return; } + } - if (auto reservation = rService->getReservationById(reservationId)) { - reservation->update(reservationId, (unsigned int) connectorId, expiryDate, idTag, parentIdTag); - reservationStatus = "Accepted"; - return; - } + auto configServce = context.getModel16().getConfigurationService(); + auto reserveConnectorZeroSupportedBool = configServce ? configServce->declareConfiguration("ReserveConnectorZeroSupported", true, MO_CONFIGURATION_VOLATILE) : nullptr; + if (!reserveConnectorZeroSupportedBool) { + errorCode = "InternalError"; + return; + } - Connector *connector = nullptr; - - if (connectorId > 0) { - connector = model.getConnector((unsigned int) connectorId); - } + if (connectorId == 0 && !reserveConnectorZeroSupportedBool->getBool()) { + reservationStatus = "Rejected"; + return; + } - if (chargePoint->getStatus() == ChargePointStatus_Faulted || - (connector && connector->getStatus() == ChargePointStatus_Faulted)) { - reservationStatus = "Faulted"; - return; - } + if (auto reservation = rService.getReservationById(reservationId)) { + reservation->update(reservationId, (unsigned int) connectorId, expiryDate, idTag, parentIdTag); + reservationStatus = "Accepted"; + return; + } - if (chargePoint->getStatus() == ChargePointStatus_Unavailable || - (connector && connector->getStatus() == ChargePointStatus_Unavailable)) { - reservationStatus = "Unavailable"; - return; - } + if (availSvcCp->getStatus() == MO_ChargePointStatus_Faulted || + (availSvcEvse && availSvcEvse->getStatus() == MO_ChargePointStatus_Faulted)) { + reservationStatus = "Faulted"; + return; + } - if (connector && connector->getStatus() != ChargePointStatus_Available) { - reservationStatus = "Occupied"; - return; - } + if (availSvcCp->getStatus() == MO_ChargePointStatus_Unavailable || + (availSvcEvse && availSvcEvse->getStatus() == MO_ChargePointStatus_Unavailable)) { + reservationStatus = "Unavailable"; + return; + } - if (rService->updateReservation(reservationId, (unsigned int) connectorId, expiryDate, idTag, parentIdTag)) { - reservationStatus = "Accepted"; - } else { - reservationStatus = "Occupied"; - } + if (availSvcEvse && availSvcEvse->getStatus() != MO_ChargePointStatus_Available) { + reservationStatus = "Occupied"; + return; + } + + if (rService.updateReservation(reservationId, (unsigned int) connectorId, expiryDate, idTag, parentIdTag)) { + reservationStatus = "Accepted"; } else { - errorCode = "InternalError"; + reservationStatus = "Occupied"; } } @@ -125,8 +134,8 @@ std::unique_ptr ReserveNow::createConf(){ MO_DBG_ERR("didn't set reservationStatus"); payload["status"] = "Rejected"; } - + return doc; } -#endif //MO_ENABLE_RESERVATION +#endif //MO_ENABLE_V16 && MO_ENABLE_RESERVATION diff --git a/src/MicroOcpp/Operations/ReserveNow.h b/src/MicroOcpp/Operations/ReserveNow.h index cf162ad6..6b810eba 100644 --- a/src/MicroOcpp/Operations/ReserveNow.h +++ b/src/MicroOcpp/Operations/ReserveNow.h @@ -5,25 +5,27 @@ #ifndef MO_RESERVENOW_H #define MO_RESERVENOW_H +#include #include -#if MO_ENABLE_RESERVATION - -#include +#if MO_ENABLE_V16 && MO_ENABLE_RESERVATION namespace MicroOcpp { -class Model; +class Context; -namespace Ocpp16 { +namespace v16 { + +class ReservationService; class ReserveNow : public Operation, public MemoryManaged { private: - Model& model; + Context& context; + ReservationService& rService; const char *errorCode = nullptr; const char *reservationStatus = nullptr; public: - ReserveNow(Model& model); + ReserveNow(Context& context, ReservationService& rService); ~ReserveNow(); @@ -36,8 +38,7 @@ class ReserveNow : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp - -#endif //MO_ENABLE_RESERVATION +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_RESERVATION #endif diff --git a/src/MicroOcpp/Operations/Reset.cpp b/src/MicroOcpp/Operations/Reset.cpp index 9ab42422..3d07acf5 100644 --- a/src/MicroOcpp/Operations/Reset.cpp +++ b/src/MicroOcpp/Operations/Reset.cpp @@ -3,77 +3,70 @@ // MIT License #include -#include #include +#include #include -using MicroOcpp::Ocpp16::Reset; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 + +using namespace MicroOcpp; + +v16::Reset::Reset(ResetService& resetService) : MemoryManaged("v16.Operation.", "Reset"), resetService(resetService) { -Reset::Reset(Model& model) : MemoryManaged("v16.Operation.", "Reset"), model(model) { - } -const char* Reset::getOperationType(){ +const char* v16::Reset::getOperationType(){ return "Reset"; } -void Reset::processReq(JsonObject payload) { +void v16::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"); - } - } - } + if (!resetService.isExecuteResetDefined()) { + MO_DBG_ERR("No reset handler set. Abort operation"); + resetAccepted = false; } else { - resetAccepted = true; //assume that onReceiveReset is set + if (!resetService.isPreResetDefined() || resetService.notifyReset(isHard) || isHard) { + resetAccepted = true; + resetService.initiateReset(isHard); + } } } -std::unique_ptr Reset::createConf() { +std::unique_ptr v16::Reset::createConf() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = resetAccepted ? "Accepted" : "Rejected"; return doc; } +#endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 -#include +using namespace MicroOcpp; -namespace MicroOcpp { -namespace Ocpp201 { +v201::Reset::Reset(ResetService& resetService) : MemoryManaged("v201.Operation.", "Reset"), resetService(resetService) { -Reset::Reset(ResetService& resetService) : MemoryManaged("v201.Operation.", "Reset"), resetService(resetService) { - } -const char* Reset::getOperationType(){ +const char* v201::Reset::getOperationType(){ return "Reset"; } -void Reset::processReq(JsonObject payload) { +void v201::Reset::processReq(JsonObject payload) { - ResetType type; + MO_ResetType type; const char *typeCstr = payload["type"] | "_Undefined"; - + if (!strcmp(typeCstr, "Immediate")) { - type = ResetType_Immediate; + type = MO_ResetType_Immediate; } else if (!strcmp(typeCstr, "OnIdle")) { - type = ResetType_OnIdle; + type = MO_ResetType_OnIdle; } else { errorCode = "FormationViolation"; return; @@ -91,7 +84,7 @@ void Reset::processReq(JsonObject payload) { status = resetService.initiateReset(type, evseId); } -std::unique_ptr Reset::createConf() { +std::unique_ptr v201::Reset::createConf() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); @@ -114,6 +107,4 @@ std::unique_ptr Reset::createConf() { return doc; } -} //end namespace Ocpp201 -} //end namespace MicroOcpp #endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/Reset.h b/src/MicroOcpp/Operations/Reset.h index 53aca0d0..19c6b4db 100644 --- a/src/MicroOcpp/Operations/Reset.h +++ b/src/MicroOcpp/Operations/Reset.h @@ -2,25 +2,27 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef RESET_H -#define RESET_H +#ifndef MO_RESET_H +#define MO_RESET_H #include #include #include +#include -namespace MicroOcpp { +#if MO_ENABLE_V16 -class Model; +namespace MicroOcpp { +namespace v16 { -namespace Ocpp16 { +class ResetService; class Reset : public Operation, public MemoryManaged { private: - Model& model; + ResetService& resetService; bool resetAccepted {false}; public: - Reset(Model& model); + Reset(ResetService& resetService); const char* getOperationType() override; @@ -29,13 +31,14 @@ class Reset : public Operation, public MemoryManaged { std::unique_ptr createConf() override; }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #if MO_ENABLE_V201 namespace MicroOcpp { -namespace Ocpp201 { +namespace v201 { class ResetService; @@ -56,8 +59,7 @@ class Reset : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp201 -} //end namespace MicroOcpp - +} //namespace v201 +} //namespace MicroOcpp #endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Operations/SecurityEventNotification.cpp b/src/MicroOcpp/Operations/SecurityEventNotification.cpp index d4837315..78cacbd7 100644 --- a/src/MicroOcpp/Operations/SecurityEventNotification.cpp +++ b/src/MicroOcpp/Operations/SecurityEventNotification.cpp @@ -2,18 +2,20 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_V201 - #include + +#include #include -using MicroOcpp::Ocpp201::SecurityEventNotification; -using MicroOcpp::JsonDoc; +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SECURITY_EVENT -SecurityEventNotification::SecurityEventNotification(const char *type, const Timestamp& timestamp) : MemoryManaged("v201.Operation.", "SecurityEventNotification"), type(makeString(getMemoryTag(), type ? type : "")), timestamp(timestamp) { +using namespace MicroOcpp; +SecurityEventNotification::SecurityEventNotification(Context& context, const char *type, const Timestamp& timestamp) : MemoryManaged("v201.Operation.", "SecurityEventNotification"), context(context), timestamp(timestamp) { + auto ret = snprintf(this->type, sizeof(this->type), "%s", type); + if (ret < 0 || (size_t)ret >= sizeof(this->type)) { + MO_DBG_ERR("type too long (ignored excess characters)"); + } } const char* SecurityEventNotification::getOperationType(){ @@ -24,14 +26,17 @@ std::unique_ptr SecurityEventNotification::createReq() { auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2) + - JSONDATE_LENGTH + 1); + MO_JSONDATE_SIZE); JsonObject payload = doc->to(); - payload["type"] = type.c_str(); + payload["type"] = (const char*)type; //force zero-copy - char timestampStr [JSONDATE_LENGTH + 1]; - timestamp.toJsonString(timestampStr, sizeof(timestampStr)); + char timestampStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!context.getClock().toJsonString(timestamp, timestampStr, sizeof(timestampStr))) { + MO_DBG_ERR("internal error"); + timestampStr[0] = '\0'; + } payload["timestamp"] = timestampStr; return doc; @@ -51,4 +56,4 @@ std::unique_ptr SecurityEventNotification::createConf() { return createEmptyDocument(); } -#endif // MO_ENABLE_V201 +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SECURITY_EVENT diff --git a/src/MicroOcpp/Operations/SecurityEventNotification.h b/src/MicroOcpp/Operations/SecurityEventNotification.h index 5d618c00..09a61bf4 100644 --- a/src/MicroOcpp/Operations/SecurityEventNotification.h +++ b/src/MicroOcpp/Operations/SecurityEventNotification.h @@ -1,30 +1,29 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_SECURITYEVENTNOTIFICATION_H #define MO_SECURITYEVENTNOTIFICATION_H -#include - -#if MO_ENABLE_V201 - #include #include #include +#include +#include -namespace MicroOcpp { +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SECURITY_EVENT -namespace Ocpp201 { +namespace MicroOcpp { class SecurityEventNotification : public Operation, public MemoryManaged { private: - String type; + Context& context; + char type [MO_SECURITY_EVENT_TYPE_LENGTH + 1]; Timestamp timestamp; const char *errorCode = nullptr; public: - SecurityEventNotification(const char *type, const Timestamp& timestamp); + SecurityEventNotification(Context& context, const char *type, const Timestamp& timestamp); // the length of type, not counting the terminating 0, must be <= MO_SECURITY_EVENT_TYPE_LENGTH. Excess characters are cropped const char* getOperationType() override; @@ -40,9 +39,6 @@ class SecurityEventNotification : public Operation, public MemoryManaged { }; -} //namespace Ocpp201 } //namespace MicroOcpp - -#endif //MO_ENABLE_V201 - +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SECURITY_EVENT #endif diff --git a/src/MicroOcpp/Operations/SendLocalList.cpp b/src/MicroOcpp/Operations/SendLocalList.cpp index 45f0dfbe..469437e7 100644 --- a/src/MicroOcpp/Operations/SendLocalList.cpp +++ b/src/MicroOcpp/Operations/SendLocalList.cpp @@ -2,23 +2,21 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_LOCAL_AUTH - #include #include #include -using MicroOcpp::Ocpp16::SendLocalList; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; SendLocalList::SendLocalList(AuthorizationService& authService) : MemoryManaged("v16.Operation.", "SendLocalList"), authService(authService) { - + } SendLocalList::~SendLocalList() { - + } const char* SendLocalList::getOperationType(){ @@ -67,8 +65,8 @@ std::unique_ptr SendLocalList::createConf(){ } else { payload["status"] = "Accepted"; } - + return doc; } -#endif //MO_ENABLE_LOCAL_AUTH +#endif //MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Operations/SendLocalList.h b/src/MicroOcpp/Operations/SendLocalList.h index d58265bd..02881e33 100644 --- a/src/MicroOcpp/Operations/SendLocalList.h +++ b/src/MicroOcpp/Operations/SendLocalList.h @@ -5,18 +5,16 @@ #ifndef MO_SENDLOCALLIST_H #define MO_SENDLOCALLIST_H +#include #include -#if MO_ENABLE_LOCAL_AUTH - -#include +#if MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH namespace MicroOcpp { +namespace v16 { class AuthorizationService; -namespace Ocpp16 { - class SendLocalList : public Operation, public MemoryManaged { private: AuthorizationService& authService; @@ -37,8 +35,7 @@ class SendLocalList : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp - -#endif //MO_ENABLE_LOCAL_AUTH +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_LOCAL_AUTH #endif diff --git a/src/MicroOcpp/Operations/SetChargingProfile.cpp b/src/MicroOcpp/Operations/SetChargingProfile.cpp index 1ff28acd..aba8350c 100644 --- a/src/MicroOcpp/Operations/SetChargingProfile.cpp +++ b/src/MicroOcpp/Operations/SetChargingProfile.cpp @@ -3,15 +3,19 @@ // MIT License #include + +#include #include #include -#include +#include +#include #include -using MicroOcpp::Ocpp16::SetChargingProfile; -using MicroOcpp::JsonDoc; +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING + +using namespace MicroOcpp; -SetChargingProfile::SetChargingProfile(Model& model, SmartChargingService& scService) : MemoryManaged("v16.Operation.", "SetChargingProfile"), model(model), scService(scService) { +SetChargingProfile::SetChargingProfile(Context& context, SmartChargingService& scService) : MemoryManaged("v16.Operation.", "SetChargingProfile"), context(context), scService(scService), ocppVersion(context.getOcppVersion()) { } @@ -25,64 +29,117 @@ const char* SetChargingProfile::getOperationType(){ void SetChargingProfile::processReq(JsonObject payload) { - int connectorId = payload["connectorId"] | -1; - if (connectorId < 0 || !payload.containsKey("csChargingProfiles")) { - errorCode = "FormationViolation"; - return; + int evseId = -1; + JsonObject chargingProfileJson; + + #if MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + evseId = payload["connectorId"] | -1; + if (evseId < 0 || !payload.containsKey("csChargingProfiles")) { + errorCode = "FormationViolation"; + return; + } + chargingProfileJson = payload["csChargingProfiles"]; + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + evseId = payload["evseId"] | -1; + if (evseId < 0 || !payload.containsKey("chargingProfile")) { + errorCode = "FormationViolation"; + return; + } + chargingProfileJson = payload["chargingProfile"]; } + #endif //MO_ENABLE_V201 - if ((unsigned int) connectorId >= model.getNumConnectors()) { + if ((unsigned int) evseId >= context.getModelCommon().getNumEvseId()) { errorCode = "PropertyConstraintViolation"; return; } - JsonObject csChargingProfiles = payload["csChargingProfiles"]; - - auto chargingProfile = loadChargingProfile(csChargingProfiles); + auto chargingProfile = std::unique_ptr(new ChargingProfile()); if (!chargingProfile) { - errorCode = "PropertyConstraintViolation"; - errorDescription = "csChargingProfiles validation failed"; + MO_DBG_ERR("OOM"); + errorCode = "InternalError"; return; } - if (chargingProfile->getChargingProfilePurpose() == ChargingProfilePurposeType::TxProfile) { + bool valid = chargingProfile->parseJson(context.getClock(), ocppVersion, chargingProfileJson); + if (!valid) { + errorCode = "FormationViolation"; + errorDescription = "chargingProfile validation failed"; + return; + } + + if (chargingProfile->chargingProfilePurpose == ChargingProfilePurposeType::TxProfile) { // if TxProfile, check if a transaction is running - if (connectorId == 0) { + if (evseId == 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 MO_ENABLE_V16 + if (ocppVersion == MO_OCPP_V16) { + auto txService = context.getModel16().getTransactionService(); + auto txServiceEvse = txService ? txService->getEvse(evseId) : nullptr; + if (!txServiceEvse) { + errorCode = "InternalError"; + return; + } + + auto transaction = txServiceEvse->getTransaction(); + if (!transaction || !transaction->isRunning()) { + //no transaction running, reject profile + accepted = false; + return; + } + + if (chargingProfile->transactionId16 >= 0 && + chargingProfile->transactionId16 != transaction->getTransactionId()) { + //transactionId undefined / mismatch + accepted = false; + return; + } } - - if (chargingProfile->getTransactionId() >= 0 && - chargingProfile->getTransactionId() != transaction->getTransactionId()) { - //transactionId undefined / mismatch - accepted = false; - return; + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (ocppVersion == MO_OCPP_V201) { + auto txService = context.getModel201().getTransactionService(); + auto txServiceEvse = txService ? txService->getEvse(evseId) : nullptr; + if (!txServiceEvse) { + errorCode = "InternalError"; + return; + } + + auto transaction = txServiceEvse->getTransaction(); + if (!transaction || !(transaction->started && !transaction->stopped)) { + //no transaction running, reject profile + accepted = false; + return; + } + + if (*chargingProfile->transactionId201 >= 0 && + strcmp(chargingProfile->transactionId201, transaction->transactionId)) { + //transactionId undefined / mismatch + accepted = false; + return; + } } + #endif //MO_ENABLE_V201 //seems good - } else if (chargingProfile->getChargingProfilePurpose() == ChargingProfilePurposeType::ChargePointMaxProfile) { - if (connectorId > 0) { + } else if (chargingProfile->chargingProfilePurpose == ChargingProfilePurposeType::ChargePointMaxProfile) { + if (evseId > 0) { errorCode = "PropertyConstraintViolation"; errorDescription = "Cannot set ChargePointMaxProfile at connectorId > 0"; return; } } - accepted = scService.setChargingProfile(connectorId, std::move(chargingProfile)); + accepted = scService.setChargingProfile(evseId, std::move(chargingProfile)); } std::unique_ptr SetChargingProfile::createConf(){ @@ -95,3 +152,5 @@ std::unique_ptr SetChargingProfile::createConf(){ } return doc; } + +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING diff --git a/src/MicroOcpp/Operations/SetChargingProfile.h b/src/MicroOcpp/Operations/SetChargingProfile.h index 1ce35f1e..22559b63 100644 --- a/src/MicroOcpp/Operations/SetChargingProfile.h +++ b/src/MicroOcpp/Operations/SetChargingProfile.h @@ -6,24 +6,26 @@ #define MO_SETCHARGINGPROFILE_H #include +#include + +#if (MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING namespace MicroOcpp { -class Model; +class Context; class SmartChargingService; -namespace Ocpp16 { - class SetChargingProfile : public Operation, public MemoryManaged { private: - Model& model; + Context& context; SmartChargingService& scService; + int ocppVersion = -1; bool accepted = false; const char *errorCode = nullptr; const char *errorDescription = ""; public: - SetChargingProfile(Model& model, SmartChargingService& scService); + SetChargingProfile(Context& context, SmartChargingService& scService); ~SetChargingProfile(); @@ -37,6 +39,6 @@ class SetChargingProfile : public Operation, public MemoryManaged { const char *getErrorDescription() override {return errorDescription;} }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace MicroOcpp +#endif //(MO_ENABLE_V16 || MO_ENABLE_V201) && MO_ENABLE_SMARTCHARGING #endif diff --git a/src/MicroOcpp/Operations/SetVariables.cpp b/src/MicroOcpp/Operations/SetVariables.cpp index 23bd043d..7e407477 100644 --- a/src/MicroOcpp/Operations/SetVariables.cpp +++ b/src/MicroOcpp/Operations/SetVariables.cpp @@ -2,24 +2,21 @@ // 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; +#if MO_ENABLE_V201 + +using namespace MicroOcpp; +using namespace MicroOcpp::v201; 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(){ @@ -87,7 +84,7 @@ void SetVariables::processReq(JsonObject payload) { query.attributeStatus = variableService.setVariable( query.attributeType, query.attributeValue, - ComponentId(query.componentName.c_str(), + ComponentId(query.componentName.c_str(), EvseId(query.componentEvseId, query.componentEvseConnectorId)), query.variableName.c_str()); } @@ -102,7 +99,7 @@ void SetVariables::processReq(JsonObject payload) { std::unique_ptr SetVariables::createConf(){ size_t capacity = JSON_ARRAY_SIZE(queries.size()); for (const auto& data : queries) { - capacity += + capacity += JSON_OBJECT_SIZE(5) + // setVariableResult JSON_OBJECT_SIZE(2) + // component data.componentName.length() + 1 + @@ -182,4 +179,4 @@ std::unique_ptr SetVariables::createConf(){ return doc; } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/SetVariables.h b/src/MicroOcpp/Operations/SetVariables.h index fb0ca889..55cf9b2c 100644 --- a/src/MicroOcpp/Operations/SetVariables.h +++ b/src/MicroOcpp/Operations/SetVariables.h @@ -5,20 +5,18 @@ #ifndef MO_SETVARIABLES_H #define MO_SETVARIABLES_H -#include - -#if MO_ENABLE_V201 - #include #include #include +#include + +#if MO_ENABLE_V201 namespace MicroOcpp { +namespace v201 { class VariableService; -namespace Ocpp201 { - // SetVariableDataType (2.44) and // SetVariableResultType (2.45) struct SetVariableData { @@ -55,7 +53,7 @@ class SetVariables : public Operation, public MemoryManaged { }; -} //namespace Ocpp201 +} //namespace v201 } //namespace MicroOcpp #endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/StartTransaction.cpp b/src/MicroOcpp/Operations/StartTransaction.cpp index 907baaf3..2df04446 100644 --- a/src/MicroOcpp/Operations/StartTransaction.cpp +++ b/src/MicroOcpp/Operations/StartTransaction.cpp @@ -3,24 +3,26 @@ // MIT License #include + +#include #include #include #include #include #include #include -#include -using MicroOcpp::Ocpp16::StartTransaction; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 + +using namespace MicroOcpp; +using namespace MicroOcpp::v16; +StartTransaction::StartTransaction(Context& context, Transaction *transaction) : MemoryManaged("v16.Operation.", "StartTransaction"), context(context), transaction(transaction) { -StartTransaction::StartTransaction(Model& model, std::shared_ptr transaction) : MemoryManaged("v16.Operation.", "StartTransaction"), model(model), transaction(transaction) { - } StartTransaction::~StartTransaction() { - + } const char* StartTransaction::getOperationType() { @@ -30,29 +32,24 @@ const char* StartTransaction::getOperationType() { std::unique_ptr StartTransaction::createReq() { auto doc = makeJsonDoc(getMemoryTag(), - JSON_OBJECT_SIZE(6) + - (IDTAG_LEN_MAX + 1) + - (JSONDATE_LENGTH + 1)); - + JSON_OBJECT_SIZE(6) + + MO_JSONDATE_SIZE); + JsonObject payload = doc->to(); payload["connectorId"] = transaction->getConnectorId(); - payload["idTag"] = (char*) transaction->getIdTag(); + payload["idTag"] = 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[MO_JSONDATE_SIZE] = {'\0'}; + if (!context.getClock().toJsonString(transaction->getStartTimestamp(), timestamp, sizeof(timestamp))) { + MO_DBG_ERR("internal error"); + timestamp[0] = '\0'; } - - char timestamp[JSONDATE_LENGTH + 1] = {'\0'}; - transaction->getStartTimestamp().toJsonString(timestamp, JSONDATE_LENGTH + 1); payload["timestamp"] = timestamp; return doc; @@ -77,31 +74,25 @@ void StartTransaction::processConf(JsonObject payload) { } transaction->getStartSync().confirm(); - transaction->commit(); + + if (auto filesystem = context.getFilesystem()) { + TransactionStore::store(filesystem, context, *transaction); + } #if MO_ENABLE_LOCAL_AUTH - if (auto authService = model.getAuthorizationService()) { + if (auto authService = context.getModel16().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"; +#if MO_ENABLE_MOCK_SERVER +int StartTransaction::writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData) { + (void)userStatus; + (void)userData; static int uniqueTxId = 1000; - payload["transactionId"] = uniqueTxId++; //sample data for debug purpose - - return doc; + return snprintf(buf, size, "{\"idTagInfo\":{\"status\":\"Accepted\"}, \"transactionId\":%i}", uniqueTxId++); } +#endif //MO_ENABLE_MOCK_SERVER + +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Operations/StartTransaction.h b/src/MicroOcpp/Operations/StartTransaction.h index a73b5368..9a15ae78 100644 --- a/src/MicroOcpp/Operations/StartTransaction.h +++ b/src/MicroOcpp/Operations/StartTransaction.h @@ -7,23 +7,25 @@ #include #include -#include -#include +#include + +#if MO_ENABLE_V16 namespace MicroOcpp { -class Model; -class Transaction; +class Context; + +namespace v16 { -namespace Ocpp16 { +class Transaction; class StartTransaction : public Operation, public MemoryManaged { private: - Model& model; - std::shared_ptr transaction; + Context& context; + Transaction *transaction = nullptr; public: - StartTransaction(Model& model, std::shared_ptr transaction); + StartTransaction(Context& context, Transaction *transaction); ~StartTransaction(); @@ -33,11 +35,12 @@ class StartTransaction : public Operation, public MemoryManaged { void processConf(JsonObject payload) override; - void processReq(JsonObject payload) override; - - std::unique_ptr createConf() override; +#if MO_ENABLE_MOCK_SERVER + static int writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData); +#endif }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #endif diff --git a/src/MicroOcpp/Operations/StatusNotification.cpp b/src/MicroOcpp/Operations/StatusNotification.cpp index c5fb1a60..3660b05b 100644 --- a/src/MicroOcpp/Operations/StatusNotification.cpp +++ b/src/MicroOcpp/Operations/StatusNotification.cpp @@ -3,62 +3,31 @@ // MIT License #include + +#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"; - } -} +#if MO_ENABLE_V16 + +using namespace MicroOcpp; -namespace Ocpp16 { +v16::StatusNotification::StatusNotification(Context& context, int connectorId, MO_ChargePointStatus currentStatus, const Timestamp ×tamp, MO_ErrorData errorData) + : MemoryManaged("v16.Operation.", "StatusNotification"), context(context), connectorId(connectorId), currentStatus(currentStatus), timestamp(timestamp), errorData(errorData) { -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); + if (currentStatus != MO_ChargePointStatus_UNDEFINED) { + MO_DBG_INFO("New status: %s (connectorId %d)", mo_serializeChargePointStatus(currentStatus), connectorId); } } -const char* StatusNotification::getOperationType(){ +const char* v16::StatusNotification::getOperationType(){ return "StatusNotification"; } -std::unique_ptr StatusNotification::createReq() { - auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(7) + (JSONDATE_LENGTH + 1)); +std::unique_ptr v16::StatusNotification::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(7) + MO_JSONDATE_SIZE); JsonObject payload = doc->to(); payload["connectorId"] = connectorId; @@ -75,23 +44,26 @@ std::unique_ptr StatusNotification::createReq() { if (errorData.vendorErrorCode) { payload["vendorErrorCode"] = errorData.vendorErrorCode; } - } else if (currentStatus == ChargePointStatus_UNDEFINED) { + } else if (currentStatus == MO_ChargePointStatus_UNDEFINED) { MO_DBG_ERR("Reporting undefined status"); payload["errorCode"] = "InternalError"; } else { payload["errorCode"] = "NoError"; } - payload["status"] = cstrFromOcppEveState(currentStatus); + payload["status"] = mo_serializeChargePointStatus(currentStatus); - char timestamp_cstr[JSONDATE_LENGTH + 1] = {'\0'}; - timestamp.toJsonString(timestamp_cstr, JSONDATE_LENGTH + 1); - payload["timestamp"] = timestamp_cstr; + char timestampStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!context.getClock().toJsonString(timestamp, timestampStr, sizeof(timestampStr))) { + MO_DBG_ERR("internal error"); + timestampStr[0] = '\0'; + } + payload["timestamp"] = timestampStr; return doc; } -void StatusNotification::processConf(JsonObject payload) { +void v16::StatusNotification::processConf(JsonObject payload) { /* * Empty payload */ @@ -100,42 +72,43 @@ void StatusNotification::processConf(JsonObject payload) { /* * For debugging only */ -void StatusNotification::processReq(JsonObject payload) { +void v16::StatusNotification::processReq(JsonObject payload) { } /* * For debugging only */ -std::unique_ptr StatusNotification::createConf(){ +std::unique_ptr v16::StatusNotification::createConf(){ return createEmptyDocument(); } -} // namespace Ocpp16 -} // namespace MicroOcpp +#endif //MO_ENABLE_V16 #if MO_ENABLE_V201 -namespace MicroOcpp { -namespace Ocpp201 { +using namespace MicroOcpp; -StatusNotification::StatusNotification(EvseId evseId, ChargePointStatus currentStatus, const Timestamp ×tamp) - : MemoryManaged("v201.Operation.", "StatusNotification"), evseId(evseId), timestamp(timestamp), currentStatus(currentStatus) { +v201::StatusNotification::StatusNotification(Context& context, EvseId evseId, MO_ChargePointStatus currentStatus, const Timestamp ×tamp) + : MemoryManaged("v201.Operation.", "StatusNotification"), context(context), evseId(evseId), timestamp(timestamp), currentStatus(currentStatus) { } -const char* StatusNotification::getOperationType(){ +const char* v201::StatusNotification::getOperationType(){ return "StatusNotification"; } -std::unique_ptr StatusNotification::createReq() { - auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(4) + (JSONDATE_LENGTH + 1)); +std::unique_ptr v201::StatusNotification::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(4) + MO_JSONDATE_SIZE); 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); + char timestampStr [MO_JSONDATE_SIZE] = {'\0'}; + if (!context.getClock().toJsonString(timestamp, timestampStr, sizeof(timestampStr))) { + MO_DBG_ERR("internal error"); + timestampStr[0] = '\0'; + } + payload["timestamp"] = timestampStr; + payload["connectorStatus"] = mo_serializeChargePointStatus(currentStatus); payload["evseId"] = evseId.id; payload["connectorId"] = evseId.id == 0 ? 0 : evseId.connectorId >= 0 ? evseId.connectorId : 1; @@ -143,13 +116,24 @@ std::unique_ptr StatusNotification::createReq() { } -void StatusNotification::processConf(JsonObject payload) { +void v201::StatusNotification::processConf(JsonObject payload) { /* * Empty payload */ } -} // namespace Ocpp201 -} // namespace MicroOcpp +/* + * For debugging only + */ +void v201::StatusNotification::processReq(JsonObject payload) { + +} + +/* + * For debugging only + */ +std::unique_ptr v201::StatusNotification::createConf(){ + return createEmptyDocument(); +} #endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/StatusNotification.h b/src/MicroOcpp/Operations/StatusNotification.h index 2e65dcec..f588c9ae 100644 --- a/src/MicroOcpp/Operations/StatusNotification.h +++ b/src/MicroOcpp/Operations/StatusNotification.h @@ -2,29 +2,32 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef STATUSNOTIFICATION_H -#define STATUSNOTIFICATION_H +#ifndef MO_STATUSNOTIFICATION_H +#define MO_STATUSNOTIFICATION_H #include #include -#include -#include +#include +#include #include +#if MO_ENABLE_V16 + namespace MicroOcpp { - -const char *cstrFromOcppEveState(ChargePointStatus state); -namespace Ocpp16 { +class Context; + +namespace v16 { class StatusNotification : public Operation, public MemoryManaged { private: + Context& context; int connectorId = 1; - ChargePointStatus currentStatus = ChargePointStatus_UNDEFINED; + MO_ChargePointStatus currentStatus = MO_ChargePointStatus_UNDEFINED; Timestamp timestamp; - ErrorData errorData; + MO_ErrorData errorData; public: - StatusNotification(int connectorId, ChargePointStatus currentStatus, const Timestamp ×tamp, ErrorData errorData = nullptr); + StatusNotification(Context& context, int connectorId, MO_ChargePointStatus currentStatus, const Timestamp ×tamp, MO_ErrorData errorData); const char* getOperationType() override; @@ -35,40 +38,38 @@ class StatusNotification : public Operation, public MemoryManaged { void processReq(JsonObject payload) override; std::unique_ptr createConf() override; - - int getConnectorId() { - return connectorId; - } }; -} // namespace Ocpp16 -} // namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #if MO_ENABLE_V201 -#include - namespace MicroOcpp { -namespace Ocpp201 { +namespace v201 { class StatusNotification : public Operation, public MemoryManaged { private: + Context& context; EvseId evseId; Timestamp timestamp; - ChargePointStatus currentStatus = ChargePointStatus_UNDEFINED; + MO_ChargePointStatus currentStatus = MO_ChargePointStatus_UNDEFINED; public: - StatusNotification(EvseId evseId, ChargePointStatus currentStatus, const Timestamp ×tamp); + StatusNotification(Context& context, EvseId evseId, MO_ChargePointStatus currentStatus, const Timestamp ×tamp); const char* getOperationType() override; std::unique_ptr createReq() override; void processConf(JsonObject payload) override; -}; -} // namespace Ocpp201 -} // namespace MicroOcpp + void processReq(JsonObject payload) override; -#endif //MO_ENABLE_V201 + std::unique_ptr createConf() override; +}; +} //namespace v201 +} //namespace MicroOcpp +#endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Operations/StopTransaction.cpp b/src/MicroOcpp/Operations/StopTransaction.cpp index a3aef101..c6c2d94b 100644 --- a/src/MicroOcpp/Operations/StopTransaction.cpp +++ b/src/MicroOcpp/Operations/StopTransaction.cpp @@ -3,6 +3,8 @@ // MIT License #include + +#include #include #include #include @@ -10,18 +12,14 @@ #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) { +#if MO_ENABLE_V16 -} +using namespace MicroOcpp; +using namespace MicroOcpp::v16; -StopTransaction::StopTransaction(Model& model, std::shared_ptr transaction, Vector> transactionData) - : MemoryManaged("v16.Operation.", "StopTransaction"), model(model), transaction(transaction), transactionData(std::move(transactionData)) { +StopTransaction::StopTransaction(Context& context, Transaction *transaction) + : MemoryManaged("v16.Operation.", "StopTransaction"), context(context), transaction(transaction) { } @@ -31,85 +29,58 @@ const char* StopTransaction::getOperationType() { 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); - } - } - } + size_t capacity = + JSON_OBJECT_SIZE(6) + //total of 6 fields + MO_JSONDATE_SIZE; //timestamp string - 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; + auto& txMeterValues = transaction->getTxMeterValues(); + capacity += JSON_ARRAY_SIZE(txMeterValues.size()); + for (size_t i = 0; i < txMeterValues.size(); i++) { + int ret = txMeterValues[i]->getJsonCapacity(MO_OCPP_V16, /*internalFormat*/ false); + if (ret >= 0) { + capacity += (size_t)ret; + } else { + MO_DBG_ERR("tx meter values error"); } - 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()); + auto doc = makeJsonDoc(getMemoryTag(), capacity); JsonObject payload = doc->to(); if (transaction->getStopIdTag() && *transaction->getStopIdTag()) { - payload["idTag"] = (char*) transaction->getStopIdTag(); + payload["idTag"] = transaction->getStopIdTag(); } payload["meterStop"] = transaction->getMeterStop(); - char timestamp[JSONDATE_LENGTH + 1] = {'\0'}; - transaction->getStopTimestamp().toJsonString(timestamp, JSONDATE_LENGTH + 1); + char timestamp [MO_JSONDATE_SIZE]; + if (!context.getClock().toJsonString(transaction->getStopTimestamp(), timestamp, sizeof(timestamp))) { + MO_DBG_ERR("internal error"); + } payload["timestamp"] = timestamp; payload["transactionId"] = transaction->getTransactionId(); - + if (transaction->getStopReason() && *transaction->getStopReason()) { - payload["reason"] = (char*) transaction->getStopReason(); + payload["reason"] = transaction->getStopReason(); } - if (!transactionData.empty()) { - payload["transactionData"] = txDataDoc; + if (!txMeterValues.empty()) { + JsonArray txMeterValuesJson = payload.createNestedArray("transactionData"); + + for (size_t i = 0; i < txMeterValues.size(); i++) { + auto mv = txMeterValues[i]; + auto mvJson = txMeterValuesJson.createNestedObject(); + + if (mv->getJsonCapacity(MO_OCPP_V16, /*internalFormat*/ false) < 0) { + //measurement has failed - ensure that serialization won't be attempted + continue; + } + + if (!mv->toJson(context.getClock(), MO_OCPP_V16, /*internal format*/ false, mvJson)) { + MO_DBG_ERR("serialization error"); + } + } } return doc; @@ -117,44 +88,23 @@ std::unique_ptr StopTransaction::createReq() { void StopTransaction::processConf(JsonObject payload) { - if (transaction) { - transaction->getStopSync().confirm(); - transaction->commit(); - } + transaction->getStopSync().confirm(); MO_DBG_INFO("Request has been accepted!"); #if MO_ENABLE_LOCAL_AUTH - if (auto authService = model.getAuthorizationService()) { + if (auto authService = context.getModel16().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 - */ +#if MO_ENABLE_MOCK_SERVER +int StopTransaction::writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData) { + (void)userStatus; + (void)userData; + return snprintf(buf, size, "{\"idTagInfo\":{\"status\":\"Accepted\"}}"); } +#endif //MO_ENABLE_MOCK_SERVER -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; -} +#endif //MO_ENABLE_V16 diff --git a/src/MicroOcpp/Operations/StopTransaction.h b/src/MicroOcpp/Operations/StopTransaction.h index c573135c..8b1ce41b 100644 --- a/src/MicroOcpp/Operations/StopTransaction.h +++ b/src/MicroOcpp/Operations/StopTransaction.h @@ -8,29 +8,25 @@ #include #include #include -#include +#include + +#if MO_ENABLE_V16 namespace MicroOcpp { -class Model; +class Context; -class SampledValue; -class MeterValue; +namespace v16 { class Transaction; -namespace Ocpp16 { - class StopTransaction : public Operation, public MemoryManaged { private: - Model& model; - std::shared_ptr transaction; - Vector> transactionData; + Context& context; + Transaction *transaction = nullptr; public: - StopTransaction(Model& model, std::shared_ptr transaction); - - StopTransaction(Model& model, std::shared_ptr transaction, Vector> transactionData); + StopTransaction(Context& context, Transaction *transaction); const char* getOperationType() override; @@ -38,13 +34,12 @@ class StopTransaction : public Operation, public MemoryManaged { 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; +#if MO_ENABLE_MOCK_SERVER + static int writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData); +#endif }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 #endif diff --git a/src/MicroOcpp/Operations/TransactionEvent.cpp b/src/MicroOcpp/Operations/TransactionEvent.cpp index 12d91afb..55e5d06d 100644 --- a/src/MicroOcpp/Operations/TransactionEvent.cpp +++ b/src/MicroOcpp/Operations/TransactionEvent.cpp @@ -2,20 +2,20 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - -#if MO_ENABLE_V201 - #include + +#include #include #include #include -using namespace MicroOcpp::Ocpp201; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V201 + +using namespace MicroOcpp; +using namespace MicroOcpp::v201; -TransactionEvent::TransactionEvent(Model& model, TransactionEventData *txEvent) - : MemoryManaged("v201.Operation.", "TransactionEvent"), model(model), txEvent(txEvent) { +TransactionEvent::TransactionEvent(Context& context, TransactionEventData *txEvent) + : MemoryManaged("v201.Operation.", "TransactionEvent"), context(context), txEvent(txEvent) { } @@ -29,36 +29,33 @@ std::unique_ptr TransactionEvent::createReq() { 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(); + capacity += txEvent->transaction->sampledDataTxEnded[i]->getJsonCapacity(MO_OCPP_V201, /* internalFormat */ false); } } 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 += txEvent->meterValue[i]->getJsonCapacity(MO_OCPP_V201, /* internalFormat */ false); } 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 + MO_JSONDATE_SIZE + //timestamp string + JSON_OBJECT_SIZE(5); //transactionInfo 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); + char timestamp [MO_JSONDATE_SIZE]; + if (!context.getClock().toJsonString(txEvent->timestamp, timestamp, sizeof(timestamp))) { + MO_DBG_ERR("internal error"); + timestamp[0] = '\0'; + } payload["timestamp"] = timestamp; - if (serializeTransactionEventTriggerReason(txEvent->triggerReason)) { - payload["triggerReason"] = serializeTransactionEventTriggerReason(txEvent->triggerReason); + if (serializeTxEventTriggerReason(txEvent->triggerReason)) { + payload["triggerReason"] = serializeTxEventTriggerReason(txEvent->triggerReason); } else { MO_DBG_ERR("serialization error"); } @@ -82,13 +79,13 @@ std::unique_ptr TransactionEvent::createReq() { } JsonObject transactionInfo = payload.createNestedObject("transactionInfo"); - transactionInfo["transactionId"] = txEvent->transaction->transactionId; + transactionInfo["transactionId"] = (const char*)txEvent->transaction->transactionId; //force zero-copy if (serializeTransactionEventChargingState(txEvent->chargingState)) { // optional transactionInfo["chargingState"] = serializeTransactionEventChargingState(txEvent->chargingState); } - if (txEvent->transaction->stoppedReason != Transaction::StoppedReason::Local && + if (txEvent->transaction->stoppedReason != MO_TxStoppedReason_Local && serializeTransactionStoppedReason(txEvent->transaction->stoppedReason)) { // optional transactionInfo["stoppedReason"] = serializeTransactionStoppedReason(txEvent->transaction->stoppedReason); } @@ -113,16 +110,14 @@ std::unique_ptr TransactionEvent::createReq() { 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); + JsonObject meterValueJson = payload["meterValue"].createNestedObject(); + txEvent->transaction->sampledDataTxEnded[i]->toJson(context.getClock(), MO_OCPP_V201, /*internal format*/ false, meterValueJson); } } for (size_t i = 0; i < txEvent->meterValue.size(); i++) { - JsonDoc meterValueJson = initJsonDoc(getMemoryTag()); - txEvent->meterValue[i]->toJson(meterValueJson); - payload["meterValue"].add(meterValueJson); + JsonObject meterValueJson = payload["meterValue"].createNestedObject(); + txEvent->meterValue[i]->toJson(context.getClock(), MO_OCPP_V201, /*internal format*/ false, meterValueJson); } return doc; @@ -139,14 +134,28 @@ void TransactionEvent::processConf(JsonObject payload) { } } -void TransactionEvent::processReq(JsonObject payload) { - /** - * Ignore Contents of this Req-message, because this is for debug purposes only - */ +#if MO_ENABLE_MOCK_SERVER +void TransactionEvent::onRequestMock(const char *operationType, const char *payloadJson, void **userStatus, void *userData) { + //if request contains `"idToken"`, then send response with `"idTokenInfo"`. Instead of building the + //full JSON DOM, just search for the substring + if (strstr(payloadJson, "\"idToken\"")) { + //found, pass status to `writeMockConf` + *userStatus = reinterpret_cast(1); + } else { + //not found, pass status to `writeMockConf` + *userStatus = reinterpret_cast(0); + } } -std::unique_ptr TransactionEvent::createConf() { - return createEmptyDocument(); +int TransactionEvent::writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData) { + (void)userData; + + if (userStatus == reinterpret_cast(1)) { + return snprintf(buf, size, "{\"idTokenInfo\":{\"status\":\"Accepted\"}}"); + } else { + return snprintf(buf, size, "{}"); + } } +#endif //MO_ENABLE_MOCK_SERVER -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/TransactionEvent.h b/src/MicroOcpp/Operations/TransactionEvent.h index af8bbf86..edea7fb2 100644 --- a/src/MicroOcpp/Operations/TransactionEvent.h +++ b/src/MicroOcpp/Operations/TransactionEvent.h @@ -5,29 +5,28 @@ #ifndef MO_TRANSACTIONEVENT_H #define MO_TRANSACTIONEVENT_H +#include #include #if MO_ENABLE_V201 -#include - namespace MicroOcpp { -class Model; +class Context; -namespace Ocpp201 { +namespace v201 { class TransactionEventData; class TransactionEvent : public Operation, public MemoryManaged { private: - Model& model; - TransactionEventData *txEvent; + Context& context; + TransactionEventData *txEvent; //does not take ownership const char *errorCode = nullptr; public: - TransactionEvent(Model& model, TransactionEventData *txEvent); + TransactionEvent(Context& context, TransactionEventData *txEvent); const char* getOperationType() override; @@ -37,12 +36,13 @@ class TransactionEvent : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} - void processReq(JsonObject payload) override; - - std::unique_ptr createConf() override; +#if MO_ENABLE_MOCK_SERVER + static void onRequestMock(const char *operationType, const char *payloadJson, void **userStatus, void *userData); + static int writeMockConf(const char *operationType, char *buf, size_t size, void *userStatus, void *userData); +#endif }; -} //end namespace Ocpp201 -} //end namespace MicroOcpp -#endif // MO_ENABLE_V201 +} //namespace v201 +} //namespace MicroOcpp +#endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Operations/TriggerMessage.cpp b/src/MicroOcpp/Operations/TriggerMessage.cpp index 9b930697..e86b572b 100644 --- a/src/MicroOcpp/Operations/TriggerMessage.cpp +++ b/src/MicroOcpp/Operations/TriggerMessage.cpp @@ -3,17 +3,20 @@ // MIT License #include -#include -#include + +#include #include -#include #include +#include +#include +#include #include -using MicroOcpp::Ocpp16::TriggerMessage; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 || MO_ENABLE_V201 -TriggerMessage::TriggerMessage(Context& context) : MemoryManaged("v16.Operation.", "TriggerMessage"), context(context) { +using namespace MicroOcpp; + +TriggerMessage::TriggerMessage(Context& context, RemoteControlService& rcService) : MemoryManaged("v16.Operation.", "TriggerMessage"), context(context), rcService(rcService) { } @@ -23,63 +26,66 @@ const char* TriggerMessage::getOperationType(){ 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 { + if (!payload.containsKey("requestedMessage") || !payload["requestedMessage"].is()) { + errorCode = "FormationViolation"; + return; + } + + const char *requestedMessage = payload["requestedMessage"]; + int evseId = -1; + + #if MO_ENABLE_V16 + if (context.getOcppVersion() == MO_OCPP_V16) { + evseId = payload["connectorId"] | -1; + } + #endif //MO_ENABLE_V16 + #if MO_ENABLE_V201 + if (context.getOcppVersion() == MO_OCPP_V201) { + evseId = payload["evse"]["id"] | -1; + + if ((payload["evse"]["connectorId"] | 1) != 1) { errorCode = "PropertyConstraintViolation"; + return; } + } + #endif //MO_ENABLE_V201 - 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"; - } + if (evseId >= 0 && (unsigned int)evseId >= context.getModelCommon().getNumEvseId()) { + errorCode = "PropertyConstraintViolation"; + return; + } + + MO_DBG_INFO("Execute for message type %s, evseId = %i", requestedMessage, evseId); + + status = rcService.triggerMessage(requestedMessage, evseId); + if (status == TriggerMessageStatus::ERR_INTERNAL) { + MO_DBG_ERR("triggerMessage() failed"); + errorCode = "InternalError"; + return; } } std::unique_ptr TriggerMessage::createConf(){ auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); - payload["status"] = statusMessage; + const char *statusStr = "Rejected"; + switch (status) { + case TriggerMessageStatus::Accepted: + statusStr = "Accepted"; + break; + case TriggerMessageStatus::Rejected: + statusStr = "Rejected"; + break; + case TriggerMessageStatus::NotImplemented: + statusStr = "NotImplemented"; + break; + case TriggerMessageStatus::ERR_INTERNAL: + //dead code + statusStr = "Rejected"; + break; + } + payload["status"] = statusStr; return doc; } + +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/TriggerMessage.h b/src/MicroOcpp/Operations/TriggerMessage.h index 7869e5a5..396c565d 100644 --- a/src/MicroOcpp/Operations/TriggerMessage.h +++ b/src/MicroOcpp/Operations/TriggerMessage.h @@ -6,22 +6,26 @@ #define MO_TRIGGERMESSAGE_H #include +#include #include +#include + +#if MO_ENABLE_V16 || MO_ENABLE_V201 namespace MicroOcpp { class Context; - -namespace Ocpp16 { +class RemoteControlService; class TriggerMessage : public Operation, public MemoryManaged { private: Context& context; - const char *statusMessage = nullptr; + RemoteControlService& rcService; + TriggerMessageStatus status = TriggerMessageStatus::Rejected; const char *errorCode = nullptr; public: - TriggerMessage(Context& context); + TriggerMessage(Context& context, RemoteControlService& rcService); const char* getOperationType() override; @@ -32,6 +36,6 @@ class TriggerMessage : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 || MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Operations/UnlockConnector.cpp b/src/MicroOcpp/Operations/UnlockConnector.cpp index a64b7bd4..e728860b 100644 --- a/src/MicroOcpp/Operations/UnlockConnector.cpp +++ b/src/MicroOcpp/Operations/UnlockConnector.cpp @@ -3,100 +3,99 @@ // MIT License #include +#include #include +#include #include -using MicroOcpp::Ocpp16::UnlockConnector; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 + +using namespace MicroOcpp; + +v16::UnlockConnector::UnlockConnector(Context& context, RemoteControlService& rcService) : MemoryManaged("v16.Operation.", "UnlockConnector"), context(context), rcService(rcService) { -UnlockConnector::UnlockConnector(Model& model) : MemoryManaged("v16.Operation.", "UnlockConnector"), model(model) { - } -const char* UnlockConnector::getOperationType(){ +const char* v16::UnlockConnector::getOperationType(){ return "UnlockConnector"; } -void UnlockConnector::processReq(JsonObject payload) { +void v16::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) { + rcEvse = rcService.getEvse(connectorId); + if (!rcEvse) { // NotSupported + status = UnlockStatus::NotSupported; return; } - connector->endTransaction(nullptr, "UnlockCommand"); - connector->updateTxNotification(TxNotification_RemoteStop); - - cbUnlockResult = unlockConnector(); - - timerStart = mocpp_tick_ms(); + status = rcEvse->unlockConnector16(); + timerStart = context.getClock().getUptime(); } #endif //MO_ENABLE_CONNECTOR_LOCK } -std::unique_ptr UnlockConnector::createConf() { - - const char *status = "NotSupported"; +std::unique_ptr v16::UnlockConnector::createConf() { #if MO_ENABLE_CONNECTOR_LOCK - if (unlockConnector) { + { + int32_t dtTimerStart; + context.getClock().delta(context.getClock().getUptime(), timerStart, dtTimerStart); - if (mocpp_tick_ms() - timerStart < MO_UNLOCK_TIMEOUT) { - //do poll and if more time is needed, delay creation of conf msg + if (rcEvse && status == UnlockStatus::PENDING && dtTimerStart < MO_UNLOCK_TIMEOUT) { + status = rcEvse->unlockConnector16(); - if (cbUnlockResult == UnlockConnectorResult_Pending) { - cbUnlockResult = unlockConnector(); - if (cbUnlockResult == UnlockConnectorResult_Pending) { - return nullptr; //no result yet - delay confirmation response - } + if (status == UnlockStatus::PENDING) { + return nullptr; //no result yet - delay confirmation response } } - - if (cbUnlockResult == UnlockConnectorResult_Unlocked) { - status = "Unlocked"; - } else { - status = "UnlockFailed"; - } } +#else + status = UnlockStatus::NotSupported; #endif //MO_ENABLE_CONNECTOR_LOCK + const char *statusStr = ""; + switch (status) { + case UnlockStatus::Unlocked: + statusStr = "Unlocked"; + break; + case UnlockStatus::UnlockFailed: + statusStr = "UnlockFailed"; + break; + case UnlockStatus::NotSupported: + statusStr = "NotSupported"; + 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"] = status; + payload["status"] = statusStr; return doc; } +#endif //MO_ENABLE_V16 -#if MO_ENABLE_V201 -#if MO_ENABLE_CONNECTOR_LOCK - -#include +#if MO_ENABLE_V201 && MO_ENABLE_CONNECTOR_LOCK -namespace MicroOcpp { -namespace Ocpp201 { +using namespace MicroOcpp; -UnlockConnector::UnlockConnector(RemoteControlService& rcService) : MemoryManaged("v201.Operation.UnlockConnector"), rcService(rcService) { +v201::UnlockConnector::UnlockConnector(Context& context, RemoteControlService& rcService) : MemoryManaged("v201.Operation.UnlockConnector"), context(context), rcService(rcService) { } -const char* UnlockConnector::getOperationType(){ +const char* v201::UnlockConnector::getOperationType(){ return "UnlockConnector"; } -void UnlockConnector::processReq(JsonObject payload) { +void v201::UnlockConnector::processReq(JsonObject payload) { int evseId = payload["evseId"] | -1; int connectorId = payload["connectorId"] | -1; @@ -107,45 +106,48 @@ void UnlockConnector::processReq(JsonObject payload) { } if (connectorId != 1) { - status = UnlockStatus_UnknownConnector; + status = UnlockStatus::UnknownConnector; return; } rcEvse = rcService.getEvse(evseId); if (!rcEvse) { - status = UnlockStatus_UnlockFailed; + status = UnlockStatus::UnlockFailed; return; } - status = rcEvse->unlockConnector(); - timerStart = mocpp_tick_ms(); + status = rcEvse->unlockConnector201(); + timerStart = context.getClock().getUptime(); } -std::unique_ptr UnlockConnector::createConf() { +std::unique_ptr v201::UnlockConnector::createConf() { + + int32_t dtTimerStart; + context.getClock().delta(context.getClock().getUptime(), timerStart, dtTimerStart); - if (rcEvse && status == UnlockStatus_PENDING && mocpp_tick_ms() - timerStart < MO_UNLOCK_TIMEOUT) { - status = rcEvse->unlockConnector(); + if (rcEvse && status == UnlockStatus::PENDING && dtTimerStart < MO_UNLOCK_TIMEOUT) { + status = rcEvse->unlockConnector201(); - if (status == UnlockStatus_PENDING) { + if (status == UnlockStatus::PENDING) { return nullptr; //no result yet - delay confirmation response } } const char *statusStr = ""; switch (status) { - case UnlockStatus_Unlocked: + case UnlockStatus::Unlocked: statusStr = "Unlocked"; break; - case UnlockStatus_UnlockFailed: + case UnlockStatus::UnlockFailed: statusStr = "UnlockFailed"; break; - case UnlockStatus_OngoingAuthorizedTransaction: + case UnlockStatus::OngoingAuthorizedTransaction: statusStr = "OngoingAuthorizedTransaction"; break; - case UnlockStatus_UnknownConnector: + case UnlockStatus::UnknownConnector: statusStr = "UnknownConnector"; break; - case UnlockStatus_PENDING: + case UnlockStatus::PENDING: MO_DBG_ERR("UnlockConnector timeout"); statusStr = "UnlockFailed"; break; @@ -157,8 +159,4 @@ std::unique_ptr UnlockConnector::createConf() { return doc; } -} // namespace Ocpp201 -} // namespace MicroOcpp - -#endif //MO_ENABLE_CONNECTOR_LOCK -#endif //MO_ENABLE_V201 +#endif //MO_ENABLE_V201 && MO_ENABLE_CONNECTOR_LOCK diff --git a/src/MicroOcpp/Operations/UnlockConnector.h b/src/MicroOcpp/Operations/UnlockConnector.h index 518653cd..e76d4114 100644 --- a/src/MicroOcpp/Operations/UnlockConnector.h +++ b/src/MicroOcpp/Operations/UnlockConnector.h @@ -5,31 +5,35 @@ #ifndef UNLOCKCONNECTOR_H #define UNLOCKCONNECTOR_H -#include +#include #include -#include -#include +#include +#include +#include + +#if MO_ENABLE_V16 namespace MicroOcpp { -class Model; +class Context; +class RemoteControlService; +class RemoteControlServiceEvse; -namespace Ocpp16 { +namespace v16 { class UnlockConnector : public Operation, public MemoryManaged { private: - Model& model; + Context& context; + RemoteControlService& rcService; + RemoteControlServiceEvse *rcEvse = nullptr; -#if MO_ENABLE_CONNECTOR_LOCK - std::function unlockConnector; - UnlockConnectorResult cbUnlockResult; - unsigned long timerStart = 0; //for timeout -#endif //MO_ENABLE_CONNECTOR_LOCK + UnlockStatus status; + Timestamp timerStart; //for timeout const char *errorCode = nullptr; public: - UnlockConnector(Model& model); + UnlockConnector(Context& context, RemoteControlService& rcService); const char* getOperationType() override; @@ -40,32 +44,34 @@ class UnlockConnector : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 -#if MO_ENABLE_V201 -#if MO_ENABLE_CONNECTOR_LOCK +#if MO_ENABLE_V201 && MO_ENABLE_CONNECTOR_LOCK #include namespace MicroOcpp { +class Context; class RemoteControlService; class RemoteControlServiceEvse; -namespace Ocpp201 { +namespace v201 { class UnlockConnector : public Operation, public MemoryManaged { private: + Context& context; RemoteControlService& rcService; RemoteControlServiceEvse *rcEvse = nullptr; UnlockStatus status; - unsigned long timerStart = 0; //for timeout + Timestamp timerStart; //for timeout const char *errorCode = nullptr; public: - UnlockConnector(RemoteControlService& rcService); + UnlockConnector(Context& context, RemoteControlService& rcService); const char* getOperationType() override; @@ -76,10 +82,7 @@ class UnlockConnector : public Operation, public MemoryManaged { const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp201 -} //end namespace MicroOcpp - -#endif //MO_ENABLE_CONNECTOR_LOCK -#endif //MO_ENABLE_V201 - +} //namespace v201 +} //namespace MicroOcpp +#endif //MO_ENABLE_V201 && MO_ENABLE_CONNECTOR_LOCK #endif diff --git a/src/MicroOcpp/Operations/UpdateFirmware.cpp b/src/MicroOcpp/Operations/UpdateFirmware.cpp index d9be551f..7832ac23 100644 --- a/src/MicroOcpp/Operations/UpdateFirmware.cpp +++ b/src/MicroOcpp/Operations/UpdateFirmware.cpp @@ -5,12 +5,16 @@ #include #include #include +#include +#include #include -using MicroOcpp::Ocpp16::UpdateFirmware; -using MicroOcpp::JsonDoc; +#if MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT -UpdateFirmware::UpdateFirmware(FirmwareService& fwService) : MemoryManaged("v16.Operation.", "UpdateFirmware"), fwService(fwService) { +using namespace MicroOcpp; +using namespace MicroOcpp::v16; + +UpdateFirmware::UpdateFirmware(Context& context, FirmwareService& fwService) : MemoryManaged("v16.Operation.", "UpdateFirmware"), context(context), fwService(fwService) { } @@ -22,7 +26,7 @@ void UpdateFirmware::processReq(JsonObject payload) { errorCode = "FormationViolation"; return; } - + int retries = payload["retries"] | 1; int retryInterval = payload["retryInterval"] | 180; if (retries < 0 || retryInterval < 0) { @@ -37,15 +41,17 @@ void UpdateFirmware::processReq(JsonObject payload) { } Timestamp retrieveDate; - if (!retrieveDate.setTime(payload["retrieveDate"] | "Invalid")) { - errorCode = "PropertyConstraintViolation"; + if (!context.getClock().parseString(payload["retrieveDate"] | "_Unvalid", retrieveDate)) { + errorCode = "FormationViolation"; MO_DBG_WARN("bad time format"); return; } - + fwService.scheduleFirmwareUpdate(location, retrieveDate, (unsigned int) retries, (unsigned int) retryInterval); } std::unique_ptr UpdateFirmware::createConf(){ return createEmptyDocument(); } + +#endif //MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT diff --git a/src/MicroOcpp/Operations/UpdateFirmware.h b/src/MicroOcpp/Operations/UpdateFirmware.h index da962141..fffde4ff 100644 --- a/src/MicroOcpp/Operations/UpdateFirmware.h +++ b/src/MicroOcpp/Operations/UpdateFirmware.h @@ -7,31 +7,37 @@ #include #include +#include + +#if MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT namespace MicroOcpp { -class FirmwareService; +class Context; -namespace Ocpp16 { +namespace v16 { + +class FirmwareService; class UpdateFirmware : public Operation, public MemoryManaged { private: - FirmwareService& fwService; + Context& context; + FirmwareService& fwService; - const char *errorCode = nullptr; + const char *errorCode = nullptr; public: - UpdateFirmware(FirmwareService& fwService); + UpdateFirmware(Context& context, FirmwareService& fwService); - const char* getOperationType() override {return "UpdateFirmware";} + const char* getOperationType() override {return "UpdateFirmware";} - void processReq(JsonObject payload) override; + void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; - const char *getErrorCode() override {return errorCode;} + const char *getErrorCode() override {return errorCode;} }; -} //end namespace Ocpp16 -} //end namespace MicroOcpp - +} //namespace v16 +} //namespace MicroOcpp +#endif //MO_ENABLE_V16 && MO_ENABLE_FIRMWAREMANAGEMENT #endif diff --git a/src/MicroOcpp/Platform.cpp b/src/MicroOcpp/Platform.cpp index 75980725..e087367c 100644 --- a/src/MicroOcpp/Platform.cpp +++ b/src/MicroOcpp/Platform.cpp @@ -1,112 +1,107 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #include -#ifdef MO_CUSTOM_CONSOLE - -char _mo_console_msg_buf [MO_CUSTOM_CONSOLE_MAXMSGSIZE]; +#if MO_PLATFORM == MO_PLATFORM_ARDUINO +#include +#ifndef MO_USE_SERIAL +#define MO_USE_SERIAL Serial +#endif 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 defaultDebugCbImpl(const char *msg) { + MO_USE_SERIAL.printf("%s", 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"); - } +uint32_t defaultTickCbImpl() { + return millis(); } -#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; -} +} //namespace MicroOcpp -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 +#elif MO_PLATFORM == MO_PLATFORM_ESPIDF +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" namespace MicroOcpp { -decltype(xTaskGetTickCount()) mocpp_ticks_count = 0; -unsigned long mocpp_millis_count = 0; - +void defaultDebugCbImpl(const char *msg) { + printf("%s", msg); } -unsigned long mocpp_tick_ms_espidf() { +decltype(xTaskGetTickCount()) mocpp_ticks_count = 0; +uint32_t mocpp_millis_count = 0; + +uint32_t defaultTickCbImpl() { 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; + mocpp_millis_count += ((ticks_now - mocpp_ticks_count) * 1000UL) / configTICK_RATE_HZ; + mocpp_ticks_count = ticks_now; + return mocpp_millis_count; } +} //namespace MicroOcpp #elif MO_PLATFORM == MO_PLATFORM_UNIX +#include #include namespace MicroOcpp { +void defaultDebugCbImpl(const char *msg) { + printf("%s", msg); +} + 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; +uint32_t defaultTickCbImpl() { + if (!clock_initialized) { + clock_reference = std::chrono::steady_clock::now(); + clock_initialized = true; } std::chrono::milliseconds ms = std::chrono::duration_cast( - std::chrono::steady_clock::now() - MicroOcpp::clock_reference); - return (unsigned long) ms.count(); + std::chrono::steady_clock::now() - clock_reference); + return (uint32_t) ms.count(); } -#endif + +} //namespace MicroOcpp +#else +namespace MicroOcpp { +void (*defaultDebugCbImpl)(const char*) = nullptr; +uint32_t (*defaultTickCbImpl)() = nullptr; +} //namespace MicroOcpp #endif -#ifdef MO_CUSTOM_RNG -uint32_t (*mocpp_rng_impl)() = nullptr; +namespace MicroOcpp { -void mocpp_set_rng(uint32_t (*rng)()) { - mocpp_rng_impl = rng; +void (*getDefaultDebugCb())(const char*) { + return defaultDebugCbImpl; } -uint32_t mocpp_rng_custom(void) { - if (mocpp_rng_impl) { - return mocpp_rng_impl(); - } else { - return 0; - } +uint32_t (*getDefaultTickCb())() { + return defaultTickCbImpl; } -#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) { +uint32_t defaultRngCbImpl(void) { static uint32_t prng_state = 1; - uint32_t entropy = mocpp_tick_ms(); + uint32_t entropy = defaultTickCbImpl(); //ensure that RNG is only executed when defaultTickCbImpl is defined prng_state = (prng_state ^ entropy)*1664525U + 1013904223U; // assuming complement-2 integers and non-signaling overflow return prng_state; } -#endif + +uint32_t (*getDefaultRngCb())() { + if (!defaultTickCbImpl) { + //default RNG depends on default TickCb + return nullptr; + } + return defaultRngCbImpl; +} + +} //namespace MicroOcpp diff --git a/src/MicroOcpp/Platform.h b/src/MicroOcpp/Platform.h index a3da99ea..b594cf79 100644 --- a/src/MicroOcpp/Platform.h +++ b/src/MicroOcpp/Platform.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_PLATFORM_H @@ -17,96 +17,18 @@ #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) +namespace MicroOcpp { -#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 || MO_PLATFORM == MO_PLATFORM_UNIX -#include +void (*getDefaultDebugCb())(const char*); +uint32_t (*getDefaultTickCb())(); +uint32_t (*getDefaultRngCb())(); -#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 +} //namespace MicroOcpp +#endif //__cplusplus #ifndef MO_MAX_JSON_CAPACITY #if MO_PLATFORM == MO_PLATFORM_UNIX -#define MO_MAX_JSON_CAPACITY 16384 +#define MO_MAX_JSON_CAPACITY 8192 #else #define MO_MAX_JSON_CAPACITY 4096 #endif diff --git a/src/MicroOcpp/Version.h b/src/MicroOcpp/Version.h index 92a2ea46..81acca34 100644 --- a/src/MicroOcpp/Version.h +++ b/src/MicroOcpp/Version.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #ifndef MO_VERSION_H @@ -8,37 +8,49 @@ /* * Version specification of MicroOcpp library (not related with the OCPP version) */ -#define MO_VERSION "1.2.0" +#define MO_VERSION "2.0.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 + * OCPP version identifiers */ -#ifndef MO_ENABLE_V201 -#define MO_ENABLE_V201 0 -#endif - -#ifdef __cplusplus - -namespace MicroOcpp { +#define MO_OCPP_V16 160 // OCPP 1.6 +#define MO_OCPP_V201 201 // OCPP 2.0.1 /* - * OCPP version type, defined in Model + * Enable OCPP 1.6 support. If multiple OCPP versions are enabled, the final choice + * of the protocol can be done dynamically during setup */ -struct ProtocolVersion { - const int major, minor, patch; - ProtocolVersion(int major = 1, int minor = 6, int patch = 0) : major(major), minor(minor), patch(patch) { } -}; +#ifndef MO_ENABLE_V16 +#define MO_ENABLE_V16 1 +#endif -} +/* + * Enable OCPP 2.0.1 support. If multiple OCPP versions are enabled, the final choice + * of the protocol can be done dynamically during setup + */ +#ifndef MO_ENABLE_V201 +#define MO_ENABLE_V201 1 +#endif -#endif //__cplusplus +/* + * Enable internal mock server. When MO uses the Loopback connection, or is connected + * to an echo WS server, then MO can respond to its own requests and mock real OCPP + * communication. This is useful for development and testing. + */ +#ifndef MO_ENABLE_MOCK_SERVER +#define MO_ENABLE_MOCK_SERVER 1 +#endif // 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 +// Security Event log (UC A04). Works with OCPP 1.6 and 2.0.1 +#ifndef MO_ENABLE_SECURITY_EVENT +#define MO_ENABLE_SECURITY_EVENT MO_ENABLE_V201 +#endif + // Reservations #ifndef MO_ENABLE_RESERVATION #define MO_ENABLE_RESERVATION 1 @@ -49,4 +61,16 @@ struct ProtocolVersion { #define MO_ENABLE_LOCAL_AUTH 1 #endif +#ifndef MO_ENABLE_SMARTCHARGING +#define MO_ENABLE_SMARTCHARGING 1 +#endif + +#ifndef MO_ENABLE_FIRMWAREMANAGEMENT +#define MO_ENABLE_FIRMWAREMANAGEMENT 1 +#endif + +#ifndef MO_ENABLE_DIAGNOSTICS +#define MO_ENABLE_DIAGNOSTICS 1 +#endif + #endif diff --git a/src/MicroOcpp_c.cpp b/src/MicroOcpp_c.cpp deleted file mode 100644 index 381084b0..00000000 --- a/src/MicroOcpp_c.cpp +++ /dev/null @@ -1,462 +0,0 @@ -// 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)); -} diff --git a/src/MicroOcpp_c.h b/src/MicroOcpp_c.h deleted file mode 100644 index a517ff1d..00000000 --- a/src/MicroOcpp_c.h +++ /dev/null @@ -1,217 +0,0 @@ -// 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 diff --git a/tests/Boot.cpp b/tests/Boot.cpp index a4ea610f..e5f2c2dc 100644 --- a/tests/Boot.cpp +++ b/tests/Boot.cpp @@ -64,9 +64,9 @@ TEST_CASE( "Boot Behavior" ) { bool checkProcessed = false; - getOcppContext()->getOperationRegistry().registerOperation("BootNotification", + getOcppContext()->getMessageService().registerOperation("BootNotification", [&checkProcessed] () { - return new Ocpp16::CustomOperation("BootNotification", + return new v16::CustomOperation("BootNotification", [ &checkProcessed] (JsonObject payload) { //process req checkProcessed = true; @@ -113,9 +113,9 @@ TEST_CASE( "Boot Behavior" ) { mocpp_initialize(loopback, ChargerCredentials()); - getOcppContext()->getOperationRegistry().registerOperation("BootNotification", + getOcppContext()->getMessageService().registerOperation("BootNotification", [] () { - return new Ocpp16::CustomOperation("BootNotification", + return new v16::CustomOperation("BootNotification", [] (JsonObject payload) { //ignore req }, @@ -143,7 +143,7 @@ TEST_CASE( "Boot Behavior" ) { bool checkProcessedHeartbeat = false; - auto heartbeat = makeRequest(new Ocpp16::CustomOperation( + auto heartbeat = makeRequest(new v16::CustomOperation( "Heartbeat", [] () { //create req @@ -186,7 +186,7 @@ TEST_CASE( "Boot Behavior" ) { bool executedTriggerMessage = false; - getOcppContext()->getOperationRegistry().registerOperation("TriggeredOperation", + getOcppContext()->getMessageService().registerOperation("TriggeredOperation", [&executedTriggerMessage] () {return new TriggeredOperation(executedTriggerMessage);}); loopback.sendTXT(TRIGGER_MESSAGE, sizeof(TRIGGER_MESSAGE) - 1); @@ -202,9 +202,9 @@ TEST_CASE( "Boot Behavior" ) { MO_DBG_INFO("Now, accept BN and check if all queued messages finally arrive"); - getOcppContext()->getOperationRegistry().registerOperation("BootNotification", + getOcppContext()->getMessageService().registerOperation("BootNotification", [] () { - return new Ocpp16::CustomOperation("BootNotification", + return new v16::CustomOperation("BootNotification", [] (JsonObject payload) { //ignore req }, @@ -254,9 +254,9 @@ TEST_CASE( "Boot Behavior" ) { //start another transaction while BN is pending - getOcppContext()->getOperationRegistry().registerOperation("BootNotification", + getOcppContext()->getMessageService().registerOperation("BootNotification", [] () { - return new Ocpp16::CustomOperation("BootNotification", + return new v16::CustomOperation("BootNotification", [] (JsonObject payload) { //ignore req }, @@ -287,9 +287,9 @@ TEST_CASE( "Boot Behavior" ) { //Now, accept BN and check again - getOcppContext()->getOperationRegistry().registerOperation("BootNotification", + getOcppContext()->getMessageService().registerOperation("BootNotification", [] () { - return new Ocpp16::CustomOperation("BootNotification", + return new v16::CustomOperation("BootNotification", [] (JsonObject payload) { //ignore req }, @@ -435,9 +435,9 @@ TEST_CASE( "Boot Behavior" ) { bool checkProcessed = false; - getOcppContext()->getOperationRegistry().registerOperation("BootNotification", + getOcppContext()->getMessageService().registerOperation("BootNotification", [&checkProcessed] () { - return new Ocpp16::CustomOperation("BootNotification", + return new v16::CustomOperation("BootNotification", [ &checkProcessed] (JsonObject payload) { //process req checkProcessed = true; diff --git a/tests/Certificates.cpp b/tests/Certificates.cpp index 79d0b967..b87262ec 100644 --- a/tests/Certificates.cpp +++ b/tests/Certificates.cpp @@ -163,7 +163,7 @@ TEST_CASE( "M - Certificates" ) { SECTION("M05 InstallCertificate operation") { bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "InstallCertificate", [] () { //create req @@ -195,7 +195,7 @@ TEST_CASE( "M - Certificates" ) { REQUIRE(ret == InstallCertificateStatus_Accepted); bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "DeleteCertificate", [] () { //create req @@ -223,7 +223,7 @@ TEST_CASE( "M - Certificates" ) { REQUIRE(ret == InstallCertificateStatus_Accepted); bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "GetInstalledCertificateIds", [] () { //create req diff --git a/tests/ChargePointError.cpp b/tests/ChargePointError.cpp index 5f498de6..b6a80dbd 100644 --- a/tests/ChargePointError.cpp +++ b/tests/ChargePointError.cpp @@ -82,9 +82,9 @@ TEST_CASE( "ChargePointError" ) { bool checkProcessed = false; - getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", + getOcppContext()->getMessageService().registerOperation("StatusNotification", [&checkProcessed] () { - return new Ocpp16::CustomOperation("StatusNotification", + return new v16::CustomOperation("StatusNotification", [ &checkProcessed] (JsonObject payload) { //process req checkProcessed = true; @@ -110,9 +110,9 @@ TEST_CASE( "ChargePointError" ) { #if MO_REPORT_NOERROR checkProcessed = false; - getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", + getOcppContext()->getMessageService().registerOperation("StatusNotification", [&checkProcessed] () { - return new Ocpp16::CustomOperation("StatusNotification", + return new v16::CustomOperation("StatusNotification", [ &checkProcessed] (JsonObject payload) { //process req checkProcessed = true; @@ -208,9 +208,9 @@ TEST_CASE( "ChargePointError" ) { const char *errorInfo = "*"; bool checkErrorInfo = false; - getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", + getOcppContext()->getMessageService().registerOperation("StatusNotification", [&checkErrorCode, &checkErrorInfo, &errorInfo, &errorCode] () { - return new Ocpp16::CustomOperation("StatusNotification", + return new v16::CustomOperation("StatusNotification", [&checkErrorCode, &checkErrorInfo, &errorInfo, &errorCode] (JsonObject payload) { //process req if (strcmp(errorInfo, "*")) { diff --git a/tests/ChargingSessions.cpp b/tests/ChargingSessions.cpp index 82090f16..ccf237bd 100644 --- a/tests/ChargingSessions.cpp +++ b/tests/ChargingSessions.cpp @@ -3,250 +3,374 @@ // MIT License #include -#include -#include +#include +#include #include -#include -#include -#include -#include -#include -#include +#include #include #include #include "./helpers/testHelper.h" -#include +#include -#define BASE_TIME "2023-01-01T00:00:00.000Z" +#define BASE_TIME_UNIX 1750000000 +#define BASE_TIME_STRING "2025-06-15T15:06:40Z" using namespace MicroOcpp; +std::vector> sentOperations; + +void addSentOperation(const char *operationType, const char *payloadJson, void*) { + DynamicJsonDocument doc (1024); + auto err = deserializeJson(doc, payloadJson); + if (err) { + char buf [100]; + snprintf(buf, sizeof(buf), "JSON deserialization error: %s", err.c_str()); + FAIL(buf); + } + sentOperations.emplace_back(operationType, std::move(doc)); +} + +std::string getLastSentStatus(int ocppVersion, int evseId = 1) { + for (auto operation = sentOperations.rbegin(); operation != sentOperations.rend(); operation++) { + if (operation->first == "StatusNotification") { + if (ocppVersion == MO_OCPP_V16) { + if ((operation->second["connectorId"] | -1) != evseId) { + continue; + } + //found + return operation->second["status"] | "_Invalid"; + } else if (ocppVersion == MO_OCPP_V201) { + if ((operation->second["evseId"] | -1) != evseId) { + continue; + } + //found + return operation->second["connectorStatus"] | "_Invalid"; + } + } + } + return ""; +} TEST_CASE( "Charging sessions" ) { printf("\nRun %s\n", "Charging sessions"); - //initialize Context with dummy socket + sentOperations.clear(); + + //initialize Context without any configs + mo_initialize(); + + mo_getContext()->setTicksCb(custom_timer_cb); + 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]); - } - }); + mo_getContext()->setConnection(&loopback); - SECTION("Check idle state"){ + auto ocppVersion = GENERATE(MO_OCPP_V16, MO_OCPP_V201); + mo_setOcppVersion(ocppVersion); - 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"); - }); + mo_setBootNotificationData("TestModel", "TestVendor"); + + mo_setup(); + + mo_setOnReceiveRequest(mo_getApiContext(), "BootNotification", addSentOperation, nullptr); + mo_setOnReceiveRequest(mo_getApiContext(), "StatusNotification", addSentOperation, nullptr); + mo_setOnReceiveRequest(mo_getApiContext(), "StartTransaction", addSentOperation, nullptr); + mo_setOnReceiveRequest(mo_getApiContext(), "StopTransaction", addSentOperation, nullptr); + mo_setOnReceiveRequest(mo_getApiContext(), "TransactionEvent", addSentOperation, nullptr); + + mo_setVarConfigInt(mo_getApiContext(), "TxCtrlr", "EVConnectionTimeOut", "ConnectionTimeOut", 30); + + if (ocppVersion == MO_OCPP_V16) { + mo_setConfigurationInt("MinimumStatusDuration", 0); + } else if (ocppVersion == MO_OCPP_V201) { + mo_setVarConfigString(mo_getApiContext(), "TxCtrlr", "TxStartPoint", NULL, "PowerPathClosed"); + mo_setVarConfigString(mo_getApiContext(), "TxCtrlr", "TxStopPoint", NULL, "PowerPathClosed"); + } + + SECTION("Clean up files") { + auto filesystem = mo_getFilesystem(); + FilesystemUtils::removeByPrefix(filesystem, ""); + } + + SECTION("Check idle state"){ - REQUIRE( !isOperative() ); //not operative before reaching loop stage + REQUIRE( !mo_isOperative() ); //not operative before reaching loop stage loop(); loop(); - REQUIRE( checkedBN ); - REQUIRE( checkedSN[0] ); - REQUIRE( checkedSN[1] ); - REQUIRE( isOperative() ); - REQUIRE( !getTransaction() ); - REQUIRE( !ocppPermitsCharge() ); - } - loop(); + size_t bootNotificationCount = 0; - SECTION("StartTx") { - SECTION("StartTx directly"){ - startTransaction("mIdTag"); - loop(); - REQUIRE(ocppPermitsCharge()); + for (size_t i = 0; i < sentOperations.size(); i++) { + if (sentOperations[i].first == "BootNotification") { + if (ocppVersion == MO_OCPP_V16) { + REQUIRE(sentOperations[i].second["chargePointModel"].as() == "TestModel"); + REQUIRE(sentOperations[i].second["chargePointVendor"].as() == "TestVendor"); + } else if (ocppVersion == MO_OCPP_V201) { + REQUIRE(sentOperations[i].second["chargingStation"]["model"].as() == "TestModel"); + REQUIRE(sentOperations[i].second["chargingStation"]["vendorName"].as() == "TestVendor"); + } + bootNotificationCount++; + } } + REQUIRE( bootNotificationCount == 1 ); + REQUIRE( mo_isOperative() ); + REQUIRE( !mo_isTransactionActive() ); + REQUIRE( !mo_isTransactionRunning() ); + REQUIRE( !mo_ocppPermitsCharge() ); + } + + SECTION("StartTx") { + + loop(); SECTION("StartTx via session management - plug in first") { - expectedSN[1] = "Preparing"; - setConnectorPluggedInput([] () {return true;}); + + sentOperations.clear(); + mo_setConnectorPluggedInput([] () {return true;}); loop(); - REQUIRE(checkedSN[1]); + if (ocppVersion == MO_OCPP_V16) { + REQUIRE(getLastSentStatus(ocppVersion) == "Preparing"); + } else if (ocppVersion == MO_OCPP_V201) { + REQUIRE(getLastSentStatus(ocppVersion) == "Occupied"); + } - checkedSN[1] = false; - expectedSN[1] = "Charging"; - beginTransaction("mIdTag"); + sentOperations.clear(); + mo_beginTransaction("mIdTag"); loop(); - REQUIRE(checkedSN[1]); - REQUIRE(ocppPermitsCharge()); + if (ocppVersion == MO_OCPP_V16) { + REQUIRE(getLastSentStatus(ocppVersion) == "Charging"); + } else if (ocppVersion == MO_OCPP_V201) { + REQUIRE(getLastSentStatus(ocppVersion) == ""); + } } SECTION("StartTx via session management - authorization first") { - expectedSN[1] = "Preparing"; - setConnectorPluggedInput([] () {return false;}); - beginTransaction("mIdTag"); + sentOperations.clear(); + mo_setConnectorPluggedInput([] () {return false;}); + mo_beginTransaction("mIdTag"); loop(); - REQUIRE(checkedSN[1]); + if (ocppVersion == MO_OCPP_V16) { + REQUIRE(getLastSentStatus(ocppVersion) == "Preparing"); + } else if (ocppVersion == MO_OCPP_V201) { + REQUIRE(getLastSentStatus(ocppVersion) == ""); + } - checkedSN[1] = false; - expectedSN[1] = "Charging"; - setConnectorPluggedInput([] () {return true;}); + REQUIRE(mo_getTransactionIdTag() != nullptr); + REQUIRE(std::string(mo_getTransactionIdTag()) == "mIdTag"); + + sentOperations.clear(); + mo_setConnectorPluggedInput([] () {return true;}); loop(); - REQUIRE(checkedSN[1]); - REQUIRE(ocppPermitsCharge()); + if (ocppVersion == MO_OCPP_V16) { + REQUIRE(getLastSentStatus(ocppVersion) == "Charging"); + } else if (ocppVersion == MO_OCPP_V201) { + REQUIRE(getLastSentStatus(ocppVersion) == "Occupied"); + } } SECTION("StartTx via session management - no plug") { - expectedSN[1] = "Charging"; - beginTransaction("mIdTag"); + sentOperations.clear(); + mo_beginTransaction("mIdTag"); loop(); - REQUIRE(checkedSN[1]); + if (ocppVersion == MO_OCPP_V16) { + REQUIRE(getLastSentStatus(ocppVersion) == "Charging"); + } else if (ocppVersion == MO_OCPP_V201) { + REQUIRE(getLastSentStatus(ocppVersion) == ""); + } } - 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]); + REQUIRE(mo_ocppPermitsCharge()); + REQUIRE(mo_getTransactionId() != nullptr); + REQUIRE(!std::string(mo_getTransactionId()).empty()); + REQUIRE(mo_isTransactionActive()); + REQUIRE(mo_isTransactionRunning()); + + bool startTxSent = false; + for (auto& operation : sentOperations) { + if (operation.first == "StartTransaction") { + startTxSent = true; + break; + } else if (operation.first == "TransactionEvent") { + if (operation.second["eventType"].as() == "Started") { + startTxSent = true; + break; + } + } } + REQUIRE(startTxSent); + + mo_endTransaction(nullptr, nullptr); + loop(); + } + + SECTION("StartTx - ConnectionTimeOut") { + + loop(); + sentOperations.clear(); + mo_setConnectorPluggedInput([] () {return false;}); + mo_beginTransaction("mIdTag"); loop(); - if (ocppPermitsCharge()) { - stopTransaction(); + if (ocppVersion == MO_OCPP_V16) { + REQUIRE(getLastSentStatus(ocppVersion) == "Preparing"); + } else if (ocppVersion == MO_OCPP_V201) { + REQUIRE(getLastSentStatus(ocppVersion) == ""); } + + REQUIRE(mo_getTransactionIdTag() != nullptr); + REQUIRE(std::string(mo_getTransactionIdTag()) == "mIdTag"); + if (ocppVersion == MO_OCPP_V16) { + REQUIRE(mo_getTransactionId() == nullptr); + } else if (ocppVersion == MO_OCPP_V201) { + REQUIRE(mo_getTransactionId() != nullptr); + REQUIRE(!std::string(mo_getTransactionId()).empty()); + } + + sentOperations.clear(); + int connectionTimeOut = 0; + mo_getVarConfigInt(mo_getApiContext(), "TxCtrlr", "EVConnectionTimeOut", "ConnectionTimeOut", &connectionTimeOut); + mtime += connectionTimeOut * 1000; loop(); + if (ocppVersion == MO_OCPP_V16) { + REQUIRE(getLastSentStatus(ocppVersion) == "Available"); + } else if (ocppVersion == MO_OCPP_V201) { + REQUIRE(getLastSentStatus(ocppVersion) == ""); + } + REQUIRE(mo_getTransactionIdTag() == nullptr); + REQUIRE(!mo_isTransactionActive()); } SECTION("StopTx") { - startTransaction("mIdTag"); + loop(); - expectedSN[1] = "Available"; - SECTION("directly") { - stopTransaction(); - loop(); - REQUIRE(checkedSN[1]); - REQUIRE(!ocppPermitsCharge()); - } + mo_beginTransaction("mIdTag"); + loop(); + + REQUIRE(mo_ocppPermitsCharge()); + REQUIRE(mo_getTransactionId() != nullptr); + REQUIRE(!std::string(mo_getTransactionId()).empty()); + + sentOperations.clear(); SECTION("via session management - deauthorize") { - endTransaction(); + mo_endTransaction("mIdTag", nullptr); loop(); - REQUIRE(checkedSN[1]); - REQUIRE(!ocppPermitsCharge()); + if (ocppVersion == MO_OCPP_V16) { + REQUIRE(getLastSentStatus(ocppVersion) == "Available"); + } else if (ocppVersion == MO_OCPP_V201) { + REQUIRE(getLastSentStatus(ocppVersion) == ""); + } } SECTION("via session management - deauthorize first") { - expectedSN[1] = "Finishing"; - setConnectorPluggedInput([] () {return true;}); - endTransaction(); + mo_setConnectorPluggedInput([] () {return true;}); loop(); - REQUIRE(checkedSN[1]); - REQUIRE(!ocppPermitsCharge()); + + sentOperations.clear(); + mo_endTransaction("mIdTag", nullptr); + loop(); + if (ocppVersion == MO_OCPP_V16) { + REQUIRE(getLastSentStatus(ocppVersion) == "Finishing"); + } else if (ocppVersion == MO_OCPP_V201) { + REQUIRE(getLastSentStatus(ocppVersion) == ""); + } + REQUIRE(!mo_isTransactionActive()); + REQUIRE(!mo_isTransactionRunning()); - checkedSN[1] = false; - expectedSN[1] = "Available"; - setConnectorPluggedInput([] () {return false;}); + sentOperations.clear(); + mo_setConnectorPluggedInput([] () {return false;}); loop(); - REQUIRE(checkedSN[1]); - REQUIRE(!ocppPermitsCharge()); + REQUIRE(getLastSentStatus(ocppVersion) == "Available"); } SECTION("via session management - plug out") { - setConnectorPluggedInput([] () {return false;}); + mo_setConnectorPluggedInput([] () {return true;}); + loop(); + + sentOperations.clear(); + mo_setConnectorPluggedInput([] () {return false;}); loop(); - REQUIRE(checkedSN[1]); - REQUIRE(!ocppPermitsCharge()); + REQUIRE(getLastSentStatus(ocppVersion) == "Available"); } - if (ocppPermitsCharge()) { - stopTransaction(); - } loop(); + + REQUIRE(!mo_ocppPermitsCharge()); + REQUIRE(mo_getTransactionId() == nullptr); + REQUIRE(!mo_isTransactionActive()); + REQUIRE(!mo_isTransactionRunning()); } 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(); + mo_setVarConfigBool(mo_getApiContext(), "CustomizationCtrlr", "PreBootTransactions", MO_CONFIG_EXT_PREFIX "PreBootTransactions", true); loop(); - beginTransaction_authorized("mIdTag"); + mo_beginTransaction_authorized("mIdTag", nullptr); loop(); - REQUIRE(isTransactionRunning()); + REQUIRE(mo_isTransactionRunning()); mtime += 3600 * 1000; //transaction duration ~1h - endTransaction(); + mo_endTransaction("mIdTag", nullptr); loop(); mtime += 3600 * 1000; //set base time one hour later - bool checkStartProcessed = false; + loop(); - getOcppContext()->getModel().getClock().setTime(BASE_TIME); - Timestamp basetime = Timestamp(); - basetime.setTime(BASE_TIME); + mo_setUnixTime(BASE_TIME_UNIX); - getOcppContext()->getOperationRegistry().setOnRequest("StartTransaction", - [&checkStartProcessed, basetime] (JsonObject payload) { - checkStartProcessed = true; - Timestamp timestamp; - timestamp.setTime(payload["timestamp"].as()); + loopback.setOnline(true); + loop(); - auto adjustmentDelay = basetime - timestamp; - REQUIRE((adjustmentDelay > 2 * 3600 - 10 && adjustmentDelay < 2 * 3600 + 10)); - }); - - bool checkStopProcessed = false; + const char *startTimeStr = nullptr; + const char *stopTimeStr = nullptr; - getOcppContext()->getOperationRegistry().setOnRequest("StopTransaction", - [&checkStopProcessed, basetime] (JsonObject payload) { - checkStopProcessed = true; - Timestamp timestamp; - timestamp.setTime(payload["timestamp"].as()); + for (auto& operation : sentOperations) { + if (operation.first == "StartTransaction") { + startTimeStr = operation.second["timestamp"] | "_Invalid"; + } else if (operation.first == "StopTransaction") { + stopTimeStr = operation.second["timestamp"] | "_Invalid"; + } else if (operation.first == "TransactionEvent") { + if (operation.second["eventType"].as() == "Started") { + startTimeStr = operation.second["timestamp"] | "_Invalid"; + } else if (operation.second["eventType"].as() == "Ended") { + stopTimeStr = operation.second["timestamp"] | "_Invalid"; + } + } + } - auto adjustmentDelay = basetime - timestamp; - REQUIRE((adjustmentDelay > 3600 - 10 && adjustmentDelay < 3600 + 10)); - }); - - loopback.setOnline(true); - loop(); + REQUIRE(startTimeStr != nullptr); + REQUIRE(stopTimeStr != nullptr); + + MicroOcpp::Timestamp baseTime, startTime, stopTime; + auto& clock = mo_getContext()->getClock(); + clock.fromUnixTime(baseTime, BASE_TIME_UNIX); + clock.parseString(startTimeStr, startTime); + clock.parseString(stopTimeStr, stopTime); - REQUIRE(checkStartProcessed); - REQUIRE(checkStopProcessed); + int32_t dtStart = -1, dtStop = -1; + clock.delta(baseTime, startTime, dtStart); + clock.delta(baseTime, stopTime, dtStop); + + REQUIRE(dtStart >= 2 * 3600 - 30); + REQUIRE(dtStart <= 2 * 3600 + 30); + REQUIRE(dtStop >= 3600 - 30); + REQUIRE(dtStop <= 3600 + 30); } + #if 0 //remaining tests need migration to MO v2 + SECTION("Preboot transactions - lose StartTx timestamp") { mocpp_deinitialize(); @@ -389,8 +513,8 @@ TEST_CASE( "Charging sessions" ) { int txId_generate = txId_base; int txId_confirm = txId_base; - getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&txId_generate] () { - return new Ocpp16::CustomOperation("StartTransaction", + getOcppContext()->getMessageService().registerOperation("StartTransaction", [&txId_generate] () { + return new v16::CustomOperation("StartTransaction", [] (JsonObject payload) {}, //ignore req [&txId_generate] () { //create conf @@ -404,8 +528,8 @@ TEST_CASE( "Charging sessions" ) { return doc; });}); - getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&txId_generate, &txId_confirm] () { - return new Ocpp16::CustomOperation("StopTransaction", + getOcppContext()->getMessageService().registerOperation("StopTransaction", [&txId_generate, &txId_confirm] () { + return new v16::CustomOperation("StopTransaction", [&txId_generate, &txId_confirm] (JsonObject payload) { //receive req REQUIRE( payload["transactionId"].as() == txId_generate ); @@ -469,8 +593,8 @@ TEST_CASE( "Charging sessions" ) { int txId_generate = txId_base; int txId_confirm = txId_base; - getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&txId_generate] () { - return new Ocpp16::CustomOperation("StartTransaction", + getOcppContext()->getMessageService().registerOperation("StartTransaction", [&txId_generate] () { + return new v16::CustomOperation("StartTransaction", [] (JsonObject payload) {}, //ignore req [&txId_generate] () { //create conf @@ -484,8 +608,8 @@ TEST_CASE( "Charging sessions" ) { return doc; });}); - getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&txId_generate, &txId_confirm] () { - return new Ocpp16::CustomOperation("StopTransaction", + getOcppContext()->getMessageService().registerOperation("StopTransaction", [&txId_generate, &txId_confirm] () { + return new v16::CustomOperation("StopTransaction", [&txId_generate, &txId_confirm] (JsonObject payload) { //receive req REQUIRE( payload["transactionId"].as() == txId_generate ); @@ -571,10 +695,10 @@ TEST_CASE( "Charging sessions" ) { // 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", + getOcppContext()->getMessageService().registerOperation("StartTransaction", [&check_1, &check_2, &check_3, &check_4, tx1_idTag, tx2_idTag, tx3_idTag, tx4_idTag] () { - return new Ocpp16::CustomOperation("StartTransaction", + return new v16::CustomOperation("StartTransaction", [&check_1, &check_2, &check_3, &check_4, tx1_idTag, tx2_idTag, tx3_idTag, tx4_idTag] (JsonObject payload) { //process req @@ -633,7 +757,7 @@ TEST_CASE( "Charging sessions" ) { bool checkProcessed = false; - auto changeAvailability = makeRequest(new Ocpp16::CustomOperation( + auto changeAvailability = makeRequest(new v16::CustomOperation( "ChangeAvailability", [] () { //create req @@ -689,7 +813,7 @@ TEST_CASE( "Charging sessions" ) { bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest( - new MicroOcpp::Ocpp16::CustomOperation("UnlockConnector", + new MicroOcpp::v16::CustomOperation("UnlockConnector", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); @@ -716,7 +840,7 @@ TEST_CASE( "Charging sessions" ) { checkProcessed = false; getOcppContext()->initiateRequest(makeRequest( - new MicroOcpp::Ocpp16::CustomOperation("UnlockConnector", + new MicroOcpp::v16::CustomOperation("UnlockConnector", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); @@ -741,7 +865,7 @@ TEST_CASE( "Charging sessions" ) { checkProcessed = false; getOcppContext()->initiateRequest(makeRequest( - new MicroOcpp::Ocpp16::CustomOperation("UnlockConnector", + new MicroOcpp::v16::CustomOperation("UnlockConnector", [] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); @@ -828,8 +952,8 @@ TEST_CASE( "Charging sessions" ) { * - final failure to send txMsg after tx terminated */ - getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&checkProcessedStartTx, &txId] () { - return new Ocpp16::CustomOperation("StartTransaction", + getOcppContext()->getMessageService().registerOperation("StartTransaction", [&checkProcessedStartTx, &txId] () { + return new v16::CustomOperation("StartTransaction", [&checkProcessedStartTx] (JsonObject payload) { //receive req checkProcessedStartTx = true; @@ -845,8 +969,8 @@ TEST_CASE( "Charging sessions" ) { return doc; });}); - getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&checkProcessedStopTx] () { - return new Ocpp16::CustomOperation("StopTransaction", + getOcppContext()->getMessageService().registerOperation("StopTransaction", [&checkProcessedStopTx] () { + return new v16::CustomOperation("StopTransaction", [&checkProcessedStopTx] (JsonObject payload) { //receive req checkProcessedStopTx = true; @@ -957,8 +1081,8 @@ TEST_CASE( "Charging sessions" ) { unsigned int attemptNr = 0; - getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&checkProcessedStartTx, &txId, &attemptNr] () { - return new Ocpp16::CustomOperation("StartTransaction", + getOcppContext()->getMessageService().registerOperation("StartTransaction", [&checkProcessedStartTx, &txId, &attemptNr] () { + return new v16::CustomOperation("StartTransaction", [&attemptNr] (JsonObject payload) { //receive req attemptNr++; @@ -1025,8 +1149,8 @@ TEST_CASE( "Charging sessions" ) { getOcppContext()->getModel().getClock().setTime(BASE_TIME); - getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&checkProcessedStartTx, &txId, &attemptNr] () { - return new Ocpp16::CustomOperation("StartTransaction", + getOcppContext()->getMessageService().registerOperation("StartTransaction", [&checkProcessedStartTx, &txId, &attemptNr] () { + return new v16::CustomOperation("StartTransaction", [&attemptNr] (JsonObject payload) { //receive req attemptNr++; @@ -1046,8 +1170,8 @@ TEST_CASE( "Charging sessions" ) { return attemptNr < NUM_ATTEMPTS ? "InternalError" : (const char*)nullptr; });}); - getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&checkProcessedStopTx] () { - return new Ocpp16::CustomOperation("StopTransaction", + getOcppContext()->getMessageService().registerOperation("StopTransaction", [&checkProcessedStopTx] () { + return new v16::CustomOperation("StopTransaction", [&checkProcessedStopTx] (JsonObject payload) { //receive req checkProcessedStopTx = true; @@ -1279,5 +1403,7 @@ TEST_CASE( "Charging sessions" ) { //Note: queueing offline transactions without FS is currently not implemented } - mocpp_deinitialize(); +#endif + + mo_deinitialize(); } diff --git a/tests/Configuration.cpp b/tests/Configuration.cpp index 91fa1e28..ac7ea503 100644 --- a/tests/Configuration.cpp +++ b/tests/Configuration.cpp @@ -284,7 +284,7 @@ TEST_CASE( "Configuration" ) { bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "GetConfiguration", [] () { //create req @@ -321,7 +321,7 @@ TEST_CASE( "Configuration" ) { checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "GetConfiguration", [] () { //create req @@ -375,7 +375,7 @@ TEST_CASE( "Configuration" ) { //update existing config bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ChangeConfiguration", [] () { //create req @@ -397,7 +397,7 @@ TEST_CASE( "Configuration" ) { //try to update not existing key checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ChangeConfiguration", [] () { //create req @@ -418,7 +418,7 @@ TEST_CASE( "Configuration" ) { //try to update config with malformatted value checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ChangeConfiguration", [] () { //create req @@ -445,7 +445,7 @@ TEST_CASE( "Configuration" ) { //validation success checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ChangeConfiguration", [] () { //create req @@ -467,7 +467,7 @@ TEST_CASE( "Configuration" ) { //validation failure checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ChangeConfiguration", [] () { //create req diff --git a/tests/ConfigurationBehavior.cpp b/tests/ConfigurationBehavior.cpp index fc30b356..f0694707 100644 --- a/tests/ConfigurationBehavior.cpp +++ b/tests/ConfigurationBehavior.cpp @@ -239,7 +239,7 @@ TEST_CASE( "Configuration Behavior" ) { SECTION("set true") { configBool->setBool(true); - context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + context->initiateRequest(makeRequest(new v16::CustomOperation( "RemoteStartTransaction", [] () { //create req @@ -261,7 +261,7 @@ TEST_CASE( "Configuration Behavior" ) { SECTION("set false") { configBool->setBool(false); - context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + context->initiateRequest(makeRequest(new v16::CustomOperation( "RemoteStartTransaction", [] () { //create req diff --git a/tests/Core.cpp b/tests/Core.cpp new file mode 100644 index 00000000..8751806e --- /dev/null +++ b/tests/Core.cpp @@ -0,0 +1,139 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include "./helpers/testHelper.h" + +using namespace MicroOcpp; + +TEST_CASE( "Time" ) { + printf("\nRun %s\n", "Time"); + + //initialize Context without any configs + mo_initialize(); + + mtime = 0; + mo_getContext()->setTicksCb(custom_timer_cb); + + LoopbackConnection loopback; + mo_getContext()->setConnection(&loopback); + + mo_setup(); + + auto& clock = mo_getContext()->getClock(); + + SECTION("increment system time") { + + auto t0 = clock.now(); + auto uptime0 = clock.getUptime(); + + mo_loop(); + + mtime = 200; //200 ms + + mo_loop(); + + auto t200 = clock.now(); + auto uptime200 = clock.getUptime(); + + int32_t dt; + REQUIRE( clock.delta(t200, t0, dt) ); + REQUIRE( dt == 0 ); //floored to 0s + REQUIRE( clock.delta(uptime200, uptime0, dt) ); + REQUIRE( dt == 0 ); //floored to 0s + + mtime = 1000; //1s + + mo_loop(); + + auto t1 = clock.now(); + auto uptime1 = clock.getUptime(); + + REQUIRE( clock.delta(t1, t0, dt) ); + REQUIRE( dt == 1 ); + REQUIRE( clock.delta(uptime1, uptime0, dt) ); + REQUIRE( dt == 1 ); + + mtime = 3000; //3s, skipped 2s step + + mo_loop(); + + auto t3 = clock.now(); + auto uptime3 = clock.getUptime(); + + REQUIRE( clock.delta(t3, t0, dt) ); + REQUIRE( dt == 3 ); + REQUIRE( clock.delta(t3, t1, dt) ); + REQUIRE( dt == 2 ); + REQUIRE( clock.delta(uptime3, uptime0, dt) ); + REQUIRE( dt == 3 ); + REQUIRE( clock.delta(uptime3, uptime1, dt) ); + REQUIRE( dt == 2 ); + + //system time rolls over, need two steps because clock works with signed values internally + mtime = (std::numeric_limits::max() - 999UL) / 2UL; + mo_loop(); + mtime = std::numeric_limits::max() - 999UL; + mo_loop(); + + auto tRoll_min1 = clock.now(); + auto uptimeRoll_min1 = clock.getUptime(); + + mtime = 0; + mo_loop(); + + auto tRoll_0 = clock.now(); + auto uptimeRoll_0 = clock.getUptime(); + + REQUIRE( clock.delta(tRoll_0, tRoll_min1, dt) ); + REQUIRE( dt == 1 ); + REQUIRE( clock.delta(tRoll_0, t0, dt) ); + REQUIRE( dt == (int32_t)(std::numeric_limits::max() / 1000UL) ); + REQUIRE( clock.delta(uptimeRoll_0, uptimeRoll_min1, dt) ); + REQUIRE( dt == 1 ); + REQUIRE( clock.delta(uptimeRoll_0, uptime0, dt) ); + REQUIRE( dt == (int32_t)(std::numeric_limits::max() / 1000UL) ); + + //system time rolls over too early + mtime = std::numeric_limits::max() / 100UL - 999UL; //rollover from ~0.5 days to 0 instead of ~50 days to 0 + mo_loop(); + + tRoll_min1 = clock.now(); + uptimeRoll_min1 = clock.getUptime(); + + mtime = 0; + mo_loop(); + + tRoll_0 = clock.now(); + uptimeRoll_0 = clock.getUptime(); + + REQUIRE( clock.delta(tRoll_0, tRoll_min1, dt) ); + REQUIRE( dt == 0 ); //value corrected to 0 + REQUIRE( clock.delta(uptimeRoll_0, uptimeRoll_min1, dt) ); + REQUIRE( dt == 0 ); + } + + SECTION("serialize and parse time strings") { + + int32_t tRefInt = 1750000000; + const char *tRefString = "2025-06-15T15:06:40.123Z"; + + Timestamp t; + REQUIRE( clock.parseString(tRefString, t) ); + int32_t tUnix; + REQUIRE( clock.toUnixTime(t, tUnix) ); + REQUIRE( tUnix == tRefInt ); + + char buf [MO_JSONDATE_SIZE]; + REQUIRE( clock.toJsonString(t, buf, sizeof(buf)) ); + INFO(buf); + REQUIRE( !strcmp(buf, tRefString) ); + } + + + + mo_deinitialize(); +} diff --git a/tests/FirmwareManagement.cpp b/tests/FirmwareManagement.cpp index 5f11b8c4..eaec328d 100644 --- a/tests/FirmwareManagement.cpp +++ b/tests/FirmwareManagement.cpp @@ -50,9 +50,9 @@ TEST_CASE( "FirmwareManagement" ) { bool checkProcessed = false; - getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + getOcppContext()->getMessageService().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { - return new Ocpp16::CustomOperation("FirmwareStatusNotification", + return new v16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req checkProcessed = true; @@ -67,7 +67,7 @@ TEST_CASE( "FirmwareManagement" ) { }); }); - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "UpdateFirmware", [] () { //create req @@ -90,9 +90,9 @@ TEST_CASE( "FirmwareManagement" ) { int checkProcessed = 0; - getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + getOcppContext()->getMessageService().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { - return new Ocpp16::CustomOperation("FirmwareStatusNotification", + return new v16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req if (checkProcessed == 0) { @@ -131,7 +131,7 @@ TEST_CASE( "FirmwareManagement" ) { } }); - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "UpdateFirmware", [] () { //create req @@ -159,9 +159,9 @@ TEST_CASE( "FirmwareManagement" ) { int checkProcessed = 0; - getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + getOcppContext()->getMessageService().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { - return new Ocpp16::CustomOperation("FirmwareStatusNotification", + return new v16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req if (checkProcessed == 0) { @@ -198,7 +198,7 @@ TEST_CASE( "FirmwareManagement" ) { } }); - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "UpdateFirmware", [] () { //create req @@ -226,9 +226,9 @@ TEST_CASE( "FirmwareManagement" ) { int checkProcessed = 0; - getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + getOcppContext()->getMessageService().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { - return new Ocpp16::CustomOperation("FirmwareStatusNotification", + return new v16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req if (checkProcessed == 0) { @@ -285,7 +285,7 @@ TEST_CASE( "FirmwareManagement" ) { } }); - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "UpdateFirmware", [] () { //create req @@ -315,9 +315,9 @@ TEST_CASE( "FirmwareManagement" ) { int checkProcessed = 0; - getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + getOcppContext()->getMessageService().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { - return new Ocpp16::CustomOperation("FirmwareStatusNotification", + return new v16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req if (checkProcessed == 0) { @@ -369,7 +369,7 @@ TEST_CASE( "FirmwareManagement" ) { return true; }); - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "UpdateFirmware", [] () { //create req @@ -398,9 +398,9 @@ TEST_CASE( "FirmwareManagement" ) { int checkProcessed = 0; - getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + getOcppContext()->getMessageService().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { - return new Ocpp16::CustomOperation("FirmwareStatusNotification", + return new v16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req if (checkProcessed == 0) { @@ -448,7 +448,7 @@ TEST_CASE( "FirmwareManagement" ) { } }); - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "UpdateFirmware", [] () { //create req @@ -478,9 +478,9 @@ TEST_CASE( "FirmwareManagement" ) { int checkProcessed = 0; - getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + getOcppContext()->getMessageService().registerOperation("FirmwareStatusNotification", [&checkProcessed] () { - return new Ocpp16::CustomOperation("FirmwareStatusNotification", + return new v16::CustomOperation("FirmwareStatusNotification", [ &checkProcessed] (JsonObject payload) { //process req if (checkProcessed == 0) { @@ -537,7 +537,7 @@ TEST_CASE( "FirmwareManagement" ) { } }); - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "UpdateFirmware", [] () { //create req diff --git a/tests/LocalAuthList.cpp b/tests/LocalAuthList.cpp index 855a980b..e927b313 100644 --- a/tests/LocalAuthList.cpp +++ b/tests/LocalAuthList.cpp @@ -37,7 +37,7 @@ void generateAuthList(JsonArray out, size_t size, bool compact) { idTagInfo = authData["idTagInfo"].to(); } - char buf [IDTAG_LEN_MAX + 1]; + char buf [MO_IDTAG_LEN_MAX + 1]; sprintf(buf, "mIdTag%zu", i); authData[AUTHDATA_KEY_IDTAG(compact)] = buf; idTagInfo[AUTHDATA_KEY_STATUS(compact)] = "Accepted"; @@ -402,8 +402,8 @@ TEST_CASE( "LocalAuth" ) { authService->updateLocalList(localAuthList.as(), 1, false); //patch Authorize so it will reject all idTags - getOcppContext()->getOperationRegistry().registerOperation("Authorize", [] () { - return new Ocpp16::CustomOperation("Authorize", + getOcppContext()->getMessageService().registerOperation("Authorize", [] () { + return new v16::CustomOperation("Authorize", [] (JsonObject) {}, //ignore req [] () { //create conf @@ -445,8 +445,8 @@ TEST_CASE( "LocalAuth" ) { //patch Authorize so it will reject all idTags bool checkAuthorize = false; - getOcppContext()->getOperationRegistry().registerOperation("Authorize", [&checkAuthorize] () { - return new Ocpp16::CustomOperation("Authorize", + getOcppContext()->getMessageService().registerOperation("Authorize", [&checkAuthorize] () { + return new v16::CustomOperation("Authorize", [&checkAuthorize] (JsonObject) { checkAuthorize = true; }, @@ -460,8 +460,8 @@ TEST_CASE( "LocalAuth" ) { //patch StartTransaction so it will DeAuthorize all txs bool checkStartTx = false; - getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&checkStartTx] () { - return new Ocpp16::CustomOperation("StartTransaction", + getOcppContext()->getMessageService().registerOperation("StartTransaction", [&checkStartTx] () { + return new v16::CustomOperation("StartTransaction", [&checkStartTx] (JsonObject) { checkStartTx = true; }, @@ -477,8 +477,8 @@ TEST_CASE( "LocalAuth" ) { //check resulting StatusNotification message bool checkLocalListConflict = false; - getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", [&checkLocalListConflict] () { - return new Ocpp16::CustomOperation("StatusNotification", + getOcppContext()->getMessageService().registerOperation("StatusNotification", [&checkLocalListConflict] () { + return new v16::CustomOperation("StatusNotification", [&checkLocalListConflict] (JsonObject payload) { if (payload["connectorId"] == 0 && !strcmp(payload["errorCode"] | "_Undefined", "LocalListConflict")) { @@ -645,7 +645,7 @@ TEST_CASE( "LocalAuth" ) { //Full update - happy path bool checkAccepted = false; getOcppContext()->initiateRequest(makeRequest( - new Ocpp16::CustomOperation("SendLocalList", + new v16::CustomOperation("SendLocalList", [&listVersion, &listSize, &populatedEntryIdTag] () { //create req auto doc = makeJsonDoc("UnitTests", @@ -688,7 +688,7 @@ TEST_CASE( "LocalAuth" ) { checkAccepted = false; getOcppContext()->initiateRequest(makeRequest( - new Ocpp16::CustomOperation("SendLocalList", + new v16::CustomOperation("SendLocalList", [&listVersion, &listSize] () { //create req auto doc = makeJsonDoc("UnitTests", @@ -714,7 +714,7 @@ TEST_CASE( "LocalAuth" ) { bool checkVersionMismatch = false; getOcppContext()->initiateRequest(makeRequest( - new Ocpp16::CustomOperation("SendLocalList", + new v16::CustomOperation("SendLocalList", [&listVersion, &listSizeInvalid] () { //create req auto doc = makeJsonDoc("UnitTests", @@ -741,7 +741,7 @@ TEST_CASE( "LocalAuth" ) { checkAccepted = false; getOcppContext()->initiateRequest(makeRequest( - new Ocpp16::CustomOperation("SendLocalList", + new v16::CustomOperation("SendLocalList", [&listVersion, &listSize] () { //create req auto doc = makeJsonDoc("UnitTests", @@ -767,7 +767,7 @@ TEST_CASE( "LocalAuth" ) { checkAccepted = false; getOcppContext()->initiateRequest(makeRequest( - new Ocpp16::CustomOperation("SendLocalList", + new v16::CustomOperation("SendLocalList", [&listVersion, &listSize] () { //create req auto doc = makeJsonDoc("UnitTests", @@ -794,7 +794,7 @@ TEST_CASE( "LocalAuth" ) { bool errOccurence = false; getOcppContext()->initiateRequest(makeRequest( - new Ocpp16::CustomOperation("SendLocalList", + new v16::CustomOperation("SendLocalList", [&listVersionInvalid, &listSizeInvalid] () { //create req auto doc = makeJsonDoc("UnitTests", @@ -830,7 +830,7 @@ TEST_CASE( "LocalAuth" ) { //Full update - happy path bool checkAccepted = false; getOcppContext()->initiateRequest(makeRequest( - new Ocpp16::CustomOperation("SendLocalList", + new v16::CustomOperation("SendLocalList", [&i] () { //create req auto doc = makeJsonDoc("UnitTests", @@ -838,7 +838,7 @@ TEST_CASE( "LocalAuth" ) { auto payload = doc->to(); payload["listVersion"] = (int) i; - char buf [IDTAG_LEN_MAX + 1]; + char buf [MO_IDTAG_LEN_MAX + 1]; sprintf(buf, "mIdTag%zu", i-1); payload["localAuthorizationList"][0]["idTag"] = buf; payload["localAuthorizationList"][0]["idTagInfo"]["status"] = "Accepted"; @@ -868,7 +868,7 @@ TEST_CASE( "LocalAuth" ) { bool checkFailed = false; getOcppContext()->initiateRequest(makeRequest( - new Ocpp16::CustomOperation("SendLocalList", + new v16::CustomOperation("SendLocalList", [&listVersionInvalid] () { //create req auto doc = makeJsonDoc("UnitTests", @@ -877,7 +877,7 @@ TEST_CASE( "LocalAuth" ) { payload["listVersion"] = listVersionInvalid; //update already existing entry - char buf [IDTAG_LEN_MAX + 1]; + char buf [MO_IDTAG_LEN_MAX + 1]; sprintf(buf, "mIdTag%zu", 0UL); payload["localAuthorizationList"][0]["idTag"] = buf; payload["localAuthorizationList"][0]["idTagInfo"]["status"] = "Accepted"; @@ -909,7 +909,7 @@ TEST_CASE( "LocalAuth" ) { int checkListVerion = -1; getOcppContext()->initiateRequest(makeRequest( - new Ocpp16::CustomOperation("GetLocalListVersion", + new v16::CustomOperation("GetLocalListVersion", [] () { //create req return createEmptyDocument(); diff --git a/tests/Metering.cpp b/tests/Metering.cpp index aa555e99..77786e16 100644 --- a/tests/Metering.cpp +++ b/tests/Metering.cpp @@ -1,12 +1,11 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #include #include #include #include -#include #include #include #include @@ -635,8 +634,8 @@ TEST_CASE("Metering") { unsigned int attemptNr = 0; - getOcppContext()->getOperationRegistry().registerOperation("MeterValues", [&attemptNr] () { - return new Ocpp16::CustomOperation("MeterValues", + getOcppContext()->getMessageService().registerOperation("MeterValues", [&attemptNr] () { + return new v16::CustomOperation("MeterValues", [&attemptNr] (JsonObject payload) { //receive req attemptNr++; diff --git a/tests/Reservation.cpp b/tests/Reservation.cpp index 306b804a..85ea307f 100644 --- a/tests/Reservation.cpp +++ b/tests/Reservation.cpp @@ -88,7 +88,7 @@ TEST_CASE( "Reservation" ) { rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "RemoteStartTransaction", [] () { //create req @@ -111,7 +111,7 @@ TEST_CASE( "Reservation" ) { rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "RemoteStartTransaction", [idTag] () { //create req @@ -170,9 +170,9 @@ TEST_CASE( "Reservation" ) { REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); bool checkProcessed = false; - getOcppContext()->getOperationRegistry().registerOperation("Authorize", + getOcppContext()->getMessageService().registerOperation("Authorize", [parentIdTag, &checkProcessed] () { - return new Ocpp16::CustomOperation("Authorize", + return new v16::CustomOperation("Authorize", [] (JsonObject) {}, //ignore req payload [parentIdTag, &checkProcessed] () { //create conf @@ -255,8 +255,8 @@ TEST_CASE( "Reservation" ) { REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); Timestamp expired = expiryDate + 1; - char expired_cstr [JSONDATE_LENGTH + 1]; - expired.toJsonString(expired_cstr, JSONDATE_LENGTH + 1); + char expired_cstr [MO_JSONDATE_SIZE]; + expired.toJsonString(expired_cstr, MO_JSONDATE_SIZE); model.getClock().setTime(expired_cstr); REQUIRE( connector->getStatus() == ChargePointStatus_Available ); @@ -315,17 +315,17 @@ TEST_CASE( "Reservation" ) { //simple reservation bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ReserveNow", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(5) + - JSONDATE_LENGTH + 1); + MO_JSONDATE_SIZE); auto payload = doc->to(); payload["connectorId"] = connectorId; - char expiryDate_cstr [JSONDATE_LENGTH + 1]; - expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1); + char expiryDate_cstr [MO_JSONDATE_SIZE]; + expiryDate.toJsonString(expiryDate_cstr, MO_JSONDATE_SIZE); payload["expiryDate"] = expiryDate_cstr; payload["idTag"] = idTag; payload["parentIdTag"] = parentIdTag; @@ -351,17 +351,17 @@ TEST_CASE( "Reservation" ) { REQUIRE( connector->getStatus() == ChargePointStatus_Faulted ); checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ReserveNow", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(5) + - JSONDATE_LENGTH + 1); + MO_JSONDATE_SIZE); auto payload = doc->to(); payload["connectorId"] = connectorId; - char expiryDate_cstr [JSONDATE_LENGTH + 1]; - expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1); + char expiryDate_cstr [MO_JSONDATE_SIZE]; + expiryDate.toJsonString(expiryDate_cstr, MO_JSONDATE_SIZE); payload["expiryDate"] = expiryDate_cstr; payload["idTag"] = idTag; payload["parentIdTag"] = parentIdTag; @@ -386,17 +386,17 @@ TEST_CASE( "Reservation" ) { REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ReserveNow", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(5) + - JSONDATE_LENGTH + 1); + MO_JSONDATE_SIZE); auto payload = doc->to(); payload["connectorId"] = connectorId; - char expiryDate_cstr [JSONDATE_LENGTH + 1]; - expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1); + char expiryDate_cstr [MO_JSONDATE_SIZE]; + expiryDate.toJsonString(expiryDate_cstr, MO_JSONDATE_SIZE); payload["expiryDate"] = expiryDate_cstr; payload["idTag"] = idTag; payload["parentIdTag"] = parentIdTag; @@ -423,17 +423,17 @@ TEST_CASE( "Reservation" ) { REQUIRE( connector->getStatus() == ChargePointStatus_Unavailable ); checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ReserveNow", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(5) + - JSONDATE_LENGTH + 1); + MO_JSONDATE_SIZE); auto payload = doc->to(); payload["connectorId"] = connectorId; - char expiryDate_cstr [JSONDATE_LENGTH + 1]; - expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1); + char expiryDate_cstr [MO_JSONDATE_SIZE]; + expiryDate.toJsonString(expiryDate_cstr, MO_JSONDATE_SIZE); payload["expiryDate"] = expiryDate_cstr; payload["idTag"] = idTag; payload["parentIdTag"] = parentIdTag; @@ -469,7 +469,7 @@ TEST_CASE( "Reservation" ) { //CancelReservation successfully bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "CancelReservation", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req @@ -490,7 +490,7 @@ TEST_CASE( "Reservation" ) { //CancelReservation while no reservation exists checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "CancelReservation", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req diff --git a/tests/Reset.cpp b/tests/Reset.cpp index 1390b336..90b8e96d 100644 --- a/tests/Reset.cpp +++ b/tests/Reset.cpp @@ -39,8 +39,8 @@ TEST_CASE( "Reset" ) { mocpp_set_timer(custom_timer_cb); - getOcppContext()->getOperationRegistry().registerOperation("Authorize", [] () { - return new Ocpp16::CustomOperation("Authorize", + getOcppContext()->getMessageService().registerOperation("Authorize", [] () { + return new v16::CustomOperation("Authorize", [] (JsonObject) {}, //ignore req [] () { //create conf @@ -50,8 +50,8 @@ TEST_CASE( "Reset" ) { return doc; });}); - getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [] () { - return new Ocpp16::CustomOperation("TransactionEvent", + getOcppContext()->getMessageService().registerOperation("TransactionEvent", [] () { + return new v16::CustomOperation("TransactionEvent", [] (JsonObject) {}, //ignore req [] () { //create conf @@ -97,7 +97,7 @@ TEST_CASE( "Reset" ) { bool checkProcessed = false; - auto resetRequest = makeRequest(new Ocpp16::CustomOperation( + auto resetRequest = makeRequest(new v16::CustomOperation( "Reset", [] () { //create req @@ -150,7 +150,7 @@ TEST_CASE( "Reset" ) { bool checkProcessed = false; - auto resetRequest = makeRequest(new Ocpp16::CustomOperation( + auto resetRequest = makeRequest(new v16::CustomOperation( "Reset", [] () { //create req @@ -234,8 +234,8 @@ TEST_CASE( "Reset" ) { bool checkProcessedTx = false; - getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkProcessedTx] () { - return new Ocpp16::CustomOperation("TransactionEvent", + getOcppContext()->getMessageService().registerOperation("TransactionEvent", [&checkProcessedTx] () { + return new v16::CustomOperation("TransactionEvent", [&checkProcessedTx] (JsonObject payload) { //process req checkProcessedTx = true; @@ -251,7 +251,7 @@ TEST_CASE( "Reset" ) { bool checkProcessed = false; - auto resetRequest = makeRequest(new Ocpp16::CustomOperation( + auto resetRequest = makeRequest(new v16::CustomOperation( "Reset", [] () { //create req @@ -305,7 +305,7 @@ TEST_CASE( "Reset" ) { bool checkProcessed = false; - auto resetRequest = makeRequest(new Ocpp16::CustomOperation( + auto resetRequest = makeRequest(new v16::CustomOperation( "Reset", [] () { //create req @@ -337,7 +337,7 @@ TEST_CASE( "Reset" ) { bool checkProcessed = false; - auto resetRequest = makeRequest(new Ocpp16::CustomOperation( + auto resetRequest = makeRequest(new v16::CustomOperation( "Reset", [] () { //create req @@ -374,4 +374,4 @@ TEST_CASE( "Reset" ) { mocpp_deinitialize(); } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/tests/Security.cpp b/tests/Security.cpp index 43bf2d7a..f7503e89 100644 --- a/tests/Security.cpp +++ b/tests/Security.cpp @@ -43,7 +43,7 @@ TEST_CASE( "Security" ) { MO_MEM_RESET(); - getOcppContext()->initiateRequest(makeRequest(new Ocpp201::SecurityEventNotification( + getOcppContext()->initiateRequest(makeRequest(new v201::SecurityEventNotification( "ReconfigurationOfSecurityParameters", getOcppContext()->getModel().getClock().now()))); @@ -55,4 +55,4 @@ TEST_CASE( "Security" ) { mocpp_deinitialize(); } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/tests/SmartCharging.cpp b/tests/SmartCharging.cpp index 7cf53b32..8d5036ad 100644 --- a/tests/SmartCharging.cpp +++ b/tests/SmartCharging.cpp @@ -3,19 +3,18 @@ // MIT License #include -#include -#include +#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 "[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_V201 "[2,\"testmsg\",\"SetChargingProfile\",{\"evseId\": 1,\"chargingProfile\": {\"id\": 0,\"stackLevel\":0,\"chargingProfilePurpose\":\"TxDefaultProfile\",\"chargingProfileKind\":\"Recurring\",\"recurrencyKind\":\"Daily\",\"validFrom\":\"2022-06-12T00:00:00.000Z\",\"validTo\":\"2023-06-21T00:00:00.000Z\",\"chargingSchedule\":{\"id\":0,\"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}}}]" @@ -33,10 +32,51 @@ #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_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_9_VIA_REQSTARTTX_20A "[2,\"testmsg\",\"RequestStartTransaction\",{\"evseId\":1, \"idToken\":{\"idToken\":\"mIdTag\",\"type\":\"ISO14443\"},\"chargingProfile\":{\"id\":9, \"stackLevel\":0,\"chargingProfilePurpose\":\"TxProfile\",\"chargingProfileKind\":\"Relative\",\"chargingSchedule\":{\"id\":9,\"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}]}}}]" +char setChargingProfile [1024]; +void updateSetChargingProfile(const char *scprofile_raw, int ocppVersion) { + if (ocppVersion == MO_OCPP_V16) { + auto ret = snprintf(setChargingProfile, sizeof(setChargingProfile), "%s", scprofile_raw); + if (ret < 0 || (size_t)ret >= sizeof(setChargingProfile)) { + FAIL("snprintf"); + } + return; + } + + DynamicJsonDocument doc (4096); + auto err = deserializeJson(doc, scprofile_raw); + if (err) { + char buf [100]; + snprintf(buf, sizeof(buf), "JSON deserialization error: %s", err.c_str()); + FAIL(buf); + } + + JsonObject payload = doc[3]; + + payload["evseId"] = payload["connectorId"]; + payload.remove("connectorId"); + payload["chargingProfile"] = payload["csChargingProfiles"]; + payload.remove("csChargingProfiles"); + JsonObject chargingProfile = payload["chargingProfile"]; + chargingProfile["id"] = chargingProfile["chargingProfileId"]; + chargingProfile.remove("chargingProfileId"); + chargingProfile["chargingSchedule"]["id"] = chargingProfile["id"]; + + auto ret = serializeJson(doc, setChargingProfile); + + if (doc.isNull() || doc.overflowed() || ret >= sizeof(setChargingProfile) - 1 || ret < 2) { + FAIL("failure to convert JSON to OCPP 2.0.1"); + } +} + +float g_limit_current = -1.f; +void setCurrentCb(float limit_current) { + g_limit_current = limit_current; +} using namespace MicroOcpp; @@ -44,140 +84,124 @@ using namespace MicroOcpp; TEST_CASE( "SmartCharging" ) { printf("\nRun %s\n", "SmartCharging"); - //initialize Context with dummy socket - LoopbackConnection loopback; - mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + g_limit_current = -1.f; - auto context = getOcppContext(); - auto& model = context->getModel(); + //initialize Context without any configs + mo_initialize(); - mocpp_set_timer(custom_timer_cb); + mo_getContext()->setTicksCb(custom_timer_cb); - model.getClock().setTime(BASE_TIME); + LoopbackConnection loopback; + mo_getContext()->setConnection(&loopback); - endTransaction(); + auto ocppVersion = GENERATE(MO_OCPP_V16, MO_OCPP_V201); + mo_setOcppVersion(ocppVersion); - SECTION("Load Smart Charging Service"){ + mo_setBootNotificationData("TestModel", "TestVendor"); - REQUIRE(!model.getSmartChargingService()); + mo_setSmartChargingPowerOutput([] (float) {}); - setSmartChargingOutput([] (float, float, int) {}); + mo_setup(); - REQUIRE(model.getSmartChargingService()); - } + loop(); - setSmartChargingOutput([] (float, float, int) {}); - auto scService = model.getSmartChargingService(); + MicroOcpp::SmartChargingService *scService = mo_getContext()->getModelCommon().getSmartChargingService(); + REQUIRE(scService != nullptr); - scService->clearChargingProfile([] (int, int, ChargingProfilePurposeType, int) { - return true; - }); + SECTION("Clean up files") { + auto filesystem = mo_getFilesystem(); + FilesystemUtils::removeByPrefix(filesystem, ""); + } 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(scService->getChargingProfilesCount() == 0); - REQUIRE(count == 1); + if (ocppVersion == MO_OCPP_V16) { + loopback.sendTXT(SCPROFILE_0, strlen(SCPROFILE_0)); + } else if (ocppVersion == MO_OCPP_V201) { + loopback.sendTXT(SCPROFILE_0_V201, strlen(SCPROFILE_0_V201)); + } + updateSetChargingProfile(SCPROFILE_0, ocppVersion); + loopback.sendTXT(setChargingProfile, strlen(setChargingProfile)); - count = 0; - scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { - count++; - return true; - }); + loop(); - REQUIRE(count == 1); + REQUIRE(scService->getChargingProfilesCount() == 1); - count = 0; - scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { - count++; - return true; - }); + REQUIRE(scService->clearChargingProfile(-1, -1, MicroOcpp::ChargingProfilePurposeType::UNDEFINED, -1)); - REQUIRE(count == 0); + REQUIRE(scService->getChargingProfilesCount() == 0); } SECTION("Charging Profiles persistency over reboots") { - loopback.sendTXT(SCPROFILE_0, strlen(SCPROFILE_0)); - - mocpp_deinitialize(); + updateSetChargingProfile(SCPROFILE_0, ocppVersion); + loopback.sendTXT(setChargingProfile, strlen(setChargingProfile)); + loop(); - mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + mo_deinitialize(); - setSmartChargingOutput([] (float, float, int) {}); - scService = getOcppContext()->getModel().getSmartChargingService(); + mo_initialize(); + mo_getContext()->setConnection(&loopback); + mo_setOcppVersion(ocppVersion); + mo_setup(); + scService = mo_getContext()->getModelCommon().getSmartChargingService(); - unsigned int count = 0; - scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { - count++; - return true; - }); - - REQUIRE (count == 1); + REQUIRE(scService->getChargingProfilesCount() == 1); + REQUIRE(scService->clearChargingProfile(-1, -1, MicroOcpp::ChargingProfilePurposeType::UNDEFINED, -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; - }); + updateSetChargingProfile(SCPROFILE_0, ocppVersion); + loopback.sendTXT(setChargingProfile, strlen(setChargingProfile)); + loop(); - REQUIRE(count == 1); + updateSetChargingProfile(SCPROFILE_0_ALT_SAME_ID, ocppVersion); + loopback.sendTXT(setChargingProfile, strlen(setChargingProfile)); + loop(); - loopback.sendTXT(SCPROFILE_0, strlen(SCPROFILE_0)); + REQUIRE(scService->getChargingProfilesCount() == 1); + REQUIRE(scService->clearChargingProfile(-1, -1, MicroOcpp::ChargingProfilePurposeType::UNDEFINED, -1)); - loopback.sendTXT(SCPROFILE_0_ALT_SAME_STACKEVEL_PURPOSE, strlen(SCPROFILE_0_ALT_SAME_STACKEVEL_PURPOSE)); + updateSetChargingProfile(SCPROFILE_0, ocppVersion); + loopback.sendTXT(setChargingProfile, strlen(setChargingProfile)); + loop(); - count = 0; - scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { - count++; - return true; - }); + updateSetChargingProfile(SCPROFILE_0_ALT_SAME_STACKEVEL_PURPOSE, ocppVersion); + loopback.sendTXT(setChargingProfile, strlen(setChargingProfile)); + loop(); - REQUIRE(count == 1); + REQUIRE(scService->getChargingProfilesCount() == 1); + REQUIRE(scService->clearChargingProfile(-1, -1, MicroOcpp::ChargingProfilePurposeType::UNDEFINED, -1)); } SECTION("Set charging profile via RmtStartTx") { - float current = -1.f; - setSmartChargingOutput([¤t] (float, float limit_current, int) { - current = limit_current; - }); + mo_setSmartChargingCurrentOutput(setCurrentCb); + g_limit_current = -1.f; loop(); - loopback.sendTXT(SCPROFILE_9_VIA_RMTSTARTTX_20A, strlen(SCPROFILE_9_VIA_RMTSTARTTX_20A)); + if (ocppVersion == MO_OCPP_V16) { + loopback.sendTXT(SCPROFILE_9_VIA_RMTSTARTTX_20A, strlen(SCPROFILE_9_VIA_RMTSTARTTX_20A)); + } else if (ocppVersion == MO_OCPP_V201) { + loopback.sendTXT(SCPROFILE_9_VIA_REQSTARTTX_20A, strlen(SCPROFILE_9_VIA_REQSTARTTX_20A)); + } loop(); - REQUIRE((current > 19.99f && current < 20.01f)); + REQUIRE(g_limit_current > 19.99f); + REQUIRE(g_limit_current < 20.01f); - endTransaction(); + mo_endTransaction(nullptr, nullptr); loop(); } + #if 0 + SECTION("Set ChargePointMaxProfile - Absolute") { float current = -1.f; @@ -472,7 +496,7 @@ TEST_CASE( "SmartCharging" ) { bool checkProcessed = false; - auto getCompositeSchedule = makeRequest(new Ocpp16::CustomOperation( + auto getCompositeSchedule = makeRequest(new v16::CustomOperation( "GetCompositeSchedule", [] () { //create req @@ -489,8 +513,8 @@ TEST_CASE( "SmartCharging" ) { REQUIRE(!strcmp(payload["status"], "Accepted")); REQUIRE(payload["connectorId"] == 1); - char checkScheduleStart [JSONDATE_LENGTH + 1]; - model.getClock().now().toJsonString(checkScheduleStart, JSONDATE_LENGTH + 1); + char checkScheduleStart [MO_JSONDATE_SIZE]; + model.getClock().now().toJsonString(checkScheduleStart, MO_JSONDATE_SIZE); REQUIRE(!strcmp(payload["scheduleStart"], checkScheduleStart)); JsonObject chargingScheduleJson = payload["chargingSchedule"]; @@ -548,7 +572,7 @@ TEST_CASE( "SmartCharging" ) { loop(); bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -566,7 +590,7 @@ TEST_CASE( "SmartCharging" ) { REQUIRE( checkProcessed ); checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -585,7 +609,7 @@ TEST_CASE( "SmartCharging" ) { REQUIRE( checkProcessed ); checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -608,7 +632,7 @@ TEST_CASE( "SmartCharging" ) { // replace existing profile - OK checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -630,7 +654,7 @@ TEST_CASE( "SmartCharging" ) { // try to install additional profile - not okay checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -654,7 +678,7 @@ TEST_CASE( "SmartCharging" ) { loop(); bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -673,7 +697,7 @@ TEST_CASE( "SmartCharging" ) { REQUIRE( checkProcessed ); checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -699,7 +723,7 @@ TEST_CASE( "SmartCharging" ) { loop(); bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -724,7 +748,7 @@ TEST_CASE( "SmartCharging" ) { REQUIRE( checkProcessed ); checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -760,7 +784,7 @@ TEST_CASE( "SmartCharging" ) { setSmartChargingPowerOutput([] (float) { }); bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -778,7 +802,7 @@ TEST_CASE( "SmartCharging" ) { REQUIRE( checkProcessed ); checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -800,7 +824,7 @@ TEST_CASE( "SmartCharging" ) { setSmartChargingCurrentOutput([] (float) { }); checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -818,7 +842,7 @@ TEST_CASE( "SmartCharging" ) { REQUIRE( checkProcessed ); checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "SetChargingProfile", [] () { //create req @@ -836,10 +860,10 @@ TEST_CASE( "SmartCharging" ) { REQUIRE( checkProcessed ); } - scService->clearChargingProfile([] (int, int, ChargingProfilePurposeType, int) { - return true; - }); + #endif + + scService->clearChargingProfile(-1, -1, MicroOcpp::ChargingProfilePurposeType::UNDEFINED, -1); - mocpp_deinitialize(); + mo_deinitialize(); } diff --git a/tests/TransactionSafety.cpp b/tests/TransactionSafety.cpp index f3593e01..4109b08b 100644 --- a/tests/TransactionSafety.cpp +++ b/tests/TransactionSafety.cpp @@ -4,11 +4,8 @@ #include #include -#include #include -#include -#include -#include +#include #include #include #include "./helpers/testHelper.h" @@ -19,81 +16,79 @@ using namespace MicroOcpp; TEST_CASE( "Transaction safety" ) { printf("\nRun %s\n", "Transaction safety"); - //initialize Context with dummy socket + //initialize Context without any configs + mo_initialize(); + + mo_getContext()->setTicksCb(custom_timer_cb); + LoopbackConnection loopback; - mocpp_initialize(loopback); + mo_getContext()->setConnection(&loopback); - mocpp_set_timer(custom_timer_cb); + auto ocppVersion = GENERATE(MO_OCPP_V16, MO_OCPP_V201); + mo_setOcppVersion(ocppVersion); - declareConfiguration("ConnectionTimeOut", 30)->setInt(30); + mo_setup(); - SECTION("Basic transaction") { - MO_DBG_DEBUG("Basic transaction"); - loop(); - startTransaction("mIdTag"); - loop(); - REQUIRE(ocppPermitsCharge()); - stopTransaction(); - loop(); - REQUIRE(!ocppPermitsCharge()); + mo_setVarConfigInt(mo_getApiContext(), "TxCtrlr", "EVConnectionTimeOut", "ConnectionTimeOut", 30); + + if (ocppVersion == MO_OCPP_V201) { + mo_setVarConfigString(mo_getApiContext(), "TxCtrlr", "TxStartPoint", NULL, "PowerPathClosed"); + mo_setVarConfigString(mo_getApiContext(), "TxCtrlr", "TxStopPoint", NULL, "PowerPathClosed"); + } - mocpp_deinitialize(); + SECTION("Clean up files") { + auto filesystem = mo_getFilesystem(); + FilesystemUtils::removeByPrefix(filesystem, ""); } SECTION("Managed transaction") { MO_DBG_DEBUG("Managed transaction"); loop(); - setConnectorPluggedInput([] () {return true;}); - beginTransaction("mIdTag"); + mo_setConnectorPluggedInput([] () {return true;}); + mo_beginTransaction("mIdTag"); loop(); - REQUIRE(ocppPermitsCharge()); - endTransaction(); + REQUIRE(mo_ocppPermitsCharge()); + mo_endTransaction("mIdTag", NULL); loop(); - REQUIRE(!ocppPermitsCharge()); - - mocpp_deinitialize(); + REQUIRE(!mo_ocppPermitsCharge()); } SECTION("Reset during transaction 01 - interrupt initiation") { MO_DBG_DEBUG("Reset during transaction 01 - interrupt initiation"); - setConnectorPluggedInput([] () {return false;}); + mo_setConnectorPluggedInput([] () {return false;}); loop(); - beginTransaction("mIdTag"); + mo_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;}); + mo_setConnectorPluggedInput([] () {return false;}); loop(); - REQUIRE(!ocppPermitsCharge()); - mocpp_deinitialize(); + REQUIRE(!mo_ocppPermitsCharge()); } SECTION("Reset during transaction 03 - interrupt running tx") { MO_DBG_DEBUG("Reset during transaction 03 - interrupt running tx"); - setConnectorPluggedInput([] () {return true;}); + mo_setConnectorPluggedInput([] () {return true;}); loop(); - REQUIRE(ocppPermitsCharge()); - mocpp_deinitialize(); + REQUIRE(mo_ocppPermitsCharge()); } SECTION("Reset during transaction 04 - interrupt stopping tx") { MO_DBG_DEBUG("Reset during transaction 04 - interrupt stopping tx"); - setConnectorPluggedInput([] () {return true;}); + mo_setConnectorPluggedInput([] () {return true;}); loop(); - REQUIRE(ocppPermitsCharge()); - endTransaction(); - mocpp_deinitialize(); + REQUIRE(mo_ocppPermitsCharge()); + mo_endTransaction("mIdTag", NULL); } SECTION("Reset during transaction 06 - check tx finished") { MO_DBG_DEBUG("Reset during transaction 06 - check tx finished"); - setConnectorPluggedInput([] () {return true;}); + mo_setConnectorPluggedInput([] () {return true;}); loop(); - REQUIRE(!ocppPermitsCharge()); - mocpp_deinitialize(); + REQUIRE(!mo_ocppPermitsCharge()); } + mo_deinitialize(); } diff --git a/tests/Transactions.cpp b/tests/Transactions.cpp index 56d50c97..9716e784 100644 --- a/tests/Transactions.cpp +++ b/tests/Transactions.cpp @@ -38,8 +38,8 @@ TEST_CASE( "Transactions" ) { mocpp_set_timer(custom_timer_cb); - getOcppContext()->getOperationRegistry().registerOperation("Authorize", [] () { - return new Ocpp16::CustomOperation("Authorize", + getOcppContext()->getMessageService().registerOperation("Authorize", [] () { + return new v16::CustomOperation("Authorize", [] (JsonObject) {}, //ignore req [] () { //create conf @@ -49,8 +49,8 @@ TEST_CASE( "Transactions" ) { return doc; });}); - getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [] () { - return new Ocpp16::CustomOperation("TransactionEvent", + getOcppContext()->getMessageService().registerOperation("TransactionEvent", [] () { + return new v16::CustomOperation("TransactionEvent", [] (JsonObject) {}, //ignore req [] () { //create conf @@ -292,8 +292,8 @@ TEST_CASE( "Transactions" ) { bool checkReceivedStarted = false, checkReceivedEnded = false; - getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded] () { - return new Ocpp16::CustomOperation("TransactionEvent", + getOcppContext()->getMessageService().registerOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded] () { + return new v16::CustomOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded] (JsonObject request) { //process req const char *eventType = request["eventType"] | (const char*)nullptr; @@ -343,8 +343,8 @@ TEST_CASE( "Transactions" ) { bool checkReceivedStarted = false, checkReceivedEnded = false; size_t checkSeqNosSize = 0; - getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded, &checkSeqNosSize] () { - return new Ocpp16::CustomOperation("TransactionEvent", + getOcppContext()->getMessageService().registerOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded, &checkSeqNosSize] () { + return new v16::CustomOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded, &checkSeqNosSize] (JsonObject request) { //process req const char *eventType = request["eventType"] | (const char*)nullptr; @@ -416,8 +416,8 @@ TEST_CASE( "Transactions" ) { std::map> txEventRequests; - getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&txEventRequests] () { - return new Ocpp16::CustomOperation("TransactionEvent", + getOcppContext()->getMessageService().registerOperation("TransactionEvent", [&txEventRequests] () { + return new v16::CustomOperation("TransactionEvent", [&txEventRequests] (JsonObject request) { //process req const char *eventType = request["eventType"] | (const char*)nullptr; @@ -521,8 +521,8 @@ TEST_CASE( "Transactions" ) { bool checkProcessed = false; - getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkProcessed, txId] () { - return new Ocpp16::CustomOperation("TransactionEvent", + getOcppContext()->getMessageService().registerOperation("TransactionEvent", [&checkProcessed, txId] () { + return new v16::CustomOperation("TransactionEvent", [&checkProcessed, txId] (JsonObject request) { //process req const char *eventType = request["eventType"] | (const char*)nullptr; @@ -607,8 +607,8 @@ TEST_CASE( "Transactions" ) { bool checkReceivedStarted = false, checkReceivedEnded = false; - getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded, txId, &checkSeqNosSize] () { - return new Ocpp16::CustomOperation("TransactionEvent", + getOcppContext()->getMessageService().registerOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded, txId, &checkSeqNosSize] () { + return new v16::CustomOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded, txId, &checkSeqNosSize] (JsonObject request) { //process req const char *eventType = request["eventType"] | (const char*)nullptr; @@ -698,8 +698,8 @@ TEST_CASE( "Transactions" ) { 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", + getOcppContext()->getMessageService().registerOperation("TransactionEvent", [&txEventRequests] () { + return new v16::CustomOperation("TransactionEvent", [&txEventRequests] (JsonObject request) { //process req const char *eventType = request["eventType"] | (const char*)nullptr; @@ -734,4 +734,4 @@ TEST_CASE( "Transactions" ) { mocpp_deinitialize(); } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/tests/Variables.cpp b/tests/Variables.cpp index 158140fc..96885d58 100644 --- a/tests/Variables.cpp +++ b/tests/Variables.cpp @@ -252,7 +252,7 @@ TEST_CASE( "Variable" ) { bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "GetVariables", [] () { //create req @@ -289,7 +289,7 @@ TEST_CASE( "Variable" ) { checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "GetVariable", [] () { //create req @@ -343,7 +343,7 @@ TEST_CASE( "Variable" ) { //update existing config bool checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ChangeVariable", [] () { //create req @@ -365,7 +365,7 @@ TEST_CASE( "Variable" ) { //try to update not existing key checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ChangeVariable", [] () { //create req @@ -386,7 +386,7 @@ TEST_CASE( "Variable" ) { //try to update config with malformatted value checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ChangeVariable", [] () { //create req @@ -413,7 +413,7 @@ TEST_CASE( "Variable" ) { //validation success checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ChangeVariable", [] () { //create req @@ -435,7 +435,7 @@ TEST_CASE( "Variable" ) { //validation failure checkProcessed = false; - getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + getOcppContext()->initiateRequest(makeRequest(new v16::CustomOperation( "ChangeVariable", [] () { //create req @@ -504,7 +504,7 @@ TEST_CASE( "Variable" ) { bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest( - new Ocpp16::CustomOperation("GetVariables", + new v16::CustomOperation("GetVariables", [] () { //create req auto doc = makeJsonDoc("UnitTests", @@ -553,7 +553,7 @@ TEST_CASE( "Variable" ) { bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest( - new Ocpp16::CustomOperation("SetVariables", + new v16::CustomOperation("SetVariables", [] () { //create req auto doc = makeJsonDoc("UnitTests", @@ -601,9 +601,9 @@ TEST_CASE( "Variable" ) { bool checkProcessedNotification = false; Timestamp checkTimestamp; - getOcppContext()->getOperationRegistry().registerOperation("NotifyReport", + getOcppContext()->getMessageService().registerOperation("NotifyReport", [&checkProcessedNotification, &checkTimestamp] () { - return new Ocpp16::CustomOperation("NotifyReport", + return new v16::CustomOperation("NotifyReport", [ &checkProcessedNotification, &checkTimestamp] (JsonObject payload) { //process req checkProcessedNotification = true; @@ -629,7 +629,7 @@ TEST_CASE( "Variable" ) { bool checkProcessed = false; getOcppContext()->initiateRequest(makeRequest( - new Ocpp16::CustomOperation("GetBaseReport", + new v16::CustomOperation("GetBaseReport", [] () { //create req auto doc = makeJsonDoc("UnitTests", @@ -658,4 +658,4 @@ TEST_CASE( "Variable" ) { mocpp_deinitialize(); } -#endif // MO_ENABLE_V201 +#endif //MO_ENABLE_V201 diff --git a/tests/benchmarks/firmware_size/main.cpp b/tests/benchmarks/firmware_size/main.cpp index 6e849b8d..66acbbf2 100644 --- a/tests/benchmarks/firmware_size/main.cpp +++ b/tests/benchmarks/firmware_size/main.cpp @@ -1,68 +1,135 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2024 +// Copyright Matthias Akstaller 2019 - 2025 // MIT License #include -#include -#include -#include -MicroOcpp::LoopbackConnection g_loopback; +/* + * Dummy invokations of all functions in MicroOcpp.h to avoid link-time optimization. This + * file is only useful for firmware size analysis. The function invokations should not be + * taken as an example. + */ void setup() { - ocpp_deinitialize(); + // Basic lifecycle + mo_deinitialize(); + mo_initialize(); + mo_setup(); + mo_isInitialized(); + MO_Context *ctx = mo_getApiContext(); +#if MO_USE_FILEAPI != MO_CUSTOM_FS + mo_setFilesystemConfig(MO_FS_OPT_DISABLE); + mo_setFilesystemConfig2(ctx, MO_FS_OPT_DISABLE, "/tmp"); +#endif + +#if MO_WS_USE == MO_WS_ARDUINO + mo_setWebsocketUrl("wss://example.com", "chargeBoxId", "authKey", nullptr); +#endif + + // Connection + mo_setConnection(nullptr); + mo_setConnection2(ctx, nullptr); + + // OCPP version + mo_setOcppVersion(MO_OCPP_V16); + mo_setOcppVersion2(ctx, MO_OCPP_V16); + + // BootNotification + mo_setBootNotificationData("Model", "Vendor"); + MO_BootNotificationData bnData; + mo_bootNotificationData_init(&bnData); + mo_setBootNotificationData2(ctx, bnData); + + // Inputs/Outputs + mo_setConnectorPluggedInput([] () {return false;}); + mo_setConnectorPluggedInput2(ctx, 1, [] (unsigned int, void*) {return false;}, nullptr); + mo_setEnergyMeterInput([] () {return 0;}); + mo_setEnergyMeterInput2(ctx, 1, [] (MO_ReadingContext, unsigned int, void*) {return 0;}, nullptr); + mo_setPowerMeterInput([] () {return 0.f;}); + mo_setPowerMeterInput2(ctx, 1, [] (MO_ReadingContext, unsigned int, void*) {return 0.f;}, nullptr); +#if MO_ENABLE_SMARTCHARGING + mo_setSmartChargingPowerOutput([] (float) {}); + mo_setSmartChargingCurrentOutput([] (float) {}); + mo_setSmartChargingOutput(ctx, 1, [] (MO_ChargeRate, unsigned int, void*) {}, true, true, true, true, nullptr); +#endif + mo_setEvReadyInput([] () {return false;}); + mo_setEvReadyInput2(ctx, 1, [] (unsigned int, void*) {return false;}, nullptr); + mo_setEvseReadyInput([] () {return false;}); + mo_setEvseReadyInput2(ctx, 1, [] (unsigned int, void*) {return false;}, nullptr); + mo_addErrorCodeInput([] () {return (const char*)nullptr;}); +#if MO_ENABLE_V16 + mo_v16_addErrorDataInput(ctx, 1, [] (unsigned int, void*) {MO_ErrorData e; mo_errorData_init(&e); return e;}, nullptr); +#endif +#if MO_ENABLE_V211 + mo_v201_addFaultedInput(ctx, 1, [] (unsigned int, void*) {return false;}, nullptr); +#endif + mo_addMeterValueInputInt([] () {return 0;}, nullptr, nullptr, nullptr, nullptr); + mo_addMeterValueInputInt2(ctx, 1, [] (MO_ReadingContext, unsigned int, void*) {return 0;}, nullptr, nullptr, nullptr, nullptr, nullptr); + mo_addMeterValueInputFloat([] () {return 0.f;}, nullptr, nullptr, nullptr, nullptr); + mo_addMeterValueInputFloat2(ctx, 1, [] (MO_ReadingContext, unsigned int, void*) {return 0.f;}, nullptr, nullptr, nullptr, nullptr, nullptr); + mo_addMeterValueInputString([] (char *buf, size_t size) {if (size > 0) {buf[0] = '\0'; return 0;} return 0;}, nullptr, nullptr, nullptr, nullptr); + + // Transaction management + mo_beginTransaction(""); + mo_beginTransaction2(ctx, 1, ""); + mo_beginTransaction_authorized("", ""); + mo_beginTransaction_authorized2(ctx, 1, "", ""); +#if MO_ENABLE_V201 + mo_authorizeTransaction(""); + mo_authorizeTransaction2(ctx, 1, "", MO_IdTokenType_ISO14443); + mo_authorizeTransaction3(ctx, 1, "", MO_IdTokenType_ISO14443, false, ""); +#endif + mo_endTransaction(nullptr, nullptr); + mo_endTransaction2(ctx, 1, nullptr, nullptr); + mo_endTransaction_authorized(nullptr, nullptr); + mo_endTransaction_authorized2(ctx, 1, nullptr, nullptr); #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()); + mo_deauthorizeTransaction(nullptr); + mo_deauthorizeTransaction2(ctx, 1, nullptr, MO_IdTokenType_ISO14443); + mo_deauthorizeTransaction3(ctx, 1, nullptr, MO_IdTokenType_ISO14443, false, nullptr); + mo_abortTransaction(MO_TxStoppedReason_Other, MO_TxEventTriggerReason_AbnormalCondition); + mo_abortTransaction2(ctx, 1, MO_TxStoppedReason_Other, MO_TxEventTriggerReason_AbnormalCondition); #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;}); + // Status and info + mo_ocppPermitsCharge(); + mo_ocppPermitsCharge2(ctx, 1); + mo_isTransactionActive(); + mo_isTransactionActive2(ctx, 1); + mo_isTransactionRunning(); + mo_isTransactionRunning2(ctx, 1); + mo_getTransactionIdTag(); + mo_getTransactionIdTag2(ctx, 1); +#if MO_ENABLE_V201 + mo_getTransactionId(); + mo_getTransactionId2(ctx, 1); #endif +#if MO_ENABLE_V16 + mo_v16_getTransactionId(); + mo_v16_getTransactionId2(ctx, 1); +#endif + mo_getChargePointStatus(); + mo_getChargePointStatus2(ctx, 1); - isOperative(); - setOnResetNotify([] (bool) {return false;}); - setOnResetExecute([] (bool) {return false;}); - getFirmwareService()->getFirmwareStatus(); - getDiagnosticsService()->getDiagnosticsStatus(); + mo_setOnUnlockConnector([] () {return MO_UnlockConnectorResult_UnlockFailed;}); -#if MO_ENABLE_CERT_MGMT - setCertificateStore(nullptr); + mo_isOperative(); +#if MO_ENABLE_V16 + mo_v16_setOnResetNotify([] (bool) {return false;}); #endif + mo_setOnResetExecute([] () {}); + + mo_sendRequest(ctx, "", "", [] (const char*,void*) {}, [] (void*) {}, nullptr); + mo_setRequestHandler(ctx, "", [] (const char*,const char*,void**,void*) {}, [] (const char*,char*,size_t,void*,void*) {return 0;}, [] (const char*,void*,void*) {}, nullptr); + mo_setOnReceiveRequest(ctx, "", [] (const char*,const char*,void*) {}, nullptr); + mo_setOnSendConf(ctx, "", [] (const char*, const char*, void*) {}, nullptr); - getOcppContext(); + mo_getContext(); } void loop() { - mocpp_loop(); + mo_loop(); } diff --git a/tests/benchmarks/firmware_size/platformio.ini b/tests/benchmarks/firmware_size/platformio.ini index 121a8de5..146aade1 100644 --- a/tests/benchmarks/firmware_size/platformio.ini +++ b/tests/benchmarks/firmware_size/platformio.ini @@ -9,8 +9,11 @@ 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 + -D MO_DBG_LEVEL=MO_DL_WARN ; don't take debug messages into account + -D MO_WS_USE=MO_WS_CUSTOM + -D MO_ENABLE_MBEDTLS=1 + -D MO_ENABLE_CERT_MGMT=1 + -D MO_ENABLE_SECURITY_EVENT=1 [env:v16] platform = ${common.platform} @@ -19,8 +22,8 @@ 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_V16=1 + -D MO_ENABLE_V201=0 -D MO_ENABLE_RESERVATION=1 -D MO_ENABLE_LOCAL_AUTH=1 -D MO_REPORT_NOERROR=1 @@ -33,6 +36,19 @@ framework = ${common.framework} lib_deps = ${common.lib_deps} build_flags = ${common.build_flags} + -D MO_ENABLE_V16=0 -D MO_ENABLE_V201=1 - -D MO_ENABLE_MBEDTLS=1 - -D MO_ENABLE_CERT_MGMT=1 + +[env:v16_v201] +platform = ${common.platform} +board = ${common.board} +framework = ${common.framework} +lib_deps = ${common.lib_deps} +build_flags = + ${common.build_flags} + -D MO_ENABLE_V16=1 + -D MO_ENABLE_V201=1 + -D MO_ENABLE_RESERVATION=1 + -D MO_ENABLE_LOCAL_AUTH=1 + -D MO_REPORT_NOERROR=1 + -D MO_ENABLE_CONNECTOR_LOCK=1 diff --git a/tests/benchmarks/scripts/eval_firmware_size.py b/tests/benchmarks/scripts/eval_firmware_size.py index a952f7f8..c9f62683 100755 --- a/tests/benchmarks/scripts/eval_firmware_size.py +++ b/tests/benchmarks/scripts/eval_firmware_size.py @@ -4,332 +4,163 @@ # load data -COLUMN_BINSIZE = 'Binary size (Bytes)' +COLUMN_CUNITS = 'Compile Unit' +COLUMN_SIZE_V16 = 'OCPP 1.6 (Bytes)' +COLUMN_SIZE_V201 = 'OCPP 2.0.1 (Bytes)' +COLUMN_SIZE_V16_V201 = 'Both enabled (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 = pd.read_csv(fn, index_col="compileunits").filter(like="src/MicroOcpp", axis=0).filter(['Module','vmsize'], axis=1).sort_index() + df.index.names = [COLUMN_CUNITS] 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("src/MicroOcpp/"):] if s.startswith("src/MicroOcpp/") else s) + df.index = df.index.map(lambda s: s[len("src/"):] if s.startswith("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_v16.rename(columns={'vmsize': COLUMN_SIZE_V16}, inplace=True) cunits_v201 = load_compilation_units('docs/assets/tables/bloaty_v201.csv') +cunits_v201.rename(columns={'vmsize': COLUMN_SIZE_V201}, inplace=True) +cunits_v16_v201 = load_compilation_units('docs/assets/tables/bloaty_v16_v201.csv') +cunits_v16_v201.rename(columns={'vmsize': COLUMN_SIZE_V16_V201}, inplace=True) + +cunits = pd.merge(cunits_v16, cunits_v201, on=COLUMN_CUNITS, how='outer') +cunits = pd.merge(cunits, cunits_v16_v201, on=COLUMN_CUNITS, how='outer') # 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_MO = ' General library functions' MODULE_SECURITY = 'A - Security' - MODULE_PROVISIONING = 'B - Provisioning' - MODULE_PROVISIONING_VARS = 'B - Provisioning - Variables' + MODULE_PROVISIONING_CONF = 'B - Provisioning - Configuration (v16)' + MODULE_PROVISIONING_VARS = 'B - Provisioning - Variables (v201)' + MODULE_PROVISIONING = 'B - Provisioning - Other' MODULE_AUTHORIZATION = 'C - Authorization' - MODULE_LOCALAUTH = 'D - Local Authorization List Management' + MODULE_LOCALAUTH = 'D - LocalAuthorizationListManagement' 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 + MODULE_FW_MNGT = 'L - FirmwareManagement' + MODULE_CERTS = 'M - CertificateManagement' + MODULE_DIAG = 'N - Diagnostics' + MODULE_DATATRANSFER = 'P - DataTransfer' + + df.at['Context.cpp', 'Module'] = MODULE_MO + df.at['Core/Connection.cpp', 'Module'] = MODULE_MO + df.at['Core/FilesystemAdapter.cpp', 'Module'] = MODULE_MO + df.at['Core/FilesystemUtils.cpp', 'Module'] = MODULE_MO + df.at['Core/FtpMbedTLS.cpp', 'Module'] = MODULE_MO + df.at['Core/Memory.cpp', 'Module'] = MODULE_MO + df.at['Core/MessageService.cpp', 'Module'] = MODULE_MO + df.at['Core/Operation.cpp', 'Module'] = MODULE_MO + df.at['Core/PersistencyUtils.cpp', 'Module'] = MODULE_MO + df.at['Core/Request.cpp', 'Module'] = MODULE_MO + df.at['Core/RequestQueue.cpp', 'Module'] = MODULE_MO + df.at['Core/Time.cpp', 'Module'] = MODULE_MO + df.at['Core/UuidUtils.cpp', 'Module'] = MODULE_MO + df.at['Debug.cpp', 'Module'] = MODULE_MO + df.at['MicroOcpp.cpp', 'Module'] = MODULE_MO 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/Authorization/IdToken.cpp', 'Module'] = MODULE_AUTHORIZATION + df.at['Model/Availability/AvailabilityDefs.cpp', 'Module'] = MODULE_AVAILABILITY + df.at['Model/Availability/AvailabilityService.cpp', 'Module'] = MODULE_AVAILABILITY 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/Configuration/Configuration.cpp', 'Module'] = MODULE_PROVISIONING_CONF + df.at['Model/Configuration/ConfigurationContainer.cpp', 'Module'] = MODULE_PROVISIONING_CONF + df.at['Model/Configuration/ConfigurationService.cpp', 'Module'] = MODULE_PROVISIONING_CONF + df.at['Model/Diagnostics/Diagnostics.cpp', 'Module'] = MODULE_DIAG + df.at['Model/Diagnostics/DiagnosticsService.cpp', 'Module'] = MODULE_DIAG 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/Metering/ReadingContext.cpp', 'Module'] = MODULE_METERVALUES + df.at['Model/Model.cpp', 'Module'] = MODULE_MO + df.at['Model/RemoteControl/RemoteControlService.cpp', 'Module'] = MODULE_REMOTECONTROL 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/SecurityEvent/SecurityEventService.cpp', 'Module'] = MODULE_SECURITY 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/TransactionService16.cpp', 'Module'] = MODULE_TX + df.at['Model/Transactions/TransactionService201.cpp', 'Module'] = MODULE_TX 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['Model/Variables/Variable.cpp', 'Module'] = MODULE_PROVISIONING_VARS + df.at['Model/Variables/VariableContainer.cpp', 'Module'] = MODULE_PROVISIONING_VARS + df.at['Model/Variables/VariableService.cpp', 'Module'] = MODULE_PROVISIONING_VARS 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/ChangeConfiguration.cpp', 'Module'] = MODULE_PROVISIONING_CONF + df.at['Operations/ClearCache.cpp', 'Module'] = MODULE_AUTHORIZATION 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/CustomOperation.cpp', 'Module'] = MODULE_MO + df.at['Operations/DataTransfer.cpp', 'Module'] = MODULE_DATATRANSFER 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/DiagnosticsStatusNotification.cpp', 'Module'] = MODULE_DIAG 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/GetBaseReport.cpp', 'Module'] = MODULE_PROVISIONING_VARS 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/GetConfiguration.cpp', 'Module'] = MODULE_PROVISIONING_CONF + df.at['Operations/GetDiagnostics.cpp', 'Module'] = MODULE_DIAG + df.at['Operations/GetInstalledCertificateIds.cpp', 'Module'] = MODULE_CERTS 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/GetLog.cpp', 'Module'] = MODULE_DIAG + df.at['Operations/GetVariables.cpp', 'Module'] = MODULE_PROVISIONING_VARS 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/LogStatusNotification.cpp', 'Module'] = MODULE_DIAG 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/NotifyReport.cpp', 'Module'] = MODULE_PROVISIONING_VARS 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/RequestStartTransaction.cpp', 'Module'] = MODULE_TX + df.at['Operations/RequestStopTransaction.cpp', 'Module'] = MODULE_TX 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/SecurityEventNotification.cpp', 'Module'] = MODULE_SECURITY 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/SetVariables.cpp', 'Module'] = MODULE_PROVISIONING_VARS 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/TransactionEvent.cpp', 'Module'] = MODULE_TX + df.at['Operations/TriggerMessage.cpp', 'Module'] = MODULE_REMOTECONTROL + df.at['Operations/UnlockConnector.cpp', 'Module'] = MODULE_REMOTECONTROL 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 + df.at['Platform.cpp', 'Module'] = MODULE_MO print(df) -categorize_table(cunits_v16) -categorize_table(cunits_v201) +categorize_table(cunits) 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()]) +if (cunits[[COLUMN_SIZE_V16, COLUMN_SIZE_V201, COLUMN_SIZE_V16_V201]].isna().all(axis=1)).any(): + print('Error: categorized non-existing compile units:\n') + print(cunits.loc[cunits[[COLUMN_SIZE_V16, COLUMN_SIZE_V201, COLUMN_SIZE_V16_V201]].isna().all(axis=1)]) 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: +if (cunits['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 == '']) + print(cunits.loc[cunits['Module'].values == '']) categorize_success = False if not categorize_success: @@ -337,28 +168,13 @@ def categorize_table(df): # 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") +cunits.to_csv("docs/assets/tables/compile_units.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()] +modules = cunits[['Module', COLUMN_SIZE_V16, COLUMN_SIZE_V201, COLUMN_SIZE_V16_V201]].groupby('Module').sum() +modules.loc['**Total**'] = [modules[COLUMN_SIZE_V16].sum(), modules[COLUMN_SIZE_V201].sum(), modules[COLUMN_SIZE_V16_V201].sum()] -print(modules_v201_by_module) +print(modules) -modules_v201_by_module.to_csv('docs/assets/tables/modules_v201.csv') +modules.to_csv('docs/assets/tables/modules.csv') diff --git a/tests/benchmarks/scripts/measure_heap.py b/tests/benchmarks/scripts/measure_heap.py index 0b050544..9cf19518 100644 --- a/tests/benchmarks/scripts/measure_heap.py +++ b/tests/benchmarks/scripts/measure_heap.py @@ -8,12 +8,17 @@ import json import time import pandas as pd +import subprocess +import threading 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_01_CS', + 'TC_B_02_CS', + 'TC_B_03_CS', 'TC_B_06_CS', 'TC_B_07_CS', 'TC_B_09_CS', @@ -96,6 +101,9 @@ 'TC_J_08_CS', 'TC_J_09_CS', 'TC_J_10_CS', + 'TC_N_25_CS', + 'TC_N_26_CS', + 'TC_N_35_CS', ] # Result data set @@ -105,105 +113,138 @@ max_memory_total = 0 min_memory_base = 1000 * 1000 * 1000 -def connect_ssh(): +watchdog_timer = 0 +exit_watchdog = False +watchdog_triggered = False - 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') +# Ensure Simulator is still running despite Reset requests via OCPP or the rmt_ctrl interface +run_simulator = False +exit_run_simulator_process = False - 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 run_simulator_process(): -def close_ssh(client: paramiko.SSHClient): + global run_simulator + global exit_run_simulator_process + global watchdog_triggered - client.close() + track_run_simulator = run_simulator + iterations_since_last_check = 0 -def deploy_simulator(): + while not exit_run_simulator_process: - print('Deploy Simulator') + time.sleep(0.001) + iterations_since_last_check += 1 - client = connect_ssh() + if track_run_simulator == run_simulator and iterations_since_last_check <= 1000: + continue - 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') + iterations_since_last_check = 0 + + if not track_run_simulator and run_simulator and not watchdog_triggered: + print('Starting simulator') + os.system(os.path.join('MicroOcppSimulator', 'build', 'mo_simulator') + ' &') + track_run_simulator = True + + if track_run_simulator and not run_simulator: + print('Shutdown simulator') + os.system('killall -s SIGINT mo_simulator') + track_run_simulator = False + + if track_run_simulator == run_simulator: + # Check if mo_simulator process can be found + try: + subprocess.check_output(['pgrep', '-f', 'mo_simulator']) + # Found, still running + track_run_simulator = True + except subprocess.CalledProcessError: + # Exited, restart + track_run_simulator = False + + print('Stopped run_simulator_process') def cleanup_simulator(): + global run_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') + run_simulator = False + time.sleep(1) - print(' - clean deployment') - stdin, stdout, stderr = client.exec_command('rm -rf ' + os.path.join('MicroOcppSimulator')) + print(' - clean state') + os.system('rm -rf ' + os.path.join('mo_store')) + os.system('mkdir ' + os.path.join('mo_store')) - close_ssh(client) print(' - done') def setup_simulator(): + global run_simulator - print('Setup Simulator') - - client = connect_ssh() + cleanup_simulator() - print(' - stop Simulator, if still running') - stdin, stdout, stderr = client.exec_command('killall -s SIGINT mo_simulator') + print('Setup Simulator') - print(' - clean state') - stdin, stdout, stderr = client.exec_command('rm -rf ' + os.path.join('MicroOcppSimulator', 'mo_store', '*')) + print(' - set credentials') - 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() + with open(os.path.join('mo_store', 'simulator.jsn'), 'w') as f: + f.write(os.environ['MO_SIM_CONFIG']) + with open(os.path.join('mo_store', 'ws-conn-v201.jsn'), 'w') as f: + f.write(os.environ['MO_SIM_OCPP_SERVER']) + with open(os.path.join('mo_store', 'rmt_ctrl.jsn'), 'w') as f: + f.write(os.environ['MO_SIM_RMT_CTRL_CONFIG']) + with open(os.path.join('mo_store', 'rmt_ctrl.pem'), 'w') as f: + f.write(os.environ['MO_SIM_RMT_CTRL_CERT']) 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) + run_simulator = True + time.sleep(1) print(' - done') +def cleanup_test_driver(): + try: + print("Stop Test Driver") + response = requests.post(os.environ['TEST_DRIVER_URL'] + '/session/stop', + headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}) + print(f'Test Driver /session/stop:\n > {response.status_code}') + #print(json.dumps(response.json(), indent=4)) + except: + print('Error stopping Test Driver') + traceback.print_exc() + +def watchdog_thread(): + + global watchdog_timer + global exit_watchdog + global watchdog_triggered + + while not exit_watchdog and watchdog_timer < 120: + watchdog_timer += 1 + time.sleep(1) + + if exit_watchdog: + # watchdog has been exited gracefully + return + + watchdog_triggered = True + cleanup_test_driver() + cleanup_simulator() + print("\nTest execution timeout - terminate") + os._exit(1) + def run_measurements(): global max_memory_total global min_memory_base + global run_simulator + global watchdog_timer + global watchdog_triggered 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) + headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}) #print(json.dumps(response.json(), indent=4)) @@ -227,19 +268,14 @@ def run_measurements(): print(i['header'] + ' --- ' + j['functional_block'] + ' --- ' + j['description']) testcases.append(j) - deploy_simulator() - print('Get Simulator base memory data') setup_simulator() + time.sleep(1) - 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'])) + response = requests.post('http://localhost:8000/api/memory/reset') 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'])) + response = requests.get('http://localhost:8000/api/memory/info') 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"]) @@ -247,8 +283,7 @@ def run_measurements(): 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) + headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}) print(f'Test Driver /*/*/session/start/*:\n > {response.status_code}') #print(json.dumps(response.json(), indent=4)) @@ -259,6 +294,11 @@ def run_measurements(): print('Test case already executed - skip') continue + if watchdog_triggered: + break + + watchdog_timer = 0 + setup_simulator() time.sleep(1) @@ -266,11 +306,10 @@ def run_measurements(): 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) + headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}) print(f'Test Driver /sut_connection_status:\n > {response.status_code}') #print(json.dumps(response.json(), indent=4)) - if response.status_code == 200: + if response.status_code == 200 and response.json()['isConnected']: simulator_connected = True break else: @@ -281,23 +320,18 @@ def run_measurements(): 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'])) + response = requests.post('http://localhost:8000/api/memory/reset') 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) + headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}) 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'])) + sim_response = requests.get('http://localhost:8000/api/memory/info') 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"]))] @@ -305,14 +339,11 @@ def run_measurements(): 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)) + #if False and test_response.json()['data'][0]['verdict'] != "pass": + # print('Test failure, abort') + # break + cleanup_test_driver() cleanup_simulator() print('Store test results') @@ -346,46 +377,66 @@ def run_measurements(): print('Stored test results to CSV') +run_measurements_success = False + 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): + global exit_run_simulator_process + global exit_watchdog + global run_measurements_success + + 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_RMT_CTRL_CONFIG' not in os.environ or + 'MO_SIM_RMT_CTRL_CERT' not in os.environ): sys.exit('\nCould not read environment variables') + m_watchdog_thread = threading.Thread(target=watchdog_thread) + m_watchdog_thread.start() + n_tries = 3 for i in range(n_tries): + run_simulator_thread = None + try: + # Keepalive Simulator while running the test cases (tests may triger Reset commands) + + exit_run_simulator_process = False + run_simulator_thread = threading.Thread(target=run_simulator_process, daemon=True) + run_simulator_thread.start() + run_measurements() print('\n **Test cases executed successfully**') + run_measurements_success = True 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_test_driver() + cleanup_simulator() - cleanup_simulator() + exit_run_simulator_process = True + run_simulator_thread.join() - if i + 1 < n_tries: - print('Retry test cases') - else: - print('\n **Test case execution aborted**') - sys.exit('\nError running test cases') + if i + 1 < n_tries: + print('Retry test cases') + else: + print('\n **Test case execution aborted**') + run_measurements_success = False + + exit_watchdog = True # terminate watchdog thread + m_watchdog_thread.join() run_measurements_and_retry() + +if run_measurements_success: + sys.exit(0) +else: + sys.exit(1) diff --git a/tests/helpers/testHelper.cpp b/tests/helpers/testHelper.cpp index ffc6fac5..f386ab98 100644 --- a/tests/helpers/testHelper.cpp +++ b/tests/helpers/testHelper.cpp @@ -10,15 +10,15 @@ using namespace MicroOcpp; -unsigned long mtime = 10000; -unsigned long custom_timer_cb() { +uint32_t mtime = 10000; +uint32_t custom_timer_cb() { return mtime; } void loop() { for (int i = 0; i < 30; i++) { mtime += 100; - mocpp_loop(); + mo_loop(); } } diff --git a/tests/helpers/testHelper.h b/tests/helpers/testHelper.h index 04e35d57..13d3ec52 100644 --- a/tests/helpers/testHelper.h +++ b/tests/helpers/testHelper.h @@ -7,8 +7,8 @@ #define UNIT_MEM_TAG "UnitTests" -extern unsigned long mtime; -unsigned long custom_timer_cb(); +extern uint32_t mtime; +uint32_t custom_timer_cb(); void loop(); diff --git a/tests/ocppEngineLifecycle.cpp b/tests/ocppEngineLifecycle.cpp index b3de2a18..267bb377 100644 --- a/tests/ocppEngineLifecycle.cpp +++ b/tests/ocppEngineLifecycle.cpp @@ -3,24 +3,22 @@ // MIT License #include -#include #include #include "./helpers/testHelper.h" TEST_CASE( "Context lifecycle" ) { printf("\nRun %s\n", "Context lifecycle"); - //initialize Context with dummy socket - MicroOcpp::LoopbackConnection loopback; - mocpp_initialize(loopback); + //initialize Context without any configs + mo_initialize(); SECTION("OCPP is initialized"){ - REQUIRE( getOcppContext() ); + REQUIRE( mo_getApiContext() ); } - mocpp_deinitialize(); + mo_deinitialize(); SECTION("OCPP is deinitialized"){ - REQUIRE( !( getOcppContext() ) ); + REQUIRE( !( mo_getApiContext() ) ); } }