diff --git a/CHANGELOG.md b/CHANGELOG.md index fd7a7961..eb5f732c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ - 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)) +- 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)) ## [1.1.0] - 2024-05-21 diff --git a/src/MicroOcpp/Core/Request.cpp b/src/MicroOcpp/Core/Request.cpp index 56477fa8..bf4db6b3 100644 --- a/src/MicroOcpp/Core/Request.cpp +++ b/src/MicroOcpp/Core/Request.cpp @@ -29,7 +29,7 @@ namespace MicroOcpp { using namespace MicroOcpp; -Request::Request(std::unique_ptr msg, const char *memory_tag) : MemoryManaged(memory_tag), messageID(makeString(getMemoryTag())), operation(std::move(msg)) { +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(); } @@ -289,19 +289,15 @@ bool Request::isRequestSent() { namespace MicroOcpp { -std::unique_ptr makeRequest(std::unique_ptr operation, const char *memoryTag){ +std::unique_ptr makeRequest(std::unique_ptr operation){ if (operation == nullptr) { return nullptr; } - return std::unique_ptr(new Request(std::move(operation), memoryTag)); + return std::unique_ptr(new Request(std::move(operation))); } std::unique_ptr makeRequest(Operation *operation) { return makeRequest(std::unique_ptr(operation)); } -std::unique_ptr makeRequest(const char *memoryTag, Operation *operation) { - return makeRequest(std::unique_ptr(operation), memoryTag); -} - } //end namespace MicroOcpp diff --git a/src/MicroOcpp/Core/Request.h b/src/MicroOcpp/Core/Request.h index 4b608217..62e42967 100644 --- a/src/MicroOcpp/Core/Request.h +++ b/src/MicroOcpp/Core/Request.h @@ -41,7 +41,7 @@ class Request : public MemoryManaged { bool requestSent = false; public: - Request(std::unique_ptr msg, const char *memory_tag = "Request"); + Request(std::unique_ptr msg); ~Request(); @@ -121,9 +121,8 @@ class Request : public MemoryManaged { /* * Simple factory functions */ -std::unique_ptr makeRequest(std::unique_ptr op, const char *memoryTag = "Request"); +std::unique_ptr makeRequest(std::unique_ptr op); std::unique_ptr makeRequest(Operation *op); //takes ownership of op -std::unique_ptr makeRequest(const char *memoryTag, Operation *op); //takes ownership of op } //end namespace MicroOcpp diff --git a/src/MicroOcpp/Core/RequestQueue.h b/src/MicroOcpp/Core/RequestQueue.h index 1375ad90..0cdfe7bc 100644 --- a/src/MicroOcpp/Core/RequestQueue.h +++ b/src/MicroOcpp/Core/RequestQueue.h @@ -18,7 +18,7 @@ #endif #ifndef MO_NUM_REQUEST_QUEUES -#define MO_NUM_REQUEST_QUEUES 5 +#define MO_NUM_REQUEST_QUEUES 10 #endif namespace MicroOcpp { diff --git a/src/MicroOcpp/Model/ConnectorBase/Connector.cpp b/src/MicroOcpp/Model/ConnectorBase/Connector.cpp index 04f04e9e..d1adc1ea 100644 --- a/src/MicroOcpp/Model/ConnectorBase/Connector.cpp +++ b/src/MicroOcpp/Model/ConnectorBase/Connector.cpp @@ -92,14 +92,14 @@ Connector::Connector(Context& context, std::shared_ptr filesy if (txNrPivot == std::numeric_limits::max()) { txNrPivot = parsedTxNr; txNrBegin = parsedTxNr; - txNrBack = (parsedTxNr + 1) % MAX_TX_CNT; + 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 > (txNrBack + MAX_TX_CNT - txNrPivot) % MAX_TX_CNT) { - txNrBack = (parsedTxNr + 1) % MAX_TX_CNT; + 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 @@ -108,16 +108,16 @@ Connector::Connector(Context& context, std::shared_ptr filesy } } - MO_DBG_DEBUG("found %s%u.jsn - Internal range from %u to %u (exclusive)", txFnamePrefix, parsedTxNr, txNrBegin, txNrBack); + 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)", (txNrBack + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT, connectorId, txNrBegin, txNrBack); + 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 = (txNrBack + MAX_TX_CNT - 1) % MAX_TX_CNT; //txNr of the most recent tx on flash + 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"); @@ -263,26 +263,26 @@ void Connector::loop() { } if (removed) { - if (txNrFront == txNrBack) { + if (txNrFront == txNrEnd) { txNrFront = transaction->getTxNr(); } - txNrBack = transaction->getTxNr(); //roll back creation of last tx entry + 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, txNrBack=%u", txNrBegin, txNrFront, txNrBack); + 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, txNrBack=%u", txNrBegin, txNrFront, txNrBack); + 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, txNrBack=%u", txNrBegin, txNrFront, txNrBack); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); transaction = nullptr; } @@ -550,8 +550,8 @@ std::shared_ptr Connector::allocateTransaction() { std::shared_ptr tx; //clean possible aborted tx - unsigned int txr = txNrBack; - unsigned int txSize = (txNrBack + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; + 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 @@ -567,10 +567,10 @@ std::shared_ptr Connector::allocateTransaction() { removed &= model.getTransactionStore()->remove(connectorId, txr); } if (removed) { - if (txNrFront == txNrBack) { + if (txNrFront == txNrEnd) { txNrFront = txr; } - txNrBack = txr; + txNrEnd = txr; MO_DBG_WARN("deleted dangling silent or aborted tx for new transaction"); } else { MO_DBG_ERR("memory corruption"); @@ -582,18 +582,18 @@ std::shared_ptr Connector::allocateTransaction() { } } - txSize = (txNrBack + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; //refresh after cleaning txs + 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, txNrBack); + tx = model.getTransactionStore()->createTransaction(connectorId, txNrEnd); } if (!tx) { //could not create transaction - now, try to replace tx history entry unsigned int txl = txNrBegin; - txSize = (txNrBack + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; + txSize = (txNrEnd + MAX_TX_CNT - txNrBegin) % MAX_TX_CNT; for (unsigned int i = 0; i < txSize; i++) { @@ -621,9 +621,9 @@ std::shared_ptr Connector::allocateTransaction() { txNrFront = txNrBegin; } MO_DBG_DEBUG("deleted tx history entry for new transaction"); - MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrBack=%u", txNrBegin, txNrFront, txNrBack); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); - tx = model.getTransactionStore()->createTransaction(connectorId, txNrBack); + tx = model.getTransactionStore()->createTransaction(connectorId, txNrEnd); } else { MO_DBG_ERR("memory corruption"); break; @@ -643,7 +643,7 @@ std::shared_ptr Connector::allocateTransaction() { //couldn't create normal transaction -> check if to start charging without real transaction if (silentOfflineTransactionsBool && silentOfflineTransactionsBool->getBool()) { //try to handle charging session without sending StartTx or StopTx to the server - tx = model.getTransactionStore()->createTransaction(connectorId, txNrBack, true); + tx = model.getTransactionStore()->createTransaction(connectorId, txNrEnd, true); if (tx) { MO_DBG_DEBUG("created silent transaction"); @@ -661,9 +661,9 @@ std::shared_ptr Connector::allocateTransaction() { } if (tx) { - txNrBack = (txNrBack + 1) % MAX_TX_CNT; - MO_DBG_DEBUG("advance txNrBack %u-%u", connectorId, txNrBack); - MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrBack=%u", txNrBegin, txNrFront, txNrBack); + 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; @@ -1083,12 +1083,12 @@ unsigned int Connector::getFrontRequestOpNr() { * Advance front transaction? */ - unsigned int txSize = (txNrBack + MAX_TX_CNT - txNrFront) % MAX_TX_CNT; + 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, txNrBack=%u", txNrBegin, txNrFront, txNrBack); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); transactionFront = nullptr; } @@ -1110,7 +1110,7 @@ unsigned int Connector::getFrontRequestOpNr() { 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, txNrBack=%u", txNrBegin, txNrFront, txNrBack); + MO_DBG_VERBOSE("txNrBegin=%u, txNrFront=%u, txNrEnd=%u", txNrBegin, txNrFront, txNrEnd); } else { //front is accurate. Done here break; @@ -1166,7 +1166,7 @@ std::unique_ptr Connector::fetchFrontRequest() { } Timestamp nextAttempt = transactionFront->getStartSync().getAttemptTime() + - transactionFront->getStartSync().getAttemptNr() * transactionMessageRetryIntervalInt->getInt(); + transactionFront->getStartSync().getAttemptNr() * std::max(0, transactionMessageRetryIntervalInt->getInt()); if (nextAttempt > model.getClock().now()) { return nullptr; @@ -1223,7 +1223,7 @@ std::unique_ptr Connector::fetchFrontRequest() { } Timestamp nextAttempt = transactionFront->getStopSync().getAttemptTime() + - transactionFront->getStopSync().getAttemptNr() * transactionMessageRetryIntervalInt->getInt(); + transactionFront->getStopSync().getAttemptNr() * std::max(0, transactionMessageRetryIntervalInt->getInt()); if (nextAttempt > model.getClock().now()) { return nullptr; @@ -1270,3 +1270,15 @@ std::unique_ptr Connector::fetchFrontRequest() { return nullptr; } + +unsigned int Connector::getTxNrBeginHistory() { + return txNrBegin; +} + +unsigned int Connector::getTxNrFront() { + return txNrFront; +} + +unsigned int Connector::getTxNrEnd() { + return txNrEnd; +} diff --git a/src/MicroOcpp/Model/ConnectorBase/Connector.h b/src/MicroOcpp/Model/ConnectorBase/Connector.h index 84d2722f..9f4be32a 100644 --- a/src/MicroOcpp/Model/ConnectorBase/Connector.h +++ b/src/MicroOcpp/Model/ConnectorBase/Connector.h @@ -95,7 +95,7 @@ class Connector : public RequestEmitter, public MemoryManaged { 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 txNrBack = 0; //one position behind newest transaction + unsigned int txNrEnd = 0; //one position behind newest transaction std::shared_ptr transactionFront; public: @@ -157,6 +157,10 @@ class Connector : public RequestEmitter, public MemoryManaged { unsigned int getFrontRequestOpNr() override; std::unique_ptr fetchFrontRequest() override; + + unsigned int getTxNrBeginHistory(); //if getTxNrBeginHistory() != getTxNrFront(), then return value is the txNr of the oldest tx history entry. If equal to getTxNrFront(), then the history is empty + unsigned int getTxNrFront(); //if getTxNrEnd() != getTxNrFront(), then return value is the txNr of the oldest transaction queued to be sent to the server. If equal to getTxNrEnd(), then there is no tx to be sent to the server + unsigned int getTxNrEnd(); //upper limit for the range of valid txNrs }; } //end namespace MicroOcpp diff --git a/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp index 6eb9a3e0..6257ad57 100644 --- a/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp +++ b/src/MicroOcpp/Model/Diagnostics/DiagnosticsService.cpp @@ -19,8 +19,9 @@ #include //for MO_ENABLE_V201 #include //for MO_ENABLE_CONNECTOR_LOCK -using namespace MicroOcpp; -using Ocpp16::DiagnosticsStatus; +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())) { diff --git a/src/MicroOcpp/Model/Metering/MeterValue.cpp b/src/MicroOcpp/Model/Metering/MeterValue.cpp index c00c4f34..dd52741d 100644 --- a/src/MicroOcpp/Model/Metering/MeterValue.cpp +++ b/src/MicroOcpp/Model/Metering/MeterValue.cpp @@ -2,6 +2,8 @@ // Copyright Matthias Akstaller 2019 - 2024 // MIT License +#include + #include #include #include @@ -67,6 +69,42 @@ ReadingContext MeterValue::getReadingContext() { return ReadingContext::NOT_SET; } +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"), diff --git a/src/MicroOcpp/Model/Metering/MeterValue.h b/src/MicroOcpp/Model/Metering/MeterValue.h index 74351943..0637ad33 100644 --- a/src/MicroOcpp/Model/Metering/MeterValue.h +++ b/src/MicroOcpp/Model/Metering/MeterValue.h @@ -18,6 +18,11 @@ 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; @@ -30,6 +35,18 @@ class MeterValue : public MemoryManaged { 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 { diff --git a/src/MicroOcpp/Model/Metering/MeteringConnector.cpp b/src/MicroOcpp/Model/Metering/MeteringConnector.cpp index 737a106e..ad3b6807 100644 --- a/src/MicroOcpp/Model/Metering/MeteringConnector.cpp +++ b/src/MicroOcpp/Model/Metering/MeteringConnector.cpp @@ -5,8 +5,11 @@ #include #include #include +#include #include +#include #include +#include #include #include #include @@ -17,12 +20,13 @@ using namespace MicroOcpp; using namespace MicroOcpp::Ocpp16; -MeteringConnector::MeteringConnector(Model& model, int connectorId, MeterStore& meterStore) - : MemoryManaged("v16.Metering.MeteringConnector"), model(model), connectorId{connectorId}, meterStore(meterStore), meterData(makeVector>(getMemoryTag())), samplers(makeVector>(getMemoryTag())) { +MeteringConnector::MeteringConnector(Context& context, int connectorId, MeterStore& meterStore) + : MemoryManaged("v16.Metering.MeteringConnector"), context(context), model(context.getModel()), connectorId{connectorId}, meterStore(meterStore), meterData(makeVector>(getMemoryTag())), samplers(makeVector>(getMemoryTag())) { + + context.getRequestQueue().addSendQueue(this); auto meterValuesSampledDataString = declareConfiguration("MeterValuesSampledData", ""); declareConfiguration("MeterValuesSampledDataMaxLength", 8, CONFIGURATION_VOLATILE, true); - meterValueCacheSizeInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "MeterValueCacheSize", 1); meterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval", 60); auto stopTxnSampledDataString = declareConfiguration("StopTxnSampledData", ""); @@ -37,13 +41,16 @@ MeteringConnector::MeteringConnector(Model& model, int connectorId, MeterStore& meterValuesInTxOnlyBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "MeterValuesInTxOnly", true); stopTxnDataCapturePeriodicBool = declareConfiguration(MO_CONFIG_EXT_PREFIX "StopTxnDataCapturePeriodic", false); + transactionMessageAttemptsInt = declareConfiguration("TransactionMessageAttempts", 3); + transactionMessageRetryIntervalInt = declareConfiguration("TransactionMessageRetryInterval", 60); + sampledDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, meterValuesSampledDataString)); alignedDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, meterValuesAlignedDataString)); stopTxnSampledDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, stopTxnSampledDataString)); stopTxnAlignedDataBuilder = std::unique_ptr(new MeterValueBuilder(samplers, stopTxnAlignedDataString)); } -std::unique_ptr MeteringConnector::loop() { +void MeteringConnector::loop() { bool txBreak = false; if (model.getConnector(connectorId)) { @@ -56,12 +63,6 @@ std::unique_ptr MeteringConnector::loop() { lastSampleTime = mocpp_tick_ms(); } - if ((txBreak || meterData.size() >= (size_t) meterValueCacheSizeInt->getInt()) && !meterData.empty()) { - auto meterValues = std::unique_ptr(new MeterValues(model, std::move(meterData), connectorId, transaction)); - meterData = makeVector>(getMemoryTag()); - return std::move(meterValues); //std::move is required for some compilers even if it's not mandated by standard C++ - } - if (model.getConnector(connectorId)) { if (transaction != model.getConnector(connectorId)->getTransaction()) { transaction = model.getConnector(connectorId)->getTransaction(); @@ -80,8 +81,7 @@ std::unique_ptr MeteringConnector::loop() { if (connectorId != 0 && meterValuesInTxOnlyBool->getBool()) { //don't take any MeterValues outside of transactions on connectorIds other than 0 - meterData.clear(); - return nullptr; + return; } } } @@ -97,9 +97,17 @@ std::unique_ptr MeteringConnector::loop() { abs(dt) <= 60 ? "in time (tolerance <= 60s)" : "off, e.g. because of first run. Ignore"); if (abs(dt) <= 60) { //is measurement still "clock-aligned"? - auto alignedMeterValues = alignedDataBuilder->takeSample(model.getClock().now(), ReadingContext::SampleClock); - if (alignedMeterValues) { - meterData.push_back(std::move(alignedMeterValues)); + + if (auto alignedMeterValue = alignedDataBuilder->takeSample(model.getClock().now(), ReadingContext::SampleClock)) { + if (meterData.size() >= MO_METERVALUES_CACHE_MAXSIZE) { + MO_DBG_INFO("MeterValue cache full. Drop old MV"); + meterData.erase(meterData.begin()); + } + alignedMeterValue->setOpNr(context.getRequestQueue().getNextOpNr()); + if (transaction) { + alignedMeterValue->setTxNr(transaction->getTxNr()); + } + meterData.push_back(std::move(alignedMeterValue)); } if (stopTxnData) { @@ -130,9 +138,16 @@ std::unique_ptr MeteringConnector::loop() { //record periodic tx data if (mocpp_tick_ms() - lastSampleTime >= (unsigned long) (meterValueSampleIntervalInt->getInt() * 1000)) { - auto sampleMeterValues = sampledDataBuilder->takeSample(model.getClock().now(), ReadingContext::SamplePeriodic); - if (sampleMeterValues) { - meterData.push_back(std::move(sampleMeterValues)); + if (auto sampledMeterValue = sampledDataBuilder->takeSample(model.getClock().now(), ReadingContext::SamplePeriodic)) { + if (meterData.size() >= MO_METERVALUES_CACHE_MAXSIZE) { + MO_DBG_INFO("MeterValue cache full. Drop old MV"); + meterData.erase(meterData.begin()); + } + sampledMeterValue->setOpNr(context.getRequestQueue().getNextOpNr()); + if (transaction) { + sampledMeterValue->setTxNr(transaction->getTxNr()); + } + meterData.push_back(std::move(sampledMeterValue)); } if (stopTxnData && stopTxnDataCapturePeriodicBool->getBool()) { @@ -144,12 +159,6 @@ std::unique_ptr MeteringConnector::loop() { lastSampleTime = mocpp_tick_ms(); } } - - if (clockAlignedDataIntervalInt->getInt() < 1 && meterValueSampleIntervalInt->getInt() < 1) { - meterData.clear(); - } - - return nullptr; //successful method completition. Currently there is no reason to send a MeterValues Msg. } std::unique_ptr MeteringConnector::takeTriggeredMeterValues() { @@ -160,15 +169,12 @@ std::unique_ptr MeteringConnector::takeTriggeredMeterValues() { return nullptr; } - decltype(meterData) mv_now; - mv_now.push_back(std::move(sample)); - std::shared_ptr transaction = nullptr; if (model.getConnector(connectorId)) { transaction = model.getConnector(connectorId)->getTransaction(); } - return std::unique_ptr(new MeterValues(model, std::move(mv_now), connectorId, transaction)); + return std::unique_ptr(new MeterValues(model, std::move(sample), connectorId, transaction)); } void MeteringConnector::addMeterValueSampler(std::unique_ptr meterValueSampler) { @@ -240,3 +246,57 @@ bool MeteringConnector::existsSampler(const char *measurand, size_t len) { return false; } + +unsigned int MeteringConnector::getFrontRequestOpNr() { + if (!meterDataFront && !meterData.empty()) { + MO_DBG_DEBUG("advance MV front"); + meterDataFront = std::move(meterData.front()); + meterData.erase(meterData.begin()); + } + if (meterDataFront) { + return meterDataFront->getOpNr(); + } + return NoOperation; +} + +std::unique_ptr MeteringConnector::fetchFrontRequest() { + + if (!meterDataFront) { + return nullptr; + } + + if ((int)meterDataFront->getAttemptNr() >= transactionMessageAttemptsInt->getInt()) { + MO_DBG_WARN("exceeded TransactionMessageAttempts. Discard MeterValue"); + meterDataFront.reset(); + return nullptr; + } + + if (mocpp_tick_ms() - meterDataFront->getAttemptTime() < meterDataFront->getAttemptNr() * (unsigned long)(std::max(0, transactionMessageRetryIntervalInt->getInt())) * 1000UL) { + return nullptr; + } + + meterDataFront->advanceAttemptNr(); + meterDataFront->setAttemptTime(mocpp_tick_ms()); + + //fetch tx for meterValue + std::shared_ptr tx; + if (meterDataFront->getTxNr() >= 0) { + tx = model.getTransactionStore()->getTransaction(connectorId, meterDataFront->getTxNr()); + } + + //discard MV if it belongs to silent tx + if (tx && tx->isSilent()) { + MO_DBG_DEBUG("Drop MeterValue belonging to silent tx"); + meterDataFront.reset(); + return nullptr; + } + + auto meterValues = makeRequest(new MeterValues(model, meterDataFront.get(), connectorId, tx)); + meterValues->setOnReceiveConfListener([this] (JsonObject) { + //operation success + MO_DBG_DEBUG("drop MV front"); + meterDataFront.reset(); + }); + + return meterValues; +} diff --git a/src/MicroOcpp/Model/Metering/MeteringConnector.h b/src/MicroOcpp/Model/Metering/MeteringConnector.h index 881c7aa1..75873ff6 100644 --- a/src/MicroOcpp/Model/Metering/MeteringConnector.h +++ b/src/MicroOcpp/Model/Metering/MeteringConnector.h @@ -12,22 +12,29 @@ #include #include #include +#include #include +#ifndef MO_METERVALUES_CACHE_MAXSIZE +#define MO_METERVALUES_CACHE_MAXSIZE MO_REQUEST_CACHE_MAXSIZE +#endif + namespace MicroOcpp { +class Context; class Model; class Operation; -class Transaction; class MeterStore; -class MeteringConnector : public MemoryManaged { +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; @@ -49,16 +56,18 @@ class MeteringConnector : public MemoryManaged { int energySamplerIndex {-1}; std::shared_ptr meterValueSampleIntervalInt; - std::shared_ptr meterValueCacheSizeInt; std::shared_ptr clockAlignedDataIntervalInt; std::shared_ptr meterValuesInTxOnlyBool; std::shared_ptr stopTxnDataCapturePeriodicBool; + + std::shared_ptr transactionMessageAttemptsInt; + std::shared_ptr transactionMessageRetryIntervalInt; public: - MeteringConnector(Model& model, int connectorId, MeterStore& meterStore); + MeteringConnector(Context& context, int connectorId, MeterStore& meterStore); - std::unique_ptr loop(); + void loop(); void addMeterValueSampler(std::unique_ptr meterValueSampler); @@ -76,6 +85,10 @@ class MeteringConnector : public MemoryManaged { bool existsSampler(const char *measurand, size_t len); + //RequestEmitter implementation + unsigned int getFrontRequestOpNr() override; + std::unique_ptr fetchFrontRequest() override; + }; } //end namespace MicroOcpp diff --git a/src/MicroOcpp/Model/Metering/MeteringService.cpp b/src/MicroOcpp/Model/Metering/MeteringService.cpp index a404ab48..287f026a 100644 --- a/src/MicroOcpp/Model/Metering/MeteringService.cpp +++ b/src/MicroOcpp/Model/Metering/MeteringService.cpp @@ -22,8 +22,9 @@ MeteringService::MeteringService(Context& context, int numConn, std::shared_ptr< declareConfiguration("MeterValuesAlignedData", "Energy.Active.Import.Register,Power.Active.Import"); declareConfiguration("StopTxnAlignedData", ""); + connectors.reserve(numConn); for (int i = 0; i < numConn; i++) { - connectors.emplace_back(new MeteringConnector(context.getModel(), i, meterStore)); + connectors.emplace_back(new MeteringConnector(context, i, meterStore)); } std::function validateSelectString = [this] (const char *csl) { @@ -83,14 +84,8 @@ MeteringService::MeteringService(Context& context, int numConn, std::shared_ptr< } void MeteringService::loop(){ - for (unsigned int i = 0; i < connectors.size(); i++){ - auto meterValuesMsg = connectors[i]->loop(); - if (meterValuesMsg != nullptr) { - auto meterValues = makeRequest(std::move(meterValuesMsg)); - meterValues->setTimeout(120000); - context.initiateRequest(std::move(meterValues)); - } + connectors[i]->loop(); } } diff --git a/src/MicroOcpp/Operations/MeterValues.cpp b/src/MicroOcpp/Operations/MeterValues.cpp index f6ab2d7c..ee2b4c7e 100644 --- a/src/MicroOcpp/Operations/MeterValues.cpp +++ b/src/MicroOcpp/Operations/MeterValues.cpp @@ -11,18 +11,21 @@ using MicroOcpp::Ocpp16::MeterValues; using MicroOcpp::JsonDoc; -#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(Model& model) : MemoryManaged("v16.Operation.", "MeterValues"), model(model) { } -MeterValues::MeterValues(Model& model, Vector>&& meterValue, unsigned int connectorId, std::shared_ptr transaction) - : MemoryManaged("v16.Operation.", "MeterValues"), model(model), meterValue{std::move(meterValue)}, connectorId{connectorId}, transaction{transaction} { +MeterValues::MeterValues(Model& model, MeterValue *meterValue, unsigned int connectorId, std::shared_ptr transaction) + : MemoryManaged("v16.Operation.", "MeterValues"), model(model), meterValue{meterValue}, connectorId{connectorId}, transaction{transaction} { } +MeterValues::MeterValues(Model& model, std::unique_ptr meterValue, unsigned int connectorId, std::shared_ptr transaction) + : MeterValues(model, meterValue.get(), connectorId, transaction) { + this->meterValueOwnership = std::move(meterValue); +} + MeterValues::~MeterValues(){ } @@ -34,27 +37,27 @@ const char* MeterValues::getOperationType(){ std::unique_ptr MeterValues::createReq() { size_t capacity = 0; - - auto entries = makeVector>(getMemoryTag()); - for (auto mv = meterValue.begin(); mv != meterValue.end(); mv++) { - if ((*mv)->getTimestamp() < MIN_TIME) { + std::unique_ptr meterValueJson; + + if (meterValue) { + + if (meterValue->getTimestamp() < MIN_TIME) { MO_DBG_DEBUG("adjust preboot MeterValue timestamp"); - Timestamp adjusted = model.getClock().adjustPrebootTimestamp((*mv)->getTimestamp()); - (*mv)->setTimestamp(adjusted); + Timestamp adjusted = model.getClock().adjustPrebootTimestamp(meterValue->getTimestamp()); + meterValue->setTimestamp(adjusted); } - auto entry = (*mv)->toJson(); - if (entry) { - capacity += entry->capacity(); - entries.push_back(std::move(entry)); + 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(entries.size()); + capacity += JSON_ARRAY_SIZE(1); auto doc = makeJsonDoc(getMemoryTag(), capacity); auto payload = doc->to(); @@ -64,9 +67,9 @@ std::unique_ptr MeterValues::createReq() { payload["transactionId"] = transaction->getTransactionId(); } - auto meterValueJson = payload.createNestedArray("meterValue"); - for (auto entry = entries.begin(); entry != entries.end(); entry++) { - meterValueJson.add(**entry); + auto meterValueArray = payload.createNestedArray("meterValue"); + if (meterValueJson) { + meterValueArray.add(*meterValueJson); } return doc; diff --git a/src/MicroOcpp/Operations/MeterValues.h b/src/MicroOcpp/Operations/MeterValues.h index f4e4be4e..774a538a 100644 --- a/src/MicroOcpp/Operations/MeterValues.h +++ b/src/MicroOcpp/Operations/MeterValues.h @@ -20,14 +20,16 @@ namespace Ocpp16 { class MeterValues : public Operation, public MemoryManaged { private: Model& model; //for adjusting the timestamp if MeterValue has been created before BootNotification - Vector> meterValue; + MeterValue *meterValue = nullptr; + std::unique_ptr meterValueOwnership; unsigned int connectorId = 0; std::shared_ptr transaction; public: - MeterValues(Model& model, Vector>&& meterValue, unsigned int connectorId, std::shared_ptr transaction = nullptr); + MeterValues(Model& model, MeterValue *meterValue, unsigned int connectorId, std::shared_ptr transaction = nullptr); + MeterValues(Model& model, std::unique_ptr meterValue, unsigned int connectorId, std::shared_ptr transaction = nullptr); MeterValues(Model& model); //for debugging only. Make this for the server pendant diff --git a/src/MicroOcpp/Operations/TriggerMessage.cpp b/src/MicroOcpp/Operations/TriggerMessage.cpp index 518353a0..7a537f6d 100644 --- a/src/MicroOcpp/Operations/TriggerMessage.cpp +++ b/src/MicroOcpp/Operations/TriggerMessage.cpp @@ -36,12 +36,16 @@ void TriggerMessage::processReq(JsonObject payload) { if (connectorId < 0) { auto nConnectors = mService->getNumConnectors(); for (decltype(nConnectors) cId = 0; cId < nConnectors; cId++) { - context.getRequestQueue().sendRequestPreBoot(mService->takeTriggeredMeterValues(cId)); - statusMessage = "Accepted"; + if (auto meterValues = mService->takeTriggeredMeterValues(cId)) { + context.getRequestQueue().sendRequestPreBoot(std::move(meterValues)); + statusMessage = "Accepted"; + } } } else if (connectorId < mService->getNumConnectors()) { - context.getRequestQueue().sendRequestPreBoot(mService->takeTriggeredMeterValues(connectorId)); - statusMessage = "Accepted"; + if (auto meterValues = mService->takeTriggeredMeterValues(connectorId)) { + context.getRequestQueue().sendRequestPreBoot(std::move(meterValues)); + statusMessage = "Accepted"; + } } else { errorCode = "PropertyConstraintViolation"; } diff --git a/tests/Metering.cpp b/tests/Metering.cpp index 144d4a37..23e79fe5 100644 --- a/tests/Metering.cpp +++ b/tests/Metering.cpp @@ -6,12 +6,16 @@ #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") { @@ -101,19 +105,14 @@ TEST_CASE("Metering") { auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(10); - auto MeterValueCacheSizeInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "MeterValueCacheSize", 0); - MeterValueCacheSizeInt->setInt(2); - bool checkProcessed = false; setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; - Timestamp t0, t1; + Timestamp t0; t0.setTime(payload["meterValue"][0]["timestamp"] | ""); - t1.setTime(payload["meterValue"][1]["timestamp"] | ""); REQUIRE((t0 - base >= 10 && t0 - base <= 11)); - REQUIRE((t1 - base >= 20 && t1 - base <= 21)); REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["context"] | "", "Sample.Periodic")); }); @@ -130,10 +129,6 @@ TEST_CASE("Metering") { mtime = trackMtime + 10 * 1000; - loop(), - - mtime = trackMtime + 20 * 1000; - loop(); endTransaction(); @@ -163,19 +158,14 @@ TEST_CASE("Metering") { auto MeterValuesAlignedDataString = declareConfiguration("MeterValuesAlignedData", "", CONFIGURATION_FN); MeterValuesAlignedDataString->setString("Energy.Active.Import.Register"); - auto MeterValueCacheSizeInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "MeterValueCacheSize", 0, CONFIGURATION_FN); - MeterValueCacheSizeInt->setInt(2); - bool checkProcessed = false; setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; - Timestamp t0, t1; + Timestamp t0; t0.setTime(payload["meterValue"][0]["timestamp"] | ""); - t1.setTime(payload["meterValue"][1]["timestamp"] | ""); REQUIRE((t0 - base >= 900 && t0 - base <= 901)); - REQUIRE((t1 - base >= 1800 && t1 - base <= 1801)); REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["context"] | "", "Sample.Clock")); }); @@ -194,10 +184,6 @@ TEST_CASE("Metering") { loop(); model.getClock().setTime("2023-01-01T00:29:50Z"); - loop(); - - model.getClock().setTime("2023-01-01T00:30:00Z"); - loop(); endTransaction(); @@ -329,22 +315,16 @@ TEST_CASE("Metering") { auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); MeterValueSampleIntervalInt->setInt(10); - auto MeterValueCacheSizeInt = declareConfiguration(MO_CONFIG_EXT_PREFIX "MeterValueCacheSize", 0, CONFIGURATION_FN); - MeterValueCacheSizeInt->setInt(10); - bool checkProcessed = false; setOnReceiveRequest("MeterValues", [base, &checkProcessed] (JsonObject payload) { checkProcessed = true; - Timestamp t0, t1; + Timestamp t0; t0.setTime(payload["meterValue"][0]["timestamp"] | ""); - t1.setTime(payload["meterValue"][1]["timestamp"] | ""); REQUIRE((t0 - base >= 10 && t0 - base <= 11)); - REQUIRE((t1 - base >= 20 && t1 - base <= 21)); - REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["measurand"] | "", "Energy.Active.Import.Register")); - REQUIRE(!strcmp(payload["meterValue"][1]["sampledValue"][0]["measurand"] | "", "Power.Active.Import")); + REQUIRE(!strcmp(payload["meterValue"][0]["sampledValue"][0]["measurand"] | "", "Power.Active.Import")); }); loop(); @@ -355,15 +335,135 @@ TEST_CASE("Metering") { beginTransaction_authorized("mIdTag"); + MeterValuesSampledDataString->setString("Power.Active.Import"); + loop(); mtime = trackMtime + 10 * 1000; loop(); - MeterValuesSampledDataString->setString("Power.Active.Import"); + endTransaction(); - mtime = trackMtime + 20 * 1000; + loop(); + + REQUIRE(checkProcessed); + } + + SECTION("Preserve order of tx-related msgs") { + + loopback.setConnected(false); + + declareConfiguration(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true)->setBool(true); + + Timestamp base; + base.setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + auto MeterValuesSampledDataString = declareConfiguration("MeterValuesSampledData","", CONFIGURATION_FN); + MeterValuesSampledDataString->setString("Energy.Active.Import.Register"); + + auto MeterValueSampleIntervalInt = declareConfiguration("MeterValueSampleInterval",0, CONFIGURATION_FN); + MeterValueSampleIntervalInt->setInt(10); + + configuration_save(); + + unsigned int countProcessed = 0; + + setOnReceiveRequest("StartTransaction", [&countProcessed] (JsonObject) { + REQUIRE(countProcessed == 0); + countProcessed++; + }); + + int assignedTxId = -1; + + setOnSendConf("StartTransaction", [&assignedTxId] (JsonObject conf) { + assignedTxId = conf["transactionId"]; + }); + + setOnReceiveRequest("MeterValues", [&countProcessed, &assignedTxId] (JsonObject req) { + REQUIRE(countProcessed == 1); + countProcessed++; + + int transactionId = req["transactionId"] | -1000; + + REQUIRE(assignedTxId == transactionId); + }); + + setOnReceiveRequest("StopTransaction", [&countProcessed] (JsonObject) { + REQUIRE(countProcessed == 2); + countProcessed++; + }); + + loop(); + + auto trackMtime = mtime; + + beginTransaction_authorized("mIdTag"); + + loop(); + + mtime = trackMtime + 10 * 1000; + + loop(); + + endTransaction(); + + loop(); + + loopback.setConnected(true); + + loop(); + + REQUIRE(countProcessed == 3); + + /* + * Combine test case with power loss. Start tx before power loss, then enqueue 1 MV, then StopTx + */ + + countProcessed = 0; + + beginTransaction("mIdTag"); + + loop(); + + mocpp_deinitialize(); + + loopback.setConnected(false); + + mocpp_initialize(loopback, ChargerCredentials()); + getOcppContext()->getModel().getClock().setTime(BASE_TIME); + + base.setTime(BASE_TIME); + + addMeterValueInput([base] () { + //simulate 3600W consumption + return getOcppContext()->getModel().getClock().now() - base; + }, "Energy.Active.Import.Register"); + + setOnReceiveRequest("MeterValues", [&countProcessed, &assignedTxId] (JsonObject req) { + REQUIRE(countProcessed == 1); + countProcessed++; + + int transactionId = req["transactionId"] | -1000; + + REQUIRE(assignedTxId == transactionId); + }); + + setOnReceiveRequest("StopTransaction", [&countProcessed] (JsonObject) { + REQUIRE(countProcessed == 2); + countProcessed++; + }); + + trackMtime = mtime; + + loop(); + + mtime = trackMtime + 10 * 1000; loop(); @@ -371,7 +471,259 @@ TEST_CASE("Metering") { 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; + + auto tx = beginTransaction_authorized("mIdTag"); + + 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();