Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,75 @@
set(TEST_TARGET_NAME ${PROJECT_NAME}_EvseManager_tests)
set(TESTS_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/tests/include")
add_executable(${TEST_TARGET_NAME})
add_dependencies(${TEST_TARGET_NAME} ${MODULE_NAME})
get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR)
target_include_directories(${TEST_TARGET_NAME} PRIVATE
.. ${TESTS_INCLUDE_DIR}
${GENERATED_INCLUDE_DIR}
${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME}
)
target_sources(${TEST_TARGET_NAME} PRIVATE
EventQueueTest.cpp
ErrorHandlingTest.cpp
IECStateMachineTest.cpp
OverVoltageMonitorTest.cpp
VoltagePlausibilityMonitorTest.cpp
../ErrorHandling.cpp
../IECStateMachine.cpp
../backtrace.cpp
../over_voltage/OverVoltageMonitor.cpp
../voltage_plausibility/VoltagePlausibilityMonitor.cpp
)
target_compile_definitions(${TEST_TARGET_NAME} PRIVATE
BUILD_TESTING_MODULE_EVSE_MANAGER
)
target_link_libraries(${TEST_TARGET_NAME} PRIVATE
GTest::gmock
GTest::gtest_main
everest::log
everest::framework
everest::helpers
sigslot
)
add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME})
ev_register_test_target(${TEST_TARGET_NAME})
set(CHARGER_TEST_TARGET_NAME ${PROJECT_NAME}_EvseManagerCharger_tests)
add_executable(${CHARGER_TEST_TARGET_NAME})
add_dependencies(${CHARGER_TEST_TARGET_NAME} ${MODULE_NAME})
target_include_directories(${CHARGER_TEST_TARGET_NAME} PRIVATE
.. ${TESTS_INCLUDE_DIR}
${GENERATED_INCLUDE_DIR}
${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME}
)
target_sources(${CHARGER_TEST_TARGET_NAME} PRIVATE
ChargerTest.cpp
../Charger.cpp
)
target_compile_definitions(${CHARGER_TEST_TARGET_NAME} PRIVATE
BUILD_TESTING_MODULE_EVSE_MANAGER
)
target_link_libraries(${CHARGER_TEST_TARGET_NAME} PRIVATE
GTest::gmock
GTest::gtest_main
everest::framework
everest::helpers
sigslot
)
add_test(${CHARGER_TEST_TARGET_NAME} ${CHARGER_TEST_TARGET_NAME})
ev_register_test_target(${CHARGER_TEST_TARGET_NAME})

View File

@@ -0,0 +1,955 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "generated/types/evse_manager.hpp"
#include <gtest/gtest.h>
#include <Charger.hpp>
#include <memory>
#include <optional>
namespace {
using namespace module;
using namespace types::evse_manager;
// ----------------------------------------------------------------------------
// test classes
// class that provides access to internal state from the Charger class
struct ChargerDerived : public Charger {
using Charger::Charger;
using Charger::get_enable_disable_source_table;
using Charger::get_shared_context;
using Charger::run_state_machine;
// updated when a non-zero connector is used to enable_disable()
constexpr const auto& connector_enabled() {
return get_shared_context().connector_enabled;
}
constexpr const auto& current_state() {
return get_shared_context().current_state;
}
constexpr void current_state(EvseState state) {
get_shared_context().current_state = state;
}
constexpr const auto& flag_disable_requested() {
return get_shared_context().flag_disable_requested;
}
};
// class that creates a consistent starting state for tests
struct ChargerTest : public testing::Test {
// charger requirements
std::unique_ptr<IECStateMachine> charger_bsp;
std::unique_ptr<ErrorHandling> charger_error_handling;
std::vector<std::unique_ptr<powermeterIntf>> charger_powermeter_billing;
std::unique_ptr<PersistentStore> charger_store;
// error handling requirements
std::unique_ptr<evse_board_supportIntf> error_handler_bsp;
std::vector<std::unique_ptr<ISO15118_chargerIntf>> error_handler_hlc;
std::vector<std::unique_ptr<connector_lockIntf>> error_handler_connector_lock;
std::vector<std::unique_ptr<ac_rcdIntf>> error_handler_ac_rcd;
std::unique_ptr<evse_managerImplBase> error_handler_evse;
std::vector<std::unique_ptr<isolation_monitorIntf>> error_handler_imd;
std::vector<std::unique_ptr<power_supply_DCIntf>> error_handler_powersupply;
std::vector<std::unique_ptr<powermeterIntf>> error_handler_powermeter;
std::vector<std::unique_ptr<over_voltage_monitorIntf>> error_handler_over_voltage_monitor;
std::unique_ptr<ChargerDerived> charger;
ChargerTest() :
charger_error_handling(std::make_unique<ErrorHandling>(
error_handler_bsp, error_handler_hlc, error_handler_connector_lock, error_handler_ac_rcd,
error_handler_evse, error_handler_imd, error_handler_powersupply, error_handler_powermeter,
error_handler_over_voltage_monitor, false)) {
}
void SetUp() override {
reset_last_event();
charger = std::make_unique<ChargerDerived>(
charger_bsp, charger_error_handling, charger_powermeter_billing, charger_store,
types::evse_board_support::Connector_type::IEC62196Type2Socket, "EVSETEST");
charger->signal_simple_event.connect(&ChargerTest::session_event, this);
}
void TearDown() override {
charger.reset(nullptr);
}
void session_event(SessionEventEnum event) {
last_event = event;
}
static constexpr SessionEventEnum default_event{SessionEventEnum::SessionFinished};
static constexpr EnableDisableSource default_source{Enable_source::Unspecified, Enable_state::Unassigned, 10000};
SessionEventEnum last_event{default_event};
constexpr void reset_last_event() {
last_event = default_event;
}
};
// ----------------------------------------------------------------------------
// tests for enable_disable()
// interesting variables:
// - enable_disable_source_table (not directly available)
// - shared_context.connector_enabled - charger->connector_enabled()
// - shared_context.current_state - charger->current_state()
// - signal_simple_event - last_event
TEST_F(ChargerTest, EnableDisableSourceInit) {
// check the default values on startup
// this is the starting point for all tests
const auto last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, default_source);
EXPECT_EQ(last_event, default_event);
EXPECT_TRUE(charger->connector_enabled());
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
}
TEST_F(ChargerTest, EnableDisableSourceInitPlusStateEnabled) {
charger->current_state(Charger::EvseState::Idle);
// check the default values on startup
// this is the starting point for all tests
auto last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, default_source);
EXPECT_EQ(last_event, default_event);
EXPECT_TRUE(charger->connector_enabled());
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
reset_last_event();
constexpr EnableDisableSource enable_default{Enable_source::Unspecified, Enable_state::Enable, 10000};
charger->enable_disable_initial_state_publish();
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_default);
EXPECT_EQ(last_event, SessionEventEnum::Enabled);
EXPECT_TRUE(charger->connector_enabled());
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
// already enabled so no event
reset_last_event();
constexpr EnableDisableSource enable_source{Enable_source::CSMS, Enable_state::Enable, 100};
EXPECT_TRUE(charger->enable_disable(0, enable_source));
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source);
EXPECT_EQ(last_event, default_event);
}
TEST_F(ChargerTest, EnableDisableSourceInitPlusStateDisabled) {
charger->current_state(Charger::EvseState::Disabled);
// check the default values on startup
// this is the starting point for all tests
auto last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, default_source);
EXPECT_EQ(last_event, default_event);
EXPECT_TRUE(charger->connector_enabled());
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
reset_last_event();
constexpr EnableDisableSource disable_default{Enable_source::Unspecified, Enable_state::Disable, 10000};
charger->enable_disable_initial_state_publish();
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, disable_default);
EXPECT_EQ(last_event, SessionEventEnum::Disabled);
EXPECT_TRUE(charger->connector_enabled());
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
// already disabled so no event
reset_last_event();
constexpr EnableDisableSource disable_source{Enable_source::CSMS, Enable_state::Disable, 100};
EXPECT_FALSE(charger->enable_disable(0, disable_source));
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, disable_source);
EXPECT_EQ(last_event, default_event);
}
TEST_F(ChargerTest, EnableDisableSourceConnectorEnabled0) {
// connector_enabled must only change when a non-zero connector ID is used
ASSERT_TRUE(charger->connector_enabled());
constexpr EnableDisableSource enable_source{Enable_source::CSMS, Enable_state::Enable, 100};
constexpr EnableDisableSource disable_source{Enable_source::CSMS, Enable_state::Disable, 100};
// test with connector ID 0: connector_enabled must not change
EXPECT_FALSE(charger->enable_disable(0, disable_source));
EXPECT_TRUE(charger->connector_enabled());
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
EXPECT_TRUE(charger->enable_disable(0, enable_source));
EXPECT_TRUE(charger->connector_enabled());
// tricky case, connector_enabled is true but the change was to connector 0
// this is what the original code does ...
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
// force connector_enabled false
EXPECT_FALSE(charger->enable_disable(1, disable_source));
EXPECT_FALSE(charger->connector_enabled());
// test with connector ID 0: connector_enabled must not change
EXPECT_FALSE(charger->enable_disable(0, disable_source));
EXPECT_FALSE(charger->connector_enabled());
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
EXPECT_TRUE(charger->enable_disable(0, enable_source));
EXPECT_FALSE(charger->connector_enabled());
// enable on connector 0 does not change state to Idle
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
}
TEST_F(ChargerTest, EnableDisableSourceConnectorEnabled1) {
// connector_enabled must only change when a non-zero connector ID is used
ASSERT_TRUE(charger->connector_enabled());
constexpr EnableDisableSource enable_source{Enable_source::CSMS, Enable_state::Enable, 100};
constexpr EnableDisableSource disable_source{Enable_source::CSMS, Enable_state::Disable, 100};
// test with connector ID 1: connector_enabled must change
EXPECT_FALSE(charger->enable_disable(1, disable_source));
EXPECT_FALSE(charger->connector_enabled());
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
EXPECT_TRUE(charger->enable_disable(1, enable_source));
EXPECT_TRUE(charger->connector_enabled());
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
EXPECT_FALSE(charger->enable_disable(1, disable_source));
EXPECT_FALSE(charger->connector_enabled());
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
}
constexpr EnableDisableSource enable_source_10{Enable_source::CSMS, Enable_state::Enable, 10};
constexpr EnableDisableSource enable_source_100{Enable_source::CSMS, Enable_state::Enable, 100};
constexpr EnableDisableSource enable_source_1000{Enable_source::CSMS, Enable_state::Enable, 1000};
constexpr EnableDisableSource disable_source_10{Enable_source::CSMS, Enable_state::Disable, 10};
constexpr EnableDisableSource disable_source_100{Enable_source::CSMS, Enable_state::Disable, 100};
constexpr EnableDisableSource disable_source_1000{Enable_source::CSMS, Enable_state::Disable, 1000};
constexpr EnableDisableSource unassigned_source_10{Enable_source::CSMS, Enable_state::Unassigned, 10};
constexpr EnableDisableSource unassigned_source_100{Enable_source::CSMS, Enable_state::Unassigned, 100};
constexpr EnableDisableSource unassigned_source_1000{Enable_source::CSMS, Enable_state::Unassigned, 1000};
TEST_F(ChargerTest, EnableDisableTableSingleSource) {
// enable_disable settings are added to a table
// parse_enable_disable_source_table() processes the table and updates
// active_enable_disable_source which is available via get_last_enable_disable_source()
// enable_disable() updates the table and calls parse_enable_disable_source_table()
// EnableDisableTable
// Evaluation will be done based on priorities. 0 is the highest priority, 10000 the lowest
// If all sources are unassigned, the connector is enabled
// If two sources have the same priority, "disabled" has priority over "enabled"
const int connector_id = 1; // use a consistent value
const auto& enable_disable_source_table = charger->get_enable_disable_source_table();
EXPECT_TRUE(enable_disable_source_table.empty());
EXPECT_EQ(enable_disable_source_table.size(), 0);
auto last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, default_source);
// check source change of state
EXPECT_TRUE(charger->enable_disable(connector_id, enable_source_100));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source_100);
EXPECT_FALSE(charger->enable_disable(connector_id, disable_source_100));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, disable_source_100);
EXPECT_TRUE(charger->enable_disable(connector_id, unassigned_source_100));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
// unexpected
EXPECT_EQ(last_source, default_source);
// check source change of priority
EXPECT_TRUE(charger->enable_disable(connector_id, enable_source_1000));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source_1000);
EXPECT_TRUE(charger->enable_disable(connector_id, enable_source_100));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source_100);
EXPECT_TRUE(charger->enable_disable(connector_id, enable_source_10));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source_10);
EXPECT_FALSE(charger->enable_disable(connector_id, disable_source_1000));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, disable_source_1000);
EXPECT_FALSE(charger->enable_disable(connector_id, disable_source_100));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, disable_source_100);
EXPECT_FALSE(charger->enable_disable(connector_id, disable_source_10));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, disable_source_10);
// last_source is always the default since Unassigned
// entries are ignored
EXPECT_TRUE(charger->enable_disable(connector_id, unassigned_source_1000));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, default_source);
EXPECT_TRUE(charger->enable_disable(connector_id, unassigned_source_100));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, default_source);
EXPECT_TRUE(charger->enable_disable(connector_id, unassigned_source_10));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, default_source);
}
TEST_F(ChargerTest, EnableDisableTableSourceMulti) {
// enable_disable settings are added to a table
// parse_enable_disable_source_table() processes the table and updates
// active_enable_disable_source which is available via get_last_enable_disable_source()
// enable_disable() updates the table and calls parse_enable_disable_source_table()
// EnableDisableTable
// Evaluation will be done based on priorities. 0 is the highest priority, 10000 the lowest
// If all sources are unassigned, the connector is enabled
// If two sources have the same priority, "disabled" has priority over "enabled"
const int connector_id = 1; // use a consistent value
const auto& enable_disable_source_table = charger->get_enable_disable_source_table();
EXPECT_TRUE(enable_disable_source_table.empty());
EXPECT_EQ(enable_disable_source_table.size(), 0);
auto last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, default_source);
// check multiple sources are added to the table
EXPECT_TRUE(charger->enable_disable(connector_id, enable_source_10));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source_10);
EXPECT_TRUE(charger->enable_disable(connector_id, {Enable_source::FirmwareUpdate, Enable_state::Enable, 11}));
EXPECT_EQ(enable_disable_source_table.size(), 2);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source_10);
EXPECT_TRUE(charger->enable_disable(connector_id, {Enable_source::LocalAPI, Enable_state::Enable, 11}));
EXPECT_EQ(enable_disable_source_table.size(), 3);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source_10);
EXPECT_TRUE(charger->enable_disable(connector_id, {Enable_source::LocalKeyLock, Enable_state::Enable, 11}));
EXPECT_EQ(enable_disable_source_table.size(), 4);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source_10);
EXPECT_TRUE(charger->enable_disable(connector_id, {Enable_source::MobileApp, Enable_state::Enable, 11}));
EXPECT_EQ(enable_disable_source_table.size(), 5);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source_10);
EXPECT_TRUE(charger->enable_disable(connector_id, {Enable_source::RemoteKeyLock, Enable_state::Enable, 11}));
EXPECT_EQ(enable_disable_source_table.size(), 6);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source_10);
const EnableDisableSource disable_source{Enable_source::ServiceTechnician, Enable_state::Disable, 11};
EXPECT_TRUE(charger->enable_disable(connector_id, disable_source));
EXPECT_EQ(enable_disable_source_table.size(), 7);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source_10);
EXPECT_TRUE(charger->enable_disable(connector_id, {Enable_source::Unspecified, Enable_state::Enable, 11}));
EXPECT_EQ(enable_disable_source_table.size(), 8);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, enable_source_10);
// update CSMS - next highest expected - disable_source
EXPECT_FALSE(charger->enable_disable(connector_id, enable_source_100));
EXPECT_EQ(enable_disable_source_table.size(), 8);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, disable_source);
}
TEST_F(ChargerTest, EnableDisableTablePriority) {
// enable_disable settings are added to a table
// parse_enable_disable_source_table() processes the table and updates
// active_enable_disable_source which is available via get_last_enable_disable_source()
// enable_disable() updates the table and calls parse_enable_disable_source_table()
// EnableDisableTable
// Evaluation will be done based on priorities. 0 is the highest priority, 10000 the lowest
// If all sources are unassigned, the connector is enabled
// If two sources have the same priority, "disabled" has priority over "enabled"
const int connector_id = 1; // use a consistent value
const auto& enable_disable_source_table = charger->get_enable_disable_source_table();
EXPECT_TRUE(enable_disable_source_table.empty());
EXPECT_EQ(enable_disable_source_table.size(), 0);
auto last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, default_source);
// check priority is being respected (via different sources)
// base priority is 10 - higher values must be ignored
EXPECT_FALSE(charger->enable_disable(connector_id, disable_source_10));
EXPECT_EQ(enable_disable_source_table.size(), 1);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, disable_source_10);
// ignored because disable has higher priority over enabled
const EnableDisableSource next_expected{Enable_source::FirmwareUpdate, Enable_state::Enable, 10};
EXPECT_FALSE(charger->enable_disable(connector_id, next_expected));
EXPECT_EQ(enable_disable_source_table.size(), 2);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, disable_source_10);
// ignored because unassigned is ignored
EXPECT_FALSE(charger->enable_disable(connector_id, {Enable_source::LocalAPI, Enable_state::Unassigned, 9}));
EXPECT_EQ(enable_disable_source_table.size(), 3);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, disable_source_10);
// ignored because priority is lower
EXPECT_FALSE(charger->enable_disable(connector_id, {Enable_source::LocalAPI, Enable_state::Disable, 12}));
EXPECT_EQ(enable_disable_source_table.size(), 3);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, disable_source_10);
// ignored because priority is even lower
EXPECT_FALSE(charger->enable_disable(connector_id, {Enable_source::LocalAPI, Enable_state::Enable, 200}));
EXPECT_EQ(enable_disable_source_table.size(), 3);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, disable_source_10);
// overrides CSMS so it is ignored - expected is
EXPECT_TRUE(charger->enable_disable(connector_id, unassigned_source_10));
EXPECT_EQ(enable_disable_source_table.size(), 3);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, next_expected);
// disabled takes priority
const EnableDisableSource last_expected{Enable_source::LocalAPI, Enable_state::Disable, 10};
EXPECT_FALSE(charger->enable_disable(connector_id, last_expected));
EXPECT_EQ(enable_disable_source_table.size(), 3);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, last_expected);
// higher priority
const EnableDisableSource higher_expected{Enable_source::FirmwareUpdate, Enable_state::Enable, 5};
EXPECT_TRUE(charger->enable_disable(connector_id, higher_expected));
EXPECT_EQ(enable_disable_source_table.size(), 3);
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, higher_expected);
}
TEST_F(ChargerTest, EnableDisableSourceEnable0) {
constexpr EnableDisableSource sourceA{Enable_source::CSMS, Enable_state::Unassigned, 100};
constexpr EnableDisableSource sourceB{Enable_source::FirmwareUpdate, Enable_state::Enable, 90};
// enable from default state
EXPECT_TRUE(charger->enable_disable(0, sourceA));
auto last_source = charger->get_last_enable_disable_source();
// Unassigned updates do not change the result from get_last_enable_disable_source()
EXPECT_EQ(last_source, default_source);
// There should not be an update event - default state is enabled
EXPECT_EQ(last_event, default_event);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
EXPECT_TRUE(charger->connector_enabled()); // default state
reset_last_event();
EXPECT_TRUE(charger->enable_disable(0, sourceB));
last_source = charger->get_last_enable_disable_source();
// Unassigned updates do not change the result from get_last_enable_disable_source()
EXPECT_EQ(last_source, sourceB);
// There should not be an update event - enabled -> enabled
EXPECT_EQ(last_event, default_event);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
EXPECT_TRUE(charger->connector_enabled()); // default state
}
TEST_F(ChargerTest, EnableDisableSourceEnable1A) {
constexpr EnableDisableSource sourceA{Enable_source::CSMS, Enable_state::Disable, 100};
constexpr EnableDisableSource sourceB{Enable_source::FirmwareUpdate, Enable_state::Enable, 90};
// force a complete disable state
EXPECT_FALSE(charger->enable_disable(1, sourceA));
auto last_source = charger->get_last_enable_disable_source();
// Unassigned updates do not change the result from get_last_enable_disable_source()
EXPECT_EQ(last_source, sourceA);
EXPECT_EQ(last_event, SessionEventEnum::Disabled);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
EXPECT_FALSE(charger->connector_enabled());
// enable on connector 1
reset_last_event();
EXPECT_TRUE(charger->enable_disable(1, sourceB));
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, sourceB);
EXPECT_EQ(last_event, SessionEventEnum::Enabled);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
EXPECT_TRUE(charger->connector_enabled());
}
TEST_F(ChargerTest, EnableDisableSourceEnable1B) {
constexpr EnableDisableSource sourceA{Enable_source::CSMS, Enable_state::Disable, 100};
constexpr EnableDisableSource sourceB{Enable_source::FirmwareUpdate, Enable_state::Enable, 90};
// force a complete disable state
EXPECT_FALSE(charger->enable_disable(1, sourceA));
auto last_source = charger->get_last_enable_disable_source();
// Unassigned updates do not change the result from get_last_enable_disable_source()
EXPECT_EQ(last_source, sourceA);
EXPECT_EQ(last_event, SessionEventEnum::Disabled);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
EXPECT_FALSE(charger->connector_enabled());
// enable on connector 0
reset_last_event();
EXPECT_TRUE(charger->enable_disable(0, sourceB));
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, sourceB);
EXPECT_EQ(last_event, SessionEventEnum::Enabled);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
EXPECT_FALSE(charger->connector_enabled());
// enable on connector 1
reset_last_event();
EXPECT_TRUE(charger->enable_disable(1, sourceB));
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, sourceB);
// enable -> enable so no event
EXPECT_EQ(last_event, default_event);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
EXPECT_TRUE(charger->connector_enabled());
}
TEST_F(ChargerTest, EnableDisableSourceDisable0) {
constexpr EnableDisableSource sourceA{Enable_source::CSMS, Enable_state::Unassigned, 100};
constexpr EnableDisableSource sourceB{Enable_source::FirmwareUpdate, Enable_state::Disable, 100};
// disable from default state
EXPECT_TRUE(charger->enable_disable(0, sourceA));
auto last_source = charger->get_last_enable_disable_source();
// Unassigned updates do not change the result from get_last_enable_disable_source()
EXPECT_EQ(last_source, default_source);
// This is possibly an error
EXPECT_EQ(last_event, default_event);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
reset_last_event();
EXPECT_FALSE(charger->enable_disable(0, sourceB));
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, sourceB);
EXPECT_EQ(last_event, SessionEventEnum::Disabled);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
}
TEST_F(ChargerTest, EnableDisableSourceDisable1) {
constexpr EnableDisableSource sourceA{Enable_source::CSMS, Enable_state::Unassigned, 100};
constexpr EnableDisableSource sourceB{Enable_source::FirmwareUpdate, Enable_state::Disable, 100};
// disable from default state
EXPECT_TRUE(charger->enable_disable(0, sourceA));
auto last_source = charger->get_last_enable_disable_source();
// Unassigned updates do not change the result from get_last_enable_disable_source()
EXPECT_EQ(last_source, default_source);
// This is possibly an error
EXPECT_EQ(last_event, default_event);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
reset_last_event();
EXPECT_FALSE(charger->enable_disable(0, sourceB));
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, sourceB);
EXPECT_EQ(last_event, SessionEventEnum::Disabled);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
reset_last_event();
EXPECT_FALSE(charger->enable_disable(1, sourceB));
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, sourceB);
// disable -> disable so no event
EXPECT_EQ(last_event, default_event);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
}
TEST_F(ChargerTest, EnableDisableSourceDisableEnable) {
constexpr EnableDisableSource sourceA{Enable_source::CSMS, Enable_state::Unassigned, 100};
constexpr EnableDisableSource sourceB{Enable_source::FirmwareUpdate, Enable_state::Disable, 100};
constexpr EnableDisableSource sourceC{Enable_source::LocalAPI, Enable_state::Enable, 90};
// default state
EXPECT_TRUE(charger->enable_disable(0, sourceA));
auto last_source = charger->get_last_enable_disable_source();
// Unassigned updates do not change the result from get_last_enable_disable_source()
EXPECT_EQ(last_source, default_source);
// This is possibly an error
EXPECT_EQ(last_event, default_event);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
// disable 0
reset_last_event();
EXPECT_FALSE(charger->enable_disable(0, sourceB));
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, sourceB);
EXPECT_EQ(last_event, SessionEventEnum::Disabled);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
// disable 1
reset_last_event();
EXPECT_FALSE(charger->enable_disable(1, sourceB));
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, sourceB);
// no event: disable -> disable
EXPECT_EQ(last_event, default_event);
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
// enable 0
reset_last_event();
EXPECT_TRUE(charger->enable_disable(0, sourceC));
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, sourceC);
EXPECT_EQ(last_event, SessionEventEnum::Enabled);
// remains disabled because the enable was on connector 0
EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled);
// enable 1
reset_last_event();
EXPECT_TRUE(charger->enable_disable(1, sourceC));
last_source = charger->get_last_enable_disable_source();
EXPECT_EQ(last_source, sourceC);
// enable->enable hence no event
EXPECT_EQ(last_event, default_event);
// updated because not connector 0
EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle);
}
// tests for cancel_transaction() / authorize() interaction
TEST_F(ChargerTest, DelayedAuthorizeAfterCancelTransactionIsIgnored) {
auto& ctx = charger->get_shared_context();
// Simulate an active charging session
ctx.flag_transaction_active = true;
ctx.session_active = true;
ctx.flag_authorized = true;
// External cancellation (e.g. OCPP RemoteStop)
types::evse_manager::StopTransactionRequest stop_request;
stop_request.reason = types::evse_manager::StopTransactionReason::Remote;
EXPECT_TRUE(charger->cancel_transaction(stop_request));
EXPECT_FALSE(ctx.flag_authorized);
EXPECT_TRUE(ctx.flag_externally_cancelled);
// Delayed authorization response arrives after the cancellation
types::authorization::ProvidedIdToken token;
token.id_token.value = "DELAYED_TOKEN";
token.id_token.type = types::authorization::IdTokenType::Central;
token.authorization_type = types::authorization::AuthorizationType::OCPP;
types::authorization::ValidationResult validation_result;
validation_result.authorization_status = types::authorization::AuthorizationStatus::Accepted;
charger->authorize(true, token, validation_result);
// The delayed response must not restore authorization
EXPECT_FALSE(ctx.flag_authorized);
EXPECT_TRUE(ctx.flag_externally_cancelled);
}
// Test that disabling while a transaction is active goes through the proper
// StoppingCharging->Finished->Disabled sequence instead of jumping directly.
TEST_F(ChargerTest, DisableDuringActiveTransaction) {
constexpr EnableDisableSource disable_source{Enable_source::CSMS, Enable_state::Disable, 100};
auto& ctx = charger->get_shared_context();
// Simulate an active charging session with contactors closed
ctx.current_state = Charger::EvseState::Charging;
ctx.flag_transaction_active = true;
ctx.session_active = true;
ctx.flag_authorized = true;
ctx.contactor_open = false;
reset_last_event();
EXPECT_FALSE(charger->enable_disable(1, disable_source));
// enable_disable calls run_state_machine synchronously: Charging->StoppingCharging.
// Must NOT immediately jump to Disabled — session needs proper teardown.
EXPECT_EQ(ctx.current_state, Charger::EvseState::StoppingCharging);
EXPECT_TRUE(charger->flag_disable_requested());
EXPECT_EQ(ctx.last_stop_transaction_reason, StopTransactionReason::EVSEDisabled);
// Simulate relay opening and transaction already stopped
ctx.contactor_open = true;
ctx.flag_transaction_active = false;
// State machine: StoppingCharging->Finished->Disabled (all in one loop)
reset_last_event();
charger->run_state_machine();
EXPECT_EQ(ctx.current_state, Charger::EvseState::Disabled);
EXPECT_EQ(last_event, SessionEventEnum::Disabled);
}
// Test that disabling while in WaitingForAuthentication (no transaction yet)
// ends the session and transitions to Disabled without going through StoppingCharging.
TEST_F(ChargerTest, DisableDuringWaitingForAuthentication) {
constexpr EnableDisableSource disable_source{Enable_source::CSMS, Enable_state::Disable, 100};
auto& ctx = charger->get_shared_context();
// Simulate EV plugged in, waiting for auth — no transaction started yet
ctx.current_state = Charger::EvseState::WaitingForAuthentication;
ctx.session_active = true;
ctx.flag_ev_plugged_in = true;
ctx.flag_transaction_active = false;
ctx.flag_authorized = false;
reset_last_event();
EXPECT_FALSE(charger->enable_disable(1, disable_source));
// run_state_machine is called synchronously inside enable_disable, so by
// the time enable_disable returns the state machine has already driven
// WaitingForAuthentication -> Finished -> Disabled.
EXPECT_EQ(ctx.current_state, Charger::EvseState::Disabled);
EXPECT_EQ(last_event, SessionEventEnum::Disabled);
}
// Test that disabling while in Idle (no session) immediately transitions to Disabled
TEST_F(ChargerTest, DisableDuringIdle) {
constexpr EnableDisableSource disable_source{Enable_source::CSMS, Enable_state::Disable, 100};
auto& ctx = charger->get_shared_context();
// Simulate EV plugged in, no session active
ctx.current_state = Charger::EvseState::Idle;
ctx.session_active = false;
ctx.flag_ev_plugged_in = true;
ctx.flag_transaction_active = false;
ctx.flag_authorized = false;
reset_last_event();
EXPECT_FALSE(charger->enable_disable(1, disable_source));
// Must immediately transition to Disabled
EXPECT_EQ(ctx.current_state, Charger::EvseState::Disabled);
EXPECT_EQ(last_event, SessionEventEnum::Disabled);
}
} // namespace
// ----------------------------------------------------------------------------
// the following code is stubs to enable testing Charger in isolation.
// If these stubs are needed elsewhere then they could go into separate files
// ----------------------------------------------------------------------------
// backtrace stub
namespace Everest {
void signal_handler(int signo) {
}
void install_backtrace_handler() {
}
void request_backtrace(pthread_t id) {
}
} // namespace Everest
namespace module {
// ----------------------------------------------------------------------------
// IECStateMachine stub
IECStateMachine::IECStateMachine(const std::unique_ptr<evse_board_supportIntf>& r_bsp_,
bool lock_connector_in_state_b_) :
r_bsp(r_bsp_) {
}
void IECStateMachine::process_bsp_event(const types::board_support_common::BspEvent& bsp_event) {
}
void IECStateMachine::allow_power_on(bool value, types::evse_board_support::Reason reason) {
}
std::optional<double> IECStateMachine::read_pp_ampacity() {
return std::nullopt;
}
void IECStateMachine::switch_three_phases_while_charging(bool n) {
}
void IECStateMachine::setup(bool has_ventilation) {
}
void IECStateMachine::set_overcurrent_limit(double amps) {
}
void IECStateMachine::set_pwm(double value) {
}
void IECStateMachine::set_cp_state_X1() {
}
void IECStateMachine::set_cp_state_F() {
}
void IECStateMachine::enable(bool en) {
}
void IECStateMachine::connector_force_unlock() {
}
const std::string cpevent_to_string(CPEvent e) {
switch (e) {
case CPEvent::CarPluggedIn:
return "CarPluggedIn";
case CPEvent::CarRequestedPower:
return "CarRequestedPower";
case CPEvent::PowerOn:
return "PowerOn";
case CPEvent::PowerOff:
return "PowerOff";
case CPEvent::CarRequestedStopPower:
return "CarRequestedStopPower";
case CPEvent::CarUnplugged:
return "CarUnplugged";
case CPEvent::EFtoBCD:
return "EFtoBCD";
case CPEvent::BCDtoEF:
return "BCDtoEF";
case CPEvent::BCDtoE:
return "BCDtoE";
}
throw std::out_of_range("No known string conversion for provided enum of type CPEvent");
}
// ----------------------------------------------------------------------------
// ErrorHandling stub
ErrorHandling::ErrorHandling(const std::unique_ptr<evse_board_supportIntf>& r_bsp,
const std::vector<std::unique_ptr<ISO15118_chargerIntf>>& r_hlc,
const std::vector<std::unique_ptr<connector_lockIntf>>& r_connector_lock,
const std::vector<std::unique_ptr<ac_rcdIntf>>& r_ac_rcd,
const std::unique_ptr<evse_managerImplBase>& _p_evse,
const std::vector<std::unique_ptr<isolation_monitorIntf>>& _r_imd,
const std::vector<std::unique_ptr<power_supply_DCIntf>>& _r_powersupply,
const std::vector<std::unique_ptr<powermeterIntf>>& _r_powermeter,
const std::vector<std::unique_ptr<over_voltage_monitorIntf>>& _r_over_voltage_monitor,
bool _inoperative_error_use_vendor_id) :
r_bsp(r_bsp),
r_hlc(r_hlc),
r_connector_lock(r_connector_lock),
r_ac_rcd(r_ac_rcd),
p_evse(p_evse),
r_imd(_r_imd),
r_powersupply(r_powersupply),
r_powermeter(_r_powermeter),
r_over_voltage_monitor(_r_over_voltage_monitor),
inoperative_error_use_vendor_id(_inoperative_error_use_vendor_id) {
}
void ErrorHandling::raise_overcurrent_error(const std::string& description) {
}
void ErrorHandling::clear_overcurrent_error() {
}
void ErrorHandling::raise_over_voltage_error(Everest::error::Severity severity, const std::string& description) {
}
void ErrorHandling::clear_over_voltage_error() {
}
void ErrorHandling::raise_internal_error(const std::string& description) {
}
void ErrorHandling::clear_internal_error() {
}
void ErrorHandling::raise_authorization_timeout_error(const std::string& description) {
}
void ErrorHandling::clear_authorization_timeout_error() {
}
void ErrorHandling::raise_powermeter_transaction_start_failed_error(const std::string& description) {
}
void ErrorHandling::clear_powermeter_transaction_start_failed_error() {
}
void ErrorHandling::raise_isolation_resistance_fault(const std::string& description, const std::string& sub_type) {
}
void ErrorHandling::clear_isolation_resistance_fault(const std::string& sub_type) {
}
void ErrorHandling::raise_cable_check_fault(const std::string& description) {
}
void ErrorHandling::clear_cable_check_fault() {
}
void ErrorHandling::clear_voltage_plausibility_fault() {
}
// ----------------------------------------------------------------------------
// SessionLog stub
SessionLog::SessionLog() {
}
SessionLog::~SessionLog() {
}
void SessionLog::setPath(const std::string& path) {
}
void SessionLog::setMqtt(const std::function<void(const nlohmann::json& data)>& mqtt_provider) {
}
void SessionLog::enable() {
}
std::optional<std::filesystem::path> SessionLog::startSession(const std::string& suffix_string) {
return {};
}
void SessionLog::stopSession() {
}
void SessionLog::car(bool iso15118, const std::string& msg) {
}
void SessionLog::car(bool iso15118, const std::string& msg, const std::string& xml, const std::string& xml_hex,
const std::string& xml_base64, const std::string& json_str) {
}
void SessionLog::evse(bool iso15118, const std::string& msg) {
}
void SessionLog::evse(bool iso15118, const std::string& msg, const std::string& xml, const std::string& xml_hex,
const std::string& xml_base64, const std::string& json_str) {
}
void SessionLog::xmlOutput(bool e) {
}
void SessionLog::sys(const std::string& msg) {
}
SessionLog session_log;
// ----------------------------------------------------------------------------
// PersistentStore stub
PersistentStore::PersistentStore(const std::vector<std::unique_ptr<kvsIntf>>& r_store, const std::string module_id) :
r_store(r_store) {
}
void PersistentStore::store_session(const std::string& session_uuid) {
}
void PersistentStore::clear_session() {
}
std::string PersistentStore::get_session() {
return {};
}
} // namespace module

View File

@@ -0,0 +1,352 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "ErrorHandling.hpp"
#include "EvseManagerStub.hpp"
#include "evse_board_supportIntfStub.hpp"
#include <gtest/gtest.h>
#include <memory>
#include <set>
namespace Everest::error {
bool operator<(const Error& lhs, const Error& rhs) {
return lhs.type < rhs.type;
}
bool operator==(const Error& lhs, const Error& rhs) {
return (lhs.type == rhs.type) && (lhs.sub_type == rhs.sub_type);
}
} // namespace Everest::error
namespace {
struct ErrorHandlingTest : public module::ErrorHandling {
using module::ErrorHandling::ErrorHandling;
using module::ErrorHandling::raise_inoperative_error;
};
struct ErrorDatabaseStub : public Everest::error::ErrorDatabase {
using Error = Everest::error::Error;
using ErrorPtr = Everest::error::ErrorPtr;
using ErrorFilter = Everest::error::ErrorFilter;
std::list<Error>& active_errors;
ErrorDatabaseStub(std::list<Error>& active) : Everest::error::ErrorDatabase(), active_errors(active) {
}
virtual void add_error(ErrorPtr error) {
}
virtual std::list<ErrorPtr> get_errors(const std::list<ErrorFilter>& filters) const {
std::list<ErrorPtr> result;
for (const auto& error : active_errors) {
result.push_back(std::make_shared<Error>(error));
}
return result;
}
virtual std::list<ErrorPtr> edit_errors(const std::list<ErrorFilter>& filters, EditErrorFunc edit_func) {
return {};
}
virtual std::list<ErrorPtr> remove_errors(const std::list<ErrorFilter>& filters) {
return {};
}
};
struct EvseManagerModuleAdapterStub : public module::stub::EvseManagerModuleAdapter {
std::shared_ptr<Everest::error::ErrorTypeMap> error_type_map;
std::shared_ptr<ErrorDatabaseStub> error_database;
std::list<Everest::error::ErrorType> error_list;
std::list<Everest::error::Error> active_errors;
EvseManagerModuleAdapterStub() :
module::stub::EvseManagerModuleAdapter(),
error_type_map(std::make_shared<Everest::error::ErrorTypeMap>()),
error_database(std::make_shared<ErrorDatabaseStub>(active_errors)),
error_list{} {
}
virtual std::shared_ptr<Everest::error::ErrorManagerImpl> get_error_manager_impl_fn(const std::string& str) {
return std::make_shared<Everest::error::ErrorManagerImpl>(
error_type_map, std::make_shared<Everest::error::ErrorDatabaseMap>(), error_list,
[this](const Everest::error::Error& error) {
if (error_raise.find(error.type) == error_raise.end()) {
throw std::runtime_error("Error type " + error.type + " not found");
}
error_raise[error.type](error);
active_errors.push_back(error);
},
[this](const Everest::error::Error& error) {
if (error_raise.find(error.type) == error_raise.end()) {
throw std::runtime_error("Error type " + error.type + " not found");
}
error_clear[error.type](error);
active_errors.remove(error);
},
false);
}
virtual std::shared_ptr<Everest::error::ErrorManagerReq> get_error_manager_req_fn(const Requirement& req) {
return std::make_shared<Everest::error::ErrorManagerReq>(
error_type_map, std::make_shared<Everest::error::ErrorDatabaseMap>(), error_list,
[this](const Everest::error::ErrorType& error_type, const Everest::error::ErrorCallback& callback,
const Everest::error::ErrorCallback& clear_callback) {
error_raise[error_type] = callback;
error_clear[error_type] = clear_callback;
});
}
virtual std::shared_ptr<Everest::error::ErrorFactory> get_error_factory_fn(const std::string&) {
return std::make_shared<Everest::error::ErrorFactory>(error_type_map);
}
virtual std::shared_ptr<Everest::error::ErrorStateMonitor> get_error_state_monitor_impl_fn(const std::string&) {
return std::make_shared<Everest::error::ErrorStateMonitor>(error_database);
}
};
struct ErrorHandlingTesting : public testing::Test {
EvseManagerModuleAdapterStub adapter;
std::map<Everest::error::ErrorType, std::string> error_types_map;
std::unique_ptr<evse_board_supportIntf> r_bsp{};
const std::vector<std::unique_ptr<ISO15118_chargerIntf>> r_hlc{};
const std::vector<std::unique_ptr<connector_lockIntf>> r_connector_lock{};
const std::vector<std::unique_ptr<ac_rcdIntf>> r_ac_rcd{};
std::unique_ptr<evse_managerImplBase> p_evse{};
const std::vector<std::unique_ptr<isolation_monitorIntf>> _r_imd{};
const std::vector<std::unique_ptr<power_supply_DCIntf>> _r_powersupply{};
const std::vector<std::unique_ptr<powermeterIntf>> _r_powermeter{};
const std::vector<std::unique_ptr<over_voltage_monitorIntf>> _r_over_voltage_monitor{};
std::unique_ptr<ErrorHandlingTest> error_handler;
void construct(bool _inoperative_error_use_vendor_id) {
error_types_map = {{"evse_board_support/VendorWarning", "warning"},
{"evse_manager/Inoperative", "inoperative"}};
adapter.error_type_map->load_error_types_map(error_types_map);
adapter.active_errors.clear();
for (const auto& [error, description] : error_types_map) {
adapter.error_list.push_back(error);
}
r_bsp = std::make_unique<module::stub::evse_board_supportIntfStub>(adapter);
p_evse = std::make_unique<module::stub::evse_managerImplStub>(&adapter, "manager");
error_handler = std::make_unique<ErrorHandlingTest>(r_bsp, r_hlc, r_connector_lock, r_ac_rcd, p_evse, _r_imd,
_r_powersupply, _r_powermeter, _r_over_voltage_monitor,
_inoperative_error_use_vendor_id);
}
static constexpr std::string_view default_description{"description"};
static constexpr std::string_view default_no_vendor_id{"EVerest"};
static constexpr std::string_view default_vendor_id{"vendor_id"};
Everest::error::Error test_description(const std::string& type, const std::string& subtype) {
Everest::error::Error error{};
error.type = type;
error.sub_type = subtype;
error.message = "message";
error.description = default_description;
error.vendor_id = default_vendor_id;
error_handler->raise_inoperative_error(error);
EXPECT_EQ(adapter.active_errors.size(), 1);
auto& active = adapter.active_errors.front();
EXPECT_EQ(active.type, "evse_manager/Inoperative");
EXPECT_EQ(active.sub_type, "");
EXPECT_EQ(active.message, error.type);
return active;
}
};
TEST_F(ErrorHandlingTesting, NoType) {
construct(true);
auto error = test_description("", "sub-type");
EXPECT_EQ(error.description, default_description);
EXPECT_EQ(error.vendor_id, default_vendor_id);
construct(false);
error = test_description("", "sub-type");
EXPECT_EQ(error.description, default_description);
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
}
TEST_F(ErrorHandlingTesting, TypeNoSlashA) {
construct(true);
auto error = test_description("type", "sub-type");
EXPECT_EQ(error.description, default_description);
EXPECT_EQ(error.vendor_id, default_vendor_id);
construct(false);
error = test_description("type", "sub-type");
EXPECT_EQ(error.description, default_description);
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
}
TEST_F(ErrorHandlingTesting, TypeNoSlashB) {
construct(true);
auto error = test_description("type", "");
EXPECT_EQ(error.description, default_description);
EXPECT_EQ(error.vendor_id, default_vendor_id);
construct(false);
error = test_description("type", "");
EXPECT_EQ(error.description, default_description);
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
}
TEST_F(ErrorHandlingTesting, TypeSlashEndA) {
construct(true);
auto error = test_description("type/", "sub-type");
EXPECT_EQ(error.description, "type/sub-type");
EXPECT_EQ(error.vendor_id, default_vendor_id);
construct(false);
error = test_description("type/", "sub-type");
EXPECT_EQ(error.description, "type/sub-type");
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
}
TEST_F(ErrorHandlingTesting, TypeSlashEndB) {
construct(true);
auto error = test_description("type/", "");
EXPECT_EQ(error.description, "type");
EXPECT_EQ(error.vendor_id, default_vendor_id);
construct(false);
error = test_description("type/", "");
EXPECT_EQ(error.description, "type");
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
}
TEST_F(ErrorHandlingTesting, TypeSlashBeginA) {
construct(true);
auto error = test_description("/type", "sub-type");
EXPECT_EQ(error.description, "type/sub-type");
EXPECT_EQ(error.vendor_id, default_vendor_id);
construct(false);
error = test_description("/type", "sub-type");
EXPECT_EQ(error.description, "type/sub-type");
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
}
TEST_F(ErrorHandlingTesting, TypeSlashBeginB) {
construct(true);
auto error = test_description("/type", "");
EXPECT_EQ(error.description, "type");
EXPECT_EQ(error.vendor_id, default_vendor_id);
construct(false);
error = test_description("/type", "");
EXPECT_EQ(error.description, "type");
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
}
TEST_F(ErrorHandlingTesting, TypeFullA) {
construct(true);
auto error = test_description("module/type", "sub-type");
EXPECT_EQ(error.description, "type/sub-type");
EXPECT_EQ(error.vendor_id, default_vendor_id);
construct(false);
error = test_description("module/type", "sub-type");
EXPECT_EQ(error.description, "type/sub-type");
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
}
TEST_F(ErrorHandlingTesting, TypeFullB) {
construct(true);
auto error = test_description("module/type", "");
EXPECT_EQ(error.description, "type");
EXPECT_EQ(error.vendor_id, default_vendor_id);
construct(false);
error = test_description("module/type", "");
EXPECT_EQ(error.description, "type");
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
}
TEST_F(ErrorHandlingTesting, TypeFullC) {
construct(true);
auto error = test_description("module/type/", "sub-type");
EXPECT_EQ(error.description, "type/sub-type");
EXPECT_EQ(error.vendor_id, default_vendor_id);
construct(false);
error = test_description("module/type/", "sub-type");
EXPECT_EQ(error.description, "type/sub-type");
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
}
TEST_F(ErrorHandlingTesting, TypeFullD) {
construct(true);
auto error = test_description("module/type/", "");
EXPECT_EQ(error.description, "type");
EXPECT_EQ(error.vendor_id, default_vendor_id);
construct(false);
error = test_description("module/type/", "");
EXPECT_EQ(error.description, "type");
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
}
TEST_F(ErrorHandlingTesting, NoVendorId) {
Everest::error::Error raised_error{};
raised_error.type = "module/type";
raised_error.sub_type = "sub-type";
raised_error.message = "message";
raised_error.description = default_description;
raised_error.vendor_id = "";
construct(true);
error_handler->raise_inoperative_error(raised_error);
EXPECT_EQ(adapter.active_errors.size(), 1);
auto& error = adapter.active_errors.front();
EXPECT_EQ(error.type, "evse_manager/Inoperative");
EXPECT_EQ(error.sub_type, "");
EXPECT_EQ(error.message, raised_error.type);
EXPECT_EQ(error.description, "type/sub-type");
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
construct(false);
error_handler->raise_inoperative_error(raised_error);
EXPECT_EQ(adapter.active_errors.size(), 1);
error = adapter.active_errors.front();
EXPECT_EQ(error.type, "evse_manager/Inoperative");
EXPECT_EQ(error.sub_type, "");
EXPECT_EQ(error.message, raised_error.type);
EXPECT_EQ(error.description, "type/sub-type");
EXPECT_EQ(error.vendor_id, default_no_vendor_id);
}
TEST_F(ErrorHandlingTesting, IsActive) {
Everest::error::Error first_error{};
first_error.type = "module/type";
first_error.sub_type = "sub-type";
first_error.message = "message";
first_error.description = "description";
first_error.vendor_id = "vendor_id";
construct(true);
EXPECT_FALSE(p_evse->error_state_monitor->is_error_active("evse_manager/Inoperative", ""));
error_handler->raise_inoperative_error(first_error);
EXPECT_EQ(adapter.active_errors.size(), 1);
auto error = adapter.active_errors.front();
EXPECT_EQ(error.type, "evse_manager/Inoperative");
EXPECT_EQ(error.sub_type, "");
EXPECT_EQ(error.message, first_error.type);
EXPECT_EQ(error.description, "type/sub-type");
EXPECT_EQ(error.vendor_id, first_error.vendor_id);
EXPECT_TRUE(p_evse->error_state_monitor->is_error_active("evse_manager/Inoperative", ""));
Everest::error::Error raised_error{};
raised_error.type = "module/new-type";
raised_error.sub_type = "new-sub-type";
raised_error.message = "new-message";
raised_error.description = "new-description";
raised_error.vendor_id = "new-vendor_id";
// should not be a new error
error_handler->raise_inoperative_error(raised_error);
EXPECT_EQ(adapter.active_errors.size(), 1);
error = adapter.active_errors.front();
// should be first error
EXPECT_EQ(error.type, "evse_manager/Inoperative");
EXPECT_EQ(error.sub_type, "");
EXPECT_EQ(error.message, first_error.type);
EXPECT_EQ(error.description, "type/sub-type");
EXPECT_EQ(error.vendor_id, first_error.vendor_id);
}
} // namespace

View File

@@ -0,0 +1,94 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <EventQueue.hpp>
#include <gtest/gtest.h>
#include <condition_variable>
#include <mutex>
#include <thread>
namespace {
enum class ErrorHandlingEvents : std::uint8_t {
PreventCharging,
PreventChargingWelded,
AllErrorsCleared
};
TEST(EventQueue, init) {
module::EventQueue<ErrorHandlingEvents> queue;
auto events = queue.get_events();
EXPECT_EQ(events.size(), 0);
}
TEST(EventQueue, one) {
module::EventQueue<ErrorHandlingEvents> queue;
auto events = queue.get_events();
EXPECT_EQ(events.size(), 0);
queue.push(ErrorHandlingEvents::PreventCharging);
events = queue.get_events();
ASSERT_EQ(events.size(), 1);
EXPECT_EQ(events[0], ErrorHandlingEvents::PreventCharging);
events = queue.get_events();
EXPECT_EQ(events.size(), 0);
}
TEST(EventQueue, two) {
module::EventQueue<ErrorHandlingEvents> queue;
auto events = queue.get_events();
EXPECT_EQ(events.size(), 0);
queue.push(ErrorHandlingEvents::PreventCharging);
queue.push(ErrorHandlingEvents::PreventChargingWelded);
events = queue.get_events();
ASSERT_EQ(events.size(), 2);
EXPECT_EQ(events[0], ErrorHandlingEvents::PreventCharging);
EXPECT_EQ(events[1], ErrorHandlingEvents::PreventChargingWelded);
events = queue.get_events();
EXPECT_EQ(events.size(), 0);
}
TEST(EventQueue, wait) {
module::EventQueue<ErrorHandlingEvents> queue;
auto events = queue.get_events();
EXPECT_EQ(events.size(), 0);
std::size_t count = 0;
std::condition_variable cv;
std::mutex mux;
bool ready{false};
std::thread wait_thread([&cv, &count, &queue, &ready, &mux]() {
{
std::lock_guard<std::mutex> lock(mux);
ready = true;
}
cv.notify_one();
auto events = queue.wait();
count = events.size();
{
std::lock_guard<std::mutex> lock(mux);
ready = false;
}
cv.notify_one();
});
std::unique_lock<std::mutex> ul(mux);
cv.wait(ul, [&ready] { return ready; });
ASSERT_EQ(count, 0U);
queue.push(ErrorHandlingEvents::PreventCharging);
cv.wait(ul, [&ready] { return !ready; });
ASSERT_EQ(count, 1U);
events = queue.get_events();
EXPECT_EQ(events.size(), 0);
wait_thread.join();
}
} // namespace

View File

@@ -0,0 +1,117 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef EVSEMANAGERSTUB_H_
#define EVSEMANAGERSTUB_H_
#include <ErrorHandling.hpp>
#include <ModuleAdapterStub.hpp>
//-----------------------------------------------------------------------------
namespace module::stub {
struct evse_managerImplStub : public evse_managerImplBase {
evse_managerImplStub(Everest::ModuleAdapter* ev, const std::string& name) : evse_managerImplBase(ev, name) {
}
evse_managerImplStub() : evse_managerImplBase(nullptr, "manager") {
}
virtual void init() {
}
virtual void ready() {
}
virtual types::evse_manager::Evse handle_get_evse() {
return types::evse_manager::Evse();
}
virtual bool handle_enable(int& connector_id) {
return true;
}
virtual bool handle_disable(int& connector_id) {
return true;
}
virtual bool handle_enable_disable(int& connector_id, types::evse_manager::EnableDisableSource& cmd_source) {
return true;
}
virtual void handle_authorize_response(types::authorization::ProvidedIdToken& provided_token,
types::authorization::ValidationResult& validation_result) {
}
virtual void handle_withdraw_authorization() {
}
virtual bool handle_reserve(int& reservation_id) {
return true;
}
virtual void handle_cancel_reservation() {
}
virtual bool handle_pause_charging() {
return true;
}
virtual bool handle_resume_charging() {
return true;
}
virtual bool handle_stop_transaction(types::evse_manager::StopTransactionRequest& request) {
return true;
}
virtual bool handle_force_unlock(int& connector_id) {
return true;
}
virtual void handle_set_external_limits(types::energy::ExternalLimits& value) {
}
virtual types::evse_manager::SwitchThreePhasesWhileChargingResult
handle_switch_three_phases_while_charging(bool& three_phases) {
return types::evse_manager::SwitchThreePhasesWhileChargingResult::Success;
}
virtual bool handle_external_ready_to_start_charging() {
return true;
}
virtual void handle_set_plug_and_charge_configuration(
types::evse_manager::PlugAndChargeConfiguration& plug_and_charge_configuration) {
}
virtual types::evse_manager::UpdateAllowedEnergyTransferModesResult handle_update_allowed_energy_transfer_modes(
std::vector<types::iso15118::EnergyTransferMode>& allowed_energy_transfer_modes) {
return types::evse_manager::UpdateAllowedEnergyTransferModesResult::Accepted;
}
};
struct EvseManagerModuleAdapter : public ModuleAdapterStub {
EvseManagerModuleAdapter() : id("evse_manager", "main") {
}
ImplementationIdentifier id;
std::map<std::string, Everest::error::ErrorCallback> error_raise;
std::map<std::string, Everest::error::ErrorCallback> error_clear;
virtual std::shared_ptr<Everest::error::ErrorManagerReq> get_error_manager_req_fn(const Requirement& req) {
return std::make_shared<Everest::error::ErrorManagerReq>(
std::make_shared<Everest::error::ErrorTypeMap>(), std::make_shared<Everest::error::ErrorDatabaseMap>(),
std::list<Everest::error::ErrorType>({Everest::error::ErrorType("evse_board_support/VendorWarning")}),
[this](const Everest::error::ErrorType& error_type, const Everest::error::ErrorCallback& callback,
const Everest::error::ErrorCallback& clear_callback) {
error_raise[error_type] = callback;
error_clear[error_type] = clear_callback;
});
}
virtual std::shared_ptr<Everest::error::ErrorManagerImpl> get_error_manager_impl_fn(const std::string& str) {
return std::make_shared<Everest::error::ErrorManagerImpl>(
std::make_shared<Everest::error::ErrorTypeMap>(), std::make_shared<Everest::error::ErrorDatabaseMap>(),
std::list<Everest::error::ErrorType>(),
[this](const Everest::error::Error& error) {
std::printf("publish_raised_error\n");
if (error_raise.find(error.type) == error_raise.end()) {
throw std::runtime_error("Error type " + error.type + " not found");
}
error_raise[error.type](error);
},
[this](const Everest::error::Error& error) {
std::printf("publish_cleared_error\n");
if (error_raise.find(error.type) == error_raise.end()) {
throw std::runtime_error("Error type " + error.type + " not found");
}
error_clear[error.type](error);
},
false);
}
};
} // namespace module::stub
#endif

View File

@@ -0,0 +1,337 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "evse_board_supportIntfStub.hpp"
#include <EventQueue.hpp>
#include <IECStateMachine.hpp>
#include <backtrace.hpp>
#include <chrono>
#include <gtest/gtest.h>
#include <memory>
#include <string>
#include <thread>
namespace {
using BspEvent = types::board_support_common::BspEvent;
using Event = types::board_support_common::Event;
using Reason = types::evse_board_support::Reason;
using namespace std::chrono_literals;
struct BspStub : public module::stub::ModuleAdapterStub {
typedef Result (BspStub::*bsp_fn)(Parameters p);
ValueCallback event_cb;
std::map<std::string, bsp_fn> _bsp;
BspStub() : module::stub::ModuleAdapterStub() {
_bsp["allow_power_on"] = &BspStub::call_allow_power_on;
_bsp["enable"] = &BspStub::call_enable;
_bsp["cp_state_X1"] = &BspStub::call_cp_state_X1;
_bsp["pwm_on"] = &BspStub::call_pwm_on;
}
// ------------------------------------------------------------------------
// test interface (calls from BSP)
void raise_event(Event event) {
if (event_cb != nullptr) {
BspEvent bsp_event;
bsp_event.event = event;
event_cb(bsp_event);
}
};
// ------------------------------------------------------------------------
// BSP calls
virtual Result call_allow_power_on(Parameters p) {
std::cout << "call_allow_power_on(" << p << ")" << std::endl;
return std::nullopt;
}
virtual Result call_enable(Parameters p) {
std::cout << "call_enable(" << p << ")" << std::endl;
return std::nullopt;
}
virtual Result call_cp_state_X1(Parameters p) {
std::cout << "call_cp_state_X1(" << p << ")" << std::endl;
return std::nullopt;
}
virtual Result call_pwm_on(Parameters p) {
std::cout << "call_pwm_on(" << p << ")" << std::endl;
return std::nullopt;
}
// ------------------------------------------------------------------------
// internal interface
virtual void subscribe_fn(const Requirement&, const std::string& fn, ValueCallback cb) override {
std::printf("subscribe_fn(%s)\n", fn.c_str());
event_cb = cb;
}
virtual Result call_fn(const Requirement& req, const std::string& fn, Parameters p) override {
if (auto itt = _bsp.find(fn); itt == _bsp.end()) {
std::cout << "<missing> call_fn(" << fn << "," << p << ")" << std::endl;
return std::nullopt;
} else {
return std::invoke(itt->second, this, p);
}
}
};
TEST(IECStateMachine, init) {
module::stub::ModuleAdapterStub module_adapter = module::stub::ModuleAdapterStub();
std::unique_ptr<evse_board_supportIntf> bsp_if =
std::make_unique<module::stub::evse_board_supportIntfStub>(module_adapter);
module::IECStateMachine state_machine(std::move(bsp_if), true);
}
#if 0
// test to demonstrate the output from backtrace
struct BspStubTimeout : public BspStub {
Result call_allow_power_on(Parameters p) {
std::cout << "call_allow_power_on(" << p << ")" << std::endl;
std::cout << "call_allow_power_on: sleep (" << std::this_thread::get_id() << ")" << std::endl;
std::this_thread::sleep_for(200s);
std::cout << "call_allow_power_on: finished" << std::endl;
return std::nullopt;
}
};
TEST(IECStateMachine, init_subscribe) {
#ifdef EVEREST_USE_BACKTRACES
Everest::install_backtrace_handler();
#endif
BspStubTimeout bsp;
std::unique_ptr<evse_board_supportIntf> bsp_if = std::make_unique<module::stub::evse_board_supportIntfStub>(bsp);
module::IECStateMachine state_machine(std::move(bsp_if), true);
state_machine.enable(true);
state_machine.allow_power_on(true, Reason::FullPowerCharging);
bsp.raise_event(Event::A);
bsp.raise_event(Event::B);
bsp.raise_event(Event::PowerOn);
bsp.raise_event(Event::C);
std::cout << "main: sleep (" << std::this_thread::get_id() << ")" << std::endl;
std::this_thread::sleep_for(300s);
}
#endif
struct BspStubDeadlock : public BspStub {
std::vector<std::chrono::time_point<std::chrono::steady_clock>> call_cp_state_X1_times;
std::vector<std::chrono::time_point<std::chrono::steady_clock>> call_allow_power_on_times;
int count = 0;
enum class events_t : std::uint8_t {
call_allow_power_on,
call_enable,
call_cp_state_X1,
call_pwm_on,
signal_lock,
sleeping,
};
module::EventQueue<events_t> events;
std::string to_string(module::EventQueue<events_t>::events_t e) {
std::string result;
for (const auto& i : e) {
result += std::to_string(static_cast<std::uint8_t>(i));
result += " ";
}
return result;
}
bool contains(module::EventQueue<events_t>::events_t e, events_t event) {
for (const auto& i : e) {
if (i == event) {
return true;
}
}
return false;
}
auto wait() {
return events.wait();
}
virtual Result call_allow_power_on(Parameters p) {
std::cout << "call_allow_power_on(" << p << ")" << std::endl;
call_allow_power_on_times.push_back(std::chrono::steady_clock::now());
events.push(events_t::call_allow_power_on);
return std::nullopt;
}
virtual Result call_enable(Parameters p) {
std::cout << "call_enable(" << p << ")" << std::endl;
events.push(events_t::call_enable);
return std::nullopt;
}
virtual Result call_cp_state_X1(Parameters p) {
std::cout << "call_cp_state_X1(" << p << ")" << std::endl;
call_cp_state_X1_times.push_back(std::chrono::steady_clock::now());
if (count == 2) {
std::cout << "sleeping ..." << std::endl;
events.push(events_t::sleeping);
std::this_thread::sleep_for(7s);
}
count++;
events.push(events_t::call_cp_state_X1);
return std::nullopt;
}
virtual Result call_pwm_on(Parameters p) {
std::cout << "call_pwm_on(" << p << ")" << std::endl;
events.push(events_t::call_pwm_on);
return std::nullopt;
}
auto elapsed() const {
auto last_call_cp_state_X1_time = call_cp_state_X1_times.back();
auto last_call_allow_power_on_time = call_allow_power_on_times.back();
return std::chrono::duration_cast<std::chrono::milliseconds>(last_call_allow_power_on_time -
last_call_cp_state_X1_time)
.count();
}
};
TEST(IECStateMachine, deadlock_test) {
GTEST_SKIP() << "relies on thread timing so occasionally fails";
/*
* a deadlock was caused by timeout_state_c1 timing out and
* trying to raise an event while state_machine() was running
* and wanting to stop the timer
* 61851-1 state C starts the timer (from x2)
*
* check the timer runs for 6 seconds
*/
#ifdef EVEREST_USE_BACKTRACES
Everest::install_backtrace_handler();
#endif
BspStubDeadlock bsp;
std::unique_ptr<evse_board_supportIntf> bsp_if = std::make_unique<module::stub::evse_board_supportIntfStub>(bsp);
module::IECStateMachine state_machine(std::move(bsp_if), true);
std::uint8_t signal_lock_count = 0;
state_machine.signal_lock.connect([&signal_lock_count, &bsp]() {
signal_lock_count++;
std::cout << "signal_lock" << std::endl;
// bsp.events.push(BspStubDeadlock::events_t::signal_lock);
});
state_machine.enable(true);
auto actions = bsp.wait();
ASSERT_EQ(actions.size(), 1);
ASSERT_EQ(actions[0], BspStubDeadlock::events_t::call_enable);
bsp.raise_event(Event::A);
actions = bsp.wait();
ASSERT_TRUE(bsp.contains(actions, BspStubDeadlock::events_t::call_cp_state_X1));
bsp.raise_event(Event::B);
actions = bsp.wait();
// std::cout << bsp.to_string(actions) << std::endl;
// call_allow_power_on and/or signal_lock
// ASSERT_TRUE(bsp.contains(actions, BspStubDeadlock::events_t::call_allow_power_on));
state_machine.set_pwm(0.5);
actions = bsp.wait();
// std::cout << bsp.to_string(actions) << std::endl;
ASSERT_TRUE(bsp.contains(actions, BspStubDeadlock::events_t::call_pwm_on));
state_machine.allow_power_on(true, types::evse_board_support::Reason::FullPowerCharging);
std::this_thread::sleep_for(1s);
// actions = bsp.wait();
// ASSERT_TRUE(bsp.contains(actions, BspStubDeadlock::events_t::signal_lock));
bsp.raise_event(Event::C);
actions = bsp.wait();
// std::cout << bsp.to_string(actions) << std::endl;
ASSERT_TRUE(bsp.contains(actions, BspStubDeadlock::events_t::call_allow_power_on));
state_machine.set_cp_state_X1();
// expect state_machine to run once and then again when the
// timer expires
std::this_thread::sleep_for(8s);
auto elapsed = bsp.elapsed();
std::cout << "Elapsed: " << elapsed << std::endl;
EXPECT_GT(elapsed, 6000);
EXPECT_LT(elapsed, 6500);
}
TEST(IECStateMachine, deadlock_fix) {
GTEST_SKIP() << "relies on thread timing so occasionally fails";
/*
* a deadlock was caused by timeout_state_c1 timing out and
* trying to raise an event while state_machine() was running
* and wanting to stop the timer
* 61851-1 state C starts the timer (from X2)
*/
#ifdef EVEREST_USE_BACKTRACES
Everest::install_backtrace_handler();
#endif
BspStubDeadlock bsp;
std::unique_ptr<evse_board_supportIntf> bsp_if = std::make_unique<module::stub::evse_board_supportIntfStub>(bsp);
module::IECStateMachine state_machine(std::move(bsp_if), true);
std::uint8_t signal_lock_count = 0;
state_machine.signal_lock.connect([&signal_lock_count, &bsp]() {
signal_lock_count++;
std::cout << "signal_lock" << std::endl;
// bsp.events.push(BspStubDeadlock::events_t::signal_lock);
});
state_machine.enable(true);
auto actions = bsp.wait();
ASSERT_EQ(actions.size(), 1);
ASSERT_EQ(actions[0], BspStubDeadlock::events_t::call_enable);
bsp.raise_event(Event::A);
actions = bsp.wait();
// std::cout << bsp.to_string(actions) << std::endl;
ASSERT_TRUE(bsp.contains(actions, BspStubDeadlock::events_t::call_cp_state_X1));
bsp.raise_event(Event::B);
actions = bsp.wait();
// call_allow_power_on and/or signal_lock
// ASSERT_TRUE(bsp.contains(actions, BspStubDeadlock::events_t::call_allow_power_on));
state_machine.set_pwm(0.5);
actions = bsp.wait();
ASSERT_TRUE(bsp.contains(actions, BspStubDeadlock::events_t::call_pwm_on));
state_machine.allow_power_on(true, types::evse_board_support::Reason::FullPowerCharging);
std::this_thread::sleep_for(1s);
bsp.raise_event(Event::C);
actions = bsp.wait();
ASSERT_TRUE(bsp.contains(actions, BspStubDeadlock::events_t::call_allow_power_on));
state_machine.set_cp_state_X1();
// expect state_machine to run once and then again when the
// timer expires
actions = bsp.wait();
ASSERT_TRUE(bsp.contains(actions, BspStubDeadlock::events_t::call_cp_state_X1));
// this would previously trigger the deadlock
bsp.raise_event(Event::A);
actions = bsp.wait();
// std::cout << bsp.to_string(actions) << std::endl;
ASSERT_TRUE(bsp.contains(actions, BspStubDeadlock::events_t::sleeping));
std::this_thread::sleep_for(10s);
// if there is a deadlock the test won't finish
}
} // namespace

View File

@@ -0,0 +1,190 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <gtest/gtest.h>
#include "over_voltage/OverVoltageMonitor.hpp"
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>
using namespace module;
using namespace std::chrono_literals;
class OverVoltageMonitorTest : public ::testing::Test {
protected:
struct CallbackState {
std::mutex mtx;
std::condition_variable cv;
bool called{false};
OverVoltageMonitor::FaultType type;
std::string reason;
};
static OverVoltageMonitor make_monitor(CallbackState& state, std::chrono::milliseconds duration) {
return OverVoltageMonitor(
[&state](OverVoltageMonitor::FaultType type, const std::string& reason) {
std::lock_guard<std::mutex> lock(state.mtx);
state.called = true;
state.type = type;
state.reason = reason;
state.cv.notify_all();
},
duration);
}
static bool wait_for_callback(CallbackState& state, std::chrono::milliseconds timeout = 500ms) {
std::unique_lock<std::mutex> lock(state.mtx);
return state.cv.wait_for(lock, timeout, [&state] { return state.called; });
}
};
TEST_F(OverVoltageMonitorTest, no_fault_below_limits) {
CallbackState state;
auto monitor = make_monitor(state, 100ms);
monitor.set_limits(450.0, 420.0);
monitor.start_monitor();
monitor.update_voltage(400.0);
monitor.update_voltage(410.0);
EXPECT_FALSE(wait_for_callback(state));
}
TEST_F(OverVoltageMonitorTest, emergency_fault_triggers_immediately) {
CallbackState state;
auto monitor = make_monitor(state, 200ms);
monitor.set_limits(450.0, 420.0);
monitor.start_monitor();
monitor.update_voltage(460.0);
ASSERT_TRUE(wait_for_callback(state));
EXPECT_EQ(state.type, OverVoltageMonitor::FaultType::Emergency);
}
TEST_F(OverVoltageMonitorTest, error_fault_triggers_after_duration) {
CallbackState state;
auto monitor = make_monitor(state, 100ms);
monitor.set_limits(450.0, 420.0);
monitor.start_monitor();
monitor.update_voltage(430.0);
ASSERT_TRUE(wait_for_callback(state, 300ms));
EXPECT_EQ(state.type, OverVoltageMonitor::FaultType::Error);
}
TEST_F(OverVoltageMonitorTest, voltage_drop_cancels_error_timer) {
CallbackState state;
auto monitor = make_monitor(state, 150ms);
monitor.set_limits(450.0, 420.0);
monitor.start_monitor();
monitor.update_voltage(430.0); // above error limit
std::this_thread::sleep_for(50ms);
monitor.update_voltage(410.0); // below error limit
EXPECT_FALSE(wait_for_callback(state, 300ms));
}
TEST_F(OverVoltageMonitorTest, zero_duration_triggers_immediately) {
CallbackState state;
auto monitor = make_monitor(state, 0ms);
monitor.set_limits(450.0, 420.0);
monitor.start_monitor();
monitor.update_voltage(425.0);
ASSERT_TRUE(wait_for_callback(state));
EXPECT_EQ(state.type, OverVoltageMonitor::FaultType::Error);
}
TEST_F(OverVoltageMonitorTest, fault_is_latched) {
CallbackState state;
auto monitor = make_monitor(state, 50ms);
monitor.set_limits(450.0, 420.0);
monitor.start_monitor();
monitor.update_voltage(430.0);
ASSERT_TRUE(wait_for_callback(state));
{
std::lock_guard<std::mutex> lock(state.mtx);
state.called = false;
}
// Further voltage updates should not retrigger
monitor.update_voltage(460.0);
EXPECT_FALSE(wait_for_callback(state, 200ms));
}
TEST_F(OverVoltageMonitorTest, stop_monitor_suppresses_fault) {
CallbackState state;
auto monitor = make_monitor(state, 100ms);
monitor.set_limits(450.0, 420.0);
monitor.start_monitor();
monitor.update_voltage(430.0);
monitor.stop_monitor();
EXPECT_FALSE(wait_for_callback(state, 300ms));
}
TEST_F(OverVoltageMonitorTest, reset_clears_latched_fault_and_allows_retrigger) {
CallbackState state;
auto monitor = make_monitor(state, 50ms);
monitor.set_limits(450.0, 420.0);
monitor.start_monitor();
// First fault
monitor.update_voltage(430.0);
ASSERT_TRUE(wait_for_callback(state, 300ms));
EXPECT_EQ(state.type, OverVoltageMonitor::FaultType::Error);
// Clear callback state
{
std::lock_guard<std::mutex> lock(state.mtx);
state.called = false;
}
// Reset should clear latch and timers
monitor.reset();
// Must restart monitoring explicitly
monitor.start_monitor();
// Second fault should be possible again
monitor.update_voltage(430.0);
ASSERT_TRUE(wait_for_callback(state, 300ms));
EXPECT_EQ(state.type, OverVoltageMonitor::FaultType::Error);
}
TEST_F(OverVoltageMonitorTest, multiple_voltage_updates_update_timer_snapshot) {
CallbackState state;
auto monitor = make_monitor(state, 100ms);
monitor.set_limits(500.0, 420.0);
monitor.start_monitor();
// First update arms the timer
monitor.update_voltage(430.0);
// Subsequent updates while timer is active
monitor.update_voltage(435.0);
monitor.update_voltage(432.0);
monitor.update_voltage(440.0); // highest value
ASSERT_TRUE(wait_for_callback(state, 300ms));
EXPECT_EQ(state.type, OverVoltageMonitor::FaultType::Error);
}

View File

@@ -0,0 +1,168 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <gtest/gtest.h>
#include "voltage_plausibility/VoltagePlausibilityMonitor.hpp"
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <string>
#include <thread>
using namespace module;
using namespace std::chrono_literals;
class VoltagePlausibilityMonitorTest : public ::testing::Test {
protected:
struct CallbackState {
std::mutex mtx;
std::condition_variable cv;
bool called{false};
std::string reason;
};
static VoltagePlausibilityMonitor make_monitor(CallbackState& state, double max_spread_threshold_V,
std::chrono::milliseconds fault_duration) {
return VoltagePlausibilityMonitor(
[&state](const std::string& reason) {
std::lock_guard<std::mutex> lock(state.mtx);
state.called = true;
state.reason = reason;
state.cv.notify_all();
},
max_spread_threshold_V, fault_duration);
}
static bool wait_for_callback(CallbackState& state, std::chrono::milliseconds timeout = 500ms) {
std::unique_lock<std::mutex> lock(state.mtx);
return state.cv.wait_for(lock, timeout, [&state] { return state.called; });
}
static void clear_callback_state(CallbackState& state) {
std::lock_guard<std::mutex> lock(state.mtx);
state.called = false;
state.reason.clear();
}
static void set_two_sources(VoltagePlausibilityMonitor& monitor, double power_supply_V, double powermeter_V) {
monitor.update_power_supply_voltage(power_supply_V);
monitor.update_powermeter_voltage(powermeter_V);
}
};
TEST_F(VoltagePlausibilityMonitorTest, no_fault_below_threshold) {
CallbackState state;
auto monitor = make_monitor(state, /*max_spread_threshold_V=*/10.0, /*fault_duration=*/50ms);
monitor.start_monitor();
set_two_sources(monitor, 400.0, 405.0); // spread 5 V <= 10 V
EXPECT_FALSE(wait_for_callback(state, 200ms));
monitor.stop_monitor();
}
TEST_F(VoltagePlausibilityMonitorTest, requires_at_least_two_sources) {
CallbackState state;
auto monitor = make_monitor(state, /*max_spread_threshold_V=*/1.0, /*fault_duration=*/50ms);
monitor.start_monitor();
monitor.update_power_supply_voltage(400.0); // only one source available
EXPECT_FALSE(wait_for_callback(state, 200ms));
monitor.stop_monitor();
}
TEST_F(VoltagePlausibilityMonitorTest, zero_duration_triggers_immediately) {
CallbackState state;
auto monitor = make_monitor(state, /*max_spread_threshold_V=*/5.0, /*fault_duration=*/0ms);
monitor.start_monitor();
set_two_sources(monitor, 400.0, 420.0); // spread 20 V > 5 V
ASSERT_TRUE(wait_for_callback(state, 200ms));
EXPECT_NE(state.reason.find("fault immediately"), std::string::npos);
}
TEST_F(VoltagePlausibilityMonitorTest, spread_above_threshold_triggers_after_duration) {
CallbackState state;
auto monitor = make_monitor(state, /*max_spread_threshold_V=*/5.0, /*fault_duration=*/50ms);
monitor.start_monitor();
set_two_sources(monitor, 400.0, 420.0); // arms timer
ASSERT_TRUE(wait_for_callback(state, 300ms));
EXPECT_NE(state.reason.find("for at least"), std::string::npos);
EXPECT_NE(state.reason.find("exceeded threshold"), std::string::npos);
}
TEST_F(VoltagePlausibilityMonitorTest, returning_within_threshold_cancels_timer) {
CallbackState state;
auto monitor = make_monitor(state, /*max_spread_threshold_V=*/5.0, /*fault_duration=*/120ms);
monitor.start_monitor();
set_two_sources(monitor, 400.0, 420.0); // spread 20 V -> arm timer
std::this_thread::sleep_for(40ms);
// Bring spread back within the threshold before the timer expires
monitor.update_powermeter_voltage(402.0); // spread now 2 V -> cancel timer
EXPECT_FALSE(wait_for_callback(state, 250ms));
}
TEST_F(VoltagePlausibilityMonitorTest, fault_is_latched_and_not_retriggered) {
CallbackState state;
auto monitor = make_monitor(state, /*max_spread_threshold_V=*/1.0, /*fault_duration=*/0ms);
monitor.start_monitor();
set_two_sources(monitor, 400.0, 405.0); // immediate fault
ASSERT_TRUE(wait_for_callback(state, 200ms));
clear_callback_state(state);
// After a fault the monitor stops and latches the fault; further updates must not retrigger.
set_two_sources(monitor, 410.0, 430.0);
EXPECT_FALSE(wait_for_callback(state, 150ms));
}
TEST_F(VoltagePlausibilityMonitorTest, stop_monitor_suppresses_fault) {
CallbackState state;
auto monitor = make_monitor(state, /*max_spread_threshold_V=*/5.0, /*fault_duration=*/80ms);
monitor.start_monitor();
set_two_sources(monitor, 400.0, 420.0); // arm timer
monitor.stop_monitor(); // cancels timer
EXPECT_FALSE(wait_for_callback(state, 250ms));
}
TEST_F(VoltagePlausibilityMonitorTest, reset_clears_samples_so_old_values_are_not_used) {
CallbackState state;
auto monitor = make_monitor(state, /*max_spread_threshold_V=*/5.0, /*fault_duration=*/80ms);
monitor.start_monitor();
// Prime both sources with a large spread, then stop and reset.
set_two_sources(monitor, 400.0, 420.0);
monitor.stop_monitor();
monitor.reset();
// Restart monitoring. Provide only a single new source sample.
// If reset() didn't clear the old powermeter sample timestamp, we'd still have 2 sources and might arm/fault.
monitor.start_monitor();
monitor.update_power_supply_voltage(400.0);
EXPECT_FALSE(wait_for_callback(state, 250ms));
}

View File

@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef EVSE_BOARD_SUPPORTINTFSTUB_H_
#define EVSE_BOARD_SUPPORTINTFSTUB_H_
#include "ModuleAdapterStub.hpp"
#include <generated/interfaces/evse_board_support/Interface.hpp>
//-----------------------------------------------------------------------------
namespace module::stub {
struct evse_board_supportIntfStub : public evse_board_supportIntf {
explicit evse_board_supportIntfStub(ModuleAdapterStub& adapter) :
evse_board_supportIntf(&adapter, Requirement{"requirement", 1}, "EvseManager", std::nullopt) {
}
};
} // namespace module::stub
#endif