Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ bazel_dep(name = "boost.utility", version = BOOST_VERSION)
bazel_dep(name = "boost.program_options", version = BOOST_VERSION)
bazel_dep(name = "cxx.rs", version = "1.0.153")
bazel_dep(name = "fast_float", version = "6.1.6")
bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
bazel_dep(name = "fmt", version = "12.1.0", repo_name = "com_github_fmtlib_fmt")
bazel_dep(name = "nlohmann_json", version = "3.12.0.bcr.1", repo_name = "com_github_nlohmann_json")
bazel_dep(name = "libcap", version = "2.27.bcr.1")
Expand Down
76 changes: 76 additions & 0 deletions modules/EVSE/EvseManager/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
load("@rules_cc//cc:defs.bzl", "cc_test")
load("//modules:module.bzl", "cc_everest_module")

IMPLS = [
Expand All @@ -24,4 +25,79 @@ cc_everest_module(
"*.hpp",
],
),
)

cc_test(
name = "EvseManager_tests",
srcs = [
# Test sources
"tests/EventQueueTest.cpp",
"tests/ErrorHandlingTest.cpp",
"tests/IECStateMachineTest.cpp",
"tests/OverVoltageMonitorTest.cpp",
"tests/VoltagePlausibilityMonitorTest.cpp",
# Test stub headers
"tests/evse_board_supportIntfStub.hpp",
"tests/EvseManagerStub.hpp",
# Module sources needed by tests
"ErrorHandling.cpp",
"IECStateMachine.cpp",
"backtrace.cpp",
"over_voltage/OverVoltageMonitor.cpp",
"voltage_plausibility/VoltagePlausibilityMonitor.cpp",
# Module headers
] + glob([
"*.hpp",
"over_voltage/*.hpp",
"voltage_plausibility/*.hpp",
]) + [
# Generated ld-ev header only (not ld-ev.cpp which pulls in impl files)
"generated/modules/EvseManager/ld-ev.hpp",
],
includes = [
".",
"tests",
"generated/modules/EvseManager",
],
copts = ["-std=c++17"],
defines = ["BUILD_TESTING_MODULE_EVSE_MANAGER"],
deps = [
"@googletest//:gtest_main",
"@sigslot//:sigslot",
"//interfaces:interfaces_lib",
"//lib/everest/framework:framework",
"//lib/everest/helpers",
"//tests/include:module_adapter_stub",
],
)

cc_test(
name = "EvseManagerCharger_tests",
srcs = [
# Test source
"tests/ChargerTest.cpp",
# Module source
"Charger.cpp",
] + glob([
"*.hpp",
]) + [
# Generated ld-ev header only
"generated/modules/EvseManager/ld-ev.hpp",
],
includes = [
".",
"tests",
"generated/modules/EvseManager",
],
copts = ["-std=c++17"],
defines = ["BUILD_TESTING_MODULE_EVSE_MANAGER"],
deps = [
"@googletest//:gtest_main",
"@googletest//:gtest",
"@sigslot//:sigslot",
"//interfaces:interfaces_lib",
"//lib/everest/framework:framework",
"//lib/everest/helpers",
"//tests/include:module_adapter_stub",
],
)
2 changes: 2 additions & 0 deletions modules/EVSE/EvseManager/Charger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,7 @@ void Charger::start_session(bool authfirst) {
void Charger::stop_session() {
shared_context.session_active = false;
shared_context.authorized = false;
bsp->set_authorized(false);
signal_simple_event(types::evse_manager::SessionEventEnum::SessionFinished);
shared_context.session_uuid.clear();
}
Expand Down Expand Up @@ -1573,6 +1574,7 @@ void Charger::authorize(bool a, const types::authorization::ProvidedIdToken& tok
}
signal_simple_event(types::evse_manager::SessionEventEnum::Authorized);
shared_context.authorized = true;
bsp->set_authorized(true);
shared_context.authorized_pnc =
token.authorization_type == types::authorization::AuthorizationType::PlugAndCharge;
} else {
Expand Down
19 changes: 18 additions & 1 deletion modules/EVSE/EvseManager/IECStateMachine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ std::queue<CPEvent> IECStateMachine::state_machine(RawCPState cp_state) {
// Table A.6: Sequence 7 EV stops charging
// Table A.6: Sequence 8.2 EV supply equipment
// responds to EV opens S2 (w/o PWM)
if (lock_connector_in_state_b) {
if (lock_connector_in_state_b or authorized) {
connector_lock();
} else {
connector_unlock();
Expand Down Expand Up @@ -525,4 +525,21 @@ void IECStateMachine::check_connector_lock() {
}
}

void IECStateMachine::set_authorized(bool a) {
authorized = a;
RawCPState cp;
{
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::IEC_set_authorized);
cp = last_cp_state;
}
if (cp == RawCPState::B) {
if (authorized or lock_connector_in_state_b) {
connector_lock();
} else {
connector_unlock();
}
check_connector_lock();
}
}

} // namespace module
4 changes: 4 additions & 0 deletions modules/EVSE/EvseManager/IECStateMachine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ class IECStateMachine {

void connector_force_unlock();

void set_authorized(bool a);

void set_ev_simplified_mode_evse_limit(bool l) {
ev_simplified_mode_evse_limit = l;
}
Expand Down Expand Up @@ -137,6 +139,8 @@ class IECStateMachine {
types::evse_board_support::Reason power_on_reason{types::evse_board_support::Reason::PowerOff};
void call_allow_power_on_bsp(bool value);

std::atomic_bool authorized{false};

std::atomic_bool is_locked{false};
std::atomic_bool should_be_locked{false};
std::atomic_bool force_unlocked{false};
Expand Down
3 changes: 3 additions & 0 deletions modules/EVSE/EvseManager/scoped_lock_timeout.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ enum class MutexDescription {
IEC_set_cp_state_F,
IEC_allow_power_on,
IEC_force_unlock,
IEC_set_authorized,
EVSE_charger_ready,
EVSE_set_ev_info,
EVSE_publish_ev_info,
Expand Down Expand Up @@ -187,6 +188,8 @@ static std::string to_string(MutexDescription d) {
return "IECStateMachine::allow_power_on";
case MutexDescription::IEC_force_unlock:
return "IECStateMachine::force_unlock";
case MutexDescription::IEC_set_authorized:
return "IECStateMachine::set_authorized";
case MutexDescription::EVSE_charger_ready:
return "EvseManager.cpp: charger_ready";
case MutexDescription::EVSE_set_ev_info:
Expand Down
3 changes: 3 additions & 0 deletions modules/EVSE/EvseManager/tests/ChargerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,9 @@ void IECStateMachine::enable(bool en) {
void IECStateMachine::connector_force_unlock() {
}

void IECStateMachine::set_authorized(bool a) {
}

const std::string cpevent_to_string(CPEvent e) {
switch (e) {
case CPEvent::CarPluggedIn:
Expand Down
143 changes: 143 additions & 0 deletions modules/EVSE/EvseManager/tests/IECStateMachineTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,147 @@ TEST(IECStateMachine, deadlock_fix) {
// if there is a deadlock the test won't finish
}

// ---------------------------------------------------------------------------
// Tests for auth-aware connector locking in State B
//
// When lock_connector_in_state_b is false, the connector should only lock
// in State B once the session is authorized. This prevents trapping cables
// before authorization while still keeping them locked during BMS pauses.

struct ConnectorLockTest : public testing::Test {
BspStub bsp;
std::unique_ptr<evse_board_supportIntf> bsp_if;
int lock_count{0};
int unlock_count{0};

void reset_counts() {
lock_count = 0;
unlock_count = 0;
}

std::unique_ptr<module::IECStateMachine> create_state_machine(bool lock_in_b) {
bsp_if = std::make_unique<module::stub::evse_board_supportIntfStub>(bsp);
auto sm = std::make_unique<module::IECStateMachine>(bsp_if, lock_in_b);
sm->signal_lock.connect([this]() { lock_count++; });
sm->signal_unlock.connect([this]() { unlock_count++; });
sm->enable(true);
return sm;
}

// Drive the state machine to State B via A→B (simulates plug-in)
void plug_in() {
bsp.raise_event(Event::A);
bsp.raise_event(Event::B);
}
};

TEST_F(ConnectorLockTest, lock_in_state_b_true_always_locks) {
// Default behavior: lock_connector_in_state_b=true locks immediately in B
auto sm = create_state_machine(true);

plug_in();

EXPECT_GT(lock_count, 0) << "connector should lock in State B when lock_connector_in_state_b is true";
}

TEST_F(ConnectorLockTest, lock_in_state_b_false_no_auth_stays_unlocked) {
// With lock_connector_in_state_b=false and no authorization,
// State B should NOT lock the connector (cable free to unplug)
auto sm = create_state_machine(false);

plug_in();

EXPECT_EQ(lock_count, 0) << "connector should not lock without authorization";

// Re-enter B to re-trigger the state machine evaluation
bsp.raise_event(Event::B);

EXPECT_EQ(lock_count, 0) << "connector should not lock in State B without authorization";
}

TEST_F(ConnectorLockTest, set_authorized_in_state_b_locks) {
// Car plugs in → State B → no lock → authorize → lock engages
auto sm = create_state_machine(false);

plug_in();

EXPECT_EQ(lock_count, 0) << "connector should not lock without authorization";

sm->set_authorized(true);

EXPECT_GT(lock_count, 0) << "connector should lock when authorized in State B";
}

TEST_F(ConnectorLockTest, set_authorized_first) {
// Car plugs in → State B → no lock → authorize → lock engages
auto sm = create_state_machine(false);

sm->set_authorized(true);

EXPECT_EQ(lock_count, 0) << "connector should not lock without authorization";

plug_in();

EXPECT_GT(lock_count, 0) << "connector should lock when authorized in State B";
}

TEST_F(ConnectorLockTest, set_deauthorized_in_state_b_unlocks) {
// After authorization, deauthorizing in State B should unlock
auto sm = create_state_machine(false);

sm->set_authorized(true);
plug_in();
reset_counts();

sm->set_authorized(false);

EXPECT_GT(unlock_count, 0) << "connector should unlock when deauthorized in State B";
}

TEST_F(ConnectorLockTest, bms_pause_stays_locked) {
// Authorized session: C→B (BMS pause) should keep the connector locked
auto sm = create_state_machine(false);

plug_in();
sm->set_authorized(true);

// Transition to State C (car requests power)
bsp.raise_event(Event::C);
reset_counts();

// BMS pause: car goes back to B
bsp.raise_event(Event::B);

// Lock should re-engage (or stay engaged), no unlock should fire
EXPECT_EQ(unlock_count, 0) << "connector should not unlock during BMS pause (C->B) while authorized";
EXPECT_EQ(lock_count, 0) << "connector already locked";
}

TEST_F(ConnectorLockTest, set_authorized_outside_state_b_no_lock_change) {
// Setting authorized while in State A should not trigger lock/unlock
auto sm = create_state_machine(false);

bsp.raise_event(Event::A);
reset_counts();

sm->set_authorized(true);

EXPECT_EQ(lock_count, 0) << "set_authorized in State A should not trigger lock";
EXPECT_EQ(unlock_count, 0) << "set_authorized in State A should not trigger unlock";
}

TEST_F(ConnectorLockTest, lock_in_state_b_true_ignores_auth_state) {
// With lock_connector_in_state_b=true, authorization state is irrelevant
auto sm = create_state_machine(true);

plug_in();
EXPECT_GT(lock_count, 0);
reset_counts();

// Deauthorize should not unlock when lock_connector_in_state_b is true
sm->set_authorized(false);

EXPECT_EQ(unlock_count, 0) << "lock_connector_in_state_b=true should keep lock regardless of auth state";
}

} // namespace
12 changes: 12 additions & 0 deletions tests/include/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
load("@rules_cc//cc:defs.bzl", "cc_library")

cc_library(
name = "module_adapter_stub",
hdrs = ["ModuleAdapterStub.hpp"],
copts = ["-std=c++17"],
strip_include_prefix = ".",
visibility = ["//visibility:public"],
deps = [
"//lib/everest/framework:framework",
],
)
Loading