diff --git a/CHANGELOG.md b/CHANGELOG.md index e464fd1b..682e69bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Config `Cst_TxStartOnPowerPathClosed` to put back TxStartPoint - Function `bool isConnected()` in `Connection` interface ([#282](https://github.com/matth-x/MicroOcpp/pull/282)) - Build flags for customizing memory limits of SmartCharging +- SConscript ([#287](https://github.com/matth-x/MicroOcpp/pull/287)) - Operation GetInstalledCertificateIds, UC M03 ([#262](https://github.com/matth-x/MicroOcpp/pull/262)) - Operation DeleteCertificate, UC M04 ([#262](https://github.com/matth-x/MicroOcpp/pull/262)) - Operation InstallCertificate, UC M05 ([#262](https://github.com/matth-x/MicroOcpp/pull/262)) @@ -33,7 +34,7 @@ - Ignore UnlockConnector when handler not set - Reject ChargingProfile if unit not supported - Fix building with debug level warn and error -- Fix transaction freeze in offline mode ([#279](https://github.com/matth-x/MicroOcpp/pull/279)) +- 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 diff --git a/SConscript.py b/SConscript.py new file mode 100644 index 00000000..5bccda82 --- /dev/null +++ b/SConscript.py @@ -0,0 +1,50 @@ +# 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", "ARDUINOJSON_DIR") + +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() + +source_dirs = getAllDirs(SOURCE_DIR.Dir("src")) +source_dirs += getAllDirs(ARDUINOJSON_DIR.Dir("src")) + +source_files = [] + +for folder in source_dirs: + source_files += folder.glob("*.cpp") + env["CPPPATH"].append(folder) + +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_dirs.copy() +} + +Return("exports") diff --git a/src/MicroOcpp/Core/RequestQueueStorageStrategy.cpp b/src/MicroOcpp/Core/RequestQueueStorageStrategy.cpp index 507f4a7f..a9b61422 100644 --- a/src/MicroOcpp/Core/RequestQueueStorageStrategy.cpp +++ b/src/MicroOcpp/Core/RequestQueueStorageStrategy.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -104,12 +104,12 @@ Request *PersistentRequestQueue::front() { tailCache.pop_front(); } else { //cache miss -> case B) or A) -> try to fetch operation from flash (check for case B)) or take first cached element as front - auto storageHandler = opStore.makeOpHandler(); std::unique_ptr fetched; unsigned int range = (opStore.getOpEnd() + MO_MAX_OPNR - nextOpNr) % MO_MAX_OPNR; for (size_t i = 0; i < range; i++) { + auto storageHandler = opStore.makeOpHandler(); bool exists = storageHandler->restore(nextOpNr); if (exists) { //case B) -> load operation from flash and take it as front element diff --git a/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp index 99202320..90a86e46 100644 --- a/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp +++ b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp @@ -48,7 +48,7 @@ void DiagnosticsService::loop() { if (uploadIssued) { if (uploadStatusInput != nullptr && uploadStatusInput() == UploadStatus::Uploaded) { //success! - MO_DBG_DEBUG("end upload routine (by status)") + MO_DBG_DEBUG("end upload routine (by status)"); uploadIssued = false; retries = 0; } diff --git a/src/MicroOcpp/Model/Transactions/Transaction.cpp b/src/MicroOcpp/Model/Transactions/Transaction.cpp index 9c6535fa..a7846265 100644 --- a/src/MicroOcpp/Model/Transactions/Transaction.cpp +++ b/src/MicroOcpp/Model/Transactions/Transaction.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -73,6 +73,10 @@ int32_t ocpp_tx_getMeterStop(OCPP_Transaction *tx) { return reinterpret_cast(tx)->getMeterStop(); } +void ocpp_tx_setMeterStop(OCPP_Transaction* tx, int32_t meter) { + return reinterpret_cast(tx)->setMeterStop(meter); +} + bool ocpp_tx_getStopTimestamp(OCPP_Transaction *tx, char *buf, size_t len) { return reinterpret_cast(tx)->getStopTimestamp().toJsonString(buf, len); } diff --git a/src/MicroOcpp/Model/Transactions/Transaction.h b/src/MicroOcpp/Model/Transactions/Transaction.h index f02b2c09..788655c2 100644 --- a/src/MicroOcpp/Model/Transactions/Transaction.h +++ b/src/MicroOcpp/Model/Transactions/Transaction.h @@ -347,6 +347,7 @@ bool ocpp_tx_getStartTimestamp(OCPP_Transaction *tx, char *buf, size_t len); const char *ocpp_tx_getStopIdTag(OCPP_Transaction *tx); int32_t ocpp_tx_getMeterStop(OCPP_Transaction *tx); +void ocpp_tx_setMeterStop(OCPP_Transaction* tx, int32_t meter); bool ocpp_tx_getStopTimestamp(OCPP_Transaction *tx, char *buf, size_t len); diff --git a/src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp b/src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp index b1262ff5..895c1634 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp +++ b/src/MicroOcpp/Model/Transactions/TransactionDeserialize.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -234,7 +234,7 @@ bool deserializeTransaction(Transaction& tx, JsonObject state) { tx.setSilent(); } - MO_DBG_DEBUG("DUMP TX"); + MO_DBG_DEBUG("DUMP TX (%s)", tx.getIdTag() ? tx.getIdTag() : "idTag missing"); MO_DBG_DEBUG("Session | idTag %s, active: %i, authorized: %i, deauthorized: %i", tx.getIdTag(), tx.isActive(), tx.isAuthorized(), tx.isIdTagDeauthorized()); MO_DBG_DEBUG("Start RPC | req: %i, conf: %i", tx.getStartSync().isRequested(), tx.getStartSync().isConfirmed()); MO_DBG_DEBUG("Stop RPC | req: %i, conf: %i", tx.getStopSync().isRequested(), tx.getStopSync().isConfirmed()); diff --git a/src/MicroOcpp/Operations/GetConfiguration.cpp b/src/MicroOcpp/Operations/GetConfiguration.cpp index a8b3804c..1dbaabb2 100644 --- a/src/MicroOcpp/Operations/GetConfiguration.cpp +++ b/src/MicroOcpp/Operations/GetConfiguration.cpp @@ -1,5 +1,5 @@ // matth-x/MicroOcpp -// Copyright Matthias Akstaller 2019 - 2023 +// Copyright Matthias Akstaller 2019 - 2024 // MIT License #include @@ -135,7 +135,7 @@ std::unique_ptr GetConfiguration::createConf(){ if (!unknownKeys.empty()) { JsonArray jsonUnknownKey = payload.createNestedArray("unknownKey"); for (auto key : unknownKeys) { - MO_DBG_DEBUG("Unknown key: %s", key) + MO_DBG_DEBUG("Unknown key: %s", key); jsonUnknownKey.add(key); } } diff --git a/tests/ChargingSessions.cpp b/tests/ChargingSessions.cpp index 166b18e1..e6cc8bf5 100644 --- a/tests/ChargingSessions.cpp +++ b/tests/ChargingSessions.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include "./catch2/catch.hpp" #include "./helpers/testHelper.h" @@ -343,6 +345,279 @@ TEST_CASE( "Charging sessions" ) { 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 = beginTransaction_authorized("mIdTag"); + REQUIRE( !tx ); + + 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 = 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"; + 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 = beginTransaction_authorized("mIdTag"); + REQUIRE( tx ); + + 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 = 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"; + txId_generate++; + payload["transactionId"] = txId_generate; + return doc; + });}); + + getOcppContext()->getOperationRegistry().registerOperation("StopTransaction", [&txId_generate, &txId_confirm] () { + return new Ocpp16::CustomOperation("StopTransaction", + [&txId_generate, &txId_confirm] (JsonObject payload) { + //receive req + REQUIRE( payload["transactionId"].as() == txId_generate ); + REQUIRE( payload["transactionId"].as() == txId_confirm + 1 ); + txId_confirm = payload["transactionId"].as(); + }, + [] () { + //create conf + return createEmptyDocument(); + });}); + + loopback.setConnected(true); + loop(); + + REQUIRE( txId_confirm == txId_base + MO_TXRECORD_SIZE ); + } + + SECTION("Preboot transactions - mix PreBoot with Offline tx") { + + /* + * The charger boots and connects to the OCPP server normally. It looses connection and then starts + * transaction #1 which is persisted on flash. Then a power loss occurs, but the charger doesn't reconnect. + * Start transaction #2 in PreBoot mode. Trigger another power loss, start transaction #3 while still + * being offline and then, after reconnection to the server, transaction #4. + * + * Tx #1 can be fully restored. The timestamp information for Tx #2 is missing, so it is discarded. Tx #3 is + * missing absolute timestamps at first, but after reconnection with the server, the timestamps get updated + * with absolute values from the server. Tx #4 is the standard case for transactions and should start normally. + */ + + // use idTags to identify the transactions + const char *tx1_idTag = "Tx#1"; + const char *tx2_idTag = "Tx#2"; + const char *tx3_idTag = "Tx#3"; + const char *tx4_idTag = "Tx#4"; + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true, CONFIGURATION_FN)->setBool(true); + configuration_save(); + loop(); + + // start Tx #1 (offline tx) + loopback.setConnected(false); + + MO_DBG_DEBUG("begin tx (%s)", tx1_idTag); + beginTransaction_authorized(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_authorized(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_authorized(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 = 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; + });}); + + // get online + loopback.setConnected(true); + loop(); + + // start Tx #4 + MO_DBG_DEBUG("begin tx (%s)", tx4_idTag); + beginTransaction_authorized(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");