diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 00000000..9d23daeb --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,214 @@ +# matth-x/MicroOcpp +# Copyright Matthias Akstaller 2019 - 2024 +# MIT License + +name: Documentation +on: + push: + branches: + - main + pull_request: + +permissions: + contents: write + +jobs: + build_simulator: + name: Build Simulator + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - uses: actions/cache@v4 + with: + key: ${{ github.ref }} + path: .cache + - name: Get build tools + run: | + sudo apt update + sudo apt install cmake libssl-dev build-essential + - name: Checkout Simulator + uses: actions/checkout@v3 + with: + repository: matth-x/MicroOcppSimulator + path: MicroOcppSimulator + ref: 2cb07cdbe53954a694a29336ab31eac2d2b48673 + submodules: 'recursive' + - name: Clean MicroOcpp submodule + run: | + rm -rf MicroOcppSimulator/lib/MicroOcpp + - name: Checkout MicroOcpp submodule + uses: actions/checkout@v3 + with: + path: MicroOcppSimulator/lib/MicroOcpp + - name: Generate CMake files + run: cmake -S ./MicroOcppSimulator -B ./MicroOcppSimulator/build -DCMAKE_CXX_FLAGS="-DMO_OVERRIDE_ALLOCATION=1 -DMO_ENABLE_HEAP_PROFILER=1" + - name: Compile + run: cmake --build ./MicroOcppSimulator/build -j 32 --target mo_simulator + - name: Upload Simulator executable + uses: actions/upload-artifact@v4 + with: + name: Simulator executable + path: | + MicroOcppSimulator/build/mo_simulator + MicroOcppSimulator/public/bundle.html.gz + if-no-files-found: error + retention-days: 1 + + measure_heap: + needs: build_simulator + name: Heap measurements + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Install Python dependencies + run: pip install requests paramiko pandas + - name: Get Simulator + uses: actions/download-artifact@v4 + with: + name: Simulator executable + path: MicroOcppSimulator + - name: Measure heap and create reports + run: | + mkdir -p docs/assets/tables + python tests/benchmarks/scripts/measure_heap.py + env: + TEST_DRIVER_URL: ${{ secrets.TEST_DRIVER_URL }} + TEST_DRIVER_CONFIG: ${{ secrets.TEST_DRIVER_CONFIG }} + TEST_DRIVER_KEY: ${{ secrets.TEST_DRIVER_KEY }} + MO_SIM_CONFIG: ${{ secrets.MO_SIM_CONFIG }} + MO_SIM_OCPP_SERVER: ${{ secrets.MO_SIM_OCPP_SERVER }} + MO_SIM_API_CERT: ${{ secrets.MO_SIM_API_CERT }} + MO_SIM_API_KEY: ${{ secrets.MO_SIM_API_KEY }} + MO_SIM_API_CONFIG: ${{ secrets.MO_SIM_API_CONFIG }} + SSH_LOCAL_PRIV: ${{ secrets.SSH_LOCAL_PRIV }} + SSH_HOST_PUB: ${{ secrets.SSH_HOST_PUB }} + - name: Upload reports + uses: actions/upload-artifact@v4 + with: + name: Memory usage reports CSV + path: docs/assets/tables + if-no-files-found: error + + build_firmware_size: + name: Build firmware + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Cache PlatformIO + uses: actions/cache@v4 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + - name: Set up Python + uses: actions/setup-python@v4 + - name: Install PlatformIO + run: | + python -m pip install --upgrade pip + pip install --upgrade platformio + - name: Run PlatformIO + run: pio ci --lib="." --build-dir="${{ github.workspace }}/../build" --keep-build-dir --project-conf="./tests/benchmarks/firmware_size/platformio.ini" ./tests/benchmarks/firmware_size/main.cpp + - name: Move firmware files # change path to location without parent dir ('..') statement (to make upload-artifact happy) + run: | + mkdir firmware + mv "${{ github.workspace }}/../build/.pio/build/v16/firmware.elf" firmware/firmware_v16.elf + mv "${{ github.workspace }}/../build/.pio/build/v201/firmware.elf" firmware/firmware_v201.elf + - name: Upload firmware linker files + uses: actions/upload-artifact@v4 + with: + name: Firmware linker files + path: firmware + if-no-files-found: error + retention-days: 1 + + evaluate_firmware: + needs: build_firmware_size + name: Static firmware analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - uses: actions/cache@v4 + with: + key: ${{ github.ref }} + path: .cache + - name: Install Python dependencies + run: pip install pandas + - name: Get build tools + run: | + sudo apt update + sudo apt install build-essential cmake ninja-build + sudo apt -y install gcc-9 g++-9 + g++ --version + - name: Check out bloaty + uses: actions/checkout@v3 + with: + repository: google/bloaty + ref: e1155149d54bb09b81e86f0e4e5cb7fbd2a318eb + path: tools/bloaty + submodules: recursive + - name: Install bloaty + run: | + cmake -B tools/bloaty/build -G Ninja -S tools/bloaty + cmake --build tools/bloaty/build -j 32 + - name: Get firmware linker files + uses: actions/download-artifact@v4 + with: + name: Firmware linker files + path: firmware + - name: Run bloaty + run: | + mkdir -p docs/assets/tables + tools/bloaty/build/bloaty firmware/firmware_v16.elf -d compileunits --csv -n 0 > docs/assets/tables/bloaty_v16.csv + tools/bloaty/build/bloaty firmware/firmware_v201.elf -d compileunits --csv -n 0 > docs/assets/tables/bloaty_v201.csv + - name: Evaluate and create reports + run: python tests/benchmarks/scripts/eval_firmware_size.py + - name: Upload reports + uses: actions/upload-artifact@v4 + with: + name: Firmware size reports CSV + path: docs/assets/tables + if-no-files-found: error + + deploy: + needs: [evaluate_firmware, measure_heap] + name: Deploy docs + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - uses: actions/cache@v4 + with: + key: ${{ github.ref }} + path: .cache + - name: Install Python dependencies + run: pip install pandas mkdocs-material mkdocs-table-reader-plugin + - name: Get firmware size reports + uses: actions/download-artifact@v4 + with: + name: Firmware size reports CSV + path: docs/assets/tables + - name: Get memory occupation reports + uses: actions/download-artifact@v4 + with: + name: Memory usage reports CSV + path: docs/assets/tables + - name: Run mkdocs + run: mkdocs gh-deploy --force diff --git a/.github/workflows/esp-idf.yml b/.github/workflows/esp-idf.yml new file mode 100644 index 00000000..279cd4a8 --- /dev/null +++ b/.github/workflows/esp-idf.yml @@ -0,0 +1,53 @@ +# matth-x/MicroOcpp +# Copyright Matthias Akstaller 2019 - 2024 +# MIT License + +name: ESP-IDF CI + +on: + push: + branches: + - main + + pull_request: + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout ESP-IDF example folder structure + uses: actions/checkout@v3 + with: + sparse-checkout: examples/ESP-IDF + - name: Clean sumodules folders template + run: rm -r ./examples/ESP-IDF/components/* + - name: Checkout main repo + uses: actions/checkout@v3 + with: + path: examples/ESP-IDF/components/MicroOcpp + - name: Checkout Mongoose + uses: actions/checkout@v3 + with: + repository: cesanta/mongoose-esp-idf + path: examples/ESP-IDF/components/mongoose + submodules: 'recursive' + - name: Checkout Mongoose WS adapter + uses: actions/checkout@v3 + with: + repository: matth-x/MicroOcppMongoose + ref: v1.2.0 + path: examples/ESP-IDF/components/MicroOcppMongoose + - name: Checkout ArduinoJson + uses: actions/checkout@v3 + with: + repository: bblanchon/ArduinoJson + ref: 3e1be980d93e47b2a0073efeeb9a9396fd7a83be + path: examples/ESP-IDF/components/ArduinoJson + - name: esp-idf build + uses: espressif/esp-idf-ci-action@v1 + with: + esp_idf_version: v4.4 + target: esp32 + path: './examples/ESP-IDF' diff --git a/.github/workflows/pio.yml b/.github/workflows/pio.yml index 819bc365..189351fd 100644 --- a/.github/workflows/pio.yml +++ b/.github/workflows/pio.yml @@ -1,9 +1,15 @@ +# matth-x/MicroOcpp +# Copyright Matthias Akstaller 2019 - 2024 +# MIT License + name: PlatformIO CI on: push: branches: - - develop + - main + + pull_request: jobs: build: @@ -14,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 a380452c..58e7b1ee 100644 --- a/.github/workflows/platformless.yml +++ b/.github/workflows/platformless.yml @@ -1,9 +1,15 @@ +# matth-x/MicroOcpp +# Copyright Matthias Akstaller 2019 - 2024 +# MIT License + name: Default Compilation on: push: branches: - - develop + - main + + pull_request: jobs: @@ -23,4 +29,4 @@ jobs: - name: Get ArduinoJson run: wget -Uri https://github.com/bblanchon/ArduinoJson/releases/download/v6.19.4/ArduinoJson-v6.19.4.h -O ./src/ArduinoJson.h - name: Compile - run: g++ -c -std=c++14 -I ./src $(find ./src -type f -iregex ".*\.cpp") -DAO_CUSTOM_WS -DAO_CUSTOM_UPDATER -DAO_CUSTOM_RESET -DAO_USE_FILEAPI=POSIX_FILEAPI -DAO_DBG_LEVEL=AO_DL_DEBUG -DAO_TRAFFIC_OUT -DAO_FILENAME_PREFIX='"./ao_store"' -DAO_PLATFORM=AO_PLATFORM_UNIX -DAO_CUSTOM_TIMER -DAO_DEACTIVATE_FLASH_SMARTCHARGING -Wall + run: g++ -c -std=c++11 -I ./src $(find ./src -type f -iregex ".*\.cpp") -DMO_PLATFORM=MO_PLATFORM_NONE -Wall -Wextra -Wno-unused-parameter -Wno-redundant-move -Werror diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f7ae612a..38b1d04e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,9 +1,15 @@ -name: Test Runner +# matth-x/MicroOcpp +# Copyright Matthias Akstaller 2019 - 2024 +# MIT License + +name: Unit tests on: push: branches: - - develop + - main + + pull_request: jobs: @@ -13,18 +19,43 @@ jobs: steps: - name: Check out repository code uses: actions/checkout@v3 - - name: Get gcc compiler + - name: Check out MbedTLS + uses: actions/checkout@v3 + with: + repository: Mbed-TLS/mbedtls + ref: v2.28.10 + path: lib/mbedtls + - name: Get build tools run: | sudo apt update - sudo apt install build-essential + sudo apt install build-essential cmake lcov valgrind sudo apt -y install gcc-9 g++-9 g++ --version echo "g++ version must be 9.4.0" - name: Get ArduinoJson - run: wget -Uri https://github.com/bblanchon/ArduinoJson/releases/download/v6.19.4/ArduinoJson-v6.19.4.h -O ./src/ArduinoJson.h + run: wget -Uri https://github.com/bblanchon/ArduinoJson/releases/download/v6.21.3/ArduinoJson-v6.21.3.h -O ./src/ArduinoJson.h + - name: Generate CMake build files + run: cmake -S . -B ./build -DMO_BUILD_UNIT_MBEDTLS=True - name: Compile - run: g++ -std=c++14 -I ./src $(find ./src ./tests -type f -iregex ".*\.cpp") -DAO_CUSTOM_WS -DAO_CUSTOM_UPDATER -DAO_CUSTOM_RESET -DAO_USE_FILEAPI=POSIX_FILEAPI -DAO_DBG_LEVEL=AO_DL_DEBUG -DAO_TRAFFIC_OUT -DAO_FILENAME_PREFIX='"./ao_store"' -DAO_PLATFORM=AO_PLATFORM_UNIX -DAO_CUSTOM_TIMER -DAO_DEACTIVATE_FLASH_SMARTCHARGING -o ./output -Wall + run: cmake --build ./build -j 32 --target mo_unit_tests - name: Configure FS - run: mkdir ao_store - - name: Run tests - run: ./output + run: mkdir mo_store + - name: Run tests (valgrind) + run: valgrind --error-exitcode=1 --leak-check=full ./build/mo_unit_tests --abort + - name: Generate CMake build files (AddressSanitizer, UndefinedBehaviorSanitizer) + run: | + rm -r ./build + cmake -S . -B ./build -DCMAKE_CXX_FLAGS="-fsanitize=address -fsanitize=undefined" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address -fsanitize=undefined" -DMO_BUILD_UNIT_MBEDTLS=True + - name: Compile (ASan, UBSan) + run: cmake --build ./build -j 32 --target mo_unit_tests + - name: Run tests (ASan, UBSan) + run: ./build/mo_unit_tests --abort + - name: Create coverage report + run: | + lcov --directory . --capture --output-file coverage.info --ignore-errors mismatch + lcov --remove coverage.info '/usr/*' '*/tests/*' '*/ArduinoJson.h' --output-file coverage.info + lcov --list coverage.info + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 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 new file mode 100644 index 00000000..3b7759aa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,201 @@ +# Changelog + +## Unreleased + +### Changed + +- Change `MicroOcpp::TxNotification` into C-style enum, replace `OCPP_TxNotication` ([#386](https://github.com/matth-x/MicroOcpp/pull/386)) +- Improved UUID generation ([#383](https://github.com/matth-x/MicroOcpp/pull/383)) +- `beginTransaction()` returns bool for better v2.0.1 interop ([#386](https://github.com/matth-x/MicroOcpp/pull/386)) +- Configurations C-API updates ([#400](https://github.com/matth-x/MicroOcpp/pull/400)) +- Platform integrations C-API upates ([#400](https://github.com/matth-x/MicroOcpp/pull/400)) + +### Added + +- `getTransactionV201()` exposes v201 Tx in API ([#386](https://github.com/matth-x/MicroOcpp/pull/386)) +- v201 support in Transaction.h C-API ([#386](https://github.com/matth-x/MicroOcpp/pull/386)) +- Write-only Configurations ([#400](https://github.com/matth-x/MicroOcpp/pull/400)) + +### Fixed + +- Timing issues for OCTT test cases ([#383](https://github.com/matth-x/MicroOcpp/pull/383)) +- Misleading Reset failure dbg msg ([#388](https://github.com/matth-x/MicroOcpp/pull/388)) +- Reject negative ints in ChangeConfig ([#388](https://github.com/matth-x/MicroOcpp/pull/388)) +- Revised SCons integration ([#400](https://github.com/matth-x/MicroOcpp/pull/400)) + +## [1.2.0] - 2024-11-03 + +### Changed + +- Change `MicroOcpp::ChargePointStatus` into C-style enum ([#309](https://github.com/matth-x/MicroOcpp/pull/309)) +- Connector lock disabled by default per `MO_ENABLE_CONNECTOR_LOCK` ([#312](https://github.com/matth-x/MicroOcpp/pull/312)) +- Relaxed temporal order of non-tx-related operations ([#345](https://github.com/matth-x/MicroOcpp/pull/345)) +- Use pseudo-GUIDs as messageId ([#345](https://github.com/matth-x/MicroOcpp/pull/345)) +- ISO 8601 milliseconds omitted by default ([352](https://github.com/matth-x/MicroOcpp/pull/352)) +- Rename `MO_NUM_EVSE` into `MO_NUM_EVSEID` (v2.0.1) ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) +- Change `MicroOcpp::ReadingContext` into C-style struct ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) +- Refactor RequestStartTransaction (v2.0.1) ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) + +### Added + +- Provide ChargePointStatus in API ([#309](https://github.com/matth-x/MicroOcpp/pull/309)) +- Built-in OTA over FTP ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) +- Built-in Diagnostics over FTP ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) +- Error `severity` mechanism ([#331](https://github.com/matth-x/MicroOcpp/pull/331)) +- Build flag `MO_REPORT_NOERROR` to report error recovery ([#331](https://github.com/matth-x/MicroOcpp/pull/331)) +- Support for `parentIdTag` ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) +- Input validation for unsigned int Configs ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) +- Support for TransactionMessageAttempts/-RetryInterval ([#345](https://github.com/matth-x/MicroOcpp/pull/345), [#380](https://github.com/matth-x/MicroOcpp/pull/380)) +- Heap profiler and custom allocator support ([#350](https://github.com/matth-x/MicroOcpp/pull/350)) +- Migration of persistent storage ([#355](https://github.com/matth-x/MicroOcpp/pull/355)) +- Benchmarks pipeline ([#369](https://github.com/matth-x/MicroOcpp/pull/369), [#376](https://github.com/matth-x/MicroOcpp/pull/376)) +- MeterValues port for OCPP 2.0.1 ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) +- UnlockConnector port for OCPP 2.0.1 ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) +- More APIs ported to OCPP 2.0.1 ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) +- Support for AuthorizeRemoteTxRequests ([#373](https://github.com/matth-x/MicroOcpp/pull/373)) +- Persistent Variable and Tx store for OCPP 2.0.1 ([#379](https://github.com/matth-x/MicroOcpp/pull/379)) + +### Removed + +- ESP32 built-in HTTP OTA ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) +- Operation store (files op-*.jsn and opstore.jsn) ([#345](https://github.com/matth-x/MicroOcpp/pull/345)) +- Explicit tracking of txNr (file txstore.jsn) ([#345](https://github.com/matth-x/MicroOcpp/pull/345)) +- SimpleRequestFactory ([#351](https://github.com/matth-x/MicroOcpp/pull/351)) + +### Fixed + +- Skip Unix files . and .. in ftw_root ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) +- Skip clock-aligned measurements when time not set +- Hold back error StatusNotifs when time not set ([#311](https://github.com/matth-x/MicroOcpp/issues/311)) +- Don't send Available when tx occupies connector ([#315](https://github.com/matth-x/MicroOcpp/issues/315)) +- Make ChargingScheduleAllowedChargingRateUnit read-only ([#328](https://github.com/matth-x/MicroOcpp/issues/328)) +- ~Don't send StatusNotifs while offline ([#344](https://github.com/matth-x/MicroOcpp/pull/344))~ (see ([#371](https://github.com/matth-x/MicroOcpp/pull/371))) +- Don't change into Unavailable upon Reset ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) +- Reject DataTransfer by default ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) +- UnlockConnector NotSupported if connectorId invalid ([#344](https://github.com/matth-x/MicroOcpp/pull/344)) +- Fix regression bug of [#345](https://github.com/matth-x/MicroOcpp/pull/345) ([#353](https://github.com/matth-x/MicroOcpp/pull/353), [#356](https://github.com/matth-x/MicroOcpp/pull/356)) +- Correct MeterValue PreBoot timestamp ([#354](https://github.com/matth-x/MicroOcpp/pull/354)) +- Send errorCode in triggered StatusNotif ([#359](https://github.com/matth-x/MicroOcpp/pull/359)) +- Remove int to bool conversion in ChangeConfig ([#362](https://github.com/matth-x/MicroOcpp/pull/362)) +- Multiple fixes of the OCPP 2.0.1 extension ([#371](https://github.com/matth-x/MicroOcpp/pull/371)) + +## [1.1.0] - 2024-05-21 + +### Changed + +- Replace `PollResult` with enum `UnlockConnectorResult` ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) +- Rename master branch into main +- Tx logic directly checks if WebSocket is offline ([#282](https://github.com/matth-x/MicroOcpp/pull/282)) +- `ocppPermitsCharge` ignores Faulted state ([#279](https://github.com/matth-x/MicroOcpp/pull/279)) +- `setEnergyMeterInput` expects `int` input ([#301](https://github.com/matth-x/MicroOcpp/pull/301)) + +### Added + +- File index ([#270](https://github.com/matth-x/MicroOcpp/pull/270)) +- Config `Cst_TxStartOnPowerPathClosed` to put back TxStartPoint ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) +- Build flag `MO_ENABLE_RESERVATION=0` disables Reservation module ([#302](https://github.com/matth-x/MicroOcpp/pull/302)) +- Build flag `MO_ENABLE_LOCAL_AUTH=0` disables LocalAuthList module ([#303](https://github.com/matth-x/MicroOcpp/pull/303)) +- Function `bool isConnected()` in `Connection` interface ([#282](https://github.com/matth-x/MicroOcpp/pull/282)) +- Build flags for customizing memory limits of SmartCharging ([#260](https://github.com/matth-x/MicroOcpp/pull/260)) +- SConscript ([#287](https://github.com/matth-x/MicroOcpp/pull/287)) +- C-API for custom Configs store ([297](https://github.com/matth-x/MicroOcpp/pull/297)) +- Certificate Management, UCs M03 - M05 ([#262](https://github.com/matth-x/MicroOcpp/pull/262), [#274](https://github.com/matth-x/MicroOcpp/pull/274), [#292](https://github.com/matth-x/MicroOcpp/pull/292)) +- FTP Client ([#291](https://github.com/matth-x/MicroOcpp/pull/291)) +- `ProtocolVersion` selects v1.6 or v2.0.1 ([#247](https://github.com/matth-x/MicroOcpp/pull/247)) +- Build flag `MO_ENABLE_V201=1` enables OCPP 2.0.1 features ([#247](https://github.com/matth-x/MicroOcpp/pull/247)) + - Variables (non-persistent), UCs B05 - B07 ([#247](https://github.com/matth-x/MicroOcpp/pull/247), [#284](https://github.com/matth-x/MicroOcpp/pull/284)) + - Transactions (preview only), UCs E01 - E12 ([#247](https://github.com/matth-x/MicroOcpp/pull/247)) + - StatusNotification compatibility ([#247](https://github.com/matth-x/MicroOcpp/pull/247)) + - ChangeAvailability compatibility ([#285](https://github.com/matth-x/MicroOcpp/pull/285)) + - Reset compatibility, UCs B11 - B12 ([#286](https://github.com/matth-x/MicroOcpp/pull/286)) + - RequestStart-/StopTransaction, UCs F01 - F02 ([#289](https://github.com/matth-x/MicroOcpp/pull/289)) + +### Fixed + +- Fix defect idTag check in `endTransaction` ([#275](https://github.com/matth-x/MicroOcpp/pull/275)) +- Make field localAuthorizationList in SendLocalList optional +- Update charging profiles when flash disabled (relates to [#260](https://github.com/matth-x/MicroOcpp/pull/260)) +- Ignore UnlockConnector when handler not set ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) +- Reject ChargingProfile if unit not supported ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) +- Fix building with debug level warn and error +- Reduce debug output FW size overhead ([#304](https://github.com/matth-x/MicroOcpp/pull/304)) +- Fix transaction freeze in offline mode ([#279](https://github.com/matth-x/MicroOcpp/pull/279), [#287](https://github.com/matth-x/MicroOcpp/pull/287)) +- Fix compilation error caused by `PRId32` ([#279](https://github.com/matth-x/MicroOcpp/pull/279)) +- Don't load FW-mngt. module when no handlers set ([#271](https://github.com/matth-x/MicroOcpp/pull/271)) +- Change arduinoWebSockets URL param to path ([#278](https://github.com/matth-x/MicroOcpp/issues/278)) +- Avoid creating conf when operation fails ([#290](https://github.com/matth-x/MicroOcpp/pull/290)) +- Fix whitespaces in MeterValues ([#301](https://github.com/matth-x/MicroOcpp/pull/301)) +- Make SmartChargingProfile txId field optional ([#348](https://github.com/matth-x/MicroOcpp/pull/348)) + +## [1.0.3] - 2024-04-06 + +### Fixed + +- Fix nullptr access in endTransaction ([#275](https://github.com/matth-x/MicroOcpp/pull/275)) +- Backport: Fix building with debug level warn and error + +## [1.0.2] - 2024-03-24 + +### Fixed + +- Correct MO version numbers in code (they were still `1.0.0`) + +## [1.0.1] - 2024-02-27 + +### Fixed + +- Allow `nullptr` as parameter for `mocpp_set_console_out` ([#224](https://github.com/matth-x/MicroOcpp/issues/224)) +- Fix `mocpp_tick_ms()` on esp-idf roll-over after 12 hours +- Pin ArduinoJson to v6.21 ([#245](https://github.com/matth-x/MicroOcpp/issues/245)) +- Fix bounds checking in SmartCharging module ([#260](https://github.com/matth-x/MicroOcpp/pull/260)) + +## [1.0.0] - 2023-10-22 + +_First release_ + +### Changed + +- `mocpp_initialize` takes OCPP URL without explicit host, port ([#220](https://github.com/matth-x/MicroOcpp/pull/220)) +- `endTransaction` checks authorization of `idTag` +- Update configurations API ([#195](https://github.com/matth-x/MicroOcpp/pull/195)) +- Update Firmware- and DiagnosticsService API ([#207](https://github.com/matth-x/MicroOcpp/pull/207)) +- Update Connection interface +- Update Authorization module functions ([#213](https://github.com/matth-x/MicroOcpp/pull/213)) +- Reflect changes in C-API +- Change build flag prefix from `MOCPP_` to `MO_` +- Change `mo_set_console_out` to `mocpp_set_console_out` +- Revise README.md +- Revise misleading debug messages +- Update Arduino IDE manifest ([#206](https://github.com/matth-x/MicroOcpp/issues/206)) + +### Added + +- Auto-recovery switch in `mocpp_initialize` params +- WebAssembly port +- Configurable `MO_PARTITION_LABEL` for the esp-idf SPIFFS integration ([#218](https://github.com/matth-x/MicroOcpp/pull/218)) +- `MO_TX_CLEAN_ABORTED=0` keeps aborted txs in journal +- `MO_VERSION` specifier +- `MO_PLATFORM_NONE` for compilation on custom platforms +- `endTransaction_authorized` enforces the tx end +- Add valgrind, ASan, UBSan CI/CD steps ([#189](https://github.com/matth-x/MicroOcpp/pull/189)) + +### Fixed + +- Reservation ([#196](https://github.com/matth-x/MicroOcpp/pull/196)) +- Fix immediate FW-update Download phase abort ([#216](https://github.com/matth-x/MicroOcpp/pull/216)) +- `stat` usage on arduino-esp32 LittleFS +- SetChargingProfile JSON capacity calculation +- Set correct idTag when Reset triggers StopTx +- Execute operations only once despite multiple .conf send attempts ([#207](https://github.com/matth-x/MicroOcpp/pull/207)) +- ConnectionTimeOut only applies when connector is still unplugged +- Fix valgrind warnings + +## [1eff6e5] - 23-08-23 + +_Previous point with breaking changes on master_ + +Renaming to MicroOcpp is completed since this commit. See the [migration guide](https://matth-x.github.io/MicroOcpp/migration/) for more details on what's changed. Changelogs and semantic versioning are adopted starting with v1.0.0 + +## [0.3.0] - 23-08-19 + +_Last version under the project name ArduinoOcpp_ diff --git a/CMakeLists.txt b/CMakeLists.txt index e7f6f8de..bf1d5f7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,101 +1,218 @@ -# matth-x/ArduinoOcpp -# Copyright Matthias Akstaller 2019 - 2023 +# matth-x/MicroOcpp +# Copyright Matthias Akstaller 2019 - 2024 # MIT License cmake_minimum_required(VERSION 3.15) -set(AO_SRC - src/ArduinoOcpp/Core/Configuration.cpp - src/ArduinoOcpp/Core/ConfigurationContainer.cpp - src/ArduinoOcpp/Core/ConfigurationContainerFlash.cpp - src/ArduinoOcpp/Core/ConfigurationKeyValue.cpp - src/ArduinoOcpp/Core/FilesystemAdapter.cpp - src/ArduinoOcpp/Core/FilesystemUtils.cpp - src/ArduinoOcpp/Core/OcppConnection.cpp - src/ArduinoOcpp/Core/OcppEngine.cpp - src/ArduinoOcpp/Core/OcppMessage.cpp - src/ArduinoOcpp/Core/OcppModel.cpp - src/ArduinoOcpp/Core/OcppOperation.cpp - src/ArduinoOcpp/Core/OcppOperationTimeout.cpp - src/ArduinoOcpp/Core/OcppServer.cpp - src/ArduinoOcpp/Core/OcppSocket.cpp - src/ArduinoOcpp/Core/OcppTime.cpp - src/ArduinoOcpp/Core/OperationsQueue.cpp - src/ArduinoOcpp/Core/OperationStore.cpp - src/ArduinoOcpp/MessagesV16/Authorize.cpp - src/ArduinoOcpp/MessagesV16/BootNotification.cpp - src/ArduinoOcpp/MessagesV16/ChangeAvailability.cpp - src/ArduinoOcpp/MessagesV16/ChangeConfiguration.cpp - src/ArduinoOcpp/MessagesV16/ClearCache.cpp - src/ArduinoOcpp/MessagesV16/ClearChargingProfile.cpp - src/ArduinoOcpp/MessagesV16/DataTransfer.cpp - src/ArduinoOcpp/MessagesV16/DiagnosticsStatusNotification.cpp - src/ArduinoOcpp/MessagesV16/FirmwareStatusNotification.cpp - src/ArduinoOcpp/MessagesV16/GetCompositeSchedule.cpp - src/ArduinoOcpp/MessagesV16/GetConfiguration.cpp - src/ArduinoOcpp/MessagesV16/GetDiagnostics.cpp - src/ArduinoOcpp/MessagesV16/Heartbeat.cpp - src/ArduinoOcpp/MessagesV16/MeterValues.cpp - src/ArduinoOcpp/MessagesV16/RemoteStartTransaction.cpp - src/ArduinoOcpp/MessagesV16/RemoteStopTransaction.cpp - src/ArduinoOcpp/MessagesV16/Reset.cpp - src/ArduinoOcpp/MessagesV16/SetChargingProfile.cpp - src/ArduinoOcpp/MessagesV16/StartTransaction.cpp - src/ArduinoOcpp/MessagesV16/StatusNotification.cpp - src/ArduinoOcpp/MessagesV16/StopTransaction.cpp - src/ArduinoOcpp/MessagesV16/TriggerMessage.cpp - src/ArduinoOcpp/MessagesV16/UnlockConnector.cpp - src/ArduinoOcpp/MessagesV16/UpdateFirmware.cpp - src/ArduinoOcpp/Platform.cpp - src/ArduinoOcpp/SimpleOcppOperationFactory.cpp - src/ArduinoOcpp/Tasks/ChargePointStatus/ChargePointStatusService.cpp - src/ArduinoOcpp/Tasks/ChargePointStatus/ConnectorStatus.cpp - src/ArduinoOcpp/Tasks/Diagnostics/DiagnosticsService.cpp - src/ArduinoOcpp/Tasks/FirmwareManagement/FirmwareService.cpp - src/ArduinoOcpp/Tasks/Heartbeat/HeartbeatService.cpp - src/ArduinoOcpp/Tasks/Metering/ConnectorMeterValuesRecorder.cpp - src/ArduinoOcpp/Tasks/Metering/MeteringService.cpp - src/ArduinoOcpp/Tasks/Metering/MeterStore.cpp - src/ArduinoOcpp/Tasks/Metering/MeterValue.cpp - src/ArduinoOcpp/Tasks/Metering/SampledValue.cpp - src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingModel.cpp - src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingService.cpp - src/ArduinoOcpp/Tasks/Transactions/Transaction.cpp - src/ArduinoOcpp/Tasks/Transactions/TransactionProcess.cpp - src/ArduinoOcpp/Tasks/Transactions/TransactionStore.cpp - src/ArduinoOcpp.cpp - src/ArduinoOcpp_c.cpp +set(CMAKE_CXX_STANDARD 11) + +set(MO_SRC + src/MicroOcpp/Core/Configuration_c.cpp + src/MicroOcpp/Core/Configuration.cpp + src/MicroOcpp/Core/ConfigurationContainer.cpp + src/MicroOcpp/Core/ConfigurationContainerFlash.cpp + src/MicroOcpp/Core/ConfigurationKeyValue.cpp + src/MicroOcpp/Core/FilesystemAdapter.cpp + src/MicroOcpp/Core/FilesystemUtils.cpp + src/MicroOcpp/Core/FtpMbedTLS.cpp + src/MicroOcpp/Core/Memory.cpp + src/MicroOcpp/Core/RequestQueue.cpp + src/MicroOcpp/Core/Context.cpp + src/MicroOcpp/Core/Operation.cpp + src/MicroOcpp/Model/Model.cpp + src/MicroOcpp/Core/Request.cpp + src/MicroOcpp/Core/Connection.cpp + src/MicroOcpp/Core/Time.cpp + src/MicroOcpp/Core/UuidUtils.cpp + src/MicroOcpp/Operations/Authorize.cpp + src/MicroOcpp/Operations/BootNotification.cpp + src/MicroOcpp/Operations/CancelReservation.cpp + src/MicroOcpp/Operations/ChangeAvailability.cpp + src/MicroOcpp/Operations/ChangeConfiguration.cpp + src/MicroOcpp/Operations/ClearCache.cpp + src/MicroOcpp/Operations/ClearChargingProfile.cpp + src/MicroOcpp/Operations/CustomOperation.cpp + src/MicroOcpp/Operations/DataTransfer.cpp + src/MicroOcpp/Operations/DeleteCertificate.cpp + src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp + src/MicroOcpp/Operations/FirmwareStatusNotification.cpp + src/MicroOcpp/Operations/GetBaseReport.cpp + src/MicroOcpp/Operations/GetCompositeSchedule.cpp + src/MicroOcpp/Operations/GetConfiguration.cpp + src/MicroOcpp/Operations/GetDiagnostics.cpp + src/MicroOcpp/Operations/GetInstalledCertificateIds.cpp + src/MicroOcpp/Operations/GetLocalListVersion.cpp + src/MicroOcpp/Operations/GetVariables.cpp + src/MicroOcpp/Operations/Heartbeat.cpp + src/MicroOcpp/Operations/MeterValues.cpp + src/MicroOcpp/Operations/NotifyReport.cpp + src/MicroOcpp/Operations/RemoteStartTransaction.cpp + src/MicroOcpp/Operations/RemoteStopTransaction.cpp + src/MicroOcpp/Operations/RequestStartTransaction.cpp + src/MicroOcpp/Operations/RequestStopTransaction.cpp + src/MicroOcpp/Operations/ReserveNow.cpp + src/MicroOcpp/Operations/Reset.cpp + src/MicroOcpp/Operations/SecurityEventNotification.cpp + src/MicroOcpp/Operations/SendLocalList.cpp + src/MicroOcpp/Operations/SetChargingProfile.cpp + src/MicroOcpp/Operations/SetVariables.cpp + src/MicroOcpp/Operations/StartTransaction.cpp + src/MicroOcpp/Operations/StatusNotification.cpp + src/MicroOcpp/Operations/StopTransaction.cpp + src/MicroOcpp/Operations/TransactionEvent.cpp + src/MicroOcpp/Operations/TriggerMessage.cpp + src/MicroOcpp/Operations/InstallCertificate.cpp + src/MicroOcpp/Operations/UnlockConnector.cpp + src/MicroOcpp/Operations/UpdateFirmware.cpp + src/MicroOcpp/Debug.cpp + src/MicroOcpp/Platform.cpp + src/MicroOcpp/Core/OperationRegistry.cpp + src/MicroOcpp/Model/Availability/AvailabilityService.cpp + src/MicroOcpp/Model/Authorization/AuthorizationData.cpp + src/MicroOcpp/Model/Authorization/AuthorizationList.cpp + src/MicroOcpp/Model/Authorization/AuthorizationService.cpp + src/MicroOcpp/Model/Authorization/IdToken.cpp + src/MicroOcpp/Model/Boot/BootService.cpp + src/MicroOcpp/Model/Certificates/Certificate.cpp + src/MicroOcpp/Model/Certificates/Certificate_c.cpp + src/MicroOcpp/Model/Certificates/CertificateMbedTLS.cpp + src/MicroOcpp/Model/Certificates/CertificateService.cpp + src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp + src/MicroOcpp/Model/ConnectorBase/Connector.cpp + src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp + src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp + src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp + src/MicroOcpp/Model/Metering/MeteringConnector.cpp + src/MicroOcpp/Model/Metering/MeteringService.cpp + src/MicroOcpp/Model/Metering/MeterStore.cpp + src/MicroOcpp/Model/Metering/MeterValue.cpp + src/MicroOcpp/Model/Metering/MeterValuesV201.cpp + src/MicroOcpp/Model/Metering/ReadingContext.cpp + src/MicroOcpp/Model/Metering/SampledValue.cpp + src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp + src/MicroOcpp/Model/Reservation/Reservation.cpp + src/MicroOcpp/Model/Reservation/ReservationService.cpp + src/MicroOcpp/Model/Reset/ResetService.cpp + src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp + src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp + src/MicroOcpp/Model/Transactions/Transaction.cpp + src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp + src/MicroOcpp/Model/Transactions/TransactionService.cpp + src/MicroOcpp/Model/Transactions/TransactionStore.cpp + src/MicroOcpp/Model/Variables/Variable.cpp + src/MicroOcpp/Model/Variables/VariableContainer.cpp + src/MicroOcpp/Model/Variables/VariableService.cpp + src/MicroOcpp.cpp + src/MicroOcpp_c.cpp ) if(ESP_PLATFORM) - idf_component_register(SRCS ${AO_SRC} - INCLUDE_DIRS "./src" "${PROJECT_DIR}/include" - PRIV_REQUIRES spiffs) + idf_component_register(SRCS ${MO_SRC} + INCLUDE_DIRS "./src" "../ArduinoJson/src" + PRIV_REQUIRES spiffs + ) target_compile_options(${COMPONENT_TARGET} PUBLIC - -DAO_CUSTOM_WS - -DAO_CUSTOM_UPDATER - -DAO_CUSTOM_RESET - -DAO_PLATFORM=AO_PLATFORM_ESPIDF) + -DMO_PLATFORM=MO_PLATFORM_ESPIDF + ) return() endif() -add_library(ArduinoOcpp ${AO_SRC}) - -target_include_directories(ArduinoOcpp PUBLIC - "./src" - "../ArduinoJson/src" - ) - -target_compile_definitions(ArduinoOcpp PUBLIC - AO_PLATFORM=AO_PLATFORM_UNIX - AO_CUSTOM_WS - AO_CUSTOM_UPDATER - AO_CUSTOM_RESET - AO_DBG_LEVEL=AO_DL_DEBUG - AO_TRAFFIC_OUT - AO_FILENAME_PREFIX="./ao_store" - AO_DEACTIVATE_FLASH_SMARTCHARGING +project(MicroOcpp VERSION 1.2.0) + +add_library(MicroOcpp ${MO_SRC}) + +target_include_directories(MicroOcpp PUBLIC + "./src" + "../ArduinoJson/src" +) + +target_compile_definitions(MicroOcpp PUBLIC + MO_PLATFORM=MO_PLATFORM_UNIX +) + +# Unit tests + +set(MO_SRC_UNIT + tests/helpers/testHelper.cpp + tests/ocppEngineLifecycle.cpp + tests/TransactionSafety.cpp + tests/ChargingSessions.cpp + tests/ConfigurationBehavior.cpp + tests/SmartCharging.cpp + tests/Api.cpp + tests/Metering.cpp + tests/Configuration.cpp + tests/Reservation.cpp + tests/Reset.cpp + tests/LocalAuthList.cpp + tests/Variables.cpp + tests/Transactions.cpp + tests/RemoteStartTransaction.cpp + tests/Certificates.cpp + tests/FirmwareManagement.cpp + tests/ChargePointError.cpp + tests/Boot.cpp + tests/Security.cpp +) + +add_executable(mo_unit_tests + ${MO_SRC} + ${MO_SRC_UNIT} + ./tests/catch2/catchMain.cpp +) + +if (MO_BUILD_UNIT_MBEDTLS) + add_subdirectory(lib/mbedtls) + target_link_libraries(mo_unit_tests PUBLIC + mbedtls + mbedcrypto + mbedx509 + ) + + target_compile_definitions(mo_unit_tests PUBLIC + MO_ENABLE_MBEDTLS=1 ) +endif() + +target_include_directories(mo_unit_tests PUBLIC + "./tests" + "./tests/helpers" + "./src" +) + +target_compile_definitions(mo_unit_tests PUBLIC + MO_PLATFORM=MO_PLATFORM_UNIX + MO_NUMCONNECTORS=3 + MO_CUSTOM_TIMER + MO_DBG_LEVEL=MO_DL_INFO + MO_TRAFFIC_OUT + MO_FILENAME_PREFIX="./mo_store/" + MO_LocalAuthListMaxLength=8 + MO_SendLocalListMaxLength=4 + MO_ENABLE_FILE_INDEX=1 + MO_ChargeProfileMaxStackLevel=2 + MO_ChargingScheduleMaxPeriods=4 + MO_MaxChargingProfilesInstalled=3 + MO_ENABLE_CERT_MGMT=1 + MO_ENABLE_CONNECTOR_LOCK=1 + MO_REPORT_NOERROR=1 + MO_ENABLE_V201=1 + MO_OVERRIDE_ALLOCATION=1 + MO_ENABLE_HEAP_PROFILER=1 + MO_HEAP_PROFILER_EXTERNAL_CONTROL=1 + CATCH_CONFIG_EXTERNAL_INTERFACES +) + +target_compile_options(mo_unit_tests PUBLIC + -Wall + -O0 + -g + --coverage +) + +target_link_options(mo_unit_tests PUBLIC + --coverage +) diff --git a/LICENSE b/LICENSE index c24c6219..d0b6f8ec 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 - 2022 matth-x +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 58c018ed..de09d875 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,110 @@ -# Icon   ArduinoOcpp +# Icon   MicroOCPP -[![Build Status]( https://github.com/matth-x/ArduinoOcpp/workflows/PlatformIO%20CI/badge.svg)](https://github.com/matth-x/ArduinoOcpp/actions) +[![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-J 1.6 client for embedded microcontrollers. Portable C/C++. Compatible with Espressif, NXP, Texas Instruments and STM. +OCPP 1.6 / 2.0.1 client for microcontrollers. Portable C/C++. Compatible with Espressif, Arduino, NXP, STM, Linux and more. -Reference usage: [OpenEVSE](https://github.com/OpenEVSE/ESP32_WiFi_V4.x/blob/master/src/ocpp.cpp) +:heavy_check_mark: Works with [15+ commercial Central Systems](https://www.micro-ocpp.com/#h.314525e8447cc93c_81) -PlatformIO package: [ArduinoOcpp](https://platformio.org/lib/show/11975/ArduinoOcpp) +:heavy_check_mark: Eligible for public chargers (Eichrecht-compliant) -Website: [www.arduino-ocpp.com](https://www.arduino-ocpp.com) +: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) -Fully integrated into the Arduino platform and the ESP32 / ESP8266. Runs on ESP-IDF, FreeRTOS and generic embedded C/C++ platforms. +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) -## Make your EVSE ready for OCPP :car::electric_plug::battery: +## AI-friendly code -This library allows your EVSE to communicate with an OCPP Backend and to participate in commercial charging networks. You can integrate it into an existing firmware development, or start a new EVSE project from scratch with it. +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. -:heavy_check_mark: Works with [SteVe](https://github.com/RWTH-i5-IDSG/steve) and [15+ commercial Central Systems](https://www.arduino-ocpp.com/#h.314525e8447cc93c_81) +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: Tested in many charging stations +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. -:heavy_check_mark: Eligible for public chargers (Eichrecht-compliant) +## Tester / Demo App + +*Main repository: [MicroOcppSimulator](https://github.com/matth-x/MicroOcppSimulator)* + +The Simulator is a demo & development tool for MicroOCPP which allows to quickly assess the compatibility with different OCPP backends. It simulates a full charging station, adds a GUI and a mocked hardware binding to MicroOCPP and runs in the browser (using WebAssembly): [Try it](https://demo.micro-ocpp.com/) + +
Screenshot
+ +#### Usage + +**OCPP server setup**: Navigate to "Control Center". In the WebSocket options, add the OCPP backend URL, charge box ID and authorization key if existent. Press "Update WebSocket" to save. The Simulator should connect to the OCPP server. To check the connection status, it could be helpful to open the developer tools of the browser. + +If you don't have an OCPP server at hand, leave the charge box ID blank and enter the following backend address: `wss://echo.websocket.events/` (this server is sponsored by Lob.com) -Try it (no hardware required): [ArduinoOcppSimulator](https://github.com/matth-x/ArduinoOcppSimulator) +**RFID authentication**: Go to "Control Center" > "Connectors" > "Transaction" and update the idTag with the desired value. -### Features +## Benchmarks -- handles the OCPP communication with the charging network -- implements the standard OCPP charging process -- provides an API for the integration of the hardware modules of your EVSE -- works with any TLS implementation and WebSocket library. E.g. - - Arduino networking stack: [Links2004/arduinoWebSockets](https://github.com/Links2004/arduinoWebSockets), or - - generic embedded systems: [Mongoose Networking Library](https://github.com/cesanta/mongoose) +*Full report: [MicroOCPP benchmarks](https://matth-x.github.io/MicroOcpp/benchmarks/)* -For simple chargers, the necessary hardware and internet integration is usually far below 1000 LOCs. +The following measurements were taken on the ESP32 @ 160MHz and represent the optimistic best case scenario for a charger with two physical connectors (i.e. compiled with `-Os`, disabled debug output and logs). + +| Description | Value | +| :--- | ---: | +| Flash size (minimal) | 121,170 B | +| Heap occupation (idle) | 12,308 B | +| Heap occupation (peak) | 21,916 B | +| Initailization | 21 ms | +| `loop()` call (idle) | 0.05 ms | +| Large message sent | 5 ms | + +In practical setups, the execution time is largely determined by IO delays and the heap occupation is significantly influenced by the configuration with reservation, local authorization and charging profile lists. ## Developers guide -Please take `examples/ESP/main.cpp` as the starting point for your 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 [`ArduinoOcpp.h`](https://github.com/matth-x/ArduinoOcpp/blob/master/src/ArduinoOcpp.h). +PlatformIO package: [MicroOcpp](https://registry.platformio.org/libraries/matth-x/MicroOcpp) + +MicroOCPP is an implementation of the OCPP communication behavior. It automatically initiates the corresponding OCPP operations once the hardware status changes or the RFID input is updated with a new value. Conversely it processes new data from the server, stores it locally and updates the hardware controls when applicable. + +Please take `examples/ESP/main.cpp` as the starting point for the first project. It is a minimal example which shows how to establish an OCPP connection and how to start and stop charging sessions. The API documentation can be found in [`MicroOcpp.h`](https://github.com/matth-x/MicroOcpp/blob/main/src/MicroOcpp.h). Also check out the [Docs](https://matth-x.github.io/MicroOcpp). ### Dependencies Mandatory: -- [bblanchon/ArduinoJSON](https://github.com/bblanchon/ArduinoJson) (please upgrade to version `6.19.1`) +- [bblanchon/ArduinoJSON](https://github.com/bblanchon/ArduinoJson) (version `6.21`) If compiled with the Arduino integration: -- [Links2004/arduinoWebSockets](https://github.com/Links2004/arduinoWebSockets) (please upgrade to version `2.3.6`) +- [Links2004/arduinoWebSockets](https://github.com/Links2004/arduinoWebSockets) (version `2.4.1`) + +If using the built-in certificate store (to enable, set build flag `MO_ENABLE_MBEDTLS=1`): + +- [Mbed-TLS/mbedtls](https://github.com/Mbed-TLS/mbedtls) (version `2.28.1`) + +In case you use PlatformIO, you can copy all dependencies from `platformio.ini` into your own configuration file. Alternatively, you can install the full library with dependencies by adding `matth-x/MicroOcpp@1.2.0` in the PIO library manager. -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/ArduinoOcpp` in the PIO library manager. +## OCPP 2.0.1 and ISO 15118 -### Next development steps +The following OCPP 2.0.1 use cases are implemented: -- [x] reach full compliance to OCPP 1.6 Smart Charging Profile -- [ ] integrate Authorization Cache -- [ ] **get ready for OCPP 2.0.1 and ISO 15118** +| 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 | -## Supported Feature Profiles +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). -| Feature profile | supported | in progress | -| -------------- | :---------: | :-----------: | -| **Core** | :heavy_check_mark: | -| **Smart charging** | :heavy_check_mark: | -| **Remote trigger** | :heavy_check_mark: | -| **Firmware management** | :heavy_check_mark: | +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. -## Further help +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. -I hope this guide can help you to successfully integrate an OCPP controller into your EVSE. If something needs clarification or if you have a question, please send me a message. +## Contact -:envelope: : matthias A⊤ arduino-ocpp DО⊤ com +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: -If you want professional assistance for your EVSE project, you can contact me as well. I'm looking forward to hearing about your ideas! +: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 new file mode 100644 index 00000000..a6423759 --- /dev/null +++ b/docs/img/components_overview.svg @@ -0,0 +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 diff --git a/docs/img/components_selective.svg b/docs/img/components_selective.svg new file mode 100644 index 00000000..7788f100 --- /dev/null +++ b/docs/img/components_selective.svg @@ -0,0 +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 diff --git a/docs/img/favicon.ico b/docs/img/favicon.ico new file mode 100644 index 00000000..42096b5f Binary files /dev/null and b/docs/img/favicon.ico differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..3705cfff --- /dev/null +++ b/docs/index.md @@ -0,0 +1,15 @@ +MicroOCPP is an OCPP client which runs on microcontrollers and enables EVSEs to participate in OCPP charging networks. As a software library, it can be added to the firmware of the EVSE and will become a new part of it. If the EVSE has already an internet controller, then most likely, no extra hardware is required. + +[Technical introduction](intro-tech) + +[Migrating to v1.0](migration) + +[Modules](modules) + +[Development tools and basic prerequisites](prerequisites) + +[Security whitepaper](security) + + + +*Documentation WIP. See the [GitHub Readme](https://github.com/matth-x/MicroOcpp) or the [API description](https://github.com/matth-x/MicroOcpp/blob/main/src/MicroOcpp.h) as reference.* diff --git a/docs/intro-tech.md b/docs/intro-tech.md new file mode 100644 index 00000000..d99457f5 --- /dev/null +++ b/docs/intro-tech.md @@ -0,0 +1,85 @@ +# Technical introduction + +This chapter covers the technical concepts of MicroOCPP. + +## Scope of MicroOCPP + +The OCPP specification defines a charger data model, operations on the data model and the resulting physical behavior on the charger side. MicroOCPP implements the full scope of OCPP, i.e. a minimalistic data store for the data model, the OCPP operations and an interface to the surrounding firmware. + +Another part of OCPP is its messaging mechanism, the so-called Remote Procedure Calls (RPC) framework. MicroOCPP also implements the specified RPC framework with the required guarantees of message delivery or the corresponding error handling. + +At the lowest layer, OCPP relies on standard WebSockets. MicroOCPP works with any WebSocket library and has a lean interface to integrate them. + +The high-level API in `MicroOcpp.h` bundles all touch points of the EVSE firmware with the OCPP library. + +

+ +
+ Overview of the architecture +

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

+ +
+ Selective usage of MicroOCPP +

+ +Customizations of the library allow to integrate use cases for which the high-level API is too restrictive. The high-level API is designed to provide a facade for the expected usage of the library, but since the charging sector is driven by innovation, new use cases for OCPP emerge every day. If a custom use case cannot be integrated on the API level, the main firmware can access the internal data structures of MicroOCPP and complement the required functionality or replace parts of the internal behavior with custom implementations which fits the concrete scenarios better. + +## Main-loop paradigm + +MicroOCPP works with the common main-loop execution model of microcontrollers. After initialization, the EVSE firmware most likely enters a main-loop and repeats it infinitely. To run MicroOCPP, a call to its loop function must be placed into the main loop of the firmware. Then at each main-loop iteration, MicroOCPP executes its internal routines, i.e. it processes input data, updates its data model, executes operations and creates new output data. The MicroOCPP loop function does not block the main loop but executes immediately. This library does not contain any delay functions. Some activities of the library spread over many loop iterations like the start of a charging session which needs to await the approval of an NFC card and a hardware diagnosis of the high power electronics for example. All activities in MicroOCPP support the distribution over many loop calls, leading to a pseudo-parallel execution behavior. + +No separate RTOS task is needed and MicroOCPP does not have an internal mechanism for multi-task synchronization. However, it is of course possible to create a dedicated OCPP task, as long as extra care is taken of the synchronization. + +## How the API works + +The high-level API consists of four parts: + +- **Library lifecycle**: The library has initialize functions with a few initialization options. Dynamic system components like the WebSocket adapter need to be set at initialization time. The deinitialize function reverts the library into an unitialized state. That's useful for memory inspection tools like valgrind or to disable the OCPP communication. The loop function also counts as part of the lifecycle management. +- **Sensor Inputs**: EVSEs are mechanical systems with a variety of sensor information. OCPP is used to send parts of the sensor readings to the server. The other part of the sensor data flows into the local charger model of MicroOCPP where it is further processed. To update MicroOCPP with the input data from the sensors, the firmware needs to bind the sensors to the library. An *Input-binding*, or in short *Input*, is a function which transfers the current sensor value to MicroOCPP. Inputs are callback functions which read a specific sensor value and pass the value in the return statement. The firmware defines those callback functions for each sensor and adds them to MicroOCPP during initialization. After initialization, MicroOCPP uses the callbacks and executes them to fetch the most recent sensor values.
+This concept is reused for the data *Outputs* of the library to the firmware, where the callback applies output data from MicroOCPP to the firmware. +- **Transaction management**: OCPP considers EVSEs as vending machines. To enable payment processing and the billing of the EVSE usage, all charging activity is assigned to transactions. A big portion of OCPP is about transactions, their prerequisites, runtime and their termination scenarios. The MicroOCPP API breaks transactions down into an initiation and termination function and gives a transparent view on the current process status, authorization result and offline behavior strategy. For non-commercial setups, the transaction mechanism is the same but has only informational purposes. +- **Device management**: MicroOCPP implements the OCPP side of the device management operations. For the actual execution, the firmware needs to provide the charger-side implementations of the operations to MicroOCPP by passing handler functions to the API. For example, the OCPP server can restart the charger. Upon receipt of the request, MicroOCPP terminates the transactions and eventually triggers the system restart using the handler function which the firmware has provided through the high-level API. + +## Transaction safety + +Software in EVSEs needs to withstand hazardous operating conditions. EVSEs are located on the street or in garages where the WiFi or LTE signal strength is often weak, leading to long offline periods or where random power cuts can occur. In addition to that, the lack of process virtualization on microcontrollers means that a malfunction in one part of the firmware leads to the crash of all other parts. + +The transaction process of MicroOCPP is robust against random failures or resets. A minimal transaction log on the flash storage ensures that each operation on a transaction is fully executed. It will always result in a consistent state between the EVSE and the OCPP server, even over resets of the microcontroller. The RPC queue facilitates this by tracking the delivery status of relevant messages. If the microcontroller is reset while the delivery status of a message is unknown, MicroOCPP takes up the message delivery again at the next start up and completes it. + +A requirement for the transaction safety feature is the availability of a journaling file system. Examples include LittleFS, SPIFFS and the POSIX file API, but some microcontroller platforms don't support this natively, so an extension would be required. + +## Unit testing + +MicroOCPP includes a number of unit tests based on the [Catch2](https://github.com/catchorg/Catch2) framework. A [GitHub Action](https://github.com/matth-x/MicroOcpp/actions) runs the unit tests against each new commit in the MicroOCPP repository, which ensures that new features don't break old code. + +The scope of the unit tests is to to ensure a correct implementation of OCPP and to validate the high-level API against its definition. For that, it is not necessary to establish an actual test connection to an OCPP server. In fact, real-world communication would disturb the tests and make them undeterministic. That's why the test suite is fully based on an integrated, tiny OCPP test server which the OCPP client reaches over a loopback connection. The test suite does not access the WebSocket library. When making the unit tests of the main firmware, it is not necessary to check the full OCPP communication, but only to validate correct usage of the high-level API. An example of how the library can be initialized with a loopback connection can be found in its test suite. + +## Microcontroller optimization + +As a library for microcontrollers, the design of MicroOCPP considers the strict memory limits and complies with the best practices of embedded software development. Also, a few measures were taken to optimize the memory usage which include the spare inclusion of external libraries, an optimization of the internal data structures and the exclusion of C++ run-time type information (RTTI) and exceptions. Features of C++ which may have a larger footprint are carefully used such as the standard template library (STL) and lambda functions. The STL increases the robustness of the code and lambdas prove to be a powerful tool to deal with the complexity of asynchronous data processing in embedded systems. That's also why the high-level API has many functional parameters. + +Because of the high importance of C in the embedded world, MicroOCPP provides its high-level API in C too. It is typically simple to instruct the compiler to compile and link the C++-based library in a C-based firmware development. In case that the firmware requires custom features which are not part of the C-API, then the firmware can implement it in a new C++ source file, export the new functions to the C namespace and use it normally in the main source. + +While memory constraints are of concern, the execution time generally is not. OCPP is rather uncomplex on the algorithmic side for clients, since there is no need for elaborate planning algorithms or complex data transformations. + +Low resource requirements also allow new usage areas on top of EV charging. For example, MicroOCPP has been ported to ordinary IoT equipment such as Wi-Fi sockets to integrate further electric devices into OCPP networks. + +Although MicroOCPP is optimized for the usage on microcontrollers, it is also suitable for embedded Linux systems. With more memory available, the upper limits of the internal data structures can be increased, leading to a more versatile support of charging use cases. Also, the separation of the charger firmware into multiple processes can lead to more robustness. MicroOCPP can be extended by an inter-process communication (IPC) interface to run in a separate process. diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 00000000..8db223f8 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,54 @@ +# Migrating to v1.1 + +As a new minor version, all features should work the same as in v1.0 and existing integrations are mostly backwards compatible. However, some fixes / cleanup steps in MicroOCPP require syntactic changes or special consideration when upgrading from v1.0 to v1.1. The known pitfalls are as follows: + +- The default branch has been renamed from `master` into `main` +- Need to include extra headers: the transitive includes have been cleaned a bit. Probably it's necessary to add more includes next to `#include `. E.g.
`#include `
`#include ` +- `ocppPermitsCharge()` does not consider failures reported by the charger anymore. Before v1.1 it was possible to report failures to MicroOCPP using ErrorCodeInputs and then to rely on `ocppPermitsCharge()` becoming false when a failure occurs. For backwards compatibility, complement any occurence to `ocppPermitsCharge() && !isFaulted()` +- `setEnergyMeterInput` changed the expected return type of the callback function from `float` to `int` (see [#301](https://github.com/matth-x/MicroOcpp/pull/301)) +- The return type of the UnlockConnector handler also changed from `PollResult` to enum `UnlockConnectorResult` (see [#271](https://github.com/matth-x/MicroOcpp/pull/271)) + +If upgrading MicroOcppMongoose at the same time, then the following changes are very important to consider: + +- Certificates are no longer copied into heap memory, but the MO-Mongoose class takes the passed certificate pointer as a zero-copy parameter. The string behind the passed pointer must outlive the MO-Mongoose class (see [#10](https://github.com/matth-x/MicroOcppMongoose/pull/10)) +- WebSocket authorization keys are no longer stored as c-strings, but as `unsigned char` buffers. For backwards compatibility, a null-byte is still appended and the buffer can be accessed as c-string, but this should be tested in existing deployments. Furtermore, MicroOCPP only accepts hex-encoded keys coming via ChangeConfiguration which is mandated by the standard. This also may break existing deployments (see [#4](https://github.com/matth-x/MicroOcppMongoose/pull/4)). + +If accessing the MicroOCPP modules directly (i.e. not over `MicroOcpp.h` or `MicroOcpp_c.h`) then there are likely some more modifications to be done. See the history of pull requests where each change to the code is documented. However, if the existing integration compiles under the new MO version, then there shouldn't be too many unexpected incompatibilities. + +## Migrating to v1.0 + +The API has been continously improved to best suit the common use cases for MicroOCPP. Moreover, the project has been given a new name to prevent confusion with the relation to the Arduino platform and to reflect the project goals properly. With the new project name, the API has been frozen for the v1.0 release. + +### Adopting the new project name in existing projects + +Find and replace the keywords in the following. + +If using the C-facade (skip if you don't use anything from *ArduinoOcpp_c.h*): + +- `AO_Connection` to `OCPP_Connection` +- `AO_Transaction` to `OCPP_Transaction` +- `AO_FilesystemOpt` to `OCPP_FilesystemOpt` +- `AO_TxNotification` to `OCPP_TxNotification` +- `ao_set_console_out_c` to `ocpp_set_console_out_c` + +Change this in any case: + +- `ArduinoOcpp` to `MicroOcpp` +- `"AO_` to `"Cst_` (define build flag `MO_CONFIG_EXT_PREFIX="AO_"` to keep old config keys) +- `AO_` to `MO_` +- `ocpp_` to `mocpp_` + +Change this if used anywhere: + +- `ao_set_console_out` to `mocpp_set_console_out` +- `ao_tick_ms` to `mocpp_tick_ms` + +If using the C-facade, change this as the final step: + +- `ao_` to `ocpp_` + +### Further API changes to consider + +In addition to the new project name, the API has also been reworked for more consistency. After renaming the existing project as described above, also take a look at the [changelogs](https://github.com/matth-x/MicroOcpp/blob/1.0.x/CHANGELOG.md) (see Section Changed for v1.0.0). + +**If something is missing in this guide, please share the issue here:** [https://github.com/matth-x/MicroOcpp/issues/176](https://github.com/matth-x/MicroOcpp/issues/176) diff --git a/docs/modules.md b/docs/modules.md new file mode 100644 index 00000000..47d2abe2 --- /dev/null +++ b/docs/modules.md @@ -0,0 +1,56 @@ +# Modules + +This chapter gives an overview of the class structure of MicroOCPP. + +## Context + +The *Context* contains all runtime data of MicroOCPP. Every data object which this library creates is stored in the Context instance, except only the Configuration. So it is the basic entry point to the internals of the library. The structure of the context follows the main architecture as described in [this introduction](intro-tech) and consists of the Request queue and message deserializer for the RPC framework and the Model object for the OCPP model and behavior (see below). + +When the library is initialized, `getOcppContext()` returns the current Context object. + +## Model + +The *Model* represents the OCPP device model and behavior. OCPP defines a rough charger model, i.e. the hardware parts of the charger and their basic functionality in relation to the OCPP operations. Furthermore, OCPP specifies a few only software related features like the reservation of the charger. This charger model is implemented as straightforward C++ data structures and corresponding algorithms. + +The implementation of the Model is structured into a top-level Model class and the subordinate Service classes. Each Service class represents a functional block of the OCPP specification and implements the corresponding data structures and functionality. The definition of the functional blocks in MicroOCPP is very similar to the feature profiles in OCPP. Only the Core profile is split into multiple functional blocks to keep a smaller module scope. + +The following list contains the resulting functional blocks: + +- **Authorization**: local information of user identifiers and their authorization status +- **Boot**: implementation of the *preboot* behavior, i.e. sending and processing the BootNotification message +- **ChargingSession**: management of charging sessions and control of the high power charging hardware +- **Diagnostics**: GetDiagnostics upload routine +- **FirmwareManagement**: UpdateFirmware download routine +- **Heartbeat**: periodic OCPP Heartbeats (not including WebSocket ping-pongs) +- **Metering**: periodic MeterValue messages and local caching +- **Reservation**: management of Reservation lists and their effect on the authorization routine +- **Reset**: execution of OCPP Reset message +- **Transactions**: transaction journal behind StartTransaction and StopTransaction messages and *Transaction* class for extensions of the transaction mechanism + +## Requests + +The *Request* class and all similarly named classes implement the Remote Procdure Call (RPC) framework of OCPP. A request executes an operation on the remote end of an OCPP connection. If a charger sends a request to a server, then the server will update its data base with the payload and vice versa. After receiving a request, each node replies with a confirmation, acknowledging the successful execution of the operation or notifying about an error. + +When being offline, outgoing requests must be queued before sending which is implemented in *RequestQueue*. Queueing is especially challenging for longer offline periods when the number of cached messages exceeds the memory limit. To address this, messages are swapped to the flash memory when the queue limit is reached as implemented in the *RequestStore* and *RequestQueueStorageStrategy* class. Incoming messages can be processed directly and don't have an extensive queueing mechanism. + +## Operations + +Every OCPP operation (e.g. Heartbeat, BootNotification) has a dedicated class for creating outgoing messages, interpreting incoming messages, executing the specified OCPP action and handling responses. Operations work on the data structures of the Model layer. + +To send operations to the OCPP server, they must be wrapped into a Request object. The RPC framework and operations are separated modules. While the RPC framework (including the Request class) deals with the messaging mechanism and transfering data to the other OCPP device, operations define the effect on the OCPP model and data structure and execute the desired action. The operation classes inherit from *Operation* which is the interface visible to the Request class. + +Incoming messages are unmarshalled using the *OperationRegistry*. During the initialization phase of the library, the Model classes register all supported operations with their name and an instantiator. The instantiator, when executed, provides the Request interpreter with an instance of the corresponding Operation subclasses. It is possible to extend MicroOCPP by adding new Operation instantiators to the registry, or to modify the behavior by overriding the default Operation implementations. In addition to that, event handlers can be set which the RPC queue will notify with the payload once operations are sent or received. + +## Configuration + +Configurations like the HeartbeatInterval are managed by the *Configuration* module which consists of + +- *AbstractConfiguration*: a single configuration as a key-value pair without value type + - *Configuration*: a concrete configuration with a value type like `bool` or `const char*`. Inherits from AbstractConfiguration +- *ConfigurationContainer*: a collection of AbstractConfigurations and an optional storage implementation. Multiple containers can be set for a separation of the configurations and different storage strategies. Each container has a unique file name + - *ConfigurationContainerVolatile*: no persistency and access to the file system + - *ConfigurationContainerFlash*: persistency by storing JSON files on the flash + +If another storage implementation is required (e.g. for syncing with an external configuration manager), then it's possible to add a custom ConfigurationContainer. + +In the initialization phase, MicroOCPP loads the built-in Configurations with hard-coded factory defaults and a default storage structure. To customize the factory defaults or which ConfigurationContainers will be used, the Configuration module must be initialized before loading the library. To do so, call `configuration_init(...)`. Then the factory defaults can be applied by calling `declareConfiguration(...)` with the desired default value. To use a custom ConfigurationContainer, call `addConfigurationContainer(...)` with the custom implementation. When the library is loaded afterwards, it will use the previously provided Configurations / Containers and create only the data structure which hasn't been set already. diff --git a/docs/prerequisites.md b/docs/prerequisites.md new file mode 100644 index 00000000..4cc63613 --- /dev/null +++ b/docs/prerequisites.md @@ -0,0 +1,33 @@ +# Development tools and basic prerequisites + +This page explains how to work with this library using the appropriate development tools. Skip it if your IDE is set up and you already have an OCPP test server. + +## Development tools prerequisites + +Throughout these document pages, it is assumed that you already have set up your development environment and that you are familiar with the corresponding building, flashing and (basic) debugging routines. MicroOCPP runs in many environments (from the Arduino-IDE to proprietary microcontroller IDEs like the Code Composer Studio). If you do not have any preferences yet, it is highly recommended to get started with VSCode + the PlatformIO add-on, since it is the favorite setup of the community and therefore you find the most related information in the Issues pages of the main repository. + +There are many high-quality tutorials for out there for setting up VSCode + PIO. The following site covers everything you need to know: +[https://randomnerdtutorials.com/vs-code-platformio-ide-esp32-esp8266-arduino/](https://randomnerdtutorials.com/vs-code-platformio-ide-esp32-esp8266-arduino/) + +Once that's done, adding MicroOCPP is no big deal anymore. However, let's discuss another very important tool for your project first. + +## OCPP Server prerequisites + +MicroOCPP is just a client, but all the magic of OCPP lives in the communication between a client and a server. Although it *is* possible to run MicroOCPP without a real server for testing purposes, the best approach for getting started is to get the hands on a real server. So you can always use the client in a practical setup, see immediate results and simplify development a lot. + +Perhaps you were already given access to an OCPP server for your project. Then you can use that, it should work fine. If you don't have a server already, it is highly recommended to get +SteVe ([https://github.com/steve-community/steve](https://github.com/steve-community/steve)). +It allows to control every detail of the OCPP operations and shows detail-rich information about the results. And again, it is the favorite test server of the community, so you will find the most related information on the Web. For the installation instructions, please refer to the +[SteVe docs](https://github.com/steve-community/steve#configuration-and-installation). + +In case you can't wait to get started, you can make the first connection test with a WebSocket echo server as a fake OCPP service. MicroOCPP supports that: it can send all messages to an echo server which reflects all traffic. MicroOCPP gets back its own messages and replies to itself with mocked responses. Complicated, but it does work and the console will show a valid OCPP communication. An example echo server is given in the following section. For the further development though, you will definitely need a real OCPP server. + +## Project structure + +MicroOCPP is a library, i.e. it is not a full firmware, but just solves one specific task in your project which is the OCPP connectivity. The project structure should reflect this: typically you download MicroOCPP into a libraries or dependencies subfolder, while the main part of the development takes place in a main source folder. All dependencies of MicroOCPP (i.e. ArduinoJson, see the dependencies sections) should be located in the same libraries or dependencies folder. + +When the include paths are correctly set up, you should be able `#include ` at the top of your own source files. This setup keeps the OCPP library source separate from your integration and gives the project a clear structure. + +## Dependency managers + +Currently, the PlatformIO dependency manager is supported. In the `platformio.ini` manifest, you can add `matth-x/MicroOcpp` to the `lib_deps` section. diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 00000000..d4838580 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,48 @@ +# Security + +MicroOCPP is designed to be compatible with IoT devices which leads to special considerations regarding cyber security. This section describes the challenges and security concepts of MicroOCPP. + +## Challenges of using microcontrollers in safety-critical environments + +The two challenges are as follows: + +1. Lack of process virtualization in RTOS operating systems +2. Less attention for potential vulnerabilities in the used libraries + +In a general purpose OS like Linux, the internet communication modules of an application typically run in a different process than the data base or the hardware supervision / control function. In contrast, on a typical RTOS, all modules are compiled into the same binary, sharing the same address space and lifecycle when being executed. This means that once the network stack crashes, all software on the chip is reset and a vulnerability on the network stack could be exploited to read or manipulate the data of the full runtime environment. + +Challenge 2) is due to the fact that OCPP uses standard web technology (WebSocket over TLS), but microcontrollers are missing out the most widespread networking software like OpenSSL or the networking libraries of Linux. The available networking libraries for microcontrollers are also audited well (e.g. lwIP, mbedTLS), but in general there is more attention on potential vulnerabilites in the Linux world, because a huge share of commercial IT systems is based on Linux. + +On the upside, an advantage of microcontrollers is their single purpose usage and thus, reduced complexity. Many security breaches are caused by misconfigured and often even superflous software components (e.g. due to overlooked open ports) which are not a regular part of a microcontroller firmware. + +## Security measures of MicroOCPP + +To address the challenges, the following measures were taken: + +- Input sanitazion: MicroOCPP only accepts the JSON format for all input. It is validated by ArduinoJson. Every JSON value is checked against the expected format and for conformity with the OCPP specification before using it. The JSON object is discarded immediately after interpretation +- Transaction safety: to address crashes and random reboots of the microcontroller during operation, all activities of the OCPP library are programmed so that they will either be resumed or fully reverted after reboots, preventing inconsistent states. See also [Transaction safety](../intro-tech/#transaction-safety) +- Careful choice of the dependencies: the mandatory dependency, [ArduinoJson](https://github.com/bblanchon/ArduinoJson), has a test coverage of nearly 100% and is fuzzed. The same goes for the recommended WebSocket library, [Mongoose](https://github.com/cesanta/mongoose). Both projects are very relevant in their field with over 6k and 9k stars on GitHub + +Two further measures would be beneficial and could be requested via support request: + +- Precautious memory allocation: migrating memory management to the stack and where possible would simplify code analysis and reduce the potential of vulnerabilities +- OCPP fuzzer: as a stateful application protocol, there are specific challenges of developing a fuzzer. An open source fuzzing framework for OCPP could reveal vulnerabilities and be of use for other OCPP projects as well. MicroOCPP is a good foundation for trying new fuzzing approaches. The exposure of the main-loop function and the clock allow a fine-grained access to the program flow and facilitating random alterations of the environment conditions. Furthermore, all persistent data is stored in the JSON format and it is possible to develop a grammatic which contains both a device status and incoming OCPP messages. The Configuration interface could be reused for further status variables which don't need to be persistent in practice, but would improve fuzzing performance when being accessible by the fuzzer. +- Memory pool: object orientation is a very helpful programming paradigm for OCPP. The standard contains a lot of polymorphic entities and optional or variable length data fields. MicroOCPP makes use of the heap and allocates new chunks of memory as the device model is populated with data. On the upside this allows to save a lot of memory during normal operation, but it also entails the risk of memory depletion of the whole controller. A fixed memory pool for OCPP would encapsulate the heap usage to a certain address space and set a hard limit for the memory consumption and avoid polluting the shared heap area by heap fragmentation. To realize the memory pool, it would be necessary to make the allocate and deallocate functions configurable by the client code. Then appropriate (de)allocators can be injected limiting the memory use to a restricted address area. As a consequence, a more thorough allocation error handling in the MicroOCPP code is required and test cases which randomly suppress allocations to test if the library always reverts to a consistent state. A less invasive alternative to memory pools is to inject measured (de)allocators which just prevent the allocation of new memory chunks after a certain threshold has been exceeded. This programming technique would also allow to create much more fine-grained benchmarks of the library. + +## Measures to be taken by the EVSE vendor + +As a general rule, the communication controller which is exposed to the internet shouldn't be used for safety-critical tasks on the charging hardware. That's because the networking stack is a very complex piece of software which very likely still has open bugs which can crash the controller despite all the effort to improve it. Safety-critical tasks on the charging hardware shouldn't rely on a controller which could crash at any time because of incoming network traffic. To mitigate this, either the OCPP library and internet functionality should be placed onto a separate chip, or the most vital safety functionality should get a dedicated controller. + +The recommended [Mongoose WebSocket adapter for MicroOCPP](https://github.com/matth-x/MicroOcppMongoose) supports the OCPP Security Profile 2 (TLS with Basic Authentication) and needs to be provided with the necessary TLS certificate. + +Most IoT-controllers have built-in mechanisms to ensure the authenticity of their firmware. For example, the Espressif32 supports [Secure Boot](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/secure-boot-v2.html) which is a signature verification of the installed firmware before that firmware is executed. Many platforms also have a built-in signature verification for incoming OTA firmware updates. To prove the authenticity of the charger to the OCPP server, it is also important to keep the WebSocket key secret by [encrypting the flash memory](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/flash-encryption.html). These security mechanisms heavily depend on the host controller which runs MicroOCPP. It is the responsibility of the main firmware to make proper use of them. + +## OCPP Security Whitepaper and ISO 15118 + +With MicroOCPP, the recommended way of handling certificates on microcontrollers is to compile them into the firmware binary and to rely on the built-in firmware signature checks of the host microcontroller platform. This lean approach results in a smaller attack vector compared to establishing a separate infrastructure for the server- and firmware certificate. It can be assumed that the OTA functionality of the microcontrollers is thoroughly tested and consequently, reaching a comparable level of robustness would require much effort. + +In case the certificate handling mechanism of the Security Whitepaper is preferred, then the EVSE vendor needs to implement it via a custom extension. Unfortunately, this mechanism hasn't been requested yet and is not natively supported by MicroOCPP yet. The new custom operations can be implemented by extending the class `Operation`. A handler for incoming messages can be registered via `OperationRegistry::registerOperation(...)`. To send custom messages to the server, use `Context::initiateRequest(...)`. + +A further challenge for microcontrollers is the relatively low processor speed which becomes relevant for a potential ISO 15118 integration. Some incoming message types (`AuthorizationReq` and `MeteringReceiptReq`) include a signature which needs to be verified on the communications controller of the EVSE. Moreover, messages in the ISO 15118 V2G protocol have a maximum round trip time (which is 2 seconds for the message types in question) and so the signature verification is time-contrained. [These benchmarks](https://web.archive.org/web/20230724184529/https://www.oryx-embedded.com/benchmark/espressif/crypto-esp32.html) for the Espressif32 show that for some signature algorithms, the verification time can get close or exceed the timing requirements of ISO 15118 if done on the processor only. As a consequence, hardware acceleration by the crypto-core is mandatory to ensure a robust communication between the EVSE and EV. Before making a communications controller with ISO 15118 support, the performance of the host controller should be benchmarked and checked against the requirements. + +*Disclaimer: the outlined risks in this section are not a complete list. Also, every system has unique security challenges which require individual attention. In doubt, please consult an IT-security specialist.* diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 00000000..fc5e6a38 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,3 @@ +:root { + --md-primary-fg-color: #2984C7; + } diff --git a/examples/ESP-IDF/CMakeLists.txt b/examples/ESP-IDF/CMakeLists.txt index 5358fdcb..63fe414b 100644 --- a/examples/ESP-IDF/CMakeLists.txt +++ b/examples/ESP-IDF/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.5) include($ENV{IDF_PATH}/tools/cmake/project.cmake) -project(ao_example) +project(mo_example) -idf_build_set_property(COMPILE_OPTIONS -DAO_TRAFFIC_OUT APPEND) +idf_build_set_property(COMPILE_OPTIONS -DMO_TRAFFIC_OUT APPEND) +idf_build_set_property(COMPILE_OPTIONS -DMO_FILENAME_PREFIX="/mo_store/" APPEND) diff --git a/examples/ESP-IDF/Makefile b/examples/ESP-IDF/Makefile index 3ad0575f..e6da8227 100644 --- a/examples/ESP-IDF/Makefile +++ b/examples/ESP-IDF/Makefile @@ -3,6 +3,6 @@ # project subdirectory. # -PROJECT_NAME := ao_example +PROJECT_NAME := mo_example include $(IDF_PATH)/make/project.mk diff --git a/examples/ESP-IDF/README.md b/examples/ESP-IDF/README.md index e62dce59..7b6a8df0 100644 --- a/examples/ESP-IDF/README.md +++ b/examples/ESP-IDF/README.md @@ -1,31 +1,40 @@ # ESP-IDF integration example -To run ArduinoOcpp on the ESP-IDF platform, please take this example as the starting point. It is widely based on the [Wi-Fi Station Example](https://github.com/espressif/esp-idf/tree/release/v4.4/examples/wifi/getting_started/station) of Espressif. This example works with the ESP-IDF version `4.4`. For a general guide how to setup and use the ESP-IDF, please refer to the documentation of Espressif. +To run MicroOcpp on the ESP-IDF platform, please take this example as the starting point. It is widely based on the [Wi-Fi Station Example](https://github.com/espressif/esp-idf/tree/release/v4.4/examples/wifi/getting_started/station) of Espressif. This example works with the ESP-IDF version `4.4`. For a general guide how to setup and use the ESP-IDF, please refer to the documentation of Espressif. ## Setup guide ### Dependencies -Please clone the following repositories into the respective components-directory: +Please clone the following repositories into the respective components-directories: +- [MicroOcpp](https://github.com/matth-x/MicroOcpp) into `components/MicroOcpp` - [Mongoose (ESP-IDF integration)](https://github.com/cesanta/mongoose-esp-idf) into `components/mongoose` -- [ArduinoOcpp](https://github.com/matth-x/ArduinoOcpp) into `components/ArduinoOcpp` -- [Mongoose adapter for ArduinoOcpp](https://github.com/matth-x/AOcppMongoose) into `components/AOcppMongoose` - -The following header-only library needs to go into the include-section: - -- [ArduinoJson header file](https://github.com/bblanchon/ArduinoJson/releases/download/v6.19.4/ArduinoJson-v6.19.4.h), renamed and moved to `include/ArduinoJson.h` +- [Mongoose adapter for MicroOcpp](https://github.com/matth-x/MicroOcppMongoose) into `components/MicroOcppMongoose` +- [ArduinoJson (v6.21)](https://github.com/bblanchon/ArduinoJson) into `components/ArduinoJson` For setup, the following commands could come handy (change to the root directory of the ESP-IDF project first): ``` rm components/mongoose/.gitkeep -rm components/ArduinoOcpp/.gitkeep -rm components/AOcppMongoose/.gitkeep +rm components/MicroOcpp/.gitkeep +rm components/MicroOcppMongoose/.gitkeep +rm components/ArduinoJson/.gitkeep +git clone https://github.com/matth-x/MicroOcpp components/MicroOcpp git clone --recurse-submodules https://github.com/cesanta/mongoose-esp-idf.git components/mongoose -git clone https://github.com/matth-x/ArduinoOcpp components/ArduinoOcpp -git clone https://github.com/matth-x/AOcppMongoose components/AOcppMongoose -wget -Uri https://github.com/bblanchon/ArduinoJson/releases/download/v6.19.4/ArduinoJson-v6.19.4.h -O ./include/ArduinoJson.h +git clone https://github.com/matth-x/MicroOcppMongoose components/MicroOcppMongoose +git clone https://github.com/bblanchon/ArduinoJson components/ArduinoJson +cd components/ArduinoJson +git checkout 3e1be980d93e47b2a0073efeeb9a9396fd7a83be +``` + +The setup is done if the following include statements work: + +```cpp +#include +#include +#include +#include ``` ### Configure the project @@ -42,4 +51,3 @@ In the `Example Configuration` menu: * Set `AuthorizationKey`, or leave empty if not necessary Optional: If you need, change the other options according to your requirements. - diff --git a/examples/ESP-IDF/components/AOcppMongoose/.gitkeep b/examples/ESP-IDF/components/ArduinoJson/.gitkeep similarity index 100% rename from examples/ESP-IDF/components/AOcppMongoose/.gitkeep rename to examples/ESP-IDF/components/ArduinoJson/.gitkeep diff --git a/examples/ESP-IDF/components/ArduinoOcppMongoose/.gitkeep b/examples/ESP-IDF/components/ArduinoOcppMongoose/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/ESP-IDF/components/README.md b/examples/ESP-IDF/components/README.md index 4cd447ac..b4acfa92 100644 --- a/examples/ESP-IDF/components/README.md +++ b/examples/ESP-IDF/components/README.md @@ -2,8 +2,9 @@ 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) -- [ArduinoOcpp](https://github.com/matth-x/ArduinoOcpp) -- [Mongoose adapter for ArduinoOcpp](https://github.com/matth-x/AOcppMongoose) +- [Mongoose adapter for MicroOcpp](https://github.com/matth-x/MicroOcppMongoose) +- [ArduinoJson (v6.21)](https://github.com/bblanchon/ArduinoJson) This example only provides the folder structure. You need to clone every project into it in order to run the example. diff --git a/examples/ESP-IDF/include/README.md b/examples/ESP-IDF/include/README.md deleted file mode 100644 index 5e098b2f..00000000 --- a/examples/ESP-IDF/include/README.md +++ /dev/null @@ -1,9 +0,0 @@ -## Includes - -[ArduinoJson](https://github.com/bblanchon/ArduinoJson) is the JSON (de)serializer of ArduinoOcpp and a dependency. It is a header-only library and can go into the `include` folder. Any other location will work too as long as the header file `ArduinoJson.h` is on the include-path, i.e. the following `#include` works: - -```cpp -#include -``` - -Version `6.19.4` is required. You can download [this header file](https://github.com/bblanchon/ArduinoJson/releases/download/v6.19.4/ArduinoJson-v6.19.4.h), rename it into `ArduinoJson.h` and move it here. diff --git a/examples/ESP-IDF/main/Kconfig.projbuild b/examples/ESP-IDF/main/Kconfig.projbuild index 420f3095..32aa3a1e 100644 --- a/examples/ESP-IDF/main/Kconfig.projbuild +++ b/examples/ESP-IDF/main/Kconfig.projbuild @@ -18,19 +18,19 @@ menu "Example Configuration" help Set the Maximum retry to avoid station reconnecting to the AP unlimited when the AP is really inexistent. - config AO_OCPP_BACKEND + config MO_OCPP_BACKEND string "OCPP backend URL" default "ws://echo.websocket.events/" help URL of the OCPP backend - config AO_CHARGEBOXID + config MO_CHARGEBOXID string "ChargeBoxId" default "" help ChargeBoxId as it appears in the WebSocket connection URL - config AO_AUTHORIZATIONKEY + config MO_AUTHORIZATIONKEY string "Authorization Key" default "" help diff --git a/examples/ESP-IDF/main/main.c b/examples/ESP-IDF/main/main.c index 14b7b005..b0911702 100644 --- a/examples/ESP-IDF/main/main.c +++ b/examples/ESP-IDF/main/main.c @@ -16,10 +16,10 @@ #include "lwip/err.h" #include "lwip/sys.h" -/* ArduinoOcpp includes */ +/* MicroOcpp includes */ #include -#include //C-facade of ArduinoOcpp -#include //WebSocket integration for ESP-IDF +#include //C-facade of MicroOcpp +#include //WebSocket integration for ESP-IDF /* The examples use WiFi configuration that you can set via project configuration menu @@ -29,9 +29,9 @@ #define EXAMPLE_ESP_WIFI_SSID CONFIG_ESP_WIFI_SSID #define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD #define EXAMPLE_ESP_MAXIMUM_RETRY CONFIG_ESP_MAXIMUM_RETRY -#define EXAMPLE_AO_OCPP_BACKEND CONFIG_AO_OCPP_BACKEND -#define EXAMPLE_AO_CHARGEBOXID CONFIG_AO_CHARGEBOXID -#define EXAMPLE_AO_AUTHORIZATIONKEY CONFIG_AO_AUTHORIZATIONKEY +#define EXAMPLE_MO_OCPP_BACKEND CONFIG_MO_OCPP_BACKEND +#define EXAMPLE_MO_CHARGEBOXID CONFIG_MO_CHARGEBOXID +#define EXAMPLE_MO_AUTHORIZATIONKEY CONFIG_MO_AUTHORIZATIONKEY /* FreeRTOS event group to signal when we are connected*/ static EventGroupHandle_t s_wifi_event_group; @@ -148,31 +148,29 @@ void app_main(void) ESP_LOGI(TAG, "ESP_WIFI_MODE_STA"); wifi_init_sta(); - /* Initialize Mongoose (necessary for ArduinoOcpp)*/ + /* Initialize Mongoose (necessary for MicroOcpp)*/ struct mg_mgr mgr; // Event manager mg_mgr_init(&mgr); // Initialise event manager mg_log_set(MG_LL_DEBUG); // Set log level - /* Initialize ArduinoOcpp */ - struct AO_FilesystemOpt fsopt = { .use = true, .mount = true, .formatFsOnFail = true}; + /* Initialize MicroOcpp */ + struct OCPP_FilesystemOpt fsopt = { .use = true, .mount = true, .formatFsOnFail = true}; - AOcppSocket *osock = ao_makeOcppSocket(&mgr, - EXAMPLE_AO_OCPP_BACKEND, - EXAMPLE_AO_CHARGEBOXID, - EXAMPLE_AO_AUTHORIZATIONKEY, "", fsopt); - ao_initialize(osock, 230.f /* European grid voltage */, fsopt); - - ao_bootNotification("ESP-IDF charger", "Your brand name here", NULL, NULL, NULL, NULL); //send first OCPP message + OCPP_Connection *osock = ocpp_makeConnection(&mgr, + EXAMPLE_MO_OCPP_BACKEND, + EXAMPLE_MO_CHARGEBOXID, + EXAMPLE_MO_AUTHORIZATIONKEY, "", fsopt); + ocpp_initialize(osock, "ESP-IDF charger", "Your brand name here", fsopt, false, false); /* Enter infinite loop */ while (1) { mg_mgr_poll(&mgr, 10); - ao_loop(); + ocpp_loop(); } /* Deallocate ressources */ - ao_deinitOcppSocket(osock); - ao_deinitialize(); + ocpp_deinitialize(); + ocpp_deinitConnection(osock); mg_mgr_free(&mgr); return; } diff --git a/examples/ESP-IDF/partitions.csv b/examples/ESP-IDF/partitions.csv index d521f8bf..a9ab150b 100644 --- a/examples/ESP-IDF/partitions.csv +++ b/examples/ESP-IDF/partitions.csv @@ -4,4 +4,4 @@ otadata, data, ota, , 0x2000, phy_init, data, phy, , 0x1000, ota_0, app, ota_0, , 0x190000, ota_1, app, ota_1, , 0x190000, -ao, data, spiffs, , 0xD0000, \ No newline at end of file +mo, data, spiffs, , 0xD0000, \ No newline at end of file diff --git a/examples/ESP-IDF/sdkconfig b/examples/ESP-IDF/sdkconfig index 4e8f5df7..52bcb0b6 100644 --- a/examples/ESP-IDF/sdkconfig +++ b/examples/ESP-IDF/sdkconfig @@ -137,9 +137,9 @@ CONFIG_PARTITION_TABLE_MD5=y CONFIG_ESP_WIFI_SSID="myssid" CONFIG_ESP_WIFI_PASSWORD="mypassword" CONFIG_ESP_MAXIMUM_RETRY=5 -CONFIG_AO_OCPP_BACKEND="ws://echo.websocket.events/" -CONFIG_AO_CHARGEBOXID="" -CONFIG_AO_AUTHORIZATIONKEY="" +CONFIG_MO_OCPP_BACKEND="ws://echo.websocket.events/" +CONFIG_MO_CHARGEBOXID="" +CONFIG_MO_AUTHORIZATIONKEY="" # end of Example Configuration # diff --git a/examples/ESP-TLS/main.cpp b/examples/ESP-TLS/main.cpp index 0bb11e53..7c4c7c1b 100644 --- a/examples/ESP-TLS/main.cpp +++ b/examples/ESP-TLS/main.cpp @@ -1,5 +1,5 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -13,26 +13,19 @@ ESP8266WiFiMulti WiFiMulti; #error only ESP32 or ESP8266 supported at the moment #endif -#include -#include //need for setting TLS credentials +#include -#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 {}; -ArduinoOcpp::EspWiFi::OcppClientSocket 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,28 +100,23 @@ 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//////////////// - OCPP_initialize(osockSecure); + 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 ArduinoOcpp.h for how to integrate the EVSE hardware. + * ... 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 - */ - - /* - * Notify the Central System that this station is ready + * see the other examples */ - bootNotification("My Charging Station", "My company name"); } void loop() { @@ -137,7 +124,7 @@ void loop() { /* * Execute all charge point routines and handle WebSocket */ - OCPP_loop(); + mocpp_loop(); - //... see ArduinoOcpp.h and the other examples for how to integrate the EVSE hardware. + //... see MicroOcpp.h and the other examples for how to integrate the EVSE hardware. } diff --git a/examples/ESP/main.cpp b/examples/ESP/main.cpp index d1149bc2..306e700b 100644 --- a/examples/ESP/main.cpp +++ b/examples/ESP/main.cpp @@ -1,5 +1,5 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -13,21 +13,19 @@ ESP8266WiFiMulti WiFiMulti; #error only ESP32 or ESP8266 supported at the moment #endif -#include +#include #define STASSID "YOUR_WIFI_SSID" #define STAPSK "YOUR_WIFI_PW" -#define OCPP_HOST "echo.websocket.events" -#define OCPP_PORT 80 -#define OCPP_URL "ws://echo.websocket.events/" +#define OCPP_BACKEND_URL "ws://echo.websocket.events" +#define OCPP_CHARGE_BOX_ID "" // // Settings which worked for my SteVe instance: // -//#define OCPP_HOST "my.instance.com" -//#define OCPP_PORT 80 -//#define OCPP_URL "ws://my.instance.com/steve/websocket/CentralSystemService/esp-charger" +//#define OCPP_BACKEND_URL "ws://192.168.178.100:8180/steve/websocket/CentralSystemService" +//#define OCPP_CHARGE_BOX_ID "esp-charger" void setup() { @@ -60,7 +58,7 @@ void setup() { /* * Initialize the OCPP library */ - OCPP_initialize(OCPP_HOST, OCPP_PORT, OCPP_URL); + mocpp_initialize(OCPP_BACKEND_URL, OCPP_CHARGE_BOX_ID, "My Charging Station", "My company name"); /* * Integrate OCPP functionality. You can leave out the following part if your EVSE doesn't need it. @@ -70,7 +68,7 @@ void setup() { return 0.f; }); - setSmartChargingOutput([](float limit) { + setSmartChargingCurrentOutput([](float limit) { //set the SAE J1772 Control Pilot value here Serial.printf("[main] Smart Charging allows maximum charge rate: %.0f\n", limit); }); @@ -80,12 +78,7 @@ void setup() { return false; }); - //... see ArduinoOcpp.h for more settings - - /* - * Notify the Central System that this station is ready - */ - bootNotification("My Charging Station", "My company name"); + //... see MicroOcpp.h for more settings } void loop() { @@ -93,10 +86,10 @@ void loop() { /* * Do all OCPP stuff (process WebSocket input, send recorded meter values to Central System, etc.) */ - OCPP_loop(); + mocpp_loop(); /* - * Check internal OCPP state and bind EVSE hardware to it + * Energize EV plug if OCPP transaction is up and running */ if (ocppPermitsCharge()) { //OCPP set up and transaction running. Energize the EV plug here @@ -105,43 +98,41 @@ void loop() { } /* - * Detect if something physical happened at your EVSE and trigger the corresponding OCPP messages + * Use NFC reader to start and stop transactions */ if (/* RFID chip detected? */ false) { String idTag = "0123456789ABCD"; //e.g. idTag = RFID.readIdTag(); - if (!getTransactionIdTag()) { - //no idTag registered yet. Start a new transaction - - authorize(idTag.c_str(), [idTag] (JsonObject response) { - //check if user with idTag is authorized - if (!strcmp("Accepted", response["idTagInfo"]["status"] | "Invalid")){ - Serial.println(F("[main] User is authorized to start a transaction")); - - auto ret = beginTransaction(idTag.c_str()); //begin Tx locally - - if (ret) { - Serial.println(F("[main] Transaction initiated. StartTransaction will be sent when ConnectorPlugged Input becomes true")); - } else { - Serial.println(F("[main] No transaction initiated")); - } - } else { - Serial.printf("[main] Authorize denied. Reason: %s\n", response["idTagInfo"]["status"] | ""); - } - }); - Serial.printf("[main] Authorizing user with idTag %s\n", idTag.c_str()); + if (!getTransaction()) { + //no transaction running or preparing. Begin a new transaction + Serial.printf("[main] Begin Transaction with idTag %s\n", idTag.c_str()); + + /* + * Begin Transaction. The OCPP lib will prepare transaction by checking the Authorization + * and listen to the ConnectorPlugged Input. When the Authorization succeeds and an EV + * is plugged, the OCPP lib will send the StartTransaction + */ + auto ret = beginTransaction(idTag.c_str()); + + if (ret) { + Serial.println(F("[main] Transaction initiated. OCPP lib will send a StartTransaction when" \ + "ConnectorPlugged Input becomes true and if the Authorization succeeds")); + } else { + Serial.println(F("[main] No transaction initiated")); + } + } else { //Transaction already initiated. Check if to stop current Tx by RFID card if (idTag.equals(getTransactionIdTag())) { //card matches -> user can stop Tx Serial.println(F("[main] End transaction by RFID card")); - endTransaction(); + endTransaction(idTag.c_str()); } else { Serial.println(F("[main] Cannot end transaction by RFID card (different card?)")); } } } - //... see ArduinoOcpp.h for more possibilities + //... see MicroOcpp.h for more possibilities } diff --git a/examples/SECC/README.md b/examples/SECC/README.md deleted file mode 100644 index 8543d558..00000000 --- a/examples/SECC/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Simple EVSE OCPP communication controller - -With this sketch you can use an ESP8266/ESP32 as the central control unit of an EVSE. It controls the HW peripherals of the EVSE and connects the charge point to an OCPP server. When booting up the EVSE, it opens a maintenance web dashboard for setting the Wi-Fi and OCPP credentials (thanks to tzapu's WiFiManager). - -## Architecture - -The communication controller can connect to an SAE J1772 driver using the GPIO pins of the ESP. Furthermore, it can show the online status and charge point availability status via status LEDs. Displays, RFID readers or energy meters are not supported, though for your project you can integrate further HW components of course. - -You can find the interface descriptions in `main.cpp`. Please be aware that although this program modulates the maximum charge rate with PWM analogous to the SAE J1772 standard, the PWM signal does not reflect the EVSE state. The latter is encoded via the other GPIO pins. - -## Usage - -- Create an empty project in the PlatformIO IDE. -- Copy the `platformio.ini` from this directory to your root directory. Alternatively, just add `https://github.com/tzapu/WiFiManager.git` to the lib_deps of your `platformio.ini`. -- Copy the `main.cpp` from this example folder into your source directory. Adapt the pinout settings if needed. -- Finished. You should be able to compile and upload the sketch onto your ESP. - -When booting, the ESP opens up the Wi-Fi configuration portal. Please connect your PC to the network with the SSID `EVSE-Config` within the first 30s of the boot routine of the ESP (the portal has a timeout). The passphrase is `evse1234`. - -## Standalone mode - -If you just want to check out ArduinoOcpp you can run this sketch on an ESP without any peripherals. The pinout of the ESP8266-NodeMCU board is already fully configured in `main.cpp` so that board is the most convenient to start testing. diff --git a/examples/SECC/main.cpp b/examples/SECC/main.cpp deleted file mode 100644 index d1c3aaa1..00000000 --- a/examples/SECC/main.cpp +++ /dev/null @@ -1,495 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License -// -// -// IMPORTANT: Please add https://github.com/tzapu/WiFiManager.git to the lib_deps of your project! -// -// This sketch demonstrates a complete OCPP Wi-Fi module of an EVSE. You can flash -// an ESP8266/ESP32 with this program, build it into your EVSE and start charging with -// OCPP-connectivity. -// -// The pin mapping and additional comments can be found in this file. You probably need -// to adapt a few things before deployment. -// - - -#include -#include //please add to lib_deps: https://github.com/tzapu/WiFiManager.git - -#include -#include //load and save settings of WiFi captive portal - -/* - * Pin mapping part I - * Interface to the SECC (Supply Equipment Communication Controller, e.g. the SAE J1772 module) - */ -#define AMPERAGE_PIN 4 //modulated as PWM -#if defined(ESP32) -#define PWM_CHANNEL 0 //PWM channel (only for ESP32) -#endif - -#define EV_PLUG_PIN 14 // Input pin | Read if an EV is connected to the EVSE -#if defined(ESP32) -#define EV_PLUGGED HIGH -#define EV_UNPLUGGED LOW -#elif defined(ESP8266) -#define EV_PLUGGED LOW -#define EV_UNPLUGGED HIGH -#endif - -#define OCPP_CHARGE_PERMISSION_PIN 5 // Output pin | Signal if OCPP allows / forbids energy flow -#define OCPP_CHARGE_PERMITTED HIGH -#define OCPP_CHARGE_FORBIDDEN LOW - -#define EV_CHARGE_PIN 12 // Input pin | Read if EV requests energy (corresponds to SAE J1772 State C) -#if defined(ESP32) -#define EV_CHARGING LOW -#define EV_SUSPENDED HIGH -#elif defined(ESP8266) -#define EV_CHARGING HIGH -#define EV_SUSPENDED LOW -#endif - -#define OCPP_AVAILABILITY_PIN 0 // Output pin | Signal if this EVSE is out of order (set by Central System) -#define OCPP_AVAILABLE HIGH -#define OCPP_UNAVAILABLE LOW - -#define EVSE_GROUND_FAULT_PIN 13 // Input pin | Read ground fault detector -#define EVSE_GROUND_FAULTED HIGH -#define EVSE_GROUND_CLEAR LOW - -/* - * Pin mapping part II - * Internal LED of ESP8266/ESP32 + additional ESP8266 development board LED - */ - -#define SERVER_CONNECT_LED 16 // Output pin | Signal if connection to OCPP server has succeeded -#define SERVER_CONNECT_ON LOW -#define SERVER_CONNECT_OFF HIGH - -#define CHARGE_PERMISSION_LED 2 // Output pin | Signal if OCPP allows / forbids energy flow -#if defined(ESP32) -#define CHARGE_PERMISSION_ON HIGH -#define CHARGE_PERMISSION_OFF LOW -#elif defined(ESP8266) -#define CHARGE_PERMISSION_ON LOW -#define CHARGE_PERMISSION_OFF HIGH -#endif - -#if !defined(SECC_NO_DEBUG) -#define PRINT(...) Serial.print(__VA_ARGS__) -#define PRINTF(...) Serial.printf(__VA_ARGS__) -#define PRINTLN(...) Serial.println(__VA_ARGS__) -#else -#define PRINT(...) -#define PRINTF(...) -#define PRINTLN(...) -#endif - -WebSocketsClient wSock; -ArduinoOcpp::EspWiFi::OcppClientSocket oSock{&wSock}; - -int evPlugged = EV_UNPLUGGED; - -bool booted = false; -ulong scheduleReboot = 0; //0 = no reboot scheduled; otherwise reboot scheduled in X ms -ulong reboot_timestamp = 0; //timestamp of the triggering event; if scheduleReboot=0, the timestamp has no meaning - -// ============ CAPTIVE PORTAL -#define CAPTIVE_PORTAL_TIMEOUT 60 //in seconds -#define WIFI_CONNECTION_TIMEOUT 30 //in seconds - -struct Ocpp_URL { - bool isTLS = true; - String host = String('\0'); - uint16_t port = 443; - String url = String('\0'); - bool parse(String& url); -}; - -Ocpp_URL ocppUrlParsed = Ocpp_URL(); - -bool runWiFiManager(); -// ============ END CAPTIVE PORTAL - -void setup() { - - /* - * Initialize peripherals - */ - Serial.begin(115200); -#if !defined(SECC_NO_DEBUG) - Serial.setDebugOutput(true); -#endif - pinMode(EV_PLUG_PIN, INPUT); - pinMode(EV_CHARGE_PIN, INPUT); - pinMode(EVSE_GROUND_FAULT_PIN, INPUT); - pinMode(OCPP_CHARGE_PERMISSION_PIN, OUTPUT); - digitalWrite(OCPP_CHARGE_PERMISSION_PIN, OCPP_CHARGE_FORBIDDEN); - pinMode(OCPP_AVAILABILITY_PIN, OUTPUT); - digitalWrite(OCPP_AVAILABILITY_PIN, OCPP_UNAVAILABLE); - pinMode(CHARGE_PERMISSION_LED, OUTPUT); - digitalWrite(CHARGE_PERMISSION_LED, CHARGE_PERMISSION_OFF); - - pinMode(AMPERAGE_PIN, OUTPUT); -#if defined(ESP32) - pinMode(AMPERAGE_PIN, OUTPUT); - ledcSetup(PWM_CHANNEL, 1000, 8); //channel=PWM_CHANNEL, freq=1000Hz, range=(2^8)-1 - ledcAttachPin(AMPERAGE_PIN, PWM_CHANNEL); //pin, channel - ledcWrite(PWM_CHANNEL, 256); //channel, duty cycle (256 is constant +3.3V DC) -#elif defined(ESP8266) - analogWriteRange(255); //range=(2^8)-1 - analogWriteFreq(1000); //freq=1000Hz - analogWrite(AMPERAGE_PIN, 256); //256 is constant +3.3V DC -#else -#error Can only run on ESP8266 and ESP32 on the Arduino platform! -#endif - - pinMode(SERVER_CONNECT_LED, OUTPUT); - digitalWrite(SERVER_CONNECT_LED, SERVER_CONNECT_ON); //signal device reboot - delay(100); - digitalWrite(SERVER_CONNECT_LED, SERVER_CONNECT_OFF); - - /* - * You can use ArduinoOcpp's internal configurations store for credentials other than those which are - * specified by OCPP. To use it before ArduinoOcpp is initialized, you need to call configuration_init() - * - * This snippet also shows how to integrate a custom filesystem. Just subclass FilesystemAdapter and pass - * it to the library - */ - std::shared_ptr filesystem = ArduinoOcpp::makeDefaultFilesystemAdapter(ArduinoOcpp::FilesystemOpt::Use_Mount_FormatOnFail); - ArduinoOcpp::configuration_init(filesystem); - - /* - * WiFiManager opens a captive portal, lets the user enter the WiFi credentials and provides a settings - * page for the OCPP connection - */ - if (!runWiFiManager()) { - //couldn't connect - PRINTLN(F("[main] Couldn't connect to WiFi after multiple attempts")); - delay(30000); - ESP.restart(); - } - - std::shared_ptr> ocppUrl = ArduinoOcpp::declareConfiguration( - "ocppUrl", "", CONFIGURATION_FN, false, false, true, true); - std::shared_ptr> CA_cert = ArduinoOcpp::declareConfiguration( - "CA_cert", "", CONFIGURATION_FN, false, false, true, true); - std::shared_ptr> httpAuthentication = ArduinoOcpp::declareConfiguration( - "httpAuthentication", "", CONFIGURATION_FN, false, false, true, true); - - String ocppUrlString = String(*ocppUrl); - if (!ocppUrlParsed.parse(ocppUrlString)) { - PRINTLN(F("[main] Invalid OCPP URL. Restart.")); - delay(10000); - ESP.restart(); - } - - PRINTF("[main] host, port, URL: %s, %hu, %s\n", - ocppUrlParsed.host.isEmpty() ? "undefined" : ocppUrlParsed.host.c_str(), - ocppUrlParsed.port, - ocppUrlParsed.url.isEmpty() ? "undefined" : ocppUrlParsed.url.c_str()); - - /* - * Initialize ArduinoOcpp framework. - */ - wSock.setReconnectInterval(5000); - wSock.enableHeartbeat(15000, 3000, 2); - - if (httpAuthentication->getBuffsize() > 1) { - wSock.setAuthorization(*httpAuthentication); - } - - if (ocppUrlParsed.isTLS) { - if (CA_cert->getBuffsize() > 0) { - //TODOS: ESP8266: limit BearSSL buffsize - configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov"); //alternatively: settimeofday(&de, &tz); - PRINT(F("[main] Wait for NTP time (used for certificate validation) ")); - time_t now = time(nullptr); - while (now < 8 * 3600 * 2) { - delay(500); - PRINT('.'); - now = time(nullptr); - } - PRINTF(" finished. Unix timestamp is %lu\n", now); - - wSock.beginSslWithCA(ocppUrlParsed.host.c_str(), ocppUrlParsed.port, ocppUrlParsed.url.c_str(), *CA_cert, "ocpp1.6"); - } else { - wSock.beginSSL(ocppUrlParsed.host.c_str(), ocppUrlParsed.port, ocppUrlParsed.url.c_str(), nullptr, "ocpp1.6"); - } - } else { - wSock.begin(ocppUrlParsed.host, ocppUrlParsed.port, ocppUrlParsed.url, "ocpp1.6"); - } - - OCPP_initialize(oSock, - 230.f, //European grid voltage - ArduinoOcpp::FilesystemOpt::Use_Mount_FormatOnFail); - - /* - * Integrate OCPP functionality. You can leave out the following part if your EVSE doesn't need it. - */ - setEnergyMeterInput([]() { - //read the energy input register of the EVSE here and return the value in Wh - /* - * Approximated value. Replace with real reading - */ - static ulong lastSampled = millis(); - static float energyMeter = 0.f; - if (getTransactionId() > 0) - energyMeter += ((float) (millis() - lastSampled)) * 0.003f; //increase by 0.003Wh per ms (~ 10.8kWh per h) - lastSampled = millis(); - return energyMeter; - }); - - setSmartChargingOutput([](float limit) { - //set the SAE J1772 Control Pilot value here - const float voltage = 230.f; // European grid - const uint nPhases = 1; //one, two or three phase charging - float amps = limit / (voltage * (float) nPhases); - if (amps > 51.f) - amps = 51.f; - - PRINTF("[main] Smart Charging allows maximum charge rate: %iW; convert to Control Pilot amerage: %.2fA\n", (int) limit, amps); - - int pwmVal; - if (amps < 6.f) { - pwmVal = 256; // = constant +3.3V DC - } else { - pwmVal = (int) (4.26667f * amps); - } - -#if defined(ESP32) - ledcWrite(PWM_CHANNEL, pwmVal); -#elif defined(ESP8266) - analogWrite(AMPERAGE_PIN, pwmVal); -#endif - }); - - setEvReadyInput([]() { - //return true if the EV is in state "Ready for charging" (see https://en.wikipedia.org/wiki/SAE_J1772#Control_Pilot) - return digitalRead(EV_CHARGE_PIN) == EV_CHARGING; - }); - - addErrorCodeInput([] () { - //Uncomment if Ground fault pin is used - //if (digitalRead(EVSE_GROUND_FAULT_PIN) != EVSE_GROUND_CLEAR) { - // return "GroundFault"; - //} - return (const char *) nullptr; - }); - - setOnResetSendConf([] (JsonObject confirmation) { - if (getTransactionId() >= 0) - stopTransaction(); - - PRINTLN(F("[main] Execute reset command")); - reboot_timestamp = millis(); - scheduleReboot = 5000; //reboot will be executed in loop() - booted = false; - }); - - //... see ArduinoOcpp.h for more settings - - /* - * Notify the Central System that this station is ready - */ - bootNotification("My Charging Station", "My company name", [] (JsonObject response) { - if (response["status"].as().equals("Accepted")) { - booted = true; - digitalWrite(SERVER_CONNECT_LED, SERVER_CONNECT_ON); - } else { - //Wait for the connection retry - reboot_timestamp = millis(); - scheduleReboot = 60000; //wait for 60s until reboot; reboot will be executed in loop() - } - }); -} - -void loop() { - - /* - * Do all OCPP stuff (process WebSocket input, send recorded meter values to Central System, etc.) - */ - OCPP_loop(); - - //NFC reader integration example - if (/* RFID chip detected? */ false) { - const char *idTag = "my-id-tag"; //e.g. idTag = RFID.readIdTag(); - authorize(idTag); - } - - if (ocppPermitsCharge()) { - //EVSE is in a charging session and charging is permitted by the OCPP server - digitalWrite(OCPP_CHARGE_PERMISSION_PIN, OCPP_CHARGE_PERMITTED); - digitalWrite(CHARGE_PERMISSION_LED, CHARGE_PERMISSION_ON); - } else { - //Charging is not allowed due to OCPP rules - digitalWrite(OCPP_CHARGE_PERMISSION_PIN, OCPP_CHARGE_FORBIDDEN); - digitalWrite(CHARGE_PERMISSION_LED, CHARGE_PERMISSION_OFF); - } - - if (scheduleReboot > 0 && millis() - reboot_timestamp >= scheduleReboot) { - ESP.restart(); - } - - if (!booted) { - return; - } - - auto readEvPlugged = digitalRead(EV_PLUG_PIN); - if (evPlugged == EV_UNPLUGGED && readEvPlugged == EV_PLUGGED //transition from unplugged to plugged - && getTransactionId() < 0 //no transaction yet - && isOperative()) { //EVSE is in operative mode - startTransaction("my-id-tag"); - } else if (evPlugged == EV_PLUGGED && readEvPlugged == EV_UNPLUGGED //transition from plugged to unplugged - && getTransactionId() >= 0) { //need to stop transaction - stopTransaction(); - } - evPlugged = readEvPlugged; - - //... see ArduinoOcpp.h for more possibilities -} - - - -// ### End of the OCPP integration. The following code integrates the -// ### WiFi-Manager into this example sketch. - -bool runWiFiManager() { - - /* - * Initialize WiFi and start captive portal to set connection credentials - */ - WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP - - std::shared_ptr> ocppUrl = ArduinoOcpp::declareConfiguration( - "ocppUrl", "", CONFIGURATION_FN, false, false, true, true); - std::shared_ptr> CA_cert = ArduinoOcpp::declareConfiguration( - "CA_cert", "", CONFIGURATION_FN, false, false, true, true); - std::shared_ptr> httpAuthentication = ArduinoOcpp::declareConfiguration( - "httpAuthentication", "", CONFIGURATION_FN, false, false, true, true); - - WiFiManager wifiManager; - wifiManager.setTitle("EVSE configuration portal"); - wifiManager.setParamsPage(true); - WiFiManagerParameter ocppUrlParam ("ocppUrl", "OCPP 1.6 Server URL + Charge Box ID", *ocppUrl, 150,"placeholder=\"wss://<domain>:<port>/<path>/<chargeBoxId>\""); - wifiManager.addParameter(&ocppUrlParam); - - WiFiManagerParameter divider("

"); - wifiManager.addParameter(÷r); - - WiFiManagerParameter caCertParam ("caCert", "CA Certificate", "", 1500,"placeholder=\"Paste here or leave blank\""); - wifiManager.addParameter(&caCertParam); - WiFiManagerParameter caCertSavePararm ("
"); - wifiManager.addParameter(&caCertSavePararm); - - WiFiManagerParameter httpAuthenticationParam ("httpAuthentication", "HTTP-authentication token", "", 150,"placeholder=\"e.g.: aKu0Q4NjMRC9yO3wRo08\""); - wifiManager.addParameter(&httpAuthenticationParam); - WiFiManagerParameter httpAuthenticationSavePararm ("
"); - wifiManager.addParameter(&httpAuthenticationSavePararm); - - wifiManager.setSaveParamsCallback([&ocppUrlParam, ocppUrl, &wifiManager, CA_cert, &caCertParam, httpAuthentication, &httpAuthenticationParam] () { - String newOcppUrl = String(ocppUrlParam.getValue()); - newOcppUrl.trim(); - Ocpp_URL newOcppUrlParsed = Ocpp_URL(); - if (newOcppUrlParsed.parse(newOcppUrl)) { - //success - *ocppUrl = newOcppUrl.c_str(); - } - - bool caCertSave = false; - String caCertChangedParam = String ("caCertSave"); - if(wifiManager.server->hasArg(caCertChangedParam)) { - String caCertChanged = String(wifiManager.server->arg(caCertChangedParam)); - if (caCertChanged.length() > 0) { - caCertSave = caCertChanged.charAt(0) == '1' || caCertChanged.charAt(0) == 't' || caCertChanged.charAt(0) == 'o'; - } - } - if (caCertSave) { - *CA_cert = caCertParam.getValue(); - } - - bool httpAuthenticationSave = false; - String authChangedParam = String ("httpAuthenticationSave"); - if(wifiManager.server->hasArg(authChangedParam)) { - String authChanged = String(wifiManager.server->arg(authChangedParam)); - if (authChanged.length() > 0) { - httpAuthenticationSave = authChanged.charAt(0) == '1' || authChanged.charAt(0) == 't' || authChanged.charAt(0) == 'o'; - } - } - if (httpAuthenticationSave) { - *httpAuthentication = httpAuthenticationParam.getValue(); - } - ArduinoOcpp::configuration_save(); - }); - - wifiManager.setDarkMode(true); - - //wifiManager.setConfigPortalTimeout(CAPATITIVE_PORTAL_TIMEOUT / 1000); //if nobody logs in to the portal, continue after timeout - wifiManager.setTimeout(CAPTIVE_PORTAL_TIMEOUT); //if nobody logs in to the portal, continue after timeout - wifiManager.setConnectTimeout(WIFI_CONNECTION_TIMEOUT); - //wifiManager.setSaveConnect(true); - wifiManager.setAPClientCheck(true); // avoid timeout if client connected to softap - PRINTLN(F("[main] Start capatitive portal")); - - if (wifiManager.startConfigPortal("EVSE-Config", "evse1234")) { - return true; - } else { - return wifiManager.autoConnect("EVSE-Config", "evse1234"); - } -} - -bool Ocpp_URL::parse(String &ocppUrl) { - String inputUrl = String(ocppUrl); // wss://host.com:433/path - inputUrl.trim(); - - if (ocppUrl.isEmpty()) { - return false; - } - url = inputUrl; - - inputUrl.toLowerCase(); - - if (inputUrl.startsWith("wss://")) { - isTLS = true; - port = 443; - } else if (inputUrl.startsWith("ws://")) { - isTLS = false; - port = 80; - } else { - return false; - } - inputUrl = inputUrl.substring(inputUrl.indexOf("://") + strlen("://")); // host.com:433/path - if (inputUrl.isEmpty()) { - return false; - } - - host = String('\0'); - for (uint i = 0; i < inputUrl.length(); i++) { - if (inputUrl.charAt(i) == ':') { //case host.com:433/path - host = inputUrl.substring(0, i); - uint16_t inputPort = 0; - for (unsigned int j = i + 1; j < inputUrl.length(); j++) { - if (isDigit(inputUrl.charAt(j))) { - inputPort *= (uint16_t) 10; - inputPort += inputUrl.charAt(j) - '0'; - } else { - break; - } - } - if (inputPort > 0) { - port = inputPort; - } - break; - } else if (inputUrl.charAt(i) == '/') { //case host.com/path - host = inputUrl.substring(0, i); - break; - } - } - if (host.isEmpty()) { - host = inputUrl; - } - - return true; -} diff --git a/examples/SECC/platformio.ini b/examples/SECC/platformio.ini deleted file mode 100644 index 72b36270..00000000 --- a/examples/SECC/platformio.ini +++ /dev/null @@ -1,21 +0,0 @@ -; matth-x/ArduinoOcpp -; Copyright Matthias Akstaller 2019 - 2022 -; MIT License - -[env:esp32dev] -platform = espressif32@3.3.2 -board = esp32dev -framework = arduino -lib_deps = - https://github.com/matth-x/ArduinoOcpp.git - https://github.com/tzapu/WiFiManager.git#4dcf0cc78bb031351d65e0f9b9271569df8720ed -monitor_speed = 115200 - -[env:nodemcuv2] -platform = espressif8266@2.6.3 -board = nodemcuv2 -framework = arduino -lib_deps = - https://github.com/matth-x/ArduinoOcpp.git - https://github.com/tzapu/WiFiManager.git#4dcf0cc78bb031351d65e0f9b9271569df8720ed -monitor_speed = 115200 diff --git a/library.json b/library.json index 5fb9c93d..ec1b8a1e 100644 --- a/library.json +++ b/library.json @@ -1,61 +1,52 @@ { - "name": "ArduinoOcpp", - "version": "0.2.0", - "description": "OCPP 1.6 Client for the ESP8266 and ESP32", - "keywords": "OCPP, 1.6, OCPP 1.6, Smart Energy, Smart Charging, client, ESP8266, ESP32, Arduino, EVSE, Charge Point", + "name": "MicroOcpp", + "version": "1.2.0", + "description": "OCPP 1.6 / 2.0.1 Client for microcontrollers", + "keywords": "OCPP, 1.6, OCPP 1.6, OCPP 2.0.1, Smart Energy, Smart Charging, client, ESP8266, ESP32, Arduino, esp-idf, EVSE, Charge Point", "repository": { "type": "git", - "url": "https://github.com/matth-x/ArduinoOcpp/" + "url": "https://github.com/matth-x/MicroOcpp/" }, "authors": [ { "name": "Matthias Akstaller", - "url": "https://www.arduino-ocpp.com", + "url": "https://www.micro-ocpp.com", "maintainer": true } ], "license": "MIT", - "homepage": "https://www.arduino-ocpp.com", + "homepage": "https://www.micro-ocpp.com", "dependencies": [ { "owner": "bblanchon", "name": "ArduinoJson", - "version": "6.19.1" + "version": "6.20.1" }, { "owner": "links2004", "name": "WebSockets", - "version": "2.3.6" - }, - { - "owner": "lorol", - "name": "LittleFS_esp32", - "version": "1.0.5", - "platforms": ["espressif32"] + "version": "2.4.1" } ], - "frameworks": "arduino", + "frameworks": "arduino,espidf", "platforms": "espressif8266, espressif32", "export": { "include": [ + "docs/*", + "examples/*", "src/*", - "examples/ESP/*", - "examples/ESP-TLS/*", - "examples/SECC/*", - "platformio.ini", + "CHANGELOG.md", + "CMakeLists.txt", "library.json", - "README.md", - "LICENSE" - ], - "exclude": - [ - "src/sdkconfig*", - "examples/SECC/WiFiManager*", - "src/main*" + "library.properties", + "LICENSE", + "mkdocs.yml", + "platformio.ini", + "README.md" ] }, @@ -75,13 +66,11 @@ ] }, { - "name": "EVSE Communications Controller", - "base": "examples/SECC", + "name": "ESP-IDF integration", + "base": "examples/ESP-IDF", "files": [ - "main.cpp", - "platformio.ini", - "README.md" + "main/main.c" ] - } - ] - } \ No newline at end of file + } + ] +} diff --git a/library.properties b/library.properties index 7e944f3f..8a1a3e01 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,10 @@ -name=ArduinoOcpp -version=0.1.0 +name=MicroOcpp +version=1.2.0 author=Matthias Akstaller maintainer=Matthias Akstaller -sentence=OCPP 1.6 Client for the ESP8266 and ESP32 (more coming soon) +sentence=OCPP 1.6 Client for microcontrollers paragraph= category=Communication -url=https://github.com/matth-x/ArduinoOcpp/ -architectures=espressif8266, espressif32 +url=https://github.com/matth-x/MicroOcpp/ +architectures=* +depends=ArduinoJson, WebSockets diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..22027d12 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,55 @@ +site_name: MicroOCPP docs + +theme: + name: material + features: + - announce.dismiss + - content.action.edit + - content.action.view + - content.code.annotate + - content.code.copy + # - content.tabs.link + - content.tooltips + # - header.autohide + # - navigation.expand + - navigation.footer + - navigation.indexes + # - navigation.instant + # - navigation.prune + - navigation.sections + - navigation.tabs + # - navigation.tabs.sticky + - navigation.top + - navigation.tracking + - search.highlight + - search.share + - search.suggest + - toc.follow + # - toc.integrate + palette: + - scheme: default + primary: custom + accent: custom + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: custom + accent: custom + toggle: + icon: material/brightness-4 + name: Switch to light mode + font: + text: Roboto + code: Roboto Mono + favicon: img/favicon.ico + icon: + logo: logo + +extra_css: + - stylesheets/extra.css + +plugins: + - search + - table-reader: + data_path: "docs/assets/tables" diff --git a/platformio.ini b/platformio.ini index a04c0917..b5660ac8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,5 +1,5 @@ -; matth-x/ArduinoOcpp -; Copyright Matthias Akstaller 2019 - 2022 +; matth-x/MicroOcpp +; Copyright Matthias Akstaller 2019 - 2024 ; MIT License [platformio] @@ -8,8 +8,8 @@ default_envs = esp32-development-board [common] framework = arduino lib_deps = - bblanchon/ArduinoJson@6.19.1 - links2004/WebSockets@2.3.6 + bblanchon/ArduinoJson@6.20.1 + links2004/WebSockets@2.4.1 monitor_speed = 115200 [env:nodemcuv2] @@ -19,21 +19,20 @@ framework = ${common.framework} lib_deps = ${common.lib_deps} monitor_speed = ${common.monitor_speed} build_flags = - -D AO_DBG_LEVEL=AO_DL_INFO ; flood the serial monitor with information about the internal state - -DAO_TRAFFIC_OUT ; print the OCPP communication to the serial monitor + -D MO_DBG_LEVEL=MO_DL_INFO ; flood the serial monitor with information about the internal state + -DMO_TRAFFIC_OUT ; print the OCPP communication to the serial monitor -D ARDUINOJSON_ENABLE_STD_STRING=1 [env:esp32-development-board] -platform = espressif32@3.5.0 +platform = espressif32@6.0.1 board = esp-wrover-kit framework = ${common.framework} -lib_deps = - ${common.lib_deps} - lorol/LittleFS_esp32@1.0.5 +lib_deps = ${common.lib_deps} monitor_speed = ${common.monitor_speed} build_flags = - -D AO_DBG_LEVEL=AO_DL_INFO ; flood the serial monitor with information about the internal state - -DAO_TRAFFIC_OUT ; print the OCPP communication to the serial monitor - -DCONFIG_LITTLEFS_FOR_IDF_3_2 + -D MO_DBG_LEVEL=MO_DL_INFO ; flood the serial monitor with information about the internal state + -DMO_TRAFFIC_OUT ; print the OCPP communication to the serial monitor board_build.partitions = min_spiffs.csv upload_speed = 921600 +monitor_filters = + esp32_exception_decoder diff --git a/src/ArduinoOcpp.cpp b/src/ArduinoOcpp.cpp deleted file mode 100644 index 248845d4..00000000 --- a/src/ArduinoOcpp.cpp +++ /dev/null @@ -1,705 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include "ArduinoOcpp.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include - -namespace ArduinoOcpp { -namespace Facade { - -#ifndef AO_CUSTOM_WS -WebSocketsClient *webSocket {nullptr}; -OcppSocket *ocppSocket {nullptr}; -#endif - -OcppEngine *ocppEngine {nullptr}; -std::shared_ptr filesystem; -FilesystemOpt fileSystemOpt {}; -float voltage_eff {230.f}; - -#ifndef AO_NUMCONNECTORS -#define AO_NUMCONNECTORS 2 -#endif - -#define OCPP_ID_OF_CP 0 -bool OCPP_booted = false; //if BootNotification succeeded - -} //end namespace ArduinoOcpp::Facade -} //end namespace ArduinoOcpp - -using namespace ArduinoOcpp; -using namespace ArduinoOcpp::Facade; -using namespace ArduinoOcpp::Ocpp16; - -#ifndef AO_CUSTOM_WS -void OCPP_initialize(const char *CS_hostname, uint16_t CS_port, const char *CS_url, float V_eff, ArduinoOcpp::FilesystemOpt fsOpt) { - if (ocppEngine) { - AO_DBG_WARN("Can't be called two times. Either restart ESP, or call OCPP_deinitialize() before"); - return; - } - - if (!webSocket) - webSocket = new WebSocketsClient(); - - // server address, port and URL - webSocket->begin(CS_hostname, CS_port, CS_url, "ocpp1.6"); - - // try ever 5000 again if connection has failed - webSocket->setReconnectInterval(5000); - - // start heartbeat (optional) - // ping server every 15000 ms - // expect pong from server within 3000 ms - // consider connection disconnected if pong is not received 2 times - webSocket->enableHeartbeat(15000, 3000, 2); //comment this one out to for specific OCPP servers - - delete ocppSocket; - ocppSocket = new EspWiFi::OcppClientSocket(webSocket); - - OCPP_initialize(*ocppSocket, V_eff, fsOpt); -} -#endif - -void OCPP_initialize(OcppSocket& ocppSocket, float V_eff, ArduinoOcpp::FilesystemOpt fsOpt) { - if (ocppEngine) { - AO_DBG_WARN("Can't be called two times. To change the credentials, either restart ESP, or call OCPP_deinitialize() before"); - return; - } - - voltage_eff = V_eff; - fileSystemOpt = fsOpt; - -#ifndef AO_DEACTIVATE_FLASH - filesystem = makeDefaultFilesystemAdapter(fileSystemOpt); -#endif - AO_DBG_DEBUG("filesystem %s", filesystem ? "loaded" : "error"); - - configuration_init(filesystem); //call before each other library call - - ocppEngine = new OcppEngine(ocppSocket, Clocks::DEFAULT_CLOCK, filesystem); - auto& model = ocppEngine->getOcppModel(); - - model.setTransactionStore(std::unique_ptr( - new TransactionStore(AO_NUMCONNECTORS, filesystem))); - model.setChargePointStatusService(std::unique_ptr( - new ChargePointStatusService(*ocppEngine, AO_NUMCONNECTORS))); - model.setHeartbeatService(std::unique_ptr( - new HeartbeatService(*ocppEngine))); - -#if !defined(AO_CUSTOM_UPDATER) && !defined(AO_CUSTOM_WS) - model.setFirmwareService(std::unique_ptr( - EspWiFi::makeFirmwareService(*ocppEngine, "1234578901"))); //instantiate FW service + ESP installation routine -#else - model.setFirmwareService(std::unique_ptr( - new FirmwareService(*ocppEngine))); //only instantiate FW service -#endif - -#if !defined(AO_CUSTOM_DIAGNOSTICS) && !defined(AO_CUSTOM_WS) - model.setDiagnosticsService(std::unique_ptr( - EspWiFi::makeDiagnosticsService(*ocppEngine))); //will only return "Rejected" because client needs to implement logging -#else - model.setDiagnosticsService(std::unique_ptr( - new DiagnosticsService(*ocppEngine))); -#endif - -#if AO_PLATFORM == AO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) - if (!model.getChargePointStatusService()->getExecuteReset()) - model.getChargePointStatusService()->setExecuteReset(makeDefaultResetFn()); -#endif - - ocppEngine->setRunOcppTasks(false); //prevent OCPP classes from doing anything while booting -} - -void OCPP_deinitialize() { - - delete ocppEngine; - ocppEngine = nullptr; - -#ifndef AO_CUSTOM_WS - delete ocppSocket; - ocppSocket = nullptr; - delete webSocket; - webSocket = nullptr; -#endif - - simpleOcppFactory_deinitialize(); - - fileSystemOpt = FilesystemOpt(); - voltage_eff = 230.f; - - OCPP_booted = false; -} - -void OCPP_loop() { - if (!ocppEngine) { - AO_DBG_WARN("Please call OCPP_initialize before"); - return; - } - - ocppEngine->loop(); - - - if (!OCPP_booted) { - auto csService = ocppEngine->getOcppModel().getChargePointStatusService(); - if (!csService || csService->isBooted()) { - OCPP_booted = true; - ocppEngine->setRunOcppTasks(true); - } else { - return; //wait until the first BootNotification succeeded - } - } - -} - -void bootNotification(const char *chargePointModel, const char *chargePointVendor, OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, std::unique_ptr timeout) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - - auto credentials = std::unique_ptr(new DynamicJsonDocument( - JSON_OBJECT_SIZE(2) + strlen(chargePointModel) + strlen(chargePointVendor) + 2)); - (*credentials)["chargePointModel"] = (char*) chargePointModel; - (*credentials)["chargePointVendor"] = (char*) chargePointVendor; - - bootNotification(std::move(credentials), onConf, onAbort, onTimeout, onError, std::move(timeout)); -} - -void bootNotification(std::unique_ptr payload, OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, std::unique_ptr timeout) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - auto bootNotification = makeOcppOperation( - new BootNotification(std::move(payload))); - if (onConf) - bootNotification->setOnReceiveConfListener(onConf); - if (onAbort) - bootNotification->setOnAbortListener(onAbort); - if (onTimeout) - bootNotification->setOnTimeoutListener(onTimeout); - if (onError) - bootNotification->setOnReceiveErrorListener(onError); - if (timeout) - bootNotification->setTimeout(std::move(timeout)); - else - bootNotification->setTimeout(std::unique_ptr(new SuppressedTimeout())); - ocppEngine->initiateOperation(std::move(bootNotification)); -} - -void authorize(const char *idTag, OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, std::unique_ptr timeout) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX) { - AO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); - return; - } - auto authorize = makeOcppOperation( - new Authorize(idTag)); - if (onConf) - authorize->setOnReceiveConfListener(onConf); - if (onAbort) - authorize->setOnAbortListener(onAbort); - if (onTimeout) - authorize->setOnTimeoutListener(onTimeout); - if (onError) - authorize->setOnReceiveErrorListener(onError); - if (timeout) - authorize->setTimeout(std::move(timeout)); - else - authorize->setTimeout(std::unique_ptr(new FixedTimeout(20000))); - ocppEngine->initiateOperation(std::move(authorize)); -} - -bool beginTransaction(const char *idTag, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return false; - } - if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX) { - AO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); - return false; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return false; - } - connector->beginSession(idTag); - return true; -} - -bool endTransaction(const char *reason, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return false; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return false; - } - bool res = connector->getSessionIdTag(); - connector->endSession(reason); - return res; -} - -bool isTransactionRunning(unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return false; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return false; - } - return connector->isTransactionRunning(); -} - -bool ocppPermitsCharge(unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_WARN("Please call OCPP_initialize before"); - return false; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return false; - } - return connector->ocppPermitsCharge(); -} - -void setConnectorPluggedInput(std::function pluggedInput, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return; - } - connector->setConnectorPluggedSampler(pluggedInput); - - if (pluggedInput) { - AO_DBG_INFO("Added ConnectorPluggedSampler. Transaction-management is in auto mode now"); - } else { - AO_DBG_INFO("Reset ConnectorPluggedSampler. Transaction-management is in manual mode now"); - } -} - -void setEnergyMeterInput(std::function energyInput, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - auto& model = ocppEngine->getOcppModel(); - if (!model.getMeteringService()) { - model.setMeteringSerivce(std::unique_ptr( - new MeteringService(*ocppEngine, AO_NUMCONNECTORS, filesystem))); - } - SampledValueProperties meterProperties; - meterProperties.setMeasurand("Energy.Active.Import.Register"); - meterProperties.setUnit("Wh"); - auto mvs = std::unique_ptr>>( - new SampledValueSamplerConcrete>( - meterProperties, - [energyInput] (ReadingContext) {return energyInput();} - )); - model.getMeteringService()->addMeterValueSampler(connectorId, std::move(mvs)); - model.getMeteringService()->setEnergySampler(connectorId, energyInput); -} - -void setPowerMeterInput(std::function powerInput, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - - auto& model = ocppEngine->getOcppModel(); - if (!model.getMeteringService()) { - model.setMeteringSerivce(std::unique_ptr( - new MeteringService(*ocppEngine, AO_NUMCONNECTORS, filesystem))); - } - SampledValueProperties meterProperties; - meterProperties.setMeasurand("Power.Active.Import"); - meterProperties.setUnit("W"); - auto mvs = std::unique_ptr>>( - new SampledValueSamplerConcrete>( - meterProperties, - [powerInput] (ReadingContext) {return powerInput();} - )); - model.getMeteringService()->addMeterValueSampler(connectorId, std::move(mvs)); - model.getMeteringService()->setPowerSampler(connectorId, powerInput); -} - -void setSmartChargingOutput(std::function chargingLimitOutput, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - if (connectorId != 1) { - AO_DBG_WARN("Smart charging for multiple connectorId %u not implemented yet", connectorId); - return; - } - auto& model = ocppEngine->getOcppModel(); - if (!model.getSmartChargingService()) { - model.setSmartChargingService(std::unique_ptr( - new SmartChargingService(*ocppEngine, 11000.0f, voltage_eff, AO_NUMCONNECTORS, fileSystemOpt))); //default charging limit: 11kW - } - model.getSmartChargingService()->setOnLimitChange(chargingLimitOutput); -} - -void setEvReadyInput(std::function evReadyInput, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return; - } - connector->setEvRequestsEnergySampler(evReadyInput); -} - -void setEvseReadyInput(std::function evseReadyInput, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return; - } - connector->setConnectorEnergizedSampler(evseReadyInput); -} - -void addErrorCodeInput(std::function errorCodeInput, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return; - } - connector->addConnectorErrorCodeSampler(errorCodeInput); -} - -void addMeterValueInput(std::function valueInput, const char *measurand, const char *unit, const char *location, const char *phase, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - - if (!valueInput) { - AO_DBG_ERR("value undefined"); - return; - } - - if (!measurand) { - measurand = "Energy.Active.Import.Register"; - AO_DBG_WARN("Measurand unspecified; assume %s", measurand); - } - - SampledValueProperties properties; - properties.setMeasurand(measurand); //mandatory for AO - - if (unit) - properties.setUnit(unit); - if (location) - properties.setLocation(location); - if (phase) - properties.setPhase(phase); - - auto valueSampler = std::unique_ptr>>( - new ArduinoOcpp::SampledValueSamplerConcrete>( - properties, - [valueInput] (ArduinoOcpp::ReadingContext) {return valueInput();})); - addMeterValueInput(std::move(valueSampler), connectorId); -} - -void addMeterValueInput(std::unique_ptr valueInput, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - auto& model = ocppEngine->getOcppModel(); - if (!model.getMeteringService()) { - model.setMeteringSerivce(std::unique_ptr( - new MeteringService(*ocppEngine, AO_NUMCONNECTORS, filesystem))); - } - model.getMeteringService()->addMeterValueSampler(connectorId, std::move(valueInput)); -} - -void setOnResetNotify(std::function onResetNotify) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - - if (auto csService = ocppEngine->getOcppModel().getChargePointStatusService()) { - csService->setPreReset(onResetNotify); - } -} - -void setOnResetExecute(std::function onResetExecute) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - - if (auto csService = ocppEngine->getOcppModel().getChargePointStatusService()) { - csService->setExecuteReset(onResetExecute); - } -} - -void setOnUnlockConnectorInOut(std::function()> onUnlockConnectorInOut, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return; - } - connector->setOnUnlockConnector(onUnlockConnectorInOut); -} - -void setConnectorLockInOut(std::function lockConnectorInOut, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return; - } - connector->setConnectorLock(lockConnectorInOut); -} - -void setTxBasedMeterInOut(std::function txMeterInOut, unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return; - } - connector->setTxBasedMeterUpdate(txMeterInOut); -} - -bool isOperative(unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_WARN("Please call OCPP_initialize before"); - return true; //assume "true" as default state - } - auto& model = ocppEngine->getOcppModel(); - auto chargePoint = model.getConnectorStatus(OCPP_ID_OF_CP); - auto connector = model.getConnectorStatus(connectorId); - if (!chargePoint || !connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return true; //assume "true" as default state - } - return (chargePoint->getAvailability() != AVAILABILITY_INOPERATIVE) - && (connector->getAvailability() != AVAILABILITY_INOPERATIVE); -} - -int getTransactionId(unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_WARN("Please call OCPP_initialize before"); - return -1; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return -1; - } - return connector->getTransactionId(); -} - -const char *getTransactionIdTag(unsigned int connectorId) { - if (!ocppEngine) { - AO_DBG_WARN("Please call OCPP_initialize before"); - return nullptr; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(connectorId); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return nullptr; - } - return connector->getSessionIdTag(); -} - -#if defined(AO_CUSTOM_UPDATER) || defined(AO_CUSTOM_WS) -ArduinoOcpp::FirmwareService *getFirmwareService() { - auto& model = ocppEngine->getOcppModel(); - return model.getFirmwareService(); -} -#endif - -#if defined(AO_CUSTOM_DIAGNOSTICS) || defined(AO_CUSTOM_WS) -ArduinoOcpp::DiagnosticsService *getDiagnosticsService() { - auto& model = ocppEngine->getOcppModel(); - return model.getDiagnosticsService(); -} -#endif - -OcppEngine *getOcppEngine() { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return nullptr; - } - - return ocppEngine; -} - -void setOnSetChargingProfileRequest(OnReceiveReqListener onReceiveReq) { - setOnSetChargingProfileRequestListener(onReceiveReq); -} - -void setOnRemoteStartTransactionSendConf(OnSendConfListener onSendConf) { - setOnRemoteStartTransactionSendConfListener(onSendConf); -} - -void setOnRemoteStopTransactionReceiveReq(OnReceiveReqListener onReceiveReq) { - setOnRemoteStopTransactionReceiveRequestListener(onReceiveReq); -} - -void setOnRemoteStopTransactionSendConf(OnSendConfListener onSendConf) { - setOnRemoteStopTransactionSendConfListener(onSendConf); -} - -void setOnResetSendConf(OnSendConfListener onSendConf) { - setOnResetSendConfListener(onSendConf); -} - -void setOnResetRequest(OnReceiveReqListener onReceiveReq) { - setOnResetReceiveRequestListener(onReceiveReq); -} - -#define OCPP_ID_OF_CONNECTOR 1 - -bool startTransaction(const char *idTag, OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, std::unique_ptr timeout) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return false; - } - if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX) { - AO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); - return false; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(OCPP_ID_OF_CONNECTOR); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return false; - } - auto transaction = connector->getTransaction(); - if (transaction) { - if (transaction->getStartRpcSync().isRequested()) { - AO_DBG_ERR("Transaction already in progress. Must call stopTransaction()"); - return false; - } - transaction->setIdTag(idTag); - } else { - beginTransaction(idTag); //request new transaction object - transaction = connector->getTransaction(); - if (!transaction) { - AO_DBG_WARN("Transaction queue full"); - return false; - } - } - - auto startTransaction = makeOcppOperation( - new StartTransaction(transaction)); - if (onConf) - startTransaction->setOnReceiveConfListener(onConf); - if (onAbort) - startTransaction->setOnAbortListener(onAbort); - if (onTimeout) - startTransaction->setOnTimeoutListener(onTimeout); - if (onError) - startTransaction->setOnReceiveErrorListener(onError); - if (timeout) - startTransaction->setTimeout(std::move(timeout)); - else - startTransaction->setTimeout(std::unique_ptr(new SuppressedTimeout())); - ocppEngine->initiateOperation(std::move(startTransaction)); - - return true; -} - -bool stopTransaction(OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, std::unique_ptr timeout) { - if (!ocppEngine) { - AO_DBG_ERR("OCPP uninitialized"); //please call OCPP_initialize before - return false; - } - auto connector = ocppEngine->getOcppModel().getConnectorStatus(OCPP_ID_OF_CONNECTOR); - if (!connector) { - AO_DBG_ERR("Could not find connector. Ignore"); - return false; - } - - auto transaction = connector->getTransaction(); - if (!transaction || !transaction->isRunning()) { - AO_DBG_ERR("No running Tx to stop"); - return false; - } - - connector->endSession("Local"); - - const char *idTag = transaction->getIdTag(); - if (idTag) { - transaction->setStopIdTag(idTag); - } - - transaction->setStopReason("Local"); - - auto stopTransaction = makeOcppOperation( - new StopTransaction(transaction)); - if (onConf) - stopTransaction->setOnReceiveConfListener(onConf); - if (onAbort) - stopTransaction->setOnAbortListener(onAbort); - if (onTimeout) - stopTransaction->setOnTimeoutListener(onTimeout); - if (onError) - stopTransaction->setOnReceiveErrorListener(onError); - if (timeout) - stopTransaction->setTimeout(std::move(timeout)); - else - stopTransaction->setTimeout(std::unique_ptr(new SuppressedTimeout())); - ocppEngine->initiateOperation(std::move(stopTransaction)); - - return true; -} diff --git a/src/ArduinoOcpp.h b/src/ArduinoOcpp.h deleted file mode 100644 index 38a97913..00000000 --- a/src/ArduinoOcpp.h +++ /dev/null @@ -1,318 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef ARDUINOOCPP_H -#define ARDUINOOCPP_H - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -using ArduinoOcpp::OnReceiveConfListener; -using ArduinoOcpp::OnReceiveReqListener; -using ArduinoOcpp::OnSendConfListener; -using ArduinoOcpp::OnAbortListener; -using ArduinoOcpp::OnTimeoutListener; -using ArduinoOcpp::OnReceiveErrorListener; - -using ArduinoOcpp::Timeout; - -#ifndef AO_CUSTOM_WS -//use links2004/WebSockets library - -/* - * Initialize the library with the OCPP URL, EVSE voltage and filesystem configuration. - * - * If the connections fails, please refer to - * https://github.com/matth-x/ArduinoOcpp/issues/36#issuecomment-989716573 for recommendations on - * how to track down the issue with the connection. - * - * This is a convenience function only available for Arduino. For a full initialization with TLS, - * please refer to https://github.com/matth-x/ArduinoOcpp/tree/master/examples/ESP-TLS - */ -void OCPP_initialize( - const char *CS_hostname, //e.g. "example.com" - uint16_t CS_port, //e.g. 80 - const char *CS_url, //e.g. "ws://example.com/steve/websocket/CentralSystemService/charger001" - float V_eff = 230.f, //Grid voltage of your country. e.g. 230.f (European voltage) - ArduinoOcpp::FilesystemOpt fsOpt = ArduinoOcpp::FilesystemOpt::Use_Mount_FormatOnFail); //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h -#endif - -/* - * Initialize the library with a WebSocket connection which is configured with protocol=ocpp1.6 - * (=OcppSocket), 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/ArduinoOcpp/tree/master/examples/ESP-TLS for an example how to use it. - * - * This GitHub project also delivers an OcppSocket implementation based on links2004/WebSockets. If - * you need another WebSockets implementation, you can subclass the OcppSocket class and pass it to - * this initialize() function. Please refer to - * https://github.com/OpenEVSE/ESP32_WiFi_V4.x/blob/master/src/MongooseOcppSocketClient.cpp for - * an example. - */ -void OCPP_initialize( - ArduinoOcpp::OcppSocket& ocppSocket, //WebSocket adapter for ArduinoOcpp - float V_eff = 230.f, //Grid voltage of your country. e.g. 230.f (European voltage) - ArduinoOcpp::FilesystemOpt fsOpt = ArduinoOcpp::FilesystemOpt::Use_Mount_FormatOnFail); //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h - -/* - * Stop the OCPP library and release allocated resources. - */ -void OCPP_deinitialize(); - -/* - * To be called in the main loop (e.g. place it inside loop()) - */ -void OCPP_loop(); - -/* - * Send OCPP operations. - * - * You only need to send the following operation types manually: - * - BootNotification: Notify the OCPP server that this EVSE is ready and start the OCPP - * routines. - * - Authorize: Validate an RFID tag before using it for a transaction - * - * All other operation types are handled automatically by this library. - * - * On receipt of the .conf() response the library calls the callback function - * "OnReceiveConfListener onConf" and passes the OCPP payload to it. - * - * For your first EVSE integration, the `onReceiveConfListener` is probably sufficient. For - * advanced EVSE projects, the other listeners likely become relevant: - * - `onAbortListener`: will be called whenever the engine stops trying to finish an operation - * normally which was initiated by this device. - * - `onTimeoutListener`: will be executed when the operation is not answered until the timeout - * expires. Note that timeouts also trigger the `onAbortListener`. - * - `onReceiveErrorListener`: will be called when the Central System returns a CallError. - * Again, each error also triggers the `onAbortListener`. - * - * The functions for sending OCPP operations are non-blocking. The program will resume immediately - * with the code after with the subsequent code in any case. - */ - -void bootNotification( - const char *chargePointModel, //model name of the EVSE - const char *chargePointVendor, //vendor name - OnReceiveConfListener onConf = nullptr, //callback (confirmation received) - OnAbortListener onAbort = nullptr, //callback (confirmation not received), optional - OnTimeoutListener onTimeout = nullptr, //callback (timeout expired), optional - OnReceiveErrorListener onError = nullptr, //callback (error code received), optional - std::unique_ptr timeout = nullptr); //custom timeout behavior, optional - -//Alternative version for sending a complete BootNotification payload -void bootNotification( - std::unique_ptr payload,//manually defined payload - OnReceiveConfListener onConf = nullptr, //callback (confirmation received) - OnAbortListener onAbort = nullptr, //callback (confirmation not received), optional - OnTimeoutListener onTimeout = nullptr, //callback (timeout expired), optional - OnReceiveErrorListener onError = nullptr, //callback (error code received), optional - std::unique_ptr timeout = nullptr); //custom timeout behavior, optional - -void authorize( - const char *idTag, //RFID tag (e.g. ISO 14443 UID tag with 4 or 7 bytes) - OnReceiveConfListener onConf = nullptr, //callback (confirmation received) - OnAbortListener onAbort = nullptr, //callback (confirmation not received), optional - OnTimeoutListener onTimeout = nullptr, //callback (timeout expired), optional - OnReceiveErrorListener onError = nullptr, //callback (error code received), optional - std::unique_ptr timeout = nullptr); //custom timeout behavior, optional - -/* - * Transaction management. - * - * Begin the transaction process by creating a new transaction and setting its user identification. - * The OCPP library will try to send a StartTransaction request with all data as soon as possible - * after all requirements for a transaction are met (i.e. the connector is plugged). - * - * Returns true if it was possible to create a transaction. If it returned false, either another - * transaction is still running or you need to try it again later. - */ -bool beginTransaction(const char *idTag, unsigned int connectorId = 1); - -/* - * End the transaction process by terminating the transaction and setting a reason for its termination. - * Please refer to to OCPP 1.6 Specification - Edition 2 p. 90 for a list of valid reasons. "reason" - * can also be nullptr. - * - * It is safe to call this function at any time, i.e. when no transaction runs or when the transaction - * has already been ended. For example you can place `endTransaction("Reboot");` in the beginning of - * the program just to ensure that there is no transaction from a previous run. - * - * Returns true if this action actually ended a transaction. False otherwise - */ -bool endTransaction(const char *reason = nullptr, unsigned int connectorId = 1); - -/* - * Returns if the library has started the transaction by sending a StartTransaction and if it hasn't - * been stopped already by sending a StopTransaction. - */ -bool isTransactionRunning(unsigned int connectorId = 1); - -/* - * Returns if the OCPP library allows the EVSE to charge at the moment. - * - * If you integrate it into a J1772 charger, true means that the Control Pilot can send the PWM signal - * and false means that the Control Pilot must be at a DC voltage. - */ -bool ocppPermitsCharge(unsigned int connectorId = 1); - -/* - * Define the Inputs and Outputs of this library. - * - * This library interacts with the hardware of your charger by Inputs and Outputs. Inputs and Outputs - * are tiny function-objects which read information from the EVSE or control the behavior of the EVSE. - * - * An Input is a function which returns the current state of a variable of the EVSE. For example, if - * the energy meter stores the energy register in the global variable `e_reg`, then you can allow - * this library to read it by defining the Input - * `[] () {return e_reg;}` - * and passing it to the library. - * - * An Output is a function which gets a state value from the OCPP library and applies it to the EVSE. - * For example, to let Smart Charging control the PWM signal of the Control Pilot, define the Output - * `[] (float p_max) {pwm = p_max / PWM_FACTOR;}` (simplified example) - * and pass it to the library. - * - * Configure the library with Inputs and Outputs once in the setup() function. - */ - -void setConnectorPluggedInput(std::function pluggedInput, unsigned int connectorId = 1); //Input about if an EV is plugged to this EVSE - -void setEnergyMeterInput(std::function energyInput, unsigned int connectorId = 1); //Input of the electricity meter register - -void setPowerMeterInput(std::function powerInput, unsigned int connectorId = 1); //Input of the power meter reading - -void setSmartChargingOutput(std::function chargingLimitOutput, unsigned int connectorId = 1); //Output for the Smart Charging limit - -/* - * Define the Inputs and Outputs of this library. (Advanced) - * - * These Inputs and Outputs are optional depending on the use case of your charger. - */ - -void setEvReadyInput(std::function evReadyInput, unsigned int connectorId = 1); //Input if EV is ready to charge (= J1772 State C) - -void setEvseReadyInput(std::function evseReadyInput, unsigned int connectorId = 1); //Input if EVSE allows charge (= PWM signal on) - -void addErrorCodeInput(std::function errorCodeInput, unsigned int connectorId = 1); //Input for Error codes (please refer to OCPP 1.6, Edit2, p. 71 and 72 for valid error codes) - -void addMeterValueInput(std::function valueInput, const char *measurand = nullptr, const char *unit = nullptr, const char *location = nullptr, const char *phase = nullptr, unsigned int connectorId = 1); //integrate further metering Inputs - -void addMeterValueInput(std::unique_ptr valueInput, unsigned int connectorId = 1); //integrate further metering Inputs (more extensive alternative) - -void setOnResetNotify(std::function onResetNotify); //call onResetNotify(isHard) before Reset. If you return false, Reset will be aborted. Optional - -void setOnResetExecute(std::function onResetExecute); //reset handler. This function should reboot this controller immediately. Already defined for the ESP32 on Arduino - -/* - * 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 - */ -void setOnUnlockConnectorInOut(std::function()> onUnlockConnectorInOut, unsigned int connectorId = 1); - -/* - * Set an Input/Output for setting the state of the connector lock. Called in the course of normal - * transactions (not as part of UnlockConnector). - * Param. values: - * - TxTrigger::Active if connector should be locked - * - TxTrigger::Inactive if connector should be unlocked - * Return values: - * - TxEnableState::Active if connector is locked and ready for transaction - * - TxEnableState::Inactive if connector lock is released - * - TxEnableState::Pending otherwise, e.g. if transitioning between the states - */ -void setConnectorLockInOut(std::function lockConnectorInOut, unsigned int connectorId = 1); - -/* - * Set an Input/Output to interact with a transaction-based energy meter. When this OCPP library is - * about to start a transaction, it toggles the Output to the energy meter to TxTrigger::Active so - * the energy meter can take a measurement right before a transaction. The same goes for the stop of - * transactions. With the Input, the energy meter signals if the measurement is ready and the - * transaction can finally start / stop. - * Param. values: - * - TxTrigger::Active if the tx-based meter should be in transaction-mode - * - TxTrigger::Inactive if the tx-based meter should be in non-transaction-mode - * Return values: - * - TxEnableState::Active if the tx-based meter confirms to be in the transaction-mode - * - TxEnableState::Inactive if the tx-based meter has transitioned into a non-transaction-mode - * - TxEnableState::Pending otherwise, e.g. if transitioning between the states - */ -void setTxBasedMeterInOut(std::function txMeterInOut, unsigned int connectorId = 1); - -/* - * Access further information about the internal state of the library - */ - -bool isOperative(unsigned int connectorId = 1); //if the charge point is operative or inoperative (see OCPP1.6 Edit2, p. 45) - -int getTransactionId(unsigned int connectorId = 1); //returns the txId if known, -1 if no transaction is running and 0 if txId not assigned yet - -const char *getTransactionIdTag(unsigned int connectorId = 1); //returns the authorization token if applicable, or nullptr otherwise - -void setOnResetRequest(OnReceiveReqListener onReceiveReq); - -/* - * Configure the device management - */ - -#if defined(AO_CUSTOM_UPDATER) || defined(AO_CUSTOM_WS) -#include - -/* - * 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 ArduinoOcpp/Tasks/FirmwareManagement/FirmwareService.h - */ -ArduinoOcpp::FirmwareService *getFirmwareService(); -#endif - -#if defined(AO_CUSTOM_DIAGNOSTICS) || defined(AO_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 ArduinoOcpp/Tasks/Diagnostics/DiagnosticsService.h - */ -ArduinoOcpp::DiagnosticsService *getDiagnosticsService(); -#endif - -namespace ArduinoOcpp { -class OcppEngine; -} - -//Get access to internal functions and data structures. The returned OcppEngine object allows -//you to bypass the facade functions of this header and implement custom functionality. -ArduinoOcpp::OcppEngine *getOcppEngine(); - -/* - * Deprecated functions or functions to be moved to ArduinoOcppExtended.h - */ - -void setOnSetChargingProfileRequest(OnReceiveReqListener onReceiveReq); //optional - -void setOnRemoteStartTransactionSendConf(OnSendConfListener onSendConf); - -void setOnRemoteStopTransactionSendConf(OnSendConfListener onSendConf); -void setOnRemoteStopTransactionReceiveReq(OnReceiveReqListener onReceiveReq); - -void setOnResetSendConf(OnSendConfListener onSendConf); - -bool startTransaction(const char *idTag, OnReceiveConfListener onConf = nullptr, OnAbortListener onAbort = nullptr, OnTimeoutListener onTimeout = nullptr, OnReceiveErrorListener onError = nullptr, std::unique_ptr timeout = nullptr); - -bool stopTransaction(OnReceiveConfListener onConf = nullptr, OnAbortListener onAbort = nullptr, OnTimeoutListener onTimeout = nullptr, OnReceiveErrorListener onError = nullptr, std::unique_ptr timeout = nullptr); - -#endif diff --git a/src/ArduinoOcpp/Core/Configuration.cpp b/src/ArduinoOcpp/Core/Configuration.cpp deleted file mode 100644 index 4285b554..00000000 --- a/src/ArduinoOcpp/Core/Configuration.cpp +++ /dev/null @@ -1,234 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include - -#include -#include -#include -#include - -namespace ArduinoOcpp { - -std::shared_ptr filesystem; - -template -std::shared_ptr> createConfiguration(const char *key, T value) { - - std::shared_ptr> configuration = std::make_shared>(); - - if (!configuration->setKey(key)) { - AO_DBG_ERR("Cannot set key! Abort"); - return nullptr; - } - - *configuration = value; - - return configuration; -} - -std::shared_ptr> createConfiguration(const char *key, const char *value) { - - std::shared_ptr> configuration = std::make_shared>(); - - if (!configuration->setKey(key)) { - AO_DBG_ERR("Cannot set key! Abort"); - return nullptr; - } - - if (!configuration->setValue(value, strlen(value) + 1)) { - AO_DBG_ERR("Cannot set value! Abort"); - return nullptr; - } - - return configuration; -} - -std::unique_ptr createConfigurationContainer(const char *filename) { - //create non-persistent Configuration store (i.e. lives only in RAM) if - // - Flash FS usage is switched off OR - // - Filename starts with "/volatile" - if (!filesystem || - !strncmp(filename, CONFIGURATION_VOLATILE, strlen(CONFIGURATION_VOLATILE))) { - return std::unique_ptr(new ConfigurationContainerVolatile(filename)); - } else { - //create persistent Configuration store. This is the normal caseS - return std::unique_ptr(new ConfigurationContainerFlash(filesystem, filename)); - } -} - -std::vector> configurationContainers; - -void addConfigurationContainer(std::shared_ptr container) { - configurationContainers.push_back(container); -} - -std::vector>::iterator getConfigurationContainersBegin() { - return configurationContainers.begin(); -} - -std::vector>::iterator getConfigurationContainersEnd() { - return configurationContainers.end(); -} - -std::shared_ptr getContainer(const char *filename) { - std::vector>::iterator container = std::find_if(configurationContainers.begin(), configurationContainers.end(), - [filename](std::shared_ptr &elem) { - return !strcmp(elem->getFilename(), filename); - }); - - if (container != configurationContainers.end()) { - return *container; - } else { - return nullptr; - } -} - -template -std::shared_ptr> declareConfiguration(const char *key, T defaultValue, const char *filename, bool remotePeerCanWrite, bool remotePeerCanRead, bool localClientCanWrite, bool rebootRequiredWhenChanged) { - //already existent? --> stored in last session --> do set default content, but set writepermission flag - - std::shared_ptr container = getContainer(filename); - - if (!container) { - AO_DBG_INFO("init new configurations container: %s", filename); - - container = createConfigurationContainer(filename); - configurationContainers.push_back(container); - - if (!container->load()) { - AO_DBG_WARN("Cannot load file contents. Path will be overwritten"); - } - } - - std::shared_ptr configuration = container->getConfiguration(key); - - if (configuration && strcmp(configuration->getSerializedType(), SerializedType::get())) { - AO_DBG_ERR("conflicting declared types. Discard old config"); - container->removeConfiguration(configuration); - configuration = nullptr; - } - - std::shared_ptr> configurationConcrete = std::static_pointer_cast>(configuration); - - if (!configurationConcrete) { - configurationConcrete = createConfiguration(key, defaultValue); - configuration = std::static_pointer_cast(configurationConcrete); - - if (!configuration) { - AO_DBG_ERR("Cannot find configuration stored from previous session and cannot create new one! Abort"); - return nullptr; - } - container->addConfiguration(configuration); - } - - if (!remotePeerCanWrite) - configuration->revokePermissionRemotePeerCanWrite(); - if (!remotePeerCanRead) - configuration->revokePermissionRemotePeerCanRead(); - if (!localClientCanWrite) - configuration->revokePermissionLocalClientCanWrite(); - if (rebootRequiredWhenChanged) - configuration->requireRebootWhenChanged(); - - return configurationConcrete; -} - -namespace Ocpp16 { - -std::shared_ptr getConfiguration(const char *key) { - std::shared_ptr result = nullptr; - - for (auto container = configurationContainers.begin(); container != configurationContainers.end(); container++) { - result = (*container)->getConfiguration(key); - if (result) - return result; - } - return nullptr; -} - -std::unique_ptr>> getAllConfigurations() { //TODO maybe change to iterator? - auto result = std::unique_ptr>>( - new std::vector>() - ); - - for (auto container = configurationContainers.begin(); container != configurationContainers.end(); container++) { - for (auto config = (*container)->configurationsIteratorBegin(); config != (*container)->configurationsIteratorEnd(); config++) { - if ((*config)->permissionRemotePeerCanRead()) { - result->push_back(*config); - } - } - } - - return result; -} - -} //end namespace Ocpp16 - -bool configuration_inited = false; - -bool configuration_init(std::shared_ptr _filesystem) { - if (configuration_inited) - return true; //configuration_init() already called; tolerate multiple calls so user can use this store for - //credentials outside ArduinoOcpp which need to be loaded before OCPP_initialize() - - filesystem = _filesystem; - - if (!filesystem) { - configuration_inited = true; - return true; //no filesystem, nothing can go wrong - } - - std::shared_ptr containerDefault = nullptr; - for (auto container = configurationContainers.begin(); container != configurationContainers.end(); container++) { - if (!strcmp((*container)->getFilename(), CONFIGURATION_FN)) { - containerDefault = (*container); - break; - } - } - - bool success = true; - - if (containerDefault) { - AO_DBG_DEBUG("Found default container before calling configuration_init(). If you added"); - AO_DBG_DEBUG(" > the container manually, please ensure to call load(). If not, it is a hint"); - AO_DBG_DEBUG(" > that declareConfiguration() was called too early"); - (void)0; - } else { - containerDefault = createConfigurationContainer(CONFIGURATION_FN); - if (!containerDefault->load()) { - AO_DBG_ERR("Loading default configurations file failed"); - success = false; - } - configurationContainers.push_back(containerDefault); - } - - configuration_inited = success; - return success; -} - -bool configuration_save() { - bool success = true; - - for (auto container = configurationContainers.begin(); container != configurationContainers.end(); container++) { - if (!(*container)->save()) { - success = false; - } - } - - return success; -} - -template std::shared_ptr> createConfiguration(const char *key, int value); -template std::shared_ptr> createConfiguration(const char *key, float value); -template std::shared_ptr> createConfiguration(const char *key, bool value); -template std::shared_ptr> createConfiguration(const char *key, const char * value); - -template std::shared_ptr> declareConfiguration(const char *key, int defaultValue, const char *filename, bool remotePeerCanWrite, bool remotePeerCanRead, bool localClientCanWrite, bool rebootRequiredWhenChanged); -template std::shared_ptr> declareConfiguration(const char *key, float defaultValue, const char *filename, bool remotePeerCanWrite, bool remotePeerCanRead, bool localClientCanWrite, bool rebootRequiredWhenChanged); -template std::shared_ptr> declareConfiguration(const char *key, bool defaultValue, const char *filename, bool remotePeerCanWrite, bool remotePeerCanRead, bool localClientCanWrite, bool rebootRequiredWhenChanged); -template std::shared_ptr> declareConfiguration(const char *key, const char *defaultValue, const char *filename, bool remotePeerCanWrite, bool remotePeerCanRead, bool localClientCanWrite, bool rebootRequiredWhenChanged); - -} //end namespace ArduinoOcpp diff --git a/src/ArduinoOcpp/Core/Configuration.h b/src/ArduinoOcpp/Core/Configuration.h deleted file mode 100644 index e7d9f911..00000000 --- a/src/ArduinoOcpp/Core/Configuration.h +++ /dev/null @@ -1,39 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef CONFIGURATION_H -#define CONFIGURATION_H - -#include -#include -#include -#include - -#include -#include - -#define CONFIGURATION_FN (AO_FILENAME_PREFIX "/arduino-ocpp.cnf") -#define CONFIGURATION_VOLATILE "/volatile" - -namespace ArduinoOcpp { - -template -std::shared_ptr> declareConfiguration(const char *key, T defaultValue, const char *filename = CONFIGURATION_FN, bool remotePeerCanWrite = true, bool remotePeerCanRead = true, bool localClientCanWrite = true, bool rebootRequiredWhenChanged = false); - -void addConfigurationContainer(std::shared_ptr container); -std::vector>::iterator getConfigurationContainersBegin(); -std::vector>::iterator getConfigurationContainersEnd(); - - -namespace Ocpp16 { - - std::shared_ptr getConfiguration(const char *key); - std::unique_ptr>> getAllConfigurations(); -} - -bool configuration_init(std::shared_ptr filesytem); -bool configuration_save(); - -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/Core/ConfigurationContainer.cpp b/src/ArduinoOcpp/Core/ConfigurationContainer.cpp deleted file mode 100644 index 9b3418b1..00000000 --- a/src/ArduinoOcpp/Core/ConfigurationContainer.cpp +++ /dev/null @@ -1,74 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include - -namespace ArduinoOcpp { - -std::shared_ptr ConfigurationContainer::getConfiguration(const char *key) { - for (std::vector>::iterator configuration = configurations.begin(); configuration != configurations.end(); configuration++) { - if ((*configuration)->keyEquals(key)) { - return *configuration; - } - } - return NULL; -} - -bool ConfigurationContainer::removeConfiguration(std::shared_ptr configuration) { - - auto config = configurations.begin(); - auto config_rev = configurations_revision.begin(); - while (config != configurations.end()) { - if ((*config) == configuration) { - configurations.erase(config); - if (config_rev != configurations_revision.end()) - configurations_revision.erase(config_rev); - return true; - } - config++; - if (config_rev != configurations_revision.end()) - config_rev++; - } - - return false; -} - -void ConfigurationContainer::addConfiguration(std::shared_ptr configuration) { - configurations.push_back(configuration); -} - -bool ConfigurationContainer::configurationsUpdated() { - bool updated = false; - - auto config = configurations.begin(); - auto config_rev = configurations_revision.begin(); - while (config != configurations.end()) { - if (config_rev == configurations_revision.end()) { - //vectors are not the same length -> added configurations - updated = true; - break; - } - - if ((*config)->getValueRevision() != *config_rev) { - updated = true; - break; - } - - config++; - config_rev++; - } - - if (updated) { - configurations_revision.erase(config_rev, configurations_revision.end()); - - while (config != configurations.end()) { - configurations_revision.push_back((*config)->getValueRevision()); - config++; - } - } - - return updated; -} - -} //end namespace ArduinoOcpp diff --git a/src/ArduinoOcpp/Core/ConfigurationContainer.h b/src/ArduinoOcpp/Core/ConfigurationContainer.h deleted file mode 100644 index 1074ff96..00000000 --- a/src/ArduinoOcpp/Core/ConfigurationContainer.h +++ /dev/null @@ -1,54 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef CONFIGURATIONCONTAINER_H -#define CONFIGURATIONCONTAINER_H - -#include -#include - -#include - -namespace ArduinoOcpp { - -class ConfigurationContainer { -private: - std::vector configurations_revision; - const char *filename; - -protected: - std::vector> configurations; - - ConfigurationContainer(const char *filename) : filename(filename) { } - - //Checks if configurations_revision is equal to (for all) configurations->getValueRevision(). If not, it refreshes the record - bool configurationsUpdated(); -public: - virtual ~ConfigurationContainer() = default; - - virtual bool load() = 0; - - virtual bool save() = 0; - - const char *getFilename() {return filename;}; - - std::shared_ptr getConfiguration(const char *key); - std::vector>::iterator configurationsIteratorBegin() {return configurations.begin();} - std::vector>::iterator configurationsIteratorEnd() {return configurations.end();} - bool removeConfiguration(std::shared_ptr configuration); - void addConfiguration(std::shared_ptr configuration); -}; - -class ConfigurationContainerVolatile : public ConfigurationContainer { -public: - ConfigurationContainerVolatile(const char *filename) : ConfigurationContainer(filename) { } - - bool load() {return true;} - - bool save() {return true;} -}; - -} //end namespace ArduinoOcpp - -#endif diff --git a/src/ArduinoOcpp/Core/ConfigurationContainerFlash.cpp b/src/ArduinoOcpp/Core/ConfigurationContainerFlash.cpp deleted file mode 100644 index d8717c13..00000000 --- a/src/ArduinoOcpp/Core/ConfigurationContainerFlash.cpp +++ /dev/null @@ -1,201 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include - -#include - -#define MAX_FILE_SIZE 4000 -#define MAX_CONFIGURATIONS 50 -#define MAX_CONFJSON_CAPACITY 4000 - -namespace ArduinoOcpp { - -bool ConfigurationContainerFlash::load() { - - if (!filesystem) { - return false; - } - - if (configurations.size() > 0) { - AO_DBG_ERR("Error: declared configurations before calling container->load(). " \ - "All previously declared values won't be written back"); - (void)0; - } - - size_t file_size = 0; - if (filesystem->stat(getFilename(), &file_size) != 0 // file does not exist - || file_size == 0) { // file exists, but empty - AO_DBG_DEBUG("Populate FS: create configuration file"); - return save(); - } - - if (file_size > MAX_FILE_SIZE) { - AO_DBG_ERR("Unable to initialize: filesize is too long"); - return false; - } - - auto file = filesystem->open(getFilename(), "r"); - - if (!file) { - AO_DBG_ERR("Unable to initialize: could not open configuration file %s", getFilename()); - return false; - } - - auto jsonCapacity = std::max(file_size, (size_t) 256); - DynamicJsonDocument doc {0}; - DeserializationError err = DeserializationError::NoMemory; - - while (err == DeserializationError::NoMemory) { - if (jsonCapacity > MAX_CONFJSON_CAPACITY) { - AO_DBG_ERR("JSON capacity exceeded"); - return false; - } - - AO_DBG_DEBUG("Configs JSON capacity: %zu", jsonCapacity); - - doc = DynamicJsonDocument(jsonCapacity); - ArduinoJsonFileAdapter file_adapt {file.get()}; - err = deserializeJson(doc, file_adapt); - - jsonCapacity *= 3; - jsonCapacity /= 2; - file->seek(0); - } - - if (err) { - AO_DBG_ERR("Unable to initialize: config file deserialization failed: %s", err.c_str()); - return false; - } - - JsonObject configHeader = doc["head"]; - - if (strcmp(configHeader["content-type"] | "Invalid", "ao_configuration_file")) { - AO_DBG_ERR("Unable to initialize: unrecognized configuration file format"); - return false; - } - - if (strcmp(configHeader["version"] | "Invalid", "1.1")) { - AO_DBG_ERR("Unable to initialize: unsupported version"); - return false; - } - - JsonArray configurationsArray = doc["configurations"]; - if (configurationsArray.size() > MAX_CONFIGURATIONS) { - AO_DBG_ERR("Unable to initialize: configurations_len is too big (=%zu)", configurationsArray.size()); - return false; - } - - for (JsonObject config : configurationsArray) { - const char *type = config["type"] | "Undefined"; - - std::shared_ptr configuration = nullptr; - - if (!strcmp(type, SerializedType::get())){ - configuration = std::make_shared>(config); - } else if (!strcmp(type, SerializedType::get())){ - configuration = std::make_shared>(config); - } else if (!strcmp(type, SerializedType::get())){ - configuration = std::make_shared>(config); - } else if (!strcmp(type, SerializedType::get())){ - configuration = std::make_shared>(config); - } - - if (configuration) { - configurations.push_back(configuration); - } else { - AO_DBG_ERR("Initialization fault: could not read key-value pair %s of type %s", config["key"].as(), config["type"].as()); - } - } - - configurationsUpdated(); - - AO_DBG_DEBUG("Initialization finished"); - return true; -} - -bool ConfigurationContainerFlash::save() { - - if (!filesystem) { - return false; - } - - if (!configurationsUpdated()) { - return true; //nothing to be done - } - - size_t file_size = 0; - if (filesystem->stat(getFilename(), &file_size) == 0) { - filesystem->remove(getFilename()); - } - - auto file = filesystem->open(getFilename(), "w"); - if (!file) { - AO_DBG_ERR("Unable to save: could not open configuration file %s", getFilename()); - return false; - } - - size_t jsonCapacity = 2 * JSON_OBJECT_SIZE(2); //head + configurations + head payload - - std::vector> entries; - - for (auto config = configurations.begin(); config != configurations.end(); config++) { - std::shared_ptr entry = (*config)->toJsonStorageEntry(); - if (entry) { - entries.push_back(entry); - } - if (entries.size() >= MAX_CONFIGURATIONS) { - AO_DBG_ERR("Max No of configratuions exceeded. Crop configs file (by FCFS)"); - break; - } - } - - jsonCapacity += JSON_ARRAY_SIZE(entries.size()); //length of configurations - for (auto entry = entries.begin(); entry != entries.end(); entry++) { - jsonCapacity += (*entry)->capacity(); - } - - jsonCapacity = std::max(jsonCapacity, (size_t) 256); - DynamicJsonDocument doc {0}; - bool jsonDocOverflow = true; - - while (jsonDocOverflow) { - if (jsonCapacity > MAX_CONFJSON_CAPACITY) { - AO_DBG_ERR("JSON capacity exceeded"); - return false; - } - - file->seek(0); - - doc = DynamicJsonDocument(jsonCapacity); - JsonObject head = doc.createNestedObject("head"); - head["content-type"] = "ao_configuration_file"; - head["version"] = "1.1"; - - JsonArray configurationsArray = doc.createNestedArray("configurations"); - for (auto entry = entries.begin(); entry != entries.end(); entry++) { - configurationsArray.add((*entry)->as()); - } - - ArduinoJsonFileAdapter file_adapt {file.get()}; - size_t written = serializeJson(doc, file_adapt); - - jsonCapacity *= 3; - jsonCapacity /= 2; - jsonDocOverflow = doc.overflowed(); - - if (!jsonDocOverflow && written < 20) { //plausibility check - AO_DBG_ERR("Config serialization: unkown error for file %s", getFilename()); - return false; - } - } - - //success - AO_DBG_DEBUG("Saving configurations finished"); - return true; -} - -} //end namespace ArduinoOcpp diff --git a/src/ArduinoOcpp/Core/ConfigurationContainerFlash.h b/src/ArduinoOcpp/Core/ConfigurationContainerFlash.h deleted file mode 100644 index 865756de..00000000 --- a/src/ArduinoOcpp/Core/ConfigurationContainerFlash.h +++ /dev/null @@ -1,29 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef CONFIGURATIONCONTAINERFLASH_H -#define CONFIGURATIONCONTAINERFLASH_H - -#include -#include - -namespace ArduinoOcpp { - -class ConfigurationContainerFlash : public ConfigurationContainer { - std::shared_ptr filesystem; -public: - ConfigurationContainerFlash(std::shared_ptr filesystem, const char *filename) : - ConfigurationContainer(filename), filesystem(filesystem) { } - - ~ConfigurationContainerFlash() = default; - - bool load(); - - bool save(); - -}; - -} //end namespace ArduinoOcpp - -#endif diff --git a/src/ArduinoOcpp/Core/ConfigurationKeyValue.cpp b/src/ArduinoOcpp/Core/ConfigurationKeyValue.cpp deleted file mode 100644 index acbebab2..00000000 --- a/src/ArduinoOcpp/Core/ConfigurationKeyValue.cpp +++ /dev/null @@ -1,329 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include - -#include -#include -#include - -#define KEY_MAXLEN 60 -#define STRING_VAL_MAXLEN 2000 //allow TLS certificates in ... - -namespace ArduinoOcpp { - -AbstractConfiguration::AbstractConfiguration() { - -} - -AbstractConfiguration::AbstractConfiguration(JsonObject &storedKeyValuePair) { - if (!setKey(storedKeyValuePair["key"] | "")) { - AO_DBG_ERR("validation error"); - } -} - -AbstractConfiguration::~AbstractConfiguration() { - -} - -void AbstractConfiguration::printKey() { - AO_CONSOLE_PRINTF("%s", key.c_str()); - (void)0; -} - -size_t AbstractConfiguration::getStorageHeaderJsonCapacity() { - return JSON_OBJECT_SIZE(1) //key - + key.size() + 1; -} - -void AbstractConfiguration::storeStorageHeader(JsonObject &keyValuePair) { - keyValuePair["key"] = key; -} - -size_t AbstractConfiguration::getOcppMsgHeaderJsonCapacity() { - return JSON_OBJECT_SIZE(2) //key + readonly field - + key.size() + 1; -} - -void AbstractConfiguration::storeOcppMsgHeader(JsonObject &keyValuePair) { - keyValuePair["key"] = key; - if (remotePeerCanWrite) { - keyValuePair["readonly"] = false; - } else { - keyValuePair["readonly"] = true; - } -} - -bool AbstractConfiguration::isValid() { - return initializedValue && !key.empty(); -} - -bool AbstractConfiguration::setKey(const char *newKey) { - if (!key.empty()) { - AO_DBG_ERR("cannot override key"); - return false; - } - - if (!newKey || *newKey == '\0') { - AO_DBG_ERR("invalid argument"); - return false; - } - - key = newKey; - return true; -} - -void AbstractConfiguration::requireRebootWhenChanged() { - rebootRequiredWhenChanged = true; -} - -bool AbstractConfiguration::requiresRebootWhenChanged() { - return rebootRequiredWhenChanged; -} - -uint16_t AbstractConfiguration::getValueRevision() { - return value_revision; -} - -bool AbstractConfiguration::keyEquals(const char *other) { - return !key.compare(other); -} - -template -Configuration::Configuration() { - -} - -Configuration::Configuration() { - -} - -template -Configuration::Configuration(JsonObject &storedKeyValuePair) : AbstractConfiguration(storedKeyValuePair) { - auto jsonEntry = storedKeyValuePair["value"].as(); - if (jsonEntry.is()) { - this->operator=(jsonEntry.as()); - } else { - AO_DBG_ERR("Type mismatch: cannot deserialize Json to given type"); - } -} - -//helper functions -void printValue(int value) { - AO_CONSOLE_PRINTF("%i", value); -} - -void printValue(float value) { - AO_CONSOLE_PRINTF("%f", value); -} - -void printValue(bool value) { - AO_CONSOLE_PRINTF("%s", value ? "true" : "false"); -} - -template -const T &Configuration::operator=(const T & newVal) { - - if (permissionLocalClientCanWrite() || !initializedValue) { - if (AO_DBG_LEVEL >= AO_DL_DEBUG && !initializedValue) { - AO_DBG_DEBUG("Initialized config object:"); - AO_CONSOLE_PRINTF("[AO] > Key = "); - printKey(); - AO_CONSOLE_PRINTF(", value = "); - printValue(newVal); - AO_CONSOLE_PRINTF("\n"); - } - initializedValue = true; - if (value != newVal) { - value_revision++; - } - value = newVal; - } else { - AO_DBG_ERR("Tried to override read-only configuration:"); - AO_CONSOLE_PRINTF("[AO] > Key = "); - printKey(); - AO_CONSOLE_PRINTF("\n"); - } - return newVal; -} - -template -Configuration::operator T() { - return value; -} - -template -bool Configuration::isValid() { - return AbstractConfiguration::isValid(); -} - -template -size_t Configuration::getValueJsonCapacity() { - return JSON_OBJECT_SIZE(1); -} - -size_t Configuration::getValueJsonCapacity() { - return JSON_OBJECT_SIZE(1) + value.size() + 1; -} - -template -std::shared_ptr Configuration::toJsonStorageEntry() { - if (!isValid()) { - return nullptr; - } - size_t capacity = getStorageHeaderJsonCapacity() - + getValueJsonCapacity() - + JSON_OBJECT_SIZE(3); //type, header, value - - std::shared_ptr doc = std::make_shared(capacity); - JsonObject keyValuePair = doc->to(); - keyValuePair["type"] = SerializedType::get(); - storeStorageHeader(keyValuePair); - keyValuePair["value"] = value; - return doc; -} - -int toCStringValue(char *buf, size_t length, int value) { - return snprintf(buf, length, "%d", value); -} - -int toCStringValue(char *buf, size_t length, float value) { - int ilength = (int) std::min((size_t) 100, length); - return snprintf(buf, length, "%.*g", ilength >= 7 ? ilength - 7 : 0, value); -} - -int toCStringValue(char *buf, size_t length, bool value) { - return snprintf(buf, length, "%s", value ? "true" : "false"); -} - -template -std::shared_ptr Configuration::toJsonOcppMsgEntry() { - if (!isValid()) { - return nullptr; - } - const size_t VALUE_MAXSIZE = 50; - size_t capacity = getOcppMsgHeaderJsonCapacity() - + getValueJsonCapacity() + VALUE_MAXSIZE - + JSON_OBJECT_SIZE(2); // header, value - - std::shared_ptr doc = std::make_shared(capacity); - JsonObject keyValuePair = doc->to(); - storeOcppMsgHeader(keyValuePair); - char value_str [VALUE_MAXSIZE] = {'\0'}; - toCStringValue(value_str, VALUE_MAXSIZE, value); - keyValuePair["value"] = value_str; - return doc; -} - -std::shared_ptr Configuration::toJsonStorageEntry() { - if (!isValid()) { - return nullptr; - } - size_t capacity = getStorageHeaderJsonCapacity() - + getValueJsonCapacity() - + JSON_OBJECT_SIZE(3); //type, header, value - - std::shared_ptr doc = std::make_shared(capacity); - JsonObject keyValuePair = doc->to(); - keyValuePair["type"] = SerializedType::get(); - storeStorageHeader(keyValuePair); - keyValuePair["value"] = value; - return doc; -} - -std::shared_ptr Configuration::toJsonOcppMsgEntry() { - if (!isValid()) { - return nullptr; - } - size_t capacity = getOcppMsgHeaderJsonCapacity() - + getValueJsonCapacity() - + JSON_OBJECT_SIZE(2); // header, value - - std::shared_ptr doc = std::make_shared(capacity); - JsonObject keyValuePair = doc->to(); - storeOcppMsgHeader(keyValuePair); - keyValuePair["value"] = value; - return doc; -} - -Configuration::Configuration(JsonObject &storedKeyValuePair) : AbstractConfiguration(storedKeyValuePair) { - if (storedKeyValuePair["value"].as().is()) { - const char *storedValue = storedKeyValuePair["value"].as().as(); - if (storedValue) { - size_t storedValueSize = strlen(storedValue) + 1; - storedValueSize++; - setValue(storedValue, storedValueSize); - } else { - AO_DBG_WARN("Stored value is empty"); - } - } else { - AO_DBG_ERR("Type mismatch: cannot deserialize Json to given type"); - } -} - -Configuration::~Configuration() { - -} - -bool Configuration::setValue(const char *new_value, size_t buffsize) { - if (!permissionLocalClientCanWrite() && initializedValue) { - AO_DBG_ERR("Tried to override read-only configuration:"); - AO_CONSOLE_PRINTF("[AO] > Key = "); - return false; - } - - if (!new_value) { - AO_DBG_ERR("Argument is null. No change"); - return false; - } - - if (value.compare(new_value) || !initializedValue) { - value = new_value; - value_revision++; - } - - if (AO_DBG_LEVEL >= AO_DL_DEBUG && !initializedValue) { - AO_DBG_DEBUG("Initialized config object:"); - AO_CONSOLE_PRINTF("[AO] > Key = "); - printKey(); - AO_CONSOLE_PRINTF(", value = %s\n", value.c_str()); - } - initializedValue = true; - return true; -} - -const char *Configuration::operator=(const char *newVal) { - if (!setValue(newVal, strlen(newVal) + 1)) { - AO_DBG_ERR("Setting value in operator= was unsuccessful"); - } - return newVal; -} - -Configuration::operator const char*() { - return value.c_str(); -} - -bool Configuration::isValid() { - return AbstractConfiguration::isValid(); -} - -size_t Configuration::getBuffsize() { - return value.size() + 1; -} - -void Configuration::setValidator(std::function validator) { - this->validator = validator; -} - -std::function Configuration::getValidator() { - return this->validator; -} - -template class Configuration; -template class Configuration; -template class Configuration; -template class Configuration; - -} //end namespace ArduinoOcpp diff --git a/src/ArduinoOcpp/Core/ConfigurationKeyValue.h b/src/ArduinoOcpp/Core/ConfigurationKeyValue.h deleted file mode 100644 index 4038a89c..00000000 --- a/src/ArduinoOcpp/Core/ConfigurationKeyValue.h +++ /dev/null @@ -1,117 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef CONFIGURATIONKEYVALUE_H -#define CONFIGURATIONKEYVALUE_H - -#include -#include -#include -#include - -namespace ArduinoOcpp { - -class AbstractConfiguration { -private: - std::string key; - - bool rebootRequiredWhenChanged = false; - - bool remotePeerCanWrite = true; - bool remotePeerCanRead = true; - bool localClientCanWrite = true; -protected: - uint16_t value_revision = 0; //number of memory-relevant changes of subclass-member "value" (deleting counts too). This will be important for the client to detect if there was a change - bool initializedValue = false; - - AbstractConfiguration(); - AbstractConfiguration(JsonObject &storedKeyValuePair); - size_t getStorageHeaderJsonCapacity(); - void storeStorageHeader(JsonObject &keyValuePair); - size_t getOcppMsgHeaderJsonCapacity(); - void storeOcppMsgHeader(JsonObject &keyValuePair); - bool isValid(); - - bool permissionLocalClientCanWrite() {return localClientCanWrite;} -public: - virtual ~AbstractConfiguration(); - bool setKey(const char *key); - void printKey(); - - void requireRebootWhenChanged(); - bool requiresRebootWhenChanged(); - - uint16_t getValueRevision(); - bool keyEquals(const char *other); - - virtual std::shared_ptr toJsonStorageEntry() = 0; - virtual std::shared_ptr toJsonOcppMsgEntry() = 0; - - virtual const char *getSerializedType() = 0; - - bool permissionRemotePeerCanWrite() {return remotePeerCanWrite;} - bool permissionRemotePeerCanRead() {return remotePeerCanRead;} - void revokePermissionRemotePeerCanWrite() {remotePeerCanWrite = false;} - void revokePermissionRemotePeerCanRead() {remotePeerCanRead = false;} - void revokePermissionLocalClientCanWrite() {localClientCanWrite = false;} -}; - - -template -struct SerializedType { - static const char *get() {return "undefined";} -}; - -template<> struct SerializedType {static const char *get() {return "int";}}; -template<> struct SerializedType {static const char *get() {return "float";}}; -template<> struct SerializedType {static const char *get() {return "bool";}}; -template<> struct SerializedType {static const char *get() {return "string";}}; - -template -class Configuration : public AbstractConfiguration { -private: - T value; - size_t getValueJsonCapacity(); -public: - Configuration(); - Configuration(JsonObject &storedKeyValuePair); - const T &operator=(const T & newVal); - operator T(); - bool isValid(); - - std::shared_ptr toJsonStorageEntry(); - std::shared_ptr toJsonOcppMsgEntry(); - - const char *getSerializedType() {return SerializedType::get();} //returns "int" or "float" as written to the configuration Json file -}; - -template <> -class Configuration : public AbstractConfiguration { -private: - std::string value; - size_t getValueJsonCapacity(); - - std::function validator; -public: - Configuration(); - Configuration(JsonObject &storedKeyValuePair); - ~Configuration(); - bool setValue(const char *newVal, size_t buffsize); - const char *operator=(const char *newVal); - operator const char*(); - bool isValid(); - size_t getBuffsize(); - - std::shared_ptr toJsonStorageEntry(); - std::shared_ptr toJsonOcppMsgEntry(); - - const char *getSerializedType() {return SerializedType::get();} - - void setValidator(std::function validator); - std::function getValidator(); -}; - -} //end namespace ArduinoOcpp - -#endif diff --git a/src/ArduinoOcpp/Core/FilesystemAdapter.cpp b/src/ArduinoOcpp/Core/FilesystemAdapter.cpp deleted file mode 100644 index 3c04eb69..00000000 --- a/src/ArduinoOcpp/Core/FilesystemAdapter.cpp +++ /dev/null @@ -1,389 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include //FilesystemOpt -#include - -#ifndef AO_DEACTIVATE_FLASH - -/* - * Platform specific implementations. Currently supported: - * - Arduino LittleFs - * - Arduino SPIFFS - * - ESP-IDF SPIFFS - * - POSIX-like API (tested on Ubuntu 20.04) - * - * You can add support for other file systems by passing a custom adapter to OCPP_initialize(...) - */ - - -#if AO_USE_FILEAPI == ARDUINO_LITTLEFS || AO_USE_FILEAPI == ARDUINO_SPIFFS - -#if AO_USE_FILEAPI == ARDUINO_LITTLEFS -#include -#define USE_FS LITTLEFS -#elif AO_USE_FILEAPI == ARDUINO_SPIFFS -#include -#define USE_FS SPIFFS -#endif - -namespace ArduinoOcpp { - -class ArduinoFileAdapter : public FileAdapter { - File file; -public: - ArduinoFileAdapter(File&& file) : file(file) {} - - ~ArduinoFileAdapter() { - if (file) { - file.close(); - } - } - - int read() override { - return file.read(); - }; - size_t read(char *buf, size_t len) override { - return file.readBytes(buf, len); - } - size_t write(const char *buf, size_t len) override { - return file.printf("%.*s", len, buf); - } - size_t seek(size_t offset) override { - return file.seek(offset); - } -}; - -class ArduinoFilesystemAdapter : public FilesystemAdapter { -private: - bool valid = false; - FilesystemOpt config; -public: - ArduinoFilesystemAdapter(FilesystemOpt config) : config(config) { - valid = true; - - if (config.mustMount()) { -#if AO_USE_FILEAPI == ARDUINO_LITTLEFS - if(!USE_FS.begin(config.formatOnFail())) { - AO_DBG_ERR("Error while mounting LITTLEFS"); - valid = false; - } else { - AO_DBG_DEBUG("LittleFS mount success"); - } -#elif AO_USE_FILEAPI == ARDUINO_SPIFFS - //ESP8266 - SPIFFSConfig cfg; - cfg.setAutoFormat(config.formatOnFail()); - SPIFFS.setConfig(cfg); - - if (!SPIFFS.begin()) { - AO_DBG_ERR("Unable to initialize: unable to mount SPIFFS"); - valid = false; - } -#else -#error -#endif - } //end if mustMount() - -#if AO_DBG_LEVEL >= AO_DL_DEBUG - if (valid) { - File root = USE_FS.open("/"); - File file = root.openNextFile(); - - AO_DBG_DEBUG("filesystem content:"); - while(file) { - AO_CONSOLE_PRINTF("[AO] > %s\n", file.name() ? file.name() : "null"); - file = root.openNextFile(); - } - - root.close(); - } -#endif //endif AO_DBG_LEVEL >= AO_DL_DBUG - } - - ~ArduinoFilesystemAdapter() { - if (config.mustMount()) { - USE_FS.end(); - } - } - - operator bool() {return valid;} - - int stat(const char *path, size_t *size) override { - if (!USE_FS.exists(path)) { - return -1; - } - File f = USE_FS.open(path, "r"); - if (!f) { - return -1; - } - - int status = -1; - if (!f.isDirectory()) { - *size = f.size(); - status = 0; - } else { - //fetch more information for directory when ArduinoOcpp also uses them - //status = 0; - } - - f.close(); - return status; - } - - std::unique_ptr open(const char *fn, const char *mode) override { - File file = USE_FS.open(fn, mode); - if (file && !file.isDirectory()) { - AO_DBG_DEBUG("File open successful: %s", fn); - return std::unique_ptr(new ArduinoFileAdapter(std::move(file))); - } else { - return nullptr; - } - } - bool remove(const char *fn) override { - return USE_FS.remove(fn); - }; -}; - -std::weak_ptr filesystemCache; - -std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { - - if (auto cached = filesystemCache.lock()) { - return cached; - } - - if (!config.accessAllowed()) { - AO_DBG_DEBUG("Access to Arduino FS not allowed by config"); - return nullptr; - } - - auto fs_concrete = new ArduinoFilesystemAdapter(config); - auto fs = std::shared_ptr(fs_concrete); - filesystemCache = fs; - - if (*fs_concrete) { - return fs; - } else { - return nullptr; - } -} - -} //end namespace ArduinoOcpp - -#elif AO_USE_FILEAPI == ESPIDF_SPIFFS - -#include -#include "esp_spiffs.h" - -namespace ArduinoOcpp { - -class EspIdfFileAdapter : public FileAdapter { - FILE *file {nullptr}; -public: - EspIdfFileAdapter(FILE *file) : file(file) {} - - ~EspIdfFileAdapter() { - fclose(file); - } - - size_t read(char *buf, size_t len) override { - return fread(buf, 1, len, file); - } - - size_t write(const char *buf, size_t len) override { - return fwrite(buf, 1, len, file); - } - - size_t seek(size_t offset) override { - return fseek(file, offset, SEEK_SET); - } - - int read() { - return fgetc(file); - } -}; - -class EspIdfFilesystemAdapter : public FilesystemAdapter { -public: - FilesystemOpt config; -public: - EspIdfFilesystemAdapter(FilesystemOpt config) : config(config) { } - - ~EspIdfFilesystemAdapter() { - if (config.mustMount()) { - esp_vfs_spiffs_unregister("ao"); //partition label - AO_DBG_DEBUG("SPIFFS unmounted"); - } - } - - int stat(const char *path, size_t *size) override { - struct ::stat st; - auto ret = ::stat(path, &st); - if (ret == 0) { - *size = st.st_size; - } - return ret; - } - - std::unique_ptr open(const char *fn, const char *mode) override { - auto file = fopen(fn, mode); - if (file) { - return std::unique_ptr(new EspIdfFileAdapter(std::move(file))); - } else { - AO_DBG_DEBUG("Failed to open file path %s", fn); - return nullptr; - } - } - - bool remove(const char *fn) override { - return unlink(fn) == 0; - } -}; - -std::weak_ptr filesystemCache; - -std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { - - if (auto cached = filesystemCache.lock()) { - return cached; - } - - if (!config.accessAllowed()) { - AO_DBG_DEBUG("Access to ESP-IDF SPIFFS not allowed by config"); - return nullptr; - } - - bool mounted = true; - - if (config.mustMount()) { - mounted = false; - - esp_vfs_spiffs_conf_t conf = { - .base_path = AO_FILENAME_PREFIX, - .partition_label = "ao", //also see deconstructor - .max_files = 5, - .format_if_mount_failed = config.formatOnFail() - }; - - esp_err_t ret = esp_vfs_spiffs_register(&conf); - - if (ret == ESP_OK) { - mounted = true; - AO_DBG_DEBUG("SPIFFS mounted"); - } else { - if (ret == ESP_FAIL) { - AO_DBG_ERR("Failed to mount or format filesystem"); - } else if (ret == ESP_ERR_NOT_FOUND) { - AO_DBG_ERR("Failed to find SPIFFS partition"); - } else { - AO_DBG_ERR("Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); - } - } - } - - if (mounted) { - auto fs = std::shared_ptr(new EspIdfFilesystemAdapter(config)); - filesystemCache = fs; - return fs; - } else { - return nullptr; - } -} - -} //end namespace ArduinoOcpp - -#elif AO_USE_FILEAPI == POSIX_FILEAPI - -#include -#include - -namespace ArduinoOcpp { - -class PosixFileAdapter : public FileAdapter { - FILE *file {nullptr}; -public: - PosixFileAdapter(FILE *file) : file(file) {} - - ~PosixFileAdapter() { - fclose(file); - } - - size_t read(char *buf, size_t len) override { - return fread(buf, 1, len, file); - } - - size_t write(const char *buf, size_t len) override { - return fwrite(buf, 1, len, file); - } - - size_t seek(size_t offset) override { - return fseek(file, offset, SEEK_SET); - } - - int read() { - return fgetc(file); - } -}; - -class PosixFilesystemAdapter : public FilesystemAdapter { -public: - FilesystemOpt config; -public: - PosixFilesystemAdapter(FilesystemOpt config) : config(config) { } - - ~PosixFilesystemAdapter() = default; - - int stat(const char *path, size_t *size) override { - struct ::stat st; - auto ret = ::stat(path, &st); - if (ret == 0) { - *size = st.st_size; - } - return ret; - } - - std::unique_ptr open(const char *fn, const char *mode) override { - auto file = fopen(fn, mode); - if (file) { - return std::unique_ptr(new PosixFileAdapter(std::move(file))); - } else { - AO_DBG_DEBUG("Failed to open file path %s", fn); - return nullptr; - } - } - - bool remove(const char *fn) override { - return ::remove(fn) == 0; - } -}; - -std::weak_ptr filesystemCache; - -std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { - - if (auto cached = filesystemCache.lock()) { - return cached; - } - - if (!config.accessAllowed()) { - AO_DBG_DEBUG("Access to FS not allowed by config"); - return nullptr; - } - - if (config.mustMount()) { - AO_DBG_DEBUG("Skip mounting on UNIX host"); - } - - auto fs = std::shared_ptr(new PosixFilesystemAdapter(config)); - filesystemCache = fs; - return fs; -} - -} //end namespace ArduinoOcpp - -#endif //switch-case AO_USE_FILEAPI - -#endif //!AO_DEACTIVATE_FLASH diff --git a/src/ArduinoOcpp/Core/FilesystemAdapter.h b/src/ArduinoOcpp/Core/FilesystemAdapter.h deleted file mode 100644 index 47e788b0..00000000 --- a/src/ArduinoOcpp/Core/FilesystemAdapter.h +++ /dev/null @@ -1,88 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef AO_FILESYSTEMADAPTER_H -#define AO_FILESYSTEMADAPTER_H - -#ifndef AO_FILENAME_PREFIX -#define AO_FILENAME_PREFIX "" -#endif - -#define MAX_PATH_SIZE 30 - -#define ARDUINO_LITTLEFS 1 -#define ARDUINO_SPIFFS 2 -#define ESPIDF_SPIFFS 3 -#define POSIX_FILEAPI 4 - -#include -#include - -namespace ArduinoOcpp { - -class FileAdapter { -public: - virtual ~FileAdapter() = default; - virtual size_t read(char *buf, size_t len) = 0; - virtual size_t write(const char *buf, size_t len) = 0; - virtual size_t seek(size_t offset) = 0; - // virtual void close() = 0; implemented in deconstructor - - virtual int read() = 0; -}; - -class FilesystemAdapter { -public: - virtual ~FilesystemAdapter() = default; - virtual int stat(const char *path, size_t *size) = 0; - virtual std::unique_ptr open(const char *fn, const char *mode) = 0; - virtual bool remove(const char *fn) = 0; -}; - -} //end namespace ArduinoOcpp - -#ifndef AO_DEACTIVATE_FLASH - -//Set default parameters; assume usage with Arduino if no build flags are present -#ifndef AO_USE_FILEAPI -#if AO_PLATFORM == AO_PLATFORM_ARDUINO -#if defined(ESP32) -#define AO_USE_FILEAPI ARDUINO_LITTLEFS -#else -#define AO_USE_FILEAPI ARDUINO_SPIFFS -#endif -#elif AO_PLATFORM == AO_PLATFORM_ESPIDF -#define AO_USE_FILEAPI ESPIDF_SPIFFS -#elif AO_PLATFORM == AO_PLATFORM_UNIX -#define AO_USE_FILEAPI POSIX_FILEAPI -#endif //switch-case AO_PLATFORM -#endif //ndef AO_USE_FILEAPI - -/* - * Platform specific implementations. Currently supported: - * - Arduino LittleFs - * - Arduino SPIFFS - * - ESP-IDF SPIFFS - * - POSIX-like API (tested on Ubuntu 20.04) - * - * You can add support for other file systems by passing a custom adapter to OCPP_initialize(...) - */ - -#if AO_USE_FILEAPI == ARDUINO_LITTLEFS || \ - AO_USE_FILEAPI == ARDUINO_SPIFFS || \ - AO_USE_FILEAPI == ESPIDF_SPIFFS || \ - AO_USE_FILEAPI == POSIX_FILEAPI - -#include - -namespace ArduinoOcpp { - -std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config); - -} //end namespace ArduinoOcpp -#endif - -#endif //ndef AO_DEACTIVATE_FLASH - -#endif diff --git a/src/ArduinoOcpp/Core/FilesystemUtils.cpp b/src/ArduinoOcpp/Core/FilesystemUtils.cpp deleted file mode 100644 index 8ed689f8..00000000 --- a/src/ArduinoOcpp/Core/FilesystemUtils.cpp +++ /dev/null @@ -1,119 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include //FilesystemOpt -#include - -#define MAX_JSON_CAPACITY 4096 - -using namespace ArduinoOcpp; - -std::unique_ptr FilesystemUtils::loadJson(std::shared_ptr filesystem, const char *fn) { - if (!filesystem || !fn || *fn == '\0') { - AO_DBG_ERR("Format error"); - return nullptr; - } - - if (strnlen(fn, MAX_PATH_SIZE) >= MAX_PATH_SIZE) { - AO_DBG_ERR("Fn too long: %.*s", MAX_PATH_SIZE, fn); - return nullptr; - } - - size_t fsize = 0; - if (filesystem->stat(fn, &fsize) != 0) { - AO_DBG_DEBUG("File does not exist: %s", fn); - return nullptr; - } - - if (fsize < 2) { - AO_DBG_ERR("File too small for JSON, collect %s", fn); - filesystem->remove(fn); - return nullptr; - } - - auto file = filesystem->open(fn, "r"); - if (!file) { - AO_DBG_ERR("Could not open file %s", fn); - return nullptr; - } - - size_t capacity = (3 * fsize) / 2; - if (capacity < 32) { - capacity = 32; - } - if (capacity > MAX_JSON_CAPACITY) { - capacity = MAX_JSON_CAPACITY; - } - - auto doc = std::unique_ptr(nullptr); - DeserializationError err = DeserializationError::NoMemory; - ArduinoJsonFileAdapter fileReader {file.get()}; - - while (err == DeserializationError::NoMemory && capacity <= MAX_JSON_CAPACITY) { - - doc.reset(new DynamicJsonDocument(capacity)); - err = deserializeJson(*doc, fileReader); - - capacity *= 3; - capacity /= 2; - - file->seek(0); //rewind file to beginning - } - - if (err) { - AO_DBG_ERR("Error deserializing file %s: %s", fn, err.c_str()); - //skip this file - return nullptr; - } - - AO_DBG_DEBUG("Loaded JSON file: %s", fn); - - return doc; -} - -bool FilesystemUtils::storeJson(std::shared_ptr filesystem, const char *fn, const DynamicJsonDocument& doc) { - if (!filesystem || !fn || *fn == '\0') { - AO_DBG_ERR("Format error"); - return false; - } - - if (strnlen(fn, MAX_PATH_SIZE) >= MAX_PATH_SIZE) { - AO_DBG_ERR("Fn too long: %.*s", MAX_PATH_SIZE, fn); - return false; - } - - if (doc.isNull() || doc.overflowed()) { - AO_DBG_ERR("Invalid JSON %s", fn); - return false; - } - - size_t file_size = 0; - if (filesystem->stat(fn, &file_size) == 0) { - filesystem->remove(fn); - } - - auto file = filesystem->open(fn, "w"); - if (!file) { - AO_DBG_ERR("Could not open file %s", fn); - return false; - } - - ArduinoJsonFileAdapter fileWriter {file.get()}; - - size_t written = serializeJson(doc, fileWriter); - - if (written < 2) { - AO_DBG_ERR("Error writing file %s", fn); - if (filesystem->stat(fn, &file_size) == 0) { - AO_DBG_DEBUG("Collect invalid file %s", fn); - filesystem->remove(fn); - } - return false; - } - - AO_DBG_DEBUG("Wrote JSON file: %s", fn); - return true; -} diff --git a/src/ArduinoOcpp/Core/OcppConnection.cpp b/src/ArduinoOcpp/Core/OcppConnection.cpp deleted file mode 100644 index 1c5c1308..00000000 --- a/src/ArduinoOcpp/Core/OcppConnection.cpp +++ /dev/null @@ -1,279 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include - -#include - -#define HEAP_GUARD 2000UL //will not accept JSON messages if it will result in less than HEAP_GUARD free bytes in heap - -size_t removePayload(const char *src, size_t src_size, char *dst, size_t dst_size); - -using namespace ArduinoOcpp; - -OcppConnection::OcppConnection(OcppSocket& ocppSock, std::shared_ptr baseModel, std::shared_ptr filesystem) - : baseModel{baseModel}, filesystem{filesystem}, initiatedOcppOperations{baseModel, filesystem} { - ReceiveTXTcallback callback = [this] (const char *payload, size_t length) { - return this->processOcppSocketInputTXT(payload, length); - }; - - ocppSock.setReceiveTXTcallback(callback); -} - -void OcppConnection::loop(OcppSocket& ocppSock) { - - /** - * Work through the initiatedOcppOperations queue. Start with the first element by calling req() on it. If - * the operation is (still) pending, it will return false and therefore each other queued element must - * wait until this operation is over. If an ocppOperation is finished, it returns true on a req() call, - * can be dequeued and the following ocppOperation is processed. - */ - - auto inited = initiatedOcppOperations.front(); - if (inited) { - bool timeout = inited->sendReq(ocppSock); //The only reason to dequeue elements here is when a timeout occurs. Normally - if (timeout){ //the Conf msg processing routine dequeues finished elements - initiatedOcppOperations.pop_front(); - } - } - - /* - * Activate timeout detection on the msgs other than the first in the queue. - */ - - auto cached = initiatedOcppOperations.begin_tail(); - while (cached != initiatedOcppOperations.end_tail()) { - Timeout *timer = (*cached)->getTimeout(); - if (!timer) { - ++cached; //no timeouts, nothing to do in this iteration - continue; - } - timer->tick(false); //false: did not send a frame prior to calling tick - if (timer->isExceeded() && - //dropping operations out-of-order is only possible if they do not own an opNr - (!(*cached)->getStorageHandler() || (*cached)->getStorageHandler()->getOpNr() < 0)) { - AO_DBG_INFO("Discarding cached due to timeout:"); - (*cached)->print_debug(); - cached = initiatedOcppOperations.erase_tail(cached); - } else { - ++cached; - } - } - - /** - * Work through the receivedOcppOperations queue. Start with the first element by calling conf() on it. - * If an ocppOperation is finished, it returns true on a conf() call, and is dequeued. - */ - - auto received = receivedOcppOperations.begin(); - while (received != receivedOcppOperations.end()){ - bool success = (*received)->sendConf(ocppSock); - if (success){ - received = receivedOcppOperations.erase(received); - } 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, which is now at receivedOcppOperations[i+1] - ++received; //TODO review: this makes confs out-of-order. But if the first Op fails because of lacking RAM, this could save the device. - } - } -} - -void OcppConnection::initiateOcppOperation(std::unique_ptr o){ - if (!o) { - AO_DBG_ERR("Called with null. Ignore"); - return; - } - if (!o->isFullyConfigured()){ - AO_DBG_ERR("Called without the operation being configured and ready to send. Discard operation!"); - return; //o gets destroyed - } - - initiatedOcppOperations.initiate(std::move(o)); -} - -bool OcppConnection::processOcppSocketInputTXT(const char* payload, size_t length) { - - AO_DBG_TRAFFIC_IN((int) length, payload); - - bool deserializationSuccess = false; - - auto doc = std::unique_ptr{nullptr}; - size_t capacity = length + 100; - - DeserializationError err = DeserializationError::NoMemory; - while (capacity + HEAP_GUARD < ao_avail_heap() && err == DeserializationError::NoMemory) { - doc = std::unique_ptr(new DynamicJsonDocument(capacity)); - err = deserializeJson(*doc, payload, length); - - capacity *= 3; - capacity /= 2; - } - - //TODO insert validateRpcHeader at suitable position - - switch (err.code()) { - case DeserializationError::Ok: - if (doc != nullptr){ - int messageTypeId = (*doc)[0] | -1; - switch(messageTypeId) { - case MESSAGE_TYPE_CALL: - handleReqMessage(*doc); - deserializationSuccess = true; - break; - case MESSAGE_TYPE_CALLRESULT: - handleConfMessage(*doc); - deserializationSuccess = true; - break; - case MESSAGE_TYPE_CALLERROR: - handleErrMessage(*doc); - deserializationSuccess = true; - break; - default: - AO_DBG_WARN("Invalid OCPP message! (though JSON has successfully been deserialized)"); - break; - } - } else { //unlikely corner case - AO_DBG_ERR("Deserialization is okay but doc is nullptr"); - } - break; - case DeserializationError::InvalidInput: - AO_DBG_WARN("Invalid input! Not a JSON"); - break; - case DeserializationError::NoMemory: - { - AO_DBG_WARN("OOP! Incoming operation exceeds reserved heap. Input length = %zu, free heap = %u", length, ao_avail_heap()); - - /* - * If websocket input is of message type MESSAGE_TYPE_CALL, send back a message of type MESSAGE_TYPE_CALLERROR. - * Then the communication counterpart knows that this operation failed. - * If the input type is MESSAGE_TYPE_CALLRESULT, it can be ignored. This controller will automatically resend the corresponding request message. - */ - - doc = std::unique_ptr(new DynamicJsonDocument(200)); - char onlyRpcHeader[200]; - size_t onlyRpcHeader_len = removePayload(payload, length, onlyRpcHeader, sizeof(onlyRpcHeader)); - DeserializationError err2 = deserializeJson(*doc, onlyRpcHeader, onlyRpcHeader_len); - if (err2.code() == DeserializationError::Ok) { - int messageTypeId2 = (*doc)[0] | -1; - if (messageTypeId2 == MESSAGE_TYPE_CALL) { - deserializationSuccess = true; - auto op = makeOcppOperation(new OutOfMemory(ao_avail_heap(), length)); - handleReqMessage(*doc, std::move(op)); - } - } - } - break; - default: - AO_DBG_WARN("Deserialization failed: %s", err.c_str()); - break; - } - - return deserializationSuccess; -} - -/** - * call conf() on each element of the queue. Start with first element. On successful message delivery, - * delete the element from the list. Try all the pending OCPP Operations until the right one is found. - * - * This function could result in improper behavior in Charging Stations, because messages are not - * guaranteed to be received and therefore processed in the right order. - */ -void OcppConnection::handleConfMessage(JsonDocument& json) { - - bool success = false; - - initiatedOcppOperations.drop_if( - [&json, &success] (std::unique_ptr& operation) { - bool match = operation->receiveConf(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 OcppOperation - AO_DBG_WARN("Received CALLRESULT doesn't match any pending operation"); - (void)0; - } -} - -void OcppConnection::handleReqMessage(JsonDocument& json) { - auto op = makeFromJson(json); - if (op == nullptr) { - AO_DBG_WARN("Couldn't make OppOperation from Request. Ignore request"); - return; - } - handleReqMessage(json, std::move(op)); -} - -void OcppConnection::handleReqMessage(JsonDocument& json, std::unique_ptr op) { - if (op == nullptr) { - AO_DBG_ERR("Invalid argument"); - return; - } - op->setOcppModel(baseModel); - op->receiveReq(json); //"fire" the operation - receivedOcppOperations.push_back(std::move(op)); //enqueue so loop() plans conf sending -} - -void OcppConnection::handleErrMessage(JsonDocument& json) { - - bool success = false; - - initiatedOcppOperations.drop_if( - [&json, &success] (std::unique_ptr& operation) { - bool match = operation->receiveError(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) { - //No OcppOperation was aborted because of the error message - AO_DBG_WARN("Received CALLERROR did not abort a pending operation"); - (void)0; - } -} - -/* - * Tries to recover the Ocpp-Operation header from a broken message. - * - * Example input: - * [2, "75705e50-682d-404e-b400-1bca33d41e19", "ChangeConfiguration", {"key":"now the msg breaks... - * - * The Json library returns an error code when trying to deserialize that broken message. This - * function searches for the first occurence of the character '{' and writes "}]" after it. - * - * Example output: - * [2, "75705e50-682d-404e-b400-1bca33d41e19", "ChangeConfiguration", {}] - * - */ -size_t removePayload(const char *src, size_t src_size, char *dst, size_t dst_size) { - size_t res_len = 0; - for (size_t i = 0; i < src_size && i < dst_size-3; i++) { - if (src[i] == '\0'){ - //no payload found within specified range. Cancel execution - break; - } - dst[i] = src[i]; - if (src[i] == '{') { - dst[i+1] = '}'; - dst[i+2] = ']'; - res_len = i+3; - break; - } - } - dst[res_len] = '\0'; - res_len++; - return res_len; -} diff --git a/src/ArduinoOcpp/Core/OcppConnection.h b/src/ArduinoOcpp/Core/OcppConnection.h deleted file mode 100644 index 8f00f4fc..00000000 --- a/src/ArduinoOcpp/Core/OcppConnection.h +++ /dev/null @@ -1,44 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OCPPCONNECTION_H -#define OCPPCONNECTION_H - -#include - -#include -#include -#include - -namespace ArduinoOcpp { - -class OcppModel; -class OcppSocket; -class OcppOperation; -class FilesystemAdapter; - -class OcppConnection { -private: - std::shared_ptr baseModel; - std::shared_ptr filesystem; - - OperationsQueue initiatedOcppOperations; - std::deque> receivedOcppOperations; - - void handleConfMessage(JsonDocument& json); - void handleReqMessage(JsonDocument& json); - void handleReqMessage(JsonDocument& json, std::unique_ptr op); - void handleErrMessage(JsonDocument& json); -public: - OcppConnection(OcppSocket& oSock, std::shared_ptr baseModel, std::shared_ptr filesystem); - - void loop(OcppSocket& oSock); - - void initiateOcppOperation(std::unique_ptr o); - - bool processOcppSocketInputTXT(const char* payload, size_t length); -}; - -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/Core/OcppEngine.cpp b/src/ArduinoOcpp/Core/OcppEngine.cpp deleted file mode 100644 index e920bc2d..00000000 --- a/src/ArduinoOcpp/Core/OcppEngine.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include - -using namespace ArduinoOcpp; - -OcppEngine *ArduinoOcpp::defaultOcppEngine = nullptr; - -OcppEngine::OcppEngine(OcppSocket& ocppSocket, const OcppClock& system_clock, std::shared_ptr filesystem) - : oSock(ocppSocket), oModel{std::make_shared(system_clock)}, oConn{oSock, oModel, filesystem} { - defaultOcppEngine = this; -} - -OcppEngine::~OcppEngine() { - defaultOcppEngine = nullptr; -} - -void OcppEngine::loop() { - oSock.loop(); - oConn.loop(oSock); - - if (runOcppTasks) - oModel->loop(); -} - -void OcppEngine::initiateOperation(std::unique_ptr op) { - if (op) { - op->setOcppModel(oModel); - oConn.initiateOcppOperation(std::move(op)); - } -} - -OcppModel& OcppEngine::getOcppModel() { - return *oModel; -} diff --git a/src/ArduinoOcpp/Core/OcppEngine.h b/src/ArduinoOcpp/Core/OcppEngine.h deleted file mode 100644 index e4c03871..00000000 --- a/src/ArduinoOcpp/Core/OcppEngine.h +++ /dev/null @@ -1,42 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OCPPENGINE_H -#define OCPPENGINE_H - -#include -#include -#include - -namespace ArduinoOcpp { - -class OcppSocket; -class OcppModel; -class FilesystemAdapter; - -class OcppEngine { -private: - OcppSocket& oSock; - std::shared_ptr oModel; - OcppConnection oConn; - - bool runOcppTasks = true; -public: - OcppEngine(OcppSocket& ocppSocket, const OcppClock& system_clock, std::shared_ptr filesystem); - ~OcppEngine(); - - void loop(); - - void setRunOcppTasks(bool enable) {runOcppTasks = enable;} - - void initiateOperation(std::unique_ptr op); - - OcppModel& getOcppModel(); -}; - -extern OcppEngine *defaultOcppEngine; - -} //end namespace ArduinoOcpp - -#endif diff --git a/src/ArduinoOcpp/Core/OcppError.h b/src/ArduinoOcpp/Core/OcppError.h deleted file mode 100644 index f29514e3..00000000 --- a/src/ArduinoOcpp/Core/OcppError.h +++ /dev/null @@ -1,56 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OCPPERROR_H -#define OCPPERROR_H - -#include - -#include - -namespace ArduinoOcpp { - -class NotImplemented : public OcppMessage { -public: - const char *getErrorCode() { - return "NotImplemented"; - } -}; - -class OutOfMemory : public OcppMessage { -private: - uint32_t freeHeap; - size_t msgLen; -public: - OutOfMemory(uint32_t freeHeap, size_t msgLen) : freeHeap(freeHeap), msgLen(msgLen) { } - const char *getErrorCode() { - return "InternalError"; - } - const char *getErrorDescription() { - return "Too little free memory on the controller. Operation denied"; - } - std::unique_ptr getErrorDetails() { - auto errDoc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); - JsonObject err = errDoc->to(); - err["free_heap"] = freeHeap; - err["msg_length"] = msgLen; - return errDoc; - } -}; - -class WebSocketError : public OcppMessage { -private: - const char *description; -public: - WebSocketError(const char *description) : description(description) { } - const char *getErrorCode() { - return "GenericError"; - } - const char *getErrorDescription() { - return description; - } -}; - -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/Core/OcppMessage.cpp b/src/ArduinoOcpp/Core/OcppMessage.cpp deleted file mode 100644 index 40781d1f..00000000 --- a/src/ArduinoOcpp/Core/OcppMessage.cpp +++ /dev/null @@ -1,53 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include - -#include - -using ArduinoOcpp::OcppMessage; - -OcppMessage::OcppMessage() {} - -OcppMessage::~OcppMessage() {} - -const char* OcppMessage::getOcppOperationType(){ - AO_DBG_ERR("Unsupported operation: getOcppOperationType() is not implemented"); - return "CustomOperation"; -} - -void OcppMessage::setOcppModel(std::shared_ptr ocppModel) { - if (!ocppModelInitialized) { //prevent the ocppModel from being overwritten - this->ocppModel = ocppModel; //can still be nullptr - ocppModelInitialized = true; - } -} - -void OcppMessage::initiate() { - //called after initiateOcppOperation(anyMsg) -} - -std::unique_ptr OcppMessage::createReq() { - AO_DBG_ERR("Unsupported operation: createReq() is not implemented"); - return nullptr; -} - -void OcppMessage::processConf(JsonObject payload) { - AO_DBG_ERR("Unsupported operation: processConf() is not implemented"); -} - -void OcppMessage::processReq(JsonObject payload) { - AO_DBG_ERR("Unsupported operation: processReq() is not implemented"); -} - -std::unique_ptr OcppMessage::createConf() { - AO_DBG_ERR("Unsupported operation: createConf() is not implemented"); - return nullptr; -} - -std::unique_ptr ArduinoOcpp::createEmptyDocument() { - auto emptyDoc = std::unique_ptr(new DynamicJsonDocument(0)); - emptyDoc->to(); - return emptyDoc; -} diff --git a/src/ArduinoOcpp/Core/OcppModel.cpp b/src/ArduinoOcpp/Core/OcppModel.cpp deleted file mode 100644 index 3c7eaee7..00000000 --- a/src/ArduinoOcpp/Core/OcppModel.cpp +++ /dev/null @@ -1,110 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -using namespace ArduinoOcpp; - -OcppModel::OcppModel(const OcppClock& system_clock) - : ocppTime{system_clock} { - -} - -OcppModel::~OcppModel() = default; - -void OcppModel::loop() { - if (chargePointStatusService) - chargePointStatusService->loop(); - - if (smartChargingService) - smartChargingService->loop(); - - if (heartbeatService) - heartbeatService->loop(); - - if (meteringService) - meteringService->loop(); - - if (diagnosticsService) - diagnosticsService->loop(); - - if (firmwareService) - firmwareService->loop(); -} - -void OcppModel::setTransactionStore(std::unique_ptr ts) { - transactionStore = std::move(ts); -} - -TransactionStore *OcppModel::getTransactionStore() { - return transactionStore.get(); -} - -void OcppModel::setSmartChargingService(std::unique_ptr scs) { - smartChargingService = std::move(scs); -} - -SmartChargingService* OcppModel::getSmartChargingService() const { - return smartChargingService.get(); -} - -void OcppModel::setChargePointStatusService(std::unique_ptr cpss){ - chargePointStatusService = std::move(cpss); -} - -ChargePointStatusService *OcppModel::getChargePointStatusService() const { - return chargePointStatusService.get(); -} - -ConnectorStatus *OcppModel::getConnectorStatus(int connectorId) const { - if (getChargePointStatusService() == nullptr) return nullptr; - - auto result = getChargePointStatusService()->getConnector(connectorId); - if (result == nullptr) { - AO_DBG_ERR("Cannot fetch connector with given connectorId. Return nullptr"); - //no error catch - } - return result; -} - -void OcppModel::setMeteringSerivce(std::unique_ptr ms) { - meteringService = std::move(ms); -} - -MeteringService* OcppModel::getMeteringService() const { - return meteringService.get(); -} - -void OcppModel::setFirmwareService(std::unique_ptr fws) { - firmwareService = std::move(fws); -} - -FirmwareService *OcppModel::getFirmwareService() const { - return firmwareService.get(); -} - -void OcppModel::setDiagnosticsService(std::unique_ptr ds) { - diagnosticsService = std::move(ds); -} - -DiagnosticsService *OcppModel::getDiagnosticsService() const { - return diagnosticsService.get(); -} - -void OcppModel::setHeartbeatService(std::unique_ptr hs) { - heartbeatService = std::move(hs); -} - -OcppTime& OcppModel::getOcppTime() { - return ocppTime; -} diff --git a/src/ArduinoOcpp/Core/OcppModel.h b/src/ArduinoOcpp/Core/OcppModel.h deleted file mode 100644 index ff9f4439..00000000 --- a/src/ArduinoOcpp/Core/OcppModel.h +++ /dev/null @@ -1,68 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OCPPMODEL_H -#define OCPPMODEL_H - -#include - -#include - -namespace ArduinoOcpp { - -class TransactionStore; -class SmartChargingService; -class ChargePointStatusService; -class ConnectorStatus; -class MeteringService; -class FirmwareService; -class DiagnosticsService; -class HeartbeatService; - -class OcppModel { -private: - std::unique_ptr transactionStore; - std::unique_ptr smartChargingService; - std::unique_ptr chargePointStatusService; - std::unique_ptr meteringService; - std::unique_ptr firmwareService; - std::unique_ptr diagnosticsService; - std::unique_ptr heartbeatService; - OcppTime ocppTime; - -public: - OcppModel(const OcppClock& system_clock); - OcppModel() = delete; - OcppModel(const OcppModel& rhs) = delete; - ~OcppModel(); - - void loop(); - - void setTransactionStore(std::unique_ptr transactionStore); - TransactionStore *getTransactionStore(); - - void setSmartChargingService(std::unique_ptr scs); - SmartChargingService* getSmartChargingService() const; - - void setChargePointStatusService(std::unique_ptr cpss); - ChargePointStatusService *getChargePointStatusService() const; - ConnectorStatus *getConnectorStatus(int connectorId) const; - - void setMeteringSerivce(std::unique_ptr meteringService); - MeteringService* getMeteringService() const; - - void setFirmwareService(std::unique_ptr firmwareService); - FirmwareService *getFirmwareService() const; - - void setDiagnosticsService(std::unique_ptr diagnosticsService); - DiagnosticsService *getDiagnosticsService() const; - - void setHeartbeatService(std::unique_ptr heartbeatService); - - OcppTime &getOcppTime(); -}; - -} //end namespace ArduinoOcpp - -#endif diff --git a/src/ArduinoOcpp/Core/OcppOperation.cpp b/src/ArduinoOcpp/Core/OcppOperation.cpp deleted file mode 100644 index 9f89cc59..00000000 --- a/src/ArduinoOcpp/Core/OcppOperation.cpp +++ /dev/null @@ -1,457 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -int unique_id_counter = 1000000; - -using namespace ArduinoOcpp; - -OcppOperation::OcppOperation(std::unique_ptr msg) : ocppMessage(std::move(msg)) { - -} - -OcppOperation::OcppOperation() { - -} - -OcppOperation::~OcppOperation(){ - -} - -void OcppOperation::setOcppMessage(std::unique_ptr msg){ - ocppMessage = std::move(msg); -} - -void OcppOperation::setOcppModel(std::shared_ptr oModel) { - if (!ocppMessage) { - AO_DBG_ERR("Must be called after setOcppMessage(). Abort"); - return; - } - if (!oModel) { - AO_DBG_ERR("Passed nullptr. Ignore"); - return; - } - ocppMessage->setOcppModel(oModel); -} - -void OcppOperation::setTimeout(std::unique_ptr to){ - if (!to){ - AO_DBG_ERR("Passed nullptr! Ignore"); - return; - } - timeout = std::move(to); - timeout->setOnTimeoutListener(onTimeoutListener); - timeout->setOnAbortListener(onAbortListener); -} - -Timeout *OcppOperation::getTimeout() { - return timeout.get(); -} - -void OcppOperation::setMessageID(const std::string &id){ - if (!messageID.empty()){ - AO_DBG_WARN("MessageID is set twice or is set after first usage!"); - } - messageID = id; -} - -const std::string *OcppOperation::getMessageID() { - if (messageID.empty()) { - char id_str [16] = {'\0'}; - sprintf(id_str, "%d", unique_id_counter++); - messageID = std::string {id_str}; - //messageID = std::to_string(unique_id_counter++); - } - return &messageID; -} - -bool OcppOperation::sendReq(OcppSocket& ocppSocket){ - - /* - * timeout behaviour - * - * if timeout, print out error message and treat this operation as completed (-> return true) - */ - if (timeout->isExceeded()) { - //cancel this operation - AO_DBG_INFO("%s has timed out! Discard operation", ocppMessage->getOcppOperationType()); - return true; - } - - /* - * retry behaviour - * - * if retry, run the rest of this function, i.e. resend the message. If not, just return false - */ - if (ao_tick_ms() <= retry_start + RETRY_INTERVAL * retry_interval_mult) { - //NO retry - return false; - } - // Do retry. Increase timer by factor 2 - if (RETRY_INTERVAL * retry_interval_mult * 2 <= RETRY_INTERVAL_MAX) - retry_interval_mult *= 2; - - /* - * Create the OCPP message - */ - auto requestPayload = ocppMessage->createReq(); - if (!requestPayload) { - return false; - } - - /* - * Create OCPP-J Remote Procedure Call header - */ - size_t json_buffsize = JSON_ARRAY_SIZE(4) + (getMessageID()->length() + 1) + requestPayload->capacity(); - DynamicJsonDocument requestJson(json_buffsize); - - requestJson.add(MESSAGE_TYPE_CALL); //MessageType - requestJson.add(*getMessageID()); //Unique message ID - requestJson.add(ocppMessage->getOcppOperationType()); //Action - requestJson.add(*requestPayload); //Payload - - /* - * Serialize and send. Destroy serialization and JSON object. - * - * If sending was successful, start timer - * - * Return that this function must be called again (-> false) - */ - std::string out {}; - serializeJson(requestJson, out); - - if (printReqCounter > 5000) { - printReqCounter = 0; - AO_DBG_DEBUG("Try to send request: %s", out.c_str()); - } - printReqCounter++; - - bool success = ocppSocket.sendTXT(out); - - timeout->tick(success); - - if (success) { - AO_DBG_TRAFFIC_OUT(out.c_str()); - retry_start = ao_tick_ms(); - } else { - //ocppSocket is not able to put any data on TCP stack. Maybe because we're offline - retry_start = 0; - retry_interval_mult = 1; - } - - return false; -} - -bool OcppOperation::receiveConf(JsonDocument& confJson){ - /* - * check if messageIDs match. If yes, continue with this function. If not, return false for message not consumed - */ - if (*getMessageID() != confJson[1].as()){ - return false; - } - - /* - * Hand the payload over to the OcppMessage object - */ - JsonObject payload = confJson[2]; - ocppMessage->processConf(payload); - - /* - * Hand the payload over to the onReceiveConf Callback - */ - onReceiveConfListener(payload); - - /* - * return true as this message has been consumed - */ - return true; -} - -bool OcppOperation::receiveError(JsonDocument& confJson){ - /* - * check if messageIDs match. If yes, continue with this function. If not, return false for message not consumed - */ - if (*getMessageID() != confJson[1].as()){ - return false; - } - - /* - * Hand the error over to the OcppMessage object - */ - const char *errorCode = confJson[2]; - const char *errorDescription = confJson[3]; - JsonObject errorDetails = confJson[4]; - bool abortOperation = ocppMessage->processErr(errorCode, errorDescription, errorDetails); - - if (abortOperation) { - onReceiveErrorListener(errorCode, errorDescription, errorDetails); - onAbortListener(); - } else { - //restart operation - timeout->restart(); - retry_start = 0; - retry_interval_mult = 1; - } - - return abortOperation; -} - -bool OcppOperation::receiveReq(JsonDocument& reqJson){ - - std::string reqId = reqJson[1]; - setMessageID(reqId); - - //TODO What if client receives requests two times? Can happen if previous conf is lost. In the Smart Charging Profile - // it probably doesn't matter to repeat an operation on the EVSE. Change in future? - - /* - * Hand the payload over to the OcppOperation object - */ - JsonObject payload = reqJson[3]; - ocppMessage->processReq(payload); - - /* - * Hand the payload over to the first Callback. It is a callback that notifies the client that request has been processed in the OCPP-library - */ - onReceiveReqListener(payload); - - reqExecuted = true; //ensure that the conf is only sent after the req has been executed - - return true; //true because everything was successful. If there will be an error check in future, this value becomes more reasonable -} - -bool OcppOperation::sendConf(OcppSocket& ocppSocket){ - - if (!reqExecuted) { - //wait until req has been executed - return false; - } - - /* - * Create the OCPP message - */ - std::unique_ptr confJson = nullptr; - std::unique_ptr confPayload = ocppMessage->createConf(); - std::unique_ptr errorDetails = nullptr; - - bool operationFailure = ocppMessage->getErrorCode() != nullptr; - - if (!operationFailure && !confPayload) { - return false; //confirmation message still pending - } - - if (!operationFailure) { - - /* - * Create OCPP-J Remote Procedure Call header - */ - size_t json_buffsize = JSON_ARRAY_SIZE(3) + (getMessageID()->length() + 1) + confPayload->capacity(); - confJson = std::unique_ptr(new DynamicJsonDocument(json_buffsize)); - - confJson->add(MESSAGE_TYPE_CALLRESULT); //MessageType - confJson->add(*getMessageID()); //Unique message ID - confJson->add(*confPayload); //Payload - } else { - //operation failure. Send error message instead - - const char *errorCode = ocppMessage->getErrorCode(); - const char *errorDescription = ocppMessage->getErrorDescription(); - errorDetails = std::unique_ptr(ocppMessage->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()); - } - - /* - * Create OCPP-J Remote Procedure Call header - */ - size_t json_buffsize = JSON_ARRAY_SIZE(5) - + (getMessageID()->length() + 1) - + strlen(errorCode) + 1 - + strlen(errorDescription) + 1 - + errorDetails->capacity(); - confJson = std::unique_ptr(new DynamicJsonDocument(json_buffsize)); - - confJson->add(MESSAGE_TYPE_CALLERROR); //MessageType - confJson->add(*getMessageID()); //Unique message ID - confJson->add(errorCode); - confJson->add(errorDescription); - confJson->add(*errorDetails); //Error description - } - - /* - * Serialize and send. Destroy serialization and JSON object. - */ - std::string out {}; - serializeJson(*confJson, out); - bool wsSuccess = ocppSocket.sendTXT(out); - - if (wsSuccess) { - if (!operationFailure) { - AO_DBG_TRAFFIC_OUT(out.c_str()); - onSendConfListener(confPayload->as()); - } else { - AO_DBG_WARN("Operation failed. JSON CallError message: %s", out.c_str()); - onAbortListener(); - } - } - - return wsSuccess; -} - -void OcppOperation::initiate(std::unique_ptr opStorage) { - if (ocppMessage) { - - /* - * Create OCPP-J Remote Procedure Call header as storage data (doesn't necessarily have to comply with OCPP RPC header) - */ - if (opStorage) { - opStore = std::move(opStorage); - size_t json_buffsize = JSON_ARRAY_SIZE(3) + (getMessageID()->length() + 1); - auto rpcData = std::unique_ptr(new DynamicJsonDocument(json_buffsize)); - - rpcData->add(MESSAGE_TYPE_CALL); //MessageType - rpcData->add(*getMessageID()); //Unique message ID - rpcData->add(ocppMessage->getOcppOperationType()); //Action - - opStore->setRpc(std::move(rpcData)); - } - - bool inited = ocppMessage->initiate(opStore.get()); - - if (opStore) { - opStore->clearBuffer(); - } - - if (!inited) { //legacy support - ocppMessage->initiate(); - } - } else { - AO_DBG_ERR("Missing ocppMessage instance"); - } -} - -bool OcppOperation::restore(std::unique_ptr opStorage, std::shared_ptr oModel) { - if (!opStorage) { - AO_DBG_ERR("invalid argument"); - return false; - } - - opStore = std::move(opStorage); - - auto rpcData = opStore->getRpc(); - if (!rpcData) { - AO_DBG_ERR("corrupted storage"); - return false; - } - - messageID = (*rpcData)[1] | std::string(); - std::string opType = (*rpcData)[2] | std::string(); - if (messageID.empty() || opType.empty()) { - AO_DBG_ERR("corrupted storage"); - messageID.clear(); - return false; - } - - int parsedMessageID = -1; - if (sscanf(messageID.c_str(), "%d", &parsedMessageID) == 1) { - if (parsedMessageID > unique_id_counter) { - AO_DBG_DEBUG("restore unique_id_counter with %d", parsedMessageID); - unique_id_counter = parsedMessageID + 1; //next unique value is parsedId + 1 - } - } else { - AO_DBG_ERR("cannot set unique msgID counter"); - (void)0; - //skip this step but don't abort restore - } - - if (!strcmp(opType.c_str(), "StartTransaction")) { //TODO this will get a nicer solution - ocppMessage = std::unique_ptr(new Ocpp16::StartTransaction()); - } else if (!strcmp(opType.c_str(), "StopTransaction")) { - ocppMessage = std::unique_ptr(new Ocpp16::StopTransaction()); - } - - if (!ocppMessage) { - AO_DBG_ERR("cannot create msg"); - return false; - } - - ocppMessage->setOcppModel(oModel); - - bool success = ocppMessage->restore(opStore.get()); - opStore->clearBuffer(); - - if (success) { - AO_DBG_DEBUG("restored opNr %i: %s", opStore->getOpNr(), ocppMessage->getOcppOperationType()); - (void)0; - } else { - AO_DBG_ERR("restore opNr %i error", opStore->getOpNr()); - (void)0; - } - - return success; -} - -void OcppOperation::setOnReceiveConfListener(OnReceiveConfListener onReceiveConf){ - if (onReceiveConf) - onReceiveConfListener = onReceiveConf; -} - -/** - * Sets a Listener that is called after this machine processed a request by the communication counterpart - */ -void OcppOperation::setOnReceiveReqListener(OnReceiveReqListener onReceiveReq){ - if (onReceiveReq) - onReceiveReqListener = onReceiveReq; -} - -void OcppOperation::setOnSendConfListener(OnSendConfListener onSendConf){ - if (onSendConf) - onSendConfListener = onSendConf; -} - -void OcppOperation::setOnTimeoutListener(OnTimeoutListener onTimeout) { - if (onTimeout) - onTimeoutListener = onTimeout; -} - -void OcppOperation::setOnReceiveErrorListener(OnReceiveErrorListener onReceiveError) { - if (onReceiveError) - onReceiveErrorListener = onReceiveError; -} - -void OcppOperation::setOnAbortListener(OnAbortListener onAbort) { - if (onAbort) - onAbortListener = onAbort; -} - -bool OcppOperation::isFullyConfigured(){ - return ocppMessage != nullptr; -} - -void OcppOperation::rebaseMsgId(int msgIdCounter) { - unique_id_counter = msgIdCounter; - getMessageID(); //apply msgIdCounter to this operation -} - -void OcppOperation::print_debug() { - if (ocppMessage) { - AO_CONSOLE_PRINTF("OcppOperation of type %s\n", ocppMessage->getOcppOperationType()); - } else { - AO_CONSOLE_PRINTF("OcppOperation (no type)\n"); - } -} diff --git a/src/ArduinoOcpp/Core/OcppOperation.h b/src/ArduinoOcpp/Core/OcppOperation.h deleted file mode 100644 index 64b451da..00000000 --- a/src/ArduinoOcpp/Core/OcppOperation.h +++ /dev/null @@ -1,145 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OCPPOPERATION_H -#define OCPPOPERATION_H - -#define MESSAGE_TYPE_CALL 2 -#define MESSAGE_TYPE_CALLRESULT 3 -#define MESSAGE_TYPE_CALLERROR 4 - -#include - -#include - -namespace ArduinoOcpp { - -class OcppMessage; -class OcppModel; -class OcppSocket; -class StoredOperationHandler; - -class OcppOperation { -private: - std::string messageID {}; - std::unique_ptr ocppMessage; - const std::string *getMessageID(); - void setMessageID(const std::string &id); - OnReceiveConfListener onReceiveConfListener = [] (JsonObject payload) {}; - OnReceiveReqListener onReceiveReqListener = [] (JsonObject payload) {}; - OnSendConfListener onSendConfListener = [] (JsonObject payload) {}; - OnTimeoutListener onTimeoutListener = [] () {}; - OnReceiveErrorListener onReceiveErrorListener = [] (const char *code, const char *description, JsonObject details) {}; - OnAbortListener onAbortListener = [] () {}; - bool reqExecuted = false; - - std::unique_ptr timeout{new OfflineSensitiveTimeout(40000)}; - - const unsigned long RETRY_INTERVAL = 3000; //in ms; first retry after ... ms; second retry after 2 * ... ms; third after 4 ... - const unsigned long RETRY_INTERVAL_MAX = 20000; //in ms; - unsigned long retry_start = 0; - unsigned long retry_interval_mult = 1; // RETRY_INTERVAL * retry_interval_mult gives longer periods with each iteration - - uint16_t printReqCounter = 0; - - std::unique_ptr opStore; -public: - - OcppOperation(std::unique_ptr msg); - - OcppOperation(); - - ~OcppOperation(); - - void setOcppMessage(std::unique_ptr msg); - - void setOcppModel(std::shared_ptr oModel); - - void setTimeout(std::unique_ptr timeout); - - Timeout *getTimeout(); - - /** - * Sends the message(s) that belong to the OCPP Operation. This function puts a JSON message on the lower protocol layer. - * - * For instance operation Authorize: sends Authorize.req(idTag) - * - * This function is usually called multiple times by the Arduino loop(). On first call, the request is initially sent. In the - * succeeding calls, the implementers decide to either resend the request, or do nothing as the operation is still pending. When - * the operation is completed (for example when conf() has been called), return true. When the operation is still pending, return - * false. - */ - bool sendReq(OcppSocket& ocppSocket); - - /** - * Decides if message belongs to this operation instance and if yes, proccesses it. For example, multiple instances of an - * operation type can run in the case of Metering Data being sent. - * - * Returns true if JSON object has been consumed, false otherwise. - */ - bool receiveConf(JsonDocument& json); - - /** - * Decides if message belongs to this operation instance and if yes, notifies the OcppMessage object about the CallError. - * - * Returns true if JSON object has been consumed, false otherwise. - */ - bool receiveError(JsonDocument& json); - - /** - * Processes the request in the JSON document. Returns true on success, false on error. - * - * Returns false if the request doesn't belong to the corresponding operation instance - */ - bool receiveReq(JsonDocument& json); - - /** - * After processing a request sent by the communication counterpart, this function sends a confirmation - * message. Returns true on success, false otherwise. Returns also true if a CallError has successfully - * been sent - */ - bool sendConf(OcppSocket& ocppSocket); - - void initiate(std::unique_ptr opStorage); - - bool restore(std::unique_ptr opStorage, std::shared_ptr oModel); - - 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 setOnReceiveErrorListener(OnReceiveErrorListener onReceiveError); - - /** - * The listener onAbort will be called whenever the engine stops trying to execute an operation normally which were initiated - * on this device. This includes timeouts or if the ocpp counterpart sends an error (then it will be called in addition to - * onTimeout or onReceiveError, respectively). Causes for onAbort: - * - * - Cannot create OCPP payload - * - Timeout - * - Receives error msg instead of confirmation msg - * - * The engine uses this listener in both modes: EVSE mode and Central system mode - */ - void setOnAbortListener(OnAbortListener onAbort); - - bool isFullyConfigured(); - - void rebaseMsgId(int msgIdCounter); //workaround; remove when random UUID msg IDs are introduced - - void print_debug(); -}; - -} //end namespace ArduinoOcpp - - #endif diff --git a/src/ArduinoOcpp/Core/OcppOperationCallbacks.h b/src/ArduinoOcpp/Core/OcppOperationCallbacks.h deleted file mode 100644 index 957458a6..00000000 --- a/src/ArduinoOcpp/Core/OcppOperationCallbacks.h +++ /dev/null @@ -1,24 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OCPP_OPERATION_CALLBACKS -#define OCPP_OPERATION_CALLBACKS - -#include -#include - -#include - -namespace ArduinoOcpp { - -using OnReceiveConfListener = std::function; -using OnReceiveReqListener = std::function; -using OnSendConfListener = std::function; -//using OnTimeoutListener = std::function; //in OcppOperationTimeout. Workaround for circle include. Fix by extracting type definitions to new source file -using OnReceiveErrorListener = std::function; //will be called if OCPP communication partner returns error code -//using OnAbortListener = std::function; //will be called whenever the engine will stop trying to execute the operation normallythere is a timeout or error (onAbort = onTimeout || onReceiveError) - - -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/Core/OcppOperationTimeout.cpp b/src/ArduinoOcpp/Core/OcppOperationTimeout.cpp deleted file mode 100644 index cd1eb25a..00000000 --- a/src/ArduinoOcpp/Core/OcppOperationTimeout.cpp +++ /dev/null @@ -1,87 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include - -using namespace ArduinoOcpp; - -void Timeout::setOnTimeoutListener(OnTimeoutListener onTimeout) { - if (onTimeout) - onTimeoutListener = onTimeout; -} - -void Timeout::setOnAbortListener(OnAbortListener onAbort) { - if (onAbort) - onAbortListener = onAbort; -} - -void Timeout::trigger() { - if (!triggered && timerIsExceeded()) { - triggered = true; - onTimeoutListener(); - onAbortListener(); - } -} -void Timeout::tick(bool sendingSuccessful) { - timerTick(sendingSuccessful); - trigger(); -} -void Timeout::restart() { - triggered = false; - timerRestart(); -} -bool Timeout::isExceeded() { - bool exceeded = timerIsExceeded(); - trigger(); - return exceeded; -} - -FixedTimeout::FixedTimeout(unsigned long TIMEOUT_DURATION) : TIMEOUT_DURATION(TIMEOUT_DURATION) { - timeout_active = false; -} - -void FixedTimeout::timerTick(bool sendingSuccessful) { - if (!timeout_active) { - timeout_active = true; - timeout_start = ao_tick_ms(); - } -} -void FixedTimeout::timerRestart() { - timeout_start = ao_tick_ms(); - timeout_active = false; -} -bool FixedTimeout::timerIsExceeded() { - return timeout_active && ao_tick_ms() - timeout_start >= TIMEOUT_DURATION; -} - - -OfflineSensitiveTimeout::OfflineSensitiveTimeout(unsigned long TIMEOUT_DURATION) : TIMEOUT_DURATION(TIMEOUT_DURATION) { - timeout_active = false; -} - -void OfflineSensitiveTimeout::timerTick(bool sendingSuccessful) { - unsigned long t = ao_tick_ms(); - - if (!timeout_active) { - timeout_active = true; - timeout_start = t; - last_tick = t; - } else { - if (!sendingSuccessful) { - timeout_start += t - last_tick; - } - } - - last_tick = t; -} -void OfflineSensitiveTimeout::timerRestart() { - unsigned long t = ao_tick_ms(); - timeout_start = t; - last_tick = t; - timeout_active = false; -} -bool OfflineSensitiveTimeout::timerIsExceeded() { - return timeout_active && ao_tick_ms() - timeout_start >= TIMEOUT_DURATION; -} diff --git a/src/ArduinoOcpp/Core/OcppOperationTimeout.h b/src/ArduinoOcpp/Core/OcppOperationTimeout.h deleted file mode 100644 index a6a2f0c6..00000000 --- a/src/ArduinoOcpp/Core/OcppOperationTimeout.h +++ /dev/null @@ -1,69 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OCPPOPERATIONTIMEOUT_H -#define OCPPOPERATIONTIMEOUT_H - -#include - -namespace ArduinoOcpp { - -using OnTimeoutListener = std::function; -using OnAbortListener = std::function; - -class Timeout { -private: - OnTimeoutListener onTimeoutListener = [] () {}; - OnAbortListener onAbortListener = [] () {}; - bool triggered = false; - void trigger(); -protected: - Timeout() = default; -public: - void setOnTimeoutListener(OnTimeoutListener onTimeoutListener); - void setOnAbortListener(OnAbortListener onAbortListener); - virtual ~Timeout() = default; - void tick(bool sendingSuccessful); - virtual void timerTick(bool sendingSuccessful) = 0; - void restart(); - virtual void timerRestart() = 0; - bool isExceeded(); - virtual bool timerIsExceeded() = 0; -}; - -class FixedTimeout : public Timeout { -private: - unsigned long TIMEOUT_DURATION; - unsigned long timeout_start; - bool timeout_active; -public: - FixedTimeout(unsigned long TIMEOUT_EXPIRE); - void timerTick(bool sendingSuccessful); - void timerRestart(); - bool timerIsExceeded(); -}; - -class OfflineSensitiveTimeout : public Timeout { -private: - unsigned long TIMEOUT_DURATION; - unsigned long timeout_start; - unsigned long last_tick; - bool timeout_active; -public: - OfflineSensitiveTimeout(unsigned long TIMEOUT_EXPIRE); - void timerTick(bool sendingSuccessful); - void timerRestart(); - bool timerIsExceeded(); -}; - -class SuppressedTimeout : public Timeout { -public: - SuppressedTimeout() = default; - void timerTick(bool sendingSuccessful) {} - void timerRestart() {} - bool timerIsExceeded() {return false;} -}; - -} //end namespace ArduinoOcpp -#endif \ No newline at end of file diff --git a/src/ArduinoOcpp/Core/OcppServer.cpp b/src/ArduinoOcpp/Core/OcppServer.cpp deleted file mode 100644 index 2614a66d..00000000 --- a/src/ArduinoOcpp/Core/OcppServer.cpp +++ /dev/null @@ -1,169 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include - -#ifndef AO_CUSTOM_WS - -#define DEBUG_OUT (AO_DBG_LEVEL >= AO_DL_INFO) - -#ifdef AO_TRAFFIC_OUT -#define TRAFFIC_OUT true -#else -#define TRAFFIC_OUT false -#endif - -using namespace ArduinoOcpp::EspWiFi; - -OcppServer::OcppServer() { - AO_DBG_WARN("OCPP Server only suitable for tests at the moment"); - wsockServer.begin(); - wsockServer.onEvent([this](WsClient num, WStype_t type, uint8_t * payload, size_t length) { - this->wsockEvent(num, type, payload, length); - }); - - instance = this; -} - -void OcppServer::wsockEvent(WsClient num, WStype_t type, uint8_t * payload, size_t length) { - - switch(type) { - case WStype_DISCONNECTED: - { - if (DEBUG_OUT) Serial.print(F("[OcppServer] WsClient disconnected! num = ")); - if (DEBUG_OUT) Serial.println(num); - } - break; - case WStype_CONNECTED: - { - IPAddress ip = wsockServer.remoteIP(num); - if (DEBUG_OUT) { - Serial.print(F("[OcppServer] WsClient connected! num = ")); - Serial.print(num); - Serial.print(F(", Connected from IP = ")); - ip.printTo(Serial); - Serial.print(F(", with payload ")); - Serial.println((const char*) payload); - } - - bool found = false; - for (auto route = receiveTXTrouting.begin(); route != receiveTXTrouting.end(); ++route) { - if (ip == (*route).ip_addr) { - if (DEBUG_OUT) Serial.print(F("[OcppServer] IPAddress matches existing route!\n")); - (*route).num = num; - found = true; - break; - } - } - - if (!found) { - Serial.print(F("[OcppServer] Unknown IP address! Please see addReceiveTXTcallback(IPAddress ...)\n")); - } - } - break; - case WStype_TEXT: - { - if (DEBUG_OUT || TRAFFIC_OUT) { - Serial.print(F("[OcppServer] Get TXT from client: ")); - Serial.print(num); - Serial.print(F(", TXT = ")); - Serial.println((const char*) payload); - } - - bool found = false; - for (auto route = receiveTXTrouting.begin(); route != receiveTXTrouting.end(); ++route) { - if (num == (*route).num) { - if (DEBUG_OUT) Serial.print(F("\n")); - if (!((*route).processTXT((const char*) payload, length))) { - Serial.print(F("[OcppServer] Processing WebSocket input event failed!\n")); - } - found = true; - break; - } - } - - if (!found) { - Serial.print(F("[OcppServer] Received msg from unknown client!\n")); - } - } - break; - case WStype_PING: - // pong will be send automatically - Serial.printf("[OcppServer] get ping, client = %u\n", num); - break; - case WStype_PONG: - // answer to a ping we send - Serial.printf("[OcppServer] get pong, client = %u\n", num); - break; - case WStype_FRAGMENT_TEXT_START: //fragments are not supported - case WStype_BIN: - default: - Serial.print(F("[OcppServer] Unsupported WebSocket event type, client = ")); - Serial.println(num); - break; - } -} - -OcppServer *OcppServer::instance = NULL; - -OcppServer *OcppServer::getInstance() { - if (!instance){ - instance = new OcppServer(); - } - return instance; -} - -void OcppServer::loop() { - wsockServer.loop(); -} - -void OcppServer::setReceiveTXTcallback(IPAddress &ip_addr, ReceiveTXTcallback &callback) { - /* - * Does route identified by ip_addr already exist? - */ - std::vector::iterator result = std::find_if(receiveTXTrouting.begin(), receiveTXTrouting.end(), - [ip_addr](const ReceiveTXTroute &elem) { - return elem.ip_addr == ip_addr; - }); - - if (result != receiveTXTrouting.end()) { - //found a route. Update callback - (*result).processTXT = callback; - } else { - //no route found. Add new one - ReceiveTXTroute route {}; - route.ip_addr = ip_addr; - route.processTXT = callback; - receiveTXTrouting.push_back(route); - } -} - -void OcppServer::removeReceiveTXTcallback(IPAddress &ip_addr) { - receiveTXTrouting.erase(std::remove_if(receiveTXTrouting.begin(), receiveTXTrouting.end(), - [ip_addr](const ReceiveTXTroute &elem) { - return elem.ip_addr == ip_addr; - }), receiveTXTrouting.end()); -} - -bool OcppServer::sendTXT(IPAddress &ip_addr, std::string &out) { - - WsClient mClient; - - std::vector::iterator result = std::find_if(receiveTXTrouting.begin(), receiveTXTrouting.end(), - [ip_addr](const ReceiveTXTroute &elem) { - return elem.ip_addr == ip_addr; - }); - - if (result != receiveTXTrouting.end()) { - mClient = (*result).num; - } else { - Serial.print(F("[OcppServer] Tried to send TXT for unregistered IP address! Abort\n")); - return false; - } - - return wsockServer.sendTXT(mClient, out.c_str(), out.length()); -} - -#endif //ndef AO_CUSTOM_WS diff --git a/src/ArduinoOcpp/Core/OcppServer.h b/src/ArduinoOcpp/Core/OcppServer.h deleted file mode 100644 index 64b43afb..00000000 --- a/src/ArduinoOcpp/Core/OcppServer.h +++ /dev/null @@ -1,56 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OCPPSERVER_H -#define OCPPSERVER_H - -#include -#include - -namespace ArduinoOcpp { - -using WsClient = uint8_t; - -} //end namespace ArduinoOcpp - -#ifndef AO_CUSTOM_WS - -#include - -namespace ArduinoOcpp { -namespace EspWiFi { - -struct ReceiveTXTroute { - IPAddress ip_addr; - WsClient num; - ReceiveTXTcallback processTXT; -}; - -class OcppServer { -private: - - std::vector receiveTXTrouting; - - WebSocketsServer wsockServer = WebSocketsServer(80); - - OcppServer(); - static OcppServer *instance; -public: - static OcppServer *getInstance(); - - void loop(); - - void wsockEvent(WsClient num, WStype_t type, uint8_t * payload, size_t length); - - void setReceiveTXTcallback(IPAddress &ip_addr, ReceiveTXTcallback &callback); - - void removeReceiveTXTcallback(IPAddress &ip_addr); - - bool sendTXT(IPAddress &ip_addr, std::string &out); -}; - -} //end namespace EspWiFi -} //end namespace ArduinoOcpp -#endif //ndef AO_CUSTOM_WS -#endif diff --git a/src/ArduinoOcpp/Core/OcppSocket.cpp b/src/ArduinoOcpp/Core/OcppSocket.cpp deleted file mode 100644 index f7468aca..00000000 --- a/src/ArduinoOcpp/Core/OcppSocket.cpp +++ /dev/null @@ -1,81 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include - -#ifndef AO_CUSTOM_WS - -using namespace ArduinoOcpp; - -using namespace ArduinoOcpp::EspWiFi; - -OcppClientSocket::OcppClientSocket(WebSocketsClient *wsock) : wsock(wsock) { - -} - -void OcppClientSocket::loop() { - wsock->loop(); -} - -bool OcppClientSocket::sendTXT(std::string &out) { - return wsock->sendTXT(out.c_str(), out.length()); -} - -void OcppClientSocket::setReceiveTXTcallback(ReceiveTXTcallback &callback) { - wsock->onEvent([callback](WStype_t type, uint8_t * payload, size_t length) { - switch (type) { - case WStype_DISCONNECTED: - AO_DBG_INFO("Disconnected"); - break; - case WStype_CONNECTED: - AO_DBG_INFO("Connected to url: %s", payload); - break; - case WStype_TEXT: - if (!callback((const char *) payload, length)) { //forward message to OcppEngine - AO_DBG_WARN("Processing WebSocket input event failed"); - } - break; - case WStype_BIN: - AO_DBG_WARN("Binary data stream not supported"); - break; - case WStype_PING: - // pong will be send automatically - AO_DBG_TRAFFIC_IN(8, "WS ping"); - break; - case WStype_PONG: - // answer to a ping we send - AO_DBG_TRAFFIC_IN(8, "WS pong"); - break; - case WStype_FRAGMENT_TEXT_START: //fragments are not supported - default: - AO_DBG_WARN("Unsupported WebSocket event type"); - break; - } - }); -} - -OcppServerSocket::OcppServerSocket(IPAddress &ip_addr) : ip_addr(ip_addr) { - -} - -OcppServerSocket::~OcppServerSocket() { - OcppServer::getInstance()->removeReceiveTXTcallback(this->ip_addr); -} - -void OcppServerSocket::loop() { - //nothing here. The client must call the EspWiFi server loop function -} - -bool OcppServerSocket::sendTXT(std::string &out) { - AO_DBG_TRAFFIC_OUT(out.c_str()); - return OcppServer::getInstance()->sendTXT(ip_addr, out); -} - -void OcppServerSocket::setReceiveTXTcallback(ReceiveTXTcallback &callback) { - OcppServer::getInstance()->setReceiveTXTcallback(ip_addr, callback); -} - -#endif diff --git a/src/ArduinoOcpp/Core/OcppSocket.h b/src/ArduinoOcpp/Core/OcppSocket.h deleted file mode 100644 index bb8c534a..00000000 --- a/src/ArduinoOcpp/Core/OcppSocket.h +++ /dev/null @@ -1,93 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OCPPSOCKET_H -#define OCPPSOCKET_H - -#include -#include -#include - -namespace ArduinoOcpp { - -using ReceiveTXTcallback = std::function; - -class OcppSocket { -public: - OcppSocket() = default; - virtual ~OcppSocket() = default; - - virtual void loop() = 0; - - virtual bool sendTXT(std::string &out) = 0; - - virtual void setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT) = 0; //ReceiveTXTcallback is defined in OcppServer.h -}; - -class OcppEchoSocket : public OcppSocket { -private: - ReceiveTXTcallback receiveTXT; - - bool connected = true; //for simulating connection losses -public: - void loop() override { } - bool sendTXT(std::string &out) override { - if (!connected) { - return true; - } - if (receiveTXT) { - return receiveTXT(out.c_str(), out.length()); - } else { - return false; - } - } - void setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT) override { - this->receiveTXT = receiveTXT; - } - - void setConnected(bool connected) {this->connected = connected;} - bool isConnected() {return connected;} -}; - -} //end namespace ArduinoOcpp - -#ifndef AO_CUSTOM_WS - -#include -#include - -namespace ArduinoOcpp { -namespace EspWiFi { - -class OcppClientSocket : public OcppSocket { -private: - WebSocketsClient *wsock; -public: - OcppClientSocket(WebSocketsClient *wsock); - - void loop(); - - bool sendTXT(std::string &out); - - void setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT); -}; - -class OcppServerSocket : public OcppSocket { -private: - IPAddress ip_addr; -public: - OcppServerSocket(IPAddress &ip_addr); - ~OcppServerSocket(); - - void loop(); - - bool sendTXT(std::string &out); - - void setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT); -}; - -} //end namespace EspWiFi -} //end namespace ArduinoOcpp -#endif //ndef AO_CUSTOM_WS -#endif diff --git a/src/ArduinoOcpp/Core/OcppTime.h b/src/ArduinoOcpp/Core/OcppTime.h deleted file mode 100644 index 2d18dedf..00000000 --- a/src/ArduinoOcpp/Core/OcppTime.h +++ /dev/null @@ -1,129 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OCPPTIME_H -#define OCPPTIME_H - -#include -#include -#include - -namespace ArduinoOcpp { - -typedef int32_t otime_t; //requires 32bit signed integer or bigger -#define OTIME_MAX ((otime_t) INT32_MAX) -typedef std::function OcppClock; - -#define INFINITY_THLD (OTIME_MAX - ((otime_t) (400 * 24 * 3600))) //Upper limiter for valid time range. From this value on, a scalar time means "infinity". It's 400 days before the "year 2038" problem. -#define JSONDATE_LENGTH 24 - -namespace Clocks { - -/* - * Basic clock implementation. Works if ao_tick_ms() is exact enough for you and if device doesn't go in sleep mode. - */ -extern OcppClock DEFAULT_CLOCK; -} //end namespace Clocks - -class OcppTimestamp { -private: - /* - * Internal representation of the current time. The initial values correspond to UNIX-time 0. January - * corresponds to month 0 and the first day in the month is day 0. - */ - int16_t year = 1970; - int16_t month = 0; - int16_t day = 0; - int32_t hour = 0; - int32_t minute = 0; - int32_t second = 0; - -public: - - OcppTimestamp(); - - OcppTimestamp(int16_t year, int16_t month, int16_t day, int32_t hour, int32_t minute, int32_t second) : - year(year), month(month), day(day), hour(hour), minute(minute), second(second) { }; - - /** - * Expects a date string like - * 2020-10-01T20:53:32.486Z - * - * as generated in JavaScript by calling toJSON() on a Date object - * - * Only processes the first 19 characters. The subsequent are ignored until terminating 0. - * - * Has a semi-sophisticated type check included. Will return true on successful time set and false if - * the given string is not a JSON Date string. - * - * jsonDateString: 0-terminated string - */ - bool setTime(const char* jsonDateString); - - bool toJsonString(char *out, size_t buffsize) const; - - OcppTimestamp &operator=(const OcppTimestamp &rhs); - - OcppTimestamp &operator+=(int secs); - OcppTimestamp &operator-=(int secs); - - otime_t operator-(const OcppTimestamp &rhs) const; - - friend OcppTimestamp operator+(const OcppTimestamp &lhs, int secs); - friend OcppTimestamp operator-(const OcppTimestamp &lhs, int secs); - - friend bool operator==(const OcppTimestamp &lhs, const OcppTimestamp &rhs); - friend bool operator!=(const OcppTimestamp &lhs, const OcppTimestamp &rhs); - friend bool operator<(const OcppTimestamp &lhs, const OcppTimestamp &rhs); - friend bool operator<=(const OcppTimestamp &lhs, const OcppTimestamp &rhs); - friend bool operator>(const OcppTimestamp &lhs, const OcppTimestamp &rhs); - friend bool operator>=(const OcppTimestamp &lhs, const OcppTimestamp &rhs); -}; - -extern const OcppTimestamp MIN_TIME; -extern const OcppTimestamp MAX_TIME; - -class OcppTime { -private: - - OcppTimestamp ocpp_basetime = OcppTimestamp(); - otime_t system_basetime = 0; //your system clock's time at the moment when the OCPP server's time was taken - bool ocppTimeIsSet = false; - - OcppClock system_clock = [] () {return (otime_t) 0;}; - - OcppTimestamp currentTime = OcppTimestamp(); - otime_t previousUpdate = -1; - -public: - - OcppTime(const OcppClock& system_clock); - //OcppTime(const OcppTime& ocppTime) = default; - - otime_t getOcppTimeScalar(); //returns current time of the OCPP server in non-UNIX but signed integer format. t2 - t1 is the time difference in seconds. - const OcppTimestamp &getOcppTimestampNow(); - OcppTimestamp createTimestamp(otime_t scalar); //creates a timestamp in a JSON-serializable format. createTimestamp(getOcppTimeScalar()) will return the current OCPP time - otime_t toOcppTimeScalar(const OcppTimestamp &otimestamp); - - /** - * Expects a date string like - * 2020-10-01T20:53:32.486Z - * - * as generated in JavaScript by calling toJSON() on a Date object - * - * Only processes the first 19 characters. The subsequent are ignored until terminating 0. - * - * Has a semi-sophisticated type check included. Will return true on successful time set and false if - * the given string is not a JSON Date string. - * - * jsonDateString: 0-terminated string - */ - bool setOcppTime(const char* jsonDateString); - - bool isValid() {return ocppTimeIsSet;} -}; - -} - -#endif diff --git a/src/ArduinoOcpp/Core/OperationStore.cpp b/src/ArduinoOcpp/Core/OperationStore.cpp deleted file mode 100644 index 1f26d12f..00000000 --- a/src/ArduinoOcpp/Core/OperationStore.cpp +++ /dev/null @@ -1,208 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include - -#ifndef AO_OPSTORE_DIR -#define AO_OPSTORE_DIR AO_FILENAME_PREFIX "/" -#endif - -#define AO_OPSTORE_FN AO_FILENAME_PREFIX "/opstore.cnf" - -#define AO_OPHISTORY_SIZE 3 - -using namespace ArduinoOcpp; - -bool StoredOperationHandler::commit() { - if (isPersistent) { - AO_DBG_ERR("cannot call two times"); - return false; - } - if (!filesystem) { - AO_DBG_DEBUG("filesystem"); - return false; - } - - if (!rpc || !payload) { - AO_DBG_ERR("unitialized"); - return false; - } - - opNr = context.reserveOpNr(); - - char fn [MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MAX_PATH_SIZE, AO_OPSTORE_DIR "op" "-%u.jsn", opNr); - if (ret < 0 || ret >= MAX_PATH_SIZE) { - AO_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)) { - AO_DBG_ERR("FS error"); - return false; - } - - isPersistent = true; - return true; -} - -bool StoredOperationHandler::restore(unsigned int opNrToLoad) { - if (isPersistent) { - AO_DBG_ERR("cannot restore after commit"); - return false; - } - if (!filesystem) { - AO_DBG_DEBUG("filesystem"); - return false; - } - - opNr = opNrToLoad; - - char fn [MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MAX_PATH_SIZE, AO_OPSTORE_DIR "op" "-%u.jsn", opNr); - if (ret < 0 || ret >= MAX_PATH_SIZE) { - AO_DBG_ERR("fn error: %i", ret); - return false; - } - - size_t msize; - if (filesystem->stat(fn, &msize) != 0) { - AO_DBG_VERBOSE("operation %u does not exist", opNr); - return false; - } - - auto doc = FilesystemUtils::loadJson(filesystem, fn); - if (!doc) { - AO_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; -} - -OperationStore::OperationStore(std::shared_ptr filesystem) : filesystem(filesystem) { - opBegin = declareConfiguration("AO_opBegin", 0, AO_OPSTORE_FN, false, false, true, false); - - if (!opBegin || *opBegin < 0) { - AO_DBG_ERR("init failure"); - } else if (filesystem) { - opEnd = *opBegin; - - unsigned int misses = 0; - unsigned int i = opEnd; - while (misses < 3) { - char fn [MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MAX_PATH_SIZE, AO_OPSTORE_DIR "op" "-%u.jsn", i); - if (ret < 0 || ret >= MAX_PATH_SIZE) { - AO_DBG_ERR("fn error: %i", ret); - misses++; - i = (i + 1) % AO_MAX_OPNR; - continue; - } - - size_t msize; - if (filesystem->stat(fn, &msize) != 0) { - AO_DBG_DEBUG("operation %u does not exist", i); - misses++; - i = (i + 1) % AO_MAX_OPNR; - continue; - } - - //file exists - misses = 0; - i = (i + 1) % AO_MAX_OPNR; - opEnd = i; - } - } -} - -std::unique_ptr OperationStore::makeOpHandler() { - return std::unique_ptr(new StoredOperationHandler(*this, filesystem)); -} - -unsigned int OperationStore::reserveOpNr() { - AO_DBG_DEBUG("reserved opNr %u", opEnd); - auto res = opEnd; - opEnd++; - opEnd %= AO_MAX_OPNR; - return res; -} - -void OperationStore::advanceOpNr(unsigned int oldOpNr) { - if (!opBegin || *opBegin < 0) { - AO_DBG_ERR("init failure"); - return; - } - - if (oldOpNr != (unsigned int) *opBegin) { - if ((oldOpNr + AO_MAX_OPNR - (unsigned int) *opBegin) % AO_MAX_OPNR < 100) { - AO_DBG_ERR("synchronization failure - try to fix"); - (void)0; - } else { - AO_DBG_ERR("synchronization failure"); - return; - } - } - - unsigned int opNr = (oldOpNr + 1) % AO_MAX_OPNR; - - //delete range [*opBegin ... opNr) - - unsigned int rangeSize = (opNr + AO_MAX_OPNR - (unsigned int) *opBegin) % AO_MAX_OPNR; - - AO_DBG_DEBUG("delete %u operations", rangeSize); - - for (unsigned int i = 0; i < rangeSize; i++) { - unsigned int op = ((unsigned int) *opBegin + i + AO_MAX_OPNR - AO_OPHISTORY_SIZE) % AO_MAX_OPNR; - - char fn [MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MAX_PATH_SIZE, AO_OPSTORE_DIR "op" "-%u.jsn", op); - if (ret < 0 || ret >= MAX_PATH_SIZE) { - AO_DBG_ERR("fn error: %i", ret); - break; - } - - size_t msize; - if (filesystem->stat(fn, &msize) != 0) { - AO_DBG_DEBUG("operation %u does not exist", i); - continue; - } - - bool success = filesystem->remove(fn); - if (!success) { - AO_DBG_ERR("error deleting %s", fn); - (void)0; - } - } - - AO_DBG_DEBUG("advance opBegin: %u", opNr); - *opBegin = opNr; - configuration_save(); -} - -unsigned int OperationStore::getOpBegin() { - if (!opBegin || *opBegin < 0) { - AO_DBG_ERR("invalid state"); - return 0; - } - return (unsigned int) *opBegin; -} diff --git a/src/ArduinoOcpp/Core/OperationStore.h b/src/ArduinoOcpp/Core/OperationStore.h deleted file mode 100644 index 1cba6bf5..00000000 --- a/src/ArduinoOcpp/Core/OperationStore.h +++ /dev/null @@ -1,70 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OPERATIONSTORE_H -#define OPERATIONSTORE_H - -#include -#include -#include - -#define AO_MAX_OPNR 10000 - -namespace ArduinoOcpp { - -class OperationStore; -class FilesystemAdapter; -template class Configuration; - -class StoredOperationHandler { -private: - OperationStore& context; - int opNr = -1; - std::shared_ptr filesystem; - - std::unique_ptr rpc; - std::unique_ptr payload; - - bool isPersistent = false; - -public: - StoredOperationHandler(OperationStore& context, std::shared_ptr filesystem) : context(context), filesystem(filesystem) {} - - void setRpc(std::unique_ptr rpc) {this->rpc = std::move(rpc);} - void setPayload(std::unique_ptr payload) {this->payload = std::move(payload);} - - std::unique_ptr getRpc() {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 OperationStore { -private: - std::shared_ptr filesystem; - std::shared_ptr> opBegin; //Tx-related operations are stored; index of the first pending operation - unsigned int opEnd = 0; //one place after last number - -public: - OperationStore() = delete; - OperationStore(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/ArduinoOcpp/Core/OperationsQueue.cpp b/src/ArduinoOcpp/Core/OperationsQueue.cpp deleted file mode 100644 index 8cacc00c..00000000 --- a/src/ArduinoOcpp/Core/OperationsQueue.cpp +++ /dev/null @@ -1,156 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include - -#include -#include -#include - -#include - -#define AO_OPERATIONCACHE_MAXSIZE 10 - -using namespace ArduinoOcpp; - -OperationsQueue::OperationsQueue(std::shared_ptr baseModel, std::shared_ptr filesystem) - : opStore(filesystem), baseModel(baseModel) { } - -OperationsQueue::~OperationsQueue() { - -} - -OcppOperation *OperationsQueue::front() { - if (!head && !tailCache.empty()) { - AO_DBG_ERR("invalid state"); - pop_front(); - } - return head.get(); -} - -void OperationsQueue::pop_front() { - - if (head && head->getStorageHandler() && head->getStorageHandler()->getOpNr() >= 0) { - opStore.advanceOpNr(head->getStorageHandler()->getOpNr()); - AO_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; - - bool exists = false; - unsigned int range = (opStore.getOpEnd() + AO_MAX_OPNR - nextOpNr) % AO_MAX_OPNR; - for (size_t i = 0; i < range; i++) { - exists = storageHandler->restore(nextOpNr); - if (exists) { - break; - } - nextOpNr++; - nextOpNr %= AO_MAX_OPNR; - } - - if (exists) { - //case B) -> load operation from flash and take it as front element - fetched = makeOcppOperation(); - - bool success = fetched->restore(std::move(storageHandler), baseModel); - - if (!success) { - AO_DBG_ERR("could not restore operation"); - fetched.reset(); - } - - if (!fetched->isFullyConfigured()) { - AO_DBG_ERR("stored op initialization failure"); - fetched.reset(); - } - } - - if (fetched) { - //found operation in flash -> case B) - head = std::move(fetched); - AO_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(); - } - } - } - - AO_DBG_VERBOSE("popped front"); -} - -void OperationsQueue::initiate(std::unique_ptr op) { - - op->initiate(opStore.makeOpHandler()); - - if (!head && !tailCache.empty()) { - AO_DBG_ERR("invalid state"); - pop_front(); - } - - if (!head) { - head = std::move(op); - } else { - if (tailCache.size() >= AO_OPERATIONCACHE_MAXSIZE) { - AO_DBG_INFO("Replace cached operation (cache full): "); - tailCache.front()->print_debug(); - tailCache.pop_front(); - } - - tailCache.push_back(std::move(op)); - } -} - -void OperationsQueue::drop_if(std::function&)> pred) { - - while (head && pred(head)) { - pop_front(); - } - - tailCache.erase(std::remove_if(tailCache.begin(), tailCache.end(), pred), tailCache.end()); -} - -std::deque>::iterator OperationsQueue::begin_tail() { - return tailCache.begin(); -} - -std::deque>::iterator OperationsQueue::end_tail() { - return tailCache.end(); -} - -std::deque>::iterator OperationsQueue::erase_tail(std::deque>::iterator el) { - return tailCache.erase(el); -} diff --git a/src/ArduinoOcpp/Core/OperationsQueue.h b/src/ArduinoOcpp/Core/OperationsQueue.h deleted file mode 100644 index 1eb8df4a..00000000 --- a/src/ArduinoOcpp/Core/OperationsQueue.h +++ /dev/null @@ -1,46 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OPERATIONSQUEUE_H -#define OPERATIONSQUEUE_H - -#include - -#include -#include -#include - -namespace ArduinoOcpp { - -class FilesystemAdapter; -class OcppOperation; -class OcppModel; - -class OperationsQueue { -private: - OperationStore opStore; - std::shared_ptr baseModel; - - std::unique_ptr head; - std::deque> tailCache; -public: - - OperationsQueue(std::shared_ptr baseModel, std::shared_ptr filesystem); - ~OperationsQueue(); - - OcppOperation *front(); - void pop_front(); - - void initiate(std::unique_ptr op); - - std::deque>::iterator begin_tail(); //iterator for all cached elements except head - std::deque>::iterator end_tail(); - std::deque>::iterator erase_tail(std::deque>::iterator el); - void drop_if(std::function&)> pred); //drops operations from this queue where pred(operation) == true. Executes pred in order - -}; - -} - -#endif diff --git a/src/ArduinoOcpp/Core/PollResult.h b/src/ArduinoOcpp/Core/PollResult.h deleted file mode 100644 index a21324f8..00000000 --- a/src/ArduinoOcpp/Core/PollResult.h +++ /dev/null @@ -1,51 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef POLLRESULT_H -#define POLLRESULT_H - -#include -#include - -namespace ArduinoOcpp { - -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) { - AO_DBG_ERR("Not ready"); - (void)0; - } - ready = false; - return std::move(value); - } - T& getValue() const { - if (!ready) { - AO_DBG_ERR("Not ready"); - (void)0; - } - return *value; - } - operator bool() const {return ready;} - - static PollResult Await() {return PollResult();} -}; - -} - -#endif diff --git a/src/ArduinoOcpp/Debug.h b/src/ArduinoOcpp/Debug.h deleted file mode 100644 index 3eb8ab4c..00000000 --- a/src/ArduinoOcpp/Debug.h +++ /dev/null @@ -1,77 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef AO_DEBUG_H -#define AO_DEBUG_H - -#include - -#define AO_DL_NONE 0x00 //suppress all output to the console -#define AO_DL_ERROR 0x01 //report failures -#define AO_DL_WARN 0x02 //report observed or assumed inconsistent state -#define AO_DL_INFO 0x03 //make internal state apparent -#define AO_DL_DEBUG 0x04 //relevant info for debugging -#define AO_DL_VERBOSE 0x05 //all output - -#ifndef AO_DBG_LEVEL -#define AO_DBG_LEVEL AO_DL_INFO //default -#endif - -#define AO_DBG(level, X) \ - do { \ - AO_CONSOLE_PRINTF("[AO] %s (%s:%i): ",level, __FILE__,__LINE__); \ - AO_CONSOLE_PRINTF X; \ - AO_CONSOLE_PRINTF("\n"); \ - } while (0); - -#if AO_DBG_LEVEL >= AO_DL_ERROR -#define AO_DBG_ERR(...) AO_DBG("ERROR",(__VA_ARGS__)) -#else -#define AO_DBG_ERR(...) -#endif - -#if AO_DBG_LEVEL >= AO_DL_WARN -#define AO_DBG_WARN(...) AO_DBG("warning",(__VA_ARGS__)) -#else -#define AO_DBG_WARN(...) -#endif - -#if AO_DBG_LEVEL >= AO_DL_INFO -#define AO_DBG_INFO(...) AO_DBG("info",(__VA_ARGS__)) -#else -#define AO_DBG_INFO(...) -#endif - -#if AO_DBG_LEVEL >= AO_DL_DEBUG -#define AO_DBG_DEBUG(...) AO_DBG("debug",(__VA_ARGS__)) -#else -#define AO_DBG_DEBUG(...) -#endif - -#if AO_DBG_LEVEL >= AO_DL_VERBOSE -#define AO_DBG_VERBOSE(...) AO_DBG("verbose",(__VA_ARGS__)) -#else -#define AO_DBG_VERBOSE(...) -#endif - -#ifdef AO_TRAFFIC_OUT - -#define AO_DBG_TRAFFIC_OUT(...) \ - do { \ - AO_CONSOLE_PRINTF("[AO] Send: %s",__VA_ARGS__); \ - AO_CONSOLE_PRINTF("\n"); \ - } while (0) - -#define AO_DBG_TRAFFIC_IN(...) \ - do { \ - AO_CONSOLE_PRINTF("[AO] Recv: %.*s",__VA_ARGS__); \ - AO_CONSOLE_PRINTF("\n"); \ - } while (0) - -#else -#define AO_DBG_TRAFFIC_OUT(...) -#define AO_DBG_TRAFFIC_IN(...) -#endif - -#endif diff --git a/src/ArduinoOcpp/MessagesV16/Authorize.cpp b/src/ArduinoOcpp/MessagesV16/Authorize.cpp deleted file mode 100644 index 4227f4ff..00000000 --- a/src/ArduinoOcpp/MessagesV16/Authorize.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include - -#include - -using ArduinoOcpp::Ocpp16::Authorize; - -//Authorize::Authorize() { -// snprintf(this->idTag, IDTAG_LEN_MAX + 1, "A0-00-00-00"); //Use a default payload. In the typical use case of this library, you probably you don't even need Authorization at all -//} - -Authorize::Authorize(const char *idTagIn) { - if (idTagIn && strnlen(idTagIn, IDTAG_LEN_MAX + 2) <= IDTAG_LEN_MAX) { - snprintf(idTag, IDTAG_LEN_MAX + 1, "%s", idTagIn); - } else { - AO_DBG_WARN("Format violation of idTag. Use default idTag"); - snprintf(idTag, IDTAG_LEN_MAX + 1, "A0-00-00-00"); - } -} - -const char* Authorize::getOcppOperationType(){ - return "Authorize"; -} - -std::unique_ptr Authorize::createReq() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1) + (IDTAG_LEN_MAX + 1))); - JsonObject payload = doc->to(); - payload["idTag"] = idTag; - return doc; -} - -void Authorize::processConf(JsonObject payload){ - const char *idTagInfo = payload["idTagInfo"]["status"] | "not specified"; - - if (!strcmp(idTagInfo, "Accepted")) { - AO_DBG_INFO("Request has been accepted"); - - //TODO add entry in offline auth cache - - } else { - AO_DBG_INFO("Request has been denied. Reason: %s", 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 = std::unique_ptr(new DynamicJsonDocument(2 * JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); - idTagInfo["status"] = "Accepted"; - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/Authorize.h b/src/ArduinoOcpp/MessagesV16/Authorize.h deleted file mode 100644 index 83ee7ee5..00000000 --- a/src/ArduinoOcpp/MessagesV16/Authorize.h +++ /dev/null @@ -1,37 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef AUTHORIZE_H -#define AUTHORIZE_H - -#include -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class Authorize : public OcppMessage { -private: - char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; -public: -// Authorize(); - - Authorize(const char *idTag); - - const char* getOcppOperationType(); - - std::unique_ptr createReq(); - - void processConf(JsonObject payload); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); - -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp - -#endif diff --git a/src/ArduinoOcpp/MessagesV16/BootNotification.cpp b/src/ArduinoOcpp/MessagesV16/BootNotification.cpp deleted file mode 100644 index 2c4f3b86..00000000 --- a/src/ArduinoOcpp/MessagesV16/BootNotification.cpp +++ /dev/null @@ -1,132 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include - -#include - -using ArduinoOcpp::Ocpp16::BootNotification; - -BootNotification::BootNotification() { - -} - -BootNotification::BootNotification(std::unique_ptr payload) : credentials(std::move(payload)) { - -} - -const char* BootNotification::getOcppOperationType(){ - return "BootNotification"; -} - -void BootNotification::initiate() { - if (credentials && - ocppModel && ocppModel->getChargePointStatusService()) { - auto cpStatus = ocppModel->getChargePointStatusService(); - cpStatus->setChargePointCredentials(*credentials); - credentials.reset(); - } -} - -std::unique_ptr BootNotification::createReq() { - - if (ocppModel && ocppModel->getChargePointStatusService()) { - auto cpStatus = ocppModel->getChargePointStatusService(); - const auto& cpCredentials = cpStatus->getChargePointCredentials(); - - std::unique_ptr doc; - size_t capacity = JSON_OBJECT_SIZE(9) + cpCredentials.size(); - DeserializationError err = DeserializationError::NoMemory; - while (err == DeserializationError::NoMemory) { - doc.reset(new DynamicJsonDocument(capacity)); - err = deserializeJson(*doc, cpCredentials); - - capacity *= 3; - capacity /= 2; - } - - if (!err) { - return doc; - } else { - AO_DBG_ERR("could not parse stored credentials: %s", err.c_str()); - } - } - - if (credentials) { - return std::unique_ptr(new DynamicJsonDocument(*credentials)); - } - - AO_DBG_ERR("payload undefined"); - return createEmptyDocument(); -} - -void BootNotification::processConf(JsonObject payload){ - const char* currentTime = payload["currentTime"] | "Invalid"; - if (strcmp(currentTime, "Invalid")) { - if (ocppModel && ocppModel->getOcppTime().setOcppTime(currentTime)) { - //success - } else { - AO_DBG_ERR("Time string format violation. Expect format like 2022-02-01T20:53:32.486Z"); - } - } else { - AO_DBG_ERR("Missing attribute currentTime"); - } - - int interval = payload["interval"] | -1; - - //only write if in valid range - if (interval >= 1) { - std::shared_ptr> intervalConf = declareConfiguration("HeartbeatInterval", 86400); - if (intervalConf && interval != *intervalConf) { - *intervalConf = interval; - configuration_save(); - } - } - - const char* status = payload["status"] | "Invalid"; - - if (!strcmp(status, "Accepted")) { - AO_DBG_INFO("Request has been accepted"); - if (ocppModel && ocppModel->getChargePointStatusService() != nullptr) { - ocppModel->getChargePointStatusService()->boot(); - } - } else { - AO_DBG_WARN("Request unsuccessful"); - } -} - -void BootNotification::processReq(JsonObject payload){ - /* - * Ignore Contents of this Req-message, because this is for debug purposes only - */ -} - -std::unique_ptr BootNotification::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(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 - OcppTimestamp ocppTimeReference = OcppTimestamp(2022,0,27,11,59,55); - OcppTimestamp ocppSelect = ocppTimeReference; - if (ocppModel) { - auto& ocppTime = ocppModel->getOcppTime(); - OcppTimestamp ocppNow = ocppTime.getOcppTimestampNow(); - if (ocppNow > ocppTimeReference) { - //time has already been set - ocppSelect = ocppNow; - } - } - - char ocppNowJson [JSONDATE_LENGTH + 1] = {'\0'}; - ocppSelect.toJsonString(ocppNowJson, JSONDATE_LENGTH + 1); - payload["currentTime"] = ocppNowJson; - - payload["interval"] = 86400; //heartbeat send interval - not relevant for JSON variant of OCPP so send dummy value that likely won't break - payload["status"] = "Accepted"; - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/BootNotification.h b/src/ArduinoOcpp/MessagesV16/BootNotification.h deleted file mode 100644 index 5f33364a..00000000 --- a/src/ArduinoOcpp/MessagesV16/BootNotification.h +++ /dev/null @@ -1,45 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef BOOTNOTIFICATION_H -#define BOOTNOTIFICATION_H - -#include -#include - -#define CP_MODEL_LEN_MAX CiString20TypeLen -#define CP_SERIALNUMBER_LEN_MAX CiString25TypeLen -#define CP_VENDOR_LEN_MAX CiString20TypeLen -#define FW_VERSION_LEN_MAX CiString50TypeLen - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class BootNotification : public OcppMessage { -private: - std::unique_ptr credentials; -public: - BootNotification(); - - ~BootNotification() = default; - - BootNotification(std::unique_ptr payload); - - const char* getOcppOperationType(); - - void initiate(); - - std::unique_ptr createReq(); - - void processConf(JsonObject payload); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp - -#endif diff --git a/src/ArduinoOcpp/MessagesV16/ChangeAvailability.cpp b/src/ArduinoOcpp/MessagesV16/ChangeAvailability.cpp deleted file mode 100644 index 0d3687ec..00000000 --- a/src/ArduinoOcpp/MessagesV16/ChangeAvailability.cpp +++ /dev/null @@ -1,71 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include - -#include - -using ArduinoOcpp::Ocpp16::ChangeAvailability; - -ChangeAvailability::ChangeAvailability() { - -} - -const char* ChangeAvailability::getOcppOperationType(){ - return "ChangeAvailability"; -} - -void ChangeAvailability::processReq(JsonObject payload) { - int connectorId = payload["connectorId"] | -1; - if (!ocppModel || !ocppModel->getChargePointStatusService()) { - return; - } - auto cpStatus = ocppModel->getChargePointStatusService(); - if (connectorId < 0 || connectorId >= cpStatus->getNumConnectors()) { - return; - } - - const char *type = payload["type"] | "INVALID"; - bool available = false; - - if (!strcmp(type, "Operative")) { - accepted = true; - available = true; - } else if (!strcmp(type, "Inoperative")) { - accepted = true; - available = false; - } else { - return; - } - - if (connectorId == 0) { - for (int i = 0; i < cpStatus->getNumConnectors(); i++) { - cpStatus->getConnector(i)->setAvailability(available); - if (cpStatus->getConnector(i)->getAvailability() == AVAILABILITY_INOPERATIVE_SCHEDULED) { - scheduled = true; - } - } - } else { - cpStatus->getConnector(connectorId)->setAvailability(available); - if (cpStatus->getConnector(connectorId)->getAvailability() == AVAILABILITY_INOPERATIVE_SCHEDULED) { - scheduled = true; - } - } -} - -std::unique_ptr ChangeAvailability::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - if (!accepted) { - payload["status"] = "Rejected"; - } else if (scheduled) { - payload["status"] = "Scheduled"; - } else { - payload["status"] = "Accepted"; - } - - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/ChangeAvailability.h b/src/ArduinoOcpp/MessagesV16/ChangeAvailability.h deleted file mode 100644 index c8f088c7..00000000 --- a/src/ArduinoOcpp/MessagesV16/ChangeAvailability.h +++ /dev/null @@ -1,29 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef CHANGEAVAILABILITY_H -#define CHANGEAVAILABILITY_H - -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class ChangeAvailability : public OcppMessage { -private: - bool scheduled = false; - bool accepted = false; -public: - ChangeAvailability(); - - const char* getOcppOperationType(); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/ChangeConfiguration.cpp b/src/ArduinoOcpp/MessagesV16/ChangeConfiguration.cpp deleted file mode 100644 index 71373447..00000000 --- a/src/ArduinoOcpp/MessagesV16/ChangeConfiguration.cpp +++ /dev/null @@ -1,192 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include - -#include //for isnan check -#include //for tolower - -using ArduinoOcpp::Ocpp16::ChangeConfiguration; - -ChangeConfiguration::ChangeConfiguration() { - -} - -const char* ChangeConfiguration::getOcppOperationType(){ - return "ChangeConfiguration"; -} - -void ChangeConfiguration::processReq(JsonObject payload) { - const char *key = payload["key"] | ""; - if (strlen(key) < 1) { - errorCode = "FormationViolation"; - AO_DBG_WARN("Could not read key"); - return; - } - - if (!payload["value"].is()) { - errorCode = "FormationViolation"; - AO_DBG_WARN("Message is lacking value"); - return; - } - - const char *value = payload["value"]; - - std::shared_ptr configuration = getConfiguration(key); - - if (!configuration) { - //configuration not found or hidden configuration - notSupported = true; - return; - } - - if (!configuration->permissionRemotePeerCanWrite()) { - AO_DBG_WARN("Trying to override readonly value"); - - if (configuration->permissionRemotePeerCanRead()) { - readOnly = true; - } else { - //can neither read nor write -> hidden config - notSupported = true; - } - return; - } - - //write config - - /* - * Try to interpret input as number - */ - - bool convertibleInt = true; - int numInt = 0; - bool convertibleFloat = true; - float numFloat = 0.f; - 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) - float numFloatTranslate = 1.f; - 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'; - } - - //float interpretation - numFloat *= 10.f; - numFloat += (float) (*c - '0'); - if (nDots != 0) { - numFloatTranslate *= 10.f; - } - } else if (*c == '.') { - nDots++; - } else if (c == value && *c == '-') { - nSign++; - } else { - nNonDigits++; - } - } - - numFloat /= numFloatTranslate; // "1." <-- numFlTrans = 1.f; "-1.234" <-- numFlTrans = 1000.f - - if (nSign == 1) { - numInt = -numInt; - numFloat *= -1.f; - } - - 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; - convertibleFloat = false; - } - - if (nDigits > INT_MAXDIGITS) { - AO_DBG_DEBUG("Possible integer overflow: key = %s, value = %s", key, value); - convertibleInt = false; - } - - if (std::isnan(numFloat)) { - convertibleFloat = 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; - } - - //Store (parsed) value to Config - - if (!strcmp(configuration->getSerializedType(), SerializedType::get()) && convertibleInt) { - std::shared_ptr> configurationConcrete = std::static_pointer_cast>(configuration); - *configurationConcrete = numInt; - } else if (!strcmp(configuration->getSerializedType(), SerializedType::get()) && convertibleFloat) { - std::shared_ptr> configurationConcrete = std::static_pointer_cast>(configuration); - *configurationConcrete = numFloat; - } else if (!strcmp(configuration->getSerializedType(), SerializedType::get()) && convertibleBool) { - std::shared_ptr> configurationConcrete = std::static_pointer_cast>(configuration); - *configurationConcrete = numBool; - } else if (!strcmp(configuration->getSerializedType(), SerializedType::get())) { - std::shared_ptr> configurationConcrete = std::static_pointer_cast>(configuration); - - //string configurations can have a validator - auto validator = configurationConcrete->getValidator(); - if (validator && !validator(value)) { - //validator exists and validation fails - reject = true; - AO_DBG_WARN("validation failed for key=%s value=%s", key, value); - return; - } - - *configurationConcrete = value; - } else { - reject = true; - AO_DBG_WARN("Value has incompatible type"); - return; - } - - //success - - if (!configuration_save()) { - AO_DBG_ERR("could not write changes to flash"); - errorCode = "InternalError"; - return; - } - - if (configuration->requiresRebootWhenChanged()) { - rebootRequired = true; - } - - //success -} - -std::unique_ptr ChangeConfiguration::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - if (notSupported) { - payload["status"] = "NotSupported"; - } else if (reject || readOnly) { - payload["status"] = "Rejected"; - } else if (rebootRequired) { - payload["status"] = "RebootRequired"; - } else { - payload["status"] = "Accepted"; - } - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/ChangeConfiguration.h b/src/ArduinoOcpp/MessagesV16/ChangeConfiguration.h deleted file mode 100644 index 9467cba8..00000000 --- a/src/ArduinoOcpp/MessagesV16/ChangeConfiguration.h +++ /dev/null @@ -1,36 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef CHANGECONFIGURATION_H -#define CHANGECONFIGURATION_H - -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class ChangeConfiguration : public OcppMessage { -private: - bool reject = false; - bool rebootRequired = false; - bool readOnly = false; - bool notSupported = false; - - const char *errorCode = nullptr; -public: - ChangeConfiguration(); - - const char* getOcppOperationType(); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); - - const char *getErrorCode() {return errorCode;} - -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/ClearCache.cpp b/src/ArduinoOcpp/MessagesV16/ClearCache.cpp deleted file mode 100644 index d761c135..00000000 --- a/src/ArduinoOcpp/MessagesV16/ClearCache.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include - -using ArduinoOcpp::Ocpp16::ClearCache; - -ClearCache::ClearCache() { - -} - -const char* ClearCache::getOcppOperationType(){ - return "ClearCache"; -} - -void ClearCache::processReq(JsonObject payload) { - AO_DBG_WARN("Authorization Cache not supported - ClearCache is without effect"); -} - -std::unique_ptr ClearCache::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - payload["status"] = "Accepted"; //"Accepted", because the intended postcondition is true - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/ClearCache.h b/src/ArduinoOcpp/MessagesV16/ClearCache.h deleted file mode 100644 index ea166580..00000000 --- a/src/ArduinoOcpp/MessagesV16/ClearCache.h +++ /dev/null @@ -1,26 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef CLEARCACHE_H -#define CLEARCACHE_H - -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class ClearCache : public OcppMessage { -public: - ClearCache(); - - const char* getOcppOperationType(); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/ClearChargingProfile.h b/src/ArduinoOcpp/MessagesV16/ClearChargingProfile.h deleted file mode 100644 index 43c14c54..00000000 --- a/src/ArduinoOcpp/MessagesV16/ClearChargingProfile.h +++ /dev/null @@ -1,28 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef CLEARCHARGINGPROFILE_H -#define CLEARCHARGINGPROFILE_H - -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class ClearChargingProfile : public OcppMessage { -private: - bool matchingProfilesFound = false; -public: - ClearChargingProfile(); - - const char* getOcppOperationType(); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/DataTransfer.cpp b/src/ArduinoOcpp/MessagesV16/DataTransfer.cpp deleted file mode 100644 index ee544883..00000000 --- a/src/ArduinoOcpp/MessagesV16/DataTransfer.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include - -using ArduinoOcpp::Ocpp16::DataTransfer; - -DataTransfer::DataTransfer(const std::string &msg) { - this->msg = msg; -} - -const char* DataTransfer::getOcppOperationType(){ - return "DataTransfer"; -} - -std::unique_ptr DataTransfer::createReq() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2) + (msg.length() + 1))); - JsonObject payload = doc->to(); - payload["vendorId"] = "CustomVendor"; - payload["data"] = msg; - return doc; -} - -void DataTransfer::processConf(JsonObject payload){ - std::string status = payload["status"] | "Invalid"; - - if (status == "Accepted") { - AO_DBG_DEBUG("Request has been accepted"); - } else { - AO_DBG_INFO("Request has been denied"); - } -} diff --git a/src/ArduinoOcpp/MessagesV16/DataTransfer.h b/src/ArduinoOcpp/MessagesV16/DataTransfer.h deleted file mode 100644 index f4bdf741..00000000 --- a/src/ArduinoOcpp/MessagesV16/DataTransfer.h +++ /dev/null @@ -1,29 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef DATATRANSFER_H -#define DATATRANSFER_H - -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class DataTransfer : public OcppMessage { -private: - std::string msg {}; -public: - DataTransfer(const std::string &msg); - - const char* getOcppOperationType(); - - std::unique_ptr createReq(); - - void processConf(JsonObject payload); - -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/DiagnosticsStatusNotification.cpp b/src/ArduinoOcpp/MessagesV16/DiagnosticsStatusNotification.cpp deleted file mode 100644 index c53187f1..00000000 --- a/src/ArduinoOcpp/MessagesV16/DiagnosticsStatusNotification.cpp +++ /dev/null @@ -1,52 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include - -using ArduinoOcpp::Ocpp16::DiagnosticsStatusNotification; - -DiagnosticsStatusNotification::DiagnosticsStatusNotification() { - if (defaultOcppEngine && defaultOcppEngine->getOcppModel().getDiagnosticsService()) { - auto diagnosticsService = defaultOcppEngine->getOcppModel().getDiagnosticsService(); - status = diagnosticsService->getDiagnosticsStatus(); - } else { - status = DiagnosticsStatus::Idle; - } -} - -DiagnosticsStatusNotification::DiagnosticsStatusNotification(DiagnosticsStatus status) : status(status) { - -} - -const char *DiagnosticsStatusNotification::cstrFromStatus(DiagnosticsStatus status) { - switch (status) { - case (DiagnosticsStatus::Idle): - return "Idle"; - break; - case (DiagnosticsStatus::Uploaded): - return "Uploaded"; - break; - case (DiagnosticsStatus::UploadFailed): - return "UploadFailed"; - break; - case (DiagnosticsStatus::Uploading): - return "Uploading"; - break; - } - return nullptr; //cannot be reached -} - -std::unique_ptr DiagnosticsStatusNotification::createReq() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - payload["status"] = cstrFromStatus(status); - return doc; -} - -void DiagnosticsStatusNotification::processConf(JsonObject payload){ - // no payload, nothing to do -} diff --git a/src/ArduinoOcpp/MessagesV16/DiagnosticsStatusNotification.h b/src/ArduinoOcpp/MessagesV16/DiagnosticsStatusNotification.h deleted file mode 100644 index abf7db09..00000000 --- a/src/ArduinoOcpp/MessagesV16/DiagnosticsStatusNotification.h +++ /dev/null @@ -1,34 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include - -#ifndef DIAGNOSTICSSTATUSNOTIFICATION_H -#define DIAGNOSTICSSTATUSNOTIFICATION_H - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class DiagnosticsStatusNotification : public OcppMessage { -private: - DiagnosticsStatus status; - static const char *cstrFromStatus(DiagnosticsStatus status); -public: - DiagnosticsStatusNotification(); - - DiagnosticsStatusNotification(DiagnosticsStatus status); - - const char* getOcppOperationType() {return "DiagnosticsStatusNotification"; } - - std::unique_ptr createReq(); - - void processConf(JsonObject payload); - -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp - -#endif diff --git a/src/ArduinoOcpp/MessagesV16/FirmwareStatusNotification.h b/src/ArduinoOcpp/MessagesV16/FirmwareStatusNotification.h deleted file mode 100644 index 7f2a882f..00000000 --- a/src/ArduinoOcpp/MessagesV16/FirmwareStatusNotification.h +++ /dev/null @@ -1,35 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include - -#include - -#ifndef FIRMWARESTATUSNOTIFICATION_H -#define FIRMWARESTATUSNOTIFICATION_H - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class FirmwareStatusNotification : public OcppMessage { -private: - FirmwareStatus status; - static const char *cstrFromFwStatus(FirmwareStatus status); -public: - FirmwareStatusNotification(); - - FirmwareStatusNotification(FirmwareStatus status); - - const char* getOcppOperationType() {return "FirmwareStatusNotification"; } - - std::unique_ptr createReq(); - - void processConf(JsonObject payload); - -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp - -#endif diff --git a/src/ArduinoOcpp/MessagesV16/GetCompositeSchedule.cpp b/src/ArduinoOcpp/MessagesV16/GetCompositeSchedule.cpp deleted file mode 100644 index b45620ed..00000000 --- a/src/ArduinoOcpp/MessagesV16/GetCompositeSchedule.cpp +++ /dev/null @@ -1,98 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include - -#include - -using ArduinoOcpp::Ocpp16::GetCompositeSchedule; - -GetCompositeSchedule::GetCompositeSchedule() { - -} - -const char* GetCompositeSchedule::getOcppOperationType(){ - return "GetCompositeSchedule"; -} - -void GetCompositeSchedule::processReq(JsonObject payload) { - - connectorId = payload["connectorId"] | -1; - duration = payload["duration"] | 0; - auto unitString = payload["chargingRateUnit"] | "W"; - - if (unitString[0] == 'A' || unitString[0] == 'a') { - chargingRateUnit = ChargingRateUnitType::Amp; - } else if (unitString[0] == 'W' || unitString[0] == 'w') { - chargingRateUnit = ChargingRateUnitType::Watt; - } else { - errorCode = "PropertyConstraintViolation"; - } - - if (ocppModel && ocppModel->getChargePointStatusService()) { - if (connectorId >= ocppModel->getChargePointStatusService()->getNumConnectors()) { - errorCode = "PropertyConstraintViolation"; - } - } - - if (connectorId < 0 || !payload.containsKey("duration")) { - errorCode = "FormationViolation"; - } - - if (!ocppModel || !ocppModel->getSmartChargingService()) { - AO_DBG_ERR("SmartChargingService not initialized! Ignore request"); - errorCode = "NotSupported"; - } -} - -std::unique_ptr GetCompositeSchedule::createConf(){ - if (!ocppModel || !ocppModel->getSmartChargingService()) { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - payload["status"] = "Rejected"; - return doc; - } - - auto scService = ocppModel->getSmartChargingService(); - ChargingSchedule *composite = scService->getCompositeSchedule(connectorId, duration); - DynamicJsonDocument *compositeJson {nullptr}; - - if (composite) { - compositeJson = composite->toJsonDocument(); - } - - std::unique_ptr doc; - - if (compositeJson) { - doc.reset(new DynamicJsonDocument(JSON_OBJECT_SIZE(4) + JSONDATE_LENGTH + 1 + compositeJson->capacity())); - JsonObject payload = doc->to(); - payload["status"] = "Accepted"; - if (connectorId > 0) - payload["connectorId"] = connectorId; - - char scheduleStart [JSONDATE_LENGTH + 1] {'\0'}; - auto startSchedule = (*compositeJson)["startSchedule"] | ""; - if (startSchedule[0] != '\0') { - strncpy(scheduleStart, startSchedule, JSONDATE_LENGTH + 1); - } else { - ocppModel->getOcppTime().getOcppTimestampNow().toJsonString(scheduleStart, JSONDATE_LENGTH + 1); - } - payload["scheduleStart"] = scheduleStart; - - payload["chargingSchedule"] = *compositeJson; - } else { - doc.reset(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - payload["status"] = "Rejected"; - } - - delete compositeJson; - delete composite; - - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/GetCompositeSchedule.h b/src/ArduinoOcpp/MessagesV16/GetCompositeSchedule.h deleted file mode 100644 index 00482796..00000000 --- a/src/ArduinoOcpp/MessagesV16/GetCompositeSchedule.h +++ /dev/null @@ -1,36 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef GETCOMPOSITESCHEDULE_H -#define GETCOMPOSITESCHEDULE_H - -#include -#include -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class GetCompositeSchedule : public OcppMessage { -private: - int connectorId {-1}; - otime_t duration {0}; - ChargingRateUnitType chargingRateUnit {ChargingRateUnitType::Watt}; - - const char *errorCode {nullptr}; -public: - GetCompositeSchedule(); - - const char* getOcppOperationType(); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); - - const char *getErrorCode() {return errorCode;} -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/GetConfiguration.cpp b/src/ArduinoOcpp/MessagesV16/GetConfiguration.cpp deleted file mode 100644 index 3989137a..00000000 --- a/src/ArduinoOcpp/MessagesV16/GetConfiguration.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include - -using ArduinoOcpp::Ocpp16::GetConfiguration; - -GetConfiguration::GetConfiguration() { - -} - -const char* GetConfiguration::getOcppOperationType(){ - return "GetConfiguration"; -} - -void GetConfiguration::processReq(JsonObject payload) { - - JsonArray requestedKeys = payload["key"]; - for (size_t i = 0; i < requestedKeys.size(); i++) { - keys.push_back(requestedKeys[i].as()); - } -} - -std::unique_ptr GetConfiguration::createConf(){ - - std::unique_ptr>> configurationKeys; - std::vector unknownKeys; - - if (keys.size() == 0){ //return all existing keys - configurationKeys = getAllConfigurations(); - } else { //only return keys that were searched using the "key" parameter - configurationKeys = std::unique_ptr>>( - new std::vector>() - ); - for (size_t i = 0; i < keys.size(); i++) { - std::shared_ptr entry = getConfiguration(keys.at(i).c_str()); - if (entry) - configurationKeys->push_back(entry); - else - unknownKeys.push_back(keys.at(i).c_str()); - } - } - - size_t capacity = 0; - std::vector> configurationKeysJson; - - for (auto confKey = configurationKeys->begin(); confKey != configurationKeys->end(); confKey++) { - std::shared_ptr entry = (*confKey)->toJsonOcppMsgEntry(); - if (entry) { - configurationKeysJson.push_back(entry); - capacity += entry->capacity(); - } - } - - for (auto unknownKey = unknownKeys.begin(); unknownKey != unknownKeys.end(); unknownKey++) { - capacity += unknownKey->length() + 1; - } - - capacity += JSON_OBJECT_SIZE(2) //configurationKey, unknownKey - + JSON_ARRAY_SIZE(configurationKeysJson.size()) - + JSON_ARRAY_SIZE(unknownKeys.size()); - - auto doc = std::unique_ptr(new DynamicJsonDocument(capacity)); - - JsonObject payload = doc->to(); - - JsonArray jsonConfigurationKey = payload.createNestedArray("configurationKey"); - for (size_t i = 0; i < configurationKeys->size(); i++) { - jsonConfigurationKey.add(configurationKeysJson.at(i)->as()); - } - - if (unknownKeys.size() > 0) { - JsonArray jsonUnknownKey = payload.createNestedArray("unknownKey"); - for (auto unknownKey = unknownKeys.begin(); unknownKey != unknownKeys.end(); unknownKey++) { - AO_DBG_DEBUG("Unknown key: %s", unknownKey->c_str()) - jsonUnknownKey.add(*unknownKey); - } - } - - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/GetConfiguration.h b/src/ArduinoOcpp/MessagesV16/GetConfiguration.h deleted file mode 100644 index 3cfc0127..00000000 --- a/src/ArduinoOcpp/MessagesV16/GetConfiguration.h +++ /dev/null @@ -1,31 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef GETCONFIGURATION_H -#define GETCONFIGURATION_H - -#include - -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class GetConfiguration : public OcppMessage { -private: - std::vector keys; -public: - GetConfiguration(); - - const char* getOcppOperationType(); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); - -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/GetDiagnostics.cpp b/src/ArduinoOcpp/MessagesV16/GetDiagnostics.cpp deleted file mode 100644 index 6dd5fd72..00000000 --- a/src/ArduinoOcpp/MessagesV16/GetDiagnostics.cpp +++ /dev/null @@ -1,71 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include - -using ArduinoOcpp::Ocpp16::GetDiagnostics; - -GetDiagnostics::GetDiagnostics() { - -} - -void GetDiagnostics::processReq(JsonObject payload) { - /* - * Process the application data here. Note: you have to implement the FW update procedure in your client code. You have to set - * a onSendConfListener in which you initiate a FW update (e.g. calling ESPhttpUpdate.update(...) ) - */ - - const char *loc = payload["location"] | ""; - location = loc; - //check location URL. Maybe introduce Same-Origin-Policy? - if (location.empty()) { - formatError = true; - AO_DBG_WARN("Could not read location. Abort"); - return; - } - - retries = payload["retries"] | 1; - retryInterval = payload["retryInterval"] | 180; - - //check the integrity of startTime - if (payload.containsKey("startTime")) { - const char *startTimeRaw = payload["startTime"] | "Invalid"; - if (!startTime.setTime(startTimeRaw)) { - formatError = true; - AO_DBG_WARN("Could not read startTime. Abort"); - return; - } - } - - //check the integrity of stopTime - if (payload.containsKey("startTime")) { - const char *stopTimeRaw = payload["stopTime"] | "Invalid"; - if (!stopTime.setTime(stopTimeRaw)) { - formatError = true; - AO_DBG_WARN("Could not read stopTime. Abort"); - return; - } - } -} - -std::unique_ptr GetDiagnostics::createConf(){ - if (ocppModel && ocppModel->getDiagnosticsService()) { - fileName = ocppModel->getDiagnosticsService()->requestDiagnosticsUpload(location, retries, retryInterval, startTime, stopTime); - } else { - AO_DBG_WARN("DiagnosticsService has not been initialized before! Please have a look at ArduinoOcpp.cpp for an example. Abort"); - return createEmptyDocument(); - } - - if (fileName.empty()) { - return createEmptyDocument(); - } else { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1) + fileName.length() + 1)); - JsonObject payload = doc->to(); - payload["fileName"] = fileName; - return doc; - } -} diff --git a/src/ArduinoOcpp/MessagesV16/GetDiagnostics.h b/src/ArduinoOcpp/MessagesV16/GetDiagnostics.h deleted file mode 100644 index 52440d7c..00000000 --- a/src/ArduinoOcpp/MessagesV16/GetDiagnostics.h +++ /dev/null @@ -1,40 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef GETDIAGNOSTICS_H -#define GETDIAGNOSTICS_H - -#include -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class GetDiagnostics : public OcppMessage { -private: - std::string location {}; - int retries = 1; - unsigned long retryInterval = 180; - OcppTimestamp startTime = OcppTimestamp(); - OcppTimestamp stopTime = OcppTimestamp(); - - std::string fileName {}; - - bool formatError = false; -public: - GetDiagnostics(); - - const char* getOcppOperationType() {return "GetDiagnostics";} - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); - - const char *getErrorCode() {return formatError ? "FormationViolation" : nullptr;} -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp - -#endif diff --git a/src/ArduinoOcpp/MessagesV16/Heartbeat.cpp b/src/ArduinoOcpp/MessagesV16/Heartbeat.cpp deleted file mode 100644 index d019079c..00000000 --- a/src/ArduinoOcpp/MessagesV16/Heartbeat.cpp +++ /dev/null @@ -1,67 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include - -using ArduinoOcpp::Ocpp16::Heartbeat; - -Heartbeat::Heartbeat() { - -} - -const char* Heartbeat::getOcppOperationType(){ - return "Heartbeat"; -} - -std::unique_ptr Heartbeat::createReq() { - return createEmptyDocument(); -} - -void Heartbeat::processConf(JsonObject payload) { - - const char* currentTime = payload["currentTime"] | "Invalid"; - if (strcmp(currentTime, "Invalid")) { - if (ocppModel && ocppModel->getOcppTime().setOcppTime(currentTime)) { - //success - AO_DBG_DEBUG("Request has been accepted"); - } else { - AO_DBG_WARN("Could not read time string. Expect format like 2020-02-01T20:53:32.486Z"); - } - } else { - AO_DBG_WARN("Missing field currentTime. Expect format like 2020-02-01T20:53:32.486Z"); - } -} - -void Heartbeat::processReq(JsonObject payload) { - - /** - * Ignore Contents of this Req-message, because this is for debug purposes only - */ - -} - -std::unique_ptr Heartbeat::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(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 - OcppTimestamp ocppTimeReference = OcppTimestamp(2019,10,0,11,59,55); - OcppTimestamp ocppSelect = ocppTimeReference; - if (ocppModel) { - auto& ocppNow = ocppModel->getOcppTime().getOcppTimestampNow(); - if (ocppNow > ocppTimeReference) { - //time has already been set - ocppSelect = ocppNow; - } - } - - char ocppNowJson [JSONDATE_LENGTH + 1] = {'\0'}; - ocppSelect.toJsonString(ocppNowJson, JSONDATE_LENGTH + 1); - payload["currentTime"] = ocppNowJson; - - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/Heartbeat.h b/src/ArduinoOcpp/MessagesV16/Heartbeat.h deleted file mode 100644 index cfde53c7..00000000 --- a/src/ArduinoOcpp/MessagesV16/Heartbeat.h +++ /dev/null @@ -1,30 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef HEARTBEAT_H -#define HEARTBEAT_H - -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class Heartbeat : public OcppMessage { -public: - Heartbeat(); - - const char* getOcppOperationType(); - - std::unique_ptr createReq(); - - void processConf(JsonObject payload); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/MeterValues.cpp b/src/ArduinoOcpp/MessagesV16/MeterValues.cpp deleted file mode 100644 index 6af25835..00000000 --- a/src/ArduinoOcpp/MessagesV16/MeterValues.cpp +++ /dev/null @@ -1,88 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include - -using ArduinoOcpp::Ocpp16::MeterValues; - -#define ENERGY_METER_TIMEOUT_MS 30 * 1000 //after waiting for 30s, send MeterValues without missing readings - -//can only be used for echo server debugging -MeterValues::MeterValues() { - -} - -MeterValues::MeterValues(std::vector>&& meterValue, unsigned int connectorId, std::shared_ptr transaction) - : meterValue{std::move(meterValue)}, connectorId{connectorId}, transaction{transaction} { - -} - -MeterValues::~MeterValues(){ - -} - -const char* MeterValues::getOcppOperationType(){ - return "MeterValues"; -} - -void MeterValues::initiate() { - -} - -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)); - } else { - AO_DBG_ERR("Energy meter reading not convertible to JSON"); - (void)0; - } - } - - capacity += JSON_OBJECT_SIZE(3); - capacity += JSON_ARRAY_SIZE(entries.size()); - - auto doc = std::unique_ptr(new DynamicJsonDocument(capacity + 100)); //TODO remove safety space - auto payload = doc->to(); - payload["connectorId"] = connectorId; - - if (transaction && !transaction->isSilent()) { //add txId if MVs are assigned to a tx with txId - payload["transactionId"] = transaction->getTransactionId(); - } - - auto meterValueJson = payload.createNestedArray("meterValue"); - for (auto entry = entries.begin(); entry != entries.end(); entry++) { - meterValueJson.add(**entry); - } - - return doc; -} - -void MeterValues::processConf(JsonObject payload) { - AO_DBG_DEBUG("Request has been confirmed"); -} - - -void MeterValues::processReq(JsonObject payload) { - - /** - * Ignore Contents of this Req-message, because this is for debug purposes only - */ - -} - -std::unique_ptr MeterValues::createConf(){ - return createEmptyDocument(); -} diff --git a/src/ArduinoOcpp/MessagesV16/MeterValues.h b/src/ArduinoOcpp/MessagesV16/MeterValues.h deleted file mode 100644 index 4c413c79..00000000 --- a/src/ArduinoOcpp/MessagesV16/MeterValues.h +++ /dev/null @@ -1,50 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef METERVALUES_H -#define METERVALUES_H - -#include -#include - -#include - -namespace ArduinoOcpp { - -class MeterValue; -class Transaction; - -namespace Ocpp16 { - -class MeterValues : public OcppMessage { -private: - std::vector> meterValue; - - unsigned int connectorId = 0; - - std::shared_ptr transaction; - -public: - MeterValues(std::vector>&& meterValue, unsigned int connectorId, std::shared_ptr transaction = nullptr); - - MeterValues(); //for debugging only. Make this for the server pendant - - ~MeterValues(); - - const char* getOcppOperationType(); - - void initiate() override; - - std::unique_ptr createReq(); - - void processConf(JsonObject payload); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/RemoteStartTransaction.cpp b/src/ArduinoOcpp/MessagesV16/RemoteStartTransaction.cpp deleted file mode 100644 index 864035d0..00000000 --- a/src/ArduinoOcpp/MessagesV16/RemoteStartTransaction.cpp +++ /dev/null @@ -1,140 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - - -#include -#include -#include -#include -#include -#include - -using ArduinoOcpp::Ocpp16::RemoteStartTransaction; - -RemoteStartTransaction::RemoteStartTransaction() { - -} - -const char* RemoteStartTransaction::getOcppOperationType() { - return "RemoteStartTransaction"; -} - -void RemoteStartTransaction::processReq(JsonObject payload) { - connectorId = payload["connectorId"] | -1; - - const char *idTagIn = payload["idTag"] | ""; - size_t len = strnlen(idTagIn, IDTAG_LEN_MAX + 2); - if (len <= IDTAG_LEN_MAX) { - snprintf(idTag, IDTAG_LEN_MAX + 1, "%s", idTagIn); - } - - if (*idTag == '\0') { - AO_DBG_WARN("idTag format violation"); - errorCode = "FormationViolation"; - } - - if (payload.containsKey("chargingProfile")) { - AO_DBG_INFO("Setting Charging profile via RemoteStartTransaction"); - - JsonObject chargingProfile = payload["chargingProfile"]; - if ((chargingProfile["chargingProfileId"] | -1) < 0) { - AO_DBG_WARN("RemoteStartTx profile requires non-negative chargingProfileId"); - errorCode = chargingProfile.containsKey("chargingProfileId") ? - "PropertyConstraintViolation" : "FormationViolation"; - } - chargingProfileDoc = DynamicJsonDocument(chargingProfile.memoryUsage()); //copy TxProfile - chargingProfileDoc.set(chargingProfile); - } -} - -std::unique_ptr RemoteStartTransaction::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - - bool canStartTransaction = false; - if (connectorId >= 1) { - //connectorId specified for given connector, try to start Transaction here - if (ocppModel && ocppModel->getConnectorStatus(connectorId)){ - auto connector = ocppModel->getConnectorStatus(connectorId); - if (connector->getTransactionId() < 0 && - connector->getAvailability() == AVAILABILITY_OPERATIVE && - connector->getSessionIdTag() == nullptr) { - canStartTransaction = true; - } - } - } else { - //connectorId not specified. Find free connector - if (ocppModel && ocppModel->getChargePointStatusService()) { - auto cpStatusService = ocppModel->getChargePointStatusService(); - for (int i = 1; i < cpStatusService->getNumConnectors(); i++) { - auto connIter = cpStatusService->getConnector(i); - if (connIter->getTransactionId() < 0 && - connIter->getAvailability() == AVAILABILITY_OPERATIVE && - connIter->getSessionIdTag() == nullptr) { - canStartTransaction = true; - connectorId = i; - break; - } - } - } - } - - if (canStartTransaction) { - - auto sRmtProfileId = declareConfiguration("AO_SRMTPROFILEID_CONN_1", -1, CONFIGURATION_FN, false, false, true, false); - - if (ocppModel && ocppModel->getSmartChargingService()) { - auto scService = ocppModel->getSmartChargingService(); - - if (*sRmtProfileId >= 0) { - int clearProfileId = *sRmtProfileId; - bool ret = scService->clearChargingProfile([clearProfileId](int id, int, ChargingProfilePurposeType, int) { - return id == clearProfileId; - }); - (void)ret; - - *sRmtProfileId = -1; - AO_DBG_DEBUG("Cleared Charging Profile from previous RemoteStartTx: %s", ret ? "success" : "already cleared"); - configuration_save(); - } - } - - if (ocppModel && ocppModel->getConnectorStatus(connectorId)) { - auto connector = ocppModel->getConnectorStatus(connectorId); - - connector->beginSession(idTag); - } - - if (!chargingProfileDoc.isNull() - && (ocppModel && ocppModel->getSmartChargingService())) { - auto scService = ocppModel->getSmartChargingService(); - - JsonObject chargingProfile = chargingProfileDoc.as(); - scService->setChargingProfile(chargingProfile); - *sRmtProfileId = chargingProfile["chargingProfileId"].as(); - AO_DBG_DEBUG("Charging Profile from RemoteStartTx set"); - configuration_save(); - } - - payload["status"] = "Accepted"; - } else { - AO_DBG_INFO("No connector to start transaction"); - payload["status"] = "Rejected"; - } - - return doc; -} - -std::unique_ptr RemoteStartTransaction::createReq() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - - payload["idTag"] = "A0-00-00-00"; - - return doc; -} - -void RemoteStartTransaction::processConf(JsonObject payload){ - -} diff --git a/src/ArduinoOcpp/MessagesV16/RemoteStartTransaction.h b/src/ArduinoOcpp/MessagesV16/RemoteStartTransaction.h deleted file mode 100644 index f3d077b6..00000000 --- a/src/ArduinoOcpp/MessagesV16/RemoteStartTransaction.h +++ /dev/null @@ -1,39 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef REMOTESTARTTRANSACTION_H -#define REMOTESTARTTRANSACTION_H - -#include -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class RemoteStartTransaction : public OcppMessage { -private: - int connectorId; - char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; - DynamicJsonDocument chargingProfileDoc {0}; - - const char *errorCode {nullptr}; -public: - RemoteStartTransaction(); - - const char* getOcppOperationType(); - - std::unique_ptr createReq(); - - void processConf(JsonObject payload); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); - - const char *getErrorCode() {return errorCode;} -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/RemoteStopTransaction.cpp b/src/ArduinoOcpp/MessagesV16/RemoteStopTransaction.cpp deleted file mode 100644 index 461398b8..00000000 --- a/src/ArduinoOcpp/MessagesV16/RemoteStopTransaction.cpp +++ /dev/null @@ -1,47 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include - -using ArduinoOcpp::Ocpp16::RemoteStopTransaction; - -RemoteStopTransaction::RemoteStopTransaction() { - -} - -const char* RemoteStopTransaction::getOcppOperationType(){ - return "RemoteStopTransaction"; -} - -void RemoteStopTransaction::processReq(JsonObject payload) { - transactionId = payload["transactionId"] | -1; -} - -std::unique_ptr RemoteStopTransaction::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - - bool canStopTransaction = false; - - if (ocppModel && ocppModel->getChargePointStatusService()) { - auto cpStatusService = ocppModel->getChargePointStatusService(); - for (int i = 0; i < cpStatusService->getNumConnectors(); i++) { - auto connIter = cpStatusService->getConnector(i); - if (connIter->getTransactionId() == transactionId) { - canStopTransaction = true; - connIter->endSession("Remote"); - } - } - } - - if (canStopTransaction){ - payload["status"] = "Accepted"; - } else { - payload["status"] = "Rejected"; - } - - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/RemoteStopTransaction.h b/src/ArduinoOcpp/MessagesV16/RemoteStopTransaction.h deleted file mode 100644 index c8a03a93..00000000 --- a/src/ArduinoOcpp/MessagesV16/RemoteStopTransaction.h +++ /dev/null @@ -1,28 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef REMOTESTOPTRANSACTION_H -#define REMOTESTOPTRANSACTION_H - -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class RemoteStopTransaction : public OcppMessage { -private: - int transactionId; -public: - RemoteStopTransaction(); - - const char* getOcppOperationType(); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/Reset.cpp b/src/ArduinoOcpp/MessagesV16/Reset.cpp deleted file mode 100644 index 4283eea5..00000000 --- a/src/ArduinoOcpp/MessagesV16/Reset.cpp +++ /dev/null @@ -1,54 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include - -using ArduinoOcpp::Ocpp16::Reset; - -Reset::Reset() { - -} - -const char* Reset::getOcppOperationType(){ - return "Reset"; -} - -void Reset::processReq(JsonObject payload) { - /* - * Process the application data here. Note: you have to implement the device reset procedure in your client code. You have to set - * a onSendConfListener in which you initiate a reset (e.g. calling ESP.reset() ) - */ - bool isHard = !strcmp(payload["type"] | "undefined", "Hard"); - - if (ocppModel && ocppModel->getChargePointStatusService()) { - auto cpsService = ocppModel->getChargePointStatusService(); - if (!cpsService->getExecuteReset()) { - AO_DBG_ERR("No reset handler set. Abort operation"); - (void)0; - //resetAccepted remains false - } else { - if (!cpsService->getPreReset() || cpsService->getPreReset()(isHard) || isHard) { - resetAccepted = true; - cpsService->initiateReset(isHard); - for (int i = 0; i < cpsService->getNumConnectors(); i++) { - auto connector = cpsService->getConnector(i); - if (connector) { - connector->endSession(isHard ? "HardReset" : "SoftReset"); - } - } - } - } - } else { - resetAccepted = true; //assume that onReceiveReset is set - } -} - -std::unique_ptr Reset::createConf() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - payload["status"] = resetAccepted ? "Accepted" : "Rejected"; - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/Reset.h b/src/ArduinoOcpp/MessagesV16/Reset.h deleted file mode 100644 index 7693dddb..00000000 --- a/src/ArduinoOcpp/MessagesV16/Reset.h +++ /dev/null @@ -1,28 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef RESET_H -#define RESET_H - -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class Reset : public OcppMessage { -private: - bool resetAccepted {false}; -public: - Reset(); - - const char* getOcppOperationType(); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/SetChargingProfile.cpp b/src/ArduinoOcpp/MessagesV16/SetChargingProfile.cpp deleted file mode 100644 index 5006130a..00000000 --- a/src/ArduinoOcpp/MessagesV16/SetChargingProfile.cpp +++ /dev/null @@ -1,61 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include - -using ArduinoOcpp::Ocpp16::SetChargingProfile; - -SetChargingProfile::SetChargingProfile() { - -} - -SetChargingProfile::SetChargingProfile(std::unique_ptr payloadToClient) - : payloadToClient{std::move(payloadToClient)} { - -} - -SetChargingProfile::~SetChargingProfile() { - -} - -const char* SetChargingProfile::getOcppOperationType(){ - return "SetChargingProfile"; -} - -void SetChargingProfile::processReq(JsonObject payload) { - - //int connectorID = payload["connectorId"]; - - JsonObject csChargingProfiles = payload["csChargingProfiles"]; - - if (ocppModel && ocppModel->getSmartChargingService()) { - auto smartChargingService = ocppModel->getSmartChargingService(); - smartChargingService->setChargingProfile(csChargingProfiles); - } -} - -std::unique_ptr SetChargingProfile::createConf(){ //TODO review - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - payload["status"] = "Accepted"; - return doc; -} - -std::unique_ptr SetChargingProfile::createReq() { - if (payloadToClient != nullptr) { - auto result = std::unique_ptr(new DynamicJsonDocument(*payloadToClient)); - return result; - } - return nullptr; -} - -void SetChargingProfile::processConf(JsonObject payload) { - const char* status = payload["status"] | "Invalid"; - if (strcmp(status, "Accepted")) { - AO_DBG_WARN("Send profile: rejected by client"); - } -} diff --git a/src/ArduinoOcpp/MessagesV16/SetChargingProfile.h b/src/ArduinoOcpp/MessagesV16/SetChargingProfile.h deleted file mode 100644 index 1ab388e7..00000000 --- a/src/ArduinoOcpp/MessagesV16/SetChargingProfile.h +++ /dev/null @@ -1,36 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef SETCHARGINGPROFILE_H -#define SETCHARGINGPROFILE_H - -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class SetChargingProfile : public OcppMessage { -private: - std::unique_ptr payloadToClient; -public: - SetChargingProfile(); - - SetChargingProfile(std::unique_ptr payloadToClient); - - ~SetChargingProfile(); - - const char* getOcppOperationType(); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); - - std::unique_ptr createReq(); - - void processConf(JsonObject payload); -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/StartTransaction.cpp b/src/ArduinoOcpp/MessagesV16/StartTransaction.cpp deleted file mode 100644 index 31d25888..00000000 --- a/src/ArduinoOcpp/MessagesV16/StartTransaction.cpp +++ /dev/null @@ -1,176 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include -#include -#include - -using ArduinoOcpp::Ocpp16::StartTransaction; -using ArduinoOcpp::TransactionRPC; - - -StartTransaction::StartTransaction(std::shared_ptr transaction) : transaction(transaction) { - -} - -const char* StartTransaction::getOcppOperationType() { - return "StartTransaction"; -} - -void StartTransaction::initiate() { - if (ocppModel && transaction && !transaction->getStartRpcSync().isRequested()) { - //fill out tx data if not happened before - - auto meteringService = ocppModel->getMeteringService(); - if (transaction->getMeterStart() < 0 && meteringService) { - auto meterStart = meteringService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext::TransactionBegin); - if (meterStart && *meterStart) { - transaction->setMeterStart(meterStart->toInteger()); - } else { - AO_DBG_ERR("MeterStart undefined"); - } - } - - if (transaction->getStartTimestamp() <= MIN_TIME) { - transaction->setStartTimestamp(ocppModel->getOcppTime().getOcppTimestampNow()); - } - - transaction->getStartRpcSync().setRequested(); - - transaction->commit(); - } - - AO_DBG_INFO("StartTransaction initiated"); -} - -bool StartTransaction::initiate(StoredOperationHandler *opStore) { - if (!opStore || !ocppModel || !transaction) { - AO_DBG_ERR("-> legacy"); - return false; //execute legacy initiate instead - } - - auto payload = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); - (*payload)["connectorId"] = transaction->getConnectorId(); - (*payload)["txNr"] = transaction->getTxNr(); - - opStore->setPayload(std::move(payload)); - - opStore->commit(); - - transaction->getStartRpcSync().setRequested(); - - transaction->commit(); - - return true; //don't execute legacy initiate -} - -bool StartTransaction::restore(StoredOperationHandler *opStore) { - if (!ocppModel) { - AO_DBG_ERR("invalid state"); - return false; - } - - if (!opStore) { - AO_DBG_ERR("invalid argument"); - return false; - } - - auto payload = opStore->getPayload(); - if (!payload) { - AO_DBG_ERR("memory corruption"); - return false; - } - - int connectorId = (*payload)["connectorId"] | -1; - int txNr = (*payload)["txNr"] | -1; - if (connectorId < 0 || txNr < 0) { - AO_DBG_ERR("record incomplete"); - return false; - } - - auto txStore = ocppModel->getTransactionStore(); - - if (!txStore) { - AO_DBG_ERR("invalid state"); - return false; - } - - transaction = txStore->getTransaction(connectorId, txNr); - if (!transaction) { - AO_DBG_ERR("referential integrity violation"); - return false; - } - - return true; -} - -std::unique_ptr StartTransaction::createReq() { - - auto doc = std::unique_ptr(new DynamicJsonDocument( - JSON_OBJECT_SIZE(5) + - (IDTAG_LEN_MAX + 1) + - (JSONDATE_LENGTH + 1))); - - JsonObject payload = doc->to(); - - payload["connectorId"] = transaction->getConnectorId(); - - if (transaction->getIdTag() && *transaction->getIdTag()) { - payload["idTag"] = (char*) transaction->getIdTag(); - } - - if (transaction->isMeterStartDefined()) { - payload["meterStart"] = transaction->getMeterStart(); - } - - if (transaction->getStartTimestamp() > MIN_TIME) { - char timestamp[JSONDATE_LENGTH + 1] = {'\0'}; - transaction->getStartTimestamp().toJsonString(timestamp, JSONDATE_LENGTH + 1); - payload["timestamp"] = timestamp; - } - - return doc; -} - -void StartTransaction::processConf(JsonObject payload) { - - const char* idTagInfoStatus = payload["idTagInfo"]["status"] | "not specified"; - if (!strcmp(idTagInfoStatus, "Accepted")) { - AO_DBG_INFO("Request has been accepted"); - } else { - AO_DBG_INFO("Request has been denied. Reason: %s", idTagInfoStatus); - transaction->setIdTagDeauthorized(); - } - - int transactionId = payload["transactionId"] | -1; - transaction->setTransactionId(transactionId); - - transaction->getStartRpcSync().confirm(); - transaction->commit(); -} - -void StartTransaction::processReq(JsonObject payload) { - - /** - * Ignore Contents of this Req-message, because this is for debug purposes only - */ - -} - -std::unique_ptr StartTransaction::createConf() { - auto doc = std::unique_ptr(new DynamicJsonDocument(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; -} diff --git a/src/ArduinoOcpp/MessagesV16/StartTransaction.h b/src/ArduinoOcpp/MessagesV16/StartTransaction.h deleted file mode 100644 index 4b43b535..00000000 --- a/src/ArduinoOcpp/MessagesV16/StartTransaction.h +++ /dev/null @@ -1,48 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef STARTTRANSACTION_H -#define STARTTRANSACTION_H - -#include -#include -#include -#include - -namespace ArduinoOcpp { - -class Transaction; -class TransactionRPC; -class StoredOperationHandler; - -namespace Ocpp16 { - -class StartTransaction : public OcppMessage { -private: - std::shared_ptr transaction; -public: - - StartTransaction(std::shared_ptr transaction); - - StartTransaction() = default; //for debugging only. Make this for the server pendant - - const char* getOcppOperationType(); - - void initiate(); - bool initiate(StoredOperationHandler *opStore) override; - - bool restore(StoredOperationHandler *opStore) override; - - std::unique_ptr createReq(); - - void processConf(JsonObject payload); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/StatusNotification.cpp b/src/ArduinoOcpp/MessagesV16/StatusNotification.cpp deleted file mode 100644 index 9c405a33..00000000 --- a/src/ArduinoOcpp/MessagesV16/StatusNotification.cpp +++ /dev/null @@ -1,137 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include - -#include - -using ArduinoOcpp::Ocpp16::StatusNotification; - -//helper function -namespace ArduinoOcpp { -namespace Ocpp16 { -const char *cstrFromOcppEveState(OcppEvseState state) { - switch (state) { - case (OcppEvseState::Available): - return "Available"; - case (OcppEvseState::Preparing): - return "Preparing"; - case (OcppEvseState::Charging): - return "Charging"; - case (OcppEvseState::SuspendedEVSE): - return "SuspendedEVSE"; - case (OcppEvseState::SuspendedEV): - return "SuspendedEV"; - case (OcppEvseState::Finishing): - return "Finishing"; - case (OcppEvseState::Reserved): - return "Reserved"; - case (OcppEvseState::Unavailable): - return "Unavailable"; - case (OcppEvseState::Faulted): - return "Faulted"; - default: - AO_DBG_ERR("OcppEvseState not specified"); - (void)0; - /* fall through */ - case (OcppEvseState::NOT_SET): - return "NOT_SET"; - } -} -}} //end namespaces - -StatusNotification::StatusNotification(int connectorId, OcppEvseState currentStatus, const OcppTimestamp &otimestamp, const char *errorCode) - : connectorId(connectorId), currentStatus(currentStatus), otimestamp(otimestamp), errorCode(errorCode) { - - AO_DBG_INFO("New status: %s (connectorId %d)", cstrFromOcppEveState(currentStatus), connectorId); -} - -const char* StatusNotification::getOcppOperationType(){ - return "StatusNotification"; -} - -void StatusNotification::initiate() { - //set the most recent EVSE status, but only if it hasn't been specified by the constructor before - if (currentStatus != OcppEvseState::NOT_SET) { - return; - } - - if (ocppModel && ocppModel->getChargePointStatusService()) { - auto cpsService = ocppModel->getChargePointStatusService(); - if (connectorId < 0 || connectorId >= cpsService->getNumConnectors()) { - if (cpsService->getNumConnectors() == 2) { - //special case: EVSE has exactly 1 physical connector -> take status of the connector - connectorId = 1; - } else { - //generic EVSE: take status of the whole EVSE - connectorId = 0; - } - } - auto connector = cpsService->getConnector(connectorId); - if (connector) { - currentStatus = connector->inferenceStatus(); - } - } - - if (ocppModel) { - otimestamp = ocppModel->getOcppTime().getOcppTimestampNow(); - } else { - otimestamp = MIN_TIME; - } - if (currentStatus == OcppEvseState::NOT_SET) { - AO_DBG_ERR("Could not determine EVSE status"); - } -} - -//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 OcppEvseStateService will enqueue a new operation itself -std::unique_ptr StatusNotification::createReq() { - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(4) + (JSONDATE_LENGTH + 1))); - JsonObject payload = doc->to(); - - payload["connectorId"] = connectorId; - if (errorCode != nullptr) { - payload["errorCode"] = errorCode; - } else if (currentStatus == OcppEvseState::NOT_SET) { - AO_DBG_ERR("Reporting undefined status"); - payload["errorCode"] = "InternalError"; - } else { - payload["errorCode"] = "NoError"; - } - - payload["status"] = cstrFromOcppEveState(currentStatus); - - char timestamp[JSONDATE_LENGTH + 1] = {'\0'}; - otimestamp.toJsonString(timestamp, JSONDATE_LENGTH + 1); - payload["timestamp"] = timestamp; - - return doc; -} - - -void StatusNotification::processConf(JsonObject payload) { - /* - * Empty payload - */ -} - -StatusNotification::StatusNotification(int connectorId) : connectorId(connectorId) { - -} - -/* - * For debugging only - */ -void StatusNotification::processReq(JsonObject payload) { - -} - -/* - * For debugging only - */ -std::unique_ptr StatusNotification::createConf(){ - return createEmptyDocument(); -} diff --git a/src/ArduinoOcpp/MessagesV16/StatusNotification.h b/src/ArduinoOcpp/MessagesV16/StatusNotification.h deleted file mode 100644 index 5078044e..00000000 --- a/src/ArduinoOcpp/MessagesV16/StatusNotification.h +++ /dev/null @@ -1,43 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef STATUSNOTIFICATION_H -#define STATUSNOTIFICATION_H - -#include -#include -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class StatusNotification : public OcppMessage { -private: - int connectorId = 1; - OcppEvseState currentStatus = OcppEvseState::NOT_SET; - OcppTimestamp otimestamp; - const char *errorCode = nullptr; //nullptr is equivalent to "NoError" -public: - StatusNotification(int connectorId, OcppEvseState currentStatus, const OcppTimestamp &otimestamp, const char *errorCode = nullptr); - - StatusNotification(int connectorId = -1); - - const char* getOcppOperationType(); - - void initiate(); - - std::unique_ptr createReq(); - - void processConf(JsonObject payload); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); -}; - -const char *cstrFromOcppEveState(OcppEvseState state); - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/StopTransaction.cpp b/src/ArduinoOcpp/MessagesV16/StopTransaction.cpp deleted file mode 100644 index 7204f752..00000000 --- a/src/ArduinoOcpp/MessagesV16/StopTransaction.cpp +++ /dev/null @@ -1,216 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using ArduinoOcpp::Ocpp16::StopTransaction; -using ArduinoOcpp::TransactionRPC; - -StopTransaction::StopTransaction(std::shared_ptr transaction) - : transaction(transaction) { - -} - -StopTransaction::StopTransaction(std::shared_ptr transaction, std::vector> transactionData) - : transaction(transaction), transactionData(std::move(transactionData)) { - -} - -StopTransaction::StopTransaction() { } - -const char* StopTransaction::getOcppOperationType(){ - return "StopTransaction"; -} - -void StopTransaction::initiate() { - - if (ocppModel && transaction && !transaction->getStopRpcSync().isRequested()) { - //fill out tx data if not happened before - - auto meteringService = ocppModel->getMeteringService(); - if (transaction->getMeterStop() < 0 && meteringService) { - auto meterStop = meteringService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext::TransactionEnd); - if (meterStop && *meterStop) { - transaction->setMeterStop(meterStop->toInteger()); - } else { - AO_DBG_ERR("MeterStop undefined"); - } - } - - if (transaction->getStopTimestamp() <= MIN_TIME) { - transaction->setStopTimestamp(ocppModel->getOcppTime().getOcppTimestampNow()); - } - - transaction->getStopRpcSync().setRequested(); - - transaction->commit(); - } - AO_DBG_INFO("StopTransaction initiated!"); -} - -bool StopTransaction::initiate(StoredOperationHandler *opStore) { - if (!opStore || !ocppModel || !transaction) { - AO_DBG_ERR("-> legacy"); - return false; //execute legacy initiate instead - } - - auto payload = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(2))); - (*payload)["connectorId"] = transaction->getConnectorId(); - (*payload)["txNr"] = transaction->getTxNr(); - - opStore->setPayload(std::move(payload)); - - opStore->commit(); - - transaction->getStopRpcSync().setRequested(); - - transaction->commit(); - - return true; //don't execute legacy initiate -} - -bool StopTransaction::restore(StoredOperationHandler *opStore) { - if (!ocppModel) { - AO_DBG_ERR("invalid state"); - return false; - } - - if (!opStore) { - AO_DBG_ERR("invalid argument"); - return false; - } - - auto payload = opStore->getPayload(); - if (!payload) { - AO_DBG_ERR("memory corruption"); - return false; - } - - int connectorId = (*payload)["connectorId"] | -1; - int txNr = (*payload)["txNr"] | -1; - if (connectorId < 0 || txNr < 0) { - AO_DBG_ERR("record incomplete"); - return false; - } - - auto txStore = ocppModel->getTransactionStore(); - - if (!txStore) { - AO_DBG_ERR("invalid state"); - return false; - } - - transaction = txStore->getTransaction(connectorId, txNr); - if (!transaction) { - AO_DBG_ERR("referential integrity violation"); - return false; - } - - if (auto mSerivce = ocppModel->getMeteringService()) { - if (auto txData = mSerivce->getStopTxMeterData(transaction.get())) { - transactionData = txData->retrieveStopTxData(); - } - } - - return true; -} - -std::unique_ptr StopTransaction::createReq() { - - std::vector> txDataJson; - size_t txDataJson_size = 0; - for (auto mv = transactionData.begin(); mv != transactionData.end(); mv++) { - auto mvJson = (*mv)->toJson(); - if (!mvJson) { - return nullptr; - } - txDataJson_size += mvJson->capacity(); - txDataJson.emplace_back(std::move(mvJson)); - } - - DynamicJsonDocument txDataDoc = DynamicJsonDocument(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( - JSON_OBJECT_SIZE(6) + //total of 6 fields - (IDTAG_LEN_MAX + 1) + //stop idTag - (JSONDATE_LENGTH + 1) + //timestamp string - (REASON_LEN_MAX + 1) + //reason string - txDataDoc.capacity())); - JsonObject payload = doc->to(); - - if (transaction->getStopIdTag() && *transaction->getStopIdTag()) { - payload["idTag"] = (char*) transaction->getStopIdTag(); - } - - if (transaction->isMeterStopDefined()) { - payload["meterStop"] = transaction->getMeterStop(); - } - - if (transaction->getStopTimestamp() > MIN_TIME) { - char timestamp [JSONDATE_LENGTH + 1] = {'\0'}; - transaction->getStopTimestamp().toJsonString(timestamp, JSONDATE_LENGTH + 1); - payload["timestamp"] = timestamp; - } - - payload["transactionId"] = transaction->getTransactionId(); - - if (transaction->getStopReason() && *transaction->getStopReason()) { - payload["reason"] = (char*) transaction->getStopReason(); - } - - if (!transactionData.empty()) { - payload["transactionData"] = txDataDoc; - } - - return doc; -} - -void StopTransaction::processConf(JsonObject payload) { - - if (transaction) { - transaction->getStopRpcSync().confirm(); - transaction->commit(); - } - - AO_DBG_INFO("Request has been accepted!"); -} - -bool StopTransaction::processErr(const char *code, const char *description, JsonObject details) { - - if (transaction) { - transaction->getStopRpcSync().confirm(); //no retry behavior for now; consider data "arrived" at server - transaction->commit(); - } - - AO_DBG_ERR("Server error, data loss!"); - - return false; -} - -void StopTransaction::processReq(JsonObject payload) { - /** - * Ignore Contents of this Req-message, because this is for debug purposes only - */ -} - -std::unique_ptr StopTransaction::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(2 * JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - - JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); - idTagInfo["status"] = "Accepted"; - - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/StopTransaction.h b/src/ArduinoOcpp/MessagesV16/StopTransaction.h deleted file mode 100644 index 2e27ba8e..00000000 --- a/src/ArduinoOcpp/MessagesV16/StopTransaction.h +++ /dev/null @@ -1,55 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef STOPTRANSACTION_H -#define STOPTRANSACTION_H - -#include -#include -#include -#include - -namespace ArduinoOcpp { - -class SampledValue; -class MeterValue; - -class Transaction; -class TransactionRPC; - -namespace Ocpp16 { - -class StopTransaction : public OcppMessage { -private: - std::shared_ptr transaction; - std::vector> transactionData; -public: - - StopTransaction(std::shared_ptr transaction); - - StopTransaction(std::shared_ptr transaction, std::vector> transactionData); - - StopTransaction(); //for debugging only. Make this for the server pendant - - const char* getOcppOperationType() override; - - void initiate() override; - bool initiate(StoredOperationHandler *opStore) override; - - bool restore(StoredOperationHandler *opStore) override; - - std::unique_ptr createReq() override; - - void processConf(JsonObject payload) override; - - bool processErr(const char *code, const char *description, JsonObject details) override; - - void processReq(JsonObject payload) override; - - std::unique_ptr createConf() override; -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/TriggerMessage.cpp b/src/ArduinoOcpp/MessagesV16/TriggerMessage.cpp deleted file mode 100644 index 3673a589..00000000 --- a/src/ArduinoOcpp/MessagesV16/TriggerMessage.cpp +++ /dev/null @@ -1,92 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include -#include - -using ArduinoOcpp::Ocpp16::TriggerMessage; - -const char* TriggerMessage::getOcppOperationType(){ - return "TriggerMessage"; -} - -void TriggerMessage::processReq(JsonObject payload) { - - const char *requestedMessage = payload["requestedMessage"] | "Invalid"; - const int connectorId = payload["connectorId"] | -1; - - AO_DBG_INFO("Execute for message type %s, connectorId = %i", requestedMessage, connectorId); - - statusMessage = "Rejected"; - - if (!strcmp(requestedMessage, "MeterValues")) { - if (ocppModel && ocppModel->getMeteringService()) { - auto mService = ocppModel->getMeteringService(); - if (connectorId < 0) { - auto nConnectors = mService->getNumConnectors(); - for (decltype(nConnectors) i = 0; i < nConnectors; i++) { - triggeredOperations.push_back(mService->takeTriggeredMeterValues(i)); - } - } else if (connectorId < mService->getNumConnectors()) { - triggeredOperations.push_back(mService->takeTriggeredMeterValues(connectorId)); - } else { - errorCode = "PropertyConstraintViolation"; - } - } - } else if (!strcmp(requestedMessage, "StatusNotification")) { - if (ocppModel && ocppModel->getChargePointStatusService()) { - auto cpsService = ocppModel->getChargePointStatusService(); - if (connectorId < 0) { - auto nConnectors = cpsService->getNumConnectors(); - for (decltype(nConnectors) i = 0; i < nConnectors; i++) { - triggeredOperations.push_back(makeOcppOperation(requestedMessage, i)); - } - } else if (connectorId < cpsService->getNumConnectors()) { - triggeredOperations.push_back(makeOcppOperation(requestedMessage, connectorId)); - } else { - errorCode = "PropertyConstraintViolation"; - } - } - } else { - auto msg = makeOcppOperation(requestedMessage, connectorId); - if (msg) { - triggeredOperations.push_back(std::move(msg)); - } else { - statusMessage = "NotImplemented"; - } - } - - if (!triggeredOperations.empty()) { - statusMessage = "Accepted"; - } else { - if (errorCode) { - AO_DBG_ERR("errorCode: %s", errorCode); - } else { - AO_DBG_WARN("TriggerMessage denied. statusMessage: %s", statusMessage); - } - } - -} - -std::unique_ptr TriggerMessage::createConf(){ - auto doc = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(1))); - JsonObject payload = doc->to(); - - payload["status"] = statusMessage; - - if (defaultOcppEngine) { - auto op = triggeredOperations.begin(); - while (op != triggeredOperations.end()) { - defaultOcppEngine->initiateOperation(std::move(triggeredOperations.front())); - op = triggeredOperations.erase(op); - } - } - - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/TriggerMessage.h b/src/ArduinoOcpp/MessagesV16/TriggerMessage.h deleted file mode 100644 index eb6128aa..00000000 --- a/src/ArduinoOcpp/MessagesV16/TriggerMessage.h +++ /dev/null @@ -1,38 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef TRIGGERMESSAGE_H -#define TRIGGERMESSAGE_H - -#include - -#include - -namespace ArduinoOcpp { - -class OcppOperation; - -namespace Ocpp16 { - -class TriggerMessage : public OcppMessage { -private: - std::vector> triggeredOperations; - const char *statusMessage {nullptr}; - - const char *errorCode = nullptr; -public: - TriggerMessage() = default; - - const char* getOcppOperationType(); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); - - const char *getErrorCode() {return errorCode;} -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/UnlockConnector.cpp b/src/ArduinoOcpp/MessagesV16/UnlockConnector.cpp deleted file mode 100644 index ed6a338d..00000000 --- a/src/ArduinoOcpp/MessagesV16/UnlockConnector.cpp +++ /dev/null @@ -1,69 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include - -using ArduinoOcpp::Ocpp16::UnlockConnector; - -#define AO_UNLOCK_TIMEOUT 10000 - -UnlockConnector::UnlockConnector() { - -} - -const char* UnlockConnector::getOcppOperationType(){ - return "UnlockConnector"; -} - -void UnlockConnector::processReq(JsonObject payload) { - - auto connectorId = payload["connectorId"] | -1; - - if (!ocppModel || !ocppModel->getConnectorStatus(connectorId)) { - err = true; - return; - } - - auto connector = ocppModel->getConnectorStatus(connectorId); - - connector->endSession("UnlockCommand"); - - unlockConnector = connector->getOnUnlockConnector(); - if (unlockConnector != nullptr) { - cbUnlockResult = unlockConnector(); - } else { - AO_DBG_WARN("Unlock CB undefined"); - } - - timerStart = ao_tick_ms(); -} - -std::unique_ptr UnlockConnector::createConf() { - if (!err && ao_tick_ms() - timerStart < AO_UNLOCK_TIMEOUT) { - //do poll and if more time is needed, delay creation of conf msg - - if (unlockConnector) { - if (!cbUnlockResult) { - cbUnlockResult = unlockConnector(); - if (!cbUnlockResult) { - return nullptr; //no result yet - delay confirmation response - } - } - } - } - - auto doc = std::unique_ptr(new DynamicJsonDocument(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"; - } - return doc; -} diff --git a/src/ArduinoOcpp/MessagesV16/UnlockConnector.h b/src/ArduinoOcpp/MessagesV16/UnlockConnector.h deleted file mode 100644 index f5625b9d..00000000 --- a/src/ArduinoOcpp/MessagesV16/UnlockConnector.h +++ /dev/null @@ -1,33 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef UNLOCKCONNECTOR_H -#define UNLOCKCONNECTOR_H - -#include -#include -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class UnlockConnector : public OcppMessage { -private: - bool err = false; - std::function ()> unlockConnector; - PollResult cbUnlockResult; - unsigned long timerStart = 0; //for timeout -public: - UnlockConnector(); - - const char* getOcppOperationType(); - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/MessagesV16/UpdateFirmware.cpp b/src/ArduinoOcpp/MessagesV16/UpdateFirmware.cpp deleted file mode 100644 index 078f73b4..00000000 --- a/src/ArduinoOcpp/MessagesV16/UpdateFirmware.cpp +++ /dev/null @@ -1,52 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include - -using ArduinoOcpp::Ocpp16::UpdateFirmware; - -UpdateFirmware::UpdateFirmware() { - -} - -void UpdateFirmware::processReq(JsonObject payload) { - /* - * Process the application data here. Note: you have to implement the FW update procedure in your client code. You have to set - * a onSendConfListener in which you initiate a FW update (e.g. calling ESPhttpUpdate.update(...) ) - */ - - const char *loc = payload["location"] | ""; - location = loc; - //check location URL. Maybe introduce Same-Origin-Policy? - if (location.empty()) { - formatError = true; - AO_DBG_WARN("Could not read location. Abort"); - return; - } - - //check the integrity of retrieveDate - const char *retrieveDateRaw = payload["retrieveDate"] | "Invalid"; - if (!retreiveDate.setTime(retrieveDateRaw)) { - formatError = true; - AO_DBG_WARN("Could not read retrieveDate. Abort"); - return; - } - - retries = payload["retries"] | 1; - retryInterval = payload["retryInterval"] | 180; -} - -std::unique_ptr UpdateFirmware::createConf(){ - if (ocppModel && ocppModel->getFirmwareService()) { - auto fwService = ocppModel->getFirmwareService(); - fwService->scheduleFirmwareUpdate(location, retreiveDate, retries, retryInterval); - } else { - AO_DBG_ERR("FirmwareService has not been initialized before! Please have a look at ArduinoOcpp.cpp for an example. Abort"); - } - - return createEmptyDocument(); -} diff --git a/src/ArduinoOcpp/MessagesV16/UpdateFirmware.h b/src/ArduinoOcpp/MessagesV16/UpdateFirmware.h deleted file mode 100644 index d4841031..00000000 --- a/src/ArduinoOcpp/MessagesV16/UpdateFirmware.h +++ /dev/null @@ -1,36 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef UPDATEFIRMWARE_H -#define UPDATEFIRMWARE_H - -#include -#include - -namespace ArduinoOcpp { -namespace Ocpp16 { - -class UpdateFirmware : public OcppMessage { -private: - std::string location {}; - OcppTimestamp retreiveDate = OcppTimestamp(); - int retries = 1; - unsigned long retryInterval = 180; - bool formatError = false; -public: - UpdateFirmware(); - - const char* getOcppOperationType() {return "UpdateFirmware";} - - void processReq(JsonObject payload); - - std::unique_ptr createConf(); - - const char *getErrorCode() {if (formatError) return "FormationViolation"; else return NULL;} -}; - -} //end namespace Ocpp16 -} //end namespace ArduinoOcpp - -#endif diff --git a/src/ArduinoOcpp/Platform.cpp b/src/ArduinoOcpp/Platform.cpp deleted file mode 100644 index 463d7cd5..00000000 --- a/src/ArduinoOcpp/Platform.cpp +++ /dev/null @@ -1,61 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include - -#ifdef AO_CUSTOM_CONSOLE - -namespace ArduinoOcpp { -void (*ao_console_out_impl)(const char *msg) = nullptr; -} - -void ArduinoOcpp::ao_console_out(const char *msg) { - if (ao_console_out_impl) { - ao_console_out_impl(msg); - } -} - -void ao_set_console_out(void (*console_out)(const char *msg)) { - ArduinoOcpp::ao_console_out_impl = console_out; - console_out("[AO] console initialized\n"); -} - -#endif - -#ifdef AO_CUSTOM_TIMER -unsigned long (*ao_tick_ms_impl)() = nullptr; - -void ao_set_timer(unsigned long (*get_ms)()) { - ao_tick_ms_impl = get_ms; -} - -unsigned long ao_tick_ms_custom() { - if (ao_tick_ms_impl) { - return ao_tick_ms_impl(); - } else { - return 0; - } -} -#endif - -#if AO_PLATFORM == AO_PLATFORM_UNIX -#include - -namespace ArduinoOcpp { - -std::chrono::steady_clock::time_point clock_reference; -bool clock_initialized = false; - -} - -unsigned long ao_tick_ms_unix() { - if (!ArduinoOcpp::clock_initialized) { - ArduinoOcpp::clock_reference = std::chrono::steady_clock::now(); - ArduinoOcpp::clock_initialized = true; - } - std::chrono::milliseconds ms = std::chrono::duration_cast( - std::chrono::steady_clock::now() - ArduinoOcpp::clock_reference); - return (unsigned long) ms.count(); -} -#endif diff --git a/src/ArduinoOcpp/Platform.h b/src/ArduinoOcpp/Platform.h deleted file mode 100644 index 04fa5306..00000000 --- a/src/ArduinoOcpp/Platform.h +++ /dev/null @@ -1,99 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef AO_PLATFORM_H -#define AO_PLATFORM_H - -#ifdef __cplusplus -#define EXT_C extern "C" -#else -#define EXT_C -#endif - -#define AO_PLATFORM_ARDUINO 0 -#define AO_PLATFORM_ESPIDF 1 -#define AO_PLATFORM_UNIX 2 - -#ifndef AO_PLATFORM -#define AO_PLATFORM AO_PLATFORM_ARDUINO -#endif - -#ifdef AO_CUSTOM_CONSOLE -#include - -#ifndef AO_CUSTOM_CONSOLE_MAXMSGSIZE -#define AO_CUSTOM_CONSOLE_MAXMSGSIZE 196 -#endif - -void ao_set_console_out(void (*console_out)(const char *msg)); - -namespace ArduinoOcpp { -void ao_console_out(const char *msg); -} -#define AO_CONSOLE_PRINTF(X, ...) \ - do { \ - char msg [AO_CUSTOM_CONSOLE_MAXMSGSIZE]; \ - if (snprintf(msg, AO_CUSTOM_CONSOLE_MAXMSGSIZE, X, ##__VA_ARGS__) < 0) { \ - sprintf(msg + AO_CUSTOM_CONSOLE_MAXMSGSIZE - 7, " [...]"); \ - } \ - ArduinoOcpp::ao_console_out(msg); \ - } while (0) -#else -#define ao_set_console_out(X) \ - do { \ - X("[AO] CONSOLE ERROR: ao_set_console_out ignored if AO_CUSTOM_CONSOLE " \ - "not defined\n"); \ - char msg [100]; \ - snprintf(msg, 100, " > see %s:%i",__FILE__,__LINE__); \ - X(msg); \ - X("\n > see ArduinoOcpp/Platform.h\n"); \ - } while (0) - -#if AO_PLATFORM == AO_PLATFORM_ARDUINO -#include -#ifndef AO_USE_SERIAL -#define AO_USE_SERIAL Serial -#endif - -#define AO_CONSOLE_PRINTF(X, ...) AO_USE_SERIAL.printf_P(PSTR(X), ##__VA_ARGS__) -#elif AO_PLATFORM == AO_PLATFORM_ESPIDF || AO_PLATFORM == AO_PLATFORM_UNIX -#include - -#define AO_CONSOLE_PRINTF(X, ...) printf(X, ##__VA_ARGS__) -#endif -#endif - -#ifdef AO_CUSTOM_TIMER -void ao_set_timer(unsigned long (*get_ms)()); - -unsigned long ao_tick_ms_custom(); -#define ao_tick_ms ao_tick_ms_custom -#else - -#if AO_PLATFORM == AO_PLATFORM_ARDUINO -#include -#define ao_tick_ms millis -#elif AO_PLATFORM == AO_PLATFORM_ESPIDF -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#define ao_tick_ms(X) ((xTaskGetTickCount() * 1000UL) / configTICK_RATE_HZ) -#elif AO_PLATFORM == AO_PLATFORM_UNIX -unsigned long ao_tick_ms_unix(); -#define ao_tick_ms ao_tick_ms_unix -#endif -#endif - -#ifndef ao_avail_heap -#if AO_PLATFORM == AO_PLATFORM_ARDUINO -#include -#define ao_avail_heap ESP.getFreeHeap -#elif AO_PLATFORM == AO_PLATFORM_ESPIDF -#include "freertos/FreeRTOS.h" -#define ao_avail_heap xPortGetFreeHeapSize -#elif AO_PLATFORM == AO_PLATFORM_UNIX -#define ao_avail_heap(X) 1000000 //suppress this technique on unix -#endif -#endif - -#endif diff --git a/src/ArduinoOcpp/SimpleOcppOperationFactory.cpp b/src/ArduinoOcpp/SimpleOcppOperationFactory.cpp deleted file mode 100644 index 59a62ff1..00000000 --- a/src/ArduinoOcpp/SimpleOcppOperationFactory.cpp +++ /dev/null @@ -1,303 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -namespace ArduinoOcpp { - -class TBDeiniter { -public: - virtual ~TBDeiniter() = default; -}; - -std::vector> toBeDeinitialized; - -template -class TBDeiniterConcrete : public TBDeiniter { -private: - T& obj; -public: - TBDeiniterConcrete(T& obj) : obj(obj) { } - ~TBDeiniterConcrete() {obj = nullptr;} -}; - -template -void deinit_afterwards(T& obj) { - toBeDeinitialized.emplace_back(std::unique_ptr{new TBDeiniterConcrete{obj}}); -} - -OnReceiveReqListener onAuthorizeRequest; -void setOnAuthorizeRequestListener(OnReceiveReqListener listener){ - onAuthorizeRequest = listener; - deinit_afterwards(onAuthorizeRequest); -} - -OnReceiveReqListener onBootNotificationRequest; -void setOnBootNotificationRequestListener(OnReceiveReqListener listener){ - onBootNotificationRequest = listener; - deinit_afterwards(onBootNotificationRequest); -} - -OnReceiveReqListener onSetChargingProfileRequest; -void setOnSetChargingProfileRequestListener(OnReceiveReqListener listener){ - onSetChargingProfileRequest = listener; - deinit_afterwards(onSetChargingProfileRequest); -} - -OnReceiveReqListener onStartTransactionRequest; -void setOnStartTransactionRequestListener(OnReceiveReqListener listener){ - onStartTransactionRequest = listener; - deinit_afterwards(onStartTransactionRequest); -} - -OnReceiveReqListener onTriggerMessageRequest; -void setOnTriggerMessageRequestListener(OnReceiveReqListener listener){ - onTriggerMessageRequest = listener; - deinit_afterwards(onTriggerMessageRequest); -} - -OnReceiveReqListener onRemoteStartTransactionReceiveRequest; -void setOnRemoteStartTransactionReceiveRequestListener(OnReceiveReqListener listener) { - onRemoteStartTransactionReceiveRequest = listener; - deinit_afterwards(onRemoteStartTransactionReceiveRequest); -} - -OnSendConfListener onRemoteStartTransactionSendConf; -void setOnRemoteStartTransactionSendConfListener(OnSendConfListener listener){ - onRemoteStartTransactionSendConf = listener; - deinit_afterwards(onRemoteStartTransactionSendConf); -} - -OnReceiveReqListener onRemoteStopTransactionReceiveRequest; -void setOnRemoteStopTransactionReceiveRequestListener(OnReceiveReqListener listener){ - onRemoteStopTransactionReceiveRequest = listener; - deinit_afterwards(onRemoteStopTransactionReceiveRequest); -} - -OnSendConfListener onRemoteStopTransactionSendConf; -void setOnRemoteStopTransactionSendConfListener(OnSendConfListener listener){ - onRemoteStopTransactionSendConf = listener; - deinit_afterwards(onRemoteStopTransactionSendConf); -} - -OnSendConfListener onChangeConfigurationReceiveReq; -void setOnChangeConfigurationReceiveRequestListener(OnReceiveReqListener listener){ - onChangeConfigurationReceiveReq = listener; - deinit_afterwards(onChangeConfigurationReceiveReq); -} - -OnSendConfListener onChangeConfigurationSendConf; -void setOnChangeConfigurationSendConfListener(OnSendConfListener listener){ - onChangeConfigurationSendConf = listener; - deinit_afterwards(onChangeConfigurationSendConf); -} - -OnSendConfListener onGetConfigurationReceiveReq; -void setOnGetConfigurationReceiveReqListener(OnSendConfListener listener){ - onGetConfigurationReceiveReq = listener; - deinit_afterwards(onGetConfigurationReceiveReq); -} - -OnSendConfListener onGetConfigurationSendConf; -void setOnGetConfigurationSendConfListener(OnSendConfListener listener){ - onGetConfigurationSendConf = listener; - deinit_afterwards(onGetConfigurationSendConf); -} - -OnSendConfListener onResetReceiveReq; -void setOnResetReceiveRequestListener(OnReceiveReqListener listener) { - onResetReceiveReq = listener; - deinit_afterwards(onResetReceiveReq); -} - -OnSendConfListener onResetSendConf; -void setOnResetSendConfListener(OnSendConfListener listener){ - onResetSendConf = listener; - deinit_afterwards(onResetSendConf); -} - -OnReceiveReqListener onUpdateFirmwareReceiveReq; -void setOnUpdateFirmwareReceiveRequestListener(OnReceiveReqListener listener) { - onUpdateFirmwareReceiveReq = listener; - deinit_afterwards(onUpdateFirmwareReceiveReq); -} - -OnReceiveReqListener onMeterValuesReceiveReq; -void setOnMeterValuesReceiveRequestListener(OnReceiveReqListener listener) { - onMeterValuesReceiveReq = listener; - deinit_afterwards(onMeterValuesReceiveReq); -} - -struct CustomOcppMessageCreatorEntry { - const char *messageType; - OcppMessageCreator creator; - OnReceiveReqListener onReceiveReq; -}; - -std::vector customMessagesRegistry; -void registerCustomOcppMessage(const char *messageType, OcppMessageCreator ocppMessageCreator, OnReceiveReqListener onReceiveReq) { - customMessagesRegistry.erase(std::remove_if(customMessagesRegistry.begin(), - customMessagesRegistry.end(), - [messageType](CustomOcppMessageCreatorEntry &el) { - return !strcmp(messageType, el.messageType); - }), - customMessagesRegistry.end()); - - CustomOcppMessageCreatorEntry entry; - entry.messageType = messageType; - entry.creator = ocppMessageCreator; - entry.onReceiveReq = onReceiveReq; - - customMessagesRegistry.push_back(entry); -} - -void simpleOcppFactory_deinitialize() { - customMessagesRegistry.clear(); - toBeDeinitialized.clear(); -} - -CustomOcppMessageCreatorEntry *makeCustomOcppMessage(const char *messageType) { - for (auto it = customMessagesRegistry.begin(); it != customMessagesRegistry.end(); ++it) { - if (!strcmp(it->messageType, messageType)) { - return &(*it); - } - } - return nullptr; -} - -std::unique_ptr makeFromJson(const JsonDocument& json) { - const char* messageType = json[2]; - return makeOcppOperation(messageType); -} - -std::unique_ptr makeOcppOperation(const char *messageType, int connectorId) { - auto operation = makeOcppOperation(); - auto msg = std::unique_ptr{nullptr}; - - if (CustomOcppMessageCreatorEntry *entry = makeCustomOcppMessage(messageType)) { - msg = std::unique_ptr(entry->creator()); - operation->setOnReceiveReqListener(entry->onReceiveReq); - } else if (!strcmp(messageType, "Authorize")) { - msg = std::unique_ptr(new Ocpp16::Authorize("A0-00-00-00")); //send default idTag - operation->setOnReceiveReqListener(onAuthorizeRequest); - } else if (!strcmp(messageType, "BootNotification")) { - msg = std::unique_ptr(new Ocpp16::BootNotification()); - operation->setOnReceiveReqListener(onBootNotificationRequest); - } else if (!strcmp(messageType, "GetCompositeSchedule")) { - msg = std::unique_ptr(new Ocpp16::GetCompositeSchedule()); - } else if (!strcmp(messageType, "Heartbeat")) { - msg = std::unique_ptr(new Ocpp16::Heartbeat()); - } else if (!strcmp(messageType, "MeterValues")) { - msg = std::unique_ptr(new Ocpp16::MeterValues()); - operation->setOnReceiveReqListener(onMeterValuesReceiveReq); - } else if (!strcmp(messageType, "SetChargingProfile")) { - msg = std::unique_ptr(new Ocpp16::SetChargingProfile()); - operation->setOnReceiveReqListener(onSetChargingProfileRequest); - } else if (!strcmp(messageType, "StatusNotification")) { - msg = std::unique_ptr(new Ocpp16::StatusNotification(connectorId)); - } else if (!strcmp(messageType, "StartTransaction")) { - msg = std::unique_ptr(new Ocpp16::StartTransaction()); - operation->setOnReceiveReqListener(onStartTransactionRequest); - } else if (!strcmp(messageType, "StopTransaction")) { - msg = std::unique_ptr(new Ocpp16::StopTransaction()); - } else if (!strcmp(messageType, "TriggerMessage")) { - msg = std::unique_ptr(new Ocpp16::TriggerMessage()); - operation->setOnReceiveReqListener(onTriggerMessageRequest); - } else if (!strcmp(messageType, "RemoteStartTransaction")) { - msg = std::unique_ptr(new Ocpp16::RemoteStartTransaction()); - operation->setOnReceiveReqListener(onRemoteStartTransactionReceiveRequest); - operation->setOnSendConfListener(onRemoteStartTransactionSendConf); - } else if (!strcmp(messageType, "RemoteStopTransaction")) { - msg = std::unique_ptr(new Ocpp16::RemoteStopTransaction()); - operation->setOnReceiveReqListener(onRemoteStopTransactionReceiveRequest); - operation->setOnSendConfListener(onRemoteStopTransactionSendConf); - } else if (!strcmp(messageType, "ChangeConfiguration")) { - msg = std::unique_ptr(new Ocpp16::ChangeConfiguration()); - operation->setOnReceiveReqListener(onChangeConfigurationReceiveReq); - operation->setOnSendConfListener(onChangeConfigurationSendConf); - } else if (!strcmp(messageType, "GetConfiguration")) { - msg = std::unique_ptr(new Ocpp16::GetConfiguration()); - operation->setOnReceiveReqListener(onGetConfigurationReceiveReq); - operation->setOnSendConfListener(onGetConfigurationSendConf); - } else if (!strcmp(messageType, "Reset")) { - msg = std::unique_ptr(new Ocpp16::Reset()); - operation->setOnReceiveReqListener(onResetReceiveReq); - operation->setOnSendConfListener(onResetSendConf); - } else if (!strcmp(messageType, "UpdateFirmware")) { - msg = std::unique_ptr(new Ocpp16::UpdateFirmware()); - operation->setOnReceiveReqListener(onUpdateFirmwareReceiveReq); - } else if (!strcmp(messageType, "FirmwareStatusNotification")) { - msg = std::unique_ptr(new Ocpp16::FirmwareStatusNotification()); - } else if (!strcmp(messageType, "GetDiagnostics")) { - msg = std::unique_ptr(new Ocpp16::GetDiagnostics()); - } else if (!strcmp(messageType, "DiagnosticsStatusNotification")) { - msg = std::unique_ptr(new Ocpp16::DiagnosticsStatusNotification()); - } else if (!strcmp(messageType, "UnlockConnector")) { - msg = std::unique_ptr(new Ocpp16::UnlockConnector()); - } else if (!strcmp(messageType, "ClearChargingProfile")) { - msg = std::unique_ptr(new Ocpp16::ClearChargingProfile()); - } else if (!strcmp(messageType, "ChangeAvailability")) { - msg = std::unique_ptr(new Ocpp16::ChangeAvailability()); - } else if (!strcmp(messageType, "ClearCache")) { - msg = std::unique_ptr(new Ocpp16::ClearCache()); - } else { - AO_DBG_WARN("Operation not supported"); - msg = std::unique_ptr(new NotImplemented()); - } - - if (msg == nullptr) { - return nullptr; - } else { - operation->setOcppMessage(std::move(msg)); - return operation; - } -} - -std::unique_ptr makeOcppOperation(OcppMessage *msg){ - if (msg == nullptr) { - AO_DBG_ERR("msg is null"); - return nullptr; - } - auto operation = makeOcppOperation(); - operation->setOcppMessage(std::unique_ptr(msg)); - return operation; -} - -std::unique_ptr makeOcppOperation(){ - auto result = std::unique_ptr(new OcppOperation()); - return result; -} - -} //end namespace ArduinoOcpp diff --git a/src/ArduinoOcpp/SimpleOcppOperationFactory.h b/src/ArduinoOcpp/SimpleOcppOperationFactory.h deleted file mode 100644 index 875b90ad..00000000 --- a/src/ArduinoOcpp/SimpleOcppOperationFactory.h +++ /dev/null @@ -1,48 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef SIMPLEOCPPOPERATIONFACTORY_H -#define SIMPLEOCPPOPERATIONFACTORY_H - -#include -#include -#include -#include - -namespace ArduinoOcpp { - -using OcppMessageCreator = std::function; - -std::unique_ptr makeFromJson(const JsonDocument& request); - -std::unique_ptr makeOcppOperation(); - -std::unique_ptr makeOcppOperation(OcppMessage *msg); - -std::unique_ptr makeOcppOperation(const char *actionCode, int connectorId = -1); - -void registerCustomOcppMessage(const char *messageType, OcppMessageCreator ocppMessageCreator, OnReceiveReqListener onReceiveReq = NULL); - -void setOnAuthorizeRequestListener(OnReceiveReqListener onReceiveReq); -void setOnBootNotificationRequestListener(OnReceiveReqListener onReceiveReq); -void setOnSetChargingProfileRequestListener(OnReceiveReqListener onReceiveReq); -void setOnStartTransactionRequestListener(OnReceiveReqListener onReceiveReq); -void setOnTriggerMessageRequestListener(OnReceiveReqListener onReceiveReq); -void setOnRemoteStartTransactionReceiveRequestListener(OnReceiveReqListener onReceiveReq); -void setOnRemoteStartTransactionSendConfListener(OnSendConfListener onSendConf); -void setOnRemoteStopTransactionReceiveRequestListener(OnReceiveReqListener onReceiveReq); -void setOnRemoteStopTransactionSendConfListener(OnSendConfListener onSendConf); -void setOnChangeConfigurationReceiveRequestListener(OnReceiveReqListener onReceiveReq); -void setOnChangeConfigurationSendConfListener(OnSendConfListener onSendConf); -void setOnGetConfigurationReceiveRequestListener(OnReceiveReqListener onReceiveReq); -void setOnGetConfigurationSendConfListener(OnSendConfListener onSendConf); -void setOnResetReceiveRequestListener(OnReceiveReqListener onReceiveReq); -void setOnResetSendConfListener(OnSendConfListener onSendConf); -void setOnUpdateFirmwareReceiveRequestListener(OnReceiveReqListener onReceiveReq); -void setOnMeterValuesReceiveRequestListener(OnReceiveReqListener onReceiveReq); - -void simpleOcppFactory_deinitialize(); - -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/Tasks/ChargePointStatus/ChargePointStatusService.cpp b/src/ArduinoOcpp/Tasks/ChargePointStatus/ChargePointStatusService.cpp deleted file mode 100644 index f7328171..00000000 --- a/src/ArduinoOcpp/Tasks/ChargePointStatus/ChargePointStatusService.cpp +++ /dev/null @@ -1,173 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include - -#include - -#include -#include - -#define RESET_DELAY 15000 - -using namespace ArduinoOcpp; - -ChargePointStatusService::ChargePointStatusService(OcppEngine& context, unsigned int numConn) - : context(context) { - - for (unsigned int i = 0; i < numConn; i++) { - connectors.push_back(std::unique_ptr(new ConnectorStatus(context.getOcppModel(), i))); - } - - std::shared_ptr> numberOfConnectors = - declareConfiguration("NumberOfConnectors", numConn >= 1 ? numConn - 1 : 0, CONFIGURATION_VOLATILE, false, true, false, false); - - const char *fpId = "Core,RemoteTrigger"; - const char *fpIdCore = "Core"; - const char *fpIdRTrigger = "RemoteTrigger"; - auto fProfile = declareConfiguration("SupportedFeatureProfiles",fpId, CONFIGURATION_VOLATILE, false, true, true, false); - if (!strstr(*fProfile, fpIdCore)) { - auto fProfilePlus = std::string(*fProfile); - if (!fProfilePlus.empty() && fProfilePlus.back() != ',') - fProfilePlus += ","; - fProfilePlus += fpIdCore; - fProfile->setValue(fProfilePlus.c_str(), fProfilePlus.length() + 1); - } - if (!strstr(*fProfile, fpIdRTrigger)) { - auto fProfilePlus = std::string(*fProfile); - if (!fProfilePlus.empty() && fProfilePlus.back() != ',') - fProfilePlus += ","; - fProfilePlus += fpIdRTrigger; - fProfile->setValue(fProfilePlus.c_str(), fProfilePlus.length() + 1); - } - - resetRetries = declareConfiguration("ResetRetries", 2, CONFIGURATION_FN, true, true, false, false); - - /* - * Further configuration keys which correspond to the Core profile - */ - declareConfiguration("AuthorizeRemoteTxRequests",false,CONFIGURATION_VOLATILE,false,true,false,false); - declareConfiguration("GetConfigurationMaxKeys",30,CONFIGURATION_VOLATILE,false,true,false,false); -} - -ChargePointStatusService::~ChargePointStatusService() { - -} - -void ChargePointStatusService::loop() { - if (!booted) return; - for (auto connector = connectors.begin(); connector != connectors.end(); connector++) { - auto transactionMsg = (*connector)->loop(); - if (transactionMsg != nullptr) { - auto transactionOp = makeOcppOperation(transactionMsg); - transactionOp->setTimeout(std::unique_ptr(new SuppressedTimeout())); - context.initiateOperation(std::move(transactionOp)); - } - } - - if (outstandingResetRetries > 0 && ao_tick_ms() - t_resetRetry >= RESET_DELAY) { - t_resetRetry = ao_tick_ms(); - outstandingResetRetries--; - if (executeReset) { - AO_DBG_INFO("Reset device"); - executeReset(isHardReset); - } else { - AO_DBG_ERR("No Reset function set! Abort"); - outstandingResetRetries = 0; - } - AO_DBG_ERR("Reset device failure. %s", outstandingResetRetries == 0 ? "Abort" : "Retry"); - - if (outstandingResetRetries <= 0) { - for (auto connector = connectors.begin(); connector != connectors.end(); connector++) { - (*connector)->setAvailabilityVolatile(true); - } - } - } -} - -ConnectorStatus *ChargePointStatusService::getConnector(int connectorId) { - if (connectorId < 0 || connectorId >= (int) connectors.size()) { - AO_DBG_ERR("connectorId is out of bounds"); - return nullptr; - } - - return connectors.at(connectorId).get(); -} - -void ChargePointStatusService::boot() { - booted = true; -} - -bool ChargePointStatusService::isBooted() { - return booted; -} - -int ChargePointStatusService::getNumConnectors() { - return connectors.size(); -} - -void ChargePointStatusService::setChargePointCredentials(DynamicJsonDocument &credentials) { - if (!credentials.is()) { - AO_DBG_ERR("Payload must be JSON object"); - cpCredentials.clear(); - return; - } - auto written = serializeJson(credentials, cpCredentials); - if (written <= 2) { - AO_DBG_ERR("Could not parse CP credentials: %s", written == 2 ? "format violation" : "invalid JSON"); - cpCredentials.clear(); - return; - } - //success -} - -std::string& ChargePointStatusService::getChargePointCredentials() { - if (cpCredentials.size() <= 2) { - cpCredentials = "{}"; - } - - return cpCredentials; -} - -void ChargePointStatusService::setPreReset(std::function preReset) { - this->preReset = preReset; -} - -std::function ChargePointStatusService::getPreReset() { - return this->preReset; -} - -void ChargePointStatusService::setExecuteReset(std::function executeReset) { - this->executeReset = executeReset; -} - -std::function ChargePointStatusService::getExecuteReset() { - return this->executeReset; -} - -void ChargePointStatusService::initiateReset(bool isHard) { - isHardReset = isHard; - outstandingResetRetries = 1 + *resetRetries; //one initial try + no. of retries - if (outstandingResetRetries > 5) { - AO_DBG_ERR("no. of reset trials exceeds 5"); - outstandingResetRetries = 5; - } - t_resetRetry = ao_tick_ms(); - - for (auto connector = connectors.begin(); connector != connectors.end(); connector++) { - (*connector)->setAvailabilityVolatile(false); - } -} - -#if AO_PLATFORM == AO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) -std::function ArduinoOcpp::makeDefaultResetFn() { - return [] (bool isHard) { - AO_DBG_DEBUG("Perform ESP reset"); - ESP.restart(); - }; -} -#endif //AO_PLATFORM == AO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) diff --git a/src/ArduinoOcpp/Tasks/ChargePointStatus/ChargePointStatusService.h b/src/ArduinoOcpp/Tasks/ChargePointStatus/ChargePointStatusService.h deleted file mode 100644 index 62f902e3..00000000 --- a/src/ArduinoOcpp/Tasks/ChargePointStatus/ChargePointStatusService.h +++ /dev/null @@ -1,72 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef CHARGEPOINTSTATUSSERVICE_H -#define CHARGEPOINTSTATUSSERVICE_H - -#include -#include - -#include - -namespace ArduinoOcpp { - -class OcppEngine; - -class ChargePointStatusService { -private: - OcppEngine& context; - - std::vector> connectors; - - bool booted = false; - - std::string cpCredentials; - - std::function preReset; //true: reset is possible; false: reject reset; Await: need more time to determine - std::function executeReset; //please disconnect WebSocket (AO remains initialized), shut down device and restart with normal initialization routine; on failure reconnect WebSocket - unsigned int outstandingResetRetries = 0; //0 = do not reset device - bool isHardReset = false; - unsigned long t_resetRetry; - - std::shared_ptr> resetRetries; - -public: - ChargePointStatusService(OcppEngine& context, unsigned int numConnectors); - - ~ChargePointStatusService(); - - void loop(); - - void boot(); - bool isBooted(); - - ConnectorStatus *getConnector(int connectorId); - int getNumConnectors(); - - void setChargePointCredentials(DynamicJsonDocument &credentials); - std::string& getChargePointCredentials(); - - void setPreReset(std::function preReset); - std::function getPreReset(); - - void setExecuteReset(std::function executeReset); - std::function getExecuteReset(); - - void initiateReset(bool isHard); -}; - -} //end namespace ArduinoOcpp - -#if AO_PLATFORM == AO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) - -namespace ArduinoOcpp { - -std::function makeDefaultResetFn(); - -} - -#endif //AO_PLATFORM == AO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) - -#endif diff --git a/src/ArduinoOcpp/Tasks/ChargePointStatus/ConnectorStatus.cpp b/src/ArduinoOcpp/Tasks/ChargePointStatus/ConnectorStatus.cpp deleted file mode 100644 index 64106d23..00000000 --- a/src/ArduinoOcpp/Tasks/ChargePointStatus/ConnectorStatus.cpp +++ /dev/null @@ -1,622 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include - -#include -#include -#include -#include - -#include -#include -#include -#include - -#include - -#include - -using namespace ArduinoOcpp; -using namespace ArduinoOcpp::Ocpp16; - -ConnectorStatus::ConnectorStatus(OcppModel& context, int connectorId) - : context(context), connectorId{connectorId}, txProcess(connectorId) { - - char availabilityKey [CONF_KEYLEN_MAX + 1] = {'\0'}; - snprintf(availabilityKey, CONF_KEYLEN_MAX + 1, "AO_AVAIL_CONN_%d", connectorId); - availability = declareConfiguration(availabilityKey, AVAILABILITY_OPERATIVE, CONFIGURATION_FN, false, false, true, false); - - connectionTimeOut = declareConfiguration("ConnectionTimeOut", 30, CONFIGURATION_FN, true, true, true, false); - minimumStatusDuration = declareConfiguration("MinimumStatusDuration", 0, CONFIGURATION_FN, true, true, true, false); - stopTransactionOnInvalidId = declareConfiguration("StopTransactionOnInvalidId", true, CONFIGURATION_FN, true, true, true, false); - stopTransactionOnEVSideDisconnect = declareConfiguration("StopTransactionOnEVSideDisconnect", true, CONFIGURATION_FN, true, true, true, false); - unlockConnectorOnEVSideDisconnect = declareConfiguration("UnlockConnectorOnEVSideDisconnect", true, CONFIGURATION_FN, true, true, true, false); - localAuthorizeOffline = declareConfiguration("LocalAuthorizeOffline", false, CONFIGURATION_FN, true, true, true, false); - localPreAuthorize = declareConfiguration("LocalPreAuthorize", false, CONFIGURATION_FN, true, true, true, false); - - //if the EVSE goes offline, can it continue to charge without sending StartTx / StopTx to the server when going online again? - silentOfflineTransactions = declareConfiguration("AO_SilentOfflineTransactions", false, CONFIGURATION_FN, true, true, true, false); - - //FreeVend mode - freeVendActive = declareConfiguration("AO_FreeVendActive", false, CONFIGURATION_FN, true, true, true, false); - freeVendIdTag = declareConfiguration("AO_FreeVendIdTag", "", CONFIGURATION_FN, true, true, true, false); - - if (!availability) { - AO_DBG_ERR("Cannot declare availability"); - } - - /* - * Initialize standard EVSE behavior. - * By default, transactions are triggered by a valid IdTag (+ connected plug as soon as set) - * The default necessary steps before starting a transaction are - * - lock the connector (if handler is set) - * - instruct the tx-based meter to begin a transaction (if tx-based meter handler is set) - */ - txProcess.addTrigger([this] () -> TxTrigger { - if (transaction && transaction->isInSession() && transaction->isActive()) { - return TxTrigger::Active; - } else { - return TxTrigger::Inactive; - } - }); - txProcess.addEnableStep([this] (TxTrigger cond) -> TxEnableState { - if (onTxBasedMeterPollTx) { - return onTxBasedMeterPollTx(cond); - } - return cond == TxTrigger::Active ? TxEnableState::Active : TxEnableState::Inactive; - }); - txProcess.addEnableStep([this] (TxTrigger cond) -> TxEnableState { - if (onConnectorLockPollTx) { - return onConnectorLockPollTx(cond); - } - return cond == TxTrigger::Active ? TxEnableState::Active : TxEnableState::Inactive; - }); - - if (context.getTransactionStore()) { - transaction = context.getTransactionStore()->getLatestTransaction(connectorId); - } else { - AO_DBG_ERR("must initialize TxStore before ConnectorStatus"); - (void)0; - } -} - -OcppEvseState ConnectorStatus::inferenceStatus() { - /* - * Handle special case: This is the ConnectorStatus for the whole CP (i.e. connectorId=0) --> only states Available, Unavailable, Faulted are possible - */ - if (connectorId == 0) { - if (getErrorCode() != nullptr) { - return OcppEvseState::Faulted; - } else if (getAvailability() == AVAILABILITY_INOPERATIVE) { - return OcppEvseState::Unavailable; - } else { - return OcppEvseState::Available; - } - } - - if (getErrorCode() != nullptr) { - return OcppEvseState::Faulted; - } else if (getAvailability() == AVAILABILITY_INOPERATIVE) { - return OcppEvseState::Unavailable; - } else if (transaction && transaction->isRunning()) { - //Transaction is currently running - if (!ocppPermitsCharge() || - (connectorEnergizedSampler && !connectorEnergizedSampler())) { - return OcppEvseState::SuspendedEVSE; - } - if (evRequestsEnergySampler && !evRequestsEnergySampler()) { - return OcppEvseState::SuspendedEV; - } - return OcppEvseState::Charging; - } else if (!txProcess.existsActiveTrigger() && txProcess.getState() == TxEnableState::Inactive) { - return OcppEvseState::Available; - } else { - /* - * Either in Preparing or Finishing state. Only way to know is from previous state - */ - const auto previous = currentStatus; - if (previous == OcppEvseState::Finishing || - previous == OcppEvseState::Charging || - previous == OcppEvseState::SuspendedEV || - previous == OcppEvseState::SuspendedEVSE) { - return OcppEvseState::Finishing; - } else { - return OcppEvseState::Preparing; - } - } - - AO_DBG_VERBOSE("Cannot infere status"); - return OcppEvseState::Faulted; //internal error -} - -bool ConnectorStatus::ocppPermitsCharge() { - if (connectorId == 0) { - AO_DBG_WARN("not supported for connectorId == 0"); - return false; - } - - bool suspendDeAuthorizedIdTag = transaction && transaction->isIdTagDeauthorized(); //if idTag status is "DeAuthorized" and if charging should stop - - //check special case for DeAuthorized idTags: FreeVend mode - if (suspendDeAuthorizedIdTag && freeVendActive && *freeVendActive) { - suspendDeAuthorizedIdTag = false; - } - - return transaction && - transaction->isRunning() && - transaction->isActive() && - !getErrorCode() && - !suspendDeAuthorizedIdTag; -} - -OcppMessage *ConnectorStatus::loop() { - - if (!isTransactionRunning()) { - if (*availability == AVAILABILITY_INOPERATIVE_SCHEDULED) { - *availability = AVAILABILITY_INOPERATIVE; - configuration_save(); - } - if (availabilityVolatile == AVAILABILITY_INOPERATIVE_SCHEDULED) { - availabilityVolatile = AVAILABILITY_INOPERATIVE; - } - } - - if (transaction && transaction->isAborted()) { - //If the transaction is aborted (invalidated before started), 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 = context.getMeteringService()) { - removed &= mService->removeTxMeterData(connectorId, transaction->getTxNr()); - } - - if (removed) { - removed &= context.getTransactionStore()->remove(connectorId, transaction->getTxNr()); - } - - if (removed) { - context.getTransactionStore()->setTxEnd(connectorId, transaction->getTxNr()); //roll back creation of last tx entry - } - - AO_DBG_DEBUG("collect aborted transaction %u-%u %s", connectorId, transaction->getTxNr(), removed ? "" : "failure"); - transaction = nullptr; - } - - if (transaction && transaction->isCompleted()) { - AO_DBG_DEBUG("collect obsolete transaction %u-%u", connectorId, transaction->getTxNr()); - transaction = nullptr; - } - - txProcess.evaluateProcessSteps(); - - if (transaction) { //begin exclusively transaction-related operations - - if (connectorPluggedSampler) { - if (transaction->isRunning() && transaction->isInSession() && !connectorPluggedSampler()) { - if (!stopTransactionOnEVSideDisconnect || *stopTransactionOnEVSideDisconnect) { - AO_DBG_DEBUG("Stop Tx due to EV disconnect"); - transaction->setStopReason("EVDisconnected"); - transaction->endSession(); - transaction->commit(); - } - } - } - - if (transaction->isInSession() && - !transaction->getStartRpcSync().isRequested() && - transaction->getSessionTimestamp() > MIN_TIME && - connectionTimeOut && *connectionTimeOut > 0 && - context.getOcppTime().getOcppTimestampNow() - transaction->getSessionTimestamp() >= (otime_t) *connectionTimeOut) { - - AO_DBG_INFO("Session mngt: timeout"); - transaction->endSession(); - transaction->commit(); - } - - if (transaction->isInSession() && transaction->isIdTagDeauthorized()) { - if (!stopTransactionOnInvalidId || *stopTransactionOnInvalidId) { - AO_DBG_DEBUG("DeAuthorize session"); - transaction->setStopReason("DeAuthorized"); - transaction->endSession(); - transaction->commit(); - } - } - - auto txEnable = txProcess.getState(); - - /* - * Check conditions for start or stop transaction - */ - - if (txEnable == TxEnableState::Active) { - - if (transaction->isPreparing() && !getErrorCode()) { - //start Transaction - - AO_DBG_INFO("Session mngt: trigger StartTransaction"); - - auto meteringService = context.getMeteringService(); - if (transaction->getMeterStart() < 0 && meteringService) { - auto meterStart = meteringService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext::TransactionBegin); - if (meterStart && *meterStart) { - transaction->setMeterStart(meterStart->toInteger()); - } else { - AO_DBG_ERR("MeterStart undefined"); - } - } - - if (transaction->getStartTimestamp() <= MIN_TIME) { - transaction->setStartTimestamp(context.getOcppTime().getOcppTimestampNow()); - } - - if (transaction->isSilent()) { - AO_DBG_INFO("silent Transaction: omit StartTx"); - transaction->getStartRpcSync().setRequested(); - transaction->getStartRpcSync().confirm(); - transaction->commit(); - return nullptr; - } - - transaction->commit(); - - if (context.getMeteringService()) { - context.getMeteringService()->beginTxMeterData(transaction.get()); - } - - return new StartTransaction(transaction); - } - } else if (transaction->isRunning()) { - - if (transaction->isInSession()) { - AO_DBG_DEBUG("Tx process not active"); - transaction->endSession(); - transaction->commit(); - } - - if (txEnable == TxEnableState::Inactive) { - //stop transaction - - AO_DBG_INFO("Session mngt: trigger StopTransaction"); - - if (transaction->isSilent()) { - AO_DBG_INFO("silent Transaction: omit StopTx"); - transaction->getStopRpcSync().setRequested(); - transaction->getStopRpcSync().confirm(); - if (auto mService = context.getMeteringService()) { - mService->removeTxMeterData(connectorId, transaction->getTxNr()); - } - context.getTransactionStore()->remove(connectorId, transaction->getTxNr()); - context.getTransactionStore()->setTxEnd(connectorId, transaction->getTxNr()); - transaction = nullptr; - return nullptr; - } - - auto meteringService = context.getMeteringService(); - if (transaction->getMeterStop() < 0 && meteringService) { - auto meterStop = meteringService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext::TransactionEnd); - if (meterStop && *meterStop) { - transaction->setMeterStop(meterStop->toInteger()); - } else { - AO_DBG_ERR("MeterStop undefined"); - } - } - - if (transaction->getStopTimestamp() <= MIN_TIME) { - transaction->setStopTimestamp(context.getOcppTime().getOcppTimestampNow()); - } - - transaction->commit(); - - std::shared_ptr stopTxData; - - if (meteringService) { - stopTxData = meteringService->endTxMeterData(transaction.get()); - } - - if (stopTxData) { - return new StopTransaction(std::move(transaction), stopTxData->retrieveStopTxData()); - } else { - return new StopTransaction(std::move(transaction)); - } - } - } - } //end transaction-related operations - - //handle FreeVend mode - if (freeVendActive && *freeVendActive && connectorPluggedSampler) { - if (!freeVendTrackPlugged && connectorPluggedSampler() && !transaction) { - const char *idTag = freeVendIdTag && *freeVendIdTag ? *freeVendIdTag : ""; - if (!idTag || *idTag == '\0') { - idTag = "A0000000"; - } - AO_DBG_INFO("begin FreeVend Tx using idTag %s", idTag); - beginSession(idTag); - - if (!transaction) { - AO_DBG_ERR("could not begin FreeVend Tx"); - (void)0; - } - } - - freeVendTrackPlugged = connectorPluggedSampler(); - } - - auto inferedStatus = inferenceStatus(); - - if (inferedStatus != currentStatus) { - currentStatus = inferedStatus; - t_statusTransition = ao_tick_ms(); - AO_DBG_DEBUG("Status changed%s", *minimumStatusDuration ? ", will report delayed" : ""); - } - - if (reportedStatus != currentStatus && - (*minimumStatusDuration <= 0 || //MinimumStatusDuration disabled - ao_tick_ms() - t_statusTransition >= ((unsigned long) *minimumStatusDuration) * 1000UL)) { - reportedStatus = currentStatus; - OcppTimestamp reportedTimestamp = context.getOcppTime().getOcppTimestampNow(); - reportedTimestamp -= (ao_tick_ms() - t_statusTransition) / 1000UL; - - return new StatusNotification(connectorId, reportedStatus, reportedTimestamp, getErrorCode()); - } - - return nullptr; -} - -const char *ConnectorStatus::getErrorCode() { - for (auto s = connectorErrorCodeSamplers.begin(); s != connectorErrorCodeSamplers.end(); s++) { - const char *err = s->operator()(); - if (err != nullptr) { - return err; - } - } - return nullptr; -} - -void ConnectorStatus::beginSession(const char *sessionIdTag) { - //(special case: tx already exists -> update the sessionIdTag and good) - - if (!transaction) { - //no tx running, before creating new one, clean possible aorted tx - - auto txr = context.getTransactionStore()->getTxEnd(connectorId); - auto txsize = context.getTransactionStore()->size(connectorId); - for (decltype(txsize) i = 0; i < txsize; i++) { - txr = (txr + MAX_TX_CNT - 1) % MAX_TX_CNT; //decrement by 1 - - auto tx = context.getTransactionStore()->getTransaction(connectorId, txr); - //check if dangling silent tx, aborted tx, or corrupted entry (tx == null) - if (!tx || tx->isSilent() || tx->isAborted()) { - //yes, remove - bool removed = true; - if (auto mService = context.getMeteringService()) { - removed &= mService->removeTxMeterData(connectorId, txr); - } - if (removed) { - removed &= context.getTransactionStore()->remove(connectorId, txr); - } - if (removed) { - context.getTransactionStore()->setTxEnd(connectorId, txr); - AO_DBG_WARN("deleted dangling silent tx for new transaction"); - } else { - AO_DBG_ERR("memory corruption"); - break; - } - } else { - //no, tx record trimmed, end - break; - } - } - - //try to create new transaction - transaction = context.getTransactionStore()->createTransaction(connectorId); - } - - if (!transaction) { - //could not create transaction - now, try to replace tx history entry - - auto txl = context.getTransactionStore()->getTxBegin(connectorId); - auto txsize = context.getTransactionStore()->size(connectorId); - - for (decltype(txsize) i = 0; i < txsize; i++) { - - if (transaction) { - //success, finished here - break; - } - - //no transaction allocated, delete history entry to make space - - auto txhist = context.getTransactionStore()->getTransaction(connectorId, txl); - //oldest entry, now check if it's history and can be removed or corrupted entry - if (!txhist || txhist->isCompleted()) { - //yes, remove - bool removed = true; - if (auto mService = context.getMeteringService()) { - removed &= mService->removeTxMeterData(connectorId, txl); - } - if (removed) { - removed &= context.getTransactionStore()->remove(connectorId, txl); - } - if (removed) { - context.getTransactionStore()->setTxBegin(connectorId, (txl + 1) % MAX_TX_CNT); - AO_DBG_DEBUG("deleted tx history entry for new transaction"); - - transaction = context.getTransactionStore()->createTransaction(connectorId); - } else { - AO_DBG_ERR("memory corruption"); - break; - } - } else { - //no, end of history reached, don't delete further tx - AO_DBG_DEBUG("cannot delete more tx"); - break; - } - - txl++; - txl %= MAX_TX_CNT; - } - } - - if (!transaction) { - //couldn't create normal transaction -> check if to start charging without real transaction - if (silentOfflineTransactions && *silentOfflineTransactions) { - //try to handle charging session without sending StartTx or StopTx to the server - transaction = context.getTransactionStore()->createTransaction(connectorId, true); - - if (transaction) { - AO_DBG_DEBUG("created silent transaction"); - (void)0; - } - } - } - - if (!transaction) { - AO_DBG_ERR("could not allocate Tx"); - return; - } - - AO_DBG_DEBUG("Begin session with idTag %s, overwriting idTag %s", sessionIdTag != nullptr ? sessionIdTag : "", transaction->getIdTag()); - if (!sessionIdTag || *sessionIdTag == '\0') { - //input string is empty - transaction->setIdTag("A0-00-00-00"); - } else { - transaction->setIdTag(sessionIdTag); - } - - transaction->setSessionTimestamp(context.getOcppTime().getOcppTimestampNow()); - - transaction->commit(); -} - -void ConnectorStatus::endSession(const char *reason) { - - if (!transaction) { - AO_DBG_WARN("Cannot end session if no transaction running"); - return; - } - - if (transaction->isInSession()) { - AO_DBG_DEBUG("End session with idTag %s for reason %s, %s previous reason", - transaction->getIdTag(), reason ? reason : "undefined", - *transaction->getStopReason() ? "overruled by" : "no"); - - if (reason) { - transaction->setStopReason(reason); - } - transaction->endSession(); - transaction->commit(); - } -} - -const char *ConnectorStatus::getSessionIdTag() { - - if (!transaction) { - return nullptr; - } - - return transaction->isInSession() ? transaction->getIdTag() : nullptr; -} - -uint16_t ConnectorStatus::getSessionWriteCount() { - return transaction ? transaction->getTxNr() : 0; -} - -bool ConnectorStatus::isTransactionRunning() { - if (!transaction) { - return false; - } - - return transaction->isRunning(); -} - -int ConnectorStatus::getTransactionId() { - - if (!transaction) { - return -1; - } - - if (transaction->isRunning()) { - if (transaction->getStartRpcSync().isConfirmed()) { - return transaction->getTransactionId(); - } else { - return 0; - } - } else { - return -1; - } -} - -std::shared_ptr& ConnectorStatus::getTransaction() { - return transaction; -} - -int ConnectorStatus::getAvailability() { - if (availabilityVolatile == AVAILABILITY_INOPERATIVE || *availability == AVAILABILITY_INOPERATIVE) { - return AVAILABILITY_INOPERATIVE; - } else if (availabilityVolatile == AVAILABILITY_INOPERATIVE_SCHEDULED || *availability == AVAILABILITY_INOPERATIVE_SCHEDULED) { - return AVAILABILITY_INOPERATIVE_SCHEDULED; - } else { - return AVAILABILITY_OPERATIVE; - } -} - -void ConnectorStatus::setAvailability(bool available) { - if (available) { - *availability = AVAILABILITY_OPERATIVE; - } else { - if (isTransactionRunning()) { - *availability = AVAILABILITY_INOPERATIVE_SCHEDULED; - } else { - *availability = AVAILABILITY_INOPERATIVE; - } - } - configuration_save(); -} - -void ConnectorStatus::setAvailabilityVolatile(bool available) { - if (available) { - availabilityVolatile = AVAILABILITY_OPERATIVE; - } else { - if (isTransactionRunning()) { - availabilityVolatile = AVAILABILITY_INOPERATIVE_SCHEDULED; - } else { - availabilityVolatile = AVAILABILITY_INOPERATIVE; - } - } -} - -void ConnectorStatus::setConnectorPluggedSampler(std::function connectorPlugged) { - this->connectorPluggedSampler = connectorPlugged; - txProcess.addTrigger([this] () -> TxTrigger { - return connectorPluggedSampler() ? TxTrigger::Active : TxTrigger::Inactive; - }); -} - -void ConnectorStatus::setEvRequestsEnergySampler(std::function evRequestsEnergy) { - this->evRequestsEnergySampler = evRequestsEnergy; -} - -void ConnectorStatus::setConnectorEnergizedSampler(std::function connectorEnergized) { - this->connectorEnergizedSampler = connectorEnergized; -} - -void ConnectorStatus::addConnectorErrorCodeSampler(std::function connectorErrorCode) { - this->connectorErrorCodeSamplers.push_back(connectorErrorCode); -} - -void ConnectorStatus::setOnUnlockConnector(std::function()> unlockConnector) { - this->onUnlockConnector = unlockConnector; -} - -std::function()> ConnectorStatus::getOnUnlockConnector() { - return this->onUnlockConnector; -} - -void ConnectorStatus::setConnectorLock(std::function onConnectorLockPollTx) { - this->onConnectorLockPollTx = onConnectorLockPollTx; -} - -void ConnectorStatus::setTxBasedMeterUpdate(std::function onTxBasedMeterPollTx) { - this->onTxBasedMeterPollTx = onTxBasedMeterPollTx; -} diff --git a/src/ArduinoOcpp/Tasks/ChargePointStatus/ConnectorStatus.h b/src/ArduinoOcpp/Tasks/ChargePointStatus/ConnectorStatus.h deleted file mode 100644 index f6e69459..00000000 --- a/src/ArduinoOcpp/Tasks/ChargePointStatus/ConnectorStatus.h +++ /dev/null @@ -1,114 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef CONNECTORSTATUS_H -#define CONNECTORSTATUS_H - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#define AVAILABILITY_OPERATIVE 2 -#define AVAILABILITY_INOPERATIVE_SCHEDULED 1 -#define AVAILABILITY_INOPERATIVE 0 - -namespace ArduinoOcpp { - -class OcppModel; -class OcppMessage; -class Transaction; - -class ConnectorStatus { -private: - OcppModel& context; - - const int connectorId; - - std::shared_ptr transaction; - - std::shared_ptr> availability; - int availabilityVolatile = AVAILABILITY_OPERATIVE; - - std::function connectorPluggedSampler; - std::function evRequestsEnergySampler; - std::function connectorEnergizedSampler; - std::vector> connectorErrorCodeSamplers; - const char *getErrorCode(); - - OcppEvseState currentStatus = OcppEvseState::NOT_SET; - std::shared_ptr> minimumStatusDuration; //in seconds - OcppEvseState reportedStatus = OcppEvseState::NOT_SET; - unsigned long t_statusTransition = 0; - - std::function()> onUnlockConnector; - - std::function onConnectorLockPollTx; - std::function onTxBasedMeterPollTx; - - TransactionProcess txProcess; - - std::shared_ptr> connectionTimeOut; //in seconds - std::shared_ptr> stopTransactionOnInvalidId; - std::shared_ptr> stopTransactionOnEVSideDisconnect; - std::shared_ptr> unlockConnectorOnEVSideDisconnect; - std::shared_ptr> localAuthorizeOffline; - std::shared_ptr> localPreAuthorize; - - std::shared_ptr> silentOfflineTransactions; - std::shared_ptr> freeVendActive; - std::shared_ptr> freeVendIdTag; - bool freeVendTrackPlugged = false; -public: - ConnectorStatus(OcppModel& context, int connectorId); - - /* - * Relation Session <-> Transaction - * Session: the EV user is authorized and the OCPP transaction can start immediately without - * further confirmation by the user or the OCPP backend - * Transaction: started by "StartTransaction" and stopped by "StopTransaction". - * - * A session (between the EV user and the OCPP system) is on of two prerequisites to start a - * transaction. The other prerequisite is that the EV is properly connected to the EVSE - * (given by ConnectorPluggedSampler and no error code) - */ - void beginSession(const char *idTag); - void endSession(const char *reason = nullptr); - const char *getSessionIdTag(); - uint16_t getSessionWriteCount(); - bool isTransactionRunning(); - int getTransactionId(); - int getTransactionIdSync(); - std::shared_ptr& getTransaction(); - - int getAvailability(); - void setAvailability(bool available); - void setAvailabilityVolatile(bool available); //set inoperative state but keep only until reboot at most - void setAuthorizationProvider(std::function authorization); - void setConnectorPluggedSampler(std::function connectorPlugged); - void setEvRequestsEnergySampler(std::function evRequestsEnergy); - void setConnectorEnergizedSampler(std::function connectorEnergized); - void addConnectorErrorCodeSampler(std::function connectorErrorCode); - - OcppMessage *loop(); - - OcppEvseState inferenceStatus(); - - bool ocppPermitsCharge(); - - void setOnUnlockConnector(std::function()> unlockConnector); - std::function()> getOnUnlockConnector(); - - void setConnectorLock(std::function lockConnector); - void setTxBasedMeterUpdate(std::function updateTxBasedMeter); -}; - -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/Tasks/ChargePointStatus/OcppEvseState.h b/src/ArduinoOcpp/Tasks/ChargePointStatus/OcppEvseState.h deleted file mode 100644 index a3d9964a..00000000 --- a/src/ArduinoOcpp/Tasks/ChargePointStatus/OcppEvseState.h +++ /dev/null @@ -1,24 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef OCPP_EVSE_STATE -#define OCPP_EVSE_STATE - -namespace ArduinoOcpp { - -enum class OcppEvseState { - Available, - Preparing, - Charging, - SuspendedEVSE, - SuspendedEV, - Finishing, - Reserved, - Unavailable, - Faulted, - NOT_SET //internal value for "undefined" -}; - -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/Tasks/Diagnostics/DiagnosticsService.cpp b/src/ArduinoOcpp/Tasks/Diagnostics/DiagnosticsService.cpp deleted file mode 100644 index 8540b4d6..00000000 --- a/src/ArduinoOcpp/Tasks/Diagnostics/DiagnosticsService.cpp +++ /dev/null @@ -1,186 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include - -#include - -using namespace ArduinoOcpp; -using Ocpp16::DiagnosticsStatus; - -DiagnosticsService::DiagnosticsService(OcppEngine& context) : context(context) { - const char *fpId = "FirmwareManagement"; - auto fProfile = declareConfiguration("SupportedFeatureProfiles",fpId, CONFIGURATION_VOLATILE, false, true, true, false); - if (!strstr(*fProfile, fpId)) { - auto fProfilePlus = std::string(*fProfile); - if (!fProfilePlus.empty() && fProfilePlus.back() != ',') - fProfilePlus += ","; - fProfilePlus += fpId; - fProfile->setValue(fProfilePlus.c_str(), fProfilePlus.length() + 1); - } -} - -void DiagnosticsService::loop() { - auto notification = getDiagnosticsStatusNotification(); - if (notification) { - context.initiateOperation(std::move(notification)); - } - - const auto& timestampNow = context.getOcppModel().getOcppTime().getOcppTimestampNow(); - if (retries > 0 && timestampNow >= nextTry) { - - if (!uploadIssued) { - if (onUpload != nullptr) { - AO_DBG_DEBUG("Call onUpload"); - onUpload(location, startTime, stopTime); - uploadIssued = true; - } else { - AO_DBG_ERR("onUpload must be set! (see setOnUpload). Will abort"); - retries = 0; - uploadIssued = false; - } - } - - if (uploadIssued) { - if (uploadStatusSampler != nullptr && uploadStatusSampler() == UploadStatus::Uploaded) { - //success! - AO_DBG_DEBUG("end upload routine (by status)") - uploadIssued = false; - retries = 0; - } - - //check if maximum time elapsed or failed - const int UPLOAD_TIMEOUT = 60; - if (timestampNow - nextTry >= UPLOAD_TIMEOUT - || (uploadStatusSampler != nullptr && uploadStatusSampler() == UploadStatus::UploadFailed)) { - //maximum upload time elapsed or failed - - if (uploadStatusSampler == nullptr) { - //No way to find out if failed. But maximum time has elapsed. Assume success - AO_DBG_DEBUG("end upload routine (by timer)"); - uploadIssued = false; - retries = 0; - } else { - //either we have UploadFailed status or (NotDownloaded + timeout) here - AO_DBG_WARN("Upload timeout or failed"); - const int TRANSITION_DELAY = 10; - if (retryInterval <= UPLOAD_TIMEOUT + TRANSITION_DELAY) { - nextTry = timestampNow; - nextTry += TRANSITION_DELAY; //wait for another 10 seconds - } else { - nextTry += retryInterval; - } - retries--; - } - } - } //end if (uploadIssued) - } //end try upload -} - -//timestamps before year 2021 will be treated as "undefined" -std::string DiagnosticsService::requestDiagnosticsUpload(const std::string &location, int retries, unsigned int retryInterval, OcppTimestamp startTime, OcppTimestamp stopTime) { - if (onUpload == nullptr) //maybe add further plausibility checks - return nullptr; - - this->location = location; - this->retries = retries; - this->retryInterval = retryInterval; - this->startTime = startTime; - - OcppTimestamp stopMin = OcppTimestamp(2021,0,0,0,0,0); - if (stopTime >= stopMin) { - this->stopTime = stopTime; - } else { - auto newStop = context.getOcppModel().getOcppTime().getOcppTimestampNow(); - newStop += 3600 * 24 * 365; //set new stop time one year in future - this->stopTime = newStop; - } - - 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); - - AO_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); - - nextTry = context.getOcppModel().getOcppTime().getOcppTimestampNow(); - nextTry += 5; //wait for 5s before upload - uploadIssued = false; - - nextTry.toJsonString(dbuf, JSONDATE_LENGTH + 1); - AO_DBG_DEBUG("Initial try at %s", dbuf); - - return "diagnostics.log"; -} - - -DiagnosticsStatus DiagnosticsService::getDiagnosticsStatus() { - if (uploadIssued) { - if (uploadStatusSampler != nullptr) { - switch (uploadStatusSampler()) { - case UploadStatus::NotUploaded: - return DiagnosticsStatus::Uploading; - case UploadStatus::Uploaded: - return DiagnosticsStatus::Uploaded; - case UploadStatus::UploadFailed: - return DiagnosticsStatus::UploadFailed; - } - } - return DiagnosticsStatus::Uploading; - } - return DiagnosticsStatus::Idle; -} - -std::unique_ptr DiagnosticsService::getDiagnosticsStatusNotification() { - - if (getDiagnosticsStatus() != lastReportedStatus) { - lastReportedStatus = getDiagnosticsStatus(); - if (lastReportedStatus != DiagnosticsStatus::Idle) { - OcppMessage *diagNotificationMsg = new Ocpp16::DiagnosticsStatusNotification(lastReportedStatus); - auto diagNotification = makeOcppOperation(diagNotificationMsg); - return diagNotification; - } - } - - return nullptr; -} - -void DiagnosticsService::setOnUpload(std::function onUpload) { - this->onUpload = onUpload; -} - -void DiagnosticsService::setOnUploadStatusSampler(std::function uploadStatusSampler) { - this->uploadStatusSampler = uploadStatusSampler; -} - -#if !defined(AO_CUSTOM_DIAGNOSTICS) && !defined(AO_CUSTOM_WS) -#if defined(ESP32) || defined(ESP8266) - -DiagnosticsService *EspWiFi::makeDiagnosticsService(OcppEngine& context) { - auto diagService = new DiagnosticsService(context); - - /* - * add onUpload and uploadStatusSampler when logging was implemented - */ - - return diagService; -} - -#endif //defined(ESP32) || defined(ESP8266) -#endif //!defined(AO_CUSTOM_UPDATER) && !defined(AO_CUSTOM_WS) diff --git a/src/ArduinoOcpp/Tasks/Diagnostics/DiagnosticsService.h b/src/ArduinoOcpp/Tasks/Diagnostics/DiagnosticsService.h deleted file mode 100644 index bdc7de55..00000000 --- a/src/ArduinoOcpp/Tasks/Diagnostics/DiagnosticsService.h +++ /dev/null @@ -1,76 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef DIAGNOSTICSSERVICE_H -#define DIAGNOSTICSSERVICE_H - -#include -#include -#include -#include -#include - -namespace ArduinoOcpp { - -enum class UploadStatus { - NotUploaded, - Uploaded, - UploadFailed -}; - -class OcppEngine; -class OcppOperation; - -class DiagnosticsService { -private: - OcppEngine& context; - - std::string location {}; - int retries = 0; - unsigned int retryInterval = 0; - OcppTimestamp startTime = OcppTimestamp(); - OcppTimestamp stopTime = OcppTimestamp(); - - OcppTimestamp nextTry = OcppTimestamp(); - - std::function onUpload = nullptr; - std::function uploadStatusSampler = nullptr; - bool uploadIssued = false; - - std::unique_ptr getDiagnosticsStatusNotification(); - - Ocpp16::DiagnosticsStatus lastReportedStatus = Ocpp16::DiagnosticsStatus::Idle; - -public: - DiagnosticsService(OcppEngine& context); - - 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 std::string &location, int retries = 1, unsigned int retryInterval = 0, OcppTimestamp startTime = OcppTimestamp(), OcppTimestamp stopTime = OcppTimestamp()); - - Ocpp16::DiagnosticsStatus getDiagnosticsStatus(); - - void setOnUpload(std::function onUpload); - - void setOnUploadStatusSampler(std::function uploadStatusSampler); -}; - -#if !defined(AO_CUSTOM_DIAGNOSTICS) && !defined(AO_CUSTOM_WS) -#if defined(ESP32) || defined(ESP8266) - -namespace EspWiFi { - -DiagnosticsService *makeDiagnosticsService(OcppEngine& context); - -} - -#endif //defined(ESP32) || defined(ESP8266) -#endif //!defined(AO_CUSTOM_UPDATER) && !defined(AO_CUSTOM_WS) - -} //end namespace ArduinoOcpp - -#endif diff --git a/src/ArduinoOcpp/Tasks/Diagnostics/DiagnosticsStatus.h b/src/ArduinoOcpp/Tasks/Diagnostics/DiagnosticsStatus.h deleted file mode 100644 index f9e7188a..00000000 --- a/src/ArduinoOcpp/Tasks/Diagnostics/DiagnosticsStatus.h +++ /dev/null @@ -1,20 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef DIAGNOSTICS_STATUS -#define DIAGNOSTICS_STATUS - -namespace ArduinoOcpp { -namespace Ocpp16 { - -enum class DiagnosticsStatus { - Idle, - Uploaded, - UploadFailed, - Uploading -}; - -} -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/Tasks/FirmwareManagement/FirmwareService.cpp b/src/ArduinoOcpp/Tasks/FirmwareManagement/FirmwareService.cpp deleted file mode 100644 index 72b025b0..00000000 --- a/src/ArduinoOcpp/Tasks/FirmwareManagement/FirmwareService.cpp +++ /dev/null @@ -1,410 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include - -#include - -#include -#include - -using namespace ArduinoOcpp; -using ArduinoOcpp::Ocpp16::FirmwareStatus; - -FirmwareService::FirmwareService(OcppEngine& context) : context(context) { - const char *fpId = "FirmwareManagement"; - auto fProfile = declareConfiguration("SupportedFeatureProfiles",fpId, CONFIGURATION_VOLATILE, false, true, true, false); - if (!strstr(*fProfile, fpId)) { - auto fProfilePlus = std::string(*fProfile); - if (!fProfilePlus.empty() && fProfilePlus.back() != ',') - fProfilePlus += ","; - fProfilePlus += fpId; - fProfile->setValue(fProfilePlus.c_str(), fProfilePlus.length() + 1); - } -} - -void FirmwareService::setBuildNumber(const char *buildNumber) { - if (buildNumber == nullptr) - return; - this->buildNumber = buildNumber; - previousBuildNumber = declareConfiguration("BUILD_NUMBER", buildNumber, CONFIGURATION_FN, false, false, true, false); - checkedSuccessfulFwUpdate = false; //--> CS will be notified -} - -void FirmwareService::loop() { - auto notification = getFirmwareStatusNotification(); - if (notification) { - context.initiateOperation(std::move(notification)); - } - - if (ao_tick_ms() - timestampTransition < delayTransition) { - return; - } - - OcppTimestamp timestampNow = context.getOcppModel().getOcppTime().getOcppTimestampNow(); - if (retries > 0 && timestampNow >= retreiveDate) { - - auto cpStatusService = context.getOcppModel().getChargePointStatusService(); - - //if (!downloadIssued) { - if (stage == UpdateStage::Idle) { - AO_DBG_INFO("Start update"); - - if (cpStatusService) { - ConnectorStatus *evse = cpStatusService->getConnector(0); - evse->setAvailabilityVolatile(false); - } - if (onDownload == nullptr) { - stage = UpdateStage::AfterDownload; - } else { - downloadIssued = true; - stage = UpdateStage::AwaitDownload; - timestampTransition = ao_tick_ms(); - delayTransition = 5000; //delay between state "Downloading" and actually starting the download - return; - } - } - - //if (!onDownloadCalled) { - if (stage == UpdateStage::AwaitDownload) { - AO_DBG_INFO("Start download"); - stage = UpdateStage::Downloading; - if (onDownload != nullptr) { - onDownload(location); - timestampTransition = ao_tick_ms(); - delayTransition = 30000; //give the download at least 30s - return; - } - } - - const int DOWNLOAD_TIMEOUT = 120; - //if (downloadIssued && !installationIssued && - if (stage == UpdateStage::Downloading) { - - //check if client reports download to be finished - if (downloadStatusSampler != nullptr && downloadStatusSampler() == DownloadStatus::Downloaded) { - stage = UpdateStage::AfterDownload; - } - - //if client doesn't report download state, assume download to be finished (at least 30s download time have passed until here) - if (downloadStatusSampler == nullptr) { - stage = UpdateStage::AfterDownload; - } - - //check for timeout or error condition - if (timestampNow - retreiveDate >= DOWNLOAD_TIMEOUT - || (downloadStatusSampler != nullptr && downloadStatusSampler() == DownloadStatus::DownloadFailed)) { - - AO_DBG_INFO("Download timeout or failed! Retry"); - if (retryInterval < DOWNLOAD_TIMEOUT) - retreiveDate = timestampNow; - else - retreiveDate += retryInterval; - retries--; - resetStage(); - - timestampTransition = ao_tick_ms(); - delayTransition = 10000; - return; - } - - } - - //if (!installationIssued) { - if (stage == UpdateStage::AfterDownload) { - bool ongoingTx = false; - if (cpStatusService) { - for (int i = 0; i < cpStatusService->getNumConnectors(); i++) { - auto connector = cpStatusService->getConnector(i); - if (connector && connector->getTransactionId() >= 0) { - ongoingTx = true; - break; - } - } - } - - if (!ongoingTx) { - stage = UpdateStage::AwaitInstallation; - installationIssued = true; - - timestampTransition = ao_tick_ms(); - delayTransition = 10000; - } - - return; - } - - if (stage == UpdateStage::AwaitInstallation) { - AO_DBG_INFO("Installing"); - stage = UpdateStage::Installing; - - if (onInstall != nullptr) { - onInstall(location); //should restart the device on success - } else { - AO_DBG_WARN("onInstall must be set! (see setOnInstall). Will abort"); - } - - timestampTransition = ao_tick_ms(); - delayTransition = 40000; - return; - } - - if (stage == UpdateStage::Installing) { - - //check if client reports installation to be finished - if (installationStatusSampler == nullptr || installationStatusSampler() == InstallationStatus::Installed) { - //if client doesn't report installation state, assume download to be finished (at least 40s installation time have passed until here) - - //Client should reboot during onInstall. If not, client is responsible to reboot at a later point - resetStage(); - retries = 0; //End of update routine. Client must reboot on its own - return; - } - - //check for timeout or error condition - const int INSTALLATION_TIMEOUT = 120; - if ((timestampNow - retreiveDate >= INSTALLATION_TIMEOUT + DOWNLOAD_TIMEOUT) - || (installationStatusSampler != nullptr && installationStatusSampler() == InstallationStatus::InstallationFailed)) { - - AO_DBG_INFO("Installation timeout or failed! Retry"); - if (retryInterval < INSTALLATION_TIMEOUT + DOWNLOAD_TIMEOUT) - retreiveDate = timestampNow; - else - retreiveDate += retryInterval; - retries--; - resetStage(); - - timestampTransition = ao_tick_ms(); - delayTransition = 10000; - return; - } - } - - //should never reach this code - AO_DBG_ERR("Firmware update failed"); - retries = 0; - resetStage(); - } -} - -void FirmwareService::scheduleFirmwareUpdate(const std::string &location, OcppTimestamp retreiveDate, int retries, unsigned int retryInterval) { - this->location = location; - this->retreiveDate = retreiveDate; - this->retries = retries; - this->retryInterval = retryInterval; - - char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; - this->retreiveDate.toJsonString(dbuf, JSONDATE_LENGTH + 1); - - AO_DBG_INFO("Scheduled FW update!\n" \ - " location = %s\n" \ - " retrieveDate = %s\n" \ - " retries = %i" \ - ", retryInterval = %u", - this->location.c_str(), - dbuf, - this->retries, - this->retryInterval); - - resetStage(); -} - -FirmwareStatus FirmwareService::getFirmwareStatus() { - if (installationIssued) { - if (installationStatusSampler != nullptr) { - if (installationStatusSampler() == InstallationStatus::Installed) { - return FirmwareStatus::Installed; - } else if (installationStatusSampler() == InstallationStatus::InstallationFailed) { - return FirmwareStatus::InstallationFailed; - } - } - if (onInstall != nullptr) - return FirmwareStatus::Installing; - } - - if (downloadIssued) { - if (downloadStatusSampler != nullptr) { - if (downloadStatusSampler() == DownloadStatus::Downloaded) { - return FirmwareStatus::Downloaded; - } else if (downloadStatusSampler() == DownloadStatus::DownloadFailed) { - return FirmwareStatus::DownloadFailed; - } - } - if (onDownload != nullptr) - return FirmwareStatus::Downloading; - } - - return FirmwareStatus::Idle; -} - -std::unique_ptr FirmwareService::getFirmwareStatusNotification() { - /* - * Check if FW has been updated previously, but only once - */ - if (!checkedSuccessfulFwUpdate && buildNumber != nullptr && previousBuildNumber != nullptr) { - checkedSuccessfulFwUpdate = true; - - size_t buildNoSize = previousBuildNumber->getBuffsize(); - if (strncmp(buildNumber, *previousBuildNumber, buildNoSize)) { - //new FW - previousBuildNumber->setValue(buildNumber, strlen(buildNumber) + 1); - configuration_save(); - - lastReportedStatus = FirmwareStatus::Installed; - OcppMessage *fwNotificationMsg = new Ocpp16::FirmwareStatusNotification(lastReportedStatus); - auto fwNotification = makeOcppOperation(fwNotificationMsg); - return fwNotification; - } - } - - if (getFirmwareStatus() != lastReportedStatus) { - lastReportedStatus = getFirmwareStatus(); - if (lastReportedStatus != FirmwareStatus::Idle && lastReportedStatus != FirmwareStatus::Installed) { - OcppMessage *fwNotificationMsg = new Ocpp16::FirmwareStatusNotification(lastReportedStatus); - auto fwNotification = makeOcppOperation(fwNotificationMsg); - return fwNotification; - } - } - - return nullptr; -} - -void FirmwareService::setOnDownload(std::function onDownload) { - this->onDownload = onDownload; -} - -void FirmwareService::setDownloadStatusSampler(std::function downloadStatusSampler) { - this->downloadStatusSampler = downloadStatusSampler; -} - -void FirmwareService::setOnInstall(std::function onInstall) { - this->onInstall = onInstall; -} - -void FirmwareService::setInstallationStatusSampler(std::function installationStatusSampler) { - this->installationStatusSampler = installationStatusSampler; -} - -void FirmwareService::resetStage() { - stage = UpdateStage::Idle; - downloadIssued = false; - installationIssued = false; -} - -#if !defined(AO_CUSTOM_UPDATER) && !defined(AO_CUSTOM_WS) -#if defined(ESP32) - -#include - -FirmwareService *EspWiFi::makeFirmwareService(OcppEngine& context, const char *buildNumber) { - FirmwareService *fwService = new FirmwareService(context); - fwService->setBuildNumber(buildNumber); - - /* - * example of how to integrate a separate download phase (optional) - */ -#if 0 //separate download phase - fwService->setOnDownload([] (const std::string &location) { - //download the new binary - //... - return true; - }); - - fwService->setDownloadStatusSampler([] () { - //report the download progress - //... - return DownloadStatus::NotDownloaded; - }); -#endif //separate download phase - - fwService->setOnInstall([fwService] (const std::string &location) { - - fwService->setInstallationStatusSampler([](){return InstallationStatus::NotInstalled;}); - - 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.c_str()); - - switch (ret) { - case HTTP_UPDATE_FAILED: - fwService->setInstallationStatusSampler([](){return InstallationStatus::InstallationFailed;}); - AO_DBG_WARN("HTTP_UPDATE_FAILED Error (%d): %s\n", httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str()); - break; - case HTTP_UPDATE_NO_UPDATES: - fwService->setInstallationStatusSampler([](){return InstallationStatus::InstallationFailed;}); - AO_DBG_WARN("HTTP_UPDATE_NO_UPDATES"); - break; - case HTTP_UPDATE_OK: - fwService->setInstallationStatusSampler([](){return InstallationStatus::Installed;}); - AO_DBG_INFO("HTTP_UPDATE_OK"); - ESP.restart(); - break; - } - - return true; - }); - - fwService->setInstallationStatusSampler([] () { - return InstallationStatus::NotInstalled; - }); - - return fwService; -} - -#elif defined(ESP8266) - -#include - -FirmwareService *EspWiFi::makeFirmwareService(OcppEngine& context, const char *buildNumber) { - FirmwareService *fwService = new FirmwareService(context); - fwService->setBuildNumber(buildNumber); - - fwService->setOnInstall([fwService] (const std::string &location) { - - WiFiClient client; - //WiFiClientSecure client; - //client.setCACert(rootCACertificate); - client.setTimeout(60); //in seconds - - //ESPhttpUpdate.setLedPin(downloadStatusLedPin); - - HTTPUpdateResult ret = ESPhttpUpdate.update(client, location.c_str()); - - switch (ret) { - case HTTP_UPDATE_FAILED: - fwService->setInstallationStatusSampler([](){return InstallationStatus::InstallationFailed;}); - AO_DBG_WARN("HTTP_UPDATE_FAILED Error (%d): %s\n", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); - break; - case HTTP_UPDATE_NO_UPDATES: - fwService->setInstallationStatusSampler([](){return InstallationStatus::InstallationFailed;}); - AO_DBG_WARN("HTTP_UPDATE_NO_UPDATES"); - break; - case HTTP_UPDATE_OK: - fwService->setInstallationStatusSampler([](){return InstallationStatus::Installed;}); - AO_DBG_INFO("HTTP_UPDATE_OK"); - ESP.restart(); - break; - } - - return true; - }); - - fwService->setInstallationStatusSampler([] () { - return InstallationStatus::NotInstalled; - }); - - return fwService; -} - -#endif //defined(ESP8266) -#endif //!defined(AO_CUSTOM_UPDATER) && !defined(AO_CUSTOM_WS) diff --git a/src/ArduinoOcpp/Tasks/FirmwareManagement/FirmwareService.h b/src/ArduinoOcpp/Tasks/FirmwareManagement/FirmwareService.h deleted file mode 100644 index 2e888de7..00000000 --- a/src/ArduinoOcpp/Tasks/FirmwareManagement/FirmwareService.h +++ /dev/null @@ -1,108 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef FIRMWARESERVICE_H -#define FIRMWARESERVICE_H - -#include -#include - -#include -#include -#include - -namespace ArduinoOcpp { - -enum class DownloadStatus { - NotDownloaded, // == before download or during download - Downloaded, - DownloadFailed -}; - -enum class InstallationStatus { - NotInstalled, // == before installation or during installation - Installed, - InstallationFailed -}; - -class OcppEngine; -class OcppOperation; - -class FirmwareService { -private: - OcppEngine& context; - - std::shared_ptr> previousBuildNumber = NULL; - const char *buildNumber = NULL; - - std::function downloadStatusSampler = NULL; - bool downloadIssued = false; - - std::function installationStatusSampler = NULL; - bool installationIssued = false; - - Ocpp16::FirmwareStatus lastReportedStatus = Ocpp16::FirmwareStatus::Idle; - bool checkedSuccessfulFwUpdate = false; - - std::string location {}; - OcppTimestamp retreiveDate = OcppTimestamp(); - int retries = 0; - unsigned int retryInterval = 0; - - std::function onDownload = NULL; - std::function onInstall = NULL; - - unsigned long delayTransition = 0; - unsigned long timestampTransition = 0; - - enum class UpdateStage { - Idle, - AwaitDownload, - Downloading, - AfterDownload, - AwaitInstallation, - Installing - } stage; - - void resetStage(); - - std::unique_ptr getFirmwareStatusNotification(); - -public: - FirmwareService(OcppEngine& context); - - void setBuildNumber(const char *buildNumber); - - void loop(); - - void scheduleFirmwareUpdate(const std::string &location, OcppTimestamp retreiveDate, int retries = 1, unsigned int retryInterval = 0); - - Ocpp16::FirmwareStatus getFirmwareStatus(); - - void setOnDownload(std::function onDownload); - - void setDownloadStatusSampler(std::function downloadStatusSampler); - - void setOnInstall(std::function onInstall); - - void setInstallationStatusSampler(std::function installationStatusSampler); -}; - -} //endif namespace ArduinoOcpp - -#if !defined(AO_CUSTOM_UPDATER) && !defined(AO_CUSTOM_WS) -#if defined(ESP32) || defined(ESP8266) - -namespace ArduinoOcpp { -namespace EspWiFi { - -FirmwareService *makeFirmwareService(OcppEngine& context, const char *buildNumber); - -} -} - -#endif //defined(ESP32) || defined(ESP8266) -#endif //!defined(AO_CUSTOM_UPDATER) && !defined(AO_CUSTOM_WS) - -#endif diff --git a/src/ArduinoOcpp/Tasks/Heartbeat/HeartbeatService.cpp b/src/ArduinoOcpp/Tasks/Heartbeat/HeartbeatService.cpp deleted file mode 100644 index 06e57647..00000000 --- a/src/ArduinoOcpp/Tasks/Heartbeat/HeartbeatService.cpp +++ /dev/null @@ -1,30 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include - -using namespace ArduinoOcpp; - -HeartbeatService::HeartbeatService(OcppEngine& context) : context(context) { - heartbeatInterval = declareConfiguration("HeartbeatInterval", 86400); - lastHeartbeat = ao_tick_ms(); -} - -void HeartbeatService::loop() { - unsigned long hbInterval = *heartbeatInterval; - hbInterval *= 1000UL; //conversion s -> ms - unsigned long now = ao_tick_ms(); - - if (now - lastHeartbeat >= hbInterval) { - lastHeartbeat = now; - - auto heartbeat = makeOcppOperation("Heartbeat"); - context.initiateOperation(std::move(heartbeat)); - } -} diff --git a/src/ArduinoOcpp/Tasks/Heartbeat/HeartbeatService.h b/src/ArduinoOcpp/Tasks/Heartbeat/HeartbeatService.h deleted file mode 100644 index 05e2c57f..00000000 --- a/src/ArduinoOcpp/Tasks/Heartbeat/HeartbeatService.h +++ /dev/null @@ -1,30 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef HEARTBEATSERVICE_H -#define HEARTBEATSERVICE_H - -#include -#include - -namespace ArduinoOcpp { - -class OcppEngine; - -class HeartbeatService { -private: - OcppEngine& context; - - unsigned long lastHeartbeat; - std::shared_ptr> heartbeatInterval; - -public: - HeartbeatService(OcppEngine& context); - - void loop(); -}; - -} - -#endif diff --git a/src/ArduinoOcpp/Tasks/Metering/ConnectorMeterValuesRecorder.cpp b/src/ArduinoOcpp/Tasks/Metering/ConnectorMeterValuesRecorder.cpp deleted file mode 100644 index bbfd9d44..00000000 --- a/src/ArduinoOcpp/Tasks/Metering/ConnectorMeterValuesRecorder.cpp +++ /dev/null @@ -1,292 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -using namespace ArduinoOcpp; -using namespace ArduinoOcpp::Ocpp16; - -ConnectorMeterValuesRecorder::ConnectorMeterValuesRecorder(OcppModel& context, int connectorId, MeterStore& meterStore) - : context(context), connectorId{connectorId}, meterStore(meterStore) { - - auto MeterValuesSampledData = declareConfiguration( - "MeterValuesSampledData", - "Energy.Active.Import.Register,Power.Active.Import", - CONFIGURATION_FN, - true,true,true,false - ); - declareConfiguration("MeterValuesSampledDataMaxLength", 8, CONFIGURATION_VOLATILE, false, true, false, false); - MeterValueCacheSize = declareConfiguration("AO_MeterValueCacheSize", 1, CONFIGURATION_FN, true, true, true, false); - MeterValueSampleInterval = declareConfiguration("MeterValueSampleInterval", 60); - - auto StopTxnSampledData = declareConfiguration( - "StopTxnSampledData", - "", - CONFIGURATION_FN, - true,true,true,false - ); - declareConfiguration("StopTxnSampledDataMaxLength", 8, CONFIGURATION_VOLATILE, false, true, false, false); - - auto MeterValuesAlignedData = declareConfiguration( - "MeterValuesAlignedData", - "Energy.Active.Import.Register,Power.Active.Import", - CONFIGURATION_FN, - true,true,true,false - ); - declareConfiguration("MeterValuesAlignedDataMaxLength", 8, CONFIGURATION_VOLATILE, false, true, false, false); - ClockAlignedDataInterval = declareConfiguration("ClockAlignedDataInterval", 0); - - auto StopTxnAlignedData = declareConfiguration( - "StopTxnAlignedData", - "", - CONFIGURATION_FN, - true,true,true,false - ); - - MeterValuesInTxOnly = declareConfiguration("AO_MeterValuesInTxOnly", true, CONFIGURATION_FN, true, true, true, false); - StopTxnDataCapturePeriodic = declareConfiguration("AO_StopTxnDataCapturePeriodic", false, CONFIGURATION_FN, true, true, true, false); - - sampledDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, MeterValuesSampledData)); - alignedDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, MeterValuesAlignedData)); - stopTxnSampledDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, StopTxnSampledData)); - stopTxnAlignedDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, StopTxnAlignedData)); - - std::function validateSelectString = [this] (const char *csl) { - bool isValid = true; - const char *l = csl; //the beginning of an entry of the comma-separated list - const char *r = l; //one place after the last character of the entry beginning with l - while (*l) { - if (*l == ',') { - l++; - continue; - } - r = l + 1; - while (*r != '\0' && *r != ',') { - r++; - } - bool found = false; - for (size_t i = 0; i < samplers.size(); i++) { - auto &measurand = samplers[i]->getProperties().getMeasurand(); - if ((std::ptrdiff_t) measurand.length() == r - l && //same length - !strncmp(l, measurand.c_str(), measurand.length())) { //same content - found = true; - break; - } - } - if (!found) { - isValid = false; - AO_DBG_WARN("could not find metering device for %.*s", (int) (r - l), l); - break; - } - l = r; - } - return isValid; - }; - MeterValuesSampledData->setValidator(validateSelectString); - StopTxnSampledData->setValidator(validateSelectString); - MeterValuesAlignedData->setValidator(validateSelectString); - StopTxnAlignedData->setValidator(validateSelectString); -} - -OcppMessage *ConnectorMeterValuesRecorder::loop() { - - bool txBreak = false; - if (context.getConnectorStatus(connectorId)) { - auto &curTx = context.getConnectorStatus(connectorId)->getTransaction(); - txBreak = (curTx && curTx->isRunning()) != trackTxRunning; - trackTxRunning = (curTx && curTx->isRunning()); - } - - if (txBreak) { - lastSampleTime = ao_tick_ms(); - } - - if ((txBreak || meterData.size() >= (size_t) *MeterValueCacheSize) && !meterData.empty()) { - auto meterValues = new MeterValues(std::move(meterData), connectorId, transaction); - meterData.clear(); - return meterValues; - } - - if (context.getConnectorStatus(connectorId)) { - if (transaction != context.getConnectorStatus(connectorId)->getTransaction()) { - transaction = context.getConnectorStatus(connectorId)->getTransaction(); - } - - if (transaction && transaction->isRunning() && !transaction->isSilent()) { - //check during transaction - - if (!stopTxnData || stopTxnData->getTxNr() != transaction->getTxNr()) { - AO_DBG_WARN("reload stopTxnData"); - //reload (e.g. after power cut during transaction) - stopTxnData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction.get()); - } - } else { - //check outside of transaction - - if (!MeterValuesInTxOnly || *MeterValuesInTxOnly) { - //don't take any MeterValues outside of transactions - meterData.clear(); - return nullptr; - } - } - } - - if (*ClockAlignedDataInterval >= 1) { - - auto& timestampNow = context.getOcppTime().getOcppTimestampNow(); - auto dt = nextAlignedTime - timestampNow; - if (dt <= 0 || //normal case: interval elapsed - dt > *ClockAlignedDataInterval) { //special case: clock has been adjusted or first run - - AO_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(context.getOcppTime().getOcppTimestampNow(), ReadingContext::SampleClock); - if (alignedMeterValues) { - meterData.push_back(std::move(alignedMeterValues)); - } - - if (stopTxnData) { - auto alignedStopTx = stopTxnAlignedDataBuilder->takeSample(context.getOcppTime().getOcppTimestampNow(), ReadingContext::SampleClock); - if (alignedStopTx) { - stopTxnData->addTxData(std::move(alignedStopTx)); - } - } - - } - - OcppTimestamp midnightBase = OcppTimestamp(2010,0,0,0,0,0); - auto intervall = timestampNow - midnightBase; - intervall %= 3600 * 24; - OcppTimestamp midnight = timestampNow - intervall; - intervall += *ClockAlignedDataInterval; - if (intervall >= 3600 * 24) { - //next measurement is tomorrow; set to precisely 00:00 - nextAlignedTime = midnight; - nextAlignedTime += 3600 * 24; - } else { - intervall /= *ClockAlignedDataInterval; - nextAlignedTime = midnight + (intervall * *ClockAlignedDataInterval); - } - } - } - - if (*MeterValueSampleInterval >= 1) { - //record periodic tx data - - if (ao_tick_ms() - lastSampleTime >= (unsigned long) (*MeterValueSampleInterval * 1000)) { - auto sampleMeterValues = sampledDataBuilder->takeSample(context.getOcppTime().getOcppTimestampNow(), ReadingContext::SamplePeriodic); - if (sampleMeterValues) { - meterData.push_back(std::move(sampleMeterValues)); - } - - if (stopTxnData && StopTxnDataCapturePeriodic && *StopTxnDataCapturePeriodic) { - auto sampleStopTx = stopTxnSampledDataBuilder->takeSample(context.getOcppTime().getOcppTimestampNow(), ReadingContext::SamplePeriodic); - if (sampleStopTx) { - stopTxnData->addTxData(std::move(sampleStopTx)); - } - } - lastSampleTime = ao_tick_ms(); - } - } - - if (*ClockAlignedDataInterval < 1 && *MeterValueSampleInterval < 1) { - meterData.clear(); - } - - return nullptr; //successful method completition. Currently there is no reason to send a MeterValues Msg. -} - -OcppMessage *ConnectorMeterValuesRecorder::takeTriggeredMeterValues() { - - auto sample = sampledDataBuilder->takeSample(context.getOcppTime().getOcppTimestampNow(), ReadingContext::Trigger); - - if (!sample) { - return nullptr; - } - - decltype(meterData) mv_now; - mv_now.push_back(std::move(sample)); - - std::shared_ptr transaction = nullptr; - if (context.getConnectorStatus(connectorId)) { - transaction = context.getConnectorStatus(connectorId)->getTransaction(); - } - - return new MeterValues(std::move(mv_now), connectorId, transaction); -} - -void ConnectorMeterValuesRecorder::setPowerSampler(PowerSampler ps){ - this->powerSampler = ps; -} - -void ConnectorMeterValuesRecorder::setEnergySampler(EnergySampler es){ - this->energySampler = es; -} - -void ConnectorMeterValuesRecorder::addMeterValueSampler(std::unique_ptr meterValueSampler) { - if (!meterValueSampler->getProperties().getMeasurand().compare("Energy.Active.Import.Register")) { - energySamplerIndex = samplers.size(); - } - samplers.push_back(std::move(meterValueSampler)); -} - -std::unique_ptr ConnectorMeterValuesRecorder::readTxEnergyMeter(ReadingContext context) { - if (energySamplerIndex >= 0 && (size_t) energySamplerIndex < samplers.size()) { - return samplers[energySamplerIndex]->takeValue(context); - } else { - AO_DBG_DEBUG("Called readTxEnergyMeter(), but no energySampler or handling strategy set"); - return nullptr; - } -} - -void ConnectorMeterValuesRecorder::beginTxMeterData(Transaction *transaction) { - if (!stopTxnData || stopTxnData->getTxNr() != transaction->getTxNr()) { - stopTxnData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction); - } - - if (stopTxnData) { - auto sampleTxBegin = stopTxnSampledDataBuilder->takeSample(context.getOcppTime().getOcppTimestampNow(), ReadingContext::TransactionBegin); - if (sampleTxBegin) { - stopTxnData->addTxData(std::move(sampleTxBegin)); - } - } -} - -std::shared_ptr ConnectorMeterValuesRecorder::endTxMeterData(Transaction *transaction) { - if (!stopTxnData || stopTxnData->getTxNr() != transaction->getTxNr()) { - stopTxnData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction); - } - - if (stopTxnData) { - auto sampleTxEnd = stopTxnSampledDataBuilder->takeSample(context.getOcppTime().getOcppTimestampNow(), ReadingContext::TransactionEnd); - if (sampleTxEnd) { - stopTxnData->addTxData(std::move(sampleTxEnd)); - } - } - - return std::move(stopTxnData); -} - -std::shared_ptr ConnectorMeterValuesRecorder::getStopTxMeterData(Transaction *transaction) { - auto txData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction); - - if (!txData) { - AO_DBG_ERR("could not create TxData"); - return nullptr; - } - - return txData; -} diff --git a/src/ArduinoOcpp/Tasks/Metering/ConnectorMeterValuesRecorder.h b/src/ArduinoOcpp/Tasks/Metering/ConnectorMeterValuesRecorder.h deleted file mode 100644 index 514c594b..00000000 --- a/src/ArduinoOcpp/Tasks/Metering/ConnectorMeterValuesRecorder.h +++ /dev/null @@ -1,87 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef CONNECTOR_METER_VALUES_RECORDER -#define CONNECTOR_METER_VALUES_RECORDER - -#include -#include -#include - -#include -#include -#include -#include - -namespace ArduinoOcpp { - -using PowerSampler = std::function; -using EnergySampler = std::function; - -class OcppModel; -class OcppMessage; -class Transaction; -class MeterStore; - -class ConnectorMeterValuesRecorder { -private: - OcppModel& context; - const int connectorId; - MeterStore& meterStore; - - std::vector> meterData; - std::shared_ptr stopTxnData; - - std::unique_ptr sampledDataBuilder; - std::unique_ptr alignedDataBuilder; - std::unique_ptr stopTxnSampledDataBuilder; - std::unique_ptr stopTxnAlignedDataBuilder; - - std::shared_ptr> sampledDataSelect; - std::shared_ptr> alignedDataSelect; - std::shared_ptr> stopTxnSampledDataSelect; - std::shared_ptr> stopTxnAlignedDataSelect; - - unsigned long lastSampleTime = 0; //0 means not charging right now - OcppTimestamp nextAlignedTime; - std::shared_ptr transaction; - bool trackTxRunning = false; - - PowerSampler powerSampler = nullptr; - EnergySampler energySampler = nullptr; - std::vector> samplers; - int energySamplerIndex {-1}; - - std::shared_ptr> MeterValueSampleInterval; - std::shared_ptr> MeterValueCacheSize; - - std::shared_ptr> ClockAlignedDataInterval; - - std::shared_ptr> MeterValuesInTxOnly; - std::shared_ptr> StopTxnDataCapturePeriodic; -public: - ConnectorMeterValuesRecorder(OcppModel& context, int connectorId, MeterStore& meterStore); - - OcppMessage *loop(); - - void setPowerSampler(PowerSampler powerSampler); - - void setEnergySampler(EnergySampler energySampler); - - void addMeterValueSampler(std::unique_ptr meterValueSampler); - - std::unique_ptr readTxEnergyMeter(ReadingContext context); - - OcppMessage *takeTriggeredMeterValues(); - - void beginTxMeterData(Transaction *transaction); - - std::shared_ptr endTxMeterData(Transaction *transaction); - - std::shared_ptr getStopTxMeterData(Transaction *transaction); - -}; - -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/Tasks/Metering/MeterValue.cpp b/src/ArduinoOcpp/Tasks/Metering/MeterValue.cpp deleted file mode 100644 index ace0471e..00000000 --- a/src/ArduinoOcpp/Tasks/Metering/MeterValue.cpp +++ /dev/null @@ -1,139 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include - -using ArduinoOcpp::MeterValue; -using ArduinoOcpp::MeterValueBuilder; - -std::unique_ptr MeterValue::toJson() { - size_t capacity = 0; - std::vector> entries; - for (auto sample = sampledValue.begin(); sample != sampledValue.end(); sample++) { - auto json = (*sample)->toJson(); - if (!json) { - return nullptr; - } - capacity += json->capacity(); - entries.push_back(std::move(json)); - } - - capacity += JSON_ARRAY_SIZE(entries.size()); - capacity += JSONDATE_LENGTH + 1; - capacity += JSON_OBJECT_SIZE(2); - - auto result = std::unique_ptr(new DynamicJsonDocument(capacity + 100)); //TODO remove safety space - auto jsonPayload = result->to(); - - char timestampStr [JSONDATE_LENGTH + 1] = {'\0'}; - if (timestamp.toJsonString(timestampStr, JSONDATE_LENGTH + 1)) { - jsonPayload["timestamp"] = timestampStr; - } - auto jsonMeterValue = jsonPayload.createNestedArray("sampledValue"); - for (auto entry = entries.begin(); entry != entries.end(); entry++) { - jsonMeterValue.add(**entry); - } - return result; -} - -MeterValueBuilder::MeterValueBuilder(const std::vector> &samplers, - std::shared_ptr> samplers_select) : - samplers(samplers), - select(samplers_select) { - - updateObservedSamplers(); - select_observe = select->getValueRevision(); -} - -void MeterValueBuilder::updateObservedSamplers() { - - if (select_mask.size() != samplers.size()) { - select_mask.resize(samplers.size(), false); - select_n = 0; - } - - auto selectStr = select->operator const char *(); - size_t sl = 0, sr = 0; - while (selectStr && sl < select->getBuffsize()) { - while (sr < select->getBuffsize()) { - if (selectStr[sr] == ',') { - break; - } - sr++; - } - - if (sr != sl + 1) { - for (size_t i = 0; i < samplers.size(); i++) { - if (!strncmp(samplers[i]->getProperties().getMeasurand().c_str(), selectStr + sl, sr - sl)) { - select_mask[i] = true; - select_n++; - } - } - } - - sr++; - sl = sr; - } -} - -std::unique_ptr MeterValueBuilder::takeSample(const OcppTimestamp& timestamp, const ReadingContext& context) { - if (select_observe != select->getValueRevision() || //OCPP server has changed configuration about which measurands to take - samplers.size() != select_mask.size()) { //Client has added another Measurand; synchronize lists - AO_DBG_DEBUG("Updating observed samplers due to config change or samplers added"); - updateObservedSamplers(); - select_observe = select->getValueRevision(); - } - - if (select_n == 0) { - return nullptr; - } - - auto sample = std::unique_ptr(new MeterValue(timestamp)); - - for (size_t i = 0; i < select_mask.size(); i++) { - if (select_mask[i]) { - sample->addSampledValue(samplers[i]->takeValue(context)); - } - } - - return sample; -} - -std::unique_ptr MeterValueBuilder::deserializeSample(const JsonObject mvJson) { - - OcppTimestamp timestamp; - bool ret = timestamp.setTime(mvJson["timestamp"] | "Invalid"); - if (!ret) { - AO_DBG_ERR("invalid timestamp"); - return nullptr; - } - - auto sample = std::unique_ptr(new MeterValue(timestamp)); - - JsonArray sampledValue = mvJson["sampledValue"]; - for (JsonObject svJson : sampledValue) { //for each sampled value, search sampler with matching measurand type - for (auto& sampler : samplers) { - auto& properties = sampler->getProperties(); - if (!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"] | "")) { - //found correct sampler - auto dVal = sampler->deserializeValue(svJson); - if (dVal) { - sample->addSampledValue(std::move(dVal)); - } else { - AO_DBG_ERR("deserialization error"); - } - break; - } - } - } - - AO_DBG_VERBOSE("deserialized MV"); - return sample; -} diff --git a/src/ArduinoOcpp/Tasks/Metering/MeterValue.h b/src/ArduinoOcpp/Tasks/Metering/MeterValue.h deleted file mode 100644 index d387cb10..00000000 --- a/src/ArduinoOcpp/Tasks/Metering/MeterValue.h +++ /dev/null @@ -1,50 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef METERVALUE_H -#define METERVALUE_H - -#include -#include -#include -#include -#include -#include - -namespace ArduinoOcpp { - -class MeterValue { -private: - OcppTimestamp timestamp; - std::vector> sampledValue; -public: - MeterValue(OcppTimestamp timestamp) : timestamp(timestamp) { } - MeterValue(const MeterValue& other) = delete; - - void addSampledValue(std::unique_ptr sample) {sampledValue.push_back(std::move(sample));} - - std::unique_ptr toJson(); -}; - -class MeterValueBuilder { -private: - const std::vector> &samplers; - std::shared_ptr> select; - std::vector select_mask; - unsigned int select_n {0}; - decltype(select->getValueRevision()) select_observe; - - void updateObservedSamplers(); -public: - MeterValueBuilder(const std::vector> &samplers, - std::shared_ptr> samplers_select); - - std::unique_ptr takeSample(const OcppTimestamp& timestamp, const ReadingContext& context); - - std::unique_ptr deserializeSample(const JsonObject mvJson); -}; - -} - -#endif diff --git a/src/ArduinoOcpp/Tasks/Metering/MeteringService.cpp b/src/ArduinoOcpp/Tasks/Metering/MeteringService.cpp deleted file mode 100644 index 1bc2acb8..00000000 --- a/src/ArduinoOcpp/Tasks/Metering/MeteringService.cpp +++ /dev/null @@ -1,133 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include - -using namespace ArduinoOcpp; - -MeteringService::MeteringService(OcppEngine& context, int numConn, std::shared_ptr filesystem) - : context(context), meterStore(filesystem) { - - for (int i = 0; i < numConn; i++) { - connectors.push_back(std::unique_ptr(new ConnectorMeterValuesRecorder(context.getOcppModel(), i, meterStore))); - } -} - -void MeteringService::loop(){ - - for (unsigned int i = 0; i < connectors.size(); i++){ - auto meterValuesMsg = connectors[i]->loop(); - if (meterValuesMsg != nullptr) { - auto meterValues = makeOcppOperation(meterValuesMsg); - meterValues->setTimeout(std::unique_ptr{new FixedTimeout(120000)}); - context.initiateOperation(std::move(meterValues)); - } - } -} - -void MeteringService::setPowerSampler(int connectorId, PowerSampler ps){ - if (connectorId < 0 || connectorId >= (int) connectors.size()) { - AO_DBG_ERR("connectorId is out of bounds"); - return; - } - connectors[connectorId]->setPowerSampler(ps); -} - -void MeteringService::setEnergySampler(int connectorId, EnergySampler es){ - if (connectorId < 0 || connectorId >= (int) connectors.size()) { - AO_DBG_ERR("connectorId is out of bounds"); - return; - } - connectors[connectorId]->setEnergySampler(es); -} - -void MeteringService::addMeterValueSampler(int connectorId, std::unique_ptr meterValueSampler) { - if (connectorId < 0 || connectorId >= (int) connectors.size()) { - AO_DBG_ERR("connectorId is out of bounds"); - return; - } - connectors[connectorId]->addMeterValueSampler(std::move(meterValueSampler)); -} - -std::unique_ptr MeteringService::readTxEnergyMeter(int connectorId, ReadingContext context) { - if (connectorId < 0 || (size_t) connectorId >= connectors.size()) { - AO_DBG_ERR("connectorId is out of bounds"); - return nullptr; - } - return connectors[connectorId]->readTxEnergyMeter(context); -} - -std::unique_ptr MeteringService::takeTriggeredMeterValues(int connectorId) { - if (connectorId < 0 || connectorId >= (int) connectors.size()) { - AO_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 = makeOcppOperation(msg); - meterValues->setTimeout(std::unique_ptr{new FixedTimeout(120000)}); - return meterValues; - } - AO_DBG_DEBUG("Did not take any samples for connectorId %d", connectorId); - return nullptr; - } - AO_DBG_ERR("Could not find connector"); - return nullptr; -} - -void MeteringService::beginTxMeterData(Transaction *transaction) { - if (!transaction) { - AO_DBG_ERR("invalid argument"); - return; - } - auto connectorId = transaction->getConnectorId(); - if (connectorId >= connectors.size()) { - AO_DBG_ERR("connectorId is out of bounds"); - return; - } - auto& connector = connectors[connectorId]; - - connector->beginTxMeterData(transaction); -} - -std::shared_ptr MeteringService::endTxMeterData(Transaction *transaction) { - if (!transaction) { - AO_DBG_ERR("invalid argument"); - return nullptr; - } - auto connectorId = transaction->getConnectorId(); - if (connectorId >= connectors.size()) { - AO_DBG_ERR("connectorId is out of bounds"); - return nullptr; - } - auto& connector = connectors[connectorId]; - - return connector->endTxMeterData(transaction); -} - -std::shared_ptr MeteringService::getStopTxMeterData(Transaction *transaction) { - if (!transaction) { - AO_DBG_ERR("invalid argument"); - return nullptr; - } - auto connectorId = transaction->getConnectorId(); - if (connectorId >= connectors.size()) { - AO_DBG_ERR("connectorId is out of bounds"); - return nullptr; - } - auto& connector = connectors[connectorId]; - - return connector->getStopTxMeterData(transaction); -} - -bool MeteringService::removeTxMeterData(unsigned int connectorId, unsigned int txNr) { - return meterStore.remove(connectorId, txNr); -} diff --git a/src/ArduinoOcpp/Tasks/Metering/MeteringService.h b/src/ArduinoOcpp/Tasks/Metering/MeteringService.h deleted file mode 100644 index 58390aea..00000000 --- a/src/ArduinoOcpp/Tasks/Metering/MeteringService.h +++ /dev/null @@ -1,58 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef METERINGSERVICE_H -#define METERINGSERVICE_H - -#include -#include -#include - -#include -#include -#include - -namespace ArduinoOcpp { - -using PowerSampler = std::function; //in Watts (W) -using EnergySampler = std::function; //in Watt-hours (Wh) - -class OcppEngine; -class OcppOperation; -class FilesystemAdapter; - -class MeteringService { -private: - OcppEngine& context; - MeterStore meterStore; - - std::vector> connectors; -public: - MeteringService(OcppEngine& context, int numConnectors, std::shared_ptr filesystem); - - void loop(); - - void setPowerSampler(int connectorId, PowerSampler powerSampler); - - void setEnergySampler(int connectorId, EnergySampler energySampler); - - void addMeterValueSampler(int connectorId, std::unique_ptr meterValueSampler); - - std::unique_ptr readTxEnergyMeter(int connectorId, ReadingContext reason); - - std::unique_ptr takeTriggeredMeterValues(int connectorId); //snapshot of all meters now - - void beginTxMeterData(Transaction *transaction); - - std::shared_ptr endTxMeterData(Transaction *transaction); //use return value to keep data in cache - - std::shared_ptr getStopTxMeterData(Transaction *transaction); //prefer endTxMeterData when possible - - bool removeTxMeterData(unsigned int connectorId, unsigned int txNr); - - int getNumConnectors() {return connectors.size();} -}; - -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/Tasks/Metering/SampledValue.cpp b/src/ArduinoOcpp/Tasks/Metering/SampledValue.cpp deleted file mode 100644 index d687e8d6..00000000 --- a/src/ArduinoOcpp/Tasks/Metering/SampledValue.cpp +++ /dev/null @@ -1,100 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include - -using ArduinoOcpp::SampledValue; - -//helper function -namespace ArduinoOcpp { -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: - AO_DBG_ERR("ReadingContext not specified"); - /* fall through */ - case (ReadingContext::NOT_SET): - return nullptr; - } -} -ReadingContext deserializeReadingContext(const char *context) { - if (!context) { - AO_DBG_ERR("Invalid argument"); - return ReadingContext::NOT_SET; - } - - if (!strcmp(context, "NOT_SET")) { - AO_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; - } - - AO_DBG_ERR("ReadingContext not specified %.10s", context); - return ReadingContext::NOT_SET; -} -}} //end namespaces - -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 - auto payload = result->to(); - payload["value"] = value; - auto context_cstr = Ocpp16::serializeReadingContext(context); - if (context_cstr) - payload["context"] = context_cstr; - if (!properties.getFormat().empty()) - payload["format"] = properties.getFormat(); - if (!properties.getMeasurand().empty()) - payload["measurand"] = properties.getMeasurand(); - if (!properties.getPhase().empty()) - payload["phase"] = properties.getPhase(); - if (!properties.getLocation().empty()) - payload["location"] = properties.getLocation(); - if (!properties.getUnit().empty()) - payload["unit"] = properties.getUnit(); - return result; -} diff --git a/src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingModel.cpp b/src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingModel.cpp deleted file mode 100644 index 1a4b469e..00000000 --- a/src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingModel.cpp +++ /dev/null @@ -1,423 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include - -#include -#include - -using namespace ArduinoOcpp; - -ChargingSchedulePeriod::ChargingSchedulePeriod(JsonObject &json){ - startPeriod = json["startPeriod"]; - limit = json["limit"]; //one fractural digit at most - numberPhases = json["numberPhases"] | -1; -} - -ChargingSchedulePeriod::ChargingSchedulePeriod(int startPeriod, float limit){ - this->startPeriod = startPeriod; - this->limit = limit; - numberPhases = -1; //support later -} - -int ChargingSchedulePeriod::getStartPeriod(){ - return this->startPeriod; -} - -float ChargingSchedulePeriod::getLimit(){ - return this->limit; -} - -void ChargingSchedulePeriod::scale(float factor){ - limit *= factor; - if (limit < 0.f) - limit *= -1.f; -} - -void ChargingSchedulePeriod::add(float value){ - limit += value; - if (limit < 0.f) - limit = 0; -} - -int ChargingSchedulePeriod::getNumberPhases(){ - return this->numberPhases; -} - -void ChargingSchedulePeriod::printPeriod(){ - AO_DBG_VERBOSE("CHARGING SCHEDULE PERIOD:\n" \ - " startPeriod: %i\n" \ - " limit: %f\n" \ - " numberPhases: %i\n", - startPeriod, - limit, - numberPhases); -} - -ChargingSchedule::ChargingSchedule(JsonObject &json, ChargingProfileKindType chargingProfileKind, RecurrencyKindType recurrencyKind) - : chargingProfileKind(chargingProfileKind) - , recurrencyKind(recurrencyKind) { - - duration = json["duration"] | -1; - startSchedule = OcppTimestamp(); - if (!startSchedule.setTime(json["startSchedule"] | "Invalid")) { - //non-success - startSchedule = MIN_TIME; - } - const char *unit = json["chargingRateUnit"] | "__Invalid"; - if (unit[0] == 'a' || unit[0] == 'A') { - chargingRateUnit = ChargingRateUnitType::Amp; - } else { - chargingRateUnit = ChargingRateUnitType::Watt; - } - - JsonArray periodJsonArray = json["chargingSchedulePeriod"]; - for (JsonObject periodJson : periodJsonArray) { - auto period = std::unique_ptr(new ChargingSchedulePeriod(periodJson)); - chargingSchedulePeriod.push_back(std::move(period)); - } - - //Expecting sorted list of periods but specification doesn't garantuee it - std::sort(chargingSchedulePeriod.begin(), chargingSchedulePeriod.end(), - [] (const std::unique_ptr &p1, const std::unique_ptr &p2) { - return p1->getStartPeriod() < p2->getStartPeriod(); - }); - - minChargingRate = json["minChargingRate"] | -1.0f; -} - -ChargingSchedule::ChargingSchedule(ChargingSchedule &other) { - chargingProfileKind = other.chargingProfileKind; - recurrencyKind = other.recurrencyKind; - duration = other.duration; - startSchedule = other.startSchedule; - chargingRateUnit = other.chargingRateUnit; - - for (auto period = other.chargingSchedulePeriod.begin(); period != other.chargingSchedulePeriod.end(); period++) { - auto p = std::unique_ptr(new ChargingSchedulePeriod(**period)); - chargingSchedulePeriod.push_back(std::move(p)); - } - - //Expecting sorted list of periods but specification doesn't garantuee it - std::sort(chargingSchedulePeriod.begin(), chargingSchedulePeriod.end(), - [] (const std::unique_ptr &p1, const std::unique_ptr &p2) { - return p1->getStartPeriod() < p2->getStartPeriod(); - }); - - minChargingRate = other.minChargingRate; -} - -ChargingSchedule::ChargingSchedule(const OcppTimestamp &startT, int duration) { - //create empty but valid Charging Schedule - this->duration = duration; - startSchedule = startT; - chargingRateUnit = ChargingRateUnitType::Watt; - //float minChargingRate = 0.f; - - chargingProfileKind = ChargingProfileKindType::Absolute; //copied from ChargingProfile to increase cohesion of limit inferencing methods - recurrencyKind = RecurrencyKindType::NOT_SET; //copied from ChargingProfile to increase cohesion of limit inferencing methods -} - -bool ChargingSchedule::inferenceLimit(const OcppTimestamp &t, const OcppTimestamp &startOfCharging, float *limit, OcppTimestamp *nextChange) { - OcppTimestamp basis = OcppTimestamp(); //point in time to which schedule-related times are relative - *nextChange = MAX_TIME; //defaulted to Infinity - switch (chargingProfileKind) { - case (ChargingProfileKindType::Absolute): - //check if schedule is not valid yet but begins in future - if (startSchedule > t) { - //not valid YET - *nextChange = startSchedule; - return false; - } - //If charging profile is absolute, prefer startSchedule as basis. If absent, use chargingStart instead. If absent, no - //behaviour is defined - if (startSchedule > MIN_TIME) { - basis = startSchedule; - } else if (startOfCharging > MIN_TIME && startOfCharging < t) { - basis = startOfCharging; - } else { - AO_DBG_ERR("Absolute profile, but neither startSchedule, nor start of charging are set. Undefined behavior, abort"); - return false; - } - break; - case (ChargingProfileKindType::Recurring): - if (recurrencyKind == RecurrencyKindType::Daily) { - basis = t - ((t - startSchedule) % (24 * 3600)); - *nextChange = basis + (24 * 3600); //constrain nextChange to basis + one day - } else if (recurrencyKind == RecurrencyKindType::Weekly) { - basis = t - ((t - startSchedule) % (7 * 24 * 3600)); - *nextChange = basis + (7 * 24 * 3600); - } else { - AO_DBG_ERR("Recurring ChargingProfile but no RecurrencyKindType set. Undefined behavior, assume 'Daily'"); - basis = t - ((t - startSchedule) % (24 * 3600)); - *nextChange = basis + (24 * 3600); - } - break; - case (ChargingProfileKindType::Relative): - //assumed, that it is relative to start of charging - //start of charging must be before t or equal to t - if (startOfCharging > t) { - //Relative charging profiles only work with a currently active charging session which is not the case here - return false; - } - basis = startOfCharging; - break; - } - - if (t < basis) { //check for error - AO_DBG_ERR("time basis is smaller than t, but t must be >= basis"); - return false; - } - - int t_toBasis = t - basis; - - if (duration > 0){ - //duration is set - - //check if duration is exceeded and if yes, abort inferencing limit - //if no, the duration is an upper limit for the validity of the schedule - if (t_toBasis >= duration) { //"duration" is given relative to basis - return false; - } else { - if (*nextChange - basis > duration) { - *nextChange = basis + duration; - } - } - } - - /* - * Work through the ChargingProfilePeriods here. If the right period was found, assign the limit parameter from it - * and make nextChange equal the beginning of the following period. If the right period is the last one, nextChange - * will remain the time determined before. - */ - float limit_res = -1.0f; //If limit_res is still -1 after the loop, the inference process failed - for (auto period = chargingSchedulePeriod.begin(); period != chargingSchedulePeriod.end(); period++) { - if ((*period)->getStartPeriod() > t_toBasis) { - // found the first period that comes after t_toBasis. - *nextChange = basis + (*period)->getStartPeriod(); - break; //The currently valid limit was set the iteration before - } - limit_res = (*period)->getLimit(); - - } - - if (limit_res >= 0.0f) { - *limit = std::max(limit_res, minChargingRate); - return true; - } else { - return false; //No limit was found. Either there is no ChargingProfilePeriod, or each period begins after t_toBasis - } -} - -bool ChargingSchedule::addChargingSchedulePeriod(std::unique_ptr period) { - if (!period) return false; - - if (period->getStartPeriod() >= duration) { - return false; - } - - chargingSchedulePeriod.push_back(std::move(period)); - return true; -} - -void ChargingSchedule::scale(float factor) { - for (auto p = chargingSchedulePeriod.begin(); p != chargingSchedulePeriod.end(); p++) { - (*p)->scale(factor); - } -} - -void ChargingSchedule::translate(float offset) { - for (auto p = chargingSchedulePeriod.begin(); p != chargingSchedulePeriod.end(); p++) { - (*p)->add(offset); - } -} - -DynamicJsonDocument *ChargingSchedule::toJsonDocument() { - 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); - - DynamicJsonDocument *result = new DynamicJsonDocument(capacity); - JsonObject payload = result->to(); - if (duration >= 0) - payload["duration"] = duration; - char startScheduleJson [JSONDATE_LENGTH + 1] = {'\0'}; - startSchedule.toJsonString(startScheduleJson, JSONDATE_LENGTH + 1); - payload["startSchedule"] = startScheduleJson; - payload["chargingRateUnit"] = chargingRateUnit == (ChargingRateUnitType::Amp) ? "A" : "W"; - JsonArray periodArray = payload.createNestedArray("chargingSchedulePeriod"); - for (auto period = chargingSchedulePeriod.begin(); period != chargingSchedulePeriod.end(); period++) { - JsonObject entry = periodArray.createNestedObject(); - entry["startPeriod"] = (*period)->getStartPeriod(); - entry["limit"] = (*period)->getLimit(); - if ((*period)->getNumberPhases() >= 0) { - entry["numberPhases"] = (*period)->getNumberPhases(); - } - } - if (minChargingRate >= 0) - payload["minChargeRate"] = minChargingRate; - - return result; -} - -void ChargingSchedule::printSchedule(){ - - char tmp[JSONDATE_LENGTH + 1] = {'\0'}; - startSchedule.toJsonString(tmp, JSONDATE_LENGTH + 1); - - AO_DBG_VERBOSE("CHARGING SCHEDULE:\n" \ - " duration: %i\n" \ - " startSchedule: %s\n" \ - " chargingRateUnit: %s\n" \ - " minChargingRate: %f\n", - duration, - tmp, - chargingRateUnit == (ChargingRateUnitType::Amp) ? "A" : - chargingRateUnit == (ChargingRateUnitType::Watt) ? "W" : "Error", - minChargingRate); - - for (auto period = chargingSchedulePeriod.begin(); period != chargingSchedulePeriod.end(); period++) { - (*period)->printPeriod(); - } -} - -ChargingProfile::ChargingProfile(JsonObject &json){ - - chargingProfileId = json["chargingProfileId"] | -1; - transactionId = json["transactionId"] | -1; - stackLevel = json["stackLevel"] | 0; - - const char *chargingProfilePurposeStr = json["chargingProfilePurpose"] | "Invalid"; - if (!strcmp(chargingProfilePurposeStr, "ChargePointMaxProfile")) { - chargingProfilePurpose = ChargingProfilePurposeType::ChargePointMaxProfile; - } else if (!strcmp(chargingProfilePurposeStr, "TxDefaultProfile")) { - chargingProfilePurpose = ChargingProfilePurposeType::TxDefaultProfile; - //} else if (!strcmp(chargingProfilePurposeStr, "TxProfile")) { - } else { - chargingProfilePurpose = ChargingProfilePurposeType::TxProfile; - } - const char *chargingProfileKindStr = json["chargingProfileKind"] | "Invalid"; - if (!strcmp(chargingProfileKindStr, "Absolute")) { - chargingProfileKind = ChargingProfileKindType::Absolute; - } else if (!strcmp(chargingProfileKindStr, "Recurring")) { - chargingProfileKind = ChargingProfileKindType::Recurring; - //} else if (!strcmp(chargingProfileKindStr, "Relative")) { - } else { - chargingProfileKind = ChargingProfileKindType::Relative; - } - const char *recurrencyKindStr = json["recurrencyKind"] | "Invalid"; - if (!strcmp(recurrencyKindStr, "Daily")) { - recurrencyKind = RecurrencyKindType::Daily; - } else if (!strcmp(recurrencyKindStr, "Weekly")) { - recurrencyKind = RecurrencyKindType::Weekly; - } else { - recurrencyKind = RecurrencyKindType::NOT_SET; //not part of OCPP 1.6 - } - - AO_DBG_DEBUG("Deserialize JSON: chargingProfileId=%i, chargingProfilePurpose=%s, recurrencyKind=%s", chargingProfileId, chargingProfilePurposeStr, recurrencyKindStr); - - if (!validFrom.setTime(json["validFrom"] | "Invalid")) { - //non-success - AO_DBG_DEBUG("validFrom undefined. Expect format like 2022-02-01T20:53:32.486Z. Assume unlimited validity"); - validFrom = MIN_TIME; - } - - if (!validTo.setTime(json["validTo"] | "Invalid")) { - //non-success - AO_DBG_DEBUG("validTo undefined. Expect format like 2022-02-01T20:53:32.486Z. Assume unlimited validity"); - validTo = MIN_TIME; - } - - JsonObject schedule = json["chargingSchedule"]; - chargingSchedule = std::unique_ptr(new ChargingSchedule(schedule, chargingProfileKind, recurrencyKind)); -} - -bool ChargingProfile::inferenceLimit(const OcppTimestamp &t, const OcppTimestamp &startOfCharging, float *limit, OcppTimestamp *nextChange){ - if (t > validTo && validTo > MIN_TIME) { - *nextChange = MAX_TIME; - return false; //no limit defined - } - if (t < validFrom) { - *nextChange = validFrom; - return false; //no limit defined - } - - return chargingSchedule->inferenceLimit(t, startOfCharging, limit, nextChange); -} - -bool ChargingProfile::inferenceLimit(const OcppTimestamp &t, float *limit, OcppTimestamp *nextChange){ - return inferenceLimit(t, MAX_TIME, limit, nextChange); -} - -bool ChargingProfile::checkTransactionAssignment(int txId, int profileId) { - if (chargingProfilePurpose != ChargingProfilePurposeType::TxProfile) { - AO_DBG_ERR("assignment only exists for TxProfiles"); - return true; //does not apply to this profile -> no restriction - } - - if (txId <= 0 && profileId < 0) { - //no search parameters set - return true; - } - - if (chargingProfileId >= 0 && profileId >= 0) { //profileIDs are valid - return chargingProfileId == profileId; //return if they match (case of remote charging profiles) - } - - if (transactionId > 0 && txId > 0) { //txIDs are valid - return transactionId == txId; //return if they do match - } - - AO_DBG_DEBUG("Neither txIds nor profileIDs apply"); - return true; -} - -int ChargingProfile::getStackLevel(){ - return stackLevel; -} - -ChargingProfilePurposeType ChargingProfile::getChargingProfilePurpose(){ - return chargingProfilePurpose; -} - -int ChargingProfile::getChargingProfileId() { - return chargingProfileId; -} - -void ChargingProfile::printProfile(){ - - char tmp[JSONDATE_LENGTH + 1] = {'\0'}; - validFrom.toJsonString(tmp, JSONDATE_LENGTH + 1); - char tmp2[JSONDATE_LENGTH + 1] = {'\0'}; - validTo.toJsonString(tmp2, JSONDATE_LENGTH + 1); - - AO_DBG_VERBOSE("CHARGING PROFILE:\n" \ - " chargingProfileId: %i\n" \ - " transactionId: %i\n" \ - " stackLevel: %i\n" \ - " chargingProfilePurpose: %s\n" \ - " chargingProfileKind: %s\n" \ - " recurrencyKind: %s\n" \ - " validFrom: %s\n" \ - " validTo: %s\n", - chargingProfileId, - transactionId, - stackLevel, - chargingProfilePurpose == (ChargingProfilePurposeType::ChargePointMaxProfile) ? "ChargePointMaxProfile" : - chargingProfilePurpose == (ChargingProfilePurposeType::TxDefaultProfile) ? "TxDefaultProfile" : - chargingProfilePurpose == (ChargingProfilePurposeType::TxProfile) ? "TxProfile" : "Error", - chargingProfileKind == (ChargingProfileKindType::Absolute) ? "Absolute" : - chargingProfileKind == (ChargingProfileKindType::Recurring) ? "Recurring" : - chargingProfileKind == (ChargingProfileKindType::Relative) ? "Relative" : "Error", - recurrencyKind == (RecurrencyKindType::Daily) ? "Daily" : - recurrencyKind == (RecurrencyKindType::Weekly) ? "Weekly" : - recurrencyKind == (RecurrencyKindType::NOT_SET) ? "NOT_SET" : "Error", - tmp, - tmp2 - ); - - chargingSchedule->printSchedule(); -} diff --git a/src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingModel.h b/src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingModel.h deleted file mode 100644 index 2cb8b37d..00000000 --- a/src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingModel.h +++ /dev/null @@ -1,139 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef SMARTCHARGINGMODEL_H -#define SMARTCHARGINGMODEL_H - -#include -#include -#include -#include - -namespace ArduinoOcpp { - -enum class ChargingProfilePurposeType { - ChargePointMaxProfile, - TxDefaultProfile, - TxProfile -}; - -enum class ChargingProfileKindType { - Absolute, - Recurring, - Relative -}; - -enum class RecurrencyKindType { - NOT_SET, //not part of OCPP 1.6 - Daily, - Weekly -}; - -enum class ChargingRateUnitType { - Watt, - Amp -}; - -class ChargingSchedulePeriod { -private: - int startPeriod; - float limit; //one fractural digit at most - int numberPhases = -1; -public: - ChargingSchedulePeriod(JsonObject &json); - ChargingSchedulePeriod(int startPeriod, float limit); - int getStartPeriod(); - float getLimit(); - void scale(float factor); - void add(float value); - int getNumberPhases(); - void printPeriod(); -}; - -class ChargingSchedule { -private: - int duration = -1; - OcppTimestamp startSchedule; - ChargingRateUnitType chargingRateUnit; - std::vector> chargingSchedulePeriod; - float minChargingRate = -1.0f; - - ChargingProfileKindType chargingProfileKind; //copied from ChargingProfile to increase cohesion of limit inferencing methods - RecurrencyKindType recurrencyKind; //copied from ChargingProfile to increase cohesion of limit inferencing methods -public: - ChargingSchedule(JsonObject &json, ChargingProfileKindType chargingProfileKind, RecurrencyKindType recurrencyKind); - ChargingSchedule(ChargingSchedule &other); - ChargingSchedule(const OcppTimestamp &startSchedule, int duration); - - /** - * limit: output parameter - * nextChange: output parameter - * - * returns if charging profile defines a limit at time t - * if true, limit and nextChange will be set according to this Schedule - * if false, only nextChange will be set - */ - bool inferenceLimit(const OcppTimestamp &t, const OcppTimestamp &startOfCharging, float *limit, OcppTimestamp *nextChange); - - bool addChargingSchedulePeriod(std::unique_ptr period); - - void scale(float factor); - void translate(float offset); - - DynamicJsonDocument *toJsonDocument(); - - /* - * print on console - */ - void printSchedule(); -}; - -class ChargingProfile { -private: - int chargingProfileId = -1; - int transactionId = -1; - int stackLevel = 0; - ChargingProfilePurposeType chargingProfilePurpose {ChargingProfilePurposeType::TxProfile}; - ChargingProfileKindType chargingProfileKind {ChargingProfileKindType::Relative}; //copied to ChargingSchedule to increase cohesion of limit inferencing methods - RecurrencyKindType recurrencyKind {RecurrencyKindType::NOT_SET}; // copied to ChargingSchedule to increase cohesion - OcppTimestamp validFrom; - OcppTimestamp validTo; - std::unique_ptr chargingSchedule; -public: - ChargingProfile(JsonObject &json); - - /** - * limit: output parameter - * nextChange: output parameter - * - * returns if charging profile defines a limit at time t - * if true, limit and nextChange will be set according to this Schedule - * if false, only nextChange will be set - */ - bool inferenceLimit(const OcppTimestamp &t, const OcppTimestamp &startOfCharging, float *limit, OcppTimestamp *nextChange); - - /* - * Simpler function if startOfCharging is not available. Caution: This likely will differ from inference with startOfCharging - */ - bool inferenceLimit(const OcppTimestamp &t, float *limit, OcppTimestamp *nextChange); - - /* - * Check if this profile belongs to transaction with ID txId or idTag alternatively - */ - bool checkTransactionAssignment(int txId, int profileId); - - int getStackLevel(); - - ChargingProfilePurposeType getChargingProfilePurpose(); - - int getChargingProfileId(); - - /* - * print on console - */ - void printProfile(); -}; - -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingService.cpp b/src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingService.cpp deleted file mode 100644 index 23d50a71..00000000 --- a/src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingService.cpp +++ /dev/null @@ -1,575 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include - -#if !defined(AO_DEACTIVATE_FLASH) && defined(AO_DEACTIVATE_FLASH_SMARTCHARGING) -#define AO_DEACTIVATE_FLASH -#endif - -#ifndef AO_DEACTIVATE_FLASH -#include -#if AO_USE_FILEAPI == ESPIDF_SPIFFS -#define AO_DEACTIVATE_FLASH //migrate to File utils -#endif -#endif - -#ifndef AO_DEACTIVATE_FLASH -#if defined(ESP32) -#include -#define USE_FS LITTLEFS -#else -#include -#define USE_FS SPIFFS -#endif -#endif - -#define SINGLE_CONNECTOR_ID 1 - -#define PROFILE_FN_PREFIX "/ocpp-" -#define PROFILE_FN_SUFFIX ".cnf" -#define PROFILE_FN_MAXSIZE 30 -#define PROFILE_CUSTOM_CAPACITY 500 -#define PROFILE_MAX_CAPACITY 4000 - -using namespace::ArduinoOcpp; - -SmartChargingService::SmartChargingService(OcppEngine& context, float chargeLimit, float V_eff, int numConnectors, FilesystemOpt filesystemOpt) - : context(context), DEFAULT_CHARGE_LIMIT{chargeLimit}, V_eff{V_eff}, filesystemOpt{filesystemOpt} { - - if (numConnectors > 2) { - AO_DBG_ERR("Only one connector supported at the moment"); - } - - limitBeforeChange = -1.0f; - nextChange = MIN_TIME; - chargingSessionStart = MAX_TIME; - char max_timestamp [JSONDATE_LENGTH + 1] = {'\0'}; - chargingSessionStart.toJsonString(max_timestamp, JSONDATE_LENGTH + 1); - txStartTime = declareConfiguration("AO_TXSTARTTIME_CONN_1", max_timestamp, CONFIGURATION_FN, false, false, true, false); - chargingSessionTransactionID = -1; - sRmtProfileId = declareConfiguration("AO_SRMTPROFILEID_CONN_1", -1, CONFIGURATION_FN, false, false, true, false); - for (int i = 0; i < CHARGEPROFILEMAXSTACKLEVEL; i++) { - ChargePointMaxProfile[i] = NULL; - TxDefaultProfile[i] = NULL; - TxProfile[i] = NULL; - } - declareConfiguration("ChargeProfileMaxStackLevel", CHARGEPROFILEMAXSTACKLEVEL, CONFIGURATION_VOLATILE, false, true, false, false); - declareConfiguration("ChargingScheduleAllowedChargingRateUnit ", "Power", CONFIGURATION_VOLATILE, false, true, false, false); - declareConfiguration("ChargingScheduleMaxPeriods", CHARGINGSCHEDULEMAXPERIODS, CONFIGURATION_VOLATILE, false, true, false, false); - declareConfiguration("MaxChargingProfilesInstalled", MAXCHARGINGPROFILESINSTALLED, CONFIGURATION_VOLATILE, false, true, false, false); - - const char *fpId = "SmartCharging"; - auto fProfile = declareConfiguration("SupportedFeatureProfiles",fpId, CONFIGURATION_VOLATILE, false, true, true, false); - if (!strstr(*fProfile, fpId)) { - auto fProfilePlus = std::string(*fProfile); - if (!fProfilePlus.empty() && fProfilePlus.back() != ',') - fProfilePlus += ","; - fProfilePlus += fpId; - fProfile->setValue(fProfilePlus.c_str(), fProfilePlus.length() + 1); - } - - loadProfiles(); -} - -void SmartChargingService::loop(){ - - refreshChargingSessionState(); - - /** - * check if to call onLimitChange - */ - if (context.getOcppModel().getOcppTime().getOcppTimestampNow() >= nextChange){ - auto& tNow = context.getOcppModel().getOcppTime().getOcppTimestampNow(); - float limit = -1.0f; - OcppTimestamp validTo = OcppTimestamp(); - inferenceLimit(tNow, &limit, &validTo); - -#if (AO_DBG_LEVEL >= AO_DL_INFO) - char timestamp1[JSONDATE_LENGTH + 1] = {'\0'}; - nextChange.toJsonString(timestamp1, JSONDATE_LENGTH + 1); - char timestamp2[JSONDATE_LENGTH + 1] = {'\0'}; - validTo.toJsonString(timestamp2, JSONDATE_LENGTH + 1); - AO_DBG_INFO("New limit for connector 1, scheduled at = %s, nextChange = %s, limit = %f", - timestamp1, timestamp2, limit); -#endif - - nextChange = validTo; - if (limit != limitBeforeChange){ - if (onLimitChange != NULL) { - onLimitChange(limit); - } - } - limitBeforeChange = limit; - } -} - -float SmartChargingService::inferenceLimitNow(){ - float limit = 0.0f; - OcppTimestamp validTo = OcppTimestamp(); //not needed - auto& tNow = context.getOcppModel().getOcppTime().getOcppTimestampNow(); - inferenceLimit(tNow, &limit, &validTo); - return limit; -} - -void SmartChargingService::setOnLimitChange(OnLimitChange onLtChg){ - onLimitChange = onLtChg; -} - -/** - * validToOutParam: The begin of the next SmartCharging restriction after time t. It is not taken into - * account if the next Profile will be a prevailing one. If the profile at time t ends before any - * other profile engages, the end of this profile will be written into validToOutParam. - */ -void SmartChargingService::inferenceLimit(const OcppTimestamp &t, float *limitOutParam, OcppTimestamp *validToOutParam){ - OcppTimestamp validToMin = MAX_TIME; - /* - * TxProfile rules over TxDefaultProfile. ChargePointMaxProfile rules over both of them - * - * if (TxProfile is present) - * take limit from TxProfile with the highest stackLevel - * else - * take limit from TxDefaultProfile with the highest stackLevel - * take maximum from ChargePointMaxProfile with the highest stackLevel - * return minimum(limit, maximum from ChargePointMaxProfile) - */ - - //evaluate limit from TxProfiles - float limit_tx = 0.0f; - bool limit_defined_tx = false; - for (int i = CHARGEPROFILEMAXSTACKLEVEL - 1; i >= 0; i--) { - if (!TxProfile[i]) - continue; - if (!TxProfile[i]->checkTransactionAssignment(chargingSessionTransactionID, *sRmtProfileId)) - continue; - OcppTimestamp nextChange = MAX_TIME; - limit_defined_tx = TxProfile[i]->inferenceLimit(t, chargingSessionStart, &limit_tx, &nextChange); - if (nextChange < validToMin) - validToMin = nextChange; //nextChange is always >= t here - if (limit_defined_tx) { - //The valid profile with the highest stack level is found. It prevails over any other so end this loop. - break; - } - } - - //evaluate limit from TxDefaultProfiles - float limit_txdef = 0.0f; - bool limit_defined_txdef = false; - for (int i = CHARGEPROFILEMAXSTACKLEVEL - 1; i >= 0; i--){ - if (TxDefaultProfile[i] == NULL) continue; - OcppTimestamp nextChange = MAX_TIME; - limit_defined_txdef = TxDefaultProfile[i]->inferenceLimit(t, chargingSessionStart, &limit_txdef, &nextChange); - if (nextChange < validToMin) - validToMin = nextChange; //nextChange is always >= t here - if (limit_defined_txdef) { - //The valid profile with the highest stack level is found. It prevails over any other so end this loop. - break; - } - } - - //evaluate limit from ChargePointMaxProfile - float limit_cpmax = 0.0f; - bool limit_defined_cpmax = false; - for (int i = CHARGEPROFILEMAXSTACKLEVEL - 1; i >= 0; i--){ - if (ChargePointMaxProfile[i] == NULL) continue; - OcppTimestamp nextChange = MAX_TIME; - limit_defined_cpmax = ChargePointMaxProfile[i]->inferenceLimit(t, chargingSessionStart, &limit_cpmax, &nextChange); - if (nextChange < validToMin) - validToMin = nextChange; //nextChange is always >= t here - if (limit_defined_cpmax) { - //The valid profile with the highest stack level is found. It prevails over any other so end this loop. - break; - } - } - - *validToOutParam = validToMin; //validTo output parameter has successfully been determined here - - //choose which limit to set according to specification - bool applicable_profile_found = false; - if (limit_defined_txdef){ - *limitOutParam = limit_txdef; - applicable_profile_found = true; - } - if (limit_defined_tx){ - *limitOutParam = limit_tx; - applicable_profile_found = true; - } - if (limit_defined_cpmax){ - //Warning: This block MUST be rewritten when multiple connector support is introduced - if (applicable_profile_found) { - if (limit_cpmax < *limitOutParam){ - //TxProfile or TxDefaultProfile exceeds the maximum for the whole CP - *limitOutParam = limit_cpmax; - } //else: TxProfile or TxDefaultProfile are within their boundary. Do nothing - } else { - //No TxProfile or TxDefaultProfile found. The limit is set to the maximum of the CP - *limitOutParam = limit_cpmax; - } - applicable_profile_found = true; - } - if (!applicable_profile_found) { - *limitOutParam = DEFAULT_CHARGE_LIMIT; - } -} - -ChargingSchedule *SmartChargingService::getCompositeSchedule(int connectorId, otime_t duration){ - auto& startSchedule = context.getOcppModel().getOcppTime().getOcppTimestampNow(); - ChargingSchedule *result = new ChargingSchedule(startSchedule, duration); - OcppTimestamp periodBegin = OcppTimestamp(startSchedule); - OcppTimestamp periodStop = OcppTimestamp(startSchedule); - while (periodBegin - startSchedule < duration) { - float limit = 0.f; - inferenceLimit(periodBegin, &limit, &periodStop); - auto p = std::unique_ptr(new ChargingSchedulePeriod(periodBegin - startSchedule, limit)); - if (!result->addChargingSchedulePeriod(std::move(p))) { - break; - } - periodBegin = periodStop; - } - return result; -} - -void SmartChargingService::refreshChargingSessionState() { - if (!context.getOcppModel().getConnectorStatus(SINGLE_CONNECTOR_ID)) { - return; //charging session state does not apply - } - - auto connector = context.getOcppModel().getConnectorStatus(SINGLE_CONNECTOR_ID); - - if (!chargingSessionStateInitialized) { - chargingSessionStateInitialized = true; - - chargingSessionStart.setTime(*txStartTime); - chargingSessionTransactionID = connector->getTransactionId(); - sessionIdTagRev = connector->getSessionWriteCount(); - sRmtProfileIdRev = sRmtProfileId->getValueRevision(); - - //fuzzy check if session engaged at reboot (during first loop run) - auto chargingSessionStartCheck = MAX_TIME; - chargingSessionStartCheck -= 1000000; - if (chargingSessionStart >= chargingSessionStartCheck) { - //charging session start lies in future -> null-value -> no charging session before reboot - chargingSessionTransactionID = -1; - } - } - - if (connector->getTransactionId() != chargingSessionTransactionID) { - //transition! - - bool txStartUpdated = false; - if (chargingSessionTransactionID != 0 && connector->getTransactionId() >= 0) { - chargingSessionStart = context.getOcppModel().getOcppTime().getOcppTimestampNow(); - txStartUpdated = true; - } else if (chargingSessionTransactionID >= 0 && connector->getTransactionId() < 0) { - chargingSessionStart = MAX_TIME; - txStartUpdated = true; - } - - if (txStartUpdated) { - char timestamp [JSONDATE_LENGTH + 1] = {'\0'}; - chargingSessionStart.toJsonString(timestamp, JSONDATE_LENGTH + 1); - *txStartTime = timestamp; - configuration_save(); - } - - nextChange = context.getOcppModel().getOcppTime().getOcppTimestampNow(); - } - - if (*sRmtProfileId >= 0 && //Remote profile set? Check if to delete - (!connector->getSessionIdTag() //Always delete Rmt profile if there is no session - || (sessionIdTagRev != connector->getSessionWriteCount() && sRmtProfileIdRev == sRmtProfileId->getValueRevision()))) { - //Alternaternively delete if session state has been overwritten - - //after RemoteTx session expired, clean charging profile - int clearProfileId = *sRmtProfileId; - bool ret = clearChargingProfile([clearProfileId] (int id, int, ChargingProfilePurposeType, int) { - return id == clearProfileId; - }); - (void)ret; - - AO_DBG_DEBUG("Clearing RmtTx Charging Profile after session expiry: %s", ret ? "success" : "already cleared"); - - *sRmtProfileId = -1; - } - - chargingSessionTransactionID = connector->getTransactionId(); - sessionIdTagRev = connector->getSessionWriteCount(); - sRmtProfileIdRev = sRmtProfileId->getValueRevision(); -} - -void SmartChargingService::setChargingProfile(JsonObject json) { - ChargingProfile *pointer = updateProfileStack(json); - if (pointer) - writeProfileToFlash(json, pointer); -} - -ChargingProfile *SmartChargingService::updateProfileStack(JsonObject json){ - ChargingProfile *chargingProfile = new ChargingProfile(json); - - if (AO_DBG_LEVEL >= AO_DL_VERBOSE) { - AO_DBG_VERBOSE("Charging Profile internal model:"); - chargingProfile->printProfile(); - } - - int stackLevel = chargingProfile->getStackLevel(); - if (stackLevel >= CHARGEPROFILEMAXSTACKLEVEL || stackLevel < 0) { - AO_DBG_ERR("Stacklevel of Charging Profile is smaller or greater than CHARGEPROFILEMAXSTACKLEVEL"); - stackLevel = CHARGEPROFILEMAXSTACKLEVEL - 1; - } - - ChargingProfile **profilePurposeStack; //select which stack this profile belongs to due to its purpose - - switch (chargingProfile->getChargingProfilePurpose()) { - case (ChargingProfilePurposeType::TxDefaultProfile): - profilePurposeStack = TxDefaultProfile; - break; - case (ChargingProfilePurposeType::TxProfile): - profilePurposeStack = TxProfile; - break; - default: - //case (ChargingProfilePurposeType::ChargePointMaxProfile): - profilePurposeStack = ChargePointMaxProfile; - break; - } - - if (profilePurposeStack[stackLevel] != NULL){ - delete profilePurposeStack[stackLevel]; - } - - profilePurposeStack[stackLevel] = chargingProfile; - - /** - * Invalidate the last limit inference by setting the nextChange to now. By the next loop()-call, the limit - * and nextChange will be recalculated and onLimitChanged will be called. - */ - nextChange = context.getOcppModel().getOcppTime().getOcppTimestampNow(); - - return chargingProfile; -} - -bool SmartChargingService::clearChargingProfile(const std::function& filter) { - int nMatches = 0; - - ChargingProfile **profileStacks [] = {ChargePointMaxProfile, TxDefaultProfile, TxProfile}; - - for (ChargingProfile **profileStack : profileStacks) { - for (int iLevel = 0; iLevel < CHARGEPROFILEMAXSTACKLEVEL; iLevel++) { - ChargingProfile *chargingProfile = profileStack[iLevel]; - if (chargingProfile == NULL) - continue; - - // -1: multiple connectors are not supported yet for Smart Charging - bool tbCleared = filter(chargingProfile->getChargingProfileId(), -1, chargingProfile->getChargingProfilePurpose(), iLevel); - - if (tbCleared) { - nMatches++; - -#ifndef AO_DEACTIVATE_FLASH - if (filesystemOpt.accessAllowed()) { - char fn [PROFILE_FN_MAXSIZE] = {'\0'}; - - switch (chargingProfile->getChargingProfilePurpose()) { - case (ChargingProfilePurposeType::ChargePointMaxProfile): - snprintf(fn, PROFILE_FN_MAXSIZE, PROFILE_FN_PREFIX "CpMaxProfile-%d" PROFILE_FN_SUFFIX, chargingProfile->getStackLevel()); - break; - case (ChargingProfilePurposeType::TxDefaultProfile): - snprintf(fn, PROFILE_FN_MAXSIZE, PROFILE_FN_PREFIX "TxDefProfile-%d" PROFILE_FN_SUFFIX, chargingProfile->getStackLevel()); - break; - case (ChargingProfilePurposeType::TxProfile): - snprintf(fn, PROFILE_FN_MAXSIZE, PROFILE_FN_PREFIX "TxProfile-%d" PROFILE_FN_SUFFIX, chargingProfile->getStackLevel()); - break; - } - - if (USE_FS.exists(fn)) { - USE_FS.remove(fn); - } - } else { - AO_DBG_DEBUG("Prohibit access to FS"); - } -#endif - delete chargingProfile; - profileStack[iLevel] = nullptr; - } - } - } - - /** - * Invalidate the last limit inference by setting the nextChange to now. By the next loop()-call, the limit - * and nextChange will be recalculated and onLimitChanged will be called. - */ - nextChange = context.getOcppModel().getOcppTime().getOcppTimestampNow(); - - return nMatches > 0; -} - -bool SmartChargingService::writeProfileToFlash(JsonObject json, ChargingProfile *chargingProfile) { -#ifndef AO_DEACTIVATE_FLASH - - if (!filesystemOpt.accessAllowed()) { - AO_DBG_DEBUG("Prohibit access to FS"); - return true; - } - - char fn [PROFILE_FN_MAXSIZE] = {'\0'}; - - switch (chargingProfile->getChargingProfilePurpose()) { - case (ChargingProfilePurposeType::ChargePointMaxProfile): - snprintf(fn, PROFILE_FN_MAXSIZE, PROFILE_FN_PREFIX "CpMaxProfile-%d" PROFILE_FN_SUFFIX, chargingProfile->getStackLevel()); - break; - case (ChargingProfilePurposeType::TxDefaultProfile): - snprintf(fn, PROFILE_FN_MAXSIZE, PROFILE_FN_PREFIX "TxDefProfile-%d" PROFILE_FN_SUFFIX, chargingProfile->getStackLevel()); - break; - case (ChargingProfilePurposeType::TxProfile): - snprintf(fn, PROFILE_FN_MAXSIZE, PROFILE_FN_PREFIX "TxProfile-%d" PROFILE_FN_SUFFIX, chargingProfile->getStackLevel()); - break; - } - - if (USE_FS.exists(fn)) { - USE_FS.remove(fn); - } - - File file = USE_FS.open(fn, "w"); - - if (!file) { - AO_DBG_ERR("Unable to save: could not save profile: %s", fn); - return false; - } - - // Serialize JSON to file - if (serializeJson(json, file) == 0) { - AO_DBG_ERR("Unable to save: could not serialize JSON for profile: %s", fn); - file.close(); - return false; - } - - //success - file.close(); - - AO_DBG_DEBUG("Saving profile successful"); - -#endif //ndef AO_DEACTIVATE_FLASH - return true; -} - -bool SmartChargingService::loadProfiles() { - - bool success = true; - -#ifndef AO_DEACTIVATE_FLASH - if (!filesystemOpt.accessAllowed()) { - AO_DBG_DEBUG("Prohibit access to FS"); - return true; - } - - ChargingProfilePurposeType purposes[] = {ChargingProfilePurposeType::ChargePointMaxProfile, ChargingProfilePurposeType::TxDefaultProfile, ChargingProfilePurposeType::TxProfile}; - - char fn [PROFILE_FN_MAXSIZE] = {'\0'}; - - for (const ChargingProfilePurposeType purpose : purposes) { - - for (int iLevel = 0; iLevel < CHARGEPROFILEMAXSTACKLEVEL; iLevel++) { - - switch (purpose) { - case (ChargingProfilePurposeType::ChargePointMaxProfile): - snprintf(fn, PROFILE_FN_MAXSIZE, PROFILE_FN_PREFIX "CpMaxProfile-%d" PROFILE_FN_SUFFIX, iLevel); - break; - case (ChargingProfilePurposeType::TxDefaultProfile): - snprintf(fn, PROFILE_FN_MAXSIZE, PROFILE_FN_PREFIX "TxDefProfile-%d" PROFILE_FN_SUFFIX, iLevel); - break; - case (ChargingProfilePurposeType::TxProfile): - snprintf(fn, PROFILE_FN_MAXSIZE, PROFILE_FN_PREFIX "TxProfile-%d" PROFILE_FN_SUFFIX, iLevel); - break; - } - - if (!USE_FS.exists(fn)) { - continue; //There is not a profile on the stack iStack with stacklevel iLevel. Normal case, just continue. - } - - File file = USE_FS.open(fn, "r"); - - if (file) { - AO_DBG_DEBUG("Load profile from file: %s", fn); - } else { - AO_DBG_ERR("Unable to initialize: could not open file for profile: %s", fn); - success = false; - continue; - } - - if (!file.available()) { - AO_DBG_ERR("Unable to initialize: empty file for profile: %s", fn); - file.close(); - success = false; - continue; - } - - int file_size = file.size(); - - if (file_size < 2) { - AO_DBG_ERR("Unable to initialize: too short for json: %s", fn); - success = false; - continue; - } - - size_t capacity = 2*file_size; - if (capacity < PROFILE_CUSTOM_CAPACITY) - capacity = PROFILE_CUSTOM_CAPACITY; - if (capacity > PROFILE_MAX_CAPACITY) - capacity = PROFILE_MAX_CAPACITY; - - while (capacity <= PROFILE_MAX_CAPACITY) { - bool increaseCapacity = false; - bool error = true; - - DynamicJsonDocument profileDoc(capacity); - - DeserializationError jsonError = deserializeJson(profileDoc, file); - switch (jsonError.code()) { - case DeserializationError::Ok: - error = false; - break; - case DeserializationError::InvalidInput: - AO_DBG_ERR("Unable to initialize: invalid json in file: %s", fn); - success = false; - break; - case DeserializationError::NoMemory: - increaseCapacity = true; - error = false; - break; - default: - AO_DBG_ERR("Unable to initialize: error in file: %s", fn); - success = false; - break; - } - - if (error) { - break; - } - - if (increaseCapacity) { - capacity *= 3; - capacity /= 2; - file.seek(0, SeekSet); //rewind file to beginning - AO_DBG_DEBUG("Initialization: increase JsonCapacity to %zu for file: %s", capacity, fn); - continue; - } - - JsonObject profileJson = profileDoc.as(); - updateProfileStack(profileJson); - - profileDoc.clear(); - break; - } - - file.close(); - } - } - -#endif //ndef AO_DEACTIVATE_FLASH - return success; -} diff --git a/src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingService.h b/src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingService.h deleted file mode 100644 index ef288ee6..00000000 --- a/src/ArduinoOcpp/Tasks/SmartCharging/SmartChargingService.h +++ /dev/null @@ -1,64 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef SMARTCHARGINGSERVICE_H -#define SMARTCHARGINGSERVICE_H - -#define CHARGEPROFILEMAXSTACKLEVEL 8 -#define CHARGINGSCHEDULEMAXPERIODS 24 -#define MAXCHARGINGPROFILESINSTALLED 10 - -#include -#include - -#include -#include -#include - -namespace ArduinoOcpp { - -using OnLimitChange = std::function; - -class OcppEngine; - -class SmartChargingService { -private: - OcppEngine& context; - - const float DEFAULT_CHARGE_LIMIT; - const float V_eff; //use for approximation: chargingLimit in A * V_eff = chargingLimit in W - ChargingProfile *ChargePointMaxProfile[CHARGEPROFILEMAXSTACKLEVEL]; - ChargingProfile *TxDefaultProfile[CHARGEPROFILEMAXSTACKLEVEL]; - ChargingProfile *TxProfile[CHARGEPROFILEMAXSTACKLEVEL]; - OnLimitChange onLimitChange = NULL; - float limitBeforeChange; - OcppTimestamp nextChange; - - bool chargingSessionStateInitialized {false}; - std::shared_ptr> txStartTime; - OcppTimestamp chargingSessionStart; - int chargingSessionTransactionID; - std::shared_ptr> sRmtProfileId; - uint16_t sRmtProfileIdRev {0}; - uint16_t sessionIdTagRev {0}; - void refreshChargingSessionState(); - - ChargingProfile *updateProfileStack(JsonObject json); - FilesystemOpt filesystemOpt; - bool writeProfileToFlash(JsonObject json, ChargingProfile *chargingProfile); - bool loadProfiles(); - -public: - SmartChargingService(OcppEngine& context, float chargeLimit, float V_eff, int numConnectors, FilesystemOpt filesystemOpt = FilesystemOpt::Use_Mount_FormatOnFail); - void setChargingProfile(JsonObject json); - bool clearChargingProfile(const std::function& filter); - void inferenceLimit(const OcppTimestamp &t, float *limit, OcppTimestamp *validTo); - float inferenceLimitNow(); - void setOnLimitChange(OnLimitChange onLimitChange); - ChargingSchedule *getCompositeSchedule(int connectorId, otime_t duration); - void loop(); -}; - -} //end namespace ArduinoOcpp -#endif diff --git a/src/ArduinoOcpp/Tasks/Transactions/Transaction.cpp b/src/ArduinoOcpp/Tasks/Transactions/Transaction.cpp deleted file mode 100644 index 247444fe..00000000 --- a/src/ArduinoOcpp/Tasks/Transactions/Transaction.cpp +++ /dev/null @@ -1,209 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include - -#include - -using namespace ArduinoOcpp; - -bool TransactionRPC::serializeSessionState(JsonObject rpc) { - rpc["requested"] = requested; - rpc["confirmed"] = confirmed; - return true; -} - -bool Transaction::serializeSessionState(DynamicJsonDocument& out) { - out = DynamicJsonDocument(1024); - JsonObject state = out.to(); - - JsonObject sessionState = state.createNestedObject("session"); - if (session.idTag[0] != '\0') { - sessionState["idTag"] = session.idTag; - } - if (session.timestamp > MIN_TIME) { - char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; - session.timestamp.toJsonString(timeStr, JSONDATE_LENGTH + 1); - sessionState["timestamp"] = timeStr; - } - if (session.txProfileId >= 0) { - sessionState["txProfileId"] = session.txProfileId; - } - if (!session.active) { - sessionState["active"] = session.active; - } - - JsonObject txStart = state.createNestedObject("start"); - - JsonObject txStartRPC = txStart.createNestedObject("rpc"); - if (!start.rpc.serializeSessionState(txStartRPC)) { - return false; - } - - JsonObject txStartClientSide = txStart.createNestedObject("client"); - - if (start.client.timestamp > MIN_TIME) { - char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; - start.client.timestamp.toJsonString(timeStr, JSONDATE_LENGTH + 1); - txStartClientSide["timestamp"] = timeStr; - } - - if (start.client.meter >= 0) { - txStartClientSide["meter"] = start.client.meter; - } - - - if (start.rpc.confirmed) { - JsonObject txStartServerSide = txStart.createNestedObject("server"); - txStartServerSide["transactionId"] = start.server.transactionId; - txStartServerSide["authorized"] = start.server.authorized; - } - - JsonObject txStop = state.createNestedObject("stop"); - - JsonObject txStopRPC = txStop.createNestedObject("rpc"); - if (!stop.rpc.serializeSessionState(txStopRPC)) { - return false; - } - - JsonObject txStopClientSide = txStop.createNestedObject("client"); - - if (stop.client.timestamp > MIN_TIME) { - char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; - stop.client.timestamp.toJsonString(timeStr, JSONDATE_LENGTH + 1); - txStopClientSide["timestamp"] = timeStr; - } - - if (stop.client.meter >= 0) { - txStopClientSide["meter"] = stop.client.meter; - } - - if (stop.client.idTag[0] != '\0') { - txStopClientSide["idTag"] = stop.client.idTag; - } - - if (stop.client.reason[0] != '\0') { - txStopClientSide["reason"] = stop.client.reason; - } - - if (silent) { - state["silent"] = true; - } - - if (out.overflowed()) { - AO_DBG_ERR("JSON capacity exceeded"); - return false; - } - - return true; -} - -bool TransactionRPC::deserializeSessionState(JsonObject rpc) { - if (rpc["requested"] | false) { - requested = true; - } - if (rpc["confirmed"] | false) { - confirmed = true; - } - return true; -} - -bool Transaction::deserializeSessionState(JsonObject state) { - - JsonObject sessionState = state["session"]; - - if (sessionState.containsKey("idTag")) { - if (snprintf(session.idTag, sizeof(session.idTag), "%s", sessionState["idTag"] | "") < 0) { - AO_DBG_ERR("Read err"); - return false; - } - } - if (sessionState.containsKey("timestamp")) { - session.timestamp.setTime(sessionState["timestamp"] | "Invalid"); - } - if (sessionState.containsKey("txProfileId")) { - session.txProfileId = sessionState["txProfileId"] | -1; - } - if (sessionState.containsKey("active")) { - session.active = sessionState["active"] | true; - } - - JsonObject txStart = state["start"]; - - if (txStart.containsKey("rpc")) { - JsonObject txStartRPC = txStart["rpc"]; - if (!start.rpc.deserializeSessionState(txStartRPC)) { - return false; - } - } - - JsonObject txStartClientSide = txStart["client"]; - - if (txStartClientSide.containsKey("timestamp")) { - start.client.timestamp.setTime(txStartClientSide["timestamp"] | "Invalid"); - } - - if (txStartClientSide.containsKey("meter")) { - start.client.meter = txStartClientSide["meter"] | 0; - } - - if (start.rpc.confirmed) { - JsonObject txStartServerSide = txStart["server"]; - start.server.transactionId = txStartServerSide["transactionId"] | -1; - start.server.authorized = txStartServerSide["authorized"] | false; - } - - JsonObject txStop = state["stop"]; - - if (txStop.containsKey("rpc")) { - JsonObject txStopRPC = txStop["rpc"]; - if (!stop.rpc.deserializeSessionState(txStopRPC)) { - return false; - } - } - - JsonObject txStopClientSide = txStop["client"]; - - if (txStopClientSide.containsKey("timestamp")) { - stop.client.timestamp.setTime(txStopClientSide["timestamp"] | "Invalid"); - } - - if (txStopClientSide.containsKey("meter")) { - stop.client.meter = txStopClientSide["meter"] | 0; - } - - if (txStopClientSide.containsKey("idTag")) { - if (snprintf(stop.client.idTag, sizeof(stop.client.idTag), "%s", txStopClientSide["idTag"] | "") < 0) { - AO_DBG_ERR("Read err"); - return false; - } - } - - if (txStopClientSide.containsKey("reason")) { - if (snprintf(stop.client.reason, sizeof(stop.client.reason), "%s", txStopClientSide["reason"] | "") < 0) { - AO_DBG_ERR("Read err"); - return false; - } - } - - if (state.containsKey("silent")) { - silent = state["silent"] | false; - } - - AO_DBG_DEBUG("DUMP TX"); - AO_DBG_DEBUG("Session | idTag %s", session.idTag); - AO_DBG_DEBUG("Start RPC | req: %i, conf: %i", start.rpc.requested, start.rpc.confirmed); - AO_DBG_DEBUG("Stop RPC | req: %i, conf: %i", stop.rpc.requested, stop.rpc.confirmed); - if (silent) { - AO_DBG_DEBUG(" | silent Tx"); - (void)0; - } - - return true; -} - -bool Transaction::commit() { - return context.commit(this); -} diff --git a/src/ArduinoOcpp/Tasks/Transactions/Transaction.h b/src/ArduinoOcpp/Tasks/Transactions/Transaction.h deleted file mode 100644 index ea1c2da4..00000000 --- a/src/ArduinoOcpp/Tasks/Transactions/Transaction.h +++ /dev/null @@ -1,181 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef TRANSACTION_H -#define TRANSACTION_H - -#include -#include -#include -#include - -namespace ArduinoOcpp { - -/* - * A transaction is initiated by the client (charging station) and processed by the server (central system). - * The client side of a transaction is all data that is generated or collected at the charging station. The - * server side is all transaction data that is assigned by the central system. - * - * "ClientTransaction" is short for the client-side data, and the same goes for "ServerTranaction". The rest - * of the terminology is documented in OCPP 1.6 Specification - Edition 2, sections 3.6, 4.8, 4.10 and 5.11. - */ - -class ConnectorTransactionStore; - -class TransactionRPC { -private: - friend class Transaction; - - bool requested = false; - bool confirmed = false; - - bool serializeSessionState(JsonObject out); - bool deserializeSessionState(JsonObject in); -public: - void setRequested() {this->requested = true;} - bool isRequested() {return requested;} - void confirm() {confirmed = true;} - bool isConfirmed() {return confirmed;} - bool isCompleted() {return isRequested() && isConfirmed();} -}; - -class ClientTransactionStart { -private: - friend class Transaction; - - OcppTimestamp timestamp = MIN_TIME; //timestamp of StartTx; can be set before actually initiating - int32_t meter = -1; //meterStart of StartTx -}; - -class ServerTransactionStart { -private: - friend class Transaction; - - bool authorized = true; //authorization status; only valid if confirmed = true - int transactionId = -1; //only valid if confirmed = true -}; - -class TransactionStart { -private: - friend class Transaction; - - TransactionRPC rpc; - ClientTransactionStart client; - ServerTransactionStart server; -}; - -class ClientTransactionStop { -private: - friend class Transaction; - - char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; - OcppTimestamp timestamp = MIN_TIME; - int32_t meter = -1; - char reason [REASON_LEN_MAX + 1] = {'\0'}; -}; - -class ServerTransactionStop { -//no data at the moment -}; - -class TransactionStop { -private: - friend class Transaction; - - TransactionRPC rpc; - ClientTransactionStop client; - ServerTransactionStop server; -}; - -class ChargingSession { -private: - friend class Transaction; - - char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; - OcppTimestamp timestamp = MIN_TIME; - int txProfileId = -1; - - bool active = true; //true: ignore - //false before StartTx init: abort - //false between StartTx init and StopTx init: end - //false after StopTx init: ignore -}; - -class Transaction { -private: - ConnectorTransactionStore& context; - - ChargingSession session; - TransactionStart start; - TransactionStop stop; - - unsigned int connectorId = 0; - unsigned int txNr = 0; - - bool silent = false; //silent Tx: process tx locally, without reporting to the server -public: - Transaction(ConnectorTransactionStore& context, unsigned int connectorId, unsigned int txNr, bool silent = false) : - context(context), - connectorId(connectorId), - txNr(txNr), - silent(silent) {} - - bool serializeSessionState(DynamicJsonDocument& out); - bool deserializeSessionState(JsonObject in); - - unsigned int getConnectorId() {return connectorId;} - void setConnectorId(unsigned int connectorId) {this->connectorId = connectorId;} - unsigned int getTxNr() {return txNr;} //only valid if getConnectorId() >= 0 - void setTxNr(unsigned int txNr) {this->txNr = txNr;} - - TransactionRPC& getStartRpcSync() {return start.rpc;} - - TransactionRPC& getStopRpcSync() {return stop.rpc;} - - bool isAborted() {return !start.rpc.requested && !session.active;} - bool isCompleted() {return stop.rpc.isConfirmed();} - bool isPreparing() {return session.active && !start.rpc.isRequested() && !stop.rpc.isRequested();} - bool isRunning() {return start.rpc.isRequested() && !stop.rpc.isRequested();} - bool isActive() {return session.active && !stop.rpc.isRequested();} - bool isInSession() {return isActive() && *session.idTag;} - - const char *getIdTag() {return session.idTag;} //only for testing in StartTx.req - void setIdTag(const char *idTag) {snprintf(session.idTag, IDTAG_LEN_MAX + 1, "%s", idTag);} - OcppTimestamp& getSessionTimestamp() {return session.timestamp;} - void setSessionTimestamp(OcppTimestamp timestamp) {session.timestamp = timestamp;} - - const char *getStopReason() {return stop.client.reason;} - void setStopReason(const char *reason) {snprintf(stop.client.reason, REASON_LEN_MAX + 1, "%s", reason);} - void endSession() {session.active = false;} - - void setIdTagDeauthorized() {start.server.authorized = false;} - bool isIdTagDeauthorized() {return start.rpc.isConfirmed() && !start.server.authorized;} - - int getTransactionId() {return start.server.transactionId;} - void setTransactionId(int transactionId) {start.server.transactionId = transactionId;} - - void setMeterStart(int32_t meter) {start.client.meter = meter;} - bool isMeterStartDefined() {return start.client.meter >= 0;} //should introduce extra variable later - int32_t getMeterStart() {return start.client.meter;} - - void setStartTimestamp(OcppTimestamp timestamp) {start.client.timestamp = timestamp;} - OcppTimestamp& getStartTimestamp() {return start.client.timestamp;} - - void setMeterStop(int32_t meter) {stop.client.meter = meter;} - bool isMeterStopDefined() {return stop.client.meter >= 0;} //should introduce extra variable later - int32_t getMeterStop() {return stop.client.meter;} - - void setStopTimestamp(OcppTimestamp timestamp) {stop.client.timestamp = timestamp;} - OcppTimestamp& getStopTimestamp() {return stop.client.timestamp;} - - const char *getStopIdTag() {return stop.client.idTag;} //only for testing in StartTx.req - void setStopIdTag(const char *idTag) {snprintf(stop.client.idTag, IDTAG_LEN_MAX + 1, "%s", idTag);} - - bool commit(); - bool isSilent() {return silent;} //no data will be sent to server and server will not assign transactionId -}; - -} - -#endif diff --git a/src/ArduinoOcpp/Tasks/Transactions/TransactionPrerequisites.h b/src/ArduinoOcpp/Tasks/Transactions/TransactionPrerequisites.h deleted file mode 100644 index 9a503718..00000000 --- a/src/ArduinoOcpp/Tasks/Transactions/TransactionPrerequisites.h +++ /dev/null @@ -1,32 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef TXPREREQUISITES_H -#define TXPREREQUISITES_H - -namespace ArduinoOcpp { - -/* - * Type definitions for extending the transaction initiation process - */ - -enum class TxPrecondition { - Active, - Inactive -}; - -enum class TxTrigger { - Active, - Inactive -}; - -enum class TxEnableState { - Active, - Inactive, - Pending -}; - -} - -#endif diff --git a/src/ArduinoOcpp/Tasks/Transactions/TransactionProcess.cpp b/src/ArduinoOcpp/Tasks/Transactions/TransactionProcess.cpp deleted file mode 100644 index bc997715..00000000 --- a/src/ArduinoOcpp/Tasks/Transactions/TransactionProcess.cpp +++ /dev/null @@ -1,89 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include - -#include - -using namespace ArduinoOcpp; - -TransactionProcess::TransactionProcess(unsigned int connectorId) { - -} - -/* - * Evaluate the preconditions and triggers. If all are Active, then execute the transaction enable sequence. - * That is an ordered sequence of preparation steps which must be true before the transaction can start. When - * the transaction is finished, disable the preparation steps in reversed order. - * - * txEnable is the output variable. See getState() for getting the result. - */ -void TransactionProcess::evaluateProcessSteps() { - -#if AO_DBG_LEVEL >= AO_DL_DEBUG - //print transitions to debug console - TxEnableState txEnableBefore = txEnable; -#endif - - //Check Tx preconditions: Tx is only possible if all of them are true; search for a false precondition - auto txPrecondition = TxPrecondition::Active; - for (auto cond : txPreconditions) { - if (cond() != TxPrecondition::Active) { - txPrecondition = TxPrecondition::Inactive; - break; - } - } - - //Check Tx triggers: all of them need to be true to trigger a tx; prepare that search here - auto txTrigger = txTriggers.empty() ? TxTrigger::Inactive : TxTrigger::Active; - if (txPrecondition != TxPrecondition::Active) { //Only trigger a tx if the preconditions are met - txTrigger = TxTrigger::Inactive; - } - - //Determine if - // - No trigger is active -> activeTriggerExists = false, txTrigger = Inactive - // - All triggers are active -> activeTriggerExists = true, txTrigger = Active - // - Some are active, some not -> activeTriggerExists = true, txTrigger = Inactive - activeTriggerExists = false; - for (auto trigger = txTriggers.begin(); trigger != txTriggers.end(); trigger++) { - auto result = trigger->operator()(); - if (result == TxTrigger::Active) { - activeTriggerExists = true; - } else { - txTrigger = TxTrigger::Inactive; - } - } - - //Also check if all devices on the charger are enabled for the Tx - if (txTrigger == TxTrigger::Active) { //Search for an unready device - txEnable = TxEnableState::Active; - - for (auto step = txEnableSequence.rbegin(); step != txEnableSequence.rend(); step++) { - auto result = step->operator()(TxTrigger::Active); - if (result != TxEnableState::Active) { - txEnable = TxEnableState::Pending; - break; - } - } - } else { //Search for a device that is still enabled - txEnable = TxEnableState::Inactive; - - for (auto step = txEnableSequence.begin(); step != txEnableSequence.end(); step++) { - auto result = step->operator()(TxTrigger::Inactive); - if (result != TxEnableState::Inactive) { - txEnable = TxEnableState::Pending; - break; - } - } - } - -#if AO_DBG_LEVEL >= AO_DL_DEBUG - if (txEnableBefore != txEnable) { - AO_DBG_DEBUG("Transition from %s to %s", - txEnableBefore == TxEnableState::Active ? "Active" : txEnableBefore == TxEnableState::Inactive ? "Inactive" : "Pending", - txEnable == TxEnableState::Active ? "Active" : txEnable == TxEnableState::Inactive ? "Inactive" : "Pending"); - } -#endif - -} diff --git a/src/ArduinoOcpp/Tasks/Transactions/TransactionProcess.h b/src/ArduinoOcpp/Tasks/Transactions/TransactionProcess.h deleted file mode 100644 index 187ec1bd..00000000 --- a/src/ArduinoOcpp/Tasks/Transactions/TransactionProcess.h +++ /dev/null @@ -1,44 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef TRANSACTIONPROCESS_H -#define TRANSACTIONPROCESS_H - -#include -#include - -#include -#include - -namespace ArduinoOcpp { - -class TransactionProcess { -private: - - std::vector> txPreconditions; - std::vector> txTriggers; - std::vector> txEnableSequence; - - bool activeTriggerExists = false; - TxEnableState txEnable {TxEnableState::Inactive}; // = Result of Trigger and Enable Sequence - -public: - - TransactionProcess(unsigned int connectorId); - - void addPrecondition(std::function fn) {txPreconditions.push_back(fn);} - - void addTrigger(std::function fn) {txTriggers.push_back(fn);} - - void addEnableStep(std::function fn) {txEnableSequence.push_back(fn);} - - void evaluateProcessSteps(); - - bool existsActiveTrigger() {return activeTriggerExists;} - TxEnableState getState() {return txEnable;} -}; - -} - -#endif diff --git a/src/ArduinoOcpp/Tasks/Transactions/TransactionStore.cpp b/src/ArduinoOcpp/Tasks/Transactions/TransactionStore.cpp deleted file mode 100644 index 0d80e2f7..00000000 --- a/src/ArduinoOcpp/Tasks/Transactions/TransactionStore.cpp +++ /dev/null @@ -1,356 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#include -#include -#include -#include -#include -#include -#include - -#include - -using namespace ArduinoOcpp; - -#ifndef AO_TXSTORE_DIR -#define AO_TXSTORE_DIR AO_FILENAME_PREFIX "/" -#endif - -#define AO_TXSTORE_META_FN AO_FILENAME_PREFIX "/txstore.jsn" - -ConnectorTransactionStore::ConnectorTransactionStore(TransactionStore& context, unsigned int connectorId, std::shared_ptr filesystem) : - context(context), - connectorId(connectorId), - filesystem(filesystem) { - - char key [30] = {'\0'}; - if (snprintf(key, 30, "AO_txEnd_%u", connectorId) < 0) { - AO_DBG_ERR("Invalid key"); - (void)0; - } - txEnd = declareConfiguration(key, 0, AO_TXSTORE_META_FN, false, false, true, false); - - if (snprintf(key, 30, "AO_txBegin_%u", connectorId) < 0) { - AO_DBG_ERR("Invalid key"); - (void)0; - } - txBegin = declareConfiguration(key, 0, AO_TXSTORE_META_FN, false, false, true, false); -} - -std::shared_ptr ConnectorTransactionStore::getTransaction(unsigned int txNr) { - - //check for most recent element of cache first because of temporal locality - if (!transactions.empty()) { - if (auto cached = transactions.back().lock()) { - if (cached->getTxNr() == txNr) { - //cache hit - return cached; - } - } - } - - //check all other elements (and free up unused entries) - auto cached = transactions.begin(); - while (cached != transactions.end()) { - if (auto tx = cached->lock()) { - if (tx->getTxNr() == txNr) { - //cache hit - return tx; - } - cached++; - } else { - //collect outdated cache reference - cached = transactions.erase(cached); - } - } - - //cache miss - load tx from flash if existent - - if (!filesystem) { - AO_DBG_DEBUG("no FS adapter"); - return nullptr; - } - - char fn [MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MAX_PATH_SIZE, AO_TXSTORE_DIR "tx" "-%u-%u.jsn", connectorId, txNr); - if (ret < 0 || ret >= MAX_PATH_SIZE) { - AO_DBG_ERR("fn error: %i", ret); - return nullptr; - } - - size_t msize; - if (filesystem->stat(fn, &msize) != 0) { - AO_DBG_DEBUG("%u-%u does not exist", connectorId, txNr); - return nullptr; - } - - auto doc = FilesystemUtils::loadJson(filesystem, fn); - - if (!doc) { - AO_DBG_ERR("memory corruption"); - return nullptr; - } - - auto transaction = std::make_shared(*this, connectorId, txNr); - JsonObject txJson = doc->as(); - if (!transaction->deserializeSessionState(txJson)) { - AO_DBG_ERR("deserialization error"); - return nullptr; - } - - //before adding new entry, clean cache - transactions.erase(std::remove_if(transactions.begin(), transactions.end(), - [](std::weak_ptr tx) { - return tx.expired(); - }), - transactions.end()); - - transactions.push_back(transaction); - return transaction; -} - -std::shared_ptr ConnectorTransactionStore::createTransaction(bool silent) { - - if (!txBegin || *txBegin < 0 || !txEnd || *txEnd < 0) { - AO_DBG_ERR("memory corruption"); - return nullptr; - } - - //check if maximum number of queued tx already reached - if ((*txEnd + MAX_TX_CNT - *txBegin) % MAX_TX_CNT >= AO_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) *txEnd, silent); - - *txEnd = (*txEnd + 1) % MAX_TX_CNT; - configuration_save(); - - if (!commit(transaction.get())) { - AO_DBG_ERR("FS error"); - return nullptr; - } - - //before adding new entry, clean cache - transactions.erase(std::remove_if(transactions.begin(), transactions.end(), - [](std::weak_ptr tx) { - return tx.expired(); - }), - transactions.end()); - - transactions.push_back(transaction); - return transaction; -} - -std::shared_ptr ConnectorTransactionStore::getLatestTransaction() { - if (!txEnd || *txEnd < 0) { - AO_DBG_ERR("memory corruption"); - return nullptr; - } - - unsigned int latest = ((unsigned int) *txEnd + MAX_TX_CNT - 1) % MAX_TX_CNT; - - return getTransaction(latest); -} - -bool ConnectorTransactionStore::commit(Transaction *transaction) { - - if (!filesystem) { - AO_DBG_DEBUG("no FS: nothing to commit"); - return true; - } - - char fn [MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MAX_PATH_SIZE, AO_TXSTORE_DIR "tx" "-%u-%u.jsn", connectorId, transaction->getTxNr()); - if (ret < 0 || ret >= MAX_PATH_SIZE) { - AO_DBG_ERR("fn error: %i", ret); - return false; - } - - DynamicJsonDocument txDoc {0}; - if (!transaction->serializeSessionState(txDoc)) { - AO_DBG_ERR("Serialization error"); - return false; - } - - if (!FilesystemUtils::storeJson(filesystem, fn, txDoc)) { - AO_DBG_ERR("FS error"); - return false; - } - - //success - return true; -} - -bool ConnectorTransactionStore::remove(unsigned int txNr) { - - if (!filesystem) { - AO_DBG_DEBUG("no FS: nothing to remove"); - return true; - } - - char fn [MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MAX_PATH_SIZE, AO_TXSTORE_DIR "tx" "-%u-%u.jsn", connectorId, txNr); - if (ret < 0 || ret >= MAX_PATH_SIZE) { - AO_DBG_ERR("fn error: %i", ret); - return false; - } - - size_t msize; - if (filesystem->stat(fn, &msize) != 0) { - AO_DBG_DEBUG("%s already removed", fn); - return true; - } - - AO_DBG_DEBUG("remove %s", fn); - - return filesystem->remove(fn); -} - -int ConnectorTransactionStore::getTxBegin() { - if (!txBegin || *txBegin < 0) { - AO_DBG_ERR("memory corruption"); - return -1; - } - - return *txBegin; -} - -int ConnectorTransactionStore::getTxEnd() { - if (!txBegin || *txBegin < 0) { - AO_DBG_ERR("memory corruption"); - return -1; - } - - return *txEnd; -} - -void ConnectorTransactionStore::setTxBegin(unsigned int txNr) { - if (!txBegin || *txBegin < 0) { - AO_DBG_ERR("memory corruption"); - return; - } - - *txBegin = txNr; - configuration_save(); -} - -void ConnectorTransactionStore::setTxEnd(unsigned int txNr) { - if (!txBegin || *txBegin < 0 || !txEnd || *txEnd < 0) { - AO_DBG_ERR("memory corruption"); - return; - } - - *txEnd = txNr; - configuration_save(); -} - -unsigned int ConnectorTransactionStore::size() { - if (!txBegin || *txBegin < 0 || !txEnd || *txEnd < 0) { - AO_DBG_ERR("memory corruption"); - return 0; - } - - return (*txEnd + MAX_TX_CNT - *txBegin) % MAX_TX_CNT; -} - -TransactionStore::TransactionStore(unsigned int nConnectors, std::shared_ptr filesystem) { - - for (unsigned int i = 0; i < nConnectors; i++) { - connectors.push_back(std::unique_ptr( - new ConnectorTransactionStore(*this, i, filesystem))); - } -} - -std::shared_ptr TransactionStore::getLatestTransaction(unsigned int connectorId) { - if (connectorId >= connectors.size()) { - AO_DBG_ERR("Invalid connectorId"); - return nullptr; - } - return connectors[connectorId]->getLatestTransaction(); -} - -bool TransactionStore::commit(Transaction *transaction) { - if (!transaction) { - AO_DBG_ERR("Invalid arg"); - return false; - } - auto connectorId = transaction->getConnectorId(); - if (connectorId >= connectors.size()) { - AO_DBG_ERR("Invalid tx"); - return false; - } - return connectors[connectorId]->commit(transaction); -} - -std::shared_ptr TransactionStore::getTransaction(unsigned int connectorId, unsigned int txNr) { - if (connectorId >= connectors.size()) { - AO_DBG_ERR("Invalid connectorId"); - return nullptr; - } - return connectors[connectorId]->getTransaction(txNr); -} - -std::shared_ptr TransactionStore::createTransaction(unsigned int connectorId, bool silent) { - if (connectorId >= connectors.size()) { - AO_DBG_ERR("Invalid connectorId"); - return nullptr; - } - return connectors[connectorId]->createTransaction(silent); -} - -bool TransactionStore::remove(unsigned int connectorId, unsigned int txNr) { - if (connectorId >= connectors.size()) { - AO_DBG_ERR("Invalid connectorId"); - return false; - } - return connectors[connectorId]->remove(txNr); -} - -int TransactionStore::getTxBegin(unsigned int connectorId) { - if (connectorId >= connectors.size()) { - AO_DBG_ERR("Invalid connectorId"); - return -1; - } - return connectors[connectorId]->getTxBegin(); -} - -int TransactionStore::getTxEnd(unsigned int connectorId) { - if (connectorId >= connectors.size()) { - AO_DBG_ERR("Invalid connectorId"); - return -1; - } - return connectors[connectorId]->getTxEnd(); -} - -void TransactionStore::setTxBegin(unsigned int connectorId, unsigned int txNr) { - if (connectorId >= connectors.size()) { - AO_DBG_ERR("Invalid connectorId"); - return; - } - return connectors[connectorId]->setTxBegin(txNr); -} - -void TransactionStore::setTxEnd(unsigned int connectorId, unsigned int txNr) { - if (connectorId >= connectors.size()) { - AO_DBG_ERR("Invalid connectorId"); - return; - } - return connectors[connectorId]->setTxEnd(txNr); -} - -unsigned int TransactionStore::size(unsigned int connectorId) { - if (connectorId >= connectors.size()) { - AO_DBG_ERR("Invalid connectorId"); - return 0; - } - return connectors[connectorId]->size(); -} diff --git a/src/ArduinoOcpp/Tasks/Transactions/TransactionStore.h b/src/ArduinoOcpp/Tasks/Transactions/TransactionStore.h deleted file mode 100644 index 87954916..00000000 --- a/src/ArduinoOcpp/Tasks/Transactions/TransactionStore.h +++ /dev/null @@ -1,77 +0,0 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 -// MIT License - -#ifndef TRANSACTIONSTORE_H -#define TRANSACTIONSTORE_H - -#include -#include -#include -#include - -#define MAX_TX_CNT 100000U - -#ifndef AO_TXRECORD_SIZE -#define AO_TXRECORD_SIZE 4 //no. of tx to hold on flash storage -#endif - -namespace ArduinoOcpp { - -class TransactionStore; - -class ConnectorTransactionStore { -private: - TransactionStore& context; - const unsigned int connectorId; - - std::shared_ptr filesystem; - std::shared_ptr> txBegin; //if txNr < txBegin, tx has been safely deleted - std::shared_ptr> txEnd; - - std::deque> transactions; - -public: - ConnectorTransactionStore(TransactionStore& context, unsigned int connectorId, std::shared_ptr filesystem); - - std::shared_ptr getLatestTransaction(); - bool commit(Transaction *transaction); - - std::shared_ptr getTransaction(unsigned int txNr); - std::shared_ptr createTransaction(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 { -private: - std::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); - - bool remove(unsigned int connectorId, unsigned int txNr); - - 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); - - unsigned int size(unsigned int connectorId); -}; - -} - -#endif diff --git a/src/ArduinoOcpp_c.cpp b/src/ArduinoOcpp_c.cpp deleted file mode 100644 index 47a86783..00000000 --- a/src/ArduinoOcpp_c.cpp +++ /dev/null @@ -1,359 +0,0 @@ -#include "ArduinoOcpp_c.h" -#include "ArduinoOcpp.h" - -#include - -ArduinoOcpp::OcppSocket *ocppSocket = nullptr; - -void ao_initialize(AOcppSocket *osock, float V_eff, struct AO_FilesystemOpt fsopt) { - if (!osock) { - AO_DBG_ERR("osock is null"); - } - - ocppSocket = reinterpret_cast(osock); - - ArduinoOcpp::FilesystemOpt adaptFsopt = fsopt; - - OCPP_initialize(*ocppSocket, V_eff, adaptFsopt); -} - -void ao_deinitialize() { - OCPP_deinitialize(); -} - -void ao_loop() { - OCPP_loop(); -} - -/* - * Helper functions for transforming callback functions from C-style to C++style - */ - -ArduinoOcpp::PollResult adaptScl(enum OptionalBool v) { - if (v == OptionalTrue) { - return true; - } else if (v == OptionalFalse) { - return false; - } else if (v == OptionalNone) { - return ArduinoOcpp::PollResult::Await(); - } else { - AO_DBG_ERR("illegal argument"); - return false; - } -} - -enum TxTrigger_t adaptScl(ArduinoOcpp::TxTrigger v) { - return v == ArduinoOcpp::TxTrigger::Active ? TxTrigger_t::TxTrg_Active : TxTrigger_t::TxTrg_Inactive; -} - -ArduinoOcpp::TxEnableState adaptScl(enum TxEnableState_t v) { - if (v == TxEna_Pending) { - return ArduinoOcpp::TxEnableState::Pending; - } else if (v == TxEna_Active) { - return ArduinoOcpp::TxEnableState::Active; - } else if (v == TxEna_Inactive) { - return ArduinoOcpp::TxEnableState::Inactive; - } else { - AO_DBG_ERR("illegal argument"); - return ArduinoOcpp::TxEnableState::Inactive; - } -} - -std::function adaptFn(InputBool fn) { - return fn; -} - -std::function adaptFn(unsigned int connectorId, InputBool_m fn) { - return [fn, connectorId] () {return fn(connectorId);}; -} - -std::function adaptFn(InputString fn) { - return fn; -} - -std::function adaptFn(unsigned int connectorId, InputString_m fn) { - return [fn, connectorId] () {return fn(connectorId);}; -} - -std::function adaptFn(InputFloat fn) { - return fn; -} - -std::function adaptFn(unsigned int connectorId, InputFloat_m fn) { - return [fn, connectorId] () {return fn(connectorId);}; -} - -std::function adaptFn(InputInt fn) { - return fn; -} - -std::function adaptFn(unsigned int connectorId, InputInt_m fn) { - return [fn, connectorId] () {return fn(connectorId);}; -} - -std::function adaptFn(OutputFloat fn) { - return fn; -} - -std::function adaptFn(unsigned int connectorId, OutputFloat_m fn) { - return [fn, connectorId] (float value) {return fn(connectorId, value);}; -} - -std::function adaptFn(void (*fn)(void)) { - return fn; -} - -#ifndef AO_RECEIVE_PAYLOAD_BUFSIZE -#define AO_RECEIVE_PAYLOAD_BUFSIZE 512 -#endif - -char ao_recv_payload_buff [AO_RECEIVE_PAYLOAD_BUFSIZE] = {'\0'}; - -std::function adaptFn(OnOcppMessage fn) { - if (!fn) return nullptr; - return [fn] (JsonObject payload) { - auto len = serializeJson(payload, ao_recv_payload_buff, AO_RECEIVE_PAYLOAD_BUFSIZE); - if (len <= 0) { - AO_DBG_WARN("Received payload buffer exceeded. Continue without payload"); - } - fn(len > 0 ? ao_recv_payload_buff : "", len); - }; -} - -std::function adaptFn(const char *idTag_cstr, OnAuthorize fn) { - if (!fn) return nullptr; - std::string idTag = idTag_cstr ? idTag_cstr : "undefined"; - return [fn, idTag] (JsonObject payload) { - auto len = serializeJson(payload, ao_recv_payload_buff, AO_RECEIVE_PAYLOAD_BUFSIZE); - if (len <= 0) { - AO_DBG_WARN("Received payload buffer exceeded. Continue without payload"); - } - fn(idTag.c_str(), len > 0 ? ao_recv_payload_buff : "", len); - }; -} - -ArduinoOcpp::OnReceiveErrorListener adaptFn(OnOcppError fn) { - if (!fn) return nullptr; - return [fn] (const char *code, const char *description, JsonObject details) { - auto len = serializeJson(details, ao_recv_payload_buff, AO_RECEIVE_PAYLOAD_BUFSIZE); - if (len <= 0) { - AO_DBG_WARN("Received payload buffer exceeded. Continue without payload"); - } - fn(code, description, len > 0 ? ao_recv_payload_buff : "", len); - }; -} - -std::function()> adaptFn(PollBool fn) { - return [fn] () {return adaptScl(fn());}; -} - -std::function()> adaptFn(unsigned int connectorId, PollBool_m fn) { - return [fn, connectorId] () {return adaptScl(fn(connectorId));}; -} - -std::function adaptFn(TxStepInOut fn) { - if (!fn) return nullptr; - return [fn] (ArduinoOcpp::TxTrigger trigger) -> ArduinoOcpp::TxEnableState { - auto res = fn(adaptScl(trigger)); - return adaptScl(res); - }; -} - -std::function adaptFn(unsigned int connectorId, TxStepInOut_m fn) { - if (!fn) return nullptr; - return [fn, connectorId] (ArduinoOcpp::TxTrigger trigger) -> ArduinoOcpp::TxEnableState { - auto res = fn(connectorId, adaptScl(trigger)); - return adaptScl(res); - }; -} - -void ao_bootNotification(const char *chargePointModel, const char *chargePointVendor, OnOcppMessage onConfirmation, OnOcppAbort onAbort, OnOcppTimeout onTimeout, OnOcppError onError) { - bootNotification(chargePointModel, chargePointVendor, adaptFn(onConfirmation), adaptFn(onAbort), adaptFn(onTimeout), adaptFn(onError)); -} - -void ao_bootNotification_full(const char *payloadJson, OnOcppMessage onConfirmation, OnOcppAbort onAbort, OnOcppTimeout onTimeout, OnOcppError onError) { - auto payload = std::unique_ptr(new DynamicJsonDocument(JSON_OBJECT_SIZE(9) + 230 + 9)); // BootNotification has at most 9 attributes with at most 230 chars + null terminators - auto err = deserializeJson(*payload, payloadJson); - if (err) { - AO_DBG_ERR("Could not process input: %s", err.c_str()); - (void)0; - } - - bootNotification(std::move(payload), adaptFn(onConfirmation), adaptFn(onAbort), adaptFn(onTimeout), adaptFn(onError)); -} - -void ao_authorize(const char *idTag, OnAuthorize onConfirmation, OnOcppAbort onAbort, OnOcppTimeout onTimeout, OnOcppError onError) { - authorize(idTag, adaptFn(idTag, onConfirmation), adaptFn(onAbort), adaptFn(onTimeout), adaptFn(onError)); -} - -void ao_beginTransaction(const char *idTag) { - ao_beginTransaction(idTag); -} -void ao_beginTransaction_m(unsigned int connectorId, const char *idTag) { - beginTransaction(idTag, 1); -} - -bool ao_endTransaction(const char *reason) { - return endTransaction(reason); -} -bool ao_endTransaction_m(unsigned int connectorId, const char *reason) { - return endTransaction(reason, connectorId); -} - -bool ao_isTransactionRunning() { - return isTransactionRunning(); -} -bool ao_isTransactionRunning_m(unsigned int connectorId) { - return isTransactionRunning(connectorId); -} - -bool ao_ocppPermitsCharge() { - return ocppPermitsCharge(); -} -bool ao_ocppPermitsCharge_m(unsigned int connectorId) { - return ocppPermitsCharge(connectorId); -} - -void ao_setConnectorPluggedInput(InputBool pluggedInput) { - setConnectorPluggedInput(adaptFn(pluggedInput)); -} -void ao_setConnectorPluggedInput_m(unsigned int connectorId, InputBool_m pluggedInput) { - setConnectorPluggedInput(adaptFn(connectorId, pluggedInput), connectorId); -} - -void ao_setEnergyMeterInput(InputInt energyInput) { - setEnergyMeterInput(adaptFn(energyInput)); -} -void ao_setEnergyMeterInput_m(unsigned int connectorId, InputInt_m energyInput) { - setEnergyMeterInput(adaptFn(connectorId, energyInput), connectorId); -} - -void ao_setPowerMeterInput(InputFloat powerInput) { - setPowerMeterInput(adaptFn(powerInput)); -} -void ao_setPowerMeterInput_m(unsigned int connectorId, InputFloat_m powerInput) { - setPowerMeterInput(adaptFn(connectorId, powerInput), connectorId); -} - -void ao_setSmartChargingOutput(OutputFloat chargingLimitOutput) { - setSmartChargingOutput(adaptFn(chargingLimitOutput)); -} -void ao_setSmartChargingOutput_m(unsigned int connectorId, OutputFloat_m chargingLimitOutput) { - setSmartChargingOutput(adaptFn(connectorId, chargingLimitOutput), connectorId); -} - -void ao_setEvReadyInput(InputBool evReadyInput) { - setEvReadyInput(adaptFn(evReadyInput)); -} -void ao_setEvReadyInput_m(unsigned int connectorId, InputBool_m evReadyInput) { - setEvReadyInput(adaptFn(connectorId, evReadyInput), connectorId); -} - -void ao_setEvseReadyInput(InputBool evseReadyInput) { - setEvseReadyInput(adaptFn(evseReadyInput)); -} -void ao_setEvseReadyInput_m(unsigned int connectorId, InputBool_m evseReadyInput) { - setEvseReadyInput(adaptFn(connectorId, evseReadyInput), connectorId); -} - -void ao_addErrorCodeInput(InputString errorCodeInput) { - addErrorCodeInput(adaptFn(errorCodeInput)); -} -void ao_addErrorCodeInput_m(unsigned int connectorId, InputString_m errorCodeInput) { - addErrorCodeInput(adaptFn(connectorId, errorCodeInput), connectorId); -} - -void ao_addMeterValueInputInt(InputInt valueInput, const char *measurand, const char *unit, const char *location, const char *phase) { - addMeterValueInput(adaptFn(valueInput), measurand, unit, location, phase, 1); -} -void ao_addMeterValueInputInt_m(unsigned int connectorId, InputInt_m valueInput, const char *measurand, const char *unit, const char *location, const char *phase) { - addMeterValueInput(adaptFn(connectorId, valueInput), measurand, unit, location, phase, connectorId); -} - -void ao_addMeterValueInput(MeterValueInput *meterValueInput) { - ao_addMeterValueInput_m(1, meterValueInput); -} -void ao_addMeterValueInput_m(unsigned int connectorId, MeterValueInput *meterValueInput) { - auto svs = std::unique_ptr( - reinterpret_cast(meterValueInput)); - - addMeterValueInput(std::move(svs), connectorId); -} - -void ao_setOnUnlockConnectorInOut(PollBool onUnlockConnectorInOut) { - setOnUnlockConnectorInOut(adaptFn(onUnlockConnectorInOut)); -} -void ao_setOnUnlockConnectorInOut_m(unsigned int connectorId, PollBool_m onUnlockConnectorInOut) { - setOnUnlockConnectorInOut(adaptFn(connectorId, onUnlockConnectorInOut), connectorId); -} - -void ao_setConnectorLockInOut(TxStepInOut lockConnectorInOut) { - setConnectorLockInOut(adaptFn(lockConnectorInOut)); -} -void ao_setConnectorLockInOut_m(unsigned int connectorId, TxStepInOut_m lockConnectorInOut) { - setConnectorLockInOut(adaptFn(connectorId, lockConnectorInOut), connectorId); -} - -void ao_setTxBasedMeterInOut(TxStepInOut txMeterInOut) { - setTxBasedMeterInOut(adaptFn(txMeterInOut)); -} -void ao_setTxBasedMeterInOut_m(unsigned int connectorId, TxStepInOut_m txMeterInOut) { - setTxBasedMeterInOut(adaptFn(connectorId, txMeterInOut), connectorId); -} - -bool ao_isOperative() { - return isOperative(); -} -bool ao_isOperative_m(unsigned int connectorId) { - return isOperative(connectorId); -} - -int ao_getTransactionId() { - return getTransactionId(); -} -int ao_getTransactionId_m(unsigned int connectorId) { - return getTransactionId(connectorId); -} - -const char *ao_getTransactionIdTag() { - return getTransactionIdTag(); -} -const char *ao_getTransactionIdTag_m(unsigned int connectorId) { - return getTransactionIdTag(connectorId); -} - -void ao_setOnResetRequest(OnOcppMessage onRequest) { - setOnResetRequest(adaptFn(onRequest)); -} - -void ao_set_console_out_c(void (*console_out)(const char *msg)) { - ao_set_console_out(console_out); -} - -OcppHandle *getOcppHandle() { - return reinterpret_cast(getOcppEngine()); -} - -void ao_onRemoteStartTransactionSendConf(OnOcppMessage onSendConf) { - setOnRemoteStopTransactionSendConf(adaptFn(onSendConf)); -} - -void ao_onRemoteStopTransactionSendConf(OnOcppMessage onSendConf) { - setOnRemoteStopTransactionSendConf(adaptFn(onSendConf)); -} - -void ao_onRemoteStopTransactionRequest(OnOcppMessage onRequest) { - setOnRemoteStopTransactionReceiveReq(adaptFn(onRequest)); -} - -void ao_onResetSendConf(OnOcppMessage onSendConf) { - setOnResetSendConf(adaptFn(onSendConf)); -} - -void ao_startTransaction(const char *idTag, OnOcppMessage onConfirmation, OnOcppAbort onAbort, OnOcppTimeout onTimeout, OnOcppError onError) { - startTransaction(idTag, adaptFn(onConfirmation), adaptFn(onAbort), adaptFn(onTimeout), adaptFn(onError)); -} - -void ao_stopTransaction(OnOcppMessage onConfirmation, OnOcppAbort onAbort, OnOcppTimeout onTimeout, OnOcppError onError) { - stopTransaction(adaptFn(onConfirmation), adaptFn(onAbort), adaptFn(onTimeout), adaptFn(onError)); -} diff --git a/src/ArduinoOcpp_c.h b/src/ArduinoOcpp_c.h deleted file mode 100644 index 3c532d4a..00000000 --- a/src/ArduinoOcpp_c.h +++ /dev/null @@ -1,173 +0,0 @@ -#ifndef ARDUINOOCPP_C_H -#define ARDUINOOCPP_C_H - -#include -#include - -struct AOcppSocket; -typedef struct AOcppSocket AOcppSocket; - -struct MeterValueInput; -typedef struct MeterValueInput MeterValueInput; - -struct OcppHandle; -typedef struct OcppHandle OcppHandle; - -typedef void (*OnOcppMessage) (const char *payload, size_t len); -typedef void (*OnAuthorize) (const char *idTag, const char *payload, size_t len); -typedef void (*OnOcppAbort) (); -typedef void (*OnOcppTimeout) (); -typedef void (*OnOcppError) (const char *code, const char *description, const char *details_json, size_t details_len); - -typedef float (*InputFloat)(); -typedef float (*InputFloat_m)(unsigned int connectorId); //multiple connectors version -typedef int (*InputInt)(); -typedef int (*InputInt_m)(unsigned int connectorId); -typedef bool (*InputBool)(); -typedef bool (*InputBool_m)(unsigned int connectorId); -typedef const char* (*InputString)(); -typedef const char* (*InputString_m)(unsigned int connectorId); -typedef void (*OutputFloat)(float limit); -typedef void (*OutputFloat_m)(unsigned int connectorId, float limit); -enum OptionalBool {OptionalTrue, OptionalFalse, OptionalNone}; -typedef enum OptionalBool (*PollBool)(); -typedef enum OptionalBool (*PollBool_m)(unsigned int connectorId); -enum TxTrigger_t {TxTrg_Active, TxTrg_Inactive}; -enum TxEnableState_t {TxEna_Active, TxEna_Inactive, TxEna_Pending}; -typedef enum TxEnableState_t (*TxStepInOut)(enum TxTrigger_t triggerIn); -typedef enum TxEnableState_t (*TxStepInOut_m)(unsigned int connectorId, enum TxTrigger_t triggerIn); - - -#ifdef __cplusplus -extern "C" { -#endif - -/* - * Please refer to ArduinoOcpp.h for the documentation - */ - -void ao_initialize( - AOcppSocket *osock, //WebSocket adapter for ArduinoOcpp - float V_eff, //Grid voltage of your country. e.g. 230.f (European voltage) - struct AO_FilesystemOpt fsopt); //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h - -void ao_deinitialize(); - -void ao_loop(); -/* - * Send OCPP operations - */ - -void ao_bootNotification(const char *chargePointModel, const char *chargePointVendor, OnOcppMessage onConfirmation, OnOcppAbort onAbort, OnOcppTimeout onTimeout, OnOcppError onError); - -void ao_bootNotification_full(const char *payloadJson, OnOcppMessage onConfirmation, OnOcppAbort onAbort, OnOcppTimeout onTimeout, OnOcppError onError); - -void ao_authorize(const char *idTag, OnAuthorize onConfirmation, OnOcppAbort onAbort, OnOcppTimeout onTimeout, OnOcppError onError); - -/* - * Charging session management - */ - -void ao_beginTransaction(const char *idTag); -void ao_beginTransaction_m(unsigned int connectorId, const char *idTag); //multiple connectors version - -bool ao_endTransaction(const char *reason); //reason can be NULL -bool ao_endTransaction_m(unsigned int connectorId, const char *reason); //reason can be NULL - -bool ao_isTransactionRunning(); -bool ao_isTransactionRunning_m(unsigned int connectorId); - -bool ao_ocppPermitsCharge(); -bool ao_ocppPermitsCharge_m(unsigned int connectorId); - -/* - * Define the Inputs and Outputs of this library. - */ - -void ao_setConnectorPluggedInput(InputBool pluggedInput); -void ao_setConnectorPluggedInput_m(unsigned int connectorId, InputBool_m pluggedInput); - -void ao_setEnergyMeterInput(InputInt energyInput); -void ao_setEnergyMeterInput_m(unsigned int connectorId, InputInt_m energyInput); - -void ao_setPowerMeterInput(InputFloat powerInput); -void ao_setPowerMeterInput_m(unsigned int connectorId, InputFloat_m powerInput); - -void ao_setSmartChargingOutput(OutputFloat chargingLimitOutput); -void ao_setSmartChargingOutput_m(unsigned int connectorId, OutputFloat_m chargingLimitOutput); - -/* - * Define the Inputs and Outputs of this library. (Advanced) - */ - -void ao_setEvReadyInput(InputBool evReadyInput); -void ao_setEvReadyInput_m(unsigned int connectorId, InputBool_m evReadyInput); - -void ao_setEvseReadyInput(InputBool evseReadyInput); -void ao_setEvseReadyInput_m(unsigned int connectorId, InputBool_m evseReadyInput); - -void ao_addErrorCodeInput(InputString errorCodeInput); -void ao_addErrorCodeInput_m(unsigned int connectorId, InputString_m errorCodeInput); - -void ao_addMeterValueInputInt(InputInt valueInput, const char *measurand, const char *unit, const char *location, const char *phase); //measurand, unit, location and phase can be NULL -void ao_addMeterValueInputInt_m(unsigned int connectorId, InputInt_m valueInput, const char *measurand, const char *unit, const char *location, const char *phase); //measurand, unit, location and phase can be NULL - -void ao_addMeterValueInput(MeterValueInput *meterValueInput); //takes ownership of meterValueInput -void ao_addMeterValueInput_m(unsigned int connectorId, MeterValueInput *meterValueInput); //takes ownership of meterValueInput - -void ao_setOnUnlockConnectorInOut(PollBool onUnlockConnectorInOut); -void ao_setOnUnlockConnectorInOut_m(unsigned int connectorId, PollBool_m onUnlockConnectorInOut); - -void ao_setConnectorLockInOut(TxStepInOut lockConnectorInOut); -void ao_setConnectorLockInOut_m(unsigned int connectorId, TxStepInOut_m lockConnectorInOut); - -void ao_setTxBasedMeterInOut(TxStepInOut txMeterInOut); -void ao_setTxBasedMeterInOut_m(unsigned int connectorId, TxStepInOut_m txMeterInOut); - -/* - * Access further information about the internal state of the library - */ - -bool ao_isOperative(); -bool ao_isOperative_m(unsigned int connectorId); - -int ao_getTransactionId(); -int ao_getTransactionId_m(unsigned int connectorId); - -const char *ao_getTransactionIdTag(); -const char *ao_getTransactionIdTag_m(unsigned int connectorId); - -void ao_setOnResetRequest(OnOcppMessage onRequest); - -/* - * If build flag AO_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 ao_set_console_out_c(void (*console_out)(const char *msg)); - -/* - * Get access to the internals - */ - -OcppHandle *ao_getOcppHandle(); - -/* - * Deprecated functions or functions to be moved to ArduinoOcppExtended.h - */ - -void ao_onRemoteStartTransactionSendConf(OnOcppMessage onSendConf); //important, energize the power plug here and capture the idTag - -void ao_onRemoteStopTransactionSendConf(OnOcppMessage onSendConf); //important, de-energize the power plug here -void ao_onRemoteStopTransactionRequest(OnOcppMessage onRequest); //optional, to de-energize the power plug immediately - -void ao_setOnResetSendConf(OnOcppMessage onSendConf); - -void ao_startTransaction(const char *idTag, OnOcppMessage onConfirmation, OnOcppAbort onAbort, OnOcppTimeout onTimeout, OnOcppError onError); - -void ao_stopTransaction(OnOcppMessage onConfirmation, OnOcppAbort onAbort, OnOcppTimeout onTimeout, OnOcppError onError); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/src/MicroOcpp.cpp b/src/MicroOcpp.cpp new file mode 100644 index 00000000..5dbd3714 --- /dev/null +++ b/src/MicroOcpp.cpp @@ -0,0 +1,1533 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include "MicroOcpp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace MicroOcpp { +namespace Facade { + +#ifndef MO_CUSTOM_WS +WebSocketsClient *webSocket {nullptr}; +Connection *connection {nullptr}; +#endif + +Context *context {nullptr}; +std::shared_ptr filesystem; + +#ifndef MO_NUMCONNECTORS +#define MO_NUMCONNECTORS 2 +#endif + +#define OCPP_ID_OF_CP 0 +#define OCPP_ID_OF_CONNECTOR 1 + +} //end namespace MicroOcpp::Facade +} //end namespace MicroOcpp + +#if MO_ENABLE_HEAP_PROFILER +#ifndef MO_HEAP_PROFILER_EXTERNAL_CONTROL +#define MO_HEAP_PROFILER_EXTERNAL_CONTROL 0 //enable if you want to manually reset the heap profiler (e.g. for keeping stats over multiple MO lifecycles) +#endif +#endif + +using namespace MicroOcpp; +using namespace MicroOcpp::Facade; +using namespace MicroOcpp::Ocpp16; + +#ifndef MO_CUSTOM_WS +void mocpp_initialize(const char *backendUrl, const char *chargeBoxId, const char *chargePointModel, const char *chargePointVendor, FilesystemOpt fsOpt, const char *password, const char *CA_cert, bool autoRecover) { + if (context) { + MO_DBG_WARN("already initialized. To reinit, call mocpp_deinitialize() before"); + return; + } + + if (!backendUrl || !chargePointModel || !chargePointVendor) { + MO_DBG_ERR("invalid args"); + return; + } + + if (!chargeBoxId) { + chargeBoxId = ""; + } + + /* + * parse backendUrl so that it suits the links2004/arduinoWebSockets interface + */ + auto url = makeString("MicroOcpp.cpp", backendUrl); + + //tolower protocol specifier + for (auto c = url.begin(); *c != ':' && c != url.end(); c++) { + *c = tolower(*c); + } + + bool isTLS = true; + if (!strncmp(url.c_str(),"wss://",strlen("wss://"))) { + isTLS = true; + } else if (!strncmp(url.c_str(),"ws://",strlen("ws://"))) { + isTLS = false; + } else { + MO_DBG_ERR("only ws:// and wss:// supported"); + return; + } + + //parse host, port + auto host_port_path = url.substr(url.find_first_of("://") + strlen("://")); + auto host_port = host_port_path.substr(0, host_port_path.find_first_of('/')); + auto path = host_port_path.substr(host_port.length()); + auto host = host_port.substr(0, host_port.find_first_of(':')); + if (host.empty()) { + MO_DBG_ERR("could not parse host: %s", url.c_str()); + return; + } + uint16_t port = 0; + auto port_str = host_port.substr(host.length()); + if (port_str.empty()) { + port = isTLS ? 443U : 80U; + } else { + //skip leading ':' + port_str = port_str.substr(1); + for (auto c = port_str.begin(); c != port_str.end(); c++) { + if (*c < '0' || *c > '9') { + MO_DBG_ERR("could not parse port: %s", url.c_str()); + return; + } + auto p = port * 10U + (*c - '0'); + if (p < port) { + MO_DBG_ERR("could not parse port (overflow): %s", url.c_str()); + return; + } + port = p; + } + } + + if (path.empty()) { + path = "/"; + } + + if ((!*chargeBoxId) == '\0') { + if (path.back() != '/') { + path += '/'; + } + + path += chargeBoxId; + } + + MO_DBG_INFO("connecting to %s -- (host: %s, port: %u, path: %s)", url.c_str(), host.c_str(), port, path.c_str()); + + if (!webSocket) + webSocket = new WebSocketsClient(); + + if (isTLS) { + // server address, port, path and TLS certificate + webSocket->beginSslWithCA(host.c_str(), port, path.c_str(), CA_cert, "ocpp1.6"); + } else { + // server address, port, path + webSocket->begin(host.c_str(), port, path.c_str(), "ocpp1.6"); + } + + // try ever 5000 again if connection has failed + webSocket->setReconnectInterval(5000); + + // start heartbeat (optional) + // ping server every 15000 ms + // expect pong from server within 3000 ms + // consider connection disconnected if pong is not received 2 times + webSocket->enableHeartbeat(15000, 3000, 2); //comment this one out to for specific OCPP servers + + // add authentication data (optional) + if (password && strlen(password) + strlen(chargeBoxId) >= 4) { + webSocket->setAuthorization(chargeBoxId, password); + } + + delete connection; + connection = new EspWiFi::WSClient(webSocket); + + mocpp_initialize(*connection, ChargerCredentials(chargePointModel, chargePointVendor), makeDefaultFilesystemAdapter(fsOpt), autoRecover); +} +#endif + +ChargerCredentials::ChargerCredentials(const char *cpModel, const char *cpVendor, const char *fWv, const char *cpSNr, const char *meterSNr, const char *meterType, const char *cbSNr, const char *iccid, const char *imsi) { + + StaticJsonDocument<512> creds; + if (cbSNr) + creds["chargeBoxSerialNumber"] = cbSNr; + if (cpModel) + creds["chargePointModel"] = cpModel; + if (cpSNr) + creds["chargePointSerialNumber"] = cpSNr; + if (cpVendor) + creds["chargePointVendor"] = cpVendor; + if (fWv) + creds["firmwareVersion"] = fWv; + if (iccid) + creds["iccid"] = iccid; + if (imsi) + creds["imsi"] = imsi; + if (meterSNr) + creds["meterSerialNumber"] = meterSNr; + if (meterType) + creds["meterType"] = meterType; + + if (creds.overflowed()) { + MO_DBG_ERR("Charger Credentials too long"); + } + + size_t written = serializeJson(creds, payload, 512); + + if (written < 2) { + MO_DBG_ERR("Charger Credentials could not be written"); + sprintf(payload, "{}"); + } +} + +ChargerCredentials ChargerCredentials::v201(const char *cpModel, const char *cpVendor, const char *fWv, const char *cpSNr, const char *meterSNr, const char *meterType, const char *cbSNr, const char *iccid, const char *imsi) { + + ChargerCredentials res; + + StaticJsonDocument<512> creds; + if (cpSNr) + creds["serialNumber"] = cpSNr; + if (cpModel) + creds["model"] = cpModel; + if (cpVendor) + creds["vendorName"] = cpVendor; + if (fWv) + creds["firmwareVersion"] = fWv; + if (iccid) + creds["modem"]["iccid"] = iccid; + if (imsi) + creds["modem"]["imsi"] = imsi; + + if (creds.overflowed()) { + MO_DBG_ERR("Charger Credentials too long"); + } + + size_t written = serializeJson(creds, res.payload, 512); + + if (written < 2) { + MO_DBG_ERR("Charger Credentials could not be written"); + sprintf(res.payload, "{}"); + } + + return res; +} + +void mocpp_initialize(Connection& connection, const char *bootNotificationCredentials, std::shared_ptr fs, bool autoRecover, MicroOcpp::ProtocolVersion version) { + if (context) { + MO_DBG_WARN("already initialized. To reinit, call mocpp_deinitialize() before"); + return; + } + + MO_DBG_DEBUG("initialize OCPP"); + + filesystem = fs; + MO_DBG_DEBUG("filesystem %s", filesystem ? "loaded" : "deactivated"); + + BootStats bootstats; + BootService::loadBootStats(filesystem, bootstats); + + if (autoRecover && bootstats.getBootFailureCount() > 3) { + BootService::recover(filesystem, bootstats); + bootstats = BootStats(); + } + + BootService::migrate(filesystem, bootstats); + + bootstats.bootNr++; //assign new boot number to this run + BootService::storeBootStats(filesystem, bootstats); + + configuration_init(filesystem); //call before each other library call + + context = new Context(connection, filesystem, bootstats.bootNr, version); + +#if MO_ENABLE_MBEDTLS + context->setFtpClient(makeFtpClientMbedTLS()); +#endif //MO_ENABLE_MBEDTLS + + auto& model = context->getModel(); + + model.setBootService(std::unique_ptr( + new BootService(*context, filesystem))); + +#if MO_ENABLE_V201 + if (version.major == 2) { + model.setAvailabilityService(std::unique_ptr( + new AvailabilityService(*context, MO_NUM_EVSEID))); + model.setVariableService(std::unique_ptr( + new VariableService(*context, filesystem))); + model.setTransactionService(std::unique_ptr( + new TransactionService(*context, filesystem, MO_NUM_EVSEID))); + model.setRemoteControlService(std::unique_ptr( + new RemoteControlService(*context, MO_NUM_EVSEID))); + model.setResetServiceV201(std::unique_ptr( + new Ocpp201::ResetService(*context))); + } else +#endif + { + model.setTransactionStore(std::unique_ptr( + new TransactionStore(MO_NUMCONNECTORS, filesystem))); + model.setConnectorsCommon(std::unique_ptr( + new ConnectorsCommon(*context, MO_NUMCONNECTORS, filesystem))); + auto connectors = makeVector>("v16.ConnectorBase.Connector"); + for (unsigned int connectorId = 0; connectorId < MO_NUMCONNECTORS; connectorId++) { + connectors.emplace_back(new Connector(*context, filesystem, connectorId)); + } + model.setConnectors(std::move(connectors)); + +#if MO_ENABLE_LOCAL_AUTH + model.setAuthorizationService(std::unique_ptr( + new AuthorizationService(*context, filesystem))); +#endif //MO_ENABLE_LOCAL_AUTH + +#if MO_ENABLE_RESERVATION + model.setReservationService(std::unique_ptr( + new ReservationService(*context, MO_NUMCONNECTORS))); +#endif + + model.setResetService(std::unique_ptr( + new ResetService(*context))); + } + + model.setHeartbeatService(std::unique_ptr( + new HeartbeatService(*context))); + +#if MO_ENABLE_CERT_MGMT && MO_ENABLE_CERT_STORE_MBEDTLS + std::unique_ptr certStore = makeCertificateStoreMbedTLS(filesystem); + if (certStore) { + model.setCertificateService(std::unique_ptr( + new CertificateService(*context))); + } + if (certStore && model.getCertificateService()) { + model.getCertificateService()->setCertificateStore(std::move(certStore)); + } +#endif + +#if !defined(MO_CUSTOM_UPDATER) +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS + model.setFirmwareService( + makeDefaultFirmwareService(*context)); //instantiate FW service + ESP installation routine +#elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP8266) + model.setFirmwareService( + makeDefaultFirmwareService(*context)); //instantiate FW service + ESP installation routine +#endif //MO_PLATFORM +#endif //!defined(MO_CUSTOM_UPDATER) + +#if !defined(MO_CUSTOM_DIAGNOSTICS) +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS + model.setDiagnosticsService( + makeDefaultDiagnosticsService(*context, filesystem)); //instantiate Diag service + ESP hardware diagnostics +#elif MO_ENABLE_MBEDTLS + model.setDiagnosticsService( + makeDefaultDiagnosticsService(*context, filesystem)); //instantiate Diag service +#endif //MO_PLATFORM +#endif //!defined(MO_CUSTOM_DIAGNOSTICS) + +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) + setOnResetExecute(makeDefaultResetFn()); +#endif + + model.getBootService()->setChargePointCredentials(bootNotificationCredentials); + + auto credsJson = model.getBootService()->getChargePointCredentials(); + if (model.getFirmwareService() && credsJson && credsJson->containsKey("firmwareVersion")) { + model.getFirmwareService()->setBuildNumber((*credsJson)["firmwareVersion"]); + } + credsJson.reset(); + + configuration_load(); + +#if MO_ENABLE_V201 + if (version.major == 2) { + model.getVariableService()->load(); + } +#endif //MO_ENABLE_V201 + + MO_DBG_INFO("initialized MicroOcpp v" MO_VERSION " running OCPP %i.%i.%i", version.major, version.minor, version.patch); +} + +void mocpp_deinitialize() { + + if (context) { + //release bootstats recovery mechanism + BootStats bootstats; + BootService::loadBootStats(filesystem, bootstats); + if (bootstats.lastBootSuccess != bootstats.bootNr) { + MO_DBG_DEBUG("boot success timer override"); + bootstats.lastBootSuccess = bootstats.bootNr; + BootService::storeBootStats(filesystem, bootstats); + } + } + + delete context; + context = nullptr; + +#ifndef MO_CUSTOM_WS + delete connection; + connection = nullptr; + delete webSocket; + webSocket = nullptr; +#endif + + filesystem.reset(); + + configuration_deinit(); + +#if !MO_HEAP_PROFILER_EXTERNAL_CONTROL + MO_MEM_DEINIT(); +#endif + + MO_DBG_DEBUG("deinitialized OCPP\n"); +} + +void mocpp_loop() { + if (!context) { + MO_DBG_WARN("need to call mocpp_initialize before"); + return; + } + + context->loop(); +} + +bool beginTransaction(const char *idTag, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (!idTag || strnlen(idTag, MO_IDTOKEN_LEN_MAX + 2) > MO_IDTOKEN_LEN_MAX) { + MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", MO_IDTOKEN_LEN_MAX); + return false; + } + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->beginAuthorization(idTag, true); + } + #endif + + if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX) { + MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); + return false; + } + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return false; + } + + return connector->beginTransaction(idTag) != nullptr; +} + +bool beginTransaction_authorized(const char *idTag, const char *parentIdTag, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (!idTag || strnlen(idTag, MO_IDTOKEN_LEN_MAX + 2) > MO_IDTOKEN_LEN_MAX) { + MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", MO_IDTOKEN_LEN_MAX); + return false; + } + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->beginAuthorization(idTag, false); + } + #endif + + if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX || + (parentIdTag && strnlen(parentIdTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX)) { + MO_DBG_ERR("(parent)idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); + return false; + } + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return false; + } + + return connector->beginTransaction_authorized(idTag, parentIdTag) != nullptr; +} + +bool endTransaction(const char *idTag, const char *reason, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (!idTag || strnlen(idTag, MO_IDTOKEN_LEN_MAX + 2) > MO_IDTOKEN_LEN_MAX) { + MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", MO_IDTOKEN_LEN_MAX); + return false; + } + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->endAuthorization(idTag, true); + } + #endif + + bool res = false; + if (isTransactionActive(connectorId) && getTransactionIdTag(connectorId)) { + //end transaction now if either idTag is nullptr (i.e. force stop) or the idTag matches beginTransaction + if (!idTag || !strcmp(idTag, getTransactionIdTag(connectorId))) { + res = endTransaction_authorized(idTag, reason, connectorId); + } else { + auto tx = getTransaction(connectorId); + const char *parentIdTag = tx->getParentIdTag(); + if (strlen(parentIdTag) > 0) + { + // We have a parent ID tag, so we need to check if this new card also has one + auto authorize = makeRequest(new Ocpp16::Authorize(context->getModel(), idTag)); + auto idTag_capture = makeString("MicroOcpp.cpp", idTag); + auto reason_capture = makeString("MicroOcpp.cpp", reason ? reason : ""); + authorize->setOnReceiveConfListener([idTag_capture, reason_capture, connectorId, tx] (JsonObject response) { + JsonObject idTagInfo = response["idTagInfo"]; + + if (strcmp("Accepted", idTagInfo["status"] | "UNDEFINED")) { + //Authorization rejected, do nothing + MO_DBG_DEBUG("Authorize rejected (%s), continue transaction", idTag_capture.c_str()); + auto connector = context->getModel().getConnector(connectorId); + if (connector) { + connector->updateTxNotification(TxNotification_AuthorizationRejected); + } + return; + } + if (idTagInfo.containsKey("parentIdTag") && !strcmp(idTagInfo["parenIdTag"], tx->getParentIdTag())) + { + endTransaction_authorized(idTag_capture.c_str(), reason_capture.empty() ? (const char*)nullptr : reason_capture.c_str(), connectorId); + } + }); + + authorize->setOnTimeoutListener([idTag_capture, connectorId] () { + //Authorization timed out, do nothing + MO_DBG_DEBUG("Authorization timeout (%s), continue transaction", idTag_capture.c_str()); + auto connector = context->getModel().getConnector(connectorId); + if (connector) { + connector->updateTxNotification(TxNotification_AuthorizationTimeout); + } + }); + + auto authorizationTimeoutInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", 20); + authorize->setTimeout(authorizationTimeoutInt && authorizationTimeoutInt->getInt() > 0 ? authorizationTimeoutInt->getInt() * 1000UL : 20UL * 1000UL); + + context->initiateRequest(std::move(authorize)); + res = true; + } else { + MO_DBG_INFO("endTransaction: idTag doesn't match"); + (void)0; + } + } + } + return res; +} + +bool endTransaction_authorized(const char *idTag, const char *reason, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (!idTag || strnlen(idTag, MO_IDTOKEN_LEN_MAX + 2) > MO_IDTOKEN_LEN_MAX) { + MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", MO_IDTOKEN_LEN_MAX); + return false; + } + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->endAuthorization(idTag, false); + } + #endif + + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return false; + } + auto res = isTransactionActive(connectorId); + connector->endTransaction(idTag, reason); + return res; +} + +bool isTransactionActive(unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->getTransaction() && evse->getTransaction()->active; + } + #endif + + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return false; + } + auto& tx = connector->getTransaction(); + return tx ? tx->isActive() : false; +} + +bool isTransactionRunning(unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->getTransaction() && evse->getTransaction()->started && !evse->getTransaction()->stopped; + } + #endif + + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return false; + } + auto& tx = connector->getTransaction(); + return tx ? tx->isRunning() : false; +} + +const char *getTransactionIdTag(unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return nullptr; + } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return nullptr; + } + return evse->getTransaction() ? evse->getTransaction()->idToken.get() : nullptr; + } + #endif + + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return nullptr; + } + auto& tx = connector->getTransaction(); + return tx ? tx->getIdTag() : nullptr; +} + +std::shared_ptr mocpp_undefinedTx; + +std::shared_ptr& getTransaction(unsigned int connectorId) { + if (!context) { + MO_DBG_WARN("OCPP uninitialized"); + return mocpp_undefinedTx; + } + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + MO_DBG_ERR("only supported in v16"); + return mocpp_undefinedTx; + } + #endif + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return mocpp_undefinedTx; + } + return connector->getTransaction(); +} + +#if MO_ENABLE_V201 +Ocpp201::Transaction *getTransactionV201(unsigned int evseId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return nullptr; + } + + if (context->getVersion().major != 2) { + MO_DBG_ERR("only supported in v201"); + return nullptr; + } + + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(evseId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return nullptr; + } + return evse->getTransaction(); +} +#endif //MO_ENABLE_V201 + +bool ocppPermitsCharge(unsigned int connectorId) { + if (!context) { + MO_DBG_WARN("OCPP uninitialized"); + return false; + } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return false; + } + return evse->ocppPermitsCharge(); + } +#endif + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return false; + } + return connector->ocppPermitsCharge(); +} + +ChargePointStatus getChargePointStatus(unsigned int connectorId) { + if (!context) { + MO_DBG_WARN("OCPP uninitialized"); + return ChargePointStatus_UNDEFINED; + } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto availabilityService = context->getModel().getAvailabilityService()) { + if (auto evse = availabilityService->getEvse(connectorId)) { + return evse->getStatus(); + } + } + } +#endif + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return ChargePointStatus_UNDEFINED; + } + return connector->getStatus(); +} + +void setConnectorPluggedInput(std::function pluggedInput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto availabilityService = context->getModel().getAvailabilityService()) { + if (auto evse = availabilityService->getEvse(connectorId)) { + evse->setConnectorPluggedInput(pluggedInput); + } + } + if (auto txService = context->getModel().getTransactionService()) { + if (auto evse = txService->getEvse(connectorId)) { + evse->setConnectorPluggedInput(pluggedInput); + } + } + return; + } +#endif + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return; + } + connector->setConnectorPluggedInput(pluggedInput); +} + +void setEnergyMeterInput(std::function energyInput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + addMeterValueInput([energyInput] () {return static_cast(energyInput());}, "Energy.Active.Import.Register", "Wh", nullptr, nullptr, connectorId); + return; + } + #endif + + SampledValueProperties meterProperties; + meterProperties.setMeasurand("Energy.Active.Import.Register"); + meterProperties.setUnit("Wh"); + auto mvs = std::unique_ptr>>( + new SampledValueSamplerConcrete>( + meterProperties, + [energyInput] (ReadingContext) {return energyInput();} + )); + addMeterValueInput(std::move(mvs), connectorId); +} + +void setPowerMeterInput(std::function powerInput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + addMeterValueInput([powerInput] () {return static_cast(powerInput());}, "Power.Active.Import", "W", nullptr, nullptr, connectorId); + return; + } + #endif + + SampledValueProperties meterProperties; + meterProperties.setMeasurand("Power.Active.Import"); + meterProperties.setUnit("W"); + auto mvs = std::unique_ptr>>( + new SampledValueSamplerConcrete>( + meterProperties, + [powerInput] (ReadingContext) {return powerInput();} + )); + addMeterValueInput(std::move(mvs), connectorId); +} + +void setSmartChargingPowerOutput(std::function chargingLimitOutput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + if (!context->getModel().getConnector(connectorId)) { + MO_DBG_ERR("could not find connector"); + return; + } + + if (chargingLimitOutput) { + setSmartChargingOutput([chargingLimitOutput] (float power, float current, int nphases) -> void { + chargingLimitOutput(power); + }, connectorId); + } else { + setSmartChargingOutput(nullptr, connectorId); + } + + if (auto scService = context->getModel().getSmartChargingService()) { + if (chargingLimitOutput) { + scService->updateAllowedChargingRateUnit(true, false); + } else { + scService->updateAllowedChargingRateUnit(false, false); + } + } +} + +void setSmartChargingCurrentOutput(std::function chargingLimitOutput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + if (!context->getModel().getConnector(connectorId)) { + MO_DBG_ERR("could not find connector"); + return; + } + + if (chargingLimitOutput) { + setSmartChargingOutput([chargingLimitOutput] (float power, float current, int nphases) -> void { + chargingLimitOutput(current); + }, connectorId); + } else { + setSmartChargingOutput(nullptr, connectorId); + } + + if (auto scService = context->getModel().getSmartChargingService()) { + if (chargingLimitOutput) { + scService->updateAllowedChargingRateUnit(false, true); + } else { + scService->updateAllowedChargingRateUnit(false, false); + } + } +} + +void setSmartChargingOutput(std::function chargingLimitOutput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + if (!context->getModel().getConnector(connectorId)) { + MO_DBG_ERR("could not find connector"); + return; + } + + auto& model = context->getModel(); + if (!model.getSmartChargingService() && chargingLimitOutput) { + model.setSmartChargingService(std::unique_ptr( + new SmartChargingService(*context, filesystem, MO_NUMCONNECTORS))); + } + + if (auto scService = context->getModel().getSmartChargingService()) { + scService->setSmartChargingOutput(connectorId, chargingLimitOutput); + if (chargingLimitOutput) { + scService->updateAllowedChargingRateUnit(true, true); + } else { + scService->updateAllowedChargingRateUnit(false, false); + } + } +} + +void setEvReadyInput(std::function evReadyInput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto txService = context->getModel().getTransactionService()) { + if (auto evse = txService->getEvse(connectorId)) { + evse->setEvReadyInput(evReadyInput); + } + } + return; + } +#endif + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return; + } + connector->setEvReadyInput(evReadyInput); +} + +void setEvseReadyInput(std::function evseReadyInput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto txService = context->getModel().getTransactionService()) { + if (auto evse = txService->getEvse(connectorId)) { + evse->setEvseReadyInput(evseReadyInput); + } + } + return; + } +#endif + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return; + } + connector->setEvseReadyInput(evseReadyInput); +} + +void addErrorCodeInput(std::function errorCodeInput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return; + } + connector->addErrorCodeInput(errorCodeInput); +} + +void addErrorDataInput(std::function errorDataInput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return; + } + connector->addErrorDataInput(errorDataInput); +} + +void addMeterValueInput(std::function valueInput, const char *measurand, const char *unit, const char *location, const char *phase, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + + if (!valueInput) { + MO_DBG_ERR("value undefined"); + return; + } + + if (!measurand) { + measurand = "Energy.Active.Import.Register"; + MO_DBG_WARN("measurand unspecified; assume %s", measurand); + } + + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + auto& model = context->getModel(); + if (!model.getMeteringServiceV201()) { + model.setMeteringServiceV201(std::unique_ptr( + new Ocpp201::MeteringService(context->getModel(), MO_NUM_EVSEID))); + } + if (auto mEvse = model.getMeteringServiceV201()->getEvse(connectorId)) { + + Ocpp201::SampledValueProperties properties; + properties.setMeasurand(measurand); //mandatory for MO + + if (unit) + properties.setUnitOfMeasureUnit(unit); + if (location) + properties.setLocation(location); + if (phase) + properties.setPhase(phase); + + mEvse->addMeterValueInput([valueInput] (ReadingContext) {return static_cast(valueInput());}, properties); + } else { + MO_DBG_ERR("inalid arg"); + } + return; + } + #endif + + SampledValueProperties properties; + properties.setMeasurand(measurand); //mandatory for MO + + if (unit) + properties.setUnit(unit); + if (location) + properties.setLocation(location); + if (phase) + properties.setPhase(phase); + + auto valueSampler = std::unique_ptr>>( + new MicroOcpp::SampledValueSamplerConcrete>( + properties, + [valueInput] (ReadingContext) {return valueInput();})); + addMeterValueInput(std::move(valueSampler), connectorId); +} + +void addMeterValueInput(std::unique_ptr valueInput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + MO_DBG_ERR("addMeterValueInput(std::unique_ptr...) not compatible with v201. Use addMeterValueInput(std::function...) instead"); + return; + } + #endif + auto& model = context->getModel(); + if (!model.getMeteringService()) { + model.setMeteringSerivce(std::unique_ptr( + new MeteringService(*context, MO_NUMCONNECTORS, filesystem))); + } + model.getMeteringService()->addMeterValueSampler(connectorId, std::move(valueInput)); +} + +void setOccupiedInput(std::function occupied, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto availabilityService = context->getModel().getAvailabilityService()) { + if (auto evse = availabilityService->getEvse(connectorId)) { + evse->setOccupiedInput(occupied); + } + } + return; + } +#endif + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return; + } + connector->setOccupiedInput(occupied); +} + +void setStartTxReadyInput(std::function startTxReady, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return; + } + connector->setStartTxReadyInput(startTxReady); +} + +void setStopTxReadyInput(std::function stopTxReady, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return; + } + connector->setStopTxReadyInput(stopTxReady); +} + +void setTxNotificationOutput(std::function notificationOutput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + #if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + MO_DBG_ERR("only supported in v16"); + return; + } + #endif + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return; + } + connector->setTxNotificationOutput(notificationOutput); +} + +#if MO_ENABLE_V201 +void setTxNotificationOutputV201(std::function notificationOutput, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + + if (context->getVersion().major != 2) { + MO_DBG_ERR("only supported in v201"); + return; + } + + TransactionService::Evse *evse = nullptr; + if (auto txService = context->getModel().getTransactionService()) { + evse = txService->getEvse(connectorId); + } + if (!evse) { + MO_DBG_ERR("could not find EVSE"); + return; + } + evse->setTxNotificationOutput(notificationOutput); +} +#endif //MO_ENABLE_V201 + +#if MO_ENABLE_CONNECTOR_LOCK +void setOnUnlockConnectorInOut(std::function onUnlockConnectorInOut, unsigned int connectorId) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + auto connector = context->getModel().getConnector(connectorId); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return; + } + connector->setOnUnlockConnector(onUnlockConnectorInOut); +} +#endif //MO_ENABLE_CONNECTOR_LOCK + +bool isOperative(unsigned int connectorId) { + if (!context) { + MO_DBG_WARN("OCPP uninitialized"); + return true; //assume "true" as default state + } +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto availabilityService = context->getModel().getAvailabilityService()) { + auto chargePoint = availabilityService->getEvse(OCPP_ID_OF_CP); + auto connector = availabilityService->getEvse(connectorId); + if (!chargePoint || !connector) { + MO_DBG_ERR("could not find connector"); + return true; //assume "true" as default state + } + return chargePoint->isAvailable() && connector->isAvailable(); + } + } +#endif + auto& model = context->getModel(); + auto chargePoint = model.getConnector(OCPP_ID_OF_CP); + auto connector = model.getConnector(connectorId); + if (!chargePoint || !connector) { + MO_DBG_ERR("could not find connector"); + return true; //assume "true" as default state + } + return chargePoint->isOperative() && connector->isOperative(); +} + +void setOnResetNotify(std::function onResetNotify) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto rService = context->getModel().getResetServiceV201()) { + rService->setNotifyReset([onResetNotify] (ResetType) {return onResetNotify(true);}); + } + return; + } +#endif + + if (auto rService = context->getModel().getResetService()) { + rService->setPreReset(onResetNotify); + } +} + +void setOnResetExecute(std::function onResetExecute) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + +#if MO_ENABLE_V201 + if (context->getVersion().major == 2) { + if (auto rService = context->getModel().getResetServiceV201()) { + rService->setExecuteReset([onResetExecute] () {onResetExecute(true); return true;}); + } + return; + } +#endif + + if (auto rService = context->getModel().getResetService()) { + rService->setExecuteReset(onResetExecute); + } +} + +FirmwareService *getFirmwareService() { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return nullptr; + } + + auto& model = context->getModel(); + if (!model.getFirmwareService()) { + model.setFirmwareService(std::unique_ptr( + new FirmwareService(*context))); + } + + return model.getFirmwareService(); +} + +DiagnosticsService *getDiagnosticsService() { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return nullptr; + } + + auto& model = context->getModel(); + if (!model.getDiagnosticsService()) { + model.setDiagnosticsService(std::unique_ptr( + new DiagnosticsService(*context))); + } + + return model.getDiagnosticsService(); +} + +#if MO_ENABLE_CERT_MGMT + +void setCertificateStore(std::unique_ptr certStore) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + + auto& model = context->getModel(); + if (!model.getCertificateService()) { + model.setCertificateService(std::unique_ptr( + new CertificateService(*context))); + } + if (auto certService = model.getCertificateService()) { + certService->setCertificateStore(std::move(certStore)); + } else { + MO_DBG_ERR("OOM"); + } +} +#endif //MO_ENABLE_CERT_MGMT + +Context *getOcppContext() { + return context; +} + +void setOnReceiveRequest(const char *operationType, OnReceiveReqListener onReceiveReq) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + if (!operationType) { + MO_DBG_ERR("invalid args"); + return; + } + context->getOperationRegistry().setOnRequest(operationType, onReceiveReq); +} + +void setOnSendConf(const char *operationType, OnSendConfListener onSendConf) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + if (!operationType) { + MO_DBG_ERR("invalid args"); + return; + } + context->getOperationRegistry().setOnResponse(operationType, onSendConf); +} + +void sendRequest(const char *operationType, + std::function ()> fn_createReq, + std::function fn_processConf) { + + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + if (!operationType || !fn_createReq || !fn_processConf) { + MO_DBG_ERR("invalid args"); + return; + } + + auto request = makeRequest(new CustomOperation(operationType, fn_createReq, fn_processConf)); + context->initiateRequest(std::move(request)); +} + +void setRequestHandler(const char *operationType, + std::function fn_processReq, + std::function ()> fn_createConf) { + + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + if (!operationType || !fn_processReq || !fn_createConf) { + MO_DBG_ERR("invalid args"); + return; + } + + auto captureOpType = makeString("MicroOcpp.cpp", operationType); + + context->getOperationRegistry().registerOperation(operationType, [captureOpType, fn_processReq, fn_createConf] () { + return new CustomOperation(captureOpType.c_str(), fn_processReq, fn_createConf); + }); +} + +void authorize(const char *idTag, OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, unsigned int timeout) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return; + } + if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX) { + MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); + return; + } + auto authorize = makeRequest( + new Authorize(context->getModel(), idTag)); + if (onConf) + authorize->setOnReceiveConfListener(onConf); + if (onAbort) + authorize->setOnAbortListener(onAbort); + if (onTimeout) + authorize->setOnTimeoutListener(onTimeout); + if (onError) + authorize->setOnReceiveErrorListener(onError); + if (timeout) + authorize->setTimeout(timeout); + else + authorize->setTimeout(20000); + context->initiateRequest(std::move(authorize)); +} + +bool startTransaction(const char *idTag, OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, unsigned int timeout) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + if (!idTag || strnlen(idTag, IDTAG_LEN_MAX + 2) > IDTAG_LEN_MAX) { + MO_DBG_ERR("idTag format violation. Expect c-style string with at most %u characters", IDTAG_LEN_MAX); + return false; + } + auto connector = context->getModel().getConnector(OCPP_ID_OF_CONNECTOR); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return false; + } + auto transaction = connector->getTransaction(); + if (transaction) { + if (transaction->getStartSync().isRequested()) { + MO_DBG_ERR("transaction already in progress. Must call stopTransaction()"); + return false; + } + transaction->setIdTag(idTag); + } else { + beginTransaction_authorized(idTag); //request new transaction object + transaction = connector->getTransaction(); + if (!transaction) { + MO_DBG_WARN("transaction queue full"); + return false; + } + } + + if (auto mService = context->getModel().getMeteringService()) { + auto meterStart = mService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext_TransactionBegin); + if (meterStart && *meterStart) { + transaction->setMeterStart(meterStart->toInteger()); + } else { + MO_DBG_ERR("meterStart undefined"); + } + } + + transaction->setStartTimestamp(context->getModel().getClock().now()); + + transaction->commit(); + + auto startTransaction = makeRequest( + new StartTransaction(context->getModel(), transaction)); + if (onConf) + startTransaction->setOnReceiveConfListener(onConf); + if (onAbort) + startTransaction->setOnAbortListener(onAbort); + if (onTimeout) + startTransaction->setOnTimeoutListener(onTimeout); + if (onError) + startTransaction->setOnReceiveErrorListener(onError); + if (timeout) + startTransaction->setTimeout(timeout); + else + startTransaction->setTimeout(0); + context->initiateRequest(std::move(startTransaction)); + + return true; +} + +bool stopTransaction(OnReceiveConfListener onConf, OnAbortListener onAbort, OnTimeoutListener onTimeout, OnReceiveErrorListener onError, unsigned int timeout) { + if (!context) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return false; + } + auto connector = context->getModel().getConnector(OCPP_ID_OF_CONNECTOR); + if (!connector) { + MO_DBG_ERR("could not find connector"); + return false; + } + + auto transaction = connector->getTransaction(); + if (!transaction || !transaction->isRunning()) { + MO_DBG_ERR("no running Tx to stop"); + return false; + } + + connector->endTransaction(transaction->getIdTag(), "Local"); + + const char *idTag = transaction->getIdTag(); + if (idTag) { + transaction->setStopIdTag(idTag); + } + + if (auto mService = context->getModel().getMeteringService()) { + auto meterStop = mService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext_TransactionEnd); + if (meterStop && *meterStop) { + transaction->setMeterStop(meterStop->toInteger()); + } else { + MO_DBG_ERR("meterStop undefined"); + } + } + + transaction->setStopTimestamp(context->getModel().getClock().now()); + + transaction->commit(); + + auto stopTransaction = makeRequest( + new StopTransaction(context->getModel(), transaction)); + if (onConf) + stopTransaction->setOnReceiveConfListener(onConf); + if (onAbort) + stopTransaction->setOnAbortListener(onAbort); + if (onTimeout) + stopTransaction->setOnTimeoutListener(onTimeout); + if (onError) + stopTransaction->setOnReceiveErrorListener(onError); + if (timeout) + stopTransaction->setTimeout(timeout); + else + stopTransaction->setTimeout(0); + context->initiateRequest(std::move(stopTransaction)); + + return true; +} diff --git a/src/MicroOcpp.h b/src/MicroOcpp.h new file mode 100644 index 00000000..f15c13cc --- /dev/null +++ b/src/MicroOcpp.h @@ -0,0 +1,583 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_MICROOCPP_H +#define MO_MICROOCPP_H + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using MicroOcpp::OnReceiveConfListener; +using MicroOcpp::OnReceiveReqListener; +using MicroOcpp::OnSendConfListener; +using MicroOcpp::OnAbortListener; +using MicroOcpp::OnTimeoutListener; +using MicroOcpp::OnReceiveErrorListener; + +#ifndef MO_CUSTOM_WS +//use links2004/WebSockets library + +/* + * Initialize the library with the OCPP URL, EVSE voltage and filesystem configuration. + * + * If the connections fails, please refer to + * https://github.com/matth-x/MicroOcpp/issues/36#issuecomment-989716573 for recommendations on + * how to track down the issue with the connection. + * + * This is a convenience function only available for Arduino. + */ +void mocpp_initialize( + const char *backendUrl, //e.g. "wss://example.com:8443/steve/websocket/CentralSystemService" + const char *chargeBoxId, //e.g. "charger001" + const char *chargePointModel = "Demo Charger", //model name of this charger + const char *chargePointVendor = "My Company Ltd.", //brand name + MicroOcpp::FilesystemOpt fsOpt = MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail, //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h + const char *password = nullptr, //password present in the websocket message header + const char *CA_cert = nullptr, //TLS certificate + bool autoRecover = false); //automatically sanitize the local data store when the lib detects recurring crashes. Not recommended during development +#endif + +/* + * Convenience initialization: use this for passing the BootNotification payload JSON to the mocpp_initialize(...) below + * + * Example usage: + * + * mocpp_initialize(osock, ChargerCredentials("Demo Charger", "My Company Ltd.")); + * + * For a description of the fields, refer to OCPP 1.6 Specification - Edition 2 p. 60 + */ +struct ChargerCredentials { + ChargerCredentials( + const char *chargePointModel = "Demo Charger", + const char *chargePointVendor = "My Company Ltd.", + const char *firmwareVersion = nullptr, + const char *chargePointSerialNumber = nullptr, + const char *meterSerialNumber = nullptr, + const char *meterType = nullptr, + const char *chargeBoxSerialNumber = nullptr, + const char *iccid = nullptr, + const char *imsi = nullptr); + + /* + * OCPP 2.0.1 compatible charger credentials. Use this if initializing the library with ProtocolVersion(2,0,1) + */ + static ChargerCredentials v201( + const char *chargePointModel = "Demo Charger", + const char *chargePointVendor = "My Company Ltd.", + const char *firmwareVersion = nullptr, + const char *chargePointSerialNumber = nullptr, + const char *meterSerialNumber = nullptr, + const char *meterType = nullptr, + const char *chargeBoxSerialNumber = nullptr, + const char *iccid = nullptr, + const char *imsi = nullptr); + + operator const char *() {return payload;} + +private: + char payload [512] = {'{', '}', '\0'}; +}; + +/* + * Initialize the library with a WebSocket connection which is configured with protocol=ocpp1.6 + * (=Connection), EVSE voltage and filesystem configuration. This library requires that you handle + * establishing the connection and keeping it alive. Please refer to + * https://github.com/matth-x/MicroOcpp/tree/main/examples/ESP-TLS for an example how to use it. + * + * This GitHub project also delivers an Connection implementation based on links2004/WebSockets. If + * you need another WebSockets implementation, you can subclass the Connection class and pass it to + * this initialize() function. Please refer to + * https://github.com/OpenEVSE/ESP32_WiFi_V4.x/blob/master/src/MongooseConnectionClient.cpp for + * an example. + */ +void mocpp_initialize( + MicroOcpp::Connection& connection, //WebSocket adapter for MicroOcpp + const char *bootNotificationCredentials = ChargerCredentials("Demo Charger", "My Company Ltd."), //e.g. '{"chargePointModel":"Demo Charger","chargePointVendor":"My Company Ltd."}' (refer to OCPP 1.6 Specification - Edition 2 p. 60) + std::shared_ptr filesystem = + MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail), //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h + bool autoRecover = false, //automatically sanitize the local data store when the lib detects recurring crashes. Not recommended during development + MicroOcpp::ProtocolVersion version = MicroOcpp::ProtocolVersion(1,6)); + +/* + * Stop the OCPP library and release allocated resources. + */ +void mocpp_deinitialize(); + +/* + * To be called in the main loop (e.g. place it inside loop()) + */ +void mocpp_loop(); + +/* + * Transaction management. + * + * OCPP 1.6 (2.0.1 see below): + * Begin the transaction process and prepare it. When all conditions for the transaction are true, + * eventually send a StartTransaction request to the OCPP server. + * Conditions: + * 1) the connector is operative (no faults reported, not set "Unavailable" by the backend) + * 2) no reservation blocks the connector + * 3) the idTag is authorized for charging. The transaction process will send an Authorize message + * to the server for approval, except if the charger is offline, then the Local Authorization + * rules will apply as in the specification. + * 4) the vehicle is already plugged or will be plugged soon (only applicable if the + * ConnectorPlugged Input is set) + * + * See beginTransaction_authorized for skipping steps 1) to 3) + * + * Returns true if it was possible to create the transaction process. Returns + * false if either another transaction process is still active or you need to try it again later. + * + * OCPP 2.0.1: + * Authorize a transaction. Like the OCPP 1.6 behavior, this should be called when the user swipes the + * card to start charging, but the semantic is slightly different. This function begins the authorized + * phase, but a transaction may already have started due to an earlier transaction start point. + */ +bool beginTransaction(const char *idTag, unsigned int connectorId = 1); + +/* + * Begin the transaction process and skip the OCPP-side authorization. See beginTransaction(...) for a + * complete description + */ +bool beginTransaction_authorized(const char *idTag, const char *parentIdTag = nullptr, unsigned int connectorId = 1); + +/* + * OCPP 1.6 (2.0.1 see below): + * End the transaction process if idTag is authorized to stop the transaction. The OCPP lib sends + * a StopTransaction request if the following conditions are true: + * Conditions: + * 1) Currently, a transaction is running which hasn't been terminated yet AND + * 2) idTag is either + * - nullptr OR + * - matches the idTag of beginTransaction (or RemoteStartTransaction) OR + * - [Planned, not released yet] is part of the current LocalList and the parentIdTag + * matches with the parentIdTag of beginTransaction. + * - [Planned, not released yet] If none of step 2) applies, then the OCPP lib will check + * the authorization status via an Authorize request + * + * See endTransaction_authorized for skipping the authorization check, i.e. step 2) + * + * If the transaction is ended by swiping an RFID card, then idTag should contain its identifier. If + * charging stops for a different reason than swiping the card, idTag should be null or empty. + * + * Please refer to OCPP 1.6 Specification - Edition 2 p. 90 for a list of valid reasons. `reason` + * can also be nullptr. + * + * It is safe to call this function at any time, i.e. when no transaction runs or when the transaction + * has already been ended. For example you can place + * `endTransaction(nullptr, "Reboot");` + * in the beginning of the program just to ensure that there is no transaction from a previous run. + * + * If called with idTag=nullptr, this is functionally equivalent to + * `endTransaction_authorized(nullptr, reason);` + * + * Returns true if there is a transaction which could eventually be ended by this action + * + * OCPP 2.0.1: + * End the user authorization. Like when running with OCPP 1.6, this should be called when the user + * swipes the card to stop charging. The difference between the 1.6/2.0.1 behavior is that in 1.6, + * endTransaction always sets the transaction inactive so that it wants to stop. In 2.0.1, this only + * revokes the user authorization which may terminate the transaction but doesn't have to if the + * transaction stop point is set to EvConnected. + * + * Note: the stop reason parameter is ignored when running with OCPP 2.0.1. It's always Local + */ +bool endTransaction(const char *idTag = nullptr, const char *reason = nullptr, unsigned int connectorId = 1); + +/* + * End the transaction process definitely without authorization check. See endTransaction(...) for a + * complete description. + * + * Use this function if you manage authorization on your own and want to bypass the Authorization + * management of this lib. + */ +bool endTransaction_authorized(const char *idTag, const char *reason = nullptr, unsigned int connectorId = 1); + +/* + * Get information about the current Transaction lifecycle. A transaction can enter the following + * states: + * - Idle: no transaction running or being started + * - Preparing: before a potential transaction + * - Aborted: transaction not started and never will be started + * - Running: transaction started and running + * - Running/StopTxAwait: transaction still running but will end at the next possible time + * - Finished: transaction stopped + * + * isTransactionActive() and isTransactionRunning() give the status by combining them: + * + * State | isTransactionActive() | isTransactionRunning() + * --------------------+-----------------------+----------------------- + * Preparing | true | false + * Running | true | true + * Running/StopTxAwait | false | true + * Finished / Aborted | | + * / Idle | false | false + */ +bool isTransactionActive(unsigned int connectorId = 1); +bool isTransactionRunning(unsigned int connectorId = 1); + +/* + * Get the idTag which has been used to start the transaction. If no transaction process is + * running, this function returns nullptr + */ +const char *getTransactionIdTag(unsigned int connectorId = 1); + +/* + * Returns the current transaction process. Returns nullptr if no transaction is running, preparing or finishing + * + * See the class definition in MicroOcpp/Model/Transactions/Transaction.h for possible uses of this object + * + * Examples: + * auto tx = getTransaction(); //fetch tx object + * if (tx) { //check if tx object exists + * bool active = tx->isActive(); //active tells if the transaction is preparing or continuing to run + * //inactive means that the transaction is about to stop, stopped or won't be started anymore + * int transactionId = tx->getTransactionId(); //the transactionId as assigned by the OCPP server + * bool deauthorized = tx->isIdTagDeauthorized(); //if StartTransaction has been rejected + * } + */ +std::shared_ptr& getTransaction(unsigned int connectorId = 1); + +#if MO_ENABLE_V201 +/* + * OCPP 2.0.1 version of getTransaction(). Note that the return transaction object is of another type + * and unlike the 1.6 version, this function does not give ownership. + */ +MicroOcpp::Ocpp201::Transaction *getTransactionV201(unsigned int evseId = 1); +#endif //MO_ENABLE_V201 + +/* + * Returns if the OCPP library allows the EVSE to charge at the moment. + * + * If you integrate it into a J1772 charger, true means that the Control Pilot can send the PWM signal + * and false means that the Control Pilot must be at a DC voltage. + */ +bool ocppPermitsCharge(unsigned int connectorId = 1); + +/* + * Returns the latest ChargePointStatus as reported via StatusNotification (standard OCPP data type) + */ +ChargePointStatus getChargePointStatus(unsigned int connectorId = 1); + +/* + * Define the Inputs and Outputs of this library. + * + * This library interacts with the hardware of your charger by Inputs and Outputs. Inputs and Outputs + * are tiny function-objects which read information from the EVSE or control the behavior of the EVSE. + * + * An Input is a function which returns the current state of a variable of the EVSE. For example, if + * the energy meter stores the energy register in the global variable `e_reg`, then you can allow + * this library to read it by defining the Input + * `[] () {return e_reg;}` + * and passing it to the library. + * + * An Output is a function which gets a state value from the OCPP library and applies it to the EVSE. + * For example, to let Smart Charging control the PWM signal of the Control Pilot, define the Output + * `[] (float p_max) {pwm = p_max / PWM_FACTOR;}` (simplified example) + * and pass it to the library. + * + * Configure the library with Inputs and Outputs once in the setup() function. + */ + +void setConnectorPluggedInput(std::function pluggedInput, unsigned int connectorId = 1); //Input about if an EV is plugged to this EVSE + +void setEnergyMeterInput(std::function energyInput, unsigned int connectorId = 1); //Input of the electricity meter register in Wh + +void setPowerMeterInput(std::function powerInput, unsigned int connectorId = 1); //Input of the power meter reading in W + +//Smart Charging Output, alternative for Watts only, Current only, or Watts x Current x numberPhases. +//Only one of the Smart Charging Outputs can be set at a time. +//MO will execute the callback whenever the OCPP charging limit changes and will pass the limit for now +//to the callback. If OCPP does not define a limit, then MO passes the value -1 for "undefined". +void setSmartChargingPowerOutput(std::function chargingLimitOutput, unsigned int connectorId = 1); //Output (in Watts) for the Smart Charging limit +void setSmartChargingCurrentOutput(std::function chargingLimitOutput, unsigned int connectorId = 1); //Output (in Amps) for the Smart Charging limit +void setSmartChargingOutput(std::function chargingLimitOutput, unsigned int connectorId = 1); //Output (in Watts, Amps, numberPhases) for the Smart Charging limit + +/* + * Define the Inputs and Outputs of this library. (Advanced) + * + * These Inputs and Outputs are optional depending on the use case of your charger. + */ + +void setEvReadyInput(std::function evReadyInput, unsigned int connectorId = 1); //Input if EV is ready to charge (= J1772 State C) + +void setEvseReadyInput(std::function evseReadyInput, unsigned int connectorId = 1); //Input if EVSE allows charge (= PWM signal on) + +void addErrorCodeInput(std::function errorCodeInput, unsigned int connectorId = 1); //Input for Error codes (please refer to OCPP 1.6, Edit2, p. 71 and 72 for valid error codes) +void addErrorDataInput(std::function errorDataInput, unsigned int connectorId = 1); + +void addMeterValueInput(std::function valueInput, const char *measurand = nullptr, const char *unit = nullptr, const char *location = nullptr, const char *phase = nullptr, unsigned int connectorId = 1); //integrate further metering Inputs + +void addMeterValueInput(std::unique_ptr valueInput, unsigned int connectorId = 1); //integrate further metering Inputs (more extensive alternative) + +void setOccupiedInput(std::function occupied, unsigned int connectorId = 1); //Input if instead of Available, send StatusNotification Preparing / Finishing + +void setStartTxReadyInput(std::function startTxReady, unsigned int connectorId = 1); //Input if the charger is ready for StartTransaction + +void setStopTxReadyInput(std::function stopTxReady, unsigned int connectorId = 1); //Input if charger is ready for StopTransaction + +void setTxNotificationOutput(std::function notificationOutput, unsigned int connectorId = 1); //called when transaction state changes (see TxNotification for possible events). Transaction can be null + +#if MO_ENABLE_V201 +void setTxNotificationOutputV201(std::function notificationOutput, unsigned int connectorId = 1); +#endif //MO_ENABLE_V201 + +#if MO_ENABLE_CONNECTOR_LOCK +/* + * Set an InputOutput (reads and sets information at the same time) for forcing to unlock the + * connector. Called as part of the OCPP operation "UnlockConnector" + * Return values: + * - UnlockConnectorResult_Pending if action needs more time to complete (MO will call this cb again later or eventually time out) + * - UnlockConnectorResult_Unlocked if successful + * - UnlockConnectorResult_UnlockFailed if not successful (e.g. lock stuck) + */ +void setOnUnlockConnectorInOut(std::function onUnlockConnectorInOut, unsigned int connectorId = 1); +#endif //MO_ENABLE_CONNECTOR_LOCK + +/* + * Access further information about the internal state of the library + */ + +bool isOperative(unsigned int connectorId = 1); //if the charge point is operative (see OCPP1.6 Edit2, p. 45) and ready for transactions + +/* + * Configure the device management + */ + +void setOnResetNotify(std::function onResetNotify); //call onResetNotify(isHard) before Reset. If you return false, Reset will be aborted. Optional + +void setOnResetExecute(std::function onResetExecute); //reset handler. This function should reboot this controller immediately. Already defined for the ESP32 on Arduino + + +namespace MicroOcpp { +class FirmwareService; +class DiagnosticsService; +} + +/* + * You need to configure this object if FW updates are relevant for you. This project already + * brings a simple configuration for the ESP32 and ESP8266 for prototyping purposes, however + * for the productive system you will have to develop a configuration targeting the specific + * OCPP backend. + * See MicroOcpp/Model/FirmwareManagement/FirmwareService.h + * + * Lazy initialization: The FW Service will be created at the first call to this function + * + * To use, add `#include ` + */ +MicroOcpp::FirmwareService *getFirmwareService(); + +/* + * This library implements the OCPP messaging side of Diagnostics, but no logging or the + * log upload to your backend. + * To integrate Diagnostics, see MicroOcpp/Model/Diagnostics/DiagnosticsService.h + * + * Lazy initialization: The Diag Service will be created at the first call to this function + * + * To use, add `#include ` + */ +MicroOcpp::DiagnosticsService *getDiagnosticsService(); + +#if MO_ENABLE_CERT_MGMT +/* + * Set a custom Certificate Store which implements certificate updates on the host system. + * MicroOcpp will forward OCPP-side update requests to the certificate store, as well as + * query the certificate store upon server request. + * + * To enable OCPP-side certificate updates (UCs M03 - M05), set the build flag + * MO_ENABLE_CERT_MGMT=1 so that this function becomes accessible. + * + * To use the built-in certificate store (depends on MbedTLS), set the build flag + * MO_ENABLE_MBEDTLS=1. To not use the built-in implementation, but still enable MbedTLS, + * additionally set MO_ENABLE_CERT_STORE_MBEDTLS=0. + */ +void setCertificateStore(std::unique_ptr certStore); +#endif //MO_ENABLE_CERT_MGMT + +/* + * Add features and customize the behavior of the OCPP client + */ + +namespace MicroOcpp { +class Context; +} + +//Get access to internal functions and data structures. The returned Context object allows +//you to bypass the facade functions of this header and implement custom functionality. +//To use, add `#include ` +MicroOcpp::Context *getOcppContext(); + +/* + * Set a listener which is notified when the OCPP lib processes an incoming operation of type + * operationType. After the operation has been interpreted, onReceiveReq will be called with + * the original message from the OCPP server. + * + * Example usage: + * + * setOnReceiveRequest("SetChargingProfile", [] (JsonObject payload) { + * Serial.print("[main] received charging profile for connector: "; //Arduino print function + * Serial.printf("update connector %i with chargingProfileId %i\n", + * payload["connectorId"], //ArduinoJson object access + * payload["csChargingProfiles"]["chargingProfileId"]); + * }); + */ +void setOnReceiveRequest(const char *operationType, OnReceiveReqListener onReceiveReq); + +/* + * Set a listener which is notified when the OCPP lib sends the confirmation to an incoming + * operation of type operation type. onSendConf will be passed the original output of the + * OCPP lib. + * + * Example usage: + * + * setOnSendConf("RemoteStopTransaction", [] (JsonObject payload) -> void { + * if (!strcmp(payload["status"], "Rejected")) { + * //the OCPP lib rejected the RemoteStopTransaction command. In this example, the customer + * //wishes to stop the running transaction in any case and to log this case + * endTransaction(nullptr, "Remote"); //end transaction and send StopTransaction + * Serial.println("[main] override rejected RemoteStopTransaction"); //Arduino print function + * } + * }); + * + */ +void setOnSendConf(const char *operationType, OnSendConfListener onSendConf); + +/* + * Create and send an operation without using the built-in Operation class. This function bypasses + * the business logic which comes with this library. E.g. you can send unknown operations, extend + * OCPP or replace parts of the business logic with custom behavior. + * + * Use case 1, extend the library by sending additional operations. E.g. DataTransfer: + * + * sendRequest("DataTransfer", [] () -> std::unique_ptr { + * //will be called to create the request once this operation is being sent out + * size_t capacity = JSON_OBJECT_SIZE(3) + + * JSON_OBJECT_SIZE(2); //for calculating the required capacity, see https://arduinojson.org/v6/assistant/ + * auto res = std::unique_ptr(new MicroOcpp::JsonDoc(capacity)); + * JsonObject request = *res; + * request["vendorId"] = "My company Ltd."; + * request["messageId"] = "TargetValues"; + * request["data"]["battery_capacity"] = 89; + * request["data"]["battery_soc"] = 34; + * return res; + * }, [] (JsonObject response) -> void { + * //will be called with the confirmation response of the server + * if (!strcmp(response["status"], "Accepted")) { + * //DataTransfer has been accepted + * int max_energy = response["data"]["max_energy"]; + * } + * }); + * + * Use case 2, bypass the business logic of this library for custom behavior. E.g. StartTransaction: + * + * sendRequest("StartTransaction", [] () -> std::unique_ptr { + * //will be called to create the request once this operation is being sent out + * size_t capacity = JSON_OBJECT_SIZE(4); //for calculating the required capacity, see https://arduinojson.org/v6/assistant/ + * auto res = std::unique_ptr(new MicroOcpp::JsonDoc(capacity)); + * JsonObject request = res->to(); + * request["connectorId"] = 1; + * request["idTag"] = "A9C3CE1D7B71EA"; + * request["meterStart"] = 1234; + * request["timestamp"] = "2023-06-01T11:07:43Z"; //e.g. some historic transaction + * return res; + * }, [] (JsonObject response) -> void { + * //will be called with the confirmation response of the server + * const char *status = response["idTagInfo"]["status"]; + * int transactionId = response["transactionId"]; + * }); + * + * In Use case 2, the library won't send any further StatusNotification or StopTransaction on + * its own. + */ +void sendRequest(const char *operationType, + std::function ()> fn_createReq, + std::function fn_processConf); + +/* + * Set a custom handler for an incoming operation type. This will update the core Operation registry + * of the library, potentially replacing the built-in Operation handler and bypassing the + * business logic of this library. + * + * Note that when replacing an operation handler, the attached listeners will be reset. + * + * Example usage: + * + * setRequestHandler("DataTransfer", [] (JsonObject request) -> void { + * //will be called with the request message from the server + * const char *vendorId = request["vendorId"]; + * const char *messageId = request["messageId"]; + * int battery_capacity = request["data"]["battery_capacity"]; + * int battery_soc = request["data"]["battery_soc"]; + * }, [] () -> std::unique_ptr { + * //will be called to create the response once this operation is being sent out + * size_t capacity = JSON_OBJECT_SIZE(2) + + * JSON_OBJECT_SIZE(1); //for calculating the required capacity, see https://arduinojson.org/v6/assistant/ + * auto res = std::unique_ptr(new MicroOcpp::JsonDoc(capacity)); + * JsonObject response = res->to(); + * response["status"] = "Accepted"; + * response["data"]["max_energy"] = 59; + * return res; + * }); + */ +void setRequestHandler(const char *operationType, + std::function fn_processReq, + std::function ()> fn_createConf); + +/* + * Send OCPP operations manually not bypassing the internal business logic + * + * On receipt of the .conf() response the library calls the callback function + * "OnReceiveConfListener onConf" and passes the OCPP payload to it. + * + * For your first EVSE integration, the `onReceiveConfListener` is probably sufficient. For + * advanced EVSE projects, the other listeners likely become relevant: + * - `onAbortListener`: will be called whenever the engine stops trying to finish an operation + * normally which was initiated by this device. + * - `onTimeoutListener`: will be executed when the operation is not answered until the timeout + * expires. Note that timeouts also trigger the `onAbortListener`. + * - `onReceiveErrorListener`: will be called when the Central System returns a CallError. + * Again, each error also triggers the `onAbortListener`. + * + * The functions for sending OCPP operations are non-blocking. The program will resume immediately + * with the code after with the subsequent code in any case. + */ + +void authorize( + const char *idTag, //RFID tag (e.g. ISO 14443 UID tag with 4 or 7 bytes) + OnReceiveConfListener onConf = nullptr, //callback (confirmation received) + OnAbortListener onAbort = nullptr, //callback (confirmation not received), optional + OnTimeoutListener onTimeout = nullptr, //callback (timeout expired), optional + OnReceiveErrorListener onError = nullptr, //callback (error code received), optional + unsigned int timeout = 0); //custom timeout behavior, optional + +bool startTransaction( + const char *idTag, + OnReceiveConfListener onConf = nullptr, + OnAbortListener onAbort = nullptr, + OnTimeoutListener onTimeout = nullptr, + OnReceiveErrorListener onError = nullptr, + unsigned int timeout = 0); + +bool stopTransaction( + OnReceiveConfListener onConf = nullptr, + OnAbortListener onAbort = nullptr, + OnTimeoutListener onTimeout = nullptr, + OnReceiveErrorListener onError = nullptr, + unsigned int timeout = 0); + +#endif diff --git a/src/MicroOcpp/Core/Configuration.cpp b/src/MicroOcpp/Core/Configuration.cpp new file mode 100644 index 00000000..cb0a8ff5 --- /dev/null +++ b/src/MicroOcpp/Core/Configuration.cpp @@ -0,0 +1,258 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include + +#include +#include +#include + +namespace MicroOcpp { + +struct Validator { + const char *key = nullptr; + std::function checkValue; + Validator(const char *key, std::function checkValue) : key(key), checkValue(checkValue) { + + } +}; + +namespace ConfigurationLocal { + +std::shared_ptr filesystem; +auto configurationContainers = makeVector>("v16.Configuration.Containers"); +auto validators = makeVector("v16.Configuration.Validators"); + +} + +using namespace ConfigurationLocal; + +std::unique_ptr createConfigurationContainer(const char *filename, bool accessible) { + //create non-persistent Configuration store (i.e. lives only in RAM) if + // - Flash FS usage is switched off OR + // - Filename starts with "/volatile" + if (!filesystem || + !strncmp(filename, CONFIGURATION_VOLATILE, strlen(CONFIGURATION_VOLATILE))) { + return makeConfigurationContainerVolatile(filename, accessible); + } else { + //create persistent Configuration store. This is the normal case + return makeConfigurationContainerFlash(filesystem, filename, accessible); + } +} + + +void addConfigurationContainer(std::shared_ptr container) { + configurationContainers.push_back(container); +} + +std::shared_ptr getContainer(const char *filename) { + auto container = std::find_if(configurationContainers.begin(), configurationContainers.end(), + [filename](decltype(configurationContainers)::value_type &elem) { + return !strcmp(elem->getFilename(), filename); + }); + + if (container != configurationContainers.end()) { + return *container; + } else { + return nullptr; + } +} + +ConfigurationContainer *declareContainer(const char *filename, bool accessible) { + + auto container = getContainer(filename); + + if (!container) { + MO_DBG_DEBUG("init new configurations container: %s", filename); + + container = createConfigurationContainer(filename, accessible); + if (!container) { + MO_DBG_ERR("OOM"); + return nullptr; + } + configurationContainers.push_back(container); + } + + if (container->isAccessible() != accessible) { + MO_DBG_ERR("%s: conflicting accessibility declarations (expect %s)", filename, container->isAccessible() ? "accessible" : "inaccessible"); + } + + return container.get(); +} + +std::shared_ptr loadConfiguration(TConfig type, const char *key, bool accessible) { + for (auto& container : configurationContainers) { + if (auto config = container->getConfiguration(key)) { + if (config->getType() != type) { + MO_DBG_ERR("conflicting type for %s - remove old config", key); + container->remove(config.get()); + continue; + } + if (container->isAccessible() != accessible) { + MO_DBG_ERR("conflicting accessibility for %s", key); + } + container->loadStaticKey(*config.get(), key); + return config; + } + } + return nullptr; +} + +template +bool loadFactoryDefault(Configuration& config, T loadFactoryDefault); + +template<> +bool loadFactoryDefault(Configuration& config, int factoryDef) { + config.setInt(factoryDef); + return true; +} + +template<> +bool loadFactoryDefault(Configuration& config, bool factoryDef) { + config.setBool(factoryDef); + return true; +} + +template<> +bool loadFactoryDefault(Configuration& config, const char *factoryDef) { + return config.setString(factoryDef); +} + +void loadPermissions(Configuration& config, bool readonly, bool rebootRequired) { + if (readonly) { + config.setReadOnly(); + } + + if (rebootRequired) { + config.setRebootRequired(); + } +} + +template +std::shared_ptr declareConfiguration(const char *key, T factoryDef, const char *filename, bool readonly, bool rebootRequired, bool accessible) { + + std::shared_ptr res = loadConfiguration(convertType(), key, accessible); + if (!res) { + auto container = declareContainer(filename, accessible); + if (!container) { + return nullptr; + } + + res = container->createConfiguration(convertType(), key); + if (!res) { + return nullptr; + } + + if (!loadFactoryDefault(*res.get(), factoryDef)) { + container->remove(res.get()); + return nullptr; + } + } + + loadPermissions(*res.get(), readonly, rebootRequired); + return res; +} + +template std::shared_ptr declareConfiguration(const char *key, int factoryDef, const char *filename, bool readonly, bool rebootRequired, bool accessible); +template std::shared_ptr declareConfiguration(const char *key, bool factoryDef, const char *filename, bool readonly, bool rebootRequired, bool accessible); +template std::shared_ptr declareConfiguration(const char *key, const char *factoryDef, const char *filename, bool readonly, bool rebootRequired, bool accessible); + +std::function *getConfigurationValidator(const char *key) { + for (auto& v : validators) { + if (!strcmp(v.key, key)) { + return &v.checkValue; + } + } + return nullptr; +} + +void registerConfigurationValidator(const char *key, std::function validator) { + for (auto& v : validators) { + if (!strcmp(v.key, key)) { + v.checkValue = validator; + return; + } + } + validators.push_back(Validator{key, validator}); +} + +Configuration *getConfigurationPublic(const char *key) { + for (auto& container : configurationContainers) { + if (container->isAccessible()) { + if (auto res = container->getConfiguration(key)) { + return res.get(); + } + } + } + + return nullptr; +} + +Vector getConfigurationContainersPublic() { + auto res = makeVector("v16.Configuration.Containers"); + + for (auto& container : configurationContainers) { + if (container->isAccessible()) { + res.push_back(container.get()); + } + } + + return res; +} + +bool configuration_init(std::shared_ptr _filesystem) { + filesystem = _filesystem; + return true; +} + +void configuration_deinit() { + makeVector("v16.Configuration.Containers").swap(configurationContainers); //release allocated memory (see https://cplusplus.com/reference/vector/vector/clear/) + makeVector("v16.Configuration.Validators").swap(validators); + filesystem.reset(); +} + +bool configuration_load(const char *filename) { + bool success = true; + + for (auto& container : configurationContainers) { + if ((!filename || !strcmp(filename, container->getFilename())) && !container->load()) { + success = false; + } + } + + return success; +} + +bool configuration_save() { + bool success = true; + + for (auto& container : configurationContainers) { + if (!container->save()) { + success = false; + } + } + + return success; +} + +bool configuration_clean_unused() { + for (auto& container : configurationContainers) { + container->removeUnused(); + } + return configuration_save(); +} + +bool VALIDATE_UNSIGNED_INT(const char *value) { + for(size_t i = 0; value[i] != '\0'; i++) { + if (value[i] < '0' || value[i] > '9') { + return false; + } + } + return true; +} + +} //end namespace MicroOcpp diff --git a/src/MicroOcpp/Core/Configuration.h b/src/MicroOcpp/Core/Configuration.h new file mode 100644 index 00000000..486b9c69 --- /dev/null +++ b/src/MicroOcpp/Core/Configuration.h @@ -0,0 +1,45 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CONFIGURATION_H +#define MO_CONFIGURATION_H + +#include +#include +#include +#include + +#include + +#define CONFIGURATION_FN (MO_FILENAME_PREFIX "ocpp-config.jsn") +#define CONFIGURATION_VOLATILE "/volatile" +#define MO_KEYVALUE_FN (MO_FILENAME_PREFIX "client-state.jsn") + +namespace MicroOcpp { + +template +std::shared_ptr declareConfiguration(const char *key, T factoryDefault, const char *filename = CONFIGURATION_FN, bool readonly = false, bool rebootRequired = false, bool accessible = true); + +std::function *getConfigurationValidator(const char *key); +void registerConfigurationValidator(const char *key, std::function validator); + +void addConfigurationContainer(std::shared_ptr container); + +Configuration *getConfigurationPublic(const char *key); +Vector getConfigurationContainersPublic(); + +bool configuration_init(std::shared_ptr filesytem); +void configuration_deinit(); + +bool configuration_load(const char *filename = nullptr); + +bool configuration_save(); + +bool configuration_clean_unused(); //remove configs which haven't been accessed + +//default implementation for common validator +bool VALIDATE_UNSIGNED_INT(const char*); + +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Core/ConfigurationContainer.cpp b/src/MicroOcpp/Core/ConfigurationContainer.cpp new file mode 100644 index 00000000..af0cba2f --- /dev/null +++ b/src/MicroOcpp/Core/ConfigurationContainer.cpp @@ -0,0 +1,76 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include + +using namespace MicroOcpp; + +ConfigurationContainer::~ConfigurationContainer() { + +} + +ConfigurationContainerVolatile::ConfigurationContainerVolatile(const char *filename, bool accessible) : + ConfigurationContainer(filename, accessible), MemoryManaged("v16.Configuration.ContainerVoltaile.", filename), configurations(makeVector>(getMemoryTag())) { + +} + +bool ConfigurationContainerVolatile::load() { + return true; +} + +bool ConfigurationContainerVolatile::save() { + return true; +} + +std::shared_ptr ConfigurationContainerVolatile::createConfiguration(TConfig type, const char *key) { + auto res = std::shared_ptr(makeConfiguration(type, key).release(), std::default_delete(), makeAllocator("v16.Configuration.", key)); + if (!res) { + //allocation failure - OOM + MO_DBG_ERR("OOM"); + return nullptr; + } + configurations.push_back(res); + return res; +} + +void ConfigurationContainerVolatile::remove(Configuration *config) { + for (auto entry = configurations.begin(); entry != configurations.end();) { + if (entry->get() == config) { + entry = configurations.erase(entry); + } else { + entry++; + } + } +} + +size_t ConfigurationContainerVolatile::size() { + return configurations.size(); +} + +Configuration *ConfigurationContainerVolatile::getConfiguration(size_t i) { + return configurations[i].get(); +} + +std::shared_ptr ConfigurationContainerVolatile::getConfiguration(const char *key) { + for (auto& entry : configurations) { + if (entry->getKey() && !strcmp(entry->getKey(), key)) { + return entry; + } + } + return nullptr; +} + +void ConfigurationContainerVolatile::add(std::shared_ptr c) { + configurations.push_back(std::move(c)); +} + +namespace MicroOcpp { + +std::unique_ptr makeConfigurationContainerVolatile(const char *filename, bool accessible) { + return std::unique_ptr(new ConfigurationContainerVolatile(filename, accessible)); +} + +} //end namespace MicroOcpp diff --git a/src/MicroOcpp/Core/ConfigurationContainer.h b/src/MicroOcpp/Core/ConfigurationContainer.h new file mode 100644 index 00000000..9a3ff3ae --- /dev/null +++ b/src/MicroOcpp/Core/ConfigurationContainer.h @@ -0,0 +1,65 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CONFIGURATIONCONTAINER_H +#define MO_CONFIGURATIONCONTAINER_H + +#include + +#include +#include + +namespace MicroOcpp { + +class ConfigurationContainer { +private: + const char *filename; + bool accessible; +public: + ConfigurationContainer(const char *filename, bool accessible) : filename(filename), accessible(accessible) { } + + virtual ~ConfigurationContainer(); + + const char *getFilename() {return filename;} + bool isAccessible() {return accessible;} + + virtual bool load() = 0; //called at the end of mocpp_intialize, to load the configurations with the stored value + virtual bool save() = 0; + + virtual std::shared_ptr createConfiguration(TConfig type, const char *key) = 0; + virtual void remove(Configuration *config) = 0; + + virtual size_t size() = 0; + virtual Configuration *getConfiguration(size_t i) = 0; + virtual std::shared_ptr getConfiguration(const char *key) = 0; + + virtual void loadStaticKey(Configuration& config, const char *key) { } //possible optimization: can replace internal key with passed static key + + virtual void removeUnused() { } //remove configs which haven't been accessed (optional and only if known) +}; + +class ConfigurationContainerVolatile : public ConfigurationContainer, public MemoryManaged { +private: + Vector> configurations; +public: + ConfigurationContainerVolatile(const char *filename, bool accessible); + + //ConfigurationContainer definitions + bool load() override; + bool save() override; + std::shared_ptr createConfiguration(TConfig type, const char *key) override; + void remove(Configuration *config) override; + size_t size() override; + Configuration *getConfiguration(size_t i) override; + std::shared_ptr getConfiguration(const char *key) override; + + //add custom Configuration object + void add(std::shared_ptr c); +}; + +std::unique_ptr makeConfigurationContainerVolatile(const char *filename, bool accessible); + +} //end namespace MicroOcpp + +#endif diff --git a/src/MicroOcpp/Core/ConfigurationContainerFlash.cpp b/src/MicroOcpp/Core/ConfigurationContainerFlash.cpp new file mode 100644 index 00000000..b32f1af8 --- /dev/null +++ b/src/MicroOcpp/Core/ConfigurationContainerFlash.cpp @@ -0,0 +1,362 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include +#include +#include +#include + +#define MAX_CONFIGURATIONS 50 + +namespace MicroOcpp { + +class ConfigurationContainerFlash : public ConfigurationContainer, public MemoryManaged { +private: + Vector> configurations; + std::shared_ptr filesystem; + uint16_t revisionSum = 0; + + bool loaded = false; + + Vector keyPool; + + void clearKeyPool(const char *key) { + auto it = keyPool.begin(); + while (it != keyPool.end()) { + if (!strcmp(*it, key)) { + MO_DBG_VERBOSE("clear key %s", key); + MO_FREE(*it); + it = keyPool.erase(it); + } else { + ++it; + } + } + } + + bool configurationsUpdated() { + auto revisionSum_old = revisionSum; + + revisionSum = 0; + for (auto& config : configurations) { + revisionSum += config->getValueRevision(); + } + + return revisionSum != revisionSum_old; + } +public: + ConfigurationContainerFlash(std::shared_ptr filesystem, const char *filename, bool accessible) : + ConfigurationContainer(filename, accessible), MemoryManaged("v16.Configuration.ContainerFlash.", filename), configurations(makeVector>(getMemoryTag())), filesystem(filesystem), keyPool(makeVector(getMemoryTag())) { } + + ~ConfigurationContainerFlash() { + auto it = keyPool.begin(); + while (it != keyPool.end()) { + MO_FREE(*it); + it = keyPool.erase(it); + } + } + + bool load() override { + + if (loaded) { + return true; + } + + if (!filesystem) { + return false; + } + + size_t file_size = 0; + if (filesystem->stat(getFilename(), &file_size) != 0 // file does not exist + || file_size == 0) { // file exists, but empty + MO_DBG_DEBUG("Populate FS: create configuration file"); + return save(); + } + + auto doc = FilesystemUtils::loadJson(filesystem, getFilename(), getMemoryTag()); + if (!doc) { + MO_DBG_ERR("failed to load %s", getFilename()); + return false; + } + + JsonObject root = doc->as(); + + JsonObject configHeader = root["head"]; + + if (strcmp(configHeader["content-type"] | "Invalid", "ocpp_config_file") && + strcmp(configHeader["content-type"] | "Invalid", "ao_configuration_file")) { //backwards-compatibility + MO_DBG_ERR("Unable to initialize: unrecognized configuration file format"); + return false; + } + + if (strcmp(configHeader["version"] | "Invalid", "2.0") && + strcmp(configHeader["version"] | "Invalid", "1.1")) { //backwards-compatibility + MO_DBG_ERR("Unable to initialize: unsupported version"); + return false; + } + + JsonArray configurationsArray = root["configurations"]; + if (configurationsArray.size() > MAX_CONFIGURATIONS) { + MO_DBG_ERR("Unable to initialize: configurations_len is too big (=%zu)", configurationsArray.size()); + return false; + } + + for (JsonObject stored : configurationsArray) { + TConfig type; + if (!deserializeTConfig(stored["type"] | "_Undefined", type)) { + MO_DBG_ERR("corrupt config"); + continue; + } + + const char *key = stored["key"] | ""; + if (!*key) { + MO_DBG_ERR("corrupt config"); + continue; + } + + if (!stored.containsKey("value")) { + MO_DBG_ERR("corrupt config"); + continue; + } + + char *key_pooled = nullptr; + + auto config = getConfiguration(key).get(); + if (config && config->getType() != type) { + MO_DBG_ERR("conflicting type for %s - remove old config", key); + remove(config); + config = nullptr; + } + if (!config) { + #if MO_ENABLE_HEAP_PROFILER + char memoryTag [64]; + snprintf(memoryTag, sizeof(memoryTag), "%s%s", "v16.Configuration.", key); + #else + const char *memoryTag = nullptr; + (void)memoryTag; + #endif + key_pooled = static_cast(MO_MALLOC(memoryTag, strlen(key) + 1)); + if (!key_pooled) { + MO_DBG_ERR("OOM: %s", key); + return false; + } + strcpy(key_pooled, key); + } + + switch (type) { + case TConfig::Int: { + if (!stored["value"].is()) { + MO_DBG_ERR("corrupt config"); + MO_FREE(key_pooled); + continue; + } + int value = stored["value"] | 0; + if (!config) { + //create new config + config = createConfiguration(TConfig::Int, key_pooled).get(); + } + if (config) { + config->setInt(value); + } + break; + } + case TConfig::Bool: { + if (!stored["value"].is()) { + MO_DBG_ERR("corrupt config"); + MO_FREE(key_pooled); + continue; + } + bool value = stored["value"] | false; + if (!config) { + //create new config + config = createConfiguration(TConfig::Bool, key_pooled).get(); + } + if (config) { + config->setBool(value); + } + break; + } + case TConfig::String: { + if (!stored["value"].is()) { + MO_DBG_ERR("corrupt config"); + MO_FREE(key_pooled); + continue; + } + const char *value = stored["value"] | ""; + if (!config) { + //create new config + config = createConfiguration(TConfig::String, key_pooled).get(); + } + if (config) { + config->setString(value); + } + break; + } + } + + if (config) { + //success + + if (key_pooled) { + //allocated key, need to store + keyPool.push_back(std::move(key_pooled)); + } + } else { + MO_DBG_ERR("OOM: %s", key); + MO_FREE(key_pooled); + } + } + + configurationsUpdated(); + + MO_DBG_DEBUG("Initialization finished"); + loaded = true; + return true; + } + + bool save() override { + + if (!filesystem) { + return false; + } + + if (!configurationsUpdated()) { + return true; //nothing to be done + } + + //during mocpp_deinitialize(), key owners are destructed. Don't store if this container is affected + for (auto& config : configurations) { + if (!config->getKey()) { + MO_DBG_DEBUG("don't write back container with destructed key(s)"); + return false; + } + } + + size_t jsonCapacity = 2 * JSON_OBJECT_SIZE(2); //head + configurations + head payload + jsonCapacity += JSON_ARRAY_SIZE(configurations.size()); //configurations array + jsonCapacity += configurations.size() * JSON_OBJECT_SIZE(3); //config entries in array + + if (jsonCapacity > MO_MAX_JSON_CAPACITY) { + MO_DBG_ERR("configs JSON exceeds maximum capacity (%s, %zu entries). Crop configs file (by FCFS)", getFilename(), configurations.size()); + jsonCapacity = MO_MAX_JSON_CAPACITY; + } + + auto doc = initJsonDoc(getMemoryTag(), jsonCapacity); + JsonObject head = doc.createNestedObject("head"); + head["content-type"] = "ocpp_config_file"; + head["version"] = "2.0"; + + JsonArray configurationsArray = doc.createNestedArray("configurations"); + + size_t trackCapacity = 0; + + for (size_t i = 0; i < configurations.size(); i++) { + auto& config = *configurations[i]; + + size_t entryCapacity = JSON_OBJECT_SIZE(3) + (JSON_ARRAY_SIZE(2) - JSON_ARRAY_SIZE(1)); + if (trackCapacity + entryCapacity > MO_MAX_JSON_CAPACITY) { + break; + } + + trackCapacity += entryCapacity; + + auto stored = configurationsArray.createNestedObject(); + + stored["type"] = serializeTConfig(config.getType()); + stored["key"] = config.getKey(); + + switch (config.getType()) { + case TConfig::Int: + stored["value"] = config.getInt(); + break; + case TConfig::Bool: + stored["value"] = config.getBool(); + break; + case TConfig::String: + stored["value"] = config.getString(); + break; + } + } + + bool success = FilesystemUtils::storeJson(filesystem, getFilename(), doc); + + if (success) { + MO_DBG_DEBUG("Saving configurations finished"); + } else { + MO_DBG_ERR("could not save configs file: %s", getFilename()); + } + + return success; + } + + std::shared_ptr createConfiguration(TConfig type, const char *key) override { + auto res = std::shared_ptr(makeConfiguration(type, key).release(), std::default_delete(), makeAllocator("v16.Configuration.", key)); + if (!res) { + //allocation failure - OOM + MO_DBG_ERR("OOM"); + return nullptr; + } + configurations.push_back(res); + return res; + } + + void remove(Configuration *config) override { + const char *key = config->getKey(); + configurations.erase(std::remove_if(configurations.begin(), configurations.end(), + [config] (std::shared_ptr& entry) { + return entry.get() == config; + }), configurations.end()); + if (key) { + clearKeyPool(key); + } + } + + size_t size() override { + return configurations.size(); + } + + Configuration *getConfiguration(size_t i) override { + return configurations[i].get(); + } + + std::shared_ptr getConfiguration(const char *key) override { + for (auto& entry : configurations) { + if (entry->getKey() && !strcmp(entry->getKey(), key)) { + return entry; + } + } + return nullptr; + } + + void loadStaticKey(Configuration& config, const char *key) override { + config.setKey(key); + clearKeyPool(key); + } + + void removeUnused() override { + //if a config's key is still in the keyPool, we know it's unused because it has never been declared in FW (originates from an older FW version) + + auto key = keyPool.begin(); + while (key != keyPool.end()) { + + for (auto config = configurations.begin(); config != configurations.end(); ++config) { + if ((*config)->getKey() == *key) { + MO_DBG_DEBUG("remove unused config %s", (*config)->getKey()); + configurations.erase(config); + break; + } + } + + MO_FREE(*key); + key = keyPool.erase(key); + } + } +}; + +std::unique_ptr makeConfigurationContainerFlash(std::shared_ptr filesystem, const char *filename, bool accessible) { + return std::unique_ptr(new ConfigurationContainerFlash(filesystem, filename, accessible)); +} + +} //end namespace MicroOcpp diff --git a/src/MicroOcpp/Core/ConfigurationContainerFlash.h b/src/MicroOcpp/Core/ConfigurationContainerFlash.h new file mode 100644 index 00000000..950f3cd7 --- /dev/null +++ b/src/MicroOcpp/Core/ConfigurationContainerFlash.h @@ -0,0 +1,17 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CONFIGURATIONCONTAINERFLASH_H +#define MO_CONFIGURATIONCONTAINERFLASH_H + +#include +#include + +namespace MicroOcpp { + +std::unique_ptr makeConfigurationContainerFlash(std::shared_ptr filesystem, const char *filename, bool accessible); + +} //end namespace MicroOcpp + +#endif diff --git a/src/MicroOcpp/Core/ConfigurationKeyValue.cpp b/src/MicroOcpp/Core/ConfigurationKeyValue.cpp new file mode 100644 index 00000000..77e8ad85 --- /dev/null +++ b/src/MicroOcpp/Core/ConfigurationKeyValue.cpp @@ -0,0 +1,298 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include + +#include +#include + +#define KEY_MAXLEN 60 +#define STRING_VAL_MAXLEN 512 + +namespace MicroOcpp { + +template<> TConfig convertType() {return TConfig::Int;} +template<> TConfig convertType() {return TConfig::Bool;} +template<> TConfig convertType() {return TConfig::String;} + +Configuration::~Configuration() { + +} + +void Configuration::setInt(int) { +#if MO_CONFIG_TYPECHECK + MO_DBG_ERR("type err"); +#endif +} + +void Configuration::setBool(bool) { +#if MO_CONFIG_TYPECHECK + MO_DBG_ERR("type err"); +#endif +} + +bool Configuration::setString(const char*) { +#if MO_CONFIG_TYPECHECK + MO_DBG_ERR("type err"); +#endif + return false; +} + +int Configuration::getInt() { +#if MO_CONFIG_TYPECHECK + MO_DBG_ERR("type err"); +#endif + return 0; +} + +bool Configuration::getBool() { +#if MO_CONFIG_TYPECHECK + MO_DBG_ERR("type err"); +#endif + return false; +} + +const char *Configuration::getString() { +#if MO_CONFIG_TYPECHECK + MO_DBG_ERR("type err"); +#endif + return ""; +} + +revision_t Configuration::getValueRevision() { + return value_revision; +} + +void Configuration::setRebootRequired() { + rebootRequired = true; +} + +bool Configuration::isRebootRequired() { + return rebootRequired; +} + +void Configuration::setReadOnly() { + if (mutability == Mutability::ReadWrite) { + mutability = Mutability::ReadOnly; + } else { + mutability = Mutability::None; + } +} + +bool Configuration::isReadOnly() { + return mutability == Mutability::ReadOnly; +} + +bool Configuration::isReadable() { + return mutability == Mutability::ReadWrite || mutability == Mutability::ReadOnly; +} + +void Configuration::setWriteOnly() { + if (mutability == Mutability::ReadWrite) { + mutability = Mutability::WriteOnly; + } else { + mutability = Mutability::None; + } +} + +/* + * Default implementations of the Configuration interface. + * + * How to use custom implementations: for each OCPP config, pass a config instance to the OCPP lib + * before its initialization stage. Then the library won't create new config objects but + */ + +class ConfigInt : public Configuration, public MemoryManaged { +private: + const char *key = nullptr; + int val = 0; +public: + + ~ConfigInt() = default; + + bool setKey(const char *key) override { + this->key = key; + updateMemoryTag("v16.Configuration.", key); + return true; + } + + const char *getKey() override { + return key; + } + + TConfig getType() override { + return TConfig::Int; + } + + void setInt(int val) override { + this->val = val; + value_revision++; + } + + int getInt() override { + return val; + } +}; + +class ConfigBool : public Configuration, public MemoryManaged { +private: + const char *key = nullptr; + bool val = false; +public: + + ~ConfigBool() = default; + + bool setKey(const char *key) override { + this->key = key; + updateMemoryTag("v16.Configuration.", key); + return true; + } + + const char *getKey() override { + return key; + } + + TConfig getType() override { + return TConfig::Bool; + } + + void setBool(bool val) override { + this->val = val; + value_revision++; + } + + bool getBool() override { + return val; + } +}; + +class ConfigString : public Configuration, public MemoryManaged { +private: + const char *key = nullptr; + char *val = nullptr; +public: + ConfigString() = default; + ConfigString(const ConfigString&) = delete; + ConfigString(ConfigString&&) = delete; + ConfigString& operator=(const ConfigString&) = delete; + + ~ConfigString() { + MO_FREE(val); + } + + bool setKey(const char *key) override { + this->key = key; + updateMemoryTag("v16.Configuration.", key); + if (val) { + MO_MEM_SET_TAG(val, getMemoryTag()); + } + return true; + } + + const char *getKey() override { + return key; + } + + TConfig getType() override { + return TConfig::String; + } + + bool setString(const char *src) override { + bool src_empty = !src || !*src; + + if (!val && src_empty) { + return true; + } + + if (this->val && src && !strcmp(this->val, src)) { + return true; + } + + size_t size = 0; + if (!src_empty) { + size = strlen(src) + 1; + } + + if (size > MO_CONFIG_MAX_VALSTRSIZE) { + return false; + } + + value_revision++; + + if (this->val) { + MO_FREE(this->val); + this->val = nullptr; + } + + if (!src_empty) { + this->val = (char*) MO_MALLOC(getMemoryTag(), size); + if (!this->val) { + return false; + } + strcpy(this->val, src); + } + + return true; + } + + const char *getString() override { + if (!val) { + return ""; + } + return val; + } +}; + +std::unique_ptr makeConfiguration(TConfig type, const char *key) { + std::unique_ptr res; + switch (type) { + case TConfig::Int: + res.reset(new ConfigInt()); + break; + case TConfig::Bool: + res.reset(new ConfigBool()); + break; + case TConfig::String: + res.reset(new ConfigString()); + break; + } + if (!res) { + MO_DBG_ERR("OOM"); + return nullptr; + } + res->setKey(key); + return res; +} + +bool deserializeTConfig(const char *serialized, TConfig& out) { + if (!strcmp(serialized, "int")) { + out = TConfig::Int; + return true; + } else if (!strcmp(serialized, "bool")) { + out = TConfig::Bool; + return true; + } else if (!strcmp(serialized, "string")) { + out = TConfig::String; + return true; + } else { + MO_DBG_WARN("config type error"); + return false; + } +} + +const char *serializeTConfig(TConfig type) { + switch (type) { + case TConfig::Int: + return "int"; + case TConfig::Bool: + return "bool"; + case TConfig::String: + return "string"; + } + return "_Undefined"; +} + +} //end namespace MicroOcpp diff --git a/src/MicroOcpp/Core/ConfigurationKeyValue.h b/src/MicroOcpp/Core/ConfigurationKeyValue.h new file mode 100644 index 00000000..3e631c1e --- /dev/null +++ b/src/MicroOcpp/Core/ConfigurationKeyValue.h @@ -0,0 +1,89 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef CONFIGURATIONKEYVALUE_H +#define CONFIGURATIONKEYVALUE_H + +#include +#include + +#define MO_CONFIG_MAX_VALSTRSIZE 128 + +#ifndef MO_CONFIG_EXT_PREFIX +#define MO_CONFIG_EXT_PREFIX "Cst_" +#endif + +#ifndef MO_CONFIG_TYPECHECK +#define MO_CONFIG_TYPECHECK 1 //enable this for debugging +#endif + +namespace MicroOcpp { + +using revision_t = uint16_t; + +enum class TConfig : uint8_t { + Int, + Bool, + String +}; + +template +TConfig convertType(); + +class Configuration { +protected: + revision_t value_revision = 0; //write access counter; used to check if this config has been changed +private: + bool rebootRequired = false; + + enum class Mutability : uint8_t { + ReadWrite, + ReadOnly, + WriteOnly, + None + }; + Mutability mutability = Mutability::ReadWrite; + +public: + virtual ~Configuration(); + + virtual bool setKey(const char *key) = 0; + virtual const char *getKey() = 0; + + virtual void setInt(int); + virtual void setBool(bool); + virtual bool setString(const char*); + + virtual int getInt(); + virtual bool getBool(); + virtual const char *getString(); //always returns c-string (empty if undefined) + + virtual TConfig getType() = 0; + + virtual revision_t getValueRevision(); + + void setRebootRequired(); + bool isRebootRequired(); + + void setReadOnly(); + bool isReadOnly(); + bool isReadable(); + + void setWriteOnly(); +}; + +/* + * Default implementations of the Configuration interface. + * + * How to use custom implementations: for each OCPP config, pass a config instance to the OCPP lib + * before its initialization stage. Then the library won't create new config objects but + */ +std::unique_ptr makeConfiguration(TConfig type, const char *key); + +const char *serializeTConfig(TConfig type); +bool deserializeTConfig(const char *serialized, TConfig& out); + +} //end namespace MicroOcpp + +#endif diff --git a/src/ArduinoOcpp/Core/ConfigurationOptions.h b/src/MicroOcpp/Core/ConfigurationOptions.h similarity index 80% rename from src/ArduinoOcpp/Core/ConfigurationOptions.h rename to src/MicroOcpp/Core/ConfigurationOptions.h index a9e0f2b7..0e9e227f 100644 --- a/src/ArduinoOcpp/Core/ConfigurationOptions.h +++ b/src/MicroOcpp/Core/ConfigurationOptions.h @@ -1,9 +1,9 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef CONFIGURATIONOPTIONS_H -#define CONFIGURATIONOPTIONS_H +#ifndef MO_CONFIGURATIONOPTIONS_H +#define MO_CONFIGURATIONOPTIONS_H #include @@ -11,7 +11,7 @@ extern "C" { #endif -struct AO_FilesystemOpt { +struct OCPP_FilesystemOpt { bool use; bool mount; bool formatFsOnFail; @@ -20,7 +20,7 @@ struct AO_FilesystemOpt { #ifdef __cplusplus } -namespace ArduinoOcpp { +namespace MicroOcpp { class FilesystemOpt{ private: @@ -46,7 +46,7 @@ class FilesystemOpt{ break; } } - FilesystemOpt(struct AO_FilesystemOpt fsopt) { + FilesystemOpt(struct OCPP_FilesystemOpt fsopt) { this->use = fsopt.use; this->mount = fsopt.mount; this->formatFsOnFail = fsopt.formatFsOnFail; @@ -57,7 +57,7 @@ class FilesystemOpt{ bool formatOnFail() {return formatFsOnFail;} }; -} //end namespace ArduinoOcpp +} //end namespace MicroOcpp #endif //__cplusplus 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 new file mode 100644 index 00000000..ae2ee2fb --- /dev/null +++ b/src/MicroOcpp/Core/Connection.cpp @@ -0,0 +1,121 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include + +using namespace MicroOcpp; + +LoopbackConnection::LoopbackConnection() : MemoryManaged("WebSocketLoopback") { } + +void LoopbackConnection::loop() { } + +bool LoopbackConnection::sendTXT(const char *msg, size_t length) { + if (!connected || !online) { + return false; + } + if (receiveTXT) { + lastRecv = mocpp_tick_ms(); + return receiveTXT(msg, length); + } else { + return false; + } +} + +void LoopbackConnection::setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT) { + this->receiveTXT = receiveTXT; +} + +unsigned long LoopbackConnection::getLastRecv() { + return lastRecv; +} + +unsigned long LoopbackConnection::getLastConnected() { + return lastConn; +} + +void LoopbackConnection::setOnline(bool online) { + if (online) { + lastConn = mocpp_tick_ms(); + } + this->online = online; +} + +void LoopbackConnection::setConnected(bool connected) { + if (connected) { + lastConn = mocpp_tick_ms(); + } + this->connected = connected; +} + +#ifndef MO_CUSTOM_WS + +using namespace MicroOcpp::EspWiFi; + +WSClient::WSClient(WebSocketsClient *wsock) : MemoryManaged("WebSocketsClient"), wsock(wsock) { + +} + +void WSClient::loop() { + wsock->loop(); +} + +bool WSClient::sendTXT(const char *msg, size_t length) { + return wsock->sendTXT(msg, length); +} + +void WSClient::setReceiveTXTcallback(ReceiveTXTcallback &callback) { + auto& captureLastRecv = lastRecv; + auto& captureLastConnected = lastConnected; + wsock->onEvent([callback, &captureLastRecv, &captureLastConnected](WStype_t type, uint8_t * payload, size_t length) { + switch (type) { + case WStype_DISCONNECTED: + MO_DBG_INFO("Disconnected"); + break; + case WStype_CONNECTED: + MO_DBG_INFO("Connected (path: %s)", payload); + captureLastRecv = mocpp_tick_ms(); + captureLastConnected = mocpp_tick_ms(); + break; + case WStype_TEXT: + if (callback((const char *) payload, length)) { //forward message to RequestQueue + captureLastRecv = mocpp_tick_ms(); + } else { + MO_DBG_WARN("Processing WebSocket input event failed"); + } + break; + case WStype_BIN: + MO_DBG_WARN("Binary data stream not supported"); + break; + case WStype_PING: + // pong will be send automatically + MO_DBG_TRAFFIC_IN(8, "WS ping"); + captureLastRecv = mocpp_tick_ms(); + break; + case WStype_PONG: + // answer to a ping we send + MO_DBG_TRAFFIC_IN(8, "WS pong"); + captureLastRecv = mocpp_tick_ms(); + break; + case WStype_FRAGMENT_TEXT_START: //fragments are not supported + default: + MO_DBG_WARN("Unsupported WebSocket event type"); + break; + } + }); +} + +unsigned long WSClient::getLastRecv() { + return lastRecv; +} + +unsigned long WSClient::getLastConnected() { + return lastConnected; +} + +bool WSClient::isConnected() { + return wsock->isConnected(); +} + +#endif diff --git a/src/MicroOcpp/Core/Connection.h b/src/MicroOcpp/Core/Connection.h new file mode 100644 index 00000000..2c2f7d5c --- /dev/null +++ b/src/MicroOcpp/Core/Connection.h @@ -0,0 +1,131 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CONNECTION_H +#define MO_CONNECTION_H + +#include +#include + +#include +#include + +//On all platforms other than Arduino, the integrated WS lib (links2004/arduinoWebSockets) cannot be +//used. On Arduino its usage is optional. +#ifndef MO_CUSTOM_WS +#if MO_PLATFORM != MO_PLATFORM_ARDUINO +#define MO_CUSTOM_WS +#endif +#endif //ndef MO_CUSTOM_WS + +namespace MicroOcpp { + +using ReceiveTXTcallback = std::function; + +class Connection { +public: + Connection() = default; + virtual ~Connection() = default; + + /* + * The OCPP library will call this function frequently. If you need to execute regular routines, like + * calling the loop-function of the WebSocket library, implement them here + */ + virtual void loop() = 0; + + /* + * The OCPP library calls this function for sending out OCPP messages to the server + */ + virtual bool sendTXT(const char *msg, size_t length) = 0; + + /* + * The OCPP library calls this function once during initialization. It passes a callback function to + * the socket. The socket should forward any incoming payload from the OCPP server to the receiveTXT callback + */ + virtual void setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT) = 0; + + /* + * Returns the timestamp of the last incoming message. Use mocpp_tick_ms() for creating the correct timestamp + * + * DEPRECATED: this function is superseded by isConnected(). Will be removed in MO v2.0 + */ + virtual unsigned long getLastRecv() {return 0;} + + /* + * Returns the timestamp of the last time a connection got successfully established. Use mocpp_tick_ms() for creating the correct timestamp + */ + virtual unsigned long getLastConnected() = 0; + + /* + * NEW IN v1.1 + * + * Returns true if the connection is open; false if the charger is known to be offline. + * + * This function determines if MO is in "offline mode". In offline mode, MO doesn't wait for Authorize responses + * before performing fully local authorization. If the connection is disrupted but isConnected is still true, then + * MO will first wait for a timeout to expire (20 seconds) before going into offline mode. + * + * Returning true will have no further effects other than using the timeout-then-offline mechanism. If the + * connection status is uncertain, it's best to return true by default. + */ + virtual bool isConnected() {return true;} //MO ignores true. This default implementation keeps backwards-compatibility +}; + +class LoopbackConnection : public Connection, public MemoryManaged { +private: + ReceiveTXTcallback receiveTXT; + + //for simulating connection losses + bool online = true; + bool connected = true; + unsigned long lastRecv = 0; + unsigned long lastConn = 0; +public: + LoopbackConnection(); + + void loop() override; + bool sendTXT(const char *msg, size_t length) override; + void setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT) override; + unsigned long getLastRecv() override; + unsigned long getLastConnected() override; + + void setOnline(bool online); //"online": sent messages are going through + bool isOnline() {return online;} + void setConnected(bool connected); //"connected": connection has been established, but messages may not go through (e.g. weak connection) + bool isConnected() override {return connected;} +}; + +} //end namespace MicroOcpp + +#ifndef MO_CUSTOM_WS + +#include + +namespace MicroOcpp { +namespace EspWiFi { + +class WSClient : public Connection, public MemoryManaged { +private: + WebSocketsClient *wsock; + unsigned long lastRecv = 0, lastConnected = 0; +public: + WSClient(WebSocketsClient *wsock); + + void loop(); + + bool sendTXT(const char *msg, size_t length); + + void setReceiveTXTcallback(ReceiveTXTcallback &receiveTXT); + + unsigned long getLastRecv() override; //get time of last successful receive in millis + + unsigned long getLastConnected() override; //get last connection creation in millis + + bool isConnected() override; +}; + +} //end namespace EspWiFi +} //end namespace MicroOcpp +#endif //ndef MO_CUSTOM_WS +#endif diff --git a/src/MicroOcpp/Core/Context.cpp b/src/MicroOcpp/Core/Context.cpp new file mode 100644 index 00000000..1ab65fcb --- /dev/null +++ b/src/MicroOcpp/Core/Context.cpp @@ -0,0 +1,63 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include + +#include + +using namespace MicroOcpp; + +Context::Context(Connection& connection, std::shared_ptr filesystem, uint16_t bootNr, ProtocolVersion version) + : MemoryManaged("Context"), connection(connection), model{version, bootNr}, reqQueue{connection, operationRegistry} { + +} + +Context::~Context() { + +} + +void Context::loop() { + connection.loop(); + reqQueue.loop(); + model.loop(); +} + +void Context::initiateRequest(std::unique_ptr op) { + if (!op) { + MO_DBG_ERR("invalid arg"); + return; + } + reqQueue.sendRequest(std::move(op)); +} + +Model& Context::getModel() { + return model; +} + +OperationRegistry& Context::getOperationRegistry() { + return operationRegistry; +} + +const ProtocolVersion& Context::getVersion() { + return model.getVersion(); +} + +Connection& Context::getConnection() { + return connection; +} + +RequestQueue& Context::getRequestQueue() { + return reqQueue; +} + +void Context::setFtpClient(std::unique_ptr ftpClient) { + this->ftpClient = std::move(ftpClient); +} + +FtpClient *Context::getFtpClient() { + return ftpClient.get(); +} diff --git a/src/MicroOcpp/Core/Context.h b/src/MicroOcpp/Core/Context.h new file mode 100644 index 00000000..2df691d1 --- /dev/null +++ b/src/MicroOcpp/Core/Context.h @@ -0,0 +1,55 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CONTEXT_H +#define MO_CONTEXT_H + +#include + +#include +#include +#include +#include +#include +#include + +namespace MicroOcpp { + +class Connection; +class FilesystemAdapter; + +class Context : public MemoryManaged { +private: + Connection& connection; + OperationRegistry operationRegistry; + Model model; + RequestQueue reqQueue; + + std::unique_ptr ftpClient; + +public: + Context(Connection& connection, std::shared_ptr filesystem, uint16_t bootNr, ProtocolVersion version); + ~Context(); + + void loop(); + + void initiateRequest(std::unique_ptr op); + + Model& getModel(); + + OperationRegistry& getOperationRegistry(); + + const ProtocolVersion& getVersion(); + + Connection& getConnection(); + + RequestQueue& getRequestQueue(); + + void setFtpClient(std::unique_ptr ftpClient); + FtpClient *getFtpClient(); +}; + +} //end namespace MicroOcpp + +#endif diff --git a/src/MicroOcpp/Core/FilesystemAdapter.cpp b/src/MicroOcpp/Core/FilesystemAdapter.cpp new file mode 100644 index 00000000..b442e583 --- /dev/null +++ b/src/MicroOcpp/Core/FilesystemAdapter.cpp @@ -0,0 +1,800 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include //FilesystemOpt +#include +#include + +#include + +/* + * Platform specific implementations. Currently supported: + * - Arduino LittleFs + * - Arduino SPIFFS + * - ESP-IDF SPIFFS + * - POSIX-like API (tested on Ubuntu 20.04) + * Plus a filesystem index decorator working with any of the above + * + * You can add support for other file systems by passing a custom adapter to mocpp_initialize(...) + */ + +#if MO_ENABLE_FILE_INDEX + +#include + +namespace MicroOcpp { + +class FilesystemAdapterIndex; + +class IndexedFileAdapter : public FileAdapter, public MemoryManaged { +private: + FilesystemAdapterIndex& index; + char fn [MO_MAX_PATH_SIZE]; + std::unique_ptr file; + + size_t written = 0; +public: + IndexedFileAdapter(FilesystemAdapterIndex& index, const char *fn, std::unique_ptr file) + : MemoryManaged("FilesystemIndex"), index(index), file(std::move(file)) { + snprintf(this->fn, sizeof(this->fn), "%s", fn); + } + + ~IndexedFileAdapter(); // destructor updates file index with written size + + size_t read(char *buf, size_t len) override { + return file->read(buf, len); + } + + size_t write(const char *buf, size_t len) override { + auto ret = file->write(buf, len); + written += ret; + return ret; + } + + size_t seek(size_t offset) override { + auto ret = file->seek(offset); + written = ret; + return ret; + } + + int read() override { + return file->read(); + } +}; + +class FilesystemAdapterIndex : public FilesystemAdapter, public MemoryManaged { +private: + std::shared_ptr filesystem; + + struct IndexEntry { + String fname; + size_t size; + + IndexEntry(const char *fname, size_t size) : fname(makeString("FilesystemIndex", fname)), size(size) { } + }; + + Vector index; + + IndexEntry *getEntryByFname(const char *fn) { + auto entry = std::find_if(index.begin(), index.end(), + [fn] (const IndexEntry& el) -> bool { + return el.fname.compare(fn) == 0; + }); + + if (entry != index.end()) { + return &(*entry); + } else { + return nullptr; + } + } + + IndexEntry *getEntryByPath(const char *path) { + if (strlen(path) < sizeof(MO_FILENAME_PREFIX) - 1) { + MO_DBG_ERR("invalid fn"); + return nullptr; + } + + const char *fn = path + sizeof(MO_FILENAME_PREFIX) - 1; + return getEntryByFname(fn); + } + + void (*onDestruct)(void*) = nullptr; +public: + FilesystemAdapterIndex(std::shared_ptr filesystem, void (*onDestruct)(void*) = nullptr) : MemoryManaged("FilesystemIndex"), filesystem(std::move(filesystem)), index(makeVector("FilesystemIndex")), onDestruct(onDestruct) { } + + ~FilesystemAdapterIndex() { + if (onDestruct) { + onDestruct(this); + } + } + + int stat(const char *path, size_t *size) override { + if (auto file = getEntryByPath(path)) { + *size = file->size; + return 0; + } else { + return -1; + } + } + + std::unique_ptr open(const char *path, const char *mode) { + if (!strcmp(mode, "r")) { + return filesystem->open(path, "r"); + } else if (!strcmp(mode, "w")) { + + if (strlen(path) < sizeof(MO_FILENAME_PREFIX) - 1) { + MO_DBG_ERR("invalid fn"); + return nullptr; + } + + const char *fn = path + sizeof(MO_FILENAME_PREFIX) - 1; + + auto file = filesystem->open(path, "w"); + if (!file) { + return nullptr; + } + + IndexEntry *entry = nullptr; + if (!(entry = getEntryByFname(fn))) { + index.emplace_back(fn, 0); + entry = &index.back(); + } + + if (!entry) { + MO_DBG_ERR("internal error"); + return nullptr; + } + + entry->size = 0; //write always empties the file + + return std::unique_ptr(new IndexedFileAdapter(*this, entry->fname.c_str(), std::move(file))); + } else { + MO_DBG_ERR("only support r or w"); + return nullptr; + } + } + + bool remove(const char *path) override { + if (strlen(path) >= sizeof(MO_FILENAME_PREFIX) - 1) { + //valid path + const char *fn = path + sizeof(MO_FILENAME_PREFIX) - 1; + index.erase(std::remove_if(index.begin(), index.end(), + [fn] (const IndexEntry& el) -> bool { + return el.fname.compare(fn) == 0; + }), index.end()); + } + + return filesystem->remove(path); + } + + int ftw_root(std::function fn) { + // allow fn to remove elements + for (size_t it = 0; it < index.size();) { + auto size_before = index.size(); + auto err = fn(index[it].fname.c_str()); + if (err) { + return err; + } + if (index.size() + 1 == size_before) { + // element removed + continue; + } + // normal execution + it++; + } + + return 0; + } + + bool createIndex() { + if (!index.empty()) { + return false; + } + auto ret = filesystem->ftw_root([this] (const char *fn) -> int { + int ret; + char path [MO_MAX_PATH_SIZE]; + + ret = snprintf(path, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "%s", fn); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return 0; //ignore this entry and continue ftw + } + + size_t size; + ret = filesystem->stat(path, &size); + if (ret == 0) { + //add fn and size to index + MO_DBG_DEBUG("add file to index: %s (%zuB)", fn, size); + index.emplace_back(fn, size); + return 0; //successfully added filename to index + } else { + MO_DBG_ERR("unexpected entry: %s", fn); + return 0; //ignore this entry and continue ftw + } + }); + + MO_DBG_DEBUG("create fs index: %s, %zu entries", ret == 0 ? "success" : "failure", index.size()); + + return ret == 0; + } + + void updateFilesize(const char *fn, size_t size) { + if (auto entry = getEntryByFname(fn)) { + entry->size = size; + MO_DBG_DEBUG("update index: %s (%zuB)", entry->fname.c_str(), entry->size); + } + } +}; + +IndexedFileAdapter::~IndexedFileAdapter() { + index.updateFilesize(fn, written); +} + +std::shared_ptr decorateIndex(std::shared_ptr filesystem, void (*onDestruct)(void*) = nullptr) { + + auto fsIndex = std::allocate_shared(makeAllocator("FilesystemIndex"), std::move(filesystem), onDestruct); + if (!fsIndex) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + if (!fsIndex->createIndex()) { + MO_DBG_ERR("createIndex err"); + return nullptr; + } + + return fsIndex; +} + +} // namespace MicroOcpp + +#endif //MO_ENABLE_FILE_INDEX + +#if MO_USE_FILEAPI == ARDUINO_LITTLEFS || MO_USE_FILEAPI == ARDUINO_SPIFFS + +#if MO_USE_FILEAPI == ARDUINO_LITTLEFS +#include +#include +#define USE_FS LittleFS +#elif MO_USE_FILEAPI == ARDUINO_SPIFFS +#include +#define USE_FS SPIFFS +#endif + +namespace MicroOcpp { + +class ArduinoFileAdapter : public FileAdapter, public MemoryManaged { + File file; +public: + ArduinoFileAdapter(File&& file) : MemoryManaged("Filesystem"), file(file) {} + + ~ArduinoFileAdapter() { + if (file) { + file.close(); + } + } + + int read() override { + return file.read(); + }; + size_t read(char *buf, size_t len) override { + return file.readBytes(buf, len); + } + size_t write(const char *buf, size_t len) override { + return file.printf("%.*s", len, buf); + } + size_t seek(size_t offset) override { + return file.seek(offset); + } +}; + +class ArduinoFilesystemAdapter : public FilesystemAdapter, public MemoryManaged { +private: + bool valid = false; + FilesystemOpt config; + + void (* onDestruct)(void*) = nullptr; +public: + ArduinoFilesystemAdapter(FilesystemOpt config, void (*onDestruct)(void*) = nullptr) : MemoryManaged("Filesystem"), config(config), onDestruct(onDestruct) { + valid = true; + + if (config.mustMount()) { +#if MO_USE_FILEAPI == ARDUINO_LITTLEFS + if(!USE_FS.begin(config.formatOnFail())) { + MO_DBG_ERR("Error while mounting LITTLEFS"); + valid = false; + } else { + MO_DBG_DEBUG("LittleFS mount success"); + } +#elif MO_USE_FILEAPI == ARDUINO_SPIFFS + //ESP8266 + SPIFFSConfig cfg; + cfg.setAutoFormat(config.formatOnFail()); + SPIFFS.setConfig(cfg); + + if (!SPIFFS.begin()) { + MO_DBG_ERR("Unable to initialize: unable to mount SPIFFS"); + valid = false; + } +#else +#error +#endif + } //end if mustMount() + + } + + ~ArduinoFilesystemAdapter() { + if (config.mustMount()) { + USE_FS.end(); + } + + if (onDestruct) { + onDestruct(this); + } + } + + operator bool() {return valid;} + + int stat(const char *path, size_t *size) override { +#if MO_USE_FILEAPI == ARDUINO_LITTLEFS + char partition_path [MO_MAX_PATH_SIZE]; + auto ret = snprintf(partition_path, MO_MAX_PATH_SIZE, "/littlefs%s", path); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return -1; + } + struct ::stat st; + auto status = ::stat(partition_path, &st); + if (status == 0) { + *size = st.st_size; + } + return status; +#elif MO_USE_FILEAPI == ARDUINO_SPIFFS + if (!USE_FS.exists(path)) { + return -1; + } + File f = USE_FS.open(path, "r"); + if (!f) { + return -1; + } + + int status = -1; + if (!f.isDirectory()) { + *size = f.size(); + status = 0; + } else { + //fetch more information for directory when MicroOcpp also uses them + //status = 0; + } + + f.close(); + return status; +#else +#error +#endif + } //end stat + + std::unique_ptr open(const char *fn, const char *mode) override { + File file = USE_FS.open(fn, mode); + if (file && !file.isDirectory()) { + MO_DBG_DEBUG("File open successful: %s", fn); + return std::unique_ptr(new ArduinoFileAdapter(std::move(file))); + } else { + return nullptr; + } + } + bool remove(const char *fn) override { + return USE_FS.remove(fn); + }; + int ftw_root(std::function fn) override { +#if MO_USE_FILEAPI == ARDUINO_LITTLEFS + auto dir = USE_FS.open(MO_FILENAME_PREFIX); + if (!dir) { + MO_DBG_ERR("cannot open root directory: " MO_FILENAME_PREFIX); + return -1; + } + + int err = 0; + while (auto entry = dir.openNextFile()) { + + char fname [MO_MAX_PATH_SIZE]; + auto ret = snprintf(fname, MO_MAX_PATH_SIZE, "%s", entry.name()); + entry.close(); + + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return -1; + } + + err = fn(fname); + if (err) { + break; + } + } + return err; +#elif MO_USE_FILEAPI == ARDUINO_SPIFFS + auto dir = USE_FS.openDir(MO_FILENAME_PREFIX); + int err = 0; + while (dir.next()) { + auto fname = dir.fileName(); + if (fname.c_str()) { + err = fn(fname.c_str() + strlen(MO_FILENAME_PREFIX)); + } else { + MO_DBG_ERR("fs error"); + err = -1; + } + if (err) { + break; + } + } + return err; +#else +#error +#endif + } +}; + +std::weak_ptr filesystemCache; + +void resetFilesystemCache(void*) { + filesystemCache.reset(); +} + +std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { + + if (auto cached = filesystemCache.lock()) { + return cached; + } + + if (!config.accessAllowed()) { + MO_DBG_DEBUG("Access to Arduino FS not allowed by config"); + return nullptr; + } + + auto fs_concrete = new ArduinoFilesystemAdapter(config, resetFilesystemCache); + auto fs = std::shared_ptr(fs_concrete, std::default_delete(), makeAllocator("Filesystem")); + +#if MO_ENABLE_FILE_INDEX + fs = decorateIndex(fs, resetFilesystemCache); +#endif // MO_ENABLE_FILE_INDEX + + filesystemCache = fs; + + if (*fs_concrete) { + return fs; + } else { + return nullptr; + } +} + +} //end namespace MicroOcpp + +#elif MO_USE_FILEAPI == ESPIDF_SPIFFS + +#include +#include +#include "esp_spiffs.h" + +#ifndef MO_PARTITION_LABEL +#define MO_PARTITION_LABEL "mo" +#endif + +namespace MicroOcpp { + +class EspIdfFileAdapter : public FileAdapter, public MemoryManaged { + FILE *file {nullptr}; +public: + EspIdfFileAdapter(FILE *file) : MemoryManaged("Filesystem"), file(file) {} + + ~EspIdfFileAdapter() { + fclose(file); + } + + size_t read(char *buf, size_t len) override { + return fread(buf, 1, len, file); + } + + size_t write(const char *buf, size_t len) override { + return fwrite(buf, 1, len, file); + } + + size_t seek(size_t offset) override { + return fseek(file, offset, SEEK_SET); + } + + int read() override { + return fgetc(file); + } +}; + +class EspIdfFilesystemAdapter : public FilesystemAdapter, public MemoryManaged { +public: + FilesystemOpt config; + + void (* onDestruct)(void*) = nullptr; +public: + EspIdfFilesystemAdapter(FilesystemOpt config, void (* onDestruct)(void*) = nullptr) : MemoryManaged("Filesystem"), config(config), onDestruct(onDestruct) { } + + ~EspIdfFilesystemAdapter() { + if (config.mustMount()) { + esp_vfs_spiffs_unregister(MO_PARTITION_LABEL); + MO_DBG_DEBUG("SPIFFS unmounted"); + } + + if (onDestruct) { + onDestruct(this); + } + } + + int stat(const char *path, size_t *size) override { + struct ::stat st; + auto ret = ::stat(path, &st); + if (ret == 0) { + *size = st.st_size; + } + return ret; + } + + std::unique_ptr open(const char *fn, const char *mode) override { + auto file = fopen(fn, mode); + if (file) { + return std::unique_ptr(new EspIdfFileAdapter(std::move(file))); + } else { + MO_DBG_DEBUG("Failed to open file path %s", fn); + return nullptr; + } + } + + bool remove(const char *fn) override { + return unlink(fn) == 0; + } + + int ftw_root(std::function fn) override { + //open MO root directory + char dname [MO_MAX_PATH_SIZE]; + auto dlen = snprintf(dname, MO_MAX_PATH_SIZE, "%s", MO_FILENAME_PREFIX); + if (dlen < 0 || dlen >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", dlen); + return -1; + } + + // trim trailing '/' if not root directory + if (dlen >= 2 && dname[dlen - 1] == '/') { + dname[dlen - 1] = '\0'; + } + + auto dir = opendir(dname); + if (!dir) { + MO_DBG_ERR("cannot open root directory: %s", dname); + return -1; + } + + int err = 0; + while (auto entry = readdir(dir)) { + err = fn(entry->d_name); + if (err) { + break; + } + } + + closedir(dir); + return err; + } +}; + +std::weak_ptr filesystemCache; + +void resetFilesystemCache(void*) { + filesystemCache.reset(); +} + +std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { + + if (auto cached = filesystemCache.lock()) { + return cached; + } + + if (!config.accessAllowed()) { + MO_DBG_DEBUG("Access to ESP-IDF SPIFFS not allowed by config"); + return nullptr; + } + + bool mounted = true; + + if (config.mustMount()) { + mounted = false; + + char fnpref [MO_MAX_PATH_SIZE]; + auto fnpref_len = snprintf(fnpref, MO_MAX_PATH_SIZE, "%s", MO_FILENAME_PREFIX); + if (fnpref_len < 0 || fnpref_len >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("MO_FILENAME_PREFIX error %i", fnpref_len); + return nullptr; + } else if (fnpref_len <= 2) { //shortest possible prefix: "/p/", i.e. length = 3 + MO_DBG_ERR("MO_FILENAME_PREFIX cannot be root on ESP-IDF (working example: \"/mo_store/\")"); + return nullptr; + } + + // trim trailing '/' + if (fnpref[fnpref_len - 1] == '/') { + fnpref[fnpref_len - 1] = '\0'; + } + + esp_vfs_spiffs_conf_t conf = { + .base_path = fnpref, + .partition_label = MO_PARTITION_LABEL, + .max_files = 5, + .format_if_mount_failed = config.formatOnFail() + }; + + esp_err_t ret = esp_vfs_spiffs_register(&conf); + + if (ret == ESP_OK) { + mounted = true; + MO_DBG_DEBUG("SPIFFS mounted"); + } else { + if (ret == ESP_FAIL) { + MO_DBG_ERR("Failed to mount or format filesystem"); + } else if (ret == ESP_ERR_NOT_FOUND) { + MO_DBG_ERR("Failed to find SPIFFS partition"); + } else { + MO_DBG_ERR("Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); + } + } + } + + if (mounted) { + auto fs = std::shared_ptr(new EspIdfFilesystemAdapter(config, resetFilesystemCache), std::default_delete(), makeAllocator("Filesystem")); + +#if MO_ENABLE_FILE_INDEX + fs = decorateIndex(fs, resetFilesystemCache); +#endif // MO_ENABLE_FILE_INDEX + + filesystemCache = fs; + return fs; + } else { + return nullptr; + } +} + +} //end namespace MicroOcpp + +#elif MO_USE_FILEAPI == POSIX_FILEAPI + +#include +#include +#include + +namespace MicroOcpp { + +class PosixFileAdapter : public FileAdapter, public MemoryManaged { + FILE *file {nullptr}; +public: + PosixFileAdapter(FILE *file) : MemoryManaged("Filesystem"), file(file) {} + + ~PosixFileAdapter() { + fclose(file); + } + + size_t read(char *buf, size_t len) override { + return fread(buf, 1, len, file); + } + + size_t write(const char *buf, size_t len) override { + return fwrite(buf, 1, len, file); + } + + size_t seek(size_t offset) override { + return fseek(file, offset, SEEK_SET); + } + + int read() override { + return fgetc(file); + } +}; + +class PosixFilesystemAdapter : public FilesystemAdapter, public MemoryManaged { +public: + FilesystemOpt config; + + void (* onDestruct)(void*) = nullptr; +public: + PosixFilesystemAdapter(FilesystemOpt config, void (* onDestruct)(void*) = nullptr) : MemoryManaged("Filesystem"), config(config), onDestruct(onDestruct) { } + + ~PosixFilesystemAdapter() { + if (onDestruct) { + onDestruct(this); + } + } + + int stat(const char *path, size_t *size) override { + struct ::stat st; + auto ret = ::stat(path, &st); + if (ret == 0) { + *size = st.st_size; + } + return ret; + } + + std::unique_ptr open(const char *fn, const char *mode) override { + auto file = fopen(fn, mode); + if (file) { + return std::unique_ptr(new PosixFileAdapter(std::move(file))); + } else { + MO_DBG_DEBUG("Failed to open file path %s", fn); + return nullptr; + } + } + + bool remove(const char *fn) override { + return ::remove(fn) == 0; + } + + int ftw_root(std::function fn) override { + auto dir = opendir(MO_FILENAME_PREFIX); // use c_str() to convert the path string to a C-style string + if (!dir) { + MO_DBG_ERR("cannot open root directory: " MO_FILENAME_PREFIX); + return -1; + } + + int err = 0; + while (auto entry = readdir(dir)) { + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) { + continue; //files . and .. are specific to desktop systems and rarely appear on microcontroller filesystems. Filter them + } + err = fn(entry->d_name); + if (err) { + break; + } + } + + closedir(dir); + return err; + } +}; + +std::weak_ptr filesystemCache; + +void resetFilesystemCache(void*) { + filesystemCache.reset(); +} + +std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { + + if (auto cached = filesystemCache.lock()) { + return cached; + } + + if (!config.accessAllowed()) { + MO_DBG_DEBUG("Access to FS not allowed by config"); + return nullptr; + } + + if (config.mustMount()) { + MO_DBG_DEBUG("Skip mounting on UNIX host"); + } + + auto fs = std::shared_ptr(new PosixFilesystemAdapter(config, resetFilesystemCache), std::default_delete(), makeAllocator("Filesystem")); + +#if MO_ENABLE_FILE_INDEX + fs = decorateIndex(fs, resetFilesystemCache); +#endif // MO_ENABLE_FILE_INDEX + + filesystemCache = fs; + return fs; +} + +} //end namespace MicroOcpp + +#else //filesystem disabled + +namespace MicroOcpp { + +std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config) { + return nullptr; +} + +} //end namespace MicroOcpp + +#endif //switch-case MO_USE_FILEAPI diff --git a/src/MicroOcpp/Core/FilesystemAdapter.h b/src/MicroOcpp/Core/FilesystemAdapter.h new file mode 100644 index 00000000..7e92e7cf --- /dev/null +++ b/src/MicroOcpp/Core/FilesystemAdapter.h @@ -0,0 +1,94 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_FILESYSTEMADAPTER_H +#define MO_FILESYSTEMADAPTER_H + +#include +#include + +#include +#include + +#define DISABLE_FS 0 +#define ARDUINO_LITTLEFS 1 +#define ARDUINO_SPIFFS 2 +#define ESPIDF_SPIFFS 3 +#define POSIX_FILEAPI 4 + +// choose FileAPI if not given by build flag; assume usage with Arduino if no build flags are present +#ifndef MO_USE_FILEAPI +#if MO_PLATFORM == MO_PLATFORM_ARDUINO +#if defined(ESP32) +#define MO_USE_FILEAPI ARDUINO_LITTLEFS +#else +#define MO_USE_FILEAPI ARDUINO_SPIFFS +#endif +#elif MO_PLATFORM == MO_PLATFORM_ESPIDF +#define MO_USE_FILEAPI ESPIDF_SPIFFS +#elif MO_PLATFORM == MO_PLATFORM_UNIX +#define MO_USE_FILEAPI POSIX_FILEAPI +#else +#define MO_USE_FILEAPI DISABLE_FS +#endif //switch-case MO_PLATFORM +#endif //ndef MO_USE_FILEAPI + +#ifndef MO_FILENAME_PREFIX +#if MO_USE_FILEAPI == ESPIDF_SPIFFS +#define MO_FILENAME_PREFIX "/mo_store/" +#else +#define MO_FILENAME_PREFIX "/" +#endif +#endif + +// set default max path size parameters +#ifndef MO_MAX_PATH_SIZE +#if MO_USE_FILEAPI == POSIX_FILEAPI +#define MO_MAX_PATH_SIZE 128 +#else +#define MO_MAX_PATH_SIZE 30 +#endif +#endif + +#ifndef MO_ENABLE_FILE_INDEX +#define MO_ENABLE_FILE_INDEX 0 +#endif + +namespace MicroOcpp { + +class FileAdapter { +public: + virtual ~FileAdapter() = default; + virtual size_t read(char *buf, size_t len) = 0; + virtual size_t write(const char *buf, size_t len) = 0; + virtual size_t seek(size_t offset) = 0; + + virtual int read() = 0; +}; + +class FilesystemAdapter { +public: + virtual ~FilesystemAdapter() = default; + virtual int stat(const char *path, size_t *size) = 0; + virtual std::unique_ptr open(const char *fn, const char *mode) = 0; + virtual bool remove(const char *fn) = 0; + virtual int ftw_root(std::function fn) = 0; //enumerate the files in the mo_store root folder +}; + +/* + * Platform specific implementation. Currently supported: + * - Arduino LittleFs + * - Arduino SPIFFS + * - ESP-IDF SPIFFS + * - POSIX-like API (tested on Ubuntu 20.04) + * + * You can add support for other file systems by passing a custom adapter to mocpp_initialize(...) + * + * Returns null if platform is not supported or Filesystem is disabled + */ +std::shared_ptr makeDefaultFilesystemAdapter(FilesystemOpt config); + +} //end namespace MicroOcpp + +#endif diff --git a/src/MicroOcpp/Core/FilesystemUtils.cpp b/src/MicroOcpp/Core/FilesystemUtils.cpp new file mode 100644 index 00000000..de6a034d --- /dev/null +++ b/src/MicroOcpp/Core/FilesystemUtils.cpp @@ -0,0 +1,140 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include //FilesystemOpt +#include + +using namespace MicroOcpp; + +std::unique_ptr FilesystemUtils::loadJson(std::shared_ptr filesystem, const char *fn, const char *memoryTag) { + if (!filesystem || !fn || *fn == '\0') { + MO_DBG_ERR("Format error"); + return nullptr; + } + + if (strnlen(fn, MO_MAX_PATH_SIZE) >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("Fn too long: %.*s", MO_MAX_PATH_SIZE, fn); + return nullptr; + } + + size_t fsize = 0; + if (filesystem->stat(fn, &fsize) != 0) { + MO_DBG_DEBUG("File does not exist: %s", fn); + return nullptr; + } + + if (fsize < 2) { + MO_DBG_ERR("File too small for JSON, collect %s", fn); + filesystem->remove(fn); + return nullptr; + } + + auto file = filesystem->open(fn, "r"); + if (!file) { + MO_DBG_ERR("Could not open file %s", fn); + return nullptr; + } + + size_t capacity_init = (3 * fsize) / 2; + + //capacity = ceil capacity_init to the next power of two; should be at least 128 + + size_t capacity = 128; + while (capacity < capacity_init && capacity < MO_MAX_JSON_CAPACITY) { + capacity *= 2; + } + if (capacity > MO_MAX_JSON_CAPACITY) { + capacity = MO_MAX_JSON_CAPACITY; + } + + std::unique_ptr doc; + DeserializationError err = DeserializationError::NoMemory; + ArduinoJsonFileAdapter fileReader {file.get()}; + + while (err == DeserializationError::NoMemory && capacity <= MO_MAX_JSON_CAPACITY) { + + doc = makeJsonDoc(memoryTag, capacity); + err = deserializeJson(*doc, fileReader); + + capacity *= 2; + + file->seek(0); //rewind file to beginning + } + + if (err) { + MO_DBG_ERR("Error deserializing file %s: %s", fn, err.c_str()); + //skip this file + return nullptr; + } + + MO_DBG_DEBUG("Loaded JSON file: %s", fn); + + return doc; +} + +bool FilesystemUtils::storeJson(std::shared_ptr filesystem, const char *fn, const JsonDoc& doc) { + if (!filesystem || !fn || *fn == '\0') { + MO_DBG_ERR("Format error"); + return false; + } + + if (strnlen(fn, MO_MAX_PATH_SIZE) >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("Fn too long: %.*s", MO_MAX_PATH_SIZE, fn); + return false; + } + + if (doc.isNull() || doc.overflowed()) { + MO_DBG_ERR("Invalid JSON %s", fn); + return false; + } + + auto file = filesystem->open(fn, "w"); + if (!file) { + MO_DBG_ERR("Could not open file %s", fn); + return false; + } + + ArduinoJsonFileAdapter fileWriter {file.get()}; + + size_t written = serializeJson(doc, fileWriter); + + if (written < 2) { + MO_DBG_ERR("Error writing file %s", fn); + size_t file_size = 0; + if (filesystem->stat(fn, &file_size) == 0) { + MO_DBG_DEBUG("Collect invalid file %s", fn); + filesystem->remove(fn); + } + return false; + } + + MO_DBG_DEBUG("Wrote JSON file: %s", fn); + return true; +} + +bool FilesystemUtils::remove_if(std::shared_ptr filesystem, std::function pred) { + auto ret = filesystem->ftw_root([filesystem, pred] (const char *fpath) { + if (pred(fpath)) { + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "%s", fpath); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return -1; + } + + filesystem->remove(fn); + //no error handling - just skip failed file + } + return 0; + }); + + if (ret != 0) { + MO_DBG_ERR("ftw_root: %i", ret); + } + + return ret == 0; +} diff --git a/src/ArduinoOcpp/Core/FilesystemUtils.h b/src/MicroOcpp/Core/FilesystemUtils.h similarity index 56% rename from src/ArduinoOcpp/Core/FilesystemUtils.h rename to src/MicroOcpp/Core/FilesystemUtils.h index 13af63cf..31a36a73 100644 --- a/src/ArduinoOcpp/Core/FilesystemUtils.h +++ b/src/MicroOcpp/Core/FilesystemUtils.h @@ -1,15 +1,16 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef AO_FILESYSTEMUTILS_H -#define AO_FILESYSTEMUTILS_H +#ifndef MO_FILESYSTEMUTILS_H +#define MO_FILESYSTEMUTILS_H -#include +#include +#include #include #include -namespace ArduinoOcpp { +namespace MicroOcpp { class ArduinoJsonFileAdapter { private: @@ -36,8 +37,10 @@ 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 new file mode 100644 index 00000000..826c3215 --- /dev/null +++ b/src/MicroOcpp/Core/OcppError.h @@ -0,0 +1,44 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_OCPPERROR_H +#define MO_OCPPERROR_H + +#include +#include + +namespace MicroOcpp { + +class NotImplemented : public Operation, public MemoryManaged { +public: + NotImplemented() : MemoryManaged("v16.CallError.", "NotImplemented") { } + + const char *getErrorCode() override { + return "NotImplemented"; + } +}; + +class MsgBufferExceeded : public Operation, public MemoryManaged { +private: + size_t maxCapacity; + size_t msgLen; +public: + MsgBufferExceeded(size_t maxCapacity, size_t msgLen) : MemoryManaged("v16.CallError.", "GenericError"), maxCapacity(maxCapacity), msgLen(msgLen) { } + const char *getErrorCode() override { + return "GenericError"; + } + const char *getErrorDescription() override { + return "JSON too long or too many fields. Cannot deserialize"; + } + std::unique_ptr getErrorDetails() override { + auto errDoc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2)); + JsonObject err = errDoc->to(); + err["max_capacity"] = maxCapacity; + err["msg_length"] = msgLen; + return errDoc; + } +}; + +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Core/Operation.cpp b/src/MicroOcpp/Core/Operation.cpp new file mode 100644 index 00000000..1289381b --- /dev/null +++ b/src/MicroOcpp/Core/Operation.cpp @@ -0,0 +1,42 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include + +using namespace MicroOcpp; + +Operation::Operation() {} + +Operation::~Operation() {} + +const char* Operation::getOperationType(){ + MO_DBG_ERR("Unsupported operation: getOperationType() is not implemented"); + return "CustomOperation"; +} + +std::unique_ptr Operation::createReq() { + MO_DBG_ERR("Unsupported operation: createReq() is not implemented"); + return createEmptyDocument(); +} + +void Operation::processConf(JsonObject payload) { + MO_DBG_ERR("Unsupported operation: processConf() is not implemented"); +} + +void Operation::processReq(JsonObject payload) { + MO_DBG_ERR("Unsupported operation: processReq() is not implemented"); +} + +std::unique_ptr Operation::createConf() { + MO_DBG_ERR("Unsupported operation: createConf() is not implemented"); + return createEmptyDocument(); +} + +std::unique_ptr MicroOcpp::createEmptyDocument() { + auto emptyDoc = makeJsonDoc("EmptyJsonDoc", 0); + emptyDoc->to(); + return emptyDoc; +} diff --git a/src/ArduinoOcpp/Core/OcppMessage.h b/src/MicroOcpp/Core/Operation.h similarity index 59% rename from src/ArduinoOcpp/Core/OcppMessage.h rename to src/MicroOcpp/Core/Operation.h index 107de536..9c1756a1 100644 --- a/src/ArduinoOcpp/Core/OcppMessage.h +++ b/src/MicroOcpp/Core/Operation.h @@ -1,5 +1,5 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 // MIT License /** @@ -7,46 +7,32 @@ * destination properly (the "Remote procedure call" header, e.g. message Id). Second, transmit the application data * as specified in the OCPP 1.6 document. * - * The remote procedure call (RPC) part is implemented by the class OcppOperation. The application data part is implemented by - * the respective OcppMessage subclasses, e.g. BootNotification, StartTransaction, ect. + * The remote procedure call (RPC) part is implemented by the class Request. The application data part is implemented by + * the respective Operation subclasses, e.g. BootNotification, StartTransaction, ect. * - * The resulting structure is that the RPC header (=instance of OcppOperation) holds a reference to the payload + * The resulting structure is that the RPC header (=instance of Request) holds a reference to the payload * message creator (=instance of BootNotification, StartTransaction, ...). Both objects working together give the complete * OCPP operation. */ - #ifndef OCPPMESSAGE_H - #define OCPPMESSAGE_H + #ifndef MO_OPERATION_H + #define MO_OPERATION_H -#include #include +#include +#include -namespace ArduinoOcpp { - -std::unique_ptr createEmptyDocument(); +namespace MicroOcpp { -class OcppModel; -class TransactionRPC; -class StoredOperationHandler; +std::unique_ptr createEmptyDocument(); -class OcppMessage { -private: - bool ocppModelInitialized = false; -protected: - std::shared_ptr ocppModel; +class Operation { public: - OcppMessage(); + Operation(); - virtual ~OcppMessage(); + virtual ~Operation(); - virtual const char* getOcppOperationType(); - - void setOcppModel(std::shared_ptr ocppModel); - - virtual void initiate(); - virtual bool initiate(StoredOperationHandler *rpcData) {return false;} - - virtual bool restore(StoredOperationHandler *rpcData) {return false;} + virtual const char* getOperationType(); /** * Create the payload for the respective OCPP message @@ -56,7 +42,7 @@ class OcppMessage { * 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); @@ -75,12 +61,12 @@ class OcppMessage { * 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 ArduinoOcpp +} //end namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Core/OperationRegistry.cpp b/src/MicroOcpp/Core/OperationRegistry.cpp new file mode 100644 index 00000000..0bd17d45 --- /dev/null +++ b/src/MicroOcpp/Core/OperationRegistry.cpp @@ -0,0 +1,80 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include + +using namespace MicroOcpp; + +OperationRegistry::OperationRegistry() : registry(makeVector("OperationRegistry")) { + +} + +OperationCreator *OperationRegistry::findCreator(const char *operationType) { + for (auto it = registry.begin(); it != registry.end(); ++it) { + if (!strcmp(it->operationType, operationType)) { + return &*it; + } + } + return nullptr; +} + +void OperationRegistry::registerOperation(const char *operationType, std::function creator) { + registry.erase(std::remove_if(registry.begin(), registry.end(), + [operationType] (const OperationCreator& el) { + return !strcmp(operationType, el.operationType); + }), + registry.end()); + + OperationCreator entry; + entry.operationType = operationType; + entry.creator = creator; + + registry.push_back(entry); + + MO_DBG_DEBUG("registered operation %s", operationType); +} + +void OperationRegistry::setOnRequest(const char *operationType, OnReceiveReqListener onRequest) { + if (auto entry = findCreator(operationType)) { + entry->onRequest = onRequest; + } else { + MO_DBG_ERR("%s not registered", operationType); + } +} + +void OperationRegistry::setOnResponse(const char *operationType, OnSendConfListener onResponse) { + if (auto entry = findCreator(operationType)) { + entry->onResponse = onResponse; + } else { + MO_DBG_ERR("%s not registered", operationType); + } +} + +std::unique_ptr OperationRegistry::deserializeOperation(const char *operationType) { + + if (auto entry = findCreator(operationType)) { + auto payload = entry->creator(); + if (payload) { + auto result = std::unique_ptr(new Request( + std::unique_ptr(payload))); + result->setOnReceiveReqListener(entry->onRequest); + result->setOnSendConfListener(entry->onResponse); + return result; + } + } + + return std::unique_ptr(new Request( + std::unique_ptr(new NotImplemented()))); +} + +void OperationRegistry::debugPrint() { + for (auto& creator : registry) { + MO_CONSOLE_PRINTF("[OCPP] > %s\n", creator.operationType); + } +} diff --git a/src/MicroOcpp/Core/OperationRegistry.h b/src/MicroOcpp/Core/OperationRegistry.h new file mode 100644 index 00000000..0d0d44b5 --- /dev/null +++ b/src/MicroOcpp/Core/OperationRegistry.h @@ -0,0 +1,44 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_OPERATIONREGISTRY_H +#define MO_OPERATIONREGISTRY_H + +#include +#include +#include +#include + +namespace MicroOcpp { + +class Operation; +class Request; + +struct OperationCreator { + const char *operationType {nullptr}; + std::function creator {nullptr}; + OnReceiveReqListener onRequest {nullptr}; + OnSendConfListener onResponse {nullptr}; +}; + +class OperationRegistry { +private: + Vector registry; + OperationCreator *findCreator(const char *operationType); + +public: + OperationRegistry(); + + void registerOperation(const char *operationType, std::function creator); + void setOnRequest(const char *operationType, OnReceiveReqListener onRequest); + void setOnResponse(const char *operationType, OnSendConfListener onResponse); + + std::unique_ptr deserializeOperation(const char *operationType); + + void debugPrint(); +}; + +} + +#endif diff --git a/src/MicroOcpp/Core/Request.cpp b/src/MicroOcpp/Core/Request.cpp new file mode 100644 index 00000000..af6e2885 --- /dev/null +++ b/src/MicroOcpp/Core/Request.cpp @@ -0,0 +1,297 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace MicroOcpp { + unsigned int g_randSeed = 1394827383; + + void writeRandomNonsecure(unsigned char *buf, size_t len) { + g_randSeed += mocpp_tick_ms(); + const unsigned int a = 16807; + const unsigned int m = 2147483647; + for (size_t i = 0; i < len; i++) { + g_randSeed = (a * g_randSeed) % m; + buf[i] = g_randSeed; + } + } +} + +using namespace MicroOcpp; + +Request::Request(std::unique_ptr msg) : MemoryManaged("Request.", msg->getOperationType()), messageID(makeString(getMemoryTag())), operation(std::move(msg)) { + timeout_start = mocpp_tick_ms(); + debugRequest_start = mocpp_tick_ms(); +} + +Request::~Request(){ + +} + +Operation *Request::getOperation(){ + return operation.get(); +} + +void Request::setTimeout(unsigned long timeout) { + this->timeout_period = timeout; +} + +bool Request::isTimeoutExceeded() { + return timed_out || (timeout_period && mocpp_tick_ms() - timeout_start >= timeout_period); +} + +void Request::executeTimeout() { + if (!timed_out) { + onTimeoutListener(); + onAbortListener(); + } + timed_out = true; +} + +void Request::setMessageID(const char *id){ + if (!messageID.empty()){ + MO_DBG_ERR("messageID already defined"); + } + messageID = id; +} + +Request::CreateRequestResult Request::createRequest(JsonDoc& requestJson) { + + if (messageID.empty()) { + char uuid [37] = {'\0'}; + generateUUID(uuid, 37); + messageID = uuid; + } + + /* + * Create the OCPP message + */ + auto requestPayload = operation->createReq(); + if (!requestPayload) { + return CreateRequestResult::Failure; + } + + /* + * Create OCPP-J Remote Procedure Call header + */ + size_t json_buffsize = JSON_ARRAY_SIZE(4) + (messageID.length() + 1) + requestPayload->capacity(); + requestJson = initJsonDoc(getMemoryTag(), json_buffsize); + + requestJson.add(MESSAGE_TYPE_CALL); //MessageType + requestJson.add(messageID); //Unique message ID + requestJson.add(operation->getOperationType()); //Action + requestJson.add(*requestPayload); //Payload + + if (MO_DBG_LEVEL >= MO_DL_DEBUG && mocpp_tick_ms() - debugRequest_start >= 10000) { //print contents on the console + debugRequest_start = mocpp_tick_ms(); + + char *buf = new char[1024]; + size_t len = 0; + if (buf) { + len = serializeJson(requestJson, buf, 1024); + } + + if (!buf || len < 1) { + MO_DBG_DEBUG("Try to send request: %s", operation->getOperationType()); + } else { + MO_DBG_DEBUG("Try to send request: %.*s (...)", 128, buf); + } + + delete[] buf; + } + + return CreateRequestResult::Success; +} + +bool Request::receiveResponse(JsonArray response){ + /* + * check if messageIDs match. If yes, continue with this function. If not, return false for message not consumed + */ + if (messageID.compare(response[1].as())){ + return false; + } + + int messageTypeId = response[0] | -1; + + if (messageTypeId == MESSAGE_TYPE_CALLRESULT) { + + /* + * Hand the payload over to the Operation object + */ + JsonObject payload = response[2]; + operation->processConf(payload); + + /* + * Hand the payload over to the onReceiveConf Callback + */ + onReceiveConfListener(payload); + + /* + * return true as this message has been consumed + */ + return true; + } else if (messageTypeId == MESSAGE_TYPE_CALLERROR) { + + /* + * Hand the error over to the Operation object + */ + const char *errorCode = response[2]; + const char *errorDescription = response[3]; + JsonObject errorDetails = response[4]; + bool abortOperation = operation->processErr(errorCode, errorDescription, errorDetails); + + if (abortOperation) { + onReceiveErrorListener(errorCode, errorDescription, errorDetails); + onAbortListener(); + } + + return abortOperation; + } else { + MO_DBG_WARN("invalid response"); + return false; //don't discard this message but retry sending it + } + +} + +bool Request::receiveRequest(JsonArray request) { + + if (!request[1].is()) { + MO_DBG_ERR("malformatted msgId"); + return false; + } + + setMessageID(request[1].as()); + + /* + * Hand the payload over to the Request object + */ + JsonObject payload = request[3]; + operation->processReq(payload); + + /* + * Hand the payload over to the first Callback. It is a callback that notifies the client that request has been processed in the OCPP-library + */ + onReceiveReqListener(payload); + + return true; //success +} + +Request::CreateResponseResult Request::createResponse(JsonDoc& response) { + + bool operationFailure = operation->getErrorCode() != nullptr; + + if (!operationFailure) { + + std::unique_ptr payload = operation->createConf(); + + if (!payload) { + return CreateResponseResult::Pending; //confirmation message still pending + } + + /* + * Create OCPP-J Remote Procedure Call header + */ + size_t json_buffsize = JSON_ARRAY_SIZE(3) + payload->capacity(); + response = initJsonDoc(getMemoryTag(), json_buffsize); + + response.add(MESSAGE_TYPE_CALLRESULT); //MessageType + response.add(messageID.c_str()); //Unique message ID + response.add(*payload); //Payload + + if (onSendConfListener) { + onSendConfListener(payload->as()); + } + } else { + //operation failure. Send error message instead + + const char *errorCode = operation->getErrorCode(); + const char *errorDescription = operation->getErrorDescription(); + std::unique_ptr errorDetails = operation->getErrorDetails(); + + /* + * Create OCPP-J Remote Procedure Call header + */ + size_t json_buffsize = JSON_ARRAY_SIZE(5) + + errorDetails->capacity(); + response = initJsonDoc(getMemoryTag(), json_buffsize); + + response.add(MESSAGE_TYPE_CALLERROR); //MessageType + response.add(messageID.c_str()); //Unique message ID + response.add(errorCode); + response.add(errorDescription); + response.add(*errorDetails); //Error description + } + + return CreateResponseResult::Success; +} + +void Request::setOnReceiveConfListener(OnReceiveConfListener onReceiveConf){ + if (onReceiveConf) + onReceiveConfListener = onReceiveConf; +} + +/** + * Sets a Listener that is called after this machine processed a request by the communication counterpart + */ +void Request::setOnReceiveReqListener(OnReceiveReqListener onReceiveReq){ + if (onReceiveReq) + onReceiveReqListener = onReceiveReq; +} + +void Request::setOnSendConfListener(OnSendConfListener onSendConf){ + if (onSendConf) + onSendConfListener = onSendConf; +} + +void Request::setOnTimeoutListener(OnTimeoutListener onTimeout) { + if (onTimeout) + onTimeoutListener = onTimeout; +} + +void Request::setOnReceiveErrorListener(OnReceiveErrorListener onReceiveError) { + if (onReceiveError) + onReceiveErrorListener = onReceiveError; +} + +void Request::setOnAbortListener(OnAbortListener onAbort) { + if (onAbort) + onAbortListener = onAbort; +} + +const char *Request::getOperationType() { + return operation ? operation->getOperationType() : "UNDEFINED"; +} + +void Request::setRequestSent() { + requestSent = true; +} + +bool Request::isRequestSent() { + return requestSent; +} + +namespace MicroOcpp { + +std::unique_ptr makeRequest(std::unique_ptr operation){ + if (operation == nullptr) { + return nullptr; + } + return std::unique_ptr(new Request(std::move(operation))); +} + +std::unique_ptr makeRequest(Operation *operation) { + return makeRequest(std::unique_ptr(operation)); +} + +} //end namespace MicroOcpp diff --git a/src/MicroOcpp/Core/Request.h b/src/MicroOcpp/Core/Request.h new file mode 100644 index 00000000..62e42967 --- /dev/null +++ b/src/MicroOcpp/Core/Request.h @@ -0,0 +1,129 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_REQUEST_H +#define MO_REQUEST_H + +#define MESSAGE_TYPE_CALL 2 +#define MESSAGE_TYPE_CALLRESULT 3 +#define MESSAGE_TYPE_CALLERROR 4 + +#include + +#include + +#include + +namespace MicroOcpp { + +class Operation; +class Model; + +class Request : public MemoryManaged { +private: + String messageID; + std::unique_ptr operation; + void setMessageID(const char *id); + OnReceiveConfListener onReceiveConfListener = [] (JsonObject payload) {}; + OnReceiveReqListener onReceiveReqListener = [] (JsonObject payload) {}; + OnSendConfListener onSendConfListener = [] (JsonObject payload) {}; + OnTimeoutListener onTimeoutListener = [] () {}; + OnReceiveErrorListener onReceiveErrorListener = [] (const char *code, const char *description, JsonObject details) {}; + OnAbortListener onAbortListener = [] () {}; + + unsigned long timeout_start = 0; + unsigned long timeout_period = 40000; + bool timed_out = false; + + unsigned long debugRequest_start = 0; + + bool requestSent = false; +public: + + Request(std::unique_ptr msg); + + ~Request(); + + Operation *getOperation(); + + void setTimeout(unsigned long timeout); //0 = disable timeout + bool isTimeoutExceeded(); + void executeTimeout(); //call Timeout Listener + void setOnTimeoutListener(OnTimeoutListener onTimeout); + + /** + * Sends the message(s) that belong to the OCPP Operation. This function puts a JSON message on the lower protocol layer. + * + * For instance operation Authorize: sends Authorize.req(idTag) + * + * This function is usually called multiple times by the Arduino loop(). On first call, the request is initially sent. In the + * succeeding calls, the implementers decide to either resend the request, or do nothing as the operation is still pending. + */ + enum class CreateRequestResult { + Success, + Failure + }; + CreateRequestResult createRequest(JsonDoc& out); + + /** + * Decides if message belongs to this operation instance and if yes, proccesses it. Receives both Confirmations and Errors + * + * Returns true if JSON object has been consumed, false otherwise. + */ + bool receiveResponse(JsonArray json); + + /** + * Processes the request in the JSON document. Returns true on success, false on error. + * + * Returns false if the request doesn't belong to the corresponding operation instance + */ + bool receiveRequest(JsonArray json); + + /** + * After processing a request sent by the communication counterpart, this function sends a confirmation + * message. Returns true on success, false otherwise. Returns also true if a CallError has successfully + * been sent + */ + enum class CreateResponseResult { + Success, + Pending, + Failure + }; + + CreateResponseResult createResponse(JsonDoc& out); + + void setOnReceiveConfListener(OnReceiveConfListener onReceiveConf); //listener executed when we received the .conf() to a .req() we sent + void setOnReceiveReqListener(OnReceiveReqListener onReceiveReq); //listener executed when we receive a .req() + void setOnSendConfListener(OnSendConfListener onSendConf); //listener executed when we send a .conf() to a .req() we received + + void setOnReceiveErrorListener(OnReceiveErrorListener onReceiveError); + + /** + * The listener onAbort will be called whenever the engine stops trying to execute an operation normally which were initiated + * on this device. This includes timeouts or if the ocpp counterpart sends an error (then it will be called in addition to + * onTimeout or onReceiveError, respectively). Causes for onAbort: + * + * - Cannot create OCPP payload + * - Timeout + * - Receives error msg instead of confirmation msg + * + * The engine uses this listener in both modes: EVSE mode and Central system mode + */ + void setOnAbortListener(OnAbortListener onAbort); + + const char *getOperationType(); + + void setRequestSent(); + bool isRequestSent(); +}; + +/* + * Simple factory functions + */ +std::unique_ptr makeRequest(std::unique_ptr op); +std::unique_ptr makeRequest(Operation *op); //takes ownership of op + +} //end namespace MicroOcpp + + #endif diff --git a/src/MicroOcpp/Core/RequestCallbacks.h b/src/MicroOcpp/Core/RequestCallbacks.h new file mode 100644 index 00000000..4959a3bc --- /dev/null +++ b/src/MicroOcpp/Core/RequestCallbacks.h @@ -0,0 +1,21 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_REQUESTCALLBACKS_H +#define MO_REQUESTCALLBACKS_H + +#include +#include + +namespace MicroOcpp { + +using OnReceiveConfListener = std::function; +using OnReceiveReqListener = std::function; +using OnSendConfListener = std::function; +using OnTimeoutListener = std::function; +using OnReceiveErrorListener = std::function; //will be called if OCPP communication partner returns error code +using OnAbortListener = std::function; //will be called whenever the engine will stop trying to execute the operation normallythere is a timeout or error (onAbort = onTimeout || onReceiveError) + +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Core/RequestQueue.cpp b/src/MicroOcpp/Core/RequestQueue.cpp new file mode 100644 index 00000000..104a2e97 --- /dev/null +++ b/src/MicroOcpp/Core/RequestQueue.cpp @@ -0,0 +1,408 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include +#include +#include +#include +#include +#include + +#include + +size_t removePayload(const char *src, size_t src_size, char *dst, size_t dst_size); + +using namespace MicroOcpp; + +VolatileRequestQueue::VolatileRequestQueue() : MemoryManaged("VolatileRequestQueue") { + +} + +VolatileRequestQueue::~VolatileRequestQueue() = default; + +void VolatileRequestQueue::loop() { + + /* + * Drop timed out operations + */ + size_t i = 0; + while (i < len) { + size_t index = (front + i) % MO_REQUEST_CACHE_MAXSIZE; + auto& request = requests[index]; + + if (request->isTimeoutExceeded()) { + MO_DBG_INFO("operation timeout: %s", request->getOperationType()); + request->executeTimeout(); + + if (index == front) { + requests[front].reset(); + front = (front + 1) % MO_REQUEST_CACHE_MAXSIZE; + len--; + } else { + requests[index].reset(); + for (size_t i = (index + MO_REQUEST_CACHE_MAXSIZE - front) % MO_REQUEST_CACHE_MAXSIZE; i < len - 1; i++) { + requests[(front + i) % MO_REQUEST_CACHE_MAXSIZE] = std::move(requests[(front + i + 1) % MO_REQUEST_CACHE_MAXSIZE]); + } + len--; + } + } else { + i++; + } + } +} + +unsigned int VolatileRequestQueue::getFrontRequestOpNr() { + if (len == 0) { + return NoOperation; + } + + return 1; //return OpNr 1 to grant PreBoot queue higher priority (=0), but send messages before tx-msg queue (starting with 10) +} + +std::unique_ptr VolatileRequestQueue::fetchFrontRequest() { + if (len == 0) { + return nullptr; + } + + std::unique_ptr result = std::move(requests[front]); + front = (front + 1) % MO_REQUEST_CACHE_MAXSIZE; + len--; + + MO_DBG_VERBOSE("front %zu len %zu", front, len); + + return result; +} + +bool VolatileRequestQueue::pushRequestBack(std::unique_ptr request) { + + // Don't queue up multiple StatusNotification messages for the same connectorId + #if 0 // Leads to ASAN failure when executed by Unit test suite (CustomOperation is casted to StatusNotification) + if (strcmp(request->getOperationType(), "StatusNotification") == 0) + { + size_t i = 0; + while (i < len) { + size_t index = (front + i) % MO_REQUEST_CACHE_MAXSIZE; + + if (strcmp(requests[index]->getOperationType(), "StatusNotification")!= 0) + { + i++; + continue; + } + auto new_status_notification = static_cast(request->getOperation()); + auto old_status_notification = static_cast(requests[index]->getOperation()); + if (old_status_notification->getConnectorId() == new_status_notification->getConnectorId()) { + requests[index].reset(); + for (size_t i = (index + MO_REQUEST_CACHE_MAXSIZE - front) % MO_REQUEST_CACHE_MAXSIZE; i < len - 1; i++) { + requests[(front + i) % MO_REQUEST_CACHE_MAXSIZE] = std::move(requests[(front + i + 1) % MO_REQUEST_CACHE_MAXSIZE]); + } + len--; + } else { + i++; + } + } + } + #endif + + if (len >= MO_REQUEST_CACHE_MAXSIZE) { + MO_DBG_INFO("Drop cached operation (cache full): %s", requests[front]->getOperationType()); + requests[front]->executeTimeout(); + requests[front].reset(); + front = (front + 1) % MO_REQUEST_CACHE_MAXSIZE; + len--; + } + + requests[(front + len) % MO_REQUEST_CACHE_MAXSIZE] = std::move(request); + len++; + return true; +} + +RequestQueue::RequestQueue(Connection& connection, OperationRegistry& operationRegistry) + : MemoryManaged("RequestQueue"), connection(connection), operationRegistry(operationRegistry) { + + ReceiveTXTcallback callback = [this] (const char *payload, size_t length) { + return this->receiveMessage(payload, length); + }; + + connection.setReceiveTXTcallback(callback); + + memset(sendQueues, 0, sizeof(sendQueues)); + addSendQueue(&defaultSendQueue); +} + +void RequestQueue::loop() { + + /* + * Check if front request timed out + */ + if (sendReqFront && sendReqFront->isTimeoutExceeded()) { + MO_DBG_INFO("operation timeout: %s", sendReqFront->getOperationType()); + sendReqFront->executeTimeout(); + sendReqFront.reset(); + } + + if (recvReqFront && recvReqFront->isTimeoutExceeded()) { + MO_DBG_INFO("operation timeout: %s", recvReqFront->getOperationType()); + recvReqFront->executeTimeout(); + recvReqFront.reset(); + } + + defaultSendQueue.loop(); + + if (!connection.isConnected()) { + return; + } + + /** + * Send and dequeue a pending confirmation message, if existing + * + * If a message has been sent, terminate this loop() function. + */ + + if (!recvReqFront) { + recvReqFront = recvQueue.fetchFrontRequest(); + } + + if (recvReqFront) { + + auto response = initJsonDoc(getMemoryTag()); + auto ret = recvReqFront->createResponse(response); + + if (ret == Request::CreateResponseResult::Success) { + auto out = makeString(getMemoryTag()); + serializeJson(response, out); + + bool success = connection.sendTXT(out.c_str(), out.length()); + + if (success) { + MO_DBG_TRAFFIC_OUT(out.c_str()); + recvReqFront.reset(); + } + + return; + } //else: There will be another attempt to send this conf message in a future loop call + } + + /** + * Send pending req message + */ + + if (!sendReqFront) { + + unsigned int minOpNr = RequestEmitter::NoOperation; + size_t index = MO_NUM_REQUEST_QUEUES; + for (size_t i = 0; i < MO_NUM_REQUEST_QUEUES && sendQueues[i]; i++) { + auto opNr = sendQueues[i]->getFrontRequestOpNr(); + if (opNr < minOpNr) { + minOpNr = opNr; + index = i; + } + } + + if (index < MO_NUM_REQUEST_QUEUES) { + sendReqFront = sendQueues[index]->fetchFrontRequest(); + } + } + + if (sendReqFront && !sendReqFront->isRequestSent()) { + + auto request = initJsonDoc(getMemoryTag()); + auto ret = sendReqFront->createRequest(request); + + if (ret == Request::CreateRequestResult::Success) { + + //send request + auto out = makeString(getMemoryTag()); + serializeJson(request, out); + + bool success = connection.sendTXT(out.c_str(), out.length()); + + if (success) { + MO_DBG_TRAFFIC_OUT(out.c_str()); + sendReqFront->setRequestSent(); //mask as sent and wait for response / timeout + } + + return; + } + } +} + +void RequestQueue::sendRequest(std::unique_ptr op){ + defaultSendQueue.pushRequestBack(std::move(op)); +} + +void RequestQueue::sendRequestPreBoot(std::unique_ptr op){ + if (!preBootSendQueue) { + MO_DBG_ERR("did not set PreBoot queue"); + return; + } + preBootSendQueue->pushRequestBack(std::move(op)); +} + +void RequestQueue::addSendQueue(RequestEmitter* sendQueue) { + for (size_t i = 0; i < MO_NUM_REQUEST_QUEUES; i++) { + if (!sendQueues[i]) { + sendQueues[i] = sendQueue; + return; + } + } + MO_DBG_ERR("exceeded sendQueue capacity"); +} + +void RequestQueue::setPreBootSendQueue(VolatileRequestQueue *preBootQueue) { + this->preBootSendQueue = preBootQueue; + addSendQueue(preBootQueue); +} + +unsigned int RequestQueue::getNextOpNr() { + return nextOpNr++; +} + +bool RequestQueue::receiveMessage(const char* payload, size_t length) { + + MO_DBG_TRAFFIC_IN((int) length, payload); + + size_t capacity_init = (3 * length) / 2; + + //capacity = ceil capacity_init to the next power of two; should be at least 128 + + size_t capacity = 128; + while (capacity < capacity_init && capacity < MO_MAX_JSON_CAPACITY) { + capacity *= 2; + } + if (capacity > MO_MAX_JSON_CAPACITY) { + capacity = MO_MAX_JSON_CAPACITY; + } + + auto doc = initJsonDoc(getMemoryTag()); + DeserializationError err = DeserializationError::NoMemory; + + while (err == DeserializationError::NoMemory && capacity <= MO_MAX_JSON_CAPACITY) { + + doc = initJsonDoc(getMemoryTag(), capacity); + err = deserializeJson(doc, payload, length); + + capacity *= 2; + } + + bool success = false; + + switch (err.code()) { + case DeserializationError::Ok: { + int messageTypeId = doc[0] | -1; + + if (messageTypeId == MESSAGE_TYPE_CALL) { + receiveRequest(doc.as()); + success = true; + } else if (messageTypeId == MESSAGE_TYPE_CALLRESULT || + messageTypeId == MESSAGE_TYPE_CALLERROR) { + receiveResponse(doc.as()); + success = true; + } else { + MO_DBG_WARN("Invalid OCPP message! (though JSON has successfully been deserialized)"); + } + break; + } + case DeserializationError::InvalidInput: + MO_DBG_WARN("Invalid input! Not a JSON"); + break; + case DeserializationError::NoMemory: { + MO_DBG_WARN("incoming operation exceeds buffer capacity. Input length = %zu, max capacity = %d", length, MO_MAX_JSON_CAPACITY); + + /* + * If websocket input is of message type MESSAGE_TYPE_CALL, send back a message of type MESSAGE_TYPE_CALLERROR. + * Then the communication counterpart knows that this operation failed. + * If the input type is MESSAGE_TYPE_CALLRESULT, then abort the operation to avoid getting stalled. + */ + + doc = initJsonDoc(getMemoryTag(), 200); + char onlyRpcHeader[200]; + size_t onlyRpcHeader_len = removePayload(payload, length, onlyRpcHeader, sizeof(onlyRpcHeader)); + DeserializationError err2 = deserializeJson(doc, onlyRpcHeader, onlyRpcHeader_len); + if (err2.code() == DeserializationError::Ok) { + int messageTypeId = doc[0] | -1; + if (messageTypeId == MESSAGE_TYPE_CALL) { + success = true; + auto op = makeRequest(new MsgBufferExceeded(MO_MAX_JSON_CAPACITY, length)); + receiveRequest(doc.as(), std::move(op)); + } else if (messageTypeId == MESSAGE_TYPE_CALLRESULT || + messageTypeId == MESSAGE_TYPE_CALLERROR) { + success = true; + MO_DBG_WARN("crop incoming response"); + receiveResponse(doc.as()); + } + } + break; + } + default: + MO_DBG_WARN("Deserialization failed: %s", err.c_str()); + break; + } + + return success; +} + +/** + * call conf() on each element of the queue. Start with first element. On successful message delivery, + * delete the element from the list. Try all the pending OCPP Operations until the right one is found. + * + * This function could result in improper behavior in Charging Stations, because messages are not + * guaranteed to be received and therefore processed in the right order. + */ +void RequestQueue::receiveResponse(JsonArray json) { + + if (!sendReqFront || !sendReqFront->receiveResponse(json)) { + MO_DBG_WARN("Received response doesn't match pending operation"); + } + + sendReqFront.reset(); +} + +void RequestQueue::receiveRequest(JsonArray json) { + auto op = operationRegistry.deserializeOperation(json[2] | "UNDEFINED"); + if (op == nullptr) { + MO_DBG_WARN("OOM"); + return; + } + receiveRequest(json, std::move(op)); +} + +void RequestQueue::receiveRequest(JsonArray json, std::unique_ptr op) { + op->receiveRequest(json); //execute the operation + recvQueue.pushRequestBack(std::move(op)); //enqueue so loop() plans conf sending +} + +/* + * Tries to recover the Ocpp-Operation header from a broken message. + * + * Example input: + * [2, "75705e50-682d-404e-b400-1bca33d41e19", "ChangeConfiguration", {"key":"now the message breaks... + * + * The Json library returns an error code when trying to deserialize that broken message. This + * function searches for the first occurence of the character '{' and writes "}]" after it. + * + * Example output: + * [2, "75705e50-682d-404e-b400-1bca33d41e19", "ChangeConfiguration", {}] + * + */ +size_t removePayload(const char *src, size_t src_size, char *dst, size_t dst_size) { + size_t res_len = 0; + for (size_t i = 0; i < src_size && i < dst_size-3; i++) { + if (src[i] == '\0'){ + //no payload found within specified range. Cancel execution + break; + } + dst[i] = src[i]; + if (src[i] == '{') { + dst[i+1] = '}'; + dst[i+2] = ']'; + res_len = i+3; + break; + } + } + dst[res_len] = '\0'; + res_len++; + return res_len; +} diff --git a/src/MicroOcpp/Core/RequestQueue.h b/src/MicroOcpp/Core/RequestQueue.h new file mode 100644 index 00000000..0cdfe7bc --- /dev/null +++ b/src/MicroOcpp/Core/RequestQueue.h @@ -0,0 +1,93 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_REQUESTQUEUE_H +#define MO_REQUESTQUEUE_H + +#include + +#include +#include + +#include +#include + +#ifndef MO_REQUEST_CACHE_MAXSIZE +#define MO_REQUEST_CACHE_MAXSIZE 10 +#endif + +#ifndef MO_NUM_REQUEST_QUEUES +#define MO_NUM_REQUEST_QUEUES 10 +#endif + +namespace MicroOcpp { + +class Connection; +class OperationRegistry; +class Request; + +class RequestEmitter { +public: + static const unsigned int NoOperation = std::numeric_limits::max(); + + virtual unsigned int getFrontRequestOpNr() = 0; //return OpNr of front request or NoOperation if queue is empty + virtual std::unique_ptr fetchFrontRequest() = 0; +}; + +class VolatileRequestQueue : public RequestEmitter, public MemoryManaged { +private: + std::unique_ptr requests [MO_REQUEST_CACHE_MAXSIZE]; + size_t front = 0, len = 0; +public: + VolatileRequestQueue(); + ~VolatileRequestQueue(); + void loop(); + + unsigned int getFrontRequestOpNr() override; + std::unique_ptr fetchFrontRequest() override; + + bool pushRequestBack(std::unique_ptr request); +}; + +class RequestQueue : public MemoryManaged { +private: + Connection& connection; + OperationRegistry& operationRegistry; + + RequestEmitter* sendQueues [MO_NUM_REQUEST_QUEUES]; + VolatileRequestQueue defaultSendQueue; + VolatileRequestQueue *preBootSendQueue = nullptr; + std::unique_ptr sendReqFront; + + VolatileRequestQueue recvQueue; + std::unique_ptr recvReqFront; + + bool receiveMessage(const char* payload, size_t length); //receive from server: either a request or response + void receiveRequest(JsonArray json); + void receiveRequest(JsonArray json, std::unique_ptr op); + void receiveResponse(JsonArray json); + + unsigned long sockTrackLastConnected = 0; + + unsigned int nextOpNr = 10; //Nr 0 - 9 reservered for internal purposes +public: + RequestQueue() = delete; + RequestQueue(const RequestQueue&) = delete; + RequestQueue(const RequestQueue&&) = delete; + + RequestQueue(Connection& connection, OperationRegistry& operationRegistry); + + void loop(); //polls all reqQueues and decides which request to send (if any) + + void sendRequest(std::unique_ptr request); //send an OCPP operation request to the server; adds request to default queue + void sendRequestPreBoot(std::unique_ptr request); //send an OCPP operation request to the server; adds request to preBootQueue + + void addSendQueue(RequestEmitter* sendQueue); + void setPreBootSendQueue(VolatileRequestQueue *preBootQueue); + + unsigned int getNextOpNr(); +}; + +} //end namespace MicroOcpp +#endif diff --git a/src/ArduinoOcpp/Core/OcppTime.cpp b/src/MicroOcpp/Core/Time.cpp similarity index 56% rename from src/ArduinoOcpp/Core/OcppTime.cpp rename to src/MicroOcpp/Core/Time.cpp index 9a805e1a..1bcd0caa 100644 --- a/src/ArduinoOcpp/Core/OcppTime.cpp +++ b/src/MicroOcpp/Core/Time.cpp @@ -1,46 +1,39 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include -#include +#include #include #include -namespace ArduinoOcpp { +namespace MicroOcpp { -const OcppTimestamp MIN_TIME = OcppTimestamp(2010, 0, 0, 0, 0, 0); -const OcppTimestamp MAX_TIME = OcppTimestamp(2037, 0, 0, 0, 0, 0); +const Timestamp MIN_TIME = Timestamp(2010, 0, 0, 0, 0, 0); +const Timestamp MAX_TIME = Timestamp(2037, 0, 0, 0, 0, 0); -namespace Clocks { - -unsigned long lastClockReading = 0; -otime_t lastClockValue = 0; - -/* - * Basic clock implementation. Works if ao_tick_ms() is exact enough for you and if device doesn't go in sleep mode. - */ -OcppClock DEFAULT_CLOCK = [] () { - unsigned long tReading = (ao_tick_ms() - lastClockReading) / 1000UL; - if (tReading > 0) { - lastClockValue += tReading; - lastClockReading += tReading * 1000UL; - } - return lastClockValue;}; -} //end namespace Clocks - - -OcppTimestamp::OcppTimestamp() { +Timestamp::Timestamp() : MemoryManaged("Timestamp") { } +Timestamp::Timestamp(const Timestamp& other) : MemoryManaged("Timestamp") { + *this = other; +} + +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + Timestamp::Timestamp(int16_t year, int16_t month, int16_t day, int32_t hour, int32_t minute, int32_t second, int32_t ms) : + MemoryManaged("Timestamp"), year(year), month(month), day(day), hour(hour), minute(minute), second(second), ms(ms) { } +#else + Timestamp::Timestamp(int16_t year, int16_t month, int16_t day, int32_t hour, int32_t minute, int32_t second) : + MemoryManaged("Timestamp"), year(year), month(month), day(day), hour(hour), minute(minute), second(second) { } +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + int noDays(int month, int year) { return (month == 0 || month == 2 || month == 4 || month == 6 || month == 7 || month == 9 || month == 11) ? 31 : ((month == 3 || month == 5 || month == 8 || month == 10) ? 30 : ((year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) ? 29 : 28)); } -bool OcppTimestamp::setTime(const char *jsonDateString) { +bool Timestamp::setTime(const char *jsonDateString) { const int JSONDATE_MINLENGTH = 19; @@ -85,14 +78,29 @@ bool OcppTimestamp::setTime(const char *jsonDateString) { (jsonDateString[15] - '0'); int second = (jsonDateString[17] - '0') * 10 + (jsonDateString[18] - '0'); - //ignore fractals + + //optional fractals + int ms = 0; + if (jsonDateString[19] == '.') { + if (isdigit(jsonDateString[20]) || //1 + isdigit(jsonDateString[21]) || //2 + isdigit(jsonDateString[22])) { + + ms = (jsonDateString[20] - '0') * 100 + + (jsonDateString[21] - '0') * 10 + + (jsonDateString[22] - '0'); + } else { + return false; + } + } if (year < 1970 || year >= 2038 || month < 0 || month >= 12 || day < 0 || day >= noDays(month, year) || hour < 0 || hour >= 24 || minute < 0 || minute >= 60 || - second < 0 || second > 60) { //tolerate leap seconds -- (23:59:60) can be a valid time + second < 0 || second > 60 || //tolerate leap seconds -- (23:59:60) can be a valid time + ms < 0 || ms >= 1000) { return false; } @@ -102,11 +110,14 @@ bool OcppTimestamp::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; } -bool OcppTimestamp::toJsonString(char *jsonDateString, size_t buffsize) const { +bool Timestamp::toJsonString(char *jsonDateString, size_t buffsize) const { if (buffsize < JSONDATE_LENGTH + 1) return false; jsonDateString[0] = ((char) ((year / 1000) % 10)) + '0'; @@ -128,16 +139,22 @@ bool OcppTimestamp::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] = '0'; //ignore fractals - jsonDateString[21] = '0'; - jsonDateString[22] = '0'; + 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; } -OcppTimestamp &OcppTimestamp::operator+=(int secs) { +Timestamp &Timestamp::operator+=(int secs) { second += secs; @@ -188,14 +205,31 @@ OcppTimestamp &OcppTimestamp::operator+=(int secs) { } return *this; -}; +} + +#if MO_ENABLE_TIMESTAMP_MILLISECONDS +Timestamp &Timestamp::addMilliseconds(int val) { + + ms += val; -OcppTimestamp &OcppTimestamp::operator-=(int secs) { + if (ms >= 0 && ms < 1000) return *this; + + auto dsecond = ms / 1000; + ms %= 1000; + if (ms < 0) { + dsecond--; + ms += 1000; + } + return this->operator+=(dsecond); +} +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + +Timestamp &Timestamp::operator-=(int secs) { return operator+=(-secs); } -otime_t OcppTimestamp::operator-(const OcppTimestamp &rhs) const { - //dt = rhs - ocpp_base +int Timestamp::operator-(const Timestamp &rhs) const { + //dt = rhs - mocpp_base int16_t year_base, year_end; if (year <= rhs.year) { @@ -220,40 +254,54 @@ otime_t OcppTimestamp::operator-(const OcppTimestamp &rhs) const { } } - otime_t dt = (lhsDays - rhsDays) * (24 * 3600) + (hour - rhs.hour) * 3600 + (minute - rhs.minute) * 60 + second - rhs.second; + 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; } -OcppTimestamp &OcppTimestamp::operator=(const OcppTimestamp &rhs) { +Timestamp &Timestamp::operator=(const Timestamp &rhs) { year = rhs.year; month = rhs.month; day = rhs.day; hour = rhs.hour; minute = rhs.minute; second = rhs.second; +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + ms = rhs.ms; +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS return *this; } -OcppTimestamp operator+(const OcppTimestamp &lhs, int secs) { - OcppTimestamp res = lhs; +Timestamp operator+(const Timestamp &lhs, int secs) { + Timestamp res = lhs; res += secs; return res; } -OcppTimestamp operator-(const OcppTimestamp &lhs, int secs) { +Timestamp operator-(const Timestamp &lhs, int secs) { return operator+(lhs, -secs); } -bool operator==(const OcppTimestamp &lhs, const OcppTimestamp &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; +bool operator==(const Timestamp &lhs, const Timestamp &rhs) { + return lhs.year == rhs.year && lhs.month == rhs.month && lhs.day == rhs.day && lhs.hour == rhs.hour && lhs.minute == rhs.minute && lhs.second == rhs.second +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + && lhs.ms == rhs.ms +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + ; } -bool operator!=(const OcppTimestamp &lhs, const OcppTimestamp &rhs) { +bool operator!=(const Timestamp &lhs, const Timestamp &rhs) { return !(lhs == rhs); } -bool operator<(const OcppTimestamp &lhs, const OcppTimestamp &rhs) { +bool operator<(const Timestamp &lhs, const Timestamp &rhs) { if (lhs.year != rhs.year) return lhs.year < rhs.year; if (lhs.month != rhs.month) @@ -266,65 +314,69 @@ bool operator<(const OcppTimestamp &lhs, const OcppTimestamp &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; } -bool operator<=(const OcppTimestamp &lhs, const OcppTimestamp &rhs) { +bool operator<=(const Timestamp &lhs, const Timestamp &rhs) { return lhs < rhs || lhs == rhs; } -bool operator>(const OcppTimestamp &lhs, const OcppTimestamp &rhs) { +bool operator>(const Timestamp &lhs, const Timestamp &rhs) { return rhs < lhs; } -bool operator>=(const OcppTimestamp &lhs, const OcppTimestamp &rhs) { +bool operator>=(const Timestamp &lhs, const Timestamp &rhs) { return rhs <= lhs; } -OcppTime::OcppTime(const OcppClock& system_clock) : system_clock(system_clock) { +Clock::Clock() { } -bool OcppTime::setOcppTime(const char* jsonDateString) { +bool Clock::setTime(const char* jsonDateString) { - OcppTimestamp timestamp = OcppTimestamp(); + Timestamp timestamp = Timestamp(); if (!timestamp.setTime(jsonDateString)) { return false; } - system_basetime = system_clock(); - ocpp_basetime = timestamp; - ocppTimeIsSet = true; + system_basetime = mocpp_tick_ms(); + mocpp_basetime = timestamp; - currentTime = ocpp_basetime; - previousUpdate = system_basetime; + currentTime = mocpp_basetime; + lastUpdate = system_basetime; return true; } -otime_t OcppTime::getOcppTimeScalar() { - return system_clock(); -} - -const OcppTimestamp &OcppTime::getOcppTimestampNow() { - otime_t tNow = system_clock(); - if (previousUpdate != tNow) { - currentTime += (tNow - previousUpdate); - previousUpdate = tNow; - } - return currentTime; -} +const Timestamp &Clock::now() { + auto tReading = mocpp_tick_ms(); + auto delta = tReading - lastUpdate; -OcppTimestamp OcppTime::createTimestamp(otime_t scalar) { - OcppTimestamp res = ocpp_basetime + (scalar - system_basetime); +#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 res; + return currentTime; } -otime_t OcppTime::toOcppTimeScalar(const OcppTimestamp &otimestamp) { - return otimestamp.operator-(ocpp_basetime); +Timestamp Clock::adjustPrebootTimestamp(const Timestamp& t) { + auto systemtime_in = t - Timestamp(); + if (systemtime_in > (int) system_basetime / 1000) { + return mocpp_basetime; + } + return mocpp_basetime - ((int) (system_basetime / 1000) - systemtime_in); } } diff --git a/src/MicroOcpp/Core/Time.h b/src/MicroOcpp/Core/Time.h new file mode 100644 index 00000000..9a82753b --- /dev/null +++ b/src/MicroOcpp/Core/Time.h @@ -0,0 +1,143 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_TIME_H +#define MO_TIME_H + +#include +#include +#include + +#include +#include + +#ifndef MO_ENABLE_TIMESTAMP_MILLISECONDS +#define MO_ENABLE_TIMESTAMP_MILLISECONDS 0 +#endif + +#if MO_ENABLE_TIMESTAMP_MILLISECONDS +#define JSONDATE_LENGTH 24 //max. ISO 8601 date length, excluding the terminating zero +#else +#define JSONDATE_LENGTH 20 //ISO 8601 date length, excluding the terminating zero +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + +namespace MicroOcpp { + +class Timestamp : public MemoryManaged { +private: + /* + * Internal representation of the current time. The initial values correspond to UNIX-time 0. January + * corresponds to month 0 and the first day in the month is day 0. + */ + int16_t year = 1970; + int16_t month = 0; + int16_t day = 0; + int32_t hour = 0; + int32_t minute = 0; + int32_t second = 0; +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + int32_t ms = 0; +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + +public: + + Timestamp(); + + Timestamp(const Timestamp& other); + +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + Timestamp(int16_t year, int16_t month, int16_t day, int32_t hour, int32_t minute, int32_t second, int32_t ms = 0); +#else + Timestamp(int16_t year, int16_t month, int16_t day, int32_t hour, int32_t minute, int32_t second); +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + + /** + * Expects a date string like + * 2020-10-01T20:53:32.486Z + * + * as generated in JavaScript by calling toJSON() on a Date object + * + * Only processes the first 19 characters. The subsequent are ignored until terminating 0. + * + * Has a semi-sophisticated type check included. Will return true on successful time set and false if + * the given string is not a JSON Date string. + * + * jsonDateString: 0-terminated string + */ + bool setTime(const char* jsonDateString); + + bool toJsonString(char *out, size_t buffsize) const; + + Timestamp &operator=(const Timestamp &rhs); + +#if MO_ENABLE_TIMESTAMP_MILLISECONDS + Timestamp &addMilliseconds(int ms); +#endif //MO_ENABLE_TIMESTAMP_MILLISECONDS + + /* + * Time periods are given in seconds for all of the following arithmetic operations + */ + Timestamp &operator+=(int secs); + Timestamp &operator-=(int secs); + + int operator-(const Timestamp &rhs) const; + + friend Timestamp operator+(const Timestamp &lhs, int secs); + friend Timestamp operator-(const Timestamp &lhs, int secs); + + friend bool operator==(const Timestamp &lhs, const Timestamp &rhs); + friend bool operator!=(const Timestamp &lhs, const Timestamp &rhs); + friend bool operator<(const Timestamp &lhs, const Timestamp &rhs); + friend bool operator<=(const Timestamp &lhs, const Timestamp &rhs); + friend bool operator>(const Timestamp &lhs, const Timestamp &rhs); + friend bool operator>=(const Timestamp &lhs, const Timestamp &rhs); +}; + +extern const Timestamp MIN_TIME; +extern const Timestamp MAX_TIME; + +class Clock { +private: + + Timestamp mocpp_basetime = Timestamp(); + decltype(mocpp_tick_ms()) system_basetime = 0; //the value of mocpp_tick_ms() when OCPP server's time was taken + decltype(mocpp_tick_ms()) lastUpdate = 0; + + Timestamp currentTime = Timestamp(); + +public: + + Clock(); + Clock(const Clock&) = delete; + Clock(const Clock&&) = delete; + Clock& operator=(const Clock&) = delete; + + const Timestamp &now(); + + /** + * Expects a date string like + * 2020-10-01T20:53:32.486Z + * + * as generated in JavaScript by calling toJSON() on a Date object + * + * Only processes the first 23 characters. The subsequent are ignored + * + * Has a semi-sophisticated type check included. Will return true on successful time set and false if + * the given string is not a JSON Date string. + * + * jsonDateString: 0-terminated string + */ + bool setTime(const char* jsonDateString); + + /* + * Timestamps which were taken before the Clock was initially set can be adjusted retrospectively. Two + * conditions must be true: the Clock was set in the meantime and the Timestamp was taken at the same + * run of this library. The caller must check this + */ + Timestamp adjustPrebootTimestamp(const Timestamp& t); +}; + +} + +#endif 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 new file mode 100644 index 00000000..51e71216 --- /dev/null +++ b/src/MicroOcpp/Debug.h @@ -0,0 +1,102 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_DEBUG_H +#define MO_DEBUG_H + +#include + +#define MO_DL_NONE 0x00 //suppress all output to the console +#define MO_DL_ERROR 0x01 //report failures +#define MO_DL_WARN 0x02 //report observed or assumed inconsistent state +#define MO_DL_INFO 0x03 //inform about internal state changes +#define MO_DL_DEBUG 0x04 //relevant info for debugging +#define MO_DL_VERBOSE 0x05 //all output + +#ifndef MO_DBG_LEVEL +#define MO_DBG_LEVEL MO_DL_INFO //default +#endif + +//MbedTLS debug level documented in mbedtls/debug.h: +#ifndef MO_DBG_LEVEL_MBEDTLS +#define MO_DBG_LEVEL_MBEDTLS 1 +#endif + +#define MO_DF_MINIMAL 0x00 //don't reveal origin of a debug message +#define MO_DF_COMPACT 0x01 //print module by file name and line number +#define MO_DF_FILE_LINE 0x02 //print file and line number +#define MO_DF_FULL 0x03 //print path and file and line numbr + +#ifndef MO_DBG_FORMAT +#define MO_DBG_FORMAT MO_DF_FILE_LINE //default +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +void mo_dbg_print_prefix(int level, const char *fn, int line); +void mo_dbg_print_suffix(); + +#ifdef __cplusplus +} +#endif + +#define MO_DBG(level, X) \ + do { \ + mo_dbg_print_prefix(level, __FILE__, __LINE__); \ + MO_CONSOLE_PRINTF X; \ + mo_dbg_print_suffix(); \ + } while (0) + +#if MO_DBG_LEVEL >= MO_DL_ERROR +#define MO_DBG_ERR(...) MO_DBG(MO_DL_ERROR,(__VA_ARGS__)) +#else +#define MO_DBG_ERR(...) ((void)0) +#endif + +#if MO_DBG_LEVEL >= MO_DL_WARN +#define MO_DBG_WARN(...) MO_DBG(MO_DL_WARN,(__VA_ARGS__)) +#else +#define MO_DBG_WARN(...) ((void)0) +#endif + +#if MO_DBG_LEVEL >= MO_DL_INFO +#define MO_DBG_INFO(...) MO_DBG(MO_DL_INFO,(__VA_ARGS__)) +#else +#define MO_DBG_INFO(...) ((void)0) +#endif + +#if MO_DBG_LEVEL >= MO_DL_DEBUG +#define MO_DBG_DEBUG(...) MO_DBG(MO_DL_DEBUG,(__VA_ARGS__)) +#else +#define MO_DBG_DEBUG(...) ((void)0) +#endif + +#if MO_DBG_LEVEL >= MO_DL_VERBOSE +#define MO_DBG_VERBOSE(...) MO_DBG(MO_DL_VERBOSE,(__VA_ARGS__)) +#else +#define MO_DBG_VERBOSE(...) ((void)0) +#endif + +#ifdef MO_TRAFFIC_OUT + +#define MO_DBG_TRAFFIC_OUT(...) \ + do { \ + MO_CONSOLE_PRINTF("[MO] Send: %s",__VA_ARGS__); \ + MO_CONSOLE_PRINTF("\n"); \ + } while (0) + +#define MO_DBG_TRAFFIC_IN(...) \ + do { \ + MO_CONSOLE_PRINTF("[MO] Recv: %.*s",__VA_ARGS__); \ + MO_CONSOLE_PRINTF("\n"); \ + } while (0) + +#else +#define MO_DBG_TRAFFIC_OUT(...) ((void)0) +#define MO_DBG_TRAFFIC_IN(...) ((void)0) +#endif + +#endif diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationData.cpp b/src/MicroOcpp/Model/Authorization/AuthorizationData.cpp new file mode 100644 index 00000000..08cbfbca --- /dev/null +++ b/src/MicroOcpp/Model/Authorization/AuthorizationData.cpp @@ -0,0 +1,183 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_LOCAL_AUTH + +#include +#include + +using namespace MicroOcpp; + +AuthorizationData::AuthorizationData() : MemoryManaged("v16.Authorization.AuthorizationData") { + +} + +AuthorizationData::AuthorizationData(AuthorizationData&& other) : MemoryManaged("v16.Authorization.AuthorizationData") { + operator=(std::move(other)); +} + +AuthorizationData::~AuthorizationData() { + MO_FREE(parentIdTag); + parentIdTag = nullptr; +} + +AuthorizationData& AuthorizationData::operator=(AuthorizationData&& other) { + parentIdTag = other.parentIdTag; + other.parentIdTag = nullptr; + expiryDate = std::move(other.expiryDate); + strncpy(idTag, other.idTag, IDTAG_LEN_MAX + 1); + idTag[IDTAG_LEN_MAX] = '\0'; + status = other.status; + return *this; +} + +void AuthorizationData::readJson(JsonObject entry, bool compact) { + if (entry.containsKey(AUTHDATA_KEY_IDTAG(compact))) { + strncpy(idTag, entry[AUTHDATA_KEY_IDTAG(compact)], IDTAG_LEN_MAX + 1); + idTag[IDTAG_LEN_MAX] = '\0'; + } else { + idTag[0] = '\0'; + } + + JsonObject idTagInfo; + if (compact){ + idTagInfo = entry; + } else { + idTagInfo = entry[AUTHDATA_KEY_IDTAGINFO]; + } + + if (idTagInfo.containsKey(AUTHDATA_KEY_EXPIRYDATE(compact))) { + expiryDate = std::unique_ptr(new Timestamp()); + if (!expiryDate->setTime(idTagInfo[AUTHDATA_KEY_EXPIRYDATE(compact)])) { + expiryDate.reset(); + } + } else { + expiryDate.reset(); + } + + if (idTagInfo.containsKey(AUTHDATA_KEY_PARENTIDTAG(compact))) { + MO_FREE(parentIdTag); + parentIdTag = nullptr; + parentIdTag = static_cast(MO_MALLOC(getMemoryTag(), IDTAG_LEN_MAX + 1)); + if (parentIdTag) { + strncpy(parentIdTag, idTagInfo[AUTHDATA_KEY_PARENTIDTAG(compact)], IDTAG_LEN_MAX + 1); + parentIdTag[IDTAG_LEN_MAX] = '\0'; + } else { + MO_DBG_ERR("OOM"); + } + } else { + MO_FREE(parentIdTag); + parentIdTag = nullptr; + } + + if (idTagInfo.containsKey(AUTHDATA_KEY_STATUS(compact))) { + status = deserializeAuthorizationStatus(idTagInfo[AUTHDATA_KEY_STATUS(compact)]); + } else { + if (compact) { + status = AuthorizationStatus::Accepted; + } else { + status = AuthorizationStatus::UNDEFINED; + } + } +} + +size_t AuthorizationData::getJsonCapacity() const { + return JSON_OBJECT_SIZE(2) + + (idTag[0] != '\0' ? + JSON_OBJECT_SIZE(1) : 0) + + (expiryDate ? + JSON_OBJECT_SIZE(1) + JSONDATE_LENGTH + 1 : 0) + + (parentIdTag ? + JSON_OBJECT_SIZE(1) : 0) + + (status != AuthorizationStatus::UNDEFINED ? + JSON_OBJECT_SIZE(1) : 0); +} + +void AuthorizationData::writeJson(JsonObject& entry, bool compact) { + if (idTag[0] != '\0') { + entry[AUTHDATA_KEY_IDTAG(compact)] = (const char*) idTag; + } + + JsonObject idTagInfo; + if (compact) { + idTagInfo = entry; + } else { + idTagInfo = entry.createNestedObject(AUTHDATA_KEY_IDTAGINFO); + } + + if (expiryDate) { + char buf [JSONDATE_LENGTH + 1]; + if (expiryDate->toJsonString(buf, JSONDATE_LENGTH + 1)) { + idTagInfo[AUTHDATA_KEY_EXPIRYDATE(compact)] = buf; + } + } + + if (parentIdTag) { + idTagInfo[AUTHDATA_KEY_PARENTIDTAG(compact)] = (const char *) parentIdTag; + } + + if (status != AuthorizationStatus::Accepted) { + idTagInfo[AUTHDATA_KEY_STATUS(compact)] = serializeAuthorizationStatus(status); + } else if (!compact) { + idTagInfo[AUTHDATA_KEY_STATUS(compact)] = serializeAuthorizationStatus(AuthorizationStatus::Invalid); + } +} + +const char *AuthorizationData::getIdTag() const { + return idTag; +} +Timestamp *AuthorizationData::getExpiryDate() const { + return expiryDate.get(); +} +const char *AuthorizationData::getParentIdTag() const { + return parentIdTag; +} +AuthorizationStatus AuthorizationData::getAuthorizationStatus() const { + return status; +} + +void AuthorizationData::reset() { + idTag[0] = '\0'; +} + +const char *MicroOcpp::serializeAuthorizationStatus(AuthorizationStatus status) { + switch (status) { + case (AuthorizationStatus::Accepted): + return "Accepted"; + case (AuthorizationStatus::Blocked): + return "Blocked"; + case (AuthorizationStatus::Expired): + return "Expired"; + case (AuthorizationStatus::Invalid): + return "Invalid"; + case (AuthorizationStatus::ConcurrentTx): + return "ConcurrentTx"; + default: + return "UNDEFINED"; + } +} + +MicroOcpp::AuthorizationStatus MicroOcpp::deserializeAuthorizationStatus(const char *cstr) { + if (!cstr) { + return AuthorizationStatus::UNDEFINED; + } + + if (!strcmp(cstr, "Accepted")) { + return AuthorizationStatus::Accepted; + } else if (!strcmp(cstr, "Blocked")) { + return AuthorizationStatus::Blocked; + } else if (!strcmp(cstr, "Expired")) { + return AuthorizationStatus::Expired; + } else if (!strcmp(cstr, "Invalid")) { + return AuthorizationStatus::Invalid; + } else if (!strcmp(cstr, "ConcurrentTx")) { + return AuthorizationStatus::ConcurrentTx; + } else { + return AuthorizationStatus::UNDEFINED; + } +} + +#endif //MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationData.h b/src/MicroOcpp/Model/Authorization/AuthorizationData.h new file mode 100644 index 00000000..7bcdccd7 --- /dev/null +++ b/src/MicroOcpp/Model/Authorization/AuthorizationData.h @@ -0,0 +1,73 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_AUTHORIZATIONDATA_H +#define MO_AUTHORIZATIONDATA_H + +#include + +#if MO_ENABLE_LOCAL_AUTH + +#include +#include +#include +#include +#include + +namespace MicroOcpp { + +enum class AuthorizationStatus : uint8_t { + Accepted, + Blocked, + Expired, + Invalid, + ConcurrentTx, + UNDEFINED //not part of OCPP 1.6 +}; + +#define AUTHDATA_KEY_IDTAG(COMPACT) (COMPACT ? "it" : "idTag") +#define AUTHDATA_KEY_IDTAGINFO "idTagInfo" +#define AUTHDATA_KEY_EXPIRYDATE(COMPACT) (COMPACT ? "ed" : "expiryDate") +#define AUTHDATA_KEY_PARENTIDTAG(COMPACT) (COMPACT ? "pi" : "parentIdTag") +#define AUTHDATA_KEY_STATUS(COMPACT) (COMPACT ? "st" : "status") + +#define AUTHORIZATIONSTATUS_LEN_MAX (sizeof("ConcurrentTx") - 1) //max length of serialized AuthStatus + +const char *serializeAuthorizationStatus(AuthorizationStatus status); +AuthorizationStatus deserializeAuthorizationStatus(const char *cstr); + +class AuthorizationData : public MemoryManaged { +private: + //data structure optimized for memory consumption + + char *parentIdTag = nullptr; + std::unique_ptr expiryDate; + + char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; + + AuthorizationStatus status = AuthorizationStatus::UNDEFINED; +public: + AuthorizationData(); + AuthorizationData(AuthorizationData&& other); + ~AuthorizationData(); + + AuthorizationData& operator=(AuthorizationData&& other); + + void readJson(JsonObject entry, bool compact = false); //compact: compressed representation for flash storage + + size_t getJsonCapacity() const; + void writeJson(JsonObject& entry, bool compact = false); //compact: compressed representation for flash storage + + const char *getIdTag() const; + Timestamp *getExpiryDate() const; + const char *getParentIdTag() const; + AuthorizationStatus getAuthorizationStatus() const; + + void reset(); +}; + +} + +#endif //MO_ENABLE_LOCAL_AUTH +#endif diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationList.cpp b/src/MicroOcpp/Model/Authorization/AuthorizationList.cpp new file mode 100644 index 00000000..dd438ff8 --- /dev/null +++ b/src/MicroOcpp/Model/Authorization/AuthorizationList.cpp @@ -0,0 +1,202 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_LOCAL_AUTH + +#include +#include + +#include +#include + +using namespace MicroOcpp; + +AuthorizationList::AuthorizationList() : MemoryManaged("v16.Authorization.AuthorizationList"), localAuthorizationList(makeVector(getMemoryTag())) { + +} + +AuthorizationList::~AuthorizationList() { + +} + +MicroOcpp::AuthorizationData *AuthorizationList::get(const char *idTag) { + //binary search + + if (!idTag) { + return nullptr; + } + + int l = 0; + int r = ((int) localAuthorizationList.size()) - 1; + while (l <= r) { + auto m = (r + l) / 2; + auto diff = strcmp(localAuthorizationList[m].getIdTag(), idTag); + if (diff < 0) { + l = m + 1; + } else if (diff > 0) { + r = m - 1; + } else { + return &localAuthorizationList[m]; + } + } + return nullptr; +} + +bool AuthorizationList::readJson(JsonArray authlistJson, int listVersion, bool differential, bool compact) { + + if (compact) { + //compact representations don't contain remove commands + differential = false; + } + + for (size_t i = 0; i < authlistJson.size(); i++) { + + //check if JSON object is valid + if (!authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAG(compact))) { + return false; + } + } + + auto authlist_index = makeVector(getMemoryTag()); + auto remove_list = makeVector(getMemoryTag()); + + unsigned int resultingListLength = 0; + + if (!differential) { + //every entry will insert an idTag + resultingListLength = authlistJson.size(); + } else { + //update type is differential; only unkown entries will insert an idTag + + resultingListLength = localAuthorizationList.size(); + + //also, build index here + authlist_index.resize(authlistJson.size(), -1); + + for (size_t i = 0; i < authlistJson.size(); i++) { + + //check if locally stored auth info is present; if yes, apply it to the index + AuthorizationData *found = get(authlistJson[i][AUTHDATA_KEY_IDTAG(compact)]); + + if (found) { + + authlist_index[i] = (int) (found - localAuthorizationList.data()); + + //remove or update? + if (!authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAGINFO)) { + //this entry should be removed + found->reset(); //mark for deletion + remove_list.push_back((int) (found - localAuthorizationList.data())); + resultingListLength--; + } //else: this entry should be updated + } else { + //insert or ignore? + if (authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAGINFO)) { + //add + resultingListLength++; + } //else: ignore + } + } + } + + if (resultingListLength > MO_LocalAuthListMaxLength) { + MO_DBG_WARN("localAuthList capacity exceeded"); + return false; + } + + //apply new list + + if (compact) { + localAuthorizationList.clear(); + + for (size_t i = 0; i < authlistJson.size(); i++) { + localAuthorizationList.emplace_back(); + localAuthorizationList.back().readJson(authlistJson[i], compact); + } + } else if (differential) { + + for (size_t i = 0; i < authlistJson.size(); i++) { + + //is entry a remove command? + if (!authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAGINFO)) { + continue; //yes, remove command, will be deleted afterwards + } + + //update, or insert + + if (authlist_index[i] < 0) { + //auth list does not contain idTag yet -> insert new entry + + //reuse removed AuthData object? + if (!remove_list.empty()) { + //yes, reuse + authlist_index[i] = remove_list.back(); + remove_list.pop_back(); + } else { + //no, create new + authlist_index[i] = localAuthorizationList.size(); + localAuthorizationList.emplace_back(); + } + } + + localAuthorizationList[authlist_index[i]].readJson(authlistJson[i], compact); + } + + } else { + localAuthorizationList.clear(); + + for (size_t i = 0; i < authlistJson.size(); i++) { + if (authlistJson[i].as().containsKey(AUTHDATA_KEY_IDTAGINFO)) { + localAuthorizationList.emplace_back(); + localAuthorizationList.back().readJson(authlistJson[i], compact); + } + } + } + + localAuthorizationList.erase(std::remove_if(localAuthorizationList.begin(), localAuthorizationList.end(), + [] (const AuthorizationData& elem) { + return elem.getIdTag()[0] == '\0'; //"" means no idTag --> marked for removal + }), localAuthorizationList.end()); + + std::sort(localAuthorizationList.begin(), localAuthorizationList.end(), + [] (const AuthorizationData& lhs, const AuthorizationData& rhs) { + return strcmp(lhs.getIdTag(), rhs.getIdTag()) < 0; + }); + + this->listVersion = listVersion; + + if (localAuthorizationList.empty()) { + this->listVersion = 0; + } + + return true; +} + +void AuthorizationList::clear() { + localAuthorizationList.clear(); + listVersion = 0; +} + +size_t AuthorizationList::getJsonCapacity() { + size_t res = JSON_ARRAY_SIZE(localAuthorizationList.size()); + for (auto& entry : localAuthorizationList) { + res += entry.getJsonCapacity(); + } + return res; +} + +void AuthorizationList::writeJson(JsonArray authListOut, bool compact) { + for (auto& entry : localAuthorizationList) { + JsonObject entryJson = authListOut.createNestedObject(); + entry.writeJson(entryJson, compact); + } +} + +size_t AuthorizationList::size() { + return localAuthorizationList.size(); +} + +#endif //MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationList.h b/src/MicroOcpp/Model/Authorization/AuthorizationList.h new file mode 100644 index 00000000..0a083777 --- /dev/null +++ b/src/MicroOcpp/Model/Authorization/AuthorizationList.h @@ -0,0 +1,49 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_AUTHORIZATIONLIST_H +#define MO_AUTHORIZATIONLIST_H + +#include + +#if MO_ENABLE_LOCAL_AUTH + +#include +#include + +#ifndef MO_LocalAuthListMaxLength +#define MO_LocalAuthListMaxLength 48 +#endif + +#ifndef MO_SendLocalListMaxLength +#define MO_SendLocalListMaxLength MO_LocalAuthListMaxLength +#endif + +namespace MicroOcpp { + +class AuthorizationList : public MemoryManaged { +private: + int listVersion = 0; + Vector localAuthorizationList; //sorted list +public: + AuthorizationList(); + ~AuthorizationList(); + + AuthorizationData *get(const char *idTag); + + bool readJson(JsonArray localAuthorizationList, int listVersion, bool differential = false, bool compact = false); //compact: if true, then use compact non-ocpp representation + void clear(); + + size_t getJsonCapacity(); + void writeJson(JsonArray authListOut, bool compact = false); + + int getListVersion() {return listVersion;} + size_t size(); //used in unit tests + +}; + +} + +#endif //MO_ENABLE_LOCAL_AUTH +#endif diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationService.cpp b/src/MicroOcpp/Model/Authorization/AuthorizationService.cpp new file mode 100644 index 00000000..dc395ff9 --- /dev/null +++ b/src/MicroOcpp/Model/Authorization/AuthorizationService.cpp @@ -0,0 +1,207 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_LOCAL_AUTH + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MO_LOCALAUTHORIZATIONLIST_FN (MO_FILENAME_PREFIX "localauth.jsn") + +using namespace MicroOcpp; + +AuthorizationService::AuthorizationService(Context& context, std::shared_ptr filesystem) : MemoryManaged("v16.Authorization.AuthorizationService"), context(context), filesystem(filesystem) { + + localAuthListEnabledBool = declareConfiguration("LocalAuthListEnabled", true, CONFIGURATION_FN, false, true); + declareConfiguration("LocalAuthListMaxLength", MO_LocalAuthListMaxLength, CONFIGURATION_VOLATILE, true); + declareConfiguration("SendLocalListMaxLength", MO_SendLocalListMaxLength, CONFIGURATION_VOLATILE, true); + + if (!localAuthListEnabledBool) { + MO_DBG_ERR("initialization error"); + } + + context.getOperationRegistry().registerOperation("GetLocalListVersion", [&context] () { + return new Ocpp16::GetLocalListVersion(context.getModel());}); + context.getOperationRegistry().registerOperation("SendLocalList", [this] () { + return new Ocpp16::SendLocalList(*this);}); + + loadLists(); +} + +AuthorizationService::~AuthorizationService() { + +} + +bool AuthorizationService::loadLists() { + if (!filesystem) { + MO_DBG_WARN("no fs access"); + return true; + } + + size_t msize = 0; + if (filesystem->stat(MO_LOCALAUTHORIZATIONLIST_FN, &msize) != 0) { + MO_DBG_DEBUG("no local authorization list stored already"); + return true; + } + + auto doc = FilesystemUtils::loadJson(filesystem, MO_LOCALAUTHORIZATIONLIST_FN, getMemoryTag()); + if (!doc) { + MO_DBG_ERR("failed to load %s", MO_LOCALAUTHORIZATIONLIST_FN); + return false; + } + + JsonObject root = doc->as(); + + int listVersion = root["listVersion"] | 0; + + if (!localAuthorizationList.readJson(root["localAuthorizationList"].as(), listVersion, false, true)) { + MO_DBG_ERR("list read failure"); + return false; + } + + return true; +} + +AuthorizationData *AuthorizationService::getLocalAuthorization(const char *idTag) { + if (!localAuthListEnabled()) { + return nullptr; //auth cache will follow + } + + auto authData = localAuthorizationList.get(idTag); + if (!authData) { + return nullptr; + } + + //check status + if (authData->getAuthorizationStatus() != AuthorizationStatus::Accepted) { + MO_DBG_DEBUG("idTag %s local auth status %s", idTag, serializeAuthorizationStatus(authData->getAuthorizationStatus())); + return authData; + } + + return authData; +} + +int AuthorizationService::getLocalListVersion() { + return localAuthorizationList.getListVersion(); +} + +size_t AuthorizationService::getLocalListSize() { + return localAuthorizationList.size(); +} + +bool AuthorizationService::localAuthListEnabled() const { + return localAuthListEnabledBool && localAuthListEnabledBool->getBool(); +} + +bool AuthorizationService::updateLocalList(JsonArray localAuthorizationListJson, int listVersion, bool differential) { + //TC_043_3_CS-Send Local Authorization List - Failed + //return false; + + bool success = localAuthorizationList.readJson(localAuthorizationListJson, listVersion, differential, false); + + if (success) { + + auto doc = initJsonDoc(getMemoryTag(), + JSON_OBJECT_SIZE(3) + + localAuthorizationList.getJsonCapacity()); + + JsonObject root = doc.to(); + root["listVersion"] = listVersion; + JsonArray authListCompact = root.createNestedArray("localAuthorizationList"); + localAuthorizationList.writeJson(authListCompact, true); + success = FilesystemUtils::storeJson(filesystem, MO_LOCALAUTHORIZATIONLIST_FN, doc); + + if (!success) { + loadLists(); + } + } + + return success; +} + +void AuthorizationService::notifyAuthorization(const char *idTag, JsonObject idTagInfo) { + //check local list conflicts. In future: also update authorization cache + + if (!localAuthListEnabled()) { + return; //auth cache will follow + } + + if (!idTagInfo.containsKey("status")) { + return; //empty idTagInfo + } + + auto localInfo = localAuthorizationList.get(idTag); + if (!localInfo) { + return; + } + + //check for conflicts + + auto incomingStatus = deserializeAuthorizationStatus(idTagInfo["status"]); + auto localStatus = localInfo->getAuthorizationStatus(); + + if (incomingStatus == AuthorizationStatus::UNDEFINED) { //ignore invalid messages (handled elsewhere) + return; + } + + if (incomingStatus == AuthorizationStatus::ConcurrentTx) { //incoming status ConcurrentTx is equivalent to local Accepted + incomingStatus = AuthorizationStatus::Accepted; + } + + if (localStatus == AuthorizationStatus::Accepted && localInfo->getExpiryDate()) { //check for expiry + auto& t_now = context.getModel().getClock().now(); + if (t_now > *localInfo->getExpiryDate()) { + MO_DBG_DEBUG("local auth expired"); + localStatus = AuthorizationStatus::Expired; + } + } + + bool equivalent = true; + + if (incomingStatus != localStatus) { + MO_DBG_WARN("local auth list status conflict"); + equivalent = false; + } + + //check if parentIdTag definitions mismatch + if (equivalent && + strcmp(localInfo->getParentIdTag() ? localInfo->getParentIdTag() : "", idTagInfo["parentIdTag"] | "")) { + MO_DBG_WARN("local auth list parentIdTag conflict"); + equivalent = false; + } + + MO_DBG_DEBUG("idTag %s fully evaluated: %s conflict", idTag, equivalent ? "no" : "contains"); + + if (!equivalent) { + //send error code "LocalListConflict" to server + + ChargePointStatus cpStatus = ChargePointStatus_UNDEFINED; + if (context.getModel().getNumConnectors() > 0) { + cpStatus = context.getModel().getConnector(0)->getStatus(); + } + + auto statusNotification = makeRequest(new Ocpp16::StatusNotification( + 0, + cpStatus, //will be determined in StatusNotification::initiate + context.getModel().getClock().now(), + "LocalListConflict")); + + statusNotification->setTimeout(60000); + + context.initiateRequest(std::move(statusNotification)); + } +} + +#endif //MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Model/Authorization/AuthorizationService.h b/src/MicroOcpp/Model/Authorization/AuthorizationService.h new file mode 100644 index 00000000..536bba1f --- /dev/null +++ b/src/MicroOcpp/Model/Authorization/AuthorizationService.h @@ -0,0 +1,49 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_AUTHORIZATIONSERVICE_H +#define MO_AUTHORIZATIONSERVICE_H + +#include + +#if MO_ENABLE_LOCAL_AUTH + +#include +#include +#include +#include + +namespace MicroOcpp { + +class Context; + +class AuthorizationService : public MemoryManaged { +private: + Context& context; + std::shared_ptr filesystem; + AuthorizationList localAuthorizationList; + + std::shared_ptr localAuthListEnabledBool; + +public: + AuthorizationService(Context& context, std::shared_ptr filesystem); + ~AuthorizationService(); + + bool loadLists(); + + AuthorizationData *getLocalAuthorization(const char *idTag); + + int getLocalListVersion(); + bool localAuthListEnabled() const; + size_t getLocalListSize(); //number of entries in current localAuthList; used in unit tests + + bool updateLocalList(JsonArray localAuthorizationListJson, int listVersion, bool differential); + + void notifyAuthorization(const char *idTag, JsonObject idTagInfo); +}; + +} + +#endif //MO_ENABLE_LOCAL_AUTH +#endif 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 new file mode 100644 index 00000000..1aabb200 --- /dev/null +++ b/src/MicroOcpp/Model/Boot/BootService.cpp @@ -0,0 +1,265 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace MicroOcpp; + +unsigned int PreBootQueue::getFrontRequestOpNr() { + if (!activatedPostBootCommunication) { + return 0; + } + + return VolatileRequestQueue::getFrontRequestOpNr(); +} + +void PreBootQueue::activatePostBootCommunication() { + activatedPostBootCommunication = true; +} + +RegistrationStatus MicroOcpp::deserializeRegistrationStatus(const char *serialized) { + if (!strcmp(serialized, "Accepted")) { + return RegistrationStatus::Accepted; + } else if (!strcmp(serialized, "Pending")) { + return RegistrationStatus::Pending; + } else if (!strcmp(serialized, "Rejected")) { + return RegistrationStatus::Rejected; + } else { + MO_DBG_ERR("deserialization error"); + return RegistrationStatus::UNDEFINED; + } +} + +BootService::BootService(Context& context, std::shared_ptr filesystem) : MemoryManaged("v16.Boot.BootService"), context(context), filesystem(filesystem), cpCredentials{makeString(getMemoryTag())} { + + context.getRequestQueue().setPreBootSendQueue(&preBootQueue); //register PreBootQueue in RequestQueue module + + //if transactions can start before the BootNotification succeeds + preBootTransactionsBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", false); + + if (!preBootTransactionsBool) { + MO_DBG_ERR("initialization error"); + } + + //Register message handler for TriggerMessage operation + context.getOperationRegistry().registerOperation("BootNotification", [this] () { + return new Ocpp16::BootNotification(this->context.getModel(), getChargePointCredentials());}); +} + +void BootService::loop() { + + if (!executedFirstTime) { + executedFirstTime = true; + firstExecutionTimestamp = mocpp_tick_ms(); + } + + if (!executedLongTime && mocpp_tick_ms() - firstExecutionTimestamp >= MO_BOOTSTATS_LONGTIME_MS) { + executedLongTime = true; + MO_DBG_DEBUG("boot success timer reached"); + + configuration_clean_unused(); + + BootStats bootstats; + loadBootStats(filesystem, bootstats); + bootstats.lastBootSuccess = bootstats.bootNr; + storeBootStats(filesystem, bootstats); + } + + preBootQueue.loop(); + + if (!activatedPostBootCommunication && status == RegistrationStatus::Accepted) { + preBootQueue.activatePostBootCommunication(); + activatedPostBootCommunication = true; + } + + if (!activatedModel && (status == RegistrationStatus::Accepted || preBootTransactionsBool->getBool())) { + context.getModel().activateTasks(); + activatedModel = true; + } + + if (status == RegistrationStatus::Accepted) { + return; + } + + if (mocpp_tick_ms() - lastBootNotification < (interval_s * 1000UL)) { + return; + } + + /* + * Create BootNotification. The BootNotifaction object will fetch its paremeters from + * this class and notify this class about the response + */ + auto bootNotification = makeRequest(new Ocpp16::BootNotification(context.getModel(), getChargePointCredentials())); + bootNotification->setTimeout(interval_s * 1000UL); + context.getRequestQueue().sendRequestPreBoot(std::move(bootNotification)); + + lastBootNotification = mocpp_tick_ms(); +} + +void BootService::setChargePointCredentials(JsonObject credentials) { + auto written = serializeJson(credentials, cpCredentials); + if (written < 2) { + MO_DBG_ERR("serialization error"); + cpCredentials = "{}"; + } +} + +void BootService::setChargePointCredentials(const char *credentials) { + cpCredentials = credentials; + if (cpCredentials.size() < 2) { + cpCredentials = "{}"; + } +} + +std::unique_ptr BootService::getChargePointCredentials() { + if (cpCredentials.size() <= 2) { + return createEmptyDocument(); + } + + std::unique_ptr doc; + size_t capacity = JSON_OBJECT_SIZE(9) + cpCredentials.size(); + DeserializationError err = DeserializationError::NoMemory; + while (err == DeserializationError::NoMemory && capacity <= MO_MAX_JSON_CAPACITY) { + doc = makeJsonDoc(getMemoryTag(), capacity); + err = deserializeJson(*doc, cpCredentials); + + capacity *= 2; + } + + if (!err) { + return doc; + } else { + MO_DBG_ERR("could not parse stored credentials: %s", err.c_str()); + return nullptr; + } +} + +void BootService::notifyRegistrationStatus(RegistrationStatus status) { + this->status = status; + lastBootNotification = mocpp_tick_ms(); +} + +void BootService::setRetryInterval(unsigned long interval_s) { + if (interval_s == 0) { + this->interval_s = MO_BOOT_INTERVAL_DEFAULT; + } else { + this->interval_s = interval_s; + } + lastBootNotification = mocpp_tick_ms(); +} + +bool BootService::loadBootStats(std::shared_ptr filesystem, BootStats& bstats) { + if (!filesystem) { + return false; + } + + size_t msize = 0; + if (filesystem->stat(MO_FILENAME_PREFIX "bootstats.jsn", &msize) == 0) { + + bool success = true; + + auto json = FilesystemUtils::loadJson(filesystem, MO_FILENAME_PREFIX "bootstats.jsn", "v16.Boot.BootService"); + if (json) { + int bootNrIn = (*json)["bootNr"] | -1; + if (bootNrIn >= 0 && bootNrIn <= std::numeric_limits::max()) { + bstats.bootNr = (uint16_t) bootNrIn; + } else { + success = false; + } + + int lastSuccessIn = (*json)["lastSuccess"] | -1; + if (lastSuccessIn >= 0 && lastSuccessIn <= std::numeric_limits::max()) { + bstats.lastBootSuccess = (uint16_t) lastSuccessIn; + } else { + success = false; + } + + const char *microOcppVersionIn = (*json)["MicroOcppVersion"] | (const char*)nullptr; + if (microOcppVersionIn) { + auto ret = snprintf(bstats.microOcppVersion, sizeof(bstats.microOcppVersion), "%s", microOcppVersionIn); + if (ret < 0 || (size_t)ret >= sizeof(bstats.microOcppVersion)) { + success = false; + } + } //else: version specifier can be missing after upgrade from pre 1.2.0 version + } else { + success = false; + } + + if (!success) { + MO_DBG_ERR("bootstats corrupted"); + filesystem->remove(MO_FILENAME_PREFIX "bootstats.jsn"); + bstats = BootStats(); + } + + return success; + } else { + return false; + } +} + +bool BootService::storeBootStats(std::shared_ptr filesystem, BootStats& bstats) { + if (!filesystem) { + return false; + } + + auto json = initJsonDoc("v16.Boot.BootService", JSON_OBJECT_SIZE(3)); + + json["bootNr"] = bstats.bootNr; + json["lastSuccess"] = bstats.lastBootSuccess; + json["MicroOcppVersion"] = (const char*)bstats.microOcppVersion; + + return FilesystemUtils::storeJson(filesystem, MO_FILENAME_PREFIX "bootstats.jsn", json); +} + +bool BootService::recover(std::shared_ptr filesystem, BootStats& bstats) { + if (!filesystem) { + return false; + } + + bool success = FilesystemUtils::remove_if(filesystem, [] (const char *fname) -> bool { + return !strncmp(fname, "sd", strlen("sd")) || + !strncmp(fname, "tx", strlen("tx")) || + !strncmp(fname, "sc-", strlen("sc-")) || + !strncmp(fname, "reservation", strlen("reservation")) || + !strncmp(fname, "client-state", strlen("client-state")); + }); + MO_DBG_ERR("clear local state files (recovery): %s", success ? "success" : "not completed"); + + return success; +} + +bool BootService::migrate(std::shared_ptr filesystem, BootStats& bstats) { + if (!filesystem) { + return false; + } + + bool success = true; + + if (strcmp(bstats.microOcppVersion, MO_VERSION)) { + MO_DBG_INFO("migrate persistent storage to MO v" MO_VERSION); + success = FilesystemUtils::remove_if(filesystem, [] (const char *fname) -> bool { + return !strncmp(fname, "sd", strlen("sd")) || + !strncmp(fname, "tx", strlen("tx")) || + !strncmp(fname, "op", strlen("op")) || + !strncmp(fname, "sc-", strlen("sc-")) || + !strcmp(fname, "client-state.cnf") || + !strcmp(fname, "arduino-ocpp.cnf") || + !strcmp(fname, "ocpp-creds.jsn"); + }); + + snprintf(bstats.microOcppVersion, sizeof(bstats.microOcppVersion), "%s", MO_VERSION); + MO_DBG_DEBUG("clear local state files (migration): %s", success ? "success" : "not completed"); + } + return success; +} diff --git a/src/MicroOcpp/Model/Boot/BootService.h b/src/MicroOcpp/Model/Boot/BootService.h new file mode 100644 index 00000000..6091dc2c --- /dev/null +++ b/src/MicroOcpp/Model/Boot/BootService.h @@ -0,0 +1,100 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_BOOTSERVICE_H +#define MO_BOOTSERVICE_H + +#include +#include +#include +#include +#include + +#define MO_BOOT_INTERVAL_DEFAULT 60 + +#ifndef MO_BOOTSTATS_LONGTIME_MS +#define MO_BOOTSTATS_LONGTIME_MS 180 * 1000 +#endif + +namespace MicroOcpp { + +#define MO_BOOTSTATS_VERSION_SIZE 10 + +struct BootStats { + uint16_t bootNr = 0; + uint16_t lastBootSuccess = 0; + + uint16_t getBootFailureCount() { + return bootNr - lastBootSuccess; + } + + char microOcppVersion [MO_BOOTSTATS_VERSION_SIZE] = {'\0'}; +}; + +enum class RegistrationStatus { + Accepted, + Pending, + Rejected, + UNDEFINED +}; + +RegistrationStatus deserializeRegistrationStatus(const char *serialized); + +class PreBootQueue : public VolatileRequestQueue { +private: + bool activatedPostBootCommunication = false; +public: + unsigned int getFrontRequestOpNr() override; //override FrontRequestOpNr behavior: in PreBoot mode, always return 0 to avoid other RequestEmitters from sending msgs + + void activatePostBootCommunication(); //end PreBoot mode, now send Requests normally +}; + +class Context; + +class BootService : public MemoryManaged { +private: + Context& context; + std::shared_ptr filesystem; + + PreBootQueue preBootQueue; + + unsigned long interval_s = MO_BOOT_INTERVAL_DEFAULT; + unsigned long lastBootNotification = -1UL / 2; + + RegistrationStatus status = RegistrationStatus::Pending; + + String cpCredentials; + + std::shared_ptr preBootTransactionsBool; + + bool activatedModel = false; + bool activatedPostBootCommunication = false; + + unsigned long firstExecutionTimestamp = 0; + bool executedFirstTime = false; + bool executedLongTime = false; + +public: + BootService(Context& context, std::shared_ptr filesystem); + + void loop(); + + void setChargePointCredentials(JsonObject credentials); + void setChargePointCredentials(const char *credentials); //credentials: serialized BootNotification payload + std::unique_ptr getChargePointCredentials(); + + void notifyRegistrationStatus(RegistrationStatus status); + void setRetryInterval(unsigned long interval); + + static bool loadBootStats(std::shared_ptr filesystem, BootStats& bstats); + static bool storeBootStats(std::shared_ptr filesystem, BootStats& bstats); + + static bool recover(std::shared_ptr filesystem, BootStats& bstats); //delete all persistent files which could lead to a crash + + static bool migrate(std::shared_ptr filesystem, BootStats& bstats); //migrate persistent storage if running on a new MO version +}; + +} + +#endif 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 new file mode 100644 index 00000000..6c7b04f2 --- /dev/null +++ b/src/MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h @@ -0,0 +1,33 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CHARGEPOINTERRORCODE_H +#define MO_CHARGEPOINTERRORCODE_H + +#include + +namespace MicroOcpp { + +struct ErrorData { + bool isError = false; //if any error information is set + bool isFaulted = false; //if this is a severe error and the EVSE should go into the faulted state + uint8_t severity = 1; //severity: don't send less severe errors during highly severe error condition + const char *errorCode = nullptr; //see ChargePointErrorCode (p. 76/77) for possible values + const char *info = nullptr; //Additional free format information related to the error + const char *vendorId = nullptr; //vendor-specific implementation identifier + const char *vendorErrorCode = nullptr; //vendor-specific error code + + ErrorData() = default; + + ErrorData(const char *errorCode = nullptr) : errorCode(errorCode) { + if (errorCode) { + isError = true; + isFaulted = true; + } + } +}; + +} + +#endif diff --git a/src/MicroOcpp/Model/ConnectorBase/ChargePointStatus.h b/src/MicroOcpp/Model/ConnectorBase/ChargePointStatus.h new file mode 100644 index 00000000..019fce27 --- /dev/null +++ b/src/MicroOcpp/Model/ConnectorBase/ChargePointStatus.h @@ -0,0 +1,36 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CHARGEPOINTSTATUS_H +#define MO_CHARGEPOINTSTATUS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum ChargePointStatus { + ChargePointStatus_UNDEFINED, //internal use only - no OCPP standard value + ChargePointStatus_Available, + ChargePointStatus_Preparing, + ChargePointStatus_Charging, + ChargePointStatus_SuspendedEVSE, + ChargePointStatus_SuspendedEV, + ChargePointStatus_Finishing, + ChargePointStatus_Reserved, + ChargePointStatus_Unavailable, + ChargePointStatus_Faulted + +#if MO_ENABLE_V201 + ,ChargePointStatus_Occupied +#endif + +} ChargePointStatus; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/MicroOcpp/Model/ConnectorBase/Connector.cpp b/src/MicroOcpp/Model/ConnectorBase/Connector.cpp new file mode 100644 index 00000000..11142dc9 --- /dev/null +++ b/src/MicroOcpp/Model/ConnectorBase/Connector.cpp @@ -0,0 +1,1322 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#ifndef MO_TX_CLEAN_ABORTED +#define MO_TX_CLEAN_ABORTED 1 +#endif + +using namespace MicroOcpp; + +Connector::Connector(Context& context, std::shared_ptr filesystem, unsigned int connectorId) + : MemoryManaged("v16.ConnectorBase.Connector"), context(context), model(context.getModel()), filesystem(filesystem), connectorId(connectorId), + errorDataInputs(makeVector>(getMemoryTag())), trackErrorDataInputs(makeVector(getMemoryTag())) { + + context.getRequestQueue().addSendQueue(this); //register at RequestQueue as Request emitter + + snprintf(availabilityBoolKey, sizeof(availabilityBoolKey), MO_CONFIG_EXT_PREFIX "AVAIL_CONN_%d", connectorId); + availabilityBool = declareConfiguration(availabilityBoolKey, true, MO_KEYVALUE_FN, false, false, false); + +#if MO_ENABLE_CONNECTOR_LOCK + declareConfiguration("UnlockConnectorOnEVSideDisconnect", true); //read-write +#else + declareConfiguration("UnlockConnectorOnEVSideDisconnect", false, CONFIGURATION_VOLATILE, true); //read-only because there is no connector lock +#endif //MO_ENABLE_CONNECTOR_LOCK + + connectionTimeOutInt = declareConfiguration("ConnectionTimeOut", 30); + registerConfigurationValidator("ConnectionTimeOut", VALIDATE_UNSIGNED_INT); + minimumStatusDurationInt = declareConfiguration("MinimumStatusDuration", 0); + registerConfigurationValidator("MinimumStatusDuration", VALIDATE_UNSIGNED_INT); + stopTransactionOnInvalidIdBool = declareConfiguration("StopTransactionOnInvalidId", true); + stopTransactionOnEVSideDisconnectBool = declareConfiguration("StopTransactionOnEVSideDisconnect", true); + localPreAuthorizeBool = declareConfiguration("LocalPreAuthorize", false); + localAuthorizeOfflineBool = declareConfiguration("LocalAuthorizeOffline", true); + allowOfflineTxForUnknownIdBool = MicroOcpp::declareConfiguration("AllowOfflineTxForUnknownId", false); + + //if the EVSE goes offline, can it continue to charge without sending StartTx / StopTx to the server when going online again? + silentOfflineTransactionsBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "SilentOfflineTransactions", false); + + //how long the EVSE tries the Authorize request before it enters offline mode + authorizationTimeoutInt = MicroOcpp::declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", 20); + registerConfigurationValidator(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", VALIDATE_UNSIGNED_INT); + + //FreeVend mode + freeVendActiveBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "FreeVendActive", false); + freeVendIdTagString = declareConfiguration(MO_CONFIG_EXT_PREFIX "FreeVendIdTag", ""); + + txStartOnPowerPathClosedBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "TxStartOnPowerPathClosed", false); + + transactionMessageAttemptsInt = declareConfiguration("TransactionMessageAttempts", 3); + registerConfigurationValidator("TransactionMessageAttempts", VALIDATE_UNSIGNED_INT); + transactionMessageRetryIntervalInt = declareConfiguration("TransactionMessageRetryInterval", 60); + registerConfigurationValidator("TransactionMessageRetryInterval", VALIDATE_UNSIGNED_INT); + + if (!availabilityBool) { + MO_DBG_ERR("Cannot declare availabilityBool"); + } + + char txFnamePrefix [30]; + snprintf(txFnamePrefix, sizeof(txFnamePrefix), "tx-%u-", connectorId); + size_t txFnamePrefixLen = strlen(txFnamePrefix); + + unsigned int txNrPivot = std::numeric_limits::max(); + + if (filesystem) { + filesystem->ftw_root([this, txFnamePrefix, txFnamePrefixLen, &txNrPivot] (const char *fname) { + if (!strncmp(fname, txFnamePrefix, txFnamePrefixLen)) { + unsigned int parsedTxNr = 0; + for (size_t i = txFnamePrefixLen; fname[i] >= '0' && fname[i] <= '9'; i++) { + parsedTxNr *= 10; + parsedTxNr += fname[i] - '0'; + } + + if (txNrPivot == std::numeric_limits::max()) { + txNrPivot = parsedTxNr; + txNrBegin = parsedTxNr; + txNrEnd = (parsedTxNr + 1) % MAX_TX_CNT; + return 0; + } + + if ((parsedTxNr + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT < MAX_TX_CNT / 2) { + //parsedTxNr is after pivot point + if ((parsedTxNr + 1 + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT > (txNrEnd + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT) { + txNrEnd = (parsedTxNr + 1) % MAX_TX_CNT; + } + } else if ((txNrPivot + MAX_TX_CNT - parsedTxNr) % MAX_TX_CNT < MAX_TX_CNT / 2) { + //parsedTxNr is before pivot point + if ((txNrPivot + MAX_TX_CNT - parsedTxNr) % MAX_TX_CNT > (txNrPivot + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT) { + txNrBegin = parsedTxNr; + } + } + + MO_DBG_DEBUG("found %s%u.jsn - Internal range from %u to %u (exclusive)", txFnamePrefix, parsedTxNr, txNrBegin, txNrEnd); + } + return 0; + }); + } + + MO_DBG_DEBUG("found %u transactions for connector %u. Internal range from %u to %u (exclusive)", (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT, connectorId, txNrBegin, txNrEnd); + txNrFront = txNrBegin; + + if (model.getTransactionStore()) { + unsigned int txNrLatest = (txNrEnd + MAX_TX_CNT - 1) % MAX_TX_CNT; //txNr of the most recent tx on flash + transaction = model.getTransactionStore()->getTransaction(connectorId, txNrLatest); //returns nullptr if txNrLatest does not exist on flash + } else { + MO_DBG_ERR("must initialize TxStore before Connector"); + } +} + +Connector::~Connector() { + if (availabilityBool->getKey() == availabilityBoolKey) { + availabilityBool->setKey(nullptr); + } +} + +ChargePointStatus Connector::getStatus() { + + ChargePointStatus res = ChargePointStatus_UNDEFINED; + + /* + * Handle special case: This is the Connector for the whole CP (i.e. connectorId=0) --> only states Available, Unavailable, Faulted are possible + */ + if (connectorId == 0) { + if (isFaulted()) { + res = ChargePointStatus_Faulted; + } else if (!isOperative()) { + res = ChargePointStatus_Unavailable; + } else { + res = ChargePointStatus_Available; + } + return res; + } + + if (isFaulted()) { + res = ChargePointStatus_Faulted; + } else if (!isOperative()) { + res = ChargePointStatus_Unavailable; + } else if (transaction && transaction->isRunning()) { + //Transaction is currently running + if (connectorPluggedInput && !connectorPluggedInput()) { //special case when StopTransactionOnEVSideDisconnect is false + res = ChargePointStatus_SuspendedEV; + } else if (!ocppPermitsCharge() || + (evseReadyInput && !evseReadyInput())) { + res = ChargePointStatus_SuspendedEVSE; + } else if (evReadyInput && !evReadyInput()) { + res = ChargePointStatus_SuspendedEV; + } else { + res = ChargePointStatus_Charging; + } + } + #if MO_ENABLE_RESERVATION + else if (model.getReservationService() && model.getReservationService()->getReservation(connectorId)) { + res = ChargePointStatus_Reserved; + } + #endif + else if ((!transaction) && //no transaction process occupying the connector + (!connectorPluggedInput || !connectorPluggedInput()) && //no vehicle plugged + (!occupiedInput || !occupiedInput())) { //occupied override clear + res = ChargePointStatus_Available; + } else { + /* + * Either in Preparing or Finishing state. Only way to know is from previous state + */ + const auto previous = currentStatus; + if (previous == ChargePointStatus_Finishing || + previous == ChargePointStatus_Charging || + previous == ChargePointStatus_SuspendedEV || + previous == ChargePointStatus_SuspendedEVSE || + (transaction && transaction->getStartSync().isRequested())) { //transaction process still occupying the connector + res = ChargePointStatus_Finishing; + } else { + res = ChargePointStatus_Preparing; + } + } + +#if MO_ENABLE_V201 + if (model.getVersion().major == 2) { + //OCPP 2.0.1: map v1.6 status onto v2.0.1 + if (res == ChargePointStatus_Preparing || + res == ChargePointStatus_Charging || + res == ChargePointStatus_SuspendedEV || + res == ChargePointStatus_SuspendedEVSE || + res == ChargePointStatus_Finishing) { + res = ChargePointStatus_Occupied; + } + } +#endif + + if (res == ChargePointStatus_UNDEFINED) { + MO_DBG_DEBUG("status undefined"); + return ChargePointStatus_Faulted; //internal error + } + + return res; +} + +bool Connector::ocppPermitsCharge() { + if (connectorId == 0) { + MO_DBG_WARN("not supported for connectorId == 0"); + return false; + } + + bool suspendDeAuthorizedIdTag = transaction && transaction->isIdTagDeauthorized(); //if idTag status is "DeAuthorized" and if charging should stop + + //check special case for DeAuthorized idTags: FreeVend mode + if (suspendDeAuthorizedIdTag && freeVendActiveBool && freeVendActiveBool->getBool()) { + suspendDeAuthorizedIdTag = false; + } + + // check charge permission depending on TxStartPoint + if (txStartOnPowerPathClosedBool && txStartOnPowerPathClosedBool->getBool()) { + // tx starts when the power path is closed. Advertise charging before transaction + return transaction && + transaction->isActive() && + transaction->isAuthorized() && + !suspendDeAuthorizedIdTag; + } else { + // tx must be started before the power path can be closed + return transaction && + transaction->isRunning() && + transaction->isActive() && + !suspendDeAuthorizedIdTag; + } +} + +void Connector::loop() { + + if (!trackLoopExecute) { + trackLoopExecute = true; + if (connectorPluggedInput) { + freeVendTrackPlugged = connectorPluggedInput(); + } + } + + if (transaction && ((transaction->isAborted() && MO_TX_CLEAN_ABORTED) || (transaction->isSilent() && transaction->getStopSync().isRequested()))) { + //If the transaction is aborted (invalidated before started) or is silent and has stopped. Delete all artifacts from flash + //This is an optimization. The memory management will attempt to remove those files again later + bool removed = true; + if (auto mService = model.getMeteringService()) { + mService->abortTxMeterData(connectorId); + removed &= mService->removeTxMeterData(connectorId, transaction->getTxNr()); + } + + if (removed) { + removed &= model.getTransactionStore()->remove(connectorId, transaction->getTxNr()); + } + + if (removed) { + if (txNrFront == txNrEnd) { + txNrFront = transaction->getTxNr(); + } + txNrEnd = transaction->getTxNr(); //roll back creation of last tx entry + } + + MO_DBG_DEBUG("collect aborted or silent transaction %u-%u %s", connectorId, transaction->getTxNr(), removed ? "" : "failure"); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + transaction = nullptr; + } + + if (transaction && transaction->isAborted()) { + MO_DBG_DEBUG("collect aborted transaction %u-%u", connectorId, transaction->getTxNr()); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + transaction = nullptr; + } + + if (transaction && transaction->getStopSync().isRequested()) { + MO_DBG_DEBUG("collect obsolete transaction %u-%u", connectorId, transaction->getTxNr()); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + transaction = nullptr; + } + + if (transaction) { //begin exclusively transaction-related operations + + if (connectorPluggedInput) { + if (transaction->isRunning() && transaction->isActive() && !connectorPluggedInput()) { + if (!stopTransactionOnEVSideDisconnectBool || stopTransactionOnEVSideDisconnectBool->getBool()) { + MO_DBG_DEBUG("Stop Tx due to EV disconnect"); + transaction->setStopReason("EVDisconnected"); + transaction->setInactive(); + transaction->commit(); + } + } + + if (transaction->isActive() && + !transaction->getStartSync().isRequested() && + transaction->getBeginTimestamp() > MIN_TIME && + connectionTimeOutInt && connectionTimeOutInt->getInt() > 0 && + !connectorPluggedInput() && + model.getClock().now() - transaction->getBeginTimestamp() > connectionTimeOutInt->getInt()) { + + MO_DBG_INFO("Session mngt: timeout"); + transaction->setInactive(); + transaction->commit(); + + updateTxNotification(TxNotification_ConnectionTimeout); + } + } + + if (transaction->isActive() && + transaction->isIdTagDeauthorized() && ( //transaction has been deAuthorized + !transaction->isRunning() || //if transaction hasn't started yet, always end + !stopTransactionOnInvalidIdBool || stopTransactionOnInvalidIdBool->getBool())) { //if transaction is running, behavior depends on StopTransactionOnInvalidId + + MO_DBG_DEBUG("DeAuthorize session"); + transaction->setStopReason("DeAuthorized"); + transaction->setInactive(); + transaction->commit(); + } + + /* + * Check conditions for start or stop transaction + */ + + if (!transaction->isRunning()) { + //start tx? + + if (transaction->isActive() && transaction->isAuthorized() && //tx must be authorized + (!connectorPluggedInput || connectorPluggedInput()) && //if applicable, connector must be plugged + isOperative() && //only start tx if charger is free of error conditions + (!txStartOnPowerPathClosedBool || !txStartOnPowerPathClosedBool->getBool() || !evReadyInput || evReadyInput()) && //if applicable, postpone tx start point to PowerPathClosed + (!startTxReadyInput || startTxReadyInput())) { //if defined, user Input for allowing StartTx must be true + //start Transaction + + MO_DBG_INFO("Session mngt: trigger StartTransaction"); + + auto meteringService = model.getMeteringService(); + if (transaction->getMeterStart() < 0 && meteringService) { + auto meterStart = meteringService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext_TransactionBegin); + if (meterStart && *meterStart) { + transaction->setMeterStart(meterStart->toInteger()); + } else { + MO_DBG_ERR("MeterStart undefined"); + } + } + + if (transaction->getStartTimestamp() <= MIN_TIME) { + transaction->setStartTimestamp(model.getClock().now()); + transaction->setStartBootNr(model.getBootNr()); + } + + transaction->getStartSync().setRequested(); + transaction->getStartSync().setOpNr(context.getRequestQueue().getNextOpNr()); + + if (transaction->isSilent()) { + MO_DBG_INFO("silent Transaction: omit StartTx"); + transaction->getStartSync().confirm(); + } else { + //normal transaction, record txMeterData + if (model.getMeteringService()) { + model.getMeteringService()->beginTxMeterData(transaction.get()); + } + } + + transaction->commit(); + + updateTxNotification(TxNotification_StartTx); + + //fetchFrontRequest will create the StartTransaction and pass it to the message sender + return; + } + } else { + //stop tx? + + if (!transaction->isActive() && + (!stopTxReadyInput || stopTxReadyInput())) { + //stop transaction + + MO_DBG_INFO("Session mngt: trigger StopTransaction"); + + auto meteringService = model.getMeteringService(); + if (transaction->getMeterStop() < 0 && meteringService) { + auto meterStop = meteringService->readTxEnergyMeter(transaction->getConnectorId(), ReadingContext_TransactionEnd); + if (meterStop && *meterStop) { + transaction->setMeterStop(meterStop->toInteger()); + } else { + MO_DBG_ERR("MeterStop undefined"); + } + } + + if (transaction->getStopTimestamp() <= MIN_TIME) { + transaction->setStopTimestamp(model.getClock().now()); + transaction->setStopBootNr(model.getBootNr()); + } + + transaction->getStopSync().setRequested(); + transaction->getStopSync().setOpNr(context.getRequestQueue().getNextOpNr()); + + if (transaction->isSilent()) { + MO_DBG_INFO("silent Transaction: omit StopTx"); + transaction->getStopSync().confirm(); + } else { + //normal transaction, record txMeterData + if (model.getMeteringService()) { + model.getMeteringService()->endTxMeterData(transaction.get()); + } + } + + transaction->commit(); + + updateTxNotification(TxNotification_StopTx); + + //fetchFrontRequest will create the StopTransaction and pass it to the message sender + return; + } + } + } //end transaction-related operations + + //handle FreeVend mode + if (freeVendActiveBool && freeVendActiveBool->getBool() && connectorPluggedInput) { + if (!freeVendTrackPlugged && connectorPluggedInput() && !transaction) { + const char *idTag = freeVendIdTagString ? freeVendIdTagString->getString() : ""; + if (!idTag || *idTag == '\0') { + idTag = "A0000000"; + } + MO_DBG_INFO("begin FreeVend Tx using idTag %s", idTag); + beginTransaction_authorized(idTag); + + if (!transaction) { + MO_DBG_ERR("could not begin FreeVend Tx"); + } + } + + freeVendTrackPlugged = connectorPluggedInput(); + } + + ErrorData errorData {nullptr}; + errorData.severity = 0; + int errorDataIndex = -1; + + if (model.getVersion().major == 1 && model.getClock().now() >= MIN_TIME) { + //OCPP 1.6: use StatusNotification to send error codes + + if (reportedErrorIndex >= 0) { + auto error = errorDataInputs[reportedErrorIndex].operator()(); + if (error.isError) { + errorData = error; + errorDataIndex = reportedErrorIndex; + } + } + + for (auto i = std::min(errorDataInputs.size(), trackErrorDataInputs.size()); i >= 1; i--) { + auto index = i - 1; + ErrorData error {nullptr}; + if ((int)index != errorDataIndex) { + error = errorDataInputs[index].operator()(); + } else { + error = errorData; + } + if (error.isError && !trackErrorDataInputs[index] && error.severity >= errorData.severity) { + //new error + errorData = error; + errorDataIndex = index; + } else if (error.isError && error.severity > errorData.severity) { + errorData = error; + errorDataIndex = index; + } else if (!error.isError && trackErrorDataInputs[index]) { + //reset error + trackErrorDataInputs[index] = false; + } + } + + if (errorDataIndex != reportedErrorIndex) { + if (errorDataIndex >= 0 || MO_REPORT_NOERROR) { + reportedStatus = ChargePointStatus_UNDEFINED; //trigger sending currentStatus again with code NoError + } else { + reportedErrorIndex = -1; + } + } + } //if (model.getVersion().major == 1) + + auto status = getStatus(); + + if (status != currentStatus) { + MO_DBG_DEBUG("Status changed %s -> %s %s", + currentStatus == ChargePointStatus_UNDEFINED ? "" : cstrFromOcppEveState(currentStatus), + cstrFromOcppEveState(status), + minimumStatusDurationInt->getInt() ? " (will report delayed)" : ""); + currentStatus = status; + t_statusTransition = mocpp_tick_ms(); + } + + if (reportedStatus != currentStatus && + model.getClock().now() >= MIN_TIME && + (minimumStatusDurationInt->getInt() <= 0 || //MinimumStatusDuration disabled + mocpp_tick_ms() - t_statusTransition >= ((unsigned long) minimumStatusDurationInt->getInt()) * 1000UL)) { + reportedStatus = currentStatus; + reportedErrorIndex = errorDataIndex; + if (errorDataIndex >= 0) { + trackErrorDataInputs[errorDataIndex] = true; + } + Timestamp reportedTimestamp = model.getClock().now(); + reportedTimestamp -= (mocpp_tick_ms() - t_statusTransition) / 1000UL; + + auto statusNotification = + #if MO_ENABLE_V201 + model.getVersion().major == 2 ? + makeRequest( + new Ocpp201::StatusNotification(connectorId, reportedStatus, reportedTimestamp)) : + #endif //MO_ENABLE_V201 + makeRequest( + new Ocpp16::StatusNotification(connectorId, reportedStatus, reportedTimestamp, errorData)); + + statusNotification->setTimeout(0); + context.initiateRequest(std::move(statusNotification)); + return; + } + + return; +} + +bool Connector::isFaulted() { + //for (auto i = errorDataInputs.begin(); i != errorDataInputs.end(); ++i) { + for (size_t i = 0; i < errorDataInputs.size(); i++) { + if (errorDataInputs[i].operator()().isFaulted) { + return true; + } + } + return false; +} + +const char *Connector::getErrorCode() { + if (reportedErrorIndex >= 0) { + auto error = errorDataInputs[reportedErrorIndex].operator()(); + if (error.isError && error.errorCode) { + return error.errorCode; + } + } + return nullptr; +} + +std::shared_ptr Connector::allocateTransaction() { + + std::shared_ptr tx; + + //clean possible aborted tx + unsigned int txr = txNrEnd; + unsigned int txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; + for (unsigned int i = 0; i < txSize; i++) { + txr = (txr + MAX_TX_CNT - 1) % MAX_TX_CNT; //decrement by 1 + + auto tx = model.getTransactionStore()->getTransaction(connectorId, txr); + //check if dangling silent tx, aborted tx, or corrupted entry (tx == null) + if (!tx || tx->isSilent() || (tx->isAborted() && MO_TX_CLEAN_ABORTED)) { + //yes, remove + bool removed = true; + if (auto mService = model.getMeteringService()) { + removed &= mService->removeTxMeterData(connectorId, txr); + } + if (removed) { + removed &= model.getTransactionStore()->remove(connectorId, txr); + } + if (removed) { + if (txNrFront == txNrEnd) { + txNrFront = txr; + } + txNrEnd = txr; + MO_DBG_WARN("deleted dangling silent or aborted tx for new transaction"); + } else { + MO_DBG_ERR("memory corruption"); + break; + } + } else { + //no, tx record trimmed, end + break; + } + } + + txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; //refresh after cleaning txs + + //try to create new transaction + if (txSize < MO_TXRECORD_SIZE) { + tx = model.getTransactionStore()->createTransaction(connectorId, txNrEnd); + } + + if (!tx) { + //could not create transaction - now, try to replace tx history entry + + unsigned int txl = txNrBegin; + txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; + + for (unsigned int i = 0; i < txSize; i++) { + + if (tx) { + //success, finished here + break; + } + + //no transaction allocated, delete history entry to make space + + auto txhist = model.getTransactionStore()->getTransaction(connectorId, txl); + //oldest entry, now check if it's history and can be removed or corrupted entry + if (!txhist || txhist->isCompleted() || txhist->isAborted() || (txhist->isSilent() && txhist->getStopSync().isRequested())) { + //yes, remove + bool removed = true; + if (auto mService = model.getMeteringService()) { + removed &= mService->removeTxMeterData(connectorId, txl); + } + if (removed) { + removed &= model.getTransactionStore()->remove(connectorId, txl); + } + if (removed) { + txNrBegin = (txl + 1) % MAX_TX_CNT; + if (txNrFront == txl) { + txNrFront = txNrBegin; + } + MO_DBG_DEBUG("deleted tx history entry for new transaction"); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + + tx = model.getTransactionStore()->createTransaction(connectorId, txNrEnd); + } else { + MO_DBG_ERR("memory corruption"); + break; + } + } else { + //no, end of history reached, don't delete further tx + MO_DBG_DEBUG("cannot delete more tx"); + break; + } + + txl++; + txl %= MAX_TX_CNT; + } + } + + if (!tx) { + //couldn't create normal transaction -> check if to start charging without real transaction + if (silentOfflineTransactionsBool && silentOfflineTransactionsBool->getBool()) { + //try to handle charging session without sending StartTx or StopTx to the server + tx = model.getTransactionStore()->createTransaction(connectorId, txNrEnd, true); + + if (tx) { + MO_DBG_DEBUG("created silent transaction"); + } + } + } + + if (tx) { + //clean meter data which could still be here from a rolled-back transaction + if (auto mService = model.getMeteringService()) { + if (!mService->removeTxMeterData(connectorId, tx->getTxNr())) { + MO_DBG_ERR("memory corruption"); + } + } + } + + if (tx) { + txNrEnd = (txNrEnd + 1) % MAX_TX_CNT; + MO_DBG_DEBUG("advance txNrEnd %u-%u", connectorId, txNrEnd); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + } + + return tx; +} + +std::shared_ptr Connector::beginTransaction(const char *idTag) { + + if (transaction) { + MO_DBG_WARN("tx process still running. Please call endTransaction(...) before"); + return nullptr; + } + + MO_DBG_DEBUG("Begin transaction process (%s), prepare", idTag != nullptr ? idTag : ""); + + bool localAuthFound = false; + const char *parentIdTag = nullptr; //locally stored parentIdTag + bool offlineBlockedAuth = false; //if offline authorization will be blocked by local auth list entry + + //check local OCPP whitelist + #if MO_ENABLE_LOCAL_AUTH + if (auto authService = model.getAuthorizationService()) { + auto localAuth = authService->getLocalAuthorization(idTag); + + //check authorization status + if (localAuth && localAuth->getAuthorizationStatus() != AuthorizationStatus::Accepted) { + MO_DBG_DEBUG("local auth denied (%s)", idTag); + offlineBlockedAuth = true; + localAuth = nullptr; + } + + //check expiry + if (localAuth && localAuth->getExpiryDate() && *localAuth->getExpiryDate() < model.getClock().now()) { + MO_DBG_DEBUG("idTag %s local auth entry expired", idTag); + offlineBlockedAuth = true; + localAuth = nullptr; + } + + if (localAuth) { + localAuthFound = true; + parentIdTag = localAuth->getParentIdTag(); + } + } + #endif //MO_ENABLE_LOCAL_AUTH + + int reservationId = -1; + bool offlineBlockedResv = false; //if offline authorization will be blocked by reservation + + //check if blocked by reservation + #if MO_ENABLE_RESERVATION + if (model.getReservationService()) { + + auto reservation = model.getReservationService()->getReservation( + connectorId, + idTag, + parentIdTag); + + if (reservation) { + reservationId = reservation->getReservationId(); + } + + if (reservation && !reservation->matches( + idTag, + parentIdTag)) { + //reservation blocks connector + offlineBlockedResv = true; //when offline, tx is always blocked + + //if parentIdTag is known, abort this tx immediately, otherwise wait for Authorize.conf to decide + if (parentIdTag) { + //parentIdTag known + MO_DBG_INFO("connector %u reserved - abort transaction", connectorId); + updateTxNotification(TxNotification_ReservationConflict); + return nullptr; + } else { + //parentIdTag unkown but local authorization failed in any case + MO_DBG_INFO("connector %u reserved - no local auth", connectorId); + localAuthFound = false; + } + } + } + #endif //MO_ENABLE_RESERVATION + + transaction = allocateTransaction(); + + if (!transaction) { + MO_DBG_ERR("could not allocate Tx"); + return nullptr; + } + + if (!idTag || *idTag == '\0') { + //input string is empty + transaction->setIdTag(""); + } else { + transaction->setIdTag(idTag); + } + + if (parentIdTag) { + transaction->setParentIdTag(parentIdTag); + } + + transaction->setBeginTimestamp(model.getClock().now()); + + //check for local preauthorization + if (localAuthFound && localPreAuthorizeBool && localPreAuthorizeBool->getBool()) { + MO_DBG_DEBUG("Begin transaction process (%s), preauthorized locally", idTag != nullptr ? idTag : ""); + + if (reservationId >= 0) { + transaction->setReservationId(reservationId); + } + transaction->setAuthorized(); + + updateTxNotification(TxNotification_Authorized); + } + + transaction->commit(); + + auto authorize = makeRequest(new Ocpp16::Authorize(context.getModel(), idTag)); + authorize->setTimeout(authorizationTimeoutInt && authorizationTimeoutInt->getInt() > 0 ? authorizationTimeoutInt->getInt() * 1000UL : 20UL * 1000UL); + + if (!context.getConnection().isConnected()) { + //WebSockt unconnected. Enter offline mode immediately + authorize->setTimeout(1); + } + + auto tx = transaction; + authorize->setOnReceiveConfListener([this, tx] (JsonObject response) { + JsonObject idTagInfo = response["idTagInfo"]; + + if (strcmp("Accepted", idTagInfo["status"] | "UNDEFINED")) { + //Authorization rejected, abort transaction + MO_DBG_DEBUG("Authorize rejected (%s), abort tx process", tx->getIdTag()); + tx->setIdTagDeauthorized(); + tx->commit(); + updateTxNotification(TxNotification_AuthorizationRejected); + return; + } + + #if MO_ENABLE_RESERVATION + if (model.getReservationService()) { + auto reservation = model.getReservationService()->getReservation( + connectorId, + tx->getIdTag(), + idTagInfo["parentIdTag"] | (const char*) nullptr); + if (reservation) { + //reservation found for connector + if (reservation->matches( + tx->getIdTag(), + idTagInfo["parentIdTag"] | (const char*) nullptr)) { + MO_DBG_INFO("connector %u matches reservationId %i", connectorId, reservation->getReservationId()); + tx->setReservationId(reservation->getReservationId()); + } else { + //reservation found for connector but does not match idTag or parentIdTag + MO_DBG_INFO("connector %u reserved - abort transaction", connectorId); + tx->setInactive(); + tx->commit(); + updateTxNotification(TxNotification_ReservationConflict); + return; + } + } + } + #endif //MO_ENABLE_RESERVATION + + if (idTagInfo.containsKey("parentIdTag")) { + tx->setParentIdTag(idTagInfo["parentIdTag"] | ""); + } + + MO_DBG_DEBUG("Authorized transaction process (%s)", tx->getIdTag()); + tx->setAuthorized(); + tx->commit(); + + updateTxNotification(TxNotification_Authorized); + }); + + //capture local auth and reservation check in for timeout handler + authorize->setOnTimeoutListener([this, tx, + offlineBlockedAuth, + offlineBlockedResv, + localAuthFound, + reservationId] () { + + if (offlineBlockedAuth) { + //local auth entry exists, but is expired -> avoid offline tx + MO_DBG_DEBUG("Abort transaction process (%s), timeout, expired local auth", tx->getIdTag()); + tx->setInactive(); + tx->commit(); + updateTxNotification(TxNotification_AuthorizationTimeout); + return; + } + + if (offlineBlockedResv) { + //reservation found for connector but does not match idTag or parentIdTag + MO_DBG_INFO("connector %u reserved (offline) - abort transaction", connectorId); + tx->setInactive(); + tx->commit(); + updateTxNotification(TxNotification_ReservationConflict); + return; + } + + if (localAuthFound && localAuthorizeOfflineBool && localAuthorizeOfflineBool->getBool()) { + MO_DBG_DEBUG("Offline transaction process (%s), locally authorized", tx->getIdTag()); + if (reservationId >= 0) { + tx->setReservationId(reservationId); + } + tx->setAuthorized(); + tx->commit(); + + updateTxNotification(TxNotification_Authorized); + return; + } + + if (allowOfflineTxForUnknownIdBool && allowOfflineTxForUnknownIdBool->getBool()) { + MO_DBG_DEBUG("Offline transaction process (%s), allow unknown ID", tx->getIdTag()); + if (reservationId >= 0) { + tx->setReservationId(reservationId); + } + tx->setAuthorized(); + tx->commit(); + updateTxNotification(TxNotification_Authorized); + return; + } + + MO_DBG_DEBUG("Abort transaction process (%s): timeout", tx->getIdTag()); + tx->setInactive(); + tx->commit(); + updateTxNotification(TxNotification_AuthorizationTimeout); + return; //offline tx disabled + }); + context.initiateRequest(std::move(authorize)); + + return transaction; +} + +std::shared_ptr Connector::beginTransaction_authorized(const char *idTag, const char *parentIdTag) { + + if (transaction) { + MO_DBG_WARN("tx process still running. Please call endTransaction(...) before"); + return nullptr; + } + + transaction = allocateTransaction(); + + if (!transaction) { + MO_DBG_ERR("could not allocate Tx"); + return nullptr; + } + + if (!idTag || *idTag == '\0') { + //input string is empty + transaction->setIdTag(""); + } else { + transaction->setIdTag(idTag); + } + + if (parentIdTag) { + transaction->setParentIdTag(parentIdTag); + } + + transaction->setBeginTimestamp(model.getClock().now()); + + MO_DBG_DEBUG("Begin transaction process (%s), already authorized", idTag != nullptr ? idTag : ""); + + transaction->setAuthorized(); + + #if MO_ENABLE_RESERVATION + if (model.getReservationService()) { + if (auto reservation = model.getReservationService()->getReservation(connectorId, idTag, parentIdTag)) { + if (reservation->matches(idTag, parentIdTag)) { + transaction->setReservationId(reservation->getReservationId()); + } + } + } + #endif //MO_ENABLE_RESERVATION + + transaction->commit(); + + return transaction; +} + +void Connector::endTransaction(const char *idTag, const char *reason) { + + if (!transaction || !transaction->isActive()) { + //transaction already ended / not active anymore + return; + } + + MO_DBG_DEBUG("End session started by idTag %s", + transaction->getIdTag()); + + if (idTag && *idTag != '\0') { + transaction->setStopIdTag(idTag); + } + + if (reason) { + transaction->setStopReason(reason); + } + transaction->setInactive(); + transaction->commit(); +} + +std::shared_ptr& Connector::getTransaction() { + return transaction; +} + +bool Connector::isOperative() { + if (isFaulted()) { + return false; + } + + if (!trackLoopExecute) { + return false; + } + + //check for running transaction(s) - if yes then the connector is always operative + if (connectorId == 0) { + for (unsigned int cId = 1; cId < model.getNumConnectors(); cId++) { + if (model.getConnector(cId)->getTransaction() && model.getConnector(cId)->getTransaction()->isRunning()) { + return true; + } + } + } else { + if (transaction && transaction->isRunning()) { + return true; + } + } + + #if MO_ENABLE_V201 + if (model.getVersion().major == 2 && model.getTransactionService()) { + auto txService = model.getTransactionService(); + + if (connectorId == 0) { + for (unsigned int cId = 1; cId < model.getNumConnectors(); cId++) { + if (txService->getEvse(cId)->getTransaction() && + txService->getEvse(cId)->getTransaction()->started && + !txService->getEvse(cId)->getTransaction()->stopped) { + return true; + } + } + } else { + if (txService->getEvse(connectorId)->getTransaction() && + txService->getEvse(connectorId)->getTransaction()->started && + !txService->getEvse(connectorId)->getTransaction()->stopped) { + return true; + } + } + } + #endif //MO_ENABLE_V201 + + return availabilityVolatile && availabilityBool->getBool(); +} + +void Connector::setAvailability(bool available) { + availabilityBool->setBool(available); + configuration_save(); +} + +void Connector::setAvailabilityVolatile(bool available) { + availabilityVolatile = available; +} + +void Connector::setConnectorPluggedInput(std::function connectorPlugged) { + this->connectorPluggedInput = connectorPlugged; +} + +void Connector::setEvReadyInput(std::function evRequestsEnergy) { + this->evReadyInput = evRequestsEnergy; +} + +void Connector::setEvseReadyInput(std::function connectorEnergized) { + this->evseReadyInput = connectorEnergized; +} + +void Connector::addErrorCodeInput(std::function connectorErrorCode) { + addErrorDataInput([connectorErrorCode] () -> ErrorData { + return ErrorData(connectorErrorCode()); + }); +} + +void Connector::addErrorDataInput(std::function errorDataInput) { + this->errorDataInputs.push_back(errorDataInput); + this->trackErrorDataInputs.push_back(false); +} + +#if MO_ENABLE_CONNECTOR_LOCK +void Connector::setOnUnlockConnector(std::function unlockConnector) { + this->onUnlockConnector = unlockConnector; +} + +std::function Connector::getOnUnlockConnector() { + return this->onUnlockConnector; +} +#endif //MO_ENABLE_CONNECTOR_LOCK + +void Connector::setStartTxReadyInput(std::function startTxReady) { + this->startTxReadyInput = startTxReady; +} + +void Connector::setStopTxReadyInput(std::function stopTxReady) { + this->stopTxReadyInput = stopTxReady; +} + +void Connector::setOccupiedInput(std::function occupied) { + this->occupiedInput = occupied; +} + +void Connector::setTxNotificationOutput(std::function txNotificationOutput) { + this->txNotificationOutput = txNotificationOutput; +} + +void Connector::updateTxNotification(TxNotification event) { + if (txNotificationOutput) { + txNotificationOutput(transaction.get(), event); + } +} + +unsigned int Connector::getFrontRequestOpNr() { + + /* + * Advance front transaction? + */ + + unsigned int txSize = (txNrEnd + MAX_TX_CNT - txNrFront) % MAX_TX_CNT; + + if (transactionFront && txSize == 0) { + //catch edge case where txBack has been rolled back and txFront was equal to txBack + MO_DBG_DEBUG("collect front transaction %u-%u after tx rollback", connectorId, transactionFront->getTxNr()); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + transactionFront = nullptr; + } + + for (unsigned int i = 0; i < txSize; i++) { + + if (!transactionFront) { + transactionFront = model.getTransactionStore()->getTransaction(connectorId, txNrFront); + + #if MO_DBG_LEVEL >= MO_DL_VERBOSE + if (transactionFront) + { + MO_DBG_VERBOSE("load front transaction %u-%u", connectorId, transactionFront->getTxNr()); + } + #endif + } + + if (transactionFront && (transactionFront->isAborted() || transactionFront->isCompleted() || transactionFront->isSilent())) { + //advance front + MO_DBG_DEBUG("collect front transaction %u-%u", connectorId, transactionFront->getTxNr()); + transactionFront = nullptr; + txNrFront = (txNrFront + 1) % MAX_TX_CNT; + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); + } else { + //front is accurate. Done here + break; + } + } + + if (transactionFront) { + if (transactionFront->getStartSync().isRequested() && !transactionFront->getStartSync().isConfirmed()) { + return transactionFront->getStartSync().getOpNr(); + } + + if (transactionFront->getStopSync().isRequested() && !transactionFront->getStopSync().isConfirmed()) { + return transactionFront->getStopSync().getOpNr(); + } + } + + return NoOperation; +} + +std::unique_ptr Connector::fetchFrontRequest() { + + if (transactionFront && !transactionFront->isSilent()) { + if (transactionFront->getStartSync().isRequested() && !transactionFront->getStartSync().isConfirmed()) { + //send StartTx? + + bool cancelStartTx = false; + + if (transactionFront->getStartTimestamp() < MIN_TIME && + transactionFront->getStartBootNr() != model.getBootNr()) { + //time not set, cannot be restored anymore -> invalid tx + MO_DBG_ERR("cannot recover tx from previus run"); + + cancelStartTx = true; + } + + if ((int)transactionFront->getStartSync().getAttemptNr() >= transactionMessageAttemptsInt->getInt()) { + MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard transaction"); + + cancelStartTx = true; + } + + if (cancelStartTx) { + transactionFront->setSilent(); + transactionFront->setInactive(); + transactionFront->commit(); + + //clean up possible tx records + if (auto mSerivce = model.getMeteringService()) { + mSerivce->removeTxMeterData(connectorId, transactionFront->getTxNr()); + } + //next getFrontRequestOpNr() call will collect transactionFront + return nullptr; + } + + Timestamp nextAttempt = transactionFront->getStartSync().getAttemptTime() + + transactionFront->getStartSync().getAttemptNr() * std::max(0, transactionMessageRetryIntervalInt->getInt()); + + if (nextAttempt > model.getClock().now()) { + return nullptr; + } + + transactionFront->getStartSync().advanceAttemptNr(); + transactionFront->getStartSync().setAttemptTime(model.getClock().now()); + transactionFront->commit(); + + auto startTx = makeRequest(new Ocpp16::StartTransaction(model, transactionFront)); + startTx->setOnReceiveConfListener([this] (JsonObject response) { + //fetch authorization status from StartTransaction.conf() for user notification + + const char* idTagInfoStatus = response["idTagInfo"]["status"] | "_Undefined"; + if (strcmp(idTagInfoStatus, "Accepted")) { + updateTxNotification(TxNotification_DeAuthorized); + } + }); + auto transactionFront_capture = transactionFront; + startTx->setOnAbortListener([this, transactionFront_capture] () { + //shortcut to the attemptNr check above. Relevant if other operations block the queue while this StartTx is timing out + if (transactionFront_capture && (int)transactionFront_capture->getStartSync().getAttemptNr() >= transactionMessageAttemptsInt->getInt()) { + MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard transaction"); + + transactionFront_capture->setSilent(); + transactionFront_capture->setInactive(); + transactionFront_capture->commit(); + + //clean up possible tx records + if (auto mSerivce = model.getMeteringService()) { + mSerivce->removeTxMeterData(connectorId, transactionFront_capture->getTxNr()); + } + //next getFrontRequestOpNr() call will collect transactionFront + } + }); + + return startTx; + } + + if (transactionFront->getStopSync().isRequested() && !transactionFront->getStopSync().isConfirmed()) { + //send StopTx? + + if ((int)transactionFront->getStopSync().getAttemptNr() >= transactionMessageAttemptsInt->getInt()) { + MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard transaction"); + + transactionFront->setSilent(); + + //clean up possible tx records + if (auto mSerivce = model.getMeteringService()) { + mSerivce->removeTxMeterData(connectorId, transactionFront->getTxNr()); + } + //next getFrontRequestOpNr() call will collect transactionFront + return nullptr; + } + + Timestamp nextAttempt = transactionFront->getStopSync().getAttemptTime() + + transactionFront->getStopSync().getAttemptNr() * std::max(0, transactionMessageRetryIntervalInt->getInt()); + + if (nextAttempt > model.getClock().now()) { + return nullptr; + } + + transactionFront->getStopSync().advanceAttemptNr(); + transactionFront->getStopSync().setAttemptTime(model.getClock().now()); + transactionFront->commit(); + + std::shared_ptr stopTxData; + + if (auto meteringService = model.getMeteringService()) { + stopTxData = meteringService->getStopTxMeterData(transactionFront.get()); + } + + std::unique_ptr stopTx; + + if (stopTxData) { + stopTx = makeRequest(new Ocpp16::StopTransaction(model, transactionFront, stopTxData->retrieveStopTxData())); + } else { + stopTx = makeRequest(new Ocpp16::StopTransaction(model, transactionFront)); + } + auto transactionFront_capture = transactionFront; + stopTx->setOnAbortListener([this, transactionFront_capture] () { + //shortcut to the attemptNr check above. Relevant if other operations block the queue while this StopTx is timing out + if ((int)transactionFront_capture->getStopSync().getAttemptNr() >= transactionMessageAttemptsInt->getInt()) { + MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard transaction"); + + transactionFront_capture->setSilent(); + transactionFront_capture->setInactive(); + transactionFront_capture->commit(); + + //clean up possible tx records + if (auto mSerivce = model.getMeteringService()) { + mSerivce->removeTxMeterData(connectorId, transactionFront_capture->getTxNr()); + } + //next getFrontRequestOpNr() call will collect transactionFront + } + }); + + return stopTx; + } + } + + return nullptr; +} + +bool Connector::triggerStatusNotification() { + + ErrorData errorData {nullptr}; + errorData.severity = 0; + + if (reportedErrorIndex >= 0) { + errorData = errorDataInputs[reportedErrorIndex].operator()(); + } else { + //find errorData with maximum severity + for (auto i = errorDataInputs.size(); i >= 1; i--) { + auto index = i - 1; + ErrorData error = errorDataInputs[index].operator()(); + if (error.isError && error.severity >= errorData.severity) { + errorData = error; + } + } + } + + auto statusNotification = makeRequest(new Ocpp16::StatusNotification( + connectorId, + getStatus(), + context.getModel().getClock().now(), + errorData)); + + statusNotification->setTimeout(60000); + + context.getRequestQueue().sendRequestPreBoot(std::move(statusNotification)); + + return true; +} + +unsigned int Connector::getTxNrBeginHistory() { + return txNrBegin; +} + +unsigned int Connector::getTxNrFront() { + return txNrFront; +} + +unsigned int Connector::getTxNrEnd() { + return txNrEnd; +} diff --git a/src/MicroOcpp/Model/ConnectorBase/Connector.h b/src/MicroOcpp/Model/ConnectorBase/Connector.h new file mode 100644 index 00000000..d8d61639 --- /dev/null +++ b/src/MicroOcpp/Model/ConnectorBase/Connector.h @@ -0,0 +1,166 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CONNECTOR_H +#define MO_CONNECTOR_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifndef MO_TXRECORD_SIZE +#define MO_TXRECORD_SIZE 4 //no. of tx to hold on flash storage +#endif + +#ifndef MO_REPORT_NOERROR +#define MO_REPORT_NOERROR 0 +#endif + +namespace MicroOcpp { + +class Context; +class Model; +class Operation; + +class Connector : public RequestEmitter, public MemoryManaged { +private: + Context& context; + Model& model; + std::shared_ptr filesystem; + + const unsigned int connectorId; + + std::shared_ptr transaction; + + std::shared_ptr availabilityBool; + char availabilityBoolKey [sizeof(MO_CONFIG_EXT_PREFIX "AVAIL_CONN_xxxx") + 1]; + bool availabilityVolatile = true; + + std::function connectorPluggedInput; + std::function evReadyInput; + std::function evseReadyInput; + Vector> errorDataInputs; + Vector trackErrorDataInputs; + int reportedErrorIndex = -1; //last reported error + bool isFaulted(); + const char *getErrorCode(); + + ChargePointStatus currentStatus = ChargePointStatus_UNDEFINED; + std::shared_ptr minimumStatusDurationInt; //in seconds + ChargePointStatus reportedStatus = ChargePointStatus_UNDEFINED; + unsigned long t_statusTransition = 0; + +#if MO_ENABLE_CONNECTOR_LOCK + std::function onUnlockConnector; +#endif //MO_ENABLE_CONNECTOR_LOCK + + std::function startTxReadyInput; //the StartTx request will be delayed while this Input is false + std::function stopTxReadyInput; //the StopTx request will be delayed while this Input is false + std::function occupiedInput; //instead of Available, go into Preparing / Finishing state + + std::function txNotificationOutput; + + std::shared_ptr connectionTimeOutInt; //in seconds + std::shared_ptr stopTransactionOnInvalidIdBool; + std::shared_ptr stopTransactionOnEVSideDisconnectBool; + std::shared_ptr localPreAuthorizeBool; + std::shared_ptr localAuthorizeOfflineBool; + std::shared_ptr allowOfflineTxForUnknownIdBool; + + std::shared_ptr silentOfflineTransactionsBool; + std::shared_ptr authorizationTimeoutInt; //in seconds + std::shared_ptr freeVendActiveBool; + std::shared_ptr freeVendIdTagString; + bool freeVendTrackPlugged = false; + + std::shared_ptr txStartOnPowerPathClosedBool; // this postpones the tx start point to when evReadyInput becomes true + + std::shared_ptr transactionMessageAttemptsInt; + std::shared_ptr transactionMessageRetryIntervalInt; + + bool trackLoopExecute = false; //if loop has been executed once + + unsigned int txNrBegin = 0; //oldest (historical) transaction on flash. Has no function, but is useful for error diagnosis + unsigned int txNrFront = 0; //oldest transaction which is still queued to be sent to the server + unsigned int txNrEnd = 0; //one position behind newest transaction + + std::shared_ptr transactionFront; +public: + Connector(Context& context, std::shared_ptr filesystem, unsigned int connectorId); + Connector(const Connector&) = delete; + Connector(Connector&&) = delete; + Connector& operator=(const Connector&) = delete; + + ~Connector(); + + /* + * beginTransaction begins the transaction process which eventually leads to a StartTransaction + * request in the normal case. + * + * If the transaction process begins successfully, a Transaction object is returned + * If no transaction process begins due to this call, nullptr is returned (e.g. memory allocation failed) + */ + std::shared_ptr beginTransaction(const char *idTag); + std::shared_ptr beginTransaction_authorized(const char *idTag, const char *parentIdTag = nullptr); + + /* + * End the current transaction process, if existing and not ended yet. This eventually leads to + * a StopTransaction request, if the transaction process has actually ended due to this call. It + * is safe to call this function at any time even if no transaction is running + */ + void endTransaction(const char *idTag = nullptr, const char *reason = nullptr); + + std::shared_ptr& getTransaction(); + + //create detached transaction - won't have any side-effects with the transaction handling of this lib + std::shared_ptr allocateTransaction(); + + bool isOperative(); + void setAvailability(bool available); + void setAvailabilityVolatile(bool available); //set inoperative state but keep only until reboot at most + void setConnectorPluggedInput(std::function connectorPlugged); + void setEvReadyInput(std::function evRequestsEnergy); + void setEvseReadyInput(std::function connectorEnergized); + void addErrorCodeInput(std::function connectorErrorCode); + void addErrorDataInput(std::function errorCodeInput); + + void loop(); + + ChargePointStatus getStatus(); + + bool ocppPermitsCharge(); + +#if MO_ENABLE_CONNECTOR_LOCK + void setOnUnlockConnector(std::function unlockConnector); + std::function getOnUnlockConnector(); +#endif //MO_ENABLE_CONNECTOR_LOCK + + void setStartTxReadyInput(std::function startTxReady); + void setStopTxReadyInput(std::function stopTxReady); + void setOccupiedInput(std::function occupied); + + void setTxNotificationOutput(std::function txNotificationOutput); + void updateTxNotification(TxNotification event); + + unsigned int getFrontRequestOpNr() override; + std::unique_ptr fetchFrontRequest() override; + + bool triggerStatusNotification(); + + unsigned int getTxNrBeginHistory(); //if getTxNrBeginHistory() != getTxNrFront(), then return value is the txNr of the oldest tx history entry. If equal to getTxNrFront(), then the history is empty + unsigned int getTxNrFront(); //if getTxNrEnd() != getTxNrFront(), then return value is the txNr of the oldest transaction queued to be sent to the server. If equal to getTxNrEnd(), then there is no tx to be sent to the server + unsigned int getTxNrEnd(); //upper limit for the range of valid txNrs +}; + +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp b/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp new file mode 100644 index 00000000..cfc32c82 --- /dev/null +++ b/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.cpp @@ -0,0 +1,92 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +using namespace MicroOcpp; + +ConnectorsCommon::ConnectorsCommon(Context& context, unsigned int numConn, std::shared_ptr filesystem) : + MemoryManaged("v16.ConnectorBase.ConnectorsCommon"), context(context) { + + declareConfiguration("NumberOfConnectors", numConn >= 1 ? numConn - 1 : 0, CONFIGURATION_VOLATILE, true); + + /* + * Further configuration keys which correspond to the Core profile + */ + declareConfiguration("AuthorizeRemoteTxRequests", false); + declareConfiguration("GetConfigurationMaxKeys", 30, CONFIGURATION_VOLATILE, true); + + context.getOperationRegistry().registerOperation("ChangeAvailability", [&context] () { + return new Ocpp16::ChangeAvailability(context.getModel());}); + context.getOperationRegistry().registerOperation("ChangeConfiguration", [] () { + return new Ocpp16::ChangeConfiguration();}); + context.getOperationRegistry().registerOperation("ClearCache", [filesystem] () { + return new Ocpp16::ClearCache(filesystem);}); + context.getOperationRegistry().registerOperation("DataTransfer", [] () { + return new Ocpp16::DataTransfer();}); + context.getOperationRegistry().registerOperation("GetConfiguration", [] () { + return new Ocpp16::GetConfiguration();}); + context.getOperationRegistry().registerOperation("RemoteStartTransaction", [&context] () { + return new Ocpp16::RemoteStartTransaction(context.getModel());}); + context.getOperationRegistry().registerOperation("RemoteStopTransaction", [&context] () { + return new Ocpp16::RemoteStopTransaction(context.getModel());}); + context.getOperationRegistry().registerOperation("Reset", [&context] () { + return new Ocpp16::Reset(context.getModel());}); + context.getOperationRegistry().registerOperation("TriggerMessage", [&context] () { + return new Ocpp16::TriggerMessage(context);}); + context.getOperationRegistry().registerOperation("UnlockConnector", [&context] () { + return new Ocpp16::UnlockConnector(context.getModel());}); + + /* + * Register further message handlers to support echo mode: when this library + * is connected with a WebSocket echo server, let it reply to its own requests. + * Mocking an OCPP Server on the same device makes running (unit) tests easier. + */ +#if MO_ENABLE_V201 + if (context.getVersion().major == 2) { + // OCPP 2.0.1 compliant echo messages + context.getOperationRegistry().registerOperation("Authorize", [&context] () { + return new Ocpp201::Authorize(context.getModel(), "");}); + context.getOperationRegistry().registerOperation("TransactionEvent", [&context] () { + return new Ocpp201::TransactionEvent(context.getModel(), nullptr);}); + } else +#endif //MO_ENABLE_V201 + { + // OCPP 1.6 compliant echo messages + context.getOperationRegistry().registerOperation("Authorize", [&context] () { + return new Ocpp16::Authorize(context.getModel(), "");}); + context.getOperationRegistry().registerOperation("StartTransaction", [&context] () { + return new Ocpp16::StartTransaction(context.getModel(), nullptr);}); + context.getOperationRegistry().registerOperation("StopTransaction", [&context] () { + return new Ocpp16::StopTransaction(context.getModel(), nullptr);}); + } + // OCPP 1.6 + 2.0.1 compliant echo messages + context.getOperationRegistry().registerOperation("StatusNotification", [&context] () { + return new Ocpp16::StatusNotification(-1, ChargePointStatus_UNDEFINED, Timestamp());}); +} + +void ConnectorsCommon::loop() { + //do nothing +} diff --git a/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.h b/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.h new file mode 100644 index 00000000..4bc9f78a --- /dev/null +++ b/src/MicroOcpp/Model/ConnectorBase/ConnectorsCommon.h @@ -0,0 +1,26 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CHARGECONTROLCOMMON_H +#define MO_CHARGECONTROLCOMMON_H + +#include +#include + +namespace MicroOcpp { + +class Context; + +class ConnectorsCommon : public MemoryManaged { +private: + Context& context; +public: + ConnectorsCommon(Context& context, unsigned int numConnectors, std::shared_ptr filesystem); + + void loop(); +}; + +} //end namespace MicroOcpp + +#endif diff --git a/src/MicroOcpp/Model/ConnectorBase/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/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 new file mode 100644 index 00000000..94bd9609 --- /dev/null +++ b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp @@ -0,0 +1,565 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include + +#include +#include + +//Fetch relevant data from other modules for diagnostics +#include +#include //for serializing ChargePointStatus +#include +#include +#include //for MO_ENABLE_V201 +#include //for MO_ENABLE_CONNECTOR_LOCK + +using MicroOcpp::DiagnosticsService; +using MicroOcpp::Ocpp16::DiagnosticsStatus; +using MicroOcpp::Request; + +DiagnosticsService::DiagnosticsService(Context& context) : MemoryManaged("v16.Diagnostics.DiagnosticsService"), context(context), location(makeString(getMemoryTag())), diagFileList(makeVector(getMemoryTag())) { + + context.getOperationRegistry().registerOperation("GetDiagnostics", [this] () { + return new Ocpp16::GetDiagnostics(*this);}); + + //Register message handler for TriggerMessage operation + context.getOperationRegistry().registerOperation("DiagnosticsStatusNotification", [this] () { + return new Ocpp16::DiagnosticsStatusNotification(getDiagnosticsStatus());}); +} + +DiagnosticsService::~DiagnosticsService() { + MO_FREE(diagPreamble); + MO_FREE(diagPostamble); +} + +void DiagnosticsService::loop() { + + if (ftpUpload && ftpUpload->isActive()) { + ftpUpload->loop(); + } + + if (ftpUpload) { + if (ftpUpload->isActive()) { + ftpUpload->loop(); + } else { + MO_DBG_DEBUG("Deinit FTP upload"); + ftpUpload.reset(); + } + } + + auto notification = getDiagnosticsStatusNotification(); + if (notification) { + context.initiateRequest(std::move(notification)); + } + + const auto& timestampNow = context.getModel().getClock().now(); + if (retries > 0 && timestampNow >= nextTry) { + + if (!uploadIssued) { + if (onUpload != nullptr) { + MO_DBG_DEBUG("Call onUpload"); + onUpload(location.c_str(), startTime, stopTime); + uploadIssued = true; + uploadFailure = false; + } else { + MO_DBG_ERR("onUpload must be set! (see setOnUpload). Will abort"); + retries = 0; + uploadIssued = false; + uploadFailure = true; + } + } + + if (uploadIssued) { + if (uploadStatusInput != nullptr && uploadStatusInput() == UploadStatus::Uploaded) { + //success! + MO_DBG_DEBUG("end upload routine (by status)"); + uploadIssued = false; + retries = 0; + } + + //check if maximum time elapsed or failed + const int UPLOAD_TIMEOUT = 60; + if (timestampNow - nextTry >= UPLOAD_TIMEOUT + || (uploadStatusInput != nullptr && uploadStatusInput() == UploadStatus::UploadFailed)) { + //maximum upload time elapsed or failed + + if (uploadStatusInput == nullptr) { + //No way to find out if failed. But maximum time has elapsed. Assume success + MO_DBG_DEBUG("end upload routine (by timer)"); + uploadIssued = false; + retries = 0; + } else { + //either we have UploadFailed status or (NotUploaded + timeout) here + MO_DBG_WARN("Upload timeout or failed"); + ftpUpload.reset(); + + const int TRANSITION_DELAY = 10; + if (retryInterval <= UPLOAD_TIMEOUT + TRANSITION_DELAY) { + nextTry = timestampNow; + nextTry += TRANSITION_DELAY; //wait for another 10 seconds + } else { + nextTry += retryInterval; + } + retries--; + + if (retries == 0) { + MO_DBG_DEBUG("end upload routine (no more retry)"); + uploadFailure = true; + } + } + } + } //end if (uploadIssued) + } //end try upload +} + +//timestamps before year 2021 will be treated as "undefined" +MicroOcpp::String DiagnosticsService::requestDiagnosticsUpload(const char *location, unsigned int retries, unsigned int retryInterval, Timestamp startTime, Timestamp stopTime) { + if (onUpload == nullptr) { + return makeString(getMemoryTag()); + } + + String fileName; + if (refreshFilename) { + fileName = refreshFilename().c_str(); + } else { + fileName = "diagnostics.log"; + } + + this->location.reserve(strlen(location) + 1 + fileName.size()); + + this->location = location; + + if (!this->location.empty() && this->location.back() != '/') { + this->location.append("/"); + } + this->location.append(fileName.c_str()); + + this->retries = retries; + this->retryInterval = retryInterval; + this->startTime = startTime; + + Timestamp stopMin = Timestamp(2021,0,0,0,0,0); + if (stopTime >= stopMin) { + this->stopTime = stopTime; + } else { + auto newStop = context.getModel().getClock().now(); + newStop += 3600 * 24 * 365; //set new stop time one year in future + this->stopTime = newStop; + } + +#if MO_DBG_LEVEL >= MO_DL_INFO + { + char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; + char dbuf2 [JSONDATE_LENGTH + 1] = {'\0'}; + this->startTime.toJsonString(dbuf, JSONDATE_LENGTH + 1); + this->stopTime.toJsonString(dbuf2, JSONDATE_LENGTH + 1); + + MO_DBG_INFO("Scheduled Diagnostics upload!\n" \ + " location = %s\n" \ + " retries = %i" \ + ", retryInterval = %u" \ + " startTime = %s\n" \ + " stopTime = %s", + this->location.c_str(), + this->retries, + this->retryInterval, + dbuf, + dbuf2); + } +#endif + + nextTry = context.getModel().getClock().now(); + nextTry += 5; //wait for 5s before upload + uploadIssued = false; + +#if MO_DBG_LEVEL >= MO_DL_DEBUG + { + char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; + nextTry.toJsonString(dbuf, JSONDATE_LENGTH + 1); + MO_DBG_DEBUG("Initial try at %s", dbuf); + } +#endif + + return fileName; +} + +DiagnosticsStatus DiagnosticsService::getDiagnosticsStatus() { + if (uploadFailure) { + return DiagnosticsStatus::UploadFailed; + } + + if (uploadIssued) { + if (uploadStatusInput != nullptr) { + switch (uploadStatusInput()) { + case UploadStatus::NotUploaded: + return DiagnosticsStatus::Uploading; + case UploadStatus::Uploaded: + return DiagnosticsStatus::Uploaded; + case UploadStatus::UploadFailed: + return DiagnosticsStatus::UploadFailed; + } + } + return DiagnosticsStatus::Uploading; + } + return DiagnosticsStatus::Idle; +} + +std::unique_ptr DiagnosticsService::getDiagnosticsStatusNotification() { + + if (getDiagnosticsStatus() != lastReportedStatus) { + lastReportedStatus = getDiagnosticsStatus(); + if (lastReportedStatus != DiagnosticsStatus::Idle) { + Operation *diagNotificationMsg = new Ocpp16::DiagnosticsStatusNotification(lastReportedStatus); + auto diagNotification = makeRequest(diagNotificationMsg); + return diagNotification; + } + } + + return nullptr; +} + +void DiagnosticsService::setRefreshFilename(std::function refreshFn) { + this->refreshFilename = refreshFn; +} + +void DiagnosticsService::setOnUpload(std::function onUpload) { + this->onUpload = onUpload; +} + +void DiagnosticsService::setOnUploadStatusInput(std::function uploadStatusInput) { + this->uploadStatusInput = uploadStatusInput; +} + +void DiagnosticsService::setDiagnosticsReader(std::function diagnosticsReader, std::function onClose, std::shared_ptr filesystem) { + + this->onUpload = [this, diagnosticsReader, onClose, filesystem] (const char *location, Timestamp &startTime, Timestamp &stopTime) -> bool { + + auto ftpClient = context.getFtpClient(); + if (!ftpClient) { + MO_DBG_ERR("FTP client not set"); + this->ftpUploadStatus = UploadStatus::UploadFailed; + return false; + } + + const size_t diagPreambleSize = 128; + diagPreamble = static_cast(MO_MALLOC(getMemoryTag(), diagPreambleSize)); + if (!diagPreamble) { + MO_DBG_ERR("OOM"); + this->ftpUploadStatus = UploadStatus::UploadFailed; + return false; + } + diagPreambleLen = 0; + diagPreambleTransferred = 0; + + diagReaderHasData = diagnosticsReader ? true : false; + + const size_t diagPostambleSize = 1024; + diagPostamble = static_cast(MO_MALLOC(getMemoryTag(), diagPostambleSize)); + if (!diagPostamble) { + MO_DBG_ERR("OOM"); + this->ftpUploadStatus = UploadStatus::UploadFailed; + MO_FREE(diagPreamble); + return false; + } + diagPostambleLen = 0; + diagPostambleTransferred = 0; + diagFilesBackTransferred = 0; + + auto& model = context.getModel(); + + auto cpVendor = makeString(getMemoryTag()); + auto cpModel = makeString(getMemoryTag()); + auto fwVersion = makeString(getMemoryTag()); + + if (auto bootService = model.getBootService()) { + if (auto cpCreds = bootService->getChargePointCredentials()) { + cpVendor = (*cpCreds)["chargePointVendor"] | "Vendor"; + cpModel = (*cpCreds)["chargePointModel"] | "Charger"; + fwVersion = (*cpCreds)["firmwareVersion"] | ""; + } + } + + char jsonDate [JSONDATE_LENGTH + 1]; + model.getClock().now().toJsonString(jsonDate, sizeof(jsonDate)); + + int ret; + + ret = snprintf(diagPreamble, diagPreambleSize, + "### %s %s - Hardware Diagnostics%s%s\n%s\n", + cpVendor.c_str(), + cpModel.c_str(), + fwVersion.empty() ? "" : " - v. ", fwVersion.c_str(), + jsonDate); + + if (ret < 0 || (size_t)ret >= diagPreambleSize) { + MO_DBG_ERR("snprintf: %i", ret); + this->ftpUploadStatus = UploadStatus::UploadFailed; + MO_FREE(diagPreamble); + MO_FREE(diagPostamble); + return false; + } + + diagPreambleLen += (size_t)ret; + + Connector *connector0 = model.getConnector(0); + Connector *connector1 = model.getConnector(1); + Transaction *connector1Tx = connector1 ? connector1->getTransaction().get() : nullptr; + Connector *connector2 = model.getNumConnectors() > 2 ? model.getConnector(2) : nullptr; + Transaction *connector2Tx = connector2 ? connector2->getTransaction().get() : nullptr; + + ret = 0; + + if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { + diagPostambleLen += (size_t)ret; + ret = snprintf(diagPostamble + diagPostambleLen, diagPostambleSize - diagPostambleLen, + "\n# OCPP" + "\nclient_version=%s" + "\nuptime=%lus" + "%s%s" + "%s%s" + "%s%s" + "\nws_status=%s" + "\nws_last_conn=%lus" + "\nws_last_recv=%lus" + "%s%s" + "%s%s" + "%s%s" + "%s%s" + "%s%s" + "%s%s" + "%s%s" + "%s%s" + "\nENABLE_CONNECTOR_LOCK=%i" + "\nENABLE_FILE_INDEX=%i" + "\nENABLE_V201=%i" + "\n", + MO_VERSION, + mocpp_tick_ms() / 1000UL, + connector0 ? "\nocpp_status_cId0=" : "", connector0 ? cstrFromOcppEveState(connector0->getStatus()) : "", + connector1 ? "\nocpp_status_cId1=" : "", connector1 ? cstrFromOcppEveState(connector1->getStatus()) : "", + connector2 ? "\nocpp_status_cId2=" : "", connector2 ? cstrFromOcppEveState(connector2->getStatus()) : "", + context.getConnection().isConnected() ? "connected" : "unconnected", + context.getConnection().getLastConnected() / 1000UL, + context.getConnection().getLastRecv() / 1000UL, + connector1 ? "\ncId1_hasTx=" : "", connector1 ? (connector1Tx ? "1" : "0") : "", + connector1Tx ? "\ncId1_txActive=" : "", connector1Tx ? (connector1Tx->isActive() ? "1" : "0") : "", + connector1Tx ? "\ncId1_txHasStarted=" : "", connector1Tx ? (connector1Tx->getStartSync().isRequested() ? "1" : "0") : "", + connector1Tx ? "\ncId1_txHasStopped=" : "", connector1Tx ? (connector1Tx->getStopSync().isRequested() ? "1" : "0") : "", + connector2 ? "\ncId2_hasTx=" : "", connector2 ? (connector2Tx ? "1" : "0") : "", + connector2Tx ? "\ncId2_txActive=" : "", connector2Tx ? (connector2Tx->isActive() ? "1" : "0") : "", + connector2Tx ? "\ncId2_txHasStarted=" : "", connector2Tx ? (connector2Tx->getStartSync().isRequested() ? "1" : "0") : "", + connector2Tx ? "\ncId2_txHasStopped=" : "", connector2Tx ? (connector2Tx->getStopSync().isRequested() ? "1" : "0") : "", + MO_ENABLE_CONNECTOR_LOCK, + MO_ENABLE_FILE_INDEX, + MO_ENABLE_V201 + ); + } + + if (filesystem) { + + if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { + diagPostambleLen += (size_t)ret; + ret = snprintf(diagPostamble + diagPostambleLen, diagPostambleSize - diagPostambleLen, "\n# Filesystem\n"); + } + + filesystem->ftw_root([this, &ret] (const char *fname) -> int { + if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { + diagPostambleLen += (size_t)ret; + ret = snprintf(diagPostamble + diagPostambleLen, diagPostambleSize - diagPostambleLen, "%s\n", fname); + } + diagFileList.emplace_back(fname); + return 0; + }); + + MO_DBG_DEBUG("discovered %zu files", diagFileList.size()); + } + + if (ret >= 0 && (size_t)ret + diagPostambleLen < diagPostambleSize) { + diagPostambleLen += (size_t)ret; + } else { + char errMsg [64]; + auto errLen = snprintf(errMsg, sizeof(errMsg), "\n[Diagnostics cut]\n"); + size_t ellipseStart = std::min(diagPostambleSize - (size_t)errLen - 1, diagPostambleLen); + auto ret2 = snprintf(diagPostamble + ellipseStart, diagPostambleSize - ellipseStart, "%s", errMsg); + diagPostambleLen += (size_t)ret2; + } + + this->ftpUpload = ftpClient->postFile(location, + [this, diagnosticsReader, filesystem] (unsigned char *buf, size_t size) -> size_t { + size_t written = 0; + if (written < size && diagPreambleTransferred < diagPreambleLen) { + size_t writeLen = std::min(size - written, diagPreambleLen - diagPreambleTransferred); + memcpy(buf + written, diagPreamble + diagPreambleTransferred, writeLen); + diagPreambleTransferred += writeLen; + written += writeLen; + } + + while (written < size && diagReaderHasData && diagnosticsReader) { + size_t writeLen = diagnosticsReader((char*)buf + written, size - written); + if (writeLen == 0) { + diagReaderHasData = false; + } + written += writeLen; + } + + if (written < size && diagPostambleTransferred < diagPostambleLen) { + size_t writeLen = std::min(size - written, diagPostambleLen - diagPostambleTransferred); + memcpy(buf + written, diagPostamble + diagPostambleTransferred, writeLen); + diagPostambleTransferred += writeLen; + written += writeLen; + } + + while (written < size && !diagFileList.empty() && filesystem) { + + char fpath [MO_MAX_PATH_SIZE]; + auto ret = snprintf(fpath, sizeof(fpath), "%s%s", MO_FILENAME_PREFIX, diagFileList.back().c_str()); + if (ret < 0 || (size_t)ret >= sizeof(fpath)) { + MO_DBG_ERR("fn error: %i", ret); + diagFileList.pop_back(); + // next file starts from offset 0 + diagFilesBackTransferred = 0; + continue; + } + + if (auto file = filesystem->open(fpath, "r")) { + + if (diagFilesBackTransferred == 0) { + char fileHeading [30 + MO_MAX_PATH_SIZE]; + auto writeLen = snprintf(fileHeading, sizeof(fileHeading), "\n\n# File %s:\n", diagFileList.back().c_str()); + if (writeLen < 0 || (size_t)writeLen >= sizeof(fileHeading)) { + MO_DBG_ERR("fn error: %i", ret); + diagFileList.pop_back(); + diagFilesBackTransferred = 0; + continue; + } + if (writeLen + written > size || //heading doesn't fit anymore, return with a bit unused buffer space and print heading the next time + writeLen + written == size) { //filling the buffer up exactly would mean that no file payload is written and this head gets printed again + + MO_DBG_DEBUG("upload diag chunk (%zuB)", written); + return written; + } + + memcpy(buf + written, fileHeading, (size_t)writeLen); + written += (size_t)writeLen; + } + + file->seek(diagFilesBackTransferred); + size_t writeLen = file->read((char*)buf + written, size - written); + // advance per-file offset + diagFilesBackTransferred += writeLen; + if (writeLen < size - written) { + // EOF for this file; move to next and reset offset + MO_DBG_DEBUG("upload diag chunk %zu (done)", diagFilesBackTransferred); + diagFileList.pop_back(); + diagFilesBackTransferred = 0; + } + written += writeLen; + } else { + MO_DBG_ERR("could not open file: %s", fpath); + diagFileList.pop_back(); + diagFilesBackTransferred = 0; + } + } + + MO_DBG_DEBUG("upload diag chunk (%zuB)", written); + return written; + }, + [this, onClose] (MO_FtpCloseReason reason) -> void { + if (reason == MO_FtpCloseReason_Success) { + MO_DBG_INFO("FTP upload success"); + this->ftpUploadStatus = UploadStatus::Uploaded; + } else { + MO_DBG_INFO("FTP upload failure (%i)", reason); + this->ftpUploadStatus = UploadStatus::UploadFailed; + } + + MO_FREE(diagPreamble); + MO_FREE(diagPostamble); + diagFileList.clear(); + diagFilesBackTransferred = 0; //reset offset for future uploads + + if (onClose) { + onClose(); + } + }); + + if (this->ftpUpload) { + this->ftpUploadStatus = UploadStatus::NotUploaded; + return true; + } else { + this->ftpUploadStatus = UploadStatus::UploadFailed; + return false; + } + }; + + this->uploadStatusInput = [this] () { + return this->ftpUploadStatus; + }; +} + +void DiagnosticsService::setFtpServerCert(const char *cert) { + this->ftpServerCert = cert; +} + +#if !defined(MO_CUSTOM_DIAGNOSTICS) + +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS + +#include "esp_heap_caps.h" +#include + +bool g_diagsSent = false; + +std::unique_ptr MicroOcpp::makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem) { + std::unique_ptr diagService = std::unique_ptr(new DiagnosticsService(context)); + + diagService->setDiagnosticsReader( + [] (char *buf, size_t size) -> size_t { + if (!g_diagsSent) { + g_diagsSent = true; + int ret = snprintf(buf, size, + "\n# Memory\n" + "freeHeap=%zu\n" + "minHeap=%zu\n" + "maxAllocHeap=%zu\n" + "LittleFS_used=%zu\n" + "LittleFS_total=%zu\n", + heap_caps_get_free_size(MALLOC_CAP_DEFAULT), + heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT), + heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT), + LittleFS.usedBytes(), + LittleFS.totalBytes() + ); + if (ret < 0 || (size_t)ret >= size) { + MO_DBG_ERR("snprintf: %i", ret); + return 0; + } + return (size_t)ret; + } + return 0; + }, [] () { + g_diagsSent = false; + }, + filesystem); + + return diagService; +} + +#elif MO_ENABLE_MBEDTLS + +std::unique_ptr MicroOcpp::makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem) { + std::unique_ptr diagService = std::unique_ptr(new DiagnosticsService(context)); + + diagService->setDiagnosticsReader(nullptr, nullptr, filesystem); //report the built-in MO defaults + + return diagService; +} + +#endif //MO_PLATFORM +#endif //!defined(MO_CUSTOM_DIAGNOSTICS) diff --git a/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.h b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.h new file mode 100644 index 00000000..ed3ae3c0 --- /dev/null +++ b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.h @@ -0,0 +1,123 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef DIAGNOSTICSSERVICE_H +#define DIAGNOSTICSSERVICE_H + +#include +#include +#include +#include +#include +#include + +namespace MicroOcpp { + +enum class UploadStatus { + NotUploaded, + Uploaded, + UploadFailed +}; + +class Context; +class Request; +class FilesystemAdapter; + +class DiagnosticsService : public MemoryManaged { +private: + Context& context; + + String location; + unsigned int retries = 0; + unsigned int retryInterval = 0; + Timestamp startTime; + Timestamp stopTime; + + Timestamp nextTry; + + std::function refreshFilename; + std::function onUpload; + std::function uploadStatusInput; + bool uploadIssued = false; + bool uploadFailure = false; + + std::unique_ptr ftpUpload; + UploadStatus ftpUploadStatus = UploadStatus::NotUploaded; + const char *ftpServerCert = nullptr; + char *diagPreamble = nullptr; + size_t diagPreambleLen = 0; + size_t diagPreambleTransferred = 0; + bool diagReaderHasData = false; + char *diagPostamble = nullptr; + size_t diagPostambleLen = 0; + size_t diagPostambleTransferred = 0; + Vector diagFileList; + size_t diagFilesBackTransferred = 0; + + std::unique_ptr getDiagnosticsStatusNotification(); + + Ocpp16::DiagnosticsStatus lastReportedStatus = Ocpp16::DiagnosticsStatus::Idle; + +public: + DiagnosticsService(Context& context); + ~DiagnosticsService(); + + void loop(); + + //timestamps before year 2021 will be treated as "undefined" + //returns empty std::string if onUpload is missing or upload cannot be scheduled for another reason + //returns fileName of diagnostics file to be uploaded if upload has been scheduled + String requestDiagnosticsUpload(const char *location, unsigned int retries = 1, unsigned int retryInterval = 0, Timestamp startTime = Timestamp(), Timestamp stopTime = Timestamp()); + + Ocpp16::DiagnosticsStatus getDiagnosticsStatus(); + + void setRefreshFilename(std::function refreshFn); //refresh a new filename which will be used for the subsequent upload tries + + /* + * Sets the diagnostics data reader. When the server sends a GetDiagnostics operation, then MO will open an FTP + * connection to the FTP server and upload a diagnostics file. MO automatically creates a small report about + * the OCPP-related status data + it uploads the contents of the OCPP directory. In addition to the automatic + * report, MO also sends all data provided by the custom diagnosticsReader. Use the diagnosticsReader to add + * all data which could be helpful for troubleshooting, i.e. + * - internal status variables, or state machine states + * - error trip counters + * - current sensor readings and all GPIO values + * - Heap statistics, flash memory statistics + * - and more. The more the better + * + * MO calls the diagnosticsReader output buffer `buf` and the bufsize `size`. Write at most `size` bytes and + * return the number of bytes actually written (without terminating zero-byte). It's not necessary to append + * a terminating zero, MO will ignore any data after the string. To end the reading process, return 0. + * + * Note that this function only works if MO_ENABLE_MBEDTLS=1, or MO has been configured with a custom FTP client + */ + void setDiagnosticsReader(std::function diagnosticsReader, std::function onClose, std::shared_ptr filesystem); + + void setFtpServerCert(const char *cert); //zero-copy mode, i.e. cert must outlive MO + + void setOnUpload(std::function onUpload); + + void setOnUploadStatusInput(std::function uploadStatusInput); +}; + +} //end namespace MicroOcpp + +#if !defined(MO_CUSTOM_DIAGNOSTICS) + +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS + +namespace MicroOcpp { +std::unique_ptr makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem); +} + +#elif MO_ENABLE_MBEDTLS + +namespace MicroOcpp { +std::unique_ptr makeDefaultDiagnosticsService(Context& context, std::shared_ptr filesystem); +} + +#endif //MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS +#endif //!defined(MO_CUSTOM_DIAGNOSTICS) + +#endif diff --git a/src/MicroOcpp/Model/Diagnostics/DiagnosticsStatus.h b/src/MicroOcpp/Model/Diagnostics/DiagnosticsStatus.h new file mode 100644 index 00000000..632708a4 --- /dev/null +++ b/src/MicroOcpp/Model/Diagnostics/DiagnosticsStatus.h @@ -0,0 +1,20 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_DIAGNOSTICS_STATUS +#define MO_DIAGNOSTICS_STATUS + +namespace MicroOcpp { +namespace Ocpp16 { + +enum class DiagnosticsStatus { + Idle, + Uploaded, + UploadFailed, + Uploading +}; + +} +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp b/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp new file mode 100644 index 00000000..7eb006e1 --- /dev/null +++ b/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.cpp @@ -0,0 +1,502 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +//debug option: update immediately and don't wait for the retreive date +#ifndef MO_IGNORE_FW_RETR_DATE +#define MO_IGNORE_FW_RETR_DATE 0 +#endif + +using MicroOcpp::FirmwareService; +using MicroOcpp::Ocpp16::FirmwareStatus; +using MicroOcpp::Request; + +FirmwareService::FirmwareService(Context& context) : MemoryManaged("v16.Firmware.FirmwareService"), context(context), buildNumber(makeString(getMemoryTag())), location(makeString(getMemoryTag())) { + + context.getOperationRegistry().registerOperation("UpdateFirmware", [this] () { + return new Ocpp16::UpdateFirmware(*this);}); + + //Register message handler for TriggerMessage operation + context.getOperationRegistry().registerOperation("FirmwareStatusNotification", [this] () { + return new Ocpp16::FirmwareStatusNotification(getFirmwareStatus());}); +} + +void FirmwareService::setBuildNumber(const char *buildNumber) { + if (buildNumber == nullptr) + return; + this->buildNumber = buildNumber; + previousBuildNumberString = declareConfiguration("BUILD_NUMBER", this->buildNumber.c_str(), MO_KEYVALUE_FN, false, false, false); + checkedSuccessfulFwUpdate = false; //--> CS will be notified +} + +void FirmwareService::loop() { + + if (ftpDownload && ftpDownload->isActive()) { + ftpDownload->loop(); + } + + if (ftpDownload) { + if (ftpDownload->isActive()) { + ftpDownload->loop(); + } else { + MO_DBG_DEBUG("Deinit FTP download"); + ftpDownload.reset(); + } + } + + auto notification = getFirmwareStatusNotification(); + if (notification) { + context.initiateRequest(std::move(notification)); + } + + if (mocpp_tick_ms() - timestampTransition < delayTransition) { + return; + } + + auto& timestampNow = context.getModel().getClock().now(); + if (retries > 0 && timestampNow >= retreiveDate) { + + if (stage == UpdateStage::Idle) { + MO_DBG_INFO("Start update"); + + if (context.getModel().getNumConnectors() > 0) { + auto cp = context.getModel().getConnector(0); + cp->setAvailabilityVolatile(false); + } + if (onDownload == nullptr) { + stage = UpdateStage::AfterDownload; + } else { + downloadIssued = true; + stage = UpdateStage::AwaitDownload; + timestampTransition = mocpp_tick_ms(); + delayTransition = 2000; //delay between state "Downloading" and actually starting the download + return; + } + } + + if (stage == UpdateStage::AwaitDownload) { + MO_DBG_INFO("Start download"); + stage = UpdateStage::Downloading; + if (onDownload != nullptr) { + onDownload(location.c_str()); + timestampTransition = mocpp_tick_ms(); + delayTransition = downloadStatusInput ? 1000 : 30000; //give the download at least 30s + return; + } + } + + if (stage == UpdateStage::Downloading) { + + if (downloadStatusInput) { + //check if client reports download to be finished + + if (downloadStatusInput() == DownloadStatus::Downloaded) { + //passed download stage + stage = UpdateStage::AfterDownload; + } else if (downloadStatusInput() == DownloadStatus::DownloadFailed) { + MO_DBG_INFO("Download timeout or failed"); + retreiveDate = timestampNow; + retreiveDate += retryInterval; + retries--; + resetStage(); + + timestampTransition = mocpp_tick_ms(); + delayTransition = 10000; + } + return; + } else { + //if client doesn't report download state, assume download to be finished (at least 30s download time have passed until here) + stage = UpdateStage::AfterDownload; + } + } + + if (stage == UpdateStage::AfterDownload) { + bool ongoingTx = false; + for (unsigned int cId = 0; cId < context.getModel().getNumConnectors(); cId++) { + auto connector = context.getModel().getConnector(cId); + if (connector && connector->getTransaction() && connector->getTransaction()->isRunning()) { + ongoingTx = true; + break; + } + } + + if (!ongoingTx) { + if (onInstall == nullptr) { + stage = UpdateStage::Installing; + } else { + stage = UpdateStage::AwaitInstallation; + } + timestampTransition = mocpp_tick_ms(); + delayTransition = 2000; + installationIssued = true; + } + + return; + } + + if (stage == UpdateStage::AwaitInstallation) { + MO_DBG_INFO("Installing"); + stage = UpdateStage::Installing; + + if (onInstall) { + onInstall(location.c_str()); //may restart the device on success + + timestampTransition = mocpp_tick_ms(); + delayTransition = installationStatusInput ? 1000 : 120 * 1000; + } + return; + } + + if (stage == UpdateStage::Installing) { + + if (installationStatusInput) { + if (installationStatusInput() == InstallationStatus::Installed) { + MO_DBG_INFO("FW update finished"); + //Charger may reboot during onInstall. If it doesn't, server will send Reset request + resetStage(); + retries = 0; //End of update routine + stage = UpdateStage::Installed; + location.clear(); + } else if (installationStatusInput() == InstallationStatus::InstallationFailed) { + MO_DBG_INFO("Installation timeout or failed! Retry"); + retreiveDate = timestampNow; + retreiveDate += retryInterval; + retries--; + resetStage(); + + timestampTransition = mocpp_tick_ms(); + delayTransition = 10000; + } + return; + } else { + MO_DBG_INFO("FW update finished"); + //Charger may reboot during onInstall. If it doesn't, server will send Reset request + resetStage(); + stage = UpdateStage::Installed; + retries = 0; //End of update routine + location.clear(); + return; + } + } + + //should never reach this code + MO_DBG_ERR("Firmware update failed"); + retries = 0; + resetStage(); + stage = UpdateStage::InternalError; + location.clear(); + } +} + +void FirmwareService::scheduleFirmwareUpdate(const char *location, Timestamp retreiveDate, unsigned int retries, unsigned int retryInterval) { + + if (!onDownload && !onInstall) { + MO_DBG_ERR("FW service not configured"); + stage = UpdateStage::InternalError; //will send "InstallationFailed" and not proceed with update + return; + } + + this->location = location; + this->retreiveDate = retreiveDate; + this->retries = retries; + this->retryInterval = retryInterval; + + if (MO_IGNORE_FW_RETR_DATE) { + MO_DBG_DEBUG("ignore FW update retreive date"); + this->retreiveDate = context.getModel().getClock().now(); + } + + char dbuf [JSONDATE_LENGTH + 1] = {'\0'}; + this->retreiveDate.toJsonString(dbuf, JSONDATE_LENGTH + 1); + + MO_DBG_INFO("Scheduled FW update!\n" \ + " location = %s\n" \ + " retrieveDate = %s\n" \ + " retries = %u" \ + ", retryInterval = %u", + this->location.c_str(), + dbuf, + this->retries, + this->retryInterval); + + timestampTransition = mocpp_tick_ms(); + delayTransition = 1000; + + resetStage(); +} + +FirmwareStatus FirmwareService::getFirmwareStatus() { + + if (stage == UpdateStage::Installed) { + return FirmwareStatus::Installed; + } else if (stage == UpdateStage::InternalError) { + return FirmwareStatus::InstallationFailed; + } + + if (installationIssued) { + if (installationStatusInput != nullptr) { + if (installationStatusInput() == InstallationStatus::Installed) { + return FirmwareStatus::Installed; + } else if (installationStatusInput() == InstallationStatus::InstallationFailed) { + return FirmwareStatus::InstallationFailed; + } + } + if (onInstall != nullptr) + return FirmwareStatus::Installing; + } + + if (downloadIssued) { + if (downloadStatusInput != nullptr) { + if (downloadStatusInput() == DownloadStatus::Downloaded) { + return FirmwareStatus::Downloaded; + } else if (downloadStatusInput() == DownloadStatus::DownloadFailed) { + return FirmwareStatus::DownloadFailed; + } + } + if (onDownload != nullptr) + return FirmwareStatus::Downloading; + } + + return FirmwareStatus::Idle; +} + +std::unique_ptr FirmwareService::getFirmwareStatusNotification() { + /* + * Check if FW has been updated previously, but only once + */ + if (!checkedSuccessfulFwUpdate && !buildNumber.empty() && previousBuildNumberString != nullptr) { + checkedSuccessfulFwUpdate = true; + + MO_DBG_DEBUG("Previous build number: %s, new build number: %s", previousBuildNumberString->getString(), buildNumber.c_str()); + + if (buildNumber.compare(previousBuildNumberString->getString())) { + //new FW + previousBuildNumberString->setString(buildNumber.c_str()); + configuration_save(); + + buildNumber.clear(); + + lastReportedStatus = FirmwareStatus::Installed; + auto fwNotificationMsg = new Ocpp16::FirmwareStatusNotification(lastReportedStatus); + auto fwNotification = makeRequest(fwNotificationMsg); + return fwNotification; + } + } + + if (getFirmwareStatus() != lastReportedStatus) { + lastReportedStatus = getFirmwareStatus(); + if (lastReportedStatus != FirmwareStatus::Idle) { + auto fwNotificationMsg = new Ocpp16::FirmwareStatusNotification(lastReportedStatus); + auto fwNotification = makeRequest(fwNotificationMsg); + return fwNotification; + } + } + + return nullptr; +} + +void FirmwareService::setOnDownload(std::function onDownload) { + this->onDownload = onDownload; +} + +void FirmwareService::setDownloadStatusInput(std::function downloadStatusInput) { + this->downloadStatusInput = downloadStatusInput; +} + +void FirmwareService::setOnInstall(std::function onInstall) { + this->onInstall = onInstall; +} + +void FirmwareService::setInstallationStatusInput(std::function installationStatusInput) { + this->installationStatusInput = installationStatusInput; +} + +void FirmwareService::resetStage() { + stage = UpdateStage::Idle; + downloadIssued = false; + installationIssued = false; +} + +void FirmwareService::setDownloadFileWriter(std::function firmwareWriter, std::function onClose) { + + this->onDownload = [this, firmwareWriter, onClose] (const char *location) -> bool { + + auto ftpClient = context.getFtpClient(); + if (!ftpClient) { + MO_DBG_ERR("FTP client not set"); + this->ftpDownloadStatus = DownloadStatus::DownloadFailed; + return false; + } + + this->ftpDownload = ftpClient->getFile(location, firmwareWriter, + [this, onClose] (MO_FtpCloseReason reason) -> void { + if (reason == MO_FtpCloseReason_Success) { + MO_DBG_INFO("FTP download success"); + this->ftpDownloadStatus = DownloadStatus::Downloaded; + } else { + MO_DBG_INFO("FTP download failure (%i)", reason); + this->ftpDownloadStatus = DownloadStatus::DownloadFailed; + } + + onClose(reason); + }); + + if (this->ftpDownload) { + this->ftpDownloadStatus = DownloadStatus::NotDownloaded; + return true; + } else { + this->ftpDownloadStatus = DownloadStatus::DownloadFailed; + return false; + } + }; + + this->downloadStatusInput = [this] () { + return this->ftpDownloadStatus; + }; +} + +void FirmwareService::setFtpServerCert(const char *cert) { + this->ftpServerCert = cert; +} + +#if !defined(MO_CUSTOM_UPDATER) +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS + +#include + +std::unique_ptr MicroOcpp::makeDefaultFirmwareService(Context& context) { + std::unique_ptr fwService = std::unique_ptr(new FirmwareService(context)); + auto ftServicePtr = fwService.get(); + + fwService->setDownloadFileWriter( + [ftServicePtr] (const unsigned char *data, size_t size) -> size_t { + if (!Update.isRunning()) { + MO_DBG_DEBUG("start writing FW"); + MO_DBG_WARN("Built-in updater for ESP32 is only intended for demonstration purposes"); + ftServicePtr->setInstallationStatusInput([](){return InstallationStatus::NotInstalled;}); + + auto ret = Update.begin(); + if (!ret) { + MO_DBG_ERR("cannot start update: %i", ret); + return 0; + } + } + + size_t written = Update.write((uint8_t*) data, size); + + #if MO_DBG_LEVEL >= MO_DL_INFO + { + size_t progress = Update.progress(); + + bool printProgress = false; + + if (progress <= 10000) { + size_t p1k = progress / 1000; + printProgress = progress < p1k * 1000 + written && progress >= p1k * 1000; + } else if (progress <= 100000) { + size_t p10k = progress / 10000; + printProgress = progress < p10k * 10000 + written && progress >= p10k * 10000; + } else { + size_t p100k = progress / 100000; + printProgress = progress < p100k * 100000 + written && progress >= p100k * 100000; + } + + if (printProgress) { + MO_DBG_INFO("update progress: %zu kB", progress / 1000); + } + } + #endif //MO_DBG_LEVEL >= MO_DL_DEBUG + + return written; + }, [] (MO_FtpCloseReason reason) { + if (reason != MO_FtpCloseReason_Success) { + Update.abort(); + } + }); + + fwService->setOnInstall([ftServicePtr] (const char *location) { + + if (Update.isRunning() && Update.end(true)) { + MO_DBG_DEBUG("update success"); + ftServicePtr->setInstallationStatusInput([](){return InstallationStatus::Installed;}); + + ESP.restart(); + } else { + MO_DBG_ERR("update failed"); + ftServicePtr->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); + } + + return true; + }); + + fwService->setInstallationStatusInput([] () { + return InstallationStatus::NotInstalled; + }); + + return fwService; +} + +#elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP8266) + +#include + +std::unique_ptr MicroOcpp::makeDefaultFirmwareService(Context& context) { + std::unique_ptr fwService = std::unique_ptr(new FirmwareService(context)); + auto fwServicePtr = fwService.get(); + + fwService->setOnInstall([fwServicePtr] (const char *location) { + + MO_DBG_WARN("Built-in updater for ESP8266 is only intended for demonstration purposes. HTTP support only"); + + WiFiClient client; + //WiFiClientSecure client; + //client.setCACert(rootCACertificate); + client.setTimeout(60); //in seconds + + //ESPhttpUpdate.setLedPin(downloadStatusLedPin); + + HTTPUpdateResult ret = ESPhttpUpdate.update(client, location); + + switch (ret) { + case HTTP_UPDATE_FAILED: + fwServicePtr->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); + MO_DBG_WARN("HTTP_UPDATE_FAILED Error (%d): %s\n", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); + break; + case HTTP_UPDATE_NO_UPDATES: + fwServicePtr->setInstallationStatusInput([](){return InstallationStatus::InstallationFailed;}); + MO_DBG_WARN("HTTP_UPDATE_NO_UPDATES"); + break; + case HTTP_UPDATE_OK: + fwServicePtr->setInstallationStatusInput([](){return InstallationStatus::Installed;}); + MO_DBG_INFO("HTTP_UPDATE_OK"); + ESP.restart(); + break; + } + + return true; + }); + + fwService->setInstallationStatusInput([] () { + return InstallationStatus::NotInstalled; + }); + + return fwService; +} + +#endif //MO_PLATFORM +#endif //!defined(MO_CUSTOM_UPDATER) diff --git a/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.h b/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.h new file mode 100644 index 00000000..5f4671f2 --- /dev/null +++ b/src/MicroOcpp/Model/FirmwareManagement/FirmwareService.h @@ -0,0 +1,135 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef FIRMWARESERVICE_H +#define FIRMWARESERVICE_H + +#include +#include + +#include +#include +#include +#include +#include + +namespace MicroOcpp { + +enum class DownloadStatus { + NotDownloaded, // == before download or during download + Downloaded, + DownloadFailed +}; + +enum class InstallationStatus { + NotInstalled, // == before installation or during installation + Installed, + InstallationFailed +}; + +class Context; +class Request; + +class FirmwareService : public MemoryManaged { +private: + Context& context; + + std::shared_ptr previousBuildNumberString; + String buildNumber; + + std::function downloadStatusInput; + bool downloadIssued = false; + + std::unique_ptr ftpDownload; + DownloadStatus ftpDownloadStatus = DownloadStatus::NotDownloaded; + const char *ftpServerCert = nullptr; + + std::function installationStatusInput; + bool installationIssued = false; + + Ocpp16::FirmwareStatus lastReportedStatus = Ocpp16::FirmwareStatus::Idle; + bool checkedSuccessfulFwUpdate = false; + + String location; + Timestamp retreiveDate; + unsigned int retries = 0; + unsigned int retryInterval = 0; + + std::function onDownload; + std::function onInstall; + + unsigned long delayTransition = 0; + unsigned long timestampTransition = 0; + + enum class UpdateStage { + Idle, + AwaitDownload, + Downloading, + AfterDownload, + AwaitInstallation, + Installing, + Installed, + InternalError + } stage = UpdateStage::Idle; + + void resetStage(); + + std::unique_ptr getFirmwareStatusNotification(); + +public: + FirmwareService(Context& context); + + void setBuildNumber(const char *buildNumber); + + void loop(); + + void scheduleFirmwareUpdate(const char *location, Timestamp retreiveDate, unsigned int retries = 1, unsigned int retryInterval = 0); + + Ocpp16::FirmwareStatus getFirmwareStatus(); + + /* + * Sets the firmware writer. During the UpdateFirmware process, MO will use an FTP client to download the firmware and forward + * the binary data to `firmwareWriter`. The binary data comes in chunks. MO executes `firmwareWriter` with `buf` containing the + * next chunk of FW data and `size` being the chunk size. `firmwareWriter` must return the number of bytes written, whereas + * the result can be between 1 and `size`, and 0 aborts the download. MO executes `onClose` with the reason why the connection + * has been closed. If the download hasn't been successful, MO will abort the update routine in any case. + * + * Note that this function only works if MO_ENABLE_MBEDTLS=1, or MO has been configured with a custom FTP client + */ + void setDownloadFileWriter(std::function firmwareWriter, std::function onClose); + + void setFtpServerCert(const char *cert); //zero-copy mode, i.e. cert must outlive MO + + /* + * Manual alternative for FTP download handler `setDownloadFileWriter` + */ + void setOnDownload(std::function onDownload); + + void setDownloadStatusInput(std::function downloadStatusInput); + + void setOnInstall(std::function onInstall); + + void setInstallationStatusInput(std::function installationStatusInput); +}; + +} //endif namespace MicroOcpp + +#if !defined(MO_CUSTOM_UPDATER) + +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP32) && MO_ENABLE_MBEDTLS + +namespace MicroOcpp { +std::unique_ptr makeDefaultFirmwareService(Context& context); +} + +#elif MO_PLATFORM == MO_PLATFORM_ARDUINO && defined(ESP8266) + +namespace MicroOcpp { +std::unique_ptr makeDefaultFirmwareService(Context& context); +} + +#endif //MO_PLATFORM +#endif //!defined(MO_CUSTOM_UPDATER) + +#endif diff --git a/src/ArduinoOcpp/Tasks/FirmwareManagement/FirmwareStatus.h b/src/MicroOcpp/Model/FirmwareManagement/FirmwareStatus.h similarity index 53% rename from src/ArduinoOcpp/Tasks/FirmwareManagement/FirmwareStatus.h rename to src/MicroOcpp/Model/FirmwareManagement/FirmwareStatus.h index e6157654..3c8c7c2c 100644 --- a/src/ArduinoOcpp/Tasks/FirmwareManagement/FirmwareStatus.h +++ b/src/MicroOcpp/Model/FirmwareManagement/FirmwareStatus.h @@ -1,11 +1,11 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#ifndef FIRMWARE_STATUS -#define FIRMWARE_STATUS +#ifndef MO_FIRMWARE_STATUS +#define MO_FIRMWARE_STATUS -namespace ArduinoOcpp { +namespace MicroOcpp { namespace Ocpp16 { enum class FirmwareStatus { @@ -19,5 +19,5 @@ enum class FirmwareStatus { }; } -} //end namespace ArduinoOcpp +} //end namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp b/src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp new file mode 100644 index 00000000..4ebab502 --- /dev/null +++ b/src/MicroOcpp/Model/Heartbeat/HeartbeatService.cpp @@ -0,0 +1,37 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include + +using namespace MicroOcpp; + +HeartbeatService::HeartbeatService(Context& context) : MemoryManaged("v16.Heartbeat.HeartbeatService"), context(context) { + heartbeatIntervalInt = declareConfiguration("HeartbeatInterval", 86400); + registerConfigurationValidator("HeartbeatInterval", VALIDATE_UNSIGNED_INT); + lastHeartbeat = mocpp_tick_ms(); + + //Register message handler for TriggerMessage operation + context.getOperationRegistry().registerOperation("Heartbeat", [&context] () { + return new Ocpp16::Heartbeat(context.getModel());}); +} + +void HeartbeatService::loop() { + unsigned long hbInterval = heartbeatIntervalInt->getInt(); + hbInterval *= 1000UL; //conversion s -> ms + unsigned long now = mocpp_tick_ms(); + + if (now - lastHeartbeat >= hbInterval) { + lastHeartbeat = now; + + auto heartbeat = makeRequest(new Ocpp16::Heartbeat(context.getModel())); + // Heartbeats can not deviate more than 4s from the configured interval + heartbeat->setTimeout(std::min(4000UL, hbInterval)); + context.initiateRequest(std::move(heartbeat)); + } +} diff --git a/src/MicroOcpp/Model/Heartbeat/HeartbeatService.h b/src/MicroOcpp/Model/Heartbeat/HeartbeatService.h new file mode 100644 index 00000000..ea632cc8 --- /dev/null +++ b/src/MicroOcpp/Model/Heartbeat/HeartbeatService.h @@ -0,0 +1,32 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_HEARTBEATSERVICE_H +#define MO_HEARTBEATSERVICE_H + +#include + +#include +#include + +namespace MicroOcpp { + +class Context; + +class HeartbeatService : public MemoryManaged { +private: + Context& context; + + unsigned long lastHeartbeat; + std::shared_ptr heartbeatIntervalInt; + +public: + HeartbeatService(Context& context); + + void loop(); +}; + +} + +#endif diff --git a/src/ArduinoOcpp/Tasks/Metering/MeterStore.cpp b/src/MicroOcpp/Model/Metering/MeterStore.cpp similarity index 62% rename from src/ArduinoOcpp/Tasks/Metering/MeterStore.cpp rename to src/MicroOcpp/Model/Metering/MeterStore.cpp index ec087987..e4bc9e85 100644 --- a/src/ArduinoOcpp/Tasks/Metering/MeterStore.cpp +++ b/src/MicroOcpp/Model/Metering/MeterStore.cpp @@ -1,48 +1,45 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include -#include +#include +#include -#include +#include #include -#ifndef AO_METERSTORE_DIR -#define AO_METERSTORE_DIR AO_FILENAME_PREFIX "/" +#ifndef MO_MAX_STOPTXDATA_LEN +#define MO_MAX_STOPTXDATA_LEN 4 #endif -#define AO_MAX_STOPTXDATA_LEN 4 - -using namespace ArduinoOcpp; +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) { - AO_DBG_DEBUG("volatile mode"); - (void)0; + MO_DBG_DEBUG("volatile mode"); } } bool TransactionMeterData::addTxData(std::unique_ptr mv) { if (isFinalized()) { - AO_DBG_ERR("immutable"); + MO_DBG_ERR("immutable"); return false; } if (!mv) { - AO_DBG_ERR("null"); + MO_DBG_ERR("null"); return false; } - if (AO_MAX_STOPTXDATA_LEN <= 0) { + if (MO_MAX_STOPTXDATA_LEN <= 0) { //txData off return true; } - bool replaceLast = mvCount >= AO_MAX_STOPTXDATA_LEN; //txData size exceeded? overwrite last entry instead of appending + bool replaceLast = mvCount >= MO_MAX_STOPTXDATA_LEN; //txData size exceeded? overwrite last entry instead of appending if (filesystem) { @@ -53,21 +50,21 @@ bool TransactionMeterData::addTxData(std::unique_ptr mv) { mvIndex = mvCount ; } - char fn [MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MAX_PATH_SIZE, AO_METERSTORE_DIR "sd" "-%u-%u-%u.jsn", connectorId, txNr, mvIndex); - if (ret < 0 || ret >= MAX_PATH_SIZE) { - AO_DBG_ERR("fn error: %i", ret); + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "sd" "-%u-%u-%u.jsn", connectorId, txNr, mvIndex); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); return false; } auto mvDoc = mv->toJson(); if (!mvDoc) { - AO_DBG_ERR("MV not ready yet"); + MO_DBG_ERR("MV not ready yet"); return false; } if (!FilesystemUtils::storeJson(filesystem, fn, *mvDoc)) { - AO_DBG_ERR("FS error"); + MO_DBG_ERR("FS error"); return false; } @@ -78,47 +75,48 @@ bool TransactionMeterData::addTxData(std::unique_ptr mv) { if (replaceLast) { txData.back() = std::move(mv); - AO_DBG_DEBUG("updated latest sd"); + MO_DBG_DEBUG("updated latest sd"); } else { txData.push_back(std::move(mv)); - AO_DBG_DEBUG("added sd"); + MO_DBG_DEBUG("added sd"); } return true; } -std::vector> TransactionMeterData::retrieveStopTxData() { +Vector> TransactionMeterData::retrieveStopTxData() { if (isFinalized()) { - AO_DBG_ERR("Can only retrieve once"); - return decltype(txData) {}; + MO_DBG_ERR("Can only retrieve once"); + return makeVector>(getMemoryTag()); } finalize(); - AO_DBG_DEBUG("creating sd"); + MO_DBG_DEBUG("creating sd"); return std::move(txData); } bool TransactionMeterData::restore(MeterValueBuilder& mvBuilder) { if (!filesystem) { - AO_DBG_DEBUG("No FS - nothing to restore"); + MO_DBG_DEBUG("No FS - nothing to restore"); return true; } const unsigned int MISSES_LIMIT = 3; + unsigned int i = 0; unsigned int misses = 0; while (misses < MISSES_LIMIT) { //search until region without mvs found - char fn [MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MAX_PATH_SIZE, AO_METERSTORE_DIR "sd" "-%u-%u-%u.jsn", connectorId, txNr, mvCount); - if (ret < 0 || ret >= MAX_PATH_SIZE) { - AO_DBG_ERR("fn error: %i", ret); + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "sd" "-%u-%u-%u.jsn", connectorId, txNr, i); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); return false; //all files have same length } - auto doc = FilesystemUtils::loadJson(filesystem, fn); + auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); if (!doc) { misses++; - mvCount++; + i++; continue; } @@ -126,32 +124,32 @@ bool TransactionMeterData::restore(MeterValueBuilder& mvBuilder) { std::unique_ptr mv = mvBuilder.deserializeSample(mvJson); if (!mv) { - AO_DBG_ERR("Deserialization error"); + MO_DBG_ERR("Deserialization error"); misses++; - mvCount++; + i++; continue; } - if (txData.size() >= AO_MAX_STOPTXDATA_LEN) { - AO_DBG_ERR("corrupted memory"); + if (txData.size() >= MO_MAX_STOPTXDATA_LEN) { + MO_DBG_ERR("corrupted memory"); return false; } txData.push_back(std::move(mv)); - mvCount++; + i++; + mvCount = i; misses = 0; } - AO_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) { - AO_DBG_DEBUG("volatile mode"); - (void)0; + MO_DBG_DEBUG("volatile mode"); } } @@ -189,13 +187,13 @@ 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 [MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MAX_PATH_SIZE, AO_METERSTORE_DIR "sd" "-%u-%u-%u.jsn", connectorId, txNr, 0); - if (ret < 0 || ret >= MAX_PATH_SIZE) { - AO_DBG_ERR("fn error: %i", ret); + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "sd" "-%u-%u-%u.jsn", connectorId, txNr, 0); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); return nullptr; //cannot store } @@ -205,14 +203,14 @@ std::shared_ptr MeterStore::getTxMeterData(MeterValueBuild if (exists) { if (!tx->restore(mvBuilder)) { remove(connectorId, txNr); - AO_DBG_ERR("removed corrupted tx entries"); + MO_DBG_ERR("removed corrupted tx entries"); } } } txMeterData.push_back(tx); - AO_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; } @@ -247,10 +245,10 @@ bool MeterStore::remove(unsigned int connectorId, unsigned int txNr) { while (misses < MISSES_LIMIT) { //search until region without mvs found - char fn [MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MAX_PATH_SIZE, AO_METERSTORE_DIR "sd" "-%u-%u-%u.jsn", connectorId, txNr, i); - if (ret < 0 || ret >= MAX_PATH_SIZE) { - AO_DBG_ERR("fn error: %i", ret); + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "sd" "-%u-%u-%u.jsn", connectorId, txNr, i); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); return false; //all files have same length } @@ -267,15 +265,15 @@ bool MeterStore::remove(unsigned int connectorId, unsigned int txNr) { } } - AO_DBG_DEBUG("remove %u mvs for txNr %u", mvCount, txNr); + MO_DBG_DEBUG("remove %u mvs for txNr %u", mvCount, txNr); for (unsigned int i = 0; i < mvCount; i++) { unsigned int sd = mvCount - 1U - i; - char fn [MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MAX_PATH_SIZE, AO_METERSTORE_DIR "sd" "-%u-%u-%u.jsn", connectorId, txNr, sd); - if (ret < 0 || ret >= MAX_PATH_SIZE) { - AO_DBG_ERR("fn error: %i", ret); + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "sd" "-%u-%u-%u.jsn", connectorId, txNr, sd); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); return false; } @@ -292,10 +290,9 @@ bool MeterStore::remove(unsigned int connectorId, unsigned int txNr) { txMeterData.end()); if (success) { - AO_DBG_DEBUG("Removed meter values for cId %u, txNr %u", connectorId, txNr); - (void)0; + MO_DBG_DEBUG("Removed meter values for cId %u, txNr %u", connectorId, txNr); } else { - AO_DBG_DEBUG("corrupted fs"); + MO_DBG_DEBUG("corrupted fs"); } return success; diff --git a/src/ArduinoOcpp/Tasks/Metering/MeterStore.h b/src/MicroOcpp/Model/Metering/MeterStore.h similarity index 64% rename from src/ArduinoOcpp/Tasks/Metering/MeterStore.h rename to src/MicroOcpp/Model/Metering/MeterStore.h index 1e6bef5f..bdb67441 100644 --- a/src/ArduinoOcpp/Tasks/Metering/MeterStore.h +++ b/src/MicroOcpp/Model/Metering/MeterStore.h @@ -1,37 +1,35 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// 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 +#include -#include -#include +namespace MicroOcpp { -namespace ArduinoOcpp { - -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 new file mode 100644 index 00000000..740905da --- /dev/null +++ b/src/MicroOcpp/Model/Metering/MeterValue.cpp @@ -0,0 +1,212 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include +#include +#include + +using namespace MicroOcpp; + +MeterValue::MeterValue(const Timestamp& timestamp) : + MemoryManaged("v16.Metering.MeterValue"), + timestamp(timestamp), + sampledValue(makeVector>(getMemoryTag())) { + +} + +void MeterValue::addSampledValue(std::unique_ptr sample) { + sampledValue.push_back(std::move(sample)); +} + +std::unique_ptr MeterValue::toJson() { + size_t capacity = 0; + auto entries = makeVector>(getMemoryTag()); + for (auto sample = sampledValue.begin(); sample != sampledValue.end(); sample++) { + auto json = (*sample)->toJson(); + if (!json) { + return nullptr; + } + capacity += json->capacity(); + entries.push_back(std::move(json)); + } + + capacity += JSON_ARRAY_SIZE(entries.size()); + capacity += JSONDATE_LENGTH + 1; + capacity += JSON_OBJECT_SIZE(2); + + auto result = makeJsonDoc(getMemoryTag(), capacity); + auto jsonPayload = result->to(); + + char timestampStr [JSONDATE_LENGTH + 1] = {'\0'}; + if (timestamp.toJsonString(timestampStr, JSONDATE_LENGTH + 1)) { + jsonPayload["timestamp"] = timestampStr; + } + auto jsonMeterValue = jsonPayload.createNestedArray("sampledValue"); + for (auto entry = entries.begin(); entry != entries.end(); entry++) { + jsonMeterValue.add(**entry); + } + return result; +} + +const Timestamp& MeterValue::getTimestamp() { + return timestamp; +} + +void MeterValue::setTimestamp(Timestamp timestamp) { + this->timestamp = timestamp; +} + +ReadingContext MeterValue::getReadingContext() { + //all sampledValues have the same ReadingContext. Just get the first result + for (auto sample = sampledValue.begin(); sample != sampledValue.end(); sample++) { + if ((*sample)->getReadingContext() != ReadingContext_UNDEFINED) { + return (*sample)->getReadingContext(); + } + } + return ReadingContext_UNDEFINED; +} + +void MeterValue::setTxNr(unsigned int txNr) { + if (txNr > (unsigned int)std::numeric_limits::max()) { + MO_DBG_ERR("invalid arg"); + return; + } + this->txNr = (int)txNr; +} + +int MeterValue::getTxNr() { + return txNr; +} + +void MeterValue::setOpNr(unsigned int opNr) { + this->opNr = opNr; +} + +unsigned int MeterValue::getOpNr() { + return opNr; +} + +void MeterValue::advanceAttemptNr() { + attemptNr++; +} + +unsigned int MeterValue::getAttemptNr() { + return attemptNr; +} + +unsigned long MeterValue::getAttemptTime() { + return attemptTime; +} + +void MeterValue::setAttemptTime(unsigned long timestamp) { + this->attemptTime = timestamp; +} + +MeterValueBuilder::MeterValueBuilder(const Vector> &samplers, + std::shared_ptr samplersSelectStr) : + MemoryManaged("v16.Metering.MeterValueBuilder"), + samplers(samplers), + selectString(samplersSelectStr), + select_mask(makeVector(getMemoryTag())) { + + updateObservedSamplers(); + select_observe = selectString->getValueRevision(); +} + +void MeterValueBuilder::updateObservedSamplers() { + + if (select_mask.size() != samplers.size()) { + select_mask.resize(samplers.size(), false); + select_n = 0; + } + + for (size_t i = 0; i < select_mask.size(); i++) { + select_mask[i] = false; + } + + auto sstring = selectString->getString(); + auto ssize = strlen(sstring) + 1; + size_t sl = 0, sr = 0; + while (sstring && sl < ssize) { + while (sr < ssize) { + if (sstring[sr] == ',') { + break; + } + sr++; + } + + if (sr != sl + 1) { + for (size_t i = 0; i < samplers.size(); i++) { + if (!strncmp(samplers[i]->getProperties().getMeasurand(), sstring + sl, sr - sl)) { + select_mask[i] = true; + select_n++; + } + } + } + + sr++; + sl = sr; + } +} + +std::unique_ptr MeterValueBuilder::takeSample(const Timestamp& timestamp, const ReadingContext& context) { + if (select_observe != selectString->getValueRevision() || //OCPP server has changed configuration about which measurands to take + samplers.size() != select_mask.size()) { //Client has added another Measurand; synchronize lists + MO_DBG_DEBUG("Updating observed samplers due to config change or samplers added"); + updateObservedSamplers(); + select_observe = selectString->getValueRevision(); + } + + if (select_n == 0) { + return nullptr; + } + + auto sample = std::unique_ptr(new MeterValue(timestamp)); + + for (size_t i = 0; i < select_mask.size(); i++) { + if (select_mask[i]) { + sample->addSampledValue(samplers[i]->takeValue(context)); + } + } + + return sample; +} + +std::unique_ptr MeterValueBuilder::deserializeSample(const JsonObject mvJson) { + + Timestamp timestamp; + bool ret = timestamp.setTime(mvJson["timestamp"] | "Invalid"); + if (!ret) { + MO_DBG_ERR("invalid timestamp"); + return nullptr; + } + + auto sample = std::unique_ptr(new MeterValue(timestamp)); + + JsonArray sampledValue = mvJson["sampledValue"]; + for (JsonObject svJson : sampledValue) { //for each sampled value, search sampler with matching measurand type + for (auto& sampler : samplers) { + auto& properties = sampler->getProperties(); + if (!strcmp(properties.getMeasurand(), svJson["measurand"] | "") && + !strcmp(properties.getFormat(), svJson["format"] | "") && + !strcmp(properties.getPhase(), svJson["phase"] | "") && + !strcmp(properties.getLocation(), svJson["location"] | "") && + !strcmp(properties.getUnit(), svJson["unit"] | "")) { + //found correct sampler + auto dVal = sampler->deserializeValue(svJson); + if (dVal) { + sample->addSampledValue(std::move(dVal)); + } else { + MO_DBG_ERR("deserialization error"); + } + break; + } + } + } + + MO_DBG_VERBOSE("deserialized MV"); + return sample; +} diff --git a/src/MicroOcpp/Model/Metering/MeterValue.h b/src/MicroOcpp/Model/Metering/MeterValue.h new file mode 100644 index 00000000..0637ad33 --- /dev/null +++ b/src/MicroOcpp/Model/Metering/MeterValue.h @@ -0,0 +1,72 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_METERVALUE_H +#define MO_METERVALUE_H + +#include +#include +#include +#include +#include +#include + +namespace MicroOcpp { + +class MeterValue : public MemoryManaged { +private: + Timestamp timestamp; + Vector> sampledValue; + + int txNr = -1; + unsigned int opNr = 1; + unsigned int attemptNr = 0; + unsigned long attemptTime = 0; +public: + MeterValue(const Timestamp& timestamp); + MeterValue(const MeterValue& other) = delete; + + void addSampledValue(std::unique_ptr sample); + + std::unique_ptr toJson(); + + const Timestamp& getTimestamp(); + void setTimestamp(Timestamp timestamp); + + ReadingContext getReadingContext(); + + void setTxNr(unsigned int txNr); + int getTxNr(); + + void setOpNr(unsigned int opNr); + unsigned int getOpNr(); + + void advanceAttemptNr(); + unsigned int getAttemptNr(); + + unsigned long getAttemptTime(); + void setAttemptTime(unsigned long timestamp); +}; + +class MeterValueBuilder : public MemoryManaged { +private: + const Vector> &samplers; + std::shared_ptr selectString; + Vector select_mask; + unsigned int select_n {0}; + decltype(selectString->getValueRevision()) select_observe; + + void updateObservedSamplers(); +public: + MeterValueBuilder(const Vector> &samplers, + std::shared_ptr samplersSelectStr); + + std::unique_ptr takeSample(const Timestamp& timestamp, const ReadingContext& context); + + std::unique_ptr deserializeSample(const JsonObject mvJson); +}; + +} + +#endif 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 new file mode 100644 index 00000000..378232a5 --- /dev/null +++ b/src/MicroOcpp/Model/Metering/MeteringConnector.cpp @@ -0,0 +1,304 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace MicroOcpp; +using namespace MicroOcpp::Ocpp16; + +MeteringConnector::MeteringConnector(Context& context, int connectorId, MeterStore& meterStore) + : MemoryManaged("v16.Metering.MeteringConnector"), context(context), model(context.getModel()), connectorId{connectorId}, meterStore(meterStore), meterData(makeVector>(getMemoryTag())), samplers(makeVector>(getMemoryTag())) { + + context.getRequestQueue().addSendQueue(this); + + auto meterValuesSampledDataString = declareConfiguration("MeterValuesSampledData", ""); + declareConfiguration("MeterValuesSampledDataMaxLength", 8, CONFIGURATION_VOLATILE, true); + meterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval", 60); + registerConfigurationValidator("MeterValueSampleInterval", VALIDATE_UNSIGNED_INT); + + auto stopTxnSampledDataString = declareConfiguration("StopTxnSampledData", ""); + declareConfiguration("StopTxnSampledDataMaxLength", 8, CONFIGURATION_VOLATILE, true); + + auto meterValuesAlignedDataString = declareConfiguration("MeterValuesAlignedData", ""); + declareConfiguration("MeterValuesAlignedDataMaxLength", 8, CONFIGURATION_VOLATILE, true); + clockAlignedDataIntervalInt = declareConfiguration("ClockAlignedDataInterval", 0); + registerConfigurationValidator("ClockAlignedDataInterval", VALIDATE_UNSIGNED_INT); + + auto stopTxnAlignedDataString = declareConfiguration("StopTxnAlignedData", ""); + + meterValuesInTxOnlyBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "MeterValuesInTxOnly", true); + stopTxnDataCapturePeriodicBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "StopTxnDataCapturePeriodic", false); + + transactionMessageAttemptsInt = declareConfiguration("TransactionMessageAttempts", 3); + transactionMessageRetryIntervalInt = declareConfiguration("TransactionMessageRetryInterval", 60); + + sampledDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, meterValuesSampledDataString)); + alignedDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, meterValuesAlignedDataString)); + stopTxnSampledDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, stopTxnSampledDataString)); + stopTxnAlignedDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, stopTxnAlignedDataString)); +} + +void MeteringConnector::loop() { + + bool txBreak = false; + if (model.getConnector(connectorId)) { + auto &curTx = model.getConnector(connectorId)->getTransaction(); + txBreak = (curTx && curTx->isRunning()) != trackTxRunning; + trackTxRunning = (curTx && curTx->isRunning()); + } + + if (txBreak) { + lastSampleTime = mocpp_tick_ms(); + } + + if (model.getConnector(connectorId)) { + if (transaction != model.getConnector(connectorId)->getTransaction()) { + transaction = model.getConnector(connectorId)->getTransaction(); + } + + if (transaction && transaction->isRunning() && !transaction->isSilent()) { + //check during transaction + + if (!stopTxnData || stopTxnData->getTxNr() != transaction->getTxNr()) { + MO_DBG_WARN("reload stopTxnData, %s, for tx-%u-%u", stopTxnData ? "replace" : "first time", connectorId, transaction->getTxNr()); + //reload (e.g. after power cut during transaction) + stopTxnData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction.get()); + } + } else { + //check outside of transaction + + if (connectorId != 0 && meterValuesInTxOnlyBool->getBool()) { + //don't take any MeterValues outside of transactions on connectorIds other than 0 + return; + } + } + } + + if (clockAlignedDataIntervalInt->getInt() >= 1 && model.getClock().now() >= MIN_TIME) { + + auto& timestampNow = model.getClock().now(); + auto dt = nextAlignedTime - timestampNow; + if (dt < 0 || //normal case: interval elapsed + dt > clockAlignedDataIntervalInt->getInt()) { //special case: clock has been adjusted or first run + + MO_DBG_DEBUG("Clock aligned measurement %ds: %s", dt, + abs(dt) <= 60 ? + "in time (tolerance <= 60s)" : "off, e.g. because of first run. Ignore"); + if (abs(dt) <= 60) { //is measurement still "clock-aligned"? + + if (auto alignedMeterValue = alignedDataBuilder->takeSample(model.getClock().now(), ReadingContext_SampleClock)) { + if (meterData.size() >= MO_METERVALUES_CACHE_MAXSIZE) { + MO_DBG_INFO("MeterValue cache full. Drop old MV"); + meterData.erase(meterData.begin()); + } + alignedMeterValue->setOpNr(context.getRequestQueue().getNextOpNr()); + if (transaction) { + alignedMeterValue->setTxNr(transaction->getTxNr()); + } + meterData.push_back(std::move(alignedMeterValue)); + } + + if (stopTxnData) { + auto alignedStopTx = stopTxnAlignedDataBuilder->takeSample(model.getClock().now(), ReadingContext_SampleClock); + if (alignedStopTx) { + stopTxnData->addTxData(std::move(alignedStopTx)); + } + } + } + + Timestamp midnightBase = Timestamp(2010,0,0,0,0,0); + auto intervall = timestampNow - midnightBase; + intervall %= 3600 * 24; + Timestamp midnight = timestampNow - intervall; + intervall += clockAlignedDataIntervalInt->getInt(); + if (intervall >= 3600 * 24) { + //next measurement is tomorrow; set to precisely 00:00 + nextAlignedTime = midnight; + nextAlignedTime += 3600 * 24; + } else { + intervall /= clockAlignedDataIntervalInt->getInt(); + nextAlignedTime = midnight + (intervall * clockAlignedDataIntervalInt->getInt()); + } + } + } + + if (meterValueSampleIntervalInt->getInt() >= 1) { + //record periodic tx data + + if (mocpp_tick_ms() - lastSampleTime >= (unsigned long) (meterValueSampleIntervalInt->getInt() * 1000)) { + if (auto sampledMeterValue = sampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_SamplePeriodic)) { + if (meterData.size() >= MO_METERVALUES_CACHE_MAXSIZE) { + MO_DBG_INFO("MeterValue cache full. Drop old MV"); + meterData.erase(meterData.begin()); + } + sampledMeterValue->setOpNr(context.getRequestQueue().getNextOpNr()); + if (transaction) { + sampledMeterValue->setTxNr(transaction->getTxNr()); + } + meterData.push_back(std::move(sampledMeterValue)); + } + + if (stopTxnData && stopTxnDataCapturePeriodicBool->getBool()) { + auto sampleStopTx = stopTxnSampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_SamplePeriodic); + if (sampleStopTx) { + stopTxnData->addTxData(std::move(sampleStopTx)); + } + } + lastSampleTime = mocpp_tick_ms(); + } + } +} + +std::unique_ptr MeteringConnector::takeTriggeredMeterValues() { + + auto sample = sampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_Trigger); + + if (!sample) { + return nullptr; + } + + std::shared_ptr transaction = nullptr; + if (model.getConnector(connectorId)) { + transaction = model.getConnector(connectorId)->getTransaction(); + } + + return std::unique_ptr(new MeterValues(model, std::move(sample), connectorId, transaction)); +} + +void MeteringConnector::addMeterValueSampler(std::unique_ptr meterValueSampler) { + if (!strcmp(meterValueSampler->getProperties().getMeasurand(), "Energy.Active.Import.Register")) { + energySamplerIndex = samplers.size(); + } + samplers.push_back(std::move(meterValueSampler)); +} + +std::unique_ptr MeteringConnector::readTxEnergyMeter(ReadingContext model) { + if (energySamplerIndex >= 0 && (size_t) energySamplerIndex < samplers.size()) { + return samplers[energySamplerIndex]->takeValue(model); + } else { + MO_DBG_DEBUG("Called readTxEnergyMeter(), but no energySampler or handling strategy set"); + return nullptr; + } +} + +void MeteringConnector::beginTxMeterData(Transaction *transaction) { + if (!stopTxnData || stopTxnData->getTxNr() != transaction->getTxNr()) { + stopTxnData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction); + } + + if (stopTxnData) { + auto sampleTxBegin = stopTxnSampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_TransactionBegin); + if (sampleTxBegin) { + stopTxnData->addTxData(std::move(sampleTxBegin)); + } + } +} + +std::shared_ptr MeteringConnector::endTxMeterData(Transaction *transaction) { + if (!stopTxnData || stopTxnData->getTxNr() != transaction->getTxNr()) { + stopTxnData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction); + } + + if (stopTxnData) { + auto sampleTxEnd = stopTxnSampledDataBuilder->takeSample(model.getClock().now(), ReadingContext_TransactionEnd); + if (sampleTxEnd) { + stopTxnData->addTxData(std::move(sampleTxEnd)); + } + } + + return std::move(stopTxnData); +} + +void MeteringConnector::abortTxMeterData() { + stopTxnData.reset(); +} + +std::shared_ptr MeteringConnector::getStopTxMeterData(Transaction *transaction) { + auto txData = meterStore.getTxMeterData(*stopTxnSampledDataBuilder, transaction); + + if (!txData) { + MO_DBG_ERR("could not create TxData"); + return nullptr; + } + + return txData; +} + +bool MeteringConnector::existsSampler(const char *measurand, size_t len) { + for (size_t i = 0; i < samplers.size(); i++) { + if (strlen(samplers[i]->getProperties().getMeasurand()) == len && + !strncmp(measurand, samplers[i]->getProperties().getMeasurand(), len)) { + return true; + } + } + + return false; +} + +unsigned int MeteringConnector::getFrontRequestOpNr() { + if (!meterDataFront && !meterData.empty()) { + MO_DBG_DEBUG("advance MV front"); + meterDataFront = std::move(meterData.front()); + meterData.erase(meterData.begin()); + } + if (meterDataFront) { + return meterDataFront->getOpNr(); + } + return NoOperation; +} + +std::unique_ptr MeteringConnector::fetchFrontRequest() { + + if (!meterDataFront) { + return nullptr; + } + + if ((int)meterDataFront->getAttemptNr() >= transactionMessageAttemptsInt->getInt()) { + MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard MeterValue"); + meterDataFront.reset(); + return nullptr; + } + + if (mocpp_tick_ms() - meterDataFront->getAttemptTime() < meterDataFront->getAttemptNr() * (unsigned long)(std::max(0, transactionMessageRetryIntervalInt->getInt())) * 1000UL) { + return nullptr; + } + + meterDataFront->advanceAttemptNr(); + meterDataFront->setAttemptTime(mocpp_tick_ms()); + + //fetch tx for meterValue + std::shared_ptr tx; + if (meterDataFront->getTxNr() >= 0) { + tx = model.getTransactionStore()->getTransaction(connectorId, meterDataFront->getTxNr()); + } + + //discard MV if it belongs to silent tx + if (tx && tx->isSilent()) { + MO_DBG_DEBUG("Drop MeterValue belonging to silent tx"); + meterDataFront.reset(); + return nullptr; + } + + auto meterValues = makeRequest(new MeterValues(model, meterDataFront.get(), connectorId, tx)); + meterValues->setOnReceiveConfListener([this] (JsonObject) { + //operation success + MO_DBG_DEBUG("drop MV front"); + meterDataFront.reset(); + }); + + return meterValues; +} diff --git a/src/MicroOcpp/Model/Metering/MeteringConnector.h b/src/MicroOcpp/Model/Metering/MeteringConnector.h new file mode 100644 index 00000000..75873ff6 --- /dev/null +++ b/src/MicroOcpp/Model/Metering/MeteringConnector.h @@ -0,0 +1,95 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_METERING_CONNECTOR_H +#define MO_METERING_CONNECTOR_H + +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifndef MO_METERVALUES_CACHE_MAXSIZE +#define MO_METERVALUES_CACHE_MAXSIZE MO_REQUEST_CACHE_MAXSIZE +#endif + +namespace MicroOcpp { + +class Context; +class Model; +class Operation; +class MeterStore; + +class MeteringConnector : public MemoryManaged, public RequestEmitter { +private: + Context& context; + Model& model; + const int connectorId; + MeterStore& meterStore; + + Vector> meterData; + std::unique_ptr meterDataFront; + std::shared_ptr stopTxnData; + + std::unique_ptr sampledDataBuilder; + std::unique_ptr alignedDataBuilder; + std::unique_ptr stopTxnSampledDataBuilder; + std::unique_ptr stopTxnAlignedDataBuilder; + + std::shared_ptr sampledDataSelectString; + std::shared_ptr alignedDataSelectString; + std::shared_ptr stopTxnSampledDataSelectString; + std::shared_ptr stopTxnAlignedDataSelectString; + + unsigned long lastSampleTime = 0; //0 means not charging right now + Timestamp nextAlignedTime; + std::shared_ptr transaction; + bool trackTxRunning = false; + + Vector> samplers; + int energySamplerIndex {-1}; + + std::shared_ptr meterValueSampleIntervalInt; + + std::shared_ptr clockAlignedDataIntervalInt; + + std::shared_ptr meterValuesInTxOnlyBool; + std::shared_ptr stopTxnDataCapturePeriodicBool; + + std::shared_ptr transactionMessageAttemptsInt; + std::shared_ptr transactionMessageRetryIntervalInt; +public: + MeteringConnector(Context& context, int connectorId, MeterStore& meterStore); + + void loop(); + + void addMeterValueSampler(std::unique_ptr meterValueSampler); + + std::unique_ptr readTxEnergyMeter(ReadingContext model); + + std::unique_ptr takeTriggeredMeterValues(); + + void beginTxMeterData(Transaction *transaction); + + std::shared_ptr endTxMeterData(Transaction *transaction); + + void abortTxMeterData(); + + std::shared_ptr getStopTxMeterData(Transaction *transaction); + + bool existsSampler(const char *measurand, size_t len); + + //RequestEmitter implementation + unsigned int getFrontRequestOpNr() override; + std::unique_ptr fetchFrontRequest() override; + +}; + +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Model/Metering/MeteringService.cpp b/src/MicroOcpp/Model/Metering/MeteringService.cpp new file mode 100644 index 00000000..73ab4deb --- /dev/null +++ b/src/MicroOcpp/Model/Metering/MeteringService.cpp @@ -0,0 +1,163 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace MicroOcpp; + +MeteringService::MeteringService(Context& context, int numConn, std::shared_ptr filesystem) + : MemoryManaged("v16.Metering.MeteringService"), context(context), meterStore(filesystem), connectors(makeVector>(getMemoryTag())) { + + //set factory defaults for Metering-related config keys + declareConfiguration("MeterValuesSampledData", "Energy.Active.Import.Register,Power.Active.Import"); + declareConfiguration("StopTxnSampledData", ""); + declareConfiguration("MeterValuesAlignedData", "Energy.Active.Import.Register,Power.Active.Import"); + declareConfiguration("StopTxnAlignedData", ""); + + connectors.reserve(numConn); + for (int i = 0; i < numConn; i++) { + connectors.emplace_back(new MeteringConnector(context, i, meterStore)); + } + + std::function validateSelectString = [this] (const char *csl) { + bool isValid = true; + const char *l = csl; //the beginning of an entry of the comma-separated list + const char *r = l; //one place after the last character of the entry beginning with l + while (*l) { + if (*l == ',') { + l++; + continue; + } + r = l + 1; + while (*r != '\0' && *r != ',') { + r++; + } + bool found = false; + for (size_t cId = 0; cId < connectors.size(); cId++) { + if (connectors[cId]->existsSampler(l, (size_t) (r - l))) { + found = true; + break; + } + } + if (!found) { + isValid = false; + MO_DBG_WARN("could not find metering device for %.*s", (int) (r - l), l); + break; + } + l = r; + } + return isValid; + }; + + registerConfigurationValidator("MeterValuesSampledData", validateSelectString); + registerConfigurationValidator("StopTxnSampledData", validateSelectString); + registerConfigurationValidator("MeterValuesAlignedData", validateSelectString); + registerConfigurationValidator("StopTxnAlignedData", validateSelectString); + registerConfigurationValidator("MeterValueSampleInterval", VALIDATE_UNSIGNED_INT); + registerConfigurationValidator("ClockAlignedDataInterval", VALIDATE_UNSIGNED_INT); + + /* + * Register further message handlers to support echo mode: when this library + * is connected with a WebSocket echo server, let it reply to its own requests. + * Mocking an OCPP Server on the same device makes running (unit) tests easier. + */ + context.getOperationRegistry().registerOperation("MeterValues", [this] () { + return new Ocpp16::MeterValues(this->context.getModel());}); +} + +void MeteringService::loop(){ + for (unsigned int i = 0; i < connectors.size(); i++){ + connectors[i]->loop(); + } +} + +void MeteringService::addMeterValueSampler(int connectorId, std::unique_ptr meterValueSampler) { + if (connectorId < 0 || connectorId >= (int) connectors.size()) { + MO_DBG_ERR("connectorId is out of bounds"); + return; + } + connectors[connectorId]->addMeterValueSampler(std::move(meterValueSampler)); +} + +std::unique_ptr MeteringService::readTxEnergyMeter(int connectorId, ReadingContext context) { + if (connectorId < 0 || (size_t) connectorId >= connectors.size()) { + MO_DBG_ERR("connectorId is out of bounds"); + return nullptr; + } + return connectors[connectorId]->readTxEnergyMeter(context); +} + +std::unique_ptr MeteringService::takeTriggeredMeterValues(int connectorId) { + if (connectorId < 0 || connectorId >= (int) connectors.size()) { + MO_DBG_ERR("connectorId out of bounds. Ignore"); + return nullptr; + } + + auto msg = connectors[connectorId]->takeTriggeredMeterValues(); + if (msg) { + auto meterValues = makeRequest(std::move(msg)); + meterValues->setTimeout(120000); + return meterValues; + } + MO_DBG_DEBUG("Did not take any samples for connectorId %d", connectorId); + return nullptr; +} + +void MeteringService::beginTxMeterData(Transaction *transaction) { + if (!transaction) { + MO_DBG_ERR("invalid argument"); + return; + } + auto connectorId = transaction->getConnectorId(); + if (connectorId >= connectors.size()) { + MO_DBG_ERR("connectorId is out of bounds"); + return; + } + connectors[connectorId]->beginTxMeterData(transaction); +} + +std::shared_ptr MeteringService::endTxMeterData(Transaction *transaction) { + if (!transaction) { + MO_DBG_ERR("invalid argument"); + return nullptr; + } + auto connectorId = transaction->getConnectorId(); + if (connectorId >= connectors.size()) { + MO_DBG_ERR("connectorId is out of bounds"); + return nullptr; + } + return connectors[connectorId]->endTxMeterData(transaction); +} + +void MeteringService::abortTxMeterData(unsigned int connectorId) { + if (connectorId >= connectors.size()) { + MO_DBG_ERR("connectorId is out of bounds"); + return; + } + connectors[connectorId]->abortTxMeterData(); +} + +std::shared_ptr MeteringService::getStopTxMeterData(Transaction *transaction) { + if (!transaction) { + MO_DBG_ERR("invalid argument"); + return nullptr; + } + auto connectorId = transaction->getConnectorId(); + if (connectorId >= connectors.size()) { + MO_DBG_ERR("connectorId is out of bounds"); + return nullptr; + } + return connectors[connectorId]->getStopTxMeterData(transaction); +} + +bool MeteringService::removeTxMeterData(unsigned int connectorId, unsigned int txNr) { + return meterStore.remove(connectorId, txNr); +} diff --git a/src/MicroOcpp/Model/Metering/MeteringService.h b/src/MicroOcpp/Model/Metering/MeteringService.h new file mode 100644 index 00000000..748a0b4a --- /dev/null +++ b/src/MicroOcpp/Model/Metering/MeteringService.h @@ -0,0 +1,53 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_METERINGSERVICE_H +#define MO_METERINGSERVICE_H + +#include +#include + +#include +#include +#include +#include + +namespace MicroOcpp { + +class Context; +class Request; +class FilesystemAdapter; + +class MeteringService : public MemoryManaged { +private: + Context& context; + MeterStore meterStore; + + Vector> connectors; +public: + MeteringService(Context& context, int numConnectors, std::shared_ptr filesystem); + + void loop(); + + void addMeterValueSampler(int connectorId, std::unique_ptr meterValueSampler); + + std::unique_ptr readTxEnergyMeter(int connectorId, ReadingContext reason); + + std::unique_ptr takeTriggeredMeterValues(int connectorId); //snapshot of all meters now + + void beginTxMeterData(Transaction *transaction); + + std::shared_ptr endTxMeterData(Transaction *transaction); //use return value to keep data in cache + + void abortTxMeterData(unsigned int connectorId); //call this to free resources if txMeterData record is not ended normally. Does not remove files + + std::shared_ptr getStopTxMeterData(Transaction *transaction); //prefer endTxMeterData when possible + + bool removeTxMeterData(unsigned int connectorId, unsigned int txNr); + + int getNumConnectors() {return connectors.size();} +}; + +} //end namespace MicroOcpp +#endif 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 new file mode 100644 index 00000000..4168a048 --- /dev/null +++ b/src/MicroOcpp/Model/Metering/SampledValue.cpp @@ -0,0 +1,61 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include + +#ifndef MO_SAMPLEDVALUE_FLOAT_FORMAT +#define MO_SAMPLEDVALUE_FLOAT_FORMAT "%.2f" +#endif + +using namespace MicroOcpp; + +int32_t SampledValueDeSerializer::deserialize(const char *str) { + return strtol(str, nullptr, 10); +} + +MicroOcpp::String SampledValueDeSerializer::serialize(int32_t& val) { + char str [12] = {'\0'}; + snprintf(str, 12, "%" PRId32, val); + return makeString("v16.Metering.SampledValueDeSerializer", str); +} + +MicroOcpp::String SampledValueDeSerializer::serialize(float& val) { + char str [20]; + str[0] = '\0'; + snprintf(str, 20, MO_SAMPLEDVALUE_FLOAT_FORMAT, val); + return makeString("v16.Metering.SampledValueDeSerializer", str); +} + +std::unique_ptr SampledValue::toJson() { + auto value = serializeValue(); + if (value.empty()) { + return nullptr; + } + size_t capacity = 0; + capacity += JSON_OBJECT_SIZE(8); + capacity += value.length() + 1; + auto result = makeJsonDoc("v16.Metering.SampledValue", capacity); + auto payload = result->to(); + payload["value"] = value; + auto context_cstr = serializeReadingContext(context); + if (context_cstr) + payload["context"] = context_cstr; + if (*properties.getFormat()) + payload["format"] = properties.getFormat(); + if (*properties.getMeasurand()) + payload["measurand"] = properties.getMeasurand(); + if (*properties.getPhase()) + payload["phase"] = properties.getPhase(); + if (*properties.getLocation()) + payload["location"] = properties.getLocation(); + if (*properties.getUnit()) + payload["unit"] = properties.getUnit(); + return result; +} + +ReadingContext SampledValue::getReadingContext() { + return context; +} diff --git a/src/ArduinoOcpp/Tasks/Metering/SampledValue.h b/src/MicroOcpp/Model/Metering/SampledValue.h similarity index 64% rename from src/ArduinoOcpp/Tasks/Metering/SampledValue.h rename to src/MicroOcpp/Model/Metering/SampledValue.h index ea9a007d..350990ae 100644 --- a/src/ArduinoOcpp/Tasks/Metering/SampledValue.h +++ b/src/MicroOcpp/Model/Metering/SampledValue.h @@ -1,5 +1,5 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #ifndef SAMPLEDVALUE_H @@ -9,28 +9,28 @@ #include #include -namespace ArduinoOcpp { +#include +#include +#include + +namespace MicroOcpp { template 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); }; template <> class SampledValueDeSerializer { // example class public: - static int32_t deserialize(const char *str) {return strtol(str, nullptr,10);} + 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) { - char str [12] = {'\0'}; - snprintf(str, 12, "%d", val); - return std::string(str); - } - static int32_t toInteger(int32_t& val) {return val;} + static String serialize(int32_t& val); + static int32_t toInteger(int32_t& val) {return val;} //no conversion required }; template <> @@ -38,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,9,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), @@ -65,62 +66,47 @@ 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;} + const char *getUnit() const {return unit.c_str();} }; -enum class ReadingContext { - InterruptionBegin, - InterruptionEnd, - Other, - SampleClock, - SamplePeriodic, - TransactionBegin, - TransactionEnd, - Trigger, - NOT_SET -}; - -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; + + ReadingContext getReadingContext(); }; 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,11 +137,11 @@ 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"] | ""))); } }; -} //end namespace ArduinoOcpp +} //end namespace MicroOcpp #endif diff --git a/src/MicroOcpp/Model/Model.cpp b/src/MicroOcpp/Model/Model.cpp new file mode 100644 index 00000000..fe59568f --- /dev/null +++ b/src/MicroOcpp/Model/Model.cpp @@ -0,0 +1,352 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace MicroOcpp; + +Model::Model(ProtocolVersion version, uint16_t bootNr) : MemoryManaged("Model"), connectors(makeVector>(getMemoryTag())), version(version), bootNr(bootNr) { + +} + +Model::~Model() = default; + +void Model::loop() { + + if (bootService) { + bootService->loop(); + } + + if (capabilitiesUpdated) { + updateSupportedStandardProfiles(); + capabilitiesUpdated = false; + } + + if (!runTasks) { + return; + } + + for (auto& connector : connectors) { + connector->loop(); + } + + if (chargeControlCommon) + chargeControlCommon->loop(); + + if (smartChargingService) + smartChargingService->loop(); + + if (heartbeatService) + heartbeatService->loop(); + + if (meteringService) + meteringService->loop(); + + if (diagnosticsService) + diagnosticsService->loop(); + + if (firmwareService) + firmwareService->loop(); + +#if MO_ENABLE_RESERVATION + if (reservationService) + reservationService->loop(); +#endif //MO_ENABLE_RESERVATION + + if (resetService) + resetService->loop(); + +#if MO_ENABLE_V201 + if (availabilityService) + availabilityService->loop(); + + if (transactionService) + transactionService->loop(); + + if (resetServiceV201) + resetServiceV201->loop(); +#endif +} + +void Model::setTransactionStore(std::unique_ptr ts) { + transactionStore = std::move(ts); + capabilitiesUpdated = true; +} + +TransactionStore *Model::getTransactionStore() { + return transactionStore.get(); +} + +void Model::setSmartChargingService(std::unique_ptr scs) { + smartChargingService = std::move(scs); + capabilitiesUpdated = true; +} + +SmartChargingService* Model::getSmartChargingService() const { + return smartChargingService.get(); +} + +void Model::setConnectorsCommon(std::unique_ptr ccs) { + chargeControlCommon = std::move(ccs); + capabilitiesUpdated = true; +} + +ConnectorsCommon *Model::getConnectorsCommon() { + return chargeControlCommon.get(); +} + +void Model::setConnectors(Vector>&& connectors) { + this->connectors = std::move(connectors); + capabilitiesUpdated = true; +} + +unsigned int Model::getNumConnectors() const { + return connectors.size(); +} + +Connector *Model::getConnector(unsigned int connectorId) { + if (connectorId >= connectors.size()) { + MO_DBG_ERR("connector with connectorId %u does not exist", connectorId); + return nullptr; + } + + return connectors[connectorId].get(); +} + +void Model::setMeteringSerivce(std::unique_ptr ms) { + meteringService = std::move(ms); + capabilitiesUpdated = true; +} + +MeteringService* Model::getMeteringService() const { + return meteringService.get(); +} + +void Model::setFirmwareService(std::unique_ptr fws) { + firmwareService = std::move(fws); + capabilitiesUpdated = true; +} + +FirmwareService *Model::getFirmwareService() const { + return firmwareService.get(); +} + +void Model::setDiagnosticsService(std::unique_ptr ds) { + diagnosticsService = std::move(ds); + capabilitiesUpdated = true; +} + +DiagnosticsService *Model::getDiagnosticsService() const { + return diagnosticsService.get(); +} + +void Model::setHeartbeatService(std::unique_ptr hs) { + heartbeatService = std::move(hs); + capabilitiesUpdated = true; +} + +#if MO_ENABLE_LOCAL_AUTH +void Model::setAuthorizationService(std::unique_ptr as) { + authorizationService = std::move(as); + capabilitiesUpdated = true; +} + +AuthorizationService *Model::getAuthorizationService() { + return authorizationService.get(); +} +#endif //MO_ENABLE_LOCAL_AUTH + +#if MO_ENABLE_RESERVATION +void Model::setReservationService(std::unique_ptr rs) { + reservationService = std::move(rs); + capabilitiesUpdated = true; +} + +ReservationService *Model::getReservationService() { + return reservationService.get(); +} +#endif //MO_ENABLE_RESERVATION + +void Model::setBootService(std::unique_ptr bs){ + bootService = std::move(bs); + capabilitiesUpdated = true; +} + +BootService *Model::getBootService() const { + return bootService.get(); +} + +void Model::setResetService(std::unique_ptr rs) { + this->resetService = std::move(rs); + capabilitiesUpdated = true; +} + +ResetService *Model::getResetService() const { + return resetService.get(); +} + +#if MO_ENABLE_CERT_MGMT +void Model::setCertificateService(std::unique_ptr cs) { + this->certService = std::move(cs); + capabilitiesUpdated = true; +} + +CertificateService *Model::getCertificateService() const { + return certService.get(); +} +#endif //MO_ENABLE_CERT_MGMT + +#if MO_ENABLE_V201 +void Model::setAvailabilityService(std::unique_ptr as) { + this->availabilityService = std::move(as); + capabilitiesUpdated = true; +} + +AvailabilityService *Model::getAvailabilityService() const { + return availabilityService.get(); +} + +void Model::setVariableService(std::unique_ptr vs) { + this->variableService = std::move(vs); + capabilitiesUpdated = true; +} + +VariableService *Model::getVariableService() const { + return variableService.get(); +} + +void Model::setTransactionService(std::unique_ptr ts) { + this->transactionService = std::move(ts); + capabilitiesUpdated = true; +} + +TransactionService *Model::getTransactionService() const { + return transactionService.get(); +} + +void Model::setResetServiceV201(std::unique_ptr rs) { + this->resetServiceV201 = std::move(rs); + capabilitiesUpdated = true; +} + +Ocpp201::ResetService *Model::getResetServiceV201() const { + return resetServiceV201.get(); +} + +void Model::setMeteringServiceV201(std::unique_ptr rs) { + this->meteringServiceV201 = std::move(rs); + capabilitiesUpdated = true; +} + +Ocpp201::MeteringService *Model::getMeteringServiceV201() const { + return meteringServiceV201.get(); +} + +void Model::setRemoteControlService(std::unique_ptr rs) { + remoteControlService = std::move(rs); + capabilitiesUpdated = true; +} + +RemoteControlService *Model::getRemoteControlService() const { + return remoteControlService.get(); +} +#endif + +Clock& Model::getClock() { + return clock; +} + +const ProtocolVersion& Model::getVersion() const { + return version; +} + +uint16_t Model::getBootNr() { + return bootNr; +} + +void Model::updateSupportedStandardProfiles() { + + auto supportedFeatureProfilesString = + declareConfiguration("SupportedFeatureProfiles", "", CONFIGURATION_VOLATILE, true); + + if (!supportedFeatureProfilesString) { + MO_DBG_ERR("OOM"); + return; + } + + auto buf = makeString(getMemoryTag(), supportedFeatureProfilesString->getString()); + + if (chargeControlCommon && + heartbeatService && + bootService) { + if (!strstr(supportedFeatureProfilesString->getString(), "Core")) { + if (!buf.empty()) buf += ','; + buf += "Core"; + } + } + + if (firmwareService || + diagnosticsService) { + if (!strstr(supportedFeatureProfilesString->getString(), "FirmwareManagement")) { + if (!buf.empty()) buf += ','; + buf += "FirmwareManagement"; + } + } + +#if MO_ENABLE_LOCAL_AUTH + if (authorizationService && authorizationService->localAuthListEnabled()) { + if (!strstr(supportedFeatureProfilesString->getString(), "LocalAuthListManagement")) { + if (!buf.empty()) buf += ','; + buf += "LocalAuthListManagement"; + } + } +#endif //MO_ENABLE_LOCAL_AUTH + +#if MO_ENABLE_RESERVATION + if (reservationService) { + if (!strstr(supportedFeatureProfilesString->getString(), "Reservation")) { + if (!buf.empty()) buf += ','; + buf += "Reservation"; + } + } +#endif //MO_ENABLE_RESERVATION + + if (smartChargingService) { + if (!strstr(supportedFeatureProfilesString->getString(), "SmartCharging")) { + if (!buf.empty()) buf += ','; + buf += "SmartCharging"; + } + } + + if (!strstr(supportedFeatureProfilesString->getString(), "RemoteTrigger")) { + if (!buf.empty()) buf += ','; + buf += "RemoteTrigger"; + } + + supportedFeatureProfilesString->setString(buf.c_str()); + + MO_DBG_DEBUG("supported feature profiles: %s", buf.c_str()); +} diff --git a/src/MicroOcpp/Model/Model.h b/src/MicroOcpp/Model/Model.h new file mode 100644 index 00000000..ff31bdb2 --- /dev/null +++ b/src/MicroOcpp/Model/Model.h @@ -0,0 +1,179 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_MODEL_H +#define MO_MODEL_H + +#include + +#include +#include +#include +#include + +namespace MicroOcpp { + +class TransactionStore; +class SmartChargingService; +class ConnectorsCommon; +class MeteringService; +class FirmwareService; +class DiagnosticsService; +class HeartbeatService; +class BootService; +class ResetService; + +#if MO_ENABLE_LOCAL_AUTH +class AuthorizationService; +#endif //MO_ENABLE_LOCAL_AUTH + +#if MO_ENABLE_RESERVATION +class ReservationService; +#endif //MO_ENABLE_RESERVATION + +#if MO_ENABLE_CERT_MGMT +class CertificateService; +#endif //MO_ENABLE_CERT_MGMT + +#if MO_ENABLE_V201 +class AvailabilityService; +class VariableService; +class TransactionService; +class RemoteControlService; + +namespace Ocpp201 { +class ResetService; +class MeteringService; +} +#endif //MO_ENABLE_V201 + +class Model : public MemoryManaged { +private: + Vector> connectors; + std::unique_ptr transactionStore; + std::unique_ptr smartChargingService; + std::unique_ptr chargeControlCommon; + std::unique_ptr meteringService; + std::unique_ptr firmwareService; + std::unique_ptr diagnosticsService; + std::unique_ptr heartbeatService; + std::unique_ptr bootService; + std::unique_ptr resetService; + +#if MO_ENABLE_LOCAL_AUTH + std::unique_ptr authorizationService; +#endif //MO_ENABLE_LOCAL_AUTH + +#if MO_ENABLE_RESERVATION + std::unique_ptr reservationService; +#endif //MO_ENABLE_RESERVATION + +#if MO_ENABLE_CERT_MGMT + std::unique_ptr certService; +#endif //MO_ENABLE_CERT_MGMT + +#if MO_ENABLE_V201 + std::unique_ptr availabilityService; + std::unique_ptr variableService; + std::unique_ptr transactionService; + std::unique_ptr resetServiceV201; + std::unique_ptr meteringServiceV201; + std::unique_ptr remoteControlService; +#endif + + Clock clock; + + ProtocolVersion version; + + bool capabilitiesUpdated = true; + void updateSupportedStandardProfiles(); + + bool runTasks = false; + + const uint16_t bootNr = 0; //each boot of this lib has a unique number + +public: + Model(ProtocolVersion version = ProtocolVersion(1,6), uint16_t bootNr = 0); + Model(const Model& rhs) = delete; + ~Model(); + + void loop(); + + void activateTasks() {runTasks = true;} + + void setTransactionStore(std::unique_ptr transactionStore); + TransactionStore *getTransactionStore(); + + void setSmartChargingService(std::unique_ptr scs); + SmartChargingService* getSmartChargingService() const; + + void setConnectorsCommon(std::unique_ptr ccs); + ConnectorsCommon *getConnectorsCommon(); + + void setConnectors(Vector>&& connectors); + unsigned int getNumConnectors() const; + Connector *getConnector(unsigned int connectorId); + + void setMeteringSerivce(std::unique_ptr meteringService); + MeteringService* getMeteringService() const; + + void setFirmwareService(std::unique_ptr firmwareService); + FirmwareService *getFirmwareService() const; + + void setDiagnosticsService(std::unique_ptr diagnosticsService); + DiagnosticsService *getDiagnosticsService() const; + + void setHeartbeatService(std::unique_ptr heartbeatService); + +#if MO_ENABLE_LOCAL_AUTH + void setAuthorizationService(std::unique_ptr authorizationService); + AuthorizationService *getAuthorizationService(); +#endif //MO_ENABLE_LOCAL_AUTH + +#if MO_ENABLE_RESERVATION + void setReservationService(std::unique_ptr reservationService); + ReservationService *getReservationService(); +#endif //MO_ENABLE_RESERVATION + + void setBootService(std::unique_ptr bs); + BootService *getBootService() const; + + void setResetService(std::unique_ptr rs); + ResetService *getResetService() const; + +#if MO_ENABLE_CERT_MGMT + void setCertificateService(std::unique_ptr cs); + CertificateService *getCertificateService() const; +#endif //MO_ENABLE_CERT_MGMT + +#if MO_ENABLE_V201 + void setAvailabilityService(std::unique_ptr as); + AvailabilityService *getAvailabilityService() const; + + void setVariableService(std::unique_ptr vs); + VariableService *getVariableService() const; + + void setTransactionService(std::unique_ptr ts); + TransactionService *getTransactionService() const; + + void setResetServiceV201(std::unique_ptr rs); + Ocpp201::ResetService *getResetServiceV201() const; + + void setMeteringServiceV201(std::unique_ptr ms); + Ocpp201::MeteringService *getMeteringServiceV201() const; + + void setRemoteControlService(std::unique_ptr rs); + RemoteControlService *getRemoteControlService() const; +#endif + + Clock &getClock(); + + const ProtocolVersion& getVersion() const; + + uint16_t getBootNr(); +}; + +} //end namespace MicroOcpp + +#endif 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 new file mode 100644 index 00000000..eb856b5d --- /dev/null +++ b/src/MicroOcpp/Model/Reservation/Reservation.cpp @@ -0,0 +1,137 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_RESERVATION + +#include +#include +#include + +using namespace MicroOcpp; + +Reservation::Reservation(Model& model, unsigned int slot) : MemoryManaged("v16.Reservation.Reservation"), model(model), slot(slot) { + + snprintf(connectorIdKey, sizeof(connectorIdKey), MO_RESERVATION_CID_KEY "%u", slot); + connectorIdInt = declareConfiguration(connectorIdKey, -1, RESERVATION_FN, false, false, false); + + snprintf(expiryDateRawKey, sizeof(expiryDateRawKey), MO_RESERVATION_EXPDATE_KEY "%u", slot); + expiryDateRawString = declareConfiguration(expiryDateRawKey, "", RESERVATION_FN, false, false, false); + + snprintf(idTagKey, sizeof(idTagKey), MO_RESERVATION_IDTAG_KEY "%u", slot); + idTagString = declareConfiguration(idTagKey, "", RESERVATION_FN, false, false, false); + + snprintf(reservationIdKey, sizeof(reservationIdKey), MO_RESERVATION_RESID_KEY "%u", slot); + reservationIdInt = declareConfiguration(reservationIdKey, -1, RESERVATION_FN, false, false, false); + + snprintf(parentIdTagKey, sizeof(parentIdTagKey), MO_RESERVATION_PARENTID_KEY "%u", slot); + parentIdTagString = declareConfiguration(parentIdTagKey, "", RESERVATION_FN, false, false, false); + + if (!connectorIdInt || !expiryDateRawString || !idTagString || !reservationIdInt || !parentIdTagString) { + MO_DBG_ERR("initialization failure"); + } +} + +Reservation::~Reservation() { + if (connectorIdInt->getKey() == connectorIdKey) { + connectorIdInt->setKey(nullptr); + } + if (expiryDateRawString->getKey() == expiryDateRawKey) { + expiryDateRawString->setKey(nullptr); + } + if (idTagString->getKey() == idTagKey) { + idTagString->setKey(nullptr); + } + if (reservationIdInt->getKey() == reservationIdKey) { + reservationIdInt->setKey(nullptr); + } + if (parentIdTagString->getKey() == parentIdTagKey) { + parentIdTagString->setKey(nullptr); + } +} + +bool Reservation::isActive() { + if (connectorIdInt->getInt() < 0) { + //reservation invalidated + return false; + } + + if (model.getClock().now() > getExpiryDate()) { + //reservation expired + return false; + } + + return true; +} + +bool Reservation::matches(unsigned int connectorId) { + return (int) connectorId == connectorIdInt->getInt(); +} + +bool Reservation::matches(const char *idTag, const char *parentIdTag) { + if (idTag == nullptr && parentIdTag == nullptr) { + return true; + } + + if (idTag && !strcmp(idTag, idTagString->getString())) { + return true; + } + + if (parentIdTag && !strcmp(parentIdTag, parentIdTagString->getString())) { + return true; + } + + return false; +} + +int Reservation::getConnectorId() { + return connectorIdInt->getInt(); +} + +Timestamp& Reservation::getExpiryDate() { + if (expiryDate == MIN_TIME && *expiryDateRawString->getString()) { + expiryDate.setTime(expiryDateRawString->getString()); + } + return expiryDate; +} + +const char *Reservation::getIdTag() { + return idTagString->getString(); +} + +int Reservation::getReservationId() { + return reservationIdInt->getInt(); +} + +const char *Reservation::getParentIdTag() { + return parentIdTagString->getString(); +} + +void Reservation::update(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag) { + reservationIdInt->setInt(reservationId); + connectorIdInt->setInt((int) connectorId); + this->expiryDate = expiryDate; + char expiryDate_cstr [JSONDATE_LENGTH + 1]; + if (this->expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1)) { + expiryDateRawString->setString(expiryDate_cstr); + } + idTagString->setString(idTag); + parentIdTagString->setString(parentIdTag); + + configuration_save(); +} + +void Reservation::clear() { + connectorIdInt->setInt(-1); + expiryDate = MIN_TIME; + expiryDateRawString->setString(""); + idTagString->setString(""); + reservationIdInt->setInt(-1); + parentIdTagString->setString(""); + + configuration_save(); +} + +#endif //MO_ENABLE_RESERVATION diff --git a/src/MicroOcpp/Model/Reservation/Reservation.h b/src/MicroOcpp/Model/Reservation/Reservation.h new file mode 100644 index 00000000..daae197a --- /dev/null +++ b/src/MicroOcpp/Model/Reservation/Reservation.h @@ -0,0 +1,74 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_RESERVATION_H +#define MO_RESERVATION_H + +#include + +#if MO_ENABLE_RESERVATION + +#include +#include +#include + +#ifndef RESERVATION_FN +#define RESERVATION_FN (MO_FILENAME_PREFIX "reservations.jsn") +#endif + +#define MO_RESERVATION_CID_KEY "cid_" +#define MO_RESERVATION_EXPDATE_KEY "expdt_" +#define MO_RESERVATION_IDTAG_KEY "idt_" +#define MO_RESERVATION_RESID_KEY "rsvid_" +#define MO_RESERVATION_PARENTID_KEY "pidt_" + +namespace MicroOcpp { + +class Model; + +class Reservation : public MemoryManaged { +private: + Model& model; + const unsigned int slot; + + std::shared_ptr connectorIdInt; + char connectorIdKey [sizeof(MO_RESERVATION_CID_KEY "xxx") + 1]; //"xxx" = placeholder for digits + std::shared_ptr expiryDateRawString; + char expiryDateRawKey [sizeof(MO_RESERVATION_EXPDATE_KEY "xxx") + 1]; + + Timestamp expiryDate = MIN_TIME; + std::shared_ptr idTagString; + char idTagKey [sizeof(MO_RESERVATION_IDTAG_KEY "xxx") + 1]; + std::shared_ptr reservationIdInt; + char reservationIdKey [sizeof(MO_RESERVATION_RESID_KEY "xxx") + 1]; + std::shared_ptr parentIdTagString; + char parentIdTagKey [sizeof(MO_RESERVATION_PARENTID_KEY "xxx") + 1]; + +public: + Reservation(Model& model, unsigned int slot); + Reservation(const Reservation&) = delete; + Reservation(Reservation&&) = delete; + Reservation& operator=(const Reservation&) = delete; + + ~Reservation(); + + bool isActive(); //if this object contains a valid, unexpired reservation + + bool matches(unsigned int connectorId); + bool matches(const char *idTag, const char *parentIdTag = nullptr); //idTag == parentIdTag == nullptr -> return True + + int getConnectorId(); + Timestamp& getExpiryDate(); + const char *getIdTag(); + int getReservationId(); + const char *getParentIdTag(); + + void update(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag = nullptr); + void clear(); +}; + +} + +#endif //MO_ENABLE_RESERVATION +#endif diff --git a/src/MicroOcpp/Model/Reservation/ReservationService.cpp b/src/MicroOcpp/Model/Reservation/ReservationService.cpp new file mode 100644 index 00000000..dde39aea --- /dev/null +++ b/src/MicroOcpp/Model/Reservation/ReservationService.cpp @@ -0,0 +1,218 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_RESERVATION + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace MicroOcpp; + +ReservationService::ReservationService(Context& context, unsigned int numConnectors) : MemoryManaged("v16.Reservation.ReservationService"), context(context), maxReservations((int) numConnectors - 1), reservations(makeVector>(getMemoryTag())) { + if (maxReservations > 0) { + reservations.reserve((size_t) maxReservations); + for (int i = 0; i < maxReservations; i++) { + reservations.emplace_back(new Reservation(context.getModel(), i)); + } + } + + reserveConnectorZeroSupportedBool = declareConfiguration("ReserveConnectorZeroSupported", true, CONFIGURATION_VOLATILE, true); + + context.getOperationRegistry().registerOperation("CancelReservation", [this] () { + return new Ocpp16::CancelReservation(*this);}); + context.getOperationRegistry().registerOperation("ReserveNow", [&context] () { + return new Ocpp16::ReserveNow(context.getModel());}); +} + +void ReservationService::loop() { + //check if to end reservations + + for (auto& reservation : reservations) { + if (!reservation->isActive()) { + continue; + } + + if (auto connector = context.getModel().getConnector(reservation->getConnectorId())) { + + //check if connector went inoperative + auto cStatus = connector->getStatus(); + if (cStatus == ChargePointStatus_Faulted || cStatus == ChargePointStatus_Unavailable) { + reservation->clear(); + continue; + } + + //check if other tx started at this connector (e.g. due to RemoteStartTransaction) + if (connector->getTransaction() && connector->getTransaction()->isAuthorized()) { + reservation->clear(); + continue; + } + } + + //check if tx with same idTag or reservationId has started + for (unsigned int cId = 1; cId < context.getModel().getNumConnectors(); cId++) { + auto& transaction = context.getModel().getConnector(cId)->getTransaction(); + if (transaction && transaction->isAuthorized()) { + const char *cIdTag = transaction->getIdTag(); + if (transaction->getReservationId() == reservation->getReservationId() || + (cIdTag && !strcmp(cIdTag, reservation->getIdTag()))) { + + reservation->clear(); + break; + } + } + } + } +} + +Reservation *ReservationService::getReservation(unsigned int connectorId) { + if (connectorId == 0) { + MO_DBG_DEBUG("tried to fetch connectorId 0"); + return nullptr; //cannot fetch for connectorId 0 because multiple reservations are possible at a time + } + + for (auto& reservation : reservations) { + if (reservation->isActive() && reservation->matches(connectorId)) { + return reservation.get(); + } + } + + return nullptr; +} + +Reservation *ReservationService::getReservation(const char *idTag, const char *parentIdTag) { + if (idTag == nullptr) { + MO_DBG_ERR("invalid input"); + return nullptr; + } + + Reservation *connectorReservation = nullptr; + + for (auto& reservation : reservations) { + if (!reservation->isActive()) { + continue; + } + + //TODO check for parentIdTag + + if (reservation->matches(idTag, parentIdTag)) { + if (reservation->getConnectorId() == 0) { + return reservation.get(); //reservation at connectorId 0 has higher priority + } else { + connectorReservation = reservation.get(); + } + } + } + + return connectorReservation; +} + +Reservation *ReservationService::getReservation(unsigned int connectorId, const char *idTag, const char *parentIdTag) { + + //is connector blocked by a reservation? + if (auto reservation = getReservation(connectorId)) { + //connector has reservation -> will always be the prevailing reservation + return reservation; + } + + //is there any reservation at this charge point for idTag? + if (idTag) { + if (auto reservation = getReservation(idTag, parentIdTag)) { + //yes, can use reservation with different connectorId + return reservation; + } + } + + if (reserveConnectorZeroSupportedBool && !reserveConnectorZeroSupportedBool->getBool()) { + //no connectorZero check - all done + MO_DBG_DEBUG("no reservation"); + return nullptr; + } + + //connectorZero check + Reservation *blockingReservation = nullptr; //any reservation which blocks this connector now + + //Check if there are enough free connectors to satisfy all reservations at connectorId 0 + unsigned int unspecifiedReservations = 0; + for (auto& reservation : reservations) { + if (reservation->isActive() && reservation->getConnectorId() == 0) { + unspecifiedReservations++; + blockingReservation = reservation.get(); + } + } + + unsigned int availableCount = 0; + for (unsigned int cId = 1; cId < context.getModel().getNumConnectors(); cId++) { + if (cId == connectorId) { + //don't count this connector + continue; + } + if (auto connector = context.getModel().getConnector(cId)) { + if (connector->getStatus() == ChargePointStatus_Available) { + availableCount++; + } + } + } + + if (availableCount >= unspecifiedReservations) { + //enough other connectors available to satisfy all reservations + return nullptr; + } else { + //not sufficient connectors for all reservations after this action + return blockingReservation; + } +} + +Reservation *ReservationService::getReservationById(int reservationId) { + for (auto& reservation : reservations) { + if (reservation->isActive() && reservation->getReservationId() == reservationId) { + return reservation.get(); + } + } + + return nullptr; +} + +bool ReservationService::updateReservation(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag) { + if (auto reservation = getReservationById(reservationId)) { + if (getReservation(connectorId) && getReservation(connectorId) != reservation && getReservation(connectorId)->isActive()) { + MO_DBG_DEBUG("found blocking reservation at connectorId %u", connectorId); + return false; //cannot transfer reservation to other connector with existing reservation + } + reservation->update(reservationId, connectorId, expiryDate, idTag, parentIdTag); + return true; + } + +// Alternative condition: avoids that one idTag can make two reservations at a time. The specification doesn't +// mention that double-reservations should be possible but it seems to mean it. + if (auto reservation = getReservation(connectorId, nullptr, nullptr)) { +// payload["idTag"], +// payload.containsKey("parentIdTag") ? payload["parentIdTag"] : nullptr)) { +// if (auto reservation = getReservation(payload["connectorId"].as())) { + MO_DBG_DEBUG("found blocking reservation at connectorId %u", reservation->getConnectorId()); + (void)reservation; + return false; + } + + //update free reservation slot + for (auto& reservation : reservations) { + if (!reservation->isActive()) { + reservation->update(reservationId, connectorId, expiryDate, idTag, parentIdTag); + return true; + } + } + + MO_DBG_ERR("error finding blocking reservation"); + return false; +} + +#endif //MO_ENABLE_RESERVATION diff --git a/src/MicroOcpp/Model/Reservation/ReservationService.h b/src/MicroOcpp/Model/Reservation/ReservationService.h new file mode 100644 index 00000000..367f1f59 --- /dev/null +++ b/src/MicroOcpp/Model/Reservation/ReservationService.h @@ -0,0 +1,53 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_RESERVATIONSERVICE_H +#define MO_RESERVATIONSERVICE_H + +#include + +#if MO_ENABLE_RESERVATION + +#include +#include + +#include + +namespace MicroOcpp { + +class Context; + +class ReservationService : public MemoryManaged { +private: + Context& context; + + const int maxReservations; // = number of physical connectors + Vector> reservations; + + std::shared_ptr reserveConnectorZeroSupportedBool; + +public: + ReservationService(Context& context, unsigned int numConnectors); + + void loop(); + + Reservation *getReservation(unsigned int connectorId); //by connectorId + Reservation *getReservation(const char *idTag, const char *parentIdTag = nullptr); //by idTag + + /* + * Get prevailing reservation for a charging session authorized by idTag/parentIdTag at connectorId. + * returns nullptr if there is no reservation in question + * returns a reservation if applicable. Caller must check if idTag/parentIdTag match before starting a transaction + */ + Reservation *getReservation(unsigned int connectorId, const char *idTag, const char *parentIdtag = nullptr); + + Reservation *getReservationById(int reservationId); + + bool updateReservation(int reservationId, unsigned int connectorId, Timestamp expiryDate, const char *idTag, const char *parentIdTag = nullptr); +}; + +} + +#endif //MO_ENABLE_RESERVATION +#endif 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 new file mode 100644 index 00000000..2eef02b8 --- /dev/null +++ b/src/MicroOcpp/Model/Reset/ResetService.cpp @@ -0,0 +1,333 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include + +#ifndef MO_RESET_DELAY +#define MO_RESET_DELAY 10000 +#endif + +using namespace MicroOcpp; + +ResetService::ResetService(Context& context) + : MemoryManaged("v16.Reset.ResetService"), context(context) { + + resetRetriesInt = declareConfiguration("ResetRetries", 2); + registerConfigurationValidator("ResetRetries", VALIDATE_UNSIGNED_INT); + + context.getOperationRegistry().registerOperation("Reset", [&context] () { + return new Ocpp16::Reset(context.getModel());}); +} + +ResetService::~ResetService() { + +} + +void ResetService::loop() { + + if (outstandingResetRetries > 0 && mocpp_tick_ms() - t_resetRetry >= MO_RESET_DELAY) { + t_resetRetry = mocpp_tick_ms(); + outstandingResetRetries--; + if (executeReset) { + MO_DBG_INFO("Reset device"); + executeReset(isHardReset); + } else { + MO_DBG_ERR("No Reset function set! Abort"); + outstandingResetRetries = 0; + } + + if (outstandingResetRetries <= 0) { + + MO_DBG_ERR("Reset device failure. Abort"); + + ChargePointStatus cpStatus = ChargePointStatus_UNDEFINED; + if (context.getModel().getNumConnectors() > 0) { + cpStatus = context.getModel().getConnector(0)->getStatus(); + } + + auto statusNotification = makeRequest(new Ocpp16::StatusNotification( + 0, + cpStatus, //will be determined in StatusNotification::initiate + context.getModel().getClock().now(), + "ResetFailure")); + statusNotification->setTimeout(60000); + context.initiateRequest(std::move(statusNotification)); + } + } +} + +void ResetService::setPreReset(std::function preReset) { + this->preReset = preReset; +} + +std::function ResetService::getPreReset() { + return this->preReset; +} + +void ResetService::setExecuteReset(std::function executeReset) { + this->executeReset = executeReset; +} + +std::function ResetService::getExecuteReset() { + return this->executeReset; +} + +void ResetService::initiateReset(bool isHard) { + isHardReset = isHard; + outstandingResetRetries = 1 + resetRetriesInt->getInt(); //one initial try + no. of retries + if (outstandingResetRetries > 5) { + MO_DBG_ERR("no. of reset trials exceeds 5"); + outstandingResetRetries = 5; + } + t_resetRetry = mocpp_tick_ms(); +} + +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) +std::function MicroOcpp::makeDefaultResetFn() { + return [] (bool isHard) { + MO_DBG_DEBUG("Perform ESP reset"); + ESP.restart(); + }; +} +#endif //MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) + +#if MO_ENABLE_V201 +namespace MicroOcpp { +namespace Ocpp201 { + +ResetService::ResetService(Context& context) + : MemoryManaged("v201.Reset.ResetService"), context(context), evses(makeVector(getMemoryTag())) { + + auto varService = context.getModel().getVariableService(); + resetRetriesInt = varService->declareVariable("OCPPCommCtrlr", "ResetRetries", 0); + + context.getOperationRegistry().registerOperation("Reset", [this] () { + return new Ocpp201::Reset(*this);}); +} + +ResetService::~ResetService() { + +} + +ResetService::Evse::Evse(Context& context, ResetService& resetService, unsigned int evseId) : context(context), resetService(resetService), evseId(evseId) { + auto varService = context.getModel().getVariableService(); + varService->declareVariable(ComponentId("EVSE", evseId >= 1 ? evseId : -1), "AllowReset", true, Variable::Mutability::ReadOnly, false); +} + +void ResetService::Evse::loop() { + + if (outstandingResetRetries && awaitTxStop) { + + for (unsigned int eId = std::max(1U, evseId); eId < (evseId == 0 ? MO_NUM_EVSEID : evseId + 1); eId++) { + //If evseId > 0, execute this block one time for evseId. If evseId == 0, then iterate over all evseIds > 0 + + auto txService = context.getModel().getTransactionService(); + if (txService && txService->getEvse(eId) && txService->getEvse(eId)->getTransaction()) { + auto tx = txService->getEvse(eId)->getTransaction(); + + if (!tx->stopped) { + // wait until tx stopped + return; + } + } + } + + awaitTxStop = false; + + MO_DBG_INFO("Reset - tx stopped"); + t_resetRetry = mocpp_tick_ms(); // wait for some more time until final reset + } + + if (outstandingResetRetries && mocpp_tick_ms() - t_resetRetry >= MO_RESET_DELAY) { + t_resetRetry = mocpp_tick_ms(); + outstandingResetRetries--; + + MO_DBG_INFO("Reset device"); + + bool success = executeReset(); + + if (success) { + outstandingResetRetries = 0; + + if (evseId != 0) { + //Set this EVSE Available again + if (auto connector = context.getModel().getConnector(evseId)) { + connector->setAvailabilityVolatile(true); + } + } + } else if (!outstandingResetRetries) { + MO_DBG_ERR("Reset device failure"); + + if (evseId == 0) { + //Set all EVSEs Available again + for (unsigned int cId = 0; cId < context.getModel().getNumConnectors(); cId++) { + auto connector = context.getModel().getConnector(cId); + connector->setAvailabilityVolatile(true); + } + } else { + //Set only this EVSE Available + if (auto connector = context.getModel().getConnector(evseId)) { + connector->setAvailabilityVolatile(true); + } + } + } + } +} + +ResetService::Evse *ResetService::getEvse(unsigned int evseId) { + for (size_t i = 0; i < evses.size(); i++) { + if (evses[i].evseId == evseId) { + return &evses[i]; + } + } + return nullptr; +} + +ResetService::Evse *ResetService::getOrCreateEvse(unsigned int evseId) { + if (auto evse = getEvse(evseId)) { + return evse; + } + + if (evseId >= MO_NUM_EVSEID) { + MO_DBG_ERR("evseId out of bound"); + return nullptr; + } + + evses.emplace_back(context, *this, evseId); + return &evses.back(); +} + +void ResetService::loop() { + for (Evse& evse : evses) { + evse.loop(); + } +} + +void ResetService::setNotifyReset(std::function notifyReset, unsigned int evseId) { + Evse *evse = getOrCreateEvse(evseId); + if (!evse) { + MO_DBG_ERR("evseId not found"); + return; + } + evse->notifyReset = notifyReset; +} + +std::function ResetService::getNotifyReset(unsigned int evseId) { + Evse *evse = getOrCreateEvse(evseId); + if (!evse) { + MO_DBG_ERR("evseId not found"); + return nullptr; + } + return evse->notifyReset; +} + +void ResetService::setExecuteReset(std::function executeReset, unsigned int evseId) { + Evse *evse = getOrCreateEvse(evseId); + if (!evse) { + MO_DBG_ERR("evseId not found"); + return; + } + evse->executeReset = executeReset; +} + +std::function ResetService::getExecuteReset(unsigned int evseId) { + Evse *evse = getOrCreateEvse(evseId); + if (!evse) { + MO_DBG_ERR("evseId not found"); + return nullptr; + } + return evse->executeReset; +} + +ResetStatus ResetService::initiateReset(ResetType type, unsigned int evseId) { + auto evse = getEvse(evseId); + if (!evse) { + MO_DBG_ERR("evseId not found"); + return ResetStatus_Rejected; + } + + if (!evse->executeReset) { + MO_DBG_INFO("EVSE %u does not support Reset", evseId); + return ResetStatus_Rejected; + } + + //Check if EVSEs are ready for Reset + for (unsigned int eId = evseId; eId < (evseId == 0 ? MO_NUM_EVSEID : evseId + 1); eId++) { + //If evseId > 0, execute this block one time for evseId. If evseId == 0, then iterate over all evseIds + + if (auto it = getEvse(eId)) { + if (it->notifyReset && !it->notifyReset(type)) { + MO_DBG_INFO("EVSE %u not able to Reset", evseId); + return ResetStatus_Rejected; + } + } + } + + //Set EVSEs Unavailable + if (evseId == 0) { + //Set all EVSEs Unavailable + for (unsigned int cId = 0; cId < context.getModel().getNumConnectors(); cId++) { + auto connector = context.getModel().getConnector(cId); + connector->setAvailabilityVolatile(false); + } + } else { + //Set this EVSE Unavailable + if (auto connector = context.getModel().getConnector(evseId)) { + connector->setAvailabilityVolatile(false); + } + } + + bool scheduled = false; + + //Tx-related behavior: if immediate Reset, stop txs; otherwise schedule Reset + for (unsigned int eId = std::max(1U, evseId); eId < (evseId == 0 ? MO_NUM_EVSEID : evseId + 1); eId++) { + //If evseId > 0, execute this block one time for evseId. If evseId == 0, then iterate over all evseIds > 0 + + auto txService = context.getModel().getTransactionService(); + if (txService && txService->getEvse(eId) && txService->getEvse(eId)->getTransaction()) { + auto tx = txService->getEvse(eId)->getTransaction(); + if (tx->active) { + //Tx in progress. Check behavior + if (type == ResetType_Immediate) { + txService->getEvse(eId)->abortTransaction(Transaction::StoppedReason::ImmediateReset, TransactionEventTriggerReason::ResetCommand); + } else { + scheduled = true; + break; + } + } + } + } + + //Actually engage Reset + + if (resetRetriesInt->getInt() >= 5) { + MO_DBG_ERR("no. of reset trials exceeds 5"); + evse->outstandingResetRetries = 5; + } else { + evse->outstandingResetRetries = 1 + resetRetriesInt->getInt(); //one initial try + no. of retries + } + evse->t_resetRetry = mocpp_tick_ms(); + evse->awaitTxStop = scheduled; + + return scheduled ? ResetStatus_Scheduled : ResetStatus_Accepted; +} + +} //namespace MicroOcpp +} //namespace Ocpp201 +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Reset/ResetService.h b/src/MicroOcpp/Model/Reset/ResetService.h new file mode 100644 index 00000000..a84e82d6 --- /dev/null +++ b/src/MicroOcpp/Model/Reset/ResetService.h @@ -0,0 +1,114 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_RESETSERVICE_H +#define MO_RESETSERVICE_H + +#include + +#include +#include +#include +#include + +namespace MicroOcpp { + +class Context; + +class ResetService : public MemoryManaged { +private: + Context& context; + + std::function preReset; //true: reset is possible; false: reject reset; Await: need more time to determine + std::function executeReset; //please disconnect WebSocket (MO remains initialized), shut down device and restart with normal initialization routine; on failure reconnect WebSocket + unsigned int outstandingResetRetries = 0; //0 = do not reset device + bool isHardReset = false; + unsigned long t_resetRetry; + + std::shared_ptr resetRetriesInt; + +public: + ResetService(Context& context); + + ~ResetService(); + + void loop(); + + void setPreReset(std::function preReset); + std::function getPreReset(); + + void setExecuteReset(std::function executeReset); + std::function getExecuteReset(); + + void initiateReset(bool isHard); +}; + +} //end namespace MicroOcpp + +#if MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) + +namespace MicroOcpp { + +std::function makeDefaultResetFn(); + +} + +#endif //MO_PLATFORM == MO_PLATFORM_ARDUINO && (defined(ESP32) || defined(ESP8266)) + +#if MO_ENABLE_V201 + +namespace MicroOcpp { + +class Variable; + +namespace Ocpp201 { + +class ResetService : public MemoryManaged { +private: + Context& context; + + struct Evse { + Context& context; + ResetService& resetService; + const unsigned int evseId; + + std::function notifyReset; //notify firmware about a Reset command. Return true if Reset is okay; false if Reset cannot be executed + std::function executeReset; //execute Reset of connector. Return true if Reset will be executed; false if there is a failure to Reset + + unsigned int outstandingResetRetries = 0; //0 = do not reset device + unsigned long t_resetRetry; + + bool awaitTxStop = false; + + Evse(Context& context, ResetService& resetService, unsigned int evseId); + + void loop(); + }; + + Vector evses; + Evse *getEvse(unsigned int connectorId); + Evse *getOrCreateEvse(unsigned int connectorId); + + Variable *resetRetriesInt = nullptr; + +public: + ResetService(Context& context); + ~ResetService(); + + void loop(); + + void setNotifyReset(std::function notifyReset, unsigned int evseId = 0); + std::function getNotifyReset(unsigned int evseId = 0); + + void setExecuteReset(std::function executeReset, unsigned int evseId = 0); + std::function getExecuteReset(unsigned int evseId = 0); + + ResetStatus initiateReset(ResetType type, unsigned int evseId = 0); +}; + +} //namespace Ocpp201 +} //namespace MicroOcpp +#endif //MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp new file mode 100644 index 00000000..3c71d539 --- /dev/null +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.cpp @@ -0,0 +1,514 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include + +#include +#include + +using namespace MicroOcpp; + +ChargeRate MicroOcpp::chargeRate_min(const ChargeRate& a, const ChargeRate& b) { + ChargeRate res; + res.power = std::min(a.power, b.power); + res.current = std::min(a.current, b.current); + res.nphases = std::min(a.nphases, b.nphases); + return res; +} + +ChargingSchedule::ChargingSchedule() : MemoryManaged("v16.SmartCharging.SmartChargingModel"), chargingSchedulePeriod{makeVector(getMemoryTag())} { + +} + +bool ChargingSchedule::calculateLimit(const Timestamp &t, const Timestamp &startOfCharging, ChargeRate& limit, Timestamp& nextChange) { + Timestamp basis = Timestamp(); //point in time to which schedule-related times are relative + switch (chargingProfileKind) { + case (ChargingProfileKindType::Absolute): + //check if schedule is not valid yet but begins in future + if (startSchedule > t) { + //not valid YET + nextChange = std::min(nextChange, startSchedule); + return false; + } + //If charging profile is absolute, prefer startSchedule as basis. If absent, use chargingStart instead. If absent, no + //behaviour is defined + if (startSchedule > MIN_TIME) { + basis = startSchedule; + } else if (startOfCharging > MIN_TIME && startOfCharging < t) { + basis = startOfCharging; + } else { + MO_DBG_ERR("Absolute profile, but neither startSchedule, nor start of charging are set. Undefined behavior, abort"); + return false; + } + break; + case (ChargingProfileKindType::Recurring): + if (recurrencyKind == RecurrencyKindType::Daily) { + basis = t - ((t - startSchedule) % (24 * 3600)); + nextChange = std::min(nextChange, basis + (24 * 3600)); //constrain nextChange to basis + one day + } else if (recurrencyKind == RecurrencyKindType::Weekly) { + basis = t - ((t - startSchedule) % (7 * 24 * 3600)); + nextChange = std::min(nextChange, basis + (7 * 24 * 3600)); + } else { + MO_DBG_ERR("Recurring ChargingProfile but no RecurrencyKindType set. Undefined behavior, assume 'Daily'"); + basis = t - ((t - startSchedule) % (24 * 3600)); + nextChange = std::min(nextChange, basis + (24 * 3600)); + } + break; + case (ChargingProfileKindType::Relative): + //assumed, that it is relative to start of charging + //start of charging must be before t or equal to t + if (startOfCharging > t) { + //Relative charging profiles only work with a currently active charging session which is not the case here + return false; + } + basis = startOfCharging; + break; + } + + if (t < basis) { //check for error + MO_DBG_ERR("time basis is smaller than t, but t must be >= basis"); + return false; + } + + int t_toBasis = t - basis; + + if (duration > 0){ + //duration is set + + //check if duration is exceeded and if yes, abort limit algorithm + //if no, the duration is an upper limit for the validity of the schedule + if (t_toBasis >= duration) { //"duration" is given relative to basis + return false; + } else { + nextChange = std::min(nextChange, basis + duration); + } + } + + /* + * Work through the ChargingProfilePeriods here. If the right period was found, assign the limit parameter from it + * and make nextChange equal the beginning of the following period. If the right period is the last one, nextChange + * will remain the time determined before. + */ + float limit_res = -1.0f; //If limit_res is still -1 after the loop, the limit algorithm failed + int nphases_res = -1; + for (auto period = chargingSchedulePeriod.begin(); period != chargingSchedulePeriod.end(); period++) { + if (period->startPeriod > t_toBasis) { + // found the first period that comes after t_toBasis. + nextChange = basis + period->startPeriod; + nextChange = std::min(nextChange, basis + period->startPeriod); + break; //The currently valid limit was set the iteration before + } + limit_res = period->limit; + nphases_res = period->numberPhases; + } + + if (limit_res >= 0.0f) { + limit_res = std::max(limit_res, minChargingRate); + + if (chargingRateUnit == ChargingRateUnitType::Amp) { + limit.current = limit_res; + } else { + limit.power = limit_res; + } + + limit.nphases = nphases_res; + return true; + } else { + return false; //No limit was found. Either there is no ChargingProfilePeriod, or each period begins after t_toBasis + } +} + +bool ChargingSchedule::toJson(JsonDoc& doc) { + size_t capacity = 0; + capacity += JSON_OBJECT_SIZE(5); //no of fields of ChargingSchedule + capacity += JSONDATE_LENGTH + 1; //startSchedule + capacity += JSON_ARRAY_SIZE(chargingSchedulePeriod.size()) + chargingSchedulePeriod.size() * JSON_OBJECT_SIZE(3); + + doc = initJsonDoc("v16.SmartCharging.ChargingSchedule", capacity); + if (duration >= 0) { + doc["duration"] = duration; + } + char startScheduleJson [JSONDATE_LENGTH + 1] = {'\0'}; + startSchedule.toJsonString(startScheduleJson, JSONDATE_LENGTH + 1); + doc["startSchedule"] = startScheduleJson; + doc["chargingRateUnit"] = chargingRateUnit == (ChargingRateUnitType::Amp) ? "A" : "W"; + JsonArray periodArray = doc.createNestedArray("chargingSchedulePeriod"); + for (auto period = chargingSchedulePeriod.begin(); period != chargingSchedulePeriod.end(); period++) { + JsonObject entry = periodArray.createNestedObject(); + entry["startPeriod"] = period->startPeriod; + entry["limit"] = period->limit; + if (period->numberPhases != 3) { + entry["numberPhases"] = period->numberPhases; + } + } + if (minChargingRate >= 0) { + doc["minChargeRate"] = minChargingRate; + } + + return true; +} + +void ChargingSchedule::printSchedule(){ + + char tmp[JSONDATE_LENGTH + 1] = {'\0'}; + startSchedule.toJsonString(tmp, JSONDATE_LENGTH + 1); + + MO_CONSOLE_PRINTF(" > CHARGING SCHEDULE:\n" \ + " > duration: %i\n" \ + " > startSchedule: %s\n" \ + " > chargingRateUnit: %s\n" \ + " > minChargingRate: %f\n", + duration, + tmp, + chargingRateUnit == (ChargingRateUnitType::Amp) ? "A" : + chargingRateUnit == (ChargingRateUnitType::Watt) ? "W" : "Error", + minChargingRate); + + for (auto period = chargingSchedulePeriod.begin(); period != chargingSchedulePeriod.end(); period++) { + MO_CONSOLE_PRINTF(" > CHARGING SCHEDULE PERIOD:\n" \ + " > startPeriod: %i\n" \ + " > limit: %f\n" \ + " > numberPhases: %i\n", + period->startPeriod, + period->limit, + period->numberPhases); + } +} + +ChargingProfile::ChargingProfile() : MemoryManaged("v16.SmartCharging.ChargingProfile") { + +} + +bool ChargingProfile::calculateLimit(const Timestamp &t, const Timestamp &startOfCharging, ChargeRate& limit, Timestamp& nextChange){ + if (t > validTo && validTo > MIN_TIME) { + return false; //no limit defined + } + if (t < validFrom) { + nextChange = std::min(nextChange, validFrom); + return false; //no limit defined + } + + return chargingSchedule.calculateLimit(t, startOfCharging, limit, nextChange); +} + +bool ChargingProfile::calculateLimit(const Timestamp &t, ChargeRate& limit, Timestamp& nextChange){ + return calculateLimit(t, MIN_TIME, limit, nextChange); +} + +int ChargingProfile::getChargingProfileId() { + return chargingProfileId; +} + +int ChargingProfile::getTransactionId() { + return transactionId; +} + +int ChargingProfile::getStackLevel(){ + return stackLevel; +} + +ChargingProfilePurposeType ChargingProfile::getChargingProfilePurpose(){ + return chargingProfilePurpose; +} + +bool ChargingProfile::toJson(JsonDoc& doc) { + + auto chargingScheduleDoc = initJsonDoc("v16.SmartCharging.ChargingSchedule"); + if (!chargingSchedule.toJson(chargingScheduleDoc)) { + return false; + } + + doc = initJsonDoc("v16.SmartCharging.ChargingProfile", + JSON_OBJECT_SIZE(9) + //no. of fields in ChargingProfile + 2 * (JSONDATE_LENGTH + 1) + //validFrom and validTo + chargingScheduleDoc.memoryUsage()); //nested JSON object + + doc["chargingProfileId"] = chargingProfileId; + if (transactionId >= 0) { + doc["transactionId"] = transactionId; + } + doc["stackLevel"] = stackLevel; + + switch (chargingProfilePurpose) { + case (ChargingProfilePurposeType::ChargePointMaxProfile): + doc["chargingProfilePurpose"] = "ChargePointMaxProfile"; + break; + case (ChargingProfilePurposeType::TxDefaultProfile): + doc["chargingProfilePurpose"] = "TxDefaultProfile"; + break; + case (ChargingProfilePurposeType::TxProfile): + doc["chargingProfilePurpose"] = "TxProfile"; + break; + } + + switch (chargingProfileKind) { + case (ChargingProfileKindType::Absolute): + doc["chargingProfileKind"] = "Absolute"; + break; + case (ChargingProfileKindType::Recurring): + doc["chargingProfileKind"] = "Recurring"; + break; + case (ChargingProfileKindType::Relative): + doc["chargingProfileKind"] = "Relative"; + break; + } + + switch (recurrencyKind) { + case (RecurrencyKindType::Daily): + doc["recurrencyKind"] = "Daily"; + break; + case (RecurrencyKindType::Weekly): + doc["recurrencyKind"] = "Weekly"; + break; + default: + break; + } + + char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; + + if (validFrom > MIN_TIME) { + if (!validFrom.toJsonString(timeStr, JSONDATE_LENGTH + 1)) { + MO_DBG_ERR("serialization error"); + return false; + } + doc["validFrom"] = timeStr; + } + + if (validTo > MIN_TIME) { + if (!validTo.toJsonString(timeStr, JSONDATE_LENGTH + 1)) { + MO_DBG_ERR("serialization error"); + return false; + } + doc["validTo"] = timeStr; + } + + doc["chargingSchedule"] = chargingScheduleDoc; + + return true; +} + +void ChargingProfile::printProfile(){ + + char tmp[JSONDATE_LENGTH + 1] = {'\0'}; + validFrom.toJsonString(tmp, JSONDATE_LENGTH + 1); + char tmp2[JSONDATE_LENGTH + 1] = {'\0'}; + validTo.toJsonString(tmp2, JSONDATE_LENGTH + 1); + + MO_CONSOLE_PRINTF(" > CHARGING PROFILE:\n" \ + " > chargingProfileId: %i\n" \ + " > transactionId: %i\n" \ + " > stackLevel: %i\n" \ + " > chargingProfilePurpose: %s\n", + chargingProfileId, + transactionId, + stackLevel, + chargingProfilePurpose == (ChargingProfilePurposeType::ChargePointMaxProfile) ? "ChargePointMaxProfile" : + chargingProfilePurpose == (ChargingProfilePurposeType::TxDefaultProfile) ? "TxDefaultProfile" : + chargingProfilePurpose == (ChargingProfilePurposeType::TxProfile) ? "TxProfile" : "Error" + ); + MO_CONSOLE_PRINTF( + " > chargingProfileKind: %s\n" \ + " > recurrencyKind: %s\n" \ + " > validFrom: %s\n" \ + " > validTo: %s\n", + chargingProfileKind == (ChargingProfileKindType::Absolute) ? "Absolute" : + chargingProfileKind == (ChargingProfileKindType::Recurring) ? "Recurring" : + chargingProfileKind == (ChargingProfileKindType::Relative) ? "Relative" : "Error", + recurrencyKind == (RecurrencyKindType::Daily) ? "Daily" : + recurrencyKind == (RecurrencyKindType::Weekly) ? "Weekly" : + recurrencyKind == (RecurrencyKindType::NOT_SET) ? "NOT_SET" : "Error", + tmp, + tmp2 + ); + + chargingSchedule.printSchedule(); +} + +namespace MicroOcpp { + +bool loadChargingSchedulePeriod(JsonObject& json, ChargingSchedulePeriod& out) { + int startPeriod = json["startPeriod"] | -1; + if (startPeriod >= 0) { + out.startPeriod = startPeriod; + } else { + MO_DBG_WARN("format violation"); + return false; + } + + float limit = json["limit"] | -1.f; + if (limit >= 0.f) { + out.limit = limit; + } else { + MO_DBG_WARN("format violation"); + return false; + } + + if (json.containsKey("numberPhases")) { + int numberPhases = json["numberPhases"]; + if (numberPhases >= 0 && numberPhases <= 3) { + out.numberPhases = numberPhases; + } else { + MO_DBG_WARN("format violation"); + return false; + } + } + + return true; +} + +} //end namespace MicroOcpp + +std::unique_ptr MicroOcpp::loadChargingProfile(JsonObject& json) { + auto res = std::unique_ptr(new ChargingProfile()); + + int chargingProfileId = json["chargingProfileId"] | -1; + if (chargingProfileId >= 0) { + res->chargingProfileId = chargingProfileId; + } else { + MO_DBG_WARN("format violation"); + return nullptr; + } + + int transactionId = json["transactionId"] | -1; + if (transactionId >= 0) { + res->transactionId = transactionId; + } + + int stackLevel = json["stackLevel"] | -1; + if (stackLevel >= 0 && stackLevel <= MO_ChargeProfileMaxStackLevel) { + res->stackLevel = stackLevel; + } else { + MO_DBG_WARN("format violation"); + return nullptr; + } + + const char *chargingProfilePurposeStr = json["chargingProfilePurpose"] | "Invalid"; + if (!strcmp(chargingProfilePurposeStr, "ChargePointMaxProfile")) { + res->chargingProfilePurpose = ChargingProfilePurposeType::ChargePointMaxProfile; + } else if (!strcmp(chargingProfilePurposeStr, "TxDefaultProfile")) { + res->chargingProfilePurpose = ChargingProfilePurposeType::TxDefaultProfile; + } else if (!strcmp(chargingProfilePurposeStr, "TxProfile")) { + res->chargingProfilePurpose = ChargingProfilePurposeType::TxProfile; + } else { + MO_DBG_WARN("format violation"); + return nullptr; + } + + const char *chargingProfileKindStr = json["chargingProfileKind"] | "Invalid"; + if (!strcmp(chargingProfileKindStr, "Absolute")) { + res->chargingProfileKind = ChargingProfileKindType::Absolute; + } else if (!strcmp(chargingProfileKindStr, "Recurring")) { + res->chargingProfileKind = ChargingProfileKindType::Recurring; + } else if (!strcmp(chargingProfileKindStr, "Relative")) { + res->chargingProfileKind = ChargingProfileKindType::Relative; + } else { + MO_DBG_WARN("format violation"); + return nullptr; + } + + const char *recurrencyKindStr = json["recurrencyKind"] | "Invalid"; + if (!strcmp(recurrencyKindStr, "Daily")) { + res->recurrencyKind = RecurrencyKindType::Daily; + } else if (!strcmp(recurrencyKindStr, "Weekly")) { + res->recurrencyKind = RecurrencyKindType::Weekly; + } + + MO_DBG_DEBUG("Deserialize JSON: chargingProfileId=%i, chargingProfilePurpose=%s, recurrencyKind=%s", chargingProfileId, chargingProfilePurposeStr, recurrencyKindStr); + + if (json.containsKey("validFrom")) { + if (!res->validFrom.setTime(json["validFrom"] | "Invalid")) { + //non-success + MO_DBG_WARN("datetime format violation, expect format like 2022-02-01T20:53:32.486Z"); + return nullptr; + } + } else { + res->validFrom = MIN_TIME; + } + + if (json.containsKey("validTo")) { + if (!res->validTo.setTime(json["validTo"] | "Invalid")) { + //non-success + MO_DBG_WARN("datetime format violation, expect format like 2022-02-01T20:53:32.486Z"); + return nullptr; + } + } else { + res->validTo = MIN_TIME; + } + + JsonObject scheduleJson = json["chargingSchedule"]; + ChargingSchedule& schedule = res->chargingSchedule; + auto success = loadChargingSchedule(scheduleJson, schedule); + if (!success) { + return nullptr; + } + + //duplicate some fields to chargingSchedule to simplify the max charge rate calculation + schedule.chargingProfileKind = res->chargingProfileKind; + schedule.recurrencyKind = res->recurrencyKind; + + return res; +} + +bool MicroOcpp::loadChargingSchedule(JsonObject& json, ChargingSchedule& out) { + if (json.containsKey("duration")) { + int duration = json["duration"] | -1; + if (duration >= 0) { + out.duration = duration; + } else { + MO_DBG_WARN("format violation"); + return false; + } + } + + if (json.containsKey("startSchedule")) { + if (!out.startSchedule.setTime(json["startSchedule"] | "Invalid")) { + //non-success + MO_DBG_WARN("datetime format violation, expect format like 2022-02-01T20:53:32.486Z"); + return false; + } + } else { + out.startSchedule = MIN_TIME; + } + + const char *unit = json["chargingRateUnit"] | "_Undefined"; + if (unit[0] == 'a' || unit[0] == 'A') { + out.chargingRateUnit = ChargingRateUnitType::Amp; + } else if (unit[0] == 'w' || unit[0] == 'W') { + out.chargingRateUnit = ChargingRateUnitType::Watt; + } else { + MO_DBG_WARN("format violation"); + return false; + } + + JsonArray periodJsonArray = json["chargingSchedulePeriod"]; + if (periodJsonArray.size() < 1) { + MO_DBG_WARN("format violation"); + return false; + } + + if (periodJsonArray.size() > MO_ChargingScheduleMaxPeriods) { + MO_DBG_WARN("exceed ChargingScheduleMaxPeriods"); + return false; + } + + for (JsonObject periodJson : periodJsonArray) { + out.chargingSchedulePeriod.emplace_back(); + if (!loadChargingSchedulePeriod(periodJson, out.chargingSchedulePeriod.back())) { + return false; + } + } + + if (json.containsKey("minChargingRate")) { + float minChargingRate = json["minChargingRate"]; + if (minChargingRate >= 0.f) { + out.minChargingRate = minChargingRate; + } else { + MO_DBG_WARN("format violation"); + return false; + } + } + + return true; +} diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.h b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.h new file mode 100644 index 00000000..825d6866 --- /dev/null +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingModel.h @@ -0,0 +1,158 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef SMARTCHARGINGMODEL_H +#define SMARTCHARGINGMODEL_H + +#ifndef MO_ChargeProfileMaxStackLevel +#define MO_ChargeProfileMaxStackLevel 8 +#endif + +#ifndef MO_ChargingScheduleMaxPeriods +#define MO_ChargingScheduleMaxPeriods 24 +#endif + +#ifndef MO_MaxChargingProfilesInstalled +#define MO_MaxChargingProfilesInstalled 10 +#endif + +#include +#include + +#include + +#include +#include + +namespace MicroOcpp { + +enum class ChargingProfilePurposeType { + ChargePointMaxProfile, + TxDefaultProfile, + TxProfile +}; + +enum class ChargingProfileKindType { + Absolute, + Recurring, + Relative +}; + +enum class RecurrencyKindType { + NOT_SET, //not part of OCPP 1.6 + Daily, + Weekly +}; + +enum class ChargingRateUnitType { + Watt, + Amp +}; + +struct ChargeRate { + float power = std::numeric_limits::max(); + float current = std::numeric_limits::max(); + int nphases = std::numeric_limits::max(); + + bool operator==(const ChargeRate& rhs) { + return power == rhs.power && + current == rhs.current && + nphases == rhs.nphases; + } + bool operator!=(const ChargeRate& rhs) { + return !(*this == rhs); + } +}; + +//returns a new vector with the minimum of each component +ChargeRate chargeRate_min(const ChargeRate& a, const ChargeRate& b); + +class ChargingSchedulePeriod { +public: + int startPeriod; + float limit; + int numberPhases = 3; +}; + +class ChargingSchedule : public MemoryManaged { +public: + int duration = -1; + Timestamp startSchedule; + ChargingRateUnitType chargingRateUnit; + Vector chargingSchedulePeriod; + float minChargingRate = -1.0f; + + ChargingProfileKindType chargingProfileKind; //copied from ChargingProfile to increase cohesion of limit algorithms + RecurrencyKindType recurrencyKind = RecurrencyKindType::NOT_SET; //copied from ChargingProfile to increase cohesion of limit algorithms + + ChargingSchedule(); + + /** + * limit: output parameter + * nextChange: output parameter + * + * returns if charging profile defines a limit at time t + * if true, limit and nextChange will be set according to this Schedule + * if false, only nextChange will be set + */ + bool calculateLimit(const Timestamp &t, const Timestamp &startOfCharging, ChargeRate& limit, Timestamp& nextChange); + + bool toJson(JsonDoc& out); + + /* + * print on console + */ + void printSchedule(); +}; + +class ChargingProfile : public MemoryManaged { +public: + int chargingProfileId = -1; + int transactionId = -1; + int stackLevel = 0; + ChargingProfilePurposeType chargingProfilePurpose {ChargingProfilePurposeType::TxProfile}; + ChargingProfileKindType chargingProfileKind {ChargingProfileKindType::Relative}; //copied to ChargingSchedule to increase cohesion of limit algorithms + RecurrencyKindType recurrencyKind {RecurrencyKindType::NOT_SET}; // copied to ChargingSchedule to increase cohesion + Timestamp validFrom; + Timestamp validTo; + ChargingSchedule chargingSchedule; + + ChargingProfile(); + + /** + * limit: output parameter + * nextChange: output parameter + * + * returns if charging profile defines a limit at time t + * if true, limit and nextChange will be set according to this Schedule + * if false, only nextChange will be set + */ + bool calculateLimit(const Timestamp &t, const Timestamp &startOfCharging, ChargeRate& limit, Timestamp& nextChange); + + /* + * Simpler function if startOfCharging is not available. Caution: This likely will differ from function with startOfCharging + */ + bool calculateLimit(const Timestamp &t, ChargeRate& limit, Timestamp& nextChange); + + int getChargingProfileId(); + int getTransactionId(); + int getStackLevel(); + + ChargingProfilePurposeType getChargingProfilePurpose(); + + bool toJson(JsonDoc& out); + + /* + * print on console + */ + void printProfile(); +}; + +std::unique_ptr loadChargingProfile(JsonObject& json); + +bool loadChargingSchedule(JsonObject& json, ChargingSchedule& out); + +} //end namespace MicroOcpp + +#endif diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp new file mode 100644 index 00000000..762bd6ac --- /dev/null +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.cpp @@ -0,0 +1,770 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace::MicroOcpp; + +SmartChargingConnector::SmartChargingConnector(Model& model, std::shared_ptr filesystem, unsigned int connectorId, ProfileStack& ChargePointMaxProfile, ProfileStack& ChargePointTxDefaultProfile) : + MemoryManaged("v16.SmartCharging.SmartChargingConnector"), model(model), filesystem{filesystem}, connectorId{connectorId}, ChargePointMaxProfile(ChargePointMaxProfile), ChargePointTxDefaultProfile(ChargePointTxDefaultProfile) { + +} + +SmartChargingConnector::~SmartChargingConnector() { + +} + +/* + * limitOut: the calculated maximum charge rate at the moment, or the default value if no limit is defined + * validToOut: The begin of the next SmartCharging restriction after time t + */ +void SmartChargingConnector::calculateLimit(const Timestamp &t, ChargeRate& limitOut, Timestamp& validToOut) { + + //initialize output parameters with the default values + limitOut = ChargeRate(); + validToOut = MAX_TIME; + + bool txLimitDefined = false; + ChargeRate txLimit; + + //first, check if TxProfile is defined and limits charging + for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { + if (TxProfile[i] && ((trackTxRmtProfileId >= 0 && trackTxRmtProfileId == TxProfile[i]->getChargingProfileId()) || + TxProfile[i]->getTransactionId() < 0 || + trackTxId == TxProfile[i]->getTransactionId())) { + ChargeRate crOut; + bool defined = TxProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); + if (defined) { + txLimitDefined = true; + txLimit = crOut; + break; + } + } + } + + //if no TxProfile limits charging, check the TxDefaultProfiles for this connector + if (!txLimitDefined && trackTxStart < MAX_TIME) { + for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { + if (TxDefaultProfile[i]) { + ChargeRate crOut; + bool defined = TxDefaultProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); + if (defined) { + txLimitDefined = true; + txLimit = crOut; + break; + } + } + } + } + + //if no appropriate TxDefaultProfile is set for this connector, search in the general TxDefaultProfiles + if (!txLimitDefined && trackTxStart < MAX_TIME) { + for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { + if (ChargePointTxDefaultProfile[i]) { + ChargeRate crOut; + bool defined = ChargePointTxDefaultProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); + if (defined) { + txLimitDefined = true; + txLimit = crOut; + break; + } + } + } + } + + ChargeRate cpLimit; + + //the calculated maximum charge rate is also limited by the ChargePointMaxProfiles + for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { + if (ChargePointMaxProfile[i]) { + ChargeRate crOut; + bool defined = ChargePointMaxProfile[i]->calculateLimit(t, trackTxStart, crOut, validToOut); + if (defined) { + cpLimit = crOut; + break; + } + } + } + + //apply ChargePointMaxProfile value to calculated limit + limitOut = chargeRate_min(txLimit, cpLimit); +} + +void SmartChargingConnector::trackTransaction() { + + Transaction *tx = nullptr; + if (model.getConnector(connectorId)) { + tx = model.getConnector(connectorId)->getTransaction().get(); + } + + bool update = false; + + if (tx) { + if (tx->getTxProfileId() != trackTxRmtProfileId) { + update = true; + trackTxRmtProfileId = tx->getTxProfileId(); + } + if (tx->getStartSync().isRequested() && tx->getStartTimestamp() != trackTxStart) { + update = true; + trackTxStart = tx->getStartTimestamp(); + } + if (tx->getStartSync().isConfirmed() && tx->getTransactionId() != trackTxId) { + update = true; + trackTxId = tx->getTransactionId(); + } + } else { + //check if transaction has just been completed + if (trackTxRmtProfileId >= 0 || trackTxStart < MAX_TIME || trackTxId >= 0) { + //yes, clear data + update = true; + trackTxRmtProfileId = -1; + trackTxStart = MAX_TIME; + trackTxId = -1; + + clearChargingProfile([this] (int, int connectorId, ChargingProfilePurposeType purpose, int) { + return purpose == ChargingProfilePurposeType::TxProfile && + (int) this->connectorId == connectorId; + }); + } + } + + if (update) { + nextChange = model.getClock().now(); //will refresh limit calculation + } +} + +void SmartChargingConnector::loop(){ + + trackTransaction(); + + /** + * check if to call onLimitChange + */ + auto& tnow = model.getClock().now(); + + if (tnow >= nextChange){ + + ChargeRate limit; + nextChange = MAX_TIME; //reset nextChange to default value and refresh it + + calculateLimit(tnow, limit, nextChange); + +#if MO_DBG_LEVEL >= MO_DL_INFO + { + char timestamp1[JSONDATE_LENGTH + 1] = {'\0'}; + tnow.toJsonString(timestamp1, JSONDATE_LENGTH + 1); + char timestamp2[JSONDATE_LENGTH + 1] = {'\0'}; + nextChange.toJsonString(timestamp2, JSONDATE_LENGTH + 1); + MO_DBG_INFO("New limit for connector %u, scheduled at = %s, nextChange = %s, limit = {%.1f, %.1f, %i}", + connectorId, + timestamp1, timestamp2, + limit.power != std::numeric_limits::max() ? limit.power : -1.f, + limit.current != std::numeric_limits::max() ? limit.current : -1.f, + limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); + } +#endif + + if (trackLimitOutput != limit) { + if (limitOutput) { + + limitOutput( + limit.power != std::numeric_limits::max() ? limit.power : -1.f, + limit.current != std::numeric_limits::max() ? limit.current : -1.f, + limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); + trackLimitOutput = limit; + } + } + } +} + +void SmartChargingConnector::setSmartChargingOutput(std::function limitOutput) { + if (this->limitOutput) { + MO_DBG_WARN("replacing existing SmartChargingOutput"); + } + this->limitOutput = limitOutput; +} + +ChargingProfile *SmartChargingConnector::updateProfiles(std::unique_ptr chargingProfile) { + + int stackLevel = chargingProfile->getStackLevel(); //already validated + + switch (chargingProfile->getChargingProfilePurpose()) { + case (ChargingProfilePurposeType::ChargePointMaxProfile): + break; + case (ChargingProfilePurposeType::TxDefaultProfile): + TxDefaultProfile[stackLevel] = std::move(chargingProfile); + return TxDefaultProfile[stackLevel].get(); + case (ChargingProfilePurposeType::TxProfile): + TxProfile[stackLevel] = std::move(chargingProfile); + return TxProfile[stackLevel].get(); + } + + MO_DBG_ERR("invalid args"); + return nullptr; +} + +void SmartChargingConnector::notifyProfilesUpdated() { + nextChange = model.getClock().now(); +} + +bool SmartChargingConnector::clearChargingProfile(const std::function filter) { + bool found = false; + + ProfileStack *profileStacks [] = {&TxProfile, &TxDefaultProfile}; + + for (auto stack : profileStacks) { + for (size_t iLevel = 0; iLevel < stack->size(); iLevel++) { + if (auto& profile = stack->at(iLevel)) { + if (profile && filter(profile->getChargingProfileId(), connectorId, profile->getChargingProfilePurpose(), iLevel)) { + found = true; + SmartChargingServiceUtils::removeProfile(filesystem, connectorId, profile->getChargingProfilePurpose(), iLevel); + profile.reset(); + } + } + } + } + + return found; +} + +std::unique_ptr SmartChargingConnector::getCompositeSchedule(int duration, ChargingRateUnitType_Optional unit) { + + auto& startSchedule = model.getClock().now(); + + auto schedule = std::unique_ptr(new ChargingSchedule()); + schedule->duration = duration; + schedule->startSchedule = startSchedule; + schedule->chargingProfileKind = ChargingProfileKindType::Absolute; + schedule->recurrencyKind = RecurrencyKindType::NOT_SET; + + auto& periods = schedule->chargingSchedulePeriod; + + Timestamp periodBegin = Timestamp(startSchedule); + Timestamp periodStop = Timestamp(startSchedule); + + while (periodBegin - startSchedule < duration && periods.size() < MO_ChargingScheduleMaxPeriods) { + + //calculate limit + ChargeRate limit; + calculateLimit(periodBegin, limit, periodStop); + + //if the unit is still unspecified, guess by taking the unit of the first limit + if (unit == ChargingRateUnitType_Optional::None) { + if (limit.power < limit.current) { + unit = ChargingRateUnitType_Optional::Watt; + } else { + unit = ChargingRateUnitType_Optional::Amp; + } + } + + periods.emplace_back(); + float limit_opt = unit == ChargingRateUnitType_Optional::Watt ? limit.power : limit.current; + periods.back().limit = limit_opt != std::numeric_limits::max() ? limit_opt : -1.f, + periods.back().numberPhases = limit.nphases != std::numeric_limits::max() ? limit.nphases : -1; + periods.back().startPeriod = periodBegin - startSchedule; + + periodBegin = periodStop; + } + + if (unit == ChargingRateUnitType_Optional::Watt) { + schedule->chargingRateUnit = ChargingRateUnitType::Watt; + } else { + schedule->chargingRateUnit = ChargingRateUnitType::Amp; + } + + return schedule; +} + +size_t SmartChargingConnector::getChargingProfilesCount() { + size_t chargingProfilesCount = 0; + for (size_t i = 0; i < MO_ChargeProfileMaxStackLevel + 1; i++) { + if (TxDefaultProfile[i]) { + chargingProfilesCount++; + } + if (TxProfile[i]) { + chargingProfilesCount++; + } + } + return chargingProfilesCount; +} + +SmartChargingConnector *SmartChargingService::getScConnectorById(unsigned int connectorId) { + if (connectorId == 0) { + return nullptr; + } + + if (connectorId - 1 >= connectors.size()) { + return nullptr; + } + + return &connectors[connectorId-1]; +} + +SmartChargingService::SmartChargingService(Context& context, std::shared_ptr filesystem, unsigned int numConnectors) + : MemoryManaged("v16.SmartCharging.SmartChargingService"), context(context), filesystem{filesystem}, connectors{makeVector(getMemoryTag())}, numConnectors(numConnectors) { + + for (unsigned int cId = 1; cId < numConnectors; cId++) { + connectors.emplace_back(context.getModel(), filesystem, cId, ChargePointMaxProfile, ChargePointTxDefaultProfile); + } + + declareConfiguration("ChargeProfileMaxStackLevel", MO_ChargeProfileMaxStackLevel, CONFIGURATION_VOLATILE, true); + declareConfiguration("ChargingScheduleAllowedChargingRateUnit", "", CONFIGURATION_VOLATILE, true); + declareConfiguration("ChargingScheduleMaxPeriods", MO_ChargingScheduleMaxPeriods, CONFIGURATION_VOLATILE, true); + declareConfiguration("MaxChargingProfilesInstalled", MO_MaxChargingProfilesInstalled, CONFIGURATION_VOLATILE, true); + + context.getOperationRegistry().registerOperation("ClearChargingProfile", [this] () { + return new Ocpp16::ClearChargingProfile(*this);}); + context.getOperationRegistry().registerOperation("GetCompositeSchedule", [&context, this] () { + return new Ocpp16::GetCompositeSchedule(context.getModel(), *this);}); + context.getOperationRegistry().registerOperation("SetChargingProfile", [&context, this] () { + return new Ocpp16::SetChargingProfile(context.getModel(), *this);}); + + loadProfiles(); +} + +SmartChargingService::~SmartChargingService() { + +} + +ChargingProfile *SmartChargingService::updateProfiles(unsigned int connectorId, std::unique_ptr chargingProfile){ + + if ((connectorId > 0 && !getScConnectorById(connectorId)) || !chargingProfile) { + MO_DBG_ERR("invalid args"); + return nullptr; + } + + if (MO_DBG_LEVEL >= MO_DL_VERBOSE) { + MO_DBG_VERBOSE("Charging Profile internal model:"); + chargingProfile->printProfile(); + } + + int stackLevel = chargingProfile->getStackLevel(); + if (stackLevel< 0 || stackLevel >= MO_ChargeProfileMaxStackLevel + 1) { + MO_DBG_ERR("input validation failed"); + return nullptr; + } + + size_t chargingProfilesCount = 0; + for (size_t i = 0; i < MO_ChargeProfileMaxStackLevel + 1; i++) { + if (ChargePointTxDefaultProfile[i]) { + chargingProfilesCount++; + } + if (ChargePointMaxProfile[i]) { + chargingProfilesCount++; + } + } + for (size_t i = 0; i < connectors.size(); i++) { + chargingProfilesCount += connectors[i].getChargingProfilesCount(); + } + + if (chargingProfilesCount >= MO_MaxChargingProfilesInstalled) { + MO_DBG_WARN("number of maximum charging profiles exceeded"); + return nullptr; + } + + ChargingProfile *res = nullptr; + + switch (chargingProfile->getChargingProfilePurpose()) { + case (ChargingProfilePurposeType::ChargePointMaxProfile): + if (connectorId != 0) { + MO_DBG_WARN("invalid charging profile"); + return nullptr; + } + ChargePointMaxProfile[stackLevel] = std::move(chargingProfile); + res = ChargePointMaxProfile[stackLevel].get(); + break; + case (ChargingProfilePurposeType::TxDefaultProfile): + if (connectorId == 0) { + ChargePointTxDefaultProfile[stackLevel] = std::move(chargingProfile); + res = ChargePointTxDefaultProfile[stackLevel].get(); + } else { + res = getScConnectorById(connectorId)->updateProfiles(std::move(chargingProfile)); + } + break; + case (ChargingProfilePurposeType::TxProfile): + if (connectorId == 0) { + MO_DBG_WARN("invalid charging profile"); + return nullptr; + } else { + res = getScConnectorById(connectorId)->updateProfiles(std::move(chargingProfile)); + } + break; + } + + /** + * Invalidate the last limit by setting the nextChange to now. By the next loop()-call, the limit + * and nextChange will be recalculated and onLimitChanged will be called. + */ + if (res) { + nextChange = context.getModel().getClock().now(); + for (size_t i = 0; i < connectors.size(); i++) { + connectors[i].notifyProfilesUpdated(); + } + } + + return res; +} + +bool SmartChargingService::loadProfiles() { + + bool success = true; + + if (!filesystem) { + MO_DBG_DEBUG("no filesystem"); + return true; //not an error + } + + ChargingProfilePurposeType purposes[] = {ChargingProfilePurposeType::ChargePointMaxProfile, ChargingProfilePurposeType::TxDefaultProfile, ChargingProfilePurposeType::TxProfile}; + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + + for (auto purpose : purposes) { + for (unsigned int cId = 0; cId < numConnectors; cId++) { + if (cId > 0 && purpose == ChargingProfilePurposeType::ChargePointMaxProfile) { + continue; + } + for (unsigned int iLevel = 0; iLevel < MO_ChargeProfileMaxStackLevel; iLevel++) { + + if (!SmartChargingServiceUtils::printProfileFileName(fn, MO_MAX_PATH_SIZE, cId, purpose, iLevel)) { + return false; + } + + size_t msize = 0; + if (filesystem->stat(fn, &msize) != 0) { + continue; //There is not a profile on the stack iStack with stacklevel iLevel. Normal case, just continue. + } + + auto profileDoc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); + if (!profileDoc) { + success = false; + MO_DBG_ERR("profile corrupt: %s, remove", fn); + filesystem->remove(fn); + continue; + } + + JsonObject profileJson = profileDoc->as(); + auto chargingProfile = loadChargingProfile(profileJson); + + bool valid = false; + if (chargingProfile) { + valid = updateProfiles(cId, std::move(chargingProfile)); + } + if (!valid) { + success = false; + MO_DBG_ERR("profile corrupt: %s, remove", fn); + filesystem->remove(fn); + } + } + } + } + + return success; +} + +/* + * limitOut: the calculated maximum charge rate at the moment, or the default value if no limit is defined + * validToOut: The begin of the next SmartCharging restriction after time t + */ +void SmartChargingService::calculateLimit(const Timestamp &t, ChargeRate& limitOut, Timestamp& validToOut){ + + //initialize output parameters with the default values + limitOut = ChargeRate(); + validToOut = MAX_TIME; + + //get ChargePointMaxProfile with the highest stackLevel + for (int i = MO_ChargeProfileMaxStackLevel; i >= 0; i--) { + if (ChargePointMaxProfile[i]) { + ChargeRate crOut; + bool defined = ChargePointMaxProfile[i]->calculateLimit(t, crOut, validToOut); + if (defined) { + limitOut = crOut; + break; + } + } + } +} + +void SmartChargingService::loop(){ + + for (size_t i = 0; i < connectors.size(); i++) { + connectors[i].loop(); + } + + /** + * check if to call onLimitChange + */ + auto& tnow = context.getModel().getClock().now(); + + if (tnow >= nextChange){ + + ChargeRate limit; + nextChange = MAX_TIME; //reset nextChange to default value and refresh it + + calculateLimit(tnow, limit, nextChange); + +#if MO_DBG_LEVEL >= MO_DL_INFO + { + char timestamp1[JSONDATE_LENGTH + 1] = {'\0'}; + tnow.toJsonString(timestamp1, JSONDATE_LENGTH + 1); + char timestamp2[JSONDATE_LENGTH + 1] = {'\0'}; + nextChange.toJsonString(timestamp2, JSONDATE_LENGTH + 1); + MO_DBG_INFO("New limit for connector %u, scheduled at = %s, nextChange = %s, limit = {%.1f, %.1f, %i}", + 0, + timestamp1, timestamp2, + limit.power != std::numeric_limits::max() ? limit.power : -1.f, + limit.current != std::numeric_limits::max() ? limit.current : -1.f, + limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); + } +#endif + + if (trackLimitOutput != limit) { + if (limitOutput) { + + limitOutput( + limit.power != std::numeric_limits::max() ? limit.power : -1.f, + limit.current != std::numeric_limits::max() ? limit.current : -1.f, + limit.nphases != std::numeric_limits::max() ? limit.nphases : -1); + trackLimitOutput = limit; + } + } + } +} + +void SmartChargingService::setSmartChargingOutput(unsigned int connectorId, std::function limitOutput) { + if ((connectorId > 0 && !getScConnectorById(connectorId))) { + MO_DBG_ERR("invalid args"); + return; + } + + if (connectorId == 0) { + if (this->limitOutput) { + MO_DBG_WARN("replacing existing SmartChargingOutput"); + } + this->limitOutput = limitOutput; + } else { + getScConnectorById(connectorId)->setSmartChargingOutput(limitOutput); + } +} + +void SmartChargingService::updateAllowedChargingRateUnit(bool powerSupported, bool currentSupported) { + if ((powerSupported != this->powerSupported) || (currentSupported != this->currentSupported)) { + + auto chargingScheduleAllowedChargingRateUnitString = declareConfiguration("ChargingScheduleAllowedChargingRateUnit", "", CONFIGURATION_VOLATILE); + if (chargingScheduleAllowedChargingRateUnitString) { + if (powerSupported && currentSupported) { + chargingScheduleAllowedChargingRateUnitString->setString("Current,Power"); + } else if (powerSupported) { + chargingScheduleAllowedChargingRateUnitString->setString("Power"); + } else if (currentSupported) { + chargingScheduleAllowedChargingRateUnitString->setString("Current"); + } + } + + this->powerSupported = powerSupported; + this->currentSupported = currentSupported; + } +} + +bool SmartChargingService::setChargingProfile(unsigned int connectorId, std::unique_ptr chargingProfile) { + + if ((connectorId > 0 && !getScConnectorById(connectorId)) || !chargingProfile) { + MO_DBG_ERR("invalid args"); + return false; + } + + if ((!currentSupported && chargingProfile->chargingSchedule.chargingRateUnit == ChargingRateUnitType::Amp) || + (!powerSupported && chargingProfile->chargingSchedule.chargingRateUnit == ChargingRateUnitType::Watt)) { + MO_DBG_WARN("unsupported charge rate unit"); + return false; + } + + int chargingProfileId = chargingProfile->getChargingProfileId(); + clearChargingProfile([chargingProfileId] (int id, int, ChargingProfilePurposeType, int) { + return id == chargingProfileId; + }); + + bool success = false; + + auto profilePtr = updateProfiles(connectorId, std::move(chargingProfile)); + + if (profilePtr) { + success = SmartChargingServiceUtils::storeProfile(filesystem, connectorId, profilePtr); + + if (!success) { + clearChargingProfile([chargingProfileId] (int id, int, ChargingProfilePurposeType, int) { + return id == chargingProfileId; + }); + } + } + + return success; +} + +bool SmartChargingService::clearChargingProfile(std::function filter) { + bool found = false; + + for (size_t cId = 0; cId < connectors.size(); cId++) { + found |= connectors[cId].clearChargingProfile(filter); + } + + ProfileStack *profileStacks [] = {&ChargePointMaxProfile, &ChargePointTxDefaultProfile}; + + for (auto stack : profileStacks) { + for (size_t iLevel = 0; iLevel < stack->size(); iLevel++) { + if (auto& profile = stack->at(iLevel)) { + if (filter(profile->getChargingProfileId(), 0, profile->getChargingProfilePurpose(), iLevel)) { + found = true; + SmartChargingServiceUtils::removeProfile(filesystem, 0, profile->getChargingProfilePurpose(), iLevel); + profile.reset(); + } + } + } + } + + /** + * Invalidate the last limit by setting the nextChange to now. By the next loop()-call, the limit + * and nextChange will be recalculated and onLimitChanged will be called. + */ + nextChange = context.getModel().getClock().now(); + for (size_t i = 0; i < connectors.size(); i++) { + connectors[i].notifyProfilesUpdated(); + } + + return found; +} + +std::unique_ptr SmartChargingService::getCompositeSchedule(unsigned int connectorId, int duration, ChargingRateUnitType_Optional unit) { + + if (connectorId > 0 && !getScConnectorById(connectorId)) { + MO_DBG_ERR("invalid args"); + return nullptr; + } + + if (unit == ChargingRateUnitType_Optional::None) { + if (powerSupported && !currentSupported) { + unit = ChargingRateUnitType_Optional::Watt; + } else if (!powerSupported && currentSupported) { + unit = ChargingRateUnitType_Optional::Amp; + } + } + + if (connectorId > 0) { + return getScConnectorById(connectorId)->getCompositeSchedule(duration, unit); + } + + auto& startSchedule = context.getModel().getClock().now(); + + auto schedule = std::unique_ptr(new ChargingSchedule()); + schedule->duration = duration; + schedule->startSchedule = startSchedule; + schedule->chargingProfileKind = ChargingProfileKindType::Absolute; + schedule->recurrencyKind = RecurrencyKindType::NOT_SET; + + auto& periods = schedule->chargingSchedulePeriod; + + Timestamp periodBegin = Timestamp(startSchedule); + Timestamp periodStop = Timestamp(startSchedule); + + while (periodBegin - startSchedule < duration && periods.size() < MO_ChargingScheduleMaxPeriods) { + + //calculate limit + ChargeRate limit; + calculateLimit(periodBegin, limit, periodStop); + + //if the unit is still unspecified, guess by taking the unit of the first limit + if (unit == ChargingRateUnitType_Optional::None) { + if (limit.power < limit.current) { + unit = ChargingRateUnitType_Optional::Watt; + } else { + unit = ChargingRateUnitType_Optional::Amp; + } + } + + periods.push_back(ChargingSchedulePeriod()); + float limit_opt = unit == ChargingRateUnitType_Optional::Watt ? limit.power : limit.current; + periods.back().limit = limit_opt != std::numeric_limits::max() ? limit_opt : -1.f; + periods.back().numberPhases = limit.nphases != std::numeric_limits::max() ? limit.nphases : -1; + periods.back().startPeriod = periodBegin - startSchedule; + + periodBegin = periodStop; + } + + if (unit == ChargingRateUnitType_Optional::Watt) { + schedule->chargingRateUnit = ChargingRateUnitType::Watt; + } else { + schedule->chargingRateUnit = ChargingRateUnitType::Amp; + } + + return schedule; +} + +bool SmartChargingServiceUtils::printProfileFileName(char *out, size_t bufsize, unsigned int connectorId, ChargingProfilePurposeType purpose, unsigned int stackLevel) { + int pret = 0; + + switch (purpose) { + case (ChargingProfilePurposeType::ChargePointMaxProfile): + pret = snprintf(out, bufsize, MO_FILENAME_PREFIX "sc-cm-%u.jsn", stackLevel); + break; + case (ChargingProfilePurposeType::TxDefaultProfile): + pret = snprintf(out, bufsize, MO_FILENAME_PREFIX "sc-td-%u-%u.jsn", connectorId, stackLevel); + break; + case (ChargingProfilePurposeType::TxProfile): + pret = snprintf(out, bufsize, MO_FILENAME_PREFIX "sc-tx-%u-%u.jsn", connectorId, stackLevel); + break; + } + + if (pret < 0 || (size_t) pret >= bufsize) { + MO_DBG_ERR("fn error: %i", pret); + return false; + } + + return true; +} + +bool SmartChargingServiceUtils::storeProfile(std::shared_ptr filesystem, unsigned int connectorId, ChargingProfile *chargingProfile) { + + if (!filesystem) { + MO_DBG_DEBUG("no filesystem"); + return true; //not an error + } + + auto chargingProfileJson = initJsonDoc("v16.SmartCharging.ChargingProfile"); + if (!chargingProfile->toJson(chargingProfileJson)) { + return false; + } + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + + if (!printProfileFileName(fn, MO_MAX_PATH_SIZE, connectorId, chargingProfile->getChargingProfilePurpose(), chargingProfile->getStackLevel())) { + return false; + } + + return FilesystemUtils::storeJson(filesystem, fn, chargingProfileJson); +} + +bool SmartChargingServiceUtils::removeProfile(std::shared_ptr filesystem, unsigned int connectorId, ChargingProfilePurposeType purpose, unsigned int stackLevel) { + + if (!filesystem) { + return false; + } + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + + if (!printProfileFileName(fn, MO_MAX_PATH_SIZE, connectorId, purpose, stackLevel)) { + return false; + } + + return filesystem->remove(fn); +} + + diff --git a/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h new file mode 100644 index 00000000..57952a72 --- /dev/null +++ b/src/MicroOcpp/Model/SmartCharging/SmartChargingService.h @@ -0,0 +1,124 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef SMARTCHARGINGSERVICE_H +#define SMARTCHARGINGSERVICE_H + +#include +#include + +#include + +#include +#include +#include +#include + +namespace MicroOcpp { + +enum class ChargingRateUnitType_Optional { + Watt, + Amp, + None +}; + +class Context; +class Model; + +using ProfileStack = std::array, MO_ChargeProfileMaxStackLevel + 1>; + +class SmartChargingConnector : public MemoryManaged { +private: + Model& model; + std::shared_ptr filesystem; + const unsigned int connectorId; + + ProfileStack& ChargePointMaxProfile; + ProfileStack& ChargePointTxDefaultProfile; + ProfileStack TxDefaultProfile; + ProfileStack TxProfile; + + std::function limitOutput; + + int trackTxRmtProfileId = -1; //optional Charging Profile ID when tx is started via RemoteStartTx + Timestamp trackTxStart = MAX_TIME; //time basis for relative profiles + int trackTxId = -1; //transactionId assigned by OCPP server + + Timestamp nextChange = MIN_TIME; + + ChargeRate trackLimitOutput; + + void calculateLimit(const Timestamp &t, ChargeRate& limitOut, Timestamp& validToOut); + + void trackTransaction(); + +public: + SmartChargingConnector(Model& model, std::shared_ptr filesystem, unsigned int connectorId, ProfileStack& ChargePointMaxProfile, ProfileStack& ChargePointTxDefaultProfile); + SmartChargingConnector(SmartChargingConnector&&) = default; + ~SmartChargingConnector(); + + void loop(); + + void setSmartChargingOutput(std::function limitOutput); //read maximum Watt x Amps x numberPhases + + ChargingProfile *updateProfiles(std::unique_ptr chargingProfile); + + void notifyProfilesUpdated(); + + bool clearChargingProfile(std::function filter); + + std::unique_ptr getCompositeSchedule(int duration, ChargingRateUnitType_Optional unit); + + size_t getChargingProfilesCount(); +}; + +class SmartChargingService : public MemoryManaged { +private: + Context& context; + std::shared_ptr filesystem; + Vector connectors; //connectorId 0 excluded + SmartChargingConnector *getScConnectorById(unsigned int connectorId); + unsigned int numConnectors; //connectorId 0 included + + ProfileStack ChargePointMaxProfile; + ProfileStack ChargePointTxDefaultProfile; + + std::function limitOutput; + ChargeRate trackLimitOutput; + bool powerSupported = false; + bool currentSupported = false; + + Timestamp nextChange = MIN_TIME; + + ChargingProfile *updateProfiles(unsigned int connectorId, std::unique_ptr chargingProfile); + bool loadProfiles(); + + void calculateLimit(const Timestamp &t, ChargeRate& limitOut, Timestamp& validToOut); + +public: + SmartChargingService(Context& context, std::shared_ptr filesystem, unsigned int numConnectors); + ~SmartChargingService(); + + void loop(); + + void setSmartChargingOutput(unsigned int connectorId, std::function limitOutput); //read maximum Watt x Amps x numberPhases + void updateAllowedChargingRateUnit(bool powerSupported, bool currentSupported); //set supported measurand of SmartChargingOutput + + bool setChargingProfile(unsigned int connectorId, std::unique_ptr chargingProfile); + + bool clearChargingProfile(std::function filter); + + std::unique_ptr getCompositeSchedule(unsigned int connectorId, int duration, ChargingRateUnitType_Optional unit = ChargingRateUnitType_Optional::None); +}; + +//filesystem-related helper functions +namespace SmartChargingServiceUtils { +bool printProfileFileName(char *out, size_t bufsize, unsigned int connectorId, ChargingProfilePurposeType purpose, unsigned int stackLevel); +bool storeProfile(std::shared_ptr filesystem, unsigned int connectorId, ChargingProfile *chargingProfile); +bool removeProfile(std::shared_ptr filesystem, unsigned int connectorId, ChargingProfilePurposeType purpose, unsigned int stackLevel); +} + +} //end namespace MicroOcpp + +#endif diff --git a/src/MicroOcpp/Model/Transactions/Transaction.cpp b/src/MicroOcpp/Model/Transactions/Transaction.cpp new file mode 100644 index 00000000..a53b68f1 --- /dev/null +++ b/src/MicroOcpp/Model/Transactions/Transaction.cpp @@ -0,0 +1,534 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include + +using namespace MicroOcpp; + +bool Transaction::setIdTag(const char *idTag) { + auto ret = snprintf(this->idTag, IDTAG_LEN_MAX + 1, "%s", idTag); + return ret >= 0 && ret < IDTAG_LEN_MAX + 1; +} + +bool Transaction::setParentIdTag(const char *idTag) { + auto ret = snprintf(this->parentIdTag, IDTAG_LEN_MAX + 1, "%s", idTag); + return ret >= 0 && ret < IDTAG_LEN_MAX + 1; +} + +bool Transaction::setStopIdTag(const char *idTag) { + auto ret = snprintf(stop_idTag, IDTAG_LEN_MAX + 1, "%s", idTag); + return ret >= 0 && ret < IDTAG_LEN_MAX + 1; +} + +bool Transaction::setStopReason(const char *reason) { + auto ret = snprintf(stop_reason, REASON_LEN_MAX + 1, "%s", reason); + return ret >= 0 && ret < REASON_LEN_MAX + 1; +} + +bool Transaction::commit() { + return context.commit(this); +} + +#if MO_ENABLE_V201 + +namespace MicroOcpp { +namespace Ocpp201 { + +const char *serializeTransactionStoppedReason(Transaction::StoppedReason stoppedReason) { + const char *stoppedReasonCstr = nullptr; + switch (stoppedReason) { + case Transaction::StoppedReason::UNDEFINED: + // optional, okay + break; + case Transaction::StoppedReason::Local: + stoppedReasonCstr = "Local"; + break; + case Transaction::StoppedReason::DeAuthorized: + stoppedReasonCstr = "DeAuthorized"; + break; + case Transaction::StoppedReason::EmergencyStop: + stoppedReasonCstr = "EmergencyStop"; + break; + case Transaction::StoppedReason::EnergyLimitReached: + stoppedReasonCstr = "EnergyLimitReached"; + break; + case Transaction::StoppedReason::EVDisconnected: + stoppedReasonCstr = "EVDisconnected"; + break; + case Transaction::StoppedReason::GroundFault: + stoppedReasonCstr = "GroundFault"; + break; + case Transaction::StoppedReason::ImmediateReset: + stoppedReasonCstr = "ImmediateReset"; + break; + case Transaction::StoppedReason::LocalOutOfCredit: + stoppedReasonCstr = "LocalOutOfCredit"; + break; + case Transaction::StoppedReason::MasterPass: + stoppedReasonCstr = "MasterPass"; + break; + case Transaction::StoppedReason::Other: + stoppedReasonCstr = "Other"; + break; + case Transaction::StoppedReason::OvercurrentFault: + stoppedReasonCstr = "OvercurrentFault"; + break; + case Transaction::StoppedReason::PowerLoss: + stoppedReasonCstr = "PowerLoss"; + break; + case Transaction::StoppedReason::PowerQuality: + stoppedReasonCstr = "PowerQuality"; + break; + case Transaction::StoppedReason::Reboot: + stoppedReasonCstr = "Reboot"; + break; + case Transaction::StoppedReason::Remote: + stoppedReasonCstr = "Remote"; + break; + case Transaction::StoppedReason::SOCLimitReached: + stoppedReasonCstr = "SOCLimitReached"; + break; + case Transaction::StoppedReason::StoppedByEV: + stoppedReasonCstr = "StoppedByEV"; + break; + case Transaction::StoppedReason::TimeLimitReached: + stoppedReasonCstr = "TimeLimitReached"; + break; + case Transaction::StoppedReason::Timeout: + stoppedReasonCstr = "Timeout"; + break; + } + + return stoppedReasonCstr; +} +bool deserializeTransactionStoppedReason(const char *stoppedReasonCstr, Transaction::StoppedReason& stoppedReasonOut) { + if (!stoppedReasonCstr || !*stoppedReasonCstr) { + stoppedReasonOut = Transaction::StoppedReason::UNDEFINED; + } else if (!strcmp(stoppedReasonCstr, "DeAuthorized")) { + stoppedReasonOut = Transaction::StoppedReason::DeAuthorized; + } else if (!strcmp(stoppedReasonCstr, "EmergencyStop")) { + stoppedReasonOut = Transaction::StoppedReason::EmergencyStop; + } else if (!strcmp(stoppedReasonCstr, "EnergyLimitReached")) { + stoppedReasonOut = Transaction::StoppedReason::EnergyLimitReached; + } else if (!strcmp(stoppedReasonCstr, "EVDisconnected")) { + stoppedReasonOut = Transaction::StoppedReason::EVDisconnected; + } else if (!strcmp(stoppedReasonCstr, "GroundFault")) { + stoppedReasonOut = Transaction::StoppedReason::GroundFault; + } else if (!strcmp(stoppedReasonCstr, "ImmediateReset")) { + stoppedReasonOut = Transaction::StoppedReason::ImmediateReset; + } else if (!strcmp(stoppedReasonCstr, "Local")) { + stoppedReasonOut = Transaction::StoppedReason::Local; + } else if (!strcmp(stoppedReasonCstr, "LocalOutOfCredit")) { + stoppedReasonOut = Transaction::StoppedReason::LocalOutOfCredit; + } else if (!strcmp(stoppedReasonCstr, "MasterPass")) { + stoppedReasonOut = Transaction::StoppedReason::MasterPass; + } else if (!strcmp(stoppedReasonCstr, "Other")) { + stoppedReasonOut = Transaction::StoppedReason::Other; + } else if (!strcmp(stoppedReasonCstr, "OvercurrentFault")) { + stoppedReasonOut = Transaction::StoppedReason::OvercurrentFault; + } else if (!strcmp(stoppedReasonCstr, "PowerLoss")) { + stoppedReasonOut = Transaction::StoppedReason::PowerLoss; + } else if (!strcmp(stoppedReasonCstr, "PowerQuality")) { + stoppedReasonOut = Transaction::StoppedReason::PowerQuality; + } else if (!strcmp(stoppedReasonCstr, "Reboot")) { + stoppedReasonOut = Transaction::StoppedReason::Reboot; + } else if (!strcmp(stoppedReasonCstr, "Remote")) { + stoppedReasonOut = Transaction::StoppedReason::Remote; + } else if (!strcmp(stoppedReasonCstr, "SOCLimitReached")) { + stoppedReasonOut = Transaction::StoppedReason::SOCLimitReached; + } else if (!strcmp(stoppedReasonCstr, "StoppedByEV")) { + stoppedReasonOut = Transaction::StoppedReason::StoppedByEV; + } else if (!strcmp(stoppedReasonCstr, "TimeLimitReached")) { + stoppedReasonOut = Transaction::StoppedReason::TimeLimitReached; + } else if (!strcmp(stoppedReasonCstr, "Timeout")) { + stoppedReasonOut = Transaction::StoppedReason::Timeout; + } else { + MO_DBG_ERR("deserialization error"); + return false; + } + return true; +} + +const char *serializeTransactionEventType(TransactionEventData::Type type) { + const char *typeCstr = ""; + switch (type) { + case TransactionEventData::Type::Ended: + typeCstr = "Ended"; + break; + case TransactionEventData::Type::Started: + typeCstr = "Started"; + break; + case TransactionEventData::Type::Updated: + typeCstr = "Updated"; + break; + } + return typeCstr; +} +bool deserializeTransactionEventType(const char *typeCstr, TransactionEventData::Type& typeOut) { + if (!strcmp(typeCstr, "Ended")) { + typeOut = TransactionEventData::Type::Ended; + } else if (!strcmp(typeCstr, "Started")) { + typeOut = TransactionEventData::Type::Started; + } else if (!strcmp(typeCstr, "Updated")) { + typeOut = TransactionEventData::Type::Updated; + } else { + MO_DBG_ERR("deserialization error"); + return false; + } + return true; +} + +const char *serializeTransactionEventTriggerReason(TransactionEventTriggerReason triggerReason) { + + const char *triggerReasonCstr = nullptr; + switch(triggerReason) { + case TransactionEventTriggerReason::UNDEFINED: + break; + case TransactionEventTriggerReason::Authorized: + triggerReasonCstr = "Authorized"; + break; + case TransactionEventTriggerReason::CablePluggedIn: + triggerReasonCstr = "CablePluggedIn"; + break; + case TransactionEventTriggerReason::ChargingRateChanged: + triggerReasonCstr = "ChargingRateChanged"; + break; + case TransactionEventTriggerReason::ChargingStateChanged: + triggerReasonCstr = "ChargingStateChanged"; + break; + case TransactionEventTriggerReason::Deauthorized: + triggerReasonCstr = "Deauthorized"; + break; + case TransactionEventTriggerReason::EnergyLimitReached: + triggerReasonCstr = "EnergyLimitReached"; + break; + case TransactionEventTriggerReason::EVCommunicationLost: + triggerReasonCstr = "EVCommunicationLost"; + break; + case TransactionEventTriggerReason::EVConnectTimeout: + triggerReasonCstr = "EVConnectTimeout"; + break; + case TransactionEventTriggerReason::MeterValueClock: + triggerReasonCstr = "MeterValueClock"; + break; + case TransactionEventTriggerReason::MeterValuePeriodic: + triggerReasonCstr = "MeterValuePeriodic"; + break; + case TransactionEventTriggerReason::TimeLimitReached: + triggerReasonCstr = "TimeLimitReached"; + break; + case TransactionEventTriggerReason::Trigger: + triggerReasonCstr = "Trigger"; + break; + case TransactionEventTriggerReason::UnlockCommand: + triggerReasonCstr = "UnlockCommand"; + break; + case TransactionEventTriggerReason::StopAuthorized: + triggerReasonCstr = "StopAuthorized"; + break; + case TransactionEventTriggerReason::EVDeparted: + triggerReasonCstr = "EVDeparted"; + break; + case TransactionEventTriggerReason::EVDetected: + triggerReasonCstr = "EVDetected"; + break; + case TransactionEventTriggerReason::RemoteStop: + triggerReasonCstr = "RemoteStop"; + break; + case TransactionEventTriggerReason::RemoteStart: + triggerReasonCstr = "RemoteStart"; + break; + case TransactionEventTriggerReason::AbnormalCondition: + triggerReasonCstr = "AbnormalCondition"; + break; + case TransactionEventTriggerReason::SignedDataReceived: + triggerReasonCstr = "SignedDataReceived"; + break; + case TransactionEventTriggerReason::ResetCommand: + triggerReasonCstr = "ResetCommand"; + break; + } + + return triggerReasonCstr; +} +bool deserializeTransactionEventTriggerReason(const char *triggerReasonCstr, TransactionEventTriggerReason& triggerReasonOut) { + if (!triggerReasonCstr || !*triggerReasonCstr) { + triggerReasonOut = TransactionEventTriggerReason::UNDEFINED; + } else if (!strcmp(triggerReasonCstr, "Authorized")) { + triggerReasonOut = TransactionEventTriggerReason::Authorized; + } else if (!strcmp(triggerReasonCstr, "CablePluggedIn")) { + triggerReasonOut = TransactionEventTriggerReason::CablePluggedIn; + } else if (!strcmp(triggerReasonCstr, "ChargingRateChanged")) { + triggerReasonOut = TransactionEventTriggerReason::ChargingRateChanged; + } else if (!strcmp(triggerReasonCstr, "ChargingStateChanged")) { + triggerReasonOut = TransactionEventTriggerReason::ChargingStateChanged; + } else if (!strcmp(triggerReasonCstr, "Deauthorized")) { + triggerReasonOut = TransactionEventTriggerReason::Deauthorized; + } else if (!strcmp(triggerReasonCstr, "EnergyLimitReached")) { + triggerReasonOut = TransactionEventTriggerReason::EnergyLimitReached; + } else if (!strcmp(triggerReasonCstr, "EVCommunicationLost")) { + triggerReasonOut = TransactionEventTriggerReason::EVCommunicationLost; + } else if (!strcmp(triggerReasonCstr, "EVConnectTimeout")) { + triggerReasonOut = TransactionEventTriggerReason::EVConnectTimeout; + } else if (!strcmp(triggerReasonCstr, "MeterValueClock")) { + triggerReasonOut = TransactionEventTriggerReason::MeterValueClock; + } else if (!strcmp(triggerReasonCstr, "MeterValuePeriodic")) { + triggerReasonOut = TransactionEventTriggerReason::MeterValuePeriodic; + } else if (!strcmp(triggerReasonCstr, "TimeLimitReached")) { + triggerReasonOut = TransactionEventTriggerReason::TimeLimitReached; + } else if (!strcmp(triggerReasonCstr, "Trigger")) { + triggerReasonOut = TransactionEventTriggerReason::Trigger; + } else if (!strcmp(triggerReasonCstr, "UnlockCommand")) { + triggerReasonOut = TransactionEventTriggerReason::UnlockCommand; + } else if (!strcmp(triggerReasonCstr, "StopAuthorized")) { + triggerReasonOut = TransactionEventTriggerReason::StopAuthorized; + } else if (!strcmp(triggerReasonCstr, "EVDeparted")) { + triggerReasonOut = TransactionEventTriggerReason::EVDeparted; + } else if (!strcmp(triggerReasonCstr, "EVDetected")) { + triggerReasonOut = TransactionEventTriggerReason::EVDetected; + } else if (!strcmp(triggerReasonCstr, "RemoteStop")) { + triggerReasonOut = TransactionEventTriggerReason::RemoteStop; + } else if (!strcmp(triggerReasonCstr, "RemoteStart")) { + triggerReasonOut = TransactionEventTriggerReason::RemoteStart; + } else if (!strcmp(triggerReasonCstr, "AbnormalCondition")) { + triggerReasonOut = TransactionEventTriggerReason::AbnormalCondition; + } else if (!strcmp(triggerReasonCstr, "SignedDataReceived")) { + triggerReasonOut = TransactionEventTriggerReason::SignedDataReceived; + } else if (!strcmp(triggerReasonCstr, "ResetCommand")) { + triggerReasonOut = TransactionEventTriggerReason::ResetCommand; + } else { + MO_DBG_ERR("deserialization error"); + return false; + } + return true; +} + +const char *serializeTransactionEventChargingState(TransactionEventData::ChargingState chargingState) { + const char *chargingStateCstr = nullptr; + switch (chargingState) { + case TransactionEventData::ChargingState::UNDEFINED: + // optional, okay + break; + case TransactionEventData::ChargingState::Charging: + chargingStateCstr = "Charging"; + break; + case TransactionEventData::ChargingState::EVConnected: + chargingStateCstr = "EVConnected"; + break; + case TransactionEventData::ChargingState::SuspendedEV: + chargingStateCstr = "SuspendedEV"; + break; + case TransactionEventData::ChargingState::SuspendedEVSE: + chargingStateCstr = "SuspendedEVSE"; + break; + case TransactionEventData::ChargingState::Idle: + chargingStateCstr = "Idle"; + break; + } + return chargingStateCstr; +} +bool deserializeTransactionEventChargingState(const char *chargingStateCstr, TransactionEventData::ChargingState& chargingStateOut) { + if (!chargingStateCstr || !*chargingStateCstr) { + chargingStateOut = TransactionEventData::ChargingState::UNDEFINED; + } else if (!strcmp(chargingStateCstr, "Charging")) { + chargingStateOut = TransactionEventData::ChargingState::Charging; + } else if (!strcmp(chargingStateCstr, "EVConnected")) { + chargingStateOut = TransactionEventData::ChargingState::EVConnected; + } else if (!strcmp(chargingStateCstr, "SuspendedEV")) { + chargingStateOut = TransactionEventData::ChargingState::SuspendedEV; + } else if (!strcmp(chargingStateCstr, "SuspendedEVSE")) { + chargingStateOut = TransactionEventData::ChargingState::SuspendedEVSE; + } else if (!strcmp(chargingStateCstr, "Idle")) { + chargingStateOut = TransactionEventData::ChargingState::Idle; + } else { + MO_DBG_ERR("deserialization error"); + return false; + } + return true; +} + +} //namespace Ocpp201 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V201 + +#if MO_ENABLE_V201 +bool g_ocpp_tx_compat_v201; + +void ocpp_tx_compat_setV201(bool isV201) { + g_ocpp_tx_compat_v201 = isV201; +} +#endif + +int ocpp_tx_getTransactionId(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return -1; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->getTransactionId(); +} +#if MO_ENABLE_V201 +const char *ocpp_tx_getTransactionIdV201(OCPP_Transaction *tx) { + if (!g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v201"); + return nullptr; + } + return reinterpret_cast(tx)->transactionId; +} +#endif //MO_ENABLE_V201 +bool ocpp_tx_isAuthorized(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + return reinterpret_cast(tx)->isAuthorized; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->isAuthorized(); +} +bool ocpp_tx_isIdTagDeauthorized(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + return reinterpret_cast(tx)->isDeauthorized; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->isIdTagDeauthorized(); +} + +bool ocpp_tx_isRunning(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + auto transaction = reinterpret_cast(tx); + return transaction->started && !transaction->stopped; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->isRunning(); +} +bool ocpp_tx_isActive(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + return reinterpret_cast(tx)->active; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->isActive(); +} +bool ocpp_tx_isAborted(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + auto transaction = reinterpret_cast(tx); + return !transaction->active && !transaction->started; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->isAborted(); +} +bool ocpp_tx_isCompleted(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + auto transaction = reinterpret_cast(tx); + return transaction->stopped && transaction->seqNos.empty(); + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->isCompleted(); +} + +const char *ocpp_tx_getIdTag(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + auto transaction = reinterpret_cast(tx); + return transaction->idToken.get(); + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->getIdTag(); +} + +const char *ocpp_tx_getParentIdTag(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return nullptr; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->getParentIdTag(); +} + +bool ocpp_tx_getBeginTimestamp(OCPP_Transaction *tx, char *buf, size_t len) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + return reinterpret_cast(tx)->beginTimestamp.toJsonString(buf, len); + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->getBeginTimestamp().toJsonString(buf, len); +} + +int32_t ocpp_tx_getMeterStart(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return -1; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->getMeterStart(); +} + +bool ocpp_tx_getStartTimestamp(OCPP_Transaction *tx, char *buf, size_t len) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return -1; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->getStartTimestamp().toJsonString(buf, len); +} + +const char *ocpp_tx_getStopIdTag(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + auto transaction = reinterpret_cast(tx); + return transaction->stopIdToken ? transaction->stopIdToken->get() : ""; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->getStopIdTag(); +} + +int32_t ocpp_tx_getMeterStop(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return -1; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->getMeterStop(); +} + +void ocpp_tx_setMeterStop(OCPP_Transaction* tx, int32_t meter) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->setMeterStop(meter); +} + +bool ocpp_tx_getStopTimestamp(OCPP_Transaction *tx, char *buf, size_t len) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + MO_DBG_ERR("only supported in v16"); + return -1; + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->getStopTimestamp().toJsonString(buf, len); +} + +const char *ocpp_tx_getStopReason(OCPP_Transaction *tx) { + #if MO_ENABLE_V201 + if (g_ocpp_tx_compat_v201) { + auto transaction = reinterpret_cast(tx); + return serializeTransactionStoppedReason(transaction->stoppedReason); + } + #endif //MO_ENABLE_V201 + return reinterpret_cast(tx)->getStopReason(); +} diff --git a/src/MicroOcpp/Model/Transactions/Transaction.h b/src/MicroOcpp/Model/Transactions/Transaction.h new file mode 100644 index 00000000..5dc3e869 --- /dev/null +++ b/src/MicroOcpp/Model/Transactions/Transaction.h @@ -0,0 +1,497 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef TRANSACTION_H +#define TRANSACTION_H + +#include + +/* General Tx defs */ +#ifdef __cplusplus +extern "C" { +#endif //__cplusplus + +//TxNotification - event from MO to the main firmware to notify it about transaction state changes +typedef enum { + TxNotification_UNDEFINED, + + //Authorization events + TxNotification_Authorized, //success + TxNotification_AuthorizationRejected, //IdTag/token not authorized + TxNotification_AuthorizationTimeout, //authorization failed - offline + TxNotification_ReservationConflict, //connector/evse reserved for other IdTag + + TxNotification_ConnectionTimeout, //user took to long to plug vehicle after the authorization + TxNotification_DeAuthorized, //server rejected StartTx/TxEvent + TxNotification_RemoteStart, //authorized via RemoteStartTx/RequestStartTx + TxNotification_RemoteStop, //stopped via RemoteStopTx/RequestStopTx + + //Tx lifecycle events + TxNotification_StartTx, //entered running state (StartTx/TxEvent was initiated) + TxNotification_StopTx, //left running state (StopTx/TxEvent was initiated) +} TxNotification; + +#ifdef __cplusplus +} +#endif //__cplusplus + +#ifdef __cplusplus + +#include +#include +#include + +#define MAX_TX_CNT 100000U //upper limit of txNr (internal usage). Must be at least 2*MO_TXRECORD_SIZE+1 + +namespace MicroOcpp { + +/* + * A transaction is initiated by the client (charging station) and processed by the server (central system). + * The client side of a transaction is all data that is generated or collected at the charging station. The + * server side is all transaction data that is assigned by the central system. + * + * See OCPP 1.6 Specification - Edition 2, sections 3.6, 4.8, 4.10 and 5.11. + */ + +class ConnectorTransactionStore; + +class SendStatus { +private: + bool requested = false; + bool confirmed = false; + + unsigned int opNr = 0; + unsigned int attemptNr = 0; + Timestamp attemptTime = MIN_TIME; +public: + void setRequested() {this->requested = true;} + bool isRequested() {return requested;} + void confirm() {confirmed = true;} + bool isConfirmed() {return confirmed;} + void setOpNr(unsigned int opNr) {this->opNr = opNr;} + unsigned int getOpNr() {return opNr;} + void advanceAttemptNr() {attemptNr++;} + void setAttemptNr(unsigned int attemptNr) {this->attemptNr = attemptNr;} + unsigned int getAttemptNr() {return attemptNr;} + const Timestamp& getAttemptTime() {return attemptTime;} + void setAttemptTime(const Timestamp& timestamp) {attemptTime = timestamp;} +}; + +class Transaction : public MemoryManaged { +private: + ConnectorTransactionStore& context; + + bool active = true; //once active is false, the tx must stop (or cannot start at all) + + /* + * Attributes existing before StartTransaction + */ + char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; + char parentIdTag [IDTAG_LEN_MAX + 1] = {'\0'}; + bool authorized = false; //if the given idTag was authorized + bool deauthorized = false; //if the server revoked a local authorization + Timestamp begin_timestamp = MIN_TIME; + int reservationId = -1; + int txProfileId = -1; + + /* + * Attributes of StartTransaction + */ + SendStatus start_sync; + int32_t start_meter = -1; //meterStart of StartTx + Timestamp start_timestamp = MIN_TIME; //timestamp of StartTx; can be set before actually initiating + uint16_t start_bootNr = 0; + int transactionId = -1; //only valid if confirmed = true + + /* + * Attributes of StopTransaction + */ + SendStatus stop_sync; + char stop_idTag [IDTAG_LEN_MAX + 1] = {'\0'}; + int32_t stop_meter = -1; + Timestamp stop_timestamp = MIN_TIME; + uint16_t stop_bootNr = 0; + char stop_reason [REASON_LEN_MAX + 1] = {'\0'}; + + /* + * General attributes + */ + unsigned int connectorId = 0; + unsigned int txNr = 0; //client-side key of this tx object (!= transactionId) + + bool silent = false; //silent Tx: process tx locally, without reporting to the server + +public: + Transaction(ConnectorTransactionStore& context, unsigned int connectorId, unsigned int txNr, bool silent = false) : + MemoryManaged("v16.Transactions.Transaction"), + context(context), + connectorId(connectorId), + txNr(txNr), + silent(silent) {} + + /* + * data assigned by OCPP server + */ + int getTransactionId() {return transactionId;} + bool isAuthorized() {return authorized;} //Authorize has been accepted + bool isIdTagDeauthorized() {return deauthorized;} //StartTransaction has been rejected + + /* + * Transaction life cycle + */ + bool isRunning() {return start_sync.isRequested() && !stop_sync.isRequested();} //tx is running + bool isActive() {return active;} //tx continues to run or is preparing + bool isAborted() {return !start_sync.isRequested() && !active;} //tx ended before startTx was sent + bool isCompleted() {return stop_sync.isConfirmed();} //tx ended and startTx and stopTx have been confirmed by server + + /* + * After modifying a field of tx, commit to make the data persistent + */ + bool commit(); + + /* + * Getters and setters for (mostly) internal use + */ + void setInactive() {active = false;} + + bool setIdTag(const char *idTag); + const char *getIdTag() {return idTag;} + + bool setParentIdTag(const char *idTag); + const char *getParentIdTag() {return parentIdTag;} + + void setAuthorized() {authorized = true;} + void setIdTagDeauthorized() {deauthorized = true;} + + void setBeginTimestamp(Timestamp timestamp) {begin_timestamp = timestamp;} + const Timestamp& getBeginTimestamp() {return begin_timestamp;} + + void setReservationId(int reservationId) {this->reservationId = reservationId;} + int getReservationId() {return reservationId;} + + void setTxProfileId(int txProfileId) {this->txProfileId = txProfileId;} + int getTxProfileId() {return txProfileId;} + + SendStatus& getStartSync() {return start_sync;} + + void setMeterStart(int32_t meter) {start_meter = meter;} + bool isMeterStartDefined() {return start_meter >= 0;} + int32_t getMeterStart() {return start_meter;} + + void setStartTimestamp(Timestamp timestamp) {start_timestamp = timestamp;} + const Timestamp& getStartTimestamp() {return start_timestamp;} + + void setStartBootNr(uint16_t bootNr) {start_bootNr = bootNr;} + uint16_t getStartBootNr() {return start_bootNr;} + + void setTransactionId(int transactionId) {this->transactionId = transactionId;} + + SendStatus& getStopSync() {return stop_sync;} + + bool setStopIdTag(const char *idTag); + const char *getStopIdTag() {return stop_idTag;} + + void setMeterStop(int32_t meter) {stop_meter = meter;} + bool isMeterStopDefined() {return stop_meter >= 0;} + int32_t getMeterStop() {return stop_meter;} + + void setStopTimestamp(Timestamp timestamp) {stop_timestamp = timestamp;} + const Timestamp& getStopTimestamp() {return stop_timestamp;} + + void setStopBootNr(uint16_t bootNr) {stop_bootNr = bootNr;} + uint16_t getStopBootNr() {return stop_bootNr;} + + bool setStopReason(const char *reason); + const char *getStopReason() {return stop_reason;} + + void setConnectorId(unsigned int connectorId) {this->connectorId = connectorId;} + unsigned int getConnectorId() {return connectorId;} + + void setTxNr(unsigned int txNr) {this->txNr = txNr;} + unsigned int getTxNr() {return txNr;} //internal primary key of this tx object + + void setSilent() {silent = true;} + bool isSilent() {return silent;} //no data will be sent to server and server will not assign transactionId +}; + +} // namespace MicroOcpp + +#if MO_ENABLE_V201 + +#include +#include + +#include +#include +#include +#include + +#ifndef MO_SAMPLEDDATATXENDED_SIZE_MAX +#define MO_SAMPLEDDATATXENDED_SIZE_MAX 5 +#endif + +namespace MicroOcpp { +namespace Ocpp201 { + +// TriggerReasonEnumType (3.82) +enum class TransactionEventTriggerReason : uint8_t { + UNDEFINED, // not part of OCPP + Authorized, + CablePluggedIn, + ChargingRateChanged, + ChargingStateChanged, + Deauthorized, + EnergyLimitReached, + EVCommunicationLost, + EVConnectTimeout, + MeterValueClock, + MeterValuePeriodic, + TimeLimitReached, + Trigger, + UnlockCommand, + StopAuthorized, + EVDeparted, + EVDetected, + RemoteStop, + RemoteStart, + AbnormalCondition, + SignedDataReceived, + ResetCommand +}; + +class Transaction : public MemoryManaged { +public: + + // ReasonEnumType (3.67) + enum class StoppedReason : uint8_t { + UNDEFINED, // not part of OCPP + DeAuthorized, + EmergencyStop, + EnergyLimitReached, + EVDisconnected, + GroundFault, + ImmediateReset, + Local, + LocalOutOfCredit, + MasterPass, + Other, + OvercurrentFault, + PowerLoss, + PowerQuality, + Reboot, + Remote, + SOCLimitReached, + StoppedByEV, + TimeLimitReached, + Timeout + }; + +//private: + /* + * Transaction substates. Notify server about any change when transaction is running + */ + //bool trackParkingBayOccupancy; // not supported + bool trackEvConnected = false; + bool trackAuthorized = false; + bool trackDataSigned = false; + bool trackPowerPathClosed = false; + bool trackEnergyTransfer = false; + + /* + * Transaction lifecycle + */ + bool active = true; //once active is false, the tx must stop (or cannot start at all) + bool started = false; //if a TxEvent with event type TxStarted has been initiated + bool stopped = false; //if a TxEvent with event type TxEnded has been initiated + + /* + * Global transaction data + */ + bool isAuthorizationActive = false; //period between beginAuthorization and endAuthorization + bool isAuthorized = false; //if the given idToken was authorized + bool isDeauthorized = false; //if the server revoked a local authorization + IdToken idToken; + Timestamp beginTimestamp = MIN_TIME; + char transactionId [MO_TXID_LEN_MAX + 1] = {'\0'}; + int remoteStartId = -1; + + //if to fill next TxEvent with optional fields + bool notifyEvseId = false; + bool notifyIdToken = false; + bool notifyStopIdToken = false; + bool notifyReservationId = false; + bool notifyChargingState = false; + bool notifyRemoteStartId = false; + + bool evConnectionTimeoutListen = true; + + StoppedReason stoppedReason = StoppedReason::UNDEFINED; + TransactionEventTriggerReason stopTrigger = TransactionEventTriggerReason::UNDEFINED; + std::unique_ptr stopIdToken; // if null, then stopIdToken equals idToken + + /* + * Tx-related metering + */ + + Vector> sampledDataTxEnded; + + unsigned long lastSampleTimeTxUpdated = 0; //0 means not charging right now + unsigned long lastSampleTimeTxEnded = 0; + + /* + * Attributes for internal store + */ + unsigned int evseId = 0; + unsigned int txNr = 0; //internal key attribute (!= transactionId); {evseId*txNr} is unique key + + unsigned int seqNoEnd = 0; // increment by 1 for each event + Vector seqNos; //track stored txEvents + + bool silent = false; //silent Tx: process tx locally, without reporting to the server + + Transaction() : + MemoryManaged("v201.Transactions.Transaction"), + sampledDataTxEnded(makeVector>(getMemoryTag())), + seqNos(makeVector(getMemoryTag())) { } + + void addSampledDataTxEnded(std::unique_ptr mv) { + if (sampledDataTxEnded.size() >= MO_SAMPLEDDATATXENDED_SIZE_MAX) { + int deltaMin = std::numeric_limits::max(); + size_t indexMin = sampledDataTxEnded.size(); + for (size_t i = 1; i + 1 <= sampledDataTxEnded.size(); i++) { + size_t t0 = sampledDataTxEnded.size() - i - 1; + size_t t1 = sampledDataTxEnded.size() - i; + + auto delta = sampledDataTxEnded[t1]->getTimestamp() - sampledDataTxEnded[t0]->getTimestamp(); + + if (delta < deltaMin) { + deltaMin = delta; + indexMin = t1; + } + } + + sampledDataTxEnded.erase(sampledDataTxEnded.begin() + indexMin); + } + + sampledDataTxEnded.push_back(std::move(mv)); + } +}; + +// TransactionEventRequest (1.60.1) +class TransactionEventData : public MemoryManaged { +public: + + // TransactionEventEnumType (3.80) + enum class Type : uint8_t { + Ended, + Started, + Updated + }; + + // ChargingStateEnumType (3.16) + enum class ChargingState : uint8_t { + UNDEFINED, // not part of OCPP + Charging, + EVConnected, + SuspendedEV, + SuspendedEVSE, + Idle + }; + +//private: + Transaction *transaction; + Type eventType; + Timestamp timestamp; + uint16_t bootNr = 0; + TransactionEventTriggerReason triggerReason; + const unsigned int seqNo; + bool offline = false; + int numberOfPhasesUsed = -1; + int cableMaxCurrent = -1; + int reservationId = -1; + int remoteStartId = -1; + + // TransactionType (2.48) + ChargingState chargingState = ChargingState::UNDEFINED; + //int timeSpentCharging = 0; // not supported + std::unique_ptr idToken; + EvseId evse = -1; + //meterValue not supported + Vector> meterValue; + + unsigned int opNr = 0; + unsigned int attemptNr = 0; + Timestamp attemptTime = MIN_TIME; + + TransactionEventData(Transaction *transaction, unsigned int seqNo) : MemoryManaged("v201.Transactions.TransactionEventData"), transaction(transaction), seqNo(seqNo), meterValue(makeVector>(getMemoryTag())) { } +}; + +const char *serializeTransactionStoppedReason(Transaction::StoppedReason stoppedReason); +bool deserializeTransactionStoppedReason(const char *stoppedReasonCstr, Transaction::StoppedReason& stoppedReasonOut); + +const char *serializeTransactionEventType(TransactionEventData::Type type); +bool deserializeTransactionEventType(const char *typeCstr, TransactionEventData::Type& typeOut); + +const char *serializeTransactionEventTriggerReason(TransactionEventTriggerReason triggerReason); +bool deserializeTransactionEventTriggerReason(const char *triggerReasonCstr, TransactionEventTriggerReason& triggerReasonOut); + +const char *serializeTransactionEventChargingState(TransactionEventData::ChargingState chargingState); +bool deserializeTransactionEventChargingState(const char *chargingStateCstr, TransactionEventData::ChargingState& chargingStateOut); + + +} // namespace Ocpp201 +} // namespace MicroOcpp + +#endif // MO_ENABLE_V201 + +extern "C" { +#endif //__cplusplus + +struct OCPP_Transaction; +typedef struct OCPP_Transaction OCPP_Transaction; + +/* + * Compat mode for transactions. This means that all following C-wrapper functions will interprete the handle as v201 transactions + */ +#if MO_ENABLE_V201 +void ocpp_tx_compat_setV201(bool isV201); //if set, all OCPP_Transaction* handles are treated as v201 transactions +#endif + +int ocpp_tx_getTransactionId(OCPP_Transaction *tx); +#if MO_ENABLE_V201 +const char *ocpp_tx_getTransactionIdV201(OCPP_Transaction *tx); +#endif + +bool ocpp_tx_isAuthorized(OCPP_Transaction *tx); +bool ocpp_tx_isIdTagDeauthorized(OCPP_Transaction *tx); + +bool ocpp_tx_isRunning(OCPP_Transaction *tx); +bool ocpp_tx_isActive(OCPP_Transaction *tx); +bool ocpp_tx_isAborted(OCPP_Transaction *tx); +bool ocpp_tx_isCompleted(OCPP_Transaction *tx); + +const char *ocpp_tx_getIdTag(OCPP_Transaction *tx); + +const char *ocpp_tx_getParentIdTag(OCPP_Transaction *tx); + +bool ocpp_tx_getBeginTimestamp(OCPP_Transaction *tx, char *buf, size_t len); + +int32_t ocpp_tx_getMeterStart(OCPP_Transaction *tx); + +bool ocpp_tx_getStartTimestamp(OCPP_Transaction *tx, char *buf, size_t len); + +const char *ocpp_tx_getStopIdTag(OCPP_Transaction *tx); + +int32_t ocpp_tx_getMeterStop(OCPP_Transaction *tx); +void ocpp_tx_setMeterStop(OCPP_Transaction* tx, int32_t meter); + +bool ocpp_tx_getStopTimestamp(OCPP_Transaction *tx, char *buf, size_t len); + +const char *ocpp_tx_getStopReason(OCPP_Transaction *tx); + +#ifdef __cplusplus +} //end extern "C" +#endif + +#endif 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 new file mode 100644 index 00000000..e3c412f0 --- /dev/null +++ b/src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp @@ -0,0 +1,280 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include +#include + +namespace MicroOcpp { + +bool serializeSendStatus(SendStatus& status, JsonObject out) { + if (status.isRequested()) { + out["requested"] = true; + } + if (status.isConfirmed()) { + out["confirmed"] = true; + } + out["opNr"] = status.getOpNr(); + if (status.getAttemptNr() != 0) { + out["attemptNr"] = status.getAttemptNr(); + } + if (status.getAttemptTime() > MIN_TIME) { + char attemptTime [JSONDATE_LENGTH + 1]; + status.getAttemptTime().toJsonString(attemptTime, sizeof(attemptTime)); + out["attemptTime"] = attemptTime; + } + return true; +} + +bool deserializeSendStatus(SendStatus& status, JsonObject in) { + if (in["requested"] | false) { + status.setRequested(); + } + if (in["confirmed"] | false) { + status.confirm(); + } + unsigned int opNr = in["opNr"] | (unsigned int)0; + if (opNr >= 10) { //10 is first valid tx-related opNr + status.setOpNr(opNr); + } + status.setAttemptNr(in["attemptNr"] | (unsigned int)0); + if (in.containsKey("attemptTime")) { + Timestamp attemptTime; + if (!attemptTime.setTime(in["attemptTime"] | "_Invalid")) { + MO_DBG_ERR("deserialization error"); + return false; + } + status.setAttemptTime(attemptTime); + } + return true; +} + +bool serializeTransaction(Transaction& tx, JsonDoc& out) { + out = initJsonDoc("v16.Transactions.TransactionDeserialize", 1024); + JsonObject state = out.to(); + + JsonObject sessionState = state.createNestedObject("session"); + if (!tx.isActive()) { + sessionState["active"] = false; + } + if (tx.getIdTag()[0] != '\0') { + sessionState["idTag"] = tx.getIdTag(); + } + if (tx.getParentIdTag()[0] != '\0') { + sessionState["parentIdTag"] = tx.getParentIdTag(); + } + if (tx.isAuthorized()) { + sessionState["authorized"] = true; + } + if (tx.isIdTagDeauthorized()) { + sessionState["deauthorized"] = true; + } + if (tx.getBeginTimestamp() > MIN_TIME) { + char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; + tx.getBeginTimestamp().toJsonString(timeStr, JSONDATE_LENGTH + 1); + sessionState["timestamp"] = timeStr; + } + if (tx.getReservationId() >= 0) { + sessionState["reservationId"] = tx.getReservationId(); + } + if (tx.getTxProfileId() >= 0) { + sessionState["txProfileId"] = tx.getTxProfileId(); + } + + JsonObject txStart = state.createNestedObject("start"); + + if (!serializeSendStatus(tx.getStartSync(), txStart)) { + return false; + } + + if (tx.isMeterStartDefined()) { + txStart["meter"] = tx.getMeterStart(); + } + + char startTimeStr [JSONDATE_LENGTH + 1] = {'\0'}; + tx.getStartTimestamp().toJsonString(startTimeStr, JSONDATE_LENGTH + 1); + txStart["timestamp"] = startTimeStr; + + txStart["bootNr"] = tx.getStartBootNr(); + + if (tx.getStartSync().isConfirmed()) { + txStart["transactionId"] = tx.getTransactionId(); + } + + JsonObject txStop = state.createNestedObject("stop"); + + if (!serializeSendStatus(tx.getStopSync(), txStop)) { + return false; + } + + if (tx.getStopIdTag()[0] != '\0') { + txStop["idTag"] = tx.getStopIdTag(); + } + + if (tx.isMeterStopDefined()) { + txStop["meter"] = tx.getMeterStop(); + } + + char stopTimeStr [JSONDATE_LENGTH + 1] = {'\0'}; + tx.getStopTimestamp().toJsonString(stopTimeStr, JSONDATE_LENGTH + 1); + txStop["timestamp"] = stopTimeStr; + + txStop["bootNr"] = tx.getStopBootNr(); + + if (tx.getStopReason()[0] != '\0') { + txStop["reason"] = tx.getStopReason(); + } + + if (tx.isSilent()) { + state["silent"] = true; + } + + if (out.overflowed()) { + MO_DBG_ERR("JSON capacity exceeded"); + return false; + } + + return true; +} + +bool deserializeTransaction(Transaction& tx, JsonObject state) { + + JsonObject sessionState = state["session"]; + + if (!(sessionState["active"] | true)) { + tx.setInactive(); + } + + if (sessionState.containsKey("idTag")) { + if (!tx.setIdTag(sessionState["idTag"] | "")) { + MO_DBG_ERR("read err"); + return false; + } + } + + if (sessionState.containsKey("parentIdTag")) { + if (!tx.setParentIdTag(sessionState["parentIdTag"] | "")) { + MO_DBG_ERR("read err"); + return false; + } + } + + if (sessionState["authorized"] | false) { + tx.setAuthorized(); + } + + if (sessionState["deauthorized"] | false) { + tx.setIdTagDeauthorized(); + } + + if (sessionState.containsKey("timestamp")) { + Timestamp timestamp; + if (!timestamp.setTime(sessionState["timestamp"] | "Invalid")) { + MO_DBG_ERR("read err"); + return false; + } + tx.setBeginTimestamp(timestamp); + } + + if (sessionState.containsKey("reservationId")) { + tx.setReservationId(sessionState["reservationId"] | -1); + } + + if (sessionState.containsKey("txProfileId")) { + tx.setTxProfileId(sessionState["txProfileId"] | -1); + } + + JsonObject txStart = state["start"]; + + if (!deserializeSendStatus(tx.getStartSync(), txStart)) { + return false; + } + + if (txStart.containsKey("meter")) { + tx.setMeterStart(txStart["meter"] | 0); + } + + if (txStart.containsKey("timestamp")) { + Timestamp timestamp; + if (!timestamp.setTime(txStart["timestamp"] | "Invalid")) { + MO_DBG_ERR("read err"); + return false; + } + tx.setStartTimestamp(timestamp); + } + + if (txStart.containsKey("bootNr")) { + int bootNrIn = txStart["bootNr"]; + if (bootNrIn >= 0 && bootNrIn <= std::numeric_limits::max()) { + tx.setStartBootNr((uint16_t) bootNrIn); + } else { + MO_DBG_ERR("read err"); + return false; + } + } + + if (txStart.containsKey("transactionId")) { + tx.setTransactionId(txStart["transactionId"] | -1); + } + + JsonObject txStop = state["stop"]; + + if (!deserializeSendStatus(tx.getStopSync(), txStop)) { + return false; + } + + if (txStop.containsKey("idTag")) { + if (!tx.setStopIdTag(txStop["idTag"] | "")) { + MO_DBG_ERR("read err"); + return false; + } + } + + if (txStop.containsKey("meter")) { + tx.setMeterStop(txStop["meter"] | 0); + } + + if (txStop.containsKey("timestamp")) { + Timestamp timestamp; + if (!timestamp.setTime(txStop["timestamp"] | "Invalid")) { + MO_DBG_ERR("read err"); + return false; + } + tx.setStopTimestamp(timestamp); + } + + if (txStop.containsKey("bootNr")) { + int bootNrIn = txStop["bootNr"]; + if (bootNrIn >= 0 && bootNrIn <= std::numeric_limits::max()) { + tx.setStopBootNr((uint16_t) bootNrIn); + } else { + MO_DBG_ERR("read err"); + return false; + } + } + + if (txStop.containsKey("reason")) { + if (!tx.setStopReason(txStop["reason"] | "")) { + MO_DBG_ERR("read err"); + return false; + } + } + + if (state["silent"] | false) { + tx.setSilent(); + } + + MO_DBG_DEBUG("DUMP TX (%s)", tx.getIdTag() ? tx.getIdTag() : "idTag missing"); + MO_DBG_DEBUG("Session | idTag %s, active: %i, authorized: %i, deauthorized: %i", tx.getIdTag(), tx.isActive(), tx.isAuthorized(), tx.isIdTagDeauthorized()); + MO_DBG_DEBUG("Start RPC | req: %i, conf: %i", tx.getStartSync().isRequested(), tx.getStartSync().isConfirmed()); + MO_DBG_DEBUG("Stop RPC | req: %i, conf: %i", tx.getStopSync().isRequested(), tx.getStopSync().isConfirmed()); + if (tx.isSilent()) { + MO_DBG_DEBUG(" | silent Tx"); + } + + return true; +} + +} diff --git a/src/MicroOcpp/Model/Transactions/TransactionDeserialize.h b/src/MicroOcpp/Model/Transactions/TransactionDeserialize.h new file mode 100644 index 00000000..c8cfc427 --- /dev/null +++ b/src/MicroOcpp/Model/Transactions/TransactionDeserialize.h @@ -0,0 +1,20 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_TRANSACTIONDESERIALIZE_H +#define MO_TRANSACTIONDESERIALIZE_H + +#include +#include + +#include + +namespace MicroOcpp { + +bool serializeTransaction(Transaction& tx, JsonDoc& out); +bool deserializeTransaction(Transaction& tx, JsonObject in); + +} + +#endif diff --git a/src/MicroOcpp/Model/Transactions/TransactionService.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 new file mode 100644 index 00000000..39c096ba --- /dev/null +++ b/src/MicroOcpp/Model/Transactions/TransactionStore.cpp @@ -0,0 +1,1110 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include + +using namespace MicroOcpp; + +ConnectorTransactionStore::ConnectorTransactionStore(TransactionStore& context, unsigned int connectorId, std::shared_ptr filesystem) : + MemoryManaged("v16.Transactions.TransactionStore"), + context(context), + connectorId(connectorId), + filesystem(filesystem), + transactions{makeVector>(getMemoryTag())} { + +} + +ConnectorTransactionStore::~ConnectorTransactionStore() { + +} + +std::shared_ptr ConnectorTransactionStore::getTransaction(unsigned int txNr) { + + //check for most recent element of cache first because of temporal locality + if (!transactions.empty()) { + if (auto cached = transactions.back().lock()) { + if (cached->getTxNr() == txNr) { + //cache hit + return cached; + } + } + } + + //check all other elements (and free up unused entries) + auto cached = transactions.begin(); + while (cached != transactions.end()) { + if (auto tx = cached->lock()) { + if (tx->getTxNr() == txNr) { + //cache hit + return tx; + } + cached++; + } else { + //collect outdated cache reference + cached = transactions.erase(cached); + } + } + + //cache miss - load tx from flash if existent + + if (!filesystem) { + MO_DBG_DEBUG("no FS adapter"); + return nullptr; + } + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.json", connectorId, txNr); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return nullptr; + } + + size_t msize; + if (filesystem->stat(fn, &msize) != 0) { + MO_DBG_DEBUG("%u-%u does not exist", connectorId, txNr); + return nullptr; + } + + auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); + + if (!doc) { + MO_DBG_ERR("memory corruption"); + return nullptr; + } + + auto transaction = std::allocate_shared(makeAllocator(getMemoryTag()), *this, connectorId, txNr); + JsonObject txJson = doc->as(); + if (!deserializeTransaction(*transaction, txJson)) { + MO_DBG_ERR("deserialization error"); + return nullptr; + } + + //before adding new entry, clean cache + cached = transactions.begin(); + while (cached != transactions.end()) { + if (cached->expired()) { + //collect outdated cache reference + cached = transactions.erase(cached); + } else { + cached++; + } + } + + transactions.push_back(transaction); + return transaction; +} + +std::shared_ptr ConnectorTransactionStore::createTransaction(unsigned int txNr, bool silent) { + + auto transaction = std::allocate_shared(makeAllocator(getMemoryTag()), *this, connectorId, txNr, silent); + + if (!commit(transaction.get())) { + MO_DBG_ERR("FS error"); + return nullptr; + } + + //before adding new entry, clean cache + auto cached = transactions.begin(); + while (cached != transactions.end()) { + if (cached->expired()) { + //collect outdated cache reference + cached = transactions.erase(cached); + } else { + cached++; + } + } + + transactions.push_back(transaction); + return transaction; +} + +bool ConnectorTransactionStore::commit(Transaction *transaction) { + + if (!filesystem) { + MO_DBG_DEBUG("no FS: nothing to commit"); + return true; + } + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.json", connectorId, transaction->getTxNr()); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return false; + } + + auto txDoc = initJsonDoc(getMemoryTag()); + if (!serializeTransaction(*transaction, txDoc)) { + MO_DBG_ERR("Serialization error"); + return false; + } + + if (!FilesystemUtils::storeJson(filesystem, fn, txDoc)) { + MO_DBG_ERR("FS error"); + return false; + } + + //success + return true; +} + +bool ConnectorTransactionStore::remove(unsigned int txNr) { + + if (!filesystem) { + MO_DBG_DEBUG("no FS: nothing to remove"); + return true; + } + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.json", connectorId, txNr); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return false; + } + + size_t msize; + if (filesystem->stat(fn, &msize) != 0) { + MO_DBG_DEBUG("%s already removed", fn); + return true; + } + + MO_DBG_DEBUG("remove %s", fn); + + return filesystem->remove(fn); +} + +TransactionStore::TransactionStore(unsigned int nConnectors, std::shared_ptr filesystem) : + MemoryManaged{"v16.Transactions.TransactionStore"}, connectors{makeVector>(getMemoryTag())} { + + for (unsigned int i = 0; i < nConnectors; i++) { + connectors.push_back(std::unique_ptr( + new ConnectorTransactionStore(*this, i, filesystem))); + } +} + +bool TransactionStore::commit(Transaction *transaction) { + if (!transaction) { + MO_DBG_ERR("Invalid arg"); + return false; + } + auto connectorId = transaction->getConnectorId(); + if (connectorId >= connectors.size()) { + MO_DBG_ERR("Invalid tx"); + return false; + } + return connectors[connectorId]->commit(transaction); +} + +std::shared_ptr TransactionStore::getTransaction(unsigned int connectorId, unsigned int txNr) { + if (connectorId >= connectors.size()) { + MO_DBG_ERR("Invalid connectorId"); + return nullptr; + } + return connectors[connectorId]->getTransaction(txNr); +} + +std::shared_ptr TransactionStore::createTransaction(unsigned int connectorId, unsigned int txNr, bool silent) { + if (connectorId >= connectors.size()) { + MO_DBG_ERR("Invalid connectorId"); + return nullptr; + } + return connectors[connectorId]->createTransaction(txNr, silent); +} + +bool TransactionStore::remove(unsigned int connectorId, unsigned int txNr) { + if (connectorId >= connectors.size()) { + MO_DBG_ERR("Invalid connectorId"); + return false; + } + return connectors[connectorId]->remove(txNr); +} + +#if MO_ENABLE_V201 + +#include + +namespace MicroOcpp { +namespace Ocpp201 { + +bool TransactionStoreEvse::serializeTransaction(Transaction& tx, JsonObject txJson) { + + if (tx.trackEvConnected) { + txJson["trackEvConnected"] = tx.trackEvConnected; + } + + if (tx.trackAuthorized) { + txJson["trackAuthorized"] = tx.trackAuthorized; + } + + if (tx.trackDataSigned) { + txJson["trackDataSigned"] = tx.trackDataSigned; + } + + if (tx.trackPowerPathClosed) { + txJson["trackPowerPathClosed"] = tx.trackPowerPathClosed; + } + + if (tx.trackEnergyTransfer) { + txJson["trackEnergyTransfer"] = tx.trackEnergyTransfer; + } + + if (tx.active) { + txJson["active"] = true; + } + if (tx.started) { + txJson["started"] = true; + } + if (tx.stopped) { + txJson["stopped"] = true; + } + + if (tx.isAuthorizationActive) { + txJson["isAuthorizationActive"] = true; + } + if (tx.isAuthorized) { + txJson["isAuthorized"] = true; + } + if (tx.isDeauthorized) { + txJson["isDeauthorized"] = true; + } + + if (tx.idToken.get()) { + txJson["idToken"]["idToken"] = tx.idToken.get(); + txJson["idToken"]["type"] = tx.idToken.getTypeCstr(); + } + + if (tx.beginTimestamp > MIN_TIME) { + char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; + tx.beginTimestamp.toJsonString(timeStr, JSONDATE_LENGTH + 1); + txJson["beginTimestamp"] = timeStr; + } + + if (tx.remoteStartId >= 0) { + txJson["remoteStartId"] = tx.remoteStartId; + } + + if (tx.evConnectionTimeoutListen) { + txJson["evConnectionTimeoutListen"] = true; + } + + if (serializeTransactionStoppedReason(tx.stoppedReason)) { // optional + txJson["stoppedReason"] = serializeTransactionStoppedReason(tx.stoppedReason); + } + + if (serializeTransactionEventTriggerReason(tx.stopTrigger)) { + txJson["stopTrigger"] = serializeTransactionEventTriggerReason(tx.stopTrigger); + } + + if (tx.stopIdToken) { + JsonObject stopIdToken = txJson.createNestedObject("stopIdToken"); + stopIdToken["idToken"] = tx.stopIdToken->get(); + stopIdToken["type"] = tx.stopIdToken->getTypeCstr(); + } + + //sampledDataTxEnded not supported yet + + if (tx.silent) { + txJson["silent"] = true; + } + + txJson["txId"] = (const char*)tx.transactionId; //force zero-copy + + return true; +} + +bool TransactionStoreEvse::deserializeTransaction(Transaction& tx, JsonObject txJson) { + + if (txJson.containsKey("trackEvConnected") && !txJson["trackEvConnected"].is()) { + return false; + } + tx.trackEvConnected = txJson["trackEvConnected"] | false; + + if (txJson.containsKey("trackAuthorized") && !txJson["trackAuthorized"].is()) { + return false; + } + tx.trackAuthorized = txJson["trackAuthorized"] | false; + + if (txJson.containsKey("trackDataSigned") && !txJson["trackDataSigned"].is()) { + return false; + } + tx.trackDataSigned = txJson["trackDataSigned"] | false; + + if (txJson.containsKey("trackPowerPathClosed") && !txJson["trackPowerPathClosed"].is()) { + return false; + } + tx.trackPowerPathClosed = txJson["trackPowerPathClosed"] | false; + + if (txJson.containsKey("trackEnergyTransfer") && !txJson["trackEnergyTransfer"].is()) { + return false; + } + tx.trackEnergyTransfer = txJson["trackEnergyTransfer"] | false; + + if (txJson.containsKey("active") && !txJson["active"].is()) { + return false; + } + tx.active = txJson["active"] | false; + if (txJson.containsKey("started") && !txJson["started"].is()) { + return false; + } + tx.started = txJson["started"] | false; + + if (txJson.containsKey("stopped") && !txJson["stopped"].is()) { + return false; + } + tx.stopped = txJson["stopped"] | false; + + if (txJson.containsKey("isAuthorizationActive") && !txJson["isAuthorizationActive"].is()) { + return false; + } + tx.isAuthorizationActive = txJson["isAuthorizationActive"] | false; + if (txJson.containsKey("isAuthorized") && !txJson["isAuthorized"].is()) { + return false; + } + tx.isAuthorized = txJson["isAuthorized"] | false; + + if (txJson.containsKey("isDeauthorized") && !txJson["isDeauthorized"].is()) { + return false; + } + tx.isDeauthorized = txJson["isDeauthorized"] | false; + + if (txJson.containsKey("idToken")) { + IdToken idToken; + if (!idToken.parseCstr( + txJson["idToken"]["idToken"] | (const char*)nullptr, + txJson["idToken"]["type"] | (const char*)nullptr)) { + return false; + } + tx.idToken = idToken; + } + + if (txJson.containsKey("beginTimestamp")) { + if (!tx.beginTimestamp.setTime(txJson["beginTimestamp"] | "_Undefined")) { + return false; + } + } + + if (txJson.containsKey("remoteStartId")) { + int remoteStartIdIn = txJson["remoteStartId"] | -1; + if (remoteStartIdIn < 0) { + return false; + } + tx.remoteStartId = remoteStartIdIn; + } + + if (txJson.containsKey("evConnectionTimeoutListen") && !txJson["evConnectionTimeoutListen"].is()) { + return false; + } + tx.evConnectionTimeoutListen = txJson["evConnectionTimeoutListen"] | false; + + Transaction::StoppedReason stoppedReason; + if (!deserializeTransactionStoppedReason(txJson["stoppedReason"] | (const char*)nullptr, stoppedReason)) { + return false; + } + tx.stoppedReason = stoppedReason; + + TransactionEventTriggerReason stopTrigger; + if (!deserializeTransactionEventTriggerReason(txJson["stopTrigger"] | (const char*)nullptr, stopTrigger)) { + return false; + } + tx.stopTrigger = stopTrigger; + + if (txJson.containsKey("stopIdToken")) { + auto stopIdToken = std::unique_ptr(new IdToken()); + if (!stopIdToken) { + MO_DBG_ERR("OOM"); + return false; + } + if (!stopIdToken->parseCstr( + txJson["stopIdToken"]["idToken"] | (const char*)nullptr, + txJson["stopIdToken"]["type"] | (const char*)nullptr)) { + return false; + } + tx.stopIdToken = std::move(stopIdToken); + } + + //sampledDataTxEnded not supported yet + + if (auto txId = txJson["txId"] | (const char*)nullptr) { + auto ret = snprintf(tx.transactionId, sizeof(tx.transactionId), "%s", txId); + if (ret < 0 || (size_t)ret >= sizeof(tx.transactionId)) { + return false; + } + } else { + return false; + } + + if (txJson.containsKey("silent") && !txJson["silent"].is()) { + return false; + } + tx.silent = txJson["silent"] | false; + + return true; +} + +bool TransactionStoreEvse::serializeTransactionEvent(TransactionEventData& txEvent, JsonObject txEventJson) { + + if (txEvent.eventType != TransactionEventData::Type::Updated) { + txEventJson["eventType"] = serializeTransactionEventType(txEvent.eventType); + } + + if (txEvent.timestamp > MIN_TIME) { + char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; + txEvent.timestamp.toJsonString(timeStr, JSONDATE_LENGTH + 1); + txEventJson["timestamp"] = timeStr; + } + + txEventJson["bootNr"] = txEvent.bootNr; + + if (serializeTransactionEventTriggerReason(txEvent.triggerReason)) { + txEventJson["triggerReason"] = serializeTransactionEventTriggerReason(txEvent.triggerReason); + } + + if (txEvent.offline) { + txEventJson["offline"] = true; + } + + if (txEvent.numberOfPhasesUsed >= 0) { + txEventJson["numberOfPhasesUsed"] = txEvent.numberOfPhasesUsed; + } + + if (txEvent.cableMaxCurrent >= 0) { + txEventJson["cableMaxCurrent"] = txEvent.cableMaxCurrent; + } + + if (txEvent.reservationId >= 0) { + txEventJson["reservationId"] = txEvent.reservationId; + } + + if (txEvent.remoteStartId >= 0) { + txEventJson["remoteStartId"] = txEvent.remoteStartId; + } + + if (serializeTransactionEventChargingState(txEvent.chargingState)) { // optional + txEventJson["chargingState"] = serializeTransactionEventChargingState(txEvent.chargingState); + } + + if (txEvent.idToken) { + JsonObject idToken = txEventJson.createNestedObject("idToken"); + idToken["idToken"] = txEvent.idToken->get(); + idToken["type"] = txEvent.idToken->getTypeCstr(); + } + + if (txEvent.evse.id >= 0) { + JsonObject evse = txEventJson.createNestedObject("evse"); + evse["id"] = txEvent.evse.id; + if (txEvent.evse.connectorId >= 0) { + evse["connectorId"] = txEvent.evse.connectorId; + } + } + + //meterValue not supported yet + + txEventJson["opNr"] = txEvent.opNr; + txEventJson["attemptNr"] = txEvent.attemptNr; + + if (txEvent.attemptTime > MIN_TIME) { + char timeStr [JSONDATE_LENGTH + 1] = {'\0'}; + txEvent.attemptTime.toJsonString(timeStr, JSONDATE_LENGTH + 1); + txEventJson["attemptTime"] = timeStr; + } + + return true; +} + +bool TransactionStoreEvse::deserializeTransactionEvent(TransactionEventData& txEvent, JsonObject txEventJson) { + + TransactionEventData::Type eventType; + if (!deserializeTransactionEventType(txEventJson["eventType"] | "Updated", eventType)) { + return false; + } + txEvent.eventType = eventType; + + if (txEventJson.containsKey("timestamp")) { + if (!txEvent.timestamp.setTime(txEventJson["timestamp"] | "_Undefined")) { + return false; + } + } + + int bootNrIn = txEventJson["bootNr"] | -1; + if (bootNrIn >= 0 && bootNrIn <= std::numeric_limits::max()) { + txEvent.bootNr = (uint16_t)bootNrIn; + } else { + return false; + } + + TransactionEventTriggerReason triggerReason; + if (!deserializeTransactionEventTriggerReason(txEventJson["triggerReason"] | "_Undefined", triggerReason)) { + return false; + } + txEvent.triggerReason = triggerReason; + + if (txEventJson.containsKey("offline") && !txEventJson["offline"].is()) { + return false; + } + txEvent.offline = txEventJson["offline"] | false; + + if (txEventJson.containsKey("numberOfPhasesUsed")) { + int numberOfPhasesUsedIn = txEventJson["numberOfPhasesUsed"] | -1; + if (numberOfPhasesUsedIn < 0) { + return false; + } + txEvent.numberOfPhasesUsed = numberOfPhasesUsedIn; + } + + if (txEventJson.containsKey("cableMaxCurrent")) { + int cableMaxCurrentIn = txEventJson["cableMaxCurrent"] | -1; + if (cableMaxCurrentIn < 0) { + return false; + } + txEvent.cableMaxCurrent = cableMaxCurrentIn; + } + + if (txEventJson.containsKey("reservationId")) { + int reservationIdIn = txEventJson["reservationId"] | -1; + if (reservationIdIn < 0) { + return false; + } + txEvent.reservationId = reservationIdIn; + } + + if (txEventJson.containsKey("remoteStartId")) { + int remoteStartIdIn = txEventJson["remoteStartId"] | -1; + if (remoteStartIdIn < 0) { + return false; + } + txEvent.remoteStartId = remoteStartIdIn; + } + + TransactionEventData::ChargingState chargingState; + if (!deserializeTransactionEventChargingState(txEventJson["chargingState"] | (const char*)nullptr, chargingState)) { + return false; + } + txEvent.chargingState = chargingState; + + if (txEventJson.containsKey("idToken")) { + auto idToken = std::unique_ptr(new IdToken()); + if (!idToken) { + MO_DBG_ERR("OOM"); + return false; + } + if (!idToken->parseCstr( + txEventJson["idToken"]["idToken"] | (const char*)nullptr, + txEventJson["idToken"]["type"] | (const char*)nullptr)) { + return false; + } + txEvent.idToken = std::move(idToken); + } + + if (txEventJson.containsKey("evse")) { + int evseId = txEventJson["evse"]["id"] | -1; + if (evseId < 0) { + return false; + } + if (txEventJson["evse"].containsKey("connectorId")) { + int connectorId = txEventJson["evse"]["connectorId"] | -1; + if (connectorId < 0) { + return false; + } + txEvent.evse = EvseId(evseId, connectorId); + } else { + txEvent.evse = EvseId(evseId); + } + } + + //meterValue not supported yet + + int opNrIn = txEventJson["opNr"] | -1; + if (opNrIn >= 0) { + txEvent.opNr = (unsigned int)opNrIn; + } else { + return false; + } + + int attemptNrIn = txEventJson["attemptNr"] | -1; + if (attemptNrIn >= 0) { + txEvent.attemptNr = (unsigned int)attemptNrIn; + } else { + return false; + } + + if (txEventJson.containsKey("attemptTime")) { + if (!txEvent.attemptTime.setTime(txEventJson["attemptTime"] | "_Undefined")) { + return false; + } + } + + return true; +} + +TransactionStoreEvse::TransactionStoreEvse(TransactionStore& txStore, unsigned int evseId, std::shared_ptr filesystem) : + MemoryManaged("v201.Transactions.TransactionStore"), + txStore(txStore), + evseId(evseId), + filesystem(filesystem) { + +} + +bool TransactionStoreEvse::discoverStoredTx(unsigned int& txNrBeginOut, unsigned int& txNrEndOut) { + + if (!filesystem) { + MO_DBG_DEBUG("no FS adapter"); + return true; + } + + char fnPrefix [MO_MAX_PATH_SIZE]; + snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%u-", evseId); + size_t fnPrefixLen = strlen(fnPrefix); + + unsigned int txNrPivot = std::numeric_limits::max(); + unsigned int txNrBegin = 0, txNrEnd = 0; + + auto ret = filesystem->ftw_root([fnPrefix, fnPrefixLen, &txNrPivot, &txNrBegin, &txNrEnd] (const char *fn) { + if (!strncmp(fn, fnPrefix, fnPrefixLen)) { + unsigned int parsedTxNr = 0; + for (size_t i = fnPrefixLen; fn[i] >= '0' && fn[i] <= '9'; i++) { + parsedTxNr *= 10; + parsedTxNr += fn[i] - '0'; + } + + if (txNrPivot == std::numeric_limits::max()) { + txNrPivot = parsedTxNr; + txNrBegin = parsedTxNr; + txNrEnd = (parsedTxNr + 1) % MAX_TX_CNT; + return 0; + } + + if ((parsedTxNr + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT < MAX_TX_CNT / 2) { + //parsedTxNr is after pivot point + if ((parsedTxNr + 1 + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT > (txNrEnd + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT) { + txNrEnd = (parsedTxNr + 1) % MAX_TX_CNT; + } + } else if ((txNrPivot + MAX_TX_CNT - parsedTxNr) % MAX_TX_CNT < MAX_TX_CNT / 2) { + //parsedTxNr is before pivot point + if ((txNrPivot + MAX_TX_CNT - parsedTxNr) % MAX_TX_CNT > (txNrPivot + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT) { + txNrBegin = parsedTxNr; + } + } + + MO_DBG_DEBUG("found %s%u-*.json - Internal range from %u to %u (exclusive)", fnPrefix, parsedTxNr, txNrBegin, txNrEnd); + } + return 0; + }); + + if (ret == 0) { + txNrBeginOut = txNrBegin; + txNrEndOut = txNrEnd; + return true; + } else { + MO_DBG_ERR("fs error"); + return false; + } +} + +std::unique_ptr TransactionStoreEvse::loadTransaction(unsigned int txNr) { + + if (!filesystem) { + MO_DBG_DEBUG("no FS adapter"); + return nullptr; + } + + char fnPrefix [MO_MAX_PATH_SIZE]; + auto ret= snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%u-%u-", evseId, txNr); + if (ret < 0 || (size_t)ret >= sizeof(fnPrefix)) { + MO_DBG_ERR("fn error"); + return nullptr; + } + size_t fnPrefixLen = strlen(fnPrefix); + + Vector seqNos = makeVector(getMemoryTag()); + + filesystem->ftw_root([fnPrefix, fnPrefixLen, &seqNos] (const char *fn) { + if (!strncmp(fn, fnPrefix, fnPrefixLen)) { + unsigned int parsedSeqNo = 0; + for (size_t i = fnPrefixLen; fn[i] >= '0' && fn[i] <= '9'; i++) { + parsedSeqNo *= 10; + parsedSeqNo += fn[i] - '0'; + } + + seqNos.push_back(parsedSeqNo); + } + return 0; + }); + + if (seqNos.empty()) { + MO_DBG_DEBUG("no tx at tx201-%u-%u", evseId, txNr); + return nullptr; + } + + std::sort(seqNos.begin(), seqNos.end()); + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx201" "-%u-%u-%u.json", evseId, txNr, seqNos.back()); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return nullptr; + } + + size_t msize; + if (filesystem->stat(fn, &msize) != 0) { + MO_DBG_ERR("tx201-%u-%u memory corruption", evseId, txNr); + return nullptr; + } + + auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); + + if (!doc) { + MO_DBG_ERR("memory corruption"); + return nullptr; + } + + auto transaction = std::unique_ptr(new Transaction()); + if (!transaction) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + transaction->evseId = evseId; + transaction->txNr = txNr; + transaction->seqNos = std::move(seqNos); + + JsonObject txJson = (*doc)["tx"]; + + if (!deserializeTransaction(*transaction, txJson)) { + MO_DBG_ERR("deserialization error"); + return nullptr; + } + + //determine seqNoEnd and trim seqNos record + if (doc->containsKey("txEvent")) { + //last tx201 file contains txEvent -> txNoEnd is one place after tx201 file and seqNos is accurate + transaction->seqNoEnd = transaction->seqNos.back() + 1; + } else { + //last tx201 file contains only tx status information, but no txEvent -> remove from seqNos record and set seqNoEnd to this + transaction->seqNoEnd = transaction->seqNos.back(); + transaction->seqNos.pop_back(); + } + + MO_DBG_DEBUG("loaded tx %u-%u, seqNos.size()=%zu", evseId, txNr, transaction->seqNos.size()); + + return transaction; +} + +std::unique_ptr TransactionStoreEvse::createTransaction(unsigned int txNr, const char *txId) { + + //clean data which could still be here from a rolled-back transaction + if (!remove(txNr)) { + MO_DBG_ERR("txNr not clean"); + return nullptr; + } + + auto transaction = std::unique_ptr(new Transaction()); + if (!transaction) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + transaction->evseId = evseId; + transaction->txNr = txNr; + + auto ret = snprintf(transaction->transactionId, sizeof(transaction->transactionId), "%s", txId); + if (ret < 0 || (size_t)ret >= sizeof(transaction->transactionId)) { + MO_DBG_ERR("invalid arg"); + return nullptr; + } + + if (!commit(transaction.get())) { + MO_DBG_ERR("FS error"); + return nullptr; + } + + return transaction; +} + +std::unique_ptr TransactionStoreEvse::createTransactionEvent(Transaction& tx) { + + auto txEvent = std::unique_ptr(new TransactionEventData(&tx, tx.seqNoEnd)); + if (!txEvent) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + //success + return txEvent; +} + +std::unique_ptr TransactionStoreEvse::loadTransactionEvent(Transaction& tx, unsigned int seqNo) { + + if (!filesystem) { + MO_DBG_DEBUG("no FS adapter"); + return nullptr; + } + + bool found = false; + for (size_t i = 0; i < tx.seqNos.size(); i++) { + if (tx.seqNos[i] == seqNo) { + found = true; + } + } + if (!found) { + MO_DBG_DEBUG("%u-%u-%u does not exist", evseId, tx.txNr, seqNo); + return nullptr; + } + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx201" "-%u-%u-%u.json", evseId, tx.txNr, seqNo); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return nullptr; + } + + size_t msize; + if (filesystem->stat(fn, &msize) != 0) { + MO_DBG_ERR("seqNos out of sync: could not find %u-%u-%u", evseId, tx.txNr, seqNo); + return nullptr; + } + + auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); + + if (!doc) { + MO_DBG_ERR("memory corruption"); + return nullptr; + } + + if (!doc->containsKey("txEvent")) { + MO_DBG_DEBUG("%u-%u-%u does not contain txEvent", evseId, tx.txNr, seqNo); + return nullptr; + } + + auto txEvent = std::unique_ptr(new TransactionEventData(&tx, seqNo)); + if (!txEvent) { + MO_DBG_ERR("OOM"); + return nullptr; + } + + if (!deserializeTransactionEvent(*txEvent, (*doc)["txEvent"])) { + MO_DBG_ERR("deserialization error"); + return nullptr; + } + + return txEvent; +} + +bool TransactionStoreEvse::commit(Transaction& tx, TransactionEventData *txEvent) { + + if (!filesystem) { + MO_DBG_DEBUG("no FS: nothing to commit"); + return true; + } + + unsigned int seqNo = 0; + + if (txEvent) { + seqNo = txEvent->seqNo; + } else { + //update tx state in new or reused tx201 file + seqNo = tx.seqNoEnd; + } + + size_t seqNosNewSize = tx.seqNos.size() + 1; + for (size_t i = 0; i < tx.seqNos.size(); i++) { + if (tx.seqNos[i] == seqNo) { + seqNosNewSize -= 1; + break; + } + } + + // Check if to delete intermediate offline txEvent + if (seqNosNewSize > MO_TXEVENTRECORD_SIZE_V201) { + auto deltaMin = std::numeric_limits::max(); + size_t indexMin = tx.seqNos.size(); + for (size_t i = 2; i + 1 <= tx.seqNos.size(); i++) { //always keep first and final txEvent + size_t t0 = tx.seqNos.size() - i - 1; + size_t t1 = tx.seqNos.size() - i; + size_t t2 = tx.seqNos.size() - i + 1; + + auto delta = tx.seqNos[t2] - tx.seqNos[t0]; + + if (delta < deltaMin) { + deltaMin = delta; + indexMin = t1; + } + } + + if (indexMin < tx.seqNos.size()) { + MO_DBG_DEBUG("delete intermediate txEvent %u-%u-%u - delta=%u", evseId, tx.txNr, tx.seqNos[indexMin], deltaMin); + remove(tx, tx.seqNos[indexMin]); //remove can call commit() again. Ensure that remove is not executed for last element + } else { + MO_DBG_ERR("internal error"); + return false; + } + } + + char fn [MO_MAX_PATH_SIZE] = {'\0'}; + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx201" "-%u-%u-%u.json", evseId, tx.txNr, seqNo); + if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { + MO_DBG_ERR("fn error: %i", ret); + return false; + } + + auto txDoc = initJsonDoc("v201.Transactions.TransactionStoreEvse", 2048); + + if (!serializeTransaction(tx, txDoc.createNestedObject("tx"))) { + MO_DBG_ERR("Serialization error"); + return false; + } + + if (txEvent && !serializeTransactionEvent(*txEvent, txDoc.createNestedObject("txEvent"))) { + MO_DBG_ERR("Serialization error"); + return false; + } + + if (!FilesystemUtils::storeJson(filesystem, fn, txDoc)) { + MO_DBG_ERR("FS error"); + return false; + } + + if (txEvent && seqNo == tx.seqNoEnd) { + tx.seqNos.push_back(seqNo); + tx.seqNoEnd++; + } + + MO_DBG_DEBUG("comitted tx %u-%u-%u", evseId, tx.txNr, seqNo); + + //success + return true; +} + +bool TransactionStoreEvse::commit(Transaction *transaction) { + return commit(*transaction, nullptr); +} + +bool TransactionStoreEvse::commit(TransactionEventData *txEvent) { + return commit(*txEvent->transaction, txEvent); +} + +bool TransactionStoreEvse::remove(unsigned int txNr) { + + if (!filesystem) { + MO_DBG_DEBUG("no FS: nothing to remove"); + return true; + } + + char fnPrefix [MO_MAX_PATH_SIZE]; + auto ret= snprintf(fnPrefix, sizeof(fnPrefix), "tx201-%u-%u-", evseId, txNr); + if (ret < 0 || (size_t)ret >= sizeof(fnPrefix)) { + MO_DBG_ERR("fn error"); + return false; + } + size_t fnPrefixLen = strlen(fnPrefix); + + auto success = FilesystemUtils::remove_if(filesystem, [fnPrefix, fnPrefixLen] (const char *fn) { + return !strncmp(fn, fnPrefix, fnPrefixLen); + }); + + return success; +} + +bool TransactionStoreEvse::remove(Transaction& tx, unsigned int seqNo) { + + if (tx.seqNos.empty()) { + //nothing to do + return true; + } + + if (tx.seqNos.back() == seqNo) { + //special case: deletion of last tx201 file could also delete information about tx. Make sure all tx-related + //information is commited into tx201 file at seqNoEnd, then delete file at seqNo + + char fn [MO_MAX_PATH_SIZE]; + auto ret = snprintf(fn, sizeof(fn), "%stx201-%u-%u-%u.json", MO_FILENAME_PREFIX, evseId, tx.txNr, tx.seqNoEnd); + if (ret < 0 || (size_t)ret >= sizeof(fn)) { + MO_DBG_ERR("fn error"); + return false; + } + + auto doc = FilesystemUtils::loadJson(filesystem, fn, getMemoryTag()); + + if (!doc || !doc->containsKey("tx")) { + //no valid tx201 file at seqNoEnd. Commit tx into file seqNoEnd, then remove file at seqNo + + if (!commit(tx, nullptr)) { + MO_DBG_ERR("fs error"); + return false; + } + } + + //seqNoEnd contains all tx data which should be persisted. Continue + } + + bool found = false; + for (size_t i = 0; i < tx.seqNos.size(); i++) { + if (tx.seqNos[i] == seqNo) { + found = true; + } + } + if (!found) { + MO_DBG_DEBUG("%u-%u-%u does not exist", evseId, tx.txNr, seqNo); + return true; + } + + bool success = true; + + if (filesystem) { + char fn [MO_MAX_PATH_SIZE]; + auto ret = snprintf(fn, sizeof(fn), "%stx201-%u-%u-%u.json", MO_FILENAME_PREFIX, evseId, tx.txNr, seqNo); + if (ret < 0 || (size_t)ret >= sizeof(fn)) { + MO_DBG_ERR("fn error"); + return false; + } + + size_t msize; + if (filesystem->stat(fn, &msize) == 0) { + success &= filesystem->remove(fn); + } else { + MO_DBG_ERR("internal error: seqNos out of sync"); + (void)0; + } + } + + if (success) { + auto it = tx.seqNos.begin(); + while (it != tx.seqNos.end()) { + if (*it == seqNo) { + it = tx.seqNos.erase(it); + } else { + it++; + } + } + } + + return success; +} + +TransactionStore::TransactionStore(std::shared_ptr filesystem, size_t numEvses) : + MemoryManaged{"v201.Transactions.TransactionStore"} { + + for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID && (size_t)evseId < numEvses; evseId++) { + evses[evseId] = new TransactionStoreEvse(*this, evseId, filesystem); + } +} + +TransactionStore::~TransactionStore() { + for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { + delete evses[evseId]; + } +} + +TransactionStoreEvse *TransactionStore::getEvse(unsigned int evseId) { + if (evseId >= MO_NUM_EVSEID) { + return nullptr; + } + return evses[evseId]; +} + +} //namespace Ocpp201 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Model/Transactions/TransactionStore.h b/src/MicroOcpp/Model/Transactions/TransactionStore.h new file mode 100644 index 00000000..c5d3421e --- /dev/null +++ b/src/MicroOcpp/Model/Transactions/TransactionStore.h @@ -0,0 +1,117 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_TRANSACTIONSTORE_H +#define MO_TRANSACTIONSTORE_H + +#include +#include +#include +#include + +namespace MicroOcpp { + +class TransactionStore; + +class ConnectorTransactionStore : public MemoryManaged { +private: + TransactionStore& context; + const unsigned int connectorId; + + std::shared_ptr filesystem; + + Vector> transactions; + +public: + ConnectorTransactionStore(TransactionStore& context, unsigned int connectorId, std::shared_ptr filesystem); + ConnectorTransactionStore(const ConnectorTransactionStore&) = delete; + ConnectorTransactionStore(ConnectorTransactionStore&&) = delete; + ConnectorTransactionStore& operator=(const ConnectorTransactionStore&) = delete; + + ~ConnectorTransactionStore(); + + bool commit(Transaction *transaction); + + std::shared_ptr getTransaction(unsigned int txNr); + std::shared_ptr createTransaction(unsigned int txNr, bool silent = false); + + bool remove(unsigned int txNr); +}; + +class TransactionStore : public MemoryManaged { +private: + Vector> connectors; +public: + TransactionStore(unsigned int nConnectors, std::shared_ptr filesystem); + + bool commit(Transaction *transaction); + + std::shared_ptr getTransaction(unsigned int connectorId, unsigned int txNr); + std::shared_ptr createTransaction(unsigned int connectorId, unsigned int txNr, bool silent = false); + + bool remove(unsigned int connectorId, unsigned int txNr); +}; + +} + +#if MO_ENABLE_V201 + +#ifndef MO_TXEVENTRECORD_SIZE_V201 +#define MO_TXEVENTRECORD_SIZE_V201 10 //maximum number of of txEvents per tx to hold on flash storage +#endif + +namespace MicroOcpp { +namespace Ocpp201 { + +class TransactionStore; + +class TransactionStoreEvse : public MemoryManaged { +private: + TransactionStore& txStore; + const unsigned int evseId; + + std::shared_ptr filesystem; + + bool serializeTransaction(Transaction& tx, JsonObject out); + bool serializeTransactionEvent(TransactionEventData& txEvent, JsonObject out); + bool deserializeTransaction(Transaction& tx, JsonObject in); + bool deserializeTransactionEvent(TransactionEventData& txEvent, JsonObject in); + + bool commit(Transaction& transaction, TransactionEventData *transactionEvent); + +public: + TransactionStoreEvse(TransactionStore& txStore, unsigned int evseId, std::shared_ptr filesystem); + + bool discoverStoredTx(unsigned int& txNrBeginOut, unsigned int& txNrEndOut); + + bool commit(Transaction *transaction); + bool commit(TransactionEventData *transactionEvent); + + std::unique_ptr loadTransaction(unsigned int txNr); + std::unique_ptr createTransaction(unsigned int txNr, const char *txId); + + std::unique_ptr createTransactionEvent(Transaction& tx); + std::unique_ptr loadTransactionEvent(Transaction& tx, unsigned int seqNo); + + bool remove(unsigned int txNr); + bool remove(Transaction& tx, unsigned int seqNo); +}; + +class TransactionStore : public MemoryManaged { +private: + TransactionStoreEvse *evses [MO_NUM_EVSEID] = {nullptr}; +public: + TransactionStore(std::shared_ptr filesystem, size_t numEvses); + + ~TransactionStore(); + + TransactionStoreEvse *getEvse(unsigned int evseId); +}; + +} //namespace Ocpp201 +} //namespace MicroOcpp + +#endif //MO_ENABLE_V201 + +#endif 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 new file mode 100644 index 00000000..e12050be --- /dev/null +++ b/src/MicroOcpp/Operations/Authorize.cpp @@ -0,0 +1,122 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include + +#include + +using namespace MicroOcpp; + +namespace MicroOcpp { +namespace Ocpp16 { + +Authorize::Authorize(Model& model, const char *idTagIn) : MemoryManaged("v16.Operation.", "Authorize"), model(model) { + if (idTagIn && strnlen(idTagIn, IDTAG_LEN_MAX + 2) <= IDTAG_LEN_MAX) { + snprintf(idTag, IDTAG_LEN_MAX + 1, "%s", idTagIn); + } else { + MO_DBG_WARN("Format violation of idTag. Discard idTag"); + } +} + +const char* Authorize::getOperationType(){ + return "Authorize"; +} + +std::unique_ptr Authorize::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + (IDTAG_LEN_MAX + 1)); + JsonObject payload = doc->to(); + payload["idTag"] = idTag; + return doc; +} + +void Authorize::processConf(JsonObject payload){ + const char *idTagInfo = payload["idTagInfo"]["status"] | "not specified"; + + if (!strcmp(idTagInfo, "Accepted")) { + MO_DBG_INFO("Request has been accepted"); + } else { + MO_DBG_INFO("Request has been denied. Reason: %s", idTagInfo); + } + +#if MO_ENABLE_LOCAL_AUTH + if (auto authService = model.getAuthorizationService()) { + authService->notifyAuthorization(idTag, payload["idTagInfo"]); + } +#endif //MO_ENABLE_LOCAL_AUTH +} + +void Authorize::processReq(JsonObject payload){ + /* + * Ignore Contents of this Req-message, because this is for debug purposes only + */ +} + +std::unique_ptr Authorize::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), 2 * JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); + idTagInfo["status"] = "Accepted"; + return doc; +} + +} // namespace Ocpp16 +} // namespace MicroOcpp + +#if MO_ENABLE_V201 + +namespace MicroOcpp { +namespace Ocpp201 { + +Authorize::Authorize(Model& model, const IdToken& idToken) : MemoryManaged("v201.Operation.Authorize"), model(model) { + this->idToken = idToken; +} + +const char* Authorize::getOperationType(){ + return "Authorize"; +} + +std::unique_ptr Authorize::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), + JSON_OBJECT_SIZE(1) + + JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + payload["idToken"]["idToken"] = idToken.get(); + payload["idToken"]["type"] = idToken.getTypeCstr(); + return doc; +} + +void Authorize::processConf(JsonObject payload){ + const char *idTagInfo = payload["idTokenInfo"]["status"] | "_Undefined"; + + if (!strcmp(idTagInfo, "Accepted")) { + MO_DBG_INFO("Request has been accepted"); + } else { + MO_DBG_INFO("Request has been denied. Reason: %s", idTagInfo); + } + + //if (model.getAuthorizationService()) { + // model.getAuthorizationService()->notifyAuthorization(idTag, payload["idTagInfo"]); + //} +} + +void Authorize::processReq(JsonObject payload){ + /* + * Ignore Contents of this Req-message, because this is for debug purposes only + */ +} + +std::unique_ptr Authorize::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), 2 * JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + JsonObject idTagInfo = payload.createNestedObject("idTokenInfo"); + idTagInfo["status"] = "Accepted"; + return doc; +} + +} // namespace Ocpp201 +} // namespace MicroOcpp + +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/Authorize.h b/src/MicroOcpp/Operations/Authorize.h new file mode 100644 index 00000000..b49226ef --- /dev/null +++ b/src/MicroOcpp/Operations/Authorize.h @@ -0,0 +1,71 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef AUTHORIZE_H +#define AUTHORIZE_H + +#include +#include +#include + +namespace MicroOcpp { + +class Model; + +namespace Ocpp16 { + +class Authorize : public Operation, public MemoryManaged { +private: + Model& model; + char idTag [IDTAG_LEN_MAX + 1] = {'\0'}; +public: + Authorize(Model& model, const char *idTag); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#if MO_ENABLE_V201 + +#include + +namespace MicroOcpp { +namespace Ocpp201 { + +class Authorize : public Operation, public MemoryManaged { +private: + Model& model; + IdToken idToken; +public: + Authorize(Model& model, const IdToken& idToken); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Operations/BootNotification.cpp b/src/MicroOcpp/Operations/BootNotification.cpp new file mode 100644 index 00000000..1f4b7c77 --- /dev/null +++ b/src/MicroOcpp/Operations/BootNotification.cpp @@ -0,0 +1,124 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include + +#include + +using MicroOcpp::Ocpp16::BootNotification; +using MicroOcpp::JsonDoc; + +BootNotification::BootNotification(Model& model, std::unique_ptr payload) : MemoryManaged("v16.Operation.", "BootNotification"), model(model), credentials(std::move(payload)) { + +} + +const char* BootNotification::getOperationType(){ + return "BootNotification"; +} + +std::unique_ptr BootNotification::createReq() { + if (credentials) { +#if MO_ENABLE_V201 + if (model.getVersion().major == 2) { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2) + credentials->memoryUsage()); + JsonObject payload = doc->to(); + payload["reason"] = "PowerUp"; + payload["chargingStation"] = *credentials; + return doc; + } +#endif + return std::unique_ptr(new JsonDoc(*credentials)); + } else { + MO_DBG_ERR("payload undefined"); + return createEmptyDocument(); + } +} + +void BootNotification::processConf(JsonObject payload) { + const char* currentTime = payload["currentTime"] | "Invalid"; + if (strcmp(currentTime, "Invalid")) { + if (model.getClock().setTime(currentTime)) { + //success + } else { + MO_DBG_ERR("Time string format violation. Expect format like 2022-02-01T20:53:32.486Z"); + errorCode = "PropertyConstraintViolation"; + return; + } + } else { + MO_DBG_ERR("Missing attribute currentTime"); + errorCode = "FormationViolation"; + return; + } + + int interval = payload["interval"] | -1; + if (interval < 0) { + errorCode = "FormationViolation"; + return; + } + + RegistrationStatus status = deserializeRegistrationStatus(payload["status"] | "Invalid"); + + if (status == RegistrationStatus::UNDEFINED) { + errorCode = "FormationViolation"; + return; + } + + if (status == RegistrationStatus::Accepted) { + //only write if in valid range + if (interval >= 1) { + auto heartbeatIntervalInt = declareConfiguration("HeartbeatInterval", 86400); + if (heartbeatIntervalInt && interval != heartbeatIntervalInt->getInt()) { + heartbeatIntervalInt->setInt(interval); + configuration_save(); + } + } + } + + if (auto bootService = model.getBootService()) { + + if (status != RegistrationStatus::Accepted) { + bootService->setRetryInterval(interval); + } + + bootService->notifyRegistrationStatus(status); + } + + MO_DBG_INFO("request has been %s", status == RegistrationStatus::Accepted ? "Accepted" : + status == RegistrationStatus::Pending ? "replied with Pending" : + "Rejected"); +} + +void BootNotification::processReq(JsonObject payload){ + /* + * Ignore Contents of this Req-message, because this is for debug purposes only + */ +} + +std::unique_ptr BootNotification::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(3) + (JSONDATE_LENGTH + 1)); + JsonObject payload = doc->to(); + + //safety mechanism; in some test setups the library has to answer BootNotifications without valid system time + Timestamp ocppTimeReference = Timestamp(2022,0,27,11,59,55); + Timestamp ocppSelect = ocppTimeReference; + auto& ocppTime = model.getClock(); + Timestamp ocppNow = ocppTime.now(); + if (ocppNow > ocppTimeReference) { + //time has already been set + ocppSelect = ocppNow; + } + + char ocppNowJson [JSONDATE_LENGTH + 1] = {'\0'}; + ocppSelect.toJsonString(ocppNowJson, JSONDATE_LENGTH + 1); + payload["currentTime"] = ocppNowJson; + + payload["interval"] = 86400; //heartbeat send interval - not relevant for JSON variant of OCPP so send dummy value that likely won't break + payload["status"] = "Accepted"; + return doc; +} diff --git a/src/MicroOcpp/Operations/BootNotification.h b/src/MicroOcpp/Operations/BootNotification.h new file mode 100644 index 00000000..c560e03f --- /dev/null +++ b/src/MicroOcpp/Operations/BootNotification.h @@ -0,0 +1,48 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_BOOTNOTIFICATION_H +#define MO_BOOTNOTIFICATION_H + +#include +#include + +#define CP_MODEL_LEN_MAX CiString20TypeLen +#define CP_SERIALNUMBER_LEN_MAX CiString25TypeLen +#define CP_VENDOR_LEN_MAX CiString20TypeLen +#define FW_VERSION_LEN_MAX CiString50TypeLen + +namespace MicroOcpp { + +class Model; + +namespace Ocpp16 { + +class BootNotification : public Operation, public MemoryManaged { +private: + Model& model; + std::unique_ptr credentials; + const char *errorCode = nullptr; +public: + BootNotification(Model& model, std::unique_ptr payload); + + ~BootNotification() = default; + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#endif diff --git a/src/MicroOcpp/Operations/CancelReservation.cpp b/src/MicroOcpp/Operations/CancelReservation.cpp new file mode 100644 index 00000000..7741d9b9 --- /dev/null +++ b/src/MicroOcpp/Operations/CancelReservation.cpp @@ -0,0 +1,47 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_RESERVATION + +#include +#include +#include + +using MicroOcpp::Ocpp16::CancelReservation; +using MicroOcpp::JsonDoc; + +CancelReservation::CancelReservation(ReservationService& reservationService) : MemoryManaged("v16.Operation.", "CancelReservation"), reservationService(reservationService) { + +} + +const char* CancelReservation::getOperationType() { + return "CancelReservation"; +} + +void CancelReservation::processReq(JsonObject payload) { + if (!payload.containsKey("reservationId")) { + errorCode = "FormationViolation"; + return; + } + + if (auto reservation = reservationService.getReservationById(payload["reservationId"])) { + found = true; + reservation->clear(); + } +} + +std::unique_ptr CancelReservation::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + if (found) { + payload["status"] = "Accepted"; + } else { + payload["status"] = "Rejected"; + } + return doc; +} + +#endif //MO_ENABLE_RESERVATION diff --git a/src/MicroOcpp/Operations/CancelReservation.h b/src/MicroOcpp/Operations/CancelReservation.h new file mode 100644 index 00000000..0e39bb60 --- /dev/null +++ b/src/MicroOcpp/Operations/CancelReservation.h @@ -0,0 +1,41 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CANCELRESERVATION_H +#define MO_CANCELRESERVATION_H + +#include + +#if MO_ENABLE_RESERVATION + +#include + +namespace MicroOcpp { + +class ReservationService; + +namespace Ocpp16 { + +class CancelReservation : public Operation, public MemoryManaged { +private: + ReservationService& reservationService; + bool found = false; + const char *errorCode = nullptr; +public: + CancelReservation(ReservationService& reservationService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_RESERVATION +#endif diff --git a/src/MicroOcpp/Operations/ChangeAvailability.cpp b/src/MicroOcpp/Operations/ChangeAvailability.cpp new file mode 100644 index 00000000..7377657c --- /dev/null +++ b/src/MicroOcpp/Operations/ChangeAvailability.cpp @@ -0,0 +1,161 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include + +#include + +namespace MicroOcpp { +namespace Ocpp16 { + +ChangeAvailability::ChangeAvailability(Model& model) : MemoryManaged("v16.Operation.", "ChangeAvailability"), model(model) { + +} + +const char* ChangeAvailability::getOperationType(){ + return "ChangeAvailability"; +} + +void ChangeAvailability::processReq(JsonObject payload) { + int connectorIdRaw = payload["connectorId"] | -1; + if (connectorIdRaw < 0) { + errorCode = "FormationViolation"; + return; + } + unsigned int connectorId = (unsigned int)connectorIdRaw; + + if (connectorId >= model.getNumConnectors()) { + errorCode = "PropertyConstraintViolation"; + return; + } + + const char *type = payload["type"] | "INVALID"; + bool available = false; + + if (!strcmp(type, "Operative")) { + accepted = true; + available = true; + } else if (!strcmp(type, "Inoperative")) { + accepted = true; + available = false; + } else { + errorCode = "PropertyConstraintViolation"; + return; + } + + if (connectorId == 0) { + for (unsigned int cId = 0; cId < model.getNumConnectors(); cId++) { + auto connector = model.getConnector(cId); + connector->setAvailability(available); + if (connector->isOperative() && !available) { + scheduled = true; + } + } + } else { + auto connector = model.getConnector(connectorId); + connector->setAvailability(available); + if (connector->isOperative() && !available) { + scheduled = true; + } + } +} + +std::unique_ptr ChangeAvailability::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + if (!accepted) { + payload["status"] = "Rejected"; + } else if (scheduled) { + payload["status"] = "Scheduled"; + } else { + payload["status"] = "Accepted"; + } + + return doc; +} + +} // namespace Ocpp16 +} // namespace MicroOcpp + +#if MO_ENABLE_V201 + +#include + +namespace MicroOcpp { +namespace Ocpp201 { + +ChangeAvailability::ChangeAvailability(AvailabilityService& availabilityService) : MemoryManaged("v201.Operation.", "ChangeAvailability"), availabilityService(availabilityService) { + +} + +const char* ChangeAvailability::getOperationType(){ + return "ChangeAvailability"; +} + +void ChangeAvailability::processReq(JsonObject payload) { + + unsigned int evseId = 0; + + if (payload.containsKey("evse")) { + int evseIdRaw = payload["evse"]["id"] | -1; + if (evseIdRaw < 0) { + errorCode = "FormationViolation"; + return; + } + evseId = (unsigned int)evseIdRaw; + + if ((payload["evse"]["connectorId"] | 1) != 1) { + errorCode = "PropertyConstraintViolation"; + return; + } + } + + auto availabilityEvse = availabilityService.getEvse(evseId); + if (!availabilityEvse) { + errorCode = "PropertyConstraintViolation"; + return; + } + + const char *type = payload["operationalStatus"] | "_Undefined"; + + bool operative = false; + + if (!strcmp(type, "Operative")) { + operative = true; + } else if (!strcmp(type, "Inoperative")) { + operative = false; + } else { + errorCode = "PropertyConstraintViolation"; + return; + } + + status = availabilityEvse->changeAvailability(operative); +} + +std::unique_ptr ChangeAvailability::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + + switch (status) { + case ChangeAvailabilityStatus::Accepted: + payload["status"] = "Accepted"; + break; + case ChangeAvailabilityStatus::Scheduled: + payload["status"] = "Scheduled"; + break; + case ChangeAvailabilityStatus::Rejected: + payload["status"] = "Rejected"; + break; + } + + return doc; +} + +} // namespace Ocpp201 +} // namespace MicroOcpp + +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/ChangeAvailability.h b/src/MicroOcpp/Operations/ChangeAvailability.h new file mode 100644 index 00000000..08914e52 --- /dev/null +++ b/src/MicroOcpp/Operations/ChangeAvailability.h @@ -0,0 +1,72 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CHANGEAVAILABILITY_H +#define MO_CHANGEAVAILABILITY_H + +#include + +namespace MicroOcpp { + +class Model; + +namespace Ocpp16 { + +class ChangeAvailability : public Operation, public MemoryManaged { +private: + Model& model; + bool scheduled = false; + bool accepted = false; + + const char *errorCode {nullptr}; +public: + ChangeAvailability(Model& model); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#if MO_ENABLE_V201 + +#include +#include + +namespace MicroOcpp { + +class AvailabilityService; + +namespace Ocpp201 { + +class ChangeAvailability : public Operation, public MemoryManaged { +private: + AvailabilityService& availabilityService; + ChangeAvailabilityStatus status = ChangeAvailabilityStatus::Rejected; + + const char *errorCode {nullptr}; +public: + ChangeAvailability(AvailabilityService& availabilityService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Operations/ChangeConfiguration.cpp b/src/MicroOcpp/Operations/ChangeConfiguration.cpp new file mode 100644 index 00000000..79abaf29 --- /dev/null +++ b/src/MicroOcpp/Operations/ChangeConfiguration.cpp @@ -0,0 +1,156 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include + +#include //for tolower + +using MicroOcpp::Ocpp16::ChangeConfiguration; +using MicroOcpp::JsonDoc; + +ChangeConfiguration::ChangeConfiguration() : MemoryManaged("v16.Operation.", "ChangeConfiguration") { + +} + +const char* ChangeConfiguration::getOperationType(){ + return "ChangeConfiguration"; +} + +void ChangeConfiguration::processReq(JsonObject payload) { + const char *key = payload["key"] | ""; + if (!*key) { + errorCode = "FormationViolation"; + MO_DBG_WARN("Could not read key"); + return; + } + + if (!payload["value"].is()) { + errorCode = "FormationViolation"; + MO_DBG_WARN("Message is lacking value"); + return; + } + + const char *value = payload["value"]; + + auto configuration = getConfigurationPublic(key); + + if (!configuration) { + //configuration not found or hidden configuration + notSupported = true; + return; + } + + if (configuration->isReadOnly()) { + MO_DBG_WARN("Trying to override readonly value"); + readOnly = true; + return; + } + + //write config + + /* + * Try to interpret input as number + */ + + bool convertibleInt = true; + int numInt = 0; + bool convertibleBool = true; + bool numBool = false; + + int nDigits = 0, nNonDigits = 0, nDots = 0, nSign = 0; //"-1.234" has 4 digits, 0 nonDigits, 1 dot and 1 sign. Don't allow comma as seperator. Don't allow e-expressions (e.g. 1.23e-7) + for (const char *c = value; *c; ++c) { + if (*c >= '0' && *c <= '9') { + //int interpretation + if (nDots == 0) { //only append number if before floating point + nDigits++; + numInt *= 10; + numInt += *c - '0'; + } + } else if (*c == '.') { + nDots++; + } else if (c == value && *c == '-') { + nSign++; + } else { + nNonDigits++; + } + } + + if (nSign == 1) { + numInt = -numInt; + } + + int INT_MAXDIGITS; //plausibility check: this allows a numerical range of (-999,999,999 to 999,999,999), or (-9,999 to 9,999) respectively + if (sizeof(int) >= 4UL) + INT_MAXDIGITS = 9; + else + INT_MAXDIGITS = 4; + + if (nNonDigits > 0 || nDigits == 0 || nSign > 1 || nDots > 1) { + convertibleInt = false; + } + + if (nDigits > INT_MAXDIGITS) { + MO_DBG_DEBUG("Possible integer overflow: key = %s, value = %s", key, value); + convertibleInt = false; + } + + if (tolower(value[0]) == 't' && tolower(value[1]) == 'r' && tolower(value[2]) == 'u' && tolower(value[3]) == 'e' && !value[4]) { + numBool = true; + } else if (tolower(value[0]) == 'f' && tolower(value[1]) == 'a' && tolower(value[2]) == 'l' && tolower(value[3]) == 's' && tolower(value[4]) == 'e' && !value[5]) { + numBool = false; + } else { + convertibleBool = false; + } + + //check against optional validator + + auto validator = getConfigurationValidator(key); + if (validator && !(*validator)(value)) { + //validator exists and validation fails + reject = true; + MO_DBG_WARN("validation failed for key=%s value=%s", key, value); + return; + } + + //Store (parsed) value to Config + + if (configuration->getType() == TConfig::Int && convertibleInt) { + configuration->setInt(numInt); + } else if (configuration->getType() == TConfig::Bool && convertibleBool) { + configuration->setBool(numBool); + } else if (configuration->getType() == TConfig::String) { + configuration->setString(value); + } else { + reject = true; + MO_DBG_WARN("Value has incompatible type"); + return; + } + + if (!configuration_save()) { + MO_DBG_ERR("could not write changes to flash"); + errorCode = "InternalError"; + return; + } + + if (configuration->isRebootRequired()) { + rebootRequired = true; + } +} + +std::unique_ptr ChangeConfiguration::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + if (notSupported) { + payload["status"] = "NotSupported"; + } else if (reject || readOnly) { + payload["status"] = "Rejected"; + } else if (rebootRequired) { + payload["status"] = "RebootRequired"; + } else { + payload["status"] = "Accepted"; + } + return doc; +} diff --git a/src/MicroOcpp/Operations/ChangeConfiguration.h b/src/MicroOcpp/Operations/ChangeConfiguration.h new file mode 100644 index 00000000..3699cf6d --- /dev/null +++ b/src/MicroOcpp/Operations/ChangeConfiguration.h @@ -0,0 +1,36 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CHANGECONFIGURATION_H +#define MO_CHANGECONFIGURATION_H + +#include + +namespace MicroOcpp { +namespace Ocpp16 { + +class ChangeConfiguration : public Operation, public MemoryManaged { +private: + bool reject = false; + bool rebootRequired = false; + bool readOnly = false; + bool notSupported = false; + + const char *errorCode = nullptr; +public: + ChangeConfiguration(); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} + +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif diff --git a/src/ArduinoOcpp/MessagesV16/CiStrings.h b/src/MicroOcpp/Operations/CiStrings.h similarity index 71% rename from src/ArduinoOcpp/MessagesV16/CiStrings.h rename to src/MicroOcpp/Operations/CiStrings.h index 2d84cdd7..d52e176c 100644 --- a/src/ArduinoOcpp/MessagesV16/CiStrings.h +++ b/src/MicroOcpp/Operations/CiStrings.h @@ -1,13 +1,13 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// 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 @@ -20,6 +20,6 @@ #define CONF_KEYLEN_MAX CiString50TypeLen //not specified by OCPP -#define REASON_LEN_MAX CiString25TypeLen +#define REASON_LEN_MAX 15 #endif diff --git a/src/MicroOcpp/Operations/ClearCache.cpp b/src/MicroOcpp/Operations/ClearCache.cpp new file mode 100644 index 00000000..6666ae51 --- /dev/null +++ b/src/MicroOcpp/Operations/ClearCache.cpp @@ -0,0 +1,44 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include + +using MicroOcpp::Ocpp16::ClearCache; +using MicroOcpp::JsonDoc; + +ClearCache::ClearCache(std::shared_ptr filesystem) : MemoryManaged("v16.Operation.", "ClearCache"), filesystem(filesystem) { + +} + +const char* ClearCache::getOperationType(){ + return "ClearCache"; +} + +void ClearCache::processReq(JsonObject payload) { + MO_DBG_WARN("Clear transaction log (Authorization Cache not supported)"); + + if (!filesystem) { + //no persistency - nothing to do + return; + } + + success = FilesystemUtils::remove_if(filesystem, [] (const char *fname) -> bool { + return !strncmp(fname, "sd", strlen("sd")) || + !strncmp(fname, "tx", strlen("tx")) || + !strncmp(fname, "op", strlen("op")); + }); +} + +std::unique_ptr ClearCache::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + if (success) { + payload["status"] = "Accepted"; //"Accepted", because the intended postcondition is true + } else { + payload["status"] = "Rejected"; + } + return doc; +} diff --git a/src/MicroOcpp/Operations/ClearCache.h b/src/MicroOcpp/Operations/ClearCache.h new file mode 100644 index 00000000..110ab14a --- /dev/null +++ b/src/MicroOcpp/Operations/ClearCache.h @@ -0,0 +1,30 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CLEARCACHE_H +#define MO_CLEARCACHE_H + +#include +#include + +namespace MicroOcpp { +namespace Ocpp16 { + +class ClearCache : public Operation, public MemoryManaged { +private: + std::shared_ptr filesystem; + bool success = true; +public: + ClearCache(std::shared_ptr filesystem); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif diff --git a/src/ArduinoOcpp/MessagesV16/ClearChargingProfile.cpp b/src/MicroOcpp/Operations/ClearChargingProfile.cpp similarity index 64% rename from src/ArduinoOcpp/MessagesV16/ClearChargingProfile.cpp rename to src/MicroOcpp/Operations/ClearChargingProfile.cpp index 5dced006..e99f9bd7 100644 --- a/src/ArduinoOcpp/MessagesV16/ClearChargingProfile.cpp +++ b/src/MicroOcpp/Operations/ClearChargingProfile.cpp @@ -1,21 +1,21 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include -#include -#include -#include +#include +#include +#include #include -using ArduinoOcpp::Ocpp16::ClearChargingProfile; +using MicroOcpp::Ocpp16::ClearChargingProfile; +using MicroOcpp::JsonDoc; -ClearChargingProfile::ClearChargingProfile() { +ClearChargingProfile::ClearChargingProfile(SmartChargingService& scService) : MemoryManaged("v16.Operation.", "ClearChargingProfile"), scService(scService) { } -const char* ClearChargingProfile::getOcppOperationType(){ +const char* ClearChargingProfile::getOperationType(){ return "ClearChargingProfile"; } @@ -25,13 +25,17 @@ void ClearChargingProfile::processReq(JsonObject payload) { (int chargingProfileId, int connectorId, ChargingProfilePurposeType chargingProfilePurpose, int stackLevel) { if (payload.containsKey("id")) { - if (chargingProfileId != (payload["id"] | -1)) { + if (chargingProfileId == (payload["id"] | -1)) { + return true; + } else { return false; } } if (payload.containsKey("connectorId")) { - AO_DBG_WARN("Smart Charging does not implement multiple connectors yet. Ignore connectorId"); + if (connectorId != (payload["connectorId"] | -1)) { + return false; + } } if (payload.containsKey("chargingProfilePurpose")) { @@ -63,16 +67,11 @@ void ClearChargingProfile::processReq(JsonObject payload) { return true; }; - if (!ocppModel || !ocppModel->getSmartChargingService()) { - AO_DBG_ERR("SmartChargingService not initialized! Ignore request"); - return; - } - - matchingProfilesFound = ocppModel->getSmartChargingService()->clearChargingProfile(filter); + 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 new file mode 100644 index 00000000..40b7d210 --- /dev/null +++ b/src/MicroOcpp/Operations/ClearChargingProfile.h @@ -0,0 +1,33 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CLEARCHARGINGPROFILE_H +#define MO_CLEARCHARGINGPROFILE_H + +#include + +namespace MicroOcpp { + +class SmartChargingService; + +namespace Ocpp16 { + +class ClearChargingProfile : public Operation, public MemoryManaged { +private: + SmartChargingService& scService; + bool matchingProfilesFound = false; +public: + ClearChargingProfile(SmartChargingService& scService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Operations/CustomOperation.cpp b/src/MicroOcpp/Operations/CustomOperation.cpp new file mode 100644 index 00000000..317d01ef --- /dev/null +++ b/src/MicroOcpp/Operations/CustomOperation.cpp @@ -0,0 +1,91 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +using MicroOcpp::Ocpp16::CustomOperation; +using MicroOcpp::JsonDoc; + +CustomOperation::CustomOperation(const char *operationType, + std::function ()> fn_createReq, + std::function fn_processConf, + std::function fn_processErr) : + MemoryManaged("Operation.Custom.", operationType), + operationType{makeString(getMemoryTag(), operationType)}, + fn_createReq{fn_createReq}, + fn_processConf{fn_processConf}, + fn_processErr{fn_processErr} { + +} + +CustomOperation::CustomOperation(const char *operationType, + std::function fn_processReq, + std::function ()> fn_createConf, + std::function fn_getErrorCode, + std::function fn_getErrorDescription, + std::function ()> fn_getErrorDetails) : + MemoryManaged("Operation.Custom.", operationType), + operationType{makeString(getMemoryTag(), operationType)}, + fn_processReq{fn_processReq}, + fn_createConf{fn_createConf}, + fn_getErrorCode{fn_getErrorCode}, + fn_getErrorDescription{fn_getErrorDescription}, + fn_getErrorDetails{fn_getErrorDetails} { + +} + +CustomOperation::~CustomOperation() { + +} + +const char* CustomOperation::getOperationType() { + return operationType.c_str(); +} + +std::unique_ptr CustomOperation::createReq() { + return fn_createReq(); +} + +void CustomOperation::processConf(JsonObject payload) { + return fn_processConf(payload); +} + +bool CustomOperation::processErr(const char *code, const char *description, JsonObject details) { + if (fn_processErr) { + return fn_processErr(code, description, details); + } + return true; +} + +void CustomOperation::processReq(JsonObject payload) { + return fn_processReq(payload); +} + +std::unique_ptr CustomOperation::createConf() { + return fn_createConf(); +} + +const char *CustomOperation::getErrorCode() { + if (fn_getErrorCode) { + return fn_getErrorCode(); + } else { + return nullptr; + } +} + +const char *CustomOperation::getErrorDescription() { + if (fn_getErrorDescription) { + return fn_getErrorDescription(); + } else { + return ""; + } +} + +std::unique_ptr CustomOperation::getErrorDetails() { + if (fn_getErrorDetails) { + return fn_getErrorDetails(); + } else { + return createEmptyDocument(); + } +} diff --git a/src/MicroOcpp/Operations/CustomOperation.h b/src/MicroOcpp/Operations/CustomOperation.h new file mode 100644 index 00000000..02f72e88 --- /dev/null +++ b/src/MicroOcpp/Operations/CustomOperation.h @@ -0,0 +1,63 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_CUSTOMOPERATION_H +#define MO_CUSTOMOPERATION_H + +#include + +#include + +namespace MicroOcpp { + +namespace Ocpp16 { + +class CustomOperation : public Operation, public MemoryManaged { +private: + String operationType; + std::function ()> fn_createReq; + std::function fn_processConf; + std::function fn_processErr; //optional + std::function fn_processReq; + std::function ()> fn_createConf; + std::function fn_getErrorCode; //optional + std::function fn_getErrorDescription; //optional + std::function ()> fn_getErrorDetails; //optional +public: + + //for operations initiated at this device + CustomOperation(const char *operationType, + std::function ()> fn_createReq, + std::function fn_processConf, + std::function fn_processErr = nullptr); + + //for operations receied from remote + CustomOperation(const char *operationType, + std::function fn_processReq, + std::function ()> fn_createConf, + std::function fn_getErrorCode = nullptr, + std::function fn_getErrorDescription = nullptr, + std::function ()> fn_getErrorDetails = nullptr); + + ~CustomOperation(); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + bool processErr(const char *code, const char *description, JsonObject details) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + const char *getErrorCode() override; + const char *getErrorDescription() override; + std::unique_ptr getErrorDetails() override; +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Operations/DataTransfer.cpp b/src/MicroOcpp/Operations/DataTransfer.cpp new file mode 100644 index 00000000..5c605039 --- /dev/null +++ b/src/MicroOcpp/Operations/DataTransfer.cpp @@ -0,0 +1,50 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include + +using MicroOcpp::Ocpp16::DataTransfer; +using MicroOcpp::JsonDoc; + +DataTransfer::DataTransfer() : MemoryManaged("v16.Operation.", "DataTransfer") { + +} + +DataTransfer::DataTransfer(const String &msg) : MemoryManaged("v16.Operation.", "DataTransfer"), msg{makeString(getMemoryTag(), msg.c_str())} { + +} + +const char* DataTransfer::getOperationType(){ + return "DataTransfer"; +} + +std::unique_ptr DataTransfer::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(2) + (msg.length() + 1)); + JsonObject payload = doc->to(); + payload["vendorId"] = "CustomVendor"; + payload["data"] = msg; + return doc; +} + +void DataTransfer::processConf(JsonObject payload){ + const char *status = payload["status"] | "Invalid"; + + if (!strcmp(status, "Accepted")) { + MO_DBG_DEBUG("Request has been accepted"); + } else { + MO_DBG_INFO("Request has been denied"); + } +} + +void DataTransfer::processReq(JsonObject payload) { + // Do nothing - we're just required to reject these DataTransfer requests +} + +std::unique_ptr DataTransfer::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + payload["status"] = "Rejected"; + return doc; +} diff --git a/src/MicroOcpp/Operations/DataTransfer.h b/src/MicroOcpp/Operations/DataTransfer.h new file mode 100644 index 00000000..f7967202 --- /dev/null +++ b/src/MicroOcpp/Operations/DataTransfer.h @@ -0,0 +1,34 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_DATATRANSFER_H +#define MO_DATATRANSFER_H + +#include + +namespace MicroOcpp { +namespace Ocpp16 { + +class DataTransfer : public Operation, public MemoryManaged { +private: + String msg; +public: + DataTransfer(); + DataTransfer(const String &msg); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif 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 new file mode 100644 index 00000000..8fc99913 --- /dev/null +++ b/src/MicroOcpp/Operations/DiagnosticsStatusNotification.cpp @@ -0,0 +1,44 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::DiagnosticsStatusNotification; +using MicroOcpp::JsonDoc; + +DiagnosticsStatusNotification::DiagnosticsStatusNotification(DiagnosticsStatus status) : MemoryManaged("v16.Operation.", "DiagnosticsStatusNotification"), status(status) { + +} + +const char *DiagnosticsStatusNotification::cstrFromStatus(DiagnosticsStatus status) { + switch (status) { + case (DiagnosticsStatus::Idle): + return "Idle"; + break; + case (DiagnosticsStatus::Uploaded): + return "Uploaded"; + break; + case (DiagnosticsStatus::UploadFailed): + return "UploadFailed"; + break; + case (DiagnosticsStatus::Uploading): + return "Uploading"; + break; + } + return nullptr; //cannot be reached +} + +std::unique_ptr DiagnosticsStatusNotification::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + payload["status"] = cstrFromStatus(status); + return doc; +} + +void DiagnosticsStatusNotification::processConf(JsonObject payload){ + // no payload, nothing to do +} diff --git a/src/MicroOcpp/Operations/DiagnosticsStatusNotification.h b/src/MicroOcpp/Operations/DiagnosticsStatusNotification.h new file mode 100644 index 00000000..e21c8c07 --- /dev/null +++ b/src/MicroOcpp/Operations/DiagnosticsStatusNotification.h @@ -0,0 +1,32 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include + +#ifndef MO_DIAGNOSTICSSTATUSNOTIFICATION_H +#define MO_DIAGNOSTICSSTATUSNOTIFICATION_H + +namespace MicroOcpp { +namespace Ocpp16 { + +class DiagnosticsStatusNotification : public Operation, public MemoryManaged { +private: + DiagnosticsStatus status = DiagnosticsStatus::Idle; + static const char *cstrFromStatus(DiagnosticsStatus status); +public: + DiagnosticsStatusNotification(DiagnosticsStatus status); + + const char* getOperationType() override {return "DiagnosticsStatusNotification"; } + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#endif diff --git a/src/ArduinoOcpp/MessagesV16/FirmwareStatusNotification.cpp b/src/MicroOcpp/Operations/FirmwareStatusNotification.cpp similarity index 55% rename from src/ArduinoOcpp/MessagesV16/FirmwareStatusNotification.cpp rename to src/MicroOcpp/Operations/FirmwareStatusNotification.cpp index 5737b790..0b52c56f 100644 --- a/src/ArduinoOcpp/MessagesV16/FirmwareStatusNotification.cpp +++ b/src/MicroOcpp/Operations/FirmwareStatusNotification.cpp @@ -1,24 +1,16 @@ -// matth-x/ArduinoOcpp -// Copyright Matthias Akstaller 2019 - 2022 +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 // MIT License -#include -#include -#include -#include +#include +#include +#include +#include -using ArduinoOcpp::Ocpp16::FirmwareStatusNotification; +using MicroOcpp::Ocpp16::FirmwareStatusNotification; +using MicroOcpp::JsonDoc; -FirmwareStatusNotification::FirmwareStatusNotification() { - if (defaultOcppEngine && defaultOcppEngine->getOcppModel().getFirmwareService()) { - auto firmwareService = defaultOcppEngine->getOcppModel().getFirmwareService(); - status = firmwareService->getFirmwareStatus(); - } else { - status = FirmwareStatus::Idle; - } -} - -FirmwareStatusNotification::FirmwareStatusNotification(FirmwareStatus status) : status{status} { +FirmwareStatusNotification::FirmwareStatusNotification(FirmwareStatus status) : MemoryManaged("v16.Operation.", "FirmwareStatusNotification"), status{status} { } @@ -49,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 new file mode 100644 index 00000000..026ea45b --- /dev/null +++ b/src/MicroOcpp/Operations/FirmwareStatusNotification.h @@ -0,0 +1,33 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include + +#ifndef MO_FIRMWARESTATUSNOTIFICATION_H +#define MO_FIRMWARESTATUSNOTIFICATION_H + +namespace MicroOcpp { +namespace Ocpp16 { + +class FirmwareStatusNotification : public Operation, public MemoryManaged { +private: + FirmwareStatus status = FirmwareStatus::Idle; + static const char *cstrFromFwStatus(FirmwareStatus status); +public: + FirmwareStatusNotification(FirmwareStatus status); + + const char* getOperationType() override {return "FirmwareStatusNotification"; } + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#endif 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 new file mode 100644 index 00000000..760e0e2f --- /dev/null +++ b/src/MicroOcpp/Operations/GetCompositeSchedule.cpp @@ -0,0 +1,77 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::GetCompositeSchedule; +using MicroOcpp::JsonDoc; + +GetCompositeSchedule::GetCompositeSchedule(Model& model, SmartChargingService& scService) : MemoryManaged("v16.Operation.", "GetCompositeSchedule"), model(model), scService(scService) { + +} + +const char* GetCompositeSchedule::getOperationType() { + return "GetCompositeSchedule"; +} + +void GetCompositeSchedule::processReq(JsonObject payload) { + + connectorId = payload["connectorId"] | -1; + duration = payload["duration"] | 0; + + if (connectorId < 0 || !payload.containsKey("duration")) { + errorCode = "FormationViolation"; + return; + } + + if ((unsigned int) connectorId >= model.getNumConnectors()) { + errorCode = "PropertyConstraintViolation"; + } + + const char *unitStr = payload["chargingRateUnit"] | "_Undefined"; + + if (unitStr[0] == 'A' || unitStr[0] == 'a') { + chargingRateUnit = ChargingRateUnitType_Optional::Amp; + } else if (unitStr[0] == 'W' || unitStr[0] == 'w') { + chargingRateUnit = ChargingRateUnitType_Optional::Watt; + } +} + +std::unique_ptr GetCompositeSchedule::createConf(){ + + bool success = false; + + auto chargingSchedule = scService.getCompositeSchedule((unsigned int) connectorId, duration, chargingRateUnit); + JsonDoc chargingScheduleDoc {0}; + + if (chargingSchedule) { + success = chargingSchedule->toJson(chargingScheduleDoc); + } + + char scheduleStart_str [JSONDATE_LENGTH + 1] = {'\0'}; + + if (success && chargingSchedule) { + success = chargingSchedule->startSchedule.toJsonString(scheduleStart_str, JSONDATE_LENGTH + 1); + } + + if (success && chargingSchedule) { + auto doc = makeJsonDoc(getMemoryTag(), + JSON_OBJECT_SIZE(4) + + chargingScheduleDoc.memoryUsage()); + JsonObject payload = doc->to(); + payload["status"] = "Accepted"; + payload["connectorId"] = connectorId; + payload["scheduleStart"] = scheduleStart_str; + payload["chargingSchedule"] = chargingScheduleDoc; + return doc; + } else { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + payload["status"] = "Rejected"; + return doc; + } +} diff --git a/src/MicroOcpp/Operations/GetCompositeSchedule.h b/src/MicroOcpp/Operations/GetCompositeSchedule.h new file mode 100644 index 00000000..30e427c4 --- /dev/null +++ b/src/MicroOcpp/Operations/GetCompositeSchedule.h @@ -0,0 +1,41 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_GETCOMPOSITESCHEDULE_H +#define MO_GETCOMPOSITESCHEDULE_H + +#include +#include +#include + +namespace MicroOcpp { + +class Model; + +namespace Ocpp16 { + +class GetCompositeSchedule : public Operation, public MemoryManaged { +private: + Model& model; + SmartChargingService& scService; + int connectorId = -1; + int duration = -1; + ChargingRateUnitType_Optional chargingRateUnit = ChargingRateUnitType_Optional::None; + + const char *errorCode {nullptr}; +public: + GetCompositeSchedule(Model& model, SmartChargingService& scService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Operations/GetConfiguration.cpp b/src/MicroOcpp/Operations/GetConfiguration.cpp new file mode 100644 index 00000000..8146bba1 --- /dev/null +++ b/src/MicroOcpp/Operations/GetConfiguration.cpp @@ -0,0 +1,150 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include + +using MicroOcpp::Ocpp16::GetConfiguration; +using MicroOcpp::JsonDoc; + +GetConfiguration::GetConfiguration() : MemoryManaged("v16.Operation.", "GetConfiguration"), keys{makeVector(getMemoryTag())} { + +} + +const char* GetConfiguration::getOperationType(){ + return "GetConfiguration"; +} + +void GetConfiguration::processReq(JsonObject payload) { + + JsonArray requestedKeys = payload["key"]; + for (size_t i = 0; i < requestedKeys.size(); i++) { + keys.push_back(makeString(getMemoryTag(), requestedKeys[i].as())); + } +} + +std::unique_ptr GetConfiguration::createConf(){ + + Vector configurations = makeVector(getMemoryTag()); + Vector unknownKeys = makeVector(getMemoryTag()); + + auto containers = getConfigurationContainersPublic(); + + if (keys.empty()) { + //return all existing keys + for (auto container : containers) { + for (size_t i = 0; i < container->size(); i++) { + if (!container->getConfiguration(i)->getKey()) { + MO_DBG_ERR("invalid config"); + continue; + } + if (!container->getConfiguration(i)->isReadable()) { + continue; + } + configurations.push_back(container->getConfiguration(i)); + } + } + } else { + //only return keys that were searched using the "key" parameter + for (auto& key : keys) { + Configuration *res = nullptr; + for (auto container : containers) { + if ((res = container->getConfiguration(key.c_str()).get())) { + break; + } + } + + if (res && res->isReadable()) { + configurations.push_back(res); + } else { + unknownKeys.push_back(key.c_str()); + } + } + } + + #define VALUE_BUFSIZE 30 + + //capacity of the resulting document + size_t jcapacity = JSON_OBJECT_SIZE(2); //document root: configurationKey, unknownKey + + jcapacity += JSON_ARRAY_SIZE(configurations.size()) + configurations.size() * JSON_OBJECT_SIZE(3); //configurationKey: [{"key":...},{"key":...}] + for (auto config : configurations) { + //need to store ints by copied string: measure necessary capacity + if (config->getType() == TConfig::Int) { + char vbuf [VALUE_BUFSIZE]; + auto ret = snprintf(vbuf, VALUE_BUFSIZE, "%i", config->getInt()); + if (ret < 0 || ret >= VALUE_BUFSIZE) { + continue; + } + jcapacity += ret + 1; + } + } + + jcapacity += JSON_ARRAY_SIZE(unknownKeys.size()); + + MO_DBG_DEBUG("GetConfiguration capacity: %zu", jcapacity); + + std::unique_ptr doc; + + if (jcapacity <= MO_MAX_JSON_CAPACITY) { + doc = makeJsonDoc(getMemoryTag(), jcapacity); + } + + if (!doc || doc->capacity() < jcapacity) { + if (doc) { + MO_DBG_ERR("OOM"); + } + + errorCode = "InternalError"; + errorDescription = "Query too big. Try fewer keys"; + return nullptr; + } + + JsonObject payload = doc->to(); + + JsonArray jsonConfigurationKey = payload.createNestedArray("configurationKey"); + for (auto config : configurations) { + char vbuf [VALUE_BUFSIZE]; + const char *v = ""; + switch (config->getType()) { + case TConfig::Int: { + auto ret = snprintf(vbuf, VALUE_BUFSIZE, "%i", config->getInt()); + if (ret < 0 || ret >= VALUE_BUFSIZE) { + MO_DBG_ERR("value error"); + continue; + } + v = vbuf; + break; + } + case TConfig::Bool: + v = config->getBool() ? "true" : "false"; + break; + case TConfig::String: + v = config->getString(); + break; + } + + JsonObject jconfig = jsonConfigurationKey.createNestedObject(); + jconfig["key"] = config->getKey(); + jconfig["readonly"] = config->isReadOnly(); + if (v == vbuf) { + //value points to buffer on stack, needs to be copied into JSON memory pool + jconfig["value"] = (char*) v; + } else { + //value is static, no-copy mode + jconfig["value"] = v; + } + } + + if (!unknownKeys.empty()) { + JsonArray jsonUnknownKey = payload.createNestedArray("unknownKey"); + for (auto key : unknownKeys) { + MO_DBG_DEBUG("Unknown key: %s", key); + jsonUnknownKey.add(key); + } + } + + return doc; +} diff --git a/src/MicroOcpp/Operations/GetConfiguration.h b/src/MicroOcpp/Operations/GetConfiguration.h new file mode 100644 index 00000000..c6781119 --- /dev/null +++ b/src/MicroOcpp/Operations/GetConfiguration.h @@ -0,0 +1,36 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_GETCONFIGURATION_H +#define MO_GETCONFIGURATION_H + +#include +#include + +namespace MicroOcpp { +namespace Ocpp16 { + +class GetConfiguration : public Operation, public MemoryManaged { +private: + Vector keys; + + const char *errorCode {nullptr}; + const char *errorDescription = ""; +public: + GetConfiguration(); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} + const char *getErrorDescription() override {return errorDescription;} + +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Operations/GetDiagnostics.cpp b/src/MicroOcpp/Operations/GetDiagnostics.cpp new file mode 100644 index 00000000..e3bab7eb --- /dev/null +++ b/src/MicroOcpp/Operations/GetDiagnostics.cpp @@ -0,0 +1,63 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::GetDiagnostics; +using MicroOcpp::JsonDoc; + +GetDiagnostics::GetDiagnostics(DiagnosticsService& diagService) : MemoryManaged("v16.Operation.", "GetDiagnostics"), diagService(diagService), fileName(makeString(getMemoryTag())) { + +} + +void GetDiagnostics::processReq(JsonObject payload) { + + const char *location = payload["location"] | ""; + //check location URL. Maybe introduce Same-Origin-Policy? + if (!*location) { + errorCode = "FormationViolation"; + return; + } + + int retries = payload["retries"] | 1; + int retryInterval = payload["retryInterval"] | 180; + if (retries < 0 || retryInterval < 0) { + errorCode = "PropertyConstraintViolation"; + return; + } + + Timestamp startTime; + if (payload.containsKey("startTime")) { + if (!startTime.setTime(payload["startTime"] | "Invalid")) { + errorCode = "PropertyConstraintViolation"; + MO_DBG_WARN("bad time format"); + return; + } + } + + Timestamp stopTime; + if (payload.containsKey("stopTime")) { + if (!stopTime.setTime(payload["stopTime"] | "Invalid")) { + errorCode = "PropertyConstraintViolation"; + MO_DBG_WARN("bad time format"); + return; + } + } + + fileName = diagService.requestDiagnosticsUpload(location, (unsigned int) retries, (unsigned int) retryInterval, startTime, stopTime); +} + +std::unique_ptr GetDiagnostics::createConf(){ + if (fileName.empty()) { + return createEmptyDocument(); + } else { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + payload["fileName"] = fileName.c_str(); + return doc; + } +} diff --git a/src/MicroOcpp/Operations/GetDiagnostics.h b/src/MicroOcpp/Operations/GetDiagnostics.h new file mode 100644 index 00000000..704f9199 --- /dev/null +++ b/src/MicroOcpp/Operations/GetDiagnostics.h @@ -0,0 +1,38 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_GETDIAGNOSTICS_H +#define MO_GETDIAGNOSTICS_H + +#include +#include + +namespace MicroOcpp { + +class DiagnosticsService; + +namespace Ocpp16 { + +class GetDiagnostics : public Operation, public MemoryManaged { +private: + DiagnosticsService& diagService; + String fileName; + + const char *errorCode = nullptr; +public: + GetDiagnostics(DiagnosticsService& diagService); + + const char* getOperationType() override {return "GetDiagnostics";} + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#endif 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 new file mode 100644 index 00000000..4c2d4688 --- /dev/null +++ b/src/MicroOcpp/Operations/GetLocalListVersion.cpp @@ -0,0 +1,43 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_LOCAL_AUTH + +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::GetLocalListVersion; +using MicroOcpp::JsonDoc; + +GetLocalListVersion::GetLocalListVersion(Model& model) : MemoryManaged("v16.Operation.", "GetLocalListVersion"), model(model) { + +} + +const char* GetLocalListVersion::getOperationType(){ + return "GetLocalListVersion"; +} + +void GetLocalListVersion::processReq(JsonObject payload) { + //empty payload +} + +std::unique_ptr GetLocalListVersion::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + + auto authService = model.getAuthorizationService(); + if (authService && authService->localAuthListEnabled()) { + payload["listVersion"] = authService->getLocalListVersion(); + } else { + //TC_042_1_CS Get Local List Version (not supported) + payload["listVersion"] = -1; + } + return doc; +} + +#endif //MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Operations/GetLocalListVersion.h b/src/MicroOcpp/Operations/GetLocalListVersion.h new file mode 100644 index 00000000..554f7a6b --- /dev/null +++ b/src/MicroOcpp/Operations/GetLocalListVersion.h @@ -0,0 +1,37 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_GETLOCALLISTVERSION_H +#define MO_GETLOCALLISTVERSION_H + +#include + +#if MO_ENABLE_LOCAL_AUTH + +#include + +namespace MicroOcpp { + +class Model; + +namespace Ocpp16 { + +class GetLocalListVersion : public Operation, public MemoryManaged { +private: + Model& model; +public: + GetLocalListVersion(Model& model); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_LOCAL_AUTH +#endif 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 new file mode 100644 index 00000000..4dfd0439 --- /dev/null +++ b/src/MicroOcpp/Operations/Heartbeat.cpp @@ -0,0 +1,66 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::Heartbeat; +using MicroOcpp::JsonDoc; + +Heartbeat::Heartbeat(Model& model) : MemoryManaged("v16.Operation.", "Heartbeat"), model(model) { + +} + +const char* Heartbeat::getOperationType(){ + return "Heartbeat"; +} + +std::unique_ptr Heartbeat::createReq() { + return createEmptyDocument(); +} + +void Heartbeat::processConf(JsonObject payload) { + + const char* currentTime = payload["currentTime"] | "Invalid"; + if (strcmp(currentTime, "Invalid")) { + if (model.getClock().setTime(currentTime)) { + //success + MO_DBG_DEBUG("Request has been accepted"); + } else { + MO_DBG_WARN("Could not read time string. Expect format like 2020-02-01T20:53:32.486Z"); + } + } else { + MO_DBG_WARN("Missing field currentTime. Expect format like 2020-02-01T20:53:32.486Z"); + } +} + +void Heartbeat::processReq(JsonObject payload) { + + /** + * Ignore Contents of this Req-message, because this is for debug purposes only + */ + +} + +std::unique_ptr Heartbeat::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + (JSONDATE_LENGTH + 1)); + JsonObject payload = doc->to(); + + //safety mechanism; in some test setups the library could have to answer Heartbeats without valid system time + Timestamp ocppTimeReference = Timestamp(2019,10,0,11,59,55); + Timestamp ocppSelect = ocppTimeReference; + auto& ocppNow = model.getClock().now(); + if (ocppNow > ocppTimeReference) { + //time has already been set + ocppSelect = ocppNow; + } + + char ocppNowJson [JSONDATE_LENGTH + 1] = {'\0'}; + ocppSelect.toJsonString(ocppNowJson, JSONDATE_LENGTH + 1); + payload["currentTime"] = ocppNowJson; + + return doc; +} diff --git a/src/MicroOcpp/Operations/Heartbeat.h b/src/MicroOcpp/Operations/Heartbeat.h new file mode 100644 index 00000000..b3ce6a0e --- /dev/null +++ b/src/MicroOcpp/Operations/Heartbeat.h @@ -0,0 +1,35 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_HEARTBEAT_H +#define MO_HEARTBEAT_H + +#include + +namespace MicroOcpp { + +class Model; + +namespace Ocpp16 { + +class Heartbeat : public Operation, public MemoryManaged { +private: + Model& model; +public: + Heartbeat(Model& model); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif 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 new file mode 100644 index 00000000..ee2b4c7e --- /dev/null +++ b/src/MicroOcpp/Operations/MeterValues.cpp @@ -0,0 +1,93 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::MeterValues; +using MicroOcpp::JsonDoc; + +//can only be used for echo server debugging +MeterValues::MeterValues(Model& model) : MemoryManaged("v16.Operation.", "MeterValues"), model(model) { + +} + +MeterValues::MeterValues(Model& model, MeterValue *meterValue, unsigned int connectorId, std::shared_ptr transaction) + : MemoryManaged("v16.Operation.", "MeterValues"), model(model), meterValue{meterValue}, connectorId{connectorId}, transaction{transaction} { + +} + +MeterValues::MeterValues(Model& model, std::unique_ptr meterValue, unsigned int connectorId, std::shared_ptr transaction) + : MeterValues(model, meterValue.get(), connectorId, transaction) { + this->meterValueOwnership = std::move(meterValue); +} + +MeterValues::~MeterValues(){ + +} + +const char* MeterValues::getOperationType(){ + return "MeterValues"; +} + +std::unique_ptr MeterValues::createReq() { + + size_t capacity = 0; + + std::unique_ptr meterValueJson; + + if (meterValue) { + + if (meterValue->getTimestamp() < MIN_TIME) { + MO_DBG_DEBUG("adjust preboot MeterValue timestamp"); + Timestamp adjusted = model.getClock().adjustPrebootTimestamp(meterValue->getTimestamp()); + meterValue->setTimestamp(adjusted); + } + + meterValueJson = meterValue->toJson(); + if (meterValueJson) { + capacity += meterValueJson->capacity(); + } else { + MO_DBG_ERR("Energy meter reading not convertible to JSON"); + } + } + + capacity += JSON_OBJECT_SIZE(3); + capacity += JSON_ARRAY_SIZE(1); + + auto doc = makeJsonDoc(getMemoryTag(), capacity); + auto payload = doc->to(); + payload["connectorId"] = connectorId; + + if (transaction && transaction->getTransactionId() > 0) { //add txId if MVs are assigned to a tx with txId + payload["transactionId"] = transaction->getTransactionId(); + } + + auto meterValueArray = payload.createNestedArray("meterValue"); + if (meterValueJson) { + meterValueArray.add(*meterValueJson); + } + + return doc; +} + +void MeterValues::processConf(JsonObject payload) { + MO_DBG_DEBUG("Request has been confirmed"); +} + + +void MeterValues::processReq(JsonObject payload) { + + /** + * Ignore Contents of this Req-message, because this is for debug purposes only + */ + +} + +std::unique_ptr MeterValues::createConf(){ + return createEmptyDocument(); +} diff --git a/src/MicroOcpp/Operations/MeterValues.h b/src/MicroOcpp/Operations/MeterValues.h new file mode 100644 index 00000000..774a538a --- /dev/null +++ b/src/MicroOcpp/Operations/MeterValues.h @@ -0,0 +1,51 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_METERVALUES_H +#define MO_METERVALUES_H + +#include +#include +#include + +namespace MicroOcpp { + +class Model; +class MeterValue; +class Transaction; + +namespace Ocpp16 { + +class MeterValues : public Operation, public MemoryManaged { +private: + Model& model; //for adjusting the timestamp if MeterValue has been created before BootNotification + MeterValue *meterValue = nullptr; + std::unique_ptr meterValueOwnership; + + unsigned int connectorId = 0; + + std::shared_ptr transaction; + +public: + MeterValues(Model& model, MeterValue *meterValue, unsigned int connectorId, std::shared_ptr transaction = nullptr); + MeterValues(Model& model, std::unique_ptr meterValue, unsigned int connectorId, std::shared_ptr transaction = nullptr); + + MeterValues(Model& model); //for debugging only. Make this for the server pendant + + ~MeterValues(); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif 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 new file mode 100644 index 00000000..977f675d --- /dev/null +++ b/src/MicroOcpp/Operations/RemoteStartTransaction.cpp @@ -0,0 +1,155 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + + +#include +#include +#include +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::RemoteStartTransaction; +using MicroOcpp::JsonDoc; + +RemoteStartTransaction::RemoteStartTransaction(Model& model) : MemoryManaged("v16.Operation.", "RemoteStartTransaction"), model(model) { + +} + +const char* RemoteStartTransaction::getOperationType() { + return "RemoteStartTransaction"; +} + +void RemoteStartTransaction::processReq(JsonObject payload) { + int connectorId = payload["connectorId"] | -1; + + // OCPP 1.6 specification: connectorId SHALL be > 0 (TC_027_CS) + if (connectorId == 0) { + MO_DBG_INFO("RemoteStartTransaction rejected: connectorId SHALL not be 0"); + accepted = false; + return; + } + + if (!payload.containsKey("idTag")) { + errorCode = "FormationViolation"; + return; + } + + const char *idTag = payload["idTag"] | ""; + size_t len = strnlen(idTag, IDTAG_LEN_MAX + 1); + if (len == 0 || len > IDTAG_LEN_MAX) { + errorCode = "PropertyConstraintViolation"; + errorDescription = "idTag empty or too long"; + return; + } + + std::unique_ptr chargingProfile; + + if (payload.containsKey("chargingProfile") && model.getSmartChargingService()) { + MO_DBG_INFO("Setting Charging profile via RemoteStartTransaction"); + + JsonObject chargingProfileJson = payload["chargingProfile"]; + chargingProfile = loadChargingProfile(chargingProfileJson); + + if (!chargingProfile) { + errorCode = "PropertyConstraintViolation"; + errorDescription = "chargingProfile validation failed"; + return; + } + + if (chargingProfile->getChargingProfilePurpose() != ChargingProfilePurposeType::TxProfile) { + errorCode = "PropertyConstraintViolation"; + errorDescription = "Can only set TxProfile here"; + return; + } + + if (chargingProfile->getChargingProfileId() < 0) { + errorCode = "PropertyConstraintViolation"; + errorDescription = "RemoteStartTx profile requires non-negative chargingProfileId"; + return; + } + } + + Connector *selectConnector = nullptr; + if (connectorId >= 1) { + //connectorId specified for given connector, try to start Transaction here + if (auto connector = model.getConnector(connectorId)){ + if (!connector->getTransaction() && + connector->isOperative()) { + selectConnector = connector; + } + } + } else { + //connectorId not specified. Find free connector + for (unsigned int cid = 1; cid < model.getNumConnectors(); cid++) { + auto connector = model.getConnector(cid); + if (!connector->getTransaction() && + connector->isOperative()) { + selectConnector = connector; + connectorId = cid; + break; + } + } + } + + if (selectConnector) { + + bool success = true; + + int chargingProfileId = -1; //keep Id after moving charging profile to SCService + + if (chargingProfile && model.getSmartChargingService()) { + chargingProfileId = chargingProfile->getChargingProfileId(); + success = model.getSmartChargingService()->setChargingProfile(connectorId, std::move(chargingProfile)); + } + + if (success) { + std::shared_ptr tx; + auto authorizeRemoteTxRequests = declareConfiguration("AuthorizeRemoteTxRequests", false); + if (authorizeRemoteTxRequests && authorizeRemoteTxRequests->getBool()) { + tx = selectConnector->beginTransaction(idTag); + } else { + tx = selectConnector->beginTransaction_authorized(idTag); + } + selectConnector->updateTxNotification(TxNotification_RemoteStart); + if (tx) { + if (chargingProfileId >= 0) { + tx->setTxProfileId(chargingProfileId); + } + } else { + success = false; + } + } + + accepted = success; + } else { + MO_DBG_INFO("No connector to start transaction"); + accepted = false; + } +} + +std::unique_ptr RemoteStartTransaction::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + if (accepted) { + payload["status"] = "Accepted"; + } else { + payload["status"] = "Rejected"; + } + return doc; +} + +std::unique_ptr RemoteStartTransaction::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + + payload["idTag"] = "A0000000"; + + return doc; +} + +void RemoteStartTransaction::processConf(JsonObject payload){ + +} diff --git a/src/MicroOcpp/Operations/RemoteStartTransaction.h b/src/MicroOcpp/Operations/RemoteStartTransaction.h new file mode 100644 index 00000000..411578bd --- /dev/null +++ b/src/MicroOcpp/Operations/RemoteStartTransaction.h @@ -0,0 +1,45 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_REMOTESTARTTRANSACTION_H +#define MO_REMOTESTARTTRANSACTION_H + +#include +#include + +namespace MicroOcpp { + +class Model; +class ChargingProfile; + +namespace Ocpp16 { + +class RemoteStartTransaction : public Operation, public MemoryManaged { +private: + Model& model; + + bool accepted = false; + + const char *errorCode {nullptr}; + const char *errorDescription = ""; +public: + RemoteStartTransaction(Model& model); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} + const char *getErrorDescription() override {return errorDescription;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Operations/RemoteStopTransaction.cpp b/src/MicroOcpp/Operations/RemoteStopTransaction.cpp new file mode 100644 index 00000000..8a36699a --- /dev/null +++ b/src/MicroOcpp/Operations/RemoteStopTransaction.cpp @@ -0,0 +1,47 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::RemoteStopTransaction; +using MicroOcpp::JsonDoc; + +RemoteStopTransaction::RemoteStopTransaction(Model& model) : MemoryManaged("v16.Operation.", "RemoteStopTransaction"), model(model) { + +} + +const char* RemoteStopTransaction::getOperationType(){ + return "RemoteStopTransaction"; +} + +void RemoteStopTransaction::processReq(JsonObject payload) { + + if (!payload.containsKey("transactionId")) { + errorCode = "FormationViolation"; + } + int transactionId = payload["transactionId"]; + + for (unsigned int cId = 0; cId < model.getNumConnectors(); cId++) { + auto connector = model.getConnector(cId); + if (connector->getTransaction() && + connector->getTransaction()->getTransactionId() == transactionId) { + connector->endTransaction(nullptr, "Remote"); + accepted = true; + } + } +} + +std::unique_ptr RemoteStopTransaction::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + if (accepted){ + payload["status"] = "Accepted"; + } else { + payload["status"] = "Rejected"; + } + return doc; +} diff --git a/src/MicroOcpp/Operations/RemoteStopTransaction.h b/src/MicroOcpp/Operations/RemoteStopTransaction.h new file mode 100644 index 00000000..2a42d695 --- /dev/null +++ b/src/MicroOcpp/Operations/RemoteStopTransaction.h @@ -0,0 +1,36 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_REMOTESTOPTRANSACTION_H +#define MO_REMOTESTOPTRANSACTION_H + +#include + +namespace MicroOcpp { + +class Model; + +namespace Ocpp16 { + +class RemoteStopTransaction : public Operation, public MemoryManaged { +private: + Model& model; + bool accepted = false; + + const char *errorCode = nullptr; +public: + RemoteStopTransaction(Model& model); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif 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 new file mode 100644 index 00000000..26857fdb --- /dev/null +++ b/src/MicroOcpp/Operations/ReserveNow.cpp @@ -0,0 +1,132 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_RESERVATION + +#include +#include +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::ReserveNow; +using MicroOcpp::JsonDoc; + +ReserveNow::ReserveNow(Model& model) : MemoryManaged("v16.Operation.", "ReserveNow"), model(model) { + +} + +ReserveNow::~ReserveNow() { + +} + +const char* ReserveNow::getOperationType(){ + return "ReserveNow"; +} + +void ReserveNow::processReq(JsonObject payload) { + if (!payload.containsKey("connectorId") || + payload["connectorId"] < 0 || + !payload.containsKey("expiryDate") || + !payload.containsKey("idTag") || + //parentIdTag is optional + !payload.containsKey("reservationId")) { + errorCode = "FormationViolation"; + return; + } + + int connectorId = payload["connectorId"] | -1; + if (connectorId < 0 || (unsigned int) connectorId >= model.getNumConnectors()) { + errorCode = "PropertyConstraintViolation"; + return; + } + + Timestamp expiryDate; + if (!expiryDate.setTime(payload["expiryDate"])) { + MO_DBG_WARN("bad time format"); + errorCode = "PropertyConstraintViolation"; + return; + } + + const char *idTag = payload["idTag"] | ""; + if (!*idTag) { + errorCode = "PropertyConstraintViolation"; + return; + } + + const char *parentIdTag = nullptr; + if (payload.containsKey("parentIdTag")) { + parentIdTag = payload["parentIdTag"]; + } + + int reservationId = payload["reservationId"] | -1; + + if (model.getReservationService() && + model.getNumConnectors() > 0) { + auto rService = model.getReservationService(); + auto chargePoint = model.getConnector(0); + + auto reserveConnectorZeroSupportedBool = declareConfiguration("ReserveConnectorZeroSupported", true, CONFIGURATION_VOLATILE); + if (connectorId == 0 && (!reserveConnectorZeroSupportedBool || !reserveConnectorZeroSupportedBool->getBool())) { + reservationStatus = "Rejected"; + return; + } + + if (auto reservation = rService->getReservationById(reservationId)) { + reservation->update(reservationId, (unsigned int) connectorId, expiryDate, idTag, parentIdTag); + reservationStatus = "Accepted"; + return; + } + + Connector *connector = nullptr; + + if (connectorId > 0) { + connector = model.getConnector((unsigned int) connectorId); + } + + if (chargePoint->getStatus() == ChargePointStatus_Faulted || + (connector && connector->getStatus() == ChargePointStatus_Faulted)) { + reservationStatus = "Faulted"; + return; + } + + if (chargePoint->getStatus() == ChargePointStatus_Unavailable || + (connector && connector->getStatus() == ChargePointStatus_Unavailable)) { + reservationStatus = "Unavailable"; + return; + } + + if (connector && connector->getStatus() != ChargePointStatus_Available) { + reservationStatus = "Occupied"; + return; + } + + if (rService->updateReservation(reservationId, (unsigned int) connectorId, expiryDate, idTag, parentIdTag)) { + reservationStatus = "Accepted"; + } else { + reservationStatus = "Occupied"; + } + } else { + errorCode = "InternalError"; + } +} + +std::unique_ptr ReserveNow::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + + if (reservationStatus) { + payload["status"] = reservationStatus; + } else { + MO_DBG_ERR("didn't set reservationStatus"); + payload["status"] = "Rejected"; + } + + return doc; +} + +#endif //MO_ENABLE_RESERVATION diff --git a/src/MicroOcpp/Operations/ReserveNow.h b/src/MicroOcpp/Operations/ReserveNow.h new file mode 100644 index 00000000..cf162ad6 --- /dev/null +++ b/src/MicroOcpp/Operations/ReserveNow.h @@ -0,0 +1,43 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_RESERVENOW_H +#define MO_RESERVENOW_H + +#include + +#if MO_ENABLE_RESERVATION + +#include + +namespace MicroOcpp { + +class Model; + +namespace Ocpp16 { + +class ReserveNow : public Operation, public MemoryManaged { +private: + Model& model; + const char *errorCode = nullptr; + const char *reservationStatus = nullptr; +public: + ReserveNow(Model& model); + + ~ReserveNow(); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_RESERVATION +#endif diff --git a/src/MicroOcpp/Operations/Reset.cpp b/src/MicroOcpp/Operations/Reset.cpp new file mode 100644 index 00000000..9ab42422 --- /dev/null +++ b/src/MicroOcpp/Operations/Reset.cpp @@ -0,0 +1,119 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::Reset; +using MicroOcpp::JsonDoc; + +Reset::Reset(Model& model) : MemoryManaged("v16.Operation.", "Reset"), model(model) { + +} + +const char* Reset::getOperationType(){ + return "Reset"; +} + +void Reset::processReq(JsonObject payload) { + /* + * Process the application data here. Note: you have to implement the device reset procedure in your client code. You have to set + * a onSendConfListener in which you initiate a reset (e.g. calling ESP.reset() ) + */ + bool isHard = !strcmp(payload["type"] | "undefined", "Hard"); + + if (auto rService = model.getResetService()) { + if (!rService->getExecuteReset()) { + MO_DBG_ERR("No reset handler set. Abort operation"); + //resetAccepted remains false + } else { + if (!rService->getPreReset() || rService->getPreReset()(isHard) || isHard) { + resetAccepted = true; + rService->initiateReset(isHard); + for (unsigned int cId = 0; cId < model.getNumConnectors(); cId++) { + model.getConnector(cId)->endTransaction(nullptr, isHard ? "HardReset" : "SoftReset"); + } + } + } + } else { + resetAccepted = true; //assume that onReceiveReset is set + } +} + +std::unique_ptr Reset::createConf() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + payload["status"] = resetAccepted ? "Accepted" : "Rejected"; + return doc; +} + +#if MO_ENABLE_V201 + +#include + +namespace MicroOcpp { +namespace Ocpp201 { + +Reset::Reset(ResetService& resetService) : MemoryManaged("v201.Operation.", "Reset"), resetService(resetService) { + +} + +const char* Reset::getOperationType(){ + return "Reset"; +} + +void Reset::processReq(JsonObject payload) { + + ResetType type; + const char *typeCstr = payload["type"] | "_Undefined"; + + if (!strcmp(typeCstr, "Immediate")) { + type = ResetType_Immediate; + } else if (!strcmp(typeCstr, "OnIdle")) { + type = ResetType_OnIdle; + } else { + errorCode = "FormationViolation"; + return; + } + + int evseIdRaw = payload["evseId"] | 0; + + if (evseIdRaw < 0 || evseIdRaw >= MO_NUM_EVSEID) { + errorCode = "PropertyConstraintViolation"; + return; + } + + unsigned int evseId = (unsigned int) evseIdRaw; + + status = resetService.initiateReset(type, evseId); +} + +std::unique_ptr Reset::createConf() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + + const char *statusCstr = ""; + switch (status) { + case ResetStatus_Accepted: + statusCstr = "Accepted"; + break; + case ResetStatus_Rejected: + statusCstr = "Rejected"; + break; + case ResetStatus_Scheduled: + statusCstr = "Scheduled"; + break; + default: + MO_DBG_ERR("internal error"); + break; + } + payload["status"] = statusCstr; + return doc; +} + +} //end namespace Ocpp201 +} //end namespace MicroOcpp +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/Reset.h b/src/MicroOcpp/Operations/Reset.h new file mode 100644 index 00000000..53aca0d0 --- /dev/null +++ b/src/MicroOcpp/Operations/Reset.h @@ -0,0 +1,63 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef RESET_H +#define RESET_H + +#include +#include +#include + +namespace MicroOcpp { + +class Model; + +namespace Ocpp16 { + +class Reset : public Operation, public MemoryManaged { +private: + Model& model; + bool resetAccepted {false}; +public: + Reset(Model& model); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#if MO_ENABLE_V201 + +namespace MicroOcpp { +namespace Ocpp201 { + +class ResetService; + +class Reset : public Operation, public MemoryManaged { +private: + ResetService& resetService; + ResetStatus status; + const char *errorCode = nullptr; +public: + Reset(ResetService& resetService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_V201 +#endif 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 new file mode 100644 index 00000000..29890024 --- /dev/null +++ b/src/MicroOcpp/Operations/SendLocalList.cpp @@ -0,0 +1,80 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_LOCAL_AUTH + +#include +#include +#include + +using MicroOcpp::Ocpp16::SendLocalList; +using MicroOcpp::JsonDoc; + +SendLocalList::SendLocalList(AuthorizationService& authService) : MemoryManaged("v16.Operation.", "SendLocalList"), authService(authService) { + +} + +SendLocalList::~SendLocalList() { + +} + +const char* SendLocalList::getOperationType(){ + return "SendLocalList"; +} + +void SendLocalList::processReq(JsonObject payload) { + + //TC_043_1_CS Send Local Authorization List - NotSupported + if (!authService.localAuthListEnabled()) { + errorCode = "NotSupported"; + return; + } + + if (!payload.containsKey("listVersion") || !payload.containsKey("updateType")) { + errorCode = "FormationViolation"; + return; + } + + if (payload.containsKey("localAuthorizationList") && !payload["localAuthorizationList"].is()) { + errorCode = "FormationViolation"; + return; + } + + JsonArray localAuthorizationList = payload["localAuthorizationList"]; + + if (localAuthorizationList.size() > MO_SendLocalListMaxLength) { + errorCode = "OccurenceConstraintViolation"; + return; + } + + bool differential = !strcmp("Differential", payload["updateType"] | "Invalid"); //updateType Differential or Full + + int listVersion = payload["listVersion"]; + + if (differential && authService.getLocalListVersion() >= listVersion) { + versionMismatch = true; + return; + } + + updateFailure = !authService.updateLocalList(localAuthorizationList, listVersion, differential); +} + +std::unique_ptr SendLocalList::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + + if (versionMismatch) { + payload["status"] = "VersionMismatch"; + } else if (updateFailure) { + payload["status"] = "Failed"; + } else { + payload["status"] = "Accepted"; + } + + return doc; +} + +#endif //MO_ENABLE_LOCAL_AUTH diff --git a/src/MicroOcpp/Operations/SendLocalList.h b/src/MicroOcpp/Operations/SendLocalList.h new file mode 100644 index 00000000..d58265bd --- /dev/null +++ b/src/MicroOcpp/Operations/SendLocalList.h @@ -0,0 +1,44 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_SENDLOCALLIST_H +#define MO_SENDLOCALLIST_H + +#include + +#if MO_ENABLE_LOCAL_AUTH + +#include + +namespace MicroOcpp { + +class AuthorizationService; + +namespace Ocpp16 { + +class SendLocalList : public Operation, public MemoryManaged { +private: + AuthorizationService& authService; + const char *errorCode = nullptr; + bool updateFailure = true; + bool versionMismatch = false; +public: + SendLocalList(AuthorizationService& authService); + + ~SendLocalList(); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_LOCAL_AUTH +#endif diff --git a/src/MicroOcpp/Operations/SetChargingProfile.cpp b/src/MicroOcpp/Operations/SetChargingProfile.cpp new file mode 100644 index 00000000..1ff28acd --- /dev/null +++ b/src/MicroOcpp/Operations/SetChargingProfile.cpp @@ -0,0 +1,97 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::SetChargingProfile; +using MicroOcpp::JsonDoc; + +SetChargingProfile::SetChargingProfile(Model& model, SmartChargingService& scService) : MemoryManaged("v16.Operation.", "SetChargingProfile"), model(model), scService(scService) { + +} + +SetChargingProfile::~SetChargingProfile() { + +} + +const char* SetChargingProfile::getOperationType(){ + return "SetChargingProfile"; +} + +void SetChargingProfile::processReq(JsonObject payload) { + + int connectorId = payload["connectorId"] | -1; + if (connectorId < 0 || !payload.containsKey("csChargingProfiles")) { + errorCode = "FormationViolation"; + return; + } + + if ((unsigned int) connectorId >= model.getNumConnectors()) { + errorCode = "PropertyConstraintViolation"; + return; + } + + JsonObject csChargingProfiles = payload["csChargingProfiles"]; + + auto chargingProfile = loadChargingProfile(csChargingProfiles); + if (!chargingProfile) { + errorCode = "PropertyConstraintViolation"; + errorDescription = "csChargingProfiles validation failed"; + return; + } + + if (chargingProfile->getChargingProfilePurpose() == ChargingProfilePurposeType::TxProfile) { + // if TxProfile, check if a transaction is running + + if (connectorId == 0) { + errorCode = "PropertyConstraintViolation"; + errorDescription = "Cannot set TxProfile at connectorId 0"; + return; + } + Connector *connector = model.getConnector(connectorId); + if (!connector) { + errorCode = "PropertyConstraintViolation"; + return; + } + + auto& transaction = connector->getTransaction(); + if (!transaction || !connector->getTransaction()->isRunning()) { + //no transaction running, reject profile + accepted = false; + return; + } + + if (chargingProfile->getTransactionId() >= 0 && + chargingProfile->getTransactionId() != transaction->getTransactionId()) { + //transactionId undefined / mismatch + accepted = false; + return; + } + + //seems good + } else if (chargingProfile->getChargingProfilePurpose() == ChargingProfilePurposeType::ChargePointMaxProfile) { + if (connectorId > 0) { + errorCode = "PropertyConstraintViolation"; + errorDescription = "Cannot set ChargePointMaxProfile at connectorId > 0"; + return; + } + } + + accepted = scService.setChargingProfile(connectorId, std::move(chargingProfile)); +} + +std::unique_ptr SetChargingProfile::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + if (accepted) { + payload["status"] = "Accepted"; + } else { + payload["status"] = "Rejected"; + } + return doc; +} diff --git a/src/MicroOcpp/Operations/SetChargingProfile.h b/src/MicroOcpp/Operations/SetChargingProfile.h new file mode 100644 index 00000000..1ce35f1e --- /dev/null +++ b/src/MicroOcpp/Operations/SetChargingProfile.h @@ -0,0 +1,42 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_SETCHARGINGPROFILE_H +#define MO_SETCHARGINGPROFILE_H + +#include + +namespace MicroOcpp { + +class Model; +class SmartChargingService; + +namespace Ocpp16 { + +class SetChargingProfile : public Operation, public MemoryManaged { +private: + Model& model; + SmartChargingService& scService; + + bool accepted = false; + const char *errorCode = nullptr; + const char *errorDescription = ""; +public: + SetChargingProfile(Model& model, SmartChargingService& scService); + + ~SetChargingProfile(); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} + const char *getErrorDescription() override {return errorDescription;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif 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 new file mode 100644 index 00000000..907baaf3 --- /dev/null +++ b/src/MicroOcpp/Operations/StartTransaction.cpp @@ -0,0 +1,107 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::StartTransaction; +using MicroOcpp::JsonDoc; + + +StartTransaction::StartTransaction(Model& model, std::shared_ptr transaction) : MemoryManaged("v16.Operation.", "StartTransaction"), model(model), transaction(transaction) { + +} + +StartTransaction::~StartTransaction() { + +} + +const char* StartTransaction::getOperationType() { + return "StartTransaction"; +} + +std::unique_ptr StartTransaction::createReq() { + + auto doc = makeJsonDoc(getMemoryTag(), + JSON_OBJECT_SIZE(6) + + (IDTAG_LEN_MAX + 1) + + (JSONDATE_LENGTH + 1)); + + JsonObject payload = doc->to(); + + payload["connectorId"] = transaction->getConnectorId(); + payload["idTag"] = (char*) transaction->getIdTag(); + payload["meterStart"] = transaction->getMeterStart(); + + if (transaction->getReservationId() >= 0) { + payload["reservationId"] = transaction->getReservationId(); + } + + if (transaction->getStartTimestamp() < MIN_TIME && + transaction->getStartBootNr() == model.getBootNr()) { + MO_DBG_DEBUG("adjust preboot StartTx timestamp"); + Timestamp adjusted = model.getClock().adjustPrebootTimestamp(transaction->getStartTimestamp()); + transaction->setStartTimestamp(adjusted); + } + + char timestamp[JSONDATE_LENGTH + 1] = {'\0'}; + transaction->getStartTimestamp().toJsonString(timestamp, JSONDATE_LENGTH + 1); + payload["timestamp"] = timestamp; + + return doc; +} + +void StartTransaction::processConf(JsonObject payload) { + + const char* idTagInfoStatus = payload["idTagInfo"]["status"] | "not specified"; + if (!strcmp(idTagInfoStatus, "Accepted")) { + MO_DBG_INFO("Request has been accepted"); + } else { + MO_DBG_INFO("Request has been denied. Reason: %s", idTagInfoStatus); + transaction->setIdTagDeauthorized(); + } + + int transactionId = payload["transactionId"] | -1; + transaction->setTransactionId(transactionId); + + if (payload["idTagInfo"].containsKey("parentIdTag")) + { + transaction->setParentIdTag(payload["idTagInfo"]["parentIdTag"]); + } + + transaction->getStartSync().confirm(); + transaction->commit(); + +#if MO_ENABLE_LOCAL_AUTH + if (auto authService = model.getAuthorizationService()) { + authService->notifyAuthorization(transaction->getIdTag(), payload["idTagInfo"]); + } +#endif //MO_ENABLE_LOCAL_AUTH +} + +void StartTransaction::processReq(JsonObject payload) { + + /** + * Ignore Contents of this Req-message, because this is for debug purposes only + */ + +} + +std::unique_ptr StartTransaction::createConf() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + + JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); + idTagInfo["status"] = "Accepted"; + static int uniqueTxId = 1000; + payload["transactionId"] = uniqueTxId++; //sample data for debug purpose + + return doc; +} diff --git a/src/MicroOcpp/Operations/StartTransaction.h b/src/MicroOcpp/Operations/StartTransaction.h new file mode 100644 index 00000000..a73b5368 --- /dev/null +++ b/src/MicroOcpp/Operations/StartTransaction.h @@ -0,0 +1,43 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_STARTTRANSACTION_H +#define MO_STARTTRANSACTION_H + +#include +#include +#include +#include + +namespace MicroOcpp { + +class Model; +class Transaction; + +namespace Ocpp16 { + +class StartTransaction : public Operation, public MemoryManaged { +private: + Model& model; + std::shared_ptr transaction; +public: + + StartTransaction(Model& model, std::shared_ptr transaction); + + ~StartTransaction(); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Operations/StatusNotification.cpp b/src/MicroOcpp/Operations/StatusNotification.cpp new file mode 100644 index 00000000..c5fb1a60 --- /dev/null +++ b/src/MicroOcpp/Operations/StatusNotification.cpp @@ -0,0 +1,155 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include + +#include + +namespace MicroOcpp { + +//helper function +const char *cstrFromOcppEveState(ChargePointStatus state) { + switch (state) { + case (ChargePointStatus_Available): + return "Available"; + case (ChargePointStatus_Preparing): + return "Preparing"; + case (ChargePointStatus_Charging): + return "Charging"; + case (ChargePointStatus_SuspendedEVSE): + return "SuspendedEVSE"; + case (ChargePointStatus_SuspendedEV): + return "SuspendedEV"; + case (ChargePointStatus_Finishing): + return "Finishing"; + case (ChargePointStatus_Reserved): + return "Reserved"; + case (ChargePointStatus_Unavailable): + return "Unavailable"; + case (ChargePointStatus_Faulted): + return "Faulted"; +#if MO_ENABLE_V201 + case (ChargePointStatus_Occupied): + return "Occupied"; +#endif + default: + MO_DBG_ERR("ChargePointStatus not specified"); + /* fall through */ + case (ChargePointStatus_UNDEFINED): + return "UNDEFINED"; + } +} + +namespace Ocpp16 { + +StatusNotification::StatusNotification(int connectorId, ChargePointStatus currentStatus, const Timestamp ×tamp, ErrorData errorData) + : MemoryManaged("v16.Operation.", "StatusNotification"), connectorId(connectorId), currentStatus(currentStatus), timestamp(timestamp), errorData(errorData) { + + if (currentStatus != ChargePointStatus_UNDEFINED) { + MO_DBG_INFO("New status: %s (connectorId %d)", cstrFromOcppEveState(currentStatus), connectorId); + } +} + +const char* StatusNotification::getOperationType(){ + return "StatusNotification"; +} + +std::unique_ptr StatusNotification::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(7) + (JSONDATE_LENGTH + 1)); + JsonObject payload = doc->to(); + + payload["connectorId"] = connectorId; + if (errorData.isError) { + if (errorData.errorCode) { + payload["errorCode"] = errorData.errorCode; + } + if (errorData.info) { + payload["info"] = errorData.info; + } + if (errorData.vendorId) { + payload["vendorId"] = errorData.vendorId; + } + if (errorData.vendorErrorCode) { + payload["vendorErrorCode"] = errorData.vendorErrorCode; + } + } else if (currentStatus == ChargePointStatus_UNDEFINED) { + MO_DBG_ERR("Reporting undefined status"); + payload["errorCode"] = "InternalError"; + } else { + payload["errorCode"] = "NoError"; + } + + payload["status"] = cstrFromOcppEveState(currentStatus); + + char timestamp_cstr[JSONDATE_LENGTH + 1] = {'\0'}; + timestamp.toJsonString(timestamp_cstr, JSONDATE_LENGTH + 1); + payload["timestamp"] = timestamp_cstr; + + return doc; +} + +void StatusNotification::processConf(JsonObject payload) { + /* + * Empty payload + */ +} + +/* + * For debugging only + */ +void StatusNotification::processReq(JsonObject payload) { + +} + +/* + * For debugging only + */ +std::unique_ptr StatusNotification::createConf(){ + return createEmptyDocument(); +} + +} // namespace Ocpp16 +} // namespace MicroOcpp + +#if MO_ENABLE_V201 + +namespace MicroOcpp { +namespace Ocpp201 { + +StatusNotification::StatusNotification(EvseId evseId, ChargePointStatus currentStatus, const Timestamp ×tamp) + : MemoryManaged("v201.Operation.", "StatusNotification"), evseId(evseId), timestamp(timestamp), currentStatus(currentStatus) { + +} + +const char* StatusNotification::getOperationType(){ + return "StatusNotification"; +} + +std::unique_ptr StatusNotification::createReq() { + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(4) + (JSONDATE_LENGTH + 1)); + JsonObject payload = doc->to(); + + char timestamp_cstr[JSONDATE_LENGTH + 1] = {'\0'}; + timestamp.toJsonString(timestamp_cstr, JSONDATE_LENGTH + 1); + payload["timestamp"] = timestamp_cstr; + payload["connectorStatus"] = cstrFromOcppEveState(currentStatus); + payload["evseId"] = evseId.id; + payload["connectorId"] = evseId.id == 0 ? 0 : evseId.connectorId >= 0 ? evseId.connectorId : 1; + + return doc; +} + + +void StatusNotification::processConf(JsonObject payload) { + /* + * Empty payload + */ +} + +} // namespace Ocpp201 +} // namespace MicroOcpp + +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/StatusNotification.h b/src/MicroOcpp/Operations/StatusNotification.h new file mode 100644 index 00000000..2e65dcec --- /dev/null +++ b/src/MicroOcpp/Operations/StatusNotification.h @@ -0,0 +1,74 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef STATUSNOTIFICATION_H +#define STATUSNOTIFICATION_H + +#include +#include +#include +#include +#include + +namespace MicroOcpp { + +const char *cstrFromOcppEveState(ChargePointStatus state); + +namespace Ocpp16 { + +class StatusNotification : public Operation, public MemoryManaged { +private: + int connectorId = 1; + ChargePointStatus currentStatus = ChargePointStatus_UNDEFINED; + Timestamp timestamp; + ErrorData errorData; +public: + StatusNotification(int connectorId, ChargePointStatus currentStatus, const Timestamp ×tamp, ErrorData errorData = nullptr); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + int getConnectorId() { + return connectorId; + } +}; + +} // namespace Ocpp16 +} // namespace MicroOcpp + +#if MO_ENABLE_V201 + +#include + +namespace MicroOcpp { +namespace Ocpp201 { + +class StatusNotification : public Operation, public MemoryManaged { +private: + EvseId evseId; + Timestamp timestamp; + ChargePointStatus currentStatus = ChargePointStatus_UNDEFINED; +public: + StatusNotification(EvseId evseId, ChargePointStatus currentStatus, const Timestamp ×tamp); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; +}; + +} // namespace Ocpp201 +} // namespace MicroOcpp + +#endif //MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Operations/StopTransaction.cpp b/src/MicroOcpp/Operations/StopTransaction.cpp new file mode 100644 index 00000000..a3aef101 --- /dev/null +++ b/src/MicroOcpp/Operations/StopTransaction.cpp @@ -0,0 +1,160 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::StopTransaction; +using MicroOcpp::JsonDoc; + +StopTransaction::StopTransaction(Model& model, std::shared_ptr transaction) + : MemoryManaged("v16.Operation.", "StopTransaction"), model(model), transaction(transaction) { + +} + +StopTransaction::StopTransaction(Model& model, std::shared_ptr transaction, Vector> transactionData) + : MemoryManaged("v16.Operation.", "StopTransaction"), model(model), transaction(transaction), transactionData(std::move(transactionData)) { + +} + +const char* StopTransaction::getOperationType() { + return "StopTransaction"; +} + +std::unique_ptr StopTransaction::createReq() { + + /* + * Adjust timestamps in case they were taken before initial Clock setting + */ + if (transaction->getStopTimestamp() < MIN_TIME) { + //Timestamp taken before Clock value defined. Determine timestamp + if (transaction->getStopBootNr() == model.getBootNr()) { + //possible to calculate real timestamp + Timestamp adjusted = model.getClock().adjustPrebootTimestamp(transaction->getStopTimestamp()); + transaction->setStopTimestamp(adjusted); + } else if (transaction->getStartTimestamp() >= MIN_TIME) { + MO_DBG_WARN("set stopTime = startTime because correct time is not available"); + transaction->setStopTimestamp(transaction->getStartTimestamp() + 1); //1s behind startTime to keep order in backend DB + } else { + MO_DBG_ERR("failed to determine StopTx timestamp"); + //send invalid value + } + } + + // if StopTx timestamp is before StartTx timestamp, something probably went wrong. Restore reasonable temporal order + if (transaction->getStopTimestamp() < transaction->getStartTimestamp()) { + MO_DBG_WARN("set stopTime = startTime because stopTime was before startTime"); + transaction->setStopTimestamp(transaction->getStartTimestamp() + 1); //1s behind startTime to keep order in backend DB + } + + for (auto mv = transactionData.begin(); mv != transactionData.end(); mv++) { + if ((*mv)->getTimestamp() < MIN_TIME) { + //time off. Try to adjust, otherwise send invalid value + if ((*mv)->getReadingContext() == ReadingContext_TransactionBegin) { + (*mv)->setTimestamp(transaction->getStartTimestamp()); + } else if ((*mv)->getReadingContext() == ReadingContext_TransactionEnd) { + (*mv)->setTimestamp(transaction->getStopTimestamp()); + } else { + (*mv)->setTimestamp(transaction->getStartTimestamp() + 1); + } + } + } + + auto txDataJson = makeVector>(getMemoryTag()); + size_t txDataJson_size = 0; + for (auto mv = transactionData.begin(); mv != transactionData.end(); mv++) { + auto mvJson = (*mv)->toJson(); + if (!mvJson) { + return nullptr; + } + txDataJson_size += mvJson->capacity(); + txDataJson.emplace_back(std::move(mvJson)); + } + + auto txDataDoc = initJsonDoc(getMemoryTag(), JSON_ARRAY_SIZE(txDataJson.size()) + txDataJson_size); + for (auto mvJson = txDataJson.begin(); mvJson != txDataJson.end(); mvJson++) { + txDataDoc.add(**mvJson); + } + + auto doc = makeJsonDoc(getMemoryTag(), + JSON_OBJECT_SIZE(6) + //total of 6 fields + (IDTAG_LEN_MAX + 1) + //stop idTag + (JSONDATE_LENGTH + 1) + //timestamp string + (REASON_LEN_MAX + 1) + //reason string + txDataDoc.capacity()); + JsonObject payload = doc->to(); + + if (transaction->getStopIdTag() && *transaction->getStopIdTag()) { + payload["idTag"] = (char*) transaction->getStopIdTag(); + } + + payload["meterStop"] = transaction->getMeterStop(); + + char timestamp[JSONDATE_LENGTH + 1] = {'\0'}; + transaction->getStopTimestamp().toJsonString(timestamp, JSONDATE_LENGTH + 1); + payload["timestamp"] = timestamp; + + payload["transactionId"] = transaction->getTransactionId(); + + if (transaction->getStopReason() && *transaction->getStopReason()) { + payload["reason"] = (char*) transaction->getStopReason(); + } + + if (!transactionData.empty()) { + payload["transactionData"] = txDataDoc; + } + + return doc; +} + +void StopTransaction::processConf(JsonObject payload) { + + if (transaction) { + transaction->getStopSync().confirm(); + transaction->commit(); + } + + MO_DBG_INFO("Request has been accepted!"); + +#if MO_ENABLE_LOCAL_AUTH + if (auto authService = model.getAuthorizationService()) { + authService->notifyAuthorization(transaction->getIdTag(), payload["idTagInfo"]); + } +#endif //MO_ENABLE_LOCAL_AUTH +} + +bool StopTransaction::processErr(const char *code, const char *description, JsonObject details) { + + if (transaction) { + transaction->getStopSync().confirm(); //no retry behavior for now; consider data "arrived" at server + transaction->commit(); + } + + MO_DBG_ERR("Server error, data loss!"); + + return false; +} + +void StopTransaction::processReq(JsonObject payload) { + /** + * Ignore Contents of this Req-message, because this is for debug purposes only + */ +} + +std::unique_ptr StopTransaction::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), 2 * JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + + JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); + idTagInfo["status"] = "Accepted"; + + return doc; +} diff --git a/src/MicroOcpp/Operations/StopTransaction.h b/src/MicroOcpp/Operations/StopTransaction.h new file mode 100644 index 00000000..c573135c --- /dev/null +++ b/src/MicroOcpp/Operations/StopTransaction.h @@ -0,0 +1,50 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_STOPTRANSACTION_H +#define MO_STOPTRANSACTION_H + +#include +#include +#include +#include + +namespace MicroOcpp { + +class Model; + +class SampledValue; +class MeterValue; + +class Transaction; + +namespace Ocpp16 { + +class StopTransaction : public Operation, public MemoryManaged { +private: + Model& model; + std::shared_ptr transaction; + Vector> transactionData; +public: + + StopTransaction(Model& model, std::shared_ptr transaction); + + StopTransaction(Model& model, std::shared_ptr transaction, Vector> transactionData); + + const char* getOperationType() override; + + std::unique_ptr createReq() override; + + void processConf(JsonObject payload) override; + + bool processErr(const char *code, const char *description, JsonObject details) override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif 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 new file mode 100644 index 00000000..9b930697 --- /dev/null +++ b/src/MicroOcpp/Operations/TriggerMessage.cpp @@ -0,0 +1,85 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::TriggerMessage; +using MicroOcpp::JsonDoc; + +TriggerMessage::TriggerMessage(Context& context) : MemoryManaged("v16.Operation.", "TriggerMessage"), context(context) { + +} + +const char* TriggerMessage::getOperationType(){ + return "TriggerMessage"; +} + +void TriggerMessage::processReq(JsonObject payload) { + + const char *requestedMessage = payload["requestedMessage"] | "Invalid"; + const int connectorId = payload["connectorId"] | -1; + + MO_DBG_INFO("Execute for message type %s, connectorId = %i", requestedMessage, connectorId); + + statusMessage = "Rejected"; + + if (!strcmp(requestedMessage, "MeterValues")) { + if (auto mService = context.getModel().getMeteringService()) { + if (connectorId < 0) { + auto nConnectors = mService->getNumConnectors(); + for (decltype(nConnectors) cId = 0; cId < nConnectors; cId++) { + if (auto meterValues = mService->takeTriggeredMeterValues(cId)) { + context.getRequestQueue().sendRequestPreBoot(std::move(meterValues)); + statusMessage = "Accepted"; + } + } + } else if (connectorId < mService->getNumConnectors()) { + if (auto meterValues = mService->takeTriggeredMeterValues(connectorId)) { + context.getRequestQueue().sendRequestPreBoot(std::move(meterValues)); + statusMessage = "Accepted"; + } + } else { + errorCode = "PropertyConstraintViolation"; + } + } + } else if (!strcmp(requestedMessage, "StatusNotification")) { + unsigned int cIdRangeBegin = 0, cIdRangeEnd = 0; + if (connectorId < 0) { + cIdRangeEnd = context.getModel().getNumConnectors(); + } else if ((unsigned int) connectorId < context.getModel().getNumConnectors()) { + cIdRangeBegin = connectorId; + cIdRangeEnd = connectorId + 1; + } else { + errorCode = "PropertyConstraintViolation"; + } + + for (auto i = cIdRangeBegin; i < cIdRangeEnd; i++) { + auto connector = context.getModel().getConnector(i); + if (connector->triggerStatusNotification()) { + statusMessage = "Accepted"; + } + } + } else { + auto msg = context.getOperationRegistry().deserializeOperation(requestedMessage); + if (msg) { + context.getRequestQueue().sendRequestPreBoot(std::move(msg)); + statusMessage = "Accepted"; + } else { + statusMessage = "NotImplemented"; + } + } +} + +std::unique_ptr TriggerMessage::createConf(){ + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + payload["status"] = statusMessage; + return doc; +} diff --git a/src/MicroOcpp/Operations/TriggerMessage.h b/src/MicroOcpp/Operations/TriggerMessage.h new file mode 100644 index 00000000..7869e5a5 --- /dev/null +++ b/src/MicroOcpp/Operations/TriggerMessage.h @@ -0,0 +1,37 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_TRIGGERMESSAGE_H +#define MO_TRIGGERMESSAGE_H + +#include +#include + +namespace MicroOcpp { + +class Context; + +namespace Ocpp16 { + +class TriggerMessage : public Operation, public MemoryManaged { +private: + Context& context; + const char *statusMessage = nullptr; + + const char *errorCode = nullptr; +public: + TriggerMessage(Context& context); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp +#endif diff --git a/src/MicroOcpp/Operations/UnlockConnector.cpp b/src/MicroOcpp/Operations/UnlockConnector.cpp new file mode 100644 index 00000000..a64b7bd4 --- /dev/null +++ b/src/MicroOcpp/Operations/UnlockConnector.cpp @@ -0,0 +1,164 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include + +using MicroOcpp::Ocpp16::UnlockConnector; +using MicroOcpp::JsonDoc; + +UnlockConnector::UnlockConnector(Model& model) : MemoryManaged("v16.Operation.", "UnlockConnector"), model(model) { + +} + +const char* UnlockConnector::getOperationType(){ + return "UnlockConnector"; +} + +void UnlockConnector::processReq(JsonObject payload) { + +#if MO_ENABLE_CONNECTOR_LOCK + { + auto connectorId = payload["connectorId"] | -1; + + auto connector = model.getConnector(connectorId); + + if (!connector) { + // NotSupported + return; + } + + unlockConnector = connector->getOnUnlockConnector(); + + if (!unlockConnector) { + // NotSupported + return; + } + + connector->endTransaction(nullptr, "UnlockCommand"); + connector->updateTxNotification(TxNotification_RemoteStop); + + cbUnlockResult = unlockConnector(); + + timerStart = mocpp_tick_ms(); + } +#endif //MO_ENABLE_CONNECTOR_LOCK +} + +std::unique_ptr UnlockConnector::createConf() { + + const char *status = "NotSupported"; + +#if MO_ENABLE_CONNECTOR_LOCK + if (unlockConnector) { + + if (mocpp_tick_ms() - timerStart < MO_UNLOCK_TIMEOUT) { + //do poll and if more time is needed, delay creation of conf msg + + if (cbUnlockResult == UnlockConnectorResult_Pending) { + cbUnlockResult = unlockConnector(); + if (cbUnlockResult == UnlockConnectorResult_Pending) { + return nullptr; //no result yet - delay confirmation response + } + } + } + + if (cbUnlockResult == UnlockConnectorResult_Unlocked) { + status = "Unlocked"; + } else { + status = "UnlockFailed"; + } + } +#endif //MO_ENABLE_CONNECTOR_LOCK + + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + payload["status"] = status; + return doc; +} + + +#if MO_ENABLE_V201 +#if MO_ENABLE_CONNECTOR_LOCK + +#include + +namespace MicroOcpp { +namespace Ocpp201 { + +UnlockConnector::UnlockConnector(RemoteControlService& rcService) : MemoryManaged("v201.Operation.UnlockConnector"), rcService(rcService) { + +} + +const char* UnlockConnector::getOperationType(){ + return "UnlockConnector"; +} + +void UnlockConnector::processReq(JsonObject payload) { + + int evseId = payload["evseId"] | -1; + int connectorId = payload["connectorId"] | -1; + + if (evseId < 1 || evseId >= MO_NUM_EVSEID || connectorId < 1) { + errorCode = "PropertyConstraintViolation"; + return; + } + + if (connectorId != 1) { + status = UnlockStatus_UnknownConnector; + return; + } + + rcEvse = rcService.getEvse(evseId); + if (!rcEvse) { + status = UnlockStatus_UnlockFailed; + return; + } + + status = rcEvse->unlockConnector(); + timerStart = mocpp_tick_ms(); +} + +std::unique_ptr UnlockConnector::createConf() { + + if (rcEvse && status == UnlockStatus_PENDING && mocpp_tick_ms() - timerStart < MO_UNLOCK_TIMEOUT) { + status = rcEvse->unlockConnector(); + + if (status == UnlockStatus_PENDING) { + return nullptr; //no result yet - delay confirmation response + } + } + + const char *statusStr = ""; + switch (status) { + case UnlockStatus_Unlocked: + statusStr = "Unlocked"; + break; + case UnlockStatus_UnlockFailed: + statusStr = "UnlockFailed"; + break; + case UnlockStatus_OngoingAuthorizedTransaction: + statusStr = "OngoingAuthorizedTransaction"; + break; + case UnlockStatus_UnknownConnector: + statusStr = "UnknownConnector"; + break; + case UnlockStatus_PENDING: + MO_DBG_ERR("UnlockConnector timeout"); + statusStr = "UnlockFailed"; + break; + } + + auto doc = makeJsonDoc(getMemoryTag(), JSON_OBJECT_SIZE(1)); + JsonObject payload = doc->to(); + payload["status"] = statusStr; + return doc; +} + +} // namespace Ocpp201 +} // namespace MicroOcpp + +#endif //MO_ENABLE_CONNECTOR_LOCK +#endif //MO_ENABLE_V201 diff --git a/src/MicroOcpp/Operations/UnlockConnector.h b/src/MicroOcpp/Operations/UnlockConnector.h new file mode 100644 index 00000000..518653cd --- /dev/null +++ b/src/MicroOcpp/Operations/UnlockConnector.h @@ -0,0 +1,85 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef UNLOCKCONNECTOR_H +#define UNLOCKCONNECTOR_H + +#include + +#include +#include +#include + +namespace MicroOcpp { + +class Model; + +namespace Ocpp16 { + +class UnlockConnector : public Operation, public MemoryManaged { +private: + Model& model; + +#if MO_ENABLE_CONNECTOR_LOCK + std::function unlockConnector; + UnlockConnectorResult cbUnlockResult; + unsigned long timerStart = 0; //for timeout +#endif //MO_ENABLE_CONNECTOR_LOCK + + const char *errorCode = nullptr; +public: + UnlockConnector(Model& model); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#if MO_ENABLE_V201 +#if MO_ENABLE_CONNECTOR_LOCK + +#include + +namespace MicroOcpp { + +class RemoteControlService; +class RemoteControlServiceEvse; + +namespace Ocpp201 { + +class UnlockConnector : public Operation, public MemoryManaged { +private: + RemoteControlService& rcService; + RemoteControlServiceEvse *rcEvse = nullptr; + + UnlockStatus status; + unsigned long timerStart = 0; //for timeout + + const char *errorCode = nullptr; +public: + UnlockConnector(RemoteControlService& rcService); + + const char* getOperationType() override; + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp201 +} //end namespace MicroOcpp + +#endif //MO_ENABLE_CONNECTOR_LOCK +#endif //MO_ENABLE_V201 + +#endif diff --git a/src/MicroOcpp/Operations/UpdateFirmware.cpp b/src/MicroOcpp/Operations/UpdateFirmware.cpp new file mode 100644 index 00000000..d9be551f --- /dev/null +++ b/src/MicroOcpp/Operations/UpdateFirmware.cpp @@ -0,0 +1,51 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include + +using MicroOcpp::Ocpp16::UpdateFirmware; +using MicroOcpp::JsonDoc; + +UpdateFirmware::UpdateFirmware(FirmwareService& fwService) : MemoryManaged("v16.Operation.", "UpdateFirmware"), fwService(fwService) { + +} + +void UpdateFirmware::processReq(JsonObject payload) { + + const char *location = payload["location"] | ""; + //check location URL. Maybe introduce Same-Origin-Policy? + if (!*location) { + errorCode = "FormationViolation"; + return; + } + + int retries = payload["retries"] | 1; + int retryInterval = payload["retryInterval"] | 180; + if (retries < 0 || retryInterval < 0) { + errorCode = "PropertyConstraintViolation"; + return; + } + + //check the integrity of retrieveDate + if (!payload.containsKey("retrieveDate")) { + errorCode = "FormationViolation"; + return; + } + + Timestamp retrieveDate; + if (!retrieveDate.setTime(payload["retrieveDate"] | "Invalid")) { + errorCode = "PropertyConstraintViolation"; + MO_DBG_WARN("bad time format"); + return; + } + + fwService.scheduleFirmwareUpdate(location, retrieveDate, (unsigned int) retries, (unsigned int) retryInterval); +} + +std::unique_ptr UpdateFirmware::createConf(){ + return createEmptyDocument(); +} diff --git a/src/MicroOcpp/Operations/UpdateFirmware.h b/src/MicroOcpp/Operations/UpdateFirmware.h new file mode 100644 index 00000000..da962141 --- /dev/null +++ b/src/MicroOcpp/Operations/UpdateFirmware.h @@ -0,0 +1,37 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_UPDATEFIRMWARE_H +#define MO_UPDATEFIRMWARE_H + +#include +#include + +namespace MicroOcpp { + +class FirmwareService; + +namespace Ocpp16 { + +class UpdateFirmware : public Operation, public MemoryManaged { +private: + FirmwareService& fwService; + + const char *errorCode = nullptr; +public: + UpdateFirmware(FirmwareService& fwService); + + const char* getOperationType() override {return "UpdateFirmware";} + + void processReq(JsonObject payload) override; + + std::unique_ptr createConf() override; + + const char *getErrorCode() override {return errorCode;} +}; + +} //end namespace Ocpp16 +} //end namespace MicroOcpp + +#endif diff --git a/src/MicroOcpp/Platform.cpp b/src/MicroOcpp/Platform.cpp new file mode 100644 index 00000000..75980725 --- /dev/null +++ b/src/MicroOcpp/Platform.cpp @@ -0,0 +1,112 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#ifdef MO_CUSTOM_CONSOLE + +char _mo_console_msg_buf [MO_CUSTOM_CONSOLE_MAXMSGSIZE]; + +namespace MicroOcpp { +void (*mocpp_console_out_impl)(const char *msg) = nullptr; +} + +void _mo_console_out(const char *msg) { + if (MicroOcpp::mocpp_console_out_impl) { + MicroOcpp::mocpp_console_out_impl(msg); + } +} + +void mocpp_set_console_out(void (*console_out)(const char *msg)) { + MicroOcpp::mocpp_console_out_impl = console_out; + if (console_out) { + console_out("[OCPP] console initialized\n"); + } +} + +#endif + +#ifdef MO_CUSTOM_TIMER +unsigned long (*mocpp_tick_ms_impl)() = nullptr; + +void mocpp_set_timer(unsigned long (*get_ms)()) { + mocpp_tick_ms_impl = get_ms; +} + +unsigned long mocpp_tick_ms_custom() { + if (mocpp_tick_ms_impl) { + return mocpp_tick_ms_impl(); + } else { + return 0; + } +} +#else + +#if MO_PLATFORM == MO_PLATFORM_ESPIDF +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +namespace MicroOcpp { + +decltype(xTaskGetTickCount()) mocpp_ticks_count = 0; +unsigned long mocpp_millis_count = 0; + +} + +unsigned long mocpp_tick_ms_espidf() { + auto ticks_now = xTaskGetTickCount(); + MicroOcpp::mocpp_millis_count += ((ticks_now - MicroOcpp::mocpp_ticks_count) * 1000UL) / configTICK_RATE_HZ; + MicroOcpp::mocpp_ticks_count = ticks_now; + return MicroOcpp::mocpp_millis_count; +} + +#elif MO_PLATFORM == MO_PLATFORM_UNIX +#include + +namespace MicroOcpp { + +std::chrono::steady_clock::time_point clock_reference; +bool clock_initialized = false; + +} + +unsigned long mocpp_tick_ms_unix() { + if (!MicroOcpp::clock_initialized) { + MicroOcpp::clock_reference = std::chrono::steady_clock::now(); + MicroOcpp::clock_initialized = true; + } + std::chrono::milliseconds ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - MicroOcpp::clock_reference); + return (unsigned long) ms.count(); +} +#endif +#endif + +#ifdef MO_CUSTOM_RNG +uint32_t (*mocpp_rng_impl)() = nullptr; + +void mocpp_set_rng(uint32_t (*rng)()) { + mocpp_rng_impl = rng; +} + +uint32_t mocpp_rng_custom(void) { + if (mocpp_rng_impl) { + return mocpp_rng_impl(); + } else { + return 0; + } +} +#else + +// Time-based Pseudo RNG. +// Contains internal state which is mixed with the current timestamp +// each time it is called. Then this is passed through a multiply-with-carry +// PRNG operation to get a pseudo-random number. +uint32_t mocpp_time_based_prng(void) { + static uint32_t prng_state = 1; + uint32_t entropy = mocpp_tick_ms(); + prng_state = (prng_state ^ entropy)*1664525U + 1013904223U; // assuming complement-2 integers and non-signaling overflow + return prng_state; +} +#endif diff --git a/src/MicroOcpp/Platform.h b/src/MicroOcpp/Platform.h new file mode 100644 index 00000000..e5b8de09 --- /dev/null +++ b/src/MicroOcpp/Platform.h @@ -0,0 +1,123 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_PLATFORM_H +#define MO_PLATFORM_H + +#include + +#define MO_PLATFORM_NONE 0 +#define MO_PLATFORM_ARDUINO 1 +#define MO_PLATFORM_ESPIDF 2 +#define MO_PLATFORM_UNIX 3 + +#ifndef MO_PLATFORM +#define MO_PLATFORM MO_PLATFORM_ARDUINO +#endif + +#ifdef __cplusplus +#define MO_EXTERN_C extern "C" +#else +#define MO_EXTERN_C +#endif + +#if MO_PLATFORM == MO_PLATFORM_NONE +#ifndef MO_CUSTOM_CONSOLE +#define MO_CUSTOM_CONSOLE +#endif +#ifndef MO_CUSTOM_TIMER +#define MO_CUSTOM_TIMER +#endif +#endif + +#ifdef MO_CUSTOM_CONSOLE +#include + +#ifndef MO_CUSTOM_CONSOLE_MAXMSGSIZE +#define MO_CUSTOM_CONSOLE_MAXMSGSIZE 256 +#endif + +extern char _mo_console_msg_buf [MO_CUSTOM_CONSOLE_MAXMSGSIZE]; //define msg_buf in data section to save memory (see https://github.com/matth-x/MicroOcpp/pull/304) +MO_EXTERN_C void _mo_console_out(const char *msg); + +MO_EXTERN_C void mocpp_set_console_out(void (*console_out)(const char *msg)); + +#define MO_CONSOLE_PRINTF(X, ...) \ + do { \ + auto _mo_ret = snprintf(_mo_console_msg_buf, MO_CUSTOM_CONSOLE_MAXMSGSIZE, X, ##__VA_ARGS__); \ + if (_mo_ret < 0 || _mo_ret >= MO_CUSTOM_CONSOLE_MAXMSGSIZE) { \ + sprintf(_mo_console_msg_buf + MO_CUSTOM_CONSOLE_MAXMSGSIZE - 7, " [...]"); \ + } \ + _mo_console_out(_mo_console_msg_buf); \ + } while (0) +#else +#define mocpp_set_console_out(X) \ + do { \ + X("[OCPP] CONSOLE ERROR: mocpp_set_console_out ignored if MO_CUSTOM_CONSOLE " \ + "not defined\n"); \ + char msg [196]; \ + snprintf(msg, 196, " > see %s:%i",__FILE__,__LINE__); \ + X(msg); \ + X("\n > see MicroOcpp/Platform.h\n"); \ + } while (0) + +#if MO_PLATFORM == MO_PLATFORM_ARDUINO +#include +#ifndef MO_USE_SERIAL +#define MO_USE_SERIAL Serial +#endif + +#define MO_CONSOLE_PRINTF(X, ...) MO_USE_SERIAL.printf_P(PSTR(X), ##__VA_ARGS__) +#elif MO_PLATFORM == MO_PLATFORM_ESPIDF +#include "esp_log.h" + +#define MO_CONSOLE_PRINTF(X, ...) esp_log_write(ESP_LOG_INFO, "MicroOcpp", X, ##__VA_ARGS__) +#elif MO_PLATFORM == MO_PLATFORM_UNIX +#include + +#define MO_CONSOLE_PRINTF(X, ...) printf(X, ##__VA_ARGS__) +#endif +#endif + +#ifdef MO_CUSTOM_TIMER +MO_EXTERN_C void mocpp_set_timer(unsigned long (*get_ms)()); + +MO_EXTERN_C unsigned long mocpp_tick_ms_custom(); +#define mocpp_tick_ms mocpp_tick_ms_custom +#else + +#if MO_PLATFORM == MO_PLATFORM_ARDUINO +#include +#define mocpp_tick_ms millis +#elif MO_PLATFORM == MO_PLATFORM_ESPIDF +MO_EXTERN_C unsigned long mocpp_tick_ms_espidf(); +#define mocpp_tick_ms mocpp_tick_ms_espidf +#elif MO_PLATFORM == MO_PLATFORM_UNIX +MO_EXTERN_C unsigned long mocpp_tick_ms_unix(); +#define mocpp_tick_ms mocpp_tick_ms_unix +#endif +#endif + +#ifdef MO_CUSTOM_RNG +MO_EXTERN_C void mocpp_set_rng(uint32_t (*rng)()); +MO_EXTERN_C uint32_t mocpp_rng_custom(); +#define mocpp_rng mocpp_rng_custom +#else +MO_EXTERN_C uint32_t mocpp_time_based_prng(void); +#define mocpp_rng mocpp_time_based_prng +#endif + +#ifndef MO_MAX_JSON_CAPACITY +#if MO_PLATFORM == MO_PLATFORM_UNIX +#define MO_MAX_JSON_CAPACITY 16384 +#else +#define MO_MAX_JSON_CAPACITY 4096 +#endif +#endif + +#ifndef MO_ENABLE_MBEDTLS +#define MO_ENABLE_MBEDTLS 0 +#endif + +#endif diff --git a/src/MicroOcpp/Version.h b/src/MicroOcpp/Version.h new file mode 100644 index 00000000..92a2ea46 --- /dev/null +++ b/src/MicroOcpp/Version.h @@ -0,0 +1,52 @@ +// 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 new file mode 100644 index 00000000..381084b0 --- /dev/null +++ b/src/MicroOcpp_c.cpp @@ -0,0 +1,462 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include "MicroOcpp_c.h" +#include "MicroOcpp.h" + +#include +#include +#include +#include +#include + +#include +#include + +MicroOcpp::Connection *ocppSocket = nullptr; + +void ocpp_initialize(OCPP_Connection *conn, const char *chargePointModel, const char *chargePointVendor, struct OCPP_FilesystemOpt fsopt, bool autoRecover, bool ocpp201) { + ocpp_initialize_full(conn, ocpp201 ? + ChargerCredentials::v201(chargePointModel, chargePointVendor) : + ChargerCredentials(chargePointModel, chargePointVendor), + fsopt, autoRecover, ocpp201); +} + +void ocpp_initialize_full(OCPP_Connection *conn, const char *bootNotificationCredentials, struct OCPP_FilesystemOpt fsopt, bool autoRecover, bool ocpp201) { + if (!conn) { + MO_DBG_ERR("conn is null"); + } + + ocppSocket = reinterpret_cast(conn); + + MicroOcpp::FilesystemOpt adaptFsopt = fsopt; + + mocpp_initialize(*ocppSocket, bootNotificationCredentials, MicroOcpp::makeDefaultFilesystemAdapter(adaptFsopt), autoRecover, + ocpp201 ? + MicroOcpp::ProtocolVersion(2,0,1) : + MicroOcpp::ProtocolVersion(1,6)); +} + +void ocpp_initialize_full2(OCPP_Connection *conn, const char *bootNotificationCredentials, FilesystemAdapterC *filesystem, bool autoRecover, bool ocpp201) { + if (!conn) { + MO_DBG_ERR("conn is null"); + } + + ocppSocket = reinterpret_cast(conn); + + mocpp_initialize(*ocppSocket, bootNotificationCredentials, *reinterpret_cast*>(filesystem), autoRecover, + ocpp201 ? + MicroOcpp::ProtocolVersion(2,0,1) : + MicroOcpp::ProtocolVersion(1,6)); +} + +void ocpp_deinitialize() { + mocpp_deinitialize(); +} + +bool ocpp_is_initialized() { + return getOcppContext() != nullptr; +} + +void ocpp_loop() { + mocpp_loop(); +} + +/* + * Helper functions for transforming callback functions from C-style to C++style + */ + +std::function adaptFn(InputBool fn) { + return fn; +} + +std::function adaptFn(unsigned int connectorId, InputBool_m fn) { + return [fn, connectorId] () {return fn(connectorId);}; +} + +std::function adaptFn(InputString fn) { + return fn; +} + +std::function adaptFn(unsigned int connectorId, InputString_m fn) { + return [fn, connectorId] () {return fn(connectorId);}; +} + +std::function adaptFn(InputFloat fn) { + return fn; +} + +std::function adaptFn(unsigned int connectorId, InputFloat_m fn) { + return [fn, connectorId] () {return fn(connectorId);}; +} + +std::function adaptFn(InputInt fn) { + return fn; +} + +std::function adaptFn(unsigned int connectorId, InputInt_m fn) { + return [fn, connectorId] () {return fn(connectorId);}; +} + +std::function adaptFn(OutputFloat fn) { + return fn; +} + +std::function adaptFn(OutputSmartCharging fn) { + return fn; +} + +std::function adaptFn(unsigned int connectorId, OutputSmartCharging_m fn) { + return [fn, connectorId] (float power, float current, int nphases) {fn(connectorId, power, current, nphases);}; +} + +std::function adaptFn(unsigned int connectorId, OutputFloat_m fn) { + return [fn, connectorId] (float value) {return fn(connectorId, value);}; +} + +std::function adaptFn(void (*fn)(void)) { + return fn; +} + +#ifndef MO_RECEIVE_PAYLOAD_BUFSIZE +#define MO_RECEIVE_PAYLOAD_BUFSIZE 512 +#endif + +char ocpp_recv_payload_buff [MO_RECEIVE_PAYLOAD_BUFSIZE] = {'\0'}; + +std::function adaptFn(OnMessage fn) { + if (!fn) return nullptr; + return [fn] (JsonObject payload) { + auto len = serializeJson(payload, ocpp_recv_payload_buff, MO_RECEIVE_PAYLOAD_BUFSIZE); + if (len <= 0) { + MO_DBG_WARN("Received payload buffer exceeded. Continue without payload"); + } + fn(len > 0 ? ocpp_recv_payload_buff : "", len); + }; +} + +MicroOcpp::OnReceiveErrorListener adaptFn(OnCallError fn) { + if (!fn) return nullptr; + return [fn] (const char *code, const char *description, JsonObject details) { + auto len = serializeJson(details, ocpp_recv_payload_buff, MO_RECEIVE_PAYLOAD_BUFSIZE); + if (len <= 0) { + MO_DBG_WARN("Received payload buffer exceeded. Continue without payload"); + } + fn(code, description, len > 0 ? ocpp_recv_payload_buff : "", len); + }; +} + +#if MO_ENABLE_CONNECTOR_LOCK +std::function adaptFn(PollUnlockResult fn) { + return [fn] () {return fn();}; +} + +std::function adaptFn(unsigned int connectorId, PollUnlockResult_m fn) { + return [fn, connectorId] () {return fn(connectorId);}; +} +#endif //MO_ENABLE_CONNECTOR_LOCK + +bool ocpp_beginTransaction(const char *idTag) { + return beginTransaction(idTag); +} +bool ocpp_beginTransaction_m(unsigned int connectorId, const char *idTag) { + return beginTransaction(idTag, connectorId); +} + +bool ocpp_beginTransaction_authorized(const char *idTag, const char *parentIdTag) { + return beginTransaction_authorized(idTag, parentIdTag); +} +bool ocpp_beginTransaction_authorized_m(unsigned int connectorId, const char *idTag, const char *parentIdTag) { + return beginTransaction_authorized(idTag, parentIdTag, connectorId); +} + +bool ocpp_endTransaction(const char *idTag, const char *reason) { + return endTransaction(idTag, reason); +} +bool ocpp_endTransaction_m(unsigned int connectorId, const char *idTag, const char *reason) { + return endTransaction(idTag, reason, connectorId); +} + +bool ocpp_endTransaction_authorized(const char *idTag, const char *reason) { + return endTransaction_authorized(idTag, reason); +} +bool ocpp_endTransaction_authorized_m(unsigned int connectorId, const char *idTag, const char *reason) { + return endTransaction_authorized(idTag, reason, connectorId); +} + +bool ocpp_isTransactionActive() { + return isTransactionActive(); +} +bool ocpp_isTransactionActive_m(unsigned int connectorId) { + return isTransactionActive(connectorId); +} + +bool ocpp_isTransactionRunning() { + return isTransactionRunning(); +} +bool ocpp_isTransactionRunning_m(unsigned int connectorId) { + return isTransactionRunning(connectorId); +} + +const char *ocpp_getTransactionIdTag() { + return getTransactionIdTag(); +} +const char *ocpp_getTransactionIdTag_m(unsigned int connectorId) { + return getTransactionIdTag(connectorId); +} + +OCPP_Transaction *ocpp_getTransaction() { + return ocpp_getTransaction_m(1); +} +OCPP_Transaction *ocpp_getTransaction_m(unsigned int connectorId) { + #if MO_ENABLE_V201 + { + if (!getOcppContext()) { + MO_DBG_ERR("OCPP uninitialized"); //need to call mocpp_initialize before + return nullptr; + } + if (getOcppContext()->getModel().getVersion().major == 2) { + ocpp_tx_compat_setV201(true); //set the ocpp_tx C-API into v201 mode globally + if (getTransactionV201(connectorId)) { + return reinterpret_cast(getTransactionV201(connectorId)); + } else { + return nullptr; + } + } else { + ocpp_tx_compat_setV201(false); //set the ocpp_tx C-API into v16 mode globally + //continue with V16 implementation + } + } + #endif //MO_ENABLE_V201 + if (getTransaction(connectorId)) { + return reinterpret_cast(getTransaction(connectorId).get()); + } else { + return nullptr; + } +} + +bool ocpp_ocppPermitsCharge() { + return ocppPermitsCharge(); +} +bool ocpp_ocppPermitsCharge_m(unsigned int connectorId) { + return ocppPermitsCharge(connectorId); +} + +ChargePointStatus ocpp_getChargePointStatus() { + return getChargePointStatus(); +} + +ChargePointStatus ocpp_getChargePointStatus_m(unsigned int connectorId) { + return getChargePointStatus(connectorId); +} + +void ocpp_setConnectorPluggedInput(InputBool pluggedInput) { + setConnectorPluggedInput(adaptFn(pluggedInput)); +} +void ocpp_setConnectorPluggedInput_m(unsigned int connectorId, InputBool_m pluggedInput) { + setConnectorPluggedInput(adaptFn(connectorId, pluggedInput), connectorId); +} + +void ocpp_setEnergyMeterInput(InputInt energyInput) { + setEnergyMeterInput(adaptFn(energyInput)); +} +void ocpp_setEnergyMeterInput_m(unsigned int connectorId, InputInt_m energyInput) { + setEnergyMeterInput(adaptFn(connectorId, energyInput), connectorId); +} + +void ocpp_setPowerMeterInput(InputFloat powerInput) { + setPowerMeterInput(adaptFn(powerInput)); +} +void ocpp_setPowerMeterInput_m(unsigned int connectorId, InputFloat_m powerInput) { + setPowerMeterInput(adaptFn(connectorId, powerInput), connectorId); +} + +void ocpp_setSmartChargingPowerOutput(OutputFloat maxPowerOutput) { + setSmartChargingPowerOutput(adaptFn(maxPowerOutput)); +} +void ocpp_setSmartChargingPowerOutput_m(unsigned int connectorId, OutputFloat_m maxPowerOutput) { + setSmartChargingPowerOutput(adaptFn(connectorId, maxPowerOutput), connectorId); +} +void ocpp_setSmartChargingCurrentOutput(OutputFloat maxCurrentOutput) { + setSmartChargingCurrentOutput(adaptFn(maxCurrentOutput)); +} +void ocpp_setSmartChargingCurrentOutput_m(unsigned int connectorId, OutputFloat_m maxCurrentOutput) { + setSmartChargingCurrentOutput(adaptFn(connectorId, maxCurrentOutput), connectorId); +} +void ocpp_setSmartChargingOutput(OutputSmartCharging chargingLimitOutput) { + setSmartChargingOutput(adaptFn(chargingLimitOutput)); +} +void ocpp_setSmartChargingOutput_m(unsigned int connectorId, OutputSmartCharging_m chargingLimitOutput) { + setSmartChargingOutput(adaptFn(connectorId, chargingLimitOutput), connectorId); +} + +void ocpp_setEvReadyInput(InputBool evReadyInput) { + setEvReadyInput(adaptFn(evReadyInput)); +} +void ocpp_setEvReadyInput_m(unsigned int connectorId, InputBool_m evReadyInput) { + setEvReadyInput(adaptFn(connectorId, evReadyInput), connectorId); +} + +void ocpp_setEvseReadyInput(InputBool evseReadyInput) { + setEvseReadyInput(adaptFn(evseReadyInput)); +} +void ocpp_setEvseReadyInput_m(unsigned int connectorId, InputBool_m evseReadyInput) { + setEvseReadyInput(adaptFn(connectorId, evseReadyInput), connectorId); +} + +void ocpp_addErrorCodeInput(InputString errorCodeInput) { + addErrorCodeInput(adaptFn(errorCodeInput)); +} +void ocpp_addErrorCodeInput_m(unsigned int connectorId, InputString_m errorCodeInput) { + addErrorCodeInput(adaptFn(connectorId, errorCodeInput), connectorId); +} + +void ocpp_addMeterValueInputFloat(InputFloat valueInput, const char *measurand, const char *unit, const char *location, const char *phase) { + addMeterValueInput(adaptFn(valueInput), measurand, unit, location, phase, 1); +} +void ocpp_addMeterValueInputFloat_m(unsigned int connectorId, InputFloat_m valueInput, const char *measurand, const char *unit, const char *location, const char *phase) { + addMeterValueInput(adaptFn(connectorId, valueInput), measurand, unit, location, phase, connectorId); +} + +void ocpp_addMeterValueInputIntTx(int (*valueInput)(ReadingContext), const char *measurand, const char *unit, const char *location, const char *phase) { + MicroOcpp::SampledValueProperties props; + props.setMeasurand(measurand); + props.setUnit(unit); + props.setLocation(location); + props.setPhase(phase); + auto mvs = std::unique_ptr>>( + new MicroOcpp::SampledValueSamplerConcrete>( + props, + [valueInput] (ReadingContext readingContext) {return valueInput(readingContext);} + )); + addMeterValueInput(std::move(mvs)); +} +void ocpp_addMeterValueInputIntTx_m(unsigned int connectorId, int (*valueInput)(unsigned int cId, ReadingContext), const char *measurand, const char *unit, const char *location, const char *phase) { + MicroOcpp::SampledValueProperties props; + props.setMeasurand(measurand); + props.setUnit(unit); + props.setLocation(location); + props.setPhase(phase); + auto mvs = std::unique_ptr>>( + new MicroOcpp::SampledValueSamplerConcrete>( + props, + [valueInput, connectorId] (ReadingContext readingContext) {return valueInput(connectorId, readingContext);} + )); + addMeterValueInput(std::move(mvs), connectorId); +} + +void ocpp_addMeterValueInput(MeterValueInput *meterValueInput) { + ocpp_addMeterValueInput_m(1, meterValueInput); +} +void ocpp_addMeterValueInput_m(unsigned int connectorId, MeterValueInput *meterValueInput) { + auto svs = std::unique_ptr( + reinterpret_cast(meterValueInput)); + + addMeterValueInput(std::move(svs), connectorId); +} + + +#if MO_ENABLE_CONNECTOR_LOCK +void ocpp_setOnUnlockConnectorInOut(PollUnlockResult onUnlockConnectorInOut) { + setOnUnlockConnectorInOut(adaptFn(onUnlockConnectorInOut)); +} +void ocpp_setOnUnlockConnectorInOut_m(unsigned int connectorId, PollUnlockResult_m onUnlockConnectorInOut) { + setOnUnlockConnectorInOut(adaptFn(connectorId, onUnlockConnectorInOut), connectorId); +} +#endif //MO_ENABLE_CONNECTOR_LOCK + +void ocpp_setStartTxReadyInput(InputBool startTxReady) { + setStartTxReadyInput(adaptFn(startTxReady)); +} +void ocpp_setStartTxReadyInput_m(unsigned int connectorId, InputBool_m startTxReady) { + setStartTxReadyInput(adaptFn(connectorId, startTxReady), connectorId); +} + +void ocpp_setStopTxReadyInput(InputBool stopTxReady) { + setStopTxReadyInput(adaptFn(stopTxReady)); +} +void ocpp_setStopTxReadyInput_m(unsigned int connectorId, InputBool_m stopTxReady) { + setStopTxReadyInput(adaptFn(connectorId, stopTxReady), connectorId); +} + +void ocpp_setTxNotificationOutput(void (*notificationOutput)(OCPP_Transaction*, TxNotification)) { + setTxNotificationOutput([notificationOutput] (MicroOcpp::Transaction *tx, TxNotification notification) { + notificationOutput(reinterpret_cast(tx), notification); + }); +} +void ocpp_setTxNotificationOutput_m(unsigned int connectorId, void (*notificationOutput)(unsigned int, OCPP_Transaction*, TxNotification)) { + setTxNotificationOutput([notificationOutput, connectorId] (MicroOcpp::Transaction *tx, TxNotification notification) { + notificationOutput(connectorId, reinterpret_cast(tx), notification); + }, connectorId); +} + +void ocpp_setOccupiedInput(InputBool occupied) { + setOccupiedInput(adaptFn(occupied)); +} +void ocpp_setOccupiedInput_m(unsigned int connectorId, InputBool_m occupied) { + setOccupiedInput(adaptFn(connectorId, occupied), connectorId); +} + +bool ocpp_isOperative() { + return isOperative(); +} +bool ocpp_isOperative_m(unsigned int connectorId) { + return isOperative(connectorId); +} +void ocpp_setOnResetNotify(bool (*onResetNotify)(bool)) { + setOnResetNotify([onResetNotify] (bool isHard) {return onResetNotify(isHard);}); +} + +void ocpp_setOnResetExecute(void (*onResetExecute)(bool)) { + setOnResetExecute([onResetExecute] (bool isHard) {onResetExecute(isHard);}); +} + +#if MO_ENABLE_CERT_MGMT +void ocpp_setCertificateStore(ocpp_cert_store *certs) { + std::unique_ptr certsCwrapper; + if (certs) { + certsCwrapper = MicroOcpp::makeCertificateStoreCwrapper(certs); + } + setCertificateStore(std::move(certsCwrapper)); +} +#endif //MO_ENABLE_CERT_MGMT + +void ocpp_setOnReceiveRequest(const char *operationType, OnMessage onRequest) { + setOnReceiveRequest(operationType, adaptFn(onRequest)); +} + +void ocpp_setOnSendConf(const char *operationType, OnMessage onConfirmation) { + setOnSendConf(operationType, adaptFn(onConfirmation)); +} + +void ocpp_authorize(const char *idTag, AuthorizeConfCallback onConfirmation, AuthorizeAbortCallback onAbort, AuthorizeTimeoutCallback onTimeout, AuthorizeErrorCallback onError, void *user_data) { + + auto idTag_capture = MicroOcpp::makeString("MicroOcpp_c.cpp", idTag); + + authorize(idTag, + onConfirmation ? [onConfirmation, idTag_capture, user_data] (JsonObject payload) { + auto len = serializeJson(payload, ocpp_recv_payload_buff, MO_RECEIVE_PAYLOAD_BUFSIZE); + if (len <= 0) {MO_DBG_WARN("Received payload buffer exceeded. Continue without payload");} + onConfirmation(idTag_capture.c_str(), len > 0 ? ocpp_recv_payload_buff : "", len, user_data); + } : OnReceiveConfListener(nullptr), + onAbort ? [onAbort, idTag_capture, user_data] () -> void { + onAbort(idTag_capture.c_str(), user_data); + } : OnAbortListener(nullptr), + onTimeout ? [onTimeout, idTag_capture, user_data] () { + onTimeout(idTag_capture.c_str(), user_data); + } : OnTimeoutListener(nullptr), + onError ? [onError, idTag_capture, user_data] (const char *code, const char *description, JsonObject details) { + auto len = serializeJson(details, ocpp_recv_payload_buff, MO_RECEIVE_PAYLOAD_BUFSIZE); + if (len <= 0) {MO_DBG_WARN("Received payload buffer exceeded. Continue without payload");} + onError(idTag_capture.c_str(), code, description, len > 0 ? ocpp_recv_payload_buff : "", len, user_data); + } : OnReceiveErrorListener(nullptr)); +} + +void ocpp_startTransaction(const char *idTag, OnMessage onConfirmation, OnAbort onAbort, OnTimeout onTimeout, OnCallError onError) { + startTransaction(idTag, adaptFn(onConfirmation), adaptFn(onAbort), adaptFn(onTimeout), adaptFn(onError)); +} + +void ocpp_stopTransaction(OnMessage onConfirmation, OnAbort onAbort, OnTimeout onTimeout, OnCallError onError) { + stopTransaction(adaptFn(onConfirmation), adaptFn(onAbort), adaptFn(onTimeout), adaptFn(onError)); +} diff --git a/src/MicroOcpp_c.h b/src/MicroOcpp_c.h new file mode 100644 index 00000000..a517ff1d --- /dev/null +++ b/src/MicroOcpp_c.h @@ -0,0 +1,217 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#ifndef MO_MICROOCPP_C_H +#define MO_MICROOCPP_C_H + +#include + +#include +#include +#include +#include +#include +#include + +struct OCPP_Connection; +typedef struct OCPP_Connection OCPP_Connection; + +struct MeterValueInput; +typedef struct MeterValueInput MeterValueInput; + +struct FilesystemAdapterC; +typedef struct FilesystemAdapterC FilesystemAdapterC; + +typedef void (*OnMessage) (const char *payload, size_t len); +typedef void (*OnAbort) (); +typedef void (*OnTimeout) (); +typedef void (*OnCallError) (const char *code, const char *description, const char *details_json, size_t details_len); +typedef void (*AuthorizeConfCallback) (const char *idTag, const char *payload, size_t len, void *user_data); +typedef void (*AuthorizeAbortCallback) (const char *idTag, void* user_data); +typedef void (*AuthorizeTimeoutCallback) (const char *idTag, void* user_data); +typedef void (*AuthorizeErrorCallback) (const char *idTag, const char *code, const char *description, const char *details_json, size_t details_len, void* user_data); + +typedef float (*InputFloat)(); +typedef float (*InputFloat_m)(unsigned int connectorId); //multiple connectors version +typedef int (*InputInt)(); +typedef int (*InputInt_m)(unsigned int connectorId); +typedef bool (*InputBool)(); +typedef bool (*InputBool_m)(unsigned int connectorId); +typedef const char* (*InputString)(); +typedef const char* (*InputString_m)(unsigned int connectorId); +typedef void (*OutputFloat)(float limit); +typedef void (*OutputFloat_m)(unsigned int connectorId, float limit); +typedef void (*OutputSmartCharging)(float power, float current, int nphases); +typedef void (*OutputSmartCharging_m)(unsigned int connectorId, float power, float current, int nphases); + +#if MO_ENABLE_CONNECTOR_LOCK +typedef UnlockConnectorResult (*PollUnlockResult)(); +typedef UnlockConnectorResult (*PollUnlockResult_m)(unsigned int connectorId); +#endif //MO_ENABLE_CONNECTOR_LOCK + + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Please refer to MicroOcpp.h for the documentation + */ + +void ocpp_initialize( + OCPP_Connection *conn, //WebSocket adapter for MicroOcpp + const char *chargePointModel, //model name of this charger (e.g. "My Charger") + const char *chargePointVendor, //brand name (e.g. "My Company Ltd.") + struct OCPP_FilesystemOpt fsopt, //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h + bool autoRecover, //automatically sanitize the local data store when the lib detects recurring crashes. During development, `false` is recommended + bool ocpp201); //true to select OCPP 2.0.1, false for OCPP 1.6 + +//same as above, but more fields for the BootNotification +void ocpp_initialize_full( + OCPP_Connection *conn, //WebSocket adapter for MicroOcpp + const char *bootNotificationCredentials, //e.g. '{"chargePointModel":"Demo Charger","chargePointVendor":"My Company Ltd."}' (refer to OCPP 1.6 Specification - Edition 2 p. 60) + struct OCPP_FilesystemOpt fsopt, //If this library should format the flash if necessary. Find further options in ConfigurationOptions.h + bool autoRecover, //automatically sanitize the local data store when the lib detects recurring crashes. During development, `false` is recommended + bool ocpp201); //true to select OCPP 2.0.1, false for OCPP 1.6 + +//same as above, but pass FS handle instead of FS options +void ocpp_initialize_full2( + OCPP_Connection *conn, //WebSocket adapter for MicroOcpp + const char *bootNotificationCredentials, //e.g. '{"chargePointModel":"Demo Charger","chargePointVendor":"My Company Ltd."}' (refer to OCPP 1.6 Specification - Edition 2 p. 60) + FilesystemAdapterC *filesystem, //FilesystemAdapter handle initialized by client. MO takes ownership and deletes it during deinitialization + bool autoRecover, //automatically sanitize the local data store when the lib detects recurring crashes. During development, `false` is recommended + bool ocpp201); //true to select OCPP 2.0.1, false for OCPP 1.6 + +void ocpp_deinitialize(); + +bool ocpp_is_initialized(); + +void ocpp_loop(); + +/* + * Charging session management + */ + +bool ocpp_beginTransaction(const char *idTag); +bool ocpp_beginTransaction_m(unsigned int connectorId, const char *idTag); //multiple connectors version + +bool ocpp_beginTransaction_authorized(const char *idTag, const char *parentIdTag); +bool ocpp_beginTransaction_authorized_m(unsigned int connectorId, const char *idTag, const char *parentIdTag); + +bool ocpp_endTransaction(const char *idTag, const char *reason); //idTag, reason can be NULL +bool ocpp_endTransaction_m(unsigned int connectorId, const char *idTag, const char *reason); //idTag, reason can be NULL + +bool ocpp_endTransaction_authorized(const char *idTag, const char *reason); //idTag, reason can be NULL +bool ocpp_endTransaction_authorized_m(unsigned int connectorId, const char *idTag, const char *reason); //idTag, reason can be NULL + +bool ocpp_isTransactionActive(); +bool ocpp_isTransactionActive_m(unsigned int connectorId); + +bool ocpp_isTransactionRunning(); +bool ocpp_isTransactionRunning_m(unsigned int connectorId); + +const char *ocpp_getTransactionIdTag(); +const char *ocpp_getTransactionIdTag_m(unsigned int connectorId); + +OCPP_Transaction *ocpp_getTransaction(); +OCPP_Transaction *ocpp_getTransaction_m(unsigned int connectorId); + +bool ocpp_ocppPermitsCharge(); +bool ocpp_ocppPermitsCharge_m(unsigned int connectorId); + +ChargePointStatus ocpp_getChargePointStatus(); +ChargePointStatus ocpp_getChargePointStatus_m(unsigned int connectorId); + +/* + * Define the Inputs and Outputs of this library. + */ + +void ocpp_setConnectorPluggedInput(InputBool pluggedInput); +void ocpp_setConnectorPluggedInput_m(unsigned int connectorId, InputBool_m pluggedInput); + +void ocpp_setEnergyMeterInput(InputInt energyInput); +void ocpp_setEnergyMeterInput_m(unsigned int connectorId, InputInt_m energyInput); + +void ocpp_setPowerMeterInput(InputFloat powerInput); +void ocpp_setPowerMeterInput_m(unsigned int connectorId, InputFloat_m powerInput); + +void ocpp_setSmartChargingPowerOutput(OutputFloat maxPowerOutput); +void ocpp_setSmartChargingPowerOutput_m(unsigned int connectorId, OutputFloat_m maxPowerOutput); +void ocpp_setSmartChargingCurrentOutput(OutputFloat maxCurrentOutput); +void ocpp_setSmartChargingCurrentOutput_m(unsigned int connectorId, OutputFloat_m maxCurrentOutput); +void ocpp_setSmartChargingOutput(OutputSmartCharging chargingLimitOutput); +void ocpp_setSmartChargingOutput_m(unsigned int connectorId, OutputSmartCharging_m chargingLimitOutput); + +/* + * Define the Inputs and Outputs of this library. (Advanced) + */ + +void ocpp_setEvReadyInput(InputBool evReadyInput); +void ocpp_setEvReadyInput_m(unsigned int connectorId, InputBool_m evReadyInput); + +void ocpp_setEvseReadyInput(InputBool evseReadyInput); +void ocpp_setEvseReadyInput_m(unsigned int connectorId, InputBool_m evseReadyInput); + +void ocpp_addErrorCodeInput(InputString errorCodeInput); +void ocpp_addErrorCodeInput_m(unsigned int connectorId, InputString_m errorCodeInput); + +void ocpp_addMeterValueInputFloat(InputFloat valueInput, const char *measurand, const char *unit, const char *location, const char *phase); //measurand, unit, location and phase can be NULL +void ocpp_addMeterValueInputFloat_m(unsigned int connectorId, InputFloat_m valueInput, const char *measurand, const char *unit, const char *location, const char *phase); //measurand, unit, location and phase can be NULL + +void ocpp_addMeterValueInputIntTx(int (*valueInput)(ReadingContext), const char *measurand, const char *unit, const char *location, const char *phase); //measurand, unit, location and phase can be NULL +void ocpp_addMeterValueInputIntTx_m(unsigned int connectorId, int (*valueInput)(unsigned int cId, ReadingContext), const char *measurand, const char *unit, const char *location, const char *phase); //measurand, unit, location and phase can be NULL + +void ocpp_addMeterValueInput(MeterValueInput *meterValueInput); //takes ownership of meterValueInput +void ocpp_addMeterValueInput_m(unsigned int connectorId, MeterValueInput *meterValueInput); //takes ownership of meterValueInput + +void ocpp_setOccupiedInput(InputBool occupied); +void ocpp_setOccupiedInput_m(unsigned int connectorId, InputBool_m occupied); + +void ocpp_setStartTxReadyInput(InputBool startTxReady); +void ocpp_setStartTxReadyInput_m(unsigned int connectorId, InputBool_m startTxReady); + +void ocpp_setStopTxReadyInput(InputBool stopTxReady); +void ocpp_setStopTxReadyInput_m(unsigned int connectorId, InputBool_m stopTxReady); + +void ocpp_setTxNotificationOutput(void (*notificationOutput)(OCPP_Transaction*, TxNotification)); +void ocpp_setTxNotificationOutput_m(unsigned int connectorId, void (*notificationOutput)(unsigned int, OCPP_Transaction*, TxNotification)); + +#if MO_ENABLE_CONNECTOR_LOCK +void ocpp_setOnUnlockConnectorInOut(PollUnlockResult onUnlockConnectorInOut); +void ocpp_setOnUnlockConnectorInOut_m(unsigned int connectorId, PollUnlockResult_m onUnlockConnectorInOut); +#endif //MO_ENABLE_CONNECTOR_LOCK + +/* + * Access further information about the internal state of the library + */ + +bool ocpp_isOperative(); +bool ocpp_isOperative_m(unsigned int connectorId); + +void ocpp_setOnResetNotify(bool (*onResetNotify)(bool)); + +void ocpp_setOnResetExecute(void (*onResetExecute)(bool)); + +#if MO_ENABLE_CERT_MGMT +void ocpp_setCertificateStore(ocpp_cert_store *certs); +#endif //MO_ENABLE_CERT_MGMT + +void ocpp_setOnReceiveRequest(const char *operationType, OnMessage onRequest); + +void ocpp_setOnSendConf(const char *operationType, OnMessage onConfirmation); + +/* + * Send OCPP operations + */ +void ocpp_authorize(const char *idTag, AuthorizeConfCallback onConfirmation, AuthorizeAbortCallback onAbort, AuthorizeTimeoutCallback onTimeout, AuthorizeErrorCallback onError, void *user_data); + +void ocpp_startTransaction(const char *idTag, OnMessage onConfirmation, OnAbort onAbort, OnTimeout onTimeout, OnCallError onError); + +void ocpp_stopTransaction(OnMessage onConfirmation, OnAbort onAbort, OnTimeout onTimeout, OnCallError onError); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/tests/Api.cpp b/tests/Api.cpp new file mode 100644 index 00000000..c2470172 --- /dev/null +++ b/tests/Api.cpp @@ -0,0 +1,376 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "./helpers/testHelper.h" + +#include + +#define BASE_TIME "2023-01-01T00:00:00.000Z" +#define SCPROFILE "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":0,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"chargingSchedule\":{\"duration\":1000000,\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"W\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3}]}}}]" + +TEST_CASE( "C++ API test" ) { + printf("\nRun %s\n", "C++ API test"); + + //initialize Context with dummy socket + MicroOcpp::LoopbackConnection loopback; + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + auto context = getOcppContext(); + auto& model = context->getModel(); + + mocpp_set_timer(custom_timer_cb); + + model.getClock().setTime(BASE_TIME); + + endTransaction(); + + SECTION("Run all functions") { + + //Set all possible Inputs and outputs + std::array checkpoints {false}; + size_t ncheck = 0; + + setConnectorPluggedInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return true;}); + setEnergyMeterInput([c = &checkpoints[ncheck++]] () -> float {*c = true; return 0.f;}); + setPowerMeterInput([c = &checkpoints[ncheck++]] () -> float {*c = true; return 0.f;}); + setSmartChargingPowerOutput([] (float) {}); //overridden by CurrentOutput + setSmartChargingCurrentOutput([] (float) {}); //overridden by generic SmartChargingOutput + setSmartChargingOutput([c = &checkpoints[ncheck++]] (float, float, int) {*c = true;}); + setEvReadyInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return true;}); + setEvseReadyInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return true;}); + addErrorCodeInput([c = &checkpoints[ncheck++]] () -> const char* {*c = true; return nullptr;}); + addErrorDataInput([c = &checkpoints[ncheck++]] () -> MicroOcpp::ErrorData {*c = true; return nullptr;}); + addMeterValueInput([c = &checkpoints[ncheck++]] () -> float {*c = true; return 0.f;}, "Current.Import"); + + MicroOcpp::SampledValueProperties svprops; + svprops.setMeasurand("Current.Offered"); + auto valueSampler = std::unique_ptr>>( + new MicroOcpp::SampledValueSamplerConcrete>( + svprops, + [c = &checkpoints[ncheck++]] (ReadingContext) -> int32_t {*c = true; return 0;})); + addMeterValueInput(std::move(valueSampler)); + + setOccupiedInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return false;}); + setStartTxReadyInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return true;}); + setStopTxReadyInput([c = &checkpoints[ncheck++]] () -> bool {*c = true; return true;}); + setTxNotificationOutput([c = &checkpoints[ncheck++]] (MicroOcpp::Transaction*, TxNotification) {*c = true;}); + +#if MO_ENABLE_CONNECTOR_LOCK + setOnUnlockConnectorInOut([c = &checkpoints[ncheck++]] () -> UnlockConnectorResult {*c = true; return UnlockConnectorResult_Unlocked;}); +#endif //MO_ENABLE_CONNECTOR_LOCK + + setOnResetNotify([c = &checkpoints[ncheck++]] (bool) -> bool {*c = true; return true;}); + setOnResetExecute([c = &checkpoints[ncheck++]] (bool) {*c = true;}); + + REQUIRE( getFirmwareService() != nullptr ); + REQUIRE( getDiagnosticsService() != nullptr ); + REQUIRE( getOcppContext() != nullptr ); + + setOnReceiveRequest("StatusNotification", [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); + setOnSendConf("StatusNotification", [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); + sendRequest("DataTransfer", [c = &checkpoints[ncheck++]] () { + *c = true; + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + doc->to(); + return doc; + }, [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); + setRequestHandler("DataTransfer", [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}, [c = &checkpoints[ncheck++]] () { + *c = true; + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + doc->to(); + return doc; + }); + + //set configuration which uses all Inputs and Outputs + + auto MeterValuesSampledDataString = MicroOcpp::declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register,Power.Active.Import,Current.Import,Current.Offered"); + + loopback.sendTXT(SCPROFILE, strlen(SCPROFILE)); + + //run tx management + + mocpp_loop(); + + loop(); + + beginTransaction("mIdTag"); + + loop(); + + REQUIRE(isTransactionActive()); + REQUIRE(isTransactionRunning()); + REQUIRE(getTransactionIdTag() != nullptr); + REQUIRE(getTransaction() != nullptr); + REQUIRE(ocppPermitsCharge()); + + endTransaction(); + + loop(); + + beginTransaction_authorized("mIdTag"); + + loop(); + + mtime += 3600 * 1000; + + loop(); + + endTransaction(); + + loop(); + + authorize("mIdTag", [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); + startTransaction("mIdTag", [c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); + + loop(); + + stopTransaction([c = &checkpoints[ncheck++]] (JsonObject) {*c = true;}); + + //occupied Input will be validated when vehiclePlugged is false or undefined + setConnectorPluggedInput(nullptr); + + loop(); + + //run device management + + REQUIRE(isOperative()); + + sendRequest("UnlockConnector", [] () { + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + (*doc)["connectorId"] = 1; + return doc; + }, [] (JsonObject) {}); + + sendRequest("Reset", [] () { + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + (*doc)["type"] = "Hard"; + return doc; + }, [] (JsonObject) {}); + + loop(); + + mtime += 3600 * 1000; + + loop(); + + MO_DBG_DEBUG("added %zu checkpoints", ncheck); + + bool checkpointsPassed = true; + for (unsigned int i = 0; i < ncheck; i++) { + if (!checkpoints[i]) { + MO_DBG_ERR("missed checkpoint %u", i); + checkpointsPassed = false; + } + } + + REQUIRE(checkpointsPassed); + } + + mocpp_deinitialize(); + + REQUIRE(!getOcppContext()); +} + +#include +#include + +std::array checkpointsc {false}; +size_t ncheckc = 0; + +TEST_CASE( "C API test" ) { + + //initialize Context with dummy socket + struct OCPP_FilesystemOpt fsopt; + fsopt.use = true; + fsopt.mount = true; + fsopt.formatFsOnFail = true; + + MicroOcpp::LoopbackConnection loopback; + ocpp_initialize(reinterpret_cast(&loopback), "test-runner1234", "vendor", fsopt, false, false); + + auto context = getOcppContext(); + auto& model = context->getModel(); + + mocpp_set_timer(custom_timer_cb); + + model.getClock().setTime(BASE_TIME); + + ocpp_endTransaction(NULL, NULL); + + SECTION("Run all functions") { + + ocpp_setConnectorPluggedInput([] () -> bool {checkpointsc[0] = true; return true;}); ncheckc++; + ocpp_setConnectorPluggedInput_m(2, [] (unsigned int) -> bool {checkpointsc[1] = true; return true;}); ncheckc++; + ocpp_setEnergyMeterInput([] () -> int {checkpointsc[2] = true; return 0;}); ncheckc++; + ocpp_setEnergyMeterInput_m(2, [] (unsigned int) -> int {checkpointsc[3] = true; return 0;}); ncheckc++; + ocpp_setPowerMeterInput([] () -> float {checkpointsc[4] = true; return 0.f;}); ncheckc++; + ocpp_setPowerMeterInput_m(2, [] (unsigned int) -> float {checkpointsc[5] = true; return 0.f;}); ncheckc++; + ocpp_setSmartChargingPowerOutput([] (float) {}); //overridden by CurrentOutput + ocpp_setSmartChargingPowerOutput_m(2, [] (unsigned int, float) {}); //overridden by CurrentOutput + ocpp_setSmartChargingCurrentOutput([] (float) {}); //overridden by generic SmartChargingOutput + ocpp_setSmartChargingCurrentOutput_m(2, [] (unsigned int, float) {}); //overridden by generic SmartChargingOutput + ocpp_setSmartChargingOutput([] (float, float, int) {checkpointsc[6] = true;}); ncheckc++; + ocpp_setSmartChargingOutput_m(2, [] (unsigned int, float, float, int) {checkpointsc[7] = true;}); ncheckc++; + ocpp_setEvReadyInput([] () -> bool {checkpointsc[8] = true; return true;}); ncheckc++; + ocpp_setEvReadyInput_m(2, [] (unsigned int) -> bool {checkpointsc[9] = true; return true;}); ncheckc++; + ocpp_setEvseReadyInput([] () -> bool {checkpointsc[10] = true; return true;}); ncheckc++; + ocpp_setEvseReadyInput_m(2, [] (unsigned int) -> bool {checkpointsc[11] = true; return true;}); ncheckc++; + ocpp_addErrorCodeInput([] () -> const char* {checkpointsc[12] = true; return nullptr;}); ncheckc++; + ocpp_addErrorCodeInput_m(2, [] (unsigned int) -> const char* {checkpointsc[13] = true; return nullptr;}); ncheckc++; + ocpp_addMeterValueInputFloat([] () -> float {checkpointsc[14] = true; return 0.f;}, "Current.Import", "A", NULL, NULL); ncheckc++; + ocpp_addMeterValueInputFloat_m(2, [] (unsigned int) -> float {checkpointsc[15] = true; return 0.f;}, "Current.Import", "A", NULL, NULL); ncheckc++; + + MicroOcpp::SampledValueProperties svprops; + svprops.setMeasurand("Current.Offered"); + auto valueSampler = std::unique_ptr>>( + new MicroOcpp::SampledValueSamplerConcrete>( + svprops, + [] (ReadingContext) -> int32_t {checkpointsc[16] = true; return 0;})); ncheckc++; + ocpp_addMeterValueInput(reinterpret_cast(valueSampler.release())); + + valueSampler = std::unique_ptr>>( + new MicroOcpp::SampledValueSamplerConcrete>( + svprops, + [] (ReadingContext) -> int32_t {checkpointsc[17] = true; return 0;})); ncheckc++; + ocpp_addMeterValueInput_m(2, reinterpret_cast(valueSampler.release())); + + ocpp_setOccupiedInput([] () -> bool {checkpointsc[18] = true; return true;}); ncheckc++; + ocpp_setOccupiedInput_m(2, [] (unsigned int) -> bool {checkpointsc[19] = true; return true;}); ncheckc++; + ocpp_setStartTxReadyInput([] () -> bool {checkpointsc[20] = true; return true;}); ncheckc++; + ocpp_setStartTxReadyInput_m(2, [] (unsigned int) -> bool {checkpointsc[21] = true; return true;}); ncheckc++; + ocpp_setStopTxReadyInput([] () -> bool {checkpointsc[22] = true; return true;}); ncheckc++; + ocpp_setStopTxReadyInput_m(2, [] (unsigned int) -> bool {checkpointsc[23] = true; return true;}); ncheckc++; + ocpp_setTxNotificationOutput([] (OCPP_Transaction*, TxNotification) {checkpointsc[24] = true;}); ncheckc++; + ocpp_setTxNotificationOutput_m(2, [] (unsigned int, OCPP_Transaction*, TxNotification) {checkpointsc[25] = true;}); ncheckc++; + +#if MO_ENABLE_CONNECTOR_LOCK + ocpp_setOnUnlockConnectorInOut([] () -> UnlockConnectorResult {checkpointsc[26] = true; return UnlockConnectorResult_Unlocked;}); ncheckc++; + ocpp_setOnUnlockConnectorInOut_m(2, [] (unsigned int) -> UnlockConnectorResult {checkpointsc[27] = true; return UnlockConnectorResult_Unlocked;}); ncheckc++; +#else + checkpointsc[26] = true; + checkpointsc[27] = true; +#endif //MO_ENABLE_CONNECTOR_LOCK + + ocpp_setOnResetNotify([] (bool) -> bool {checkpointsc[28] = true; return true;}); ncheckc++; + ocpp_setOnResetExecute([] (bool) {checkpointsc[29] = true;}); ncheckc++; + + ocpp_setOnReceiveRequest("StatusNotification", [] (const char*,size_t) {checkpointsc[30] = true;}); ncheckc++; + ocpp_setOnSendConf("StatusNotification", [] (const char*,size_t) {checkpointsc[31] = true;}); ncheckc++; + + //set configuration which uses all Inputs and Outputs + + auto MeterValuesSampledDataString = MicroOcpp::declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register,Power.Active.Import,Current.Import,Current.Offered"); + + loopback.sendTXT(SCPROFILE, strlen(SCPROFILE)); + + //run tx management + + ocpp_loop(); + + loop(); + + ocpp_beginTransaction("mIdTag"); + ocpp_beginTransaction_m(2, "mIdTag"); + + loop(); + + REQUIRE(ocpp_isTransactionActive()); + REQUIRE(ocpp_isTransactionActive_m(2)); + REQUIRE(ocpp_isTransactionRunning()); + REQUIRE(ocpp_isTransactionRunning_m(2)); + REQUIRE(ocpp_getTransactionIdTag() != nullptr); + REQUIRE(ocpp_getTransactionIdTag_m(2) != nullptr); + REQUIRE(ocpp_getTransaction() != nullptr); + REQUIRE(ocpp_getTransaction_m(2) != nullptr); + REQUIRE(ocpp_ocppPermitsCharge()); + REQUIRE(ocpp_ocppPermitsCharge_m(2)); + + ocpp_endTransaction("mIdTag", NULL); + ocpp_endTransaction_m(2, "mIdTag", NULL); + + loop(); + + ocpp_beginTransaction_authorized("mIdTag", NULL); + ocpp_beginTransaction_authorized_m(2, "mIdTag", NULL); + + loop(); + + mtime += 3600 * 1000; + + loop(); + + ocpp_endTransaction_authorized(NULL, NULL); + ocpp_endTransaction_authorized_m(2, NULL, NULL); + + loop(); + + ocpp_authorize("mIdTag", [] (const char*,const char*,size_t,void*) {checkpointsc[32] = true;}, NULL, NULL, NULL, NULL); ncheckc++; + ocpp_startTransaction("mIdTag", [] (const char*,size_t) {checkpointsc[33] = true;}, NULL, NULL, NULL); ncheckc++; + + loop(); + + ocpp_stopTransaction([] (const char*,size_t) {checkpointsc[34] = true;}, NULL, NULL, NULL); ncheckc++; + + //occupied Input will be validated when vehiclePlugged is false or undefined + ocpp_setConnectorPluggedInput([] () {return false;}); + ocpp_setConnectorPluggedInput_m(2,[] (unsigned int) {return false;}); + + loop(); + + //run device management + + REQUIRE(ocpp_isOperative()); + REQUIRE(ocpp_isOperative_m(2)); + + sendRequest("UnlockConnector", [] () { + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + (*doc)["connectorId"] = 1; + return doc; + }, [] (JsonObject) {}); + sendRequest("UnlockConnector", [] () { + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + (*doc)["connectorId"] = 2; + return doc; + }, [] (JsonObject) {}); + + sendRequest("Reset", [] () { + auto doc = MicroOcpp::makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + (*doc)["type"] = "Hard"; + return doc; + }, [] (JsonObject) {}); + + loop(); + + mtime += 3600 * 1000; + + loop(); + + MO_DBG_DEBUG("added %zu checkpoints", ncheckc); + + bool checkpointsPassed = true; + for (unsigned int i = 0; i < ncheckc; i++) { + if (!checkpointsc[i]) { + MO_DBG_ERR("missed checkpoint %u", i); + checkpointsPassed = false; + } + } + + REQUIRE(checkpointsPassed); + } + + ocpp_deinitialize(); + + REQUIRE(!getOcppContext()); +} 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 58d45cb6..82090f16 100644 --- a/tests/ChargingSessions.cpp +++ b/tests/ChargingSessions.cpp @@ -1,55 +1,74 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include "./catch2/catch.hpp" +// 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 ArduinoOcpp; +#include + +#define BASE_TIME "2023-01-01T00:00:00.000Z" + +using namespace MicroOcpp; TEST_CASE( "Charging sessions" ) { + printf("\nRun %s\n", "Charging sessions"); + + //initialize Context with dummy socket + LoopbackConnection loopback; + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); - //initialize OcppEngine with dummy socket - OcppEchoSocket echoSocket; - OCPP_initialize(echoSocket); + auto engine = getOcppContext(); + auto& checkMsg = engine->getOperationRegistry(); - ao_set_timer(custom_timer_cb); + mocpp_set_timer(custom_timer_cb); - auto connectionTimeOut = declareConfiguration("ConnectionTimeOut", 30, CONFIGURATION_FN); - *connectionTimeOut = 30; - auto minimumStatusDuration = declareConfiguration("MinimumStatusDuration", 0, CONFIGURATION_FN); - *minimumStatusDuration = 0; + auto connectionTimeOutInt = declareConfiguration("ConnectionTimeOut", 30, CONFIGURATION_FN); + connectionTimeOutInt->setInt(30); + auto minimumStatusDurationInt = declareConfiguration("MinimumStatusDuration", 0, CONFIGURATION_FN); + minimumStatusDurationInt->setInt(0); std::array expectedSN {"Available", "Available"}; std::array checkedSN {false, false}; - registerCustomOcppMessage("StatusNotification", [] () {return new Ocpp16::StatusNotification();}, + 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; - checkedSN[connectorId] = !strcmp(request["status"] | "Invalid", expectedSN[connectorId]); + if (connectorId == 0 || connectorId == 1) { //only test single connector case here + checkedSN[connectorId] = !strcmp(request["status"] | "Invalid", expectedSN[connectorId]); + } }); - bootNotification("dummy1234", ""); - SECTION("Check idle state"){ bool checkedBN = false; - registerCustomOcppMessage("BootNotification", [] () {return new Ocpp16::BootNotification();}, + checkMsg.registerOperation("BootNotification", [engine] () -> Operation* {return new Ocpp16::BootNotification(engine->getModel(), makeJsonDoc("UnitTests"));}); + checkMsg.setOnRequest("BootNotification", [&checkedBN] (JsonObject request) { - checkedBN = !strcmp(request["chargePointModel"] | "Invalid", "dummy1234"); + checkedBN = !strcmp(request["chargePointModel"] | "Invalid", "test-runner1234"); }); + REQUIRE( !isOperative() ); //not operative before reaching loop stage + loop(); loop(); REQUIRE( checkedBN ); REQUIRE( checkedSN[0] ); REQUIRE( checkedSN[1] ); REQUIRE( isOperative() ); - REQUIRE( !getTransactionIdTag() ); + REQUIRE( !getTransaction() ); REQUIRE( !ocppPermitsCharge() ); } @@ -108,7 +127,7 @@ TEST_CASE( "Charging sessions" ) { checkedSN[1] = false; expectedSN[1] = "Available"; - mtime += *connectionTimeOut * 1000; + mtime += connectionTimeOutInt->getInt() * 1000; loop(); REQUIRE(checkedSN[1]); } @@ -133,7 +152,7 @@ TEST_CASE( "Charging sessions" ) { } SECTION("via session management - deauthorize") { - endTransaction("Local"); + endTransaction(); loop(); REQUIRE(checkedSN[1]); REQUIRE(!ocppPermitsCharge()); @@ -142,7 +161,7 @@ TEST_CASE( "Charging sessions" ) { SECTION("via session management - deauthorize first") { expectedSN[1] = "Finishing"; setConnectorPluggedInput([] () {return true;}); - endTransaction("Local"); + endTransaction(); loop(); REQUIRE(checkedSN[1]); REQUIRE(!ocppPermitsCharge()); @@ -168,71 +187,1097 @@ TEST_CASE( "Charging sessions" ) { loop(); } - SECTION("Advanced session management") { - bool connectorPlugged = false; - setConnectorPluggedInput([&connectorPlugged] () {return connectorPlugged;}); + SECTION("Preboot transactions - tx before BootNotification") { + mocpp_deinitialize(); - TxEnableState meterState = TxEnableState::Inactive; - setTxBasedMeterInOut([&meterState] (TxTrigger) {return meterState;}); + loopback.setOnline(false); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); - TxEnableState lockState = TxEnableState::Inactive; - setConnectorLockInOut([&lockState] (TxTrigger) {return lockState;}); + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); + configuration_save(); - beginTransaction("mIdTag"); loop(); - REQUIRE(!ocppPermitsCharge()); - connectorPlugged = true; + beginTransaction_authorized("mIdTag"); + loop(); - REQUIRE(!ocppPermitsCharge()); + + REQUIRE(isTransactionRunning()); + + mtime += 3600 * 1000; //transaction duration ~1h + + endTransaction(); + + loop(); + + mtime += 3600 * 1000; //set base time one hour later + + bool checkStartProcessed = false; + + getOcppContext()->getModel().getClock().setTime(BASE_TIME); + Timestamp basetime = Timestamp(); + basetime.setTime(BASE_TIME); + + getOcppContext()->getOperationRegistry().setOnRequest("StartTransaction", + [&checkStartProcessed, basetime] (JsonObject payload) { + checkStartProcessed = true; + Timestamp timestamp; + timestamp.setTime(payload["timestamp"].as()); + + auto adjustmentDelay = basetime - timestamp; + REQUIRE((adjustmentDelay > 2 * 3600 - 10 && adjustmentDelay < 2 * 3600 + 10)); + }); + + bool checkStopProcessed = false; + + getOcppContext()->getOperationRegistry().setOnRequest("StopTransaction", + [&checkStopProcessed, basetime] (JsonObject payload) { + checkStopProcessed = true; + Timestamp timestamp; + timestamp.setTime(payload["timestamp"].as()); + + auto adjustmentDelay = basetime - timestamp; + REQUIRE((adjustmentDelay > 3600 - 10 && adjustmentDelay < 3600 + 10)); + }); - meterState = TxEnableState::Active; + loopback.setOnline(true); loop(); - REQUIRE(!ocppPermitsCharge()); + + REQUIRE(checkStartProcessed); + REQUIRE(checkStopProcessed); + } + + SECTION("Preboot transactions - lose StartTx timestamp") { + + mocpp_deinitialize(); + + loopback.setOnline(false); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); + configuration_save(); + + loop(); + + beginTransaction_authorized("mIdTag"); + loop(); + + REQUIRE(isTransactionRunning()); + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); + configuration_save(); + + bool checkProcessed = false; + + getOcppContext()->getOperationRegistry().setOnRequest("StartTransaction", [&checkProcessed] (JsonObject) { + checkProcessed = true; + }); + + getOcppContext()->getOperationRegistry().setOnRequest("StopTransaction", [&checkProcessed] (JsonObject) { + checkProcessed = true; + }); + + loopback.setOnline(true); + + loop(); + + REQUIRE(!isTransactionRunning()); + REQUIRE(!checkProcessed); + } + + SECTION("Preboot transactions - lose StopTx timestamp") { - meterState = TxEnableState::Pending; - lockState = TxEnableState::Active; + const char *starTxTimestampStr = "2023-02-01T00:00:00.000Z"; + getOcppContext()->getModel().getClock().setTime(starTxTimestampStr); + + beginTransaction_authorized("mIdTag"); + + loop(); + + REQUIRE(isTransactionRunning()); + + mocpp_deinitialize(); + + loopback.setOnline(false); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); + configuration_save(); + + loop(); + + REQUIRE(isTransactionRunning()); + + endTransaction(); + + loop(); + + REQUIRE(!isTransactionRunning()); + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); + configuration_save(); + + bool checkProcessed = false; + + getOcppContext()->getOperationRegistry().setOnRequest("StopTransaction", + [&checkProcessed, starTxTimestampStr] (JsonObject payload) { + checkProcessed = true; + Timestamp timestamp; + timestamp.setTime(payload["timestamp"].as()); + + Timestamp starTxTimestamp = Timestamp(); + starTxTimestamp.setTime(starTxTimestampStr); + + auto adjustmentDelay = timestamp - starTxTimestamp; + REQUIRE(adjustmentDelay == 1); + }); + + loopback.setOnline(true); + loop(); + + REQUIRE(checkProcessed); + } + + SECTION("Preboot transactions - reject tx if limit exceeded") { + mocpp_deinitialize(); + + loopback.setConnected(false); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); + declareConfiguration(MO_CONFIG_EXT_PREFIX "SilentOfflineTransactions", false, CONFIGURATION_FN)->setBool(false); // do not start more txs if tx journal is full + configuration_save(); + + loop(); + + for (size_t i = 0; i < MO_TXRECORD_SIZE; i++) { + beginTransaction_authorized("mIdTag"); + + loop(); + + REQUIRE(isTransactionRunning()); + + endTransaction(); + + loop(); + + REQUIRE(!isTransactionRunning()); + } + + // now, tx journal is full. Block any further charging session + + auto tx_success = beginTransaction_authorized("mIdTag"); + REQUIRE( !tx_success ); + + loop(); + + REQUIRE(!isTransactionRunning()); REQUIRE(!ocppPermitsCharge()); + + // Check if all 4 cached transctions are transmitted after going online + + const int txId_base = 10000; + int txId_generate = txId_base; + int txId_confirm = txId_base; + + getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&txId_generate] () { + return new Ocpp16::CustomOperation("StartTransaction", + [] (JsonObject payload) {}, //ignore req + [&txId_generate] () { + //create conf + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + + JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); + idTagInfo["status"] = "Accepted"; + txId_generate++; + payload["transactionId"] = txId_generate; + return doc; + });}); + + getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&txId_generate, &txId_confirm] () { + return new Ocpp16::CustomOperation("StopTransaction", + [&txId_generate, &txId_confirm] (JsonObject payload) { + //receive req + REQUIRE( payload["transactionId"].as() == txId_generate ); + REQUIRE( payload["transactionId"].as() == txId_confirm + 1 ); + txId_confirm = payload["transactionId"].as(); + }, + [] () { + //create conf + return createEmptyDocument(); + });}); + + loopback.setConnected(true); + loop(); + + REQUIRE( txId_confirm == txId_base + MO_TXRECORD_SIZE ); + } + + SECTION("Preboot transactions - charge without tx if limit exceeded") { + mocpp_deinitialize(); + + loopback.setConnected(false); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); + declareConfiguration(MO_CONFIG_EXT_PREFIX "SilentOfflineTransactions", false, CONFIGURATION_FN)->setBool(true); // don't report further transactions to server but charge anyway + configuration_save(); + + loop(); + + for (size_t i = 0; i < MO_TXRECORD_SIZE; i++) { + beginTransaction_authorized("mIdTag"); + + loop(); + + REQUIRE(isTransactionRunning()); + + endTransaction(); + + loop(); + + REQUIRE(!isTransactionRunning()); + } + + // now, tx journal is full. Block any further charging session + + auto tx_success = beginTransaction_authorized("mIdTag"); + REQUIRE( tx_success ); + + loop(); + + REQUIRE(isTransactionRunning()); + REQUIRE(ocppPermitsCharge()); + + endTransaction(); + + loop(); + + // Check if all 4 cached transctions are transmitted after going online + + const int txId_base = 10000; + int txId_generate = txId_base; + int txId_confirm = txId_base; + + getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&txId_generate] () { + return new Ocpp16::CustomOperation("StartTransaction", + [] (JsonObject payload) {}, //ignore req + [&txId_generate] () { + //create conf + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); + JsonObject payload = doc->to(); + + JsonObject idTagInfo = payload.createNestedObject("idTagInfo"); + idTagInfo["status"] = "Accepted"; + txId_generate++; + payload["transactionId"] = txId_generate; + return doc; + });}); + + getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&txId_generate, &txId_confirm] () { + return new Ocpp16::CustomOperation("StopTransaction", + [&txId_generate, &txId_confirm] (JsonObject payload) { + //receive req + REQUIRE( payload["transactionId"].as() == txId_generate ); + REQUIRE( payload["transactionId"].as() == txId_confirm + 1 ); + txId_confirm = payload["transactionId"].as(); + }, + [] () { + //create conf + return createEmptyDocument(); + });}); + + loopback.setConnected(true); + loop(); + + REQUIRE( txId_confirm == txId_base + MO_TXRECORD_SIZE ); + } + + SECTION("Preboot transactions - mix PreBoot with Offline tx") { - meterState = TxEnableState::Active; - lockState = TxEnableState::Pending; + /* + * 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(); - REQUIRE(!ocppPermitsCharge()); - checkedSN[1] = false; - expectedSN[1] = "Charging"; + // 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; + });}); - lockState = TxEnableState::Active; + // get online + loopback.setConnected(true); loop(); - REQUIRE(ocppPermitsCharge()); - REQUIRE(checkedSN[1]); - checkedSN[1] = false; - expectedSN[1] = "SuspendedEVSE"; + // 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"); - lockState = TxEnableState::Pending; loop(); - REQUIRE(!ocppPermitsCharge()); - REQUIRE(!getTransactionIdTag()); - REQUIRE(checkedSN[1]); - checkedSN[1] = false; - expectedSN[1] = "Finishing"; + auto connector = getOcppContext()->getModel().getConnector(1); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + REQUIRE(isOperative()); + + bool checkProcessed = false; + + auto changeAvailability = makeRequest(new Ocpp16::CustomOperation( + "ChangeAvailability", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["connectorId"] = 1; + payload["type"] = "Inoperative"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE(!strcmp(payload["status"], "Scheduled"));})); + + getOcppContext()->initiateRequest(std::move(changeAvailability)); - meterState = TxEnableState::Inactive; - lockState = TxEnableState::Inactive; loop(); - REQUIRE(checkedSN[1]); - checkedSN[1] = false; - expectedSN[1] = "Available"; + REQUIRE(checkProcessed); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + REQUIRE(isOperative()); + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + connector = getOcppContext()->getModel().getConnector(1); + + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + REQUIRE(isOperative()); + + endTransaction(); - connectorPlugged = false; loop(); - REQUIRE(checkedSN[1]); + + REQUIRE(connector->getStatus() == ChargePointStatus_Unavailable); + REQUIRE(!isOperative()); + + connector->setAvailability(true); + + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + REQUIRE(isOperative()); } - OCPP_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 + } -} \ No newline at end of file + mocpp_deinitialize(); +} diff --git a/tests/Configuration.cpp b/tests/Configuration.cpp new file mode 100644 index 00000000..91fa1e28 --- /dev/null +++ b/tests/Configuration.cpp @@ -0,0 +1,609 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#include +#include +#include +#include "./helpers/testHelper.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace MicroOcpp; + +#define GET_CONFIG_ALL "[2,\"test-msg\",\"GetConfiguration\",{}]" +#define KNOWN_KEY "__ExistingKey" +#define UNKOWN_KEY "__UnknownKey" +#define GET_CONFIG_KNOWN_UNKOWN "[2,\"test-mst\",\"GetConfiguration\",{\"key\":[\"" KNOWN_KEY "\",\"" UNKOWN_KEY "\"]}]" + +// some globals for the C-API tests +bool g_checkProcessed [10]; +ocpp_configuration g_configs [2]; +int g_config_values [2]; +uint16_t g_config_write_count[2]; + +TEST_CASE( "Configuration" ) { + printf("\nRun %s\n", "Configuration"); + + //clean state + auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); + FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); + + LoopbackConnection loopback; //initialize Context with dummy socket + + mocpp_set_timer(custom_timer_cb); + + SECTION("Basic container operations"){ + std::unique_ptr container; + + SECTION("Volatile storage") { + container = makeConfigurationContainerVolatile(CONFIGURATION_VOLATILE "/volatile1", true); + } + + SECTION("Persistent storage") { + container = makeConfigurationContainerFlash(filesystem, MO_FILENAME_PREFIX "persistent1.jsn", true); + } + + //check emptyness + REQUIRE( container->size() == 0 ); + + //add first config, fetch by index + auto configFirst = container->createConfiguration(TConfig::Int, "cFirst"); + REQUIRE( container->size() == 1 ); + REQUIRE( container->getConfiguration((size_t) 0) == configFirst.get()); + + //add one config of each type + auto cInt = container->createConfiguration(TConfig::Int, "cInt"); + auto cBool = container->createConfiguration(TConfig::Bool, "cBool"); + auto cString = container->createConfiguration(TConfig::String, "cString"); + + REQUIRE( container->size() == 4 ); + + //fetch config by key + REQUIRE( container->getConfiguration(cBool->getKey()) == cBool); + + //remove config + container->remove(cBool.get()); + REQUIRE( container->size() == 3 ); + REQUIRE( container->getConfiguration(cBool->getKey()) == nullptr); + + //clean container + container->remove(container->getConfiguration((size_t) 0)); + container->remove(container->getConfiguration((size_t) 0)); + container->remove(container->getConfiguration((size_t) 0)); + REQUIRE( container->size() == 0 ); + } + + SECTION("Persistency on filesystem") { + + auto container = makeConfigurationContainerFlash(filesystem, MO_FILENAME_PREFIX "persistent1.jsn", true); + + //trivial load call + REQUIRE( container->load() ); + REQUIRE( container->size() == 0 ); + + //add config, store, load again + auto cString = container->createConfiguration(TConfig::String, "cString"); + cString->setString("mValue"); + + REQUIRE( container->save() ); //store + + container.reset(); //destroy + + //...load again + auto container2 = makeConfigurationContainerFlash(filesystem, MO_FILENAME_PREFIX "persistent1.jsn", true); + REQUIRE( container2->size() == 0 ); + REQUIRE( container2->load() ); + REQUIRE( container2->size() == 1 ); + + auto cString2 = container2->getConfiguration("cString"); + REQUIRE( cString2 != nullptr ); + REQUIRE( !strcmp(cString2->getString(), "mValue") ); + } + + SECTION("Configuration API") { + + //declare configs + REQUIRE( configuration_init(filesystem) ); + auto cInt = declareConfiguration("cInt", 42); + REQUIRE( cInt != nullptr ); + declareConfiguration("cBool", true); + declareConfiguration("cString", "mValue"); + + //fetch config + REQUIRE( getConfigurationPublic("cInt")->getInt() == 42 ); + + //store, destroy, reload + REQUIRE( configuration_save() ); + cInt.reset(); + configuration_deinit(); + REQUIRE( getConfigurationPublic("cInt") == nullptr); + + REQUIRE( configuration_init(filesystem) ); //reload + + //fetch configs (declare with different factory default - should remain at original value) + auto cInt2 = declareConfiguration("cInt", -1); + auto cBool2 = declareConfiguration("cBool", false); + auto cString2 = declareConfiguration("cString", "no effect"); + REQUIRE( configuration_load() ); //load config objects with stored values + + //check load result + REQUIRE( cInt2->getInt() == 42 ); + REQUIRE( cBool2->getBool() == true ); + REQUIRE( !strcmp(cString2->getString(), "mValue") ); + + //declare config twice + auto cInt3 = declareConfiguration("cInt", -1); + REQUIRE( cInt3 == cInt2 ); + + //declare config twice but in different container + auto cInt4 = declareConfiguration("cInt", -1, CONFIGURATION_VOLATILE); + REQUIRE( cInt4 == cInt2 ); + + //declare config twice but with conflicting type (will supersede old type because to simplify FW upgrades) + auto cNewType = declareConfiguration("cInt", "mValue2"); + REQUIRE( cNewType != cInt2 ); + REQUIRE( !strcmp(cNewType->getString(), "mValue2") ); + + //store, destroy, reload + REQUIRE( configuration_save() ); + configuration_deinit(); + REQUIRE( getConfigurationPublic("cInt") == nullptr); + REQUIRE( configuration_init(filesystem) ); //reload + auto cNewType2 = declareConfiguration("cInt", "no effect"); + REQUIRE( configuration_load() ); + REQUIRE( !strcmp(cNewType2->getString(), "mValue2") ); + + //get config before declared (container needs to be declared already at this point) + auto cString3 = getConfigurationPublic("cString"); + REQUIRE( !strcmp(cString3->getString(), "mValue") ); + configuration_deinit(); + + //value needs to outlive container + configuration_init(filesystem); + auto cString4 = declareConfiguration("cString2", "mValue3"); + configuration_deinit(); + REQUIRE( !strcmp(cString4->getString(), "mValue3") ); + + FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); + + //config accessibility / permissions + configuration_init(filesystem); + bool readonly = false; + bool rebootRequired = false; + bool isPublic = true; + auto cInt6 = declareConfiguration("cInt", 42, CONFIGURATION_FN, readonly, rebootRequired, isPublic); + REQUIRE( !cInt6->isReadOnly() ); + REQUIRE( !cInt6->isRebootRequired() ); + REQUIRE( getConfigurationPublic("cInt") ); + + //revoke permissions + readonly = true; + rebootRequired = true; + declareConfiguration("cInt", 42, CONFIGURATION_FN, readonly, rebootRequired, isPublic); + REQUIRE( cInt6->isReadOnly() ); + REQUIRE( cInt6->isRebootRequired() ); + + //revoked permissions cannot be reverted + readonly = false; + rebootRequired = false; + auto cInt7 = declareConfiguration("cInt", 42, CONFIGURATION_FN, readonly, rebootRequired, isPublic); + REQUIRE( cInt7->isReadOnly() ); + REQUIRE( cInt7->isRebootRequired() ); + + //accessibility cannot be changed after first initialization + isPublic = false; + declareConfiguration("cInt", 42, CONFIGURATION_FN, false, rebootRequired, isPublic); + declareConfiguration("cInt2", 42, CONFIGURATION_FN, false, rebootRequired, isPublic); + REQUIRE( getConfigurationPublic("cInt") ); + REQUIRE( getConfigurationPublic("cInt2") ); + + //create config in hidden container + isPublic = false; + auto cHidden = declareConfiguration("cHidden", 42, MO_FILENAME_PREFIX "hidden.jsn", false, false, isPublic); + REQUIRE( !getConfigurationPublic("cHidden") ); + + //hidden container cannot be fetched + auto configsPublic = getConfigurationContainersPublic(); + REQUIRE( configsPublic.size() == 1 ); + + configuration_deinit(); + } + + SECTION("ContainerFlash memory optimization") { + + //key storage optimization: the static key provided by declareConfiguration is preferred. If + //no static key is available for the config (if the config is loaded from flash without being + //declared before), then a key on the heap is allocated. If the config is later allocated, + //the heap-key is replaced by the static key + + const char *key_static = "cInt"; + + configuration_init(filesystem); + auto cInt = declareConfiguration(key_static, 42); + configuration_save(); + configuration_deinit(); + + configuration_init(filesystem); + declareConfiguration("dummy", 23); //dummy config to declare CONFIGURATION_FN + configuration_load(); + const char *key_heap = getConfigurationPublic(key_static)->getKey(); + REQUIRE( key_heap != key_static ); + + declareConfiguration(key_static, 42); //replace heap key with static key here + + const char *key_replaced = getConfigurationPublic(key_static)->getKey(); + REQUIRE( key_replaced == key_static ); + + configuration_deinit(); + } + + SECTION("Main lib integration") { + + //basic lifecycle + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + REQUIRE( getConfigurationPublic("ConnectionTimeOut") ); + REQUIRE( !getConfigurationContainersPublic().empty() ); + mocpp_deinitialize(); + REQUIRE( !getConfigurationPublic("ConnectionTimeOut") ); + REQUIRE( getConfigurationContainersPublic().empty() ); + + //modify standard config ConnectionTimeOut. This config is not modified by the main lib during normal initialization / deinitialization + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + auto config = getConfigurationPublic("ConnectionTimeOut"); + + config->setInt(1234); //update + configuration_save(); //write back + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + REQUIRE( getConfigurationPublic("ConnectionTimeOut")->getInt() == 1234 ); + + mocpp_deinitialize(); + } + + SECTION("GetConfiguration") { + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + loop(); + + declareConfiguration(KNOWN_KEY, 1234, MO_FILENAME_PREFIX "persistent1.jsn", false); + + bool checkProcessed = false; + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "GetConfiguration", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + doc->to(); + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + JsonArray configurationKey = payload["configurationKey"]; + + bool foundCustomConfig = false; + bool foundStandardConfig = false; + for (JsonObject keyvalue : configurationKey) { + MO_DBG_DEBUG("key %s", keyvalue["key"] | "_Undefined"); + if (!strcmp(keyvalue["key"] | "_Undefined", KNOWN_KEY)) { + foundCustomConfig = true; + REQUIRE( (keyvalue["readonly"] | true) == false ); + REQUIRE( !strcmp(keyvalue["value"] | "_Undefined", "1234") ); + } else if (!strcmp(keyvalue["key"] | "_Undefined", "ConnectionTimeOut")) { + foundStandardConfig = true; + } + } + + REQUIRE( foundCustomConfig ); + REQUIRE( foundStandardConfig ); + } + ))); + + loop(); + + REQUIRE(checkProcessed); + + checkProcessed = false; + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "GetConfiguration", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(2)); + auto payload = doc->to(); + auto key = payload.createNestedArray("key"); + key.add(KNOWN_KEY); + key.add(UNKOWN_KEY); + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + JsonArray configurationKey = payload["configurationKey"]; + + bool foundCustomConfig = false; + for (JsonObject keyvalue : configurationKey) { + if (!strcmp(keyvalue["key"] | "_Undefined", KNOWN_KEY)) { + foundCustomConfig = true; + break; + } + } + REQUIRE( foundCustomConfig ); + + JsonArray unknownKey = payload["unknownKey"]; + + bool foundUnkownKey = false; + for (const char *key : unknownKey) { + if (!strcmp(key, UNKOWN_KEY)) { + foundUnkownKey = true; + } + } + + REQUIRE( foundUnkownKey ); + } + ))); + + loop(); + + REQUIRE(checkProcessed); + + mocpp_deinitialize(); + } + + SECTION("ChangeConfiguration") { + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + loop(); + + declareConfiguration(KNOWN_KEY, 0, MO_FILENAME_PREFIX "persistent1.jsn", false); + + //update existing config + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ChangeConfiguration", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["key"] = KNOWN_KEY; + payload["value"] = "1234"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE(checkProcessed); + REQUIRE( getConfigurationPublic(KNOWN_KEY)->getInt() == 1234 ); + + //try to update not existing key + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ChangeConfiguration", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["key"] = UNKOWN_KEY; + payload["value"] = "no effect"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "NotSupported") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + //try to update config with malformatted value + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ChangeConfiguration", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["key"] = KNOWN_KEY; + payload["value"] = "not convertible to int"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Rejected") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + //try to update config with value validation + //value is valid if it begins with 1 + registerConfigurationValidator(KNOWN_KEY, [] (const char *v) { + return v[0] == '1'; + }); + + //validation success + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ChangeConfiguration", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["key"] = KNOWN_KEY; + payload["value"] = "100234"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + REQUIRE( getConfigurationPublic(KNOWN_KEY)->getInt() == 100234 ); + + //validation failure + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ChangeConfiguration", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["key"] = KNOWN_KEY; + payload["value"] = "4321"; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Rejected") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + REQUIRE( getConfigurationPublic(KNOWN_KEY)->getInt() == 100234 ); //keep old value + + mocpp_deinitialize(); + } + + SECTION("Define factory defaults for standard configs") { + + //set factory default for standard config ConnectionTimeOut + configuration_init(filesystem); + auto factoryConnectionTimeOut = declareConfiguration("ConnectionTimeOut", 1234, MO_FILENAME_PREFIX "factory.jsn"); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + auto connectionTimeout2 = declareConfiguration("ConnectionTimeOut", 4321); + REQUIRE( connectionTimeout2->getInt() == 1234 ); + REQUIRE( connectionTimeout2 == factoryConnectionTimeOut ); + + configuration_save(); + mocpp_deinitialize(); + + //this time, factory default is not given (will lead to duplicates, should be considered in sanitization) + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + REQUIRE( getConfigurationPublic("ConnectionTimeOut")->getInt() != 1234 ); + mocpp_deinitialize(); + + //provide factory default again + configuration_init(filesystem); + declareConfiguration("ConnectionTimeOut", 4321, MO_FILENAME_PREFIX "factory.jsn"); + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + REQUIRE( getConfigurationPublic("ConnectionTimeOut")->getInt() == 1234 ); + mocpp_deinitialize(); + + } + + SECTION("C-API") { + ocpp_configuration_container container; + memset(&container, 0, sizeof(container)); + + bool check_load = false; + + container.load = [] (void *user_data) { + g_checkProcessed[0] = true; + return true; + }; + + container.save = [] (void *user_data) { + g_checkProcessed[1] = true; + return true; + }; + + ocpp_configuration *config_predefined = &g_configs[0]; + config_predefined->get_key = [] (void *user_data) -> const char* {return "ConnectionTimeOut";}; // existing OCPP key to use custom config store + config_predefined->get_type = [] (void *user_data) -> ocpp_config_datatype {return ENUM_CDT_INT;}; + config_predefined->set_int = [] (void *user_data, int val) -> void {g_config_values[0] = val;}; + config_predefined->get_int = [] (void *user_data) -> int {return g_config_values[0];}; + config_predefined->get_write_count = [] (void *user_data) -> uint16_t {return g_config_write_count[0];}; + + container.create_configuration = [] (void *user_data, ocpp_config_datatype dt, const char *key) -> ocpp_configuration* { + ocpp_configuration *config_created = &g_configs[1]; + config_created->get_key = [] (void *user_data) -> const char* {return "MCreatedConfig";}; // non-existing key to test create_configuration function + config_created->get_type = [] (void *user_data) -> ocpp_config_datatype {return ENUM_CDT_INT;}; + config_created->set_int = [] (void *user_data, int val) -> void {g_config_values[1] = val;}; + config_created->get_int = [] (void *user_data) -> int {return g_config_values[1];}; + config_created->get_write_count = [] (void *user_data) -> uint16_t {return g_config_write_count[1];}; + return config_created; + }; + + container.remove = [] (void *user_data, const char *key) -> void { + g_checkProcessed[2] = true; + }; + + container.size = [] (void *user_data) { + return sizeof(g_configs) / sizeof(g_configs[0]); + }; + + container.get_configuration = [] (void *user_data, size_t i) -> ocpp_configuration* { + return &g_configs[i]; + }; + + container.get_configuration_by_key = [] (void *user_data, const char *key) -> ocpp_configuration* { + if (!strcmp(key, "ConnectionTimeOut")) { + return &g_configs[0]; + } else if (!strcmp(key, "MCreatedConfig")) { + return g_configs[1].get_key ? + &g_configs[1] : // createConfig has already been called + nullptr; // config hasn't been created yet + } + return nullptr; + }; + + ocpp_configuration_container_add(&container, "MContainerPath", true); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + loop(); + + REQUIRE( g_checkProcessed[0] ); + + auto test_predefined = declareConfiguration("ConnectionTimeOut", 0); + + test_predefined->setInt(12345); + REQUIRE( test_predefined->getInt() == 12345 ); + REQUIRE( config_predefined->get_int(config_predefined->user_data) == 12345 ); + + g_checkProcessed[1] = false; // check if store is executed + test_predefined->setInt(555); + g_config_write_count[0]; + + configuration_save(); + + REQUIRE( g_checkProcessed[1] ); + + // test if declaring new configs is handled + auto test_created = declareConfiguration("MCreatedConfig", 123, "MContainerPath"); + REQUIRE( test_created != nullptr ); + REQUIRE( test_created->getInt() == 123 ); + ocpp_configuration *config_created = &g_configs[1]; + REQUIRE( config_created->get_int(config_created->user_data) == 123 ); + + mocpp_deinitialize(); + } + +} diff --git a/tests/ConfigurationBehavior.cpp b/tests/ConfigurationBehavior.cpp new file mode 100644 index 00000000..fc30b356 --- /dev/null +++ b/tests/ConfigurationBehavior.cpp @@ -0,0 +1,288 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "./helpers/testHelper.h" + +using namespace MicroOcpp; + +class CustomAuthorize : public Operation { +private: + const char *status; +public: + CustomAuthorize(const char *status) : status(status) { }; + const char *getOperationType() override {return "Authorize";} + void processReq(JsonObject payload) override { + //ignore payload - result is determined at construction time + } + std::unique_ptr createConf() override { + auto res = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = res->to(); + payload["idTagInfo"]["status"] = status; + return res; + } +}; + +class CustomStartTransaction : public Operation { +private: + const char *status; +public: + CustomStartTransaction(const char *status) : status(status) { }; + const char *getOperationType() override {return "StartTransaction";} + void processReq(JsonObject payload) override { + //ignore payload - result is determined at construction time + } + std::unique_ptr createConf() override { + auto res = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(1)); + auto payload = res->to(); + payload["idTagInfo"]["status"] = status; + payload["transactionId"] = 1000; + return res; + } +}; + +TEST_CASE( "Configuration Behavior" ) { + printf("\nRun %s\n", "Configuration Behavior"); + + //clean state + auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); + MO_DBG_DEBUG("remove all"); + FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); + + //initialize Context with dummy socket + LoopbackConnection loopback; + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + auto context = getOcppContext(); + auto& checkMsg = context->getOperationRegistry(); + + auto connector = context->getModel().getConnector(1); + + mocpp_set_timer(custom_timer_cb); + + loop(); + + SECTION("StopTransactionOnEVSideDisconnect") { + setConnectorPluggedInput([] () {return true;}); + startTransaction("mIdTag"); + loop(); + + auto configBool = declareConfiguration("StopTransactionOnEVSideDisconnect", true); + + SECTION("set true") { + configBool->setBool(true); + + setConnectorPluggedInput([] () {return false;}); + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + + SECTION("set false") { + configBool->setBool(false); + + setConnectorPluggedInput([] () {return false;}); + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_SuspendedEV); + + endTransaction(); + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + } + + SECTION("StopTransactionOnInvalidId") { + auto configBool = declareConfiguration("StopTransactionOnInvalidId", true); + + checkMsg.registerOperation("Authorize", [] () {return new CustomAuthorize("Invalid");}); + checkMsg.registerOperation("StartTransaction", [] () {return new CustomStartTransaction("Invalid");}); + + SECTION("set true") { + configBool->setBool(true); + + beginTransaction("mIdTag_invalid"); + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + + beginTransaction_authorized("mIdTag_invalid2"); + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + + SECTION("set false") { + configBool->setBool(false); + + beginTransaction("mIdTag"); + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + + beginTransaction_authorized("mIdTag"); + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_SuspendedEVSE); + + endTransaction(); + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + } + + SECTION("AllowOfflineTxForUnknownId") { + auto configBool = declareConfiguration("AllowOfflineTxForUnknownId", true); + auto authorizationTimeoutInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", 1); + authorizationTimeoutInt->setInt(1); //try normal Authorize for 1s, then enter offline mode + + loopback.setOnline(false); //connection loss + + SECTION("set true") { + configBool->setBool(true); + + beginTransaction("mIdTag"); + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + + endTransaction(); + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + + SECTION("set false") { + configBool->setBool(false); + + beginTransaction("mIdTag"); + REQUIRE(connector->getStatus() == ChargePointStatus_Preparing); + + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + + endTransaction(); + loopback.setOnline(true); + } + +#if MO_ENABLE_LOCAL_AUTH + SECTION("LocalPreAuthorize") { + auto configBool = declareConfiguration("LocalPreAuthorize", true); + auto authorizationTimeoutInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", 20); + authorizationTimeoutInt->setInt(300); //try normal Authorize for 5 minutes + + declareConfiguration("LocalAuthorizeOffline", true)->setBool(true); + declareConfiguration("LocalAuthListEnabled", true)->setBool(true); + + //define local auth list with entry local-idtag + const char *localListMsg = "[2,\"testmsg-01\",\"SendLocalList\",{\"listVersion\":1,\"localAuthorizationList\":[{\"idTag\":\"local-idtag\",\"idTagInfo\":{\"status\":\"Accepted\"}}],\"updateType\":\"Full\"}]"; + loopback.sendTXT(localListMsg, strlen(localListMsg)); + loop(); + + loopback.setOnline(false); //connection loss + + SECTION("set true - accepted idtag") { + configBool->setBool(true); + + beginTransaction("local-idtag"); + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + } + + SECTION("set false") { + configBool->setBool(false); + + beginTransaction("local-idtag"); + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Preparing); + + loopback.setOnline(true); + mtime += 20000; //Authorize will be retried after a few seconds + loop(); + + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + } + + endTransaction(); + loopback.setOnline(true); + } +#endif //MO_ENABLE_LOCAL_AUTH + + SECTION("AuthorizeRemoteTxRequests") { + auto configBool = declareConfiguration("AuthorizeRemoteTxRequests", false); + + bool receivedAuthorize = false; + + setOnReceiveRequest("Authorize", [&receivedAuthorize] (JsonObject payload) { + receivedAuthorize = true; + REQUIRE( !strcmp(payload["idTag"] | "_Undefined", "mIdTag") ); + }); + + SECTION("set true") { + configBool->setBool(true); + + context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTag"] = "mIdTag"; + return doc;}, + [] (JsonObject) { + //ignore conf + } + ))); + + loop(); + + REQUIRE(receivedAuthorize); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + } + + SECTION("set false") { + configBool->setBool(false); + + context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTag"] = "mIdTag"; + return doc;}, + [] (JsonObject) { + //ignore conf + } + ))); + + loop(); + + REQUIRE(!receivedAuthorize); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + } + + endTransaction(); + loop(); + } + + mocpp_deinitialize(); +} 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 new file mode 100644 index 00000000..855a980b --- /dev/null +++ b/tests/LocalAuthList.cpp @@ -0,0 +1,929 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_LOCAL_AUTH + +#include +#include +#include +#include "./helpers/testHelper.h" + +#include +#include +#include + +#include +#include +#include +#include + + +#define BASE_TIME "2023-01-01T00:00:00.000Z" + +using namespace MicroOcpp; + +void generateAuthList(JsonArray out, size_t size, bool compact) { + for (size_t i = 0; i < size; i++) { + JsonObject authData = out.createNestedObject(); + JsonObject idTagInfo; + if (compact) { + //flat structure + idTagInfo = authData; + } else { + //nested idTagInfo + idTagInfo = authData["idTagInfo"].to(); + } + + char buf [IDTAG_LEN_MAX + 1]; + sprintf(buf, "mIdTag%zu", i); + authData[AUTHDATA_KEY_IDTAG(compact)] = buf; + idTagInfo[AUTHDATA_KEY_STATUS(compact)] = "Accepted"; + } +} + +TEST_CASE( "LocalAuth" ) { + printf("\nRun %s\n", "LocalAuth"); + + //clean state + auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); + FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); + + //initialize Context with dummy socket + LoopbackConnection loopback; + + mocpp_set_timer(custom_timer_cb); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + auto& model = getOcppContext()->getModel(); + auto authService = model.getAuthorizationService(); + auto connector = model.getConnector(1); + model.getClock().setTime(BASE_TIME); + + loop(); + + //enable local auth + declareConfiguration("LocalAuthListEnabled", true)->setBool(true); + + //set Authorize timeout after which the charger is considered offline + const unsigned long AUTH_TIMEOUT_MS = 60000; + MicroOcpp::declareConfiguration(MO_CONFIG_EXT_PREFIX "AuthorizationTimeout", -1)->setInt(AUTH_TIMEOUT_MS / 1000); + + //fetch local auth configs + auto localAuthorizeOffline = declareConfiguration("LocalAuthorizeOffline", false); + auto localPreAuthorize = declareConfiguration("LocalPreAuthorize", false); + + SECTION("Basic local auth - LocalPreAuthorize") { + + localAuthorizeOffline->setBool(false); + localPreAuthorize->setBool(true); + + //set local list + StaticJsonDocument<256> localAuthList; + localAuthList[0]["idTag"] = "mIdTag"; + localAuthList[0]["idTagInfo"]["status"] = "Accepted"; + + REQUIRE( authService->updateLocalList(localAuthList.as(), 1, false) ); + + REQUIRE( authService->getLocalListSize() == 1 ); + REQUIRE( authService->getLocalAuthorization("mIdTag") != nullptr ); + REQUIRE( authService->getLocalAuthorization("mIdTag")->getAuthorizationStatus() == AuthorizationStatus::Accepted ); + + //check TX notification + bool checkTxAuthorized = false; + setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { + if (txNotification == TxNotification_Authorized) { + checkTxAuthorized = true; + } + }); + + //begin transaction and delay Authorize request - tx should start immediately + loopback.setOnline(false); //Authorize delayed by short offline period + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + beginTransaction("mIdTag"); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + REQUIRE( checkTxAuthorized ); + + loopback.setOnline(true); + endTransaction(); + loop(); + + //begin transaction delay Authorize request, but idTag doesn't match local list - tx should start when online again + checkTxAuthorized = false; + loopback.setOnline(false); + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + beginTransaction("wrong idTag"); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); + REQUIRE( !checkTxAuthorized ); + + loopback.setOnline(true); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + REQUIRE( checkTxAuthorized ); + + endTransaction(); + loop(); + } + + SECTION("Basic local auth - LocalAuthorizeOffline") { + + localAuthorizeOffline->setBool(true); + localPreAuthorize->setBool(false); + + //set local list + StaticJsonDocument<256> localAuthList; + localAuthList[0]["idTag"] = "mIdTag"; + localAuthList[0]["idTagInfo"]["status"] = "Accepted"; + authService->updateLocalList(localAuthList.as(), 1, false); + + //check TX notification + bool checkTxAuthorized = false; + setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { + if (txNotification == TxNotification_Authorized) { + checkTxAuthorized = true; + } + }); + + //make charger offline and begin tx - tx should begin after Authorize timeout + loopback.setOnline(false); + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + unsigned long t_before = mocpp_tick_ms(); + + beginTransaction("mIdTag"); + loop(); + + REQUIRE( mocpp_tick_ms() - t_before < AUTH_TIMEOUT_MS ); //if this fails, increase AUTH_TIMEOUT_MS + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); + REQUIRE( !checkTxAuthorized ); + + mtime += AUTH_TIMEOUT_MS - (mocpp_tick_ms() - t_before); //increment clock so that auth timeout is exceeded + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + REQUIRE( checkTxAuthorized ); + + loopback.setOnline(true); + endTransaction(); + loop(); + + //make charger offline and begin tx, but idTag doesn't match - tx should be aborted + bool checkTxTimeout = false; + setTxNotificationOutput([&checkTxTimeout] (Transaction*, TxNotification txNotification) { + if (txNotification == TxNotification_AuthorizationTimeout) { + checkTxTimeout = true; + } + }); + loopback.setOnline(false); + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + t_before = mocpp_tick_ms(); + + beginTransaction("wrong idTag"); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); + REQUIRE( !checkTxTimeout ); + + mtime += AUTH_TIMEOUT_MS - (mocpp_tick_ms() - t_before); //increment clock so that auth timeout is exceeded + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + REQUIRE( checkTxTimeout ); + + loopback.setOnline(true); + loop(); + } + + SECTION("Basic local auth - AllowOfflineTxForUnknownId") { + + localAuthorizeOffline->setBool(false); + localPreAuthorize->setBool(false); + MicroOcpp::declareConfiguration("AllowOfflineTxForUnknownId", true)->setBool(true); + + //check TX notification + bool checkTxAuthorized = false; + setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { + if (txNotification == TxNotification_Authorized) { + checkTxAuthorized = true; + } + }); + + //make charger offline and begin tx - tx should begin after Authorize timeout + loopback.setOnline(false); + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + unsigned long t_before = mocpp_tick_ms(); + + beginTransaction("unknownIdTag"); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); + REQUIRE( !checkTxAuthorized ); + + mtime += AUTH_TIMEOUT_MS - (mocpp_tick_ms() - t_before); //increment clock so that auth timeout is exceeded + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + REQUIRE( checkTxAuthorized ); + + loopback.setOnline(true); + endTransaction(); + loop(); + } + + SECTION("Local auth - check WS online status") { + + localAuthorizeOffline->setBool(false); + localPreAuthorize->setBool(false); + MicroOcpp::declareConfiguration("AllowOfflineTxForUnknownId", true)->setBool(true); + + //check TX notification + bool checkTxAuthorized = false; + setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { + if (txNotification == TxNotification_Authorized) { + checkTxAuthorized = true; + } + }); + + //disconnect WS and begin tx - charger should enter offline mode immediately + loopback.setConnected(false); + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + beginTransaction("unknownIdTag"); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + REQUIRE( checkTxAuthorized ); + + loopback.setConnected(true); + endTransaction(); + loop(); + } + + SECTION("Local auth list entry expired / unauthorized") { + + localPreAuthorize->setBool(true); + + //set local list with expired / unauthorized entry + StaticJsonDocument<512> localAuthList; + localAuthList[0]["idTag"] = "mIdTagExpired"; + localAuthList[0]["idTagInfo"]["status"] = "Accepted"; + localAuthList[0]["idTagInfo"]["expiryDate"] = BASE_TIME; //is in past + localAuthList[1]["idTag"] = "mIdTagUnauthorized"; + localAuthList[1]["idTagInfo"]["status"] = "Blocked"; + authService->updateLocalList(localAuthList.as(), 1, false); + REQUIRE( authService->getLocalAuthorization("mIdTagExpired") ); + REQUIRE( authService->getLocalAuthorization("mIdTagUnauthorized") ); + + REQUIRE( authService->getLocalAuthorization("mIdTagExpired") ); + + //begin transaction and delay Authorize request - cannot PreAuthorize because entry is expired + loopback.setOnline(false); //Authorize delayed by short offline period + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + beginTransaction("mIdTagExpired"); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); + + loopback.setOnline(true); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + + endTransaction(); + loop(); + + //begin transaction and delay Authorize request - cannot PreAuthorize because entry is unauthorized + loopback.setOnline(false); //Authorize delayed by short offline period + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + beginTransaction("mIdTagUnauthorized"); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); + + loopback.setOnline(true); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + endTransaction(); + loop(); + } + + SECTION("Mix local authorization modes") { + + localAuthorizeOffline->setBool(true); + localPreAuthorize->setBool(true); + MicroOcpp::declareConfiguration("AllowOfflineTxForUnknownId", true)->setBool(true); + + //set local list with accepted and accepted entry + StaticJsonDocument<512> localAuthList; + localAuthList[0]["idTag"] = "mIdTagExpired"; + localAuthList[0]["idTagInfo"]["status"] = "Accepted"; + localAuthList[0]["idTagInfo"]["expiryDate"] = BASE_TIME; //is in past + localAuthList[1]["idTag"] = "mIdTagAccepted"; + localAuthList[1]["idTagInfo"]["status"] = "Accepted"; + authService->updateLocalList(localAuthList.as(), 1, false); + REQUIRE( authService->getLocalAuthorization("mIdTagExpired") ); + REQUIRE( authService->getLocalAuthorization("mIdTagAccepted") ); + + //begin transaction and delay Authorize request - tx should start immediately + loopback.setOnline(false); + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + beginTransaction("mIdTagAccepted"); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + + loopback.setOnline(true); + endTransaction(); + loop(); + + //begin transaction, but idTag is expired - AllowOfflineTxForUnknownId must not apply + loopback.setOnline(false); + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + unsigned long t_before = mocpp_tick_ms(); + + beginTransaction("mIdTagExpired"); + loop(); + + REQUIRE( mocpp_tick_ms() - t_before < AUTH_TIMEOUT_MS ); //if this fails, increase AUTH_TIMEOUT_MS + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); + + mtime += AUTH_TIMEOUT_MS - (mocpp_tick_ms() - t_before); //increment clock so that auth timeout is exceeded + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + loopback.setOnline(true); + loop(); + } + + SECTION("DeAuthorize locally authorized tx") { + + localAuthorizeOffline->setBool(false); + localPreAuthorize->setBool(true); + + //check TX notification + bool checkTxAuthorized = false; + setTxNotificationOutput([&checkTxAuthorized] (Transaction*, TxNotification txNotification) { + if (txNotification == TxNotification_Authorized) { + checkTxAuthorized = true; + } + }); + + //set local list + StaticJsonDocument<256> localAuthList; + localAuthList[0]["idTag"] = "mIdTag"; + localAuthList[0]["idTagInfo"]["status"] = "Accepted"; + authService->updateLocalList(localAuthList.as(), 1, false); + + //patch Authorize so it will reject all idTags + getOcppContext()->getOperationRegistry().registerOperation("Authorize", [] () { + return new Ocpp16::CustomOperation("Authorize", + [] (JsonObject) {}, //ignore req + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTagInfo"]["status"] = "Blocked"; + return doc; + });}); + + //begin transaction and delay Authorize request - tx should start immediately + loopback.setOnline(false); //Authorize delayed by short offline period + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + beginTransaction("mIdTag"); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + REQUIRE( checkTxAuthorized ); + + //check TX notification + bool checkTxRejected = false; + setTxNotificationOutput([&checkTxRejected] (Transaction*, TxNotification txNotification) { + if (txNotification == TxNotification_AuthorizationRejected) { + checkTxRejected = true; + } + }); + + loopback.setOnline(true); + loop(); + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + REQUIRE( checkTxRejected ); + + loop(); + } + + SECTION("LocalListConflict") { + + //patch Authorize so it will reject all idTags + bool checkAuthorize = false; + getOcppContext()->getOperationRegistry().registerOperation("Authorize", [&checkAuthorize] () { + return new Ocpp16::CustomOperation("Authorize", + [&checkAuthorize] (JsonObject) { + checkAuthorize = true; + }, + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", 2 * JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTagInfo"]["status"] = "Blocked"; + return doc; + });}); + + //patch StartTransaction so it will DeAuthorize all txs + bool checkStartTx = false; + getOcppContext()->getOperationRegistry().registerOperation("StartTransaction", [&checkStartTx] () { + return new Ocpp16::CustomOperation("StartTransaction", + [&checkStartTx] (JsonObject) { + checkStartTx = true; + }, + [] () { + //create conf + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTagInfo"]["status"] = "Blocked"; + payload["transactionId"] = 1000; + return doc; + });}); + + //check resulting StatusNotification message + bool checkLocalListConflict = false; + getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", [&checkLocalListConflict] () { + return new Ocpp16::CustomOperation("StatusNotification", + [&checkLocalListConflict] (JsonObject payload) { + if (payload["connectorId"] == 0 && + !strcmp(payload["errorCode"] | "_Undefined", "LocalListConflict")) { + checkLocalListConflict = true; + } + }, + [] () { + //create conf + return createEmptyDocument(); + });}); + + //set local list + StaticJsonDocument<256> localAuthList; + localAuthList[0]["idTag"] = "mIdTag"; + localAuthList[0]["idTagInfo"]["status"] = "Accepted"; + authService->updateLocalList(localAuthList.as(), 1, false); + + //send Authorize and StartTx and check if they trigger LocalListConflict + beginTransaction("mIdTag"); + loop(); + REQUIRE( checkLocalListConflict ); + REQUIRE( checkAuthorize ); + REQUIRE( !checkStartTx ); + + checkLocalListConflict = false; + checkAuthorize = false; + beginTransaction_authorized("mIdTag"); + loop(); + REQUIRE( checkLocalListConflict ); + REQUIRE( !checkAuthorize ); + REQUIRE( checkStartTx ); + } + + SECTION("Update local list") { + + REQUIRE( authService->getLocalListSize() == 0 ); //idle, empty local list + + //set local list + int localListVersion = 42; + StaticJsonDocument<256> localAuthList; + localAuthList[0]["idTag"] = "mIdTag"; + localAuthList[0]["idTagInfo"]["status"] = "Accepted"; + authService->updateLocalList(localAuthList.as(), localListVersion, false); + REQUIRE( authService->getLocalListVersion() == localListVersion ); + REQUIRE( authService->getLocalListSize() == 1 ); + REQUIRE( authService->getLocalAuthorization("mIdTag") != nullptr ); + + //overwrite list + localListVersion++; + localAuthList.clear(); + localAuthList[0]["idTag"] = "mIdTag2"; + localAuthList[0]["idTagInfo"]["status"] = "Accepted"; + authService->updateLocalList(localAuthList.as(), localListVersion, false); + REQUIRE( authService->getLocalListVersion() == localListVersion ); + REQUIRE( authService->getLocalListSize() == 1 ); + REQUIRE( authService->getLocalAuthorization("mIdTag") == nullptr ); + REQUIRE( authService->getLocalAuthorization("mIdTag2") != nullptr ); + + //reset list + localListVersion++; + localAuthList.clear(); + localAuthList.to(); + authService->updateLocalList(localAuthList.as(), localListVersion, false); + REQUIRE( authService->getLocalListVersion() == 0 ); //localListVersion is ignored - empty list always resets version + REQUIRE( authService->getLocalListSize() == 0 ); + + //consecutive updates in Differential mode + localListVersion = 1; + localAuthList.clear(); + localAuthList[0]["idTag"] = "mIdTag"; + localAuthList[0]["idTagInfo"]["status"] = "Accepted"; + authService->updateLocalList(localAuthList.as(), localListVersion, true); + REQUIRE( authService->getLocalListVersion() == localListVersion ); + REQUIRE( authService->getLocalListSize() == 1 ); + + //append further entry + localListVersion++; + localAuthList.clear(); + localAuthList[0]["idTag"] = "mIdTag2"; + localAuthList[0]["idTagInfo"]["status"] = "Accepted"; + authService->updateLocalList(localAuthList.as(), localListVersion, true); + REQUIRE( authService->getLocalListVersion() == localListVersion ); + REQUIRE( authService->getLocalListSize() == 2 ); + + //overwrite previous entry + REQUIRE( authService->getLocalAuthorization("mIdTag")->getAuthorizationStatus() == AuthorizationStatus::Accepted ); + localListVersion++; + localAuthList.clear(); + localAuthList[0]["idTag"] = "mIdTag"; + localAuthList[0]["idTagInfo"]["status"] = "Blocked"; + authService->updateLocalList(localAuthList.as(), localListVersion, true); + REQUIRE( authService->getLocalListVersion() == localListVersion ); + REQUIRE( authService->getLocalListSize() == 2 ); + REQUIRE( authService->getLocalAuthorization("mIdTag")->getAuthorizationStatus() == AuthorizationStatus::Blocked ); + + //empty update keeps previous entries + localListVersion++; + localAuthList.clear(); + localAuthList.to(); + authService->updateLocalList(localAuthList.as(), localListVersion, true); + REQUIRE( authService->getLocalListVersion() == localListVersion ); + REQUIRE( authService->getLocalListSize() == 2 ); + + //delete entries + localListVersion++; + localAuthList.clear(); + localAuthList[0]["idTag"] = "mIdTag"; + authService->updateLocalList(localAuthList.as(), localListVersion, true); + REQUIRE( authService->getLocalListVersion() == localListVersion ); + REQUIRE( authService->getLocalListSize() == 1 ); + + localListVersion++; + localAuthList.clear(); + localAuthList[0]["idTag"] = "mIdTag2"; + authService->updateLocalList(localAuthList.as(), localListVersion, true); + REQUIRE( authService->getLocalListVersion() == 0 ); + REQUIRE( authService->getLocalListSize() == 0 ); + } + + SECTION("LocalList persistency") { + + int listVersion = 42; + + StaticJsonDocument<512> localAuthList; + localAuthList[0]["idTag"] = "mIdTagMinimal"; + localAuthList[0]["idTagInfo"]["status"] = "Accepted"; + localAuthList[1]["idTag"] = "mIdTagFull"; + localAuthList[1]["idTagInfo"]["expiryDate"] = BASE_TIME; + localAuthList[1]["idTagInfo"]["parentIdTag"] = "mParentIdTag"; + localAuthList[1]["idTagInfo"]["status"] = "Blocked"; + authService->updateLocalList(localAuthList.as(), listVersion, false); + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + authService = getOcppContext()->getModel().getAuthorizationService(); + + REQUIRE( authService->getLocalListVersion() == listVersion ); + REQUIRE( authService->getLocalListSize() == 2 ); + auto auth0 = authService->getLocalAuthorization("mIdTagMinimal"); + REQUIRE( auth0 != nullptr ); + REQUIRE( !strcmp(auth0->getIdTag(), "mIdTagMinimal") ); + REQUIRE( auth0->getExpiryDate() == nullptr ); + REQUIRE( auth0->getParentIdTag() == nullptr ); + REQUIRE( auth0->getAuthorizationStatus() == AuthorizationStatus::Accepted ); + + auto auth1 = authService->getLocalAuthorization("mIdTagFull"); + REQUIRE( auth1 != nullptr ); + REQUIRE( !strcmp(auth1->getIdTag(), "mIdTagFull") ); + REQUIRE( auth1->getExpiryDate() != nullptr ); + Timestamp baseTimeParsed; + baseTimeParsed.setTime(BASE_TIME); + REQUIRE( *auth1->getExpiryDate() == baseTimeParsed ); + REQUIRE( !strcmp(auth1->getParentIdTag(), "mParentIdTag") ); + REQUIRE( auth1->getAuthorizationStatus() == AuthorizationStatus::Blocked ); + } + + SECTION("SendLocalList") { + + int listVersion = 42; + size_t listSize = 2; + auto populatedEntryIdTag = makeString("UnitTests"); //local auth list entry to be fully populated + + //Full update - happy path + bool checkAccepted = false; + getOcppContext()->initiateRequest(makeRequest( + new Ocpp16::CustomOperation("SendLocalList", + [&listVersion, &listSize, &populatedEntryIdTag] () { + //create req + auto doc = makeJsonDoc("UnitTests", + 4096); + auto payload = doc->to(); + payload["listVersion"] = listVersion; + generateAuthList(payload["localAuthorizationList"].to(), listSize, false); + + //fully populate first entry + populatedEntryIdTag = payload["localAuthorizationList"][0]["idTag"] | "_Undefined"; + payload["localAuthorizationList"][0]["idTagInfo"]["expiryDate"] = BASE_TIME; + payload["localAuthorizationList"][0]["idTagInfo"]["parentIdTag"] = "mParentIdTag"; + payload["localAuthorizationList"][0]["idTagInfo"]["status"] = "Blocked"; + payload["updateType"] = "Full"; + return doc; + }, + [&checkAccepted] (JsonObject payload) { + //process conf + checkAccepted = !strcmp(payload["status"] | "_Undefined", "Accepted"); + }))); + + loop(); + REQUIRE( authService->getLocalListVersion() == listVersion ); + REQUIRE( authService->getLocalListSize() == listSize ); + REQUIRE( !populatedEntryIdTag.empty() ); + auto localAuth = authService->getLocalAuthorization(populatedEntryIdTag.c_str()); + REQUIRE( localAuth != nullptr ); + Timestamp baseTimeParsed; + baseTimeParsed.setTime(BASE_TIME); + REQUIRE( localAuth->getExpiryDate() ); + REQUIRE( *localAuth->getExpiryDate() == baseTimeParsed ); + REQUIRE( !strcmp(localAuth->getIdTag(), populatedEntryIdTag.c_str()) ); + REQUIRE( !strcmp(localAuth->getParentIdTag(), "mParentIdTag") ); + REQUIRE( localAuth->getAuthorizationStatus() == AuthorizationStatus::Blocked ); + REQUIRE( checkAccepted ); + + //Differential update - happy path + listVersion++; + listSize++; //add one extra entry and update all others + + checkAccepted = false; + getOcppContext()->initiateRequest(makeRequest( + new Ocpp16::CustomOperation("SendLocalList", + [&listVersion, &listSize] () { + //create req + auto doc = makeJsonDoc("UnitTests", + 1024); + auto payload = doc->to(); + payload["listVersion"] = listVersion; + generateAuthList(payload["localAuthorizationList"].to(), listSize, false); + payload["updateType"] = "Differential"; + return doc; + }, + [&checkAccepted] (JsonObject payload) { + //process conf + checkAccepted = !strcmp(payload["status"] | "_Undefined", "Accepted"); + }))); + + loop(); + REQUIRE( authService->getLocalListVersion() == listVersion ); + REQUIRE( authService->getLocalListSize() == listSize ); + REQUIRE( checkAccepted ); + + //Differential update - version mismatch + size_t listSizeInvalid = listSize + 1; + + bool checkVersionMismatch = false; + getOcppContext()->initiateRequest(makeRequest( + new Ocpp16::CustomOperation("SendLocalList", + [&listVersion, &listSizeInvalid] () { + //create req + auto doc = makeJsonDoc("UnitTests", + 1024); + auto payload = doc->to(); + payload["listVersion"] = listVersion; + generateAuthList(payload["localAuthorizationList"].to(), listSizeInvalid, false); + payload["updateType"] = "Differential"; + return doc; + }, + [&checkVersionMismatch] (JsonObject payload) { + //process conf + checkVersionMismatch = !strcmp(payload["status"] | "_Undefined", "VersionMismatch"); + }))); + + loop(); + REQUIRE( authService->getLocalListVersion() == listVersion ); + REQUIRE( authService->getLocalListSize() == listSize ); + REQUIRE( checkVersionMismatch ); + + //Boundary check - maximum entries per SendLocalList + listVersion = 42; + listSize = (size_t) declareConfiguration("SendLocalListMaxLength", -1)->getInt(); + + checkAccepted = false; + getOcppContext()->initiateRequest(makeRequest( + new Ocpp16::CustomOperation("SendLocalList", + [&listVersion, &listSize] () { + //create req + auto doc = makeJsonDoc("UnitTests", + 4096); + auto payload = doc->to(); + payload["listVersion"] = listVersion; + generateAuthList(payload["localAuthorizationList"].to(), listSize, false); + payload["updateType"] = "Full"; + return doc; + }, + [&checkAccepted] (JsonObject payload) { + //process conf + checkAccepted = !strcmp(payload["status"] | "_Undefined", "Accepted"); + }))); + + loop(); + REQUIRE( authService->getLocalListVersion() == listVersion ); + REQUIRE( authService->getLocalListSize() == listSize ); + REQUIRE( checkAccepted ); + + //Boundary check - maximum entries per SendLocalList in Differential mode + listVersion++; + + checkAccepted = false; + getOcppContext()->initiateRequest(makeRequest( + new Ocpp16::CustomOperation("SendLocalList", + [&listVersion, &listSize] () { + //create req + auto doc = makeJsonDoc("UnitTests", + 4096); + auto payload = doc->to(); + payload["listVersion"] = listVersion; + generateAuthList(payload["localAuthorizationList"].to(), listSize, false); + payload["updateType"] = "Differential"; + return doc; + }, + [&checkAccepted] (JsonObject payload) { + //process conf + checkAccepted = !strcmp(payload["status"] | "_Undefined", "Accepted"); + }))); + + loop(); + REQUIRE( authService->getLocalListVersion() == listVersion ); + REQUIRE( authService->getLocalListSize() == listSize ); + REQUIRE( checkAccepted ); + + //Boundary check - exceed maximum entries per SendLocalList + int listVersionInvalid = listVersion + 1; + listSizeInvalid = listSize + 1; + + bool errOccurence = false; + getOcppContext()->initiateRequest(makeRequest( + new Ocpp16::CustomOperation("SendLocalList", + [&listVersionInvalid, &listSizeInvalid] () { + //create req + auto doc = makeJsonDoc("UnitTests", + 4096); + auto payload = doc->to(); + payload["listVersion"] = listVersionInvalid; + generateAuthList(payload["localAuthorizationList"].to(), listSizeInvalid, false); + payload["updateType"] = "Full"; + return doc; + }, + [] (JsonObject) { }, //ignore conf + [&errOccurence] (const char *errorCode, const char*, JsonObject) { + errOccurence = !strcmp(errorCode, "OccurenceConstraintViolation"); + return true; + }))); + + loop(); + REQUIRE( authService->getLocalListVersion() == listVersion ); + REQUIRE( authService->getLocalListSize() == listSize ); + REQUIRE( errOccurence ); + + //Boundary check - exceed maximum local list size by multiple Differerntial updates + + //clear local list + StaticJsonDocument<64> emptyDoc; + emptyDoc.to(); + authService->updateLocalList(emptyDoc.as(), 1, false); + + size_t localAuthListMaxLength = (size_t) declareConfiguration("LocalAuthListMaxLength", -1)->getInt(); + + //send Differential lists with 2 entries: update an existing entry and add a new entry + for (size_t i = 1; i < localAuthListMaxLength; i++) { + //Full update - happy path + bool checkAccepted = false; + getOcppContext()->initiateRequest(makeRequest( + new Ocpp16::CustomOperation("SendLocalList", + [&i] () { + //create req + auto doc = makeJsonDoc("UnitTests", + 1024); + auto payload = doc->to(); + payload["listVersion"] = (int) i; + + char buf [IDTAG_LEN_MAX + 1]; + sprintf(buf, "mIdTag%zu", i-1); + payload["localAuthorizationList"][0]["idTag"] = buf; + payload["localAuthorizationList"][0]["idTagInfo"]["status"] = "Accepted"; + + sprintf(buf, "mIdTag%zu", i); + payload["localAuthorizationList"][1]["idTag"] = buf; + payload["localAuthorizationList"][1]["idTagInfo"]["status"] = "Accepted"; + + payload["updateType"] = "Differential"; + return doc; + }, + [&checkAccepted] (JsonObject payload) { + //process conf + checkAccepted = !strcmp(payload["status"] | "_Undefined", "Accepted"); + }))); + + loop(); + REQUIRE( authService->getLocalListVersion() == (int)i ); + REQUIRE( authService->getLocalListSize() == i + 1 ); + REQUIRE( checkAccepted ); + } + + //now exceed local list by sending overflow entry + listVersion = authService->getLocalListVersion(); + listVersionInvalid = listVersion + 1; + listSize = authService->getLocalListSize(); + + bool checkFailed = false; + getOcppContext()->initiateRequest(makeRequest( + new Ocpp16::CustomOperation("SendLocalList", + [&listVersionInvalid] () { + //create req + auto doc = makeJsonDoc("UnitTests", + 1024); + auto payload = doc->to(); + payload["listVersion"] = listVersionInvalid; + + //update already existing entry + char buf [IDTAG_LEN_MAX + 1]; + sprintf(buf, "mIdTag%zu", 0UL); + payload["localAuthorizationList"][0]["idTag"] = buf; + payload["localAuthorizationList"][0]["idTagInfo"]["status"] = "Accepted"; + + //additional overflowing entry + payload["localAuthorizationList"][1]["idTag"] = "overflow idTag"; + payload["localAuthorizationList"][1]["idTagInfo"]["status"] = "Accepted"; + + payload["updateType"] = "Differential"; + return doc; + }, + [&checkFailed] (JsonObject payload) { + //process conf + checkFailed = !strcmp(payload["status"] | "_Undefined", "Failed"); + }))); + loop(); + REQUIRE( authService->getLocalListVersion() == listVersion ); + REQUIRE( authService->getLocalListSize() == listSize ); + REQUIRE( checkFailed ); + } + + SECTION("GetLocalListVersion") { + + int localListVersion = 42; + StaticJsonDocument<256> localAuthList; + localAuthList[0]["idTag"] = "mIdTag"; + localAuthList[0]["idTagInfo"]["status"] = "Accepted"; + authService->updateLocalList(localAuthList.as(), localListVersion, false); + + int checkListVerion = -1; + getOcppContext()->initiateRequest(makeRequest( + new Ocpp16::CustomOperation("GetLocalListVersion", + [] () { + //create req + return createEmptyDocument(); + }, + [&checkListVerion] (JsonObject payload) { + //process conf + checkListVerion = payload["listVersion"] | -1; + }))); + + loop(); + REQUIRE( checkListVerion == localListVersion ); + } + + mocpp_deinitialize(); +} + +#endif //MO_ENABLE_LOCAL_AUTH diff --git a/tests/Metering.cpp b/tests/Metering.cpp new file mode 100644 index 00000000..aa555e99 --- /dev/null +++ b/tests/Metering.cpp @@ -0,0 +1,731 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include +#include "./helpers/testHelper.h" + +#define BASE_TIME "2023-01-01T00:00:00.000Z" + +#define TRIGGER_METERVALUES "[2,\"msgId01\",\"TriggerMessage\",{\"requestedMessage\":\"MeterValues\"}]" + +using namespace MicroOcpp; + +TEST_CASE("Metering") { + printf("\nRun %s\n", "Metering"); + + //initialize Context with dummy socket + LoopbackConnection loopback; + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + auto context = getOcppContext(); + auto& model = context->getModel(); + + mocpp_set_timer(custom_timer_cb); + + model.getClock().setTime(BASE_TIME); + + endTransaction(); + + SECTION("Configure Metering Service") { + + addMeterValueInput([] () { + return 0; + }, "Energy.Active.Import.Register"); + + addMeterValueInput([] () { + return 0; + }, "Voltage"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData",""); + MeterValuesSampledDataString->setString(""); + + bool checkProcessed = false; + + //set up measurands and check validation + sendRequest("ChangeConfiguration", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["key"] = "MeterValuesSampledData"; + payload["value"] = "Energy.Active.Import.Register,INVALID,Voltage"; //invalid request + return doc; + }, [&checkProcessed] (JsonObject payload) { + checkProcessed = true; + REQUIRE(!strcmp(payload["status"], "Rejected")); + }); + + loop(); + + REQUIRE(checkProcessed); + REQUIRE(!strcmp(MeterValuesSampledDataString->getString(), "")); + + checkProcessed = false; + + sendRequest("ChangeConfiguration", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["key"] = "MeterValuesSampledData"; + payload["value"] = "Voltage,Energy.Active.Import.Register"; //valid request + return doc; + }, [&checkProcessed, &model] (JsonObject payload) { + checkProcessed = true; + REQUIRE(!strcmp(payload["status"], "Accepted")); + }); + + loop(); + + REQUIRE(checkProcessed); + REQUIRE(!strcmp(MeterValuesSampledDataString->getString(), "Voltage,Energy.Active.Import.Register")); + } + + SECTION("Capture Periodic data") { + + Timestamp base; + base.setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + bool checkProcessed = false; + + setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { + checkProcessed = true; + Timestamp t0; + t0.setTime(payload["meterValue"][0]["timestamp"] | ""); + + REQUIRE((t0 - base >= 10 && t0 - base <= 11)); + + REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["context"] | "", "Sample.Periodic")); + }); + + loop(); + + model.getClock().setTime(BASE_TIME); + + auto trackMtime = mtime; + + beginTransaction_authorized("mIdTag"); + + loop(); + + mtime = trackMtime + 10 * 1000; + + loop(); + + endTransaction(); + + loop(); + + REQUIRE(checkProcessed); + } + + SECTION("Capture Clock-aligned data") { + + Timestamp base; + base.setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + //disablee sampled data + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(0); + + auto ClockAlignedDataIntervalInt = declareConfiguration("ClockAlignedDataInterval", 0, CONFIGURATION_FN); + ClockAlignedDataIntervalInt->setInt(900); + + auto MeterValuesAlignedDataString = declareConfiguration("MeterValuesAlignedData", "", CONFIGURATION_FN); + MeterValuesAlignedDataString->setString("Energy.Active.Import.Register"); + + bool checkProcessed = false; + + setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { + checkProcessed = true; + Timestamp t0; + t0.setTime(payload["meterValue"][0]["timestamp"] | ""); + + REQUIRE((t0 - base >= 900 && t0 - base <= 901)); + + REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["context"] | "", "Sample.Clock")); + }); + + model.getClock().setTime("2023-01-01T00:00:10Z"); + loop(); + + beginTransaction_authorized("mIdTag"); + + loop(); + + model.getClock().setTime("2023-01-01T00:10:00Z"); + loop(); + + model.getClock().setTime("2023-01-01T00:15:00Z"); + loop(); + + model.getClock().setTime("2023-01-01T00:29:50Z"); + + endTransaction(); + + loop(); + + REQUIRE(checkProcessed); + } + + SECTION("Capture transaction-aligned data") { + + Timestamp base; + base.setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + auto StopTxnSampledDataString = declareConfiguration("StopTxnSampledData", "", CONFIGURATION_FN); + StopTxnSampledDataString->setString("Energy.Active.Import.Register"); + + configuration_save(); + + loop(); + + model.getClock().setTime(BASE_TIME); + + beginTransaction_authorized("mIdTag"); + + loop(); + + mocpp_deinitialize(); //check if StopData is stored over reboots + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + bool checkProcessed = false; + + setOnReceiveRequest("StopTransaction", [base, &checkProcessed] (JsonObject payload) { + checkProcessed = true; + + REQUIRE(payload["transactionData"].size() >= 2); + + Timestamp t0, t1; + t0.setTime(payload["transactionData"][0]["timestamp"] | ""); + t1.setTime(payload["transactionData"][1]["timestamp"] | ""); + + REQUIRE((t0 - base >= 0 && t0 - base <= 1)); + REQUIRE((t1 - base >= 3600 && t1 - base <= 3601)); + + REQUIRE(!strcmp(payload["transactionData"][0]["sampledValue"][0]["context"] | "", "Transaction.Begin")); + REQUIRE(!strcmp(payload["transactionData"][1]["sampledValue"][0]["context"] | "", "Transaction.End")); + }); + + loop(); + + getOcppContext()->getModel().getClock().setTime("2023-01-01T01:00:00Z"); + + endTransaction(); + + loop(); + + REQUIRE(checkProcessed); + } + + SECTION("Capture measurements at connectorId 0") { + + Timestamp base; + base.setTime(BASE_TIME); + + const unsigned int connectorId = 0; + + addMeterValueInput([base] () { + //simulate 3600W consumption + return 3600; + }, + "Power.Active.Import", + nullptr, + nullptr, + nullptr, + connectorId); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Power.Active.Import"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + bool checkProcessed = false; + + setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { + checkProcessed = true; + REQUIRE( !strncmp(payload["meterValue"][0]["sampledValue"][0]["value"] | "", "3600", strlen("3600")) ); + }); + + loop(); + + mtime += 10 * 1000; + + loop(); + + REQUIRE(checkProcessed); + } + + SECTION("Change measurands live") { + + Timestamp base; + base.setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + addMeterValueInput([] () { + return 3600; + }, "Power.Active.Import"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + bool checkProcessed = false; + + setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { + checkProcessed = true; + Timestamp t0; + t0.setTime(payload["meterValue"][0]["timestamp"] | ""); + + REQUIRE((t0 - base >= 10 && t0 - base <= 11)); + + REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["measurand"] | "", "Power.Active.Import")); + }); + + loop(); + + model.getClock().setTime(BASE_TIME); + + auto trackMtime = mtime; + + beginTransaction_authorized("mIdTag"); + + MeterValuesSampledDataString->setString("Power.Active.Import"); + + loop(); + + mtime = trackMtime + 10 * 1000; + + loop(); + + endTransaction(); + + loop(); + + REQUIRE(checkProcessed); + } + + SECTION("Preserve order of tx-related msgs") { + + loopback.setConnected(false); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true)->setBool(true); + + Timestamp base; + base.setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + configuration_save(); + + unsigned int countProcessed = 0; + + setOnReceiveRequest("StartTransaction", [&countProcessed] (JsonObject) { + REQUIRE(countProcessed == 0); + countProcessed++; + }); + + int assignedTxId = -1; + + setOnSendConf("StartTransaction", [&assignedTxId] (JsonObject conf) { + assignedTxId = conf["transactionId"]; + }); + + setOnReceiveRequest("MeterValues", [&countProcessed, &assignedTxId] (JsonObject req) { + REQUIRE(countProcessed == 1); + countProcessed++; + + int transactionId = req["transactionId"] | -1000; + + REQUIRE(assignedTxId == transactionId); + }); + + setOnReceiveRequest("StopTransaction", [&countProcessed] (JsonObject) { + REQUIRE(countProcessed == 2); + countProcessed++; + }); + + loop(); + + auto trackMtime = mtime; + + beginTransaction_authorized("mIdTag"); + + loop(); + + mtime = trackMtime + 10 * 1000; + + loop(); + + endTransaction(); + + loop(); + + loopback.setConnected(true); + + loop(); + + REQUIRE(countProcessed == 3); + + /* + * Combine test case with power loss. Start tx before power loss, then enqueue 1 MV, then StopTx + */ + + countProcessed = 0; + + beginTransaction("mIdTag"); + + loop(); + + mocpp_deinitialize(); + + loopback.setConnected(false); + + mocpp_initialize(loopback, ChargerCredentials()); + getOcppContext()->getModel().getClock().setTime(BASE_TIME); + + base.setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + setOnReceiveRequest("MeterValues", [&countProcessed, &assignedTxId] (JsonObject req) { + REQUIRE(countProcessed == 1); + countProcessed++; + + int transactionId = req["transactionId"] | -1000; + + REQUIRE(assignedTxId == transactionId); + }); + + setOnReceiveRequest("StopTransaction", [&countProcessed] (JsonObject) { + REQUIRE(countProcessed == 2); + countProcessed++; + }); + + trackMtime = mtime; + + loop(); + + mtime = trackMtime + 10 * 1000; + + loop(); + + endTransaction(); + + loop(); + + loopback.setConnected(true); + + loop(); + + REQUIRE(countProcessed == 3); + } + + SECTION("Queue multiple MeterValues") { + + Timestamp base; + base.setTime(BASE_TIME); + model.getClock().setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + unsigned int nrInitiated = 0; + unsigned int countProcessed = 0; + + setOnReceiveRequest("MeterValues", [&base, &nrInitiated, &countProcessed] (JsonObject payload) { + countProcessed++; + + Timestamp t0; + t0.setTime(payload["meterValue"][0]["timestamp"] | ""); + + REQUIRE((t0 - base >= 10 * ((int)nrInitiated - (MO_METERVALUES_CACHE_MAXSIZE - (int)countProcessed)) && t0 - base <= 1 + 10 * ((int)nrInitiated - (MO_METERVALUES_CACHE_MAXSIZE - (int)countProcessed)))); + }); + + + loop(); + + beginTransaction_authorized("mIdTag"); + + base = model.getClock().now(); + auto trackMtime = mtime; + + loop(); + + loopback.setConnected(false); + + //initiate 10 more MeterValues than can be cached + for (unsigned long i = 1; i <= 10 + MO_METERVALUES_CACHE_MAXSIZE; i++) { + mtime = trackMtime + i * 10 * 1000; + loop(); + + nrInitiated++; + } + + loopback.setConnected(true); + + loop(); + + REQUIRE(countProcessed == MO_METERVALUES_CACHE_MAXSIZE); + + endTransaction(); + + loop(); + + } + + SECTION("Drop MeterValues for silent tx") { + + loopback.setConnected(false); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true)->setBool(true); + + Timestamp base; + base.setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + configuration_save(); + + unsigned int countProcessed = 0; + + setOnReceiveRequest("StartTransaction", [&countProcessed] (JsonObject) { + countProcessed++; + }); + + int assignedTxId = -1; + + setOnSendConf("StartTransaction", [&assignedTxId] (JsonObject conf) { + assignedTxId = conf["transactionId"]; + }); + + setOnReceiveRequest("MeterValues", [&countProcessed, &assignedTxId] (JsonObject req) { + countProcessed++; + }); + + setOnReceiveRequest("StopTransaction", [&countProcessed] (JsonObject) { + REQUIRE(countProcessed == 2); + }); + + loop(); + + auto trackMtime = mtime; + + beginTransaction_authorized("mIdTag"); + auto tx = getTransaction(); + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + + mtime = trackMtime + 10 * 1000; + + loop(); + + endTransaction(); + + loop(); + + tx->setSilent(); + tx->commit(); + + loopback.setConnected(true); + + loop(); + + REQUIRE(countProcessed == 0); + } + + SECTION("TxMsg retry behavior") { + + Timestamp base; + + addMeterValueInput([&base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + configuration_save(); + + const size_t NUM_ATTEMPTS = 3; + const int RETRY_INTERVAL_SECS = 3600; + + declareConfiguration("TransactionMessageAttempts", 0)->setInt(NUM_ATTEMPTS); + declareConfiguration("TransactionMessageRetryInterval", 0)->setInt(RETRY_INTERVAL_SECS); + + unsigned int attemptNr = 0; + + getOcppContext()->getOperationRegistry().registerOperation("MeterValues", [&attemptNr] () { + return new Ocpp16::CustomOperation("MeterValues", + [&attemptNr] (JsonObject payload) { + //receive req + attemptNr++; + }, + [] () { + //create conf + return createEmptyDocument(); + }, + [] () { + //ErrorCode for CALLERROR + return "InternalError"; + });}); + + loop(); + + auto trackMtime = mtime; + base = model.getClock().now(); + + beginTransaction("mIdTag"); + + loop(); + + mtime = trackMtime + 10 * 1000; + + loop(); + + REQUIRE(attemptNr == 1); + + endTransaction(); + + mtime = trackMtime + 20 * 1000; + loop(); + REQUIRE(attemptNr == 1); + + mtime = trackMtime + 10 * 1000 + RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE(attemptNr == 2); + + mtime = trackMtime + 10 * 1000 + 2 * RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE(attemptNr == 2); + + mtime = trackMtime + 10 * 1000 + 3 * RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE(attemptNr == 3); + + mtime = trackMtime + 10 * 1000 + 7 * RETRY_INTERVAL_SECS * 1000; + loop(); + REQUIRE(attemptNr == 3); + } + + SECTION("TriggerMessage") { + + addMeterValueInput([] () { + return 12345; + }, "Energy.Active.Import.Register"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + Timestamp base; + + bool checkProcessed = false; + + setOnReceiveRequest("MeterValues", [&base, &checkProcessed] (JsonObject payload) { + int connectorId = payload["connectorId"] | -1; + if (connectorId != 1) { + return; + } + + checkProcessed = true; + + Timestamp t0; + t0.setTime(payload["meterValue"][0]["timestamp"] | ""); + + REQUIRE( std::abs(t0 - base) <= 1 ); + REQUIRE( !strncmp(payload["meterValue"][0]["sampledValue"][0]["value"] | "", "12345", strlen("12345")) ); + }); + + loop(); + + base = model.getClock().now(); + + loopback.sendTXT(TRIGGER_METERVALUES, sizeof(TRIGGER_METERVALUES) - 1); + loop(); + + REQUIRE(checkProcessed); + + } + + mocpp_deinitialize(); +} 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 new file mode 100644 index 00000000..306b804a --- /dev/null +++ b/tests/Reservation.cpp @@ -0,0 +1,516 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include + +#if MO_ENABLE_RESERVATION + +#include +#include +#include +#include "./helpers/testHelper.h" + +#include +#include +#include + +#include +#include +#include +#include + + +#define BASE_TIME "2023-01-01T00:00:00.000Z" + +using namespace MicroOcpp; + +TEST_CASE( "Reservation" ) { + printf("\nRun %s\n", "Reservation"); + + //clean state + auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); + FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); + + //initialize Context with dummy socket + LoopbackConnection loopback; + + mocpp_set_timer(custom_timer_cb); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + auto& model = getOcppContext()->getModel(); + auto rService = model.getReservationService(); + auto connector = model.getConnector(1); + model.getClock().setTime(BASE_TIME); + + loop(); + + SECTION("Basic reservation") { + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + REQUIRE( rService ); + + //set reservation + int reservationId = 123; + unsigned int connectorId = 1; + Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future + const char *idTag = "mIdTag"; + const char *parentIdTag = nullptr; + + rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); + + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); + + //transaction blocked by reservation + bool checkTxRejected = false; + setTxNotificationOutput([&checkTxRejected] (Transaction*, TxNotification txNotification) { + if (txNotification == TxNotification_ReservationConflict) { + checkTxRejected = true; + } + }); + + beginTransaction("wrong idTag"); + loop(); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); + REQUIRE( checkTxRejected ); + + //idTag matches reservation + beginTransaction("mIdTag"); + loop(); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + REQUIRE( connector->getTransaction()->getReservationId() == reservationId ); + + //reservation is reset after tx + endTransaction(); + loop(); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + //RemoteStartTx - idTag doesn't match. The tx will start anyway assuming some start trigger in the backend prevails over reservations in the backend implementation + rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTag"] = "wrong idTag"; + return doc;}, + [] (JsonObject) { } //ignore conf + ))); + loop(); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + REQUIRE( connector->getTransaction()->getReservationId() != reservationId ); + + //reservation is reset after tx + endTransaction(); + loop(); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + //RemoteStartTx - idTag does match + rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); + + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [idTag] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["idTag"] = idTag; + return doc;}, + [] (JsonObject) { } //ignore conf + ))); + loop(); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + REQUIRE( connector->getTransaction()->getReservationId() == reservationId ); + + //reservation is reset after tx + endTransaction(); + loop(); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + } + + SECTION("Tx on other connector") { + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + //set reservation + int reservationId = 123; + unsigned int connectorIdResvd = 1; //reserve connector 1 + unsigned int connectorIdOther = 2; //start charging on other connector + Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future + const char *idTag = "mIdTag"; + const char *parentIdTag = nullptr; + + rService->updateReservation(reservationId, connectorIdResvd, expiryDate, idTag, parentIdTag); + REQUIRE( model.getConnector(connectorIdResvd)->getStatus() == ChargePointStatus_Reserved ); + + beginTransaction(idTag, connectorIdOther); + loop(); + REQUIRE( model.getConnector(connectorIdResvd)->getStatus() == ChargePointStatus_Available ); //reservation on first connector withdrawed + REQUIRE( model.getConnector(connectorIdOther)->getStatus() == ChargePointStatus_Charging ); + REQUIRE( getTransaction(connectorIdOther)->getReservationId() == reservationId ); //reservation transferred to other connector + + endTransaction(nullptr, nullptr, connectorIdOther); + loop(); + REQUIRE( model.getConnector(connectorIdOther)->getStatus() == ChargePointStatus_Available ); + } + + SECTION("parentIdTag") { + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + //set reservation + int reservationId = 123; + unsigned int connectorId = 1; + Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future + const char *idTag = "mIdTag"; + const char *parentIdTag = "mParentIdTag"; + + rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); + + bool checkProcessed = false; + getOcppContext()->getOperationRegistry().registerOperation("Authorize", + [parentIdTag, &checkProcessed] () { + return new Ocpp16::CustomOperation("Authorize", + [] (JsonObject) {}, //ignore req payload + [parentIdTag, &checkProcessed] () { + //create conf + checkProcessed = true; + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(1) + //payload root + JSON_OBJECT_SIZE(3)); //idTagInfo + auto payload = doc->to(); + payload["idTagInfo"]["parentIdTag"] = parentIdTag; + payload["idTagInfo"]["status"] = "Accepted"; + return doc;}); + }); + beginTransaction("other idTag"); + loop(); + REQUIRE( checkProcessed ); + REQUIRE( connector->getStatus() == ChargePointStatus_Charging ); + REQUIRE( connector->getTransaction()->getReservationId() == reservationId ); + + //reset tx + endTransaction(); + loop(); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + } + + SECTION("ConnectorZero") { + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + //set reservation + Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future + const char *idTag = "mIdTag"; + const char *parentIdTag = nullptr; + + //if connector 0 is reserved, accept at most one further reservation + REQUIRE( rService->updateReservation(1000, 0, expiryDate, idTag, parentIdTag) ); + REQUIRE( rService->updateReservation(1001, 1, expiryDate, idTag, parentIdTag) ); + REQUIRE( !rService->updateReservation(1002, 2, expiryDate, idTag, parentIdTag) ); + REQUIRE( model.getConnector(2)->getStatus() == ChargePointStatus_Available ); + + //reset reservations + rService->getReservationById(1000)->clear(); + rService->getReservationById(1001)->clear(); + REQUIRE( model.getConnector(1)->getStatus() == ChargePointStatus_Available ); + + //if connector 0 is reserved, ensure that at least one physical connector remains available for the idTag of the reservation + REQUIRE( rService->updateReservation(1000, 0, expiryDate, idTag, parentIdTag) ); + + beginTransaction("other idTag", 1); + loop(); + REQUIRE( model.getConnector(1)->getStatus() == ChargePointStatus_Charging ); + + bool checkTxRejected = false; + setTxNotificationOutput([&checkTxRejected] (Transaction*, TxNotification txNotification) { + if (txNotification == TxNotification_ReservationConflict) { + checkTxRejected = true; + } + }, 2); + + beginTransaction("other idTag 2", 2); + loop(); + REQUIRE( checkTxRejected ); + REQUIRE( model.getConnector(2)->getStatus() == ChargePointStatus_Available ); + + + endTransaction(nullptr, nullptr, 1); + loop(); + } + + SECTION("Expiry date") { + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + //set reservation + int reservationId = 123; + unsigned int connectorId = 1; + Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future + const char *idTag = "mIdTag"; + const char *parentIdTag = nullptr; + + rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); + + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); + + Timestamp expired = expiryDate + 1; + char expired_cstr [JSONDATE_LENGTH + 1]; + expired.toJsonString(expired_cstr, JSONDATE_LENGTH + 1); + model.getClock().setTime(expired_cstr); + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + } + + SECTION("Reservation persistency") { + unsigned int connectorId = 1; + REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus_Available ); + + //set reservation + int reservationId = 123; + Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future + const char *idTag = "mIdTag"; + const char *parentIdTag = "mParentIdTag"; + + getOcppContext()->getModel().getReservationService()->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); + + REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus_Reserved ); + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + getOcppContext()->getModel().getClock().setTime(BASE_TIME); + loop(); + + REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus_Reserved ); + + auto reservation = getOcppContext()->getModel().getReservationService()->getReservationById(reservationId); + REQUIRE( reservation->getReservationId() == reservationId ); + REQUIRE( reservation->getConnectorId() == (int)connectorId ); + REQUIRE( reservation->getExpiryDate() == expiryDate ); + REQUIRE( !strcmp(reservation->getIdTag(), idTag) ); + REQUIRE( !strcmp(reservation->getParentIdTag(), parentIdTag) ); + + reservation->clear(); + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + getOcppContext()->getModel().getClock().setTime(BASE_TIME); + loop(); + + REQUIRE( getOcppContext()->getModel().getConnector(connectorId)->getStatus() == ChargePointStatus_Available ); + } + + SECTION("ReserveNow") { + + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + //set reservation + int reservationId = 123; + unsigned int connectorId = 1; + Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future + const char *idTag = "mIdTag"; + const char *parentIdTag = nullptr; + + //simple reservation + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ReserveNow", + [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { + //create req + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(5) + + JSONDATE_LENGTH + 1); + auto payload = doc->to(); + payload["connectorId"] = connectorId; + char expiryDate_cstr [JSONDATE_LENGTH + 1]; + expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1); + payload["expiryDate"] = expiryDate_cstr; + payload["idTag"] = idTag; + payload["parentIdTag"] = parentIdTag; + payload["reservationId"] = reservationId; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); + + model.getReservationService()->getReservationById(reservationId)->clear(); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + //reserve while charger is in Faulted state + const char *errorCode = "OtherError"; + addErrorCodeInput([&errorCode] () {return errorCode;}); + REQUIRE( connector->getStatus() == ChargePointStatus_Faulted ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ReserveNow", + [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { + //create req + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(5) + + JSONDATE_LENGTH + 1); + auto payload = doc->to(); + payload["connectorId"] = connectorId; + char expiryDate_cstr [JSONDATE_LENGTH + 1]; + expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1); + payload["expiryDate"] = expiryDate_cstr; + payload["idTag"] = idTag; + payload["parentIdTag"] = parentIdTag; + payload["reservationId"] = reservationId; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Faulted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + REQUIRE( connector->getStatus() == ChargePointStatus_Faulted ); + + errorCode = nullptr; //reset error + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + //reserve while connector is already occupied + setConnectorPluggedInput([] {return true;}); //plug EV + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ReserveNow", + [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { + //create req + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(5) + + JSONDATE_LENGTH + 1); + auto payload = doc->to(); + payload["connectorId"] = connectorId; + char expiryDate_cstr [JSONDATE_LENGTH + 1]; + expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1); + payload["expiryDate"] = expiryDate_cstr; + payload["idTag"] = idTag; + payload["parentIdTag"] = parentIdTag; + payload["reservationId"] = reservationId; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Occupied") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + REQUIRE( connector->getStatus() == ChargePointStatus_Preparing ); + + setConnectorPluggedInput(nullptr); //reset ConnectorPluggedInput + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + //Rejected ReserveNow status not possible + + //reserve while connector is inoperative + connector->setAvailabilityVolatile(false); + REQUIRE( connector->getStatus() == ChargePointStatus_Unavailable ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "ReserveNow", + [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { + //create req + auto doc = makeJsonDoc("UnitTests", + JSON_OBJECT_SIZE(5) + + JSONDATE_LENGTH + 1); + auto payload = doc->to(); + payload["connectorId"] = connectorId; + char expiryDate_cstr [JSONDATE_LENGTH + 1]; + expiryDate.toJsonString(expiryDate_cstr, JSONDATE_LENGTH + 1); + payload["expiryDate"] = expiryDate_cstr; + payload["idTag"] = idTag; + payload["parentIdTag"] = parentIdTag; + payload["reservationId"] = reservationId; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Unavailable") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + REQUIRE( connector->getStatus() == ChargePointStatus_Unavailable ); + + connector->setAvailabilityVolatile(true); //revert Unavailable status + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + } + + SECTION("CancelReservation") { + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + //set reservation + int reservationId = 123; + unsigned int connectorId = 1; + Timestamp expiryDate = model.getClock().now() + 3600; //expires one hour in future + const char *idTag = "mIdTag"; + const char *parentIdTag = nullptr; + + rService->updateReservation(reservationId, connectorId, expiryDate, idTag, parentIdTag); + REQUIRE( connector->getStatus() == ChargePointStatus_Reserved ); + + //CancelReservation successfully + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "CancelReservation", + [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["reservationId"] = reservationId; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + + //CancelReservation while no reservation exists + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "CancelReservation", + [reservationId, connectorId, expiryDate, idTag, parentIdTag] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(1)); + auto payload = doc->to(); + payload["reservationId"] = reservationId; + return doc;}, + [&checkProcessed] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Rejected") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + REQUIRE( connector->getStatus() == ChargePointStatus_Available ); + } + + mocpp_deinitialize(); +} + +#endif //MO_ENABLE_RESERVATION 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 new file mode 100644 index 00000000..7cf53b32 --- /dev/null +++ b/tests/SmartCharging.cpp @@ -0,0 +1,845 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "./helpers/testHelper.h" + +#define BASE_TIME "2023-01-01T00:00:00.000Z" + +#define SCPROFILE_0 "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":1,\"csChargingProfiles\":{\"chargingProfileId\":0,\"stackLevel\":0,\"chargingProfilePurpose\":\"TxDefaultProfile\",\"chargingProfileKind\":\"Recurring\",\"recurrencyKind\":\"Daily\",\"validFrom\":\"2022-06-12T00:00:00.000Z\",\"validTo\":\"2023-06-21T00:00:00.000Z\",\"chargingSchedule\":{\"duration\":1000000,\"startSchedule\":\"2023-06-18T00:00:00.000Z\",\"chargingRateUnit\":\"W\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3},{\"startPeriod\":18000,\"limit\":32,\"numberPhases\":3}],\"minChargingRate\":6}}}]" +#define SCPROFILE_0_ALT_SAME_ID "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":1,\"csChargingProfiles\":{\"chargingProfileId\":0,\"stackLevel\":1,\"chargingProfilePurpose\":\"TxDefaultProfile\",\"chargingProfileKind\":\"Recurring\",\"recurrencyKind\":\"Daily\",\"validFrom\":\"2022-06-12T00:00:00.000Z\",\"validTo\":\"2023-06-21T00:00:00.000Z\",\"chargingSchedule\":{\"duration\":1000000,\"startSchedule\":\"2023-06-18T00:00:00.000Z\",\"chargingRateUnit\":\"W\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3},{\"startPeriod\":18000,\"limit\":32,\"numberPhases\":3}],\"minChargingRate\":6}}}]" +#define SCPROFILE_0_ALT_SAME_STACKEVEL_PURPOSE "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":1,\"csChargingProfiles\":{\"chargingProfileId\":1,\"stackLevel\":0,\"chargingProfilePurpose\":\"TxDefaultProfile\",\"chargingProfileKind\":\"Recurring\",\"recurrencyKind\":\"Daily\",\"validFrom\":\"2022-06-12T00:00:00.000Z\",\"validTo\":\"2023-06-21T00:00:00.000Z\",\"chargingSchedule\":{\"duration\":1000000,\"startSchedule\":\"2023-06-18T00:00:00.000Z\",\"chargingRateUnit\":\"W\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3},{\"startPeriod\":18000,\"limit\":32,\"numberPhases\":3}],\"minChargingRate\":6}}}]" + +#define SCPROFILE_1_ABSOLUTE_LIMIT_16A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":1,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"chargingSchedule\":{\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3}]}}}]" + +#define SCPROFILE_2_RELATIVE_TXDEF_24A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":1,\"csChargingProfiles\":{\"chargingProfileId\":2,\"stackLevel\":0,\"chargingProfilePurpose\":\"TxDefaultProfile\",\"chargingProfileKind\":\"Relative\",\"chargingSchedule\":{\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":24,\"numberPhases\":3}]}}}]" + +#define SCPROFILE_3_TXPROF_TXID123_20A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":1,\"csChargingProfiles\":{\"chargingProfileId\":3,\"transactionId\":123,\"stackLevel\":0,\"chargingProfilePurpose\":\"TxProfile\",\"chargingProfileKind\":\"Relative\",\"chargingSchedule\":{\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":20,\"numberPhases\":3}]}}}]" + +#define SCPROFILE_4_VALID_FROM_2024_16A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":4,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"validFrom\":\"2024-01-01T00:00:00.000Z\",\"chargingSchedule\":{\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3}]}}}]" +#define SCPROFILE_5_VALID_UNTIL_2022_16A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":5,\"stackLevel\":1,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"validTo\": \"2022-01-01T00:00:00.000Z\",\"chargingSchedule\":{\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3}]}}}]" + +#define SCPROFILE_6_MULTIPLE_PERIODS_16A_20A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":6,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"chargingSchedule\":{\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3},{\"startPeriod\":3600,\"limit\":20,\"numberPhases\":3}]}}}]" + +#define SCPROFILE_7_RECURRING_DAY_2H_16A_20A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":7,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Recurring\",\"recurrencyKind\":\"Daily\", \"chargingSchedule\":{\"duration\":7200,\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":16,\"numberPhases\":3},{\"startPeriod\":3600,\"limit\":20,\"numberPhases\":3}]}}}]" +#define SCPROFILE_8_RECURRING_WEEK_2H_10A_12A "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":8,\"stackLevel\":1,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Recurring\",\"recurrencyKind\":\"Weekly\",\"chargingSchedule\":{\"duration\":7200,\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":10,\"numberPhases\":3},{\"startPeriod\":3600,\"limit\":12,\"numberPhases\":3}]}}}]" + +#define SCPROFILE_9_VIA_RMTSTARTTX_20A "[2,\"testmsg\",\"RemoteStartTransaction\",{\"connectorId\":1,\"idTag\":\"mIdTag\",\"chargingProfile\":{\"chargingProfileId\":9,\"stackLevel\":0,\"chargingProfilePurpose\":\"TxProfile\",\"chargingProfileKind\":\"Relative\",\"chargingSchedule\":{\"chargingRateUnit\":\"A\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":20,\"numberPhases\":1}]}}}]" + +#define SCPROFILE_10_ABSOLUTE_LIMIT_5KW "[2,\"testmsg\",\"SetChargingProfile\",{\"connectorId\":0,\"csChargingProfiles\":{\"chargingProfileId\":10,\"stackLevel\":0,\"chargingProfilePurpose\":\"ChargePointMaxProfile\",\"chargingProfileKind\":\"Absolute\",\"chargingSchedule\":{\"startSchedule\":\"2023-01-01T00:00:00.000Z\",\"chargingRateUnit\":\"W\",\"chargingSchedulePeriod\":[{\"startPeriod\":0,\"limit\":5000,\"numberPhases\":3}]}}}]" + + +using namespace MicroOcpp; + + +TEST_CASE( "SmartCharging" ) { + printf("\nRun %s\n", "SmartCharging"); + + //initialize Context with dummy socket + LoopbackConnection loopback; + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + auto context = getOcppContext(); + auto& model = context->getModel(); + + mocpp_set_timer(custom_timer_cb); + + model.getClock().setTime(BASE_TIME); + + endTransaction(); + + SECTION("Load Smart Charging Service"){ + + REQUIRE(!model.getSmartChargingService()); + + setSmartChargingOutput([] (float, float, int) {}); + + REQUIRE(model.getSmartChargingService()); + } + + setSmartChargingOutput([] (float, float, int) {}); + auto scService = model.getSmartChargingService(); + + scService->clearChargingProfile([] (int, int, ChargingProfilePurposeType, int) { + return true; + }); + + SECTION("Set Charging Profile and clear") { + + unsigned int count = 0; + scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { + count++; + return true; + }); + + REQUIRE(count == 0); + + loopback.sendTXT(SCPROFILE_0, strlen(SCPROFILE_0)); + + //check if filter works by comparing the outcome of returning false and true and repeating the test + count = 0; + scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { + count++; + return false; + }); + + REQUIRE(count == 1); + + count = 0; + scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { + count++; + return true; + }); + + REQUIRE(count == 1); + + count = 0; + scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { + count++; + return true; + }); + + REQUIRE(count == 0); + } + + SECTION("Charging Profiles persistency over reboots") { + + loopback.sendTXT(SCPROFILE_0, strlen(SCPROFILE_0)); + + mocpp_deinitialize(); + + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + + setSmartChargingOutput([] (float, float, int) {}); + scService = getOcppContext()->getModel().getSmartChargingService(); + + unsigned int count = 0; + scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { + count++; + return true; + }); + + REQUIRE (count == 1); + } + + SECTION("Set conflicting profile") { + + loopback.sendTXT(SCPROFILE_0, strlen(SCPROFILE_0)); + + loopback.sendTXT(SCPROFILE_0_ALT_SAME_ID, strlen(SCPROFILE_0_ALT_SAME_ID)); + + unsigned int count = 0; + scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { + count++; + return true; + }); + + REQUIRE(count == 1); + + loopback.sendTXT(SCPROFILE_0, strlen(SCPROFILE_0)); + + loopback.sendTXT(SCPROFILE_0_ALT_SAME_STACKEVEL_PURPOSE, strlen(SCPROFILE_0_ALT_SAME_STACKEVEL_PURPOSE)); + + count = 0; + scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { + count++; + return true; + }); + + REQUIRE(count == 1); + } + + SECTION("Set charging profile via RmtStartTx") { + + float current = -1.f; + setSmartChargingOutput([¤t] (float, float limit_current, int) { + current = limit_current; + }); + + loop(); + + loopback.sendTXT(SCPROFILE_9_VIA_RMTSTARTTX_20A, strlen(SCPROFILE_9_VIA_RMTSTARTTX_20A)); + + loop(); + + REQUIRE((current > 19.99f && current < 20.01f)); + + endTransaction(); + + loop(); + } + + SECTION("Set ChargePointMaxProfile - Absolute") { + + float current = -1.f; + setSmartChargingOutput([¤t] (float, float limit_current, int) { + current = limit_current; + }); + + loopback.sendTXT(SCPROFILE_1_ABSOLUTE_LIMIT_16A, strlen(SCPROFILE_1_ABSOLUTE_LIMIT_16A)); + + loop(); + + REQUIRE((current > 15.99f && current < 16.01f)); + } + + SECTION("Set TxDefaultProfile - Relative") { + + float current = -1.f; + setSmartChargingOutput([¤t] (float, float limit_current, int) { + current = limit_current; + }); + + loopback.sendTXT(SCPROFILE_2_RELATIVE_TXDEF_24A, strlen(SCPROFILE_2_RELATIVE_TXDEF_24A)); + + loop(); + + REQUIRE(current < 0.f); + + beginTransaction_authorized("mIdTag"); + + loop(); + + REQUIRE((current > 23.99f && current < 24.01f)); + + endTransaction(); + + loop(); + + REQUIRE(current < 0.f); + } + + SECTION("Set TxProfile - tx fit and mismatch") { + + float current = -1.f; + setSmartChargingOutput([¤t] (float, float limit_current, int) { + current = limit_current; + }); + + //send before transaction - expect rejection + loopback.sendTXT(SCPROFILE_3_TXPROF_TXID123_20A, strlen(SCPROFILE_3_TXPROF_TXID123_20A)); + + unsigned int count = 0; + scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { + count++; + return true; + }); + + REQUIRE(count == 0); + + loop(); + beginTransaction_authorized("mIdTag"); + + //send during transaction but wrong txId - expect rejection + loopback.sendTXT(SCPROFILE_3_TXPROF_TXID123_20A, strlen(SCPROFILE_3_TXPROF_TXID123_20A)); + + loop(); + + count = 0; + scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { + count++; + return true; + }); + + REQUIRE(count == 0); + + getTransaction()->setTransactionId(123); + + //send during tx with matchin txId - accept + loopback.sendTXT(SCPROFILE_3_TXPROF_TXID123_20A, strlen(SCPROFILE_3_TXPROF_TXID123_20A)); + + loop(); + + REQUIRE((current > 19.99f && current < 20.01f)); + + endTransaction(); + + loop(); + + //check if SCService deleted TxProfiles after tx + count = 0; + scService->clearChargingProfile([&count] (int, int, ChargingProfilePurposeType, int) { + count++; + return true; + }); + + REQUIRE(count == 0); + } + + SECTION("Time validity check") { + + float current = -1.f; + setSmartChargingOutput([¤t] (float, float limit_current, int) { + current = limit_current; + }); + + loopback.sendTXT(SCPROFILE_4_VALID_FROM_2024_16A, strlen(SCPROFILE_4_VALID_FROM_2024_16A)); + + loopback.sendTXT(SCPROFILE_5_VALID_UNTIL_2022_16A, strlen(SCPROFILE_5_VALID_UNTIL_2022_16A)); + + loop(); + + REQUIRE(current < 0.f); + + //now reach validity period of future profile + model.getClock().setTime("2024-01-01T00:00:00.000Z"); + + loop(); + + REQUIRE((current > 15.99f && current < 16.01f)); + } + + SECTION("Multiple periods") { + + float current = -1.f; + setSmartChargingOutput([¤t] (float, float limit_current, int) { + current = limit_current; + }); + + loopback.sendTXT(SCPROFILE_6_MULTIPLE_PERIODS_16A_20A, strlen(SCPROFILE_6_MULTIPLE_PERIODS_16A_20A)); + + loop(); + + REQUIRE((current > 15.99f && current < 16.01f)); + + //now reach next period + model.getClock().setTime("2023-01-01T01:00:00.000Z"); + + loop(); + + REQUIRE((current > 19.99f && current < 20.01f)); + } + + SECTION("Recurring profiles - Daily") { + + float current = -1.f; + setSmartChargingOutput([¤t] (float, float limit_current, int) { + current = limit_current; + }); + + loopback.sendTXT(SCPROFILE_7_RECURRING_DAY_2H_16A_20A, strlen(SCPROFILE_7_RECURRING_DAY_2H_16A_20A)); + + loop(); + + REQUIRE((current > 15.99f && current < 16.01f)); + + //now exceed duration + model.getClock().setTime("2023-01-01T02:00:00.000Z"); + + loop(); + + REQUIRE(current < 0.f); + + //check second period three days afterwards + model.getClock().setTime("2023-01-04T01:00:00.000Z"); + + loop(); + + REQUIRE((current > 19.99f && current < 20.01f)); + } + + SECTION("Recurring profiles - Weekly") { + + float current = -1.f; + setSmartChargingOutput([¤t] (float, float limit_current, int) { + current = limit_current; + }); + + loopback.sendTXT(SCPROFILE_8_RECURRING_WEEK_2H_10A_12A, strlen(SCPROFILE_8_RECURRING_WEEK_2H_10A_12A)); + + loop(); + + REQUIRE((current > 9.99f && current < 10.01f)); + + //now exceed duration + model.getClock().setTime("2023-01-01T02:00:00.000Z"); + + loop(); + + REQUIRE(current < 0.f); + + //check second period three weeks afterwards + model.getClock().setTime("2023-01-22T01:00:00.000Z"); + + loop(); + + REQUIRE((current > 11.99f && current < 12.01f)); + } + + SECTION("Stacking recurring profiles") { + + float current = -1.f; + setSmartChargingOutput([¤t] (float, float limit_current, int) { + current = limit_current; + }); + + loopback.sendTXT(SCPROFILE_7_RECURRING_DAY_2H_16A_20A, strlen(SCPROFILE_7_RECURRING_DAY_2H_16A_20A)); //stackLevel: 0 + + loopback.sendTXT(SCPROFILE_8_RECURRING_WEEK_2H_10A_12A, strlen(SCPROFILE_8_RECURRING_WEEK_2H_10A_12A)); //stackLevel: 1 + + loop(); + + REQUIRE((current > 9.99f && current < 10.01f)); //Weekly schedule prevails + + //check again during the week + model.getClock().setTime("2023-01-03T00:00:00.000Z"); + + loop(); + + REQUIRE((current > 15.99f && current < 16.01f)); //Weekly schedule out of duration, only Daily defined + + //check again three weeks later + model.getClock().setTime("2023-01-22T00:00:00.000Z"); + + loop(); + + REQUIRE((current > 9.99f && current < 10.01f)); //Weekly schedule prevails again + + //check again during the week + model.getClock().setTime("2023-01-23T00:00:00.000Z"); + + loop(); + + REQUIRE((current > 15.99f && current < 16.01f)); //Weekly schedule out of duration again, only Daily defined again + } + + SECTION("TxProfile capped by ChargePointMaxProfile") { + + float current = -1.f; + int numberPhases = -1; + setSmartChargingOutput([¤t, &numberPhases] (float, float limit_current, int limit_numberPhases) { + current = limit_current; + numberPhases = limit_numberPhases; + }); + + loop(); + + loopback.sendTXT(SCPROFILE_9_VIA_RMTSTARTTX_20A, strlen(SCPROFILE_9_VIA_RMTSTARTTX_20A)); + + loop(); + + loopback.sendTXT(SCPROFILE_1_ABSOLUTE_LIMIT_16A, strlen(SCPROFILE_1_ABSOLUTE_LIMIT_16A)); + + loop(); + + REQUIRE((current > 15.99f && current < 16.01f)); //current limited by ChargePointMaxProfile + REQUIRE(numberPhases == 1); //numberPhases limited by TxProfile + + endTransaction(); + + loop(); + } + + SECTION("TxProfile and ChargePointMaxProfile with mixed units") { + + float power = -1.f; + float current = -1.f; + setSmartChargingOutput([&power, ¤t] (float limit_power, float limit_current, int) { + power = limit_power; + current = limit_current; + }); + + loop(); + + loopback.sendTXT(SCPROFILE_9_VIA_RMTSTARTTX_20A, strlen(SCPROFILE_9_VIA_RMTSTARTTX_20A)); + + loop(); + + loopback.sendTXT(SCPROFILE_10_ABSOLUTE_LIMIT_5KW, strlen(SCPROFILE_10_ABSOLUTE_LIMIT_5KW)); + + loop(); + + REQUIRE((power > 4999.f && power < 5001.f)); //ChargePointMaxProfile defines power + REQUIRE((current > 19.99f && current < 20.01f)); //TxProfile defines current + + endTransaction(); + + loop(); + } + + SECTION("Get composite schedule") { + + loopback.sendTXT(SCPROFILE_6_MULTIPLE_PERIODS_16A_20A, strlen(SCPROFILE_6_MULTIPLE_PERIODS_16A_20A)); + + bool checkProcessed = false; + + auto getCompositeSchedule = makeRequest(new Ocpp16::CustomOperation( + "GetCompositeSchedule", + [] () { + //create req + auto doc = makeJsonDoc("UnitTests", JSON_OBJECT_SIZE(3)); + auto payload = doc->to(); + payload["connectorId"] = 1; + payload["duration"] = 86400; + payload["chargingRateUnit"] = "A"; + return doc;}, + [&checkProcessed, &model] (JsonObject payload) { + //receive conf + checkProcessed = true; + + REQUIRE(!strcmp(payload["status"], "Accepted")); + REQUIRE(payload["connectorId"] == 1); + + char checkScheduleStart [JSONDATE_LENGTH + 1]; + model.getClock().now().toJsonString(checkScheduleStart, JSONDATE_LENGTH + 1); + REQUIRE(!strcmp(payload["scheduleStart"], checkScheduleStart)); + + JsonObject chargingScheduleJson = payload["chargingSchedule"]; + ChargingSchedule schedule; + bool success = loadChargingSchedule(chargingScheduleJson, schedule); + + REQUIRE(success); + REQUIRE(schedule.chargingSchedulePeriod.size() == 2); + + REQUIRE((schedule.chargingSchedulePeriod[0].limit > 15.99f && + schedule.chargingSchedulePeriod[0].limit < 16.01f)); + REQUIRE(schedule.chargingSchedulePeriod[0].startPeriod == 0); + + REQUIRE((schedule.chargingSchedulePeriod[1].limit > 19.99f && + schedule.chargingSchedulePeriod[1].limit < 20.01f)); + REQUIRE(schedule.chargingSchedulePeriod[1].startPeriod == 3600); + } + )); + + context->initiateRequest(std::move(getCompositeSchedule)); + + loop(); + + REQUIRE(checkProcessed); + } + + SECTION("Get composite schedule with definition gap") { + + loopback.sendTXT(SCPROFILE_7_RECURRING_DAY_2H_16A_20A, strlen(SCPROFILE_7_RECURRING_DAY_2H_16A_20A)); + + auto schedule = scService->getCompositeSchedule(1, 86401); + + REQUIRE(schedule != nullptr); + REQUIRE(schedule->duration == 86401); + REQUIRE(schedule->chargingSchedulePeriod.size() == 4); + + REQUIRE((schedule->chargingSchedulePeriod[0].limit > 15.99f && + schedule->chargingSchedulePeriod[0].limit < 16.01f)); + REQUIRE(schedule->chargingSchedulePeriod[0].startPeriod == 0); + + REQUIRE((schedule->chargingSchedulePeriod[1].limit > 19.99f && + schedule->chargingSchedulePeriod[1].limit < 20.01f)); + REQUIRE(schedule->chargingSchedulePeriod[1].startPeriod == 3600); + + REQUIRE(schedule->chargingSchedulePeriod[2].limit < 0.f); //undefined during this period + REQUIRE(schedule->chargingSchedulePeriod[2].startPeriod == 2 * 3600); + + REQUIRE((schedule->chargingSchedulePeriod[3].limit > 15.99f && + schedule->chargingSchedulePeriod[3].limit < 16.01f)); + REQUIRE(schedule->chargingSchedulePeriod[3].startPeriod == 86400); + } + + SECTION("SmartCharging memory limits - MaxChargingProfilesInstalled") { + + loop(); + + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_0_ALT_SAME_ID); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + (*doc)["connectorId"] = 2; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_1_ABSOLUTE_LIMIT_16A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + // 3 distinct ChargingProfiles installed. Check if further Profiles are rejected correctly + + for (size_t i = 0; i < 2; i++) { + // replace existing profile - OK + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_1_ABSOLUTE_LIMIT_16A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + } + + for (size_t i = 0; i < 2; i++) { + // try to install additional profile - not okay + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_5_VALID_UNTIL_2022_16A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + } + } + + SECTION("SmartCharging memory limits - ChargeProfileMaxStackLevel") { + + loop(); + + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + (*doc)["csChargingProfiles"]["stackLevel"] = MO_ChargeProfileMaxStackLevel; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + (*doc)["csChargingProfiles"]["stackLevel"] = MO_ChargeProfileMaxStackLevel + 1; + return doc;}, + [] (JsonObject) { }, //ignore conf + [&checkProcessed] (const char*, const char*, JsonObject) { + // process error + checkProcessed = true; + return true; + } + ))); + loop(); + REQUIRE( checkProcessed ); + } + + SECTION("SmartCharging memory limits - ChargingScheduleMaxPeriods") { + + loop(); + + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + JsonArray chargingSchedulePeriod = (*doc)["csChargingProfiles"]["chargingSchedule"]["chargingSchedulePeriod"]; + chargingSchedulePeriod.clear(); + for (size_t i = 0; i < MO_ChargingScheduleMaxPeriods; i++) { + auto period = chargingSchedulePeriod.createNestedObject(); + period["startPeriod"] = i; + period["limit"] = 16; + } + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_2_RELATIVE_TXDEF_24A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + JsonArray chargingSchedulePeriod = (*doc)["csChargingProfiles"]["chargingSchedule"]["chargingSchedulePeriod"]; + chargingSchedulePeriod.clear(); + for (size_t i = 0; i < MO_ChargingScheduleMaxPeriods + 1; i++) { + auto period = chargingSchedulePeriod.createNestedObject(); + period["startPeriod"] = i; + period["limit"] = 16; + } + return doc;}, + [] (JsonObject) { }, //ignore conf + [&checkProcessed] (const char*, const char*, JsonObject) { + // process error + checkProcessed = true; + return true; + } + ))); + loop(); + REQUIRE( checkProcessed ); + } + + SECTION("ChargingScheduleAllowedChargingRateUnit") { + + setSmartChargingOutput(nullptr); + loop(); + + // accept power, reject current + setSmartChargingPowerOutput([] (float) { }); + + bool checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_0); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_1_ABSOLUTE_LIMIT_16A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + // reject power, accept current + setSmartChargingPowerOutput(nullptr); + setSmartChargingCurrentOutput([] (float) { }); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_0); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + + checkProcessed = false; + getOcppContext()->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "SetChargingProfile", + [] () { + //create req + StaticJsonDocument<2048> raw; + deserializeJson(raw, SCPROFILE_1_ABSOLUTE_LIMIT_16A); + auto doc = makeJsonDoc("UnitTests", 2048); + *doc = raw[3]; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Accepted") ); + } + ))); + loop(); + REQUIRE( checkProcessed ); + } + + scService->clearChargingProfile([] (int, int, ChargingProfilePurposeType, int) { + return true; + }); + + mocpp_deinitialize(); + +} diff --git a/tests/TransactionProcess.cpp b/tests/TransactionProcess.cpp deleted file mode 100644 index 0f0726f4..00000000 --- a/tests/TransactionProcess.cpp +++ /dev/null @@ -1,185 +0,0 @@ -#include -#include "./catch2/catch.hpp" -#include "./helpers/testHelper.h" - -#include - -using namespace ArduinoOcpp; - -TEST_CASE( "Transaction process - trivial" ) { - - TransactionProcess txProcess {1}; - - SECTION("Trivial transaction process"){ - txProcess.evaluateProcessSteps(); - REQUIRE( !txProcess.existsActiveTrigger() ); - REQUIRE( txProcess.getState() == TxEnableState::Inactive ); - } - - SECTION("Trivial transaction process with outputs"){ - bool notified = false; - txProcess.addEnableStep([¬ified] (TxTrigger trigger) -> TxEnableState { - REQUIRE( trigger == TxTrigger::Inactive ); - notified = true; - return TxEnableState::Inactive; - }); - txProcess.evaluateProcessSteps(); - REQUIRE( notified ); - REQUIRE( txProcess.getState() == TxEnableState::Inactive ); - } -} - -TEST_CASE("Transaction process - inputs") { - - TransactionProcess txProcess {1}; - - TxPrecondition precondition = TxPrecondition::Active; - txProcess.addPrecondition([&precondition] () { - return precondition; - }); - - TxTrigger trigger = TxTrigger::Active; - txProcess.addTrigger([&trigger] () { - return trigger; - }); - - TxEnableState enable = TxEnableState::Active; - TxTrigger checkTrigger = TxTrigger::Active; - txProcess.addEnableStep([&enable, &checkTrigger] (TxTrigger input) { - REQUIRE(checkTrigger == input); - return enable; - }); - - SECTION("All active") { - txProcess.evaluateProcessSteps(); - REQUIRE(txProcess.getState() == TxEnableState::Active); - } - - SECTION("Precondition") { - precondition = TxPrecondition::Inactive; - checkTrigger = TxTrigger::Inactive; - txProcess.evaluateProcessSteps(); - REQUIRE( txProcess.getState() != TxEnableState::Active ); - } - - SECTION("Trigger") { - trigger = TxTrigger::Inactive; - checkTrigger = TxTrigger::Inactive; - txProcess.evaluateProcessSteps(); - REQUIRE( txProcess.getState() != TxEnableState::Active ); - } - - SECTION("Output enable") { - enable = TxEnableState::Inactive; - txProcess.evaluateProcessSteps(); - REQUIRE( txProcess.getState() == TxEnableState::Pending ); - - enable = TxEnableState::Pending; - txProcess.evaluateProcessSteps(); - REQUIRE( txProcess.getState() == TxEnableState::Pending ); - } -} - -TEST_CASE("Transaction process - execution order") { - - TransactionProcess txProcess {1}; - - std::array preconditions {TxPrecondition::Active, TxPrecondition::Active}; - for (unsigned int i = 0; i < preconditions.size(); i++) { - txProcess.addPrecondition([&preconditions, i] () { - return preconditions[i]; - }); - } - - std::array triggers {TxTrigger::Active, TxTrigger::Active}; - for (unsigned int i = 0; i < triggers.size(); i++) { - txProcess.addTrigger([&triggers, i] () { - return triggers[i]; - }); - } - - std::array enableSteps {TxEnableState::Active, TxEnableState::Active}; - std::array checkSeq {-1, -1}; - int checkCount = 0; - for (unsigned int i = 0; i < enableSteps.size(); i++) { - txProcess.addEnableStep([&enableSteps, &checkSeq, &checkCount, i] (TxTrigger) { - checkSeq[i] = checkCount; - checkCount++; - return enableSteps[i]; - }); - } - - SECTION("Precondition"){ - preconditions[0] = TxPrecondition::Inactive; - txProcess.evaluateProcessSteps(); - REQUIRE( txProcess.getState() != TxEnableState::Active ); - - preconditions[0] = TxPrecondition::Active; - preconditions[1] = TxPrecondition::Inactive; - txProcess.evaluateProcessSteps(); - REQUIRE( txProcess.getState() != TxEnableState::Active ); - } - - SECTION("Trigger") { - triggers[0] = TxTrigger::Inactive; - txProcess.evaluateProcessSteps(); - REQUIRE( txProcess.getState() != TxEnableState::Active ); - REQUIRE( txProcess.existsActiveTrigger()); - - triggers[0] = TxTrigger::Active; - triggers[1] = TxTrigger::Inactive; - txProcess.evaluateProcessSteps(); - REQUIRE( txProcess.getState() != TxEnableState::Active ); - REQUIRE( txProcess.existsActiveTrigger()); - } - - SECTION("Output enable - all active"){ - txProcess.evaluateProcessSteps(); - REQUIRE(checkSeq[0] == 1); - REQUIRE(checkSeq[1] == 0); - } - - SECTION("Output enable - active pending"){ - enableSteps[1] = TxEnableState::Pending; - txProcess.evaluateProcessSteps(); - REQUIRE(checkSeq[0] == -1); - REQUIRE(checkSeq[1] == 0); - } - - SECTION("Output enable - inactive"){ - preconditions[0] = TxPrecondition::Inactive; - enableSteps[0] = TxEnableState::Inactive; - txProcess.evaluateProcessSteps(); - REQUIRE(checkSeq[0] == 0); - REQUIRE(checkSeq[1] == 1); - } - - SECTION("Output enable - inactive pending"){ - preconditions[0] = TxPrecondition::Inactive; - enableSteps[0] = TxEnableState::Pending; - txProcess.evaluateProcessSteps(); - REQUIRE(checkSeq[0] == 0); - REQUIRE(checkSeq[1] == -1); - } -} - -TEST_CASE("Transaction process - begin new transaction") { - TransactionProcess txProcess {1}; - - txProcess.addPrecondition([] () { - return TxPrecondition::Active; - }); - - TxTrigger trigger = TxTrigger::Active; - txProcess.addTrigger([&trigger] () { - return trigger; - }); - - TxEnableState enable = TxEnableState::Active; - TxTrigger checkTrigger = TxTrigger::Active; - txProcess.addEnableStep([&enable, &checkTrigger] (TxTrigger input) { - checkTrigger = input; - return enable; - }); - -} diff --git a/tests/TransactionSafety.cpp b/tests/TransactionSafety.cpp index 85913f2e..f3593e01 100644 --- a/tests/TransactionSafety.cpp +++ b/tests/TransactionSafety.cpp @@ -1,111 +1,99 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "./catch2/catch.hpp" +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "./helpers/testHelper.h" -using namespace ArduinoOcpp; +using namespace MicroOcpp; TEST_CASE( "Transaction safety" ) { + printf("\nRun %s\n", "Transaction safety"); - //initialize OcppEngine with dummy socket - OcppEchoSocket echoSocket; - OCPP_initialize(echoSocket); + //initialize Context with dummy socket + LoopbackConnection loopback; + mocpp_initialize(loopback); - ao_set_timer(custom_timer_cb); + mocpp_set_timer(custom_timer_cb); - auto connectionTimeOut = declareConfiguration("ConnectionTimeOut", 30, CONFIGURATION_FN); - *connectionTimeOut = 30; - - bootNotification("dummy1234", ""); + declareConfiguration("ConnectionTimeOut", 30)->setInt(30); SECTION("Basic transaction") { - AO_DBG_DEBUG("Basic transaction"); - OCPP_loop(); - OCPP_loop(); - OCPP_loop(); + MO_DBG_DEBUG("Basic transaction"); + loop(); startTransaction("mIdTag"); - OCPP_loop(); + loop(); REQUIRE(ocppPermitsCharge()); stopTransaction(); - OCPP_loop(); + loop(); REQUIRE(!ocppPermitsCharge()); - OCPP_deinitialize(); + mocpp_deinitialize(); } SECTION("Managed transaction") { - AO_DBG_DEBUG("Managed transaction"); - OCPP_loop(); - OCPP_loop(); - OCPP_loop(); + MO_DBG_DEBUG("Managed transaction"); + loop(); setConnectorPluggedInput([] () {return true;}); beginTransaction("mIdTag"); - OCPP_loop(); + loop(); REQUIRE(ocppPermitsCharge()); endTransaction(); - OCPP_loop(); + loop(); REQUIRE(!ocppPermitsCharge()); - OCPP_deinitialize(); + mocpp_deinitialize(); } SECTION("Reset during transaction 01 - interrupt initiation") { - AO_DBG_DEBUG("Reset during transaction 01 - interrupt initiation"); + MO_DBG_DEBUG("Reset during transaction 01 - interrupt initiation"); setConnectorPluggedInput([] () {return false;}); - OCPP_loop(); - OCPP_loop(); - OCPP_loop(); + loop(); beginTransaction("mIdTag"); - OCPP_deinitialize(); //reset and jump to next section + loop(); + mocpp_deinitialize(); //reset and jump to next section } SECTION("Reset during transaction 02 - interrupt initiation second time") { - AO_DBG_DEBUG("Reset during transaction 02 - interrupt initiation second time"); + MO_DBG_DEBUG("Reset during transaction 02 - interrupt initiation second time"); setConnectorPluggedInput([] () {return false;}); - OCPP_loop(); - OCPP_loop(); - OCPP_loop(); + loop(); REQUIRE(!ocppPermitsCharge()); - OCPP_deinitialize(); + mocpp_deinitialize(); } SECTION("Reset during transaction 03 - interrupt running tx") { - AO_DBG_DEBUG("Reset during transaction 03 - interrupt running tx"); + MO_DBG_DEBUG("Reset during transaction 03 - interrupt running tx"); setConnectorPluggedInput([] () {return true;}); - OCPP_loop(); - OCPP_loop(); - OCPP_loop(); + loop(); REQUIRE(ocppPermitsCharge()); - OCPP_deinitialize(); + mocpp_deinitialize(); } SECTION("Reset during transaction 04 - interrupt stopping tx") { - AO_DBG_DEBUG("Reset during transaction 04 - interrupt stopping tx"); + MO_DBG_DEBUG("Reset during transaction 04 - interrupt stopping tx"); setConnectorPluggedInput([] () {return true;}); - OCPP_loop(); - OCPP_loop(); - OCPP_loop(); + loop(); REQUIRE(ocppPermitsCharge()); endTransaction(); - OCPP_deinitialize(); + mocpp_deinitialize(); } SECTION("Reset during transaction 06 - check tx finished") { - AO_DBG_DEBUG("Reset during transaction 06 - check tx finished"); + MO_DBG_DEBUG("Reset during transaction 06 - check tx finished"); setConnectorPluggedInput([] () {return true;}); - OCPP_loop(); - OCPP_loop(); - OCPP_loop(); + loop(); REQUIRE(!ocppPermitsCharge()); - OCPP_deinitialize(); + mocpp_deinitialize(); } } 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 2ac00670..ffc6fac5 100644 --- a/tests/helpers/testHelper.cpp +++ b/tests/helpers/testHelper.cpp @@ -1,11 +1,14 @@ -#include -#include +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License -using namespace ArduinoOcpp; +#include +#include -void cpp_console_out(const char *msg) { - std::cout << msg; -} +#include +#include + +using namespace MicroOcpp; unsigned long mtime = 10000; unsigned long custom_timer_cb() { @@ -13,10 +16,20 @@ unsigned long custom_timer_cb() { } void loop() { - mtime += 1000; - OCPP_loop(); - mtime += 1000; - OCPP_loop(); - mtime += 1000; - OCPP_loop(); + for (int i = 0; i < 30; i++) { + mtime += 100; + 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 441f298d..b3de2a18 100644 --- a/tests/ocppEngineLifecycle.cpp +++ b/tests/ocppEngineLifecycle.cpp @@ -1,24 +1,26 @@ -#include -#include -#include "./catch2/catch.hpp" -#include "./helpers/testHelper.h" +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License -TEST_CASE( "OcppEngine lifecycle" ) { +#include +#include +#include +#include "./helpers/testHelper.h" - //set console output to the cpp console to display outputs - //ao_set_console_out(cpp_console_out); +TEST_CASE( "Context lifecycle" ) { + printf("\nRun %s\n", "Context lifecycle"); - //initialize OcppEngine with dummy socket - ArduinoOcpp::OcppEchoSocket echoSocket; - OCPP_initialize(echoSocket); + //initialize Context with dummy socket + MicroOcpp::LoopbackConnection loopback; + mocpp_initialize(loopback); SECTION("OCPP is initialized"){ - REQUIRE( getOcppEngine() ); + REQUIRE( getOcppContext() ); } - OCPP_deinitialize(); + mocpp_deinitialize(); SECTION("OCPP is deinitialized"){ - REQUIRE( !( getOcppEngine() ) ); + REQUIRE( !( getOcppContext() ) ); } }