From 3b6870a3b06e02acc93769abfa547eb6f16f6b82 Mon Sep 17 00:00:00 2001 From: Cornelius Claussen Date: Thu, 13 Jun 2024 11:02:20 +0200 Subject: [PATCH] CableCheck: Adapt to IEC61851-23:2023 (Issue #649) Signed-off-by: Cornelius Claussen --- config/config-sil-dc.yaml | 3 + dependencies.yaml | 2 +- errors/isolation_monitor.yaml | 20 ++ errors/powermeter.yaml | 6 + errors/system.yaml | 6 + interfaces/isolation_monitor.yaml | 19 +- interfaces/powermeter.yaml | 2 + interfaces/system.yaml | 2 + modules/EvseManager/Charger.cpp | 7 +- modules/EvseManager/Charger.hpp | 2 +- modules/EvseManager/ErrorHandling.cpp | 95 +++++- modules/EvseManager/ErrorHandling.hpp | 18 +- modules/EvseManager/EvseManager.cpp | 316 +++++++++++++----- modules/EvseManager/EvseManager.hpp | 14 +- modules/EvseManager/manifest.yaml | 20 +- modules/EvseManager/scoped_lock_timeout.hpp | 3 + .../main/power_supply_DCImpl.hpp | 2 +- .../main/isolation_monitorImpl.cpp | 16 +- .../main/isolation_monitorImpl.hpp | 8 +- modules/simulation/IMDSimulator/manifest.yaml | 4 + types/evse_manager.yaml | 1 + 21 files changed, 451 insertions(+), 115 deletions(-) create mode 100644 errors/isolation_monitor.yaml create mode 100644 errors/powermeter.yaml create mode 100644 errors/system.yaml diff --git a/config/config-sil-dc.yaml b/config/config-sil-dc.yaml index ef2f029410..0002d575b4 100644 --- a/config/config-sil-dc.yaml +++ b/config/config-sil-dc.yaml @@ -54,6 +54,9 @@ active_modules: slac: module: JsSlacSimulator imd: + config_implementation: + main: + selftest_success: true module: IMDSimulator ev_manager: module: JsEvManager diff --git a/dependencies.yaml b/dependencies.yaml index 3cbe51a9d5..2e0dc1dd43 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -4,7 +4,7 @@ --- everest-framework: git: https://github.com/EVerest/everest-framework.git - git_tag: v0.14.0 + git_tag: 82117ad705f835e1cf8a3c6d61cd70aa4d44d372 options: ["BUILD_TESTING OFF"] sigslot: git: https://github.com/palacaze/sigslot diff --git a/errors/isolation_monitor.yaml b/errors/isolation_monitor.yaml new file mode 100644 index 0000000000..9d1f44d634 --- /dev/null +++ b/errors/isolation_monitor.yaml @@ -0,0 +1,20 @@ +description: >- + Errors for Isolation Monitor + + Note that actual isolation faults should just be reported as resistance values, + EvseManager will interpret them according to the limits given in the norm and stop charging. + + This is only to report device errors to indicate valid isolation resistance measurements etc + are no longer possible. +errors: + - name: DeviceFault + description: The IMD device is not fully functional anymore and cannot be used to monitor the isolation resistance. + - name: CommunicationFault + description: >- + The communication to the hardware or underlying driver is lost or has errors. + - name: VendorError + description: >- + Vendor specific error code. Will stop charging session. + - name: VendorWarning + description: >- + Vendor specific error code. Charging may continue. diff --git a/errors/powermeter.yaml b/errors/powermeter.yaml new file mode 100644 index 0000000000..d387d1fcf0 --- /dev/null +++ b/errors/powermeter.yaml @@ -0,0 +1,6 @@ +description: >- + Errors for Powermeter +errors: + - name: CommunicationFault + description: >- + The communication to the hardware or underlying driver is lost or has errors. diff --git a/errors/system.yaml b/errors/system.yaml new file mode 100644 index 0000000000..51c238357e --- /dev/null +++ b/errors/system.yaml @@ -0,0 +1,6 @@ +description: >- + Errors for System +errors: + - name: CommunicationFault + description: >- + The communication to the hardware or underlying driver is lost or has errors. diff --git a/interfaces/isolation_monitor.yaml b/interfaces/isolation_monitor.yaml index 1aba8a513f..16e39dee59 100644 --- a/interfaces/isolation_monitor.yaml +++ b/interfaces/isolation_monitor.yaml @@ -12,8 +12,25 @@ cmds: description: >- Stop recurring measurements. The device should stop to monitor the isolation resistance and stop publishing the data. + start_self_test: + description: >- + Start self test. This will be done during the CableCheck phase, so a DC voltage will be present + according to IEC 61851-23 (2023). The command should return immediately. + The "self_test_result" variable must be published once the self testing is done. + Note that on many hardware devices this can take a long time (e.g. 20 seconds). + arguments: + test_voltage_V: + description: >- + Specifies the test voltage [V] that is applied on the DC pins during self test. + This can be used to verify the internal voltage measurement of the IMD. + type: number vars: - IsolationMeasurement: + isolation_measurement: description: Isolation monitoring measurement results type: object $ref: /isolation_monitor#/IsolationMeasurement + self_test_result: + description: Indicates the self test is done and publishes the result. Set "true" for success, "false" for failure. + type: boolean +errors: + - reference: /errors/isolation_monitor diff --git a/interfaces/powermeter.yaml b/interfaces/powermeter.yaml index 90acfe2309..0c8f5a9a82 100644 --- a/interfaces/powermeter.yaml +++ b/interfaces/powermeter.yaml @@ -26,3 +26,5 @@ vars: description: Measured dataset type: object $ref: /powermeter#/Powermeter +errors: + - reference: /errors/powermeter diff --git a/interfaces/system.yaml b/interfaces/system.yaml index 7363fdc542..e7977ae707 100644 --- a/interfaces/system.yaml +++ b/interfaces/system.yaml @@ -75,3 +75,5 @@ vars: type: object $ref: /system#/LogStatus +errors: + - reference: /errors/system diff --git a/modules/EvseManager/Charger.cpp b/modules/EvseManager/Charger.cpp index 3036eba338..190688f1ab 100644 --- a/modules/EvseManager/Charger.cpp +++ b/modules/EvseManager/Charger.cpp @@ -962,7 +962,8 @@ bool Charger::set_max_current(float c, std::chrono::time_point // is it still valid? if (validUntil > date::utc_clock::now()) { { - std::lock_guard lock(state_machine_mutex); + Everest::scoped_lock_timeout lock(state_machine_mutex, + Everest::MutexDescription::Charger_pause_charging); shared_context.max_current = c; shared_context.max_current_valid_until = validUntil; } @@ -1524,7 +1525,9 @@ void Charger::check_soft_over_current() { // i.e. max_current is in valid range bool Charger::power_available() { if (shared_context.max_current_valid_until < date::utc_clock::now()) { - EVLOG_warning << "Power budget expired, falling back to 0."; + EVLOG_warning << "Power budget expired, falling back to 0. Last update: " + << Everest::Date::to_rfc3339(shared_context.max_current_valid_until) + << " Now:" << Everest::Date::to_rfc3339(date::utc_clock::now()); if (shared_context.max_current > 0.) { shared_context.max_current = 0.; signal_max_current(shared_context.max_current); diff --git a/modules/EvseManager/Charger.hpp b/modules/EvseManager/Charger.hpp index a2269a1513..20f24c6357 100644 --- a/modules/EvseManager/Charger.hpp +++ b/modules/EvseManager/Charger.hpp @@ -348,7 +348,7 @@ class Charger { EventQueue error_handling_event_queue; // constants - static constexpr float CHARGER_ABSOLUTE_MAX_CURRENT{80.}; + static constexpr float CHARGER_ABSOLUTE_MAX_CURRENT{1000.}; constexpr static int LEGACY_WAKEUP_TIMEOUT{30000}; // valid Length of BCB toggles static constexpr auto TP_EV_VALD_STATE_DURATION_MIN = diff --git a/modules/EvseManager/ErrorHandling.cpp b/modules/EvseManager/ErrorHandling.cpp index 6907669ec1..50e4e89e2d 100644 --- a/modules/EvseManager/ErrorHandling.cpp +++ b/modules/EvseManager/ErrorHandling.cpp @@ -21,8 +21,14 @@ ErrorHandling::ErrorHandling(const std::unique_ptr& _r_b const std::vector>& _r_hlc, const std::vector>& _r_connector_lock, const std::vector>& _r_ac_rcd, - const std::unique_ptr& _p_evse) : - r_bsp(_r_bsp), r_hlc(_r_hlc), r_connector_lock(_r_connector_lock), r_ac_rcd(_r_ac_rcd), p_evse(_p_evse) { + const std::unique_ptr& _p_evse, + const std::vector>& _r_imd) : + 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) { if (r_hlc.size() > 0) { hlc = true; @@ -163,6 +169,52 @@ ErrorHandling::ErrorHandling(const std::unique_ptr& _r_b } }); } + + // Subscribe to ac_rcd to receive errors from IMD hardware + if (r_imd.size() > 0) { + r_imd[0]->subscribe_all_errors( + [this](const Everest::error::Error& error) { + types::evse_manager::ErrorEnum evse_error{types::evse_manager::ErrorEnum::VendorWarning}; + types::evse_manager::Error output_error; + output_error.error_description = error.description; + output_error.error_severity = to_evse_manager_severity(error.severity); + + if (modify_error_imd(error, true, evse_error)) { + // signal to charger a new error has been set that prevents charging + output_error.error_code = evse_error; + signal_error(output_error, true); + } else { + // signal an error that does not prevent charging + output_error.error_code = evse_error; + signal_error(output_error, false); + } + }, + [this](const Everest::error::Error& error) { + types::evse_manager::ErrorEnum evse_error{types::evse_manager::ErrorEnum::VendorWarning}; + types::evse_manager::Error output_error; + output_error.error_description = error.description; + output_error.error_severity = to_evse_manager_severity(error.severity); + + if (modify_error_imd(error, false, evse_error)) { + // signal to charger an error has been cleared that prevents charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, true); + } else { + // signal an error cleared that does not prevent charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, false); + } + + if (active_errors.all_cleared()) { + // signal to charger that all errors are cleared now + signal_all_errors_cleared(); + // clear errors with HLC stack + if (hlc) { + r_hlc[0]->call_reset_error(); + } + } + }); + } } void ErrorHandling::raise_overcurrent_error(const std::string& description) { @@ -583,4 +635,43 @@ bool ErrorHandling::modify_error_evse_manager(const std::string& error_type, boo return true; }; +bool ErrorHandling::modify_error_imd(const Everest::error::Error& error, bool active, + types::evse_manager::ErrorEnum& evse_error) { + const std::string& error_type = error.type; + + if (active) { + EVLOG_error << "Raised error " << error_type << ": " << error.description << " (" << error.message << ")"; + } else { + EVLOG_info << "Cleared error " << error_type << ": " << error.description << " (" << error.message << ")"; + } + + if (error_type == "isolation_monitor/DeviceFault") { + active_errors.imd.set(IMDErrors::DeviceFault, active); + evse_error = types::evse_manager::ErrorEnum::IMDFault; + if (hlc && active) { + r_hlc[0]->call_send_error(types::iso15118_charger::EvseError::Error_Malfunction); + } + } else if (error_type == "isolation_monitor/CommunicationFault") { + active_errors.imd.set(IMDErrors::CommunicationFault, active); + evse_error = types::evse_manager::ErrorEnum::IMDFault; + if (hlc && active) { + r_hlc[0]->call_send_error(types::iso15118_charger::EvseError::Error_Malfunction); + } + } else if (error_type == "isolation_monitor/VendorError") { + active_errors.connector_lock.set(ConnectorLockErrors::VendorError, active); + evse_error = types::evse_manager::ErrorEnum::VendorError; + if (hlc && active) { + r_hlc[0]->call_send_error(types::iso15118_charger::EvseError::Error_Malfunction); + } + } else { + // Errors that do not stop charging + if (error_type == "isolation_monitor/VendorWarning") { + evse_error = types::evse_manager::ErrorEnum::VendorWarning; + } + return false; + } + // Error stops charging + return true; +}; + } // namespace module diff --git a/modules/EvseManager/ErrorHandling.hpp b/modules/EvseManager/ErrorHandling.hpp index 38b77daae2..1e498e2099 100644 --- a/modules/EvseManager/ErrorHandling.hpp +++ b/modules/EvseManager/ErrorHandling.hpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include "EnumFlags.hpp" @@ -94,14 +95,24 @@ enum class ConnectorLockErrors : std::uint8_t { last = VendorError }; +enum class IMDErrors : std::uint8_t { + DeviceFault, + CommunicationFault, + VendorWarning, + VendorError, + last = VendorError +}; + struct ActiveErrors { AtomicEnumFlags bsp; AtomicEnumFlags evse_manager; AtomicEnumFlags ac_rcd; AtomicEnumFlags connector_lock; + AtomicEnumFlags imd; inline bool all_cleared() { - return bsp.all_reset() && evse_manager.all_reset() && ac_rcd.all_reset() && connector_lock.all_reset(); + return bsp.all_reset() && evse_manager.all_reset() && ac_rcd.all_reset() && connector_lock.all_reset() && + imd.all_reset(); }; }; @@ -112,7 +123,8 @@ class ErrorHandling { const std::vector>& r_hlc, const std::vector>& r_connector_lock, const std::vector>& r_ac_rcd, - const std::unique_ptr& _p_evse); + const std::unique_ptr& _p_evse, + const std::vector>& _r_imd); // Signal that one error has been raised. Bool argument is true if it preventing charging. sigslot::signal signal_error; @@ -136,6 +148,7 @@ class ErrorHandling { const std::vector>& r_connector_lock; const std::vector>& r_ac_rcd; const std::unique_ptr& p_evse; + const std::vector>& r_imd; bool modify_error_bsp(const Everest::error::Error& error, bool active, types::evse_manager::ErrorEnum& evse_error); bool modify_error_connector_lock(const Everest::error::Error& error, bool active, @@ -145,6 +158,7 @@ class ErrorHandling { bool modify_error_evse_manager(const std::string& error_type, bool active, types::evse_manager::ErrorEnum& evse_error); + bool modify_error_imd(const Everest::error::Error& error, bool active, types::evse_manager::ErrorEnum& evse_error); bool hlc{false}; ActiveErrors active_errors; diff --git a/modules/EvseManager/EvseManager.cpp b/modules/EvseManager/EvseManager.cpp index 8f8cd9798e..082eeeb67d 100644 --- a/modules/EvseManager/EvseManager.cpp +++ b/modules/EvseManager/EvseManager.cpp @@ -117,7 +117,7 @@ void EvseManager::init() { void EvseManager::ready() { bsp = std::unique_ptr(new IECStateMachine(r_bsp)); error_handling = - std::unique_ptr(new ErrorHandling(r_bsp, r_hlc, r_connector_lock, r_ac_rcd, p_evse)); + std::unique_ptr(new ErrorHandling(r_bsp, r_hlc, r_connector_lock, r_ac_rcd, p_evse, r_imd)); hw_capabilities = r_bsp->call_get_hw_capabilities(); @@ -234,11 +234,22 @@ void EvseManager::ready() { imd_stop(); - r_imd[0]->subscribe_IsolationMeasurement([this](types::isolation_monitor::IsolationMeasurement m) { + r_imd[0]->subscribe_isolation_measurement([this](types::isolation_monitor::IsolationMeasurement m) { // new DC isolation monitoring measurement received - session_log.evse(false, fmt::format("Isolation measurement R_F {}.", m.resistance_F_Ohm)); + + // Are we in charge loop? + if (charger->get_current_state() == Charger::EvseState::Charging and + not check_isolation_resistance_in_range(m.resistance_F_Ohm)) { + charger->set_hlc_error(); + r_hlc[0]->call_send_error(types::iso15118_charger::EvseError::Error_EmergencyShutdown); + } isolation_measurement = m; }); + + r_imd[0]->subscribe_self_test_result([this](bool result) { + session_log.evse(false, fmt::format("Isolation monitor self test result: {}", result)); + selftest_result = result; + }); } // Get voltage/current from DC power supply @@ -1206,19 +1217,62 @@ void EvseManager::charger_was_authorized() { } } +static double get_cable_check_voltage(double ev_max_cpd, double evse_max_cpd) { + double cable_check_voltage = 500; + // IEC 61851-23 (2023) CC.4.1.2 / Formular CC.1 + if (ev_max_cpd <= 500) { + if ((ev_max_cpd + 50) < cable_check_voltage) { + cable_check_voltage = (ev_max_cpd + 50); + } + if (evse_max_cpd < cable_check_voltage) { + cable_check_voltage = evse_max_cpd; + } + } else { + cable_check_voltage = evse_max_cpd; + if (1.1 * ev_max_cpd < cable_check_voltage) { + cable_check_voltage = 1.1 * ev_max_cpd; + } + } + + return cable_check_voltage; +} + +bool EvseManager::cable_check_should_exit() { + return charger->get_current_state() not_eq Charger::EvseState::PrepareCharging; +} + +bool EvseManager::check_isolation_resistance_in_range(double resistance) { + if (resistance < CABLECHECK_INSULATION_FAULT_RESISTANCE_OHM) { + session_log.evse(false, fmt::format("Isolation measurement FAULT R_F {}.", resistance)); + r_hlc[0]->call_update_isolation_status(types::iso15118_charger::IsolationStatus::Fault); + return false; + } else { + session_log.evse(false, fmt::format("Isolation measurement Ok R_F {}.", resistance)); + r_hlc[0]->call_update_isolation_status(types::iso15118_charger::IsolationStatus::Valid); + } + return true; +} + void EvseManager::cable_check() { if (r_imd.empty()) { // If no IMD is connected, we skip isolation checking. - EVLOG_info << "No IMD: skippint cable check."; + EVLOG_info << "No IMD: skipping cable check."; r_hlc[0]->call_update_isolation_status(types::iso15118_charger::IsolationStatus::No_IMD); r_hlc[0]->call_cable_check_finished(true); return; } + // start cable check in a seperate thread. std::thread t([this]() { session_log.evse(true, "Start cable check..."); - bool ok = false; + + // Verify output is below 60V initially + if (not wait_powersupply_DC_below_voltage(CABLECHECK_SAFE_VOLTAGE)) { + EVLOG_error << "Voltage did not drop below " << CABLECHECK_SAFE_VOLTAGE << "V within timeout."; + fail_cable_check(); + return; + } // normally contactors should be closed before entering cable check routine. // On some hardware implementation it may take some time until the confirmation arrives though, @@ -1230,102 +1284,165 @@ void EvseManager::cable_check() { Timeout timeout; timeout.start(CABLECHECK_CONTACTORS_CLOSE_TIMEOUT); - while (not timeout.reached()) { + while (not timeout.reached() and not cable_check_should_exit()) { if (not contactor_open) { break; } std::this_thread::sleep_for(100ms); } - // verify the relais are really switched on and set 500V output - if (not contactor_open) { - if (powersupply_DC_set(config.dc_isolation_voltage_V, 2)) { - powersupply_DC_on(); - imd_start(); - - // wait until the voltage has rised to the target value - if (not wait_powersupply_DC_voltage_reached(config.dc_isolation_voltage_V)) { - EVLOG_info << "Voltage did not rise to 500V within timeout"; - powersupply_DC_off(); - fail_session(); - ok = false; - imd_stop(); - } else { - auto caps = get_powersupply_capabilities(); - // read out one new isolation resistance - isolation_measurement.clear(); - types::isolation_monitor::IsolationMeasurement m; - if (not isolation_measurement.wait_for(m, 10s)) { - EVLOG_info << "Did not receive isolation measurement from IMD within 10 seconds."; - powersupply_DC_off(); - ok = false; - fail_session(); - } else { - // wait until the voltage is back to safe level - float minvoltage = - (config.switch_to_minimum_voltage_after_cable_check ? caps.min_export_voltage_V - : config.dc_isolation_voltage_V); - - // We do not want to shut down power supply - if (minvoltage < 60) { - minvoltage = 60; - } - powersupply_DC_set(minvoltage, 2); - - if (not wait_powersupply_DC_below_voltage(minvoltage + 20)) { - EVLOG_info << "Voltage did not go back to minimal voltage within timeout."; - ok = false; - fail_session(); - } else { - // verify it is within ranges. Warning level is <500 Ohm/V_max_output_rating, Fault - // is <100 - const double min_resistance_ok = 500. * caps.max_export_voltage_V; - const double min_resistance_warning = 100. * caps.max_export_voltage_V; - - if (m.resistance_F_Ohm < min_resistance_warning) { - session_log.evse( - false, fmt::format("Isolation measurement FAULT R_F {}.", m.resistance_F_Ohm)); - ok = true; // this just means that we are finished measuring, not that we are ok with - // the result - r_hlc[0]->call_update_isolation_status(types::iso15118_charger::IsolationStatus::Fault); - imd_stop(); - fail_session(); - } else if (m.resistance_F_Ohm < min_resistance_ok) { - session_log.evse( - false, fmt::format("Isolation measurement WARNING R_F {}.", m.resistance_F_Ohm)); - ok = true; - r_hlc[0]->call_update_isolation_status( - types::iso15118_charger::IsolationStatus::Warning); - } else { - session_log.evse(false, - fmt::format("Isolation measurement Ok R_F {}.", m.resistance_F_Ohm)); - ok = true; - r_hlc[0]->call_update_isolation_status(types::iso15118_charger::IsolationStatus::Valid); - } - } - } - } - } else { - EVLOG_error << fmt::format("CableCheck Thread: Could not set DC power supply voltage and current."); - fail_session(); + // If relais are still open after timeout, give up + if (contactor_open) { + EVLOG_error << "CableCheck: Contactors are still open after timeout, giving up."; + fail_cable_check(); + return; + } + + // Get correct voltage used to test the isolation + for (int retry_ev_info = 0; retry_ev_info < 10; retry_ev_info++) { + auto ev_info = get_ev_info(); + if (ev_info.maximum_voltage_limit.has_value()) { + break; } + std::this_thread::sleep_for(100ms); + } + + float ev_max_voltage = 500.; + + if (ev_info.maximum_voltage_limit.has_value()) { + EVLOG_info << "EV reports " << ev_info.maximum_voltage_limit.has_value() << " V as maximum voltage"; + ev_max_voltage = ev_info.maximum_voltage_limit.value(); + } else { + EVLOG_error << "CableCheck: Did not receive EV maximum voltage, falling back to 500V"; + } + + auto evse_caps = get_powersupply_capabilities(); + + double cable_check_voltage = get_cable_check_voltage(ev_max_voltage, evse_caps.max_export_voltage_V); + + // Allow overriding the cable check voltage from a configuration value + if (config.dc_isolation_voltage_V > 0) { + cable_check_voltage = config.dc_isolation_voltage_V; + } + + // Set the DC ouput voltage for testing + if (not powersupply_DC_set(cable_check_voltage, CABLECHECK_CURRENT_LIMIT)) { + EVLOG_error << "CableCheck: Could not set DC power supply voltage and current."; + fail_cable_check(); + return; } else { - EVLOG_error << fmt::format("CableCheck Thread: Contactors are still open after timeout, giving up."); - fail_session(); + EVLOG_info << "CableCheck: Using " << cable_check_voltage << " V"; } - if (config.hack_pause_imd_during_precharge) + // Switch on output voltage + powersupply_DC_on(); + + // Wait until the voltage has rised to the target value. + // This also handles the short circuit test according to IEC 61851-23 (2023) 6.3.1.109: + // CC.7.6.20.3: the maximum R for the short circuit test is 110 Ohms. + // CC.7.6.20.7: maximum current should be reduced to <5A within 1s. We set a current limit below 5A, so the + // power supply should always achieve that. + // Within 2.5s present voltage at side B must be below 60V. As the power supply ramp up speed varies greatly, + // we can only achieve this by limiting the current to I < cable_check_voltage/110 Ohm. The hard coded limit + // above fulfills that for all voltage ranges. + if (not wait_powersupply_DC_voltage_reached(cable_check_voltage)) { + EVLOG_error << "CableCheck: Voltage did not rise to " << cable_check_voltage << " V within timeout"; + fail_cable_check(); + return; + } + + // CC 4.1.3: Now relais are closed, voltage is up. We need to perform a self test of the IMD device + if (config.cable_check_enable_imd_self_test) { + selftest_result.clear(); + r_imd[0]->call_start_self_test(cable_check_voltage); + EVLOG_info << "CableCheck: IMD self test started."; + + // Wait for the result of the self test + bool result{false}; + bool result_received{false}; + + for (int wait_seconds = 0; wait_seconds < CABLECHECK_SELFTEST_TIMEOUT; wait_seconds++) { + if (cable_check_should_exit()) { + EVLOG_warning << "Cancel cable check"; + fail_cable_check(); + return; + } + if (selftest_result.wait_for(result, 1s)) { + result_received = true; + break; + } + } + + if (not result_received) { + EVLOG_error << "CableCheck: Did not get a self test result from IMD within timeout"; + fail_cable_check(); + return; + } + + if (not result) { + EVLOG_error << "CableCheck: IMD Self test failed"; + fail_cable_check(); + return; + } + } + + // CC.4.1.4: Perform the insulation resistance check + imd_start(); + + // read out new isolation resistance value + isolation_measurement.clear(); + types::isolation_monitor::IsolationMeasurement m; + + EVLOG_info << "CableCheck: Waiting for " << config.cable_check_wait_number_of_imd_measurements + << " isolation measurement sample(s)"; + // Wait for N isolation measurement values + for (int i = 0; i < config.cable_check_wait_number_of_imd_measurements; i++) { + if (not isolation_measurement.wait_for(m, 5s) or cable_check_should_exit()) { + EVLOG_info << "Did not receive isolation measurement from IMD within 5 seconds."; + imd_stop(); + fail_cable_check(); + return; + } + } + + // Now the value is valid and can be trusted. + // Verify it is within ranges. Fault is <100 kOhm + // Note that 2023 edition removed the warning level which was included in the 2014 edition. + // Refer to IEC 61851-23 (2023) 6.3.1.105 and CC.4.1.2 / CC.4.1.4 + if (not check_isolation_resistance_in_range(m.resistance_F_Ohm)) { imd_stop(); + fail_cable_check(); + return; + } + + // We are done with the isolation measurement and can now report success to the EV, + // but before we do so we need to do a few things for cleanup + + if (config.hack_pause_imd_during_precharge) { + imd_stop(); + } // Sleep before submitting result to spend more time in cable check. This is needed for some solar inverters - // used as DC chargers for them to warm up. - sleep(config.hack_sleep_in_cable_check); + // used as DC chargers for them to warm up. Don't use it. + std::this_thread::sleep_for(std::chrono::seconds(config.hack_sleep_in_cable_check)); if (car_manufacturer == types::evse_manager::CarManufacturer::VolkswagenGroup) { - sleep(config.hack_sleep_in_cable_check_volkswagen); + std::this_thread::sleep_for(std::chrono::seconds(config.hack_sleep_in_cable_check_volkswagen)); + } + + // CC.4.1.2: We need to wait until voltage is below 60V before sending a CableCheck Finished to the EV + powersupply_DC_off(); + + if (not wait_powersupply_DC_below_voltage(CABLECHECK_SAFE_VOLTAGE)) { + EVLOG_error << "Voltage did not drop below " << CABLECHECK_SAFE_VOLTAGE << "V within timeout."; + imd_stop(); + fail_cable_check(); + return; } - // submit result to HLC - r_hlc[0]->call_cable_check_finished(ok); + EVLOG_info << "CableCheck done, output is below " << CABLECHECK_SAFE_VOLTAGE << "V"; + + // Report CableCheck Finished with success to EV + r_hlc[0]->call_cable_check_finished(true); }); // Detach thread and exit command handler right away t.detach(); @@ -1443,9 +1560,17 @@ void EvseManager::powersupply_DC_off() { bool EvseManager::wait_powersupply_DC_voltage_reached(double target_voltage) { // wait until the voltage has rised to the target value Timeout timeout; - timeout.start(30s); + timeout.start(10s); bool voltage_ok = false; while (not timeout.reached()) { + if (cable_check_should_exit()) { + EVLOG_warning << "Cancel cable check wait voltage reached"; + powersupply_DC_off(); + r_hlc[0]->call_cable_check_finished(false); + charger->set_hlc_error(); + r_hlc[0]->call_send_error(types::iso15118_charger::EvseError::Error_EmergencyShutdown); + break; + } types::power_supply_DC::VoltageCurrent m; if (powersupply_measurement.wait_for(m, 2000ms)) { if (fabs(m.voltage_V - target_voltage) < 10) { @@ -1464,9 +1589,17 @@ bool EvseManager::wait_powersupply_DC_voltage_reached(double target_voltage) { bool EvseManager::wait_powersupply_DC_below_voltage(double target_voltage) { // wait until the voltage is below the target voltage Timeout timeout; - timeout.start(30s); + timeout.start(10s); bool voltage_ok = false; while (not timeout.reached()) { + if (cable_check_should_exit()) { + EVLOG_warning << "Cancel cable check wait below voltage"; + powersupply_DC_off(); + r_hlc[0]->call_cable_check_finished(false); + charger->set_hlc_error(); + r_hlc[0]->call_send_error(types::iso15118_charger::EvseError::Error_EmergencyShutdown); + break; + } types::power_supply_DC::VoltageCurrent m; if (powersupply_measurement.wait_for(m, 2000ms)) { if (m.voltage_V < target_voltage) { @@ -1506,12 +1639,17 @@ types::energy::ExternalLimits EvseManager::getLocalEnergyLimits() { return local_energy_limits; } -void EvseManager::fail_session() { - r_hlc[0]->call_send_error(types::iso15118_charger::EvseError::Error_EmergencyShutdown); +void EvseManager::fail_cable_check() { if (config.charge_mode == "DC") { powersupply_DC_off(); + // CC.4.1.2: We need to wait until voltage is below 60V before sending a CableCheck Finished to the EV + if (not wait_powersupply_DC_below_voltage(CABLECHECK_SAFE_VOLTAGE)) { + EVLOG_error << "Voltage did not drop below 60V within timeout, sending CableCheck Finished(false) anyway"; + } + r_hlc[0]->call_cable_check_finished(false); } charger->set_hlc_error(); + r_hlc[0]->call_send_error(types::iso15118_charger::EvseError::Error_EmergencyShutdown); } types::evse_manager::EVInfo EvseManager::get_ev_info() { diff --git a/modules/EvseManager/EvseManager.hpp b/modules/EvseManager/EvseManager.hpp index 5feff5cb14..b64e2e0c5b 100644 --- a/modules/EvseManager/EvseManager.hpp +++ b/modules/EvseManager/EvseManager.hpp @@ -73,7 +73,8 @@ struct Conf { bool dbg_hlc_auth_after_tstep; int hack_sleep_in_cable_check; int hack_sleep_in_cable_check_volkswagen; - bool switch_to_minimum_voltage_after_cable_check; + int cable_check_wait_number_of_imd_measurements; + bool cable_check_enable_imd_self_test; bool hack_skoda_enyaq; int hack_present_current_offset; bool hack_pause_imd_during_precharge; @@ -266,6 +267,7 @@ class EvseManager : public Everest::ModuleBase { VarContainer isolation_measurement; VarContainer powersupply_measurement; + VarContainer selftest_result; double latest_target_voltage; double latest_target_current; @@ -296,6 +298,8 @@ class EvseManager : public Everest::ModuleBase { bool wait_powersupply_DC_voltage_reached(double target_voltage); bool wait_powersupply_DC_below_voltage(double target_voltage); + bool cable_check_should_exit(); + // EV information Everest::timed_mutex_traceable ev_info_mutex; types::evse_manager::EVInfo ev_info; @@ -305,12 +309,18 @@ class EvseManager : public Everest::ModuleBase { void imd_start(); Everest::Thread telemetryThreadHandle; - void fail_session(); + void fail_cable_check(); // setup sae j2847/2 v2h mode void setup_v2h_mode(); + bool check_isolation_resistance_in_range(double resistance); + static constexpr auto CABLECHECK_CONTACTORS_CLOSE_TIMEOUT{std::chrono::seconds(5)}; + static constexpr double CABLECHECK_CURRENT_LIMIT{2}; + static constexpr double CABLECHECK_INSULATION_FAULT_RESISTANCE_OHM{100000.}; + static constexpr double CABLECHECK_SAFE_VOLTAGE{60.}; + static constexpr int CABLECHECK_SELFTEST_TIMEOUT{30}; std::atomic_bool current_demand_active{false}; // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 diff --git a/modules/EvseManager/manifest.yaml b/modules/EvseManager/manifest.yaml index 6ceffbe1b3..2bf11e8754 100644 --- a/modules/EvseManager/manifest.yaml +++ b/modules/EvseManager/manifest.yaml @@ -94,10 +94,10 @@ config: type: boolean default: false dc_isolation_voltage_V: - description: DC voltage used to test isolation in CableCheck. - Set to 500V. + description: Override DC voltage used to test isolation in CableCheck. + Default is 0, which means the voltage will be determined according to IEC 61851-23 (2023) CC.4.1.2 type: integer - default: 500 + default: 0 dbg_hlc_auth_after_tstep: description: >- Special mode: send HLC auth ok only after t_step_XX is finished (true) or directly when available (false) @@ -111,12 +111,18 @@ config: description: "Hack: Additional sleep for Volkswagen cars for n seconds at the end of cable check" type: integer default: 0 - switch_to_minimum_voltage_after_cable_check: + cable_check_wait_number_of_imd_measurements: + description: >- + Amount of isolation measurement samples to collect before the value can be trusted. This does not average, + it will evaluate the last measurement. Some IMDs (e.g. from Bender) need to measure for 10s to really get a trustable result. + In this case, at 1 Hz sample rate, specify 10 samples here. + type: integer + default: 1 + cable_check_enable_imd_self_test: description: >- - When cable check is completed, switch to minimal voltage of DC output. - Normally disabled. + Enable self testing of IMD in cable check. This is required for IEC 61851-23 (2023) compliance. type: boolean - default: false + default: true hack_skoda_enyaq: description: >- Skoda Enyaq requests DC charging voltages below its battery level or even below 0 initially. diff --git a/modules/EvseManager/scoped_lock_timeout.hpp b/modules/EvseManager/scoped_lock_timeout.hpp index 185620710d..11ff5ef361 100644 --- a/modules/EvseManager/scoped_lock_timeout.hpp +++ b/modules/EvseManager/scoped_lock_timeout.hpp @@ -52,6 +52,7 @@ enum class MutexDescription { Charger_set_hlc_allow_close_contactor, Charger_set_hlc_error, Charger_errors_prevent_charging, + Charger_set_max_current, IEC_process_bsp_event, IEC_state_machine, IEC_set_pwm, @@ -163,6 +164,8 @@ static std::string to_string(MutexDescription d) { return "Charger.cpp: set_hlc_error"; case MutexDescription::Charger_errors_prevent_charging: return "Charger.cpp: errors_prevent_charging"; + case MutexDescription::Charger_set_max_current: + return "Charger.cpp: set max current"; case MutexDescription::IEC_process_bsp_event: return "IECStateMachine::process_bsp_event"; case MutexDescription::IEC_state_machine: diff --git a/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.hpp b/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.hpp index 8a4305a002..f70878a55f 100644 --- a/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.hpp +++ b/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.hpp @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright chargebyte GmbH and Contributors to EVerest +// Copyright Pionix GmbH and Contributors to EVerest #ifndef MAIN_POWER_SUPPLY_DC_IMPL_HPP #define MAIN_POWER_SUPPLY_DC_IMPL_HPP diff --git a/modules/simulation/IMDSimulator/main/isolation_monitorImpl.cpp b/modules/simulation/IMDSimulator/main/isolation_monitorImpl.cpp index 862f94b027..00aedf7421 100644 --- a/modules/simulation/IMDSimulator/main/isolation_monitorImpl.cpp +++ b/modules/simulation/IMDSimulator/main/isolation_monitorImpl.cpp @@ -18,9 +18,6 @@ void isolation_monitorImpl::init() { void isolation_monitorImpl::ready() { } -isolation_monitorImpl::~isolation_monitorImpl() { -} - void isolation_monitorImpl::handle_start() { if (this->isolation_monitoring_active == false) { this->isolation_monitoring_active = true; @@ -28,6 +25,10 @@ void isolation_monitorImpl::handle_start() { } }; +void isolation_monitorImpl::handle_start_self_test(double& test_voltage_V) { + selftest_running_countdown = 3 * 1000 / LOOP_SLEEP_MS; +} + void isolation_monitorImpl::isolation_measurement_worker() { while (true) { if (this->isolation_measurement_thread_handle.shouldExit()) { @@ -35,11 +36,18 @@ void isolation_monitorImpl::isolation_measurement_worker() { } if (this->isolation_monitoring_active == true) { - this->mod->p_main->publish_IsolationMeasurement(this->isolation_measurement); + this->mod->p_main->publish_isolation_measurement(this->isolation_measurement); EVLOG_debug << "Simulated isolation measurement finished"; std::this_thread::sleep_for(std::chrono::milliseconds(this->config_interval - this->LOOP_SLEEP_MS)); } + if (this->selftest_running_countdown > 0) { + this->selftest_running_countdown--; + if (this->selftest_running_countdown == 0) { + this->mod->p_main->publish_self_test_result(config.selftest_success); + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(this->LOOP_SLEEP_MS)); } } diff --git a/modules/simulation/IMDSimulator/main/isolation_monitorImpl.hpp b/modules/simulation/IMDSimulator/main/isolation_monitorImpl.hpp index 106caa497c..94e3c10e2b 100644 --- a/modules/simulation/IMDSimulator/main/isolation_monitorImpl.hpp +++ b/modules/simulation/IMDSimulator/main/isolation_monitorImpl.hpp @@ -1,6 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2023 chargebyte GmbH -// Copyright (C) 2023 Contributors to EVerest +// Copyright Pionix GmbH and Contributors to EVerest #ifndef MAIN_ISOLATION_MONITOR_IMPL_HPP #define MAIN_ISOLATION_MONITOR_IMPL_HPP @@ -26,6 +25,7 @@ namespace main { struct Conf { double resistance_F_Ohm; int interval; + bool selftest_success; }; class isolation_monitorImpl : public isolation_monitorImplBase { @@ -33,7 +33,6 @@ class isolation_monitorImpl : public isolation_monitorImplBase { isolation_monitorImpl() = delete; isolation_monitorImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : isolation_monitorImplBase(ev, "main"), mod(mod), config(config){}; - ~isolation_monitorImpl(); // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here @@ -43,6 +42,7 @@ class isolation_monitorImpl : public isolation_monitorImplBase { // command handler functions (virtual) virtual void handle_start() override; virtual void handle_stop() override; + virtual void handle_start_self_test(double& test_voltage_V) override; // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 // insert your protected definitions here @@ -64,6 +64,8 @@ class isolation_monitorImpl : public isolation_monitorImplBase { Everest::Thread isolation_measurement_thread_handle; void isolation_measurement_worker(void); + + std::atomic_int selftest_running_countdown{0}; // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 }; diff --git a/modules/simulation/IMDSimulator/manifest.yaml b/modules/simulation/IMDSimulator/manifest.yaml index df1bf9c287..19a8c1a8df 100644 --- a/modules/simulation/IMDSimulator/manifest.yaml +++ b/modules/simulation/IMDSimulator/manifest.yaml @@ -12,6 +12,10 @@ provides: description: Measurement update interval in milliseconds type: integer default: 1000 + selftest_success: + description: Set to true for successful self testing, false for fault + type: boolean + default: true metadata: license: https://opensource.org/licenses/Apache-2.0 authors: diff --git a/types/evse_manager.yaml b/types/evse_manager.yaml index 0867879504..21109116ff 100644 --- a/types/evse_manager.yaml +++ b/types/evse_manager.yaml @@ -297,6 +297,7 @@ types: - EnergyManagement - PermanentFault - PowermeterTransactionStartFailed + - IMDFault Error: description: >- Error object that contains information about the error and optional vendor error information