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:
@@ -0,0 +1,7 @@
|
||||
ev_add_module(DPM1000)
|
||||
ev_add_module(Huawei_R100040Gx)
|
||||
ev_add_module(Huawei_V100R023C10)
|
||||
ev_add_module(UUGreenPower_UR1000X0)
|
||||
ev_add_module(InfyPower)
|
||||
ev_add_module(InfyPower_BEG1K075G)
|
||||
ev_add_module(Winline)
|
||||
@@ -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
|
||||
# insert your custom targets and additional config variables here
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
main/can_broker.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(${MODULE_NAME}
|
||||
PRIVATE
|
||||
can_protocols::dpm1000
|
||||
everest::gpio
|
||||
)
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"main/power_supply_DCImpl.cpp"
|
||||
)
|
||||
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
# insert other things like install cmds etc here
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include "DPM1000.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
void DPM1000::init() {
|
||||
invoke_init(*p_main);
|
||||
}
|
||||
|
||||
void DPM1000::ready() {
|
||||
invoke_ready(*p_main);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef DPM1000_HPP
|
||||
#define DPM1000_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/power_supply_DC/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 {
|
||||
std::string device;
|
||||
int device_address;
|
||||
double power_limit_W;
|
||||
double current_limit_A;
|
||||
double voltage_limit_V;
|
||||
std::string series_parallel_mode;
|
||||
std::string discharge_gpio_chip;
|
||||
int discharge_gpio_line;
|
||||
bool discharge_gpio_polarity;
|
||||
bool debug_print_all_telemetry;
|
||||
};
|
||||
|
||||
class DPM1000 : public Everest::ModuleBase {
|
||||
public:
|
||||
DPM1000() = delete;
|
||||
DPM1000(const ModuleInfo& info, std::unique_ptr<power_supply_DCImplBase> p_main, Conf& config) :
|
||||
ModuleBase(info), p_main(std::move(p_main)), config(config){};
|
||||
|
||||
const std::unique_ptr<power_supply_DCImplBase> 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 // DPM1000_HPP
|
||||
@@ -0,0 +1,221 @@
|
||||
#include "can_broker.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <net/if.h>
|
||||
#include <poll.h>
|
||||
#include <sys/eventfd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace dpm1000 = can::protocol::dpm1000;
|
||||
|
||||
// FIXME (aw): this helper doesn't really belong here
|
||||
static void throw_with_error(const std::string& msg) {
|
||||
throw std::runtime_error(msg + ": (" + std::string(strerror(errno)) + ")");
|
||||
}
|
||||
|
||||
CanBroker::CanBroker(const std::string& interface_name, uint8_t _device_src) : device_src(_device_src) {
|
||||
can_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
|
||||
|
||||
if (can_fd == -1) {
|
||||
throw_with_error("Failed to open socket");
|
||||
}
|
||||
|
||||
// retrieve interface index from interface name
|
||||
struct ifreq ifr;
|
||||
|
||||
if (interface_name.size() >= sizeof(ifr.ifr_name)) {
|
||||
throw_with_error("Interface name too long: " + interface_name);
|
||||
} else {
|
||||
strcpy(ifr.ifr_name, interface_name.c_str());
|
||||
}
|
||||
|
||||
if (ioctl(can_fd, SIOCGIFINDEX, &ifr) == -1) {
|
||||
throw_with_error("Failed with ioctl/SIOCGIFINDEX on interface " + interface_name);
|
||||
}
|
||||
|
||||
// bind to the interface
|
||||
struct sockaddr_can addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.can_family = AF_CAN;
|
||||
addr.can_ifindex = ifr.ifr_ifindex;
|
||||
|
||||
if (bind(can_fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) == -1) {
|
||||
throw_with_error("Failed with bind");
|
||||
}
|
||||
|
||||
event_fd = eventfd(0, 0);
|
||||
|
||||
loop_thread = std::thread(&CanBroker::loop, this);
|
||||
}
|
||||
|
||||
CanBroker::~CanBroker() {
|
||||
uint64_t quit_value = 1;
|
||||
write(event_fd, &quit_value, sizeof(quit_value));
|
||||
|
||||
loop_thread.join();
|
||||
|
||||
close(can_fd);
|
||||
close(event_fd);
|
||||
}
|
||||
|
||||
void CanBroker::loop() {
|
||||
std::array<struct pollfd, 2> pollfds = {{
|
||||
{can_fd, POLLIN, 0},
|
||||
{event_fd, POLLIN, 0},
|
||||
}};
|
||||
|
||||
while (true) {
|
||||
const auto poll_result = poll(pollfds.data(), pollfds.size(), -1);
|
||||
|
||||
if (poll_result == 0) {
|
||||
// timeout
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pollfds[0].revents & POLLIN) {
|
||||
struct can_frame frame;
|
||||
read(can_fd, &frame, sizeof(frame));
|
||||
handle_can_input(frame);
|
||||
}
|
||||
|
||||
if (pollfds[1].revents & POLLIN) {
|
||||
uint64_t tmp;
|
||||
read(event_fd, &tmp, sizeof(tmp));
|
||||
// new event, for now, we do not care, later on we could check, if it is an exit event code
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CanBroker::set_state(bool enabled) {
|
||||
struct can_frame frame;
|
||||
dpm1000::power_on(frame, enabled, enabled);
|
||||
dpm1000::set_header(frame, monitor_id, device_src);
|
||||
|
||||
write_to_can(frame);
|
||||
|
||||
// Do an extra module ON command as sometimes the bits in the header are not enough to actually switch on
|
||||
set_data_int(dpm1000::def::SetValueType::SWITCH_ON_OFF_SETTING, (enabled ? 0 : 1));
|
||||
}
|
||||
|
||||
CanBroker::AccessReturnType CanBroker::dispatch_frame(const struct can_frame& frame, uint16_t id,
|
||||
uint32_t* return_payload) {
|
||||
// wait until we get access
|
||||
std::lock_guard<std::mutex> access_lock(access_mtx);
|
||||
|
||||
std::unique_lock<std::mutex> request_lock(request.mutex);
|
||||
write_to_can(frame);
|
||||
request.msg_type = id;
|
||||
request.state = CanRequest::State::ISSUED;
|
||||
|
||||
const auto finished = request.cv.wait_for(request_lock, ACCESS_TIMEOUT,
|
||||
[this]() { return request.state != CanRequest::State::ISSUED; });
|
||||
|
||||
if (not finished) {
|
||||
return AccessReturnType::TIMEOUT;
|
||||
}
|
||||
|
||||
if (request.state == CanRequest::State::FAILED) {
|
||||
return AccessReturnType::FAILED;
|
||||
}
|
||||
|
||||
// success
|
||||
if (return_payload) {
|
||||
memcpy(return_payload, request.response.data(), sizeof(std::remove_pointer_t<decltype(return_payload)>));
|
||||
}
|
||||
|
||||
return AccessReturnType::SUCCESS;
|
||||
}
|
||||
|
||||
CanBroker::AccessReturnType CanBroker::read_data(dpm1000::def::ReadValueType value_type, float& result) {
|
||||
const auto id = static_cast<std::underlying_type_t<decltype(value_type)>>(value_type);
|
||||
|
||||
struct can_frame frame;
|
||||
dpm1000::request_data(frame, value_type);
|
||||
dpm1000::set_header(frame, monitor_id, device_src);
|
||||
|
||||
uint32_t tmp;
|
||||
const auto status = dispatch_frame(frame, id, &tmp);
|
||||
|
||||
if (status == AccessReturnType::SUCCESS) {
|
||||
memcpy(&result, &tmp, sizeof(result));
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
CanBroker::AccessReturnType CanBroker::read_data_int(dpm1000::def::ReadValueType value_type, uint32_t& result) {
|
||||
const auto id = static_cast<std::underlying_type_t<decltype(value_type)>>(value_type);
|
||||
|
||||
struct can_frame frame;
|
||||
dpm1000::request_data(frame, value_type);
|
||||
dpm1000::set_header(frame, monitor_id, device_src);
|
||||
|
||||
uint32_t tmp;
|
||||
const auto status = dispatch_frame(frame, id, &tmp);
|
||||
|
||||
if (status == AccessReturnType::SUCCESS) {
|
||||
result = tmp;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
CanBroker::AccessReturnType CanBroker::set_data(dpm1000::def::SetValueType value_type, float payload) {
|
||||
const auto id = static_cast<std::underlying_type_t<decltype(value_type)>>(value_type);
|
||||
|
||||
uint8_t raw_payload[sizeof(payload)];
|
||||
memcpy(raw_payload, &payload, sizeof(payload));
|
||||
|
||||
struct can_frame frame;
|
||||
dpm1000::set_data(frame, value_type, {raw_payload[3], raw_payload[2], raw_payload[1], raw_payload[0]});
|
||||
dpm1000::set_header(frame, monitor_id, device_src);
|
||||
|
||||
return dispatch_frame(frame, id);
|
||||
}
|
||||
|
||||
CanBroker::AccessReturnType CanBroker::set_data_int(dpm1000::def::SetValueType value_type, uint32_t payload) {
|
||||
const auto id = static_cast<std::underlying_type_t<decltype(value_type)>>(value_type);
|
||||
|
||||
uint8_t raw_payload[sizeof(payload)];
|
||||
memcpy(raw_payload, &payload, sizeof(payload));
|
||||
|
||||
struct can_frame frame;
|
||||
dpm1000::set_data(frame, value_type, {raw_payload[3], raw_payload[2], raw_payload[1], raw_payload[0]});
|
||||
dpm1000::set_header(frame, monitor_id, device_src);
|
||||
|
||||
return dispatch_frame(frame, id);
|
||||
}
|
||||
|
||||
void CanBroker::write_to_can(const struct can_frame& frame) {
|
||||
write(can_fd, &frame, sizeof(frame));
|
||||
}
|
||||
|
||||
void CanBroker::handle_can_input(const struct can_frame& frame) {
|
||||
if (((frame.can_id >> dpm1000::def::MESSAGE_HEADER_BIT_SHIFT) & dpm1000::def::MESSAGE_HEADER_MASK) !=
|
||||
dpm1000::def::MESSAGE_HEADER) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> request_lock(request.mutex);
|
||||
if ((request.state != CanRequest::State::ISSUED) or (request.msg_type != dpm1000::parse_msg_type(frame))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dpm1000::is_error_flag_set(frame)) {
|
||||
request.state = CanRequest::State::FAILED;
|
||||
} else {
|
||||
// this is ugly
|
||||
for (auto i = 0; i < request.response.size(); ++i) {
|
||||
request.response[i] = frame.data[7 - i];
|
||||
}
|
||||
request.state = CanRequest::State::COMPLETED;
|
||||
}
|
||||
|
||||
request_lock.unlock();
|
||||
request.cv.notify_one();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef DPM1000_MAIN_DC_CAN_BROKER_HPP
|
||||
#define DPM1000_MAIN_DC_CAN_BROKER_HPP
|
||||
#include <array>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <everest/can/protocol/dpm1000.hpp>
|
||||
|
||||
struct CanRequest {
|
||||
enum class State {
|
||||
IDLE,
|
||||
ISSUED,
|
||||
COMPLETED,
|
||||
FAILED,
|
||||
} state{State::IDLE};
|
||||
|
||||
uint16_t msg_type;
|
||||
std::array<uint8_t, 4> response;
|
||||
std::condition_variable cv;
|
||||
std::mutex mutex;
|
||||
};
|
||||
|
||||
class CanBroker {
|
||||
public:
|
||||
enum class AccessReturnType {
|
||||
SUCCESS,
|
||||
FAILED,
|
||||
TIMEOUT,
|
||||
NOT_READY,
|
||||
};
|
||||
CanBroker(const std::string& interface_name, uint8_t _device_src);
|
||||
|
||||
AccessReturnType read_data(can::protocol::dpm1000::def::ReadValueType, float& result);
|
||||
AccessReturnType read_data_int(can::protocol::dpm1000::def::ReadValueType, uint32_t& result);
|
||||
|
||||
AccessReturnType set_data(can::protocol::dpm1000::def::SetValueType, float value);
|
||||
AccessReturnType set_data_int(can::protocol::dpm1000::def::SetValueType, uint32_t value);
|
||||
void set_state(bool enabled);
|
||||
|
||||
~CanBroker();
|
||||
|
||||
private:
|
||||
constexpr static auto ACCESS_TIMEOUT = std::chrono::milliseconds(250);
|
||||
void loop();
|
||||
|
||||
void write_to_can(const struct can_frame& frame);
|
||||
AccessReturnType dispatch_frame(const struct can_frame& frame, uint16_t id, uint32_t* result = nullptr);
|
||||
|
||||
void handle_can_input(const struct can_frame& frame);
|
||||
|
||||
uint8_t device_src;
|
||||
|
||||
std::mutex access_mtx;
|
||||
CanRequest request;
|
||||
|
||||
const uint8_t monitor_id{0xf0};
|
||||
|
||||
std::thread loop_thread;
|
||||
|
||||
int event_fd{-1};
|
||||
int can_fd{-1};
|
||||
};
|
||||
|
||||
#endif // DPM1000_MAIN_DC_CAN_BROKER_HPP
|
||||
@@ -0,0 +1,294 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include "power_supply_DCImpl.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "can_broker.hpp"
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <utils/formatter.hpp>
|
||||
|
||||
std::unique_ptr<CanBroker> can_broker;
|
||||
|
||||
namespace dpm1000 = can::protocol::dpm1000;
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
static void log_status_on_fail(const std::string& msg, CanBroker::AccessReturnType status) {
|
||||
using ReturnStatus = CanBroker::AccessReturnType;
|
||||
|
||||
std::string reason;
|
||||
switch (status) {
|
||||
case ReturnStatus::FAILED:
|
||||
reason = "failed";
|
||||
break;
|
||||
case ReturnStatus::NOT_READY:
|
||||
reason = "not ready";
|
||||
break;
|
||||
case ReturnStatus::TIMEOUT:
|
||||
reason = "timeout";
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
EVLOG_info << msg << " reason: (" << reason << ")";
|
||||
}
|
||||
|
||||
static std::string alarm_to_string(uint32_t alarm) {
|
||||
std::string alarmflags;
|
||||
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::FUSE_BURN_OUT))
|
||||
alarmflags += "[FUSE_BURN_OUT]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::PFC_DCDC_COMMUNICATION_ERROR))
|
||||
alarmflags += "[PFC_DCDC_COMMUNICATION_ERROR]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::UNBALANCED_BUS_VOLTAGE))
|
||||
alarmflags += "[UNBALANCED_BUS_VOLTAGE]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::BUS_OVER_VOLTAGE))
|
||||
alarmflags += "[BUS_OVER_VOLTAGE]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::BUS_ABNORMAL_VOLTAGE))
|
||||
alarmflags += "[BUS_ABNORMAL_VOLTAGE]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::PHASE_OVER_VOLTAGE))
|
||||
alarmflags += "[PHASE_OVER_VOLTAGE]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::ID_NUMBER_REPETITION))
|
||||
alarmflags += "[ID_NUMBER_REPETITION]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::BUS_UNDER_VOLTAGE))
|
||||
alarmflags += "[BUS_UNDER_VOLTAGE]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::PHASE_LOSS))
|
||||
alarmflags += "[PHASE_LOSS]";
|
||||
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::PHASE_UNDER_VOLTAGE))
|
||||
alarmflags += "[PHASE_UNDER_VOLTAGE]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::CAN_COMMUNICATION_FAULT))
|
||||
alarmflags += "[CAN_COMMUNICATION_FAULT]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::DCDC_UNEVEN_CURRENT_SHARING))
|
||||
alarmflags += "[DCDC_UNEVEN_CURRENT_SHARING]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::PFC_POWER_OFF))
|
||||
alarmflags += "[PFC_POWER_OFF]";
|
||||
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::FAN_FULL_SPEED))
|
||||
alarmflags += "[FAN_FULL_SPEED]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::POWER_LIMITING))
|
||||
alarmflags += "[POWER_LIMITING]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::DCDC_POWER_OFF))
|
||||
alarmflags += "[DCDC_POWER_OFF]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::TEMPERATURE_POWER_LIMITING))
|
||||
alarmflags += "[TEMPERATURE_POWER_LIMITING]";
|
||||
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::AC_POWER_LIMITING))
|
||||
alarmflags += "[AC_POWER_LIMITING]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::DCDC_EEPROM_FAULTS))
|
||||
alarmflags += "[DCDC_EEPROM_FAULTS]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::FAN_FAULTS))
|
||||
alarmflags += "[FAN_FAULTS]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::DCDC_SHORT_CIRCUIT))
|
||||
alarmflags += "[DCDC_SHORT_CIRCUIT]";
|
||||
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::PFC_EEPROM_FAULTS))
|
||||
alarmflags += "[PFC_EEPROM_FAULTS]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::DCDC_OVER_TEMPERATURE))
|
||||
alarmflags += "[DCDC_OVER_TEMPERATURE]";
|
||||
if (alarm & (1 << (int)dpm1000::def::Alarm::DCDC_OUTPUT_OVER_VOLTAGE))
|
||||
alarmflags += "[DCDC_OUTPUT_OVER_VOLTAGE]";
|
||||
|
||||
return alarmflags;
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::init() {
|
||||
current = 0;
|
||||
voltage = 300;
|
||||
|
||||
config_current_limit = mod->config.current_limit_A;
|
||||
config_voltage_limit = mod->config.voltage_limit_V;
|
||||
config_power_limit = mod->config.power_limit_W;
|
||||
|
||||
if (!mod->config.discharge_gpio_chip.empty()) {
|
||||
discharge_gpio.open(mod->config.discharge_gpio_chip, mod->config.discharge_gpio_line,
|
||||
!mod->config.discharge_gpio_polarity);
|
||||
discharge_gpio.set_output(false);
|
||||
}
|
||||
|
||||
can_broker = std::make_unique<CanBroker>(mod->config.device, mod->config.device_address);
|
||||
|
||||
// ensure the module is switched off
|
||||
can_broker->set_state(false);
|
||||
|
||||
// Configure module for series or parallel mode
|
||||
// 0 is automatic switching mode
|
||||
float series_parallel_mode = 0.;
|
||||
|
||||
if (mod->config.series_parallel_mode == "Series") {
|
||||
series_parallel_mode = 1050.;
|
||||
config_min_voltage_limit = 300.;
|
||||
parallel_mode = false;
|
||||
} else if (mod->config.series_parallel_mode == "Parallel") {
|
||||
series_parallel_mode = 520.;
|
||||
if (config_voltage_limit > 520) {
|
||||
config_voltage_limit = 520;
|
||||
}
|
||||
parallel_mode = true;
|
||||
}
|
||||
|
||||
// WTF: This really uses a float to set one of the three modes automatic, series or parallel.
|
||||
auto status = can_broker->set_data(dpm1000::def::SetValueType::SERIES_PARALLEL_MODE, series_parallel_mode);
|
||||
log_status_on_fail("Set current limit failed", status);
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::ready() {
|
||||
types::power_supply_DC::Capabilities caps;
|
||||
caps.bidirectional = false;
|
||||
caps.max_export_current_A = config_current_limit;
|
||||
caps.max_export_voltage_V = config_voltage_limit;
|
||||
caps.min_export_current_A = 0;
|
||||
caps.min_export_voltage_V = config_min_voltage_limit;
|
||||
caps.max_export_power_W = config_power_limit;
|
||||
caps.current_regulation_tolerance_A = 0.5;
|
||||
caps.peak_current_ripple_A = 1;
|
||||
caps.conversion_efficiency_export = 0.95;
|
||||
|
||||
publish_capabilities(caps);
|
||||
|
||||
while (true) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
|
||||
// Send voltage, current and power limits
|
||||
|
||||
auto status = can_broker->set_data(dpm1000::def::SetValueType::CURRENT_LIMIT, current);
|
||||
log_status_on_fail("Set current limit failed", status);
|
||||
|
||||
status = can_broker->set_data(dpm1000::def::SetValueType::DEFAULT_CURRENT_LIMIT, 1.0);
|
||||
log_status_on_fail("Set default current limit failed", status);
|
||||
|
||||
status = can_broker->set_data(dpm1000::def::SetValueType::VOLTAGE, voltage);
|
||||
log_status_on_fail("Set voltage failed", status);
|
||||
|
||||
status = can_broker->set_data(dpm1000::def::SetValueType::POWER_LIMIT, 1.0);
|
||||
log_status_on_fail("Set current limit failed", status);
|
||||
|
||||
// Read voltage and current
|
||||
float tmp;
|
||||
types::power_supply_DC::VoltageCurrent vc;
|
||||
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::VOLTAGE, tmp);
|
||||
log_status_on_fail("Read voltage failed", status);
|
||||
if (status != CanBroker::AccessReturnType::SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
vc.voltage_V = tmp;
|
||||
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::CURRENT, tmp);
|
||||
log_status_on_fail("Read current failed", status);
|
||||
if (status != CanBroker::AccessReturnType::SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Publish voltage and current var
|
||||
// Current scaling depends on series/parallel mode operation.
|
||||
vc.current_A = tmp;
|
||||
if (parallel_mode) {
|
||||
vc.current_A *= 2.;
|
||||
}
|
||||
publish_voltage_current(vc);
|
||||
|
||||
// read alarm flags
|
||||
uint32_t alarm = 0;
|
||||
status = can_broker->read_data_int(dpm1000::def::ReadValueType::ALARM, alarm);
|
||||
log_status_on_fail("Read alarm failed", status);
|
||||
if (status == CanBroker::AccessReturnType::SUCCESS) {
|
||||
if (last_alarm_flags != alarm) {
|
||||
auto alarmflags = alarm_to_string(alarm);
|
||||
if (alarmflags != "") {
|
||||
EVLOG_warning << "Alarm flags changed: " << alarmflags;
|
||||
} else {
|
||||
EVLOG_info << "All Alarm flags cleared.";
|
||||
}
|
||||
last_alarm_flags = alarm;
|
||||
}
|
||||
}
|
||||
|
||||
// Discharge output if it is higher then setpoint voltage.
|
||||
// Note that this has no timeout, so HW must be designed to sustain the worst case load (e.g. 1000V) continously
|
||||
if (vc.voltage_V > (voltage + 10)) {
|
||||
discharge_gpio.set(true);
|
||||
} else {
|
||||
discharge_gpio.set(false);
|
||||
}
|
||||
|
||||
if (mod->config.debug_print_all_telemetry) {
|
||||
// read additional meta data
|
||||
float current_real_part = 0, current_limit = 0, dcdc_temperature = 0, ac_voltage = 0, voltage_limit = 0,
|
||||
pfc0_voltage = 0, pfc1_voltage = 0, env_temperature = 0, ac_voltage_phase_a = 0,
|
||||
ac_voltage_phase_b = 0, ac_voltage_phase_c = 0, pfc_temperature = 0, power_limit = 0;
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::CURRENT_REAL_PART, current_real_part);
|
||||
log_status_on_fail("Read CURRENT_REAL_PART failed", status);
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::CURRENT_LIMIT, current_limit);
|
||||
log_status_on_fail("Read CURRENT_LIMIT failed", status);
|
||||
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::DCDC_TEMPERATURE, dcdc_temperature);
|
||||
log_status_on_fail("Read DCDC_TEMPERATURE failed", status);
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::AC_VOLTAGE, ac_voltage);
|
||||
log_status_on_fail("Read AC_VOLTAGE failed", status);
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::VOLTAGE_LIMIT, voltage_limit);
|
||||
log_status_on_fail("Read VOLTAGE_LIMIT failed", status);
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::PFC0_VOLTAGE, pfc0_voltage);
|
||||
log_status_on_fail("Read PFC0_VOLTAGE failed", status);
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::PFC1_VOLTAGE, pfc1_voltage);
|
||||
log_status_on_fail("Read PFC1_VOLTAGE failed", status);
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::ENV_TEMPERATURE, env_temperature);
|
||||
log_status_on_fail("Read ENV_TEMPERATURE failed", status);
|
||||
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::AC_VOLTAGE_PHASE_A, ac_voltage_phase_a);
|
||||
log_status_on_fail("Read AC_VOLTAGE_PHASE_A failed", status);
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::AC_VOLTAGE_PHASE_B, ac_voltage_phase_b);
|
||||
log_status_on_fail("Read AC_VOLTAGE_PHASE_B failed", status);
|
||||
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::AC_VOLTAGE_PHASE_C, ac_voltage_phase_c);
|
||||
log_status_on_fail("Read AC_VOLTAGE_PHASE_C failed", status);
|
||||
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::PFC_TEMPERATURE, pfc_temperature);
|
||||
log_status_on_fail("Read PFC_TEMPERATURE failed", status);
|
||||
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::POWER_LIMIT, power_limit);
|
||||
log_status_on_fail("Read POWER_LIMIT failed", status);
|
||||
|
||||
status = can_broker->read_data(dpm1000::def::ReadValueType::ENV_TEMPERATURE, env_temperature);
|
||||
log_status_on_fail("Read ENV_TEMPERATURE failed", status);
|
||||
|
||||
EVLOG_info << fmt::format(
|
||||
"set_voltage {} set_current {} vc.current_A {} vc.voltage_V {} current_real_part {} current_limit {} "
|
||||
"dcdc_temperature {} ac_voltage {} "
|
||||
"voltage_limit "
|
||||
"{} pfc0_voltage {} pfc1_voltage {} env_temperature {} ac_voltage_phase_a {} ac_voltage_phase_b {} "
|
||||
"ac_voltage_phase_c {} pfc_temperature {} power_limit {}",
|
||||
voltage, current, vc.current_A, vc.voltage_V, current_real_part, current_limit, dcdc_temperature,
|
||||
ac_voltage, voltage_limit, pfc0_voltage, pfc1_voltage, env_temperature, ac_voltage_phase_a,
|
||||
ac_voltage_phase_b, ac_voltage_phase_c, pfc_temperature, power_limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) {
|
||||
if (mode == types::power_supply_DC::Mode::Export) {
|
||||
can_broker->set_state(true);
|
||||
} else {
|
||||
can_broker->set_state(false);
|
||||
}
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double& current) {
|
||||
if (voltage <= config_voltage_limit && voltage >= config_min_voltage_limit && current <= config_current_limit) {
|
||||
this->voltage = voltage;
|
||||
this->current = current / 100.;
|
||||
} else {
|
||||
EVLOG_error << fmt::format("Out of range voltage/current settings ignored: {}V / {}A", voltage, current);
|
||||
}
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setImportVoltageCurrent(double& voltage, double& current) {
|
||||
// power supply is uni directional only
|
||||
}
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef MAIN_POWER_SUPPLY_DC_IMPL_HPP
|
||||
#define MAIN_POWER_SUPPLY_DC_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
|
||||
#include "../DPM1000.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
#include <atomic>
|
||||
|
||||
#include <everest/gpio/gpio.hpp>
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class power_supply_DCImpl : public power_supply_DCImplBase {
|
||||
public:
|
||||
power_supply_DCImpl() = delete;
|
||||
power_supply_DCImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<DPM1000>& mod, Conf& config) :
|
||||
power_supply_DCImplBase(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_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) override;
|
||||
virtual void handle_setExportVoltageCurrent(double& voltage, double& current) override;
|
||||
virtual void handle_setImportVoltageCurrent(double& voltage, double& current) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<DPM1000>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
std::atomic<float> voltage;
|
||||
std::atomic<float> current;
|
||||
std::atomic<uint32_t> last_alarm_flags{0};
|
||||
|
||||
float config_current_limit{0};
|
||||
float config_voltage_limit{0};
|
||||
float config_power_limit{0};
|
||||
float config_min_voltage_limit{50.};
|
||||
|
||||
Everest::Gpio discharge_gpio;
|
||||
bool parallel_mode{false};
|
||||
// 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_POWER_SUPPLY_DC_IMPL_HPP
|
||||
@@ -0,0 +1,61 @@
|
||||
description: DC Power Supply Driver
|
||||
provides:
|
||||
main:
|
||||
description: Power supply driver for DPM 1000-30 from SCU Power. Currently supports only one module.
|
||||
interface: power_supply_DC
|
||||
config:
|
||||
device:
|
||||
description: Interface name for can device
|
||||
type: string
|
||||
default: can0
|
||||
device_address:
|
||||
description: Device address (as selected on front LED panel)
|
||||
type: integer
|
||||
default: 0
|
||||
power_limit_W:
|
||||
description: Maximum Power Limit in Watt
|
||||
type: number
|
||||
maximum: 30000
|
||||
default: 30000
|
||||
current_limit_A:
|
||||
description: Maximum Current Limit in Ampere
|
||||
type: number
|
||||
maximum: 100
|
||||
default: 100
|
||||
voltage_limit_V:
|
||||
description: Maximum Voltage Limit in Volt. Will be limited by series parallel setting as well.
|
||||
type: number
|
||||
maximum: 1000
|
||||
default: 1000
|
||||
series_parallel_mode:
|
||||
description:
|
||||
Select series (300-1000V), parallel (50-500) or automatic switching mode (50-1000).
|
||||
This switches the internal configuration of one module and should not be confused with parallel operation of multiple modules.
|
||||
type: string
|
||||
enum:
|
||||
- Series
|
||||
- Parallel
|
||||
- Automatic
|
||||
default: Series
|
||||
discharge_gpio_chip:
|
||||
description: >-
|
||||
GPIO chip to use to switch external discharge load on and off. An empty string disables discharging.
|
||||
Note that the hardware load must be designed to allow permanent discharge from the highest voltage (e.g. 1000V)
|
||||
type: string
|
||||
default: ''
|
||||
discharge_gpio_line:
|
||||
description: GPIO line to use to switch discharge load
|
||||
type: integer
|
||||
default: 0
|
||||
discharge_gpio_polarity:
|
||||
description: GPIO polarity, false means active low, true means active high
|
||||
type: boolean
|
||||
default: true
|
||||
debug_print_all_telemetry:
|
||||
description: Read and print all telemetry from the power module. Helpful while debugging.
|
||||
type: boolean
|
||||
default: false
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- aw@pionix.de
|
||||
@@ -0,0 +1,32 @@
|
||||
#
|
||||
# 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
|
||||
# insert your custom targets and additional config variables here
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"can_driver_acdc/CanDevice.cpp"
|
||||
"can_driver_acdc/HwCanDevice.cpp"
|
||||
"can_driver_acdc/CanPackets.cpp"
|
||||
)
|
||||
|
||||
target_link_libraries(${MODULE_NAME}
|
||||
PRIVATE
|
||||
Pal::Sigslot
|
||||
)
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"main/power_supply_DCImpl.cpp"
|
||||
)
|
||||
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
# insert other things like install cmds etc here
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "Huawei_R100040Gx.hpp"
|
||||
#include <regex>
|
||||
|
||||
namespace module {
|
||||
|
||||
static std::vector<std::string> split_by_delimeters(const std::string& s, const std::string& delimeters) {
|
||||
std::regex re("[" + delimeters + "]");
|
||||
std::sregex_token_iterator first{s.begin(), s.end(), re, -1}, last;
|
||||
return {first, last};
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> parse_module_addresses(const std::string a) {
|
||||
std::vector<uint8_t> addresses;
|
||||
auto adr = split_by_delimeters(a, ",");
|
||||
for (const auto& ad : adr) {
|
||||
addresses.push_back(std::stoi(ad));
|
||||
}
|
||||
return addresses;
|
||||
}
|
||||
|
||||
void Huawei_R100040Gx::init() {
|
||||
invoke_init(*p_main);
|
||||
|
||||
// open DCDC CAN device
|
||||
acdc.open_device(config.can_device);
|
||||
|
||||
// configure module address
|
||||
if (not config.module_addresses.empty()) {
|
||||
auto module_addresses = parse_module_addresses(config.module_addresses);
|
||||
EVLOG_info << "Amount of modules: " << module_addresses.size();
|
||||
acdc.set_module_address(module_addresses);
|
||||
} else {
|
||||
acdc.set_module_autodetection();
|
||||
}
|
||||
}
|
||||
|
||||
void Huawei_R100040Gx::ready() {
|
||||
invoke_ready(*p_main);
|
||||
acdc.run();
|
||||
acdc.switch_on(false);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef HUAWEI_R100040GX_HPP
|
||||
#define HUAWEI_R100040GX_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
#include "can_driver_acdc/HwCanDevice.hpp"
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {
|
||||
std::string can_device;
|
||||
std::string module_addresses;
|
||||
int max_export_current_A;
|
||||
int max_export_power_W;
|
||||
};
|
||||
|
||||
class Huawei_R100040Gx : public Everest::ModuleBase {
|
||||
public:
|
||||
Huawei_R100040Gx() = delete;
|
||||
Huawei_R100040Gx(const ModuleInfo& info, std::unique_ptr<power_supply_DCImplBase> p_main, Conf& config) :
|
||||
ModuleBase(info), p_main(std::move(p_main)), config(config){};
|
||||
|
||||
const std::unique_ptr<power_supply_DCImplBase> p_main;
|
||||
const Conf& config;
|
||||
|
||||
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
|
||||
HwCanDevice acdc;
|
||||
// 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 // HUAWEI_R100040GX_HPP
|
||||
@@ -0,0 +1,157 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <net/if.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include <linux/can.h>
|
||||
#include <linux/can/raw.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include "CanDevice.hpp"
|
||||
|
||||
CanDevice::CanDevice() : exit_rx_thread{false} {
|
||||
can_fd = 0;
|
||||
}
|
||||
|
||||
CanDevice::~CanDevice() {
|
||||
close_device();
|
||||
}
|
||||
|
||||
void CanDevice::open_device(const std::string& dev) {
|
||||
can_device = dev;
|
||||
try_open_device_internal();
|
||||
// spawn read thread
|
||||
exit_rx_thread = false;
|
||||
rx_thread_handle = std::thread(&CanDevice::rx_thread, this);
|
||||
}
|
||||
|
||||
bool CanDevice::try_open_device_internal() {
|
||||
|
||||
if (is_open) {
|
||||
return false;
|
||||
}
|
||||
|
||||
is_open = false;
|
||||
if ((can_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) < 0) {
|
||||
return false;
|
||||
|
||||
} else {
|
||||
|
||||
// retrieve interface index from interface name
|
||||
struct ifreq ifr;
|
||||
strcpy(ifr.ifr_name, can_device.c_str());
|
||||
if (ioctl(can_fd, SIOCGIFINDEX, &ifr) < 0) {
|
||||
close(can_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// bind to the interface
|
||||
struct sockaddr_can addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.can_family = AF_CAN;
|
||||
addr.can_ifindex = ifr.ifr_ifindex;
|
||||
|
||||
if (bind(can_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
close(can_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// set non blocking
|
||||
int v = fcntl(can_fd, F_GETFD, 0);
|
||||
fcntl(can_fd, F_SETFD, v | O_NONBLOCK);
|
||||
|
||||
is_open = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool CanDevice::close_device() {
|
||||
is_open = false;
|
||||
if (can_fd != 0 && close(can_fd) == 0) {
|
||||
can_fd = 0;
|
||||
exit_rx_thread = true;
|
||||
rx_thread_handle.join();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void CanDevice::close_device_internal() {
|
||||
is_open = false;
|
||||
if (can_fd != 0 && close(can_fd) == 0) {
|
||||
can_fd = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void CanDevice::rx_thread() {
|
||||
can_frame frame;
|
||||
|
||||
while (!exit_rx_thread) {
|
||||
if (is_open) {
|
||||
/* Read non-blocking from CAN Bus.
|
||||
Note that an implementation with select/poll seems to be unreliable on socket CAN.
|
||||
Sometimes select() returns 0 even though there are bytes to read. If that happens, it will never return
|
||||
anything but 0 again. Sometimes it returns 1, but the subsequent read blocks (even in non blocking mode).
|
||||
Maybe this can be fixed by adjusting some buffering in ioctl, but no working solution has been found.
|
||||
*/
|
||||
size_t nbytes = read(can_fd, &frame, sizeof(struct can_frame));
|
||||
|
||||
if (nbytes <= 0) {
|
||||
// If there is nothing to read, give it some time before we try again.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
} else if (nbytes > 0) {
|
||||
// Received a new CAN packet...
|
||||
std::vector<uint8_t> payload;
|
||||
payload.assign(frame.data, frame.data + frame.can_dlc);
|
||||
rx_handler(frame.can_id, payload);
|
||||
}
|
||||
} else {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CanDevice::rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload) {
|
||||
std::cout << "CAN frame received" << std::endl;
|
||||
}
|
||||
|
||||
void CanDevice::connection_established() {
|
||||
std::cout << "Connection established" << std::endl;
|
||||
}
|
||||
|
||||
bool CanDevice::_tx(uint32_t can_id, const std::vector<uint8_t>& payload) {
|
||||
|
||||
try_open_device_internal();
|
||||
|
||||
if (not is_open) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct can_frame frame;
|
||||
if (payload.size() > sizeof(frame.data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
frame.can_id = can_id;
|
||||
frame.can_dlc = payload.size();
|
||||
memcpy(frame.data, payload.data(), payload.size());
|
||||
|
||||
if (write(can_fd, &frame, sizeof(can_frame)) != sizeof(can_frame)) {
|
||||
// On CAN, frames cannot be partially written. If we cannot write the packet, something is wrong with the bus.
|
||||
// Close device.
|
||||
close_device_internal();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef CAN_DEVICE_HPP
|
||||
#define CAN_DEVICE_HPP
|
||||
|
||||
#include "CanPackets.hpp"
|
||||
#include <atomic>
|
||||
#include <linux/can.h>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
class CanDevice {
|
||||
public:
|
||||
CanDevice();
|
||||
virtual ~CanDevice();
|
||||
void open_device(const std::string& dev);
|
||||
bool close_device();
|
||||
bool is_running() {
|
||||
return is_open;
|
||||
};
|
||||
|
||||
protected:
|
||||
virtual void rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload);
|
||||
virtual void connection_established();
|
||||
bool _tx(uint32_t can_id, const std::vector<uint8_t>& payload);
|
||||
|
||||
private:
|
||||
bool try_open_device_internal();
|
||||
void close_device_internal();
|
||||
bool is_open{false};
|
||||
std::string can_device;
|
||||
int can_fd;
|
||||
std::atomic_bool exit_rx_thread;
|
||||
std::thread rx_thread_handle;
|
||||
void rx_thread();
|
||||
};
|
||||
|
||||
#endif // CAN_DEVICE_HPP
|
||||
@@ -0,0 +1,259 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "CanPackets.hpp"
|
||||
#include "Conversions.hpp"
|
||||
#include <iostream>
|
||||
|
||||
namespace Huawei {
|
||||
|
||||
static std::string to_string(MessageId m) {
|
||||
switch (m) {
|
||||
|
||||
case MessageId::Invalid:
|
||||
return "Invalid";
|
||||
case MessageId::ControlCommand:
|
||||
return "ControlCommand";
|
||||
case MessageId::ConfigurationCommand:
|
||||
return "ConfigurationCommand";
|
||||
case MessageId::QueryCommand:
|
||||
return "QueryCommand";
|
||||
case MessageId::ModuleReport:
|
||||
return "ModuleReport";
|
||||
case MessageId::QueryAllRealtimeData:
|
||||
return "QueryAllRealtimeData";
|
||||
case MessageId::QueryInherentModuleInformation:
|
||||
return "QueryInherentModuleInformation";
|
||||
case MessageId::ReceiveELabelFromMonitoringUnit:
|
||||
return "ReceiveELabelFromMonitoringUnit";
|
||||
case MessageId::ReturnELabelToMonitoringUnit:
|
||||
return "ReturnELabelToMonitoringUnit";
|
||||
case MessageId::OnlineLoadingStartup1:
|
||||
return "OnlineLoadingStartup1";
|
||||
case MessageId::OnlineLoadingStartup2:
|
||||
return "OnlineLoadingStartup2";
|
||||
case MessageId::FrameTransmission1:
|
||||
return "FrameTransmission1";
|
||||
case MessageId::FrameTransmission2:
|
||||
return "FrameTransmission2";
|
||||
case MessageId::FrameConfirmation1:
|
||||
return "FrameConfirmation1";
|
||||
case MessageId::FrameConfirmation2:
|
||||
return "FrameConfirmation2";
|
||||
case MessageId::DownloadEnd1:
|
||||
return "DownloadEnd1";
|
||||
case MessageId::DownloadEnd2:
|
||||
return "DownloadEnd2";
|
||||
case MessageId::LoadingFailure1:
|
||||
return "LoadingFailure1";
|
||||
case MessageId::LoadingFailure2:
|
||||
return "LoadingFailure2";
|
||||
case MessageId::CommonBlackBoxDataQuery:
|
||||
return "CommonBlackBoxDataQuery";
|
||||
}
|
||||
return "InvalidEnum/" + std::to_string(static_cast<uint16_t>(m));
|
||||
}
|
||||
|
||||
static std::string to_string(SignalId id) {
|
||||
switch (id) {
|
||||
case SignalId::NotImplemented:
|
||||
return "NotImplemented";
|
||||
case SignalId::FeatureField:
|
||||
return "FeatureField";
|
||||
case SignalId::SerialNumber:
|
||||
return "SerialNumber";
|
||||
case SignalId::ProductionInformation1:
|
||||
return "ProductionInformation1";
|
||||
case SignalId::ProductionInformation2:
|
||||
return "ProductionInformation2";
|
||||
case SignalId::SwHwVersion:
|
||||
return "SwHwVersion";
|
||||
case SignalId::HardwareAddr:
|
||||
return "HardwareAddr";
|
||||
case SignalId::BatchQuery:
|
||||
return "BatchQuery";
|
||||
case SignalId::VoltageRange:
|
||||
return "VoltageRange";
|
||||
case SignalId::FlowLimitingRange:
|
||||
return "FlowLimitingRange";
|
||||
case SignalId::OutputVoltage:
|
||||
return "OutputVoltage";
|
||||
case SignalId::DefaultVoltage:
|
||||
return "DefaultVoltage";
|
||||
case SignalId::OutputOVPThreshold:
|
||||
return "OutputOVPThreshold";
|
||||
case SignalId::OutputCurrentLimit:
|
||||
return "OutputCurrentLimit";
|
||||
case SignalId::DefaultOutputCurrentLimit:
|
||||
return "DefaultOutputCurrentLimit";
|
||||
case SignalId::OutputPowerLimit:
|
||||
return "OutputPowerLimit";
|
||||
case SignalId::DefaultOutputPowerLimit:
|
||||
return "DefaultOutputPowerLimit";
|
||||
case SignalId::SetOnOffVoltageCurrent:
|
||||
return "SetOnOffVoltageCurrent";
|
||||
case SignalId::RunningTime:
|
||||
return "RunningTime";
|
||||
case SignalId::OutputCurrent:
|
||||
return "OutputCurrent";
|
||||
case SignalId::DefaultOutputCurrent:
|
||||
return "DefaultOutputCurrent";
|
||||
case SignalId::PFCOnOff:
|
||||
return "PFCOnOff";
|
||||
case SignalId::FanDutyCycle:
|
||||
return "FanDutyCycle";
|
||||
case SignalId::RealTimeClock:
|
||||
return "RealTimeClock";
|
||||
case SignalId::CanCommunicationTimeout:
|
||||
return "CanCommunicationTimeout";
|
||||
case SignalId::GroupSettingClearingCmd:
|
||||
return "GroupSettingClearingCmd";
|
||||
case SignalId::TemporaryGroupSettingClearingCmd:
|
||||
return "TemporaryGroupSettingClearingCmd";
|
||||
case SignalId::EmergencyPowerOffControl:
|
||||
return "EmergencyPowerOffControl";
|
||||
case SignalId::PowerOnOff:
|
||||
return "PowerOnOff";
|
||||
case SignalId::ClearOutputOVP:
|
||||
return "ClearOutputOVP";
|
||||
case SignalId::FullFanSpeed:
|
||||
return "FullFanSpeed";
|
||||
case SignalId::ModuleSearch:
|
||||
return "ModuleSearch";
|
||||
case SignalId::DCDCOnOff:
|
||||
return "DCDCOnOff";
|
||||
case SignalId::AllocateSwAddresses:
|
||||
return "AllocateSwAddresses";
|
||||
case SignalId::UnassociatedOutputOVP:
|
||||
return "UnassociatedOutputOVP";
|
||||
case SignalId::ClearShortCircuit:
|
||||
return "ClearShortCircuit";
|
||||
case SignalId::DisableFlowEqualization:
|
||||
return "DisableFlowEqualization";
|
||||
case SignalId::OutputSilentMode:
|
||||
return "OutputSilentMode";
|
||||
case SignalId::ClearRectifierOutput:
|
||||
return "ClearRectifierOutput";
|
||||
case SignalId::AutomaticOutputModeSwitch:
|
||||
return "AutomaticOutputModeSwitch";
|
||||
case SignalId::DefaultOutputMode:
|
||||
return "DefaultOutputMode";
|
||||
case SignalId::CanBaudRate:
|
||||
return "CanBaudRate";
|
||||
case SignalId::ACInputPower:
|
||||
return "ACInputPower";
|
||||
case SignalId::ACInputFrequency:
|
||||
return "ACInputFrequency";
|
||||
case SignalId::ACInputCurrent:
|
||||
return "ACInputCurrent";
|
||||
case SignalId::DCOutputPower:
|
||||
return "DCOutputPower";
|
||||
case SignalId::RealTimeEfficiency:
|
||||
return "RealTimeEfficiency";
|
||||
case SignalId::OutputVoltageQuery:
|
||||
return "OutputVoltageQuery";
|
||||
case SignalId::ACInputVoltageUV:
|
||||
return "ACInputVoltageUV";
|
||||
case SignalId::ACInputVoltageVW:
|
||||
return "ACInputVoltageVW";
|
||||
case SignalId::ACInputVoltageWU:
|
||||
return "ACInputVoltageWU";
|
||||
case SignalId::PFCTemperature:
|
||||
return "PFCTemperature";
|
||||
case SignalId::InternalTemperature:
|
||||
return "InternalTemperature";
|
||||
case SignalId::TemperatureAirIntake:
|
||||
return "TemperatureAirIntake";
|
||||
case SignalId::AlarmStatus:
|
||||
return "AlarmStatus";
|
||||
case SignalId::RatedModuleCurrent:
|
||||
return "RatedModuleCurrent";
|
||||
case SignalId::AlarmInformation:
|
||||
return "AlarmInformation";
|
||||
case SignalId::OutputCurrent1:
|
||||
return "OutputCurrent1";
|
||||
case SignalId::OutputCurrent2:
|
||||
return "OutputCurrent2";
|
||||
case SignalId::OutputVoltageCurrentStatus:
|
||||
return "OutputVoltageCurrentStatus";
|
||||
}
|
||||
return "InvalidEnum/" + std::to_string(static_cast<uint16_t>(id));
|
||||
}
|
||||
|
||||
static std::string to_string(ErrorType e) {
|
||||
switch (e) {
|
||||
|
||||
case ErrorType::Success:
|
||||
return "Success";
|
||||
case ErrorType::InvalidCommand:
|
||||
return "InvalidCommand";
|
||||
case ErrorType::AddressIdentificationInProgress:
|
||||
return "AddressIdentificationInProgress";
|
||||
}
|
||||
return "InvalidEnum/" + std::to_string(static_cast<uint16_t>(e));
|
||||
}
|
||||
|
||||
// encode the 29 bits ID field
|
||||
uint32_t encode_can_id(uint8_t module_address, MessageId message_id) {
|
||||
constexpr uint8_t PROTOCOL_ID = 0x0D;
|
||||
|
||||
uint32_t id = PROTOCOL_ID << 23; // constant protocol id bits 23..28 (6 bits)
|
||||
|
||||
uint32_t m_addr = module_address & 0x7F; // ensure it is only 7 bits long
|
||||
id |= (m_addr << 16); // Module address is bits 16..22 (7 bits long)
|
||||
|
||||
id |= (static_cast<std::underlying_type<MessageId>::type>(message_id)
|
||||
<< 8); // Command/Message id is bits 8..15 (8 bits long)
|
||||
|
||||
id |= (1 << 7); // Source is control unit (bit 7)
|
||||
|
||||
id |= (0x1F << 2); // Group ID: default is all bits 1 (no grouping). Bits 2..6 (5 bits long)
|
||||
|
||||
// Bit 1: Set to 0 to use hardware address
|
||||
// Bit 0: Set to 0 there is no further data frame
|
||||
return id;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& out, const Packet& self) {
|
||||
out << "Huawei " << (self.packet_source_control_unit ? "H->M" : "M->H")
|
||||
<< " Addr: " << std::to_string(self.module_address) << " MsgId: " << to_string(self.message_id)
|
||||
<< " ErrorType: " << to_string(self.error_type) << " SignalId: " << to_string(self.signal_id)
|
||||
<< " Byte2/3: " << std::to_string(self.byte2) << "/" << std::to_string(self.byte3)
|
||||
<< " Data: " << std::to_string(self.data) << (self.last_packet ? "" : " [+]");
|
||||
return out;
|
||||
}
|
||||
|
||||
// Packs the packet as 8 byte binary
|
||||
Packet::operator std::vector<uint8_t>() {
|
||||
std::vector<uint8_t> out;
|
||||
// 4 bits ErrorType and 12 bits Signal ID
|
||||
to_raw(static_cast<uint8_t>(((static_cast<uint8_t>(error_type) & 0x0F) << 4) |
|
||||
((static_cast<uint16_t>(signal_id) & 0x0FFF) >> 8)),
|
||||
out);
|
||||
to_raw(static_cast<uint8_t>(((static_cast<uint16_t>(signal_id) & 0x00FF))), out);
|
||||
|
||||
to_raw(byte2, out); // 1 byte
|
||||
to_raw(byte3, out); // 1 byte
|
||||
to_raw(data, out); // 4 bytes of actual data
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Constructor to initialize from received raw packet
|
||||
Packet::Packet(const uint32_t can_id, const std::vector<uint8_t>& raw) {
|
||||
module_address = module_address_from_can_id(can_id);
|
||||
message_id = message_id_from_can_id(can_id);
|
||||
group_address = group_address_from_can_id(can_id);
|
||||
packet_source_control_unit = packet_source_from_can_id(can_id);
|
||||
protocol_id = protocol_id_from_can_id(can_id);
|
||||
last_packet = last_packet_from_can_id(can_id);
|
||||
signal_id = static_cast<SignalId>(static_cast<uint16_t>(((from_raw<uint8_t>(raw, 0) & 0x0F) << 8)) |
|
||||
(from_raw<uint8_t>(raw, 1)));
|
||||
error_type = static_cast<ErrorType>(from_raw<uint8_t>(raw, 0) >> 4);
|
||||
|
||||
byte2 = from_raw<uint8_t>(raw, 2);
|
||||
byte3 = from_raw<uint8_t>(raw, 3);
|
||||
data = from_raw<uint32_t>(raw, 4);
|
||||
}
|
||||
|
||||
} // namespace Huawei
|
||||
@@ -0,0 +1,208 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef CAN_PACKETS_HPP
|
||||
#define CAN_PACKETS_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <linux/can.h>
|
||||
#include <ostream>
|
||||
#include <vector>
|
||||
|
||||
namespace Huawei {
|
||||
|
||||
constexpr uint32_t RTQ_RUNNING_TIME{1 << 0};
|
||||
constexpr uint32_t RTQ_AC_INPUT_POWER{1 << 1};
|
||||
constexpr uint32_t RTQ_AC_INPUT_FREQ{1 << 2};
|
||||
constexpr uint32_t RTQ_AC_INPUT_CURRENT{1 << 3};
|
||||
constexpr uint32_t RTQ_DC_OUTPUT_POWER{1 << 4};
|
||||
constexpr uint32_t RTQ_EFFICIENCY{1 << 5};
|
||||
constexpr uint32_t RTQ_OUTPUT_VOLTAGE{1 << 6};
|
||||
constexpr uint32_t RTQ_DC_OUTPUT_CURRENT_LIMIT{1 << 7};
|
||||
// Bit 8 seems to be a bug in the documentation (same as bit 9)
|
||||
constexpr uint32_t RTQ_AC_INPUT_VOLTAGE_UV{1 << 9};
|
||||
constexpr uint32_t RTQ_AC_INPUT_VOLTAGE_VW{1 << 10};
|
||||
constexpr uint32_t RTQ_AC_INPUT_VOLTAGE_WU{1 << 11};
|
||||
constexpr uint32_t RTQ_INTERNAL_TEMPERATURE{1 << 12};
|
||||
constexpr uint32_t RTQ_TEMPERATURE_AIR_INTAKE{1 << 13};
|
||||
constexpr uint32_t RTQ_OUTPUT_CURRENT_1{1 << 14};
|
||||
constexpr uint32_t RTQ_OUTPUT_CURRENT_2{1 << 15};
|
||||
constexpr uint32_t RTQ_ALARM_STATUS{1 << 16};
|
||||
constexpr uint32_t RTQ_DC_OUTPUT_VOLTAGE_SETPOINT{1 << 17};
|
||||
constexpr uint32_t RTQ_DC_OUTPUT_CURRENT_PERCENTAGE{1 << 18};
|
||||
constexpr uint32_t RTQ_DC_OUTPUT_CURRENT_SETPOINT{1 << 19};
|
||||
constexpr uint32_t RTQ_ALARM_INFORMATION{1 << 20};
|
||||
constexpr uint32_t RTQ_SETTING_QUERY{1 << 21};
|
||||
constexpr uint32_t RTQ_OUTPUT_VOLTAGE_CURRENT_STATUS{1 << 22};
|
||||
constexpr uint32_t RTQ_ACTUAL_OUTPUT_POWER_LIMITING{1 << 23};
|
||||
|
||||
enum class MessageId : uint8_t {
|
||||
Invalid = 0x00,
|
||||
ControlCommand = 0x80,
|
||||
ConfigurationCommand = 0x81,
|
||||
QueryCommand = 0x82,
|
||||
ModuleReport = 0x83,
|
||||
QueryAllRealtimeData = 0x40,
|
||||
QueryInherentModuleInformation = 0x50,
|
||||
ReceiveELabelFromMonitoringUnit = 0xD1,
|
||||
ReturnELabelToMonitoringUnit = 0xD2,
|
||||
OnlineLoadingStartup1 = 0xD3,
|
||||
OnlineLoadingStartup2 = 0xE3,
|
||||
FrameTransmission1 = 0xD4,
|
||||
FrameTransmission2 = 0xE4,
|
||||
FrameConfirmation1 = 0xD5,
|
||||
FrameConfirmation2 = 0xE5,
|
||||
DownloadEnd1 = 0xD6,
|
||||
DownloadEnd2 = 0xE6,
|
||||
LoadingFailure1 = 0xD7,
|
||||
LoadingFailure2 = 0xE7,
|
||||
CommonBlackBoxDataQuery = 0xDA,
|
||||
};
|
||||
|
||||
// encode the 29 bits ID field
|
||||
uint32_t encode_can_id(uint8_t module_address, MessageId message_id);
|
||||
|
||||
inline uint8_t module_address_from_can_id(const uint32_t id) {
|
||||
return (id >> 16) & 0x7F;
|
||||
};
|
||||
|
||||
inline uint8_t group_address_from_can_id(const uint32_t id) {
|
||||
return (id >> 2) & 0x1F;
|
||||
};
|
||||
|
||||
inline bool packet_source_from_can_id(const uint32_t id) {
|
||||
return ((id >> 7) & 0x01) == 0x01;
|
||||
};
|
||||
|
||||
inline bool last_packet_from_can_id(const uint32_t id) {
|
||||
return (id & 0x01) == 0x00;
|
||||
};
|
||||
|
||||
inline uint8_t protocol_id_from_can_id(const uint32_t id) {
|
||||
return ((id >> 23) & 0x3F);
|
||||
};
|
||||
|
||||
inline MessageId message_id_from_can_id(const uint32_t id) {
|
||||
const uint8_t m_id = (id >> 8) & 0xFF;
|
||||
return static_cast<MessageId>(m_id);
|
||||
};
|
||||
|
||||
enum class ErrorType : uint8_t {
|
||||
Success = 0x00,
|
||||
InvalidCommand = 0x02,
|
||||
AddressIdentificationInProgress = 0x03,
|
||||
};
|
||||
|
||||
enum class SignalId : uint16_t {
|
||||
NotImplemented = 0xFFFF,
|
||||
BatchQuery = 0x000,
|
||||
FeatureField = 0x001,
|
||||
SerialNumber = 0x002,
|
||||
ProductionInformation1 = 0x003,
|
||||
ProductionInformation2 = 0x004,
|
||||
SwHwVersion = 0x005,
|
||||
HardwareAddr = 0x006,
|
||||
VoltageRange = 0x009,
|
||||
FlowLimitingRange = 0x00A,
|
||||
OutputVoltage = 0x100,
|
||||
DefaultVoltage = 0x101,
|
||||
OutputOVPThreshold = 0x102,
|
||||
OutputCurrentLimit = 0x103,
|
||||
DefaultOutputCurrentLimit = 0x104,
|
||||
OutputPowerLimit = 0x105,
|
||||
DefaultOutputPowerLimit = 0x106,
|
||||
SetOnOffVoltageCurrent = 0x108,
|
||||
RunningTime = 0x10E,
|
||||
OutputCurrent = 0x10F,
|
||||
DefaultOutputCurrent = 0x110,
|
||||
PFCOnOff = 0x111,
|
||||
FanDutyCycle = 0x114,
|
||||
RealTimeClock = 0x117,
|
||||
CanCommunicationTimeout = 0x118,
|
||||
GroupSettingClearingCmd = 0x119,
|
||||
TemporaryGroupSettingClearingCmd = 0x11A,
|
||||
EmergencyPowerOffControl = 0x131,
|
||||
PowerOnOff = 0x132,
|
||||
ClearOutputOVP = 0x133,
|
||||
FullFanSpeed = 0x134,
|
||||
ModuleSearch = 0x135,
|
||||
DCDCOnOff = 0x136,
|
||||
AllocateSwAddresses = 0x13A,
|
||||
UnassociatedOutputOVP = 0x13C,
|
||||
ClearShortCircuit = 0x145,
|
||||
DisableFlowEqualization = 0x146,
|
||||
OutputSilentMode = 0x148,
|
||||
ClearRectifierOutput = 0x149,
|
||||
AutomaticOutputModeSwitch = 0x14A,
|
||||
DefaultOutputMode = 0x14C,
|
||||
CanBaudRate = 0x14D,
|
||||
ACInputPower = 0x170,
|
||||
ACInputFrequency = 0x171,
|
||||
ACInputCurrent = 0x172,
|
||||
DCOutputPower = 0x173,
|
||||
RealTimeEfficiency = 0x174,
|
||||
OutputVoltageQuery = 0x175,
|
||||
ACInputVoltageUV = 0x179,
|
||||
ACInputVoltageVW = 0x17A,
|
||||
ACInputVoltageWU = 0x17B,
|
||||
PFCTemperature = 0x17E,
|
||||
InternalTemperature = 0x17F,
|
||||
TemperatureAirIntake = 0x180,
|
||||
OutputCurrent1 = 0x181,
|
||||
OutputCurrent2 = 0x182,
|
||||
AlarmStatus = 0x183,
|
||||
RatedModuleCurrent = 0x188,
|
||||
AlarmInformation = 0x18F,
|
||||
OutputVoltageCurrentStatus = 0x191,
|
||||
};
|
||||
|
||||
// Addresses
|
||||
const uint8_t ADDR_BROADCAST = 0x00;
|
||||
|
||||
struct Packet {
|
||||
// Constructor for Packets to be sent to modules
|
||||
Packet(MessageId _message_id, SignalId _signal_id, uint8_t _byte2, uint8_t _byte3, uint32_t _data) :
|
||||
message_id(_message_id),
|
||||
signal_id(_signal_id),
|
||||
byte2(_byte2),
|
||||
byte3(_byte3),
|
||||
data(_data),
|
||||
packet_source_control_unit(true){};
|
||||
|
||||
// Constructor for simple requests to module
|
||||
Packet(MessageId _message_id, SignalId _signal_id) :
|
||||
message_id(_message_id), signal_id(_signal_id), packet_source_control_unit(true){};
|
||||
|
||||
// Constructor to initialize from received raw packet
|
||||
Packet(const uint32_t can_id, const std::vector<uint8_t>& raw);
|
||||
friend std::ostream& operator<<(std::ostream& out, const Packet& self);
|
||||
operator std::vector<uint8_t>();
|
||||
|
||||
void set_module_address(uint8_t address) {
|
||||
module_address = address;
|
||||
};
|
||||
|
||||
uint32_t get_can_id() {
|
||||
return encode_can_id(module_address, message_id);
|
||||
};
|
||||
|
||||
// Part of CAN ID field
|
||||
uint8_t protocol_id{0x0D};
|
||||
uint8_t module_address{0};
|
||||
uint8_t group_address{0};
|
||||
MessageId message_id{MessageId::Invalid};
|
||||
|
||||
// In CAN payload bytes
|
||||
ErrorType error_type{ErrorType::Success};
|
||||
uint8_t byte2{0};
|
||||
uint8_t byte3{0};
|
||||
uint32_t data{0};
|
||||
SignalId signal_id{SignalId::NotImplemented};
|
||||
bool packet_source_control_unit{false};
|
||||
|
||||
bool last_packet{true};
|
||||
};
|
||||
|
||||
} // namespace Huawei
|
||||
|
||||
#endif // CAN_PACKETS_HPP
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef CONVERSIONS_HPP
|
||||
#define CONVERSIONS_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <type_traits>
|
||||
|
||||
#include <endian.h>
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 1, T> from_raw(const std::vector<uint8_t> raw, int idx) {
|
||||
T ret = raw[idx];
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 2, T> from_raw(const std::vector<uint8_t> raw, int idx) {
|
||||
uint16_t tmp = be16toh(*((uint16_t*)((uint8_t*)raw.data() + idx)));
|
||||
T ret;
|
||||
memcpy(&ret, &tmp, 2);
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 4, T> from_raw(const std::vector<uint8_t> raw, int idx) {
|
||||
uint32_t tmp = be32toh(*((uint32_t*)((uint8_t*)raw.data() + idx)));
|
||||
T ret;
|
||||
memcpy(&ret, &tmp, 4);
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 8, T> from_raw(const std::vector<uint8_t> raw, int idx) {
|
||||
uint64_t tmp = be64toh(*((uint64_t*)((uint8_t*)raw.data() + idx)));
|
||||
T ret;
|
||||
memcpy(&ret, &tmp, 8);
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 1> to_raw(T src, std::vector<uint8_t>& dest) {
|
||||
uint8_t tmp;
|
||||
memcpy(&tmp, &src, 1);
|
||||
dest.push_back(tmp);
|
||||
}
|
||||
|
||||
// FIXME (aw): these conversions should be optimized!
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 2> to_raw(T src, std::vector<uint8_t>& dest) {
|
||||
uint16_t tmp;
|
||||
memcpy(&tmp, &src, 2);
|
||||
tmp = htobe16(tmp);
|
||||
uint8_t ret[2];
|
||||
memcpy(ret, &tmp, 2);
|
||||
dest.insert(dest.end(), {ret[0], ret[1]});
|
||||
}
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 4> to_raw(T src, std::vector<uint8_t>& dest) {
|
||||
uint32_t tmp;
|
||||
memcpy(&tmp, &src, 4);
|
||||
tmp = htobe32(tmp);
|
||||
uint8_t ret[4];
|
||||
memcpy(ret, &tmp, 4);
|
||||
dest.insert(dest.end(), {ret[0], ret[1], ret[2], ret[3]});
|
||||
}
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 8> to_raw(T src, std::vector<uint8_t>& dest) {
|
||||
uint64_t tmp;
|
||||
memcpy(&tmp, &src, 8);
|
||||
tmp = htobe64(tmp);
|
||||
uint8_t ret[8];
|
||||
memcpy(ret, &tmp, 8);
|
||||
dest.insert(dest.end(), {ret[0], ret[1], ret[2], ret[3], ret[4], ret[5], ret[6], ret[7]});
|
||||
}
|
||||
|
||||
#endif // CONVERSIONS_HPP
|
||||
@@ -0,0 +1,298 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "HwCanDevice.hpp"
|
||||
#include "CanPackets.hpp"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
HwCanDevice::~HwCanDevice() {
|
||||
exit_tx_thread = true;
|
||||
}
|
||||
|
||||
bool HwCanDevice::switch_on(bool on) {
|
||||
// actual switching on will be handled in tx thread
|
||||
requested_on = on;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HwCanDevice::switch_on_nolock(bool on) {
|
||||
// Power On/Off Packet
|
||||
Huawei::Packet power_on_off(Huawei::MessageId::ControlCommand, Huawei::SignalId::PowerOnOff);
|
||||
power_on_off.byte3 = not on;
|
||||
send_to_all_modules(power_on_off);
|
||||
is_on = on;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HwCanDevice::set_voltage_current(float voltage, float current) {
|
||||
requested_set_point_voltage = voltage;
|
||||
requested_set_point_current = current;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HwCanDevice::set_voltage_current_nolock(float voltage, float current) {
|
||||
Huawei::Packet set_voltage(Huawei::MessageId::ControlCommand, Huawei::SignalId::OutputVoltage);
|
||||
set_voltage.data = voltage * 1024;
|
||||
send_to_all_modules(set_voltage);
|
||||
|
||||
Huawei::Packet set_current(Huawei::MessageId::ControlCommand, Huawei::SignalId::OutputCurrent);
|
||||
set_current.data = current * 1024 / module_addresses.size();
|
||||
set_current.byte3 = module_addresses.size();
|
||||
send_to_all_modules(set_current);
|
||||
|
||||
set_point_voltage = voltage;
|
||||
set_point_current = current;
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string to_serial_number(uint8_t b2, uint8_t b3, uint8_t data) {
|
||||
return fmt::format("{}:{}:{}", b2, b3, data);
|
||||
}
|
||||
|
||||
static std::string to_version_string(uint8_t b2, uint8_t b3, uint32_t data) {
|
||||
uint16_t hw_version = b2 << 8 | b3;
|
||||
|
||||
int dcdc_versoion_hi = data >> 24;
|
||||
int dcdc_versoion_lo = data >> 16 & 0xFF;
|
||||
|
||||
int pfc_versoion_hi = data >> 8 & 0xFF;
|
||||
int pfc_versoion_lo = data & 0xFF;
|
||||
return fmt::format("HW Version: {} | DCDC: {}.{} | PFC: {}.{}", hw_version, dcdc_versoion_hi, dcdc_versoion_lo,
|
||||
pfc_versoion_hi, pfc_versoion_lo);
|
||||
}
|
||||
|
||||
static bool address_already_in_list(std::vector<uint8_t>& v, uint8_t nv) {
|
||||
auto it = std::find(v.begin(), v.end(), nv);
|
||||
if (it == v.end()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void HwCanDevice::connection_established() {
|
||||
switch_on(false);
|
||||
request_module_info();
|
||||
}
|
||||
|
||||
void HwCanDevice::rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload) {
|
||||
// We only use extended frames here
|
||||
if (!(can_id & CAN_EFF_FLAG)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// parse packet
|
||||
auto packet = Huawei::Packet(can_id, payload);
|
||||
|
||||
if (packet.packet_source_control_unit or packet.protocol_id not_eq 0x0D) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet.error_type not_eq Huawei::ErrorType::Success) {
|
||||
std::cout << "Error in CAN packet: " << packet << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// We received a packet from the PSU, reset timeout timer
|
||||
last_communication_received = std::chrono::steady_clock::now();
|
||||
|
||||
uint8_t source_address = packet.module_address;
|
||||
bool packet_handled = true;
|
||||
|
||||
if (packet.message_id == Huawei::MessageId::QueryInherentModuleInformation) {
|
||||
switch (packet.signal_id) {
|
||||
case Huawei::SignalId::SerialNumber:
|
||||
if (module_auto_detection) {
|
||||
module_serial_numbers[packet.module_address] =
|
||||
"[auto detected] " + to_serial_number(packet.byte2, packet.byte3, packet.data);
|
||||
|
||||
if (not address_already_in_list(module_addresses, source_address)) {
|
||||
// Found a new module? Add it to the list
|
||||
module_addresses.push_back(source_address);
|
||||
// Update capabilities
|
||||
auto caps = get_capabilities();
|
||||
signal_capabilities(caps);
|
||||
}
|
||||
|
||||
} else {
|
||||
module_serial_numbers[packet.module_address] =
|
||||
to_serial_number(packet.byte2, packet.byte3, packet.data);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Huawei::SignalId::SwHwVersion:
|
||||
if (not address_already_in_list(module_addresses_reported, source_address)) {
|
||||
signal_serial_number(packet.module_address,
|
||||
module_serial_numbers[packet.module_address] + " | " +
|
||||
to_version_string(packet.byte2, packet.byte3, packet.data));
|
||||
module_addresses_reported.push_back(source_address);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Ignore all other Inherent Module Information packages
|
||||
packet_handled = true;
|
||||
break;
|
||||
}
|
||||
} else if (packet.message_id == Huawei::MessageId::QueryAllRealtimeData) {
|
||||
switch (packet.signal_id) {
|
||||
case Huawei::SignalId::OutputVoltageCurrentStatus: {
|
||||
const float v = (packet.data & 0x0000FFFF) / 10.;
|
||||
const float c = ((packet.data & 0xFFFF0000) >> 16) / 10.;
|
||||
|
||||
// report sum of all currents
|
||||
telemetries[source_address].current = c;
|
||||
|
||||
// calculate total current
|
||||
total_current = 0.;
|
||||
for (const auto& t : telemetries) {
|
||||
total_current += t.second.current;
|
||||
}
|
||||
|
||||
telemetries[source_address].voltage = v;
|
||||
// report average voltage
|
||||
float voltage = 0.;
|
||||
for (const auto& t : telemetries) {
|
||||
voltage += t.second.voltage;
|
||||
}
|
||||
voltage /= telemetries.size();
|
||||
|
||||
signal_voltage_current(voltage, total_current);
|
||||
} break;
|
||||
default:
|
||||
packet_handled = false;
|
||||
break;
|
||||
}
|
||||
} else if (packet.message_id == Huawei::MessageId::ConfigurationCommand) {
|
||||
// Ignore all config command replies for now
|
||||
packet_handled = true;
|
||||
} else if (packet.message_id == Huawei::MessageId::ControlCommand) {
|
||||
// Ignore all control command replies for now
|
||||
packet_handled = true;
|
||||
} else {
|
||||
packet_handled = false;
|
||||
}
|
||||
|
||||
if (not packet_handled) {
|
||||
std::cout << "UNHANDLED Packet received: " << packet << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
types::power_supply_DC::Capabilities HwCanDevice::get_capabilities() {
|
||||
|
||||
// IMPROVE ME: this could be queried from the power supply instead
|
||||
types::power_supply_DC::Capabilities caps;
|
||||
caps.bidirectional = false;
|
||||
|
||||
caps.current_regulation_tolerance_A = 1;
|
||||
caps.peak_current_ripple_A = 0.5;
|
||||
|
||||
caps.min_export_current_A = 0;
|
||||
caps.max_export_current_A = 133 * module_addresses.size();
|
||||
caps.min_export_voltage_V = 150;
|
||||
caps.max_export_voltage_V = 1000;
|
||||
caps.max_export_power_W = 40000 * module_addresses.size();
|
||||
|
||||
caps.max_import_current_A = 0;
|
||||
caps.min_import_current_A = 0;
|
||||
caps.max_import_power_W = 0;
|
||||
caps.min_import_voltage_V = 0;
|
||||
caps.max_import_voltage_V = 0;
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
void HwCanDevice::request_module_info() {
|
||||
// Request information about modules once
|
||||
send_to_broadcast({Huawei::MessageId::QueryInherentModuleInformation, Huawei::SignalId::BatchQuery});
|
||||
}
|
||||
|
||||
void HwCanDevice::set_mode() {
|
||||
// Set mode to automatic switching
|
||||
Huawei::Packet mode(Huawei::MessageId::ConfigurationCommand, Huawei::SignalId::AutomaticOutputModeSwitch);
|
||||
mode.byte3 = 0x01; // switch according to actual voltage on the terminals
|
||||
send_to_all_modules(mode);
|
||||
}
|
||||
|
||||
void HwCanDevice::tx_thread() {
|
||||
|
||||
Huawei::Packet query_rt_data(Huawei::MessageId::QueryAllRealtimeData, Huawei::SignalId::BatchQuery);
|
||||
query_rt_data.byte3 = 0xAA; // Enable bit configuration of individual messages
|
||||
query_rt_data.data = Huawei::RTQ_OUTPUT_VOLTAGE_CURRENT_STATUS;
|
||||
|
||||
uint8_t telemetry_cnt = 0;
|
||||
|
||||
bool comm_timeout_err = false;
|
||||
bool comm_bus_err = false;
|
||||
|
||||
bool last_err_present = false;
|
||||
|
||||
set_mode();
|
||||
|
||||
while (!exit_tx_thread) {
|
||||
{
|
||||
// Is there a communication problem in the CAN stack?
|
||||
comm_bus_err = not is_running();
|
||||
|
||||
// Did we receive something from the PSU within timeout?
|
||||
// I.e. the CAN stack is working but the hardware does not send anything
|
||||
|
||||
comm_timeout_err = std::chrono::steady_clock::now() - last_communication_received > COMMS_TIMEOUT;
|
||||
|
||||
bool err_present = comm_bus_err or comm_timeout_err;
|
||||
|
||||
if (err_present not_eq last_err_present) {
|
||||
signal_communication_error(err_present,
|
||||
(comm_bus_err ? "CAN Bus error: No such device or wrong settings"
|
||||
: "Power supply does not respond on CAN bus"));
|
||||
}
|
||||
last_err_present = err_present;
|
||||
|
||||
// Request all real time data (telemetry) as configured above
|
||||
if (telemetry_cnt++ % 5 == 0) {
|
||||
send_to_all_modules(query_rt_data);
|
||||
}
|
||||
|
||||
// Configure mode roughly every second
|
||||
if (not is_on and telemetry_cnt++ % 20 == 0) {
|
||||
set_mode();
|
||||
}
|
||||
|
||||
// Read serial numbers every few seconds
|
||||
if (telemetry_cnt++ % 40 == 0) {
|
||||
request_module_info();
|
||||
}
|
||||
|
||||
// Do we need to switch on or off?
|
||||
if (last_requested_on not_eq requested_on) {
|
||||
last_requested_on = requested_on;
|
||||
switch_on_nolock(requested_on);
|
||||
signal_on_off(requested_on);
|
||||
set_voltage_current_nolock(requested_set_point_voltage, requested_set_point_current);
|
||||
}
|
||||
|
||||
// Do we need to set voltage/current limits?
|
||||
if (requested_set_point_voltage not_eq set_point_voltage or
|
||||
requested_set_point_current not_eq set_point_current) {
|
||||
set_voltage_current_nolock(requested_set_point_voltage, requested_set_point_current);
|
||||
}
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
}
|
||||
}
|
||||
|
||||
bool HwCanDevice::tx(Huawei::Packet& packet) {
|
||||
uint32_t can_id = packet.get_can_id();
|
||||
can_id |= 0x80000000U; // Extended frame format
|
||||
return _tx(can_id, packet);
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& out, const HwCanDevice::Telemetry& self) {
|
||||
out << "DC output: " << std::to_string(self.voltage) << "V / " << std::to_string(self.current) << "A" << std::endl;
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef INFY_CAN_DEVICE_HPP
|
||||
#define INFY_CAN_DEVICE_HPP
|
||||
|
||||
#include "CanDevice.hpp"
|
||||
#include <atomic>
|
||||
#include <linux/can.h>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <sigslot/signal.hpp>
|
||||
#include <thread>
|
||||
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
|
||||
class HwCanDevice : public CanDevice {
|
||||
public:
|
||||
HwCanDevice(){
|
||||
|
||||
};
|
||||
|
||||
~HwCanDevice();
|
||||
|
||||
void run() {
|
||||
// spawn thread that requests some data periodically to keep the connection alive
|
||||
last_communication_received = std::chrono::steady_clock::now();
|
||||
exit_tx_thread = false;
|
||||
tx_thread_handle = std::thread(&HwCanDevice::tx_thread, this);
|
||||
};
|
||||
|
||||
void set_module_address(const std::vector<uint8_t>& _module_addresses) {
|
||||
module_addresses = _module_addresses;
|
||||
// Update capabilities
|
||||
auto caps = get_capabilities();
|
||||
signal_capabilities(caps);
|
||||
};
|
||||
|
||||
void set_module_autodetection() {
|
||||
module_auto_detection = true;
|
||||
};
|
||||
|
||||
// Commands
|
||||
bool switch_on(bool on);
|
||||
bool set_voltage_current(float voltage, float current);
|
||||
|
||||
int get_number_of_modules() {
|
||||
return module_addresses.size();
|
||||
}
|
||||
|
||||
void request_module_info();
|
||||
|
||||
// Data out
|
||||
sigslot::signal<float, float> signal_voltage_current;
|
||||
sigslot::signal<bool> signal_on_off; // true: on, false: off
|
||||
sigslot::signal<int, const std::string&> signal_serial_number;
|
||||
sigslot::signal<const types::power_supply_DC::Capabilities&> signal_capabilities;
|
||||
sigslot::signal<bool, const std::string&>
|
||||
signal_communication_error; // true for error, false means error was cleared, error description
|
||||
|
||||
struct Telemetry {
|
||||
float voltage{0.};
|
||||
float current{0.};
|
||||
};
|
||||
|
||||
float total_current{0.};
|
||||
|
||||
std::map<uint8_t, Telemetry> telemetries;
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& out, const Telemetry& self);
|
||||
|
||||
protected:
|
||||
virtual void rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload);
|
||||
virtual void connection_established();
|
||||
|
||||
private:
|
||||
std::atomic_bool exit_tx_thread;
|
||||
std::thread tx_thread_handle;
|
||||
void tx_thread();
|
||||
|
||||
void set_mode();
|
||||
|
||||
bool tx(Huawei::Packet& packet);
|
||||
|
||||
types::power_supply_DC::Capabilities get_capabilities();
|
||||
|
||||
void send_to_all_modules(Huawei::Packet packet) {
|
||||
// Send to each module
|
||||
for (auto module_address : module_addresses) {
|
||||
packet.set_module_address(module_address);
|
||||
tx(packet);
|
||||
}
|
||||
};
|
||||
|
||||
void send_to_broadcast(Huawei::Packet packet) {
|
||||
packet.set_module_address(Huawei::ADDR_BROADCAST);
|
||||
tx(packet);
|
||||
};
|
||||
|
||||
std::vector<uint8_t> module_addresses{};
|
||||
std::vector<uint8_t> module_addresses_reported{};
|
||||
|
||||
bool switch_on_nolock(bool on);
|
||||
bool set_voltage_current_nolock(float voltage, float current);
|
||||
|
||||
std::atomic_bool module_auto_detection{false};
|
||||
|
||||
float set_point_voltage{0.};
|
||||
float set_point_current{0.};
|
||||
|
||||
std::atomic<float> requested_set_point_voltage{0.};
|
||||
std::atomic<float> requested_set_point_current{0.};
|
||||
|
||||
bool is_on{false};
|
||||
std::atomic_bool requested_on{false};
|
||||
bool last_requested_on{false};
|
||||
|
||||
std::chrono::time_point<std::chrono::steady_clock> last_communication_received;
|
||||
static constexpr std::chrono::seconds COMMS_TIMEOUT{2};
|
||||
|
||||
std::unordered_map<uint8_t, std::string> module_serial_numbers;
|
||||
};
|
||||
|
||||
#endif // INFY_CAN_DEVICE_HPP
|
||||
@@ -0,0 +1,119 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "power_supply_DCImpl.hpp"
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
void power_supply_DCImpl::init() {
|
||||
|
||||
caps.bidirectional = false;
|
||||
|
||||
caps.current_regulation_tolerance_A = 1;
|
||||
caps.peak_current_ripple_A = 0.5;
|
||||
|
||||
caps.min_export_current_A = 0;
|
||||
caps.max_export_current_A = mod->config.max_export_current_A;
|
||||
caps.min_export_voltage_V = 150;
|
||||
caps.max_export_voltage_V = 1000;
|
||||
caps.max_export_power_W = mod->config.max_export_power_W;
|
||||
|
||||
caps.max_import_current_A = 0;
|
||||
caps.min_import_current_A = 0;
|
||||
caps.max_import_power_W = 0;
|
||||
caps.min_import_voltage_V = 0;
|
||||
caps.max_import_voltage_V = 0;
|
||||
|
||||
mod->acdc.signal_capabilities.connect([this](const types::power_supply_DC::Capabilities& c) {
|
||||
caps = c;
|
||||
// limit by config values
|
||||
if (caps.max_export_current_A > mod->config.max_export_current_A) {
|
||||
caps.max_export_current_A = mod->config.max_export_current_A;
|
||||
}
|
||||
if (caps.max_export_power_W > mod->config.max_export_power_W) {
|
||||
caps.max_export_power_W = mod->config.max_export_power_W;
|
||||
}
|
||||
publish_capabilities(caps);
|
||||
});
|
||||
|
||||
mod->acdc.signal_voltage_current.connect([this](float voltage, float current) {
|
||||
types::power_supply_DC::VoltageCurrent vc;
|
||||
vc.current_A = current;
|
||||
vc.voltage_V = voltage;
|
||||
publish_voltage_current(vc);
|
||||
});
|
||||
|
||||
mod->acdc.signal_serial_number.connect([this](int module_id, const std::string& serial_number) {
|
||||
EVLOG_info << "Module ID " << module_id << ": " << serial_number;
|
||||
});
|
||||
|
||||
mod->acdc.signal_on_off.connect([this](bool on) {
|
||||
if (on) {
|
||||
publish_mode(types::power_supply_DC::Mode::Import);
|
||||
} else {
|
||||
publish_mode(types::power_supply_DC::Mode::Off);
|
||||
}
|
||||
});
|
||||
|
||||
mod->acdc.signal_communication_error.connect([this](bool err, const std::string& desc) {
|
||||
if (err) {
|
||||
Everest::error::Error error_object = error_factory->create_error("power_supply_DC/CommunicationFault", "",
|
||||
desc, Everest::error::Severity::High);
|
||||
raise_error(error_object);
|
||||
comm_fault_error_raised = true;
|
||||
} else {
|
||||
if (comm_fault_error_raised) {
|
||||
clear_error("power_supply_DC/CommunicationFault");
|
||||
comm_fault_error_raised = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::ready() {
|
||||
mod->p_main->publish_capabilities(caps);
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) {
|
||||
std::scoped_lock lock(settings_mutex);
|
||||
|
||||
if (mode == types::power_supply_DC::Mode::Off) {
|
||||
mod->acdc.switch_on(false);
|
||||
} else if (mode == types::power_supply_DC::Mode::Export) {
|
||||
mod->acdc.switch_on(true);
|
||||
} else if (mode == types::power_supply_DC::Mode::Import) {
|
||||
mod->acdc.switch_on(false);
|
||||
} else if (mode == types::power_supply_DC::Mode::Fault) {
|
||||
mod->acdc.switch_on(false);
|
||||
}
|
||||
};
|
||||
|
||||
void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double& current) {
|
||||
|
||||
if (voltage > caps.max_export_voltage_V)
|
||||
voltage = caps.max_export_voltage_V;
|
||||
else if (voltage < caps.min_export_voltage_V)
|
||||
voltage = caps.min_export_voltage_V;
|
||||
|
||||
if (current > caps.max_export_current_A)
|
||||
current = caps.max_export_current_A;
|
||||
else if (current < caps.min_export_current_A)
|
||||
current = caps.min_export_current_A;
|
||||
|
||||
std::scoped_lock lock(settings_mutex);
|
||||
|
||||
export_voltage = voltage;
|
||||
export_current_limit = current;
|
||||
|
||||
EVLOG_info << "Updating voltage/current via CAN: " << export_voltage << "V / " << export_current_limit << "A";
|
||||
mod->acdc.set_voltage_current(export_voltage, export_current_limit);
|
||||
};
|
||||
|
||||
void power_supply_DCImpl::handle_setImportVoltageCurrent(double& voltage, double& current) {
|
||||
EVLOG_error << "Not implemented";
|
||||
}
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
@@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef MAIN_POWER_SUPPLY_DC_IMPL_HPP
|
||||
#define MAIN_POWER_SUPPLY_DC_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
|
||||
#include "../Huawei_R100040Gx.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 {};
|
||||
|
||||
class power_supply_DCImpl : public power_supply_DCImplBase {
|
||||
public:
|
||||
power_supply_DCImpl() = delete;
|
||||
power_supply_DCImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<Huawei_R100040Gx>& mod, Conf& config) :
|
||||
power_supply_DCImplBase(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_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) override;
|
||||
virtual void handle_setExportVoltageCurrent(double& voltage, double& current) override;
|
||||
virtual void handle_setImportVoltageCurrent(double& voltage, double& current) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<Huawei_R100040Gx>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
std::mutex settings_mutex;
|
||||
double export_voltage{0.};
|
||||
double export_current_limit{0.};
|
||||
double minImportVoltage{0.};
|
||||
double importCurrentLimit{0.};
|
||||
types::power_supply_DC::Capabilities caps;
|
||||
|
||||
types::power_supply_DC::Mode last_publish_mode{types::power_supply_DC::Mode::Off};
|
||||
bool comm_fault_error_raised{false};
|
||||
// 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_POWER_SUPPLY_DC_IMPL_HPP
|
||||
@@ -0,0 +1,42 @@
|
||||
description: >-
|
||||
Driver for Huawei_R100040Gx ACDC power supply. Supports multiple stacked modules.
|
||||
This module never exits. If communication is lost to the power supply, a CommunicationFault is raised.
|
||||
The error is cleared once communication is back up again.
|
||||
The Linux can device (e.g. can0) should ideally be present and correctly configured when the module is started,
|
||||
but it also works if the device comes up later. Once it was up once, it should not disappear again.
|
||||
The PSU may be appear or disappear on the CAN bus itself any time.
|
||||
config:
|
||||
can_device:
|
||||
description: CAN interface name
|
||||
type: string
|
||||
default: can0
|
||||
module_addresses:
|
||||
description: >-
|
||||
Module Addresses to use. Set to "" for auto detect (Uses all modules that reply to broadcast requests on this CAN bus).
|
||||
If multiple hardware addresses are listed (e.g. "120,121"), only those will be used. Current will be shared between them.
|
||||
type: string
|
||||
default: ""
|
||||
max_export_current_A:
|
||||
description: >-
|
||||
Maximum current that the PSU module can deliver. When using multiple PSUs in parallel, this refers to the total current capability of all modules.
|
||||
Set to 0 to use hardware defaults.
|
||||
type: integer
|
||||
default: 0
|
||||
max_export_power_W:
|
||||
description: >-
|
||||
Maximum power that the PSU module can deliver. When using multiple PSUs in parallel, this refers to the total power capability of all modules.
|
||||
Set to 0 to use hardware defaults.
|
||||
type: integer
|
||||
default: 0
|
||||
provides:
|
||||
main:
|
||||
description: Main interface
|
||||
interface: power_supply_DC
|
||||
config: {}
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Andreas Heinrich
|
||||
- Cornelius Claussen
|
||||
- Florin Mihut
|
||||
- Jan Christoph Habig
|
||||
@@ -0,0 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
## December 2025
|
||||
|
||||
### Module
|
||||
|
||||
- The module can now publish telemetry data on a specified mqtt base topic, set via the config option `telemetry_topic_prefix`. The concrete telemetry data is published only when the data changes to reduce mqtt traffic. The telemetry data is published as json objects per dispenser and per connector. See the module documentation for details.
|
||||
- The module now supports setting some specific BSP errors to the PSU as dispenser and connector alarms:
|
||||
- Per dispenser:
|
||||
- `evse_board_support/EnclosureOpen` set as Door status alarm to the PSU
|
||||
- `evse_board_support/WaterIngressDetected` set as Water alarm
|
||||
- `evse_board_support/MREC8EmergencyStop` set as EPO alarm
|
||||
- `evse_board_support/TiltDetected` set as Tilt alarm
|
||||
- Per connector:
|
||||
- `evse_board_support/MREC17EVSEContactorFault` set as DC output contactor fault
|
||||
|
||||
## June 2025
|
||||
|
||||
- Module
|
||||
- The module now verifies the HMAC of received goose messages by default (this was not the case before). This can be disabled with the module config `verify_secure_goose: false`
|
||||
- The modules' goose security options are now finer grained. `secure_goose` has been split into `send_secure_goose`, which controls the security of outgoing messages, `allow_insecure_goose` and `verify_secure_goose`, which control the security of incoming messages. See manifest.yaml for more details
|
||||
- Module allocation failure (including module allocation response timeout) is now treated as a warning instead of an error
|
||||
- Some info messages have been changed to debug messages to reduce log noise
|
||||
- Capabilities are now used from the Powersupply instead of hardcoded values. An error is raised as long as the capabilities are not set by the Powersupply. When the powersupply communication fails, the stored capabilities are cleared and the error is raised again.
|
||||
- The Ethernet socket now filters the received packages on kernel level to improve performance
|
||||
- Adds a hack to use voltage readings from a over voltage monitor during cable check. For this enough voltage monitors must be configured and the config option `HACK_use_ovm_while_cable_check` must be enabled.
|
||||
- Adds `upstream_voltage_source` config option to select which upstream voltage source to use.
|
||||
- Mock
|
||||
- The mock can now accept multiple connections to simulate multiple dispensers
|
||||
- The mock has new options to enable or disable the security of incoming and outgoing goose messages. These are accessible via the environment variables `FUSION_CHARGER_MOCK_DISABLE_SEND_HMAC` and `FUSION_CHARGER_MOCK_DISABLE_VERIFY_HMAC`
|
||||
- The mock has a new option to change the ethernet interface used for goose messages. This is accessible via the environment variable `FUSION_CHARGER_MOCK_ETH`
|
||||
- A bug that caused the mock to use the wrong hmac key has been fixed
|
||||
- The mock now waits 5 seconds before sending the capabilities to reflect the real hardware behavior better
|
||||
- The mock can now send received power requirements to a mqtt broker. For this, set hte environment variables `FUSION_CHARGER_MOCK_MQTT_HOST` and `FUSION_CHARGER_MOCK_MQTT_PORT` and optionally `FUSION_CHARGER_MOCK_MQTT_BASE_TOPIC` (defaults to `fusion_charger_mock/`). The mock then publishes under `{base_topic}/{global connector number}/power_request` a json object with `{"voltage": <voltage>, "current": <current>}`
|
||||
@@ -0,0 +1,55 @@
|
||||
#
|
||||
# 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
|
||||
# insert your custom targets and additional config variables here
|
||||
|
||||
target_link_libraries(${MODULE_NAME}
|
||||
PRIVATE
|
||||
fusion_charger_dispenser
|
||||
)
|
||||
|
||||
target_link_libraries(${MODULE_NAME}
|
||||
PRIVATE
|
||||
atomic
|
||||
)
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"connector_1/power_supply_DCImpl.cpp"
|
||||
"connector_2/power_supply_DCImpl.cpp"
|
||||
"connector_3/power_supply_DCImpl.cpp"
|
||||
"connector_4/power_supply_DCImpl.cpp"
|
||||
)
|
||||
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
target_sources(${MODULE_NAME} PRIVATE
|
||||
"connector_base/base.cpp"
|
||||
"telemetry_publisher_everest.cpp"
|
||||
)
|
||||
add_subdirectory(fusion_charger_lib)
|
||||
|
||||
ev_register_library_target(fusion_charger_dispenser)
|
||||
ev_register_library_target(goose-ethernet)
|
||||
ev_register_library_target(goose)
|
||||
ev_register_library_target(fusion_charger_modbus_extensions)
|
||||
ev_register_library_target(fusion_charger_modbus_driver)
|
||||
ev_register_library_target(fusion_charger_goose_driver)
|
||||
ev_register_library_target(modbus-base)
|
||||
ev_register_library_target(modbus-server)
|
||||
ev_register_library_target(modbus-client)
|
||||
ev_register_library_target(modbus-ssl)
|
||||
ev_register_library_target(modbus-registers)
|
||||
|
||||
option(INSTALL_FUSION_CHARGER_MOCK "Install fusion charger mock" OFF)
|
||||
if(INSTALL_FUSION_CHARGER_MOCK)
|
||||
install(TARGETS fusion_charger_mock)
|
||||
endif()
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
@@ -0,0 +1,327 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "Huawei_V100R023C10.hpp"
|
||||
#include "connector_1/power_supply_DCImpl.hpp"
|
||||
#include "connector_2/power_supply_DCImpl.hpp"
|
||||
#include "connector_3/power_supply_DCImpl.hpp"
|
||||
#include "connector_4/power_supply_DCImpl.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
static ConnectorBase* get_connector_impl(Huawei_V100R023C10* mod, std::uint8_t connector) {
|
||||
switch (connector) {
|
||||
case 0:
|
||||
return &(dynamic_cast<connector_1::power_supply_DCImpl*>(mod->p_connector_1.get()))->base;
|
||||
break;
|
||||
case 1:
|
||||
return &(dynamic_cast<connector_2::power_supply_DCImpl*>(mod->p_connector_2.get()))->base;
|
||||
break;
|
||||
case 2:
|
||||
return &(dynamic_cast<connector_3::power_supply_DCImpl*>(mod->p_connector_3.get()))->base;
|
||||
break;
|
||||
case 3:
|
||||
return &(dynamic_cast<connector_4::power_supply_DCImpl*>(mod->p_connector_4.get()))->base;
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("Connector number out of bounds (expected 0-3): " + std::to_string(connector));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static std::vector<ConnectorBase*> get_connector_bases(Huawei_V100R023C10* mod, std::uint8_t connectors_used) {
|
||||
std::vector<ConnectorBase*> connector_bases;
|
||||
for (std::uint8_t i = 0; i < connectors_used; i++) {
|
||||
connector_bases.push_back(get_connector_impl(mod, i));
|
||||
}
|
||||
return connector_bases;
|
||||
}
|
||||
|
||||
static std::string get_everest_error_for_dispenser_alarm(DispenserAlarms alarm) {
|
||||
switch (alarm) {
|
||||
case DispenserAlarms::DOOR_STATUS_ALARM:
|
||||
return "evse_board_support/EnclosureOpen";
|
||||
case DispenserAlarms::WATER_ALARM:
|
||||
return "evse_board_support/WaterIngressDetected";
|
||||
case DispenserAlarms::EPO_ALARM:
|
||||
return "evse_board_support/MREC8EmergencyStop";
|
||||
case DispenserAlarms::TILT_ALARM:
|
||||
return "evse_board_support/TiltDetected";
|
||||
}
|
||||
|
||||
throw std::runtime_error("Unknown DispenserAlarm enum value");
|
||||
}
|
||||
|
||||
void Huawei_V100R023C10::init() {
|
||||
this->communication_fault_raised = false;
|
||||
this->psu_not_running_raised = false;
|
||||
this->initial_hmac_acquired = false;
|
||||
|
||||
number_of_connectors_used = this->r_board_support.size();
|
||||
if (number_of_connectors_used > 4) {
|
||||
throw std::runtime_error("Got more board support modules than connectors supported");
|
||||
}
|
||||
|
||||
EVLOG_info << "Assuming number of connectors used = " << number_of_connectors_used
|
||||
<< " (based on number of connected board support modules)";
|
||||
|
||||
if (config.upstream_voltage_source == "IMD") {
|
||||
upstream_voltage_source = Huawei_V100R023C10::UpstreamVoltageSource::IMD;
|
||||
} else if (config.upstream_voltage_source == "OVM") {
|
||||
upstream_voltage_source = Huawei_V100R023C10::UpstreamVoltageSource::OVM;
|
||||
} else {
|
||||
EVLOG_AND_THROW(std::runtime_error("Invalid upstream voltage source: " + config.upstream_voltage_source));
|
||||
}
|
||||
|
||||
bool imds_necessary = upstream_voltage_source == UpstreamVoltageSource::IMD;
|
||||
bool ovms_necessary = upstream_voltage_source == UpstreamVoltageSource::OVM ||
|
||||
config.HACK_use_ovm_while_cable_check; // note that if the hack is enabled we also need OVMs
|
||||
|
||||
if (this->r_carside_powermeter.size() != 0 and this->r_carside_powermeter.size() != number_of_connectors_used) {
|
||||
EVLOG_AND_THROW(std::runtime_error(
|
||||
"Either use no carside powermeters or use the same number of powermeters as connectors in use"));
|
||||
}
|
||||
if (imds_necessary and this->r_isolation_monitor.size() != number_of_connectors_used) {
|
||||
EVLOG_AND_THROW(
|
||||
std::runtime_error("IMDs are necessary but number of IMDs does not match number of connectors in use"));
|
||||
}
|
||||
if (ovms_necessary and this->r_over_voltage_monitor.size() != number_of_connectors_used) {
|
||||
EVLOG_AND_THROW(
|
||||
std::runtime_error("OVMs are necessary but number of OVMs does not match number of connectors in use"));
|
||||
}
|
||||
|
||||
if (config.telemetry_topic_prefix.empty()) {
|
||||
this->telemetry_publisher = std::make_shared<fusion_charger::telemetry::TelemetryPublisherNull>();
|
||||
} else {
|
||||
this->telemetry_publisher = std::make_shared<TelemetryPublisherEverest>(
|
||||
[this](const std::string& topic, const nlohmann::json& data) {
|
||||
try {
|
||||
mqtt.publish(topic, data.dump());
|
||||
} catch (std::exception& e) {
|
||||
EVLOG_error << "Failed publishing telemetry data to MQTT topic " << topic << ": " << e.what();
|
||||
}
|
||||
},
|
||||
config.telemetry_topic_prefix);
|
||||
}
|
||||
|
||||
// Initialize all connectors. After that the config was loaded and we can initialize the dispenser
|
||||
for (int i = 0; i < number_of_connectors_used; i++) {
|
||||
invoke_init(*implementations[i]);
|
||||
}
|
||||
|
||||
DispenserConfig dispenser_config;
|
||||
dispenser_config.psu_host = config.psu_ip;
|
||||
dispenser_config.psu_port = (std::uint16_t)config.psu_port;
|
||||
dispenser_config.eth_interface = config.ethernet_interface;
|
||||
// fixed
|
||||
dispenser_config.manufacturer = 0x02;
|
||||
dispenser_config.model = 0x80;
|
||||
dispenser_config.charging_connector_count = number_of_connectors_used;
|
||||
// end fixed
|
||||
|
||||
dispenser_config.esn = config.esn;
|
||||
dispenser_config.send_secure_goose = config.send_secure_goose;
|
||||
dispenser_config.allow_unsecured_goose = config.allow_insecure_goose;
|
||||
dispenser_config.verify_secure_goose_hmac = config.verify_secure_goose;
|
||||
dispenser_config.module_placeholder_allocation_timeout =
|
||||
std::chrono::seconds(config.module_placeholder_allocation_timeout_s);
|
||||
|
||||
dispenser_config.telemetry_publisher = this->telemetry_publisher;
|
||||
|
||||
if (config.tls_enabled) {
|
||||
tls_util::MutualTlsClientConfig mutual_tls_config;
|
||||
mutual_tls_config.ca_cert = config.psu_ca_cert;
|
||||
mutual_tls_config.client_cert = config.client_cert;
|
||||
mutual_tls_config.client_key = config.client_key;
|
||||
dispenser_config.tls_config = mutual_tls_config;
|
||||
}
|
||||
|
||||
logs::LogIntf log{logs::LogFun([](const std::string& message) { EVLOG_error << message; }),
|
||||
logs::LogFun([](const std::string& message) { EVLOG_warning << message; }),
|
||||
logs::LogFun([](const std::string& message) { EVLOG_info << message; }),
|
||||
logs::LogFun([](const std::string& message) { EVLOG_debug << message; }),
|
||||
logs::LogFun([](const std::string& message) { EVLOG_verbose << message; })};
|
||||
|
||||
std::vector<ConnectorConfig> connector_configs;
|
||||
for (auto& connector : get_connector_bases(this, number_of_connectors_used)) {
|
||||
connector_configs.push_back(connector->get_connector_config());
|
||||
}
|
||||
|
||||
dispenser = std::make_unique<Dispenser>(dispenser_config, connector_configs, log);
|
||||
|
||||
// Subscribe to BSP Dispenser Alarms
|
||||
for (int bsp_idx = 0; bsp_idx < number_of_connectors_used; bsp_idx++) {
|
||||
dispenser_alarms_per_bsp.push_back(std::set<DispenserAlarms>{});
|
||||
|
||||
for (auto& alarm : get_all_dispenser_alarms()) {
|
||||
std::string everest_error = get_everest_error_for_dispenser_alarm(alarm);
|
||||
|
||||
r_board_support[bsp_idx]->subscribe_error(
|
||||
everest_error,
|
||||
[this, bsp_idx, alarm, everest_error](const ::Everest::error::Error& e) {
|
||||
// Error raised
|
||||
auto& alarms = dispenser_alarms_per_bsp[bsp_idx];
|
||||
if (alarms.find(alarm) == alarms.end()) {
|
||||
alarms.insert(alarm);
|
||||
|
||||
EVLOG_info << "Raising dispenser alarm due to BSP error " << everest_error;
|
||||
dispenser->set_dispenser_alarm(alarm, true);
|
||||
}
|
||||
},
|
||||
[this, bsp_idx, alarm, everest_error](const ::Everest::error::Error& e) {
|
||||
// Error cleared
|
||||
auto& alarms = dispenser_alarms_per_bsp[bsp_idx];
|
||||
if (alarms.find(alarm) != alarms.end()) {
|
||||
alarms.erase(alarm);
|
||||
|
||||
// check if any other BSP raised this alarm
|
||||
bool alarm_still_raised = false;
|
||||
for (const auto& other_alarms : dispenser_alarms_per_bsp) {
|
||||
if (other_alarms.find(alarm) != other_alarms.end()) {
|
||||
alarm_still_raised = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (not alarm_still_raised) {
|
||||
EVLOG_info << "Clearing dispenser alarm as all BSPs cleared error " << everest_error;
|
||||
dispenser->set_dispenser_alarm(alarm, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Huawei_V100R023C10::ready() {
|
||||
this->dispenser->start();
|
||||
|
||||
for (int i = 0; i < number_of_connectors_used; i++) {
|
||||
invoke_ready(*implementations[i]);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
if (this->dispenser->get_psu_running_mode() == PSURunningMode::RUNNING && !initial_hmac_acquired) {
|
||||
acquire_initial_hmac_keys_for_all_connectors();
|
||||
initial_hmac_acquired = true;
|
||||
}
|
||||
|
||||
update_psu_not_running_error();
|
||||
update_communication_errors();
|
||||
update_vendor_errors();
|
||||
restart_dispenser_if_needed();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
void Huawei_V100R023C10::acquire_initial_hmac_keys_for_all_connectors() {
|
||||
std::vector<std::thread> threads;
|
||||
for (int i = 0; i < number_of_connectors_used; i++) {
|
||||
threads.push_back(std::thread([this, i] { get_connector_impl(this, i)->do_init_hmac_acquire(); }));
|
||||
}
|
||||
|
||||
for (auto& thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void Huawei_V100R023C10::update_communication_errors() {
|
||||
auto connector_bases = get_connector_bases(this, number_of_connectors_used);
|
||||
|
||||
if (this->dispenser->get_psu_communication_state() != DispenserPsuCommunicationState::READY) {
|
||||
if (!psu_not_running_raised) {
|
||||
for (auto& connector : connector_bases) {
|
||||
connector->raise_communication_fault();
|
||||
}
|
||||
psu_not_running_raised = true;
|
||||
}
|
||||
} else {
|
||||
if (psu_not_running_raised) {
|
||||
for (auto& connector : connector_bases) {
|
||||
connector->clear_communication_fault();
|
||||
}
|
||||
psu_not_running_raised = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Huawei_V100R023C10::update_psu_not_running_error() {
|
||||
auto connector_bases = get_connector_bases(this, number_of_connectors_used);
|
||||
|
||||
if (this->dispenser->get_psu_running_mode() != PSURunningMode::RUNNING) {
|
||||
if (!communication_fault_raised) {
|
||||
for (auto& connector : connector_bases) {
|
||||
connector->raise_psu_not_running();
|
||||
}
|
||||
communication_fault_raised = true;
|
||||
}
|
||||
} else {
|
||||
if (communication_fault_raised) {
|
||||
for (auto& connector : connector_bases) {
|
||||
connector->clear_psu_not_running();
|
||||
}
|
||||
communication_fault_raised = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Huawei_V100R023C10::restart_dispenser_if_needed() {
|
||||
if (this->dispenser->get_psu_communication_state() == DispenserPsuCommunicationState::FAILED) {
|
||||
// Clear the stored capabilities in all connectors so that the missing cababilities error is raised
|
||||
// until we get new capabilities
|
||||
for (auto& connector : get_connector_bases(this, number_of_connectors_used)) {
|
||||
connector->clear_stored_capabilities();
|
||||
}
|
||||
EVLOG_info << "Dispenser: restarting communication (stopping first)";
|
||||
this->dispenser->stop();
|
||||
EVLOG_info << "Dispenser: starting communications again";
|
||||
this->dispenser->start();
|
||||
}
|
||||
}
|
||||
|
||||
void Huawei_V100R023C10::update_vendor_errors() {
|
||||
auto connector_bases = get_connector_bases(this, number_of_connectors_used);
|
||||
auto new_error_set = this->dispenser->get_raised_errors();
|
||||
|
||||
ErrorEventSet new_raised_errors;
|
||||
std::set_difference(new_error_set.begin(), new_error_set.end(), raised_errors.begin(), raised_errors.end(),
|
||||
std::inserter(new_raised_errors, new_raised_errors.begin()));
|
||||
|
||||
for (auto raised_error : new_raised_errors) {
|
||||
for (auto& connector : connector_bases) {
|
||||
connector->raise_psu_error(raised_error);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorEventSet new_cleared_errors;
|
||||
std::set_difference(raised_errors.begin(), raised_errors.end(), new_error_set.begin(), new_error_set.end(),
|
||||
std::inserter(new_cleared_errors, new_cleared_errors.begin()));
|
||||
|
||||
for (auto cleared_error : new_cleared_errors) {
|
||||
for (auto& connector : connector_bases) {
|
||||
connector->clear_psu_error(cleared_error);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorEventSet errors_intersection;
|
||||
std::set_intersection(raised_errors.begin(), raised_errors.end(), new_error_set.begin(), new_error_set.end(),
|
||||
std::inserter(errors_intersection, errors_intersection.begin()));
|
||||
|
||||
ErrorEventSet changed_errors;
|
||||
for (auto error : errors_intersection) {
|
||||
auto old_error = raised_errors.find(error);
|
||||
auto new_error = new_error_set.find(error);
|
||||
|
||||
if (old_error->payload.raw != new_error->payload.raw) {
|
||||
for (auto& connector : connector_bases) {
|
||||
connector->clear_psu_error(*old_error);
|
||||
connector->raise_psu_error(*new_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
raised_errors = new_error_set;
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,143 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef HUAWEI_V100R023C10_HPP
|
||||
#define HUAWEI_V100R023C10_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
|
||||
// headers for required interface implementations
|
||||
#include <generated/interfaces/evse_board_support/Interface.hpp>
|
||||
#include <generated/interfaces/isolation_monitor/Interface.hpp>
|
||||
#include <generated/interfaces/over_voltage_monitor/Interface.hpp>
|
||||
#include <generated/interfaces/powermeter/Interface.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
// insert your custom include headers here
|
||||
#include "telemetry_publisher_everest.hpp"
|
||||
#include <dispenser.hpp>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {
|
||||
std::string ethernet_interface;
|
||||
std::string psu_ip;
|
||||
int psu_port;
|
||||
bool tls_enabled;
|
||||
std::string psu_ca_cert;
|
||||
std::string client_cert;
|
||||
std::string client_key;
|
||||
int module_placeholder_allocation_timeout_s;
|
||||
std::string esn;
|
||||
bool HACK_publish_requested_voltage_current;
|
||||
bool HACK_use_ovm_while_cable_check;
|
||||
bool send_secure_goose;
|
||||
bool allow_insecure_goose;
|
||||
bool verify_secure_goose;
|
||||
std::string upstream_voltage_source;
|
||||
std::string telemetry_topic_prefix;
|
||||
};
|
||||
|
||||
class Huawei_V100R023C10 : public Everest::ModuleBase {
|
||||
public:
|
||||
Huawei_V100R023C10() = delete;
|
||||
Huawei_V100R023C10(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider,
|
||||
std::unique_ptr<power_supply_DCImplBase> p_connector_1,
|
||||
std::unique_ptr<power_supply_DCImplBase> p_connector_2,
|
||||
std::unique_ptr<power_supply_DCImplBase> p_connector_3,
|
||||
std::unique_ptr<power_supply_DCImplBase> p_connector_4,
|
||||
std::vector<std::unique_ptr<evse_board_supportIntf>> r_board_support,
|
||||
std::vector<std::unique_ptr<isolation_monitorIntf>> r_isolation_monitor,
|
||||
std::vector<std::unique_ptr<powermeterIntf>> r_carside_powermeter,
|
||||
std::vector<std::unique_ptr<over_voltage_monitorIntf>> r_over_voltage_monitor, Conf& config) :
|
||||
ModuleBase(info),
|
||||
mqtt(mqtt_provider),
|
||||
p_connector_1(std::move(p_connector_1)),
|
||||
p_connector_2(std::move(p_connector_2)),
|
||||
p_connector_3(std::move(p_connector_3)),
|
||||
p_connector_4(std::move(p_connector_4)),
|
||||
r_board_support(std::move(r_board_support)),
|
||||
r_isolation_monitor(std::move(r_isolation_monitor)),
|
||||
r_carside_powermeter(std::move(r_carside_powermeter)),
|
||||
r_over_voltage_monitor(std::move(r_over_voltage_monitor)),
|
||||
config(config){};
|
||||
|
||||
Everest::MqttProvider& mqtt;
|
||||
const std::unique_ptr<power_supply_DCImplBase> p_connector_1;
|
||||
const std::unique_ptr<power_supply_DCImplBase> p_connector_2;
|
||||
const std::unique_ptr<power_supply_DCImplBase> p_connector_3;
|
||||
const std::unique_ptr<power_supply_DCImplBase> p_connector_4;
|
||||
const std::vector<std::unique_ptr<evse_board_supportIntf>> r_board_support;
|
||||
const std::vector<std::unique_ptr<isolation_monitorIntf>> r_isolation_monitor;
|
||||
const std::vector<std::unique_ptr<powermeterIntf>> r_carside_powermeter;
|
||||
const std::vector<std::unique_ptr<over_voltage_monitorIntf>> r_over_voltage_monitor;
|
||||
const Conf& config;
|
||||
|
||||
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
|
||||
// insert your public definitions here
|
||||
/**
|
||||
* @brief Number of connectors that are really used and initialized
|
||||
*/
|
||||
std::uint16_t number_of_connectors_used;
|
||||
std::unique_ptr<Dispenser> dispenser;
|
||||
|
||||
std::atomic<bool> communication_fault_raised;
|
||||
std::atomic<bool> psu_not_running_raised;
|
||||
|
||||
std::atomic<bool> initial_hmac_acquired;
|
||||
|
||||
std::vector<power_supply_DCImplBase*> implementations = {p_connector_1.get(), p_connector_2.get(),
|
||||
p_connector_3.get(), p_connector_4.get()};
|
||||
|
||||
// List of sets of active DispenserAlarms for each BSP module
|
||||
std::vector<std::set<DispenserAlarms>> dispenser_alarms_per_bsp;
|
||||
|
||||
enum class UpstreamVoltageSource {
|
||||
IMD,
|
||||
OVM,
|
||||
};
|
||||
// PSU upstream voltage source
|
||||
UpstreamVoltageSource upstream_voltage_source;
|
||||
|
||||
std::shared_ptr<fusion_charger::telemetry::TelemetryPublisherBase> telemetry_publisher;
|
||||
// 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
|
||||
ErrorEventSet raised_errors;
|
||||
|
||||
void acquire_initial_hmac_keys_for_all_connectors();
|
||||
void update_psu_not_running_error();
|
||||
void update_communication_errors();
|
||||
void update_vendor_errors();
|
||||
void restart_dispenser_if_needed();
|
||||
// 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 // HUAWEI_V100R023C10_HPP
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "power_supply_DCImpl.hpp"
|
||||
|
||||
namespace module {
|
||||
namespace connector_1 {
|
||||
|
||||
void power_supply_DCImpl::init() {
|
||||
base.ev_set_config(EverestConnectorConfig::from_everest(config));
|
||||
base.ev_set_mod(mod);
|
||||
base.ev_init();
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::ready() {
|
||||
base.ev_ready();
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) {
|
||||
base.ev_handle_setMode(mode, phase);
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double& current) {
|
||||
base.ev_handle_setExportVoltageCurrent(voltage, current);
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setImportVoltageCurrent(double& voltage, double& current) {
|
||||
base.ev_handle_setImportVoltageCurrent(voltage, current);
|
||||
}
|
||||
|
||||
} // namespace connector_1
|
||||
} // namespace module
|
||||
@@ -0,0 +1,71 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef CONNECTOR_1_POWER_SUPPLY_DC_IMPL_HPP
|
||||
#define CONNECTOR_1_POWER_SUPPLY_DC_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
|
||||
#include "../Huawei_V100R023C10.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
#include "../connector_base/base.hpp"
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace connector_1 {
|
||||
|
||||
struct Conf {
|
||||
int global_connector_number;
|
||||
double max_export_current_A;
|
||||
double max_export_power_W;
|
||||
};
|
||||
|
||||
class power_supply_DCImpl : public power_supply_DCImplBase {
|
||||
public:
|
||||
power_supply_DCImpl() = delete;
|
||||
power_supply_DCImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<Huawei_V100R023C10>& mod,
|
||||
Conf& config) :
|
||||
power_supply_DCImplBase(ev, "connector_1"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
ConnectorBase base = ConnectorBase(0, this);
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual void handle_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) override;
|
||||
virtual void handle_setExportVoltageCurrent(double& voltage, double& current) override;
|
||||
virtual void handle_setImportVoltageCurrent(double& voltage, double& current) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<Huawei_V100R023C10>& 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 connector_1
|
||||
} // namespace module
|
||||
|
||||
#endif // CONNECTOR_1_POWER_SUPPLY_DC_IMPL_HPP
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "power_supply_DCImpl.hpp"
|
||||
|
||||
namespace module {
|
||||
namespace connector_2 {
|
||||
|
||||
void power_supply_DCImpl::init() {
|
||||
base.ev_set_config(EverestConnectorConfig::from_everest(config));
|
||||
base.ev_set_mod(mod);
|
||||
base.ev_init();
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::ready() {
|
||||
base.ev_ready();
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) {
|
||||
base.ev_handle_setMode(mode, phase);
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double& current) {
|
||||
base.ev_handle_setExportVoltageCurrent(voltage, current);
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setImportVoltageCurrent(double& voltage, double& current) {
|
||||
base.ev_handle_setImportVoltageCurrent(voltage, current);
|
||||
}
|
||||
|
||||
} // namespace connector_2
|
||||
} // namespace module
|
||||
@@ -0,0 +1,71 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef CONNECTOR_2_POWER_SUPPLY_DC_IMPL_HPP
|
||||
#define CONNECTOR_2_POWER_SUPPLY_DC_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
|
||||
#include "../Huawei_V100R023C10.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
#include "../connector_base/base.hpp"
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace connector_2 {
|
||||
|
||||
struct Conf {
|
||||
int global_connector_number;
|
||||
double max_export_current_A;
|
||||
double max_export_power_W;
|
||||
};
|
||||
|
||||
class power_supply_DCImpl : public power_supply_DCImplBase {
|
||||
public:
|
||||
power_supply_DCImpl() = delete;
|
||||
power_supply_DCImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<Huawei_V100R023C10>& mod,
|
||||
Conf& config) :
|
||||
power_supply_DCImplBase(ev, "connector_2"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
ConnectorBase base = ConnectorBase(1, this);
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual void handle_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) override;
|
||||
virtual void handle_setExportVoltageCurrent(double& voltage, double& current) override;
|
||||
virtual void handle_setImportVoltageCurrent(double& voltage, double& current) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<Huawei_V100R023C10>& 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 connector_2
|
||||
} // namespace module
|
||||
|
||||
#endif // CONNECTOR_2_POWER_SUPPLY_DC_IMPL_HPP
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "power_supply_DCImpl.hpp"
|
||||
|
||||
namespace module {
|
||||
namespace connector_3 {
|
||||
|
||||
void power_supply_DCImpl::init() {
|
||||
base.ev_set_config(EverestConnectorConfig::from_everest(config));
|
||||
base.ev_set_mod(mod);
|
||||
base.ev_init();
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::ready() {
|
||||
base.ev_ready();
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) {
|
||||
base.ev_handle_setMode(mode, phase);
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double& current) {
|
||||
base.ev_handle_setExportVoltageCurrent(voltage, current);
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setImportVoltageCurrent(double& voltage, double& current) {
|
||||
base.ev_handle_setImportVoltageCurrent(voltage, current);
|
||||
}
|
||||
|
||||
} // namespace connector_3
|
||||
} // namespace module
|
||||
@@ -0,0 +1,71 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef CONNECTOR_3_POWER_SUPPLY_DC_IMPL_HPP
|
||||
#define CONNECTOR_3_POWER_SUPPLY_DC_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
|
||||
#include "../Huawei_V100R023C10.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
#include "../connector_base/base.hpp"
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace connector_3 {
|
||||
|
||||
struct Conf {
|
||||
int global_connector_number;
|
||||
double max_export_current_A;
|
||||
double max_export_power_W;
|
||||
};
|
||||
|
||||
class power_supply_DCImpl : public power_supply_DCImplBase {
|
||||
public:
|
||||
power_supply_DCImpl() = delete;
|
||||
power_supply_DCImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<Huawei_V100R023C10>& mod,
|
||||
Conf& config) :
|
||||
power_supply_DCImplBase(ev, "connector_3"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
ConnectorBase base = ConnectorBase(2, this);
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual void handle_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) override;
|
||||
virtual void handle_setExportVoltageCurrent(double& voltage, double& current) override;
|
||||
virtual void handle_setImportVoltageCurrent(double& voltage, double& current) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<Huawei_V100R023C10>& 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 connector_3
|
||||
} // namespace module
|
||||
|
||||
#endif // CONNECTOR_3_POWER_SUPPLY_DC_IMPL_HPP
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "power_supply_DCImpl.hpp"
|
||||
|
||||
namespace module {
|
||||
namespace connector_4 {
|
||||
|
||||
void power_supply_DCImpl::init() {
|
||||
base.ev_set_config(EverestConnectorConfig::from_everest(config));
|
||||
base.ev_set_mod(mod);
|
||||
base.ev_init();
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::ready() {
|
||||
base.ev_ready();
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) {
|
||||
base.ev_handle_setMode(mode, phase);
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double& current) {
|
||||
base.ev_handle_setExportVoltageCurrent(voltage, current);
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setImportVoltageCurrent(double& voltage, double& current) {
|
||||
base.ev_handle_setImportVoltageCurrent(voltage, current);
|
||||
}
|
||||
|
||||
} // namespace connector_4
|
||||
} // namespace module
|
||||
@@ -0,0 +1,71 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef CONNECTOR_4_POWER_SUPPLY_DC_IMPL_HPP
|
||||
#define CONNECTOR_4_POWER_SUPPLY_DC_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
|
||||
#include "../Huawei_V100R023C10.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
#include "../connector_base/base.hpp"
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace connector_4 {
|
||||
|
||||
struct Conf {
|
||||
int global_connector_number;
|
||||
double max_export_current_A;
|
||||
double max_export_power_W;
|
||||
};
|
||||
|
||||
class power_supply_DCImpl : public power_supply_DCImplBase {
|
||||
public:
|
||||
power_supply_DCImpl() = delete;
|
||||
power_supply_DCImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<Huawei_V100R023C10>& mod,
|
||||
Conf& config) :
|
||||
power_supply_DCImplBase(ev, "connector_4"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
ConnectorBase base = ConnectorBase(3, this);
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual void handle_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) override;
|
||||
virtual void handle_setExportVoltageCurrent(double& voltage, double& current) override;
|
||||
virtual void handle_setImportVoltageCurrent(double& voltage, double& current) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<Huawei_V100R023C10>& 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 connector_4
|
||||
} // namespace module
|
||||
|
||||
#endif // CONNECTOR_4_POWER_SUPPLY_DC_IMPL_HPP
|
||||
@@ -0,0 +1,461 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "base.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
namespace module {
|
||||
|
||||
ConnectorBase::ConnectorBase(std::uint8_t connector, power_supply_DCImplBase* impl) :
|
||||
connector_no(connector), impl(impl), log_prefix("Connector #" + std::to_string(connector + 1) + ": ") {
|
||||
}
|
||||
|
||||
void ConnectorBase::ev_set_config(EverestConnectorConfig config) {
|
||||
this->config = config;
|
||||
}
|
||||
|
||||
void ConnectorBase::ev_set_mod(const Everest::PtrContainer<Huawei_V100R023C10>& mod) {
|
||||
this->mod = mod;
|
||||
}
|
||||
|
||||
void ConnectorBase::do_init_hmac_acquire() {
|
||||
std::lock_guard lock(connector_mutex);
|
||||
|
||||
EVLOG_info << log_prefix << "Trying to acquire hmac key to stop charging if it is still running";
|
||||
this->get_connector()->car_connect_disconnect_cycle(std::chrono::seconds(10));
|
||||
EVLOG_info << log_prefix << "Acquired hmac key";
|
||||
}
|
||||
|
||||
ConnectorConfig ConnectorBase::get_connector_config() {
|
||||
ConnectorConfig connector_config;
|
||||
|
||||
connector_config.global_connector_number = (std::uint16_t)config.global_connector_number;
|
||||
connector_config.connector_type = ConnectorType::CCS2;
|
||||
connector_config.max_rated_charge_current = (float)config.max_export_current_A;
|
||||
connector_config.max_rated_output_power = (float)config.max_export_power_W;
|
||||
|
||||
ConnectorCallbacks connector_callbacks;
|
||||
|
||||
connector_callbacks.connector_upstream_voltage = [this]() -> float {
|
||||
return this->external_provided_data.upstream_voltage.load();
|
||||
};
|
||||
connector_callbacks.output_voltage = [this]() -> float {
|
||||
return this->external_provided_data.output_voltage.load();
|
||||
};
|
||||
connector_callbacks.output_current = [this]() -> float {
|
||||
return this->external_provided_data.output_current.load();
|
||||
};
|
||||
connector_callbacks.contactor_status = [this]() -> ContactorStatus {
|
||||
return this->external_provided_data.contactor_status.load();
|
||||
};
|
||||
|
||||
// we just return LOCKED as default value
|
||||
connector_callbacks.electronic_lock_status = [this]() -> ElectronicLockStatus {
|
||||
return ElectronicLockStatus::LOCKED;
|
||||
};
|
||||
connector_config.connector_callbacks = connector_callbacks;
|
||||
return connector_config;
|
||||
}
|
||||
|
||||
void ConnectorBase::ev_init() {
|
||||
if (config.global_connector_number < 0) {
|
||||
EVLOG_critical << log_prefix << ": initialized but global connector number is invalid";
|
||||
throw std::runtime_error("Invalid global connector number");
|
||||
}
|
||||
|
||||
init_capabilities();
|
||||
|
||||
telemetry_subtopic = "connector/" + std::to_string(config.global_connector_number) + "/dispenser_to_psu";
|
||||
mod->telemetry_publisher->add_subtopic(telemetry_subtopic);
|
||||
|
||||
mod->telemetry_publisher->initialize_datapoint(telemetry_subtopic, telemetry_datapoint_keys::BSP_EVENT);
|
||||
|
||||
mod->r_board_support[this->connector_no]->subscribe_event(
|
||||
[this](const types::board_support_common::BspEvent& event) {
|
||||
EVLOG_verbose << log_prefix << "Received event: " << event;
|
||||
|
||||
if (event.event == types::board_support_common::Event::PowerOn) {
|
||||
this->external_provided_data.contactor_status = ContactorStatus::ON;
|
||||
} else if (event.event == types::board_support_common::Event::PowerOff) {
|
||||
this->external_provided_data.contactor_status = ContactorStatus::OFF;
|
||||
}
|
||||
|
||||
mod->telemetry_publisher->datapoint_changed(telemetry_subtopic, telemetry_datapoint_keys::BSP_EVENT,
|
||||
types::board_support_common::event_to_string(event.event));
|
||||
|
||||
auto connector = this->get_connector();
|
||||
std::lock_guard lock(connector_mutex);
|
||||
|
||||
if (event.event == types::board_support_common::Event::A) {
|
||||
connector->on_car_disconnected();
|
||||
} else if (event.event == types::board_support_common::Event::B) {
|
||||
connector->on_car_connected();
|
||||
}
|
||||
});
|
||||
|
||||
mod->telemetry_publisher->initialize_datapoint(telemetry_subtopic, "dc_output_contactor_fault_alarm", false);
|
||||
|
||||
mod->r_board_support[this->connector_no]->subscribe_error(
|
||||
"evse_board_support/MREC17EVSEContactorFault",
|
||||
[this](const Everest::error::Error& error) {
|
||||
get_connector()->set_dc_output_contactor_fault_alarm(true);
|
||||
mod->telemetry_publisher->datapoint_changed(telemetry_subtopic, "dc_output_contactor_fault_alarm", true);
|
||||
EVLOG_info << "Received contactor fault error from BSP";
|
||||
},
|
||||
[this](const Everest::error::Error& error) {
|
||||
get_connector()->set_dc_output_contactor_fault_alarm(false);
|
||||
mod->telemetry_publisher->datapoint_changed(telemetry_subtopic, "dc_output_contactor_fault_alarm", false);
|
||||
EVLOG_info << "Contactor fault error from BSP cleared";
|
||||
});
|
||||
|
||||
mod->telemetry_publisher->initialize_datapoint(telemetry_subtopic, telemetry_datapoint_keys::OUTPUT_VOLTAGE);
|
||||
mod->telemetry_publisher->initialize_datapoint(telemetry_subtopic, telemetry_datapoint_keys::OUTPUT_CURRENT);
|
||||
|
||||
if (not mod->r_carside_powermeter.empty()) {
|
||||
mod->r_carside_powermeter[this->connector_no]->subscribe_powermeter(
|
||||
[this](const types::powermeter::Powermeter& power) {
|
||||
EVLOG_verbose << log_prefix << "Received powermeter measurement: " << power;
|
||||
|
||||
auto output_voltage = power.voltage_V.value_or(types::units::Voltage{.DC = 0}).DC.value_or(0);
|
||||
auto output_current = power.current_A.value_or(types::units::Current{.DC = 0}).DC.value_or(0);
|
||||
|
||||
this->external_provided_data.output_voltage = output_voltage;
|
||||
this->external_provided_data.output_current = output_current;
|
||||
|
||||
if (mod->config.HACK_use_ovm_while_cable_check and
|
||||
last_phase == types::power_supply_DC::ChargingPhase::CableCheck) {
|
||||
return; // do not publish the powermeter values during cable check phase when HACK is enabled
|
||||
}
|
||||
|
||||
types::power_supply_DC::VoltageCurrent export_vc = {
|
||||
.voltage_V = output_voltage,
|
||||
.current_A = output_current,
|
||||
};
|
||||
|
||||
EVLOG_debug << log_prefix << "Publishing voltage/current from powermeter: " << output_voltage << "V "
|
||||
<< output_current << "A";
|
||||
|
||||
// Everest voltage measurement publishing
|
||||
this->impl->publish_voltage_current(export_vc);
|
||||
|
||||
mod->telemetry_publisher->datapoint_changed(telemetry_subtopic,
|
||||
telemetry_datapoint_keys::OUTPUT_VOLTAGE, output_voltage);
|
||||
mod->telemetry_publisher->datapoint_changed(telemetry_subtopic,
|
||||
telemetry_datapoint_keys::OUTPUT_CURRENT, output_current);
|
||||
});
|
||||
}
|
||||
|
||||
mod->telemetry_publisher->initialize_datapoint(telemetry_subtopic, telemetry_datapoint_keys::UPSTREAM_VOLTAGE);
|
||||
|
||||
// note that Huawei_V100R023C10 already checks, if the required interfaces are available
|
||||
if (mod->upstream_voltage_source == Huawei_V100R023C10::UpstreamVoltageSource::IMD) {
|
||||
mod->r_isolation_monitor[this->connector_no]->subscribe_isolation_measurement(
|
||||
[this](const types::isolation_monitor::IsolationMeasurement& iso) {
|
||||
EVLOG_verbose << log_prefix << "Received isolation measurement: " << iso;
|
||||
|
||||
// Upstream voltage publishing
|
||||
EVLOG_debug << log_prefix << "Publishing upstream voltage from IMD: " << iso.voltage_V.value_or(0)
|
||||
<< "V";
|
||||
this->external_provided_data.upstream_voltage = iso.voltage_V.value_or(0);
|
||||
mod->telemetry_publisher->datapoint_changed(telemetry_subtopic,
|
||||
telemetry_datapoint_keys::UPSTREAM_VOLTAGE,
|
||||
this->external_provided_data.upstream_voltage);
|
||||
});
|
||||
}
|
||||
|
||||
// note that Huawei_V100R023C10 already checks, if the required interfaces are available
|
||||
if (mod->upstream_voltage_source == Huawei_V100R023C10::UpstreamVoltageSource::OVM or
|
||||
mod->config.HACK_use_ovm_while_cable_check) {
|
||||
mod->r_over_voltage_monitor[this->connector_no]->subscribe_voltage_measurement_V([this](double voltage) {
|
||||
EVLOG_verbose << log_prefix << "Received OVM voltage measurement: " << voltage << "V";
|
||||
|
||||
// Upstream voltage publishing
|
||||
if (mod->upstream_voltage_source == Huawei_V100R023C10::UpstreamVoltageSource::OVM) {
|
||||
EVLOG_debug << log_prefix << "Publishing upstream voltage from OVM: " << voltage << "V";
|
||||
this->external_provided_data.upstream_voltage = voltage;
|
||||
mod->telemetry_publisher->datapoint_changed(telemetry_subtopic,
|
||||
telemetry_datapoint_keys::UPSTREAM_VOLTAGE,
|
||||
this->external_provided_data.upstream_voltage);
|
||||
}
|
||||
|
||||
// Everest voltage measurement publishing
|
||||
// only publish the ovm values if we are in the cable check phase
|
||||
if (mod->config.HACK_use_ovm_while_cable_check and
|
||||
last_phase == types::power_supply_DC::ChargingPhase::CableCheck) {
|
||||
|
||||
EVLOG_debug << log_prefix << "Publishing voltage/current from OVM: " << voltage << "V 0A";
|
||||
|
||||
types::power_supply_DC::VoltageCurrent export_vc = {
|
||||
.voltage_V = (float)voltage,
|
||||
.current_A = 0,
|
||||
};
|
||||
|
||||
this->impl->publish_voltage_current(export_vc);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mod->telemetry_publisher->initialize_datapoint(telemetry_subtopic, telemetry_datapoint_keys::EVEREST_MODE);
|
||||
mod->telemetry_publisher->initialize_datapoint(telemetry_subtopic, telemetry_datapoint_keys::EVEREST_PHASE);
|
||||
mod->telemetry_publisher->initialize_datapoint(telemetry_subtopic, telemetry_datapoint_keys::EXPORT_VOLTAGE);
|
||||
mod->telemetry_publisher->initialize_datapoint(telemetry_subtopic, telemetry_datapoint_keys::EXPORT_CURRENT);
|
||||
}
|
||||
|
||||
void ConnectorBase::ev_ready() {
|
||||
capabilities_not_received_raised = true;
|
||||
raise_missing_capabilities_error();
|
||||
|
||||
this->worker_thread_handle = std::thread(std::bind(&ConnectorBase::worker_thread, this));
|
||||
|
||||
this->impl->publish_capabilities(caps);
|
||||
this->impl->publish_mode(types::power_supply_DC::Mode::Off);
|
||||
}
|
||||
|
||||
void ConnectorBase::ev_handle_setMode(types::power_supply_DC::Mode mode, types::power_supply_DC::ChargingPhase phase) {
|
||||
|
||||
// if we get the stop request after cable check, we keep the phase
|
||||
if (last_mode == types::power_supply_DC::Mode::Export && mode == types::power_supply_DC::Mode::Off &&
|
||||
last_phase == types::power_supply_DC::ChargingPhase::CableCheck) {
|
||||
phase = types::power_supply_DC::ChargingPhase::CableCheck;
|
||||
}
|
||||
|
||||
EVLOG_debug << log_prefix << "Setting mode to " << mode << " and phase to " << phase;
|
||||
|
||||
last_mode = mode;
|
||||
last_phase = phase;
|
||||
|
||||
std::lock_guard lock(connector_mutex);
|
||||
|
||||
switch (mode) {
|
||||
case types::power_supply_DC::Mode::Off:
|
||||
switch (phase) {
|
||||
case types::power_supply_DC::ChargingPhase::CableCheck:
|
||||
this->get_connector()->on_mode_phase_change(ModePhase::OffCableCheck);
|
||||
break;
|
||||
default:
|
||||
this->get_connector()->on_mode_phase_change(ModePhase::Off);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case types::power_supply_DC::Mode::Export:
|
||||
switch (phase) {
|
||||
case types::power_supply_DC::ChargingPhase::CableCheck:
|
||||
this->get_connector()->on_mode_phase_change(ModePhase::ExportCableCheck);
|
||||
break;
|
||||
case types::power_supply_DC::ChargingPhase::PreCharge:
|
||||
this->get_connector()->on_mode_phase_change(ModePhase::ExportPrecharge);
|
||||
break;
|
||||
case types::power_supply_DC::ChargingPhase::Charging:
|
||||
this->get_connector()->on_mode_phase_change(ModePhase::ExportCharging);
|
||||
break;
|
||||
default:
|
||||
EVLOG_info << log_prefix << "Unknown Export phase: " << phase;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
mod->telemetry_publisher->datapoint_changed(telemetry_subtopic, telemetry_datapoint_keys::EVEREST_MODE,
|
||||
types::power_supply_DC::mode_to_string(mode));
|
||||
mod->telemetry_publisher->datapoint_changed(telemetry_subtopic, telemetry_datapoint_keys::EVEREST_PHASE,
|
||||
types::power_supply_DC::charging_phase_to_string(phase));
|
||||
|
||||
this->impl->publish_mode(mode);
|
||||
}
|
||||
void ConnectorBase::ev_handle_setExportVoltageCurrent(double voltage, double current) {
|
||||
EVLOG_debug << log_prefix << "Setting export voltage to " << voltage << "V and current to " << current << "A";
|
||||
if (voltage > caps.max_export_voltage_V)
|
||||
voltage = caps.max_export_voltage_V;
|
||||
else if (voltage < caps.min_export_voltage_V)
|
||||
voltage = caps.min_export_voltage_V;
|
||||
|
||||
if (current > caps.max_export_current_A)
|
||||
current = caps.max_export_current_A;
|
||||
else if (current < caps.min_export_current_A)
|
||||
current = caps.min_export_current_A;
|
||||
|
||||
std::lock_guard lock(connector_mutex);
|
||||
|
||||
export_voltage = voltage;
|
||||
export_current_limit = current;
|
||||
|
||||
mod->telemetry_publisher->datapoint_changed(telemetry_subtopic, telemetry_datapoint_keys::EXPORT_VOLTAGE, voltage);
|
||||
mod->telemetry_publisher->datapoint_changed(telemetry_subtopic, telemetry_datapoint_keys::EXPORT_CURRENT, current);
|
||||
|
||||
this->get_connector()->new_export_voltage_current(voltage, current);
|
||||
}
|
||||
void ConnectorBase::ev_handle_setImportVoltageCurrent(double voltage, double current) {
|
||||
EVLOG_error << "Not implemented";
|
||||
}
|
||||
|
||||
void ConnectorBase::worker_thread() {
|
||||
for (;;) {
|
||||
update_module_placeholder_errors();
|
||||
update_hack();
|
||||
update_and_publish_capabilities();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectorBase::update_module_placeholder_errors() {
|
||||
if (this->get_connector()->module_placeholder_allocation_failed()) {
|
||||
this->raise_module_placeholder_allocation_failure();
|
||||
} else {
|
||||
this->clear_module_placeholder_allocation_failure();
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectorBase::update_hack() {
|
||||
if (this->mod->config.HACK_publish_requested_voltage_current) {
|
||||
types::power_supply_DC::VoltageCurrent export_vc;
|
||||
export_vc.voltage_V = (float)this->export_voltage;
|
||||
export_vc.current_A = (float)this->export_current_limit;
|
||||
if (this->last_mode == types::power_supply_DC::Mode::Off) {
|
||||
export_vc.voltage_V = 0;
|
||||
export_vc.current_A = 0;
|
||||
}
|
||||
this->impl->publish_voltage_current(export_vc);
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectorBase::update_and_publish_capabilities() {
|
||||
auto new_caps = this->get_connector()->get_capabilities();
|
||||
|
||||
// apply config limits to the psu capabilities
|
||||
new_caps.max_export_current_A =
|
||||
std::min(static_cast<double>(new_caps.max_export_current_A), config.max_export_current_A);
|
||||
new_caps.max_export_power_W = std::min(static_cast<double>(new_caps.max_export_power_W), config.max_export_power_W);
|
||||
|
||||
std::lock_guard lock(connector_mutex);
|
||||
|
||||
bool caps_changed = false;
|
||||
|
||||
if (caps.max_export_voltage_V != new_caps.max_export_voltage_V) {
|
||||
caps.max_export_voltage_V = new_caps.max_export_voltage_V;
|
||||
caps_changed = true;
|
||||
}
|
||||
if (caps.min_export_voltage_V != new_caps.min_export_voltage_V) {
|
||||
caps.min_export_voltage_V = new_caps.min_export_voltage_V;
|
||||
caps_changed = true;
|
||||
}
|
||||
if (caps.max_export_current_A != new_caps.max_export_current_A) {
|
||||
caps.max_export_current_A = new_caps.max_export_current_A;
|
||||
caps_changed = true;
|
||||
}
|
||||
if (caps.min_export_current_A != new_caps.min_export_current_A) {
|
||||
caps.min_export_current_A = new_caps.min_export_current_A;
|
||||
caps_changed = true;
|
||||
}
|
||||
if (caps.max_export_power_W != new_caps.max_export_power_W) {
|
||||
caps.max_export_power_W = new_caps.max_export_power_W;
|
||||
caps_changed = true;
|
||||
}
|
||||
|
||||
if (caps_changed) {
|
||||
EVLOG_info << log_prefix << "Updating capabilities";
|
||||
this->impl->publish_capabilities(caps);
|
||||
|
||||
if (capabilities_not_received_raised and caps.max_export_current_A > 0 and caps.max_export_voltage_V > 0 and
|
||||
caps.max_export_power_W > 0) {
|
||||
capabilities_not_received_raised = false;
|
||||
this->clear_missing_capabilities_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectorBase::raise_missing_capabilities_error() {
|
||||
this->impl->raise_error(this->impl->error_factory->create_error(
|
||||
"power_supply_DC/HardwareFault", "capabilities_not_received", "", Everest::error::Severity::High));
|
||||
}
|
||||
void ConnectorBase::clear_missing_capabilities_error() {
|
||||
this->impl->clear_error("power_supply_DC/HardwareFault", "capabilities_not_received");
|
||||
}
|
||||
|
||||
void ConnectorBase::init_capabilities() {
|
||||
caps.current_regulation_tolerance_A = 1;
|
||||
caps.peak_current_ripple_A = 0.5;
|
||||
|
||||
caps.min_export_current_A = 0;
|
||||
caps.max_export_current_A = 0;
|
||||
caps.min_export_voltage_V = 0;
|
||||
caps.max_export_voltage_V = 0;
|
||||
caps.max_export_power_W = 0;
|
||||
|
||||
caps.max_import_current_A = 0;
|
||||
caps.min_import_current_A = 0;
|
||||
caps.max_import_power_W = 0;
|
||||
caps.min_import_voltage_V = 0;
|
||||
caps.max_import_voltage_V = 0;
|
||||
|
||||
// Nominal values are derived from configured limits
|
||||
caps.nominal_max_export_current_A = static_cast<float>(config.max_export_current_A);
|
||||
caps.nominal_max_export_power_W = static_cast<float>(config.max_export_power_W);
|
||||
|
||||
caps.conversion_efficiency_import = 0.85f;
|
||||
caps.conversion_efficiency_export = 0.9f;
|
||||
|
||||
caps.bidirectional = false;
|
||||
}
|
||||
|
||||
void ConnectorBase::raise_communication_fault() {
|
||||
this->impl->raise_error(this->impl->error_factory->create_error(
|
||||
"power_supply_DC/CommunicationFault", "", "Communication error", Everest::error::Severity::High));
|
||||
}
|
||||
void ConnectorBase::clear_communication_fault() {
|
||||
this->impl->clear_error("power_supply_DC/CommunicationFault");
|
||||
}
|
||||
|
||||
void ConnectorBase::raise_psu_not_running() {
|
||||
this->impl->raise_error(this->impl->error_factory->create_error("power_supply_DC/HardwareFault", "psu_not_running",
|
||||
"", Everest::error::Severity::High));
|
||||
}
|
||||
void ConnectorBase::clear_psu_not_running() {
|
||||
this->impl->clear_error("power_supply_DC/HardwareFault", "psu_not_running");
|
||||
}
|
||||
|
||||
void ConnectorBase::raise_module_placeholder_allocation_failure() {
|
||||
if (!module_placeholder_allocation_failure_raised) {
|
||||
this->impl->raise_error(this->impl->error_factory->create_error("power_supply_DC/VendorWarning",
|
||||
"module_placeholder_allocation_failed", "",
|
||||
Everest::error::Severity::High));
|
||||
module_placeholder_allocation_failure_raised = true;
|
||||
}
|
||||
}
|
||||
void ConnectorBase::clear_module_placeholder_allocation_failure() {
|
||||
if (module_placeholder_allocation_failure_raised) {
|
||||
this->impl->clear_error("power_supply_DC/VendorWarning", "module_placeholder_allocation_failed");
|
||||
module_placeholder_allocation_failure_raised = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connector* ConnectorBase::get_connector() {
|
||||
return this->mod->dispenser->get_connector(this->connector_no + 1).get();
|
||||
}
|
||||
|
||||
void ConnectorBase::raise_psu_error(ErrorEvent error) {
|
||||
this->impl->raise_error(
|
||||
this->impl->error_factory->create_error("power_supply_DC/VendorWarning", error.to_everest_subtype(),
|
||||
error.to_error_log_string(), Everest::error::Severity::Medium));
|
||||
}
|
||||
|
||||
void ConnectorBase::clear_psu_error(ErrorEvent error) {
|
||||
this->impl->clear_error("power_supply_DC/VendorWarning", error.to_everest_subtype());
|
||||
}
|
||||
|
||||
void ConnectorBase::clear_stored_capabilities() {
|
||||
std::lock_guard lock(connector_mutex);
|
||||
// If the error is not raised yet, raise it and clear the capabilities until we get them again
|
||||
if (not capabilities_not_received_raised) {
|
||||
capabilities_not_received_raised = true;
|
||||
init_capabilities();
|
||||
this->get_connector()->reset_psu_capabilities();
|
||||
this->impl->publish_capabilities(caps);
|
||||
raise_missing_capabilities_error();
|
||||
}
|
||||
}
|
||||
|
||||
}; // namespace module
|
||||
@@ -0,0 +1,131 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include "../Huawei_V100R023C10.hpp"
|
||||
#include <connector.hpp>
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
#include <generated/interfaces/power_supply_DC/Types.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
namespace module {
|
||||
|
||||
struct EverestConnectorConfig {
|
||||
int global_connector_number;
|
||||
double max_export_current_A;
|
||||
double max_export_power_W;
|
||||
|
||||
template <typename T> static EverestConnectorConfig from_everest(T in) {
|
||||
EverestConnectorConfig out;
|
||||
out.global_connector_number = in.global_connector_number;
|
||||
out.max_export_current_A = in.max_export_current_A;
|
||||
out.max_export_power_W = in.max_export_power_W;
|
||||
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
namespace telemetry_datapoint_keys {
|
||||
static const std::string UPSTREAM_VOLTAGE = "upstream_voltage";
|
||||
static const std::string OUTPUT_VOLTAGE = "output_voltage";
|
||||
static const std::string OUTPUT_CURRENT = "output_current";
|
||||
static const std::string EXPORT_VOLTAGE = "export_voltage";
|
||||
static const std::string EXPORT_CURRENT = "export_current";
|
||||
static const std::string BSP_EVENT = "bsp_event";
|
||||
static const std::string EVEREST_MODE = "everest_mode";
|
||||
static const std::string EVEREST_PHASE = "everest_phase";
|
||||
}; // namespace telemetry_datapoint_keys
|
||||
|
||||
class ConnectorBase {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor
|
||||
*
|
||||
* @param connector Connector number 0-3
|
||||
* @param ev_callbacks everest callbacks
|
||||
*/
|
||||
ConnectorBase(std::uint8_t connector, power_supply_DCImplBase* impl);
|
||||
|
||||
// Note that in init() the dispenser in the main class was not initialized yet
|
||||
void ev_init();
|
||||
void ev_ready();
|
||||
void ev_handle_setMode(types::power_supply_DC::Mode mode, types::power_supply_DC::ChargingPhase phase);
|
||||
void ev_handle_setExportVoltageCurrent(double voltage, double current);
|
||||
void ev_handle_setImportVoltageCurrent(double voltage, double current);
|
||||
|
||||
void ev_set_config(EverestConnectorConfig config);
|
||||
void ev_set_mod(const Everest::PtrContainer<Huawei_V100R023C10>& mod);
|
||||
|
||||
Connector* get_connector();
|
||||
|
||||
ConnectorConfig get_connector_config();
|
||||
|
||||
void raise_communication_fault();
|
||||
void clear_communication_fault();
|
||||
|
||||
void raise_psu_not_running();
|
||||
void clear_psu_not_running();
|
||||
|
||||
void raise_psu_error(ErrorEvent error);
|
||||
void clear_psu_error(ErrorEvent error);
|
||||
|
||||
/**
|
||||
* @brief Does an car connect - disconnect cycle blockingly
|
||||
*
|
||||
*/
|
||||
void do_init_hmac_acquire();
|
||||
|
||||
/**
|
||||
* @brief Clear all stored PSU capabilities and publish the resetted capabilities.
|
||||
* This also raises the missing capabilities error until new capabilities are received.
|
||||
*/
|
||||
void clear_stored_capabilities();
|
||||
|
||||
private:
|
||||
void raise_module_placeholder_allocation_failure();
|
||||
void clear_module_placeholder_allocation_failure();
|
||||
|
||||
void raise_missing_capabilities_error();
|
||||
void clear_missing_capabilities_error();
|
||||
|
||||
void worker_thread();
|
||||
std::thread worker_thread_handle;
|
||||
|
||||
void update_module_placeholder_errors();
|
||||
|
||||
void update_hack();
|
||||
void update_and_publish_capabilities();
|
||||
void init_capabilities();
|
||||
|
||||
std::string log_prefix;
|
||||
std::string telemetry_subtopic;
|
||||
|
||||
std::atomic<bool> module_placeholder_allocation_failure_raised;
|
||||
|
||||
std::mutex connector_mutex;
|
||||
|
||||
bool capabilities_not_received_raised{false};
|
||||
|
||||
std::uint16_t connector_no; // 0-3
|
||||
power_supply_DCImplBase* impl;
|
||||
EverestConnectorConfig config;
|
||||
Everest::PtrContainer<Huawei_V100R023C10> mod;
|
||||
|
||||
types::power_supply_DC::Capabilities caps;
|
||||
types::power_supply_DC::Mode last_mode;
|
||||
types::power_supply_DC::ChargingPhase last_phase;
|
||||
|
||||
double export_voltage{0.};
|
||||
double export_current_limit{0.};
|
||||
|
||||
struct {
|
||||
std::atomic<float> upstream_voltage;
|
||||
std::atomic<float> output_voltage;
|
||||
std::atomic<float> output_current;
|
||||
std::atomic<ContactorStatus> contactor_status;
|
||||
} external_provided_data;
|
||||
};
|
||||
|
||||
}; // namespace module
|
||||
@@ -0,0 +1,189 @@
|
||||
.. _everest_modules_handwritten_Huawei_V100R023C10:
|
||||
|
||||
.. ######################
|
||||
.. Huawei V100R023C10 PSU
|
||||
.. ######################
|
||||
|
||||
Voltage measurements
|
||||
====================
|
||||
|
||||
The Huawei V100R023C10 does not provide voltage measurements, instead it needs an external voltage
|
||||
measurement device that measures the "upstream" voltage (meaning directly after the PSU, before any relay).
|
||||
Also, Everest needs a voltage and current measurement regularly.
|
||||
|
||||
For the upstream voltage two options are available which (see `upstream_voltage_source` config option):
|
||||
|
||||
- Using an isolation monitoring device (``IMD``)
|
||||
- Using an overvoltage monitoring device (``OVM``)
|
||||
|
||||
For the everest measurements two options are available:
|
||||
|
||||
- None (not recommended, needs ``HACK_publish_requested_voltage_current`` to work properly)
|
||||
- Using a carside powermeter (ideally the powermeter that is connected to the EvseManager's ``powermeter_car_side``)
|
||||
- Using a carside powermeter but during cable check using an ``OVM`` (see ``HACK_use_ovm_while_cable_check`` config option)
|
||||
|
||||
Telemetry
|
||||
=========
|
||||
|
||||
The module can publish telemetry data on a specified mqtt base topic, set via the config option ``telemetry_topic_prefix``.
|
||||
The concrete telemetry data is published only when the data changes to reduce mqtt traffic.
|
||||
|
||||
The data published looks like this (example for base topic ``base_topic``):
|
||||
|
||||
``base_topic/connector/1``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"max_rated_psu_current": 100.0,
|
||||
"max_rated_psu_voltage": 1000.0,
|
||||
"min_rated_psu_current": 1.0,
|
||||
"min_rated_psu_voltage": 100.0,
|
||||
"psu_port_available": "AVAILABLE",
|
||||
"rated_output_power_psu": 60000.0
|
||||
}
|
||||
|
||||
``base_topic/connector/1/dispenser_to_psu``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"bsp_event": "PowerOn",
|
||||
"dc_output_contactor_fault_alarm": false,
|
||||
"everest_mode": "Export",
|
||||
"everest_phase": "Charging",
|
||||
"export_current": 20.0,
|
||||
"export_voltage": 400.0,
|
||||
"output_current": 0.0,
|
||||
"output_voltage": 0.0,
|
||||
"upstream_voltage": 0.0
|
||||
}
|
||||
|
||||
``base_topic/psu``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ac_input_current_a": 10.0,
|
||||
"ac_input_current_b": 10.5,
|
||||
"ac_input_current_c": 9.5,
|
||||
"ac_input_voltage_a": 230.0,
|
||||
"ac_input_voltage_b": 231.0,
|
||||
"ac_input_voltage_c": 229.0,
|
||||
"psu_running_mode": "RUNNING",
|
||||
"total_historic_input_energy": 100000.0
|
||||
}
|
||||
|
||||
``base_topic/dispenser/published_alarms``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"door_status_alarm": false,
|
||||
"epo_alarm": false,
|
||||
"tilt_alarm": false,
|
||||
"water_alarm": false
|
||||
}
|
||||
|
||||
The units are SI units (Amps, Volts, Watts, Watt-hours).
|
||||
|
||||
.. note::
|
||||
|
||||
All telemetry values can be null, indicating that no value has been received or sent yet.
|
||||
|
||||
BSP Errors
|
||||
==========
|
||||
|
||||
This driver supports setting specific errors to the Power supply unit as Dispenser and Connector Alarms as a reaction to EVerest BSP errors:
|
||||
|
||||
+-------------------------------------------------+---------------------------+---------------+
|
||||
| Everest BSP Error | PSU Modbus Register name | Scope |
|
||||
+=================================================+===========================+===============+
|
||||
| ``evse_board_support/EnclosureOpen`` | Door status alarm | Dispenser |
|
||||
+-------------------------------------------------+---------------------------+---------------+
|
||||
| ``evse_board_support/WaterIngressDetected`` | Water alarm | Dispenser |
|
||||
+-------------------------------------------------+---------------------------+---------------+
|
||||
| ``evse_board_support/MREC8EmergencyStop`` | EPO alarm | Dispenser |
|
||||
+-------------------------------------------------+---------------------------+---------------+
|
||||
| ``evse_board_support/TiltDetected`` | Tilt alarm | Dispenser |
|
||||
+-------------------------------------------------+---------------------------+---------------+
|
||||
| ``evse_board_support/MREC17EVSEContactorFault`` | DC output contactor fault | Per Connector |
|
||||
+-------------------------------------------------+---------------------------+---------------+
|
||||
|
||||
The connector alarms are published 1:1 to the connectors (if the BSP for connector 1 has the error, connector 1 gets the alarm, etc).
|
||||
|
||||
For the dispenser alarms, if any of the BSPs has the error, the alarm is published to the dispenser. If all BSPs clear the error, the alarm is cleared.
|
||||
|
||||
Power Supply Mock
|
||||
==================
|
||||
|
||||
The mock is a single executable that simulates the communication behaviour of a Huawei V100R023C10 power supply.
|
||||
It is used to test the software stack without needing the actual hardware.
|
||||
|
||||
It opens a socket on port 8502 to accept connections from the everest module and receives and answers goose messages.
|
||||
|
||||
The mock is built together with the everest module, but can also be build separately if needed.
|
||||
The mock is not installed by default but can be if ``INSTALL_FUSION_CHARGER_MOCK`` is set to ``ON`` in cmake.
|
||||
|
||||
Note that the mock uses a constant hmac key instead of generating a new one for each charge session.
|
||||
|
||||
Build separately from module
|
||||
----------------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cd modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make -j$(nproc)
|
||||
|
||||
Binary is located in:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/build/fusion-charger-dispenser-library/power_stack_mock/fusion_charger_mock
|
||||
|
||||
Mock options
|
||||
------------
|
||||
|
||||
The mock has a few environment variables (enable or disable by setting them to `1`/`true` or `0`/`false`):
|
||||
|
||||
- ``FUSION_CHARGER_MOCK_DISABLE_SEND_HMAC``: If set the mock will disable securing the goose messages with an hmac.
|
||||
They are still sent, just not secured.
|
||||
- ``FUSION_CHARGER_MOCK_DISABLE_VERIFY_HMAC``: If set the mock will disable verifying the hmac of the received goose
|
||||
messages. This also allows to receive completely unsigned messages.
|
||||
- ``FUSION_CHARGER_MOCK_ETH``: The ethernet interface to use for receiving and sending goose messages. Defaults to
|
||||
``veth0``.
|
||||
- ``FUSION_CHARGER_MOCK_PORT``: The Modbus TCP port to listen on. Defaults to ``8502``.
|
||||
- ``FUSION_CHARGER_MOCK_MQTT_HOST``: MQTT broker hostname or IP address. When set together with
|
||||
``FUSION_CHARGER_MOCK_MQTT_PORT``, the mock will publish power request data via MQTT.
|
||||
- ``FUSION_CHARGER_MOCK_MQTT_PORT``: MQTT broker port. Required together with ``FUSION_CHARGER_MOCK_MQTT_HOST`` to
|
||||
enable MQTT publishing.
|
||||
- ``FUSION_CHARGER_MOCK_MQTT_BASE_TOPIC``: Base topic for MQTT publishing. Defaults to ``fusion_charger_mock/``.
|
||||
Power requests are published to ``<base_topic>/<connector_number>/power_request`` with JSON payload containing
|
||||
voltage and current values.
|
||||
|
||||
It also has one optional command line argument, being the path to a folder with certificates and keys for mTLS.
|
||||
|
||||
Mock mTLS
|
||||
---------
|
||||
|
||||
The mock can be run with mTLS enabled. For this, one needs to create a folder with the following files:
|
||||
|
||||
- ``dispenser_ca.crt.pem``: The CA certificate used to sign the dispensers' certificates.
|
||||
- ``psu.crt.pem``: The certificate used by the mock to identify itself as a PSU.
|
||||
- ``psu.key.pem``: The private key of the PSU certificate.
|
||||
|
||||
These files can be generated with dummy values using the script located here (Note that this also generates the
|
||||
corresponding files for the dispenser):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/test_certificates/generate.sh
|
||||
|
||||
Then run the mock with the path to the certificates folder:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./fusion_charger_mock /path/to/certificates
|
||||
@@ -0,0 +1,5 @@
|
||||
.vscode/settings.json
|
||||
|
||||
build/
|
||||
.venv/
|
||||
.cache/
|
||||
@@ -0,0 +1,41 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(huawei-fusion-charger)
|
||||
|
||||
if(DEFINED MODULE_NAME)
|
||||
set(BUILDING_IN_EVEREST ON)
|
||||
else()
|
||||
set(BUILDING_IN_EVEREST OFF)
|
||||
endif()
|
||||
|
||||
if(BUILDING_IN_EVEREST)
|
||||
set(FUSION_CHARGER_LIB_BUILD_TESTS ${EVEREST_CORE_BUILD_TESTING})
|
||||
else()
|
||||
option(BUILD_TESTING "Build tests" OFF)
|
||||
set(FUSION_CHARGER_LIB_BUILD_TESTS ${BUILD_TESTING})
|
||||
endif()
|
||||
|
||||
if(NOT BUILDING_IN_EVEREST)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
if(POLICY CMP0135)
|
||||
cmake_policy(SET CMP0135 NEW)
|
||||
endif()
|
||||
|
||||
if(FUSION_CHARGER_LIB_BUILD_TESTS)
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
|
||||
)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
endif()
|
||||
|
||||
if(FUSION_CHARGER_LIB_BUILD_TESTS)
|
||||
include(CTest)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_subdirectory(fusion-charger-dispenser-library)
|
||||
add_subdirectory(goose-lib)
|
||||
add_subdirectory(huawei-fusioncharge-driver)
|
||||
add_subdirectory(modbus-server)
|
||||
add_subdirectory(log)
|
||||
@@ -0,0 +1,5 @@
|
||||
.vscode/settings.json
|
||||
|
||||
build/
|
||||
.venv/
|
||||
.cache/
|
||||
@@ -0,0 +1,21 @@
|
||||
file(GLOB LIB_SOURCES lib/*.cpp)
|
||||
add_library(fusion_charger_dispenser STATIC ${LIB_SOURCES})
|
||||
target_include_directories(fusion_charger_dispenser PUBLIC include)
|
||||
target_link_libraries(fusion_charger_dispenser PUBLIC fusion_charger_goose_driver fusion_charger_modbus_driver fusion_charger_modbus_extensions modbus-ssl Huawei::FusionCharger::LogInterface)
|
||||
|
||||
if(FUSION_CHARGER_LIB_BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
|
||||
option(BUILD_ACCEPTANCE_TESTS "Build acceptance tests" OFF)
|
||||
if (BUILD_ACCEPTANCE_TESTS)
|
||||
add_subdirectory(user-acceptance-tests)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
MQTT-C
|
||||
GIT_REPOSITORY https://github.com/LiamBindle/MQTT-C
|
||||
GIT_TAG v1.1.6
|
||||
)
|
||||
FetchContent_MakeAvailable(MQTT-C)
|
||||
add_subdirectory(power_stack_mock)
|
||||
endif()
|
||||
endif()
|
||||
@@ -0,0 +1,134 @@
|
||||
# Fusion Charger Dispenser Lib
|
||||
|
||||
A Library that provides a high-level interface for the Huawei Fusion-Charge Power-Suply Unit
|
||||
|
||||
## Create virtual ethernet interface
|
||||
|
||||
```bash
|
||||
sudo ip link add veth0 type veth peer name veth1
|
||||
sudo ip link set dev veth0 up
|
||||
sudo ip link set dev veth1 up
|
||||
```
|
||||
|
||||
### Delete again
|
||||
```bash
|
||||
sudo ip link delete veth0
|
||||
```
|
||||
|
||||
## FSM
|
||||
|
||||
```mermaid
|
||||
stateDiagram
|
||||
[*] --> CarDisconnected
|
||||
|
||||
state CarConnected {
|
||||
[*] --> NoKeyYet
|
||||
NoKeyYet
|
||||
ConnectedNoAllocation
|
||||
Running
|
||||
Completed
|
||||
}
|
||||
|
||||
CarDisconnected --> CarConnected : Car Connected
|
||||
|
||||
CarConnected --> CarDisconnected : Car Disconnected
|
||||
|
||||
NoKeyYet --> ConnectedNoAllocation : HMAC Key received
|
||||
ConnectedNoAllocation --> Running : Module allocation received or Timeout
|
||||
state Running {
|
||||
ExportCablecheck
|
||||
OffCablecheck
|
||||
ExportPrecharge
|
||||
ExportCharging
|
||||
ExportCharging
|
||||
}
|
||||
note right of Running
|
||||
All transitions in Running are possible, they are dependent on the EV Mode and EV Phase (see below). To exit Running a Off Mode in a non Cablecheck phase is required.
|
||||
end note
|
||||
|
||||
Running --> Completed : Mode Off / [!Cablecheck]
|
||||
Completed --> NoKeyYet : Mode Export
|
||||
```
|
||||
|
||||
### Detailed States
|
||||
|
||||
#### CarDisconnected
|
||||
|
||||
The car is not connected to the charger, we send stop goose frames and report an **Standby** working state
|
||||
|
||||
#### NoKeyYet
|
||||
|
||||
The car is now connected to the charger but we don't have a hmac key yet. We set the working state to **StandbyWithConnectorInserted** which will trigger a hmac key generation on the PSU (which we receive via modbus). We still send stop goose frames.
|
||||
|
||||
#### ConnectedNoAllocation
|
||||
|
||||
After we receive the hmac key via modbus we send placeholder requests to the PSU. To do that we have to be in the **ChargingStarting** working state. If we don't receive a response from the PSU in time we will still go to the Running state (as the goose answer frame might just have been dropped and we will not know that). If we receive a response we go to the Running state.
|
||||
|
||||
#### Running
|
||||
|
||||
We now have successfully allocated a module on the PSU and have a HMAC key thus we can start charging!
|
||||
|
||||
Initially we still send placeholder requests to the PSU and are in the ChargingStarting working State.
|
||||
|
||||
After a while everest will report some other Mode and phase. Depending on the mode and phase we go to different working states (see below).
|
||||
|
||||
When we receive an Off without being in the Cablecheck phase we go to the Completed state.
|
||||
|
||||
#### Completed
|
||||
|
||||
We are done charging and ready to charge again. If the car disconnects we go back to CarDisconnected; if we receive another Export mode we go back to NoKeyYet and acquire a new key and module placeholder allocation
|
||||
|
||||
### Workingstates
|
||||
|
||||
| State (siehe oben) | EV Mode | EV Phase | Working state |
|
||||
| --------------------- | ------- | ---------- | ---------------------------- |
|
||||
| CarDisconnected | * | * | Standby |
|
||||
| NoKeyYet | * | * | StandbyWithConnectorInserted |
|
||||
| ConnectedNoAllocation | * | * | ChargingStarting |
|
||||
| Running | Off | * | ChargingStarting |
|
||||
| Running | Export | Cablecheck | ChargingStarting |
|
||||
| Running | Export | Precharge | ChargingStarting |
|
||||
| Running | Export | Charge | Charging |
|
||||
| Completed | * | * | ChargingCompleted |
|
||||
|
||||
### Goose Frames
|
||||
|
||||
| State (see above) | EV Mode | EV Phase | Goose type | Goose PowerRequirement type |
|
||||
| --------------------- | ------- | -------------- | ---------------- | ---------------------------------------- |
|
||||
| CarDisconnected | * | * | Stop | |
|
||||
| NoKeyYet | * | * | Stop | |
|
||||
| ConnectedNoAllocation | * | * | PowerRequirement | ModulePlaceholderRequest |
|
||||
| Running | Off | * (low weight) | PowerRequirement | ModulePlaceholderRequest |
|
||||
| Running | Export | Cablecheck | PowerRequirement | InsulationDetectionVoltageOutput |
|
||||
| Running | Off | Cablecheck | PowerRequirement | InsulationDetectionVoltageOutputStoppage |
|
||||
| Running | Export | Precharge | PowerRequirement | Precharge |
|
||||
| Running | Export | Charge | PowerRequirement | RequirementDuringCharging |
|
||||
| Completed | * | * | Stop | |
|
||||
|
||||
|
||||
### Connection State
|
||||
|
||||
| State(see above) | ConnectionState |
|
||||
| ---------------- | --------------- |
|
||||
| CarDisconnected | NOT_CONNECTED |
|
||||
| * | FULLY_CONNECTED |
|
||||
|
||||
### Charge Event
|
||||
|
||||
- Transition to Running: STOP_TO_START
|
||||
- Transition to Completed: START_TO_STOP
|
||||
|
||||
### Module Placeholder Allocation failed
|
||||
|
||||
When we get the response from the PSU that the module placeholder allocation failed we go to Running thus we store a flag whether the module placeholder allocation was successful or not.
|
||||
|
||||
This flag is reset when we go out of the Running state.
|
||||
|
||||
### Connection state
|
||||
|
||||
The Module keeps track of the connection state with the PSU. There are the following states:
|
||||
|
||||
- `UNINITIALIZED` The Dispenser has not been started yet or has been stopped. No communication is happening
|
||||
- `INITIALIZING` `start()` has been called, a modbus connection is being established. The communication is not yet ready
|
||||
- `READY` The PSU's Ethernet MAC was received, communication is ready and working
|
||||
- `FAILED` The communication failed, no communication is happening in this state; the module stays in this state until `stop()` is called
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <fusion_charger/modbus/registers/connector.hpp>
|
||||
|
||||
using ElectronicLockStatus =
|
||||
fusion_charger::modbus_driver::raw_registers::CollectedConnectorRegisters::ElectronicLockStatus;
|
||||
using ContactorStatus = fusion_charger::modbus_driver::raw_registers::CollectedConnectorRegisters::ContactorStatus;
|
||||
|
||||
struct ConnectorCallbacks {
|
||||
std::function<float()> connector_upstream_voltage;
|
||||
std::function<float()> output_voltage;
|
||||
std::function<float()> output_current;
|
||||
std::function<ContactorStatus()> contactor_status;
|
||||
std::function<ElectronicLockStatus()> electronic_lock_status;
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
#include "callbacks.hpp"
|
||||
#include "fusion_charger/modbus/registers/raw.hpp"
|
||||
#include "telemetry.hpp"
|
||||
#include "tls_util.hpp"
|
||||
|
||||
typedef fusion_charger::modbus_driver::raw_registers::ConnectorType ConnectorType;
|
||||
|
||||
struct DispenserConfig {
|
||||
std::string psu_host;
|
||||
std::uint16_t psu_port;
|
||||
std::string eth_interface;
|
||||
|
||||
std::uint16_t manufacturer;
|
||||
std::uint16_t model;
|
||||
std::uint16_t protocol_version;
|
||||
std::uint16_t hardware_version;
|
||||
std::string software_version;
|
||||
|
||||
std::uint16_t charging_connector_count;
|
||||
std::string esn;
|
||||
|
||||
std::chrono::milliseconds modbus_timeout_ms = std::chrono::seconds(60);
|
||||
|
||||
bool send_secure_goose = true; // if set to true send secured goose frames,
|
||||
// if false only send unsecured frames
|
||||
bool allow_unsecured_goose = false; // if set to true allow unsecured goose frames from the PSU, if
|
||||
// false only allow secured frames (hmac not verified)
|
||||
bool verify_secure_goose_hmac = true; // if set to true verify the HMAC of secured goose frames, if false
|
||||
// do not verify the HMAC
|
||||
|
||||
// Optional TLS configuration
|
||||
// If not set plain TCP will be used
|
||||
std::optional<tls_util::MutualTlsClientConfig> tls_config;
|
||||
|
||||
std::chrono::milliseconds module_placeholder_allocation_timeout;
|
||||
|
||||
std::shared_ptr<fusion_charger::telemetry::TelemetryPublisherBase> telemetry_publisher =
|
||||
std::make_shared<fusion_charger::telemetry::TelemetryPublisherNull>();
|
||||
};
|
||||
|
||||
struct ConnectorConfig {
|
||||
std::uint16_t global_connector_number;
|
||||
ConnectorType connector_type = ConnectorType::CCS2;
|
||||
|
||||
// Maximum current that the connector can deliver in A
|
||||
float max_rated_charge_current = 0.0;
|
||||
|
||||
// Maximum output power that the connector can deliver in W
|
||||
float max_rated_output_power = 0.0;
|
||||
|
||||
ConnectorCallbacks connector_callbacks;
|
||||
};
|
||||
@@ -0,0 +1,303 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <fusion_charger/modbus/registers/connector.hpp>
|
||||
#include <mutex>
|
||||
|
||||
#include "callbacks.hpp"
|
||||
#include "configuration.hpp"
|
||||
#include "connector_goose_sender.hpp"
|
||||
#include "logs/logs.hpp"
|
||||
|
||||
struct Capabilities {
|
||||
float max_export_voltage_V;
|
||||
float min_export_voltage_V;
|
||||
float max_export_current_A;
|
||||
float min_export_current_A;
|
||||
float max_export_power_W;
|
||||
};
|
||||
|
||||
enum class ModePhase {
|
||||
Off,
|
||||
ExportCableCheck,
|
||||
OffCableCheck,
|
||||
ExportPrecharge,
|
||||
ExportCharging,
|
||||
};
|
||||
|
||||
enum class States {
|
||||
CarDisconnected,
|
||||
NoKeyYet,
|
||||
ConnectedNoAllocation,
|
||||
Running,
|
||||
Completed
|
||||
};
|
||||
|
||||
fusion_charger::modbus_driver::raw_registers::WorkingStatus state_to_ws(States state, ModePhase mode_phase);
|
||||
std::string state_to_string(States state);
|
||||
|
||||
/**
|
||||
* @brief Returns true if the mode phase is an export mode
|
||||
*
|
||||
* @param mode_phase the mode phase
|
||||
* @return true if the mode phase is ExportCableCheck, ExportPrecharge or
|
||||
* ExportCharging
|
||||
* @return false otherwise
|
||||
*/
|
||||
bool mode_phase_is_export_mode(ModePhase mode_phase);
|
||||
|
||||
class ConnectorFSM {
|
||||
States current_state;
|
||||
ModePhase current_mode_phase;
|
||||
|
||||
logs::LogIntf log;
|
||||
std::mutex mutex;
|
||||
|
||||
std::string log_prefix; // Prefix for log messages, can be set in the constructor
|
||||
|
||||
/**
|
||||
* @brief Do an state or mode phase transition. This will do the transition
|
||||
* and call the corresponding callbacks
|
||||
*
|
||||
* @note Please acquire the mutex before calling this
|
||||
*
|
||||
* @param new_state
|
||||
* @param new_mode_phase
|
||||
*/
|
||||
void transition(std::optional<States> new_state, std::optional<ModePhase> new_mode_phase);
|
||||
|
||||
public:
|
||||
struct Callbacks {
|
||||
// Called only when the state changes
|
||||
std::optional<std::function<void(States)>> state_transition;
|
||||
// Called only when the mode phase changes
|
||||
std::optional<std::function<void(ModePhase)>> mode_phase_transition;
|
||||
// Called when either state or mode phase changes
|
||||
std::optional<std::function<void(States, ModePhase)>> any_transition;
|
||||
} callbacks;
|
||||
|
||||
ConnectorFSM(Callbacks callbacks, logs::LogIntf log, std::string log_prefix = "");
|
||||
|
||||
States get_state();
|
||||
ModePhase get_mode_phase();
|
||||
|
||||
void on_car_connected();
|
||||
void on_car_disconnected();
|
||||
void on_mode_phase_change(ModePhase mode_phase);
|
||||
void on_module_placeholder_allocation_response(bool request_successful);
|
||||
void on_hmac_key_received();
|
||||
};
|
||||
|
||||
class Dispenser;
|
||||
|
||||
class Connector {
|
||||
typedef fusion_charger::modbus_driver::raw_registers::WorkingStatus WorkingStatus;
|
||||
|
||||
typedef fusion_charger::modbus_driver::raw_registers::PsuOutputPortAvailability PsuOutputPortAvailability;
|
||||
|
||||
typedef fusion_charger::modbus_driver::raw_registers::ConnectionStatus ConnectionStatus;
|
||||
|
||||
typedef fusion_charger::modbus_driver::ConnectorRegistersConfig ConnectorRegistersConfig;
|
||||
|
||||
typedef fusion_charger::modbus_driver::ConnectorRegisters ConnectorRegisters;
|
||||
|
||||
friend class Dispenser;
|
||||
|
||||
public:
|
||||
Connector(
|
||||
ConnectorConfig connector_config, uint16_t local_connector_number, DispenserConfig dispenser_config,
|
||||
logs::LogIntf log, std::function<void()> trigger_unsolicited_report_cb = []() {});
|
||||
Connector(const Connector&) = delete;
|
||||
~Connector();
|
||||
|
||||
// apply from dispenser (partly)
|
||||
void start();
|
||||
// apply from dispenser (partly)
|
||||
void stop();
|
||||
|
||||
/// @brief Check whether the last module placeholder allocation failed.
|
||||
/// @return true if the last module placeholder allocation failed.
|
||||
bool module_placeholder_allocation_failed();
|
||||
|
||||
PsuOutputPortAvailability get_output_port_availability();
|
||||
|
||||
/// @brief Get the currently published power capabilities. Currently there
|
||||
/// might only be the power set.
|
||||
/// @return
|
||||
Capabilities get_capabilities();
|
||||
|
||||
/// @brief Set the total historical energy charged. This is one value per
|
||||
/// dispenser.
|
||||
/// @param energy_charged total energy charged in kWh; Precision: 3.; Range:
|
||||
/// 0-4_294_967_295
|
||||
void set_total_historical_energy_charged_per_connector(double energy_charged);
|
||||
|
||||
WorkingStatus get_working_status();
|
||||
|
||||
/**
|
||||
* @brief Please call this if the car gets connected
|
||||
*
|
||||
*/
|
||||
void on_car_connected();
|
||||
|
||||
/**
|
||||
* @brief Please call this if the car gets disconnected
|
||||
*
|
||||
*/
|
||||
void on_car_disconnected();
|
||||
|
||||
/**
|
||||
* @brief This should be called if the current charging mode/phase changed
|
||||
*
|
||||
* @param mode_phase the new mode phase
|
||||
*/
|
||||
void on_mode_phase_change(ModePhase mode_phase);
|
||||
|
||||
/**
|
||||
* @brief This should be called if a new export voltage and current is
|
||||
* available
|
||||
*
|
||||
* @param voltage the new export voltage
|
||||
* @param current the new export current
|
||||
*/
|
||||
void new_export_voltage_current(double voltage, double current);
|
||||
|
||||
/**
|
||||
* @brief Does an car connect - disconnect cycle blockingly, while only trying
|
||||
* to acquire the HMAC key
|
||||
*
|
||||
* @param timeout the timeout to wait for the HMAC key
|
||||
*/
|
||||
void car_connect_disconnect_cycle(std::chrono::milliseconds timeout);
|
||||
|
||||
/**
|
||||
* @brief Get the current hmac key for this connector.
|
||||
*
|
||||
* @return the current hmac key
|
||||
*/
|
||||
std::vector<std::uint8_t> get_hmac_key();
|
||||
|
||||
/**
|
||||
* @brief Reset all stored PSU capabilities from our registers
|
||||
*/
|
||||
void reset_psu_capabilities();
|
||||
|
||||
/**
|
||||
* @brief Set or clear the DC output contactor fault alarm for this connector.
|
||||
* This will immediately publish the alarm state via Modbus.
|
||||
*/
|
||||
void set_dc_output_contactor_fault_alarm(bool active);
|
||||
|
||||
private:
|
||||
logs::LogIntf log;
|
||||
std::string log_prefix; // Prefix for log messages
|
||||
std::uint16_t local_connector_number; // 1-4
|
||||
ConnectorConfig connector_config;
|
||||
DispenserConfig dispenser_config;
|
||||
std::shared_ptr<goose_ethernet::EthernetInterface> eth_interface;
|
||||
ConnectorGooseSender goose_sender;
|
||||
ConnectorRegistersConfig connector_registers_config;
|
||||
ConnectorRegisters connector_registers;
|
||||
std::function<void()> trigger_unsolicited_report_cb; // callback to dispenser to trigger an
|
||||
// unsolicited report
|
||||
|
||||
std::optional<float> rated_output_power_psu; // in kW, set by register callback
|
||||
std::optional<float> max_rated_psu_voltage; // in V, set by register callback
|
||||
std::optional<float> max_rated_psu_current; // in A, set by register callback
|
||||
|
||||
// dc output contactor fault alarm state; used directly in register read
|
||||
// callback
|
||||
std::atomic<bool> dc_output_contactor_fault_alarm_active;
|
||||
|
||||
ConnectorFSM fsm;
|
||||
bool last_module_placeholder_allocation_failed;
|
||||
|
||||
struct {
|
||||
double voltage;
|
||||
double current;
|
||||
} current_requested_voltage_current;
|
||||
|
||||
struct {
|
||||
std::thread thread;
|
||||
std::mutex received_mutex;
|
||||
std::condition_variable received_cv;
|
||||
} module_placeholder_allocation_timeout;
|
||||
|
||||
/**
|
||||
* @brief (Re-)send the currently needed goose frame according to the current
|
||||
* state, modephase and voltage/current
|
||||
*
|
||||
* @param state the current state
|
||||
* @param mode_phase the current mode phase
|
||||
*/
|
||||
void send_needed_goose_frame(States state, ModePhase mode_phase);
|
||||
|
||||
/**
|
||||
* @brief Same as \c send_needed_goose_frame but current state and modephase
|
||||
* are taken from the fsm
|
||||
*/
|
||||
void send_needed_goose_frame();
|
||||
|
||||
/**
|
||||
* @brief Set the working status modbus register
|
||||
*
|
||||
* @param status the new working status
|
||||
*/
|
||||
void set_working_status(WorkingStatus status);
|
||||
|
||||
/**
|
||||
* @brief Set the connection status modbus register
|
||||
*
|
||||
* @param status the new connection status
|
||||
*/
|
||||
void set_connection_status(ConnectionStatus status);
|
||||
|
||||
/**
|
||||
* @brief Called upon module placeholder allocation response by the dispenser
|
||||
*
|
||||
* @param request_successful true if the request was successful
|
||||
*/
|
||||
void on_module_placeholder_allocation_response(bool request_successful);
|
||||
|
||||
/**
|
||||
* @brief Called when the psu sends a new mac address for goose frames
|
||||
*
|
||||
* @param mac_address the new mac address
|
||||
*/
|
||||
void on_psu_mac_change(std::vector<std::uint8_t> mac_address);
|
||||
|
||||
/**
|
||||
* @brief Called by the FSM when a state transition happens
|
||||
*
|
||||
* Reports an connector event (if necessary), updates the connector connection
|
||||
* status and resets \c last_module_placeholder_allocation_failed if possible
|
||||
*
|
||||
* @param state the new state
|
||||
*/
|
||||
void on_state_transition(States state);
|
||||
|
||||
/**
|
||||
* @brief Called by the FSM when any transition happens. Either the state, the
|
||||
* mode phase or both changed
|
||||
*
|
||||
* Updates the working status and calls \c send_needed_goose_frame
|
||||
*
|
||||
* @param state the new state
|
||||
* @param mode_phase the new mode phase
|
||||
*/
|
||||
void on_state_mode_phase_transition(States state, ModePhase mode_phase);
|
||||
|
||||
/**
|
||||
* @brief Called when the state is ConnectedNoAllocation and no response came
|
||||
* in time ( \c dispenser_config.module_placeholder_allocation_timeout ).
|
||||
*
|
||||
*/
|
||||
void on_module_placeholder_allocation_timeout();
|
||||
|
||||
/**
|
||||
* @brief Stop the thread which waits for the module placeholder allocation.
|
||||
* This function can be called even when the thread is not running.
|
||||
*/
|
||||
void cancel_module_placeholder_allocation_timeout();
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <fusion_charger/goose/power_request.hpp>
|
||||
#include <fusion_charger/goose/stop_charge_request.hpp>
|
||||
#include <goose/sender.hpp>
|
||||
#include <logs/logs.hpp>
|
||||
|
||||
struct PowerRequirement {
|
||||
fusion_charger::goose::RequirementType type;
|
||||
fusion_charger::goose::Mode mode;
|
||||
float current;
|
||||
float voltage;
|
||||
};
|
||||
|
||||
class ConnectorGooseSender {
|
||||
goose::sender::Sender goose_sender;
|
||||
std::optional<std::vector<std::uint8_t>> hmac_key;
|
||||
logs::LogIntf logs;
|
||||
bool secure;
|
||||
|
||||
void send_goose_frame(goose::frame::GoosePDU pdu, std::uint16_t appid);
|
||||
std::uint8_t destination_mac_address[6];
|
||||
|
||||
public:
|
||||
ConnectorGooseSender(std::shared_ptr<goose_ethernet::EthernetInterfaceIntf> intf, bool secure = false,
|
||||
logs::LogIntf log = logs::log_printf);
|
||||
|
||||
/**
|
||||
* @brief Start the sender thread (analogue to
|
||||
* \c goose::sender::Sender::start)
|
||||
*
|
||||
*/
|
||||
void start();
|
||||
/**
|
||||
* @brief Stop the sender thread (analogue to
|
||||
* \c goose::sender::Sender::stop)
|
||||
*
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* @brief Call this if the dispenser receives a new hmac key
|
||||
*
|
||||
* @param hmac_key the new hmac key
|
||||
*/
|
||||
void on_new_hmac_key(std::vector<std::uint8_t> hmac_key);
|
||||
/**
|
||||
* @brief Call this if the dispenser receives a new destination mac address
|
||||
*
|
||||
* @param mac_address the new destination mac address
|
||||
*/
|
||||
void on_new_mac_address(std::vector<std::uint8_t> mac_address);
|
||||
|
||||
/**
|
||||
* @brief Send a stop request Goose frame; either secure or insecure,
|
||||
* depending on whether the secure flag was sent in the constructor and if the
|
||||
* hmac key was received
|
||||
*
|
||||
* @param connector_no The global connector number
|
||||
*/
|
||||
void send_stop_request(std::uint16_t connector_no);
|
||||
|
||||
/**
|
||||
* @brief Send a PowerRequirement goose frame; either secure or insecure (see
|
||||
* \c send_stop_request)
|
||||
*
|
||||
*
|
||||
* @param connector_no The global connector number
|
||||
* @param requirement The power requirement which is converted to a goose
|
||||
* frame
|
||||
*/
|
||||
void send_power_requirement(std::uint16_t connector_no, PowerRequirement requirement);
|
||||
|
||||
/**
|
||||
* @brief Send a PowerRequirement frame with type ModulePlaceholderRequest via
|
||||
* \c send_power_requirement
|
||||
*
|
||||
* @param connector_no The global connector number
|
||||
*/
|
||||
void send_module_placeholder_request(std::uint16_t connector_no);
|
||||
|
||||
/**
|
||||
* @brief Send an PowerRequirement frame with type
|
||||
* InsulationDetectionVoltageOutput via \c send_power_requirement
|
||||
*
|
||||
* @param connector_no The global connector number
|
||||
* @param voltage The requested voltage
|
||||
* @param current The requested current
|
||||
*/
|
||||
void send_insulation_detection_voltage_output(std::uint16_t connector_no, float voltage, float current);
|
||||
|
||||
/**
|
||||
* @brief Send an PowerRequirement frame with type
|
||||
* InsulationDetectionVoltageOutputStoppage via \c send_power_requirement
|
||||
*
|
||||
* @param connector_no The global connector number
|
||||
*/
|
||||
void send_insulation_detection_voltage_output_stoppage(std::uint16_t connector_no);
|
||||
|
||||
/**
|
||||
* @brief Send an PowerRequirement frame with type PrechargeVoltageOutput via
|
||||
* \c send_power_requirement
|
||||
*
|
||||
* @param connector_no The global connector number
|
||||
* @param voltage The requested voltage
|
||||
* @param current The requested current
|
||||
*/
|
||||
void send_precharge_voltage_output(std::uint16_t connector_no, float voltage, float current);
|
||||
|
||||
/**
|
||||
* @brief Send an PowerRequirement frame with type RequirementDuringCharging
|
||||
* via \c send_power_requirement
|
||||
*
|
||||
* @param connector_no The global connector number
|
||||
* @param voltage The requested voltage
|
||||
* @param current The requested current
|
||||
*/
|
||||
void send_charging_voltage_output(std::uint16_t connector_no, float voltage, float current);
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <fusion_charger/goose/power_request.hpp>
|
||||
#include <fusion_charger/goose/stop_charge_request.hpp>
|
||||
#include <fusion_charger/modbus/extensions/unsolicitated_registry.hpp>
|
||||
#include <fusion_charger/modbus/extensions/unsolicitated_report_server.hpp>
|
||||
#include <fusion_charger/modbus/registers/connector.hpp>
|
||||
#include <fusion_charger/modbus/registers/dispenser.hpp>
|
||||
#include <fusion_charger/modbus/registers/error.hpp>
|
||||
#include <fusion_charger/modbus/registers/power_unit.hpp>
|
||||
#include <fusion_charger/modbus/registers/raw.hpp>
|
||||
#include <goose-ethernet/driver.hpp>
|
||||
#include <logs/logs.hpp>
|
||||
#include <modbus-server/modbus_basic_server.hpp>
|
||||
#include <modbus-ssl/openssl_transport.hpp>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
|
||||
#include "configuration.hpp"
|
||||
#include "connector.hpp"
|
||||
#include "connector_goose_sender.hpp"
|
||||
#include "state.hpp"
|
||||
#include "telemetry.hpp"
|
||||
|
||||
using namespace fusion_charger::modbus_driver::raw_registers;
|
||||
using namespace fusion_charger::modbus_driver;
|
||||
using namespace fusion_charger::modbus_extensions;
|
||||
|
||||
typedef fusion_charger::modbus_driver::raw_registers::CollectedConnectorRegisters::ContactorStatus ContactorStatus;
|
||||
|
||||
typedef fusion_charger::modbus_driver::raw_registers::WorkingStatus WorkingStatus;
|
||||
typedef fusion_charger::modbus_driver::raw_registers::ConnectionStatus ConnectionStatus;
|
||||
|
||||
typedef fusion_charger::goose::RequirementType PowerRequirementType;
|
||||
|
||||
typedef fusion_charger::goose::Mode PowerRequirementsMode;
|
||||
|
||||
typedef fusion_charger::goose::StopChargeRequest::Reason StopChargeReason;
|
||||
|
||||
typedef fusion_charger::modbus_driver::raw_registers::SettingPowerUnitRegisters::PSURunningMode PSURunningMode;
|
||||
|
||||
// add a custom comparator to the set which ignores the payload value
|
||||
// This is necessary to avoid having several error events with the same
|
||||
// category and subcategory
|
||||
typedef std::set<ErrorEvent, ErrorEventComparator> ErrorEventSet;
|
||||
|
||||
enum class DispenserAlarms {
|
||||
DOOR_STATUS_ALARM,
|
||||
WATER_ALARM,
|
||||
EPO_ALARM,
|
||||
TILT_ALARM,
|
||||
};
|
||||
|
||||
/// @brief Get a list of all possible DispenserAlarms
|
||||
const std::vector<DispenserAlarms>& get_all_dispenser_alarms();
|
||||
|
||||
class Dispenser {
|
||||
private:
|
||||
std::vector<std::shared_ptr<Connector>> connectors;
|
||||
logs::LogIntf log;
|
||||
|
||||
std::atomic<DispenserPsuCommunicationState> psu_communication_state = DispenserPsuCommunicationState::UNINITIALIZED;
|
||||
|
||||
DispenserConfig dispenser_config;
|
||||
std::vector<ConnectorConfig> connector_configs;
|
||||
|
||||
goose_ethernet::EthernetInterface eth_interface;
|
||||
|
||||
std::atomic<StopChargeReason> stop_charge_reason = StopChargeReason::INSULATION_FAULT;
|
||||
|
||||
std::optional<int> modbus_socket;
|
||||
std::optional<std::thread> modbus_event_loop_thread;
|
||||
std::optional<std::thread> modbus_unsolicitated_event_thread;
|
||||
std::optional<std::thread> goose_receiver_thread;
|
||||
|
||||
std::shared_ptr<modbus_server::ModbusTransport> transport;
|
||||
|
||||
std::optional<std::tuple<SSL*, SSL_CTX*>> openssl_data;
|
||||
std::shared_ptr<modbus_server::ModbusTCPProtocol> protocol;
|
||||
std::shared_ptr<modbus_server::PDUCorrelationLayer> pcl;
|
||||
std::optional<UnsolicitatedReportBasicServer> server;
|
||||
std::optional<UnsolicitatedRegistry> registry;
|
||||
|
||||
std::optional<DispenserRegisters> dispenser_registers;
|
||||
std::optional<PowerUnitRegisters> psu_registers;
|
||||
std::optional<ErrorRegisters> error_registers;
|
||||
|
||||
// Raised errors by the PSU
|
||||
ErrorEventSet raised_errors = {};
|
||||
std::mutex raised_error_mutex;
|
||||
|
||||
// Raised errrors by the dispenser (this driver)
|
||||
std::unordered_map<DispenserAlarms, std::atomic<bool>> dispenser_alarms;
|
||||
|
||||
std::optional<SettingPowerUnitRegisters::PSURunningMode> psu_running_mode = std::nullopt;
|
||||
|
||||
// Mutex + CV combination to trigger unsolicited reports
|
||||
std::mutex unsolicited_report_mutex;
|
||||
std::condition_variable unsolicited_report_cv;
|
||||
|
||||
const int MAX_NUMBER_OF_CONNECTORS = 4;
|
||||
|
||||
static const std::string DISPENSER_TELEMETRY_ALARMS_SUBTOPIC;
|
||||
|
||||
// true if the psu wrote its mac address via modbus
|
||||
bool psu_mac_received = false;
|
||||
// true if the psu wrote the connectors hmac key via modbus
|
||||
bool connector_hmac_received = false;
|
||||
|
||||
void init();
|
||||
|
||||
void update_psu_communication_state();
|
||||
|
||||
const int do_connect(const char* ip, std::uint16_t port);
|
||||
const int connect_with_retry(const char* ip, std::uint16_t port, int retries);
|
||||
void modbus_event_loop_thread_run();
|
||||
void modbus_unsolicitated_event_thread_run();
|
||||
void goose_receiver_thread_run();
|
||||
bool psu_communication_is_ok();
|
||||
bool is_stop_requested();
|
||||
|
||||
/// @brief get the state of a dispenser alarm, true if active
|
||||
bool get_dispenser_alarm_state(DispenserAlarms alarm);
|
||||
|
||||
/// @brief get the telemetry datapoint key for a dispenser alarm
|
||||
std::string dispenser_alarm_to_telemetry_datapoint(DispenserAlarms alarm);
|
||||
|
||||
public:
|
||||
Dispenser(DispenserConfig dispenser_config, std::vector<ConnectorConfig> connector_configs,
|
||||
logs::LogIntf log = logs::log_printf);
|
||||
~Dispenser();
|
||||
|
||||
/// @brief start threads that will run dispenser
|
||||
void start();
|
||||
|
||||
/// @brief stop running threads that are running dispenser. May take some
|
||||
/// time.
|
||||
void stop();
|
||||
|
||||
PSURunningMode get_psu_running_mode();
|
||||
// PsuOutputPortAvailability get_output_port_availability();
|
||||
DispenserPsuCommunicationState get_psu_communication_state();
|
||||
|
||||
/// @brief Retrieves the new error events. Clears the internal set of error
|
||||
/// events since the last call to this function
|
||||
/// @return The ErrorEvents that occured since the last call to this function
|
||||
ErrorEventSet get_raised_errors();
|
||||
|
||||
/// @brief Get the connector with the given local connector number.
|
||||
/// @param local_connector_number 1-4
|
||||
std::shared_ptr<Connector> get_connector(int local_connector_number) {
|
||||
if (local_connector_number == 0) {
|
||||
throw std::runtime_error("Connector number must be greater than 0");
|
||||
}
|
||||
if (local_connector_number > connectors.size()) {
|
||||
throw std::runtime_error("Connector number too high. Max local connector number: " +
|
||||
std::to_string(connectors.size()));
|
||||
}
|
||||
|
||||
// Connector numbers start at 1
|
||||
return connectors[local_connector_number - 1];
|
||||
}
|
||||
|
||||
/// @brief Trigger an unsolicited report to be sent now.
|
||||
void trigger_unsolicited_report();
|
||||
|
||||
/// @brief Set state for a dispenser alarm. Also triggers an immediate
|
||||
/// unsolicited report.
|
||||
/// @param alarm the alarm to set
|
||||
/// @param active true to set the alarm, false to clear it
|
||||
void set_dispenser_alarm(DispenserAlarms alarm, bool active);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
enum class DispenserPsuCommunicationState {
|
||||
UNINITIALIZED = 0,
|
||||
INITIALIZING = 1,
|
||||
READY = 2,
|
||||
FAILED = 3,
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <modbus-registers/data_provider.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace fusion_charger::telemetry {
|
||||
|
||||
/**
|
||||
* @brief Base interface class for fusion charger telemetry managers which are responsible for publishing telemetry
|
||||
* datapoints
|
||||
*/
|
||||
class TelemetryPublisherBase {
|
||||
public:
|
||||
virtual ~TelemetryPublisherBase() = default;
|
||||
/**
|
||||
* @brief Add a new subtopic for telemetry datapoints
|
||||
* @param subtopic The subtopic name
|
||||
*/
|
||||
virtual void add_subtopic(const std::string& subtopic) = 0;
|
||||
/**
|
||||
* @brief Notify that a datapoint for a subtopic has changed (string value)
|
||||
* @param subtopic The subtopic name
|
||||
* @param datapoint The datapoint name
|
||||
* @param value The new value
|
||||
*/
|
||||
virtual void datapoint_changed(const std::string& subtopic, const std::string& datapoint,
|
||||
const std::string& value) = 0;
|
||||
/**
|
||||
* @brief Notify that a datapoint for a subtopic has changed (double value)
|
||||
* @param subtopic The subtopic name
|
||||
* @param datapoint The datapoint name
|
||||
* @param value The new value
|
||||
*/
|
||||
virtual void datapoint_changed(const std::string& subtopic, const std::string& datapoint, double value) = 0;
|
||||
|
||||
/**
|
||||
* @brief Notify that a datapoint for a subtopic has changed (bool value)
|
||||
* @param subtopic The subtopic name
|
||||
* @param datapoint The datapoint name
|
||||
* @param value The new value
|
||||
*/
|
||||
virtual void datapoint_changed(const std::string& subtopic, const std::string& datapoint, bool value) = 0;
|
||||
|
||||
/**
|
||||
* @brief Check if a datapoint exists for a subtopic
|
||||
* @param subtopic The subtopic name
|
||||
* @param datapoint The datapoint name
|
||||
* @return true if the datapoint exists, false otherwise
|
||||
*/
|
||||
virtual bool datapoint_exists(const std::string& subtopic, const std::string& datapoint) = 0;
|
||||
|
||||
/**
|
||||
* @brief Initialize a datapoint for a subtopic with null
|
||||
* @param subtopic The subtopic name
|
||||
* @param datapoint The datapoint name
|
||||
*/
|
||||
virtual void initialize_datapoint(const std::string& subtopic, const std::string& datapoint) = 0;
|
||||
|
||||
/**
|
||||
* @brief Initialize a datapoint for a subtopic with a value
|
||||
* @note Does nothing if the datapoint already exists
|
||||
* @param subtopic The subtopic name
|
||||
* @param datapoint The datapoint name
|
||||
* @param value The initial value
|
||||
*/
|
||||
template <typename T>
|
||||
void initialize_datapoint(const std::string& subtopic, const std::string& datapoint, T value) {
|
||||
if (!datapoint_exists(subtopic, datapoint)) {
|
||||
datapoint_changed(subtopic, datapoint, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Utility to add a callback to a holding complex register data provider that notifies the telemetry manager
|
||||
* on value changes
|
||||
* @tparam T The type of the data provider value
|
||||
* @param subtopic The subtopic name
|
||||
* @param datapoint The datapoint name
|
||||
* @param data_provider The data provider to add the callback to
|
||||
* @param conversion_func An optional conversion function to convert (e.g. to SI units) the value before sending it
|
||||
* to the telemetry manager
|
||||
*/
|
||||
template <typename T>
|
||||
void
|
||||
register_complex_register_data_provider(const std::string& subtopic, const std::string& datapoint,
|
||||
modbus::registers::data_providers::DataProviderHolding<T>* data_provider,
|
||||
std::function<T(const T&)> conversion_func = nullptr) {
|
||||
initialize_datapoint(subtopic, datapoint);
|
||||
data_provider->add_write_callback([this, subtopic, datapoint, conversion_func](T value) {
|
||||
if (conversion_func) {
|
||||
value = conversion_func(value);
|
||||
}
|
||||
this->datapoint_changed(subtopic, datapoint, value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Utility to add a callback to a holding complex register data provider that notifies the telemetry manager
|
||||
* on value changes, converting the value to a string using the provided function
|
||||
* @tparam T The type of the data provider value
|
||||
* @param subtopic The subtopic name
|
||||
* @param datapoint The datapoint name
|
||||
* @param data_provider The data provider to add the callback to
|
||||
* @param to_string_func The function to convert the value to a string
|
||||
*/
|
||||
template <typename T>
|
||||
void register_complex_register_data_provider_enum(
|
||||
const std::string& subtopic, const std::string& datapoint,
|
||||
modbus::registers::data_providers::DataProviderHolding<T>* data_provider,
|
||||
std::function<std::string(const T&)> to_string_func) {
|
||||
initialize_datapoint(subtopic, datapoint);
|
||||
data_provider->add_write_callback([this, subtopic, datapoint, to_string_func](T value) {
|
||||
this->datapoint_changed(subtopic, datapoint, to_string_func(value));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Null implementation of the TelemetryPublisherBase that does nothing
|
||||
*/
|
||||
class TelemetryPublisherNull : public TelemetryPublisherBase {
|
||||
public:
|
||||
void add_subtopic(const std::string& subtopic) override {
|
||||
}
|
||||
void datapoint_changed(const std::string& subtopic, const std::string& datapoint,
|
||||
const std::string& value) override {
|
||||
}
|
||||
void datapoint_changed(const std::string& subtopic, const std::string& datapoint, double value) override {
|
||||
}
|
||||
void datapoint_changed(const std::string& subtopic, const std::string& datapoint, bool value) override {
|
||||
}
|
||||
void initialize_datapoint(const std::string& subtopic, const std::string& datapoint) override {
|
||||
}
|
||||
bool datapoint_exists(const std::string& subtopic, const std::string& datapoint) override {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
}; // namespace fusion_charger::telemetry
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <openssl/ssl.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace tls_util {
|
||||
struct MutualTlsClientConfig {
|
||||
// path to ca certificate of the server
|
||||
std::string ca_cert;
|
||||
|
||||
// path to client certificate
|
||||
std::string client_cert;
|
||||
// path to client key
|
||||
std::string client_key;
|
||||
};
|
||||
|
||||
std::tuple<SSL*, SSL_CTX*> init_mutual_tls_client(int socket, MutualTlsClientConfig config);
|
||||
|
||||
struct MutualTlsServerConfig {
|
||||
std::string client_ca;
|
||||
|
||||
std::string server_cert;
|
||||
std::string server_key;
|
||||
};
|
||||
|
||||
std::tuple<SSL*, SSL_CTX*> init_mutual_tls_server(int socket, MutualTlsServerConfig config);
|
||||
|
||||
void free_ssl(std::tuple<SSL*, SSL_CTX*> ssl);
|
||||
|
||||
} // namespace tls_util
|
||||
@@ -0,0 +1,560 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "connector.hpp"
|
||||
|
||||
#include "fusion_charger/modbus/registers/connector.hpp"
|
||||
|
||||
using namespace fusion_charger::modbus_driver::raw_registers;
|
||||
|
||||
bool mode_phase_is_export_mode(ModePhase mode_phase) {
|
||||
return mode_phase == ModePhase::ExportCableCheck || mode_phase == ModePhase::ExportPrecharge ||
|
||||
mode_phase == ModePhase::ExportCharging;
|
||||
}
|
||||
|
||||
ConnectorFSM::ConnectorFSM(ConnectorFSM::Callbacks callbacks, logs::LogIntf log, std::string _log_prefix) :
|
||||
current_state(States::CarDisconnected),
|
||||
current_mode_phase(ModePhase::Off),
|
||||
callbacks(callbacks),
|
||||
log(log),
|
||||
log_prefix(_log_prefix) {
|
||||
}
|
||||
|
||||
void ConnectorFSM::transition(std::optional<States> new_state, std::optional<ModePhase> new_mode_phase) {
|
||||
bool state_transitioned = false;
|
||||
bool modephase_transitioned = false;
|
||||
|
||||
if (new_state.has_value() && current_state != new_state.value()) {
|
||||
log.info << log_prefix + "New state: " + state_to_string(current_state) + " -> " +
|
||||
state_to_string(new_state.value());
|
||||
|
||||
current_state = new_state.value();
|
||||
state_transitioned = true;
|
||||
}
|
||||
|
||||
if (new_mode_phase.has_value() && current_mode_phase != new_mode_phase.value()) {
|
||||
current_mode_phase = new_mode_phase.value();
|
||||
modephase_transitioned = true;
|
||||
}
|
||||
|
||||
if (!state_transitioned && !modephase_transitioned) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Call callbacks as needed
|
||||
if (state_transitioned) {
|
||||
if (callbacks.state_transition.has_value()) {
|
||||
callbacks.state_transition.value()(current_state);
|
||||
}
|
||||
}
|
||||
|
||||
if (modephase_transitioned) {
|
||||
if (callbacks.mode_phase_transition.has_value()) {
|
||||
callbacks.mode_phase_transition.value()(current_mode_phase);
|
||||
}
|
||||
}
|
||||
|
||||
if (callbacks.any_transition.has_value()) {
|
||||
callbacks.any_transition.value()(current_state, current_mode_phase);
|
||||
}
|
||||
}
|
||||
|
||||
States ConnectorFSM::get_state() {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return current_state;
|
||||
}
|
||||
|
||||
ModePhase ConnectorFSM::get_mode_phase() {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return current_mode_phase;
|
||||
}
|
||||
|
||||
void ConnectorFSM::on_car_connected() {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
|
||||
if (current_state == States::CarDisconnected) {
|
||||
transition(States::NoKeyYet, std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectorFSM::on_car_disconnected() {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
|
||||
if (current_state != States::CarDisconnected) {
|
||||
transition(States::CarDisconnected, std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectorFSM::on_mode_phase_change(ModePhase mode_phase) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
|
||||
if (current_state == States::Running && mode_phase == ModePhase::Off) {
|
||||
transition(States::Completed, mode_phase);
|
||||
} else if (current_state == States::Completed && mode_phase_is_export_mode(mode_phase)) {
|
||||
transition(States::NoKeyYet, mode_phase);
|
||||
} else {
|
||||
transition(std::nullopt, mode_phase);
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectorFSM::on_module_placeholder_allocation_response(bool success) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
|
||||
if (current_state == States::ConnectedNoAllocation) {
|
||||
// note: we transition to Running even if the allocation failed
|
||||
transition(States::Running, std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectorFSM::on_hmac_key_received() {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
|
||||
if (current_state == States::NoKeyYet) {
|
||||
transition(States::ConnectedNoAllocation, std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
WorkingStatus state_to_ws(States state, ModePhase mode_phase) {
|
||||
switch (state) {
|
||||
case States::CarDisconnected:
|
||||
return WorkingStatus::STANDBY;
|
||||
case States::NoKeyYet:
|
||||
return WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED;
|
||||
case States::ConnectedNoAllocation:
|
||||
return WorkingStatus::CHARGING_STARTING;
|
||||
case States::Running: {
|
||||
switch (mode_phase) {
|
||||
case ModePhase::Off:
|
||||
return WorkingStatus::CHARGING_STARTING;
|
||||
case ModePhase::ExportCableCheck:
|
||||
return WorkingStatus::CHARGING_STARTING;
|
||||
case ModePhase::OffCableCheck:
|
||||
return WorkingStatus::CHARGING_STARTING;
|
||||
case ModePhase::ExportPrecharge:
|
||||
return WorkingStatus::CHARGING_STARTING;
|
||||
case ModePhase::ExportCharging:
|
||||
return WorkingStatus::CHARGING;
|
||||
}
|
||||
}
|
||||
case States::Completed:
|
||||
return WorkingStatus::CHARGING_COMPLETE;
|
||||
}
|
||||
|
||||
throw std::runtime_error("Unknown state");
|
||||
return WorkingStatus::STANDBY;
|
||||
}
|
||||
|
||||
std::string state_to_string(States state) {
|
||||
switch (state) {
|
||||
case States::CarDisconnected:
|
||||
return "CarDisconnected";
|
||||
case States::NoKeyYet:
|
||||
return "NoKeyYet";
|
||||
case States::ConnectedNoAllocation:
|
||||
return "ConnectedNoAllocation";
|
||||
case States::Running:
|
||||
return "Running";
|
||||
case States::Completed:
|
||||
return "Completed";
|
||||
}
|
||||
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
Connector::Connector(ConnectorConfig connector_config, uint16_t local_connector_number,
|
||||
DispenserConfig dispenser_config, logs::LogIntf log,
|
||||
std::function<void()> trigger_unsolicited_report_cb) :
|
||||
connector_config(connector_config),
|
||||
local_connector_number(local_connector_number),
|
||||
dispenser_config(dispenser_config),
|
||||
eth_interface(std::make_shared<goose_ethernet::EthernetInterface>(dispenser_config.eth_interface.c_str())),
|
||||
goose_sender(eth_interface, dispenser_config.send_secure_goose, log),
|
||||
log_prefix("Connector #" + std::to_string(local_connector_number) + ": "),
|
||||
connector_registers_config([this, &connector_config, local_connector_number]() {
|
||||
ConnectorRegistersConfig config;
|
||||
const auto mac_address = eth_interface->get_mac_address();
|
||||
config.mac_address[0] = mac_address[0];
|
||||
config.mac_address[1] = mac_address[1];
|
||||
config.mac_address[2] = mac_address[2];
|
||||
config.mac_address[3] = mac_address[3];
|
||||
config.mac_address[4] = mac_address[4];
|
||||
config.mac_address[5] = mac_address[5];
|
||||
config.type = connector_config.connector_type;
|
||||
config.global_connector_no = connector_config.global_connector_number;
|
||||
config.connector_number = local_connector_number;
|
||||
config.max_rated_charge_current = connector_config.max_rated_charge_current;
|
||||
config.rated_output_power_connector = connector_config.max_rated_output_power / 1000;
|
||||
config.get_contactor_upstream_voltage = connector_config.connector_callbacks.connector_upstream_voltage;
|
||||
config.get_output_voltage = connector_config.connector_callbacks.output_voltage;
|
||||
config.get_output_current = connector_config.connector_callbacks.output_current;
|
||||
config.get_contactor_status = connector_config.connector_callbacks.contactor_status;
|
||||
config.get_electronic_lock_status = connector_config.connector_callbacks.electronic_lock_status;
|
||||
config.get_dc_output_contact_fault = [this]() { return dc_output_contactor_fault_alarm_active.load(); };
|
||||
return config;
|
||||
}()),
|
||||
connector_registers(connector_registers_config),
|
||||
log(log),
|
||||
fsm(
|
||||
[this]() {
|
||||
ConnectorFSM::Callbacks callbacks;
|
||||
callbacks.state_transition = std::bind(&Connector::on_state_transition, this, std::placeholders::_1);
|
||||
callbacks.any_transition = std::bind(&Connector::on_state_mode_phase_transition, this,
|
||||
std::placeholders::_1, std::placeholders::_2);
|
||||
return callbacks;
|
||||
}(),
|
||||
log, log_prefix),
|
||||
trigger_unsolicited_report_cb(trigger_unsolicited_report_cb),
|
||||
dc_output_contactor_fault_alarm_active(false) {
|
||||
}
|
||||
|
||||
Connector::~Connector() {
|
||||
cancel_module_placeholder_allocation_timeout();
|
||||
}
|
||||
|
||||
void Connector::on_state_transition(States state) {
|
||||
// Update Connector event
|
||||
if (state == States::Running) {
|
||||
connector_registers.charging_event_connector.report(
|
||||
CollectedConnectorRegisters::ChargingEventConnector::STOP_TO_START);
|
||||
} else {
|
||||
connector_registers.charging_event_connector.report(
|
||||
CollectedConnectorRegisters::ChargingEventConnector::START_TO_STOP);
|
||||
}
|
||||
|
||||
// Update connection status
|
||||
if (state == States::CarDisconnected) {
|
||||
set_connection_status(ConnectionStatus::NOT_CONNECTED);
|
||||
} else {
|
||||
set_connection_status(ConnectionStatus::FULL_CONNECTED);
|
||||
}
|
||||
|
||||
// Module placeholder allocation timeout
|
||||
if (state == States::ConnectedNoAllocation) {
|
||||
cancel_module_placeholder_allocation_timeout();
|
||||
|
||||
// start timeout thread
|
||||
log.verbose << log_prefix + "Starting module placeholder allocation timeout thread";
|
||||
module_placeholder_allocation_timeout.thread = std::thread([this]() {
|
||||
std::cv_status wait_resp;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(module_placeholder_allocation_timeout.received_mutex);
|
||||
wait_resp = module_placeholder_allocation_timeout.received_cv.wait_for(
|
||||
lock, dispenser_config.module_placeholder_allocation_timeout);
|
||||
}
|
||||
|
||||
if (wait_resp == std::cv_status::no_timeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.verbose << log_prefix + "MPAC Timeout thread: timeout reached, checking state";
|
||||
if (fsm.get_state() == States::ConnectedNoAllocation) {
|
||||
on_module_placeholder_allocation_timeout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state != States::Running) {
|
||||
// The new states after a transition from the Running state are either
|
||||
// Completed or CarDisconnected, thus this is the perfect
|
||||
// time to do it
|
||||
last_module_placeholder_allocation_failed = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Connector::on_state_mode_phase_transition(States state, ModePhase mode_phase) {
|
||||
auto ws = state_to_ws(state, mode_phase);
|
||||
set_working_status(ws);
|
||||
|
||||
send_needed_goose_frame(state, mode_phase);
|
||||
}
|
||||
|
||||
void Connector::start() {
|
||||
last_module_placeholder_allocation_failed = false;
|
||||
// Re-Init Connector Registers to reset them on every start
|
||||
connector_registers = ConnectorRegisters(connector_registers_config),
|
||||
|
||||
connector_registers.hmac_key.add_write_callback([this](const std::uint8_t* value) {
|
||||
char hmac_str[97];
|
||||
for (int i = 0; i < 48; i++) {
|
||||
sprintf(hmac_str + i * 2, "%02X", value[i]);
|
||||
}
|
||||
log.info << log_prefix + "🔑 HMAC key changed to " + std::string(hmac_str);
|
||||
|
||||
goose_sender.on_new_hmac_key(std::vector<std::uint8_t>(value, value + 48));
|
||||
|
||||
fsm.on_hmac_key_received();
|
||||
});
|
||||
|
||||
connector_registers.psu_port_available.add_write_callback([this](PsuOutputPortAvailability value) {
|
||||
log.debug << log_prefix + "PSU port available changed to " + std::to_string((std::uint16_t)value);
|
||||
});
|
||||
|
||||
connector_registers.rated_output_power_psu.add_write_callback([this](float value) {
|
||||
if (rated_output_power_psu.has_value() and rated_output_power_psu.value() == value) {
|
||||
return; // no change
|
||||
}
|
||||
|
||||
rated_output_power_psu = value;
|
||||
log.info << log_prefix + "PSU Rated output power changed to " + std::to_string(value) + " kW";
|
||||
});
|
||||
|
||||
connector_registers.max_rated_psu_voltage.add_write_callback([this](float value) {
|
||||
if (max_rated_psu_voltage.has_value() and max_rated_psu_voltage.value() == value) {
|
||||
return; // no change
|
||||
}
|
||||
|
||||
max_rated_psu_voltage = value;
|
||||
log.info << log_prefix + "PSU Max rated voltage changed to " + std::to_string(value) + " V";
|
||||
});
|
||||
|
||||
connector_registers.max_rated_psu_current.add_write_callback([this](float value) {
|
||||
if (max_rated_psu_current.has_value() and max_rated_psu_current.value() == value) {
|
||||
return; // no change
|
||||
}
|
||||
|
||||
max_rated_psu_current = value;
|
||||
log.info << log_prefix + "PSU Max rated current changed to " + std::to_string(value) + " A";
|
||||
});
|
||||
|
||||
// todo: reset fsm?
|
||||
|
||||
goose_sender.start();
|
||||
|
||||
std::string connector_telemetry_subtopic = "connector/" + std::to_string(connector_config.global_connector_number);
|
||||
dispenser_config.telemetry_publisher->add_subtopic(connector_telemetry_subtopic);
|
||||
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider(
|
||||
connector_telemetry_subtopic, "max_rated_psu_current", &connector_registers.max_rated_psu_current);
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider(
|
||||
connector_telemetry_subtopic, "min_rated_psu_current", &connector_registers.min_rated_psu_current);
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider(
|
||||
connector_telemetry_subtopic, "max_rated_psu_voltage", &connector_registers.max_rated_psu_voltage);
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider(
|
||||
connector_telemetry_subtopic, "min_rated_psu_voltage", &connector_registers.min_rated_psu_voltage);
|
||||
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider<float>(
|
||||
connector_telemetry_subtopic, "rated_output_power_psu", &connector_registers.rated_output_power_psu,
|
||||
[](const float& kw) { return kw * 1000.0; });
|
||||
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider_enum<PsuOutputPortAvailability>(
|
||||
connector_telemetry_subtopic, "psu_port_available", &connector_registers.psu_port_available,
|
||||
psu_output_port_availability_to_string);
|
||||
}
|
||||
|
||||
void Connector::stop() {
|
||||
goose_sender.stop();
|
||||
cancel_module_placeholder_allocation_timeout();
|
||||
}
|
||||
|
||||
bool Connector::module_placeholder_allocation_failed() {
|
||||
return last_module_placeholder_allocation_failed;
|
||||
}
|
||||
|
||||
PsuOutputPortAvailability Connector::get_output_port_availability() {
|
||||
return connector_registers.psu_port_available.get_value();
|
||||
}
|
||||
|
||||
Capabilities Connector::get_capabilities() {
|
||||
Capabilities caps;
|
||||
caps.max_export_voltage_V = connector_registers.max_rated_psu_voltage.get_value();
|
||||
caps.min_export_voltage_V = connector_registers.min_rated_psu_voltage.get_value();
|
||||
caps.max_export_current_A = connector_registers.max_rated_psu_current.get_value();
|
||||
caps.min_export_current_A = connector_registers.min_rated_psu_current.get_value();
|
||||
caps.max_export_power_W = connector_registers.rated_output_power_psu.get_value() * 1000;
|
||||
return caps;
|
||||
}
|
||||
|
||||
void Connector::reset_psu_capabilities() {
|
||||
connector_registers.rated_output_power_psu.update_value(0);
|
||||
connector_registers.max_rated_psu_voltage.update_value(0);
|
||||
connector_registers.max_rated_psu_current.update_value(0);
|
||||
|
||||
rated_output_power_psu.reset();
|
||||
max_rated_psu_voltage.reset();
|
||||
max_rated_psu_current.reset();
|
||||
}
|
||||
|
||||
void Connector::set_total_historical_energy_charged_per_connector(double energy_charged) {
|
||||
connector_registers.total_energy_charged.update_value(energy_charged);
|
||||
}
|
||||
|
||||
WorkingStatus Connector::get_working_status() {
|
||||
return connector_registers.working_status.get_value();
|
||||
}
|
||||
|
||||
void Connector::on_car_connected() {
|
||||
fsm.on_car_connected();
|
||||
}
|
||||
|
||||
void Connector::on_car_disconnected() {
|
||||
fsm.on_car_disconnected();
|
||||
}
|
||||
|
||||
void Connector::on_mode_phase_change(ModePhase mode_phase) {
|
||||
fsm.on_mode_phase_change(mode_phase);
|
||||
}
|
||||
|
||||
void Connector::new_export_voltage_current(double voltage, double current) {
|
||||
this->current_requested_voltage_current.voltage = voltage;
|
||||
this->current_requested_voltage_current.current = current;
|
||||
|
||||
if (fsm.get_state() == States::Running) {
|
||||
send_needed_goose_frame();
|
||||
}
|
||||
}
|
||||
|
||||
void Connector::send_needed_goose_frame() {
|
||||
send_needed_goose_frame(fsm.get_state(), fsm.get_mode_phase());
|
||||
}
|
||||
void Connector::send_needed_goose_frame(States state, ModePhase mode_phase) {
|
||||
switch (state) {
|
||||
case States::CarDisconnected:
|
||||
case States::NoKeyYet:
|
||||
goose_sender.send_stop_request(connector_config.global_connector_number);
|
||||
break;
|
||||
case States::ConnectedNoAllocation:
|
||||
goose_sender.send_module_placeholder_request(connector_config.global_connector_number);
|
||||
break;
|
||||
|
||||
case States::Running: {
|
||||
switch (mode_phase) {
|
||||
case ModePhase::Off:
|
||||
// as we are in the running state, we have to continue charging
|
||||
// stuff, not stopping stuff. If we get a new ModePhase::Off we
|
||||
// then have to stop charging (goes to States::Completed
|
||||
// immediately through on_mode_phase_change) thus we continue to send
|
||||
// MPRs until we get another ModePhase
|
||||
goose_sender.send_module_placeholder_request(connector_config.global_connector_number);
|
||||
break;
|
||||
case ModePhase::ExportCableCheck:
|
||||
goose_sender.send_insulation_detection_voltage_output(connector_config.global_connector_number,
|
||||
current_requested_voltage_current.voltage,
|
||||
current_requested_voltage_current.current);
|
||||
break;
|
||||
case ModePhase::OffCableCheck:
|
||||
goose_sender.send_insulation_detection_voltage_output_stoppage(connector_config.global_connector_number);
|
||||
break;
|
||||
case ModePhase::ExportPrecharge:
|
||||
goose_sender.send_precharge_voltage_output(connector_config.global_connector_number,
|
||||
current_requested_voltage_current.voltage,
|
||||
current_requested_voltage_current.current);
|
||||
break;
|
||||
case ModePhase::ExportCharging:
|
||||
// initial send of charging power requirement; more will be sent
|
||||
// upon new_export_voltage_current
|
||||
goose_sender.send_charging_voltage_output(connector_config.global_connector_number,
|
||||
current_requested_voltage_current.voltage,
|
||||
current_requested_voltage_current.current);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case States::Completed:
|
||||
goose_sender.send_stop_request(connector_config.global_connector_number);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Connector::set_working_status(WorkingStatus status) {
|
||||
if (status == connector_registers.working_status.get_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info << log_prefix + "Set working status to: " + working_status_to_string(status);
|
||||
|
||||
connector_registers.working_status.update_value(status);
|
||||
}
|
||||
|
||||
void Connector::set_connection_status(ConnectionStatus status) {
|
||||
connector_registers.connection_status.update_value(status);
|
||||
}
|
||||
|
||||
void Connector::on_module_placeholder_allocation_response(bool success) {
|
||||
if (fsm.get_state() != States::ConnectedNoAllocation) {
|
||||
log.debug << log_prefix + "Module placeholder allocation response received, but "
|
||||
"not in ConnectedNoAllocation state, ignoring";
|
||||
return;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
log.info << log_prefix + "Module placeholder allocation received response, SUCCESS";
|
||||
} else {
|
||||
log.warning << log_prefix + "Module placeholder allocation received response, FAILED";
|
||||
}
|
||||
|
||||
last_module_placeholder_allocation_failed = !success;
|
||||
|
||||
cancel_module_placeholder_allocation_timeout();
|
||||
|
||||
fsm.on_module_placeholder_allocation_response(success);
|
||||
}
|
||||
|
||||
void Connector::on_module_placeholder_allocation_timeout() {
|
||||
if (fsm.get_state() != States::ConnectedNoAllocation) {
|
||||
log.debug << log_prefix + "Module placeholder allocation timeout, but not in "
|
||||
"ConnectedNoAllocation state, ignoring";
|
||||
return;
|
||||
}
|
||||
|
||||
log.warning << log_prefix + "Module placeholder allocation timeout";
|
||||
|
||||
// On timeout we still transition to running state as this is not a critical
|
||||
// error. See ../README.md for more information
|
||||
this->last_module_placeholder_allocation_failed = true;
|
||||
fsm.on_module_placeholder_allocation_response(false);
|
||||
}
|
||||
|
||||
void Connector::cancel_module_placeholder_allocation_timeout() {
|
||||
if (this->module_placeholder_allocation_timeout.thread.joinable()) {
|
||||
log.verbose << log_prefix + "Cancelling module placeholder allocation timeout thread";
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(module_placeholder_allocation_timeout.received_mutex);
|
||||
module_placeholder_allocation_timeout.received_cv.notify_all();
|
||||
}
|
||||
this->module_placeholder_allocation_timeout.thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void Connector::car_connect_disconnect_cycle(std::chrono::milliseconds timeout) {
|
||||
auto mode_phase_before = fsm.get_mode_phase();
|
||||
fsm.on_mode_phase_change(ModePhase::Off);
|
||||
|
||||
auto timeout_timestamp = std::chrono::steady_clock::now() + timeout;
|
||||
|
||||
on_car_connected();
|
||||
// Wait until we got the hmac key
|
||||
while (fsm.get_state() != States::ConnectedNoAllocation && fsm.get_state() != States::Running) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
if (std::chrono::steady_clock::now() > timeout_timestamp) {
|
||||
log.error << log_prefix + "Timeout while waiting for the hmac key";
|
||||
break;
|
||||
}
|
||||
}
|
||||
on_car_disconnected();
|
||||
|
||||
// If in the time we did car connect - disconnect cycle the mode/phase didnt
|
||||
// change go back to the previous mode/phase
|
||||
if (fsm.get_mode_phase() == ModePhase::Off) {
|
||||
fsm.on_mode_phase_change(mode_phase_before);
|
||||
}
|
||||
}
|
||||
|
||||
void Connector::on_psu_mac_change(std::vector<std::uint8_t> mac_address) {
|
||||
this->goose_sender.on_new_mac_address(mac_address);
|
||||
this->send_needed_goose_frame();
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> Connector::get_hmac_key() {
|
||||
const std::uint8_t* hmac_key = connector_registers.hmac_key.get_value(); // pointer to private memory
|
||||
return std::vector<std::uint8_t>(hmac_key, hmac_key + connector_registers.hmac_key.get_size());
|
||||
}
|
||||
|
||||
void Connector::set_dc_output_contactor_fault_alarm(bool active) {
|
||||
dc_output_contactor_fault_alarm_active = active;
|
||||
|
||||
// immediately do an unsolicited report
|
||||
if (trigger_unsolicited_report_cb) {
|
||||
trigger_unsolicited_report_cb();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "connector_goose_sender.hpp"
|
||||
|
||||
ConnectorGooseSender::ConnectorGooseSender(std::shared_ptr<goose_ethernet::EthernetInterfaceIntf> intf, bool secure,
|
||||
logs::LogIntf log) :
|
||||
secure(secure),
|
||||
logs(log),
|
||||
goose_sender(std::chrono::milliseconds(1000),
|
||||
std::vector<std::chrono::milliseconds>{std::chrono::milliseconds(2), std::chrono::milliseconds(2),
|
||||
std::chrono::milliseconds(4), std::chrono::milliseconds(8)},
|
||||
intf, log) {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
destination_mac_address[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectorGooseSender::start() {
|
||||
goose_sender.start();
|
||||
}
|
||||
|
||||
void ConnectorGooseSender::stop() {
|
||||
goose_sender.stop();
|
||||
}
|
||||
|
||||
void ConnectorGooseSender::on_new_hmac_key(std::vector<std::uint8_t> hmac_key) {
|
||||
this->hmac_key = hmac_key;
|
||||
}
|
||||
|
||||
void ConnectorGooseSender::on_new_mac_address(std::vector<std::uint8_t> mac_address) {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
this->destination_mac_address[i] = mac_address[i];
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectorGooseSender::send_goose_frame(goose::frame::GoosePDU pdu, std::uint16_t appid) {
|
||||
if (secure && !hmac_key) {
|
||||
logs.verbose << "Not sending goose frame, because no hmac key was received";
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_ptr<goose::sender::SendPacketIntf> packet;
|
||||
|
||||
if (secure) {
|
||||
goose::frame::SecureGooseFrame frame;
|
||||
memcpy(frame.destination_mac_address, destination_mac_address, 6);
|
||||
memcpy(frame.source_mac_address, goose_sender.get_mac_address(), 6);
|
||||
frame.appid[0] = (appid >> 8) & 0xff;
|
||||
frame.appid[1] = (appid >> 0) & 0xff;
|
||||
frame.pdu = pdu;
|
||||
frame.vlan_id = 0;
|
||||
frame.priority = 5;
|
||||
packet = std::make_unique<goose::sender::SendPacketSecure>(frame, hmac_key.value());
|
||||
} else {
|
||||
goose::frame::GooseFrame frame;
|
||||
memcpy(frame.destination_mac_address, destination_mac_address, 6);
|
||||
memcpy(frame.source_mac_address, goose_sender.get_mac_address(), 6);
|
||||
frame.appid[0] = (appid >> 8) & 0xff;
|
||||
frame.appid[1] = (appid >> 0) & 0xff;
|
||||
frame.pdu = pdu;
|
||||
frame.vlan_id = 0;
|
||||
frame.priority = 5;
|
||||
packet = std::make_unique<goose::sender::SendPacketNormal>(frame);
|
||||
}
|
||||
|
||||
goose_sender.send(std::move(packet));
|
||||
}
|
||||
|
||||
void ConnectorGooseSender::send_stop_request(std::uint16_t connector_no) {
|
||||
logs.verbose << "Sending stop request for connector ";
|
||||
|
||||
fusion_charger::goose::StopChargeRequest request;
|
||||
request.charging_connector_no = connector_no;
|
||||
request.charging_sn = 0xffff;
|
||||
request.reason = fusion_charger::goose::StopChargeRequest::Reason::NORMAL;
|
||||
|
||||
send_goose_frame(request.to_pdu(), 0x3002);
|
||||
}
|
||||
|
||||
void ConnectorGooseSender::send_power_requirement(std::uint16_t connector_no, PowerRequirement requirement) {
|
||||
logs.verbose << "Sending power requirement for connector ";
|
||||
fusion_charger::goose::PowerRequirementRequest request;
|
||||
request.charging_connector_no = connector_no;
|
||||
request.charging_sn = 0xffff;
|
||||
request.requirement_type = requirement.type;
|
||||
request.mode = requirement.mode;
|
||||
request.voltage = requirement.voltage;
|
||||
request.current = requirement.current;
|
||||
|
||||
send_goose_frame(request.to_pdu(), 0x0001);
|
||||
}
|
||||
|
||||
void ConnectorGooseSender::send_module_placeholder_request(std::uint16_t connector_no) {
|
||||
PowerRequirement requirement;
|
||||
requirement.type = fusion_charger::goose::RequirementType::ModulePlaceholderRequest;
|
||||
requirement.mode = fusion_charger::goose::Mode::ConstantCurrent; // todo: None?
|
||||
requirement.current = 0;
|
||||
requirement.voltage = 0;
|
||||
send_power_requirement(connector_no, requirement);
|
||||
}
|
||||
|
||||
void ConnectorGooseSender::send_insulation_detection_voltage_output(std::uint16_t connector_no, float voltage,
|
||||
float current) {
|
||||
PowerRequirement requirement;
|
||||
requirement.type = fusion_charger::goose::RequirementType::InsulationDetectionVoltageOutput;
|
||||
requirement.mode = fusion_charger::goose::Mode::ConstantCurrent;
|
||||
requirement.current = current;
|
||||
requirement.voltage = voltage;
|
||||
send_power_requirement(connector_no, requirement);
|
||||
}
|
||||
|
||||
void ConnectorGooseSender::send_insulation_detection_voltage_output_stoppage(std::uint16_t connector_no) {
|
||||
PowerRequirement requirement;
|
||||
requirement.type = fusion_charger::goose::RequirementType::InsulationDetectionVoltageOutputStoppage;
|
||||
requirement.mode = fusion_charger::goose::Mode::ConstantCurrent;
|
||||
requirement.current = 0;
|
||||
requirement.voltage = 0;
|
||||
send_power_requirement(connector_no, requirement);
|
||||
}
|
||||
|
||||
void ConnectorGooseSender::send_precharge_voltage_output(std::uint16_t connector_no, float voltage, float current) {
|
||||
PowerRequirement requirement;
|
||||
requirement.type = fusion_charger::goose::RequirementType::PrechargeVoltageOutput;
|
||||
requirement.mode = fusion_charger::goose::Mode::ConstantCurrent;
|
||||
requirement.current = current;
|
||||
requirement.voltage = voltage;
|
||||
send_power_requirement(connector_no, requirement);
|
||||
}
|
||||
|
||||
void ConnectorGooseSender::send_charging_voltage_output(std::uint16_t connector_no, float voltage, float current) {
|
||||
PowerRequirement requirement;
|
||||
requirement.type = fusion_charger::goose::RequirementType::Charging;
|
||||
requirement.mode = fusion_charger::goose::Mode::ConstantCurrent;
|
||||
requirement.current = current;
|
||||
requirement.voltage = voltage;
|
||||
send_power_requirement(connector_no, requirement);
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "dispenser.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
using namespace fusion_charger::modbus_driver::raw_registers;
|
||||
using namespace fusion_charger::modbus_driver;
|
||||
using namespace fusion_charger::modbus_extensions;
|
||||
|
||||
const std::string Dispenser::DISPENSER_TELEMETRY_ALARMS_SUBTOPIC = "dispenser/published_alarms";
|
||||
|
||||
const std::vector<DispenserAlarms>& get_all_dispenser_alarms() {
|
||||
static const std::vector<DispenserAlarms> alarms = {
|
||||
DispenserAlarms::DOOR_STATUS_ALARM,
|
||||
DispenserAlarms::WATER_ALARM,
|
||||
DispenserAlarms::EPO_ALARM,
|
||||
DispenserAlarms::TILT_ALARM,
|
||||
};
|
||||
return alarms;
|
||||
}
|
||||
|
||||
void Dispenser::modbus_unsolicitated_event_thread_run() {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
while (psu_communication_is_ok()) {
|
||||
try {
|
||||
auto req = registry->unsolicitated_report();
|
||||
if (req.has_value()) {
|
||||
server->send_unsolicitated_report(req.value(), std::chrono::seconds(3));
|
||||
}
|
||||
} catch (modbus_server::transport_exceptions::ConnectionClosedException& e) {
|
||||
log.error << "Unsolicitated reporter noticed an closed connection; exiting...";
|
||||
break;
|
||||
} catch (std::runtime_error& e) {
|
||||
log.error << "Unsolicitated reporter thread error: " + std::string(e.what());
|
||||
break;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(unsolicited_report_mutex);
|
||||
unsolicited_report_cv.wait_for(lock, std::chrono::seconds(1));
|
||||
}
|
||||
|
||||
if (!is_stop_requested()) {
|
||||
psu_communication_state = DispenserPsuCommunicationState::FAILED;
|
||||
}
|
||||
|
||||
log.debug << "Unsolicitated reporter thread exiting";
|
||||
}
|
||||
|
||||
void Dispenser::goose_receiver_thread_run() {
|
||||
while (psu_communication_is_ok()) {
|
||||
auto p = eth_interface.receive_packet();
|
||||
if (!p.has_value()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
auto packet = p.value();
|
||||
|
||||
// we are only interested in GOOSE packets and there a other packets
|
||||
if (packet.ethertype != goose::frame::GOOSE_ETHERTYPE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// filter src, if the source mac is our own mac, we ignore the packet
|
||||
if (std::memcmp(packet.source, eth_interface.get_mac_address(), 6) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Settings tag, because it is lost during transmission
|
||||
packet.eth_802_1q_tag = 0x8100A000;
|
||||
|
||||
goose::frame::GoosePDU pdu;
|
||||
|
||||
bool decoded = false;
|
||||
|
||||
try {
|
||||
goose::frame::SecureGooseFrame frame(packet); // note: hmac is verified below (only if configured)
|
||||
pdu = frame.pdu;
|
||||
decoded = true;
|
||||
log.verbose << "Received secure goose frame";
|
||||
} catch (std::runtime_error& e) {
|
||||
log.verbose << "Could not parse goose frame as secure: " + std::string(e.what());
|
||||
}
|
||||
|
||||
if (this->dispenser_config.allow_unsecured_goose && !decoded) {
|
||||
try {
|
||||
goose::frame::GooseFrame frame(packet);
|
||||
pdu = frame.pdu;
|
||||
decoded = true;
|
||||
log.verbose << "Received non-secure goose frame";
|
||||
} catch (std::runtime_error& e) {
|
||||
log.verbose << "Could not parse goose frame as non-secure: " + std::string(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
if (!decoded) {
|
||||
log.warning << "Received frame could not be decoded";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(pdu.go_id, "CC/0$GO$PowerRequestReply") != 0) {
|
||||
log.info << "Received goose frame with weird go_id: " + std::string(pdu.go_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
fusion_charger::goose::PowerRequirementResponse response;
|
||||
response.from_pdu(pdu);
|
||||
|
||||
bool corresponding_connector_found = false;
|
||||
for (auto& c : connectors) {
|
||||
if (c->connector_config.global_connector_number == response.charging_connector_no) {
|
||||
corresponding_connector_found = true;
|
||||
|
||||
// verify hmac if configured
|
||||
if (dispenser_config.verify_secure_goose_hmac) {
|
||||
try {
|
||||
goose::frame::SecureGooseFrame secure_frame(packet, c->get_hmac_key());
|
||||
log.verbose << "HMAC verified for secure goose frame";
|
||||
} catch (std::exception& e) {
|
||||
log.error << "Received secure goose frame, but HMAC verification "
|
||||
"failed: " +
|
||||
std::string(e.what());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
c->on_module_placeholder_allocation_response(
|
||||
response.result == fusion_charger::goose::PowerRequirementResponse::Result::SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
if (!corresponding_connector_found) {
|
||||
log.verbose << "Received module replacement goose frame but charging "
|
||||
"connector no is wrong!";
|
||||
}
|
||||
}
|
||||
|
||||
log.debug << "Goose receiver thread exiting";
|
||||
}
|
||||
|
||||
void Dispenser::modbus_event_loop_thread_run() {
|
||||
auto timeout = std::chrono::steady_clock::now() + dispenser_config.modbus_timeout_ms;
|
||||
|
||||
try {
|
||||
while (psu_communication_is_ok()) {
|
||||
bool data_received = pcl->poll();
|
||||
if (data_received) {
|
||||
timeout = std::chrono::steady_clock::now() + dispenser_config.modbus_timeout_ms;
|
||||
} else {
|
||||
if (timeout < std::chrono::steady_clock::now()) {
|
||||
throw std::runtime_error("No Modbus data received for " +
|
||||
std::to_string(dispenser_config.modbus_timeout_ms.count()) + " ms");
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
} catch (modbus_server::transport_exceptions::ConnectionClosedException& e) {
|
||||
log.error << "Poll thread noticed an closed connection; exiting... Error: " + std::string(e.what());
|
||||
} catch (modbus_ssl::OpenSSLTransportException& e) {
|
||||
log.error << "Poll thread noticed an OpenSSL error; exiting... Error: " + std::string(e.what());
|
||||
} catch (std::runtime_error& e) {
|
||||
log.error << "Poll thread error: " + std::string(e.what());
|
||||
} catch (...) {
|
||||
log.error << "Poll thread error: unknown";
|
||||
}
|
||||
|
||||
if (!is_stop_requested()) {
|
||||
psu_communication_state = DispenserPsuCommunicationState::FAILED;
|
||||
}
|
||||
|
||||
log.debug << "Poll thread exiting";
|
||||
}
|
||||
|
||||
Dispenser::Dispenser(DispenserConfig dispenser_config, std::vector<ConnectorConfig> connector_configs,
|
||||
logs::LogIntf log) :
|
||||
log(log),
|
||||
dispenser_config(dispenser_config),
|
||||
connector_configs(connector_configs),
|
||||
eth_interface(dispenser_config.eth_interface.c_str()) {
|
||||
if (connector_configs.size() > MAX_NUMBER_OF_CONNECTORS) {
|
||||
throw std::runtime_error("Too many connectors: " + std::to_string(connector_configs.size()) +
|
||||
"Max: " + std::to_string(MAX_NUMBER_OF_CONNECTORS));
|
||||
}
|
||||
|
||||
for (const auto& dispenser_alarm : get_all_dispenser_alarms()) {
|
||||
dispenser_alarms[dispenser_alarm] = false;
|
||||
}
|
||||
|
||||
// number connectors from 1 to n
|
||||
for (size_t local_connector_number = 1; local_connector_number <= connector_configs.size();
|
||||
local_connector_number++) {
|
||||
std::shared_ptr<Connector> connector =
|
||||
std::make_shared<Connector>(connector_configs[local_connector_number - 1], local_connector_number,
|
||||
dispenser_config, log, [this]() { trigger_unsolicited_report(); });
|
||||
connectors.push_back(connector);
|
||||
}
|
||||
}
|
||||
|
||||
void Dispenser::start() {
|
||||
if (modbus_socket.has_value()) {
|
||||
stop();
|
||||
}
|
||||
|
||||
psu_communication_state = DispenserPsuCommunicationState::INITIALIZING;
|
||||
|
||||
modbus_event_loop_thread = std::thread([this]() {
|
||||
try {
|
||||
init();
|
||||
update_psu_communication_state();
|
||||
|
||||
for (auto& c : connectors) {
|
||||
c->start();
|
||||
}
|
||||
|
||||
modbus_event_loop_thread_run();
|
||||
} catch (std::runtime_error& e) {
|
||||
if (!is_stop_requested()) {
|
||||
log.error << "Error initializing: " + std::string(e.what());
|
||||
psu_communication_state = DispenserPsuCommunicationState::FAILED;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Dispenser::stop() {
|
||||
psu_communication_state = DispenserPsuCommunicationState::UNINITIALIZED;
|
||||
|
||||
if (modbus_unsolicitated_event_thread.has_value()) {
|
||||
modbus_unsolicitated_event_thread->join();
|
||||
modbus_unsolicitated_event_thread = std::nullopt;
|
||||
log.verbose << "Modbus unsolicitated event thread joined";
|
||||
}
|
||||
|
||||
if (modbus_event_loop_thread.has_value()) {
|
||||
modbus_event_loop_thread->join();
|
||||
modbus_event_loop_thread = std::nullopt;
|
||||
log.verbose << "Modbus event loop thread joined";
|
||||
}
|
||||
|
||||
if (goose_receiver_thread.has_value()) {
|
||||
goose_receiver_thread->join();
|
||||
goose_receiver_thread = std::nullopt;
|
||||
log.verbose << "Goose receiver thread joined";
|
||||
}
|
||||
|
||||
for (auto& c : connectors) {
|
||||
c->stop();
|
||||
}
|
||||
|
||||
if (modbus_socket.has_value()) {
|
||||
if (close(modbus_socket.value()) != 0) {
|
||||
log.error << "Could not close modbus socket";
|
||||
};
|
||||
modbus_socket.reset();
|
||||
log.verbose << "Modbus socket closed";
|
||||
}
|
||||
|
||||
if (this->openssl_data.has_value()) {
|
||||
tls_util::free_ssl(this->openssl_data.value());
|
||||
this->openssl_data.reset();
|
||||
}
|
||||
|
||||
this->psu_running_mode.reset();
|
||||
|
||||
this->server.reset();
|
||||
this->pcl.reset();
|
||||
this->protocol.reset();
|
||||
this->transport.reset();
|
||||
|
||||
this->registry.reset();
|
||||
this->dispenser_registers.reset();
|
||||
this->psu_registers.reset();
|
||||
this->error_registers.reset();
|
||||
}
|
||||
|
||||
const int Dispenser::do_connect(const char* ip, std::uint16_t port) {
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock < 0) {
|
||||
log.error << "Could not open modbus socket";
|
||||
throw std::runtime_error("Could not open modbus socket");
|
||||
}
|
||||
|
||||
struct sockaddr_in addr;
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = inet_addr(ip);
|
||||
|
||||
log.info << "Connecting to " + std::string(ip) + ":" + std::to_string(port);
|
||||
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
close(sock);
|
||||
log.error << "Could not connect to PSU";
|
||||
throw std::runtime_error("Could not connect to PSU");
|
||||
}
|
||||
log.info << "Connected to PSU via TCP";
|
||||
|
||||
return sock;
|
||||
}
|
||||
|
||||
const int Dispenser::connect_with_retry(const char* ip, std::uint16_t port, int retries) {
|
||||
for (int i = 0; i < retries; i++) {
|
||||
try {
|
||||
return do_connect(ip, port);
|
||||
} catch (std::runtime_error& e) {
|
||||
log.error << "Connection attempt " + std::to_string(i + 1) + " failed";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds((int)(10 * std::pow(2, i))));
|
||||
if (i == retries - 1) {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw std::runtime_error("Unreachable code when connecting to PSU");
|
||||
}
|
||||
|
||||
PSURunningMode Dispenser::get_psu_running_mode() {
|
||||
return psu_registers->psu_running_mode.get_value();
|
||||
}
|
||||
|
||||
void Dispenser::init() {
|
||||
log.info << "Using host, port and interface: " + dispenser_config.psu_host + ":" +
|
||||
std::to_string(dispenser_config.psu_port) + " % " + dispenser_config.eth_interface;
|
||||
|
||||
modbus_socket = connect_with_retry(dispenser_config.psu_host.c_str(), dispenser_config.psu_port, 10);
|
||||
|
||||
if (dispenser_config.tls_config.has_value()) {
|
||||
log.info << "Using TLS to connect to PSU";
|
||||
auto tls = dispenser_config.tls_config.value();
|
||||
try {
|
||||
auto openssl_data = tls_util::init_mutual_tls_client(modbus_socket.value(), tls);
|
||||
this->openssl_data = openssl_data;
|
||||
SSL* ssl = std::get<0>(openssl_data);
|
||||
|
||||
transport = std::make_shared<modbus_ssl::OpenSSLTransport>(ssl);
|
||||
} catch (std::runtime_error& e) {
|
||||
log.error << "Could not connect to PSU using TLS: " + std::string(e.what());
|
||||
throw;
|
||||
}
|
||||
|
||||
} else {
|
||||
log.info << "TLS not configured, using plain TCP to connect to PSU";
|
||||
transport = std::make_shared<modbus_server::ModbusSocketTransport>(modbus_socket.value());
|
||||
}
|
||||
|
||||
protocol = std::make_shared<modbus_server::ModbusTCPProtocol>(transport, 0, 0);
|
||||
pcl = std::make_shared<modbus_server::PDUCorrelationLayer>(protocol);
|
||||
server.emplace(pcl, log);
|
||||
|
||||
error_registers.emplace();
|
||||
|
||||
psu_registers.emplace();
|
||||
|
||||
DispenserRegistersConfig dispenser_registers_config;
|
||||
dispenser_registers_config.manufacturer = dispenser_config.manufacturer;
|
||||
dispenser_registers_config.model = dispenser_config.model;
|
||||
dispenser_registers_config.protocol_version = dispenser_config.protocol_version;
|
||||
dispenser_registers_config.hardware_version = dispenser_config.hardware_version;
|
||||
dispenser_registers_config.software_version = dispenser_config.software_version;
|
||||
dispenser_registers_config.esn = dispenser_config.esn;
|
||||
dispenser_registers_config.connector_count = dispenser_config.charging_connector_count;
|
||||
dispenser_registers_config.get_door_status_alarm = [this]() {
|
||||
return get_dispenser_alarm_state(DispenserAlarms::DOOR_STATUS_ALARM);
|
||||
};
|
||||
dispenser_registers_config.get_water_alarm = [this]() {
|
||||
return get_dispenser_alarm_state(DispenserAlarms::WATER_ALARM);
|
||||
};
|
||||
dispenser_registers_config.get_epo_alarm = [this]() {
|
||||
return get_dispenser_alarm_state(DispenserAlarms::EPO_ALARM);
|
||||
};
|
||||
dispenser_registers_config.get_tilt_alarm = [this]() {
|
||||
return get_dispenser_alarm_state(DispenserAlarms::TILT_ALARM);
|
||||
};
|
||||
|
||||
dispenser_registers.emplace(dispenser_registers_config);
|
||||
|
||||
// add received callbacks
|
||||
psu_registers->psu_mac.add_write_callback([this](const std::uint8_t* value) { this->psu_mac_received = true; });
|
||||
|
||||
// Callbacks for common power unit registers
|
||||
psu_registers->manufacturer.add_write_callback([this](std::uint16_t value) {
|
||||
log.debug << "Dispenser : PSU Manufacturer changed to " + std::to_string(value);
|
||||
});
|
||||
psu_registers->protocol_version.add_write_callback([this](std::uint16_t value) {
|
||||
log.debug << "Dispenser : PSU Protocol version changed to " + std::to_string(value);
|
||||
});
|
||||
psu_registers->esn_control_board.add_write_callback(
|
||||
[this](const std::string& value) { log.debug << "Dispenser : PSU ESN Control Board changed to " + value; });
|
||||
psu_registers->software_version.add_write_callback(
|
||||
[this](const std::string& value) { log.debug << "Dispenser : PSU Software version changed to " + value; });
|
||||
psu_registers->hardware_version.add_write_callback(
|
||||
[this](std::uint16_t val) { log.debug << "Dispenser : PSU HW version changed to " + std::to_string(val); });
|
||||
|
||||
psu_registers->psu_running_mode.add_write_callback([this](SettingPowerUnitRegisters::PSURunningMode new_value) {
|
||||
if (psu_running_mode.has_value() and psu_running_mode.value() == new_value) {
|
||||
return; // no change
|
||||
}
|
||||
|
||||
psu_running_mode = new_value;
|
||||
log.info << "Dispenser : PSU Running mode changed to " +
|
||||
SettingPowerUnitRegisters::psu_running_mode_to_string(new_value);
|
||||
});
|
||||
|
||||
psu_registers->psu_mac.add_write_callback([this](const std::uint8_t* value) {
|
||||
char mac_str[18];
|
||||
sprintf(mac_str, "%02X:%02X:%02X:%02X:%02X:%02X", value[0], value[1], value[2], value[3], value[4], value[5]);
|
||||
log.debug << "Dispenser : 🍔 PSU (Big) MAC changed to " + std::string(mac_str);
|
||||
|
||||
auto mac = std::vector<std::uint8_t>(value, value + 6);
|
||||
|
||||
for (auto& c : connectors) {
|
||||
c->on_psu_mac_change(mac);
|
||||
}
|
||||
|
||||
update_psu_communication_state();
|
||||
});
|
||||
|
||||
registry.emplace(UnsolicitatedRegistry());
|
||||
|
||||
error_registers->add_to_registry(registry.value());
|
||||
error_registers->add_callback([this](ErrorEvent event) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->raised_error_mutex);
|
||||
|
||||
if (event.payload.is_error()) {
|
||||
if (raised_errors.find(event) != raised_errors.end()) {
|
||||
raised_errors.erase(event);
|
||||
}
|
||||
raised_errors.insert(event);
|
||||
} else {
|
||||
if (raised_errors.find(event) != raised_errors.end()) {
|
||||
raised_errors.erase(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dispenser_registers->add_to_registry(registry.value());
|
||||
psu_registers->add_to_registry(registry.value());
|
||||
|
||||
for (auto& c : connectors) {
|
||||
c->connector_registers.add_to_registry(registry.value());
|
||||
}
|
||||
|
||||
registry->verify_overlap();
|
||||
|
||||
server->set_read_holding_registers_request_cb([this](const modbus_server::pdu::ReadHoldingRegistersRequest& req) {
|
||||
auto data = registry->on_read(req.register_start, req.register_count);
|
||||
return modbus_server::pdu::ReadHoldingRegistersResponse(req, data);
|
||||
});
|
||||
server->set_write_multiple_registers_request_cb(
|
||||
[this](const modbus_server::pdu::WriteMultipleRegistersRequest& req) {
|
||||
registry->on_write(req.register_start, req.register_data);
|
||||
return modbus_server::pdu::WriteMultipleRegistersResponse(req);
|
||||
});
|
||||
server->set_write_single_register_request_cb([this](const modbus_server::pdu::WriteSingleRegisterRequest& req) {
|
||||
registry->on_write(req.register_address,
|
||||
{(std::uint8_t)(req.register_value >> 8), (std::uint8_t)(req.register_value & 0xff)});
|
||||
return modbus_server::pdu::WriteSingleRegisterResponse(req);
|
||||
});
|
||||
|
||||
modbus_unsolicitated_event_thread = std::thread([this]() { modbus_unsolicitated_event_thread_run(); });
|
||||
|
||||
goose_receiver_thread = std::thread([this]() { goose_receiver_thread_run(); });
|
||||
|
||||
// add telemetry callbacks
|
||||
dispenser_config.telemetry_publisher->add_subtopic("psu");
|
||||
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider("psu", "ac_input_voltage_a",
|
||||
&psu_registers->ac_input_voltage_a);
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider("psu", "ac_input_voltage_b",
|
||||
&psu_registers->ac_input_voltage_b);
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider("psu", "ac_input_voltage_c",
|
||||
&psu_registers->ac_input_voltage_c);
|
||||
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider("psu", "ac_input_current_a",
|
||||
&psu_registers->ac_input_current_a);
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider("psu", "ac_input_current_b",
|
||||
&psu_registers->ac_input_current_b);
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider("psu", "ac_input_current_c",
|
||||
&psu_registers->ac_input_current_c);
|
||||
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider_enum<PSURunningMode>(
|
||||
"psu", "psu_running_mode", &psu_registers->psu_running_mode,
|
||||
[](const PSURunningMode& mode) { return SettingPowerUnitRegisters::psu_running_mode_to_string(mode); });
|
||||
|
||||
dispenser_config.telemetry_publisher->register_complex_register_data_provider<double>(
|
||||
"psu", "total_historic_input_energy", &psu_registers->total_historic_input_energy,
|
||||
[](const double& kwh) { return kwh * 1000.0; });
|
||||
|
||||
// publish alarms
|
||||
dispenser_config.telemetry_publisher->add_subtopic(DISPENSER_TELEMETRY_ALARMS_SUBTOPIC);
|
||||
|
||||
for (auto alarm : get_all_dispenser_alarms()) {
|
||||
dispenser_config.telemetry_publisher->initialize_datapoint(
|
||||
DISPENSER_TELEMETRY_ALARMS_SUBTOPIC, dispenser_alarm_to_telemetry_datapoint(alarm), false);
|
||||
}
|
||||
}
|
||||
|
||||
void Dispenser::update_psu_communication_state() {
|
||||
if (!psu_mac_received) {
|
||||
return;
|
||||
}
|
||||
// todo: do we have to check whether received mac is "valid"?
|
||||
|
||||
psu_communication_state = DispenserPsuCommunicationState::READY;
|
||||
}
|
||||
|
||||
DispenserPsuCommunicationState Dispenser::get_psu_communication_state() {
|
||||
return psu_communication_state.load();
|
||||
}
|
||||
|
||||
ErrorEventSet Dispenser::get_raised_errors() {
|
||||
std::lock_guard<std::mutex> lock(raised_error_mutex);
|
||||
return raised_errors;
|
||||
}
|
||||
|
||||
bool Dispenser::psu_communication_is_ok() {
|
||||
auto current_state = psu_communication_state.load();
|
||||
return current_state == DispenserPsuCommunicationState::INITIALIZING ||
|
||||
current_state == DispenserPsuCommunicationState::READY;
|
||||
}
|
||||
|
||||
bool Dispenser::is_stop_requested() {
|
||||
return psu_communication_state == DispenserPsuCommunicationState::UNINITIALIZED;
|
||||
}
|
||||
|
||||
Dispenser::~Dispenser() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void Dispenser::trigger_unsolicited_report() {
|
||||
std::lock_guard<std::mutex> lock(unsolicited_report_mutex);
|
||||
unsolicited_report_cv.notify_all();
|
||||
}
|
||||
|
||||
void Dispenser::set_dispenser_alarm(DispenserAlarms alarm, bool active) {
|
||||
dispenser_alarms[alarm] = active;
|
||||
|
||||
dispenser_config.telemetry_publisher->datapoint_changed(DISPENSER_TELEMETRY_ALARMS_SUBTOPIC,
|
||||
dispenser_alarm_to_telemetry_datapoint(alarm), active);
|
||||
|
||||
trigger_unsolicited_report();
|
||||
}
|
||||
|
||||
bool Dispenser::get_dispenser_alarm_state(DispenserAlarms alarm) {
|
||||
// note that dispenser_alarms[alarm] exists for all DispenserAlarms due to initialization in constructor
|
||||
return this->dispenser_alarms[alarm].load();
|
||||
}
|
||||
|
||||
std::string Dispenser::dispenser_alarm_to_telemetry_datapoint(DispenserAlarms alarm) {
|
||||
switch (alarm) {
|
||||
case DispenserAlarms::DOOR_STATUS_ALARM:
|
||||
return "door_status_alarm";
|
||||
case DispenserAlarms::WATER_ALARM:
|
||||
return "water_alarm";
|
||||
case DispenserAlarms::EPO_ALARM:
|
||||
return "epo_alarm";
|
||||
case DispenserAlarms::TILT_ALARM:
|
||||
return "tilt_alarm";
|
||||
}
|
||||
|
||||
throw std::runtime_error("Unknown dispenser alarm");
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "tls_util.hpp"
|
||||
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
|
||||
using namespace tls_util;
|
||||
|
||||
std::tuple<SSL*, SSL_CTX*> tls_util::init_mutual_tls_client(int socket, MutualTlsClientConfig config) {
|
||||
SSL_load_error_strings();
|
||||
SSL_library_init();
|
||||
OpenSSL_add_all_algorithms();
|
||||
|
||||
SSL_CTX* ctx = SSL_CTX_new(TLS_client_method());
|
||||
if (ctx == NULL) {
|
||||
throw std::runtime_error("SSL_CTX_new failed");
|
||||
}
|
||||
|
||||
if (SSL_CTX_use_certificate_chain_file(ctx, config.ca_cert.c_str()) != 1) {
|
||||
throw std::runtime_error("Could not load CA certificate: " + config.ca_cert);
|
||||
}
|
||||
if (SSL_CTX_load_verify_locations(ctx, config.ca_cert.c_str(), NULL) != 1) {
|
||||
throw std::runtime_error("Could not load CA certificate");
|
||||
}
|
||||
|
||||
printf("Client cert: %s\n", config.client_cert.c_str());
|
||||
if (SSL_CTX_use_certificate_file(ctx, config.client_cert.c_str(), SSL_FILETYPE_PEM) != 1) {
|
||||
throw std::runtime_error("Could not load client certificate");
|
||||
}
|
||||
|
||||
printf("Client key: %s\n", config.client_key.c_str());
|
||||
if (SSL_CTX_use_PrivateKey_file(ctx, config.client_key.c_str(), SSL_FILETYPE_PEM) != 1) {
|
||||
throw std::runtime_error("Could not load client key");
|
||||
}
|
||||
|
||||
if (!SSL_CTX_check_private_key(ctx)) {
|
||||
throw std::runtime_error("Private key invalid");
|
||||
}
|
||||
|
||||
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);
|
||||
|
||||
auto rc = BIO_socket_nbio(socket, 1);
|
||||
if (rc != 1) {
|
||||
throw std::runtime_error("BIO_socket_nbio failed");
|
||||
}
|
||||
|
||||
SSL* ssl = SSL_new(ctx);
|
||||
SSL_set_fd(ssl, socket);
|
||||
|
||||
bool has_worked = false;
|
||||
|
||||
do {
|
||||
auto error = SSL_connect(ssl);
|
||||
|
||||
if (error != 1) {
|
||||
auto error_code = SSL_get_error(ssl, error);
|
||||
// printf("SSL Error code: %d\n", error_code);
|
||||
|
||||
if (error_code == SSL_ERROR_WANT_READ) {
|
||||
std::this_thread::yield();
|
||||
// std::this_thread::sleep_for(std::chrono::microseconds(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (error_code == SSL_ERROR_WANT_WRITE) {
|
||||
std::this_thread::yield();
|
||||
// std::this_thread::sleep_for(std::chrono::microseconds(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
SSL_free(ssl);
|
||||
SSL_CTX_free(ctx);
|
||||
|
||||
throw std::runtime_error("Could not connect to server");
|
||||
}
|
||||
|
||||
has_worked = true;
|
||||
|
||||
} while (!has_worked);
|
||||
|
||||
return {ssl, ctx};
|
||||
}
|
||||
|
||||
std::tuple<SSL*, SSL_CTX*> tls_util::init_mutual_tls_server(int socket, MutualTlsServerConfig config) {
|
||||
SSL_load_error_strings();
|
||||
SSL_library_init();
|
||||
OpenSSL_add_all_algorithms();
|
||||
|
||||
SSL_CTX* ctx = SSL_CTX_new(TLS_server_method());
|
||||
|
||||
if (ctx == NULL) {
|
||||
throw std::runtime_error("SSL_CTX_new failed");
|
||||
}
|
||||
|
||||
if (SSL_CTX_use_certificate_chain_file(ctx, config.client_ca.c_str()) != 1) {
|
||||
throw std::runtime_error("Could not load CA certificate: " + config.client_ca);
|
||||
}
|
||||
|
||||
if (SSL_CTX_load_verify_locations(ctx, config.client_ca.c_str(), NULL) != 1) {
|
||||
throw std::runtime_error("Could not load CA certificate");
|
||||
}
|
||||
|
||||
if (SSL_CTX_use_certificate_file(ctx, config.server_cert.c_str(), SSL_FILETYPE_PEM) != 1) {
|
||||
throw std::runtime_error("Could not load server certificate");
|
||||
}
|
||||
|
||||
if (SSL_CTX_use_PrivateKey_file(ctx, config.server_key.c_str(), SSL_FILETYPE_PEM) != 1) {
|
||||
throw std::runtime_error("Could not load server key");
|
||||
}
|
||||
|
||||
if (!SSL_CTX_check_private_key(ctx)) {
|
||||
throw std::runtime_error("Private key invalid");
|
||||
}
|
||||
|
||||
auto rc = BIO_socket_nbio(socket, 1);
|
||||
if (rc != 1) {
|
||||
throw std::runtime_error("BIO_socket_nbio failed");
|
||||
}
|
||||
|
||||
SSL* ssl = SSL_new(ctx);
|
||||
SSL_set_fd(ssl, socket);
|
||||
|
||||
for (;;) {
|
||||
int ret = SSL_accept(ssl);
|
||||
|
||||
if (ret == 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
int err = SSL_get_error(ssl, ret);
|
||||
if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) {
|
||||
printf("TLS Connection failed\n");
|
||||
SSL_free(ssl);
|
||||
SSL_CTX_free(ctx);
|
||||
close(socket);
|
||||
|
||||
throw std::runtime_error("TLS Connection failed");
|
||||
}
|
||||
|
||||
std::this_thread::yield();
|
||||
}
|
||||
|
||||
return {ssl, ctx};
|
||||
}
|
||||
|
||||
void tls_util::free_ssl(std::tuple<SSL*, SSL_CTX*> ssl) {
|
||||
SSL_free(std::get<0>(ssl));
|
||||
SSL_CTX_free(std::get<1>(ssl));
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
file(GLOB_RECURSE LIB_SOURCES "lib/*.cpp")
|
||||
add_library(power_stack_mock_lib STATIC ${LIB_SOURCES})
|
||||
target_include_directories(power_stack_mock_lib PUBLIC include)
|
||||
|
||||
target_link_libraries(power_stack_mock_lib
|
||||
PUBLIC
|
||||
fusion_charger_dispenser
|
||||
modbus-ssl
|
||||
modbus-client
|
||||
atomic
|
||||
)
|
||||
|
||||
file (GLOB_RECURSE EXECUTABLE_SOURCES "src/*.cpp")
|
||||
|
||||
add_executable(fusion_charger_mock ${EXECUTABLE_SOURCES})
|
||||
target_link_libraries(fusion_charger_mock power_stack_mock_lib)
|
||||
target_link_libraries(fusion_charger_mock atomic mqttc)
|
||||
@@ -0,0 +1,162 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <cstdint>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <fusion_charger/goose/power_request.hpp>
|
||||
#include <goose-ethernet/driver.hpp>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "dispenser.hpp"
|
||||
#include "fusion_charger/goose/stop_charge_request.hpp"
|
||||
#include "fusion_charger/modbus/extensions/unsolicitated_report.hpp"
|
||||
#include "fusion_charger/modbus/registers/raw.hpp"
|
||||
#include "modbus-server/client.hpp"
|
||||
#include "modbus-server/frames.hpp"
|
||||
#include "modbus-server/pdu_correlation.hpp"
|
||||
#include "modbus-server/transport.hpp"
|
||||
#include "modbus-server/transport_protocol.hpp"
|
||||
#include "tls_util.hpp"
|
||||
|
||||
typedef fusion_charger::modbus_driver::raw_registers::SettingPowerUnitRegisters::PSURunningMode PSURunningMode;
|
||||
|
||||
struct DispenserInformation {
|
||||
std::uint16_t manufacturer;
|
||||
std::uint16_t model;
|
||||
std::uint16_t protocol_version;
|
||||
std::uint16_t hardware_version;
|
||||
std::string software_version;
|
||||
|
||||
bool operator==(const DispenserInformation& rhs) const;
|
||||
bool operator!=(const DispenserInformation& rhs) const;
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& os, const DispenserInformation& info);
|
||||
};
|
||||
|
||||
struct ConnectorCallbackResults {
|
||||
float connector_upstream_voltage;
|
||||
float output_voltage;
|
||||
float output_current;
|
||||
ContactorStatus contactor_status;
|
||||
ElectronicLockStatus electronic_lock_status;
|
||||
|
||||
bool operator==(const ConnectorCallbackResults& rhs) const;
|
||||
bool operator!=(const ConnectorCallbackResults& rhs) const;
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& os, const ConnectorCallbackResults& results);
|
||||
};
|
||||
|
||||
struct PowerStackMockConfig {
|
||||
std::string eth;
|
||||
std::uint16_t port;
|
||||
std::uint8_t hmac_key[48];
|
||||
bool enable_hmac = true; // if true sign goose frames with hmac key
|
||||
bool verify_hmac = true; // if true verify received goose frames with hmac key
|
||||
|
||||
std::optional<tls_util::MutualTlsServerConfig> tls_config;
|
||||
|
||||
std::function<void(const fusion_charger::goose::PowerRequirementRequest&)> power_requirement_request_callback;
|
||||
std::function<void(const fusion_charger::goose::StopChargeRequest&)> stop_charge_request_callback;
|
||||
};
|
||||
|
||||
class PowerStackMock {
|
||||
public:
|
||||
static PowerStackMock* from_config(PowerStackMockConfig config);
|
||||
static PowerStackMock* from_config(PowerStackMockConfig config, int socket);
|
||||
~PowerStackMock();
|
||||
|
||||
void goose_receiver_thread_run();
|
||||
void stop();
|
||||
int client_socket();
|
||||
|
||||
void start_modbus_event_loop();
|
||||
void stop_modbus_event_loop();
|
||||
|
||||
std::vector<std::uint16_t> get_unsolicited_report_data(std::uint16_t start_address, std::uint16_t quantity);
|
||||
std::vector<std::uint16_t> read_registers(std::uint16_t start_address, std::uint16_t quantity);
|
||||
void write_registers(std::uint16_t start_address, const std::vector<std::uint16_t>& values);
|
||||
|
||||
void set_psu_running_mode(PSURunningMode mode);
|
||||
void send_mac_address();
|
||||
void send_hmac_key(std::uint16_t local_connector_number);
|
||||
void send_max_rated_current_of_output_port(float current, std::uint16_t local_connector_number);
|
||||
void send_min_rated_current_of_output_port(float current, std::uint16_t local_connector_number);
|
||||
void send_max_rated_voltage_of_output_port(float voltage, std::uint16_t local_connector_number);
|
||||
void send_min_rated_voltage_of_output_port(float voltage, std::uint16_t local_connector_number);
|
||||
void send_rated_power_of_output_port(float power, std::uint16_t local_connector_number);
|
||||
void send_total_historical_ac_input_energy(double energy);
|
||||
void send_ac_input_voltages_currents(float voltage_a, float voltage_b, float voltage_c, float current_a,
|
||||
float current_b, float current_c);
|
||||
void send_port_available(bool available, std::uint16_t local_connector_number);
|
||||
|
||||
std::optional<fusion_charger::goose::PowerRequirementRequest>
|
||||
get_last_power_requirement_request(std::uint16_t global_connector_number);
|
||||
std::uint32_t get_power_requirements_counter(std::uint16_t global_connector_number);
|
||||
std::optional<fusion_charger::goose::StopChargeRequest>
|
||||
get_last_stop_charge_request(std::uint16_t global_connector_number);
|
||||
std::uint32_t get_stop_charge_request_counter(std::uint16_t global_connector_number);
|
||||
fusion_charger::modbus_driver::raw_registers::ConnectionStatus
|
||||
get_connection_status(std::uint16_t local_connector_number);
|
||||
float get_maximum_rated_charge_current(std::uint16_t local_connector_number);
|
||||
std::optional<fusion_charger::modbus_driver::raw_registers::WorkingStatus>
|
||||
get_working_status(std::uint16_t local_connector_number);
|
||||
DispenserInformation get_dispenser_information();
|
||||
std::string get_dispenser_esn();
|
||||
std::uint32_t get_utc_time();
|
||||
|
||||
ConnectorCallbackResults get_connector_callback_values(std::uint16_t local_connector_number);
|
||||
|
||||
void set_enable_answer_module_placeholder_allocation(bool enable);
|
||||
|
||||
/**
|
||||
* @brief get the global connector number from the local connector number (range 1-4)
|
||||
* @returns the global connector number or std::nullopt if not found / error reading
|
||||
*/
|
||||
std::optional<int> get_global_connector_number_from_local(std::uint16_t local_connector_number);
|
||||
|
||||
private:
|
||||
PowerStackMockConfig config;
|
||||
goose_ethernet::EthernetInterface eth;
|
||||
|
||||
std::unordered_map<std::uint16_t, fusion_charger::modbus_extensions::UnsolicitatedReportRequest::Segment>
|
||||
pdu_registers;
|
||||
std::mutex pdu_registers_mutex;
|
||||
|
||||
// Keep the order of the elements, as this determines the order of the
|
||||
// initialization
|
||||
|
||||
int client_sock;
|
||||
std::optional<std::tuple<SSL*, SSL_CTX*>> openssl_data;
|
||||
std::shared_ptr<modbus_server::ModbusTransport> transport;
|
||||
std::shared_ptr<modbus_server::ModbusTCPProtocol> protocol;
|
||||
std::shared_ptr<modbus_server::PDUCorrelationLayer> pas;
|
||||
modbus_server::client::ModbusClient client;
|
||||
|
||||
std::optional<std::thread> modbus_event_loop;
|
||||
std::vector<std::uint16_t> read_and_check(std::uint16_t start_address, std::uint16_t quantity);
|
||||
|
||||
std::atomic<bool> running = true;
|
||||
std::atomic<bool> answer_module_placeholder_allocation = true;
|
||||
std::thread goose_receiver_thread;
|
||||
|
||||
std::map<std::uint16_t, std::atomic<fusion_charger::goose::PowerRequirementRequest>>
|
||||
last_power_requirement_requests;
|
||||
std::map<std::uint16_t, std::atomic<std::uint32_t>> power_requirement_request_counter = {};
|
||||
std::map<std::uint16_t, std::atomic<fusion_charger::goose::StopChargeRequest>> last_stop_charge_requests;
|
||||
std::map<std::uint16_t, std::atomic<std::uint32_t>> stop_charge_request_counter = {};
|
||||
|
||||
void on_pdu(const modbus_server::pdu::GenericPDU& pdu);
|
||||
|
||||
static int open_socket(std::uint16_t port);
|
||||
float registers_to_float(std::vector<std::uint16_t> registers);
|
||||
|
||||
PowerStackMock(int client_socket, std::optional<std::tuple<SSL*, SSL_CTX*>> openssl_data,
|
||||
std::shared_ptr<modbus_server::ModbusTransport> transport, PowerStackMockConfig config);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
namespace user_acceptance_tests {
|
||||
namespace test_utils {
|
||||
|
||||
void psu_printf(const char* fmt, ...);
|
||||
void tester_printf(const char* fmt, ...);
|
||||
|
||||
void fail_printf(const char* fmt, ...);
|
||||
void vfail_printf(const char* fmt, va_list args);
|
||||
|
||||
void dispenser_printf(const char* fmt, ...);
|
||||
|
||||
void fdispenser_printf(std::ostream& stream, const char* fmt, ...);
|
||||
|
||||
float uint16_vec_to_float(std::vector<std::uint16_t> vec);
|
||||
std::vector<std::uint16_t> float_to_uint16_vec(float value);
|
||||
|
||||
double uint16_vec_to_double(std::vector<std::uint16_t> vec);
|
||||
std::vector<std::uint16_t> double_to_uint16_vec(double value);
|
||||
|
||||
std::uint32_t uint16_vec_to_uint32(std::vector<std::uint16_t> vec);
|
||||
|
||||
} // namespace test_utils
|
||||
|
||||
} // namespace user_acceptance_tests
|
||||
@@ -0,0 +1,597 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "power_stack_mock/power_stack_mock.hpp"
|
||||
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <modbus-server/client.hpp>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "fusion_charger/goose/power_request.hpp"
|
||||
#include "fusion_charger/modbus/registers/raw.hpp"
|
||||
#include "goose/frame.hpp"
|
||||
#include "modbus-ssl/openssl_transport.hpp"
|
||||
#include "power_stack_mock/util.hpp"
|
||||
|
||||
using namespace user_acceptance_tests::test_utils;
|
||||
|
||||
using fusion_charger::modbus_driver::raw_registers::offset_from_connector_number;
|
||||
|
||||
bool DispenserInformation::operator==(const DispenserInformation& rhs) const {
|
||||
return manufacturer == rhs.manufacturer && model == rhs.model && protocol_version == rhs.protocol_version &&
|
||||
hardware_version == rhs.hardware_version && software_version == rhs.software_version;
|
||||
}
|
||||
|
||||
bool DispenserInformation::operator!=(const DispenserInformation& rhs) const {
|
||||
return !(*this == rhs);
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const DispenserInformation& info) {
|
||||
os << "DispenserInformation{" << std::endl;
|
||||
os << " manufacturer: " << info.manufacturer << std::endl;
|
||||
os << " model: " << info.model << std::endl;
|
||||
os << " protocol_version: " << info.protocol_version << std::endl;
|
||||
os << " hardware_version: " << info.hardware_version << std::endl;
|
||||
os << " software_version: " << info.software_version << std::endl;
|
||||
os << "}";
|
||||
return os;
|
||||
}
|
||||
|
||||
bool ConnectorCallbackResults::operator==(const ConnectorCallbackResults& rhs) const {
|
||||
return connector_upstream_voltage == rhs.connector_upstream_voltage && output_voltage == rhs.output_voltage &&
|
||||
output_current == rhs.output_current && contactor_status == rhs.contactor_status &&
|
||||
electronic_lock_status == rhs.electronic_lock_status;
|
||||
}
|
||||
|
||||
bool ConnectorCallbackResults::operator!=(const ConnectorCallbackResults& rhs) const {
|
||||
return !(*this == rhs);
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const ConnectorCallbackResults& results) {
|
||||
os << "ConnectorCallbackResults{" << std::endl;
|
||||
os << "connector_upstream_voltage: " << results.connector_upstream_voltage << std::endl;
|
||||
os << "output_voltage: " << results.output_voltage << std::endl;
|
||||
os << "output_current: " << results.output_current << std::endl;
|
||||
os << "contactor_status: " << (std::uint32_t)results.contactor_status << std::endl;
|
||||
os << "electronic_lock_status: " << (std::uint32_t)results.electronic_lock_status << std::endl;
|
||||
os << "}";
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
PowerStackMock::PowerStackMock(int client_socket, std::optional<std::tuple<SSL*, SSL_CTX*>> openssl_data,
|
||||
std::shared_ptr<modbus_server::ModbusTransport> transport, PowerStackMockConfig config) :
|
||||
client_sock(client_socket),
|
||||
openssl_data(openssl_data),
|
||||
transport(transport),
|
||||
protocol(std::make_shared<modbus_server::ModbusTCPProtocol>(transport)),
|
||||
pas(std::make_shared<modbus_server::PDUCorrelationLayer>(protocol)),
|
||||
client(pas),
|
||||
config(config),
|
||||
eth(goose_ethernet::EthernetInterface(config.eth.c_str())) {
|
||||
pas->set_on_pdu([this](const modbus_server::pdu::GenericPDU& pdu) {
|
||||
this->on_pdu(pdu);
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
goose_receiver_thread = std::thread([this] { goose_receiver_thread_run(); });
|
||||
}
|
||||
|
||||
PowerStackMock::~PowerStackMock() {
|
||||
running = false;
|
||||
goose_receiver_thread.join();
|
||||
|
||||
if (openssl_data) {
|
||||
tls_util::free_ssl(openssl_data.value());
|
||||
}
|
||||
|
||||
close(client_sock);
|
||||
}
|
||||
|
||||
PowerStackMock* PowerStackMock::from_config(PowerStackMockConfig config) {
|
||||
int client_socket = open_socket(config.port);
|
||||
|
||||
return from_config(config, client_socket);
|
||||
}
|
||||
|
||||
PowerStackMock* PowerStackMock::from_config(PowerStackMockConfig config, int client_socket) {
|
||||
if (config.tls_config.has_value()) {
|
||||
auto openssl_data = init_mutual_tls_server(client_socket, config.tls_config.value());
|
||||
SSL* ssl = std::get<0>(openssl_data);
|
||||
return new PowerStackMock(client_socket, openssl_data, std::make_shared<modbus_ssl::OpenSSLTransport>(ssl),
|
||||
config);
|
||||
}
|
||||
|
||||
return new PowerStackMock(client_socket, std::nullopt,
|
||||
std::make_shared<modbus_server::ModbusSocketTransport>(client_socket), config);
|
||||
}
|
||||
|
||||
std::vector<std::uint16_t> PowerStackMock::read_and_check(std::uint16_t start_address, std::uint16_t quantity) {
|
||||
auto registers = client.read_holding_registers(start_address, quantity);
|
||||
if (registers.size() != quantity) {
|
||||
fail_printf("Holding register at 0x%X, reading %d registers returned vector "
|
||||
"of length: %d",
|
||||
start_address, quantity, registers.size());
|
||||
}
|
||||
|
||||
return registers;
|
||||
}
|
||||
|
||||
std::unique_ptr<goose::frame::GooseFrameIntf>
|
||||
parse_goose_frame(const goose_ethernet::EthernetFrame& frame,
|
||||
std::optional<std::vector<std::uint8_t>> hmac_key = std::nullopt) {
|
||||
// if hmac_key is provided, only allow secure goose frames
|
||||
if (hmac_key.has_value()) {
|
||||
return std::make_unique<goose::frame::SecureGooseFrame>(frame, hmac_key.value());
|
||||
}
|
||||
|
||||
try {
|
||||
return std::make_unique<goose::frame::SecureGooseFrame>(frame);
|
||||
} catch (std::exception& e) {
|
||||
return std::make_unique<goose::frame::GooseFrame>(frame);
|
||||
}
|
||||
}
|
||||
|
||||
void PowerStackMock::goose_receiver_thread_run() {
|
||||
while (running) {
|
||||
auto p = eth.receive_packet();
|
||||
if (!p.has_value()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto eth_mac = eth.get_mac_address();
|
||||
if (memcmp(p.value().destination, eth_mac, 6) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (p.value().ethertype != goose::frame::GOOSE_ETHERTYPE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto packet = p.value();
|
||||
// Settings tag, because it is lost during transmission
|
||||
packet.eth_802_1q_tag = 0x8100A000;
|
||||
std::unique_ptr<goose::frame::GooseFrameIntf> frame;
|
||||
try {
|
||||
if (config.verify_hmac) {
|
||||
frame = parse_goose_frame(packet, std::vector<std::uint8_t>(config.hmac_key, config.hmac_key + 48));
|
||||
} else {
|
||||
frame = parse_goose_frame(packet);
|
||||
}
|
||||
} catch (std::runtime_error& e) {
|
||||
fail_printf("Could not parse goose frame as secure: %s", e.what());
|
||||
continue;
|
||||
}
|
||||
|
||||
goose::frame::GoosePDU pdu = frame->pdu;
|
||||
|
||||
if (strcmp(pdu.go_id, "CC/0$GO$PowerRequest") == 0) {
|
||||
fusion_charger::goose::PowerRequirementRequest new_request;
|
||||
new_request.from_pdu(pdu);
|
||||
if (config.power_requirement_request_callback) {
|
||||
config.power_requirement_request_callback(new_request);
|
||||
}
|
||||
auto global_connector_number = new_request.charging_connector_no;
|
||||
|
||||
if (power_requirement_request_counter.find(global_connector_number) ==
|
||||
power_requirement_request_counter.end()) {
|
||||
power_requirement_request_counter[global_connector_number] = 0;
|
||||
}
|
||||
power_requirement_request_counter[global_connector_number] += 1;
|
||||
|
||||
if (new_request.requirement_type == fusion_charger::goose::RequirementType::ModulePlaceholderRequest &&
|
||||
answer_module_placeholder_allocation) {
|
||||
printf("Received module placeholder request; sending answer\n");
|
||||
// send a reply
|
||||
fusion_charger::goose::PowerRequirementResponse response;
|
||||
response.charging_connector_no = new_request.charging_connector_no;
|
||||
response.charging_sn = new_request.charging_sn;
|
||||
response.requirement_type = new_request.requirement_type;
|
||||
response.mode = new_request.mode;
|
||||
response.voltage = new_request.voltage;
|
||||
response.current = new_request.current;
|
||||
response.result = fusion_charger::goose::PowerRequirementResponse::Result::SUCCESS;
|
||||
|
||||
goose::frame::GoosePDU response_pdu = response.to_pdu();
|
||||
std::unique_ptr<goose::frame::GooseFrameIntf> response_frame;
|
||||
if (config.enable_hmac) {
|
||||
response_frame = std::make_unique<goose::frame::SecureGooseFrame>();
|
||||
} else {
|
||||
response_frame = std::make_unique<goose::frame::GooseFrame>();
|
||||
}
|
||||
memcpy(response_frame->destination_mac_address, frame->source_mac_address, 6);
|
||||
memcpy(response_frame->source_mac_address, eth.get_mac_address(), 6);
|
||||
response_frame->vlan_id = 0;
|
||||
response_frame->priority = 5;
|
||||
response_frame->appid[0] = 0x30;
|
||||
response_frame->appid[1] = 0x01;
|
||||
response_frame->pdu = response_pdu;
|
||||
|
||||
goose_ethernet::EthernetFrame frame;
|
||||
if (config.enable_hmac) {
|
||||
std::vector<std::uint8_t> hmac_key(config.hmac_key, config.hmac_key + 48);
|
||||
frame = ((goose::frame::SecureGooseFrame*)response_frame.get())->serialize(hmac_key);
|
||||
} else {
|
||||
frame = ((goose::frame::GooseFrame*)response_frame.get())->serialize();
|
||||
}
|
||||
eth.send_packet(frame);
|
||||
}
|
||||
|
||||
last_power_requirement_requests[global_connector_number] = new_request;
|
||||
}
|
||||
|
||||
if (strcmp(pdu.go_id, "CC/0$GO$ShutdownRequest") == 0) {
|
||||
fusion_charger::goose::StopChargeRequest new_request;
|
||||
new_request.from_pdu(pdu);
|
||||
if (config.stop_charge_request_callback) {
|
||||
config.stop_charge_request_callback(new_request);
|
||||
}
|
||||
auto global_connector_number = new_request.charging_connector_no;
|
||||
|
||||
if (stop_charge_request_counter.find(global_connector_number) == stop_charge_request_counter.end()) {
|
||||
stop_charge_request_counter[global_connector_number] = 0;
|
||||
}
|
||||
stop_charge_request_counter[global_connector_number] += 1;
|
||||
|
||||
last_stop_charge_requests[global_connector_number] = new_request;
|
||||
}
|
||||
}
|
||||
|
||||
psu_printf("Exiting Goose Receiver Thread");
|
||||
}
|
||||
|
||||
void PowerStackMock::start_modbus_event_loop() {
|
||||
if (modbus_event_loop.has_value()) {
|
||||
fail_printf("Modbus event loop already started");
|
||||
return;
|
||||
}
|
||||
|
||||
modbus_event_loop = std::thread([this]() {
|
||||
psu_printf("Started: Modbus event loop");
|
||||
try {
|
||||
while (running) {
|
||||
bool poll = pas->poll();
|
||||
if (!poll) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
fail_printf("Exception in event loop: %s", e.what());
|
||||
}
|
||||
|
||||
running = false;
|
||||
});
|
||||
}
|
||||
|
||||
void PowerStackMock::stop_modbus_event_loop() {
|
||||
if (!modbus_event_loop.has_value() || !modbus_event_loop->joinable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
running = false;
|
||||
|
||||
modbus_event_loop->join();
|
||||
}
|
||||
|
||||
void PowerStackMock::stop() {
|
||||
stop_modbus_event_loop();
|
||||
close(client_sock);
|
||||
}
|
||||
|
||||
void PowerStackMock::on_pdu(const modbus_server::pdu::GenericPDU& pdu) {
|
||||
|
||||
fusion_charger::modbus_extensions::UnsolicitatedReportRequest req;
|
||||
req.from_generic(pdu);
|
||||
|
||||
auto segments = req.devices[0].segments;
|
||||
|
||||
std::lock_guard<std::mutex> guard(pdu_registers_mutex);
|
||||
|
||||
for (auto& segment : segments) {
|
||||
pdu_registers.insert_or_assign(segment.registers_start, segment);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint16_t> PowerStackMock::get_unsolicited_report_data(std::uint16_t start_address,
|
||||
std::uint16_t quantity) {
|
||||
std::lock_guard<std::mutex> guard(pdu_registers_mutex);
|
||||
|
||||
if (pdu_registers.find(start_address) == pdu_registers.end()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto segment = pdu_registers.at(start_address);
|
||||
|
||||
std::vector<std::uint16_t> registers;
|
||||
|
||||
if (segment.registers_count != quantity) {
|
||||
throw std::runtime_error("Expected " + std::to_string(quantity) + " registers, got " +
|
||||
std::to_string(segment.registers_count));
|
||||
}
|
||||
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
// convert to big endian
|
||||
registers.push_back(segment.registers[i * 2] << 8 | segment.registers[i * 2 + 1]);
|
||||
}
|
||||
|
||||
return registers;
|
||||
}
|
||||
|
||||
std::vector<std::uint16_t> PowerStackMock::read_registers(std::uint16_t start_address, std::uint16_t quantity) {
|
||||
return client.read_holding_registers(start_address, quantity);
|
||||
}
|
||||
|
||||
void PowerStackMock::write_registers(std::uint16_t start_address, const std::vector<std::uint16_t>& values) {
|
||||
client.write_multiple_registers(start_address, values);
|
||||
}
|
||||
|
||||
void PowerStackMock::set_psu_running_mode(PSURunningMode mode) {
|
||||
client.write_single_register(0x2006, static_cast<std::uint16_t>(mode));
|
||||
}
|
||||
|
||||
void PowerStackMock::send_mac_address() {
|
||||
auto address = eth.get_mac_address();
|
||||
|
||||
auto mac = std::vector<std::uint16_t>();
|
||||
for (int i = 0; i < 6; i += 2) {
|
||||
mac.push_back(address[i] << 8 | address[i + 1]);
|
||||
}
|
||||
|
||||
client.write_multiple_registers(0x2111, mac);
|
||||
}
|
||||
|
||||
void PowerStackMock::send_hmac_key(std::uint16_t local_connector_number) {
|
||||
std::vector<std::uint16_t> hmac_key_vec;
|
||||
|
||||
for (int i = 0; i < 48; i += 2) {
|
||||
hmac_key_vec.push_back((config.hmac_key[i] << 8) | (config.hmac_key[i + 1] & 0xFF));
|
||||
}
|
||||
|
||||
client.write_multiple_registers(
|
||||
0x2115 + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)), hmac_key_vec);
|
||||
}
|
||||
|
||||
void PowerStackMock::send_max_rated_current_of_output_port(float current, std::uint16_t local_connector_number) {
|
||||
client.write_multiple_registers(
|
||||
0x2102 + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)),
|
||||
float_to_uint16_vec(current));
|
||||
}
|
||||
|
||||
void PowerStackMock::send_min_rated_current_of_output_port(float current, std::uint16_t local_connector_number) {
|
||||
client.write_multiple_registers(
|
||||
0x2107 + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)),
|
||||
float_to_uint16_vec(current));
|
||||
}
|
||||
|
||||
void PowerStackMock::send_max_rated_voltage_of_output_port(float voltage, std::uint16_t local_connector_number) {
|
||||
client.write_multiple_registers(
|
||||
0x2100 + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)),
|
||||
float_to_uint16_vec(voltage));
|
||||
}
|
||||
|
||||
void PowerStackMock::send_min_rated_voltage_of_output_port(float voltage, std::uint16_t local_connector_number) {
|
||||
client.write_multiple_registers(
|
||||
0x2105 + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)),
|
||||
float_to_uint16_vec(voltage));
|
||||
}
|
||||
|
||||
void PowerStackMock::send_rated_power_of_output_port(float power, std::uint16_t local_connector_number) {
|
||||
client.write_multiple_registers(
|
||||
0x212D + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)),
|
||||
float_to_uint16_vec(power));
|
||||
}
|
||||
|
||||
void PowerStackMock::send_total_historical_ac_input_energy(double energy) {
|
||||
client.write_multiple_registers(0x2013, double_to_uint16_vec(energy));
|
||||
}
|
||||
|
||||
void PowerStackMock::send_ac_input_voltages_currents(float voltage_a, float voltage_b, float voltage_c, float current_a,
|
||||
float current_b, float current_c) {
|
||||
client.write_multiple_registers(0x2007, float_to_uint16_vec(voltage_a));
|
||||
client.write_multiple_registers(0x2009, float_to_uint16_vec(voltage_b));
|
||||
client.write_multiple_registers(0x200B, float_to_uint16_vec(voltage_c));
|
||||
client.write_multiple_registers(0x200D, float_to_uint16_vec(current_a));
|
||||
client.write_multiple_registers(0x200F, float_to_uint16_vec(current_b));
|
||||
client.write_multiple_registers(0x2011, float_to_uint16_vec(current_c));
|
||||
}
|
||||
|
||||
void PowerStackMock::send_port_available(bool available, std::uint16_t local_connector_number) {
|
||||
client.write_single_register(
|
||||
0x212F + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)), available ? 1 : 0);
|
||||
}
|
||||
|
||||
int PowerStackMock::open_socket(std::uint16_t port) {
|
||||
psu_printf("Waiting for modbus connection\n");
|
||||
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
bool is_true = true;
|
||||
// makes the socket-address reusable
|
||||
// if not set, socket may take some time to be cleaned up
|
||||
// making the application fail for a short period of time
|
||||
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &is_true, sizeof(int));
|
||||
struct sockaddr_in serv_addr;
|
||||
serv_addr.sin_family = AF_INET;
|
||||
serv_addr.sin_addr.s_addr = INADDR_ANY;
|
||||
serv_addr.sin_port = htons(port);
|
||||
int err = bind(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
|
||||
if (err < 0) {
|
||||
throw std::runtime_error("Failed to bind");
|
||||
}
|
||||
|
||||
err = listen(sock, 1);
|
||||
if (err < 0) {
|
||||
throw std::runtime_error("Failed to listen");
|
||||
}
|
||||
|
||||
printf("Accepting new connection\n");
|
||||
int client_sock = accept(sock, nullptr, nullptr);
|
||||
if (client_sock < 0) {
|
||||
fail_printf("Failed to accept with error: %d", errno);
|
||||
close(sock);
|
||||
|
||||
throw std::runtime_error("Failed to accept with error: " + std::to_string(errno));
|
||||
}
|
||||
// close the server socket, but keep the connection socket open
|
||||
close(sock);
|
||||
|
||||
return client_sock;
|
||||
}
|
||||
|
||||
std::optional<fusion_charger::goose::PowerRequirementRequest>
|
||||
PowerStackMock::get_last_power_requirement_request(std::uint16_t global_connector_number) {
|
||||
auto it = last_power_requirement_requests.find(global_connector_number);
|
||||
if (it != last_power_requirement_requests.end()) {
|
||||
return it->second;
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t PowerStackMock::get_power_requirements_counter(std::uint16_t global_connector_number) {
|
||||
auto it = power_requirement_request_counter.find(global_connector_number);
|
||||
if (it != power_requirement_request_counter.end()) {
|
||||
return it->second;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<fusion_charger::goose::StopChargeRequest>
|
||||
PowerStackMock::get_last_stop_charge_request(std::uint16_t global_connector_number) {
|
||||
auto it = last_stop_charge_requests.find(global_connector_number);
|
||||
if (it != last_stop_charge_requests.end()) {
|
||||
return it->second;
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t PowerStackMock::get_stop_charge_request_counter(std::uint16_t global_connector_number) {
|
||||
auto it = stop_charge_request_counter.find(global_connector_number);
|
||||
if (it != stop_charge_request_counter.end()) {
|
||||
return it->second;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
fusion_charger::modbus_driver::raw_registers::ConnectionStatus
|
||||
PowerStackMock::get_connection_status(std::uint16_t local_connector_number) {
|
||||
return static_cast<fusion_charger::modbus_driver::raw_registers::ConnectionStatus>(client.read_holding_registers(
|
||||
0x110D + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)), 1)[0]);
|
||||
}
|
||||
float PowerStackMock::get_maximum_rated_charge_current(std::uint16_t local_connector_number) {
|
||||
auto read_result = client.read_holding_registers(
|
||||
0x1105 + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)), 2);
|
||||
|
||||
return registers_to_float(read_result);
|
||||
}
|
||||
|
||||
std::optional<fusion_charger::modbus_driver::raw_registers::WorkingStatus>
|
||||
PowerStackMock::get_working_status(std::uint16_t local_connector_number) {
|
||||
auto read_result = client.read_holding_registers(
|
||||
0x110B + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)), 1);
|
||||
|
||||
if (read_result.size() != 1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return static_cast<fusion_charger::modbus_driver::raw_registers::WorkingStatus>(read_result[0]);
|
||||
}
|
||||
|
||||
DispenserInformation PowerStackMock::get_dispenser_information() {
|
||||
auto read_result = client.read_holding_registers(0x0000, 3);
|
||||
auto hardware_version = client.read_holding_registers(0x0004, 1)[0];
|
||||
auto software_version_registers = client.read_holding_registers(0x0013, 24);
|
||||
std::vector<std::uint8_t> software_version_bytes;
|
||||
for (auto reg : software_version_registers) {
|
||||
software_version_bytes.push_back(reg >> 8);
|
||||
software_version_bytes.push_back(reg & 0xFF);
|
||||
}
|
||||
|
||||
std::string software_version(software_version_bytes.begin(),
|
||||
std::find(software_version_bytes.begin(), software_version_bytes.end(), '\0'));
|
||||
|
||||
DispenserInformation dispenser_info;
|
||||
dispenser_info.manufacturer = read_result[0];
|
||||
dispenser_info.model = read_result[1];
|
||||
dispenser_info.protocol_version = read_result[2];
|
||||
dispenser_info.hardware_version = hardware_version;
|
||||
dispenser_info.software_version = software_version;
|
||||
return dispenser_info;
|
||||
}
|
||||
|
||||
std::string PowerStackMock::get_dispenser_esn() {
|
||||
auto registers = client.read_holding_registers(0x1016, 11);
|
||||
std::vector<std::uint8_t> bytes;
|
||||
for (auto reg : registers) {
|
||||
bytes.push_back(reg >> 8);
|
||||
bytes.push_back(reg & 0xFF);
|
||||
}
|
||||
|
||||
std::string esn(bytes.begin(), std::find(bytes.begin(), bytes.end(), '\0'));
|
||||
|
||||
return esn;
|
||||
}
|
||||
|
||||
std::uint32_t PowerStackMock::get_utc_time() {
|
||||
auto registers = client.read_holding_registers(0x1024, 2);
|
||||
|
||||
return (registers[0] << 16) | registers[1];
|
||||
}
|
||||
|
||||
ConnectorCallbackResults PowerStackMock::get_connector_callback_values(std::uint16_t local_connector_number) {
|
||||
auto output_voltage = registers_to_float(client.read_holding_registers(
|
||||
0x1107 + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)), 2));
|
||||
|
||||
auto output_current = registers_to_float(client.read_holding_registers(
|
||||
0x1109 + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)), 2));
|
||||
|
||||
auto contactors_upstream_voltage = registers_to_float(client.read_holding_registers(
|
||||
0x1113 + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)), 2));
|
||||
|
||||
auto contactors_status = (ContactorStatus)client.read_holding_registers(
|
||||
0x1154 + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)), 1)[0];
|
||||
|
||||
auto electronic_lock_status = (ElectronicLockStatus)client.read_holding_registers(
|
||||
0x1156 + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)), 1)[0];
|
||||
|
||||
ConnectorCallbackResults results;
|
||||
results.connector_upstream_voltage = contactors_upstream_voltage;
|
||||
results.output_voltage = output_voltage;
|
||||
results.output_current = output_current;
|
||||
results.contactor_status = contactors_status;
|
||||
results.electronic_lock_status = electronic_lock_status;
|
||||
return results;
|
||||
}
|
||||
|
||||
float PowerStackMock::registers_to_float(std::vector<std::uint16_t> registers) {
|
||||
std::uint16_t high_byte = registers[0];
|
||||
std::uint16_t low_byte = registers[1];
|
||||
std::uint32_t combined_bytes = (static_cast<std::uint32_t>(high_byte) << 16) | low_byte;
|
||||
float result = *reinterpret_cast<float*>(&combined_bytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
int PowerStackMock::client_socket() {
|
||||
return client_sock;
|
||||
}
|
||||
|
||||
void PowerStackMock::set_enable_answer_module_placeholder_allocation(bool enable) {
|
||||
answer_module_placeholder_allocation = enable;
|
||||
}
|
||||
|
||||
std::optional<int> PowerStackMock::get_global_connector_number_from_local(std::uint16_t local_connector_number) {
|
||||
auto read_result = client.read_holding_registers(
|
||||
0x110E + static_cast<std::uint16_t>(offset_from_connector_number(local_connector_number)), 1);
|
||||
|
||||
if (read_result.size() != 1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return static_cast<int>(read_result[0]);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "power_stack_mock/util.hpp"
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace user_acceptance_tests {
|
||||
namespace test_utils {
|
||||
|
||||
static void print(std::ostream& stream, const char* fmt, const char* prefix, va_list args) {
|
||||
std::stringstream ss;
|
||||
ss << prefix;
|
||||
ss << ": ";
|
||||
|
||||
char buffer[256];
|
||||
vsnprintf(buffer, sizeof(buffer), fmt, args);
|
||||
|
||||
ss << buffer;
|
||||
ss << "\n";
|
||||
|
||||
std::string result = ss.str();
|
||||
|
||||
stream << result;
|
||||
}
|
||||
|
||||
void psu_printf(const char* fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
print(std::cout, fmt, "PSU", args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void vfail_printf(const char* fmt, va_list args) {
|
||||
print(std::cout, fmt, "FAIL", args);
|
||||
}
|
||||
|
||||
void fail_printf(const char* fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
print(std::cout, fmt, "FAIL", args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void tester_printf(const char* fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
print(std::cout, fmt, "TESTER", args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void fdispenser_printf(std::ostream& stream, const char* fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
print(stream, fmt, "DISPENSER", args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
float uint16_vec_to_float(std::vector<std::uint16_t> vec) {
|
||||
std::uint8_t v0[4] = {
|
||||
(std::uint8_t)(vec[1] & 0xFF),
|
||||
(std::uint8_t)(vec[1] >> 8),
|
||||
(std::uint8_t)(vec[0] & 0xFF),
|
||||
(std::uint8_t)(vec[0] >> 8),
|
||||
};
|
||||
|
||||
auto f = *((float*)v0);
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
std::vector<std::uint16_t> float_to_uint16_vec(float value) {
|
||||
std::uint8_t* v = reinterpret_cast<std::uint8_t*>(&value);
|
||||
std::uint8_t v0[4] = {v[3], v[2], v[1], v[0]};
|
||||
|
||||
std::vector<std::uint16_t> v1;
|
||||
v1.push_back(static_cast<std::uint16_t>(v0[0] << 8 | v0[1]));
|
||||
v1.push_back(static_cast<std::uint16_t>(v0[2] << 8 | v0[3]));
|
||||
|
||||
return v1;
|
||||
}
|
||||
|
||||
double uint16_vec_to_double(std::vector<std::uint16_t> vec) {
|
||||
std::uint8_t v0[8] = {
|
||||
static_cast<std::uint8_t>(vec[3] & 0xFF), static_cast<std::uint8_t>(vec[3] >> 8),
|
||||
static_cast<std::uint8_t>(vec[2] & 0xFF), static_cast<std::uint8_t>(vec[2] >> 8),
|
||||
static_cast<std::uint8_t>(vec[1] & 0xFF), static_cast<std::uint8_t>(vec[1] >> 8),
|
||||
static_cast<std::uint8_t>(vec[0] & 0xFF), static_cast<std::uint8_t>(vec[0] >> 8),
|
||||
};
|
||||
|
||||
return *((double*)v0);
|
||||
}
|
||||
|
||||
std::vector<std::uint16_t> double_to_uint16_vec(double value) {
|
||||
std::uint8_t* v = reinterpret_cast<std::uint8_t*>(&value);
|
||||
|
||||
std::vector<std::uint16_t> out;
|
||||
out.push_back(static_cast<std::uint16_t>(v[7] << 8 | v[6]));
|
||||
out.push_back(static_cast<std::uint16_t>(v[5] << 8 | v[4]));
|
||||
out.push_back(static_cast<std::uint16_t>(v[3] << 8 | v[2]));
|
||||
out.push_back(static_cast<std::uint16_t>(v[1] << 8 | v[0]));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::uint32_t uint16_vec_to_uint32(std::vector<std::uint16_t> vec) {
|
||||
std::uint16_t v0[2] = {
|
||||
static_cast<std::uint16_t>(vec[1]),
|
||||
static_cast<std::uint16_t>(vec[0]),
|
||||
};
|
||||
|
||||
return *reinterpret_cast<std::uint32_t*>(v0);
|
||||
}
|
||||
|
||||
void dispenser_printf(const char* fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
print(std::cout, fmt, "DISPENSER", args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
} // namespace test_utils
|
||||
|
||||
} // namespace user_acceptance_tests
|
||||
@@ -0,0 +1,365 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#define MOCK_REGULAR_ERRORS 0
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "dispenser.hpp"
|
||||
#include "mqtt.hpp"
|
||||
#include "power_stack_mock/power_stack_mock.hpp"
|
||||
#include "socket_server.hpp"
|
||||
|
||||
using namespace fusion_charger::goose;
|
||||
|
||||
#if MOCK_REGULAR_ERRORS
|
||||
bool has_error = false;
|
||||
std::uint16_t error_value = 0;
|
||||
#endif
|
||||
|
||||
static bool environment_variable_enabled(const std::string& name) {
|
||||
const char* value = std::getenv(name.c_str());
|
||||
if (value == nullptr) {
|
||||
return false; // Environment variable not set
|
||||
}
|
||||
std::string value_str(value);
|
||||
return value_str == "1" || value_str == "true";
|
||||
}
|
||||
|
||||
class MqttPowerRequestPublisher {
|
||||
constexpr static std::chrono::milliseconds PUBLISH_INTERVAL{1000};
|
||||
|
||||
public:
|
||||
MqttPowerRequestPublisher(std::shared_ptr<MqttClient> mqtt_client, const std::string& base_topic) :
|
||||
mqtt_client(mqtt_client), base_topic(base_topic) {
|
||||
}
|
||||
|
||||
void publish(double voltage, double current, std::uint16_t global_connector_number) {
|
||||
const auto data =
|
||||
"{\"voltage\": " + std::to_string(voltage) + ", \"current\": " + std::to_string(current) + "}";
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(last_publish_mutex);
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
if (last_publish_data.find(global_connector_number) != last_publish_data.end()) {
|
||||
std::string last_data = last_publish_data[global_connector_number];
|
||||
auto deadline_at = publish_deadline[global_connector_number];
|
||||
|
||||
if (last_data == data and now < deadline_at) {
|
||||
return; // data is the same and deadline has not expired yet -> no
|
||||
// need to publish
|
||||
}
|
||||
}
|
||||
|
||||
publish_deadline[global_connector_number] = now + PUBLISH_INTERVAL;
|
||||
last_publish_data[global_connector_number] = data;
|
||||
}
|
||||
|
||||
std::string topic = base_topic + std::to_string(global_connector_number) + "/power_request";
|
||||
|
||||
mqtt_client->publish(topic, data);
|
||||
}
|
||||
|
||||
void publish(const PowerRequirementRequest& req) {
|
||||
publish(req.voltage, req.current, req.charging_connector_no);
|
||||
}
|
||||
|
||||
void publish(const StopChargeRequest& req) {
|
||||
publish(0.0, 0.0, req.charging_connector_no);
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<MqttClient> mqtt_client;
|
||||
std::string base_topic;
|
||||
|
||||
std::unordered_map<std::uint8_t, std::chrono::steady_clock::time_point> publish_deadline;
|
||||
std::unordered_map<std::uint8_t, std::string> last_publish_data;
|
||||
std::mutex last_publish_mutex;
|
||||
};
|
||||
|
||||
// Mock for a single dispenser that simulates a PowerStack device
|
||||
class Mock {
|
||||
private:
|
||||
std::uint16_t used_connectors;
|
||||
std::unique_ptr<PowerStackMock> mock;
|
||||
|
||||
std::chrono::_V2::steady_clock::time_point periodic_update_deadline = std::chrono::steady_clock::now();
|
||||
|
||||
int not_sending_capabilities_counter = 0; // used to test "capabilities not received" error
|
||||
|
||||
double mock_total_historical_ac_input_energy = 0.0;
|
||||
|
||||
/**
|
||||
* @brief generate voltage, current and total historic power values and send them to the dispenser
|
||||
*/
|
||||
void generate_and_send_voltage_current_power(int seconds_since_last_call) {
|
||||
double used_power = 0.0; // power in W that is being drawn by the EV(s)
|
||||
|
||||
for (int i = 1; i <= used_connectors; i++) {
|
||||
auto working_status = mock->get_working_status(i);
|
||||
if (not working_status.has_value() || working_status.value() != WorkingStatus::CHARGING) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto global_connector_number = mock->get_global_connector_number_from_local(i);
|
||||
if (!global_connector_number.has_value()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto req_opt = mock->get_last_power_requirement_request(i);
|
||||
if (req_opt.has_value()) {
|
||||
auto req = req_opt.value();
|
||||
used_power += req.voltage * req.current;
|
||||
}
|
||||
}
|
||||
|
||||
// add to total historical power
|
||||
mock_total_historical_ac_input_energy += (used_power * seconds_since_last_call) / 3600.0 / 1000.0; // kWh
|
||||
mock_total_historical_ac_input_energy += 0.00001; // add a bit more to simulate standby consumption
|
||||
|
||||
double ac_base_voltage = 230.0;
|
||||
ac_base_voltage +=
|
||||
static_cast<double>((std::rand() % 256) / 10.0 - 12.8); // add noise between -12.8V and +12.7V
|
||||
|
||||
double ac_base_current = used_power / ac_base_voltage / 3.0; // 3 phases
|
||||
|
||||
mock->send_total_historical_ac_input_energy(mock_total_historical_ac_input_energy);
|
||||
mock->send_ac_input_voltages_currents(ac_base_voltage, ac_base_voltage, ac_base_voltage, ac_base_current,
|
||||
ac_base_current, ac_base_current);
|
||||
}
|
||||
|
||||
void periodic_update() {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (now < periodic_update_deadline) {
|
||||
return;
|
||||
}
|
||||
periodic_update_deadline = now + std::chrono::seconds(5);
|
||||
|
||||
#if MOCK_REGULAR_ERRORS
|
||||
mock->write_registers(0x4000, {has_error ? 0x0001 : 0x0000});
|
||||
has_error = !has_error;
|
||||
|
||||
error_value = (error_value + 1) % 3;
|
||||
mock->write_registers(0x40D0, {0, error_value});
|
||||
#endif
|
||||
|
||||
mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
mock->send_mac_address();
|
||||
|
||||
if (not_sending_capabilities_counter > 1) {
|
||||
for (int i = 1; i <= used_connectors; i++) { // connector number starts at 1
|
||||
mock->send_max_rated_current_of_output_port(100.0, i);
|
||||
mock->send_min_rated_current_of_output_port(1.0, i);
|
||||
mock->send_max_rated_voltage_of_output_port(1000.0, i);
|
||||
mock->send_min_rated_voltage_of_output_port(100.0, i);
|
||||
mock->send_rated_power_of_output_port(60.0, i);
|
||||
|
||||
mock->send_port_available(true, i);
|
||||
}
|
||||
} else {
|
||||
not_sending_capabilities_counter++;
|
||||
}
|
||||
|
||||
generate_and_send_voltage_current_power(5); // called every 5 seconds, the approximation is good enough
|
||||
}
|
||||
|
||||
std::array<bool, 4> car_plugged_in = {false, false, false, false};
|
||||
|
||||
void send_goose_key_on_car_plugged_in(std::uint8_t local_connector_number) {
|
||||
auto offset = offset_from_connector_number(local_connector_number);
|
||||
|
||||
auto raw = mock->get_unsolicited_report_data(0x110B + (std::uint16_t)offset, 1);
|
||||
|
||||
if (raw.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto working_status = (WorkingStatus)raw[0];
|
||||
if (!car_plugged_in[local_connector_number] &&
|
||||
working_status == WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED) {
|
||||
car_plugged_in[local_connector_number] = true;
|
||||
|
||||
mock->send_hmac_key(local_connector_number);
|
||||
|
||||
printf("Car plugged in\n");
|
||||
} else if (car_plugged_in[local_connector_number] && working_status == WorkingStatus::STANDBY) {
|
||||
car_plugged_in[local_connector_number] = false;
|
||||
printf("Car unplugged\n");
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
Mock(std::unique_ptr<PowerStackMock> mock) : mock(std::move(mock)) {
|
||||
}
|
||||
|
||||
void run() {
|
||||
mock->start_modbus_event_loop();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
used_connectors = mock->read_registers(0x1015, 1)[0];
|
||||
|
||||
printf("Using %d connectors\n", used_connectors);
|
||||
|
||||
while (true) {
|
||||
periodic_update();
|
||||
|
||||
for (int i = 1; i <= used_connectors; i++) {
|
||||
send_goose_key_on_car_plugged_in(i);
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (mock) {
|
||||
mock->stop_modbus_event_loop();
|
||||
mock.reset();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// parse args and update config for mTLS
|
||||
void init_tls(int argc, char* argv[], PowerStackMockConfig& config) {
|
||||
if (argc < 2) {
|
||||
return;
|
||||
}
|
||||
printf("Using mutual TLS\n");
|
||||
|
||||
std::string tls_certificates_folder = argv[1];
|
||||
|
||||
if (tls_certificates_folder.back() != '/') {
|
||||
tls_certificates_folder += "/";
|
||||
}
|
||||
|
||||
config.tls_config = tls_util::MutualTlsServerConfig{
|
||||
tls_certificates_folder + "dispenser_ca.crt.pem",
|
||||
tls_certificates_folder + "psu.crt.pem",
|
||||
tls_certificates_folder + "psu.key.pem",
|
||||
};
|
||||
}
|
||||
|
||||
std::mutex mocks_mutex;
|
||||
std::atomic<int> active_mocks; // counts all active mocks
|
||||
std::condition_variable mocks_cv;
|
||||
|
||||
std::vector<std::thread> mock_threads;
|
||||
|
||||
void on_socket(int socket, void* context) {
|
||||
PowerStackMockConfig* config = (PowerStackMockConfig*)context;
|
||||
|
||||
printf("New client\n");
|
||||
|
||||
std::lock_guard<std::mutex> lock(mocks_mutex);
|
||||
active_mocks++;
|
||||
|
||||
auto mock = std::make_unique<Mock>(std::unique_ptr<PowerStackMock>(PowerStackMock::from_config(*config, socket)));
|
||||
|
||||
auto thread = std::thread([mock = std::move(mock)]() {
|
||||
try {
|
||||
mock->run();
|
||||
} catch (const std::exception& e) {
|
||||
}
|
||||
|
||||
mock->stop();
|
||||
|
||||
std::lock_guard<std::mutex> lock(mocks_mutex);
|
||||
active_mocks--;
|
||||
mocks_cv.notify_all();
|
||||
});
|
||||
|
||||
mock_threads.push_back(std::move(thread));
|
||||
|
||||
mocks_cv.notify_all();
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
PowerStackMockConfig config{
|
||||
"veth1",
|
||||
8502,
|
||||
{0x67, 0xe4, 0x26, 0x56, 0x0a, 0x70, 0xca, 0x4a, 0x83, 0x3c, 0x44, 0xb3, 0x12, 0x70, 0xca, 0x93,
|
||||
0x55, 0xd8, 0x7b, 0x02, 0x0f, 0x57, 0x8e, 0x1e, 0x9d, 0x19, 0x74, 0xc0, 0x2f, 0xa6, 0xf6, 0x80,
|
||||
0x4c, 0x2f, 0xcb, 0xdf, 0x73, 0x5e, 0x71, 0x1c, 0xec, 0x08, 0x5b, 0x93, 0x81, 0x47, 0x16, 0xad},
|
||||
true,
|
||||
true,
|
||||
};
|
||||
|
||||
init_tls(argc, argv, config);
|
||||
|
||||
// Disables securing outgoing GOOSE frames with HMAC (does not affect
|
||||
// receiving)
|
||||
if (environment_variable_enabled("FUSION_CHARGER_MOCK_DISABLE_SEND_HMAC")) {
|
||||
config.enable_hmac = false;
|
||||
printf("Sending HMAC disabled\n");
|
||||
}
|
||||
// Disables verifying HMAC of incoming GOOSE frames (does not affect sending)
|
||||
// If this is set to true, the mock will also allow unsecured GOOSE frames
|
||||
if (environment_variable_enabled("FUSION_CHARGER_MOCK_DISABLE_VERIFY_HMAC")) {
|
||||
config.verify_hmac = false;
|
||||
printf("Verifying HMAC disabled\n");
|
||||
}
|
||||
|
||||
// Set the Ethernet interface to use
|
||||
if (std::getenv("FUSION_CHARGER_MOCK_ETH")) {
|
||||
config.eth = std::getenv("FUSION_CHARGER_MOCK_ETH");
|
||||
printf("Using Ethernet interface: %s\n", config.eth.c_str());
|
||||
} else {
|
||||
printf("Using default Ethernet interface: %s\n", config.eth.c_str());
|
||||
}
|
||||
|
||||
// Set the Modbus TCP port to use
|
||||
if (std::getenv("FUSION_CHARGER_MOCK_PORT")) {
|
||||
config.port = std::atoi(std::getenv("FUSION_CHARGER_MOCK_PORT"));
|
||||
}
|
||||
|
||||
printf("Waiting for connections on port %d\n", config.port);
|
||||
|
||||
std::shared_ptr<MqttPowerRequestPublisher> mqtt_publisher;
|
||||
|
||||
// If both environment variables are set, use them to create an MQTT client
|
||||
if (std::getenv("FUSION_CHARGER_MOCK_MQTT_HOST") && std::getenv("FUSION_CHARGER_MOCK_MQTT_PORT")) {
|
||||
std::string mqtt_host = std::getenv("FUSION_CHARGER_MOCK_MQTT_HOST");
|
||||
std::string mqtt_port = std::getenv("FUSION_CHARGER_MOCK_MQTT_PORT");
|
||||
std::string mqtt_base_topic = "fusion_charger_mock/";
|
||||
if (std::getenv("FUSION_CHARGER_MOCK_MQTT_BASE_TOPIC")) {
|
||||
mqtt_base_topic = std::getenv("FUSION_CHARGER_MOCK_MQTT_BASE_TOPIC");
|
||||
if (mqtt_base_topic.back() != '/') {
|
||||
mqtt_base_topic += "/";
|
||||
}
|
||||
}
|
||||
|
||||
mqtt_publisher = std::make_shared<MqttPowerRequestPublisher>(std::make_shared<MqttClient>(mqtt_host, mqtt_port),
|
||||
mqtt_base_topic);
|
||||
printf("Using MQTT client with host: %s and port: %s\n", mqtt_host.c_str(), mqtt_port.c_str());
|
||||
printf("Using MQTT base topic: %s\n", mqtt_base_topic.c_str());
|
||||
}
|
||||
|
||||
if (mqtt_publisher) {
|
||||
config.power_requirement_request_callback = [&mqtt_publisher](const PowerRequirementRequest& req) {
|
||||
mqtt_publisher->publish(req);
|
||||
};
|
||||
|
||||
config.stop_charge_request_callback = [&mqtt_publisher](const StopChargeRequest& req) {
|
||||
mqtt_publisher->publish(req);
|
||||
};
|
||||
}
|
||||
|
||||
SocketServer socket_server(config.port, (void*)&config, on_socket);
|
||||
|
||||
for (;;) {
|
||||
std::unique_lock<std::mutex> lock(mocks_mutex);
|
||||
mocks_cv.wait(lock);
|
||||
if (active_mocks == 0) {
|
||||
printf("No dispensers connected anymore, exiting\n");
|
||||
for (auto& thread : mock_threads) {
|
||||
if (thread.joinable()) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "mqtt.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.h>
|
||||
#include <mqtt.h>
|
||||
#include <netdb.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
MqttClient::MqttClient(std::string mqtt_host, std::string mqtt_port) {
|
||||
int sockfd = open_socket(mqtt_host, mqtt_port);
|
||||
if (sockfd < 0) {
|
||||
fprintf(stderr, "Failed to open MQTT socket\n");
|
||||
throw std::runtime_error("Failed to open MQTT socket");
|
||||
}
|
||||
MQTTErrors err = mqtt_init(&client, sockfd, (std::uint8_t*)sendbuf, sizeof(sendbuf), (std::uint8_t*)recvbuf,
|
||||
sizeof(recvbuf), NULL);
|
||||
|
||||
client.publish_response_callback_state = this;
|
||||
|
||||
if (err != MQTT_OK) {
|
||||
fprintf(stderr, "Failed to initialize MQTT client: %s\n", mqtt_error_str(err));
|
||||
throw std::runtime_error("Failed to initialize MQTT client");
|
||||
}
|
||||
|
||||
err = mqtt_connect(&client, NULL, NULL, NULL, 0, NULL, NULL, MQTT_CONNECT_CLEAN_SESSION, 400);
|
||||
if (err != MQTT_OK) {
|
||||
fprintf(stderr, "Failed to connect to MQTT broker: %s\n", mqtt_error_str(err));
|
||||
throw std::runtime_error("Failed to connect to MQTT broker");
|
||||
}
|
||||
|
||||
background_thread = std::thread(&MqttClient::background_thread_fn, this);
|
||||
}
|
||||
|
||||
MqttClient::~MqttClient() {
|
||||
stop_flag = true;
|
||||
|
||||
if (background_thread.joinable()) {
|
||||
background_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void MqttClient::publish(const std::string& topic, const std::string& message) {
|
||||
std::lock_guard<std::mutex> lock(publish_queue_mutex);
|
||||
publish_queue.push_back({topic, message});
|
||||
}
|
||||
|
||||
void MqttClient::background_thread_fn() {
|
||||
while (!stop_flag) {
|
||||
std::vector<PublishQueueEntry> queue_copy;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(publish_queue_mutex);
|
||||
queue_copy.swap(publish_queue);
|
||||
}
|
||||
|
||||
for (const auto& entry : queue_copy) {
|
||||
MQTTErrors err = mqtt_publish(&client, entry.topic.c_str(), entry.message.data(), entry.message.size(),
|
||||
MQTT_PUBLISH_QOS_0);
|
||||
if (err != MQTT_OK) {
|
||||
fprintf(stderr, "Failed to publish message: %s\n", mqtt_error_str(err));
|
||||
}
|
||||
}
|
||||
|
||||
mqtt_sync(&client);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}
|
||||
|
||||
int MqttClient::open_socket(std::string host, std::string port) {
|
||||
struct addrinfo hints = {0};
|
||||
|
||||
hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
|
||||
hints.ai_socktype = SOCK_STREAM; /* Must be TCP */
|
||||
int sockfd = -1;
|
||||
int rv;
|
||||
struct addrinfo *p, *servinfo;
|
||||
|
||||
/* get address information */
|
||||
rv = getaddrinfo(host.c_str(), port.c_str(), &hints, &servinfo);
|
||||
if (rv != 0) {
|
||||
fprintf(stderr, "Failed to open socket (getaddrinfo): %s\n", gai_strerror(rv));
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* open the first possible socket */
|
||||
for (p = servinfo; p != NULL; p = p->ai_next) {
|
||||
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
|
||||
if (sockfd == -1)
|
||||
continue;
|
||||
|
||||
/* connect to server */
|
||||
rv = connect(sockfd, p->ai_addr, p->ai_addrlen);
|
||||
if (rv == -1) {
|
||||
close(sockfd);
|
||||
sockfd = -1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/* free servinfo */
|
||||
freeaddrinfo(servinfo);
|
||||
|
||||
/* make non-blocking */
|
||||
if (sockfd != -1)
|
||||
fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL) | O_NONBLOCK);
|
||||
|
||||
/* return the new socket fd */
|
||||
return sockfd;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <mqtt.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
class MqttClient {
|
||||
public:
|
||||
MqttClient(std::string mqtt_host, std::string mqtt_port);
|
||||
~MqttClient();
|
||||
|
||||
void publish(const std::string& topic, const std::string& message);
|
||||
|
||||
private:
|
||||
struct mqtt_client client;
|
||||
char sendbuf[500 * 1024];
|
||||
char recvbuf[1024];
|
||||
std::thread background_thread;
|
||||
bool stop_flag = false;
|
||||
|
||||
struct PublishQueueEntry {
|
||||
std::string topic;
|
||||
std::string message;
|
||||
};
|
||||
std::mutex publish_queue_mutex;
|
||||
std::vector<PublishQueueEntry> publish_queue;
|
||||
|
||||
static int open_socket(std::string host, std::string port);
|
||||
|
||||
void background_thread_fn();
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "socket_server.hpp"
|
||||
|
||||
SocketServer::SocketServer(int port, void* context, std::function<void(int, void*)> on_client) :
|
||||
on_client(std::move(on_client)), context(context), port(port), server_sock(-1) {
|
||||
server_thread = std::thread([this]() { main(); });
|
||||
}
|
||||
|
||||
SocketServer::~SocketServer() {
|
||||
if (server_sock >= 0) {
|
||||
shutdown(server_sock, SHUT_RDWR);
|
||||
}
|
||||
if (server_thread.joinable()) {
|
||||
server_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void SocketServer::main() {
|
||||
server_sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
int is_true = 1;
|
||||
setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &is_true, sizeof(int));
|
||||
struct sockaddr_in serv_addr;
|
||||
serv_addr.sin_family = AF_INET;
|
||||
serv_addr.sin_addr.s_addr = INADDR_ANY;
|
||||
serv_addr.sin_port = htons(port);
|
||||
int err = bind(server_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
|
||||
if (err < 0) {
|
||||
throw std::runtime_error("Failed to bind");
|
||||
}
|
||||
|
||||
err = listen(server_sock, 1);
|
||||
if (err < 0) {
|
||||
throw std::runtime_error("Failed to listen");
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
int client_sock = accept(server_sock, nullptr, nullptr);
|
||||
if (client_sock < 0) {
|
||||
if (errno == EBADF || errno == EINVAL) {
|
||||
// Socket was closed, exit gracefully
|
||||
break;
|
||||
}
|
||||
printf("Failed to accept with error: %d", errno);
|
||||
close(server_sock);
|
||||
|
||||
throw std::runtime_error("Failed to accept with error: " + std::to_string(errno));
|
||||
}
|
||||
|
||||
on_client(client_sock, context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <arpa/inet.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <functional>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
// Simple socket server that accepts (unlimited) connections and calls a
|
||||
// callback on each accepted client.
|
||||
class SocketServer {
|
||||
void* context;
|
||||
std::function<void(int, void*)> on_client; // called when a client connects
|
||||
std::thread server_thread;
|
||||
int port;
|
||||
int server_sock;
|
||||
|
||||
public:
|
||||
SocketServer(int port, void* context, std::function<void(int, void*)> on_client);
|
||||
~SocketServer();
|
||||
|
||||
private:
|
||||
void main();
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
include(GoogleTest)
|
||||
|
||||
file(GLOB_RECURSE FUSION_CHARGER_DISPENSER_LIB_TEST_SOURCES "*.cpp")
|
||||
|
||||
add_executable(dispenser-lib-tests ${FUSION_CHARGER_DISPENSER_LIB_TEST_SOURCES})
|
||||
target_link_libraries(dispenser-lib-tests PRIVATE gtest_main fusion_charger_dispenser)
|
||||
|
||||
gtest_discover_tests(dispenser-lib-tests)
|
||||
@@ -0,0 +1,103 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <connector.hpp>
|
||||
|
||||
struct ConnectorFSM_Fixture : public ::testing::Test {
|
||||
ConnectorFSM::Callbacks callbacks{
|
||||
.state_transition = [this](States state) { state_transition_counter++; },
|
||||
.mode_phase_transition = [this](ModePhase mode_phase) { mode_phase_transition_counter++; },
|
||||
.any_transition = [this](States state, ModePhase mode_phase) { any_transition_counter++; },
|
||||
};
|
||||
ConnectorFSM fsm{callbacks, logs::log_printf};
|
||||
|
||||
std::uint32_t state_transition_counter = 0;
|
||||
std::uint32_t mode_phase_transition_counter = 0;
|
||||
std::uint32_t any_transition_counter = 0;
|
||||
|
||||
void SetUp() override {
|
||||
state_transition_counter = 0;
|
||||
mode_phase_transition_counter = 0;
|
||||
any_transition_counter = 0;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ConnectorFSM_Fixture, initial_state) {
|
||||
EXPECT_EQ(fsm.get_state(), States::CarDisconnected);
|
||||
EXPECT_EQ(fsm.get_mode_phase(), ModePhase::Off);
|
||||
}
|
||||
|
||||
TEST_F(ConnectorFSM_Fixture, connect_car) {
|
||||
fsm.on_car_connected();
|
||||
EXPECT_EQ(fsm.get_state(), States::NoKeyYet);
|
||||
EXPECT_EQ(state_transition_counter, 1);
|
||||
EXPECT_EQ(mode_phase_transition_counter, 0);
|
||||
EXPECT_EQ(any_transition_counter, 1);
|
||||
}
|
||||
|
||||
TEST_F(ConnectorFSM_Fixture, connect_car_twice) {
|
||||
fsm.on_car_connected();
|
||||
fsm.on_car_connected();
|
||||
EXPECT_EQ(fsm.get_state(), States::NoKeyYet);
|
||||
EXPECT_EQ(state_transition_counter, 1);
|
||||
EXPECT_EQ(mode_phase_transition_counter, 0);
|
||||
EXPECT_EQ(any_transition_counter, 1);
|
||||
}
|
||||
|
||||
TEST_F(ConnectorFSM_Fixture, regular_state_flow) {
|
||||
fsm.on_car_connected();
|
||||
EXPECT_EQ(fsm.get_state(), States::NoKeyYet);
|
||||
fsm.on_hmac_key_received();
|
||||
EXPECT_EQ(fsm.get_state(), States::ConnectedNoAllocation);
|
||||
fsm.on_module_placeholder_allocation_response(true);
|
||||
EXPECT_EQ(fsm.get_state(), States::Running);
|
||||
fsm.on_mode_phase_change(ModePhase::ExportCableCheck);
|
||||
EXPECT_EQ(fsm.get_state(), States::Running);
|
||||
EXPECT_EQ(fsm.get_mode_phase(), ModePhase::ExportCableCheck);
|
||||
fsm.on_mode_phase_change(ModePhase::OffCableCheck);
|
||||
EXPECT_EQ(fsm.get_state(), States::Running);
|
||||
EXPECT_EQ(fsm.get_mode_phase(), ModePhase::OffCableCheck);
|
||||
fsm.on_mode_phase_change(ModePhase::ExportPrecharge);
|
||||
EXPECT_EQ(fsm.get_state(), States::Running);
|
||||
EXPECT_EQ(fsm.get_mode_phase(), ModePhase::ExportPrecharge);
|
||||
fsm.on_mode_phase_change(ModePhase::ExportCharging);
|
||||
EXPECT_EQ(fsm.get_state(), States::Running);
|
||||
EXPECT_EQ(fsm.get_mode_phase(), ModePhase::ExportCharging);
|
||||
fsm.on_mode_phase_change(ModePhase::Off);
|
||||
EXPECT_EQ(fsm.get_state(), States::Completed);
|
||||
EXPECT_EQ(fsm.get_mode_phase(), ModePhase::Off);
|
||||
fsm.on_car_disconnected();
|
||||
EXPECT_EQ(fsm.get_state(), States::CarDisconnected);
|
||||
}
|
||||
|
||||
TEST_F(ConnectorFSM_Fixture, car_disconnect_from_any) {
|
||||
// From NoKeyYet
|
||||
fsm.on_car_connected();
|
||||
EXPECT_EQ(fsm.get_state(), States::NoKeyYet);
|
||||
fsm.on_car_disconnected();
|
||||
EXPECT_EQ(fsm.get_state(), States::CarDisconnected);
|
||||
|
||||
// From ConnectedNoAllocation
|
||||
fsm.on_car_connected();
|
||||
fsm.on_hmac_key_received();
|
||||
EXPECT_EQ(fsm.get_state(), States::ConnectedNoAllocation);
|
||||
fsm.on_car_disconnected();
|
||||
EXPECT_EQ(fsm.get_state(), States::CarDisconnected);
|
||||
|
||||
// From Running
|
||||
fsm.on_car_connected();
|
||||
fsm.on_hmac_key_received();
|
||||
fsm.on_module_placeholder_allocation_response(true);
|
||||
EXPECT_EQ(fsm.get_state(), States::Running);
|
||||
fsm.on_car_disconnected();
|
||||
EXPECT_EQ(fsm.get_state(), States::CarDisconnected);
|
||||
|
||||
// From Completed
|
||||
fsm.on_car_connected();
|
||||
fsm.on_hmac_key_received();
|
||||
fsm.on_module_placeholder_allocation_response(true);
|
||||
fsm.on_mode_phase_change(ModePhase::Off);
|
||||
EXPECT_EQ(fsm.get_state(), States::Completed);
|
||||
fsm.on_car_disconnected();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
test_certificates/**
|
||||
!test_certificates/generate.sh
|
||||
@@ -0,0 +1,17 @@
|
||||
file(GLOB_RECURSE POWER_STACK_TEST_SOURCES "*.cpp")
|
||||
add_executable(user-acceptance-tests ${POWER_STACK_TEST_SOURCES})
|
||||
target_include_directories(user-acceptance-tests PRIVATE include)
|
||||
target_compile_options(user-acceptance-tests PRIVATE -g -O0)
|
||||
|
||||
target_link_libraries(user-acceptance-tests
|
||||
fusion_charger_dispenser
|
||||
fusion_charger_goose_driver
|
||||
fusion_charger_modbus_driver
|
||||
fusion_charger_modbus_extensions
|
||||
modbus-client
|
||||
power_stack_mock_lib
|
||||
gtest_main
|
||||
gmock_main
|
||||
)
|
||||
|
||||
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/test_certificates DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
|
||||
@@ -0,0 +1,108 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "dispenser.hpp"
|
||||
#include "power_stack_mock/power_stack_mock.hpp"
|
||||
|
||||
namespace user_acceptance_tests {
|
||||
namespace dispenser_fixture {
|
||||
|
||||
class DispenserTestBase : public ::testing::Test {
|
||||
protected:
|
||||
struct DispenserTestParams {
|
||||
DispenserConfig dispenser_config; // Move this above 'dispenser'
|
||||
std::vector<ConnectorConfig> connector_configs;
|
||||
|
||||
float dispenser_connector_upstream_voltage;
|
||||
float dispenser_output_voltage;
|
||||
float dispesner_output_current;
|
||||
ContactorStatus dispenser_contactor_status;
|
||||
ElectronicLockStatus dispenser_electronic_lock_status;
|
||||
|
||||
PowerStackMockConfig power_stack_mock_config;
|
||||
};
|
||||
|
||||
DispenserConfig dispenser_config; // Move this above 'dispenser'
|
||||
std::vector<ConnectorConfig> connector_configs;
|
||||
|
||||
std::atomic<float> dispenser_connector_upstream_voltage;
|
||||
std::atomic<float> dispenser_output_voltage;
|
||||
std::atomic<float> dispesner_output_current;
|
||||
std::atomic<ContactorStatus> dispenser_contactor_status;
|
||||
std::atomic<ElectronicLockStatus> dispenser_electronic_lock_status;
|
||||
|
||||
ConnectorCallbacks connector_callbacks;
|
||||
std::shared_ptr<Dispenser> dispenser; // Move this below 'dispenser_config'
|
||||
//
|
||||
PowerStackMockConfig power_stack_mock_config;
|
||||
std::shared_ptr<PowerStackMock> power_stack_mock;
|
||||
|
||||
protected:
|
||||
DispenserTestBase(DispenserTestParams params);
|
||||
|
||||
virtual void SetUp() override;
|
||||
virtual void TearDown() override;
|
||||
|
||||
virtual void sleep_for_ms(std::uint32_t ms);
|
||||
};
|
||||
|
||||
class DispenserWithTlsTest : public DispenserTestBase {
|
||||
public:
|
||||
DispenserWithTlsTest();
|
||||
|
||||
const std::uint16_t global_connector_number = connector_configs[0].global_connector_number;
|
||||
const std::uint16_t local_connector_number = 1;
|
||||
|
||||
std::shared_ptr<Connector> connector();
|
||||
std::optional<fusion_charger::goose::PowerRequirementRequest> get_last_power_requirement_request();
|
||||
std::uint32_t get_stop_request_counter();
|
||||
std::uint32_t get_power_requirements_counter();
|
||||
float get_maximum_rated_charge_current();
|
||||
ConnectionStatus get_connection_status();
|
||||
};
|
||||
|
||||
class DispenserWithoutTlsTest : public DispenserTestBase {
|
||||
public:
|
||||
DispenserWithoutTlsTest();
|
||||
|
||||
const std::uint16_t global_connector_number = connector_configs[0].global_connector_number;
|
||||
const std::uint16_t local_connector_number = 1;
|
||||
|
||||
std::shared_ptr<Connector> connector();
|
||||
std::optional<fusion_charger::goose::PowerRequirementRequest> get_last_power_requirement_request();
|
||||
std::uint32_t get_stop_request_counter();
|
||||
std::uint32_t get_power_requirements_counter();
|
||||
float get_maximum_rated_charge_current();
|
||||
ConnectionStatus get_connection_status();
|
||||
};
|
||||
|
||||
class DispenserWithMultipleConnectors : public DispenserTestBase {
|
||||
public:
|
||||
std::uint16_t local_connector_number1 = 1;
|
||||
std::uint16_t local_connector_number2 = 2;
|
||||
std::uint16_t local_connector_number3 = 3;
|
||||
std::uint16_t local_connector_number4 = 4;
|
||||
|
||||
DispenserWithMultipleConnectors();
|
||||
|
||||
std::shared_ptr<Connector> get_connector(std::uint16_t local_connector_number);
|
||||
void set_up_psu_for_operation();
|
||||
void connect_car(std::uint16_t local_connector_number);
|
||||
void send_hmac_key(std::uint16_t local_connector_number);
|
||||
void set_export_values(std::uint16_t local_connector_number, float voltage, float current);
|
||||
void set_mode_phase(std::uint16_t local_connector_number, ModePhase mode_phase);
|
||||
std::array<std::uint32_t, 4> get_stop_request_counter();
|
||||
void disconnect_car(std::uint16_t local_connector_number);
|
||||
|
||||
void assert_working_status(std::array<WorkingStatus, 4> expected_status);
|
||||
void assert_requirement_type(std::array<std::optional<fusion_charger::goose::RequirementType>, 4> expected_types);
|
||||
|
||||
void assert_stop_request_counter_greater_or_equal(std::array<std::uint32_t, 4> expected);
|
||||
};
|
||||
|
||||
} // namespace dispenser_fixture
|
||||
|
||||
} // namespace user_acceptance_tests
|
||||
@@ -0,0 +1,412 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "user_acceptance_tests/dispenser_test_fixture.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "configuration.hpp"
|
||||
|
||||
namespace user_acceptance_tests {
|
||||
namespace dispenser_fixture {
|
||||
|
||||
using namespace std;
|
||||
|
||||
const ConnectorCallbacks default_connector_callbacks = ConnectorCallbacks{
|
||||
[]() { return 0.0f; },
|
||||
[]() { return 0.0f; },
|
||||
[]() { return 0.0f; },
|
||||
[]() { return ContactorStatus::ON; },
|
||||
[]() { return ElectronicLockStatus::UNLOCKED; },
|
||||
};
|
||||
|
||||
const DispenserConfig dispenser_config_without_tls = DispenserConfig{
|
||||
"127.0.0.1",
|
||||
8502,
|
||||
"veth0",
|
||||
0x0002,
|
||||
0x0080,
|
||||
0x0001,
|
||||
0x0003,
|
||||
"v1.2.3+456",
|
||||
1,
|
||||
"01234567890ABCDEF",
|
||||
std::chrono::seconds(2),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
nullopt,
|
||||
std::chrono::seconds(3),
|
||||
};
|
||||
|
||||
const PowerStackMockConfig default_power_stack_mock_config_without_tls = PowerStackMockConfig{
|
||||
"veth1",
|
||||
8502,
|
||||
{0x67, 0xe4, 0x26, 0x56, 0x0a, 0x70, 0xca, 0x4a, 0x83, 0x3c, 0x44, 0xb3, 0x12, 0x70, 0xca, 0x93,
|
||||
0x55, 0xd8, 0x7b, 0x02, 0x0f, 0x57, 0x8e, 0x1e, 0x9d, 0x19, 0x74, 0xc0, 0x2f, 0xa6, 0xf6, 0x80,
|
||||
0x4c, 0x2f, 0xcb, 0xdf, 0x73, 0x5e, 0x71, 0x1c, 0xec, 0x08, 0x5b, 0x93, 0x81, 0x47, 0x16, 0xad},
|
||||
true,
|
||||
true,
|
||||
std::nullopt,
|
||||
};
|
||||
|
||||
DispenserConfig dispenser_config_with_tls = DispenserConfig{
|
||||
"127.0.0.1",
|
||||
8502,
|
||||
"veth0",
|
||||
0x0002,
|
||||
0x0080,
|
||||
0x0001,
|
||||
0x0003,
|
||||
"v1.2.3+456",
|
||||
1,
|
||||
"01234567890ABCDEF",
|
||||
std::chrono::seconds(2),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
tls_util::MutualTlsClientConfig{"modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/"
|
||||
"fusion_charger_lib/fusion-charger-dispenser-library/"
|
||||
"user-acceptance-tests/"
|
||||
"test_certificates/"
|
||||
"psu_ca.crt.pem",
|
||||
"modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/"
|
||||
"fusion_charger_lib/fusion-charger-dispenser-library/"
|
||||
"user-acceptance-tests/"
|
||||
"test_certificates/"
|
||||
"dispenser.crt.pem",
|
||||
"modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/"
|
||||
"fusion_charger_lib/fusion-charger-dispenser-library/"
|
||||
"user-acceptance-tests/"
|
||||
"test_certificates/"
|
||||
"dispenser.key.pem"},
|
||||
std::chrono::seconds(3),
|
||||
};
|
||||
|
||||
const PowerStackMockConfig default_power_stack_mock_config_with_tls = PowerStackMockConfig{
|
||||
"veth1",
|
||||
8502,
|
||||
{0x67, 0xe4, 0x26, 0x56, 0x0a, 0x70, 0xca, 0x4a, 0x83, 0x3c, 0x44, 0xb3, 0x12, 0x70, 0xca, 0x93,
|
||||
0x55, 0xd8, 0x7b, 0x02, 0x0f, 0x57, 0x8e, 0x1e, 0x9d, 0x19, 0x74, 0xc0, 0x2f, 0xa6, 0xf6, 0x80,
|
||||
0x4c, 0x2f, 0xcb, 0xdf, 0x73, 0x5e, 0x71, 0x1c, 0xec, 0x08, 0x5b, 0x93, 0x81, 0x47, 0x16, 0xad},
|
||||
true,
|
||||
true,
|
||||
tls_util::MutualTlsServerConfig{
|
||||
"modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/"
|
||||
"fusion_charger_lib/fusion-charger-dispenser-library/"
|
||||
"user-acceptance-tests/test_certificates/"
|
||||
"dispenser_ca.crt.pem",
|
||||
"modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/"
|
||||
"fusion_charger_lib/fusion-charger-dispenser-library/"
|
||||
"user-acceptance-tests/test_certificates/"
|
||||
"psu.crt.pem",
|
||||
"modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/"
|
||||
"fusion_charger_lib/fusion-charger-dispenser-library/"
|
||||
"user-acceptance-tests/test_certificates/"
|
||||
"psu.key.pem",
|
||||
},
|
||||
};
|
||||
|
||||
DispenserTestBase::DispenserTestBase(DispenserTestParams params) :
|
||||
dispenser_config(params.dispenser_config),
|
||||
connector_configs(params.connector_configs),
|
||||
dispenser_connector_upstream_voltage(params.dispenser_connector_upstream_voltage),
|
||||
dispenser_output_voltage(params.dispenser_output_voltage),
|
||||
dispesner_output_current(params.dispesner_output_current),
|
||||
dispenser_contactor_status(params.dispenser_contactor_status),
|
||||
dispenser_electronic_lock_status(params.dispenser_electronic_lock_status),
|
||||
power_stack_mock_config(params.power_stack_mock_config) {
|
||||
}
|
||||
|
||||
void DispenserTestBase::SetUp() {
|
||||
cout << "=-=-=-=-=-= SetUp start =-=-=-=-=-=" << endl;
|
||||
dispenser = std::make_shared<Dispenser>(dispenser_config, connector_configs);
|
||||
dispenser->start();
|
||||
power_stack_mock = std::shared_ptr<PowerStackMock>(PowerStackMock::from_config(power_stack_mock_config));
|
||||
power_stack_mock->start_modbus_event_loop();
|
||||
sleep_for_ms(20);
|
||||
cout << "=-=-=-=-=-= SetUp complete =-=-=-=-=-=" << endl;
|
||||
}
|
||||
|
||||
void DispenserTestBase::TearDown() {
|
||||
cout << "=-=-=-=-=-= TearDown started =-=-=-=-=-=" << endl;
|
||||
sleep_for_ms(20);
|
||||
dispenser->stop();
|
||||
power_stack_mock->stop();
|
||||
cout << "=-=-=-=-=-= TearDown complete =-=-=-=-=-=" << endl;
|
||||
}
|
||||
|
||||
void DispenserTestBase::sleep_for_ms(std::uint32_t ms) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
||||
}
|
||||
|
||||
DispenserWithTlsTest::DispenserWithTlsTest() :
|
||||
DispenserTestBase(DispenserTestParams{
|
||||
dispenser_config_with_tls, // dispenser_config
|
||||
{ConnectorConfig{
|
||||
// connector_configs
|
||||
5, // global_connector_number
|
||||
ConnectorType::CCS2, // connector_type
|
||||
100.0, // max_rated_charge_current
|
||||
0.0, // max_rated_output_power
|
||||
ConnectorCallbacks{
|
||||
// connector_callbacks
|
||||
[this]() { // connector_upstream_voltage
|
||||
return this->dispenser_connector_upstream_voltage.load();
|
||||
},
|
||||
[this]() { // output_voltage
|
||||
return this->dispenser_output_voltage.load();
|
||||
},
|
||||
[this]() { // output_current
|
||||
return this->dispesner_output_current.load();
|
||||
},
|
||||
[this]() { // contactor_status
|
||||
return this->dispenser_contactor_status.load();
|
||||
},
|
||||
[this]() { // electronic_lock_status
|
||||
return this->dispenser_electronic_lock_status.load();
|
||||
},
|
||||
},
|
||||
}},
|
||||
0.0, // dispenser_connector_upstream_voltage
|
||||
0.0, // dispenser_output_voltage
|
||||
0.0, // dispesner_output_current
|
||||
ContactorStatus::OFF, // dispenser_contactor_status
|
||||
ElectronicLockStatus::UNLOCKED, // dispenser_electronic_lock_status
|
||||
default_power_stack_mock_config_with_tls, // power_stack_mock_config
|
||||
}) {
|
||||
}
|
||||
|
||||
std::shared_ptr<Connector> DispenserWithTlsTest::connector() {
|
||||
return dispenser->get_connector(local_connector_number);
|
||||
}
|
||||
|
||||
std::optional<fusion_charger::goose::PowerRequirementRequest>
|
||||
DispenserWithTlsTest::get_last_power_requirement_request() {
|
||||
return power_stack_mock->get_last_power_requirement_request(global_connector_number);
|
||||
}
|
||||
|
||||
std::uint32_t DispenserWithTlsTest::get_stop_request_counter() {
|
||||
return power_stack_mock->get_stop_charge_request_counter(global_connector_number);
|
||||
}
|
||||
|
||||
std::uint32_t DispenserWithTlsTest::get_power_requirements_counter() {
|
||||
return power_stack_mock->get_power_requirements_counter(global_connector_number);
|
||||
}
|
||||
|
||||
float DispenserWithTlsTest::get_maximum_rated_charge_current() {
|
||||
return power_stack_mock->get_maximum_rated_charge_current(local_connector_number);
|
||||
}
|
||||
|
||||
ConnectionStatus DispenserWithTlsTest::get_connection_status() {
|
||||
return power_stack_mock->get_connection_status(local_connector_number);
|
||||
}
|
||||
|
||||
DispenserWithoutTlsTest::DispenserWithoutTlsTest() :
|
||||
DispenserTestBase(DispenserTestParams{
|
||||
dispenser_config_without_tls, // dispenser_config
|
||||
{ConnectorConfig{
|
||||
// connector_configs
|
||||
5, // global_connector_number
|
||||
ConnectorType::CCS2, // connector_type
|
||||
100.0, // max_rated_charge_current
|
||||
0.0, // max_rated_output_power
|
||||
ConnectorCallbacks{
|
||||
// connector_callbacks
|
||||
[this]() { // connector_upstream_voltage
|
||||
return this->dispenser_connector_upstream_voltage.load();
|
||||
},
|
||||
[this]() { // output_voltage
|
||||
return this->dispenser_output_voltage.load();
|
||||
},
|
||||
[this]() { // output_current
|
||||
return this->dispesner_output_current.load();
|
||||
},
|
||||
[this]() { // contactor_status
|
||||
return this->dispenser_contactor_status.load();
|
||||
},
|
||||
[this]() { // electronic_lock_status
|
||||
return this->dispenser_electronic_lock_status.load();
|
||||
},
|
||||
},
|
||||
}},
|
||||
0.0, // dispenser_connector_upstream_voltage
|
||||
0.0, // dispenser_output_voltage
|
||||
0.0, // dispesner_output_current
|
||||
ContactorStatus::OFF, // dispenser_contactor_status
|
||||
ElectronicLockStatus::UNLOCKED, // dispenser_electronic_lock_status
|
||||
default_power_stack_mock_config_without_tls, // power_stack_mock_config
|
||||
}) {
|
||||
}
|
||||
|
||||
std::shared_ptr<Connector> DispenserWithoutTlsTest::connector() {
|
||||
return dispenser->get_connector(local_connector_number);
|
||||
}
|
||||
|
||||
std::optional<fusion_charger::goose::PowerRequirementRequest>
|
||||
dispenser_fixture::DispenserWithoutTlsTest::get_last_power_requirement_request() {
|
||||
return power_stack_mock->get_last_power_requirement_request(global_connector_number);
|
||||
}
|
||||
|
||||
std::uint32_t DispenserWithoutTlsTest::get_stop_request_counter() {
|
||||
return power_stack_mock->get_stop_charge_request_counter(global_connector_number);
|
||||
}
|
||||
|
||||
std::uint32_t DispenserWithoutTlsTest::get_power_requirements_counter() {
|
||||
return power_stack_mock->get_power_requirements_counter(global_connector_number);
|
||||
}
|
||||
|
||||
float DispenserWithoutTlsTest::get_maximum_rated_charge_current() {
|
||||
return power_stack_mock->get_maximum_rated_charge_current(local_connector_number);
|
||||
}
|
||||
|
||||
ConnectionStatus DispenserWithoutTlsTest::get_connection_status() {
|
||||
return power_stack_mock->get_connection_status(local_connector_number);
|
||||
}
|
||||
|
||||
dispenser_fixture::DispenserWithMultipleConnectors::DispenserWithMultipleConnectors() :
|
||||
DispenserTestBase(DispenserTestParams{
|
||||
dispenser_config_with_tls, // dispenser_config
|
||||
{
|
||||
// connector_configs
|
||||
ConnectorConfig{5, // global_connector_number
|
||||
ConnectorType::CCS2, // connector_type
|
||||
100.0, // max_rated_charge_current
|
||||
0.0, // max_rated_output_power
|
||||
ConnectorCallbacks{
|
||||
[]() { return 100; }, // connector_upstream_voltage
|
||||
[]() { return 101; }, // output_voltage
|
||||
[]() { return 102; }, // output_current
|
||||
[]() { return ContactorStatus::ON; }, // contactor_status
|
||||
[]() { return ElectronicLockStatus::UNLOCKED; }, // electronic_lock_status
|
||||
}},
|
||||
ConnectorConfig{10, // global_connector_number
|
||||
ConnectorType::CCS2, // connector_type
|
||||
200.0, // max_rated_charge_current
|
||||
0.0, // max_rated_output_power
|
||||
ConnectorCallbacks{
|
||||
[]() { return 200; }, // connector_upstream_voltage
|
||||
[]() { return 201; }, // output_voltage
|
||||
[]() { return 202; }, // output_current
|
||||
[]() { return ContactorStatus::ON; }, // contactor_status
|
||||
[]() { return ElectronicLockStatus::LOCKED; }, // electronic_lock_status
|
||||
}},
|
||||
ConnectorConfig{15, // global_connector_number
|
||||
ConnectorType::CCS2, // connector_type
|
||||
300.0, // max_rated_charge_current
|
||||
0.0, // max_rated_output_power
|
||||
ConnectorCallbacks{
|
||||
[]() { return 300; }, // connector_upstream_voltage
|
||||
[]() { return 301; }, // output_voltage
|
||||
[]() { return 302; }, // output_current
|
||||
[]() { return ContactorStatus::OFF; }, // contactor_status
|
||||
[]() { return ElectronicLockStatus::UNLOCKED; }, // electronic_lock_status
|
||||
}},
|
||||
ConnectorConfig{4, // global_connector_number
|
||||
ConnectorType::CCS2, // connector_type
|
||||
400.0, // max_rated_charge_current
|
||||
0.0, // max_rated_output_power
|
||||
ConnectorCallbacks{
|
||||
[]() { return 400; }, // connector_upstream_voltage
|
||||
[]() { return 401; }, // output_voltage
|
||||
[]() { return 402; }, // output_current
|
||||
[]() { return ContactorStatus::OFF; }, // contactor_status
|
||||
[]() { return ElectronicLockStatus::LOCKED; }, // electronic_lock_status
|
||||
}},
|
||||
},
|
||||
0.0, // dispenser_connector_upstream_voltage
|
||||
0.0, // dispenser_output_voltage
|
||||
0.0, // dispesner_output_current
|
||||
ContactorStatus::OFF, // dispenser_contactor_status
|
||||
ElectronicLockStatus::UNLOCKED, // dispenser_electronic_lock_status
|
||||
default_power_stack_mock_config_with_tls, // power_stack_mock_config
|
||||
})
|
||||
|
||||
{
|
||||
}
|
||||
|
||||
std::shared_ptr<Connector> DispenserWithMultipleConnectors::get_connector(std::uint16_t local_connector_number) {
|
||||
return dispenser->get_connector(local_connector_number);
|
||||
}
|
||||
|
||||
void DispenserWithMultipleConnectors::set_up_psu_for_operation() {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
sleep_for_ms(10);
|
||||
}
|
||||
|
||||
void DispenserWithMultipleConnectors::connect_car(std::uint16_t local_connector_number) {
|
||||
get_connector(local_connector_number)->on_car_connected();
|
||||
sleep_for_ms(10);
|
||||
}
|
||||
void DispenserWithMultipleConnectors::send_hmac_key(std::uint16_t local_connector_number) {
|
||||
power_stack_mock->send_hmac_key(local_connector_number);
|
||||
sleep_for_ms(10);
|
||||
}
|
||||
|
||||
void DispenserWithMultipleConnectors::set_export_values(std::uint16_t local_connector_number, float voltage,
|
||||
float current) {
|
||||
get_connector(local_connector_number)->new_export_voltage_current(voltage, current);
|
||||
sleep_for_ms(10);
|
||||
}
|
||||
|
||||
void DispenserWithMultipleConnectors::set_mode_phase(std::uint16_t local_connector_number, ModePhase mode_phase) {
|
||||
get_connector(local_connector_number)->on_mode_phase_change(mode_phase);
|
||||
sleep_for_ms(10);
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> DispenserWithMultipleConnectors::get_stop_request_counter() {
|
||||
auto result = std::array<std::uint32_t, 4>();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
auto counter = power_stack_mock->get_stop_charge_request_counter(connector_configs[i].global_connector_number);
|
||||
|
||||
result[i] = counter;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void DispenserWithMultipleConnectors::disconnect_car(std::uint16_t local_connector_number) {
|
||||
get_connector(local_connector_number)->on_car_disconnected();
|
||||
sleep_for_ms(10);
|
||||
}
|
||||
|
||||
void DispenserWithMultipleConnectors::assert_working_status(std::array<WorkingStatus, 4> expected_status) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
auto actual_status = get_connector(i + 1)->get_working_status();
|
||||
EXPECT_EQ(actual_status, expected_status[i]) << "Regarding connector " << i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void DispenserWithMultipleConnectors::assert_requirement_type(
|
||||
std::array<std::optional<fusion_charger::goose::RequirementType>, 4> expected_types) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
auto actual_status =
|
||||
power_stack_mock->get_last_power_requirement_request(connector_configs[i].global_connector_number);
|
||||
|
||||
if (actual_status == nullopt && expected_types[i] == nullopt) {
|
||||
continue;
|
||||
} else if (actual_status == nullopt) {
|
||||
FAIL() << "Actual Status is NULL , but expected was: " << (std::uint16_t)expected_types[i].value()
|
||||
<< " regarding connector " << i + 1;
|
||||
} else if (expected_types[i] == nullopt) {
|
||||
FAIL() << "Actual Status is: " << (std::uint16_t)actual_status.value().requirement_type
|
||||
<< " , but expected was NULL"
|
||||
<< " regarding connector " << i + 1;
|
||||
}
|
||||
|
||||
EXPECT_EQ(actual_status->requirement_type, expected_types[i]) << "Regarding connector " << i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void DispenserWithMultipleConnectors::assert_stop_request_counter_greater_or_equal(
|
||||
std::array<std::uint32_t, 4> expected) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
auto actual = power_stack_mock->get_stop_charge_request_counter(connector_configs[i].global_connector_number);
|
||||
|
||||
EXPECT_GE(actual, expected[i]) << "Regarding connector " << i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dispenser_fixture
|
||||
|
||||
} // namespace user_acceptance_tests
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "fusion_charger/goose/power_request.hpp"
|
||||
#include "user_acceptance_tests/dispenser_test_fixture.hpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
using namespace user_acceptance_tests::dispenser_fixture;
|
||||
using namespace fusion_charger::goose;
|
||||
|
||||
TEST_F(DispenserWithMultipleConnectors, ChargingSession) {
|
||||
set_up_psu_for_operation();
|
||||
|
||||
assert_working_status(
|
||||
{WorkingStatus::STANDBY, WorkingStatus::STANDBY, WorkingStatus::STANDBY, WorkingStatus::STANDBY});
|
||||
|
||||
connect_car(1);
|
||||
connect_car(3);
|
||||
|
||||
assert_working_status({WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED, WorkingStatus::STANDBY,
|
||||
WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED, WorkingStatus::STANDBY});
|
||||
|
||||
connect_car(2);
|
||||
send_hmac_key(2);
|
||||
|
||||
set_export_values(2, 200, 15);
|
||||
assert_requirement_type({nullopt, RequirementType::ModulePlaceholderRequest, nullopt, nullopt});
|
||||
|
||||
set_mode_phase(2, ModePhase::ExportCharging);
|
||||
assert_working_status({WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED, WorkingStatus::CHARGING,
|
||||
WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED, WorkingStatus::STANDBY});
|
||||
|
||||
connect_car(3);
|
||||
send_hmac_key(3);
|
||||
set_export_values(3, 300, 30);
|
||||
set_mode_phase(3, ModePhase::ExportCharging);
|
||||
assert_requirement_type({nullopt, RequirementType::Charging, RequirementType::Charging, nullopt});
|
||||
assert_working_status({WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED, WorkingStatus::CHARGING,
|
||||
WorkingStatus::CHARGING, WorkingStatus::STANDBY});
|
||||
|
||||
set_mode_phase(2, ModePhase::Off);
|
||||
|
||||
auto current_stop_request_coutner = get_stop_request_counter();
|
||||
assert_working_status({WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED, WorkingStatus::CHARGING_COMPLETE,
|
||||
WorkingStatus::CHARGING, WorkingStatus::STANDBY});
|
||||
|
||||
// Wait for the stop requests to be send to the mock
|
||||
sleep_for_ms(20);
|
||||
|
||||
current_stop_request_coutner[1] += 1;
|
||||
assert_stop_request_counter_greater_or_equal(current_stop_request_coutner);
|
||||
|
||||
disconnect_car(2);
|
||||
assert_working_status({WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED, WorkingStatus::STANDBY,
|
||||
WorkingStatus::CHARGING, WorkingStatus::STANDBY});
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithMultipleConnectors, ConnectorCallbacks) {
|
||||
auto actual_1 = power_stack_mock->get_connector_callback_values(1);
|
||||
auto expected_1 = ConnectorCallbackResults{
|
||||
100.0f, 101.0f, 102.0f, ContactorStatus::ON, ElectronicLockStatus::UNLOCKED,
|
||||
};
|
||||
EXPECT_EQ(actual_1, expected_1);
|
||||
|
||||
auto actual_2 = power_stack_mock->get_connector_callback_values(2);
|
||||
auto expected_2 = ConnectorCallbackResults{
|
||||
200.0f, 201.0f, 202.0f, ContactorStatus::ON, ElectronicLockStatus::LOCKED,
|
||||
};
|
||||
EXPECT_EQ(actual_2, expected_2);
|
||||
|
||||
auto actual_3 = power_stack_mock->get_connector_callback_values(3);
|
||||
auto expected_3 = ConnectorCallbackResults{
|
||||
300.0f, 301.0f, 302.0f, ContactorStatus::OFF, ElectronicLockStatus::UNLOCKED,
|
||||
};
|
||||
EXPECT_EQ(actual_3, expected_3);
|
||||
|
||||
auto actual_4 = power_stack_mock->get_connector_callback_values(4);
|
||||
auto expected_4 = ConnectorCallbackResults{
|
||||
400.0f, 401.0f, 402.0f, ContactorStatus::OFF, ElectronicLockStatus::LOCKED,
|
||||
};
|
||||
EXPECT_EQ(actual_4, expected_4);
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "dispenser.hpp"
|
||||
#include "fusion_charger/goose/power_request.hpp"
|
||||
#include "fusion_charger/modbus/registers/error.hpp"
|
||||
#include "power_stack_mock/power_stack_mock.hpp"
|
||||
#include "user_acceptance_tests/dispenser_test_fixture.hpp"
|
||||
#include "gmock/gmock.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
using namespace user_acceptance_tests::dispenser_fixture;
|
||||
|
||||
TEST_F(DispenserWithTlsTest, StateCarDisconnected) {
|
||||
EXPECT_EQ(get_connection_status(), ConnectionStatus::NOT_CONNECTED);
|
||||
EXPECT_EQ(dispenser->get_psu_running_mode(), PSURunningMode::STARTING_UP);
|
||||
EXPECT_EQ(connector()->module_placeholder_allocation_failed(), false);
|
||||
EXPECT_EQ(connector()->get_output_port_availability(), PsuOutputPortAvailability::NOT_AVAILABLE);
|
||||
EXPECT_EQ(dispenser->get_psu_communication_state(), DispenserPsuCommunicationState::INITIALIZING);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY);
|
||||
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
EXPECT_EQ(dispenser->get_psu_running_mode(), PSURunningMode::RUNNING);
|
||||
|
||||
power_stack_mock->send_mac_address();
|
||||
EXPECT_EQ(dispenser->get_psu_communication_state(), DispenserPsuCommunicationState::READY);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, CarConnectedAndReadyToCharge) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
|
||||
connector()->on_car_connected();
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED);
|
||||
EXPECT_EQ(get_connection_status(), ConnectionStatus::FULL_CONNECTED);
|
||||
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(5);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_STARTING);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type,
|
||||
fusion_charger::goose::RequirementType::ModulePlaceholderRequest);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, ChargingACarUpToRegularDisconnect) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
connector()->on_car_connected();
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
|
||||
// Export Cable Check
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCableCheck);
|
||||
sleep_for_ms(10);
|
||||
auto stop_request_counter_before_charging = get_stop_request_counter();
|
||||
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type,
|
||||
fusion_charger::goose::RequirementType::InsulationDetectionVoltageOutput);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->mode, fusion_charger::goose::Mode::ConstantCurrent);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 5);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 200);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_STARTING);
|
||||
|
||||
connector()->new_export_voltage_current(100, 1);
|
||||
sleep_for_ms(10);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 1);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 100);
|
||||
|
||||
// OffCableCheck
|
||||
connector()->on_mode_phase_change(ModePhase::OffCableCheck);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type,
|
||||
fusion_charger::goose::RequirementType::InsulationDetectionVoltageOutputStoppage);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->mode, fusion_charger::goose::Mode::ConstantCurrent);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 0);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 0);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_STARTING);
|
||||
|
||||
// Export Precharge
|
||||
connector()->on_mode_phase_change(ModePhase::ExportPrecharge);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type,
|
||||
fusion_charger::goose::RequirementType::PrechargeVoltageOutput);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->mode, fusion_charger::goose::Mode::ConstantCurrent);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 1);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 100);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_STARTING);
|
||||
|
||||
connector()->new_export_voltage_current(300, 10);
|
||||
sleep_for_ms(10);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 10);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 300);
|
||||
|
||||
// Export Charging
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCharging);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type, fusion_charger::goose::RequirementType::Charging);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->mode, fusion_charger::goose::Mode::ConstantCurrent);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 10);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 300);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING);
|
||||
|
||||
connector()->new_export_voltage_current(30, 1);
|
||||
sleep_for_ms(10);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 1);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 30);
|
||||
|
||||
auto stop_request_counter_before_charge_complete = get_stop_request_counter();
|
||||
EXPECT_EQ(stop_request_counter_before_charge_complete, stop_request_counter_before_charging);
|
||||
|
||||
// Completed
|
||||
connector()->on_mode_phase_change(ModePhase::Off);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_COMPLETE);
|
||||
EXPECT_GT(get_stop_request_counter(), stop_request_counter_before_charge_complete);
|
||||
|
||||
// Completed
|
||||
connector()->on_car_disconnected();
|
||||
EXPECT_EQ(get_connection_status(), ConnectionStatus::NOT_CONNECTED);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, ChargingRestartWithoutDisconnect) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
connector()->on_car_connected();
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
// Export Charging
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCharging);
|
||||
sleep_for_ms(10);
|
||||
|
||||
// Completed
|
||||
connector()->on_mode_phase_change(ModePhase::Off);
|
||||
sleep_for_ms(10);
|
||||
|
||||
//////// Restart Charging ////////
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCableCheck);
|
||||
sleep_for_ms(10);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED);
|
||||
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_STARTING);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type,
|
||||
fusion_charger::goose::RequirementType::InsulationDetectionVoltageOutput);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 5);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 200);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, CarDisconnectBeforeHmacKey) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
connector()->on_car_connected();
|
||||
sleep_for_ms(10);
|
||||
|
||||
connector()->on_car_disconnected();
|
||||
sleep_for_ms(10);
|
||||
// We can't test stop requests because we don't have an HMAC key
|
||||
EXPECT_EQ(get_connection_status(), ConnectionStatus::NOT_CONNECTED);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, CarDisconnectBeforeCableCheck) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
connector()->on_car_connected();
|
||||
sleep_for_ms(10);
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
sleep_for_ms(10);
|
||||
|
||||
auto power_stack_counter = get_stop_request_counter();
|
||||
connector()->on_car_disconnected();
|
||||
sleep_for_ms(10);
|
||||
EXPECT_GT(get_stop_request_counter(), power_stack_counter);
|
||||
EXPECT_EQ(get_connection_status(), ConnectionStatus::NOT_CONNECTED);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, CarDisconnectDuringCharging) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
connector()->on_car_connected();
|
||||
sleep_for_ms(10);
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCableCheck);
|
||||
sleep_for_ms(10);
|
||||
|
||||
auto power_stack_counter = get_stop_request_counter();
|
||||
connector()->on_car_disconnected();
|
||||
sleep_for_ms(10);
|
||||
EXPECT_GT(get_stop_request_counter(), power_stack_counter);
|
||||
EXPECT_EQ(get_connection_status(), ConnectionStatus::NOT_CONNECTED);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, FaultsGetPropagatedCorrectly) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
connector()->on_car_connected();
|
||||
sleep_for_ms(10);
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCableCheck);
|
||||
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::FAULTY);
|
||||
|
||||
EXPECT_EQ(dispenser->get_psu_running_mode(), PSURunningMode::FAULTY);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, CheckMaximumRatedChargeCurrentIsSet) {
|
||||
EXPECT_EQ(get_maximum_rated_charge_current(), 100);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, ReadDispenserInformationAndConfiguration) {
|
||||
auto expected_dispenser_information = DispenserInformation{.manufacturer = 0x0002,
|
||||
.model = 0x0080,
|
||||
.protocol_version = 1,
|
||||
.hardware_version = 3,
|
||||
.software_version = "v1.2.3+456"};
|
||||
|
||||
EXPECT_EQ(power_stack_mock->get_dispenser_information(), expected_dispenser_information);
|
||||
|
||||
auto expected_esn = "01234567890ABCDEF";
|
||||
EXPECT_EQ(power_stack_mock->get_dispenser_esn(), expected_esn);
|
||||
}
|
||||
|
||||
// This leads to the everest module restarting the dispenser object tested
|
||||
// here
|
||||
// (stop(); start(); is called)
|
||||
TEST_F(DispenserWithTlsTest, PsuCommunicationStateIsFailedAfterModbusHeartbeatTimeout) {
|
||||
sleep_for_ms(2'100);
|
||||
auto init_state = dispenser->get_psu_communication_state();
|
||||
|
||||
EXPECT_EQ(init_state, DispenserPsuCommunicationState::FAILED);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, CarConnectWithoutModePhaseChangeContinuesSendingModulePlaceholderRequests) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
|
||||
connector()->on_car_connected();
|
||||
sleep_for_ms(10);
|
||||
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
auto post_connect_stop_frame_counter = get_stop_request_counter();
|
||||
auto post_connect_power_requirement_counter = get_power_requirements_counter();
|
||||
|
||||
sleep_for_ms(50);
|
||||
|
||||
// Still sends module placeholder requests
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type,
|
||||
fusion_charger::goose::RequirementType::ModulePlaceholderRequest);
|
||||
EXPECT_GT(get_power_requirements_counter(), post_connect_power_requirement_counter);
|
||||
|
||||
// No stop requests sent
|
||||
EXPECT_EQ(get_stop_request_counter(), post_connect_stop_frame_counter);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, TimeSyncEverySecond) {
|
||||
auto time1 = power_stack_mock->get_utc_time();
|
||||
EXPECT_NEAR(time1, (std::uint32_t)std::time(NULL), 1);
|
||||
|
||||
sleep_for_ms(1'000);
|
||||
auto time2 = power_stack_mock->get_utc_time();
|
||||
|
||||
EXPECT_EQ(time1 + 1, time2);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, ErrorReportingDefaultState) {
|
||||
EXPECT_THAT(dispenser->get_raised_errors(), testing::IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, ErrorReportingPowerUnit) {
|
||||
power_stack_mock->write_registers(0x4000, {(std::uint16_t)AlarmStatus::ALARM});
|
||||
sleep_for_ms(10);
|
||||
|
||||
ErrorEvent error_event_1;
|
||||
error_event_1.error_category = ErrorCategory::PowerUnit;
|
||||
error_event_1.error_subcategory.power_unit = ErrorSubcategoryPowerUnit::HighVoltageDoorStatusSensor;
|
||||
error_event_1.payload.alarm = AlarmStatus::ALARM;
|
||||
|
||||
EXPECT_THAT(dispenser->get_raised_errors(), testing::ElementsAre(error_event_1));
|
||||
|
||||
ErrorEvent error_event_2;
|
||||
error_event_2.error_category = ErrorCategory::PowerUnit;
|
||||
error_event_2.error_subcategory.power_unit = ErrorSubcategoryPowerUnit::HighVoltageDoorStatusSensor;
|
||||
error_event_2.payload.alarm = AlarmStatus::ALARM;
|
||||
|
||||
EXPECT_THAT(dispenser->get_raised_errors(), testing::ElementsAre(error_event_2));
|
||||
|
||||
power_stack_mock->write_registers(0x4000, {(std::uint16_t)AlarmStatus::NORMAL});
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_THAT(dispenser->get_raised_errors(), testing::IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, ErrorReportingChargingPowerUnit) {
|
||||
power_stack_mock->write_registers(0x4008, {(std::uint16_t)AlarmStatus::ALARM});
|
||||
power_stack_mock->write_registers(0x4012, {25});
|
||||
sleep_for_ms(10);
|
||||
|
||||
ErrorEvent error_event_1;
|
||||
error_event_1.error_category = ErrorCategory::ChargingPowerUnit;
|
||||
error_event_1.error_subcategory.charging_power_unit = ErrorSubcategoryChargingPowerUnit::SoftStartFault;
|
||||
error_event_1.payload.alarm = AlarmStatus::ALARM;
|
||||
|
||||
ErrorEvent error_event_2;
|
||||
error_event_2.error_category = ErrorCategory::ChargingPowerUnit;
|
||||
error_event_2.error_subcategory.charging_power_unit = ErrorSubcategoryChargingPowerUnit::ModbusTcpCertificate;
|
||||
error_event_2.payload.error_flags = 25;
|
||||
|
||||
EXPECT_THAT(dispenser->get_raised_errors(), testing::ElementsAre(error_event_1, error_event_2));
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, ErrorReportingAcBranch) {
|
||||
power_stack_mock->write_registers(0x4020, {0, 1});
|
||||
power_stack_mock->write_registers(0x4022, {1, 0});
|
||||
sleep_for_ms(10);
|
||||
|
||||
ErrorEvent error_event_1;
|
||||
error_event_1.error_category = ErrorCategory::AcBranch;
|
||||
error_event_1.error_subcategory.ac_branch = ErrorSubcategoryAcBranch::AcBranch1;
|
||||
error_event_1.payload.error_flags = 1;
|
||||
|
||||
ErrorEvent error_event_2;
|
||||
error_event_2.error_category = ErrorCategory::AcBranch;
|
||||
error_event_2.error_subcategory.ac_branch = ErrorSubcategoryAcBranch::AcBranch2;
|
||||
error_event_2.payload.error_flags = 0x10000;
|
||||
|
||||
EXPECT_THAT(dispenser->get_raised_errors(), testing::ElementsAre(error_event_1, error_event_2));
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, ErrorReportingAcDcRectifier) {
|
||||
power_stack_mock->write_registers(0x4040, {0, 1});
|
||||
power_stack_mock->write_registers(0x404A, {1, 0});
|
||||
sleep_for_ms(10);
|
||||
|
||||
ErrorEvent error_event_1;
|
||||
error_event_1.error_category = ErrorCategory::AcDcRectifier;
|
||||
error_event_1.error_subcategory.ac_dc_rectifier = ErrorSubcategoryAcDcRectifier::rectifier_1;
|
||||
error_event_1.payload.error_flags = 1;
|
||||
|
||||
ErrorEvent error_event_2;
|
||||
error_event_2.error_category = ErrorCategory::AcDcRectifier;
|
||||
error_event_2.error_subcategory.ac_dc_rectifier = ErrorSubcategoryAcDcRectifier::rectifier_6;
|
||||
error_event_2.payload.error_flags = 0x10000;
|
||||
|
||||
EXPECT_THAT(dispenser->get_raised_errors(), testing::ElementsAre(error_event_1, error_event_2));
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, ErrorReportingDcDcChargingModule) {
|
||||
power_stack_mock->write_registers(0x4070, {0, 1});
|
||||
power_stack_mock->write_registers(0x4086, {1, 0});
|
||||
sleep_for_ms(10);
|
||||
|
||||
ErrorEvent error_event_1;
|
||||
error_event_1.error_category = ErrorCategory::DcDcChargingModule;
|
||||
error_event_1.error_subcategory.dc_dc_charging_module = ErrorSubcategoryDcDcChargingModule::DcDcModule1;
|
||||
error_event_1.payload.error_flags = 1;
|
||||
|
||||
ErrorEvent error_event_2;
|
||||
error_event_2.error_category = ErrorCategory::DcDcChargingModule;
|
||||
error_event_2.error_subcategory.dc_dc_charging_module = ErrorSubcategoryDcDcChargingModule::DcDcModule12;
|
||||
error_event_2.payload.error_flags = 0x10000;
|
||||
|
||||
EXPECT_THAT(dispenser->get_raised_errors(), testing::ElementsAre(error_event_1, error_event_2));
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, ErrorReportingCoolingSection) {
|
||||
power_stack_mock->write_registers(0x40D0, {0x12, 0x3456});
|
||||
sleep_for_ms(10);
|
||||
|
||||
ErrorEvent error_event;
|
||||
error_event.error_category = ErrorCategory::CoolingSection;
|
||||
error_event.error_subcategory.cooling_section = ErrorSubcategoryCoolingSection::CoolingUnit1;
|
||||
error_event.payload.error_flags = 0x123456;
|
||||
|
||||
EXPECT_THAT(dispenser->get_raised_errors(), testing::ElementsAre(error_event));
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, ErrorReportingPowerDistributionModule) {
|
||||
power_stack_mock->write_registers(0x40E0, {0, 1});
|
||||
power_stack_mock->write_registers(0x40E8, {1, 0});
|
||||
sleep_for_ms(10);
|
||||
|
||||
ErrorEvent error_event_1;
|
||||
error_event_1.error_category = ErrorCategory::ErrorSubcategoryPowerDistributionModule;
|
||||
error_event_1.error_subcategory.power_distribution_module =
|
||||
ErrorSubcategoryPowerDistributionModule::PowerDistributionModule1;
|
||||
error_event_1.payload.error_flags = 1;
|
||||
|
||||
ErrorEvent error_event_2;
|
||||
error_event_2.error_category = ErrorCategory::ErrorSubcategoryPowerDistributionModule;
|
||||
error_event_2.error_subcategory.power_distribution_module =
|
||||
ErrorSubcategoryPowerDistributionModule::PowerDistributionModule5;
|
||||
error_event_2.payload.error_flags = 0x10000;
|
||||
|
||||
EXPECT_THAT(dispenser->get_raised_errors(), testing::ElementsAre(error_event_1, error_event_2));
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, String) {
|
||||
ErrorEvent e;
|
||||
e.error_category = ErrorCategory::PowerUnit;
|
||||
e.error_subcategory.power_unit = ErrorSubcategoryPowerUnit::HighVoltageDoorStatusSensor;
|
||||
e.payload.alarm = AlarmStatus::ALARM;
|
||||
|
||||
printf("%s\n", e.to_error_log_string().c_str());
|
||||
|
||||
e.error_category = ErrorCategory::ChargingPowerUnit;
|
||||
e.error_subcategory = {.charging_power_unit = ErrorSubcategoryChargingPowerUnit::PhaseSequenceAbornmalAlarm};
|
||||
e.payload.alarm = AlarmStatus::NORMAL;
|
||||
printf("%s\n", e.to_error_log_string().c_str());
|
||||
|
||||
e.error_category = ErrorCategory::ChargingPowerUnit;
|
||||
e.error_subcategory = {.charging_power_unit = ErrorSubcategoryChargingPowerUnit::ModbusTcpCertificate};
|
||||
e.payload.error_flags = 2;
|
||||
printf("%s\n", e.to_error_log_string().c_str());
|
||||
|
||||
e.error_category = ErrorCategory::CoolingSection;
|
||||
e.error_subcategory = {.cooling_section = ErrorSubcategoryCoolingSection::CoolingUnit1};
|
||||
e.payload.error_flags = 0x12345678;
|
||||
printf("%s\n", e.to_error_log_string().c_str());
|
||||
|
||||
e.payload.error_flags = 0x102;
|
||||
printf("%s\n", e.to_error_log_string().c_str());
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, module_placeholder_allocation_timeout) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
|
||||
power_stack_mock->set_enable_answer_module_placeholder_allocation(false);
|
||||
|
||||
connector()->on_car_connected();
|
||||
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCharging);
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
|
||||
sleep_for_ms(100);
|
||||
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_STARTING);
|
||||
|
||||
// Wait 3s while keeping the modbus connection happy
|
||||
for (int i = 0; i < 4; i++) {
|
||||
sleep_for_ms(1000);
|
||||
power_stack_mock->send_mac_address();
|
||||
}
|
||||
|
||||
sleep_for_ms(500);
|
||||
|
||||
// connection still established
|
||||
EXPECT_EQ(dispenser->get_psu_communication_state(), DispenserPsuCommunicationState::READY);
|
||||
|
||||
EXPECT_EQ(connector()->module_placeholder_allocation_failed(), true);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, module_placeholder_allocation_timeout_then_normal) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
|
||||
power_stack_mock->set_enable_answer_module_placeholder_allocation(false);
|
||||
|
||||
connector()->on_car_connected();
|
||||
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCharging);
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
|
||||
sleep_for_ms(100);
|
||||
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_STARTING);
|
||||
|
||||
// Wait 3s while keeping the modbus connection happy
|
||||
for (int i = 0; i < 3; i++) {
|
||||
sleep_for_ms(1000);
|
||||
power_stack_mock->send_mac_address();
|
||||
}
|
||||
|
||||
sleep_for_ms(500);
|
||||
|
||||
// connection still established
|
||||
EXPECT_EQ(dispenser->get_psu_communication_state(), DispenserPsuCommunicationState::READY);
|
||||
|
||||
EXPECT_EQ(connector()->module_placeholder_allocation_failed(), true);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING);
|
||||
connector()->on_car_disconnected();
|
||||
|
||||
power_stack_mock->set_enable_answer_module_placeholder_allocation(true);
|
||||
|
||||
sleep_for_ms(100);
|
||||
connector()->on_car_connected();
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCharging);
|
||||
sleep_for_ms(100);
|
||||
EXPECT_EQ(connector()->module_placeholder_allocation_failed(), false);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, acquire_hmac_post_start) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
|
||||
auto stop_frame_counter = this->get_stop_request_counter();
|
||||
sleep_for_ms(10);
|
||||
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCableCheck);
|
||||
|
||||
EXPECT_EQ(this->get_stop_request_counter(), stop_frame_counter);
|
||||
|
||||
auto thread = std::thread([this]() {
|
||||
while (connector()->get_working_status() != WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED) {
|
||||
std::this_thread::sleep_for(100ms);
|
||||
// send mac address to keep modbus alive
|
||||
this->power_stack_mock->send_mac_address();
|
||||
}
|
||||
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
});
|
||||
connector()->car_connect_disconnect_cycle(std::chrono::seconds(10));
|
||||
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_GT(this->get_stop_request_counter(), stop_frame_counter);
|
||||
|
||||
thread.join();
|
||||
|
||||
// Now do a simple charge (and check that on_mode_phase_change is
|
||||
// persistent/restored)
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
connector()->on_car_connected();
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type,
|
||||
fusion_charger::goose::RequirementType::InsulationDetectionVoltageOutput);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithTlsTest, acquire_hmac_post_start_timeout) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
|
||||
auto stop_frame_counter = this->get_stop_request_counter();
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(this->get_stop_request_counter(), stop_frame_counter);
|
||||
|
||||
// Same as acquire_hmac_post_start but without sending the hmac key
|
||||
auto thread = std::thread([this]() {
|
||||
while (connector()->get_working_status() != WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED) {
|
||||
std::this_thread::sleep_for(100ms);
|
||||
// send mac address to keep modbus alive
|
||||
this->power_stack_mock->send_mac_address();
|
||||
}
|
||||
});
|
||||
|
||||
auto time_before = std::chrono::steady_clock::now();
|
||||
connector()->car_connect_disconnect_cycle(std::chrono::milliseconds(3000));
|
||||
auto time_needed = std::chrono::steady_clock::now() - time_before;
|
||||
auto ms_needed = std::chrono::duration_cast<std::chrono::milliseconds>(time_needed).count();
|
||||
|
||||
EXPECT_NEAR(ms_needed, 3000, 150); // wait for 3 seconds (timeout)
|
||||
|
||||
sleep_for_ms(10);
|
||||
|
||||
// No stop requests sent (as no hmac key was sent)
|
||||
EXPECT_EQ(this->get_stop_request_counter(), stop_frame_counter);
|
||||
|
||||
thread.join();
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "dispenser.hpp"
|
||||
#include "fusion_charger/goose/power_request.hpp"
|
||||
#include "power_stack_mock/power_stack_mock.hpp"
|
||||
#include "user_acceptance_tests/dispenser_test_fixture.hpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
using namespace user_acceptance_tests::dispenser_fixture;
|
||||
|
||||
TEST_F(DispenserWithoutTlsTest, StateCarDisconnected) {
|
||||
EXPECT_EQ(get_connection_status(), ConnectionStatus::NOT_CONNECTED);
|
||||
EXPECT_EQ(dispenser->get_psu_running_mode(), PSURunningMode::STARTING_UP);
|
||||
EXPECT_EQ(connector()->module_placeholder_allocation_failed(), false);
|
||||
EXPECT_EQ(connector()->get_output_port_availability(), PsuOutputPortAvailability::NOT_AVAILABLE);
|
||||
EXPECT_EQ(dispenser->get_psu_communication_state(), DispenserPsuCommunicationState::INITIALIZING);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY);
|
||||
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
EXPECT_EQ(dispenser->get_psu_running_mode(), PSURunningMode::RUNNING);
|
||||
|
||||
power_stack_mock->send_mac_address();
|
||||
EXPECT_EQ(dispenser->get_psu_communication_state(), DispenserPsuCommunicationState::READY);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithoutTlsTest, CarConnectedAndReadyToCharge) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
|
||||
connector()->on_car_connected();
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED);
|
||||
EXPECT_EQ(get_connection_status(), ConnectionStatus::FULL_CONNECTED);
|
||||
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(5);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_STARTING);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type,
|
||||
fusion_charger::goose::RequirementType::ModulePlaceholderRequest);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithoutTlsTest, ChargingACarUpToRegularDisconnect) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
connector()->on_car_connected();
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
|
||||
// Export Cable Check
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCableCheck);
|
||||
sleep_for_ms(10);
|
||||
auto stop_request_counter_before_charging = get_stop_request_counter();
|
||||
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type,
|
||||
fusion_charger::goose::RequirementType::InsulationDetectionVoltageOutput);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->mode, fusion_charger::goose::Mode::ConstantCurrent);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 5);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 200);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_STARTING);
|
||||
|
||||
connector()->new_export_voltage_current(100, 1);
|
||||
sleep_for_ms(10);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 1);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 100);
|
||||
|
||||
// OffCableCheck
|
||||
connector()->on_mode_phase_change(ModePhase::OffCableCheck);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type,
|
||||
fusion_charger::goose::RequirementType::InsulationDetectionVoltageOutputStoppage);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->mode, fusion_charger::goose::Mode::ConstantCurrent);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 0);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 0);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_STARTING);
|
||||
|
||||
// Export Precharge
|
||||
connector()->on_mode_phase_change(ModePhase::ExportPrecharge);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type,
|
||||
fusion_charger::goose::RequirementType::PrechargeVoltageOutput);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->mode, fusion_charger::goose::Mode::ConstantCurrent);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 1);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 100);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_STARTING);
|
||||
|
||||
connector()->new_export_voltage_current(300, 10);
|
||||
sleep_for_ms(10);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 10);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 300);
|
||||
|
||||
// Export Charging
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCharging);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type, fusion_charger::goose::RequirementType::Charging);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->mode, fusion_charger::goose::Mode::ConstantCurrent);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 10);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 300);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING);
|
||||
|
||||
connector()->new_export_voltage_current(30, 1);
|
||||
sleep_for_ms(10);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 1);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 30);
|
||||
|
||||
auto stop_request_counter_before_charge_complete = get_stop_request_counter();
|
||||
EXPECT_EQ(stop_request_counter_before_charge_complete, stop_request_counter_before_charging);
|
||||
|
||||
// Completed
|
||||
connector()->on_mode_phase_change(ModePhase::Off);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_COMPLETE);
|
||||
EXPECT_GT(get_stop_request_counter(), stop_request_counter_before_charge_complete);
|
||||
|
||||
// Completed
|
||||
connector()->on_car_disconnected();
|
||||
EXPECT_EQ(get_connection_status(), ConnectionStatus::NOT_CONNECTED);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithoutTlsTest, ChargingRestartWithoutDisconnect) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
connector()->on_car_connected();
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
// Export Charging
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCharging);
|
||||
sleep_for_ms(10);
|
||||
|
||||
// Completed
|
||||
connector()->on_mode_phase_change(ModePhase::Off);
|
||||
sleep_for_ms(10);
|
||||
|
||||
//////// Restart Charging ////////
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCableCheck);
|
||||
sleep_for_ms(10);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED);
|
||||
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::CHARGING_STARTING);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->requirement_type,
|
||||
fusion_charger::goose::RequirementType::InsulationDetectionVoltageOutput);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->current, 5);
|
||||
EXPECT_EQ(get_last_power_requirement_request()->voltage, 200);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithoutTlsTest, CarDisconnectBeforeHmacKey) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
connector()->on_car_connected();
|
||||
sleep_for_ms(10);
|
||||
|
||||
connector()->on_car_disconnected();
|
||||
sleep_for_ms(10);
|
||||
// We can't test stop requests because we don't have an HMAC key
|
||||
EXPECT_EQ(get_connection_status(), ConnectionStatus::NOT_CONNECTED);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithoutTlsTest, CarDisconnectBeforeCableCheck) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
connector()->on_car_connected();
|
||||
sleep_for_ms(10);
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
sleep_for_ms(10);
|
||||
|
||||
auto power_stack_counter = get_stop_request_counter();
|
||||
connector()->on_car_disconnected();
|
||||
sleep_for_ms(10);
|
||||
EXPECT_GT(get_stop_request_counter(), power_stack_counter);
|
||||
EXPECT_EQ(get_connection_status(), ConnectionStatus::NOT_CONNECTED);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithoutTlsTest, CarDisconnectDuringCharging) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
connector()->on_car_connected();
|
||||
sleep_for_ms(10);
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCableCheck);
|
||||
sleep_for_ms(10);
|
||||
|
||||
auto power_stack_counter = get_stop_request_counter();
|
||||
connector()->on_car_disconnected();
|
||||
sleep_for_ms(10);
|
||||
EXPECT_GT(get_stop_request_counter(), power_stack_counter);
|
||||
EXPECT_EQ(get_connection_status(), ConnectionStatus::NOT_CONNECTED);
|
||||
EXPECT_EQ(connector()->get_working_status(), WorkingStatus::STANDBY);
|
||||
}
|
||||
|
||||
TEST_F(DispenserWithoutTlsTest, FaultsGetPropagatedCorrectly) {
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::RUNNING);
|
||||
power_stack_mock->send_mac_address();
|
||||
connector()->on_car_connected();
|
||||
sleep_for_ms(10);
|
||||
power_stack_mock->send_hmac_key(1);
|
||||
sleep_for_ms(10);
|
||||
connector()->new_export_voltage_current(200, 5);
|
||||
connector()->on_mode_phase_change(ModePhase::ExportCableCheck);
|
||||
|
||||
power_stack_mock->set_psu_running_mode(PSURunningMode::FAULTY);
|
||||
|
||||
EXPECT_EQ(dispenser->get_psu_running_mode(), PSURunningMode::FAULTY);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
openssl genrsa -out psu_ca.key.pem 2048
|
||||
openssl genrsa -out dispenser_ca.key.pem 2048
|
||||
|
||||
openssl genrsa -out psu.key.pem 2048
|
||||
openssl genrsa -out dispenser.key.pem 2048
|
||||
|
||||
openssl req -new -x509 -days 1000 -key psu_ca.key.pem -out psu_ca.crt.pem -subj "/C=DE/O=Frickly Systems GmbH/CN=The one and only Root CA"
|
||||
openssl req -new -x509 -days 1000 -key dispenser_ca.key.pem -out dispenser_ca.crt.pem -subj "/C=DE/O=Frickly Systems GmbH/CN=The one and only Root CA"
|
||||
|
||||
openssl req -new -key psu.key.pem -out psu.csr.pem -subj "/C=DE/O=Frickly Systems GmbH/CN=localhost"
|
||||
openssl req -new -key dispenser.key.pem -out dispenser.csr.pem -subj "/C=DE/O=Frickly Systems GmbH/CN=client"
|
||||
|
||||
openssl x509 -req -in psu.csr.pem -out psu.crt.pem -CA psu_ca.crt.pem -CAkey psu_ca.key.pem -CAcreateserial -days 1000
|
||||
openssl x509 -req -in dispenser.csr.pem -out dispenser.crt.pem -CA dispenser_ca.crt.pem -CAkey dispenser_ca.key.pem -CAcreateserial -days 1000
|
||||
@@ -0,0 +1,5 @@
|
||||
.vscode/settings.json
|
||||
|
||||
build/
|
||||
.venv/
|
||||
.cache/
|
||||
@@ -0,0 +1,6 @@
|
||||
if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
|
||||
set(MACOSX TRUE)
|
||||
endif()
|
||||
|
||||
add_subdirectory(libs)
|
||||
add_subdirectory(examples)
|
||||
@@ -0,0 +1,12 @@
|
||||
# Goose library
|
||||
|
||||
Provides:
|
||||
- Ethernet interface for macOS and Linux
|
||||
- note that macOS implementation is not really tested; Linux's implementation is mostly based on Huawei's FusionCharger documentation
|
||||
- Ethernet frame abstraction
|
||||
- Goose Frame abstraction
|
||||
- contains Goose PDU abstraction which in turn contains APDU BER encoded data, which is partly abstracted
|
||||
|
||||
## Build and test
|
||||
|
||||
This library is built and tested as part of the build process of EVerest
|
||||
@@ -0,0 +1,2 @@
|
||||
add_executable(ethernet_frame ethernet_frame.cpp)
|
||||
target_link_libraries(ethernet_frame goose-ethernet)
|
||||
@@ -0,0 +1,59 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <stdio.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <goose-ethernet/driver.hpp>
|
||||
#include <goose-ethernet/frame.hpp>
|
||||
#include <iostream>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "Usage: %s <interface>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
goose_ethernet::EthernetInterface interface(argv[1]);
|
||||
const std::uint8_t* mac = interface.get_mac_address();
|
||||
|
||||
goose_ethernet::EthernetFrame frame;
|
||||
frame.destination[0] = 0x00;
|
||||
frame.destination[1] = 0x00;
|
||||
frame.destination[2] = 0x00;
|
||||
frame.destination[3] = 0x00;
|
||||
frame.destination[4] = 0x00;
|
||||
frame.destination[5] = 0x00;
|
||||
memcpy(frame.source, mac, 6);
|
||||
frame.ethertype = 0x88B8;
|
||||
frame.payload.resize(46);
|
||||
// appid
|
||||
frame.payload[0] = 0xff;
|
||||
frame.payload[1] = 0x00;
|
||||
|
||||
interface.send_packet(frame);
|
||||
|
||||
while (1) {
|
||||
auto recveive = interface.receive_packet();
|
||||
if (!recveive.has_value()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto recv = recveive.value();
|
||||
|
||||
printf("Received packet from: ");
|
||||
for (size_t i = 0; i < 6; i++) {
|
||||
printf("%02x ", recv.source[i]);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
printf("EtherType: %04x\n", recv.ethertype);
|
||||
|
||||
printf("Received packet payload: ");
|
||||
for (size_t i = 0; i < recv.payload.size(); i++) {
|
||||
printf("%02x ", recv.payload[i]);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
add_subdirectory(goose-ethernet)
|
||||
add_subdirectory(goose)
|
||||
@@ -0,0 +1,24 @@
|
||||
file(GLOB_RECURSE GOOSE_ETHERNET_SOURCES "src/*.cpp")
|
||||
|
||||
if (MACOSX)
|
||||
list(FILTER GOOSE_ETHERNET_SOURCES EXCLUDE REGEX ".+linux\.cpp")
|
||||
else()
|
||||
list(FILTER GOOSE_ETHERNET_SOURCES EXCLUDE REGEX ".+mac\.cpp")
|
||||
endif()
|
||||
|
||||
add_library(goose-ethernet STATIC ${GOOSE_ETHERNET_SOURCES})
|
||||
target_include_directories(goose-ethernet PUBLIC include)
|
||||
|
||||
if (MACOSX)
|
||||
target_link_libraries(goose-ethernet PRIVATE pcap)
|
||||
endif()
|
||||
|
||||
include(CheckIncludeFile)
|
||||
check_include_file(net/ethernet.h HAVE_NET_ETHERNET_H)
|
||||
if(HAVE_NET_ETHERNET_H AND USING_MUSL)
|
||||
target_compile_definitions(goose-ethernet PRIVATE _NETINET_IF_ETHER_H)
|
||||
endif()
|
||||
|
||||
if(FUSION_CHARGER_LIB_BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <pcap/pcap.h>
|
||||
#endif
|
||||
|
||||
#include "frame.hpp"
|
||||
|
||||
namespace goose_ethernet {
|
||||
|
||||
class EthernetInterfaceIntf {
|
||||
protected:
|
||||
virtual void send_packet_raw(const std::uint8_t* packet, size_t size) = 0;
|
||||
virtual std::optional<std::vector<std::uint8_t>> receive_packet_raw() = 0;
|
||||
|
||||
public:
|
||||
virtual ~EthernetInterfaceIntf() = default;
|
||||
|
||||
// send frame, throws runtime_error on failure or SerializeError if the
|
||||
// frame could not be serialized
|
||||
void send_packet(const EthernetFrame& frame);
|
||||
// receive frame blocking, throws runtime_error on failure or DeserializeError
|
||||
// if the frame could not be deserialized
|
||||
std::optional<EthernetFrame> receive_packet();
|
||||
|
||||
virtual const std::uint8_t* get_mac_address() const = 0;
|
||||
};
|
||||
|
||||
class EthernetInterface : public EthernetInterfaceIntf {
|
||||
protected:
|
||||
std::uint8_t mac_address[6];
|
||||
#ifdef __APPLE__
|
||||
pcap_t* pcap;
|
||||
#else
|
||||
int fd;
|
||||
#endif
|
||||
|
||||
void send_packet_raw(const std::uint8_t* packet, size_t size) override;
|
||||
std::optional<std::vector<std::uint8_t>> receive_packet_raw() override;
|
||||
|
||||
public:
|
||||
EthernetInterface(const char* interface_name);
|
||||
~EthernetInterface();
|
||||
|
||||
const std::uint8_t* get_mac_address() const override;
|
||||
};
|
||||
|
||||
}; // namespace goose_ethernet
|
||||
@@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace goose_ethernet {
|
||||
|
||||
class DeserializeError : public std::runtime_error {
|
||||
public:
|
||||
DeserializeError(const std::string& what) : std::runtime_error(what) {
|
||||
}
|
||||
};
|
||||
|
||||
class SerializeError : public std::runtime_error {
|
||||
public:
|
||||
SerializeError(const std::string& what) : std::runtime_error(what) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Ethernet frame without crc, thus minimum size is 60 bytes, payload
|
||||
* size on non-802.1Q frames is 46 bytes, on 802.1Q frames it is 42 bytes
|
||||
*/
|
||||
struct EthernetFrame {
|
||||
std::uint8_t destination[6];
|
||||
std::uint8_t source[6];
|
||||
std::optional<std::uint16_t> eth_802_1q_tag; // already in system byte order
|
||||
std::uint16_t ethertype;
|
||||
std::vector<std::uint8_t> payload;
|
||||
|
||||
EthernetFrame() = default;
|
||||
|
||||
/**
|
||||
* @brief Deserializing Constructor
|
||||
*
|
||||
* @param data Ethernet frame data
|
||||
* @param size Size of the data
|
||||
* @throws DeserializeError if the data is too short/long
|
||||
*/
|
||||
EthernetFrame(const std::uint8_t* data, size_t size);
|
||||
|
||||
/**
|
||||
* @brief Deserializing Constructor
|
||||
*
|
||||
* @param data Ethernet frame data
|
||||
* @throws DeserializeError if the data is too short/long
|
||||
*/
|
||||
EthernetFrame(const std::vector<std::uint8_t>& data);
|
||||
|
||||
/**
|
||||
* @brief Serialize the Ethernet frame with header and payload, without crc
|
||||
*
|
||||
* @return std::vector<std::uint8_t> Serialized Ethernet frame
|
||||
* @throws SerializeError if the frame is invalid; e.g. payload is too
|
||||
* long/short
|
||||
*/
|
||||
std::vector<std::uint8_t> serialize() const;
|
||||
|
||||
/**
|
||||
* @brief Check whether the ethertype filed is a length field, this is the
|
||||
* case for 802.3 frames. (Ethertype is present in Ethernet II frames)
|
||||
*
|
||||
* @return true ethertype is a length field
|
||||
* @return false ethertype is a type field
|
||||
*/
|
||||
bool ethertype_is_length();
|
||||
};
|
||||
|
||||
} // namespace goose_ethernet
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <goose-ethernet/driver.hpp>
|
||||
|
||||
using namespace goose_ethernet;
|
||||
|
||||
void EthernetInterfaceIntf::send_packet(const EthernetFrame& frame) {
|
||||
auto serialized = frame.serialize();
|
||||
this->send_packet_raw(serialized.data(), serialized.size());
|
||||
}
|
||||
|
||||
std::optional<EthernetFrame> EthernetInterfaceIntf::receive_packet() {
|
||||
auto received = this->receive_packet_raw();
|
||||
if (!received.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (received.value().size() < 60) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return EthernetFrame(received.value());
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <arpa/inet.h>
|
||||
#include <linux/filter.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/if_packet.h>
|
||||
#include <net/ethernet.h>
|
||||
#include <net/if.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <csignal>
|
||||
#include <cstring>
|
||||
#include <goose-ethernet/driver.hpp>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "poll.h"
|
||||
|
||||
using namespace goose_ethernet;
|
||||
|
||||
EthernetInterface::EthernetInterface(const char* interface_name) {
|
||||
this->fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
|
||||
if (this->fd == -1) {
|
||||
throw std::runtime_error("Failed to open socket for ethernet interface: " + std::string(interface_name) +
|
||||
", maybe add CAP_NET_RAW capability to executble");
|
||||
}
|
||||
|
||||
int ifindex;
|
||||
|
||||
// retrieve interface index and mac address
|
||||
{
|
||||
struct ifreq ifr;
|
||||
strncpy(ifr.ifr_name, interface_name, IFNAMSIZ);
|
||||
|
||||
int ret = ioctl(this->fd, SIOCGIFINDEX, &ifr);
|
||||
if (ret == -1) {
|
||||
throw std::runtime_error("Failed to get interface index");
|
||||
}
|
||||
|
||||
ifindex = ifr.ifr_ifindex;
|
||||
|
||||
ret = ioctl(fd, SIOCGIFHWADDR, &ifr);
|
||||
if (ret == -1) {
|
||||
throw std::runtime_error("Failed to get interface MAC address");
|
||||
}
|
||||
|
||||
memcpy(this->mac_address, ifr.ifr_hwaddr.sa_data, 6);
|
||||
}
|
||||
|
||||
// setup filter for GOOSE packets
|
||||
{
|
||||
// generated using `tcpdump -y EN10MB "ether proto 0x88B8" -dd`
|
||||
// where EN10MB is the ethernet datalink type and 0x88B8 is the GOOSE
|
||||
// ethernet protocol
|
||||
static struct sock_filter code[] = {
|
||||
{0x28, 0, 0, 0x0000000c},
|
||||
{0x15, 0, 1, 0x000088b8},
|
||||
{0x6, 0, 0, 0x00040000},
|
||||
{0x6, 0, 0, 0x00000000},
|
||||
};
|
||||
struct sock_fprog socket_filter = {
|
||||
.len = sizeof(code) / sizeof(*code),
|
||||
.filter = code,
|
||||
};
|
||||
|
||||
int ret = setsockopt(this->fd, SOL_SOCKET, SO_ATTACH_FILTER, &socket_filter, sizeof(socket_filter));
|
||||
if (ret < 0) {
|
||||
throw std::runtime_error("Failed to set socket filter, errno: " + std::to_string(errno));
|
||||
}
|
||||
}
|
||||
|
||||
struct sockaddr_ll sll;
|
||||
sll.sll_family = AF_PACKET;
|
||||
sll.sll_protocol = htons(ETH_P_ALL);
|
||||
sll.sll_ifindex = ifindex;
|
||||
|
||||
int ret = bind(this->fd, (struct sockaddr*)&sll, sizeof(sll));
|
||||
if (ret == -1) {
|
||||
throw std::runtime_error("Failed to bind socket");
|
||||
}
|
||||
}
|
||||
|
||||
EthernetInterface::~EthernetInterface() {
|
||||
close(this->fd);
|
||||
}
|
||||
|
||||
void EthernetInterface::send_packet_raw(const std::uint8_t* packet, size_t size) {
|
||||
ssize_t ret = write(this->fd, packet, size);
|
||||
if (ret == -1) {
|
||||
throw std::runtime_error("Failed to send packet, errno: " + std::to_string(errno));
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::vector<std::uint8_t>> EthernetInterface::receive_packet_raw() {
|
||||
std::vector<std::uint8_t> buffer(2000); // more than enough for an ethernet frame
|
||||
//
|
||||
//
|
||||
struct pollfd pfd[1];
|
||||
pfd[0].fd = this->fd;
|
||||
pfd[0].events = POLLIN;
|
||||
|
||||
auto result_code = poll(pfd, 1, 50);
|
||||
auto error = errno;
|
||||
if (result_code < 0) {
|
||||
throw std::runtime_error("Failed to poll ethernet frame, errno: " + std::to_string(error));
|
||||
}
|
||||
|
||||
if (result_code == 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ssize_t ret = read(this->fd, buffer.data(), buffer.size());
|
||||
if (ret == -1) {
|
||||
throw std::runtime_error("Failed to receive packet, errno: " + std::to_string(errno));
|
||||
}
|
||||
|
||||
buffer.resize(ret);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const std::uint8_t* EthernetInterface::get_mac_address() const {
|
||||
return this->mac_address;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
#include <net/if_dl.h>
|
||||
|
||||
#include <goose-ethernet/driver.hpp>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
using namespace goose_ethernet;
|
||||
|
||||
// This implementation uses libpcap to send and receive Ethernet frames.
|
||||
// This implementation is not ideal, as we currently have to have a timeout of
|
||||
// 10ms and retry receiving until we get a packet
|
||||
|
||||
// this should work with timeout 0, but it doesn't (in my case)
|
||||
// as the macos implementation is not that important we can leave it like this
|
||||
// and do more important things
|
||||
// todo: improve this
|
||||
|
||||
EthernetInterface::EthernetInterface(const char* interface_name) {
|
||||
char errbuf[PCAP_ERRBUF_SIZE];
|
||||
// timeout of 100ms
|
||||
pcap = pcap_open_live(interface_name, 65535, 1, 250, errbuf);
|
||||
if (pcap == NULL) {
|
||||
throw std::runtime_error("pcap_open_live failed: " + std::string(errbuf));
|
||||
}
|
||||
|
||||
if (pcap_datalink(pcap) != DLT_EN10MB) {
|
||||
pcap_close(pcap);
|
||||
throw std::runtime_error("pcap_datalink failed: not an Ethernet interface");
|
||||
}
|
||||
|
||||
struct ifaddrs *ifaddr, *ifa;
|
||||
if (getifaddrs(&ifaddr) == -1) {
|
||||
pcap_close(pcap);
|
||||
throw std::runtime_error("cannot get interface address; getifaddrs failed");
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
|
||||
if (ifa->ifa_addr == NULL)
|
||||
continue;
|
||||
|
||||
// Check if the interface name matches the one we are looking for
|
||||
if (strcmp(ifa->ifa_name, interface_name) == 0 && ifa->ifa_addr->sa_family == AF_LINK) {
|
||||
struct sockaddr_dl* sdl = (struct sockaddr_dl*)ifa->ifa_addr;
|
||||
std::uint8_t* mac_address = reinterpret_cast<std::uint8_t*>(LLADDR(sdl));
|
||||
memcpy(this->mac_address, mac_address, 6);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
freeifaddrs(ifaddr);
|
||||
|
||||
if (!found) {
|
||||
pcap_close(pcap);
|
||||
throw std::runtime_error("cannot get interface address; interface not found");
|
||||
}
|
||||
|
||||
// add filter to ignore outgoing packets (sent by us)
|
||||
char filter_exp[sizeof("not ether src XX:XX:XX:XX:XX:XX")];
|
||||
snprintf(filter_exp, sizeof(filter_exp), "not ether src %02X:%02X:%02X:%02X:%02X:%02X", this->mac_address[0],
|
||||
this->mac_address[1], this->mac_address[2], this->mac_address[3], this->mac_address[4],
|
||||
this->mac_address[5]);
|
||||
|
||||
struct bpf_program fp;
|
||||
if (pcap_compile(pcap, &fp, filter_exp, 0, PCAP_NETMASK_UNKNOWN) == -1) {
|
||||
pcap_close(pcap);
|
||||
throw std::runtime_error("pcap_compile failed: " + std::string(pcap_geterr(pcap)));
|
||||
}
|
||||
|
||||
if (pcap_setfilter(pcap, &fp) == -1) {
|
||||
pcap_close(pcap);
|
||||
throw std::runtime_error("pcap_setfilter failed: " + std::string(pcap_geterr(pcap)));
|
||||
}
|
||||
}
|
||||
|
||||
void EthernetInterface::send_packet_raw(const std::uint8_t* packet, size_t size) {
|
||||
int ret = pcap_sendpacket(pcap, (std::uint8_t*)packet, size);
|
||||
if (ret != 0) {
|
||||
throw std::runtime_error("pcap_sendpacket failed: " + std::string(pcap_geterr(pcap)));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> EthernetInterface::receive_packet_raw() {
|
||||
pcap_pkthdr* header;
|
||||
const std::uint8_t* data;
|
||||
while (1) {
|
||||
int ret = pcap_next_ex(pcap, &header, &data);
|
||||
|
||||
switch (ret) {
|
||||
case 1:
|
||||
return std::vector<std::uint8_t>(data, data + header->caplen);
|
||||
case 0:
|
||||
// timeout, try again
|
||||
continue;
|
||||
case -1:
|
||||
throw std::runtime_error("pcap_next_ex failed: " + std::string(pcap_geterr(pcap)));
|
||||
case -2:
|
||||
throw std::runtime_error("pcap_next_ex failed: no more packets");
|
||||
default:
|
||||
throw std::runtime_error("pcap_next_ex failed: unknown error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EthernetInterface::~EthernetInterface() {
|
||||
pcap_close(pcap);
|
||||
}
|
||||
|
||||
const std::uint8_t* EthernetInterface::get_mac_address() const {
|
||||
return this->mac_address;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <cstring>
|
||||
#include <goose-ethernet/frame.hpp>
|
||||
|
||||
using namespace goose_ethernet;
|
||||
|
||||
EthernetFrame::EthernetFrame(const std::uint8_t* data, size_t size) {
|
||||
// minimum size of a normal ethernet frame is 64 bytes, without crc it is 60
|
||||
if (size < 60) {
|
||||
throw DeserializeError("Ethernet frame too short (size < 60)");
|
||||
}
|
||||
|
||||
memcpy(destination, data, 6);
|
||||
memcpy(source, data + 6, 6);
|
||||
ethertype = (data[12] << 8) | data[13];
|
||||
if (ethertype == 0x8100) {
|
||||
eth_802_1q_tag = (data[14] << 8) | data[15];
|
||||
ethertype = (data[16] << 8) | data[17];
|
||||
payload = std::vector<std::uint8_t>(data + 18, data + size);
|
||||
} else {
|
||||
eth_802_1q_tag = std::nullopt;
|
||||
payload = std::vector<std::uint8_t>(data + 14, data + size);
|
||||
}
|
||||
|
||||
// todo: check if redundant
|
||||
if (payload.size() < 42) {
|
||||
throw DeserializeError("Ethernet frame payload too short (payload size < 42)");
|
||||
}
|
||||
}
|
||||
|
||||
EthernetFrame::EthernetFrame(const std::vector<std::uint8_t>& data) : EthernetFrame(data.data(), data.size()) {
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> EthernetFrame::serialize() const {
|
||||
if (payload.size() > 1500) {
|
||||
throw SerializeError("Ethernet frame too long (payload size > 1500)");
|
||||
}
|
||||
|
||||
size_t package_size = 14 + payload.size() + (eth_802_1q_tag.has_value() ? 4 : 0);
|
||||
|
||||
// todo: maybe not throw but append zeros
|
||||
if (package_size < 60) {
|
||||
throw SerializeError("Ethernet frame too short (size < 60)");
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> data;
|
||||
data.reserve(package_size);
|
||||
|
||||
data.insert(data.end(), destination, destination + 6);
|
||||
data.insert(data.end(), source, source + 6);
|
||||
|
||||
if (eth_802_1q_tag.has_value()) {
|
||||
data.push_back(0x81);
|
||||
data.push_back(0x00);
|
||||
data.push_back(eth_802_1q_tag.value() >> 8);
|
||||
data.push_back(eth_802_1q_tag.value() & 0xff);
|
||||
}
|
||||
|
||||
data.push_back(ethertype >> 8);
|
||||
data.push_back(ethertype & 0xff);
|
||||
|
||||
data.insert(data.end(), payload.begin(), payload.end());
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
bool EthernetFrame::ethertype_is_length() {
|
||||
return ethertype <= 1500;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
include(GoogleTest)
|
||||
|
||||
file(GLOB_RECURSE GOOSE_ETHERNET_TEST_SOURCES "*.cpp")
|
||||
add_executable(goose-ethernet-tests ${GOOSE_ETHERNET_TEST_SOURCES})
|
||||
target_link_libraries(goose-ethernet-tests PRIVATE goose-ethernet gtest_main)
|
||||
gtest_discover_tests(goose-ethernet-tests)
|
||||
@@ -0,0 +1,295 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <goose-ethernet/frame.hpp>
|
||||
|
||||
using namespace goose_ethernet;
|
||||
|
||||
TEST(EthernetFrame, deserialization_positive_test) {
|
||||
std::uint8_t raw_data[60] = {
|
||||
0xde, 0xad, 0xbe, 0xef, 0xfe, 0xed, // destination
|
||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, // source
|
||||
0x08, 0x00, // ethertype
|
||||
0xca, 0xfe, 0xba, 0xbe, 0x00, 0x01, // payload (rest is padding)
|
||||
};
|
||||
|
||||
EthernetFrame frame = EthernetFrame(raw_data, sizeof(raw_data));
|
||||
|
||||
EXPECT_EQ(frame.destination[0], 0xde);
|
||||
EXPECT_EQ(frame.destination[1], 0xad);
|
||||
EXPECT_EQ(frame.destination[2], 0xbe);
|
||||
EXPECT_EQ(frame.destination[3], 0xef);
|
||||
EXPECT_EQ(frame.destination[4], 0xfe);
|
||||
EXPECT_EQ(frame.destination[5], 0xed);
|
||||
|
||||
EXPECT_EQ(frame.source[0], 0x01);
|
||||
EXPECT_EQ(frame.source[1], 0x23);
|
||||
EXPECT_EQ(frame.source[2], 0x45);
|
||||
EXPECT_EQ(frame.source[3], 0x67);
|
||||
EXPECT_EQ(frame.source[4], 0x89);
|
||||
EXPECT_EQ(frame.source[5], 0xab);
|
||||
|
||||
EXPECT_EQ(frame.ethertype, 0x0800);
|
||||
EXPECT_FALSE(frame.eth_802_1q_tag.has_value());
|
||||
EXPECT_EQ(frame.payload.size(), 46);
|
||||
EXPECT_EQ(frame.payload[0], 0xca);
|
||||
EXPECT_EQ(frame.payload[1], 0xfe);
|
||||
EXPECT_EQ(frame.payload[2], 0xba);
|
||||
EXPECT_EQ(frame.payload[3], 0xbe);
|
||||
EXPECT_EQ(frame.payload[4], 0x00);
|
||||
EXPECT_EQ(frame.payload[5], 0x01);
|
||||
|
||||
EXPECT_FALSE(frame.ethertype_is_length());
|
||||
}
|
||||
|
||||
TEST(EthernetFrame, deserialization_802_1Q_positive_test) {
|
||||
std::uint8_t raw_data[60] = {
|
||||
0xde, 0xad, 0xbe, 0xef, 0xfe, 0xed, // destination
|
||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, // source
|
||||
0x81, 0x00, // 802.1Q ID
|
||||
0xfe, 0xdc, // 802.1Q tag
|
||||
0x08, 0x00, // ethertype
|
||||
0xca, 0xfe, 0xba, 0xbe, 0x00, 0x01, // payload (rest is padding)
|
||||
};
|
||||
|
||||
EthernetFrame frame = EthernetFrame(raw_data, sizeof(raw_data));
|
||||
|
||||
EXPECT_EQ(frame.destination[0], 0xde);
|
||||
EXPECT_EQ(frame.destination[1], 0xad);
|
||||
EXPECT_EQ(frame.destination[2], 0xbe);
|
||||
EXPECT_EQ(frame.destination[3], 0xef);
|
||||
EXPECT_EQ(frame.destination[4], 0xfe);
|
||||
EXPECT_EQ(frame.destination[5], 0xed);
|
||||
|
||||
EXPECT_EQ(frame.source[0], 0x01);
|
||||
EXPECT_EQ(frame.source[1], 0x23);
|
||||
EXPECT_EQ(frame.source[2], 0x45);
|
||||
EXPECT_EQ(frame.source[3], 0x67);
|
||||
EXPECT_EQ(frame.source[4], 0x89);
|
||||
EXPECT_EQ(frame.source[5], 0xab);
|
||||
|
||||
EXPECT_TRUE(frame.eth_802_1q_tag.has_value());
|
||||
EXPECT_EQ(frame.eth_802_1q_tag.value(), 0xfedc);
|
||||
EXPECT_EQ(frame.ethertype, 0x0800);
|
||||
|
||||
EXPECT_EQ(frame.payload.size(), 42);
|
||||
EXPECT_EQ(frame.payload[0], 0xca);
|
||||
EXPECT_EQ(frame.payload[1], 0xfe);
|
||||
EXPECT_EQ(frame.payload[2], 0xba);
|
||||
EXPECT_EQ(frame.payload[3], 0xbe);
|
||||
EXPECT_EQ(frame.payload[4], 0x00);
|
||||
EXPECT_EQ(frame.payload[5], 0x01);
|
||||
|
||||
EXPECT_FALSE(frame.ethertype_is_length());
|
||||
}
|
||||
|
||||
TEST(EthernetFrame, deserialization_frame_too_short) {
|
||||
std::uint8_t raw_data[59] = {
|
||||
0xde, 0xad, 0xbe, 0xef, 0xfe, 0xed, // destination
|
||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, // source
|
||||
0x08, 0x00, // ethertype
|
||||
0xca, 0xfe, 0xba, 0xbe, 0x00, 0x01, // payload (rest is padding)
|
||||
};
|
||||
|
||||
EXPECT_THROW(EthernetFrame(raw_data, 59), DeserializeError);
|
||||
}
|
||||
|
||||
TEST(EthernetFrame, deserialization_frame_802_1q_too_short) {
|
||||
std::uint8_t raw_data[59] = {
|
||||
0xde, 0xad, 0xbe, 0xef, 0xfe, 0xed, // destination
|
||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, // source
|
||||
0x81, 0x00, // 802.1Q ID
|
||||
0xfe, 0xdc, // 802.1Q tag
|
||||
0x08, 0x00, // ethertype
|
||||
0xca, 0xfe, 0xba, 0xbe, 0x00, 0x01, // payload (rest is padding)
|
||||
};
|
||||
|
||||
EXPECT_THROW(EthernetFrame(raw_data, sizeof(raw_data)), DeserializeError);
|
||||
}
|
||||
|
||||
TEST(EthernetFrame, serialize_positive_test) {
|
||||
EthernetFrame frame;
|
||||
frame.destination[0] = 0xde;
|
||||
frame.destination[1] = 0xad;
|
||||
frame.destination[2] = 0xbe;
|
||||
frame.destination[3] = 0xef;
|
||||
frame.destination[4] = 0xfe;
|
||||
frame.destination[5] = 0xed;
|
||||
|
||||
frame.source[0] = 0x01;
|
||||
frame.source[1] = 0x23;
|
||||
frame.source[2] = 0x45;
|
||||
frame.source[3] = 0x67;
|
||||
frame.source[4] = 0x89;
|
||||
frame.source[5] = 0xab;
|
||||
|
||||
frame.eth_802_1q_tag = std::nullopt;
|
||||
frame.ethertype = 0x0800;
|
||||
|
||||
frame.payload.resize(46);
|
||||
frame.payload[0] = 0xca;
|
||||
frame.payload[1] = 0xfe;
|
||||
|
||||
auto serialized = frame.serialize();
|
||||
|
||||
EXPECT_EQ(serialized.size(), 60);
|
||||
EXPECT_EQ(serialized[0], 0xde);
|
||||
EXPECT_EQ(serialized[1], 0xad);
|
||||
EXPECT_EQ(serialized[2], 0xbe);
|
||||
EXPECT_EQ(serialized[3], 0xef);
|
||||
EXPECT_EQ(serialized[4], 0xfe);
|
||||
EXPECT_EQ(serialized[5], 0xed);
|
||||
|
||||
EXPECT_EQ(serialized[6], 0x01);
|
||||
EXPECT_EQ(serialized[7], 0x23);
|
||||
EXPECT_EQ(serialized[8], 0x45);
|
||||
EXPECT_EQ(serialized[9], 0x67);
|
||||
EXPECT_EQ(serialized[10], 0x89);
|
||||
EXPECT_EQ(serialized[11], 0xab);
|
||||
|
||||
EXPECT_EQ(serialized[12], 0x08);
|
||||
EXPECT_EQ(serialized[13], 0x00);
|
||||
|
||||
EXPECT_EQ(serialized[14], 0xca);
|
||||
EXPECT_EQ(serialized[15], 0xfe);
|
||||
}
|
||||
|
||||
TEST(EthernetFrame, serialize_positive_test_802_1q) {
|
||||
EthernetFrame frame;
|
||||
frame.destination[0] = 0xde;
|
||||
frame.destination[1] = 0xad;
|
||||
frame.destination[2] = 0xbe;
|
||||
frame.destination[3] = 0xef;
|
||||
frame.destination[4] = 0xfe;
|
||||
frame.destination[5] = 0xed;
|
||||
|
||||
frame.source[0] = 0x01;
|
||||
frame.source[1] = 0x23;
|
||||
frame.source[2] = 0x45;
|
||||
frame.source[3] = 0x67;
|
||||
frame.source[4] = 0x89;
|
||||
frame.source[5] = 0xab;
|
||||
|
||||
frame.eth_802_1q_tag = 0xfedc;
|
||||
frame.ethertype = 0x0800;
|
||||
|
||||
frame.payload.resize(42);
|
||||
frame.payload[0] = 0xca;
|
||||
frame.payload[1] = 0xfe;
|
||||
|
||||
auto serialized = frame.serialize();
|
||||
|
||||
EXPECT_EQ(serialized.size(), 60);
|
||||
EXPECT_EQ(serialized[0], 0xde);
|
||||
EXPECT_EQ(serialized[1], 0xad);
|
||||
EXPECT_EQ(serialized[2], 0xbe);
|
||||
EXPECT_EQ(serialized[3], 0xef);
|
||||
EXPECT_EQ(serialized[4], 0xfe);
|
||||
EXPECT_EQ(serialized[5], 0xed);
|
||||
|
||||
EXPECT_EQ(serialized[6], 0x01);
|
||||
EXPECT_EQ(serialized[7], 0x23);
|
||||
EXPECT_EQ(serialized[8], 0x45);
|
||||
EXPECT_EQ(serialized[9], 0x67);
|
||||
EXPECT_EQ(serialized[10], 0x89);
|
||||
EXPECT_EQ(serialized[11], 0xab);
|
||||
|
||||
EXPECT_EQ(serialized[12], 0x81);
|
||||
EXPECT_EQ(serialized[13], 0x00);
|
||||
EXPECT_EQ(serialized[14], 0xfe);
|
||||
EXPECT_EQ(serialized[15], 0xdc);
|
||||
|
||||
EXPECT_EQ(serialized[16], 0x08);
|
||||
EXPECT_EQ(serialized[17], 0x00);
|
||||
|
||||
EXPECT_EQ(serialized[18], 0xca);
|
||||
EXPECT_EQ(serialized[19], 0xfe);
|
||||
}
|
||||
|
||||
TEST(EthernetFrame, serialize_too_short) {
|
||||
EthernetFrame frame;
|
||||
frame.eth_802_1q_tag = std::nullopt;
|
||||
frame.payload.resize(45);
|
||||
ASSERT_THROW(frame.serialize(), SerializeError);
|
||||
frame.payload.resize(46);
|
||||
ASSERT_NO_THROW(frame.serialize());
|
||||
|
||||
frame.eth_802_1q_tag = 0xfedc;
|
||||
frame.payload.resize(41);
|
||||
ASSERT_THROW(frame.serialize(), SerializeError);
|
||||
frame.payload.resize(42);
|
||||
ASSERT_NO_THROW(frame.serialize());
|
||||
}
|
||||
|
||||
TEST(EthernetFrame, serialize_too_long) {
|
||||
EthernetFrame frame;
|
||||
frame.eth_802_1q_tag = std::nullopt;
|
||||
frame.payload.resize(1500);
|
||||
ASSERT_NO_THROW(frame.serialize());
|
||||
frame.payload.resize(1501);
|
||||
ASSERT_THROW(frame.serialize(), SerializeError);
|
||||
|
||||
frame.eth_802_1q_tag = 0xfedc;
|
||||
frame.payload.resize(1500);
|
||||
ASSERT_NO_THROW(frame.serialize());
|
||||
frame.payload.resize(1501);
|
||||
ASSERT_THROW(frame.serialize(), SerializeError);
|
||||
}
|
||||
|
||||
TEST(EthernetFrame, ethertype_is_length) {
|
||||
EthernetFrame frame;
|
||||
frame.ethertype = 1500;
|
||||
EXPECT_TRUE(frame.ethertype_is_length());
|
||||
frame.ethertype = 0x0800;
|
||||
EXPECT_FALSE(frame.ethertype_is_length());
|
||||
}
|
||||
|
||||
TEST(EthernetFrame, serialize_deserialize) {
|
||||
EthernetFrame frame;
|
||||
frame.destination[0] = 0xde;
|
||||
frame.destination[1] = 0xad;
|
||||
frame.destination[2] = 0xbe;
|
||||
frame.destination[3] = 0xef;
|
||||
frame.destination[4] = 0xfe;
|
||||
frame.destination[5] = 0xed;
|
||||
|
||||
frame.source[0] = 0x01;
|
||||
frame.source[1] = 0x23;
|
||||
frame.source[2] = 0x45;
|
||||
frame.source[3] = 0x67;
|
||||
frame.source[4] = 0x89;
|
||||
frame.source[5] = 0xab;
|
||||
|
||||
frame.eth_802_1q_tag = 0xfedc;
|
||||
frame.ethertype = 0x0800;
|
||||
|
||||
frame.payload.resize(42);
|
||||
frame.payload[0] = 0xca;
|
||||
frame.payload[1] = 0xfe;
|
||||
|
||||
auto serialized = frame.serialize();
|
||||
|
||||
EthernetFrame deserialized = EthernetFrame(serialized);
|
||||
ASSERT_EQ(deserialized.destination[0], 0xde);
|
||||
ASSERT_EQ(deserialized.destination[1], 0xad);
|
||||
ASSERT_EQ(deserialized.destination[2], 0xbe);
|
||||
ASSERT_EQ(deserialized.destination[3], 0xef);
|
||||
ASSERT_EQ(deserialized.destination[4], 0xfe);
|
||||
ASSERT_EQ(deserialized.destination[5], 0xed);
|
||||
|
||||
ASSERT_EQ(deserialized.source[0], 0x01);
|
||||
ASSERT_EQ(deserialized.source[1], 0x23);
|
||||
ASSERT_EQ(deserialized.source[2], 0x45);
|
||||
ASSERT_EQ(deserialized.source[3], 0x67);
|
||||
ASSERT_EQ(deserialized.source[4], 0x89);
|
||||
ASSERT_EQ(deserialized.source[5], 0xab);
|
||||
|
||||
ASSERT_TRUE(deserialized.eth_802_1q_tag.has_value());
|
||||
ASSERT_EQ(deserialized.eth_802_1q_tag.value(), 0xfedc);
|
||||
ASSERT_EQ(deserialized.ethertype, 0x0800);
|
||||
|
||||
ASSERT_EQ(deserialized.payload.size(), 42);
|
||||
ASSERT_EQ(deserialized.payload[0], 0xca);
|
||||
ASSERT_EQ(deserialized.payload[1], 0xfe);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
file(GLOB_RECURSE GOOSE_SOURCES "src/*.cpp")
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
add_library(goose STATIC ${GOOSE_SOURCES})
|
||||
target_include_directories(goose PUBLIC include)
|
||||
target_link_libraries(goose PUBLIC goose-ethernet OpenSSL::SSL OpenSSL::Crypto Huawei::FusionCharger::LogInterface)
|
||||
|
||||
if(FUSION_CHARGER_LIB_BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -0,0 +1,131 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace goose {
|
||||
namespace frame {
|
||||
namespace ber {
|
||||
|
||||
template <typename T> std::vector<std::uint8_t> encode_be(T value) {
|
||||
std::vector<std::uint8_t> result;
|
||||
for (size_t i = 0; i < sizeof(T); i++) {
|
||||
result.push_back((value >> (8 * (sizeof(T) - i - 1))) & 0xFF);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename T> T decode_be(const std::vector<std::uint8_t>& input) {
|
||||
T result = 0;
|
||||
for (size_t i = 0; i < sizeof(T) && i < input.size(); i++) {
|
||||
result = (result << 8) | input[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
struct BEREntry {
|
||||
std::uint8_t tag;
|
||||
std::vector<std::uint8_t> value;
|
||||
|
||||
BEREntry() = default;
|
||||
BEREntry(std::uint8_t tag, std::vector<std::uint8_t> value) : tag(tag), value(value) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Input-modifying decoding constructor; removes read bytes from input
|
||||
*
|
||||
* @warning This constructor modifies the input vector by removing the read
|
||||
* bytes
|
||||
*
|
||||
* @param input
|
||||
*/
|
||||
BEREntry(std::vector<std::uint8_t>* input);
|
||||
|
||||
// Encode the entry into a vector of bytes and append it to the payload
|
||||
void add(const BEREntry& entry);
|
||||
|
||||
std::vector<std::uint8_t> encode() const;
|
||||
};
|
||||
|
||||
template <typename T> struct PrimitiveBEREntry {
|
||||
T data;
|
||||
std::uint8_t tag;
|
||||
|
||||
PrimitiveBEREntry() = default;
|
||||
PrimitiveBEREntry(T data, std::uint8_t tag) : data(data), tag(tag) {
|
||||
}
|
||||
/**
|
||||
* @brief Input-modifying decoding constructor; removes read bytes from input
|
||||
*
|
||||
* @warning This constructor modifies the input vector by removing the read
|
||||
* bytes
|
||||
*
|
||||
* @param input BER encoded data
|
||||
*/
|
||||
PrimitiveBEREntry(std::vector<std::uint8_t>& input, std::optional<std::uint8_t> expected_tag = std::nullopt) {
|
||||
BEREntry entry(&input); // Note: this constructor modifies the input vector
|
||||
|
||||
if (expected_tag.has_value()) {
|
||||
if (entry.tag != expected_tag.value()) {
|
||||
throw std::runtime_error("PrimitiveBEREntry: tag mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.value.size() > sizeof(T)) {
|
||||
throw std::runtime_error("PrimitiveBEREntry: value size too big mismatch");
|
||||
}
|
||||
|
||||
data = decode_be<T>(entry.value);
|
||||
tag = entry.tag;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> encode() const {
|
||||
return BEREntry{tag, encode_be(data)}.encode();
|
||||
}
|
||||
};
|
||||
|
||||
struct StringBEREntry {
|
||||
std::string data;
|
||||
std::uint8_t tag;
|
||||
|
||||
StringBEREntry() = default;
|
||||
StringBEREntry(const std::string& data, std::uint8_t tag) : data(data), tag(tag) {
|
||||
}
|
||||
/**
|
||||
* @brief Input-modifying decoding constructor; removes read bytes from input
|
||||
*
|
||||
* @warning This constructor modifies the input vector by removing the read
|
||||
* bytes
|
||||
*
|
||||
* @param input BER encoded data
|
||||
*/
|
||||
StringBEREntry(std::vector<std::uint8_t>& input, std::optional<std::uint8_t> expected_tag,
|
||||
std::optional<size_t> max_length = std::nullopt) {
|
||||
BEREntry entry(&input); // Note: this constructor modifies the input vector
|
||||
|
||||
if (expected_tag.has_value()) {
|
||||
if (entry.tag != expected_tag.value()) {
|
||||
throw std::runtime_error("StringBEREntry: tag mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
if (max_length.has_value() && entry.value.size() > max_length.value()) {
|
||||
throw std::runtime_error("StringBEREntry: value size too big mismatch");
|
||||
}
|
||||
|
||||
data = std::string(entry.value.begin(), entry.value.end());
|
||||
tag = entry.tag;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> encode() const {
|
||||
return BEREntry(tag, std::vector<std::uint8_t>(data.begin(), data.end())).encode();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ber
|
||||
} // namespace frame
|
||||
} // namespace goose
|
||||
@@ -0,0 +1,126 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <goose-ethernet/frame.hpp>
|
||||
#include <goose/ber.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace goose {
|
||||
namespace frame {
|
||||
|
||||
const std::uint16_t GOOSE_ETHERTYPE = 0x88B8;
|
||||
|
||||
struct GooseTimestamp {
|
||||
std::uint32_t seconds;
|
||||
// only 24 lower bits are used
|
||||
std::uint32_t fraction;
|
||||
std::uint8_t quality_of_time;
|
||||
|
||||
GooseTimestamp() = default;
|
||||
|
||||
/**
|
||||
* @param seconds number of seconds since epoch
|
||||
* @param fraction 24-bit fraction of a second (0x1000000 = 1 second). Only
|
||||
* lower 24 bits are used
|
||||
* @param quality_of_time quality of time, see IEC 61850-8-1
|
||||
*/
|
||||
GooseTimestamp(std::uint32_t seconds, std::uint32_t fraction, std::uint8_t quality_of_time) :
|
||||
seconds(seconds), fraction(fraction), quality_of_time(quality_of_time) {
|
||||
}
|
||||
|
||||
GooseTimestamp(const std::vector<std::uint8_t>& raw);
|
||||
|
||||
std::vector<std::uint8_t> encode() const;
|
||||
float to_ms();
|
||||
bool operator==(const GooseTimestamp& other) const;
|
||||
|
||||
static GooseTimestamp from_ms(std::uint64_t ms);
|
||||
static GooseTimestamp now();
|
||||
};
|
||||
|
||||
struct GoosePDU {
|
||||
// todo: check length
|
||||
char go_cb_ref[65]; // 64 bytes + null terminator
|
||||
|
||||
std::uint32_t time_allowed_to_live; // seconds
|
||||
|
||||
// todo: check length
|
||||
char dat_set[65]; // 64 bytes + null terminator
|
||||
|
||||
// todo: check length
|
||||
char go_id[65]; // 64 bytes + null terminator
|
||||
|
||||
// already parsed
|
||||
GooseTimestamp timestamp; // milliseconds
|
||||
|
||||
std::uint32_t st_num;
|
||||
std::uint32_t sq_num;
|
||||
|
||||
bool simulation;
|
||||
std::uint32_t conf_rev; // configuration revision
|
||||
std::uint8_t ndsCom;
|
||||
|
||||
std::vector<ber::BEREntry> apdu_entries;
|
||||
|
||||
GoosePDU() = default;
|
||||
GoosePDU(const std::vector<std::uint8_t>& pdu);
|
||||
|
||||
std::vector<std::uint8_t> serialize() const;
|
||||
};
|
||||
|
||||
struct GooseFrameIntf {
|
||||
std::uint8_t source_mac_address[6];
|
||||
std::uint8_t destination_mac_address[6];
|
||||
|
||||
std::uint8_t appid[2];
|
||||
|
||||
GoosePDU pdu;
|
||||
std::uint8_t priority;
|
||||
std::uint16_t vlan_id;
|
||||
|
||||
public:
|
||||
GooseFrameIntf() = default;
|
||||
GooseFrameIntf(const goose_ethernet::EthernetFrame& ethernet_frame);
|
||||
virtual ~GooseFrameIntf() = default;
|
||||
};
|
||||
|
||||
// Generic Goose Frame; without IEC 62351-6 security
|
||||
struct GooseFrame : public GooseFrameIntf {
|
||||
public:
|
||||
GooseFrame() = default;
|
||||
|
||||
/**
|
||||
* @brief GooseFrame constructor
|
||||
*/
|
||||
GooseFrame(const goose_ethernet::EthernetFrame& ethernet_frame);
|
||||
GooseFrame(const GooseFrame& other) = default;
|
||||
|
||||
goose_ethernet::EthernetFrame serialize() const;
|
||||
};
|
||||
|
||||
struct SecureGooseFrame : public GooseFrameIntf {
|
||||
public:
|
||||
SecureGooseFrame() = default;
|
||||
|
||||
/**
|
||||
* @brief SecureGooseFrame constructor, validating the HMAC if hmac_key is
|
||||
* provided
|
||||
*/
|
||||
SecureGooseFrame(const goose_ethernet::EthernetFrame& ethernet_frame,
|
||||
std::optional<std::vector<std::uint8_t>> hmac_key);
|
||||
|
||||
/**
|
||||
* @brief SecureGooseFrame constructor, not validating the HMAC
|
||||
*/
|
||||
SecureGooseFrame(const goose_ethernet::EthernetFrame& ethernet_frame) :
|
||||
SecureGooseFrame(ethernet_frame, std::nullopt) {
|
||||
}
|
||||
|
||||
goose_ethernet::EthernetFrame serialize(std::vector<std::uint8_t> hmac_key) const;
|
||||
};
|
||||
|
||||
} // namespace frame
|
||||
} // namespace goose
|
||||
@@ -0,0 +1,151 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <goose-ethernet/driver.hpp>
|
||||
#include <logs/logs.hpp>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "frame.hpp"
|
||||
|
||||
namespace goose {
|
||||
namespace sender {
|
||||
|
||||
class SendPacketIntf {
|
||||
public:
|
||||
struct PerPacketInfo {
|
||||
std::uint16_t sq_num;
|
||||
std::uint16_t st_num;
|
||||
};
|
||||
|
||||
virtual ~SendPacketIntf() = default;
|
||||
|
||||
virtual goose_ethernet::EthernetFrame build_packet(const PerPacketInfo& info) = 0;
|
||||
};
|
||||
|
||||
class SendPacketNormal : public SendPacketIntf {
|
||||
protected:
|
||||
goose::frame::GooseFrame frame;
|
||||
|
||||
public:
|
||||
SendPacketNormal(goose::frame::GooseFrame frame) : frame(frame) {
|
||||
}
|
||||
goose_ethernet::EthernetFrame build_packet(const PerPacketInfo& info) override {
|
||||
frame.pdu.st_num = info.st_num;
|
||||
frame.pdu.sq_num = info.sq_num;
|
||||
return frame.serialize();
|
||||
}
|
||||
};
|
||||
|
||||
class SendPacketSecure : public SendPacketIntf {
|
||||
protected:
|
||||
goose::frame::SecureGooseFrame frame;
|
||||
std::vector<std::uint8_t> hmac_key;
|
||||
|
||||
public:
|
||||
SendPacketSecure(goose::frame::SecureGooseFrame frame, std::vector<std::uint8_t> hmac_key) :
|
||||
frame(frame), hmac_key(hmac_key) {
|
||||
}
|
||||
|
||||
goose_ethernet::EthernetFrame build_packet(const PerPacketInfo& info) override {
|
||||
frame.pdu.st_num = info.st_num;
|
||||
frame.pdu.sq_num = info.sq_num;
|
||||
return frame.serialize(hmac_key);
|
||||
}
|
||||
};
|
||||
|
||||
class SenderIntf {
|
||||
public:
|
||||
/**
|
||||
* @brief Update the currently sent packet, the heap-allocated variant.
|
||||
*
|
||||
* @param packet the new packet to send, allocated on the heap via \c new
|
||||
* (converted to \c std::unique_ptr )
|
||||
*/
|
||||
virtual void send(SendPacketIntf* packet) = 0;
|
||||
|
||||
/**
|
||||
* @brief Update the currently sent packet, the \c std::unique_ptr variant.
|
||||
*
|
||||
* @param packet the new packet to send
|
||||
*/
|
||||
virtual void send(std::unique_ptr<SendPacketIntf> packet) = 0;
|
||||
|
||||
/**
|
||||
* @brief The thread's main function, to be run in a loop without delay
|
||||
*
|
||||
*/
|
||||
virtual void run() = 0;
|
||||
|
||||
/**
|
||||
* @brief Start the sender thread; runs \c run() repeatedly
|
||||
*/
|
||||
virtual void start() = 0;
|
||||
|
||||
/**
|
||||
* @brief If using \c start(), this will stop the sender thread with a
|
||||
* maximum delay of \c t0
|
||||
*
|
||||
* @note only works if \c start() was called before
|
||||
*/
|
||||
virtual void stop() = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief An implementation of a GOOSE sender which retransmits GOOSE frames
|
||||
*
|
||||
*/
|
||||
class Sender : public SenderIntf {
|
||||
protected:
|
||||
std::chrono::milliseconds t0;
|
||||
std::vector<std::chrono::milliseconds> ts;
|
||||
|
||||
size_t current_ts_index = 0;
|
||||
|
||||
std::optional<std::thread> thread;
|
||||
bool stop_flag = false;
|
||||
std::atomic<bool> has_new_package = false;
|
||||
std::shared_ptr<goose_ethernet::EthernetInterfaceIntf> intf;
|
||||
|
||||
std::optional<std::unique_ptr<SendPacketIntf>> current_packet;
|
||||
std::mutex current_packet_mutex;
|
||||
std::condition_variable current_packet_cv; // Condition variable to notify the sender thread of a
|
||||
// new packet to send; may also be used to signal a
|
||||
// stop
|
||||
|
||||
std::uint16_t st_num;
|
||||
std::uint16_t sq_num;
|
||||
|
||||
logs::LogIntf log;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new sender with T_0 and multiple \f$T_i\f$ (with
|
||||
* \f$t\in\N_1^+\f$)
|
||||
*
|
||||
* @param t0 the maximum delay between two frames; if no frame is sent within
|
||||
* this time, the last frame is retransmitted
|
||||
* @param ts the delays between the initial retransmits
|
||||
* @param intf the interface to send the frames on
|
||||
*/
|
||||
Sender(std::chrono::milliseconds t0, std::vector<std::chrono::milliseconds> ts,
|
||||
std::shared_ptr<goose_ethernet::EthernetInterfaceIntf> intf, logs::LogIntf log = logs::log_printf);
|
||||
|
||||
Sender(Sender&& other) : t0(other.t0), ts(other.ts), intf(other.intf), log(other.log) {
|
||||
}
|
||||
|
||||
const std::uint8_t* get_mac_address() const;
|
||||
void send(SendPacketIntf* packet) override;
|
||||
void send(std::unique_ptr<SendPacketIntf> packet) override;
|
||||
void run() override;
|
||||
void start() override;
|
||||
void stop() override;
|
||||
};
|
||||
|
||||
}; // namespace sender
|
||||
} // namespace goose
|
||||
@@ -0,0 +1,85 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <goose/ber.hpp>
|
||||
|
||||
namespace goose {
|
||||
namespace frame {
|
||||
namespace ber {
|
||||
|
||||
BEREntry::BEREntry(std::vector<std::uint8_t>* input) {
|
||||
if (input == nullptr) {
|
||||
throw std::runtime_error("BEREntry: input is nullptr");
|
||||
}
|
||||
|
||||
if (input->size() < 2) {
|
||||
throw std::runtime_error("BEREntry: input has no tag or length");
|
||||
}
|
||||
|
||||
tag = (*input)[0];
|
||||
std::uint8_t length_octets;
|
||||
size_t length;
|
||||
|
||||
if ((*input)[1] & 0x80) {
|
||||
length_octets = (*input)[1] & 0x7F;
|
||||
length = 0;
|
||||
if (length_octets > input->size() - 2) {
|
||||
throw std::runtime_error("BEREntry: input too short, length octets missing");
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < length_octets; i++) {
|
||||
length = (length << 8) | (*input)[2 + i];
|
||||
}
|
||||
} else {
|
||||
length_octets = 0;
|
||||
length = (*input)[1];
|
||||
}
|
||||
|
||||
// Remove tag and length bytes
|
||||
input->erase(input->begin(), input->begin() + 2 + length_octets);
|
||||
|
||||
// Copy and remove value bytes
|
||||
if (length > input->size()) {
|
||||
throw std::runtime_error("BEREntry: input too short, payload missing");
|
||||
}
|
||||
value.insert(value.end(), input->begin(), input->begin() + length);
|
||||
input->erase(input->begin(), input->begin() + length);
|
||||
}
|
||||
|
||||
void BEREntry::add(const BEREntry& entry) {
|
||||
auto encoded = entry.encode();
|
||||
value.insert(value.end(), encoded.begin(), encoded.end());
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> BEREntry::encode() const {
|
||||
std::vector<std::uint8_t> result;
|
||||
result.push_back(tag);
|
||||
|
||||
size_t length = value.size();
|
||||
|
||||
if (length <= 127) {
|
||||
result.push_back(length & 0x7F);
|
||||
} else {
|
||||
size_t required_bytes = 0;
|
||||
if (length & 0xFF000000) {
|
||||
required_bytes = 4;
|
||||
} else if (length & 0x00FF0000) {
|
||||
required_bytes = 3;
|
||||
} else if (length & 0x0000FF00) {
|
||||
required_bytes = 2;
|
||||
} else {
|
||||
required_bytes = 1;
|
||||
}
|
||||
|
||||
result.push_back(0x80 | required_bytes);
|
||||
for (size_t i = 0; i < required_bytes; i++) {
|
||||
result.push_back((length >> (8 * (required_bytes - i - 1))) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
result.insert(result.end(), value.begin(), value.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
}; // namespace ber
|
||||
}; // namespace frame
|
||||
}; // namespace goose
|
||||
@@ -0,0 +1,448 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <openssl/hmac.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <goose/frame.hpp>
|
||||
|
||||
using namespace goose::frame;
|
||||
|
||||
GooseTimestamp::GooseTimestamp(const std::vector<std::uint8_t>& raw) {
|
||||
if (raw.size() != 8) {
|
||||
throw std::runtime_error("GooseTimestamp: raw data is not 8 bytes");
|
||||
}
|
||||
|
||||
seconds = 0;
|
||||
for (size_t i = 0; i < 4; i++) {
|
||||
seconds = (seconds << 8) | raw[i];
|
||||
}
|
||||
|
||||
fraction = 0;
|
||||
for (size_t i = 4; i < 7; i++) {
|
||||
fraction = (fraction << 8) | raw[i];
|
||||
}
|
||||
|
||||
quality_of_time = raw[7];
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> GooseTimestamp::encode() const {
|
||||
std::vector<std::uint8_t> result;
|
||||
auto seconds_be = ber::encode_be(seconds);
|
||||
auto fraction_be = ber::encode_be(fraction);
|
||||
result.insert(result.end(), seconds_be.begin(), seconds_be.end());
|
||||
result.insert(result.end(), fraction_be.begin() + 1, fraction_be.end());
|
||||
result.push_back(quality_of_time);
|
||||
return result;
|
||||
}
|
||||
float GooseTimestamp::to_ms() {
|
||||
return static_cast<std::uint64_t>(seconds) * 1000 + (static_cast<std::uint64_t>(fraction) * 1000) / 0x1000000;
|
||||
}
|
||||
|
||||
GooseTimestamp GooseTimestamp::from_ms(std::uint64_t ms) {
|
||||
std::uint64_t ms_part = ms % 1000;
|
||||
std::uint64_t sec_part = ms / 1000;
|
||||
auto fraction = (ms_part * 0x1000000) / 1000;
|
||||
|
||||
// quality is 10 as milliseconds are used for the fraction, which
|
||||
// corresponds to about 10 bits
|
||||
return GooseTimestamp(sec_part, fraction, 10);
|
||||
}
|
||||
|
||||
GooseTimestamp GooseTimestamp::now() {
|
||||
auto now = std::chrono::system_clock().now().time_since_epoch();
|
||||
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
|
||||
|
||||
return from_ms(now_ms);
|
||||
}
|
||||
|
||||
bool GooseTimestamp::operator==(const GooseTimestamp& other) const {
|
||||
return seconds == other.seconds && fraction == other.fraction && quality_of_time == other.quality_of_time;
|
||||
}
|
||||
|
||||
GoosePDU::GoosePDU(const std::vector<std::uint8_t>& pdu) {
|
||||
auto pdu_c = pdu;
|
||||
goose::frame::ber::BEREntry root(&pdu_c);
|
||||
if (root.tag != 0x61) {
|
||||
throw std::runtime_error("GoosePDU: root tag is not 0x61");
|
||||
}
|
||||
|
||||
if (pdu_c.size() > 0) {
|
||||
throw std::runtime_error("GoosePDU: received extra data, that is not part of BER encoded "
|
||||
"region");
|
||||
}
|
||||
|
||||
auto root_value = root.value;
|
||||
|
||||
// go_cb_ref
|
||||
goose::frame::ber::BEREntry go_cb_ref_entry(&root_value);
|
||||
if (go_cb_ref_entry.tag != 0x80) {
|
||||
throw std::runtime_error("GoosePDU: go_cb_ref tag is not 0x80");
|
||||
}
|
||||
if (go_cb_ref_entry.value.size() > 65) { // todo: check length
|
||||
throw std::runtime_error("GoosePDU: go_cb_ref is too long");
|
||||
}
|
||||
memcpy(go_cb_ref, go_cb_ref_entry.value.data(), go_cb_ref_entry.value.size());
|
||||
|
||||
// time_allowed_to_live
|
||||
ber::PrimitiveBEREntry<std::uint32_t> time_allowed_to_live_entry(root_value, 0x81);
|
||||
time_allowed_to_live = time_allowed_to_live_entry.data;
|
||||
|
||||
// dat_set
|
||||
goose::frame::ber::BEREntry dat_set_entry(&root_value);
|
||||
if (dat_set_entry.tag != 0x82) {
|
||||
throw std::runtime_error("GoosePDU: dat_set tag is not 0x82");
|
||||
}
|
||||
if (dat_set_entry.value.size() > 65) { // todo: check length
|
||||
throw std::runtime_error("GoosePDU: dat_set is too long");
|
||||
}
|
||||
memcpy(dat_set, dat_set_entry.value.data(), dat_set_entry.value.size());
|
||||
|
||||
// go_id
|
||||
goose::frame::ber::BEREntry go_id_entry(&root_value);
|
||||
if (go_id_entry.tag != 0x83) {
|
||||
throw std::runtime_error("GoosePDU: go_id tag is not 0x83");
|
||||
}
|
||||
if (go_id_entry.value.size() > 65) { // todo: check length
|
||||
throw std::runtime_error("GoosePDU: go_id is too long");
|
||||
}
|
||||
memcpy(go_id, go_id_entry.value.data(), go_id_entry.value.size());
|
||||
|
||||
// timestamp
|
||||
goose::frame::ber::BEREntry timestamp_entry(&root_value);
|
||||
if (timestamp_entry.tag != 0x84) {
|
||||
throw std::runtime_error("GoosePDU: timestamp tag is not 0x84");
|
||||
}
|
||||
if (timestamp_entry.value.size() != 8) {
|
||||
throw std::runtime_error("GoosePDU: timestamp is not 8 bytes");
|
||||
}
|
||||
timestamp = GooseTimestamp(timestamp_entry.value);
|
||||
|
||||
// st_num
|
||||
ber::PrimitiveBEREntry<std::uint32_t> st_num_entry(root_value, 0x85);
|
||||
st_num = st_num_entry.data;
|
||||
|
||||
// sq_num
|
||||
ber::PrimitiveBEREntry<std::uint32_t> sq_num_entry(root_value, 0x86);
|
||||
sq_num = sq_num_entry.data;
|
||||
|
||||
// simulation
|
||||
ber::PrimitiveBEREntry<std::uint8_t> simulation_entry(root_value, 0x87);
|
||||
simulation = simulation_entry.data;
|
||||
|
||||
// conf_rev
|
||||
ber::PrimitiveBEREntry<std::uint32_t> conf_rev_entry(root_value, 0x88);
|
||||
conf_rev = conf_rev_entry.data;
|
||||
|
||||
// ndsCom
|
||||
ber::PrimitiveBEREntry<std::uint8_t> ndsCom_entry(root_value, 0x89);
|
||||
ndsCom = ndsCom_entry.data;
|
||||
|
||||
// apdu count
|
||||
ber::PrimitiveBEREntry<std::uint32_t> apdu_count_entry(root_value, 0x8A);
|
||||
auto apdu_entry_count = apdu_count_entry.data;
|
||||
|
||||
// apdu sequence
|
||||
goose::frame::ber::BEREntry apdu_entry(&root_value);
|
||||
if (apdu_entry.tag != 0xAB) {
|
||||
throw std::runtime_error("GoosePDU: apdu tag is not 0xAB");
|
||||
}
|
||||
auto apdu = apdu_entry.value;
|
||||
|
||||
// check that no more data is left in root node
|
||||
if (root_value.size() > 0) {
|
||||
throw std::runtime_error("GoosePDU: frame has extra data");
|
||||
}
|
||||
|
||||
// apdu entries
|
||||
for (size_t i = 0; i < apdu_entry_count; i++) {
|
||||
apdu_entries.emplace_back(&apdu);
|
||||
}
|
||||
|
||||
// check that no more data is left in apdu sequence node
|
||||
if (apdu.size() > 0) {
|
||||
throw std::runtime_error("GoosePDU: apdu has extra data");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> GoosePDU::serialize() const {
|
||||
goose::frame::ber::BEREntry root;
|
||||
root.tag = 0x61;
|
||||
root.value = std::vector<std::uint8_t>();
|
||||
|
||||
goose::frame::ber::BEREntry go_cb_ref_entry;
|
||||
go_cb_ref_entry.tag = 0x80;
|
||||
go_cb_ref_entry.value =
|
||||
std::vector<std::uint8_t>(go_cb_ref, go_cb_ref + strlen(go_cb_ref) + 1); // null terminated string
|
||||
root.add(go_cb_ref_entry);
|
||||
|
||||
goose::frame::ber::BEREntry time_allowed_to_live_entry;
|
||||
time_allowed_to_live_entry.tag = 0x81;
|
||||
time_allowed_to_live_entry.value = ber::encode_be((std::uint32_t)time_allowed_to_live);
|
||||
root.add(time_allowed_to_live_entry);
|
||||
|
||||
goose::frame::ber::BEREntry dat_set_entry;
|
||||
dat_set_entry.tag = 0x82;
|
||||
dat_set_entry.value = std::vector<std::uint8_t>(dat_set, dat_set + strlen(dat_set) + 1); // null terminated string
|
||||
root.add(dat_set_entry);
|
||||
|
||||
goose::frame::ber::BEREntry go_id_entry;
|
||||
go_id_entry.tag = 0x83;
|
||||
go_id_entry.value = std::vector<std::uint8_t>(go_id, go_id + strlen(go_id) + 1); // null
|
||||
// terminated
|
||||
// string
|
||||
root.add(go_id_entry);
|
||||
|
||||
goose::frame::ber::BEREntry timestamp_entry;
|
||||
timestamp_entry.tag = 0x84;
|
||||
timestamp_entry.value = timestamp.encode();
|
||||
root.add(timestamp_entry);
|
||||
|
||||
goose::frame::ber::BEREntry st_num_entry;
|
||||
st_num_entry.tag = 0x85;
|
||||
st_num_entry.value = ber::encode_be((std::uint32_t)st_num);
|
||||
root.add(st_num_entry);
|
||||
|
||||
goose::frame::ber::BEREntry sq_num_entry;
|
||||
sq_num_entry.tag = 0x86;
|
||||
sq_num_entry.value = ber::encode_be((std::uint32_t)sq_num);
|
||||
root.add(sq_num_entry);
|
||||
|
||||
goose::frame::ber::BEREntry simulation_entry;
|
||||
simulation_entry.tag = 0x87;
|
||||
simulation_entry.value = std::vector<std::uint8_t>{simulation};
|
||||
root.add(simulation_entry);
|
||||
|
||||
goose::frame::ber::BEREntry conf_rev_entry;
|
||||
conf_rev_entry.tag = 0x88;
|
||||
conf_rev_entry.value = ber::encode_be((std::uint32_t)conf_rev);
|
||||
root.add(conf_rev_entry);
|
||||
|
||||
ber::BEREntry nds_com_entry;
|
||||
nds_com_entry.tag = 0x89;
|
||||
nds_com_entry.value = std::vector<std::uint8_t>{0}; // todo
|
||||
root.add(nds_com_entry);
|
||||
|
||||
ber::BEREntry apdu_count_entry;
|
||||
apdu_count_entry.tag = 0x8A;
|
||||
apdu_count_entry.value = ber::encode_be((std::uint32_t)apdu_entries.size());
|
||||
root.add(apdu_count_entry);
|
||||
|
||||
ber::BEREntry apdu_entry;
|
||||
apdu_entry.tag = 0xAB;
|
||||
apdu_entry.value = std::vector<std::uint8_t>();
|
||||
for (const auto& entry : apdu_entries) {
|
||||
apdu_entry.add(entry);
|
||||
}
|
||||
root.add(apdu_entry);
|
||||
|
||||
return root.encode();
|
||||
}
|
||||
|
||||
GooseFrameIntf::GooseFrameIntf(const goose_ethernet::EthernetFrame& ethernet_frame) {
|
||||
memcpy(source_mac_address, ethernet_frame.source, 6);
|
||||
memcpy(destination_mac_address, ethernet_frame.destination, 6);
|
||||
|
||||
if (ethernet_frame.ethertype != GOOSE_ETHERTYPE) {
|
||||
throw std::runtime_error("GooseFrame: not a GOOSE frame");
|
||||
}
|
||||
|
||||
if (!ethernet_frame.eth_802_1q_tag.has_value()) {
|
||||
throw std::runtime_error("GooseFrame: no 802.1Q tag");
|
||||
}
|
||||
|
||||
auto tag_802_1q = ethernet_frame.eth_802_1q_tag.value();
|
||||
this->priority = (tag_802_1q & 0xE000) >> 13;
|
||||
this->vlan_id = tag_802_1q & 0x0FFF;
|
||||
|
||||
// appid
|
||||
if (ethernet_frame.payload.size() < 2) {
|
||||
throw std::runtime_error("GooseFrame: no appid");
|
||||
}
|
||||
appid[0] = ethernet_frame.payload[0];
|
||||
appid[1] = ethernet_frame.payload[1];
|
||||
|
||||
std::uint16_t length = (ethernet_frame.payload[2] << 8) | ethernet_frame.payload[3];
|
||||
|
||||
std::uint16_t reserve1 = (ethernet_frame.payload[4] << 8) | ethernet_frame.payload[5];
|
||||
std::uint16_t reserve2 = (ethernet_frame.payload[6] << 8) | ethernet_frame.payload[7];
|
||||
|
||||
// goose pdu
|
||||
goose::frame::GoosePDU pdu(
|
||||
std::vector<std::uint8_t>(ethernet_frame.payload.data() + 8, ethernet_frame.payload.data() + length));
|
||||
this->pdu = pdu;
|
||||
};
|
||||
|
||||
GooseFrame::GooseFrame(const goose_ethernet::EthernetFrame& ethernet_frame) : GooseFrameIntf(ethernet_frame) {
|
||||
std::uint16_t reserve1 = (ethernet_frame.payload[4] << 8) | ethernet_frame.payload[5];
|
||||
std::uint16_t reserve2 = (ethernet_frame.payload[6] << 8) | ethernet_frame.payload[7];
|
||||
|
||||
if (reserve1 != 0) {
|
||||
throw std::runtime_error("GooseFrame: reserve1 byte 2 is not 0");
|
||||
}
|
||||
|
||||
if (reserve2 != 0) {
|
||||
throw std::runtime_error("GooseFrame: reserve2 byte 2 is not 0");
|
||||
}
|
||||
|
||||
if (ethernet_frame.payload.size() != 8 + pdu.serialize().size()) {
|
||||
throw std::runtime_error("GooseFrame: payload size does not match");
|
||||
}
|
||||
}
|
||||
|
||||
goose_ethernet::EthernetFrame GooseFrame::serialize() const {
|
||||
goose_ethernet::EthernetFrame ethernet_frame;
|
||||
memcpy(ethernet_frame.source, source_mac_address, 6);
|
||||
memcpy(ethernet_frame.destination, destination_mac_address, 6);
|
||||
ethernet_frame.ethertype = GOOSE_ETHERTYPE;
|
||||
ethernet_frame.eth_802_1q_tag = (priority << 13) | vlan_id;
|
||||
|
||||
auto pdu_data = pdu.serialize();
|
||||
ethernet_frame.payload.clear();
|
||||
ethernet_frame.payload.push_back(appid[0]);
|
||||
ethernet_frame.payload.push_back(appid[1]);
|
||||
// length
|
||||
auto length = pdu_data.size() + 8;
|
||||
ethernet_frame.payload.push_back(length >> 8);
|
||||
ethernet_frame.payload.push_back(length & 0xFF);
|
||||
// reserve1
|
||||
ethernet_frame.payload.push_back(0);
|
||||
ethernet_frame.payload.push_back(0);
|
||||
// reserve2
|
||||
ethernet_frame.payload.push_back(0);
|
||||
ethernet_frame.payload.push_back(0);
|
||||
// pdu
|
||||
ethernet_frame.payload.insert(ethernet_frame.payload.end(), pdu_data.begin(), pdu_data.end());
|
||||
|
||||
return ethernet_frame;
|
||||
}
|
||||
|
||||
std::uint16_t crc(std::vector<std::uint8_t> data) {
|
||||
std::uint16_t crc = 0xFFFF;
|
||||
for (size_t i = 0; i < data.size(); i++) {
|
||||
crc ^= data[i];
|
||||
for (size_t j = 0; j < 8; j++) {
|
||||
if (crc & 0x0001) {
|
||||
crc = (crc >> 1) ^ 0xA001;
|
||||
} else {
|
||||
crc = crc >> 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
SecureGooseFrame::SecureGooseFrame(const goose_ethernet::EthernetFrame& ethernet_frame,
|
||||
std::optional<std::vector<std::uint8_t>> hmac_key) :
|
||||
GooseFrameIntf(ethernet_frame) {
|
||||
std::uint16_t length = (ethernet_frame.payload[2] << 8) | ethernet_frame.payload[3];
|
||||
|
||||
std::uint16_t reserve1 = (ethernet_frame.payload[4] << 8) | ethernet_frame.payload[5];
|
||||
std::uint16_t reserve2 = (ethernet_frame.payload[6] << 8) | ethernet_frame.payload[7];
|
||||
|
||||
std::uint16_t extended_length = reserve1 & 0x00FF;
|
||||
if (extended_length == 0) {
|
||||
throw std::runtime_error("GooseFrame: reserve1 byte 2 is 0, thus not a secure frame");
|
||||
}
|
||||
if (extended_length < 32) {
|
||||
throw std::runtime_error("GooseFrame: reserve1 byte 2 is less than 32, thus no hmac 256 fits");
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> reserve2_crc_data;
|
||||
reserve2_crc_data.push_back(ethernet_frame.ethertype >> 8);
|
||||
reserve2_crc_data.push_back(ethernet_frame.ethertype & 0xFF);
|
||||
reserve2_crc_data.push_back(ethernet_frame.payload[0]);
|
||||
reserve2_crc_data.push_back(ethernet_frame.payload[1]);
|
||||
reserve2_crc_data.push_back(ethernet_frame.payload[2]);
|
||||
reserve2_crc_data.push_back(ethernet_frame.payload[3]);
|
||||
reserve2_crc_data.push_back(ethernet_frame.payload[4]);
|
||||
reserve2_crc_data.push_back(ethernet_frame.payload[5]);
|
||||
|
||||
std::uint16_t crc_value = crc(reserve2_crc_data);
|
||||
if (crc_value != reserve2) {
|
||||
throw std::runtime_error("GooseFrame: crc value does not match");
|
||||
}
|
||||
|
||||
if (hmac_key.has_value()) {
|
||||
// verify hmac
|
||||
std::vector<std::uint8_t> hmac =
|
||||
std::vector<std::uint8_t>(ethernet_frame.payload.data() + length + extended_length - 32,
|
||||
ethernet_frame.payload.data() + length + extended_length);
|
||||
|
||||
std::vector<std::uint8_t> hmac_data;
|
||||
hmac_data.push_back(ethernet_frame.ethertype >> 8);
|
||||
hmac_data.push_back(ethernet_frame.ethertype & 0xFF);
|
||||
hmac_data.insert(hmac_data.end(), ethernet_frame.payload.begin(),
|
||||
ethernet_frame.payload.begin() + length + extended_length -
|
||||
35); // 35 because of the 32 bytes HMAC and 3 bytes of
|
||||
// some TLV that is not in the HMAC
|
||||
|
||||
std::vector<std::uint8_t> calculated_hmac(32);
|
||||
std::uint32_t calculated_hmac_len = calculated_hmac.size();
|
||||
auto ret = HMAC(EVP_sha256(), hmac_key.value().data(), hmac_key.value().size(), hmac_data.data(),
|
||||
hmac_data.size(), calculated_hmac.data(), &calculated_hmac_len);
|
||||
|
||||
if (ret == NULL) {
|
||||
throw std::runtime_error("SecureGooseFrame: HMAC failed");
|
||||
}
|
||||
|
||||
if (calculated_hmac != hmac) {
|
||||
throw std::runtime_error("SecureGooseFrame: HMAC does not match");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
goose_ethernet::EthernetFrame SecureGooseFrame::serialize(std::vector<std::uint8_t> hmac_key) const {
|
||||
auto pdu_data = pdu.serialize();
|
||||
|
||||
std::uint16_t length_in_header = pdu_data.size() + 8; // appid + length + reserve1 + reserve2
|
||||
|
||||
// Ethernet frame without mac's and
|
||||
// 802.1Q but with ethertype (which is removed later,
|
||||
// before putting it into the EthernetFrame)
|
||||
std::vector<std::uint8_t> ethernet_payload;
|
||||
|
||||
// ethertype
|
||||
ethernet_payload.push_back(0x88);
|
||||
ethernet_payload.push_back(0xB8);
|
||||
// appid
|
||||
ethernet_payload.push_back(appid[0]);
|
||||
ethernet_payload.push_back(appid[1]);
|
||||
// length
|
||||
ethernet_payload.push_back(length_in_header >> 8);
|
||||
ethernet_payload.push_back(length_in_header & 0xFF);
|
||||
// reserve1
|
||||
ethernet_payload.push_back(0);
|
||||
ethernet_payload.push_back(0x23); // 32 bytes hmac + 3 bytes TLV
|
||||
// reserve2
|
||||
std::uint16_t crc_val = crc(ethernet_payload);
|
||||
ethernet_payload.push_back(crc_val >> 8);
|
||||
ethernet_payload.push_back(crc_val & 0xFF);
|
||||
// pdu
|
||||
ethernet_payload.insert(ethernet_payload.end(), pdu_data.begin(), pdu_data.end());
|
||||
|
||||
// Calulate HMAC over the whole frame
|
||||
std::vector<std::uint8_t> hmac(32);
|
||||
|
||||
HMAC(EVP_sha256(), hmac_key.data(), hmac_key.size(), ethernet_payload.data(), ethernet_payload.size(), hmac.data(),
|
||||
NULL);
|
||||
|
||||
// append hmac data to the frame
|
||||
ethernet_payload.push_back(0xad);
|
||||
ethernet_payload.push_back(0x00);
|
||||
ethernet_payload.push_back(0x20);
|
||||
ethernet_payload.insert(ethernet_payload.end(), hmac.begin(), hmac.end());
|
||||
|
||||
// remove ethertype from payload (as this is not part of the EthernetFrame
|
||||
// payload and added by EthernetFrame itself)
|
||||
ethernet_payload.erase(ethernet_payload.begin(), ethernet_payload.begin() + 2);
|
||||
|
||||
// populate EthernetFrame struct
|
||||
goose_ethernet::EthernetFrame ethernet_frame;
|
||||
memcpy(ethernet_frame.source, source_mac_address, 6);
|
||||
memcpy(ethernet_frame.destination, destination_mac_address, 6);
|
||||
ethernet_frame.ethertype = GOOSE_ETHERTYPE;
|
||||
ethernet_frame.eth_802_1q_tag = (priority << 13) | vlan_id;
|
||||
ethernet_frame.payload = ethernet_payload;
|
||||
|
||||
return ethernet_frame;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <goose/sender.hpp>
|
||||
|
||||
using namespace goose::sender;
|
||||
|
||||
Sender::Sender(std::chrono::milliseconds t0, std::vector<std::chrono::milliseconds> ts,
|
||||
std::shared_ptr<goose_ethernet::EthernetInterfaceIntf> intf, logs::LogIntf log) :
|
||||
t0(t0), ts(ts), intf(intf), st_num(0), sq_num(0), current_ts_index(0), current_packet(std::nullopt), log(log) {
|
||||
}
|
||||
|
||||
void Sender::send(SendPacketIntf* packet) {
|
||||
send(std::unique_ptr<SendPacketIntf>(packet));
|
||||
}
|
||||
|
||||
void Sender::send(std::unique_ptr<SendPacketIntf> packet) {
|
||||
std::lock_guard<std::mutex> lock(current_packet_mutex);
|
||||
if (current_packet.has_value() && current_packet.value() == packet) {
|
||||
/// Already sending this packet, no need to send it again, it gets resent
|
||||
/// anyways
|
||||
return;
|
||||
}
|
||||
|
||||
current_packet = std::move(packet);
|
||||
st_num++;
|
||||
sq_num = 0;
|
||||
current_ts_index = 0;
|
||||
has_new_package = true;
|
||||
current_packet_cv.notify_all();
|
||||
}
|
||||
|
||||
void Sender::start() {
|
||||
stop_flag = false;
|
||||
thread = std::thread([this] {
|
||||
for (;;) {
|
||||
if (stop_flag) {
|
||||
return;
|
||||
}
|
||||
run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Sender::stop() {
|
||||
if (thread.has_value()) {
|
||||
stop_flag = true;
|
||||
current_packet_cv.notify_all();
|
||||
|
||||
thread->join();
|
||||
thread = std::nullopt;
|
||||
}
|
||||
stop_flag = false;
|
||||
}
|
||||
|
||||
// Note: ran periodically
|
||||
void Sender::run() {
|
||||
if (stop_flag) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(current_packet_mutex);
|
||||
// No packet to send yet, wait for a packt to be sent using send()
|
||||
if (!current_packet.has_value()) {
|
||||
log.verbose << "Waiting for first packet...";
|
||||
current_packet_cv.wait(lock, [this] { return stop_flag || current_packet.has_value(); });
|
||||
log.verbose << "Got first packet!";
|
||||
// after wait, we own the lock and send the packet
|
||||
} else {
|
||||
std::chrono::milliseconds wait_time = t0;
|
||||
if (current_ts_index < ts.size()) {
|
||||
wait_time = ts[current_ts_index];
|
||||
current_ts_index++;
|
||||
}
|
||||
current_packet_cv.wait_for(lock, wait_time, [this] { return stop_flag || has_new_package; });
|
||||
has_new_package = false;
|
||||
}
|
||||
|
||||
{
|
||||
// Maybe the stop flag was set while waiting
|
||||
if (stop_flag) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send the packet
|
||||
try {
|
||||
intf->send_packet(current_packet.value()->build_packet({
|
||||
sq_num,
|
||||
st_num,
|
||||
}));
|
||||
} catch (...) {
|
||||
log.error << "goose::sender: Failed to send packet";
|
||||
}
|
||||
sq_num++;
|
||||
|
||||
// In the next run the else case of the if above will do the waiting
|
||||
}
|
||||
|
||||
const std::uint8_t* Sender::get_mac_address() const {
|
||||
return intf->get_mac_address();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
include(GoogleTest)
|
||||
|
||||
file(GLOB_RECURSE GOOSE_TEST_SOURCES "*.cpp")
|
||||
add_executable(goose-tests ${GOOSE_TEST_SOURCES})
|
||||
target_link_libraries(goose-tests PRIVATE goose gtest_main)
|
||||
gtest_discover_tests(goose-tests EXTRA_ARGS "--gtest_repeat=3")
|
||||
@@ -0,0 +1,126 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <goose/ber.hpp>
|
||||
|
||||
TEST(BEREntry, encode_small_length) {
|
||||
goose::frame::ber::BEREntry entry;
|
||||
entry.tag = 0x01;
|
||||
entry.value = {0x03, 0x04};
|
||||
|
||||
std::vector<std::uint8_t> encoded = entry.encode();
|
||||
ASSERT_EQ(encoded.size(), 4);
|
||||
ASSERT_EQ(encoded[0], 0x01);
|
||||
ASSERT_EQ(encoded[1], 0x02);
|
||||
ASSERT_EQ(encoded[2], 0x03);
|
||||
ASSERT_EQ(encoded[3], 0x04);
|
||||
}
|
||||
|
||||
TEST(BEREntry, encode_mid_length) {
|
||||
goose::frame::ber::BEREntry entry;
|
||||
entry.tag = 0x01;
|
||||
entry.value.resize(200);
|
||||
entry.value[0] = 0xde;
|
||||
entry.value[1] = 0xad;
|
||||
|
||||
std::vector<std::uint8_t> encoded = entry.encode();
|
||||
ASSERT_EQ(encoded.size(), 203);
|
||||
ASSERT_EQ(encoded[0], 0x01);
|
||||
ASSERT_EQ(encoded[1], 0x81);
|
||||
ASSERT_EQ(encoded[2], 200);
|
||||
ASSERT_EQ(encoded[3], 0xde);
|
||||
ASSERT_EQ(encoded[4], 0xad);
|
||||
}
|
||||
|
||||
TEST(BEREntry, encode_large_length) {
|
||||
goose::frame::ber::BEREntry entry;
|
||||
entry.tag = 0x01;
|
||||
entry.value.resize(0x100);
|
||||
entry.value[0] = 0xde;
|
||||
entry.value[1] = 0xad;
|
||||
|
||||
std::vector<std::uint8_t> encoded = entry.encode();
|
||||
ASSERT_EQ(encoded.size(), 4 + 0x100);
|
||||
ASSERT_EQ(encoded[0], 0x01);
|
||||
ASSERT_EQ(encoded[1], 0x82);
|
||||
ASSERT_EQ(encoded[2], 0x01);
|
||||
ASSERT_EQ(encoded[3], 0x00);
|
||||
ASSERT_EQ(encoded[4], 0xde);
|
||||
ASSERT_EQ(encoded[5], 0xad);
|
||||
}
|
||||
|
||||
TEST(BEREntry, decode_small_frame) {
|
||||
std::vector<std::uint8_t> input = {0xde, 0x05, 0xca, 0xfe, 0xba, 0xbe, 0xef};
|
||||
goose::frame::ber::BEREntry entry(&input);
|
||||
|
||||
ASSERT_EQ(entry.tag, 0xde);
|
||||
ASSERT_EQ(entry.value.size(), 5);
|
||||
ASSERT_EQ(entry.value[0], 0xca);
|
||||
ASSERT_EQ(entry.value[1], 0xfe);
|
||||
ASSERT_EQ(entry.value[2], 0xba);
|
||||
ASSERT_EQ(entry.value[3], 0xbe);
|
||||
ASSERT_EQ(entry.value[4], 0xef);
|
||||
|
||||
ASSERT_EQ(input.size(), 0);
|
||||
}
|
||||
|
||||
TEST(BEREntry, decode_multiple_frames) {
|
||||
std::vector<std::uint8_t> input = {
|
||||
0xde, 0x05, 0xca, 0xfe, 0xba, 0xbe, 0xef, // First frame
|
||||
0xad, 0x02, 0xbe, 0xef, // Second frame
|
||||
0xca, 0x06, 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe // Third frame
|
||||
};
|
||||
|
||||
goose::frame::ber::BEREntry entry1(&input);
|
||||
ASSERT_EQ(entry1.tag, 0xde);
|
||||
ASSERT_EQ(entry1.value.size(), 5);
|
||||
|
||||
goose::frame::ber::BEREntry entry2(&input);
|
||||
ASSERT_EQ(entry2.tag, 0xad);
|
||||
ASSERT_EQ(entry2.value.size(), 2);
|
||||
|
||||
goose::frame::ber::BEREntry entry3(&input);
|
||||
ASSERT_EQ(entry3.tag, 0xca);
|
||||
ASSERT_EQ(entry3.value.size(), 6);
|
||||
|
||||
ASSERT_EQ(input.size(), 0);
|
||||
}
|
||||
|
||||
TEST(BEREntry, decode_long_frame) {
|
||||
std::vector<std::uint8_t> input = {
|
||||
0xde, // tag
|
||||
0x82, 0x01, 0x00, // length: 0x100
|
||||
0xca, 0xfe, // first 2 bytes of payload
|
||||
};
|
||||
input.resize(0x100 + 4);
|
||||
|
||||
goose::frame::ber::BEREntry entry(&input);
|
||||
|
||||
ASSERT_EQ(entry.tag, 0xde);
|
||||
ASSERT_EQ(entry.value.size(), 0x100);
|
||||
ASSERT_EQ(entry.value[0], 0xca);
|
||||
ASSERT_EQ(entry.value[1], 0xfe);
|
||||
|
||||
ASSERT_EQ(input.size(), 0);
|
||||
}
|
||||
|
||||
TEST(BEREntry, decode_mid_length_frame) {
|
||||
std::vector<std::uint8_t> input = {
|
||||
0xde, // tag
|
||||
0x81, 200, // length: 200
|
||||
0xca, 0xfe, // first 2 bytes of payload
|
||||
};
|
||||
input.resize(3 + 200);
|
||||
|
||||
goose::frame::ber::BEREntry entry(&input);
|
||||
|
||||
ASSERT_EQ(entry.tag, 0xde);
|
||||
ASSERT_EQ(entry.value.size(), 200);
|
||||
ASSERT_EQ(entry.value[0], 0xca);
|
||||
ASSERT_EQ(entry.value[1], 0xfe);
|
||||
|
||||
ASSERT_EQ(input.size(), 0);
|
||||
}
|
||||
|
||||
// todo: PrimitiveBEREntry
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <goose/frame.hpp>
|
||||
|
||||
#include "hex_to_vec.hpp"
|
||||
|
||||
TEST(GooseFrame, encode_decode) {
|
||||
goose::frame::GooseFrame goose_frame;
|
||||
memset(goose_frame.destination_mac_address, 0, 2);
|
||||
memset(goose_frame.source_mac_address, 0, 2);
|
||||
|
||||
goose_frame.vlan_id = 2;
|
||||
goose_frame.priority = 5;
|
||||
|
||||
goose_frame.appid[0] = 0x00;
|
||||
goose_frame.appid[1] = 0x01;
|
||||
|
||||
strcpy(goose_frame.pdu.go_cb_ref, "PDU");
|
||||
goose_frame.pdu.time_allowed_to_live = 10000;
|
||||
strcpy(goose_frame.pdu.dat_set, "DAT_SET");
|
||||
strcpy(goose_frame.pdu.go_id, "GO_ID");
|
||||
goose_frame.pdu.timestamp = goose::frame::GooseTimestamp::from_ms(1667349763000);
|
||||
goose_frame.pdu.st_num = 1;
|
||||
goose_frame.pdu.sq_num = 0;
|
||||
goose_frame.pdu.simulation = false;
|
||||
goose_frame.pdu.conf_rev = 0;
|
||||
goose_frame.pdu.ndsCom = 0;
|
||||
goose_frame.pdu.apdu_entries.resize(1);
|
||||
goose_frame.pdu.apdu_entries[0].tag = 0x86;
|
||||
goose_frame.pdu.apdu_entries[0].value = {0, 1};
|
||||
|
||||
auto encoded = goose_frame.serialize();
|
||||
ASSERT_EQ(encoded.eth_802_1q_tag.value(), 0xA002);
|
||||
|
||||
auto decoded = goose::frame::GooseFrame(encoded);
|
||||
|
||||
ASSERT_EQ(decoded.vlan_id, 2);
|
||||
ASSERT_EQ(decoded.priority, 5);
|
||||
ASSERT_EQ(decoded.appid[0], 0x00);
|
||||
ASSERT_EQ(decoded.appid[1], 0x01);
|
||||
ASSERT_STREQ(decoded.pdu.go_cb_ref, goose_frame.pdu.go_cb_ref);
|
||||
ASSERT_EQ(decoded.pdu.time_allowed_to_live, goose_frame.pdu.time_allowed_to_live);
|
||||
ASSERT_STREQ(decoded.pdu.dat_set, goose_frame.pdu.dat_set);
|
||||
ASSERT_STREQ(decoded.pdu.go_id, goose_frame.pdu.go_id);
|
||||
ASSERT_EQ(decoded.pdu.timestamp, goose_frame.pdu.timestamp);
|
||||
ASSERT_EQ(decoded.pdu.st_num, goose_frame.pdu.st_num);
|
||||
ASSERT_EQ(decoded.pdu.sq_num, goose_frame.pdu.sq_num);
|
||||
ASSERT_EQ(decoded.pdu.simulation, goose_frame.pdu.simulation);
|
||||
ASSERT_EQ(decoded.pdu.conf_rev, goose_frame.pdu.conf_rev);
|
||||
ASSERT_EQ(decoded.pdu.ndsCom, goose_frame.pdu.ndsCom);
|
||||
ASSERT_EQ(decoded.pdu.apdu_entries.size(), 1);
|
||||
ASSERT_EQ(decoded.pdu.apdu_entries[0].tag, 0x86);
|
||||
ASSERT_EQ(decoded.pdu.apdu_entries[0].value.size(), 2);
|
||||
ASSERT_EQ(decoded.pdu.apdu_entries[0].value[0], 0);
|
||||
ASSERT_EQ(decoded.pdu.apdu_entries[0].value[1], 1);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <goose/frame.hpp>
|
||||
|
||||
TEST(GoosePDU, decode_real_world_example_1) {
|
||||
const char hex_data[] = "618197801543432f3024474f24506f776572526571756573740081022710821543432f30"
|
||||
"24474f24506f7765725265717565737400831543432f3024474f24506f77657252657175"
|
||||
"6573740084086361bd030000000a85040000000186040000000087010088040000000089"
|
||||
"01008a0400000008ab24860200018602ffff860200018602000087040000000087040000"
|
||||
"00008602ffff8602ffff";
|
||||
std::uint8_t data[sizeof(hex_data) / 2];
|
||||
for (size_t i = 0; i < sizeof(hex_data) / 2; i++) {
|
||||
sscanf(&hex_data[i * 2], "%2hhx", &data[i]);
|
||||
}
|
||||
|
||||
goose::frame::GoosePDU pdu(std::vector<std::uint8_t>(data, data + sizeof(data)));
|
||||
ASSERT_STREQ(pdu.go_cb_ref, "CC/0$GO$PowerRequest");
|
||||
ASSERT_EQ(pdu.time_allowed_to_live, 10000);
|
||||
ASSERT_STREQ(pdu.dat_set, "CC/0$GO$PowerRequest");
|
||||
ASSERT_STREQ(pdu.go_id, "CC/0$GO$PowerRequest");
|
||||
|
||||
ASSERT_EQ(pdu.timestamp.to_ms(), 1667349763000);
|
||||
ASSERT_EQ(pdu.st_num, 1);
|
||||
ASSERT_EQ(pdu.sq_num, 0);
|
||||
ASSERT_FALSE(pdu.simulation);
|
||||
ASSERT_EQ(pdu.conf_rev, 0);
|
||||
ASSERT_EQ(pdu.ndsCom, 0);
|
||||
ASSERT_EQ(pdu.apdu_entries.size(), 8);
|
||||
|
||||
ASSERT_EQ(pdu.apdu_entries[0].tag, 0x86);
|
||||
ASSERT_EQ(pdu.apdu_entries[0].value.size(), 2);
|
||||
ASSERT_EQ(pdu.apdu_entries[0].value[0], 0);
|
||||
ASSERT_EQ(pdu.apdu_entries[0].value[1], 1);
|
||||
|
||||
ASSERT_EQ(pdu.apdu_entries[1].tag, 0x86);
|
||||
ASSERT_EQ(pdu.apdu_entries[1].value.size(), 2);
|
||||
ASSERT_EQ(pdu.apdu_entries[1].value[0], 0xff);
|
||||
ASSERT_EQ(pdu.apdu_entries[1].value[1], 0xff);
|
||||
|
||||
ASSERT_EQ(pdu.apdu_entries[2].tag, 0x86);
|
||||
ASSERT_EQ(pdu.apdu_entries[2].value.size(), 2);
|
||||
ASSERT_EQ(pdu.apdu_entries[2].value[0], 0);
|
||||
ASSERT_EQ(pdu.apdu_entries[2].value[1], 1);
|
||||
|
||||
ASSERT_EQ(pdu.apdu_entries[3].tag, 0x86);
|
||||
ASSERT_EQ(pdu.apdu_entries[3].value.size(), 2);
|
||||
ASSERT_EQ(pdu.apdu_entries[3].value[0], 0);
|
||||
ASSERT_EQ(pdu.apdu_entries[3].value[1], 0);
|
||||
|
||||
ASSERT_EQ(pdu.apdu_entries[4].tag, 0x87);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value.size(), 4);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value[0], 0);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value[1], 0);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value[2], 0);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value[3], 0);
|
||||
|
||||
// rest of the fields are similar, not very interesting to test
|
||||
}
|
||||
|
||||
TEST(GoosePDU, decode_real_world_example_2) {
|
||||
const char hex_data[] = "618197801543432f3024474f24506f776572526571756573740081022710821543432f30"
|
||||
"24474f24506f7765725265717565737400831543432f3024474f24506f77657252657175"
|
||||
"6573740084086361bd160000000a85040000000186040000000087010088040000000089"
|
||||
"01008a0400000008ab24860200018602ffff86020005860200008704439a800087044248"
|
||||
"00008602ffff8602ffff";
|
||||
std::uint8_t data[sizeof(hex_data) / 2];
|
||||
for (size_t i = 0; i < sizeof(hex_data) / 2; i++) {
|
||||
sscanf(&hex_data[i * 2], "%2hhx", &data[i]);
|
||||
}
|
||||
|
||||
goose::frame::GoosePDU pdu(std::vector<std::uint8_t>(data, data + sizeof(data)));
|
||||
ASSERT_STREQ(pdu.go_cb_ref, "CC/0$GO$PowerRequest");
|
||||
ASSERT_EQ(pdu.time_allowed_to_live, 10000);
|
||||
ASSERT_STREQ(pdu.dat_set, "CC/0$GO$PowerRequest");
|
||||
ASSERT_STREQ(pdu.go_id, "CC/0$GO$PowerRequest");
|
||||
|
||||
ASSERT_EQ(pdu.timestamp.to_ms(), 1667349782000);
|
||||
ASSERT_EQ(pdu.st_num, 1);
|
||||
ASSERT_EQ(pdu.sq_num, 0);
|
||||
ASSERT_FALSE(pdu.simulation);
|
||||
ASSERT_EQ(pdu.conf_rev, 0);
|
||||
ASSERT_EQ(pdu.ndsCom, 0);
|
||||
ASSERT_EQ(pdu.apdu_entries.size(), 8);
|
||||
|
||||
ASSERT_EQ(pdu.apdu_entries[0].tag, 0x86);
|
||||
ASSERT_EQ(pdu.apdu_entries[0].value.size(), 2);
|
||||
ASSERT_EQ(pdu.apdu_entries[0].value[0], 0);
|
||||
ASSERT_EQ(pdu.apdu_entries[0].value[1], 1);
|
||||
|
||||
ASSERT_EQ(pdu.apdu_entries[1].tag, 0x86);
|
||||
ASSERT_EQ(pdu.apdu_entries[1].value.size(), 2);
|
||||
ASSERT_EQ(pdu.apdu_entries[1].value[0], 0xff);
|
||||
ASSERT_EQ(pdu.apdu_entries[1].value[1], 0xff);
|
||||
|
||||
ASSERT_EQ(pdu.apdu_entries[2].tag, 0x86);
|
||||
ASSERT_EQ(pdu.apdu_entries[2].value.size(), 2);
|
||||
ASSERT_EQ(pdu.apdu_entries[2].value[0], 0);
|
||||
ASSERT_EQ(pdu.apdu_entries[2].value[1], 5);
|
||||
|
||||
ASSERT_EQ(pdu.apdu_entries[3].tag, 0x86);
|
||||
ASSERT_EQ(pdu.apdu_entries[3].value.size(), 2);
|
||||
ASSERT_EQ(pdu.apdu_entries[3].value[0], 0);
|
||||
ASSERT_EQ(pdu.apdu_entries[3].value[1], 0);
|
||||
|
||||
ASSERT_EQ(pdu.apdu_entries[4].tag, 0x87);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value.size(), 4);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value[0], 0x43);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value[1], 0x9a);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value[2], 0x80);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value[3], 0x00);
|
||||
|
||||
ASSERT_EQ(pdu.apdu_entries[5].tag, 0x87);
|
||||
ASSERT_EQ(pdu.apdu_entries[5].value.size(), 4);
|
||||
ASSERT_EQ(pdu.apdu_entries[5].value[0], 0x42);
|
||||
ASSERT_EQ(pdu.apdu_entries[5].value[1], 0x48);
|
||||
ASSERT_EQ(pdu.apdu_entries[5].value[2], 0x00);
|
||||
ASSERT_EQ(pdu.apdu_entries[5].value[3], 0x00);
|
||||
|
||||
// rest of the fields are similar, not very interesting to test
|
||||
}
|
||||
|
||||
TEST(GoosePDU, encode_decode_test) {
|
||||
goose::frame::GoosePDU pdu;
|
||||
strcpy(pdu.go_cb_ref, "GO_CB_REF");
|
||||
pdu.time_allowed_to_live = 10000;
|
||||
strcpy(pdu.dat_set, "DAT_SET");
|
||||
strcpy(pdu.go_id, "GO_ID");
|
||||
pdu.timestamp = goose::frame::GooseTimestamp::from_ms(1667349763000);
|
||||
pdu.st_num = 1;
|
||||
pdu.sq_num = 0;
|
||||
pdu.simulation = false;
|
||||
pdu.conf_rev = 0;
|
||||
pdu.ndsCom = 0;
|
||||
pdu.apdu_entries.resize(2);
|
||||
pdu.apdu_entries[0].tag = 0x86;
|
||||
pdu.apdu_entries[0].value = {0, 1};
|
||||
pdu.apdu_entries[1].tag = 0x87;
|
||||
pdu.apdu_entries[1].value = {0, 0, 0, 0};
|
||||
|
||||
auto encoded = pdu.serialize();
|
||||
ASSERT_EQ(encoded.size(), 90);
|
||||
|
||||
goose::frame::GoosePDU decoded(encoded);
|
||||
ASSERT_STREQ(decoded.go_cb_ref, "GO_CB_REF");
|
||||
ASSERT_EQ(decoded.time_allowed_to_live, 10000);
|
||||
ASSERT_STREQ(decoded.dat_set, "DAT_SET");
|
||||
ASSERT_STREQ(decoded.go_id, "GO_ID");
|
||||
ASSERT_EQ(decoded.timestamp.to_ms(), 1667349763000);
|
||||
ASSERT_EQ(decoded.st_num, 1);
|
||||
ASSERT_EQ(decoded.sq_num, 0);
|
||||
ASSERT_FALSE(decoded.simulation);
|
||||
ASSERT_EQ(decoded.conf_rev, 0);
|
||||
ASSERT_EQ(decoded.ndsCom, 0);
|
||||
ASSERT_EQ(decoded.apdu_entries.size(), 2);
|
||||
|
||||
ASSERT_EQ(decoded.apdu_entries[0].tag, 0x86);
|
||||
ASSERT_EQ(decoded.apdu_entries[0].value.size(), 2);
|
||||
ASSERT_EQ(decoded.apdu_entries[0].value[0], 0);
|
||||
ASSERT_EQ(decoded.apdu_entries[0].value[1], 1);
|
||||
|
||||
ASSERT_EQ(decoded.apdu_entries[1].tag, 0x87);
|
||||
ASSERT_EQ(decoded.apdu_entries[1].value.size(), 4);
|
||||
ASSERT_EQ(decoded.apdu_entries[1].value[0], 0);
|
||||
ASSERT_EQ(decoded.apdu_entries[1].value[1], 0);
|
||||
ASSERT_EQ(decoded.apdu_entries[1].value[2], 0);
|
||||
ASSERT_EQ(decoded.apdu_entries[1].value[3], 0);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user