Files
cariflex/tools/EVerest-main/modules/EVSE/EvseManager/IECStateMachine.cpp
Eric F d398a6ced2 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
2026-06-08 00:38:27 -04:00

529 lines
19 KiB
C++

// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include "IECStateMachine.hpp"
#include "everest/logging.hpp"
#include <cstdint>
#include <math.h>
#include <optional>
#include <string.h>
namespace module {
// helper type for visitor
template <class... Ts> struct overloaded : Ts... {
using Ts::operator()...;
};
template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
enum class TimerControl : std::uint8_t {
do_nothing,
start,
stop,
};
static std::variant<RawCPState, CPEvent> 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<evse_board_supportIntf>& 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<RawCPState> const& cp_state_opt) {
auto events = state_machine(cp_state_opt);
// Process all events
while (not events.empty()) {
EVLOG_debug << "CPEvent " << static_cast<int>(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<CPEvent> IECStateMachine::state_machine(std::optional<RawCPState> const& cp_state_opt) {
if (cp_state_opt) {
EVLOG_debug << "RawCPState " << static_cast<int>(cp_state_opt.value());
} else {
EVLOG_debug << "RawCPState not set";
}
std::queue<CPEvent> 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<double> 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