diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index bd1cd75b..9d23daeb 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -1,21 +1,214 @@ -name: documentation +# matth-x/MicroOcpp +# Copyright Matthias Akstaller 2019 - 2024 +# MIT License + +name: Documentation on: push: branches: - - master + - main + pull_request: + permissions: contents: write + jobs: + build_simulator: + name: Build Simulator + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - uses: actions/cache@v4 + with: + key: ${{ github.ref }} + path: .cache + - name: Get build tools + run: | + sudo apt update + sudo apt install cmake libssl-dev build-essential + - name: Checkout Simulator + uses: actions/checkout@v3 + with: + repository: matth-x/MicroOcppSimulator + path: MicroOcppSimulator + ref: 2cb07cdbe53954a694a29336ab31eac2d2b48673 + submodules: 'recursive' + - name: Clean MicroOcpp submodule + run: | + rm -rf MicroOcppSimulator/lib/MicroOcpp + - name: Checkout MicroOcpp submodule + uses: actions/checkout@v3 + with: + path: MicroOcppSimulator/lib/MicroOcpp + - name: Generate CMake files + run: cmake -S ./MicroOcppSimulator -B ./MicroOcppSimulator/build -DCMAKE_CXX_FLAGS="-DMO_OVERRIDE_ALLOCATION=1 -DMO_ENABLE_HEAP_PROFILER=1" + - name: Compile + run: cmake --build ./MicroOcppSimulator/build -j 32 --target mo_simulator + - name: Upload Simulator executable + uses: actions/upload-artifact@v4 + with: + name: Simulator executable + path: | + MicroOcppSimulator/build/mo_simulator + MicroOcppSimulator/public/bundle.html.gz + if-no-files-found: error + retention-days: 1 + + measure_heap: + needs: build_simulator + name: Heap measurements + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Install Python dependencies + run: pip install requests paramiko pandas + - name: Get Simulator + uses: actions/download-artifact@v4 + with: + name: Simulator executable + path: MicroOcppSimulator + - name: Measure heap and create reports + run: | + mkdir -p docs/assets/tables + python tests/benchmarks/scripts/measure_heap.py + env: + TEST_DRIVER_URL: ${{ secrets.TEST_DRIVER_URL }} + TEST_DRIVER_CONFIG: ${{ secrets.TEST_DRIVER_CONFIG }} + TEST_DRIVER_KEY: ${{ secrets.TEST_DRIVER_KEY }} + MO_SIM_CONFIG: ${{ secrets.MO_SIM_CONFIG }} + MO_SIM_OCPP_SERVER: ${{ secrets.MO_SIM_OCPP_SERVER }} + MO_SIM_API_CERT: ${{ secrets.MO_SIM_API_CERT }} + MO_SIM_API_KEY: ${{ secrets.MO_SIM_API_KEY }} + MO_SIM_API_CONFIG: ${{ secrets.MO_SIM_API_CONFIG }} + SSH_LOCAL_PRIV: ${{ secrets.SSH_LOCAL_PRIV }} + SSH_HOST_PUB: ${{ secrets.SSH_HOST_PUB }} + - name: Upload reports + uses: actions/upload-artifact@v4 + with: + name: Memory usage reports CSV + path: docs/assets/tables + if-no-files-found: error + + build_firmware_size: + name: Build firmware + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Cache PlatformIO + uses: actions/cache@v4 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + - name: Set up Python + uses: actions/setup-python@v4 + - name: Install PlatformIO + run: | + python -m pip install --upgrade pip + pip install --upgrade platformio + - name: Run PlatformIO + run: pio ci --lib="." --build-dir="${{ github.workspace }}/../build" --keep-build-dir --project-conf="./tests/benchmarks/firmware_size/platformio.ini" ./tests/benchmarks/firmware_size/main.cpp + - name: Move firmware files # change path to location without parent dir ('..') statement (to make upload-artifact happy) + run: | + mkdir firmware + mv "${{ github.workspace }}/../build/.pio/build/v16/firmware.elf" firmware/firmware_v16.elf + mv "${{ github.workspace }}/../build/.pio/build/v201/firmware.elf" firmware/firmware_v201.elf + - name: Upload firmware linker files + uses: actions/upload-artifact@v4 + with: + name: Firmware linker files + path: firmware + if-no-files-found: error + retention-days: 1 + + evaluate_firmware: + needs: build_firmware_size + name: Static firmware analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - uses: actions/cache@v4 + with: + key: ${{ github.ref }} + path: .cache + - name: Install Python dependencies + run: pip install pandas + - name: Get build tools + run: | + sudo apt update + sudo apt install build-essential cmake ninja-build + sudo apt -y install gcc-9 g++-9 + g++ --version + - name: Check out bloaty + uses: actions/checkout@v3 + with: + repository: google/bloaty + ref: e1155149d54bb09b81e86f0e4e5cb7fbd2a318eb + path: tools/bloaty + submodules: recursive + - name: Install bloaty + run: | + cmake -B tools/bloaty/build -G Ninja -S tools/bloaty + cmake --build tools/bloaty/build -j 32 + - name: Get firmware linker files + uses: actions/download-artifact@v4 + with: + name: Firmware linker files + path: firmware + - name: Run bloaty + run: | + mkdir -p docs/assets/tables + tools/bloaty/build/bloaty firmware/firmware_v16.elf -d compileunits --csv -n 0 > docs/assets/tables/bloaty_v16.csv + tools/bloaty/build/bloaty firmware/firmware_v201.elf -d compileunits --csv -n 0 > docs/assets/tables/bloaty_v201.csv + - name: Evaluate and create reports + run: python tests/benchmarks/scripts/eval_firmware_size.py + - name: Upload reports + uses: actions/upload-artifact@v4 + with: + name: Firmware size reports CSV + path: docs/assets/tables + if-no-files-found: error + deploy: + needs: [evaluate_firmware, measure_heap] + name: Deploy docs runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: 3.x - - uses: actions/cache@v2 - with: - key: ${{ github.ref }} - path: .cache - - run: pip install mkdocs-material - - run: mkdocs gh-deploy --force + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - uses: actions/cache@v4 + with: + key: ${{ github.ref }} + path: .cache + - name: Install Python dependencies + run: pip install pandas mkdocs-material mkdocs-table-reader-plugin + - name: Get firmware size reports + uses: actions/download-artifact@v4 + with: + name: Firmware size reports CSV + path: docs/assets/tables + - name: Get memory occupation reports + uses: actions/download-artifact@v4 + with: + name: Memory usage reports CSV + path: docs/assets/tables + - name: Run mkdocs + run: mkdocs gh-deploy --force diff --git a/.github/workflows/esp-idf.yml b/.github/workflows/esp-idf.yml index 90043470..279cd4a8 100644 --- a/.github/workflows/esp-idf.yml +++ b/.github/workflows/esp-idf.yml @@ -1,10 +1,13 @@ +# matth-x/MicroOcpp +# Copyright Matthias Akstaller 2019 - 2024 +# MIT License + name: ESP-IDF CI on: push: branches: - - master - - develop + - main pull_request: @@ -34,12 +37,13 @@ jobs: uses: actions/checkout@v3 with: repository: matth-x/MicroOcppMongoose - ref: 92ebea9fe8554999b4d34c6a03fa12c0d9faec48 + ref: v1.2.0 path: examples/ESP-IDF/components/MicroOcppMongoose - name: Checkout ArduinoJson uses: actions/checkout@v3 with: repository: bblanchon/ArduinoJson + ref: 3e1be980d93e47b2a0073efeeb9a9396fd7a83be path: examples/ESP-IDF/components/ArduinoJson - name: esp-idf build uses: espressif/esp-idf-ci-action@v1 diff --git a/.github/workflows/pio.yml b/.github/workflows/pio.yml index d0fcb568..189351fd 100644 --- a/.github/workflows/pio.yml +++ b/.github/workflows/pio.yml @@ -1,10 +1,13 @@ +# matth-x/MicroOcpp +# Copyright Matthias Akstaller 2019 - 2024 +# MIT License + name: PlatformIO CI on: push: branches: - - master - - develop + - main pull_request: @@ -17,21 +20,21 @@ jobs: example: [examples/ESP/main.cpp, examples/ESP-TLS/main.cpp] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - 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') }} - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 - name: Install PlatformIO run: | python -m pip install --upgrade pip diff --git a/.github/workflows/platformless.yml b/.github/workflows/platformless.yml index 9682e919..58e7b1ee 100644 --- a/.github/workflows/platformless.yml +++ b/.github/workflows/platformless.yml @@ -1,10 +1,13 @@ +# matth-x/MicroOcpp +# Copyright Matthias Akstaller 2019 - 2024 +# MIT License + name: Default Compilation on: push: branches: - - master - - develop + - main pull_request: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0a3df6c0..38b1d04e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,10 +1,13 @@ +# matth-x/MicroOcpp +# Copyright Matthias Akstaller 2019 - 2024 +# MIT License + name: Unit tests on: push: branches: - - master - - develop + - main pull_request: @@ -16,6 +19,12 @@ jobs: steps: - name: Check out repository code uses: actions/checkout@v3 + - name: Check out MbedTLS + uses: actions/checkout@v3 + with: + repository: Mbed-TLS/mbedtls + ref: v2.28.10 + path: lib/mbedtls - name: Get build tools run: | sudo apt update @@ -26,26 +35,24 @@ jobs: - name: Get ArduinoJson run: wget -Uri https://github.com/bblanchon/ArduinoJson/releases/download/v6.21.3/ArduinoJson-v6.21.3.h -O ./src/ArduinoJson.h - name: Generate CMake build files - run: cmake -S . -B ./build + run: cmake -S . -B ./build -DMO_BUILD_UNIT_MBEDTLS=True - name: Compile - run: cmake --build ./build -j 16 --target mo_unit_tests + run: cmake --build ./build -j 32 --target mo_unit_tests - name: Configure FS run: mkdir mo_store - - name: Run tests - run: ./build/mo_unit_tests - name: Run tests (valgrind) - run: valgrind --error-exitcode=1 --leak-check=full ./build/mo_unit_tests + run: valgrind --error-exitcode=1 --leak-check=full ./build/mo_unit_tests --abort - name: Generate CMake build files (AddressSanitizer, UndefinedBehaviorSanitizer) run: | rm -r ./build - cmake -S . -B ./build -DCMAKE_CXX_FLAGS="-fsanitize=address -fsanitize=undefined" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address -fsanitize=undefined" + cmake -S . -B ./build -DCMAKE_CXX_FLAGS="-fsanitize=address -fsanitize=undefined" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address -fsanitize=undefined" -DMO_BUILD_UNIT_MBEDTLS=True - name: Compile (ASan, UBSan) - run: cmake --build ./build -j 16 --target mo_unit_tests + run: cmake --build ./build -j 32 --target mo_unit_tests - name: Run tests (ASan, UBSan) - run: ./build/mo_unit_tests + run: ./build/mo_unit_tests --abort - name: Create coverage report run: | - lcov --directory . --capture --output-file coverage.info + lcov --directory . --capture --output-file coverage.info --ignore-errors mismatch lcov --remove coverage.info '/usr/*' '*/tests/*' '*/ArduinoJson.h' --output-file coverage.info lcov --list coverage.info - name: Upload coverage reports to Codecov diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d47070b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.pio +.vscode +build +lib +mo_store +src/ArduinoJson* +src/main.cpp +tests/helpers/ArduinoJson* +coverage.info +docs/assets diff --git a/CHANGELOG.md b/CHANGELOG.md index fc560e3c..3b7759aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,153 @@ # Changelog -## [Unreleased] +## Unreleased + +### Changed + +- Change `MicroOcpp::TxNotification` into C-style enum, replace `OCPP_TxNotication` ([#386](https://github.com/matth-x/MicroOcpp/pull/386)) +- Improved UUID generation ([#383](https://github.com/matth-x/MicroOcpp/pull/383)) +- `beginTransaction()` returns bool for better v2.0.1 interop ([#386](https://github.com/matth-x/MicroOcpp/pull/386)) +- Configurations C-API updates ([#400](https://github.com/matth-x/MicroOcpp/pull/400)) +- Platform integrations C-API upates ([#400](https://github.com/matth-x/MicroOcpp/pull/400)) + +### Added + +- `getTransactionV201()` exposes v201 Tx in API ([#386](https://github.com/matth-x/MicroOcpp/pull/386)) +- v201 support in Transaction.h C-API ([#386](https://github.com/matth-x/MicroOcpp/pull/386)) +- Write-only Configurations ([#400](https://github.com/matth-x/MicroOcpp/pull/400)) + +### Fixed + +- Timing issues for OCTT test cases ([#383](https://github.com/matth-x/MicroOcpp/pull/383)) +- Misleading Reset failure dbg msg ([#388](https://github.com/matth-x/MicroOcpp/pull/388)) +- Reject negative ints in ChangeConfig ([#388](https://github.com/matth-x/MicroOcpp/pull/388)) +- Revised SCons integration ([#400](https://github.com/matth-x/MicroOcpp/pull/400)) + +## [1.2.0] - 2024-11-03 + +### Changed + +- Change `MicroOcpp::ChargePointStatus` into C-style enum ([#309](https://github.com/matth-x/MicroOcpp/pull/309)) +- Connector lock disabled by default per `MO_ENABLE_CONNECTOR_LOCK` ([#312](https://github.com/matth-x/MicroOcpp/pull/312)) +- Relaxed temporal order of non-tx-related operations ([#345](https://github.com/matth-x/MicroOcpp/pull/345)) +- Use pseudo-GUIDs as messageId ([#345](https://github.com/matth-x/MicroOcpp/pull/345)) +- ISO 8601 milliseconds omitted by default ([352](https://github.com/matth-x/MicroOcpp/pull/352)) +- Rename `MO_NUM_EVSE` into `MO_NUM_EVSEID` (v2.0.1) ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) +- Change `MicroOcpp::ReadingContext` into C-style struct ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) +- Refactor RequestStartTransaction (v2.0.1) ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) + +### Added + +- Provide ChargePointStatus in API ([#309](https://github.com/matth-x/MicroOcpp/pull/309)) +- Built-in OTA over FTP ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) +- Built-in Diagnostics over FTP ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) +- Error `severity` mechanism ([#331](https://github.com/matth-x/MicroOcpp/pull/331)) +- Build flag `MO_REPORT_NOERROR` to report error recovery ([#331](https://github.com/matth-x/MicroOcpp/pull/331)) +- Support for `parentIdTag` ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) +- Input validation for unsigned int Configs ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) +- Support for TransactionMessageAttempts/-RetryInterval ([#345](https://github.com/matth-x/MicroOcpp/pull/345), [#380](https://github.com/matth-x/MicroOcpp/pull/380)) +- Heap profiler and custom allocator support ([#350](https://github.com/matth-x/MicroOcpp/pull/350)) +- Migration of persistent storage ([#355](https://github.com/matth-x/MicroOcpp/pull/355)) +- Benchmarks pipeline ([#369](https://github.com/matth-x/MicroOcpp/pull/369), [#376](https://github.com/matth-x/MicroOcpp/pull/376)) +- MeterValues port for OCPP 2.0.1 ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) +- UnlockConnector port for OCPP 2.0.1 ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) +- More APIs ported to OCPP 2.0.1 ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) +- Support for AuthorizeRemoteTxRequests ([#373](https://github.com/matth-x/MicroOcpp/pull/373)) +- Persistent Variable and Tx store for OCPP 2.0.1 ([#379](https://github.com/matth-x/MicroOcpp/pull/379)) + +### Removed + +- ESP32 built-in HTTP OTA ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) +- Operation store (files op-*.jsn and opstore.jsn) ([#345](https://github.com/matth-x/MicroOcpp/pull/345)) +- Explicit tracking of txNr (file txstore.jsn) ([#345](https://github.com/matth-x/MicroOcpp/pull/345)) +- SimpleRequestFactory ([#351](https://github.com/matth-x/MicroOcpp/pull/351)) + +### Fixed + +- Skip Unix files . and .. in ftw_root ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) +- Skip clock-aligned measurements when time not set +- Hold back error StatusNotifs when time not set ([#311](https://github.com/matth-x/MicroOcpp/issues/311)) +- Don't send Available when tx occupies connector ([#315](https://github.com/matth-x/MicroOcpp/issues/315)) +- Make ChargingScheduleAllowedChargingRateUnit read-only ([#328](https://github.com/matth-x/MicroOcpp/issues/328)) +- ~Don't send StatusNotifs while offline ([#344](https://github.com/matth-x/MicroOcpp/pull/344))~ (see ([#371](https://github.com/matth-x/MicroOcpp/pull/371))) +- Don't change into Unavailable upon Reset ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) +- Reject DataTransfer by default ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) +- UnlockConnector NotSupported if connectorId invalid ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) +- Fix regression bug of [#345](https://github.com/matth-x/MicroOcpp/pull/345) ([#353](https://github.com/matth-x/MicroOcpp/pull/353), [#356](https://github.com/matth-x/MicroOcpp/pull/356)) +- Correct MeterValue PreBoot timestamp ([#354](https://github.com/matth-x/MicroOcpp/pull/354)) +- Send errorCode in triggered StatusNotif ([#359](https://github.com/matth-x/MicroOcpp/pull/359)) +- Remove int to bool conversion in ChangeConfig ([#362](https://github.com/matth-x/MicroOcpp/pull/362)) +- Multiple fixes of the OCPP 2.0.1 extension ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) + +## [1.1.0] - 2024-05-21 + +### Changed + +- Replace `PollResult` with enum `UnlockConnectorResult` ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) +- Rename master branch into main +- Tx logic directly checks if WebSocket is offline ([#282](https://github.com/matth-x/MicroOcpp/pull/282)) +- `ocppPermitsCharge` ignores Faulted state ([#279](https://github.com/matth-x/MicroOcpp/pull/279)) +- `setEnergyMeterInput` expects `int` input ([#301](https://github.com/matth-x/MicroOcpp/pull/301)) + +### Added + +- File index ([#270](https://github.com/matth-x/MicroOcpp/pull/270)) +- Config `Cst_TxStartOnPowerPathClosed` to put back TxStartPoint ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) +- Build flag `MO_ENABLE_RESERVATION=0` disables Reservation module ([#302](https://github.com/matth-x/MicroOcpp/pull/302)) +- Build flag `MO_ENABLE_LOCAL_AUTH=0` disables LocalAuthList module ([#303](https://github.com/matth-x/MicroOcpp/pull/303)) +- Function `bool isConnected()` in `Connection` interface ([#282](https://github.com/matth-x/MicroOcpp/pull/282)) +- Build flags for customizing memory limits of SmartCharging ([#260](https://github.com/matth-x/MicroOcpp/pull/260)) +- SConscript ([#287](https://github.com/matth-x/MicroOcpp/pull/287)) +- C-API for custom Configs store ([297](https://github.com/matth-x/MicroOcpp/pull/297)) +- Certificate Management, UCs M03 - M05 ([#262](https://github.com/matth-x/MicroOcpp/pull/262), [#274](https://github.com/matth-x/MicroOcpp/pull/274), [#292](https://github.com/matth-x/MicroOcpp/pull/292)) +- FTP Client ([#291](https://github.com/matth-x/MicroOcpp/pull/291)) +- `ProtocolVersion` selects v1.6 or v2.0.1 ([#247](https://github.com/matth-x/MicroOcpp/pull/247)) +- Build flag `MO_ENABLE_V201=1` enables OCPP 2.0.1 features ([#247](https://github.com/matth-x/MicroOcpp/pull/247)) + - Variables (non-persistent), UCs B05 - B07 ([#247](https://github.com/matth-x/MicroOcpp/pull/247), [#284](https://github.com/matth-x/MicroOcpp/pull/284)) + - Transactions (preview only), UCs E01 - E12 ([#247](https://github.com/matth-x/MicroOcpp/pull/247)) + - StatusNotification compatibility ([#247](https://github.com/matth-x/MicroOcpp/pull/247)) + - ChangeAvailability compatibility ([#285](https://github.com/matth-x/MicroOcpp/pull/285)) + - Reset compatibility, UCs B11 - B12 ([#286](https://github.com/matth-x/MicroOcpp/pull/286)) + - RequestStart-/StopTransaction, UCs F01 - F02 ([#289](https://github.com/matth-x/MicroOcpp/pull/289)) + +### Fixed + +- Fix defect idTag check in `endTransaction` ([#275](https://github.com/matth-x/MicroOcpp/pull/275)) +- Make field localAuthorizationList in SendLocalList optional +- Update charging profiles when flash disabled (relates to [#260](https://github.com/matth-x/MicroOcpp/pull/260)) +- Ignore UnlockConnector when handler not set ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) +- Reject ChargingProfile if unit not supported ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) +- Fix building with debug level warn and error +- Reduce debug output FW size overhead ([#304](https://github.com/matth-x/MicroOcpp/pull/304)) +- Fix transaction freeze in offline mode ([#279](https://github.com/matth-x/MicroOcpp/pull/279), [#287](https://github.com/matth-x/MicroOcpp/pull/287)) +- Fix compilation error caused by `PRId32` ([#279](https://github.com/matth-x/MicroOcpp/pull/279)) +- Don't load FW-mngt. module when no handlers set ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) +- Change arduinoWebSockets URL param to path ([#278](https://github.com/matth-x/MicroOcpp/issues/278)) +- Avoid creating conf when operation fails ([#290](https://github.com/matth-x/MicroOcpp/pull/290)) +- Fix whitespaces in MeterValues ([#301](https://github.com/matth-x/MicroOcpp/pull/301)) +- Make SmartChargingProfile txId field optional ([#348](https://github.com/matth-x/MicroOcpp/pull/348)) + +## [1.0.3] - 2024-04-06 + +### Fixed + +- Fix nullptr access in endTransaction ([#275](https://github.com/matth-x/MicroOcpp/pull/275)) +- Backport: Fix building with debug level warn and error + +## [1.0.2] - 2024-03-24 + +### Fixed + +- Correct MO version numbers in code (they were still `1.0.0`) + +## [1.0.1] - 2024-02-27 ### Fixed - Allow `nullptr` as parameter for `mocpp_set_console_out` ([#224](https://github.com/matth-x/MicroOcpp/issues/224)) - Fix `mocpp_tick_ms()` on esp-idf roll-over after 12 hours +- Pin ArduinoJson to v6.21 ([#245](https://github.com/matth-x/MicroOcpp/issues/245)) +- Fix bounds checking in SmartCharging module ([#260](https://github.com/matth-x/MicroOcpp/pull/260)) ## [1.0.0] - 2023-10-22 diff --git a/CMakeLists.txt b/CMakeLists.txt index 95810e11..bf1d5f7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,17 +1,21 @@ # matth-x/MicroOcpp -# Copyright Matthias Akstaller 2019 - 2023 +# Copyright Matthias Akstaller 2019 - 2024 # MIT License cmake_minimum_required(VERSION 3.15) +set(CMAKE_CXX_STANDARD 11) set(MO_SRC + src/MicroOcpp/Core/Configuration_c.cpp src/MicroOcpp/Core/Configuration.cpp src/MicroOcpp/Core/ConfigurationContainer.cpp src/MicroOcpp/Core/ConfigurationContainerFlash.cpp src/MicroOcpp/Core/ConfigurationKeyValue.cpp src/MicroOcpp/Core/FilesystemAdapter.cpp src/MicroOcpp/Core/FilesystemUtils.cpp + src/MicroOcpp/Core/FtpMbedTLS.cpp + src/MicroOcpp/Core/Memory.cpp src/MicroOcpp/Core/RequestQueue.cpp src/MicroOcpp/Core/Context.cpp src/MicroOcpp/Core/Operation.cpp @@ -19,8 +23,7 @@ set(MO_SRC src/MicroOcpp/Core/Request.cpp src/MicroOcpp/Core/Connection.cpp src/MicroOcpp/Core/Time.cpp - src/MicroOcpp/Core/RequestQueueStorageStrategy.cpp - src/MicroOcpp/Core/RequestStore.cpp + src/MicroOcpp/Core/UuidUtils.cpp src/MicroOcpp/Operations/Authorize.cpp src/MicroOcpp/Operations/BootNotification.cpp src/MicroOcpp/Operations/CancelReservation.cpp @@ -30,36 +33,52 @@ set(MO_SRC src/MicroOcpp/Operations/ClearChargingProfile.cpp src/MicroOcpp/Operations/CustomOperation.cpp src/MicroOcpp/Operations/DataTransfer.cpp + src/MicroOcpp/Operations/DeleteCertificate.cpp src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp src/MicroOcpp/Operations/FirmwareStatusNotification.cpp + src/MicroOcpp/Operations/GetBaseReport.cpp src/MicroOcpp/Operations/GetCompositeSchedule.cpp src/MicroOcpp/Operations/GetConfiguration.cpp src/MicroOcpp/Operations/GetDiagnostics.cpp + src/MicroOcpp/Operations/GetInstalledCertificateIds.cpp src/MicroOcpp/Operations/GetLocalListVersion.cpp + src/MicroOcpp/Operations/GetVariables.cpp src/MicroOcpp/Operations/Heartbeat.cpp src/MicroOcpp/Operations/MeterValues.cpp + src/MicroOcpp/Operations/NotifyReport.cpp src/MicroOcpp/Operations/RemoteStartTransaction.cpp src/MicroOcpp/Operations/RemoteStopTransaction.cpp + src/MicroOcpp/Operations/RequestStartTransaction.cpp + src/MicroOcpp/Operations/RequestStopTransaction.cpp src/MicroOcpp/Operations/ReserveNow.cpp src/MicroOcpp/Operations/Reset.cpp + src/MicroOcpp/Operations/SecurityEventNotification.cpp src/MicroOcpp/Operations/SendLocalList.cpp src/MicroOcpp/Operations/SetChargingProfile.cpp + src/MicroOcpp/Operations/SetVariables.cpp src/MicroOcpp/Operations/StartTransaction.cpp src/MicroOcpp/Operations/StatusNotification.cpp src/MicroOcpp/Operations/StopTransaction.cpp + src/MicroOcpp/Operations/TransactionEvent.cpp src/MicroOcpp/Operations/TriggerMessage.cpp + src/MicroOcpp/Operations/InstallCertificate.cpp src/MicroOcpp/Operations/UnlockConnector.cpp src/MicroOcpp/Operations/UpdateFirmware.cpp + src/MicroOcpp/Debug.cpp src/MicroOcpp/Platform.cpp - src/MicroOcpp/Core/SimpleRequestFactory.cpp src/MicroOcpp/Core/OperationRegistry.cpp + src/MicroOcpp/Model/Availability/AvailabilityService.cpp src/MicroOcpp/Model/Authorization/AuthorizationData.cpp src/MicroOcpp/Model/Authorization/AuthorizationList.cpp src/MicroOcpp/Model/Authorization/AuthorizationService.cpp + src/MicroOcpp/Model/Authorization/IdToken.cpp src/MicroOcpp/Model/Boot/BootService.cpp + src/MicroOcpp/Model/Certificates/Certificate.cpp + src/MicroOcpp/Model/Certificates/Certificate_c.cpp + src/MicroOcpp/Model/Certificates/CertificateMbedTLS.cpp + src/MicroOcpp/Model/Certificates/CertificateService.cpp src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp src/MicroOcpp/Model/ConnectorBase/Connector.cpp - src/MicroOcpp/Model/ConnectorBase/Notification.cpp src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp @@ -67,7 +86,10 @@ set(MO_SRC src/MicroOcpp/Model/Metering/MeteringService.cpp src/MicroOcpp/Model/Metering/MeterStore.cpp src/MicroOcpp/Model/Metering/MeterValue.cpp + src/MicroOcpp/Model/Metering/MeterValuesV201.cpp + src/MicroOcpp/Model/Metering/ReadingContext.cpp src/MicroOcpp/Model/Metering/SampledValue.cpp + src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp src/MicroOcpp/Model/Reservation/Reservation.cpp src/MicroOcpp/Model/Reservation/ReservationService.cpp src/MicroOcpp/Model/Reset/ResetService.cpp @@ -75,7 +97,11 @@ set(MO_SRC src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp src/MicroOcpp/Model/Transactions/Transaction.cpp src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp + src/MicroOcpp/Model/Transactions/TransactionService.cpp src/MicroOcpp/Model/Transactions/TransactionStore.cpp + src/MicroOcpp/Model/Variables/Variable.cpp + src/MicroOcpp/Model/Variables/VariableContainer.cpp + src/MicroOcpp/Model/Variables/VariableService.cpp src/MicroOcpp.cpp src/MicroOcpp_c.cpp ) @@ -83,21 +109,18 @@ set(MO_SRC if(ESP_PLATFORM) idf_component_register(SRCS ${MO_SRC} - INCLUDE_DIRS "./src" "../ArduinoJson/src" - PRIV_REQUIRES spiffs) + INCLUDE_DIRS "./src" "../ArduinoJson/src" + PRIV_REQUIRES spiffs + ) target_compile_options(${COMPONENT_TARGET} PUBLIC -DMO_PLATFORM=MO_PLATFORM_ESPIDF - -DMO_CUSTOM_WS - -DMO_CUSTOM_UPDATER - -DMO_CUSTOM_RESET - ) + ) return() endif() -project(MicroOcpp - VERSION 0.2.0) +project(MicroOcpp VERSION 1.2.0) add_library(MicroOcpp ${MO_SRC}) @@ -108,9 +131,6 @@ target_include_directories(MicroOcpp PUBLIC target_compile_definitions(MicroOcpp PUBLIC MO_PLATFORM=MO_PLATFORM_UNIX - MO_CUSTOM_WS - MO_CUSTOM_UPDATER - MO_CUSTOM_RESET ) # Unit tests @@ -126,7 +146,16 @@ set(MO_SRC_UNIT tests/Metering.cpp tests/Configuration.cpp tests/Reservation.cpp + tests/Reset.cpp tests/LocalAuthList.cpp + tests/Variables.cpp + tests/Transactions.cpp + tests/RemoteStartTransaction.cpp + tests/Certificates.cpp + tests/FirmwareManagement.cpp + tests/ChargePointError.cpp + tests/Boot.cpp + tests/Security.cpp ) add_executable(mo_unit_tests @@ -135,8 +164,21 @@ add_executable(mo_unit_tests ./tests/catch2/catchMain.cpp ) +if (MO_BUILD_UNIT_MBEDTLS) + add_subdirectory(lib/mbedtls) + target_link_libraries(mo_unit_tests PUBLIC + mbedtls + mbedcrypto + mbedx509 + ) + + target_compile_definitions(mo_unit_tests PUBLIC + MO_ENABLE_MBEDTLS=1 + ) +endif() + target_include_directories(mo_unit_tests PUBLIC - "./tests/catch2" + "./tests" "./tests/helpers" "./src" ) @@ -145,17 +187,27 @@ target_compile_definitions(mo_unit_tests PUBLIC MO_PLATFORM=MO_PLATFORM_UNIX MO_NUMCONNECTORS=3 MO_CUSTOM_TIMER - MO_CUSTOM_WS - MO_CUSTOM_UPDATER - MO_CUSTOM_RESET - MO_DBG_LEVEL=MO_DL_DEBUG + MO_DBG_LEVEL=MO_DL_INFO MO_TRAFFIC_OUT MO_FILENAME_PREFIX="./mo_store/" MO_LocalAuthListMaxLength=8 MO_SendLocalListMaxLength=4 + MO_ENABLE_FILE_INDEX=1 + MO_ChargeProfileMaxStackLevel=2 + MO_ChargingScheduleMaxPeriods=4 + MO_MaxChargingProfilesInstalled=3 + MO_ENABLE_CERT_MGMT=1 + MO_ENABLE_CONNECTOR_LOCK=1 + MO_REPORT_NOERROR=1 + MO_ENABLE_V201=1 + MO_OVERRIDE_ALLOCATION=1 + MO_ENABLE_HEAP_PROFILER=1 + MO_HEAP_PROFILER_EXTERNAL_CONTROL=1 + CATCH_CONFIG_EXTERNAL_INTERFACES ) target_compile_options(mo_unit_tests PUBLIC + -Wall -O0 -g --coverage diff --git a/LICENSE b/LICENSE index 4211ff4f..d0b6f8ec 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 - 2023 Matthias Akstaller +Copyright (c) 2019 - 2024 Matthias Akstaller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0e38191b..de09d875 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,32 @@ -# Icon   MicroOcpp +# Icon   MicroOCPP [![Build Status]( https://github.com/matth-x/MicroOcpp/workflows/PlatformIO%20CI/badge.svg)](https://github.com/matth-x/MicroOcpp/actions) [![Unit tests]( https://github.com/matth-x/MicroOcpp/workflows/Unit%20tests/badge.svg)](https://github.com/matth-x/MicroOcpp/actions) [![codecov](https://codecov.io/github/matth-x/ArduinoOcpp/branch/develop/graph/badge.svg?token=UN6LO96HM7)](https://codecov.io/github/matth-x/ArduinoOcpp) -OCPP 1.6 client for microcontrollers. Portable C/C++. Compatible with Espressif, Arduino, NXP, STM, Linux and more. +OCPP 1.6 / 2.0.1 client for microcontrollers. Portable C/C++. Compatible with Espressif, Arduino, NXP, STM, Linux and more. -**Formerly ArduinoOcpp ([migration guide](https://matth-x.github.io/MicroOcpp/migration/))**: *the initial version of this library used the Arduino API but this dependency was dropped some time ago and the old name has become outdated. Despite the new name, nothing changes for existing users and the Arduino integration will continue to be fully functional.* +:heavy_check_mark: Works with [15+ commercial Central Systems](https://www.micro-ocpp.com/#h.314525e8447cc93c_81) + +:heavy_check_mark: Eligible for public chargers (Eichrecht-compliant) + +:heavy_check_mark: Supports all OCPP 1.6 feature profiles and the [basic OCPP 2.0.1 UCs](https://github.com/matth-x/MicroOcpp/tree/feature/prepare-release?tab=readme-ov-file#ocpp-201-and-iso-15118) Reference usage: [OpenEVSE](https://github.com/OpenEVSE/ESP32_WiFi_V4.x/blob/master/src/ocpp.cpp) | Technical introduction: [Docs](https://matth-x.github.io/MicroOcpp/intro-tech) | Website: [www.micro-ocpp.com](https://www.micro-ocpp.com) -:heavy_check_mark: Works with [SteVe](https://github.com/RWTH-i5-IDSG/steve) and [15+ commercial Central Systems](https://www.micro-ocpp.com/#h.314525e8447cc93c_81) +## AI-friendly code -:heavy_check_mark: Eligible for public chargers (Eichrecht-compliant) +AI models perform extremely well with the MicroOCPP codebase. The upcoming new release of MO (v2.0) will further optimize the code for more reliable results with AI models (preview to be found in the `develop/remodel-api` branch). The hope is to allow integrating MO into an existing EV charger project with only a few queries. + +Currently, the `develop/remodel-api` branch is not stable yet, but recommended for new developments. To get started, load `MicroOcpp.h` (now unified for C and C++) into the context window and ask what the AI model needs to know to integrate it into your codebase. -:heavy_check_mark: Supports all OCPP 1.6 feature profiles +If your tools have issues with something in MicroOCPP, please open an issue on GitHub. Any feedback on how to further optimize the codebase is also highly appreciated. ## Tester / Demo App *Main repository: [MicroOcppSimulator](https://github.com/matth-x/MicroOcppSimulator)* -(Beta) The Simulator is a demo & development tool for MicroOcpp which allows to quickly assess the compatibility with different OCPP backends. It simulates a full charging station, adds a GUI and a mocked hardware binding to MicroOcpp and runs in the browser (using WebAssembly): [Try it](https://demo.micro-ocpp.com/) +The Simulator is a demo & development tool for MicroOCPP which allows to quickly assess the compatibility with different OCPP backends. It simulates a full charging station, adds a GUI and a mocked hardware binding to MicroOCPP and runs in the browser (using WebAssembly): [Try it](https://demo.micro-ocpp.com/)
Screenshot
@@ -34,7 +40,7 @@ If you don't have an OCPP server at hand, leave the charge box ID blank and ente ## Benchmarks -*Full report: [MicroOcpp benchmark (esp-idf)](https://github.com/matth-x/MicroOcpp-benchmark)* +*Full report: [MicroOCPP benchmarks](https://matth-x.github.io/MicroOcpp/benchmarks/)* The following measurements were taken on the ESP32 @ 160MHz and represent the optimistic best case scenario for a charger with two physical connectors (i.e. compiled with `-Os`, disabled debug output and logs). @@ -53,30 +59,52 @@ In practical setups, the execution time is largely determined by IO delays and t PlatformIO package: [MicroOcpp](https://registry.platformio.org/libraries/matth-x/MicroOcpp) -MicroOcpp is an implementation of the OCPP communication behavior. It automatically initiates the corresponding OCPP operations once the hardware status changes or the RFID input is updated with a new value. Conversely it processes new data from the server, stores it locally and updates the hardware controls when applicable. +MicroOCPP is an implementation of the OCPP communication behavior. It automatically initiates the corresponding OCPP operations once the hardware status changes or the RFID input is updated with a new value. Conversely it processes new data from the server, stores it locally and updates the hardware controls when applicable. -Please take `examples/ESP/main.cpp` as the starting point for the first project. It is a minimal example which shows how to establish an OCPP connection and how to start and stop charging sessions. The API documentation can be found in [`MicroOcpp.h`](https://github.com/matth-x/MicroOcpp/blob/master/src/MicroOcpp.h). Also check out the [Docs](https://matth-x.github.io/MicroOcpp). +Please take `examples/ESP/main.cpp` as the starting point for the first project. It is a minimal example which shows how to establish an OCPP connection and how to start and stop charging sessions. The API documentation can be found in [`MicroOcpp.h`](https://github.com/matth-x/MicroOcpp/blob/main/src/MicroOcpp.h). Also check out the [Docs](https://matth-x.github.io/MicroOcpp). ### Dependencies Mandatory: -- [bblanchon/ArduinoJSON](https://github.com/bblanchon/ArduinoJson) +- [bblanchon/ArduinoJSON](https://github.com/bblanchon/ArduinoJson) (version `6.21`) If compiled with the Arduino integration: - [Links2004/arduinoWebSockets](https://github.com/Links2004/arduinoWebSockets) (version `2.4.1`) -In case you use PlatformIO, you can copy all dependencies from `platformio.ini` into your own configuration file. Alternatively, you can install the full library with dependencies by adding `matth-x/MicroOcpp@1.0.0` in the PIO library manager. +If using the built-in certificate store (to enable, set build flag `MO_ENABLE_MBEDTLS=1`): + +- [Mbed-TLS/mbedtls](https://github.com/Mbed-TLS/mbedtls) (version `2.28.1`) + +In case you use PlatformIO, you can copy all dependencies from `platformio.ini` into your own configuration file. Alternatively, you can install the full library with dependencies by adding `matth-x/MicroOcpp@1.2.0` in the PIO library manager. ## OCPP 2.0.1 and ISO 15118 -MicroOcpp will be upgraded to OCPP 2.0.1 soon. The API has already been prepared for transitioning between both versions, so an integration of the current library version will also be functional with the 2.0.1 upgrade. +The following OCPP 2.0.1 use cases are implemented: + +| UC | Description | Note | +| :--- | :--- | :--- | +| B01 - B04
B11 - B12 | Provisioning | Ported from OCPP 1.6 | +| B05 - B07 | Variables | | +| C01 - C06 | Authorization options | | +| C15 | Offline Authorization | | +| E01 - E12 | Transactions | | +| F01 - F03
F05 - F06 | RemoteControl | | +| G01 - G04 | Availability | | +| J02 | Tx-related MeterValues | persistency not supported yet | +| M03 - M05 | Certificate management | Enable Mbed-TLS to use the built-in certificate store | +| P01 - P02 | Data transfer | | +| - | Protocol negotiation | The charger can select the OCPP version at runtime | + +The OCPP 2.0.1 features are in an alpha development stage. By default, they are disabled and excluded from the build, so they have no impact on the firmware size. To enable, set the build flag `MO_ENABLE_V201=1` and initialize the library with the ProtocolVersion parameter `2.0.1` (see [this example](https://github.com/matth-x/MicroOcppSimulator/blob/657e606c3b178d3add242935d413c72624130ff3/src/main.cpp#L43-L47) in the Simulator). + +An integration of the library for OCPP 1.6 will also be functional with the 2.0.1 upgrade. It works with the same API in MicroOcpp.h. -ISO 15118 defines some use cases which include a message exchange between the charger and server. This library facilitates the integration of ISO 15118 by handling its OCPP-side communication. A public demonstration will follow with the first collaboration on an open OCPP 2.0.1 + ISO 15118 integration. +ISO 15118 defines some use cases which include a message exchange between the charger and server. This library facilitates the integration of ISO 15118 by handling its OCPP-side communication. -## Contact details +## Contact -I hope the given documentation and guidance can help you to successfully integrate an OCPP controller into your EVSE. I will be happy if you reach out! +If you have any questions, or found a potential bug, feel free to open an issue. This type of interaction is highly appreciated, because it shows problems in the codebase and helps improve the project for clarity. For further questions which shouldn't stand in public, you can reach me via LinkedIn or the following email address: -:envelope: : matthias [A⊤] arduino-ocpp [DО⊤] com +:envelope: : matthias [A⊤] micro-ocpp [DО⊤] com diff --git a/SConscript.py b/SConscript.py new file mode 100644 index 00000000..a33ab3f6 --- /dev/null +++ b/SConscript.py @@ -0,0 +1,49 @@ +# matth-x/MicroOcpp +# Copyright Matthias Akstaller 2019 - 2024 +# MIT License + +# NOTE: This SConscript is still WIP. It has thankfully been contributed from a project using SCons, +# not necessarily considering full reusability in other projects though. +# Use this file as a starting point for writing your own SCons integration. And as always, any +# contributions are highly welcome! + +Import("env") + +import os, pathlib + +def getAllDirs(root_dir): + dir_list = [] + for root, subfolders, files in os.walk(root_dir.abspath): + dir_list.append(Dir(root)) + return dir_list + +SOURCE_DIR = Dir(".").srcnode().Dir("src") + +source_dirs = getAllDirs(SOURCE_DIR) + +source_files = [] + +for folder in source_dirs: + source_files += folder.glob("*.c") + source_files += folder.glob("*.cpp") + +compiled_objects = [] +for source_file in source_files: + obj = env.Object( + target = pathlib.Path(source_file.path).stem + + ".o", + source=source_file, + ) + compiled_objects.append(obj) + +libmicroocpp = env.StaticLibrary( + target='libmicroocpp', + source=sorted(compiled_objects) +) + +exports = { + 'library': libmicroocpp, + 'CPPPATH': SOURCE_DIR +} + +Return("exports") diff --git a/docs/benchmarks.md b/docs/benchmarks.md new file mode 100644 index 00000000..665930c6 --- /dev/null +++ b/docs/benchmarks.md @@ -0,0 +1,59 @@ +# Benchmarks + +Microcontrollers have tight hardware constraints which affect how much resources the firmware can demand. It is important to make sure that the available resources are not depleted to allow for robust operation and that there is sufficient flash head room to allow for future software upgrades. + +In general, microcontrollers have three relevant hardware constraints: + +- Limited processing speed +- Limited memory size +- Limited flash size + +For OCPP, the relevant bottlenecks are especially the memory and flash size. The processing speed is no concern, since OCPP is not computationally complex and does not include any extensive planning algorithms on the charger size. A previous [benchmark on the ESP-IDF](https://github.com/matth-x/MicroOcpp-benchmark) showed that the processing times are in the lower milliseconds range and are probably outweighed by IO times and network round trip times. + +However, the memory and flash requirements are important figures, because the device model of OCPP has a significant size. The microcontroller needs to keep the model data in the heap memory for the largest part and the firmware which covers the corresponding processing routines needs to have sufficient space on flash. + +This chapter presents benchmarks of the memory and flash requirements. They should help to determine the required microcontroller capabilities, or to give general insights for taking further action on optimizing the firmware. + +## Firmware size + +When compiling a firmware with MicroOCPP, the resulting binary will contain functionality which is not related to OCPP, like hardware drivers, modules which are shared, like MbedTLS and the actual MicroOCPP object files. The size of the latter is the final flash requirement of MicroOCPP. + +For the flash benchmark, the profiler compiles a [dummy OCPP firmware](https://github.com/matth-x/MicroOcpp/tree/main/tests/benchmarks/firmware_size/main.cpp), analyzes the size of the compilation units using [bloaty](https://github.com/google/bloaty) and evaluates the bloaty report using a [Python script](https://github.com/matth-x/MicroOcpp/tree/main/tests/benchmarks/scripts/eval_firmware_size.py). To give realistic results, the firwmare is compiled with `-Os`, no RTTI or exceptions and newlib as the standard C library. The following tables show the results. + +### OCPP 1.6 + +The following table shows the cumulated size of the objects files per module. The Module category consists of the OCPP 2.0.1 functional blocks, OCPP 1.6 feature profiles and general functionality which is shared accross the library. If a feature of the implementation falls under both an OCPP 2.0.1 functional block and OCPP 1.6 feature profile definition, it is preferrably assigned to the OCPP 2.0.1 category. This allows for better comparability between both OCPP versions. + +**Table 1: Firmware size per Module** + +{{ read_csv('modules_v16.csv') }} + +### OCPP 2.0.1 + +**Table 2: Firmware size per Module** + +{{ read_csv('modules_v201.csv') }} + +## Memory usage + +MicroOCPP uses the heap memory to process incoming messages, maintain the device model and create outgoing OCPP messages. The total heap usage should remain low enough to not risk a heap depletion which would not only affect the OCPP module, but the whole controller, because heap memory is typically shared on microcontrollers. To assess the heap usage of MicroOCPP, a test suite runs a variety of simulated charger use cases and measures the maximum occupied memory. Then, the maximum observed value is considered as the memory requirement of MicroOCPP. + +Another important figure is the base level which is much closer to the average heap usage. The total heap usage consists of a base level and a dynamic part. Some memory objects are only initialized once during startup or as the device model is populated (e.g. Charging Schedules) and therefore belong to the base which changes only slowly over time. In contrast, objects for the JSON parsing and serialization and the internal execution of the operations are highly dynamic as they are instantiated for one operation and freed again after completion of the action. If the firmware contains multiple components besides MicroOCPP with this usage pattern, then the average total memory occupation of the device RAM is even closer to the base levels of the individual components. + +The following table shows the dynamic heap usage for a variety of test cases, followed by the base level and resulting maximum memory occupation of MicroOCPP. At the time being, the measurements are limited to only OCPP 2.0.1 and a narrow set of test cases. They will be gradually extended over time. + +**Table 3: Memory usage per use case and total** + +{{ read_csv('heap_v201.csv') }} + +## Full data sets + +This section contains the raw data which is the basis for the evaluations above. + +**Table 4: All compilation units for OCPP 1.6 firmware** + +{{ read_csv('compile_units_v16.csv') }} + +**Table 5: All compilation units for OCPP 2.0.1 firmware** + +{{ read_csv('compile_units_v201.csv') }} diff --git a/docs/img/components_overview.svg b/docs/img/components_overview.svg index d5dbf07f..a6423759 100644 --- a/docs/img/components_overview.svg +++ b/docs/img/components_overview.svg @@ -1,3 +1,3 @@ -
MicroOcpp library
MicroOcpp library
MicroOcpp API
MicroOcpp API
OCPP behavior and device model
OCPP behavior and device model
Remote Procedure Call (RPC) framework
Remote Procedure Call (RPC) framework
Firmware (main source)
Firmware (main source)
WebSocket library,
filesystem access,
system clock
WebSocket library,...
Text is not SVG - cannot display
\ No newline at end of file +
MicroOCPP library
MicroOCPP library
MicroOCPP API
MicroOCPP API
OCPP behavior and device model
OCPP behavior and device model
Remote Procedure Call (RPC) framework
Remote Procedure Call (RPC) framework
Firmware (main source)
Firmware (main source)
WebSocket library,
filesystem access,
system clock
WebSocket library,...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/img/components_selective.svg b/docs/img/components_selective.svg index e2dedf65..7788f100 100644 --- a/docs/img/components_selective.svg +++ b/docs/img/components_selective.svg @@ -1,3 +1,3 @@ -
MicroOcpp library
MicroOcpp library
MicroOcpp API
MicroOcpp API
OCPP behavior and device model
OCPP behavior and device model
Remote Procedure Call (RPC) framework
Remote Procedure Call (RPC) framework
Firmware
(main source)
Firmware...
WebSocket library,
filesystem access,
system clock
WebSocket library,...
MicroOcpp library
MicroOcpp library
MicroOcpp API
MicroOcpp API
OCPP behavior and device model
OCPP behavior and device model
Remote Procedure Call (RPC) framework
Remote Procedure Call (RPC) framework
Firmware
(main source)
Firmware...
WebSocket library,
filesystem access,
system clock
WebSocket library,...
custom RPC framework
custom RPC framework
Example A) Only RPC framework selected
Example A) Only RPC framework selected
Example B) OCPP logic selected
Example B) OCPP logic selected
Text is not SVG - cannot display
\ No newline at end of file +
MicroOCPP library
MicroOCPP library
MicroOCPP API
MicroOCPP API
OCPP behavior and device model
OCPP behavior and device model
Remote Procedure Call (RPC) framework
Remote Procedure Call (RPC) framework
Firmware
(main source)
Firmware...
WebSocket library,
filesystem access,
system clock
WebSocket library,...
MicroOCPP library
MicroOCPP library
MicroOCPP API
MicroOCPP API
OCPP behavior and device model
OCPP behavior and device model
Remote Procedure Call (RPC) framework
Remote Procedure Call (RPC) framework
Firmware
(main source)
Firmware...
WebSocket library,
filesystem access,
system clock
WebSocket library,...
custom RPC framework
custom RPC framework
Example A) Only RPC framework selected
Example A) Only RPC framework selected
Example B) OCPP logic selected
Example B) OCPP logic selected
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 11eac9d1..3705cfff 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -MicroOcpp is an OCPP client which runs on microcontrollers and enables EVSEs to participate in OCPP charging networks. As a software library, it can be added to the firmware of the EVSE and will become a new part of it. If the EVSE has already an internet controller, then most likely, no extra hardware is required. +MicroOCPP is an OCPP client which runs on microcontrollers and enables EVSEs to participate in OCPP charging networks. As a software library, it can be added to the firmware of the EVSE and will become a new part of it. If the EVSE has already an internet controller, then most likely, no extra hardware is required. [Technical introduction](intro-tech) @@ -12,4 +12,4 @@ MicroOcpp is an OCPP client which runs on microcontrollers and enables EVSEs to -*Documentation WIP. See the [GitHub Readme](https://github.com/matth-x/MicroOcpp) or the [API description](https://github.com/matth-x/MicroOcpp/blob/master/src/MicroOcpp.h) as reference.* +*Documentation WIP. See the [GitHub Readme](https://github.com/matth-x/MicroOcpp) or the [API description](https://github.com/matth-x/MicroOcpp/blob/main/src/MicroOcpp.h) as reference.* diff --git a/docs/intro-tech.md b/docs/intro-tech.md index 451e5ac9..d99457f5 100644 --- a/docs/intro-tech.md +++ b/docs/intro-tech.md @@ -1,14 +1,14 @@ # Technical introduction -This chapter covers the technical concepts of MicroOcpp. +This chapter covers the technical concepts of MicroOCPP. -## Scope of MicroOcpp +## Scope of MicroOCPP -The OCPP specification defines a charger data model, operations on the data model and the resulting physical behavior on the charger side. MicroOcpp implements the full scope of OCPP, i.e. a minimalistic data store for the data model, the OCPP operations and an interface to the surrounding firmware. +The OCPP specification defines a charger data model, operations on the data model and the resulting physical behavior on the charger side. MicroOCPP implements the full scope of OCPP, i.e. a minimalistic data store for the data model, the OCPP operations and an interface to the surrounding firmware. -Another part of OCPP is its messaging mechanism, the so-called Remote Procedure Calls (RPC) framework. MicroOcpp also implements the specified RPC framework with the required guarantees of message delivery or the corresponding error handling. +Another part of OCPP is its messaging mechanism, the so-called Remote Procedure Calls (RPC) framework. MicroOCPP also implements the specified RPC framework with the required guarantees of message delivery or the corresponding error handling. -At the lowest layer, OCPP relies on standard WebSockets. MicroOcpp works with any WebSocket library and has a lean interface to integrate them. +At the lowest layer, OCPP relies on standard WebSockets. MicroOCPP works with any WebSocket library and has a lean interface to integrate them. The high-level API in `MicroOcpp.h` bundles all touch points of the EVSE firmware with the OCPP library. @@ -20,7 +20,7 @@ The high-level API in `MicroOcpp.h` bundles all touch points of the EVSE firmwar ## High-level OCPP support -Being a full implementation of OCPP, MicroOcpp handles the OCPP communication, i.e. it sends OCPP requests and processes incoming OCPP requests autonomously. The messages are triggered by the internal data model and by input from the high-level API. Incoming OCPP requests are used to update the internal data model and if an action on the charger is required, the library signals that to the main firmware through the high-level API. +Being a full implementation of OCPP, MicroOCPP handles the OCPP communication, i.e. it sends OCPP requests and processes incoming OCPP requests autonomously. The messages are triggered by the internal data model and by input from the high-level API. Incoming OCPP requests are used to update the internal data model and if an action on the charger is required, the library signals that to the main firmware through the high-level API. In consequence, the high-level API decouples the main firmware from the OCPP communication and hides the operations. This has the following good reasons: @@ -30,56 +30,56 @@ In consequence, the high-level API decouples the main firmware from the OCPP com ## Customizability -One core principle of the architecture of MicroOcpp is the customizability and the selective usage of its components. +One core principle of the architecture of MicroOCPP is the customizability and the selective usage of its components. -Selective usage of components means that the EVSE firmware can use parts of MicroOcpp and work with its own implementation for the rest. In that case only the selected parts of MicroOcpp will be compiled into the firmware. For example, the main firmware can use the RPC framework and build a custom implementation of the OCPP logic on top of it. This could be necessary if the OCPP behavior should be tightly coupled to other modules of the firmware. In a different scenario, the EVSE firmware could already contain an extensive RPC framework and the OCPP client should reuse it. Then, only the business logic and high-level API are of interest. +Selective usage of components means that the EVSE firmware can use parts of MicroOCPP and work with its own implementation for the rest. In that case only the selected parts of MicroOCPP will be compiled into the firmware. For example, the main firmware can use the RPC framework and build a custom implementation of the OCPP logic on top of it. This could be necessary if the OCPP behavior should be tightly coupled to other modules of the firmware. In a different scenario, the EVSE firmware could already contain an extensive RPC framework and the OCPP client should reuse it. Then, only the business logic and high-level API are of interest.


- Selective usage of MicroOcpp + Selective usage of MicroOCPP

-Customizations of the library allow to integrate use cases for which the high-level API is too restrictive. The high-level API is designed to provide a facade for the expected usage of the library, but since the charging sector is driven by innovation, new use cases for OCPP emerge every day. If a custom use case cannot be integrated on the API level, the main firmware can access the internal data structures of MicroOcpp and complement the required functionality or replace parts of the internal behavior with custom implementations which fits the concrete scenarios better. +Customizations of the library allow to integrate use cases for which the high-level API is too restrictive. The high-level API is designed to provide a facade for the expected usage of the library, but since the charging sector is driven by innovation, new use cases for OCPP emerge every day. If a custom use case cannot be integrated on the API level, the main firmware can access the internal data structures of MicroOCPP and complement the required functionality or replace parts of the internal behavior with custom implementations which fits the concrete scenarios better. ## Main-loop paradigm -MicroOcpp works with the common main-loop execution model of microcontrollers. After initialization, the EVSE firmware most likely enters a main-loop and repeats it infinitely. To run MicroOcpp, a call to its loop function must be placed into the main loop of the firmware. Then at each main-loop iteration, MicroOcpp executes its internal routines, i.e. it processes input data, updates its data model, executes operations and creates new output data. The MicroOcpp loop function does not block the main loop but executes immediately. This library does not contain any delay functions. Some activities of the library spread over many loop iterations like the start of a charging session which needs to await the approval of an NFC card and a hardware diagnosis of the high power electronics for example. All activities in MicroOcpp support the distribution over many loop calls, leading to a pseudo-parallel execution behavior. +MicroOCPP works with the common main-loop execution model of microcontrollers. After initialization, the EVSE firmware most likely enters a main-loop and repeats it infinitely. To run MicroOCPP, a call to its loop function must be placed into the main loop of the firmware. Then at each main-loop iteration, MicroOCPP executes its internal routines, i.e. it processes input data, updates its data model, executes operations and creates new output data. The MicroOCPP loop function does not block the main loop but executes immediately. This library does not contain any delay functions. Some activities of the library spread over many loop iterations like the start of a charging session which needs to await the approval of an NFC card and a hardware diagnosis of the high power electronics for example. All activities in MicroOCPP support the distribution over many loop calls, leading to a pseudo-parallel execution behavior. -No separate RTOS task is needed and MicroOcpp does not have an internal mechanism for multi-task synchronization. However, it is of course possible to create a dedicated OCPP task, as long as extra care is taken of the synchronization. +No separate RTOS task is needed and MicroOCPP does not have an internal mechanism for multi-task synchronization. However, it is of course possible to create a dedicated OCPP task, as long as extra care is taken of the synchronization. ## How the API works The high-level API consists of four parts: - **Library lifecycle**: The library has initialize functions with a few initialization options. Dynamic system components like the WebSocket adapter need to be set at initialization time. The deinitialize function reverts the library into an unitialized state. That's useful for memory inspection tools like valgrind or to disable the OCPP communication. The loop function also counts as part of the lifecycle management. -- **Sensor Inputs**: EVSEs are mechanical systems with a variety of sensor information. OCPP is used to send parts of the sensor readings to the server. The other part of the sensor data flows into the local charger model of MicroOcpp where it is further processed. To update MicroOcpp with the input data from the sensors, the firmware needs to bind the sensors to the library. An *Input-binding*, or in short *Input*, is a function which transfers the current sensor value to MicroOcpp. Inputs are callback functions which read a specific sensor value and pass the value in the return statement. The firmware defines those callback functions for each sensor and adds them to MicroOcpp during initialization. After initialization, MicroOcpp uses the callbacks and executes them to fetch the most recent sensor values.
-This concept is reused for the data *Outputs* of the library to the firmware, where the callback applies output data from MicroOcpp to the firmware. -- **Transaction management**: OCPP considers EVSEs as vending machines. To enable payment processing and the billing of the EVSE usage, all charging activity is assigned to transactions. A big portion of OCPP is about transactions, their prerequisites, runtime and their termination scenarios. The MicroOcpp API breaks transactions down into an initiation and termination function and gives a transparent view on the current process status, authorization result and offline behavior strategy. For non-commercial setups, the transaction mechanism is the same but has only informational purposes. -- **Device management**: MicroOcpp implements the OCPP side of the device management operations. For the actual execution, the firmware needs to provide the charger-side implementations of the operations to MicroOcpp by passing handler functions to the API. For example, the OCPP server can restart the charger. Upon receipt of the request, MicroOcpp terminates the transactions and eventually triggers the system restart using the handler function which the firmware has provided through the high-level API. +- **Sensor Inputs**: EVSEs are mechanical systems with a variety of sensor information. OCPP is used to send parts of the sensor readings to the server. The other part of the sensor data flows into the local charger model of MicroOCPP where it is further processed. To update MicroOCPP with the input data from the sensors, the firmware needs to bind the sensors to the library. An *Input-binding*, or in short *Input*, is a function which transfers the current sensor value to MicroOCPP. Inputs are callback functions which read a specific sensor value and pass the value in the return statement. The firmware defines those callback functions for each sensor and adds them to MicroOCPP during initialization. After initialization, MicroOCPP uses the callbacks and executes them to fetch the most recent sensor values.
+This concept is reused for the data *Outputs* of the library to the firmware, where the callback applies output data from MicroOCPP to the firmware. +- **Transaction management**: OCPP considers EVSEs as vending machines. To enable payment processing and the billing of the EVSE usage, all charging activity is assigned to transactions. A big portion of OCPP is about transactions, their prerequisites, runtime and their termination scenarios. The MicroOCPP API breaks transactions down into an initiation and termination function and gives a transparent view on the current process status, authorization result and offline behavior strategy. For non-commercial setups, the transaction mechanism is the same but has only informational purposes. +- **Device management**: MicroOCPP implements the OCPP side of the device management operations. For the actual execution, the firmware needs to provide the charger-side implementations of the operations to MicroOCPP by passing handler functions to the API. For example, the OCPP server can restart the charger. Upon receipt of the request, MicroOCPP terminates the transactions and eventually triggers the system restart using the handler function which the firmware has provided through the high-level API. ## Transaction safety Software in EVSEs needs to withstand hazardous operating conditions. EVSEs are located on the street or in garages where the WiFi or LTE signal strength is often weak, leading to long offline periods or where random power cuts can occur. In addition to that, the lack of process virtualization on microcontrollers means that a malfunction in one part of the firmware leads to the crash of all other parts. -The transaction process of MicroOcpp is robust against random failures or resets. A minimal transaction log on the flash storage ensures that each operation on a transaction is fully executed. It will always result in a consistent state between the EVSE and the OCPP server, even over resets of the microcontroller. The RPC queue facilitates this by tracking the delivery status of relevant messages. If the microcontroller is reset while the delivery status of a message is unknown, MicroOcpp takes up the message delivery again at the next start up and completes it. +The transaction process of MicroOCPP is robust against random failures or resets. A minimal transaction log on the flash storage ensures that each operation on a transaction is fully executed. It will always result in a consistent state between the EVSE and the OCPP server, even over resets of the microcontroller. The RPC queue facilitates this by tracking the delivery status of relevant messages. If the microcontroller is reset while the delivery status of a message is unknown, MicroOCPP takes up the message delivery again at the next start up and completes it. A requirement for the transaction safety feature is the availability of a journaling file system. Examples include LittleFS, SPIFFS and the POSIX file API, but some microcontroller platforms don't support this natively, so an extension would be required. ## Unit testing -MicroOcpp includes a number of unit tests based on the [Catch2](https://github.com/catchorg/Catch2) framework. A [GitHub Action](https://github.com/matth-x/MicroOcpp/actions) runs the unit tests against each new commit in the MicroOcpp repository, which ensures that new features don't break old code. +MicroOCPP includes a number of unit tests based on the [Catch2](https://github.com/catchorg/Catch2) framework. A [GitHub Action](https://github.com/matth-x/MicroOcpp/actions) runs the unit tests against each new commit in the MicroOCPP repository, which ensures that new features don't break old code. The scope of the unit tests is to to ensure a correct implementation of OCPP and to validate the high-level API against its definition. For that, it is not necessary to establish an actual test connection to an OCPP server. In fact, real-world communication would disturb the tests and make them undeterministic. That's why the test suite is fully based on an integrated, tiny OCPP test server which the OCPP client reaches over a loopback connection. The test suite does not access the WebSocket library. When making the unit tests of the main firmware, it is not necessary to check the full OCPP communication, but only to validate correct usage of the high-level API. An example of how the library can be initialized with a loopback connection can be found in its test suite. ## Microcontroller optimization -As a library for microcontrollers, the design of MicroOcpp considers the strict memory limits and complies with the best practices of embedded software development. Also, a few measures were taken to optimize the memory usage which include the spare inclusion of external libraries, an optimization of the internal data structures and the exclusion of C++ run-time type information (RTTI) and exceptions. Features of C++ which may have a larger footprint are carefully used such as the standard template library (STL) and lambda functions. The STL increases the robustness of the code and lambdas prove to be a powerful tool to deal with the complexity of asynchronous data processing in embedded systems. That's also why the high-level API has many functional parameters. +As a library for microcontrollers, the design of MicroOCPP considers the strict memory limits and complies with the best practices of embedded software development. Also, a few measures were taken to optimize the memory usage which include the spare inclusion of external libraries, an optimization of the internal data structures and the exclusion of C++ run-time type information (RTTI) and exceptions. Features of C++ which may have a larger footprint are carefully used such as the standard template library (STL) and lambda functions. The STL increases the robustness of the code and lambdas prove to be a powerful tool to deal with the complexity of asynchronous data processing in embedded systems. That's also why the high-level API has many functional parameters. -Because of the high importance of C in the embedded world, MicroOcpp provides its high-level API in C too. It is typically simple to instruct the compiler to compile and link the C++-based library in a C-based firmware development. In case that the firmware requires custom features which are not part of the C-API, then the firmware can implement it in a new C++ source file, export the new functions to the C namespace and use it normally in the main source. +Because of the high importance of C in the embedded world, MicroOCPP provides its high-level API in C too. It is typically simple to instruct the compiler to compile and link the C++-based library in a C-based firmware development. In case that the firmware requires custom features which are not part of the C-API, then the firmware can implement it in a new C++ source file, export the new functions to the C namespace and use it normally in the main source. While memory constraints are of concern, the execution time generally is not. OCPP is rather uncomplex on the algorithmic side for clients, since there is no need for elaborate planning algorithms or complex data transformations. -Low resource requirements also allow new usage areas on top of EV charging. For example, MicroOcpp has been ported to ordinary IoT equipment such as Wi-Fi sockets to integrate further electric devices into OCPP networks. +Low resource requirements also allow new usage areas on top of EV charging. For example, MicroOCPP has been ported to ordinary IoT equipment such as Wi-Fi sockets to integrate further electric devices into OCPP networks. -Although MicroOcpp is optimized for the usage on microcontrollers, it is also suitable for embedded Linux systems. With more memory available, the upper limits of the internal data structures can be increased, leading to a more versatile support of charging use cases. Also, the separation of the charger firmware into multiple processes can lead to more robustness. MicroOcpp can be extended by an inter-process communication (IPC) interface to run in a separate process. +Although MicroOCPP is optimized for the usage on microcontrollers, it is also suitable for embedded Linux systems. With more memory available, the upper limits of the internal data structures can be increased, leading to a more versatile support of charging use cases. Also, the separation of the charger firmware into multiple processes can lead to more robustness. MicroOCPP can be extended by an inter-process communication (IPC) interface to run in a separate process. diff --git a/docs/migration.md b/docs/migration.md index 88a1cf66..8db223f8 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1,8 +1,25 @@ -# Migrating to v1.0 +# Migrating to v1.1 -The API has been continously improved to best suit the common use cases for MicroOcpp. Moreover, the project has been given a new name to prevent confusion with the relation to the Arduino platform and to reflect the project goals properly. With the new project name, the API has been frozen for the v1.0 release. +As a new minor version, all features should work the same as in v1.0 and existing integrations are mostly backwards compatible. However, some fixes / cleanup steps in MicroOCPP require syntactic changes or special consideration when upgrading from v1.0 to v1.1. The known pitfalls are as follows: -## Adopting the new project name in existing projects +- The default branch has been renamed from `master` into `main` +- Need to include extra headers: the transitive includes have been cleaned a bit. Probably it's necessary to add more includes next to `#include `. E.g.
`#include `
`#include ` +- `ocppPermitsCharge()` does not consider failures reported by the charger anymore. Before v1.1 it was possible to report failures to MicroOCPP using ErrorCodeInputs and then to rely on `ocppPermitsCharge()` becoming false when a failure occurs. For backwards compatibility, complement any occurence to `ocppPermitsCharge() && !isFaulted()` +- `setEnergyMeterInput` changed the expected return type of the callback function from `float` to `int` (see [#301](https://github.com/matth-x/MicroOcpp/pull/301)) +- The return type of the UnlockConnector handler also changed from `PollResult` to enum `UnlockConnectorResult` (see [#271](https://github.com/matth-x/MicroOcpp/pull/271)) + +If upgrading MicroOcppMongoose at the same time, then the following changes are very important to consider: + +- Certificates are no longer copied into heap memory, but the MO-Mongoose class takes the passed certificate pointer as a zero-copy parameter. The string behind the passed pointer must outlive the MO-Mongoose class (see [#10](https://github.com/matth-x/MicroOcppMongoose/pull/10)) +- WebSocket authorization keys are no longer stored as c-strings, but as `unsigned char` buffers. For backwards compatibility, a null-byte is still appended and the buffer can be accessed as c-string, but this should be tested in existing deployments. Furtermore, MicroOCPP only accepts hex-encoded keys coming via ChangeConfiguration which is mandated by the standard. This also may break existing deployments (see [#4](https://github.com/matth-x/MicroOcppMongoose/pull/4)). + +If accessing the MicroOCPP modules directly (i.e. not over `MicroOcpp.h` or `MicroOcpp_c.h`) then there are likely some more modifications to be done. See the history of pull requests where each change to the code is documented. However, if the existing integration compiles under the new MO version, then there shouldn't be too many unexpected incompatibilities. + +## Migrating to v1.0 + +The API has been continously improved to best suit the common use cases for MicroOCPP. Moreover, the project has been given a new name to prevent confusion with the relation to the Arduino platform and to reflect the project goals properly. With the new project name, the API has been frozen for the v1.0 release. + +### Adopting the new project name in existing projects Find and replace the keywords in the following. @@ -30,7 +47,7 @@ If using the C-facade, change this as the final step: - `ao_` to `ocpp_` -## Further API changes to consider +### Further API changes to consider In addition to the new project name, the API has also been reworked for more consistency. After renaming the existing project as described above, also take a look at the [changelogs](https://github.com/matth-x/MicroOcpp/blob/1.0.x/CHANGELOG.md) (see Section Changed for v1.0.0). diff --git a/docs/modules.md b/docs/modules.md index 5fa0a21b..47d2abe2 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -1,10 +1,10 @@ # Modules -This chapter gives an overview of the class structure of MicroOcpp. +This chapter gives an overview of the class structure of MicroOCPP. ## Context -The *Context* contains all runtime data of MicroOcpp. Every data object which this library creates is stored in the Context instance, except only the Configuration. So it is the basic entry point to the internals of the library. The structure of the context follows the main architecture as described in [this introduction](intro-tech) and consists of the Request queue and message deserializer for the RPC framework and the Model object for the OCPP model and behavior (see below). +The *Context* contains all runtime data of MicroOCPP. Every data object which this library creates is stored in the Context instance, except only the Configuration. So it is the basic entry point to the internals of the library. The structure of the context follows the main architecture as described in [this introduction](intro-tech) and consists of the Request queue and message deserializer for the RPC framework and the Model object for the OCPP model and behavior (see below). When the library is initialized, `getOcppContext()` returns the current Context object. @@ -12,7 +12,7 @@ When the library is initialized, `getOcppContext()` returns the current Context The *Model* represents the OCPP device model and behavior. OCPP defines a rough charger model, i.e. the hardware parts of the charger and their basic functionality in relation to the OCPP operations. Furthermore, OCPP specifies a few only software related features like the reservation of the charger. This charger model is implemented as straightforward C++ data structures and corresponding algorithms. -The implementation of the Model is structured into a top-level Model class and the subordinate Service classes. Each Service class represents a functional block of the OCPP specification and implements the corresponding data structures and functionality. The definition of the functional blocks in MicroOcpp is very similar to the feature profiles in OCPP. Only the Core profile is split into multiple functional blocks to keep a smaller module scope. +The implementation of the Model is structured into a top-level Model class and the subordinate Service classes. Each Service class represents a functional block of the OCPP specification and implements the corresponding data structures and functionality. The definition of the functional blocks in MicroOCPP is very similar to the feature profiles in OCPP. Only the Core profile is split into multiple functional blocks to keep a smaller module scope. The following list contains the resulting functional blocks: @@ -39,7 +39,7 @@ Every OCPP operation (e.g. Heartbeat, BootNotification) has a dedicated class fo To send operations to the OCPP server, they must be wrapped into a Request object. The RPC framework and operations are separated modules. While the RPC framework (including the Request class) deals with the messaging mechanism and transfering data to the other OCPP device, operations define the effect on the OCPP model and data structure and execute the desired action. The operation classes inherit from *Operation* which is the interface visible to the Request class. -Incoming messages are unmarshalled using the *OperationRegistry*. During the initialization phase of the library, the Model classes register all supported operations with their name and an instantiator. The instantiator, when executed, provides the Request interpreter with an instance of the corresponding Operation subclasses. It is possible to extend MicroOcpp by adding new Operation instantiators to the registry, or to modify the behavior by overriding the default Operation implementations. In addition to that, event handlers can be set which the RPC queue will notify with the payload once operations are sent or received. +Incoming messages are unmarshalled using the *OperationRegistry*. During the initialization phase of the library, the Model classes register all supported operations with their name and an instantiator. The instantiator, when executed, provides the Request interpreter with an instance of the corresponding Operation subclasses. It is possible to extend MicroOCPP by adding new Operation instantiators to the registry, or to modify the behavior by overriding the default Operation implementations. In addition to that, event handlers can be set which the RPC queue will notify with the payload once operations are sent or received. ## Configuration @@ -53,4 +53,4 @@ Configurations like the HeartbeatInterval are managed by the *Configuration* mod If another storage implementation is required (e.g. for syncing with an external configuration manager), then it's possible to add a custom ConfigurationContainer. -In the initialization phase, MicroOcpp loads the built-in Configurations with hard-coded factory defaults and a default storage structure. To customize the factory defaults or which ConfigurationContainers will be used, the Configuration module must be initialized before loading the library. To do so, call `configuration_init(...)`. Then the factory defaults can be applied by calling `declareConfiguration(...)` with the desired default value. To use a custom ConfigurationContainer, call `addConfigurationContainer(...)` with the custom implementation. When the library is loaded afterwards, it will use the previously provided Configurations / Containers and create only the data structure which hasn't been set already. +In the initialization phase, MicroOCPP loads the built-in Configurations with hard-coded factory defaults and a default storage structure. To customize the factory defaults or which ConfigurationContainers will be used, the Configuration module must be initialized before loading the library. To do so, call `configuration_init(...)`. Then the factory defaults can be applied by calling `declareConfiguration(...)` with the desired default value. To use a custom ConfigurationContainer, call `addConfigurationContainer(...)` with the custom implementation. When the library is loaded afterwards, it will use the previously provided Configurations / Containers and create only the data structure which hasn't been set already. diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 3abcd786..4cc63613 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -4,27 +4,27 @@ This page explains how to work with this library using the appropriate developme ## Development tools prerequisites -Throughout these document pages, it is assumed that you already have set up your development environment and that you are familiar with the corresponding building, flashing and (basic) debugging routines. MicroOcpp runs in many environments (from the Arduino-IDE to proprietary microcontroller IDEs like the Code Composer Studio). If you do not have any preferences yet, it is highly recommended to get started with VSCode + the PlatformIO add-on, since it is the favorite setup of the community and therefore you find the most related information in the Issues pages of the main repository. +Throughout these document pages, it is assumed that you already have set up your development environment and that you are familiar with the corresponding building, flashing and (basic) debugging routines. MicroOCPP runs in many environments (from the Arduino-IDE to proprietary microcontroller IDEs like the Code Composer Studio). If you do not have any preferences yet, it is highly recommended to get started with VSCode + the PlatformIO add-on, since it is the favorite setup of the community and therefore you find the most related information in the Issues pages of the main repository. There are many high-quality tutorials for out there for setting up VSCode + PIO. The following site covers everything you need to know: [https://randomnerdtutorials.com/vs-code-platformio-ide-esp32-esp8266-arduino/](https://randomnerdtutorials.com/vs-code-platformio-ide-esp32-esp8266-arduino/) -Once that's done, adding MicroOcpp is no big deal anymore. However, let's discuss another very important tool for your project first. +Once that's done, adding MicroOCPP is no big deal anymore. However, let's discuss another very important tool for your project first. ## OCPP Server prerequisites -MicroOcpp is just a client, but all the magic of OCPP lives in the communication between a client and a server. Although it *is* possible to run MicroOcpp without a real server for testing purposes, the best approach for getting started is to get the hands on a real server. So you can always use the client in a practical setup, see immediate results and simplify development a lot. +MicroOCPP is just a client, but all the magic of OCPP lives in the communication between a client and a server. Although it *is* possible to run MicroOCPP without a real server for testing purposes, the best approach for getting started is to get the hands on a real server. So you can always use the client in a practical setup, see immediate results and simplify development a lot. Perhaps you were already given access to an OCPP server for your project. Then you can use that, it should work fine. If you don't have a server already, it is highly recommended to get SteVe ([https://github.com/steve-community/steve](https://github.com/steve-community/steve)). It allows to control every detail of the OCPP operations and shows detail-rich information about the results. And again, it is the favorite test server of the community, so you will find the most related information on the Web. For the installation instructions, please refer to the [SteVe docs](https://github.com/steve-community/steve#configuration-and-installation). -In case you can't wait to get started, you can make the first connection test with a WebSocket echo server as a fake OCPP service. MicroOcpp supports that: it can send all messages to an echo server which reflects all traffic. MicroOcpp gets back its own messages and replies to itself with mocked responses. Complicated, but it does work and the console will show a valid OCPP communication. An example echo server is given in the following section. For the further development though, you will definitely need a real OCPP server. +In case you can't wait to get started, you can make the first connection test with a WebSocket echo server as a fake OCPP service. MicroOCPP supports that: it can send all messages to an echo server which reflects all traffic. MicroOCPP gets back its own messages and replies to itself with mocked responses. Complicated, but it does work and the console will show a valid OCPP communication. An example echo server is given in the following section. For the further development though, you will definitely need a real OCPP server. ## Project structure -MicroOcpp is a library, i.e. it is not a full firmware, but just solves one specific task in your project which is the OCPP connectivity. The project structure should reflect this: typically you download MicroOcpp into a libraries or dependencies subfolder, while the main part of the development takes place in a main source folder. All dependencies of MicroOcpp (i.e. ArduinoJson, see the dependencies sections) should be located in the same libraries or dependencies folder. +MicroOCPP is a library, i.e. it is not a full firmware, but just solves one specific task in your project which is the OCPP connectivity. The project structure should reflect this: typically you download MicroOCPP into a libraries or dependencies subfolder, while the main part of the development takes place in a main source folder. All dependencies of MicroOCPP (i.e. ArduinoJson, see the dependencies sections) should be located in the same libraries or dependencies folder. When the include paths are correctly set up, you should be able `#include ` at the top of your own source files. This setup keeps the OCPP library source separate from your integration and gives the project a clear structure. diff --git a/docs/security.md b/docs/security.md index 0537436a..d4838580 100644 --- a/docs/security.md +++ b/docs/security.md @@ -1,6 +1,6 @@ # Security -MicroOcpp is designed to be compatible with IoT devices which leads to special considerations regarding cyber security. This section describes the challenges and security concepts of MicroOcpp. +MicroOCPP is designed to be compatible with IoT devices which leads to special considerations regarding cyber security. This section describes the challenges and security concepts of MicroOCPP. ## Challenges of using microcontrollers in safety-critical environments @@ -15,33 +15,33 @@ Challenge 2) is due to the fact that OCPP uses standard web technology (WebSocke On the upside, an advantage of microcontrollers is their single purpose usage and thus, reduced complexity. Many security breaches are caused by misconfigured and often even superflous software components (e.g. due to overlooked open ports) which are not a regular part of a microcontroller firmware. -## Security measures of MicroOcpp +## Security measures of MicroOCPP To address the challenges, the following measures were taken: -- Input sanitazion: MicroOcpp only accepts the JSON format for all input. It is validated by ArduinoJson. Every JSON value is checked against the expected format and for conformity with the OCPP specification before using it. The JSON object is discarded immediately after interpretation +- Input sanitazion: MicroOCPP only accepts the JSON format for all input. It is validated by ArduinoJson. Every JSON value is checked against the expected format and for conformity with the OCPP specification before using it. The JSON object is discarded immediately after interpretation - Transaction safety: to address crashes and random reboots of the microcontroller during operation, all activities of the OCPP library are programmed so that they will either be resumed or fully reverted after reboots, preventing inconsistent states. See also [Transaction safety](../intro-tech/#transaction-safety) - Careful choice of the dependencies: the mandatory dependency, [ArduinoJson](https://github.com/bblanchon/ArduinoJson), has a test coverage of nearly 100% and is fuzzed. The same goes for the recommended WebSocket library, [Mongoose](https://github.com/cesanta/mongoose). Both projects are very relevant in their field with over 6k and 9k stars on GitHub Two further measures would be beneficial and could be requested via support request: - Precautious memory allocation: migrating memory management to the stack and where possible would simplify code analysis and reduce the potential of vulnerabilities -- OCPP fuzzer: as a stateful application protocol, there are specific challenges of developing a fuzzer. An open source fuzzing framework for OCPP could reveal vulnerabilities and be of use for other OCPP projects as well. MicroOcpp is a good foundation for trying new fuzzing approaches. The exposure of the main-loop function and the clock allow a fine-grained access to the program flow and facilitating random alterations of the environment conditions. Furthermore, all persistent data is stored in the JSON format and it is possible to develop a grammatic which contains both a device status and incoming OCPP messages. The Configuration interface could be reused for further status variables which don't need to be persistent in practice, but would improve fuzzing performance when being accessible by the fuzzer. -- Memory pool: object orientation is a very helpful programming paradigm for OCPP. The standard contains a lot of polymorphic entities and optional or variable length data fields. MicroOcpp makes use of the heap and allocates new chunks of memory as the device model is populated with data. On the upside this allows to save a lot of memory during normal operation, but it also entails the risk of memory depletion of the whole controller. A fixed memory pool for OCPP would encapsulate the heap usage to a certain address space and set a hard limit for the memory consumption and avoid polluting the shared heap area by heap fragmentation. To realize the memory pool, it would be necessary to make the allocate and deallocate functions configurable by the client code. Then appropriate (de)allocators can be injected limiting the memory use to a restricted address area. As a consequence, a more thorough allocation error handling in the MicroOcpp code is required and test cases which randomly suppress allocations to test if the library always reverts to a consistent state. A less invasive alternative to memory pools is to inject measured (de)allocators which just prevent the allocation of new memory chunks after a certain threshold has been exceeded. This programming technique would also allow to create much more fine-grained benchmarks of the library. +- OCPP fuzzer: as a stateful application protocol, there are specific challenges of developing a fuzzer. An open source fuzzing framework for OCPP could reveal vulnerabilities and be of use for other OCPP projects as well. MicroOCPP is a good foundation for trying new fuzzing approaches. The exposure of the main-loop function and the clock allow a fine-grained access to the program flow and facilitating random alterations of the environment conditions. Furthermore, all persistent data is stored in the JSON format and it is possible to develop a grammatic which contains both a device status and incoming OCPP messages. The Configuration interface could be reused for further status variables which don't need to be persistent in practice, but would improve fuzzing performance when being accessible by the fuzzer. +- Memory pool: object orientation is a very helpful programming paradigm for OCPP. The standard contains a lot of polymorphic entities and optional or variable length data fields. MicroOCPP makes use of the heap and allocates new chunks of memory as the device model is populated with data. On the upside this allows to save a lot of memory during normal operation, but it also entails the risk of memory depletion of the whole controller. A fixed memory pool for OCPP would encapsulate the heap usage to a certain address space and set a hard limit for the memory consumption and avoid polluting the shared heap area by heap fragmentation. To realize the memory pool, it would be necessary to make the allocate and deallocate functions configurable by the client code. Then appropriate (de)allocators can be injected limiting the memory use to a restricted address area. As a consequence, a more thorough allocation error handling in the MicroOCPP code is required and test cases which randomly suppress allocations to test if the library always reverts to a consistent state. A less invasive alternative to memory pools is to inject measured (de)allocators which just prevent the allocation of new memory chunks after a certain threshold has been exceeded. This programming technique would also allow to create much more fine-grained benchmarks of the library. ## Measures to be taken by the EVSE vendor As a general rule, the communication controller which is exposed to the internet shouldn't be used for safety-critical tasks on the charging hardware. That's because the networking stack is a very complex piece of software which very likely still has open bugs which can crash the controller despite all the effort to improve it. Safety-critical tasks on the charging hardware shouldn't rely on a controller which could crash at any time because of incoming network traffic. To mitigate this, either the OCPP library and internet functionality should be placed onto a separate chip, or the most vital safety functionality should get a dedicated controller. -The recommended [Mongoose WebSocket adapter for MicroOcpp](https://github.com/matth-x/MicroOcppMongoose) supports the OCPP Security Profile 2 (TLS with Basic Authentication) and needs to be provided with the necessary TLS certificate. +The recommended [Mongoose WebSocket adapter for MicroOCPP](https://github.com/matth-x/MicroOcppMongoose) supports the OCPP Security Profile 2 (TLS with Basic Authentication) and needs to be provided with the necessary TLS certificate. -Most IoT-controllers have built-in mechanisms to ensure the authenticity of their firmware. For example, the Espressif32 supports [Secure Boot](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/secure-boot-v2.html) which is a signature verification of the installed firmware before that firmware is executed. Many platforms also have a built-in signature verification for incoming OTA firmware updates. To prove the authenticity of the charger to the OCPP server, it is also important to keep the WebSocket key secret by [encrypting the flash memory](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/flash-encryption.html). These security mechanisms heavily depend on the host controller which runs MicroOcpp. It is the responsibility of the main firmware to make proper use of them. +Most IoT-controllers have built-in mechanisms to ensure the authenticity of their firmware. For example, the Espressif32 supports [Secure Boot](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/secure-boot-v2.html) which is a signature verification of the installed firmware before that firmware is executed. Many platforms also have a built-in signature verification for incoming OTA firmware updates. To prove the authenticity of the charger to the OCPP server, it is also important to keep the WebSocket key secret by [encrypting the flash memory](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/flash-encryption.html). These security mechanisms heavily depend on the host controller which runs MicroOCPP. It is the responsibility of the main firmware to make proper use of them. ## OCPP Security Whitepaper and ISO 15118 -With MicroOcpp, the recommended way of handling certificates on microcontrollers is to compile them into the firmware binary and to rely on the built-in firmware signature checks of the host microcontroller platform. This lean approach results in a smaller attack vector compared to establishing a separate infrastructure for the server- and firmware certificate. It can be assumed that the OTA functionality of the microcontrollers is thoroughly tested and consequently, reaching a comparable level of robustness would require much effort. +With MicroOCPP, the recommended way of handling certificates on microcontrollers is to compile them into the firmware binary and to rely on the built-in firmware signature checks of the host microcontroller platform. This lean approach results in a smaller attack vector compared to establishing a separate infrastructure for the server- and firmware certificate. It can be assumed that the OTA functionality of the microcontrollers is thoroughly tested and consequently, reaching a comparable level of robustness would require much effort. -In case the certificate handling mechanism of the Security Whitepaper is preferred, then the EVSE vendor needs to implement it via a custom extension. Unfortunately, this mechanism hasn't been requested yet and is not natively supported by MicroOcpp yet. The new custom operations can be implemented by extending the class `Operation`. A handler for incoming messages can be registered via `OperationRegistry::registerOperation(...)`. To send custom messages to the server, use `Context::initiateRequest(...)`. +In case the certificate handling mechanism of the Security Whitepaper is preferred, then the EVSE vendor needs to implement it via a custom extension. Unfortunately, this mechanism hasn't been requested yet and is not natively supported by MicroOCPP yet. The new custom operations can be implemented by extending the class `Operation`. A handler for incoming messages can be registered via `OperationRegistry::registerOperation(...)`. To send custom messages to the server, use `Context::initiateRequest(...)`. A further challenge for microcontrollers is the relatively low processor speed which becomes relevant for a potential ISO 15118 integration. Some incoming message types (`AuthorizationReq` and `MeteringReceiptReq`) include a signature which needs to be verified on the communications controller of the EVSE. Moreover, messages in the ISO 15118 V2G protocol have a maximum round trip time (which is 2 seconds for the message types in question) and so the signature verification is time-contrained. [These benchmarks](https://web.archive.org/web/20230724184529/https://www.oryx-embedded.com/benchmark/espressif/crypto-esp32.html) for the Espressif32 show that for some signature algorithms, the verification time can get close or exceed the timing requirements of ISO 15118 if done on the processor only. As a consequence, hardware acceleration by the crypto-core is mandatory to ensure a robust communication between the EVSE and EV. Before making a communications controller with ISO 15118 support, the performance of the host controller should be benchmarked and checked against the requirements. diff --git a/examples/ESP-IDF/README.md b/examples/ESP-IDF/README.md index 702e21ae..7b6a8df0 100644 --- a/examples/ESP-IDF/README.md +++ b/examples/ESP-IDF/README.md @@ -11,7 +11,7 @@ Please clone the following repositories into the respective components-directori - [MicroOcpp](https://github.com/matth-x/MicroOcpp) into `components/MicroOcpp` - [Mongoose (ESP-IDF integration)](https://github.com/cesanta/mongoose-esp-idf) into `components/mongoose` - [Mongoose adapter for MicroOcpp](https://github.com/matth-x/MicroOcppMongoose) into `components/MicroOcppMongoose` -- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) into `components/ArduinoJson` +- [ArduinoJson (v6.21)](https://github.com/bblanchon/ArduinoJson) into `components/ArduinoJson` For setup, the following commands could come handy (change to the root directory of the ESP-IDF project first): @@ -24,6 +24,8 @@ git clone https://github.com/matth-x/MicroOcpp components/MicroOcpp git clone --recurse-submodules https://github.com/cesanta/mongoose-esp-idf.git components/mongoose git clone https://github.com/matth-x/MicroOcppMongoose components/MicroOcppMongoose git clone https://github.com/bblanchon/ArduinoJson components/ArduinoJson +cd components/ArduinoJson +git checkout 3e1be980d93e47b2a0073efeeb9a9396fd7a83be ``` The setup is done if the following include statements work: diff --git a/examples/ESP-IDF/components/README.md b/examples/ESP-IDF/components/README.md index 67d997d3..b4acfa92 100644 --- a/examples/ESP-IDF/components/README.md +++ b/examples/ESP-IDF/components/README.md @@ -5,6 +5,6 @@ The ESP-IDF integration requires at least the following components: - [MicroOcpp](https://github.com/matth-x/MicroOcpp) - [Mongoose (ESP-IDF integration)](https://github.com/cesanta/mongoose-esp-idf) - [Mongoose adapter for MicroOcpp](https://github.com/matth-x/MicroOcppMongoose) -- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) +- [ArduinoJson (v6.21)](https://github.com/bblanchon/ArduinoJson) This example only provides the folder structure. You need to clone every project into it in order to run the example. diff --git a/examples/ESP-IDF/main/main.c b/examples/ESP-IDF/main/main.c index 97b6e5b7..b0911702 100644 --- a/examples/ESP-IDF/main/main.c +++ b/examples/ESP-IDF/main/main.c @@ -160,7 +160,7 @@ void app_main(void) EXAMPLE_MO_OCPP_BACKEND, EXAMPLE_MO_CHARGEBOXID, EXAMPLE_MO_AUTHORIZATIONKEY, "", fsopt); - ocpp_initialize(osock, "ESP-IDF charger", "Your brand name here", fsopt, false); + ocpp_initialize(osock, "ESP-IDF charger", "Your brand name here", fsopt, false, false); /* Enter infinite loop */ while (1) { diff --git a/examples/ESP-TLS/main.cpp b/examples/ESP-TLS/main.cpp index 5a4a6d74..7c4c7c1b 100644 --- a/examples/ESP-TLS/main.cpp +++ b/examples/ESP-TLS/main.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -14,25 +14,18 @@ ESP8266WiFiMulti WiFiMulti; #endif #include -#include //need for setting TLS credentials -#define STASSID "YOUR_WIFI_SSID" -#define STAPSK "YOUR_WIFI_PW" +#define STASSID "YOUR_WIFI_SSID" +#define STAPSK "YOUR_WIFI_PW" -#define OCPP_HOST "echo.websocket.events" -#define OCPP_PORT 443 -#define OCPP_URL "wss://echo.websocket.events/" +#define OCPP_BACKEND_URL "wss://echo.websocket.events" +#define OCPP_CHARGE_BOX_ID "" +#define OCPP_AUTH_KEY "SecureAuthKey" // OCPP Security Profile 2: TLS with Basic Authentication /* - * OCPP Security Profile 2: TLS with Basic Authentication - * - * Example credentials from the OCPP-JSON document (p. 16) + * ISRG ROOT X1 */ -#define OCPP_AUTH_ID "AL1000" -#define OCPP_AUTH_KEY "0001020304050607FFFFFFFFFFFFFFFFFFFFFFFF" - -const char ENDPOINT_CA_CERT[] PROGMEM = R"EOF( ------BEGIN CERTIFICATE----- +const char ca_cert[] PROGMEM = R"EOF(-----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 @@ -65,9 +58,6 @@ emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= -----END CERTIFICATE----- )EOF"; -WebSocketsClient wsockSecure {}; -MicroOcpp::EspWiFi::WSClient osockSecure {&wsockSecure}; - void setup() { /* @@ -76,7 +66,7 @@ void setup() { Serial.begin(115200); - Serial.print(F("[main] Wait for WiFi ")); + Serial.print(F("[main] Wait for WiFi: ")); #if defined(ESP8266) WiFiMulti.addAP(STASSID, STAPSK); @@ -87,12 +77,14 @@ void setup() { #elif defined(ESP32) WiFi.begin(STASSID, STAPSK); while (!WiFi.isConnected()) { - delay(1000); Serial.print('.'); + delay(1000); } +#else +#error only ESP32 or ESP8266 supported at the moment #endif - Serial.print(F(" connected\n")); + Serial.println(F(" connected!")); /* * Set system time (required for Certificate validation) @@ -108,23 +100,22 @@ void setup() { Serial.printf(" finished. Unix timestamp is %lu\n", now); /* - * Connect to OCPP Central System (using OCPP Security Profile 2: TLS with Basic Authentication ) + * Initialize the OCPP library (using OCPP Security Profile 2: TLS with Basic Authentication) */ - wsockSecure.beginSslWithCA(OCPP_HOST, - OCPP_PORT, - OCPP_URL, - ENDPOINT_CA_CERT, "ocpp1.6"); - wsockSecure.setReconnectInterval(5000); - wsockSecure.enableHeartbeat(15000, 3000, 2); - wsockSecure.setAuthorization(OCPP_AUTH_ID, OCPP_AUTH_KEY); // => Authorization: Basic QUwxMDAwOgABAgMEBQYH//////////////// - - mocpp_initialize(osockSecure, ChargerCredentials("My Charging Station", "My company name")); + mocpp_initialize( + OCPP_BACKEND_URL, + OCPP_CHARGE_BOX_ID, + "My Charging Station", + "My company name", + MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail, + OCPP_AUTH_KEY, + ca_cert); /* * ... see MicroOcpp.h for how to integrate the EVSE hardware. * * This example only showcases the TLS connection. For examples about the HW integration, - * please see the other examples + * see the other examples */ } diff --git a/examples/ESP/main.cpp b/examples/ESP/main.cpp index b9f46a7b..306e700b 100644 --- a/examples/ESP/main.cpp +++ b/examples/ESP/main.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include diff --git a/library.json b/library.json index 37b51377..ec1b8a1e 100644 --- a/library.json +++ b/library.json @@ -1,8 +1,8 @@ { "name": "MicroOcpp", - "version": "1.0.0", - "description": "OCPP 1.6 Client for microcontrollers", - "keywords": "OCPP, 1.6, OCPP 1.6, Smart Energy, Smart Charging, client, ESP8266, ESP32, Arduino, esp-idf, EVSE, Charge Point", + "version": "1.2.0", + "description": "OCPP 1.6 / 2.0.1 Client for microcontrollers", + "keywords": "OCPP, 1.6, OCPP 1.6, OCPP 2.0.1, Smart Energy, Smart Charging, client, ESP8266, ESP32, Arduino, esp-idf, EVSE, Charge Point", "repository": { "type": "git", @@ -36,17 +36,17 @@ "export": { "include": [ - "src/*", + "docs/*", "examples/*", - "platformio.ini", + "src/*", + "CHANGELOG.md", + "CMakeLists.txt", "library.json", "library.properties", - "README.md", - "CMakeLists.txt", - "docs/*", "LICENSE", "mkdocs.yml", - "CHANGELOG.md" + "platformio.ini", + "README.md" ] }, diff --git a/library.properties b/library.properties index b9c4637e..8a1a3e01 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=MicroOcpp -version=1.0.0 +version=1.2.0 author=Matthias Akstaller maintainer=Matthias Akstaller sentence=OCPP 1.6 Client for microcontrollers diff --git a/mkdocs.yml b/mkdocs.yml index 60c415fa..22027d12 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: MicroOcpp docs +site_name: MicroOCPP docs theme: name: material @@ -48,3 +48,8 @@ theme: extra_css: - stylesheets/extra.css + +plugins: + - search + - table-reader: + data_path: "docs/assets/tables" diff --git a/platformio.ini b/platformio.ini index 9e02cd14..b5660ac8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,5 +1,5 @@ ; matth-x/MicroOcpp -; Copyright Matthias Akstaller 2019 - 2023 +; Copyright Matthias Akstaller 2019 - 2024 ; MIT License [platformio] diff --git a/src/MicroOcpp.cpp b/src/MicroOcpp.cpp index 7d23f5a3..5dbd3714 100644 --- a/src/MicroOcpp.cpp +++ b/src/MicroOcpp.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include "MicroOcpp.h" @@ -17,10 +17,18 @@ #include #include #include -#include +#include +#include +#include +#include +#include +#include +#include #include #include #include +#include +#include #include #include @@ -28,7 +36,6 @@ #include #include -#include namespace MicroOcpp { namespace Facade { @@ -51,6 +58,12 @@ std::shared_ptr filesystem; } //end namespace MicroOcpp::Facade } //end namespace MicroOcpp +#if MO_ENABLE_HEAP_PROFILER +#ifndef MO_HEAP_PROFILER_EXTERNAL_CONTROL +#define MO_HEAP_PROFILER_EXTERNAL_CONTROL 0 //enable if you want to manually reset the heap profiler (e.g. for keeping stats over multiple MO lifecycles) +#endif +#endif + using namespace MicroOcpp; using namespace MicroOcpp::Facade; using namespace MicroOcpp::Ocpp16; @@ -74,7 +87,7 @@ void mocpp_initialize(const char *backendUrl, const char *chargeBoxId, const cha /* * parse backendUrl so that it suits the links2004/arduinoWebSockets interface */ - std::string url = backendUrl; + auto url = makeString("MicroOcpp.cpp", backendUrl); //tolower protocol specifier for (auto c = url.begin(); *c != ':' && c != url.end(); c++) { @@ -92,15 +105,16 @@ void mocpp_initialize(const char *backendUrl, const char *chargeBoxId, const cha } //parse host, port - std::string host_port_path = url.substr(url.find_first_of("://") + strlen("://")); - std::string host_port = host_port_path.substr(0, host_port_path.find_first_of('/')); - std::string host = host_port.substr(0, host_port.find_first_of(':')); + auto host_port_path = url.substr(url.find_first_of("://") + strlen("://")); + auto host_port = host_port_path.substr(0, host_port_path.find_first_of('/')); + auto path = host_port_path.substr(host_port.length()); + auto host = host_port.substr(0, host_port.find_first_of(':')); if (host.empty()) { MO_DBG_ERR("could not parse host: %s", url.c_str()); return; } uint16_t port = 0; - std::string port_str = host_port.substr(host.length()); + auto port_str = host_port.substr(host.length()); if (port_str.empty()) { port = isTLS ? 443U : 80U; } else { @@ -120,28 +134,29 @@ void mocpp_initialize(const char *backendUrl, const char *chargeBoxId, const cha } } + if (path.empty()) { + path = "/"; + } + if ((!*chargeBoxId) == '\0') { - if (url.back() != '/') { - url += '/'; + if (path.back() != '/') { + path += '/'; } - url += chargeBoxId; + path += chargeBoxId; } - MO_DBG_INFO("connecting to %s -- (host: %s, port: %u)", url.c_str(), host.c_str(), port); + 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, URL and TLS certificate - webSocket->beginSslWithCA(host.c_str(), port, url.c_str(), CA_cert, "ocpp1.6"); - } - else - { - // server address, port, URL - webSocket->begin(host.c_str(), port, url.c_str(), "ocpp1.6"); + if (isTLS) { + // server address, port, path and TLS certificate + webSocket->beginSslWithCA(host.c_str(), port, path.c_str(), CA_cert, "ocpp1.6"); + } else { + // server address, port, path + webSocket->begin(host.c_str(), port, path.c_str(), "ocpp1.6"); } // try ever 5000 again if connection has failed @@ -199,7 +214,39 @@ ChargerCredentials::ChargerCredentials(const char *cpModel, const char *cpVendor } } -void mocpp_initialize(Connection& connection, const char *bootNotificationCredentials, std::shared_ptr fs, bool autoRecover) { +ChargerCredentials ChargerCredentials::v201(const char *cpModel, const char *cpVendor, const char *fWv, const char *cpSNr, const char *meterSNr, const char *meterType, const char *cbSNr, const char *iccid, const char *imsi) { + + ChargerCredentials res; + + StaticJsonDocument<512> creds; + if (cpSNr) + creds["serialNumber"] = cpSNr; + if (cpModel) + creds["model"] = cpModel; + if (cpVendor) + creds["vendorName"] = cpVendor; + if (fWv) + creds["firmwareVersion"] = fWv; + if (iccid) + creds["modem"]["iccid"] = iccid; + if (imsi) + creds["modem"]["imsi"] = imsi; + + if (creds.overflowed()) { + MO_DBG_ERR("Charger Credentials too long"); + } + + size_t written = serializeJson(creds, res.payload, 512); + + if (written < 2) { + MO_DBG_ERR("Charger Credentials could not be written"); + sprintf(res.payload, "{}"); + } + + return res; +} + +void mocpp_initialize(Connection& connection, const char *bootNotificationCredentials, std::shared_ptr fs, bool autoRecover, MicroOcpp::ProtocolVersion version) { if (context) { MO_DBG_WARN("already initialized. To reinit, call mocpp_deinitialize() before"); return; @@ -214,86 +261,122 @@ void mocpp_initialize(Connection& connection, const char *bootNotificationCreden BootService::loadBootStats(filesystem, bootstats); if (autoRecover && bootstats.getBootFailureCount() > 3) { - MO_DBG_ERR("multiple initialization failures detected"); - if (filesystem) { - bool 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-")) || - !strncmp(fname, "reservation", strlen("reservation")); - }); - MO_DBG_ERR("clear local state files (recovery): %s", success ? "success" : "not completed"); - - bootstats = BootStats(); - } + BootService::recover(filesystem, bootstats); + bootstats = BootStats(); } + BootService::migrate(filesystem, bootstats); + bootstats.bootNr++; //assign new boot number to this run BootService::storeBootStats(filesystem, bootstats); configuration_init(filesystem); //call before each other library call - context = new Context(connection, filesystem, bootstats.bootNr); + context = new Context(connection, filesystem, bootstats.bootNr, version); + +#if MO_ENABLE_MBEDTLS + context->setFtpClient(makeFtpClientMbedTLS()); +#endif //MO_ENABLE_MBEDTLS + auto& model = context->getModel(); - model.setTransactionStore(std::unique_ptr( - new TransactionStore(MO_NUMCONNECTORS, filesystem))); model.setBootService(std::unique_ptr( new BootService(*context, filesystem))); - model.setConnectorsCommon(std::unique_ptr( - new ConnectorsCommon(*context, MO_NUMCONNECTORS, filesystem))); - std::vector> connectors; - for (unsigned int connectorId = 0; connectorId < MO_NUMCONNECTORS; connectorId++) { - connectors.emplace_back(new Connector(*context, connectorId)); + +#if MO_ENABLE_V201 + if (version.major == 2) { + model.setAvailabilityService(std::unique_ptr( + new AvailabilityService(*context, MO_NUM_EVSEID))); + model.setVariableService(std::unique_ptr( + new VariableService(*context, filesystem))); + model.setTransactionService(std::unique_ptr( + new TransactionService(*context, filesystem, MO_NUM_EVSEID))); + model.setRemoteControlService(std::unique_ptr( + new RemoteControlService(*context, MO_NUM_EVSEID))); + model.setResetServiceV201(std::unique_ptr( + new Ocpp201::ResetService(*context))); + } else +#endif + { + model.setTransactionStore(std::unique_ptr( + new TransactionStore(MO_NUMCONNECTORS, filesystem))); + model.setConnectorsCommon(std::unique_ptr( + new ConnectorsCommon(*context, MO_NUMCONNECTORS, filesystem))); + auto connectors = makeVector>("v16.ConnectorBase.Connector"); + for (unsigned int connectorId = 0; connectorId < MO_NUMCONNECTORS; connectorId++) { + connectors.emplace_back(new Connector(*context, filesystem, connectorId)); + } + model.setConnectors(std::move(connectors)); + +#if MO_ENABLE_LOCAL_AUTH + model.setAuthorizationService(std::unique_ptr( + new AuthorizationService(*context, filesystem))); +#endif //MO_ENABLE_LOCAL_AUTH + +#if MO_ENABLE_RESERVATION + model.setReservationService(std::unique_ptr( + new ReservationService(*context, MO_NUMCONNECTORS))); +#endif + + model.setResetService(std::unique_ptr( + new ResetService(*context))); } - model.setConnectors(std::move(connectors)); + model.setHeartbeatService(std::unique_ptr( new HeartbeatService(*context))); - model.setAuthorizationService(std::unique_ptr( - new AuthorizationService(*context, filesystem))); - model.setReservationService(std::unique_ptr( - new ReservationService(*context, MO_NUMCONNECTORS))); - model.setResetService(std::unique_ptr( - new ResetService(*context))); - -#if !defined(MO_CUSTOM_UPDATER) && !defined(MO_CUSTOM_WS) - model.setFirmwareService(std::unique_ptr( - EspWiFi::makeFirmwareService(*context))); //instantiate FW service + ESP installation routine -#else - model.setFirmwareService(std::unique_ptr( - new FirmwareService(*context))); //only instantiate FW service -#endif -#if !defined(MO_CUSTOM_DIAGNOSTICS) && !defined(MO_CUSTOM_WS) - model.setDiagnosticsService(std::unique_ptr( - EspWiFi::makeDiagnosticsService(*context))); //will only return "Rejected" because client needs to implement logging -#else - model.setDiagnosticsService(std::unique_ptr( - new DiagnosticsService(*context))); +#if MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS + std::unique_ptr certStore = makeCertificateStoreMbedTLS(filesystem); + if (certStore) { + model.setCertificateService(std::unique_ptr( + new CertificateService(*context))); + } + if (certStore && model.getCertificateService()) { + model.getCertificateService()->setCertificateStore(std::move(certStore)); + } #endif +#if !defined(MO_CUSTOM_UPDATER) +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS + model.setFirmwareService( + makeDefaultFirmwareService(*context)); //instantiate FW service + ESP installation routine +#elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP8266) + model.setFirmwareService( + makeDefaultFirmwareService(*context)); //instantiate FW service + ESP installation routine +#endif //MO_PLATFORM +#endif //!defined(MO_CUSTOM_UPDATER) + +#if !defined(MO_CUSTOM_DIAGNOSTICS) +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS + model.setDiagnosticsService( + makeDefaultDiagnosticsService(*context, filesystem)); //instantiate Diag service + ESP hardware diagnostics +#elif MO_ENABLE_MBEDTLS + model.setDiagnosticsService( + makeDefaultDiagnosticsService(*context, filesystem)); //instantiate Diag service +#endif //MO_PLATFORM +#endif //!defined(MO_CUSTOM_DIAGNOSTICS) + #if MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) - if (!model.getResetService()->getExecuteReset()) - model.getResetService()->setExecuteReset(makeDefaultResetFn()); + setOnResetExecute(makeDefaultResetFn()); #endif model.getBootService()->setChargePointCredentials(bootNotificationCredentials); auto credsJson = model.getBootService()->getChargePointCredentials(); - if (credsJson && credsJson->containsKey("firmwareVersion")) { + if (model.getFirmwareService() && credsJson && credsJson->containsKey("firmwareVersion")) { model.getFirmwareService()->setBuildNumber((*credsJson)["firmwareVersion"]); } credsJson.reset(); - auto mocppVersion = declareConfiguration("MicroOcppVersion", MO_VERSION, MO_KEYVALUE_FN, false, false, false); - configuration_load(); - if (mocppVersion) { - mocppVersion->setString(MO_VERSION); +#if MO_ENABLE_V201 + if (version.major == 2) { + model.getVariableService()->load(); } - MO_DBG_INFO("initialized MicroOcpp v" MO_VERSION); +#endif //MO_ENABLE_V201 + + MO_DBG_INFO("initialized MicroOcpp v" MO_VERSION " running OCPP %i.%i.%i", version.major, version.minor, version.patch); } void mocpp_deinitialize() { @@ -323,6 +406,10 @@ void mocpp_deinitialize() { configuration_deinit(); +#if !MO_HEAP_PROFILER_EXTERNAL_CONTROL + MO_MEM_DEINIT(); +#endif + MO_DBG_DEBUG("deinitialized OCPP\n"); } @@ -335,41 +422,79 @@ void mocpp_loop() { context->loop(); } -std::shared_ptr beginTransaction(const char *idTag, unsigned int connectorId) { +bool beginTransaction(const char *idTag, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return nullptr; + return false; + } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (!idTag || strnlen(idTag, MO_IDTOKEN_LEN_MAX + 2) > MO_IDTOKEN_LEN_MAX) { + MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", MO_IDTOKEN_LEN_MAX); + return false; + } + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->beginAuthorization(idTag, true); } + #endif + if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX) { MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); - return nullptr; + return false; } auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); - return nullptr; + return false; } - return connector->beginTransaction(idTag); + return connector->beginTransaction(idTag) != nullptr; } -std::shared_ptr beginTransaction_authorized(const char *idTag, const char *parentIdTag, unsigned int connectorId) { +bool beginTransaction_authorized(const char *idTag, const char *parentIdTag, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before - return nullptr; + return false; + } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (!idTag || strnlen(idTag, MO_IDTOKEN_LEN_MAX + 2) > MO_IDTOKEN_LEN_MAX) { + MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", MO_IDTOKEN_LEN_MAX); + return false; + } + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->beginAuthorization(idTag, false); } + #endif + if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX || (parentIdTag && strnlen(parentIdTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX)) { MO_DBG_ERR("(parent)idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); - return nullptr; + return false; } auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); - return nullptr; + return false; } - return connector->beginTransaction_authorized(idTag, parentIdTag); + return connector->beginTransaction_authorized(idTag, parentIdTag) != nullptr; } bool endTransaction(const char *idTag, const char *reason, unsigned int connectorId) { @@ -377,14 +502,75 @@ bool endTransaction(const char *idTag, const char *reason, unsigned int connecto MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (!idTag || strnlen(idTag, MO_IDTOKEN_LEN_MAX + 2) > MO_IDTOKEN_LEN_MAX) { + MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", MO_IDTOKEN_LEN_MAX); + return false; + } + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->endAuthorization(idTag, true); + } + #endif + bool res = false; if (isTransactionActive(connectorId) && getTransactionIdTag(connectorId)) { //end transaction now if either idTag is nullptr (i.e. force stop) or the idTag matches beginTransaction - if (!idTag || !strcmp(idTag, getTransactionIdTag())) { + if (!idTag || !strcmp(idTag, getTransactionIdTag(connectorId))) { res = endTransaction_authorized(idTag, reason, connectorId); } else { - MO_DBG_INFO("endTransaction: idTag doesn't match"); - (void)0; + auto tx = getTransaction(connectorId); + const char *parentIdTag = tx->getParentIdTag(); + if (strlen(parentIdTag) > 0) + { + // We have a parent ID tag, so we need to check if this new card also has one + auto authorize = makeRequest(new Ocpp16::Authorize(context->getModel(), idTag)); + auto idTag_capture = makeString("MicroOcpp.cpp", idTag); + auto reason_capture = makeString("MicroOcpp.cpp", reason ? reason : ""); + authorize->setOnReceiveConfListener([idTag_capture, reason_capture, connectorId, tx] (JsonObject response) { + JsonObject idTagInfo = response["idTagInfo"]; + + if (strcmp("Accepted", idTagInfo["status"] | "UNDEFINED")) { + //Authorization rejected, do nothing + MO_DBG_DEBUG("Authorize rejected (%s), continue transaction", idTag_capture.c_str()); + auto connector = context->getModel().getConnector(connectorId); + if (connector) { + connector->updateTxNotification(TxNotification_AuthorizationRejected); + } + return; + } + if (idTagInfo.containsKey("parentIdTag") && !strcmp(idTagInfo["parenIdTag"], tx->getParentIdTag())) + { + endTransaction_authorized(idTag_capture.c_str(), reason_capture.empty() ? (const char*)nullptr : reason_capture.c_str(), connectorId); + } + }); + + authorize->setOnTimeoutListener([idTag_capture, connectorId] () { + //Authorization timed out, do nothing + MO_DBG_DEBUG("Authorization timeout (%s), continue transaction", idTag_capture.c_str()); + auto connector = context->getModel().getConnector(connectorId); + if (connector) { + connector->updateTxNotification(TxNotification_AuthorizationTimeout); + } + }); + + auto authorizationTimeoutInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", 20); + authorize->setTimeout(authorizationTimeoutInt && authorizationTimeoutInt->getInt() > 0 ? authorizationTimeoutInt->getInt() * 1000UL : 20UL * 1000UL); + + context->initiateRequest(std::move(authorize)); + res = true; + } else { + MO_DBG_INFO("endTransaction: idTag doesn't match"); + (void)0; + } } } return res; @@ -395,6 +581,25 @@ bool endTransaction_authorized(const char *idTag, const char *reason, unsigned i MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (!idTag || strnlen(idTag, MO_IDTOKEN_LEN_MAX + 2) > MO_IDTOKEN_LEN_MAX) { + MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", MO_IDTOKEN_LEN_MAX); + return false; + } + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->endAuthorization(idTag, false); + } + #endif + auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); @@ -410,6 +615,21 @@ bool isTransactionActive(unsigned int connectorId) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->getTransaction() && evse->getTransaction()->active; + } + #endif + auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); @@ -424,6 +644,21 @@ bool isTransactionRunning(unsigned int connectorId) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return false; } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->getTransaction() && evse->getTransaction()->started && !evse->getTransaction()->stopped; + } + #endif + auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); @@ -438,6 +673,21 @@ const char *getTransactionIdTag(unsigned int connectorId) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return nullptr; } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return nullptr; + } + return evse->getTransaction() ? evse->getTransaction()->idToken.get() : nullptr; + } + #endif + auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); @@ -454,6 +704,12 @@ std::shared_ptr& getTransaction(unsigned int connectorId) { MO_DBG_WARN("OCPP uninitialized"); return mocpp_undefinedTx; } + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + MO_DBG_ERR("only supported in v16"); + return mocpp_undefinedTx; + } + #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); @@ -462,11 +718,48 @@ std::shared_ptr& getTransaction(unsigned int connectorId) { return connector->getTransaction(); } +#if MO_ENABLE_V201 +Ocpp201::Transaction *getTransactionV201(unsigned int evseId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return nullptr; + } + + if (context->getVersion().major != 2) { + MO_DBG_ERR("only supported in v201"); + return nullptr; + } + + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(evseId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return nullptr; + } + return evse->getTransaction(); +} +#endif //MO_ENABLE_V201 + bool ocppPermitsCharge(unsigned int connectorId) { if (!context) { MO_DBG_WARN("OCPP uninitialized"); return false; } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->ocppPermitsCharge(); + } +#endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); @@ -475,11 +768,48 @@ bool ocppPermitsCharge(unsigned int connectorId) { return connector->ocppPermitsCharge(); } +ChargePointStatus getChargePointStatus(unsigned int connectorId) { + if (!context) { + MO_DBG_WARN("OCPP uninitialized"); + return ChargePointStatus_UNDEFINED; + } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto availabilityService = context->getModel().getAvailabilityService()) { + if (auto evse = availabilityService->getEvse(connectorId)) { + return evse->getStatus(); + } + } + } +#endif + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return ChargePointStatus_UNDEFINED; + } + return connector->getStatus(); +} + void setConnectorPluggedInput(std::function pluggedInput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto availabilityService = context->getModel().getAvailabilityService()) { + if (auto evse = availabilityService->getEvse(connectorId)) { + evse->setConnectorPluggedInput(pluggedInput); + } + } + if (auto txService = context->getModel().getTransactionService()) { + if (auto evse = txService->getEvse(connectorId)) { + evse->setConnectorPluggedInput(pluggedInput); + } + } + return; + } +#endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); @@ -488,25 +818,28 @@ void setConnectorPluggedInput(std::function pluggedInput, unsigned int c connector->setConnectorPluggedInput(pluggedInput); } -void setEnergyMeterInput(std::function energyInput, unsigned int connectorId) { +void setEnergyMeterInput(std::function energyInput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } - auto& model = context->getModel(); - if (!model.getMeteringService()) { - model.setMeteringSerivce(std::unique_ptr( - new MeteringService(*context, MO_NUMCONNECTORS, filesystem))); + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + addMeterValueInput([energyInput] () {return static_cast(energyInput());}, "Energy.Active.Import.Register", "Wh", nullptr, nullptr, connectorId); + return; } + #endif + SampledValueProperties meterProperties; meterProperties.setMeasurand("Energy.Active.Import.Register"); meterProperties.setUnit("Wh"); - auto mvs = std::unique_ptr>>( - new SampledValueSamplerConcrete>( + auto mvs = std::unique_ptr>>( + new SampledValueSamplerConcrete>( meterProperties, [energyInput] (ReadingContext) {return energyInput();} )); - model.getMeteringService()->addMeterValueSampler(connectorId, std::move(mvs)); + addMeterValueInput(std::move(mvs), connectorId); } void setPowerMeterInput(std::function powerInput, unsigned int connectorId) { @@ -515,11 +848,13 @@ void setPowerMeterInput(std::function powerInput, unsigned int connecto return; } - auto& model = context->getModel(); - if (!model.getMeteringService()) { - model.setMeteringSerivce(std::unique_ptr( - new MeteringService(*context, MO_NUMCONNECTORS, filesystem))); + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + addMeterValueInput([powerInput] () {return static_cast(powerInput());}, "Power.Active.Import", "W", nullptr, nullptr, connectorId); + return; } + #endif + SampledValueProperties meterProperties; meterProperties.setMeasurand("Power.Active.Import"); meterProperties.setUnit("W"); @@ -528,7 +863,7 @@ void setPowerMeterInput(std::function powerInput, unsigned int connecto meterProperties, [powerInput] (ReadingContext) {return powerInput();} )); - model.getMeteringService()->addMeterValueSampler(connectorId, std::move(mvs)); + addMeterValueInput(std::move(mvs), connectorId); } void setSmartChargingPowerOutput(std::function chargingLimitOutput, unsigned int connectorId) { @@ -616,6 +951,16 @@ void setEvReadyInput(std::function evReadyInput, unsigned int connectorI MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto txService = context->getModel().getTransactionService()) { + if (auto evse = txService->getEvse(connectorId)) { + evse->setEvReadyInput(evReadyInput); + } + } + return; + } +#endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); @@ -629,6 +974,16 @@ void setEvseReadyInput(std::function evseReadyInput, unsigned int connec MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto txService = context->getModel().getTransactionService()) { + if (auto evse = txService->getEvse(connectorId)) { + evse->setEvseReadyInput(evseReadyInput); + } + } + return; + } +#endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); @@ -679,6 +1034,33 @@ void addMeterValueInput(std::function valueInput, const char *measuran MO_DBG_WARN("measurand unspecified; assume %s", measurand); } + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + auto& model = context->getModel(); + if (!model.getMeteringServiceV201()) { + model.setMeteringServiceV201(std::unique_ptr( + new Ocpp201::MeteringService(context->getModel(), MO_NUM_EVSEID))); + } + if (auto mEvse = model.getMeteringServiceV201()->getEvse(connectorId)) { + + Ocpp201::SampledValueProperties properties; + properties.setMeasurand(measurand); //mandatory for MO + + if (unit) + properties.setUnitOfMeasureUnit(unit); + if (location) + properties.setLocation(location); + if (phase) + properties.setPhase(phase); + + mEvse->addMeterValueInput([valueInput] (ReadingContext) {return static_cast(valueInput());}, properties); + } else { + MO_DBG_ERR("inalid arg"); + } + return; + } + #endif + SampledValueProperties properties; properties.setMeasurand(measurand); //mandatory for MO @@ -692,7 +1074,7 @@ void addMeterValueInput(std::function valueInput, const char *measuran auto valueSampler = std::unique_ptr>>( new MicroOcpp::SampledValueSamplerConcrete>( properties, - [valueInput] (MicroOcpp::ReadingContext) {return valueInput();})); + [valueInput] (ReadingContext) {return valueInput();})); addMeterValueInput(std::move(valueSampler), connectorId); } @@ -701,6 +1083,12 @@ void addMeterValueInput(std::unique_ptr valueInput, unsigne MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + MO_DBG_ERR("addMeterValueInput(std::unique_ptr...) not compatible with v201. Use addMeterValueInput(std::function...) instead"); + return; + } + #endif auto& model = context->getModel(); if (!model.getMeteringService()) { model.setMeteringSerivce(std::unique_ptr( @@ -714,6 +1102,16 @@ void setOccupiedInput(std::function occupied, unsigned int connectorId) MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto availabilityService = context->getModel().getAvailabilityService()) { + if (auto evse = availabilityService->getEvse(connectorId)) { + evse->setOccupiedInput(occupied); + } + } + return; + } +#endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); @@ -748,11 +1146,17 @@ void setStopTxReadyInput(std::function stopTxReady, unsigned int connect connector->setStopTxReadyInput(stopTxReady); } -void setTxNotificationOutput(std::function notificationOutput, unsigned int connectorId) { +void setTxNotificationOutput(std::function notificationOutput, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; } + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + MO_DBG_ERR("only supported in v16"); + return; + } + #endif auto connector = context->getModel().getConnector(connectorId); if (!connector) { MO_DBG_ERR("could not find connector"); @@ -761,7 +1165,32 @@ void setTxNotificationOutput(std::functionsetTxNotificationOutput(notificationOutput); } -void setOnUnlockConnectorInOut(std::function()> onUnlockConnectorInOut, unsigned int connectorId) { +#if MO_ENABLE_V201 +void setTxNotificationOutputV201(std::function notificationOutput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + + if (context->getVersion().major != 2) { + MO_DBG_ERR("only supported in v201"); + return; + } + + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return; + } + evse->setTxNotificationOutput(notificationOutput); +} +#endif //MO_ENABLE_V201 + +#if MO_ENABLE_CONNECTOR_LOCK +void setOnUnlockConnectorInOut(std::function onUnlockConnectorInOut, unsigned int connectorId) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before return; @@ -773,12 +1202,26 @@ void setOnUnlockConnectorInOut(std::function()> onUnlockConnect } connector->setOnUnlockConnector(onUnlockConnectorInOut); } +#endif //MO_ENABLE_CONNECTOR_LOCK bool isOperative(unsigned int connectorId) { if (!context) { MO_DBG_WARN("OCPP uninitialized"); return true; //assume "true" as default state } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto availabilityService = context->getModel().getAvailabilityService()) { + auto chargePoint = availabilityService->getEvse(OCPP_ID_OF_CP); + auto connector = availabilityService->getEvse(connectorId); + if (!chargePoint || !connector) { + MO_DBG_ERR("could not find connector"); + return true; //assume "true" as default state + } + return chargePoint->isAvailable() && connector->isAvailable(); + } + } +#endif auto& model = context->getModel(); auto chargePoint = model.getConnector(OCPP_ID_OF_CP); auto connector = model.getConnector(connectorId); @@ -795,6 +1238,15 @@ void setOnResetNotify(std::function onResetNotify) { return; } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto rService = context->getModel().getResetServiceV201()) { + rService->setNotifyReset([onResetNotify] (ResetType) {return onResetNotify(true);}); + } + return; + } +#endif + if (auto rService = context->getModel().getResetService()) { rService->setPreReset(onResetNotify); } @@ -806,24 +1258,70 @@ void setOnResetExecute(std::function onResetExecute) { return; } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto rService = context->getModel().getResetServiceV201()) { + rService->setExecuteReset([onResetExecute] () {onResetExecute(true); return true;}); + } + return; + } +#endif + if (auto rService = context->getModel().getResetService()) { rService->setExecuteReset(onResetExecute); } } -#if defined(MO_CUSTOM_UPDATER) || defined(MO_CUSTOM_WS) FirmwareService *getFirmwareService() { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return nullptr; + } + auto& model = context->getModel(); + if (!model.getFirmwareService()) { + model.setFirmwareService(std::unique_ptr( + new FirmwareService(*context))); + } + return model.getFirmwareService(); } -#endif -#if defined(MO_CUSTOM_DIAGNOSTICS) || defined(MO_CUSTOM_WS) DiagnosticsService *getDiagnosticsService() { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return nullptr; + } + auto& model = context->getModel(); + if (!model.getDiagnosticsService()) { + model.setDiagnosticsService(std::unique_ptr( + new DiagnosticsService(*context))); + } + return model.getDiagnosticsService(); } -#endif + +#if MO_ENABLE_CERT_MGMT + +void setCertificateStore(std::unique_ptr certStore) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + + auto& model = context->getModel(); + if (!model.getCertificateService()) { + model.setCertificateService(std::unique_ptr( + new CertificateService(*context))); + } + if (auto certService = model.getCertificateService()) { + certService->setCertificateStore(std::move(certStore)); + } else { + MO_DBG_ERR("OOM"); + } +} +#endif //MO_ENABLE_CERT_MGMT Context *getOcppContext() { return context; @@ -854,7 +1352,7 @@ void setOnSendConf(const char *operationType, OnSendConfListener onSendConf) { } void sendRequest(const char *operationType, - std::function ()> fn_createReq, + std::function ()> fn_createReq, std::function fn_processConf) { if (!context) { @@ -872,7 +1370,7 @@ void sendRequest(const char *operationType, void setRequestHandler(const char *operationType, std::function fn_processReq, - std::function ()> fn_createConf) { + std::function ()> fn_createConf) { if (!context) { MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before @@ -883,7 +1381,7 @@ void setRequestHandler(const char *operationType, return; } - std::string captureOpType = operationType; + auto captureOpType = makeString("MicroOcpp.cpp", operationType); context->getOperationRegistry().registerOperation(operationType, [captureOpType, fn_processReq, fn_createConf] () { return new CustomOperation(captureOpType.c_str(), fn_processReq, fn_createConf); @@ -947,7 +1445,7 @@ bool startTransaction(const char *idTag, OnReceiveConfListener onConf, OnAbortLi } if (auto mService = context->getModel().getMeteringService()) { - auto meterStart = mService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext::TransactionBegin); + auto meterStart = mService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext_TransactionBegin); if (meterStart && *meterStart) { transaction->setMeterStart(meterStart->toInteger()); } else { @@ -1003,7 +1501,7 @@ bool stopTransaction(OnReceiveConfListener onConf, OnAbortListener onAbort, OnTi } if (auto mService = context->getModel().getMeteringService()) { - auto meterStop = mService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext::TransactionEnd); + auto meterStop = mService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext_TransactionEnd); if (meterStop && *meterStop) { transaction->setMeterStop(meterStop->toInteger()); } else { diff --git a/src/MicroOcpp.h b/src/MicroOcpp.h index db672b35..f15c13cc 100644 --- a/src/MicroOcpp.h +++ b/src/MicroOcpp.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef ARDUINOOCPP_H -#define ARDUINOOCPP_H +#ifndef MO_MICROOCPP_H +#define MO_MICROOCPP_H #include #include @@ -13,11 +13,14 @@ #include #include #include -#include +#include #include #include -#include #include +#include +#include +#include +#include using MicroOcpp::OnReceiveConfListener; using MicroOcpp::OnReceiveReqListener; @@ -69,6 +72,20 @@ struct ChargerCredentials { 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;} @@ -80,7 +97,7 @@ struct ChargerCredentials { * 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/master/examples/ESP-TLS for an example how to use it. + * 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 @@ -93,7 +110,8 @@ void mocpp_initialize( 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 + bool autoRecover = false, //automatically sanitize the local data store when the lib detects recurring crashes. Not recommended during development + MicroOcpp::ProtocolVersion version = MicroOcpp::ProtocolVersion(1,6)); /* * Stop the OCPP library and release allocated resources. @@ -108,6 +126,7 @@ void mocpp_loop(); /* * Transaction management. * + * OCPP 1.6 (2.0.1 see below): * Begin the transaction process and prepare it. When all conditions for the transaction are true, * eventually send a StartTransaction request to the OCPP server. * Conditions: @@ -121,18 +140,24 @@ void mocpp_loop(); * * See beginTransaction_authorized for skipping steps 1) to 3) * - * Returns the transaction object if it was possible to create the transaction process. Returns - * nullptr if either another transaction process is still active or you need to try it again later. + * 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. */ -std::shared_ptr beginTransaction(const char *idTag, unsigned int connectorId = 1); +bool beginTransaction(const char *idTag, unsigned int connectorId = 1); /* * Begin the transaction process and skip the OCPP-side authorization. See beginTransaction(...) for a * complete description */ -std::shared_ptr beginTransaction_authorized(const char *idTag, const char *parentIdTag = nullptr, unsigned int connectorId = 1); +bool beginTransaction_authorized(const char *idTag, const char *parentIdTag = nullptr, unsigned int connectorId = 1); /* + * OCPP 1.6 (2.0.1 see below): * End the transaction process if idTag is authorized to stop the transaction. The OCPP lib sends * a StopTransaction request if the following conditions are true: * Conditions: @@ -162,6 +187,15 @@ std::shared_ptr beginTransaction_authorized(const char * * `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); @@ -219,6 +253,14 @@ const char *getTransactionIdTag(unsigned int connectorId = 1); */ std::shared_ptr& getTransaction(unsigned int connectorId = 1); +#if MO_ENABLE_V201 +/* + * OCPP 2.0.1 version of getTransaction(). Note that the return transaction object is of another type + * and unlike the 1.6 version, this function does not give ownership. + */ +MicroOcpp::Ocpp201::Transaction *getTransactionV201(unsigned int evseId = 1); +#endif //MO_ENABLE_V201 + /* * Returns if the OCPP library allows the EVSE to charge at the moment. * @@ -227,6 +269,11 @@ std::shared_ptr& getTransaction(unsigned int connectorId */ bool ocppPermitsCharge(unsigned int connectorId = 1); +/* + * Returns the latest ChargePointStatus as reported via StatusNotification (standard OCPP data type) + */ +ChargePointStatus getChargePointStatus(unsigned int connectorId = 1); + /* * Define the Inputs and Outputs of this library. * @@ -249,12 +296,14 @@ bool ocppPermitsCharge(unsigned int connectorId = 1); void setConnectorPluggedInput(std::function pluggedInput, unsigned int connectorId = 1); //Input about if an EV is plugged to this EVSE -void setEnergyMeterInput(std::function energyInput, unsigned int connectorId = 1); //Input of the electricity meter register +void setEnergyMeterInput(std::function energyInput, unsigned int connectorId = 1); //Input of the electricity meter register in Wh -void setPowerMeterInput(std::function powerInput, unsigned int connectorId = 1); //Input of the power meter reading +void setPowerMeterInput(std::function powerInput, unsigned int connectorId = 1); //Input of the power meter reading in W -//Smart Charging Output, alternative for Watts only, Current only, or Watts x Current x numberPhases. Only one -//of them can be set at a time +//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 @@ -282,15 +331,23 @@ void setStartTxReadyInput(std::function startTxReady, unsigned int conne void setStopTxReadyInput(std::function stopTxReady, unsigned int connectorId = 1); //Input if charger is ready for StopTransaction -void setTxNotificationOutput(std::function notificationOutput, unsigned int connectorId = 1); //called when transaction state changes (see TxNotification for possible events). Transaction can be null +void setTxNotificationOutput(std::function notificationOutput, unsigned int connectorId = 1); //called when transaction state changes (see TxNotification for possible events). Transaction can be null + +#if MO_ENABLE_V201 +void setTxNotificationOutputV201(std::function notificationOutput, unsigned int connectorId = 1); +#endif //MO_ENABLE_V201 +#if MO_ENABLE_CONNECTOR_LOCK /* * Set an InputOutput (reads and sets information at the same time) for forcing to unlock the * connector. Called as part of the OCPP operation "UnlockConnector" - * Return values: true on success, false on failure, PollResult::Await if not known yet - * Continues to call the Cb as long as it returns PollResult::Await + * Return values: + * - UnlockConnectorResult_Pending if action needs more time to complete (MO will call this cb again later or eventually time out) + * - UnlockConnectorResult_Unlocked if successful + * - UnlockConnectorResult_UnlockFailed if not successful (e.g. lock stuck) */ -void setOnUnlockConnectorInOut(std::function()> onUnlockConnectorInOut, unsigned int connectorId = 1); +void setOnUnlockConnectorInOut(std::function onUnlockConnectorInOut, unsigned int connectorId = 1); +#endif //MO_ENABLE_CONNECTOR_LOCK /* * Access further information about the internal state of the library @@ -306,28 +363,51 @@ void setOnResetNotify(std::function onResetNotify); //call onResetNo void setOnResetExecute(std::function onResetExecute); //reset handler. This function should reboot this controller immediately. Already defined for the ESP32 on Arduino -#if defined(MO_CUSTOM_UPDATER) || defined(MO_CUSTOM_WS) -#include + +namespace MicroOcpp { +class FirmwareService; +class DiagnosticsService; +} /* * You need to configure this object if FW updates are relevant for you. This project already * brings a simple configuration for the ESP32 and ESP8266 for prototyping purposes, however * for the productive system you will have to develop a configuration targeting the specific * OCPP backend. - * See MicroOcpp/Model/FirmwareManagement/FirmwareService.h + * See MicroOcpp/Model/FirmwareManagement/FirmwareService.h + * + * Lazy initialization: The FW Service will be created at the first call to this function + * + * To use, add `#include ` */ MicroOcpp::FirmwareService *getFirmwareService(); -#endif -#if defined(MO_CUSTOM_DIAGNOSTICS) || defined(MO_CUSTOM_WS) -#include /* * 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(); -#endif + +#if MO_ENABLE_CERT_MGMT +/* + * Set a custom Certificate Store which implements certificate updates on the host system. + * MicroOcpp will forward OCPP-side update requests to the certificate store, as well as + * query the certificate store upon server request. + * + * To enable OCPP-side certificate updates (UCs M03 - M05), set the build flag + * MO_ENABLE_CERT_MGMT=1 so that this function becomes accessible. + * + * To use the built-in certificate store (depends on MbedTLS), set the build flag + * MO_ENABLE_MBEDTLS=1. To not use the built-in implementation, but still enable MbedTLS, + * additionally set MO_ENABLE_CERT_STORE_MBEDTLS=0. + */ +void setCertificateStore(std::unique_ptr certStore); +#endif //MO_ENABLE_CERT_MGMT /* * Add features and customize the behavior of the OCPP client @@ -339,6 +419,7 @@ class Context; //Get access to internal functions and data structures. The returned Context object allows //you to bypass the facade functions of this header and implement custom functionality. +//To use, add `#include ` MicroOcpp::Context *getOcppContext(); /* @@ -383,11 +464,11 @@ void setOnSendConf(const char *operationType, OnSendConfListener onSendConf); * * Use case 1, extend the library by sending additional operations. E.g. DataTransfer: * - * sendRequest("DataTransfer", [] () -> std::unique_ptr { + * 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 DynamicJsonDocument(capacity)); + * auto res = std::unique_ptr(new MicroOcpp::JsonDoc(capacity)); * JsonObject request = *res; * request["vendorId"] = "My company Ltd."; * request["messageId"] = "TargetValues"; @@ -404,10 +485,10 @@ void setOnSendConf(const char *operationType, OnSendConfListener onSendConf); * * Use case 2, bypass the business logic of this library for custom behavior. E.g. StartTransaction: * - * sendRequest("StartTransaction", [] () -> std::unique_ptr { + * 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 DynamicJsonDocument(capacity)); + * auto res = std::unique_ptr(new MicroOcpp::JsonDoc(capacity)); * JsonObject request = res->to(); * request["connectorId"] = 1; * request["idTag"] = "A9C3CE1D7B71EA"; @@ -424,7 +505,7 @@ void setOnSendConf(const char *operationType, OnSendConfListener onSendConf); * its own. */ void sendRequest(const char *operationType, - std::function ()> fn_createReq, + std::function ()> fn_createReq, std::function fn_processConf); /* @@ -442,11 +523,11 @@ void sendRequest(const char *operationType, * const char *messageId = request["messageId"]; * int battery_capacity = request["data"]["battery_capacity"]; * int battery_soc = request["data"]["battery_soc"]; - * }, [] () -> std::unique_ptr { + * }, [] () -> 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 DynamicJsonDocument(capacity)); + * auto res = std::unique_ptr(new MicroOcpp::JsonDoc(capacity)); * JsonObject response = res->to(); * response["status"] = "Accepted"; * response["data"]["max_energy"] = 59; @@ -455,7 +536,7 @@ void sendRequest(const char *operationType, */ void setRequestHandler(const char *operationType, std::function fn_processReq, - std::function ()> fn_createConf); + std::function ()> fn_createConf); /* * Send OCPP operations manually not bypassing the internal business logic diff --git a/src/MicroOcpp/Core/Configuration.cpp b/src/MicroOcpp/Core/Configuration.cpp index 8c40e547..cb0a8ff5 100644 --- a/src/MicroOcpp/Core/Configuration.cpp +++ b/src/MicroOcpp/Core/Configuration.cpp @@ -1,13 +1,13 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include +#include #include #include -#include #include #include @@ -24,8 +24,8 @@ struct Validator { namespace ConfigurationLocal { std::shared_ptr filesystem; -std::vector> configurationContainers; -std::vector validators; +auto configurationContainers = makeVector>("v16.Configuration.Containers"); +auto validators = makeVector("v16.Configuration.Validators"); } @@ -50,8 +50,8 @@ void addConfigurationContainer(std::shared_ptr container } std::shared_ptr getContainer(const char *filename) { - std::vector>::iterator container = std::find_if(configurationContainers.begin(), configurationContainers.end(), - [filename](std::shared_ptr &elem) { + auto container = std::find_if(configurationContainers.begin(), configurationContainers.end(), + [filename](decltype(configurationContainers)::value_type &elem) { return !strcmp(elem->getFilename(), filename); }); @@ -79,7 +79,6 @@ ConfigurationContainer *declareContainer(const char *filename, bool accessible) if (container->isAccessible() != accessible) { MO_DBG_ERR("%s: conflicting accessibility declarations (expect %s)", filename, container->isAccessible() ? "accessible" : "inaccessible"); - (void)0; } return container.get(); @@ -95,7 +94,6 @@ std::shared_ptr loadConfiguration(TConfig type, const char *key, } if (container->isAccessible() != accessible) { MO_DBG_ERR("conflicting accessibility for %s", key); - (void)0; } container->loadStaticKey(*config.get(), key); return config; @@ -194,8 +192,8 @@ Configuration *getConfigurationPublic(const char *key) { return nullptr; } -std::vector getConfigurationContainersPublic() { - std::vector res; +Vector getConfigurationContainersPublic() { + auto res = makeVector("v16.Configuration.Containers"); for (auto& container : configurationContainers) { if (container->isAccessible()) { @@ -212,8 +210,8 @@ bool configuration_init(std::shared_ptr _filesystem) { } void configuration_deinit() { - configurationContainers.clear(); - validators.clear(); + 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(); } @@ -241,4 +239,20 @@ bool configuration_save() { 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 index 1d19dac6..486b9c69 100644 --- a/src/MicroOcpp/Core/Configuration.h +++ b/src/MicroOcpp/Core/Configuration.h @@ -1,16 +1,16 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef CONFIGURATION_H -#define CONFIGURATION_H +#ifndef MO_CONFIGURATION_H +#define MO_CONFIGURATION_H #include #include #include +#include #include -#include #define CONFIGURATION_FN (MO_FILENAME_PREFIX "ocpp-config.jsn") #define CONFIGURATION_VOLATILE "/volatile" @@ -27,7 +27,7 @@ void registerConfigurationValidator(const char *key, std::function container); Configuration *getConfigurationPublic(const char *key); -std::vector getConfigurationContainersPublic(); +Vector getConfigurationContainersPublic(); bool configuration_init(std::shared_ptr filesytem); void configuration_deinit(); @@ -36,5 +36,10 @@ 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 index 4af4c359..af0cba2f 100644 --- a/src/MicroOcpp/Core/ConfigurationContainer.cpp +++ b/src/MicroOcpp/Core/ConfigurationContainer.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -12,7 +12,8 @@ ConfigurationContainer::~ConfigurationContainer() { } -ConfigurationContainerVolatile::ConfigurationContainerVolatile(const char *filename, bool accessible) : ConfigurationContainer(filename, accessible) { +ConfigurationContainerVolatile::ConfigurationContainerVolatile(const char *filename, bool accessible) : + ConfigurationContainer(filename, accessible), MemoryManaged("v16.Configuration.ContainerVoltaile.", filename), configurations(makeVector>(getMemoryTag())) { } @@ -25,7 +26,7 @@ bool ConfigurationContainerVolatile::save() { } std::shared_ptr ConfigurationContainerVolatile::createConfiguration(TConfig type, const char *key) { - std::shared_ptr res = makeConfiguration(type, 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"); diff --git a/src/MicroOcpp/Core/ConfigurationContainer.h b/src/MicroOcpp/Core/ConfigurationContainer.h index 52804e3b..9a3ff3ae 100644 --- a/src/MicroOcpp/Core/ConfigurationContainer.h +++ b/src/MicroOcpp/Core/ConfigurationContainer.h @@ -1,14 +1,14 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef CONFIGURATIONCONTAINER_H -#define CONFIGURATIONCONTAINER_H +#ifndef MO_CONFIGURATIONCONTAINER_H +#define MO_CONFIGURATIONCONTAINER_H -#include #include #include +#include namespace MicroOcpp { @@ -35,11 +35,13 @@ class ConfigurationContainer { 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 { +class ConfigurationContainerVolatile : public ConfigurationContainer, public MemoryManaged { private: - std::vector> configurations; + Vector> configurations; public: ConfigurationContainerVolatile(const char *filename, bool accessible); diff --git a/src/MicroOcpp/Core/ConfigurationContainerFlash.cpp b/src/MicroOcpp/Core/ConfigurationContainerFlash.cpp index df494327..b32f1af8 100644 --- a/src/MicroOcpp/Core/ConfigurationContainerFlash.cpp +++ b/src/MicroOcpp/Core/ConfigurationContainerFlash.cpp @@ -1,37 +1,39 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include +#include #include #define MAX_CONFIGURATIONS 50 namespace MicroOcpp { -class ConfigurationContainerFlash : public ConfigurationContainer { +class ConfigurationContainerFlash : public ConfigurationContainer, public MemoryManaged { private: - std::vector> configurations; + Vector> configurations; std::shared_ptr filesystem; uint16_t revisionSum = 0; bool loaded = false; - std::vector> keyPool; + Vector keyPool; void clearKeyPool(const char *key) { - keyPool.erase(std::remove_if(keyPool.begin(), keyPool.end(), - [key] (const std::unique_ptr& k) { - #if MO_DBG_LEVEL >= MO_DL_VERBOSE - if (!strcmp(k.get(), key)) { - MO_DBG_VERBOSE("clear key %s", key); - } - #endif - return !strcmp(k.get(), key); - }), keyPool.end()); + 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() { @@ -46,8 +48,16 @@ class ConfigurationContainerFlash : public ConfigurationContainer { } public: ConfigurationContainerFlash(std::shared_ptr filesystem, const char *filename, bool accessible) : - ConfigurationContainer(filename, accessible), filesystem(filesystem) { } + 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) { @@ -65,7 +75,7 @@ class ConfigurationContainerFlash : public ConfigurationContainer { return save(); } - auto doc = FilesystemUtils::loadJson(filesystem, getFilename()); + auto doc = FilesystemUtils::loadJson(filesystem, getFilename(), getMemoryTag()); if (!doc) { MO_DBG_ERR("failed to load %s", getFilename()); return false; @@ -110,8 +120,8 @@ class ConfigurationContainerFlash : public ConfigurationContainer { MO_DBG_ERR("corrupt config"); continue; } - - std::unique_ptr key_pooled; + + char *key_pooled = nullptr; auto config = getConfiguration(key).get(); if (config && config->getType() != type) { @@ -120,20 +130,32 @@ class ConfigurationContainerFlash : public ConfigurationContainer { config = nullptr; } if (!config) { - key_pooled.reset(new char[strlen(key) + 1]); - strcpy(key_pooled.get(), key); + #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()).get(); + config = createConfiguration(TConfig::Int, key_pooled).get(); } if (config) { config->setInt(value); @@ -143,12 +165,13 @@ class ConfigurationContainerFlash : public ConfigurationContainer { 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()).get(); + config = createConfiguration(TConfig::Bool, key_pooled).get(); } if (config) { config->setBool(value); @@ -158,12 +181,13 @@ class ConfigurationContainerFlash : public ConfigurationContainer { 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()).get(); + config = createConfiguration(TConfig::String, key_pooled).get(); } if (config) { config->setString(value); @@ -181,7 +205,7 @@ class ConfigurationContainerFlash : public ConfigurationContainer { } } else { MO_DBG_ERR("OOM: %s", key); - (void)0; + MO_FREE(key_pooled); } } @@ -219,7 +243,7 @@ class ConfigurationContainerFlash : public ConfigurationContainer { jsonCapacity = MO_MAX_JSON_CAPACITY; } - DynamicJsonDocument doc {jsonCapacity}; + auto doc = initJsonDoc(getMemoryTag(), jsonCapacity); JsonObject head = doc.createNestedObject("head"); head["content-type"] = "ocpp_config_file"; head["version"] = "2.0"; @@ -268,7 +292,7 @@ class ConfigurationContainerFlash : public ConfigurationContainer { } std::shared_ptr createConfiguration(TConfig type, const char *key) override { - std::shared_ptr res = makeConfiguration(type, 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"); @@ -310,6 +334,25 @@ class ConfigurationContainerFlash : public ConfigurationContainer { 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) { diff --git a/src/MicroOcpp/Core/ConfigurationContainerFlash.h b/src/MicroOcpp/Core/ConfigurationContainerFlash.h index ecab3f69..950f3cd7 100644 --- a/src/MicroOcpp/Core/ConfigurationContainerFlash.h +++ b/src/MicroOcpp/Core/ConfigurationContainerFlash.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef CONFIGURATIONCONTAINERFLASH_H -#define CONFIGURATIONCONTAINERFLASH_H +#ifndef MO_CONFIGURATIONCONTAINERFLASH_H +#define MO_CONFIGURATIONCONTAINERFLASH_H #include #include diff --git a/src/MicroOcpp/Core/ConfigurationKeyValue.cpp b/src/MicroOcpp/Core/ConfigurationKeyValue.cpp index 52097583..77e8ad85 100644 --- a/src/MicroOcpp/Core/ConfigurationKeyValue.cpp +++ b/src/MicroOcpp/Core/ConfigurationKeyValue.cpp @@ -1,21 +1,17 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include +#include #include #include -#include #include #define KEY_MAXLEN 60 #define STRING_VAL_MAXLEN 512 -#ifndef MO_CONFIG_TYPECHECK -#define MO_CONFIG_TYPECHECK 1 //enable this for debugging -#endif - namespace MicroOcpp { template<> TConfig convertType() {return TConfig::Int;} @@ -79,11 +75,27 @@ bool Configuration::isRebootRequired() { } void Configuration::setReadOnly() { - readOnly = true; + if (mutability == Mutability::ReadWrite) { + mutability = Mutability::ReadOnly; + } else { + mutability = Mutability::None; + } } bool Configuration::isReadOnly() { - return readOnly; + 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; + } } /* @@ -93,7 +105,7 @@ bool Configuration::isReadOnly() { * before its initialization stage. Then the library won't create new config objects but */ -class ConfigInt : public Configuration { +class ConfigInt : public Configuration, public MemoryManaged { private: const char *key = nullptr; int val = 0; @@ -103,6 +115,7 @@ class ConfigInt : public Configuration { bool setKey(const char *key) override { this->key = key; + updateMemoryTag("v16.Configuration.", key); return true; } @@ -124,7 +137,7 @@ class ConfigInt : public Configuration { } }; -class ConfigBool : public Configuration { +class ConfigBool : public Configuration, public MemoryManaged { private: const char *key = nullptr; bool val = false; @@ -134,6 +147,7 @@ class ConfigBool : public Configuration { bool setKey(const char *key) override { this->key = key; + updateMemoryTag("v16.Configuration.", key); return true; } @@ -155,7 +169,7 @@ class ConfigBool : public Configuration { } }; -class ConfigString : public Configuration { +class ConfigString : public Configuration, public MemoryManaged { private: const char *key = nullptr; char *val = nullptr; @@ -166,11 +180,15 @@ class ConfigString : public Configuration { ConfigString& operator=(const ConfigString&) = delete; ~ConfigString() { - free(val); + 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; } @@ -205,12 +223,12 @@ class ConfigString : public Configuration { value_revision++; if (this->val) { - free(this->val); + MO_FREE(this->val); this->val = nullptr; } if (!src_empty) { - this->val = (char*) malloc(size); + this->val = (char*) MO_MALLOC(getMemoryTag(), size); if (!this->val) { return false; } @@ -247,7 +265,7 @@ std::unique_ptr makeConfiguration(TConfig type, const char *key) } res->setKey(key); return res; -}; +} bool deserializeTConfig(const char *serialized, TConfig& out) { if (!strcmp(serialized, "int")) { diff --git a/src/MicroOcpp/Core/ConfigurationKeyValue.h b/src/MicroOcpp/Core/ConfigurationKeyValue.h index 8af972cb..3e631c1e 100644 --- a/src/MicroOcpp/Core/ConfigurationKeyValue.h +++ b/src/MicroOcpp/Core/ConfigurationKeyValue.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef CONFIGURATIONKEYVALUE_H @@ -14,6 +14,10 @@ #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; @@ -32,7 +36,15 @@ class Configuration { revision_t value_revision = 0; //write access counter; used to check if this config has been changed private: bool rebootRequired = false; - bool readOnly = false; + + enum class Mutability : uint8_t { + ReadWrite, + ReadOnly, + WriteOnly, + None + }; + Mutability mutability = Mutability::ReadWrite; + public: virtual ~Configuration(); @@ -49,13 +61,16 @@ class Configuration { virtual TConfig getType() = 0; - revision_t getValueRevision(); + virtual revision_t getValueRevision(); void setRebootRequired(); bool isRebootRequired(); void setReadOnly(); bool isReadOnly(); + bool isReadable(); + + void setWriteOnly(); }; /* diff --git a/src/MicroOcpp/Core/ConfigurationOptions.h b/src/MicroOcpp/Core/ConfigurationOptions.h index 1cda7a91..0e9e227f 100644 --- a/src/MicroOcpp/Core/ConfigurationOptions.h +++ b/src/MicroOcpp/Core/ConfigurationOptions.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef CONFIGURATIONOPTIONS_H -#define CONFIGURATIONOPTIONS_H +#ifndef MO_CONFIGURATIONOPTIONS_H +#define MO_CONFIGURATIONOPTIONS_H #include diff --git a/src/MicroOcpp/Core/Configuration_c.cpp b/src/MicroOcpp/Core/Configuration_c.cpp new file mode 100644 index 00000000..6b6998e1 --- /dev/null +++ b/src/MicroOcpp/Core/Configuration_c.cpp @@ -0,0 +1,315 @@ +// 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 new file mode 100644 index 00000000..94172d07 --- /dev/null +++ b/src/MicroOcpp/Core/Configuration_c.h @@ -0,0 +1,84 @@ +// 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 73e64c3c..ae2ee2fb 100644 --- a/src/MicroOcpp/Core/Connection.cpp +++ b/src/MicroOcpp/Core/Connection.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -7,10 +7,12 @@ using namespace MicroOcpp; +LoopbackConnection::LoopbackConnection() : MemoryManaged("WebSocketLoopback") { } + void LoopbackConnection::loop() { } bool LoopbackConnection::sendTXT(const char *msg, size_t length) { - if (!connected) { + if (!connected || !online) { return false; } if (receiveTXT) { @@ -33,6 +35,13 @@ unsigned long LoopbackConnection::getLastConnected() { return lastConn; } +void LoopbackConnection::setOnline(bool online) { + if (online) { + lastConn = mocpp_tick_ms(); + } + this->online = online; +} + void LoopbackConnection::setConnected(bool connected) { if (connected) { lastConn = mocpp_tick_ms(); @@ -44,7 +53,7 @@ void LoopbackConnection::setConnected(bool connected) { using namespace MicroOcpp::EspWiFi; -WSClient::WSClient(WebSocketsClient *wsock) : wsock(wsock) { +WSClient::WSClient(WebSocketsClient *wsock) : MemoryManaged("WebSocketsClient"), wsock(wsock) { } @@ -65,7 +74,7 @@ void WSClient::setReceiveTXTcallback(ReceiveTXTcallback &callback) { MO_DBG_INFO("Disconnected"); break; case WStype_CONNECTED: - MO_DBG_INFO("Connected to url: %s", payload); + MO_DBG_INFO("Connected (path: %s)", payload); captureLastRecv = mocpp_tick_ms(); captureLastConnected = mocpp_tick_ms(); break; @@ -105,4 +114,8 @@ unsigned long WSClient::getLastConnected() { return lastConnected; } +bool WSClient::isConnected() { + return wsock->isConnected(); +} + #endif diff --git a/src/MicroOcpp/Core/Connection.h b/src/MicroOcpp/Core/Connection.h index 2dbed0e5..2c2f7d5c 100644 --- a/src/MicroOcpp/Core/Connection.h +++ b/src/MicroOcpp/Core/Connection.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CONNECTION_H @@ -8,10 +8,11 @@ #include #include +#include #include //On all platforms other than Arduino, the integrated WS lib (links2004/arduinoWebSockets) cannot be -//used. On Arduino it's usage is optional. +//used. On Arduino its usage is optional. #ifndef MO_CUSTOM_WS #if MO_PLATFORM != MO_PLATFORM_ARDUINO #define MO_CUSTOM_WS @@ -46,6 +47,8 @@ class Connection { /* * 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;} @@ -53,24 +56,44 @@ class Connection { * Returns the timestamp of the last time a connection got successfully established. Use mocpp_tick_ms() for creating the correct timestamp */ virtual unsigned long getLastConnected() = 0; + + /* + * NEW IN v1.1 + * + * Returns true if the connection is open; false if the charger is known to be offline. + * + * This function determines if MO is in "offline mode". In offline mode, MO doesn't wait for Authorize responses + * before performing fully local authorization. If the connection is disrupted but isConnected is still true, then + * MO will first wait for a timeout to expire (20 seconds) before going into offline mode. + * + * Returning true will have no further effects other than using the timeout-then-offline mechanism. If the + * connection status is uncertain, it's best to return true by default. + */ + virtual bool isConnected() {return true;} //MO ignores true. This default implementation keeps backwards-compatibility }; -class LoopbackConnection : public Connection { +class LoopbackConnection : public Connection, public MemoryManaged { private: ReceiveTXTcallback receiveTXT; - bool connected = true; //for simulating connection losses + //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 setConnected(bool connected); - bool isConnected() {return connected;} + void setOnline(bool online); //"online": sent messages are going through + bool isOnline() {return online;} + void setConnected(bool connected); //"connected": connection has been established, but messages may not go through (e.g. weak connection) + bool isConnected() override {return connected;} }; } //end namespace MicroOcpp @@ -82,7 +105,7 @@ class LoopbackConnection : public Connection { namespace MicroOcpp { namespace EspWiFi { -class WSClient : public Connection { +class WSClient : public Connection, public MemoryManaged { private: WebSocketsClient *wsock; unsigned long lastRecv = 0, lastConnected = 0; @@ -98,6 +121,8 @@ class WSClient : public Connection { unsigned long getLastRecv() override; //get time of last successful receive in millis unsigned long getLastConnected() override; //get last connection creation in millis + + bool isConnected() override; }; } //end namespace EspWiFi diff --git a/src/MicroOcpp/Core/Context.cpp b/src/MicroOcpp/Core/Context.cpp index 425354ac..1ab65fcb 100644 --- a/src/MicroOcpp/Core/Context.cpp +++ b/src/MicroOcpp/Core/Context.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -11,11 +11,9 @@ using namespace MicroOcpp; -Context::Context(Connection& connection, std::shared_ptr filesystem, uint16_t bootNr) - : connection(connection), model{bootNr}, reqQueue{operationRegistry, &model, filesystem} { +Context::Context(Connection& connection, std::shared_ptr filesystem, uint16_t bootNr, ProtocolVersion version) + : MemoryManaged("Context"), connection(connection), model{version, bootNr}, reqQueue{connection, operationRegistry} { - preBootQueue = std::unique_ptr(new RequestQueue(operationRegistry, &model, nullptr)); //pre boot queue doesn't need persistency - preBootQueue->setConnection(connection); } Context::~Context() { @@ -24,37 +22,16 @@ Context::~Context() { void Context::loop() { connection.loop(); - - if (preBootQueue) { - preBootQueue->loop(connection); - } else { - reqQueue.loop(connection); - } - + reqQueue.loop(); model.loop(); } -void Context::activatePostBootCommunication() { - //switch from pre boot connection to normal connetion - reqQueue.setConnection(connection); - preBootQueue.reset(); -} - void Context::initiateRequest(std::unique_ptr op) { - if (op) { - reqQueue.sendRequest(std::move(op)); - } -} - -void Context::initiatePreBootOperation(std::unique_ptr op) { - if (op) { - if (preBootQueue) { - preBootQueue->sendRequest(std::move(op)); - } else { - //not in pre boot mode anymore - initiate normally - initiateRequest(std::move(op)); - } + if (!op) { + MO_DBG_ERR("invalid arg"); + return; } + reqQueue.sendRequest(std::move(op)); } Model& Context::getModel() { @@ -64,3 +41,23 @@ Model& Context::getModel() { 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 index dfe5ff2c..2df691d1 100644 --- a/src/MicroOcpp/Core/Context.h +++ b/src/MicroOcpp/Core/Context.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CONTEXT_H @@ -9,38 +9,45 @@ #include #include +#include +#include #include +#include namespace MicroOcpp { class Connection; class FilesystemAdapter; -class Context { +class Context : public MemoryManaged { private: Connection& connection; OperationRegistry operationRegistry; Model model; RequestQueue reqQueue; - std::unique_ptr preBootQueue; + std::unique_ptr ftpClient; public: - Context(Connection& connection, std::shared_ptr filesystem, uint16_t bootNr); + Context(Connection& connection, std::shared_ptr filesystem, uint16_t bootNr, ProtocolVersion version); ~Context(); void loop(); - void activatePostBootCommunication(); - void initiateRequest(std::unique_ptr op); - - //for BootNotification and TriggerMessage: initiate operations before the first BootNotification was accepted (pre-boot mode) - void initiatePreBootOperation(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 diff --git a/src/MicroOcpp/Core/FilesystemAdapter.cpp b/src/MicroOcpp/Core/FilesystemAdapter.cpp index 2d65e778..b442e583 100644 --- a/src/MicroOcpp/Core/FilesystemAdapter.cpp +++ b/src/MicroOcpp/Core/FilesystemAdapter.cpp @@ -1,9 +1,10 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include //FilesystemOpt +#include #include #include @@ -14,10 +15,242 @@ * - Arduino SPIFFS * - ESP-IDF SPIFFS * - POSIX-like API (tested on Ubuntu 20.04) + * Plus a filesystem index decorator working with any of the above * * You can add support for other file systems by passing a custom adapter to mocpp_initialize(...) */ +#if MO_ENABLE_FILE_INDEX + +#include + +namespace MicroOcpp { + +class FilesystemAdapterIndex; + +class IndexedFileAdapter : public FileAdapter, public MemoryManaged { +private: + FilesystemAdapterIndex& index; + char fn [MO_MAX_PATH_SIZE]; + std::unique_ptr file; + + size_t written = 0; +public: + IndexedFileAdapter(FilesystemAdapterIndex& index, const char *fn, std::unique_ptr file) + : MemoryManaged("FilesystemIndex"), index(index), file(std::move(file)) { + snprintf(this->fn, sizeof(this->fn), "%s", fn); + } + + ~IndexedFileAdapter(); // destructor updates file index with written size + + size_t read(char *buf, size_t len) override { + return file->read(buf, len); + } + + size_t write(const char *buf, size_t len) override { + auto ret = file->write(buf, len); + written += ret; + return ret; + } + + size_t seek(size_t offset) override { + auto ret = file->seek(offset); + written = ret; + return ret; + } + + int read() override { + return file->read(); + } +}; + +class FilesystemAdapterIndex : public FilesystemAdapter, public MemoryManaged { +private: + std::shared_ptr filesystem; + + struct IndexEntry { + String fname; + size_t size; + + IndexEntry(const char *fname, size_t size) : fname(makeString("FilesystemIndex", fname)), size(size) { } + }; + + Vector index; + + IndexEntry *getEntryByFname(const char *fn) { + auto entry = std::find_if(index.begin(), index.end(), + [fn] (const IndexEntry& el) -> bool { + return el.fname.compare(fn) == 0; + }); + + if (entry != index.end()) { + return &(*entry); + } else { + return nullptr; + } + } + + IndexEntry *getEntryByPath(const char *path) { + if (strlen(path) < sizeof(MO_FILENAME_PREFIX) - 1) { + MO_DBG_ERR("invalid fn"); + return nullptr; + } + + const char *fn = path + sizeof(MO_FILENAME_PREFIX) - 1; + return getEntryByFname(fn); + } + + void (*onDestruct)(void*) = nullptr; +public: + FilesystemAdapterIndex(std::shared_ptr filesystem, void (*onDestruct)(void*) = nullptr) : MemoryManaged("FilesystemIndex"), filesystem(std::move(filesystem)), index(makeVector("FilesystemIndex")), onDestruct(onDestruct) { } + + ~FilesystemAdapterIndex() { + if (onDestruct) { + onDestruct(this); + } + } + + int stat(const char *path, size_t *size) override { + if (auto file = getEntryByPath(path)) { + *size = file->size; + return 0; + } else { + return -1; + } + } + + std::unique_ptr open(const char *path, const char *mode) { + if (!strcmp(mode, "r")) { + return filesystem->open(path, "r"); + } else if (!strcmp(mode, "w")) { + + if (strlen(path) < sizeof(MO_FILENAME_PREFIX) - 1) { + MO_DBG_ERR("invalid fn"); + return nullptr; + } + + const char *fn = path + sizeof(MO_FILENAME_PREFIX) - 1; + + auto file = filesystem->open(path, "w"); + if (!file) { + return nullptr; + } + + IndexEntry *entry = nullptr; + if (!(entry = getEntryByFname(fn))) { + index.emplace_back(fn, 0); + entry = &index.back(); + } + + if (!entry) { + MO_DBG_ERR("internal error"); + return nullptr; + } + + entry->size = 0; //write always empties the file + + return std::unique_ptr(new IndexedFileAdapter(*this, entry->fname.c_str(), std::move(file))); + } else { + MO_DBG_ERR("only support r or w"); + return nullptr; + } + } + + bool remove(const char *path) override { + if (strlen(path) >= sizeof(MO_FILENAME_PREFIX) - 1) { + //valid path + const char *fn = path + sizeof(MO_FILENAME_PREFIX) - 1; + index.erase(std::remove_if(index.begin(), index.end(), + [fn] (const IndexEntry& el) -> bool { + return el.fname.compare(fn) == 0; + }), index.end()); + } + + return filesystem->remove(path); + } + + int ftw_root(std::function fn) { + // allow fn to remove elements + for (size_t it = 0; it < index.size();) { + auto size_before = index.size(); + auto err = fn(index[it].fname.c_str()); + if (err) { + return err; + } + if (index.size() + 1 == size_before) { + // element removed + continue; + } + // normal execution + it++; + } + + return 0; + } + + bool createIndex() { + if (!index.empty()) { + return false; + } + auto ret = filesystem->ftw_root([this] (const char *fn) -> int { + int ret; + char path [MO_MAX_PATH_SIZE]; + + ret = snprintf(path, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "%s", fn); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return 0; //ignore this entry and continue ftw + } + + size_t size; + ret = filesystem->stat(path, &size); + if (ret == 0) { + //add fn and size to index + MO_DBG_DEBUG("add file to index: %s (%zuB)", fn, size); + index.emplace_back(fn, size); + return 0; //successfully added filename to index + } else { + MO_DBG_ERR("unexpected entry: %s", fn); + return 0; //ignore this entry and continue ftw + } + }); + + MO_DBG_DEBUG("create fs index: %s, %zu entries", ret == 0 ? "success" : "failure", index.size()); + + return ret == 0; + } + + void updateFilesize(const char *fn, size_t size) { + if (auto entry = getEntryByFname(fn)) { + entry->size = size; + MO_DBG_DEBUG("update index: %s (%zuB)", entry->fname.c_str(), entry->size); + } + } +}; + +IndexedFileAdapter::~IndexedFileAdapter() { + index.updateFilesize(fn, written); +} + +std::shared_ptr decorateIndex(std::shared_ptr filesystem, void (*onDestruct)(void*) = nullptr) { + + auto fsIndex = std::allocate_shared(makeAllocator("FilesystemIndex"), std::move(filesystem), onDestruct); + if (!fsIndex) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + if (!fsIndex->createIndex()) { + MO_DBG_ERR("createIndex err"); + return nullptr; + } + + return fsIndex; +} + +} // namespace MicroOcpp + +#endif //MO_ENABLE_FILE_INDEX #if MO_USE_FILEAPI == ARDUINO_LITTLEFS || MO_USE_FILEAPI == ARDUINO_SPIFFS @@ -32,10 +265,10 @@ namespace MicroOcpp { -class ArduinoFileAdapter : public FileAdapter { +class ArduinoFileAdapter : public FileAdapter, public MemoryManaged { File file; public: - ArduinoFileAdapter(File&& file) : file(file) {} + ArduinoFileAdapter(File&& file) : MemoryManaged("Filesystem"), file(file) {} ~ArduinoFileAdapter() { if (file) { @@ -57,12 +290,14 @@ class ArduinoFileAdapter : public FileAdapter { } }; -class ArduinoFilesystemAdapter : public FilesystemAdapter { +class ArduinoFilesystemAdapter : public FilesystemAdapter, public MemoryManaged { private: bool valid = false; FilesystemOpt config; + + void (* onDestruct)(void*) = nullptr; public: - ArduinoFilesystemAdapter(FilesystemOpt config) : config(config) { + ArduinoFilesystemAdapter(FilesystemOpt config, void (*onDestruct)(void*) = nullptr) : MemoryManaged("Filesystem"), config(config), onDestruct(onDestruct) { valid = true; if (config.mustMount()) { @@ -94,6 +329,10 @@ class ArduinoFilesystemAdapter : public FilesystemAdapter { if (config.mustMount()) { USE_FS.end(); } + + if (onDestruct) { + onDestruct(this); + } } operator bool() {return valid;} @@ -199,6 +438,10 @@ class ArduinoFilesystemAdapter : public FilesystemAdapter { std::weak_ptr filesystemCache; +void resetFilesystemCache(void*) { + filesystemCache.reset(); +} + std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { if (auto cached = filesystemCache.lock()) { @@ -210,8 +453,13 @@ std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt co return nullptr; } - auto fs_concrete = new ArduinoFilesystemAdapter(config); - auto fs = std::shared_ptr(fs_concrete); + auto fs_concrete = new ArduinoFilesystemAdapter(config, resetFilesystemCache); + auto fs = std::shared_ptr(fs_concrete, std::default_delete(), makeAllocator("Filesystem")); + +#if MO_ENABLE_FILE_INDEX + fs = decorateIndex(fs, resetFilesystemCache); +#endif // MO_ENABLE_FILE_INDEX + filesystemCache = fs; if (*fs_concrete) { @@ -235,10 +483,10 @@ std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt co namespace MicroOcpp { -class EspIdfFileAdapter : public FileAdapter { +class EspIdfFileAdapter : public FileAdapter, public MemoryManaged { FILE *file {nullptr}; public: - EspIdfFileAdapter(FILE *file) : file(file) {} + EspIdfFileAdapter(FILE *file) : MemoryManaged("Filesystem"), file(file) {} ~EspIdfFileAdapter() { fclose(file); @@ -261,17 +509,23 @@ class EspIdfFileAdapter : public FileAdapter { } }; -class EspIdfFilesystemAdapter : public FilesystemAdapter { +class EspIdfFilesystemAdapter : public FilesystemAdapter, public MemoryManaged { public: FilesystemOpt config; + + void (* onDestruct)(void*) = nullptr; public: - EspIdfFilesystemAdapter(FilesystemOpt config) : config(config) { } + EspIdfFilesystemAdapter(FilesystemOpt config, void (* onDestruct)(void*) = nullptr) : MemoryManaged("Filesystem"), config(config), onDestruct(onDestruct) { } ~EspIdfFilesystemAdapter() { if (config.mustMount()) { esp_vfs_spiffs_unregister(MO_PARTITION_LABEL); MO_DBG_DEBUG("SPIFFS unmounted"); } + + if (onDestruct) { + onDestruct(this); + } } int stat(const char *path, size_t *size) override { @@ -332,6 +586,10 @@ class EspIdfFilesystemAdapter : public FilesystemAdapter { std::weak_ptr filesystemCache; +void resetFilesystemCache(void*) { + filesystemCache.reset(); +} + std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { if (auto cached = filesystemCache.lock()) { @@ -387,7 +645,12 @@ std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt co } if (mounted) { - auto fs = std::shared_ptr(new EspIdfFilesystemAdapter(config)); + auto fs = std::shared_ptr(new EspIdfFilesystemAdapter(config, resetFilesystemCache), std::default_delete(), makeAllocator("Filesystem")); + +#if MO_ENABLE_FILE_INDEX + fs = decorateIndex(fs, resetFilesystemCache); +#endif // MO_ENABLE_FILE_INDEX + filesystemCache = fs; return fs; } else { @@ -405,10 +668,10 @@ std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt co namespace MicroOcpp { -class PosixFileAdapter : public FileAdapter { +class PosixFileAdapter : public FileAdapter, public MemoryManaged { FILE *file {nullptr}; public: - PosixFileAdapter(FILE *file) : file(file) {} + PosixFileAdapter(FILE *file) : MemoryManaged("Filesystem"), file(file) {} ~PosixFileAdapter() { fclose(file); @@ -431,13 +694,19 @@ class PosixFileAdapter : public FileAdapter { } }; -class PosixFilesystemAdapter : public FilesystemAdapter { +class PosixFilesystemAdapter : public FilesystemAdapter, public MemoryManaged { public: FilesystemOpt config; + + void (* onDestruct)(void*) = nullptr; public: - PosixFilesystemAdapter(FilesystemOpt config) : config(config) { } + PosixFilesystemAdapter(FilesystemOpt config, void (* onDestruct)(void*) = nullptr) : MemoryManaged("Filesystem"), config(config), onDestruct(onDestruct) { } - ~PosixFilesystemAdapter() = default; + ~PosixFilesystemAdapter() { + if (onDestruct) { + onDestruct(this); + } + } int stat(const char *path, size_t *size) override { struct ::stat st; @@ -471,6 +740,9 @@ class PosixFilesystemAdapter : public FilesystemAdapter { 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; @@ -484,6 +756,10 @@ class PosixFilesystemAdapter : public FilesystemAdapter { std::weak_ptr filesystemCache; +void resetFilesystemCache(void*) { + filesystemCache.reset(); +} + std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { if (auto cached = filesystemCache.lock()) { @@ -499,7 +775,12 @@ std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt co MO_DBG_DEBUG("Skip mounting on UNIX host"); } - auto fs = std::shared_ptr(new PosixFilesystemAdapter(config)); + auto fs = std::shared_ptr(new PosixFilesystemAdapter(config, resetFilesystemCache), std::default_delete(), makeAllocator("Filesystem")); + +#if MO_ENABLE_FILE_INDEX + fs = decorateIndex(fs, resetFilesystemCache); +#endif // MO_ENABLE_FILE_INDEX + filesystemCache = fs; return fs; } diff --git a/src/MicroOcpp/Core/FilesystemAdapter.h b/src/MicroOcpp/Core/FilesystemAdapter.h index 3cb6c70c..7e92e7cf 100644 --- a/src/MicroOcpp/Core/FilesystemAdapter.h +++ b/src/MicroOcpp/Core/FilesystemAdapter.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_FILESYSTEMADAPTER_H @@ -51,6 +51,10 @@ #endif #endif +#ifndef MO_ENABLE_FILE_INDEX +#define MO_ENABLE_FILE_INDEX 0 +#endif + namespace MicroOcpp { class FileAdapter { diff --git a/src/MicroOcpp/Core/FilesystemUtils.cpp b/src/MicroOcpp/Core/FilesystemUtils.cpp index 22f85863..de6a034d 100644 --- a/src/MicroOcpp/Core/FilesystemUtils.cpp +++ b/src/MicroOcpp/Core/FilesystemUtils.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -9,7 +9,7 @@ using namespace MicroOcpp; -std::unique_ptr FilesystemUtils::loadJson(std::shared_ptr filesystem, const char *fn) { +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; @@ -50,13 +50,13 @@ std::unique_ptr FilesystemUtils::loadJson(std::shared_ptr(nullptr); + std::unique_ptr doc; DeserializationError err = DeserializationError::NoMemory; ArduinoJsonFileAdapter fileReader {file.get()}; while (err == DeserializationError::NoMemory && capacity <= MO_MAX_JSON_CAPACITY) { - doc.reset(new DynamicJsonDocument(capacity)); + doc = makeJsonDoc(memoryTag, capacity); err = deserializeJson(*doc, fileReader); capacity *= 2; @@ -75,7 +75,7 @@ std::unique_ptr FilesystemUtils::loadJson(std::shared_ptr filesystem, const char *fn, const DynamicJsonDocument& doc) { +bool FilesystemUtils::storeJson(std::shared_ptr filesystem, const char *fn, const JsonDoc& doc) { if (!filesystem || !fn || *fn == '\0') { MO_DBG_ERR("Format error"); return false; @@ -116,8 +116,8 @@ bool FilesystemUtils::storeJson(std::shared_ptr filesystem, c } bool FilesystemUtils::remove_if(std::shared_ptr filesystem, std::function pred) { - return filesystem->ftw_root([filesystem, pred] (const char *fpath) { - if (pred(fpath) && fpath[0] != '.') { + auto ret = filesystem->ftw_root([filesystem, pred] (const char *fpath) { + if (pred(fpath)) { char fn [MO_MAX_PATH_SIZE] = {'\0'}; auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "%s", fpath); @@ -130,5 +130,11 @@ bool FilesystemUtils::remove_if(std::shared_ptr filesystem, s //no error handling - just skip failed file } return 0; - }) == 0; + }); + + if (ret != 0) { + MO_DBG_ERR("ftw_root: %i", ret); + } + + return ret == 0; } diff --git a/src/MicroOcpp/Core/FilesystemUtils.h b/src/MicroOcpp/Core/FilesystemUtils.h index 1845d3cd..31a36a73 100644 --- a/src/MicroOcpp/Core/FilesystemUtils.h +++ b/src/MicroOcpp/Core/FilesystemUtils.h @@ -1,11 +1,12 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_FILESYSTEMUTILS_H #define MO_FILESYSTEMUTILS_H #include +#include #include #include @@ -36,8 +37,8 @@ class ArduinoJsonFileAdapter { namespace FilesystemUtils { -std::unique_ptr loadJson(std::shared_ptr filesystem, const char *fn); -bool storeJson(std::shared_ptr filesystem, const char *fn, const DynamicJsonDocument& doc); +std::unique_ptr loadJson(std::shared_ptr filesystem, const char *fn, const char *memoryTag = nullptr); +bool storeJson(std::shared_ptr filesystem, const char *fn, const JsonDoc& doc); bool remove_if(std::shared_ptr filesystem, std::function pred); diff --git a/src/MicroOcpp/Core/Ftp.h b/src/MicroOcpp/Core/Ftp.h new file mode 100644 index 00000000..053f9e01 --- /dev/null +++ b/src/MicroOcpp/Core/Ftp.h @@ -0,0 +1,99 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_FTP_H +#define MO_FTP_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + MO_FtpCloseReason_Undefined, + MO_FtpCloseReason_Success, + MO_FtpCloseReason_Failure +} MO_FtpCloseReason; + +typedef struct ocpp_ftp_download { + void *user_data; //set this at your choice. MO passes it back to the functions below + + void (*loop)(void *user_data); + void (*is_active)(void *user_data); +} ocpp_ftp_download; + +typedef struct ocpp_ftp_upload { + void *user_data; //set this at your choice. MO passes it back to the functions below + + void (*loop)(void *user_data); + void (*is_active)(void *user_data); +} ocpp_ftp_upload; + +typedef struct ocpp_ftp_client { + void *user_data; //set this at your choice. MO passes it back to the functions below + + void (*loop)(void *user_data); + + ocpp_ftp_download* (*get_file)(void *user_data, + const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename + size_t (*file_writer)(void *mo_data, unsigned char *data, size_t len), + void (*on_close)(void *mo_data, MO_FtpCloseReason reason), + void *mo_data, + const char *ca_cert); // nullptr to disable cert check; will be ignored for non-TLS connections + + void (*get_file_free)(void *user_data, ocpp_ftp_download*); + + ocpp_ftp_upload* (*post_file)(void *user_data, + const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename + size_t (*file_reader)(void *mo_data, unsigned char *buf, size_t bufsize), + void (*on_close)(void *mo_data, MO_FtpCloseReason reason), + void *mo_data, + const char *ca_cert); // nullptr to disable cert check; will be ignored for non-TLS connections + + void (*post_file_free)(void *user_data, ocpp_ftp_upload*); +} ocpp_ftp_client; + +#ifdef __cplusplus +} //extern "C" + +#include +#include + +namespace MicroOcpp { + +class FtpDownload { +public: + virtual ~FtpDownload() = default; + virtual void loop() = 0; + virtual bool isActive() = 0; +}; + +class FtpUpload { +public: + virtual ~FtpUpload() = default; + virtual void loop() = 0; + virtual bool isActive() = 0; +}; + +class FtpClient { +public: + virtual ~FtpClient() = default; + + virtual std::unique_ptr getFile( + const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename + std::function fileWriter, + std::function onClose, + const char *ca_cert = nullptr) = 0; // nullptr to disable cert check; will be ignored for non-TLS connections + + virtual std::unique_ptr postFile( + const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename + std::function fileReader, //write at most buffsize bytes into out-buffer. Return number of bytes written + std::function onClose, + const char *ca_cert = nullptr) = 0; // nullptr to disable cert check; will be ignored for non-TLS connections +}; + +} // namespace MicroOcpp +#endif //def __cplusplus +#endif diff --git a/src/MicroOcpp/Core/FtpMbedTLS.cpp b/src/MicroOcpp/Core/FtpMbedTLS.cpp new file mode 100644 index 00000000..b40015b9 --- /dev/null +++ b/src/MicroOcpp/Core/FtpMbedTLS.cpp @@ -0,0 +1,956 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_MBEDTLS + +#include +#include + +#include "mbedtls/net_sockets.h" +#include "mbedtls/ssl.h" +#include "mbedtls/entropy.h" +#include "mbedtls/ctr_drbg.h" +#include "mbedtls/x509.h" +#include "mbedtls/error.h" + +#include +#include + +namespace MicroOcpp { + +class FtpTransferMbedTLS : public FtpUpload, public FtpDownload, public MemoryManaged { +private: + //MbedTLS common + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_ssl_config conf; + mbedtls_x509_crt cacert; + mbedtls_x509_crt clicert; + mbedtls_pk_context pkey; + const char *ca_cert = nullptr; + const char *client_cert = nullptr; + const char *client_key = nullptr; + bool isSecure = false; //tls policy + + //control connection specific + mbedtls_net_context ctrl_fd; + mbedtls_ssl_context ctrl_ssl; + bool ctrl_opened = false; + bool ctrl_ssl_established = false; + + //data connection specific + mbedtls_net_context data_fd; + mbedtls_ssl_context data_ssl; + bool data_opened = false; + bool data_ssl_established = false; + bool data_conn_accepted = false; //Server sent okay to upload / download data + + //FTP URL + String user; + String pass; + String ctrl_host; + String ctrl_port; + String dir; + String fname; + + String data_host; + String data_port; + + bool read_url_ctrl(const char *ftp_url); + bool read_url_data(const char *data_url); + + std::function fileWriter; + std::function fileReader; + std::function onClose; + + enum class Method { + Retrieve, //download file + Store, //upload file + UNDEFINED + }; + Method method = Method::UNDEFINED; + + int setup_tls(); + int connect(mbedtls_net_context& fd, mbedtls_ssl_context& ssl, const char *server_name, const char *server_port); + int connect_ctrl(); + int connect_data(); + void close_ctrl(); + void close_data(MO_FtpCloseReason reason); + + int handshake_tls(); + + void send_cmd(const char *cmd, const char *arg = nullptr, bool disable_tls_policy = false); + + void process_ctrl(); + void process_data(); + + unsigned char *data_buf = nullptr; + size_t data_buf_size = 4096; + size_t data_buf_avail = 0; + size_t data_buf_offs = 0; + +public: + FtpTransferMbedTLS(bool tls_only = false, const char *client_cert = nullptr, const char *client_key = nullptr); + ~FtpTransferMbedTLS(); + + void loop() override; + + bool isActive() override; + + bool getFile(const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename + std::function fileWriter, + std::function onClose, + const char *ca_cert = nullptr); // nullptr to disable cert check; will be ignored for non-TLS connections + + bool postFile(const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename + std::function fileReader, //write at most buffsize bytes into out-buffer. Return number of bytes written + std::function onClose, + const char *ca_cert = nullptr); // nullptr to disable cert check; will be ignored for non-TLS connections +}; + +class FtpClientMbedTLS : public FtpClient, public MemoryManaged { +private: + const char *client_cert = nullptr; + const char *client_key = nullptr; + bool tls_only = false; //tls policy +public: + + FtpClientMbedTLS(bool tls_only = false, const char *client_cert = nullptr, const char *client_key = nullptr); + + std::unique_ptr getFile(const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename + std::function fileWriter, + std::function onClose, + const char *ca_cert = nullptr) override; // nullptr to disable cert check; will be ignored for non-TLS connections + + std::unique_ptr postFile(const char *ftp_url, // ftp[s]://[user[:pass]@]host[:port][/directory]/filename + std::function fileReader, //write at most buffsize bytes into out-buffer. Return number of bytes written + std::function onClose, + const char *ca_cert = nullptr) override; // nullptr to disable cert check; will be ignored for non-TLS connections +}; + +std::unique_ptr makeFtpClientMbedTLS(bool tls_only, const char *client_cert, const char *client_key) { + return std::unique_ptr(new FtpClientMbedTLS(tls_only, client_cert, client_key)); +} + +void mo_mbedtls_log(void *user, int level, const char *file, int line, const char *str) { + + /* + * MbedTLS debug level documented in mbedtls/debug.h: + * - 0 No debug + * - 1 Error + * - 2 State change + * - 3 Informational + * - 4 Verbose + * + * To change the debug level, use the build flag MO_DBG_LEVEL_MBEDTLS accordingly + */ + const char *lstr = ""; + if (level <= 1) { + lstr = "ERROR"; + } else if (level <= 3) { + lstr = "debug"; + } else { + lstr = "verbose"; + } + + MO_CONSOLE_PRINTF("[MO] %s (%s:%i): %s\n", lstr, file, line, str); +} + +/* + * FTP implementation + */ + +FtpTransferMbedTLS::FtpTransferMbedTLS(bool tls_only, const char *client_cert, const char *client_key) : + MemoryManaged("FTP.TransferMbedTLS"), + client_cert(client_cert), + client_key(client_key), + isSecure(tls_only), + user(makeString(getMemoryTag())), + pass(makeString(getMemoryTag())), + ctrl_host(makeString(getMemoryTag())), + ctrl_port(makeString(getMemoryTag())), + dir(makeString(getMemoryTag())), + fname(makeString(getMemoryTag())), + data_host(makeString(getMemoryTag())), + data_port(makeString(getMemoryTag())) { + + mbedtls_net_init(&ctrl_fd); + mbedtls_ssl_init(&ctrl_ssl); + mbedtls_net_init(&data_fd); + mbedtls_ssl_init(&data_ssl); + mbedtls_ssl_config_init(&conf); + mbedtls_x509_crt_init(&cacert); + mbedtls_x509_crt_init(&clicert); + mbedtls_pk_init(&pkey); + mbedtls_ctr_drbg_init(&ctr_drbg); + mbedtls_entropy_init(&entropy); +} + +FtpTransferMbedTLS::~FtpTransferMbedTLS() { + if (onClose) { + onClose(MO_FtpCloseReason_Failure); //data connection not closed properly + onClose = nullptr; + } + MO_FREE(data_buf); + mbedtls_x509_crt_free(&clicert); + mbedtls_x509_crt_free(&cacert); + mbedtls_pk_free(&pkey); + mbedtls_ssl_config_free(&conf); + mbedtls_ctr_drbg_free(&ctr_drbg); + mbedtls_entropy_free(&entropy); + mbedtls_net_free(&ctrl_fd); + mbedtls_ssl_free(&ctrl_ssl); + mbedtls_net_free(&data_fd); + mbedtls_ssl_free(&data_ssl); +} + +int FtpTransferMbedTLS::setup_tls() { + + if (auto ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, + (const unsigned char*) __FILE__, + strlen(__FILE__)) != 0) { + MO_DBG_ERR("mbedtls_ctr_drbg_seed: %i", ret); + return ret; + } + + if (ca_cert) { + if (auto ret = mbedtls_x509_crt_parse(&cacert, (const unsigned char *) ca_cert, + strlen(ca_cert)) < 0) { + MO_DBG_ERR("mbedtls_x509_crt_parse(ca_cert): %i", ret); + return ret; + } + } + + if (client_cert) { + if (auto ret = mbedtls_x509_crt_parse(&clicert, (const unsigned char *) client_cert, + strlen(client_cert))) { + MO_DBG_ERR("mbedtls_x509_crt_parse(client_cert): %i", ret); + return ret; + } + } + + if (client_key) { + if (auto ret = mbedtls_pk_parse_key(&pkey, + (const unsigned char *) client_key, + strlen(client_key), + NULL, + 0)) { + MO_DBG_ERR("mbedtls_pk_parse_key: %i", ret); + return ret; + } + } + + if (auto ret = mbedtls_ssl_config_defaults(&conf, + MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT) != 0) { + MO_DBG_ERR("mbedtls_ssl_config_defaults: %i", ret); + return ret; + } + + mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_OPTIONAL); //certificate check result manually handled for now + + mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &ctr_drbg); + mbedtls_ssl_conf_dbg(&conf, mo_mbedtls_log, NULL); + + if (ca_cert) { + mbedtls_ssl_conf_ca_chain(&conf, &cacert, NULL); + } + + if (client_cert || client_key) { + if (auto ret = mbedtls_ssl_conf_own_cert(&conf, &clicert, &pkey) != 0) { + MO_DBG_ERR("mbedtls_ssl_conf_own_cert: %i", ret); + return ret; + } + } + + return 0; //success +} + +int FtpTransferMbedTLS::connect(mbedtls_net_context& fd, mbedtls_ssl_context& ssl, const char *server_name, const char *server_port) { + + if (auto ret = mbedtls_net_connect(&fd, server_name, server_port, MBEDTLS_NET_PROTO_TCP) != 0) { + MO_DBG_ERR("mbedtls_net_connect: %i", ret); + return ret; + } + + if (auto ret = mbedtls_net_set_nonblock(&fd)) { + MO_DBG_ERR("mbedtls_net_set_nonblock: %i", ret); + return ret; + } + + if (auto ret = mbedtls_ssl_setup(&ssl, &conf) != 0) { + MO_DBG_ERR("mbedtls_ssl_setup: %i", ret); + return ret; + } + + if (auto ret = mbedtls_ssl_set_hostname(&ssl, server_name) != 0) { + MO_DBG_ERR("mbedtls_ssl_set_hostname: %i", ret); + return ret; + } + + mbedtls_ssl_set_bio(&ssl, &fd, mbedtls_net_send, mbedtls_net_recv, NULL); + + return 0; //success +} + +int FtpTransferMbedTLS::connect_ctrl() { + if (auto ret = connect(ctrl_fd, ctrl_ssl, ctrl_host.c_str(), ctrl_port.c_str())) { + MO_DBG_ERR("connect: %i", ret); + return ret; + } + + ctrl_opened = true; + + //handshake will be done later during STARTTLS procedure + + return 0; //success +} + +int FtpTransferMbedTLS::connect_data() { + if (auto ret = connect(data_fd, data_ssl, data_host.c_str(), data_port.c_str())) { + MO_DBG_ERR("connect: %i", ret); + return ret; + } + + data_opened = true; + + if (isSecure) { + //reuse SSL session of ctrl conn + + if (auto ret = mbedtls_ssl_set_session(&data_ssl, + mbedtls_ssl_get_session_pointer(&ctrl_ssl))) { + MO_DBG_ERR("session reuse failure: %i", ret); + return ret; + } + + data_ssl_established = true; + } + + if (!data_buf) { + data_buf = static_cast(MO_MALLOC(getMemoryTag(), data_buf_size)); + if (!data_buf) { + MO_DBG_ERR("OOM"); + return -1; + } + memset(data_buf, 0, data_buf_size); + } + + return 0; //success +} + +void FtpTransferMbedTLS::close_ctrl() { + if (!ctrl_opened) { + return; + } + + if (ctrl_ssl_established) { + mbedtls_ssl_close_notify(&ctrl_ssl); + ctrl_ssl_established = false; + } + mbedtls_net_free(&ctrl_fd); + ctrl_opened = false; + + if (onClose && !data_opened) { + onClose(MO_FtpCloseReason_Failure); //data connection has never been opened --> failure + onClose = nullptr; + } +} + +void FtpTransferMbedTLS::close_data(MO_FtpCloseReason reason) { + if (!data_opened) { + return; + } + + MO_DBG_DEBUG("closing data conn"); + + if (data_ssl_established) { + MO_DBG_DEBUG("TLS shutdown"); + mbedtls_ssl_close_notify(&data_ssl); + data_ssl_established = false; + } + mbedtls_net_free(&data_fd); + data_opened = false; + data_conn_accepted = false; + + if (onClose) { + onClose(reason); + onClose = nullptr; + } +} + +int FtpTransferMbedTLS::handshake_tls() { + + int ret; + while ((ret = mbedtls_ssl_handshake(&ctrl_ssl)) != 0) { + if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE && ret != 1) { + char buf [1024]; + mbedtls_strerror(ret, (char *) buf, 1024); + MO_DBG_ERR("mbedtls_ssl_handshake: %i, %s", ret, buf); + return ret; + } + } + + if (ca_cert) { + //certificate validation enabled + + if ((ret = mbedtls_ssl_get_verify_result(&ctrl_ssl)) != 0) { + char vrfy_buf[512]; + mbedtls_x509_crt_verify_info(vrfy_buf, sizeof(vrfy_buf), " > ", ret); + MO_DBG_ERR("mbedtls_ssl_get_verify_result: %i, %s", ret, vrfy_buf); + return ret; + } + } + + ctrl_ssl_established = true; + + return 0; //success +} + +void FtpTransferMbedTLS::send_cmd(const char *cmd, const char *arg, bool disable_tls_policy) { + + const size_t MSG_SIZE = 128; + unsigned char msg [MSG_SIZE]; + + auto len = snprintf((char*) msg, MSG_SIZE, "%s%s%s\r\n", + cmd, //cmd mandatory (e.g. "USER") + arg ? " " : "", //line spacing if arg is provided + arg ? arg : ""); //arg optional (e.g. "anonymous") + if (len < 0 || (size_t)len >= MSG_SIZE) { + MO_DBG_ERR("could not write cmd, send QUIT instead"); + len = sprintf((char*) msg, "QUIT\r\n"); + } else { + //show outgoing traffic for debug, but shadow PASS + MO_DBG_DEBUG("SEND: %s %s", + cmd, + !strncmp((char*) cmd, "PASS", strlen("PASS")) ? "***" : arg ? (char*) arg : ""); + } + + int ret = -1; + + if (ctrl_ssl_established) { + ret = mbedtls_ssl_write(&ctrl_ssl, (unsigned char*) msg, len); + } else if (!isSecure || disable_tls_policy) { + ret = mbedtls_net_send(&ctrl_fd, (unsigned char*) msg, len); + } else { + MO_DBG_ERR("TLS policy failure"); + len = strlen("QUIT\r\n"); + ret = mbedtls_net_send(&ctrl_fd, (unsigned char*) "QUIT\r\n", len); + } + + if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE || + ret <= 0 || + ret < (int) len) { + char buf [1024]; + mbedtls_strerror(ret, (char *) buf, 1024); + MO_DBG_ERR("fatal - message on ctrl channel lost: %i, %s", ret, buf); + close_ctrl(); + return; + } +} + +bool FtpTransferMbedTLS::getFile(const char *ftp_url_raw, std::function fileWriter, std::function onClose, const char *ca_cert) { + + if (method != Method::UNDEFINED) { + MO_DBG_ERR("FTP Client reuse not supported"); + return false; + } + + if (!ftp_url_raw || !fileWriter) { + MO_DBG_ERR("invalid args"); + return false; + } + + this->ca_cert = ca_cert; + this->method = Method::Retrieve; + this->fileWriter = fileWriter; + this->onClose = onClose; + + if (!read_url_ctrl(ftp_url_raw)) { + MO_DBG_ERR("could not parse URL"); + return false; + } + + MO_DBG_DEBUG("init download from %s: %s", ctrl_host.c_str(), fname.c_str()); + + if (auto ret = setup_tls()) { + MO_DBG_ERR("could not setup MbedTLS: %i", ret); + return false; + } + + if (auto ret = connect_ctrl()) { + MO_DBG_ERR("could not establish connection to FTP server: %i", ret); + return false; + } + + return true; +} + +bool FtpTransferMbedTLS::postFile(const char *ftp_url_raw, std::function fileReader, std::function onClose, const char *ca_cert) { + + if (method != Method::UNDEFINED) { + MO_DBG_ERR("FTP Client reuse not supported"); + return false; + } + + if (!ftp_url_raw || !fileReader) { + MO_DBG_ERR("invalid args"); + return false; + } + + MO_DBG_DEBUG("init upload %s", ftp_url_raw); + + this->ca_cert = ca_cert; + this->method = Method::Store; + this->fileReader = fileReader; + this->onClose = onClose; + + if (!read_url_ctrl(ftp_url_raw)) { + MO_DBG_ERR("could not parse URL"); + return false; + } + + if (auto ret = setup_tls()) { + MO_DBG_ERR("could not setup MbedTLS: %i", ret); + return false; + } + + if (auto ret = connect_ctrl()) { + MO_DBG_ERR("could not establish connection to FTP server: %i", ret); + return false; + } + + return true; +} + +void FtpTransferMbedTLS::process_ctrl() { + // read input (if available) + + const size_t INBUF_SIZE = 128; + unsigned char inbuf [INBUF_SIZE]; + memset(inbuf, 0, INBUF_SIZE); + + int ret = -1; + + if (ctrl_ssl_established) { + ret = mbedtls_ssl_read(&ctrl_ssl, inbuf, INBUF_SIZE - 1); + } else { + ret = mbedtls_net_recv(&ctrl_fd, inbuf, INBUF_SIZE - 1); + } + + if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + //no new input data to be processed + return; + } else if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || ret == 0) { + MO_DBG_ERR("FTP transfer aborted"); + close_ctrl(); + return; + } else if (ret < 0) { + MO_DBG_ERR("mbedtls_net_recv: %i", ret); + send_cmd("QUIT"); + close_ctrl(); + return; + } + + size_t inbuf_len = ret; + + // read multi-line command + char *line_next = (char*) inbuf; + while (line_next < (char*) inbuf + inbuf_len) { + + // take current line + char *line = line_next; + + // null-terminate current line and find begin of next line + while (line_next + 1 < (char*) inbuf + inbuf_len && *line_next != '\n') { + line_next++; + } + *line_next = '\0'; + line_next++; + + MO_DBG_DEBUG("RECV: %s", line); + + if (isSecure && !ctrl_ssl_established) { //tls not established yet, set up according to RFC 4217 + if (!strncmp("220", line, 3)) { + MO_DBG_DEBUG("start TLS negotiation"); + send_cmd("AUTH TLS", nullptr, true); + return; + } else if (!strncmp("234", line, 3)) { // Proceed with TLS negotiation + MO_DBG_DEBUG("upgrade to TLS"); + + if (auto ret = handshake_tls()) { + MO_DBG_ERR("handshake: %i", ret); + send_cmd("QUIT", nullptr, true); + return; + } + } else { + MO_DBG_ERR("cannot proceed without TLS"); + send_cmd("QUIT", nullptr, true); + return; + } + } + + if (isSecure && !ctrl_ssl_established) { + //failure to establish security policy + MO_DBG_ERR("internal error"); + send_cmd("QUIT", nullptr, true); + return; + } + + //security policy met + + if (!strncmp("530", line, 3) // Not logged in + || !strncmp("220", line, 3) // Service ready for new user + || !strncmp("234", line, 3)) { // Just completed AUTH TLS handshake + MO_DBG_DEBUG("select user %s", user.empty() ? "anonymous" : user.c_str()); + send_cmd("USER", user.empty() ? "anonymous" : user.c_str()); + } else if (!strncmp("331", line, 3)) { // User name okay, need password + MO_DBG_DEBUG("enter pass %.2s***", pass.empty() ? "-" : pass.c_str()); + send_cmd("PASS", pass.c_str()); + } else if (!strncmp("230", line, 3)) { // User logged in, proceed + MO_DBG_DEBUG("select directory %s", dir.empty() ? "/" : dir.c_str()); + send_cmd("CWD", dir.empty() ? "/" : dir.c_str()); + } else if (!strncmp("250", line, 3)) { // Requested file action okay, completed + MO_DBG_DEBUG("enter passive mode"); + if (isSecure) { + send_cmd("PBSZ 0\r\n" + "PROT P\r\n" //RFC 4217: set FTP session Private + "PASV"); + } else { + send_cmd("PASV"); + } + } else if (!strncmp("227", line, 3)) { // Entering Passive Mode (h1,h2,h3,h4,p1,p2) + + if (!read_url_data(line + 3)) { //trim leading response code + MO_DBG_ERR("could not process data url. Expect format: (h1,h2,h3,h4,p1,p2)"); + send_cmd("QUIT"); + return; + } + + if (auto ret = connect_data()) { + MO_DBG_ERR("data connection failure: %i", ret); + send_cmd("QUIT"); + return; + } + + if (method == Method::Retrieve) { + MO_DBG_DEBUG("request download for %s", fname.c_str()); + send_cmd("RETR", fname.c_str()); + } else if (method == Method::Store) { + MO_DBG_DEBUG("request upload for %s", fname.c_str()); + send_cmd("STOR", fname.c_str()); + } else { + MO_DBG_ERR("internal error"); + send_cmd("QUIT"); + return; + } + + } else if (!strncmp("150", line, 3) // File status okay; about to open data connection + || !strncmp("125", line, 3)) { // Data connection already open + MO_DBG_DEBUG("data connection accepted"); + data_conn_accepted = true; + } else if (!strncmp("226", line, 3)) { // Closing data connection. Requested file action successful (for example, file transfer or file abort) + MO_DBG_INFO("FTP success: %s", line); + send_cmd("QUIT"); + return; + } else if (!strncmp("55", line, 2)) { // Requested action not taken / aborted + MO_DBG_WARN("FTP failure: %s", line); + send_cmd("QUIT"); + return; + } else if (!strncmp("200", line, 3)) { //PBSZ -> 0 and PROT -> P accepted + MO_DBG_INFO("PBSZ/PROT success: %s", line); + } else if (!strncmp("221", line, 3)) { // Server Goodbye + MO_DBG_DEBUG("closing ctrl connection"); + close_ctrl(); + return; + } else { + MO_DBG_WARN("unkown commad (close connection): %s", line); + send_cmd("QUIT"); + return; + } + } +} + +void FtpTransferMbedTLS::process_data() { + if (!data_conn_accepted) { + return; + } + + if (isSecure && !data_ssl_established) { + //failure to establish security policy + MO_DBG_ERR("internal error"); + close_data(MO_FtpCloseReason_Failure); + send_cmd("QUIT", nullptr, true); + return; + } + + if (method == Method::Retrieve) { + + if (data_buf_avail == 0) { + //load new data from socket + + data_buf_offs = 0; + + int ret = -1; + if (data_ssl_established) { + ret = mbedtls_ssl_read(&data_ssl, data_buf, data_buf_size - 1); + } else { + ret = mbedtls_net_recv(&data_fd, data_buf, data_buf_size - 1); + } + + if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + //no new input data to be processed + return; + } else if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || ret == 0) { + //download finished + close_data(MO_FtpCloseReason_Success); + return; + } else if (ret < 0) { + MO_DBG_ERR("mbedtls_net_recv: %i", ret); + close_data(MO_FtpCloseReason_Failure); + send_cmd("QUIT"); + return; + } + + data_buf_avail = ret; + } + + auto ret = fileWriter(data_buf + data_buf_offs, data_buf_avail); + + if (ret == 0) { + MO_DBG_ERR("fileWriter aborted download"); + close_data(MO_FtpCloseReason_Failure); + send_cmd("QUIT"); + return; + } else if (ret <= data_buf_avail) { + data_buf_avail -= ret; + data_buf_offs += ret; + } else { + MO_DBG_ERR("write error"); + close_data(MO_FtpCloseReason_Failure); + send_cmd("QUIT"); + return; + } + + //success + } else if (method == Method::Store) { + + if (data_buf_avail == 0) { + //load new data from file to write on socket + + data_buf_offs = 0; + + data_buf_avail = fileReader(data_buf, data_buf_size); + } + + if (data_buf_avail > 0) { + + int ret = -1; + if (data_ssl_established) { + ret = mbedtls_ssl_write(&data_ssl, data_buf + data_buf_offs, data_buf_avail); + } else { + ret = mbedtls_net_send(&data_fd, data_buf + data_buf_offs, data_buf_avail); + } + + if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + //no data sent, wait + return; + } else if (ret <= 0) { + MO_DBG_ERR("mbedtls_ssl_write: %i", ret); + close_data(MO_FtpCloseReason_Failure); + send_cmd("QUIT"); + return; + } + + //successful write + data_buf_avail -= ret; + data_buf_offs += ret; + } else { + //no data in fileReader anymore + MO_DBG_DEBUG("finished file reading"); + close_data(MO_FtpCloseReason_Success); + } + } +} + +void FtpTransferMbedTLS::loop() { + + if (ctrl_opened) { + process_ctrl(); + } + + if (data_opened) { + process_data(); + } +} + +bool FtpTransferMbedTLS::isActive() { + return ctrl_opened || data_opened; +} + +bool FtpTransferMbedTLS::read_url_ctrl(const char *ftp_url_raw) { + String ftp_url = makeString(getMemoryTag(), ftp_url_raw); //copy input ftp_url + + //tolower protocol specifier + for (auto c = ftp_url.begin(); *c != ':' && c != ftp_url.end(); c++) { + *c = tolower(*c); + } + + //parse FTP URL: protocol specifier + String proto = makeString(getMemoryTag()); + if (!strncmp(ftp_url.c_str(), "ftps://", strlen("ftps://"))) { + //FTP over TLS (RFC 4217) + proto = "ftps://"; + isSecure = true; //TLS policy + } else if (!strncmp(ftp_url.c_str(), "ftp://", strlen("ftp://"))) { + //FTP without security policies (RFC 959) + proto = "ftp://"; + } else { + MO_DBG_ERR("protocol not supported. Please use ftps:// or ftp://"); + return false; + } + + //parse FTP URL: dir and fname + auto dir_pos = ftp_url.find_first_of('/', proto.length()); + if (dir_pos != String::npos) { + auto fname_pos = ftp_url.find_last_of('/'); + dir = ftp_url.substr(dir_pos, fname_pos - dir_pos); + fname = ftp_url.substr(fname_pos + 1); + } + + if (fname.empty()) { + MO_DBG_ERR("missing filename"); + return false; + } + + MO_DBG_DEBUG("parsed dir: %s; fname: %s", dir.c_str(), fname.c_str()); + + //parse FTP URL: user, pass, host, port + + String user_pass_host_port = ftp_url.substr(proto.length(), dir_pos - proto.length()); + String user_pass = makeString(getMemoryTag()); + String host_port = makeString(getMemoryTag()); + auto user_pass_delim = user_pass_host_port.find_first_of('@'); + if (user_pass_delim != String::npos) { + host_port = user_pass_host_port.substr(user_pass_delim + 1); + user_pass = user_pass_host_port.substr(0, user_pass_delim); + } else { + host_port = user_pass_host_port; + } + + if (!user_pass.empty()) { + auto user_delim = user_pass.find_first_of(':'); + if (user_delim != String::npos) { + user = user_pass.substr(0, user_delim); + pass = user_pass.substr(user_delim + 1); + } else { + user = user_pass; + } + } + + MO_DBG_DEBUG("parsed user: %s; pass: %.2s***", user.c_str(), pass.empty() ? "-" : pass.c_str()); + + if (host_port.empty()) { + MO_DBG_ERR("missing hostname"); + return false; + } + + auto host_port_delim = host_port.find(':'); + if (host_port_delim != String::npos) { + ctrl_host = host_port.substr(0, host_port_delim); + ctrl_port = host_port.substr(host_port_delim + 1); + } else { + //use default port number + ctrl_host = host_port; + ctrl_port = "21"; + } + + MO_DBG_DEBUG("parsed host: %s; port: %s", ctrl_host.c_str(), ctrl_port.c_str()); + + return true; +} + +bool FtpTransferMbedTLS::read_url_data(const char *data_url_raw) { + + String data_url = makeString(getMemoryTag(), data_url_raw); //format like " Entering Passive Mode (h1,h2,h3,h4,p1,p2)" + + // parse address field. Replace all non-digits by delimiter character ' ' + for (char& c : data_url) { + if (c < '0' || c > '9') { + c = (unsigned char) ' '; + } + } + + unsigned int h1 = 0, h2 = 0, h3 = 0, h4 = 0, p1 = 0, p2 = 0; + + auto ntokens = sscanf(data_url.c_str(), "%u %u %u %u %u %u", &h1, &h2, &h3, &h4, &p1, &p2); + if (ntokens != 6) { + MO_DBG_ERR("could not process data url. Expect format: (h1,h2,h3,h4,p1,p2)"); + return false; + } + + unsigned int port = 256U * p1 + p2; + + char buf [64] = {'\0'}; + auto ret = snprintf(buf, 64, "%u.%u.%u.%u", h1, h2, h3, h4); + if (ret < 0 || ret >= 64) { + MO_DBG_ERR("data url format failure"); + return false; + } + data_host = buf; + + ret = snprintf(buf, 64, "%u", port); + if (ret < 0 || ret >= 64) { + MO_DBG_ERR("data url format failure"); + return false; + } + data_port = buf; + + return true; +} + +FtpClientMbedTLS::FtpClientMbedTLS(bool tls_only, const char *client_cert, const char *client_key) + : MemoryManaged("FTP.ClientMbedTLS"), client_cert(client_cert), client_key(client_key), tls_only(tls_only) { + +} + +std::unique_ptr FtpClientMbedTLS::getFile(const char *ftp_url_raw, std::function fileWriter, std::function onClose, const char *ca_cert) { + + auto ftp_handle = std::unique_ptr(new FtpTransferMbedTLS(tls_only, client_cert, client_key)); + if (!ftp_handle) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + bool success = ftp_handle->getFile(ftp_url_raw, fileWriter, onClose, ca_cert); + + if (success) { + return ftp_handle; + } else { + return nullptr; + } +} + +std::unique_ptr FtpClientMbedTLS::postFile(const char *ftp_url_raw, std::function fileReader, std::function onClose, const char *ca_cert) { + + auto ftp_handle = std::unique_ptr(new FtpTransferMbedTLS(tls_only, client_cert, client_key)); + if (!ftp_handle) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + bool success = ftp_handle->postFile(ftp_url_raw, fileReader, onClose, ca_cert); + + if (success) { + return ftp_handle; + } else { + return nullptr; + } +} + +} //namespace MicroOcpp + +#endif //MO_ENABLE_MBEDTLS diff --git a/src/MicroOcpp/Core/FtpMbedTLS.h b/src/MicroOcpp/Core/FtpMbedTLS.h new file mode 100644 index 00000000..799b3905 --- /dev/null +++ b/src/MicroOcpp/Core/FtpMbedTLS.h @@ -0,0 +1,40 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_FTP_MBEDTLS_H +#define MO_FTP_MBEDTLS_H + +/* + * Built-in FTP client (depends on MbedTLS) + * + * Moved from https://github.com/matth-x/MicroFtp + * + * Currently, the compatibility with the following FTP servers has been tested: + * + * | Server | FTP | FTPS | + * | --------------------------------------------------------------------- | --- | ---- | + * | [vsftp](https://security.appspot.com/vsftpd.html) | | x | + * | [Rebex](https://www.rebex.net/) | x | x | + * | [Windows Server 2022](https://www.microsoft.com/en-us/windows-server) | x | x | + * | [SFTPGo](https://github.com/drakkan/sftpgo) | x | | + * + */ + +#include + +#if MO_ENABLE_MBEDTLS + +#include + +#include + +namespace MicroOcpp { + +std::unique_ptr makeFtpClientMbedTLS(bool tls_only = false, const char *client_cert = nullptr, const char *client_key = nullptr); + +} //namespace MicroOcpp + +#endif //MO_ENABLE_MBEDTLS + +#endif diff --git a/src/MicroOcpp/Core/Memory.cpp b/src/MicroOcpp/Core/Memory.cpp new file mode 100644 index 00000000..a352e9ac --- /dev/null +++ b/src/MicroOcpp/Core/Memory.cpp @@ -0,0 +1,328 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include + +#if MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER + +#include + +namespace MicroOcpp { +namespace Memory { + +struct MemBlockInfo { + void* tagger_ptr = nullptr; + std::string tag; + size_t size = 0; + + MemBlockInfo(void* ptr, const char *tag, size_t size) : size{size} { + updateTag(ptr, tag); + } + + void updateTag(void* ptr, const char *tag); +}; + +std::map memBlocks; //key: memory address of malloc'd block + +struct MemTagInfo { + size_t current_size = 0; + size_t max_size = 0; + + MemTagInfo(size_t size) { + operator+=(size); + } + + void operator+=(size_t size) { + current_size += size; + max_size = std::max(max_size, current_size); + } + + void operator-=(size_t size) { + if (size > current_size) { + MO_DBG_ERR("captured size does not fit"); + //return; let it happen for now + } + current_size -= size; + } + + void reset() { + max_size = current_size; + } +}; + +std::map memTags; + +size_t memTotal, memTotalMax; + +void MemBlockInfo::updateTag(void* ptr, const char *tag) { + if (!tag) { + return; + } + if (tagger_ptr == nullptr || ptr < tagger_ptr) { + MO_DBG_VERBOSE("update tag from %s to %s, ptr from %p to %p", this->tag.c_str(), tag, tagger_ptr, ptr); + + auto tagInfo = memTags.find(this->tag); + if (tagInfo != memTags.end()) { + tagInfo->second -= size; + } + + tagInfo = memTags.find(tag); + if (tagInfo != memTags.end()) { + tagInfo->second += size; + } else { + memTags.emplace(tag, size); + } + + tagger_ptr = ptr; + this->tag = tag; + } +} + +} //namespace Memory +} //namespace MicroOcpp + +#endif //MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER + +#if MO_OVERRIDE_ALLOCATION + +namespace MicroOcpp { +namespace Memory { + +void* (*malloc_override)(size_t); +void (*free_override)(void*); + +} +} + +using namespace MicroOcpp::Memory; + +void mo_mem_set_malloc_free(void* (*malloc_override)(size_t), void (*free_override)(void*)) { + MicroOcpp::Memory::malloc_override = malloc_override; + MicroOcpp::Memory::free_override = free_override; +} + +void *mo_mem_malloc(const char *tag, size_t size) { + MO_DBG_VERBOSE("malloc %zu B (%s)", size, tag ? tag : "unspecified"); + + void *ptr; + if (malloc_override) { + ptr = malloc_override(size); + } else { + ptr = malloc(size); + } + + #if MO_ENABLE_HEAP_PROFILER + if (ptr) { + memBlocks.emplace(ptr, MemBlockInfo(ptr, tag, size)); + + memTotal += size; + memTotalMax = std::max(memTotalMax, memTotal); + } + #endif + return ptr; +} + +void mo_mem_free(void* ptr) { + MO_DBG_VERBOSE("free"); + + #if MO_ENABLE_HEAP_PROFILER + if (ptr) { + + auto blockInfo = memBlocks.find(ptr); + if (blockInfo != memBlocks.end()) { + auto tagInfo = memTags.find(blockInfo->second.tag); + if (tagInfo != memTags.end()) { + tagInfo->second -= blockInfo->second.size; + } + memTotal -= blockInfo->second.size; + } + + if (blockInfo != memBlocks.end()) { + memBlocks.erase(blockInfo); + } + } + #endif + + if (free_override) { + free_override(ptr); + } else { + free(ptr); + } +} + +#endif //MO_OVERRIDE_ALLOCATION + +#if MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER + +void mo_mem_deinit() { + memBlocks.clear(); + memTags.clear(); +} + +void mo_mem_reset() { + MO_DBG_DEBUG("Reset all maximum values to current values"); + + for (auto tagInfo = (memTags).begin(); tagInfo != memTags.end(); ++tagInfo) { + tagInfo->second.reset(); + } + + memTotalMax = memTotal; +} + +void mo_mem_set_tag(void *ptr, const char *tag) { + MO_DBG_VERBOSE("set tag (%s)", tag ? tag : "unspecified"); + + if (!tag) { + return; + } + + bool hasTagged = false; + + if (tag) { + auto foundBlock = memBlocks.upper_bound(ptr); + if (foundBlock != memBlocks.begin()) { + --foundBlock; + } + if (foundBlock != memBlocks.end() && + (unsigned char*)ptr - (unsigned char*)foundBlock->first < (std::ptrdiff_t)foundBlock->second.size) { + foundBlock->second.updateTag(ptr, tag); + hasTagged = true; + } + } + + if (!hasTagged) { + MO_DBG_VERBOSE("memory area doesn't apply"); + } +} + +void mo_mem_print_stats() { + + MO_CONSOLE_PRINTF("\n *** Heap usage statistics ***\n"); + + size_t size = 0; + + size_t untagged = 0, untagged_size = 0; + + for (const auto& heapEntry : memBlocks) { + size += heapEntry.second.size; + #if MO_DBG_LEVEL >= MO_DL_VERBOSE + { + MO_CONSOLE_PRINTF("@%p - %zu B (%s)\n", heapEntry.first, heapEntry.second.size, heapEntry.second.tag.c_str()); + } + #endif + + if (heapEntry.second.tag.empty()) { + untagged ++; + untagged_size += heapEntry.second.size; + } + } + + std::map tags; + for (const auto& heapEntry : memBlocks) { + auto foundTag = tags.find(heapEntry.second.tag); + if (foundTag != tags.end()) { + foundTag->second += heapEntry.second.size; + } else { + tags.emplace(heapEntry.second.tag, heapEntry.second.size); + } + } + + size_t size_control = 0; + + for (const auto& tag : tags) { + size_control += tag.second; + #if MO_DBG_LEVEL >= MO_DL_VERBOSE + { + MO_CONSOLE_PRINTF("%s - %zu B\n", tag.first.c_str(), tag.second); + } + #endif + } + + size_t size_control2 = 0; + for (const auto& tag : memTags) { + size_control2 += tag.second.current_size; + MO_CONSOLE_PRINTF("%s - %zu B (max. %zu B)\n", tag.first.c_str(), tag.second.current_size, tag.second.max_size); + } + + MO_CONSOLE_PRINTF(" *** Summary ***\nBlocks: %zu\nTags: %zu\nCurrent usage: %zu B\nMaximum usage: %zu B\n", memBlocks.size(), memTags.size(), memTotal, memTotalMax); + #if MO_DBG_LEVEL >= MO_DL_DEBUG + { + MO_CONSOLE_PRINTF(" *** Debug information ***\nTotal blocks (control value 1): %zu B\nTags (control value): %zu\nTotal tagged (control value 2): %zu B\nTotal tagged (control value 3): %zu B\nUntagged: %zu\nTotal untagged: %zu B\n", size, tags.size(), size_control, size_control2, untagged, untagged_size); + } + #endif +} + +int mo_mem_write_stats_json(char *buf, size_t size) { + DynamicJsonDocument doc {size * 2}; + + doc["total_current"] = memTotal; + doc["total_max"] = memTotalMax; + doc["total_blocks"] = memBlocks.size(); + + JsonArray by_tag = doc.createNestedArray("by_tag"); + for (const auto& tag : memTags) { + JsonObject entry = by_tag.createNestedObject(); + entry["tag"] = tag.first.c_str(); + entry["current"] = tag.second.current_size; + entry["max"] = tag.second.max_size; + } + + size_t untagged = 0, untagged_size = 0; + + for (const auto& heapEntry : memBlocks) { + if (heapEntry.second.tag.empty()) { + untagged ++; + untagged_size += heapEntry.second.size; + } + } + + doc["untagged_blocks"] = untagged; + doc["untagged_size"] = untagged_size; + + if (doc.overflowed()) { + MO_DBG_ERR("exceeded JSON capacity"); + return -1; + } + + return (int)serializeJson(doc, buf, size); +} + +#endif //MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER + +namespace MicroOcpp { + +String makeString(const char *tag, const char *val) { +#if MO_OVERRIDE_ALLOCATION + if (val) { + return String(val, Allocator(tag)); + } else { + return String(Allocator(tag)); + } +#else + if (val) { + return String(val); + } else { + return String(); + } +#endif +} + +JsonDoc initJsonDoc(const char *tag, size_t capacity) { +#if MO_OVERRIDE_ALLOCATION + return JsonDoc(capacity, ArduinoJsonAllocator(tag)); +#else + return JsonDoc(capacity); +#endif +} + +std::unique_ptr makeJsonDoc(const char *tag, size_t capacity) { +#if MO_OVERRIDE_ALLOCATION + return std::unique_ptr(new JsonDoc(capacity, ArduinoJsonAllocator(tag))); +#else + return std::unique_ptr(new JsonDoc(capacity)); +#endif +} + +} diff --git a/src/MicroOcpp/Core/Memory.h b/src/MicroOcpp/Core/Memory.h new file mode 100644 index 00000000..bca25a84 --- /dev/null +++ b/src/MicroOcpp/Core/Memory.h @@ -0,0 +1,444 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_MEMORY_H +#define MO_MEMORY_H + +#include + +#ifndef MO_OVERRIDE_ALLOCATION +#define MO_OVERRIDE_ALLOCATION 0 +#endif + +#ifndef MO_ENABLE_EXTERNAL_RAM +#define MO_ENABLE_EXTERNAL_RAM 0 +#endif + +#ifndef MO_ENABLE_HEAP_PROFILER +#define MO_ENABLE_HEAP_PROFILER 0 +#endif + + +#ifdef __cplusplus +extern "C" { +#endif + +#if MO_OVERRIDE_ALLOCATION + +void mo_mem_set_malloc_free(void* (*malloc_override)(size_t), void (*free_override)(void*)); //pass custom malloc and free function to be used with the OCPP lib. If not set or NULL, defaults to standard malloc + +void *mo_mem_malloc(const char *tag, size_t size); + +void mo_mem_free(void* ptr); + +#define MO_MALLOC mo_mem_malloc +#define MO_FREE mo_mem_free + +#else +#define MO_MALLOC(TAG, SIZE) malloc(SIZE) //default malloc provided by host system +#define MO_FREE(PTR) free(PTR) //default free provided by host system +#endif //MO_OVERRIDE_ALLOCATION + + +#if MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER + +void mo_mem_deinit(); //release allocated memory and deinit +void mo_mem_reset(); //reset maximum heap occuption + +void mo_mem_set_tag(void *ptr, const char *tag); + +void mo_mem_get_current_heap(const char *tag); +void mo_mem_get_maximum_heap(const char *tag); +void mo_mem_get_current_heap_by_tag(const char *tag); +void mo_mem_get_maximum_heap_by_tag(const char *tag); + +int mo_mem_write_stats_json(char *buf, size_t size); + +void mo_mem_print_stats(); + +#define MO_MEM_DEINIT mo_mem_deinit +#define MO_MEM_RESET mo_mem_reset +#define MO_MEM_SET_TAG mo_mem_set_tag +#define MO_MEM_PRINT_STATS mo_mem_print_stats + +#else +#define MO_MEM_DEINIT(...) (void)0 +#define MO_MEM_RESET(...) (void)0 +#define MO_MEM_SET_TAG(...) (void)0 +#define MO_MEM_PRINT_STATS(...) (void)0 +#endif //MO_OVERRIDE_ALLOCATION && MO_ENABLE_HEAP_PROFILER + + +#if MO_ENABLE_EXTERNAL_RAM + +void mo_mem_set_malloc_free_ext(void* (*malloc_override)(size_t), void (*free_override)(void*)); //pass malloc and free function to external RAM to be used with the OCPP lib. If not set or NULL, defaults to standard malloc + +void *mo_mem_malloc_ext(const char *tag, size_t size); + +void mo_mem_free_ext(void* ptr); + +#define MO_MALLOC_EXT(TAG, SIZE) mo_mem_malloc_ext(TAG, SIZE) +#define MO_FREE_EXT(PTR) mo_mem_free_ext(PTR) + +#else +#define MO_MALLOC_EXT MO_MALLOC +#define MO_FREE_EXT MO_FREE +#endif //MO_ENABLE_EXTERNAL_RAM + + +#ifdef __cplusplus +} + + +#include +#include +#include + +#include + +#if MO_OVERRIDE_ALLOCATION + +#include + +namespace MicroOcpp { + +class MemoryManaged { +private: + #if MO_ENABLE_HEAP_PROFILER + char *tag = nullptr; + #endif +protected: + void updateMemoryTag(const char *src1, const char *src2 = nullptr) { + #if MO_ENABLE_HEAP_PROFILER + if (!src1 && !src2) { + //empty source does not update tag + return; + } + char src [64]; + snprintf(src, sizeof(src), "%s%s", src1 ? src1 : "", src2 ? src2 : ""); + if (tag) { + if (!strcmp(src, tag)) { + //nothing to do + return; + } + MO_FREE(tag); + tag = nullptr; + } + size_t size = strlen(src) + 1; + tag = static_cast(malloc(size)); //heap profiler bypasses custom malloc to not count into the statistics + memset(tag, 0, size); + snprintf(tag, size, "%s", src); + mo_mem_set_tag(this, tag); + #else + (void)src1; + (void)src2; + #endif + } + const char *getMemoryTag() const { + #if MO_ENABLE_HEAP_PROFILER + return tag; + #else + return nullptr; + #endif + } +public: + void *operator new(size_t size) { + return MO_MALLOC(nullptr, size); + } + void operator delete(void * ptr) { + MO_FREE(ptr); + } + + MemoryManaged(const char *tag = nullptr, const char *tag_suffix = nullptr) { + #if MO_ENABLE_HEAP_PROFILER + updateMemoryTag(tag, tag_suffix); + #endif + } + + MemoryManaged(MemoryManaged&& other) { + #if MO_ENABLE_HEAP_PROFILER + tag = other.tag; + other.tag = nullptr; + #endif + } + + ~MemoryManaged() { + #if MO_ENABLE_HEAP_PROFILER + MO_FREE(tag); + tag = nullptr; + #endif + } + + void operator=(const MemoryManaged& other) { + #if MO_ENABLE_HEAP_PROFILER + updateMemoryTag(other.tag); + #endif + } +}; + +template +struct Allocator { + + Allocator(const char *tag = nullptr, const char *tag_suffix = nullptr) { + #if MO_ENABLE_HEAP_PROFILER + updateMemoryTag(tag, tag_suffix); + #endif + } + + template + Allocator(const Allocator& other) { + #if MO_ENABLE_HEAP_PROFILER + updateMemoryTag(other.tag); + #endif + } + + Allocator(const Allocator& other) { + #if MO_ENABLE_HEAP_PROFILER + updateMemoryTag(other.tag); + #endif + } + + //template + //Allocator(Allocator&& other) { + Allocator(Allocator&& other) { + #if MO_ENABLE_HEAP_PROFILER + updateMemoryTag(other.tag); //ignore move semantics for allocators as it simplifies moving std::vector>. This is okay because the Allocator's state is only the memory tag which is not exclusively owned + #endif + } + + ~Allocator() { + #if MO_ENABLE_HEAP_PROFILER + if (tag) { + //MO_FREE(tag); + free(tag); + tag = nullptr; + } + #endif + } + + T *allocate(size_t count) { + #if MO_ENABLE_HEAP_PROFILER + return static_cast(MO_MALLOC(tag, sizeof(T) * count)); + #else + return static_cast(MO_MALLOC(nullptr, sizeof(T) * count)); + #endif + } + + void deallocate(T *ptr, size_t count) { + MO_FREE(ptr); + } + + bool operator==(const Allocator& other) { + #if MO_ENABLE_HEAP_PROFILER + if (!tag && !other.tag) { + return true; + } else if (tag && other.tag) { + return !strcmp(tag, other.tag); + } else { + return false; + } + #else + return true; + #endif + } + + bool operator!=(const Allocator& other) { + return !operator==(other); + } + + typedef T value_type; + + #if MO_ENABLE_HEAP_PROFILER + char *tag = nullptr; + + void updateMemoryTag(const char *src1, const char *src2 = nullptr) { + if (!src1 && !src2) { + //empty source does not update tag + return; + } + char src [64]; + snprintf(src, sizeof(src), "%s%s", src1 ? src1 : "", src2 ? src2 : ""); + if (tag) { + if (!strcmp(src, tag)) { + //nothing to do + return; + } + //MO_FREE(tag); + free(tag); + tag = nullptr; + } + size_t size = strlen(src) + 1; + tag = static_cast(malloc(size)); + memset(tag, 0, size); + snprintf(tag, size, "%s", src); + } + #endif +}; + +template +Allocator makeAllocator(const char *tag, const char *tag_suffix = nullptr) { + return Allocator(tag, tag_suffix); +} + +using String = std::basic_string, MicroOcpp::Allocator>; + +template +using Vector = std::vector>; + +template +Vector makeVector(const char *tag) { + return Vector(Allocator(tag)); +} + +class ArduinoJsonAllocator { +private: + #if MO_ENABLE_HEAP_PROFILER + char *tag = nullptr; + + void updateMemoryTag(const char *src1, const char *src2 = nullptr) { + if (!src1 && !src2) { + //empty source does not update tag + return; + } + char src [64]; + snprintf(src, sizeof(src), "%s%s", src1 ? src1 : "", src2 ? src2 : ""); + if (tag) { + if (!strcmp(src, tag)) { + //nothing to do + return; + } + MO_FREE(tag); + tag = nullptr; + } + size_t size = strlen(src) + 1; + //tag = static_cast(MO_MALLOC("HeapProfilerInternal", size)); + tag = static_cast(malloc(size)); + memset(tag, 0, size); + snprintf(tag, size, "%s", src); + } + #endif +public: + + ArduinoJsonAllocator(const char *tag = nullptr, const char *tag_suffix = nullptr) { + #if MO_ENABLE_HEAP_PROFILER + updateMemoryTag(tag, tag_suffix); + #endif + } + + ArduinoJsonAllocator(const ArduinoJsonAllocator& other) { + #if MO_ENABLE_HEAP_PROFILER + updateMemoryTag(other.tag); + #endif + } + + ArduinoJsonAllocator(ArduinoJsonAllocator&& other) { + #if MO_ENABLE_HEAP_PROFILER + tag = other.tag; + other.tag = nullptr; + #endif + } + + ~ArduinoJsonAllocator() { + #if MO_ENABLE_HEAP_PROFILER + if (tag) { + MO_FREE(tag); + tag = nullptr; + } + #endif + } + + void *allocate(size_t size) { + #if MO_ENABLE_HEAP_PROFILER + return MO_MALLOC(tag, size); + #else + return MO_MALLOC(nullptr, size); + #endif + } + void deallocate(void *ptr) { + MO_FREE(ptr); + } +}; + +using JsonDoc = BasicJsonDocument; + +template +T *mo_mem_new(const char *tag, Args&& ...args) { + if (auto ptr = MO_MALLOC(tag, sizeof(T))) { + return new(ptr) T(std::forward(args)...); + } + return nullptr; //OOM +} + +template +void mo_mem_delete(T *ptr) { + if (ptr) { + ptr->~T(); + MO_FREE(ptr); + } +} + +} //namespace MicroOcpp + +#else + +#include + +namespace MicroOcpp { + +class MemoryManaged { +protected: + const char *getMemoryTag() const {return nullptr;} + void updateMemoryTag(const char*,const char*) { } +public: + MemoryManaged() { } + MemoryManaged(const char*) { } + MemoryManaged(const char*,const char*) { } +}; + +template +using Allocator = ::std::allocator; + +template +Allocator makeAllocator(const char *, const char *unused = nullptr) { + (void)unused; + return Allocator(); +} + +using String = std::string; + +template +using Vector = std::vector; + +template +Vector makeVector(const char *tag) { + return Vector(); +} + +using JsonDoc = DynamicJsonDocument; + +template +T *mo_mem_new(Args&& ...args) { + return new T(std::forward(args)...); +} + +template +void mo_mem_delete(T *ptr) { + delete ptr; +} + +} //namespace MicroOcpp + +#endif //MO_OVERRIDE_ALLOCATION + +namespace MicroOcpp { + +String makeString(const char *tag, const char *val = nullptr); + +JsonDoc initJsonDoc(const char *tag, size_t capacity = 0); +std::unique_ptr makeJsonDoc(const char *tag, size_t capacity = 0); + +} + +#endif //__cplusplus +#endif diff --git a/src/MicroOcpp/Core/OcppError.h b/src/MicroOcpp/Core/OcppError.h index a2d24cf3..826c3215 100644 --- a/src/MicroOcpp/Core/OcppError.h +++ b/src/MicroOcpp/Core/OcppError.h @@ -1,37 +1,38 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef OCPPERROR_H -#define OCPPERROR_H - -#include +#ifndef MO_OCPPERROR_H +#define MO_OCPPERROR_H #include +#include namespace MicroOcpp { -class NotImplemented : public Operation { +class NotImplemented : public Operation, public MemoryManaged { public: + NotImplemented() : MemoryManaged("v16.CallError.", "NotImplemented") { } + const char *getErrorCode() override { return "NotImplemented"; } }; -class MsgBufferExceeded : public Operation { +class MsgBufferExceeded : public Operation, public MemoryManaged { private: size_t maxCapacity; size_t msgLen; public: - MsgBufferExceeded(size_t maxCapacity, size_t msgLen) : maxCapacity(maxCapacity), msgLen(msgLen) { } + MsgBufferExceeded(size_t maxCapacity, size_t msgLen) : MemoryManaged("v16.CallError.", "GenericError"), maxCapacity(maxCapacity), msgLen(msgLen) { } const char *getErrorCode() override { return "GenericError"; } const char *getErrorDescription() override { return "JSON too long or too many fields. Cannot deserialize"; } - std::unique_ptr getErrorDetails() override { - auto errDoc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); + std::unique_ptr getErrorDetails() override { + auto errDoc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2)); JsonObject err = errDoc->to(); err["max_capacity"] = maxCapacity; err["msg_length"] = msgLen; diff --git a/src/MicroOcpp/Core/Operation.cpp b/src/MicroOcpp/Core/Operation.cpp index 9dc887fa..1289381b 100644 --- a/src/MicroOcpp/Core/Operation.cpp +++ b/src/MicroOcpp/Core/Operation.cpp @@ -1,12 +1,12 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include -using MicroOcpp::Operation; +using namespace MicroOcpp; Operation::Operation() {} @@ -17,15 +17,7 @@ const char* Operation::getOperationType(){ return "CustomOperation"; } -void Operation::initiate(StoredOperationHandler *rpcData) { - //called after initiateRequest(anyMsg) -} - -bool Operation::restore(StoredOperationHandler *rpcData) { - return false; -} - -std::unique_ptr Operation::createReq() { +std::unique_ptr Operation::createReq() { MO_DBG_ERR("Unsupported operation: createReq() is not implemented"); return createEmptyDocument(); } @@ -38,13 +30,13 @@ void Operation::processReq(JsonObject payload) { MO_DBG_ERR("Unsupported operation: processReq() is not implemented"); } -std::unique_ptr Operation::createConf() { +std::unique_ptr Operation::createConf() { MO_DBG_ERR("Unsupported operation: createConf() is not implemented"); return createEmptyDocument(); } -std::unique_ptr MicroOcpp::createEmptyDocument() { - auto emptyDoc = std::unique_ptr(new DynamicJsonDocument(0)); +std::unique_ptr MicroOcpp::createEmptyDocument() { + auto emptyDoc = makeJsonDoc("EmptyJsonDoc", 0); emptyDoc->to(); return emptyDoc; } diff --git a/src/MicroOcpp/Core/Operation.h b/src/MicroOcpp/Core/Operation.h index 0018a453..9c1756a1 100644 --- a/src/MicroOcpp/Core/Operation.h +++ b/src/MicroOcpp/Core/Operation.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License /** @@ -18,14 +18,13 @@ #ifndef MO_OPERATION_H #define MO_OPERATION_H -#include #include - -#include +#include +#include namespace MicroOcpp { -std::unique_ptr createEmptyDocument(); +std::unique_ptr createEmptyDocument(); class Operation { public: @@ -35,10 +34,6 @@ class Operation { virtual const char* getOperationType(); - virtual void initiate(StoredOperationHandler *rpcData); - - virtual bool restore(StoredOperationHandler *rpcData); - /** * Create the payload for the respective OCPP message * @@ -47,7 +42,7 @@ class Operation { * This function is usually called multiple times by the Arduino loop(). On first call, the request is initially sent. In the * succeeding calls, the implementers decide to either recreate the request, or do nothing as the operation is still pending. */ - virtual std::unique_ptr createReq(); + virtual std::unique_ptr createReq(); virtual void processConf(JsonObject payload); @@ -66,11 +61,11 @@ class Operation { * After successfully processing a request sent by the communication counterpart, this function creates the payload for a confirmation * message. */ - virtual std::unique_ptr createConf(); + virtual std::unique_ptr createConf(); virtual const char *getErrorCode() {return nullptr;} //nullptr means no error virtual const char *getErrorDescription() {return "";} - virtual std::unique_ptr getErrorDetails() {return createEmptyDocument();} + virtual std::unique_ptr getErrorDetails() {return createEmptyDocument();} }; } //end namespace MicroOcpp diff --git a/src/MicroOcpp/Core/OperationRegistry.cpp b/src/MicroOcpp/Core/OperationRegistry.cpp index 2960c24d..0bd17d45 100644 --- a/src/MicroOcpp/Core/OperationRegistry.cpp +++ b/src/MicroOcpp/Core/OperationRegistry.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -11,7 +11,7 @@ using namespace MicroOcpp; -OperationRegistry::OperationRegistry() { +OperationRegistry::OperationRegistry() : registry(makeVector("OperationRegistry")) { } diff --git a/src/MicroOcpp/Core/OperationRegistry.h b/src/MicroOcpp/Core/OperationRegistry.h index 23bba2e4..0d0d44b5 100644 --- a/src/MicroOcpp/Core/OperationRegistry.h +++ b/src/MicroOcpp/Core/OperationRegistry.h @@ -1,14 +1,13 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_OPERATIONREGISTRY_H #define MO_OPERATIONREGISTRY_H #include -#include #include -#include +#include #include namespace MicroOcpp { @@ -25,7 +24,7 @@ struct OperationCreator { class OperationRegistry { private: - std::vector registry; + Vector registry; OperationCreator *findCreator(const char *operationType); public: diff --git a/src/MicroOcpp/Core/PollResult.h b/src/MicroOcpp/Core/PollResult.h deleted file mode 100644 index 802f305c..00000000 --- a/src/MicroOcpp/Core/PollResult.h +++ /dev/null @@ -1,51 +0,0 @@ -// matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 -// MIT License - -#ifndef POLLRESULT_H -#define POLLRESULT_H - -#include -#include - -namespace MicroOcpp { - -template -class PollResult { -private: - bool ready; - T value; -public: - PollResult() : ready(false) {} - PollResult(T&& value) : ready(true), value(value) {} - PollResult(const PollResult&) = delete; - PollResult& operator =(const PollResult&) = delete; - PollResult& operator =(const PollResult&& other) { - ready = other.ready; - value = std::move(other.value); - return *this; - } - PollResult(PollResult&& other) : ready(other.ready), value(std::move(other.value)) {} - T&& toValue() { - if (!ready) { - MO_DBG_ERR("Not ready"); - (void)0; - } - ready = false; - return std::move(value); - } - T& getValue() const { - if (!ready) { - MO_DBG_ERR("Not ready"); - (void)0; - } - return *value; - } - operator bool() const {return ready;} - - static PollResult Await() {return PollResult();} -}; - -} - -#endif diff --git a/src/MicroOcpp/Core/Request.cpp b/src/MicroOcpp/Core/Request.cpp index c3dd1821..af6e2885 100644 --- a/src/MicroOcpp/Core/Request.cpp +++ b/src/MicroOcpp/Core/Request.cpp @@ -1,12 +1,12 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include -#include +#include #include #include @@ -14,24 +14,33 @@ #include #include -int unique_id_counter = 1000000; - -using namespace MicroOcpp; - -Request::Request(std::unique_ptr msg) : operation(std::move(msg)) { +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; + } + } } -Request::Request() { +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(){ } -void Request::setOperation(std::unique_ptr msg){ - operation = std::move(msg); +Operation *Request::getOperation(){ + return operation.get(); } void Request::setTimeout(unsigned long timeout) { @@ -50,41 +59,39 @@ void Request::executeTimeout() { timed_out = true; } -unsigned int Request::getTrialNo() { - return trialNo; -} - -void Request::setMessageID(const std::string &id){ +void Request::setMessageID(const char *id){ if (!messageID.empty()){ - MO_DBG_WARN("MessageID is set twice or is set after first usage!"); + MO_DBG_ERR("messageID already defined"); } messageID = id; } -const char *Request::getMessageID() { - return messageID.c_str(); -} +Request::CreateRequestResult Request::createRequest(JsonDoc& requestJson) { -std::unique_ptr Request::createRequest(){ + if (messageID.empty()) { + char uuid [37] = {'\0'}; + generateUUID(uuid, 37); + messageID = uuid; + } /* * Create the OCPP message */ auto requestPayload = operation->createReq(); if (!requestPayload) { - return nullptr; + return CreateRequestResult::Failure; } /* * Create OCPP-J Remote Procedure Call header */ size_t json_buffsize = JSON_ARRAY_SIZE(4) + (messageID.length() + 1) + requestPayload->capacity(); - auto requestJson = std::unique_ptr(new DynamicJsonDocument(json_buffsize)); + requestJson = initJsonDoc(getMemoryTag(), json_buffsize); - requestJson->add(MESSAGE_TYPE_CALL); //MessageType - requestJson->add(messageID); //Unique message ID - requestJson->add(operation->getOperationType()); //Action - requestJson->add(*requestPayload); //Payload + requestJson.add(MESSAGE_TYPE_CALL); //MessageType + requestJson.add(messageID); //Unique message ID + requestJson.add(operation->getOperationType()); //Action + requestJson.add(*requestPayload); //Payload if (MO_DBG_LEVEL >= MO_DL_DEBUG && mocpp_tick_ms() - debugRequest_start >= 10000) { //print contents on the console debugRequest_start = mocpp_tick_ms(); @@ -92,7 +99,7 @@ std::unique_ptr Request::createRequest(){ char *buf = new char[1024]; size_t len = 0; if (buf) { - len = serializeJson(*requestJson, buf, 1024); + len = serializeJson(requestJson, buf, 1024); } if (!buf || len < 1) { @@ -104,16 +111,14 @@ std::unique_ptr Request::createRequest(){ delete[] buf; } - trialNo++; - - return requestJson; + 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 != response[1].as()){ + if (messageID.compare(response[1].as())){ return false; } @@ -159,10 +164,14 @@ bool Request::receiveResponse(JsonArray response){ } -bool Request::receiveRequest(JsonArray request){ +bool Request::receiveRequest(JsonArray request) { + + if (!request[1].is()) { + MO_DBG_ERR("malformatted msgId"); + return false; + } - std::string msgId = request[1]; - setMessageID(msgId); + setMessageID(request[1].as()); /* * Hand the payload over to the Request object @@ -178,32 +187,27 @@ bool Request::receiveRequest(JsonArray request){ return true; //success } -std::unique_ptr Request::createResponse(){ +Request::CreateResponseResult Request::createResponse(JsonDoc& response) { - /* - * Create the OCPP message - */ - std::unique_ptr response = nullptr; - std::unique_ptr payload = operation->createConf(); - std::unique_ptr errorDetails = nullptr; - bool operationFailure = operation->getErrorCode() != nullptr; - if (!operationFailure && !payload) { - return nullptr; //confirmation message still pending - } - if (!operationFailure) { + std::unique_ptr payload = operation->createConf(); + + if (!payload) { + return CreateResponseResult::Pending; //confirmation message still pending + } + /* * Create OCPP-J Remote Procedure Call header */ - size_t json_buffsize = JSON_ARRAY_SIZE(3) + (messageID.length() + 1) + payload->capacity(); - response = std::unique_ptr(new DynamicJsonDocument(json_buffsize)); + size_t json_buffsize = JSON_ARRAY_SIZE(3) + payload->capacity(); + response = initJsonDoc(getMemoryTag(), json_buffsize); - response->add(MESSAGE_TYPE_CALLRESULT); //MessageType - response->add(messageID); //Unique message ID - response->add(*payload); //Payload + response.add(MESSAGE_TYPE_CALLRESULT); //MessageType + response.add(messageID.c_str()); //Unique message ID + response.add(*payload); //Payload if (onSendConfListener) { onSendConfListener(payload->as()); @@ -213,130 +217,23 @@ std::unique_ptr Request::createResponse(){ const char *errorCode = operation->getErrorCode(); const char *errorDescription = operation->getErrorDescription(); - errorDetails = std::unique_ptr(operation->getErrorDetails()); - if (!errorCode) { //catch corner case when payload is null but errorCode is not set too! - errorCode = "GenericError"; - errorDescription = "Could not create payload (createConf() returns Null)"; - errorDetails = std::unique_ptr(createEmptyDocument()); - } + std::unique_ptr errorDetails = operation->getErrorDetails(); /* * Create OCPP-J Remote Procedure Call header */ size_t json_buffsize = JSON_ARRAY_SIZE(5) - + (messageID.length() + 1) - + strlen(errorCode) + 1 - + strlen(errorDescription) + 1 + errorDetails->capacity(); - response = std::unique_ptr(new DynamicJsonDocument(json_buffsize)); + response = initJsonDoc(getMemoryTag(), json_buffsize); - response->add(MESSAGE_TYPE_CALLERROR); //MessageType - response->add(messageID); //Unique message ID - response->add(errorCode); - response->add(errorDescription); - response->add(*errorDetails); //Error description + response.add(MESSAGE_TYPE_CALLERROR); //MessageType + response.add(messageID.c_str()); //Unique message ID + response.add(errorCode); + response.add(errorDescription); + response.add(*errorDetails); //Error description } - return response; -} - -void Request::initiate(std::unique_ptr opStorage) { - - timeout_start = mocpp_tick_ms(); - debugRequest_start = mocpp_tick_ms(); - - //assign messageID - char id_str [16] = {'\0'}; - sprintf(id_str, "%d", unique_id_counter++); - messageID = std::string {id_str}; - - if (operation) { - - /* - * Create OCPP-J Remote Procedure Call header storage entry - */ - opStore = std::move(opStorage); - - if (opStore) { - size_t json_buffsize = JSON_ARRAY_SIZE(3) + (messageID.length() + 1); - auto rpcData = std::unique_ptr(new DynamicJsonDocument(json_buffsize)); - - rpcData->add(MESSAGE_TYPE_CALL); //MessageType - rpcData->add(messageID); //Unique message ID - rpcData->add(operation->getOperationType()); //Action - - opStore->setRpcData(std::move(rpcData)); - } - - operation->initiate(opStore.get()); - - if (opStore) { - opStore->clearBuffer(); - } - } else { - MO_DBG_ERR("Missing operation instance"); - } -} - -bool Request::restore(std::unique_ptr opStorage, Model *model) { - if (!opStorage) { - MO_DBG_ERR("invalid argument"); - return false; - } - - opStore = std::move(opStorage); - - auto rpcData = opStore->getRpcData(); - if (!rpcData) { - MO_DBG_ERR("corrupted storage"); - return false; - } - - messageID = (*rpcData)[1] | std::string(); - std::string opType = (*rpcData)[2] | std::string(); - if (messageID.empty() || opType.empty()) { - MO_DBG_ERR("corrupted storage"); - messageID.clear(); - return false; - } - - int parsedMessageID = -1; - if (sscanf(messageID.c_str(), "%d", &parsedMessageID) == 1) { - if (parsedMessageID > unique_id_counter) { - MO_DBG_DEBUG("restore unique_id_counter with %d", parsedMessageID); - unique_id_counter = parsedMessageID + 1; //next unique value is parsedId + 1 - } - } else { - MO_DBG_ERR("cannot set unique msgID counter"); - (void)0; - //skip this step but don't abort restore - } - - timeout_period = 0; //disable timeout by default for restored msgs - - if (!strcmp(opType.c_str(), "StartTransaction") && model) { //TODO this will get a nicer solution - operation = std::unique_ptr(new Ocpp16::StartTransaction(*model, nullptr)); - } else if (!strcmp(opType.c_str(), "StopTransaction") && model) { - operation = std::unique_ptr(new Ocpp16::StopTransaction(*model, nullptr)); - } - - if (!operation) { - MO_DBG_ERR("cannot create msg"); - return false; - } - - bool success = operation->restore(opStore.get()); - opStore->clearBuffer(); - - if (success) { - MO_DBG_DEBUG("restored opNr %i: %s", opStore->getOpNr(), operation->getOperationType()); - (void)0; - } else { - MO_DBG_ERR("restore opNr %i error", opStore->getOpNr()); - (void)0; - } - - return success; + return CreateResponseResult::Success; } void Request::setOnReceiveConfListener(OnReceiveConfListener onReceiveConf){ @@ -375,3 +272,26 @@ void Request::setOnAbortListener(OnAbortListener onAbort) { const char *Request::getOperationType() { return operation ? operation->getOperationType() : "UNDEFINED"; } + +void Request::setRequestSent() { + requestSent = true; +} + +bool Request::isRequestSent() { + return requestSent; +} + +namespace MicroOcpp { + +std::unique_ptr makeRequest(std::unique_ptr operation){ + if (operation == nullptr) { + return nullptr; + } + return std::unique_ptr(new Request(std::move(operation))); +} + +std::unique_ptr makeRequest(Operation *operation) { + return makeRequest(std::unique_ptr(operation)); +} + +} //end namespace MicroOcpp diff --git a/src/MicroOcpp/Core/Request.h b/src/MicroOcpp/Core/Request.h index 07a56627..62e42967 100644 --- a/src/MicroOcpp/Core/Request.h +++ b/src/MicroOcpp/Core/Request.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_REQUEST_H @@ -13,17 +13,18 @@ #include +#include + namespace MicroOcpp { class Operation; class Model; -class StoredOperationHandler; -class Request { +class Request : public MemoryManaged { private: - std::string messageID {}; + String messageID; std::unique_ptr operation; - void setMessageID(const std::string &id); + void setMessageID(const char *id); OnReceiveConfListener onReceiveConfListener = [] (JsonObject payload) {}; OnReceiveReqListener onReceiveReqListener = [] (JsonObject payload) {}; OnSendConfListener onSendConfListener = [] (JsonObject payload) {}; @@ -34,27 +35,22 @@ class Request { unsigned long timeout_start = 0; unsigned long timeout_period = 40000; bool timed_out = false; - - unsigned int trialNo = 0; unsigned long debugRequest_start = 0; - std::unique_ptr opStore; + bool requestSent = false; public: Request(std::unique_ptr msg); - Request(); - ~Request(); - void setOperation(std::unique_ptr msg); + Operation *getOperation(); void setTimeout(unsigned long timeout); //0 = disable timeout bool isTimeoutExceeded(); - void executeTimeout(); //call Timeout handler - - unsigned int getTrialNo(); //how many times createRequest() has been tried (used for retry behavior) + void executeTimeout(); //call Timeout Listener + void setOnTimeoutListener(OnTimeoutListener onTimeout); /** * Sends the message(s) that belong to the OCPP Operation. This function puts a JSON message on the lower protocol layer. @@ -64,7 +60,11 @@ class Request { * 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. */ - std::unique_ptr createRequest(); + enum class CreateRequestResult { + Success, + Failure + }; + CreateRequestResult createRequest(JsonDoc& out); /** * Decides if message belongs to this operation instance and if yes, proccesses it. Receives both Confirmations and Errors @@ -85,24 +85,17 @@ class Request { * message. Returns true on success, false otherwise. Returns also true if a CallError has successfully * been sent */ - std::unique_ptr createResponse(); - - void initiate(std::unique_ptr opStorage); + enum class CreateResponseResult { + Success, + Pending, + Failure + }; - bool restore(std::unique_ptr opStorage, Model *model); + CreateResponseResult createResponse(JsonDoc& out); - StoredOperationHandler *getStorageHandler() {return opStore.get();} - - void setOnReceiveConfListener(OnReceiveConfListener onReceiveConf); - - /** - * Sets a Listener that is called after this machine processed a request by the communication counterpart - */ - void setOnReceiveReqListener(OnReceiveReqListener onReceiveReq); - - void setOnSendConfListener(OnSendConfListener onSendConf); - - void setOnTimeoutListener(OnTimeoutListener onTimeout); + void setOnReceiveConfListener(OnReceiveConfListener onReceiveConf); //listener executed when we received the .conf() to a .req() we sent + void setOnReceiveReqListener(OnReceiveReqListener onReceiveReq); //listener executed when we receive a .req() + void setOnSendConfListener(OnSendConfListener onSendConf); //listener executed when we send a .conf() to a .req() we received void setOnReceiveErrorListener(OnReceiveErrorListener onReceiveError); @@ -119,10 +112,18 @@ class Request { */ void setOnAbortListener(OnAbortListener onAbort); - const char *getMessageID(); const char *getOperationType(); + + void setRequestSent(); + bool isRequestSent(); }; +/* + * Simple factory functions + */ +std::unique_ptr makeRequest(std::unique_ptr op); +std::unique_ptr makeRequest(Operation *op); //takes ownership of op + } //end namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Core/RequestCallbacks.h b/src/MicroOcpp/Core/RequestCallbacks.h index 33209a8d..4959a3bc 100644 --- a/src/MicroOcpp/Core/RequestCallbacks.h +++ b/src/MicroOcpp/Core/RequestCallbacks.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_REQUESTCALLBACKS_H diff --git a/src/MicroOcpp/Core/RequestQueue.cpp b/src/MicroOcpp/Core/RequestQueue.cpp index b247d0de..104a2e97 100644 --- a/src/MicroOcpp/Core/RequestQueue.cpp +++ b/src/MicroOcpp/Core/RequestQueue.cpp @@ -1,14 +1,15 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + #include #include #include -#include #include -#include #include +#include #include @@ -16,121 +17,247 @@ size_t removePayload(const char *src, size_t src_size, char *dst, size_t dst_siz using namespace MicroOcpp; -RequestQueue::RequestQueue(OperationRegistry& operationRegistry, Model *baseModel, std::shared_ptr filesystem) - : operationRegistry(operationRegistry) { - - if (filesystem) { - initiatedRequests.reset(new PersistentRequestQueue(baseModel, filesystem)); - } else { - initiatedRequests.reset(new VolatileRequestQueue()); +VolatileRequestQueue::VolatileRequestQueue() : MemoryManaged("VolatileRequestQueue") { + +} + +VolatileRequestQueue::~VolatileRequestQueue() = default; + +void VolatileRequestQueue::loop() { + + /* + * Drop timed out operations + */ + size_t i = 0; + while (i < len) { + size_t index = (front + i) % MO_REQUEST_CACHE_MAXSIZE; + auto& request = requests[index]; + + if (request->isTimeoutExceeded()) { + MO_DBG_INFO("operation timeout: %s", request->getOperationType()); + request->executeTimeout(); + + if (index == front) { + requests[front].reset(); + front = (front + 1) % MO_REQUEST_CACHE_MAXSIZE; + len--; + } else { + requests[index].reset(); + for (size_t i = (index + MO_REQUEST_CACHE_MAXSIZE - front) % MO_REQUEST_CACHE_MAXSIZE; i < len - 1; i++) { + requests[(front + i) % MO_REQUEST_CACHE_MAXSIZE] = std::move(requests[(front + i + 1) % MO_REQUEST_CACHE_MAXSIZE]); + } + len--; + } + } else { + i++; + } } } -void RequestQueue::setConnection(Connection& sock) { +unsigned int VolatileRequestQueue::getFrontRequestOpNr() { + if (len == 0) { + return NoOperation; + } + + return 1; //return OpNr 1 to grant PreBoot queue higher priority (=0), but send messages before tx-msg queue (starting with 10) +} + +std::unique_ptr VolatileRequestQueue::fetchFrontRequest() { + if (len == 0) { + return nullptr; + } + + std::unique_ptr result = std::move(requests[front]); + front = (front + 1) % MO_REQUEST_CACHE_MAXSIZE; + len--; + + MO_DBG_VERBOSE("front %zu len %zu", front, len); + + return result; +} + +bool VolatileRequestQueue::pushRequestBack(std::unique_ptr request) { + + // Don't queue up multiple StatusNotification messages for the same connectorId + #if 0 // Leads to ASAN failure when executed by Unit test suite (CustomOperation is casted to StatusNotification) + if (strcmp(request->getOperationType(), "StatusNotification") == 0) + { + size_t i = 0; + while (i < len) { + size_t index = (front + i) % MO_REQUEST_CACHE_MAXSIZE; + + if (strcmp(requests[index]->getOperationType(), "StatusNotification")!= 0) + { + i++; + continue; + } + auto new_status_notification = static_cast(request->getOperation()); + auto old_status_notification = static_cast(requests[index]->getOperation()); + if (old_status_notification->getConnectorId() == new_status_notification->getConnectorId()) { + requests[index].reset(); + for (size_t i = (index + MO_REQUEST_CACHE_MAXSIZE - front) % MO_REQUEST_CACHE_MAXSIZE; i < len - 1; i++) { + requests[(front + i) % MO_REQUEST_CACHE_MAXSIZE] = std::move(requests[(front + i + 1) % MO_REQUEST_CACHE_MAXSIZE]); + } + len--; + } else { + i++; + } + } + } + #endif + + if (len >= MO_REQUEST_CACHE_MAXSIZE) { + MO_DBG_INFO("Drop cached operation (cache full): %s", requests[front]->getOperationType()); + requests[front]->executeTimeout(); + requests[front].reset(); + front = (front + 1) % MO_REQUEST_CACHE_MAXSIZE; + len--; + } + + requests[(front + len) % MO_REQUEST_CACHE_MAXSIZE] = std::move(request); + len++; + return true; +} + +RequestQueue::RequestQueue(Connection& connection, OperationRegistry& operationRegistry) + : MemoryManaged("RequestQueue"), connection(connection), operationRegistry(operationRegistry) { + ReceiveTXTcallback callback = [this] (const char *payload, size_t length) { return this->receiveMessage(payload, length); }; - sock.setReceiveTXTcallback(callback); + connection.setReceiveTXTcallback(callback); + + memset(sendQueues, 0, sizeof(sendQueues)); + addSendQueue(&defaultSendQueue); } -void RequestQueue::loop(Connection& ocppSock) { +void RequestQueue::loop() { /* - * Sort out timed out operations + * Check if front request timed out */ - initiatedRequests->drop_if([] (std::unique_ptr& op) -> bool { - bool timed_out = op->isTimeoutExceeded(); - if (timed_out) { - MO_DBG_INFO("operation timeout: %s", op->getOperationType()); - op->executeTimeout(); - } - return 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 the first operation is awaiting, - * try with the subsequent operations. + * Send and dequeue a pending confirmation message, if existing * * If a message has been sent, terminate this loop() function. */ - for (auto received = receivedRequests.begin(); received != receivedRequests.end(); ++received) { - - auto response = (*received)->createResponse(); - if (response) { - std::string out; - serializeJson(*response, out); + 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 = ocppSock.sendTXT(out.c_str(), out.length()); + bool success = connection.sendTXT(out.c_str(), out.length()); if (success) { MO_DBG_TRAFFIC_OUT(out.c_str()); - receivedRequests.erase(received); + recvReqFront.reset(); } return; - } //else: There will be another attempt to send this conf message in a future loop call. - // Go on with the next element in the queue. + } //else: There will be another attempt to send this conf message in a future loop call } /** * Send pending req message */ - auto initedOp = initiatedRequests->front(); - - if (!initedOp) { - //queue empty - return; - } - //check backoff time + if (!sendReqFront) { - if (initedOp->getTrialNo() == 0) { - //first trial -> send immediately - sendBackoffPeriod = 0; - } + 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 (sockTrackLastConnected != ocppSock.getLastConnected()) { - //connection active (again) -> send immediately - sendBackoffPeriod = std::min(sendBackoffPeriod, 1000UL); + if (index < MO_NUM_REQUEST_QUEUES) { + sendReqFront = sendQueues[index]->fetchFrontRequest(); + } } - sockTrackLastConnected = ocppSock.getLastConnected(); - if (mocpp_tick_ms() - sendBackoffTime < sendBackoffPeriod) { - //still in backoff period - return; - } + if (sendReqFront && !sendReqFront->isRequestSent()) { - auto request = initedOp->createRequest(); + auto request = initJsonDoc(getMemoryTag()); + auto ret = sendReqFront->createRequest(request); - if (!request) { - //request not ready yet or OOM - return; - } + if (ret == Request::CreateRequestResult::Success) { - //send request - std::string out; - serializeJson(*request, out); + //send request + auto out = makeString(getMemoryTag()); + serializeJson(request, out); - bool success = ocppSock.sendTXT(out.c_str(), out.length()); + bool success = connection.sendTXT(out.c_str(), out.length()); - if (success) { - MO_DBG_TRAFFIC_OUT(out.c_str()); + if (success) { + MO_DBG_TRAFFIC_OUT(out.c_str()); + sendReqFront->setRequestSent(); //mask as sent and wait for response / timeout + } - //update backoff time - sendBackoffTime = mocpp_tick_ms(); - sendBackoffPeriod = std::min(sendBackoffPeriod + BACKOFF_PERIOD_INCREMENT, BACKOFF_PERIOD_MAX); + return; + } } } void RequestQueue::sendRequest(std::unique_ptr op){ - if (!op) { - MO_DBG_ERR("Called with null. Ignore"); + defaultSendQueue.pushRequestBack(std::move(op)); +} + +void RequestQueue::sendRequestPreBoot(std::unique_ptr op){ + if (!preBootSendQueue) { + MO_DBG_ERR("did not set PreBoot queue"); return; } - - initiatedRequests->push_back(std::move(op)); + 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) { @@ -149,12 +276,12 @@ bool RequestQueue::receiveMessage(const char* payload, size_t length) { capacity = MO_MAX_JSON_CAPACITY; } - DynamicJsonDocument doc {0}; + auto doc = initJsonDoc(getMemoryTag()); DeserializationError err = DeserializationError::NoMemory; while (err == DeserializationError::NoMemory && capacity <= MO_MAX_JSON_CAPACITY) { - doc = DynamicJsonDocument(capacity); + doc = initJsonDoc(getMemoryTag(), capacity); err = deserializeJson(doc, payload, length); capacity *= 2; @@ -190,7 +317,7 @@ bool RequestQueue::receiveMessage(const char* payload, size_t length) { * If the input type is MESSAGE_TYPE_CALLRESULT, then abort the operation to avoid getting stalled. */ - doc = DynamicJsonDocument(200); + doc = initJsonDoc(getMemoryTag(), 200); char onlyRpcHeader[200]; size_t onlyRpcHeader_len = removePayload(payload, length, onlyRpcHeader, sizeof(onlyRpcHeader)); DeserializationError err2 = deserializeJson(doc, onlyRpcHeader, onlyRpcHeader_len); @@ -226,28 +353,11 @@ bool RequestQueue::receiveMessage(const char* payload, size_t length) { */ void RequestQueue::receiveResponse(JsonArray json) { - bool success = false; - - initiatedRequests->drop_if( - [&json, &success] (std::unique_ptr& operation) { - bool match = operation->receiveResponse(json); - if (match) { - success = true; - //operation will be deleted by the surrounding drop_if - } - return match; - }); //executes in order and drops every operation where predicate(op) == true - - if (!success) { - //didn't find matching Request - if (json[0] == MESSAGE_TYPE_CALLERROR) { - MO_DBG_DEBUG("Received CALLERROR did not abort a pending operation"); - (void)0; - } else { - MO_DBG_WARN("Received response doesn't match any pending operation"); - (void)0; - } + if (!sendReqFront || !sendReqFront->receiveResponse(json)) { + MO_DBG_WARN("Received response doesn't match pending operation"); } + + sendReqFront.reset(); } void RequestQueue::receiveRequest(JsonArray json) { @@ -261,7 +371,7 @@ void RequestQueue::receiveRequest(JsonArray json) { void RequestQueue::receiveRequest(JsonArray json, std::unique_ptr op) { op->receiveRequest(json); //execute the operation - receivedRequests.push_back(std::move(op)); //enqueue so loop() plans conf sending + recvQueue.pushRequestBack(std::move(op)); //enqueue so loop() plans conf sending } /* diff --git a/src/MicroOcpp/Core/RequestQueue.h b/src/MicroOcpp/Core/RequestQueue.h index 5ce7e03d..0cdfe7bc 100644 --- a/src/MicroOcpp/Core/RequestQueue.h +++ b/src/MicroOcpp/Core/RequestQueue.h @@ -1,50 +1,92 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_REQUESTQUEUE_H #define MO_REQUESTQUEUE_H -#include +#include + +#include +#include -#include #include #include +#ifndef MO_REQUEST_CACHE_MAXSIZE +#define MO_REQUEST_CACHE_MAXSIZE 10 +#endif + +#ifndef MO_NUM_REQUEST_QUEUES +#define MO_NUM_REQUEST_QUEUES 10 +#endif + namespace MicroOcpp { -class OperationRegistry; -class Model; class Connection; +class OperationRegistry; class Request; -class FilesystemAdapter; -class RequestQueue { +class RequestEmitter { +public: + static const unsigned int NoOperation = std::numeric_limits::max(); + + virtual unsigned int getFrontRequestOpNr() = 0; //return OpNr of front request or NoOperation if queue is empty + virtual std::unique_ptr fetchFrontRequest() = 0; +}; + +class VolatileRequestQueue : public RequestEmitter, public MemoryManaged { +private: + std::unique_ptr requests [MO_REQUEST_CACHE_MAXSIZE]; + size_t front = 0, len = 0; +public: + VolatileRequestQueue(); + ~VolatileRequestQueue(); + void loop(); + + unsigned int getFrontRequestOpNr() override; + std::unique_ptr fetchFrontRequest() override; + + bool pushRequestBack(std::unique_ptr request); +}; + +class RequestQueue : public MemoryManaged { private: + Connection& connection; OperationRegistry& operationRegistry; - - std::unique_ptr initiatedRequests; - std::deque> receivedRequests; + 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 sendBackoffTime = 0; - unsigned long sendBackoffPeriod = 0; unsigned long sockTrackLastConnected = 0; - const unsigned long BACKOFF_PERIOD_MAX = 1048576; - const unsigned long BACKOFF_PERIOD_INCREMENT = BACKOFF_PERIOD_MAX / 4; + + unsigned int nextOpNr = 10; //Nr 0 - 9 reservered for internal purposes public: - RequestQueue(OperationRegistry& operationRegistry, Model *baseModel, std::shared_ptr filesystem = nullptr); + RequestQueue() = delete; + RequestQueue(const RequestQueue&) = delete; + RequestQueue(const RequestQueue&&) = delete; - void setConnection(Connection& sock); + RequestQueue(Connection& connection, OperationRegistry& operationRegistry); - void loop(Connection& ocppSock); + void loop(); //polls all reqQueues and decides which request to send (if any) - void sendRequest(std::unique_ptr o); //send an OCPP operation request to the server - - bool receiveMessage(const char* payload, size_t length); //receive from server: either a request or response + 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 diff --git a/src/MicroOcpp/Core/RequestQueueStorageStrategy.cpp b/src/MicroOcpp/Core/RequestQueueStorageStrategy.cpp deleted file mode 100644 index caa7e0e0..00000000 --- a/src/MicroOcpp/Core/RequestQueueStorageStrategy.cpp +++ /dev/null @@ -1,178 +0,0 @@ -// matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 -// MIT License - -#include -#include -#include - -#include -#include -#include - -#include - -#define MO_OPERATIONCACHE_MAXSIZE 10 - -using namespace MicroOcpp; - -VolatileRequestQueue::VolatileRequestQueue() { - -} - -VolatileRequestQueue::~VolatileRequestQueue() { - -} - -Request *VolatileRequestQueue::front() { - if (!queue.empty()) { - return queue.front().get(); - } else { - return nullptr; - } -} - -void VolatileRequestQueue::pop_front() { - queue.pop_front(); -} - -void VolatileRequestQueue::push_back(std::unique_ptr op) { - - op->initiate(nullptr); - - if (queue.size() >= MO_OPERATIONCACHE_MAXSIZE) { - MO_DBG_WARN("unsafe number of cached operations"); - (void)0; - } - - queue.push_back(std::move(op)); -} - -void VolatileRequestQueue::drop_if(std::function&)> pred) { - queue.erase(std::remove_if(queue.begin(), queue.end(), pred), queue.end()); -} - -PersistentRequestQueue::PersistentRequestQueue(Model *baseModel, std::shared_ptr filesystem) - : opStore(filesystem), baseModel(baseModel) { } - -PersistentRequestQueue::~PersistentRequestQueue() { - -} - -Request *PersistentRequestQueue::front() { - if (!head && !tailCache.empty()) { - MO_DBG_ERR("invalid state"); - pop_front(); - } - return head.get(); -} - -void PersistentRequestQueue::pop_front() { - - if (head && head->getStorageHandler() && head->getStorageHandler()->getOpNr() >= 0) { - opStore.advanceOpNr(head->getStorageHandler()->getOpNr()); - MO_DBG_DEBUG("advanced %i to %u", head->getStorageHandler()->getOpNr(), opStore.getOpBegin()); - } - - head.reset(); - - unsigned int nextOpNr = opStore.getOpBegin(); - - /* - * Find next operation to take as front. Two cases: - * A) [front, tailCache] contains all operations stored on this device - * B) [front, tailCache] does not contain all operations. The next operation after front is not present - * in the cache. Fetch the next operation from the flash to front - */ - - auto found = std::find_if(tailCache.begin(), tailCache.end(), - [nextOpNr] (std::unique_ptr& op) { - return op->getStorageHandler() && - op->getStorageHandler()->getOpNr() >= 0 && - (unsigned int) op->getStorageHandler()->getOpNr() == nextOpNr; - }); - - if (found != tailCache.end()) { - //cache hit -> case A) -> don't load from flash but just take the next element from tail - head = std::move(tailCache.front()); - tailCache.pop_front(); - } else { - //cache miss -> case B) or A) -> try to fetch operation from flash (check for case B)) or take first cached element as front - auto storageHandler = opStore.makeOpHandler(); - - std::unique_ptr fetched; - - unsigned int range = (opStore.getOpEnd() + MO_MAX_OPNR - nextOpNr) % MO_MAX_OPNR; - for (size_t i = 0; i < range; i++) { - bool exists = storageHandler->restore(nextOpNr); - if (exists) { - //case B) -> load operation from flash and take it as front element - - fetched = makeRequest(); - - bool success = fetched->restore(std::move(storageHandler), baseModel); - - if (success) { - //loaded operation from flash and will place it at head position of the queue - break; - } - - MO_DBG_ERR("could not restore operation"); - fetched.reset(); - opStore.advanceOpNr(nextOpNr); - nextOpNr = opStore.getOpBegin(); - } else { - //didn't find anything at this slot. Try next slot - nextOpNr++; - nextOpNr %= MO_MAX_OPNR; - } - } - - if (fetched) { - //found operation in flash -> case B) - head = std::move(fetched); - MO_DBG_DEBUG("restored operation from flash"); - } else { - //no operation anymore in flash -> case A) -> take next queued operation in tailCache - if (tailCache.empty()) { - //no operations anymore - } else { - head = std::move(tailCache.front()); - tailCache.pop_front(); - } - } - } - - MO_DBG_VERBOSE("popped front"); -} - -void PersistentRequestQueue::push_back(std::unique_ptr op) { - - op->initiate(opStore.makeOpHandler()); - - if (!head && !tailCache.empty()) { - MO_DBG_ERR("invalid state"); - pop_front(); - } - - if (!head) { - head = std::move(op); - } else { - if (tailCache.size() >= MO_OPERATIONCACHE_MAXSIZE) { - MO_DBG_INFO("Replace cached operation (cache full): %s", tailCache.front()->getOperationType()); - tailCache.front()->executeTimeout(); - tailCache.pop_front(); - } - - tailCache.push_back(std::move(op)); - } -} - -void PersistentRequestQueue::drop_if(std::function&)> pred) { - - while (head && pred(head)) { - pop_front(); - } - - tailCache.erase(std::remove_if(tailCache.begin(), tailCache.end(), pred), tailCache.end()); -} diff --git a/src/MicroOcpp/Core/RequestQueueStorageStrategy.h b/src/MicroOcpp/Core/RequestQueueStorageStrategy.h deleted file mode 100644 index 6d46e978..00000000 --- a/src/MicroOcpp/Core/RequestQueueStorageStrategy.h +++ /dev/null @@ -1,70 +0,0 @@ -// matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 -// MIT License - -#ifndef REQUESTQUEUESTORAGESTRATEGY_H -#define REQUESTQUEUESTORAGESTRATEGY_H - -#include - -#include -#include -#include - -namespace MicroOcpp { - -class FilesystemAdapter; -class Request; -class Model; - -class RequestQueueStorageStrategy { -public: - virtual ~RequestQueueStorageStrategy() = default; - - virtual Request *front() = 0; - virtual void pop_front() = 0; - - virtual void push_back(std::unique_ptr op) = 0; - - virtual void drop_if(std::function&)> pred) = 0; //drops operations from this queue where pred(operation) == true. Executes pred in order -}; - -class VolatileRequestQueue : public RequestQueueStorageStrategy { -private: - std::deque> queue; -public: - VolatileRequestQueue(); - ~VolatileRequestQueue(); - - Request *front() override; - void pop_front() override; - - void push_back(std::unique_ptr op) override; - - void drop_if(std::function&)> pred) override; //drops operations from this queue where pred(operation) == true. Executes pred in order -}; - -class PersistentRequestQueue : public RequestQueueStorageStrategy { -private: - RequestStore opStore; - Model *baseModel; - - std::unique_ptr head; - std::deque> tailCache; -public: - - PersistentRequestQueue(Model *baseModel, std::shared_ptr filesystem); - ~PersistentRequestQueue(); - - Request *front() override; - void pop_front() override; - - void push_back(std::unique_ptr op) override; - - void drop_if(std::function&)> pred) override; //drops operations from this queue where pred(operation) == true. Executes pred in order - -}; - -} - -#endif diff --git a/src/MicroOcpp/Core/RequestStore.cpp b/src/MicroOcpp/Core/RequestStore.cpp deleted file mode 100644 index 4fa602af..00000000 --- a/src/MicroOcpp/Core/RequestStore.cpp +++ /dev/null @@ -1,205 +0,0 @@ -// matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 -// MIT License - -#include -#include -#include -#include -#include - -#define MO_OPSTORE_FN MO_FILENAME_PREFIX "opstore.jsn" - -#define MO_OPHISTORY_SIZE 3 - -using namespace MicroOcpp; - -bool StoredOperationHandler::commit() { - if (isPersistent) { - MO_DBG_ERR("cannot call two times"); - return false; - } - if (!filesystem) { - MO_DBG_DEBUG("filesystem"); - return false; - } - - if (!rpc || !payload) { - MO_DBG_ERR("unitialized"); - return false; - } - - opNr = context.reserveOpNr(); - - char fn [MO_MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "op" "-%u.jsn", opNr); - if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { - MO_DBG_ERR("fn error: %i", ret); - return false; - } - - DynamicJsonDocument doc {JSON_OBJECT_SIZE(2) + rpc->capacity() + payload->capacity()}; - doc["rpc"] = *rpc; - doc["payload"] = *payload; - - if (!FilesystemUtils::storeJson(filesystem, fn, doc)) { - MO_DBG_ERR("FS error"); - return false; - } - - isPersistent = true; - return true; -} - -bool StoredOperationHandler::restore(unsigned int opNrToLoad) { - if (isPersistent) { - MO_DBG_ERR("cannot restore after commit"); - return false; - } - if (!filesystem) { - MO_DBG_DEBUG("filesystem"); - return false; - } - - opNr = opNrToLoad; - - char fn [MO_MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "op" "-%u.jsn", opNr); - if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { - MO_DBG_ERR("fn error: %i", ret); - return false; - } - - size_t msize; - if (filesystem->stat(fn, &msize) != 0) { - MO_DBG_VERBOSE("operation %u does not exist", opNr); - return false; - } - - auto doc = FilesystemUtils::loadJson(filesystem, fn); - if (!doc) { - MO_DBG_ERR("FS error"); - return false; - } - - JsonVariant rpc_restore = (*doc)["rpc"]; - JsonVariant payload_restore = (*doc)["payload"]; - - rpc = std::unique_ptr(new DynamicJsonDocument(rpc_restore.memoryUsage())); - payload = std::unique_ptr(new DynamicJsonDocument(payload_restore.memoryUsage())); - - *rpc = rpc_restore; - *payload = payload_restore; - - isPersistent = true; - return true; -} - -RequestStore::RequestStore(std::shared_ptr filesystem) : filesystem(filesystem) { - opBeginInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "opBegin", 0, MO_OPSTORE_FN, false, false, false); - configuration_load(MO_OPSTORE_FN); - - if (!opBeginInt || opBeginInt->getInt() < 0) { - MO_DBG_ERR("init failure"); - } else if (filesystem) { - opEnd = opBeginInt->getInt(); - - unsigned int misses = 0; - unsigned int i = opEnd; - while (misses < 3) { - char fn [MO_MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "op" "-%u.jsn", i); - if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { - MO_DBG_ERR("fn error: %i", ret); - misses++; - i = (i + 1) % MO_MAX_OPNR; - continue; - } - - size_t msize; - if (filesystem->stat(fn, &msize) != 0) { - MO_DBG_DEBUG("operation %u does not exist", i); - misses++; - i = (i + 1) % MO_MAX_OPNR; - continue; - } - - //file exists - misses = 0; - i = (i + 1) % MO_MAX_OPNR; - opEnd = i; - } - } -} - -std::unique_ptr RequestStore::makeOpHandler() { - return std::unique_ptr(new StoredOperationHandler(*this, filesystem)); -} - -unsigned int RequestStore::reserveOpNr() { - MO_DBG_DEBUG("reserved opNr %u", opEnd); - auto res = opEnd; - opEnd++; - opEnd %= MO_MAX_OPNR; - return res; -} - -void RequestStore::advanceOpNr(unsigned int oldOpNr) { - if (!opBeginInt || opBeginInt->getInt() < 0) { - MO_DBG_ERR("init failure"); - return; - } - - if (oldOpNr != (unsigned int) opBeginInt->getInt()) { - if ((oldOpNr + MO_MAX_OPNR - (unsigned int) opBeginInt->getInt()) % MO_MAX_OPNR < 100) { - MO_DBG_ERR("synchronization failure - try to fix"); - (void)0; - } else { - MO_DBG_ERR("synchronization failure"); - return; - } - } - - unsigned int opNr = (oldOpNr + 1) % MO_MAX_OPNR; - - //delete range [opBeginInt->getInt() ... opNr) - - unsigned int rangeSize = (opNr + MO_MAX_OPNR - (unsigned int) opBeginInt->getInt()) % MO_MAX_OPNR; - - MO_DBG_DEBUG("delete %u operations", rangeSize); - - for (unsigned int i = 0; i < rangeSize; i++) { - unsigned int op = ((unsigned int) opBeginInt->getInt() + i + MO_MAX_OPNR - MO_OPHISTORY_SIZE) % MO_MAX_OPNR; - - char fn [MO_MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "op" "-%u.jsn", op); - if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { - MO_DBG_ERR("fn error: %i", ret); - break; - } - - size_t msize; - if (filesystem->stat(fn, &msize) != 0) { - MO_DBG_DEBUG("operation %u does not exist", i); - continue; - } - - bool success = filesystem->remove(fn); - if (!success) { - MO_DBG_ERR("error deleting %s", fn); - (void)0; - } - } - - MO_DBG_DEBUG("advance opBegin: %u", opNr); - opBeginInt->setInt(opNr); - configuration_save(); -} - -unsigned int RequestStore::getOpBegin() { - if (!opBeginInt || opBeginInt->getInt() < 0) { - MO_DBG_ERR("invalid state"); - return 0; - } - return (unsigned int) opBeginInt->getInt(); -} diff --git a/src/MicroOcpp/Core/RequestStore.h b/src/MicroOcpp/Core/RequestStore.h deleted file mode 100644 index e6320ef5..00000000 --- a/src/MicroOcpp/Core/RequestStore.h +++ /dev/null @@ -1,70 +0,0 @@ -// matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 -// MIT License - -#ifndef MO_REQUESTSTORE_H -#define MO_REQUESTSTORE_H - -#include -#include -#include - -#define MO_MAX_OPNR 10000 - -namespace MicroOcpp { - -class RequestStore; -class FilesystemAdapter; -class Configuration; - -class StoredOperationHandler { -private: - RequestStore& context; - int opNr = -1; - std::shared_ptr filesystem; - - std::unique_ptr rpc; - std::unique_ptr payload; - - bool isPersistent = false; - -public: - StoredOperationHandler(RequestStore& context, std::shared_ptr filesystem) : context(context), filesystem(filesystem) {} - - void setRpcData(std::unique_ptr rpc) {this->rpc = std::move(rpc);} - void setPayload(std::unique_ptr payload) {this->payload = std::move(payload);} - - std::unique_ptr getRpcData() {return std::move(rpc);} - std::unique_ptr getPayload() {return std::move(payload);} - - bool commit(); - void clearBuffer() {rpc.reset(); payload.reset();} - - bool restore(unsigned int opNr); - - int getOpNr() {return isPersistent ? opNr : -1;} -}; - -class RequestStore { -private: - std::shared_ptr filesystem; - std::shared_ptr opBeginInt; //Tx-related operations are stored; index of the first pending operation - unsigned int opEnd = 0; //one place after last number - -public: - RequestStore() = delete; - RequestStore(std::shared_ptr filesystem); - - std::unique_ptr makeOpHandler(); - std::unique_ptr fetchOpHandler(unsigned int opNr); - - unsigned int reserveOpNr(); - void advanceOpNr(unsigned int oldOpNr); - - unsigned int getOpBegin(); - unsigned int getOpEnd() {return opEnd;} -}; - -} - -#endif diff --git a/src/MicroOcpp/Core/SimpleRequestFactory.cpp b/src/MicroOcpp/Core/SimpleRequestFactory.cpp deleted file mode 100644 index baf7a7ca..00000000 --- a/src/MicroOcpp/Core/SimpleRequestFactory.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 -// MIT License - -#include -#include - -namespace MicroOcpp { - -std::unique_ptr makeRequest(std::unique_ptr operation){ - if (operation == nullptr) { - return nullptr; - } - auto request = makeRequest(); - request->setOperation(std::move(operation)); - return request; -} - -std::unique_ptr makeRequest(Operation *operation) { - return makeRequest(std::unique_ptr(operation)); -} - -std::unique_ptr makeRequest(){ - auto result = std::unique_ptr(new Request()); - return result; -} - -} //end namespace MicroOcpp diff --git a/src/MicroOcpp/Core/SimpleRequestFactory.h b/src/MicroOcpp/Core/SimpleRequestFactory.h deleted file mode 100644 index 11c1f1c2..00000000 --- a/src/MicroOcpp/Core/SimpleRequestFactory.h +++ /dev/null @@ -1,23 +0,0 @@ -// matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 -// MIT License - -#ifndef MO_SIMPLEREQUESTFACTORY_H -#define MO_SIMPLEREQUESTFACTORY_H - -#include -#include -#include -#include - -namespace MicroOcpp { - -class Operation; - -std::unique_ptr makeRequest(); - -std::unique_ptr makeRequest(std::unique_ptr op); -std::unique_ptr makeRequest(Operation *op); //takes ownership of op - -} //end namespace MicroOcpp -#endif diff --git a/src/MicroOcpp/Core/Time.cpp b/src/MicroOcpp/Core/Time.cpp index 6437600b..1bcd0caa 100644 --- a/src/MicroOcpp/Core/Time.cpp +++ b/src/MicroOcpp/Core/Time.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -11,14 +11,22 @@ namespace MicroOcpp { const Timestamp MIN_TIME = Timestamp(2010, 0, 0, 0, 0, 0); const Timestamp MAX_TIME = Timestamp(2037, 0, 0, 0, 0, 0); -Timestamp::Timestamp() { +Timestamp::Timestamp() : MemoryManaged("Timestamp") { } -Timestamp::Timestamp(const Timestamp& other) { +Timestamp::Timestamp(const Timestamp& other) : MemoryManaged("Timestamp") { *this = other; } +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + Timestamp::Timestamp(int16_t year, int16_t month, int16_t day, int32_t hour, int32_t minute, int32_t second, int32_t ms) : + MemoryManaged("Timestamp"), year(year), month(month), day(day), hour(hour), minute(minute), second(second), ms(ms) { } +#else + Timestamp::Timestamp(int16_t year, int16_t month, int16_t day, int32_t hour, int32_t minute, int32_t second) : + MemoryManaged("Timestamp"), year(year), month(month), day(day), hour(hour), minute(minute), second(second) { } +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + int noDays(int month, int year) { return (month == 0 || month == 2 || month == 4 || month == 6 || month == 7 || month == 9 || month == 11) ? 31 : ((month == 3 || month == 5 || month == 8 || month == 10) ? 30 : @@ -102,7 +110,9 @@ bool Timestamp::setTime(const char *jsonDateString) { this->hour = hour; this->minute = minute; this->second = second; +#if MO_ENABLE_TIMESTAMP_MILLISECONDS this->ms = ms; +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS return true; } @@ -129,12 +139,17 @@ bool Timestamp::toJsonString(char *jsonDateString, size_t buffsize) const { jsonDateString[16] = ':'; jsonDateString[17] = ((char) ((second / 10) % 10)) + '0'; jsonDateString[18] = ((char) ((second / 1) % 10)) + '0'; +#if MO_ENABLE_TIMESTAMP_MILLISECONDS jsonDateString[19] = '.'; jsonDateString[20] = ((char) ((ms / 100) % 10)) + '0'; jsonDateString[21] = ((char) ((ms / 10) % 10)) + '0'; jsonDateString[22] = ((char) ((ms / 1) % 10)) + '0'; jsonDateString[23] = 'Z'; jsonDateString[24] = '\0'; +#else + jsonDateString[19] = 'Z'; + jsonDateString[20] = '\0'; +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS return true; } @@ -190,8 +205,9 @@ Timestamp &Timestamp::operator+=(int secs) { } return *this; -}; +} +#if MO_ENABLE_TIMESTAMP_MILLISECONDS Timestamp &Timestamp::addMilliseconds(int val) { ms += val; @@ -206,6 +222,7 @@ Timestamp &Timestamp::addMilliseconds(int val) { } return this->operator+=(dsecond); } +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS Timestamp &Timestamp::operator-=(int secs) { return operator+=(-secs); @@ -238,6 +255,13 @@ int Timestamp::operator-(const Timestamp &rhs) const { } int dt = (lhsDays - rhsDays) * (24 * 3600) + (hour - rhs.hour) * 3600 + (minute - rhs.minute) * 60 + second - rhs.second; + +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + // Make it so that we round the difference to the nearest second, instead of being up to almost a whole second off + if ((ms - rhs.ms) > 500) dt++; + if ((ms - rhs.ms) < -500) dt--; +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + return dt; } @@ -248,7 +272,9 @@ Timestamp &Timestamp::operator=(const Timestamp &rhs) { hour = rhs.hour; minute = rhs.minute; second = rhs.second; +#if MO_ENABLE_TIMESTAMP_MILLISECONDS ms = rhs.ms; +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS return *this; } @@ -264,7 +290,11 @@ Timestamp operator-(const Timestamp &lhs, int secs) { } bool operator==(const Timestamp &lhs, const Timestamp &rhs) { - return lhs.year == rhs.year && lhs.month == rhs.month && lhs.day == rhs.day && lhs.hour == rhs.hour && lhs.minute == rhs.minute && lhs.second == rhs.second && lhs.ms == rhs.ms; + return lhs.year == rhs.year && lhs.month == rhs.month && lhs.day == rhs.day && lhs.hour == rhs.hour && lhs.minute == rhs.minute && lhs.second == rhs.second +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + && lhs.ms == rhs.ms +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + ; } bool operator!=(const Timestamp &lhs, const Timestamp &rhs) { @@ -284,8 +314,10 @@ bool operator<(const Timestamp &lhs, const Timestamp &rhs) { 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; } @@ -327,8 +359,14 @@ const Timestamp &Clock::now() { auto tReading = mocpp_tick_ms(); auto delta = tReading - lastUpdate; +#if MO_ENABLE_TIMESTAMP_MILLISECONDS currentTime.addMilliseconds(delta); lastUpdate = tReading; +#else + auto deltaSecs = delta / 1000; + currentTime += deltaSecs; + lastUpdate += deltaSecs * 1000; +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS return currentTime; } diff --git a/src/MicroOcpp/Core/Time.h b/src/MicroOcpp/Core/Time.h index 83e3bd27..9a82753b 100644 --- a/src/MicroOcpp/Core/Time.h +++ b/src/MicroOcpp/Core/Time.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_TIME_H @@ -9,13 +9,22 @@ #include #include +#include #include -#define JSONDATE_LENGTH 24 +#ifndef MO_ENABLE_TIMESTAMP_MILLISECONDS +#define MO_ENABLE_TIMESTAMP_MILLISECONDS 0 +#endif + +#if MO_ENABLE_TIMESTAMP_MILLISECONDS +#define JSONDATE_LENGTH 24 //max. ISO 8601 date length, excluding the terminating zero +#else +#define JSONDATE_LENGTH 20 //ISO 8601 date length, excluding the terminating zero +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS namespace MicroOcpp { -class Timestamp { +class Timestamp : public MemoryManaged { private: /* * Internal representation of the current time. The initial values correspond to UNIX-time 0. January @@ -27,7 +36,9 @@ class Timestamp { int32_t hour = 0; int32_t minute = 0; int32_t second = 0; +#if MO_ENABLE_TIMESTAMP_MILLISECONDS int32_t ms = 0; +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS public: @@ -35,8 +46,11 @@ class Timestamp { Timestamp(const Timestamp& other); - Timestamp(int16_t year, int16_t month, int16_t day, int32_t hour, int32_t minute, int32_t second, int32_t ms = 0) : - year(year), month(month), day(day), hour(hour), minute(minute), second(second), ms(ms) { }; +#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 @@ -57,7 +71,9 @@ class Timestamp { 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 diff --git a/src/MicroOcpp/Core/UuidUtils.cpp b/src/MicroOcpp/Core/UuidUtils.cpp new file mode 100644 index 00000000..7d3ddb15 --- /dev/null +++ b/src/MicroOcpp/Core/UuidUtils.cpp @@ -0,0 +1,56 @@ +#include +#include + +#include + +namespace MicroOcpp { + +#define UUID_STR_LEN 36 + +bool generateUUID(char *uuidBuffer, size_t len) { + if (len < UUID_STR_LEN + 1) + { + return false; + } + + uint32_t ar[4]; + for (uint8_t i = 0; i < 4; i++) { + ar[i] = mocpp_rng(); + } + + // Conforming to RFC 4122 Specification + // - byte 7: four most significant bits ==> 0100 --> always 4 + // - byte 9: two most significant bits ==> 10 --> always {8, 9, A, B}. + // + // patch bits for version 1 and variant 4 here + ar[1] &= 0xFFF0FFFF; // remove 4 bits. + ar[1] |= 0x00040000; // variant 4 + ar[2] &= 0xFFFFFFF3; // remove 2 bits + ar[2] |= 0x00000008; // version 1 + + // loop through the random 16 byte array + for (uint8_t i = 0, j = 0; i < 16; i++) { + // multiples of 4 between 8 and 20 get a -. + // note we are processing 2 digits in one loop. + if ((i & 0x1) == 0) { + if ((4 <= i) && (i <= 10)) { + uuidBuffer[j++] = '-'; + } + } + + // encode the byte as two hex characters + uint8_t nr = i / 4; + uint8_t xx = ar[nr]; + uint8_t ch = xx & 0x0F; + uuidBuffer[j++] = (ch < 10)? '0' + ch : ('a' - 10) + ch; + + ch = (xx >> 4) & 0x0F; + ar[nr] >>= 8; + uuidBuffer[j++] = (ch < 10)? '0' + ch : ('a' - 10) + ch; + } + + uuidBuffer[UUID_STR_LEN] = 0; + return true; +} + +} diff --git a/src/MicroOcpp/Core/UuidUtils.h b/src/MicroOcpp/Core/UuidUtils.h new file mode 100644 index 00000000..3516effe --- /dev/null +++ b/src/MicroOcpp/Core/UuidUtils.h @@ -0,0 +1,14 @@ +#ifndef MO_UUIDUTILS_H +#define MO_UUIDUTILS_H + +#include +namespace MicroOcpp { + +// Generates a UUID (Universally Unique Identifier) and writes it into a given buffer +// Returns false if the generation failed +// The buffer must be at least 37 bytes long (36 characters + zero termination) +bool generateUUID(char *uuidBuffer, size_t len); + +} + +#endif diff --git a/src/MicroOcpp/Debug.cpp b/src/MicroOcpp/Debug.cpp new file mode 100644 index 00000000..71f42343 --- /dev/null +++ b/src/MicroOcpp/Debug.cpp @@ -0,0 +1,54 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include + +const char *level_label [] = { + "", //MO_DL_NONE 0x00 + "ERROR", //MO_DL_ERROR 0x01 + "warning", //MO_DL_WARN 0x02 + "info", //MO_DL_INFO 0x03 + "debug", //MO_DL_DEBUG 0x04 + "verbose" //MO_DL_VERBOSE 0x05 +}; + +#if MO_DBG_FORMAT == MO_DF_MINIMAL +void mo_dbg_print_prefix(int level, const char *fn, int line) { + (void)0; +} + +#elif MO_DBG_FORMAT == MO_DF_COMPACT +void mo_dbg_print_prefix(int level, const char *fn, int line) { + size_t l = strlen(fn); + size_t r = l; + while (l > 0 && fn[l-1] != '/' && fn[l-1] != '\\') { + l--; + if (fn[l] == '.') r = l; + } + MO_CONSOLE_PRINTF("%.*s:%i ", (int) (r - l), fn + l, line); +} + +#elif MO_DBG_FORMAT == MO_DF_FILE_LINE +void mo_dbg_print_prefix(int level, const char *fn, int line) { + size_t l = strlen(fn); + while (l > 0 && fn[l-1] != '/' && fn[l-1] != '\\') { + l--; + } + MO_CONSOLE_PRINTF("[MO] %s (%s:%i): ", level_label[level], fn + l, line); +} + +#elif MO_DBG_FORMAT == MO_DF_FULL +void mo_dbg_print_prefix(int level, const char *fn, int line) { + MO_CONSOLE_PRINTF("[MO] %s (%s:%i): ", level_label[level], fn, line); +} + +#else +#error invalid MO_DBG_FORMAT definition +#endif + +void mo_dbg_print_suffix() { + MO_CONSOLE_PRINTF("\n"); +} diff --git a/src/MicroOcpp/Debug.h b/src/MicroOcpp/Debug.h index 238e8b7a..51e71216 100644 --- a/src/MicroOcpp/Debug.h +++ b/src/MicroOcpp/Debug.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_DEBUG_H @@ -18,6 +18,11 @@ #define MO_DBG_LEVEL MO_DL_INFO //default #endif +//MbedTLS debug level documented in mbedtls/debug.h: +#ifndef MO_DBG_LEVEL_MBEDTLS +#define MO_DBG_LEVEL_MBEDTLS 1 +#endif + #define MO_DF_MINIMAL 0x00 //don't reveal origin of a debug message #define MO_DF_COMPACT 0x01 //print module by file name and line number #define MO_DF_FILE_LINE 0x02 //print file and line number @@ -27,81 +32,52 @@ #define MO_DBG_FORMAT MO_DF_FILE_LINE //default #endif +#ifdef __cplusplus +extern "C" { +#endif -#if MO_DBG_FORMAT == MO_DF_MINIMAL -#define MO_DBG(level, X) \ - do { \ - MO_CONSOLE_PRINTF X; \ - MO_CONSOLE_PRINTF("\n"); \ - } while (0); - -#elif MO_DBG_FORMAT == MO_DF_COMPACT -#define MO_DBG(level, X) \ - do { \ - const char *_mo_file = __FILE__; \ - size_t _mo_l = sizeof(__FILE__); \ - size_t _mo_r = _mo_l; \ - while (_mo_l > 0 && _mo_file[_mo_l-1] != '/' && _mo_file[_mo_l-1] != '\\') { \ - _mo_l--; \ - if (_mo_file[_mo_l] == '.') _mo_r = _mo_l; \ - } \ - MO_CONSOLE_PRINTF("%.*s:%i ", (int) (_mo_r - _mo_l), _mo_file + _mo_l,__LINE__); \ - MO_CONSOLE_PRINTF X; \ - MO_CONSOLE_PRINTF("\n"); \ - } while (0); - -#elif MO_DBG_FORMAT == MO_DF_FILE_LINE -#define MO_DBG(level, X) \ - do { \ - const char *_mo_file = __FILE__; \ - size_t _mo_l = sizeof(__FILE__); \ - for (; _mo_l > 0 && _mo_file[_mo_l-1] != '/' && _mo_file[_mo_l-1] != '\\'; _mo_l--); \ - MO_CONSOLE_PRINTF("[MO] %s (%s:%i): ",level, _mo_file + _mo_l,__LINE__); \ - MO_CONSOLE_PRINTF X; \ - MO_CONSOLE_PRINTF("\n"); \ - } while (0); - -#elif MO_DBG_FORMAT == MO_DF_FULL -#define MO_DBG(level, X) \ - do { \ - MO_CONSOLE_PRINTF("[MO] %s (%s:%i): ",level, __FILE__,__LINE__); \ - MO_CONSOLE_PRINTF X; \ - MO_CONSOLE_PRINTF("\n"); \ - } while (0); +void mo_dbg_print_prefix(int level, const char *fn, int line); +void mo_dbg_print_suffix(); -#else -#error invalid MO_DBG_FORMAT definition +#ifdef __cplusplus +} #endif +#define MO_DBG(level, X) \ + do { \ + mo_dbg_print_prefix(level, __FILE__, __LINE__); \ + MO_CONSOLE_PRINTF X; \ + mo_dbg_print_suffix(); \ + } while (0) #if MO_DBG_LEVEL >= MO_DL_ERROR -#define MO_DBG_ERR(...) MO_DBG("ERROR",(__VA_ARGS__)) +#define MO_DBG_ERR(...) MO_DBG(MO_DL_ERROR,(__VA_ARGS__)) #else -#define MO_DBG_ERR(...) +#define MO_DBG_ERR(...) ((void)0) #endif #if MO_DBG_LEVEL >= MO_DL_WARN -#define MO_DBG_WARN(...) MO_DBG("warning",(__VA_ARGS__)) +#define MO_DBG_WARN(...) MO_DBG(MO_DL_WARN,(__VA_ARGS__)) #else -#define MO_DBG_WARN(...) +#define MO_DBG_WARN(...) ((void)0) #endif #if MO_DBG_LEVEL >= MO_DL_INFO -#define MO_DBG_INFO(...) MO_DBG("info",(__VA_ARGS__)) +#define MO_DBG_INFO(...) MO_DBG(MO_DL_INFO,(__VA_ARGS__)) #else -#define MO_DBG_INFO(...) +#define MO_DBG_INFO(...) ((void)0) #endif #if MO_DBG_LEVEL >= MO_DL_DEBUG -#define MO_DBG_DEBUG(...) MO_DBG("debug",(__VA_ARGS__)) +#define MO_DBG_DEBUG(...) MO_DBG(MO_DL_DEBUG,(__VA_ARGS__)) #else -#define MO_DBG_DEBUG(...) +#define MO_DBG_DEBUG(...) ((void)0) #endif #if MO_DBG_LEVEL >= MO_DL_VERBOSE -#define MO_DBG_VERBOSE(...) MO_DBG("verbose",(__VA_ARGS__)) +#define MO_DBG_VERBOSE(...) MO_DBG(MO_DL_VERBOSE,(__VA_ARGS__)) #else -#define MO_DBG_VERBOSE(...) +#define MO_DBG_VERBOSE(...) ((void)0) #endif #ifdef MO_TRAFFIC_OUT @@ -119,8 +95,8 @@ } while (0) #else -#define MO_DBG_TRAFFIC_OUT(...) -#define MO_DBG_TRAFFIC_IN(...) +#define MO_DBG_TRAFFIC_OUT(...) ((void)0) +#define MO_DBG_TRAFFIC_IN(...) ((void)0) #endif #endif diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationData.cpp b/src/MicroOcpp/Model/Authorization/AuthorizationData.cpp index 140164a8..08cbfbca 100644 --- a/src/MicroOcpp/Model/Authorization/AuthorizationData.cpp +++ b/src/MicroOcpp/Model/Authorization/AuthorizationData.cpp @@ -1,25 +1,32 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + +#if MO_ENABLE_LOCAL_AUTH + #include +#include using namespace MicroOcpp; -AuthorizationData::AuthorizationData() { +AuthorizationData::AuthorizationData() : MemoryManaged("v16.Authorization.AuthorizationData") { } -AuthorizationData::AuthorizationData(AuthorizationData&& other) { +AuthorizationData::AuthorizationData(AuthorizationData&& other) : MemoryManaged("v16.Authorization.AuthorizationData") { operator=(std::move(other)); } AuthorizationData::~AuthorizationData() { - + MO_FREE(parentIdTag); + parentIdTag = nullptr; } AuthorizationData& AuthorizationData::operator=(AuthorizationData&& other) { - parentIdTag = std::move(other.parentIdTag); + parentIdTag = other.parentIdTag; + other.parentIdTag = nullptr; expiryDate = std::move(other.expiryDate); strncpy(idTag, other.idTag, IDTAG_LEN_MAX + 1); idTag[IDTAG_LEN_MAX] = '\0'; @@ -52,11 +59,18 @@ void AuthorizationData::readJson(JsonObject entry, bool compact) { } if (idTagInfo.containsKey(AUTHDATA_KEY_PARENTIDTAG(compact))) { - parentIdTag = std::unique_ptr(new char[IDTAG_LEN_MAX + 1]); - strncpy(parentIdTag.get(), idTagInfo[AUTHDATA_KEY_PARENTIDTAG(compact)], IDTAG_LEN_MAX + 1); - parentIdTag.get()[IDTAG_LEN_MAX] = '\0'; + MO_FREE(parentIdTag); + parentIdTag = nullptr; + parentIdTag = static_cast(MO_MALLOC(getMemoryTag(), IDTAG_LEN_MAX + 1)); + if (parentIdTag) { + strncpy(parentIdTag, idTagInfo[AUTHDATA_KEY_PARENTIDTAG(compact)], IDTAG_LEN_MAX + 1); + parentIdTag[IDTAG_LEN_MAX] = '\0'; + } else { + MO_DBG_ERR("OOM"); + } } else { - parentIdTag.reset(); + MO_FREE(parentIdTag); + parentIdTag = nullptr; } if (idTagInfo.containsKey(AUTHDATA_KEY_STATUS(compact))) { @@ -102,7 +116,7 @@ void AuthorizationData::writeJson(JsonObject& entry, bool compact) { } if (parentIdTag) { - idTagInfo[AUTHDATA_KEY_PARENTIDTAG(compact)] = (const char *) parentIdTag.get(); + idTagInfo[AUTHDATA_KEY_PARENTIDTAG(compact)] = (const char *) parentIdTag; } if (status != AuthorizationStatus::Accepted) { @@ -112,6 +126,19 @@ void AuthorizationData::writeJson(JsonObject& entry, bool compact) { } } +const char *AuthorizationData::getIdTag() const { + return idTag; +} +Timestamp *AuthorizationData::getExpiryDate() const { + return expiryDate.get(); +} +const char *AuthorizationData::getParentIdTag() const { + return parentIdTag; +} +AuthorizationStatus AuthorizationData::getAuthorizationStatus() const { + return status; +} + void AuthorizationData::reset() { idTag[0] = '\0'; } @@ -152,3 +179,5 @@ MicroOcpp::AuthorizationStatus MicroOcpp::deserializeAuthorizationStatus(const c return AuthorizationStatus::UNDEFINED; } } + +#endif //MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationData.h b/src/MicroOcpp/Model/Authorization/AuthorizationData.h index 69f1592c..7bcdccd7 100644 --- a/src/MicroOcpp/Model/Authorization/AuthorizationData.h +++ b/src/MicroOcpp/Model/Authorization/AuthorizationData.h @@ -1,12 +1,17 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef AUTHORIZATIONDATA_H -#define AUTHORIZATIONDATA_H +#ifndef MO_AUTHORIZATIONDATA_H +#define MO_AUTHORIZATIONDATA_H + +#include + +#if MO_ENABLE_LOCAL_AUTH #include #include +#include #include #include @@ -32,11 +37,11 @@ enum class AuthorizationStatus : uint8_t { const char *serializeAuthorizationStatus(AuthorizationStatus status); AuthorizationStatus deserializeAuthorizationStatus(const char *cstr); -class AuthorizationData { +class AuthorizationData : public MemoryManaged { private: //data structure optimized for memory consumption - std::unique_ptr parentIdTag; + char *parentIdTag = nullptr; std::unique_ptr expiryDate; char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; @@ -54,14 +59,15 @@ class AuthorizationData { size_t getJsonCapacity() const; void writeJson(JsonObject& entry, bool compact = false); //compact: compressed representation for flash storage - const char *getIdTag() const {return idTag;} - Timestamp *getExpiryDate() const {return expiryDate.get();} - const char *getParentIdTag() const {return parentIdTag.get();} - AuthorizationStatus getAuthorizationStatus() const {return status;} + const char *getIdTag() const; + Timestamp *getExpiryDate() const; + const char *getParentIdTag() const; + AuthorizationStatus getAuthorizationStatus() const; void reset(); }; } +#endif //MO_ENABLE_LOCAL_AUTH #endif diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationList.cpp b/src/MicroOcpp/Model/Authorization/AuthorizationList.cpp index 9dfaff85..dd438ff8 100644 --- a/src/MicroOcpp/Model/Authorization/AuthorizationList.cpp +++ b/src/MicroOcpp/Model/Authorization/AuthorizationList.cpp @@ -1,7 +1,11 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + +#if MO_ENABLE_LOCAL_AUTH + #include #include @@ -10,7 +14,7 @@ using namespace MicroOcpp; -AuthorizationList::AuthorizationList() { +AuthorizationList::AuthorizationList() : MemoryManaged("v16.Authorization.AuthorizationList"), localAuthorizationList(makeVector(getMemoryTag())) { } @@ -56,8 +60,8 @@ bool AuthorizationList::readJson(JsonArray authlistJson, int listVersion, bool d } } - std::vector authlist_index; - std::vector remove_list; + auto authlist_index = makeVector(getMemoryTag()); + auto remove_list = makeVector(getMemoryTag()); unsigned int resultingListLength = 0; @@ -70,7 +74,7 @@ bool AuthorizationList::readJson(JsonArray authlistJson, int listVersion, bool d resultingListLength = localAuthorizationList.size(); //also, build index here - authlist_index = std::vector(authlistJson.size(), -1); + authlist_index.resize(authlistJson.size(), -1); for (size_t i = 0; i < authlistJson.size(); i++) { @@ -109,7 +113,7 @@ bool AuthorizationList::readJson(JsonArray authlistJson, int listVersion, bool d localAuthorizationList.clear(); for (size_t i = 0; i < authlistJson.size(); i++) { - localAuthorizationList.push_back(AuthorizationData()); + localAuthorizationList.emplace_back(); localAuthorizationList.back().readJson(authlistJson[i], compact); } } else if (differential) { @@ -134,7 +138,7 @@ bool AuthorizationList::readJson(JsonArray authlistJson, int listVersion, bool d } else { //no, create new authlist_index[i] = localAuthorizationList.size(); - localAuthorizationList.push_back(AuthorizationData()); + localAuthorizationList.emplace_back(); } } @@ -146,7 +150,7 @@ bool AuthorizationList::readJson(JsonArray authlistJson, int listVersion, bool d for (size_t i = 0; i < authlistJson.size(); i++) { if (authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAGINFO)) { - localAuthorizationList.push_back(AuthorizationData()); + localAuthorizationList.emplace_back(); localAuthorizationList.back().readJson(authlistJson[i], compact); } } @@ -194,3 +198,5 @@ void AuthorizationList::writeJson(JsonArray authListOut, bool compact) { size_t AuthorizationList::size() { return localAuthorizationList.size(); } + +#endif //MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationList.h b/src/MicroOcpp/Model/Authorization/AuthorizationList.h index f2c57ac2..0a083777 100644 --- a/src/MicroOcpp/Model/Authorization/AuthorizationList.h +++ b/src/MicroOcpp/Model/Authorization/AuthorizationList.h @@ -1,12 +1,16 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef AUTHORIZATIONLIST_H -#define AUTHORIZATIONLIST_H +#ifndef MO_AUTHORIZATIONLIST_H +#define MO_AUTHORIZATIONLIST_H + +#include + +#if MO_ENABLE_LOCAL_AUTH #include -#include +#include #ifndef MO_LocalAuthListMaxLength #define MO_LocalAuthListMaxLength 48 @@ -18,10 +22,10 @@ namespace MicroOcpp { -class AuthorizationList { +class AuthorizationList : public MemoryManaged { private: int listVersion = 0; - std::vector localAuthorizationList; //sorted list + Vector localAuthorizationList; //sorted list public: AuthorizationList(); ~AuthorizationList(); @@ -41,4 +45,5 @@ class AuthorizationList { } +#endif //MO_ENABLE_LOCAL_AUTH #endif diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationService.cpp b/src/MicroOcpp/Model/Authorization/AuthorizationService.cpp index 36011afd..dc395ff9 100644 --- a/src/MicroOcpp/Model/Authorization/AuthorizationService.cpp +++ b/src/MicroOcpp/Model/Authorization/AuthorizationService.cpp @@ -1,14 +1,18 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + +#if MO_ENABLE_LOCAL_AUTH + #include #include #include #include #include #include -#include +#include #include #include #include @@ -18,9 +22,9 @@ using namespace MicroOcpp; -AuthorizationService::AuthorizationService(Context& context, std::shared_ptr filesystem) : context(context), filesystem(filesystem) { - - localAuthListEnabledBool = declareConfiguration("LocalAuthListEnabled", true); +AuthorizationService::AuthorizationService(Context& context, std::shared_ptr filesystem) : MemoryManaged("v16.Authorization.AuthorizationService"), context(context), filesystem(filesystem) { + + localAuthListEnabledBool = declareConfiguration("LocalAuthListEnabled", true, CONFIGURATION_FN, false, true); declareConfiguration("LocalAuthListMaxLength", MO_LocalAuthListMaxLength, CONFIGURATION_VOLATILE, true); declareConfiguration("SendLocalListMaxLength", MO_SendLocalListMaxLength, CONFIGURATION_VOLATILE, true); @@ -52,7 +56,7 @@ bool AuthorizationService::loadLists() { return true; } - auto doc = FilesystemUtils::loadJson(filesystem, MO_LOCALAUTHORIZATIONLIST_FN); + auto doc = FilesystemUtils::loadJson(filesystem, MO_LOCALAUTHORIZATIONLIST_FN, getMemoryTag()); if (!doc) { MO_DBG_ERR("failed to load %s", MO_LOCALAUTHORIZATIONLIST_FN); return false; @@ -71,7 +75,7 @@ bool AuthorizationService::loadLists() { } AuthorizationData *AuthorizationService::getLocalAuthorization(const char *idTag) { - if (!localAuthListEnabledBool->getBool()) { + if (!localAuthListEnabled()) { return nullptr; //auth cache will follow } @@ -97,12 +101,19 @@ size_t AuthorizationService::getLocalListSize() { return localAuthorizationList.size(); } +bool AuthorizationService::localAuthListEnabled() const { + return localAuthListEnabledBool && localAuthListEnabledBool->getBool(); +} + bool AuthorizationService::updateLocalList(JsonArray localAuthorizationListJson, int listVersion, bool differential) { + //TC_043_3_CS-Send Local Authorization List - Failed + //return false; + bool success = localAuthorizationList.readJson(localAuthorizationListJson, listVersion, differential, false); if (success) { - DynamicJsonDocument doc ( + auto doc = initJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(3) + localAuthorizationList.getJsonCapacity()); @@ -123,7 +134,7 @@ bool AuthorizationService::updateLocalList(JsonArray localAuthorizationListJson, void AuthorizationService::notifyAuthorization(const char *idTag, JsonObject idTagInfo) { //check local list conflicts. In future: also update authorization cache - if (!localAuthListEnabledBool->getBool()) { + if (!localAuthListEnabled()) { return; //auth cache will follow } @@ -176,7 +187,7 @@ void AuthorizationService::notifyAuthorization(const char *idTag, JsonObject idT if (!equivalent) { //send error code "LocalListConflict" to server - ChargePointStatus cpStatus = ChargePointStatus::NOT_SET; + ChargePointStatus cpStatus = ChargePointStatus_UNDEFINED; if (context.getModel().getNumConnectors() > 0) { cpStatus = context.getModel().getConnector(0)->getStatus(); } @@ -192,3 +203,5 @@ void AuthorizationService::notifyAuthorization(const char *idTag, JsonObject idT context.initiateRequest(std::move(statusNotification)); } } + +#endif //MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationService.h b/src/MicroOcpp/Model/Authorization/AuthorizationService.h index 8a2c6043..536bba1f 100644 --- a/src/MicroOcpp/Model/Authorization/AuthorizationService.h +++ b/src/MicroOcpp/Model/Authorization/AuthorizationService.h @@ -1,19 +1,24 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef AUTHORIZATIONSERVICE_H -#define AUTHORIZATIONSERVICE_H +#ifndef MO_AUTHORIZATIONSERVICE_H +#define MO_AUTHORIZATIONSERVICE_H + +#include + +#if MO_ENABLE_LOCAL_AUTH #include #include #include +#include namespace MicroOcpp { class Context; -class AuthorizationService { +class AuthorizationService : public MemoryManaged { private: Context& context; std::shared_ptr filesystem; @@ -30,6 +35,7 @@ class AuthorizationService { AuthorizationData *getLocalAuthorization(const char *idTag); int getLocalListVersion(); + bool localAuthListEnabled() const; size_t getLocalListSize(); //number of entries in current localAuthList; used in unit tests bool updateLocalList(JsonArray localAuthorizationListJson, int listVersion, bool differential); @@ -39,4 +45,5 @@ class AuthorizationService { } +#endif //MO_ENABLE_LOCAL_AUTH #endif diff --git a/src/MicroOcpp/Model/Authorization/IdToken.cpp b/src/MicroOcpp/Model/Authorization/IdToken.cpp new file mode 100644 index 00000000..e1637ef8 --- /dev/null +++ b/src/MicroOcpp/Model/Authorization/IdToken.cpp @@ -0,0 +1,110 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include + +#include +#include + +#include + +using namespace MicroOcpp; + +IdToken::IdToken(const char *token, Type type, const char *memoryTag) : MemoryManaged(memoryTag ? memoryTag : "v201.Authorization.IdToken"), type(type) { + if (token) { + auto ret = snprintf(idToken, MO_IDTOKEN_LEN_MAX + 1, "%s", token); + if (ret < 0 || ret >= MO_IDTOKEN_LEN_MAX + 1) { + MO_DBG_ERR("invalid token"); + *idToken = '\0'; + } + } else { + *idToken = '\0'; + } +} + +IdToken::IdToken(const IdToken& other, const char *memoryTag) : IdToken(other.idToken, other.type, memoryTag ? memoryTag : other.getMemoryTag()) { + +} + +bool IdToken::parseCstr(const char *token, const char *typeCstr) { + if (!token || !typeCstr) { + return false; + } + + if (!strcmp(typeCstr, "Central")) { + type = Type::Central; + } else if (!strcmp(typeCstr, "eMAID")) { + type = Type::eMAID; + } else if (!strcmp(typeCstr, "ISO14443")) { + type = Type::ISO14443; + } else if (!strcmp(typeCstr, "ISO15693")) { + type = Type::ISO15693; + } else if (!strcmp(typeCstr, "KeyCode")) { + type = Type::KeyCode; + } else if (!strcmp(typeCstr, "Local")) { + type = Type::Local; + } else if (!strcmp(typeCstr, "MacAddress")) { + type = Type::MacAddress; + } else if (!strcmp(typeCstr, "NoAuthorization")) { + type = Type::NoAuthorization; + } else { + return false; + } + + auto ret = snprintf(idToken, sizeof(idToken), "%s", token); + if (ret < 0 || (size_t)ret >= sizeof(idToken)) { + return false; + } + + return true; +} + +const char *IdToken::get() const { + return idToken; +} + +const char *IdToken::getTypeCstr() const { + const char *res = ""; + switch (type) { + case Type::UNDEFINED: + MO_DBG_ERR("internal error"); + break; + case Type::Central: + res = "Central"; + break; + case Type::eMAID: + res = "eMAID"; + break; + case Type::ISO14443: + res = "ISO14443"; + break; + case Type::ISO15693: + res = "ISO15693"; + break; + case Type::KeyCode: + res = "KeyCode"; + break; + case Type::Local: + res = "Local"; + break; + case Type::MacAddress: + res = "MacAddress"; + break; + case Type::NoAuthorization: + res = "NoAuthorization"; + break; + } + + return res; +} + +bool IdToken::equals(const IdToken& other) { + return type == other.type && !strcmp(idToken, other.idToken); +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Authorization/IdToken.h b/src/MicroOcpp/Model/Authorization/IdToken.h new file mode 100644 index 00000000..cb209872 --- /dev/null +++ b/src/MicroOcpp/Model/Authorization/IdToken.h @@ -0,0 +1,56 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_IDTOKEN_H +#define MO_IDTOKEN_H + +#include + +#if MO_ENABLE_V201 + +#include + +#include + +#define MO_IDTOKEN_LEN_MAX 36 + +namespace MicroOcpp { + +// IdTokenType (2.28) +class IdToken : public MemoryManaged { +public: + + // IdTokenEnumType (3.43) + enum class Type : uint8_t { + Central, + eMAID, + ISO14443, + ISO15693, + KeyCode, + Local, + MacAddress, + NoAuthorization, + UNDEFINED + }; + +private: + char idToken [MO_IDTOKEN_LEN_MAX + 1]; + Type type = Type::UNDEFINED; +public: + IdToken(const char *token = nullptr, Type type = Type::ISO14443, const char *memoryTag = nullptr); + + IdToken(const IdToken& other, const char *memoryTag = nullptr); + + bool parseCstr(const char *token, const char *typeCstr); + + const char *get() const; + const char *getTypeCstr() const; + + bool equals(const IdToken& other); +}; + +} // namespace MicroOcpp + +#endif // MO_ENABLE_V201 +#endif diff --git a/src/MicroOcpp/Model/Availability/AvailabilityService.cpp b/src/MicroOcpp/Model/Availability/AvailabilityService.cpp new file mode 100644 index 00000000..b1e59419 --- /dev/null +++ b/src/MicroOcpp/Model/Availability/AvailabilityService.cpp @@ -0,0 +1,203 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace MicroOcpp; + +AvailabilityServiceEvse::AvailabilityServiceEvse(Context& context, AvailabilityService& availabilityService, unsigned int evseId) : MemoryManaged("v201.Availability.AvailabilityServiceEvse"), context(context), availabilityService(availabilityService), evseId(evseId) { + +} + +void AvailabilityServiceEvse::loop() { + + if (evseId >= 1) { + auto status = getStatus(); + + if (status != reportedStatus && + context.getModel().getClock().now() >= MIN_TIME) { + + auto statusNotification = makeRequest(new Ocpp201::StatusNotification(evseId, status, context.getModel().getClock().now())); + statusNotification->setTimeout(0); + context.initiateRequest(std::move(statusNotification)); + reportedStatus = status; + return; + } + } +} + +void AvailabilityServiceEvse::setConnectorPluggedInput(std::function connectorPluggedInput) { + this->connectorPluggedInput = connectorPluggedInput; +} + +void AvailabilityServiceEvse::setOccupiedInput(std::function occupiedInput) { + this->occupiedInput = occupiedInput; +} + +ChargePointStatus AvailabilityServiceEvse::getStatus() { + ChargePointStatus res = ChargePointStatus_UNDEFINED; + + if (isFaulted()) { + res = ChargePointStatus_Faulted; + } else if (!isAvailable()) { + res = ChargePointStatus_Unavailable; + } + #if MO_ENABLE_RESERVATION + else if (context.getModel().getReservationService() && context.getModel().getReservationService()->getReservation(evseId)) { + res = ChargePointStatus_Reserved; + } + #endif + else if ((!connectorPluggedInput || !connectorPluggedInput()) && //no vehicle plugged + (!occupiedInput || !occupiedInput())) { //occupied override clear + res = ChargePointStatus_Available; + } else { + res = ChargePointStatus_Occupied; + } + + return res; +} + +void AvailabilityServiceEvse::setUnavailable(void *requesterId) { + for (size_t i = 0; i < MO_INOPERATIVE_REQUESTERS_MAX; i++) { + if (!unavailableRequesters[i]) { + unavailableRequesters[i] = requesterId; + return; + } + } + MO_DBG_ERR("exceeded max. unavailable requesters"); +} + +void AvailabilityServiceEvse::setAvailable(void *requesterId) { + for (size_t i = 0; i < MO_INOPERATIVE_REQUESTERS_MAX; i++) { + if (unavailableRequesters[i] == requesterId) { + unavailableRequesters[i] = nullptr; + return; + } + } + MO_DBG_ERR("could not find unavailable requester"); +} + +ChangeAvailabilityStatus AvailabilityServiceEvse::changeAvailability(bool operative) { + if (operative) { + setAvailable(this); + } else { + setUnavailable(this); + } + + if (!operative) { + if (isAvailable()) { + return ChangeAvailabilityStatus::Scheduled; + } + + if (evseId == 0) { + for (unsigned int id = 1; id < MO_NUM_EVSEID; id++) { + if (availabilityService.getEvse(id) && availabilityService.getEvse(id)->isAvailable()) { + return ChangeAvailabilityStatus::Scheduled; + } + } + } + } + + return ChangeAvailabilityStatus::Accepted; +} + +void AvailabilityServiceEvse::setFaulted(void *requesterId) { + for (size_t i = 0; i < MO_FAULTED_REQUESTERS_MAX; i++) { + if (!faultedRequesters[i]) { + faultedRequesters[i] = requesterId; + return; + } + } + MO_DBG_ERR("exceeded max. faulted requesters"); +} + +void AvailabilityServiceEvse::resetFaulted(void *requesterId) { + for (size_t i = 0; i < MO_FAULTED_REQUESTERS_MAX; i++) { + if (faultedRequesters[i] == requesterId) { + faultedRequesters[i] = nullptr; + return; + } + } + MO_DBG_ERR("could not find faulted requester"); +} + +bool AvailabilityServiceEvse::isAvailable() { + + auto txService = context.getModel().getTransactionService(); + auto txEvse = txService ? txService->getEvse(evseId) : nullptr; + if (txEvse) { + if (txEvse->getTransaction() && + txEvse->getTransaction()->started && + !txEvse->getTransaction()->stopped) { + return true; + } + } + + if (evseId > 0) { + if (availabilityService.getEvse(0) && !availabilityService.getEvse(0)->isAvailable()) { + return false; + } + } + + for (size_t i = 0; i < MO_INOPERATIVE_REQUESTERS_MAX; i++) { + if (unavailableRequesters[i]) { + return false; + } + } + return true; +} + +bool AvailabilityServiceEvse::isFaulted() { + for (size_t i = 0; i < MO_FAULTED_REQUESTERS_MAX; i++) { + if (faultedRequesters[i]) { + return true; + } + } + return false; +} + +AvailabilityService::AvailabilityService(Context& context, size_t numEvses) : MemoryManaged("v201.Availability.AvailabilityService"), context(context) { + + for (size_t i = 0; i < numEvses && i < MO_NUM_EVSEID; i++) { + evses[i] = new AvailabilityServiceEvse(context, *this, (unsigned int)i); + } + + context.getOperationRegistry().registerOperation("StatusNotification", [&context] () { + return new Ocpp16::StatusNotification(-1, ChargePointStatus_UNDEFINED, Timestamp());}); + context.getOperationRegistry().registerOperation("ChangeAvailability", [this] () { + return new Ocpp201::ChangeAvailability(*this);}); +} + +AvailabilityService::~AvailabilityService() { + for (size_t i = 0; i < MO_NUM_EVSEID && evses[i]; i++) { + delete evses[i]; + } +} + +void AvailabilityService::loop() { + for (size_t i = 0; i < MO_NUM_EVSEID && evses[i]; i++) { + evses[i]->loop(); + } +} + +AvailabilityServiceEvse *AvailabilityService::getEvse(unsigned int evseId) { + if (evseId >= MO_NUM_EVSEID) { + MO_DBG_ERR("invalid arg"); + return nullptr; + } + return evses[evseId]; +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Availability/AvailabilityService.h b/src/MicroOcpp/Model/Availability/AvailabilityService.h new file mode 100644 index 00000000..d8b74d61 --- /dev/null +++ b/src/MicroOcpp/Model/Availability/AvailabilityService.h @@ -0,0 +1,92 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +/* + * Implementation of the UCs G01, G03, G04. + * + * G02 (Heartbeat) is implemented in the HeartbeatService + */ + +#ifndef MO_AVAILABILITYSERVICE_H +#define MO_AVAILABILITYSERVICE_H + +#include + +#if MO_ENABLE_V201 + +#include + +#include +#include +#include +#include + +#ifndef MO_INOPERATIVE_REQUESTERS_MAX +#define MO_INOPERATIVE_REQUESTERS_MAX 3 +#endif + +#ifndef MO_FAULTED_REQUESTERS_MAX +#define MO_FAULTED_REQUESTERS_MAX 3 +#endif + +namespace MicroOcpp { + +class Context; +class AvailabilityService; + +class AvailabilityServiceEvse : public MemoryManaged { +private: + Context& context; + AvailabilityService& availabilityService; + const unsigned int evseId; + + std::function connectorPluggedInput; + std::function occupiedInput; //instead of Available, go into Occupied + + void *unavailableRequesters [MO_INOPERATIVE_REQUESTERS_MAX] = {nullptr}; + void *faultedRequesters [MO_FAULTED_REQUESTERS_MAX] = {nullptr}; + + ChargePointStatus reportedStatus = ChargePointStatus_UNDEFINED; +public: + AvailabilityServiceEvse(Context& context, AvailabilityService& availabilityService, unsigned int evseId); + + void loop(); + + void setConnectorPluggedInput(std::function connectorPluggedInput); + void setOccupiedInput(std::function occupiedInput); + + ChargePointStatus getStatus(); + + void setUnavailable(void *requesterId); + void setAvailable(void *requesterId); + + ChangeAvailabilityStatus changeAvailability(bool operative); + + void setFaulted(void *requesterId); + void resetFaulted(void *requesterId); + + bool isAvailable(); + bool isFaulted(); +}; + +class AvailabilityService : public MemoryManaged { +private: + Context& context; + + AvailabilityServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; + +public: + AvailabilityService(Context& context, size_t numEvses); + ~AvailabilityService(); + + void loop(); + + AvailabilityServiceEvse *getEvse(unsigned int evseId); +}; + +} // namespace MicroOcpp + +#endif // MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Model/Availability/ChangeAvailabilityStatus.h b/src/MicroOcpp/Model/Availability/ChangeAvailabilityStatus.h new file mode 100644 index 00000000..113a3768 --- /dev/null +++ b/src/MicroOcpp/Model/Availability/ChangeAvailabilityStatus.h @@ -0,0 +1,25 @@ +// 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/BootService.cpp b/src/MicroOcpp/Model/Boot/BootService.cpp index 20c07c0e..1aabb200 100644 --- a/src/MicroOcpp/Model/Boot/BootService.cpp +++ b/src/MicroOcpp/Model/Boot/BootService.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -8,18 +8,26 @@ #include #include #include -#include +#include #include #include #include #include -#ifndef MO_BOOTSTATS_LONGTIME_MS -#define MO_BOOTSTATS_LONGTIME_MS 180 * 1000 -#endif - using namespace MicroOcpp; +unsigned int PreBootQueue::getFrontRequestOpNr() { + if (!activatedPostBootCommunication) { + return 0; + } + + return VolatileRequestQueue::getFrontRequestOpNr(); +} + +void PreBootQueue::activatePostBootCommunication() { + activatedPostBootCommunication = true; +} + RegistrationStatus MicroOcpp::deserializeRegistrationStatus(const char *serialized) { if (!strcmp(serialized, "Accepted")) { return RegistrationStatus::Accepted; @@ -33,14 +41,15 @@ RegistrationStatus MicroOcpp::deserializeRegistrationStatus(const char *serializ } } -BootService::BootService(Context& context, std::shared_ptr filesystem) : context(context), filesystem(filesystem) { +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"); - (void)0; } //Register message handler for TriggerMessage operation @@ -58,14 +67,19 @@ void BootService::loop() { if (!executedLongTime && mocpp_tick_ms() - firstExecutionTimestamp >= MO_BOOTSTATS_LONGTIME_MS) { executedLongTime = true; MO_DBG_DEBUG("boot success timer reached"); + + configuration_clean_unused(); + BootStats bootstats; loadBootStats(filesystem, bootstats); bootstats.lastBootSuccess = bootstats.bootNr; storeBootStats(filesystem, bootstats); } + preBootQueue.loop(); + if (!activatedPostBootCommunication && status == RegistrationStatus::Accepted) { - context.activatePostBootCommunication(); + preBootQueue.activatePostBootCommunication(); activatedPostBootCommunication = true; } @@ -88,7 +102,7 @@ void BootService::loop() { */ auto bootNotification = makeRequest(new Ocpp16::BootNotification(context.getModel(), getChargePointCredentials())); bootNotification->setTimeout(interval_s * 1000UL); - context.initiatePreBootOperation(std::move(bootNotification)); + context.getRequestQueue().sendRequestPreBoot(std::move(bootNotification)); lastBootNotification = mocpp_tick_ms(); } @@ -108,16 +122,16 @@ void BootService::setChargePointCredentials(const char *credentials) { } } -std::unique_ptr BootService::getChargePointCredentials() { +std::unique_ptr BootService::getChargePointCredentials() { if (cpCredentials.size() <= 2) { return createEmptyDocument(); } - std::unique_ptr doc; + 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.reset(new DynamicJsonDocument(capacity)); + doc = makeJsonDoc(getMemoryTag(), capacity); err = deserializeJson(*doc, cpCredentials); capacity *= 2; @@ -155,7 +169,7 @@ bool BootService::loadBootStats(std::shared_ptr filesystem, B bool success = true; - auto json = FilesystemUtils::loadJson(filesystem, MO_FILENAME_PREFIX "bootstats.jsn"); + 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()) { @@ -170,6 +184,14 @@ bool BootService::loadBootStats(std::shared_ptr filesystem, B } 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; } @@ -186,15 +208,58 @@ bool BootService::loadBootStats(std::shared_ptr filesystem, B } } -bool BootService::storeBootStats(std::shared_ptr filesystem, BootStats bstats) { +bool BootService::storeBootStats(std::shared_ptr filesystem, BootStats& bstats) { if (!filesystem) { return false; } - DynamicJsonDocument json {JSON_OBJECT_SIZE(2)}; + auto json = initJsonDoc("v16.Boot.BootService", JSON_OBJECT_SIZE(3)); json["bootNr"] = bstats.bootNr; json["lastSuccess"] = bstats.lastBootSuccess; + json["MicroOcppVersion"] = (const char*)bstats.microOcppVersion; return FilesystemUtils::storeJson(filesystem, MO_FILENAME_PREFIX "bootstats.jsn", json); } + +bool BootService::recover(std::shared_ptr filesystem, BootStats& bstats) { + if (!filesystem) { + return false; + } + + bool success = FilesystemUtils::remove_if(filesystem, [] (const char *fname) -> bool { + return !strncmp(fname, "sd", strlen("sd")) || + !strncmp(fname, "tx", strlen("tx")) || + !strncmp(fname, "sc-", strlen("sc-")) || + !strncmp(fname, "reservation", strlen("reservation")) || + !strncmp(fname, "client-state", strlen("client-state")); + }); + MO_DBG_ERR("clear local state files (recovery): %s", success ? "success" : "not completed"); + + return success; +} + +bool BootService::migrate(std::shared_ptr filesystem, BootStats& bstats) { + if (!filesystem) { + return false; + } + + bool success = true; + + if (strcmp(bstats.microOcppVersion, MO_VERSION)) { + MO_DBG_INFO("migrate persistent storage to MO v" MO_VERSION); + success = FilesystemUtils::remove_if(filesystem, [] (const char *fname) -> bool { + return !strncmp(fname, "sd", strlen("sd")) || + !strncmp(fname, "tx", strlen("tx")) || + !strncmp(fname, "op", strlen("op")) || + !strncmp(fname, "sc-", strlen("sc-")) || + !strcmp(fname, "client-state.cnf") || + !strcmp(fname, "arduino-ocpp.cnf") || + !strcmp(fname, "ocpp-creds.jsn"); + }); + + snprintf(bstats.microOcppVersion, sizeof(bstats.microOcppVersion), "%s", MO_VERSION); + MO_DBG_DEBUG("clear local state files (migration): %s", success ? "success" : "not completed"); + } + return success; +} diff --git a/src/MicroOcpp/Model/Boot/BootService.h b/src/MicroOcpp/Model/Boot/BootService.h index 67776eed..6091dc2c 100644 --- a/src/MicroOcpp/Model/Boot/BootService.h +++ b/src/MicroOcpp/Model/Boot/BootService.h @@ -1,18 +1,26 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef BOOTSERVICE_H -#define BOOTSERVICE_H +#ifndef MO_BOOTSERVICE_H +#define MO_BOOTSERVICE_H #include #include +#include +#include #include #define MO_BOOT_INTERVAL_DEFAULT 60 +#ifndef MO_BOOTSTATS_LONGTIME_MS +#define MO_BOOTSTATS_LONGTIME_MS 180 * 1000 +#endif + namespace MicroOcpp { +#define MO_BOOTSTATS_VERSION_SIZE 10 + struct BootStats { uint16_t bootNr = 0; uint16_t lastBootSuccess = 0; @@ -20,6 +28,8 @@ struct BootStats { uint16_t getBootFailureCount() { return bootNr - lastBootSuccess; } + + char microOcppVersion [MO_BOOTSTATS_VERSION_SIZE] = {'\0'}; }; enum class RegistrationStatus { @@ -31,19 +41,30 @@ enum class RegistrationStatus { RegistrationStatus deserializeRegistrationStatus(const char *serialized); +class PreBootQueue : public VolatileRequestQueue { +private: + bool activatedPostBootCommunication = false; +public: + unsigned int getFrontRequestOpNr() override; //override FrontRequestOpNr behavior: in PreBoot mode, always return 0 to avoid other RequestEmitters from sending msgs + + void activatePostBootCommunication(); //end PreBoot mode, now send Requests normally +}; + class Context; -class BootService { +class BootService : public MemoryManaged { private: Context& context; std::shared_ptr filesystem; + PreBootQueue preBootQueue; + unsigned long interval_s = MO_BOOT_INTERVAL_DEFAULT; unsigned long lastBootNotification = -1UL / 2; RegistrationStatus status = RegistrationStatus::Pending; - std::string cpCredentials; + String cpCredentials; std::shared_ptr preBootTransactionsBool; @@ -61,13 +82,17 @@ class BootService { void setChargePointCredentials(JsonObject credentials); void setChargePointCredentials(const char *credentials); //credentials: serialized BootNotification payload - std::unique_ptr getChargePointCredentials(); + std::unique_ptr getChargePointCredentials(); void notifyRegistrationStatus(RegistrationStatus status); void setRetryInterval(unsigned long interval); - static bool loadBootStats(std::shared_ptr filesystem, BootStats& out); - static bool storeBootStats(std::shared_ptr filesystem, BootStats bstats); + static bool loadBootStats(std::shared_ptr filesystem, BootStats& bstats); + static bool storeBootStats(std::shared_ptr filesystem, BootStats& bstats); + + static bool recover(std::shared_ptr filesystem, BootStats& bstats); //delete all persistent files which could lead to a crash + + static bool migrate(std::shared_ptr filesystem, BootStats& bstats); //migrate persistent storage if running on a new MO version }; } diff --git a/src/MicroOcpp/Model/Certificates/Certificate.cpp b/src/MicroOcpp/Model/Certificates/Certificate.cpp new file mode 100644 index 00000000..1de32094 --- /dev/null +++ b/src/MicroOcpp/Model/Certificates/Certificate.cpp @@ -0,0 +1,167 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_CERT_MGMT + +#include +#include + +bool ocpp_cert_equals(const ocpp_cert_hash *h1, const ocpp_cert_hash *h2) { + return h1->hashAlgorithm == h2->hashAlgorithm && + h1->serialNumberLen == h2->serialNumberLen && !memcmp(h1->serialNumber, h2->serialNumber, h1->serialNumberLen) && + !memcmp(h1->issuerNameHash, h2->issuerNameHash, HashAlgorithmSize(h1->hashAlgorithm)) && + !memcmp(h1->issuerKeyHash, h2->issuerKeyHash, HashAlgorithmSize(h1->hashAlgorithm)); +} + +int ocpp_cert_bytes_to_hex(char *dst, size_t dst_size, const unsigned char *src, size_t src_len) { + if (!dst || !dst_size || !src) { + return -1; + } + + dst[0] = '\0'; + + size_t hexLen = 2 * src_len; // hex-encoding needs two characters per byte + + if (dst_size < hexLen + 1) { // buf will hold hex-encoding + terminating null + return -1; + } + + for (size_t i = 0; i < src_len; i++) { + snprintf(dst, 3, "%02X", src[i]); + dst += 2; + } + + return (int)hexLen; +} + +int ocpp_cert_print_issuerNameHash(const ocpp_cert_hash *src, char *buf, size_t size) { + return ocpp_cert_bytes_to_hex(buf, size, src->issuerNameHash, HashAlgorithmSize(src->hashAlgorithm)); +} + +int ocpp_cert_print_issuerKeyHash(const ocpp_cert_hash *src, char *buf, size_t size) { + return ocpp_cert_bytes_to_hex(buf, size, src->issuerKeyHash, HashAlgorithmSize(src->hashAlgorithm)); +} + +int ocpp_cert_print_serialNumber(const ocpp_cert_hash *src, char *buf, size_t size) { + + if (!buf || !size) { + return -1; + } + + buf[0] = '\0'; + + if (!src->serialNumberLen) { + return 0; + } + + int hexLen = snprintf(buf, size, "%X", src->serialNumber[0]); + if (hexLen < 0 || (size_t)hexLen >= size) { + return -1; + } + + if (src->serialNumberLen > 1) { + auto ret = ocpp_cert_bytes_to_hex(buf + (size_t)hexLen, size - (size_t)hexLen, src->serialNumber + 1, src->serialNumberLen - 1); + if (ret < 0) { + return -1; + } + hexLen += ret; + } + + return hexLen; +} + +int ocpp_cert_hex_to_bytes(unsigned char *dst, size_t dst_size, const char *hex_src) { + if (!dst || !dst_size || !hex_src) { + return -1; + } + + dst[0] = '\0'; + + size_t hex_len = strlen(hex_src); + + size_t write_len = (hex_len + 1) / 2; + + if (dst_size < write_len) { + return -1; + } + + for (size_t i = 0; i < write_len; i++) { + char octet [2]; + + if (i == 0 && hex_len % 2) { + octet[0] = '0'; + octet[1] = hex_src[2*i]; + } else { + octet[0] = hex_src[2*i]; + octet[1] = hex_src[2*i + 1]; + } + + unsigned char val = 0; + + for (size_t j = 0; j < 2; j++) { + char c = octet[j]; + if (c >= '0' && c <= '9') { + val += c - '0'; + } else if (c >= 'A' && c <= 'F') { + val += (c - 'A') + 0xA; + } else if (c >= 'a' && c <= 'f') { + val += (c - 'a') + 0xA; + } else { + return -1; + } + + if (j == 0) { + val *= 0x10; + } + } + + dst[i] = val; + } + + return (int)write_len; +} + +int ocpp_cert_set_issuerNameHash(ocpp_cert_hash *dst, const char *hex_src, HashAlgorithmType hash_algorithm) { + auto ret = ocpp_cert_hex_to_bytes(dst->issuerNameHash, sizeof(dst->issuerNameHash), hex_src); + + if (ret < 0) { + return ret; + } + + if (ret != HashAlgorithmSize(hash_algorithm)) { + return -1; + } + + return ret; +} + +int ocpp_cert_set_issuerKeyHash(ocpp_cert_hash *dst, const char *hex_src, HashAlgorithmType hash_algorithm) { + auto ret = ocpp_cert_hex_to_bytes(dst->issuerKeyHash, sizeof(dst->issuerNameHash), hex_src); + + if (ret < 0) { + return ret; + } + + if (ret != HashAlgorithmSize(hash_algorithm)) { + return -1; + } + + return ret; +} + +int ocpp_cert_set_serialNumber(ocpp_cert_hash *dst, const char *hex_src) { + auto ret = ocpp_cert_hex_to_bytes(dst->serialNumber, sizeof(dst->serialNumber), hex_src); + + if (ret < 0) { + return ret; + } + + dst->serialNumberLen = (size_t)ret; + + return ret; +} + +#endif //MO_ENABLE_CERT_MGMT diff --git a/src/MicroOcpp/Model/Certificates/Certificate.h b/src/MicroOcpp/Model/Certificates/Certificate.h new file mode 100644 index 00000000..33574351 --- /dev/null +++ b/src/MicroOcpp/Model/Certificates/Certificate.h @@ -0,0 +1,164 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CERTIFICATE_H +#define MO_CERTIFICATE_H + +#include + +#if MO_ENABLE_CERT_MGMT + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define MO_MAX_CERT_SIZE 5500 //limit of field `certificate` in InstallCertificateRequest, not counting terminating '\0'. See OCPP 2.0.1 part 2 Data Type 1.30.1 + +/* + * See OCPP 2.0.1 part 2 Data Type 3.36 + */ +typedef enum GetCertificateIdType { + GetCertificateIdType_V2GRootCertificate, + GetCertificateIdType_MORootCertificate, + GetCertificateIdType_CSMSRootCertificate, + GetCertificateIdType_V2GCertificateChain, + GetCertificateIdType_ManufacturerRootCertificate +} GetCertificateIdType; + +/* + * See OCPP 2.0.1 part 2 Data Type 3.40 + */ +typedef enum GetInstalledCertificateStatus { + GetInstalledCertificateStatus_Accepted, + GetInstalledCertificateStatus_NotFound +} GetInstalledCertificateStatus; + +/* + * See OCPP 2.0.1 part 2 Data Type 3.45 + */ +typedef enum InstallCertificateType { + InstallCertificateType_V2GRootCertificate, + InstallCertificateType_MORootCertificate, + InstallCertificateType_CSMSRootCertificate, + InstallCertificateType_ManufacturerRootCertificate +} InstallCertificateType; + +/* + * See OCPP 2.0.1 part 2 Data Type 3.28 + */ +typedef enum InstallCertificateStatus { + InstallCertificateStatus_Accepted, + InstallCertificateStatus_Rejected, + InstallCertificateStatus_Failed +} InstallCertificateStatus; + +/* + * See OCPP 2.0.1 part 2 Data Type 3.28 + */ +typedef enum DeleteCertificateStatus { + DeleteCertificateStatus_Accepted, + DeleteCertificateStatus_Failed, + DeleteCertificateStatus_NotFound +} DeleteCertificateStatus; + +/* + * See OCPP 2.0.1 part 2 Data Type 3.42 + */ +typedef enum HashAlgorithmType { + HashAlgorithmType_SHA256, + HashAlgorithmType_SHA384, + HashAlgorithmType_SHA512 +} HashAlgorithmType; + +// Convert HashAlgorithmType into string +#define HashAlgorithmLabel(alg) (alg == HashAlgorithmType_SHA256 ? "SHA256" : \ + alg == HashAlgorithmType_SHA384 ? "SHA384" : \ + alg == HashAlgorithmType_SHA512 ? "SHA512" : "_Undefined") + +// Convert HashAlgorithmType into hash size in bytes (e.g. SHA256 -> 32) +#define HashAlgorithmSize(alg) (alg == HashAlgorithmType_SHA256 ? 32 : \ + alg == HashAlgorithmType_SHA384 ? 48 : \ + alg == HashAlgorithmType_SHA512 ? 64 : 0) + +typedef struct ocpp_cert_hash { + enum HashAlgorithmType hashAlgorithm; + + unsigned char issuerNameHash [64]; // hash buf can hold 64 bytes (SHA512). Actual hash size is determined by hash algorithm + unsigned char issuerKeyHash [64]; + unsigned char serialNumber [20]; + size_t serialNumberLen; // length of serial number in bytes +} ocpp_cert_hash; + +bool ocpp_cert_equals(const ocpp_cert_hash *h1, const ocpp_cert_hash *h2); + +// Max size of hex-encoded cert hash components +#define MO_CERT_HASH_ISSUER_NAME_KEY_SIZE (128 + 1) // hex-encoding needs two characters per byte + terminating null-byte +#define MO_CERT_HASH_SERIAL_NUMBER_SIZE (40 + 1) + +/* + * Print the issuerNameHash of ocpp_cert_hash as hex-encoded string (e.g. "0123AB") into buf. Bufsize MO_CERT_HASH_ISSUER_NAME_KEY_SIZE is always enough + * + * Returns the length not counting the terminating 0 on success, -1 on failure + */ +int ocpp_cert_print_issuerNameHash(const ocpp_cert_hash *src, char *buf, size_t size); + +/* + * Print the issuerKeyHash of ocpp_cert_hash as hex-encoded string (e.g. "0123AB") into buf. Bufsize MO_CERT_HASH_ISSUER_NAME_KEY_SIZE is always enough + * + * Returns the length not counting the terminating 0 on success, -1 on failure + */ +int ocpp_cert_print_issuerKeyHash(const ocpp_cert_hash *src, char *buf, size_t size); + +/* + * Print the serialNumber of ocpp_cert_hash as hex-encoded string without leading 0s (e.g. "123AB") into buf. Bufsize MO_CERT_HASH_SERIAL_NUMBER_SIZE is always enough + * + * Returns the length not counting the terminating 0 on success, -1 on failure + */ +int ocpp_cert_print_serialNumber(const ocpp_cert_hash *src, char *buf, size_t size); + +int ocpp_cert_set_issuerNameHash(ocpp_cert_hash *dst, const char *hex_src, HashAlgorithmType hash_algorithm); + +int ocpp_cert_set_issuerKeyHash(ocpp_cert_hash *dst, const char *hex_src, HashAlgorithmType hash_algorithm); + +int ocpp_cert_set_serialNumber(ocpp_cert_hash *dst, const char *hex_src); + +#ifdef __cplusplus +} //extern "C" + +#include + +namespace MicroOcpp { + +using CertificateHash = ocpp_cert_hash; + +/* + * See OCPP 2.0.1 part 2 Data Type 2.5 + */ +struct CertificateChainHash : public MemoryManaged { + GetCertificateIdType certificateType; + CertificateHash certificateHashData; + Vector childCertificateHashData; + + CertificateChainHash() : MemoryManaged("v2.0.1.Certificates.CertificateChainHash"), childCertificateHashData(makeVector(getMemoryTag())) { } +}; + +/* + * Interface which allows MicroOcpp to interact with the certificates managed by the local TLS library + */ +class CertificateStore { +public: + virtual ~CertificateStore() = default; + + virtual GetInstalledCertificateStatus getCertificateIds(const Vector& certificateType, Vector& out) = 0; + virtual DeleteCertificateStatus deleteCertificate(const CertificateHash& hash) = 0; + virtual InstallCertificateStatus installCertificate(InstallCertificateType certificateType, const char *certificate) = 0; +}; + +} //namespace MicroOcpp + +#endif //__cplusplus +#endif //MO_ENABLE_CERT_MGMT +#endif diff --git a/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.cpp b/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.cpp new file mode 100644 index 00000000..73314158 --- /dev/null +++ b/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.cpp @@ -0,0 +1,414 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS + +#include + +#include +#include +#include +#include + +#include + +bool ocpp_get_cert_hash(mbedtls_x509_crt& cacert, HashAlgorithmType hashAlg, ocpp_cert_hash *out) { + + if (cacert.next) { + MO_DBG_ERR("only sole root certs supported"); + return false; + } + + out->hashAlgorithm = hashAlg; + + mbedtls_md_type_t hash_alg_mbed; + + switch (hashAlg) { + case HashAlgorithmType_SHA256: + hash_alg_mbed = MBEDTLS_MD_SHA256; + break; + case HashAlgorithmType_SHA384: + hash_alg_mbed = MBEDTLS_MD_SHA384; + break; + case HashAlgorithmType_SHA512: + hash_alg_mbed = MBEDTLS_MD_SHA512; + break; + default: + MO_DBG_ERR("internal error"); + return false; + } + + const mbedtls_md_info_t *md_info; + + md_info = mbedtls_md_info_from_type(hash_alg_mbed); + if (!md_info) { + MO_DBG_ERR("hash algorithmus not supported"); + return false; + } + + size_t hash_size = mbedtls_md_get_size(md_info); + if (hash_size > sizeof(out->issuerNameHash)) { + MO_DBG_ERR("internal error"); + return false; + } + + if (!cacert.issuer_raw.p) { + MO_DBG_ERR("missing issuer name"); + return false; + } + + int ret; + + if ((ret = mbedtls_md(md_info, cacert.issuer_raw.p, cacert.issuer_raw.len, out->issuerNameHash))) { + MO_DBG_ERR("mbedtls_md: %i", ret); + return false; + } + + // copy public key into pk_buf to create issuerKeyHash + size_t pk_size = cacert.pk_raw.len; + unsigned char *pk_buf = static_cast(MO_MALLOC("v201.Certificates.CertificateStoreMbedTLS", pk_size)); + if (!pk_buf) { + MO_DBG_ERR("OOM (alloc size %zu)", pk_size); + return false; + } + int pk_len = 0; + unsigned char *pk_p = pk_buf + pk_size; + + bool pk_err = false; + + if ((pk_len = mbedtls_pk_write_pubkey(&pk_p, pk_buf, &cacert.pk)) <= 0) { + pk_err = true; + char err [100]; + mbedtls_strerror(ret, err, 100); + MO_DBG_ERR("mbedtls_pk_write_pubkey_pem: %i -- %s", pk_len, err); + // return after pk_buf has been freed + } + + if (!pk_err) { + if ((ret = mbedtls_md(md_info, pk_p, pk_len, out->issuerKeyHash))) { + pk_err = true; + MO_DBG_ERR("mbedtls_md: %i", ret); + } + } + + MO_FREE(pk_buf); + if (pk_err) { + return false; + } + + size_t serial_begin = 0; //trunicate leftmost 0x00 bytes + for (; serial_begin < cacert.serial.len - 1; serial_begin++) { //keep at least 1 byte, even if 0x00 + if (cacert.serial.p[serial_begin] != 0) { + break; + } + } + + out->serialNumberLen = std::min(cacert.serial.len - serial_begin, sizeof(out->serialNumber)); + memcpy(out->serialNumber, cacert.serial.p + serial_begin, out->serialNumberLen); + + return true; +} + +bool ocpp_get_cert_hash(const unsigned char *buf, size_t len, HashAlgorithmType hashAlg, ocpp_cert_hash *out) { + + mbedtls_x509_crt cacert; + mbedtls_x509_crt_init(&cacert); + + bool success = false; + int ret; + + if((ret = mbedtls_x509_crt_parse(&cacert, buf, len + 1)) >= 0) { + success = ocpp_get_cert_hash(cacert, hashAlg, out); + } else { + char err [100]; + mbedtls_strerror(ret, err, 100); + MO_DBG_ERR("mbedtls_x509_crt_parse: %i -- %s", ret, err); + } + + mbedtls_x509_crt_free(&cacert); + return success; +} + +namespace MicroOcpp { + +class CertificateStoreMbedTLS : public CertificateStore, public MemoryManaged { +private: + std::shared_ptr filesystem; + + bool getCertHash(const char *fn, HashAlgorithmType hashAlg, CertificateHash& out) { + size_t fsize; + if (filesystem->stat(fn, &fsize) != 0) { + MO_DBG_ERR("certificate does not exist: %s", fn); + return false; + } + + if (fsize >= MO_MAX_CERT_SIZE) { + MO_DBG_ERR("cert file exceeds limit: %s, %zuB", fn, fsize); + return false; + } + + auto file = filesystem->open(fn, "r"); + if (!file) { + MO_DBG_ERR("could not open file: %s", fn); + return false; + } + + unsigned char *buf = static_cast(MO_MALLOC(getMemoryTag(), fsize + 1)); + if (!buf) { + MO_DBG_ERR("OOM"); + return false; + } + + bool success = true; + + size_t ret; + if ((ret = file->read((char*) buf, fsize)) != fsize) { + MO_DBG_ERR("read error: %zu (expect %zu)", ret, fsize); + success = false; + } + + buf[fsize] = '\0'; + + if (success) { + success &= ocpp_get_cert_hash(buf, fsize, hashAlg, &out); + } + + if (!success) { + MO_DBG_ERR("could not read cert: %s", fn); + } + + MO_FREE(buf); + return success; + } +public: + CertificateStoreMbedTLS(std::shared_ptr filesystem) + : MemoryManaged("v201.Certificates.CertificateStoreMbedTLS"), filesystem(filesystem) { + + } + + GetInstalledCertificateStatus getCertificateIds(const Vector& certificateType, Vector& out) override { + out.clear(); + + for (auto certType : certificateType) { + const char *certTypeFnStr = nullptr; + switch (certType) { + case GetCertificateIdType_CSMSRootCertificate: + certTypeFnStr = MO_CERT_FN_CSMS_ROOT; + break; + case GetCertificateIdType_ManufacturerRootCertificate: + certTypeFnStr = MO_CERT_FN_MANUFACTURER_ROOT; + break; + default: + MO_DBG_ERR("only CSMS / Manufacturer root supported"); + break; + } + + if (!certTypeFnStr) { + continue; + } + + for (size_t i = 0; i < MO_CERT_STORE_SIZE; i++) { + char fn [MO_MAX_PATH_SIZE]; + if (!printCertFn(certTypeFnStr, i, fn, MO_MAX_PATH_SIZE)) { + MO_DBG_ERR("internal error"); + out.clear(); + break; + } + + size_t msize; + if (filesystem->stat(fn, &msize) != 0) { + continue; //no cert installed at this slot + } + + out.emplace_back(); + CertificateChainHash& rootCert = out.back(); + + rootCert.certificateType = certType; + + if (!getCertHash(fn, HashAlgorithmType_SHA256, rootCert.certificateHashData)) { + MO_DBG_ERR("could not create hash: %s", fn); + out.pop_back(); + continue; + } + } + } + + return out.empty() ? + GetInstalledCertificateStatus_NotFound : + GetInstalledCertificateStatus_Accepted; + } + + DeleteCertificateStatus deleteCertificate(const CertificateHash& hash) override { + bool err = false; + + //enumerate all certs possibly installed by this CertStore implementation + for (const char *certTypeFnStr : {MO_CERT_FN_CSMS_ROOT, MO_CERT_FN_MANUFACTURER_ROOT}) { + for (size_t i = 0; i < MO_CERT_STORE_SIZE; i++) { + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; //cert fn on flash storage + + if (!printCertFn(certTypeFnStr, i, fn, MO_MAX_PATH_SIZE)) { + MO_DBG_ERR("internal error"); + return DeleteCertificateStatus_Failed; + } + + size_t msize; + if (filesystem->stat(fn, &msize) != 0) { + continue; //no cert installed at this slot + } + + CertificateHash probe; + if (!getCertHash(fn, hash.hashAlgorithm, probe)) { + MO_DBG_ERR("could not create hash: %s", fn); + err = true; + continue; + } + + if (ocpp_cert_equals(&probe, &hash)) { + //found, delete + + bool success = filesystem->remove(fn); + return success ? + DeleteCertificateStatus_Accepted : + DeleteCertificateStatus_Failed; + } + } + } + + return err ? + DeleteCertificateStatus_Failed : + DeleteCertificateStatus_NotFound; + } + + InstallCertificateStatus installCertificate(InstallCertificateType certificateType, const char *certificate) override { + const char *certTypeFnStr; + GetCertificateIdType certTypeGetType; + switch (certificateType) { + case InstallCertificateType_CSMSRootCertificate: + certTypeFnStr = MO_CERT_FN_CSMS_ROOT; + certTypeGetType = GetCertificateIdType_CSMSRootCertificate; + break; + case InstallCertificateType_ManufacturerRootCertificate: + certTypeFnStr = MO_CERT_FN_MANUFACTURER_ROOT; + certTypeGetType = GetCertificateIdType_ManufacturerRootCertificate; + break; + default: + MO_DBG_ERR("only CSMS / Manufacturer root supported"); + return InstallCertificateStatus_Failed; + } + + //check if this implementation is able to parse incoming cert + CertificateHash certId; + if (!ocpp_get_cert_hash((const unsigned char*)certificate, strlen(certificate), HashAlgorithmType_SHA256, &certId)) { + MO_DBG_ERR("unable to parse cert"); + return InstallCertificateStatus_Rejected; + } + +#if MO_DBG_LEVEL >= MO_DL_DEBUG + { + MO_DBG_DEBUG("Cert ID:"); + MO_DBG_DEBUG("hashAlgorithm: %s", HashAlgorithmLabel(certId.hashAlgorithm)); + char buf [MO_CERT_HASH_ISSUER_NAME_KEY_SIZE]; + + ocpp_cert_print_issuerNameHash(&certId, buf, sizeof(buf)); + MO_DBG_DEBUG("issuerNameHash: %s", buf); + + ocpp_cert_print_issuerKeyHash(&certId, buf, sizeof(buf)); + MO_DBG_DEBUG("issuerKeyHash: %s", buf); + + ocpp_cert_print_serialNumber(&certId, buf, sizeof(buf)); + MO_DBG_DEBUG("serialNumber: %s", buf); + } +#endif // MO_DBG_LEVEL >= MO_DL_DEBUG + + //check if cert is already stored on flash + auto installedCerts = makeVector(getMemoryTag()); + auto ret = getCertificateIds({certTypeGetType}, installedCerts); + if (ret == GetInstalledCertificateStatus_Accepted) { + for (auto &installedCert : installedCerts) { + if (ocpp_cert_equals(&installedCert.certificateHashData, &certId)) { + MO_DBG_INFO("certificate already installed"); + return InstallCertificateStatus_Accepted; + } + for (auto& installedChild : installedCert.childCertificateHashData) { + if (ocpp_cert_equals(&installedChild, &certId)) { + MO_DBG_INFO("certificate already installed"); + return InstallCertificateStatus_Accepted; + } + } + } + } + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; //cert fn on flash storage + + //check for free cert slot + for (size_t i = 0; i < MO_CERT_STORE_SIZE; i++) { + if (!printCertFn(certTypeFnStr, i, fn, MO_MAX_PATH_SIZE)) { + MO_DBG_ERR("invalid cert fn"); + return InstallCertificateStatus_Failed; + } + + size_t msize; + if (filesystem->stat(fn, &msize) != 0) { + //found free slot; fn contains result + break; + } else { + //this slot is already occupied; invalidate fn and try next + fn[0] = '\0'; + } + } + + if (fn[0] == '\0') { + MO_DBG_ERR("exceed maximum number of certs; must delete before"); + return InstallCertificateStatus_Rejected; + } + + auto file = filesystem->open(fn, "w"); + if (!file) { + MO_DBG_ERR("could not open file"); + return InstallCertificateStatus_Failed; + } + + size_t cert_len = strlen(certificate); + auto written = file->write(certificate, cert_len); + if (written < cert_len) { + MO_DBG_ERR("file write error"); + file.reset(); + filesystem->remove(fn); + return InstallCertificateStatus_Failed; + } + + MO_DBG_INFO("installed certificate: %s", fn); + return InstallCertificateStatus_Accepted; + } +}; + +std::unique_ptr makeCertificateStoreMbedTLS(std::shared_ptr filesystem) { + if (!filesystem) { + MO_DBG_WARN("default Certificate Store requires FS"); + return nullptr; + } + return std::unique_ptr(new CertificateStoreMbedTLS(filesystem)); +} + +bool printCertFn(const char *certType, size_t index, char *buf, size_t bufsize) { + if (!certType || !*certType || index >= MO_CERT_STORE_SIZE || !buf) { + MO_DBG_ERR("invalid args"); + return false; + } + + auto ret = snprintf(buf, bufsize, MO_FILENAME_PREFIX MO_CERT_FN_PREFIX "%s" "-%zu" MO_CERT_FN_SUFFIX, + certType, index); + if (ret < 0 || ret >= (int)bufsize) { + MO_DBG_ERR("fn error: %i", ret); + return false; + } + return true; +} + +} //namespace MicroOcpp + +#endif //MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS diff --git a/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.h b/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.h new file mode 100644 index 00000000..e60a2734 --- /dev/null +++ b/src/MicroOcpp/Model/Certificates/CertificateMbedTLS.h @@ -0,0 +1,70 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CERTIFICATE_MBEDTLS_H +#define MO_CERTIFICATE_MBEDTLS_H + +/* + * Built-in implementation of the Certificate interface for MbedTLS + */ + +#include +#include + +#ifndef MO_ENABLE_CERT_STORE_MBEDTLS +#define MO_ENABLE_CERT_STORE_MBEDTLS MO_ENABLE_MBEDTLS +#endif + +#if MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS + +/* + * Provide certificate interpreter to facilitate cert store in C. A full implementation is only available for C++ + */ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +bool ocpp_get_cert_hash(const unsigned char *cert, size_t len, enum HashAlgorithmType hashAlg, ocpp_cert_hash *out); + +#ifdef __cplusplus +} //extern "C" + +#include + +#include + +#ifndef MO_CERT_FN_PREFIX +#define MO_CERT_FN_PREFIX "cert-" +#endif + +#ifndef MO_CERT_FN_SUFFIX +#define MO_CERT_FN_SUFFIX ".pem" +#endif + +#ifndef MO_CERT_FN_CSMS_ROOT +#define MO_CERT_FN_CSMS_ROOT "csms" +#endif + +#ifndef MO_CERT_FN_MANUFACTURER_ROOT +#define MO_CERT_FN_MANUFACTURER_ROOT "mfact" +#endif + +#ifndef MO_CERT_STORE_SIZE +#define MO_CERT_STORE_SIZE 3 //max number of certs per certificate type (e.g. CSMS root CA, Manufacturer root CA) +#endif + +namespace MicroOcpp { + +std::unique_ptr makeCertificateStoreMbedTLS(std::shared_ptr filesystem); + +bool printCertFn(const char *certType, size_t index, char *buf, size_t bufsize); + +} //namespace MicroOcpp + +#endif //def __cplusplus +#endif //MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS + +#endif diff --git a/src/MicroOcpp/Model/Certificates/CertificateService.cpp b/src/MicroOcpp/Model/Certificates/CertificateService.cpp new file mode 100644 index 00000000..22e5ab6d --- /dev/null +++ b/src/MicroOcpp/Model/Certificates/CertificateService.cpp @@ -0,0 +1,35 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_CERT_MGMT + +#include +#include +#include +#include + +using namespace MicroOcpp; + +CertificateService::CertificateService(Context& context) + : MemoryManaged("v201.Certificates.CertificateService"), context(context) { + + context.getOperationRegistry().registerOperation("DeleteCertificate", [this] () { + return new Ocpp201::DeleteCertificate(*this);}); + context.getOperationRegistry().registerOperation("GetInstalledCertificateIds", [this] () { + return new Ocpp201::GetInstalledCertificateIds(*this);}); + context.getOperationRegistry().registerOperation("InstallCertificate", [this] () { + return new Ocpp201::InstallCertificate(*this);}); +} + +void CertificateService::setCertificateStore(std::unique_ptr certStore) { + this->certStore = std::move(certStore); +} + +CertificateStore *CertificateService::getCertificateStore() { + return certStore.get(); +} + +#endif //MO_ENABLE_CERT_MGMT diff --git a/src/MicroOcpp/Model/Certificates/CertificateService.h b/src/MicroOcpp/Model/Certificates/CertificateService.h new file mode 100644 index 00000000..31d10048 --- /dev/null +++ b/src/MicroOcpp/Model/Certificates/CertificateService.h @@ -0,0 +1,45 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +/* + * Functional Block M: ISO 15118 Certificate Management + * + * Implementation of UC: + * - M03 + * - M04 + * - M05 + */ + +#ifndef MO_CERTIFICATESERVICE_H +#define MO_CERTIFICATESERVICE_H + +#include + +#if MO_ENABLE_CERT_MGMT + +#include +#include + +#include +#include + +namespace MicroOcpp { + +class Context; + +class CertificateService : public MemoryManaged { +private: + Context& context; + std::unique_ptr certStore; +public: + CertificateService(Context& context); + + void setCertificateStore(std::unique_ptr certStore); + CertificateStore *getCertificateStore(); +}; + +} + +#endif //MO_ENABLE_CERT_MGMT +#endif diff --git a/src/MicroOcpp/Model/Certificates/Certificate_c.cpp b/src/MicroOcpp/Model/Certificates/Certificate_c.cpp new file mode 100644 index 00000000..d316140c --- /dev/null +++ b/src/MicroOcpp/Model/Certificates/Certificate_c.cpp @@ -0,0 +1,77 @@ +// 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 new file mode 100644 index 00000000..3b0bc858 --- /dev/null +++ b/src/MicroOcpp/Model/Certificates/Certificate_c.h @@ -0,0 +1,54 @@ +// 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/ChargePointErrorData.h b/src/MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h index d78981b1..6c7b04f2 100644 --- a/src/MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h +++ b/src/MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h @@ -1,15 +1,18 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// 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 diff --git a/src/MicroOcpp/Model/ConnectorBase/ChargePointStatus.h b/src/MicroOcpp/Model/ConnectorBase/ChargePointStatus.h index 308e04b8..019fce27 100644 --- a/src/MicroOcpp/Model/ConnectorBase/ChargePointStatus.h +++ b/src/MicroOcpp/Model/ConnectorBase/ChargePointStatus.h @@ -1,25 +1,36 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef OCPP_EVSE_STATE -#define OCPP_EVSE_STATE - -namespace MicroOcpp { - -enum class ChargePointStatus { - Available, - Preparing, - Charging, - SuspendedEVSE, - SuspendedEV, - Finishing, - Reserved, - Unavailable, - Faulted, - NOT_SET //internal value for "undefined" -}; - -} //end namespace MicroOcpp +#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 index 4c0274ab..11142dc9 100644 --- a/src/MicroOcpp/Model/ConnectorBase/Connector.cpp +++ b/src/MicroOcpp/Model/ConnectorBase/Connector.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -21,27 +21,39 @@ #include #include #include +#include +#include -#include +#include +#include #ifndef MO_TX_CLEAN_ABORTED #define MO_TX_CLEAN_ABORTED 1 #endif using namespace MicroOcpp; -using namespace MicroOcpp::Ocpp16; -Connector::Connector(Context& context, int connectorId) - : context(context), model(context.getModel()), connectorId{connectorId} { +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); - declareConfiguration("UnlockConnectorOnEVSideDisconnect", true, CONFIGURATION_VOLATILE, true); localPreAuthorizeBool = declareConfiguration("LocalPreAuthorize", false); localAuthorizeOfflineBool = declareConfiguration("LocalAuthorizeOffline", true); allowOfflineTxForUnknownIdBool = MicroOcpp::declareConfiguration("AllowOfflineTxForUnknownId", false); @@ -51,20 +63,71 @@ Connector::Connector(Context& context, int connectorId) //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()) { - transaction = model.getTransactionStore()->getLatestTransaction(connectorId); + 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"); - (void)0; } } @@ -75,59 +138,84 @@ Connector::~Connector() { } 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()) { - return ChargePointStatus::Faulted; + res = ChargePointStatus_Faulted; } else if (!isOperative()) { - return ChargePointStatus::Unavailable; + res = ChargePointStatus_Unavailable; } else { - return ChargePointStatus::Available; + res = ChargePointStatus_Available; } + return res; } if (isFaulted()) { - return ChargePointStatus::Faulted; + res = ChargePointStatus_Faulted; } else if (!isOperative()) { - return ChargePointStatus::Unavailable; + res = ChargePointStatus_Unavailable; } else if (transaction && transaction->isRunning()) { //Transaction is currently running if (connectorPluggedInput && !connectorPluggedInput()) { //special case when StopTransactionOnEVSideDisconnect is false - return ChargePointStatus::SuspendedEV; - } - if (!ocppPermitsCharge() || + res = ChargePointStatus_SuspendedEV; + } else if (!ocppPermitsCharge() || (evseReadyInput && !evseReadyInput())) { - return ChargePointStatus::SuspendedEVSE; - } - if (evReadyInput && !evReadyInput()) { - return ChargePointStatus::SuspendedEV; + res = ChargePointStatus_SuspendedEVSE; + } else if (evReadyInput && !evReadyInput()) { + res = ChargePointStatus_SuspendedEV; + } else { + res = ChargePointStatus_Charging; } - return ChargePointStatus::Charging; - } else if (model.getReservationService() && model.getReservationService()->getReservation(connectorId)) { - return ChargePointStatus::Reserved; - } else if ((!transaction || !transaction->isActive()) && //no transaction preparation + } + #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 - return ChargePointStatus::Available; + 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) { - return ChargePointStatus::Finishing; + 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 { - return ChargePointStatus::Preparing; + res = ChargePointStatus_Preparing; } } - MO_DBG_DEBUG("status undefined"); - return ChargePointStatus::Faulted; //internal error +#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() { @@ -143,24 +231,37 @@ bool Connector::ocppPermitsCharge() { suspendDeAuthorizedIdTag = false; } - return transaction && - transaction->isRunning() && - transaction->isActive() && - !isFaulted() && - !suspendDeAuthorizedIdTag; + // 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) { - //If the transaction is aborted (invalidated before started), delete all artifacts from flash + 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()); } @@ -169,20 +270,26 @@ void Connector::loop() { } if (removed) { - model.getTransactionStore()->setTxEnd(connectorId, transaction->getTxNr()); //roll back creation of last tx entry + if (txNrFront == txNrEnd) { + txNrFront = transaction->getTxNr(); + } + txNrEnd = transaction->getTxNr(); //roll back creation of last tx entry } - MO_DBG_DEBUG("collect aborted transaction %u-%u %s", connectorId, transaction->getTxNr(), removed ? "" : "failure"); + 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->isCompleted()) { + 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; } @@ -203,13 +310,13 @@ void Connector::loop() { transaction->getBeginTimestamp() > MIN_TIME && connectionTimeOutInt && connectionTimeOutInt->getInt() > 0 && !connectorPluggedInput() && - model.getClock().now() - transaction->getBeginTimestamp() >= connectionTimeOutInt->getInt()) { + model.getClock().now() - transaction->getBeginTimestamp() > connectionTimeOutInt->getInt()) { MO_DBG_INFO("Session mngt: timeout"); transaction->setInactive(); transaction->commit(); - updateTxNotification(TxNotification::ConnectionTimeout); + updateTxNotification(TxNotification_ConnectionTimeout); } } @@ -234,6 +341,7 @@ void Connector::loop() { 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 @@ -241,7 +349,7 @@ void Connector::loop() { auto meteringService = model.getMeteringService(); if (transaction->getMeterStart() < 0 && meteringService) { - auto meterStart = meteringService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext::TransactionBegin); + auto meterStart = meteringService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext_TransactionBegin); if (meterStart && *meterStart) { transaction->setMeterStart(meterStart->toInteger()); } else { @@ -254,36 +362,27 @@ void Connector::loop() { transaction->setStartBootNr(model.getBootNr()); } - updateTxNotification(TxNotification::StartTx); + transaction->getStartSync().setRequested(); + transaction->getStartSync().setOpNr(context.getRequestQueue().getNextOpNr()); if (transaction->isSilent()) { MO_DBG_INFO("silent Transaction: omit StartTx"); - transaction->getStartSync().setRequested(); transaction->getStartSync().confirm(); - transaction->commit(); - return; + } else { + //normal transaction, record txMeterData + if (model.getMeteringService()) { + model.getMeteringService()->beginTxMeterData(transaction.get()); + } } transaction->commit(); - if (model.getMeteringService()) { - model.getMeteringService()->beginTxMeterData(transaction.get()); - } - - auto startTx = makeRequest(new StartTransaction(model, transaction)); - startTx->setTimeout(0); - startTx->setOnReceiveConfListener([this] (JsonObject response) { - //fetch authorization status from StartTransaction.conf() for user notification + updateTxNotification(TxNotification_StartTx); - const char* idTagInfoStatus = response["idTagInfo"]["status"] | "_Undefined"; - if (strcmp(idTagInfoStatus, "Accepted")) { - updateTxNotification(TxNotification::DeAuthorized); - } - }); - context.initiateRequest(std::move(startTx)); + //fetchFrontRequest will create the StartTransaction and pass it to the message sender return; } - } else { + } else { //stop tx? if (!transaction->isActive() && @@ -291,24 +390,10 @@ void Connector::loop() { //stop transaction MO_DBG_INFO("Session mngt: trigger StopTransaction"); - - if (transaction->isSilent()) { - MO_DBG_INFO("silent Transaction: omit StopTx"); - updateTxNotification(TxNotification::StopTx); - transaction->getStopSync().setRequested(); - transaction->getStopSync().confirm(); - if (auto mService = model.getMeteringService()) { - mService->removeTxMeterData(connectorId, transaction->getTxNr()); - } - model.getTransactionStore()->remove(connectorId, transaction->getTxNr()); - model.getTransactionStore()->setTxEnd(connectorId, transaction->getTxNr()); - transaction = nullptr; - return; - } auto meteringService = model.getMeteringService(); if (transaction->getMeterStop() < 0 && meteringService) { - auto meterStop = meteringService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext::TransactionEnd); + auto meterStop = meteringService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext_TransactionEnd); if (meterStop && *meterStop) { transaction->setMeterStop(meterStop->toInteger()); } else { @@ -321,25 +406,24 @@ void Connector::loop() { transaction->setStopBootNr(model.getBootNr()); } - transaction->commit(); - - updateTxNotification(TxNotification::StopTx); - - std::shared_ptr stopTxData; + transaction->getStopSync().setRequested(); + transaction->getStopSync().setOpNr(context.getRequestQueue().getNextOpNr()); - if (meteringService) { - stopTxData = meteringService->endTxMeterData(transaction.get()); + 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()); + } } - std::unique_ptr stopTx; + transaction->commit(); - if (stopTxData) { - stopTx = makeRequest(new StopTransaction(model, std::move(transaction), stopTxData->retrieveStopTxData())); - } else { - stopTx = makeRequest(new StopTransaction(model, std::move(transaction))); - } - stopTx->setTimeout(0); - context.initiateRequest(std::move(stopTx)); + updateTxNotification(TxNotification_StopTx); + + //fetchFrontRequest will create the StopTransaction and pass it to the message sender return; } } @@ -357,50 +441,89 @@ void Connector::loop() { if (!transaction) { MO_DBG_ERR("could not begin FreeVend Tx"); - (void)0; } } freeVendTrackPlugged = connectorPluggedInput(); } - auto status = getStatus(); + ErrorData errorData {nullptr}; + errorData.severity = 0; + int errorDataIndex = -1; - for (auto i = std::min(errorDataInputs.size(), trackErrorDataInputs.size()); i >= 1; i--) { - auto index = i - 1; - auto error = errorDataInputs[index].operator()(); - if (error.isError && !trackErrorDataInputs[index]) { - //new error - auto statusNotification = makeRequest( - new StatusNotification(connectorId, status, model.getClock().now(), error)); - statusNotification->setTimeout(0); - context.initiateRequest(std::move(statusNotification)); + if (model.getVersion().major == 1 && model.getClock().now() >= MIN_TIME) { + //OCPP 1.6: use StatusNotification to send error codes - currentStatus = status; - reportedStatus = status; - trackErrorDataInputs[index] = true; - } else if (!error.isError && trackErrorDataInputs[index]) { - //reset error - trackErrorDataInputs[index] = false; + 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(); - MO_DBG_DEBUG("Status changed%s", minimumStatusDurationInt->getInt() ? ", will report delayed" : ""); } if (reportedStatus != currentStatus && - model.getClock().now() >= Timestamp(2010,0,0,0,0,0) && + 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 = makeRequest( - new StatusNotification(connectorId, reportedStatus, reportedTimestamp, getErrorCode())); + 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; @@ -420,8 +543,8 @@ bool Connector::isFaulted() { } const char *Connector::getErrorCode() { - for (auto i = errorDataInputs.size(); i >= 1; i--) { - auto error = errorDataInputs[i-1].operator()(); + if (reportedErrorIndex >= 0) { + auto error = errorDataInputs[reportedErrorIndex].operator()(); if (error.isError && error.errorCode) { return error.errorCode; } @@ -431,14 +554,14 @@ const char *Connector::getErrorCode() { std::shared_ptr Connector::allocateTransaction() { - decltype(allocateTransaction()) tx; + std::shared_ptr tx; - //clean possible aorted tx - auto txr = model.getTransactionStore()->getTxEnd(connectorId); - auto txsize = model.getTransactionStore()->size(connectorId); - for (decltype(txsize) i = 0; i < txsize; i++) { + //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)) { @@ -451,7 +574,10 @@ std::shared_ptr Connector::allocateTransaction() { removed &= model.getTransactionStore()->remove(connectorId, txr); } if (removed) { - model.getTransactionStore()->setTxEnd(connectorId, txr); + if (txNrFront == txNrEnd) { + txNrFront = txr; + } + txNrEnd = txr; MO_DBG_WARN("deleted dangling silent or aborted tx for new transaction"); } else { MO_DBG_ERR("memory corruption"); @@ -463,16 +589,20 @@ std::shared_ptr Connector::allocateTransaction() { } } + txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; //refresh after cleaning txs + //try to create new transaction - tx = model.getTransactionStore()->createTransaction(connectorId); + if (txSize < MO_TXRECORD_SIZE) { + tx = model.getTransactionStore()->createTransaction(connectorId, txNrEnd); + } if (!tx) { //could not create transaction - now, try to replace tx history entry - auto txl = model.getTransactionStore()->getTxBegin(connectorId); - auto txsize = model.getTransactionStore()->size(connectorId); + unsigned int txl = txNrBegin; + txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; - for (decltype(txsize) i = 0; i < txsize; i++) { + for (unsigned int i = 0; i < txSize; i++) { if (tx) { //success, finished here @@ -483,7 +613,7 @@ std::shared_ptr Connector::allocateTransaction() { 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()) { + if (!txhist || txhist->isCompleted() || txhist->isAborted() || (txhist->isSilent() && txhist->getStopSync().isRequested())) { //yes, remove bool removed = true; if (auto mService = model.getMeteringService()) { @@ -493,10 +623,14 @@ std::shared_ptr Connector::allocateTransaction() { removed &= model.getTransactionStore()->remove(connectorId, txl); } if (removed) { - model.getTransactionStore()->setTxBegin(connectorId, (txl + 1) % MAX_TX_CNT); + 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); + tx = model.getTransactionStore()->createTransaction(connectorId, txNrEnd); } else { MO_DBG_ERR("memory corruption"); break; @@ -516,15 +650,29 @@ std::shared_ptr Connector::allocateTransaction() { //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, true); + tx = model.getTransactionStore()->createTransaction(connectorId, txNrEnd, true); if (tx) { MO_DBG_DEBUG("created silent transaction"); - (void)0; } } } + 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; } @@ -537,12 +685,14 @@ std::shared_ptr Connector::beginTransaction(const char *idTag) { MO_DBG_DEBUG("Begin transaction process (%s), prepare", idTag != nullptr ? idTag : ""); - AuthorizationData *localAuth = nullptr; + 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 (model.getAuthorizationService()) { - localAuth = model.getAuthorizationService()->getLocalAuthorization(idTag); + #if MO_ENABLE_LOCAL_AUTH + if (auto authService = model.getAuthorizationService()) { + auto localAuth = authService->getLocalAuthorization(idTag); //check authorization status if (localAuth && localAuth->getAuthorizationStatus() != AuthorizationStatus::Accepted) { @@ -557,20 +707,30 @@ std::shared_ptr Connector::beginTransaction(const char *idTag) { offlineBlockedAuth = true; localAuth = nullptr; } + + if (localAuth) { + localAuthFound = true; + parentIdTag = localAuth->getParentIdTag(); + } } + #endif //MO_ENABLE_LOCAL_AUTH - Reservation *reservation = nullptr; + 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()) { - const char *parentIdTag = localAuth ? localAuth->getParentIdTag() : nullptr; - reservation = model.getReservationService()->getReservation( + auto reservation = model.getReservationService()->getReservation( connectorId, idTag, parentIdTag); + if (reservation) { + reservationId = reservation->getReservationId(); + } + if (reservation && !reservation->matches( idTag, parentIdTag)) { @@ -581,16 +741,17 @@ std::shared_ptr Connector::beginTransaction(const char *idTag) { if (parentIdTag) { //parentIdTag known MO_DBG_INFO("connector %u reserved - abort transaction", connectorId); - updateTxNotification(TxNotification::ReservationConflict); + 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); - localAuth = nullptr; + localAuthFound = false; } } } - + #endif //MO_ENABLE_RESERVATION + transaction = allocateTransaction(); if (!transaction) { @@ -605,24 +766,34 @@ std::shared_ptr Connector::beginTransaction(const char *idTag) { transaction->setIdTag(idTag); } + if (parentIdTag) { + transaction->setParentIdTag(parentIdTag); + } + transaction->setBeginTimestamp(model.getClock().now()); //check for local preauthorization - if (localAuth && localPreAuthorizeBool && localPreAuthorizeBool->getBool()) { + if (localAuthFound && localPreAuthorizeBool && localPreAuthorizeBool->getBool()) { MO_DBG_DEBUG("Begin transaction process (%s), preauthorized locally", idTag != nullptr ? idTag : ""); - if (reservation) { - transaction->setReservationId(reservation->getReservationId()); + if (reservationId >= 0) { + transaction->setReservationId(reservationId); } transaction->setAuthorized(); - updateTxNotification(TxNotification::Authorized); + updateTxNotification(TxNotification_Authorized); } transaction->commit(); - auto authorize = makeRequest(new Authorize(context.getModel(), idTag)); + 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"]; @@ -632,10 +803,11 @@ std::shared_ptr Connector::beginTransaction(const char *idTag) { MO_DBG_DEBUG("Authorize rejected (%s), abort tx process", tx->getIdTag()); tx->setIdTagDeauthorized(); tx->commit(); - updateTxNotification(TxNotification::AuthorizationRejected); + updateTxNotification(TxNotification_AuthorizationRejected); return; } + #if MO_ENABLE_RESERVATION if (model.getReservationService()) { auto reservation = model.getReservationService()->getReservation( connectorId, @@ -653,28 +825,29 @@ std::shared_ptr Connector::beginTransaction(const char *idTag) { MO_DBG_INFO("connector %u reserved - abort transaction", connectorId); tx->setInactive(); tx->commit(); - updateTxNotification(TxNotification::ReservationConflict); + 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); + updateTxNotification(TxNotification_Authorized); }); //capture local auth and reservation check in for timeout handler - bool localAuthFound = localAuth; - bool reservationFound = reservation; - int reservationId = reservation ? reservation->getReservationId() : -1; authorize->setOnTimeoutListener([this, tx, offlineBlockedAuth, offlineBlockedResv, localAuthFound, - reservationFound, reservationId] () { if (offlineBlockedAuth) { @@ -682,7 +855,7 @@ std::shared_ptr Connector::beginTransaction(const char *idTag) { MO_DBG_DEBUG("Abort transaction process (%s), timeout, expired local auth", tx->getIdTag()); tx->setInactive(); tx->commit(); - updateTxNotification(TxNotification::AuthorizationTimeout); + updateTxNotification(TxNotification_AuthorizationTimeout); return; } @@ -691,37 +864,37 @@ std::shared_ptr Connector::beginTransaction(const char *idTag) { MO_DBG_INFO("connector %u reserved (offline) - abort transaction", connectorId); tx->setInactive(); tx->commit(); - updateTxNotification(TxNotification::ReservationConflict); + updateTxNotification(TxNotification_ReservationConflict); return; } if (localAuthFound && localAuthorizeOfflineBool && localAuthorizeOfflineBool->getBool()) { MO_DBG_DEBUG("Offline transaction process (%s), locally authorized", tx->getIdTag()); - if (reservationFound) { + if (reservationId >= 0) { tx->setReservationId(reservationId); } tx->setAuthorized(); tx->commit(); - updateTxNotification(TxNotification::Authorized); + updateTxNotification(TxNotification_Authorized); return; } if (allowOfflineTxForUnknownIdBool && allowOfflineTxForUnknownIdBool->getBool()) { MO_DBG_DEBUG("Offline transaction process (%s), allow unknown ID", tx->getIdTag()); - if (reservationFound) { + if (reservationId >= 0) { tx->setReservationId(reservationId); } tx->setAuthorized(); tx->commit(); - updateTxNotification(TxNotification::Authorized); + updateTxNotification(TxNotification_Authorized); return; } MO_DBG_DEBUG("Abort transaction process (%s): timeout", tx->getIdTag()); tx->setInactive(); tx->commit(); - updateTxNotification(TxNotification::AuthorizationTimeout); + updateTxNotification(TxNotification_AuthorizationTimeout); return; //offline tx disabled }); context.initiateRequest(std::move(authorize)); @@ -750,12 +923,17 @@ std::shared_ptr Connector::beginTransaction_authorized(const char * 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)) { @@ -763,6 +941,7 @@ std::shared_ptr Connector::beginTransaction_authorized(const char * } } } + #endif //MO_ENABLE_RESERVATION transaction->commit(); @@ -816,6 +995,28 @@ bool Connector::isOperative() { } } + #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(); } @@ -851,13 +1052,15 @@ void Connector::addErrorDataInput(std::function errorDataInput) { this->trackErrorDataInputs.push_back(false); } -void Connector::setOnUnlockConnector(std::function()> unlockConnector) { +#if MO_ENABLE_CONNECTOR_LOCK +void Connector::setOnUnlockConnector(std::function unlockConnector) { this->onUnlockConnector = unlockConnector; } -std::function()> Connector::getOnUnlockConnector() { +std::function Connector::getOnUnlockConnector() { return this->onUnlockConnector; } +#endif //MO_ENABLE_CONNECTOR_LOCK void Connector::setStartTxReadyInput(std::function startTxReady) { this->startTxReadyInput = startTxReady; @@ -880,3 +1083,240 @@ void Connector::updateTxNotification(TxNotification event) { 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 index 3b5e6196..d8d61639 100644 --- a/src/MicroOcpp/Model/ConnectorBase/Connector.h +++ b/src/MicroOcpp/Model/ConnectorBase/Connector.h @@ -1,34 +1,44 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef CONNECTOR_H -#define CONNECTOR_H +#ifndef MO_CONNECTOR_H +#define MO_CONNECTOR_H #include #include -#include +#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 Transaction; -class Connector { +class Connector : public RequestEmitter, public MemoryManaged { private: Context& context; Model& model; + std::shared_ptr filesystem; - const int connectorId; + const unsigned int connectorId; std::shared_ptr transaction; @@ -39,17 +49,20 @@ class Connector { std::function connectorPluggedInput; std::function evReadyInput; std::function evseReadyInput; - std::vector> errorDataInputs; - std::vector trackErrorDataInputs; + Vector> errorDataInputs; + Vector trackErrorDataInputs; + int reportedErrorIndex = -1; //last reported error bool isFaulted(); const char *getErrorCode(); - ChargePointStatus currentStatus = ChargePointStatus::NOT_SET; + ChargePointStatus currentStatus = ChargePointStatus_UNDEFINED; std::shared_ptr minimumStatusDurationInt; //in seconds - ChargePointStatus reportedStatus = ChargePointStatus::NOT_SET; + ChargePointStatus reportedStatus = ChargePointStatus_UNDEFINED; unsigned long t_statusTransition = 0; - std::function()> onUnlockConnector; +#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 @@ -70,9 +83,20 @@ class Connector { 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, int connectorId); + Connector(Context& context, std::shared_ptr filesystem, unsigned int connectorId); Connector(const Connector&) = delete; Connector(Connector&&) = delete; Connector& operator=(const Connector&) = delete; @@ -116,8 +140,10 @@ class Connector { bool ocppPermitsCharge(); - void setOnUnlockConnector(std::function()> unlockConnector); - std::function()> getOnUnlockConnector(); +#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); @@ -125,6 +151,15 @@ class Connector { 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 diff --git a/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp b/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp index 2a31817b..cfc32c82 100644 --- a/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp +++ b/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp @@ -1,15 +1,14 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include - #include #include #include #include #include #include +#include #include #include #include @@ -21,20 +20,22 @@ #include #include #include +#include #include +#include using namespace MicroOcpp; ConnectorsCommon::ConnectorsCommon(Context& context, unsigned int numConn, std::shared_ptr filesystem) : - context(context) { + 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, CONFIGURATION_VOLATILE, true); + declareConfiguration("AuthorizeRemoteTxRequests", false); declareConfiguration("GetConfigurationMaxKeys", 30, CONFIGURATION_VOLATILE, true); context.getOperationRegistry().registerOperation("ChangeAvailability", [&context] () { @@ -43,6 +44,8 @@ ConnectorsCommon::ConnectorsCommon(Context& context, unsigned int numConn, std:: 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] () { @@ -61,14 +64,27 @@ ConnectorsCommon::ConnectorsCommon(Context& context, unsigned int numConn, std:: * 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("Authorize", [&context] () { - return new Ocpp16::Authorize(context.getModel(), "");}); - context.getOperationRegistry().registerOperation("StartTransaction", [&context] () { - return new Ocpp16::StartTransaction(context.getModel(), nullptr);}); +#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::NOT_SET, Timestamp());}); - context.getOperationRegistry().registerOperation("StopTransaction", [&context] () { - return new Ocpp16::StopTransaction(context.getModel(), nullptr);}); + return new Ocpp16::StatusNotification(-1, ChargePointStatus_UNDEFINED, Timestamp());}); } void ConnectorsCommon::loop() { diff --git a/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.h b/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.h index b3848a5d..4bc9f78a 100644 --- a/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.h +++ b/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.h @@ -1,17 +1,18 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef CHARGECONTROLCOMMON_H -#define CHARGECONTROLCOMMON_H +#ifndef MO_CHARGECONTROLCOMMON_H +#define MO_CHARGECONTROLCOMMON_H #include +#include namespace MicroOcpp { class Context; -class ConnectorsCommon { +class ConnectorsCommon : public MemoryManaged { private: Context& context; public: diff --git a/src/MicroOcpp/Model/ConnectorBase/EvseId.h b/src/MicroOcpp/Model/ConnectorBase/EvseId.h new file mode 100644 index 00000000..6ca5295a --- /dev/null +++ b/src/MicroOcpp/Model/ConnectorBase/EvseId.h @@ -0,0 +1,35 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_EVSEID_H +#define MO_EVSEID_H + +#include + +#if MO_ENABLE_V201 + +// number of EVSE IDs (including 0). Defaults to MO_NUMCONNECTORS if defined, otherwise to 2 +#ifndef MO_NUM_EVSEID +#if defined(MO_NUMCONNECTORS) +#define MO_NUM_EVSEID MO_NUMCONNECTORS +#else +#define MO_NUM_EVSEID 2 +#endif +#endif // MO_NUM_EVSEID + +namespace MicroOcpp { + +// EVSEType (2.23) +struct EvseId { + int id; + int connectorId = -1; //optional + + EvseId(int id) : id(id) { } + EvseId(int id, int connectorId) : id(id), connectorId(connectorId) { } +}; + +} + +#endif // MO_ENABLE_V201 +#endif diff --git a/src/MicroOcpp/Model/ConnectorBase/Notification.cpp b/src/MicroOcpp/Model/ConnectorBase/Notification.cpp deleted file mode 100644 index 2e0b53d5..00000000 --- a/src/MicroOcpp/Model/ConnectorBase/Notification.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 -// MIT License - -#include - -namespace MicroOcpp { - -OCPP_TxNotification convertTxNotification(TxNotification txn) { - auto res = OCPP_TxNotification::AuthorizationRejected; - - switch (txn) { - case TxNotification::AuthorizationRejected: - res = OCPP_TxNotification::AuthorizationRejected; - break; - case TxNotification::AuthorizationTimeout: - res = OCPP_TxNotification::AuthorizationTimeout; - break; - case TxNotification::Authorized: - res = OCPP_TxNotification::Authorized; - break; - case TxNotification::ConnectionTimeout: - res = OCPP_TxNotification::ConnectionTimeout; - break; - case TxNotification::DeAuthorized: - res = OCPP_TxNotification::DeAuthorized; - break; - case TxNotification::RemoteStart: - res = OCPP_TxNotification::RemoteStart; - break; - case TxNotification::RemoteStop: - res = OCPP_TxNotification::RemoteStop; - break; - case TxNotification::ReservationConflict: - res = OCPP_TxNotification::ReservationConflict; - break; - case TxNotification::StartTx: - res = OCPP_TxNotification::StartTx; - break; - case TxNotification::StopTx: - res = OCPP_TxNotification::StopTx; - break; - } - - return res; -} - -} //end namespace MicroOcpp diff --git a/src/MicroOcpp/Model/ConnectorBase/Notification.h b/src/MicroOcpp/Model/ConnectorBase/Notification.h deleted file mode 100644 index eb437365..00000000 --- a/src/MicroOcpp/Model/ConnectorBase/Notification.h +++ /dev/null @@ -1,62 +0,0 @@ -// matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 -// MIT License - -#ifndef MO_NOTIFICATION_H -#define MO_NOTIFICATION_H - -#ifdef __cplusplus - -namespace MicroOcpp { - -enum class TxNotification { - //Authorization events - Authorized, //success - AuthorizationRejected, //IdTag not authorized - AuthorizationTimeout, //authorization failed - offline - ReservationConflict, //connector reserved for other IdTag - - ConnectionTimeout, //user took to long to plug vehicle after the authorization - DeAuthorized, //server rejected StartTx - RemoteStart, //authorized via RemoteStartTransaction - RemoteStop, //stopped via RemoteStopTransaction - - //Tx lifecycle events - StartTx, - StopTx, -}; - -} //end namespace MicroOcpp - -extern "C" { -#endif //__cplusplus - -enum OCPP_TxNotification { - //Authorization events - Authorized, //success - AuthorizationRejected, //IdTag not authorized - AuthorizationTimeout, //authorization failed - offline - ReservationConflict, //connector reserved for other IdTag - - ConnectionTimeout, //user took to long to plug vehicle after the authorization - DeAuthorized, //server rejected StartTx - RemoteStart, //authorized via RemoteStartTransaction - RemoteStop, //stopped via RemoteStopTransaction - - //Tx lifecycle events - StartTx, - StopTx, -}; - -#ifdef __cplusplus -} //end extern "C" - -namespace MicroOcpp { - -OCPP_TxNotification convertTxNotification(TxNotification txn); - -} //end namespace MicroOcpp - -#endif //__cplusplus - -#endif diff --git a/src/MicroOcpp/Model/ConnectorBase/UnlockConnectorResult.h b/src/MicroOcpp/Model/ConnectorBase/UnlockConnectorResult.h new file mode 100644 index 00000000..0d863f2d --- /dev/null +++ b/src/MicroOcpp/Model/ConnectorBase/UnlockConnectorResult.h @@ -0,0 +1,36 @@ +// 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/DiagnosticsService.cpp b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp index dfe44caa..94bd9609 100644 --- a/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp +++ b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp @@ -1,21 +1,30 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include -#include +#include #include #include #include -using namespace MicroOcpp; -using Ocpp16::DiagnosticsStatus; +//Fetch relevant data from other modules for diagnostics +#include +#include //for serializing ChargePointStatus +#include +#include +#include //for MO_ENABLE_V201 +#include //for MO_ENABLE_CONNECTOR_LOCK + +using MicroOcpp::DiagnosticsService; +using MicroOcpp::Ocpp16::DiagnosticsStatus; +using MicroOcpp::Request; + +DiagnosticsService::DiagnosticsService(Context& context) : MemoryManaged("v16.Diagnostics.DiagnosticsService"), context(context), location(makeString(getMemoryTag())), diagFileList(makeVector(getMemoryTag())) { -DiagnosticsService::DiagnosticsService(Context& context) : context(context) { - context.getOperationRegistry().registerOperation("GetDiagnostics", [this] () { return new Ocpp16::GetDiagnostics(*this);}); @@ -24,7 +33,26 @@ DiagnosticsService::DiagnosticsService(Context& context) : context(context) { return new Ocpp16::DiagnosticsStatusNotification(getDiagnosticsStatus());}); } +DiagnosticsService::~DiagnosticsService() { + MO_FREE(diagPreamble); + MO_FREE(diagPostamble); +} + void DiagnosticsService::loop() { + + if (ftpUpload && ftpUpload->isActive()) { + ftpUpload->loop(); + } + + if (ftpUpload) { + if (ftpUpload->isActive()) { + ftpUpload->loop(); + } else { + MO_DBG_DEBUG("Deinit FTP upload"); + ftpUpload.reset(); + } + } + auto notification = getDiagnosticsStatusNotification(); if (notification) { context.initiateRequest(std::move(notification)); @@ -38,17 +66,19 @@ void DiagnosticsService::loop() { MO_DBG_DEBUG("Call onUpload"); onUpload(location.c_str(), startTime, stopTime); uploadIssued = true; + uploadFailure = false; } else { MO_DBG_ERR("onUpload must be set! (see setOnUpload). Will abort"); retries = 0; uploadIssued = false; + uploadFailure = true; } } if (uploadIssued) { if (uploadStatusInput != nullptr && uploadStatusInput() == UploadStatus::Uploaded) { //success! - MO_DBG_DEBUG("end upload routine (by status)") + MO_DBG_DEBUG("end upload routine (by status)"); uploadIssued = false; retries = 0; } @@ -65,8 +95,10 @@ void DiagnosticsService::loop() { uploadIssued = false; retries = 0; } else { - //either we have UploadFailed status or (NotDownloaded + timeout) here + //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; @@ -75,6 +107,11 @@ void DiagnosticsService::loop() { nextTry += retryInterval; } retries--; + + if (retries == 0) { + MO_DBG_DEBUG("end upload routine (no more retry)"); + uploadFailure = true; + } } } } //end if (uploadIssued) @@ -82,11 +119,27 @@ void DiagnosticsService::loop() { } //timestamps before year 2021 will be treated as "undefined" -std::string DiagnosticsService::requestDiagnosticsUpload(const char *location, unsigned int retries, unsigned int retryInterval, Timestamp startTime, Timestamp stopTime) { - if (onUpload == nullptr) //maybe add further plausibility checks - return std::string{}; - +MicroOcpp::String DiagnosticsService::requestDiagnosticsUpload(const char *location, unsigned int retries, unsigned int retryInterval, Timestamp startTime, Timestamp stopTime) { + if (onUpload == nullptr) { + return makeString(getMemoryTag()); + } + + String fileName; + if (refreshFilename) { + fileName = refreshFilename().c_str(); + } else { + fileName = "diagnostics.log"; + } + + this->location.reserve(strlen(location) + 1 + fileName.size()); + this->location = location; + + if (!this->location.empty() && this->location.back() != '/') { + this->location.append("/"); + } + this->location.append(fileName.c_str()); + this->retries = retries; this->retryInterval = retryInterval; this->startTime = startTime; @@ -101,41 +154,46 @@ std::string DiagnosticsService::requestDiagnosticsUpload(const char *location, u } #if MO_DBG_LEVEL >= MO_DL_INFO - char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; - char dbuf2 [JSONDATE_LENGTH + 1] = {'\0'}; - this->startTime.toJsonString(dbuf, JSONDATE_LENGTH + 1); - this->stopTime.toJsonString(dbuf2, JSONDATE_LENGTH + 1); - - MO_DBG_INFO("Scheduled Diagnostics upload!\n" \ - " location = %s\n" \ - " retries = %i" \ - ", retryInterval = %u" \ - " startTime = %s\n" \ - " stopTime = %s", - this->location.c_str(), - this->retries, - this->retryInterval, - dbuf, - dbuf2); + { + char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; + char dbuf2 [JSONDATE_LENGTH + 1] = {'\0'}; + this->startTime.toJsonString(dbuf, JSONDATE_LENGTH + 1); + this->stopTime.toJsonString(dbuf2, JSONDATE_LENGTH + 1); + + MO_DBG_INFO("Scheduled Diagnostics upload!\n" \ + " location = %s\n" \ + " retries = %i" \ + ", retryInterval = %u" \ + " startTime = %s\n" \ + " stopTime = %s", + this->location.c_str(), + this->retries, + this->retryInterval, + dbuf, + dbuf2); + } #endif nextTry = context.getModel().getClock().now(); nextTry += 5; //wait for 5s before upload uploadIssued = false; - nextTry.toJsonString(dbuf, JSONDATE_LENGTH + 1); - MO_DBG_DEBUG("Initial try at %s", dbuf); - - std::string fileName; - if (refreshFilename) { - fileName = refreshFilename(); - } else { - fileName = "diagnostics.log"; +#if MO_DBG_LEVEL >= MO_DL_DEBUG + { + char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; + nextTry.toJsonString(dbuf, JSONDATE_LENGTH + 1); + MO_DBG_DEBUG("Initial try at %s", dbuf); } +#endif + return fileName; } DiagnosticsStatus DiagnosticsService::getDiagnosticsStatus() { + if (uploadFailure) { + return DiagnosticsStatus::UploadFailed; + } + if (uploadIssued) { if (uploadStatusInput != nullptr) { switch (uploadStatusInput()) { @@ -178,18 +236,330 @@ void DiagnosticsService::setOnUploadStatusInput(std::function up this->uploadStatusInput = uploadStatusInput; } -#if !defined(MO_CUSTOM_DIAGNOSTICS) && !defined(MO_CUSTOM_WS) -#if defined(ESP32) || defined(ESP8266) +void DiagnosticsService::setDiagnosticsReader(std::function diagnosticsReader, std::function onClose, std::shared_ptr filesystem) { + + this->onUpload = [this, diagnosticsReader, onClose, filesystem] (const char *location, Timestamp &startTime, Timestamp &stopTime) -> bool { + + auto ftpClient = context.getFtpClient(); + if (!ftpClient) { + MO_DBG_ERR("FTP client not set"); + this->ftpUploadStatus = UploadStatus::UploadFailed; + return false; + } + + const size_t diagPreambleSize = 128; + diagPreamble = static_cast(MO_MALLOC(getMemoryTag(), diagPreambleSize)); + if (!diagPreamble) { + MO_DBG_ERR("OOM"); + this->ftpUploadStatus = UploadStatus::UploadFailed; + return false; + } + diagPreambleLen = 0; + diagPreambleTransferred = 0; + + diagReaderHasData = diagnosticsReader ? true : false; + + const size_t diagPostambleSize = 1024; + diagPostamble = static_cast(MO_MALLOC(getMemoryTag(), diagPostambleSize)); + if (!diagPostamble) { + MO_DBG_ERR("OOM"); + this->ftpUploadStatus = UploadStatus::UploadFailed; + MO_FREE(diagPreamble); + return false; + } + diagPostambleLen = 0; + diagPostambleTransferred = 0; + diagFilesBackTransferred = 0; + + auto& model = context.getModel(); + + auto cpVendor = makeString(getMemoryTag()); + auto cpModel = makeString(getMemoryTag()); + auto fwVersion = makeString(getMemoryTag()); + + if (auto bootService = model.getBootService()) { + if (auto cpCreds = bootService->getChargePointCredentials()) { + cpVendor = (*cpCreds)["chargePointVendor"] | "Vendor"; + cpModel = (*cpCreds)["chargePointModel"] | "Charger"; + fwVersion = (*cpCreds)["firmwareVersion"] | ""; + } + } + + char jsonDate [JSONDATE_LENGTH + 1]; + model.getClock().now().toJsonString(jsonDate, sizeof(jsonDate)); + + int ret; + + ret = snprintf(diagPreamble, diagPreambleSize, + "### %s %s - Hardware Diagnostics%s%s\n%s\n", + cpVendor.c_str(), + cpModel.c_str(), + fwVersion.empty() ? "" : " - v. ", fwVersion.c_str(), + jsonDate); + + if (ret < 0 || (size_t)ret >= diagPreambleSize) { + MO_DBG_ERR("snprintf: %i", ret); + this->ftpUploadStatus = UploadStatus::UploadFailed; + MO_FREE(diagPreamble); + MO_FREE(diagPostamble); + return false; + } + + diagPreambleLen += (size_t)ret; + + Connector *connector0 = model.getConnector(0); + Connector *connector1 = model.getConnector(1); + Transaction *connector1Tx = connector1 ? connector1->getTransaction().get() : nullptr; + Connector *connector2 = model.getNumConnectors() > 2 ? model.getConnector(2) : nullptr; + Transaction *connector2Tx = connector2 ? connector2->getTransaction().get() : nullptr; + + ret = 0; + + if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { + diagPostambleLen += (size_t)ret; + ret = snprintf(diagPostamble + diagPostambleLen, diagPostambleSize - diagPostambleLen, + "\n# OCPP" + "\nclient_version=%s" + "\nuptime=%lus" + "%s%s" + "%s%s" + "%s%s" + "\nws_status=%s" + "\nws_last_conn=%lus" + "\nws_last_recv=%lus" + "%s%s" + "%s%s" + "%s%s" + "%s%s" + "%s%s" + "%s%s" + "%s%s" + "%s%s" + "\nENABLE_CONNECTOR_LOCK=%i" + "\nENABLE_FILE_INDEX=%i" + "\nENABLE_V201=%i" + "\n", + MO_VERSION, + mocpp_tick_ms() / 1000UL, + connector0 ? "\nocpp_status_cId0=" : "", connector0 ? cstrFromOcppEveState(connector0->getStatus()) : "", + connector1 ? "\nocpp_status_cId1=" : "", connector1 ? cstrFromOcppEveState(connector1->getStatus()) : "", + connector2 ? "\nocpp_status_cId2=" : "", connector2 ? cstrFromOcppEveState(connector2->getStatus()) : "", + context.getConnection().isConnected() ? "connected" : "unconnected", + context.getConnection().getLastConnected() / 1000UL, + context.getConnection().getLastRecv() / 1000UL, + connector1 ? "\ncId1_hasTx=" : "", connector1 ? (connector1Tx ? "1" : "0") : "", + connector1Tx ? "\ncId1_txActive=" : "", connector1Tx ? (connector1Tx->isActive() ? "1" : "0") : "", + connector1Tx ? "\ncId1_txHasStarted=" : "", connector1Tx ? (connector1Tx->getStartSync().isRequested() ? "1" : "0") : "", + connector1Tx ? "\ncId1_txHasStopped=" : "", connector1Tx ? (connector1Tx->getStopSync().isRequested() ? "1" : "0") : "", + connector2 ? "\ncId2_hasTx=" : "", connector2 ? (connector2Tx ? "1" : "0") : "", + connector2Tx ? "\ncId2_txActive=" : "", connector2Tx ? (connector2Tx->isActive() ? "1" : "0") : "", + connector2Tx ? "\ncId2_txHasStarted=" : "", connector2Tx ? (connector2Tx->getStartSync().isRequested() ? "1" : "0") : "", + connector2Tx ? "\ncId2_txHasStopped=" : "", connector2Tx ? (connector2Tx->getStopSync().isRequested() ? "1" : "0") : "", + MO_ENABLE_CONNECTOR_LOCK, + MO_ENABLE_FILE_INDEX, + MO_ENABLE_V201 + ); + } + + if (filesystem) { + + if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { + diagPostambleLen += (size_t)ret; + ret = snprintf(diagPostamble + diagPostambleLen, diagPostambleSize - diagPostambleLen, "\n# Filesystem\n"); + } + + filesystem->ftw_root([this, &ret] (const char *fname) -> int { + if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { + diagPostambleLen += (size_t)ret; + ret = snprintf(diagPostamble + diagPostambleLen, diagPostambleSize - diagPostambleLen, "%s\n", fname); + } + diagFileList.emplace_back(fname); + return 0; + }); + + MO_DBG_DEBUG("discovered %zu files", diagFileList.size()); + } + + if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { + diagPostambleLen += (size_t)ret; + } else { + char errMsg [64]; + auto errLen = snprintf(errMsg, sizeof(errMsg), "\n[Diagnostics cut]\n"); + size_t ellipseStart = std::min(diagPostambleSize - (size_t)errLen - 1, diagPostambleLen); + auto ret2 = snprintf(diagPostamble + ellipseStart, diagPostambleSize - ellipseStart, "%s", errMsg); + diagPostambleLen += (size_t)ret2; + } + + this->ftpUpload = ftpClient->postFile(location, + [this, diagnosticsReader, filesystem] (unsigned char *buf, size_t size) -> size_t { + size_t written = 0; + if (written < size && diagPreambleTransferred < diagPreambleLen) { + size_t writeLen = std::min(size - written, diagPreambleLen - diagPreambleTransferred); + memcpy(buf + written, diagPreamble + diagPreambleTransferred, writeLen); + diagPreambleTransferred += writeLen; + written += writeLen; + } + + while (written < size && diagReaderHasData && diagnosticsReader) { + size_t writeLen = diagnosticsReader((char*)buf + written, size - written); + if (writeLen == 0) { + diagReaderHasData = false; + } + written += writeLen; + } + + if (written < size && diagPostambleTransferred < diagPostambleLen) { + size_t writeLen = std::min(size - written, diagPostambleLen - diagPostambleTransferred); + memcpy(buf + written, diagPostamble + diagPostambleTransferred, writeLen); + diagPostambleTransferred += writeLen; + written += writeLen; + } + + while (written < size && !diagFileList.empty() && filesystem) { + + char fpath [MO_MAX_PATH_SIZE]; + auto ret = snprintf(fpath, sizeof(fpath), "%s%s", MO_FILENAME_PREFIX, diagFileList.back().c_str()); + if (ret < 0 || (size_t)ret >= sizeof(fpath)) { + MO_DBG_ERR("fn error: %i", ret); + diagFileList.pop_back(); + // next file starts from offset 0 + diagFilesBackTransferred = 0; + continue; + } + + if (auto file = filesystem->open(fpath, "r")) { + + if (diagFilesBackTransferred == 0) { + char fileHeading [30 + MO_MAX_PATH_SIZE]; + auto writeLen = snprintf(fileHeading, sizeof(fileHeading), "\n\n# File %s:\n", diagFileList.back().c_str()); + if (writeLen < 0 || (size_t)writeLen >= sizeof(fileHeading)) { + MO_DBG_ERR("fn error: %i", ret); + diagFileList.pop_back(); + diagFilesBackTransferred = 0; + continue; + } + if (writeLen + written > size || //heading doesn't fit anymore, return with a bit unused buffer space and print heading the next time + writeLen + written == size) { //filling the buffer up exactly would mean that no file payload is written and this head gets printed again + + MO_DBG_DEBUG("upload diag chunk (%zuB)", written); + return written; + } + + memcpy(buf + written, fileHeading, (size_t)writeLen); + written += (size_t)writeLen; + } + + file->seek(diagFilesBackTransferred); + size_t writeLen = file->read((char*)buf + written, size - written); + // advance per-file offset + diagFilesBackTransferred += writeLen; + if (writeLen < size - written) { + // EOF for this file; move to next and reset offset + MO_DBG_DEBUG("upload diag chunk %zu (done)", diagFilesBackTransferred); + diagFileList.pop_back(); + diagFilesBackTransferred = 0; + } + written += writeLen; + } else { + MO_DBG_ERR("could not open file: %s", fpath); + diagFileList.pop_back(); + diagFilesBackTransferred = 0; + } + } + + MO_DBG_DEBUG("upload diag chunk (%zuB)", written); + return written; + }, + [this, onClose] (MO_FtpCloseReason reason) -> void { + if (reason == MO_FtpCloseReason_Success) { + MO_DBG_INFO("FTP upload success"); + this->ftpUploadStatus = UploadStatus::Uploaded; + } else { + MO_DBG_INFO("FTP upload failure (%i)", reason); + this->ftpUploadStatus = UploadStatus::UploadFailed; + } + + MO_FREE(diagPreamble); + MO_FREE(diagPostamble); + diagFileList.clear(); + diagFilesBackTransferred = 0; //reset offset for future uploads + + if (onClose) { + onClose(); + } + }); + + if (this->ftpUpload) { + this->ftpUploadStatus = UploadStatus::NotUploaded; + return true; + } else { + this->ftpUploadStatus = UploadStatus::UploadFailed; + return false; + } + }; + + this->uploadStatusInput = [this] () { + return this->ftpUploadStatus; + }; +} + +void DiagnosticsService::setFtpServerCert(const char *cert) { + this->ftpServerCert = cert; +} + +#if !defined(MO_CUSTOM_DIAGNOSTICS) + +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS + +#include "esp_heap_caps.h" +#include + +bool g_diagsSent = false; + +std::unique_ptr MicroOcpp::makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem) { + std::unique_ptr diagService = std::unique_ptr(new DiagnosticsService(context)); + + diagService->setDiagnosticsReader( + [] (char *buf, size_t size) -> size_t { + if (!g_diagsSent) { + g_diagsSent = true; + int ret = snprintf(buf, size, + "\n# Memory\n" + "freeHeap=%zu\n" + "minHeap=%zu\n" + "maxAllocHeap=%zu\n" + "LittleFS_used=%zu\n" + "LittleFS_total=%zu\n", + heap_caps_get_free_size(MALLOC_CAP_DEFAULT), + heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT), + heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT), + LittleFS.usedBytes(), + LittleFS.totalBytes() + ); + if (ret < 0 || (size_t)ret >= size) { + MO_DBG_ERR("snprintf: %i", ret); + return 0; + } + return (size_t)ret; + } + return 0; + }, [] () { + g_diagsSent = false; + }, + filesystem); + + return diagService; +} + +#elif MO_ENABLE_MBEDTLS -DiagnosticsService *EspWiFi::makeDiagnosticsService(Context& context) { - auto diagService = new DiagnosticsService(context); +std::unique_ptr MicroOcpp::makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem) { + std::unique_ptr diagService = std::unique_ptr(new DiagnosticsService(context)); - /* - * add onUpload and uploadStatusInput when logging was implemented - */ + diagService->setDiagnosticsReader(nullptr, nullptr, filesystem); //report the built-in MO defaults return diagService; } -#endif //defined(ESP32) || defined(ESP8266) -#endif //!defined(MO_CUSTOM_UPDATER) && !defined(MO_CUSTOM_WS) +#endif //MO_PLATFORM +#endif //!defined(MO_CUSTOM_DIAGNOSTICS) diff --git a/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.h b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.h index cd7d2654..ed3ae3c0 100644 --- a/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.h +++ b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef DIAGNOSTICSSERVICE_H @@ -8,8 +8,9 @@ #include #include #include +#include +#include #include -#include namespace MicroOcpp { @@ -21,12 +22,13 @@ enum class UploadStatus { class Context; class Request; +class FilesystemAdapter; -class DiagnosticsService { +class DiagnosticsService : public MemoryManaged { private: Context& context; - std::string location; + String location; unsigned int retries = 0; unsigned int retryInterval = 0; Timestamp startTime; @@ -38,6 +40,20 @@ class DiagnosticsService { std::function onUpload; std::function uploadStatusInput; bool uploadIssued = false; + bool uploadFailure = false; + + std::unique_ptr ftpUpload; + UploadStatus ftpUploadStatus = UploadStatus::NotUploaded; + const char *ftpServerCert = nullptr; + char *diagPreamble = nullptr; + size_t diagPreambleLen = 0; + size_t diagPreambleTransferred = 0; + bool diagReaderHasData = false; + char *diagPostamble = nullptr; + size_t diagPostambleLen = 0; + size_t diagPostambleTransferred = 0; + Vector diagFileList; + size_t diagFilesBackTransferred = 0; std::unique_ptr getDiagnosticsStatusNotification(); @@ -45,35 +61,63 @@ class DiagnosticsService { 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 - std::string requestDiagnosticsUpload(const char *location, unsigned int retries = 1, unsigned int retryInterval = 0, Timestamp startTime = Timestamp(), Timestamp stopTime = Timestamp()); + String requestDiagnosticsUpload(const char *location, unsigned int retries = 1, unsigned int retryInterval = 0, Timestamp startTime = Timestamp(), Timestamp stopTime = Timestamp()); Ocpp16::DiagnosticsStatus getDiagnosticsStatus(); void setRefreshFilename(std::function refreshFn); //refresh a new filename which will be used for the subsequent upload tries + /* + * Sets the diagnostics data reader. When the server sends a GetDiagnostics operation, then MO will open an FTP + * connection to the FTP server and upload a diagnostics file. MO automatically creates a small report about + * the OCPP-related status data + it uploads the contents of the OCPP directory. In addition to the automatic + * report, MO also sends all data provided by the custom diagnosticsReader. Use the diagnosticsReader to add + * all data which could be helpful for troubleshooting, i.e. + * - internal status variables, or state machine states + * - error trip counters + * - current sensor readings and all GPIO values + * - Heap statistics, flash memory statistics + * - and more. The more the better + * + * MO calls the diagnosticsReader output buffer `buf` and the bufsize `size`. Write at most `size` bytes and + * return the number of bytes actually written (without terminating zero-byte). It's not necessary to append + * a terminating zero, MO will ignore any data after the string. To end the reading process, return 0. + * + * Note that this function only works if MO_ENABLE_MBEDTLS=1, or MO has been configured with a custom FTP client + */ + void setDiagnosticsReader(std::function diagnosticsReader, std::function onClose, std::shared_ptr filesystem); + + void setFtpServerCert(const char *cert); //zero-copy mode, i.e. cert must outlive MO + void setOnUpload(std::function onUpload); void setOnUploadStatusInput(std::function uploadStatusInput); }; -#if !defined(MO_CUSTOM_DIAGNOSTICS) && !defined(MO_CUSTOM_WS) -#if defined(ESP32) || defined(ESP8266) +} //end namespace MicroOcpp -namespace EspWiFi { +#if !defined(MO_CUSTOM_DIAGNOSTICS) -DiagnosticsService *makeDiagnosticsService(Context& context); +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS +namespace MicroOcpp { +std::unique_ptr makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem); } -#endif //defined(ESP32) || defined(ESP8266) -#endif //!defined(MO_CUSTOM_UPDATER) && !defined(MO_CUSTOM_WS) +#elif MO_ENABLE_MBEDTLS -} //end namespace MicroOcpp +namespace MicroOcpp { +std::unique_ptr makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem); +} + +#endif //MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS +#endif //!defined(MO_CUSTOM_DIAGNOSTICS) #endif diff --git a/src/MicroOcpp/Model/Diagnostics/DiagnosticsStatus.h b/src/MicroOcpp/Model/Diagnostics/DiagnosticsStatus.h index 1cf9da4d..632708a4 100644 --- a/src/MicroOcpp/Model/Diagnostics/DiagnosticsStatus.h +++ b/src/MicroOcpp/Model/Diagnostics/DiagnosticsStatus.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef DIAGNOSTICS_STATUS -#define DIAGNOSTICS_STATUS +#ifndef MO_DIAGNOSTICS_STATUS +#define MO_DIAGNOSTICS_STATUS namespace MicroOcpp { namespace Ocpp16 { diff --git a/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp b/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp index 6258d148..7eb006e1 100644 --- a/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp +++ b/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include @@ -22,10 +22,11 @@ #define MO_IGNORE_FW_RETR_DATE 0 #endif -using namespace MicroOcpp; +using MicroOcpp::FirmwareService; using MicroOcpp::Ocpp16::FirmwareStatus; +using MicroOcpp::Request; -FirmwareService::FirmwareService(Context& context) : context(context) { +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);}); @@ -44,6 +45,20 @@ void FirmwareService::setBuildNumber(const char *buildNumber) { } void FirmwareService::loop() { + + if (ftpDownload && ftpDownload->isActive()) { + ftpDownload->loop(); + } + + if (ftpDownload) { + if (ftpDownload->isActive()) { + ftpDownload->loop(); + } else { + MO_DBG_DEBUG("Deinit FTP download"); + ftpDownload.reset(); + } + } + auto notification = getFirmwareStatusNotification(); if (notification) { context.initiateRequest(std::move(notification)); @@ -69,7 +84,7 @@ void FirmwareService::loop() { downloadIssued = true; stage = UpdateStage::AwaitDownload; timestampTransition = mocpp_tick_ms(); - delayTransition = 5000; //delay between state "Downloading" and actually starting the download + delayTransition = 2000; //delay between state "Downloading" and actually starting the download return; } } @@ -94,7 +109,7 @@ void FirmwareService::loop() { //passed download stage stage = UpdateStage::AfterDownload; } else if (downloadStatusInput() == DownloadStatus::DownloadFailed) { - MO_DBG_INFO("Download timeout or failed! Retry"); + MO_DBG_INFO("Download timeout or failed"); retreiveDate = timestampNow; retreiveDate += retryInterval; retries--; @@ -106,9 +121,7 @@ void FirmwareService::loop() { return; } else { //if client doesn't report download state, assume download to be finished (at least 30s download time have passed until here) - if (downloadStatusInput == nullptr) { - stage = UpdateStage::AfterDownload; - } + stage = UpdateStage::AfterDownload; } } @@ -123,11 +136,14 @@ void FirmwareService::loop() { } if (!ongoingTx) { - stage = UpdateStage::AwaitInstallation; - installationIssued = true; - + if (onInstall == nullptr) { + stage = UpdateStage::Installing; + } else { + stage = UpdateStage::AwaitInstallation; + } timestampTransition = mocpp_tick_ms(); - delayTransition = 10000; + delayTransition = 2000; + installationIssued = true; } return; @@ -138,13 +154,11 @@ void FirmwareService::loop() { stage = UpdateStage::Installing; if (onInstall) { - onInstall(location.c_str()); //should restart the device on success - } else { - MO_DBG_WARN("onInstall must be set! (see setOnInstall). Will abort"); - } + onInstall(location.c_str()); //may restart the device on success - timestampTransition = mocpp_tick_ms(); - delayTransition = installationStatusInput ? 1000 : 120 * 1000; + timestampTransition = mocpp_tick_ms(); + delayTransition = installationStatusInput ? 1000 : 120 * 1000; + } return; } @@ -153,9 +167,11 @@ void FirmwareService::loop() { if (installationStatusInput) { if (installationStatusInput() == InstallationStatus::Installed) { MO_DBG_INFO("FW update finished"); - //Client should reboot during onInstall. If not, client is responsible to reboot at a later point + //Charger may reboot during onInstall. If it doesn't, server will send Reset request resetStage(); - retries = 0; //End of update routine. Client must reboot on its own + retries = 0; //End of update routine + stage = UpdateStage::Installed; + location.clear(); } else if (installationStatusInput() == InstallationStatus::InstallationFailed) { MO_DBG_INFO("Installation timeout or failed! Retry"); retreiveDate = timestampNow; @@ -169,9 +185,11 @@ void FirmwareService::loop() { return; } else { MO_DBG_INFO("FW update finished"); - //Client should reboot during onInstall. If not, client is responsible to reboot at a later point + //Charger may reboot during onInstall. If it doesn't, server will send Reset request resetStage(); - retries = 0; //End of update routine. Client must reboot on its own + stage = UpdateStage::Installed; + retries = 0; //End of update routine + location.clear(); return; } } @@ -180,10 +198,19 @@ void FirmwareService::loop() { MO_DBG_ERR("Firmware update failed"); retries = 0; resetStage(); + stage = UpdateStage::InternalError; + location.clear(); } } void FirmwareService::scheduleFirmwareUpdate(const char *location, Timestamp retreiveDate, unsigned int retries, unsigned int retryInterval) { + + if (!onDownload && !onInstall) { + MO_DBG_ERR("FW service not configured"); + stage = UpdateStage::InternalError; //will send "InstallationFailed" and not proceed with update + return; + } + this->location = location; this->retreiveDate = retreiveDate; this->retries = retries; @@ -197,7 +224,6 @@ void FirmwareService::scheduleFirmwareUpdate(const char *location, Timestamp ret char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; this->retreiveDate.toJsonString(dbuf, JSONDATE_LENGTH + 1); -#if MO_DBG_LEVEL >= MO_DL_INFO MO_DBG_INFO("Scheduled FW update!\n" \ " location = %s\n" \ " retrieveDate = %s\n" \ @@ -207,7 +233,6 @@ void FirmwareService::scheduleFirmwareUpdate(const char *location, Timestamp ret dbuf, this->retries, this->retryInterval); -#endif timestampTransition = mocpp_tick_ms(); delayTransition = 1000; @@ -216,6 +241,13 @@ void FirmwareService::scheduleFirmwareUpdate(const char *location, Timestamp ret } FirmwareStatus FirmwareService::getFirmwareStatus() { + + if (stage == UpdateStage::Installed) { + return FirmwareStatus::Installed; + } else if (stage == UpdateStage::InternalError) { + return FirmwareStatus::InstallationFailed; + } + if (installationIssued) { if (installationStatusInput != nullptr) { if (installationStatusInput() == InstallationStatus::Installed) { @@ -268,7 +300,7 @@ std::unique_ptr FirmwareService::getFirmwareStatusNotification() { if (getFirmwareStatus() != lastReportedStatus) { lastReportedStatus = getFirmwareStatus(); - if (lastReportedStatus != FirmwareStatus::Idle && lastReportedStatus != FirmwareStatus::Installed) { + if (lastReportedStatus != FirmwareStatus::Idle) { auto fwNotificationMsg = new Ocpp16::FirmwareStatusNotification(lastReportedStatus); auto fwNotification = makeRequest(fwNotificationMsg); return fwNotification; @@ -300,57 +332,113 @@ void FirmwareService::resetStage() { installationIssued = false; } -#if !defined(MO_CUSTOM_UPDATER) && !defined(MO_CUSTOM_WS) -#if defined(ESP32) +void FirmwareService::setDownloadFileWriter(std::function firmwareWriter, std::function onClose) { -#include + this->onDownload = [this, firmwareWriter, onClose] (const char *location) -> bool { -FirmwareService *EspWiFi::makeFirmwareService(Context& context) { - FirmwareService *fwService = new FirmwareService(context); + auto ftpClient = context.getFtpClient(); + if (!ftpClient) { + MO_DBG_ERR("FTP client not set"); + this->ftpDownloadStatus = DownloadStatus::DownloadFailed; + return false; + } - /* - * example of how to integrate a separate download phase (optional) - */ -#if 0 //separate download phase - fwService->setOnDownload([] (const char *location) { - //download the new binary - //... - return true; - }); + this->ftpDownload = ftpClient->getFile(location, firmwareWriter, + [this, onClose] (MO_FtpCloseReason reason) -> void { + if (reason == MO_FtpCloseReason_Success) { + MO_DBG_INFO("FTP download success"); + this->ftpDownloadStatus = DownloadStatus::Downloaded; + } else { + MO_DBG_INFO("FTP download failure (%i)", reason); + this->ftpDownloadStatus = DownloadStatus::DownloadFailed; + } - fwService->setDownloadStatusInput([] () { - //report the download progress - //... - return DownloadStatus::NotDownloaded; - }); -#endif //separate download phase + onClose(reason); + }); - fwService->setOnInstall([fwService] (const char *location) { + if (this->ftpDownload) { + this->ftpDownloadStatus = DownloadStatus::NotDownloaded; + return true; + } else { + this->ftpDownloadStatus = DownloadStatus::DownloadFailed; + return false; + } + }; - fwService->setInstallationStatusInput([](){return InstallationStatus::NotInstalled;}); + this->downloadStatusInput = [this] () { + return this->ftpDownloadStatus; + }; +} - WiFiClient client; - //WiFiClientSecure client; - //client.setCACert(rootCACertificate); - client.setTimeout(60); //in seconds - - // httpUpdate.setLedPin(LED_BUILTIN, HIGH); - t_httpUpdate_return ret = httpUpdate.update(client, location); +void FirmwareService::setFtpServerCert(const char *cert) { + this->ftpServerCert = cert; +} - switch (ret) { - case HTTP_UPDATE_FAILED: - fwService->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); - MO_DBG_WARN("HTTP_UPDATE_FAILED Error (%d): %s\n", httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str()); - break; - case HTTP_UPDATE_NO_UPDATES: - fwService->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); - MO_DBG_WARN("HTTP_UPDATE_NO_UPDATES"); - break; - case HTTP_UPDATE_OK: - fwService->setInstallationStatusInput([](){return InstallationStatus::Installed;}); - MO_DBG_INFO("HTTP_UPDATE_OK"); - ESP.restart(); - break; +#if !defined(MO_CUSTOM_UPDATER) +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS + +#include + +std::unique_ptr MicroOcpp::makeDefaultFirmwareService(Context& context) { + std::unique_ptr fwService = std::unique_ptr(new FirmwareService(context)); + auto ftServicePtr = fwService.get(); + + fwService->setDownloadFileWriter( + [ftServicePtr] (const unsigned char *data, size_t size) -> size_t { + if (!Update.isRunning()) { + MO_DBG_DEBUG("start writing FW"); + MO_DBG_WARN("Built-in updater for ESP32 is only intended for demonstration purposes"); + ftServicePtr->setInstallationStatusInput([](){return InstallationStatus::NotInstalled;}); + + auto ret = Update.begin(); + if (!ret) { + MO_DBG_ERR("cannot start update: %i", ret); + return 0; + } + } + + size_t written = Update.write((uint8_t*) data, size); + + #if MO_DBG_LEVEL >= MO_DL_INFO + { + size_t progress = Update.progress(); + + bool printProgress = false; + + if (progress <= 10000) { + size_t p1k = progress / 1000; + printProgress = progress < p1k * 1000 + written && progress >= p1k * 1000; + } else if (progress <= 100000) { + size_t p10k = progress / 10000; + printProgress = progress < p10k * 10000 + written && progress >= p10k * 10000; + } else { + size_t p100k = progress / 100000; + printProgress = progress < p100k * 100000 + written && progress >= p100k * 100000; + } + + if (printProgress) { + MO_DBG_INFO("update progress: %zu kB", progress / 1000); + } + } + #endif //MO_DBG_LEVEL >= MO_DL_DEBUG + + return written; + }, [] (MO_FtpCloseReason reason) { + if (reason != MO_FtpCloseReason_Success) { + Update.abort(); + } + }); + + fwService->setOnInstall([ftServicePtr] (const char *location) { + + if (Update.isRunning() && Update.end(true)) { + MO_DBG_DEBUG("update success"); + ftServicePtr->setInstallationStatusInput([](){return InstallationStatus::Installed;}); + + ESP.restart(); + } else { + MO_DBG_ERR("update failed"); + ftServicePtr->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); } return true; @@ -363,15 +451,18 @@ FirmwareService *EspWiFi::makeFirmwareService(Context& context) { return fwService; } -#elif defined(ESP8266) +#elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP8266) #include -FirmwareService *EspWiFi::makeFirmwareService(Context& context) { - FirmwareService *fwService = new FirmwareService(context); +std::unique_ptr MicroOcpp::makeDefaultFirmwareService(Context& context) { + std::unique_ptr fwService = std::unique_ptr(new FirmwareService(context)); + auto fwServicePtr = fwService.get(); - 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; //WiFiClientSecure client; //client.setCACert(rootCACertificate); @@ -383,15 +474,15 @@ FirmwareService *EspWiFi::makeFirmwareService(Context& context) { switch (ret) { case HTTP_UPDATE_FAILED: - fwService->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); + fwServicePtr->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); MO_DBG_WARN("HTTP_UPDATE_FAILED Error (%d): %s\n", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); break; case HTTP_UPDATE_NO_UPDATES: - fwService->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); + fwServicePtr->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); MO_DBG_WARN("HTTP_UPDATE_NO_UPDATES"); break; case HTTP_UPDATE_OK: - fwService->setInstallationStatusInput([](){return InstallationStatus::Installed;}); + fwServicePtr->setInstallationStatusInput([](){return InstallationStatus::Installed;}); MO_DBG_INFO("HTTP_UPDATE_OK"); ESP.restart(); break; @@ -407,5 +498,5 @@ FirmwareService *EspWiFi::makeFirmwareService(Context& context) { return fwService; } -#endif //defined(ESP8266) -#endif //!defined(MO_CUSTOM_UPDATER) && !defined(MO_CUSTOM_WS) +#endif //MO_PLATFORM +#endif //!defined(MO_CUSTOM_UPDATER) diff --git a/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.h b/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.h index cf9376d2..5f4671f2 100644 --- a/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.h +++ b/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef FIRMWARESERVICE_H @@ -11,6 +11,8 @@ #include #include #include +#include +#include namespace MicroOcpp { @@ -29,23 +31,27 @@ enum class InstallationStatus { class Context; class Request; -class FirmwareService { +class FirmwareService : public MemoryManaged { private: Context& context; std::shared_ptr previousBuildNumberString; - std::string buildNumber; + String buildNumber; std::function downloadStatusInput; bool downloadIssued = false; + std::unique_ptr ftpDownload; + DownloadStatus ftpDownloadStatus = DownloadStatus::NotDownloaded; + const char *ftpServerCert = nullptr; + std::function installationStatusInput; bool installationIssued = false; Ocpp16::FirmwareStatus lastReportedStatus = Ocpp16::FirmwareStatus::Idle; bool checkedSuccessfulFwUpdate = false; - std::string location; + String location; Timestamp retreiveDate; unsigned int retries = 0; unsigned int retryInterval = 0; @@ -62,8 +68,10 @@ class FirmwareService { Downloading, AfterDownload, AwaitInstallation, - Installing - } stage; + Installing, + Installed, + InternalError + } stage = UpdateStage::Idle; void resetStage(); @@ -80,6 +88,22 @@ class FirmwareService { Ocpp16::FirmwareStatus getFirmwareStatus(); + /* + * Sets the firmware writer. During the UpdateFirmware process, MO will use an FTP client to download the firmware and forward + * the binary data to `firmwareWriter`. The binary data comes in chunks. MO executes `firmwareWriter` with `buf` containing the + * next chunk of FW data and `size` being the chunk size. `firmwareWriter` must return the number of bytes written, whereas + * the result can be between 1 and `size`, and 0 aborts the download. MO executes `onClose` with the reason why the connection + * has been closed. If the download hasn't been successful, MO will abort the update routine in any case. + * + * Note that this function only works if MO_ENABLE_MBEDTLS=1, or MO has been configured with a custom FTP client + */ + void setDownloadFileWriter(std::function firmwareWriter, std::function onClose); + + void setFtpServerCert(const char *cert); //zero-copy mode, i.e. cert must outlive MO + + /* + * Manual alternative for FTP download handler `setDownloadFileWriter` + */ void setOnDownload(std::function onDownload); void setDownloadStatusInput(std::function downloadStatusInput); @@ -91,18 +115,21 @@ class FirmwareService { } //endif namespace MicroOcpp -#if !defined(MO_CUSTOM_UPDATER) && !defined(MO_CUSTOM_WS) -#if defined(ESP32) || defined(ESP8266) +#if !defined(MO_CUSTOM_UPDATER) + +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS namespace MicroOcpp { -namespace EspWiFi { +std::unique_ptr makeDefaultFirmwareService(Context& context); +} -FirmwareService *makeFirmwareService(Context& context); +#elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP8266) -} +namespace MicroOcpp { +std::unique_ptr makeDefaultFirmwareService(Context& context); } -#endif //defined(ESP32) || defined(ESP8266) -#endif //!defined(MO_CUSTOM_UPDATER) && !defined(MO_CUSTOM_WS) +#endif //MO_PLATFORM +#endif //!defined(MO_CUSTOM_UPDATER) #endif diff --git a/src/MicroOcpp/Model/FirmwareManagement/FirmwareStatus.h b/src/MicroOcpp/Model/FirmwareManagement/FirmwareStatus.h index 0688af12..3c8c7c2c 100644 --- a/src/MicroOcpp/Model/FirmwareManagement/FirmwareStatus.h +++ b/src/MicroOcpp/Model/FirmwareManagement/FirmwareStatus.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef FIRMWARE_STATUS -#define FIRMWARE_STATUS +#ifndef MO_FIRMWARE_STATUS +#define MO_FIRMWARE_STATUS namespace MicroOcpp { namespace Ocpp16 { diff --git a/src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp b/src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp index 1de66747..4ebab502 100644 --- a/src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp +++ b/src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp @@ -1,18 +1,19 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include -#include +#include #include #include #include using namespace MicroOcpp; -HeartbeatService::HeartbeatService(Context& context) : context(context) { +HeartbeatService::HeartbeatService(Context& context) : MemoryManaged("v16.Heartbeat.HeartbeatService"), context(context) { heartbeatIntervalInt = declareConfiguration("HeartbeatInterval", 86400); + registerConfigurationValidator("HeartbeatInterval", VALIDATE_UNSIGNED_INT); lastHeartbeat = mocpp_tick_ms(); //Register message handler for TriggerMessage operation @@ -29,6 +30,8 @@ void HeartbeatService::loop() { lastHeartbeat = now; auto heartbeat = makeRequest(new Ocpp16::Heartbeat(context.getModel())); + // Heartbeats can not deviate more than 4s from the configured interval + heartbeat->setTimeout(std::min(4000UL, hbInterval)); context.initiateRequest(std::move(heartbeat)); } } diff --git a/src/MicroOcpp/Model/Heartbeat/HeartbeatService.h b/src/MicroOcpp/Model/Heartbeat/HeartbeatService.h index 5820612b..ea632cc8 100644 --- a/src/MicroOcpp/Model/Heartbeat/HeartbeatService.h +++ b/src/MicroOcpp/Model/Heartbeat/HeartbeatService.h @@ -1,18 +1,20 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef HEARTBEATSERVICE_H -#define HEARTBEATSERVICE_H +#ifndef MO_HEARTBEATSERVICE_H +#define MO_HEARTBEATSERVICE_H -#include #include +#include +#include + namespace MicroOcpp { class Context; -class HeartbeatService { +class HeartbeatService : public MemoryManaged { private: Context& context; diff --git a/src/MicroOcpp/Model/Metering/MeterStore.cpp b/src/MicroOcpp/Model/Metering/MeterStore.cpp index 324fbbe2..e4bc9e85 100644 --- a/src/MicroOcpp/Model/Metering/MeterStore.cpp +++ b/src/MicroOcpp/Model/Metering/MeterStore.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -9,16 +9,17 @@ #include +#ifndef MO_MAX_STOPTXDATA_LEN #define MO_MAX_STOPTXDATA_LEN 4 +#endif using namespace MicroOcpp; TransactionMeterData::TransactionMeterData(unsigned int connectorId, unsigned int txNr, std::shared_ptr filesystem) - : connectorId(connectorId), txNr(txNr), filesystem{filesystem} { + : MemoryManaged("v16.Metering.TransactionMeterData"), connectorId(connectorId), txNr(txNr), filesystem{filesystem}, txData{makeVector>(getMemoryTag())} { if (!filesystem) { MO_DBG_DEBUG("volatile mode"); - (void)0; } } @@ -82,10 +83,10 @@ bool TransactionMeterData::addTxData(std::unique_ptr mv) { return true; } -std::vector> TransactionMeterData::retrieveStopTxData() { +Vector> TransactionMeterData::retrieveStopTxData() { if (isFinalized()) { MO_DBG_ERR("Can only retrieve once"); - return decltype(txData) {}; + return makeVector>(getMemoryTag()); } finalize(); MO_DBG_DEBUG("creating sd"); @@ -111,7 +112,7 @@ bool TransactionMeterData::restore(MeterValueBuilder& mvBuilder) { return false; //all files have same length } - auto doc = FilesystemUtils::loadJson(filesystem, fn); + auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); if (!doc) { misses++; @@ -136,20 +137,19 @@ bool TransactionMeterData::restore(MeterValueBuilder& mvBuilder) { txData.push_back(std::move(mv)); - mvCount = i; i++; + mvCount = i; misses = 0; } - MO_DBG_DEBUG("Restored %zu meter values", txData.size()); + 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) : filesystem {filesystem} { +MeterStore::MeterStore(std::shared_ptr filesystem) : MemoryManaged("v16.Metering.MeterStore"), filesystem {filesystem}, txMeterData{makeVector>(getMemoryTag())} { if (!filesystem) { MO_DBG_DEBUG("volatile mode"); - (void)0; } } @@ -187,7 +187,7 @@ std::shared_ptr MeterStore::getTxMeterData(MeterValueBuild //create new object and cache weak pointer - auto tx = std::make_shared(connectorId, txNr, filesystem); + auto tx = std::allocate_shared(makeAllocator(getMemoryTag()), connectorId, txNr, filesystem); if (filesystem) { char fn [MO_MAX_PATH_SIZE] = {'\0'}; @@ -210,7 +210,7 @@ std::shared_ptr MeterStore::getTxMeterData(MeterValueBuild txMeterData.push_back(tx); - MO_DBG_DEBUG("Added txNr %u, now holding %zu txs", txNr, txMeterData.size()); + MO_DBG_DEBUG("Added txNr %u, now holding %zu sds", txNr, txMeterData.size()); return tx; } @@ -291,7 +291,6 @@ bool MeterStore::remove(unsigned int connectorId, unsigned int txNr) { if (success) { MO_DBG_DEBUG("Removed meter values for cId %u, txNr %u", connectorId, txNr); - (void)0; } else { MO_DBG_DEBUG("corrupted fs"); } diff --git a/src/MicroOcpp/Model/Metering/MeterStore.h b/src/MicroOcpp/Model/Metering/MeterStore.h index 356bf8f3..bdb67441 100644 --- a/src/MicroOcpp/Model/Metering/MeterStore.h +++ b/src/MicroOcpp/Model/Metering/MeterStore.h @@ -1,37 +1,35 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef METERSTORE_H -#define METERSTORE_H +#ifndef MO_METERSTORE_H +#define MO_METERSTORE_H #include #include #include - -#include -#include +#include namespace MicroOcpp { -class TransactionMeterData { +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 + unsigned int mvCount = 0; //nr of saved meter values, including gaps bool finalized = false; //if true, this is read-only std::shared_ptr filesystem; - std::vector> txData; + Vector> txData; public: TransactionMeterData(unsigned int connectorId, unsigned int txNr, std::shared_ptr filesystem); bool addTxData(std::unique_ptr mv); - std::vector> retrieveStopTxData(); //will invalidate internal cache + Vector> retrieveStopTxData(); //will invalidate internal cache bool restore(MeterValueBuilder& mvBuilder); //load record from memory; true if record found, false if nothing loaded @@ -42,11 +40,11 @@ class TransactionMeterData { bool isFinalized() {return finalized;} }; -class MeterStore { +class MeterStore : public MemoryManaged { private: std::shared_ptr filesystem; - std::vector> txMeterData; + Vector> txMeterData; public: MeterStore() = delete; diff --git a/src/MicroOcpp/Model/Metering/MeterValue.cpp b/src/MicroOcpp/Model/Metering/MeterValue.cpp index 45fd41ef..740905da 100644 --- a/src/MicroOcpp/Model/Metering/MeterValue.cpp +++ b/src/MicroOcpp/Model/Metering/MeterValue.cpp @@ -1,16 +1,29 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + #include #include #include using namespace MicroOcpp; -std::unique_ptr MeterValue::toJson() { +MeterValue::MeterValue(const Timestamp& timestamp) : + MemoryManaged("v16.Metering.MeterValue"), + timestamp(timestamp), + sampledValue(makeVector>(getMemoryTag())) { + +} + +void MeterValue::addSampledValue(std::unique_ptr sample) { + sampledValue.push_back(std::move(sample)); +} + +std::unique_ptr MeterValue::toJson() { size_t capacity = 0; - std::vector> entries; + auto entries = makeVector>(getMemoryTag()); for (auto sample = sampledValue.begin(); sample != sampledValue.end(); sample++) { auto json = (*sample)->toJson(); if (!json) { @@ -24,7 +37,7 @@ std::unique_ptr MeterValue::toJson() { capacity += JSONDATE_LENGTH + 1; capacity += JSON_OBJECT_SIZE(2); - auto result = std::unique_ptr(new DynamicJsonDocument(capacity + 100)); //TODO remove safety space + auto result = makeJsonDoc(getMemoryTag(), capacity); auto jsonPayload = result->to(); char timestampStr [JSONDATE_LENGTH + 1] = {'\0'}; @@ -49,18 +62,56 @@ void MeterValue::setTimestamp(Timestamp timestamp) { ReadingContext MeterValue::getReadingContext() { //all sampledValues have the same ReadingContext. Just get the first result for (auto sample = sampledValue.begin(); sample != sampledValue.end(); sample++) { - if ((*sample)->getReadingContext() != ReadingContext::NOT_SET) { + if ((*sample)->getReadingContext() != ReadingContext_UNDEFINED) { return (*sample)->getReadingContext(); } } - return ReadingContext::NOT_SET; + return ReadingContext_UNDEFINED; +} + +void MeterValue::setTxNr(unsigned int txNr) { + if (txNr > (unsigned int)std::numeric_limits::max()) { + MO_DBG_ERR("invalid arg"); + return; + } + this->txNr = (int)txNr; +} + +int MeterValue::getTxNr() { + return txNr; +} + +void MeterValue::setOpNr(unsigned int opNr) { + this->opNr = opNr; +} + +unsigned int MeterValue::getOpNr() { + return opNr; +} + +void MeterValue::advanceAttemptNr() { + attemptNr++; +} + +unsigned int MeterValue::getAttemptNr() { + return attemptNr; +} + +unsigned long MeterValue::getAttemptTime() { + return attemptTime; } -MeterValueBuilder::MeterValueBuilder(const std::vector> &samplers, +void MeterValue::setAttemptTime(unsigned long timestamp) { + this->attemptTime = timestamp; +} + +MeterValueBuilder::MeterValueBuilder(const Vector> &samplers, std::shared_ptr samplersSelectStr) : + MemoryManaged("v16.Metering.MeterValueBuilder"), samplers(samplers), - selectString(samplersSelectStr) { - + selectString(samplersSelectStr), + select_mask(makeVector(getMemoryTag())) { + updateObservedSamplers(); select_observe = selectString->getValueRevision(); } @@ -89,7 +140,7 @@ void MeterValueBuilder::updateObservedSamplers() { if (sr != sl + 1) { for (size_t i = 0; i < samplers.size(); i++) { - if (!strncmp(samplers[i]->getProperties().getMeasurand().c_str(), sstring + sl, sr - sl)) { + if (!strncmp(samplers[i]->getProperties().getMeasurand(), sstring + sl, sr - sl)) { select_mask[i] = true; select_n++; } @@ -139,11 +190,11 @@ std::unique_ptr MeterValueBuilder::deserializeSample(const JsonObjec for (JsonObject svJson : sampledValue) { //for each sampled value, search sampler with matching measurand type for (auto& sampler : samplers) { auto& properties = sampler->getProperties(); - if (!properties.getMeasurand().compare(svJson["measurand"] | "") && - !properties.getFormat().compare(svJson["format"] | "") && - !properties.getPhase().compare(svJson["phase"] | "") && - !properties.getLocation().compare(svJson["location"] | "") && - !properties.getUnit().compare(svJson["unit"] | "")) { + 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) { diff --git a/src/MicroOcpp/Model/Metering/MeterValue.h b/src/MicroOcpp/Model/Metering/MeterValue.h index 352994b3..0637ad33 100644 --- a/src/MicroOcpp/Model/Metering/MeterValue.h +++ b/src/MicroOcpp/Model/Metering/MeterValue.h @@ -1,48 +1,65 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef METERVALUE_H -#define METERVALUE_H +#ifndef MO_METERVALUE_H +#define MO_METERVALUE_H #include #include #include +#include #include #include -#include namespace MicroOcpp { -class MeterValue { +class MeterValue : public MemoryManaged { private: Timestamp timestamp; - std::vector> sampledValue; + Vector> sampledValue; + + int txNr = -1; + unsigned int opNr = 1; + unsigned int attemptNr = 0; + unsigned long attemptTime = 0; public: - MeterValue(Timestamp timestamp) : timestamp(timestamp) { } + MeterValue(const Timestamp& timestamp); MeterValue(const MeterValue& other) = delete; - void addSampledValue(std::unique_ptr sample) {sampledValue.push_back(std::move(sample));} + void addSampledValue(std::unique_ptr sample); - std::unique_ptr toJson(); + std::unique_ptr toJson(); const Timestamp& getTimestamp(); void setTimestamp(Timestamp timestamp); ReadingContext getReadingContext(); + + void setTxNr(unsigned int txNr); + int getTxNr(); + + void setOpNr(unsigned int opNr); + unsigned int getOpNr(); + + void advanceAttemptNr(); + unsigned int getAttemptNr(); + + unsigned long getAttemptTime(); + void setAttemptTime(unsigned long timestamp); }; -class MeterValueBuilder { +class MeterValueBuilder : public MemoryManaged { private: - const std::vector> &samplers; + const Vector> &samplers; std::shared_ptr selectString; - std::vector select_mask; + Vector select_mask; unsigned int select_n {0}; decltype(selectString->getValueRevision()) select_observe; void updateObservedSamplers(); public: - MeterValueBuilder(const std::vector> &samplers, + MeterValueBuilder(const Vector> &samplers, std::shared_ptr samplersSelectStr); std::unique_ptr takeSample(const Timestamp& timestamp, const ReadingContext& context); diff --git a/src/MicroOcpp/Model/Metering/MeterValuesV201.cpp b/src/MicroOcpp/Model/Metering/MeterValuesV201.cpp new file mode 100644 index 00000000..97376774 --- /dev/null +++ b/src/MicroOcpp/Model/Metering/MeterValuesV201.cpp @@ -0,0 +1,361 @@ +// 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 new file mode 100644 index 00000000..f35ec146 --- /dev/null +++ b/src/MicroOcpp/Model/Metering/MeterValuesV201.h @@ -0,0 +1,152 @@ +// 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 index fb84a24a..378232a5 100644 --- a/src/MicroOcpp/Model/Metering/MeteringConnector.cpp +++ b/src/MicroOcpp/Model/Metering/MeteringConnector.cpp @@ -1,12 +1,15 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include +#include #include +#include #include +#include #include #include #include @@ -17,33 +20,39 @@ using namespace MicroOcpp; using namespace MicroOcpp::Ocpp16; -MeteringConnector::MeteringConnector(Model& model, int connectorId, MeterStore& meterStore) - : model(model), connectorId{connectorId}, meterStore(meterStore) { +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); - meterValueCacheSizeInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "MeterValueCacheSize", 1); 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)); } -std::unique_ptr MeteringConnector::loop() { +void MeteringConnector::loop() { bool txBreak = false; if (model.getConnector(connectorId)) { @@ -56,22 +65,16 @@ std::unique_ptr MeteringConnector::loop() { lastSampleTime = mocpp_tick_ms(); } - if ((txBreak || meterData.size() >= (size_t) meterValueCacheSizeInt->getInt()) && !meterData.empty()) { - auto meterValues = std::unique_ptr(new MeterValues(std::move(meterData), connectorId, transaction)); - meterData.clear(); - return std::move(meterValues); - } - 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"); + 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()); } @@ -80,37 +83,43 @@ std::unique_ptr MeteringConnector::loop() { if (connectorId != 0 && meterValuesInTxOnlyBool->getBool()) { //don't take any MeterValues outside of transactions on connectorIds other than 0 - meterData.clear(); - return nullptr; + return; } } } - if (clockAlignedDataIntervalInt->getInt() >= 1) { + 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 + 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 %" PRId32 "s: %s", dt, + 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"? - auto alignedMeterValues = alignedDataBuilder->takeSample(model.getClock().now(), ReadingContext::SampleClock); - if (alignedMeterValues) { - meterData.push_back(std::move(alignedMeterValues)); + + 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); + 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; @@ -131,49 +140,47 @@ std::unique_ptr MeteringConnector::loop() { //record periodic tx data if (mocpp_tick_ms() - lastSampleTime >= (unsigned long) (meterValueSampleIntervalInt->getInt() * 1000)) { - auto sampleMeterValues = sampledDataBuilder->takeSample(model.getClock().now(), ReadingContext::SamplePeriodic); - if (sampleMeterValues) { - meterData.push_back(std::move(sampleMeterValues)); + 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); + auto sampleStopTx = stopTxnSampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_SamplePeriodic); if (sampleStopTx) { stopTxnData->addTxData(std::move(sampleStopTx)); } } lastSampleTime = mocpp_tick_ms(); - } - } - - if (clockAlignedDataIntervalInt->getInt() < 1 && meterValueSampleIntervalInt->getInt() < 1) { - meterData.clear(); + } } - - return nullptr; //successful method completition. Currently there is no reason to send a MeterValues Msg. } std::unique_ptr MeteringConnector::takeTriggeredMeterValues() { - auto sample = sampledDataBuilder->takeSample(model.getClock().now(), ReadingContext::Trigger); + auto sample = sampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_Trigger); if (!sample) { return nullptr; } - decltype(meterData) mv_now; - mv_now.push_back(std::move(sample)); - std::shared_ptr transaction = nullptr; if (model.getConnector(connectorId)) { transaction = model.getConnector(connectorId)->getTransaction(); } - return std::unique_ptr(new MeterValues(std::move(mv_now), connectorId, transaction)); + return std::unique_ptr(new MeterValues(model, std::move(sample), connectorId, transaction)); } void MeteringConnector::addMeterValueSampler(std::unique_ptr meterValueSampler) { - if (!meterValueSampler->getProperties().getMeasurand().compare("Energy.Active.Import.Register")) { + if (!strcmp(meterValueSampler->getProperties().getMeasurand(), "Energy.Active.Import.Register")) { energySamplerIndex = samplers.size(); } samplers.push_back(std::move(meterValueSampler)); @@ -194,7 +201,7 @@ void MeteringConnector::beginTxMeterData(Transaction *transaction) { } if (stopTxnData) { - auto sampleTxBegin = stopTxnSampledDataBuilder->takeSample(model.getClock().now(), ReadingContext::TransactionBegin); + auto sampleTxBegin = stopTxnSampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_TransactionBegin); if (sampleTxBegin) { stopTxnData->addTxData(std::move(sampleTxBegin)); } @@ -207,7 +214,7 @@ std::shared_ptr MeteringConnector::endTxMeterData(Transact } if (stopTxnData) { - auto sampleTxEnd = stopTxnSampledDataBuilder->takeSample(model.getClock().now(), ReadingContext::TransactionEnd); + auto sampleTxEnd = stopTxnSampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_TransactionEnd); if (sampleTxEnd) { stopTxnData->addTxData(std::move(sampleTxEnd)); } @@ -216,6 +223,10 @@ std::shared_ptr MeteringConnector::endTxMeterData(Transact return std::move(stopTxnData); } +void MeteringConnector::abortTxMeterData() { + stopTxnData.reset(); +} + std::shared_ptr MeteringConnector::getStopTxMeterData(Transaction *transaction) { auto txData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction); @@ -229,11 +240,65 @@ std::shared_ptr MeteringConnector::getStopTxMeterData(Tran bool MeteringConnector::existsSampler(const char *measurand, size_t len) { for (size_t i = 0; i < samplers.size(); i++) { - if (samplers[i]->getProperties().getMeasurand().length() == len && - !strncmp(measurand, samplers[i]->getProperties().getMeasurand().c_str(), len)) { + 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 index 72ab1d82..75873ff6 100644 --- a/src/MicroOcpp/Model/Metering/MeteringConnector.h +++ b/src/MicroOcpp/Model/Metering/MeteringConnector.h @@ -1,33 +1,40 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef METERING_CONNECTOR_H -#define METERING_CONNECTOR_H +#ifndef MO_METERING_CONNECTOR_H +#define MO_METERING_CONNECTOR_H #include #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 Transaction; class MeterStore; -class MeteringConnector { +class MeteringConnector : public MemoryManaged, public RequestEmitter { private: + Context& context; Model& model; const int connectorId; MeterStore& meterStore; - std::vector> meterData; + Vector> meterData; + std::unique_ptr meterDataFront; std::shared_ptr stopTxnData; std::unique_ptr sampledDataBuilder; @@ -45,20 +52,22 @@ class MeteringConnector { std::shared_ptr transaction; bool trackTxRunning = false; - std::vector> samplers; + Vector> samplers; int energySamplerIndex {-1}; std::shared_ptr meterValueSampleIntervalInt; - std::shared_ptr meterValueCacheSizeInt; std::shared_ptr clockAlignedDataIntervalInt; std::shared_ptr meterValuesInTxOnlyBool; std::shared_ptr stopTxnDataCapturePeriodicBool; + + std::shared_ptr transactionMessageAttemptsInt; + std::shared_ptr transactionMessageRetryIntervalInt; public: - MeteringConnector(Model& model, int connectorId, MeterStore& meterStore); + MeteringConnector(Context& context, int connectorId, MeterStore& meterStore); - std::unique_ptr loop(); + void loop(); void addMeterValueSampler(std::unique_ptr meterValueSampler); @@ -70,10 +79,16 @@ class MeteringConnector { 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 diff --git a/src/MicroOcpp/Model/Metering/MeteringService.cpp b/src/MicroOcpp/Model/Metering/MeteringService.cpp index b2bc7373..73ab4deb 100644 --- a/src/MicroOcpp/Model/Metering/MeteringService.cpp +++ b/src/MicroOcpp/Model/Metering/MeteringService.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -7,14 +7,14 @@ #include #include #include -#include +#include #include #include using namespace MicroOcpp; MeteringService::MeteringService(Context& context, int numConn, std::shared_ptr filesystem) - : context(context), meterStore(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"); @@ -22,8 +22,9 @@ MeteringService::MeteringService(Context& context, int numConn, std::shared_ptr< 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.getModel(), i, meterStore)); + connectors.emplace_back(new MeteringConnector(context, i, meterStore)); } std::function validateSelectString = [this] (const char *csl) { @@ -55,29 +56,26 @@ MeteringService::MeteringService(Context& context, int numConn, std::shared_ptr< } return isValid; }; + registerConfigurationValidator("MeterValuesSampledData", validateSelectString); registerConfigurationValidator("StopTxnSampledData", validateSelectString); registerConfigurationValidator("MeterValuesAlignedData", validateSelectString); registerConfigurationValidator("StopTxnAlignedData", validateSelectString); + registerConfigurationValidator("MeterValueSampleInterval", VALIDATE_UNSIGNED_INT); + registerConfigurationValidator("ClockAlignedDataInterval", VALIDATE_UNSIGNED_INT); /* * Register further message handlers to support echo mode: when this library * is connected with a WebSocket echo server, let it reply to its own requests. * Mocking an OCPP Server on the same device makes running (unit) tests easier. */ - context.getOperationRegistry().registerOperation("MeterValues", [] () { - return new Ocpp16::MeterValues();}); + context.getOperationRegistry().registerOperation("MeterValues", [this] () { + return new Ocpp16::MeterValues(this->context.getModel());}); } void MeteringService::loop(){ - for (unsigned int i = 0; i < connectors.size(); i++){ - auto meterValuesMsg = connectors[i]->loop(); - if (meterValuesMsg != nullptr) { - auto meterValues = makeRequest(std::move(meterValuesMsg)); - meterValues->setTimeout(120000); - context.initiateRequest(std::move(meterValues)); - } + connectors[i]->loop(); } } @@ -102,18 +100,14 @@ std::unique_ptr MeteringService::takeTriggeredMeterValues(int connector MO_DBG_ERR("connectorId out of bounds. Ignore"); return nullptr; } - auto& connector = connectors.at(connectorId); - if (connector.get()) { - auto msg = connector->takeTriggeredMeterValues(); - if (msg) { - auto meterValues = makeRequest(std::move(msg)); - meterValues->setTimeout(120000); - return meterValues; - } - MO_DBG_DEBUG("Did not take any samples for connectorId %d", connectorId); - return nullptr; + + auto msg = connectors[connectorId]->takeTriggeredMeterValues(); + if (msg) { + auto meterValues = makeRequest(std::move(msg)); + meterValues->setTimeout(120000); + return meterValues; } - MO_DBG_ERR("Could not find connector"); + MO_DBG_DEBUG("Did not take any samples for connectorId %d", connectorId); return nullptr; } @@ -127,9 +121,7 @@ void MeteringService::beginTxMeterData(Transaction *transaction) { MO_DBG_ERR("connectorId is out of bounds"); return; } - auto& connector = connectors[connectorId]; - - connector->beginTxMeterData(transaction); + connectors[connectorId]->beginTxMeterData(transaction); } std::shared_ptr MeteringService::endTxMeterData(Transaction *transaction) { @@ -142,9 +134,15 @@ std::shared_ptr MeteringService::endTxMeterData(Transactio MO_DBG_ERR("connectorId is out of bounds"); return nullptr; } - auto& connector = connectors[connectorId]; + return connectors[connectorId]->endTxMeterData(transaction); +} - return connector->endTxMeterData(transaction); +void MeteringService::abortTxMeterData(unsigned int connectorId) { + if (connectorId >= connectors.size()) { + MO_DBG_ERR("connectorId is out of bounds"); + return; + } + connectors[connectorId]->abortTxMeterData(); } std::shared_ptr MeteringService::getStopTxMeterData(Transaction *transaction) { @@ -157,9 +155,7 @@ std::shared_ptr MeteringService::getStopTxMeterData(Transa MO_DBG_ERR("connectorId is out of bounds"); return nullptr; } - auto& connector = connectors[connectorId]; - - return connector->getStopTxMeterData(transaction); + return connectors[connectorId]->getStopTxMeterData(transaction); } bool MeteringService::removeTxMeterData(unsigned int connectorId, unsigned int txNr) { diff --git a/src/MicroOcpp/Model/Metering/MeteringService.h b/src/MicroOcpp/Model/Metering/MeteringService.h index 0f2f7afc..748a0b4a 100644 --- a/src/MicroOcpp/Model/Metering/MeteringService.h +++ b/src/MicroOcpp/Model/Metering/MeteringService.h @@ -1,17 +1,17 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef METERINGSERVICE_H -#define METERINGSERVICE_H +#ifndef MO_METERINGSERVICE_H +#define MO_METERINGSERVICE_H #include -#include #include #include #include #include +#include namespace MicroOcpp { @@ -19,12 +19,12 @@ class Context; class Request; class FilesystemAdapter; -class MeteringService { +class MeteringService : public MemoryManaged { private: Context& context; MeterStore meterStore; - std::vector> connectors; + Vector> connectors; public: MeteringService(Context& context, int numConnectors, std::shared_ptr filesystem); @@ -40,6 +40,8 @@ class MeteringService { std::shared_ptr endTxMeterData(Transaction *transaction); //use return value to keep data in cache + void abortTxMeterData(unsigned int connectorId); //call this to free resources if txMeterData record is not ended normally. Does not remove files + std::shared_ptr getStopTxMeterData(Transaction *transaction); //prefer endTxMeterData when possible bool removeTxMeterData(unsigned int connectorId, unsigned int txNr); diff --git a/src/MicroOcpp/Model/Metering/ReadingContext.cpp b/src/MicroOcpp/Model/Metering/ReadingContext.cpp new file mode 100644 index 00000000..2ea24886 --- /dev/null +++ b/src/MicroOcpp/Model/Metering/ReadingContext.cpp @@ -0,0 +1,65 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include +#include + +namespace MicroOcpp { + +const char *serializeReadingContext(ReadingContext context) { + switch (context) { + case (ReadingContext_InterruptionBegin): + return "Interruption.Begin"; + case (ReadingContext_InterruptionEnd): + return "Interruption.End"; + case (ReadingContext_Other): + return "Other"; + case (ReadingContext_SampleClock): + return "Sample.Clock"; + case (ReadingContext_SamplePeriodic): + return "Sample.Periodic"; + case (ReadingContext_TransactionBegin): + return "Transaction.Begin"; + case (ReadingContext_TransactionEnd): + return "Transaction.End"; + case (ReadingContext_Trigger): + return "Trigger"; + default: + MO_DBG_ERR("ReadingContext not specified"); + /* fall through */ + case (ReadingContext_UNDEFINED): + return ""; + } +} +ReadingContext deserializeReadingContext(const char *context) { + if (!context) { + MO_DBG_ERR("Invalid argument"); + return ReadingContext_UNDEFINED; + } + + if (!strcmp(context, "Sample.Periodic")) { + return ReadingContext_SamplePeriodic; + } else if (!strcmp(context, "Sample.Clock")) { + return ReadingContext_SampleClock; + } else if (!strcmp(context, "Transaction.Begin")) { + return ReadingContext_TransactionBegin; + } else if (!strcmp(context, "Transaction.End")) { + return ReadingContext_TransactionEnd; + } else if (!strcmp(context, "Other")) { + return ReadingContext_Other; + } else if (!strcmp(context, "Interruption.Begin")) { + return ReadingContext_InterruptionBegin; + } else if (!strcmp(context, "Interruption.End")) { + return ReadingContext_InterruptionEnd; + } else if (!strcmp(context, "Trigger")) { + return ReadingContext_Trigger; + } + + MO_DBG_ERR("ReadingContext not specified %.10s", context); + return ReadingContext_UNDEFINED; +} + +} //namespace MicroOcpp diff --git a/src/MicroOcpp/Model/Metering/ReadingContext.h b/src/MicroOcpp/Model/Metering/ReadingContext.h new file mode 100644 index 00000000..592914e1 --- /dev/null +++ b/src/MicroOcpp/Model/Metering/ReadingContext.h @@ -0,0 +1,28 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_READINGCONTEXT_H +#define MO_READINGCONTEXT_H + +typedef enum { + ReadingContext_UNDEFINED, + ReadingContext_InterruptionBegin, + ReadingContext_InterruptionEnd, + ReadingContext_Other, + ReadingContext_SampleClock, + ReadingContext_SamplePeriodic, + ReadingContext_TransactionBegin, + ReadingContext_TransactionEnd, + ReadingContext_Trigger +} ReadingContext; + +#ifdef __cplusplus + +namespace MicroOcpp { +const char *serializeReadingContext(ReadingContext context); +ReadingContext deserializeReadingContext(const char *serialized); +} + +#endif +#endif diff --git a/src/MicroOcpp/Model/Metering/SampledValue.cpp b/src/MicroOcpp/Model/Metering/SampledValue.cpp index c08ce851..4168a048 100644 --- a/src/MicroOcpp/Model/Metering/SampledValue.cpp +++ b/src/MicroOcpp/Model/Metering/SampledValue.cpp @@ -1,111 +1,57 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// 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); } -std::string SampledValueDeSerializer::serialize(int32_t& val) { +MicroOcpp::String SampledValueDeSerializer::serialize(int32_t& val) { char str [12] = {'\0'}; snprintf(str, 12, "%" PRId32, val); - return std::string(str); -} - -//helper function -namespace MicroOcpp { -namespace Ocpp16 { -const char *serializeReadingContext(ReadingContext context) { - switch (context) { - case (ReadingContext::InterruptionBegin): - return "Interruption.Begin"; - case (ReadingContext::InterruptionEnd): - return "Interruption.End"; - case (ReadingContext::Other): - return "Other"; - case (ReadingContext::SampleClock): - return "Sample.Clock"; - case (ReadingContext::SamplePeriodic): - return "Sample.Periodic"; - case (ReadingContext::TransactionBegin): - return "Transaction.Begin"; - case (ReadingContext::TransactionEnd): - return "Transaction.End"; - case (ReadingContext::Trigger): - return "Trigger"; - default: - MO_DBG_ERR("ReadingContext not specified"); - /* fall through */ - case (ReadingContext::NOT_SET): - return nullptr; - } + return makeString("v16.Metering.SampledValueDeSerializer", str); } -ReadingContext deserializeReadingContext(const char *context) { - if (!context) { - MO_DBG_ERR("Invalid argument"); - return ReadingContext::NOT_SET; - } - - if (!strcmp(context, "NOT_SET")) { - MO_DBG_DEBUG("Deserialize Null-ReadingContext"); - return ReadingContext::NOT_SET; - } else if (!strcmp(context, "Sample.Periodic")) { - return ReadingContext::SamplePeriodic; - } else if (!strcmp(context, "Sample.Clock")) { - return ReadingContext::SampleClock; - } else if (!strcmp(context, "Transaction.Begin")) { - return ReadingContext::TransactionBegin; - } else if (!strcmp(context, "Transaction.End")) { - return ReadingContext::TransactionEnd; - } else if (!strcmp(context, "Other")) { - return ReadingContext::Other; - } else if (!strcmp(context, "Interruption.Begin")) { - return ReadingContext::InterruptionBegin; - } else if (!strcmp(context, "Interruption.End")) { - return ReadingContext::InterruptionEnd; - } else if (!strcmp(context, "Trigger")) { - return ReadingContext::Trigger; - } - MO_DBG_ERR("ReadingContext not specified %.10s", context); - return ReadingContext::NOT_SET; +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); } -}} //end namespaces -std::unique_ptr SampledValue::toJson() { +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 - + properties.getFormat().length() + 1 - + properties.getMeasurand().length() + 1 - + properties.getPhase().length() + 1 - + properties.getLocation().length() + 1 - + properties.getUnit().length() + 1; - auto result = std::unique_ptr(new DynamicJsonDocument(capacity + 100)); //TODO remove safety space + capacity += value.length() + 1; + auto result = makeJsonDoc("v16.Metering.SampledValue", capacity); auto payload = result->to(); payload["value"] = value; - auto context_cstr = Ocpp16::serializeReadingContext(context); + auto context_cstr = serializeReadingContext(context); if (context_cstr) payload["context"] = context_cstr; - if (!properties.getFormat().empty()) + if (*properties.getFormat()) payload["format"] = properties.getFormat(); - if (!properties.getMeasurand().empty()) + if (*properties.getMeasurand()) payload["measurand"] = properties.getMeasurand(); - if (!properties.getPhase().empty()) + if (*properties.getPhase()) payload["phase"] = properties.getPhase(); - if (!properties.getLocation().empty()) + if (*properties.getLocation()) payload["location"] = properties.getLocation(); - if (!properties.getUnit().empty()) + if (*properties.getUnit()) payload["unit"] = properties.getUnit(); return result; } diff --git a/src/MicroOcpp/Model/Metering/SampledValue.h b/src/MicroOcpp/Model/Metering/SampledValue.h index 5f12b4b8..350990ae 100644 --- a/src/MicroOcpp/Model/Metering/SampledValue.h +++ b/src/MicroOcpp/Model/Metering/SampledValue.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef SAMPLEDVALUE_H @@ -9,6 +9,8 @@ #include #include +#include +#include #include namespace MicroOcpp { @@ -18,7 +20,7 @@ class SampledValueDeSerializer { public: static T deserialize(const char *str); static bool ready(T& val); - static std::string serialize(T& val); + static String serialize(T& val); static int32_t toInteger(T& val); }; @@ -27,7 +29,7 @@ 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 std::string serialize(int32_t& val); + static String serialize(int32_t& val); static int32_t toInteger(int32_t& val) {return val;} //no conversion required }; @@ -36,24 +38,25 @@ 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 std::string serialize(float& val) { - char str[20]; - dtostrf(val,4,1,str); - return std::string(str); - } + static String serialize(float& val); static int32_t toInteger(float& val) {return (int32_t) val;} }; class SampledValueProperties { private: - std::string format; - std::string measurand; - std::string phase; - std::string location; - std::string unit; + String format; + String measurand; + String phase; + String location; + String unit; public: - SampledValueProperties() { } + 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), @@ -63,45 +66,28 @@ class SampledValueProperties { ~SampledValueProperties() = default; void setFormat(const char *format) {this->format = format;} - const std::string& getFormat() const {return format;} + const char *getFormat() const {return format.c_str();} void setMeasurand(const char *measurand) {this->measurand = measurand;} - const std::string& getMeasurand() const {return measurand;} + const char *getMeasurand() const {return measurand.c_str();} void setPhase(const char *phase) {this->phase = phase;} - const std::string& getPhase() const {return phase;} + const char *getPhase() const {return phase.c_str();} void setLocation(const char *location) {this->location = location;} - const std::string& getLocation() const {return location;} + const char *getLocation() const {return location.c_str();} void setUnit(const char *unit) {this->unit = unit;} - const std::string& getUnit() const {return unit;} -}; - -enum class ReadingContext { - InterruptionBegin, - InterruptionEnd, - Other, - SampleClock, - SamplePeriodic, - TransactionBegin, - TransactionEnd, - Trigger, - NOT_SET + const char *getUnit() const {return unit.c_str();} }; -namespace Ocpp16 { -const char *serializeReadingContext(ReadingContext context); -ReadingContext deserializeReadingContext(const char *serialized); -} - class SampledValue { protected: const SampledValueProperties& properties; const ReadingContext context; - virtual std::string serializeValue() = 0; + 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(); + std::unique_ptr toJson(); virtual operator bool() = 0; virtual int32_t toInteger() = 0; @@ -110,17 +96,17 @@ class SampledValue { }; template -class SampledValueConcrete : public SampledValue { +class SampledValueConcrete : public SampledValue, public MemoryManaged { private: T value; public: - SampledValueConcrete(const SampledValueProperties& properties, ReadingContext context, const T&& value) : SampledValue(properties, context), value(value) { } - SampledValueConcrete(const SampledValueConcrete& other) : SampledValue(other), value(other.value) { } + 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);} - std::string serializeValue() override {return DeSerializer::serialize(value);} + String serializeValue() override {return DeSerializer::serialize(value);} int32_t toInteger() override { return DeSerializer::toInteger(value);} }; @@ -137,11 +123,11 @@ class SampledValueSampler { }; template -class SampledValueSamplerConcrete : public SampledValueSampler { +class SampledValueSamplerConcrete : public SampledValueSampler, public MemoryManaged { private: std::function sampler; public: - SampledValueSamplerConcrete(SampledValueProperties properties, std::function sampler) : SampledValueSampler(properties), sampler(sampler) { } + 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, @@ -151,7 +137,7 @@ class SampledValueSamplerConcrete : public SampledValueSampler { std::unique_ptr deserializeValue(JsonObject svJson) override { return std::unique_ptr>(new SampledValueConcrete( properties, - Ocpp16::deserializeReadingContext(svJson["context"] | "NOT_SET"), + deserializeReadingContext(svJson["context"] | "NOT_SET"), DeSerializer::deserialize(svJson["value"] | ""))); } }; diff --git a/src/MicroOcpp/Model/Model.cpp b/src/MicroOcpp/Model/Model.cpp index f0bd7beb..fe59568f 100644 --- a/src/MicroOcpp/Model/Model.cpp +++ b/src/MicroOcpp/Model/Model.cpp @@ -1,15 +1,14 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include -#include - #include #include #include #include +#include #include #include #include @@ -17,6 +16,11 @@ #include #include #include +#include +#include +#include +#include +#include #include @@ -24,7 +28,7 @@ using namespace MicroOcpp; -Model::Model(uint16_t bootNr) : bootNr(bootNr) { +Model::Model(ProtocolVersion version, uint16_t bootNr) : MemoryManaged("Model"), connectors(makeVector>(getMemoryTag())), version(version), bootNr(bootNr) { } @@ -51,27 +55,40 @@ void Model::loop() { if (chargeControlCommon) chargeControlCommon->loop(); - + if (smartChargingService) smartChargingService->loop(); - + if (heartbeatService) heartbeatService->loop(); - + if (meteringService) meteringService->loop(); - + if (diagnosticsService) diagnosticsService->loop(); - + if (firmwareService) firmwareService->loop(); - + +#if MO_ENABLE_RESERVATION if (reservationService) reservationService->loop(); - +#endif //MO_ENABLE_RESERVATION + if (resetService) resetService->loop(); + +#if MO_ENABLE_V201 + if (availabilityService) + availabilityService->loop(); + + if (transactionService) + transactionService->loop(); + + if (resetServiceV201) + resetServiceV201->loop(); +#endif } void Model::setTransactionStore(std::unique_ptr ts) { @@ -101,7 +118,7 @@ ConnectorsCommon *Model::getConnectorsCommon() { return chargeControlCommon.get(); } -void Model::setConnectors(std::vector>&& connectors) { +void Model::setConnectors(Vector>&& connectors) { this->connectors = std::move(connectors); capabilitiesUpdated = true; } @@ -151,6 +168,7 @@ void Model::setHeartbeatService(std::unique_ptr hs) { capabilitiesUpdated = true; } +#if MO_ENABLE_LOCAL_AUTH void Model::setAuthorizationService(std::unique_ptr as) { authorizationService = std::move(as); capabilitiesUpdated = true; @@ -159,7 +177,9 @@ void Model::setAuthorizationService(std::unique_ptr as) { AuthorizationService *Model::getAuthorizationService() { return authorizationService.get(); } +#endif //MO_ENABLE_LOCAL_AUTH +#if MO_ENABLE_RESERVATION void Model::setReservationService(std::unique_ptr rs) { reservationService = std::move(rs); capabilitiesUpdated = true; @@ -168,6 +188,7 @@ void Model::setReservationService(std::unique_ptr rs) { ReservationService *Model::getReservationService() { return reservationService.get(); } +#endif //MO_ENABLE_RESERVATION void Model::setBootService(std::unique_ptr bs){ bootService = std::move(bs); @@ -187,10 +208,81 @@ ResetService *Model::getResetService() const { return resetService.get(); } +#if MO_ENABLE_CERT_MGMT +void Model::setCertificateService(std::unique_ptr cs) { + this->certService = std::move(cs); + capabilitiesUpdated = true; +} + +CertificateService *Model::getCertificateService() const { + return certService.get(); +} +#endif //MO_ENABLE_CERT_MGMT + +#if MO_ENABLE_V201 +void Model::setAvailabilityService(std::unique_ptr as) { + this->availabilityService = std::move(as); + capabilitiesUpdated = true; +} + +AvailabilityService *Model::getAvailabilityService() const { + return availabilityService.get(); +} + +void Model::setVariableService(std::unique_ptr vs) { + this->variableService = std::move(vs); + capabilitiesUpdated = true; +} + +VariableService *Model::getVariableService() const { + return variableService.get(); +} + +void Model::setTransactionService(std::unique_ptr ts) { + this->transactionService = std::move(ts); + capabilitiesUpdated = true; +} + +TransactionService *Model::getTransactionService() const { + return transactionService.get(); +} + +void Model::setResetServiceV201(std::unique_ptr rs) { + this->resetServiceV201 = std::move(rs); + capabilitiesUpdated = true; +} + +Ocpp201::ResetService *Model::getResetServiceV201() const { + return resetServiceV201.get(); +} + +void Model::setMeteringServiceV201(std::unique_ptr rs) { + this->meteringServiceV201 = std::move(rs); + capabilitiesUpdated = true; +} + +Ocpp201::MeteringService *Model::getMeteringServiceV201() const { + return meteringServiceV201.get(); +} + +void Model::setRemoteControlService(std::unique_ptr rs) { + remoteControlService = std::move(rs); + capabilitiesUpdated = true; +} + +RemoteControlService *Model::getRemoteControlService() const { + return remoteControlService.get(); +} +#endif + Clock& Model::getClock() { return clock; } +const ProtocolVersion& Model::getVersion() const { + return version; +} + uint16_t Model::getBootNr() { return bootNr; } @@ -205,7 +297,7 @@ void Model::updateSupportedStandardProfiles() { return; } - std::string buf = supportedFeatureProfilesString->getString(); + auto buf = makeString(getMemoryTag(), supportedFeatureProfilesString->getString()); if (chargeControlCommon && heartbeatService && @@ -216,7 +308,7 @@ void Model::updateSupportedStandardProfiles() { } } - if (firmwareService && + if (firmwareService || diagnosticsService) { if (!strstr(supportedFeatureProfilesString->getString(), "FirmwareManagement")) { if (!buf.empty()) buf += ','; @@ -224,19 +316,23 @@ void Model::updateSupportedStandardProfiles() { } } - if (authorizationService) { +#if MO_ENABLE_LOCAL_AUTH + if (authorizationService && authorizationService->localAuthListEnabled()) { if (!strstr(supportedFeatureProfilesString->getString(), "LocalAuthListManagement")) { if (!buf.empty()) buf += ','; buf += "LocalAuthListManagement"; } } +#endif //MO_ENABLE_LOCAL_AUTH +#if MO_ENABLE_RESERVATION if (reservationService) { if (!strstr(supportedFeatureProfilesString->getString(), "Reservation")) { if (!buf.empty()) buf += ','; buf += "Reservation"; } } +#endif //MO_ENABLE_RESERVATION if (smartChargingService) { if (!strstr(supportedFeatureProfilesString->getString(), "SmartCharging")) { diff --git a/src/MicroOcpp/Model/Model.h b/src/MicroOcpp/Model/Model.h index 1273153f..ff31bdb2 100644 --- a/src/MicroOcpp/Model/Model.h +++ b/src/MicroOcpp/Model/Model.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_MODEL_H @@ -8,6 +8,8 @@ #include #include +#include +#include #include namespace MicroOcpp { @@ -19,14 +21,36 @@ class MeteringService; class FirmwareService; class DiagnosticsService; class HeartbeatService; +class BootService; +class ResetService; + +#if MO_ENABLE_LOCAL_AUTH class AuthorizationService; +#endif //MO_ENABLE_LOCAL_AUTH + +#if MO_ENABLE_RESERVATION class ReservationService; -class BootService; +#endif //MO_ENABLE_RESERVATION + +#if MO_ENABLE_CERT_MGMT +class CertificateService; +#endif //MO_ENABLE_CERT_MGMT + +#if MO_ENABLE_V201 +class AvailabilityService; +class VariableService; +class TransactionService; +class RemoteControlService; + +namespace Ocpp201 { class ResetService; +class MeteringService; +} +#endif //MO_ENABLE_V201 -class Model { +class Model : public MemoryManaged { private: - std::vector> connectors; + Vector> connectors; std::unique_ptr transactionStore; std::unique_ptr smartChargingService; std::unique_ptr chargeControlCommon; @@ -34,12 +58,34 @@ class Model { std::unique_ptr firmwareService; std::unique_ptr diagnosticsService; std::unique_ptr heartbeatService; - std::unique_ptr authorizationService; - std::unique_ptr reservationService; std::unique_ptr bootService; std::unique_ptr resetService; + +#if MO_ENABLE_LOCAL_AUTH + std::unique_ptr authorizationService; +#endif //MO_ENABLE_LOCAL_AUTH + +#if MO_ENABLE_RESERVATION + std::unique_ptr reservationService; +#endif //MO_ENABLE_RESERVATION + +#if MO_ENABLE_CERT_MGMT + std::unique_ptr certService; +#endif //MO_ENABLE_CERT_MGMT + +#if MO_ENABLE_V201 + std::unique_ptr availabilityService; + std::unique_ptr variableService; + std::unique_ptr transactionService; + std::unique_ptr resetServiceV201; + std::unique_ptr meteringServiceV201; + std::unique_ptr remoteControlService; +#endif + Clock clock; + ProtocolVersion version; + bool capabilitiesUpdated = true; void updateSupportedStandardProfiles(); @@ -48,7 +94,7 @@ class Model { const uint16_t bootNr = 0; //each boot of this lib has a unique number public: - Model(uint16_t bootNr = 0); + Model(ProtocolVersion version = ProtocolVersion(1,6), uint16_t bootNr = 0); Model(const Model& rhs) = delete; ~Model(); @@ -65,7 +111,7 @@ class Model { void setConnectorsCommon(std::unique_ptr ccs); ConnectorsCommon *getConnectorsCommon(); - void setConnectors(std::vector>&& connectors); + void setConnectors(Vector>&& connectors); unsigned int getNumConnectors() const; Connector *getConnector(unsigned int connectorId); @@ -80,11 +126,15 @@ class Model { void setHeartbeatService(std::unique_ptr heartbeatService); +#if MO_ENABLE_LOCAL_AUTH void setAuthorizationService(std::unique_ptr authorizationService); AuthorizationService *getAuthorizationService(); +#endif //MO_ENABLE_LOCAL_AUTH +#if MO_ENABLE_RESERVATION void setReservationService(std::unique_ptr reservationService); ReservationService *getReservationService(); +#endif //MO_ENABLE_RESERVATION void setBootService(std::unique_ptr bs); BootService *getBootService() const; @@ -92,8 +142,35 @@ class Model { void setResetService(std::unique_ptr rs); ResetService *getResetService() const; +#if MO_ENABLE_CERT_MGMT + void setCertificateService(std::unique_ptr cs); + CertificateService *getCertificateService() const; +#endif //MO_ENABLE_CERT_MGMT + +#if MO_ENABLE_V201 + void setAvailabilityService(std::unique_ptr as); + AvailabilityService *getAvailabilityService() const; + + void setVariableService(std::unique_ptr vs); + VariableService *getVariableService() const; + + void setTransactionService(std::unique_ptr ts); + TransactionService *getTransactionService() const; + + void setResetServiceV201(std::unique_ptr rs); + Ocpp201::ResetService *getResetServiceV201() const; + + void setMeteringServiceV201(std::unique_ptr ms); + Ocpp201::MeteringService *getMeteringServiceV201() const; + + void setRemoteControlService(std::unique_ptr rs); + RemoteControlService *getRemoteControlService() const; +#endif + Clock &getClock(); + const ProtocolVersion& getVersion() const; + uint16_t getBootNr(); }; diff --git a/src/MicroOcpp/Model/RemoteControl/RemoteControlDefs.h b/src/MicroOcpp/Model/RemoteControl/RemoteControlDefs.h new file mode 100644 index 00000000..2edc83e3 --- /dev/null +++ b/src/MicroOcpp/Model/RemoteControl/RemoteControlDefs.h @@ -0,0 +1,34 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_UNLOCKCONNECTOR_H +#define MO_UNLOCKCONNECTOR_H + +#include + +#if MO_ENABLE_V201 + +#include + +#include + +typedef enum { + RequestStartStopStatus_Accepted, + RequestStartStopStatus_Rejected +} RequestStartStopStatus; + +#if MO_ENABLE_CONNECTOR_LOCK + +typedef enum { + UnlockStatus_Unlocked, + UnlockStatus_UnlockFailed, + UnlockStatus_OngoingAuthorizedTransaction, + UnlockStatus_UnknownConnector, + UnlockStatus_PENDING // unlock action not finished yet, result still unknown (MO will check again later) +} UnlockStatus; + +#endif // MO_ENABLE_CONNECTOR_LOCK + +#endif // MO_ENABLE_V201 +#endif // MO_UNLOCKCONNECTOR_H diff --git a/src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp b/src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp new file mode 100644 index 00000000..74aefe0f --- /dev/null +++ b/src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp @@ -0,0 +1,173 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace MicroOcpp; + +RemoteControlServiceEvse::RemoteControlServiceEvse(Context& context, unsigned int evseId) : MemoryManaged("v201.RemoteControl.RemoteControlServiceEvse"), context(context), evseId(evseId) { + +} + +#if MO_ENABLE_CONNECTOR_LOCK +void RemoteControlServiceEvse::setOnUnlockConnector(UnlockConnectorResult (*onUnlockConnector)(unsigned int evseId, void *userData), void *userData) { + this->onUnlockConnector = onUnlockConnector; + this->onUnlockConnectorUserData = userData; +} + +UnlockStatus RemoteControlServiceEvse::unlockConnector() { + + if (!onUnlockConnector) { + return UnlockStatus_UnlockFailed; + } + + if (auto txService = context.getModel().getTransactionService()) { + if (auto evse = txService->getEvse(evseId)) { + if (auto tx = evse->getTransaction()) { + if (tx->started && !tx->stopped && tx->isAuthorized) { + return UnlockStatus_OngoingAuthorizedTransaction; + } else { + evse->abortTransaction(Ocpp201::Transaction::StoppedReason::Other,Ocpp201::TransactionEventTriggerReason::UnlockCommand); + } + } + } + } + + auto status = onUnlockConnector(evseId, onUnlockConnectorUserData); + switch (status) { + case UnlockConnectorResult_Pending: + return UnlockStatus_PENDING; + case UnlockConnectorResult_Unlocked: + return UnlockStatus_Unlocked; + case UnlockConnectorResult_UnlockFailed: + return UnlockStatus_UnlockFailed; + } + + MO_DBG_ERR("invalid onUnlockConnector result code"); + return UnlockStatus_UnlockFailed; +} +#endif + +RemoteControlService::RemoteControlService(Context& context, size_t numEvses) : MemoryManaged("v201.RemoteControl.RemoteControlService"), context(context) { + + for (size_t i = 0; i < numEvses && i < MO_NUM_EVSEID; i++) { + evses[i] = new RemoteControlServiceEvse(context, (unsigned int)i); + } + + auto varService = context.getModel().getVariableService(); + authorizeRemoteStart = varService->declareVariable("AuthCtrlr", "AuthorizeRemoteStart", false); + + context.getOperationRegistry().registerOperation("RequestStartTransaction", [this] () -> Operation* { + if (!this->context.getModel().getTransactionService()) { + return nullptr; //-> NotSupported + } + return new Ocpp201::RequestStartTransaction(*this);}); + context.getOperationRegistry().registerOperation("RequestStopTransaction", [this] () -> Operation* { + if (!this->context.getModel().getTransactionService()) { + return nullptr; //-> NotSupported + } + return new Ocpp201::RequestStopTransaction(*this);}); +#if MO_ENABLE_CONNECTOR_LOCK + context.getOperationRegistry().registerOperation("UnlockConnector", [this] () { + return new Ocpp201::UnlockConnector(*this);}); +#endif + context.getOperationRegistry().registerOperation("TriggerMessage", [&context] () { + return new Ocpp16::TriggerMessage(context);}); +} + +RemoteControlService::~RemoteControlService() { + for (size_t i = 0; i < MO_NUM_EVSEID && evses[i]; i++) { + delete evses[i]; + } +} + +RemoteControlServiceEvse *RemoteControlService::getEvse(unsigned int evseId) { + if (evseId >= MO_NUM_EVSEID) { + MO_DBG_ERR("invalid arg"); + return nullptr; + } + return evses[evseId]; +} + +RequestStartStopStatus RemoteControlService::requestStartTransaction(unsigned int evseId, unsigned int remoteStartId, IdToken idToken, char *transactionIdOut, size_t transactionIdBufSize) { + + TransactionService *txService = context.getModel().getTransactionService(); + if (!txService) { + MO_DBG_ERR("TxService uninitialized"); + return RequestStartStopStatus_Rejected; + } + + auto evse = txService->getEvse(evseId); + if (!evse) { + MO_DBG_ERR("EVSE not found"); + return RequestStartStopStatus_Rejected; + } + + if (!evse->beginAuthorization(idToken, authorizeRemoteStart->getBool())) { + MO_DBG_INFO("EVSE still occupied with pending tx"); + if (auto tx = evse->getTransaction()) { + auto ret = snprintf(transactionIdOut, transactionIdBufSize, "%s", tx->transactionId); + if (ret < 0 || (size_t)ret >= transactionIdBufSize) { + MO_DBG_ERR("internal error"); + return RequestStartStopStatus_Rejected; + } + } + return RequestStartStopStatus_Rejected; + } + + auto tx = evse->getTransaction(); + if (!tx) { + MO_DBG_ERR("internal error"); + return RequestStartStopStatus_Rejected; + } + + auto ret = snprintf(transactionIdOut, transactionIdBufSize, "%s", tx->transactionId); + if (ret < 0 || (size_t)ret >= transactionIdBufSize) { + MO_DBG_ERR("internal error"); + return RequestStartStopStatus_Rejected; + } + + tx->remoteStartId = remoteStartId; + tx->notifyRemoteStartId = true; + + return RequestStartStopStatus_Accepted; +} + +RequestStartStopStatus RemoteControlService::requestStopTransaction(const char *transactionId) { + + TransactionService *txService = context.getModel().getTransactionService(); + if (!txService) { + MO_DBG_ERR("TxService uninitialized"); + return RequestStartStopStatus_Rejected; + } + + bool success = false; + + for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID; evseId++) { + if (auto evse = txService->getEvse(evseId)) { + if (evse->getTransaction() && !strcmp(evse->getTransaction()->transactionId, transactionId)) { + success = evse->abortTransaction(Ocpp201::Transaction::StoppedReason::Remote, Ocpp201::TransactionEventTriggerReason::RemoteStop); + break; + } + } + } + + return success ? + RequestStartStopStatus_Accepted : + RequestStartStopStatus_Rejected; +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/RemoteControl/RemoteControlService.h b/src/MicroOcpp/Model/RemoteControl/RemoteControlService.h new file mode 100644 index 00000000..0a26567b --- /dev/null +++ b/src/MicroOcpp/Model/RemoteControl/RemoteControlService.h @@ -0,0 +1,65 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_REMOTECONTROLSERVICE_H +#define MO_REMOTECONTROLSERVICE_H + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include +#include + +namespace MicroOcpp { + +class Context; +class Variable; + +class RemoteControlServiceEvse : public MemoryManaged { +private: + Context& context; + const unsigned int evseId; + +#if MO_ENABLE_CONNECTOR_LOCK + UnlockConnectorResult (*onUnlockConnector)(unsigned int evseId, void *user) = nullptr; + void *onUnlockConnectorUserData = nullptr; +#endif + +public: + RemoteControlServiceEvse(Context& context, unsigned int evseId); + +#if MO_ENABLE_CONNECTOR_LOCK + void setOnUnlockConnector(UnlockConnectorResult (*onUnlockConnector)(unsigned int evseId, void *userData), void *userData); + + UnlockStatus unlockConnector(); +#endif + +}; + +class RemoteControlService : public MemoryManaged { +private: + Context& context; + RemoteControlServiceEvse* evses [MO_NUM_EVSEID] = {nullptr}; + + Variable *authorizeRemoteStart = nullptr; + +public: + RemoteControlService(Context& context, size_t numEvses); + ~RemoteControlService(); + + RemoteControlServiceEvse *getEvse(unsigned int evseId); + + RequestStartStopStatus requestStartTransaction(unsigned int evseId, unsigned int remoteStartId, IdToken idToken, char *transactionIdOut, size_t transactionIdBufSize); //ChargingProfile, GroupIdToken not supported yet + + RequestStartStopStatus requestStopTransaction(const char *transactionId); +}; + +} // namespace MicroOcpp + +#endif // MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Model/Reservation/Reservation.cpp b/src/MicroOcpp/Model/Reservation/Reservation.cpp index 302ac8cb..eb856b5d 100644 --- a/src/MicroOcpp/Model/Reservation/Reservation.cpp +++ b/src/MicroOcpp/Model/Reservation/Reservation.cpp @@ -1,14 +1,18 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + +#if MO_ENABLE_RESERVATION + #include #include #include using namespace MicroOcpp; -Reservation::Reservation(Model& model, unsigned int slot) : model(model), slot(slot) { +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); @@ -27,7 +31,6 @@ Reservation::Reservation(Model& model, unsigned int slot) : model(model), slot(s if (!connectorIdInt || !expiryDateRawString || !idTagString || !reservationIdInt || !parentIdTagString) { MO_DBG_ERR("initialization failure"); - (void)0; } } @@ -130,3 +133,5 @@ void Reservation::clear() { configuration_save(); } + +#endif //MO_ENABLE_RESERVATION diff --git a/src/MicroOcpp/Model/Reservation/Reservation.h b/src/MicroOcpp/Model/Reservation/Reservation.h index 6722df1b..daae197a 100644 --- a/src/MicroOcpp/Model/Reservation/Reservation.h +++ b/src/MicroOcpp/Model/Reservation/Reservation.h @@ -1,12 +1,17 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef RESERVATION_H -#define RESERVATION_H +#ifndef MO_RESERVATION_H +#define MO_RESERVATION_H + +#include + +#if MO_ENABLE_RESERVATION #include #include +#include #ifndef RESERVATION_FN #define RESERVATION_FN (MO_FILENAME_PREFIX "reservations.jsn") @@ -22,7 +27,7 @@ namespace MicroOcpp { class Model; -class Reservation { +class Reservation : public MemoryManaged { private: Model& model; const unsigned int slot; @@ -65,4 +70,5 @@ class Reservation { } +#endif //MO_ENABLE_RESERVATION #endif diff --git a/src/MicroOcpp/Model/Reservation/ReservationService.cpp b/src/MicroOcpp/Model/Reservation/ReservationService.cpp index 63782cda..dde39aea 100644 --- a/src/MicroOcpp/Model/Reservation/ReservationService.cpp +++ b/src/MicroOcpp/Model/Reservation/ReservationService.cpp @@ -1,7 +1,11 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + +#if MO_ENABLE_RESERVATION + #include #include #include @@ -14,7 +18,7 @@ using namespace MicroOcpp; -ReservationService::ReservationService(Context& context, unsigned int numConnectors) : context(context), maxReservations((int) numConnectors - 1) { +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++) { @@ -24,8 +28,8 @@ ReservationService::ReservationService(Context& context, unsigned int numConnect reserveConnectorZeroSupportedBool = declareConfiguration("ReserveConnectorZeroSupported", true, CONFIGURATION_VOLATILE, true); - context.getOperationRegistry().registerOperation("CancelReservation", [&context] () { - return new Ocpp16::CancelReservation(context.getModel());}); + context.getOperationRegistry().registerOperation("CancelReservation", [this] () { + return new Ocpp16::CancelReservation(*this);}); context.getOperationRegistry().registerOperation("ReserveNow", [&context] () { return new Ocpp16::ReserveNow(context.getModel());}); } @@ -42,7 +46,7 @@ void ReservationService::loop() { //check if connector went inoperative auto cStatus = connector->getStatus(); - if (cStatus == ChargePointStatus::Faulted || cStatus == ChargePointStatus::Unavailable) { + if (cStatus == ChargePointStatus_Faulted || cStatus == ChargePointStatus_Unavailable) { reservation->clear(); continue; } @@ -153,7 +157,7 @@ Reservation *ReservationService::getReservation(unsigned int connectorId, const continue; } if (auto connector = context.getModel().getConnector(cId)) { - if (connector->getStatus() == ChargePointStatus::Available) { + if (connector->getStatus() == ChargePointStatus_Available) { availableCount++; } } @@ -210,3 +214,5 @@ bool ReservationService::updateReservation(int reservationId, unsigned int conne MO_DBG_ERR("error finding blocking reservation"); return false; } + +#endif //MO_ENABLE_RESERVATION diff --git a/src/MicroOcpp/Model/Reservation/ReservationService.h b/src/MicroOcpp/Model/Reservation/ReservationService.h index 87a6a486..367f1f59 100644 --- a/src/MicroOcpp/Model/Reservation/ReservationService.h +++ b/src/MicroOcpp/Model/Reservation/ReservationService.h @@ -1,11 +1,16 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef RESERVATIONSERVICE_H -#define RESERVATIONSERVICE_H +#ifndef MO_RESERVATIONSERVICE_H +#define MO_RESERVATIONSERVICE_H + +#include + +#if MO_ENABLE_RESERVATION #include +#include #include @@ -13,12 +18,12 @@ namespace MicroOcpp { class Context; -class ReservationService { +class ReservationService : public MemoryManaged { private: Context& context; const int maxReservations; // = number of physical connectors - std::vector> reservations; + Vector> reservations; std::shared_ptr reserveConnectorZeroSupportedBool; @@ -44,4 +49,5 @@ class ReservationService { } +#endif //MO_ENABLE_RESERVATION #endif diff --git a/src/MicroOcpp/Model/Reset/ResetDefs.h b/src/MicroOcpp/Model/Reset/ResetDefs.h new file mode 100644 index 00000000..baa5048d --- /dev/null +++ b/src/MicroOcpp/Model/Reset/ResetDefs.h @@ -0,0 +1,24 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_RESETDEFS_H +#define MO_RESETDEFS_H + +#include + +#if MO_ENABLE_V201 + +typedef enum ResetType { + ResetType_Immediate, + ResetType_OnIdle +} ResetType; + +typedef enum ResetStatus { + ResetStatus_Accepted, + ResetStatus_Rejected, + ResetStatus_Scheduled +} ResetStatus; + +#endif //MO_ENABLE_V201 +#endif diff --git a/src/MicroOcpp/Model/Reset/ResetService.cpp b/src/MicroOcpp/Model/Reset/ResetService.cpp index 43f7b680..2eef02b8 100644 --- a/src/MicroOcpp/Model/Reset/ResetService.cpp +++ b/src/MicroOcpp/Model/Reset/ResetService.cpp @@ -1,11 +1,11 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include -#include +#include #include #include @@ -14,19 +14,22 @@ #include #include -#include +#include +#include -#include -#include +#include -#define RESET_DELAY 15000 +#ifndef MO_RESET_DELAY +#define MO_RESET_DELAY 10000 +#endif using namespace MicroOcpp; ResetService::ResetService(Context& context) - : context(context) { + : MemoryManaged("v16.Reset.ResetService"), context(context) { resetRetriesInt = declareConfiguration("ResetRetries", 2); + registerConfigurationValidator("ResetRetries", VALIDATE_UNSIGNED_INT); context.getOperationRegistry().registerOperation("Reset", [&context] () { return new Ocpp16::Reset(context.getModel());}); @@ -38,7 +41,7 @@ ResetService::~ResetService() { void ResetService::loop() { - if (outstandingResetRetries > 0 && mocpp_tick_ms() - t_resetRetry >= RESET_DELAY) { + if (outstandingResetRetries > 0 && mocpp_tick_ms() - t_resetRetry >= MO_RESET_DELAY) { t_resetRetry = mocpp_tick_ms(); outstandingResetRetries--; if (executeReset) { @@ -48,15 +51,12 @@ void ResetService::loop() { MO_DBG_ERR("No Reset function set! Abort"); outstandingResetRetries = 0; } - MO_DBG_ERR("Reset device failure. %s", outstandingResetRetries == 0 ? "Abort" : "Retry"); if (outstandingResetRetries <= 0) { - for (unsigned int cId = 0; cId < context.getModel().getNumConnectors(); cId++) { - auto connector = context.getModel().getConnector(cId); - connector->setAvailabilityVolatile(true); - } - ChargePointStatus cpStatus = ChargePointStatus::NOT_SET; + MO_DBG_ERR("Reset device failure. Abort"); + + ChargePointStatus cpStatus = ChargePointStatus_UNDEFINED; if (context.getModel().getNumConnectors() > 0) { cpStatus = context.getModel().getConnector(0)->getStatus(); } @@ -96,11 +96,6 @@ void ResetService::initiateReset(bool isHard) { outstandingResetRetries = 5; } t_resetRetry = mocpp_tick_ms(); - - for (unsigned int cId = 0; cId < context.getModel().getNumConnectors(); cId++) { - auto connector = context.getModel().getConnector(cId); - connector->setAvailabilityVolatile(false); - } } #if MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) @@ -111,3 +106,228 @@ std::function MicroOcpp::makeDefaultResetFn() { }; } #endif //MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) + +#if MO_ENABLE_V201 +namespace MicroOcpp { +namespace Ocpp201 { + +ResetService::ResetService(Context& context) + : MemoryManaged("v201.Reset.ResetService"), context(context), evses(makeVector(getMemoryTag())) { + + auto varService = context.getModel().getVariableService(); + resetRetriesInt = varService->declareVariable("OCPPCommCtrlr", "ResetRetries", 0); + + context.getOperationRegistry().registerOperation("Reset", [this] () { + return new Ocpp201::Reset(*this);}); +} + +ResetService::~ResetService() { + +} + +ResetService::Evse::Evse(Context& context, ResetService& resetService, unsigned int evseId) : context(context), resetService(resetService), evseId(evseId) { + auto varService = context.getModel().getVariableService(); + varService->declareVariable(ComponentId("EVSE", evseId >= 1 ? evseId : -1), "AllowReset", true, Variable::Mutability::ReadOnly, false); +} + +void ResetService::Evse::loop() { + + if (outstandingResetRetries && awaitTxStop) { + + for (unsigned int eId = std::max(1U, evseId); eId < (evseId == 0 ? MO_NUM_EVSEID : evseId + 1); eId++) { + //If evseId > 0, execute this block one time for evseId. If evseId == 0, then iterate over all evseIds > 0 + + auto txService = context.getModel().getTransactionService(); + if (txService && txService->getEvse(eId) && txService->getEvse(eId)->getTransaction()) { + auto tx = txService->getEvse(eId)->getTransaction(); + + if (!tx->stopped) { + // wait until tx stopped + return; + } + } + } + + awaitTxStop = false; + + MO_DBG_INFO("Reset - tx stopped"); + t_resetRetry = mocpp_tick_ms(); // wait for some more time until final reset + } + + if (outstandingResetRetries && mocpp_tick_ms() - t_resetRetry >= MO_RESET_DELAY) { + t_resetRetry = mocpp_tick_ms(); + outstandingResetRetries--; + + MO_DBG_INFO("Reset device"); + + bool success = executeReset(); + + if (success) { + outstandingResetRetries = 0; + + if (evseId != 0) { + //Set this EVSE Available again + if (auto connector = context.getModel().getConnector(evseId)) { + connector->setAvailabilityVolatile(true); + } + } + } else if (!outstandingResetRetries) { + MO_DBG_ERR("Reset device failure"); + + if (evseId == 0) { + //Set all EVSEs Available again + for (unsigned int cId = 0; cId < context.getModel().getNumConnectors(); cId++) { + auto connector = context.getModel().getConnector(cId); + connector->setAvailabilityVolatile(true); + } + } else { + //Set only this EVSE Available + if (auto connector = context.getModel().getConnector(evseId)) { + connector->setAvailabilityVolatile(true); + } + } + } + } +} + +ResetService::Evse *ResetService::getEvse(unsigned int evseId) { + for (size_t i = 0; i < evses.size(); i++) { + if (evses[i].evseId == evseId) { + return &evses[i]; + } + } + return nullptr; +} + +ResetService::Evse *ResetService::getOrCreateEvse(unsigned int evseId) { + if (auto evse = getEvse(evseId)) { + return evse; + } + + if (evseId >= MO_NUM_EVSEID) { + MO_DBG_ERR("evseId out of bound"); + return nullptr; + } + + evses.emplace_back(context, *this, evseId); + return &evses.back(); +} + +void ResetService::loop() { + for (Evse& evse : evses) { + evse.loop(); + } +} + +void ResetService::setNotifyReset(std::function notifyReset, unsigned int evseId) { + Evse *evse = getOrCreateEvse(evseId); + if (!evse) { + MO_DBG_ERR("evseId not found"); + return; + } + evse->notifyReset = notifyReset; +} + +std::function ResetService::getNotifyReset(unsigned int evseId) { + Evse *evse = getOrCreateEvse(evseId); + if (!evse) { + MO_DBG_ERR("evseId not found"); + return nullptr; + } + return evse->notifyReset; +} + +void ResetService::setExecuteReset(std::function executeReset, unsigned int evseId) { + Evse *evse = getOrCreateEvse(evseId); + if (!evse) { + MO_DBG_ERR("evseId not found"); + return; + } + evse->executeReset = executeReset; +} + +std::function ResetService::getExecuteReset(unsigned int evseId) { + Evse *evse = getOrCreateEvse(evseId); + if (!evse) { + MO_DBG_ERR("evseId not found"); + return nullptr; + } + return evse->executeReset; +} + +ResetStatus ResetService::initiateReset(ResetType type, unsigned int evseId) { + auto evse = getEvse(evseId); + if (!evse) { + MO_DBG_ERR("evseId not found"); + return ResetStatus_Rejected; + } + + if (!evse->executeReset) { + MO_DBG_INFO("EVSE %u does not support Reset", evseId); + return ResetStatus_Rejected; + } + + //Check if EVSEs are ready for Reset + for (unsigned int eId = evseId; eId < (evseId == 0 ? MO_NUM_EVSEID : evseId + 1); eId++) { + //If evseId > 0, execute this block one time for evseId. If evseId == 0, then iterate over all evseIds + + if (auto it = getEvse(eId)) { + if (it->notifyReset && !it->notifyReset(type)) { + MO_DBG_INFO("EVSE %u not able to Reset", evseId); + return ResetStatus_Rejected; + } + } + } + + //Set EVSEs Unavailable + if (evseId == 0) { + //Set all EVSEs Unavailable + for (unsigned int cId = 0; cId < context.getModel().getNumConnectors(); cId++) { + auto connector = context.getModel().getConnector(cId); + connector->setAvailabilityVolatile(false); + } + } else { + //Set this EVSE Unavailable + if (auto connector = context.getModel().getConnector(evseId)) { + connector->setAvailabilityVolatile(false); + } + } + + bool scheduled = false; + + //Tx-related behavior: if immediate Reset, stop txs; otherwise schedule Reset + for (unsigned int eId = std::max(1U, evseId); eId < (evseId == 0 ? MO_NUM_EVSEID : evseId + 1); eId++) { + //If evseId > 0, execute this block one time for evseId. If evseId == 0, then iterate over all evseIds > 0 + + auto txService = context.getModel().getTransactionService(); + if (txService && txService->getEvse(eId) && txService->getEvse(eId)->getTransaction()) { + auto tx = txService->getEvse(eId)->getTransaction(); + if (tx->active) { + //Tx in progress. Check behavior + if (type == ResetType_Immediate) { + txService->getEvse(eId)->abortTransaction(Transaction::StoppedReason::ImmediateReset, TransactionEventTriggerReason::ResetCommand); + } else { + scheduled = true; + break; + } + } + } + } + + //Actually engage Reset + + if (resetRetriesInt->getInt() >= 5) { + MO_DBG_ERR("no. of reset trials exceeds 5"); + evse->outstandingResetRetries = 5; + } else { + evse->outstandingResetRetries = 1 + resetRetriesInt->getInt(); //one initial try + no. of retries + } + evse->t_resetRetry = mocpp_tick_ms(); + evse->awaitTxStop = scheduled; + + return scheduled ? ResetStatus_Scheduled : ResetStatus_Accepted; +} + +} //namespace MicroOcpp +} //namespace Ocpp201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Reset/ResetService.h b/src/MicroOcpp/Model/Reset/ResetService.h index f4409aa0..a84e82d6 100644 --- a/src/MicroOcpp/Model/Reset/ResetService.h +++ b/src/MicroOcpp/Model/Reset/ResetService.h @@ -1,19 +1,22 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef RESETSERVICE_H -#define RESETSERVICE_H +#ifndef MO_RESETSERVICE_H +#define MO_RESETSERVICE_H #include +#include #include +#include +#include namespace MicroOcpp { class Context; -class ResetService { +class ResetService : public MemoryManaged { private: Context& context; @@ -53,4 +56,59 @@ std::function makeDefaultResetFn(); #endif //MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) +#if MO_ENABLE_V201 + +namespace MicroOcpp { + +class Variable; + +namespace Ocpp201 { + +class ResetService : public MemoryManaged { +private: + Context& context; + + struct Evse { + Context& context; + ResetService& resetService; + const unsigned int evseId; + + std::function notifyReset; //notify firmware about a Reset command. Return true if Reset is okay; false if Reset cannot be executed + std::function executeReset; //execute Reset of connector. Return true if Reset will be executed; false if there is a failure to Reset + + unsigned int outstandingResetRetries = 0; //0 = do not reset device + unsigned long t_resetRetry; + + bool awaitTxStop = false; + + Evse(Context& context, ResetService& resetService, unsigned int evseId); + + void loop(); + }; + + Vector evses; + Evse *getEvse(unsigned int connectorId); + Evse *getOrCreateEvse(unsigned int connectorId); + + Variable *resetRetriesInt = nullptr; + +public: + ResetService(Context& context); + ~ResetService(); + + void loop(); + + void setNotifyReset(std::function notifyReset, unsigned int evseId = 0); + std::function getNotifyReset(unsigned int evseId = 0); + + void setExecuteReset(std::function executeReset, unsigned int evseId = 0); + std::function getExecuteReset(unsigned int evseId = 0); + + ResetStatus initiateReset(ResetType type, unsigned int evseId = 0); +}; + +} //namespace Ocpp201 +} //namespace MicroOcpp +#endif //MO_ENABLE_V201 + #endif diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp index e8355f61..3c71d539 100644 --- a/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -18,6 +18,10 @@ ChargeRate MicroOcpp::chargeRate_min(const ChargeRate& a, const ChargeRate& b) { return res; } +ChargingSchedule::ChargingSchedule() : MemoryManaged("v16.SmartCharging.SmartChargingModel"), chargingSchedulePeriod{makeVector(getMemoryTag())} { + +} + bool ChargingSchedule::calculateLimit(const Timestamp &t, const Timestamp &startOfCharging, ChargeRate& limit, Timestamp& nextChange) { Timestamp basis = Timestamp(); //point in time to which schedule-related times are relative switch (chargingProfileKind) { @@ -116,13 +120,13 @@ bool ChargingSchedule::calculateLimit(const Timestamp &t, const Timestamp &start } } -bool ChargingSchedule::toJson(DynamicJsonDocument& doc) { +bool ChargingSchedule::toJson(JsonDoc& doc) { size_t capacity = 0; capacity += JSON_OBJECT_SIZE(5); //no of fields of ChargingSchedule capacity += JSONDATE_LENGTH + 1; //startSchedule capacity += JSON_ARRAY_SIZE(chargingSchedulePeriod.size()) + chargingSchedulePeriod.size() * JSON_OBJECT_SIZE(3); - doc = DynamicJsonDocument(capacity); + doc = initJsonDoc("v16.SmartCharging.ChargingSchedule", capacity); if (duration >= 0) { doc["duration"] = duration; } @@ -173,6 +177,10 @@ void ChargingSchedule::printSchedule(){ } } +ChargingProfile::ChargingProfile() : MemoryManaged("v16.SmartCharging.ChargingProfile") { + +} + bool ChargingProfile::calculateLimit(const Timestamp &t, const Timestamp &startOfCharging, ChargeRate& limit, Timestamp& nextChange){ if (t > validTo && validTo > MIN_TIME) { return false; //no limit defined @@ -205,14 +213,14 @@ ChargingProfilePurposeType ChargingProfile::getChargingProfilePurpose(){ return chargingProfilePurpose; } -bool ChargingProfile::toJson(DynamicJsonDocument& doc) { +bool ChargingProfile::toJson(JsonDoc& doc) { - DynamicJsonDocument chargingScheduleDoc {0}; + auto chargingScheduleDoc = initJsonDoc("v16.SmartCharging.ChargingSchedule"); if (!chargingSchedule.toJson(chargingScheduleDoc)) { return false; } - doc = DynamicJsonDocument( + 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 @@ -369,7 +377,7 @@ std::unique_ptr MicroOcpp::loadChargingProfile(JsonObject& json } int stackLevel = json["stackLevel"] | -1; - if (stackLevel >= 0 && stackLevel <= CHARGEPROFILEMAXSTACKLEVEL) { + if (stackLevel >= 0 && stackLevel <= MO_ChargeProfileMaxStackLevel) { res->stackLevel = stackLevel; } else { MO_DBG_WARN("format violation"); @@ -480,8 +488,13 @@ bool MicroOcpp::loadChargingSchedule(JsonObject& json, ChargingSchedule& out) { return false; } + if (periodJsonArray.size() > MO_ChargingScheduleMaxPeriods) { + MO_DBG_WARN("exceed ChargingScheduleMaxPeriods"); + return false; + } + for (JsonObject periodJson : periodJsonArray) { - out.chargingSchedulePeriod.push_back(ChargingSchedulePeriod()); + out.chargingSchedulePeriod.emplace_back(); if (!loadChargingSchedulePeriod(periodJson, out.chargingSchedulePeriod.back())) { return false; } diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.h b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.h index abd62c1f..825d6866 100644 --- a/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.h +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.h @@ -1,21 +1,29 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef SMARTCHARGINGMODEL_H #define SMARTCHARGINGMODEL_H -#define CHARGEPROFILEMAXSTACKLEVEL 8 -#define CHARGINGSCHEDULEMAXPERIODS 24 -#define MAXCHARGINGPROFILESINSTALLED 10 +#ifndef MO_ChargeProfileMaxStackLevel +#define MO_ChargeProfileMaxStackLevel 8 +#endif + +#ifndef MO_ChargingScheduleMaxPeriods +#define MO_ChargingScheduleMaxPeriods 24 +#endif + +#ifndef MO_MaxChargingProfilesInstalled +#define MO_MaxChargingProfilesInstalled 10 +#endif #include -#include #include #include #include +#include namespace MicroOcpp { @@ -67,17 +75,19 @@ class ChargingSchedulePeriod { int numberPhases = 3; }; -class ChargingSchedule { +class ChargingSchedule : public MemoryManaged { public: int duration = -1; Timestamp startSchedule; ChargingRateUnitType chargingRateUnit; - std::vector chargingSchedulePeriod; + Vector chargingSchedulePeriod; float minChargingRate = -1.0f; ChargingProfileKindType chargingProfileKind; //copied from ChargingProfile to increase cohesion of limit algorithms RecurrencyKindType recurrencyKind = RecurrencyKindType::NOT_SET; //copied from ChargingProfile to increase cohesion of limit algorithms + ChargingSchedule(); + /** * limit: output parameter * nextChange: output parameter @@ -88,7 +98,7 @@ class ChargingSchedule { */ bool calculateLimit(const Timestamp &t, const Timestamp &startOfCharging, ChargeRate& limit, Timestamp& nextChange); - bool toJson(DynamicJsonDocument& out); + bool toJson(JsonDoc& out); /* * print on console @@ -96,7 +106,7 @@ class ChargingSchedule { void printSchedule(); }; -class ChargingProfile { +class ChargingProfile : public MemoryManaged { public: int chargingProfileId = -1; int transactionId = -1; @@ -108,6 +118,8 @@ class ChargingProfile { Timestamp validTo; ChargingSchedule chargingSchedule; + ChargingProfile(); + /** * limit: output parameter * nextChange: output parameter @@ -129,7 +141,7 @@ class ChargingProfile { ChargingProfilePurposeType getChargingProfilePurpose(); - bool toJson(DynamicJsonDocument& out); + bool toJson(JsonDoc& out); /* * print on console diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp index a3c6be08..762bd6ac 100644 --- a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -16,7 +16,7 @@ using namespace::MicroOcpp; SmartChargingConnector::SmartChargingConnector(Model& model, std::shared_ptr filesystem, unsigned int connectorId, ProfileStack& ChargePointMaxProfile, ProfileStack& ChargePointTxDefaultProfile) : - model(model), filesystem{filesystem}, connectorId{connectorId}, ChargePointMaxProfile(ChargePointMaxProfile), ChargePointTxDefaultProfile(ChargePointTxDefaultProfile) { + MemoryManaged("v16.SmartCharging.SmartChargingConnector"), model(model), filesystem{filesystem}, connectorId{connectorId}, ChargePointMaxProfile(ChargePointMaxProfile), ChargePointTxDefaultProfile(ChargePointTxDefaultProfile) { } @@ -38,7 +38,7 @@ void SmartChargingConnector::calculateLimit(const Timestamp &t, ChargeRate& limi ChargeRate txLimit; //first, check if TxProfile is defined and limits charging - for (int i = CHARGEPROFILEMAXSTACKLEVEL; i >= 0; i--) { + 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())) { @@ -54,7 +54,7 @@ void SmartChargingConnector::calculateLimit(const Timestamp &t, ChargeRate& limi //if no TxProfile limits charging, check the TxDefaultProfiles for this connector if (!txLimitDefined && trackTxStart < MAX_TIME) { - for (int i = CHARGEPROFILEMAXSTACKLEVEL; i >= 0; i--) { + for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { if (TxDefaultProfile[i]) { ChargeRate crOut; bool defined = TxDefaultProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); @@ -69,7 +69,7 @@ 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) { - for (int i = CHARGEPROFILEMAXSTACKLEVEL; i >= 0; i--) { + for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { if (ChargePointTxDefaultProfile[i]) { ChargeRate crOut; bool defined = ChargePointTxDefaultProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); @@ -85,7 +85,7 @@ void SmartChargingConnector::calculateLimit(const Timestamp &t, ChargeRate& limi ChargeRate cpLimit; //the calculated maximum charge rate is also limited by the ChargePointMaxProfiles - for (int i = CHARGEPROFILEMAXSTACKLEVEL; i >= 0; i--) { + for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { if (ChargePointMaxProfile[i]) { ChargeRate crOut; bool defined = ChargePointMaxProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); @@ -159,6 +159,21 @@ void SmartChargingConnector::loop(){ calculateLimit(tnow, limit, nextChange); +#if MO_DBG_LEVEL >= MO_DL_INFO + { + char timestamp1[JSONDATE_LENGTH + 1] = {'\0'}; + tnow.toJsonString(timestamp1, JSONDATE_LENGTH + 1); + char timestamp2[JSONDATE_LENGTH + 1] = {'\0'}; + nextChange.toJsonString(timestamp2, JSONDATE_LENGTH + 1); + MO_DBG_INFO("New limit for connector %u, scheduled at = %s, nextChange = %s, limit = {%.1f, %.1f, %i}", + connectorId, + timestamp1, timestamp2, + limit.power != std::numeric_limits::max() ? limit.power : -1.f, + limit.current != std::numeric_limits::max() ? limit.current : -1.f, + limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); + } +#endif + if (trackLimitOutput != limit) { if (limitOutput) { @@ -175,7 +190,6 @@ void SmartChargingConnector::loop(){ void SmartChargingConnector::setSmartChargingOutput(std::function limitOutput) { if (this->limitOutput) { MO_DBG_WARN("replacing existing SmartChargingOutput"); - (void)0; } this->limitOutput = limitOutput; } @@ -238,7 +252,7 @@ std::unique_ptr SmartChargingConnector::getCompositeSchedule(i Timestamp periodBegin = Timestamp(startSchedule); Timestamp periodStop = Timestamp(startSchedule); - while (periodBegin - startSchedule < duration && periods.size() < CHARGINGSCHEDULEMAXPERIODS) { + while (periodBegin - startSchedule < duration && periods.size() < MO_ChargingScheduleMaxPeriods) { //calculate limit ChargeRate limit; @@ -253,7 +267,7 @@ std::unique_ptr SmartChargingConnector::getCompositeSchedule(i } } - periods.push_back(ChargingSchedulePeriod()); + 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; @@ -273,11 +287,11 @@ std::unique_ptr SmartChargingConnector::getCompositeSchedule(i size_t SmartChargingConnector::getChargingProfilesCount() { size_t chargingProfilesCount = 0; - for (size_t i = 0; i < CHARGEPROFILEMAXSTACKLEVEL + 1; i++) { - if (ChargePointTxDefaultProfile[i]) { + for (size_t i = 0; i < MO_ChargeProfileMaxStackLevel + 1; i++) { + if (TxDefaultProfile[i]) { chargingProfilesCount++; } - if (ChargePointMaxProfile[i]) { + if (TxProfile[i]) { chargingProfilesCount++; } } @@ -297,16 +311,16 @@ SmartChargingConnector *SmartChargingService::getScConnectorById(unsigned int co } SmartChargingService::SmartChargingService(Context& context, std::shared_ptr filesystem, unsigned int numConnectors) - : context(context), filesystem{filesystem}, numConnectors(numConnectors) { + : MemoryManaged("v16.SmartCharging.SmartChargingService"), context(context), filesystem{filesystem}, connectors{makeVector(getMemoryTag())}, numConnectors(numConnectors) { for (unsigned int cId = 1; cId < numConnectors; cId++) { - connectors.push_back(std::move(SmartChargingConnector(context.getModel(), filesystem, cId, ChargePointMaxProfile, ChargePointTxDefaultProfile))); + connectors.emplace_back(context.getModel(), filesystem, cId, ChargePointMaxProfile, ChargePointTxDefaultProfile); } - declareConfiguration("ChargeProfileMaxStackLevel", CHARGEPROFILEMAXSTACKLEVEL, CONFIGURATION_VOLATILE, true); - declareConfiguration("ChargingScheduleAllowedChargingRateUnit", "", CONFIGURATION_VOLATILE); - declareConfiguration("ChargingScheduleMaxPeriods", CHARGINGSCHEDULEMAXPERIODS, CONFIGURATION_VOLATILE, true); - declareConfiguration("MaxChargingProfilesInstalled", MAXCHARGINGPROFILESINSTALLED, CONFIGURATION_VOLATILE, true); + declareConfiguration("ChargeProfileMaxStackLevel", MO_ChargeProfileMaxStackLevel, CONFIGURATION_VOLATILE, true); + declareConfiguration("ChargingScheduleAllowedChargingRateUnit", "", CONFIGURATION_VOLATILE, true); + declareConfiguration("ChargingScheduleMaxPeriods", MO_ChargingScheduleMaxPeriods, CONFIGURATION_VOLATILE, true); + declareConfiguration("MaxChargingProfilesInstalled", MO_MaxChargingProfilesInstalled, CONFIGURATION_VOLATILE, true); context.getOperationRegistry().registerOperation("ClearChargingProfile", [this] () { return new Ocpp16::ClearChargingProfile(*this);}); @@ -335,13 +349,13 @@ ChargingProfile *SmartChargingService::updateProfiles(unsigned int connectorId, } int stackLevel = chargingProfile->getStackLevel(); - if (stackLevel< 0 || stackLevel >= CHARGEPROFILEMAXSTACKLEVEL + 1) { + if (stackLevel< 0 || stackLevel >= MO_ChargeProfileMaxStackLevel + 1) { MO_DBG_ERR("input validation failed"); return nullptr; } size_t chargingProfilesCount = 0; - for (size_t i = 0; i < CHARGEPROFILEMAXSTACKLEVEL + 1; i++) { + for (size_t i = 0; i < MO_ChargeProfileMaxStackLevel + 1; i++) { if (ChargePointTxDefaultProfile[i]) { chargingProfilesCount++; } @@ -353,7 +367,7 @@ ChargingProfile *SmartChargingService::updateProfiles(unsigned int connectorId, chargingProfilesCount += connectors[i].getChargingProfilesCount(); } - if (chargingProfilesCount >= MAXCHARGINGPROFILESINSTALLED) { + if (chargingProfilesCount >= MO_MaxChargingProfilesInstalled) { MO_DBG_WARN("number of maximum charging profiles exceeded"); return nullptr; } @@ -374,19 +388,16 @@ ChargingProfile *SmartChargingService::updateProfiles(unsigned int connectorId, ChargePointTxDefaultProfile[stackLevel] = std::move(chargingProfile); res = ChargePointTxDefaultProfile[stackLevel].get(); } else { - if (!getScConnectorById(connectorId)) { - MO_DBG_WARN("invalid charging profile"); - return nullptr; - } res = getScConnectorById(connectorId)->updateProfiles(std::move(chargingProfile)); } break; case (ChargingProfilePurposeType::TxProfile): - if (!getScConnectorById(connectorId)) { + if (connectorId == 0) { MO_DBG_WARN("invalid charging profile"); return nullptr; + } else { + res = getScConnectorById(connectorId)->updateProfiles(std::move(chargingProfile)); } - res = getScConnectorById(connectorId)->updateProfiles(std::move(chargingProfile)); break; } @@ -422,7 +433,7 @@ bool SmartChargingService::loadProfiles() { if (cId > 0 && purpose == ChargingProfilePurposeType::ChargePointMaxProfile) { continue; } - for (unsigned int iLevel = 0; iLevel < CHARGEPROFILEMAXSTACKLEVEL; iLevel++) { + for (unsigned int iLevel = 0; iLevel < MO_ChargeProfileMaxStackLevel; iLevel++) { if (!SmartChargingServiceUtils::printProfileFileName(fn, MO_MAX_PATH_SIZE, cId, purpose, iLevel)) { return false; @@ -433,7 +444,7 @@ bool SmartChargingService::loadProfiles() { continue; //There is not a profile on the stack iStack with stacklevel iLevel. Normal case, just continue. } - auto profileDoc = FilesystemUtils::loadJson(filesystem, fn); + auto profileDoc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); if (!profileDoc) { success = false; MO_DBG_ERR("profile corrupt: %s, remove", fn); @@ -471,7 +482,7 @@ void SmartChargingService::calculateLimit(const Timestamp &t, ChargeRate& limitO validToOut = MAX_TIME; //get ChargePointMaxProfile with the highest stackLevel - for (int i = CHARGEPROFILEMAXSTACKLEVEL; i >= 0; i--) { + for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { if (ChargePointMaxProfile[i]) { ChargeRate crOut; bool defined = ChargePointMaxProfile[i]->calculateLimit(t, crOut, validToOut); @@ -501,16 +512,19 @@ void SmartChargingService::loop(){ calculateLimit(tnow, limit, nextChange); -#if (MO_DBG_LEVEL >= MO_DL_INFO) - char timestamp1[JSONDATE_LENGTH + 1] = {'\0'}; - tnow.toJsonString(timestamp1, JSONDATE_LENGTH + 1); - char timestamp2[JSONDATE_LENGTH + 1] = {'\0'}; - nextChange.toJsonString(timestamp2, JSONDATE_LENGTH + 1); - MO_DBG_INFO("New limit for connector 1, scheduled at = %s, nextChange = %s, limit = {%f,%f,%i}", - 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); +#if MO_DBG_LEVEL >= MO_DL_INFO + { + char timestamp1[JSONDATE_LENGTH + 1] = {'\0'}; + tnow.toJsonString(timestamp1, JSONDATE_LENGTH + 1); + char timestamp2[JSONDATE_LENGTH + 1] = {'\0'}; + nextChange.toJsonString(timestamp2, JSONDATE_LENGTH + 1); + MO_DBG_INFO("New limit for connector %u, scheduled at = %s, nextChange = %s, limit = {%.1f, %.1f, %i}", + 0, + timestamp1, timestamp2, + limit.power != std::numeric_limits::max() ? limit.power : -1.f, + limit.current != std::numeric_limits::max() ? limit.current : -1.f, + limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); + } #endif if (trackLimitOutput != limit) { @@ -535,7 +549,6 @@ void SmartChargingService::setSmartChargingOutput(unsigned int connectorId, std: if (connectorId == 0) { if (this->limitOutput) { MO_DBG_WARN("replacing existing SmartChargingOutput"); - (void)0; } this->limitOutput = limitOutput; } else { @@ -569,6 +582,12 @@ bool SmartChargingService::setChargingProfile(unsigned int connectorId, std::uni return false; } + if ((!currentSupported && chargingProfile->chargingSchedule.chargingRateUnit == ChargingRateUnitType::Amp) || + (!powerSupported && chargingProfile->chargingSchedule.chargingRateUnit == ChargingRateUnitType::Watt)) { + MO_DBG_WARN("unsupported charge rate unit"); + return false; + } + int chargingProfileId = chargingProfile->getChargingProfileId(); clearChargingProfile([chargingProfileId] (int id, int, ChargingProfilePurposeType, int) { return id == chargingProfileId; @@ -656,7 +675,7 @@ std::unique_ptr SmartChargingService::getCompositeSchedule(uns Timestamp periodBegin = Timestamp(startSchedule); Timestamp periodStop = Timestamp(startSchedule); - while (periodBegin - startSchedule < duration && periods.size() < CHARGINGSCHEDULEMAXPERIODS) { + while (periodBegin - startSchedule < duration && periods.size() < MO_ChargingScheduleMaxPeriods) { //calculate limit ChargeRate limit; @@ -715,10 +734,11 @@ bool SmartChargingServiceUtils::printProfileFileName(char *out, size_t bufsize, bool SmartChargingServiceUtils::storeProfile(std::shared_ptr filesystem, unsigned int connectorId, ChargingProfile *chargingProfile) { if (!filesystem) { - return false; + MO_DBG_DEBUG("no filesystem"); + return true; //not an error } - DynamicJsonDocument chargingProfileJson {0}; + auto chargingProfileJson = initJsonDoc("v16.SmartCharging.ChargingProfile"); if (!chargingProfile->toJson(chargingProfileJson)) { return false; } diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h index f4dfa8ee..57952a72 100644 --- a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef SMARTCHARGINGSERVICE_H @@ -11,9 +11,9 @@ #include #include -#include #include #include +#include namespace MicroOcpp { @@ -26,9 +26,9 @@ enum class ChargingRateUnitType_Optional { class Context; class Model; -using ProfileStack = std::array, CHARGEPROFILEMAXSTACKLEVEL + 1>; +using ProfileStack = std::array, MO_ChargeProfileMaxStackLevel + 1>; -class SmartChargingConnector { +class SmartChargingConnector : public MemoryManaged { private: Model& model; std::shared_ptr filesystem; @@ -73,11 +73,11 @@ class SmartChargingConnector { size_t getChargingProfilesCount(); }; -class SmartChargingService { +class SmartChargingService : public MemoryManaged { private: Context& context; std::shared_ptr filesystem; - std::vector connectors; //connectorId 0 excluded + Vector connectors; //connectorId 0 excluded SmartChargingConnector *getScConnectorById(unsigned int connectorId); unsigned int numConnectors; //connectorId 0 included diff --git a/src/MicroOcpp/Model/Transactions/Transaction.cpp b/src/MicroOcpp/Model/Transactions/Transaction.cpp index 9c6535fa..a53b68f1 100644 --- a/src/MicroOcpp/Model/Transactions/Transaction.cpp +++ b/src/MicroOcpp/Model/Transactions/Transaction.cpp @@ -1,9 +1,10 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include +#include using namespace MicroOcpp; @@ -12,6 +13,11 @@ bool Transaction::setIdTag(const char *idTag) { return ret >= 0 && ret < IDTAG_LEN_MAX + 1; } +bool Transaction::setParentIdTag(const char *idTag) { + auto ret = snprintf(this->parentIdTag, IDTAG_LEN_MAX + 1, "%s", idTag); + return ret >= 0 && ret < IDTAG_LEN_MAX + 1; +} + bool Transaction::setStopIdTag(const char *idTag) { auto ret = snprintf(stop_idTag, IDTAG_LEN_MAX + 1, "%s", idTag); return ret >= 0 && ret < IDTAG_LEN_MAX + 1; @@ -26,57 +32,503 @@ bool Transaction::commit() { return context.commit(this); } +#if MO_ENABLE_V201 + +namespace MicroOcpp { +namespace Ocpp201 { + +const char *serializeTransactionStoppedReason(Transaction::StoppedReason stoppedReason) { + const char *stoppedReasonCstr = nullptr; + switch (stoppedReason) { + case Transaction::StoppedReason::UNDEFINED: + // optional, okay + break; + case Transaction::StoppedReason::Local: + stoppedReasonCstr = "Local"; + break; + case Transaction::StoppedReason::DeAuthorized: + stoppedReasonCstr = "DeAuthorized"; + break; + case Transaction::StoppedReason::EmergencyStop: + stoppedReasonCstr = "EmergencyStop"; + break; + case Transaction::StoppedReason::EnergyLimitReached: + stoppedReasonCstr = "EnergyLimitReached"; + break; + case Transaction::StoppedReason::EVDisconnected: + stoppedReasonCstr = "EVDisconnected"; + break; + case Transaction::StoppedReason::GroundFault: + stoppedReasonCstr = "GroundFault"; + break; + case Transaction::StoppedReason::ImmediateReset: + stoppedReasonCstr = "ImmediateReset"; + break; + case Transaction::StoppedReason::LocalOutOfCredit: + stoppedReasonCstr = "LocalOutOfCredit"; + break; + case Transaction::StoppedReason::MasterPass: + stoppedReasonCstr = "MasterPass"; + break; + case Transaction::StoppedReason::Other: + stoppedReasonCstr = "Other"; + break; + case Transaction::StoppedReason::OvercurrentFault: + stoppedReasonCstr = "OvercurrentFault"; + break; + case Transaction::StoppedReason::PowerLoss: + stoppedReasonCstr = "PowerLoss"; + break; + case Transaction::StoppedReason::PowerQuality: + stoppedReasonCstr = "PowerQuality"; + break; + case Transaction::StoppedReason::Reboot: + stoppedReasonCstr = "Reboot"; + break; + case Transaction::StoppedReason::Remote: + stoppedReasonCstr = "Remote"; + break; + case Transaction::StoppedReason::SOCLimitReached: + stoppedReasonCstr = "SOCLimitReached"; + break; + case Transaction::StoppedReason::StoppedByEV: + stoppedReasonCstr = "StoppedByEV"; + break; + case Transaction::StoppedReason::TimeLimitReached: + stoppedReasonCstr = "TimeLimitReached"; + break; + case Transaction::StoppedReason::Timeout: + stoppedReasonCstr = "Timeout"; + break; + } + + return stoppedReasonCstr; +} +bool deserializeTransactionStoppedReason(const char *stoppedReasonCstr, Transaction::StoppedReason& stoppedReasonOut) { + if (!stoppedReasonCstr || !*stoppedReasonCstr) { + stoppedReasonOut = Transaction::StoppedReason::UNDEFINED; + } else if (!strcmp(stoppedReasonCstr, "DeAuthorized")) { + stoppedReasonOut = Transaction::StoppedReason::DeAuthorized; + } else if (!strcmp(stoppedReasonCstr, "EmergencyStop")) { + stoppedReasonOut = Transaction::StoppedReason::EmergencyStop; + } else if (!strcmp(stoppedReasonCstr, "EnergyLimitReached")) { + stoppedReasonOut = Transaction::StoppedReason::EnergyLimitReached; + } else if (!strcmp(stoppedReasonCstr, "EVDisconnected")) { + stoppedReasonOut = Transaction::StoppedReason::EVDisconnected; + } else if (!strcmp(stoppedReasonCstr, "GroundFault")) { + stoppedReasonOut = Transaction::StoppedReason::GroundFault; + } else if (!strcmp(stoppedReasonCstr, "ImmediateReset")) { + stoppedReasonOut = Transaction::StoppedReason::ImmediateReset; + } else if (!strcmp(stoppedReasonCstr, "Local")) { + stoppedReasonOut = Transaction::StoppedReason::Local; + } else if (!strcmp(stoppedReasonCstr, "LocalOutOfCredit")) { + stoppedReasonOut = Transaction::StoppedReason::LocalOutOfCredit; + } else if (!strcmp(stoppedReasonCstr, "MasterPass")) { + stoppedReasonOut = Transaction::StoppedReason::MasterPass; + } else if (!strcmp(stoppedReasonCstr, "Other")) { + stoppedReasonOut = Transaction::StoppedReason::Other; + } else if (!strcmp(stoppedReasonCstr, "OvercurrentFault")) { + stoppedReasonOut = Transaction::StoppedReason::OvercurrentFault; + } else if (!strcmp(stoppedReasonCstr, "PowerLoss")) { + stoppedReasonOut = Transaction::StoppedReason::PowerLoss; + } else if (!strcmp(stoppedReasonCstr, "PowerQuality")) { + stoppedReasonOut = Transaction::StoppedReason::PowerQuality; + } else if (!strcmp(stoppedReasonCstr, "Reboot")) { + stoppedReasonOut = Transaction::StoppedReason::Reboot; + } else if (!strcmp(stoppedReasonCstr, "Remote")) { + stoppedReasonOut = Transaction::StoppedReason::Remote; + } else if (!strcmp(stoppedReasonCstr, "SOCLimitReached")) { + stoppedReasonOut = Transaction::StoppedReason::SOCLimitReached; + } else if (!strcmp(stoppedReasonCstr, "StoppedByEV")) { + stoppedReasonOut = Transaction::StoppedReason::StoppedByEV; + } else if (!strcmp(stoppedReasonCstr, "TimeLimitReached")) { + stoppedReasonOut = Transaction::StoppedReason::TimeLimitReached; + } else if (!strcmp(stoppedReasonCstr, "Timeout")) { + stoppedReasonOut = Transaction::StoppedReason::Timeout; + } else { + MO_DBG_ERR("deserialization error"); + return false; + } + return true; +} + +const char *serializeTransactionEventType(TransactionEventData::Type type) { + const char *typeCstr = ""; + switch (type) { + case TransactionEventData::Type::Ended: + typeCstr = "Ended"; + break; + case TransactionEventData::Type::Started: + typeCstr = "Started"; + break; + case TransactionEventData::Type::Updated: + typeCstr = "Updated"; + break; + } + return typeCstr; +} +bool deserializeTransactionEventType(const char *typeCstr, TransactionEventData::Type& typeOut) { + if (!strcmp(typeCstr, "Ended")) { + typeOut = TransactionEventData::Type::Ended; + } else if (!strcmp(typeCstr, "Started")) { + typeOut = TransactionEventData::Type::Started; + } else if (!strcmp(typeCstr, "Updated")) { + typeOut = TransactionEventData::Type::Updated; + } else { + MO_DBG_ERR("deserialization error"); + return false; + } + return true; +} + +const char *serializeTransactionEventTriggerReason(TransactionEventTriggerReason triggerReason) { + + const char *triggerReasonCstr = nullptr; + switch(triggerReason) { + case TransactionEventTriggerReason::UNDEFINED: + break; + case TransactionEventTriggerReason::Authorized: + triggerReasonCstr = "Authorized"; + break; + case TransactionEventTriggerReason::CablePluggedIn: + triggerReasonCstr = "CablePluggedIn"; + break; + case TransactionEventTriggerReason::ChargingRateChanged: + triggerReasonCstr = "ChargingRateChanged"; + break; + case TransactionEventTriggerReason::ChargingStateChanged: + triggerReasonCstr = "ChargingStateChanged"; + break; + case TransactionEventTriggerReason::Deauthorized: + triggerReasonCstr = "Deauthorized"; + break; + case TransactionEventTriggerReason::EnergyLimitReached: + triggerReasonCstr = "EnergyLimitReached"; + break; + case TransactionEventTriggerReason::EVCommunicationLost: + triggerReasonCstr = "EVCommunicationLost"; + break; + case TransactionEventTriggerReason::EVConnectTimeout: + triggerReasonCstr = "EVConnectTimeout"; + break; + case TransactionEventTriggerReason::MeterValueClock: + triggerReasonCstr = "MeterValueClock"; + break; + case TransactionEventTriggerReason::MeterValuePeriodic: + triggerReasonCstr = "MeterValuePeriodic"; + break; + case TransactionEventTriggerReason::TimeLimitReached: + triggerReasonCstr = "TimeLimitReached"; + break; + case TransactionEventTriggerReason::Trigger: + triggerReasonCstr = "Trigger"; + break; + case TransactionEventTriggerReason::UnlockCommand: + triggerReasonCstr = "UnlockCommand"; + break; + case TransactionEventTriggerReason::StopAuthorized: + triggerReasonCstr = "StopAuthorized"; + break; + case TransactionEventTriggerReason::EVDeparted: + triggerReasonCstr = "EVDeparted"; + break; + case TransactionEventTriggerReason::EVDetected: + triggerReasonCstr = "EVDetected"; + break; + case TransactionEventTriggerReason::RemoteStop: + triggerReasonCstr = "RemoteStop"; + break; + case TransactionEventTriggerReason::RemoteStart: + triggerReasonCstr = "RemoteStart"; + break; + case TransactionEventTriggerReason::AbnormalCondition: + triggerReasonCstr = "AbnormalCondition"; + break; + case TransactionEventTriggerReason::SignedDataReceived: + triggerReasonCstr = "SignedDataReceived"; + break; + case TransactionEventTriggerReason::ResetCommand: + triggerReasonCstr = "ResetCommand"; + break; + } + + return triggerReasonCstr; +} +bool deserializeTransactionEventTriggerReason(const char *triggerReasonCstr, TransactionEventTriggerReason& triggerReasonOut) { + if (!triggerReasonCstr || !*triggerReasonCstr) { + triggerReasonOut = TransactionEventTriggerReason::UNDEFINED; + } else if (!strcmp(triggerReasonCstr, "Authorized")) { + triggerReasonOut = TransactionEventTriggerReason::Authorized; + } else if (!strcmp(triggerReasonCstr, "CablePluggedIn")) { + triggerReasonOut = TransactionEventTriggerReason::CablePluggedIn; + } else if (!strcmp(triggerReasonCstr, "ChargingRateChanged")) { + triggerReasonOut = TransactionEventTriggerReason::ChargingRateChanged; + } else if (!strcmp(triggerReasonCstr, "ChargingStateChanged")) { + triggerReasonOut = TransactionEventTriggerReason::ChargingStateChanged; + } else if (!strcmp(triggerReasonCstr, "Deauthorized")) { + triggerReasonOut = TransactionEventTriggerReason::Deauthorized; + } else if (!strcmp(triggerReasonCstr, "EnergyLimitReached")) { + triggerReasonOut = TransactionEventTriggerReason::EnergyLimitReached; + } else if (!strcmp(triggerReasonCstr, "EVCommunicationLost")) { + triggerReasonOut = TransactionEventTriggerReason::EVCommunicationLost; + } else if (!strcmp(triggerReasonCstr, "EVConnectTimeout")) { + triggerReasonOut = TransactionEventTriggerReason::EVConnectTimeout; + } else if (!strcmp(triggerReasonCstr, "MeterValueClock")) { + triggerReasonOut = TransactionEventTriggerReason::MeterValueClock; + } else if (!strcmp(triggerReasonCstr, "MeterValuePeriodic")) { + triggerReasonOut = TransactionEventTriggerReason::MeterValuePeriodic; + } else if (!strcmp(triggerReasonCstr, "TimeLimitReached")) { + triggerReasonOut = TransactionEventTriggerReason::TimeLimitReached; + } else if (!strcmp(triggerReasonCstr, "Trigger")) { + triggerReasonOut = TransactionEventTriggerReason::Trigger; + } else if (!strcmp(triggerReasonCstr, "UnlockCommand")) { + triggerReasonOut = TransactionEventTriggerReason::UnlockCommand; + } else if (!strcmp(triggerReasonCstr, "StopAuthorized")) { + triggerReasonOut = TransactionEventTriggerReason::StopAuthorized; + } else if (!strcmp(triggerReasonCstr, "EVDeparted")) { + triggerReasonOut = TransactionEventTriggerReason::EVDeparted; + } else if (!strcmp(triggerReasonCstr, "EVDetected")) { + triggerReasonOut = TransactionEventTriggerReason::EVDetected; + } else if (!strcmp(triggerReasonCstr, "RemoteStop")) { + triggerReasonOut = TransactionEventTriggerReason::RemoteStop; + } else if (!strcmp(triggerReasonCstr, "RemoteStart")) { + triggerReasonOut = TransactionEventTriggerReason::RemoteStart; + } else if (!strcmp(triggerReasonCstr, "AbnormalCondition")) { + triggerReasonOut = TransactionEventTriggerReason::AbnormalCondition; + } else if (!strcmp(triggerReasonCstr, "SignedDataReceived")) { + triggerReasonOut = TransactionEventTriggerReason::SignedDataReceived; + } else if (!strcmp(triggerReasonCstr, "ResetCommand")) { + triggerReasonOut = TransactionEventTriggerReason::ResetCommand; + } else { + MO_DBG_ERR("deserialization error"); + return false; + } + return true; +} + +const char *serializeTransactionEventChargingState(TransactionEventData::ChargingState chargingState) { + const char *chargingStateCstr = nullptr; + switch (chargingState) { + case TransactionEventData::ChargingState::UNDEFINED: + // optional, okay + break; + case TransactionEventData::ChargingState::Charging: + chargingStateCstr = "Charging"; + break; + case TransactionEventData::ChargingState::EVConnected: + chargingStateCstr = "EVConnected"; + break; + case TransactionEventData::ChargingState::SuspendedEV: + chargingStateCstr = "SuspendedEV"; + break; + case TransactionEventData::ChargingState::SuspendedEVSE: + chargingStateCstr = "SuspendedEVSE"; + break; + case TransactionEventData::ChargingState::Idle: + chargingStateCstr = "Idle"; + break; + } + return chargingStateCstr; +} +bool deserializeTransactionEventChargingState(const char *chargingStateCstr, TransactionEventData::ChargingState& chargingStateOut) { + if (!chargingStateCstr || !*chargingStateCstr) { + chargingStateOut = TransactionEventData::ChargingState::UNDEFINED; + } else if (!strcmp(chargingStateCstr, "Charging")) { + chargingStateOut = TransactionEventData::ChargingState::Charging; + } else if (!strcmp(chargingStateCstr, "EVConnected")) { + chargingStateOut = TransactionEventData::ChargingState::EVConnected; + } else if (!strcmp(chargingStateCstr, "SuspendedEV")) { + chargingStateOut = TransactionEventData::ChargingState::SuspendedEV; + } else if (!strcmp(chargingStateCstr, "SuspendedEVSE")) { + chargingStateOut = TransactionEventData::ChargingState::SuspendedEVSE; + } else if (!strcmp(chargingStateCstr, "Idle")) { + chargingStateOut = TransactionEventData::ChargingState::Idle; + } else { + MO_DBG_ERR("deserialization error"); + return false; + } + return true; +} + +} //namespace Ocpp201 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V201 + +#if MO_ENABLE_V201 +bool g_ocpp_tx_compat_v201; + +void ocpp_tx_compat_setV201(bool isV201) { + g_ocpp_tx_compat_v201 = isV201; +} +#endif + int ocpp_tx_getTransactionId(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return -1; + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getTransactionId(); } +#if MO_ENABLE_V201 +const char *ocpp_tx_getTransactionIdV201(OCPP_Transaction *tx) { + if (!g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v201"); + return nullptr; + } + return reinterpret_cast(tx)->transactionId; +} +#endif //MO_ENABLE_V201 bool ocpp_tx_isAuthorized(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + return reinterpret_cast(tx)->isAuthorized; + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->isAuthorized(); } bool ocpp_tx_isIdTagDeauthorized(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + return reinterpret_cast(tx)->isDeauthorized; + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->isIdTagDeauthorized(); } bool ocpp_tx_isRunning(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + auto transaction = reinterpret_cast(tx); + return transaction->started && !transaction->stopped; + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->isRunning(); } bool ocpp_tx_isActive(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + return reinterpret_cast(tx)->active; + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->isActive(); } bool ocpp_tx_isAborted(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + auto transaction = reinterpret_cast(tx); + return !transaction->active && !transaction->started; + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->isAborted(); } bool ocpp_tx_isCompleted(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + auto transaction = reinterpret_cast(tx); + return transaction->stopped && transaction->seqNos.empty(); + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->isCompleted(); } const char *ocpp_tx_getIdTag(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + auto transaction = reinterpret_cast(tx); + return transaction->idToken.get(); + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getIdTag(); } +const char *ocpp_tx_getParentIdTag(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return nullptr; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->getParentIdTag(); +} + bool ocpp_tx_getBeginTimestamp(OCPP_Transaction *tx, char *buf, size_t len) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + return reinterpret_cast(tx)->beginTimestamp.toJsonString(buf, len); + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getBeginTimestamp().toJsonString(buf, len); } int32_t ocpp_tx_getMeterStart(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return -1; + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getMeterStart(); } bool ocpp_tx_getStartTimestamp(OCPP_Transaction *tx, char *buf, size_t len) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return -1; + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getStartTimestamp().toJsonString(buf, len); } const char *ocpp_tx_getStopIdTag(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + auto transaction = reinterpret_cast(tx); + return transaction->stopIdToken ? transaction->stopIdToken->get() : ""; + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getStopIdTag(); } int32_t ocpp_tx_getMeterStop(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return -1; + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getMeterStop(); } +void ocpp_tx_setMeterStop(OCPP_Transaction* tx, int32_t meter) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->setMeterStop(meter); +} + bool ocpp_tx_getStopTimestamp(OCPP_Transaction *tx, char *buf, size_t len) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return -1; + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getStopTimestamp().toJsonString(buf, len); } const char *ocpp_tx_getStopReason(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + auto transaction = reinterpret_cast(tx); + return serializeTransactionStoppedReason(transaction->stoppedReason); + } + #endif //MO_ENABLE_V201 return reinterpret_cast(tx)->getStopReason(); } diff --git a/src/MicroOcpp/Model/Transactions/Transaction.h b/src/MicroOcpp/Model/Transactions/Transaction.h index 1c711da6..5dc3e869 100644 --- a/src/MicroOcpp/Model/Transactions/Transaction.h +++ b/src/MicroOcpp/Model/Transactions/Transaction.h @@ -1,15 +1,49 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef TRANSACTION_H #define TRANSACTION_H +#include + +/* General Tx defs */ +#ifdef __cplusplus +extern "C" { +#endif //__cplusplus + +//TxNotification - event from MO to the main firmware to notify it about transaction state changes +typedef enum { + TxNotification_UNDEFINED, + + //Authorization events + TxNotification_Authorized, //success + TxNotification_AuthorizationRejected, //IdTag/token not authorized + TxNotification_AuthorizationTimeout, //authorization failed - offline + TxNotification_ReservationConflict, //connector/evse reserved for other IdTag + + TxNotification_ConnectionTimeout, //user took to long to plug vehicle after the authorization + TxNotification_DeAuthorized, //server rejected StartTx/TxEvent + TxNotification_RemoteStart, //authorized via RemoteStartTx/RequestStartTx + TxNotification_RemoteStop, //stopped via RemoteStopTx/RequestStopTx + + //Tx lifecycle events + TxNotification_StartTx, //entered running state (StartTx/TxEvent was initiated) + TxNotification_StopTx, //left running state (StopTx/TxEvent was initiated) +} TxNotification; + +#ifdef __cplusplus +} +#endif //__cplusplus + #ifdef __cplusplus #include +#include #include +#define MAX_TX_CNT 100000U //upper limit of txNr (internal usage). Must be at least 2*MO_TXRECORD_SIZE+1 + namespace MicroOcpp { /* @@ -26,14 +60,25 @@ class SendStatus { private: bool requested = false; bool confirmed = false; + + unsigned int opNr = 0; + unsigned int attemptNr = 0; + Timestamp attemptTime = MIN_TIME; public: void setRequested() {this->requested = true;} bool isRequested() {return requested;} void confirm() {confirmed = true;} bool isConfirmed() {return confirmed;} + void setOpNr(unsigned int opNr) {this->opNr = opNr;} + unsigned int getOpNr() {return opNr;} + void advanceAttemptNr() {attemptNr++;} + void setAttemptNr(unsigned int attemptNr) {this->attemptNr = attemptNr;} + unsigned int getAttemptNr() {return attemptNr;} + const Timestamp& getAttemptTime() {return attemptTime;} + void setAttemptTime(const Timestamp& timestamp) {attemptTime = timestamp;} }; -class Transaction { +class Transaction : public MemoryManaged { private: ConnectorTransactionStore& context; @@ -43,6 +88,7 @@ class Transaction { * Attributes existing before StartTransaction */ char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; + char parentIdTag [IDTAG_LEN_MAX + 1] = {'\0'}; bool authorized = false; //if the given idTag was authorized bool deauthorized = false; //if the server revoked a local authorization Timestamp begin_timestamp = MIN_TIME; @@ -78,6 +124,7 @@ class Transaction { public: Transaction(ConnectorTransactionStore& context, unsigned int connectorId, unsigned int txNr, bool silent = false) : + MemoryManaged("v16.Transactions.Transaction"), context(context), connectorId(connectorId), txNr(txNr), @@ -111,6 +158,9 @@ class Transaction { bool setIdTag(const char *idTag); const char *getIdTag() {return idTag;} + bool setParentIdTag(const char *idTag); + const char *getParentIdTag() {return parentIdTag;} + void setAuthorized() {authorized = true;} void setIdTagDeauthorized() {deauthorized = true;} @@ -165,7 +215,235 @@ class Transaction { bool isSilent() {return silent;} //no data will be sent to server and server will not assign transactionId }; -} +} // namespace MicroOcpp + +#if MO_ENABLE_V201 + +#include +#include + +#include +#include +#include +#include + +#ifndef MO_SAMPLEDDATATXENDED_SIZE_MAX +#define MO_SAMPLEDDATATXENDED_SIZE_MAX 5 +#endif + +namespace MicroOcpp { +namespace Ocpp201 { + +// TriggerReasonEnumType (3.82) +enum class TransactionEventTriggerReason : uint8_t { + UNDEFINED, // not part of OCPP + Authorized, + CablePluggedIn, + ChargingRateChanged, + ChargingStateChanged, + Deauthorized, + EnergyLimitReached, + EVCommunicationLost, + EVConnectTimeout, + MeterValueClock, + MeterValuePeriodic, + TimeLimitReached, + Trigger, + UnlockCommand, + StopAuthorized, + EVDeparted, + EVDetected, + RemoteStop, + RemoteStart, + AbnormalCondition, + SignedDataReceived, + ResetCommand +}; + +class Transaction : public MemoryManaged { +public: + + // ReasonEnumType (3.67) + enum class StoppedReason : uint8_t { + UNDEFINED, // not part of OCPP + DeAuthorized, + EmergencyStop, + EnergyLimitReached, + EVDisconnected, + GroundFault, + ImmediateReset, + Local, + LocalOutOfCredit, + MasterPass, + Other, + OvercurrentFault, + PowerLoss, + PowerQuality, + Reboot, + Remote, + SOCLimitReached, + StoppedByEV, + TimeLimitReached, + Timeout + }; + +//private: + /* + * Transaction substates. Notify server about any change when transaction is running + */ + //bool trackParkingBayOccupancy; // not supported + bool trackEvConnected = false; + bool trackAuthorized = false; + bool trackDataSigned = false; + bool trackPowerPathClosed = false; + bool trackEnergyTransfer = false; + + /* + * Transaction lifecycle + */ + bool active = true; //once active is false, the tx must stop (or cannot start at all) + bool started = false; //if a TxEvent with event type TxStarted has been initiated + bool stopped = false; //if a TxEvent with event type TxEnded has been initiated + + /* + * Global transaction data + */ + bool isAuthorizationActive = false; //period between beginAuthorization and endAuthorization + bool isAuthorized = false; //if the given idToken was authorized + bool isDeauthorized = false; //if the server revoked a local authorization + IdToken idToken; + Timestamp beginTimestamp = MIN_TIME; + char transactionId [MO_TXID_LEN_MAX + 1] = {'\0'}; + int remoteStartId = -1; + + //if to fill next TxEvent with optional fields + bool notifyEvseId = false; + bool notifyIdToken = false; + bool notifyStopIdToken = false; + bool notifyReservationId = false; + bool notifyChargingState = false; + bool notifyRemoteStartId = false; + + bool evConnectionTimeoutListen = true; + + StoppedReason stoppedReason = StoppedReason::UNDEFINED; + TransactionEventTriggerReason stopTrigger = TransactionEventTriggerReason::UNDEFINED; + std::unique_ptr stopIdToken; // if null, then stopIdToken equals idToken + + /* + * Tx-related metering + */ + + Vector> sampledDataTxEnded; + + unsigned long lastSampleTimeTxUpdated = 0; //0 means not charging right now + unsigned long lastSampleTimeTxEnded = 0; + + /* + * Attributes for internal store + */ + unsigned int evseId = 0; + unsigned int txNr = 0; //internal key attribute (!= transactionId); {evseId*txNr} is unique key + + unsigned int seqNoEnd = 0; // increment by 1 for each event + Vector seqNos; //track stored txEvents + + bool silent = false; //silent Tx: process tx locally, without reporting to the server + + Transaction() : + MemoryManaged("v201.Transactions.Transaction"), + sampledDataTxEnded(makeVector>(getMemoryTag())), + seqNos(makeVector(getMemoryTag())) { } + + void addSampledDataTxEnded(std::unique_ptr mv) { + if (sampledDataTxEnded.size() >= MO_SAMPLEDDATATXENDED_SIZE_MAX) { + int deltaMin = std::numeric_limits::max(); + size_t indexMin = sampledDataTxEnded.size(); + for (size_t i = 1; i + 1 <= sampledDataTxEnded.size(); i++) { + size_t t0 = sampledDataTxEnded.size() - i - 1; + size_t t1 = sampledDataTxEnded.size() - i; + + auto delta = sampledDataTxEnded[t1]->getTimestamp() - sampledDataTxEnded[t0]->getTimestamp(); + + if (delta < deltaMin) { + deltaMin = delta; + indexMin = t1; + } + } + + sampledDataTxEnded.erase(sampledDataTxEnded.begin() + indexMin); + } + + sampledDataTxEnded.push_back(std::move(mv)); + } +}; + +// TransactionEventRequest (1.60.1) +class TransactionEventData : public MemoryManaged { +public: + + // TransactionEventEnumType (3.80) + enum class Type : uint8_t { + Ended, + Started, + Updated + }; + + // ChargingStateEnumType (3.16) + enum class ChargingState : uint8_t { + UNDEFINED, // not part of OCPP + Charging, + EVConnected, + SuspendedEV, + SuspendedEVSE, + Idle + }; + +//private: + Transaction *transaction; + Type eventType; + Timestamp timestamp; + uint16_t bootNr = 0; + TransactionEventTriggerReason triggerReason; + const unsigned int seqNo; + bool offline = false; + int numberOfPhasesUsed = -1; + int cableMaxCurrent = -1; + int reservationId = -1; + int remoteStartId = -1; + + // TransactionType (2.48) + ChargingState chargingState = ChargingState::UNDEFINED; + //int timeSpentCharging = 0; // not supported + std::unique_ptr idToken; + EvseId evse = -1; + //meterValue not supported + Vector> meterValue; + + unsigned int opNr = 0; + unsigned int attemptNr = 0; + Timestamp attemptTime = MIN_TIME; + + TransactionEventData(Transaction *transaction, unsigned int seqNo) : MemoryManaged("v201.Transactions.TransactionEventData"), transaction(transaction), seqNo(seqNo), meterValue(makeVector>(getMemoryTag())) { } +}; + +const char *serializeTransactionStoppedReason(Transaction::StoppedReason stoppedReason); +bool deserializeTransactionStoppedReason(const char *stoppedReasonCstr, Transaction::StoppedReason& stoppedReasonOut); + +const char *serializeTransactionEventType(TransactionEventData::Type type); +bool deserializeTransactionEventType(const char *typeCstr, TransactionEventData::Type& typeOut); + +const char *serializeTransactionEventTriggerReason(TransactionEventTriggerReason triggerReason); +bool deserializeTransactionEventTriggerReason(const char *triggerReasonCstr, TransactionEventTriggerReason& triggerReasonOut); + +const char *serializeTransactionEventChargingState(TransactionEventData::ChargingState chargingState); +bool deserializeTransactionEventChargingState(const char *chargingStateCstr, TransactionEventData::ChargingState& chargingStateOut); + + +} // namespace Ocpp201 +} // namespace MicroOcpp + +#endif // MO_ENABLE_V201 extern "C" { #endif //__cplusplus @@ -173,7 +451,18 @@ extern "C" { 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); @@ -184,6 +473,8 @@ 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); @@ -193,6 +484,7 @@ 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); diff --git a/src/MicroOcpp/Model/Transactions/TransactionDefs.h b/src/MicroOcpp/Model/Transactions/TransactionDefs.h new file mode 100644 index 00000000..62ef657a --- /dev/null +++ b/src/MicroOcpp/Model/Transactions/TransactionDefs.h @@ -0,0 +1,15 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_TRANSACTIONDEFS_H +#define MO_TRANSACTIONDEFS_H + +#include + +#if MO_ENABLE_V201 + +#define MO_TXID_LEN_MAX 36 + +#endif //MO_ENABLE_V201 +#endif diff --git a/src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp b/src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp index b1262ff5..e3c412f0 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp +++ b/src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -16,6 +16,15 @@ bool serializeSendStatus(SendStatus& status, JsonObject out) { 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; } @@ -26,11 +35,24 @@ bool deserializeSendStatus(SendStatus& status, JsonObject in) { 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, DynamicJsonDocument& out) { - out = DynamicJsonDocument(1024); +bool serializeTransaction(Transaction& tx, JsonDoc& out) { + out = initJsonDoc("v16.Transactions.TransactionDeserialize", 1024); JsonObject state = out.to(); JsonObject sessionState = state.createNestedObject("session"); @@ -40,6 +62,9 @@ bool serializeTransaction(Transaction& tx, DynamicJsonDocument& out) { if (tx.getIdTag()[0] != '\0') { sessionState["idTag"] = tx.getIdTag(); } + if (tx.getParentIdTag()[0] != '\0') { + sessionState["parentIdTag"] = tx.getParentIdTag(); + } if (tx.isAuthorized()) { sessionState["authorized"] = true; } @@ -129,6 +154,13 @@ bool deserializeTransaction(Transaction& tx, JsonObject state) { } } + if (sessionState.containsKey("parentIdTag")) { + if (!tx.setParentIdTag(sessionState["parentIdTag"] | "")) { + MO_DBG_ERR("read err"); + return false; + } + } + if (sessionState["authorized"] | false) { tx.setAuthorized(); } @@ -234,13 +266,12 @@ bool deserializeTransaction(Transaction& tx, JsonObject state) { tx.setSilent(); } - MO_DBG_DEBUG("DUMP TX"); + 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"); - (void)0; } return true; diff --git a/src/MicroOcpp/Model/Transactions/TransactionDeserialize.h b/src/MicroOcpp/Model/Transactions/TransactionDeserialize.h index b1f3e200..c8cfc427 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionDeserialize.h +++ b/src/MicroOcpp/Model/Transactions/TransactionDeserialize.h @@ -1,17 +1,18 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef TRANSACTIONDESERIALIZE_H -#define TRANSACTIONDESERIALIZE_H +#ifndef MO_TRANSACTIONDESERIALIZE_H +#define MO_TRANSACTIONDESERIALIZE_H #include +#include #include namespace MicroOcpp { -bool serializeTransaction(Transaction& tx, DynamicJsonDocument& out); +bool serializeTransaction(Transaction& tx, JsonDoc& out); bool deserializeTransaction(Transaction& tx, JsonObject in); } diff --git a/src/MicroOcpp/Model/Transactions/TransactionService.cpp b/src/MicroOcpp/Model/Transactions/TransactionService.cpp new file mode 100644 index 00000000..d0e58e1f --- /dev/null +++ b/src/MicroOcpp/Model/Transactions/TransactionService.cpp @@ -0,0 +1,1109 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef MO_TX_CLEAN_ABORTED +#define MO_TX_CLEAN_ABORTED 1 +#endif + +using namespace MicroOcpp; +using namespace MicroOcpp::Ocpp201; + +TransactionService::Evse::Evse(Context& context, TransactionService& txService, Ocpp201::TransactionStoreEvse& txStore, unsigned int evseId) : + MemoryManaged("v201.Transactions.TransactionServiceEvse"), + context(context), + txService(txService), + txStore(txStore), + evseId(evseId) { + + context.getRequestQueue().addSendQueue(this); //register at RequestQueue as Request emitter + + txStore.discoverStoredTx(txNrBegin, txNrEnd); //initializes txNrBegin and txNrEnd + txNrFront = txNrBegin; + MO_DBG_DEBUG("found %u transactions for evseId %u. Internal range from %u to %u (exclusive)", (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT, evseId, txNrBegin, txNrEnd); + + unsigned int txNrLatest = (txNrEnd + MAX_TX_CNT - 1) % MAX_TX_CNT; //txNr of the most recent tx on flash + transaction = txStore.loadTransaction(txNrLatest); //returns nullptr if txNrLatest does not exist on flash +} + +TransactionService::Evse::~Evse() { + +} + +bool TransactionService::Evse::beginTransaction() { + + if (transaction) { + MO_DBG_ERR("transaction still running"); + return false; + } + + std::unique_ptr tx; + + char txId [sizeof(Ocpp201::Transaction::transactionId)]; + + //simple clock-based hash + int v = context.getModel().getClock().now() - Timestamp(2020,0,0,0,0,0); + unsigned int h = v; + h += mocpp_tick_ms(); + h *= 749572633U; + h %= 24593209U; + for (size_t i = 0; i < sizeof(tx->transactionId) - 3; i += 2) { + snprintf(txId + i, 3, "%02X", (uint8_t)h); + h *= 749572633U; + h %= 24593209U; + } + + //clean possible aborted tx + unsigned int txr = txNrEnd; + unsigned int txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; + for (unsigned int i = 0; i < txSize; i++) { + txr = (txr + MAX_TX_CNT - 1) % MAX_TX_CNT; //decrement by 1 + + std::unique_ptr intermediateTx; + + Ocpp201::Transaction *txhist = nullptr; + if (transaction && transaction->txNr == txr) { + txhist = transaction.get(); + } else if (txFront && txFront->txNr == txr) { + txhist = txFront; + } else { + intermediateTx = txStore.loadTransaction(txr); + txhist = intermediateTx.get(); + } + + //check if dangling silent tx, aborted tx, or corrupted entry (txhist == null) + if (!txhist || txhist->silent || (!txhist->active && !txhist->started && MO_TX_CLEAN_ABORTED)) { + //yes, remove + if (txStore.remove(txr)) { + if (txNrFront == txNrEnd) { + txNrFront = txr; + } + txNrEnd = txr; + MO_DBG_WARN("deleted dangling silent or aborted tx for new transaction"); + } else { + MO_DBG_ERR("memory corruption"); + break; + } + } else { + //no, tx record trimmed, end + break; + } + } + + txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; //refresh after cleaning txs + + //try to create new transaction + if (txSize < MO_TXRECORD_SIZE) { + tx = txStore.createTransaction(txNrEnd, txId); + } + + if (!tx) { + //could not create transaction - now, try to replace tx history entry + + unsigned int txl = txNrBegin; + txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; + + for (unsigned int i = 0; i < txSize; i++) { + + if (tx) { + //success, finished here + break; + } + + //no transaction allocated, delete history entry to make space + std::unique_ptr intermediateTx; + + Ocpp201::Transaction *txhist = nullptr; + if (transaction && transaction->txNr == txl) { + txhist = transaction.get(); + } else if (txFront && txFront->txNr == txl) { + txhist = txFront; + } else { + intermediateTx = txStore.loadTransaction(txl); + txhist = intermediateTx.get(); + } + + //oldest entry, now check if it's history and can be removed or corrupted entry + if (!txhist || (txhist->stopped && txhist->seqNos.empty()) || (!txhist->active && !txhist->started) || (txhist->silent && txhist->stopped)) { + //yes, remove + + if (txStore.remove(txl)) { + txNrBegin = (txl + 1) % MAX_TX_CNT; + if (txNrFront == txl) { + txNrFront = txNrBegin; + } + MO_DBG_DEBUG("deleted tx history entry for new transaction"); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + + tx = txStore.createTransaction(txNrEnd, txId); + } else { + MO_DBG_ERR("memory corruption"); + break; + } + } else { + //no, end of history reached, don't delete further tx + MO_DBG_DEBUG("cannot delete more tx"); + break; + } + + txl++; + txl %= MAX_TX_CNT; + } + } + + if (!tx) { + //couldn't create normal transaction -> check if to start charging without real transaction + if (txService.silentOfflineTransactionsBool && txService.silentOfflineTransactionsBool->getBool()) { + //try to handle charging session without sending StartTx or StopTx to the server + tx = txStore.createTransaction(txNrEnd, txId); + + if (tx) { + tx->silent = true; + MO_DBG_DEBUG("created silent transaction"); + } + } + } + + if (!tx) { + MO_DBG_ERR("transaction queue full"); + return false; + } + + tx->beginTimestamp = context.getModel().getClock().now(); + + if (!txStore.commit(tx.get())) { + MO_DBG_ERR("fs error"); + return false; + } + + transaction = std::move(tx); + + txNrEnd = (txNrEnd + 1) % MAX_TX_CNT; + MO_DBG_DEBUG("advance txNrEnd %u-%u", evseId, txNrEnd); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + + return true; +} + +bool TransactionService::Evse::endTransaction(Ocpp201::Transaction::StoppedReason stoppedReason = Ocpp201::Transaction::StoppedReason::Other, Ocpp201::TransactionEventTriggerReason stopTrigger = Ocpp201::TransactionEventTriggerReason::AbnormalCondition) { + + if (!transaction || !transaction->active) { + //transaction already ended / not active anymore + return false; + } + + MO_DBG_DEBUG("End transaction started by idTag %s", + transaction->idToken.get()); + + transaction->active = false; + transaction->stopTrigger = stopTrigger; + transaction->stoppedReason = stoppedReason; + txStore.commit(transaction.get()); + + return true; +} + +void TransactionService::Evse::loop() { + + if (transaction && !transaction->active && !transaction->started) { + MO_DBG_DEBUG("collect aborted transaction %u-%s", evseId, transaction->transactionId); + if (txFront == transaction.get()) { + MO_DBG_DEBUG("pass ownership from tx to txFront"); + txFrontCache = std::move(transaction); + } + transaction = nullptr; + } + + if (transaction && transaction->stopped) { + MO_DBG_DEBUG("collect obsolete transaction %u-%s", evseId, transaction->transactionId); + if (txFront == transaction.get()) { + MO_DBG_DEBUG("pass ownership from tx to txFront"); + txFrontCache = std::move(transaction); + } + transaction = nullptr; + } + + // tx-related behavior + if (transaction) { + if (connectorPluggedInput) { + if (connectorPluggedInput()) { + // if cable has been plugged at least once, EVConnectionTimeout will never get triggered + transaction->evConnectionTimeoutListen = false; + } + + if (transaction->active && + transaction->evConnectionTimeoutListen && + transaction->beginTimestamp > MIN_TIME && + txService.evConnectionTimeOutInt && txService.evConnectionTimeOutInt->getInt() > 0 && + !connectorPluggedInput() && + context.getModel().getClock().now() - transaction->beginTimestamp >= txService.evConnectionTimeOutInt->getInt()) { + + MO_DBG_INFO("Session mngt: timeout"); + endTransaction(Ocpp201::Transaction::StoppedReason::Timeout, TransactionEventTriggerReason::EVConnectTimeout); + + updateTxNotification(TxNotification_ConnectionTimeout); + } + + if (transaction->active && + transaction->isDeauthorized && + !transaction->started && + (txService.isTxStartPoint(TxStartStopPoint::Authorized) || txService.isTxStartPoint(TxStartStopPoint::PowerPathClosed) || + txService.isTxStopPoint(TxStartStopPoint::Authorized) || txService.isTxStopPoint(TxStartStopPoint::PowerPathClosed))) { + + MO_DBG_INFO("Session mngt: Deauthorized before start"); + endTransaction(Ocpp201::Transaction::StoppedReason::DeAuthorized, TransactionEventTriggerReason::Deauthorized); + } + } + } + + std::unique_ptr txEvent; + + bool txStopCondition = false; + + { + // stop tx? + + TransactionEventTriggerReason triggerReason = TransactionEventTriggerReason::UNDEFINED; + Ocpp201::Transaction::StoppedReason stoppedReason = Ocpp201::Transaction::StoppedReason::UNDEFINED; + + if (transaction && !transaction->active) { + // tx ended via endTransaction + txStopCondition = true; + triggerReason = transaction->stopTrigger; + stoppedReason = transaction->stoppedReason; + } else if ((txService.isTxStopPoint(TxStartStopPoint::EVConnected) || + txService.isTxStopPoint(TxStartStopPoint::PowerPathClosed)) && + connectorPluggedInput && !connectorPluggedInput() && + (txService.stopTxOnEVSideDisconnectBool->getBool() || !transaction || !transaction->started)) { + txStopCondition = true; + triggerReason = TransactionEventTriggerReason::EVCommunicationLost; + stoppedReason = Ocpp201::Transaction::StoppedReason::EVDisconnected; + } else if ((txService.isTxStopPoint(TxStartStopPoint::Authorized) || + txService.isTxStopPoint(TxStartStopPoint::PowerPathClosed)) && + (!transaction || !transaction->isAuthorizationActive)) { + // user revoked authorization (or EV or any "local" entity) + txStopCondition = true; + triggerReason = TransactionEventTriggerReason::StopAuthorized; + stoppedReason = Ocpp201::Transaction::StoppedReason::Local; + } else if (txService.isTxStopPoint(TxStartStopPoint::EnergyTransfer) && + evReadyInput && !evReadyInput()) { + txStopCondition = true; + triggerReason = TransactionEventTriggerReason::ChargingStateChanged; + stoppedReason = Ocpp201::Transaction::StoppedReason::StoppedByEV; + } else if (txService.isTxStopPoint(TxStartStopPoint::EnergyTransfer) && + (evReadyInput || evseReadyInput) && // at least one of the two defined + !(evReadyInput && evReadyInput()) && + !(evseReadyInput && evseReadyInput())) { + txStopCondition = true; + triggerReason = TransactionEventTriggerReason::ChargingStateChanged; + stoppedReason = Ocpp201::Transaction::StoppedReason::Other; + } else if (txService.isTxStopPoint(TxStartStopPoint::Authorized) && + transaction && transaction->isDeauthorized && + txService.stopTxOnInvalidIdBool->getBool()) { + // OCPP server rejected authorization + txStopCondition = true; + triggerReason = TransactionEventTriggerReason::Deauthorized; + stoppedReason = Ocpp201::Transaction::StoppedReason::DeAuthorized; + } + + if (txStopCondition && + transaction && transaction->started && transaction->active) { + + MO_DBG_INFO("Session mngt: TxStopPoint reached"); + endTransaction(stoppedReason, triggerReason); + } + + if (transaction && + transaction->started && !transaction->stopped && !transaction->active && + (!stopTxReadyInput || stopTxReadyInput())) { + // yes, stop running tx + + txEvent = txStore.createTransactionEvent(*transaction); + if (!txEvent) { + // OOM + return; + } + + transaction->stopTrigger = triggerReason; + transaction->stoppedReason = stoppedReason; + + txEvent->eventType = TransactionEventData::Type::Ended; + txEvent->triggerReason = triggerReason; + } + } + + if (!txStopCondition) { + // start tx? + + bool txStartCondition = false; + + TransactionEventTriggerReason triggerReason = TransactionEventTriggerReason::UNDEFINED; + + // tx should be started? + if (txService.isTxStartPoint(TxStartStopPoint::PowerPathClosed) && + (!connectorPluggedInput || connectorPluggedInput()) && + transaction && transaction->isAuthorizationActive && transaction->isAuthorized) { + txStartCondition = true; + if (transaction->remoteStartId >= 0) { + triggerReason = TransactionEventTriggerReason::RemoteStart; + } else { + triggerReason = TransactionEventTriggerReason::CablePluggedIn; + } + } else if (txService.isTxStartPoint(TxStartStopPoint::Authorized) && + transaction && transaction->isAuthorizationActive && transaction->isAuthorized) { + txStartCondition = true; + if (transaction->remoteStartId >= 0) { + triggerReason = TransactionEventTriggerReason::RemoteStart; + } else { + triggerReason = TransactionEventTriggerReason::Authorized; + } + } else if (txService.isTxStartPoint(TxStartStopPoint::EVConnected) && + connectorPluggedInput && connectorPluggedInput()) { + txStartCondition = true; + triggerReason = TransactionEventTriggerReason::CablePluggedIn; + } else if (txService.isTxStartPoint(TxStartStopPoint::EnergyTransfer) && + (evReadyInput || evseReadyInput) && // at least one of the two defined + (!evReadyInput || evReadyInput()) && + (!evseReadyInput || evseReadyInput())) { + txStartCondition = true; + triggerReason = TransactionEventTriggerReason::ChargingStateChanged; + } + + if (txStartCondition && + (!transaction || (transaction->active && !transaction->started)) && + (!startTxReadyInput || startTxReadyInput())) { + // start tx + + if (!transaction) { + beginTransaction(); + if (!transaction) { + // OOM + return; + } + if (evseId > 0) { + transaction->notifyEvseId = true; + } + } + + txEvent = txStore.createTransactionEvent(*transaction); + if (!txEvent) { + // OOM + return; + } + + txEvent->eventType = TransactionEventData::Type::Started; + txEvent->triggerReason = triggerReason; + } + } + + TransactionEventData::ChargingState chargingState = TransactionEventData::ChargingState::Idle; + if (connectorPluggedInput && !connectorPluggedInput()) { + chargingState = TransactionEventData::ChargingState::Idle; + } else if (!transaction || !transaction->isAuthorizationActive || !transaction->isAuthorized) { + chargingState = TransactionEventData::ChargingState::EVConnected; + } else if (evseReadyInput && !evseReadyInput()) { + chargingState = TransactionEventData::ChargingState::SuspendedEVSE; + } else if (evReadyInput && !evReadyInput()) { + chargingState = TransactionEventData::ChargingState::SuspendedEV; + } else if (ocppPermitsCharge()) { + chargingState = TransactionEventData::ChargingState::Charging; + } + + //General Metering behavior. There is another section for TxStarted, Updated and TxEnded MeterValues + std::unique_ptr mvTxUpdated; + + if (transaction) { + + if (txService.sampledDataTxUpdatedInterval && txService.sampledDataTxUpdatedInterval->getInt() > 0 && mocpp_tick_ms() - transaction->lastSampleTimeTxUpdated >= (unsigned long)txService.sampledDataTxUpdatedInterval->getInt() * 1000UL) { + transaction->lastSampleTimeTxUpdated = mocpp_tick_ms(); + auto meteringService = context.getModel().getMeteringServiceV201(); + auto meteringEvse = meteringService ? meteringService->getEvse(evseId) : nullptr; + mvTxUpdated = meteringEvse ? meteringEvse->takeTxUpdatedMeterValue() : nullptr; + } + + if (transaction->started && !transaction->stopped && + txService.sampledDataTxEndedInterval && txService.sampledDataTxEndedInterval->getInt() > 0 && + mocpp_tick_ms() - transaction->lastSampleTimeTxEnded >= (unsigned long)txService.sampledDataTxEndedInterval->getInt() * 1000UL) { + transaction->lastSampleTimeTxEnded = mocpp_tick_ms(); + auto meteringService = context.getModel().getMeteringServiceV201(); + auto meteringEvse = meteringService ? meteringService->getEvse(evseId) : nullptr; + auto mvTxEnded = meteringEvse ? meteringEvse->takeTxEndedMeterValue(ReadingContext_SamplePeriodic) : nullptr; + if (mvTxEnded) { + transaction->addSampledDataTxEnded(std::move(mvTxEnded)); + } + } + } + + if (transaction) { + // update tx? + + bool txUpdateCondition = false; + + TransactionEventTriggerReason triggerReason = TransactionEventTriggerReason::UNDEFINED; + + if (chargingState != trackChargingState) { + txUpdateCondition = true; + triggerReason = TransactionEventTriggerReason::ChargingStateChanged; + transaction->notifyChargingState = true; + } + trackChargingState = chargingState; + + if ((transaction->isAuthorizationActive && transaction->isAuthorized) && !transaction->trackAuthorized) { + transaction->trackAuthorized = true; + txUpdateCondition = true; + if (transaction->remoteStartId >= 0) { + triggerReason = TransactionEventTriggerReason::RemoteStart; + } else { + triggerReason = TransactionEventTriggerReason::Authorized; + } + } else if (connectorPluggedInput && connectorPluggedInput() && !transaction->trackEvConnected) { + transaction->trackEvConnected = true; + txUpdateCondition = true; + triggerReason = TransactionEventTriggerReason::CablePluggedIn; + } else if (connectorPluggedInput && !connectorPluggedInput() && transaction->trackEvConnected) { + transaction->trackEvConnected = false; + txUpdateCondition = true; + triggerReason = TransactionEventTriggerReason::EVCommunicationLost; + } else if (!(transaction->isAuthorizationActive && transaction->isAuthorized) && transaction->trackAuthorized) { + transaction->trackAuthorized = false; + txUpdateCondition = true; + triggerReason = TransactionEventTriggerReason::StopAuthorized; + } else if (mvTxUpdated) { + txUpdateCondition = true; + triggerReason = TransactionEventTriggerReason::MeterValuePeriodic; + } else if (evReadyInput && evReadyInput() && !transaction->trackPowerPathClosed) { + transaction->trackPowerPathClosed = true; + } else if (evReadyInput && !evReadyInput() && transaction->trackPowerPathClosed) { + transaction->trackPowerPathClosed = false; + } + + if (txUpdateCondition && !txEvent && transaction->started && !transaction->stopped) { + // yes, updated + + txEvent = txStore.createTransactionEvent(*transaction); + if (!txEvent) { + // OOM + return; + } + + txEvent->eventType = TransactionEventData::Type::Updated; + txEvent->triggerReason = triggerReason; + } + } + + if (txEvent) { + txEvent->timestamp = context.getModel().getClock().now(); + if (transaction->notifyChargingState) { + txEvent->chargingState = chargingState; + transaction->notifyChargingState = false; + } + if (transaction->notifyEvseId) { + txEvent->evse = EvseId(evseId, 1); + transaction->notifyEvseId = false; + } + if (transaction->notifyRemoteStartId) { + txEvent->remoteStartId = transaction->remoteStartId; + transaction->notifyRemoteStartId = false; + } + if (txEvent->eventType == TransactionEventData::Type::Started) { + auto meteringService = context.getModel().getMeteringServiceV201(); + auto meteringEvse = meteringService ? meteringService->getEvse(evseId) : nullptr; + auto mvTxStarted = meteringEvse ? meteringEvse->takeTxStartedMeterValue() : nullptr; + if (mvTxStarted) { + txEvent->meterValue.push_back(std::move(mvTxStarted)); + } + auto mvTxEnded = meteringEvse ? meteringEvse->takeTxEndedMeterValue(ReadingContext_TransactionBegin) : nullptr; + if (mvTxEnded) { + transaction->addSampledDataTxEnded(std::move(mvTxEnded)); + } + transaction->lastSampleTimeTxEnded = mocpp_tick_ms(); + transaction->lastSampleTimeTxUpdated = mocpp_tick_ms(); + } else if (txEvent->eventType == TransactionEventData::Type::Ended) { + auto meteringService = context.getModel().getMeteringServiceV201(); + auto meteringEvse = meteringService ? meteringService->getEvse(evseId) : nullptr; + auto mvTxEnded = meteringEvse ? meteringEvse->takeTxEndedMeterValue(ReadingContext_TransactionEnd) : nullptr; + if (mvTxEnded) { + transaction->addSampledDataTxEnded(std::move(mvTxEnded)); + } + transaction->lastSampleTimeTxEnded = mocpp_tick_ms(); + } + if (mvTxUpdated) { + txEvent->meterValue.push_back(std::move(mvTxUpdated)); + } + + if (transaction->notifyStopIdToken && transaction->stopIdToken) { + txEvent->idToken = std::unique_ptr(new IdToken(*transaction->stopIdToken.get(), getMemoryTag())); + transaction->notifyStopIdToken = false; + } else if (transaction->notifyIdToken) { + txEvent->idToken = std::unique_ptr(new IdToken(transaction->idToken, getMemoryTag())); + transaction->notifyIdToken = false; + } + } + + if (txEvent) { + if (txEvent->eventType == TransactionEventData::Type::Started) { + transaction->started = true; + } else if (txEvent->eventType == TransactionEventData::Type::Ended) { + transaction->stopped = true; + } + } + + if (txEvent) { + txEvent->opNr = context.getRequestQueue().getNextOpNr(); + MO_DBG_DEBUG("enqueueing new txEvent at opNr %u", txEvent->opNr); + } + + if (txEvent) { + txStore.commit(txEvent.get()); + } + + if (txEvent) { + if (txEvent->eventType == TransactionEventData::Type::Started) { + updateTxNotification(TxNotification_StartTx); + } else if (txEvent->eventType == TransactionEventData::Type::Ended) { + updateTxNotification(TxNotification_StartTx); + } + } + + //try to pass ownership to front txEvent immediatley + if (txEvent && !txEventFront && + transaction->txNr == txNrFront && + !transaction->seqNos.empty() && transaction->seqNos.front() == txEvent->seqNo) { + + //txFront set up? + if (!txFront) { + txFront = transaction.get(); + } + + //keep txEvent loaded (otherwise ReqEmitter would load it again from flash) + MO_DBG_DEBUG("new txEvent is front element"); + txEventFront = std::move(txEvent); + } +} + +void TransactionService::Evse::setConnectorPluggedInput(std::function connectorPlugged) { + this->connectorPluggedInput = connectorPlugged; +} + +void TransactionService::Evse::setEvReadyInput(std::function evRequestsEnergy) { + this->evReadyInput = evRequestsEnergy; +} + +void TransactionService::Evse::setEvseReadyInput(std::function connectorEnergized) { + this->evseReadyInput = connectorEnergized; +} + +void TransactionService::Evse::setTxNotificationOutput(std::function txNotificationOutput) { + this->txNotificationOutput = txNotificationOutput; +} + +void TransactionService::Evse::updateTxNotification(TxNotification event) { + if (txNotificationOutput) { + txNotificationOutput(transaction.get(), event); + } +} + +bool TransactionService::Evse::beginAuthorization(IdToken idToken, bool validateIdToken) { + MO_DBG_DEBUG("begin auth: %s", idToken.get()); + + if (transaction && transaction->isAuthorizationActive) { + MO_DBG_WARN("tx process still running. Please call endTransaction(...) before"); + return false; + } + + if (!transaction) { + beginTransaction(); + if (!transaction) { + MO_DBG_ERR("could not allocate Tx"); + return false; + } + if (evseId > 0) { + transaction->notifyEvseId = true; + } + } + + transaction->isAuthorizationActive = true; + transaction->idToken = idToken; + transaction->beginTimestamp = context.getModel().getClock().now(); + + if (validateIdToken) { + auto authorize = makeRequest(new Authorize(context.getModel(), idToken)); + if (!authorize) { + // OOM + abortTransaction(); + return false; + } + + char txId [sizeof(transaction->transactionId)]; //capture txId to check if transaction reference is still the same + snprintf(txId, sizeof(txId), "%s", transaction->transactionId); + + authorize->setOnReceiveConfListener([this, txId] (JsonObject response) { + auto tx = getTransaction(); + if (!tx || strcmp(tx->transactionId, txId)) { + MO_DBG_INFO("dangling Authorize -- discard"); + return; + } + + if (strcmp(response["idTokenInfo"]["status"] | "_Undefined", "Accepted")) { + MO_DBG_DEBUG("Authorize rejected (%s), abort tx process", tx->idToken.get()); + tx->isDeauthorized = true; + txStore.commit(tx); + + updateTxNotification(TxNotification_AuthorizationRejected); + return; + } + + MO_DBG_DEBUG("Authorized tx with validation (%s)", tx->idToken.get()); + tx->isAuthorized = true; + tx->notifyIdToken = true; + txStore.commit(tx); + + updateTxNotification(TxNotification_Authorized); + }); + authorize->setOnAbortListener([this, txId] () { + auto tx = getTransaction(); + if (!tx || strcmp(tx->transactionId, txId)) { + MO_DBG_INFO("dangling Authorize -- discard"); + return; + } + + MO_DBG_DEBUG("Authorize timeout (%s)", tx->idToken.get()); + tx->isDeauthorized = true; + txStore.commit(tx); + + updateTxNotification(TxNotification_AuthorizationTimeout); + }); + authorize->setTimeout(20 * 1000); + context.initiateRequest(std::move(authorize)); + } else { + MO_DBG_DEBUG("Authorized tx directly (%s)", transaction->idToken.get()); + transaction->isAuthorized = true; + transaction->notifyIdToken = true; + txStore.commit(transaction.get()); + + updateTxNotification(TxNotification_Authorized); + } + + return true; +} +bool TransactionService::Evse::endAuthorization(IdToken idToken, bool validateIdToken) { + + if (!transaction || !transaction->isAuthorizationActive) { + //transaction already ended / not active anymore + return false; + } + + MO_DBG_DEBUG("End session started by idTag %s", + transaction->idToken.get()); + + if (transaction->idToken.equals(idToken)) { + // use same idToken like tx start + transaction->isAuthorizationActive = false; + transaction->notifyIdToken = true; + txStore.commit(transaction.get()); + + updateTxNotification(TxNotification_Authorized); + } else if (!validateIdToken) { + transaction->stopIdToken = std::unique_ptr(new IdToken(idToken, getMemoryTag())); + transaction->isAuthorizationActive = false; + transaction->notifyStopIdToken = true; + txStore.commit(transaction.get()); + + updateTxNotification(TxNotification_Authorized); + } else { + // use a different idToken for stopping the tx + + auto authorize = makeRequest(new Authorize(context.getModel(), idToken)); + if (!authorize) { + // OOM + abortTransaction(); + return false; + } + + char txId [sizeof(transaction->transactionId)]; //capture txId to check if transaction reference is still the same + snprintf(txId, sizeof(txId), "%s", transaction->transactionId); + + authorize->setOnReceiveConfListener([this, txId, idToken] (JsonObject response) { + auto tx = getTransaction(); + if (!tx || strcmp(tx->transactionId, txId)) { + MO_DBG_INFO("dangling Authorize -- discard"); + return; + } + + if (strcmp(response["idTokenInfo"]["status"] | "_Undefined", "Accepted")) { + MO_DBG_DEBUG("Authorize rejected (%s), don't stop tx", idToken.get()); + + updateTxNotification(TxNotification_AuthorizationRejected); + return; + } + + MO_DBG_DEBUG("Authorized transaction stop (%s)", idToken.get()); + + tx->stopIdToken = std::unique_ptr(new IdToken(idToken, getMemoryTag())); + if (!tx->stopIdToken) { + // OOM + if (tx->active) { + abortTransaction(); + } + return; + } + + tx->isAuthorizationActive = false; + tx->notifyStopIdToken = true; + txStore.commit(tx); + + updateTxNotification(TxNotification_Authorized); + }); + authorize->setOnTimeoutListener([this, txId] () { + auto tx = getTransaction(); + if (!tx || strcmp(tx->transactionId, txId)) { + MO_DBG_INFO("dangling Authorize -- discard"); + return; + } + + updateTxNotification(TxNotification_AuthorizationTimeout); + }); + authorize->setTimeout(20 * 1000); + context.initiateRequest(std::move(authorize)); + } + + return true; +} +bool TransactionService::Evse::abortTransaction(Ocpp201::Transaction::StoppedReason stoppedReason, TransactionEventTriggerReason stopTrigger) { + return endTransaction(stoppedReason, stopTrigger); +} +MicroOcpp::Ocpp201::Transaction *TransactionService::Evse::getTransaction() { + return transaction.get(); +} + +bool TransactionService::Evse::ocppPermitsCharge() { + return transaction && + transaction->active && + transaction->isAuthorizationActive && + transaction->isAuthorized && + !transaction->isDeauthorized; +} + +unsigned int TransactionService::Evse::getFrontRequestOpNr() { + + if (txEventFront) { + return txEventFront->opNr; + } + + /* + * Advance front transaction? + */ + + unsigned int txSize = (txNrEnd + MAX_TX_CNT - txNrFront) % MAX_TX_CNT; + + if (txFront && txSize == 0) { + //catch edge case where txBack has been rolled back and txFront was equal to txBack + MO_DBG_DEBUG("collect front transaction %u-%u after tx rollback", evseId, txFront->txNr); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + txEventFront = nullptr; + txFrontCache = nullptr; + txFront = nullptr; + } + + for (unsigned int i = 0; i < txSize; i++) { + + if (!txFront) { + if (transaction && transaction->txNr == txNrFront) { + txFront = transaction.get(); + } else { + txFrontCache = txStore.loadTransaction(txNrFront); + txFront = txFrontCache.get(); + } + + if (txFront) { + MO_DBG_DEBUG("load front transaction %u-%u", evseId, txFront->txNr); + (void)0; + } + } + + if (!txFront || (txFront && ((!txFront->active && !txFront->started) || (txFront->stopped && txFront->seqNos.empty()) || txFront->silent))) { + //advance front + MO_DBG_DEBUG("collect front transaction %u-%u", evseId, txNrFront); + txEventFront = nullptr; + txFrontCache = nullptr; + txFront = nullptr; + txNrFront = (txNrFront + 1) % MAX_TX_CNT; + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + } else { + //front is accurate. Done here + break; + } + } + + if (txFront && !txFront->seqNos.empty()) { + MO_DBG_DEBUG("load front txEvent %u-%u-%u from flash", evseId, txFront->txNr, txFront->seqNos.front()); + txEventFront = txStore.loadTransactionEvent(*txFront, txFront->seqNos.front()); + } + + if (txEventFront) { + return txEventFront->opNr; + } + + return NoOperation; +} + +std::unique_ptr TransactionService::Evse::fetchFrontRequest() { + + if (!txEventFront) { + return nullptr; + } + + if (txFront && txFront->silent) { + return nullptr; + } + + if (txEventFront->seqNo == 0 && + txEventFront->timestamp < MIN_TIME && + txEventFront->bootNr != context.getModel().getBootNr()) { + //time not set, cannot be restored anymore -> invalid tx + MO_DBG_ERR("cannot recover tx from previous power cycle"); + + txFront->silent = true; + txFront->active = false; + txStore.commit(txFront); + + //clean txEvents early + auto seqNos = txFront->seqNos; + for (size_t i = 0; i < seqNos.size(); i++) { + txStore.remove(*txFront, seqNos[i]); + } + //last remove should keep tx201 file with only tx record and without txEvent + + //next getFrontRequestOpNr() call will collect txFront + return nullptr; + } + + if ((int)txEventFront->attemptNr >= txService.messageAttemptsTransactionEventInt->getInt()) { + MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard txEvent"); + + txStore.remove(*txFront, txEventFront->seqNo); + txEventFront = nullptr; + return nullptr; + } + + Timestamp nextAttempt = txEventFront->attemptTime + + txEventFront->attemptNr * std::max(0, txService.messageAttemptIntervalTransactionEventInt->getInt()); + + if (nextAttempt > context.getModel().getClock().now()) { + return nullptr; + } + + if (txEventFrontIsRequested) { + //ensure that only one TransactionEvent request is being executed at the same time + return nullptr; + } + + txEventFront->attemptNr++; + txEventFront->attemptTime = context.getModel().getClock().now(); + txStore.commit(txEventFront.get()); + + auto txEventRequest = makeRequest(new TransactionEvent(context.getModel(), txEventFront.get())); + txEventRequest->setOnReceiveConfListener([this] (JsonObject) { + MO_DBG_DEBUG("completed front txEvent"); + txStore.remove(*txFront, txEventFront->seqNo); + txEventFront = nullptr; + txEventFrontIsRequested = false; + }); + txEventRequest->setOnAbortListener([this] () { + MO_DBG_DEBUG("unsuccessful front txEvent"); + txEventFrontIsRequested = false; + }); + txEventRequest->setTimeout(std::min(20, std::max(5, txService.messageAttemptIntervalTransactionEventInt->getInt())) * 1000); + + txEventFrontIsRequested = true; + + return txEventRequest; +} + +bool TransactionService::isTxStartPoint(TxStartStopPoint check) { + for (auto& v : txStartPointParsed) { + if (v == check) { + return true; + } + } + return false; +} +bool TransactionService::isTxStopPoint(TxStartStopPoint check) { + for (auto& v : txStopPointParsed) { + if (v == check) { + return true; + } + } + return false; +} + +bool TransactionService::parseTxStartStopPoint(const char *csl, Vector& dst) { + dst.clear(); + + while (*csl == ',') { + csl++; + } + + while (*csl) { + if (!strncmp(csl, "ParkingBayOccupancy", sizeof("ParkingBayOccupancy") - 1) + && (csl[sizeof("ParkingBayOccupancy") - 1] == '\0' || csl[sizeof("ParkingBayOccupancy") - 1] == ',')) { + dst.push_back(TxStartStopPoint::ParkingBayOccupancy); + csl += sizeof("ParkingBayOccupancy") - 1; + } else if (!strncmp(csl, "EVConnected", sizeof("EVConnected") - 1) + && (csl[sizeof("EVConnected") - 1] == '\0' || csl[sizeof("EVConnected") - 1] == ',')) { + dst.push_back(TxStartStopPoint::EVConnected); + csl += sizeof("EVConnected") - 1; + } else if (!strncmp(csl, "Authorized", sizeof("Authorized") - 1) + && (csl[sizeof("Authorized") - 1] == '\0' || csl[sizeof("Authorized") - 1] == ',')) { + dst.push_back(TxStartStopPoint::Authorized); + csl += sizeof("Authorized") - 1; + } else if (!strncmp(csl, "DataSigned", sizeof("DataSigned") - 1) + && (csl[sizeof("DataSigned") - 1] == '\0' || csl[sizeof("DataSigned") - 1] == ',')) { + dst.push_back(TxStartStopPoint::DataSigned); + csl += sizeof("DataSigned") - 1; + } else if (!strncmp(csl, "PowerPathClosed", sizeof("PowerPathClosed") - 1) + && (csl[sizeof("PowerPathClosed") - 1] == '\0' || csl[sizeof("PowerPathClosed") - 1] == ',')) { + dst.push_back(TxStartStopPoint::PowerPathClosed); + csl += sizeof("PowerPathClosed") - 1; + } else if (!strncmp(csl, "EnergyTransfer", sizeof("EnergyTransfer") - 1) + && (csl[sizeof("EnergyTransfer") - 1] == '\0' || csl[sizeof("EnergyTransfer") - 1] == ',')) { + dst.push_back(TxStartStopPoint::EnergyTransfer); + csl += sizeof("EnergyTransfer") - 1; + } else { + MO_DBG_ERR("unkown TxStartStopPoint"); + dst.clear(); + return false; + } + + while (*csl == ',') { + csl++; + } + } + + return true; +} + +namespace MicroOcpp { + +bool validateTxStartStopPoint(const char *value, void *userPtr) { + auto txService = static_cast(userPtr); + + auto validated = makeVector("v201.Transactions.TransactionService"); + return txService->parseTxStartStopPoint(value, validated); +} + +bool validateUnsignedInt(int val, void*) { + return val >= 0; +} + +} //namespace MicroOcpp + +using namespace MicroOcpp; + +TransactionService::TransactionService(Context& context, std::shared_ptr filesystem, unsigned int numEvseIds) : + MemoryManaged("v201.Transactions.TransactionService"), + context(context), + txStore(filesystem, numEvseIds), + txStartPointParsed(makeVector(getMemoryTag())), + txStopPointParsed(makeVector(getMemoryTag())) { + auto varService = context.getModel().getVariableService(); + + txStartPointString = varService->declareVariable("TxCtrlr", "TxStartPoint", "PowerPathClosed"); + txStopPointString = varService->declareVariable("TxCtrlr", "TxStopPoint", "PowerPathClosed"); + stopTxOnInvalidIdBool = varService->declareVariable("TxCtrlr", "StopTxOnInvalidId", true); + stopTxOnEVSideDisconnectBool = varService->declareVariable("TxCtrlr", "StopTxOnEVSideDisconnect", true); + evConnectionTimeOutInt = varService->declareVariable("TxCtrlr", "EVConnectionTimeOut", 30); + sampledDataTxUpdatedInterval = varService->declareVariable("SampledDataCtrlr", "TxUpdatedInterval", 0); + sampledDataTxEndedInterval = varService->declareVariable("SampledDataCtrlr", "TxEndedInterval", 0); + messageAttemptsTransactionEventInt = varService->declareVariable("OCPPCommCtrlr", "MessageAttempts", 3); + messageAttemptIntervalTransactionEventInt = varService->declareVariable("OCPPCommCtrlr", "MessageAttemptInterval", 60); + silentOfflineTransactionsBool = varService->declareVariable("CustomizationCtrlr", "SilentOfflineTransactions", false); + + varService->declareVariable("AuthCtrlr", "AuthorizeRemoteStart", false, Variable::Mutability::ReadOnly, false); + + varService->registerValidator("TxCtrlr", "TxStartPoint", validateTxStartStopPoint, this); + varService->registerValidator("TxCtrlr", "TxStopPoint", validateTxStartStopPoint, this); + varService->registerValidator("SampledDataCtrlr", "TxUpdatedInterval", validateUnsignedInt); + varService->registerValidator("SampledDataCtrlr", "TxEndedInterval", validateUnsignedInt); + + for (unsigned int evseId = 0; evseId < std::min(numEvseIds, (unsigned int)MO_NUM_EVSEID); evseId++) { + if (!txStore.getEvse(evseId)) { + MO_DBG_ERR("initialization error"); + break; + } + evses[evseId] = new Evse(context, *this, *txStore.getEvse(evseId), evseId); + } + + //make sure EVSE 0 will only trigger transactions if TxStartPoint is Authorized + if (evses[0]) { + evses[0]->connectorPluggedInput = [] () {return false;}; + evses[0]->evReadyInput = [] () {return false;}; + evses[0]->evseReadyInput = [] () {return false;}; + } +} + +TransactionService::~TransactionService() { + for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { + delete evses[evseId]; + } +} + +void TransactionService::loop() { + for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { + evses[evseId]->loop(); + } + + if (txStartPointString->getWriteCount() != trackTxStartPoint) { + parseTxStartStopPoint(txStartPointString->getString(), txStartPointParsed); + } + + if (txStopPointString->getWriteCount() != trackTxStopPoint) { + parseTxStartStopPoint(txStopPointString->getString(), txStopPointParsed); + } + + // assign tx on evseId 0 to an EVSE + if (evses[0]->transaction) { + //pending tx on evseId 0 + if (evses[0]->transaction->active) { + for (unsigned int evseId = 1; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { + if (!evses[evseId]->getTransaction() && + (!evses[evseId]->connectorPluggedInput || evses[evseId]->connectorPluggedInput())) { + MO_DBG_INFO("assign tx to evse %u", evseId); + evses[0]->transaction->notifyEvseId = true; + evses[0]->transaction->evseId = evseId; + evses[evseId]->transaction = std::move(evses[0]->transaction); + } + } + } + } +} + +TransactionService::Evse *TransactionService::getEvse(unsigned int evseId) { + if (evseId >= MO_NUM_EVSEID) { + return nullptr; + } + return evses[evseId]; +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Transactions/TransactionService.h b/src/MicroOcpp/Model/Transactions/TransactionService.h new file mode 100644 index 00000000..36a0bc99 --- /dev/null +++ b/src/MicroOcpp/Model/Transactions/TransactionService.h @@ -0,0 +1,144 @@ +// 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/TransactionStore.cpp b/src/MicroOcpp/Model/Transactions/TransactionStore.cpp index dc5ff934..39c096ba 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionStore.cpp +++ b/src/MicroOcpp/Model/Transactions/TransactionStore.cpp @@ -1,41 +1,25 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include -#include #include -#include -#include -#include #include #include -#include - using namespace MicroOcpp; -#define MO_TXSTORE_META_FN MO_FILENAME_PREFIX "txstore.jsn" - ConnectorTransactionStore::ConnectorTransactionStore(TransactionStore& context, unsigned int connectorId, std::shared_ptr filesystem) : + MemoryManaged("v16.Transactions.TransactionStore"), context(context), connectorId(connectorId), - filesystem(filesystem) { + filesystem(filesystem), + transactions{makeVector>(getMemoryTag())} { - snprintf(txBeginKey, sizeof(txBeginKey), MO_TXSTORE_TXBEGIN_KEY "%u", connectorId); - txBeginInt = declareConfiguration(txBeginKey, 0, MO_TXSTORE_META_FN, false, false, false); - - snprintf(txEndKey, sizeof(txEndKey), MO_TXSTORE_TXEND_KEY "%u", connectorId); - txEndInt = declareConfiguration(txEndKey, 0, MO_TXSTORE_META_FN, false, false, false); } ConnectorTransactionStore::~ConnectorTransactionStore() { - if (txBeginInt->getKey() == txBeginKey) { - txBeginInt->setKey(nullptr); - } - if (txEndInt->getKey() == txEndKey) { - txEndInt->setKey(nullptr); - } + } std::shared_ptr ConnectorTransactionStore::getTransaction(unsigned int txNr) { @@ -73,7 +57,7 @@ std::shared_ptr ConnectorTransactionStore::getTransaction(unsigned } char fn [MO_MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.jsn", connectorId, txNr); + 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; @@ -85,14 +69,14 @@ std::shared_ptr ConnectorTransactionStore::getTransaction(unsigned return nullptr; } - auto doc = FilesystemUtils::loadJson(filesystem, fn); + auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); if (!doc) { MO_DBG_ERR("memory corruption"); return nullptr; } - auto transaction = std::make_shared(*this, connectorId, txNr); + auto transaction = std::allocate_shared(makeAllocator(getMemoryTag()), *this, connectorId, txNr); JsonObject txJson = doc->as(); if (!deserializeTransaction(*transaction, txJson)) { MO_DBG_ERR("deserialization error"); @@ -100,38 +84,23 @@ std::shared_ptr ConnectorTransactionStore::getTransaction(unsigned } //before adding new entry, clean cache - transactions.erase(std::remove_if(transactions.begin(), transactions.end(), - [](std::weak_ptr tx) { - return tx.expired(); - }), - transactions.end()); + cached = transactions.begin(); + while (cached != transactions.end()) { + if (cached->expired()) { + //collect outdated cache reference + cached = transactions.erase(cached); + } else { + cached++; + } + } transactions.push_back(transaction); return transaction; } -std::shared_ptr ConnectorTransactionStore::createTransaction(bool silent) { - - if (!txBeginInt || txBeginInt->getInt() < 0 || !txEndInt || txEndInt->getInt() < 0) { - MO_DBG_ERR("memory corruption"); - return nullptr; - } - - //check if maximum number of queued tx already reached - if ((txEndInt->getInt() + MAX_TX_CNT - txBeginInt->getInt()) % MAX_TX_CNT >= MO_TXRECORD_SIZE) { - //limit reached - - if (!silent) { - //normal tx -> abort - return nullptr; - } - //special case: silent tx -> create tx anyway, but should be deleted immediately after charging session - } - - auto transaction = std::make_shared(*this, connectorId, (unsigned int) txEndInt->getInt(), silent); +std::shared_ptr ConnectorTransactionStore::createTransaction(unsigned int txNr, bool silent) { - txEndInt->setInt((txEndInt->getInt() + 1) % MAX_TX_CNT); - configuration_save(); + auto transaction = std::allocate_shared(makeAllocator(getMemoryTag()), *this, connectorId, txNr, silent); if (!commit(transaction.get())) { MO_DBG_ERR("FS error"); @@ -139,27 +108,20 @@ std::shared_ptr ConnectorTransactionStore::createTransaction(bool s } //before adding new entry, clean cache - transactions.erase(std::remove_if(transactions.begin(), transactions.end(), - [](std::weak_ptr tx) { - return tx.expired(); - }), - transactions.end()); + auto cached = transactions.begin(); + while (cached != transactions.end()) { + if (cached->expired()) { + //collect outdated cache reference + cached = transactions.erase(cached); + } else { + cached++; + } + } transactions.push_back(transaction); return transaction; } -std::shared_ptr ConnectorTransactionStore::getLatestTransaction() { - if (!txEndInt || txEndInt->getInt() < 0) { - MO_DBG_ERR("memory corruption"); - return nullptr; - } - - unsigned int latest = ((unsigned int) txEndInt->getInt() + MAX_TX_CNT - 1) % MAX_TX_CNT; - - return getTransaction(latest); -} - bool ConnectorTransactionStore::commit(Transaction *transaction) { if (!filesystem) { @@ -168,13 +130,13 @@ bool ConnectorTransactionStore::commit(Transaction *transaction) { } char fn [MO_MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.jsn", connectorId, transaction->getTxNr()); + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.json", connectorId, transaction->getTxNr()); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return false; } - DynamicJsonDocument txDoc {0}; + auto txDoc = initJsonDoc(getMemoryTag()); if (!serializeTransaction(*transaction, txDoc)) { MO_DBG_ERR("Serialization error"); return false; @@ -197,7 +159,7 @@ bool ConnectorTransactionStore::remove(unsigned int txNr) { } char fn [MO_MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.jsn", connectorId, txNr); + 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; @@ -214,144 +176,935 @@ bool ConnectorTransactionStore::remove(unsigned int txNr) { return filesystem->remove(fn); } -int ConnectorTransactionStore::getTxBegin() { - if (!txBeginInt || txBeginInt->getInt() < 0) { - MO_DBG_ERR("memory corruption"); - return -1; +TransactionStore::TransactionStore(unsigned int nConnectors, std::shared_ptr filesystem) : + MemoryManaged{"v16.Transactions.TransactionStore"}, connectors{makeVector>(getMemoryTag())} { + + for (unsigned int i = 0; i < nConnectors; i++) { + connectors.push_back(std::unique_ptr( + new ConnectorTransactionStore(*this, i, filesystem))); } +} - return txBeginInt->getInt(); +bool TransactionStore::commit(Transaction *transaction) { + if (!transaction) { + MO_DBG_ERR("Invalid arg"); + return false; + } + auto connectorId = transaction->getConnectorId(); + if (connectorId >= connectors.size()) { + MO_DBG_ERR("Invalid tx"); + return false; + } + return connectors[connectorId]->commit(transaction); } -int ConnectorTransactionStore::getTxEnd() { - if (!txEndInt || txEndInt->getInt() < 0) { - MO_DBG_ERR("memory corruption"); - return -1; +std::shared_ptr TransactionStore::getTransaction(unsigned int connectorId, unsigned int txNr) { + if (connectorId >= connectors.size()) { + MO_DBG_ERR("Invalid connectorId"); + return nullptr; + } + return connectors[connectorId]->getTransaction(txNr); +} + +std::shared_ptr TransactionStore::createTransaction(unsigned int connectorId, unsigned int txNr, bool silent) { + if (connectorId >= connectors.size()) { + MO_DBG_ERR("Invalid connectorId"); + return nullptr; } + return connectors[connectorId]->createTransaction(txNr, silent); +} - return txEndInt->getInt(); +bool TransactionStore::remove(unsigned int connectorId, unsigned int txNr) { + if (connectorId >= connectors.size()) { + MO_DBG_ERR("Invalid connectorId"); + return false; + } + return connectors[connectorId]->remove(txNr); } -void ConnectorTransactionStore::setTxBegin(unsigned int txNr) { - if (!txBeginInt || txBeginInt->getInt() < 0) { - MO_DBG_ERR("memory corruption"); - return; +#if MO_ENABLE_V201 + +#include + +namespace MicroOcpp { +namespace Ocpp201 { + +bool TransactionStoreEvse::serializeTransaction(Transaction& tx, JsonObject txJson) { + + if (tx.trackEvConnected) { + txJson["trackEvConnected"] = tx.trackEvConnected; + } + + if (tx.trackAuthorized) { + txJson["trackAuthorized"] = tx.trackAuthorized; + } + + if (tx.trackDataSigned) { + txJson["trackDataSigned"] = tx.trackDataSigned; + } + + if (tx.trackPowerPathClosed) { + txJson["trackPowerPathClosed"] = tx.trackPowerPathClosed; + } + + if (tx.trackEnergyTransfer) { + txJson["trackEnergyTransfer"] = tx.trackEnergyTransfer; + } + + if (tx.active) { + txJson["active"] = true; + } + if (tx.started) { + txJson["started"] = true; + } + if (tx.stopped) { + txJson["stopped"] = true; + } + + if (tx.isAuthorizationActive) { + txJson["isAuthorizationActive"] = true; + } + if (tx.isAuthorized) { + txJson["isAuthorized"] = true; } + if (tx.isDeauthorized) { + txJson["isDeauthorized"] = true; + } + + if (tx.idToken.get()) { + txJson["idToken"]["idToken"] = tx.idToken.get(); + txJson["idToken"]["type"] = tx.idToken.getTypeCstr(); + } + + if (tx.beginTimestamp > MIN_TIME) { + char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; + tx.beginTimestamp.toJsonString(timeStr, JSONDATE_LENGTH + 1); + txJson["beginTimestamp"] = timeStr; + } + + if (tx.remoteStartId >= 0) { + txJson["remoteStartId"] = tx.remoteStartId; + } + + if (tx.evConnectionTimeoutListen) { + txJson["evConnectionTimeoutListen"] = true; + } + + if (serializeTransactionStoppedReason(tx.stoppedReason)) { // optional + txJson["stoppedReason"] = serializeTransactionStoppedReason(tx.stoppedReason); + } + + if (serializeTransactionEventTriggerReason(tx.stopTrigger)) { + txJson["stopTrigger"] = serializeTransactionEventTriggerReason(tx.stopTrigger); + } + + if (tx.stopIdToken) { + JsonObject stopIdToken = txJson.createNestedObject("stopIdToken"); + stopIdToken["idToken"] = tx.stopIdToken->get(); + stopIdToken["type"] = tx.stopIdToken->getTypeCstr(); + } + + //sampledDataTxEnded not supported yet - txBeginInt->setInt(txNr); - configuration_save(); + if (tx.silent) { + txJson["silent"] = true; + } + + txJson["txId"] = (const char*)tx.transactionId; //force zero-copy + + return true; } -void ConnectorTransactionStore::setTxEnd(unsigned int txNr) { - if (!txBeginInt || txBeginInt->getInt() < 0 || !txEndInt || txEndInt->getInt() < 0) { - MO_DBG_ERR("memory corruption"); - return; +bool TransactionStoreEvse::deserializeTransaction(Transaction& tx, JsonObject txJson) { + + if (txJson.containsKey("trackEvConnected") && !txJson["trackEvConnected"].is()) { + return false; + } + tx.trackEvConnected = txJson["trackEvConnected"] | false; + + if (txJson.containsKey("trackAuthorized") && !txJson["trackAuthorized"].is()) { + return false; + } + tx.trackAuthorized = txJson["trackAuthorized"] | false; + + if (txJson.containsKey("trackDataSigned") && !txJson["trackDataSigned"].is()) { + return false; + } + tx.trackDataSigned = txJson["trackDataSigned"] | false; + + if (txJson.containsKey("trackPowerPathClosed") && !txJson["trackPowerPathClosed"].is()) { + return false; } + tx.trackPowerPathClosed = txJson["trackPowerPathClosed"] | false; - txEndInt->setInt(txNr); - configuration_save(); + if (txJson.containsKey("trackEnergyTransfer") && !txJson["trackEnergyTransfer"].is()) { + return false; + } + tx.trackEnergyTransfer = txJson["trackEnergyTransfer"] | false; + + if (txJson.containsKey("active") && !txJson["active"].is()) { + return false; + } + tx.active = txJson["active"] | false; + if (txJson.containsKey("started") && !txJson["started"].is()) { + return false; + } + tx.started = txJson["started"] | false; + + if (txJson.containsKey("stopped") && !txJson["stopped"].is()) { + return false; + } + tx.stopped = txJson["stopped"] | false; + + if (txJson.containsKey("isAuthorizationActive") && !txJson["isAuthorizationActive"].is()) { + return false; + } + tx.isAuthorizationActive = txJson["isAuthorizationActive"] | false; + if (txJson.containsKey("isAuthorized") && !txJson["isAuthorized"].is()) { + return false; + } + tx.isAuthorized = txJson["isAuthorized"] | false; + + if (txJson.containsKey("isDeauthorized") && !txJson["isDeauthorized"].is()) { + return false; + } + tx.isDeauthorized = txJson["isDeauthorized"] | false; + + if (txJson.containsKey("idToken")) { + IdToken idToken; + if (!idToken.parseCstr( + txJson["idToken"]["idToken"] | (const char*)nullptr, + txJson["idToken"]["type"] | (const char*)nullptr)) { + return false; + } + tx.idToken = idToken; + } + + if (txJson.containsKey("beginTimestamp")) { + if (!tx.beginTimestamp.setTime(txJson["beginTimestamp"] | "_Undefined")) { + return false; + } + } + + if (txJson.containsKey("remoteStartId")) { + int remoteStartIdIn = txJson["remoteStartId"] | -1; + if (remoteStartIdIn < 0) { + return false; + } + tx.remoteStartId = remoteStartIdIn; + } + + if (txJson.containsKey("evConnectionTimeoutListen") && !txJson["evConnectionTimeoutListen"].is()) { + return false; + } + tx.evConnectionTimeoutListen = txJson["evConnectionTimeoutListen"] | false; + + Transaction::StoppedReason stoppedReason; + if (!deserializeTransactionStoppedReason(txJson["stoppedReason"] | (const char*)nullptr, stoppedReason)) { + return false; + } + tx.stoppedReason = stoppedReason; + + TransactionEventTriggerReason stopTrigger; + if (!deserializeTransactionEventTriggerReason(txJson["stopTrigger"] | (const char*)nullptr, stopTrigger)) { + return false; + } + tx.stopTrigger = stopTrigger; + + if (txJson.containsKey("stopIdToken")) { + auto stopIdToken = std::unique_ptr(new IdToken()); + if (!stopIdToken) { + MO_DBG_ERR("OOM"); + return false; + } + if (!stopIdToken->parseCstr( + txJson["stopIdToken"]["idToken"] | (const char*)nullptr, + txJson["stopIdToken"]["type"] | (const char*)nullptr)) { + return false; + } + tx.stopIdToken = std::move(stopIdToken); + } + + //sampledDataTxEnded not supported yet + + if (auto txId = txJson["txId"] | (const char*)nullptr) { + auto ret = snprintf(tx.transactionId, sizeof(tx.transactionId), "%s", txId); + if (ret < 0 || (size_t)ret >= sizeof(tx.transactionId)) { + return false; + } + } else { + return false; + } + + if (txJson.containsKey("silent") && !txJson["silent"].is()) { + return false; + } + tx.silent = txJson["silent"] | false; + + return true; } -unsigned int ConnectorTransactionStore::size() { - if (!txBeginInt || txBeginInt->getInt() < 0 || !txEndInt || txEndInt->getInt() < 0) { - MO_DBG_ERR("memory corruption"); - return 0; +bool TransactionStoreEvse::serializeTransactionEvent(TransactionEventData& txEvent, JsonObject txEventJson) { + + if (txEvent.eventType != TransactionEventData::Type::Updated) { + txEventJson["eventType"] = serializeTransactionEventType(txEvent.eventType); + } + + if (txEvent.timestamp > MIN_TIME) { + char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; + txEvent.timestamp.toJsonString(timeStr, JSONDATE_LENGTH + 1); + txEventJson["timestamp"] = timeStr; + } + + txEventJson["bootNr"] = txEvent.bootNr; + + if (serializeTransactionEventTriggerReason(txEvent.triggerReason)) { + txEventJson["triggerReason"] = serializeTransactionEventTriggerReason(txEvent.triggerReason); + } + + if (txEvent.offline) { + txEventJson["offline"] = true; + } + + if (txEvent.numberOfPhasesUsed >= 0) { + txEventJson["numberOfPhasesUsed"] = txEvent.numberOfPhasesUsed; + } + + if (txEvent.cableMaxCurrent >= 0) { + txEventJson["cableMaxCurrent"] = txEvent.cableMaxCurrent; } - return (txEndInt->getInt() + MAX_TX_CNT - txBeginInt->getInt()) % MAX_TX_CNT; + if (txEvent.reservationId >= 0) { + txEventJson["reservationId"] = txEvent.reservationId; + } + + if (txEvent.remoteStartId >= 0) { + txEventJson["remoteStartId"] = txEvent.remoteStartId; + } + + if (serializeTransactionEventChargingState(txEvent.chargingState)) { // optional + txEventJson["chargingState"] = serializeTransactionEventChargingState(txEvent.chargingState); + } + + if (txEvent.idToken) { + JsonObject idToken = txEventJson.createNestedObject("idToken"); + idToken["idToken"] = txEvent.idToken->get(); + idToken["type"] = txEvent.idToken->getTypeCstr(); + } + + if (txEvent.evse.id >= 0) { + JsonObject evse = txEventJson.createNestedObject("evse"); + evse["id"] = txEvent.evse.id; + if (txEvent.evse.connectorId >= 0) { + evse["connectorId"] = txEvent.evse.connectorId; + } + } + + //meterValue not supported yet + + txEventJson["opNr"] = txEvent.opNr; + txEventJson["attemptNr"] = txEvent.attemptNr; + + if (txEvent.attemptTime > MIN_TIME) { + char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; + txEvent.attemptTime.toJsonString(timeStr, JSONDATE_LENGTH + 1); + txEventJson["attemptTime"] = timeStr; + } + + return true; } -TransactionStore::TransactionStore(unsigned int nConnectors, std::shared_ptr filesystem) { +bool TransactionStoreEvse::deserializeTransactionEvent(TransactionEventData& txEvent, JsonObject txEventJson) { + + TransactionEventData::Type eventType; + if (!deserializeTransactionEventType(txEventJson["eventType"] | "Updated", eventType)) { + return false; + } + txEvent.eventType = eventType; + + if (txEventJson.containsKey("timestamp")) { + if (!txEvent.timestamp.setTime(txEventJson["timestamp"] | "_Undefined")) { + return false; + } + } + + int bootNrIn = txEventJson["bootNr"] | -1; + if (bootNrIn >= 0 && bootNrIn <= std::numeric_limits::max()) { + txEvent.bootNr = (uint16_t)bootNrIn; + } else { + return false; + } + + TransactionEventTriggerReason triggerReason; + if (!deserializeTransactionEventTriggerReason(txEventJson["triggerReason"] | "_Undefined", triggerReason)) { + return false; + } + txEvent.triggerReason = triggerReason; - for (unsigned int i = 0; i < nConnectors; i++) { - connectors.push_back(std::unique_ptr( - new ConnectorTransactionStore(*this, i, filesystem))); + if (txEventJson.containsKey("offline") && !txEventJson["offline"].is()) { + return false; } + txEvent.offline = txEventJson["offline"] | false; - configuration_load(MO_TXSTORE_META_FN); + if (txEventJson.containsKey("numberOfPhasesUsed")) { + int numberOfPhasesUsedIn = txEventJson["numberOfPhasesUsed"] | -1; + if (numberOfPhasesUsedIn < 0) { + return false; + } + txEvent.numberOfPhasesUsed = numberOfPhasesUsedIn; + } + + if (txEventJson.containsKey("cableMaxCurrent")) { + int cableMaxCurrentIn = txEventJson["cableMaxCurrent"] | -1; + if (cableMaxCurrentIn < 0) { + return false; + } + txEvent.cableMaxCurrent = cableMaxCurrentIn; + } + + if (txEventJson.containsKey("reservationId")) { + int reservationIdIn = txEventJson["reservationId"] | -1; + if (reservationIdIn < 0) { + return false; + } + txEvent.reservationId = reservationIdIn; + } + + if (txEventJson.containsKey("remoteStartId")) { + int remoteStartIdIn = txEventJson["remoteStartId"] | -1; + if (remoteStartIdIn < 0) { + return false; + } + txEvent.remoteStartId = remoteStartIdIn; + } + + TransactionEventData::ChargingState chargingState; + if (!deserializeTransactionEventChargingState(txEventJson["chargingState"] | (const char*)nullptr, chargingState)) { + return false; + } + txEvent.chargingState = chargingState; + + if (txEventJson.containsKey("idToken")) { + auto idToken = std::unique_ptr(new IdToken()); + if (!idToken) { + MO_DBG_ERR("OOM"); + return false; + } + if (!idToken->parseCstr( + txEventJson["idToken"]["idToken"] | (const char*)nullptr, + txEventJson["idToken"]["type"] | (const char*)nullptr)) { + return false; + } + txEvent.idToken = std::move(idToken); + } + + if (txEventJson.containsKey("evse")) { + int evseId = txEventJson["evse"]["id"] | -1; + if (evseId < 0) { + return false; + } + if (txEventJson["evse"].containsKey("connectorId")) { + int connectorId = txEventJson["evse"]["connectorId"] | -1; + if (connectorId < 0) { + return false; + } + txEvent.evse = EvseId(evseId, connectorId); + } else { + txEvent.evse = EvseId(evseId); + } + } + + //meterValue not supported yet + + int opNrIn = txEventJson["opNr"] | -1; + if (opNrIn >= 0) { + txEvent.opNr = (unsigned int)opNrIn; + } else { + return false; + } + + int attemptNrIn = txEventJson["attemptNr"] | -1; + if (attemptNrIn >= 0) { + txEvent.attemptNr = (unsigned int)attemptNrIn; + } else { + return false; + } + + if (txEventJson.containsKey("attemptTime")) { + if (!txEvent.attemptTime.setTime(txEventJson["attemptTime"] | "_Undefined")) { + return false; + } + } + + return true; } -std::shared_ptr TransactionStore::getLatestTransaction(unsigned int connectorId) { - if (connectorId >= connectors.size()) { - MO_DBG_ERR("Invalid connectorId"); +TransactionStoreEvse::TransactionStoreEvse(TransactionStore& txStore, unsigned int evseId, std::shared_ptr filesystem) : + MemoryManaged("v201.Transactions.TransactionStore"), + txStore(txStore), + evseId(evseId), + filesystem(filesystem) { + +} + +bool TransactionStoreEvse::discoverStoredTx(unsigned int& txNrBeginOut, unsigned int& txNrEndOut) { + + if (!filesystem) { + MO_DBG_DEBUG("no FS adapter"); + return true; + } + + char fnPrefix [MO_MAX_PATH_SIZE]; + snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%u-", evseId); + size_t fnPrefixLen = strlen(fnPrefix); + + unsigned int txNrPivot = std::numeric_limits::max(); + unsigned int txNrBegin = 0, txNrEnd = 0; + + auto ret = filesystem->ftw_root([fnPrefix, fnPrefixLen, &txNrPivot, &txNrBegin, &txNrEnd] (const char *fn) { + if (!strncmp(fn, fnPrefix, fnPrefixLen)) { + unsigned int parsedTxNr = 0; + for (size_t i = fnPrefixLen; fn[i] >= '0' && fn[i] <= '9'; i++) { + parsedTxNr *= 10; + parsedTxNr += fn[i] - '0'; + } + + if (txNrPivot == std::numeric_limits::max()) { + txNrPivot = parsedTxNr; + txNrBegin = parsedTxNr; + txNrEnd = (parsedTxNr + 1) % MAX_TX_CNT; + return 0; + } + + if ((parsedTxNr + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT < MAX_TX_CNT / 2) { + //parsedTxNr is after pivot point + if ((parsedTxNr + 1 + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT > (txNrEnd + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT) { + txNrEnd = (parsedTxNr + 1) % MAX_TX_CNT; + } + } else if ((txNrPivot + MAX_TX_CNT - parsedTxNr) % MAX_TX_CNT < MAX_TX_CNT / 2) { + //parsedTxNr is before pivot point + if ((txNrPivot + MAX_TX_CNT - parsedTxNr) % MAX_TX_CNT > (txNrPivot + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT) { + txNrBegin = parsedTxNr; + } + } + + MO_DBG_DEBUG("found %s%u-*.json - Internal range from %u to %u (exclusive)", fnPrefix, parsedTxNr, txNrBegin, txNrEnd); + } + return 0; + }); + + if (ret == 0) { + txNrBeginOut = txNrBegin; + txNrEndOut = txNrEnd; + return true; + } else { + MO_DBG_ERR("fs error"); + return false; + } +} + +std::unique_ptr TransactionStoreEvse::loadTransaction(unsigned int txNr) { + + if (!filesystem) { + MO_DBG_DEBUG("no FS adapter"); + return nullptr; + } + + char fnPrefix [MO_MAX_PATH_SIZE]; + auto ret= snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%u-%u-", evseId, txNr); + if (ret < 0 || (size_t)ret >= sizeof(fnPrefix)) { + MO_DBG_ERR("fn error"); + return nullptr; + } + size_t fnPrefixLen = strlen(fnPrefix); + + Vector seqNos = makeVector(getMemoryTag()); + + filesystem->ftw_root([fnPrefix, fnPrefixLen, &seqNos] (const char *fn) { + if (!strncmp(fn, fnPrefix, fnPrefixLen)) { + unsigned int parsedSeqNo = 0; + for (size_t i = fnPrefixLen; fn[i] >= '0' && fn[i] <= '9'; i++) { + parsedSeqNo *= 10; + parsedSeqNo += fn[i] - '0'; + } + + seqNos.push_back(parsedSeqNo); + } + return 0; + }); + + if (seqNos.empty()) { + MO_DBG_DEBUG("no tx at tx201-%u-%u", evseId, txNr); return nullptr; } - return connectors[connectorId]->getLatestTransaction(); + + std::sort(seqNos.begin(), seqNos.end()); + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx201" "-%u-%u-%u.json", evseId, txNr, seqNos.back()); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return nullptr; + } + + size_t msize; + if (filesystem->stat(fn, &msize) != 0) { + MO_DBG_ERR("tx201-%u-%u memory corruption", evseId, txNr); + return nullptr; + } + + auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); + + if (!doc) { + MO_DBG_ERR("memory corruption"); + return nullptr; + } + + auto transaction = std::unique_ptr(new Transaction()); + if (!transaction) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + transaction->evseId = evseId; + transaction->txNr = txNr; + transaction->seqNos = std::move(seqNos); + + JsonObject txJson = (*doc)["tx"]; + + if (!deserializeTransaction(*transaction, txJson)) { + MO_DBG_ERR("deserialization error"); + return nullptr; + } + + //determine seqNoEnd and trim seqNos record + if (doc->containsKey("txEvent")) { + //last tx201 file contains txEvent -> txNoEnd is one place after tx201 file and seqNos is accurate + transaction->seqNoEnd = transaction->seqNos.back() + 1; + } else { + //last tx201 file contains only tx status information, but no txEvent -> remove from seqNos record and set seqNoEnd to this + transaction->seqNoEnd = transaction->seqNos.back(); + transaction->seqNos.pop_back(); + } + + MO_DBG_DEBUG("loaded tx %u-%u, seqNos.size()=%zu", evseId, txNr, transaction->seqNos.size()); + + return transaction; } -bool TransactionStore::commit(Transaction *transaction) { +std::unique_ptr TransactionStoreEvse::createTransaction(unsigned int txNr, const char *txId) { + + //clean data which could still be here from a rolled-back transaction + if (!remove(txNr)) { + MO_DBG_ERR("txNr not clean"); + return nullptr; + } + + auto transaction = std::unique_ptr(new Transaction()); if (!transaction) { - MO_DBG_ERR("Invalid arg"); - return false; + MO_DBG_ERR("OOM"); + return nullptr; } - auto connectorId = transaction->getConnectorId(); - if (connectorId >= connectors.size()) { - MO_DBG_ERR("Invalid tx"); - return false; + + transaction->evseId = evseId; + transaction->txNr = txNr; + + auto ret = snprintf(transaction->transactionId, sizeof(transaction->transactionId), "%s", txId); + if (ret < 0 || (size_t)ret >= sizeof(transaction->transactionId)) { + MO_DBG_ERR("invalid arg"); + return nullptr; } - return connectors[connectorId]->commit(transaction); + + if (!commit(transaction.get())) { + MO_DBG_ERR("FS error"); + return nullptr; + } + + return transaction; } -std::shared_ptr TransactionStore::getTransaction(unsigned int connectorId, unsigned int txNr) { - if (connectorId >= connectors.size()) { - MO_DBG_ERR("Invalid connectorId"); +std::unique_ptr TransactionStoreEvse::createTransactionEvent(Transaction& tx) { + + auto txEvent = std::unique_ptr(new TransactionEventData(&tx, tx.seqNoEnd)); + if (!txEvent) { + MO_DBG_ERR("OOM"); return nullptr; } - return connectors[connectorId]->getTransaction(txNr); + + //success + return txEvent; } -std::shared_ptr TransactionStore::createTransaction(unsigned int connectorId, bool silent) { - if (connectorId >= connectors.size()) { - MO_DBG_ERR("Invalid connectorId"); +std::unique_ptr TransactionStoreEvse::loadTransactionEvent(Transaction& tx, unsigned int seqNo) { + + if (!filesystem) { + MO_DBG_DEBUG("no FS adapter"); + return nullptr; + } + + bool found = false; + for (size_t i = 0; i < tx.seqNos.size(); i++) { + if (tx.seqNos[i] == seqNo) { + found = true; + } + } + if (!found) { + MO_DBG_DEBUG("%u-%u-%u does not exist", evseId, tx.txNr, seqNo); + return nullptr; + } + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx201" "-%u-%u-%u.json", evseId, tx.txNr, seqNo); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return nullptr; + } + + size_t msize; + if (filesystem->stat(fn, &msize) != 0) { + MO_DBG_ERR("seqNos out of sync: could not find %u-%u-%u", evseId, tx.txNr, seqNo); + return nullptr; + } + + auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); + + if (!doc) { + MO_DBG_ERR("memory corruption"); + return nullptr; + } + + if (!doc->containsKey("txEvent")) { + MO_DBG_DEBUG("%u-%u-%u does not contain txEvent", evseId, tx.txNr, seqNo); + return nullptr; + } + + auto txEvent = std::unique_ptr(new TransactionEventData(&tx, seqNo)); + if (!txEvent) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + if (!deserializeTransactionEvent(*txEvent, (*doc)["txEvent"])) { + MO_DBG_ERR("deserialization error"); return nullptr; } - return connectors[connectorId]->createTransaction(silent); + + return txEvent; } -bool TransactionStore::remove(unsigned int connectorId, unsigned int txNr) { - if (connectorId >= connectors.size()) { - MO_DBG_ERR("Invalid connectorId"); +bool TransactionStoreEvse::commit(Transaction& tx, TransactionEventData *txEvent) { + + if (!filesystem) { + MO_DBG_DEBUG("no FS: nothing to commit"); + return true; + } + + unsigned int seqNo = 0; + + if (txEvent) { + seqNo = txEvent->seqNo; + } else { + //update tx state in new or reused tx201 file + seqNo = tx.seqNoEnd; + } + + size_t seqNosNewSize = tx.seqNos.size() + 1; + for (size_t i = 0; i < tx.seqNos.size(); i++) { + if (tx.seqNos[i] == seqNo) { + seqNosNewSize -= 1; + break; + } + } + + // Check if to delete intermediate offline txEvent + if (seqNosNewSize > MO_TXEVENTRECORD_SIZE_V201) { + auto deltaMin = std::numeric_limits::max(); + size_t indexMin = tx.seqNos.size(); + for (size_t i = 2; i + 1 <= tx.seqNos.size(); i++) { //always keep first and final txEvent + size_t t0 = tx.seqNos.size() - i - 1; + size_t t1 = tx.seqNos.size() - i; + size_t t2 = tx.seqNos.size() - i + 1; + + auto delta = tx.seqNos[t2] - tx.seqNos[t0]; + + if (delta < deltaMin) { + deltaMin = delta; + indexMin = t1; + } + } + + if (indexMin < tx.seqNos.size()) { + MO_DBG_DEBUG("delete intermediate txEvent %u-%u-%u - delta=%u", evseId, tx.txNr, tx.seqNos[indexMin], deltaMin); + remove(tx, tx.seqNos[indexMin]); //remove can call commit() again. Ensure that remove is not executed for last element + } else { + MO_DBG_ERR("internal error"); + return false; + } + } + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx201" "-%u-%u-%u.json", evseId, tx.txNr, seqNo); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); return false; } - return connectors[connectorId]->remove(txNr); + + auto txDoc = initJsonDoc("v201.Transactions.TransactionStoreEvse", 2048); + + if (!serializeTransaction(tx, txDoc.createNestedObject("tx"))) { + MO_DBG_ERR("Serialization error"); + return false; + } + + if (txEvent && !serializeTransactionEvent(*txEvent, txDoc.createNestedObject("txEvent"))) { + MO_DBG_ERR("Serialization error"); + return false; + } + + if (!FilesystemUtils::storeJson(filesystem, fn, txDoc)) { + MO_DBG_ERR("FS error"); + return false; + } + + if (txEvent && seqNo == tx.seqNoEnd) { + tx.seqNos.push_back(seqNo); + tx.seqNoEnd++; + } + + MO_DBG_DEBUG("comitted tx %u-%u-%u", evseId, tx.txNr, seqNo); + + //success + return true; } -int TransactionStore::getTxBegin(unsigned int connectorId) { - if (connectorId >= connectors.size()) { - MO_DBG_ERR("Invalid connectorId"); - return -1; +bool TransactionStoreEvse::commit(Transaction *transaction) { + return commit(*transaction, nullptr); +} + +bool TransactionStoreEvse::commit(TransactionEventData *txEvent) { + return commit(*txEvent->transaction, txEvent); +} + +bool TransactionStoreEvse::remove(unsigned int txNr) { + + if (!filesystem) { + MO_DBG_DEBUG("no FS: nothing to remove"); + return true; + } + + char fnPrefix [MO_MAX_PATH_SIZE]; + auto ret= snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%u-%u-", evseId, txNr); + if (ret < 0 || (size_t)ret >= sizeof(fnPrefix)) { + MO_DBG_ERR("fn error"); + return false; } - return connectors[connectorId]->getTxBegin(); + size_t fnPrefixLen = strlen(fnPrefix); + + auto success = FilesystemUtils::remove_if(filesystem, [fnPrefix, fnPrefixLen] (const char *fn) { + return !strncmp(fn, fnPrefix, fnPrefixLen); + }); + + return success; } -int TransactionStore::getTxEnd(unsigned int connectorId) { - if (connectorId >= connectors.size()) { - MO_DBG_ERR("Invalid connectorId"); - return -1; +bool TransactionStoreEvse::remove(Transaction& tx, unsigned int seqNo) { + + if (tx.seqNos.empty()) { + //nothing to do + return true; } - return connectors[connectorId]->getTxEnd(); + + if (tx.seqNos.back() == seqNo) { + //special case: deletion of last tx201 file could also delete information about tx. Make sure all tx-related + //information is commited into tx201 file at seqNoEnd, then delete file at seqNo + + char fn [MO_MAX_PATH_SIZE]; + auto ret = snprintf(fn, sizeof(fn), "%stx201-%u-%u-%u.json", MO_FILENAME_PREFIX, evseId, tx.txNr, tx.seqNoEnd); + if (ret < 0 || (size_t)ret >= sizeof(fn)) { + MO_DBG_ERR("fn error"); + return false; + } + + auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); + + if (!doc || !doc->containsKey("tx")) { + //no valid tx201 file at seqNoEnd. Commit tx into file seqNoEnd, then remove file at seqNo + + if (!commit(tx, nullptr)) { + MO_DBG_ERR("fs error"); + return false; + } + } + + //seqNoEnd contains all tx data which should be persisted. Continue + } + + bool found = false; + for (size_t i = 0; i < tx.seqNos.size(); i++) { + if (tx.seqNos[i] == seqNo) { + found = true; + } + } + if (!found) { + MO_DBG_DEBUG("%u-%u-%u does not exist", evseId, tx.txNr, seqNo); + return true; + } + + bool success = true; + + if (filesystem) { + char fn [MO_MAX_PATH_SIZE]; + auto ret = snprintf(fn, sizeof(fn), "%stx201-%u-%u-%u.json", MO_FILENAME_PREFIX, evseId, tx.txNr, seqNo); + if (ret < 0 || (size_t)ret >= sizeof(fn)) { + MO_DBG_ERR("fn error"); + return false; + } + + size_t msize; + if (filesystem->stat(fn, &msize) == 0) { + success &= filesystem->remove(fn); + } else { + MO_DBG_ERR("internal error: seqNos out of sync"); + (void)0; + } + } + + if (success) { + auto it = tx.seqNos.begin(); + while (it != tx.seqNos.end()) { + if (*it == seqNo) { + it = tx.seqNos.erase(it); + } else { + it++; + } + } + } + + return success; } -void TransactionStore::setTxBegin(unsigned int connectorId, unsigned int txNr) { - if (connectorId >= connectors.size()) { - MO_DBG_ERR("Invalid connectorId"); - return; +TransactionStore::TransactionStore(std::shared_ptr filesystem, size_t numEvses) : + MemoryManaged{"v201.Transactions.TransactionStore"} { + + for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID && (size_t)evseId < numEvses; evseId++) { + evses[evseId] = new TransactionStoreEvse(*this, evseId, filesystem); } - return connectors[connectorId]->setTxBegin(txNr); } -void TransactionStore::setTxEnd(unsigned int connectorId, unsigned int txNr) { - if (connectorId >= connectors.size()) { - MO_DBG_ERR("Invalid connectorId"); - return; +TransactionStore::~TransactionStore() { + for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { + delete evses[evseId]; } - return connectors[connectorId]->setTxEnd(txNr); } -unsigned int TransactionStore::size(unsigned int connectorId) { - if (connectorId >= connectors.size()) { - MO_DBG_ERR("Invalid connectorId"); - return 0; +TransactionStoreEvse *TransactionStore::getEvse(unsigned int evseId) { + if (evseId >= MO_NUM_EVSEID) { + return nullptr; } - return connectors[connectorId]->size(); + 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 c643bd6b..c5d3421e 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionStore.h +++ b/src/MicroOcpp/Model/Transactions/TransactionStore.h @@ -1,41 +1,27 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef TRANSACTIONSTORE_H -#define TRANSACTIONSTORE_H +#ifndef MO_TRANSACTIONSTORE_H +#define MO_TRANSACTIONSTORE_H +#include #include -#include #include -#include - -#define MAX_TX_CNT 100000U - -#ifndef MO_TXRECORD_SIZE -#define MO_TXRECORD_SIZE 4 //no. of tx to hold on flash storage -#endif - -#define MO_TXSTORE_TXBEGIN_KEY "txBegin_" -#define MO_TXSTORE_TXEND_KEY "txEnd_" +#include namespace MicroOcpp { class TransactionStore; -class ConnectorTransactionStore { +class ConnectorTransactionStore : public MemoryManaged { private: TransactionStore& context; const unsigned int connectorId; std::shared_ptr filesystem; - std::shared_ptr txBeginInt; //if txNr < txBegin, tx has been safely deleted - char txBeginKey [sizeof(MO_TXSTORE_TXBEGIN_KEY "xxx") + 1]; //"xxx": placeholder for connectorId - - std::shared_ptr txEndInt; - char txEndKey [sizeof(MO_TXSTORE_TXEND_KEY "xxx") + 1]; - std::deque> transactions; + Vector> transactions; public: ConnectorTransactionStore(TransactionStore& context, unsigned int connectorId, std::shared_ptr filesystem); @@ -44,45 +30,88 @@ class ConnectorTransactionStore { ConnectorTransactionStore& operator=(const ConnectorTransactionStore&) = delete; ~ConnectorTransactionStore(); - - std::shared_ptr getLatestTransaction(); + bool commit(Transaction *transaction); std::shared_ptr getTransaction(unsigned int txNr); - std::shared_ptr createTransaction(bool silent = false); + std::shared_ptr createTransaction(unsigned int txNr, bool silent = false); bool remove(unsigned int txNr); - - int getTxBegin(); - int getTxEnd(); - void setTxBegin(unsigned int txNr); - void setTxEnd(unsigned int txNr); - - unsigned int size(); }; -class TransactionStore { +class TransactionStore : public MemoryManaged { private: - std::vector> connectors; + Vector> connectors; public: TransactionStore(unsigned int nConnectors, std::shared_ptr filesystem); - std::shared_ptr getLatestTransaction(unsigned int connectorId); bool commit(Transaction *transaction); std::shared_ptr getTransaction(unsigned int connectorId, unsigned int txNr); - std::shared_ptr createTransaction(unsigned int connectorId, bool silent = false); + std::shared_ptr createTransaction(unsigned int connectorId, unsigned int txNr, bool silent = false); bool remove(unsigned int connectorId, unsigned int txNr); +}; + +} + +#if MO_ENABLE_V201 + +#ifndef MO_TXEVENTRECORD_SIZE_V201 +#define MO_TXEVENTRECORD_SIZE_V201 10 //maximum number of of txEvents per tx to hold on flash storage +#endif + +namespace MicroOcpp { +namespace Ocpp201 { + +class TransactionStore; + +class TransactionStoreEvse : public MemoryManaged { +private: + TransactionStore& txStore; + const unsigned int evseId; + + std::shared_ptr filesystem; + + bool serializeTransaction(Transaction& tx, JsonObject out); + bool serializeTransactionEvent(TransactionEventData& txEvent, JsonObject out); + bool deserializeTransaction(Transaction& tx, JsonObject in); + bool deserializeTransactionEvent(TransactionEventData& txEvent, JsonObject in); + + bool commit(Transaction& transaction, TransactionEventData *transactionEvent); + +public: + TransactionStoreEvse(TransactionStore& txStore, unsigned int evseId, std::shared_ptr filesystem); + + bool discoverStoredTx(unsigned int& txNrBeginOut, unsigned int& txNrEndOut); + + bool commit(Transaction *transaction); + bool commit(TransactionEventData *transactionEvent); - int getTxBegin(unsigned int connectorId); - int getTxEnd(unsigned int connectorId); - void setTxBegin(unsigned int connectorId, unsigned int txNr); - void setTxEnd(unsigned int connectorId, unsigned int txNr); + std::unique_ptr loadTransaction(unsigned int txNr); + std::unique_ptr createTransaction(unsigned int txNr, const char *txId); - unsigned int size(unsigned int connectorId); + std::unique_ptr createTransactionEvent(Transaction& tx); + std::unique_ptr loadTransactionEvent(Transaction& tx, unsigned int seqNo); + + bool remove(unsigned int txNr); + bool remove(Transaction& tx, unsigned int seqNo); }; -} +class TransactionStore : public MemoryManaged { +private: + TransactionStoreEvse *evses [MO_NUM_EVSEID] = {nullptr}; +public: + TransactionStore(std::shared_ptr filesystem, size_t numEvses); + + ~TransactionStore(); + + TransactionStoreEvse *getEvse(unsigned int evseId); +}; + +} //namespace Ocpp201 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Model/Variables/Variable.cpp b/src/MicroOcpp/Model/Variables/Variable.cpp new file mode 100644 index 00000000..ee3f0229 --- /dev/null +++ b/src/MicroOcpp/Model/Variables/Variable.cpp @@ -0,0 +1,398 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +/* + * Implementation of the UCs B05 - B06 + */ + +#include + +#if MO_ENABLE_V201 + +#include + +#include + +#include + +using namespace MicroOcpp; + +ComponentId::ComponentId(const char *name) : name(name) { } +ComponentId::ComponentId(const char *name, EvseId evse) : name(name), evse(evse) { } + +bool ComponentId::equals(const ComponentId& other) const { + return !strcmp(name, other.name) && + ((evse.id < 0 && other.evse.id < 0) || (evse.id == other.evse.id)) && // evseId undefined or equal + ((evse.connectorId < 0 && other.evse.connectorId < 0) || (evse.connectorId == other.evse.connectorId)); // connectorId undefined or equal +} + +bool Variable::AttributeTypeSet::has(AttributeType type) { + switch(type) { + case AttributeType::Actual: + return flag & (1 << 0); + case AttributeType::Target: + return flag & (1 << 1); + case AttributeType::MinSet: + return flag & (1 << 2); + case AttributeType::MaxSet: + return flag & (1 << 3); + } + MO_DBG_ERR("internal error"); + return false; +} + +Variable::AttributeTypeSet& Variable::AttributeTypeSet::set(AttributeType type) { + switch(type) { + case AttributeType::Actual: + flag |= (1 << 0); + break; + case AttributeType::Target: + flag |= (1 << 1); + break; + case AttributeType::MinSet: + flag |= (1 << 2); + break; + case AttributeType::MaxSet: + flag |= (1 << 3); + break; + default: + MO_DBG_ERR("internal error"); + break; + } + return *this; +} + +size_t Variable::AttributeTypeSet::count() { + return (flag & (1 << 0) ? 1 : 0) + + (flag & (1 << 1) ? 1 : 0) + + (flag & (1 << 2) ? 1 : 0) + + (flag & (1 << 3) ? 1 : 0); +} + +Variable::AttributeTypeSet::AttributeTypeSet(AttributeType attrType) { + set(attrType); +} + +Variable::Variable(AttributeTypeSet attributes) : attributes(attributes) { } + +Variable::~Variable() { + +} + +void Variable::setName(const char *name) { + this->variableName = name; + updateMemoryTag("v201.Variables.Variable.", name); +} +const char *Variable::getName() const { + return variableName; +} + +void Variable::setComponentId(const ComponentId& componentId) { + this->component = componentId; +} +const ComponentId& Variable::getComponentId() const { + return component; +} + +void Variable::setInt(int val, AttributeType) { + MO_DBG_ERR("type err"); +} +void Variable::setBool(bool val, AttributeType) { + MO_DBG_ERR("type err"); +} +bool Variable::setString(const char *val, AttributeType) { + MO_DBG_ERR("type err"); + return false; +} + +int Variable::getInt(AttributeType) { + MO_DBG_ERR("type err"); + return 0; +} +bool Variable::getBool(AttributeType) { + MO_DBG_ERR("type err"); + return false; +} +const char *Variable::getString(AttributeType) { + MO_DBG_ERR("type err"); + return nullptr; +} + +bool Variable::hasAttribute(AttributeType attrType) { + return attributes.has(attrType); +} + +void Variable::setVariableDataType(VariableCharacteristics::DataType dataType) { + this->dataType = dataType; +} +VariableCharacteristics::DataType Variable::getVariableDataType() { + return dataType; +} + +bool Variable::getSupportsMonitoring() { + return supportsMonitoring; +} +void Variable::setSupportsMonitoring() { + supportsMonitoring = true; +} + +bool Variable::isRebootRequired() { + return rebootRequired; +} +void Variable::setRebootRequired() { + rebootRequired = true; +} + +void Variable::setMutability(Mutability m) { + this->mutability = m; +} +Variable::Mutability Variable::getMutability() { + return mutability; +} + +void Variable::setPersistent() { + persistent = true; +} +bool Variable::isPersistent() { + return persistent; +} + +void Variable::setConstant() { + constant = true; +} +bool Variable::isConstant() { + return constant; +} + +template +struct VariableSingleData { + T value = 0; + + T& get(Variable::AttributeType attribute) { + return value; + } +}; + +template +struct VariableFullData { + T actual = 0; + T target = 0; + T minSet = 0; + T maxSet = 0; + + T& get(Variable::AttributeType attribute) { + switch(attribute) { + case Variable::AttributeType::Actual: + return actual; + case Variable::AttributeType::Target: + return target; + case Variable::AttributeType::MinSet: + return minSet; + case Variable::AttributeType::MaxSet: + return maxSet; + } + MO_DBG_ERR("internal error"); + return actual; + } +}; + +template class VariableData> +class VariableInt : public Variable { +private: + VariableData value; + uint16_t writeCount = 0; + + #if MO_VARIABLE_TYPECHECK + AttributeTypeSet attributes; + #endif +public: + VariableInt(AttributeTypeSet attributes) : + Variable(attributes) + #if MO_VARIABLE_TYPECHECK + , attributes(attributes) + #endif + { + + } + + void setInt(int val, AttributeType attrType) override { + #if MO_VARIABLE_TYPECHECK + if (!attributes.has(attrType)) { + MO_DBG_ERR("type err"); + return; + } + #endif + value.get(attrType) = val; + writeCount++; + } + + int getInt(AttributeType attrType) override { + #if MO_VARIABLE_TYPECHECK + if (!attributes.has(attrType)) { + MO_DBG_ERR("type err"); + return 0; + } + #endif + return value.get(attrType); + } + + InternalDataType getInternalDataType() override { + return InternalDataType::Int; + } + + uint16_t getWriteCount() override { + return writeCount; + } +}; + +template class VariableData> +class VariableBool : public Variable { +private: + VariableData value; + uint16_t writeCount = 0; + + #if MO_VARIABLE_TYPECHECK + AttributeTypeSet attributes; + #endif +public: + VariableBool(AttributeTypeSet attributes) : + Variable(attributes) + #if MO_VARIABLE_TYPECHECK + , attributes(attributes) + #endif + { + + } + + void setBool(bool val, AttributeType attrType) override { + #if MO_VARIABLE_TYPECHECK + if (!attributes.has(attrType)) { + MO_DBG_ERR("type err"); + return; + } + #endif + value.get(attrType) = val; + writeCount++; + } + + bool getBool(AttributeType attrType) override { + #if MO_VARIABLE_TYPECHECK + if (!attributes.has(attrType)) { + MO_DBG_ERR("type err"); + return 0; + } + #endif + return value.get(attrType); + } + + InternalDataType getInternalDataType() override { + return InternalDataType::Bool; + } + + uint16_t getWriteCount() override { + return writeCount; + } +}; + +template class VariableData> +class VariableString : public Variable { +private: + VariableData value; + uint16_t writeCount = 0; + + #if MO_VARIABLE_TYPECHECK + AttributeTypeSet attributes; + #endif +public: + VariableString(AttributeTypeSet attributes) : + Variable(attributes) + #if MO_VARIABLE_TYPECHECK + , attributes(attributes) + #endif + { + + } + + ~VariableString() { + MO_FREE(value.get(AttributeType::Actual)); + value.get(AttributeType::Actual) = nullptr; + MO_FREE(value.get(AttributeType::Target)); + value.get(AttributeType::Target) = nullptr; + MO_FREE(value.get(AttributeType::MinSet)); + value.get(AttributeType::MinSet) = nullptr; + MO_FREE(value.get(AttributeType::MaxSet)); + value.get(AttributeType::MaxSet) = nullptr; + } + + bool setString(const char *val, AttributeType attrType) override { + #if MO_VARIABLE_TYPECHECK + if (!attributes.has(attrType)) { + MO_DBG_ERR("type err"); + return false; + } + #endif + + size_t len = strlen(val); + char *valNew = nullptr; + if (len != 0) { + size_t size = len + 1; + valNew = static_cast(MO_MALLOC(getMemoryTag(), size)); + if (!valNew) { + MO_DBG_ERR("OOM"); + return false; + } + memcpy(valNew, val, size); + } + MO_FREE(value.get(attrType)); + value.get(attrType) = valNew; + writeCount++; + return true; + } + + const char *getString(AttributeType attrType) override { + #if MO_VARIABLE_TYPECHECK + if (!attributes.has(attrType)) { + MO_DBG_ERR("type err"); + return 0; + } + #endif + return value.get(attrType) ? value.get(attrType) : ""; + } + + InternalDataType getInternalDataType() override { + return InternalDataType::String; + } + + uint16_t getWriteCount() override { + return writeCount; + } +}; + +std::unique_ptr MicroOcpp::makeVariable(Variable::InternalDataType dtype, Variable::AttributeTypeSet supportAttributes) { + switch(dtype) { + case Variable::InternalDataType::Int: + if (supportAttributes.count() > 1) { + return std::unique_ptr(new VariableInt(supportAttributes)); + } else { + return std::unique_ptr(new VariableInt(supportAttributes)); + } + case Variable::InternalDataType::Bool: + if (supportAttributes.count() > 1) { + return std::unique_ptr(new VariableBool(supportAttributes)); + } else { + return std::unique_ptr(new VariableBool(supportAttributes)); + } + case Variable::InternalDataType::String: + if (supportAttributes.count() > 1) { + return std::unique_ptr(new VariableString(supportAttributes)); + } else { + return std::unique_ptr(new VariableString(supportAttributes)); + } + } + + MO_DBG_ERR("internal error"); + return nullptr; +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Variables/Variable.h b/src/MicroOcpp/Model/Variables/Variable.h new file mode 100644 index 00000000..79178303 --- /dev/null +++ b/src/MicroOcpp/Model/Variables/Variable.h @@ -0,0 +1,232 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +/* + * Implementation of the UCs B05 - B06 + */ + +#ifndef MO_VARIABLE_H +#define MO_VARIABLE_H + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include + +#include +#include + +#ifndef MO_VARIABLE_TYPECHECK +#define MO_VARIABLE_TYPECHECK 1 +#endif + +namespace MicroOcpp { + +// VariableCharacteristicsType (2.51) +struct VariableCharacteristics : public MemoryManaged { + + // DataEnumType (3.26) + enum class DataType : uint8_t { + string, + decimal, + integer, + dateTime, + boolean, + OptionList, + SequenceList, + MemberList + }; + + const char *unit = nullptr; //no copy + //DataType dataType; //stored in Variable + int minLimit = std::numeric_limits::min(); + int maxLimit = std::numeric_limits::max(); + const char *valuesList = nullptr; //no copy + //bool supportsMonitoring; //stored in Variable + + VariableCharacteristics() : MemoryManaged("v201.Variables.VariableCharacteristics") { } +}; + +// SetVariableStatusEnumType (3.79) +enum class SetVariableStatus : uint8_t { + Accepted, + Rejected, + UnknownComponent, + UnknownVariable, + NotSupportedAttributeType, + RebootRequired +}; + +// GetVariableStatusEnumType (3.41) +enum class GetVariableStatus : uint8_t { + Accepted, + Rejected, + UnknownComponent, + UnknownVariable, + NotSupportedAttributeType +}; + +// ReportBaseEnumType (3.70) +typedef enum ReportBase { + ReportBase_ConfigurationInventory, + ReportBase_FullInventory, + ReportBase_SummaryInventory +} ReportBase; + +// GenericDeviceModelStatus (3.34) +typedef enum GenericDeviceModelStatus { + GenericDeviceModelStatus_Accepted, + GenericDeviceModelStatus_Rejected, + GenericDeviceModelStatus_NotSupported, + GenericDeviceModelStatus_EmptyResultSet +} GenericDeviceModelStatus; + +// VariableMonitoringType (2.52) +class VariableMonitor { +public: + //MonitorEnumType (3.55) + enum class Type { + UpperThreshold, + LowerThreshold, + Delta, + Periodic, + PeriodicClockAligned + }; +private: + int id; + bool transaction; + float value; + Type type; + int severity; +public: + VariableMonitor() = delete; + VariableMonitor(int id, bool transaction, float value, Type type, int severity) : + id(id), transaction(transaction), value(value), type(type), severity(severity) { } +}; + +// ComponentType (2.16) +struct ComponentId { + const char *name; // zero copy + //const char *instance; // not supported in this implementation + EvseId evse {-1}; + + ComponentId(const char *name = nullptr); + ComponentId(const char *name, EvseId evse); + + bool equals(const ComponentId& other) const; +}; + +/* + * Corresponds to VariableType (2.53) + * + * Template method pattern: this is a super-class which has hook-methods for storing and fetching + * the value of the variable. To make it use the host system's key-value store, extend this class + * with a custom implementation of the virtual methods and pass its instances to MO. + */ +class Variable : public MemoryManaged { +public: + //AttributeEnumType (3.2) + enum class AttributeType : uint8_t { + Actual, + Target, + MinSet, + MaxSet + }; + + struct AttributeTypeSet { + uint8_t flag = 0; + + bool has(Variable::AttributeType type); + AttributeTypeSet& set(Variable::AttributeType type); + size_t count(); + + AttributeTypeSet(AttributeType attrType = AttributeType::Actual); + }; + + //MutabilityEnumType (3.58) + enum class Mutability : uint8_t { + ReadOnly, + WriteOnly, + ReadWrite + }; + + //MO-internal optimization: if value is only in int range, store it in more compact representation + enum class InternalDataType : uint8_t { + Int, + Bool, + String + }; +private: + const char *variableName = nullptr; + ComponentId component; + + // VariableCharacteristicsType (2.51) + std::unique_ptr characteristics; //optional VariableCharacteristics + VariableCharacteristics::DataType dataType; //mandatory + bool supportsMonitoring = false; //mandatory + bool rebootRequired = false; //MO-internal: if to respond status RebootRequired on SetVariables + + // VariableAttributeType (2.50) + Mutability mutability = Mutability::ReadWrite; + bool persistent = false; + bool constant = false; + + AttributeTypeSet attributes; + + // VariableMonitoringType (2.52) + //std::vector monitors; // uncomment when testing Monitors +public: + Variable(AttributeTypeSet attributes); + + virtual ~Variable(); + + void setName(const char *name); //zero-copy + const char *getName() const; + + void setComponentId(const ComponentId& componentId); //zero-copy + const ComponentId& getComponentId() const; + + // set Value of Variable + virtual void setInt(int val, AttributeType attrType = AttributeType::Actual); + virtual void setBool(bool val, AttributeType attrType = AttributeType::Actual); + virtual bool setString(const char *val, AttributeType attrType = AttributeType::Actual); + + // get Value of Variable + virtual int getInt(AttributeType attrType = AttributeType::Actual); + virtual bool getBool(AttributeType attrType = AttributeType::Actual); + virtual const char *getString(AttributeType attrType = AttributeType::Actual); //always returns c-string (empty if undefined) + + virtual InternalDataType getInternalDataType() = 0; //corresponds to MO internal value representation + bool hasAttribute(AttributeType attrType); + + void setVariableDataType(VariableCharacteristics::DataType dataType); //corresponds to OCPP DataEnumType (3.26) + VariableCharacteristics::DataType getVariableDataType(); //corresponds to OCPP DataEnumType (3.26) + bool getSupportsMonitoring(); + void setSupportsMonitoring(); + bool isRebootRequired(); + void setRebootRequired(); + + void setMutability(Mutability m); + Mutability getMutability(); + + void setPersistent(); + bool isPersistent(); + + void setConstant(); + bool isConstant(); + + //bool addMonitor(int id, bool transaction, float value, VariableMonitor::Type type, int severity); + + virtual uint16_t getWriteCount() = 0; //get write count (use this as a pre-check if the value changed) +}; + +std::unique_ptr makeVariable(Variable::InternalDataType dtype, Variable::AttributeTypeSet supportAttributes); + +} // namespace MicroOcpp + +#endif // MO_ENABLE_V201 +#endif diff --git a/src/MicroOcpp/Model/Variables/VariableContainer.cpp b/src/MicroOcpp/Model/Variables/VariableContainer.cpp new file mode 100644 index 00000000..0d751d8f --- /dev/null +++ b/src/MicroOcpp/Model/Variables/VariableContainer.cpp @@ -0,0 +1,295 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +/* + * Implementation of the UCs B05 - B06 + */ + +#include + +#if MO_ENABLE_V201 + +#include +#include + +#include + +#include + +using namespace MicroOcpp; + +VariableContainer::~VariableContainer() { + +} + +bool VariableContainer::commit() { + return true; +} + +VariableContainerNonOwning::VariableContainerNonOwning() : + VariableContainer(), MemoryManaged("v201.Variables.VariableContainerNonOwning"), variables(makeVector(getMemoryTag())) { + +} + +size_t VariableContainerNonOwning::size() { + return variables.size(); +} + +Variable *VariableContainerNonOwning::getVariable(size_t i) { + return variables[i]; +} + +Variable *VariableContainerNonOwning::getVariable(const ComponentId& component, const char *variableName) { + for (size_t i = 0; i < variables.size(); i++) { + auto& var = variables[i]; + if (!strcmp(var->getName(), variableName) && + var->getComponentId().equals(component)) { + return var; + } + } + return nullptr; +} + +bool VariableContainerNonOwning::add(Variable *variable) { + variables.push_back(variable); + return true; +} + +bool VariableContainerOwning::checkWriteCountUpdated() { + + decltype(trackWriteCount) writeCount = 0; + + for (size_t i = 0; i < variables.size(); i++) { + writeCount += variables[i]->getWriteCount(); + } + + bool updated = writeCount != trackWriteCount; + + trackWriteCount = writeCount; + + return updated; +} + +VariableContainerOwning::VariableContainerOwning() : + VariableContainer(), MemoryManaged("v201.Variables.VariableContainerOwning"), variables(makeVector>(getMemoryTag())) { + +} + +VariableContainerOwning::~VariableContainerOwning() { + MO_FREE(filename); + filename = nullptr; +} + +size_t VariableContainerOwning::size() { + return variables.size(); +} + +Variable *VariableContainerOwning::getVariable(size_t i) { + return variables[i].get(); +} + +Variable *VariableContainerOwning::getVariable(const ComponentId& component, const char *variableName) { + for (size_t i = 0; i < variables.size(); i++) { + auto& var = variables[i]; + if (!strcmp(var->getName(), variableName) && + var->getComponentId().equals(component)) { + return var.get(); + } + } + return nullptr; +} + +bool VariableContainerOwning::add(std::unique_ptr variable) { + variables.push_back(std::move(variable)); + return true; +} + +bool VariableContainerOwning::enablePersistency(std::shared_ptr filesystem, const char *filename) { + this->filesystem = filesystem; + + MO_FREE(this->filename); + this->filename = nullptr; + + size_t fnsize = strlen(filename) + 1; + + this->filename = static_cast(MO_MALLOC(getMemoryTag(), fnsize)); + if (!this->filename) { + MO_DBG_ERR("OOM"); + return false; + } + + snprintf(this->filename, fnsize, "%s", filename); + return true; +} + +bool VariableContainerOwning::load() { + if (loaded) { + return true; + } + + if (!filesystem || !filename) { + return true; //persistency disabled - nothing to do + } + + size_t file_size = 0; + if (filesystem->stat(filename, &file_size) != 0 // file does not exist + || file_size == 0) { // file exists, but empty + MO_DBG_DEBUG("Populate FS: create variables file"); + return commit(); + } + + auto doc = FilesystemUtils::loadJson(filesystem, filename, getMemoryTag()); + if (!doc) { + MO_DBG_ERR("failed to load %s", filename); + return false; + } + + JsonArray variablesJson = (*doc)["variables"]; + + for (JsonObject stored : variablesJson) { + + const char *component = stored["component"] | (const char*)nullptr; + int evseId = stored["evseId"] | -1; + const char *name = stored["name"] | (const char*)nullptr; + + if (!component || !name) { + MO_DBG_ERR("corrupt entry: %s", filename); + continue; + } + + auto variablePtr = getVariable(ComponentId(component, EvseId(evseId)), name); + if (!variablePtr) { + MO_DBG_ERR("loaded variable does not exist: %s, %s, %s", filename, component, name); + continue; + } + + auto& variable = *variablePtr; + + switch (variable.getInternalDataType()) { + case Variable::InternalDataType::Int: + if (variable.hasAttribute(Variable::AttributeType::Actual)) variable.setInt(stored["valActual"] | 0, Variable::AttributeType::Actual); + if (variable.hasAttribute(Variable::AttributeType::Target)) variable.setInt(stored["valTarget"] | 0, Variable::AttributeType::Target); + if (variable.hasAttribute(Variable::AttributeType::MinSet)) variable.setInt(stored["valMinSet"] | 0, Variable::AttributeType::MinSet); + if (variable.hasAttribute(Variable::AttributeType::MaxSet)) variable.setInt(stored["valMaxSet"] | 0, Variable::AttributeType::MaxSet); + break; + case Variable::InternalDataType::Bool: + if (variable.hasAttribute(Variable::AttributeType::Actual)) variable.setBool(stored["valActual"] | false, Variable::AttributeType::Actual); + if (variable.hasAttribute(Variable::AttributeType::Target)) variable.setBool(stored["valTarget"] | false, Variable::AttributeType::Target); + if (variable.hasAttribute(Variable::AttributeType::MinSet)) variable.setBool(stored["valMinSet"] | false, Variable::AttributeType::MinSet); + if (variable.hasAttribute(Variable::AttributeType::MaxSet)) variable.setBool(stored["valMaxSet"] | false, Variable::AttributeType::MaxSet); + break; + case Variable::InternalDataType::String: + bool success = true; + if (variable.hasAttribute(Variable::AttributeType::Actual)) success &= variable.setString(stored["valActual"] | "", Variable::AttributeType::Actual); + if (variable.hasAttribute(Variable::AttributeType::Target)) success &= variable.setString(stored["valTarget"] | "", Variable::AttributeType::Target); + if (variable.hasAttribute(Variable::AttributeType::MinSet)) success &= variable.setString(stored["valMinSet"] | "", Variable::AttributeType::MinSet); + if (variable.hasAttribute(Variable::AttributeType::MaxSet)) success &= variable.setString(stored["valMaxSet"] | "", Variable::AttributeType::MaxSet); + if (!success) { + MO_DBG_ERR("value error: %s, %s, %s", filename, component, name); + continue; + } + break; + } + } + + checkWriteCountUpdated(); // update trackWriteCount after load is completed + + MO_DBG_DEBUG("Initialization finished"); + loaded = true; + return true; +} + +bool VariableContainerOwning::commit() { + if (!filesystem || !filename) { + //persistency disabled - nothing to do + return true; + } + + if (!checkWriteCountUpdated()) { + return true; //nothing to be done + } + + size_t jsonCapacity = JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(0); + size_t variableCapacity = 0; + for (size_t i = 0; i < variables.size(); i++) { + auto& variable = *variables[i]; + + if (!variable.isPersistent()) { + continue; + } + + size_t addedJsonCapacity = JSON_ARRAY_SIZE(variableCapacity + 1) - JSON_ARRAY_SIZE(variableCapacity); + + size_t storedEntities = 2; //component name, variable name will always be stored + storedEntities += variable.getComponentId().evse.id >= 0 ? 1 : 0; + storedEntities += variable.hasAttribute(Variable::AttributeType::Actual) ? 1 : 0; + storedEntities += variable.hasAttribute(Variable::AttributeType::Target) ? 1 : 0; + storedEntities += variable.hasAttribute(Variable::AttributeType::MinSet) ? 1 : 0; + storedEntities += variable.hasAttribute(Variable::AttributeType::MaxSet) ? 1 : 0; + + addedJsonCapacity += JSON_OBJECT_SIZE(storedEntities); + + if (jsonCapacity + addedJsonCapacity <= MO_MAX_JSON_CAPACITY) { + jsonCapacity += addedJsonCapacity; + variableCapacity++; + } else { + MO_DBG_ERR("configs JSON exceeds maximum capacity (%s, %zu entries). Crop configs file (by FCFS)", filename, variables.size()); + break; + } + } + + auto doc = initJsonDoc(getMemoryTag(), jsonCapacity); + + JsonArray variablesJson = doc.createNestedArray("variables"); + + for (size_t i = 0; i < variableCapacity; i++) { + auto& variable = *variables[i]; + + if (!variable.isPersistent()) { + continue; + } + + auto stored = variablesJson.createNestedObject(); + + stored["component"] = variable.getComponentId().name; + if (variable.getComponentId().evse.id >= 0) { + stored["evseId"] = variable.getComponentId().evse.id; + } + stored["name"] = variable.getName(); + + switch (variable.getInternalDataType()) { + case Variable::InternalDataType::Int: + if (variable.hasAttribute(Variable::AttributeType::Actual)) stored["valActual"] = variable.getInt(Variable::AttributeType::Actual); + if (variable.hasAttribute(Variable::AttributeType::Target)) stored["valTarget"] = variable.getInt(Variable::AttributeType::Target); + if (variable.hasAttribute(Variable::AttributeType::MinSet)) stored["valMinSet"] = variable.getInt(Variable::AttributeType::MinSet); + if (variable.hasAttribute(Variable::AttributeType::MaxSet)) stored["valMaxSet"] = variable.getInt(Variable::AttributeType::MaxSet); + break; + case Variable::InternalDataType::Bool: + if (variable.hasAttribute(Variable::AttributeType::Actual)) stored["valActual"] = variable.getBool(Variable::AttributeType::Actual); + if (variable.hasAttribute(Variable::AttributeType::Target)) stored["valTarget"] = variable.getBool(Variable::AttributeType::Target); + if (variable.hasAttribute(Variable::AttributeType::MinSet)) stored["valMinSet"] = variable.getBool(Variable::AttributeType::MinSet); + if (variable.hasAttribute(Variable::AttributeType::MaxSet)) stored["valMaxSet"] = variable.getBool(Variable::AttributeType::MaxSet); + break; + case Variable::InternalDataType::String: + if (variable.hasAttribute(Variable::AttributeType::Actual)) stored["valActual"] = variable.getString(Variable::AttributeType::Actual); + if (variable.hasAttribute(Variable::AttributeType::Target)) stored["valTarget"] = variable.getString(Variable::AttributeType::Target); + if (variable.hasAttribute(Variable::AttributeType::MinSet)) stored["valMinSet"] = variable.getString(Variable::AttributeType::MinSet); + if (variable.hasAttribute(Variable::AttributeType::MaxSet)) stored["valMaxSet"] = variable.getString(Variable::AttributeType::MaxSet); + break; + } + } + + + bool success = FilesystemUtils::storeJson(filesystem, filename, doc); + + if (success) { + MO_DBG_DEBUG("Saving variables finished"); + } else { + MO_DBG_ERR("could not save variables file: %s", filename); + } + + return success; +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Variables/VariableContainer.h b/src/MicroOcpp/Model/Variables/VariableContainer.h new file mode 100644 index 00000000..d4dd0c69 --- /dev/null +++ b/src/MicroOcpp/Model/Variables/VariableContainer.h @@ -0,0 +1,76 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +/* + * Implementation of the UCs B05 - B06 + */ + +#ifndef MO_VARIABLECONTAINER_H +#define MO_VARIABLECONTAINER_H + +#include + +#if MO_ENABLE_V201 + +#include + +#include +#include +#include + +namespace MicroOcpp { + +class VariableContainer { +public: + ~VariableContainer(); + virtual size_t size() = 0; + virtual Variable *getVariable(size_t i) = 0; + virtual Variable *getVariable(const ComponentId& component, const char *variableName) = 0; + + virtual bool commit(); +}; + +class VariableContainerNonOwning : public VariableContainer, public MemoryManaged { +private: + Vector variables; +public: + VariableContainerNonOwning(); + + size_t size() override; + Variable *getVariable(size_t i) override; + Variable *getVariable(const ComponentId& component, const char *variableName) override; + + bool add(Variable *variable); +}; + +class VariableContainerOwning : public VariableContainer, public MemoryManaged { +private: + Vector> variables; + std::shared_ptr filesystem; + char *filename = nullptr; + + uint16_t trackWriteCount = 0; + bool checkWriteCountUpdated(); + + bool loaded = false; + +public: + VariableContainerOwning(); + ~VariableContainerOwning(); + + size_t size() override; + Variable *getVariable(size_t i) override; + Variable *getVariable(const ComponentId& component, const char *variableName) override; + + bool add(std::unique_ptr variable); + + bool enablePersistency(std::shared_ptr filesystem, const char *filename); + bool load(); //load variables from flash + bool commit() override; +}; + +} //end namespace MicroOcpp + +#endif //MO_ENABLE_V201 +#endif diff --git a/src/MicroOcpp/Model/Variables/VariableService.cpp b/src/MicroOcpp/Model/Variables/VariableService.cpp new file mode 100644 index 00000000..9c0d6fde --- /dev/null +++ b/src/MicroOcpp/Model/Variables/VariableService.cpp @@ -0,0 +1,490 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +/* + * Implementation of the UCs B05 - B06 + */ + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace MicroOcpp { + +template +VariableValidator::VariableValidator(const ComponentId& component, const char *name, bool (*validateFn)(T, void*), void *userPtr) : + MemoryManaged("v201.Variables.VariableValidator.", name), component(component), name(name), userPtr(userPtr), validateFn(validateFn) { + +} + +template +bool VariableValidator::validate(T v) { + return validateFn(v, userPtr); +} + +template +VariableValidator *getVariableValidator(Vector>& collection, const ComponentId& component, const char *name) { + for (size_t i = 0; i < collection.size(); i++) { + auto& validator = collection[i]; + if (!strcmp(name, validator.name) && component.equals(validator.component)) { + return &validator; + } + } + return nullptr; +} + +VariableValidator *VariableService::getValidatorInt(const ComponentId& component, const char *name) { + return getVariableValidator(validatorInt, component, name); +} + +VariableValidator *VariableService::getValidatorBool(const ComponentId& component, const char *name) { + return getVariableValidator(validatorBool, component, name); +} + +VariableValidator *VariableService::getValidatorString(const ComponentId& component, const char *name) { + return getVariableValidator(validatorString, component, name); +} + +VariableContainerOwning& VariableService::getContainerInternalByVariable(const ComponentId& component, const char *name) { + unsigned int hash = 0; + for (size_t i = 0; i < strlen(component.name); i++) { + hash += (unsigned int)component.name[i]; + } + if (component.evse.id >= 0) + hash += (unsigned int)component.evse.id; + if (component.evse.connectorId >= 0) + hash += (unsigned int)component.evse.connectorId; + for (size_t i = 0; i < strlen(name); i++) { + hash += (unsigned int)name[i]; + } + return containersInternal[hash % MO_VARIABLESTORE_BUCKETS]; +} + +void VariableService::addContainer(VariableContainer *container) { + containers.push_back(container); +} + +template +bool registerVariableValidator(Vector>& collection, const ComponentId& component, const char *name, bool (*validate)(T, void*), void *userPtr) { + for (auto it = collection.begin(); it != collection.end(); it++) { + if (!strcmp(name, it->name) && component.equals(it->component)) { + collection.erase(it); + break; + } + } + collection.emplace_back(component, name, validate, userPtr); + return true; +} + +template <> +bool VariableService::registerValidator(const ComponentId& component, const char *name, bool (*validate)(int, void*), void *userPtr) { + return registerVariableValidator(validatorInt, component, name, validate, userPtr); +} + +template <> +bool VariableService::registerValidator(const ComponentId& component, const char *name, bool (*validate)(bool, void*), void *userPtr) { + return registerVariableValidator(validatorBool, component, name, validate, userPtr); +} + +template <> +bool VariableService::registerValidator(const ComponentId& component, const char *name, bool (*validate)(const char*, void*), void *userPtr) { + return registerVariableValidator(validatorString, component, name, validate, userPtr); +} + +Variable *VariableService::getVariable(const ComponentId& component, const char *name) { + + if (auto variable = getContainerInternalByVariable(component, name).getVariable(component, name)) { + return variable; + } + + for (size_t i = 0; i < containers.size(); i++) { + auto container = containers[containers.size() - i - 1]; //search from back, because internal containers at front don't contain variable + if (auto variable = container->getVariable(component, name)) { + return variable; + } + } + + return nullptr; +} + +VariableService::VariableService(Context& context, std::shared_ptr filesystem) : + MemoryManaged("v201.Variables.VariableService"), + context(context), filesystem(filesystem), + containers(makeVector(getMemoryTag())), + validatorInt(makeVector>(getMemoryTag())), + validatorBool(makeVector>(getMemoryTag())), + validatorString(makeVector>(getMemoryTag())) { + + containers.reserve(MO_VARIABLESTORE_BUCKETS + 1); + + for (unsigned int i = 0; i < MO_VARIABLESTORE_BUCKETS; i++) { + char fn [MO_MAX_PATH_SIZE]; + auto ret = snprintf(fn, sizeof(fn), "%s%02x%s", MO_VARIABLESTORE_FN_PREFIX, i, MO_VARIABLESTORE_FN_SUFFIX); + if (ret < 0 || (size_t)ret >= sizeof(fn)) { + MO_DBG_ERR("fn error"); + continue; + } + containersInternal[i].enablePersistency(filesystem, fn); + containers.push_back(&containersInternal[i]); + } + containers.push_back(&containerExternal); + + context.getOperationRegistry().registerOperation("SetVariables", [this] () { + return new Ocpp201::SetVariables(*this);}); + context.getOperationRegistry().registerOperation("GetVariables", [this] () { + return new Ocpp201::GetVariables(*this);}); + context.getOperationRegistry().registerOperation("GetBaseReport", [this] () { + return new Ocpp201::GetBaseReport(*this);}); +} + +template +bool loadVariableFactoryDefault(Variable& variable, T factoryDef); + +template<> +bool loadVariableFactoryDefault(Variable& variable, int factoryDef) { + variable.setInt(factoryDef); + return true; +} + +template<> +bool loadVariableFactoryDefault(Variable& variable, bool factoryDef) { + variable.setBool(factoryDef); + return true; +} + +template<> +bool loadVariableFactoryDefault(Variable& variable, const char *factoryDef) { + return variable.setString(factoryDef); +} + +void loadVariableCharacteristics(Variable& variable, Variable::Mutability mutability, bool persistent, bool rebootRequired, Variable::InternalDataType defaultDataType) { + if (variable.getMutability() == Variable::Mutability::ReadWrite) { + variable.setMutability(mutability); + } + + if (persistent) { + variable.setPersistent(); + } + + if (rebootRequired) { + variable.setRebootRequired(); + } + + switch (defaultDataType) { + case Variable::InternalDataType::Int: + variable.setVariableDataType(MicroOcpp::VariableCharacteristics::DataType::integer); + break; + case Variable::InternalDataType::Bool: + variable.setVariableDataType(MicroOcpp::VariableCharacteristics::DataType::boolean); + break; + case Variable::InternalDataType::String: + variable.setVariableDataType(MicroOcpp::VariableCharacteristics::DataType::string); + break; + default: + MO_DBG_ERR("internal error"); + break; + } +} + +template +Variable::InternalDataType getInternalDataType(); + +template<> Variable::InternalDataType getInternalDataType() {return Variable::InternalDataType::Int;} +template<> Variable::InternalDataType getInternalDataType() {return Variable::InternalDataType::Bool;} +template<> Variable::InternalDataType getInternalDataType() {return Variable::InternalDataType::String;} + +template +Variable *VariableService::declareVariable(const ComponentId& component, const char *name, T factoryDefault, Variable::Mutability mutability, bool persistent, Variable::AttributeTypeSet attributes, bool rebootRequired) { + + auto res = getVariable(component, name); + if (!res) { + auto variable = makeVariable(getInternalDataType(), attributes); + if (!variable) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + variable->setName(name); + variable->setComponentId(component); + + if (!loadVariableFactoryDefault(*variable, factoryDefault)) { + return nullptr; + } + + res = variable.get(); + + if (!getContainerInternalByVariable(component, name).add(std::move(variable))) { + return nullptr; + } + } + + loadVariableCharacteristics(*res, mutability, persistent, rebootRequired, getInternalDataType()); + return res; +} + +template Variable *VariableService::declareVariable( const ComponentId&, const char*, int, Variable::Mutability, bool, Variable::AttributeTypeSet, bool); +template Variable *VariableService::declareVariable( const ComponentId&, const char*, bool, Variable::Mutability, bool, Variable::AttributeTypeSet, bool); +template Variable *VariableService::declareVariable(const ComponentId&, const char*, const char*, Variable::Mutability, bool, Variable::AttributeTypeSet, bool); + +bool VariableService::addVariable(Variable *variable) { + return containerExternal.add(variable); +} + +bool VariableService::addVariable(std::unique_ptr variable) { + return getContainerInternalByVariable(variable->getComponentId(), variable->getName()).add(std::move(variable)); +} + +bool VariableService::load() { + bool success = true; + + for (size_t i = 0; i < MO_VARIABLESTORE_BUCKETS; i++) { + if (!containersInternal[i].load()) { + success = false; + } + } + + return success; +} + +bool VariableService::commit() { + bool success = true; + + for (size_t i = 0; i < containers.size(); i++) { + if (!containers[i]->commit()) { + success = false; + } + } + + return success; +} + +SetVariableStatus VariableService::setVariable(Variable::AttributeType attrType, const char *value, const ComponentId& component, const char *variableName) { + + Variable *variable = nullptr; + + bool foundComponent = false; + for (size_t i = 0; i < containers.size(); i++) { + auto container = containers[i]; + + for (size_t i = 0; i < container->size(); i++) { + auto entry = container->getVariable(i); + + if (entry->getComponentId().equals(component)) { + foundComponent = true; + + if (!strcmp(entry->getName(), variableName)) { + // found variable. Search terminated in this block + + variable = entry; + break; + } + } + } + if (variable) { + // result found in inner for-loop + break; + } + } + + if (!variable) { + if (foundComponent) { + return SetVariableStatus::UnknownVariable; + } else { + return SetVariableStatus::UnknownComponent; + } + } + + if (variable->getMutability() == Variable::Mutability::ReadOnly) { + return SetVariableStatus::Rejected; + } + + if (!variable->hasAttribute(attrType)) { + return SetVariableStatus::NotSupportedAttributeType; + } + + //write config + + /* + * Try to interpret input as number + */ + + bool convertibleInt = true; + int numInt = 0; + bool convertibleBool = true; + bool numBool = false; + + int nDigits = 0, nNonDigits = 0, nDots = 0, nSign = 0; //"-1.234" has 4 digits, 0 nonDigits, 1 dot and 1 sign. Don't allow comma as seperator. Don't allow e-expressions (e.g. 1.23e-7) + for (const char *c = value; *c; ++c) { + if (*c >= '0' && *c <= '9') { + //int interpretation + if (nDots == 0) { //only append number if before floating point + nDigits++; + numInt *= 10; + numInt += *c - '0'; + } + } else if (*c == '.') { + nDots++; + } else if (c == value && *c == '-') { + nSign++; + } else { + nNonDigits++; + } + } + + if (nSign == 1) { + numInt = -numInt; + } + + int INT_MAXDIGITS; //plausibility check: this allows a numerical range of (-999,999,999 to 999,999,999), or (-9,999 to 9,999) respectively + if (sizeof(int) >= 4UL) + INT_MAXDIGITS = 9; + else + INT_MAXDIGITS = 4; + + if (nNonDigits > 0 || nDigits == 0 || nSign > 1 || nDots > 1) { + convertibleInt = false; + } + + if (nDigits > INT_MAXDIGITS) { + MO_DBG_DEBUG("Possible integer overflow: key = %s, value = %s", variableName, value); + convertibleInt = false; + } + + if (tolower(value[0]) == 't' && tolower(value[1]) == 'r' && tolower(value[2]) == 'u' && tolower(value[3]) == 'e' && !value[4]) { + numBool = true; + } else if (tolower(value[0]) == 'f' && tolower(value[1]) == 'a' && tolower(value[2]) == 'l' && tolower(value[3]) == 's' && tolower(value[4]) == 'e' && !value[5]) { + numBool = false; + } else if (convertibleInt) { + numBool = numInt != 0; + } else { + convertibleBool = false; + } + + // validate and store (parsed) value to Config + + if (variable->getInternalDataType() == Variable::InternalDataType::Int && convertibleInt) { + auto validator = getValidatorInt(component, variableName); + if (validator && !validator->validate(numInt)) { + MO_DBG_WARN("validation failed for variable=%s", variableName); + return SetVariableStatus::Rejected; + } + variable->setInt(numInt); + } else if (variable->getInternalDataType() == Variable::InternalDataType::Bool && convertibleBool) { + auto validator = getValidatorBool(component, variableName); + if (validator && !validator->validate(numBool)) { + MO_DBG_WARN("validation failed for variable=%s", variableName); + return SetVariableStatus::Rejected; + } + variable->setBool(numBool); + } else if (variable->getInternalDataType() == Variable::InternalDataType::String) { + auto validator = getValidatorString(component, variableName); + if (validator && !validator->validate(value)) { + MO_DBG_WARN("validation failed for variable=%s", variableName); + return SetVariableStatus::Rejected; + } + variable->setString(value); + } else { + MO_DBG_WARN("Value has incompatible type"); + return SetVariableStatus::Rejected; + } + + if (variable->isRebootRequired()) { + return SetVariableStatus::RebootRequired; + } + + return SetVariableStatus::Accepted; +} + +GetVariableStatus VariableService::getVariable(Variable::AttributeType attrType, const ComponentId& component, const char *variableName, Variable **result) { + + bool foundComponent = false; + for (size_t i = 0; i < containers.size(); i++) { + auto container = containers[i]; + + for (size_t i = 0; i < container->size(); i++) { + auto variable = container->getVariable(i); + + if (variable->getComponentId().equals(component)) { + foundComponent = true; + + if (!strcmp(variable->getName(), variableName)) { + // found variable. Search terminated in this block + + if (variable->getMutability() == Variable::Mutability::WriteOnly) { + return GetVariableStatus::Rejected; + } + + if (variable->hasAttribute(attrType)) { + *result = variable; + return GetVariableStatus::Accepted; + } else { + return GetVariableStatus::NotSupportedAttributeType; + } + } + } + } + } + + if (foundComponent) { + return GetVariableStatus::UnknownVariable; + } else { + return GetVariableStatus::UnknownComponent; + } +} + +GenericDeviceModelStatus VariableService::getBaseReport(int requestId, ReportBase reportBase) { + + if (reportBase == ReportBase_SummaryInventory) { + return GenericDeviceModelStatus_NotSupported; + } + + Vector variables = makeVector(getMemoryTag()); + + for (size_t i = 0; i < containers.size(); i++) { + auto container = containers[i]; + + for (size_t i = 0; i < container->size(); i++) { + auto variable = container->getVariable(i); + + if (reportBase == ReportBase_ConfigurationInventory && variable->getMutability() == Variable::Mutability::ReadOnly) { + continue; + } + + variables.push_back(variable); + } + } + + if (variables.empty()) { + return GenericDeviceModelStatus_EmptyResultSet; + } + + auto notifyReport = makeRequest(new Ocpp201::NotifyReport( + context.getModel(), + requestId, + context.getModel().getClock().now(), + false, + 0, + variables)); + + context.initiateRequest(std::move(notifyReport)); + + return GenericDeviceModelStatus_Accepted; +} + +} // namespace MicroOcpp + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Variables/VariableService.h b/src/MicroOcpp/Model/Variables/VariableService.h new file mode 100644 index 00000000..afe19099 --- /dev/null +++ b/src/MicroOcpp/Model/Variables/VariableService.h @@ -0,0 +1,99 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +/* + * Implementation of the UCs B05 - B06 + */ + +#ifndef MO_VARIABLESERVICE_H +#define MO_VARIABLESERVICE_H + +#include +#include +#include + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include +#include + +#ifndef MO_VARIABLESTORE_FN_PREFIX +#define MO_VARIABLESTORE_FN_PREFIX (MO_FILENAME_PREFIX "ocpp-vars-") +#endif + +#ifndef MO_VARIABLESTORE_FN_SUFFIX +#define MO_VARIABLESTORE_FN_SUFFIX ".jsn" +#endif + +namespace MicroOcpp { + +template +struct VariableValidator : public MemoryManaged { + ComponentId component; + const char *name; + void *userPtr; + bool (*validateFn)(T, void*); + VariableValidator(const ComponentId& component, const char *name, bool (*validate)(T, void*), void *userPtr); + bool validate(T); +}; + +class Context; + +#ifndef MO_VARIABLESTORE_BUCKETS +#define MO_VARIABLESTORE_BUCKETS 8 +#endif + +class VariableService : public MemoryManaged { +private: + Context& context; + std::shared_ptr filesystem; + Vector containers; + VariableContainerNonOwning containerExternal; + VariableContainerOwning containersInternal [MO_VARIABLESTORE_BUCKETS]; + VariableContainerOwning& getContainerInternalByVariable(const ComponentId& component, const char *name); + + Vector> validatorInt; + Vector> validatorBool; + Vector> validatorString; + + VariableValidator *getValidatorInt(const ComponentId& component, const char *name); + VariableValidator *getValidatorBool(const ComponentId& component, const char *name); + VariableValidator *getValidatorString(const ComponentId& component, const char *name); +public: + VariableService(Context& context, std::shared_ptr filesystem); + + //Get Variable. If not existent, create Variable owned by MO and return + template + Variable *declareVariable(const ComponentId& component, const char *name, T factoryDefault, Variable::Mutability mutability = Variable::Mutability::ReadWrite, bool persistent = true, Variable::AttributeTypeSet attributes = Variable::AttributeTypeSet(), bool rebootRequired = false); + + bool addVariable(Variable *variable); //Add Variable without transferring ownership + bool addVariable(std::unique_ptr variable); //Add Variable and transfer ownership + + //Get Variable. If not existent, return nullptr + Variable *getVariable(const ComponentId& component, const char *name); + + bool load(); + bool commit(); + + void addContainer(VariableContainer *container); + + template + bool registerValidator(const ComponentId& component, const char *name, bool (*validate)(T, void*), void *userPtr = nullptr); + + SetVariableStatus setVariable(Variable::AttributeType attrType, const char *attrVal, const ComponentId& component, const char *variableName); + + GetVariableStatus getVariable(Variable::AttributeType attrType, const ComponentId& component, const char *variableName, Variable **result); + + GenericDeviceModelStatus getBaseReport(int requestId, ReportBase reportBase); +}; + +} // namespace MicroOcpp + +#endif // MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Operations/Authorize.cpp b/src/MicroOcpp/Operations/Authorize.cpp index 3528f04c..e12050be 100644 --- a/src/MicroOcpp/Operations/Authorize.cpp +++ b/src/MicroOcpp/Operations/Authorize.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -8,14 +8,16 @@ #include -using MicroOcpp::Ocpp16::Authorize; +using namespace MicroOcpp; -Authorize::Authorize(Model& model, const char *idTagIn) : model(model) { +namespace MicroOcpp { +namespace Ocpp16 { + +Authorize::Authorize(Model& model, const char *idTagIn) : MemoryManaged("v16.Operation.", "Authorize"), model(model) { if (idTagIn && strnlen(idTagIn, IDTAG_LEN_MAX + 2) <= IDTAG_LEN_MAX) { snprintf(idTag, IDTAG_LEN_MAX + 1, "%s", idTagIn); } else { MO_DBG_WARN("Format violation of idTag. Discard idTag"); - (void)0; } } @@ -23,8 +25,8 @@ const char* Authorize::getOperationType(){ return "Authorize"; } -std::unique_ptr Authorize::createReq() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1) + (IDTAG_LEN_MAX + 1))); +std::unique_ptr Authorize::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + (IDTAG_LEN_MAX + 1)); JsonObject payload = doc->to(); payload["idTag"] = idTag; return doc; @@ -39,9 +41,11 @@ void Authorize::processConf(JsonObject payload){ MO_DBG_INFO("Request has been denied. Reason: %s", idTagInfo); } - if (model.getAuthorizationService()) { - model.getAuthorizationService()->notifyAuthorization(idTag, payload["idTagInfo"]); +#if MO_ENABLE_LOCAL_AUTH + if (auto authService = model.getAuthorizationService()) { + authService->notifyAuthorization(idTag, payload["idTagInfo"]); } +#endif //MO_ENABLE_LOCAL_AUTH } void Authorize::processReq(JsonObject payload){ @@ -50,10 +54,69 @@ void Authorize::processReq(JsonObject payload){ */ } -std::unique_ptr Authorize::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(2 * JSON_OBJECT_SIZE(1))); +std::unique_ptr Authorize::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), 2 * JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); idTagInfo["status"] = "Accepted"; return doc; } + +} // namespace Ocpp16 +} // namespace MicroOcpp + +#if MO_ENABLE_V201 + +namespace MicroOcpp { +namespace Ocpp201 { + +Authorize::Authorize(Model& model, const IdToken& idToken) : MemoryManaged("v201.Operation.Authorize"), model(model) { + this->idToken = idToken; +} + +const char* Authorize::getOperationType(){ + return "Authorize"; +} + +std::unique_ptr Authorize::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), + JSON_OBJECT_SIZE(1) + + JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + payload["idToken"]["idToken"] = idToken.get(); + payload["idToken"]["type"] = idToken.getTypeCstr(); + return doc; +} + +void Authorize::processConf(JsonObject payload){ + const char *idTagInfo = payload["idTokenInfo"]["status"] | "_Undefined"; + + if (!strcmp(idTagInfo, "Accepted")) { + MO_DBG_INFO("Request has been accepted"); + } else { + MO_DBG_INFO("Request has been denied. Reason: %s", idTagInfo); + } + + //if (model.getAuthorizationService()) { + // model.getAuthorizationService()->notifyAuthorization(idTag, payload["idTagInfo"]); + //} +} + +void Authorize::processReq(JsonObject payload){ + /* + * Ignore Contents of this Req-message, because this is for debug purposes only + */ +} + +std::unique_ptr Authorize::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), 2 * JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + JsonObject idTagInfo = payload.createNestedObject("idTokenInfo"); + idTagInfo["status"] = "Accepted"; + return doc; +} + +} // namespace Ocpp201 +} // namespace MicroOcpp + +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/Authorize.h b/src/MicroOcpp/Operations/Authorize.h index 7f070e6c..b49226ef 100644 --- a/src/MicroOcpp/Operations/Authorize.h +++ b/src/MicroOcpp/Operations/Authorize.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef AUTHORIZE_H @@ -7,6 +7,7 @@ #include #include +#include namespace MicroOcpp { @@ -14,7 +15,7 @@ class Model; namespace Ocpp16 { -class Authorize : public Operation { +class Authorize : public Operation, public MemoryManaged { private: Model& model; char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; @@ -23,17 +24,48 @@ class Authorize : public Operation { const char* getOperationType() override; - std::unique_ptr createReq() override; + std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp +#if MO_ENABLE_V201 + +#include + +namespace MicroOcpp { +namespace Ocpp201 { + +class Authorize : public Operation, public MemoryManaged { +private: + Model& model; + IdToken idToken; +public: + Authorize(Model& model, const IdToken& idToken); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_V201 + #endif diff --git a/src/MicroOcpp/Operations/BootNotification.cpp b/src/MicroOcpp/Operations/BootNotification.cpp index f59c1bfe..1f4b7c77 100644 --- a/src/MicroOcpp/Operations/BootNotification.cpp +++ b/src/MicroOcpp/Operations/BootNotification.cpp @@ -1,18 +1,20 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include #include +#include #include #include using MicroOcpp::Ocpp16::BootNotification; +using MicroOcpp::JsonDoc; -BootNotification::BootNotification(Model& model, std::unique_ptr payload) : model(model), credentials(std::move(payload)) { +BootNotification::BootNotification(Model& model, std::unique_ptr payload) : MemoryManaged("v16.Operation.", "BootNotification"), model(model), credentials(std::move(payload)) { } @@ -20,9 +22,18 @@ const char* BootNotification::getOperationType(){ return "BootNotification"; } -std::unique_ptr BootNotification::createReq() { +std::unique_ptr BootNotification::createReq() { if (credentials) { - return std::unique_ptr(new DynamicJsonDocument(*credentials)); +#if MO_ENABLE_V201 + if (model.getVersion().major == 2) { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2) + credentials->memoryUsage()); + JsonObject payload = doc->to(); + payload["reason"] = "PowerUp"; + payload["chargingStation"] = *credentials; + return doc; + } +#endif + return std::unique_ptr(new JsonDoc(*credentials)); } else { MO_DBG_ERR("payload undefined"); return createEmptyDocument(); @@ -89,8 +100,8 @@ void BootNotification::processReq(JsonObject payload){ */ } -std::unique_ptr BootNotification::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(3) + (JSONDATE_LENGTH + 1))); +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 diff --git a/src/MicroOcpp/Operations/BootNotification.h b/src/MicroOcpp/Operations/BootNotification.h index 4a85833f..c560e03f 100644 --- a/src/MicroOcpp/Operations/BootNotification.h +++ b/src/MicroOcpp/Operations/BootNotification.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef BOOTNOTIFICATION_H -#define BOOTNOTIFICATION_H +#ifndef MO_BOOTNOTIFICATION_H +#define MO_BOOTNOTIFICATION_H #include #include @@ -19,25 +19,25 @@ class Model; namespace Ocpp16 { -class BootNotification : public Operation { +class BootNotification : public Operation, public MemoryManaged { private: Model& model; - std::unique_ptr credentials; + std::unique_ptr credentials; const char *errorCode = nullptr; public: - BootNotification(Model& model, std::unique_ptr payload); + BootNotification(Model& model, std::unique_ptr payload); ~BootNotification() = default; const char* getOperationType() override; - std::unique_ptr createReq() override; + std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; diff --git a/src/MicroOcpp/Operations/CancelReservation.cpp b/src/MicroOcpp/Operations/CancelReservation.cpp index 25088fe7..7741d9b9 100644 --- a/src/MicroOcpp/Operations/CancelReservation.cpp +++ b/src/MicroOcpp/Operations/CancelReservation.cpp @@ -1,19 +1,23 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + +#if MO_ENABLE_RESERVATION + #include -#include #include #include using MicroOcpp::Ocpp16::CancelReservation; +using MicroOcpp::JsonDoc; -CancelReservation::CancelReservation(Model& model) : model(model) { +CancelReservation::CancelReservation(ReservationService& reservationService) : MemoryManaged("v16.Operation.", "CancelReservation"), reservationService(reservationService) { } -const char* CancelReservation::getOperationType(){ +const char* CancelReservation::getOperationType() { return "CancelReservation"; } @@ -23,18 +27,14 @@ void CancelReservation::processReq(JsonObject payload) { return; } - if (model.getReservationService()) { - if (auto reservation = model.getReservationService()->getReservationById(payload["reservationId"])) { - found = true; - reservation->clear(); - } - } else { - errorCode = "InternalError"; + if (auto reservation = reservationService.getReservationById(payload["reservationId"])) { + found = true; + reservation->clear(); } } -std::unique_ptr CancelReservation::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr CancelReservation::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (found) { payload["status"] = "Accepted"; @@ -43,3 +43,5 @@ std::unique_ptr CancelReservation::createConf(){ } return doc; } + +#endif //MO_ENABLE_RESERVATION diff --git a/src/MicroOcpp/Operations/CancelReservation.h b/src/MicroOcpp/Operations/CancelReservation.h index b268a8fd..0e39bb60 100644 --- a/src/MicroOcpp/Operations/CancelReservation.h +++ b/src/MicroOcpp/Operations/CancelReservation.h @@ -1,31 +1,35 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef CANCELRESERVATION_H -#define CANCELRESERVATION_H +#ifndef MO_CANCELRESERVATION_H +#define MO_CANCELRESERVATION_H + +#include + +#if MO_ENABLE_RESERVATION #include namespace MicroOcpp { -class Model; +class ReservationService; namespace Ocpp16 { -class CancelReservation : public Operation { +class CancelReservation : public Operation, public MemoryManaged { private: - Model& model; + ReservationService& reservationService; bool found = false; const char *errorCode = nullptr; public: - CancelReservation(Model& model); + CancelReservation(ReservationService& reservationService); const char* getOperationType() override; void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; @@ -33,4 +37,5 @@ class CancelReservation : public Operation { } //end namespace Ocpp16 } //end namespace MicroOcpp +#endif //MO_ENABLE_RESERVATION #endif diff --git a/src/MicroOcpp/Operations/ChangeAvailability.cpp b/src/MicroOcpp/Operations/ChangeAvailability.cpp index ae66235d..7377657c 100644 --- a/src/MicroOcpp/Operations/ChangeAvailability.cpp +++ b/src/MicroOcpp/Operations/ChangeAvailability.cpp @@ -1,16 +1,18 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include +#include #include -using MicroOcpp::Ocpp16::ChangeAvailability; +namespace MicroOcpp { +namespace Ocpp16 { -ChangeAvailability::ChangeAvailability(Model& model) : model(model) { +ChangeAvailability::ChangeAvailability(Model& model) : MemoryManaged("v16.Operation.", "ChangeAvailability"), model(model) { } @@ -24,7 +26,7 @@ void ChangeAvailability::processReq(JsonObject payload) { errorCode = "FormationViolation"; return; } - unsigned int connectorId = (unsigned int) connectorIdRaw; + unsigned int connectorId = (unsigned int)connectorIdRaw; if (connectorId >= model.getNumConnectors()) { errorCode = "PropertyConstraintViolation"; @@ -62,8 +64,8 @@ void ChangeAvailability::processReq(JsonObject payload) { } } -std::unique_ptr ChangeAvailability::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr ChangeAvailability::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (!accepted) { payload["status"] = "Rejected"; @@ -75,3 +77,85 @@ std::unique_ptr ChangeAvailability::createConf(){ return doc; } + +} // namespace Ocpp16 +} // namespace MicroOcpp + +#if MO_ENABLE_V201 + +#include + +namespace MicroOcpp { +namespace Ocpp201 { + +ChangeAvailability::ChangeAvailability(AvailabilityService& availabilityService) : MemoryManaged("v201.Operation.", "ChangeAvailability"), availabilityService(availabilityService) { + +} + +const char* ChangeAvailability::getOperationType(){ + return "ChangeAvailability"; +} + +void ChangeAvailability::processReq(JsonObject payload) { + + unsigned int evseId = 0; + + if (payload.containsKey("evse")) { + int evseIdRaw = payload["evse"]["id"] | -1; + if (evseIdRaw < 0) { + errorCode = "FormationViolation"; + return; + } + evseId = (unsigned int)evseIdRaw; + + if ((payload["evse"]["connectorId"] | 1) != 1) { + errorCode = "PropertyConstraintViolation"; + return; + } + } + + auto availabilityEvse = availabilityService.getEvse(evseId); + if (!availabilityEvse) { + errorCode = "PropertyConstraintViolation"; + return; + } + + const char *type = payload["operationalStatus"] | "_Undefined"; + + bool operative = false; + + if (!strcmp(type, "Operative")) { + operative = true; + } else if (!strcmp(type, "Inoperative")) { + operative = false; + } else { + errorCode = "PropertyConstraintViolation"; + return; + } + + status = availabilityEvse->changeAvailability(operative); +} + +std::unique_ptr ChangeAvailability::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + + switch (status) { + case ChangeAvailabilityStatus::Accepted: + payload["status"] = "Accepted"; + break; + case ChangeAvailabilityStatus::Scheduled: + payload["status"] = "Scheduled"; + break; + case ChangeAvailabilityStatus::Rejected: + payload["status"] = "Rejected"; + break; + } + + return doc; +} + +} // namespace Ocpp201 +} // namespace MicroOcpp + +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/ChangeAvailability.h b/src/MicroOcpp/Operations/ChangeAvailability.h index 00fd2b7a..08914e52 100644 --- a/src/MicroOcpp/Operations/ChangeAvailability.h +++ b/src/MicroOcpp/Operations/ChangeAvailability.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef CHANGEAVAILABILITY_H -#define CHANGEAVAILABILITY_H +#ifndef MO_CHANGEAVAILABILITY_H +#define MO_CHANGEAVAILABILITY_H #include @@ -13,7 +13,7 @@ class Model; namespace Ocpp16 { -class ChangeAvailability : public Operation { +class ChangeAvailability : public Operation, public MemoryManaged { private: Model& model; bool scheduled = false; @@ -27,11 +27,46 @@ class ChangeAvailability : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp + +#if MO_ENABLE_V201 + +#include +#include + +namespace MicroOcpp { + +class AvailabilityService; + +namespace Ocpp201 { + +class ChangeAvailability : public Operation, public MemoryManaged { +private: + AvailabilityService& availabilityService; + ChangeAvailabilityStatus status = ChangeAvailabilityStatus::Rejected; + + const char *errorCode {nullptr}; +public: + ChangeAvailability(AvailabilityService& availabilityService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_V201 + #endif diff --git a/src/MicroOcpp/Operations/ChangeConfiguration.cpp b/src/MicroOcpp/Operations/ChangeConfiguration.cpp index 57d84dbf..79abaf29 100644 --- a/src/MicroOcpp/Operations/ChangeConfiguration.cpp +++ b/src/MicroOcpp/Operations/ChangeConfiguration.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -9,8 +9,9 @@ #include //for tolower using MicroOcpp::Ocpp16::ChangeConfiguration; +using MicroOcpp::JsonDoc; -ChangeConfiguration::ChangeConfiguration() { +ChangeConfiguration::ChangeConfiguration() : MemoryManaged("v16.Operation.", "ChangeConfiguration") { } @@ -100,8 +101,6 @@ void ChangeConfiguration::processReq(JsonObject payload) { numBool = true; } else if (tolower(value[0]) == 'f' && tolower(value[1]) == 'a' && tolower(value[2]) == 'l' && tolower(value[3]) == 's' && tolower(value[4]) == 'e' && !value[5]) { numBool = false; - } else if (convertibleInt) { - numBool = numInt != 0; } else { convertibleBool = false; } @@ -141,8 +140,8 @@ void ChangeConfiguration::processReq(JsonObject payload) { } } -std::unique_ptr ChangeConfiguration::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr ChangeConfiguration::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (notSupported) { payload["status"] = "NotSupported"; diff --git a/src/MicroOcpp/Operations/ChangeConfiguration.h b/src/MicroOcpp/Operations/ChangeConfiguration.h index 55cb6b47..3699cf6d 100644 --- a/src/MicroOcpp/Operations/ChangeConfiguration.h +++ b/src/MicroOcpp/Operations/ChangeConfiguration.h @@ -1,16 +1,16 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef CHANGECONFIGURATION_H -#define CHANGECONFIGURATION_H +#ifndef MO_CHANGECONFIGURATION_H +#define MO_CHANGECONFIGURATION_H #include namespace MicroOcpp { namespace Ocpp16 { -class ChangeConfiguration : public Operation { +class ChangeConfiguration : public Operation, public MemoryManaged { private: bool reject = false; bool rebootRequired = false; @@ -25,7 +25,7 @@ class ChangeConfiguration : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} diff --git a/src/MicroOcpp/Operations/CiStrings.h b/src/MicroOcpp/Operations/CiStrings.h index a963fde6..d52e176c 100644 --- a/src/MicroOcpp/Operations/CiStrings.h +++ b/src/MicroOcpp/Operations/CiStrings.h @@ -1,13 +1,13 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License /* * A collection of the fixed-length string types in the OCPP specification */ -#ifndef CI_STRINGS_H -#define CI_STRINGS_H +#ifndef MO_CI_STRINGS_H +#define MO_CI_STRINGS_H #define CiString20TypeLen 20 #define CiString25TypeLen 25 diff --git a/src/MicroOcpp/Operations/ClearCache.cpp b/src/MicroOcpp/Operations/ClearCache.cpp index d6afc1b8..6666ae51 100644 --- a/src/MicroOcpp/Operations/ClearCache.cpp +++ b/src/MicroOcpp/Operations/ClearCache.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -7,8 +7,9 @@ #include using MicroOcpp::Ocpp16::ClearCache; +using MicroOcpp::JsonDoc; -ClearCache::ClearCache(std::shared_ptr filesystem) : filesystem(filesystem) { +ClearCache::ClearCache(std::shared_ptr filesystem) : MemoryManaged("v16.Operation.", "ClearCache"), filesystem(filesystem) { } @@ -31,8 +32,8 @@ void ClearCache::processReq(JsonObject payload) { }); } -std::unique_ptr ClearCache::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr ClearCache::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (success) { payload["status"] = "Accepted"; //"Accepted", because the intended postcondition is true diff --git a/src/MicroOcpp/Operations/ClearCache.h b/src/MicroOcpp/Operations/ClearCache.h index e49a6969..110ab14a 100644 --- a/src/MicroOcpp/Operations/ClearCache.h +++ b/src/MicroOcpp/Operations/ClearCache.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef CLEARCACHE_H -#define CLEARCACHE_H +#ifndef MO_CLEARCACHE_H +#define MO_CLEARCACHE_H #include #include @@ -11,7 +11,7 @@ namespace MicroOcpp { namespace Ocpp16 { -class ClearCache : public Operation { +class ClearCache : public Operation, public MemoryManaged { private: std::shared_ptr filesystem; bool success = true; @@ -22,7 +22,7 @@ class ClearCache : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; }; } //end namespace Ocpp16 diff --git a/src/MicroOcpp/Operations/ClearChargingProfile.cpp b/src/MicroOcpp/Operations/ClearChargingProfile.cpp index cd6f5bb5..e99f9bd7 100644 --- a/src/MicroOcpp/Operations/ClearChargingProfile.cpp +++ b/src/MicroOcpp/Operations/ClearChargingProfile.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -9,8 +9,9 @@ #include using MicroOcpp::Ocpp16::ClearChargingProfile; +using MicroOcpp::JsonDoc; -ClearChargingProfile::ClearChargingProfile(SmartChargingService& scService) : scService(scService) { +ClearChargingProfile::ClearChargingProfile(SmartChargingService& scService) : MemoryManaged("v16.Operation.", "ClearChargingProfile"), scService(scService) { } @@ -69,8 +70,8 @@ void ClearChargingProfile::processReq(JsonObject payload) { matchingProfilesFound = scService.clearChargingProfile(filter); } -std::unique_ptr ClearChargingProfile::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr ClearChargingProfile::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (matchingProfilesFound) payload["status"] = "Accepted"; diff --git a/src/MicroOcpp/Operations/ClearChargingProfile.h b/src/MicroOcpp/Operations/ClearChargingProfile.h index e7befe8a..40b7d210 100644 --- a/src/MicroOcpp/Operations/ClearChargingProfile.h +++ b/src/MicroOcpp/Operations/ClearChargingProfile.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef CLEARCHARGINGPROFILE_H -#define CLEARCHARGINGPROFILE_H +#ifndef MO_CLEARCHARGINGPROFILE_H +#define MO_CLEARCHARGINGPROFILE_H #include @@ -13,7 +13,7 @@ class SmartChargingService; namespace Ocpp16 { -class ClearChargingProfile : public Operation { +class ClearChargingProfile : public Operation, public MemoryManaged { private: SmartChargingService& scService; bool matchingProfilesFound = false; @@ -24,7 +24,7 @@ class ClearChargingProfile : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; }; diff --git a/src/MicroOcpp/Operations/CustomOperation.cpp b/src/MicroOcpp/Operations/CustomOperation.cpp index 59eef187..317d01ef 100644 --- a/src/MicroOcpp/Operations/CustomOperation.cpp +++ b/src/MicroOcpp/Operations/CustomOperation.cpp @@ -1,20 +1,18 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include using MicroOcpp::Ocpp16::CustomOperation; +using MicroOcpp::JsonDoc; CustomOperation::CustomOperation(const char *operationType, - std::function ()> fn_createReq, + std::function ()> fn_createReq, std::function fn_processConf, - std::function fn_processErr, - std::function fn_initiate, - std::function fn_restore) : - operationType{operationType}, - fn_initiate{fn_initiate}, - fn_restore{fn_restore}, + std::function fn_processErr) : + MemoryManaged("Operation.Custom.", operationType), + operationType{makeString(getMemoryTag(), operationType)}, fn_createReq{fn_createReq}, fn_processConf{fn_processConf}, fn_processErr{fn_processErr} { @@ -23,11 +21,12 @@ CustomOperation::CustomOperation(const char *operationType, CustomOperation::CustomOperation(const char *operationType, std::function fn_processReq, - std::function ()> fn_createConf, + std::function ()> fn_createConf, std::function fn_getErrorCode, std::function fn_getErrorDescription, - std::function ()> fn_getErrorDetails) : - operationType{operationType}, + std::function ()> fn_getErrorDetails) : + MemoryManaged("Operation.Custom.", operationType), + operationType{makeString(getMemoryTag(), operationType)}, fn_processReq{fn_processReq}, fn_createConf{fn_createConf}, fn_getErrorCode{fn_getErrorCode}, @@ -44,21 +43,7 @@ const char* CustomOperation::getOperationType() { return operationType.c_str(); } -void CustomOperation::initiate(StoredOperationHandler *opStore) { - if (fn_initiate) { - fn_initiate(opStore); - } -} - -bool CustomOperation::restore(StoredOperationHandler *opStore) { - if (fn_restore) { - return fn_restore(opStore); - } else { - return false; - } -} - -std::unique_ptr CustomOperation::createReq() { +std::unique_ptr CustomOperation::createReq() { return fn_createReq(); } @@ -77,7 +62,7 @@ void CustomOperation::processReq(JsonObject payload) { return fn_processReq(payload); } -std::unique_ptr CustomOperation::createConf() { +std::unique_ptr CustomOperation::createConf() { return fn_createConf(); } @@ -97,7 +82,7 @@ const char *CustomOperation::getErrorDescription() { } } -std::unique_ptr CustomOperation::getErrorDetails() { +std::unique_ptr CustomOperation::getErrorDetails() { if (fn_getErrorDetails) { return fn_getErrorDetails(); } else { diff --git a/src/MicroOcpp/Operations/CustomOperation.h b/src/MicroOcpp/Operations/CustomOperation.h index 4fe07dcf..02f72e88 100644 --- a/src/MicroOcpp/Operations/CustomOperation.h +++ b/src/MicroOcpp/Operations/CustomOperation.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_CUSTOMOPERATION_H @@ -13,46 +13,38 @@ namespace MicroOcpp { namespace Ocpp16 { -class CustomOperation : public Operation { +class CustomOperation : public Operation, public MemoryManaged { private: - std::string operationType; - std::function fn_initiate; //optional - std::function fn_restore; //optional - std::function ()> fn_createReq; + 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_createConf; std::function fn_getErrorCode; //optional std::function fn_getErrorDescription; //optional - std::function ()> fn_getErrorDetails; //optional + std::function ()> fn_getErrorDetails; //optional public: //for operations initiated at this device CustomOperation(const char *operationType, - std::function ()> fn_createReq, + std::function ()> fn_createReq, std::function fn_processConf, - std::function fn_processErr = nullptr, - std::function fn_initiate = nullptr, - std::function fn_restore = nullptr); + std::function fn_processErr = nullptr); //for operations receied from remote CustomOperation(const char *operationType, std::function fn_processReq, - std::function ()> fn_createConf, + std::function ()> fn_createConf, std::function fn_getErrorCode = nullptr, std::function fn_getErrorDescription = nullptr, - std::function ()> fn_getErrorDetails = nullptr); + std::function ()> fn_getErrorDetails = nullptr); ~CustomOperation(); const char* getOperationType() override; - void initiate(StoredOperationHandler *opStore) override; - - bool restore(StoredOperationHandler *opStore) override; - - std::unique_ptr createReq() override; + std::unique_ptr createReq() override; void processConf(JsonObject payload) override; @@ -60,10 +52,10 @@ class CustomOperation : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override; const char *getErrorDescription() override; - std::unique_ptr getErrorDetails() override; + std::unique_ptr getErrorDetails() override; }; } //end namespace Ocpp16 diff --git a/src/MicroOcpp/Operations/DataTransfer.cpp b/src/MicroOcpp/Operations/DataTransfer.cpp index a7f4af1c..5c605039 100644 --- a/src/MicroOcpp/Operations/DataTransfer.cpp +++ b/src/MicroOcpp/Operations/DataTransfer.cpp @@ -1,22 +1,27 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include using MicroOcpp::Ocpp16::DataTransfer; +using MicroOcpp::JsonDoc; + +DataTransfer::DataTransfer() : MemoryManaged("v16.Operation.", "DataTransfer") { + +} + +DataTransfer::DataTransfer(const String &msg) : MemoryManaged("v16.Operation.", "DataTransfer"), msg{makeString(getMemoryTag(), msg.c_str())} { -DataTransfer::DataTransfer(const std::string &msg) { - this->msg = msg; } const char* DataTransfer::getOperationType(){ return "DataTransfer"; } -std::unique_ptr DataTransfer::createReq() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2) + (msg.length() + 1))); +std::unique_ptr DataTransfer::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2) + (msg.length() + 1)); JsonObject payload = doc->to(); payload["vendorId"] = "CustomVendor"; payload["data"] = msg; @@ -24,11 +29,22 @@ std::unique_ptr DataTransfer::createReq() { } void DataTransfer::processConf(JsonObject payload){ - std::string status = payload["status"] | "Invalid"; + const char *status = payload["status"] | "Invalid"; - if (status == "Accepted") { + if (!strcmp(status, "Accepted")) { MO_DBG_DEBUG("Request has been accepted"); } else { MO_DBG_INFO("Request has been denied"); } } + +void DataTransfer::processReq(JsonObject payload) { + // Do nothing - we're just required to reject these DataTransfer requests +} + +std::unique_ptr DataTransfer::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + payload["status"] = "Rejected"; + return doc; +} diff --git a/src/MicroOcpp/Operations/DataTransfer.h b/src/MicroOcpp/Operations/DataTransfer.h index c8faa586..f7967202 100644 --- a/src/MicroOcpp/Operations/DataTransfer.h +++ b/src/MicroOcpp/Operations/DataTransfer.h @@ -1,26 +1,31 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef DATATRANSFER_H -#define DATATRANSFER_H +#ifndef MO_DATATRANSFER_H +#define MO_DATATRANSFER_H #include namespace MicroOcpp { namespace Ocpp16 { -class DataTransfer : public Operation { +class DataTransfer : public Operation, public MemoryManaged { private: - std::string msg {}; + String msg; public: - DataTransfer(const std::string &msg); + DataTransfer(); + DataTransfer(const String &msg); const char* getOperationType() override; - std::unique_ptr createReq() override; + std::unique_ptr createReq() override; void processConf(JsonObject payload) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; }; diff --git a/src/MicroOcpp/Operations/DeleteCertificate.cpp b/src/MicroOcpp/Operations/DeleteCertificate.cpp new file mode 100644 index 00000000..b7a7267a --- /dev/null +++ b/src/MicroOcpp/Operations/DeleteCertificate.cpp @@ -0,0 +1,96 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_CERT_MGMT + +#include +#include +#include + +using MicroOcpp::Ocpp201::DeleteCertificate; +using MicroOcpp::JsonDoc; + +DeleteCertificate::DeleteCertificate(CertificateService& certService) : MemoryManaged("v201.Operation.", "DeleteCertificate"), certService(certService) { + +} + +void DeleteCertificate::processReq(JsonObject payload) { + + JsonObject certIdJson = payload["certificateHashData"]; + + if (!certIdJson.containsKey("hashAlgorithm") || + !certIdJson.containsKey("issuerNameHash") || + !certIdJson.containsKey("issuerKeyHash") || + !certIdJson.containsKey("serialNumber")) { + errorCode = "FormationViolation"; + return; + } + + const char *hashAlgorithm = certIdJson["hashAlgorithm"] | "_Invalid"; + + if (!certIdJson["issuerNameHash"].is() || + !certIdJson["issuerKeyHash"].is() || + !certIdJson["serialNumber"].is()) { + errorCode = "FormationViolation"; + return; + } + + CertificateHash cert; + + if (!strcmp(hashAlgorithm, "SHA256")) { + cert.hashAlgorithm = HashAlgorithmType_SHA256; + } else if (!strcmp(hashAlgorithm, "SHA384")) { + cert.hashAlgorithm = HashAlgorithmType_SHA384; + } else if (!strcmp(hashAlgorithm, "SHA512")) { + cert.hashAlgorithm = HashAlgorithmType_SHA512; + } else { + errorCode = "FormationViolation"; + return; + } + + auto retIN = ocpp_cert_set_issuerNameHash(&cert, certIdJson["issuerNameHash"] | "_Invalid", cert.hashAlgorithm); + auto retIK = ocpp_cert_set_issuerKeyHash(&cert, certIdJson["issuerKeyHash"] | "_Invalid", cert.hashAlgorithm); + auto retSN = ocpp_cert_set_serialNumber(&cert, certIdJson["serialNumber"] | "_Invalid"); + if (retIN < 0 || retIK < 0 || retSN < 0) { + errorCode = "FormationViolation"; + return; + } + + auto certStore = certService.getCertificateStore(); + if (!certStore) { + errorCode = "NotSupported"; + return; + } + + auto status = certStore->deleteCertificate(cert); + + switch (status) { + case DeleteCertificateStatus_Accepted: + this->status = "Accepted"; + break; + case DeleteCertificateStatus_Failed: + this->status = "Failed"; + break; + case DeleteCertificateStatus_NotFound: + this->status = "NotFound"; + break; + default: + MO_DBG_ERR("internal error"); + errorCode = "InternalError"; + return; + } + + //operation executed successfully +} + +std::unique_ptr DeleteCertificate::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + payload["status"] = status; + return doc; +} + +#endif //MO_ENABLE_CERT_MGMT diff --git a/src/MicroOcpp/Operations/DeleteCertificate.h b/src/MicroOcpp/Operations/DeleteCertificate.h new file mode 100644 index 00000000..4c07014b --- /dev/null +++ b/src/MicroOcpp/Operations/DeleteCertificate.h @@ -0,0 +1,41 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_DELETECERTIFICATE_H +#define MO_DELETECERTIFICATE_H + +#include + +#if MO_ENABLE_CERT_MGMT + +#include + +namespace MicroOcpp { + +class CertificateService; + +namespace Ocpp201 { + +class DeleteCertificate : public Operation, public MemoryManaged { +private: + CertificateService& certService; + const char *status = nullptr; + const char *errorCode = nullptr; +public: + DeleteCertificate(CertificateService& certService); + + const char* getOperationType() override {return "DeleteCertificate";} + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_CERT_MGMT +#endif diff --git a/src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp b/src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp index c220b6fa..8fc99913 100644 --- a/src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp +++ b/src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -8,8 +8,9 @@ #include using MicroOcpp::Ocpp16::DiagnosticsStatusNotification; +using MicroOcpp::JsonDoc; -DiagnosticsStatusNotification::DiagnosticsStatusNotification(DiagnosticsStatus status) : status(status) { +DiagnosticsStatusNotification::DiagnosticsStatusNotification(DiagnosticsStatus status) : MemoryManaged("v16.Operation.", "DiagnosticsStatusNotification"), status(status) { } @@ -31,8 +32,8 @@ const char *DiagnosticsStatusNotification::cstrFromStatus(DiagnosticsStatus stat return nullptr; //cannot be reached } -std::unique_ptr DiagnosticsStatusNotification::createReq() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr DiagnosticsStatusNotification::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = cstrFromStatus(status); return doc; diff --git a/src/MicroOcpp/Operations/DiagnosticsStatusNotification.h b/src/MicroOcpp/Operations/DiagnosticsStatusNotification.h index 7dcd0b3b..e21c8c07 100644 --- a/src/MicroOcpp/Operations/DiagnosticsStatusNotification.h +++ b/src/MicroOcpp/Operations/DiagnosticsStatusNotification.h @@ -1,17 +1,17 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include -#ifndef DIAGNOSTICSSTATUSNOTIFICATION_H -#define DIAGNOSTICSSTATUSNOTIFICATION_H +#ifndef MO_DIAGNOSTICSSTATUSNOTIFICATION_H +#define MO_DIAGNOSTICSSTATUSNOTIFICATION_H namespace MicroOcpp { namespace Ocpp16 { -class DiagnosticsStatusNotification : public Operation { +class DiagnosticsStatusNotification : public Operation, public MemoryManaged { private: DiagnosticsStatus status = DiagnosticsStatus::Idle; static const char *cstrFromStatus(DiagnosticsStatus status); @@ -20,7 +20,7 @@ class DiagnosticsStatusNotification : public Operation { const char* getOperationType() override {return "DiagnosticsStatusNotification"; } - std::unique_ptr createReq() override; + std::unique_ptr createReq() override; void processConf(JsonObject payload) override; diff --git a/src/MicroOcpp/Operations/FirmwareStatusNotification.cpp b/src/MicroOcpp/Operations/FirmwareStatusNotification.cpp index 20d28a60..0b52c56f 100644 --- a/src/MicroOcpp/Operations/FirmwareStatusNotification.cpp +++ b/src/MicroOcpp/Operations/FirmwareStatusNotification.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -8,8 +8,9 @@ #include using MicroOcpp::Ocpp16::FirmwareStatusNotification; +using MicroOcpp::JsonDoc; -FirmwareStatusNotification::FirmwareStatusNotification(FirmwareStatus status) : status{status} { +FirmwareStatusNotification::FirmwareStatusNotification(FirmwareStatus status) : MemoryManaged("v16.Operation.", "FirmwareStatusNotification"), status{status} { } @@ -40,8 +41,8 @@ const char *FirmwareStatusNotification::cstrFromFwStatus(FirmwareStatus status) return NULL; //cannot be reached } -std::unique_ptr FirmwareStatusNotification::createReq() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr FirmwareStatusNotification::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = cstrFromFwStatus(status); return doc; diff --git a/src/MicroOcpp/Operations/FirmwareStatusNotification.h b/src/MicroOcpp/Operations/FirmwareStatusNotification.h index 3e0feb97..026ea45b 100644 --- a/src/MicroOcpp/Operations/FirmwareStatusNotification.h +++ b/src/MicroOcpp/Operations/FirmwareStatusNotification.h @@ -1,18 +1,18 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include -#ifndef FIRMWARESTATUSNOTIFICATION_H -#define FIRMWARESTATUSNOTIFICATION_H +#ifndef MO_FIRMWARESTATUSNOTIFICATION_H +#define MO_FIRMWARESTATUSNOTIFICATION_H namespace MicroOcpp { namespace Ocpp16 { -class FirmwareStatusNotification : public Operation { +class FirmwareStatusNotification : public Operation, public MemoryManaged { private: FirmwareStatus status = FirmwareStatus::Idle; static const char *cstrFromFwStatus(FirmwareStatus status); @@ -21,7 +21,7 @@ class FirmwareStatusNotification : public Operation { const char* getOperationType() override {return "FirmwareStatusNotification"; } - std::unique_ptr createReq() override; + std::unique_ptr createReq() override; void processConf(JsonObject payload) override; diff --git a/src/MicroOcpp/Operations/GetBaseReport.cpp b/src/MicroOcpp/Operations/GetBaseReport.cpp new file mode 100644 index 00000000..95414481 --- /dev/null +++ b/src/MicroOcpp/Operations/GetBaseReport.cpp @@ -0,0 +1,81 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include + +using MicroOcpp::Ocpp201::GetBaseReport; +using MicroOcpp::JsonDoc; + +GetBaseReport::GetBaseReport(VariableService& variableService) : MemoryManaged("v201.Operation.", "GetBaseReport"), variableService(variableService) { + +} + +const char* GetBaseReport::getOperationType(){ + return "GetBaseReport"; +} + +void GetBaseReport::processReq(JsonObject payload) { + + int requestId = payload["requestId"] | -1; + if (requestId < 0) { + errorCode = "FormationViolation"; + MO_DBG_ERR("invalid requestId"); + return; + } + + ReportBase reportBase; + + const char *reportBaseCstr = payload["reportBase"] | ""; + if (!strcmp(reportBaseCstr, "ConfigurationInventory")) { + reportBase = ReportBase_ConfigurationInventory; + } else if (!strcmp(reportBaseCstr, "FullInventory")) { + reportBase = ReportBase_FullInventory; + } else if (!strcmp(reportBaseCstr, "SummaryInventory")) { + reportBase = ReportBase_SummaryInventory; + } else { + errorCode = "FormationViolation"; + MO_DBG_ERR("invalid reportBase"); + return; + } + + status = variableService.getBaseReport(requestId, reportBase); +} + +std::unique_ptr GetBaseReport::createConf(){ + + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + + const char *statusCstr = ""; + + switch (status) { + case GenericDeviceModelStatus_Accepted: + statusCstr = "Accepted"; + break; + case GenericDeviceModelStatus_Rejected: + statusCstr = "Rejected"; + break; + case GenericDeviceModelStatus_NotSupported: + statusCstr = "NotSupported"; + break; + case GenericDeviceModelStatus_EmptyResultSet: + statusCstr = "EmptyResultSet"; + break; + default: + MO_DBG_ERR("internal error"); + break; + } + + payload["status"] = statusCstr; + + return doc; +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/GetBaseReport.h b/src/MicroOcpp/Operations/GetBaseReport.h new file mode 100644 index 00000000..f23b7e81 --- /dev/null +++ b/src/MicroOcpp/Operations/GetBaseReport.h @@ -0,0 +1,47 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_GETBASEREPORT_H +#define MO_GETBASEREPORT_H + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include + +namespace MicroOcpp { + +class VariableService; + +namespace Ocpp201 { + +class GetBaseReport : public Operation, public MemoryManaged { +private: + VariableService& variableService; + + GenericDeviceModelStatus status; + + const char *errorCode = nullptr; +public: + GetBaseReport(VariableService& variableService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} + +}; + +} //namespace Ocpp201 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Operations/GetCompositeSchedule.cpp b/src/MicroOcpp/Operations/GetCompositeSchedule.cpp index 40a314ed..760e0e2f 100644 --- a/src/MicroOcpp/Operations/GetCompositeSchedule.cpp +++ b/src/MicroOcpp/Operations/GetCompositeSchedule.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -8,8 +8,9 @@ #include using MicroOcpp::Ocpp16::GetCompositeSchedule; +using MicroOcpp::JsonDoc; -GetCompositeSchedule::GetCompositeSchedule(Model& model, SmartChargingService& scService) : model(model), scService(scService) { +GetCompositeSchedule::GetCompositeSchedule(Model& model, SmartChargingService& scService) : MemoryManaged("v16.Operation.", "GetCompositeSchedule"), model(model), scService(scService) { } @@ -40,12 +41,12 @@ void GetCompositeSchedule::processReq(JsonObject payload) { } } -std::unique_ptr GetCompositeSchedule::createConf(){ +std::unique_ptr GetCompositeSchedule::createConf(){ bool success = false; auto chargingSchedule = scService.getCompositeSchedule((unsigned int) connectorId, duration, chargingRateUnit); - DynamicJsonDocument chargingScheduleDoc {0}; + JsonDoc chargingScheduleDoc {0}; if (chargingSchedule) { success = chargingSchedule->toJson(chargingScheduleDoc); @@ -58,9 +59,9 @@ std::unique_ptr GetCompositeSchedule::createConf(){ } if (success && chargingSchedule) { - auto doc = std::unique_ptr(new DynamicJsonDocument( + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(4) + - chargingScheduleDoc.memoryUsage())); + chargingScheduleDoc.memoryUsage()); JsonObject payload = doc->to(); payload["status"] = "Accepted"; payload["connectorId"] = connectorId; @@ -68,7 +69,7 @@ std::unique_ptr GetCompositeSchedule::createConf(){ payload["chargingSchedule"] = chargingScheduleDoc; return doc; } else { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = "Rejected"; return doc; diff --git a/src/MicroOcpp/Operations/GetCompositeSchedule.h b/src/MicroOcpp/Operations/GetCompositeSchedule.h index 6e1d0fae..30e427c4 100644 --- a/src/MicroOcpp/Operations/GetCompositeSchedule.h +++ b/src/MicroOcpp/Operations/GetCompositeSchedule.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef GETCOMPOSITESCHEDULE_H -#define GETCOMPOSITESCHEDULE_H +#ifndef MO_GETCOMPOSITESCHEDULE_H +#define MO_GETCOMPOSITESCHEDULE_H #include #include @@ -15,7 +15,7 @@ class Model; namespace Ocpp16 { -class GetCompositeSchedule : public Operation { +class GetCompositeSchedule : public Operation, public MemoryManaged { private: Model& model; SmartChargingService& scService; @@ -31,7 +31,7 @@ class GetCompositeSchedule : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; diff --git a/src/MicroOcpp/Operations/GetConfiguration.cpp b/src/MicroOcpp/Operations/GetConfiguration.cpp index a8b3804c..8146bba1 100644 --- a/src/MicroOcpp/Operations/GetConfiguration.cpp +++ b/src/MicroOcpp/Operations/GetConfiguration.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -7,8 +7,9 @@ #include using MicroOcpp::Ocpp16::GetConfiguration; +using MicroOcpp::JsonDoc; -GetConfiguration::GetConfiguration() { +GetConfiguration::GetConfiguration() : MemoryManaged("v16.Operation.", "GetConfiguration"), keys{makeVector(getMemoryTag())} { } @@ -20,14 +21,14 @@ void GetConfiguration::processReq(JsonObject payload) { JsonArray requestedKeys = payload["key"]; for (size_t i = 0; i < requestedKeys.size(); i++) { - keys.push_back(requestedKeys[i].as()); + keys.push_back(makeString(getMemoryTag(), requestedKeys[i].as())); } } -std::unique_ptr GetConfiguration::createConf(){ +std::unique_ptr GetConfiguration::createConf(){ - std::vector configurations; - std::vector unknownKeys; + Vector configurations = makeVector(getMemoryTag()); + Vector unknownKeys = makeVector(getMemoryTag()); auto containers = getConfigurationContainersPublic(); @@ -39,6 +40,9 @@ std::unique_ptr GetConfiguration::createConf(){ MO_DBG_ERR("invalid config"); continue; } + if (!container->getConfiguration(i)->isReadable()) { + continue; + } configurations.push_back(container->getConfiguration(i)); } } @@ -52,7 +56,7 @@ std::unique_ptr GetConfiguration::createConf(){ } } - if (res) { + if (res && res->isReadable()) { configurations.push_back(res); } else { unknownKeys.push_back(key.c_str()); @@ -82,14 +86,16 @@ std::unique_ptr GetConfiguration::createConf(){ MO_DBG_DEBUG("GetConfiguration capacity: %zu", jcapacity); - std::unique_ptr doc; + std::unique_ptr doc; if (jcapacity <= MO_MAX_JSON_CAPACITY) { - doc = std::unique_ptr(new DynamicJsonDocument(jcapacity)); + doc = makeJsonDoc(getMemoryTag(), jcapacity); } if (!doc || doc->capacity() < jcapacity) { - if (doc) {MO_DBG_ERR("OOM");(void)0;} + if (doc) { + MO_DBG_ERR("OOM"); + } errorCode = "InternalError"; errorDescription = "Query too big. Try fewer keys"; @@ -135,7 +141,7 @@ std::unique_ptr GetConfiguration::createConf(){ if (!unknownKeys.empty()) { JsonArray jsonUnknownKey = payload.createNestedArray("unknownKey"); for (auto key : unknownKeys) { - MO_DBG_DEBUG("Unknown key: %s", key) + MO_DBG_DEBUG("Unknown key: %s", key); jsonUnknownKey.add(key); } } diff --git a/src/MicroOcpp/Operations/GetConfiguration.h b/src/MicroOcpp/Operations/GetConfiguration.h index 47da8519..c6781119 100644 --- a/src/MicroOcpp/Operations/GetConfiguration.h +++ b/src/MicroOcpp/Operations/GetConfiguration.h @@ -1,20 +1,19 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef GETCONFIGURATION_H -#define GETCONFIGURATION_H +#ifndef MO_GETCONFIGURATION_H +#define MO_GETCONFIGURATION_H #include - -#include +#include namespace MicroOcpp { namespace Ocpp16 { -class GetConfiguration : public Operation { +class GetConfiguration : public Operation, public MemoryManaged { private: - std::vector keys; + Vector keys; const char *errorCode {nullptr}; const char *errorDescription = ""; @@ -25,7 +24,7 @@ class GetConfiguration : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} const char *getErrorDescription() override {return errorDescription;} diff --git a/src/MicroOcpp/Operations/GetDiagnostics.cpp b/src/MicroOcpp/Operations/GetDiagnostics.cpp index 93a76d83..e3bab7eb 100644 --- a/src/MicroOcpp/Operations/GetDiagnostics.cpp +++ b/src/MicroOcpp/Operations/GetDiagnostics.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -8,8 +8,9 @@ #include using MicroOcpp::Ocpp16::GetDiagnostics; +using MicroOcpp::JsonDoc; -GetDiagnostics::GetDiagnostics(DiagnosticsService& diagService) : diagService(diagService) { +GetDiagnostics::GetDiagnostics(DiagnosticsService& diagService) : MemoryManaged("v16.Operation.", "GetDiagnostics"), diagService(diagService), fileName(makeString(getMemoryTag())) { } @@ -39,7 +40,7 @@ void GetDiagnostics::processReq(JsonObject payload) { } Timestamp stopTime; - if (payload.containsKey("startTime")) { + if (payload.containsKey("stopTime")) { if (!stopTime.setTime(payload["stopTime"] | "Invalid")) { errorCode = "PropertyConstraintViolation"; MO_DBG_WARN("bad time format"); @@ -50,13 +51,13 @@ void GetDiagnostics::processReq(JsonObject payload) { fileName = diagService.requestDiagnosticsUpload(location, (unsigned int) retries, (unsigned int) retryInterval, startTime, stopTime); } -std::unique_ptr GetDiagnostics::createConf(){ +std::unique_ptr GetDiagnostics::createConf(){ if (fileName.empty()) { return createEmptyDocument(); } else { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1) + fileName.length() + 1)); + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); - payload["fileName"] = fileName; + payload["fileName"] = fileName.c_str(); return doc; } } diff --git a/src/MicroOcpp/Operations/GetDiagnostics.h b/src/MicroOcpp/Operations/GetDiagnostics.h index 22c14983..704f9199 100644 --- a/src/MicroOcpp/Operations/GetDiagnostics.h +++ b/src/MicroOcpp/Operations/GetDiagnostics.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef GETDIAGNOSTICS_H -#define GETDIAGNOSTICS_H +#ifndef MO_GETDIAGNOSTICS_H +#define MO_GETDIAGNOSTICS_H #include #include @@ -14,10 +14,10 @@ class DiagnosticsService; namespace Ocpp16 { -class GetDiagnostics : public Operation { +class GetDiagnostics : public Operation, public MemoryManaged { private: DiagnosticsService& diagService; - std::string fileName; + String fileName; const char *errorCode = nullptr; public: @@ -27,7 +27,7 @@ class GetDiagnostics : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; diff --git a/src/MicroOcpp/Operations/GetInstalledCertificateIds.cpp b/src/MicroOcpp/Operations/GetInstalledCertificateIds.cpp new file mode 100644 index 00000000..87d4c2dd --- /dev/null +++ b/src/MicroOcpp/Operations/GetInstalledCertificateIds.cpp @@ -0,0 +1,131 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_CERT_MGMT + +#include +#include +#include + +using MicroOcpp::Ocpp201::GetInstalledCertificateIds; +using MicroOcpp::JsonDoc; + +GetInstalledCertificateIds::GetInstalledCertificateIds(CertificateService& certService) : MemoryManaged("v201.Operation.", "GetInstalledCertificateIds"), certService(certService), certificateHashDataChain(makeVector(getMemoryTag())) { + +} + +void GetInstalledCertificateIds::processReq(JsonObject payload) { + + if (!payload.containsKey("certificateType")) { + errorCode = "FormationViolation"; + return; + } + + auto certificateType = makeVector(getMemoryTag()); + for (const char *certificateTypeCstr : payload["certificateType"].as()) { + if (!strcmp(certificateTypeCstr, "V2GRootCertificate")) { + certificateType.push_back(GetCertificateIdType_V2GRootCertificate); + } else if (!strcmp(certificateTypeCstr, "MORootCertificate")) { + certificateType.push_back(GetCertificateIdType_MORootCertificate); + } else if (!strcmp(certificateTypeCstr, "CSMSRootCertificate")) { + certificateType.push_back(GetCertificateIdType_CSMSRootCertificate); + } else if (!strcmp(certificateTypeCstr, "V2GCertificateChain")) { + certificateType.push_back(GetCertificateIdType_V2GCertificateChain); + } else if (!strcmp(certificateTypeCstr, "ManufacturerRootCertificate")) { + certificateType.push_back(GetCertificateIdType_ManufacturerRootCertificate); + } else { + errorCode = "FormationViolation"; + return; + } + } + + auto certStore = certService.getCertificateStore(); + if (!certStore) { + errorCode = "NotSupported"; + return; + } + + auto status = certStore->getCertificateIds(certificateType, certificateHashDataChain); + + switch (status) { + case GetInstalledCertificateStatus_Accepted: + this->status = "Accepted"; + break; + case GetInstalledCertificateStatus_NotFound: + this->status = "NotFound"; + break; + default: + MO_DBG_ERR("internal error"); + errorCode = "InternalError"; + return; + } + + //operation executed successfully +} + +std::unique_ptr GetInstalledCertificateIds::createConf() { + + size_t capacity = + JSON_OBJECT_SIZE(2) + //payload root + JSON_ARRAY_SIZE(certificateHashDataChain.size()); //array for field certificateHashDataChain + for (auto& cch : certificateHashDataChain) { + capacity += + JSON_OBJECT_SIZE(2) + //certificateHashDataChain root + JSON_OBJECT_SIZE(4) + //certificateHashData + (2 * HashAlgorithmSize(cch.certificateHashData.hashAlgorithm) + //issuerNameHash and issuerKeyHash + cch.certificateHashData.serialNumberLen) + * 2 + 3; //issuerNameHash, issuerKeyHash and serialNumber as hex-endoded cstring + } + + auto doc = makeJsonDoc(getMemoryTag(), capacity); + JsonObject payload = doc->to(); + payload["status"] = status; + + for (auto& chainElem : certificateHashDataChain) { + JsonObject certHashJson = payload["certificateHashDataChain"].createNestedObject(); + + const char *certificateTypeCstr = ""; + switch (chainElem.certificateType) { + case GetCertificateIdType_V2GRootCertificate: + certificateTypeCstr = "V2GRootCertificate"; + break; + case GetCertificateIdType_MORootCertificate: + certificateTypeCstr = "MORootCertificate"; + break; + case GetCertificateIdType_CSMSRootCertificate: + certificateTypeCstr = "CSMSRootCertificate"; + break; + case GetCertificateIdType_V2GCertificateChain: + certificateTypeCstr = "V2GCertificateChain"; + break; + case GetCertificateIdType_ManufacturerRootCertificate: + certificateTypeCstr = "ManufacturerRootCertificate"; + break; + } + + certHashJson["certificateType"] = (const char*) certificateTypeCstr; //use JSON zero-copy mode + certHashJson["certificateHashData"]["hashAlgorithm"] = HashAlgorithmLabel(chainElem.certificateHashData.hashAlgorithm); + + char buf [MO_CERT_HASH_ISSUER_NAME_KEY_SIZE]; + + ocpp_cert_print_issuerNameHash(&chainElem.certificateHashData, buf, sizeof(buf)); + certHashJson["certificateHashData"]["issuerNameHash"] = buf; + + ocpp_cert_print_issuerKeyHash(&chainElem.certificateHashData, buf, sizeof(buf)); + certHashJson["certificateHashData"]["issuerKeyHash"] = buf; + + ocpp_cert_print_serialNumber(&chainElem.certificateHashData, buf, sizeof(buf)); + certHashJson["certificateHashData"]["serialNumber"] = buf; + + if (!chainElem.childCertificateHashData.empty()) { + MO_DBG_ERR("only sole root certs supported"); + } + } + + return doc; +} + +#endif //MO_ENABLE_CERT_MGMT diff --git a/src/MicroOcpp/Operations/GetInstalledCertificateIds.h b/src/MicroOcpp/Operations/GetInstalledCertificateIds.h new file mode 100644 index 00000000..28a31c92 --- /dev/null +++ b/src/MicroOcpp/Operations/GetInstalledCertificateIds.h @@ -0,0 +1,43 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_GETINSTALLEDCERTIFICATEIDS_H +#define MO_GETINSTALLEDCERTIFICATEIDS_H + +#include + +#if MO_ENABLE_CERT_MGMT + +#include +#include + +namespace MicroOcpp { + +class CertificateService; + +namespace Ocpp201 { + +class GetInstalledCertificateIds : public Operation, public MemoryManaged { +private: + CertificateService& certService; + Vector certificateHashDataChain; + const char *status = nullptr; + const char *errorCode = nullptr; +public: + GetInstalledCertificateIds(CertificateService& certService); + + const char* getOperationType() override {return "GetInstalledCertificateIds";} + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_CERT_MGMT +#endif diff --git a/src/MicroOcpp/Operations/GetLocalListVersion.cpp b/src/MicroOcpp/Operations/GetLocalListVersion.cpp index 7c0a2c67..4c2d4688 100644 --- a/src/MicroOcpp/Operations/GetLocalListVersion.cpp +++ b/src/MicroOcpp/Operations/GetLocalListVersion.cpp @@ -1,15 +1,20 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + +#if MO_ENABLE_LOCAL_AUTH + #include #include #include #include using MicroOcpp::Ocpp16::GetLocalListVersion; +using MicroOcpp::JsonDoc; -GetLocalListVersion::GetLocalListVersion(Model& model) : model(model) { +GetLocalListVersion::GetLocalListVersion(Model& model) : MemoryManaged("v16.Operation.", "GetLocalListVersion"), model(model) { } @@ -21,13 +26,18 @@ void GetLocalListVersion::processReq(JsonObject payload) { //empty payload } -std::unique_ptr GetLocalListVersion::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr GetLocalListVersion::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); - if (auto authService = model.getAuthorizationService()) { + + auto authService = model.getAuthorizationService(); + if (authService && authService->localAuthListEnabled()) { payload["listVersion"] = authService->getLocalListVersion(); } else { + //TC_042_1_CS Get Local List Version (not supported) payload["listVersion"] = -1; } return doc; } + +#endif //MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Operations/GetLocalListVersion.h b/src/MicroOcpp/Operations/GetLocalListVersion.h index 90e6d099..554f7a6b 100644 --- a/src/MicroOcpp/Operations/GetLocalListVersion.h +++ b/src/MicroOcpp/Operations/GetLocalListVersion.h @@ -1,9 +1,13 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef GETLOCALLISTVERSION_H -#define GETLOCALLISTVERSION_H +#ifndef MO_GETLOCALLISTVERSION_H +#define MO_GETLOCALLISTVERSION_H + +#include + +#if MO_ENABLE_LOCAL_AUTH #include @@ -13,7 +17,7 @@ class Model; namespace Ocpp16 { -class GetLocalListVersion : public Operation { +class GetLocalListVersion : public Operation, public MemoryManaged { private: Model& model; public: @@ -23,9 +27,11 @@ class GetLocalListVersion : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp + +#endif //MO_ENABLE_LOCAL_AUTH #endif diff --git a/src/MicroOcpp/Operations/GetVariables.cpp b/src/MicroOcpp/Operations/GetVariables.cpp new file mode 100644 index 00000000..30f8dc7f --- /dev/null +++ b/src/MicroOcpp/Operations/GetVariables.cpp @@ -0,0 +1,228 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include + +#include //for tolower + +using MicroOcpp::Ocpp201::GetVariableData; +using MicroOcpp::Ocpp201::GetVariables; +using MicroOcpp::JsonDoc; + +GetVariableData::GetVariableData(const char *memory_tag) : componentName{makeString(memory_tag)}, variableName{makeString(memory_tag)} { + +} + +GetVariables::GetVariables(VariableService& variableService) : MemoryManaged("v201.Operation.", "GetVariables"), variableService(variableService), queries(makeVector(getMemoryTag())) { + +} + +const char* GetVariables::getOperationType(){ + return "GetVariables"; +} + +void GetVariables::processReq(JsonObject payload) { + for (JsonObject getVariable : payload["getVariableData"].as()) { + + queries.emplace_back(getMemoryTag()); + auto& data = queries.back(); + + if (getVariable.containsKey("attributeType")) { + const char *attributeTypeCstr = getVariable["attributeType"] | "_Undefined"; + if (!strcmp(attributeTypeCstr, "Actual")) { + data.attributeType = Variable::AttributeType::Actual; + } else if (!strcmp(attributeTypeCstr, "Target")) { + data.attributeType = Variable::AttributeType::Target; + } else if (!strcmp(attributeTypeCstr, "MinSet")) { + data.attributeType = Variable::AttributeType::MinSet; + } else if (!strcmp(attributeTypeCstr, "MaxSet")) { + data.attributeType = Variable::AttributeType::MaxSet; + } else { + errorCode = "FormationViolation"; + MO_DBG_ERR("invalid attributeType"); + return; + } + } + + const char *componentNameCstr = getVariable["component"]["name"] | (const char*) nullptr; + const char *variableNameCstr = getVariable["variable"]["name"] | (const char*) nullptr; + + if (!componentNameCstr || + !variableNameCstr) { + errorCode = "FormationViolation"; + return; + } + + data.componentName = componentNameCstr; + data.variableName = variableNameCstr; + + // TODO check against ConfigurationValueSize + + data.componentEvseId = getVariable["component"]["evse"]["id"] | -1; + data.componentEvseConnectorId = getVariable["component"]["evse"]["connectorId"] | -1; + + if (getVariable["component"].containsKey("evse") && data.componentEvseId < 0) { + errorCode = "FormationViolation"; + MO_DBG_ERR("malformatted / missing evseId"); + return; + } + } + + if (queries.empty()) { + errorCode = "FormationViolation"; + return; + } +} + +std::unique_ptr GetVariables::createConf(){ + + // process GetVariables queries + for (auto& query : queries) { + query.attributeStatus = variableService.getVariable( + query.attributeType, + ComponentId(query.componentName.c_str(), + EvseId(query.componentEvseId, query.componentEvseConnectorId)), + query.variableName.c_str(), + &query.variable); + } + + #define VALUE_BUFSIZE 30 // for primitives (int) + + size_t capacity = JSON_ARRAY_SIZE(queries.size()); + for (const auto& data : queries) { + size_t valueCapacity = 0; + if (data.variable) { + switch (data.variable->getInternalDataType()) { + case Variable::InternalDataType::Int: { + // measure int size by printing to a dummy buf + char valbuf [VALUE_BUFSIZE]; + auto ret = snprintf(valbuf, VALUE_BUFSIZE, "%i", data.variable->getInt()); + if (ret < 0 || ret >= VALUE_BUFSIZE) { + continue; + } + valueCapacity = (size_t) ret + 1; + break; + } + case Variable::InternalDataType::Bool: + // bool will be stored in zero-copy mode (string literal "true" or "false") + valueCapacity = 0; + break; + case Variable::InternalDataType::String: + valueCapacity = strlen(data.variable->getString()); // TODO limit by ReportingValueSize + break; + default: + MO_DBG_ERR("internal error"); + break; + } + } + + capacity += + JSON_OBJECT_SIZE(5) + // getVariableResult + valueCapacity + // capacity needed for storing the value + JSON_OBJECT_SIZE(2) + // component + data.componentName.length() + 1 + + JSON_OBJECT_SIZE(2) + // evse + JSON_OBJECT_SIZE(2) + // variable + data.variableName.length() + 1; + } + + auto doc = makeJsonDoc(getMemoryTag(), capacity); + + JsonObject payload = doc->to(); + JsonArray getVariableResult = payload.createNestedArray("getVariableResult"); + + for (const auto& data : queries) { + JsonObject getVariable = getVariableResult.createNestedObject(); + + const char *attributeStatusCstr = "Rejected"; + switch (data.attributeStatus) { + case GetVariableStatus::Accepted: + attributeStatusCstr = "Accepted"; + break; + case GetVariableStatus::Rejected: + attributeStatusCstr = "Rejected"; + break; + case GetVariableStatus::UnknownComponent: + attributeStatusCstr = "UnknownComponent"; + break; + case GetVariableStatus::UnknownVariable: + attributeStatusCstr = "UnknownVariable"; + break; + case GetVariableStatus::NotSupportedAttributeType: + attributeStatusCstr = "NotSupportedAttributeType"; + break; + default: + MO_DBG_ERR("internal error"); + break; + } + getVariable["attributeStatus"] = attributeStatusCstr; + + const char *attributeTypeCstr = nullptr; + switch (data.attributeType) { + case Variable::AttributeType::Actual: + // leave blank when Actual + break; + case Variable::AttributeType::Target: + attributeTypeCstr = "Target"; + break; + case Variable::AttributeType::MinSet: + attributeTypeCstr = "MinSet"; + break; + case Variable::AttributeType::MaxSet: + attributeTypeCstr = "MaxSet"; + break; + default: + MO_DBG_ERR("internal error"); + break; + } + if (attributeTypeCstr) { + getVariable["attributeType"] = attributeTypeCstr; + } + + if (data.variable) { + switch (data.variable->getInternalDataType()) { + case Variable::InternalDataType::Int: { + char valbuf [VALUE_BUFSIZE]; + auto ret = snprintf(valbuf, VALUE_BUFSIZE, "%i", data.variable->getInt()); + if (ret < 0 || ret >= VALUE_BUFSIZE) { + break; + } + getVariable["attributeValue"] = valbuf; + break; + } + case Variable::InternalDataType::Bool: + getVariable["attributeValue"] = data.variable->getBool() ? "true" : "false"; + break; + case Variable::InternalDataType::String: + getVariable["attributeValue"] = (char*) data.variable->getString(); // force zero-copy mode + break; + default: + MO_DBG_ERR("internal error"); + break; + } + } + + getVariable["component"]["name"] = (char*) data.componentName.c_str(); // force copy-mode + + if (data.componentEvseId >= 0) { + getVariable["component"]["evse"]["id"] = data.componentEvseId; + } + + if (data.componentEvseConnectorId >= 0) { + getVariable["component"]["evse"]["connectorId"] = data.componentEvseConnectorId; + } + + getVariable["variable"]["name"] = (char*) data.variableName.c_str(); // force copy-mode + } + + return doc; +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/GetVariables.h b/src/MicroOcpp/Operations/GetVariables.h new file mode 100644 index 00000000..a0360e62 --- /dev/null +++ b/src/MicroOcpp/Operations/GetVariables.h @@ -0,0 +1,63 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_GETVARIABLES_H +#define MO_GETVARIABLES_H + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include + +namespace MicroOcpp { + +class VariableService; + +namespace Ocpp201 { + +// GetVariableDataType (2.25) and +// GetVariableResultType (2.26) +struct GetVariableData { + // GetVariableDataType + Variable::AttributeType attributeType = Variable::AttributeType::Actual; + String componentName; + int componentEvseId = -1; + int componentEvseConnectorId = -1; + String variableName; + + // GetVariableResultType + GetVariableStatus attributeStatus; + Variable *variable = nullptr; + + GetVariableData(const char *memory_tag = nullptr); +}; + +class GetVariables : public Operation, public MemoryManaged { +private: + VariableService& variableService; + Vector queries; + + const char *errorCode = nullptr; +public: + GetVariables(VariableService& variableService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} + +}; + +} //namespace Ocpp201 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Operations/Heartbeat.cpp b/src/MicroOcpp/Operations/Heartbeat.cpp index 7d358a6d..4dfd0439 100644 --- a/src/MicroOcpp/Operations/Heartbeat.cpp +++ b/src/MicroOcpp/Operations/Heartbeat.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -8,8 +8,9 @@ #include using MicroOcpp::Ocpp16::Heartbeat; +using MicroOcpp::JsonDoc; -Heartbeat::Heartbeat(Model& model) : model(model) { +Heartbeat::Heartbeat(Model& model) : MemoryManaged("v16.Operation.", "Heartbeat"), model(model) { } @@ -17,7 +18,7 @@ const char* Heartbeat::getOperationType(){ return "Heartbeat"; } -std::unique_ptr Heartbeat::createReq() { +std::unique_ptr Heartbeat::createReq() { return createEmptyDocument(); } @@ -44,8 +45,8 @@ void Heartbeat::processReq(JsonObject payload) { } -std::unique_ptr Heartbeat::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1) + (JSONDATE_LENGTH + 1))); +std::unique_ptr Heartbeat::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + (JSONDATE_LENGTH + 1)); JsonObject payload = doc->to(); //safety mechanism; in some test setups the library could have to answer Heartbeats without valid system time diff --git a/src/MicroOcpp/Operations/Heartbeat.h b/src/MicroOcpp/Operations/Heartbeat.h index 23855e39..b3ce6a0e 100644 --- a/src/MicroOcpp/Operations/Heartbeat.h +++ b/src/MicroOcpp/Operations/Heartbeat.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef HEARTBEAT_H -#define HEARTBEAT_H +#ifndef MO_HEARTBEAT_H +#define MO_HEARTBEAT_H #include @@ -13,7 +13,7 @@ class Model; namespace Ocpp16 { -class Heartbeat : public Operation { +class Heartbeat : public Operation, public MemoryManaged { private: Model& model; public: @@ -21,13 +21,13 @@ class Heartbeat : public Operation { const char* getOperationType() override; - std::unique_ptr createReq() override; + std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; }; } //end namespace Ocpp16 diff --git a/src/MicroOcpp/Operations/InstallCertificate.cpp b/src/MicroOcpp/Operations/InstallCertificate.cpp new file mode 100644 index 00000000..b9859501 --- /dev/null +++ b/src/MicroOcpp/Operations/InstallCertificate.cpp @@ -0,0 +1,85 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_CERT_MGMT + +#include +#include + +using MicroOcpp::Ocpp201::InstallCertificate; +using MicroOcpp::JsonDoc; + +InstallCertificate::InstallCertificate(CertificateService& certService) : MemoryManaged("v201.Operation.", "InstallCertificate"), certService(certService) { + +} + +void InstallCertificate::processReq(JsonObject payload) { + + if (!payload.containsKey("certificateType") || + !payload.containsKey("certificate")) { + errorCode = "FormationViolation"; + return; + } + + InstallCertificateType certificateType; + + const char *certificateTypeCstr = payload["certificateType"] | "_Invalid"; + + if (!strcmp(certificateTypeCstr, "V2GRootCertificate")) { + certificateType = InstallCertificateType_V2GRootCertificate; + } else if (!strcmp(certificateTypeCstr, "MORootCertificate")) { + certificateType = InstallCertificateType_MORootCertificate; + } else if (!strcmp(certificateTypeCstr, "CSMSRootCertificate")) { + certificateType = InstallCertificateType_CSMSRootCertificate; + } else if (!strcmp(certificateTypeCstr, "ManufacturerRootCertificate")) { + certificateType = InstallCertificateType_ManufacturerRootCertificate; + } else { + errorCode = "FormationViolation"; + return; + } + + if (!payload["certificate"].is()) { + errorCode = "FormationViolation"; + return; + } + + const char *certificate = payload["certificate"]; + + auto certStore = certService.getCertificateStore(); + if (!certStore) { + errorCode = "NotSupported"; + return; + } + + auto status = certStore->installCertificate(certificateType, certificate); + + switch (status) { + case InstallCertificateStatus_Accepted: + this->status = "Accepted"; + break; + case InstallCertificateStatus_Rejected: + this->status = "Rejected"; + break; + case InstallCertificateStatus_Failed: + this->status = "Failed"; + break; + default: + MO_DBG_ERR("internal error"); + errorCode = "InternalError"; + return; + } + + //operation executed successfully +} + +std::unique_ptr InstallCertificate::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + payload["status"] = status; + return doc; +} + +#endif //MO_ENABLE_CERT_MGMT diff --git a/src/MicroOcpp/Operations/InstallCertificate.h b/src/MicroOcpp/Operations/InstallCertificate.h new file mode 100644 index 00000000..e2c22b55 --- /dev/null +++ b/src/MicroOcpp/Operations/InstallCertificate.h @@ -0,0 +1,41 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_INSTALLCERTIFICATE_H +#define MO_INSTALLCERTIFICATE_H + +#include + +#if MO_ENABLE_CERT_MGMT + +#include + +namespace MicroOcpp { + +class CertificateService; + +namespace Ocpp201 { + +class InstallCertificate : public Operation, public MemoryManaged { +private: + CertificateService& certService; + const char *status = nullptr; + const char *errorCode = nullptr; +public: + InstallCertificate(CertificateService& certService); + + const char* getOperationType() override {return "InstallCertificate";} + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_CERT_MGMT +#endif diff --git a/src/MicroOcpp/Operations/MeterValues.cpp b/src/MicroOcpp/Operations/MeterValues.cpp index 4e44dfad..ee2b4c7e 100644 --- a/src/MicroOcpp/Operations/MeterValues.cpp +++ b/src/MicroOcpp/Operations/MeterValues.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -9,19 +9,23 @@ #include using MicroOcpp::Ocpp16::MeterValues; - -#define ENERGY_METER_TIMEOUT_MS 30 * 1000 //after waiting for 30s, send MeterValues without missing readings +using MicroOcpp::JsonDoc; //can only be used for echo server debugging -MeterValues::MeterValues() { +MeterValues::MeterValues(Model& model) : MemoryManaged("v16.Operation.", "MeterValues"), model(model) { } -MeterValues::MeterValues(std::vector>&& meterValue, unsigned int connectorId, std::shared_ptr transaction) - : meterValue{std::move(meterValue)}, connectorId{connectorId}, transaction{transaction} { +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(){ } @@ -30,26 +34,32 @@ const char* MeterValues::getOperationType(){ return "MeterValues"; } -std::unique_ptr MeterValues::createReq() { +std::unique_ptr MeterValues::createReq() { size_t capacity = 0; - - std::vector> entries; - for (auto value = meterValue.begin(); value != meterValue.end(); value++) { - auto entry = (*value)->toJson(); - if (entry) { - capacity += entry->capacity(); - entries.push_back(std::move(entry)); + + 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"); - (void)0; } } capacity += JSON_OBJECT_SIZE(3); - capacity += JSON_ARRAY_SIZE(entries.size()); + capacity += JSON_ARRAY_SIZE(1); - auto doc = std::unique_ptr(new DynamicJsonDocument(capacity + 100)); //TODO remove safety space + auto doc = makeJsonDoc(getMemoryTag(), capacity); auto payload = doc->to(); payload["connectorId"] = connectorId; @@ -57,9 +67,9 @@ std::unique_ptr MeterValues::createReq() { payload["transactionId"] = transaction->getTransactionId(); } - auto meterValueJson = payload.createNestedArray("meterValue"); - for (auto entry = entries.begin(); entry != entries.end(); entry++) { - meterValueJson.add(**entry); + auto meterValueArray = payload.createNestedArray("meterValue"); + if (meterValueJson) { + meterValueArray.add(*meterValueJson); } return doc; @@ -78,6 +88,6 @@ void MeterValues::processReq(JsonObject payload) { } -std::unique_ptr MeterValues::createConf(){ +std::unique_ptr MeterValues::createConf(){ return createEmptyDocument(); } diff --git a/src/MicroOcpp/Operations/MeterValues.h b/src/MicroOcpp/Operations/MeterValues.h index d778040f..774a538a 100644 --- a/src/MicroOcpp/Operations/MeterValues.h +++ b/src/MicroOcpp/Operations/MeterValues.h @@ -1,46 +1,49 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef METERVALUES_H -#define METERVALUES_H +#ifndef MO_METERVALUES_H +#define MO_METERVALUES_H #include +#include #include -#include - namespace MicroOcpp { +class Model; class MeterValue; class Transaction; namespace Ocpp16 { -class MeterValues : public Operation { +class MeterValues : public Operation, public MemoryManaged { private: - std::vector> meterValue; + Model& model; //for adjusting the timestamp if MeterValue has been created before BootNotification + MeterValue *meterValue = nullptr; + std::unique_ptr meterValueOwnership; unsigned int connectorId = 0; std::shared_ptr transaction; public: - MeterValues(std::vector>&& meterValue, unsigned int connectorId, std::shared_ptr transaction = nullptr); + 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(); //for debugging only. Make this for the server pendant + MeterValues(Model& model); //for debugging only. Make this for the server pendant ~MeterValues(); const char* getOperationType() override; - std::unique_ptr createReq() override; + std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; }; } //end namespace Ocpp16 diff --git a/src/MicroOcpp/Operations/NotifyReport.cpp b/src/MicroOcpp/Operations/NotifyReport.cpp new file mode 100644 index 00000000..0528e452 --- /dev/null +++ b/src/MicroOcpp/Operations/NotifyReport.cpp @@ -0,0 +1,242 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include +#include + +using namespace MicroOcpp::Ocpp201; +using MicroOcpp::JsonDoc; + +NotifyReport::NotifyReport(Model& model, int requestId, const Timestamp& generatedAt, bool tbc, int seqNo, const Vector& reportData) + : MemoryManaged("v201.Operation.", "NotifyReport"), model(model), requestId(requestId), generatedAt(generatedAt), tbc(tbc), seqNo(seqNo), reportData(reportData) { + +} + +const char* NotifyReport::getOperationType() { + return "NotifyReport"; +} + +std::unique_ptr NotifyReport::createReq() { + + #define VALUE_BUFSIZE 30 // for primitives (int) + + const Variable::AttributeType enumerateAttributeTypes [] = { + Variable::AttributeType::Actual, + Variable::AttributeType::Target, + Variable::AttributeType::MinSet, + Variable::AttributeType::MaxSet + }; + + size_t capacity = + JSON_OBJECT_SIZE(5) + //total of 5 fields + JSONDATE_LENGTH + 1; //timestamp string + + capacity += JSON_ARRAY_SIZE(reportData.size()); + for (auto variable : reportData) { + capacity += JSON_OBJECT_SIZE(4); //total of 4 fields + capacity += 2 * JSON_OBJECT_SIZE(2); //component composite + capacity += JSON_OBJECT_SIZE(1); //variable composite + + size_t nAttributes = 0; + size_t valueCapacity = 0; + for (auto attributeType : enumerateAttributeTypes) { + if (!variable->hasAttribute(attributeType)) { + continue; + } + nAttributes++; + switch (variable->getInternalDataType()) { + case Variable::InternalDataType::Int: { + // measure int size by printing to a dummy buf + char valbuf [VALUE_BUFSIZE]; + auto ret = snprintf(valbuf, VALUE_BUFSIZE, "%i", variable->getInt()); + if (ret < 0 || ret >= VALUE_BUFSIZE) { + continue; + } + valueCapacity = (size_t) ret + 1; + break; + } + case Variable::InternalDataType::Bool: + // bool will be stored in zero-copy mode (string literal "true" or "false") + valueCapacity = 0; + break; + case Variable::InternalDataType::String: + valueCapacity = strlen(variable->getString()); // TODO limit by ReportingValueSize + break; + default: + MO_DBG_ERR("internal error"); + break; + } + } + + capacity += nAttributes * JSON_OBJECT_SIZE(5); //variableAttribute composite + capacity += valueCapacity; //variableAttribute value total size + + capacity += JSON_OBJECT_SIZE(2); //variableCharacteristics composite: only send two data fields + } + + auto doc = makeJsonDoc(getMemoryTag(), capacity); + + JsonObject payload = doc->to(); + payload["requestId"] = requestId; + + char generatedAtCstr [JSONDATE_LENGTH + 1]; + generatedAt.toJsonString(generatedAtCstr, sizeof(generatedAtCstr)); + payload["generatedAt"] = generatedAtCstr; + + if (tbc) { + payload["tbc"] = true; + } + + payload["seqNo"] = seqNo; + + JsonArray reportDataJsonArray = payload.createNestedArray("reportData"); + + for (auto variable : reportData) { + JsonObject reportDataJson = reportDataJsonArray.createNestedObject(); + + reportDataJson["component"]["name"] = (char*) variable->getComponentId().name; // force copy-mode + + if (variable->getComponentId().evse.id >= 0) { + reportDataJson["component"]["evse"]["id"] = variable->getComponentId().evse.id; + } + + if (variable->getComponentId().evse.connectorId >= 0) { + reportDataJson["component"]["evse"]["connectorId"] = variable->getComponentId().evse.connectorId; + } + + reportDataJson["variable"]["name"] = (char*) variable->getName(); // force copy-mode + + JsonArray variableAttribute = reportDataJson.createNestedArray("variableAttribute"); + + for (auto attributeType : enumerateAttributeTypes) { + if (!variable->hasAttribute(attributeType)) { + continue; + } + + JsonObject attribute = variableAttribute.createNestedObject(); + + const char *attributeTypeCstr = nullptr; + switch (attributeType) { + case Variable::AttributeType::Actual: + // leave blank when Actual + break; + case Variable::AttributeType::Target: + attributeTypeCstr = "Target"; + break; + case Variable::AttributeType::MinSet: + attributeTypeCstr = "MinSet"; + break; + case Variable::AttributeType::MaxSet: + attributeTypeCstr = "MaxSet"; + break; + default: + MO_DBG_ERR("internal error"); + break; + } + if (attributeTypeCstr) { + attribute["type"] = attributeTypeCstr; + } + + if (variable->getMutability() != Variable::Mutability::WriteOnly) { + switch (variable->getInternalDataType()) { + case Variable::InternalDataType::Int: { + char valbuf [VALUE_BUFSIZE]; + auto ret = snprintf(valbuf, VALUE_BUFSIZE, "%i", variable->getInt()); + if (ret < 0 || ret >= VALUE_BUFSIZE) { + break; + } + attribute["value"] = valbuf; + break; + } + case Variable::InternalDataType::Bool: + attribute["value"] = variable->getBool() ? "true" : "false"; + break; + case Variable::InternalDataType::String: + attribute["value"] = (char*) variable->getString(); // force zero-copy mode + break; + default: + MO_DBG_ERR("internal error"); + break; + } + } + + const char *mutabilityCstr = nullptr; + switch (variable->getMutability()) { + case Variable::Mutability::ReadOnly: + mutabilityCstr = "ReadOnly"; + break; + case Variable::Mutability::WriteOnly: + mutabilityCstr = "WriteOnly"; + break; + case Variable::Mutability::ReadWrite: + // leave blank when ReadWrite + break; + default: + MO_DBG_ERR("internal error"); + break; + } + if (mutabilityCstr) { + attribute["mutability"] = mutabilityCstr; + } + + if (variable->isPersistent()) { + attribute["persistent"] = true; + } + + if (variable->isConstant()) { + attribute["constant"] = true; + } + } + + JsonObject variableCharacteristics = reportDataJson.createNestedObject("variableCharacteristics"); + + const char *dataTypeCstr = ""; + switch (variable->getVariableDataType()) { + case VariableCharacteristics::DataType::string: + dataTypeCstr = "string"; + break; + case VariableCharacteristics::DataType::decimal: + dataTypeCstr = "decimal"; + break; + case VariableCharacteristics::DataType::integer: + dataTypeCstr = "integer"; + break; + case VariableCharacteristics::DataType::dateTime: + dataTypeCstr = "dateTime"; + break; + case VariableCharacteristics::DataType::boolean: + dataTypeCstr = "boolean"; + break; + case VariableCharacteristics::DataType::OptionList: + dataTypeCstr = "OptionList"; + break; + case VariableCharacteristics::DataType::SequenceList: + dataTypeCstr = "SequenceList"; + break; + case VariableCharacteristics::DataType::MemberList: + dataTypeCstr = "MemberList"; + break; + default: + MO_DBG_ERR("internal error"); + break; + } + variableCharacteristics["dataType"] = dataTypeCstr; + + variableCharacteristics["supportsMonitoring"] = variable->getSupportsMonitoring(); + } + + return doc; +} + +void NotifyReport::processConf(JsonObject payload) { + // empty payload +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/NotifyReport.h b/src/MicroOcpp/Operations/NotifyReport.h new file mode 100644 index 00000000..1a092678 --- /dev/null +++ b/src/MicroOcpp/Operations/NotifyReport.h @@ -0,0 +1,46 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_TRANSACTIONEVENT_H +#define MO_TRANSACTIONEVENT_H + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include + +namespace MicroOcpp { + +class Model; +class Variable; + +namespace Ocpp201 { + +class NotifyReport : public Operation, public MemoryManaged { +private: + Model& model; + + int requestId; + Timestamp generatedAt; + bool tbc; + int seqNo; + Vector reportData; +public: + + NotifyReport(Model& model, int requestId, const Timestamp& generatedAt, bool tbc, int seqNo, const Vector& reportData); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp +#endif // MO_ENABLE_V201 +#endif diff --git a/src/MicroOcpp/Operations/RemoteStartTransaction.cpp b/src/MicroOcpp/Operations/RemoteStartTransaction.cpp index 4e06486e..977f675d 100644 --- a/src/MicroOcpp/Operations/RemoteStartTransaction.cpp +++ b/src/MicroOcpp/Operations/RemoteStartTransaction.cpp @@ -1,9 +1,10 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include +#include #include #include #include @@ -11,8 +12,9 @@ #include using MicroOcpp::Ocpp16::RemoteStartTransaction; +using MicroOcpp::JsonDoc; -RemoteStartTransaction::RemoteStartTransaction(Model& model) : model(model) { +RemoteStartTransaction::RemoteStartTransaction(Model& model) : MemoryManaged("v16.Operation.", "RemoteStartTransaction"), model(model) { } @@ -23,6 +25,13 @@ const char* RemoteStartTransaction::getOperationType() { void RemoteStartTransaction::processReq(JsonObject payload) { int connectorId = payload["connectorId"] | -1; + // OCPP 1.6 specification: connectorId SHALL be > 0 (TC_027_CS) + if (connectorId == 0) { + MO_DBG_INFO("RemoteStartTransaction rejected: connectorId SHALL not be 0"); + accepted = false; + return; + } + if (!payload.containsKey("idTag")) { errorCode = "FormationViolation"; return; @@ -97,8 +106,14 @@ void RemoteStartTransaction::processReq(JsonObject payload) { } if (success) { - auto tx = selectConnector->beginTransaction_authorized(idTag); - selectConnector->updateTxNotification(TxNotification::RemoteStart); + 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); @@ -115,8 +130,8 @@ void RemoteStartTransaction::processReq(JsonObject payload) { } } -std::unique_ptr RemoteStartTransaction::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr RemoteStartTransaction::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (accepted) { payload["status"] = "Accepted"; @@ -126,8 +141,8 @@ std::unique_ptr RemoteStartTransaction::createConf(){ return doc; } -std::unique_ptr RemoteStartTransaction::createReq() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr RemoteStartTransaction::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["idTag"] = "A0000000"; diff --git a/src/MicroOcpp/Operations/RemoteStartTransaction.h b/src/MicroOcpp/Operations/RemoteStartTransaction.h index 90b78483..411578bd 100644 --- a/src/MicroOcpp/Operations/RemoteStartTransaction.h +++ b/src/MicroOcpp/Operations/RemoteStartTransaction.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef REMOTESTARTTRANSACTION_H -#define REMOTESTARTTRANSACTION_H +#ifndef MO_REMOTESTARTTRANSACTION_H +#define MO_REMOTESTARTTRANSACTION_H #include #include @@ -15,7 +15,7 @@ class ChargingProfile; namespace Ocpp16 { -class RemoteStartTransaction : public Operation { +class RemoteStartTransaction : public Operation, public MemoryManaged { private: Model& model; @@ -28,13 +28,13 @@ class RemoteStartTransaction : public Operation { const char* getOperationType() override; - std::unique_ptr createReq() override; + std::unique_ptr createReq() override; void processConf(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 *getErrorDescription() override {return errorDescription;} diff --git a/src/MicroOcpp/Operations/RemoteStopTransaction.cpp b/src/MicroOcpp/Operations/RemoteStopTransaction.cpp index f19dd969..8a36699a 100644 --- a/src/MicroOcpp/Operations/RemoteStopTransaction.cpp +++ b/src/MicroOcpp/Operations/RemoteStopTransaction.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -8,8 +8,9 @@ #include using MicroOcpp::Ocpp16::RemoteStopTransaction; +using MicroOcpp::JsonDoc; -RemoteStopTransaction::RemoteStopTransaction(Model& model) : model(model) { +RemoteStopTransaction::RemoteStopTransaction(Model& model) : MemoryManaged("v16.Operation.", "RemoteStopTransaction"), model(model) { } @@ -34,8 +35,8 @@ void RemoteStopTransaction::processReq(JsonObject payload) { } } -std::unique_ptr RemoteStopTransaction::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr RemoteStopTransaction::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (accepted){ payload["status"] = "Accepted"; diff --git a/src/MicroOcpp/Operations/RemoteStopTransaction.h b/src/MicroOcpp/Operations/RemoteStopTransaction.h index 984de7e5..2a42d695 100644 --- a/src/MicroOcpp/Operations/RemoteStopTransaction.h +++ b/src/MicroOcpp/Operations/RemoteStopTransaction.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef REMOTESTOPTRANSACTION_H -#define REMOTESTOPTRANSACTION_H +#ifndef MO_REMOTESTOPTRANSACTION_H +#define MO_REMOTESTOPTRANSACTION_H #include @@ -13,7 +13,7 @@ class Model; namespace Ocpp16 { -class RemoteStopTransaction : public Operation { +class RemoteStopTransaction : public Operation, public MemoryManaged { private: Model& model; bool accepted = false; @@ -26,7 +26,7 @@ class RemoteStopTransaction : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; diff --git a/src/MicroOcpp/Operations/RequestStartTransaction.cpp b/src/MicroOcpp/Operations/RequestStartTransaction.cpp new file mode 100644 index 00000000..1b37f051 --- /dev/null +++ b/src/MicroOcpp/Operations/RequestStartTransaction.cpp @@ -0,0 +1,77 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include + +using MicroOcpp::Ocpp201::RequestStartTransaction; +using MicroOcpp::JsonDoc; + +RequestStartTransaction::RequestStartTransaction(RemoteControlService& rcService) : MemoryManaged("v201.Operation.", "RequestStartTransaction"), rcService(rcService) { + +} + +const char* RequestStartTransaction::getOperationType(){ + return "RequestStartTransaction"; +} + +void RequestStartTransaction::processReq(JsonObject payload) { + + int evseId = payload["evseId"] | 0; + if (evseId < 0 || evseId >= MO_NUM_EVSEID) { + errorCode = "PropertyConstraintViolation"; + return; + } + + int remoteStartId = payload["remoteStartId"] | 0; + if (remoteStartId < 0) { + errorCode = "PropertyConstraintViolation"; + MO_DBG_ERR("IDs must be >= 0"); + return; + } + + IdToken idToken; + if (!idToken.parseCstr(payload["idToken"]["idToken"] | (const char*)nullptr, payload["idToken"]["type"] | (const char*)nullptr)) { //parseCstr rejects nullptr as argument + MO_DBG_ERR("could not parse idToken"); + errorCode = "FormationViolation"; + return; + } + + status = rcService.requestStartTransaction(evseId, remoteStartId, idToken, transactionId, sizeof(transactionId)); +} + +std::unique_ptr RequestStartTransaction::createConf(){ + + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + + const char *statusCstr = ""; + + switch (status) { + case RequestStartStopStatus_Accepted: + statusCstr = "Accepted"; + break; + case RequestStartStopStatus_Rejected: + statusCstr = "Rejected"; + break; + default: + MO_DBG_ERR("internal error"); + break; + } + + payload["status"] = statusCstr; + + if (transaction) { + payload["transactionId"] = (const char*)transaction->transactionId; //force zero-copy mode + } + + return doc; +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/RequestStartTransaction.h b/src/MicroOcpp/Operations/RequestStartTransaction.h new file mode 100644 index 00000000..4ee19761 --- /dev/null +++ b/src/MicroOcpp/Operations/RequestStartTransaction.h @@ -0,0 +1,50 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_REQUESTSTARTTRANSACTION_H +#define MO_REQUESTSTARTTRANSACTION_H + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include +#include + +namespace MicroOcpp { + +class RemoteControlService; + +namespace Ocpp201 { + +class RequestStartTransaction : public Operation, public MemoryManaged { +private: + RemoteControlService& rcService; + + RequestStartStopStatus status; + std::shared_ptr transaction; + char transactionId [MO_TXID_LEN_MAX + 1] = {'\0'}; + + const char *errorCode = nullptr; +public: + RequestStartTransaction(RemoteControlService& rcService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} + +}; + +} //namespace Ocpp201 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Operations/RequestStopTransaction.cpp b/src/MicroOcpp/Operations/RequestStopTransaction.cpp new file mode 100644 index 00000000..a316188e --- /dev/null +++ b/src/MicroOcpp/Operations/RequestStopTransaction.cpp @@ -0,0 +1,60 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include + +using MicroOcpp::Ocpp201::RequestStopTransaction; +using MicroOcpp::JsonDoc; + +RequestStopTransaction::RequestStopTransaction(RemoteControlService& rcService) : MemoryManaged("v201.Operation.", "RequestStopTransaction"), rcService(rcService) { + +} + +const char* RequestStopTransaction::getOperationType(){ + return "RequestStopTransaction"; +} + +void RequestStopTransaction::processReq(JsonObject payload) { + + if (!payload.containsKey("transactionId") || + !payload["transactionId"].is() || + strlen(payload["transactionId"].as()) > MO_TXID_LEN_MAX) { + errorCode = "FormationViolation"; + return; + } + + status = rcService.requestStopTransaction(payload["transactionId"].as()); +} + +std::unique_ptr RequestStopTransaction::createConf(){ + + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + + const char *statusCstr = ""; + + switch (status) { + case RequestStartStopStatus_Accepted: + statusCstr = "Accepted"; + break; + case RequestStartStopStatus_Rejected: + statusCstr = "Rejected"; + break; + default: + MO_DBG_ERR("internal error"); + break; + } + + payload["status"] = statusCstr; + + return doc; +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/RequestStopTransaction.h b/src/MicroOcpp/Operations/RequestStopTransaction.h new file mode 100644 index 00000000..30664b67 --- /dev/null +++ b/src/MicroOcpp/Operations/RequestStopTransaction.h @@ -0,0 +1,47 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_REQUESTSTOPTRANSACTION_H +#define MO_REQUESTSTOPTRANSACTION_H + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include + +namespace MicroOcpp { + +class RemoteControlService; + +namespace Ocpp201 { + +class RequestStopTransaction : public Operation, public MemoryManaged { +private: + RemoteControlService& rcService; + + RequestStartStopStatus status; + + const char *errorCode = nullptr; +public: + RequestStopTransaction(RemoteControlService& rcService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} + +}; + +} //namespace Ocpp201 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Operations/ReserveNow.cpp b/src/MicroOcpp/Operations/ReserveNow.cpp index 1a64391f..26857fdb 100644 --- a/src/MicroOcpp/Operations/ReserveNow.cpp +++ b/src/MicroOcpp/Operations/ReserveNow.cpp @@ -1,16 +1,22 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + +#if MO_ENABLE_RESERVATION + #include #include #include #include #include +#include using MicroOcpp::Ocpp16::ReserveNow; +using MicroOcpp::JsonDoc; -ReserveNow::ReserveNow(Model& model) : model(model) { +ReserveNow::ReserveNow(Model& model) : MemoryManaged("v16.Operation.", "ReserveNow"), model(model) { } @@ -82,19 +88,19 @@ void ReserveNow::processReq(JsonObject payload) { connector = model.getConnector((unsigned int) connectorId); } - if (chargePoint->getStatus() == ChargePointStatus::Faulted || - (connector && connector->getStatus() == ChargePointStatus::Faulted)) { + if (chargePoint->getStatus() == ChargePointStatus_Faulted || + (connector && connector->getStatus() == ChargePointStatus_Faulted)) { reservationStatus = "Faulted"; return; } - if (chargePoint->getStatus() == ChargePointStatus::Unavailable || - (connector && connector->getStatus() == ChargePointStatus::Unavailable)) { + if (chargePoint->getStatus() == ChargePointStatus_Unavailable || + (connector && connector->getStatus() == ChargePointStatus_Unavailable)) { reservationStatus = "Unavailable"; return; } - if (connector && connector->getStatus() != ChargePointStatus::Available) { + if (connector && connector->getStatus() != ChargePointStatus_Available) { reservationStatus = "Occupied"; return; } @@ -109,8 +115,8 @@ void ReserveNow::processReq(JsonObject payload) { } } -std::unique_ptr ReserveNow::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr ReserveNow::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (reservationStatus) { @@ -122,3 +128,5 @@ std::unique_ptr ReserveNow::createConf(){ return doc; } + +#endif //MO_ENABLE_RESERVATION diff --git a/src/MicroOcpp/Operations/ReserveNow.h b/src/MicroOcpp/Operations/ReserveNow.h index ddaba236..cf162ad6 100644 --- a/src/MicroOcpp/Operations/ReserveNow.h +++ b/src/MicroOcpp/Operations/ReserveNow.h @@ -1,9 +1,13 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef RESERVENOW_H -#define RESERVENOW_H +#ifndef MO_RESERVENOW_H +#define MO_RESERVENOW_H + +#include + +#if MO_ENABLE_RESERVATION #include @@ -13,7 +17,7 @@ class Model; namespace Ocpp16 { -class ReserveNow : public Operation { +class ReserveNow : public Operation, public MemoryManaged { private: Model& model; const char *errorCode = nullptr; @@ -27,7 +31,7 @@ class ReserveNow : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; @@ -35,4 +39,5 @@ class ReserveNow : public Operation { } //end namespace Ocpp16 } //end namespace MicroOcpp +#endif //MO_ENABLE_RESERVATION #endif diff --git a/src/MicroOcpp/Operations/Reset.cpp b/src/MicroOcpp/Operations/Reset.cpp index 9a799739..9ab42422 100644 --- a/src/MicroOcpp/Operations/Reset.cpp +++ b/src/MicroOcpp/Operations/Reset.cpp @@ -1,14 +1,16 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include +#include using MicroOcpp::Ocpp16::Reset; +using MicroOcpp::JsonDoc; -Reset::Reset(Model& model) : model(model) { +Reset::Reset(Model& model) : MemoryManaged("v16.Operation.", "Reset"), model(model) { } @@ -26,7 +28,6 @@ void Reset::processReq(JsonObject payload) { if (auto rService = model.getResetService()) { if (!rService->getExecuteReset()) { MO_DBG_ERR("No reset handler set. Abort operation"); - (void)0; //resetAccepted remains false } else { if (!rService->getPreReset() || rService->getPreReset()(isHard) || isHard) { @@ -42,9 +43,77 @@ void Reset::processReq(JsonObject payload) { } } -std::unique_ptr Reset::createConf() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr Reset::createConf() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = resetAccepted ? "Accepted" : "Rejected"; return doc; } + +#if MO_ENABLE_V201 + +#include + +namespace MicroOcpp { +namespace Ocpp201 { + +Reset::Reset(ResetService& resetService) : MemoryManaged("v201.Operation.", "Reset"), resetService(resetService) { + +} + +const char* Reset::getOperationType(){ + return "Reset"; +} + +void Reset::processReq(JsonObject payload) { + + ResetType type; + const char *typeCstr = payload["type"] | "_Undefined"; + + if (!strcmp(typeCstr, "Immediate")) { + type = ResetType_Immediate; + } else if (!strcmp(typeCstr, "OnIdle")) { + type = ResetType_OnIdle; + } else { + errorCode = "FormationViolation"; + return; + } + + int evseIdRaw = payload["evseId"] | 0; + + if (evseIdRaw < 0 || evseIdRaw >= MO_NUM_EVSEID) { + errorCode = "PropertyConstraintViolation"; + return; + } + + unsigned int evseId = (unsigned int) evseIdRaw; + + status = resetService.initiateReset(type, evseId); +} + +std::unique_ptr Reset::createConf() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + + const char *statusCstr = ""; + switch (status) { + case ResetStatus_Accepted: + statusCstr = "Accepted"; + break; + case ResetStatus_Rejected: + statusCstr = "Rejected"; + break; + case ResetStatus_Scheduled: + statusCstr = "Scheduled"; + break; + default: + MO_DBG_ERR("internal error"); + break; + } + payload["status"] = statusCstr; + return doc; +} + +} //end namespace Ocpp201 +} //end namespace MicroOcpp +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/Reset.h b/src/MicroOcpp/Operations/Reset.h index fd150fef..53aca0d0 100644 --- a/src/MicroOcpp/Operations/Reset.h +++ b/src/MicroOcpp/Operations/Reset.h @@ -1,11 +1,13 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef RESET_H #define RESET_H +#include #include +#include namespace MicroOcpp { @@ -13,7 +15,7 @@ class Model; namespace Ocpp16 { -class Reset : public Operation { +class Reset : public Operation, public MemoryManaged { private: Model& model; bool resetAccepted {false}; @@ -24,9 +26,38 @@ class Reset : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; }; } //end namespace Ocpp16 } //end namespace MicroOcpp + +#if MO_ENABLE_V201 + +namespace MicroOcpp { +namespace Ocpp201 { + +class ResetService; + +class Reset : public Operation, public MemoryManaged { +private: + ResetService& resetService; + ResetStatus status; + const char *errorCode = nullptr; +public: + Reset(ResetService& resetService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_V201 #endif diff --git a/src/MicroOcpp/Operations/SecurityEventNotification.cpp b/src/MicroOcpp/Operations/SecurityEventNotification.cpp new file mode 100644 index 00000000..d4837315 --- /dev/null +++ b/src/MicroOcpp/Operations/SecurityEventNotification.cpp @@ -0,0 +1,54 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include + +using MicroOcpp::Ocpp201::SecurityEventNotification; +using MicroOcpp::JsonDoc; + +SecurityEventNotification::SecurityEventNotification(const char *type, const Timestamp& timestamp) : MemoryManaged("v201.Operation.", "SecurityEventNotification"), type(makeString(getMemoryTag(), type ? type : "")), timestamp(timestamp) { + +} + +const char* SecurityEventNotification::getOperationType(){ + return "SecurityEventNotification"; +} + +std::unique_ptr SecurityEventNotification::createReq() { + + auto doc = makeJsonDoc(getMemoryTag(), + JSON_OBJECT_SIZE(2) + + JSONDATE_LENGTH + 1); + + JsonObject payload = doc->to(); + + payload["type"] = type.c_str(); + + char timestampStr [JSONDATE_LENGTH + 1]; + timestamp.toJsonString(timestampStr, sizeof(timestampStr)); + payload["timestamp"] = timestampStr; + + return doc; +} + +void SecurityEventNotification::processConf(JsonObject) { + //empty payload +} + +void SecurityEventNotification::processReq(JsonObject payload) { + /** + * Ignore Contents of this Req-message, because this is for debug purposes only + */ +} + +std::unique_ptr SecurityEventNotification::createConf() { + return createEmptyDocument(); +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/SecurityEventNotification.h b/src/MicroOcpp/Operations/SecurityEventNotification.h new file mode 100644 index 00000000..5d618c00 --- /dev/null +++ b/src/MicroOcpp/Operations/SecurityEventNotification.h @@ -0,0 +1,48 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_SECURITYEVENTNOTIFICATION_H +#define MO_SECURITYEVENTNOTIFICATION_H + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include + +namespace MicroOcpp { + +namespace Ocpp201 { + +class SecurityEventNotification : public Operation, public MemoryManaged { +private: + String type; + Timestamp timestamp; + + const char *errorCode = nullptr; +public: + SecurityEventNotification(const char *type, const Timestamp& timestamp); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + const char *getErrorCode() override {return errorCode;} + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + +}; + +} //namespace Ocpp201 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Operations/SendLocalList.cpp b/src/MicroOcpp/Operations/SendLocalList.cpp index 70895af5..29890024 100644 --- a/src/MicroOcpp/Operations/SendLocalList.cpp +++ b/src/MicroOcpp/Operations/SendLocalList.cpp @@ -1,14 +1,19 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + +#if MO_ENABLE_LOCAL_AUTH + #include #include #include using MicroOcpp::Ocpp16::SendLocalList; +using MicroOcpp::JsonDoc; -SendLocalList::SendLocalList(AuthorizationService& authService) : authService(authService) { +SendLocalList::SendLocalList(AuthorizationService& authService) : MemoryManaged("v16.Operation.", "SendLocalList"), authService(authService) { } @@ -21,12 +26,19 @@ const char* SendLocalList::getOperationType(){ } void SendLocalList::processReq(JsonObject payload) { + + //TC_043_1_CS Send Local Authorization List - NotSupported + if (!authService.localAuthListEnabled()) { + errorCode = "NotSupported"; + return; + } + if (!payload.containsKey("listVersion") || !payload.containsKey("updateType")) { errorCode = "FormationViolation"; return; } - if (!payload["localAuthorizationList"].is()) { + if (payload.containsKey("localAuthorizationList") && !payload["localAuthorizationList"].is()) { errorCode = "FormationViolation"; return; } @@ -50,8 +62,8 @@ void SendLocalList::processReq(JsonObject payload) { updateFailure = !authService.updateLocalList(localAuthorizationList, listVersion, differential); } -std::unique_ptr SendLocalList::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr SendLocalList::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (versionMismatch) { @@ -64,3 +76,5 @@ std::unique_ptr SendLocalList::createConf(){ return doc; } + +#endif //MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Operations/SendLocalList.h b/src/MicroOcpp/Operations/SendLocalList.h index 496b8126..d58265bd 100644 --- a/src/MicroOcpp/Operations/SendLocalList.h +++ b/src/MicroOcpp/Operations/SendLocalList.h @@ -1,9 +1,13 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef SENDLOCALLIST_H -#define SENDLOCALLIST_H +#ifndef MO_SENDLOCALLIST_H +#define MO_SENDLOCALLIST_H + +#include + +#if MO_ENABLE_LOCAL_AUTH #include @@ -13,7 +17,7 @@ class AuthorizationService; namespace Ocpp16 { -class SendLocalList : public Operation { +class SendLocalList : public Operation, public MemoryManaged { private: AuthorizationService& authService; const char *errorCode = nullptr; @@ -28,11 +32,13 @@ class SendLocalList : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp + +#endif //MO_ENABLE_LOCAL_AUTH #endif diff --git a/src/MicroOcpp/Operations/SetChargingProfile.cpp b/src/MicroOcpp/Operations/SetChargingProfile.cpp index 14b47605..1ff28acd 100644 --- a/src/MicroOcpp/Operations/SetChargingProfile.cpp +++ b/src/MicroOcpp/Operations/SetChargingProfile.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -9,8 +9,9 @@ #include using MicroOcpp::Ocpp16::SetChargingProfile; +using MicroOcpp::JsonDoc; -SetChargingProfile::SetChargingProfile(Model& model, SmartChargingService& scService) : model(model), scService(scService) { +SetChargingProfile::SetChargingProfile(Model& model, SmartChargingService& scService) : MemoryManaged("v16.Operation.", "SetChargingProfile"), model(model), scService(scService) { } @@ -57,12 +58,21 @@ void SetChargingProfile::processReq(JsonObject payload) { errorCode = "PropertyConstraintViolation"; return; } - if (!connector->getTransaction() || !connector->getTransaction()->isRunning()) { + + auto& transaction = connector->getTransaction(); + if (!transaction || !connector->getTransaction()->isRunning()) { //no transaction running, reject profile accepted = false; return; } + if (chargingProfile->getTransactionId() >= 0 && + chargingProfile->getTransactionId() != transaction->getTransactionId()) { + //transactionId undefined / mismatch + accepted = false; + return; + } + //seems good } else if (chargingProfile->getChargingProfilePurpose() == ChargingProfilePurposeType::ChargePointMaxProfile) { if (connectorId > 0) { @@ -75,8 +85,8 @@ void SetChargingProfile::processReq(JsonObject payload) { accepted = scService.setChargingProfile(connectorId, std::move(chargingProfile)); } -std::unique_ptr SetChargingProfile::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr SetChargingProfile::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); if (accepted) { payload["status"] = "Accepted"; diff --git a/src/MicroOcpp/Operations/SetChargingProfile.h b/src/MicroOcpp/Operations/SetChargingProfile.h index 3d0cb425..1ce35f1e 100644 --- a/src/MicroOcpp/Operations/SetChargingProfile.h +++ b/src/MicroOcpp/Operations/SetChargingProfile.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef SETCHARGINGPROFILE_H -#define SETCHARGINGPROFILE_H +#ifndef MO_SETCHARGINGPROFILE_H +#define MO_SETCHARGINGPROFILE_H #include @@ -14,7 +14,7 @@ class SmartChargingService; namespace Ocpp16 { -class SetChargingProfile : public Operation { +class SetChargingProfile : public Operation, public MemoryManaged { private: Model& model; SmartChargingService& scService; @@ -31,7 +31,7 @@ class SetChargingProfile : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} const char *getErrorDescription() override {return errorDescription;} diff --git a/src/MicroOcpp/Operations/SetVariables.cpp b/src/MicroOcpp/Operations/SetVariables.cpp new file mode 100644 index 00000000..23bd043d --- /dev/null +++ b/src/MicroOcpp/Operations/SetVariables.cpp @@ -0,0 +1,185 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include + +using MicroOcpp::Ocpp201::SetVariableData; +using MicroOcpp::Ocpp201::SetVariables; +using MicroOcpp::JsonDoc; + +SetVariableData::SetVariableData(const char *memory_tag) : componentName{makeString(memory_tag)}, variableName{makeString(memory_tag)} { + +} + +SetVariables::SetVariables(VariableService& variableService) : MemoryManaged("v201.Operation.", "SetVariables"), variableService(variableService), queries(makeVector(getMemoryTag())) { + +} + +const char* SetVariables::getOperationType(){ + return "SetVariables"; +} + +void SetVariables::processReq(JsonObject payload) { + for (JsonObject setVariable : payload["setVariableData"].as()) { + + queries.emplace_back(getMemoryTag()); + auto& data = queries.back(); + + if (setVariable.containsKey("attributeType")) { + const char *attributeTypeCstr = setVariable["attributeType"] | "_Undefined"; + if (!strcmp(attributeTypeCstr, "Actual")) { + data.attributeType = Variable::AttributeType::Actual; + } else if (!strcmp(attributeTypeCstr, "Target")) { + data.attributeType = Variable::AttributeType::Target; + } else if (!strcmp(attributeTypeCstr, "MinSet")) { + data.attributeType = Variable::AttributeType::MinSet; + } else if (!strcmp(attributeTypeCstr, "MaxSet")) { + data.attributeType = Variable::AttributeType::MaxSet; + } else { + errorCode = "FormationViolation"; + MO_DBG_ERR("invalid attributeType"); + return; + } + } + + const char *attributeValueCstr = setVariable["attributeValue"] | (const char*) nullptr; + const char *componentNameCstr = setVariable["component"]["name"] | (const char*) nullptr; + const char *variableNameCstr = setVariable["variable"]["name"] | (const char*) nullptr; + + if (!attributeValueCstr || + !componentNameCstr || + !variableNameCstr) { + errorCode = "FormationViolation"; + return; + } + + data.attributeValue = attributeValueCstr; + data.componentName = componentNameCstr; + data.variableName = variableNameCstr; + + // TODO check against ConfigurationValueSize + + data.componentEvseId = setVariable["component"]["evse"]["id"] | -1; + data.componentEvseConnectorId = setVariable["component"]["evse"]["connectorId"] | -1; + + if (setVariable["component"].containsKey("evse") && data.componentEvseId < 0) { + errorCode = "FormationViolation"; + MO_DBG_ERR("malformatted / missing evseId"); + return; + } + } + + if (queries.empty()) { + errorCode = "FormationViolation"; + return; + } + + MO_DBG_DEBUG("processing %zu setVariable queries", queries.size()); + + for (auto& query : queries) { + query.attributeStatus = variableService.setVariable( + query.attributeType, + query.attributeValue, + ComponentId(query.componentName.c_str(), + EvseId(query.componentEvseId, query.componentEvseConnectorId)), + query.variableName.c_str()); + } + + if (!variableService.commit()) { + errorCode = "InternalError"; + MO_DBG_ERR("Variables could not be stored. Rollback not possible"); + return; + } +} + +std::unique_ptr SetVariables::createConf(){ + size_t capacity = JSON_ARRAY_SIZE(queries.size()); + for (const auto& data : queries) { + capacity += + JSON_OBJECT_SIZE(5) + // setVariableResult + JSON_OBJECT_SIZE(2) + // component + data.componentName.length() + 1 + + JSON_OBJECT_SIZE(2) + // evse + JSON_OBJECT_SIZE(2) + // variable + data.variableName.length() + 1; + } + auto doc = makeJsonDoc(getMemoryTag(), capacity); + + JsonObject payload = doc->to(); + JsonArray setVariableResult = payload.createNestedArray("setVariableResult"); + + for (const auto& data : queries) { + JsonObject setVariable = setVariableResult.createNestedObject(); + + const char *attributeTypeCstr = nullptr; + switch (data.attributeType) { + case Variable::AttributeType::Actual: + // leave blank when Actual + break; + case Variable::AttributeType::Target: + attributeTypeCstr = "Target"; + break; + case Variable::AttributeType::MinSet: + attributeTypeCstr = "MinSet"; + break; + case Variable::AttributeType::MaxSet: + attributeTypeCstr = "MaxSet"; + break; + default: + MO_DBG_ERR("internal error"); + break; + } + if (attributeTypeCstr) { + setVariable["attributeType"] = attributeTypeCstr; + } + + const char *attributeStatusCstr = "Rejected"; + switch (data.attributeStatus) { + case SetVariableStatus::Accepted: + attributeStatusCstr = "Accepted"; + break; + case SetVariableStatus::Rejected: + attributeStatusCstr = "Rejected"; + break; + case SetVariableStatus::UnknownComponent: + attributeStatusCstr = "UnknownComponent"; + break; + case SetVariableStatus::UnknownVariable: + attributeStatusCstr = "UnknownVariable"; + break; + case SetVariableStatus::NotSupportedAttributeType: + attributeStatusCstr = "NotSupportedAttributeType"; + break; + case SetVariableStatus::RebootRequired: + attributeStatusCstr = "RebootRequired"; + break; + default: + MO_DBG_ERR("internal error"); + break; + } + setVariable["attributeStatus"] = attributeStatusCstr; + + setVariable["component"]["name"] = (char*) data.componentName.c_str(); // force copy-mode + + if (data.componentEvseId >= 0) { + setVariable["component"]["evse"]["id"] = data.componentEvseId; + } + + if (data.componentEvseConnectorId >= 0) { + setVariable["component"]["evse"]["connectorId"] = data.componentEvseConnectorId; + } + + setVariable["variable"]["name"] = (char*) data.variableName.c_str(); // force copy-mode + } + + return doc; +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/SetVariables.h b/src/MicroOcpp/Operations/SetVariables.h new file mode 100644 index 00000000..fb0ca889 --- /dev/null +++ b/src/MicroOcpp/Operations/SetVariables.h @@ -0,0 +1,63 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_SETVARIABLES_H +#define MO_SETVARIABLES_H + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include + +namespace MicroOcpp { + +class VariableService; + +namespace Ocpp201 { + +// SetVariableDataType (2.44) and +// SetVariableResultType (2.45) +struct SetVariableData { + // SetVariableDataType + Variable::AttributeType attributeType = Variable::AttributeType::Actual; + const char *attributeValue; // will become invalid after processReq + String componentName; + int componentEvseId = -1; + int componentEvseConnectorId = -1; + String variableName; + + // SetVariableResultType + SetVariableStatus attributeStatus; + + SetVariableData(const char *memory_tag = nullptr); +}; + +class SetVariables : public Operation, public MemoryManaged { +private: + VariableService& variableService; + Vector queries; + + const char *errorCode = nullptr; +public: + SetVariables(VariableService& variableService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} + +}; + +} //namespace Ocpp201 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Operations/StartTransaction.cpp b/src/MicroOcpp/Operations/StartTransaction.cpp index 80ecb961..907baaf3 100644 --- a/src/MicroOcpp/Operations/StartTransaction.cpp +++ b/src/MicroOcpp/Operations/StartTransaction.cpp @@ -1,20 +1,21 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include -#include #include #include #include #include #include +#include using MicroOcpp::Ocpp16::StartTransaction; +using MicroOcpp::JsonDoc; -StartTransaction::StartTransaction(Model& model, std::shared_ptr transaction) : model(model), transaction(transaction) { +StartTransaction::StartTransaction(Model& model, std::shared_ptr transaction) : MemoryManaged("v16.Operation.", "StartTransaction"), model(model), transaction(transaction) { } @@ -26,90 +27,12 @@ const char* StartTransaction::getOperationType() { return "StartTransaction"; } -void StartTransaction::initiate(StoredOperationHandler *opStore) { - if (!transaction || transaction->getStartSync().isRequested()) { - MO_DBG_ERR("initialization error"); - return; - } - - auto payload = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); - (*payload)["connectorId"] = transaction->getConnectorId(); - (*payload)["txNr"] = transaction->getTxNr(); - - if (opStore) { - opStore->setPayload(std::move(payload)); - opStore->commit(); - } - - transaction->getStartSync().setRequested(); - - transaction->commit(); - - MO_DBG_INFO("StartTransaction initiated"); -} - -bool StartTransaction::restore(StoredOperationHandler *opStore) { - if (!opStore) { - MO_DBG_ERR("invalid argument"); - return false; - } - - auto payload = opStore->getPayload(); - if (!payload) { - MO_DBG_ERR("memory corruption"); - return false; - } - - int connectorId = (*payload)["connectorId"] | -1; - int txNr = (*payload)["txNr"] | -1; - if (connectorId < 0 || txNr < 0) { - MO_DBG_ERR("record incomplete"); - return false; - } +std::unique_ptr StartTransaction::createReq() { - auto txStore = model.getTransactionStore(); - - if (!txStore) { - MO_DBG_ERR("invalid state"); - return false; - } - - transaction = txStore->getTransaction(connectorId, txNr); - if (!transaction) { - MO_DBG_ERR("referential integrity violation"); - - //clean up possible tx records - if (auto mSerivce = model.getMeteringService()) { - mSerivce->removeTxMeterData(connectorId, txNr); - } - return false; - } - - if (transaction->getStartTimestamp() < MIN_TIME && - transaction->getStartBootNr() != model.getBootNr()) { - //time not set, cannot be restored anymore -> invalid tx - MO_DBG_ERR("cannot recover tx from previus run"); - - //clean up possible tx records - if (auto mSerivce = model.getMeteringService()) { - mSerivce->removeTxMeterData(connectorId, txNr); - } - - transaction->setSilent(); - transaction->setInactive(); - transaction->commit(); - return false; - } - - return true; -} - -std::unique_ptr StartTransaction::createReq() { - - auto doc = std::unique_ptr(new DynamicJsonDocument( + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(6) + (IDTAG_LEN_MAX + 1) + - (JSONDATE_LENGTH + 1))); + (JSONDATE_LENGTH + 1)); JsonObject payload = doc->to(); @@ -148,12 +71,19 @@ void StartTransaction::processConf(JsonObject payload) { int transactionId = payload["transactionId"] | -1; transaction->setTransactionId(transactionId); + if (payload["idTagInfo"].containsKey("parentIdTag")) + { + transaction->setParentIdTag(payload["idTagInfo"]["parentIdTag"]); + } + transaction->getStartSync().confirm(); transaction->commit(); +#if MO_ENABLE_LOCAL_AUTH if (auto authService = model.getAuthorizationService()) { authService->notifyAuthorization(transaction->getIdTag(), payload["idTagInfo"]); } +#endif //MO_ENABLE_LOCAL_AUTH } void StartTransaction::processReq(JsonObject payload) { @@ -164,8 +94,8 @@ void StartTransaction::processReq(JsonObject payload) { } -std::unique_ptr StartTransaction::createConf() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2))); +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"); diff --git a/src/MicroOcpp/Operations/StartTransaction.h b/src/MicroOcpp/Operations/StartTransaction.h index f4e22311..a73b5368 100644 --- a/src/MicroOcpp/Operations/StartTransaction.h +++ b/src/MicroOcpp/Operations/StartTransaction.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef STARTTRANSACTION_H -#define STARTTRANSACTION_H +#ifndef MO_STARTTRANSACTION_H +#define MO_STARTTRANSACTION_H #include #include @@ -17,7 +17,7 @@ class Transaction; namespace Ocpp16 { -class StartTransaction : public Operation { +class StartTransaction : public Operation, public MemoryManaged { private: Model& model; std::shared_ptr transaction; @@ -29,17 +29,13 @@ class StartTransaction : public Operation { const char* getOperationType() override; - void initiate(StoredOperationHandler *opStore) override; - - bool restore(StoredOperationHandler *opStore) override; - - std::unique_ptr createReq() override; + std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; }; } //end namespace Ocpp16 diff --git a/src/MicroOcpp/Operations/StatusNotification.cpp b/src/MicroOcpp/Operations/StatusNotification.cpp index ada65894..c5fb1a60 100644 --- a/src/MicroOcpp/Operations/StatusNotification.cpp +++ b/src/MicroOcpp/Operations/StatusNotification.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -8,47 +8,48 @@ #include -using MicroOcpp::Ocpp16::StatusNotification; +namespace MicroOcpp { //helper function -namespace MicroOcpp { -namespace Ocpp16 { const char *cstrFromOcppEveState(ChargePointStatus state) { switch (state) { - case (ChargePointStatus::Available): + case (ChargePointStatus_Available): return "Available"; - case (ChargePointStatus::Preparing): + case (ChargePointStatus_Preparing): return "Preparing"; - case (ChargePointStatus::Charging): + case (ChargePointStatus_Charging): return "Charging"; - case (ChargePointStatus::SuspendedEVSE): + case (ChargePointStatus_SuspendedEVSE): return "SuspendedEVSE"; - case (ChargePointStatus::SuspendedEV): + case (ChargePointStatus_SuspendedEV): return "SuspendedEV"; - case (ChargePointStatus::Finishing): + case (ChargePointStatus_Finishing): return "Finishing"; - case (ChargePointStatus::Reserved): + case (ChargePointStatus_Reserved): return "Reserved"; - case (ChargePointStatus::Unavailable): + case (ChargePointStatus_Unavailable): return "Unavailable"; - case (ChargePointStatus::Faulted): + case (ChargePointStatus_Faulted): return "Faulted"; +#if MO_ENABLE_V201 + case (ChargePointStatus_Occupied): + return "Occupied"; +#endif default: MO_DBG_ERR("ChargePointStatus not specified"); - (void)0; /* fall through */ - case (ChargePointStatus::NOT_SET): - return "NOT_SET"; + case (ChargePointStatus_UNDEFINED): + return "UNDEFINED"; } } -}} //end namespaces + +namespace Ocpp16 { StatusNotification::StatusNotification(int connectorId, ChargePointStatus currentStatus, const Timestamp ×tamp, ErrorData errorData) - : connectorId(connectorId), currentStatus(currentStatus), timestamp(timestamp), errorData(errorData) { + : MemoryManaged("v16.Operation.", "StatusNotification"), connectorId(connectorId), currentStatus(currentStatus), timestamp(timestamp), errorData(errorData) { - if (currentStatus != ChargePointStatus::NOT_SET) { + if (currentStatus != ChargePointStatus_UNDEFINED) { MO_DBG_INFO("New status: %s (connectorId %d)", cstrFromOcppEveState(currentStatus), connectorId); - (void)0; } } @@ -56,11 +57,10 @@ const char* StatusNotification::getOperationType(){ return "StatusNotification"; } -//TODO if the status has changed again when sendReq() is called, abort the operation completely (note: if req is already sent, stick with listening to conf). The ChargePointStatusService will enqueue a new operation itself -std::unique_ptr StatusNotification::createReq() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(7) + (JSONDATE_LENGTH + 1))); +std::unique_ptr StatusNotification::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(7) + (JSONDATE_LENGTH + 1)); JsonObject payload = doc->to(); - + payload["connectorId"] = connectorId; if (errorData.isError) { if (errorData.errorCode) { @@ -75,7 +75,7 @@ std::unique_ptr StatusNotification::createReq() { if (errorData.vendorErrorCode) { payload["vendorErrorCode"] = errorData.vendorErrorCode; } - } else if (currentStatus == ChargePointStatus::NOT_SET) { + } else if (currentStatus == ChargePointStatus_UNDEFINED) { MO_DBG_ERR("Reporting undefined status"); payload["errorCode"] = "InternalError"; } else { @@ -91,7 +91,6 @@ std::unique_ptr StatusNotification::createReq() { return doc; } - void StatusNotification::processConf(JsonObject payload) { /* * Empty payload @@ -108,6 +107,49 @@ void StatusNotification::processReq(JsonObject payload) { /* * For debugging only */ -std::unique_ptr StatusNotification::createConf(){ +std::unique_ptr StatusNotification::createConf(){ return createEmptyDocument(); } + +} // namespace Ocpp16 +} // namespace MicroOcpp + +#if MO_ENABLE_V201 + +namespace MicroOcpp { +namespace Ocpp201 { + +StatusNotification::StatusNotification(EvseId evseId, ChargePointStatus currentStatus, const Timestamp ×tamp) + : MemoryManaged("v201.Operation.", "StatusNotification"), evseId(evseId), timestamp(timestamp), currentStatus(currentStatus) { + +} + +const char* StatusNotification::getOperationType(){ + return "StatusNotification"; +} + +std::unique_ptr StatusNotification::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(4) + (JSONDATE_LENGTH + 1)); + JsonObject payload = doc->to(); + + char timestamp_cstr[JSONDATE_LENGTH + 1] = {'\0'}; + timestamp.toJsonString(timestamp_cstr, JSONDATE_LENGTH + 1); + payload["timestamp"] = timestamp_cstr; + payload["connectorStatus"] = cstrFromOcppEveState(currentStatus); + payload["evseId"] = evseId.id; + payload["connectorId"] = evseId.id == 0 ? 0 : evseId.connectorId >= 0 ? evseId.connectorId : 1; + + return doc; +} + + +void StatusNotification::processConf(JsonObject payload) { + /* + * Empty payload + */ +} + +} // namespace Ocpp201 +} // namespace MicroOcpp + +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/StatusNotification.h b/src/MicroOcpp/Operations/StatusNotification.h index 74f329d6..2e65dcec 100644 --- a/src/MicroOcpp/Operations/StatusNotification.h +++ b/src/MicroOcpp/Operations/StatusNotification.h @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef STATUSNOTIFICATION_H @@ -9,14 +9,18 @@ #include #include #include +#include namespace MicroOcpp { + +const char *cstrFromOcppEveState(ChargePointStatus state); + namespace Ocpp16 { -class StatusNotification : public Operation { +class StatusNotification : public Operation, public MemoryManaged { private: int connectorId = 1; - ChargePointStatus currentStatus = ChargePointStatus::NOT_SET; + ChargePointStatus currentStatus = ChargePointStatus_UNDEFINED; Timestamp timestamp; ErrorData errorData; public: @@ -24,17 +28,47 @@ class StatusNotification : public Operation { const char* getOperationType() override; - std::unique_ptr createReq() override; + std::unique_ptr createReq() override; void processConf(JsonObject payload) override; void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; + + int getConnectorId() { + return connectorId; + } }; -const char *cstrFromOcppEveState(ChargePointStatus state); +} // namespace Ocpp16 +} // namespace MicroOcpp + +#if MO_ENABLE_V201 + +#include + +namespace MicroOcpp { +namespace Ocpp201 { + +class StatusNotification : public Operation, public MemoryManaged { +private: + EvseId evseId; + Timestamp timestamp; + ChargePointStatus currentStatus = ChargePointStatus_UNDEFINED; +public: + StatusNotification(EvseId evseId, ChargePointStatus currentStatus, const Timestamp ×tamp); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; +}; + +} // namespace Ocpp201 +} // namespace MicroOcpp + +#endif //MO_ENABLE_V201 -} //end namespace Ocpp16 -} //end namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Operations/StopTransaction.cpp b/src/MicroOcpp/Operations/StopTransaction.cpp index 4eec404a..a3aef101 100644 --- a/src/MicroOcpp/Operations/StopTransaction.cpp +++ b/src/MicroOcpp/Operations/StopTransaction.cpp @@ -1,26 +1,27 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include -#include #include #include #include #include #include #include +#include using MicroOcpp::Ocpp16::StopTransaction; +using MicroOcpp::JsonDoc; StopTransaction::StopTransaction(Model& model, std::shared_ptr transaction) - : model(model), transaction(transaction) { + : MemoryManaged("v16.Operation.", "StopTransaction"), model(model), transaction(transaction) { } -StopTransaction::StopTransaction(Model& model, std::shared_ptr transaction, std::vector> transactionData) - : model(model), transaction(transaction), transactionData(std::move(transactionData)) { +StopTransaction::StopTransaction(Model& model, std::shared_ptr transaction, Vector> transactionData) + : MemoryManaged("v16.Operation.", "StopTransaction"), model(model), transaction(transaction), transactionData(std::move(transactionData)) { } @@ -28,86 +29,7 @@ const char* StopTransaction::getOperationType() { return "StopTransaction"; } -void StopTransaction::initiate(StoredOperationHandler *opStore) { - if (!transaction || transaction->getStopSync().isRequested()) { - MO_DBG_ERR("initialization error"); - return; - } - - auto payload = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); - (*payload)["connectorId"] = transaction->getConnectorId(); - (*payload)["txNr"] = transaction->getTxNr(); - - if (opStore) { - opStore->setPayload(std::move(payload)); - opStore->commit(); - } - - transaction->getStopSync().setRequested(); - - transaction->commit(); - - MO_DBG_INFO("StopTransaction initiated"); -} - -bool StopTransaction::restore(StoredOperationHandler *opStore) { - if (!opStore) { - MO_DBG_ERR("invalid argument"); - return false; - } - - auto payload = opStore->getPayload(); - if (!payload) { - MO_DBG_ERR("memory corruption"); - return false; - } - - int connectorId = (*payload)["connectorId"] | -1; - int txNr = (*payload)["txNr"] | -1; - if (connectorId < 0 || txNr < 0) { - MO_DBG_ERR("record incomplete"); - return false; - } - - auto txStore = model.getTransactionStore(); - - if (!txStore) { - MO_DBG_ERR("invalid state"); - return false; - } - - transaction = txStore->getTransaction(connectorId, txNr); - if (!transaction) { - MO_DBG_ERR("referential integrity violation"); - - //clean up possible tx records - if (auto mSerivce = model.getMeteringService()) { - mSerivce->removeTxMeterData(connectorId, txNr); - } - return false; - } - - if (transaction->isSilent()) { - //transaction has been set silent after initializing StopTx - discard operation record - MO_DBG_WARN("tx has been set silent - discard StopTx"); - - //clean up possible tx records - if (auto mSerivce = model.getMeteringService()) { - mSerivce->removeTxMeterData(connectorId, txNr); - } - return false; - } - - if (auto mSerivce = model.getMeteringService()) { - if (auto txData = mSerivce->getStopTxMeterData(transaction.get())) { - transactionData = txData->retrieveStopTxData(); - } - } - - return true; -} - -std::unique_ptr StopTransaction::createReq() { +std::unique_ptr StopTransaction::createReq() { /* * Adjust timestamps in case they were taken before initial Clock setting @@ -123,16 +45,22 @@ std::unique_ptr StopTransaction::createReq() { transaction->setStopTimestamp(transaction->getStartTimestamp() + 1); //1s behind startTime to keep order in backend DB } else { MO_DBG_ERR("failed to determine StopTx timestamp"); - (void)0; //send invalid value + //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) { + if ((*mv)->getReadingContext() == ReadingContext_TransactionBegin) { (*mv)->setTimestamp(transaction->getStartTimestamp()); - } else if ((*mv)->getReadingContext() == ReadingContext::TransactionEnd) { + } else if ((*mv)->getReadingContext() == ReadingContext_TransactionEnd) { (*mv)->setTimestamp(transaction->getStopTimestamp()); } else { (*mv)->setTimestamp(transaction->getStartTimestamp() + 1); @@ -140,7 +68,7 @@ std::unique_ptr StopTransaction::createReq() { } } - std::vector> txDataJson; + auto txDataJson = makeVector>(getMemoryTag()); size_t txDataJson_size = 0; for (auto mv = transactionData.begin(); mv != transactionData.end(); mv++) { auto mvJson = (*mv)->toJson(); @@ -151,17 +79,17 @@ std::unique_ptr StopTransaction::createReq() { txDataJson.emplace_back(std::move(mvJson)); } - DynamicJsonDocument txDataDoc = DynamicJsonDocument(JSON_ARRAY_SIZE(txDataJson.size()) + txDataJson_size); + 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 = std::unique_ptr(new DynamicJsonDocument( + 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())); + txDataDoc.capacity()); JsonObject payload = doc->to(); if (transaction->getStopIdTag() && *transaction->getStopIdTag()) { @@ -196,9 +124,11 @@ void StopTransaction::processConf(JsonObject payload) { MO_DBG_INFO("Request has been accepted!"); +#if MO_ENABLE_LOCAL_AUTH if (auto authService = model.getAuthorizationService()) { authService->notifyAuthorization(transaction->getIdTag(), payload["idTagInfo"]); } +#endif //MO_ENABLE_LOCAL_AUTH } bool StopTransaction::processErr(const char *code, const char *description, JsonObject details) { @@ -219,8 +149,8 @@ void StopTransaction::processReq(JsonObject payload) { */ } -std::unique_ptr StopTransaction::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(2 * JSON_OBJECT_SIZE(1))); +std::unique_ptr StopTransaction::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), 2 * JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); diff --git a/src/MicroOcpp/Operations/StopTransaction.h b/src/MicroOcpp/Operations/StopTransaction.h index 49cf7fd5..c573135c 100644 --- a/src/MicroOcpp/Operations/StopTransaction.h +++ b/src/MicroOcpp/Operations/StopTransaction.h @@ -1,14 +1,14 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef STOPTRANSACTION_H -#define STOPTRANSACTION_H +#ifndef MO_STOPTRANSACTION_H +#define MO_STOPTRANSACTION_H #include +#include #include #include -#include namespace MicroOcpp { @@ -21,24 +21,20 @@ class Transaction; namespace Ocpp16 { -class StopTransaction : public Operation { +class StopTransaction : public Operation, public MemoryManaged { private: Model& model; std::shared_ptr transaction; - std::vector> transactionData; + Vector> transactionData; public: StopTransaction(Model& model, std::shared_ptr transaction); - StopTransaction(Model& model, std::shared_ptr transaction, std::vector> transactionData); + StopTransaction(Model& model, std::shared_ptr transaction, Vector> transactionData); const char* getOperationType() override; - void initiate(StoredOperationHandler *opStore) override; - - bool restore(StoredOperationHandler *opStore) override; - - std::unique_ptr createReq() override; + std::unique_ptr createReq() override; void processConf(JsonObject payload) override; @@ -46,7 +42,7 @@ class StopTransaction : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; }; } //end namespace Ocpp16 diff --git a/src/MicroOcpp/Operations/TransactionEvent.cpp b/src/MicroOcpp/Operations/TransactionEvent.cpp new file mode 100644 index 00000000..12d91afb --- /dev/null +++ b/src/MicroOcpp/Operations/TransactionEvent.cpp @@ -0,0 +1,152 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include +#include + +using namespace MicroOcpp::Ocpp201; +using MicroOcpp::JsonDoc; + +TransactionEvent::TransactionEvent(Model& model, TransactionEventData *txEvent) + : MemoryManaged("v201.Operation.", "TransactionEvent"), model(model), txEvent(txEvent) { + +} + +const char* TransactionEvent::getOperationType() { + return "TransactionEvent"; +} + +std::unique_ptr TransactionEvent::createReq() { + + size_t capacity = 0; + + if (txEvent->eventType == TransactionEventData::Type::Ended) { + for (size_t i = 0; i < txEvent->transaction->sampledDataTxEnded.size(); i++) { + JsonDoc meterValueJson = initJsonDoc(getMemoryTag()); //just measure, create again for serialization later + txEvent->transaction->sampledDataTxEnded[i]->toJson(meterValueJson); + capacity += meterValueJson.capacity(); + } + } + + for (size_t i = 0; i < txEvent->meterValue.size(); i++) { + JsonDoc meterValueJson = initJsonDoc(getMemoryTag()); //just measure, create again for serialization later + txEvent->meterValue[i]->toJson(meterValueJson); + capacity += meterValueJson.capacity(); + } + + capacity += + JSON_OBJECT_SIZE(12) + //total of 12 fields + JSONDATE_LENGTH + 1 + //timestamp string + JSON_OBJECT_SIZE(5) + //transactionInfo + MO_TXID_LEN_MAX + 1 + //transactionId + MO_IDTOKEN_LEN_MAX + 1; //idToken + + auto doc = makeJsonDoc(getMemoryTag(), capacity); + JsonObject payload = doc->to(); + + payload["eventType"] = serializeTransactionEventType(txEvent->eventType); + + char timestamp [JSONDATE_LENGTH + 1]; + txEvent->timestamp.toJsonString(timestamp, JSONDATE_LENGTH + 1); + payload["timestamp"] = timestamp; + + if (serializeTransactionEventTriggerReason(txEvent->triggerReason)) { + payload["triggerReason"] = serializeTransactionEventTriggerReason(txEvent->triggerReason); + } else { + MO_DBG_ERR("serialization error"); + } + + payload["seqNo"] = txEvent->seqNo; + + if (txEvent->offline) { + payload["offline"] = txEvent->offline; + } + + if (txEvent->numberOfPhasesUsed >= 0) { + payload["numberOfPhasesUsed"] = txEvent->numberOfPhasesUsed; + } + + if (txEvent->cableMaxCurrent >= 0) { + payload["cableMaxCurrent"] = txEvent->cableMaxCurrent; + } + + if (txEvent->reservationId >= 0) { + payload["reservationId"] = txEvent->reservationId; + } + + JsonObject transactionInfo = payload.createNestedObject("transactionInfo"); + transactionInfo["transactionId"] = txEvent->transaction->transactionId; + + if (serializeTransactionEventChargingState(txEvent->chargingState)) { // optional + transactionInfo["chargingState"] = serializeTransactionEventChargingState(txEvent->chargingState); + } + + if (txEvent->transaction->stoppedReason != Transaction::StoppedReason::Local && + serializeTransactionStoppedReason(txEvent->transaction->stoppedReason)) { // optional + transactionInfo["stoppedReason"] = serializeTransactionStoppedReason(txEvent->transaction->stoppedReason); + } + + if (txEvent->remoteStartId >= 0) { + transactionInfo["remoteStartId"] = txEvent->transaction->remoteStartId; + } + + if (txEvent->idToken) { + JsonObject idToken = payload.createNestedObject("idToken"); + idToken["idToken"] = txEvent->idToken->get(); + idToken["type"] = txEvent->idToken->getTypeCstr(); + } + + if (txEvent->evse.id >= 0) { + JsonObject evse = payload.createNestedObject("evse"); + evse["id"] = txEvent->evse.id; + if (txEvent->evse.connectorId >= 0) { + evse["connectorId"] = txEvent->evse.connectorId; + } + } + + if (txEvent->eventType == TransactionEventData::Type::Ended) { + for (size_t i = 0; i < txEvent->transaction->sampledDataTxEnded.size(); i++) { + JsonDoc meterValueJson = initJsonDoc(getMemoryTag()); + txEvent->transaction->sampledDataTxEnded[i]->toJson(meterValueJson); + payload["meterValue"].add(meterValueJson); + } + } + + for (size_t i = 0; i < txEvent->meterValue.size(); i++) { + JsonDoc meterValueJson = initJsonDoc(getMemoryTag()); + txEvent->meterValue[i]->toJson(meterValueJson); + payload["meterValue"].add(meterValueJson); + } + + return doc; +} + +void TransactionEvent::processConf(JsonObject payload) { + + if (payload.containsKey("idTokenInfo")) { + if (strcmp(payload["idTokenInfo"]["status"], "Accepted")) { + MO_DBG_INFO("transaction deAuthorized"); + txEvent->transaction->active = false; + txEvent->transaction->isDeauthorized = true; + } + } +} + +void TransactionEvent::processReq(JsonObject payload) { + /** + * Ignore Contents of this Req-message, because this is for debug purposes only + */ +} + +std::unique_ptr TransactionEvent::createConf() { + return createEmptyDocument(); +} + +#endif // MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/TransactionEvent.h b/src/MicroOcpp/Operations/TransactionEvent.h new file mode 100644 index 00000000..af8bbf86 --- /dev/null +++ b/src/MicroOcpp/Operations/TransactionEvent.h @@ -0,0 +1,48 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_TRANSACTIONEVENT_H +#define MO_TRANSACTIONEVENT_H + +#include + +#if MO_ENABLE_V201 + +#include + +namespace MicroOcpp { + +class Model; + +namespace Ocpp201 { + +class TransactionEventData; + +class TransactionEvent : public Operation, public MemoryManaged { +private: + Model& model; + TransactionEventData *txEvent; + + const char *errorCode = nullptr; +public: + + TransactionEvent(Model& model, TransactionEventData *txEvent); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + const char *getErrorCode() override {return errorCode;} + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp +#endif // MO_ENABLE_V201 +#endif diff --git a/src/MicroOcpp/Operations/TriggerMessage.cpp b/src/MicroOcpp/Operations/TriggerMessage.cpp index 7e47a64f..9b930697 100644 --- a/src/MicroOcpp/Operations/TriggerMessage.cpp +++ b/src/MicroOcpp/Operations/TriggerMessage.cpp @@ -1,19 +1,19 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #include #include -#include #include #include -#include +#include #include using MicroOcpp::Ocpp16::TriggerMessage; +using MicroOcpp::JsonDoc; -TriggerMessage::TriggerMessage(Context& context) : context(context) { +TriggerMessage::TriggerMessage(Context& context) : MemoryManaged("v16.Operation.", "TriggerMessage"), context(context) { } @@ -35,12 +35,16 @@ void TriggerMessage::processReq(JsonObject payload) { if (connectorId < 0) { auto nConnectors = mService->getNumConnectors(); for (decltype(nConnectors) cId = 0; cId < nConnectors; cId++) { - context.initiatePreBootOperation(mService->takeTriggeredMeterValues(cId)); - statusMessage = "Accepted"; + if (auto meterValues = mService->takeTriggeredMeterValues(cId)) { + context.getRequestQueue().sendRequestPreBoot(std::move(meterValues)); + statusMessage = "Accepted"; + } } } else if (connectorId < mService->getNumConnectors()) { - context.initiatePreBootOperation(mService->takeTriggeredMeterValues(connectorId)); - statusMessage = "Accepted"; + if (auto meterValues = mService->takeTriggeredMeterValues(connectorId)) { + context.getRequestQueue().sendRequestPreBoot(std::move(meterValues)); + statusMessage = "Accepted"; + } } else { errorCode = "PropertyConstraintViolation"; } @@ -58,21 +62,14 @@ void TriggerMessage::processReq(JsonObject payload) { for (auto i = cIdRangeBegin; i < cIdRangeEnd; i++) { auto connector = context.getModel().getConnector(i); - - auto statusNotification = makeRequest(new Ocpp16::StatusNotification( - i, - connector->getStatus(), //will be determined in StatusNotification::initiate - context.getModel().getClock().now())); - - statusNotification->setTimeout(60000); - - context.initiatePreBootOperation(std::move(statusNotification)); - statusMessage = "Accepted"; + if (connector->triggerStatusNotification()) { + statusMessage = "Accepted"; + } } } else { auto msg = context.getOperationRegistry().deserializeOperation(requestedMessage); if (msg) { - context.initiatePreBootOperation(std::move(msg)); + context.getRequestQueue().sendRequestPreBoot(std::move(msg)); statusMessage = "Accepted"; } else { statusMessage = "NotImplemented"; @@ -80,8 +77,8 @@ void TriggerMessage::processReq(JsonObject payload) { } } -std::unique_ptr TriggerMessage::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); +std::unique_ptr TriggerMessage::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); payload["status"] = statusMessage; return doc; diff --git a/src/MicroOcpp/Operations/TriggerMessage.h b/src/MicroOcpp/Operations/TriggerMessage.h index b6f91025..7869e5a5 100644 --- a/src/MicroOcpp/Operations/TriggerMessage.h +++ b/src/MicroOcpp/Operations/TriggerMessage.h @@ -1,13 +1,12 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef TRIGGERMESSAGE_H -#define TRIGGERMESSAGE_H +#ifndef MO_TRIGGERMESSAGE_H +#define MO_TRIGGERMESSAGE_H #include - -#include +#include namespace MicroOcpp { @@ -15,7 +14,7 @@ class Context; namespace Ocpp16 { -class TriggerMessage : public Operation { +class TriggerMessage : public Operation, public MemoryManaged { private: Context& context; const char *statusMessage = nullptr; @@ -28,7 +27,7 @@ class TriggerMessage : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; diff --git a/src/MicroOcpp/Operations/UnlockConnector.cpp b/src/MicroOcpp/Operations/UnlockConnector.cpp index a46acf15..a64b7bd4 100644 --- a/src/MicroOcpp/Operations/UnlockConnector.cpp +++ b/src/MicroOcpp/Operations/UnlockConnector.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -7,10 +7,9 @@ #include using MicroOcpp::Ocpp16::UnlockConnector; +using MicroOcpp::JsonDoc; -#define MO_UNLOCK_TIMEOUT 10000 - -UnlockConnector::UnlockConnector(Model& model) : model(model) { +UnlockConnector::UnlockConnector(Model& model) : MemoryManaged("v16.Operation.", "UnlockConnector"), model(model) { } @@ -19,51 +18,147 @@ const char* UnlockConnector::getOperationType(){ } void UnlockConnector::processReq(JsonObject payload) { - - auto connectorId = payload["connectorId"] | -1; - auto connector = model.getConnector(connectorId); +#if MO_ENABLE_CONNECTOR_LOCK + { + auto connectorId = payload["connectorId"] | -1; - if (!connector) { - err = true; - return; - } + auto connector = model.getConnector(connectorId); + + if (!connector) { + // NotSupported + return; + } + + unlockConnector = connector->getOnUnlockConnector(); + + if (!unlockConnector) { + // NotSupported + return; + } - connector->endTransaction(nullptr, "UnlockCommand"); - connector->updateTxNotification(TxNotification::RemoteStop); + connector->endTransaction(nullptr, "UnlockCommand"); + connector->updateTxNotification(TxNotification_RemoteStop); - unlockConnector = connector->getOnUnlockConnector(); - if (unlockConnector != nullptr) { cbUnlockResult = unlockConnector(); - } else { - MO_DBG_WARN("Unlock CB undefined"); - } - timerStart = mocpp_tick_ms(); + timerStart = mocpp_tick_ms(); + } +#endif //MO_ENABLE_CONNECTOR_LOCK } -std::unique_ptr UnlockConnector::createConf() { - if (!err && mocpp_tick_ms() - timerStart < MO_UNLOCK_TIMEOUT) { - //do poll and if more time is needed, delay creation of conf msg +std::unique_ptr UnlockConnector::createConf() { + + const char *status = "NotSupported"; + +#if MO_ENABLE_CONNECTOR_LOCK + if (unlockConnector) { + + if (mocpp_tick_ms() - timerStart < MO_UNLOCK_TIMEOUT) { + //do poll and if more time is needed, delay creation of conf msg - if (unlockConnector) { - if (!cbUnlockResult) { + if (cbUnlockResult == UnlockConnectorResult_Pending) { cbUnlockResult = unlockConnector(); - if (!cbUnlockResult) { + if (cbUnlockResult == UnlockConnectorResult_Pending) { return nullptr; //no result yet - delay confirmation response } } } + + if (cbUnlockResult == UnlockConnectorResult_Unlocked) { + status = "Unlocked"; + } else { + status = "UnlockFailed"; + } } +#endif //MO_ENABLE_CONNECTOR_LOCK - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); JsonObject payload = doc->to(); - if (err || !unlockConnector) { - payload["status"] = "NotSupported"; - } else if (cbUnlockResult && cbUnlockResult.toValue()) { - payload["status"] = "Unlocked"; - } else { - payload["status"] = "UnlockFailed"; + payload["status"] = status; + return doc; +} + + +#if MO_ENABLE_V201 +#if MO_ENABLE_CONNECTOR_LOCK + +#include + +namespace MicroOcpp { +namespace Ocpp201 { + +UnlockConnector::UnlockConnector(RemoteControlService& rcService) : MemoryManaged("v201.Operation.UnlockConnector"), rcService(rcService) { + +} + +const char* UnlockConnector::getOperationType(){ + return "UnlockConnector"; +} + +void UnlockConnector::processReq(JsonObject payload) { + + int evseId = payload["evseId"] | -1; + int connectorId = payload["connectorId"] | -1; + + if (evseId < 1 || evseId >= MO_NUM_EVSEID || connectorId < 1) { + errorCode = "PropertyConstraintViolation"; + return; } + + if (connectorId != 1) { + status = UnlockStatus_UnknownConnector; + return; + } + + rcEvse = rcService.getEvse(evseId); + if (!rcEvse) { + status = UnlockStatus_UnlockFailed; + return; + } + + status = rcEvse->unlockConnector(); + timerStart = mocpp_tick_ms(); +} + +std::unique_ptr UnlockConnector::createConf() { + + if (rcEvse && status == UnlockStatus_PENDING && mocpp_tick_ms() - timerStart < MO_UNLOCK_TIMEOUT) { + status = rcEvse->unlockConnector(); + + if (status == UnlockStatus_PENDING) { + return nullptr; //no result yet - delay confirmation response + } + } + + const char *statusStr = ""; + switch (status) { + case UnlockStatus_Unlocked: + statusStr = "Unlocked"; + break; + case UnlockStatus_UnlockFailed: + statusStr = "UnlockFailed"; + break; + case UnlockStatus_OngoingAuthorizedTransaction: + statusStr = "OngoingAuthorizedTransaction"; + break; + case UnlockStatus_UnknownConnector: + statusStr = "UnknownConnector"; + break; + case UnlockStatus_PENDING: + MO_DBG_ERR("UnlockConnector timeout"); + statusStr = "UnlockFailed"; + break; + } + + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + payload["status"] = statusStr; return doc; } + +} // namespace Ocpp201 +} // namespace MicroOcpp + +#endif //MO_ENABLE_CONNECTOR_LOCK +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/UnlockConnector.h b/src/MicroOcpp/Operations/UnlockConnector.h index 7489ad03..518653cd 100644 --- a/src/MicroOcpp/Operations/UnlockConnector.h +++ b/src/MicroOcpp/Operations/UnlockConnector.h @@ -1,12 +1,14 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef UNLOCKCONNECTOR_H #define UNLOCKCONNECTOR_H +#include + #include -#include +#include #include namespace MicroOcpp { @@ -15,13 +17,17 @@ class Model; namespace Ocpp16 { -class UnlockConnector : public Operation { +class UnlockConnector : public Operation, public MemoryManaged { private: Model& model; - bool err = false; - std::function ()> unlockConnector; - PollResult cbUnlockResult; + +#if MO_ENABLE_CONNECTOR_LOCK + std::function unlockConnector; + UnlockConnectorResult cbUnlockResult; unsigned long timerStart = 0; //for timeout +#endif //MO_ENABLE_CONNECTOR_LOCK + + const char *errorCode = nullptr; public: UnlockConnector(Model& model); @@ -29,9 +35,51 @@ class UnlockConnector : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} }; } //end namespace Ocpp16 } //end namespace MicroOcpp + +#if MO_ENABLE_V201 +#if MO_ENABLE_CONNECTOR_LOCK + +#include + +namespace MicroOcpp { + +class RemoteControlService; +class RemoteControlServiceEvse; + +namespace Ocpp201 { + +class UnlockConnector : public Operation, public MemoryManaged { +private: + RemoteControlService& rcService; + RemoteControlServiceEvse *rcEvse = nullptr; + + UnlockStatus status; + unsigned long timerStart = 0; //for timeout + + const char *errorCode = nullptr; +public: + UnlockConnector(RemoteControlService& rcService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_CONNECTOR_LOCK +#endif //MO_ENABLE_V201 + #endif diff --git a/src/MicroOcpp/Operations/UpdateFirmware.cpp b/src/MicroOcpp/Operations/UpdateFirmware.cpp index 8664ffe0..d9be551f 100644 --- a/src/MicroOcpp/Operations/UpdateFirmware.cpp +++ b/src/MicroOcpp/Operations/UpdateFirmware.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -8,8 +8,9 @@ #include using MicroOcpp::Ocpp16::UpdateFirmware; +using MicroOcpp::JsonDoc; -UpdateFirmware::UpdateFirmware(FirmwareService& fwService) : fwService(fwService) { +UpdateFirmware::UpdateFirmware(FirmwareService& fwService) : MemoryManaged("v16.Operation.", "UpdateFirmware"), fwService(fwService) { } @@ -45,6 +46,6 @@ void UpdateFirmware::processReq(JsonObject payload) { fwService.scheduleFirmwareUpdate(location, retrieveDate, (unsigned int) retries, (unsigned int) retryInterval); } -std::unique_ptr UpdateFirmware::createConf(){ +std::unique_ptr UpdateFirmware::createConf(){ return createEmptyDocument(); } diff --git a/src/MicroOcpp/Operations/UpdateFirmware.h b/src/MicroOcpp/Operations/UpdateFirmware.h index 8efeba37..da962141 100644 --- a/src/MicroOcpp/Operations/UpdateFirmware.h +++ b/src/MicroOcpp/Operations/UpdateFirmware.h @@ -1,9 +1,9 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef UPDATEFIRMWARE_H -#define UPDATEFIRMWARE_H +#ifndef MO_UPDATEFIRMWARE_H +#define MO_UPDATEFIRMWARE_H #include #include @@ -14,7 +14,7 @@ class FirmwareService; namespace Ocpp16 { -class UpdateFirmware : public Operation { +class UpdateFirmware : public Operation, public MemoryManaged { private: FirmwareService& fwService; @@ -26,7 +26,7 @@ class UpdateFirmware : public Operation { void processReq(JsonObject payload) override; - std::unique_ptr createConf() override; + std::unique_ptr createConf() override; const char *getErrorCode() override {return errorCode;} }; diff --git a/src/MicroOcpp/Platform.cpp b/src/MicroOcpp/Platform.cpp index 9430488b..75980725 100644 --- a/src/MicroOcpp/Platform.cpp +++ b/src/MicroOcpp/Platform.cpp @@ -1,18 +1,20 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include #ifdef MO_CUSTOM_CONSOLE +char _mo_console_msg_buf [MO_CUSTOM_CONSOLE_MAXMSGSIZE]; + namespace MicroOcpp { void (*mocpp_console_out_impl)(const char *msg) = nullptr; } -void MicroOcpp::mocpp_console_out(const char *msg) { - if (mocpp_console_out_impl) { - mocpp_console_out_impl(msg); +void _mo_console_out(const char *msg) { + if (MicroOcpp::mocpp_console_out_impl) { + MicroOcpp::mocpp_console_out_impl(msg); } } @@ -81,10 +83,30 @@ unsigned long mocpp_tick_ms_unix() { #endif #endif -#if MO_PLATFORM != MO_PLATFORM_ARDUINO -void dtostrf(float value, int min_width, int num_digits_after_decimal, char *target){ - char fmt[20]; - sprintf(fmt, "%%%d.%df", min_width, num_digits_after_decimal); - sprintf(target, fmt, value); +#ifdef MO_CUSTOM_RNG +uint32_t (*mocpp_rng_impl)() = nullptr; + +void mocpp_set_rng(uint32_t (*rng)()) { + mocpp_rng_impl = rng; +} + +uint32_t mocpp_rng_custom(void) { + if (mocpp_rng_impl) { + return mocpp_rng_impl(); + } else { + return 0; + } +} +#else + +// Time-based Pseudo RNG. +// Contains internal state which is mixed with the current timestamp +// each time it is called. Then this is passed through a multiply-with-carry +// PRNG operation to get a pseudo-random number. +uint32_t mocpp_time_based_prng(void) { + static uint32_t prng_state = 1; + uint32_t entropy = mocpp_tick_ms(); + prng_state = (prng_state ^ entropy)*1664525U + 1013904223U; // assuming complement-2 integers and non-signaling overflow + return prng_state; } #endif diff --git a/src/MicroOcpp/Platform.h b/src/MicroOcpp/Platform.h index 220c20d3..e5b8de09 100644 --- a/src/MicroOcpp/Platform.h +++ b/src/MicroOcpp/Platform.h @@ -1,15 +1,11 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef MO_PLATFORM_H #define MO_PLATFORM_H -#ifdef __cplusplus -#define EXT_C extern "C" -#else -#define EXT_C -#endif +#include #define MO_PLATFORM_NONE 0 #define MO_PLATFORM_ARDUINO 1 @@ -20,6 +16,12 @@ #define MO_PLATFORM MO_PLATFORM_ARDUINO #endif +#ifdef __cplusplus +#define MO_EXTERN_C extern "C" +#else +#define MO_EXTERN_C +#endif + #if MO_PLATFORM == MO_PLATFORM_NONE #ifndef MO_CUSTOM_CONSOLE #define MO_CUSTOM_CONSOLE @@ -30,25 +32,24 @@ #endif #ifdef MO_CUSTOM_CONSOLE -#include +#include #ifndef MO_CUSTOM_CONSOLE_MAXMSGSIZE -#define MO_CUSTOM_CONSOLE_MAXMSGSIZE 192 +#define MO_CUSTOM_CONSOLE_MAXMSGSIZE 256 #endif -void mocpp_set_console_out(void (*console_out)(const char *msg)); +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)); -namespace MicroOcpp { -void mocpp_console_out(const char *msg); -} #define MO_CONSOLE_PRINTF(X, ...) \ do { \ - char msg [MO_CUSTOM_CONSOLE_MAXMSGSIZE]; \ - auto _mo_ret = snprintf(msg, MO_CUSTOM_CONSOLE_MAXMSGSIZE, X, ##__VA_ARGS__); \ + 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(msg + MO_CUSTOM_CONSOLE_MAXMSGSIZE - 7, " [...]"); \ + sprintf(_mo_console_msg_buf + MO_CUSTOM_CONSOLE_MAXMSGSIZE - 7, " [...]"); \ } \ - MicroOcpp::mocpp_console_out(msg); \ + _mo_console_out(_mo_console_msg_buf); \ } while (0) #else #define mocpp_set_console_out(X) \ @@ -68,7 +69,11 @@ void mocpp_console_out(const char *msg); #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 +#elif MO_PLATFORM == MO_PLATFORM_ESPIDF +#include "esp_log.h" + +#define MO_CONSOLE_PRINTF(X, ...) esp_log_write(ESP_LOG_INFO, "MicroOcpp", X, ##__VA_ARGS__) +#elif MO_PLATFORM == MO_PLATFORM_UNIX #include #define MO_CONSOLE_PRINTF(X, ...) printf(X, ##__VA_ARGS__) @@ -76,9 +81,9 @@ void mocpp_console_out(const char *msg); #endif #ifdef MO_CUSTOM_TIMER -void mocpp_set_timer(unsigned long (*get_ms)()); +MO_EXTERN_C void mocpp_set_timer(unsigned long (*get_ms)()); -unsigned long mocpp_tick_ms_custom(); +MO_EXTERN_C unsigned long mocpp_tick_ms_custom(); #define mocpp_tick_ms mocpp_tick_ms_custom #else @@ -86,14 +91,23 @@ unsigned long mocpp_tick_ms_custom(); #include #define mocpp_tick_ms millis #elif MO_PLATFORM == MO_PLATFORM_ESPIDF -unsigned long mocpp_tick_ms_espidf(); +MO_EXTERN_C unsigned long mocpp_tick_ms_espidf(); #define mocpp_tick_ms mocpp_tick_ms_espidf #elif MO_PLATFORM == MO_PLATFORM_UNIX -unsigned long mocpp_tick_ms_unix(); +MO_EXTERN_C unsigned long mocpp_tick_ms_unix(); #define mocpp_tick_ms mocpp_tick_ms_unix #endif #endif +#ifdef MO_CUSTOM_RNG +MO_EXTERN_C void mocpp_set_rng(uint32_t (*rng)()); +MO_EXTERN_C uint32_t mocpp_rng_custom(); +#define mocpp_rng mocpp_rng_custom +#else +MO_EXTERN_C uint32_t mocpp_time_based_prng(void); +#define mocpp_rng mocpp_time_based_prng +#endif + #ifndef MO_MAX_JSON_CAPACITY #if MO_PLATFORM == MO_PLATFORM_UNIX #define MO_MAX_JSON_CAPACITY 16384 @@ -102,8 +116,8 @@ unsigned long mocpp_tick_ms_unix(); #endif #endif -#if MO_PLATFORM != MO_PLATFORM_ARDUINO -void dtostrf(float value, int min_width, int num_digits_after_decimal, char *target); +#ifndef MO_ENABLE_MBEDTLS +#define MO_ENABLE_MBEDTLS 0 #endif #endif diff --git a/src/MicroOcpp/Version.h b/src/MicroOcpp/Version.h index d4604681..92a2ea46 100644 --- a/src/MicroOcpp/Version.h +++ b/src/MicroOcpp/Version.h @@ -1 +1,52 @@ -#define MO_VERSION "1.0.0" +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_VERSION_H +#define MO_VERSION_H + +/* + * Version specification of MicroOcpp library (not related with the OCPP version) + */ +#define MO_VERSION "1.2.0" + +/* + * Enable OCPP 2.0.1 support. If enabled, library can be initialized with both v1.6 and v2.0.1. The choice + * of the protocol is done dynamically during initialization + */ +#ifndef MO_ENABLE_V201 +#define MO_ENABLE_V201 0 +#endif + +#ifdef __cplusplus + +namespace MicroOcpp { + +/* + * OCPP version type, defined in Model + */ +struct ProtocolVersion { + const int major, minor, patch; + ProtocolVersion(int major = 1, int minor = 6, int patch = 0) : major(major), minor(minor), patch(patch) { } +}; + +} + +#endif //__cplusplus + +// Certificate Management (UCs M03 - M05). Works with OCPP 1.6 and 2.0.1 +#ifndef MO_ENABLE_CERT_MGMT +#define MO_ENABLE_CERT_MGMT MO_ENABLE_V201 +#endif + +// Reservations +#ifndef MO_ENABLE_RESERVATION +#define MO_ENABLE_RESERVATION 1 +#endif + +// Local Authorization, i.e. feature profile LocalAuthListManagement +#ifndef MO_ENABLE_LOCAL_AUTH +#define MO_ENABLE_LOCAL_AUTH 1 +#endif + +#endif diff --git a/src/MicroOcpp_c.cpp b/src/MicroOcpp_c.cpp index 4041a368..381084b0 100644 --- a/src/MicroOcpp_c.cpp +++ b/src/MicroOcpp_c.cpp @@ -1,19 +1,29 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// 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) { - ocpp_initialize_full(conn, ChargerCredentials(chargePointModel, chargePointVendor), fsopt, autoRecover); +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) { +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"); } @@ -22,13 +32,33 @@ void ocpp_initialize_full(OCPP_Connection *conn, const char *bootNotificationCre MicroOcpp::FilesystemOpt adaptFsopt = fsopt; - mocpp_initialize(*ocppSocket, bootNotificationCredentials, MicroOcpp::makeDefaultFilesystemAdapter(adaptFsopt), autoRecover); + 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(); } @@ -37,19 +67,6 @@ void ocpp_loop() { * Helper functions for transforming callback functions from C-style to C++style */ -MicroOcpp::PollResult adaptScl(enum OptionalBool v) { - if (v == OptionalTrue) { - return true; - } else if (v == OptionalFalse) { - return false; - } else if (v == OptionalNone) { - return MicroOcpp::PollResult::Await(); - } else { - MO_DBG_ERR("illegal argument"); - return false; - } -} - std::function adaptFn(InputBool fn) { return fn; } @@ -130,26 +147,28 @@ MicroOcpp::OnReceiveErrorListener adaptFn(OnCallError fn) { }; } -std::function()> adaptFn(PollBool fn) { - return [fn] () {return adaptScl(fn());}; +#if MO_ENABLE_CONNECTOR_LOCK +std::function adaptFn(PollUnlockResult fn) { + return [fn] () {return fn();}; } -std::function()> adaptFn(unsigned int connectorId, PollBool_m fn) { - return [fn, connectorId] () {return adaptScl(fn(connectorId));}; +std::function adaptFn(unsigned int connectorId, PollUnlockResult_m fn) { + return [fn, connectorId] () {return fn(connectorId);}; } +#endif //MO_ENABLE_CONNECTOR_LOCK -void ocpp_beginTransaction(const char *idTag) { - beginTransaction(idTag); +bool ocpp_beginTransaction(const char *idTag) { + return beginTransaction(idTag); } -void ocpp_beginTransaction_m(unsigned int connectorId, const char *idTag) { - beginTransaction(idTag, connectorId); +bool ocpp_beginTransaction_m(unsigned int connectorId, const char *idTag) { + return beginTransaction(idTag, connectorId); } -void ocpp_beginTransaction_authorized(const char *idTag, const char *parentIdTag) { - beginTransaction_authorized(idTag, parentIdTag); +bool ocpp_beginTransaction_authorized(const char *idTag, const char *parentIdTag) { + return beginTransaction_authorized(idTag, parentIdTag); } -void ocpp_beginTransaction_authorized_m(unsigned int connectorId, const char *idTag, const char *parentIdTag) { - beginTransaction_authorized(idTag, parentIdTag, connectorId); +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) { @@ -191,10 +210,29 @@ 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 NULL; + return nullptr; } } @@ -205,6 +243,14 @@ 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)); } @@ -273,6 +319,33 @@ void ocpp_addMeterValueInputFloat_m(unsigned int connectorId, InputFloat_m value 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); } @@ -283,12 +356,15 @@ void ocpp_addMeterValueInput_m(unsigned int connectorId, MeterValueInput *meterV addMeterValueInput(std::move(svs), connectorId); } -void ocpp_setOnUnlockConnectorInOut(PollBool onUnlockConnectorInOut) { + +#if MO_ENABLE_CONNECTOR_LOCK +void ocpp_setOnUnlockConnectorInOut(PollUnlockResult onUnlockConnectorInOut) { setOnUnlockConnectorInOut(adaptFn(onUnlockConnectorInOut)); } -void ocpp_setOnUnlockConnectorInOut_m(unsigned int connectorId, PollBool_m 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)); @@ -304,14 +380,14 @@ void ocpp_setStopTxReadyInput_m(unsigned int connectorId, InputBool_m stopTxRead setStopTxReadyInput(adaptFn(connectorId, stopTxReady), connectorId); } -void ocpp_setTxNotificationOutput(void (*notificationOutput)(OCPP_Transaction*, enum OCPP_TxNotification)) { - setTxNotificationOutput([notificationOutput] (MicroOcpp::Transaction *tx, MicroOcpp::TxNotification notification) { - notificationOutput(reinterpret_cast(tx), convertTxNotification(notification)); +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*, enum OCPP_TxNotification)) { - setTxNotificationOutput([notificationOutput, connectorId] (MicroOcpp::Transaction *tx, MicroOcpp::TxNotification notification) { - notificationOutput(connectorId, reinterpret_cast(tx), convertTxNotification(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); } @@ -336,6 +412,16 @@ 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)); } @@ -344,13 +430,9 @@ void ocpp_setOnSendConf(const char *operationType, OnMessage onConfirmation) { setOnSendConf(operationType, adaptFn(onConfirmation)); } -void ocpp_set_console_out_c(void (*console_out)(const char *msg)) { - mocpp_set_console_out(console_out); -} - void ocpp_authorize(const char *idTag, AuthorizeConfCallback onConfirmation, AuthorizeAbortCallback onAbort, AuthorizeTimeoutCallback onTimeout, AuthorizeErrorCallback onError, void *user_data) { - std::string idTag_capture = idTag; + auto idTag_capture = MicroOcpp::makeString("MicroOcpp_c.cpp", idTag); authorize(idTag, onConfirmation ? [onConfirmation, idTag_capture, user_data] (JsonObject payload) { diff --git a/src/MicroOcpp_c.h b/src/MicroOcpp_c.h index df9d4402..a517ff1d 100644 --- a/src/MicroOcpp_c.h +++ b/src/MicroOcpp_c.h @@ -1,15 +1,18 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef ARDUINOOCPP_C_H -#define ARDUINOOCPP_C_H +#ifndef MO_MICROOCPP_C_H +#define MO_MICROOCPP_C_H #include #include -#include +#include +#include #include +#include +#include struct OCPP_Connection; typedef struct OCPP_Connection OCPP_Connection; @@ -17,6 +20,9 @@ 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) (); @@ -38,9 +44,11 @@ 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); -enum OptionalBool {OptionalTrue, OptionalFalse, OptionalNone}; -typedef enum OptionalBool (*PollBool)(); -typedef enum OptionalBool (*PollBool_m)(unsigned int connectorId); + +#if MO_ENABLE_CONNECTOR_LOCK +typedef UnlockConnectorResult (*PollUnlockResult)(); +typedef UnlockConnectorResult (*PollUnlockResult_m)(unsigned int connectorId); +#endif //MO_ENABLE_CONNECTOR_LOCK #ifdef __cplusplus @@ -56,29 +64,40 @@ void ocpp_initialize( 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 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 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 */ -void ocpp_beginTransaction(const char *idTag); -void ocpp_beginTransaction_m(unsigned int connectorId, const char *idTag); //multiple connectors version +bool ocpp_beginTransaction(const char *idTag); +bool ocpp_beginTransaction_m(unsigned int connectorId, const char *idTag); //multiple connectors version -void ocpp_beginTransaction_authorized(const char *idTag, const char *parentIdTag); -void ocpp_beginTransaction_authorized_m(unsigned int connectorId, const char *idTag, const char *parentIdTag); +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 @@ -101,6 +120,9 @@ 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. */ @@ -137,6 +159,9 @@ void ocpp_addErrorCodeInput_m(unsigned int connectorId, InputString_m errorCodeI 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 @@ -149,11 +174,13 @@ void ocpp_setStartTxReadyInput_m(unsigned int connectorId, InputBool_m startTxRe void ocpp_setStopTxReadyInput(InputBool stopTxReady); void ocpp_setStopTxReadyInput_m(unsigned int connectorId, InputBool_m stopTxReady); -void ocpp_setTxNotificationOutput(void (*notificationOutput)(OCPP_Transaction*, enum OCPP_TxNotification)); -void ocpp_setTxNotificationOutput_m(unsigned int connectorId, void (*notificationOutput)(unsigned int, OCPP_Transaction*, enum OCPP_TxNotification)); +void ocpp_setTxNotificationOutput(void (*notificationOutput)(OCPP_Transaction*, TxNotification)); +void ocpp_setTxNotificationOutput_m(unsigned int connectorId, void (*notificationOutput)(unsigned int, OCPP_Transaction*, TxNotification)); -void ocpp_setOnUnlockConnectorInOut(PollBool onUnlockConnectorInOut); -void ocpp_setOnUnlockConnectorInOut_m(unsigned int connectorId, PollBool_m onUnlockConnectorInOut); +#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 @@ -166,16 +193,14 @@ 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); -/* - * If build flag MO_CUSTOM_CONSOLE is set, all console output will be forwarded to the print - * function given by this setter. The parameter msg will also by null-terminated c-strings. - */ -void ocpp_set_console_out_c(void (*console_out)(const char *msg)); - /* * Send OCPP operations */ diff --git a/tests/Api.cpp b/tests/Api.cpp index 2ca8f468..c2470172 100644 --- a/tests/Api.cpp +++ b/tests/Api.cpp @@ -1,12 +1,20 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + #include #include #include #include +#include #include -#include -#include "./catch2/catch.hpp" +#include +#include +#include #include "./helpers/testHelper.h" +#include + #define BASE_TIME "2023-01-01T00:00:00.000Z" #define SCPROFILE "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":0,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"chargingSchedule\":{\"duration\":1000000,\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"W\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3}]}}}]" @@ -49,14 +57,17 @@ TEST_CASE( "C++ API test" ) { auto valueSampler = std::unique_ptr>>( new MicroOcpp::SampledValueSamplerConcrete>( svprops, - [c = &checkpoints[ncheck++]] (MicroOcpp::ReadingContext) -> int32_t {*c = true; return 0;})); + [c = &checkpoints[ncheck++]] (ReadingContext) -> int32_t {*c = true; return 0;})); addMeterValueInput(std::move(valueSampler)); setOccupiedInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return false;}); setStartTxReadyInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return true;}); setStopTxReadyInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return true;}); - setTxNotificationOutput([c = &checkpoints[ncheck++]] (MicroOcpp::Transaction*, MicroOcpp::TxNotification) {*c = true;}); - setOnUnlockConnectorInOut([c = &checkpoints[ncheck++]] () -> MicroOcpp::PollResult {*c = true; return true;}); + setTxNotificationOutput([c = &checkpoints[ncheck++]] (MicroOcpp::Transaction*, TxNotification) {*c = true;}); + +#if MO_ENABLE_CONNECTOR_LOCK + setOnUnlockConnectorInOut([c = &checkpoints[ncheck++]] () -> UnlockConnectorResult {*c = true; return UnlockConnectorResult_Unlocked;}); +#endif //MO_ENABLE_CONNECTOR_LOCK setOnResetNotify([c = &checkpoints[ncheck++]] (bool) -> bool {*c = true; return true;}); setOnResetExecute([c = &checkpoints[ncheck++]] (bool) {*c = true;}); @@ -69,13 +80,13 @@ TEST_CASE( "C++ API test" ) { setOnSendConf("StatusNotification", [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); sendRequest("DataTransfer", [c = &checkpoints[ncheck++]] () { *c = true; - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); doc->to(); return doc; }, [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); setRequestHandler("DataTransfer", [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}, [c = &checkpoints[ncheck++]] () { *c = true; - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); doc->to(); return doc; }); @@ -136,13 +147,13 @@ TEST_CASE( "C++ API test" ) { REQUIRE(isOperative()); sendRequest("UnlockConnector", [] () { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); (*doc)["connectorId"] = 1; return doc; }, [] (JsonObject) {}); sendRequest("Reset", [] () { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); (*doc)["type"] = "Hard"; return doc; }, [] (JsonObject) {}); @@ -186,7 +197,7 @@ TEST_CASE( "C API test" ) { fsopt.formatFsOnFail = true; MicroOcpp::LoopbackConnection loopback; - ocpp_initialize(reinterpret_cast(&loopback), "test-runner1234", "vendor", fsopt, false); + ocpp_initialize(reinterpret_cast(&loopback), "test-runner1234", "vendor", fsopt, false, false); auto context = getOcppContext(); auto& model = context->getModel(); @@ -225,13 +236,13 @@ TEST_CASE( "C API test" ) { auto valueSampler = std::unique_ptr>>( new MicroOcpp::SampledValueSamplerConcrete>( svprops, - [] (MicroOcpp::ReadingContext) -> int32_t {checkpointsc[16] = true; return 0;})); ncheckc++; + [] (ReadingContext) -> int32_t {checkpointsc[16] = true; return 0;})); ncheckc++; ocpp_addMeterValueInput(reinterpret_cast(valueSampler.release())); valueSampler = std::unique_ptr>>( new MicroOcpp::SampledValueSamplerConcrete>( svprops, - [] (MicroOcpp::ReadingContext) -> int32_t {checkpointsc[17] = true; return 0;})); ncheckc++; + [] (ReadingContext) -> int32_t {checkpointsc[17] = true; return 0;})); ncheckc++; ocpp_addMeterValueInput_m(2, reinterpret_cast(valueSampler.release())); ocpp_setOccupiedInput([] () -> bool {checkpointsc[18] = true; return true;}); ncheckc++; @@ -240,10 +251,16 @@ TEST_CASE( "C API test" ) { ocpp_setStartTxReadyInput_m(2, [] (unsigned int) -> bool {checkpointsc[21] = true; return true;}); ncheckc++; ocpp_setStopTxReadyInput([] () -> bool {checkpointsc[22] = true; return true;}); ncheckc++; ocpp_setStopTxReadyInput_m(2, [] (unsigned int) -> bool {checkpointsc[23] = true; return true;}); ncheckc++; - ocpp_setTxNotificationOutput([] (OCPP_Transaction*, OCPP_TxNotification) {checkpointsc[24] = true;}); ncheckc++; - ocpp_setTxNotificationOutput_m(2, [] (unsigned int, OCPP_Transaction*, OCPP_TxNotification) {checkpointsc[25] = true;}); ncheckc++; - ocpp_setOnUnlockConnectorInOut([] () -> OptionalBool {checkpointsc[26] = true; return OptionalBool::OptionalTrue;}); ncheckc++; - ocpp_setOnUnlockConnectorInOut_m(2, [] (unsigned int) -> OptionalBool {checkpointsc[27] = true; return OptionalBool::OptionalTrue;}); ncheckc++; + ocpp_setTxNotificationOutput([] (OCPP_Transaction*, TxNotification) {checkpointsc[24] = true;}); ncheckc++; + ocpp_setTxNotificationOutput_m(2, [] (unsigned int, OCPP_Transaction*, TxNotification) {checkpointsc[25] = true;}); ncheckc++; + +#if MO_ENABLE_CONNECTOR_LOCK + ocpp_setOnUnlockConnectorInOut([] () -> UnlockConnectorResult {checkpointsc[26] = true; return UnlockConnectorResult_Unlocked;}); ncheckc++; + ocpp_setOnUnlockConnectorInOut_m(2, [] (unsigned int) -> UnlockConnectorResult {checkpointsc[27] = true; return UnlockConnectorResult_Unlocked;}); ncheckc++; +#else + checkpointsc[26] = true; + checkpointsc[27] = true; +#endif //MO_ENABLE_CONNECTOR_LOCK ocpp_setOnResetNotify([] (bool) -> bool {checkpointsc[28] = true; return true;}); ncheckc++; ocpp_setOnResetExecute([] (bool) {checkpointsc[29] = true;}); ncheckc++; @@ -318,18 +335,18 @@ TEST_CASE( "C API test" ) { REQUIRE(ocpp_isOperative_m(2)); sendRequest("UnlockConnector", [] () { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); (*doc)["connectorId"] = 1; return doc; }, [] (JsonObject) {}); sendRequest("UnlockConnector", [] () { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); (*doc)["connectorId"] = 2; return doc; }, [] (JsonObject) {}); sendRequest("Reset", [] () { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); (*doc)["type"] = "Hard"; return doc; }, [] (JsonObject) {}); diff --git a/tests/Boot.cpp b/tests/Boot.cpp new file mode 100644 index 00000000..a4ea610f --- /dev/null +++ b/tests/Boot.cpp @@ -0,0 +1,477 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "./helpers/testHelper.h" + +#define CHARGEPOINTMODEL "Test model" +#define CHARGEPOINTVENDOR "Test vendor" + +#define BASE_TIME "2023-01-01T00:00:00.000Z" + +#define GET_CONFIGURATION "[2,\"msgId01\",\"GetConfiguration\",{\"key\":[]}]" +#define TRIGGER_MESSAGE "[2,\"msgId02\",\"TriggerMessage\",{\"requestedMessage\":\"TriggeredOperation\"}]" + +using namespace MicroOcpp; + +//dummy operation type to test TriggerMessage +class TriggeredOperation : public Operation { +private: + bool& checkExecuted; +public: + TriggeredOperation(bool& checkExecuted) : checkExecuted(checkExecuted) { } + const char* getOperationType() override {return "TriggeredOperation";} + std::unique_ptr createReq() override { + checkExecuted = true; + return createEmptyDocument(); + } + void processConf(JsonObject) override {} + void processReq(JsonObject) override {} + std::unique_ptr createConf() override {return createEmptyDocument();} +}; + + +TEST_CASE( "Boot Behavior" ) { + printf("\nRun %s\n", "Boot Behavior"); + + //clean state + auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); + FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); + + //initialize Context with dummy socket + LoopbackConnection loopback; + + + mocpp_initialize(loopback, ChargerCredentials(CHARGEPOINTMODEL, CHARGEPOINTVENDOR), filesystem); + + mocpp_set_timer(custom_timer_cb); + + SECTION("BootNotification - Accepted") { + + bool checkProcessed = false; + + getOcppContext()->getOperationRegistry().registerOperation("BootNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("BootNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + checkProcessed = true; + REQUIRE( !strcmp(payload["chargePointModel"] | "_Undefined", CHARGEPOINTMODEL) ); + REQUIRE( !strcmp(payload["chargePointVendor"] | "_Undefined", CHARGEPOINTVENDOR) ); + }, + [] () { + //create conf + auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024); + (*conf)["currentTime"] = BASE_TIME; + (*conf)["interval"] = 3600; + (*conf)["status"] = "Accepted"; + return conf; + }); + }); + + loop(); + + REQUIRE(checkProcessed); + REQUIRE(getOcppContext()->getModel().getClock().now() >= MIN_TIME); + } + + SECTION("BootNotification - Pending") { + + MO_DBG_INFO("Queue messages before BootNotification to see if they come through"); + + loop(); //normal BootNotification run + + REQUIRE( isOperative() ); //normal BN succeeded + + loopback.setConnected( false ); + + beginTransaction_authorized("mIdTag"); + + loop(); + + endTransaction(); + + mocpp_deinitialize(); + + loopback.setConnected( true ); + + MO_DBG_INFO("Start charger again with queued transaction messages, also init non-tx-related msg, but now delay BN procedure"); + + mocpp_initialize(loopback, ChargerCredentials()); + + getOcppContext()->getOperationRegistry().registerOperation("BootNotification", + [] () { + return new Ocpp16::CustomOperation("BootNotification", + [] (JsonObject payload) { + //ignore req + }, + [] () { + //create conf + auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024); + (*conf)["currentTime"] = BASE_TIME; + (*conf)["interval"] = 3600; + (*conf)["status"] = "Pending"; + return conf; + }); + }); + + bool sentTxMsg = false; + + getOcppContext()->getOperationRegistry().setOnRequest("StartTransaction", + [&sentTxMsg] (JsonObject) { + sentTxMsg = true; + }); + + getOcppContext()->getOperationRegistry().setOnRequest("StopTransaction", + [&sentTxMsg] (JsonObject) { + sentTxMsg = true; + }); + + bool checkProcessedHeartbeat = false; + + auto heartbeat = makeRequest(new Ocpp16::CustomOperation( + "Heartbeat", + [] () { + //create req + return createEmptyDocument();}, + [&checkProcessedHeartbeat] (JsonObject) { + //process conf + checkProcessedHeartbeat = true; + })); + heartbeat->setTimeout(0); //disable timeout and check if message will be sent later + + getOcppContext()->initiateRequest(std::move(heartbeat)); + + bool sentNonTxMsg = false; + + getOcppContext()->getOperationRegistry().setOnRequest("Heartbeat", + [&sentNonTxMsg] (JsonObject) { + sentNonTxMsg = true; + }); + + loop(); + + REQUIRE( !sentTxMsg ); + REQUIRE( !sentNonTxMsg ); + REQUIRE( !checkProcessedHeartbeat ); + + MO_DBG_INFO("Check if charger still responds to server-side messages and executes TriggerMessages"); + + bool reactedToServerMsg = false; + + getOcppContext()->getOperationRegistry().setOnRequest("GetConfiguration", + [&reactedToServerMsg] (JsonObject) { + reactedToServerMsg = true; + }); + + loopback.sendTXT(GET_CONFIGURATION, sizeof(GET_CONFIGURATION) - 1); + + loop(); + + REQUIRE( reactedToServerMsg ); + + bool executedTriggerMessage = false; + + getOcppContext()->getOperationRegistry().registerOperation("TriggeredOperation", + [&executedTriggerMessage] () {return new TriggeredOperation(executedTriggerMessage);}); + + loopback.sendTXT(TRIGGER_MESSAGE, sizeof(TRIGGER_MESSAGE) - 1); + + loop(); + + REQUIRE( executedTriggerMessage ); + + //other messages still didn't get through? + REQUIRE( !sentTxMsg ); + REQUIRE( !sentNonTxMsg ); + REQUIRE( !checkProcessedHeartbeat ); + + MO_DBG_INFO("Now, accept BN and check if all queued messages finally arrive"); + + getOcppContext()->getOperationRegistry().registerOperation("BootNotification", + [] () { + return new Ocpp16::CustomOperation("BootNotification", + [] (JsonObject payload) { + //ignore req + }, + [] () { + //create conf + auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024); + (*conf)["currentTime"] = BASE_TIME; + (*conf)["interval"] = 3600; + (*conf)["status"] = "Accepted"; + return conf; + }); + }); + + mtime += 3600 * 1000; + + loop(); + + REQUIRE( sentTxMsg ); + REQUIRE( sentNonTxMsg ); + REQUIRE( checkProcessedHeartbeat ); + } + + SECTION("PreBoot transactions") { + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true)->setBool(true); + declareConfiguration("AllowOfflineTxForUnknownId", true)->setBool(true); + + unsigned int startTxCount = 0; + + getOcppContext()->getOperationRegistry().setOnRequest("StartTransaction", + [&startTxCount] (JsonObject) { + startTxCount++; + }); + + //start one transaction in full offline mode + + loopback.setConnected( false ); + loop(); + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + + beginTransaction("mIdTag"); + loop(); + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + + endTransaction("mIdTag"); + loop(); + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + + //start another transaction while BN is pending + + getOcppContext()->getOperationRegistry().registerOperation("BootNotification", + [] () { + return new Ocpp16::CustomOperation("BootNotification", + [] (JsonObject payload) { + //ignore req + }, + [] () { + //create conf + auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024); + (*conf)["currentTime"] = BASE_TIME; + (*conf)["interval"] = 3600; + (*conf)["status"] = "Pending"; + return conf; + }); + }); + + loopback.setConnected( true ); + loop(); + REQUIRE( startTxCount == 0 ); + + beginTransaction("mIdTag2"); + mtime += 20 * 1000; + loop(); + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + + endTransaction(); + loop(); + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + + REQUIRE( startTxCount == 0 ); + + //Now, accept BN and check again + + getOcppContext()->getOperationRegistry().registerOperation("BootNotification", + [] () { + return new Ocpp16::CustomOperation("BootNotification", + [] (JsonObject payload) { + //ignore req + }, + [] () { + //create conf + auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024); + (*conf)["currentTime"] = BASE_TIME; + (*conf)["interval"] = 3600; + (*conf)["status"] = "Accepted"; + return conf; + }); + }); + + mtime += 3600 * 1000; + + loop(); + REQUIRE( startTxCount == 2 ); + + } + + SECTION("Auto recovery") { + + //start transaction which will persist a few boot cycles, but then will be wiped by auto recovery + loop(); + beginTransaction("mIdTag"); + loop(); + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + + declareConfiguration("keepConfigOverRecovery", "originalVal"); + configuration_save(); + + mocpp_deinitialize(); + + //MO has 2 unexpected power cycles. Probably just back luck - keep the local state and configuration + + //Increase the power cycle counter manually because it's not possible to interrupt the MO lifecycle during unit tests + BootStats bootstats; + BootService::loadBootStats(filesystem, bootstats); + bootstats.bootNr += 2; + BootService::storeBootStats(filesystem, bootstats); + + mocpp_initialize(loopback, ChargerCredentials(), filesystem, /*enable auto recovery*/ true); + BootService::loadBootStats(filesystem, bootstats); + REQUIRE( bootstats.getBootFailureCount() == 2 + 1 ); //two boot failures have been measured, +1 because each power cycle is counted as potentially failing until reaching the long runtime barrier + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + + REQUIRE( !strcmp(declareConfiguration("keepConfigOverRecovery", "otherVal")->getString(), "originalVal") ); + + //check that the power cycle counter has been updated properly after the controller has been running stable over a long time + mtime += MO_BOOTSTATS_LONGTIME_MS; + loop(); + BootService::loadBootStats(filesystem, bootstats); + REQUIRE( bootstats.getBootFailureCount() == 0 ); + + mocpp_deinitialize(); + + //MO has 10 power cycles without running for at least 3 minutes and wipes the local state, but keeps the configuration + + BootStats bootstats2; + BootService::loadBootStats(filesystem, bootstats2); + bootstats2.bootNr += 10; + BootService::storeBootStats(filesystem, bootstats2); + + mocpp_initialize(loopback, ChargerCredentials(), filesystem, /*enable auto recovery*/ true); + + REQUIRE( !strcmp(declareConfiguration("keepConfigOverRecovery", "otherVal")->getString(), "originalVal") ); + BootStats bootstats3; + BootService::loadBootStats(filesystem, bootstats3); + REQUIRE( bootstats3.getBootFailureCount() == 0 + 1 ); //failure count is reset, but +1 because each power cycle is counted as potentially failing until reaching the long runtime barrier + + loop(); + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + + } + + SECTION("Migration") { + + //migration removes files from previous MO versions which were running on the controller. This includes the + //transaction cache, but configs are preserved + + auto old_opstore = filesystem->open(MO_FILENAME_PREFIX "opstore.jsn", "w"); //the opstore has been removed in MO v1.2.0 + old_opstore->write("example content", sizeof("example content") - 1); + old_opstore.reset(); //flushes the file + + loop(); + beginTransaction("mIdTag"); //tx store will also be removed + auto tx = getTransaction(); + auto txNr = tx->getTxNr(); //remember this for later usage + tx.reset(); //reset this smart pointer + loop(); + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + endTransaction(); + loop(); + + REQUIRE( getOcppContext()->getModel().getTransactionStore()->getTransaction(1, txNr) != nullptr ); //tx exists on flash + + declareConfiguration("keepConfigOverMigration", "originalVal"); //migration keeps configs + configuration_save(); + + mocpp_deinitialize(); + + //After a FW update, the tracked version number has changed + BootStats bootstats; + BootService::loadBootStats(filesystem, bootstats); + snprintf(bootstats.microOcppVersion, sizeof(bootstats.microOcppVersion), "oldFwVers"); + BootService::storeBootStats(filesystem, bootstats); + + mocpp_initialize(loopback, ChargerCredentials(), filesystem); //MO migrates here + + size_t msize = 0; + REQUIRE( filesystem->stat(MO_FILENAME_PREFIX "opstore.jsn", &msize) != 0 ); //opstore has been removed + + REQUIRE( getOcppContext()->getModel().getTransactionStore()->getTransaction(1, txNr) == nullptr ); //tx history entry has been removed + + REQUIRE( !strcmp(declareConfiguration("keepConfigOverMigration", "otherVal")->getString(), "originalVal") ); //config has been preserved + } + + SECTION("Clean unused configs") { + + declareConfiguration("neverDeclaredInsideMO", "originalVal"); //unused configs will be cleared automatically after the controller has been running for a long time + configuration_save(); + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials(), filesystem); //all configs are loaded here, including the test config of this section + loop(); + + //unused configs will be cleared automatically after long time + mtime += MO_BOOTSTATS_LONGTIME_MS; + loop(); + + REQUIRE( !strcmp(declareConfiguration("neverDeclaredInsideMO", "newVal")->getString(), "newVal") ); //config has been removed + } + + SECTION("Boot with v201") { + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials::v201(CHARGEPOINTMODEL, CHARGEPOINTVENDOR), filesystem, false, ProtocolVersion(2,0,1)); + + bool checkProcessed = false; + + getOcppContext()->getOperationRegistry().registerOperation("BootNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("BootNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + checkProcessed = true; + REQUIRE( !strcmp(payload["reason"] | "_Undefined", "PowerUp") ); + REQUIRE( !strcmp(payload["chargingStation"]["model"] | "_Undefined", CHARGEPOINTMODEL) ); + REQUIRE( !strcmp(payload["chargingStation"]["vendorName"] | "_Undefined", CHARGEPOINTVENDOR) ); + }, + [] () { + //create conf + auto conf = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(3)); + (*conf)["currentTime"] = BASE_TIME; + (*conf)["interval"] = 3600; + (*conf)["status"] = "Accepted"; + return conf; + }); + }); + + MO_MEM_RESET(); + + loop(); + + REQUIRE(checkProcessed); + REQUIRE(getOcppContext()->getModel().getClock().now() >= MIN_TIME); + + MO_MEM_PRINT_STATS(); + + MO_MEM_RESET(); + + mtime += 3600 * 1000; + loop(); + + MO_DBG_INFO("Memory requirements UC G02:"); + MO_MEM_PRINT_STATS(); + } + + mocpp_deinitialize(); +} diff --git a/tests/Certificates.cpp b/tests/Certificates.cpp new file mode 100644 index 00000000..79d0b967 --- /dev/null +++ b/tests/Certificates.cpp @@ -0,0 +1,260 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_MBEDTLS + +#include +#include +#include +#include "./helpers/testHelper.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#define BASE_TIME "2023-01-01T00:00:00.000Z" + +//ISRG Root X1 +const char *root_cert = R"(-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- +)"; + +//precomputed identifiers of root cert above, based on Open Certificate Status Protocol (OCSP) +const char *root_cert_hash_algorithm = "SHA256"; //algorithm used for the following hashes +const char *root_cert_hash_issuer_name = "F6DB2FBD9DD85D9259DDB3C6DE7D7B2FEC3F3E0CEF1761BCBF3320571E2D30F8"; +const char *root_cert_hash_issuer_key = "F4593A1E07CC9CCEFFBED9C11DC5218356F7814D9B22949DE745E629990C6C60"; +const char *root_cert_hash_serial_number = "8210CFB0D240E3594463E0BB63828B00"; + +using namespace MicroOcpp; + +TEST_CASE( "M - Certificates" ) { + printf("\nRun %s\n", "M - Certificates"); + + //clean state + auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); + FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); + + //initialize Context with dummy socket + LoopbackConnection loopback; + + mocpp_set_timer(custom_timer_cb); + + mocpp_initialize(loopback, ChargerCredentials("test-runner")); + auto& model = getOcppContext()->getModel(); + auto certService = model.getCertificateService(); + SECTION("CertificateService initialized") { + REQUIRE(certService != nullptr); + } + auto certs = certService->getCertificateStore(); + SECTION("CertificateStore initialized") { + REQUIRE(certs != nullptr); + } + + auto connector = model.getConnector(1); + model.getClock().setTime(BASE_TIME); + + loop(); + + SECTION("M05 Install CA cert -- sent cert is valid") { + auto ret = certs->installCertificate(InstallCertificateType_CSMSRootCertificate, root_cert); + REQUIRE(ret == InstallCertificateStatus_Accepted); + + size_t msize; + char fn [MO_MAX_PATH_SIZE]; + printCertFn(MO_CERT_FN_CSMS_ROOT, 0, fn, MO_MAX_PATH_SIZE); + REQUIRE(filesystem->stat(fn, &msize) == 0); + REQUIRE(msize == strlen(root_cert)); + } + + SECTION("M03 Retrieve list of available certs -- one cert available") { + auto ret1 = certs->installCertificate(InstallCertificateType_CSMSRootCertificate, root_cert); + REQUIRE(ret1 == InstallCertificateStatus_Accepted); + + auto chain = makeVector("UnitTests"); + auto ret2 = certs->getCertificateIds({GetCertificateIdType_CSMSRootCertificate}, chain); + + REQUIRE(ret2 == GetInstalledCertificateStatus_Accepted); + REQUIRE(chain.size() == 1); + + auto& chainElem = chain.front(); + + REQUIRE(chainElem.certificateType == GetCertificateIdType_CSMSRootCertificate); + auto& certHash = chainElem.certificateHashData; + + REQUIRE(!strcmp(HashAlgorithmLabel(certHash.hashAlgorithm), root_cert_hash_algorithm)); //if this fails, please update the precomputed test hashes + + char buf [MO_CERT_HASH_ISSUER_NAME_KEY_SIZE]; + + ocpp_cert_print_issuerNameHash(&certHash, buf, sizeof(buf)); + REQUIRE(!strcmp(buf, root_cert_hash_issuer_name)); + + ocpp_cert_print_issuerKeyHash(&certHash, buf, sizeof(buf)); + REQUIRE(!strcmp(buf, root_cert_hash_issuer_key)); + + ocpp_cert_print_serialNumber(&certHash, buf, sizeof(buf)); + REQUIRE(!strcmp(buf, root_cert_hash_serial_number)); + + REQUIRE(chainElem.childCertificateHashData.empty()); //no sub certs sent + } + + SECTION("M04 Delete a specific cert -- specified cert exists") { + auto ret1 = certs->installCertificate(InstallCertificateType_CSMSRootCertificate, root_cert); + REQUIRE(ret1 == InstallCertificateStatus_Accepted); + + auto chain = makeVector("UnitTests"); + auto ret2 = certs->getCertificateIds({GetCertificateIdType_CSMSRootCertificate}, chain); + REQUIRE(ret2 == GetInstalledCertificateStatus_Accepted); + + REQUIRE(chain.size() == 1); + + auto ret3 = certs->deleteCertificate(chain.front().certificateHashData); + REQUIRE(ret3 == DeleteCertificateStatus_Accepted); + + ret2 = certs->getCertificateIds({GetCertificateIdType_CSMSRootCertificate}, chain); + REQUIRE(ret2 == GetInstalledCertificateStatus_NotFound); + + REQUIRE(chain.size() == 0); + + size_t msize; + char fn [MO_MAX_PATH_SIZE]; + printCertFn(MO_CERT_FN_CSMS_ROOT, 0, fn, MO_MAX_PATH_SIZE); + REQUIRE(filesystem->stat(fn, &msize) != 0); + } + + SECTION("M05 InstallCertificate operation") { + + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "InstallCertificate", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["certificateType"] = "CSMSRootCertificate"; //of InstallCertificateTypeEnumType + payload["certificate"] = root_cert; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + size_t msize; + char fn [MO_MAX_PATH_SIZE]; + printCertFn(MO_CERT_FN_CSMS_ROOT, 0, fn, MO_MAX_PATH_SIZE); + REQUIRE(filesystem->stat(fn, &msize) == 0); + REQUIRE(msize == strlen(root_cert)); + } + + SECTION("M04 DeleteCertificate operation") { + auto ret = certs->installCertificate(InstallCertificateType_CSMSRootCertificate, root_cert); + REQUIRE(ret == InstallCertificateStatus_Accepted); + + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "DeleteCertificate", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(4)); + auto payload = doc->to(); + payload["certificateHashData"]["hashAlgorithm"] = root_cert_hash_algorithm; //of HashAlgorithmType + payload["certificateHashData"]["issuerNameHash"] = root_cert_hash_issuer_name; + payload["certificateHashData"]["issuerKeyHash"] = root_cert_hash_issuer_key; + payload["certificateHashData"]["serialNumber"] = root_cert_hash_serial_number; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + } + + SECTION("M03 GetInstalledCertificateIds operation") { + auto ret = certs->installCertificate(InstallCertificateType_CSMSRootCertificate, root_cert); + REQUIRE(ret == InstallCertificateStatus_Accepted); + + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "GetInstalledCertificateIds", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(1)); + auto payload = doc->to(); + payload["certificateType"][0] = "CSMSRootCertificate"; //of GetCertificateIdTypeEnumType + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); + REQUIRE( payload["certificateHashDataChain"].size() == 1 ); + JsonObject certificateHashDataChain = payload["certificateHashDataChain"][0]; + REQUIRE( !strcmp(certificateHashDataChain["certificateType"] | "_Undefined", "CSMSRootCertificate") ); + JsonObject certificateHashData = certificateHashDataChain["certificateHashData"]; + REQUIRE( !strcmp(certificateHashData["hashAlgorithm"] | "_Undefined", root_cert_hash_algorithm) ); //if this fails, please update the precomputed test hashes + REQUIRE( !strcmp(certificateHashData["issuerNameHash"] | "_Undefined", root_cert_hash_issuer_name) ); + REQUIRE( !strcmp(certificateHashData["issuerKeyHash"] | "_Undefined", root_cert_hash_issuer_key) ); + REQUIRE( !strcmp(certificateHashData["serialNumber"] | "_Undefined", root_cert_hash_serial_number) ); + REQUIRE( !certificateHashDataChain.containsKey("childCertificateHashData") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + } + + mocpp_deinitialize(); +} + +#else +#warning Certificates unit tests depend on MbedTLS +#endif //MO_ENABLE_MBEDTLS diff --git a/tests/ChargePointError.cpp b/tests/ChargePointError.cpp new file mode 100644 index 00000000..5f498de6 --- /dev/null +++ b/tests/ChargePointError.cpp @@ -0,0 +1,339 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include "./helpers/testHelper.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#define BASE_TIME "2023-01-01T00:00:00.000Z" +#define BASE_TIME_1H "2023-01-01T01:00:00.000Z" +#define FTP_URL "ftps://localhost/firmware.bin" + +#define ERROR_INFO_EXAMPLE "error description" +#define ERROR_INFO_LOW_1 "low severity 1" +#define ERROR_INFO_LOW_2 "low severity 2" +#define ERROR_INFO_HIGH "high severity" + +#define ERROR_VENDOR_ID "mVendorId" +#define ERROR_VENDOR_CODE "mVendorErrorCode" + +using namespace MicroOcpp; + +TEST_CASE( "ChargePointError" ) { + printf("\nRun %s\n", "ChargePointError"); + + //clean state + auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); + FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); + + //initialize Context with dummy socket + LoopbackConnection loopback; + + mocpp_set_timer(custom_timer_cb); + + mocpp_initialize(loopback, ChargerCredentials("test-runner")); + auto& model = getOcppContext()->getModel(); + auto fwService = getFirmwareService(); + SECTION("FirmwareService initialized") { + REQUIRE(fwService != nullptr); + } + + model.getClock().setTime(BASE_TIME); + + loop(); + + SECTION("Err and resolve (soft error)") { + + bool errorCondition = false; + + addErrorDataInput([&errorCondition] () -> ErrorData { + if (errorCondition) { + ErrorData error = "OtherError"; + error.isFaulted = false; + error.info = ERROR_INFO_EXAMPLE; + error.vendorId = ERROR_VENDOR_ID; + error.vendorErrorCode = ERROR_VENDOR_CODE; + return error; + } + return nullptr; + }); + + //test error condition during transaction to check if status remains unchanged + + beginTransaction("mIdTag"); + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + REQUIRE( isOperative() ); + + bool checkProcessed = false; + + getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("StatusNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + checkProcessed = true; + REQUIRE( !strcmp(payload["errorCode"] | "_Undefined", "OtherError") ); + REQUIRE( !strcmp(payload["info"] | "_Undefined", ERROR_INFO_EXAMPLE) ); + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Charging") ); + REQUIRE( !strcmp(payload["vendorId"] | "_Undefined", ERROR_VENDOR_ID) ); + REQUIRE( !strcmp(payload["vendorErrorCode"] | "_Undefined", ERROR_VENDOR_CODE) ); + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); + + errorCondition = true; + + loop(); + + REQUIRE( checkProcessed ); + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + REQUIRE( isOperative() ); + +#if MO_REPORT_NOERROR + checkProcessed = false; + getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("StatusNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + checkProcessed = true; + REQUIRE( !strcmp(payload["errorCode"] | "_Undefined", "NoError") ); + REQUIRE( !payload.containsKey("info") ); + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Charging") ); + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); +#else + checkProcessed = true; +#endif //MO_REPORT_NOERROR + + errorCondition = false; + + loop(); + + REQUIRE( checkProcessed ); + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + REQUIRE( isOperative() ); + + } + + SECTION("Err and resolve (fatal)") { + + bool errorCondition = false; + + addErrorCodeInput([&errorCondition] () { + return errorCondition ? "OtherError" : nullptr; + }); + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + REQUIRE( isOperative() ); + + errorCondition = true; + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Faulted ); + REQUIRE( !isOperative() ); + + errorCondition = false; + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + REQUIRE( isOperative() ); + } + + SECTION("Error severity") { + + bool errorConditionLow1 = false; + bool errorConditionLow2 = false; + bool errorConditionHigh = false; + + addErrorDataInput([&errorConditionLow1] () -> ErrorData { + if (errorConditionLow1) { + ErrorData error = "OtherError"; + error.severity = 1; + error.info = ERROR_INFO_LOW_1; + return error; + } + return nullptr; + }); + + addErrorDataInput([&errorConditionLow2] () -> ErrorData { + if (errorConditionLow2) { + ErrorData error = "OtherError"; + error.severity = 1; + error.info = ERROR_INFO_LOW_2; + return error; + } + return nullptr; + }); + + addErrorDataInput([&errorConditionHigh] () -> ErrorData { + if (errorConditionHigh) { + ErrorData error = "OtherError"; + error.severity = 2; + error.info = ERROR_INFO_HIGH; + return error; + } + return nullptr; + }); + + const char *errorCode = "*"; + bool checkErrorCode = false; + const char *errorInfo = "*"; + bool checkErrorInfo = false; + + getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", + [&checkErrorCode, &checkErrorInfo, &errorInfo, &errorCode] () { + return new Ocpp16::CustomOperation("StatusNotification", + [&checkErrorCode, &checkErrorInfo, &errorInfo, &errorCode] (JsonObject payload) { + //process req + if (strcmp(errorInfo, "*")) { + MO_DBG_DEBUG("expect \"%s\", got \"%s\"", errorInfo, payload["info"] | "_Undefined"); + REQUIRE( !strcmp(payload["info"] | "_Undefined", errorInfo) ); + checkErrorInfo = true; + } + if (strcmp(errorCode, "*")) { + MO_DBG_DEBUG("expect \"%s\", got \"%s\"", errorCode, payload["errorCode"] | "_Undefined"); + REQUIRE( !strcmp(payload["errorCode"] | "_Undefined", errorCode) ); + checkErrorCode = true; + } + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); + + //sequence: low-level error 1, low-level error 2, then severe error -- all errors should go through + MO_DBG_INFO("test sequence: low-level error 1, low-level error 2, then severe error"); + + errorConditionLow1 = true; + errorInfo = ERROR_INFO_LOW_1; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionLow2 = true; + errorInfo = ERROR_INFO_LOW_2; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionHigh = true; + errorInfo = ERROR_INFO_HIGH; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionLow1 = false; + errorConditionLow2 = false; + errorConditionHigh = false; + errorInfo = "*"; + loop(); + + //sequence: low-level error 1, severe error, then low-level error 2 -- last error gets muted until severe error is resolved + MO_DBG_INFO("test sequence: low-level error 1, severe error, then low-level error 2"); + + errorConditionLow1 = true; + errorInfo = ERROR_INFO_LOW_1; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionHigh = true; + errorInfo = ERROR_INFO_HIGH; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionLow2 = true; + checkErrorInfo = false; + loop(); + REQUIRE( !checkErrorInfo ); + + errorConditionHigh = false; + errorInfo = ERROR_INFO_LOW_2; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionLow1 = false; + errorConditionLow2 = false; + errorConditionHigh = false; + errorInfo = "*"; + loop(); + + //sequence: low-level error 1, severe error, then severe error gets resolved -- low-level error is reported again + MO_DBG_INFO("test sequence: low-level error 1, severe error, then severe error gets resolved"); + + errorConditionLow1 = true; + errorInfo = ERROR_INFO_LOW_1; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionHigh = true; + errorInfo = ERROR_INFO_HIGH; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionHigh = false; + errorInfo = ERROR_INFO_LOW_1; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionLow1 = false; + errorConditionLow2 = false; + errorConditionHigh = false; + errorInfo = "*"; + loop(); + + //sequence: error, then error gets resolved -- report NoError + MO_DBG_INFO("test sequence: error, then error gets resolved"); + + errorConditionLow1 = true; + errorInfo = ERROR_INFO_LOW_1; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionLow1 = false; + errorInfo = "*"; + errorCode = "NoError"; + checkErrorCode = false; + loop(); + REQUIRE( checkErrorCode ); + } + + endTransaction(); + mocpp_deinitialize(); + +} diff --git a/tests/ChargingSessions.cpp b/tests/ChargingSessions.cpp index 8eb98fcb..82090f16 100644 --- a/tests/ChargingSessions.cpp +++ b/tests/ChargingSessions.cpp @@ -1,15 +1,23 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + #include #include #include #include #include -#include +#include #include #include #include -#include "./catch2/catch.hpp" +#include +#include +#include #include "./helpers/testHelper.h" +#include + #define BASE_TIME "2023-01-01T00:00:00.000Z" using namespace MicroOcpp; @@ -34,7 +42,7 @@ TEST_CASE( "Charging sessions" ) { std::array expectedSN {"Available", "Available"}; std::array checkedSN {false, false}; - checkMsg.registerOperation("StatusNotification", [] () -> Operation* {return new Ocpp16::StatusNotification(0, ChargePointStatus::NOT_SET, MIN_TIME);}); + 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; @@ -46,7 +54,7 @@ TEST_CASE( "Charging sessions" ) { SECTION("Check idle state"){ bool checkedBN = false; - checkMsg.registerOperation("BootNotification", [engine] () -> Operation* {return new Ocpp16::BootNotification(engine->getModel(), std::unique_ptr(new DynamicJsonDocument(0)));}); + 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"); @@ -182,7 +190,7 @@ TEST_CASE( "Charging sessions" ) { SECTION("Preboot transactions - tx before BootNotification") { mocpp_deinitialize(); - loopback.setConnected(false); + loopback.setOnline(false); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); @@ -232,7 +240,7 @@ TEST_CASE( "Charging sessions" ) { REQUIRE((adjustmentDelay > 3600 - 10 && adjustmentDelay < 3600 + 10)); }); - loopback.setConnected(true); + loopback.setOnline(true); loop(); REQUIRE(checkStartProcessed); @@ -243,7 +251,7 @@ TEST_CASE( "Charging sessions" ) { mocpp_deinitialize(); - loopback.setConnected(false); + loopback.setOnline(false); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); @@ -273,7 +281,7 @@ TEST_CASE( "Charging sessions" ) { checkProcessed = true; }); - loopback.setConnected(true); + loopback.setOnline(true); loop(); @@ -294,7 +302,7 @@ TEST_CASE( "Charging sessions" ) { mocpp_deinitialize(); - loopback.setConnected(false); + loopback.setOnline(false); mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); @@ -332,13 +340,287 @@ TEST_CASE( "Charging sessions" ) { REQUIRE(adjustmentDelay == 1); }); - loopback.setConnected(true); + loopback.setOnline(true); loop(); REQUIRE(checkProcessed); } + SECTION("Preboot transactions - reject tx if limit exceeded") { + mocpp_deinitialize(); + + loopback.setConnected(false); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); + declareConfiguration(MO_CONFIG_EXT_PREFIX "SilentOfflineTransactions", false, CONFIGURATION_FN)->setBool(false); // do not start more txs if tx journal is full + configuration_save(); + + loop(); + + for (size_t i = 0; i < MO_TXRECORD_SIZE; i++) { + beginTransaction_authorized("mIdTag"); + + loop(); + + REQUIRE(isTransactionRunning()); + + endTransaction(); + + loop(); + + REQUIRE(!isTransactionRunning()); + } + + // now, tx journal is full. Block any further charging session + + auto tx_success = beginTransaction_authorized("mIdTag"); + REQUIRE( !tx_success ); + + loop(); + + REQUIRE(!isTransactionRunning()); + REQUIRE(!ocppPermitsCharge()); + + // Check if all 4 cached transctions are transmitted after going online + + const int txId_base = 10000; + int txId_generate = txId_base; + int txId_confirm = txId_base; + + getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&txId_generate] () { + return new Ocpp16::CustomOperation("StartTransaction", + [] (JsonObject payload) {}, //ignore req + [&txId_generate] () { + //create conf + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + + JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); + idTagInfo["status"] = "Accepted"; + txId_generate++; + payload["transactionId"] = txId_generate; + return doc; + });}); + + getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&txId_generate, &txId_confirm] () { + return new Ocpp16::CustomOperation("StopTransaction", + [&txId_generate, &txId_confirm] (JsonObject payload) { + //receive req + REQUIRE( payload["transactionId"].as() == txId_generate ); + REQUIRE( payload["transactionId"].as() == txId_confirm + 1 ); + txId_confirm = payload["transactionId"].as(); + }, + [] () { + //create conf + return createEmptyDocument(); + });}); + + loopback.setConnected(true); + loop(); + + REQUIRE( txId_confirm == txId_base + MO_TXRECORD_SIZE ); + } + + SECTION("Preboot transactions - charge without tx if limit exceeded") { + mocpp_deinitialize(); + + loopback.setConnected(false); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); + declareConfiguration(MO_CONFIG_EXT_PREFIX "SilentOfflineTransactions", false, CONFIGURATION_FN)->setBool(true); // don't report further transactions to server but charge anyway + configuration_save(); + + loop(); + + for (size_t i = 0; i < MO_TXRECORD_SIZE; i++) { + beginTransaction_authorized("mIdTag"); + + loop(); + + REQUIRE(isTransactionRunning()); + + endTransaction(); + + loop(); + + REQUIRE(!isTransactionRunning()); + } + + // now, tx journal is full. Block any further charging session + + auto tx_success = beginTransaction_authorized("mIdTag"); + REQUIRE( tx_success ); + + loop(); + + REQUIRE(isTransactionRunning()); + REQUIRE(ocppPermitsCharge()); + + endTransaction(); + + loop(); + + // Check if all 4 cached transctions are transmitted after going online + + const int txId_base = 10000; + int txId_generate = txId_base; + int txId_confirm = txId_base; + + getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&txId_generate] () { + return new Ocpp16::CustomOperation("StartTransaction", + [] (JsonObject payload) {}, //ignore req + [&txId_generate] () { + //create conf + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + + JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); + idTagInfo["status"] = "Accepted"; + txId_generate++; + payload["transactionId"] = txId_generate; + return doc; + });}); + + getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&txId_generate, &txId_confirm] () { + return new Ocpp16::CustomOperation("StopTransaction", + [&txId_generate, &txId_confirm] (JsonObject payload) { + //receive req + REQUIRE( payload["transactionId"].as() == txId_generate ); + REQUIRE( payload["transactionId"].as() == txId_confirm + 1 ); + txId_confirm = payload["transactionId"].as(); + }, + [] () { + //create conf + return createEmptyDocument(); + });}); + + loopback.setConnected(true); + loop(); + + REQUIRE( txId_confirm == txId_base + MO_TXRECORD_SIZE ); + } + + SECTION("Preboot transactions - mix PreBoot with Offline tx") { + + /* + * The charger boots and connects to the OCPP server normally. It looses connection and then starts + * transaction #1 which is persisted on flash. Then a power loss occurs, but the charger doesn't reconnect. + * Start transaction #2 in PreBoot mode. Trigger another power loss, start transaction #3 while still + * being offline and then, after reconnection to the server, transaction #4. + * + * Tx #1 can be fully restored. The timestamp information for Tx #2 is missing, so it is discarded. Tx #3 is + * missing absolute timestamps at first, but after reconnection with the server, the timestamps get updated + * with absolute values from the server. Tx #4 is the standard case for transactions and should start normally. + */ + + // use idTags to identify the transactions + const char *tx1_idTag = "Tx#1"; + const char *tx2_idTag = "Tx#2"; + const char *tx3_idTag = "Tx#3"; + const char *tx4_idTag = "Tx#4"; + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true)->setBool(true); + declareConfiguration("AllowOfflineTxForUnknownId", true)->setBool(true); + configuration_save(); + loop(); + + // start Tx #1 (offline tx) + loopback.setConnected(false); + + MO_DBG_DEBUG("begin tx (%s)", tx1_idTag); + beginTransaction(tx1_idTag); + loop(); + REQUIRE(isTransactionRunning()); + endTransaction(); + loop(); + REQUIRE(!isTransactionRunning()); + + // first power cycle + mocpp_deinitialize(); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + loop(); + + // start Tx #2 (PreBoot tx, won't get timestamp) + + MO_DBG_DEBUG("begin tx (%s)", tx2_idTag); + beginTransaction(tx2_idTag); + loop(); + REQUIRE(isTransactionRunning()); + endTransaction(); + loop(); + REQUIRE(!isTransactionRunning()); + + // second power cycle + mocpp_deinitialize(); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + loop(); + + // start Tx #3 (PreBoot tx, will eventually get timestamp) + + MO_DBG_DEBUG("begin tx (%s)", tx3_idTag); + beginTransaction(tx3_idTag); + loop(); + REQUIRE(isTransactionRunning()); + endTransaction(); + loop(); + REQUIRE(!isTransactionRunning()); + + // set up checks before getting online and starting Tx #4 + bool check_1 = false, check_2 = false, check_3 = false, check_4 = false; + + getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", + [&check_1, &check_2, &check_3, &check_4, + tx1_idTag, tx2_idTag, tx3_idTag, tx4_idTag] () { + return new Ocpp16::CustomOperation("StartTransaction", + [&check_1, &check_2, &check_3, &check_4, + tx1_idTag, tx2_idTag, tx3_idTag, tx4_idTag] (JsonObject payload) { + //process req + const char *idTag = payload["idTag"] | "_Undefined"; + if (!strcmp(idTag, tx1_idTag )) { + check_1 = true; + } else if (!strcmp(idTag, tx2_idTag )) { + check_2 = true; + } else if (!strcmp(idTag, tx3_idTag )) { + check_3 = true; + } else if (!strcmp(idTag, tx4_idTag )) { + check_4 = true; + } + }, + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + + JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); + idTagInfo["status"] = "Accepted"; + static int uniqueTxId = 1000; + payload["transactionId"] = uniqueTxId++; //sample data for debug purpose + return doc; + });}); + + // get online + loopback.setConnected(true); + loop(); + + // start Tx #4 + MO_DBG_DEBUG("begin tx (%s)", tx4_idTag); + beginTransaction(tx4_idTag); + loop(); + REQUIRE(isTransactionRunning()); + endTransaction(); + loop(); + REQUIRE(!isTransactionRunning()); + + // evaluate results + REQUIRE( check_1 ); + REQUIRE( !check_2 ); // critical data for Tx #2 got lost so it must be discarded + REQUIRE( check_3 ); + REQUIRE( check_4 ); + } + SECTION("Set Unavaible"){ beginTransaction("mIdTag"); @@ -346,7 +628,7 @@ TEST_CASE( "Charging sessions" ) { loop(); auto connector = getOcppContext()->getModel().getConnector(1); - REQUIRE(connector->getStatus() == ChargePointStatus::Charging); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); REQUIRE(isOperative()); bool checkProcessed = false; @@ -355,7 +637,7 @@ TEST_CASE( "Charging sessions" ) { "ChangeAvailability", [] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["connectorId"] = 1; payload["type"] = "Inoperative"; @@ -371,7 +653,7 @@ TEST_CASE( "Charging sessions" ) { loop(); REQUIRE(checkProcessed); - REQUIRE(connector->getStatus() == ChargePointStatus::Charging); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); REQUIRE(isOperative()); mocpp_deinitialize(); @@ -381,22 +663,621 @@ TEST_CASE( "Charging sessions" ) { loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Charging); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); REQUIRE(isOperative()); endTransaction(); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Unavailable); + REQUIRE(connector->getStatus() == ChargePointStatus_Unavailable); REQUIRE(!isOperative()); connector->setAvailability(true); - REQUIRE(connector->getStatus() == ChargePointStatus::Available); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); REQUIRE(isOperative()); } - mocpp_deinitialize(); + SECTION("UnlockConnector") { + // UnlockConnector handler + + beginTransaction_authorized("mIdTag"); + + loop(); + REQUIRE( isTransactionRunning() ); + + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest( + new MicroOcpp::Ocpp16::CustomOperation("UnlockConnector", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["connectorId"] = 1; + return doc; + }, + [&checkProcessed] (JsonObject payload) { + //process conf + checkProcessed = true; + REQUIRE( !strcmp(payload["status"] | "_Undefined", "NotSupported") ); + }))); + + loop(); + REQUIRE( checkProcessed ); + REQUIRE( isTransactionRunning() ); // NotSupported doesn't lead to transaction stop + +#if MO_ENABLE_CONNECTOR_LOCK + + setOnUnlockConnectorInOut([] () -> UnlockConnectorResult { + // connector lock fails + return UnlockConnectorResult_UnlockFailed; + }); + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest( + new MicroOcpp::Ocpp16::CustomOperation("UnlockConnector", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["connectorId"] = 1; + return doc; + }, + [&checkProcessed] (JsonObject payload) { + //process conf + checkProcessed = true; + REQUIRE( !strcmp(payload["status"] | "_Undefined", "UnlockFailed") ); + }))); + + loop(); + REQUIRE( checkProcessed ); + REQUIRE( !isTransactionRunning() ); // Stop tx when UnlockConnector generally supported + + setOnUnlockConnectorInOut([] () -> UnlockConnectorResult { + // connector lock times out + return UnlockConnectorResult_Pending; + }); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest( + new MicroOcpp::Ocpp16::CustomOperation("UnlockConnector", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["connectorId"] = 1; + return doc; + }, + [&checkProcessed] (JsonObject payload) { + //process conf + checkProcessed = true; + REQUIRE( !strcmp(payload["status"] | "_Undefined", "UnlockFailed") ); + }))); + + loop(); + mtime += MO_UNLOCK_TIMEOUT; // increment clock so that MO_UNLOCK_TIMEOUT expires + loop(); + REQUIRE( checkProcessed ); + +#else + endTransaction(); + loop(); +#endif //MO_ENABLE_CONNECTOR_LOCK + + } + + SECTION("TxStartPoint - PowerPathClosed") { + + declareConfiguration(MO_CONFIG_EXT_PREFIX "TxStartOnPowerPathClosed", true)->setBool(true); + + // precondition: charge not allowed + REQUIRE( !ocppPermitsCharge() ); + REQUIRE( !isTransactionRunning() ); + + setConnectorPluggedInput([] () {return false;}); // TxStartOnPowerPathClosed removes ConnectorPlugged as a prerequisite of transactions + setEvReadyInput([] () {return false;}); // TxStartOnPowerPathClosed puts EvReady in the role of ConnectorPlugged in conventional transactions + + beginTransaction("mIdTag"); + + loop(); + + // in contrast to conventional tx mode, charge permission is granted before transaction. PowerPathClosed is a prerequisite of transactions + REQUIRE( ocppPermitsCharge() ); + REQUIRE( !isTransactionRunning() ); + + setConnectorPluggedInput([] () {return true;}); // ConnectorPlugged not sufficient to start tx + + loop(); + + REQUIRE( ocppPermitsCharge() ); + REQUIRE( !isTransactionRunning() ); + + setEvReadyInput([] () {return true;}); // now, close PowerPath. Transaction will start now + + loop(); + + REQUIRE( ocppPermitsCharge() ); + REQUIRE( isTransactionRunning() ); + + endTransaction(); + + loop(); + + } + + SECTION("TransactionMessageAttempts-/RetryInterval") { + + /* + * Scenarios: + * - final failure to send txMsg after tx terminated + * - normal communication restored after a final failure + * - StartTx fails finally during tx + * - StartTx works but StopTx fails finally after tx terminated + * - sends attempts fail until final attempt succeeds + * - after reboot, continue attempting + */ + + declareConfiguration("TransactionMessageAttempts", 1)->setInt(1); + + bool checkProcessedStartTx = false; + bool checkProcessedStopTx = false; + unsigned int txId = 1000; + + /* + * - final failure to send txMsg after tx terminated + */ + + getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&checkProcessedStartTx, &txId] () { + return new Ocpp16::CustomOperation("StartTransaction", + [&checkProcessedStartTx] (JsonObject payload) { + //receive req + checkProcessedStartTx = true; + }, + [&txId] () { + //create conf + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + + JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); + idTagInfo["status"] = "Accepted"; + payload["transactionId"] = txId++; + return doc; + });}); + + getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&checkProcessedStopTx] () { + return new Ocpp16::CustomOperation("StopTransaction", + [&checkProcessedStopTx] (JsonObject payload) { + //receive req + checkProcessedStopTx = true; + }, + [] () { + //create conf + return createEmptyDocument(); + });}); + + loopback.setOnline(false); + + REQUIRE( !ocppPermitsCharge() ); + + beginTransaction_authorized("mIdTag"); + loop(); + REQUIRE( ocppPermitsCharge() ); + + endTransaction(); + loop(); + REQUIRE( !ocppPermitsCharge() ); + + mtime += 10 * 60 * 1000; //jump 10 minutes into future + + loopback.setOnline(true); + loop(); + + REQUIRE( !checkProcessedStartTx ); + REQUIRE( !checkProcessedStopTx ); + + /* + * - normal communication restored after a final failure + */ + checkProcessedStartTx = false; + checkProcessedStopTx = false; + + beginTransaction_authorized("mIdTag"); + loop(); + REQUIRE( ocppPermitsCharge() ); + REQUIRE( checkProcessedStartTx ); + + endTransaction(); + loop(); + REQUIRE( !ocppPermitsCharge() ); + + REQUIRE( checkProcessedStopTx ); + + /* + * - StartTx fails finally during tx + */ + + checkProcessedStartTx = false; + checkProcessedStopTx = false; + + loopback.setOnline(false); + + REQUIRE( !ocppPermitsCharge() ); + + beginTransaction_authorized("mIdTag"); + loop(); + REQUIRE( ocppPermitsCharge() ); + + mtime += 10 * 60 * 1000; //jump 10 minutes into future + loop(); + REQUIRE( !ocppPermitsCharge() ); + + loopback.setOnline(true); + loop(); + + REQUIRE( !checkProcessedStartTx ); + REQUIRE( !checkProcessedStopTx ); + + /* + * - StartTx works but StopTx fails finally after tx terminated + */ + + checkProcessedStartTx = false; + checkProcessedStopTx = false; + + beginTransaction_authorized("mIdTag"); + loop(); + REQUIRE( ocppPermitsCharge() ); + REQUIRE( checkProcessedStartTx ); + + loopback.setOnline(false); + + endTransaction(); + loop(); + mtime += 10 * 60 * 1000; //jump 10 minutes into future + + loopback.setOnline(true); + loop(); + REQUIRE( !checkProcessedStopTx ); + + /* + * - sends attempts fail until final attempt succeeds + */ + + const size_t NUM_ATTEMPTS = 3; + const int RETRY_INTERVAL_SECS = 3600; + + declareConfiguration("TransactionMessageAttempts", 0)->setInt(NUM_ATTEMPTS); + declareConfiguration("TransactionMessageRetryInterval", 0)->setInt(RETRY_INTERVAL_SECS); + + configuration_save(); + + checkProcessedStartTx = false; + checkProcessedStopTx = false; + + unsigned int attemptNr = 0; + + getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&checkProcessedStartTx, &txId, &attemptNr] () { + return new Ocpp16::CustomOperation("StartTransaction", + [&attemptNr] (JsonObject payload) { + //receive req + attemptNr++; + }, + [&txId] () { + //create conf + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + + JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); + idTagInfo["status"] = "Accepted"; + payload["transactionId"] = txId++; + return doc; + }, + [&attemptNr] () { + //ErrorCode for CALLERROR + return attemptNr < NUM_ATTEMPTS ? "InternalError" : (const char*)nullptr; + });}); + + beginTransaction_authorized("mIdTag"); + loop(); + REQUIRE( ocppPermitsCharge() ); + REQUIRE( attemptNr == 1 ); + + mtime += (unsigned long)RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE( ocppPermitsCharge() ); + REQUIRE( attemptNr == 2 ); + + mtime += 2 * (unsigned long)RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE( ocppPermitsCharge() ); + REQUIRE( attemptNr == 3 ); + + mtime += 100 * (unsigned long)RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE( ocppPermitsCharge() ); + REQUIRE( attemptNr == 3 ); //no further retry after third and successful attempt + + endTransaction(); + loop(); + REQUIRE( !ocppPermitsCharge() ); + REQUIRE( attemptNr == 3 ); + REQUIRE( checkProcessedStopTx ); + + /* + * - after reboot, continue attempting + */ + + getOcppContext()->getModel().getClock().setTime(BASE_TIME); //reset system time to have roughly the same time after reboot + + checkProcessedStartTx = false; + checkProcessedStopTx = false; + attemptNr = 0; + + beginTransaction_authorized("mIdTag"); + loop(); + REQUIRE( ocppPermitsCharge() ); + REQUIRE( attemptNr == 1 ); + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + getOcppContext()->getModel().getClock().setTime(BASE_TIME); + + getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&checkProcessedStartTx, &txId, &attemptNr] () { + return new Ocpp16::CustomOperation("StartTransaction", + [&attemptNr] (JsonObject payload) { + //receive req + attemptNr++; + }, + [&txId] () { + //create conf + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + + JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); + idTagInfo["status"] = "Accepted"; + payload["transactionId"] = txId++; + return doc; + }, + [&attemptNr] () { + //ErrorCode for CALLERROR + return attemptNr < NUM_ATTEMPTS ? "InternalError" : (const char*)nullptr; + });}); + + getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&checkProcessedStopTx] () { + return new Ocpp16::CustomOperation("StopTransaction", + [&checkProcessedStopTx] (JsonObject payload) { + //receive req + checkProcessedStopTx = true; + }, + [] () { + //create conf + return createEmptyDocument(); + });}); + + loop(); + + REQUIRE( ocppPermitsCharge() ); + REQUIRE( attemptNr == 1 ); + + mtime += (unsigned long)RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE( ocppPermitsCharge() ); + REQUIRE( attemptNr == 2 ); + + mtime += 2 * (unsigned long)RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE( ocppPermitsCharge() ); + REQUIRE( attemptNr == 3 ); + + mtime += 100 * (unsigned long)RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE( ocppPermitsCharge() ); + REQUIRE( attemptNr == 3 ); //no further retry after third and successful attempt + + endTransaction(); + loop(); + REQUIRE( !ocppPermitsCharge() ); + REQUIRE( attemptNr == 3 ); + REQUIRE( checkProcessedStopTx ); + } + + SECTION("StatusNotification") { + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials()); + + bool checkProcessed = false; + const char *checkStatus = ""; + + getOcppContext()->getOperationRegistry().setOnRequest("StatusNotification", + [&checkProcessed, &checkStatus] (JsonObject payload) { + //process req + if (payload["connectorId"].as() == 1) { + checkProcessed = true; + REQUIRE( !strcmp(payload["status"] | "_Undefined", checkStatus) ); + } + }); + + checkStatus = "Available"; + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "Preparing"; + checkProcessed = false; + setConnectorPluggedInput([] () {return true;}); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "Available"; + checkProcessed = false; + setConnectorPluggedInput([] () {return false;}); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "Preparing"; + checkProcessed = false; + beginTransaction("mIdTag"); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "Available"; + checkProcessed = false; + endTransaction("mIdTag"); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "Preparing"; + beginTransaction("mIdTag"); + loop(); + checkProcessed = false; + + checkStatus = "Charging"; + checkProcessed = false; + setConnectorPluggedInput([] () {return true;}); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "SuspendedEV"; + checkProcessed = false; + setEvReadyInput([] () {return false;}); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "SuspendedEVSE"; + checkProcessed = false; + setEvReadyInput([] () {return true;}); + setEvseReadyInput([] () {return false;}); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "Charging"; + checkProcessed = false; + setEvReadyInput([] () {return true;}); + setEvseReadyInput([] () {return true;}); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "Finishing"; + checkProcessed = false; + endTransaction(); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "Available"; + checkProcessed = false; + setConnectorPluggedInput([] () {return false;}); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "Available"; + const char *checkStatus2 = checkStatus; + checkProcessed = false; + mocpp_deinitialize(); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + getOcppContext()->getOperationRegistry().setOnRequest("StatusNotification", + [&checkProcessed, &checkStatus, &checkStatus2] (JsonObject payload) { + //process req + if (payload["connectorId"].as() == 1) { + checkProcessed = true; + REQUIRE( (!strcmp(payload["status"] | "_Undefined", checkStatus) || !strcmp(payload["status"] | "_Undefined", checkStatus2)) ); + } + }); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "Charging"; + checkStatus2 = "Preparing"; + checkProcessed = false; + beginTransaction("mIdTag"); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "Charging"; + checkStatus2 = checkStatus; + checkProcessed = false; + mocpp_deinitialize(); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + getOcppContext()->getOperationRegistry().setOnRequest("StatusNotification", + [&checkProcessed, &checkStatus] (JsonObject payload) { + //process req + if (payload["connectorId"].as() == 1) { + checkProcessed = true; + REQUIRE( !strcmp(payload["status"] | "_Undefined", checkStatus) ); + } + }); + loop(); + REQUIRE( checkProcessed ); + + checkStatus = "Available"; + checkStatus2 = checkStatus; + checkProcessed = false; + endTransaction(); + mocpp_deinitialize(); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + getOcppContext()->getOperationRegistry().setOnRequest("StatusNotification", + [&checkProcessed, &checkStatus] (JsonObject payload) { + //process req + if (payload["connectorId"].as() == 1) { + checkProcessed = true; + REQUIRE( !strcmp(payload["status"] | "_Undefined", checkStatus) ); + } + }); + loop(); + REQUIRE( checkProcessed ); + } + + SECTION("No filesystem access behavior") { + + //re-init without filesystem access + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials(), MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Deactivate)); + mocpp_set_timer(custom_timer_cb); + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + REQUIRE( !ocppPermitsCharge() ); + + for (size_t i = 0; i < 3; i++) { + + beginTransaction("mIdTag"); + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + REQUIRE( ocppPermitsCharge() ); + + endTransaction(); + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + REQUIRE( !ocppPermitsCharge() ); + } + + //Tx status will be lost over reboot + + beginTransaction("mIdTag"); + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + REQUIRE( ocppPermitsCharge() ); + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials(), MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Deactivate)); + mocpp_set_timer(custom_timer_cb); + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + REQUIRE( !ocppPermitsCharge() ); + + //Note: queueing offline transactions without FS is currently not implemented + } + + mocpp_deinitialize(); } diff --git a/tests/Configuration.cpp b/tests/Configuration.cpp index c890f96f..91fa1e28 100644 --- a/tests/Configuration.cpp +++ b/tests/Configuration.cpp @@ -1,6 +1,12 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + #include #include -#include "./catch2/catch.hpp" +#include #include "./helpers/testHelper.h" #include @@ -8,10 +14,12 @@ #include #include #include +#include #include #include -#include +#include +#include using namespace MicroOcpp; @@ -20,6 +28,12 @@ using namespace MicroOcpp; #define UNKOWN_KEY "__UnknownKey" #define GET_CONFIG_KNOWN_UNKOWN "[2,\"test-mst\",\"GetConfiguration\",{\"key\":[\"" KNOWN_KEY "\",\"" UNKOWN_KEY "\"]}]" +// some globals for the C-API tests +bool g_checkProcessed [10]; +ocpp_configuration g_configs [2]; +int g_config_values [2]; +uint16_t g_config_write_count[2]; + TEST_CASE( "Configuration" ) { printf("\nRun %s\n", "Configuration"); @@ -274,8 +288,8 @@ TEST_CASE( "Configuration" ) { "GetConfiguration", [] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - auto payload = doc->to(); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + doc->to(); return doc;}, [&checkProcessed] (JsonObject payload) { //receive conf @@ -311,7 +325,7 @@ TEST_CASE( "Configuration" ) { "GetConfiguration", [] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(2))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(2)); auto payload = doc->to(); auto key = payload.createNestedArray("key"); key.add(KNOWN_KEY); @@ -365,7 +379,7 @@ TEST_CASE( "Configuration" ) { "ChangeConfiguration", [] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = KNOWN_KEY; payload["value"] = "1234"; @@ -387,7 +401,7 @@ TEST_CASE( "Configuration" ) { "ChangeConfiguration", [] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = UNKOWN_KEY; payload["value"] = "no effect"; @@ -408,7 +422,7 @@ TEST_CASE( "Configuration" ) { "ChangeConfiguration", [] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = KNOWN_KEY; payload["value"] = "not convertible to int"; @@ -435,7 +449,7 @@ TEST_CASE( "Configuration" ) { "ChangeConfiguration", [] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = KNOWN_KEY; payload["value"] = "100234"; @@ -457,7 +471,7 @@ TEST_CASE( "Configuration" ) { "ChangeConfiguration", [] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = KNOWN_KEY; payload["value"] = "4321"; @@ -505,4 +519,91 @@ TEST_CASE( "Configuration" ) { } + SECTION("C-API") { + ocpp_configuration_container container; + memset(&container, 0, sizeof(container)); + + bool check_load = false; + + container.load = [] (void *user_data) { + g_checkProcessed[0] = true; + return true; + }; + + container.save = [] (void *user_data) { + g_checkProcessed[1] = true; + return true; + }; + + ocpp_configuration *config_predefined = &g_configs[0]; + config_predefined->get_key = [] (void *user_data) -> const char* {return "ConnectionTimeOut";}; // existing OCPP key to use custom config store + config_predefined->get_type = [] (void *user_data) -> ocpp_config_datatype {return ENUM_CDT_INT;}; + config_predefined->set_int = [] (void *user_data, int val) -> void {g_config_values[0] = val;}; + config_predefined->get_int = [] (void *user_data) -> int {return g_config_values[0];}; + config_predefined->get_write_count = [] (void *user_data) -> uint16_t {return g_config_write_count[0];}; + + container.create_configuration = [] (void *user_data, ocpp_config_datatype dt, const char *key) -> ocpp_configuration* { + ocpp_configuration *config_created = &g_configs[1]; + config_created->get_key = [] (void *user_data) -> const char* {return "MCreatedConfig";}; // non-existing key to test create_configuration function + config_created->get_type = [] (void *user_data) -> ocpp_config_datatype {return ENUM_CDT_INT;}; + config_created->set_int = [] (void *user_data, int val) -> void {g_config_values[1] = val;}; + config_created->get_int = [] (void *user_data) -> int {return g_config_values[1];}; + config_created->get_write_count = [] (void *user_data) -> uint16_t {return g_config_write_count[1];}; + return config_created; + }; + + container.remove = [] (void *user_data, const char *key) -> void { + g_checkProcessed[2] = true; + }; + + container.size = [] (void *user_data) { + return sizeof(g_configs) / sizeof(g_configs[0]); + }; + + container.get_configuration = [] (void *user_data, size_t i) -> ocpp_configuration* { + return &g_configs[i]; + }; + + container.get_configuration_by_key = [] (void *user_data, const char *key) -> ocpp_configuration* { + if (!strcmp(key, "ConnectionTimeOut")) { + return &g_configs[0]; + } else if (!strcmp(key, "MCreatedConfig")) { + return g_configs[1].get_key ? + &g_configs[1] : // createConfig has already been called + nullptr; // config hasn't been created yet + } + return nullptr; + }; + + ocpp_configuration_container_add(&container, "MContainerPath", true); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + loop(); + + REQUIRE( g_checkProcessed[0] ); + + auto test_predefined = declareConfiguration("ConnectionTimeOut", 0); + + test_predefined->setInt(12345); + REQUIRE( test_predefined->getInt() == 12345 ); + REQUIRE( config_predefined->get_int(config_predefined->user_data) == 12345 ); + + g_checkProcessed[1] = false; // check if store is executed + test_predefined->setInt(555); + g_config_write_count[0]; + + configuration_save(); + + REQUIRE( g_checkProcessed[1] ); + + // test if declaring new configs is handled + auto test_created = declareConfiguration("MCreatedConfig", 123, "MContainerPath"); + REQUIRE( test_created != nullptr ); + REQUIRE( test_created->getInt() == 123 ); + ocpp_configuration *config_created = &g_configs[1]; + REQUIRE( config_created->get_int(config_created->user_data) == 123 ); + + mocpp_deinitialize(); + } + } diff --git a/tests/ConfigurationBehavior.cpp b/tests/ConfigurationBehavior.cpp index 822eee9f..fc30b356 100644 --- a/tests/ConfigurationBehavior.cpp +++ b/tests/ConfigurationBehavior.cpp @@ -1,3 +1,7 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + #include #include #include @@ -5,7 +9,11 @@ #include #include #include -#include "./catch2/catch.hpp" +#include +#include +#include +#include +#include #include "./helpers/testHelper.h" using namespace MicroOcpp; @@ -19,8 +27,8 @@ class CustomAuthorize : public Operation { void processReq(JsonObject payload) override { //ignore payload - result is determined at construction time } - std::unique_ptr createConf() override { - auto res = std::unique_ptr(new DynamicJsonDocument(2 * JSON_OBJECT_SIZE(1))); + std::unique_ptr createConf() override { + auto res = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = res->to(); payload["idTagInfo"]["status"] = status; return res; @@ -36,8 +44,8 @@ class CustomStartTransaction : public Operation { void processReq(JsonObject payload) override { //ignore payload - result is determined at construction time } - std::unique_ptr createConf() override { - auto res = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(1))); + std::unique_ptr createConf() override { + auto res = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(1)); auto payload = res->to(); payload["idTagInfo"]["status"] = status; payload["transactionId"] = 1000; @@ -57,10 +65,10 @@ TEST_CASE( "Configuration Behavior" ) { LoopbackConnection loopback; mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); - auto engine = getOcppContext(); - auto& checkMsg = engine->getOperationRegistry(); + auto context = getOcppContext(); + auto& checkMsg = context->getOperationRegistry(); - auto connector = engine->getModel().getConnector(1); + auto connector = context->getModel().getConnector(1); mocpp_set_timer(custom_timer_cb); @@ -79,7 +87,7 @@ TEST_CASE( "Configuration Behavior" ) { setConnectorPluggedInput([] () {return false;}); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Available); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); } SECTION("set false") { @@ -88,12 +96,12 @@ TEST_CASE( "Configuration Behavior" ) { setConnectorPluggedInput([] () {return false;}); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::SuspendedEV); + REQUIRE(connector->getStatus() == ChargePointStatus_SuspendedEV); endTransaction(); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Available); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); } } @@ -106,15 +114,15 @@ TEST_CASE( "Configuration Behavior" ) { SECTION("set true") { configBool->setBool(true); - beginTransaction("mIdTag"); + beginTransaction("mIdTag_invalid"); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Available); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); - beginTransaction_authorized("mIdTag"); + beginTransaction_authorized("mIdTag_invalid2"); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Available); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); } SECTION("set false") { @@ -123,17 +131,17 @@ TEST_CASE( "Configuration Behavior" ) { beginTransaction("mIdTag"); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Available); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); beginTransaction_authorized("mIdTag"); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::SuspendedEVSE); + REQUIRE(connector->getStatus() == ChargePointStatus_SuspendedEVSE); endTransaction(); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Available); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); } } @@ -142,7 +150,7 @@ TEST_CASE( "Configuration Behavior" ) { auto authorizationTimeoutInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", 1); authorizationTimeoutInt->setInt(1); //try normal Authorize for 1s, then enter offline mode - loopback.setConnected(false); //connection loss + loopback.setOnline(false); //connection loss SECTION("set true") { configBool->setBool(true); @@ -150,29 +158,30 @@ TEST_CASE( "Configuration Behavior" ) { beginTransaction("mIdTag"); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Charging); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); endTransaction(); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Available); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); } SECTION("set false") { configBool->setBool(false); beginTransaction("mIdTag"); - REQUIRE(connector->getStatus() == ChargePointStatus::Preparing); + REQUIRE(connector->getStatus() == ChargePointStatus_Preparing); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Available); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); } endTransaction(); - loopback.setConnected(true); + loopback.setOnline(true); } +#if MO_ENABLE_LOCAL_AUTH SECTION("LocalPreAuthorize") { auto configBool = declareConfiguration("LocalPreAuthorize", true); auto authorizationTimeoutInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", 20); @@ -186,7 +195,7 @@ TEST_CASE( "Configuration Behavior" ) { loopback.sendTXT(localListMsg, strlen(localListMsg)); loop(); - loopback.setConnected(false); //connection loss + loopback.setOnline(false); //connection loss SECTION("set true - accepted idtag") { configBool->setBool(true); @@ -194,7 +203,7 @@ TEST_CASE( "Configuration Behavior" ) { beginTransaction("local-idtag"); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Charging); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); } SECTION("set false") { @@ -203,17 +212,76 @@ TEST_CASE( "Configuration Behavior" ) { beginTransaction("local-idtag"); loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Preparing); + REQUIRE(connector->getStatus() == ChargePointStatus_Preparing); - loopback.setConnected(true); + loopback.setOnline(true); mtime += 20000; //Authorize will be retried after a few seconds loop(); - REQUIRE(connector->getStatus() == ChargePointStatus::Charging); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + } + + endTransaction(); + loopback.setOnline(true); + } +#endif //MO_ENABLE_LOCAL_AUTH + + SECTION("AuthorizeRemoteTxRequests") { + auto configBool = declareConfiguration("AuthorizeRemoteTxRequests", false); + + bool receivedAuthorize = false; + + setOnReceiveRequest("Authorize", [&receivedAuthorize] (JsonObject payload) { + receivedAuthorize = true; + REQUIRE( !strcmp(payload["idTag"] | "_Undefined", "mIdTag") ); + }); + + SECTION("set true") { + configBool->setBool(true); + + context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTag"] = "mIdTag"; + return doc;}, + [] (JsonObject) { + //ignore conf + } + ))); + + loop(); + + REQUIRE(receivedAuthorize); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + } + + SECTION("set false") { + configBool->setBool(false); + + context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTag"] = "mIdTag"; + return doc;}, + [] (JsonObject) { + //ignore conf + } + ))); + + loop(); + + REQUIRE(!receivedAuthorize); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); } endTransaction(); - loopback.setConnected(true); + loop(); } mocpp_deinitialize(); diff --git a/tests/FirmwareManagement.cpp b/tests/FirmwareManagement.cpp new file mode 100644 index 00000000..5f11b8c4 --- /dev/null +++ b/tests/FirmwareManagement.cpp @@ -0,0 +1,597 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include "./helpers/testHelper.h" + +#include +#include +#include + +#include +#include +#include + +#include + +#define BASE_TIME "2023-01-01T00:00:00.000Z" +#define BASE_TIME_1H "2023-01-01T01:00:00.000Z" +#define FTP_URL "ftps://localhost/firmware.bin" + +using namespace MicroOcpp; + +TEST_CASE( "FirmwareManagement" ) { + printf("\nRun %s\n", "FirmwareManagement"); + + //clean state + auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); + FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); + + //initialize Context with dummy socket + LoopbackConnection loopback; + + mocpp_set_timer(custom_timer_cb); + + mocpp_initialize(loopback, ChargerCredentials("test-runner")); + auto& model = getOcppContext()->getModel(); + auto fwService = getFirmwareService(); + SECTION("FirmwareService initialized") { + REQUIRE(fwService != nullptr); + } + + model.getClock().setTime(BASE_TIME); + + loop(); + + SECTION("Unconfigured FW service") { + + bool checkProcessed = false; + + getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("FirmwareStatusNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + checkProcessed = true; + REQUIRE(( + !strcmp(payload["status"] | "_Undefined", "DownloadFailed") || + !strcmp(payload["status"] | "_Undefined", "InstallationFailed") + )); + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "UpdateFirmware", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); + auto payload = doc->to(); + payload["location"] = FTP_URL; + payload["retries"] = 1; + payload["retrieveDate"] = BASE_TIME; + payload["retryInterval"] = 1; + return doc;}, + [] (JsonObject) { } //ignore conf + ))); + + loop(); + + REQUIRE(checkProcessed); + } + + SECTION("Download phase only") { + + int checkProcessed = 0; + + getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("FirmwareStatusNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + if (checkProcessed == 0) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloading") ); + checkProcessed++; + } else if (checkProcessed == 1) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloaded") ); + checkProcessed++; + } else if (checkProcessed == 2) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installed") ); + checkProcessed++; + } + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); + + bool checkProcessedOnDownload = false; + + fwService->setOnDownload([&checkProcessedOnDownload] (const char *location) { + checkProcessedOnDownload = true; + return true; + }); + + int checkProcessedOnDownloadStatus = 0; + + fwService->setDownloadStatusInput([&checkProcessed, &checkProcessedOnDownloadStatus] () { + if (checkProcessed == 0) { + if (checkProcessedOnDownloadStatus == 0) checkProcessedOnDownloadStatus = 1; + return DownloadStatus::NotDownloaded; + } else { + if (checkProcessedOnDownloadStatus == 1) checkProcessedOnDownloadStatus = 2; + return DownloadStatus::Downloaded; + } + }); + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "UpdateFirmware", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); + auto payload = doc->to(); + payload["location"] = FTP_URL; + payload["retries"] = 1; + payload["retrieveDate"] = BASE_TIME; + payload["retryInterval"] = 1; + return doc;}, + [] (JsonObject) { } //ignore conf + ))); + + for (unsigned int i = 0; i < 10; i++) { + loop(); + mtime += 5000; + } + + REQUIRE( checkProcessed == 3 ); //all FirmwareStatusNotification messages have been received + REQUIRE( checkProcessedOnDownload ); + REQUIRE( checkProcessedOnDownloadStatus == 2 ); + } + + SECTION("Installation phase only") { + + int checkProcessed = 0; + + getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("FirmwareStatusNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + if (checkProcessed == 0) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installing") ); + checkProcessed++; + } else if (checkProcessed == 1) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installed") ); + checkProcessed++; + } + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); + + bool checkProcessedOnInstall = false; + + fwService->setOnInstall([&checkProcessedOnInstall] (const char *location) { + checkProcessedOnInstall = true; + REQUIRE( !strcmp(location, FTP_URL) ); + return true; + }); + + int checkProcessedOnInstallStatus = 0; + + fwService->setInstallationStatusInput([&checkProcessed, &checkProcessedOnInstallStatus] () { + if (checkProcessed == 0) { + if (checkProcessedOnInstallStatus == 0) checkProcessedOnInstallStatus = 1; + return InstallationStatus::NotInstalled; + } else { + if (checkProcessedOnInstallStatus == 1) checkProcessedOnInstallStatus = 2; + return InstallationStatus::Installed; + } + }); + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "UpdateFirmware", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); + auto payload = doc->to(); + payload["location"] = FTP_URL; + payload["retries"] = 1; + payload["retrieveDate"] = BASE_TIME; + payload["retryInterval"] = 1; + return doc;}, + [] (JsonObject) { } //ignore conf + ))); + + for (unsigned int i = 0; i < 10; i++) { + loop(); + mtime += 5000; + } + + REQUIRE( checkProcessed == 2 ); //all FirmwareStatusNotification messages have been received + REQUIRE( checkProcessedOnInstall ); + REQUIRE( checkProcessedOnInstallStatus == 2 ); + } + + SECTION("Download and install") { + + int checkProcessed = 0; + + getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("FirmwareStatusNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + if (checkProcessed == 0) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloading") ); + checkProcessed++; + } else if (checkProcessed == 1) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloaded") ); + checkProcessed++; + } else if (checkProcessed == 2) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installing") ); + checkProcessed++; + } else if (checkProcessed == 3) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installed") ); + checkProcessed++; + } + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); + + bool checkProcessedOnDownload = false; + fwService->setOnDownload([&checkProcessedOnDownload] (const char *location) { + checkProcessedOnDownload = true; + return true; + }); + + int checkProcessedOnDownloadStatus = 0; + fwService->setDownloadStatusInput([&checkProcessed, &checkProcessedOnDownloadStatus] () { + if (checkProcessed == 0) { + if (checkProcessedOnDownloadStatus == 0) checkProcessedOnDownloadStatus = 1; + return DownloadStatus::NotDownloaded; + } else { + if (checkProcessedOnDownloadStatus == 1) checkProcessedOnDownloadStatus = 2; + return DownloadStatus::Downloaded; + } + }); + + bool checkProcessedOnInstall = false; + fwService->setOnInstall([&checkProcessedOnInstall] (const char *location) { + checkProcessedOnInstall = true; + return true; + }); + + int checkProcessedOnInstallStatus = 0; + fwService->setInstallationStatusInput([&checkProcessed, &checkProcessedOnInstallStatus] () { + if (checkProcessed <= 2) { + if (checkProcessedOnInstallStatus == 0) checkProcessedOnInstallStatus = 1; + return InstallationStatus::NotInstalled; + } else { + if (checkProcessedOnInstallStatus == 1) checkProcessedOnInstallStatus = 2; + return InstallationStatus::Installed; + } + }); + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "UpdateFirmware", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); + auto payload = doc->to(); + payload["location"] = FTP_URL; + payload["retries"] = 1; + payload["retrieveDate"] = BASE_TIME; + payload["retryInterval"] = 1; + return doc;}, + [] (JsonObject) { } //ignore conf + ))); + + for (unsigned int i = 0; i < 10; i++) { + loop(); + mtime += 5000; + } + + REQUIRE( checkProcessed == 4 ); //all FirmwareStatusNotification messages have been received + REQUIRE( checkProcessedOnDownload ); + REQUIRE( checkProcessedOnDownloadStatus == 2 ); + REQUIRE( checkProcessedOnInstall ); + REQUIRE( checkProcessedOnInstallStatus == 2 ); + } + + SECTION("Download failure (try 2 times)") { + + int checkProcessed = 0; + + getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("FirmwareStatusNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + if (checkProcessed == 0) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloading") ); + checkProcessed++; + } else if (checkProcessed == 1) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "DownloadFailed") ); + checkProcessed++; + } else if (checkProcessed == 2) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloading") ); + checkProcessed++; + } else if (checkProcessed == 3) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "DownloadFailed") ); + checkProcessed++; + } + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); + + int checkProcessedOnDownload = 0; + fwService->setOnDownload([&checkProcessedOnDownload] (const char *location) { + checkProcessedOnDownload++; + return true; + }); + + int checkProcessedOnDownloadStatus = 0; + fwService->setDownloadStatusInput([&checkProcessed, &checkProcessedOnDownloadStatus] () { + if (checkProcessed == 0) { + if (checkProcessedOnDownloadStatus == 0) checkProcessedOnDownloadStatus = 1; + return DownloadStatus::NotDownloaded; + } else if (checkProcessed == 1) { + if (checkProcessedOnDownloadStatus == 1) checkProcessedOnDownloadStatus = 2; + return DownloadStatus::DownloadFailed; + } else if (checkProcessed == 2) { + if (checkProcessedOnDownloadStatus == 2) checkProcessedOnDownloadStatus = 3; + return DownloadStatus::NotDownloaded; + } else { + if (checkProcessedOnDownloadStatus == 3) checkProcessedOnDownloadStatus = 4; + return DownloadStatus::DownloadFailed; + } + }); + + bool checkProcessedOnInstall = false; // must not be executed + fwService->setOnInstall([&checkProcessedOnInstall] (const char *location) { + checkProcessedOnInstall = true; + return true; + }); + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "UpdateFirmware", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); + auto payload = doc->to(); + payload["location"] = FTP_URL; + payload["retries"] = 2; + payload["retrieveDate"] = BASE_TIME; + payload["retryInterval"] = 10; + return doc;}, + [] (JsonObject) { } //ignore conf + ))); + + for (unsigned int i = 0; i < 20; i++) { + loop(); + mtime += 5000; + } + + REQUIRE( checkProcessed == 4 ); //all FirmwareStatusNotification messages have been received + REQUIRE( checkProcessedOnDownload == 2 ); + REQUIRE( checkProcessedOnDownloadStatus == 4 ); + REQUIRE( !checkProcessedOnInstall ); + } + + SECTION("Installation failure (try 2 times)") { + + int checkProcessed = 0; + + getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("FirmwareStatusNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + if (checkProcessed == 0) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installing") ); + checkProcessed++; + } else if (checkProcessed == 1) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "InstallationFailed") ); + checkProcessed++; + } else if (checkProcessed == 2) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installing") ); + checkProcessed++; + } else if (checkProcessed == 3) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "InstallationFailed") ); + checkProcessed++; + } + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); + + int checkProcessedOnInstall = 0; + + fwService->setOnInstall([&checkProcessedOnInstall] (const char *location) { + checkProcessedOnInstall++; + return true; + }); + + int checkProcessedOnInstallStatus = 0; + + fwService->setInstallationStatusInput([&checkProcessed, &checkProcessedOnInstallStatus] () { + if (checkProcessed == 0) { + if (checkProcessedOnInstallStatus == 0) checkProcessedOnInstallStatus = 1; + return InstallationStatus::NotInstalled; + } else if (checkProcessed == 1) { + if (checkProcessedOnInstallStatus == 1) checkProcessedOnInstallStatus = 2; + return InstallationStatus::InstallationFailed; + } else if (checkProcessed == 2) { + if (checkProcessedOnInstallStatus == 2) checkProcessedOnInstallStatus = 3; + return InstallationStatus::NotInstalled; + } else { + if (checkProcessedOnInstallStatus == 3) checkProcessedOnInstallStatus = 4; + return InstallationStatus::InstallationFailed; + } + }); + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "UpdateFirmware", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); + auto payload = doc->to(); + payload["location"] = FTP_URL; + payload["retries"] = 2; + payload["retrieveDate"] = BASE_TIME; + payload["retryInterval"] = 10; + return doc;}, + [] (JsonObject) { } //ignore conf + ))); + + for (unsigned int i = 0; i < 10; i++) { + loop(); + mtime += 5000; + } + + REQUIRE( checkProcessed == 4 ); //all FirmwareStatusNotification messages have been received + REQUIRE( checkProcessedOnInstall == 2 ); + REQUIRE( checkProcessedOnInstallStatus == 4 ); + } + + SECTION("Wait for retreiveDate and charging sessions end") { + + beginTransaction("mIdTag"); + + int checkProcessed = 0; + + getOcppContext()->getOperationRegistry().registerOperation("FirmwareStatusNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("FirmwareStatusNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + if (checkProcessed == 0) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloading") ); + checkProcessed++; + } else if (checkProcessed == 1) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Downloaded") ); + checkProcessed++; + } else if (checkProcessed == 2) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installing") ); + checkProcessed++; + } else if (checkProcessed == 3) { + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Installed") ); + checkProcessed++; + } + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); + + bool checkProcessedOnDownload = false; + fwService->setOnDownload([&checkProcessedOnDownload] (const char *location) { + checkProcessedOnDownload = true; + return true; + }); + + int checkProcessedOnDownloadStatus = 0; + fwService->setDownloadStatusInput([&checkProcessed, &checkProcessedOnDownloadStatus] () { + if (checkProcessed == 0) { + if (checkProcessedOnDownloadStatus == 0) checkProcessedOnDownloadStatus = 1; + return DownloadStatus::NotDownloaded; + } else { + if (checkProcessedOnDownloadStatus == 1) checkProcessedOnDownloadStatus = 2; + return DownloadStatus::Downloaded; + } + }); + + bool checkProcessedOnInstall = false; + fwService->setOnInstall([&checkProcessedOnInstall] (const char *location) { + checkProcessedOnInstall = true; + return true; + }); + + int checkProcessedOnInstallStatus = 0; + fwService->setInstallationStatusInput([&checkProcessed, &checkProcessedOnInstallStatus] () { + if (checkProcessed <= 2) { + if (checkProcessedOnInstallStatus == 0) checkProcessedOnInstallStatus = 1; + return InstallationStatus::NotInstalled; + } else { + if (checkProcessedOnInstallStatus == 1) checkProcessedOnInstallStatus = 2; + return InstallationStatus::Installed; + } + }); + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "UpdateFirmware", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(4)); + auto payload = doc->to(); + payload["location"] = FTP_URL; + payload["retries"] = 1; + payload["retrieveDate"] = BASE_TIME_1H; + payload["retryInterval"] = 1; + return doc;}, + [] (JsonObject) { } //ignore conf + ))); + + for (unsigned int i = 0; i < 10; i++) { + loop(); + mtime += 5000; + } + + //retreiveDate not reached yet + REQUIRE( checkProcessed == 0 ); + REQUIRE( !checkProcessedOnDownload ); + REQUIRE( checkProcessedOnDownloadStatus == 0 ); + REQUIRE( !checkProcessedOnInstall ); + REQUIRE( checkProcessedOnInstallStatus == 0 ); + + getOcppContext()->getModel().getClock().setTime(BASE_TIME_1H); + + for (unsigned int i = 0; i < 10; i++) { + loop(); + mtime += 5000; + } + + //download-related FirmwareStatusNotification messages have been received + REQUIRE( checkProcessed == 2 ); + REQUIRE( checkProcessedOnDownload ); + REQUIRE( checkProcessedOnDownloadStatus == 2 ); + REQUIRE( !checkProcessedOnInstall ); + REQUIRE( checkProcessedOnInstallStatus == 0 ); + + endTransaction(); + + for (unsigned int i = 0; i < 10; i++) { + loop(); + mtime += 5000; + } + + //all FirmwareStatusNotification messages have been received + REQUIRE( checkProcessed == 4 ); + REQUIRE( checkProcessedOnDownload ); + REQUIRE( checkProcessedOnDownloadStatus == 2 ); + REQUIRE( checkProcessedOnInstall ); + REQUIRE( checkProcessedOnInstallStatus == 2 ); + } + + mocpp_deinitialize(); + +} diff --git a/tests/LocalAuthList.cpp b/tests/LocalAuthList.cpp index 9104ad87..855a980b 100644 --- a/tests/LocalAuthList.cpp +++ b/tests/LocalAuthList.cpp @@ -1,11 +1,19 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_LOCAL_AUTH + #include #include -#include "./catch2/catch.hpp" +#include #include "./helpers/testHelper.h" #include #include -#include +#include #include #include @@ -86,42 +94,42 @@ TEST_CASE( "LocalAuth" ) { //check TX notification bool checkTxAuthorized = false; setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { - if (txNotification == TxNotification::Authorized) { + if (txNotification == TxNotification_Authorized) { checkTxAuthorized = true; } }); //begin transaction and delay Authorize request - tx should start immediately - loopback.setConnected(false); //Authorize delayed by short offline period + loopback.setOnline(false); //Authorize delayed by short offline period - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("mIdTag"); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Charging ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( checkTxAuthorized ); - loopback.setConnected(true); + loopback.setOnline(true); endTransaction(); loop(); //begin transaction delay Authorize request, but idTag doesn't match local list - tx should start when online again checkTxAuthorized = false; - loopback.setConnected(false); + loopback.setOnline(false); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("wrong idTag"); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Preparing ); + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); REQUIRE( !checkTxAuthorized ); - loopback.setConnected(true); + loopback.setOnline(true); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Charging ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( checkTxAuthorized ); endTransaction(); @@ -142,61 +150,61 @@ TEST_CASE( "LocalAuth" ) { //check TX notification bool checkTxAuthorized = false; setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { - if (txNotification == TxNotification::Authorized) { + if (txNotification == TxNotification_Authorized) { checkTxAuthorized = true; } }); //make charger offline and begin tx - tx should begin after Authorize timeout - loopback.setConnected(false); + loopback.setOnline(false); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); - ulong t_before = mocpp_tick_ms(); + unsigned long t_before = mocpp_tick_ms(); beginTransaction("mIdTag"); loop(); REQUIRE( mocpp_tick_ms() - t_before < AUTH_TIMEOUT_MS ); //if this fails, increase AUTH_TIMEOUT_MS - REQUIRE( connector->getStatus() == ChargePointStatus::Preparing ); + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); REQUIRE( !checkTxAuthorized ); mtime += AUTH_TIMEOUT_MS - (mocpp_tick_ms() - t_before); //increment clock so that auth timeout is exceeded loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Charging ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( checkTxAuthorized ); - loopback.setConnected(true); + loopback.setOnline(true); endTransaction(); loop(); //make charger offline and begin tx, but idTag doesn't match - tx should be aborted bool checkTxTimeout = false; setTxNotificationOutput([&checkTxTimeout] (Transaction*, TxNotification txNotification) { - if (txNotification == TxNotification::AuthorizationTimeout) { + if (txNotification == TxNotification_AuthorizationTimeout) { checkTxTimeout = true; } }); - loopback.setConnected(false); + loopback.setOnline(false); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); t_before = mocpp_tick_ms(); beginTransaction("wrong idTag"); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Preparing ); + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); REQUIRE( !checkTxTimeout ); mtime += AUTH_TIMEOUT_MS - (mocpp_tick_ms() - t_before); //increment clock so that auth timeout is exceeded loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); REQUIRE( checkTxTimeout ); - loopback.setConnected(true); + loopback.setOnline(true); loop(); } @@ -209,28 +217,58 @@ TEST_CASE( "LocalAuth" ) { //check TX notification bool checkTxAuthorized = false; setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { - if (txNotification == TxNotification::Authorized) { + if (txNotification == TxNotification_Authorized) { checkTxAuthorized = true; } }); //make charger offline and begin tx - tx should begin after Authorize timeout - loopback.setConnected(false); + loopback.setOnline(false); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); - ulong t_before = mocpp_tick_ms(); + unsigned long t_before = mocpp_tick_ms(); beginTransaction("unknownIdTag"); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Preparing ); + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); REQUIRE( !checkTxAuthorized ); mtime += AUTH_TIMEOUT_MS - (mocpp_tick_ms() - t_before); //increment clock so that auth timeout is exceeded loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Charging ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + REQUIRE( checkTxAuthorized ); + + loopback.setOnline(true); + endTransaction(); + loop(); + } + + SECTION("Local auth - check WS online status") { + + localAuthorizeOffline->setBool(false); + localPreAuthorize->setBool(false); + MicroOcpp::declareConfiguration("AllowOfflineTxForUnknownId", true)->setBool(true); + + //check TX notification + bool checkTxAuthorized = false; + setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { + if (txNotification == TxNotification_Authorized) { + checkTxAuthorized = true; + } + }); + + //disconnect WS and begin tx - charger should enter offline mode immediately + loopback.setConnected(false); + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + beginTransaction("unknownIdTag"); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( checkTxAuthorized ); loopback.setConnected(true); @@ -256,37 +294,37 @@ TEST_CASE( "LocalAuth" ) { REQUIRE( authService->getLocalAuthorization("mIdTagExpired") ); //begin transaction and delay Authorize request - cannot PreAuthorize because entry is expired - loopback.setConnected(false); //Authorize delayed by short offline period + loopback.setOnline(false); //Authorize delayed by short offline period - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("mIdTagExpired"); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Preparing ); + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); - loopback.setConnected(true); + loopback.setOnline(true); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Charging ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); endTransaction(); loop(); //begin transaction and delay Authorize request - cannot PreAuthorize because entry is unauthorized - loopback.setConnected(false); //Authorize delayed by short offline period + loopback.setOnline(false); //Authorize delayed by short offline period - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("mIdTagUnauthorized"); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Preparing ); + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); - loopback.setConnected(true); + loopback.setOnline(true); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Charging ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); endTransaction(); loop(); } @@ -309,38 +347,38 @@ TEST_CASE( "LocalAuth" ) { REQUIRE( authService->getLocalAuthorization("mIdTagAccepted") ); //begin transaction and delay Authorize request - tx should start immediately - loopback.setConnected(false); + loopback.setOnline(false); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("mIdTagAccepted"); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Charging ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); - loopback.setConnected(true); + loopback.setOnline(true); endTransaction(); loop(); //begin transaction, but idTag is expired - AllowOfflineTxForUnknownId must not apply - loopback.setConnected(false); + loopback.setOnline(false); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); - ulong t_before = mocpp_tick_ms(); + unsigned long t_before = mocpp_tick_ms(); beginTransaction("mIdTagExpired"); loop(); REQUIRE( mocpp_tick_ms() - t_before < AUTH_TIMEOUT_MS ); //if this fails, increase AUTH_TIMEOUT_MS - REQUIRE( connector->getStatus() == ChargePointStatus::Preparing ); + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); mtime += AUTH_TIMEOUT_MS - (mocpp_tick_ms() - t_before); //increment clock so that auth timeout is exceeded loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); - loopback.setConnected(true); + loopback.setOnline(true); loop(); } @@ -352,7 +390,7 @@ TEST_CASE( "LocalAuth" ) { //check TX notification bool checkTxAuthorized = false; setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { - if (txNotification == TxNotification::Authorized) { + if (txNotification == TxNotification_Authorized) { checkTxAuthorized = true; } }); @@ -369,35 +407,35 @@ TEST_CASE( "LocalAuth" ) { [] (JsonObject) {}, //ignore req [] () { //create conf - auto doc = std::unique_ptr(new DynamicJsonDocument(2 * JSON_OBJECT_SIZE(1))); + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTagInfo"]["status"] = "Blocked"; return doc; });}); //begin transaction and delay Authorize request - tx should start immediately - loopback.setConnected(false); //Authorize delayed by short offline period + loopback.setOnline(false); //Authorize delayed by short offline period - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); beginTransaction("mIdTag"); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Charging ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( checkTxAuthorized ); //check TX notification bool checkTxRejected = false; setTxNotificationOutput([&checkTxRejected] (Transaction*, TxNotification txNotification) { - if (txNotification == TxNotification::AuthorizationRejected) { + if (txNotification == TxNotification_AuthorizationRejected) { checkTxRejected = true; } }); - loopback.setConnected(true); + loopback.setOnline(true); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); REQUIRE( checkTxRejected ); loop(); @@ -414,7 +452,7 @@ TEST_CASE( "LocalAuth" ) { }, [] () { //create conf - auto doc = std::unique_ptr(new DynamicJsonDocument(2 * JSON_OBJECT_SIZE(1))); + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTagInfo"]["status"] = "Blocked"; return doc; @@ -429,8 +467,8 @@ TEST_CASE( "LocalAuth" ) { }, [] () { //create conf - auto doc = std::unique_ptr(new DynamicJsonDocument( - JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(1))); + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTagInfo"]["status"] = "Blocked"; payload["transactionId"] = 1000; @@ -602,7 +640,7 @@ TEST_CASE( "LocalAuth" ) { int listVersion = 42; size_t listSize = 2; - std::string populatedEntryIdTag; //local auth list entry to be fully populated + auto populatedEntryIdTag = makeString("UnitTests"); //local auth list entry to be fully populated //Full update - happy path bool checkAccepted = false; @@ -610,8 +648,8 @@ TEST_CASE( "LocalAuth" ) { new Ocpp16::CustomOperation("SendLocalList", [&listVersion, &listSize, &populatedEntryIdTag] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument( - 4096)); + auto doc = makeJsonDoc("UnitTests", + 4096); auto payload = doc->to(); payload["listVersion"] = listVersion; generateAuthList(payload["localAuthorizationList"].to(), listSize, false); @@ -653,8 +691,8 @@ TEST_CASE( "LocalAuth" ) { new Ocpp16::CustomOperation("SendLocalList", [&listVersion, &listSize] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument( - 1024)); + auto doc = makeJsonDoc("UnitTests", + 1024); auto payload = doc->to(); payload["listVersion"] = listVersion; generateAuthList(payload["localAuthorizationList"].to(), listSize, false); @@ -679,8 +717,8 @@ TEST_CASE( "LocalAuth" ) { new Ocpp16::CustomOperation("SendLocalList", [&listVersion, &listSizeInvalid] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument( - 1024)); + auto doc = makeJsonDoc("UnitTests", + 1024); auto payload = doc->to(); payload["listVersion"] = listVersion; generateAuthList(payload["localAuthorizationList"].to(), listSizeInvalid, false); @@ -706,8 +744,8 @@ TEST_CASE( "LocalAuth" ) { new Ocpp16::CustomOperation("SendLocalList", [&listVersion, &listSize] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument( - 4096)); + auto doc = makeJsonDoc("UnitTests", + 4096); auto payload = doc->to(); payload["listVersion"] = listVersion; generateAuthList(payload["localAuthorizationList"].to(), listSize, false); @@ -732,8 +770,8 @@ TEST_CASE( "LocalAuth" ) { new Ocpp16::CustomOperation("SendLocalList", [&listVersion, &listSize] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument( - 4096)); + auto doc = makeJsonDoc("UnitTests", + 4096); auto payload = doc->to(); payload["listVersion"] = listVersion; generateAuthList(payload["localAuthorizationList"].to(), listSize, false); @@ -759,8 +797,8 @@ TEST_CASE( "LocalAuth" ) { new Ocpp16::CustomOperation("SendLocalList", [&listVersionInvalid, &listSizeInvalid] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument( - 4096)); + auto doc = makeJsonDoc("UnitTests", + 4096); auto payload = doc->to(); payload["listVersion"] = listVersionInvalid; generateAuthList(payload["localAuthorizationList"].to(), listSizeInvalid, false); @@ -795,8 +833,8 @@ TEST_CASE( "LocalAuth" ) { new Ocpp16::CustomOperation("SendLocalList", [&i] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument( - 1024)); + auto doc = makeJsonDoc("UnitTests", + 1024); auto payload = doc->to(); payload["listVersion"] = (int) i; @@ -818,7 +856,7 @@ TEST_CASE( "LocalAuth" ) { }))); loop(); - REQUIRE( authService->getLocalListVersion() == i ); + REQUIRE( authService->getLocalListVersion() == (int)i ); REQUIRE( authService->getLocalListSize() == i + 1 ); REQUIRE( checkAccepted ); } @@ -833,8 +871,8 @@ TEST_CASE( "LocalAuth" ) { new Ocpp16::CustomOperation("SendLocalList", [&listVersionInvalid] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument( - 1024)); + auto doc = makeJsonDoc("UnitTests", + 1024); auto payload = doc->to(); payload["listVersion"] = listVersionInvalid; @@ -887,3 +925,5 @@ TEST_CASE( "LocalAuth" ) { mocpp_deinitialize(); } + +#endif //MO_ENABLE_LOCAL_AUTH diff --git a/tests/Metering.cpp b/tests/Metering.cpp index 9b5b31d1..aa555e99 100644 --- a/tests/Metering.cpp +++ b/tests/Metering.cpp @@ -1,14 +1,21 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + #include #include #include #include +#include #include -#include -#include "./catch2/catch.hpp" +#include +#include #include "./helpers/testHelper.h" #define BASE_TIME "2023-01-01T00:00:00.000Z" +#define TRIGGER_METERVALUES "[2,\"msgId01\",\"TriggerMessage\",{\"requestedMessage\":\"MeterValues\"}]" + using namespace MicroOcpp; TEST_CASE("Metering") { @@ -46,7 +53,7 @@ TEST_CASE("Metering") { sendRequest("ChangeConfiguration", [] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = "MeterValuesSampledData"; payload["value"] = "Energy.Active.Import.Register,INVALID,Voltage"; //invalid request @@ -66,7 +73,7 @@ TEST_CASE("Metering") { sendRequest("ChangeConfiguration", [] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); auto payload = doc->to(); payload["key"] = "MeterValuesSampledData"; payload["value"] = "Voltage,Energy.Active.Import.Register"; //valid request @@ -98,19 +105,14 @@ TEST_CASE("Metering") { auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(10); - auto MeterValueCacheSizeInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "MeterValueCacheSize", 0); - MeterValueCacheSizeInt->setInt(2); - bool checkProcessed = false; setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; - Timestamp t0, t1; + Timestamp t0; t0.setTime(payload["meterValue"][0]["timestamp"] | ""); - t1.setTime(payload["meterValue"][1]["timestamp"] | ""); REQUIRE((t0 - base >= 10 && t0 - base <= 11)); - REQUIRE((t1 - base >= 20 && t1 - base <= 21)); REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["context"] | "", "Sample.Periodic")); }); @@ -127,10 +129,6 @@ TEST_CASE("Metering") { mtime = trackMtime + 10 * 1000; - loop(), - - mtime = trackMtime + 20 * 1000; - loop(); endTransaction(); @@ -160,19 +158,14 @@ TEST_CASE("Metering") { auto MeterValuesAlignedDataString = declareConfiguration("MeterValuesAlignedData", "", CONFIGURATION_FN); MeterValuesAlignedDataString->setString("Energy.Active.Import.Register"); - auto MeterValueCacheSizeInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "MeterValueCacheSize", 0, CONFIGURATION_FN); - MeterValueCacheSizeInt->setInt(2); - bool checkProcessed = false; setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; - Timestamp t0, t1; + Timestamp t0; t0.setTime(payload["meterValue"][0]["timestamp"] | ""); - t1.setTime(payload["meterValue"][1]["timestamp"] | ""); REQUIRE((t0 - base >= 900 && t0 - base <= 901)); - REQUIRE((t1 - base >= 1800 && t1 - base <= 1801)); REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["context"] | "", "Sample.Clock")); }); @@ -191,10 +184,6 @@ TEST_CASE("Metering") { loop(); model.getClock().setTime("2023-01-01T00:29:50Z"); - loop(); - - model.getClock().setTime("2023-01-01T00:30:00Z"); - loop(); endTransaction(); @@ -219,6 +208,8 @@ TEST_CASE("Metering") { auto StopTxnSampledDataString = declareConfiguration("StopTxnSampledData", "", CONFIGURATION_FN); StopTxnSampledDataString->setString("Energy.Active.Import.Register"); + configuration_save(); + loop(); model.getClock().setTime(BASE_TIME); @@ -240,6 +231,9 @@ TEST_CASE("Metering") { setOnReceiveRequest("StopTransaction", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; + + REQUIRE(payload["transactionData"].size() >= 2); + Timestamp t0, t1; t0.setTime(payload["transactionData"][0]["timestamp"] | ""); t1.setTime(payload["transactionData"][1]["timestamp"] | ""); @@ -289,8 +283,7 @@ TEST_CASE("Metering") { setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; - REQUIRE((!strcmp(payload["meterValue"][0]["sampledValue"][0]["value"] | "", "3600") || - !strcmp(payload["meterValue"][0]["sampledValue"][0]["value"] | "", "3600.0"))); + REQUIRE( !strncmp(payload["meterValue"][0]["sampledValue"][0]["value"] | "", "3600", strlen("3600")) ); }); loop(); @@ -322,22 +315,16 @@ TEST_CASE("Metering") { auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(10); - auto MeterValueCacheSizeInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "MeterValueCacheSize", 0, CONFIGURATION_FN); - MeterValueCacheSizeInt->setInt(10); - bool checkProcessed = false; setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; - Timestamp t0, t1; + Timestamp t0; t0.setTime(payload["meterValue"][0]["timestamp"] | ""); - t1.setTime(payload["meterValue"][1]["timestamp"] | ""); REQUIRE((t0 - base >= 10 && t0 - base <= 11)); - REQUIRE((t1 - base >= 20 && t1 - base <= 21)); - REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["measurand"] | "", "Energy.Active.Import.Register")); - REQUIRE(!strcmp(payload["meterValue"][1]["sampledValue"][0]["measurand"] | "", "Power.Active.Import")); + REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["measurand"] | "", "Power.Active.Import")); }); loop(); @@ -348,23 +335,396 @@ TEST_CASE("Metering") { beginTransaction_authorized("mIdTag"); + MeterValuesSampledDataString->setString("Power.Active.Import"); + loop(); mtime = trackMtime + 10 * 1000; loop(); - MeterValuesSampledDataString->setString("Power.Active.Import"); + endTransaction(); - mtime = trackMtime + 20 * 1000; + loop(); + + REQUIRE(checkProcessed); + } + + SECTION("Preserve order of tx-related msgs") { + + loopback.setConnected(false); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true)->setBool(true); + + Timestamp base; + base.setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + configuration_save(); + + unsigned int countProcessed = 0; + + setOnReceiveRequest("StartTransaction", [&countProcessed] (JsonObject) { + REQUIRE(countProcessed == 0); + countProcessed++; + }); + + int assignedTxId = -1; + + setOnSendConf("StartTransaction", [&assignedTxId] (JsonObject conf) { + assignedTxId = conf["transactionId"]; + }); + + setOnReceiveRequest("MeterValues", [&countProcessed, &assignedTxId] (JsonObject req) { + REQUIRE(countProcessed == 1); + countProcessed++; + + int transactionId = req["transactionId"] | -1000; + + REQUIRE(assignedTxId == transactionId); + }); + + setOnReceiveRequest("StopTransaction", [&countProcessed] (JsonObject) { + REQUIRE(countProcessed == 2); + countProcessed++; + }); + + loop(); + + auto trackMtime = mtime; + + beginTransaction_authorized("mIdTag"); + + loop(); + + mtime = trackMtime + 10 * 1000; + + loop(); + + endTransaction(); + + loop(); + + loopback.setConnected(true); + + loop(); + + REQUIRE(countProcessed == 3); + + /* + * Combine test case with power loss. Start tx before power loss, then enqueue 1 MV, then StopTx + */ + + countProcessed = 0; + + beginTransaction("mIdTag"); + + loop(); + + mocpp_deinitialize(); + + loopback.setConnected(false); + + mocpp_initialize(loopback, ChargerCredentials()); + getOcppContext()->getModel().getClock().setTime(BASE_TIME); + + base.setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + setOnReceiveRequest("MeterValues", [&countProcessed, &assignedTxId] (JsonObject req) { + REQUIRE(countProcessed == 1); + countProcessed++; + + int transactionId = req["transactionId"] | -1000; + + REQUIRE(assignedTxId == transactionId); + }); + + setOnReceiveRequest("StopTransaction", [&countProcessed] (JsonObject) { + REQUIRE(countProcessed == 2); + countProcessed++; + }); + + trackMtime = mtime; + + loop(); + + mtime = trackMtime + 10 * 1000; + + loop(); + + endTransaction(); + + loop(); + + loopback.setConnected(true); + + loop(); + + REQUIRE(countProcessed == 3); + } + + SECTION("Queue multiple MeterValues") { + + Timestamp base; + base.setTime(BASE_TIME); + model.getClock().setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + unsigned int nrInitiated = 0; + unsigned int countProcessed = 0; + + setOnReceiveRequest("MeterValues", [&base, &nrInitiated, &countProcessed] (JsonObject payload) { + countProcessed++; + + Timestamp t0; + t0.setTime(payload["meterValue"][0]["timestamp"] | ""); + + REQUIRE((t0 - base >= 10 * ((int)nrInitiated - (MO_METERVALUES_CACHE_MAXSIZE - (int)countProcessed)) && t0 - base <= 1 + 10 * ((int)nrInitiated - (MO_METERVALUES_CACHE_MAXSIZE - (int)countProcessed)))); + }); + + + loop(); + + beginTransaction_authorized("mIdTag"); + + base = model.getClock().now(); + auto trackMtime = mtime; + + loop(); + + loopback.setConnected(false); + + //initiate 10 more MeterValues than can be cached + for (unsigned long i = 1; i <= 10 + MO_METERVALUES_CACHE_MAXSIZE; i++) { + mtime = trackMtime + i * 10 * 1000; + loop(); + + nrInitiated++; + } + + loopback.setConnected(true); + + loop(); + + REQUIRE(countProcessed == MO_METERVALUES_CACHE_MAXSIZE); + + endTransaction(); + + loop(); + + } + + SECTION("Drop MeterValues for silent tx") { + + loopback.setConnected(false); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true)->setBool(true); + + Timestamp base; + base.setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + configuration_save(); + + unsigned int countProcessed = 0; + + setOnReceiveRequest("StartTransaction", [&countProcessed] (JsonObject) { + countProcessed++; + }); + + int assignedTxId = -1; + + setOnSendConf("StartTransaction", [&assignedTxId] (JsonObject conf) { + assignedTxId = conf["transactionId"]; + }); + + setOnReceiveRequest("MeterValues", [&countProcessed, &assignedTxId] (JsonObject req) { + countProcessed++; + }); + + setOnReceiveRequest("StopTransaction", [&countProcessed] (JsonObject) { + REQUIRE(countProcessed == 2); + }); + + loop(); + + auto trackMtime = mtime; + + beginTransaction_authorized("mIdTag"); + auto tx = getTransaction(); + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + + mtime = trackMtime + 10 * 1000; + + loop(); + + endTransaction(); + + loop(); + + tx->setSilent(); + tx->commit(); + + loopback.setConnected(true); + + loop(); + + REQUIRE(countProcessed == 0); + } + + SECTION("TxMsg retry behavior") { + + Timestamp base; + + addMeterValueInput([&base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + configuration_save(); + + const size_t NUM_ATTEMPTS = 3; + const int RETRY_INTERVAL_SECS = 3600; + + declareConfiguration("TransactionMessageAttempts", 0)->setInt(NUM_ATTEMPTS); + declareConfiguration("TransactionMessageRetryInterval", 0)->setInt(RETRY_INTERVAL_SECS); + + unsigned int attemptNr = 0; + + getOcppContext()->getOperationRegistry().registerOperation("MeterValues", [&attemptNr] () { + return new Ocpp16::CustomOperation("MeterValues", + [&attemptNr] (JsonObject payload) { + //receive req + attemptNr++; + }, + [] () { + //create conf + return createEmptyDocument(); + }, + [] () { + //ErrorCode for CALLERROR + return "InternalError"; + });}); loop(); + auto trackMtime = mtime; + base = model.getClock().now(); + + beginTransaction("mIdTag"); + + loop(); + + mtime = trackMtime + 10 * 1000; + + loop(); + + REQUIRE(attemptNr == 1); + endTransaction(); + mtime = trackMtime + 20 * 1000; + loop(); + REQUIRE(attemptNr == 1); + + mtime = trackMtime + 10 * 1000 + RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE(attemptNr == 2); + + mtime = trackMtime + 10 * 1000 + 2 * RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE(attemptNr == 2); + + mtime = trackMtime + 10 * 1000 + 3 * RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE(attemptNr == 3); + + mtime = trackMtime + 10 * 1000 + 7 * RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE(attemptNr == 3); + } + + SECTION("TriggerMessage") { + + addMeterValueInput([] () { + return 12345; + }, "Energy.Active.Import.Register"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + Timestamp base; + + bool checkProcessed = false; + + setOnReceiveRequest("MeterValues", [&base, &checkProcessed] (JsonObject payload) { + int connectorId = payload["connectorId"] | -1; + if (connectorId != 1) { + return; + } + + checkProcessed = true; + + Timestamp t0; + t0.setTime(payload["meterValue"][0]["timestamp"] | ""); + + REQUIRE( std::abs(t0 - base) <= 1 ); + REQUIRE( !strncmp(payload["meterValue"][0]["sampledValue"][0]["value"] | "", "12345", strlen("12345")) ); + }); + + loop(); + + base = model.getClock().now(); + + loopback.sendTXT(TRIGGER_METERVALUES, sizeof(TRIGGER_METERVALUES) - 1); loop(); REQUIRE(checkProcessed); + } mocpp_deinitialize(); diff --git a/tests/RemoteStartTransaction.cpp b/tests/RemoteStartTransaction.cpp new file mode 100644 index 00000000..5d21ec08 --- /dev/null +++ b/tests/RemoteStartTransaction.cpp @@ -0,0 +1,168 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "./helpers/testHelper.h" + +using namespace MicroOcpp; + +TEST_CASE("RemoteStartTransaction") { + printf("\nRun %s\n", "RemoteStartTransaction"); + + LoopbackConnection loopback; + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + mocpp_set_timer(custom_timer_cb); + loop(); + + auto context = getOcppContext(); + auto connector = context->getModel().getConnector(1); + + SECTION("Basic remote start accepted") { + // Ensure connector idle + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + + context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + auto doc = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["idTag"] = "mIdTag"; + return doc;}, + [] (JsonObject) {} + ))); + + loop(); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + endTransaction(); + loop(); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + + SECTION("Same connectorId rejected when transaction active") { + // Start with connector 1 busy so remote start with connectorId=1 should not auto-assign + beginTransaction("anotherId"); + loop(); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + + bool checkProcessed = false; + + context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + auto doc = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(3)); + auto payload = doc->to(); + payload["idTag"] = "mIdTag"; + payload["connectorId"] = 1; // the same connector already in use + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); + } + ))); + + loop(); + + // Transaction should still be the original one only + REQUIRE(checkProcessed); + REQUIRE(connector->getTransaction()); + REQUIRE(strcmp(connector->getTransaction()->getIdTag(), "anotherId") == 0); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + + endTransaction(); + loop(); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + + SECTION("ConnectorId 0 rejected per spec") { + // RemoteStartTransaction response status is Rejected when connectorId == 0 + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + + bool checkProcessed = false; + + context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + auto doc = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(3)); + auto payload = doc->to(); + payload["idTag"] = "mIdTag"; + payload["connectorId"] = 0; // invalid per spec + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); + } + ))); + + loop(); + + REQUIRE(checkProcessed); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + + SECTION("No free connector so rejected") { + // Occupy all connectors (limit defined by MO_NUMCONNECTORS) + for (unsigned cId = 1; cId < context->getModel().getNumConnectors(); cId++) { + auto c = context->getModel().getConnector(cId); + if (c) { + c->beginTransaction_authorized("busyId"); + } + } + loop(); + + bool checkProcessed = false; + auto freeFound = false; + for (unsigned cId = 1; cId < context->getModel().getNumConnectors(); cId++) { + auto c = context->getModel().getConnector(cId); + if (c && !c->getTransaction()) freeFound = true; + } + REQUIRE(!freeFound); // ensure all are busy + + context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + auto doc = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["idTag"] = "mIdTag"; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); + } + ))); + + loop(); + REQUIRE(checkProcessed); + + // No new transaction should be created; keep statuses + int activeTx = 0; + for (unsigned cId = 1; cId < context->getModel().getNumConnectors(); cId++) { + auto c = context->getModel().getConnector(cId); + if (c && c->getTransaction()) activeTx++; + } + REQUIRE(activeTx == (int)context->getModel().getNumConnectors() - 1); // all occupied + + // cleanup + for (unsigned cId = 1; cId < context->getModel().getNumConnectors(); cId++) { + auto c = context->getModel().getConnector(cId); + if (c && c->getTransaction()) { + c->endTransaction(); + } + } + loop(); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + + mocpp_deinitialize(); +} diff --git a/tests/Reservation.cpp b/tests/Reservation.cpp index cf113f03..306b804a 100644 --- a/tests/Reservation.cpp +++ b/tests/Reservation.cpp @@ -1,11 +1,19 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_RESERVATION + #include #include -#include "./catch2/catch.hpp" +#include #include "./helpers/testHelper.h" #include #include -#include +#include #include #include @@ -38,7 +46,7 @@ TEST_CASE( "Reservation" ) { loop(); SECTION("Basic reservation") { - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); REQUIRE( rService ); //set reservation @@ -50,81 +58,81 @@ TEST_CASE( "Reservation" ) { rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); - REQUIRE( connector->getStatus() == ChargePointStatus::Reserved ); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); //transaction blocked by reservation bool checkTxRejected = false; setTxNotificationOutput([&checkTxRejected] (Transaction*, TxNotification txNotification) { - if (txNotification == TxNotification::ReservationConflict) { + if (txNotification == TxNotification_ReservationConflict) { checkTxRejected = true; } }); beginTransaction("wrong idTag"); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Reserved ); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); REQUIRE( checkTxRejected ); //idTag matches reservation beginTransaction("mIdTag"); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Charging ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( connector->getTransaction()->getReservationId() == reservationId ); //reservation is reset after tx endTransaction(); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //RemoteStartTx - idTag doesn't match. The tx will start anyway assuming some start trigger in the backend prevails over reservations in the backend implementation rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); - REQUIRE( connector->getStatus() == ChargePointStatus::Reserved ); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "RemoteStartTransaction", [] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTag"] = "wrong idTag"; return doc;}, [] (JsonObject) { } //ignore conf ))); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Charging ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( connector->getTransaction()->getReservationId() != reservationId ); //reservation is reset after tx endTransaction(); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //RemoteStartTx - idTag does match rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); - REQUIRE( connector->getStatus() == ChargePointStatus::Reserved ); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "RemoteStartTransaction", [idTag] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["idTag"] = idTag; return doc;}, [] (JsonObject) { } //ignore conf ))); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Charging ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( connector->getTransaction()->getReservationId() == reservationId ); //reservation is reset after tx endTransaction(); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); } SECTION("Tx on other connector") { - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //set reservation int reservationId = 123; @@ -135,21 +143,21 @@ TEST_CASE( "Reservation" ) { const char *parentIdTag = nullptr; rService->updateReservation(reservationId, connectorIdResvd, expiryDate, idTag, parentIdTag); - REQUIRE( model.getConnector(connectorIdResvd)->getStatus() == ChargePointStatus::Reserved ); + REQUIRE( model.getConnector(connectorIdResvd)->getStatus() == ChargePointStatus_Reserved ); beginTransaction(idTag, connectorIdOther); loop(); - REQUIRE( model.getConnector(connectorIdResvd)->getStatus() == ChargePointStatus::Available ); //reservation on first connector withdrawed - REQUIRE( model.getConnector(connectorIdOther)->getStatus() == ChargePointStatus::Charging ); + REQUIRE( model.getConnector(connectorIdResvd)->getStatus() == ChargePointStatus_Available ); //reservation on first connector withdrawed + REQUIRE( model.getConnector(connectorIdOther)->getStatus() == ChargePointStatus_Charging ); REQUIRE( getTransaction(connectorIdOther)->getReservationId() == reservationId ); //reservation transferred to other connector endTransaction(nullptr, nullptr, connectorIdOther); loop(); - REQUIRE( model.getConnector(connectorIdOther)->getStatus() == ChargePointStatus::Available ); + REQUIRE( model.getConnector(connectorIdOther)->getStatus() == ChargePointStatus_Available ); } SECTION("parentIdTag") { - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //set reservation int reservationId = 123; @@ -159,7 +167,7 @@ TEST_CASE( "Reservation" ) { const char *parentIdTag = "mParentIdTag"; rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); - REQUIRE( connector->getStatus() == ChargePointStatus::Reserved ); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); bool checkProcessed = false; getOcppContext()->getOperationRegistry().registerOperation("Authorize", @@ -169,9 +177,9 @@ TEST_CASE( "Reservation" ) { [parentIdTag, &checkProcessed] () { //create conf checkProcessed = true; - auto doc = std::unique_ptr(new DynamicJsonDocument( + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + //payload root - JSON_OBJECT_SIZE(3))); //idTagInfo + JSON_OBJECT_SIZE(3)); //idTagInfo auto payload = doc->to(); payload["idTagInfo"]["parentIdTag"] = parentIdTag; payload["idTagInfo"]["status"] = "Accepted"; @@ -180,17 +188,17 @@ TEST_CASE( "Reservation" ) { beginTransaction("other idTag"); loop(); REQUIRE( checkProcessed ); - REQUIRE( connector->getStatus() == ChargePointStatus::Charging ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); REQUIRE( connector->getTransaction()->getReservationId() == reservationId ); //reset tx endTransaction(); loop(); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); } SECTION("ConnectorZero") { - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //set reservation Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future @@ -201,23 +209,23 @@ TEST_CASE( "Reservation" ) { REQUIRE( rService->updateReservation(1000, 0, expiryDate, idTag, parentIdTag) ); REQUIRE( rService->updateReservation(1001, 1, expiryDate, idTag, parentIdTag) ); REQUIRE( !rService->updateReservation(1002, 2, expiryDate, idTag, parentIdTag) ); - REQUIRE( model.getConnector(2)->getStatus() == ChargePointStatus::Available ); + REQUIRE( model.getConnector(2)->getStatus() == ChargePointStatus_Available ); //reset reservations rService->getReservationById(1000)->clear(); rService->getReservationById(1001)->clear(); - REQUIRE( model.getConnector(1)->getStatus() == ChargePointStatus::Available ); + REQUIRE( model.getConnector(1)->getStatus() == ChargePointStatus_Available ); //if connector 0 is reserved, ensure that at least one physical connector remains available for the idTag of the reservation REQUIRE( rService->updateReservation(1000, 0, expiryDate, idTag, parentIdTag) ); beginTransaction("other idTag", 1); loop(); - REQUIRE( model.getConnector(1)->getStatus() == ChargePointStatus::Charging ); + REQUIRE( model.getConnector(1)->getStatus() == ChargePointStatus_Charging ); bool checkTxRejected = false; setTxNotificationOutput([&checkTxRejected] (Transaction*, TxNotification txNotification) { - if (txNotification == TxNotification::ReservationConflict) { + if (txNotification == TxNotification_ReservationConflict) { checkTxRejected = true; } }, 2); @@ -225,7 +233,7 @@ TEST_CASE( "Reservation" ) { beginTransaction("other idTag 2", 2); loop(); REQUIRE( checkTxRejected ); - REQUIRE( model.getConnector(2)->getStatus() == ChargePointStatus::Available ); + REQUIRE( model.getConnector(2)->getStatus() == ChargePointStatus_Available ); endTransaction(nullptr, nullptr, 1); @@ -233,7 +241,7 @@ TEST_CASE( "Reservation" ) { } SECTION("Expiry date") { - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //set reservation int reservationId = 123; @@ -244,19 +252,19 @@ TEST_CASE( "Reservation" ) { rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); - REQUIRE( connector->getStatus() == ChargePointStatus::Reserved ); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); Timestamp expired = expiryDate + 1; char expired_cstr [JSONDATE_LENGTH + 1]; expired.toJsonString(expired_cstr, JSONDATE_LENGTH + 1); model.getClock().setTime(expired_cstr); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); } SECTION("Reservation persistency") { unsigned int connectorId = 1; - REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus::Available ); + REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus_Available ); //set reservation int reservationId = 123; @@ -266,7 +274,7 @@ TEST_CASE( "Reservation" ) { getOcppContext()->getModel().getReservationService()->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); - REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus::Reserved ); + REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus_Reserved ); mocpp_deinitialize(); @@ -274,11 +282,11 @@ TEST_CASE( "Reservation" ) { getOcppContext()->getModel().getClock().setTime(BASE_TIME); loop(); - REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus::Reserved ); + REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus_Reserved ); auto reservation = getOcppContext()->getModel().getReservationService()->getReservationById(reservationId); REQUIRE( reservation->getReservationId() == reservationId ); - REQUIRE( reservation->getConnectorId() == connectorId ); + REQUIRE( reservation->getConnectorId() == (int)connectorId ); REQUIRE( reservation->getExpiryDate() == expiryDate ); REQUIRE( !strcmp(reservation->getIdTag(), idTag) ); REQUIRE( !strcmp(reservation->getParentIdTag(), parentIdTag) ); @@ -291,12 +299,12 @@ TEST_CASE( "Reservation" ) { getOcppContext()->getModel().getClock().setTime(BASE_TIME); loop(); - REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus::Available ); + REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus_Available ); } SECTION("ReserveNow") { - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //set reservation int reservationId = 123; @@ -311,9 +319,9 @@ TEST_CASE( "Reservation" ) { "ReserveNow", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument( + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(5) + - JSONDATE_LENGTH + 1)); + JSONDATE_LENGTH + 1); auto payload = doc->to(); payload["connectorId"] = connectorId; char expiryDate_cstr [JSONDATE_LENGTH + 1]; @@ -332,24 +340,24 @@ TEST_CASE( "Reservation" ) { ))); loop(); REQUIRE( checkProcessed ); - REQUIRE( connector->getStatus() == ChargePointStatus::Reserved ); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); model.getReservationService()->getReservationById(reservationId)->clear(); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //reserve while charger is in Faulted state const char *errorCode = "OtherError"; addErrorCodeInput([&errorCode] () {return errorCode;}); - REQUIRE( connector->getStatus() == ChargePointStatus::Faulted ); + REQUIRE( connector->getStatus() == ChargePointStatus_Faulted ); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ReserveNow", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument( + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(5) + - JSONDATE_LENGTH + 1)); + JSONDATE_LENGTH + 1); auto payload = doc->to(); payload["connectorId"] = connectorId; char expiryDate_cstr [JSONDATE_LENGTH + 1]; @@ -368,23 +376,23 @@ TEST_CASE( "Reservation" ) { ))); loop(); REQUIRE( checkProcessed ); - REQUIRE( connector->getStatus() == ChargePointStatus::Faulted ); + REQUIRE( connector->getStatus() == ChargePointStatus_Faulted ); errorCode = nullptr; //reset error - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //reserve while connector is already occupied setConnectorPluggedInput([] {return true;}); //plug EV - REQUIRE( connector->getStatus() == ChargePointStatus::Preparing ); + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ReserveNow", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument( + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(5) + - JSONDATE_LENGTH + 1)); + JSONDATE_LENGTH + 1); auto payload = doc->to(); payload["connectorId"] = connectorId; char expiryDate_cstr [JSONDATE_LENGTH + 1]; @@ -403,25 +411,25 @@ TEST_CASE( "Reservation" ) { ))); loop(); REQUIRE( checkProcessed ); - REQUIRE( connector->getStatus() == ChargePointStatus::Preparing ); + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); setConnectorPluggedInput(nullptr); //reset ConnectorPluggedInput - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //Rejected ReserveNow status not possible //reserve while connector is inoperative connector->setAvailabilityVolatile(false); - REQUIRE( connector->getStatus() == ChargePointStatus::Unavailable ); + REQUIRE( connector->getStatus() == ChargePointStatus_Unavailable ); checkProcessed = false; getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( "ReserveNow", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument( + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(5) + - JSONDATE_LENGTH + 1)); + JSONDATE_LENGTH + 1); auto payload = doc->to(); payload["connectorId"] = connectorId; char expiryDate_cstr [JSONDATE_LENGTH + 1]; @@ -440,14 +448,14 @@ TEST_CASE( "Reservation" ) { ))); loop(); REQUIRE( checkProcessed ); - REQUIRE( connector->getStatus() == ChargePointStatus::Unavailable ); + REQUIRE( connector->getStatus() == ChargePointStatus_Unavailable ); connector->setAvailabilityVolatile(true); //revert Unavailable status - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); } SECTION("CancelReservation") { - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //set reservation int reservationId = 123; @@ -457,7 +465,7 @@ TEST_CASE( "Reservation" ) { const char *parentIdTag = nullptr; rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); - REQUIRE( connector->getStatus() == ChargePointStatus::Reserved ); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); //CancelReservation successfully bool checkProcessed = false; @@ -465,7 +473,7 @@ TEST_CASE( "Reservation" ) { "CancelReservation", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["reservationId"] = reservationId; return doc;}, @@ -478,7 +486,7 @@ TEST_CASE( "Reservation" ) { ))); loop(); REQUIRE( checkProcessed ); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); //CancelReservation while no reservation exists checkProcessed = false; @@ -486,7 +494,7 @@ TEST_CASE( "Reservation" ) { "CancelReservation", [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); auto payload = doc->to(); payload["reservationId"] = reservationId; return doc;}, @@ -499,8 +507,10 @@ TEST_CASE( "Reservation" ) { ))); loop(); REQUIRE( checkProcessed ); - REQUIRE( connector->getStatus() == ChargePointStatus::Available ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); } mocpp_deinitialize(); } + +#endif //MO_ENABLE_RESERVATION diff --git a/tests/Reset.cpp b/tests/Reset.cpp new file mode 100644 index 00000000..1390b336 --- /dev/null +++ b/tests/Reset.cpp @@ -0,0 +1,377 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "./helpers/testHelper.h" + +#define BASE_TIME "2023-01-01T00:00:00.000Z" + +using namespace MicroOcpp; + + +TEST_CASE( "Reset" ) { + printf("\nRun %s\n", "Reset"); + + //initialize Context with dummy socket + LoopbackConnection loopback; + mocpp_initialize(loopback, + ChargerCredentials("test-runner1234"), + makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail), + false, + ProtocolVersion(2,0,1)); + + auto context = getOcppContext(); + + mocpp_set_timer(custom_timer_cb); + + getOcppContext()->getOperationRegistry().registerOperation("Authorize", [] () { + return new Ocpp16::CustomOperation("Authorize", + [] (JsonObject) {}, //ignore req + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTokenInfo"]["status"] = "Accepted"; + return doc; + });}); + + getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [] () { + return new Ocpp16::CustomOperation("TransactionEvent", + [] (JsonObject) {}, //ignore req + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTokenInfo"]["status"] = "Accepted"; + return doc; + });}); + + // Register Reset handlers + bool checkNotified [MO_NUM_EVSEID] = {false}; + bool checkExecuted [MO_NUM_EVSEID] = {false}; + + setOnResetNotify([&checkNotified] (bool) { + MO_DBG_DEBUG("Notify"); + checkNotified[0] = true; + return true; + }); + context->getModel().getResetServiceV201()->setExecuteReset([&checkExecuted] () { + MO_DBG_DEBUG("Execute"); + checkExecuted[0] = true; + return false; // Reset fails because we're not actually exiting the process + }); + + for (size_t i = 1; i < MO_NUM_EVSEID; i++) { + context->getModel().getResetServiceV201()->setNotifyReset([&checkNotified, i] (ResetType) { + MO_DBG_DEBUG("Notify %zu", i); + checkNotified[i] = true; + return true; + }, i); + context->getModel().getResetServiceV201()->setExecuteReset([&checkExecuted, i] () { + MO_DBG_DEBUG("Execute %zu", i); + checkExecuted[i] = true; + return true; + }, i); + } + + loop(); + + SECTION("B11 - Reset - Without ongoing transaction") { + + MO_MEM_RESET(); + + bool checkProcessed = false; + + auto resetRequest = makeRequest(new Ocpp16::CustomOperation( + "Reset", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["type"] = "OnIdle"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE(!strcmp(payload["status"], "Accepted")); + } + )); + + context->initiateRequest(std::move(resetRequest)); + + loop(); + mtime += 30000; // Reset has some delays to ensure that the WS is not cut off immediately + loop(); + + REQUIRE(checkProcessed); + + for (size_t i = 0; i < MO_NUM_EVSEID; i++) { + REQUIRE( checkNotified[i] ); + } + + MO_MEM_PRINT_STATS(); + } + + SECTION("Schedule full charger Reset") { + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + REQUIRE( context->getModel().getTransactionService()->getEvse(2)->getTransaction() == nullptr ); + + context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken"); + setConnectorPluggedInput([] () {return true;}, 1); + setEvReadyInput([] () {return true;}, 1); + setEvseReadyInput([] () {return true;}, 1); + + context->getModel().getTransactionService()->getEvse(2)->beginAuthorization("mIdToken2"); + setConnectorPluggedInput([] () {return true;}, 2); + setEvReadyInput([] () {return true;}, 2); + setEvseReadyInput([] () {return true;}, 2); + + loop(); + + REQUIRE( ocppPermitsCharge(1) ); + REQUIRE( ocppPermitsCharge(2) ); + + bool checkProcessed = false; + + auto resetRequest = makeRequest(new Ocpp16::CustomOperation( + "Reset", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["type"] = "OnIdle"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE(!strcmp(payload["status"], "Scheduled")); + } + )); + + context->initiateRequest(std::move(resetRequest)); + + loop(); + mtime += 30000; // Reset has some delays to ensure that the WS is not cut off immediately + loop(); + + REQUIRE(checkProcessed); + + for (size_t i = 0; i < MO_NUM_EVSEID; i++) { + REQUIRE( checkNotified[i] ); + } + + // Still scheduled + REQUIRE( ocppPermitsCharge(1) ); + REQUIRE( ocppPermitsCharge(2) ); + + context->getModel().getTransactionService()->getEvse(1)->endAuthorization("mIdToken"); + setConnectorPluggedInput([] () {return false;}, 1); + setEvReadyInput([] () {return false;}, 1); + setEvseReadyInput([] () {return false;}, 1); + loop(); + + // Still scheduled + REQUIRE( !ocppPermitsCharge(1) ); + REQUIRE( ocppPermitsCharge(2) ); + + //REQUIRE( getChargePointStatus(1) == ChargePointStatus_Unavailable ); //change: Reset doesn't lead to Unavailable state + + context->getModel().getTransactionService()->getEvse(2)->endAuthorization("mIdToken"); + setConnectorPluggedInput([] () {return false;}, 2); + setEvReadyInput([] () {return false;}, 2); + setEvseReadyInput([] () {return false;}, 2); + + loop(); + mtime += 30000; // Reset has some delays to ensure that the WS is not cut off immediately + loop(); + + // Not scheduled anymore; execute Reset + REQUIRE( !ocppPermitsCharge(1) ); + REQUIRE( !ocppPermitsCharge(2) ); + + REQUIRE( checkExecuted[0] ); + + // Technically, Reset failed at this point, because the program is still running. Check if connectors are Available agin + REQUIRE( getChargePointStatus(1) == ChargePointStatus_Available ); + REQUIRE( getChargePointStatus(2) == ChargePointStatus_Available ); + } + + SECTION("Immediate full charger Reset") { + + context->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + REQUIRE( context->getModel().getTransactionService()->getEvse(2)->getTransaction() == nullptr ); + + context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken"); + + context->getModel().getTransactionService()->getEvse(2)->beginAuthorization("mIdToken2"); + + loop(); + + MO_MEM_RESET(); + + REQUIRE( ocppPermitsCharge(1) ); + REQUIRE( ocppPermitsCharge(2) ); + + bool checkProcessedTx = false; + + getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkProcessedTx] () { + return new Ocpp16::CustomOperation("TransactionEvent", + [&checkProcessedTx] (JsonObject payload) { + //process req + checkProcessedTx = true; + + REQUIRE(!strcmp(payload["eventType"], "Ended")); + REQUIRE(!strcmp(payload["triggerReason"], "ResetCommand")); + REQUIRE(!strcmp(payload["transactionInfo"]["stoppedReason"], "ImmediateReset")); + }, + [] () { + //create conf + return createEmptyDocument(); + });}); + + bool checkProcessed = false; + + auto resetRequest = makeRequest(new Ocpp16::CustomOperation( + "Reset", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["type"] = "Immediate"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE(!strcmp(payload["status"], "Accepted")); + } + )); + + context->initiateRequest(std::move(resetRequest)); + + loop(); + mtime += 30000; // Reset has some delays to ensure that the WS is not cut off immediately + loop(); + + REQUIRE(checkProcessed); + REQUIRE(checkProcessedTx); + + for (size_t i = 0; i < MO_NUM_EVSEID; i++) { + REQUIRE( checkNotified[i] ); + } + + // Stopped Tx + REQUIRE( !ocppPermitsCharge(1) ); + REQUIRE( !ocppPermitsCharge(2) ); + + REQUIRE( checkExecuted[0] ); + + MO_MEM_PRINT_STATS(); + + loop(); + + // Technically, Reset failed at this point, because the program is still running. Check if connectors are Available agin + REQUIRE( getChargePointStatus(1) == ChargePointStatus_Available ); + REQUIRE( getChargePointStatus(2) == ChargePointStatus_Available ); + } + + SECTION("Reject Reset") { + + context->getModel().getResetServiceV201()->setNotifyReset([&checkNotified] (ResetType) { + MO_DBG_DEBUG("Reject Reset"); + checkNotified[2] = true; + return false; + }, 2); + + bool checkProcessed = false; + + auto resetRequest = makeRequest(new Ocpp16::CustomOperation( + "Reset", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["type"] = "Immediate"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE(!strcmp(payload["status"], "Rejected")); + } + )); + + context->initiateRequest(std::move(resetRequest)); + + loop(); + + REQUIRE(checkProcessed); + REQUIRE(checkNotified[2]); + + REQUIRE( getChargePointStatus(0) == ChargePointStatus_Available ); + REQUIRE( getChargePointStatus(1) == ChargePointStatus_Available ); + REQUIRE( getChargePointStatus(2) == ChargePointStatus_Available ); + } + + SECTION("Reset single EVSE") { + + bool checkProcessed = false; + + auto resetRequest = makeRequest(new Ocpp16::CustomOperation( + "Reset", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["type"] = "OnIdle"; + payload["evseId"] = 1; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE(!strcmp(payload["status"], "Accepted")); + } + )); + + context->initiateRequest(std::move(resetRequest)); + + loop(); + + REQUIRE(checkProcessed); + REQUIRE(checkNotified[1]); + + //REQUIRE( getChargePointStatus(1) == ChargePointStatus_Unavailable ); //change: Reset doesn't lead to Unavailable state + REQUIRE( getChargePointStatus(2) == ChargePointStatus_Available ); + + mtime += 30000; // Reset has some delays to ensure that the WS is not cut off immediately + loop(); + + REQUIRE(checkExecuted[1]); + REQUIRE( getChargePointStatus(1) == ChargePointStatus_Available ); + } + + mocpp_deinitialize(); +} + +#endif // MO_ENABLE_V201 diff --git a/tests/Security.cpp b/tests/Security.cpp new file mode 100644 index 00000000..43bf2d7a --- /dev/null +++ b/tests/Security.cpp @@ -0,0 +1,58 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "./helpers/testHelper.h" + +#define BASE_TIME "2023-01-01T00:00:00.000Z" + +using namespace MicroOcpp; + + +TEST_CASE( "Security" ) { + printf("\nRun %s\n", "Security"); + + mocpp_set_timer(custom_timer_cb); + + //initialize Context with dummy socket + LoopbackConnection loopback; + auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); + mocpp_initialize(loopback, + ChargerCredentials(), + filesystem, + false, + ProtocolVersion(2,0,1)); + + SECTION("Manual SecurityEventNotification") { + + loop(); + + MO_MEM_RESET(); + + getOcppContext()->initiateRequest(makeRequest(new Ocpp201::SecurityEventNotification( + "ReconfigurationOfSecurityParameters", + getOcppContext()->getModel().getClock().now()))); + + loop(); + + MO_MEM_PRINT_STATS(); + } + + mocpp_deinitialize(); +} + +#endif // MO_ENABLE_V201 diff --git a/tests/SmartCharging.cpp b/tests/SmartCharging.cpp index 81d73791..7cf53b32 100644 --- a/tests/SmartCharging.cpp +++ b/tests/SmartCharging.cpp @@ -1,12 +1,16 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + #include #include #include #include #include -#include +#include #include #include -#include "./catch2/catch.hpp" +#include #include "./helpers/testHelper.h" #define BASE_TIME "2023-01-01T00:00:00.000Z" @@ -472,7 +476,7 @@ TEST_CASE( "SmartCharging" ) { "GetCompositeSchedule", [] () { //create req - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(3))); + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(3)); auto payload = doc->to(); payload["connectorId"] = 1; payload["duration"] = 86400; @@ -539,6 +543,299 @@ TEST_CASE( "SmartCharging" ) { REQUIRE(schedule->chargingSchedulePeriod[3].startPeriod == 86400); } + SECTION("SmartCharging memory limits - MaxChargingProfilesInstalled") { + + loop(); + + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_0_ALT_SAME_ID); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + (*doc)["connectorId"] = 2; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_1_ABSOLUTE_LIMIT_16A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + // 3 distinct ChargingProfiles installed. Check if further Profiles are rejected correctly + + for (size_t i = 0; i < 2; i++) { + // replace existing profile - OK + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_1_ABSOLUTE_LIMIT_16A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + } + + for (size_t i = 0; i < 2; i++) { + // try to install additional profile - not okay + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_5_VALID_UNTIL_2022_16A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + } + } + + SECTION("SmartCharging memory limits - ChargeProfileMaxStackLevel") { + + loop(); + + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + (*doc)["csChargingProfiles"]["stackLevel"] = MO_ChargeProfileMaxStackLevel; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + (*doc)["csChargingProfiles"]["stackLevel"] = MO_ChargeProfileMaxStackLevel + 1; + return doc;}, + [] (JsonObject) { }, //ignore conf + [&checkProcessed] (const char*, const char*, JsonObject) { + // process error + checkProcessed = true; + return true; + } + ))); + loop(); + REQUIRE( checkProcessed ); + } + + SECTION("SmartCharging memory limits - ChargingScheduleMaxPeriods") { + + loop(); + + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + JsonArray chargingSchedulePeriod = (*doc)["csChargingProfiles"]["chargingSchedule"]["chargingSchedulePeriod"]; + chargingSchedulePeriod.clear(); + for (size_t i = 0; i < MO_ChargingScheduleMaxPeriods; i++) { + auto period = chargingSchedulePeriod.createNestedObject(); + period["startPeriod"] = i; + period["limit"] = 16; + } + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + JsonArray chargingSchedulePeriod = (*doc)["csChargingProfiles"]["chargingSchedule"]["chargingSchedulePeriod"]; + chargingSchedulePeriod.clear(); + for (size_t i = 0; i < MO_ChargingScheduleMaxPeriods + 1; i++) { + auto period = chargingSchedulePeriod.createNestedObject(); + period["startPeriod"] = i; + period["limit"] = 16; + } + return doc;}, + [] (JsonObject) { }, //ignore conf + [&checkProcessed] (const char*, const char*, JsonObject) { + // process error + checkProcessed = true; + return true; + } + ))); + loop(); + REQUIRE( checkProcessed ); + } + + SECTION("ChargingScheduleAllowedChargingRateUnit") { + + setSmartChargingOutput(nullptr); + loop(); + + // accept power, reject current + setSmartChargingPowerOutput([] (float) { }); + + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_0); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_1_ABSOLUTE_LIMIT_16A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + // reject power, accept current + setSmartChargingPowerOutput(nullptr); + setSmartChargingCurrentOutput([] (float) { }); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_0); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_1_ABSOLUTE_LIMIT_16A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + } + scService->clearChargingProfile([] (int, int, ChargingProfilePurposeType, int) { return true; }); diff --git a/tests/TransactionSafety.cpp b/tests/TransactionSafety.cpp index 79cd76a1..f3593e01 100644 --- a/tests/TransactionSafety.cpp +++ b/tests/TransactionSafety.cpp @@ -1,13 +1,16 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + #include #include #include #include #include -#include #include #include #include -#include "./catch2/catch.hpp" +#include #include "./helpers/testHelper.h" using namespace MicroOcpp; diff --git a/tests/Transactions.cpp b/tests/Transactions.cpp new file mode 100644 index 00000000..56d50c97 --- /dev/null +++ b/tests/Transactions.cpp @@ -0,0 +1,737 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "./helpers/testHelper.h" + +#define BASE_TIME "2023-01-01T00:00:00.000Z" + +using namespace MicroOcpp; + + +TEST_CASE( "Transactions" ) { + printf("\nRun %s\n", "Transactions"); + + //initialize Context with dummy socket + LoopbackConnection loopback; + mocpp_initialize(loopback, + ChargerCredentials("test-runner1234"), + makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail), + false, + ProtocolVersion(2,0,1)); + + auto context = getOcppContext(); + + mocpp_set_timer(custom_timer_cb); + + getOcppContext()->getOperationRegistry().registerOperation("Authorize", [] () { + return new Ocpp16::CustomOperation("Authorize", + [] (JsonObject) {}, //ignore req + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTokenInfo"]["status"] = "Accepted"; + return doc; + });}); + + getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [] () { + return new Ocpp16::CustomOperation("TransactionEvent", + [] (JsonObject) {}, //ignore req + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTokenInfo"]["status"] = "Accepted"; + return doc; + });}); + + loop(); + + SECTION("Basic transaction") { + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + + MO_DBG_DEBUG("plug EV"); + setConnectorPluggedInput([] () {return true;}); + + loop(); + + MO_DBG_DEBUG("authorize"); + context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken"); + + loop(); + + MO_DBG_DEBUG("EV requests charge"); + setEvReadyInput([] () {return true;}); + + loop(); + + MO_DBG_DEBUG("power circuit closed"); + setEvseReadyInput([] () {return true;}); + + loop(); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction()->started ); + REQUIRE( !context->getModel().getTransactionService()->getEvse(1)->getTransaction()->stopped ); + + MO_DBG_DEBUG("EV idle"); + setEvReadyInput([] () {return false;}); + + loop(); + + MO_DBG_DEBUG("power circuit opened"); + setEvseReadyInput([] () {return false;}); + + loop(); + + MO_DBG_DEBUG("deauthorize"); + context->getModel().getTransactionService()->getEvse(1)->endAuthorization("mIdToken"); + + loop(); + + MO_DBG_DEBUG("unplug EV"); + setConnectorPluggedInput([] () {return false;}); + + loop(); + + REQUIRE( (context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr || + context->getModel().getTransactionService()->getEvse(1)->getTransaction()->stopped)); + } + + SECTION("UC C01-04") { + + //scenario preparation + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("PowerPathClosed"); + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("PowerPathClosed"); + + setConnectorPluggedInput([] () {return false;}); + + loop(); + + MO_MEM_RESET(); + + context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken"); + loop(); + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() != nullptr ); + REQUIRE( !context->getModel().getTransactionService()->getEvse(1)->getTransaction()->started ); + REQUIRE( !context->getModel().getTransactionService()->getEvse(1)->getTransaction()->stopped ); + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + + MO_DBG_INFO("Memory requirements UC C01-04:"); + + MO_MEM_PRINT_STATS(); + + context->getModel().getTransactionService()->getEvse(1)->abortTransaction(); + loop(); + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + } + + SECTION("UC E01 - S5 / E06") { + + //scenario preparation + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("PowerPathClosed"); + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("PowerPathClosed"); + + setConnectorPluggedInput([] () {return false;}); + + loop(); + + MO_MEM_RESET(); + + //run scenario + + setConnectorPluggedInput([] () {return true;}); + loop(); + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + REQUIRE( getChargePointStatus() == ChargePointStatus_Occupied ); + + context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken"); + loop(); + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() != nullptr ); + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction()->started ); + REQUIRE( !context->getModel().getTransactionService()->getEvse(1)->getTransaction()->stopped ); + REQUIRE( getChargePointStatus() == ChargePointStatus_Occupied ); + + MO_DBG_INFO("Memory requirements UC E01 - S5:"); + + MO_MEM_PRINT_STATS(); + + MO_MEM_RESET(); + + setConnectorPluggedInput([] () {return false;}); + loop(); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + + MO_DBG_INFO("Memory requirements UC E06:"); + MO_MEM_PRINT_STATS(); + + } + + SECTION("UC G01") { + + setConnectorPluggedInput([] () {return false;}); + loop(); + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + MO_MEM_RESET(); + + setConnectorPluggedInput([] () {return true;}); + loop(); + REQUIRE( getChargePointStatus() == ChargePointStatus_Occupied ); + + MO_DBG_INFO("Memory requirements UC G01:"); + MO_MEM_PRINT_STATS(); + } + + SECTION("UC J02") { + + //scenario preparation + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("PowerPathClosed"); + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("PowerPathClosed"); + + getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "TxStartedMeasurands", "")->setString("Energy.Active.Import.Register"); + getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "TxUpdatedMeasurands", "")->setString("Power.Active.Import"); + getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "TxUpdatedInterval", 0)->setInt(60); + getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "TxEndedMeasurands", "")->setString("Current.Import"); + getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "TxEndededInterval", 0)->setInt(100); + + setConnectorPluggedInput([] () {return false;}); + setEnergyMeterInput([] () {return 100;}); + setPowerMeterInput([] () {return 200;}); + addMeterValueInput([] () {return 30;}, "Current.Import", "A"); + + Timestamp tStart, tUpdated, tEnded; + + setOnReceiveRequest("TransactionEvent", [&tStart, &tUpdated, &tEnded] (JsonObject request) { + const char *eventType = request["eventType"] | (const char*)nullptr; + bool eventTypeError = false; + if (!strcmp(eventType, "Started")) { + tStart = getOcppContext()->getModel().getClock().now(); + + REQUIRE( request["meterValue"].as().size() >= 1 ); + + Timestamp tMv; + tMv.setTime(request["meterValue"][0]["timestamp"]); + REQUIRE( std::abs(tStart - tMv) <= 1); + + REQUIRE( request["meterValue"][0]["sampledValue"].as().size() >= 1 ); + + REQUIRE( !strcmp(request["meterValue"][0]["sampledValue"][0]["measurand"] | "_Undefined", "Energy.Active.Import.Register") ); + REQUIRE( !strcmp(request["meterValue"][0]["sampledValue"][0]["measurand"] | "_Undefined", "Energy.Active.Import.Register") ); + } else if (!strcmp(eventType, "Updated")) { + tUpdated = getOcppContext()->getModel().getClock().now(); + + } else if (!strcmp(eventType, "Ended")) { + tEnded = getOcppContext()->getModel().getClock().now(); + + } else { + eventTypeError = true; + } + REQUIRE( !eventTypeError ); + }); + + loop(); + + MO_MEM_RESET(); + + //run scenario + + setConnectorPluggedInput([] () {return true;}); + context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken"); + loop(); + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() != nullptr ); + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction()->started ); + REQUIRE( !context->getModel().getTransactionService()->getEvse(1)->getTransaction()->stopped ); + REQUIRE( getChargePointStatus() == ChargePointStatus_Occupied ); + + MO_DBG_INFO("Memory requirements UC E01 - S5:"); + + MO_MEM_PRINT_STATS(); + + context->getModel().getTransactionService()->getEvse(1)->endAuthorization(); + loop(); + + REQUIRE( (tStart > MIN_TIME) ); + //REQUIRE( (tUpdated > MIN_TIME) ); + REQUIRE( (tEnded > MIN_TIME) ); + + } + + SECTION("TxEvents queue") { + + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); + + bool checkReceivedStarted = false, checkReceivedEnded = false; + + getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded] () { + return new Ocpp16::CustomOperation("TransactionEvent", + [&checkReceivedStarted, &checkReceivedEnded] (JsonObject request) { + //process req + const char *eventType = request["eventType"] | (const char*)nullptr; + if (!strcmp(eventType, "Started")) { + checkReceivedStarted = true; + } else if (!strcmp(eventType, "Ended")) { + checkReceivedEnded = true; + } + }, + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTokenInfo"]["status"] = "Accepted"; + return doc; + });}); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + + loopback.setConnected(false); + + context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken", false); + + loop(); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() != nullptr ); + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction()->started ); + + context->getModel().getTransactionService()->getEvse(1)->endAuthorization(); + + loop(); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + + loopback.setConnected(true); + loop(); + + REQUIRE( checkReceivedStarted ); + REQUIRE( checkReceivedEnded ); + } + + SECTION("TxEvents queue size limit") { + + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); + + bool checkReceivedStarted = false, checkReceivedEnded = false; + size_t checkSeqNosSize = 0; + + getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded, &checkSeqNosSize] () { + return new Ocpp16::CustomOperation("TransactionEvent", + [&checkReceivedStarted, &checkReceivedEnded, &checkSeqNosSize] (JsonObject request) { + //process req + const char *eventType = request["eventType"] | (const char*)nullptr; + if (!strcmp(eventType, "Started")) { + checkReceivedStarted = true; + } else if (!strcmp(eventType, "Ended")) { + checkReceivedEnded = true; + } + checkSeqNosSize++; + }, + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTokenInfo"]["status"] = "Accepted"; + return doc; + });}); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + + loopback.setConnected(false); + + context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken", false); + + loop(); + + auto tx = context->getModel().getTransactionService()->getEvse(1)->getTransaction(); + REQUIRE( tx != nullptr ); + + for (size_t i = 0; i < MO_TXEVENTRECORD_SIZE_V201 * 2; i++) { + setEvReadyInput([] () {return false;}); + loop(); + setEvReadyInput([] () {return true;}); + loop(); + setEvReadyInput([] () {return false;}); + loop(); + } + + REQUIRE( tx->seqNos.size() == MO_TXEVENTRECORD_SIZE_V201 ); + + for (auto seqNo : tx->seqNos) { + MO_DBG_DEBUG("stored seqNo %u", seqNo); + (void)seqNo; + } + + for (size_t i = 1; i < tx->seqNos.size(); i++) { + auto delta = tx->seqNos[i] - tx->seqNos[i-1]; + REQUIRE(delta <= 2 * tx->seqNos.back() / MO_TXEVENTRECORD_SIZE_V201 ); + } + + context->getModel().getTransactionService()->getEvse(1)->endAuthorization(); + + loop(); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + + loopback.setConnected(true); + loop(); + + REQUIRE( checkReceivedStarted ); + REQUIRE( checkReceivedEnded ); + REQUIRE( checkSeqNosSize == MO_TXEVENTRECORD_SIZE_V201 ); + } + + SECTION("Tx queue") { + + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); + + std::map> txEventRequests; + + getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&txEventRequests] () { + return new Ocpp16::CustomOperation("TransactionEvent", + [&txEventRequests] (JsonObject request) { + //process req + const char *eventType = request["eventType"] | (const char*)nullptr; + if (!strcmp(eventType, "Started")) { + std::get<0>(txEventRequests[request["transactionInfo"]["transactionId"] | "_Undefined"]) = true; + } else if (!strcmp(eventType, "Ended")) { + std::get<1>(txEventRequests[request["transactionInfo"]["transactionId"] | "_Undefined"]) = true; + } + }, + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTokenInfo"]["status"] = "Accepted"; + return doc; + });}); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + + loopback.setConnected(false); + + for (size_t i = 0; i < MO_TXRECORD_SIZE_V201; i++) { + + char idTokenBuf [MO_IDTOKEN_LEN_MAX + 1]; + snprintf(idTokenBuf, sizeof(idTokenBuf), "mIdToken-%zu", i); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->beginAuthorization(idTokenBuf, false) ); + + loop(); + + auto tx = context->getModel().getTransactionService()->getEvse(1)->getTransaction(); + + REQUIRE( tx != nullptr ); + REQUIRE( tx->started ); + txEventRequests[tx->transactionId] = {false, false}; + + context->getModel().getTransactionService()->getEvse(1)->endAuthorization(); + + loop(); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + } + + REQUIRE( !context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken", false) ); + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + + loopback.setConnected(true); + loop(); + + for (const auto& txReq : txEventRequests) { + MO_DBG_DEBUG("check txId %s", txReq.first.c_str()); + REQUIRE( std::get<0>(txReq.second) ); + REQUIRE( std::get<1>(txReq.second) ); + } + + REQUIRE( txEventRequests.size() == MO_TXRECORD_SIZE_V201 ); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->beginAuthorization("mIdToken", false) ); + loop(); + auto tx = context->getModel().getTransactionService()->getEvse(1)->getTransaction(); + REQUIRE( tx != nullptr ); + REQUIRE( tx->started ); + + context->getModel().getTransactionService()->getEvse(1)->endAuthorization(); + loop(); + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + } + + SECTION("Power loss during running transaction") { + + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); + + REQUIRE( getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + + const char *idTag = "example123"; + + getOcppContext()->getModel().getTransactionService()->getEvse(1)->beginAuthorization(idTag, false); + loop(); + + auto tx = getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction(); + REQUIRE( tx != nullptr ); + REQUIRE( tx->started ); + + auto txNr = tx->txNr; + std::string txId = tx->transactionId; + + //power cut + mocpp_deinitialize(); + + //power restored + mocpp_initialize(loopback, + ChargerCredentials(), + makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail), + false, + ProtocolVersion(2,0,1)); + mocpp_set_timer(custom_timer_cb); + + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); + + bool checkProcessed = false; + + getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkProcessed, txId] () { + return new Ocpp16::CustomOperation("TransactionEvent", + [&checkProcessed, txId] (JsonObject request) { + //process req + const char *eventType = request["eventType"] | (const char*)nullptr; + REQUIRE( strcmp(eventType, "Started") ); + if (!strcmp(eventType, "Ended")) { + checkProcessed = true; + } + REQUIRE( !txId.compare(request["transactionInfo"]["transactionId"] | "_Undefined") ); + }, + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTokenInfo"]["status"] = "Accepted"; + return doc; + });}); + + loop(); //let MO spin up and reconnect + + tx = getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction(); + REQUIRE( tx != nullptr ); + REQUIRE( tx->started ); + REQUIRE( !tx->stopped ); + REQUIRE( tx->txNr == txNr ); + REQUIRE( !txId.compare(tx->transactionId) ); + REQUIRE( !strcmp(tx->idToken.get(), idTag) ); + + getOcppContext()->getModel().getTransactionService()->getEvse(1)->endAuthorization(); + loop(); + + tx = getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction(); + REQUIRE( tx == nullptr ); + REQUIRE( checkProcessed ); //txEvent was sent + } + + SECTION("Power loss with enqueued txEvents") { + + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); + + REQUIRE( getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + + loopback.setConnected(false); + + const char *idTag = "example123"; + + getOcppContext()->getModel().getTransactionService()->getEvse(1)->beginAuthorization(idTag, false); + loop(); + + auto tx = getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction(); + REQUIRE( tx != nullptr ); + REQUIRE( tx->started ); + + auto txNr = tx->txNr; + std::string txId = tx->transactionId; + + setEvReadyInput([] () {return false;}); + loop(); + setEvReadyInput([] () {return true;}); + loop(); + setEvReadyInput([] () {return false;}); + loop(); + + size_t seqNosSize = tx->seqNos.size(); + size_t checkSeqNosSize = 0; + + //power cut + mocpp_deinitialize(); + + //power restored + mocpp_initialize(loopback, + ChargerCredentials(), + makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail), + false, + ProtocolVersion(2,0,1)); + mocpp_set_timer(custom_timer_cb); + + loopback.setConnected(true); + + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); + + bool checkReceivedStarted = false, checkReceivedEnded = false; + + getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&checkReceivedStarted, &checkReceivedEnded, txId, &checkSeqNosSize] () { + return new Ocpp16::CustomOperation("TransactionEvent", + [&checkReceivedStarted, &checkReceivedEnded, txId, &checkSeqNosSize] (JsonObject request) { + //process req + const char *eventType = request["eventType"] | (const char*)nullptr; + if (!strcmp(eventType, "Started")) { + checkReceivedStarted = true; + } else if (!strcmp(eventType, "Ended")) { + checkReceivedEnded = true; + } + REQUIRE( !txId.compare(request["transactionInfo"]["transactionId"] | "_Undefined") ); + checkSeqNosSize++; + }, + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTokenInfo"]["status"] = "Accepted"; + return doc; + });}); + + loop(); //let MO spin up and reconnect + + REQUIRE( checkReceivedStarted ); + REQUIRE( (seqNosSize == checkSeqNosSize || seqNosSize + 1 == checkSeqNosSize) ); + + tx = getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction(); + REQUIRE( tx != nullptr ); + REQUIRE( tx->started ); + REQUIRE( !tx->stopped ); + REQUIRE( tx->txNr == txNr ); + REQUIRE( !txId.compare(tx->transactionId) ); + REQUIRE( !strcmp(tx->idToken.get(), idTag) ); + + getOcppContext()->getModel().getTransactionService()->getEvse(1)->endAuthorization(); + loop(); + + tx = getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction(); + REQUIRE( tx == nullptr ); + REQUIRE( checkReceivedEnded ); //txEvent was sent + } + + SECTION("Power loss with enqueued transactions") { + + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); + + std::map> txEventRequests; + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + + loopback.setConnected(false); + + for (size_t i = 0; i < MO_TXRECORD_SIZE_V201; i++) { + + char idTokenBuf [MO_IDTOKEN_LEN_MAX + 1]; + snprintf(idTokenBuf, sizeof(idTokenBuf), "mIdToken-%zu", i); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->beginAuthorization(idTokenBuf, false) ); + + loop(); + + auto tx = context->getModel().getTransactionService()->getEvse(1)->getTransaction(); + + REQUIRE( tx != nullptr ); + REQUIRE( tx->started ); + txEventRequests[tx->transactionId] = {false, false}; + + context->getModel().getTransactionService()->getEvse(1)->endAuthorization(); + + loop(); + + REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + } + + //power cut + mocpp_deinitialize(); + + //power restored + mocpp_initialize(loopback, + ChargerCredentials(), + makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail), + false, + ProtocolVersion(2,0,1)); + mocpp_set_timer(custom_timer_cb); + + loopback.setConnected(true); + + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("Authorized"); + getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("Authorized"); + + getOcppContext()->getOperationRegistry().registerOperation("TransactionEvent", [&txEventRequests] () { + return new Ocpp16::CustomOperation("TransactionEvent", + [&txEventRequests] (JsonObject request) { + //process req + const char *eventType = request["eventType"] | (const char*)nullptr; + if (!strcmp(eventType, "Started")) { + std::get<0>(txEventRequests[request["transactionInfo"]["transactionId"] | "_Undefined"]) = true; + } else if (!strcmp(eventType, "Ended")) { + std::get<1>(txEventRequests[request["transactionInfo"]["transactionId"] | "_Undefined"]) = true; + } + }, + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTokenInfo"]["status"] = "Accepted"; + return doc; + });}); + + loopback.setConnected(true); + loop(); + + for (const auto& txReq : txEventRequests) { + MO_DBG_DEBUG("check txId %s", txReq.first.c_str()); + REQUIRE( std::get<0>(txReq.second) ); + REQUIRE( std::get<1>(txReq.second) ); + } + + REQUIRE( txEventRequests.size() == MO_TXRECORD_SIZE_V201 ); + + REQUIRE( getOcppContext()->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); + } + + mocpp_deinitialize(); +} + +#endif // MO_ENABLE_V201 diff --git a/tests/Variables.cpp b/tests/Variables.cpp new file mode 100644 index 00000000..158140fc --- /dev/null +++ b/tests/Variables.cpp @@ -0,0 +1,661 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_V201 + +#include +#include +#include +#include "./helpers/testHelper.h" + +#include +#include +#include + +#include +#include +#include + +using namespace MicroOcpp; + +#define GET_CONFIG_ALL "[2,\"test-msg\",\"GetVariable\",{}]" +#define KNOWN_KEY "__ExistingKey" +#define UNKOWN_KEY "__UnknownKey" +#define GET_CONFIG_KNOWN_UNKOWN "[2,\"test-mst\",\"GetVariable\",{\"key\":[\"" KNOWN_KEY "\",\"" UNKOWN_KEY "\"]}]" + +TEST_CASE( "Variable" ) { + printf("\nRun %s\n", "Variable"); + + //clean state + auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); + FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); + + SECTION("Basic container operations"){ + auto container = std::unique_ptr(new VariableContainerOwning()); + + //check emptyness + REQUIRE( container->size() == 0 ); + + //add first config, fetch by index + Variable::AttributeTypeSet attrs = Variable::AttributeType::Actual; + auto configFirst = makeVariable(Variable::InternalDataType::Int, attrs); + configFirst->setName("cFirst"); + configFirst->setComponentId("mComponent"); + auto configFirstRaw = configFirst.get(); + REQUIRE( container->size() == 0 ); + REQUIRE( container->add(std::move(configFirst)) ); + REQUIRE( container->size() == 1 ); + REQUIRE( container->getVariable((size_t) 0) == configFirstRaw); + + //add one config of each type + auto cInt = makeVariable(Variable::InternalDataType::Int, attrs); + cInt->setName("cInt"); + cInt->setComponentId("mComponent"); + auto cBool = makeVariable(Variable::InternalDataType::Bool, attrs); + cBool->setName("cBool"); + cBool->setComponentId("mComponent"); + auto cBoolRaw = cBool.get(); + auto cString = makeVariable(Variable::InternalDataType::String, attrs); + cString->setName("cString"); + cString->setComponentId("mComponent"); + + container->add(std::move(cInt)); + container->add(std::move(cBool)); + container->add(std::move(cString)); + + REQUIRE( container->size() == 4 ); + + //fetch config by key + REQUIRE( container->getVariable(cBoolRaw->getComponentId(), cBoolRaw->getName()) == cBoolRaw); + } + + SECTION("Persistency on filesystem") { + + auto container = std::unique_ptr(new VariableContainerOwning()); + container->enablePersistency(filesystem, MO_FILENAME_PREFIX "persistent1.jsn"); + + //trivial load call + REQUIRE( container->load() ); + REQUIRE( container->size() == 0 ); + + //add config, store, load again + auto cString = makeVariable(Variable::InternalDataType::String, Variable::AttributeType::Actual); + cString->setName("cString"); + cString->setComponentId("mComponent"); + cString->setString("mValue"); + container->add(std::move(cString)); + REQUIRE( container->size() == 1 ); + + REQUIRE( container->commit() ); //store + + container.reset(); //destroy + + //...load again + auto container2 = std::unique_ptr(new VariableContainerOwning()); + container2->enablePersistency(filesystem, MO_FILENAME_PREFIX "persistent1.jsn"); + REQUIRE( container2->size() == 0 ); + + auto cString2 = makeVariable(Variable::InternalDataType::String, Variable::AttributeType::Actual); + cString2->setName("cString"); + cString2->setComponentId("mComponent"); + cString2->setString("mValue"); + container2->add(std::move(cString2)); + REQUIRE( container2->size() == 1 ); + + REQUIRE( container2->load() ); + REQUIRE( container2->size() == 1 ); + + auto cString3 = container2->getVariable("mComponent", "cString"); + REQUIRE( cString3 != nullptr ); + REQUIRE( !strcmp(cString3->getString(), "mValue") ); + } + + LoopbackConnection loopback; //initialize Context with dummy socket + mocpp_set_timer(custom_timer_cb); + + SECTION("Variable API") { + + //declare configs + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); + auto vs = getOcppContext()->getModel().getVariableService(); + auto cInt = vs->declareVariable("mComponent", "cInt", 42); + REQUIRE( cInt != nullptr ); + vs->declareVariable("mComponent", "cBool", true); + vs->declareVariable("mComponent", "cString", "mValue"); + + //fetch config + REQUIRE( vs->declareVariable("mComponent", "cInt", -1)->getInt() == 42 ); + +#if 0 + //store, destroy, reload + REQUIRE( configuration_save() ); + cInt.reset(); + configuration_deinit(); + REQUIRE( getVariablePublic("cInt") == nullptr); + + REQUIRE( configuration_init(filesystem) ); //reload + + //fetch configs (declare with different factory default - should remain at original value) + auto cInt2 = vs->declareVariable("cInt", -1); + auto cBool2 = vs->declareVariable("cBool", false); + auto cString2 = vs->declareVariable("cString", "no effect"); + REQUIRE( configuration_load() ); //load config objects with stored values + + //check load result + REQUIRE( cInt2->getInt() == 42 ); + REQUIRE( cBool2->getBool() == true ); + REQUIRE( !strcmp(cString2->getString(), "mValue") ); +#else + auto cInt2 = cInt; +#endif + + //declare config twice + auto cInt3 = vs->declareVariable("mComponent", "cInt", -1); + REQUIRE( cInt3 == cInt2 ); + +#if 0 + //store, destroy, reload + REQUIRE( configuration_save() ); + configuration_deinit(); + REQUIRE( getVariablePublic("cInt") == nullptr); + REQUIRE( configuration_init(filesystem) ); //reload + auto cNewType2 = vs->declareVariable("cInt", "no effect"); + REQUIRE( configuration_load() ); + REQUIRE( !strcmp(cNewType2->getString(), "mValue2") ); + + //get config before declared (container needs to be declared already at this point) + auto cString3 = getVariablePublic("cString"); + REQUIRE( !strcmp(cString3->getString(), "mValue") ); + configuration_deinit(); + + //value needs to outlive container + configuration_init(filesystem); + auto cString4 = vs->declareVariable("cString2", "mValue3"); + configuration_deinit(); + REQUIRE( !strcmp(cString4->getString(), "mValue3") ); + + FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); +#else + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); +#endif + + //config accessibility / permissions + vs = getOcppContext()->getModel().getVariableService(); + Variable::Mutability mutability = Variable::Mutability::ReadWrite; + bool persistent = false; + Variable::AttributeTypeSet attrs = Variable::AttributeType::Actual; + bool rebootRequired = false; + auto cInt6 = vs->declareVariable("mComponent", "cInt", 42, mutability, persistent, attrs, rebootRequired); + REQUIRE( cInt6->getMutability() == Variable::Mutability::ReadWrite ); + REQUIRE( !cInt6->isPersistent() ); + REQUIRE( !cInt6->isRebootRequired() ); + REQUIRE( vs->declareVariable("mComponent", "cInt", 42) ); + + //revoke permissions + mutability = Variable::Mutability::ReadOnly; + persistent = true; + rebootRequired = true; + vs->declareVariable("mComponent", "cInt", 42, mutability, persistent, attrs, rebootRequired); + REQUIRE( cInt6->getMutability() == mutability ); + REQUIRE( cInt6->isPersistent() ); + REQUIRE( cInt6->isRebootRequired() ); + + //revoked permissions cannot be reverted + mutability = Variable::Mutability::ReadWrite; + persistent = false; + rebootRequired = false; + auto cInt7 = vs->declareVariable("mComponent", "cInt", 42, mutability, persistent, attrs, rebootRequired); + REQUIRE( cInt7->getMutability() == Variable::Mutability::ReadOnly ); + REQUIRE( cInt6->isPersistent() ); + REQUIRE( cInt7->isRebootRequired() ); + } + +#if 0 + SECTION("Main lib integration") { + + //basic lifecycle + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); + REQUIRE( getVariablePublic("ConnectionTimeOut") ); + REQUIRE( !getVariableContainersPublic().empty() ); + mocpp_deinitialize(); + REQUIRE( !getVariablePublic("ConnectionTimeOut") ); + REQUIRE( getVariableContainersPublic().empty() ); + + //modify standard config ConnectionTimeOut. This config is not modified by the main lib during normal initialization / deinitialization + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); + auto config = getVariablePublic("ConnectionTimeOut"); + + config->setInt(1234); //update + configuration_save(); //write back + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); + REQUIRE( getVariablePublic("ConnectionTimeOut")->getInt() == 1234 ); + + mocpp_deinitialize(); + } +#endif + +#if 0 + SECTION("GetVariables") { + + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); + loop(); + + vs->declareVariable(KNOWN_KEY, 1234, MO_FILENAME_PREFIX "persistent1.jsn", false); + + bool checkProcessed = false; + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "GetVariables", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + JsonArray configurationKey = payload["configurationKey"]; + + bool foundCustomConfig = false; + bool foundStandardConfig = false; + for (JsonObject keyvalue : configurationKey) { + MO_DBG_DEBUG("key %s", keyvalue["key"] | "_Undefined"); + if (!strcmp(keyvalue["key"] | "_Undefined", KNOWN_KEY)) { + foundCustomConfig = true; + REQUIRE( (keyvalue["readonly"] | true) == false ); + REQUIRE( !strcmp(keyvalue["value"] | "_Undefined", "1234") ); + } else if (!strcmp(keyvalue["key"] | "_Undefined", "ConnectionTimeOut")) { + foundStandardConfig = true; + } + } + + REQUIRE( foundCustomConfig ); + REQUIRE( foundStandardConfig ); + } + ))); + + loop(); + + REQUIRE(checkProcessed); + + checkProcessed = false; + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "GetVariable", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(2)); + auto payload = doc->to(); + auto key = payload.createNestedArray("key"); + key.add(KNOWN_KEY); + key.add(UNKOWN_KEY); + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + JsonArray configurationKey = payload["configurationKey"]; + + bool foundCustomConfig = false; + for (JsonObject keyvalue : configurationKey) { + if (!strcmp(keyvalue["key"] | "_Undefined", KNOWN_KEY)) { + foundCustomConfig = true; + break; + } + } + REQUIRE( foundCustomConfig ); + + JsonArray unknownKey = payload["unknownKey"]; + + bool foundUnkownKey = false; + for (const char *key : unknownKey) { + if (!strcmp(key, UNKOWN_KEY)) { + foundUnkownKey = true; + } + } + + REQUIRE( foundUnkownKey ); + } + ))); + + loop(); + + REQUIRE(checkProcessed); + + mocpp_deinitialize(); + } + + SECTION("ChangeVariable") { + + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); + loop(); + + vs->declareVariable(KNOWN_KEY, 0, MO_FILENAME_PREFIX "persistent1.jsn", false); + + //update existing config + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ChangeVariable", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["key"] = KNOWN_KEY; + payload["value"] = "1234"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE(checkProcessed); + REQUIRE( getVariablePublic(KNOWN_KEY)->getInt() == 1234 ); + + //try to update not existing key + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ChangeVariable", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["key"] = UNKOWN_KEY; + payload["value"] = "no effect"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "NotSupported") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + //try to update config with malformatted value + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ChangeVariable", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["key"] = KNOWN_KEY; + payload["value"] = "not convertible to int"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Rejected") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + //try to update config with value validation + //value is valid if it begins with 1 + registerVariableValidator(KNOWN_KEY, [] (const char *v) { + return v[0] == '1'; + }); + + //validation success + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ChangeVariable", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["key"] = KNOWN_KEY; + payload["value"] = "100234"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + REQUIRE( getVariablePublic(KNOWN_KEY)->getInt() == 100234 ); + + //validation failure + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ChangeVariable", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["key"] = KNOWN_KEY; + payload["value"] = "4321"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Rejected") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + REQUIRE( getVariablePublic(KNOWN_KEY)->getInt() == 100234 ); //keep old value + + mocpp_deinitialize(); + } + + SECTION("Define factory defaults for standard configs") { + + //set factory default for standard config ConnectionTimeOut + configuration_init(filesystem); + auto factoryConnectionTimeOut = vs->declareVariable("ConnectionTimeOut", 1234, MO_FILENAME_PREFIX "factory.jsn"); + + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); + + auto connectionTimeout2 = vs->declareVariable("ConnectionTimeOut", 4321); + REQUIRE( connectionTimeout2->getInt() == 1234 ); + REQUIRE( connectionTimeout2 == factoryConnectionTimeOut ); + + configuration_save(); + mocpp_deinitialize(); + + //this time, factory default is not given (will lead to duplicates, should be considered in sanitization) + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); + REQUIRE( getVariablePublic("ConnectionTimeOut")->getInt() != 1234 ); + mocpp_deinitialize(); + + //provide factory default again + configuration_init(filesystem); + vs->declareVariable("ConnectionTimeOut", 4321, MO_FILENAME_PREFIX "factory.jsn"); + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); + REQUIRE( getVariablePublic("ConnectionTimeOut")->getInt() == 1234 ); + mocpp_deinitialize(); + + } +#endif + + SECTION("GetVariables request") { + + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); + + auto vs = getOcppContext()->getModel().getVariableService(); + auto varString = vs->declareVariable("mComponent", "mString", "mValue"); + REQUIRE( varString != nullptr ); + REQUIRE( !strcmp(varString->getString(), "mValue") ); + + loop(); + + MO_MEM_RESET(); + + bool checkProcessed = false; + + getOcppContext()->initiateRequest(makeRequest( + new Ocpp16::CustomOperation("GetVariables", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(1) + + JSON_ARRAY_SIZE(1) + + JSON_OBJECT_SIZE(2) + + JSON_OBJECT_SIZE(1) + + JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + auto getVariableData = payload.createNestedArray("getVariableData"); + getVariableData[0]["component"]["name"] = "mComponent"; + getVariableData[0]["variable"]["name"] = "mString"; + return doc; + }, + [&checkProcessed] (JsonObject payload) { + //process conf + JsonArray getVariableResult = payload["getVariableResult"]; + REQUIRE( !strcmp(getVariableResult[0]["attributeStatus"] | "_Undefined", "Accepted") ); + REQUIRE( !strcmp(getVariableResult[0]["component"]["name"] | "_Undefined", "mComponent") ); + REQUIRE( !strcmp(getVariableResult[0]["variable"]["name"] | "_Undefined", "mString") ); + REQUIRE( !strcmp(getVariableResult[0]["attributeValue"] | "_Undefined", "mValue") ); + checkProcessed = true; + }))); + + loop(); + + REQUIRE( checkProcessed ); + + MO_MEM_PRINT_STATS(); + + } + + SECTION("SetVariables request") { + + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); + + auto vs = getOcppContext()->getModel().getVariableService(); + auto varString = vs->declareVariable("mComponent", "mString", ""); + REQUIRE( varString != nullptr ); + REQUIRE( !strcmp(varString->getString(), "") ); + + loop(); + + MO_MEM_RESET(); + + bool checkProcessed = false; + + getOcppContext()->initiateRequest(makeRequest( + new Ocpp16::CustomOperation("SetVariables", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(1) + + JSON_ARRAY_SIZE(1) + + JSON_OBJECT_SIZE(3) + + JSON_OBJECT_SIZE(1) + + JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + auto setVariableData = payload.createNestedArray("setVariableData"); + setVariableData[0]["component"]["name"] = "mComponent"; + setVariableData[0]["variable"]["name"] = "mString"; + setVariableData[0]["attributeValue"] = "mValue"; + return doc; + }, + [&checkProcessed] (JsonObject payload) { + //process conf + JsonArray setVariableResult = payload["setVariableResult"]; + REQUIRE( !strcmp(setVariableResult[0]["attributeStatus"] | "_Undefined", "Accepted") ); + REQUIRE( !strcmp(setVariableResult[0]["component"]["name"] | "_Undefined", "mComponent") ); + REQUIRE( !strcmp(setVariableResult[0]["variable"]["name"] | "_Undefined", "mString") ); + checkProcessed = true; + }))); + + loop(); + + REQUIRE( checkProcessed ); + + MO_MEM_PRINT_STATS(); + } + + SECTION("GetBaseReport request") { + + mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); + + auto vs = getOcppContext()->getModel().getVariableService(); + auto varString = vs->declareVariable("mComponent", "mString", ""); + REQUIRE( varString != nullptr ); + REQUIRE( !strcmp(varString->getString(), "") ); + + loop(); + + MO_MEM_RESET(); + + bool checkProcessedNotification = false; + Timestamp checkTimestamp; + + getOcppContext()->getOperationRegistry().registerOperation("NotifyReport", + [&checkProcessedNotification, &checkTimestamp] () { + return new Ocpp16::CustomOperation("NotifyReport", + [ &checkProcessedNotification, &checkTimestamp] (JsonObject payload) { + //process req + checkProcessedNotification = true; + REQUIRE( (payload["requestId"] | -1) == 1); + checkTimestamp.setTime(payload["generatedAt"] | "_Undefined"); + REQUIRE( (payload["seqNo"] | -1) == 0); + + bool foundVar = false; + for (auto reportData : payload["reportData"].as()) { + if (!strcmp(reportData["component"]["name"] | "_Undefined", "mComponent") && + !strcmp(reportData["variable"]["name"] | "_Undefined", "mString")) { + foundVar = true; + } + } + REQUIRE( foundVar ); + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); + + bool checkProcessed = false; + + getOcppContext()->initiateRequest(makeRequest( + new Ocpp16::CustomOperation("GetBaseReport", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["requestId"] = 1; + payload["reportBase"] = "FullInventory"; + return doc; + }, + [&checkProcessed] (JsonObject payload) { + //process conf + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); + checkProcessed = true; + }))); + + loop(); + + REQUIRE( checkProcessed ); + REQUIRE( checkProcessedNotification ); + REQUIRE( std::abs(getOcppContext()->getModel().getClock().now() - checkTimestamp) <= 10 ); + + MO_MEM_PRINT_STATS(); + + } + + mocpp_deinitialize(); +} + +#endif // MO_ENABLE_V201 diff --git a/tests/benchmarks/firmware_size/main.cpp b/tests/benchmarks/firmware_size/main.cpp new file mode 100644 index 00000000..6e849b8d --- /dev/null +++ b/tests/benchmarks/firmware_size/main.cpp @@ -0,0 +1,68 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include + +MicroOcpp::LoopbackConnection g_loopback; + +void setup() { + + ocpp_deinitialize(); + +#if MO_ENABLE_V201 + mocpp_initialize(g_loopback, ChargerCredentials::v201(),MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail),true,MicroOcpp::ProtocolVersion(2,0,1)); +#else + mocpp_initialize(g_loopback, ChargerCredentials()); +#endif + + ocpp_beginTransaction(""); + ocpp_beginTransaction_authorized("",""); + ocpp_endTransaction("",""); + ocpp_endTransaction_authorized("",""); + ocpp_isTransactionActive(); + ocpp_isTransactionRunning(); + ocpp_getTransactionIdTag(); + ocpp_getTransaction(); + ocpp_ocppPermitsCharge(); + ocpp_getChargePointStatus(); + ocpp_setConnectorPluggedInput([] () {return false;}); + ocpp_setEnergyMeterInput([] () {return 0;}); + ocpp_setPowerMeterInput([] () {return 0.f;}); + ocpp_setSmartChargingPowerOutput([] (float) {}); + ocpp_setSmartChargingCurrentOutput([] (float) {}); + ocpp_setSmartChargingOutput([] (float,float,int) {}); + ocpp_setEvReadyInput([] () {return false;}); + ocpp_setEvseReadyInput([] () {return false;}); + ocpp_addErrorCodeInput([] () {return (const char*)nullptr;}); + addErrorDataInput([] () {return MicroOcpp::ErrorData("");}); + ocpp_addMeterValueInputFloat([] () {return 0.f;},"","","",""); + ocpp_setOccupiedInput([] () {return false;}); + ocpp_setStartTxReadyInput([] () {return false;}); + ocpp_setStopTxReadyInput([] () {return false;}); + ocpp_setTxNotificationOutput([] (OCPP_Transaction*, TxNotification) {}); + +#if MO_ENABLE_CONNECTOR_LOCK + ocpp_setOnUnlockConnectorInOut([] () {return UnlockConnectorResult_UnlockFailed;}); +#endif + + isOperative(); + setOnResetNotify([] (bool) {return false;}); + setOnResetExecute([] (bool) {return false;}); + getFirmwareService()->getFirmwareStatus(); + getDiagnosticsService()->getDiagnosticsStatus(); + +#if MO_ENABLE_CERT_MGMT + setCertificateStore(nullptr); +#endif + + getOcppContext(); + +} + +void loop() { + mocpp_loop(); +} diff --git a/tests/benchmarks/firmware_size/platformio.ini b/tests/benchmarks/firmware_size/platformio.ini new file mode 100644 index 00000000..121a8de5 --- /dev/null +++ b/tests/benchmarks/firmware_size/platformio.ini @@ -0,0 +1,38 @@ +; matth-x/MicroOcpp +; Copyright Matthias Akstaller 2019 - 2024 +; MIT License + +[common] +platform = espressif32@6.8.1 +board = esp-wrover-kit +framework = arduino +lib_deps = + bblanchon/ArduinoJson@6.20.1 +build_flags= + -D MO_DBG_LEVEL=MO_DL_NONE ; don't take debug messages into account + -D MO_CUSTOM_WS + +[env:v16] +platform = ${common.platform} +board = ${common.board} +framework = ${common.framework} +lib_deps = ${common.lib_deps} +build_flags = + ${common.build_flags} + -D MO_ENABLE_MBEDTLS=1 + -D MO_ENABLE_CERT_MGMT=1 + -D MO_ENABLE_RESERVATION=1 + -D MO_ENABLE_LOCAL_AUTH=1 + -D MO_REPORT_NOERROR=1 + -D MO_ENABLE_CONNECTOR_LOCK=1 + +[env:v201] +platform = ${common.platform} +board = ${common.board} +framework = ${common.framework} +lib_deps = ${common.lib_deps} +build_flags = + ${common.build_flags} + -D MO_ENABLE_V201=1 + -D MO_ENABLE_MBEDTLS=1 + -D MO_ENABLE_CERT_MGMT=1 diff --git a/tests/benchmarks/scripts/eval_firmware_size.py b/tests/benchmarks/scripts/eval_firmware_size.py new file mode 100755 index 00000000..a952f7f8 --- /dev/null +++ b/tests/benchmarks/scripts/eval_firmware_size.py @@ -0,0 +1,364 @@ +import sys +import numpy as np +import pandas as pd + +# load data + +COLUMN_BINSIZE = 'Binary size (Bytes)' + +def load_compilation_units(fn): + df = pd.read_csv(fn, index_col="compileunits").filter(like="lib/MicroOcpp/src/MicroOcpp", axis=0).filter(['Module','v16','v201','vmsize'], axis=1).sort_index() + df.index.names = ['Compile Unit'] + df.index = df.index.map(lambda s: s[len("lib/MicroOcpp/src/"):] if s.startswith("lib/MicroOcpp/src/") else s) + df.index = df.index.map(lambda s: s[len("MicroOcpp/"):] if s.startswith("MicroOcpp/") else s) + df.rename(columns={'vmsize': COLUMN_BINSIZE}, inplace=True) + return df + +cunits_v16 = load_compilation_units('docs/assets/tables/bloaty_v16.csv') +cunits_v201 = load_compilation_units('docs/assets/tables/bloaty_v201.csv') + +# categorize data + +def categorize_table(df): + + df["v16"] = ' ' + df["v201"] = ' ' + df["Module"] = '' + + TICK = 'x' + + MODULE_GENERAL = 'General' + MODULE_HAL = 'General - Hardware Abstraction Layer' + MODULE_RPC = 'General - RPC framework' + MODULE_API = 'General - API' + MODULE_CORE = 'Core' + MODULE_CONFIGURATION = 'Configuration' + MODULE_FW_MNGT = 'Firmware Management' + MODULE_TRIGGERMESSAGE = 'TriggerMessage' + MODULE_SECURITY = 'A - Security' + MODULE_PROVISIONING = 'B - Provisioning' + MODULE_PROVISIONING_VARS = 'B - Provisioning - Variables' + MODULE_AUTHORIZATION = 'C - Authorization' + MODULE_LOCALAUTH = 'D - Local Authorization List Management' + MODULE_TX = 'E - Transactions' + MODULE_REMOTECONTROL = 'F - RemoteControl' + MODULE_AVAILABILITY = 'G - Availability' + MODULE_RESERVATION = 'H - Reservation' + MODULE_METERVALUES = 'J - MeterValues' + MODULE_SMARTCHARGING = 'K - SmartCharging' + MODULE_CERTS = 'M - Certificate Management' + + df.at['MicroOcpp.cpp', 'v16'] = TICK + df.at['MicroOcpp.cpp', 'v201'] = TICK + df.at['MicroOcpp.cpp', 'Module'] = MODULE_API + df.at['Core/Configuration.cpp', 'v16'] = TICK + df.at['Core/Configuration.cpp', 'v201'] = TICK + df.at['Core/Configuration.cpp', 'Module'] = MODULE_CONFIGURATION + if 'Core/Configuration_c.cpp' in df.index: + df.at['Core/Configuration_c.cpp', 'v16'] = TICK + df.at['Core/Configuration_c.cpp', 'v201'] = TICK + df.at['Core/Configuration_c.cpp', 'Module'] = MODULE_CONFIGURATION + df.at['Core/ConfigurationContainer.cpp', 'v16'] = TICK + df.at['Core/ConfigurationContainer.cpp', 'v201'] = TICK + df.at['Core/ConfigurationContainer.cpp', 'Module'] = MODULE_CONFIGURATION + df.at['Core/ConfigurationContainerFlash.cpp', 'v16'] = TICK + df.at['Core/ConfigurationContainerFlash.cpp', 'v201'] = TICK + df.at['Core/ConfigurationContainerFlash.cpp', 'Module'] = MODULE_CONFIGURATION + df.at['Core/ConfigurationKeyValue.cpp', 'v16'] = TICK + df.at['Core/ConfigurationKeyValue.cpp', 'v201'] = TICK + df.at['Core/ConfigurationKeyValue.cpp', 'Module'] = MODULE_CONFIGURATION + df.at['Core/Connection.cpp', 'v16'] = TICK + df.at['Core/Connection.cpp', 'v201'] = TICK + df.at['Core/Connection.cpp', 'Module'] = MODULE_HAL + df.at['Core/Context.cpp', 'v16'] = TICK + df.at['Core/Context.cpp', 'v201'] = TICK + df.at['Core/Context.cpp', 'Module'] = MODULE_GENERAL + df.at['Core/FilesystemAdapter.cpp', 'v16'] = TICK + df.at['Core/FilesystemAdapter.cpp', 'v201'] = TICK + df.at['Core/FilesystemAdapter.cpp', 'Module'] = MODULE_HAL + df.at['Core/FilesystemUtils.cpp', 'v16'] = TICK + df.at['Core/FilesystemUtils.cpp', 'v201'] = TICK + df.at['Core/FilesystemUtils.cpp', 'Module'] = MODULE_GENERAL + df.at['Core/FtpMbedTLS.cpp', 'v16'] = TICK + df.at['Core/FtpMbedTLS.cpp', 'v201'] = TICK + df.at['Core/FtpMbedTLS.cpp', 'Module'] = MODULE_GENERAL + df.at['Core/Memory.cpp', 'v16'] = TICK + df.at['Core/Memory.cpp', 'v201'] = TICK + df.at['Core/Memory.cpp', 'Module'] = MODULE_GENERAL + df.at['Core/Operation.cpp', 'v16'] = TICK + df.at['Core/Operation.cpp', 'v201'] = TICK + df.at['Core/Operation.cpp', 'Module'] = MODULE_RPC + df.at['Core/OperationRegistry.cpp', 'v16'] = TICK + df.at['Core/OperationRegistry.cpp', 'v201'] = TICK + df.at['Core/OperationRegistry.cpp', 'Module'] = MODULE_RPC + df.at['Core/Request.cpp', 'v16'] = TICK + df.at['Core/Request.cpp', 'v201'] = TICK + df.at['Core/Request.cpp', 'Module'] = MODULE_RPC + df.at['Core/RequestQueue.cpp', 'v16'] = TICK + df.at['Core/RequestQueue.cpp', 'v201'] = TICK + df.at['Core/RequestQueue.cpp', 'Module'] = MODULE_RPC + df.at['Core/Time.cpp', 'v16'] = TICK + df.at['Core/Time.cpp', 'v201'] = TICK + df.at['Core/Time.cpp', 'Module'] = MODULE_GENERAL + df.at['Core/UuidUtils.cpp', 'v16'] = TICK + df.at['Core/UuidUtils.cpp', 'v201'] = TICK + df.at['Core/UuidUtils.cpp', 'Module'] = MODULE_GENERAL + if 'Debug.cpp' in df.index: + df.at['Debug.cpp', 'v16'] = TICK + df.at['Debug.cpp', 'v201'] = TICK + df.at['Debug.cpp', 'Module'] = MODULE_HAL + if 'Platform.cpp' in df.index: + df.at['Platform.cpp', 'v16'] = TICK + df.at['Platform.cpp', 'v201'] = TICK + df.at['Platform.cpp', 'Module'] = MODULE_HAL + df.at['Model/Authorization/AuthorizationData.cpp', 'v16'] = TICK + df.at['Model/Authorization/AuthorizationData.cpp', 'Module'] = MODULE_LOCALAUTH + df.at['Model/Authorization/AuthorizationList.cpp', 'v16'] = TICK + df.at['Model/Authorization/AuthorizationList.cpp', 'Module'] = MODULE_LOCALAUTH + df.at['Model/Authorization/AuthorizationService.cpp', 'v16'] = TICK + df.at['Model/Authorization/AuthorizationService.cpp', 'Module'] = MODULE_LOCALAUTH + if 'Model/Authorization/IdToken.cpp' in df.index: + df.at['Model/Authorization/IdToken.cpp', 'v201'] = TICK + df.at['Model/Authorization/IdToken.cpp', 'Module'] = MODULE_AUTHORIZATION + if 'Model/Availability/AvailabilityService.cpp' in df.index: + df.at['Model/Availability/AvailabilityService.cpp', 'v16'] = TICK + df.at['Model/Availability/AvailabilityService.cpp', 'v201'] = TICK + df.at['Model/Availability/AvailabilityService.cpp', 'Module'] = MODULE_AVAILABILITY + df.at['Model/Boot/BootService.cpp', 'v16'] = TICK + df.at['Model/Boot/BootService.cpp', 'v201'] = TICK + df.at['Model/Boot/BootService.cpp', 'Module'] = MODULE_PROVISIONING + df.at['Model/Certificates/Certificate.cpp', 'v16'] = TICK + df.at['Model/Certificates/Certificate.cpp', 'v201'] = TICK + df.at['Model/Certificates/Certificate.cpp', 'Module'] = MODULE_CERTS + df.at['Model/Certificates/CertificateMbedTLS.cpp', 'v16'] = TICK + df.at['Model/Certificates/CertificateMbedTLS.cpp', 'v201'] = TICK + df.at['Model/Certificates/CertificateMbedTLS.cpp', 'Module'] = MODULE_CERTS + if 'Model/Certificates/Certificate_c.cpp' in df.index: + df.at['Model/Certificates/Certificate_c.cpp', 'v16'] = TICK + df.at['Model/Certificates/Certificate_c.cpp', 'v201'] = TICK + df.at['Model/Certificates/Certificate_c.cpp', 'Module'] = MODULE_CERTS + df.at['Model/Certificates/CertificateService.cpp', 'v16'] = TICK + df.at['Model/Certificates/CertificateService.cpp', 'v201'] = TICK + df.at['Model/Certificates/CertificateService.cpp', 'Module'] = MODULE_CERTS + df.at['Model/ConnectorBase/Connector.cpp', 'v16'] = TICK + df.at['Model/ConnectorBase/Connector.cpp', 'Module'] = MODULE_CORE + df.at['Model/ConnectorBase/ConnectorsCommon.cpp', 'v16'] = TICK + df.at['Model/ConnectorBase/ConnectorsCommon.cpp', 'Module'] = MODULE_CORE + df.at['Model/Diagnostics/DiagnosticsService.cpp', 'v16'] = TICK + df.at['Model/Diagnostics/DiagnosticsService.cpp', 'Module'] = MODULE_FW_MNGT + df.at['Model/FirmwareManagement/FirmwareService.cpp', 'v16'] = TICK + df.at['Model/FirmwareManagement/FirmwareService.cpp', 'Module'] = MODULE_FW_MNGT + df.at['Model/Heartbeat/HeartbeatService.cpp', 'v16'] = TICK + df.at['Model/Heartbeat/HeartbeatService.cpp', 'v201'] = TICK + df.at['Model/Heartbeat/HeartbeatService.cpp', 'Module'] = MODULE_AVAILABILITY + df.at['Model/Metering/MeteringConnector.cpp', 'v16'] = TICK + df.at['Model/Metering/MeteringConnector.cpp', 'Module'] = MODULE_METERVALUES + df.at['Model/Metering/MeteringService.cpp', 'v16'] = TICK + df.at['Model/Metering/MeteringService.cpp', 'Module'] = MODULE_METERVALUES + df.at['Model/Metering/MeterStore.cpp', 'v16'] = TICK + df.at['Model/Metering/MeterStore.cpp', 'Module'] = MODULE_METERVALUES + df.at['Model/Metering/MeterValue.cpp', 'v16'] = TICK + df.at['Model/Metering/MeterValue.cpp', 'Module'] = MODULE_METERVALUES + if 'Model/Metering/MeterValuesV201.cpp' in df.index: + df.at['Model/Metering/MeterValuesV201.cpp', 'v201'] = TICK + df.at['Model/Metering/MeterValuesV201.cpp', 'Module'] = MODULE_METERVALUES + if 'Model/Metering/ReadingContext.cpp' in df.index: + df.at['Model/Metering/ReadingContext.cpp', 'v201'] = TICK + df.at['Model/Metering/ReadingContext.cpp', 'Module'] = MODULE_METERVALUES + df.at['Model/Metering/SampledValue.cpp', 'v16'] = TICK + df.at['Model/Metering/SampledValue.cpp', 'Module'] = MODULE_METERVALUES + if 'Model/RemoteControl/RemoteControlService.cpp' in df.index: + df.at['Model/RemoteControl/RemoteControlService.cpp', 'v201'] = TICK + df.at['Model/RemoteControl/RemoteControlService.cpp', 'Module'] = MODULE_REMOTECONTROL + df.at['Model/Model.cpp', 'v16'] = TICK + df.at['Model/Model.cpp', 'v201'] = TICK + df.at['Model/Model.cpp', 'Module'] = MODULE_GENERAL + df.at['Model/Reservation/Reservation.cpp', 'v16'] = TICK + df.at['Model/Reservation/Reservation.cpp', 'Module'] = MODULE_RESERVATION + df.at['Model/Reservation/ReservationService.cpp', 'v16'] = TICK + df.at['Model/Reservation/ReservationService.cpp', 'Module'] = MODULE_RESERVATION + df.at['Model/Reset/ResetService.cpp', 'v16'] = TICK + df.at['Model/Reset/ResetService.cpp', 'v201'] = TICK + df.at['Model/Reset/ResetService.cpp', 'Module'] = MODULE_PROVISIONING + df.at['Model/SmartCharging/SmartChargingModel.cpp', 'v16'] = TICK + df.at['Model/SmartCharging/SmartChargingModel.cpp', 'Module'] = MODULE_SMARTCHARGING + df.at['Model/SmartCharging/SmartChargingService.cpp', 'v16'] = TICK + df.at['Model/SmartCharging/SmartChargingService.cpp', 'Module'] = MODULE_SMARTCHARGING + df.at['Model/Transactions/Transaction.cpp', 'v16'] = TICK + df.at['Model/Transactions/Transaction.cpp', 'v201'] = TICK + df.at['Model/Transactions/Transaction.cpp', 'Module'] = MODULE_TX + df.at['Model/Transactions/TransactionDeserialize.cpp', 'v16'] = TICK + df.at['Model/Transactions/TransactionDeserialize.cpp', 'Module'] = MODULE_TX + if 'Model/Transactions/TransactionService.cpp' in df.index: + df.at['Model/Transactions/TransactionService.cpp', 'v201'] = TICK + df.at['Model/Transactions/TransactionService.cpp', 'Module'] = MODULE_TX + df.at['Model/Transactions/TransactionStore.cpp', 'v16'] = TICK + df.at['Model/Transactions/TransactionStore.cpp', 'Module'] = MODULE_TX + if 'Model/Variables/Variable.cpp' in df.index: + df.at['Model/Variables/Variable.cpp', 'v201'] = TICK + df.at['Model/Variables/Variable.cpp', 'Module'] = MODULE_PROVISIONING_VARS + if 'Model/Variables/VariableContainer.cpp' in df.index: + df.at['Model/Variables/VariableContainer.cpp', 'v201'] = TICK + df.at['Model/Variables/VariableContainer.cpp', 'Module'] = MODULE_PROVISIONING_VARS + if 'Model/Variables/VariableService.cpp' in df.index: + df.at['Model/Variables/VariableService.cpp', 'v201'] = TICK + df.at['Model/Variables/VariableService.cpp', 'Module'] = MODULE_PROVISIONING_VARS + df.at['Operations/Authorize.cpp', 'v16'] = TICK + df.at['Operations/Authorize.cpp', 'v201'] = TICK + df.at['Operations/Authorize.cpp', 'Module'] = MODULE_AUTHORIZATION + df.at['Operations/BootNotification.cpp', 'v16'] = TICK + df.at['Operations/BootNotification.cpp', 'v201'] = TICK + df.at['Operations/BootNotification.cpp', 'Module'] = MODULE_PROVISIONING + df.at['Operations/CancelReservation.cpp', 'v16'] = TICK + df.at['Operations/CancelReservation.cpp', 'Module'] = MODULE_RESERVATION + df.at['Operations/ChangeAvailability.cpp', 'v16'] = TICK + df.at['Operations/ChangeAvailability.cpp', 'Module'] = MODULE_AVAILABILITY + df.at['Operations/ChangeConfiguration.cpp', 'v16'] = TICK + df.at['Operations/ChangeConfiguration.cpp', 'Module'] = MODULE_CONFIGURATION + df.at['Operations/ClearCache.cpp', 'v16'] = TICK + df.at['Operations/ClearCache.cpp', 'Module'] = MODULE_CORE + df.at['Operations/ClearChargingProfile.cpp', 'v16'] = TICK + df.at['Operations/ClearChargingProfile.cpp', 'Module'] = MODULE_SMARTCHARGING + df.at['Operations/CustomOperation.cpp', 'v16'] = TICK + df.at['Operations/CustomOperation.cpp', 'v201'] = TICK + df.at['Operations/CustomOperation.cpp', 'Module'] = MODULE_RPC + df.at['Operations/DataTransfer.cpp', 'v16'] = TICK + df.at['Operations/DataTransfer.cpp', 'Module'] = MODULE_CORE + df.at['Operations/DeleteCertificate.cpp', 'v16'] = TICK + df.at['Operations/DeleteCertificate.cpp', 'v201'] = TICK + df.at['Operations/DeleteCertificate.cpp', 'Module'] = MODULE_CERTS + df.at['Operations/DiagnosticsStatusNotification.cpp', 'v16'] = TICK + df.at['Operations/DiagnosticsStatusNotification.cpp', 'Module'] = MODULE_FW_MNGT + df.at['Operations/FirmwareStatusNotification.cpp', 'v16'] = TICK + df.at['Operations/FirmwareStatusNotification.cpp', 'Module'] = MODULE_FW_MNGT + if 'Operations/GetBaseReport.cpp' in df.index: + df.at['Operations/GetBaseReport.cpp', 'v201'] = TICK + df.at['Operations/GetBaseReport.cpp', 'Module'] = MODULE_PROVISIONING_VARS + df.at['Operations/GetCompositeSchedule.cpp', 'v16'] = TICK + df.at['Operations/GetCompositeSchedule.cpp', 'Module'] = MODULE_SMARTCHARGING + df.at['Operations/GetConfiguration.cpp', 'v16'] = TICK + df.at['Operations/GetConfiguration.cpp', 'v201'] = TICK + df.at['Operations/GetConfiguration.cpp', 'Module'] = MODULE_CONFIGURATION + df.at['Operations/GetDiagnostics.cpp', 'v16'] = TICK + df.at['Operations/GetDiagnostics.cpp', 'Module'] = MODULE_FW_MNGT + df.at['Operations/GetInstalledCertificateIds.cpp', 'v16'] = TICK + df.at['Operations/GetInstalledCertificateIds.cpp', 'Module'] = MODULE_SMARTCHARGING + df.at['Operations/GetLocalListVersion.cpp', 'v16'] = TICK + df.at['Operations/GetLocalListVersion.cpp', 'Module'] = MODULE_LOCALAUTH + if 'Operations/GetVariables.cpp' in df.index: + df.at['Operations/GetVariables.cpp', 'v201'] = TICK + df.at['Operations/GetVariables.cpp', 'Module'] = MODULE_PROVISIONING_VARS + df.at['Operations/Heartbeat.cpp', 'v16'] = TICK + df.at['Operations/Heartbeat.cpp', 'v201'] = TICK + df.at['Operations/Heartbeat.cpp', 'Module'] = MODULE_AVAILABILITY + df.at['Operations/InstallCertificate.cpp', 'v16'] = TICK + df.at['Operations/InstallCertificate.cpp', 'v201'] = TICK + df.at['Operations/InstallCertificate.cpp', 'Module'] = MODULE_CERTS + df.at['Operations/MeterValues.cpp', 'v16'] = TICK + df.at['Operations/MeterValues.cpp', 'Module'] = MODULE_METERVALUES + if 'Operations/NotifyReport.cpp' in df.index: + df.at['Operations/NotifyReport.cpp', 'v201'] = TICK + df.at['Operations/NotifyReport.cpp', 'Module'] = MODULE_PROVISIONING_VARS + df.at['Operations/RemoteStartTransaction.cpp', 'v16'] = TICK + df.at['Operations/RemoteStartTransaction.cpp', 'Module'] = MODULE_TX + df.at['Operations/RemoteStopTransaction.cpp', 'v16'] = TICK + df.at['Operations/RemoteStopTransaction.cpp', 'Module'] = MODULE_TX + if 'Operations/RequestStartTransaction.cpp' in df.index: + df.at['Operations/RequestStartTransaction.cpp', 'v201'] = TICK + df.at['Operations/RequestStartTransaction.cpp', 'Module'] = MODULE_TX + if 'Operations/RequestStopTransaction.cpp' in df.index: + df.at['Operations/RequestStopTransaction.cpp', 'v201'] = TICK + df.at['Operations/RequestStopTransaction.cpp', 'Module'] = MODULE_TX + df.at['Operations/ReserveNow.cpp', 'v16'] = TICK + df.at['Operations/ReserveNow.cpp', 'Module'] = MODULE_RESERVATION + df.at['Operations/Reset.cpp', 'v16'] = TICK + df.at['Operations/Reset.cpp', 'v201'] = TICK + df.at['Operations/Reset.cpp', 'Module'] = MODULE_PROVISIONING + if 'Operations/SecurityEventNotification.cpp' in df.index: + df.at['Operations/SecurityEventNotification.cpp', 'v201'] = TICK + df.at['Operations/SecurityEventNotification.cpp', 'Module'] = MODULE_SECURITY + df.at['Operations/SendLocalList.cpp', 'v16'] = TICK + df.at['Operations/SendLocalList.cpp', 'Module'] = MODULE_LOCALAUTH + df.at['Operations/SetChargingProfile.cpp', 'v16'] = TICK + df.at['Operations/SetChargingProfile.cpp', 'Module'] = MODULE_SMARTCHARGING + if 'Operations/SetVariables.cpp' in df.index: + df.at['Operations/SetVariables.cpp', 'v201'] = TICK + df.at['Operations/SetVariables.cpp', 'Module'] = MODULE_PROVISIONING_VARS + df.at['Operations/StartTransaction.cpp', 'v16'] = TICK + df.at['Operations/StartTransaction.cpp', 'Module'] = MODULE_TX + df.at['Operations/StatusNotification.cpp', 'v16'] = TICK + df.at['Operations/StatusNotification.cpp', 'v201'] = TICK + df.at['Operations/StatusNotification.cpp', 'Module'] = MODULE_AVAILABILITY + df.at['Operations/StopTransaction.cpp', 'v16'] = TICK + df.at['Operations/StopTransaction.cpp', 'Module'] = MODULE_TX + if 'Operations/TransactionEvent.cpp' in df.index: + df.at['Operations/TransactionEvent.cpp', 'v201'] = TICK + df.at['Operations/TransactionEvent.cpp', 'Module'] = MODULE_TX + df.at['Operations/TriggerMessage.cpp', 'v16'] = TICK + df.at['Operations/TriggerMessage.cpp', 'Module'] = MODULE_TRIGGERMESSAGE + df.at['Operations/UnlockConnector.cpp', 'v16'] = TICK + df.at['Operations/UnlockConnector.cpp', 'Module'] = MODULE_CORE + df.at['Operations/UpdateFirmware.cpp', 'v16'] = TICK + df.at['Operations/UpdateFirmware.cpp', 'Module'] = MODULE_FW_MNGT + if 'MicroOcpp_c.cpp' in df.index: + df.at['MicroOcpp_c.cpp', 'v16'] = TICK + df.at['MicroOcpp_c.cpp', 'v201'] = TICK + df.at['MicroOcpp_c.cpp', 'Module'] = MODULE_API + + print(df) + +categorize_table(cunits_v16) +categorize_table(cunits_v201) + +categorize_success = True + +if cunits_v16[COLUMN_BINSIZE].isnull().any(): + print('Error: categorized the following compilation units erroneously (v16):\n') + print(cunits_v16.loc[cunits_v16[COLUMN_BINSIZE].isnull()]) + categorize_success = False + +if cunits_v201[COLUMN_BINSIZE].isnull().any(): + print('Error: categorized the following compilation units erroneously (v201):\n') + print(cunits_v201.loc[cunits_v201[COLUMN_BINSIZE].isnull()]) + categorize_success = False + +if (cunits_v16['Module'].values == '').sum() > 0: + print('Error: did not categorize the following compilation units (v16):\n') + print(cunits_v16.loc[cunits_v16['Module'].values == '']) + categorize_success = False + +if (cunits_v201['Module'].values == '').sum() > 0: + print('Error: did not categorize the following compilation units (v201):\n') + print(cunits_v201.loc[cunits_v201['Module'].values == '']) + categorize_success = False + +if not categorize_success: + sys.exit('\nError categorizing compilation units') + +# store csv with all details + +print('Uncategorized compile units (v16): ', (cunits_v16['Module'].values == '').sum()) +print('Uncategorized compile units (v201): ', (cunits_v201['Module'].values == '').sum()) + +cunits_v16.to_csv("docs/assets/tables/compile_units_v16.csv") +cunits_v201.to_csv("docs/assets/tables/compile_units_v201.csv") + +# store csv with size by Module for v16 + +modules_v16 = cunits_v16.loc[cunits_v16['v16'].values == 'x'].sort_index() +modules_v16_by_module = modules_v16[['Module', COLUMN_BINSIZE]].groupby('Module').sum() +modules_v16_by_module.loc['**Total**'] = [modules_v16_by_module[COLUMN_BINSIZE].sum()] + +print(modules_v16_by_module) + +modules_v16_by_module.to_csv('docs/assets/tables/modules_v16.csv') + +# store csv with size by Module for v201 + +modules_v201 = cunits_v201.loc[cunits_v201['v201'].values == 'x'].sort_index() +modules_v201_by_module = modules_v201[['Module', COLUMN_BINSIZE]].groupby('Module').sum() +modules_v201_by_module.loc['**Total**'] = [modules_v201_by_module[COLUMN_BINSIZE].sum()] + +print(modules_v201_by_module) + +modules_v201_by_module.to_csv('docs/assets/tables/modules_v201.csv') diff --git a/tests/benchmarks/scripts/measure_heap.py b/tests/benchmarks/scripts/measure_heap.py new file mode 100644 index 00000000..0b050544 --- /dev/null +++ b/tests/benchmarks/scripts/measure_heap.py @@ -0,0 +1,391 @@ +import os +import sys +import requests +import paramiko +import base64 +import traceback +import io +import json +import time +import pandas as pd + + +requests.packages.urllib3.disable_warnings() # avoid the URL to be printed to console + +# Test case selection (commented out a few to simplify testing for now) +testcase_name_list = [ + 'TC_B_06_CS', + 'TC_B_07_CS', + 'TC_B_09_CS', + 'TC_B_10_CS', + 'TC_B_11_CS', + 'TC_B_12_CS', + 'TC_B_13_CS', + 'TC_B_32_CS', + 'TC_B_34_CS', + 'TC_B_35_CS', + 'TC_B_36_CS', + 'TC_B_37_CS', + 'TC_B_39_CS', + 'TC_C_02_CS', + 'TC_C_04_CS', + 'TC_C_06_CS', + 'TC_C_07_CS', + 'TC_C_49_CS', + 'TC_E_01_CS', + 'TC_E_02_CS', + 'TC_E_03_CS', + 'TC_E_04_CS', + 'TC_E_05_CS', + 'TC_E_06_CS', + 'TC_E_07_CS', + 'TC_E_09_CS', + 'TC_E_13_CS', + 'TC_E_15_CS', + 'TC_E_17_CS', + 'TC_E_20_CS', + 'TC_E_21_CS', + 'TC_E_24_CS', + 'TC_E_25_CS', + 'TC_E_28_CS', + 'TC_E_29_CS', + 'TC_E_30_CS', + 'TC_E_31_CS', + 'TC_E_32_CS', + 'TC_E_33_CS', + 'TC_E_34_CS', + 'TC_E_35_CS', + 'TC_E_39_CS', + 'TC_F_01_CS', + 'TC_F_02_CS', + 'TC_F_03_CS', + 'TC_F_04_CS', + 'TC_F_05_CS', + 'TC_F_06_CS', + 'TC_F_07_CS', + 'TC_F_08_CS', + 'TC_F_09_CS', + 'TC_F_10_CS', + 'TC_F_11_CS', + 'TC_F_12_CS', + 'TC_F_13_CS', + 'TC_F_14_CS', + 'TC_F_20_CS', + 'TC_F_23_CS', + 'TC_F_24_CS', + 'TC_F_26_CS', + 'TC_F_27_CS', + 'TC_G_01_CS', + 'TC_G_02_CS', + 'TC_G_03_CS', + 'TC_G_04_CS', + 'TC_G_05_CS', + 'TC_G_06_CS', + 'TC_G_07_CS', + 'TC_G_08_CS', + 'TC_G_09_CS', + 'TC_G_10_CS', + 'TC_G_11_CS', + 'TC_G_12_CS', + 'TC_G_13_CS', + 'TC_G_14_CS', + 'TC_G_15_CS', + 'TC_G_16_CS', + 'TC_G_17_CS', + 'TC_J_07_CS', + 'TC_J_08_CS', + 'TC_J_09_CS', + 'TC_J_10_CS', +] + +# Result data set +df = pd.DataFrame(columns=['FN_BLOCK', 'Testcase', 'Pass', 'Heap usage (Bytes)']) +df.index.names = ['TC_ID'] + +max_memory_total = 0 +min_memory_base = 1000 * 1000 * 1000 + +def connect_ssh(): + + if not os.path.isfile(os.path.join('tests', 'benchmarks', 'scripts', 'id_ed25519')): + file = open(os.path.join('tests', 'benchmarks', 'scripts', 'id_ed25519'), 'w') + file.write(os.environ['SSH_LOCAL_PRIV']) + file.close() + print('SSH ID written to file') + + client = paramiko.SSHClient() + client.get_host_keys().add('cicd.micro-ocpp.com', 'ssh-ed25519', paramiko.pkey.PKey.from_type_string('ssh-ed25519', base64.b64decode(os.environ['SSH_HOST_PUB']))) + client.connect('cicd.micro-ocpp.com', username='ocpp', key_filename=os.path.join('tests', 'benchmarks', 'scripts', 'id_ed25519'), look_for_keys=False) + return client + +def close_ssh(client: paramiko.SSHClient): + + client.close() + +def deploy_simulator(): + + print('Deploy Simulator') + + client = connect_ssh() + + print(' - stop Simulator, if still running') + stdin, stdout, stderr = client.exec_command('killall -s SIGINT mo_simulator') + + print(' - clean previous deployment') + stdin, stdout, stderr = client.exec_command('rm -rf ' + os.path.join('MicroOcppSimulator')) + + print(' - init folder structure') + sftp = client.open_sftp() + sftp.mkdir(os.path.join('MicroOcppSimulator')) + sftp.mkdir(os.path.join('MicroOcppSimulator', 'build')) + sftp.mkdir(os.path.join('MicroOcppSimulator', 'public')) + sftp.mkdir(os.path.join('MicroOcppSimulator', 'mo_store')) + + print(' - upload files') + sftp.put( os.path.join('MicroOcppSimulator', 'build', 'mo_simulator'), + os.path.join('MicroOcppSimulator', 'build', 'mo_simulator')) + sftp.chmod(os.path.join('MicroOcppSimulator', 'build', 'mo_simulator'), 0O777) + sftp.put( os.path.join('MicroOcppSimulator', 'public', 'bundle.html.gz'), + os.path.join('MicroOcppSimulator', 'public', 'bundle.html.gz')) + sftp.close() + close_ssh(client) + print(' - done') + +def cleanup_simulator(): + + print('Clean up Simulator') + + client = connect_ssh() + + print(' - stop Simulator, if still running') + stdin, stdout, stderr = client.exec_command('killall -s SIGINT mo_simulator') + + print(' - clean deployment') + stdin, stdout, stderr = client.exec_command('rm -rf ' + os.path.join('MicroOcppSimulator')) + + close_ssh(client) + print(' - done') + +def setup_simulator(): + + print('Setup Simulator') + + client = connect_ssh() + + print(' - stop Simulator, if still running') + stdin, stdout, stderr = client.exec_command('killall -s SIGINT mo_simulator') + + print(' - clean state') + stdin, stdout, stderr = client.exec_command('rm -rf ' + os.path.join('MicroOcppSimulator', 'mo_store', '*')) + + print(' - upload credentials') + sftp = client.open_sftp() + sftp.putfo(io.StringIO(os.environ['MO_SIM_CONFIG']), os.path.join('MicroOcppSimulator', 'mo_store', 'simulator.jsn')) + sftp.putfo(io.StringIO(os.environ['MO_SIM_OCPP_SERVER']),os.path.join('MicroOcppSimulator', 'mo_store', 'ws-conn-v201.jsn')) + sftp.putfo(io.StringIO(os.environ['MO_SIM_API_CERT']), os.path.join('MicroOcppSimulator', 'mo_store', 'api_cert.pem')) + sftp.putfo(io.StringIO(os.environ['MO_SIM_API_KEY']), os.path.join('MicroOcppSimulator', 'mo_store', 'api_key.pem')) + sftp.putfo(io.StringIO(os.environ['MO_SIM_API_CONFIG']), os.path.join('MicroOcppSimulator', 'mo_store', 'api.jsn')) + sftp.close() + + print(' - start Simulator') + + stdin, stdout, stderr = client.exec_command('mkdir -p logs && cd ' + os.path.join('MicroOcppSimulator') + ' && ./build/mo_simulator > ~/logs/sim_"$(date +%Y-%m-%d_%H-%M-%S.log)"') + close_ssh(client) + + print(' - done') + +def run_measurements(): + + global max_memory_total + global min_memory_base + + print("Fetch TCs from Test Driver") + + response = requests.get(os.environ['TEST_DRIVER_URL'] + '/ocpp2.0.1/CS/testcases/' + os.environ['TEST_DRIVER_CONFIG'], + headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}, + verify=False) + + #print(json.dumps(response.json(), indent=4)) + + testcases = [] + + for i in response.json()['data']['testcasesData']: + for j in i['data']: + is_core = False + for k in j['certification_profiles']: + if k == 'Core': + is_core = True + break + + select = False + for k in testcase_name_list: + if j['testcase_name'] == k: + select = True + break + + if select: + print(i['header'] + ' --- ' + j['functional_block'] + ' --- ' + j['description']) + testcases.append(j) + + deploy_simulator() + + print('Get Simulator base memory data') + setup_simulator() + + response = requests.post('https://cicd.micro-ocpp.com:8443/api/memory/reset', + auth=(json.loads(os.environ['MO_SIM_API_CONFIG'])['user'], + json.loads(os.environ['MO_SIM_API_CONFIG'])['pass'])) + print(f'Simulator API /memory/reset:\n > {response.status_code}') + + response = requests.get('https://cicd.micro-ocpp.com:8443/api/memory/info', + auth=(json.loads(os.environ['MO_SIM_API_CONFIG'])['user'], + json.loads(os.environ['MO_SIM_API_CONFIG'])['pass'])) + print(f'Simulator API /memory/info:\n > {response.status_code}, current heap={response.json()["total_current"]}, max heap={response.json()["total_max"]}') + base_memory_level = response.json()["total_max"] + min_memory_base = min(min_memory_base, response.json()["total_max"]) + + print("Start Test Driver") + + response = requests.post(os.environ['TEST_DRIVER_URL'] + '/ocpp2.0.1/CS/session/start/' + os.environ['TEST_DRIVER_CONFIG'], + headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}, + verify=False) + print(f'Test Driver /*/*/session/start/*:\n > {response.status_code}') + #print(json.dumps(response.json(), indent=4)) + + for testcase in testcases: + print('\nRun ' + testcase['functional_block'] + ' > ' + testcase['description'] + ' (' + testcase['testcase_name'] + ')') + + if testcase['testcase_name'] in df.index: + print('Test case already executed - skip') + continue + + setup_simulator() + time.sleep(1) + + # Check connection + simulator_connected = False + for i in range(5): + response = requests.get(os.environ['TEST_DRIVER_URL'] + '/sut_connection_status', + headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}, + verify=False) + print(f'Test Driver /sut_connection_status:\n > {response.status_code}') + #print(json.dumps(response.json(), indent=4)) + if response.status_code == 200: + simulator_connected = True + break + else: + print(f'Waiting for the Simulator to connect ({i}) ...') + time.sleep(3) + + if not simulator_connected: + print('Simulator could not connect to Test Driver') + raise Exception() + + response = requests.post('https://cicd.micro-ocpp.com:8443/api/memory/reset', + auth=(json.loads(os.environ['MO_SIM_API_CONFIG'])['user'], + json.loads(os.environ['MO_SIM_API_CONFIG'])['pass'])) + print(f'Simulator API /memory/reset:\n > {response.status_code}') + + test_response = requests.post(os.environ['TEST_DRIVER_URL'] + '/testcases/' + testcase['testcase_name'] + '/execute', + headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}, + verify=False) + print(f'Test Driver /testcases/{testcase["testcase_name"]}/execute:\n > {test_response.status_code}') + #try: + # print(json.dumps(test_response.json(), indent=4)) + #except: + # print(' > No JSON') + + sim_response = requests.get('https://cicd.micro-ocpp.com:8443/api/memory/info', + auth=(json.loads(os.environ['MO_SIM_API_CONFIG'])['user'], + json.loads(os.environ['MO_SIM_API_CONFIG'])['pass'])) + print(f'Simulator API /memory/info:\n > {sim_response.status_code}, current heap={sim_response.json()["total_current"]}, max heap={sim_response.json()["total_max"]}') + + df.loc[testcase['testcase_name']] = [testcase['functional_block'], testcase['description'], 'x' if test_response.status_code == 200 and test_response.json()['data'][0]['verdict'] == "pass" else '-', str(sim_response.json()["total_max"] - min(base_memory_level, sim_response.json()["total_current"]))] + + max_memory_total = max(max_memory_total, sim_response.json()["total_max"]) + min_memory_base = min(min_memory_base, sim_response.json()["total_current"]) + + print("Stop Test Driver") + + response = requests.post(os.environ['TEST_DRIVER_URL'] + '/session/stop', + headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}, + verify=False) + print(f'Test Driver /session/stop:\n > {response.status_code}') + #print(json.dumps(response.json(), indent=4)) + + cleanup_simulator() + + print('Store test results') + + # Add some meta information + max_memory = 0 + for index, row in df.iterrows(): + memory = row['Heap usage (Bytes)'] + if memory.isdigit(): + max_memory = max(max_memory, int(memory)) + + functional_blocks = set() + for index, row in df.iterrows(): + functional_blocks.add(row['FN_BLOCK']) + + print(functional_blocks) + + for i in functional_blocks: + df.loc[f'TC_{i[0]}'] = [i, f'**{i}**', ' ', ' '] + + df.loc['|MO_SIM_000'] = ['-', '**Simulator stats**', ' ', ' '] + df.loc['|MO_SIM_010'] = ['-', 'Base memory occupation', ' ', str(min_memory_base)] + df.loc['|MO_SIM_020'] = ['-', 'Test case maximum', ' ', str(max_memory)] + df.loc['|MO_SIM_030'] = ['-', 'Total memory maximum', ' ', str(max_memory_total)] + + df.sort_index(inplace=True) + + print(df) + + df.to_csv('docs/assets/tables/heap_v201.csv',index=False,columns=['Testcase','Pass','Heap usage (Bytes)']) + + print('Stored test results to CSV') + +def run_measurements_and_retry(): + + if ( 'TEST_DRIVER_URL' not in os.environ or + 'TEST_DRIVER_CONFIG' not in os.environ or + 'TEST_DRIVER_KEY' not in os.environ or + 'MO_SIM_CONFIG' not in os.environ or + 'MO_SIM_OCPP_SERVER' not in os.environ or + 'MO_SIM_API_CERT' not in os.environ or + 'MO_SIM_API_KEY' not in os.environ or + 'MO_SIM_API_CONFIG' not in os.environ or + 'SSH_LOCAL_PRIV' not in os.environ or + 'SSH_HOST_PUB' not in os.environ): + sys.exit('\nCould not read environment variables') + + n_tries = 3 + + for i in range(n_tries): + + try: + run_measurements() + print('\n **Test cases executed successfully**') + break + except: + print(f'Error detected ({i+1})') + + traceback.print_exc() + + print("Stop Test Driver") + response = requests.post(os.environ['TEST_DRIVER_URL'] + '/session/stop', + headers={'Authorization': 'Bearer ' + os.environ['TEST_DRIVER_KEY']}, + verify=False) + print(f'Test Driver /session/stop:\n > {response.status_code}') + #print(json.dumps(response.json(), indent=4)) + + cleanup_simulator() + + if i + 1 < n_tries: + print('Retry test cases') + else: + print('\n **Test case execution aborted**') + sys.exit('\nError running test cases') + +run_measurements_and_retry() diff --git a/tests/helpers/testHelper.cpp b/tests/helpers/testHelper.cpp index a6aec2a4..ffc6fac5 100644 --- a/tests/helpers/testHelper.cpp +++ b/tests/helpers/testHelper.cpp @@ -1,12 +1,15 @@ -#include +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include + #include +#include using namespace MicroOcpp; -void cpp_console_out(const char *msg) { - std::cout << msg; -} - unsigned long mtime = 10000; unsigned long custom_timer_cb() { return mtime; @@ -18,3 +21,15 @@ void loop() { mocpp_loop(); } } + +class TestRunListener : public Catch::TestEventListenerBase { +public: + using Catch::TestEventListenerBase::TestEventListenerBase; + + void testRunEnded( Catch::TestRunStats const& testRunStats ) override { + MO_MEM_PRINT_STATS(); + MO_MEM_DEINIT(); + } +}; + +CATCH_REGISTER_LISTENER(TestRunListener) diff --git a/tests/helpers/testHelper.h b/tests/helpers/testHelper.h index 4d9fd640..04e35d57 100644 --- a/tests/helpers/testHelper.h +++ b/tests/helpers/testHelper.h @@ -1,12 +1,11 @@ -#ifndef CPP_CONSOLE_OUT -#define CPP_CONSOLE_OUT +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License -/** -* Prints a string to the c standart console -* -* @param msg pointer to the string -*/ -void cpp_console_out(const char *msg); +#ifndef MO_TESTHELPER_H +#define MO_TESTHELPER_H + +#define UNIT_MEM_TAG "UnitTests" extern unsigned long mtime; unsigned long custom_timer_cb(); diff --git a/tests/ocppEngineLifecycle.cpp b/tests/ocppEngineLifecycle.cpp index df39f282..b3de2a18 100644 --- a/tests/ocppEngineLifecycle.cpp +++ b/tests/ocppEngineLifecycle.cpp @@ -1,14 +1,15 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + #include #include -#include "./catch2/catch.hpp" +#include #include "./helpers/testHelper.h" TEST_CASE( "Context lifecycle" ) { printf("\nRun %s\n", "Context lifecycle"); - //set console output to the cpp console to display outputs - //mocpp_set_console_out(cpp_console_out); - //initialize Context with dummy socket MicroOcpp::LoopbackConnection loopback; mocpp_initialize(loopback);