- 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
2232 lines
103 KiB
C++
2232 lines
103 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright 2020 - 2022 Pionix GmbH and Contributors to EVerest
|
|
/*
|
|
* Charger.cpp
|
|
*
|
|
* Created on: 08.03.2021
|
|
* Author: cornelius
|
|
*
|
|
*/
|
|
|
|
#include "Charger.hpp"
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <generated/types/powermeter.hpp>
|
|
#include <math.h>
|
|
#include <optional>
|
|
#include <string.h>
|
|
#include <thread>
|
|
#include <type_traits>
|
|
|
|
#include <fmt/core.h>
|
|
|
|
#include "everest/logging.hpp"
|
|
#include "scoped_lock_timeout.hpp"
|
|
#include "utils.hpp"
|
|
|
|
namespace module {
|
|
|
|
Charger::Charger(const std::unique_ptr<IECStateMachine>& bsp, const std::unique_ptr<ErrorHandling>& error_handling,
|
|
const std::vector<std::unique_ptr<powermeterIntf>>& r_powermeter_billing,
|
|
const std::unique_ptr<PersistentStore>& _store,
|
|
const types::evse_board_support::Connector_type& connector_type, const std::string& evse_id) :
|
|
bsp(bsp),
|
|
error_handling(error_handling),
|
|
r_powermeter_billing(r_powermeter_billing),
|
|
store(_store),
|
|
connector_type(connector_type),
|
|
evse_id(evse_id) {
|
|
|
|
#ifdef EVEREST_USE_BACKTRACES
|
|
Everest::install_backtrace_handler();
|
|
#endif
|
|
|
|
shared_context.connector_enabled = true;
|
|
shared_context.max_current = 6.0;
|
|
if (connector_type == types::evse_board_support::Connector_type::IEC62196Type2Socket) {
|
|
shared_context.max_current_cable = bsp->read_pp_ampacity();
|
|
}
|
|
shared_context.flag_authorized = false;
|
|
|
|
internal_context.update_pwm_last_duty_cycle = 0.;
|
|
|
|
shared_context.current_state = EvseState::Idle;
|
|
internal_context.last_state = EvseState::Disabled;
|
|
|
|
shared_context.current_drawn_by_vehicle[0] = 0.;
|
|
shared_context.current_drawn_by_vehicle[1] = 0.;
|
|
shared_context.current_drawn_by_vehicle[2] = 0.;
|
|
|
|
internal_context.t_step_EF_return_state = EvseState::Idle;
|
|
internal_context.t_step_X1_return_state = EvseState::Idle;
|
|
|
|
shared_context.matching_started = false;
|
|
|
|
shared_context.flag_transaction_active = false;
|
|
shared_context.session_active = false;
|
|
|
|
internal_context.last_state_detect_state_change = EvseState::Idle;
|
|
|
|
hlc_use_5percent_current_session = false;
|
|
|
|
// create thread for processing errors/error clearings
|
|
error_thread_handle = std::thread(&Charger::error_thread, this);
|
|
|
|
// Register callbacks for errors/error clearings
|
|
error_handling->signal_error.connect(
|
|
[this](const ErrorHandlingEvents event) { error_handling_event_queue.push(event); });
|
|
|
|
error_handling->signal_all_errors_cleared.connect([this]() {
|
|
EVLOG_info << "All errors cleared";
|
|
error_handling_event_queue.push(ErrorHandlingEvents::AllErrorCleared);
|
|
});
|
|
}
|
|
|
|
Charger::~Charger() {
|
|
cp_state_F();
|
|
// need to send an event to wake up processing
|
|
error_handling_event_queue.push(ErrorHandlingEvents::ForceEmergencyShutdown);
|
|
error_thread_handle.stop();
|
|
}
|
|
|
|
void Charger::main_thread() {
|
|
|
|
// Enable CP output
|
|
bsp->enable(true);
|
|
|
|
// publish initial values
|
|
signal_max_current(get_max_current_internal());
|
|
signal_state(shared_context.current_state);
|
|
|
|
while (!main_thread_handle.shouldExit()) {
|
|
|
|
const auto events = bsp_event_queue.wait_for(MAINLOOP_UPDATE_RATE);
|
|
|
|
for (const auto& event : events) {
|
|
process_event(event);
|
|
}
|
|
|
|
{
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_mainloop);
|
|
// update power limits
|
|
power_available();
|
|
// Run our own state machine update (i.e. run everything that needs
|
|
// to be done on regular intervals independent from events)
|
|
run_state_machine();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Charger::error_thread() {
|
|
for (;;) {
|
|
if (error_thread_handle.shouldExit()) {
|
|
break;
|
|
}
|
|
auto events = error_handling_event_queue.wait();
|
|
if (!events.empty()) {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_signal_loop);
|
|
for (auto& event : events) {
|
|
switch (event) {
|
|
case ErrorHandlingEvents::ForceErrorShutdown:
|
|
shared_context.shutdown_type = ShutdownType::ErrorShutdown;
|
|
break;
|
|
case ErrorHandlingEvents::ForceEmergencyShutdown:
|
|
shared_context.shutdown_type = ShutdownType::EmergencyShutdown;
|
|
break;
|
|
case ErrorHandlingEvents::AllErrorsPreventingChargingCleared:
|
|
shared_context.shutdown_type = ShutdownType::None;
|
|
break;
|
|
case ErrorHandlingEvents::AllErrorCleared:
|
|
shared_context.shutdown_type = ShutdownType::None;
|
|
break;
|
|
default:
|
|
EVLOG_error << "ErrorHandlingEvents invalid value: "
|
|
<< static_cast<std::underlying_type_t<ErrorHandlingEvents>>(event);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Charger::run_state_machine() {
|
|
|
|
constexpr int max_mainloop_runs = 10;
|
|
int mainloop_runs = 0;
|
|
|
|
// run over state machine loop until current_state does not change anymore
|
|
do {
|
|
mainloop_runs++;
|
|
// If a state change happened or an error recovered during a state we reinitialize the state
|
|
bool initialize_state = (internal_context.last_state_detect_state_change not_eq shared_context.current_state) or
|
|
(internal_context.last_shutdown_type not_eq shared_context.shutdown_type);
|
|
|
|
if (initialize_state) {
|
|
session_log.evse(false, fmt::format("Charger state: {}->{}",
|
|
evse_state_to_string(internal_context.last_state_detect_state_change),
|
|
evse_state_to_string(shared_context.current_state)));
|
|
}
|
|
|
|
internal_context.last_state = internal_context.last_state_detect_state_change;
|
|
internal_context.last_state_detect_state_change = shared_context.current_state;
|
|
internal_context.last_shutdown_type = shared_context.shutdown_type;
|
|
|
|
auto now = std::chrono::system_clock::now();
|
|
|
|
if (shared_context.ac_with_soc_timeout and (shared_context.ac_with_soc_timer -= 50) < 0) {
|
|
shared_context.ac_with_soc_timeout = false;
|
|
shared_context.ac_with_soc_timer = 3600000;
|
|
signal_ac_with_soc_timeout();
|
|
return;
|
|
}
|
|
|
|
if (initialize_state) {
|
|
internal_context.current_state_started = now;
|
|
signal_state(shared_context.current_state);
|
|
}
|
|
|
|
auto time_in_current_state =
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(now - internal_context.current_state_started).count();
|
|
|
|
// Clean up ongoing 1ph 3ph switching operation?
|
|
if (internal_context.last_state == EvseState::SwitchPhases and
|
|
shared_context.current_state not_eq EvseState::SwitchPhases and
|
|
shared_context.switch_3ph1ph_threephase_ongoing) {
|
|
bsp->switch_three_phases_while_charging(shared_context.switch_3ph1ph_threephase);
|
|
shared_context.switch_3ph1ph_threephase_ongoing = false;
|
|
}
|
|
|
|
switch (shared_context.current_state) {
|
|
case EvseState::Disabled:
|
|
if (initialize_state) {
|
|
cp_state_F();
|
|
bsp->enable(false);
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::Disabled);
|
|
}
|
|
if (not shared_context.flag_disable_requested) {
|
|
set_state(EvseState::Idle);
|
|
}
|
|
break;
|
|
|
|
case EvseState::Idle:
|
|
// make sure we signal availability to potential new cars
|
|
if (initialize_state) {
|
|
bcb_toggle_reset();
|
|
shared_context.iec_allow_close_contactor = false;
|
|
if (config_context.charge_mode == ChargeMode::AC) {
|
|
// For AC, a session may start in BASIC charging mode and switch to HLC mode later on.
|
|
// This variable will be set to true once the iso stack calls v2g_setup_finished.
|
|
shared_context.hlc_charging_active = false;
|
|
} else {
|
|
// For DC, it is always HLC mode.
|
|
shared_context.hlc_charging_active = true;
|
|
}
|
|
shared_context.hlc_allow_close_contactor = false;
|
|
shared_context.max_current_cable.reset();
|
|
shared_context.hlc_charging_terminate_pause = HlcTerminatePause::Unknown;
|
|
shared_context.legacy_wakeup_done = false;
|
|
shared_context.hlc_d20_active = false;
|
|
cp_state_X1();
|
|
deauthorize_internal();
|
|
shared_context.flag_transaction_active = false;
|
|
clear_errors_on_unplug();
|
|
internal_context.dc_statistics_printed = false;
|
|
}
|
|
|
|
if (shared_context.flag_disable_requested) {
|
|
set_state(EvseState::Disabled);
|
|
break;
|
|
}
|
|
|
|
if (shared_context.shutdown_type != ShutdownType::None) {
|
|
if (not internal_context.cp_state_F_active) {
|
|
cp_state_F();
|
|
}
|
|
} else {
|
|
if (internal_context.cp_state_F_active) {
|
|
cp_state_X1();
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case EvseState::WaitingForAuthentication:
|
|
|
|
// Explicitly do not allow to be powered on. This is important
|
|
// to make sure control_pilot does not switch on relais even if
|
|
// we start PWM here
|
|
if (initialize_state) {
|
|
internal_context.pp_warning_printed = false;
|
|
internal_context.no_energy_warning_printed = false;
|
|
internal_context.ac_x1_fallback_nominal_timeout_running = false;
|
|
internal_context.auth_received_printed = false;
|
|
|
|
bsp->allow_power_on(false, types::evse_board_support::Reason::PowerOff);
|
|
|
|
// First user interaction was plug in of car? Start session here.
|
|
if (not shared_context.session_active) {
|
|
start_session(false);
|
|
}
|
|
// External signal on MQTT
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::AuthRequired);
|
|
|
|
hlc_use_5percent_current_session = false;
|
|
|
|
// switch on HLC if configured. May be switched off later on after retries for this session only.
|
|
if (config_context.charge_mode == ChargeMode::AC) {
|
|
ac_hlc_enabled_current_session = config_context.ac_hlc_enabled;
|
|
if (ac_hlc_enabled_current_session) {
|
|
hlc_use_5percent_current_session = config_context.ac_hlc_use_5percent;
|
|
}
|
|
} else if (config_context.charge_mode == ChargeMode::DC) {
|
|
hlc_use_5percent_current_session = true;
|
|
} else {
|
|
// unsupported charging mode, give up here.
|
|
error_handling->raise_internal_error("Unsupported charging mode.");
|
|
}
|
|
|
|
if (hlc_use_5percent_current_session) {
|
|
// FIXME: wait for SLAC to be ready. Teslas are really fast with sending the first slac packet after
|
|
// enabling PWM.
|
|
std::this_thread::sleep_for(
|
|
std::chrono::milliseconds(config_context.sleep_before_enabling_pwm_hlc_mode_ms));
|
|
|
|
// If already authorized before plugin, do not use 5% PWM on AC HLC
|
|
if (shared_context.flag_authorized and config_context.charge_mode == ChargeMode::AC and
|
|
ac_hlc_enabled_current_session and not config_context.ac_enforce_hlc) {
|
|
hlc_use_5percent_current_session = false;
|
|
} else {
|
|
update_pwm_now(PWM_5_PERCENT);
|
|
stopwatch.mark("HLC_PWM_5%_ON");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Errors prevent charging? -> wait here, we cannot go to paused etc because we are not authorized yet
|
|
if (stop_charging_on_fatal_error_internal()) {
|
|
signal_slac_reset();
|
|
set_state(EvseState::Finished);
|
|
break;
|
|
}
|
|
|
|
if (not shared_context.flag_ev_plugged_in) {
|
|
// We did not start to charge yet, so go directly to finished.
|
|
set_state(EvseState::Finished);
|
|
break;
|
|
}
|
|
|
|
if (shared_context.flag_disable_requested) {
|
|
// Disable requested before transaction started
|
|
// go straight to Finished which will detect flag_disable_requested
|
|
// and transition to Disabled.
|
|
set_state(EvseState::Finished);
|
|
break;
|
|
}
|
|
|
|
// Read PP value in case of AC socket
|
|
if (connector_type == types::evse_board_support::Connector_type::IEC62196Type2Socket and
|
|
not shared_context.max_current_cable.has_value()) {
|
|
// retry if the value is not yet available. Some BSPs may take some time to measure the PP.
|
|
shared_context.max_current_cable = bsp->read_pp_ampacity();
|
|
if (not shared_context.max_current_cable.has_value()) {
|
|
if (not internal_context.pp_warning_printed) {
|
|
EVLOG_warning << "PP ampacity is zero, still waiting for BSP to report it...";
|
|
internal_context.pp_warning_printed = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Wait for Energy Manager to supply some power, otherwise wait here.
|
|
// If we have zero power, some cars will not like the ChargingParameter message.
|
|
if (config_context.charge_mode == ChargeMode::DC) {
|
|
// Create a copy of the atomic struct
|
|
types::iso15118::DcEvseMaximumLimits evse_limit = shared_context.current_evse_max_limits;
|
|
if (not power_available()) {
|
|
|
|
// Wait some time here in this state to see if we get energy from the EnergyManager...
|
|
if (time_in_current_state < WAIT_FOR_ENERGY_IN_AUTHLOOP_TIMEOUT_MS) {
|
|
break;
|
|
}
|
|
|
|
// If still no energy is available after the timeout, assume we will not get any for this session.
|
|
if (not internal_context.no_energy_warning_printed) {
|
|
EVLOG_warning << "No energy available, still retrying... Some EVs dont like 0W and/or 0A in "
|
|
"ChargingParameterDiscoveryRes message";
|
|
internal_context.no_energy_warning_printed = true;
|
|
signal_hlc_no_energy_available();
|
|
}
|
|
}
|
|
}
|
|
|
|
// EIM
|
|
|
|
// FIXME: In case V2G is not successfull after all, it needs
|
|
// to send a dlink_error request, which then needs to:
|
|
// AC mode: fall back to nominal PWM charging etc (not
|
|
// implemented yet!), it can do so because it is already authorized.
|
|
// DC mode: go to error_hlc and give up
|
|
|
|
// FIXME: if slac reports a dlink_ready=false while we are still waiting for auth we should:
|
|
// in AC mode: go back to non HLC nominal PWM mode
|
|
// in DC mode: go to error_slac for this session
|
|
if (shared_context.flag_authorized and not shared_context.authorized_pnc) {
|
|
if (not internal_context.auth_received_printed) {
|
|
internal_context.auth_received_printed = true;
|
|
session_log.evse(false, "EIM Authorization received");
|
|
}
|
|
|
|
// If we are restarting, the transaction may already be active
|
|
if (not shared_context.flag_transaction_active) {
|
|
if (!start_transaction()) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
const EvseState target_state(EvseState::PrepareCharging);
|
|
|
|
// EIM done and matching process not started -> we need to go through t_step_EF and fall back to nominal
|
|
// PWM. This is a complete waste of 4 precious seconds.
|
|
if (config_context.charge_mode == ChargeMode::AC) {
|
|
if (ac_hlc_enabled_current_session) {
|
|
if (config_context.ac_enforce_hlc) {
|
|
// non standard compliant mode: we just keep 5 percent running all the time like in DC
|
|
session_log.evse(
|
|
false, "AC mode, HLC enabled(ac_enforce_hlc), keeping 5 percent on until a dlink error "
|
|
"is signalled.");
|
|
hlc_use_5percent_current_session = true;
|
|
shared_context.current_state = target_state;
|
|
} else {
|
|
if (not shared_context.matching_started) {
|
|
// SLAC matching was not started when EIM arrived
|
|
|
|
session_log.evse(
|
|
false,
|
|
fmt::format(
|
|
"AC mode, HLC enabled, matching not started yet. Go through t_step_EF and "
|
|
"disable 5 percent if it was enabled before: {}",
|
|
(bool)hlc_use_5percent_current_session));
|
|
|
|
if (hlc_use_5percent_current_session) {
|
|
// Figure 3 of ISO15118-3: 5 percent start, PnC and EIM
|
|
internal_context.t_step_EF_return_state = target_state;
|
|
internal_context.t_step_EF_return_pwm = 0.;
|
|
internal_context.t_step_EF_return_ampere = 0.;
|
|
shared_context.current_state = EvseState::T_step_EF;
|
|
|
|
// fall back to nominal PWM after the t_step_EF break. Note that
|
|
// ac_hlc_enabled_current_session remains untouched as HLC can still start later in
|
|
// nominal PWM mode
|
|
hlc_use_5percent_current_session = false;
|
|
} else {
|
|
// Figure 4 of ISO15118-3: X1 start, PnC and EIM
|
|
// This figure requires a T_step_F for X1->Nominal. This does not really make sense.
|
|
// Also this is basically the same case as auth before plugin (which here in Everest
|
|
// technically is auth very shortly after plugin as the auth module assigns auth
|
|
// only on plug events) For auth before plugin -3 requires no T_step_EF. Also normal
|
|
// BC does X1>nomainal without F. We deviate from the standard here and perform no
|
|
// T_step_EF.
|
|
|
|
shared_context.current_state = target_state;
|
|
}
|
|
|
|
} else {
|
|
// SLAC matching was started already when EIM arrived
|
|
if (hlc_use_5percent_current_session) {
|
|
// Figure 5 of ISO15118-3: 5 percent start, PnC and EIM, matching already started
|
|
// when EIM was done
|
|
// This has the following real world issue: The X1 will kill the ISO-2 session and
|
|
// the EV will send a session stop. On EVs that actually support AC charge loop
|
|
// (such as VW), this would mean the end of it and they require a replug. To fix
|
|
// this, we should not go straight to X1 after EIM, but give the ISO stack some time
|
|
// to actually go into chargeloop. If that happens, we should keep 5% to not kill
|
|
// the ISO session, and if it does not within a timeout, we can still perform
|
|
// X1->nominal for EVs that performed the ISO auth loop but do not support ISO
|
|
// chargeloop (or non-HLC cars)
|
|
|
|
// If EV already sent a PowerDeliver.req
|
|
if (shared_context.hlc_charging_active) {
|
|
session_log.evse(
|
|
false,
|
|
"AC mode, HLC enabled(5percent), v2g_session_setup_finished shortly after "
|
|
"EIM. Keep 5% enabled to not accidentially kill the ISO session.");
|
|
shared_context.current_state = target_state;
|
|
} else {
|
|
if (internal_context.ac_x1_fallback_nominal_timeout_running) {
|
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() -
|
|
internal_context.ac_x1_fallback_nominal_timeout_started)
|
|
.count() > AC_X1_FALLBACK_TO_NOMINAL_TIMEOUT_MS) {
|
|
session_log.evse(false, "AC mode, HLC enabled(5percent), matching "
|
|
"already started. Go through "
|
|
"t_step_X1 and disable 5 percent.");
|
|
internal_context.t_step_X1_return_state = target_state;
|
|
internal_context.t_step_X1_return_pwm = 0.;
|
|
internal_context.t_step_EF_return_ampere = 0.;
|
|
hlc_use_5percent_current_session = false;
|
|
shared_context.current_state = EvseState::T_step_X1;
|
|
}
|
|
} else {
|
|
internal_context.ac_x1_fallback_nominal_timeout_running = true;
|
|
internal_context.ac_x1_fallback_nominal_timeout_started =
|
|
std::chrono::steady_clock::now();
|
|
}
|
|
}
|
|
} else {
|
|
// Figure 6 of ISO15118-3: X1 start, PnC and EIM, matching already started when EIM
|
|
// was done. We can go directly to PrepareCharging, as we do not need to switch from
|
|
// 5 percent to nominal first
|
|
session_log.evse(
|
|
false,
|
|
"AC mode, HLC enabled(X1), matching already started. We are in X1 so we can "
|
|
"go directly to nominal PWM.");
|
|
shared_context.current_state = target_state;
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
// HLC is disabled for this session.
|
|
// simply proceed to PrepareCharging, as we are fully authorized to start charging whenever car
|
|
// wants to.
|
|
session_log.evse(false, "AC mode, HLC disabled. We are in X1 so we can "
|
|
"go directly to nominal PWM.");
|
|
shared_context.current_state = target_state;
|
|
}
|
|
} else if (config_context.charge_mode == ChargeMode::DC) {
|
|
// Figure 8 of ISO15118-3: DC with EIM before or after plugin or PnC
|
|
// simple here as we always stay within 5 percent mode anyway.
|
|
session_log.evse(false,
|
|
"DC mode. We are in 5percent mode so we can continue without further action.");
|
|
shared_context.current_state = target_state;
|
|
} else {
|
|
// unsupported charging mode, give up here.
|
|
error_handling->raise_internal_error("Unsupported charging mode.");
|
|
}
|
|
// Contract / Plug and Charge
|
|
} else if (shared_context.flag_authorized and shared_context.authorized_pnc) {
|
|
|
|
if (not shared_context.flag_transaction_active) {
|
|
if (!start_transaction()) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
const EvseState target_state(EvseState::PrepareCharging);
|
|
|
|
// We got authorization by Plug and Charge
|
|
if (not internal_context.auth_received_printed) {
|
|
internal_context.auth_received_printed = true;
|
|
session_log.evse(false, "PnC Authorization received");
|
|
}
|
|
|
|
if (config_context.charge_mode == ChargeMode::AC) {
|
|
// Figures 3,4,5,6 of ISO15118-3: Independent on how we started we can continue with 5 percent or
|
|
// switch to nominal signalling once we got PnC authorization without going through t_step_EF or
|
|
// t_step_X1. Some EVs implement a non-standard way for PnC with AC: They perform ISO until Auth
|
|
// loop, and once authorization is provided, they close TCP (or send SessionStop). The intention is
|
|
// to use only the PnC part of ISO (which is the same as DC to that point) and then do normal BC
|
|
// instead of ISO charging loop. To support this we wait a short timeout to see if the chargeloop is
|
|
// entered, and if not, we switch back to nominal. To get a similar behaviour for PnC and EIM we
|
|
// still perform a T_step_EF here even if the standard says differently.
|
|
if (shared_context.hlc_charging_active) {
|
|
// AC HLC charging loop started
|
|
session_log.evse(false, "AC mode, HLC, PnC auth received. We will continue with 5percent as "
|
|
"HLC charing loop was started.");
|
|
hlc_use_5percent_current_session = true;
|
|
shared_context.current_state = target_state;
|
|
|
|
} else {
|
|
// AC HLC charging loop not yet started
|
|
if (internal_context.ac_x1_fallback_nominal_timeout_running) {
|
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() -
|
|
internal_context.ac_x1_fallback_nominal_timeout_started)
|
|
.count() > AC_X1_FALLBACK_TO_NOMINAL_TIMEOUT_MS) {
|
|
session_log.evse(
|
|
false,
|
|
"AC mode, HLC enabled, PnC authorized, but charging loop did not start. Go through "
|
|
"t_step_EF and disable 5 percent.");
|
|
internal_context.t_step_EF_return_state = target_state;
|
|
internal_context.t_step_EF_return_pwm = 0.;
|
|
internal_context.t_step_EF_return_ampere = 0.;
|
|
hlc_use_5percent_current_session = false;
|
|
shared_context.current_state = EvseState::T_step_EF;
|
|
}
|
|
} else {
|
|
internal_context.ac_x1_fallback_nominal_timeout_running = true;
|
|
internal_context.ac_x1_fallback_nominal_timeout_started = std::chrono::steady_clock::now();
|
|
}
|
|
}
|
|
|
|
} else if (config_context.charge_mode == ChargeMode::DC) {
|
|
// Figure 8 of ISO15118-3: DC with EIM before or after plugin or PnC
|
|
// simple here as we always stay within 5 percent mode anyway.
|
|
session_log.evse(false,
|
|
"DC mode. We are in 5percent mode so we can continue without further action.");
|
|
shared_context.current_state = target_state;
|
|
} else {
|
|
// unsupported charging mode, give up here.
|
|
error_handling->raise_internal_error("Unsupported charging mode.");
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case EvseState::SwitchPhases:
|
|
if (initialize_state) {
|
|
session_log.evse(false, "Start switching phases");
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::SwitchingPhases);
|
|
if (config_context.switch_3ph1ph_cp_state_F) {
|
|
cp_state_F();
|
|
} else {
|
|
cp_state_X1();
|
|
}
|
|
}
|
|
if (time_in_current_state >= config_context.switch_3ph1ph_delay_s * 1000) {
|
|
session_log.evse(false, "Exit switching phases");
|
|
bsp->switch_three_phases_while_charging(shared_context.switch_3ph1ph_threephase);
|
|
shared_context.switch_3ph1ph_threephase_ongoing = false;
|
|
shared_context.current_state = internal_context.switching_phases_return_state;
|
|
}
|
|
break;
|
|
|
|
case EvseState::T_step_EF:
|
|
if (initialize_state) {
|
|
session_log.evse(false, "Enter T_step_EF");
|
|
internal_context.t_step_ef_x1_pause = false;
|
|
cp_state_F();
|
|
}
|
|
if (time_in_current_state >= T_STEP_EF + STAY_IN_X1_AFTER_TSTEP_EF_MS) {
|
|
session_log.evse(false, "Exit T_step_EF");
|
|
if (internal_context.t_step_EF_return_pwm == 0.) {
|
|
cp_state_X1();
|
|
} else if (hlc_use_5percent_current_session) {
|
|
update_pwm_now(PWM_5_PERCENT);
|
|
internal_context.pwm_set_last_ampere = internal_context.t_step_EF_return_ampere;
|
|
} else {
|
|
update_pwm_now(internal_context.t_step_EF_return_pwm);
|
|
internal_context.pwm_set_last_ampere = internal_context.t_step_EF_return_ampere;
|
|
}
|
|
shared_context.current_state = internal_context.t_step_EF_return_state;
|
|
} else if (time_in_current_state >= T_STEP_EF and not internal_context.t_step_ef_x1_pause) {
|
|
internal_context.t_step_ef_x1_pause = true;
|
|
// stay in X1 for a little while as required by EV READY regulations
|
|
session_log.evse(false, "Pause in X1 for EV READY regulations");
|
|
cp_state_X1();
|
|
}
|
|
break;
|
|
|
|
case EvseState::T_step_X1:
|
|
if (initialize_state) {
|
|
session_log.evse(false, "Enter T_step_X1");
|
|
cp_state_X1();
|
|
}
|
|
if (time_in_current_state >= T_STEP_X1) {
|
|
session_log.evse(false, "Exit T_step_X1");
|
|
if (internal_context.t_step_X1_return_pwm == 0.) {
|
|
cp_state_X1();
|
|
} else {
|
|
update_pwm_now(internal_context.t_step_X1_return_pwm);
|
|
internal_context.pwm_set_last_ampere = internal_context.t_step_EF_return_ampere;
|
|
}
|
|
shared_context.current_state = internal_context.t_step_X1_return_state;
|
|
}
|
|
break;
|
|
|
|
case EvseState::PrepareCharging:
|
|
if (initialize_state) {
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::PrepareCharging);
|
|
bcb_toggle_reset();
|
|
internal_context.hlc_charge_loop_no_energy_timeout_running = false;
|
|
|
|
if (config_context.charge_mode == ChargeMode::DC) {
|
|
// Create a copy of the atomic struct
|
|
types::iso15118::DcEvseMaximumLimits evse_limit = shared_context.current_evse_max_limits;
|
|
if (not power_available()) {
|
|
signal_hlc_no_energy_available();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (stop_charging_on_fatal_error_internal() or not shared_context.flag_authorized or
|
|
not shared_context.flag_transaction_active or not shared_context.flag_ev_plugged_in or
|
|
shared_context.flag_disable_requested) {
|
|
// We started to initialize charging already, so we need to stop via StoppingCharging
|
|
set_state(EvseState::StoppingCharging);
|
|
break;
|
|
}
|
|
|
|
if (config_context.charge_mode == ChargeMode::DC) {
|
|
if (shared_context.hlc_allow_close_contactor and shared_context.iec_allow_close_contactor) {
|
|
bsp->allow_power_on(true, types::evse_board_support::Reason::DCCableCheck);
|
|
}
|
|
}
|
|
|
|
// make sure we are enabling PWM
|
|
if (not hlc_use_5percent_current_session) {
|
|
update_pwm_now_if_changed_ampere(get_max_current_internal());
|
|
} else {
|
|
update_pwm_now_if_changed(PWM_5_PERCENT);
|
|
}
|
|
|
|
if (config_context.charge_mode == ChargeMode::AC) {
|
|
// In AC mode BASIC, iec_allow is sufficient. The same is true for HLC mode when nominal PWM is
|
|
// used as the car can do BASIC and HLC charging any time. In AC HLC with 5 percent mode, we need to
|
|
// wait for both iec_allow and hlc_allow.
|
|
if (not power_available() and not hlc_use_5percent_current_session) {
|
|
// For AC BC: it is ok to wait here.
|
|
// For AC and DC ISO, continue in case we are in 5% mode. This allows us to go into
|
|
// Charge loop and report 0A/0W to the EV
|
|
break;
|
|
}
|
|
|
|
// Power is available or we are in HLC, PWM is already enabled. Check if we can go to charging
|
|
if ((shared_context.iec_allow_close_contactor and not hlc_use_5percent_current_session) or
|
|
(shared_context.iec_allow_close_contactor and shared_context.hlc_allow_close_contactor and
|
|
hlc_use_5percent_current_session)) {
|
|
set_state(EvseState::Charging);
|
|
} else {
|
|
// We have power and PWM is on, but EV did not proceed to state C yet (and/or HLC is not
|
|
// ready)
|
|
if (not shared_context.hlc_charging_active and not shared_context.legacy_wakeup_done and
|
|
time_in_current_state > LEGACY_WAKEUP_TIMEOUT) {
|
|
session_log.evse(false, "EV did not transition to state C, trying one legacy wakeup "
|
|
"according to IEC61851-1 A.5.3");
|
|
shared_context.legacy_wakeup_done = true;
|
|
internal_context.t_step_EF_return_state = EvseState::PrepareCharging;
|
|
internal_context.t_step_EF_return_pwm = ampere_to_duty_cycle(get_max_current_internal());
|
|
internal_context.t_step_EF_return_ampere = get_max_current_internal();
|
|
shared_context.current_state = EvseState::T_step_EF;
|
|
} else if (not shared_context.hlc_charging_active and shared_context.legacy_wakeup_done and
|
|
time_in_current_state > PREPARING_TIMEOUT_PAUSED_BY_EV) {
|
|
// We are still here after the wakeup plus some extra delay, so probably the EV really does
|
|
// not want to charge. Switch to ChargingPausedEV state.
|
|
shared_context.current_state = EvseState::ChargingPausedEV;
|
|
}
|
|
}
|
|
}
|
|
|
|
// For DC we go into charging when CurrentDemand started (notify_currentdemand_started)
|
|
|
|
// if (charge_mode == ChargeMode::DC) {
|
|
// DC: wait until car requests power on CP (B->C/D).
|
|
// Then we close contactor and wait for instructions from HLC.
|
|
// HLC will perform CableCheck, PreCharge, and PowerDelivery.
|
|
// These are all controlled from the handlers directly, so there is nothing we need to do here.
|
|
// Once HLC informs us about CurrentDemand has started, we will go to Charging in the handler.
|
|
//}
|
|
|
|
break;
|
|
|
|
case EvseState::Charging:
|
|
if (initialize_state) {
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::ChargingStarted);
|
|
shared_context.hlc_charging_terminate_pause = HlcTerminatePause::Unknown;
|
|
stopwatch.mark("Charging started");
|
|
stopwatch.report_phase();
|
|
auto report = stopwatch.report_all_phases();
|
|
if (config_context.charge_mode == ChargeMode::DC and not internal_context.dc_statistics_printed) {
|
|
internal_context.dc_statistics_printed = true;
|
|
EVLOG_info << "Timing statistics (Plugin to CurrentDemand)";
|
|
EVLOG_info << "-------------------------------------------";
|
|
for (const auto& r : report) {
|
|
EVLOG_info << r;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop charging on errors, user stops, pause requests, or disable
|
|
if (stop_charging_on_fatal_error_internal() or not shared_context.flag_authorized or
|
|
not shared_context.flag_transaction_active or not shared_context.flag_ev_plugged_in or
|
|
shared_context.flag_paused_by_evse or not shared_context.iec_allow_close_contactor or
|
|
shared_context.flag_disable_requested) {
|
|
set_state(EvseState::StoppingCharging);
|
|
break;
|
|
}
|
|
|
|
if (not power_available()) {
|
|
const bool hlc_session_active = shared_context.hlc_charging_active or hlc_use_5percent_current_session;
|
|
if (hlc_session_active) {
|
|
if (config_context.hlc_charge_loop_without_energy_timeout_s > 0) {
|
|
if (not internal_context.hlc_charge_loop_no_energy_timeout_running) {
|
|
internal_context.hlc_charge_loop_no_energy_timeout_running = true;
|
|
internal_context.iso_charge_loop_no_energy_timeout_started =
|
|
std::chrono::steady_clock::now();
|
|
session_log.evse(
|
|
false,
|
|
fmt::format("No energy available, starting ISO charge loop timeout of {} seconds",
|
|
config_context.hlc_charge_loop_without_energy_timeout_s));
|
|
} else {
|
|
const auto no_energy_duration_ms =
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() -
|
|
internal_context.iso_charge_loop_no_energy_timeout_started)
|
|
.count();
|
|
if (no_energy_duration_ms >=
|
|
config_context.hlc_charge_loop_without_energy_timeout_s * 1000) {
|
|
session_log.evse(false, "No energy available, ISO charge loop timeout reached");
|
|
internal_context.hlc_charge_loop_no_energy_timeout_running = false;
|
|
set_state(EvseState::StoppingCharging);
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
session_log.evse(
|
|
false, "No energy available, but no timeout configured, stopping charging immediately");
|
|
set_state(EvseState::StoppingCharging);
|
|
break;
|
|
}
|
|
} else if (config_context.charge_mode == ChargeMode::AC) {
|
|
EVLOG_info << "HLC charging not active";
|
|
// Stop immediately in basic AC mode
|
|
set_state(EvseState::StoppingCharging); // Proxy state to ChargingPausedEVSE
|
|
break;
|
|
}
|
|
} else {
|
|
internal_context.hlc_charge_loop_no_energy_timeout_running = false;
|
|
}
|
|
|
|
if (config_context.charge_mode == ChargeMode::DC) {
|
|
if (initialize_state) {
|
|
bsp->allow_power_on(true, types::evse_board_support::Reason::FullPowerCharging);
|
|
}
|
|
// Enforce EVSE target limits so that energy management
|
|
// changes are applied even when the EV does not send new targets
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (now - last_dc_enforce_target_limits.load() >=
|
|
std::chrono::milliseconds(DC_ENFORCE_TARGET_LIMITS_INTERVAL_MS)) {
|
|
last_dc_enforce_target_limits = now;
|
|
signal_dc_enforce_target_limits(shared_context.current_evse_max_limits);
|
|
}
|
|
} else {
|
|
check_soft_over_current();
|
|
|
|
if (initialize_state) {
|
|
// Allow another wake-up sequence
|
|
shared_context.legacy_wakeup_done = false;
|
|
|
|
bsp->allow_power_on(true, types::evse_board_support::Reason::FullPowerCharging);
|
|
// make sure we are enabling PWM
|
|
if (hlc_use_5percent_current_session) {
|
|
update_pwm_now_if_changed(PWM_5_PERCENT);
|
|
} else {
|
|
update_pwm_now_if_changed_ampere(get_max_current_internal());
|
|
}
|
|
} else {
|
|
// update PWM if it has changed and 5 seconds have passed since last update
|
|
if (not hlc_use_5percent_current_session) {
|
|
update_pwm_max_every_5seconds_ampere(get_max_current_internal());
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EvseState::ChargingPausedEV:
|
|
|
|
if (initialize_state) {
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::ChargingPausedEV);
|
|
}
|
|
|
|
if (not shared_context.flag_transaction_active or not shared_context.flag_authorized or
|
|
not shared_context.flag_ev_plugged_in or shared_context.flag_disable_requested) {
|
|
set_state(EvseState::StoppingCharging);
|
|
break;
|
|
}
|
|
|
|
if (config_context.charge_mode == ChargeMode::AC) {
|
|
check_soft_over_current();
|
|
}
|
|
|
|
// A pause issued by the EV needs to be handled differently for the different charging modes
|
|
|
|
// 1) BASIC AC charging: Nominal PWM needs be running, so the EV can actually resume charging when it wants
|
|
// to
|
|
|
|
// 2) HLC charging: [V2G3-M07-19] requires the EV to switch to state B, so we will end up here in this state
|
|
// [V2G3-M07-20] forces us to switch off PWM.
|
|
// This is also true for nominal PWM AC HLC charging, so an EV that does HLC AC and pauses can only
|
|
// resume in HLC mode and not in BASIC charging.
|
|
|
|
if (shared_context.hlc_charging_active) {
|
|
// This is for HLC charging (both AC and DC)
|
|
|
|
if (bcb_toggle_detected()) {
|
|
shared_context.current_state = EvseState::PrepareCharging;
|
|
}
|
|
|
|
// We come here by a state C->B transition but the ISO message may not have arrived yet,
|
|
// so we wait here until we know wether it is Terminate or Pause. Until we leave PWM on (should not
|
|
// be shut down before SessionStop.req)
|
|
|
|
if (shared_context.hlc_charging_terminate_pause == HlcTerminatePause::Terminate or
|
|
shared_context.hlc_charging_terminate_pause == HlcTerminatePause::Pause) {
|
|
// By staying in ChargingPausedEV we allow the EV to restart a session no matter if Terminate
|
|
// or Pause was used.
|
|
if (shared_context.pwm_running) {
|
|
cp_state_X1();
|
|
}
|
|
}
|
|
|
|
} else {
|
|
// This is for BASIC charging only
|
|
if (not power_available()) {
|
|
shared_context.current_state = EvseState::ChargingPausedEVSE;
|
|
break;
|
|
}
|
|
|
|
// update PWM if it has changed and 5 seconds have passed since last update
|
|
update_pwm_max_every_5seconds_ampere(get_max_current_internal());
|
|
}
|
|
break;
|
|
|
|
case EvseState::ChargingPausedEVSE:
|
|
if (initialize_state) {
|
|
if (not shared_context.hlc_charging_active or not shared_context.flag_transaction_active) {
|
|
// Basic Charging: just turn off PWM
|
|
cp_state_X1();
|
|
}
|
|
internal_context.last_charging_paused_evse_reasons = {};
|
|
}
|
|
|
|
if (not shared_context.flag_transaction_active or not shared_context.flag_authorized or
|
|
not shared_context.flag_ev_plugged_in or shared_context.flag_disable_requested) {
|
|
set_state(EvseState::StoppingCharging);
|
|
break;
|
|
}
|
|
|
|
if (config_context.state_F_after_fault_ms > 0 and not shared_context.hlc_charging_active) {
|
|
// First time we see that a fatal error became active, signal F for a short time.
|
|
// Only use in basic charging mode.
|
|
if (entered_fatal_error_state()) {
|
|
cp_state_F();
|
|
}
|
|
|
|
if (internal_context.cp_state_F_active and
|
|
time_in_fatal_error_state_ms() > config_context.state_F_after_fault_ms) {
|
|
cp_state_X1();
|
|
}
|
|
}
|
|
|
|
// This allows us to retry a session after a fault in cable check
|
|
error_handling->clear_cable_check_fault();
|
|
|
|
{
|
|
types::evse_manager::ChargingPausedEVSEReasons r;
|
|
if (stop_charging_on_fatal_error_internal()) {
|
|
r.reasons.push_back(types::evse_manager::PauseChargingEVSEReasonEnum::Error);
|
|
}
|
|
|
|
if (not power_available()) {
|
|
r.reasons.push_back(types::evse_manager::PauseChargingEVSEReasonEnum::NoEnergy);
|
|
}
|
|
|
|
if (shared_context.flag_paused_by_evse) {
|
|
r.reasons.push_back(types::evse_manager::PauseChargingEVSEReasonEnum::UserPause);
|
|
}
|
|
|
|
if (r.reasons.size() == 0) {
|
|
// resume charging
|
|
if (shared_context.hlc_charging_terminate_pause != HlcTerminatePause::Pause) {
|
|
signal_slac_start(); // wake up SLAC as well
|
|
}
|
|
shared_context.current_state = EvseState::PrepareCharging;
|
|
} else {
|
|
if (internal_context.last_charging_paused_evse_reasons not_eq r) {
|
|
signal_charging_paused_evse_event(r);
|
|
internal_context.last_charging_paused_evse_reasons = r;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
/*
|
|
Stopping Charging:
|
|
- stops an ongoing charging session actively from EVSE side if it is still ongoing
|
|
- switch to this state to stop charging gracefully
|
|
- DC/AC ISO: Sends a stop request via ISO first.
|
|
- DC/AC ISO: Sends a pause request via ISO first if the user paused the session.
|
|
- BC: Disables PWM
|
|
- After that trigger to stop the charging, it waits for the EV to go to state B (it tracks if the relays
|
|
open up).
|
|
- If they don't after a timeout, it performs a hard stop (open relays, stop DC power supplies)
|
|
- after charging is stopped, it switches to PausedEVSE/PausedEV or Finished depending on whether it may
|
|
restart or not (only a short transitional state)
|
|
*/
|
|
case EvseState::StoppingCharging:
|
|
if (initialize_state) {
|
|
bcb_toggle_reset();
|
|
shared_context.legacy_wakeup_done = false;
|
|
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::StoppingCharging);
|
|
|
|
if (shared_context.hlc_charging_active) {
|
|
if (shared_context.hlc_d20_active and shared_context.flag_paused_by_evse) {
|
|
// Request pause via ISO protocol, EV is expected to stop the charging process
|
|
signal_hlc_pause_charging();
|
|
} else {
|
|
// Request stop via ISO protocol, EV is expected to shut down session
|
|
signal_hlc_stop_charging();
|
|
}
|
|
} else {
|
|
cp_state_X1();
|
|
}
|
|
}
|
|
|
|
// Now the EV is informed and we need to wait until the relays open or a timeout occurs.
|
|
if (time_in_current_state > STOPPING_CHARGING_TIMEOUT_MS) {
|
|
EVLOG_warning << "StoppingCharging: EV did not stop within timeout, forcing hard stop.";
|
|
// Perform hard stop
|
|
signal_dc_supply_off();
|
|
bsp->allow_power_on(false, types::evse_board_support::Reason::PowerOff);
|
|
}
|
|
|
|
// Relays still closed? Wait here.
|
|
if (not shared_context.contactor_open) {
|
|
break;
|
|
}
|
|
|
|
// Those are fatal, so we can not recover without replugging.
|
|
if (not shared_context.flag_transaction_active or not shared_context.flag_ev_plugged_in or
|
|
not shared_context.flag_authorized or shared_context.flag_disable_requested) {
|
|
set_state(EvseState::Finished);
|
|
break;
|
|
}
|
|
|
|
// Charging is stopped now, move on to paused state
|
|
if (not power_available() or shared_context.flag_paused_by_evse or
|
|
stop_charging_on_fatal_error_internal()) {
|
|
// Paused was initiated by EVSE, continue to PausedEVSE
|
|
set_state(EvseState::ChargingPausedEVSE);
|
|
break;
|
|
} else {
|
|
// If the reason was not the EVSE, go to paused by EV
|
|
set_state(EvseState::ChargingPausedEV);
|
|
}
|
|
break;
|
|
|
|
// Final state that may only wait for unplug, but does not allow session restart.
|
|
// Cleans the session up.
|
|
case EvseState::Finished:
|
|
|
|
if (initialize_state) {
|
|
// Transaction may already be stopped when it was cancelled earlier.
|
|
// In that case, do not sent a second transactionFinished event.
|
|
if (shared_context.flag_transaction_active) {
|
|
stop_transaction();
|
|
}
|
|
|
|
// Everything should already be off, just do it here again as a backup.
|
|
if (config_context.charge_mode == ChargeMode::DC) {
|
|
signal_dc_supply_off();
|
|
}
|
|
bsp->allow_power_on(false, types::evse_board_support::Reason::PowerOff);
|
|
}
|
|
|
|
// If unplugged or a disable is pending, end the session.
|
|
// When disabled, do not wait for unplug. cp_state_F (set in the Disabled
|
|
// state handler) is signaled to the EV.
|
|
if (not shared_context.flag_ev_plugged_in or shared_context.flag_disable_requested) {
|
|
stop_session();
|
|
set_state(shared_context.flag_disable_requested ? EvseState::Disabled : EvseState::Idle);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (mainloop_runs > max_mainloop_runs) {
|
|
EVLOG_warning << "Charger main loop exceeded maximum number of runs, last_state "
|
|
<< evse_state_to_string(internal_context.last_state_detect_state_change)
|
|
<< " current_state: " << evse_state_to_string(shared_context.current_state);
|
|
}
|
|
} while (internal_context.last_state_detect_state_change not_eq shared_context.current_state);
|
|
}
|
|
|
|
void Charger::process_event(CPEvent cp_event) {
|
|
switch (cp_event) {
|
|
case CPEvent::CarPluggedIn:
|
|
case CPEvent::CarRequestedPower:
|
|
case CPEvent::CarRequestedStopPower:
|
|
case CPEvent::CarUnplugged:
|
|
case CPEvent::BCDtoEF:
|
|
case CPEvent::BCDtoE:
|
|
case CPEvent::EFtoBCD:
|
|
session_log.car(false, fmt::format("Event {}", cpevent_to_string(cp_event)));
|
|
break;
|
|
default:
|
|
session_log.evse(false, fmt::format("Event {}", cpevent_to_string(cp_event)));
|
|
break;
|
|
}
|
|
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_process_event);
|
|
|
|
run_state_machine();
|
|
|
|
// Process all event actions that are independent of the current state
|
|
process_cp_events_independent(cp_event);
|
|
|
|
run_state_machine();
|
|
|
|
// Process all events that depend on the current state
|
|
process_cp_events_state(cp_event);
|
|
|
|
run_state_machine();
|
|
}
|
|
|
|
void Charger::process_cp_events_state(CPEvent cp_event) {
|
|
switch (shared_context.current_state) {
|
|
|
|
case EvseState::Idle:
|
|
if (cp_event == CPEvent::CarPluggedIn) {
|
|
stopwatch.reset();
|
|
stopwatch.mark_phase("ConnSetup");
|
|
shared_context.current_state = EvseState::WaitingForAuthentication;
|
|
shared_context.flag_ev_plugged_in = true;
|
|
}
|
|
break;
|
|
|
|
case EvseState::WaitingForAuthentication:
|
|
if (cp_event == CPEvent::CarRequestedPower) {
|
|
shared_context.iec_allow_close_contactor = true;
|
|
} else if (cp_event == CPEvent::CarRequestedStopPower) {
|
|
shared_context.iec_allow_close_contactor = false;
|
|
}
|
|
break;
|
|
|
|
case EvseState::PrepareCharging:
|
|
if (cp_event == CPEvent::CarRequestedPower) {
|
|
shared_context.iec_allow_close_contactor = true;
|
|
} else if (cp_event == CPEvent::CarRequestedStopPower) {
|
|
shared_context.iec_allow_close_contactor = false;
|
|
signal_dc_supply_off();
|
|
shared_context.current_state = EvseState::ChargingPausedEVSE;
|
|
}
|
|
break;
|
|
|
|
case EvseState::Charging:
|
|
if (cp_event == CPEvent::CarRequestedStopPower) {
|
|
shared_context.iec_allow_close_contactor = false;
|
|
} else if (cp_event == CPEvent::BCDtoE) {
|
|
shared_context.iec_allow_close_contactor = false;
|
|
}
|
|
break;
|
|
|
|
case EvseState::ChargingPausedEV:
|
|
if (cp_event == CPEvent::CarRequestedPower) {
|
|
shared_context.iec_allow_close_contactor = true;
|
|
// For BASIC charging we can simply switch back to Charging
|
|
if (config_context.charge_mode == ChargeMode::AC and not shared_context.hlc_charging_active) {
|
|
shared_context.current_state = EvseState::PrepareCharging;
|
|
} else if (not shared_context.pwm_running) {
|
|
bcb_toggle_detect_start_pulse();
|
|
}
|
|
}
|
|
|
|
if (cp_event == CPEvent::CarRequestedStopPower and not shared_context.pwm_running and
|
|
shared_context.hlc_charging_active) {
|
|
bcb_toggle_detect_stop_pulse();
|
|
}
|
|
break;
|
|
|
|
case EvseState::StoppingCharging:
|
|
// Allow session restart from EV after SessionStop.terminate with BCB toggle
|
|
if (shared_context.hlc_charging_active and not shared_context.pwm_running) {
|
|
if (cp_event == CPEvent::CarRequestedPower) {
|
|
bcb_toggle_detect_start_pulse();
|
|
} else if (cp_event == CPEvent::CarRequestedStopPower) {
|
|
bcb_toggle_detect_stop_pulse();
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Charger::process_cp_events_independent(CPEvent cp_event) {
|
|
switch (cp_event) {
|
|
case CPEvent::CarRequestedStopPower:
|
|
shared_context.iec_allow_close_contactor = false;
|
|
break;
|
|
case CPEvent::CarUnplugged:
|
|
shared_context.flag_ev_plugged_in = false;
|
|
break;
|
|
case CPEvent::PowerOff:
|
|
shared_context.contactor_open = true;
|
|
break;
|
|
case CPEvent::PowerOn:
|
|
shared_context.contactor_open = false;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Charger::update_pwm_max_every_5seconds_ampere(float ampere) {
|
|
float duty_cycle = ampere_to_duty_cycle(ampere);
|
|
if (duty_cycle not_eq internal_context.update_pwm_last_duty_cycle) {
|
|
auto now = std::chrono::steady_clock::now();
|
|
auto time_since_last_update =
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(now - internal_context.last_pwm_update).count();
|
|
if (time_since_last_update >= IEC_PWM_MAX_UPDATE_INTERVAL) {
|
|
update_pwm_now(duty_cycle);
|
|
internal_context.pwm_set_last_ampere = ampere;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Charger::update_pwm_now(float duty_cycle) {
|
|
auto start = std::chrono::steady_clock::now();
|
|
internal_context.update_pwm_last_duty_cycle = duty_cycle;
|
|
shared_context.pwm_running = true;
|
|
|
|
session_log.evse(
|
|
false,
|
|
fmt::format(
|
|
"Set PWM On ({:.1f}%) took {} ms", duty_cycle * 100.,
|
|
(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start)).count()));
|
|
internal_context.last_pwm_update = std::chrono::steady_clock::now();
|
|
internal_context.cp_state_F_active = false;
|
|
bsp->set_pwm(duty_cycle);
|
|
}
|
|
|
|
void Charger::update_pwm_now_if_changed(float duty_cycle) {
|
|
if (internal_context.update_pwm_last_duty_cycle not_eq duty_cycle) {
|
|
update_pwm_now(duty_cycle);
|
|
}
|
|
}
|
|
|
|
void Charger::update_pwm_now_if_changed_ampere(float ampere) {
|
|
float duty_cycle = ampere_to_duty_cycle(ampere);
|
|
if (internal_context.update_pwm_last_duty_cycle not_eq duty_cycle) {
|
|
update_pwm_now(duty_cycle);
|
|
internal_context.pwm_set_last_ampere = ampere;
|
|
}
|
|
}
|
|
|
|
void Charger::cp_state_X1() {
|
|
session_log.evse(false, "Set PWM Off");
|
|
shared_context.pwm_running = false;
|
|
internal_context.update_pwm_last_duty_cycle = 1.;
|
|
internal_context.pwm_set_last_ampere = 0.;
|
|
internal_context.cp_state_F_active = false;
|
|
bsp->set_cp_state_X1();
|
|
}
|
|
|
|
void Charger::cp_state_F() {
|
|
session_log.evse(false, "Set PWM F");
|
|
shared_context.pwm_running = false;
|
|
internal_context.update_pwm_last_duty_cycle = 0.;
|
|
internal_context.pwm_set_last_ampere = 0.;
|
|
internal_context.cp_state_F_active = true;
|
|
bsp->set_cp_state_F();
|
|
}
|
|
|
|
void Charger::run() {
|
|
// spawn new thread and return
|
|
main_thread_handle = std::thread(&Charger::main_thread, this);
|
|
}
|
|
|
|
float Charger::ampere_to_duty_cycle(float ampere) {
|
|
float duty_cycle = 0;
|
|
|
|
// calculate max current
|
|
if (ampere < 5.9) {
|
|
// Invalid argument, switch to error
|
|
duty_cycle = 1.0;
|
|
} else if (ampere <= 6.1) {
|
|
duty_cycle = 0.1;
|
|
} else if (ampere < 52.5) {
|
|
if (ampere > 51.)
|
|
ampere = 51; // Weird gap in norm: 51A .. 52.5A has no defined PWM.
|
|
duty_cycle = ampere / 0.6 / 100.;
|
|
} else if (ampere <= 80) {
|
|
duty_cycle = ((ampere / 2.5) + 64) / 100.;
|
|
} else if (ampere <= 80.1) {
|
|
duty_cycle = 0.97;
|
|
} else {
|
|
// Invalid argument, switch to error
|
|
duty_cycle = 1.0;
|
|
}
|
|
|
|
return duty_cycle;
|
|
}
|
|
|
|
bool Charger::set_max_current(float c, std::chrono::time_point<std::chrono::steady_clock> validUntil) {
|
|
float c_abs{std::fabs(c)};
|
|
|
|
// is it still valid?
|
|
if (validUntil > std::chrono::steady_clock::now()) {
|
|
{
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_set_max_current);
|
|
shared_context.max_current = c_abs;
|
|
shared_context.max_current_valid_until = validUntil;
|
|
}
|
|
// now after max_current is updated with c_abs we can update c_abs with the internal max current which
|
|
// considers the cable limit as well
|
|
c_abs = get_max_current_internal();
|
|
bsp->set_overcurrent_limit(c_abs);
|
|
// the max_current is internally an absolute value. The sign of c is now used to signal
|
|
// if it is charging (c>0) or discharging (c<0)
|
|
signal_max_current(c < 0.0f ? -c_abs : c_abs);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// set pausing flag, it will be picked up by the state machine.
|
|
bool Charger::pause_charging() {
|
|
if (shared_context.flag_transaction_active) {
|
|
shared_context.flag_paused_by_evse = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Charger::resume_charging() {
|
|
if (shared_context.flag_transaction_active) {
|
|
shared_context.flag_paused_by_evse = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
// Cancel transaction/charging from external EvseManager interface (e.g. via OCPP)
|
|
bool Charger::cancel_transaction(const types::evse_manager::StopTransactionRequest& request) {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_cancel_transaction);
|
|
|
|
if (shared_context.flag_transaction_active) {
|
|
|
|
if (shared_context.hlc_charging_active) {
|
|
|
|
if (request.reason == types::evse_manager::StopTransactionReason::EmergencyStop) {
|
|
signal_hlc_error(types::iso15118::EvseError::Error_EmergencyShutdown);
|
|
} else if (request.reason == types::evse_manager::StopTransactionReason::PowerLoss) {
|
|
signal_hlc_error(types::iso15118::EvseError::Error_UtilityInterruptEvent);
|
|
}
|
|
}
|
|
|
|
shared_context.flag_externally_cancelled = true;
|
|
shared_context.flag_authorized = false;
|
|
shared_context.authorized_pnc = false;
|
|
shared_context.last_stop_transaction_reason = request.reason;
|
|
shared_context.stop_transaction_id_token = request.id_tag;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Charger::start_session(bool authfirst) {
|
|
shared_context.session_active = true;
|
|
shared_context.flag_authorized = false;
|
|
shared_context.authorized_pnc = false;
|
|
shared_context.flag_externally_cancelled = false;
|
|
shared_context.session_uuid = utils::generate_session_id(config_context.session_id_type);
|
|
std::optional<types::authorization::ProvidedIdToken> provided_id_token;
|
|
if (authfirst) {
|
|
shared_context.last_start_session_reason = types::evse_manager::StartSessionReason::Authorized;
|
|
provided_id_token = shared_context.id_token;
|
|
} else {
|
|
shared_context.last_start_session_reason = types::evse_manager::StartSessionReason::EVConnected;
|
|
}
|
|
signal_session_started_event(shared_context.last_start_session_reason, provided_id_token);
|
|
}
|
|
|
|
void Charger::stop_session() {
|
|
shared_context.session_active = false;
|
|
shared_context.flag_authorized = false;
|
|
shared_context.authorized_pnc = false;
|
|
shared_context.flag_externally_cancelled = false;
|
|
shared_context.flag_paused_by_evse = false;
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::SessionFinished);
|
|
shared_context.session_uuid.clear();
|
|
}
|
|
|
|
bool Charger::start_transaction() {
|
|
shared_context.stop_transaction_id_token.reset();
|
|
shared_context.last_stop_transaction_reason.reset();
|
|
|
|
types::powermeter::TransactionReq req;
|
|
req.evse_id = evse_id;
|
|
req.transaction_id = shared_context.session_uuid;
|
|
req.identification_status =
|
|
types::powermeter::OCMFUserIdentificationStatus::ASSIGNED; // currently user is always assigned
|
|
req.identification_flags = {}; // TODO: Collect IF. Not all known in EVerest
|
|
req.identification_type = utils::convert_to_ocmf_identification_type(shared_context.id_token.id_token.type);
|
|
req.identification_level = std::nullopt; // TODO: Not yet known to EVerest
|
|
req.identification_data = shared_context.id_token.id_token.value;
|
|
if (!shared_context.validation_result.tariff_messages.empty()) {
|
|
// TODO: Use proper langauge and format if multiple messages part of personal_messages
|
|
req.tariff_text = shared_context.validation_result.tariff_messages.at(0).content;
|
|
}
|
|
|
|
for (const auto& meter : r_powermeter_billing) {
|
|
const auto response = meter->call_start_transaction(req);
|
|
// If we want to start the session but the meter fail, we stop the charging since
|
|
// we can't bill the customer.
|
|
if (response.status == types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR) {
|
|
EVLOG_error << "Failed to start a transaction on the power meter " << response.error.value_or("");
|
|
if (true == config_context.fail_on_powermeter_errors) {
|
|
error_handling->raise_powermeter_transaction_start_failed_error(
|
|
"Failed to start transaction on the power meter");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
store->store_session(shared_context.session_uuid);
|
|
signal_transaction_started_event(shared_context.id_token);
|
|
shared_context.flag_transaction_active = true;
|
|
return true;
|
|
}
|
|
|
|
void Charger::stop_transaction() {
|
|
shared_context.flag_transaction_active = false;
|
|
|
|
if (!shared_context.last_stop_transaction_reason.has_value()) {
|
|
// if the stop transaction reason was already set (e.g. by cancel_transaction), we keep it, else we know it is
|
|
// EVDisconnected
|
|
shared_context.last_stop_transaction_reason = types::evse_manager::StopTransactionReason::EVDisconnected;
|
|
}
|
|
|
|
for (const auto& meter : r_powermeter_billing) {
|
|
const auto response = meter->call_stop_transaction(shared_context.session_uuid);
|
|
// If we fail to stop the transaction, we ignore since there is no
|
|
// path to recovery. Its also not clear what to do
|
|
if (response.status == types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR) {
|
|
EVLOG_error << "Failed to stop a transaction on the power meter " << response.error.value_or("");
|
|
break;
|
|
} else if (response.status == types::powermeter::TransactionRequestStatus::OK) {
|
|
shared_context.start_signed_meter_value = response.start_signed_meter_value;
|
|
shared_context.stop_signed_meter_value = response.signed_meter_value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
store->clear_session();
|
|
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::ChargingFinished);
|
|
signal_transaction_finished_event(shared_context.last_stop_transaction_reason.value(),
|
|
shared_context.stop_transaction_id_token);
|
|
}
|
|
|
|
void Charger::cleanup_transactions_on_startup() {
|
|
// See if we have an open transaction in persistent storage
|
|
auto session_uuid = store->get_session();
|
|
if (not session_uuid.empty()) {
|
|
EVLOG_info << "Cleaning up transaction with UUID " << session_uuid << " on start up";
|
|
store->clear_session();
|
|
|
|
// If yes, try to close nicely with the ID we remember and trigger a transaction finished event on success
|
|
for (const auto& meter : r_powermeter_billing) {
|
|
const auto response = meter->call_stop_transaction(session_uuid);
|
|
// If we fail to stop the transaction, it was probably just not active anymore
|
|
if (response.status == types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR) {
|
|
EVLOG_warning << "Failed to stop a transaction on the power meter " << response.error.value_or("");
|
|
break;
|
|
} else if (response.status == types::powermeter::TransactionRequestStatus::OK) {
|
|
// Fill in OCMF from the recovered transaction
|
|
shared_context.start_signed_meter_value = response.start_signed_meter_value;
|
|
shared_context.stop_signed_meter_value = response.signed_meter_value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Send out event to inform OCPP et al
|
|
std::optional<types::authorization::ProvidedIdToken> id_token;
|
|
signal_transaction_finished_event(types::evse_manager::StopTransactionReason::PowerLoss, id_token);
|
|
}
|
|
|
|
// Now we did what we could to clean up, so if there are still transactions going on in the power meter close them
|
|
// anyway. In this case we cannot generate a transaction finished event for OCPP et al since we cannot match it to
|
|
// our transaction anymore.
|
|
EVLOG_info << "Cleaning up any other transaction on start up";
|
|
for (const auto& meter : r_powermeter_billing) {
|
|
meter->call_stop_transaction("");
|
|
}
|
|
}
|
|
|
|
std::optional<types::units_signed::SignedMeterValue>
|
|
Charger::take_signed_meter_data(std::optional<types::units_signed::SignedMeterValue>& in) {
|
|
std::optional<types::units_signed::SignedMeterValue> out;
|
|
std::swap(out, in);
|
|
return out;
|
|
}
|
|
|
|
std::optional<types::units_signed::SignedMeterValue> Charger::get_stop_signed_meter_value() {
|
|
// This is used only inside of the state machine, so we do not need to lock here.
|
|
return take_signed_meter_data(shared_context.stop_signed_meter_value);
|
|
}
|
|
|
|
std::optional<types::units_signed::SignedMeterValue> Charger::get_start_signed_meter_value() {
|
|
// This is used only inside of the state machine, so we do not need to lock here.
|
|
return take_signed_meter_data(shared_context.start_signed_meter_value);
|
|
}
|
|
|
|
bool Charger::switch_three_phases_while_charging(bool n) {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex,
|
|
Everest::MutexDescription::Charger_switch_three_phases_while_charging);
|
|
|
|
if (shared_context.hlc_charging_active) {
|
|
return false;
|
|
}
|
|
|
|
if (shared_context.current_state == EvseState::Charging) {
|
|
// In charging state, we need to go via a helper state for the delay
|
|
shared_context.switch_3ph1ph_threephase = n;
|
|
shared_context.switch_3ph1ph_threephase_ongoing = true;
|
|
internal_context.switching_phases_return_state = EvseState::PrepareCharging;
|
|
shared_context.current_state = EvseState::SwitchPhases;
|
|
} else if (shared_context.current_state == EvseState::SwitchPhases) {
|
|
shared_context.switch_3ph1ph_threephase = n;
|
|
} else {
|
|
// In all other states we can tell the bsp directly.
|
|
bsp->switch_three_phases_while_charging(n);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Charger::setup(bool has_ventilation, const ChargeMode _charge_mode, bool _ac_hlc_enabled,
|
|
bool _ac_hlc_use_5percent, bool _ac_enforce_hlc, bool _ac_with_soc_timeout,
|
|
float _soft_over_current_tolerance_percent, float _soft_over_current_measurement_noise_A,
|
|
const int _switch_3ph1ph_delay_s, const std::string _switch_3ph1ph_cp_state,
|
|
const int _soft_over_current_timeout_ms, const int _state_F_after_fault_ms,
|
|
const bool fail_on_powermeter_errors, const bool raise_mrec9,
|
|
const int sleep_before_enabling_pwm_hlc_mode_ms, const utils::SessionIdType session_id_type,
|
|
const int hlc_charge_loop_without_energy_timeout_s) {
|
|
// set up board support package
|
|
bsp->setup(has_ventilation);
|
|
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_setup);
|
|
// cache our config variables
|
|
config_context.charge_mode = _charge_mode;
|
|
ac_hlc_enabled_current_session = config_context.ac_hlc_enabled = _ac_hlc_enabled;
|
|
config_context.ac_hlc_use_5percent = _ac_hlc_use_5percent;
|
|
config_context.ac_enforce_hlc = _ac_enforce_hlc;
|
|
config_context.soft_over_current_timeout_ms = _soft_over_current_timeout_ms;
|
|
shared_context.ac_with_soc_timeout = _ac_with_soc_timeout;
|
|
shared_context.ac_with_soc_timer = 3600000;
|
|
soft_over_current_tolerance_percent = _soft_over_current_tolerance_percent;
|
|
soft_over_current_measurement_noise_A = _soft_over_current_measurement_noise_A;
|
|
|
|
config_context.switch_3ph1ph_delay_s = _switch_3ph1ph_delay_s;
|
|
config_context.switch_3ph1ph_cp_state_F = _switch_3ph1ph_cp_state == "F";
|
|
|
|
config_context.state_F_after_fault_ms = _state_F_after_fault_ms;
|
|
config_context.fail_on_powermeter_errors = fail_on_powermeter_errors;
|
|
config_context.raise_mrec9 = raise_mrec9;
|
|
config_context.sleep_before_enabling_pwm_hlc_mode_ms = sleep_before_enabling_pwm_hlc_mode_ms;
|
|
config_context.session_id_type = session_id_type;
|
|
config_context.hlc_charge_loop_without_energy_timeout_s = hlc_charge_loop_without_energy_timeout_s;
|
|
|
|
if (config_context.charge_mode == ChargeMode::AC and config_context.ac_hlc_enabled)
|
|
EVLOG_info << "AC HLC mode enabled.";
|
|
}
|
|
|
|
Charger::EvseState Charger::get_current_state() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_get_current_state);
|
|
return shared_context.current_state;
|
|
}
|
|
|
|
bool Charger::get_authorized_pnc() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_get_authorized_pnc);
|
|
return (shared_context.flag_authorized and shared_context.authorized_pnc);
|
|
}
|
|
|
|
bool Charger::get_authorized_eim() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_get_authorized_eim);
|
|
return (shared_context.flag_authorized and not shared_context.authorized_pnc);
|
|
}
|
|
|
|
bool Charger::get_authorized_pnc_ready_for_hlc() {
|
|
bool auth = false, ready = false;
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex,
|
|
Everest::MutexDescription::Charger_get_authorized_pnc_ready_for_hlc);
|
|
auth = (shared_context.flag_authorized and shared_context.authorized_pnc);
|
|
ready = (shared_context.current_state == EvseState::ChargingPausedEV) or
|
|
(shared_context.current_state == EvseState::ChargingPausedEVSE) or
|
|
(shared_context.current_state == EvseState::Charging);
|
|
return (auth and ready);
|
|
}
|
|
|
|
bool Charger::get_authorized_eim_ready_for_hlc() {
|
|
bool auth = false, ready = false;
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex,
|
|
Everest::MutexDescription::Charger_get_authorized_eim_ready_for_hlc);
|
|
auth = (shared_context.flag_authorized and not shared_context.authorized_pnc);
|
|
ready = (shared_context.current_state == EvseState::ChargingPausedEV) or
|
|
(shared_context.current_state == EvseState::ChargingPausedEVSE) or
|
|
(shared_context.current_state == EvseState::Charging);
|
|
return (auth and ready);
|
|
}
|
|
|
|
std::string Charger::get_session_id() const {
|
|
return shared_context.session_uuid;
|
|
}
|
|
|
|
void Charger::authorize(bool a, const types::authorization::ProvidedIdToken& token,
|
|
const types::authorization::ValidationResult& result) {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_authorize);
|
|
if (a) {
|
|
if (shared_context.flag_externally_cancelled || shared_context.flag_disable_requested) {
|
|
EVLOG_warning
|
|
<< "Received an authorization after the session was externally cancelled. Ignoring this authorization.";
|
|
// Ignore (delayed) authorization responses after an external cancellation or while EVSE is disabled.
|
|
// Without this guard, a delayed auth could restore flag_authorized and prevent the state machine
|
|
// from routing to EvseState::Finished
|
|
return;
|
|
}
|
|
shared_context.id_token = token;
|
|
shared_context.validation_result = result;
|
|
// First user interaction was auth? Then start session already here and not at plug in
|
|
if (not shared_context.session_active) {
|
|
start_session(true);
|
|
}
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::Authorized);
|
|
shared_context.flag_authorized = true;
|
|
shared_context.authorized_pnc =
|
|
token.authorization_type == types::authorization::AuthorizationType::PlugAndCharge;
|
|
} else {
|
|
if (shared_context.session_active) {
|
|
stop_session();
|
|
}
|
|
shared_context.flag_authorized = false;
|
|
}
|
|
}
|
|
|
|
bool Charger::deauthorize() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_deauthorize);
|
|
return deauthorize_internal();
|
|
}
|
|
|
|
bool Charger::deauthorize_internal() {
|
|
if (shared_context.session_active) {
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::Deauthorized);
|
|
auto s = shared_context.current_state;
|
|
|
|
if (s == EvseState::Disabled or s == EvseState::Idle or s == EvseState::WaitingForAuthentication) {
|
|
|
|
// We can safely remove auth as it is not in use right now
|
|
if (not shared_context.flag_authorized) {
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::PluginTimeout);
|
|
if (config_context.raise_mrec9) {
|
|
error_handling->raise_authorization_timeout_error("No authorization was provided within timeout.");
|
|
}
|
|
// this will inform HLC about the timeout to escape the Authorize loop and stop the session
|
|
signal_hlc_plug_in_timeout();
|
|
return false;
|
|
}
|
|
shared_context.flag_authorized = false;
|
|
stop_session();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Charger::enable_disable_initial_state_publish() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_disable);
|
|
types::evse_manager::EnableDisableSource source{types::evse_manager::Enable_source::Unspecified,
|
|
types::evse_manager::Enable_state::Unassigned, 10000};
|
|
|
|
// check the startup state and publish it
|
|
if (shared_context.current_state == EvseState::Disabled) {
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::Disabled);
|
|
source.enable_state = types::evse_manager::Enable_state::Disable;
|
|
} else {
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::Enabled);
|
|
source.enable_state = types::evse_manager::Enable_state::Enable;
|
|
}
|
|
|
|
// ensure the state is in the table
|
|
enable_disable_source_table_update(source);
|
|
active_enable_disable_source = source;
|
|
}
|
|
|
|
bool Charger::enable_disable(int connector_id, const types::evse_manager::EnableDisableSource& source) {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_disable);
|
|
|
|
EVLOG_info << "Received enable/disable request for connector " << connector_id << " with source "
|
|
<< enable_source_to_string(source.enable_source) << " and state "
|
|
<< enable_state_to_string(source.enable_state);
|
|
|
|
const auto last = active_enable_disable_source;
|
|
|
|
// add/update enable_disable_source_table with new source information
|
|
enable_disable_source_table_update(source);
|
|
|
|
// process the table to calculate the current state
|
|
// updates/recalculates active_enable_disable_source
|
|
const bool is_enabled = parse_enable_disable_source_table();
|
|
|
|
// update state to Idle only when applied to non-zero connector ID
|
|
// note Disable changes the state regardless of the connnector ID
|
|
if (connector_id != 0) {
|
|
shared_context.connector_enabled = is_enabled;
|
|
}
|
|
|
|
if (shared_context.current_state == EvseState::Disabled && shared_context.connector_enabled) {
|
|
// When re-enabling, clear the disable flag and re-enable the BSP before
|
|
// the state machine sees Idle for the first time.
|
|
shared_context.flag_disable_requested = false;
|
|
bsp->enable(true);
|
|
shared_context.current_state = EvseState::Idle;
|
|
}
|
|
|
|
// check for state change
|
|
// for this check Unassigned and Enabled are equivalent
|
|
// previous current result
|
|
// ======== ======= ======
|
|
// Unassigned Unassigned false
|
|
// Unassigned Enable false
|
|
// Unassigned Disable true
|
|
// Enable Unassigned false
|
|
// Enable Enable false
|
|
// Enable Disable true
|
|
// Disable Unassigned true
|
|
// Disable Enable true
|
|
// Disable Disable false
|
|
//
|
|
// simplifies to:
|
|
// (previous == Disable or current == Disable) and (previous != current)
|
|
|
|
using namespace types::evse_manager;
|
|
if ((active_enable_disable_source.enable_state == Enable_state::Disable ||
|
|
last.enable_state == Enable_state::Disable) &&
|
|
active_enable_disable_source.enable_state != last.enable_state) {
|
|
// the state has changed so process events
|
|
if (is_enabled) {
|
|
signal_simple_event(types::evse_manager::SessionEventEnum::Enabled);
|
|
} else {
|
|
// Arm the state machine to tear down any active session and transition
|
|
// to Disabled. Each active charging state (Charging, ChargingPausedEV/EVSE,
|
|
// PrepareCharging) checks flag_disable_requested and routes to StoppingCharging
|
|
// or Finished. The Idle state transitions directly to Disabled.
|
|
// cp_state_F() and bsp->enable(false) are called from the Disabled state
|
|
// handler to guarantee correct ordering.
|
|
shared_context.flag_disable_requested = true;
|
|
shared_context.last_stop_transaction_reason = StopTransactionReason::EVSEDisabled;
|
|
}
|
|
// Drive the state machine synchronously so callers see the resulting
|
|
// state (Disabled / Idle) before enable_disable() returns.
|
|
run_state_machine();
|
|
}
|
|
|
|
return is_enabled;
|
|
}
|
|
|
|
bool Charger::parse_enable_disable_source_table() {
|
|
/* Decide if we are enabled or disabled from the current table.
|
|
The logic is as follows:
|
|
|
|
The source is an enum of the following source types :
|
|
|
|
- Unspecified
|
|
- LocalAPI
|
|
- LocalKeyLock
|
|
- ServiceTechnician
|
|
- RemoteKeyLock
|
|
- MobileApp
|
|
- FirmwareUpdate
|
|
- CSMS
|
|
|
|
The state can be either "enable", "disable", or "unassigned".
|
|
|
|
"enable" and "disable" enforce the state to be enable/disable, while unassigned means that the source does not care
|
|
about the state and other sources may decide.
|
|
|
|
Each call to this command will update an internal table that looks like this:
|
|
void enable_disable_source_table_update(const types::evse_manager::EnableDisableSource& update);
|
|
|
|
Evaluation will be done based on priorities. 0 is the highest priority, 10000 the lowest, so in this example the
|
|
connector will be enabled regardless of what other sources say. Imagine LocalKeyLock sends a "unassigned, prio 0",
|
|
the table will then look like this:
|
|
|
|
| Source | State | Priority |
|
|
| ------------ | ------------- | -------- |
|
|
| Unspecified | unassigned | 10000 |
|
|
| LocalAPI | disable | 42 |
|
|
| LocalKeyLock | unassigned | 0 |
|
|
|
|
So now the connector will be disabled, because the second highest priority (42) sets it to disabled.
|
|
|
|
If all sources are unassigned, the connector is enabled.
|
|
If two sources have the same priority, "disabled" has priority over "enabled".
|
|
*/
|
|
|
|
bool is_enabled = true; // By default, it is enabled.
|
|
types::evse_manager::EnableDisableSource winning_source{types::evse_manager::Enable_source::Unspecified,
|
|
types::evse_manager::Enable_state::Unassigned, 10000};
|
|
|
|
// Walk through table
|
|
for (const auto& entry : enable_disable_source_table) {
|
|
// Ignore unassigned entries
|
|
if (entry.enable_state == types::evse_manager::Enable_state::Unassigned) {
|
|
continue;
|
|
}
|
|
|
|
if (winning_source.enable_state == types::evse_manager::Enable_state::Unassigned) {
|
|
// Use the first entry that is not Unassigned
|
|
winning_source = entry;
|
|
if (entry.enable_state == types::evse_manager::Enable_state::Disable) {
|
|
is_enabled = false;
|
|
} else {
|
|
is_enabled = true;
|
|
}
|
|
} else if (entry.enable_priority == winning_source.enable_priority) {
|
|
// At the same priority, disable has higher priority then enable
|
|
if (entry.enable_state == types::evse_manager::Enable_state::Disable) {
|
|
is_enabled = false;
|
|
winning_source = entry;
|
|
}
|
|
|
|
} else if (entry.enable_priority < winning_source.enable_priority) {
|
|
winning_source = entry;
|
|
if (entry.enable_state == types::evse_manager::Enable_state::Disable) {
|
|
is_enabled = false;
|
|
} else {
|
|
is_enabled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
active_enable_disable_source = winning_source;
|
|
return is_enabled;
|
|
}
|
|
|
|
void Charger::enable_disable_source_table_update(const types::evse_manager::EnableDisableSource& source) {
|
|
// already locked
|
|
|
|
// add/update enable_disable_source_table with new source information
|
|
const auto enable_source = source.enable_source;
|
|
const auto fn = [enable_source](const auto& entry) { return entry.enable_source == enable_source; };
|
|
if (auto it = std::find_if(enable_disable_source_table.begin(), enable_disable_source_table.end(), fn);
|
|
it == enable_disable_source_table.end()) {
|
|
// add to table
|
|
enable_disable_source_table.push_back(source);
|
|
} else {
|
|
// update in table
|
|
*it = source;
|
|
}
|
|
}
|
|
|
|
std::string Charger::evse_state_to_string(EvseState s) {
|
|
switch (s) {
|
|
case EvseState::Disabled:
|
|
return ("Disabled");
|
|
break;
|
|
case EvseState::Idle:
|
|
return ("Idle");
|
|
break;
|
|
case EvseState::WaitingForAuthentication:
|
|
return ("Wait for Auth");
|
|
break;
|
|
case EvseState::Charging:
|
|
return ("Charging");
|
|
break;
|
|
case EvseState::ChargingPausedEV:
|
|
return ("Car Paused");
|
|
break;
|
|
case EvseState::ChargingPausedEVSE:
|
|
return ("EVSE Paused");
|
|
break;
|
|
case EvseState::Finished:
|
|
return ("Finished");
|
|
break;
|
|
case EvseState::T_step_EF:
|
|
return ("T_step_EF");
|
|
break;
|
|
case EvseState::T_step_X1:
|
|
return ("T_step_X1");
|
|
break;
|
|
case EvseState::PrepareCharging:
|
|
return ("PrepareCharging");
|
|
break;
|
|
case EvseState::StoppingCharging:
|
|
return ("StoppingCharging");
|
|
break;
|
|
case EvseState::SwitchPhases:
|
|
return ("SwitchPhases");
|
|
break;
|
|
}
|
|
return "Invalid";
|
|
}
|
|
|
|
float Charger::get_max_current_internal() {
|
|
auto maxc = shared_context.max_current;
|
|
const auto max_current_cable = shared_context.max_current_cable.value_or(0.0);
|
|
|
|
if (connector_type == types::evse_board_support::Connector_type::IEC62196Type2Socket and
|
|
max_current_cable < maxc and shared_context.current_state not_eq EvseState::Idle) {
|
|
maxc = max_current_cable;
|
|
}
|
|
|
|
return maxc;
|
|
}
|
|
|
|
float Charger::get_max_current_signalled_to_ev_internal() {
|
|
// For basic charging, the max current signalled to the EV may be different from the actual current limit
|
|
// for up to 5 seconds as the PWM may only be updated every 5 seconds according to IEC61851-1.
|
|
if (not shared_context.hlc_charging_active) {
|
|
return internal_context.pwm_set_last_ampere;
|
|
}
|
|
return get_max_current_internal();
|
|
}
|
|
|
|
void Charger::set_current_drawn_by_vehicle(float l1, float l2, float l3) {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex,
|
|
Everest::MutexDescription::Charger_set_current_drawn_by_vehicle);
|
|
shared_context.current_drawn_by_vehicle[0] = l1;
|
|
shared_context.current_drawn_by_vehicle[1] = l2;
|
|
shared_context.current_drawn_by_vehicle[2] = l3;
|
|
}
|
|
|
|
void Charger::check_soft_over_current() {
|
|
|
|
// Allow some tolerance
|
|
float limit = (get_max_current_signalled_to_ev_internal() + soft_over_current_measurement_noise_A) *
|
|
(1. + soft_over_current_tolerance_percent / 100.);
|
|
|
|
if (std::fabs(shared_context.current_drawn_by_vehicle[0]) > limit or
|
|
std::fabs(shared_context.current_drawn_by_vehicle[1]) > limit or
|
|
std::fabs(shared_context.current_drawn_by_vehicle[2]) > limit) {
|
|
if (not internal_context.over_current) {
|
|
internal_context.over_current = true;
|
|
// timestamp when over current happend first
|
|
internal_context.last_over_current_event = std::chrono::steady_clock::now();
|
|
session_log.evse(false,
|
|
fmt::format("Soft overcurrent event (L1:{}, L2:{}, L3:{}, limit {}), starting timer.",
|
|
shared_context.current_drawn_by_vehicle[0],
|
|
shared_context.current_drawn_by_vehicle[1],
|
|
shared_context.current_drawn_by_vehicle[2], limit));
|
|
}
|
|
} else {
|
|
internal_context.over_current = false;
|
|
}
|
|
auto now = std::chrono::steady_clock::now();
|
|
auto time_since_over_current_started =
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(now - internal_context.last_over_current_event).count();
|
|
if (internal_context.over_current and
|
|
time_since_over_current_started >= config_context.soft_over_current_timeout_ms) {
|
|
auto errstr =
|
|
fmt::format("Soft overcurrent event (L1:{}, L2:{}, L3:{}, limit {}) triggered",
|
|
shared_context.current_drawn_by_vehicle[0], shared_context.current_drawn_by_vehicle[1],
|
|
shared_context.current_drawn_by_vehicle[2], limit);
|
|
session_log.evse(false, errstr);
|
|
// raise the OC error
|
|
error_handling->raise_overcurrent_error(errstr);
|
|
}
|
|
}
|
|
|
|
// returns whether power is actually available from EnergyManager
|
|
// i.e. max_current is in valid range
|
|
bool Charger::power_available() {
|
|
const auto overrun = duration_cast<seconds>(steady_clock::now() - shared_context.max_current_valid_until).count();
|
|
if (overrun > 0) {
|
|
EVLOG_warning << "Power budget expired, falling back to 0. Last update: " << overrun << " seconds ago";
|
|
if (shared_context.max_current > 0.) {
|
|
shared_context.max_current = 0.;
|
|
signal_max_current(shared_context.max_current);
|
|
}
|
|
}
|
|
|
|
if (config_context.charge_mode == ChargeMode::AC) {
|
|
return get_max_current_internal() > 5.9;
|
|
} else {
|
|
const auto evse_limit = shared_context.current_evse_max_limits;
|
|
return evse_limit.evse_maximum_current_limit > 0 and evse_limit.evse_maximum_power_limit > 0;
|
|
}
|
|
}
|
|
|
|
void Charger::request_error_sequence() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_request_error_sequence);
|
|
if (shared_context.current_state == EvseState::WaitingForAuthentication or
|
|
shared_context.current_state == EvseState::PrepareCharging) {
|
|
internal_context.t_step_EF_return_state = shared_context.current_state;
|
|
internal_context.t_step_EF_return_ampere = 0.;
|
|
shared_context.current_state = EvseState::T_step_EF;
|
|
signal_slac_reset();
|
|
if (hlc_use_5percent_current_session) {
|
|
internal_context.t_step_EF_return_pwm = PWM_5_PERCENT;
|
|
} else {
|
|
internal_context.t_step_EF_return_pwm = 0.;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Charger::set_matching_started(bool m) {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_set_matching_started);
|
|
shared_context.matching_started = m;
|
|
}
|
|
|
|
void Charger::reset_dc_enforce_target_limits_timer() {
|
|
last_dc_enforce_target_limits = std::chrono::steady_clock::now();
|
|
}
|
|
|
|
void Charger::notify_currentdemand_started() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex,
|
|
Everest::MutexDescription::Charger_notify_currentdemand_started);
|
|
if (shared_context.current_state == EvseState::PrepareCharging) {
|
|
shared_context.current_state = EvseState::Charging;
|
|
}
|
|
}
|
|
|
|
void Charger::inform_new_evse_max_hlc_limits(const types::iso15118::DcEvseMaximumLimits& _currentEvseMaxLimits) {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex,
|
|
Everest::MutexDescription::Charger_inform_new_evse_max_hlc_limits);
|
|
shared_context.current_evse_max_limits = _currentEvseMaxLimits;
|
|
}
|
|
|
|
types::iso15118::DcEvseMaximumLimits Charger::get_evse_max_hlc_limits() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_get_evse_max_hlc_limits);
|
|
return shared_context.current_evse_max_limits;
|
|
}
|
|
|
|
void Charger::inform_new_evse_min_hlc_limits(const types::iso15118::DcEvseMinimumLimits& limits) {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex,
|
|
Everest::MutexDescription::Charger_inform_new_evse_min_hlc_limits);
|
|
shared_context.current_evse_min_limits = limits;
|
|
}
|
|
|
|
types::iso15118::DcEvseMinimumLimits Charger::get_evse_min_hlc_limits() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_get_evse_min_hlc_limits);
|
|
return shared_context.current_evse_min_limits;
|
|
}
|
|
|
|
// HLC stack signalled a pause request for the lower layers.
|
|
void Charger::dlink_pause() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_dlink_pause);
|
|
shared_context.hlc_allow_close_contactor = false;
|
|
cp_state_X1();
|
|
shared_context.hlc_charging_terminate_pause = HlcTerminatePause::Pause;
|
|
}
|
|
|
|
// HLC requested end of charging session, so we can stop the 5% PWM
|
|
void Charger::dlink_terminate() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_dlink_terminate);
|
|
shared_context.hlc_allow_close_contactor = false;
|
|
cp_state_X1();
|
|
shared_context.hlc_charging_terminate_pause = HlcTerminatePause::Terminate;
|
|
}
|
|
|
|
void Charger::dlink_error() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_dlink_error);
|
|
|
|
shared_context.hlc_allow_close_contactor = false;
|
|
|
|
// Is PWM on at the moment?
|
|
if (not shared_context.pwm_running) {
|
|
// [V2G3-M07-04]: With receiving a D-LINK_ERROR.request from HLE in X1 state, the EVSE's
|
|
// communication node shall perform a state X1 to state E/F to state X1 or X2 transition.
|
|
} else {
|
|
// [V2G3-M07-05]: With receiving a D-LINK_ERROR.request in X2 state from HLE, the EVSE's
|
|
// communication node shall perform a state X2 to X1 to state E/F to state X1 or X2 transition.
|
|
|
|
// Are we in 5% mode or not?
|
|
if (hlc_use_5percent_current_session) {
|
|
// [V2G3-M07-06] Within the control pilot state X1, the communication node shall leave the
|
|
// logical network and change the matching state to "Unmatched". [V2G3-M07-07] With reaching the
|
|
// state "Unmatched", the EVSE shall switch to state E/F.
|
|
|
|
// FIXME: We don't wait for SLAC to go to UNMATCHED in X1 for now but just do a normal 3 seconds
|
|
// t_step_X1 instead. This should be more then sufficient for the SLAC module to reset.
|
|
|
|
// Do t_step_X1 with a t_step_EF afterwards
|
|
// [V2G3-M07-08] The state E/F shall be applied at least T_step_EF: This is already handled in
|
|
// the t_step_EF state.
|
|
internal_context.t_step_X1_return_state = EvseState::T_step_EF;
|
|
internal_context.t_step_X1_return_pwm = 0.;
|
|
internal_context.t_step_EF_return_ampere = 0.;
|
|
shared_context.current_state = EvseState::T_step_X1;
|
|
|
|
// After returning from T_step_EF, go to Waiting for Auth (We are restarting the session)
|
|
internal_context.t_step_EF_return_state = EvseState::WaitingForAuthentication;
|
|
// [V2G3-M07-09] After applying state E/F, the EVSE shall switch to contol pilot state X1 or X2
|
|
// as soon as the EVSE is ready control for pilot incoming duty matching cycle requests: This is
|
|
// already handled in the Auth step.
|
|
|
|
// [V2G3-M07-05] says we need to go through X1 at the end of the sequence
|
|
internal_context.t_step_EF_return_pwm = 0.;
|
|
internal_context.t_step_EF_return_ampere = 0.;
|
|
}
|
|
// else {
|
|
// [V2G3-M07-10] Gives us two options for nominal PWM mode and HLC in case of error: We choose
|
|
// [V2G3-M07-12] (Don't interrupt basic AC charging just because an error in HLC happend) So we
|
|
// don't do anything here, SLAC will be notified anyway to reset
|
|
//}
|
|
}
|
|
}
|
|
|
|
void Charger::set_hlc_charging_active() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_set_hlc_charging_active);
|
|
shared_context.hlc_charging_active = true;
|
|
}
|
|
|
|
void Charger::set_hlc_allow_close_contactor(bool on) {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex,
|
|
Everest::MutexDescription::Charger_set_hlc_allow_close_contactor);
|
|
shared_context.hlc_allow_close_contactor = on;
|
|
}
|
|
|
|
std::optional<types::evse_manager::StopTransactionReason> Charger::get_last_stop_transaction_reason() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex,
|
|
Everest::MutexDescription::Charger_get_last_stop_transaction);
|
|
return shared_context.last_stop_transaction_reason;
|
|
}
|
|
|
|
void Charger::set_hlc_d20_active() {
|
|
shared_context.hlc_d20_active = true;
|
|
}
|
|
|
|
// this resets the BCB sequence (which may contain 1-3 toggle pulses)
|
|
void Charger::bcb_toggle_reset() {
|
|
internal_context.hlc_ev_pause_bcb_count = 0;
|
|
internal_context.hlc_bcb_sequence_started = false;
|
|
}
|
|
|
|
// call this B->C transitions
|
|
void Charger::bcb_toggle_detect_start_pulse() {
|
|
// For HLC charging, PWM already off: This is probably a BCB Toggle to wake us up from sleep mode.
|
|
// Remember start of BCB toggle.
|
|
internal_context.hlc_ev_pause_start_of_bcb = std::chrono::steady_clock::now();
|
|
if (internal_context.hlc_ev_pause_bcb_count == 0) {
|
|
// remember sequence start
|
|
internal_context.hlc_ev_pause_start_of_bcb_sequence = std::chrono::steady_clock::now();
|
|
internal_context.hlc_bcb_sequence_started = true;
|
|
}
|
|
}
|
|
|
|
// call this on C->B transitions
|
|
void Charger::bcb_toggle_detect_stop_pulse() {
|
|
if (not internal_context.hlc_bcb_sequence_started) {
|
|
return;
|
|
}
|
|
|
|
// This is probably and end of BCB toggle, verify it was not too long or too short
|
|
auto pulse_length = std::chrono::steady_clock::now() - internal_context.hlc_ev_pause_start_of_bcb;
|
|
|
|
if (pulse_length > TP_EV_VALD_STATE_DURATION_MIN and pulse_length < TP_EV_VALD_STATE_DURATION_MAX) {
|
|
internal_context.hlc_ev_pause_bcb_count++;
|
|
|
|
session_log.car(false, fmt::format("BCB toggle ({} ms), #{} in sequence",
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(pulse_length).count(),
|
|
internal_context.hlc_ev_pause_bcb_count));
|
|
|
|
} else {
|
|
internal_context.hlc_ev_pause_bcb_count = 0;
|
|
EVLOG_warning << "BCB toggle with invalid duration detected: "
|
|
<< std::chrono::duration_cast<std::chrono::milliseconds>(pulse_length).count();
|
|
}
|
|
}
|
|
|
|
// Query if a BCB sequence (of 1-3 pulses) was detected and is finished. If that is true, PWM can be enabled again
|
|
// etc
|
|
bool Charger::bcb_toggle_detected() {
|
|
auto sequence_length = std::chrono::steady_clock::now() - internal_context.hlc_ev_pause_start_of_bcb_sequence;
|
|
if (internal_context.hlc_bcb_sequence_started and
|
|
(sequence_length > TT_EVSE_VALD_TOGGLE or internal_context.hlc_ev_pause_bcb_count >= 3)) {
|
|
// no need to wait for further BCB toggles
|
|
internal_context.hlc_ev_pause_bcb_count = 0;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Charger::stop_charging_on_fatal_error() {
|
|
Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_errors_prevent_charging);
|
|
return stop_charging_on_fatal_error_internal();
|
|
}
|
|
|
|
bool Charger::entered_fatal_error_state() {
|
|
return shared_context.shutdown_type != ShutdownType::None and
|
|
shared_context.last_shutdown_type == ShutdownType::None;
|
|
}
|
|
|
|
int Charger::time_in_fatal_error_state_ms() {
|
|
if (not internal_context.fatal_error_timer_running) {
|
|
return 0;
|
|
} else {
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() -
|
|
internal_context.fatal_error_became_active)
|
|
.count();
|
|
}
|
|
}
|
|
|
|
bool Charger::stop_charging_on_fatal_error_internal() {
|
|
bool err = false;
|
|
if (shared_context.shutdown_type == ShutdownType::EmergencyShutdown) {
|
|
if (shared_context.last_shutdown_type != ShutdownType::EmergencyShutdown) {
|
|
internal_context.fatal_error_became_active = std::chrono::steady_clock::now();
|
|
emergency_shutdown();
|
|
}
|
|
err = true;
|
|
} else if (shared_context.shutdown_type == ShutdownType::ErrorShutdown) {
|
|
if (shared_context.last_shutdown_type != ShutdownType::ErrorShutdown) {
|
|
internal_context.fatal_error_became_active = std::chrono::steady_clock::now();
|
|
error_shutdown();
|
|
}
|
|
err = true;
|
|
}
|
|
|
|
internal_context.fatal_error_timer_running = err;
|
|
shared_context.last_shutdown_type = shared_context.shutdown_type;
|
|
return err;
|
|
}
|
|
|
|
void Charger::emergency_shutdown() {
|
|
// state F is handled in the state machine
|
|
EVLOG_info << "Initiating emergency shutdown";
|
|
if (shared_context.pwm_running) {
|
|
cp_state_X1();
|
|
}
|
|
|
|
// Shutdown DC power supplies
|
|
if (config_context.charge_mode == ChargeMode::DC) {
|
|
signal_dc_supply_off();
|
|
}
|
|
|
|
// open contactors
|
|
bsp->allow_power_on(false, types::evse_board_support::Reason::PowerOff);
|
|
this->signal_hlc_error(types::iso15118::EvseError::Error_EmergencyShutdown);
|
|
}
|
|
|
|
void Charger::error_shutdown() {
|
|
// state F is handled in the state machine
|
|
EVLOG_info << "Initiating error shutdown";
|
|
// we keep the PWM on. This allows us to keep the HLC session active and send the error to the EV
|
|
|
|
// Shutdown DC power supplies
|
|
if (config_context.charge_mode == ChargeMode::DC) {
|
|
signal_dc_supply_off();
|
|
}
|
|
|
|
// open contactors
|
|
bsp->allow_power_on(false, types::evse_board_support::Reason::PowerOff);
|
|
this->signal_hlc_error(types::iso15118::EvseError::Error_EmergencyShutdown);
|
|
}
|
|
|
|
void Charger::clear_errors_on_unplug() {
|
|
error_handling->clear_overcurrent_error();
|
|
error_handling->clear_internal_error();
|
|
error_handling->clear_powermeter_transaction_start_failed_error();
|
|
error_handling->clear_authorization_timeout_error();
|
|
error_handling->clear_isolation_resistance_fault("Resistance");
|
|
error_handling->clear_isolation_resistance_fault("VoltageToEarth");
|
|
error_handling->clear_cable_check_fault();
|
|
error_handling->clear_over_voltage_error();
|
|
error_handling->clear_voltage_plausibility_fault();
|
|
shared_context.last_shutdown_type = ShutdownType::None;
|
|
}
|
|
|
|
types::evse_manager::EnableDisableSource Charger::get_last_enable_disable_source() {
|
|
return active_enable_disable_source;
|
|
}
|
|
|
|
void Charger::set_state(EvseState s) {
|
|
shared_context.current_state = s;
|
|
}
|
|
|
|
} // namespace module
|