// SPDX-License-Identifier: Apache-2.0 // Copyright 2023 Pionix GmbH and Contributors to EVerest #include "IECStateMachine.hpp" #include "everest/logging.hpp" #include #include #include #include namespace module { // helper type for visitor template struct overloaded : Ts... { using Ts::operator()...; }; template overloaded(Ts...) -> overloaded; enum class TimerControl : std::uint8_t { do_nothing, start, stop, }; static std::variant from_bsp_event(types::board_support_common::Event e) { switch (e) { case types::board_support_common::Event::A: return RawCPState::A; case types::board_support_common::Event::B: return RawCPState::B; case types::board_support_common::Event::C: return RawCPState::C; case types::board_support_common::Event::D: return RawCPState::D; case types::board_support_common::Event::E: return RawCPState::E; case types::board_support_common::Event::F: return RawCPState::F; case types::board_support_common::Event::PowerOn: return CPEvent::PowerOn; case types::board_support_common::Event::PowerOff: return CPEvent::PowerOff; default: return RawCPState::Disabled; } } /// \brief Converts the given Event \p e to human readable string /// \returns a string representation of the Event 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"); } IECStateMachine::IECStateMachine(const std::unique_ptr& r_bsp_, bool lock_connector_in_state_b_) : r_bsp(r_bsp_), lock_connector_in_state_b(lock_connector_in_state_b_) { // feed the state machine whenever the timer expires timeout_state_c1.signal_reached.connect([this]() { feed_state_machine(std::nullopt); }); timeout_unlock_state_F.signal_reached.connect([this]() { feed_state_machine(std::nullopt); }); // Subscribe to bsp driver to receive BspEvents from the hardware r_bsp->subscribe_event([this](types::board_support_common::BspEvent const& event) { if (enabled) { // feed into state machine process_bsp_event(event); } else { EVLOG_info << "Ignoring BSP Event, BSP is not enabled yet."; } }); } void IECStateMachine::process_bsp_event(types::board_support_common::BspEvent const& bsp_event) { auto event = from_bsp_event(bsp_event.event); std::visit(overloaded{[this](const RawCPState& raw_state) { // If it is a raw CP state, run it through the state machine feed_state_machine(raw_state); }, // If it is another CP event, pass through [this](CPEvent& event) { // track relais state as confirmed by BSP if (event == CPEvent::PowerOn) { relais_on = true; } else if (event == CPEvent::PowerOff) { relais_on = false; } check_connector_lock(); signal_event(event); }}, event); } void IECStateMachine::feed_state_machine(std::optional const& cp_state_opt) { auto events = state_machine(cp_state_opt); // Process all events while (not events.empty()) { EVLOG_debug << "CPEvent " << static_cast(events.front()); signal_event(events.front()); events.pop(); } } // Main IEC state machine. Needs to be called whenever: // - CP state changes (both events from hardware as well as duty cycle changes) // - Allow power on changes // - The C1 6s timer expires std::queue IECStateMachine::state_machine(std::optional const& cp_state_opt) { if (cp_state_opt) { EVLOG_debug << "RawCPState " << static_cast(cp_state_opt.value()); } else { EVLOG_debug << "RawCPState not set"; } std::queue events; auto timer_state_C1 = TimerControl::do_nothing; auto timer_unlock_state_F = TimerControl::do_nothing; { // mutex protected section Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::IEC_state_machine); const auto cp_state = cp_state_opt.value_or(last_cp_state); if (cp_state not_eq RawCPState::F and last_cp_state == RawCPState::F) { timer_unlock_state_F = TimerControl::stop; } switch (cp_state) { case RawCPState::Disabled: if (last_cp_state != RawCPState::Disabled) { pwm_running = false; r_bsp->call_cp_state_X1(); ev_simplified_mode = false; timer_state_C1 = TimerControl::stop; call_allow_power_on_bsp(false); connector_unlock(); } break; case RawCPState::A: if (last_cp_state != RawCPState::A) { pwm_running = false; r_bsp->call_cp_state_X1(); ev_simplified_mode = false; car_plugged_in = false; call_allow_power_on_bsp(false); timer_state_C1 = TimerControl::stop; connector_unlock(); } // Table A.6: Sequence 2.1 Unplug at state Bx (or any other // state) Table A.6: Sequence 2.2 Unplug at state Cx, Dx if (last_cp_state != RawCPState::A && last_cp_state != RawCPState::Disabled) { events.push(CPEvent::CarUnplugged); } break; case RawCPState::B: // Table A.6: Sequence 7 EV stops charging // Table A.6: Sequence 8.2 EV supply equipment // responds to EV opens S2 (w/o PWM) if (lock_connector_in_state_b) { connector_lock(); } else { connector_unlock(); } if (last_cp_state != RawCPState::A && last_cp_state != RawCPState::B) { events.push(CPEvent::CarRequestedStopPower); // Need to switch off according to Table A.6 Sequence 8.1 // within 100ms call_allow_power_on_bsp(false); timer_state_C1 = TimerControl::stop; } // Table A.6: Sequence 1.1 Plug-in if (last_cp_state == RawCPState::A || last_cp_state == RawCPState::Disabled || (!car_plugged_in && last_cp_state == RawCPState::F)) { events.push(CPEvent::CarPluggedIn); car_plugged_in = true; ev_simplified_mode = false; } if (last_cp_state == RawCPState::E || last_cp_state == RawCPState::F) { // Triggers SLAC start events.push(CPEvent::EFtoBCD); } break; case RawCPState::D: connector_lock(); // If state D is not supported switch off. if (not has_ventilation) { call_allow_power_on_bsp(false); timer_state_C1 = TimerControl::stop; break; } // no break, intended fall through: If we support state D it is handled the same way as state C [[fallthrough]]; case RawCPState::C: connector_lock(); // Table A.6: Sequence 1.2 Plug-in if (last_cp_state == RawCPState::A || last_cp_state == RawCPState::Disabled || (!car_plugged_in && last_cp_state == RawCPState::F)) { events.push(CPEvent::CarPluggedIn); car_plugged_in = true; EVLOG_info << "Detected simplified mode."; ev_simplified_mode = true; } else if (last_cp_state == RawCPState::B) { events.push(CPEvent::CarRequestedPower); } if (!pwm_running && last_pwm_running) { // X2->C1 // Table A.6 Sequence 10.2: EV does not stop drawing power // even if PWM stops. Stop within 6 seconds (E.g. Kona1!) timer_state_C1 = TimerControl::start; } // PWM switches on while in state C if (pwm_running && !last_pwm_running) { // when resuming after a pause before the EV goes to state B, stop the timer. timer_state_C1 = TimerControl::stop; // If we resume charging and the EV never left state C during pause we allow non-compliant EVs to switch // on again. if (power_on_allowed) { call_allow_power_on_bsp(true); } } if (timeout_state_c1.reached()) { EVLOG_warning << "Timeout of 6 seconds reached, EV did not go back to state B after PWM was switched off. " "Powering off under load."; // We are still in state C, but the 6 seconds timeout has been reached. Now force power off under load. call_allow_power_on_bsp(false); } if (pwm_running) { // C2 // 1) When we come from state B: switch on if we are allowed to // 2) When we are in C2 for a while now and finally get a delayed power_on_allowed: also switch on if (power_on_allowed && (!last_power_on_allowed || last_cp_state == RawCPState::B)) { // Table A.6: Sequence 4 EV ready to charge. // Must enable power within 3 seconds. call_allow_power_on_bsp(true); } // Simulate Request power Event here for simplified mode // to ensure that this mode behaves similar for higher // layers. Note this does not work with 5% mode // correctly, but simplified mode does not support HLC // anyway. if (!last_pwm_running && ev_simplified_mode) { events.push(CPEvent::CarRequestedPower); } } break; case RawCPState::E: connector_unlock(); if (last_cp_state != RawCPState::E) { timer_state_C1 = TimerControl::stop; call_allow_power_on_bsp(false); pwm_running = false; r_bsp->call_cp_state_X1(); if (last_cp_state == RawCPState::B || last_cp_state == RawCPState::C || last_cp_state == RawCPState::D) { events.push(CPEvent::BCDtoEF); events.push(CPEvent::BCDtoE); } } break; case RawCPState::F: timer_state_C1 = TimerControl::stop; call_allow_power_on_bsp(false); if (last_cp_state not_eq RawCPState::F) { timer_unlock_state_F = TimerControl::start; pwm_running = false; } if (last_cp_state == RawCPState::B || last_cp_state == RawCPState::C || last_cp_state == RawCPState::D) { events.push(CPEvent::BCDtoEF); } if (timeout_unlock_state_F.reached()) { connector_unlock(); } break; } check_connector_lock(); last_cp_state = cp_state; last_pwm_running = pwm_running; last_power_on_allowed = power_on_allowed; // end of mutex protected section } // stopping the timer could lead to a deadlock when called from the // mutex protected section switch (timer_state_C1) { case TimerControl::start: timeout_state_c1.start(power_off_under_load_in_c1_timeout); break; case TimerControl::stop: timeout_state_c1.stop(); break; case TimerControl::do_nothing: default: break; } switch (timer_unlock_state_F) { case TimerControl::start: timeout_unlock_state_F.start(unlock_in_state_f_timeout); break; case TimerControl::stop: timeout_unlock_state_F.stop(); break; case TimerControl::do_nothing: default: break; } return events; } // High level state machine sets PWM duty cycle void IECStateMachine::set_pwm(double value) { { Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::IEC_set_pwm); if (value > 0 && value < 1) { pwm_running = true; } else { pwm_running = false; } } if (ev_simplified_mode_evse_limit and ev_simplified_mode and value > ev_simplified_mode_evse_limit_pwm) { EVLOG_warning << "Simplified mode: Limiting output PWM to 10A due to config option \"hack_simplified_mode_limit_10A\""; value = ev_simplified_mode_evse_limit_pwm; } r_bsp->call_pwm_on(value * 100); feed_state_machine(std::nullopt); } // High level state machine sets state X1 void IECStateMachine::set_cp_state_X1() { { Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::IEC_set_cp_state_X1); pwm_running = false; } r_bsp->call_cp_state_X1(); // Don't run the state machine in the callers context feed_state_machine(std::nullopt); } // High level state machine sets state F void IECStateMachine::set_cp_state_F() { { Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::IEC_set_cp_state_F); pwm_running = false; } r_bsp->call_cp_state_F(); // Don't run the state machine in the callers context feed_state_machine(std::nullopt); } // The higher level state machine in Charger.cpp calls this to indicate it allows contactors to be switched on void IECStateMachine::allow_power_on(bool value, types::evse_board_support::Reason reason) { { Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::IEC_allow_power_on); // Only set the flags here in case of power on. power_on_allowed = value; power_on_reason = reason; // In case of power off, we can directly forward this to the BSP driver here if (not power_on_allowed) { call_allow_power_on_bsp(false); } } // The actual power on will be handled in the state machine to verify it is in the correct CP state etc. // Don't run the state machine in the callers context feed_state_machine(std::nullopt); } // Private member function used to actually call the BSP driver's allow_power_on // No need to lock mutex as this will be called from state machine or locked context only void IECStateMachine::call_allow_power_on_bsp(bool value) { if (not value) { power_on_allowed = false; power_on_reason = types::evse_board_support::Reason::PowerOff; } r_bsp->call_allow_power_on({value, power_on_reason}); } void IECStateMachine::set_pp_ampacity(types::board_support_common::ProximityPilot const& pp) { switch (pp.ampacity) { case types::board_support_common::Ampacity::A_13: pp_ampacity = 13.; break; case types::board_support_common::Ampacity::A_20: pp_ampacity = 20.; break; case types::board_support_common::Ampacity::A_32: pp_ampacity = 32.; break; case types::board_support_common::Ampacity::A_63_3ph_70_1ph: if (max_phases == AcPhases::SinglePhase) { pp_ampacity = 70.; } else { pp_ampacity = 63.; } break; default: pp_ampacity = 0.; } } // High level state machine requests reading PP ampacity value. // The high level state machine will never call this if it is not used // (e.g. in DC or AC tethered charging) std::optional IECStateMachine::read_pp_ampacity() { const double tmp = pp_ampacity; if (tmp == 0.0) { return std::nullopt; } return tmp; } // Forward special request to switch the number of phases during charging. BSP will need to implement a special // sequence to not destroy cars. void IECStateMachine::switch_three_phases_while_charging(bool n) { r_bsp->call_ac_switch_three_phases_while_charging(n); } // Forwards config parameters from EvseManager module config to BSP void IECStateMachine::setup(bool has_ventilation) { this->has_ventilation = has_ventilation; } // enable/disable the charging port and CP signal void IECStateMachine::enable(bool en) { enabled = en; r_bsp->call_enable(en); } // Forward the over current detection limit to the BSP. Many BSP MCUs monitor the charge current and trigger a fault // in case of over current. This sets the target charging current value to be used in OC detection. It cannot be // derived from the PWM duty cycle, use this value instead. void IECStateMachine::set_overcurrent_limit(double amps) { if (amps != last_amps) { r_bsp->call_ac_set_overcurrent_limit_A(amps); last_amps = amps; } } void IECStateMachine::connector_lock() { should_be_locked = true; } void IECStateMachine::connector_unlock() { should_be_locked = false; force_unlocked = false; } void IECStateMachine::connector_force_unlock() { RawCPState cp; { Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::IEC_force_unlock); cp = last_cp_state; } if (not relais_on) { // Unconditionally try to unlock, as `is_locked` might not always reflect the physical state of the lock. // This can occur for example in case of a failed unlock due to a hardware issue. signal_unlock(); is_locked = false; } if (cp == RawCPState::B or cp == RawCPState::C) { force_unlocked = true; check_connector_lock(); } } void IECStateMachine::check_connector_lock() { bool should_be_locked_considering_relais_and_force = relais_on or (should_be_locked and not force_unlocked); if (not is_locked and should_be_locked_considering_relais_and_force) { signal_lock(); is_locked = true; } else if (is_locked and not should_be_locked_considering_relais_and_force) { signal_unlock(); is_locked = false; } } } // namespace module