From 9fba2a5273c541c15bb4ea08e36a70fe77368423 Mon Sep 17 00:00:00 2001 From: Michael Heimpold Date: Wed, 6 May 2026 11:36:32 +0200 Subject: [PATCH 1/7] CbParsleyDriver: improve error handling while charging When a safety error was raised during charging, let's tell EvseManager about the state chance on CE line. While at, also improve the error message when state F is requested. Signed-off-by: Michael Heimpold --- .../evse_board_supportImpl.cpp | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp index 9aa98bc..d3dae1c 100644 --- a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp +++ b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp @@ -223,14 +223,37 @@ void evse_board_supportImpl::handle_pwm_on(double& value) { } void evse_board_supportImpl::handle_cp_state_X1() { - EVLOG_info << "handle_cp_state_X1: setting new duty cycle of 100.0% (ignored)"; + if (this->mod->controller.is_emergency() and this->cp_current_state == types::cb_board_support::CPState::C) { + EVLOG_info << "handle_cp_state_X1: setting new duty cycle of 100.0% (in safe state)"; + + // When safety controller has gone into safe state, then we raised an error to EvseManager. + // EvseManagers reaction is to instruct us to go to 100%. A typical EV would switch to + // state B then. Here, the safety controller already opened S_s_3 which results in EC. + // The safety controller _could_ report EC (or then B0) but in case of CE malfunction, + // this could also not happen. To satisfy EvseManager, we report that we see state B. + // This prevents loosing sync and the error "Timeout of 6 seconds reached, EV did not + // go back to state B after PWM was switched off. Powering off under load.". + this->publish_event({types::board_support_common::Event::B}); + } else { + EVLOG_info << "handle_cp_state_X1: setting new duty cycle of 100.0% (ignored)"; + } } void evse_board_supportImpl::handle_cp_state_F() { std::scoped_lock lock(this->cp_mutex); try { - EVLOG_info << "handle_cp_state_F: generating CP state F (aka EC)"; - + if (this->mod->controller.is_emergency()) { + EVLOG_info << "handle_cp_state_F: ignored, safe state already active"; + } else { + if (this->cp_current_state == types::cb_board_support::CPState::B) { + EVLOG_info << "handle_cp_state_F: forcing safe state (via state B0)"; + } else if (this->cp_current_state == types::cb_board_support::CPState::C) { + EVLOG_info << "handle_cp_state_F: forcing safe state (via state EC)"; + } else { + EVLOG_info << "handle_cp_state_F: forcing safe state (in state A)"; + } + } + // we can unconditionally call into the method (it already checks itself) this->mod->controller.set_ec_state(); } catch (std::exception& e) { EVLOG_error << e.what(); From 0bb6ed2d5b3063e85c53e66b17425fcec6281c8e Mon Sep 17 00:00:00 2001 From: Michael Heimpold Date: Wed, 6 May 2026 14:41:18 +0200 Subject: [PATCH 2/7] CbParsleyDriver: filter log noise We can filter out some messages to improve readability of the overall logs. Signed-off-by: Michael Heimpold --- .../evse_board_support/evse_board_supportImpl.cpp | 4 ++++ modules/CbParsleyDriver/mcs/cb_mcsImpl.cpp | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp index d3dae1c..e745f07 100644 --- a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp +++ b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp @@ -83,6 +83,10 @@ void evse_board_supportImpl::init() { std::scoped_lock lock(this->cp_mutex); auto current_cp_state = cestate_to_cpstate(ce_state); + // filter out uninitialized value (e.g. after reset) + if (current_cp_state == types::cb_board_support::CPState::PowerOn) + return; + // B0 is mapped to D; we need to ignore B0 and not forward it here if (current_cp_state == types::cb_board_support::CPState::D) return; diff --git a/modules/CbParsleyDriver/mcs/cb_mcsImpl.cpp b/modules/CbParsleyDriver/mcs/cb_mcsImpl.cpp index d4a874d..6da5cf0 100644 --- a/modules/CbParsleyDriver/mcs/cb_mcsImpl.cpp +++ b/modules/CbParsleyDriver/mcs/cb_mcsImpl.cpp @@ -9,9 +9,11 @@ namespace mcs { void cb_mcsImpl::init() { // register our callback handlers this->mod->controller.on_id_change.connect([&](const types::cb_board_support::IDState& id_state) { - EVLOG_info << "ID change detected: " << this->last_id_state << " → " << id_state; - this->publish_id_state(id_state); - this->last_id_state = id_state; + if (this->last_id_state != id_state) { + EVLOG_info << "ID change detected: " << this->last_id_state << " → " << id_state; + this->publish_id_state(id_state); + this->last_id_state = id_state; + } }); this->mod->controller.on_ce_change.connect([&](const types::cb_board_support::CEState& ce_state) { From 43579dc5680d6c8565ff4fccd67bccd3de021099 Mon Sep 17 00:00:00 2001 From: Michael Heimpold Date: Thu, 7 May 2026 11:59:27 +0200 Subject: [PATCH 3/7] CbParsleyDriver: prevent warning for event conversion We cannot convert PilotFaults to a BSP event, so let's exit early here. Signed-off-by: Michael Heimpold --- .../evse_board_support/evse_board_supportImpl.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp index e745f07..e4d2369 100644 --- a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp +++ b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp @@ -106,6 +106,9 @@ void evse_board_supportImpl::init() { this->mod->controller.enable(); } + if (current_cp_state == types::cb_board_support::CPState::PilotFault) + return; + try { const types::board_support_common::BspEvent tmp = cpstate_to_bspevent(current_cp_state); this->publish_event(tmp); From bdd71f815eb0f9f53571ac823fef1418f19d39e3 Mon Sep 17 00:00:00 2001 From: Michael Heimpold Date: Thu, 7 May 2026 12:00:54 +0200 Subject: [PATCH 4/7] CbParsleyDriver: try to recover using ID state change In case of CE malfunction error, try to recover when an ID state change is detected. Signed-off-by: Michael Heimpold --- .../evse_board_supportImpl.cpp | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp index e4d2369..24f0f82 100644 --- a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp +++ b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp @@ -201,6 +201,26 @@ void evse_board_supportImpl::init() { this->clear_error("evse_board_support/VendorWarning", module_str); } }); + + this->mod->controller.on_id_change.connect([&](const types::cb_board_support::IDState& id_state) { + // general note: logging of state changes is already done in MCS interface, so we don't double it here + + // we use ID changes only to have a trigger to recover from emergency states: usually such a reset + // is done when EV is disconnected and we see a CE state change from X to A, but in case of + // CE malfunction, we might not see it and thus we would be stuck in this state; using ID here + // could help to recover + // - + if (this->cp_current_state == types::cb_board_support::CPState::PilotFault and + this->mod->controller.is_emergency() and id_state == types::cb_board_support::IDState::NotConnected) { + EVLOG_info << "recovering after safe state (ID triggered)"; + + // disable resets the controller + this->mod->controller.disable(); + + // enable starts UART frame processing again + this->mod->controller.enable(); + } + }); } void evse_board_supportImpl::ready() { From 035a882043def0372fac4bfae6b8e51fc8b391c8 Mon Sep 17 00:00:00 2001 From: Michael Heimpold Date: Fri, 8 May 2026 13:00:17 +0200 Subject: [PATCH 5/7] CbParsleyDriver: clarify log messages and also log raw CE state Signed-off-by: Michael Heimpold --- .../evse_board_supportImpl.cpp | 16 ++++++++++++---- .../evse_board_supportImpl.hpp | 3 +++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp index 24f0f82..b2dfd85 100644 --- a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp +++ b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp @@ -81,6 +81,14 @@ void evse_board_supportImpl::init() { this->mod->controller.on_ce_change.connect([&](const types::cb_board_support::CEState& ce_state) { std::scoped_lock lock(this->cp_mutex); + + if (this->ce_current_state != ce_state) { + EVLOG_info << "CE change detected: " << this->ce_current_state << " → " << ce_state; + } else { + EVLOG_debug << "CE change detected: " << this->ce_current_state << " → " << ce_state; + } + this->ce_current_state = ce_state; + auto current_cp_state = cestate_to_cpstate(ce_state); // filter out uninitialized value (e.g. after reset) @@ -91,7 +99,7 @@ void evse_board_supportImpl::init() { if (current_cp_state == types::cb_board_support::CPState::D) return; - EVLOG_info << "CP state change from " << this->cp_current_state << " to " << current_cp_state; + EVLOG_info << "simulate CP change: " << this->cp_current_state << " → " << current_cp_state; this->cp_current_state = current_cp_state; // in case safety controller was in emergency state and EV is gone, @@ -273,11 +281,11 @@ void evse_board_supportImpl::handle_cp_state_F() { EVLOG_info << "handle_cp_state_F: ignored, safe state already active"; } else { if (this->cp_current_state == types::cb_board_support::CPState::B) { - EVLOG_info << "handle_cp_state_F: forcing safe state (via state B0)"; + EVLOG_info << "handle_cp_state_F: forcing safe state (via CE state B0)"; } else if (this->cp_current_state == types::cb_board_support::CPState::C) { - EVLOG_info << "handle_cp_state_F: forcing safe state (via state EC)"; + EVLOG_info << "handle_cp_state_F: forcing safe state (via CE state EC)"; } else { - EVLOG_info << "handle_cp_state_F: forcing safe state (in state A)"; + EVLOG_info << "handle_cp_state_F: forcing safe state (in CP state A)"; } } // we can unconditionally call into the method (it already checks itself) diff --git a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.hpp b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.hpp index d37960a..1a2f566 100644 --- a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.hpp +++ b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.hpp @@ -71,6 +71,9 @@ class evse_board_supportImpl : public evse_board_supportImplBase { /// @brief Tracks whether this EVSE is enabled or not. std::atomic_bool is_enabled {false}; + /// @brief Tracks the last seen CE state. + std::atomic ce_current_state {types::cb_board_support::CEState::PowerOn}; + /// @brief Tracks the last published CP state. types::cb_board_support::CPState cp_current_state {types::cb_board_support::CPState::PowerOn}; From 56519e8bfcc535697e0febc5bbde71cc1dfd418b Mon Sep 17 00:00:00 2001 From: Michael Heimpold Date: Fri, 8 May 2026 13:00:53 +0200 Subject: [PATCH 6/7] CbParsleyDriver: fix CE state EC expectation The Safety Controller does not need to enter safe state, so let's also check whether CE state has an expected level after the request. Signed-off-by: Michael Heimpold --- modules/CbParsleyDriver/parsley/CbParsley.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/CbParsleyDriver/parsley/CbParsley.cpp b/modules/CbParsleyDriver/parsley/CbParsley.cpp index 1da619f..5be357d 100644 --- a/modules/CbParsleyDriver/parsley/CbParsley.cpp +++ b/modules/CbParsleyDriver/parsley/CbParsley.cpp @@ -580,10 +580,14 @@ void CbParsley::set_ec_state() { n = static_cast(cb_uart_com::COM_CHARGE_STATE_2); std::unique_lock cs_lock(this->ctx_mutexes[n]); - // we should see that the safety controller enters safe state within at max 1s (FIXME) - if (not this->rx_cv[n].wait_for( - cs_lock, 1s, [&] { return cb_proto_get_safe_state_active(&this->ctx) == CS_SAFESTATE_ACTIVE_SAFESTATE; })) { - throw std::runtime_error("Safety Controller did not entered safe state as requested"); + // we should see that the safety controller enters a "safe" state (but + // not necessarily _the_ safe state) within at max 1s (FIXME) + if (not this->rx_cv[n].wait_for(cs_lock, 1s, [&] { + return cb_proto_get_safe_state_active(&this->ctx) == CS_SAFESTATE_ACTIVE_SAFESTATE or + cb_proto_get_ce_state(&this->ctx) == cs2_ce_state::CS2_CE_STATE_B0 or + cb_proto_get_ce_state(&this->ctx) == cs2_ce_state::CS2_CE_STATE_EC; + })) { + throw std::runtime_error("Safety Controller did not report a safe state as expected"); } } From c59bdbfe4d93d409d30e339066eaa212e4ea7d8f Mon Sep 17 00:00:00 2001 From: Michael Heimpold Date: Fri, 8 May 2026 14:55:04 +0200 Subject: [PATCH 7/7] CbParsleyDriver: bind error handling to raw CE state instead of CP simulation Signed-off-by: Michael Heimpold --- .../evse_board_supportImpl.cpp | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp index b2dfd85..6e7ea2c 100644 --- a/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp +++ b/modules/CbParsleyDriver/evse_board_support/evse_board_supportImpl.cpp @@ -81,6 +81,7 @@ void evse_board_supportImpl::init() { this->mod->controller.on_ce_change.connect([&](const types::cb_board_support::CEState& ce_state) { std::scoped_lock lock(this->cp_mutex); + const types::cb_board_support::CEState previous_ce_state = this->ce_current_state; if (this->ce_current_state != ce_state) { EVLOG_info << "CE change detected: " << this->ce_current_state << " → " << ce_state; @@ -89,23 +90,19 @@ void evse_board_supportImpl::init() { } this->ce_current_state = ce_state; - auto current_cp_state = cestate_to_cpstate(ce_state); - // filter out uninitialized value (e.g. after reset) - if (current_cp_state == types::cb_board_support::CPState::PowerOn) + if (ce_state == types::cb_board_support::CEState::PowerOn) return; - // B0 is mapped to D; we need to ignore B0 and not forward it here - if (current_cp_state == types::cb_board_support::CPState::D) + // we need to ignore B0 and not forward it here + if (ce_state == types::cb_board_support::CEState::B0) return; - EVLOG_info << "simulate CP change: " << this->cp_current_state << " → " << current_cp_state; - this->cp_current_state = current_cp_state; - // in case safety controller was in emergency state and EV is gone, // we have to reset safety controller with a disable -> enable toggle - if (current_cp_state == types::cb_board_support::CPState::A and this->mod->controller.is_emergency()) { - EVLOG_info << "recovering after safe state"; + if (this->mod->controller.is_emergency() and previous_ce_state != types::cb_board_support::CEState::A and + ce_state == types::cb_board_support::CEState::A) { + EVLOG_info << "recovering after safe state (CE triggered)"; // disable resets the controller this->mod->controller.disable(); @@ -114,11 +111,15 @@ void evse_board_supportImpl::init() { this->mod->controller.enable(); } - if (current_cp_state == types::cb_board_support::CPState::PilotFault) + auto new_cp_state = cestate_to_cpstate(ce_state); + EVLOG_info << "simulate CP change: " << this->cp_current_state << " → " << new_cp_state; + this->cp_current_state = new_cp_state; + + if (new_cp_state == types::cb_board_support::CPState::PilotFault) return; try { - const types::board_support_common::BspEvent tmp = cpstate_to_bspevent(current_cp_state); + const types::board_support_common::BspEvent tmp = cpstate_to_bspevent(new_cp_state); this->publish_event(tmp); } catch (std::runtime_error& e) { // should never happen, when all invalid states are handled correctly @@ -217,9 +218,11 @@ void evse_board_supportImpl::init() { // is done when EV is disconnected and we see a CE state change from X to A, but in case of // CE malfunction, we might not see it and thus we would be stuck in this state; using ID here // could help to recover - // - - if (this->cp_current_state == types::cb_board_support::CPState::PilotFault and - this->mod->controller.is_emergency() and id_state == types::cb_board_support::IDState::NotConnected) { + if (this->mod->controller.is_emergency() and + ((this->ce_current_state == types::cb_board_support::CEState::Invalid and + id_state == types::cb_board_support::IDState::NotConnected) or + (this->ce_current_state == types::cb_board_support::CEState::A and + id_state == types::cb_board_support::IDState::Connected))) { EVLOG_info << "recovering after safe state (ID triggered)"; // disable resets the controller