diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 80f0365c..888d9121 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -34,7 +34,7 @@ jobs: with: repository: matth-x/MicroOcppSimulator path: MicroOcppSimulator - ref: feature/remote-api + ref: 20e8da7b76a3f5f3e50facab018a457ecdbcca59 submodules: 'recursive' - name: Clean MicroOcpp submodule run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index b02743f9..fc31c734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - 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 diff --git a/src/MicroOcpp.cpp b/src/MicroOcpp.cpp index de8e6ae8..7ca20f3a 100644 --- a/src/MicroOcpp.cpp +++ b/src/MicroOcpp.cpp @@ -290,7 +290,7 @@ void mocpp_initialize(Connection& connection, const char *bootNotificationCreden model.setVariableService(std::unique_ptr( new VariableService(*context, filesystem))); model.setTransactionService(std::unique_ptr( - new TransactionService(*context))); + new TransactionService(*context, filesystem, MO_NUM_EVSEID))); model.setRemoteControlService(std::unique_ptr( new RemoteControlService(*context, MO_NUM_EVSEID))); } else @@ -852,6 +852,7 @@ void setEvReadyInput(std::function evReadyInput, unsigned int connectorI evse->setEvReadyInput(evReadyInput); } } + return; } #endif auto connector = context->getModel().getConnector(connectorId); diff --git a/src/MicroOcpp/Model/Authorization/IdToken.cpp b/src/MicroOcpp/Model/Authorization/IdToken.cpp index f6ceec10..e1637ef8 100644 --- a/src/MicroOcpp/Model/Authorization/IdToken.cpp +++ b/src/MicroOcpp/Model/Authorization/IdToken.cpp @@ -22,6 +22,8 @@ IdToken::IdToken(const char *token, Type type, const char *memoryTag) : MemoryMa MO_DBG_ERR("invalid token"); *idToken = '\0'; } + } else { + *idToken = '\0'; } } @@ -63,11 +65,11 @@ bool IdToken::parseCstr(const char *token, const char *typeCstr) { } const char *IdToken::get() const { - return *idToken ? idToken : nullptr;; + return idToken; } const char *IdToken::getTypeCstr() const { - const char *res = nullptr; + const char *res = ""; switch (type) { case Type::UNDEFINED: MO_DBG_ERR("internal error"); @@ -98,7 +100,7 @@ const char *IdToken::getTypeCstr() const { break; } - return res ? res : ""; + return res; } bool IdToken::equals(const IdToken& other) { diff --git a/src/MicroOcpp/Model/ConnectorBase/Connector.h b/src/MicroOcpp/Model/ConnectorBase/Connector.h index 541c1f4c..824e3d3b 100644 --- a/src/MicroOcpp/Model/ConnectorBase/Connector.h +++ b/src/MicroOcpp/Model/ConnectorBase/Connector.h @@ -22,8 +22,6 @@ #define MO_TXRECORD_SIZE 4 //no. of tx to hold on flash storage #endif -#define MAX_TX_CNT 100000U //upper limit of txNr (internal usage). Must be at least 2*MO_TXRECORD_SIZE+1 - #ifndef MO_REPORT_NOERROR #define MO_REPORT_NOERROR 0 #endif diff --git a/src/MicroOcpp/Model/Metering/MeterValuesV201.cpp b/src/MicroOcpp/Model/Metering/MeterValuesV201.cpp index fc856204..97376774 100644 --- a/src/MicroOcpp/Model/Metering/MeterValuesV201.cpp +++ b/src/MicroOcpp/Model/Metering/MeterValuesV201.cpp @@ -288,6 +288,46 @@ bool MeteringServiceEvse::existsMeasurand(const char *measurand, size_t len) { 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(); @@ -298,40 +338,10 @@ MeteringService::MeteringService(Model& model, size_t numEvses) { varService->declareVariable("SampledDataCtrlr", "TxEndedMeasurands", ""); varService->declareVariable("AlignedDataCtrlr", "AlignedDataMeasurands", ""); - 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 evseId = 0; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { - if (evses[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; - }; - - varService->registerValidator("SampledDataCtrlr", "TxStartedMeasurands", validateSelectString); - varService->registerValidator("SampledDataCtrlr", "TxUpdatedMeasurands", validateSelectString); - varService->registerValidator("SampledDataCtrlr", "TxEndedMeasurands", validateSelectString); - varService->registerValidator("AlignedDataCtrlr", "AlignedDataMeasurands", validateSelectString); + 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); diff --git a/src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp b/src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp index 7d49d2bc..74aefe0f 100644 --- a/src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp +++ b/src/MicroOcpp/Model/RemoteControl/RemoteControlService.cpp @@ -40,7 +40,7 @@ UnlockStatus RemoteControlServiceEvse::unlockConnector() { if (tx->started && !tx->stopped && tx->isAuthorized) { return UnlockStatus_OngoingAuthorizedTransaction; } else { - evse->abortTransaction(Ocpp201::Transaction::StopReason::Other,Ocpp201::TransactionEventTriggerReason::UnlockCommand); + evse->abortTransaction(Ocpp201::Transaction::StoppedReason::Other,Ocpp201::TransactionEventTriggerReason::UnlockCommand); } } } @@ -102,7 +102,7 @@ RemoteControlServiceEvse *RemoteControlService::getEvse(unsigned int evseId) { return evses[evseId]; } -RequestStartStopStatus RemoteControlService::requestStartTransaction(unsigned int evseId, unsigned int remoteStartId, IdToken idToken, std::shared_ptr& transactionOut) { +RequestStartStopStatus RemoteControlService::requestStartTransaction(unsigned int evseId, unsigned int remoteStartId, IdToken idToken, char *transactionIdOut, size_t transactionIdBufSize) { TransactionService *txService = context.getModel().getTransactionService(); if (!txService) { @@ -116,21 +116,32 @@ RequestStartStopStatus RemoteControlService::requestStartTransaction(unsigned in return RequestStartStopStatus_Rejected; } - transactionOut = evse->getTransaction(); - 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; } - transactionOut = evse->getTransaction(); - if (!transactionOut) { + auto ret = snprintf(transactionIdOut, transactionIdBufSize, "%s", tx->transactionId); + if (ret < 0 || (size_t)ret >= transactionIdBufSize) { MO_DBG_ERR("internal error"); return RequestStartStopStatus_Rejected; } - transactionOut->remoteStartId = remoteStartId; - transactionOut->notifyRemoteStartId = true; + tx->remoteStartId = remoteStartId; + tx->notifyRemoteStartId = true; return RequestStartStopStatus_Accepted; } @@ -148,7 +159,7 @@ RequestStartStopStatus RemoteControlService::requestStopTransaction(const char * 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::StopReason::Remote, Ocpp201::TransactionEventTriggerReason::RemoteStop); + success = evse->abortTransaction(Ocpp201::Transaction::StoppedReason::Remote, Ocpp201::TransactionEventTriggerReason::RemoteStop); break; } } diff --git a/src/MicroOcpp/Model/RemoteControl/RemoteControlService.h b/src/MicroOcpp/Model/RemoteControl/RemoteControlService.h index a19adcc7..0a26567b 100644 --- a/src/MicroOcpp/Model/RemoteControl/RemoteControlService.h +++ b/src/MicroOcpp/Model/RemoteControl/RemoteControlService.h @@ -53,7 +53,7 @@ class RemoteControlService : public MemoryManaged { RemoteControlServiceEvse *getEvse(unsigned int evseId); - RequestStartStopStatus requestStartTransaction(unsigned int evseId, unsigned int remoteStartId, IdToken idToken, std::shared_ptr& transactionOut); //ChargingProfile, GroupIdToken not supported yet + 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); }; diff --git a/src/MicroOcpp/Model/Reset/ResetService.cpp b/src/MicroOcpp/Model/Reset/ResetService.cpp index e20c8b30..8335ce42 100644 --- a/src/MicroOcpp/Model/Reset/ResetService.cpp +++ b/src/MicroOcpp/Model/Reset/ResetService.cpp @@ -125,7 +125,7 @@ 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, MO_VARIABLE_FN, Variable::Mutability::ReadOnly); + varService->declareVariable(ComponentId("EVSE", evseId >= 1 ? evseId : -1), "AllowReset", true, Variable::Mutability::ReadOnly, false); } void ResetService::Evse::loop() { @@ -303,7 +303,7 @@ ResetStatus ResetService::initiateReset(ResetType type, unsigned int evseId) { if (tx->active) { //Tx in progress. Check behavior if (type == ResetType_Immediate) { - txService->getEvse(eId)->abortTransaction(Transaction::StopReason::ImmediateReset, TransactionEventTriggerReason::ResetCommand); + txService->getEvse(eId)->abortTransaction(Transaction::StoppedReason::ImmediateReset, TransactionEventTriggerReason::ResetCommand); } else { scheduled = true; break; diff --git a/src/MicroOcpp/Model/Transactions/Transaction.cpp b/src/MicroOcpp/Model/Transactions/Transaction.cpp index 333da16e..bd21db2e 100644 --- a/src/MicroOcpp/Model/Transactions/Transaction.cpp +++ b/src/MicroOcpp/Model/Transactions/Transaction.cpp @@ -4,6 +4,7 @@ #include #include +#include using namespace MicroOcpp; @@ -31,6 +32,329 @@ 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 + int ocpp_tx_getTransactionId(OCPP_Transaction *tx) { return reinterpret_cast(tx)->getTransactionId(); } diff --git a/src/MicroOcpp/Model/Transactions/Transaction.h b/src/MicroOcpp/Model/Transactions/Transaction.h index bf8a058c..c9d0d80a 100644 --- a/src/MicroOcpp/Model/Transactions/Transaction.h +++ b/src/MicroOcpp/Model/Transactions/Transaction.h @@ -11,6 +11,8 @@ #include #include +#define MAX_TX_CNT 100000U //upper limit of txNr (internal usage). Must be at least 2*MO_TXRECORD_SIZE+1 + namespace MicroOcpp { /* @@ -233,7 +235,7 @@ class Transaction : public MemoryManaged { public: // ReasonEnumType (3.67) - enum class StopReason : uint8_t { + enum class StoppedReason : uint8_t { UNDEFINED, // not part of OCPP DeAuthorized, EmergencyStop, @@ -273,7 +275,6 @@ class Transaction : public MemoryManaged { 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 - bool stoppedConfirmed = false; //if all TxEvents have been sent to the server (acknowledged or aborted) /* * Global transaction data @@ -281,7 +282,6 @@ class Transaction : public MemoryManaged { 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 - unsigned int seqNoCounter = 0; // increment by 1 for each event IdToken idToken; Timestamp beginTimestamp = MIN_TIME; char transactionId [MO_TXID_LEN_MAX + 1] = {'\0'}; @@ -297,7 +297,7 @@ class Transaction : public MemoryManaged { bool evConnectionTimeoutListen = true; - StopReason stopReason = StopReason::UNDEFINED; + StoppedReason stoppedReason = StoppedReason::UNDEFINED; TransactionEventTriggerReason stopTrigger = TransactionEventTriggerReason::UNDEFINED; std::unique_ptr stopIdToken; // if null, then stopIdToken equals idToken @@ -316,9 +316,15 @@ class Transaction : public MemoryManaged { 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())) { } + Transaction() : + MemoryManaged("v201.Transactions.Transaction"), + sampledDataTxEnded(makeVector>(getMemoryTag())), + seqNos(makeVector(getMemoryTag())) { } void addSampledDataTxEnded(std::unique_ptr mv) { if (sampledDataTxEnded.size() >= MO_SAMPLEDDATATXENDED_SIZE_MAX) { @@ -365,9 +371,10 @@ class TransactionEventData : public MemoryManaged { }; //private: - std::shared_ptr transaction; + Transaction *transaction; Type eventType; Timestamp timestamp; + uint16_t bootNr = 0; TransactionEventTriggerReason triggerReason; const unsigned int seqNo; bool offline = false; @@ -383,10 +390,27 @@ class TransactionEventData : public MemoryManaged { EvseId evse = -1; //meterValue not supported Vector> meterValue; + + unsigned int opNr = 0; + unsigned int attemptNr = 0; + Timestamp attemptTime = MIN_TIME; - TransactionEventData(std::shared_ptr transaction, unsigned int seqNo) : MemoryManaged("v201.Transactions.TransactionEventData"), transaction(transaction), seqNo(seqNo), meterValue(makeVector>(getMemoryTag())) { } + 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 diff --git a/src/MicroOcpp/Model/Transactions/TransactionService.cpp b/src/MicroOcpp/Model/Transactions/TransactionService.cpp index 7f74e7be..be9fb72a 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionService.cpp +++ b/src/MicroOcpp/Model/Transactions/TransactionService.cpp @@ -11,35 +11,56 @@ #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, unsigned int evseId) : +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 } -std::unique_ptr TransactionService::Evse::allocateTransaction() { - auto tx = std::unique_ptr(new Ocpp201::Transaction()); - if (!tx) { - // OOM - return nullptr; +TransactionService::Evse::~Evse() { + +} + +bool TransactionService::Evse::beginTransaction() { + + if (transaction) { + MO_DBG_ERR("transaction still running"); + return false; } - tx->evseId = evseId; - tx->txNr = txNrCounter; + 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); @@ -48,27 +69,178 @@ std::unique_ptr TransactionService::Evse::allocateTransact h *= 749572633U; h %= 24593209U; for (size_t i = 0; i < sizeof(tx->transactionId) - 3; i += 2) { - snprintf(tx->transactionId + i, 3, "%02X", (uint8_t)h); + 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(); - MO_DBG_DEBUG("allocated tx %u-%s", evseId, tx->transactionId); + 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 tx; + 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; } @@ -88,9 +260,7 @@ void TransactionService::Evse::loop() { context.getModel().getClock().now() - transaction->beginTimestamp >= txService.evConnectionTimeOutInt->getInt()) { MO_DBG_INFO("Session mngt: timeout"); - transaction->active = false; - transaction->stopTrigger = TransactionEventTriggerReason::EVConnectTimeout; - transaction->stopReason = Ocpp201::Transaction::StopReason::Timeout; + endTransaction(Ocpp201::Transaction::StoppedReason::Timeout, TransactionEventTriggerReason::EVConnectTimeout); } if (transaction->active && @@ -100,15 +270,12 @@ void TransactionService::Evse::loop() { txService.isTxStopPoint(TxStartStopPoint::Authorized) || txService.isTxStopPoint(TxStartStopPoint::PowerPathClosed))) { MO_DBG_INFO("Session mngt: Deauthorized before start"); - transaction->active = false; - transaction->stopTrigger = TransactionEventTriggerReason::Deauthorized; - transaction->stopReason = Ocpp201::Transaction::StopReason::DeAuthorized; + endTransaction(Ocpp201::Transaction::StoppedReason::DeAuthorized, TransactionEventTriggerReason::Deauthorized); } } } - - std::shared_ptr txEvent; + std::unique_ptr txEvent; bool txStopCondition = false; @@ -116,55 +283,53 @@ void TransactionService::Evse::loop() { // stop tx? TransactionEventTriggerReason triggerReason = TransactionEventTriggerReason::UNDEFINED; - Ocpp201::Transaction::StopReason stopReason = Ocpp201::Transaction::StopReason::UNDEFINED; + Ocpp201::Transaction::StoppedReason stoppedReason = Ocpp201::Transaction::StoppedReason::UNDEFINED; if (transaction && !transaction->active) { // tx ended via endTransaction txStopCondition = true; triggerReason = transaction->stopTrigger; - stopReason = transaction->stopReason; + 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; - stopReason = Ocpp201::Transaction::StopReason::EVDisconnected; + 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; - stopReason = Ocpp201::Transaction::StopReason::Local; + stoppedReason = Ocpp201::Transaction::StoppedReason::Local; } else if (txService.isTxStopPoint(TxStartStopPoint::EnergyTransfer) && evReadyInput && !evReadyInput()) { txStopCondition = true; triggerReason = TransactionEventTriggerReason::ChargingStateChanged; - stopReason = Ocpp201::Transaction::StopReason::StoppedByEV; + 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; - stopReason = Ocpp201::Transaction::StopReason::Other; + 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; - stopReason = Ocpp201::Transaction::StopReason::DeAuthorized; + stoppedReason = Ocpp201::Transaction::StoppedReason::DeAuthorized; } if (txStopCondition && transaction && transaction->started && transaction->active) { MO_DBG_INFO("Session mngt: TxStopPoint reached"); - transaction->active = false; - transaction->stopTrigger = triggerReason; - transaction->stopReason = stopReason; + endTransaction(stoppedReason, triggerReason); } if (transaction && @@ -172,14 +337,14 @@ void TransactionService::Evse::loop() { (!stopTxReadyInput || stopTxReadyInput())) { // yes, stop running tx - txEvent = std::allocate_shared(makeAllocator(getMemoryTag()), transaction, transaction->seqNoCounter++); + txEvent = txStore.createTransactionEvent(*transaction); if (!txEvent) { // OOM return; } transaction->stopTrigger = triggerReason; - transaction->stopReason = stopReason; + transaction->stoppedReason = stoppedReason; txEvent->eventType = TransactionEventData::Type::Ended; txEvent->triggerReason = triggerReason; @@ -229,7 +394,7 @@ void TransactionService::Evse::loop() { // start tx if (!transaction) { - transaction = allocateTransaction(); + beginTransaction(); if (!transaction) { // OOM return; @@ -239,7 +404,7 @@ void TransactionService::Evse::loop() { } } - txEvent = std::allocate_shared(makeAllocator(getMemoryTag()), transaction, transaction->seqNoCounter++); + txEvent = txStore.createTransactionEvent(*transaction); if (!txEvent) { // OOM return; @@ -334,7 +499,7 @@ void TransactionService::Evse::loop() { if (txUpdateCondition && !txEvent && transaction->started && !transaction->stopped) { // yes, updated - txEvent = std::allocate_shared(makeAllocator(getMemoryTag()), transaction, transaction->seqNoCounter++); + txEvent = txStore.createTransactionEvent(*transaction); if (!txEvent) { // OOM return; @@ -395,16 +560,36 @@ void TransactionService::Evse::loop() { } if (txEvent) { - auto txEventRequest = makeRequest(new Ocpp201::TransactionEvent(context.getModel(), txEvent)); - txEventRequest->setTimeout(0); - context.initiateRequest(std::move(txEventRequest)); - 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()); + } + + //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) { @@ -428,7 +613,7 @@ bool TransactionService::Evse::beginAuthorization(IdToken idToken, bool validate } if (!transaction) { - transaction = allocateTransaction(); + beginTransaction(); if (!transaction) { MO_DBG_ERR("could not allocate Tx"); return false; @@ -442,8 +627,6 @@ bool TransactionService::Evse::beginAuthorization(IdToken idToken, bool validate transaction->idToken = idToken; transaction->beginTimestamp = context.getModel().getClock().now(); - auto tx = transaction; - if (validateIdToken) { auto authorize = makeRequest(new Authorize(context.getModel(), idToken)); if (!authorize) { @@ -452,27 +635,45 @@ bool TransactionService::Evse::beginAuthorization(IdToken idToken, bool validate return false; } - authorize->setOnReceiveConfListener([this, tx] (JsonObject response) { + 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); return; } MO_DBG_DEBUG("Authorized tx with validation (%s)", tx->idToken.get()); tx->isAuthorized = true; tx->notifyIdToken = true; + txStore.commit(tx); }); - authorize->setOnAbortListener([this, tx] () { + 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); }); authorize->setTimeout(20 * 1000); context.initiateRequest(std::move(authorize)); } else { - MO_DBG_DEBUG("Authorized tx directly (%s)", tx->idToken.get()); - tx->isAuthorized = true; - tx->notifyIdToken = true; + MO_DBG_DEBUG("Authorized tx directly (%s)", transaction->idToken.get()); + transaction->isAuthorized = true; + transaction->notifyIdToken = true; } return true; @@ -498,8 +699,6 @@ bool TransactionService::Evse::endAuthorization(IdToken idToken, bool validateId } else { // use a different idToken for stopping the tx - auto tx = transaction; - auto authorize = makeRequest(new Authorize(context.getModel(), idToken)); if (!authorize) { // OOM @@ -507,7 +706,16 @@ bool TransactionService::Evse::endAuthorization(IdToken idToken, bool validateId return false; } - authorize->setOnReceiveConfListener([tx, idToken, this] (JsonObject response) { + 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()); return; @@ -519,15 +727,14 @@ bool TransactionService::Evse::endAuthorization(IdToken idToken, bool validateId if (!tx->stopIdToken) { // OOM if (tx->active) { - tx->active = false; - tx->stopTrigger = TransactionEventTriggerReason::AbnormalCondition; - tx->stopReason = Ocpp201::Transaction::StopReason::Other; + abortTransaction(); } return; } tx->isAuthorizationActive = false; tx->notifyStopIdToken = true; + txStore.commit(tx); }); authorize->setTimeout(20 * 1000); context.initiateRequest(std::move(authorize)); @@ -535,24 +742,11 @@ bool TransactionService::Evse::endAuthorization(IdToken idToken, bool validateId return true; } -bool TransactionService::Evse::abortTransaction(Ocpp201::Transaction::StopReason stopReason, TransactionEventTriggerReason stopTrigger) { - - if (!transaction || !transaction->active) { - //transaction already ended / not active anymore - return false; - } - - MO_DBG_DEBUG("Abort session started by idTag %s", - transaction->idToken.get()); - - transaction->active = false; - transaction->stopTrigger = stopTrigger; - transaction->stopReason = stopReason; - - return true; +bool TransactionService::Evse::abortTransaction(Ocpp201::Transaction::StoppedReason stoppedReason, TransactionEventTriggerReason stopTrigger) { + return endTransaction(stoppedReason, stopTrigger); } -std::shared_ptr& TransactionService::Evse::getTransaction() { - return transaction; +MicroOcpp::Ocpp201::Transaction *TransactionService::Evse::getTransaction() { + return transaction.get(); } bool TransactionService::Evse::ocppPermitsCharge() { @@ -563,6 +757,142 @@ bool TransactionService::Evse::ocppPermitsCharge() { !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) { @@ -626,10 +956,27 @@ bool TransactionService::parseTxStartStopPoint(const char *csl, Vector(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), - evses(makeVector(getMemoryTag())), + txStore(filesystem, numEvseIds), txStartPointParsed(makeVector(getMemoryTag())), txStopPointParsed(makeVector(getMemoryTag())) { auto varService = context.getModel().getVariableService(); @@ -641,41 +988,42 @@ TransactionService::TransactionService(Context& context) : 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, MO_VARIABLE_VOLATILE, Variable::Mutability::ReadOnly); - - varService->registerValidator("TxCtrlr", "TxStartPoint", [this] (const char *value) -> bool { - auto validated = makeVector(getMemoryTag()); - return this->parseTxStartStopPoint(value, validated); - }); - - varService->registerValidator("TxCtrlr", "TxStopPoint", [this] (const char *value) -> bool { - auto validated = makeVector(getMemoryTag()); - return this->parseTxStartStopPoint(value, validated); - }); - - std::function validateUnsignedInt = [] (int val) { - return val >= 0; - }; + 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); - evses.reserve(MO_NUM_EVSEID); - - for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID; evseId++) { - evses.emplace_back(context, *this, evseId); + 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 - evses[0].connectorPluggedInput = [] () {return false;}; - evses[0].evReadyInput = [] () {return false;}; - evses[0].evseReadyInput = [] () {return false;}; + 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 (Evse& evse : evses) { - evse.loop(); + for (unsigned int evseId = 0; evseId < MO_NUM_EVSEID && evses[evseId]; evseId++) { + evses[evseId]->loop(); } if (txStartPointString->getWriteCount() != trackTxStartPoint) { @@ -687,15 +1035,16 @@ void TransactionService::loop() { } // assign tx on evseId 0 to an EVSE - if (auto& tx0 = evses[0].getTransaction()) { + if (evses[0]->transaction) { //pending tx on evseId 0 - if (tx0->active) { - for (unsigned int evseId = 1; evseId < MO_NUM_EVSEID; evseId++) { - if (!evses[evseId].getTransaction() && - (!evses[evseId].connectorPluggedInput || evses[evseId].connectorPluggedInput())) { + 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); - tx0->notifyEvseId = true; - evses[evseId].transaction = std::move(tx0); + evses[0]->transaction->notifyEvseId = true; + evses[0]->transaction->evseId = evseId; + evses[evseId]->transaction = std::move(evses[0]->transaction); } } } @@ -703,12 +1052,10 @@ void TransactionService::loop() { } TransactionService::Evse *TransactionService::getEvse(unsigned int evseId) { - if (evseId < evses.size()) { - return &evses[evseId]; - } else { - MO_DBG_ERR("invalid arg"); + 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 index ca9704b7..45a0b64a 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionService.h +++ b/src/MicroOcpp/Model/Transactions/TransactionService.h @@ -14,27 +14,35 @@ #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 MemoryManaged { + class Evse : public RequestEmitter, public MemoryManaged { private: Context& context; TransactionService& txService; + Ocpp201::TransactionStoreEvse& txStore; const unsigned int evseId; unsigned int txNrCounter = 0; - std::shared_ptr transaction; + std::unique_ptr transaction; Ocpp201::TransactionEventData::ChargingState trackChargingState = Ocpp201::TransactionEventData::ChargingState::UNDEFINED; std::function connectorPluggedInput; @@ -44,10 +52,21 @@ class TransactionService : public MemoryManaged { std::function startTxReadyInput; std::function stopTxReadyInput; - std::unique_ptr allocateTransaction(); + 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, unsigned int evseId); + Evse(Context& context, TransactionService& txService, Ocpp201::TransactionStoreEvse& txStore, unsigned int evseId); + virtual ~Evse(); void loop(); @@ -56,20 +75,21 @@ class TransactionService : public MemoryManaged { void setEvseReadyInput(std::function connectorEnergized); bool beginAuthorization(IdToken idToken, bool validateIdToken = true); // authorize by swipe RFID - bool endAuthorization(IdToken idToken = IdToken(), bool validateIdToken = true); // stop authorization 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::StopReason stopReason = Ocpp201::Transaction::StopReason::Other, Ocpp201::TransactionEventTriggerReason stopTrigger = Ocpp201::TransactionEventTriggerReason::AbnormalCondition); + bool abortTransaction(Ocpp201::Transaction::StoppedReason stoppedReason = Ocpp201::Transaction::StoppedReason::Other, Ocpp201::TransactionEventTriggerReason stopTrigger = Ocpp201::TransactionEventTriggerReason::AbnormalCondition); - std::shared_ptr& getTransaction(); + Ocpp201::Transaction *getTransaction(); bool ocppPermitsCharge(); + unsigned int getFrontRequestOpNr() override; + std::unique_ptr fetchFrontRequest() override; + friend TransactionService; }; -private: - // TxStartStopPoint (2.6.4.1) enum class TxStartStopPoint : uint8_t { ParkingBayOccupancy, @@ -80,8 +100,10 @@ class TransactionService : public MemoryManaged { EnergyTransfer }; +private: Context& context; - Vector evses; + Ocpp201::TransactionStore txStore; + Evse *evses [MO_NUM_EVSEID] = {nullptr}; Variable *txStartPointString = nullptr; Variable *txStopPointString = nullptr; @@ -90,21 +112,24 @@ class TransactionService : public MemoryManaged { 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); - - bool parseTxStartStopPoint(const char *src, Vector& dst); - public: - TransactionService(Context& context); + 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 diff --git a/src/MicroOcpp/Model/Transactions/TransactionStore.cpp b/src/MicroOcpp/Model/Transactions/TransactionStore.cpp index 01ea4111..39c096ba 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionStore.cpp +++ b/src/MicroOcpp/Model/Transactions/TransactionStore.cpp @@ -57,7 +57,7 @@ std::shared_ptr ConnectorTransactionStore::getTransaction(unsigned } char fn [MO_MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.jsn", connectorId, txNr); + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.json", connectorId, txNr); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return nullptr; @@ -130,7 +130,7 @@ bool ConnectorTransactionStore::commit(Transaction *transaction) { } char fn [MO_MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.jsn", connectorId, transaction->getTxNr()); + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.json", connectorId, transaction->getTxNr()); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return false; @@ -159,7 +159,7 @@ bool ConnectorTransactionStore::remove(unsigned int txNr) { } char fn [MO_MAX_PATH_SIZE] = {'\0'}; - auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.jsn", connectorId, txNr); + auto ret = snprintf(fn, MO_MAX_PATH_SIZE, MO_FILENAME_PREFIX "tx" "-%u-%u.json", connectorId, txNr); if (ret < 0 || ret >= MO_MAX_PATH_SIZE) { MO_DBG_ERR("fn error: %i", ret); return false; @@ -221,3 +221,890 @@ bool TransactionStore::remove(unsigned int connectorId, unsigned int txNr) { } 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 index 7e4db699..c5d3421e 100644 --- a/src/MicroOcpp/Model/Transactions/TransactionStore.h +++ b/src/MicroOcpp/Model/Transactions/TransactionStore.h @@ -5,6 +5,7 @@ #ifndef MO_TRANSACTIONSTORE_H #define MO_TRANSACTIONSTORE_H +#include #include #include #include @@ -54,4 +55,63 @@ class TransactionStore : public MemoryManaged { } +#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 index 4f676d0f..ee3f0229 100644 --- a/src/MicroOcpp/Model/Variables/Variable.cpp +++ b/src/MicroOcpp/Model/Variables/Variable.cpp @@ -165,13 +165,6 @@ bool Variable::isConstant() { return constant; } -void Variable::detach() { - detached = true; -} -bool Variable::isDetached() { - return detached; -} - template struct VariableSingleData { T value = 0; diff --git a/src/MicroOcpp/Model/Variables/Variable.h b/src/MicroOcpp/Model/Variables/Variable.h index c64ffa46..79178303 100644 --- a/src/MicroOcpp/Model/Variables/Variable.h +++ b/src/MicroOcpp/Model/Variables/Variable.h @@ -177,8 +177,6 @@ class Variable : public MemoryManaged { AttributeTypeSet attributes; - bool detached = false; // MO-internal: if a conflicting declaration comes in, discard the old Variable - // VariableMonitoringType (2.52) //std::vector monitors; // uncomment when testing Monitors public: @@ -224,9 +222,6 @@ class Variable : public MemoryManaged { //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) - - void detach(); - bool isDetached(); }; std::unique_ptr makeVariable(Variable::InternalDataType dtype, Variable::AttributeTypeSet supportAttributes); diff --git a/src/MicroOcpp/Model/Variables/VariableContainer.cpp b/src/MicroOcpp/Model/Variables/VariableContainer.cpp index 1b99396d..0d751d8f 100644 --- a/src/MicroOcpp/Model/Variables/VariableContainer.cpp +++ b/src/MicroOcpp/Model/Variables/VariableContainer.cpp @@ -11,6 +11,7 @@ #if MO_ENABLE_V201 #include +#include #include @@ -22,52 +23,273 @@ VariableContainer::~VariableContainer() { } -VariableContainerVolatile::VariableContainerVolatile(const char *filename, bool accessible) : - VariableContainer(filename, accessible), MemoryManaged("v201.Variables.VariableContainerVolatile.", filename), variables(makeVector>(getMemoryTag())) { +bool VariableContainer::commit() { + return true; +} + +VariableContainerNonOwning::VariableContainerNonOwning() : + VariableContainer(), MemoryManaged("v201.Variables.VariableContainerNonOwning"), variables(makeVector(getMemoryTag())) { } -VariableContainerVolatile::~VariableContainerVolatile() { +size_t VariableContainerNonOwning::size() { + return variables.size(); +} +Variable *VariableContainerNonOwning::getVariable(size_t i) { + return variables[i]; } -bool VariableContainerVolatile::load() { - return true; +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 VariableContainerVolatile::save() { +bool VariableContainerNonOwning::add(Variable *variable) { + variables.push_back(variable); return true; } -std::unique_ptr VariableContainerVolatile::createVariable(Variable::InternalDataType dtype, Variable::AttributeTypeSet attributes) { - return makeVariable(dtype, attributes); +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; } -bool VariableContainerVolatile::add(std::unique_ptr variable) { - variables.push_back(std::move(variable)); - return true; +VariableContainerOwning::VariableContainerOwning() : + VariableContainer(), MemoryManaged("v201.Variables.VariableContainerOwning"), variables(makeVector>(getMemoryTag())) { + } -size_t VariableContainerVolatile::size() const { +VariableContainerOwning::~VariableContainerOwning() { + MO_FREE(filename); + filename = nullptr; +} + +size_t VariableContainerOwning::size() { return variables.size(); } -Variable *VariableContainerVolatile::getVariable(size_t i) const { +Variable *VariableContainerOwning::getVariable(size_t i) { return variables[i].get(); } -Variable *VariableContainerVolatile::getVariable(const ComponentId& component, const char *variableName) const { - for (auto it = variables.begin(); it != variables.end(); it++) { - if (!strcmp((*it)->getName(), variableName) && - (*it)->getComponentId().equals(component)) { - return it->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; } -std::unique_ptr MicroOcpp::makeVariableContainerVolatile(const char *filename, bool accessible) { - return std::unique_ptr(new VariableContainerVolatile(filename, accessible)); +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 index fe247abe..d4dd0c69 100644 --- a/src/MicroOcpp/Model/Variables/VariableContainer.h +++ b/src/MicroOcpp/Model/Variables/VariableContainer.h @@ -16,61 +16,59 @@ #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: - const char *filename; - bool accessible; + Vector variables; public: - VariableContainer(const char *filename, bool accessible) : filename(filename), accessible(accessible) { } - - virtual ~VariableContainer(); - - const char *getFilename() {return filename;} - bool isAccessible() const {return accessible;} //accessible by OCPP server or only used as internal persistent store - - virtual bool load() = 0; //called at the end of mocpp_intialize, to load the Variables with the stored value - virtual bool save() = 0; - - /* - * Factory method to create Variable objects. This doesn't add the returned Variable to the managed - * variable store. Instead, the caller must add the returned Variable via `add(...)` - * The function signature consists of the requested low-level data type (Int, Bool, or String) and - * a composite key to identify the Variable to create (componentName x variableName x attribute type) - * - * Variable::InternalDataType dtype: internal low-level data type. Defines which value getters / setters are valid. - * if dtype == InternalDataType::Int, then getInt() and setInt(...) are valid - * if dtype == InternalDataType::String, then getString() and setString(...) are valid. Etc. - */ - virtual std::unique_ptr createVariable(Variable::InternalDataType dtype, Variable::AttributeTypeSet attributes) = 0; // factory method - virtual bool add(std::unique_ptr variable) = 0; - - virtual size_t size() const = 0; - virtual Variable *getVariable(size_t i) const = 0; - virtual Variable *getVariable(const ComponentId& component, const char *variableName) const = 0; + 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 VariableContainerVolatile : public VariableContainer, public MemoryManaged { +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: - VariableContainerVolatile(const char *filename, bool accessible); - ~VariableContainerVolatile(); - - //VariableContainer definitions - bool load() override; - bool save() override; - std::unique_ptr createVariable(Variable::InternalDataType dtype, Variable::AttributeTypeSet attributes) override; - bool add(std::unique_ptr config) override; - size_t size() const override; - Variable *getVariable(size_t i) const override; - Variable *getVariable(const ComponentId& component, const char *variableName) const override; -}; + VariableContainerOwning(); + ~VariableContainerOwning(); -std::unique_ptr makeVariableContainerVolatile(const char *filename, bool accessible); + 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 diff --git a/src/MicroOcpp/Model/Variables/VariableService.cpp b/src/MicroOcpp/Model/Variables/VariableService.cpp index c196f805..dcdc499d 100644 --- a/src/MicroOcpp/Model/Variables/VariableService.cpp +++ b/src/MicroOcpp/Model/Variables/VariableService.cpp @@ -26,14 +26,20 @@ namespace MicroOcpp { template -VariableValidator::VariableValidator(const ComponentId& component, const char *name, std::function validate) : - MemoryManaged("v201.Variables.VariableValidator.", name), component(component), name(name), validate(validate) { +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 (auto& validator : collection) { + for (size_t i = 0; i < collection.size(); i++) { + auto& validator = collection[i]; if (!strcmp(name, validator.name) && component.equals(validator.component)) { return &validator; } @@ -53,108 +59,90 @@ VariableValidator *VariableService::getValidatorString(const Compon return getVariableValidator(validatorString, component, name); } -std::unique_ptr VariableService::createContainer(const char *filename, bool accessible) const { - //create non-persistent Variable store (i.e. lives only in RAM) if - // - Flash FS usage is switched off OR - // - Filename starts with "/volatile" -// if (!filesystem || -// !strncmp(filename, MO_VARIABLE_VOLATILE, strlen(MO_VARIABLE_VOLATILE))) { - return makeVariableContainerVolatile(filename, accessible); -// } else { -// //create persistent Variable store. This is the normal case -// return makeVariableContainerFlash(filesystem, filename, accessible); -// } -} - -void VariableService::addContainer(std::shared_ptr container) { - containers.push_back(std::move(container)); +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]; } -std::shared_ptr VariableService::getContainer(const char *filename) { - for (auto& container : containers) { - if (!strcmp(filename, container->getFilename())) { - return container; - } - } - return nullptr; +void VariableService::addContainer(VariableContainer *container) { + containers.push_back(container); } template -bool registerVariableValidator(Vector>& collection, const ComponentId& component, const char *name, std::function validate) { +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); + collection.emplace_back(component, name, validate, userPtr); return true; } template <> -bool VariableService::registerValidator(const ComponentId& component, const char *name, std::function validate) { - return registerVariableValidator(validatorInt, component, name, validate); +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, std::function validate) { - return registerVariableValidator(validatorBool, component, name, validate); +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, std::function validate) { - return registerVariableValidator(validatorString, component, name, validate); +bool VariableService::registerValidator(const ComponentId& component, const char *name, bool (*validate)(const char*, void*), void *userPtr) { + return registerVariableValidator(validatorString, component, name, validate, userPtr); } -VariableContainer *VariableService::declareContainer(const char *filename, bool accessible) { - - auto container = getContainer(filename); - - if (!container) { - MO_DBG_DEBUG("init new configurations container: %s", filename); - - container = createContainer(filename, accessible); - if (!container) { - MO_DBG_ERR("OOM"); - return nullptr; - } - containers.push_back(container); - } +Variable *VariableService::getVariable(const ComponentId& component, const char *name) { - if (container->isAccessible() != accessible) { - MO_DBG_ERR("%s: conflicting accessibility declarations (expect %s)", filename, container->isAccessible() ? "accessible" : "inaccessible"); + if (auto variable = getContainerInternalByVariable(component, name).getVariable(component, name)) { + return variable; } - return container.get(); -} - -Variable *VariableService::getVariable(Variable::InternalDataType type, const ComponentId& component, const char *name, bool accessible) { - for (auto& container : containers) { + 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)) { - if (variable->isDetached()) { - continue; - } - if (variable->getInternalDataType() != type) { - MO_DBG_ERR("conflicting type for %s - remove old variable", name); - variable->detach(); - continue; - } - if (container->isAccessible() != accessible) { - MO_DBG_ERR("conflicting accessibility for %s", name); - } return variable; } } + return nullptr; } VariableService::VariableService(Context& context, std::shared_ptr filesystem) : MemoryManaged("v201.Variables.VariableService"), context(context), filesystem(filesystem), - containers(makeVector>(getMemoryTag())), + 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] () { @@ -183,11 +171,15 @@ bool loadVariableFactoryDefault(Variable& variable, const char *fac return variable.setString(factoryDef); } -void loadVariableCharacteristics(Variable& variable, Variable::Mutability mutability, bool rebootRequired, Variable::InternalDataType defaultDataType) { +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(); } @@ -216,17 +208,13 @@ template<> Variable::InternalDataType getInternalDataType() {return Variab template<> Variable::InternalDataType getInternalDataType() {return Variable::InternalDataType::String;} template -Variable *VariableService::declareVariable(const ComponentId& component, const char *name, T factoryDefault, const char *containerPath, Variable::Mutability mutability, Variable::AttributeTypeSet attributes, bool rebootRequired, bool accessible) { +Variable *VariableService::declareVariable(const ComponentId& component, const char *name, T factoryDefault, Variable::Mutability mutability, bool persistent, Variable::AttributeTypeSet attributes, bool rebootRequired) { - auto res = getVariable(getInternalDataType(), component, name, accessible); + auto res = getVariable(component, name); if (!res) { - auto container = declareContainer(containerPath, accessible); - if (!container) { - return nullptr; - } - - auto variable = container->createVariable(getInternalDataType(), attributes); + auto variable = makeVariable(getInternalDataType(), attributes); if (!variable) { + MO_DBG_ERR("OOM"); return nullptr; } @@ -239,24 +227,32 @@ Variable *VariableService::declareVariable(const ComponentId& component, const c res = variable.get(); - if (!container->add(std::move(variable))) { + if (!getContainerInternalByVariable(component, name).add(std::move(variable))) { return nullptr; } } - loadVariableCharacteristics(*res, mutability, rebootRequired, getInternalDataType()); + loadVariableCharacteristics(*res, mutability, persistent, rebootRequired, getInternalDataType()); return res; } -template Variable *VariableService::declareVariable(const ComponentId&, const char*, int, const char*, Variable::Mutability, Variable::AttributeTypeSet, bool, bool); -template Variable *VariableService::declareVariable(const ComponentId&, const char*, bool, const char*, Variable::Mutability, Variable::AttributeTypeSet, bool, bool); -template Variable *VariableService::declareVariable(const ComponentId&, const char*, const char*, const char*, Variable::Mutability, Variable::AttributeTypeSet, bool, bool); +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::commit() { bool success = true; - for (auto& container : containers) { - if (!container->save()) { + for (size_t i = 0; i < containers.size(); i++) { + if (!containers[i]->commit()) { success = false; } } @@ -269,11 +265,9 @@ SetVariableStatus VariableService::setVariable(Variable::AttributeType attrType, Variable *variable = nullptr; bool foundComponent = false; - for (const auto& container : containers) { - if (!container->isAccessible()) { - // container intended for internal use only - continue; - } + 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); @@ -406,7 +400,9 @@ SetVariableStatus VariableService::setVariable(Variable::AttributeType attrType, GetVariableStatus VariableService::getVariable(Variable::AttributeType attrType, const ComponentId& component, const char *variableName, Variable **result) { bool foundComponent = false; - for (const auto& container : containers) { + 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); @@ -446,11 +442,9 @@ GenericDeviceModelStatus VariableService::getBaseReport(int requestId, ReportBas Vector variables = makeVector(getMemoryTag()); - for (const auto& container : containers) { - if (!container->isAccessible()) { - // container intended for internal use only - continue; - } + 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); diff --git a/src/MicroOcpp/Model/Variables/VariableService.h b/src/MicroOcpp/Model/Variables/VariableService.h index 0fff37d3..d4621053 100644 --- a/src/MicroOcpp/Model/Variables/VariableService.h +++ b/src/MicroOcpp/Model/Variables/VariableService.h @@ -12,7 +12,6 @@ #include #include #include -#include #include @@ -23,16 +22,12 @@ #include #include -#ifndef MO_VARIABLE_FN -#define MO_VARIABLE_FN (MO_FILENAME_PREFIX "ocpp-vars.jsn") +#ifndef MO_VARIABLESTORE_FN_PREFIX +#define MO_VARIABLESTORE_FN_PREFIX (MO_FILENAME_PREFIX "ocpp-vars-") #endif -#ifndef MO_VARIABLE_VOLATILE -#define MO_VARIABLE_VOLATILE "/volatile" -#endif - -#ifndef MO_VARIABLE_INTERNAL_FN -#define MO_VARIABLE_INTERNAL_FN (MO_FILENAME_PREFIX "mo-vars.jsn") +#ifndef MO_VARIABLESTORE_FN_SUFFIX +#define MO_VARIABLESTORE_FN_SUFFIX ".jsn" #endif namespace MicroOcpp { @@ -41,17 +36,26 @@ template struct VariableValidator : public MemoryManaged { ComponentId component; const char *name; - std::function validate; - VariableValidator(const ComponentId& component, const char *name, std::function validate); + 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; + Vector containers; + VariableContainerNonOwning containerExternal; + VariableContainerOwning containersInternal [MO_VARIABLESTORE_BUCKETS]; + VariableContainerOwning& getContainerInternalByVariable(const ComponentId& component, const char *name); Vector> validatorInt; Vector> validatorBool; @@ -60,27 +64,25 @@ class VariableService : public MemoryManaged { VariableValidator *getValidatorInt(const ComponentId& component, const char *name); VariableValidator *getValidatorBool(const ComponentId& component, const char *name); VariableValidator *getValidatorString(const ComponentId& component, const char *name); - - std::unique_ptr createContainer(const char *filename, bool accessible) const; - - VariableContainer *declareContainer(const char *filename, bool accessible); - - Variable *getVariable(Variable::InternalDataType type, const ComponentId& component, const char *name, bool accessible); - 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, const char *containerPath = MO_VARIABLE_FN, Variable::Mutability mutability = Variable::Mutability::ReadWrite, Variable::AttributeTypeSet attributes = Variable::AttributeTypeSet(), bool rebootRequired = false, bool accessible = true); + 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 commit(); + bool addVariable(Variable *variable); //Add Variable without transferring ownership + bool addVariable(std::unique_ptr variable); //Add Variable and transfer ownership - void addContainer(std::shared_ptr container); + //Get Variable. If not existent, return nullptr + Variable *getVariable(const ComponentId& component, const char *name); + + bool commit(); - std::shared_ptr getContainer(const char *filename); + void addContainer(VariableContainer *container); template - bool registerValidator(const ComponentId& component, const char *name, std::function validate); + 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); diff --git a/src/MicroOcpp/Operations/RequestStartTransaction.cpp b/src/MicroOcpp/Operations/RequestStartTransaction.cpp index 1b6c0b8f..1b37f051 100644 --- a/src/MicroOcpp/Operations/RequestStartTransaction.cpp +++ b/src/MicroOcpp/Operations/RequestStartTransaction.cpp @@ -43,7 +43,7 @@ void RequestStartTransaction::processReq(JsonObject payload) { return; } - status = rcService.requestStartTransaction(evseId, remoteStartId, idToken, transaction); + status = rcService.requestStartTransaction(evseId, remoteStartId, idToken, transactionId, sizeof(transactionId)); } std::unique_ptr RequestStartTransaction::createConf(){ diff --git a/src/MicroOcpp/Operations/RequestStartTransaction.h b/src/MicroOcpp/Operations/RequestStartTransaction.h index dad7d5ae..4ee19761 100644 --- a/src/MicroOcpp/Operations/RequestStartTransaction.h +++ b/src/MicroOcpp/Operations/RequestStartTransaction.h @@ -26,6 +26,7 @@ class RequestStartTransaction : public Operation, public MemoryManaged { RequestStartStopStatus status; std::shared_ptr transaction; + char transactionId [MO_TXID_LEN_MAX + 1] = {'\0'}; const char *errorCode = nullptr; public: diff --git a/src/MicroOcpp/Operations/TransactionEvent.cpp b/src/MicroOcpp/Operations/TransactionEvent.cpp index 16df68cf..12d91afb 100644 --- a/src/MicroOcpp/Operations/TransactionEvent.cpp +++ b/src/MicroOcpp/Operations/TransactionEvent.cpp @@ -14,7 +14,7 @@ using namespace MicroOcpp::Ocpp201; using MicroOcpp::JsonDoc; -TransactionEvent::TransactionEvent(Model& model, std::shared_ptr txEvent) +TransactionEvent::TransactionEvent(Model& model, TransactionEventData *txEvent) : MemoryManaged("v201.Operation.", "TransactionEvent"), model(model), txEvent(txEvent) { } @@ -51,97 +51,18 @@ std::unique_ptr TransactionEvent::createReq() { auto doc = makeJsonDoc(getMemoryTag(), capacity); JsonObject payload = doc->to(); - const char *eventType = ""; - switch (txEvent->eventType) { - case TransactionEventData::Type::Ended: - eventType = "Ended"; - break; - case TransactionEventData::Type::Started: - eventType = "Started"; - break; - case TransactionEventData::Type::Updated: - eventType = "Updated"; - break; - } - - payload["eventType"] = eventType; + payload["eventType"] = serializeTransactionEventType(txEvent->eventType); char timestamp [JSONDATE_LENGTH + 1]; txEvent->timestamp.toJsonString(timestamp, JSONDATE_LENGTH + 1); payload["timestamp"] = timestamp; - const char *triggerReason = ""; - switch(txEvent->triggerReason) { - case TransactionEventTriggerReason::UNDEFINED: - MO_DBG_ERR("internal error"); - break; - case TransactionEventTriggerReason::Authorized: - triggerReason = "Authorized"; - break; - case TransactionEventTriggerReason::CablePluggedIn: - triggerReason = "CablePluggedIn"; - break; - case TransactionEventTriggerReason::ChargingRateChanged: - triggerReason = "ChargingRateChanged"; - break; - case TransactionEventTriggerReason::ChargingStateChanged: - triggerReason = "ChargingStateChanged"; - break; - case TransactionEventTriggerReason::Deauthorized: - triggerReason = "Deauthorized"; - break; - case TransactionEventTriggerReason::EnergyLimitReached: - triggerReason = "EnergyLimitReached"; - break; - case TransactionEventTriggerReason::EVCommunicationLost: - triggerReason = "EVCommunicationLost"; - break; - case TransactionEventTriggerReason::EVConnectTimeout: - triggerReason = "EVConnectTimeout"; - break; - case TransactionEventTriggerReason::MeterValueClock: - triggerReason = "MeterValueClock"; - break; - case TransactionEventTriggerReason::MeterValuePeriodic: - triggerReason = "MeterValuePeriodic"; - break; - case TransactionEventTriggerReason::TimeLimitReached: - triggerReason = "TimeLimitReached"; - break; - case TransactionEventTriggerReason::Trigger: - triggerReason = "Trigger"; - break; - case TransactionEventTriggerReason::UnlockCommand: - triggerReason = "UnlockCommand"; - break; - case TransactionEventTriggerReason::StopAuthorized: - triggerReason = "StopAuthorized"; - break; - case TransactionEventTriggerReason::EVDeparted: - triggerReason = "EVDeparted"; - break; - case TransactionEventTriggerReason::EVDetected: - triggerReason = "EVDetected"; - break; - case TransactionEventTriggerReason::RemoteStop: - triggerReason = "RemoteStop"; - break; - case TransactionEventTriggerReason::RemoteStart: - triggerReason = "RemoteStart"; - break; - case TransactionEventTriggerReason::AbnormalCondition: - triggerReason = "AbnormalCondition"; - break; - case TransactionEventTriggerReason::SignedDataReceived: - triggerReason = "SignedDataReceived"; - break; - case TransactionEventTriggerReason::ResetCommand: - triggerReason = "ResetCommand"; - break; + if (serializeTransactionEventTriggerReason(txEvent->triggerReason)) { + payload["triggerReason"] = serializeTransactionEventTriggerReason(txEvent->triggerReason); + } else { + MO_DBG_ERR("serialization error"); } - payload["triggerReason"] = triggerReason; - payload["seqNo"] = txEvent->seqNo; if (txEvent->offline) { @@ -163,96 +84,13 @@ std::unique_ptr TransactionEvent::createReq() { JsonObject transactionInfo = payload.createNestedObject("transactionInfo"); transactionInfo["transactionId"] = txEvent->transaction->transactionId; - const char *chargingState = nullptr; - switch (txEvent->chargingState) { - case TransactionEventData::ChargingState::UNDEFINED: - // optional, okay - break; - case TransactionEventData::ChargingState::Charging: - chargingState = "Charging"; - break; - case TransactionEventData::ChargingState::EVConnected: - chargingState = "EVConnected"; - break; - case TransactionEventData::ChargingState::SuspendedEV: - chargingState = "SuspendedEV"; - break; - case TransactionEventData::ChargingState::SuspendedEVSE: - chargingState = "SuspendedEVSE"; - break; - case TransactionEventData::ChargingState::Idle: - chargingState = "Idle"; - break; - } - if (chargingState) { // optional - transactionInfo["chargingState"] = chargingState; + if (serializeTransactionEventChargingState(txEvent->chargingState)) { // optional + transactionInfo["chargingState"] = serializeTransactionEventChargingState(txEvent->chargingState); } - const char *stoppedReason = nullptr; - switch (txEvent->transaction->stopReason) { - case Transaction::StopReason::UNDEFINED: - // optional, okay - break; - case Transaction::StopReason::Local: - // omit reason Local - break; - case Transaction::StopReason::DeAuthorized: - stoppedReason = "DeAuthorized"; - break; - case Transaction::StopReason::EmergencyStop: - stoppedReason = "EmergencyStop"; - break; - case Transaction::StopReason::EnergyLimitReached: - stoppedReason = "EnergyLimitReached"; - break; - case Transaction::StopReason::EVDisconnected: - stoppedReason = "EVDisconnected"; - break; - case Transaction::StopReason::GroundFault: - stoppedReason = "GroundFault"; - break; - case Transaction::StopReason::ImmediateReset: - stoppedReason = "ImmediateReset"; - break; - case Transaction::StopReason::LocalOutOfCredit: - stoppedReason = "LocalOutOfCredit"; - break; - case Transaction::StopReason::MasterPass: - stoppedReason = "MasterPass"; - break; - case Transaction::StopReason::Other: - stoppedReason = "Other"; - break; - case Transaction::StopReason::OvercurrentFault: - stoppedReason = "OvercurrentFault"; - break; - case Transaction::StopReason::PowerLoss: - stoppedReason = "PowerLoss"; - break; - case Transaction::StopReason::PowerQuality: - stoppedReason = "PowerQuality"; - break; - case Transaction::StopReason::Reboot: - stoppedReason = "Reboot"; - break; - case Transaction::StopReason::Remote: - stoppedReason = "Remote"; - break; - case Transaction::StopReason::SOCLimitReached: - stoppedReason = "SOCLimitReached"; - break; - case Transaction::StopReason::StoppedByEV: - stoppedReason = "StoppedByEV"; - break; - case Transaction::StopReason::TimeLimitReached: - stoppedReason = "TimeLimitReached"; - break; - case Transaction::StopReason::Timeout: - stoppedReason = "Timeout"; - break; - } - if (stoppedReason) { // optional - transactionInfo["stoppedReason"] = stoppedReason; + if (txEvent->transaction->stoppedReason != Transaction::StoppedReason::Local && + serializeTransactionStoppedReason(txEvent->transaction->stoppedReason)) { // optional + transactionInfo["stoppedReason"] = serializeTransactionStoppedReason(txEvent->transaction->stoppedReason); } if (txEvent->remoteStartId >= 0) { diff --git a/src/MicroOcpp/Operations/TransactionEvent.h b/src/MicroOcpp/Operations/TransactionEvent.h index 0df02ba0..af8bbf86 100644 --- a/src/MicroOcpp/Operations/TransactionEvent.h +++ b/src/MicroOcpp/Operations/TransactionEvent.h @@ -22,12 +22,12 @@ class TransactionEventData; class TransactionEvent : public Operation, public MemoryManaged { private: Model& model; - std::shared_ptr txEvent; + TransactionEventData *txEvent; const char *errorCode = nullptr; public: - TransactionEvent(Model& model, std::shared_ptr txEvent); + TransactionEvent(Model& model, TransactionEventData *txEvent); const char* getOperationType() override; diff --git a/tests/LocalAuthList.cpp b/tests/LocalAuthList.cpp index 86ac08a6..daca34c4 100644 --- a/tests/LocalAuthList.cpp +++ b/tests/LocalAuthList.cpp @@ -265,8 +265,6 @@ TEST_CASE( "LocalAuth" ) { REQUIRE( connector->getStatus() == ChargePointStatus_Available ); - unsigned long t_before = mocpp_tick_ms(); - beginTransaction("unknownIdTag"); loop(); diff --git a/tests/Reset.cpp b/tests/Reset.cpp index c5dfcd5b..1390b336 100644 --- a/tests/Reset.cpp +++ b/tests/Reset.cpp @@ -50,6 +50,17 @@ TEST_CASE( "Reset" ) { 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}; diff --git a/tests/Transactions.cpp b/tests/Transactions.cpp index aa86df32..4e056543 100644 --- a/tests/Transactions.cpp +++ b/tests/Transactions.cpp @@ -48,6 +48,17 @@ TEST_CASE( "Transactions" ) { 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(); @@ -128,6 +139,10 @@ TEST_CASE( "Transactions" ) { 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") { @@ -164,17 +179,13 @@ TEST_CASE( "Transactions" ) { MO_MEM_PRINT_STATS(); - auto trackTx = context->getModel().getTransactionService()->getEvse(1)->getTransaction(); - MO_MEM_RESET(); setConnectorPluggedInput([] () {return false;}); loop(); REQUIRE( context->getModel().getTransactionService()->getEvse(1)->getTransaction() == nullptr ); - REQUIRE( trackTx->stopped ); REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); - trackTx.reset(); MO_DBG_INFO("Memory requirements UC E06:"); MO_MEM_PRINT_STATS(); @@ -206,10 +217,10 @@ TEST_CASE( "Transactions" ) { getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStartPoint", "")->setString("PowerPathClosed"); getOcppContext()->getModel().getVariableService()->declareVariable("TxCtrlr", "TxStopPoint", "")->setString("PowerPathClosed"); - getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "SampledDataTxStartedMeasurands", "")->setString("Energy.Active.Import.Register"); - getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "SampledDataTxUpdatedMeasurands", "")->setString("Power.Active.Import"); + 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", "SampledDataTxEndedMeasurands", "")->setString("Current.Import"); + getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "TxEndedMeasurands", "")->setString("Current.Import"); getOcppContext()->getModel().getVariableService()->declareVariable("SampledDataCtrlr", "TxEndededInterval", 0)->setInt(100); setConnectorPluggedInput([] () {return false;}); @@ -264,6 +275,459 @@ TEST_CASE( "Transactions" ) { 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); + } + + 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(); diff --git a/tests/Variables.cpp b/tests/Variables.cpp index 1d6399c4..158140fc 100644 --- a/tests/Variables.cpp +++ b/tests/Variables.cpp @@ -34,24 +34,14 @@ TEST_CASE( "Variable" ) { FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); SECTION("Basic container operations"){ - std::unique_ptr container; - - SECTION("Volatile storage") { - container = makeVariableContainerVolatile(MO_VARIABLE_VOLATILE "/volatile1", true); - } - -#if 0 - SECTION("Persistent storage") { - container = makeVariableContainerFlash(filesystem, MO_FILENAME_PREFIX "persistent1.jsn", true); - } -#endif + 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 = container->createVariable(Variable::InternalDataType::Int, attrs); + auto configFirst = makeVariable(Variable::InternalDataType::Int, attrs); configFirst->setName("cFirst"); configFirst->setComponentId("mComponent"); auto configFirstRaw = configFirst.get(); @@ -61,14 +51,14 @@ TEST_CASE( "Variable" ) { REQUIRE( container->getVariable((size_t) 0) == configFirstRaw); //add one config of each type - auto cInt = container->createVariable(Variable::InternalDataType::Int, attrs); + auto cInt = makeVariable(Variable::InternalDataType::Int, attrs); cInt->setName("cInt"); cInt->setComponentId("mComponent"); - auto cBool = container->createVariable(Variable::InternalDataType::Bool, attrs); + auto cBool = makeVariable(Variable::InternalDataType::Bool, attrs); cBool->setName("cBool"); cBool->setComponentId("mComponent"); auto cBoolRaw = cBool.get(); - auto cString = container->createVariable(Variable::InternalDataType::String, attrs); + auto cString = makeVariable(Variable::InternalDataType::String, attrs); cString->setName("cString"); cString->setComponentId("mComponent"); @@ -82,34 +72,46 @@ TEST_CASE( "Variable" ) { REQUIRE( container->getVariable(cBoolRaw->getComponentId(), cBoolRaw->getName()) == cBoolRaw); } -#if 0 SECTION("Persistency on filesystem") { - auto container = makeVariableContainerFlash(filesystem, MO_FILENAME_PREFIX "persistent1.jsn", true); + 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 = container->createVariable(Variable::InternalDataType::String, "cString"); + 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->save() ); //store + REQUIRE( container->commit() ); //store container.reset(); //destroy //...load again - auto container2 = makeVariableContainerFlash(filesystem, MO_FILENAME_PREFIX "persistent1.jsn", true); + 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 cString2 = container2->getVariable("cString"); - REQUIRE( cString2 != nullptr ); - REQUIRE( !strcmp(cString2->getString(), "mValue") ); + auto cString3 = container2->getVariable("mComponent", "cString"); + REQUIRE( cString3 != nullptr ); + REQUIRE( !strcmp(cString3->getString(), "mValue") ); } -#endif LoopbackConnection loopback; //initialize Context with dummy socket mocpp_set_timer(custom_timer_cb); @@ -154,16 +156,6 @@ TEST_CASE( "Variable" ) { auto cInt3 = vs->declareVariable("mComponent", "cInt", -1); REQUIRE( cInt3 == cInt2 ); - //declare config twice but in different container - auto cInt4 = vs->declareVariable("mComponent", "cInt", -1, MO_VARIABLE_VOLATILE "/volatile2"); - REQUIRE( cInt4 == cInt2 ); - - //declare config twice but with conflicting type (will supersede old type because to simplify FW upgrades) - auto cNewType = vs->declareVariable("mComponent", "cInt", "mValue2"); - REQUIRE( cNewType != cInt2 ); - REQUIRE( cInt2->isDetached() ); // Container should not store this - REQUIRE( !strcmp(cNewType->getString(), "mValue2") ); - #if 0 //store, destroy, reload REQUIRE( configuration_save() ); @@ -195,46 +187,32 @@ TEST_CASE( "Variable" ) { //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; - bool isAccessible = true; - auto cInt6 = vs->declareVariable("mComponent", "cInt", 42, MO_VARIABLE_VOLATILE, mutability, attrs, rebootRequired, isAccessible); + 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, MO_VARIABLE_VOLATILE, mutability, attrs, rebootRequired, isAccessible); + 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, MO_VARIABLE_VOLATILE, mutability, attrs, rebootRequired, isAccessible); + auto cInt7 = vs->declareVariable("mComponent", "cInt", 42, mutability, persistent, attrs, rebootRequired); REQUIRE( cInt7->getMutability() == Variable::Mutability::ReadOnly ); + REQUIRE( cInt6->isPersistent() ); REQUIRE( cInt7->isRebootRequired() ); - - //accessibility cannot be changed after first initialization - isAccessible = false; - vs->declareVariable("mComponent", "cInt", 42, MO_VARIABLE_VOLATILE, mutability, attrs, rebootRequired, isAccessible); - vs->declareVariable("mComponent", "cInt2", 42, MO_VARIABLE_VOLATILE, mutability, attrs, rebootRequired, isAccessible); - Variable *result = nullptr; - REQUIRE( vs->getVariable(Variable::AttributeType::Actual, "mComponent", "cInt", &result) == GetVariableStatus::Accepted ); - REQUIRE( result != nullptr ); - result = nullptr; - REQUIRE( vs->getVariable(Variable::AttributeType::Actual, "mComponent", "cInt2", &result) == GetVariableStatus::Accepted ); - REQUIRE( result != nullptr ); - - //create config in hidden container - isAccessible = false; - auto cHidden = vs->declareVariable("mComponent", "cHidden", 42, MO_VARIABLE_VOLATILE "/hidden.json", mutability, attrs, rebootRequired, isAccessible); - (void)cHidden; - result = nullptr; - REQUIRE( vs->getVariable(Variable::AttributeType::Actual, "mComponent", "cHidden", &result) == GetVariableStatus::Accepted ); - REQUIRE( result != nullptr ); } #if 0 @@ -515,7 +493,7 @@ TEST_CASE( "Variable" ) { mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); auto vs = getOcppContext()->getModel().getVariableService(); - auto varString = vs->declareVariable("mComponent", "mString", "mValue", MO_VARIABLE_VOLATILE); + auto varString = vs->declareVariable("mComponent", "mString", "mValue"); REQUIRE( varString != nullptr ); REQUIRE( !strcmp(varString->getString(), "mValue") ); @@ -564,7 +542,7 @@ TEST_CASE( "Variable" ) { mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); auto vs = getOcppContext()->getModel().getVariableService(); - auto varString = vs->declareVariable("mComponent", "mString", "", MO_VARIABLE_VOLATILE); + auto varString = vs->declareVariable("mComponent", "mString", ""); REQUIRE( varString != nullptr ); REQUIRE( !strcmp(varString->getString(), "") ); @@ -612,7 +590,7 @@ TEST_CASE( "Variable" ) { mocpp_initialize(loopback, ChargerCredentials(), filesystem, false, ProtocolVersion(2,0,1)); auto vs = getOcppContext()->getModel().getVariableService(); - auto varString = vs->declareVariable("mComponent", "mString", "", MO_VARIABLE_VOLATILE); + auto varString = vs->declareVariable("mComponent", "mString", ""); REQUIRE( varString != nullptr ); REQUIRE( !strcmp(varString->getString(), "") ); diff --git a/tests/benchmarks/scripts/measure_heap.py b/tests/benchmarks/scripts/measure_heap.py index 3c15b464..8ae75ddc 100644 --- a/tests/benchmarks/scripts/measure_heap.py +++ b/tests/benchmarks/scripts/measure_heap.py @@ -181,7 +181,7 @@ def setup_simulator(): 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.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'))