From ecdb0b42c04fe537c85bc165ca3ea01554ff442e Mon Sep 17 00:00:00 2001 From: Razvan Grigore Date: Sun, 19 Oct 2025 09:14:13 +0300 Subject: [PATCH] connectorId SHALL be > 0 in RemoteStartTransaction (TC_027_CS) --- .github/workflows/tests.yml | 2 +- .gitignore | 1 + CMakeLists.txt | 1 + .../Operations/RemoteStartTransaction.cpp | 7 + tests/RemoteStartTransaction.cpp | 168 ++++++++++++++++++ 5 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 tests/RemoteStartTransaction.cpp diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3288a443..38b1d04e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v3 with: repository: Mbed-TLS/mbedtls - ref: v2.28.1 + ref: v2.28.10 path: lib/mbedtls - name: Get build tools run: | diff --git a/.gitignore b/.gitignore index e04a3bf0..d47070b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .pio +.vscode build lib mo_store diff --git a/CMakeLists.txt b/CMakeLists.txt index 963c1d8b..bf1d5f7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,6 +150,7 @@ set(MO_SRC_UNIT tests/LocalAuthList.cpp tests/Variables.cpp tests/Transactions.cpp + tests/RemoteStartTransaction.cpp tests/Certificates.cpp tests/FirmwareManagement.cpp tests/ChargePointError.cpp diff --git a/src/MicroOcpp/Operations/RemoteStartTransaction.cpp b/src/MicroOcpp/Operations/RemoteStartTransaction.cpp index 77314f36..977f675d 100644 --- a/src/MicroOcpp/Operations/RemoteStartTransaction.cpp +++ b/src/MicroOcpp/Operations/RemoteStartTransaction.cpp @@ -25,6 +25,13 @@ const char* RemoteStartTransaction::getOperationType() { void RemoteStartTransaction::processReq(JsonObject payload) { int connectorId = payload["connectorId"] | -1; + // OCPP 1.6 specification: connectorId SHALL be > 0 (TC_027_CS) + if (connectorId == 0) { + MO_DBG_INFO("RemoteStartTransaction rejected: connectorId SHALL not be 0"); + accepted = false; + return; + } + if (!payload.containsKey("idTag")) { errorCode = "FormationViolation"; return; diff --git a/tests/RemoteStartTransaction.cpp b/tests/RemoteStartTransaction.cpp new file mode 100644 index 00000000..5d21ec08 --- /dev/null +++ b/tests/RemoteStartTransaction.cpp @@ -0,0 +1,168 @@ +// matth-x/MicroOcpp +// Copyright Matthias Akstaller 2019 - 2024 +// MIT License + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "./helpers/testHelper.h" + +using namespace MicroOcpp; + +TEST_CASE("RemoteStartTransaction") { + printf("\nRun %s\n", "RemoteStartTransaction"); + + LoopbackConnection loopback; + mocpp_initialize(loopback, ChargerCredentials("test-runner1234")); + mocpp_set_timer(custom_timer_cb); + loop(); + + auto context = getOcppContext(); + auto connector = context->getModel().getConnector(1); + + SECTION("Basic remote start accepted") { + // Ensure connector idle + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + + context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + auto doc = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["idTag"] = "mIdTag"; + return doc;}, + [] (JsonObject) {} + ))); + + loop(); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + endTransaction(); + loop(); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + + SECTION("Same connectorId rejected when transaction active") { + // Start with connector 1 busy so remote start with connectorId=1 should not auto-assign + beginTransaction("anotherId"); + loop(); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + + bool checkProcessed = false; + + context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + auto doc = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(3)); + auto payload = doc->to(); + payload["idTag"] = "mIdTag"; + payload["connectorId"] = 1; // the same connector already in use + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); + } + ))); + + loop(); + + // Transaction should still be the original one only + REQUIRE(checkProcessed); + REQUIRE(connector->getTransaction()); + REQUIRE(strcmp(connector->getTransaction()->getIdTag(), "anotherId") == 0); + REQUIRE(connector->getStatus() == ChargePointStatus_Charging); + + endTransaction(); + loop(); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + + SECTION("ConnectorId 0 rejected per spec") { + // RemoteStartTransaction response status is Rejected when connectorId == 0 + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + + bool checkProcessed = false; + + context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + auto doc = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(3)); + auto payload = doc->to(); + payload["idTag"] = "mIdTag"; + payload["connectorId"] = 0; // invalid per spec + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); + } + ))); + + loop(); + + REQUIRE(checkProcessed); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + + SECTION("No free connector so rejected") { + // Occupy all connectors (limit defined by MO_NUMCONNECTORS) + for (unsigned cId = 1; cId < context->getModel().getNumConnectors(); cId++) { + auto c = context->getModel().getConnector(cId); + if (c) { + c->beginTransaction_authorized("busyId"); + } + } + loop(); + + bool checkProcessed = false; + auto freeFound = false; + for (unsigned cId = 1; cId < context->getModel().getNumConnectors(); cId++) { + auto c = context->getModel().getConnector(cId); + if (c && !c->getTransaction()) freeFound = true; + } + REQUIRE(!freeFound); // ensure all are busy + + context->initiateRequest(makeRequest(new Ocpp16::CustomOperation( + "RemoteStartTransaction", + [] () { + auto doc = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(2)); + auto payload = doc->to(); + payload["idTag"] = "mIdTag"; + return doc;}, + [&checkProcessed] (JsonObject response) { + checkProcessed = true; + REQUIRE( !strcmp(response["status"] | "_Undefined", "Rejected") ); + } + ))); + + loop(); + REQUIRE(checkProcessed); + + // No new transaction should be created; keep statuses + int activeTx = 0; + for (unsigned cId = 1; cId < context->getModel().getNumConnectors(); cId++) { + auto c = context->getModel().getConnector(cId); + if (c && c->getTransaction()) activeTx++; + } + REQUIRE(activeTx == (int)context->getModel().getNumConnectors() - 1); // all occupied + + // cleanup + for (unsigned cId = 1; cId < context->getModel().getNumConnectors(); cId++) { + auto c = context->getModel().getConnector(cId); + if (c && c->getTransaction()) { + c->endTransaction(); + } + } + loop(); + REQUIRE(connector->getStatus() == ChargePointStatus_Available); + } + + mocpp_deinitialize(); +}