Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,3 @@
ev_add_module(EvManager)
ev_add_module(EvSlac)
ev_add_module(PyEvJosev)

View File

@@ -0,0 +1,31 @@
load("@rules_cc//cc:defs.bzl", "cc_test")
load("//modules:module.bzl", "cc_everest_module")
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_TEST_INCOMPATIBLE")
cc_everest_module(
name = "EvManager",
impls = [
"main",
"ev_manager",
],
)
cc_test(
name = "EvManager_test",
target_compatible_with = CROSS_TEST_INCOMPATIBLE,
srcs = glob(
[
"tests/*.cpp",
"main/*.hpp",
"main/*.cpp",
],
exclude = [
"main/car_simulatorImpl.*",
"main/car_simulation.*",
],
),
includes = ["."],
deps = [
"@catch2//:catch2_main",
],
)

View File

@@ -0,0 +1,33 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/car_simulatorImpl.cpp"
"main/car_simulation.cpp"
"main/simulation_command.cpp"
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/car_simulatorImpl.cpp"
"ev_manager/ev_managerImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
target_compile_features(${MODULE_NAME} PRIVATE cxx_std_17)
set_target_properties(${MODULE_NAME} PROPERTIES CXX_EXTENSIONS OFF)
if (EVEREST_CORE_BUILD_TESTING)
add_subdirectory(tests)
endif ()
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "EvManager.hpp"
#include "main/car_simulatorImpl.hpp"
namespace module {
void EvManager::init() {
invoke_init(*p_main);
invoke_init(*p_ev_manager);
}
void EvManager::ready() {
invoke_ready(*p_main);
invoke_ready(*p_ev_manager);
}
} // namespace module

View File

@@ -0,0 +1,113 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef EV_MANAGER_HPP
#define EV_MANAGER_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/car_simulator/Implementation.hpp>
#include <generated/interfaces/ev_manager/Implementation.hpp>
// headers for required interface implementations
#include <generated/interfaces/ISO15118_ev/Interface.hpp>
#include <generated/interfaces/ev_board_support/Interface.hpp>
#include <generated/interfaces/ev_slac/Interface.hpp>
#include <generated/interfaces/kvs/Interface.hpp>
#include <generated/interfaces/powermeter/Interface.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
int connector_id;
bool auto_enable;
bool auto_exec;
bool auto_exec_infinite;
std::string auto_exec_commands;
double ac_nominal_voltage;
int dc_max_current_limit;
int dc_max_power_limit;
int dc_max_voltage_limit;
int dc_energy_capacity;
int dc_target_current;
int dc_target_voltage;
bool support_sae_j2847;
int dc_discharge_max_current_limit;
int dc_discharge_max_power_limit;
int dc_discharge_target_current;
int dc_discharge_v2g_minimal_soc;
double max_current;
bool three_phases;
int departure_time;
int e_amount;
int soc;
bool keep_cross_boot_plugin_state;
std::string plugin_commands;
bool force_payment_option;
};
class EvManager : public Everest::ModuleBase {
public:
EvManager() = delete;
EvManager(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider,
std::unique_ptr<car_simulatorImplBase> p_main, std::unique_ptr<ev_managerImplBase> p_ev_manager,
std::unique_ptr<ev_board_supportIntf> r_ev_board_support,
std::vector<std::unique_ptr<ISO15118_evIntf>> r_ev, std::vector<std::unique_ptr<ev_slacIntf>> r_slac,
std::vector<std::unique_ptr<powermeterIntf>> r_powermeter, std::vector<std::unique_ptr<kvsIntf>> r_kvs,
Conf& config) :
ModuleBase(info),
mqtt(mqtt_provider),
p_main(std::move(p_main)),
p_ev_manager(std::move(p_ev_manager)),
r_ev_board_support(std::move(r_ev_board_support)),
r_ev(std::move(r_ev)),
r_slac(std::move(r_slac)),
r_powermeter(std::move(r_powermeter)),
r_kvs(std::move(r_kvs)),
config(config){};
Everest::MqttProvider& mqtt;
const std::unique_ptr<car_simulatorImplBase> p_main;
const std::unique_ptr<ev_managerImplBase> p_ev_manager;
const std::unique_ptr<ev_board_supportIntf> r_ev_board_support;
const std::vector<std::unique_ptr<ISO15118_evIntf>> r_ev;
const std::vector<std::unique_ptr<ev_slacIntf>> r_slac;
const std::vector<std::unique_ptr<powermeterIntf>> r_powermeter;
const std::vector<std::unique_ptr<kvsIntf>> r_kvs;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // EV_MANAGER_HPP

View File

@@ -0,0 +1,50 @@
.. _everest_modules_handwritten_EvManager:
.. =========
.. EvManager
.. =========
This Module implements the car simulator for a charging session.
Configuration
=============
``connector_id``
The connector id of the EVSE Manager
to which the simulator connects to.
External MQTT
-------------
The module listens to the following MQTT topics:
``everest_external/nodered/{connector_id}/carsim/cmd/enable``
| Used to enable the car simulator.
| Possible values are:
- ``true``
- ``false``
``everest_external/nodered/{connector_id}/carsim/cmd/execute_charging_session``
| Used to execute a charging session based on the semicolon separated provided command string.
::
"sleep 1;iso_wait_slac_matched;iso_start_v2g_session DC;iso_wait_pwr_ready;sleep 36000"
| (For all available commands see: :ref:`Simulator Commands <everest_modules_handwritten_EvManager_simulator_commands>`)
``everest_external/nodered/{connector_id}/carsim/cmd/modify_charging_session``
| Used to modify the current charging session.
| Follows the same format as ``execute_charging_session``.
.. _everest_modules_handwritten_EvManager_simulator_commands:
Simulator Commands
------------------
``sleep {time in seconds}``
| Sleeps for the specified time.
| Example: ``sleep 10``
``test``

View File

@@ -0,0 +1,16 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "ev_managerImpl.hpp"
namespace module {
namespace ev_manager {
void ev_managerImpl::init() {
}
void ev_managerImpl::ready() {
}
} // namespace ev_manager
} // namespace module

View File

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef EV_MANAGER_EV_MANAGER_IMPL_HPP
#define EV_MANAGER_EV_MANAGER_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/ev_manager/Implementation.hpp>
#include "../EvManager.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace ev_manager {
struct Conf {};
class ev_managerImpl : public ev_managerImplBase {
public:
ev_managerImpl() = delete;
ev_managerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<EvManager>& mod, Conf& config) :
ev_managerImplBase(ev, "ev_manager"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
// insert your public definitions here
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// no commands defined for this interface
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<EvManager>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
// insert your private definitions here
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
// insert other definitions here
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
} // namespace ev_manager
} // namespace module
#endif // EV_MANAGER_EV_MANAGER_IMPL_HPP

View File

@@ -0,0 +1,471 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "car_simulation.hpp"
#include "constants.hpp"
#include <everest/logging.hpp>
constexpr double MS_FACTOR = (1.0 / 60.0 / 60.0 / 1000.0);
void CarSimulation::state_machine() {
using types::ev_board_support::EvCpState;
const auto state_has_changed = sim_data.state != sim_data.last_state;
sim_data.last_state = sim_data.state;
switch (sim_data.state) {
case SimState::UNPLUGGED:
if (state_has_changed) {
r_ev_board_support->call_set_cp_state(EvCpState::A);
r_ev_board_support->call_allow_power_on(false);
// Wait for physical plugin (ev BSP sees state A on CP and not Disconnected)
sim_data.slac_state = types::slac::State::UNMATCHED;
if (!r_ev.empty()) {
r_ev[0]->call_stop_charging();
}
}
break;
case SimState::PLUGGED_IN:
if (state_has_changed) {
r_ev_board_support->call_set_cp_state(EvCpState::B);
r_ev_board_support->call_allow_power_on(false);
simulate_soc();
}
break;
case SimState::CHARGING_REGULATED:
if (state_has_changed || sim_data.pwm_duty_cycle != sim_data.last_pwm_duty_cycle) {
sim_data.last_pwm_duty_cycle = sim_data.pwm_duty_cycle;
// do not draw power if EVSE paused by stopping PWM
if (sim_data.pwm_duty_cycle > 7.0 && sim_data.pwm_duty_cycle < 97.0) {
r_ev_board_support->call_set_cp_state(EvCpState::C);
r_ev_board_support->call_allow_power_on(true);
} else {
r_ev_board_support->call_set_cp_state(EvCpState::B);
r_ev_board_support->call_allow_power_on(false);
}
}
break;
case SimState::CHARGING_FIXED:
// Todo(sl): What to do here
if (state_has_changed) {
// Also draw power if EVSE stopped PWM - this is a break the rules simulator->mode to test the charging
// implementation!
r_ev_board_support->call_set_cp_state(EvCpState::C);
r_ev_board_support->call_allow_power_on(true);
}
break;
case SimState::ERROR_E:
if (state_has_changed) {
r_ev_board_support->call_set_cp_state(EvCpState::E);
r_ev_board_support->call_allow_power_on(false);
}
break;
case SimState::DIODE_FAIL:
if (state_has_changed) {
r_ev_board_support->call_diode_fail(true);
r_ev_board_support->call_allow_power_on(false);
}
break;
case SimState::ISO_POWER_READY:
if (state_has_changed) {
r_ev_board_support->call_set_cp_state(EvCpState::C);
}
break;
case SimState::ISO_CHARGING_REGULATED:
if (state_has_changed) {
r_ev_board_support->call_set_cp_state(EvCpState::C);
r_ev_board_support->call_allow_power_on(true);
}
break;
case SimState::BCB_TOGGLE:
if (sim_data.bcb_toggle_C) {
r_ev_board_support->call_set_cp_state(EvCpState::C);
sim_data.bcb_toggle_C = false;
} else {
r_ev_board_support->call_set_cp_state(EvCpState::B);
sim_data.bcb_toggle_C = true;
++sim_data.bcb_toggles;
}
break;
default:
sim_data.state = SimState::UNPLUGGED;
break;
}
if (not state_has_changed and
(sim_data.state == SimState::CHARGING_REGULATED or sim_data.state == SimState::CHARGING_FIXED or
sim_data.state == SimState::ISO_CHARGING_REGULATED)) {
simulate_soc();
}
timepoint_last_update = std::chrono::steady_clock::now();
};
void CarSimulation::simulate_soc() {
const double ms =
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - timepoint_last_update)
.count();
const double factor = MS_FACTOR * ms;
double power = 0.0;
types::evse_manager::EVInfo ev_info;
switch (charge_mode) {
case ChargeMode::None:
// nothing to do
break;
case ChargeMode::AC:
power = charge_current_a * config.ac_nominal_voltage;
ev_info.target_current = charge_current_a;
ev_info.target_voltage = 0;
break;
case ChargeMode::ACThreePhase:
power = charge_current_a * config.ac_nominal_voltage * 3.0;
ev_info.target_current = charge_current_a;
ev_info.target_voltage = 0;
break;
case ChargeMode::DC:
power = config.dc_target_current * config.dc_target_voltage;
ev_info.target_current = config.dc_target_current;
ev_info.target_voltage = config.dc_target_voltage;
break;
}
if (sim_data.battery_charge_wh > sim_data.battery_capacity_wh) {
sim_data.battery_charge_wh = sim_data.battery_capacity_wh;
} else {
sim_data.battery_charge_wh += power * factor;
}
auto soc = (sim_data.battery_charge_wh / sim_data.battery_capacity_wh) * 100.0;
if (soc > 100.0) {
soc = 100.0;
} else if (soc <= 0.0) {
soc = 0.0;
}
if (latest_soc != soc) {
latest_soc = soc;
if (!r_ev.empty()) {
r_ev[0]->call_update_soc(soc);
}
}
ev_info.soc = soc;
ev_info.battery_capacity = sim_data.battery_capacity_wh;
ev_info.battery_full_soc = 100;
p_ev_manager->publish_ev_info(ev_info);
}
bool CarSimulation::sleep(const CmdArguments& arguments, size_t loop_interval_ms) {
if (not sim_data.sleep_ticks_left.has_value()) {
const auto sleep_time = std::stold(arguments[0]);
const auto sleep_time_ms = sleep_time * 1000;
sim_data.sleep_ticks_left = static_cast<long long>(sleep_time_ms / loop_interval_ms) + 1;
}
auto& sleep_ticks_left = sim_data.sleep_ticks_left.value();
sleep_ticks_left -= 1;
if (not(sleep_ticks_left > 0)) {
sim_data.sleep_ticks_left.reset();
return true;
} else {
return false;
}
}
bool CarSimulation::iec_wait_pwr_ready(const CmdArguments& arguments) {
sim_data.state = SimState::PLUGGED_IN;
return (sim_data.pwm_duty_cycle > 7.0f && sim_data.pwm_duty_cycle < 97.0f);
}
bool CarSimulation::iso_wait_pwm_is_running(const CmdArguments& arguments) {
sim_data.state = SimState::PLUGGED_IN;
return (sim_data.pwm_duty_cycle > 4.0f && sim_data.pwm_duty_cycle < 97.0f);
}
bool CarSimulation::draw_power_regulated(const CmdArguments& arguments) {
charge_current_a = std::stod(arguments[0]);
r_ev_board_support->call_set_ac_max_current(charge_current_a);
if (arguments[1] == constants::THREE_PHASES) {
r_ev_board_support->call_set_three_phases(true);
charge_mode = ChargeMode::ACThreePhase;
} else {
r_ev_board_support->call_set_three_phases(false);
charge_mode = ChargeMode::AC;
}
sim_data.state = SimState::CHARGING_REGULATED;
return true;
}
bool CarSimulation::draw_power_fixed(const CmdArguments& arguments) {
charge_current_a = std::stod(arguments[0]);
r_ev_board_support->call_set_ac_max_current(charge_current_a);
if (arguments[1] == constants::THREE_PHASES) {
r_ev_board_support->call_set_three_phases(true);
charge_mode = ChargeMode::ACThreePhase;
} else {
r_ev_board_support->call_set_three_phases(false);
charge_mode = ChargeMode::AC;
}
sim_data.state = SimState::CHARGING_FIXED;
return true;
}
bool CarSimulation::pause(const CmdArguments& arguments) {
sim_data.state = SimState::PLUGGED_IN;
return true;
}
bool CarSimulation::unplug(const CmdArguments& arguments) {
sim_data.state = SimState::UNPLUGGED;
charge_mode = ChargeMode::None;
return true;
}
bool CarSimulation::error_e(const CmdArguments& arguments) {
sim_data.state = SimState::ERROR_E;
return true;
}
bool CarSimulation::diode_fail(const CmdArguments& arguments) {
sim_data.state = SimState::DIODE_FAIL;
return true;
}
bool CarSimulation::rcd_current(const CmdArguments& arguments) {
sim_data.rcd_current_ma = std::stof(arguments[0]);
r_ev_board_support->call_set_rcd_error(sim_data.rcd_current_ma);
return true;
}
bool CarSimulation::iso_wait_slac_matched(const CmdArguments& arguments) {
sim_data.state = SimState::PLUGGED_IN;
if (sim_data.slac_state == types::slac::State::UNMATCHED) {
EVLOG_debug << "Slac UNMATCHED";
if (!r_slac.empty()) {
EVLOG_debug << "Slac trigger matching";
r_slac[0]->call_reset();
r_slac[0]->call_trigger_matching();
sim_data.slac_state = types::slac::State::MATCHING;
}
}
if (sim_data.slac_state == types::slac::State::MATCHED) {
EVLOG_debug << "Slac Matched";
return true;
}
return false;
}
bool CarSimulation::iso_wait_pwr_ready(const CmdArguments& arguments) {
if (sim_data.iso_pwr_ready) {
sim_data.state = SimState::ISO_POWER_READY;
return true;
}
return false;
}
bool CarSimulation::iso_dc_power_on(const CmdArguments& arguments) {
sim_data.state = SimState::ISO_POWER_READY;
if (sim_data.dc_power_on) {
sim_data.state = SimState::ISO_CHARGING_REGULATED;
r_ev_board_support->call_allow_power_on(true);
charge_mode = ChargeMode::DC;
return true;
}
if (sim_data.iso_charger_paused) {
const auto cmds =
std::array<std::string, 2>{"pause;iso_wait_v2g_session_stopped;sleep 2;iso_wait_pwm_is_running;",
"iso_wait_pwr_ready;iso_wait_for_stop 36000"};
EVLOG_info << "Charger wants to pause the session";
r_ev_board_support->call_allow_power_on(false);
// NOTE(sl): Change when the Energymode has more then 2 values
const std::string energy_mode = (sim_data.energy_mode == EnergyMode::AC) ? "AC" : "DC";
const std::string iso_start_v2g_session = "iso_start_v2g_session " + energy_mode + ";";
auto& modify_session_cmds = sim_data.modify_charging_session_cmds.emplace();
modify_session_cmds = cmds[0];
modify_session_cmds += iso_start_v2g_session;
modify_session_cmds += cmds[1];
sim_data.iso_pwr_ready = false;
sim_data.sleep_ticks_left.reset();
sim_data.iso_charger_paused = false;
// NOTE(sl): return false, otherwise the simulation will end too early before the session cmds can be adjusted
}
return false;
}
bool CarSimulation::iso_start_v2g_session(const CmdArguments& arguments, bool three_phases) {
const auto& energy_mode = arguments[0];
const auto& payment_option = arguments[1];
const auto& departure_time = std::stoi(arguments[2]);
const auto& e_amount = std::stoi(arguments[3]);
const auto selected_payment_option = [payment_option,
this](bool auto_payment_option) -> types::iso15118::SelectedPaymentOption {
if (auto_payment_option) {
return types::iso15118::SelectedPaymentOption{};
}
auto selected_payment_option =
types::iso15118::SelectedPaymentOption{std::nullopt, config.force_payment_option};
if (payment_option == "externalpayment") {
selected_payment_option.payment_option = types::iso15118::PaymentOption::ExternalPayment;
} else if (payment_option == "contract") {
selected_payment_option.payment_option = types::iso15118::PaymentOption::Contract;
} else {
EVLOG_warning << "Go back to auto payment because payment_option was not recognized";
selected_payment_option.enforce_payment_option.reset();
}
return selected_payment_option;
}(payment_option == "auto");
if (energy_mode == constants::AC) {
sim_data.energy_mode = EnergyMode::AC;
if (not three_phases) {
r_ev[0]->call_start_charging(types::iso15118::EnergyTransferMode::AC_single_phase_core,
selected_payment_option, departure_time, e_amount);
charge_mode = ChargeMode::AC;
} else {
r_ev[0]->call_start_charging(types::iso15118::EnergyTransferMode::AC_three_phase_core,
selected_payment_option, departure_time, e_amount);
charge_mode = ChargeMode::ACThreePhase;
}
} else if (energy_mode == constants::DC) {
r_ev[0]->call_start_charging(types::iso15118::EnergyTransferMode::DC_extended, selected_payment_option,
departure_time, e_amount);
sim_data.energy_mode = EnergyMode::DC;
charge_mode = ChargeMode::DC;
} else {
return false;
}
return true;
}
bool CarSimulation::iso_draw_power_regulated(const CmdArguments& arguments) {
charge_current_a = std::stod(arguments[0]);
r_ev_board_support->call_set_ac_max_current(charge_current_a);
if (arguments[1] == constants::THREE_PHASES) {
r_ev_board_support->call_set_three_phases(true);
charge_mode = ChargeMode::ACThreePhase;
} else {
r_ev_board_support->call_set_three_phases(false);
charge_mode = ChargeMode::AC;
}
sim_data.state = SimState::ISO_CHARGING_REGULATED;
return true;
}
bool CarSimulation::iso_stop_charging(const CmdArguments& arguments) {
r_ev[0]->call_stop_charging();
r_ev_board_support->call_allow_power_on(false);
if (sim_data.state != SimState::UNPLUGGED) {
sim_data.state = SimState::PLUGGED_IN;
}
charge_mode = ChargeMode::None;
return true;
}
bool CarSimulation::iso_wait_for_stop(const CmdArguments& arguments, size_t loop_interval_ms) {
if (not sim_data.sleep_ticks_left.has_value()) {
const auto sleep_time_ms = std::stold(arguments[0]) * 1000;
sim_data.sleep_ticks_left = static_cast<long long>(sleep_time_ms / loop_interval_ms) + 1;
}
auto& sleep_ticks_left = sim_data.sleep_ticks_left.value();
sleep_ticks_left -= 1;
if (not(sleep_ticks_left > 0)) {
r_ev[0]->call_stop_charging();
r_ev_board_support->call_allow_power_on(false);
sim_data.state = SimState::PLUGGED_IN;
sim_data.sleep_ticks_left.reset();
return true;
}
if (sim_data.iso_stopped) {
EVLOG_info << "POWER OFF iso stopped";
r_ev_board_support->call_allow_power_on(false);
sim_data.state = SimState::PLUGGED_IN;
sim_data.sleep_ticks_left.reset();
return true;
}
if (sim_data.iso_charger_paused) {
const auto cmds =
std::array<std::string, 2>{"pause;iso_wait_v2g_session_stopped;sleep 2;iso_wait_pwm_is_running;",
"iso_wait_pwr_ready;iso_wait_for_stop 36000"};
EVLOG_info << "Charger wants to pause the session";
r_ev_board_support->call_allow_power_on(false);
// NOTE(sl): Change when the Energymode has more then 2 values
const std::string energy_mode = (sim_data.energy_mode == EnergyMode::AC) ? "AC" : "DC";
const std::string iso_start_v2g_session = "iso_start_v2g_session " + energy_mode + ";";
auto& modify_session_cmds = sim_data.modify_charging_session_cmds.emplace();
modify_session_cmds = cmds[0];
modify_session_cmds += iso_start_v2g_session;
modify_session_cmds += cmds[1];
sim_data.iso_pwr_ready = false;
sim_data.sleep_ticks_left.reset();
sim_data.iso_charger_paused = false;
// NOTE(sl): return false, otherwise the simulation will end too early before the session cmds can be adjusted
return false;
}
return false;
}
bool CarSimulation::iso_wait_v2g_session_stopped(const CmdArguments& arguments) {
if (sim_data.v2g_finished) {
sim_data.v2g_finished = false;
return true;
}
return false;
}
bool CarSimulation::iso_pause_charging(const CmdArguments& arguments) {
r_ev[0]->call_pause_charging();
sim_data.state = SimState::PLUGGED_IN;
sim_data.iso_pwr_ready = false;
return true;
}
bool CarSimulation::iso_wait_for_resume(const CmdArguments& arguments) {
return false;
}
bool CarSimulation::iso_start_bcb_toggle(const CmdArguments& arguments) {
sim_data.v2g_finished = false;
sim_data.state = SimState::BCB_TOGGLE;
if (sim_data.bcb_toggles >= std::stoul(arguments[0]) || sim_data.bcb_toggles == 3) {
sim_data.bcb_toggles = 0;
sim_data.state = SimState::PLUGGED_IN;
return true;
}
return false;
}
bool CarSimulation::wait_for_real_plugin(const CmdArguments& arguments) {
using types::board_support_common::Event;
if (sim_data.actual_bsp_event == Event::A) {
EVLOG_info << "Real plugin detected";
sim_data.state = SimState::PLUGGED_IN;
return true;
}
return false;
}

View File

@@ -0,0 +1,150 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
#include "simulation_data.hpp"
#include "../EvManager.hpp"
#include <generated/interfaces/ISO15118_ev/Interface.hpp>
#include <generated/interfaces/ev_board_support/Interface.hpp>
#include <generated/interfaces/ev_manager/Implementation.hpp>
#include <generated/interfaces/ev_slac/Interface.hpp>
#include <generated/types/ev_board_support.hpp>
using CmdArguments = std::vector<std::string>;
class CarSimulation {
public:
CarSimulation(const std::unique_ptr<ev_board_supportIntf>& r_ev_board_support_,
const std::vector<std::unique_ptr<ISO15118_evIntf>>& r_ev_,
const std::vector<std::unique_ptr<ev_slacIntf>>& r_slac_,
const std::unique_ptr<ev_managerImplBase>& p_ev_manager_, const module::Conf& config_) :
r_ev_board_support(r_ev_board_support_),
r_ev(r_ev_),
r_slac(r_slac_),
p_ev_manager(p_ev_manager_),
config(config_),
timepoint_last_update(std::chrono::steady_clock::now()){};
~CarSimulation() = default;
void reset() {
sim_data = SimulationData();
sim_data.battery_capacity_wh = config.dc_energy_capacity;
double soc = config.soc;
sim_data.battery_charge_wh = config.dc_energy_capacity * (soc / 100.0);
}
void set_soc(double soc) {
if (soc < 0 || soc > 100) {
throw std::out_of_range("SoC value " + std::to_string(soc) + " is out of range (0-100)");
}
sim_data.battery_charge_wh = config.dc_energy_capacity * (soc / 100.0);
}
const SimState& get_state() const {
return sim_data.state;
}
std::optional<std::string>& get_modify_charging_session_cmds() {
return sim_data.modify_charging_session_cmds;
}
void update_modify_charging_session_cmds(const std::string& cmds) {
sim_data.modify_charging_session_cmds.emplace(cmds);
}
void set_state(SimState state) {
sim_data.state = state;
}
void set_bsp_event(types::board_support_common::Event event) {
sim_data.actual_bsp_event = event;
}
void set_pp(types::board_support_common::Ampacity pp) {
sim_data.pp = pp;
}
void set_rcd_current(float rcd_current) {
sim_data.rcd_current_ma = rcd_current;
}
void set_pwm_duty_cycle(float pwm_duty_cycle) {
sim_data.pwm_duty_cycle = pwm_duty_cycle;
}
void set_slac_state(types::slac::State slac_state) {
sim_data.slac_state = slac_state;
}
void set_iso_pwr_ready(bool iso_pwr_ready) {
sim_data.iso_pwr_ready = iso_pwr_ready;
}
void set_evse_max_current(size_t evse_max_current) {
sim_data.evse_maxcurrent = evse_max_current;
}
void set_iso_stopped(bool iso_stopped) {
sim_data.iso_stopped = iso_stopped;
}
void set_iso_charger_paused(bool iso_charger_paused) {
sim_data.iso_charger_paused = iso_charger_paused;
}
void set_v2g_finished(bool v2g_finished) {
sim_data.v2g_finished = v2g_finished;
}
void set_dc_power_on(bool dc_power_on) {
sim_data.dc_power_on = dc_power_on;
}
void state_machine();
bool sleep(const CmdArguments&, size_t);
bool iec_wait_pwr_ready(const CmdArguments&);
bool iso_wait_pwm_is_running(const CmdArguments&);
bool draw_power_regulated(const CmdArguments&);
bool draw_power_fixed(const CmdArguments&);
bool pause(const CmdArguments&);
bool unplug(const CmdArguments&);
bool error_e(const CmdArguments&);
bool diode_fail(const CmdArguments&);
bool rcd_current(const CmdArguments&);
bool iso_wait_slac_matched(const CmdArguments&);
bool iso_wait_pwr_ready(const CmdArguments&);
bool iso_dc_power_on(const CmdArguments&);
bool iso_start_v2g_session(const CmdArguments&, bool);
bool iso_draw_power_regulated(const CmdArguments&);
bool iso_stop_charging(const CmdArguments&);
bool iso_wait_for_stop(const CmdArguments&, size_t);
bool iso_wait_v2g_session_stopped(const CmdArguments&);
bool iso_pause_charging(const CmdArguments&);
bool iso_wait_for_resume(const CmdArguments&);
bool iso_start_bcb_toggle(const CmdArguments&);
bool wait_for_real_plugin(const CmdArguments&);
private:
SimulationData sim_data;
const module::Conf& config;
std::chrono::time_point<std::chrono::steady_clock> timepoint_last_update;
double charge_current_a{0};
double latest_soc{0};
enum class ChargeMode {
None,
AC,
ACThreePhase,
DC,
} charge_mode{ChargeMode::None};
const std::unique_ptr<ev_board_supportIntf>& r_ev_board_support;
const std::vector<std::unique_ptr<ISO15118_evIntf>>& r_ev;
const std::vector<std::unique_ptr<ev_slacIntf>>& r_slac;
const std::unique_ptr<ev_managerImplBase>& p_ev_manager;
void simulate_soc();
};

View File

@@ -0,0 +1,439 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "car_simulatorImpl.hpp"
#include "constants.hpp"
#include "simulation_command.hpp"
#include <everest/logging.hpp>
namespace module::main {
void car_simulatorImpl::init() {
simulated_already_plugged_in_key = fmt::format("{}_{}_simulated_already_plugged_in", mod->info.name, mod->info.id);
simulated_plugged_in_command_key = fmt::format("{}_{}_simulated_plugged_in_command", mod->info.name, mod->info.id);
last_commands = {};
loop_interval_ms = constants::DEFAULT_LOOP_INTERVAL_MS;
register_all_commands();
subscribe_to_variables_on_init();
car_simulation = std::make_unique<CarSimulation>(mod->r_ev_board_support, mod->r_ev, mod->r_slac, mod->p_ev_manager,
mod->config);
std::thread(&car_simulatorImpl::run, this).detach();
}
void car_simulatorImpl::ready() {
subscribe_to_external_mqtt();
setup_ev_parameters();
if (mod->config.keep_cross_boot_plugin_state) {
if (!mod->r_kvs.empty()) {
auto plugin_command_variant = mod->r_kvs.at(0)->call_load(simulated_plugged_in_command_key);
auto* old_plugin_command = std::get_if<std::string>(&plugin_command_variant);
// If the old plugin command was not stored use this one as a default backup which will require an unplug
std::string plugin_command{"iec_wait_pwr_ready;sleep 60"};
if (old_plugin_command != nullptr) {
plugin_command = *old_plugin_command;
}
auto already_plugged_in_variant = mod->r_kvs.at(0)->call_load(simulated_already_plugged_in_key);
bool* was_plugged_in = std::get_if<bool>(&already_plugged_in_variant);
if (was_plugged_in != nullptr && *was_plugged_in) {
enabled = true;
handle_modify_charging_session(plugin_command);
plugged_in = true;
enabled = false;
}
}
}
if (mod->config.auto_enable) {
auto enable_copy = mod->config.auto_enable;
handle_enable(enable_copy);
}
if (mod->config.auto_exec) {
auto value_copy = mod->config.auto_exec_commands;
handle_execute_charging_session(value_copy);
}
}
void car_simulatorImpl::handle_enable(bool& value) {
if (enabled == value) {
// ignore if value is the same
EVLOG_warning << "Enabled value didn't change, ignoring enable!";
return;
}
reset_car_simulation_defaults();
call_ev_board_support_functions();
enabled = value;
mod->r_ev_board_support->call_enable(value);
publish_enabled(value);
}
void car_simulatorImpl::handle_execute_charging_session(std::string& value) {
if (!check_can_execute()) {
return;
}
set_execution_active(false);
reset_car_simulation_defaults();
update_command_queue(value);
const std::lock_guard<std::mutex> lock{car_simulation_mutex};
if (!command_queue.empty()) {
set_execution_active(true);
}
}
void car_simulatorImpl::handle_modify_charging_session(std::string& value) {
if (!enabled) {
EVLOG_warning << "Simulation disabled, cannot execute charging simulation.";
return;
}
set_execution_active(false);
update_command_queue(value);
const std::lock_guard<std::mutex> lock{car_simulation_mutex};
if (!command_queue.empty()) {
set_execution_active(true);
}
}
void car_simulatorImpl::run() {
while (true) {
if (enabled && execution_active) {
bool finished = run_simulation_loop();
if (cancel_charging_session_flag) {
cancel_charging_session_flag = false;
finished = true;
}
auto& modify_session_cmds = car_simulation->get_modify_charging_session_cmds();
if (modify_session_cmds.has_value()) {
auto cmds = modify_session_cmds.value();
handle_modify_charging_session(cmds);
modify_session_cmds.reset();
}
if (finished) {
EVLOG_info << "Finished simulation.";
set_execution_active(false);
reset_car_simulation_defaults();
// If we have auto_exec_infinite configured, restart simulation when it is done
if (mod->config.auto_exec && mod->config.auto_exec_infinite) {
auto value_copy = mod->config.auto_exec_commands;
handle_execute_charging_session(value_copy);
}
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(loop_interval_ms));
}
}
void car_simulatorImpl::register_all_commands() {
command_registry = std::make_unique<CommandRegistry>();
command_registry->register_command("sleep", 1, [this](const CmdArguments& arguments) {
return this->car_simulation->sleep(arguments, loop_interval_ms);
});
command_registry->register_command("iec_wait_pwr_ready", 0, [this](const CmdArguments& arguments) {
if (!mod->r_kvs.empty() and not plugged_in) {
plugged_in = true;
mod->r_kvs.at(0)->call_store(simulated_already_plugged_in_key, true);
mod->r_kvs.at(0)->call_store(simulated_plugged_in_command_key, last_commands);
}
return this->car_simulation->iec_wait_pwr_ready(arguments);
});
command_registry->register_command("iso_wait_pwm_is_running", 0, [this](const CmdArguments& arguments) {
return this->car_simulation->iso_wait_pwm_is_running(arguments);
});
command_registry->register_command("draw_power_regulated", 2, [this](const CmdArguments& arguments) {
return this->car_simulation->draw_power_regulated(arguments);
});
command_registry->register_command("draw_power_fixed", 2, [this](const CmdArguments& arguments) {
return this->car_simulation->draw_power_fixed(arguments);
});
command_registry->register_command(
"pause", 0, [this](const CmdArguments& arguments) { return this->car_simulation->pause(arguments); });
command_registry->register_command("unplug", 0, [this](const CmdArguments& arguments) {
if (!mod->r_kvs.empty() and plugged_in) {
plugged_in = false;
mod->r_kvs.at(0)->call_store(simulated_already_plugged_in_key, false);
}
return this->car_simulation->unplug(arguments);
});
command_registry->register_command(
"error_e", 0, [this](const CmdArguments& arguments) { return this->car_simulation->error_e(arguments); });
command_registry->register_command(
"diode_fail", 0, [this](const CmdArguments& arguments) { return this->car_simulation->diode_fail(arguments); });
command_registry->register_command("rcd_current", 1, [this](const CmdArguments& arguments) {
return this->car_simulation->rcd_current(arguments);
});
command_registry->register_command("iso_draw_power_regulated", 2, [this](const CmdArguments& arguments) {
return this->car_simulation->iso_draw_power_regulated(arguments);
});
command_registry->register_command("wait_for_real_plugin", 0, [this](const CmdArguments& arguments) {
return this->car_simulation->wait_for_real_plugin(arguments);
});
command_registry->register_command("plugin", 0, [this](const CmdArguments& /*arguments*/) {
if (not mod->config.plugin_commands.empty()) {
car_simulation->update_modify_charging_session_cmds(mod->config.plugin_commands);
return false;
}
EVLOG_error << "plugin command called but \"plugin_commands\" config key not set";
return true;
});
command_registry->register_command("set_soc", 1, [this](const CmdArguments& arguments) {
try {
double soc = std::stod(arguments.at(0));
this->car_simulation->set_soc(soc);
} catch (const std::invalid_argument& e) {
EVLOG_error << "set_soc command called with invalid argument: " << arguments.at(0);
return true;
} catch (const std::out_of_range& e) {
EVLOG_error << "set_soc command called with out of range argument: " << arguments.at(0);
return true;
}
return true;
});
if (!mod->r_slac.empty()) {
command_registry->register_command("iso_wait_slac_matched", 0, [this](const CmdArguments& arguments) {
if (!mod->r_kvs.empty() and not plugged_in) {
plugged_in = true;
mod->r_kvs.at(0)->call_store(simulated_already_plugged_in_key, true);
mod->r_kvs.at(0)->call_store(simulated_plugged_in_command_key, last_commands);
}
return this->car_simulation->iso_wait_slac_matched(arguments);
});
}
if (!mod->r_ev.empty()) {
command_registry->register_command("iso_wait_pwr_ready", 0, [this](const CmdArguments& arguments) {
return this->car_simulation->iso_wait_pwr_ready(arguments);
});
command_registry->register_command("iso_dc_power_on", 0, [this](const CmdArguments& arguments) {
return this->car_simulation->iso_dc_power_on(arguments);
});
command_registry->register_command("iso_start_v2g_session", 1, [this](const CmdArguments& arguments) {
constexpr auto payment_option = "auto";
EVLOG_debug << "Using default payment_options, departure time and eamount";
CmdArguments args{arguments[0], payment_option, std::to_string(mod->config.departure_time),
std::to_string(mod->config.e_amount)};
return this->car_simulation->iso_start_v2g_session(args, mod->config.three_phases);
});
command_registry->register_command("iso_start_v2g_session", 2, [this](const CmdArguments& arguments) {
EVLOG_debug << "Using default departure time and eamount";
CmdArguments args{arguments[0], arguments[1], std::to_string(mod->config.departure_time),
std::to_string(mod->config.e_amount)};
return this->car_simulation->iso_start_v2g_session(args, mod->config.three_phases);
});
command_registry->register_command("iso_start_v2g_session", 3, [this](const CmdArguments& arguments) {
constexpr auto payment_option = "auto";
EVLOG_debug << "Using default payment_options";
CmdArguments args{arguments[0], payment_option, arguments[1], arguments[2]};
return this->car_simulation->iso_start_v2g_session(args, mod->config.three_phases);
});
command_registry->register_command("iso_start_v2g_session", 4, [this](const CmdArguments& arguments) {
// 1. Argument: EnergyMode
// 2. Argument: PaymentOption
// 3. Argument: DepartureTime
// 4. Argument: EAmount
return this->car_simulation->iso_start_v2g_session(arguments, mod->config.three_phases);
});
command_registry->register_command("iso_stop_charging", 0, [this](const CmdArguments& arguments) {
return this->car_simulation->iso_stop_charging(arguments);
});
command_registry->register_command("iso_wait_for_stop", 1, [this](const CmdArguments& arguments) {
return this->car_simulation->iso_wait_for_stop(arguments, loop_interval_ms);
});
command_registry->register_command("iso_wait_v2g_session_stopped", 0, [this](const CmdArguments& arguments) {
return this->car_simulation->iso_wait_v2g_session_stopped(arguments);
});
command_registry->register_command("iso_pause_charging", 0, [this](const CmdArguments& arguments) {
return this->car_simulation->iso_pause_charging(arguments);
});
command_registry->register_command("iso_wait_for_resume", 0, [this](const CmdArguments& arguments) {
return this->car_simulation->iso_wait_for_resume(arguments);
});
command_registry->register_command("iso_start_bcb_toggle", 1, [this](const CmdArguments& arguments) {
return this->car_simulation->iso_start_bcb_toggle(arguments);
});
}
}
bool car_simulatorImpl::run_simulation_loop() {
// Execute sim commands until a command blocks, or we are finished
const std::lock_guard<std::mutex> lock{car_simulation_mutex};
while (execution_active && !command_queue.empty()) {
auto& current_command = command_queue.front();
auto command_blocked = false;
try {
command_blocked = !current_command.execute();
} catch (const std::exception& e) {
EVLOG_error << e.what();
}
if (!command_blocked) {
command_queue.pop();
} else {
break; // command blocked, wait for timer to run this function again
}
}
car_simulation->state_machine();
return command_queue.empty();
}
bool car_simulatorImpl::check_can_execute() {
if (!enabled) {
EVLOG_warning << "Simulation disabled, cannot execute charging simulation.";
return false;
}
if (execution_active) {
EVLOG_warning << "Execution of charging session simulation already running, cannot start new one.";
return false;
}
return true;
}
void car_simulatorImpl::subscribe_to_variables_on_init() {
// subscribe bsp_event
const std::lock_guard<std::mutex> lock{car_simulation_mutex};
using types::board_support_common::BspEvent;
mod->r_ev_board_support->subscribe_bsp_event([this](const auto& bsp_event) {
car_simulation->set_bsp_event(bsp_event.event);
if (bsp_event.event == types::board_support_common::Event::Disconnected &&
car_simulation->get_state() != SimState::UNPLUGGED) {
cancel_charging_session();
}
mod->p_ev_manager->publish_bsp_event(bsp_event);
});
// subscribe bsp_measurement
using types::board_support_common::BspMeasurement;
mod->r_ev_board_support->subscribe_bsp_measurement([this](const auto& measurement) {
car_simulation->set_pp(measurement.proximity_pilot.ampacity);
car_simulation->set_pwm_duty_cycle(measurement.cp_pwm_duty_cycle);
if (measurement.rcd_current_mA.has_value()) {
car_simulation->set_rcd_current(measurement.rcd_current_mA.value());
}
});
// subscribe EVInfo
using types::evse_manager::EVInfo;
mod->r_ev_board_support->subscribe_ev_info(
[this](const auto& ev_info) { mod->p_ev_manager->publish_ev_info(ev_info); });
// subscribe slac_state
if (!mod->r_slac.empty()) {
const auto& slac = mod->r_slac.at(0);
slac->subscribe_state([this](const auto& state) { car_simulation->set_slac_state(state); });
}
// subscribe ev events
if (!mod->r_ev.empty()) {
const auto& _ev = mod->r_ev.at(0);
_ev->subscribe_ev_power_ready([this](auto value) { car_simulation->set_iso_pwr_ready(value); });
_ev->subscribe_ac_evse_max_current(
[this](auto value) { mod->r_ev_board_support->call_set_ac_max_current(value); });
_ev->subscribe_ac_evse_target_power([this](const types::iso15118::AcTargetPower& value) {
if (value.target_active_power.has_value()) {
auto phase_count = mod->config.three_phases ? 3 : 1;
if (value.target_active_power_L2.has_value() and mod->config.three_phases) {
phase_count--;
}
if (value.target_active_power_L3.has_value() and mod->config.three_phases) {
phase_count--;
}
const auto current = value.target_active_power.value() / (mod->config.ac_nominal_voltage * phase_count);
mod->r_ev_board_support->call_set_ac_max_current(current);
}
// TODO(SL): Adding missing reactive power
});
_ev->subscribe_stop_from_charger([this]() { car_simulation->set_iso_stopped(true); });
_ev->subscribe_v2g_session_finished([this]() { car_simulation->set_v2g_finished(true); });
_ev->subscribe_dc_power_on([this]() { car_simulation->set_dc_power_on(true); });
_ev->subscribe_pause_from_charger([this]() { car_simulation->set_iso_charger_paused(true); });
}
}
void car_simulatorImpl::setup_ev_parameters() {
if (!mod->r_ev.empty()) {
mod->r_ev[0]->call_set_dc_params({mod->config.dc_max_current_limit, mod->config.dc_max_power_limit,
mod->config.dc_max_voltage_limit, mod->config.dc_energy_capacity,
mod->config.dc_target_current, mod->config.dc_target_voltage});
if (mod->config.support_sae_j2847) {
mod->r_ev[0]->call_enable_sae_j2847_v2g_v2h();
mod->r_ev[0]->call_set_bpt_dc_params(
{mod->config.dc_discharge_max_current_limit, mod->config.dc_discharge_max_power_limit,
mod->config.dc_discharge_target_current, mod->config.dc_discharge_v2g_minimal_soc});
}
}
}
void car_simulatorImpl::call_ev_board_support_functions() {
mod->r_ev_board_support->call_allow_power_on(false);
mod->r_ev_board_support->call_set_ac_max_current(mod->config.max_current);
mod->r_ev_board_support->call_set_three_phases(mod->config.three_phases);
}
void car_simulatorImpl::subscribe_to_external_mqtt() {
const auto& mqtt = mod->mqtt;
mqtt.subscribe("everest_external/nodered/" + std::to_string(mod->config.connector_id) + "/carsim/cmd/enable",
[this](const std::string& message) {
auto enable = (message == "true") ? true : false;
handle_enable(enable);
});
mqtt.subscribe("everest_external/nodered/" + std::to_string(mod->config.connector_id) +
"/carsim/cmd/execute_charging_session",
[this](const auto data) {
auto data_copy{data};
handle_execute_charging_session(data_copy);
});
mqtt.subscribe("everest_external/nodered/" + std::to_string(mod->config.connector_id) +
"/carsim/cmd/modify_charging_session",
[this](auto data) {
auto data_copy = data;
handle_modify_charging_session(data_copy);
});
}
void car_simulatorImpl::reset_car_simulation_defaults() {
const std::lock_guard<std::mutex> lock{car_simulation_mutex};
car_simulation->reset();
}
void car_simulatorImpl::update_command_queue(std::string& value) {
const std::lock_guard<std::mutex> lock{car_simulation_mutex};
last_commands = value;
command_queue = SimulationCommand::parse_sim_commands(value, *command_registry);
}
void car_simulatorImpl::set_execution_active(bool value) {
execution_active = value;
}
void car_simulatorImpl::cancel_charging_session() {
cancel_charging_session_flag = true;
}
} // namespace module::main

View File

@@ -0,0 +1,95 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef MAIN_CAR_SIMULATOR_IMPL_HPP
#define MAIN_CAR_SIMULATOR_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/car_simulator/Implementation.hpp>
#include "../EvManager.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
#include "car_simulation.hpp"
#include "command_registry.hpp"
#include <queue>
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace main {
struct Conf {};
class car_simulatorImpl : public car_simulatorImplBase {
public:
car_simulatorImpl() = delete;
car_simulatorImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<EvManager>& mod, Conf& config) :
car_simulatorImplBase(ev, "main"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
// insert your public definitions here
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// command handler functions (virtual)
virtual void handle_enable(bool& value) override;
virtual void handle_execute_charging_session(std::string& value) override;
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<EvManager>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
void run();
void handle_modify_charging_session(std::string& value);
bool check_can_execute();
bool run_simulation_loop();
void register_all_commands();
void subscribe_to_variables_on_init();
void setup_ev_parameters();
void call_ev_board_support_functions();
void subscribe_to_external_mqtt();
void reset_car_simulation_defaults();
void update_command_queue(std::string& value);
void set_execution_active(bool value);
void cancel_charging_session();
std::unique_ptr<CommandRegistry> command_registry;
std::mutex car_simulation_mutex;
std::unique_ptr<CarSimulation> car_simulation;
bool enabled{false};
std::atomic<bool> execution_active{false};
std::atomic<bool> cancel_charging_session_flag{false};
std::atomic<bool> plugged_in{false};
size_t loop_interval_ms{};
std::queue<SimulationCommand> command_queue;
std::string simulated_already_plugged_in_key;
std::string simulated_plugged_in_command_key;
std::string last_commands;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
// insert other definitions here
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
} // namespace main
} // namespace module
#endif // MAIN_CAR_SIMULATOR_IMPL_HPP

View File

@@ -0,0 +1,84 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
#include <cstddef>
#include <functional>
#include <memory>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
class RegisteredCommandBase {
public:
virtual ~RegisteredCommandBase() = default;
virtual bool operator()(const std::vector<std::string>& /*arguments*/) const = 0;
[[nodiscard]] virtual std::size_t get_argument_count() const = 0;
};
class RegisteredCommand : public RegisteredCommandBase {
public:
RegisteredCommand(std::string command_, std::size_t argument_count_,
std::function<bool(std::vector<std::string>)> function_) :
command_name{std::move(command_)}, argument_count(argument_count_), function{std::move(function_)} {
}
~RegisteredCommand() override = default;
bool operator()(const std::vector<std::string>& arguments) const override {
if (arguments.size() != argument_count) {
throw std::invalid_argument{"Invalid number of arguments for: " + command_name + " expected " +
std::to_string(argument_count) + " got " + std::to_string(arguments.size())};
}
return function(arguments);
}
[[nodiscard]] std::size_t get_argument_count() const override {
return argument_count;
}
private:
std::string command_name;
std::size_t argument_count;
std::function<bool(std::vector<std::string>)> function;
};
class CommandRegistry {
public:
CommandRegistry() = default;
void register_command(std::string command_name, size_t argument_count,
const std::function<bool(std::vector<std::string>)>& function) {
registered_commands.emplace(command_name,
std::make_unique<RegisteredCommand>(command_name, argument_count, function));
}
const RegisteredCommandBase& get_registered_command(const std::string& command_name,
std::size_t arguments_count) const {
const auto cmd_count = registered_commands.count(command_name);
if (cmd_count == 1) {
if (auto iter = registered_commands.find(command_name); iter != registered_commands.end()) {
return *iter->second;
}
} else if (cmd_count >= 2) {
auto range = registered_commands.equal_range(command_name);
for (auto iter = range.first; iter != range.second; ++iter) {
if (iter->second->get_argument_count() == arguments_count) {
return *iter->second;
}
}
}
throw std::invalid_argument{"Command not found: " + command_name};
}
private:
using RegisteredCommands = std::unordered_multimap<std::string, std::unique_ptr<RegisteredCommandBase>>;
RegisteredCommands registered_commands;
};

View File

@@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
namespace constants {
static constexpr auto THREE_PHASES{"3"};
static constexpr auto DEFAULT_LOOP_INTERVAL_MS{250};
static constexpr auto AC{"ac"};
static constexpr auto DC{"dc"};
} // namespace constants

View File

@@ -0,0 +1,90 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "simulation_command.hpp"
#include "command_registry.hpp"
#include <algorithm>
#include <queue>
#include <sstream>
#include <string>
#include <vector>
SimulationCommand::SimulationCommand(const RegisteredCommandBase* registered_command_in,
const CmdArguments& arguments_in) :
arguments{arguments_in}, registered_command{registered_command_in} {
}
bool SimulationCommand::execute() const {
return (*registered_command)(arguments);
}
std::queue<SimulationCommand> SimulationCommand::parse_sim_commands(const std::string& value,
const CommandRegistry& command_registry) {
auto commands_vector{convert_commands_string_to_vector(value)};
auto commands_with_arguments{split_into_commands_with_arguments(commands_vector)};
return compile_commands(commands_with_arguments, command_registry);
}
SimulationCommand::RawCommands SimulationCommand::convert_commands_string_to_vector(const std::string& commands_view) {
auto commands = std::string{commands_view};
// convert to lower case inplace
std::transform(commands.begin(), commands.end(), commands.begin(),
[](const auto& character) { return std::tolower(character); });
// replace newlines with semicolons
std::replace(commands.begin(), commands.end(), '\n', ';');
// split by semicolons
std::stringstream commands_stream{commands};
auto command = std::string{};
auto commands_vector = std::vector<std::string>{};
while (std::getline(commands_stream, command, ';')) {
commands_vector.push_back(command);
}
return commands_vector;
}
SimulationCommand::CommandsWithArguments
SimulationCommand::split_into_commands_with_arguments(std::vector<std::string>& commands_vector) {
auto commands_with_arguments = std::vector<std::pair<std::string, std::vector<std::string>>>{};
for (auto& command : commands_vector) {
commands_with_arguments.push_back(split_into_command_with_arguments(command));
}
return commands_with_arguments;
}
SimulationCommand::CommandWithArguments SimulationCommand::split_into_command_with_arguments(std::string& command) {
// replace commas with spaces
std::replace(command.begin(), command.end(), ',', ' ');
// get command name and arguments
auto command_stream = std::stringstream{command};
auto command_name = std::string{};
auto argument = std::string{};
auto arguments = std::vector<std::string>{};
// get command name
std::getline(command_stream, command_name, ' ');
// get arguments
while (std::getline(command_stream, argument, ' ')) {
arguments.push_back(argument);
}
return {command_name, arguments};
}
std::queue<SimulationCommand> SimulationCommand::compile_commands(CommandsWithArguments& commands_with_arguments,
const CommandRegistry& command_registry) {
auto compiled_commands = std::queue<SimulationCommand>{};
for (auto& [command, arguments] : commands_with_arguments) {
compiled_commands.emplace(&command_registry.get_registered_command(command, arguments.size()), arguments);
}
return compiled_commands;
}

View File

@@ -0,0 +1,41 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
#include "command_registry.hpp"
#include <cstddef>
#include <memory>
#include <mutex>
#include <queue>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
using CmdArguments = std::vector<std::string>;
class SimulationCommand {
public:
SimulationCommand(const RegisteredCommandBase* registered_command_in, const CmdArguments& arguments_in);
bool execute() const;
static std::queue<SimulationCommand> parse_sim_commands(const std::string& value,
const CommandRegistry& command_registry);
private:
using RawCommands = std::vector<std::string>;
using CommandWithArguments = std::pair<std::string, CmdArguments>;
using CommandsWithArguments = std::vector<CommandWithArguments>;
static RawCommands convert_commands_string_to_vector(const std::string& commands_view);
static CommandsWithArguments split_into_commands_with_arguments(std::vector<std::string>& commands_vector);
static CommandWithArguments split_into_command_with_arguments(std::string& command);
static std::queue<SimulationCommand> compile_commands(CommandsWithArguments& commands_with_arguments,
const CommandRegistry& command_registry);
std::vector<std::string> arguments;
const RegisteredCommandBase* registered_command;
};

View File

@@ -0,0 +1,69 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
#include "command_registry.hpp"
#include "simulation_command.hpp"
#include <generated/types/board_support_common.hpp>
#include <generated/types/slac.hpp>
#include <optional>
#include <queue>
#include <string>
#include <utility>
#include <vector>
enum class SimState {
UNPLUGGED,
PLUGGED_IN,
CHARGING_REGULATED,
CHARGING_FIXED,
ERROR_E,
DIODE_FAIL,
ISO_POWER_READY,
ISO_CHARGING_REGULATED,
BCB_TOGGLE,
UNDEFINED,
};
enum class EnergyMode {
AC,
DC,
};
struct SimulationData {
SimulationData() = default;
SimState state{SimState::UNPLUGGED};
SimState last_state{SimState::UNDEFINED};
types::slac::State slac_state{types::slac::State::UNMATCHED};
std::optional<size_t> sleep_ticks_left{};
bool v2g_finished{false};
bool iso_stopped{false};
bool iso_charger_paused{false};
size_t evse_maxcurrent{0};
size_t max_current{0};
std::string payment{"ExternalPayment"};
EnergyMode energy_mode{EnergyMode::AC};
std::optional<std::string> modify_charging_session_cmds{std::nullopt};
bool iso_pwr_ready{false};
size_t bcb_toggles{0};
bool bcb_toggle_C{true};
types::board_support_common::Ampacity pp;
float rcd_current_ma{0.0f};
float pwm_duty_cycle{0.0f};
float last_pwm_duty_cycle{0.0f};
bool dc_power_on{false};
double battery_charge_wh{0};
double battery_capacity_wh{0};
types::board_support_common::Event actual_bsp_event{types::board_support_common::Event::Disconnected};
};

View File

@@ -0,0 +1,140 @@
description: >-
This module implements a Car simulator that can execute charging sessions using the car_simulator interface.
config:
connector_id:
description: Connector id of the evse manager to which this simulator is connected to
type: integer
auto_enable:
description: >-
Enable this simulation directly at start. Set to true for pure SIL configs, set to false for HIL.
type: boolean
default: false
auto_exec:
description: >-
Enable automatic execution of simulation commands at startup from auto_exec_commands config option.
type: boolean
default: false
auto_exec_infinite:
description: >-
If enabled the simulation commands executes infinitely.
type: boolean
default: false
auto_exec_commands:
description: >-
Simulation commands, e.g. sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug
type: string
default: ""
ac_nominal_voltage:
description: Nominal AC voltage between phase and neutral in Volt
type: number
default: 230
dc_max_current_limit:
description: Maximum current allowed by the EV
type: integer
default: 300
dc_max_power_limit:
description: Maximum power allowed by the EV
type: integer
default: 150000
dc_max_voltage_limit:
description: Maximum voltage allowed by the EV
type: integer
default: 900
dc_energy_capacity:
description: Energy capacity of the EV
type: integer
default: 60000
dc_target_current:
description: Target current requested by the EV
type: integer
default: 5
dc_target_voltage:
description: Target voltage requested by the EV
type: integer
default: 200
support_sae_j2847:
description: Supporting SAE J2847 ISO 2 bidi version
type: boolean
default: false
dc_discharge_max_current_limit:
description: Maximum discharge current allowed by the EV
type: integer
default: 300
dc_discharge_max_power_limit:
description: Maximum discharge power allowed by the EV
type: integer
default: 150000
dc_discharge_target_current:
description: Discharge target current requested by the EV
type: integer
default: 5
dc_discharge_v2g_minimal_soc:
description: Discharge minimal soc at which the evse should shutdown
type: integer
default: 20
max_current:
description: Ac max current in Ampere
type: number
default: 16
three_phases:
description: Support three phase
type: boolean
default: true
departure_time:
description: Departure time in seconds after the session starts
type: integer
default: 86400
e_amount:
description: Energy amount in Wh that should be charged during the session
type: integer
default: 0
soc:
description: SoC at start of a simulated charging process
type: integer
default: 30
keep_cross_boot_plugin_state:
description: Keep plugin state across boot (use for simulation only).
type: boolean
default: false
plugin_commands:
description: >-
Simulation commands for the EV on "plugin" sim command, e.g. sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug
type: string
default: ""
force_payment_option:
description: Force the use of the selected payment option
type: boolean
default: false
provides:
main:
interface: car_simulator
description: This implements the car simulator
ev_manager:
interface: ev_manager
description: Implementation of the ev manager to provide additional information such as detailed EV info
requires:
ev_board_support:
interface: ev_board_support
ev:
interface: ISO15118_ev
min_connections: 0
max_connections: 1
slac:
interface: ev_slac
min_connections: 0
max_connections: 1
powermeter:
interface: powermeter
min_connections: 0
max_connections: 1
kvs:
interface: kvs
min_connections: 0
max_connections: 1
enable_external_mqtt: true
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Cornelius Claussen
- Sebastian Lukas
- Tobias Marzell

View File

@@ -0,0 +1,37 @@
set(TEST_TARGET_NAME ${PROJECT_NAME}_EvManager_tests)
add_executable(${TEST_TARGET_NAME})
add_dependencies(${TEST_TARGET_NAME} ${MODULE_NAME})
get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR)
target_include_directories(${TEST_TARGET_NAME}
PRIVATE
"$<TARGET_PROPERTY:generate_cpp_files,EVEREST_GENERATED_INCLUDE_DIR>"
)
target_sources(${TEST_TARGET_NAME}
PRIVATE
CommandRegistryTest.cpp
SimCommandTest.cpp
../main/simulation_command.cpp
)
target_compile_definitions(${TEST_TARGET_NAME} PRIVATE
BUILD_TESTING_MODULE_EV_MANAGER
)
target_link_libraries(${TEST_TARGET_NAME}
PRIVATE
everest::framework
everest::log
Catch2::Catch2WithMain
)
if (NOT DISABLE_EDM)
list(APPEND CMAKE_MODULE_PATH ${CPM_PACKAGE_catch2_SOURCE_DIR}/extras)
include(Catch)
catch_discover_tests(${TEST_TARGET_NAME})
endif ()
add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME})
ev_register_test_target(${TEST_TARGET_NAME})

View File

@@ -0,0 +1,129 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "../main/command_registry.hpp"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <string>
#include <vector>
SCENARIO("Commands can be registered", "[RegisteredCommand]") {
GIVEN("A command with 0 arguments") {
auto command_registry = CommandRegistry();
const auto command_name = std::string{"test_command0"};
const auto argument_count = 0;
const auto test_comand_0_function = [](const std::vector<std::string>& arguments) { return arguments.empty(); };
WHEN("The command is registered") {
command_registry.register_command(command_name, argument_count, test_comand_0_function);
THEN("The command can be retrieved") {
const auto& registered_command = command_registry.get_registered_command(command_name, 0);
THEN("The command can be executed") {
CHECK(registered_command({}) == true);
}
THEN("The command throws when the number of arguments is invalid") {
CHECK(registered_command({}) == true);
CHECK_THROWS_WITH(registered_command({"arg1"}),
"Invalid number of arguments for: test_command0 expected 0 got 1");
CHECK_THROWS_WITH(registered_command({"arg1", "arg2"}),
"Invalid number of arguments for: test_command0 expected 0 got 2");
CHECK_THROWS_WITH(registered_command({"arg1", "arg2", "arg3"}),
"Invalid number of arguments for: test_command0 expected 0 got 3");
}
}
}
}
GIVEN("A command with 1 argument") {
auto command_registry = CommandRegistry();
const auto command_name = std::string{"test_command1"};
const auto argument_count = 1;
const auto test_comand_1_function = [](const std::vector<std::string>& arguments) {
return arguments.size() == 1;
};
WHEN("The command is registered") {
command_registry.register_command(command_name, argument_count, test_comand_1_function);
THEN("The command can be retrieved") {
const auto& registered_command = command_registry.get_registered_command(command_name, 1);
THEN("The command can be executed") {
CHECK(registered_command({"arg1"}) == true);
}
THEN("The command throws when the number of arguments is invalid") {
CHECK_THROWS_WITH(registered_command({}),
"Invalid number of arguments for: test_command1 expected 1 got 0");
CHECK_THROWS_WITH(registered_command({"arg1", "arg2"}),
"Invalid number of arguments for: test_command1 expected 1 got 2");
}
}
}
}
GIVEN("A command with 2 arguments") {
auto command_registry = CommandRegistry();
const auto command_name = std::string{"test_command2"};
const auto argument_count = 2;
const auto test_comand_2_function = [](const std::vector<std::string>& arguments) {
return arguments.size() == 2;
};
WHEN("The command is registered") {
command_registry.register_command(command_name, argument_count, test_comand_2_function);
THEN("The command can be retrieved") {
const auto& registered_command = command_registry.get_registered_command(command_name, 2);
THEN("The command can be executed") {
CHECK(registered_command({"arg1", "arg2"}) == true);
}
THEN("The command throws when the number of arguments is invalid") {
CHECK_THROWS_WITH(registered_command({}),
"Invalid number of arguments for: test_command2 expected 2 got 0");
CHECK_THROWS_WITH(registered_command({"arg1"}),
"Invalid number of arguments for: test_command2 expected 2 got 1");
CHECK_THROWS_WITH(registered_command({"arg1", "arg2", "arg3"}),
"Invalid number of arguments for: test_command2 expected 2 got 3");
}
}
}
}
GIVEN("Two identical commands with different arguments") {
auto command_registry = CommandRegistry();
const std::string command_name = std::string{"test_command"};
const auto test_command_1_function = [](const std::vector<std::string>& arguments) {
return arguments.size() == 1;
};
const auto test_command_2_function = [](const std::vector<std::string>& arguments) {
return arguments.size() == 2;
};
WHEN("The command is registered") {
command_registry.register_command(command_name, 1, test_command_1_function);
command_registry.register_command(command_name, 2, test_command_2_function);
THEN("The command can be retrieved") {
const auto& registered_command1 = command_registry.get_registered_command(command_name, 1);
const auto& registered_command2 = command_registry.get_registered_command(command_name, 2);
THEN("The command can be executed") {
CHECK(registered_command1({"arg1"}) == true);
CHECK(registered_command2({"arg1", "arg2"}) == true);
}
THEN("The command throws when the number of arguments is invalid") {
CHECK_THROWS_WITH(registered_command1({}),
"Invalid number of arguments for: test_command expected 1 got 0");
CHECK_THROWS_WITH(registered_command1({"arg1", "arg2"}),
"Invalid number of arguments for: test_command expected 1 got 2");
}
}
THEN("The wrong commands can not be retrieved") {
CHECK_THROWS_WITH(command_registry.get_registered_command(command_name, 0),
"Command not found: test_command");
CHECK_THROWS_WITH(command_registry.get_registered_command(command_name, 4),
"Command not found: test_command");
}
}
}
}

View File

@@ -0,0 +1,118 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "../main/command_registry.hpp"
#include "../main/simulation_command.hpp"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <string>
#include <vector>
SCENARIO("SimCommands can be created", "[SimCommand]") {
GIVEN("A command with 0 arguments called test_command is registered") {
const auto command_name = std::string{"test_command"};
const auto argument_count = 0;
const auto test_command_function = [](const std::vector<std::string>& arguments) { return arguments.empty(); };
auto command_registry = CommandRegistry();
command_registry.register_command(command_name, argument_count, test_command_function);
WHEN("The SimCommand is created") {
const auto sim_command = SimulationCommand{&command_registry.get_registered_command(command_name, 0), {}};
THEN("The command can be executed") {
CHECK(sim_command.execute() == true);
}
}
WHEN("The SimCommand is created with the wrong number of arguments") {
const auto sim_command =
SimulationCommand{&command_registry.get_registered_command(command_name, 1), {"arg1"}};
THEN("The command throws") {
CHECK_THROWS_WITH(sim_command.execute(),
"Invalid number of arguments for: test_command expected 0 got 1");
}
}
}
}
SCENARIO("SimCommands can be parsed", "[SimCommand]") {
GIVEN("A few commands registered and a command string") {
auto command_registry = CommandRegistry();
const auto command_name_a = std::string{"commanda"};
const auto agrument_count_a = 0;
const auto command_function_a = [](const std::vector<std::string>& arguments) { return arguments.empty(); };
command_registry.register_command(command_name_a, agrument_count_a, command_function_a);
const auto command_name_b = std::string{"commandb"};
const auto argument_count_b = 1;
const auto command_function_b = [](const std::vector<std::string>& arguments) { return arguments.size() == 1; };
command_registry.register_command(command_name_b, argument_count_b, command_function_b);
const auto command_name_c = std::string{"commandc"};
const auto argument_count_c = 2;
const auto command_function_c = [](const std::vector<std::string>& arguments) { return arguments.size() == 2; };
command_registry.register_command(command_name_c, argument_count_c, command_function_c);
WHEN("A correct command string is to be parsed") {
const auto command_string = "commanda;commandb 0;commandc abc 0.0";
auto parsed_commands = SimulationCommand::parse_sim_commands(command_string, command_registry);
THEN("A queue of executable SimCommands exists.") {
CHECK(parsed_commands.front().execute());
parsed_commands.pop();
CHECK(parsed_commands.front().execute());
parsed_commands.pop();
CHECK(parsed_commands.front().execute());
parsed_commands.pop();
CHECK(parsed_commands.empty());
}
THEN("A queue of executable SimCommands exists.") {
CHECK(parsed_commands.front().execute());
parsed_commands.pop();
CHECK(parsed_commands.front().execute());
parsed_commands.pop();
CHECK(parsed_commands.front().execute());
parsed_commands.pop();
CHECK(parsed_commands.empty());
}
}
WHEN("A command string with wrong arguments is to be parsed") {
const auto command_string = "commanda 1;commandb;commandc abc 0.0 def";
auto parsed_commands = SimulationCommand::parse_sim_commands(command_string, command_registry);
THEN("The execution of the commands should fail.") {
CHECK_THROWS_WITH(parsed_commands.front().execute(),
"Invalid number of arguments for: commanda expected 0 got 1");
parsed_commands.pop();
CHECK_THROWS_WITH(parsed_commands.front().execute(),
"Invalid number of arguments for: commandb expected 1 got 0");
parsed_commands.pop();
CHECK_THROWS_WITH(parsed_commands.front().execute(),
"Invalid number of arguments for: commandc expected 2 got 3");
parsed_commands.pop();
CHECK(parsed_commands.empty());
}
}
WHEN("A command string with unregistered arguments is to be parsed") {
const auto command_string = "commandd;commande;commandf";
THEN("Parsing should fail") {
CHECK_THROWS_WITH(SimulationCommand::parse_sim_commands(command_string, command_registry),
"Command not found: commandd");
}
}
WHEN("An empty string is to be parsed") {
const auto command_string = "";
const auto parsed_commands = SimulationCommand::parse_sim_commands(command_string, command_registry);
THEN("It should create an empty queue") {
CHECK(parsed_commands.empty());
}
}
}
}

View File

@@ -0,0 +1,31 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
main/fsm_controller.cpp
)
target_link_libraries(${MODULE_NAME}
PRIVATE
slac::io
slac::fsm::ev
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/ev_slacImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "EvSlac.hpp"
namespace module {
void EvSlac::init() {
invoke_init(*p_main);
}
void EvSlac::ready() {
invoke_ready(*p_main);
}
} // namespace module

View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef EV_SLAC_HPP
#define EV_SLAC_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/ev_slac/Implementation.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {};
class EvSlac : public Everest::ModuleBase {
public:
EvSlac() = delete;
EvSlac(const ModuleInfo& info, std::unique_ptr<ev_slacImplBase> p_main, Conf& config) :
ModuleBase(info), p_main(std::move(p_main)), config(config){};
const std::unique_ptr<ev_slacImplBase> p_main;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // EV_SLAC_HPP

View File

@@ -0,0 +1,92 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "ev_slacImpl.hpp"
#include <fmt/core.h>
#include <everest/slac/io.hpp>
#include "fsm_controller.hpp"
static std::promise<void> module_ready;
// FIXME (aw): this is ugly, but due to the design of the auto-generated module skeleton ..
static std::unique_ptr<FSMController> fsm_ctrl{nullptr};
namespace module {
namespace main {
void ev_slacImpl::init() {
// setup evse fsm thread
std::thread(&ev_slacImpl::run, this).detach();
}
void ev_slacImpl::ready() {
module_ready.set_value();
}
void ev_slacImpl::run() {
// wait until ready
module_ready.get_future().get();
// initialize slac i/o
SlacIO slac_io;
try {
slac_io.init(config.device);
} catch (const std::exception& e) {
EVLOG_error << fmt::format("Couldn't open device {} for SLAC communication. Reason: {}", config.device,
e.what());
raise_error(
error_factory->create_error("generic/CommunicationFault", "", "Could not open device " + config.device));
return;
}
// setup callbacks
slac::fsm::ev::ContextCallbacks callbacks;
callbacks.send_raw_slac = [&slac_io](slac::messages::HomeplugMessage& msg) { slac_io.send(msg); };
callbacks.signal_state = [this](const std::string& value) {
if (value == "MATCHED") {
publish_dlink_ready(true);
} else if (value == "UNMATCHED") {
publish_dlink_ready(false);
}
try {
publish_state(types::slac::string_to_state(value));
} catch (const std::exception& e) {
EVLOG_error << fmt::format("Tried to publish unknown SLAC state '{}'. Error: {}", value, e.what());
}
};
callbacks.log_debug = [](const std::string& text) { EVLOG_debug << "EvSlac: " << text; };
callbacks.log_info = [](const std::string& text) { EVLOG_info << "EvSlac: " << text; };
callbacks.log_warn = [](const std::string& text) { EVLOG_warning << "EvSlac: " << text; };
callbacks.log_error = [](const std::string& text) { EVLOG_error << "EvSlac: " << text; };
const uint8_t* if_mac = slac_io.get_mac_addr();
std::array<uint8_t, ETH_ALEN> ev_host_mac;
std::copy(if_mac, if_mac + ETH_ALEN, ev_host_mac.begin());
auto fsm_ctx = slac::fsm::ev::Context(callbacks, ev_host_mac);
// fsm_ctx.slac_config.set_key_timeout_ms = config.set_key_timeout_ms;
// fsm_ctx.slac_config.ac_mode_five_percent = config.ac_mode_five_percent;
// fsm_ctx.slac_config.sounding_atten_adjustment = config.sounding_attenuation_adjustment;
// fsm_ctx.slac_config.generate_nmk();
fsm_ctrl = std::make_unique<FSMController>(fsm_ctx);
slac_io.run([](slac::messages::HomeplugMessage& msg) { fsm_ctrl->signal_new_slac_message(msg); });
fsm_ctrl->run();
}
void ev_slacImpl::handle_reset() {
fsm_ctrl->signal_reset();
}
bool ev_slacImpl::handle_trigger_matching() {
fsm_ctrl->signal_trigger_matching();
return true;
}
} // namespace main
} // namespace module

View File

@@ -0,0 +1,65 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef MAIN_EV_SLAC_IMPL_HPP
#define MAIN_EV_SLAC_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/ev_slac/Implementation.hpp>
#include "../EvSlac.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace main {
struct Conf {
std::string device;
int set_key_timeout_ms;
};
class ev_slacImpl : public ev_slacImplBase {
public:
ev_slacImpl() = delete;
ev_slacImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<EvSlac>& mod, Conf& config) :
ev_slacImplBase(ev, "main"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
// insert your public definitions here
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// command handler functions (virtual)
virtual void handle_reset() override;
virtual bool handle_trigger_matching() override;
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
void run();
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<EvSlac>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
// insert your private definitions here
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
// insert other definitions here
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
} // namespace main
} // namespace module
#endif // MAIN_EV_SLAC_IMPL_HPP

View File

@@ -0,0 +1,75 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include "fsm_controller.hpp"
#include <everest/slac/fsm/ev/states/others.hpp>
FSMController::FSMController(slac::fsm::ev::Context& context) : ctx(context){};
void FSMController::signal_new_slac_message(slac::messages::HomeplugMessage& msg) {
if (running == false) {
return;
}
{
const std::lock_guard<std::mutex> feed_lck(feed_mtx);
ctx.slac_message = msg;
fsm.handle_event(slac::fsm::ev::Event::SLAC_MESSAGE);
}
new_event = true;
new_event_cv.notify_all();
}
void FSMController::signal_reset() {
signal_simple_event(slac::fsm::ev::Event::RESET);
}
void FSMController::signal_trigger_matching() {
signal_simple_event(slac::fsm::ev::Event::TRIGGER_MATCHING);
}
bool FSMController::signal_simple_event(slac::fsm::ev::Event ev) {
const std::lock_guard<std::mutex> feed_lck(feed_mtx);
auto event_result = fsm.handle_event(ev);
new_event = true;
new_event_cv.notify_all();
return event_result == fsm::HandleEventResult::SUCCESS;
}
void FSMController::run() {
ctx.log_info("Starting the SLAC state machine");
fsm.reset<slac::fsm::ev::ResetState>(ctx);
std::unique_lock<std::mutex> feed_lck(feed_mtx);
running = true;
while (true) {
auto feed_result = fsm.feed();
if (feed_result.transition()) {
// call immediately again
continue;
} else if (feed_result.internal_error() || feed_result.unhandled_event()) {
// FIXME (aw): would need to log here!
} else if (feed_result.has_value() == true) {
const auto timeout = *feed_result;
if (timeout == 0) {
// call feed directly again
continue;
}
new_event_cv.wait_for(feed_lck, std::chrono::milliseconds(timeout), [this] { return new_event; });
} else {
// nothing happened, no return value -> wait for new event
new_event_cv.wait(feed_lck, [this] { return new_event; });
}
if (new_event) {
// we got a new event, reset it and let run feed again
new_event = false;
}
}
}

View File

@@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EVSE_SLAC_FSM_CONTROLLER_HPP
#define EVSE_SLAC_FSM_CONTROLLER_HPP
#include <everest/slac/fsm/ev/fsm.hpp>
#include <condition_variable>
#include <mutex>
class FSMController {
public:
explicit FSMController(slac::fsm::ev::Context& ctx);
void signal_new_slac_message(slac::messages::HomeplugMessage&);
void signal_reset();
void signal_trigger_matching();
void run();
private:
bool signal_simple_event(slac::fsm::ev::Event ev);
slac::fsm::ev::Context& ctx;
slac::fsm::ev::FSM fsm;
bool running{false};
std::mutex feed_mtx;
std::condition_variable new_event_cv;
bool new_event{false};
};
#endif // EVSE_SLAC_FSM_CONTROLLER_HPP

View File

@@ -0,0 +1,19 @@
description: Implementation of EV SLAC data link negotiation according to ISO15118-3.
provides:
main:
interface: ev_slac
description: SLAC interface implementation.
config:
device:
description: Ethernet device used for PLC.
type: string
default: eth1
set_key_timeout_ms:
description: Timeout for CM_SET_KEY.REQ. Default works for QCA7000/QCA7005/CG5317.
type: integer
default: 500
metadata:
base_license: https://directory.fsf.org/wiki/License:BSD-3-Clause-Clear
license: https://opensource.org/licenses/Apache-2.0
authors:
- aw@pionix.de

View File

@@ -0,0 +1,5 @@
load("//modules:module.bzl", "py_everest_module")
py_everest_module(
name = "PyEvJosev",
)

View File

@@ -0,0 +1,70 @@
description: >-
This module implements an DIN70121, ISO15118-2 and ISO15118-20 EV using the Josev project.
config:
device:
description: >-
Ethernet device used for HLC. Any local interface that has an ipv6 link-local and a MAC addr will work.
type: string
default: eth0
supported_DIN70121:
description: The EV supports the DIN SPEC
type: boolean
default: false
supported_ISO15118_2:
description: The EV supports ISO15118-2
type: boolean
default: false
supported_ISO15118_20_AC:
description: The EV supports ISO15118-20 AC
type: boolean
default: false
supported_ISO15118_20_DC:
description: The EV supports ISO15118-20 DC
type: boolean
default: false
tls_active:
description: If true, EVCC connects to SECC as TLS client
type: boolean
default: false
enforce_tls:
description: The EVCC will enforce a TLS connection
type: boolean
default: false
is_cert_install_needed:
description: >-
If true, the contract certificate will be installed via the evse.
And any existing contract certificate will also be overwritten.
type: boolean
default: false
enable_tls_1_3:
description: The EVCC will enable TLS version 1.3
type: boolean
default: false
is_internet_service_needed:
description: If true, the ev will ask for internet service
type: boolean
default: false
request_all_service_details:
description: If true, the ev will ask for details about all offered services
type: boolean
default: false
select_all_vas_services:
description: If true, the ev will select all offered services
type: boolean
default: false
supported_d20_energy_services:
description: >-
The supported ISO15118-20 energy services (DC, DC_BPT, AC, AC_BPT) the EV supports,
provided as a prioritized list. The first entry in the list has the highest priority
and is the most likely to be selected. The services should be separated only with commas.
type: string
default: "DC,DC_BPT"
provides:
ev:
interface: ISO15118_ev
description: This module implements the ISO15118-2 implementation of an EV
enable_external_mqtt: true
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Sebastian Lukas

View File

@@ -0,0 +1,156 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
import asyncio
import sys
from pathlib import Path
import threading
import math
from everest.framework import Module, RuntimeSession, log
# fmt: off
JOSEV_WORK_DIR = Path(__file__).parent / '../../3rd_party/josev'
sys.path.append(JOSEV_WORK_DIR.as_posix())
from iso15118.evcc import EVCCHandler
from iso15118.evcc.controller.simulator import SimEVController
from iso15118.evcc.evcc_config import EVCCConfig
from iso15118.evcc.everest import context as JOSEV_CONTEXT
from iso15118.shared.exificient_exi_codec import ExificientEXICodec
from iso15118.shared.settings import set_PKI_PATH, enable_tls_1_3
from utilities import (
setup_everest_logging,
determine_network_interface,
patch_josev_config
)
setup_everest_logging()
EVEREST_CERTS_SUB_DIR = 'certs'
async def evcc_handler_main_loop(module_config: dict, exi_codec: ExificientEXICodec):
"""
Entrypoint function that starts the ISO 15118 code running on
the EVCC (EV Communication Controller)
"""
iface = determine_network_interface(module_config['device'])
evcc_config = EVCCConfig()
patch_josev_config(evcc_config, module_config)
await EVCCHandler(
evcc_config=evcc_config,
iface=iface,
exi_codec=exi_codec,
ev_controller=SimEVController(evcc_config),
).start()
class PyEVJosevModule():
def __init__(self) -> None:
self._es = JOSEV_CONTEXT.ev_state
self._session = RuntimeSession()
m = Module(self._session)
log.update_process_name(m.info.id)
self._setup = m.say_hello()
etc_certs_path = m.info.paths.etc / EVEREST_CERTS_SUB_DIR
set_PKI_PATH(str(etc_certs_path.resolve()))
if self._setup.configs.module['enable_tls_1_3']:
enable_tls_1_3()
self._es.internet_service_needed = self._setup.configs.module['is_internet_service_needed']
self._es.all_service_details = self._setup.configs.module['request_all_service_details']
self._es.all_vas_services = self._setup.configs.module['select_all_vas_services']
# setup publishing callback
def publish_callback(variable_name: str, value: any):
m.publish_variable('ev', variable_name, value)
# set publish callback for context
JOSEV_CONTEXT.set_publish_callback(publish_callback)
# setup handlers
for cmd in m.implementations['ev'].commands:
m.implement_command(
'ev', cmd, getattr(self, f'_handler_{cmd}'))
# init ready event
self._ready_event = threading.Event()
self._mod = m
self._mod.init_done(self._ready)
def start_evcc_handler(self):
exi_codec = ExificientEXICodec()
try:
while True:
self._ready_event.wait()
try:
asyncio.run(evcc_handler_main_loop(self._setup.configs.module, exi_codec))
self._mod.publish_variable('ev', 'v2g_session_finished', None)
except KeyboardInterrupt:
log.debug("SECC program terminated manually")
break
finally:
self._ready_event.clear()
finally:
exi_codec.shutdown()
def _ready(self):
log.debug("ready!")
# implementation handlers
def _handler_start_charging(self, args) -> bool:
self._es.DepartureTime = args['DepartureTime']
self._es.EAmount_kWh = args['EAmount']
self._es.EnergyTransferMode = args['EnergyTransferMode']
if "payment_option" in args['SelectedPaymentOption']:
self._es.PaymentOption = args['SelectedPaymentOption']['payment_option']
if "enforce_payment_option" in args['SelectedPaymentOption']:
self._es.enforce_payment_option = args['SelectedPaymentOption']['enforce_payment_option']
self._ready_event.set()
return True
def _handler_stop_charging(self, args):
self._es.StopCharging = True
def _handler_pause_charging(self, args):
self._es.Pause = True
def _handler_set_fault(self, args):
pass
def _handler_set_dc_params(self, args):
parameters = args['EvParameters']
self._es.dc_max_current_limit = parameters['max_current_limit']
self._es.dc_max_power_limit = parameters['max_power_limit']
self._es.dc_max_voltage_limit = parameters['max_voltage_limit']
self._es.dc_energy_capacity = parameters['energy_capacity']
self._es.dc_target_current = parameters['target_current']
self._es.dc_target_voltage = parameters['target_voltage']
def _handler_set_bpt_dc_params(self, args):
parameters = args['EvBPTParameters']
self._es.dc_discharge_max_current_limit = parameters["discharge_max_current_limit"]
self._es.dc_discharge_max_power_limit = parameters['discharge_max_power_limit']
self._es.dc_discharge_target_current = parameters['discharge_target_current']
self._es.minimal_soc = parameters["discharge_minimal_soc"]
def _handler_enable_sae_j2847_v2g_v2h(self, args):
self._es.SAEJ2847_V2H_V2G_Active = True
def _handler_update_soc(self, args):
self._es.actual_soc = math.floor(args['SoC'])
py_ev_josev = PyEVJosevModule()
py_ev_josev.start_evcc_handler()

View File

@@ -0,0 +1,107 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
import logging
import netifaces
from everest.framework import log
from iso15118.evcc.evcc_config import EVCCConfig
from iso15118.shared.utils import load_requested_protocols, load_requested_energy_services
class EverestPyLoggingHandler(logging.Handler):
def __init__(self):
logging.Handler.__init__(self)
def emit(self, record):
msg = self.format(record)
log_level: int = record.levelno
if log_level == logging.CRITICAL:
log.critical(msg)
elif log_level == logging.ERROR:
log.error(msg)
elif log_level == logging.WARNING:
log.warning(msg)
# FIXME (aw): implicitely pipe everything with loglevel INFO into DEBUG
else:
log.debug(msg)
def setup_everest_logging():
# remove all logging handler so that we'll have only our custom one
# FIXME (aw): this is probably bad practice because if everyone does that, only the last one might survive
logging.getLogger().handlers.clear()
handler = EverestPyLoggingHandler()
# NOTE (aw): the default formatting should be fine
# formatter = logging.Formatter("%(levelname)s - %(name)s (%(lineno)d): %(message)s")
# handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
def choose_first_ipv6_local() -> str:
for iface in netifaces.interfaces():
if netifaces.AF_INET6 in netifaces.ifaddresses(iface):
for netif_inet6 in netifaces.ifaddresses(iface)[netifaces.AF_INET6]:
if 'fe80' in netif_inet6['addr']:
return iface
log.warning('No necessary IPv6 link-local address was found!')
return 'eth0'
def determine_network_interface(preferred_interface: str) -> str:
if preferred_interface == "auto":
return choose_first_ipv6_local()
elif preferred_interface not in netifaces.interfaces():
log.warning(
f"The network interface {preferred_interface} was not found!")
return preferred_interface
def patch_josev_config(josev_config: EVCCConfig, everest_config: dict) -> None:
josev_config.use_tls = everest_config['tls_active']
josev_config.enforce_tls = everest_config['enforce_tls']
josev_config.is_cert_install_needed = everest_config['is_cert_install_needed']
josev_config.sdp_retry_cycles = 1
protocols = [
"DIN_SPEC_70121",
"ISO_15118_2",
"ISO_15118_20_AC",
"ISO_15118_20_DC",
]
if not everest_config['supported_DIN70121']:
protocols.remove('DIN_SPEC_70121')
if not everest_config['supported_ISO15118_2']:
protocols.remove('ISO_15118_2')
if not everest_config['supported_ISO15118_20_AC']:
protocols.remove('ISO_15118_20_AC')
if not everest_config['supported_ISO15118_20_DC']:
protocols.remove('ISO_15118_20_DC')
if not protocols:
log.error("The supporting hlc protocols were not specified")
josev_config.supported_protocols = load_requested_protocols(protocols)
if everest_config['supported_d20_energy_services']:
josev_config.supported_energy_services = load_requested_energy_services(
everest_config['supported_d20_energy_services'].split(',')
)
else:
josev_config.supported_energy_services = load_requested_energy_services(
['DC']
)

View File

@@ -0,0 +1 @@
EV side drivers and simulation modules