diff --git a/CHANGELOG.md b/CHANGELOG.md index f99c3f42..367a216a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - Provide ChargePointStatus in API ([#309](https://github.com/matth-x/MicroOcpp/pull/309)) - Built-in OTA over FTP ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) - Built-in Diagnostics over FTP ([#313](https://github.com/matth-x/MicroOcpp/pull/313)) +- Error `severity` mechanism ([#331](https://github.com/matth-x/MicroOcpp/pull/331)) +- Build flag `MO_REPORT_NOERROR` to report error recovery ([#331](https://github.com/matth-x/MicroOcpp/pull/331)) ### Removed diff --git a/CMakeLists.txt b/CMakeLists.txt index 4817e066..a034a1ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,7 @@ set(MO_SRC_UNIT tests/Transactions.cpp tests/Certificates.cpp tests/FirmwareManagement.cpp + tests/ChargePointError.cpp ) add_executable(mo_unit_tests @@ -191,6 +192,7 @@ target_compile_definitions(mo_unit_tests PUBLIC MO_MaxChargingProfilesInstalled=3 MO_ENABLE_CERT_MGMT=1 MO_ENABLE_CONNECTOR_LOCK=1 + MO_REPORT_NOERROR=1 ) target_compile_options(mo_unit_tests PUBLIC diff --git a/src/MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h b/src/MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h index ae48c09b..6c7b04f2 100644 --- a/src/MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h +++ b/src/MicroOcpp/Model/ConnectorBase/ChargePointErrorData.h @@ -5,11 +5,14 @@ #ifndef MO_CHARGEPOINTERRORCODE_H #define MO_CHARGEPOINTERRORCODE_H +#include + namespace MicroOcpp { struct ErrorData { bool isError = false; //if any error information is set bool isFaulted = false; //if this is a severe error and the EVSE should go into the faulted state + uint8_t severity = 1; //severity: don't send less severe errors during highly severe error condition const char *errorCode = nullptr; //see ChargePointErrorCode (p. 76/77) for possible values const char *info = nullptr; //Additional free format information related to the error const char *vendorId = nullptr; //vendor-specific implementation identifier diff --git a/src/MicroOcpp/Model/ConnectorBase/Connector.cpp b/src/MicroOcpp/Model/ConnectorBase/Connector.cpp index d284b8dd..18b1ff8f 100644 --- a/src/MicroOcpp/Model/ConnectorBase/Connector.cpp +++ b/src/MicroOcpp/Model/ConnectorBase/Connector.cpp @@ -408,29 +408,52 @@ void Connector::loop() { freeVendTrackPlugged = connectorPluggedInput(); } - auto status = getStatus(); + ErrorData errorData {nullptr}; + errorData.severity = 0; + int errorDataIndex = -1; if (model.getVersion().major == 1) { //OCPP 1.6: use StatusNotification to send error codes + + if (reportedErrorIndex >= 0) { + auto error = errorDataInputs[reportedErrorIndex].operator()(); + if (error.isError) { + errorData = error; + errorDataIndex = reportedErrorIndex; + } + } + for (auto i = std::min(errorDataInputs.size(), trackErrorDataInputs.size()); i >= 1; i--) { auto index = i - 1; - auto error = errorDataInputs[index].operator()(); - if (error.isError && !trackErrorDataInputs[index]) { + ErrorData error {nullptr}; + if ((int)index != errorDataIndex) { + error = errorDataInputs[index].operator()(); + } else { + error = errorData; + } + if (error.isError && !trackErrorDataInputs[index] && error.severity >= errorData.severity) { //new error - auto statusNotification = makeRequest( - new Ocpp16::StatusNotification(connectorId, status, model.getClock().now(), error)); - statusNotification->setTimeout(0); - context.initiateRequest(std::move(statusNotification)); - - currentStatus = status; - reportedStatus = status; - trackErrorDataInputs[index] = true; + errorData = error; + errorDataIndex = index; + } else if (error.isError && error.severity > errorData.severity) { + errorData = error; + errorDataIndex = index; } else if (!error.isError && trackErrorDataInputs[index]) { //reset error trackErrorDataInputs[index] = false; } } - } + + if (errorDataIndex != reportedErrorIndex) { + if (errorDataIndex >= 0 || MO_REPORT_NOERROR) { + reportedStatus = ChargePointStatus_UNDEFINED; //trigger sending currentStatus again with code NoError + } else { + reportedErrorIndex = -1; + } + } + } //if (model.getVersion().major == 1) + + auto status = getStatus(); if (status != currentStatus) { MO_DBG_DEBUG("Status changed %s -> %s %s", @@ -446,6 +469,10 @@ void Connector::loop() { (minimumStatusDurationInt->getInt() <= 0 || //MinimumStatusDuration disabled mocpp_tick_ms() - t_statusTransition >= ((unsigned long) minimumStatusDurationInt->getInt()) * 1000UL)) { reportedStatus = currentStatus; + reportedErrorIndex = errorDataIndex; + if (errorDataIndex >= 0) { + trackErrorDataInputs[errorDataIndex] = true; + } Timestamp reportedTimestamp = model.getClock().now(); reportedTimestamp -= (mocpp_tick_ms() - t_statusTransition) / 1000UL; @@ -456,7 +483,7 @@ void Connector::loop() { new Ocpp201::StatusNotification(connectorId, reportedStatus, reportedTimestamp)) : #endif //MO_ENABLE_V201 makeRequest( - new Ocpp16::StatusNotification(connectorId, reportedStatus, reportedTimestamp, getErrorCode())); + new Ocpp16::StatusNotification(connectorId, reportedStatus, reportedTimestamp, errorData)); statusNotification->setTimeout(0); context.initiateRequest(std::move(statusNotification)); @@ -477,8 +504,8 @@ bool Connector::isFaulted() { } const char *Connector::getErrorCode() { - for (auto i = errorDataInputs.size(); i >= 1; i--) { - auto error = errorDataInputs[i-1].operator()(); + if (reportedErrorIndex >= 0) { + auto error = errorDataInputs[reportedErrorIndex].operator()(); if (error.isError && error.errorCode) { return error.errorCode; } diff --git a/src/MicroOcpp/Model/ConnectorBase/Connector.h b/src/MicroOcpp/Model/ConnectorBase/Connector.h index 7da8c4d4..08828f2d 100644 --- a/src/MicroOcpp/Model/ConnectorBase/Connector.h +++ b/src/MicroOcpp/Model/ConnectorBase/Connector.h @@ -16,6 +16,10 @@ #include #include +#ifndef MO_REPORT_NOERROR +#define MO_REPORT_NOERROR 0 +#endif + namespace MicroOcpp { class Context; @@ -41,6 +45,7 @@ class Connector { std::function evseReadyInput; std::vector> errorDataInputs; std::vector trackErrorDataInputs; + int reportedErrorIndex = -1; //last reported error bool isFaulted(); const char *getErrorCode(); diff --git a/tests/ChargePointError.cpp b/tests/ChargePointError.cpp new file mode 100644 index 00000000..6ca2498c --- /dev/null +++ b/tests/ChargePointError.cpp @@ -0,0 +1,340 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include "./catch2/catch.hpp" +#include "./helpers/testHelper.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#define BASE_TIME "2023-01-01T00:00:00.000Z" +#define BASE_TIME_1H "2023-01-01T01:00:00.000Z" +#define FTP_URL "ftps://localhost/firmware.bin" + +#define ERROR_INFO_EXAMPLE "error description" +#define ERROR_INFO_LOW_1 "low severity 1" +#define ERROR_INFO_LOW_2 "low severity 2" +#define ERROR_INFO_HIGH "high severity" + +#define ERROR_VENDOR_ID "mVendorId" +#define ERROR_VENDOR_CODE "mVendorErrorCode" + +using namespace MicroOcpp; + +TEST_CASE( "ChargePointError" ) { + printf("\nRun %s\n", "ChargePointError"); + + //clean state + auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail); + FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;}); + + //initialize Context with dummy socket + LoopbackConnection loopback; + + mocpp_set_timer(custom_timer_cb); + + mocpp_initialize(loopback, ChargerCredentials("test-runner")); + auto& model = getOcppContext()->getModel(); + auto fwService = getFirmwareService(); + SECTION("FirmwareService initialized") { + REQUIRE(fwService != nullptr); + } + + model.getClock().setTime(BASE_TIME); + + loop(); + + SECTION("Err and resolve (soft error)") { + + bool errorCondition = false; + + addErrorDataInput([&errorCondition] () -> ErrorData { + if (errorCondition) { + ErrorData error = "OtherError"; + error.isFaulted = false; + error.info = ERROR_INFO_EXAMPLE; + error.vendorId = ERROR_VENDOR_ID; + error.vendorErrorCode = ERROR_VENDOR_CODE; + return error; + } + return nullptr; + }); + + //test error condition during transaction to check if status remains unchanged + + beginTransaction("mIdTag"); + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + REQUIRE( isOperative() ); + + bool checkProcessed = false; + + getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("StatusNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + checkProcessed = true; + REQUIRE( !strcmp(payload["errorCode"] | "_Undefined", "OtherError") ); + REQUIRE( !strcmp(payload["info"] | "_Undefined", ERROR_INFO_EXAMPLE) ); + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Charging") ); + REQUIRE( !strcmp(payload["vendorId"] | "_Undefined", ERROR_VENDOR_ID) ); + REQUIRE( !strcmp(payload["vendorErrorCode"] | "_Undefined", ERROR_VENDOR_CODE) ); + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); + + errorCondition = true; + + loop(); + + REQUIRE( checkProcessed ); + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + REQUIRE( isOperative() ); + +#if MO_REPORT_NOERROR + checkProcessed = false; + getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", + [&checkProcessed] () { + return new Ocpp16::CustomOperation("StatusNotification", + [ &checkProcessed] (JsonObject payload) { + //process req + checkProcessed = true; + REQUIRE( !strcmp(payload["errorCode"] | "_Undefined", "NoError") ); + REQUIRE( !payload.containsKey("info") ); + REQUIRE( !strcmp(payload["status"] | "_Undefined", "Charging") ); + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); +#else + checkProcessed = true; +#endif //MO_REPORT_NOERROR + + errorCondition = false; + + loop(); + + REQUIRE( checkProcessed ); + REQUIRE( getChargePointStatus() == ChargePointStatus_Charging ); + REQUIRE( isOperative() ); + + } + + SECTION("Err and resolve (fatal)") { + + bool errorCondition = false; + + addErrorCodeInput([&errorCondition] () { + return errorCondition ? "OtherError" : nullptr; + }); + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + REQUIRE( isOperative() ); + + errorCondition = true; + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Faulted ); + REQUIRE( !isOperative() ); + + errorCondition = false; + + loop(); + + REQUIRE( getChargePointStatus() == ChargePointStatus_Available ); + REQUIRE( isOperative() ); + } + + SECTION("Error severity") { + + bool errorConditionLow1 = false; + bool errorConditionLow2 = false; + bool errorConditionHigh = false; + + addErrorDataInput([&errorConditionLow1] () -> ErrorData { + if (errorConditionLow1) { + ErrorData error = "OtherError"; + error.severity = 1; + error.info = ERROR_INFO_LOW_1; + return error; + } + return nullptr; + }); + + addErrorDataInput([&errorConditionLow2] () -> ErrorData { + if (errorConditionLow2) { + ErrorData error = "OtherError"; + error.severity = 1; + error.info = ERROR_INFO_LOW_2; + return error; + } + return nullptr; + }); + + addErrorDataInput([&errorConditionHigh] () -> ErrorData { + if (errorConditionHigh) { + ErrorData error = "OtherError"; + error.severity = 2; + error.info = ERROR_INFO_HIGH; + return error; + } + return nullptr; + }); + + const char *errorCode = "*"; + bool checkErrorCode = false; + const char *errorInfo = "*"; + bool checkErrorInfo = false; + + getOcppContext()->getOperationRegistry().registerOperation("StatusNotification", + [&checkErrorCode, &checkErrorInfo, &errorInfo, &errorCode] () { + return new Ocpp16::CustomOperation("StatusNotification", + [&checkErrorCode, &checkErrorInfo, &errorInfo, &errorCode] (JsonObject payload) { + //process req + if (strcmp(errorInfo, "*")) { + MO_DBG_DEBUG("expect \"%s\", got \"%s\"", errorInfo, payload["info"] | "_Undefined"); + REQUIRE( !strcmp(payload["info"] | "_Undefined", errorInfo) ); + checkErrorInfo = true; + } + if (strcmp(errorCode, "*")) { + MO_DBG_DEBUG("expect \"%s\", got \"%s\"", errorCode, payload["errorCode"] | "_Undefined"); + REQUIRE( !strcmp(payload["errorCode"] | "_Undefined", errorCode) ); + checkErrorCode = true; + } + }, + [] () { + //create conf + return createEmptyDocument(); + }); + }); + + //sequence: low-level error 1, low-level error 2, then severe error -- all errors should go through + MO_DBG_INFO("test sequence: low-level error 1, low-level error 2, then severe error"); + + errorConditionLow1 = true; + errorInfo = ERROR_INFO_LOW_1; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionLow2 = true; + errorInfo = ERROR_INFO_LOW_2; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionHigh = true; + errorInfo = ERROR_INFO_HIGH; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionLow1 = false; + errorConditionLow2 = false; + errorConditionHigh = false; + errorInfo = "*"; + loop(); + + //sequence: low-level error 1, severe error, then low-level error 2 -- last error gets muted until severe error is resolved + MO_DBG_INFO("test sequence: low-level error 1, severe error, then low-level error 2"); + + errorConditionLow1 = true; + errorInfo = ERROR_INFO_LOW_1; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionHigh = true; + errorInfo = ERROR_INFO_HIGH; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionLow2 = true; + checkErrorInfo = false; + loop(); + REQUIRE( !checkErrorInfo ); + + errorConditionHigh = false; + errorInfo = ERROR_INFO_LOW_2; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionLow1 = false; + errorConditionLow2 = false; + errorConditionHigh = false; + errorInfo = "*"; + loop(); + + //sequence: low-level error 1, severe error, then severe error gets resolved -- low-level error is reported again + MO_DBG_INFO("test sequence: low-level error 1, severe error, then severe error gets resolved"); + + errorConditionLow1 = true; + errorInfo = ERROR_INFO_LOW_1; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionHigh = true; + errorInfo = ERROR_INFO_HIGH; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionHigh = false; + errorInfo = ERROR_INFO_LOW_1; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionLow1 = false; + errorConditionLow2 = false; + errorConditionHigh = false; + errorInfo = "*"; + loop(); + + //sequence: error, then error gets resolved -- report NoError + MO_DBG_INFO("test sequence: error, then error gets resolved"); + + errorConditionLow1 = true; + errorInfo = ERROR_INFO_LOW_1; + checkErrorInfo = false; + loop(); + REQUIRE( checkErrorInfo ); + + errorConditionLow1 = false; + errorInfo = "*"; + errorCode = "NoError"; + checkErrorCode = false; + loop(); + REQUIRE( checkErrorCode ); + } + + endTransaction(); + mocpp_deinitialize(); + +}