- 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
529 lines
19 KiB
C++
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
|