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,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);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <goose/frame.hpp>
|
||||
#include <thread>
|
||||
|
||||
using namespace goose::frame;
|
||||
|
||||
TEST(GooseTimestamp, from_ms_works) {
|
||||
auto timestamp = GooseTimestamp::from_ms(500);
|
||||
ASSERT_EQ(timestamp.seconds, 0);
|
||||
ASSERT_EQ(timestamp.fraction, 0x800000);
|
||||
ASSERT_EQ(timestamp.quality_of_time, 0x0a);
|
||||
|
||||
timestamp = GooseTimestamp::from_ms(1000);
|
||||
ASSERT_EQ(timestamp.seconds, 1);
|
||||
ASSERT_EQ(timestamp.fraction, 0);
|
||||
ASSERT_EQ(timestamp.quality_of_time, 0x0a);
|
||||
}
|
||||
|
||||
TEST(GooseTimestamp, to_ms_works) {
|
||||
auto timestamp = GooseTimestamp::from_ms(1500);
|
||||
ASSERT_EQ(timestamp.to_ms(), 1500);
|
||||
}
|
||||
|
||||
TEST(GooseTimestamp, encode_works) {
|
||||
auto timestamp = GooseTimestamp::from_ms(1500);
|
||||
auto encoded = timestamp.encode();
|
||||
ASSERT_EQ(encoded.size(), 8);
|
||||
ASSERT_EQ(encoded[0], 0);
|
||||
ASSERT_EQ(encoded[1], 0);
|
||||
ASSERT_EQ(encoded[2], 0);
|
||||
ASSERT_EQ(encoded[3], 1);
|
||||
ASSERT_EQ(encoded[4], 0x80);
|
||||
ASSERT_EQ(encoded[5], 0);
|
||||
ASSERT_EQ(encoded[6], 0);
|
||||
ASSERT_EQ(encoded[7], 0x0a);
|
||||
}
|
||||
|
||||
TEST(GooseTimestamp, parsing_works) {
|
||||
std::vector<std::uint8_t> raw = {0, 0, 0, 1, 0x80, 0, 0, 0x0a};
|
||||
GooseTimestamp timestamp(raw);
|
||||
ASSERT_EQ(timestamp.seconds, 1);
|
||||
ASSERT_EQ(timestamp.fraction, 0x800000);
|
||||
ASSERT_EQ(timestamp.quality_of_time, 0x0a);
|
||||
}
|
||||
|
||||
TEST(GooseTimestamp, now_works) {
|
||||
auto timestamp = GooseTimestamp::now();
|
||||
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
|
||||
|
||||
auto diff = now_ms - timestamp.to_ms();
|
||||
|
||||
ASSERT_LT(abs(diff), 10);
|
||||
}
|
||||
|
||||
TEST(GooseTimestamp, parsing_invalid_size) {
|
||||
std::vector<std::uint8_t> raw = {0, 0, 0, 1, 0x80, 0, 0, 0x0a, 0xff};
|
||||
ASSERT_THROW(GooseTimestamp timestamp(raw), std::runtime_error);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "hex_to_vec.hpp"
|
||||
|
||||
void hex_to_vec(const char* hex_data, std::uint8_t* data, size_t data_size) {
|
||||
for (size_t i = 0; i < data_size; i++) {
|
||||
sscanf(&hex_data[i * 2], "%2hhx", &data[i]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
void hex_to_vec(const char* hex_data, std::uint8_t* data, size_t data_size);
|
||||
@@ -0,0 +1,265 @@
|
||||
// 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(SecureGooseFrame, real_world_example) {
|
||||
const char data_hex[] = "2c52afb6ed180080e11614028100A0C888B80001"
|
||||
"00a200233dac618197801543432f3024"
|
||||
"474f24506f7765725265717565737400"
|
||||
"81022710821543432f3024474f24506f"
|
||||
"7765725265717565737400831543432f"
|
||||
"3024474f24506f776572526571756573"
|
||||
"740084086361bd030000000a85040000"
|
||||
"00018604000000008701008804000000"
|
||||
"008901008a0400000008ab2486020001"
|
||||
"8602ffff860200018602000087040000"
|
||||
"00008704000000008602ffff8602ffff"
|
||||
"ad00207929ec787000393de8800a61b2"
|
||||
"b996f8d7b14bf55eda560562668fc890"
|
||||
"2ba088";
|
||||
std::uint8_t data[sizeof(data_hex) / 2];
|
||||
|
||||
hex_to_vec(data_hex, data, sizeof(data));
|
||||
|
||||
goose_ethernet::EthernetFrame ethernet_frame(data, sizeof(data));
|
||||
goose::frame::SecureGooseFrame goose_frame(ethernet_frame);
|
||||
|
||||
ASSERT_EQ(goose_frame.destination_mac_address[0], 0x2c);
|
||||
ASSERT_EQ(goose_frame.destination_mac_address[1], 0x52);
|
||||
ASSERT_EQ(goose_frame.destination_mac_address[2], 0xaf);
|
||||
ASSERT_EQ(goose_frame.destination_mac_address[3], 0xb6);
|
||||
ASSERT_EQ(goose_frame.destination_mac_address[4], 0xed);
|
||||
ASSERT_EQ(goose_frame.destination_mac_address[5], 0x18);
|
||||
|
||||
ASSERT_EQ(goose_frame.source_mac_address[0], 0x00);
|
||||
ASSERT_EQ(goose_frame.source_mac_address[1], 0x80);
|
||||
ASSERT_EQ(goose_frame.source_mac_address[2], 0xe1);
|
||||
ASSERT_EQ(goose_frame.source_mac_address[3], 0x16);
|
||||
ASSERT_EQ(goose_frame.source_mac_address[4], 0x14);
|
||||
ASSERT_EQ(goose_frame.source_mac_address[5], 0x02);
|
||||
|
||||
ASSERT_EQ(goose_frame.appid[0], 0x00);
|
||||
ASSERT_EQ(goose_frame.appid[1], 0x01);
|
||||
|
||||
ASSERT_EQ(goose_frame.vlan_id, 0xC8);
|
||||
ASSERT_EQ(goose_frame.priority, 5);
|
||||
|
||||
ASSERT_STREQ(goose_frame.pdu.go_cb_ref, "CC/0$GO$PowerRequest");
|
||||
ASSERT_EQ(goose_frame.pdu.time_allowed_to_live, 10000);
|
||||
ASSERT_STREQ(goose_frame.pdu.dat_set, "CC/0$GO$PowerRequest");
|
||||
ASSERT_STREQ(goose_frame.pdu.go_id, "CC/0$GO$PowerRequest");
|
||||
ASSERT_EQ(goose_frame.pdu.timestamp.to_ms(), 1667349763000);
|
||||
ASSERT_EQ(goose_frame.pdu.st_num, 1);
|
||||
ASSERT_EQ(goose_frame.pdu.sq_num, 0);
|
||||
ASSERT_FALSE(goose_frame.pdu.simulation);
|
||||
ASSERT_EQ(goose_frame.pdu.conf_rev, 0);
|
||||
ASSERT_EQ(goose_frame.pdu.ndsCom, 0);
|
||||
ASSERT_EQ(goose_frame.pdu.apdu_entries.size(), 8);
|
||||
|
||||
ASSERT_EQ(goose_frame.pdu.apdu_entries[0].tag, 0x86);
|
||||
ASSERT_EQ(goose_frame.pdu.apdu_entries[0].value.size(), 2);
|
||||
ASSERT_EQ(goose_frame.pdu.apdu_entries[0].value[0], 0x00);
|
||||
ASSERT_EQ(goose_frame.pdu.apdu_entries[0].value[1], 0x01);
|
||||
|
||||
ASSERT_EQ(goose_frame.pdu.apdu_entries[1].tag, 0x86);
|
||||
ASSERT_EQ(goose_frame.pdu.apdu_entries[1].value.size(), 2);
|
||||
ASSERT_EQ(goose_frame.pdu.apdu_entries[1].value[0], 0xff);
|
||||
ASSERT_EQ(goose_frame.pdu.apdu_entries[1].value[1], 0xff);
|
||||
}
|
||||
|
||||
TEST(SecureGooseFrame, real_world_example_invalid_crc) {
|
||||
const char data_hex[] = "2c52afb6ed180080e11614028100A0C888B80001"
|
||||
"00a20023dead618197801543432f3024"
|
||||
"474f24506f7765725265717565737400"
|
||||
"81022710821543432f3024474f24506f"
|
||||
"7765725265717565737400831543432f"
|
||||
"3024474f24506f776572526571756573"
|
||||
"740084086361bd030000000a85040000"
|
||||
"00018604000000008701008804000000"
|
||||
"008901008a0400000008ab2486020001"
|
||||
"8602ffff860200018602000087040000"
|
||||
"00008704000000008602ffff8602ffff"
|
||||
"ad00207929ec787000393de8800a61b2"
|
||||
"b996f8d7b14bf55eda560562668fc890"
|
||||
"2ba088";
|
||||
std::uint8_t data[sizeof(data_hex) / 2];
|
||||
|
||||
hex_to_vec(data_hex, data, sizeof(data));
|
||||
|
||||
goose_ethernet::EthernetFrame ethernet_frame(data, sizeof(data));
|
||||
ASSERT_THROW(goose::frame::SecureGooseFrame goose_frame(ethernet_frame), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(SecureGooseFrame, real_world_example_invalid_root_tag) {
|
||||
const char data_hex[] = "2c52afb6ed180080e11614028100A0C888B80001"
|
||||
"00a200233dac638197801543432f3024"
|
||||
"474f24506f7765725265717565737400"
|
||||
"81022710821543432f3024474f24506f"
|
||||
"7765725265717565737400831543432f"
|
||||
"3024474f24506f776572526571756573"
|
||||
"740084086361bd030000000a85040000"
|
||||
"00018604000000008701008804000000"
|
||||
"008901008a0400000008ab2486020001"
|
||||
"8602ffff860200018602000087040000"
|
||||
"00008704000000008602ffff8602ffff"
|
||||
"ad00207929ec787000393de8800a61b2"
|
||||
"b996f8d7b14bf55eda560562668fc890"
|
||||
"2ba088";
|
||||
std::uint8_t data[sizeof(data_hex) / 2];
|
||||
|
||||
hex_to_vec(data_hex, data, sizeof(data));
|
||||
|
||||
goose_ethernet::EthernetFrame ethernet_frame(data, sizeof(data));
|
||||
ASSERT_THROW(goose::frame::SecureGooseFrame goose_frame(ethernet_frame), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(SecureGooseFrame, encode_decode) {
|
||||
goose::frame::SecureGooseFrame goose_frame;
|
||||
goose_frame.appid[0] = 0x00;
|
||||
goose_frame.appid[1] = 0x01;
|
||||
memset(goose_frame.source_mac_address, 0x00, 6);
|
||||
memset(goose_frame.destination_mac_address, 0x00, 6);
|
||||
goose_frame.vlan_id = 2;
|
||||
goose_frame.priority = 7;
|
||||
|
||||
{
|
||||
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 = {0xde, 0xad, 0xbe, 0xef};
|
||||
goose_frame.pdu = pdu;
|
||||
}
|
||||
|
||||
std::uint8_t key[48] = {0};
|
||||
for (size_t i = 0; i < sizeof(key); i++) {
|
||||
key[i] = i;
|
||||
}
|
||||
|
||||
auto serialized = goose_frame.serialize(std::vector<std::uint8_t>(key, key + sizeof(key)));
|
||||
|
||||
auto deserialized = goose::frame::SecureGooseFrame(serialized, std::vector<std::uint8_t>(key, key + sizeof(key)));
|
||||
|
||||
ASSERT_EQ(deserialized.vlan_id, 2);
|
||||
ASSERT_EQ(deserialized.priority, 7);
|
||||
|
||||
ASSERT_EQ(deserialized.appid[0], 0x00);
|
||||
ASSERT_EQ(deserialized.appid[1], 0x01);
|
||||
ASSERT_STREQ(deserialized.pdu.go_cb_ref, goose_frame.pdu.go_cb_ref);
|
||||
ASSERT_EQ(deserialized.pdu.time_allowed_to_live, goose_frame.pdu.time_allowed_to_live);
|
||||
ASSERT_STREQ(deserialized.pdu.dat_set, goose_frame.pdu.dat_set);
|
||||
ASSERT_STREQ(deserialized.pdu.go_id, goose_frame.pdu.go_id);
|
||||
ASSERT_EQ(deserialized.pdu.timestamp, goose_frame.pdu.timestamp);
|
||||
ASSERT_EQ(deserialized.pdu.st_num, goose_frame.pdu.st_num);
|
||||
ASSERT_EQ(deserialized.pdu.sq_num, goose_frame.pdu.sq_num);
|
||||
ASSERT_EQ(deserialized.pdu.simulation, goose_frame.pdu.simulation);
|
||||
ASSERT_EQ(deserialized.pdu.conf_rev, goose_frame.pdu.conf_rev);
|
||||
ASSERT_EQ(deserialized.pdu.ndsCom, goose_frame.pdu.ndsCom);
|
||||
ASSERT_EQ(deserialized.pdu.apdu_entries.size(), 2);
|
||||
|
||||
ASSERT_EQ(deserialized.pdu.apdu_entries[0].tag, 0x86);
|
||||
ASSERT_EQ(deserialized.pdu.apdu_entries[0].value.size(), 2);
|
||||
ASSERT_EQ(deserialized.pdu.apdu_entries[0].value[0], 0);
|
||||
ASSERT_EQ(deserialized.pdu.apdu_entries[0].value[1], 1);
|
||||
|
||||
ASSERT_EQ(deserialized.pdu.apdu_entries[1].tag, 0x87);
|
||||
ASSERT_EQ(deserialized.pdu.apdu_entries[1].value.size(), 4);
|
||||
ASSERT_EQ(deserialized.pdu.apdu_entries[1].value[0], 0xde);
|
||||
ASSERT_EQ(deserialized.pdu.apdu_entries[1].value[1], 0xad);
|
||||
ASSERT_EQ(deserialized.pdu.apdu_entries[1].value[2], 0xbe);
|
||||
ASSERT_EQ(deserialized.pdu.apdu_entries[1].value[3], 0xef);
|
||||
}
|
||||
|
||||
TEST(SecureGooseFrame, deserialize_different_hmac_key) {
|
||||
goose::frame::SecureGooseFrame goose_frame;
|
||||
goose_frame.appid[0] = 0x00;
|
||||
goose_frame.appid[1] = 0x01;
|
||||
memset(goose_frame.source_mac_address, 0x00, 6);
|
||||
memset(goose_frame.destination_mac_address, 0x00, 6);
|
||||
|
||||
{
|
||||
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};
|
||||
goose_frame.pdu = pdu;
|
||||
}
|
||||
|
||||
std::uint8_t key[48] = {0};
|
||||
for (size_t i = 0; i < sizeof(key); i++) {
|
||||
key[i] = i;
|
||||
}
|
||||
|
||||
auto serialized = goose_frame.serialize(std::vector<std::uint8_t>(key, key + sizeof(key)));
|
||||
|
||||
serialized.payload[serialized.payload.size() - 1]++;
|
||||
ASSERT_THROW(goose::frame::SecureGooseFrame frame(serialized, std::vector<std::uint8_t>(key, key + sizeof(key))),
|
||||
std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(SecureGooseFrame, deserialize_edited_payload_throws_invalid_hmac) {
|
||||
goose::frame::SecureGooseFrame goose_frame;
|
||||
goose_frame.appid[0] = 0x00;
|
||||
goose_frame.appid[1] = 0x01;
|
||||
memset(goose_frame.source_mac_address, 0x00, 6);
|
||||
memset(goose_frame.destination_mac_address, 0x00, 6);
|
||||
|
||||
{
|
||||
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};
|
||||
goose_frame.pdu = pdu;
|
||||
}
|
||||
|
||||
std::uint8_t key[48] = {0};
|
||||
for (size_t i = 0; i < sizeof(key); i++) {
|
||||
key[i] = i;
|
||||
}
|
||||
|
||||
auto serialized = goose_frame.serialize(std::vector<std::uint8_t>(key, key + sizeof(key)));
|
||||
|
||||
serialized.payload[65]++; // edit payload
|
||||
|
||||
ASSERT_THROW(goose::frame::SecureGooseFrame frame(serialized, std::vector<std::uint8_t>(key, key + sizeof(key))),
|
||||
std::runtime_error);
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <algorithm>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <goose/sender.hpp>
|
||||
|
||||
class DummyEthernetInterface : public goose_ethernet::EthernetInterfaceIntf {
|
||||
std::function<void(const std::uint8_t*, size_t)> send_callback;
|
||||
std::function<std::vector<std::uint8_t>()> receive_callback;
|
||||
|
||||
public:
|
||||
DummyEthernetInterface(std::function<void(const std::uint8_t*, size_t)> send_callback,
|
||||
std::function<std::vector<std::uint8_t>()> receive_callback) :
|
||||
send_callback(send_callback), receive_callback(receive_callback) {
|
||||
}
|
||||
|
||||
void send_packet_raw(const std::uint8_t* packet, size_t size) override {
|
||||
send_callback(packet, size);
|
||||
}
|
||||
|
||||
std::optional<std::vector<std::uint8_t>> receive_packet_raw() override {
|
||||
return receive_callback();
|
||||
}
|
||||
|
||||
// Dummy implementation
|
||||
const std::uint8_t* get_mac_address() const override {
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(DummyEthernetInterface, callback_works) {
|
||||
std::uint8_t send_counter = 0;
|
||||
std::uint8_t receive_counter = 0;
|
||||
|
||||
auto dummy_frame = goose_ethernet::EthernetFrame(std::vector<std::uint8_t>(60));
|
||||
|
||||
DummyEthernetInterface intf(
|
||||
[&](const std::uint8_t* packet, size_t size) {
|
||||
ASSERT_EQ(dummy_frame.serialize().size(), size);
|
||||
send_counter++;
|
||||
},
|
||||
[&]() -> std::vector<std::uint8_t> {
|
||||
receive_counter++;
|
||||
return dummy_frame.serialize();
|
||||
});
|
||||
|
||||
intf.send_packet(dummy_frame);
|
||||
EXPECT_EQ(send_counter, 1);
|
||||
|
||||
auto received = intf.receive_packet();
|
||||
if (!received.has_value()) {
|
||||
FAIL() << "Received frame is empty";
|
||||
}
|
||||
auto recv = received.value();
|
||||
EXPECT_EQ(receive_counter, 1);
|
||||
EXPECT_EQ(recv.serialize(), dummy_frame.serialize());
|
||||
}
|
||||
|
||||
TEST(Sender, single_frame_goes_through_all_delays) {
|
||||
std::vector<std::chrono::milliseconds> received_times;
|
||||
std::vector<std::uint16_t> received_st_nums;
|
||||
std::vector<std::uint16_t> received_sq_nums;
|
||||
|
||||
auto intf = std::make_shared<DummyEthernetInterface>(
|
||||
[&](const std::uint8_t* data, size_t size) {
|
||||
received_times.push_back(std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()));
|
||||
|
||||
auto frame = goose::frame::GooseFrame(goose_ethernet::EthernetFrame(data, size));
|
||||
|
||||
received_sq_nums.push_back(frame.pdu.sq_num);
|
||||
received_st_nums.push_back(frame.pdu.st_num);
|
||||
},
|
||||
[]() -> std::vector<std::uint8_t> { return {}; });
|
||||
|
||||
goose::sender::Sender sender(
|
||||
std::chrono::milliseconds(10),
|
||||
{std::chrono::milliseconds(1), std::chrono::milliseconds(2), std::chrono::milliseconds(4)}, intf);
|
||||
|
||||
sender.start();
|
||||
|
||||
// Nothing should have been sent yet
|
||||
ASSERT_EQ(received_times.size(), 0);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
ASSERT_EQ(received_times.size(), 0);
|
||||
|
||||
goose::frame::GooseFrame frame;
|
||||
frame.appid[0] = 0x01;
|
||||
frame.appid[1] = 0x02;
|
||||
strcpy(frame.pdu.go_cb_ref, "goose_cb_ref");
|
||||
strcpy(frame.pdu.dat_set, "dataset");
|
||||
strcpy(frame.pdu.go_id, "goose_id");
|
||||
frame.pdu.timestamp = goose::frame::GooseTimestamp::now();
|
||||
// Random data, should be overwritten by sender
|
||||
frame.pdu.sq_num = 0xdead;
|
||||
frame.pdu.st_num = 0xbeef;
|
||||
|
||||
sender.send(new goose::sender::SendPacketNormal(frame));
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
// this should be about one cycle of all Ts, at least 4 packets should have
|
||||
// been sent by now though no more than 5
|
||||
|
||||
ASSERT_GE(received_times.size(), 4);
|
||||
ASSERT_LE(received_times.size(), 5);
|
||||
|
||||
// first delay should be about 1ms (+-1ms due to linux not
|
||||
// being real-time)s
|
||||
ASSERT_LE((received_times[1] - received_times[0]).count(), 2);
|
||||
|
||||
// second delay should be about 2ms (+-1ms)
|
||||
ASSERT_NEAR((received_times[2] - received_times[1]).count(), 2, 1);
|
||||
|
||||
// third delay should be about 4ms (+-2ms)
|
||||
ASSERT_NEAR((received_times[3] - received_times[2]).count(), 4, 2);
|
||||
|
||||
// fourth delay should be about 10ms (+-4ms) (if a fourth packet was sent)
|
||||
if (received_times.size() >= 5) {
|
||||
ASSERT_NEAR((received_times[4] - received_times[3]).count(), 10, 4);
|
||||
}
|
||||
|
||||
// all st nums should be 1
|
||||
for (auto st_num : received_st_nums) {
|
||||
ASSERT_EQ(st_num, 1);
|
||||
}
|
||||
|
||||
sender.stop();
|
||||
}
|
||||
|
||||
TEST(Sender, multiple_frames_go_through_all_delays) {
|
||||
std::vector<std::chrono::milliseconds> received_times;
|
||||
std::vector<std::uint16_t> received_st_nums;
|
||||
std::vector<std::uint16_t> received_sq_nums;
|
||||
|
||||
auto intf = std::make_shared<DummyEthernetInterface>(
|
||||
[&](const std::uint8_t* data, size_t size) {
|
||||
received_times.push_back(std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()));
|
||||
|
||||
auto frame = goose::frame::GooseFrame(goose_ethernet::EthernetFrame(data, size));
|
||||
|
||||
received_sq_nums.push_back(frame.pdu.sq_num);
|
||||
received_st_nums.push_back(frame.pdu.st_num);
|
||||
},
|
||||
[]() -> std::vector<std::uint8_t> { return {}; });
|
||||
|
||||
auto t0 = std::chrono::milliseconds(10);
|
||||
auto ts = std::vector<std::chrono::milliseconds>{std::chrono::milliseconds(1), std::chrono::milliseconds(2),
|
||||
std::chrono::milliseconds(4)};
|
||||
|
||||
goose::sender::Sender sender(t0, ts, intf);
|
||||
|
||||
sender.start();
|
||||
|
||||
// Nothing should have been sent yet
|
||||
ASSERT_EQ(received_times.size(), 0);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
ASSERT_EQ(received_times.size(), 0);
|
||||
|
||||
goose::frame::GooseFrame frame;
|
||||
frame.appid[0] = 0x01;
|
||||
frame.appid[1] = 0x02;
|
||||
strcpy(frame.pdu.go_cb_ref, "goose_cb_ref");
|
||||
strcpy(frame.pdu.dat_set, "dataset");
|
||||
strcpy(frame.pdu.go_id, "goose_id");
|
||||
frame.pdu.timestamp = goose::frame::GooseTimestamp::now();
|
||||
// Random data, should be overwritten by sender
|
||||
frame.pdu.sq_num = 0xdead;
|
||||
frame.pdu.st_num = 0xbeef;
|
||||
|
||||
// send multiple frames
|
||||
for (int i = 0; i < 5; i++) {
|
||||
sender.send(new goose::sender::SendPacketNormal(frame));
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
|
||||
sender.stop();
|
||||
|
||||
auto snapshot_times = received_times;
|
||||
auto snapshot_st_nums = received_st_nums;
|
||||
auto snapshot_sq_nums = received_sq_nums;
|
||||
|
||||
// sometimes the snapshot_{times,st_nums,sq_nums} are not equal size; thus
|
||||
// truncate the longest
|
||||
auto min_size = std::min({snapshot_times.size(), snapshot_st_nums.size(), snapshot_sq_nums.size()});
|
||||
snapshot_times.resize(min_size);
|
||||
snapshot_st_nums.resize(min_size);
|
||||
snapshot_sq_nums.resize(min_size);
|
||||
|
||||
// Assert the predicate that the frames delay increases 3 times, then stays
|
||||
// about the same and then st num increases and the frame delay resets and
|
||||
// increases again, while the sq num increases by 1 each time and resets upon
|
||||
// st num increase
|
||||
std::chrono::milliseconds last_delay = std::chrono::milliseconds(0);
|
||||
std::uint16_t last_st_num = 1; // note that the first st num is 1
|
||||
std::uint16_t last_sq_num = 0;
|
||||
size_t i_s_since_last_st_num_change = 0;
|
||||
size_t st_increase_count = 0;
|
||||
|
||||
for (size_t i = 1; i < snapshot_times.size(); i++) {
|
||||
auto delay = snapshot_times[i] - snapshot_times[i - 1];
|
||||
|
||||
// st num should only be equal or increase by 1
|
||||
ASSERT_GE(snapshot_st_nums[i], last_st_num);
|
||||
ASSERT_LE(snapshot_st_nums[i], last_st_num + 1);
|
||||
|
||||
if (snapshot_st_nums[i] != last_st_num) {
|
||||
st_increase_count++;
|
||||
|
||||
// if st num increased, sq num should be 0
|
||||
ASSERT_EQ(snapshot_sq_nums[i], 0);
|
||||
|
||||
// delay here is unknown because a message was sent which can be at a
|
||||
// random point in time. Though it should not be greater than 20ms
|
||||
ASSERT_LE(delay.count(), 20);
|
||||
} else {
|
||||
// sq num must always get bigger (not in st num change but thats handled
|
||||
// above)
|
||||
ASSERT_EQ(snapshot_sq_nums[i], last_sq_num + 1);
|
||||
|
||||
// for the first few frames the delay should be near the set delays (ts)
|
||||
// (+- 3ms)
|
||||
if (i_s_since_last_st_num_change <= 4) {
|
||||
// if i_s_since_last_st_num_change is 0 then the last_delay is weird
|
||||
// because a message was sent there
|
||||
|
||||
if (i_s_since_last_st_num_change != 0) {
|
||||
// The delay should be near set delays (ts) (+- 3ms)
|
||||
ASSERT_NEAR(delay.count(), ts[i_s_since_last_st_num_change - 1].count(), 3);
|
||||
}
|
||||
} else {
|
||||
// delay should be about t0 (+- 5ms)
|
||||
ASSERT_NEAR(delay.count(), t0.count(), 5);
|
||||
}
|
||||
}
|
||||
|
||||
last_st_num = snapshot_st_nums[i];
|
||||
last_sq_num = snapshot_sq_nums[i];
|
||||
last_delay = delay;
|
||||
}
|
||||
|
||||
ASSERT_EQ(st_increase_count,
|
||||
4); // this should be 4 because we send 5 frames, the first
|
||||
// one with st num 1, which is not counted (see the initial
|
||||
// "last_st_num = 1" above)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.vscode/settings.json
|
||||
|
||||
build/
|
||||
.venv/
|
||||
.cache/
|
||||
@@ -0,0 +1,2 @@
|
||||
add_subdirectory(libs)
|
||||
add_subdirectory(examples)
|
||||
@@ -0,0 +1,17 @@
|
||||
# Huawei Fusion Charger Driver
|
||||
|
||||
## Build and test
|
||||
|
||||
This library is built and tested as part of the build process of EVerest.
|
||||
|
||||
## Run real_hw_first_test on fricklydevnuc3
|
||||
|
||||
```bash
|
||||
cd build/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/examples
|
||||
sudo ./examples/real_hw_first_test 192.168.11.1 502 enp86s0
|
||||
```
|
||||
|
||||
## Libs
|
||||
|
||||
- `fusion_charger_modbus_extensions` modbus extension for fusion charger (primarily unsolicitated reports)
|
||||
- `fusion_charger_modbus_driver` modbus driver stuff for fusion charger
|
||||
@@ -0,0 +1,5 @@
|
||||
add_executable(unsolicitated_decoder unsolicitated_decoder.cpp)
|
||||
target_link_libraries(unsolicitated_decoder fusion_charger_modbus_extensions)
|
||||
|
||||
add_executable(real_hw_first_test real_hw_first_test.cpp)
|
||||
target_link_libraries(real_hw_first_test fusion_charger_modbus_driver fusion_charger_goose_driver)
|
||||
@@ -0,0 +1,260 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <fusion_charger/goose/power_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/power_unit.hpp>
|
||||
#include <fusion_charger/modbus/registers/raw.hpp>
|
||||
#include <goose-ethernet/driver.hpp>
|
||||
#include <modbus-server/modbus_basic_server.hpp>
|
||||
#include <thread>
|
||||
|
||||
using namespace fusion_charger::modbus_driver::raw_registers;
|
||||
using namespace fusion_charger::modbus_driver;
|
||||
using namespace fusion_charger::modbus_extensions;
|
||||
|
||||
#define DEFAULT_IP "192.168.11.1"
|
||||
#define DEFAULT_PORT 502
|
||||
#define DEFAULT_INTERFACE "eth0"
|
||||
|
||||
const int do_connect(const char* ip, std::uint16_t port) {
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock < 0) {
|
||||
printf("Could not open ");
|
||||
perror("socket");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
struct sockaddr_in addr;
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = inet_addr(ip);
|
||||
|
||||
printf("Connecting to %s:%d\n", ip, ntohs(addr.sin_port));
|
||||
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
fprintf(stderr, "Could not ");
|
||||
perror("connect");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
printf("Connected\n");
|
||||
|
||||
return sock;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
const char* ip = DEFAULT_IP;
|
||||
std::uint16_t port = DEFAULT_PORT;
|
||||
const char* intf = DEFAULT_INTERFACE;
|
||||
|
||||
if (argc != 4) {
|
||||
printf("Assuming default IP: %s and port %u with intf %s\n", DEFAULT_IP, DEFAULT_PORT, DEFAULT_INTERFACE);
|
||||
printf("To use another ip and intf use: %s <ip> <port> <intf>\n", argv[0]);
|
||||
} else {
|
||||
ip = argv[1];
|
||||
port = strtol(argv[2], nullptr, 10);
|
||||
intf = argv[3];
|
||||
}
|
||||
|
||||
printf("Using IP, port and interface: %s:%u@%s\n", ip, port, intf);
|
||||
|
||||
goose_ethernet::EthernetInterface eth(intf);
|
||||
int sock = do_connect(ip, port);
|
||||
|
||||
auto transport = std::make_shared<modbus_server::ModbusSocketTransport>(sock);
|
||||
auto protocol = std::make_shared<modbus_server::ModbusTCPProtocol>(transport);
|
||||
auto pcl = std::make_shared<modbus_server::PDUCorrelationLayer>(protocol);
|
||||
UnsolicitatedReportBasicServer server(pcl);
|
||||
|
||||
PowerUnitRegisters psu_registers;
|
||||
DispenserRegistersConfig dispenser_registers_config;
|
||||
dispenser_registers_config.esn = "1234567890";
|
||||
dispenser_registers_config.connector_count = 1;
|
||||
DispenserRegisters dispenser_registers(dispenser_registers_config);
|
||||
ConnectorRegistersConfig connector_register_config;
|
||||
std::copy(eth.get_mac_address(), eth.get_mac_address() + 6, std::begin(connector_register_config.mac_address));
|
||||
connector_register_config.type = ConnectorType::CCS1;
|
||||
connector_register_config.global_connector_no = 1;
|
||||
connector_register_config.connector_number = 1;
|
||||
connector_register_config.max_rated_charge_current = 100.0;
|
||||
connector_register_config.rated_output_power_connector = 10000.0;
|
||||
connector_register_config.get_contactor_upstream_voltage = []() { return 0.0; };
|
||||
connector_register_config.get_output_voltage = []() { return 0.0; };
|
||||
connector_register_config.get_output_current = []() { return 0.0; };
|
||||
ConnectorRegisters connector_registers(connector_register_config);
|
||||
// Callbacks for common power unit registers
|
||||
psu_registers.manufacturer.add_write_callback(
|
||||
[](std::uint16_t value) { printf("PSU Manufacturer changed to %d\n", value); });
|
||||
psu_registers.protocol_version.add_write_callback(
|
||||
[](std::uint16_t value) { printf("PSU Protocol version changed to %d\n", value); });
|
||||
psu_registers.esn_control_board.add_write_callback(
|
||||
[](const std::string& value) { printf("PSU ESN Control Board changed to %s\n", value.c_str()); });
|
||||
psu_registers.software_version.add_write_callback(
|
||||
[](const std::string& value) { printf("PSU Software version changed to %s\n", value.c_str()); });
|
||||
psu_registers.hardware_version.add_write_callback(
|
||||
[](std::uint16_t val) { printf("PSU HW version changed to %d\n", val); });
|
||||
|
||||
psu_registers.psu_running_mode.add_write_callback([](SettingPowerUnitRegisters::PSURunningMode value) {
|
||||
printf("PSU Running mode changed to %s\n",
|
||||
SettingPowerUnitRegisters::psu_running_mode_to_string(value).c_str());
|
||||
});
|
||||
|
||||
connector_registers.hmac_key.add_write_callback([](const std::uint8_t* value) {
|
||||
printf("🎉🎉 HMAC key changed\n");
|
||||
printf("🎉🎉 New key: ");
|
||||
for (int i = 0; i < 48; i++) {
|
||||
printf("%02x", value[i]);
|
||||
}
|
||||
printf("\n");
|
||||
});
|
||||
|
||||
connector_registers.psu_port_available.add_write_callback(
|
||||
[](PsuOutputPortAvailability value) { printf("PSU port available changed to %d\n", (std::uint16_t)value); });
|
||||
|
||||
connector_registers.rated_output_power_psu.add_write_callback(
|
||||
[](float value) { printf("Rated output power PSU changed to %f\n", value); });
|
||||
|
||||
connector_registers.rated_output_power_connector.add_write_callback(
|
||||
[](float value) { printf("Rated output power connector changed to %f\n", value); });
|
||||
|
||||
UnsolicitatedRegistry register_registry;
|
||||
|
||||
dispenser_registers.add_to_registry(register_registry);
|
||||
psu_registers.add_to_registry(register_registry);
|
||||
connector_registers.add_to_registry(register_registry);
|
||||
|
||||
register_registry.verify_overlap();
|
||||
|
||||
// forward read and write
|
||||
server.set_read_holding_registers_request_cb(
|
||||
[®ister_registry](const modbus_server::pdu::ReadHoldingRegistersRequest& req) {
|
||||
auto data = register_registry.on_read(req.register_start, req.register_count);
|
||||
return modbus_server::pdu::ReadHoldingRegistersResponse(req, data);
|
||||
});
|
||||
server.set_write_multiple_registers_request_cb(
|
||||
[®ister_registry](const modbus_server::pdu::WriteMultipleRegistersRequest& req) {
|
||||
register_registry.on_write(req.register_start, req.register_data);
|
||||
return modbus_server::pdu::WriteMultipleRegistersResponse(req);
|
||||
});
|
||||
server.set_write_single_register_request_cb(
|
||||
[®ister_registry](const modbus_server::pdu::WriteSingleRegisterRequest& req) {
|
||||
register_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);
|
||||
});
|
||||
|
||||
printf("Serving\n");
|
||||
|
||||
bool closed = false;
|
||||
|
||||
auto unsolicitated_reporter = std::thread([&server, ®ister_registry, &closed]() {
|
||||
printf("Unsolicitated reporter thread started\n");
|
||||
while (true) {
|
||||
try {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
|
||||
if (closed) {
|
||||
printf("Unsolicitated reporter thread exiting\n");
|
||||
return;
|
||||
}
|
||||
|
||||
auto req = register_registry.unsolicitated_report();
|
||||
if (req.has_value()) {
|
||||
server.send_unsolicitated_report(req.value(), std::chrono::seconds(3));
|
||||
}
|
||||
} catch (modbus_server::transport_exceptions::ConnectionClosedException& e) {
|
||||
printf("Unsolicitated reporter noticed an closed connection; "
|
||||
"exiting...\n");
|
||||
closed = true;
|
||||
return;
|
||||
} catch (std::runtime_error& e) {
|
||||
printf("Unsolicitated reporter thread error: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
auto goose_thread = std::thread([ð, &closed, &psu_registers, &connector_registers]() {
|
||||
std::uint16_t stNum = 1;
|
||||
while (true) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
if (closed) {
|
||||
printf("Goose thread exiting\n");
|
||||
return;
|
||||
}
|
||||
|
||||
auto mac = psu_registers.psu_mac.get_value();
|
||||
if (mac[0] == 0 && mac[1] == 1 && mac[2] == 0 && mac[3] == 0) {
|
||||
// first 4 bytes are 0 -> mac not set
|
||||
continue;
|
||||
}
|
||||
|
||||
auto hmac = connector_registers.hmac_key.get_value();
|
||||
|
||||
if (hmac[0] == 0 && hmac[1] == 0 && hmac[2] == 0 && hmac[3] == 0) {
|
||||
// first 4 bytes are 0 -> hmac not set
|
||||
continue;
|
||||
}
|
||||
|
||||
fusion_charger::goose::PowerRequirementRequest report_pdu;
|
||||
report_pdu.charging_connector_no = 1;
|
||||
report_pdu.charging_sn = 0xffff;
|
||||
report_pdu.requirement_type = fusion_charger::goose::RequirementType::Charging;
|
||||
report_pdu.mode = fusion_charger::goose::Mode::ConstantCurrent;
|
||||
report_pdu.voltage = 400;
|
||||
report_pdu.current = 10;
|
||||
|
||||
goose::frame::SecureGooseFrame frame;
|
||||
memcpy(frame.destination_mac_address, mac, 6);
|
||||
memcpy(frame.source_mac_address, eth.get_mac_address(), 6);
|
||||
frame.vlan_id = 0;
|
||||
frame.priority = 5;
|
||||
frame.appid[0] = 0;
|
||||
frame.appid[1] = 1;
|
||||
frame.pdu = report_pdu.to_pdu();
|
||||
frame.pdu.st_num = stNum++;
|
||||
|
||||
eth.send_packet(frame.serialize(std::vector<std::uint8_t>(hmac, hmac + 48)));
|
||||
printf("🚀 Sent goose frame\n");
|
||||
}
|
||||
});
|
||||
|
||||
auto poll_thread = std::thread([&pcl, &closed]() {
|
||||
try {
|
||||
while (true) {
|
||||
pcl->blocking_poll();
|
||||
if (closed) {
|
||||
printf("Poll thread exiting\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (modbus_server::transport_exceptions::ConnectionClosedException& e) {
|
||||
printf("Poll thread noticed an closed connection; "
|
||||
"exiting...\n");
|
||||
closed = true;
|
||||
}
|
||||
});
|
||||
|
||||
auto dummy_data_changer_thread = std::thread([&connector_registers]() {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(20));
|
||||
|
||||
connector_registers.connection_status.update_value(ConnectionStatus::FULL_CONNECTED);
|
||||
printf("👨💻 Changed connection status to full connected\n");
|
||||
|
||||
connector_registers.working_status.update_value(WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED);
|
||||
printf("👨💻 Update working status to standby with inserted "
|
||||
"charger\n");
|
||||
});
|
||||
|
||||
dummy_data_changer_thread.join();
|
||||
poll_thread.join();
|
||||
unsolicitated_reporter.join();
|
||||
goose_thread.join();
|
||||
|
||||
printf("Exiting\n");
|
||||
close(sock);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <fusion_charger/modbus/extensions/unsolicitated_report.hpp>
|
||||
#include <iostream>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) {
|
||||
printf("Usage: %s <data in hex>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string data = std::string(argv[1]);
|
||||
std::vector<std::uint8_t> data_vec;
|
||||
|
||||
for (size_t i = 0; i < data.length(); i += 2) {
|
||||
std::string byte_str = data.substr(i, 2);
|
||||
data_vec.push_back(std::stoi(byte_str, nullptr, 16));
|
||||
}
|
||||
|
||||
modbus_server::pdu::GenericPDU generic(0x41, data_vec);
|
||||
fusion_charger::modbus_extensions::UnsolicitatedReportRequest pdu;
|
||||
pdu.from_generic(generic);
|
||||
|
||||
printf("Decoded payload:\n");
|
||||
|
||||
for (const fusion_charger::modbus_extensions::UnsolicitatedReportRequest::Device& device : pdu.devices) {
|
||||
printf("Device location 0x%04x\n", device.location);
|
||||
for (const fusion_charger::modbus_extensions::UnsolicitatedReportRequest::Segment& segment : device.segments) {
|
||||
printf(" 0x%04x\n", segment.registers_start);
|
||||
printf(" Count: 0x%04x\n", segment.registers_count);
|
||||
printf(" Data (hex): ");
|
||||
for (size_t i = 0; i < segment.registers.size(); i += 2) {
|
||||
std::uint16_t reg = (segment.registers[i] << 8) | segment.registers[i + 1];
|
||||
printf("0x%04x ", reg);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
add_subdirectory(fusion_charger_modbus_extensions)
|
||||
add_subdirectory(fusion_charger_modbus_driver)
|
||||
add_subdirectory(fusion_charger_goose_driver)
|
||||
@@ -0,0 +1,14 @@
|
||||
file(GLOB SOURCES "src/*.cpp")
|
||||
|
||||
add_library(fusion_charger_goose_driver STATIC ${SOURCES})
|
||||
target_include_directories(fusion_charger_goose_driver PUBLIC include)
|
||||
target_link_libraries(fusion_charger_goose_driver PUBLIC goose)
|
||||
|
||||
if(FUSION_CHARGER_LIB_BUILD_TESTS)
|
||||
include(GoogleTest)
|
||||
|
||||
file(GLOB TEST_SOURCES "tests/*.cpp")
|
||||
add_executable(fusion_charger_goose_driver_test ${TEST_SOURCES})
|
||||
target_link_libraries(fusion_charger_goose_driver_test PRIVATE fusion_charger_goose_driver gtest_main)
|
||||
gtest_discover_tests(fusion_charger_goose_driver_test)
|
||||
endif()
|
||||
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <ctime>
|
||||
#include <goose/frame.hpp>
|
||||
|
||||
namespace fusion_charger {
|
||||
namespace goose {
|
||||
namespace utils {
|
||||
|
||||
::goose::frame::ber::BEREntry make_u16(std::uint16_t value);
|
||||
|
||||
::goose::frame::ber::BEREntry make_f32(float value);
|
||||
|
||||
std::uint16_t expect_u16(const ::goose::frame::ber::BEREntry& entry);
|
||||
|
||||
float expect_f32(const ::goose::frame::ber::BEREntry& entry);
|
||||
|
||||
}; // namespace utils
|
||||
}; // namespace goose
|
||||
}; // namespace fusion_charger
|
||||
@@ -0,0 +1,147 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <goose/frame.hpp>
|
||||
|
||||
#include "driver_utils.hpp"
|
||||
|
||||
namespace fusion_charger {
|
||||
namespace goose {
|
||||
|
||||
enum class RequirementType : std::uint16_t {
|
||||
ModulePlaceholderRequest = 0x01,
|
||||
InsulationDetectionVoltageOutput = 0x02,
|
||||
InsulationDetectionVoltageOutputStoppage = 0x03,
|
||||
PrechargeVoltageOutput = 0x04,
|
||||
Charging = 0x05,
|
||||
};
|
||||
|
||||
enum class Mode : std::uint16_t {
|
||||
None = 0x00,
|
||||
ConstantVoltage = 0x01,
|
||||
ConstantCurrent = 0x02,
|
||||
};
|
||||
|
||||
// todo: tests
|
||||
// note: more or less a factory
|
||||
struct PowerRequirementRequest {
|
||||
std::uint16_t charging_connector_no;
|
||||
std::uint16_t charging_sn = 0xffff;
|
||||
RequirementType requirement_type;
|
||||
Mode mode;
|
||||
float voltage;
|
||||
float current;
|
||||
|
||||
// todo: better timestamp stuff
|
||||
::goose::frame::GoosePDU
|
||||
to_pdu(::goose::frame::GooseTimestamp timestamp = ::goose::frame::GooseTimestamp::now()) const {
|
||||
::goose::frame::GoosePDU pdu;
|
||||
strcpy(pdu.go_cb_ref, "CC/0$GO$PowerRequest");
|
||||
pdu.time_allowed_to_live = 10000;
|
||||
strcpy(pdu.dat_set, "CC/0$GO$PowerRequest");
|
||||
strcpy(pdu.go_id, "CC/0$GO$PowerRequest");
|
||||
pdu.simulation = false;
|
||||
pdu.conf_rev = 1;
|
||||
pdu.ndsCom = false;
|
||||
pdu.timestamp = timestamp;
|
||||
pdu.apdu_entries.resize(8);
|
||||
pdu.apdu_entries[0] = utils::make_u16(charging_connector_no);
|
||||
pdu.apdu_entries[1] = utils::make_u16(charging_sn);
|
||||
pdu.apdu_entries[2] = utils::make_u16(static_cast<std::uint16_t>(requirement_type));
|
||||
pdu.apdu_entries[3] = utils::make_u16(static_cast<std::uint16_t>(mode));
|
||||
pdu.apdu_entries[4] = utils::make_f32(voltage);
|
||||
pdu.apdu_entries[5] = utils::make_f32(current);
|
||||
pdu.apdu_entries[6] = utils::make_u16(0xffff);
|
||||
pdu.apdu_entries[7] = utils::make_u16(0xffff);
|
||||
return pdu;
|
||||
}
|
||||
|
||||
// todo: test
|
||||
::goose::frame::GooseTimestamp from_pdu(const ::goose::frame::GoosePDU& input) {
|
||||
if (input.apdu_entries.size() != 8) {
|
||||
throw std::runtime_error("Expected 8 APDU entries, got " + std::to_string(input.apdu_entries.size()));
|
||||
}
|
||||
|
||||
if (strcmp(input.go_cb_ref, "CC/0$GO$PowerRequest") != 0) {
|
||||
throw std::runtime_error("Expected go_cb_ref CC/0$GO$PowerRequest, got " + std::string(input.go_cb_ref));
|
||||
}
|
||||
|
||||
charging_connector_no = utils::expect_u16(input.apdu_entries[0]);
|
||||
charging_sn = utils::expect_u16(input.apdu_entries[1]);
|
||||
requirement_type = static_cast<RequirementType>(utils::expect_u16(input.apdu_entries[2]));
|
||||
mode = static_cast<Mode>(utils::expect_u16(input.apdu_entries[3]));
|
||||
voltage = utils::expect_f32(input.apdu_entries[4]);
|
||||
current = utils::expect_f32(input.apdu_entries[5]);
|
||||
|
||||
return input.timestamp;
|
||||
}
|
||||
};
|
||||
|
||||
// todo: test
|
||||
struct PowerRequirementResponse {
|
||||
enum class Result : std::uint16_t {
|
||||
SUCCESS = 0,
|
||||
FAILURE = 1,
|
||||
};
|
||||
|
||||
std::uint16_t charging_connector_no;
|
||||
std::uint16_t charging_sn = 0xffff;
|
||||
RequirementType requirement_type;
|
||||
Mode mode;
|
||||
Result result;
|
||||
float voltage;
|
||||
float current;
|
||||
|
||||
// todo: better timestamp stuff
|
||||
::goose::frame::GoosePDU
|
||||
to_pdu(::goose::frame::GooseTimestamp timestamp = ::goose::frame::GooseTimestamp::now()) const {
|
||||
::goose::frame::GoosePDU pdu;
|
||||
strcpy(pdu.go_cb_ref, "CC/0$GO$PowerRequestReply");
|
||||
pdu.time_allowed_to_live = 10000;
|
||||
strcpy(pdu.dat_set, "CC/0$GO$PowerRequestReply");
|
||||
strcpy(pdu.go_id, "CC/0$GO$PowerRequestReply");
|
||||
pdu.simulation = false;
|
||||
pdu.conf_rev = 1;
|
||||
pdu.ndsCom = false;
|
||||
pdu.timestamp = timestamp;
|
||||
// DataSheet (Introduction to the communication ...)
|
||||
// says size of 8 entries, but 9 are given.
|
||||
// Setting size of 9
|
||||
pdu.apdu_entries.resize(9);
|
||||
pdu.apdu_entries[0] = utils::make_u16(charging_connector_no);
|
||||
pdu.apdu_entries[1] = utils::make_u16(charging_sn);
|
||||
pdu.apdu_entries[2] = utils::make_u16(static_cast<std::uint16_t>(requirement_type));
|
||||
pdu.apdu_entries[3] = utils::make_u16(static_cast<std::uint16_t>(result));
|
||||
pdu.apdu_entries[4] = utils::make_u16(static_cast<std::uint16_t>(mode));
|
||||
pdu.apdu_entries[5] = utils::make_f32(voltage);
|
||||
pdu.apdu_entries[6] = utils::make_f32(current);
|
||||
pdu.apdu_entries[7] = utils::make_u16(0xffff);
|
||||
pdu.apdu_entries[8] = utils::make_u16(0xffff);
|
||||
return pdu;
|
||||
}
|
||||
|
||||
::goose::frame::GooseTimestamp from_pdu(const ::goose::frame::GoosePDU& input) {
|
||||
if (input.apdu_entries.size() != 9) {
|
||||
throw std::runtime_error("Expected 9 APDU entries, got " + std::to_string(input.apdu_entries.size()));
|
||||
}
|
||||
|
||||
if (strcmp(input.go_cb_ref, "CC/0$GO$PowerRequestReply") != 0) {
|
||||
throw std::runtime_error("Expected go_cb_ref CC/0$GO$PowerRequestReply, got " +
|
||||
std::string(input.go_cb_ref));
|
||||
}
|
||||
|
||||
charging_connector_no = utils::expect_u16(input.apdu_entries[0]);
|
||||
charging_sn = utils::expect_u16(input.apdu_entries[1]);
|
||||
requirement_type = static_cast<RequirementType>(utils::expect_u16(input.apdu_entries[2]));
|
||||
result = static_cast<Result>(utils::expect_u16(input.apdu_entries[3]));
|
||||
mode = static_cast<Mode>(utils::expect_u16(input.apdu_entries[4]));
|
||||
voltage = utils::expect_f32(input.apdu_entries[5]);
|
||||
current = utils::expect_f32(input.apdu_entries[6]);
|
||||
|
||||
return input.timestamp;
|
||||
}
|
||||
};
|
||||
|
||||
}; // namespace goose
|
||||
}; // namespace fusion_charger
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <goose/frame.hpp>
|
||||
|
||||
#include "driver_utils.hpp"
|
||||
|
||||
namespace fusion_charger {
|
||||
namespace goose {
|
||||
|
||||
// note: more or less a factory
|
||||
struct StopChargeRequest {
|
||||
enum class Reason {
|
||||
// The charging is stopped normally.
|
||||
NORMAL = 0x1000,
|
||||
// The charging connector is disconnected. (During charging, the voltage at
|
||||
// detection point 1 is not 4 V.)
|
||||
CONNECTOR_DISCONNECTED = 0x1001,
|
||||
// The charging connector is not properly inserted.
|
||||
CONNECTOR_NOT_PROPERLY_INSERTED = 0x1002,
|
||||
// An insulation fault occurs.
|
||||
INSULATION_FAULT = 0x1003,
|
||||
EPO_FAULT = 0x1004,
|
||||
VEHICLE_CHARGER_NOT_MATCHING = 0x1005,
|
||||
OTHER_FAULT_ON_CHARGER = 0x1006,
|
||||
OTHER_FAULT_ON_VEHICLE = 0x1007,
|
||||
VEHICLE_BMS_NOT_CONNECTED = 0x1008,
|
||||
POWER_UNIT_CANNOT_BE_CHARGED = 0x1009,
|
||||
};
|
||||
|
||||
std::uint16_t charging_connector_no;
|
||||
std::uint16_t charging_sn = 0xffff;
|
||||
Reason reason;
|
||||
|
||||
::goose::frame::GoosePDU to_pdu(::goose::frame::GooseTimestamp time = ::goose::frame::GooseTimestamp::now()) const {
|
||||
::goose::frame::GoosePDU pdu;
|
||||
strcpy(pdu.go_cb_ref, "CC/0$GO$ShutdownRequest");
|
||||
pdu.time_allowed_to_live = 10000;
|
||||
strcpy(pdu.dat_set, "CC/0$GO$ShutdownRequest");
|
||||
strcpy(pdu.go_id, "CC/0$GO$ShutdownRequest");
|
||||
pdu.timestamp = time;
|
||||
pdu.conf_rev = 1;
|
||||
pdu.simulation = false;
|
||||
pdu.ndsCom = false;
|
||||
pdu.apdu_entries.resize(5);
|
||||
pdu.apdu_entries[0] = utils::make_u16(charging_connector_no);
|
||||
pdu.apdu_entries[1] = utils::make_u16(charging_sn);
|
||||
pdu.apdu_entries[2] = utils::make_u16(static_cast<std::uint16_t>(reason));
|
||||
pdu.apdu_entries[3] = utils::make_u16(0xffff);
|
||||
pdu.apdu_entries[4] = utils::make_u16(0xffff);
|
||||
return pdu;
|
||||
}
|
||||
|
||||
::goose::frame::GooseTimestamp from_pdu(const ::goose::frame::GoosePDU& input) {
|
||||
if (input.apdu_entries.size() < 5) {
|
||||
throw std::runtime_error("StopChargeRequest: input has too few entries");
|
||||
}
|
||||
|
||||
if (strcmp(input.go_cb_ref, "CC/0$GO$ShutdownRequest") != 0) {
|
||||
throw std::runtime_error("StopChargeRequest: expected go_cb_ref "
|
||||
"CC/0$GO$ShutdownRequest, got " +
|
||||
std::string(input.go_cb_ref));
|
||||
}
|
||||
|
||||
charging_connector_no = utils::expect_u16(input.apdu_entries[0]);
|
||||
charging_sn = utils::expect_u16(input.apdu_entries[1]);
|
||||
reason = static_cast<Reason>(utils::expect_u16(input.apdu_entries[2]));
|
||||
|
||||
return input.timestamp;
|
||||
}
|
||||
};
|
||||
|
||||
}; // namespace goose
|
||||
}; // namespace fusion_charger
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <ctime>
|
||||
#include <goose/frame.hpp>
|
||||
|
||||
namespace fusion_charger {
|
||||
namespace goose {
|
||||
namespace utils {
|
||||
|
||||
::goose::frame::ber::BEREntry make_u16(std::uint16_t value) {
|
||||
::goose::frame::ber::BEREntry entry;
|
||||
entry.tag = 0x86;
|
||||
entry.value = ::goose::frame::ber::encode_be(value);
|
||||
return entry;
|
||||
}
|
||||
|
||||
::goose::frame::ber::BEREntry make_f32(float value) {
|
||||
::goose::frame::ber::BEREntry entry;
|
||||
entry.tag = 0x87;
|
||||
entry.value = ::goose::frame::ber::encode_be(*(std::uint32_t*)&value);
|
||||
return entry;
|
||||
}
|
||||
|
||||
std::uint16_t expect_u16(const ::goose::frame::ber::BEREntry& entry) {
|
||||
if (entry.tag != 0x86) {
|
||||
throw std::runtime_error("Expected tag 0x86, got " + std::to_string(entry.tag));
|
||||
}
|
||||
return ::goose::frame::ber::decode_be<std::uint16_t>(entry.value);
|
||||
}
|
||||
|
||||
float expect_f32(const ::goose::frame::ber::BEREntry& entry) {
|
||||
if (entry.tag != 0x87) {
|
||||
throw std::runtime_error("Expected tag 0x87, got " + std::to_string(entry.tag));
|
||||
}
|
||||
// todo: verify
|
||||
std::uint32_t val_u32 = ::goose::frame::ber::decode_be<std::uint32_t>(entry.value);
|
||||
return *reinterpret_cast<float*>(&val_u32);
|
||||
}
|
||||
|
||||
}; // namespace utils
|
||||
}; // namespace goose
|
||||
}; // namespace fusion_charger
|
||||
@@ -0,0 +1,156 @@
|
||||
// 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 <fusion_charger/goose/stop_charge_request.hpp>
|
||||
#include <thread>
|
||||
|
||||
TEST(PowerRequirementRequest, from_pdu_real_world_test_1) {
|
||||
const char raw_data[] = "618197801543432f3024474f24506f77657252657175657374008102"
|
||||
"2710821543432f3024474f24506f7765725265717565737400831543432f3024474f2450"
|
||||
"6f776572526571756573740084086361bd060000000a8504000000018604000000008701"
|
||||
"008804000000008901008a0400000008ab24860200018602ffff86020005860200008704"
|
||||
"4479c000870440a000008602ffff8602ffff";
|
||||
std::uint8_t data[sizeof(raw_data) / 2];
|
||||
for (size_t i = 0; i < sizeof(data); ++i) {
|
||||
sscanf(&raw_data[2 * i], "%2hhx", &data[i]);
|
||||
}
|
||||
|
||||
::goose::frame::GoosePDU pdu(std::vector<std::uint8_t>(data, data + sizeof(data)));
|
||||
fusion_charger::goose::PowerRequirementRequest request;
|
||||
ASSERT_NO_THROW(request.from_pdu(pdu));
|
||||
|
||||
EXPECT_EQ(request.charging_connector_no, 1);
|
||||
EXPECT_EQ(request.charging_sn, 0xffff);
|
||||
EXPECT_EQ(request.requirement_type, fusion_charger::goose::RequirementType::Charging);
|
||||
EXPECT_EQ(request.mode, fusion_charger::goose::Mode::None);
|
||||
EXPECT_FLOAT_EQ(request.voltage, 999.0f);
|
||||
EXPECT_FLOAT_EQ(request.current, 5.0f);
|
||||
}
|
||||
|
||||
TEST(PowerRequirementRequest, from_pdu_real_world_test_2) {
|
||||
const char raw_data[] = "618197801543432f3024474f24506f77657252657175657374008102"
|
||||
"2710821543432f3024474f24506f7765725265717565737400831543432f3024474f2450"
|
||||
"6f776572526571756573740084086361bd1f0000000a8504000000018604000000008701"
|
||||
"008804000000008901008a0400000008ab24860200018602ffff86020005860200008704"
|
||||
"439a80008704424800008602ffff8602ffff";
|
||||
std::uint8_t data[sizeof(raw_data) / 2];
|
||||
for (size_t i = 0; i < sizeof(data); ++i) {
|
||||
sscanf(&raw_data[2 * i], "%2hhx", &data[i]);
|
||||
}
|
||||
|
||||
::goose::frame::GoosePDU pdu(std::vector<std::uint8_t>(data, data + sizeof(data)));
|
||||
fusion_charger::goose::PowerRequirementRequest request;
|
||||
ASSERT_NO_THROW(request.from_pdu(pdu));
|
||||
|
||||
EXPECT_EQ(request.charging_connector_no, 1);
|
||||
EXPECT_EQ(request.charging_sn, 0xffff);
|
||||
EXPECT_EQ(request.requirement_type, fusion_charger::goose::RequirementType::Charging);
|
||||
EXPECT_EQ(request.mode, fusion_charger::goose::Mode::None);
|
||||
EXPECT_FLOAT_EQ(request.voltage, 309.0f);
|
||||
EXPECT_FLOAT_EQ(request.current, 50.0f);
|
||||
}
|
||||
|
||||
TEST(PowerRequirementRequest, to_pdu_positive_test) {
|
||||
fusion_charger::goose::PowerRequirementRequest request;
|
||||
|
||||
request.charging_connector_no = 0xdead;
|
||||
request.charging_sn = 0xbeef;
|
||||
request.requirement_type = fusion_charger::goose::RequirementType::Charging;
|
||||
request.mode = fusion_charger::goose::Mode::ConstantVoltage;
|
||||
request.voltage = 123.456f;
|
||||
request.current = 789.012f;
|
||||
|
||||
auto time = ::goose::frame::GooseTimestamp::now();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
|
||||
::goose::frame::GoosePDU pdu = request.to_pdu(time);
|
||||
ASSERT_STREQ(pdu.go_cb_ref, "CC/0$GO$PowerRequest");
|
||||
EXPECT_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");
|
||||
EXPECT_FALSE(pdu.simulation);
|
||||
EXPECT_EQ(pdu.conf_rev, 1);
|
||||
EXPECT_FALSE(pdu.ndsCom);
|
||||
EXPECT_EQ(pdu.timestamp, time);
|
||||
ASSERT_EQ(pdu.apdu_entries.size(), 8);
|
||||
EXPECT_EQ(pdu.apdu_entries[0].tag, 0x86);
|
||||
EXPECT_EQ(pdu.apdu_entries[0].value.size(), 2);
|
||||
EXPECT_EQ(pdu.apdu_entries[0].value[0], 0xde);
|
||||
EXPECT_EQ(pdu.apdu_entries[0].value[1], 0xad);
|
||||
EXPECT_EQ(pdu.apdu_entries[1].tag, 0x86);
|
||||
EXPECT_EQ(pdu.apdu_entries[1].value.size(), 2);
|
||||
EXPECT_EQ(pdu.apdu_entries[1].value[0], 0xbe);
|
||||
EXPECT_EQ(pdu.apdu_entries[1].value[1], 0xef);
|
||||
EXPECT_EQ(pdu.apdu_entries[2].tag, 0x86);
|
||||
EXPECT_EQ(pdu.apdu_entries[2].value.size(), 2);
|
||||
EXPECT_EQ(pdu.apdu_entries[2].value[0], 0x00);
|
||||
EXPECT_EQ(pdu.apdu_entries[2].value[1], 0x05);
|
||||
EXPECT_EQ(pdu.apdu_entries[3].tag, 0x86);
|
||||
EXPECT_EQ(pdu.apdu_entries[3].value.size(), 2);
|
||||
EXPECT_EQ(pdu.apdu_entries[3].value[0], 0x00);
|
||||
EXPECT_EQ(pdu.apdu_entries[3].value[1], 0x01);
|
||||
EXPECT_EQ(pdu.apdu_entries[4].tag, 0x87);
|
||||
EXPECT_EQ(pdu.apdu_entries[4].value.size(), 4);
|
||||
EXPECT_EQ(pdu.apdu_entries[4].value[0], 0x42);
|
||||
EXPECT_EQ(pdu.apdu_entries[4].value[1], 0xf6);
|
||||
EXPECT_EQ(pdu.apdu_entries[4].value[2], 0xe9);
|
||||
EXPECT_EQ(pdu.apdu_entries[4].value[3], 0x79);
|
||||
EXPECT_EQ(pdu.apdu_entries[5].tag, 0x87);
|
||||
EXPECT_EQ(pdu.apdu_entries[5].value.size(), 4);
|
||||
EXPECT_EQ(pdu.apdu_entries[5].value[0], 0x44);
|
||||
EXPECT_EQ(pdu.apdu_entries[5].value[1], 0x45);
|
||||
EXPECT_EQ(pdu.apdu_entries[5].value[2], 0x40);
|
||||
EXPECT_EQ(pdu.apdu_entries[5].value[3], 0xc5);
|
||||
EXPECT_EQ(pdu.apdu_entries[6].tag, 0x86);
|
||||
EXPECT_EQ(pdu.apdu_entries[6].value.size(), 2);
|
||||
EXPECT_EQ(pdu.apdu_entries[6].value[0], 0xff);
|
||||
EXPECT_EQ(pdu.apdu_entries[6].value[1], 0xff);
|
||||
EXPECT_EQ(pdu.apdu_entries[7].tag, 0x86);
|
||||
EXPECT_EQ(pdu.apdu_entries[7].value.size(), 2);
|
||||
EXPECT_EQ(pdu.apdu_entries[7].value[0], 0xff);
|
||||
EXPECT_EQ(pdu.apdu_entries[7].value[1], 0xff);
|
||||
}
|
||||
|
||||
TEST(StopChargeRequest, to_pdu_positive_test) {
|
||||
fusion_charger::goose::StopChargeRequest request;
|
||||
request.charging_connector_no = 0xbeef;
|
||||
request.charging_sn = 0xdead;
|
||||
request.reason = fusion_charger::goose::StopChargeRequest::Reason::EPO_FAULT;
|
||||
|
||||
auto time = ::goose::frame::GooseTimestamp::now();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
|
||||
::goose::frame::GoosePDU pdu = request.to_pdu(time);
|
||||
ASSERT_STREQ(pdu.go_cb_ref, "CC/0$GO$ShutdownRequest");
|
||||
ASSERT_EQ(pdu.time_allowed_to_live, 10000);
|
||||
ASSERT_STREQ(pdu.dat_set, "CC/0$GO$ShutdownRequest");
|
||||
ASSERT_STREQ(pdu.go_id, "CC/0$GO$ShutdownRequest");
|
||||
ASSERT_FALSE(pdu.simulation);
|
||||
ASSERT_EQ(pdu.conf_rev, 1);
|
||||
ASSERT_EQ(pdu.ndsCom, false);
|
||||
ASSERT_EQ(pdu.timestamp, time);
|
||||
ASSERT_EQ(pdu.apdu_entries.size(), 5);
|
||||
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], 0xbe);
|
||||
ASSERT_EQ(pdu.apdu_entries[0].value[1], 0xef);
|
||||
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], 0xde);
|
||||
ASSERT_EQ(pdu.apdu_entries[1].value[1], 0xad);
|
||||
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], 0x10);
|
||||
ASSERT_EQ(pdu.apdu_entries[2].value[1], 0x04);
|
||||
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], 0xff);
|
||||
ASSERT_EQ(pdu.apdu_entries[3].value[1], 0xff);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].tag, 0x86);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value.size(), 2);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value[0], 0xff);
|
||||
ASSERT_EQ(pdu.apdu_entries[4].value[1], 0xff);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
file(GLOB SOURCES "src/*.cpp")
|
||||
|
||||
add_library(fusion_charger_modbus_driver STATIC ${SOURCES})
|
||||
target_include_directories(fusion_charger_modbus_driver PUBLIC include)
|
||||
target_link_libraries(fusion_charger_modbus_driver PUBLIC modbus-server fusion_charger_modbus_extensions)
|
||||
|
||||
if(FUSION_CHARGER_LIB_BUILD_TESTS)
|
||||
include(GoogleTest)
|
||||
|
||||
file(GLOB TEST_SOURCES "tests/*.cpp")
|
||||
add_executable(fusion_charger_modbus_driver_test ${TEST_SOURCES})
|
||||
target_link_libraries(fusion_charger_modbus_driver_test PRIVATE fusion_charger_modbus_driver gtest_main)
|
||||
gtest_discover_tests(fusion_charger_modbus_driver_test)
|
||||
endif()
|
||||
@@ -0,0 +1,158 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
#include "raw.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
namespace fusion_charger::modbus_driver {
|
||||
using namespace modbus::registers::data_providers;
|
||||
using namespace modbus_extensions;
|
||||
|
||||
typedef raw_registers::PsuOutputPortAvailability PsuOutputPortAvailability;
|
||||
|
||||
using ConnectorOffset = raw_registers::ConnectorOffset;
|
||||
|
||||
struct ConnectorRegistersConfig {
|
||||
using ConnectorType = raw_registers::ConnectorType;
|
||||
using ContactorStatus = raw_registers::CollectedConnectorRegisters::ContactorStatus;
|
||||
using ElectronicLockStatus = raw_registers::CollectedConnectorRegisters::ElectronicLockStatus;
|
||||
|
||||
std::uint8_t mac_address[6];
|
||||
ConnectorType type;
|
||||
std::uint16_t global_connector_no;
|
||||
std::uint16_t connector_number;
|
||||
float max_rated_charge_current;
|
||||
float rated_output_power_connector;
|
||||
std::function<float()> get_contactor_upstream_voltage;
|
||||
std::function<float()> get_output_voltage;
|
||||
std::function<float()> get_output_current;
|
||||
std::function<ContactorStatus()> get_contactor_status;
|
||||
std::function<ElectronicLockStatus()> get_electronic_lock_status;
|
||||
std::function<bool()> get_dc_output_contact_fault;
|
||||
};
|
||||
|
||||
struct ConnectorRegisters {
|
||||
using ConnectorType = raw_registers::ConnectorType;
|
||||
using WorkingStatus = raw_registers::WorkingStatus;
|
||||
using ConnectionStatus = raw_registers::ConnectionStatus;
|
||||
|
||||
using ContactorStatus = raw_registers::CollectedConnectorRegisters::ContactorStatus;
|
||||
using ElectronicLockStatus = raw_registers::CollectedConnectorRegisters::ElectronicLockStatus;
|
||||
using ChargingEventConnector = raw_registers::CollectedConnectorRegisters::ChargingEventConnector;
|
||||
|
||||
/// @brief Connector number on dispenser (1-4)
|
||||
std::uint16_t connector_number;
|
||||
|
||||
DataProviderHolding<double> total_energy_charged;
|
||||
DataProviderHolding<ConnectorType> connector_type;
|
||||
// Reg 0x1105
|
||||
DataProviderHolding<float> maximum_rated_charge_current;
|
||||
DataProviderCallbacks<float> output_voltage;
|
||||
DataProviderCallbacks<float> output_current;
|
||||
DataProviderHoldingUnsolicitatedReportCallback<WorkingStatus> working_status;
|
||||
DataProviderHoldingUnsolicitatedReportCallback<ConnectionStatus> connection_status;
|
||||
DataProviderHolding<std::uint16_t> connector_no; // 1-12
|
||||
DataProviderCallbacks<float> contactor_upstream_voltage;
|
||||
DataProviderMemoryHolding<6> mac_address;
|
||||
// "Status of contactors (DC+, DC-)" 0 off, 1 on
|
||||
DataProviderCallbacksUnsolicitated<ContactorStatus> contactor_status;
|
||||
// unlocked = 0, locked = 1
|
||||
DataProviderCallbacksUnsolicitated<ElectronicLockStatus> electronic_lock_status;
|
||||
DataProviderUnsolicitatedEvent<ChargingEventConnector> charging_event_connector; // todo ??
|
||||
|
||||
// from dispenser
|
||||
DataProviderHolding<float> max_rated_psu_voltage;
|
||||
DataProviderHolding<float> max_rated_psu_current;
|
||||
DataProviderHolding<float> min_rated_psu_voltage;
|
||||
DataProviderHolding<float> min_rated_psu_current;
|
||||
|
||||
DataProviderHolding<float> rated_output_power_connector;
|
||||
DataProviderMemoryHolding<48> hmac_key;
|
||||
DataProviderHolding<float> rated_output_power_psu;
|
||||
// written by dispenser
|
||||
DataProviderHolding<PsuOutputPortAvailability> psu_port_available;
|
||||
|
||||
// alarms
|
||||
DataProviderCallbacksUnsolicitated<std::uint16_t> dc_output_contact_fault;
|
||||
DataProviderHoldingUnsolicitatedReportCallback<std::uint16_t> inverse_connection_dispenser_inlet_cable;
|
||||
|
||||
/**
|
||||
* @param connector connecter number in dispenser 1-4
|
||||
* @param global_connector_no connector number in the whole system 1-12
|
||||
* @param type Connector type \c ConnectorType
|
||||
* @param max_charge_current 0x1105: The maximum rated charging current of the
|
||||
* connector
|
||||
* @param get_output_voltage callback to get current output voltage (seems to
|
||||
* be voltage near car)
|
||||
* @param get_output_current callback to get current output current
|
||||
* @param get_contactor_upstream_voltage callback to get upstream voltage
|
||||
* (seems to be voltage near charger)
|
||||
* @param mac_address MAC address of the dispenser
|
||||
* @param rated_output_power_connector
|
||||
*/
|
||||
ConnectorRegisters(ConnectorRegistersConfig config) :
|
||||
connector_number(config.connector_number),
|
||||
total_energy_charged(0),
|
||||
connector_type(config.type),
|
||||
maximum_rated_charge_current(config.max_rated_charge_current),
|
||||
output_voltage(config.get_output_voltage, utils::ignore_write<float>),
|
||||
output_current(config.get_output_current, utils::ignore_write<float>),
|
||||
working_status(WorkingStatus::STANDBY, utils::always_report),
|
||||
connection_status(ConnectionStatus::NOT_CONNECTED, utils::always_report),
|
||||
connector_no(config.global_connector_no),
|
||||
contactor_upstream_voltage(config.get_contactor_upstream_voltage, utils::ignore_write<float>),
|
||||
mac_address(config.mac_address),
|
||||
contactor_status(config.get_contactor_status, utils::ignore_write<ContactorStatus>, utils::always_report),
|
||||
electronic_lock_status(config.get_electronic_lock_status, utils::ignore_write<ElectronicLockStatus>,
|
||||
utils::always_report),
|
||||
charging_event_connector(ChargingEventConnector::START_TO_STOP),
|
||||
max_rated_psu_voltage(0),
|
||||
max_rated_psu_current(0),
|
||||
min_rated_psu_voltage(0),
|
||||
min_rated_psu_current(0),
|
||||
rated_output_power_connector(config.rated_output_power_connector),
|
||||
hmac_key(),
|
||||
rated_output_power_psu(0),
|
||||
psu_port_available(PsuOutputPortAvailability::NOT_AVAILABLE),
|
||||
dc_output_contact_fault(utils::wrap_alarm_register_func(config.get_dc_output_contact_fault),
|
||||
utils::ignore_write<std::uint16_t>, utils::always_report),
|
||||
inverse_connection_dispenser_inlet_cable(0, utils::always_report) {
|
||||
}
|
||||
|
||||
void add_to_registry(modbus::registers::registry::ComplexRegisterRegistry& registry) {
|
||||
ConnectorOffset offset =
|
||||
fusion_charger::modbus_driver::raw_registers::offset_from_connector_number(connector_number);
|
||||
|
||||
raw_registers::CollectedConnectorRegisters::DataProviders collected_connector_registers{
|
||||
total_energy_charged,
|
||||
connector_type,
|
||||
maximum_rated_charge_current,
|
||||
output_voltage,
|
||||
output_current,
|
||||
working_status,
|
||||
connection_status,
|
||||
connector_no,
|
||||
contactor_upstream_voltage,
|
||||
mac_address,
|
||||
contactor_status,
|
||||
electronic_lock_status,
|
||||
charging_event_connector,
|
||||
};
|
||||
|
||||
raw_registers::SettingConnectorRegisters::DataProviders setting_connector_registers{
|
||||
max_rated_psu_voltage, max_rated_psu_current, min_rated_psu_voltage,
|
||||
min_rated_psu_current, rated_output_power_connector, hmac_key,
|
||||
rated_output_power_psu, psu_port_available,
|
||||
};
|
||||
|
||||
raw_registers::AlarmConnectorRegisters::DataProviders alarm_connector_registers{
|
||||
dc_output_contact_fault, inverse_connection_dispenser_inlet_cable};
|
||||
|
||||
registry.add(
|
||||
std::make_unique<raw_registers::CollectedConnectorRegisters>(offset, collected_connector_registers));
|
||||
registry.add(std::make_unique<raw_registers::SettingConnectorRegisters>(offset, setting_connector_registers));
|
||||
registry.add(std::make_unique<raw_registers::AlarmConnectorRegisters>(offset, alarm_connector_registers));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace fusion_charger::modbus_driver
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
#include <ctime>
|
||||
|
||||
#include "raw.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
namespace fusion_charger::modbus_driver {
|
||||
using namespace modbus::registers::data_providers;
|
||||
using namespace modbus_extensions;
|
||||
|
||||
struct DispenserRegistersConfig {
|
||||
std::uint16_t manufacturer;
|
||||
std::uint16_t model;
|
||||
std::uint16_t protocol_version;
|
||||
std::uint16_t hardware_version;
|
||||
std::string software_version;
|
||||
std::string esn;
|
||||
std::uint32_t connector_count;
|
||||
std::function<bool()> get_door_status_alarm;
|
||||
std::function<bool()> get_water_alarm;
|
||||
std::function<bool()> get_epo_alarm;
|
||||
std::function<bool()> get_tilt_alarm;
|
||||
};
|
||||
|
||||
struct DispenserRegisters {
|
||||
DataProviderHolding<std::uint16_t> manufacturer;
|
||||
DataProviderHolding<std::uint16_t> model;
|
||||
DataProviderHolding<std::uint16_t> protocol_version;
|
||||
DataProviderHolding<std::uint16_t> hardware_version;
|
||||
DataProviderStringHolding<48> software_version;
|
||||
|
||||
DataProviderHolding<std::uint16_t> charging_connectors_count;
|
||||
DataProviderStringHolding<22> esn_dispenser;
|
||||
DataProviderCallbacksUnsolicitated<std::uint32_t> time_sync;
|
||||
DataProviderCallbacksUnsolicitated<std::uint16_t> door_status_alarm;
|
||||
DataProviderCallbacksUnsolicitated<std::uint16_t> water_alarm;
|
||||
DataProviderCallbacksUnsolicitated<std::uint16_t> epo_alarm;
|
||||
DataProviderCallbacksUnsolicitated<std::uint16_t> tilt_alarm;
|
||||
|
||||
DispenserRegisters(DispenserRegistersConfig config) :
|
||||
manufacturer(config.manufacturer),
|
||||
model(config.model),
|
||||
protocol_version(config.protocol_version),
|
||||
hardware_version(config.hardware_version),
|
||||
software_version(config.software_version.c_str()),
|
||||
|
||||
charging_connectors_count(config.connector_count),
|
||||
esn_dispenser(config.esn.c_str()),
|
||||
time_sync([]() { return std::time(NULL); }, utils::ignore_write<std::uint32_t>, utils::always_report),
|
||||
door_status_alarm(utils::wrap_alarm_register_func(config.get_door_status_alarm),
|
||||
utils::ignore_write<std::uint16_t>, utils::always_report),
|
||||
water_alarm(utils::wrap_alarm_register_func(config.get_water_alarm), utils::ignore_write<std::uint16_t>,
|
||||
utils::always_report),
|
||||
epo_alarm(utils::wrap_alarm_register_func(config.get_epo_alarm), utils::ignore_write<std::uint16_t>,
|
||||
utils::always_report),
|
||||
tilt_alarm(utils::wrap_alarm_register_func(config.get_tilt_alarm), utils::ignore_write<std::uint16_t>,
|
||||
utils::always_report) {
|
||||
}
|
||||
void add_to_registry(modbus::registers::registry::ComplexRegisterRegistry& registry) {
|
||||
raw_registers::CommonDispenserRegisters::DataProviders common_data_providers{
|
||||
manufacturer, model, protocol_version, hardware_version, software_version};
|
||||
|
||||
raw_registers::CollectedDispenserRegisters::DataProviders collected_data_providers{
|
||||
charging_connectors_count,
|
||||
esn_dispenser,
|
||||
time_sync,
|
||||
};
|
||||
|
||||
raw_registers::AlarmDispenserRegisters::DataProviders alarm_data_providers{
|
||||
door_status_alarm,
|
||||
water_alarm,
|
||||
epo_alarm,
|
||||
tilt_alarm,
|
||||
};
|
||||
|
||||
registry.add(std::make_unique<raw_registers::CommonDispenserRegisters>(common_data_providers));
|
||||
registry.add(std::make_unique<raw_registers::CollectedDispenserRegisters>(collected_data_providers));
|
||||
registry.add(std::make_unique<raw_registers::AlarmDispenserRegisters>(alarm_data_providers));
|
||||
}
|
||||
};
|
||||
|
||||
}; // namespace fusion_charger::modbus_driver
|
||||
@@ -0,0 +1,866 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
|
||||
#include "modbus-registers/data_provider.hpp"
|
||||
#include "raw.hpp"
|
||||
|
||||
namespace fusion_charger::modbus_driver {
|
||||
|
||||
using namespace fusion_charger::modbus_driver::raw_registers;
|
||||
|
||||
enum class ErrorCategory : std::uint16_t {
|
||||
PowerUnit = 0,
|
||||
ChargingPowerUnit = 1,
|
||||
AcBranch = 2,
|
||||
AcDcRectifier = 3,
|
||||
DcDcChargingModule = 4,
|
||||
CoolingSection = 5,
|
||||
ErrorSubcategoryPowerDistributionModule = 6,
|
||||
};
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os, const ErrorCategory& category) {
|
||||
switch (category) {
|
||||
case ErrorCategory::PowerUnit:
|
||||
os << "PowerUnit";
|
||||
break;
|
||||
case ErrorCategory::ChargingPowerUnit:
|
||||
os << "ChargingPowerUnit";
|
||||
break;
|
||||
case ErrorCategory::AcBranch:
|
||||
os << "AcBranch";
|
||||
break;
|
||||
case ErrorCategory::AcDcRectifier:
|
||||
os << "AcDcRectifier";
|
||||
break;
|
||||
case ErrorCategory::DcDcChargingModule:
|
||||
os << "DcDcChargingModule";
|
||||
break;
|
||||
case ErrorCategory::CoolingSection:
|
||||
os << "CoolingSection";
|
||||
break;
|
||||
case ErrorCategory::ErrorSubcategoryPowerDistributionModule:
|
||||
os << "ErrorSubcategoryPowerDistributionModule";
|
||||
break;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
enum class ErrorSubcategoryPowerUnit : std::uint16_t {
|
||||
HighVoltageDoorStatusSensor = 0,
|
||||
DoorStatusSensor = 1,
|
||||
Water = 2,
|
||||
Smoke = 3,
|
||||
Epo = 4,
|
||||
};
|
||||
|
||||
enum class ErrorSubcategoryChargingPowerUnit : std::uint16_t {
|
||||
UnknownSystemType = 0,
|
||||
PowerDetectionException = 1,
|
||||
SyncrhonizationCableStatusFaultOfEnergyRoutingBoard = 2,
|
||||
SoftStartFault = 3,
|
||||
SoftStartModuleCommunicationFailure = 4,
|
||||
SoftStartModuleOverloaded = 5,
|
||||
SoftStartModuleFault = 6,
|
||||
SoftStartModuleOvertemperature = 7,
|
||||
SoftStartModuleUndertemperature = 8,
|
||||
SoftStartModuleDisconnectionFailure = 9,
|
||||
PhaseSequenceAbornmalAlarm = 10,
|
||||
PowerDistributionModuleCommunicationFailure = 11,
|
||||
FaultOfInsulationResistanceToGround = 12,
|
||||
ModbusTcpCertificate = 13
|
||||
};
|
||||
|
||||
enum class ErrorSubcategoryAcBranch : std::uint16_t {
|
||||
AcBranch1 = 0,
|
||||
AcBranch2 = 1,
|
||||
};
|
||||
|
||||
enum class ErrorSubcategoryAcDcRectifier : std::uint16_t {
|
||||
rectifier_1 = 0,
|
||||
rectifier_2 = 1,
|
||||
rectifier_3 = 2,
|
||||
rectifier_4 = 3,
|
||||
rectifier_5 = 4,
|
||||
rectifier_6 = 5,
|
||||
};
|
||||
enum class ErrorSubcategoryDcDcChargingModule : std::uint16_t {
|
||||
DcDcModule1 = 0,
|
||||
DcDcModule2 = 1,
|
||||
DcDcModule3 = 2,
|
||||
DcDcModule4 = 3,
|
||||
DcDcModule5 = 4,
|
||||
DcDcModule6 = 5,
|
||||
DcDcModule7 = 6,
|
||||
DcDcModule8 = 7,
|
||||
DcDcModule9 = 8,
|
||||
DcDcModule10 = 9,
|
||||
DcDcModule11 = 10,
|
||||
DcDcModule12 = 11,
|
||||
};
|
||||
|
||||
enum class ErrorSubcategoryCoolingSection : std::uint16_t {
|
||||
CoolingUnit1 = 0,
|
||||
};
|
||||
|
||||
enum class ErrorSubcategoryPowerDistributionModule : std::uint16_t {
|
||||
PowerDistributionModule1 = 0,
|
||||
PowerDistributionModule2 = 1,
|
||||
PowerDistributionModule3 = 2,
|
||||
PowerDistributionModule4 = 3,
|
||||
PowerDistributionModule5 = 4,
|
||||
};
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os, const ErrorSubcategoryPowerUnit& subcategory) {
|
||||
switch (subcategory) {
|
||||
case ErrorSubcategoryPowerUnit::HighVoltageDoorStatusSensor:
|
||||
os << "HighVoltageDoorStatusSensor";
|
||||
break;
|
||||
case ErrorSubcategoryPowerUnit::DoorStatusSensor:
|
||||
os << "DoorStatusSensor";
|
||||
break;
|
||||
case ErrorSubcategoryPowerUnit::Water:
|
||||
os << "Water";
|
||||
break;
|
||||
case ErrorSubcategoryPowerUnit::Smoke:
|
||||
os << "Smoke";
|
||||
break;
|
||||
case ErrorSubcategoryPowerUnit::Epo:
|
||||
os << "Epo";
|
||||
break;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os, const ErrorSubcategoryChargingPowerUnit& subcategory) {
|
||||
switch (subcategory) {
|
||||
case ErrorSubcategoryChargingPowerUnit::UnknownSystemType:
|
||||
os << "UnknownSystemType";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::PowerDetectionException:
|
||||
os << "PowerDetectionException";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::SyncrhonizationCableStatusFaultOfEnergyRoutingBoard:
|
||||
os << "SyncrhonizationCableStatusFaultOfEnergyRoutingBoard";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartFault:
|
||||
os << "SoftStartFault";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartModuleCommunicationFailure:
|
||||
os << "SoftStartModuleCommunicationFailure";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartModuleOverloaded:
|
||||
os << "SoftStartModuleOverloaded";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartModuleFault:
|
||||
os << "SoftStartModuleFault";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartModuleOvertemperature:
|
||||
os << "SoftStartModuleOvertemperature";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartModuleUndertemperature:
|
||||
os << "SoftStartModuleUndertemperature";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartModuleDisconnectionFailure:
|
||||
os << "SoftStartModuleDisconnectionFailure";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::PhaseSequenceAbornmalAlarm:
|
||||
os << "PhaseSequenceAbornmalAlarm";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::PowerDistributionModuleCommunicationFailure:
|
||||
os << "PowerDistributionModuleCommunicationFailure";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::FaultOfInsulationResistanceToGround:
|
||||
os << "FaultOfInsulationResistanceToGround";
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::ModbusTcpCertificate:
|
||||
os << "ModbusTcpCertificate";
|
||||
break;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os, const ErrorSubcategoryAcBranch& subcategory) {
|
||||
switch (subcategory) {
|
||||
case ErrorSubcategoryAcBranch::AcBranch1:
|
||||
os << "AcBranch1";
|
||||
break;
|
||||
case ErrorSubcategoryAcBranch::AcBranch2:
|
||||
os << "AcBranch2";
|
||||
break;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os, const ErrorSubcategoryAcDcRectifier& subcategory) {
|
||||
switch (subcategory) {
|
||||
case ErrorSubcategoryAcDcRectifier::rectifier_1:
|
||||
os << "rectifier_1";
|
||||
break;
|
||||
case ErrorSubcategoryAcDcRectifier::rectifier_2:
|
||||
os << "rectifier_2";
|
||||
break;
|
||||
case ErrorSubcategoryAcDcRectifier::rectifier_3:
|
||||
os << "rectifier_3";
|
||||
break;
|
||||
case ErrorSubcategoryAcDcRectifier::rectifier_4:
|
||||
os << "rectifier_4";
|
||||
break;
|
||||
case ErrorSubcategoryAcDcRectifier::rectifier_5:
|
||||
os << "rectifier_5";
|
||||
break;
|
||||
case ErrorSubcategoryAcDcRectifier::rectifier_6:
|
||||
os << "rectifier_6";
|
||||
break;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os, const ErrorSubcategoryDcDcChargingModule& subcategory) {
|
||||
switch (subcategory) {
|
||||
case ErrorSubcategoryDcDcChargingModule::DcDcModule1:
|
||||
os << "DcDcModule1";
|
||||
break;
|
||||
case ErrorSubcategoryDcDcChargingModule::DcDcModule2:
|
||||
os << "DcDcModule2";
|
||||
break;
|
||||
case ErrorSubcategoryDcDcChargingModule::DcDcModule3:
|
||||
os << "DcDcModule3";
|
||||
break;
|
||||
case ErrorSubcategoryDcDcChargingModule::DcDcModule4:
|
||||
os << "DcDcModule4";
|
||||
break;
|
||||
case ErrorSubcategoryDcDcChargingModule::DcDcModule5:
|
||||
os << "DcDcModule5";
|
||||
break;
|
||||
case ErrorSubcategoryDcDcChargingModule::DcDcModule6:
|
||||
os << "DcDcModule6";
|
||||
break;
|
||||
case ErrorSubcategoryDcDcChargingModule::DcDcModule7:
|
||||
os << "DcDcModule7";
|
||||
break;
|
||||
case ErrorSubcategoryDcDcChargingModule::DcDcModule8:
|
||||
os << "DcDcModule8";
|
||||
break;
|
||||
case ErrorSubcategoryDcDcChargingModule::DcDcModule9:
|
||||
os << "DcDcModule9";
|
||||
break;
|
||||
case ErrorSubcategoryDcDcChargingModule::DcDcModule10:
|
||||
os << "DcDcModule10";
|
||||
break;
|
||||
case ErrorSubcategoryDcDcChargingModule::DcDcModule11:
|
||||
os << "DcDcModule11";
|
||||
break;
|
||||
case ErrorSubcategoryDcDcChargingModule::DcDcModule12:
|
||||
os << "DcDcModule12";
|
||||
break;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os, const ErrorSubcategoryCoolingSection& subcategory) {
|
||||
switch (subcategory) {
|
||||
case ErrorSubcategoryCoolingSection::CoolingUnit1:
|
||||
os << "CoolingUnit1";
|
||||
break;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os, const ErrorSubcategoryPowerDistributionModule& subcategory) {
|
||||
switch (subcategory) {
|
||||
case ErrorSubcategoryPowerDistributionModule::PowerDistributionModule1:
|
||||
os << "PowerDistributionModule1";
|
||||
break;
|
||||
case ErrorSubcategoryPowerDistributionModule::PowerDistributionModule2:
|
||||
os << "PowerDistributionModule2";
|
||||
break;
|
||||
case ErrorSubcategoryPowerDistributionModule::PowerDistributionModule3:
|
||||
os << "PowerDistributionModule3";
|
||||
break;
|
||||
case ErrorSubcategoryPowerDistributionModule::PowerDistributionModule4:
|
||||
os << "PowerDistributionModule4";
|
||||
break;
|
||||
case ErrorSubcategoryPowerDistributionModule::PowerDistributionModule5:
|
||||
os << "PowerDistributionModule5";
|
||||
break;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
union ErrorSubcategory {
|
||||
ErrorSubcategoryPowerUnit power_unit;
|
||||
ErrorSubcategoryChargingPowerUnit charging_power_unit;
|
||||
ErrorSubcategoryAcBranch ac_branch;
|
||||
ErrorSubcategoryAcDcRectifier ac_dc_rectifier;
|
||||
ErrorSubcategoryDcDcChargingModule dc_dc_charging_module;
|
||||
ErrorSubcategoryCoolingSection cooling_section;
|
||||
ErrorSubcategoryPowerDistributionModule power_distribution_module;
|
||||
std::uint16_t raw;
|
||||
|
||||
bool operator<(const ErrorSubcategory& rhs) const {
|
||||
return raw < rhs.raw;
|
||||
}
|
||||
bool operator==(const ErrorSubcategory& rhs) const {
|
||||
return raw == rhs.raw;
|
||||
}
|
||||
bool operator!=(const ErrorSubcategory& rhs) const {
|
||||
return raw != rhs.raw;
|
||||
}
|
||||
};
|
||||
|
||||
union ErrorPayload {
|
||||
std::uint32_t error_flags;
|
||||
AlarmStatus alarm;
|
||||
std::uint32_t raw;
|
||||
|
||||
bool is_error() const {
|
||||
return raw != 0;
|
||||
}
|
||||
bool operator==(const ErrorPayload& rhs) const {
|
||||
return raw == rhs.raw;
|
||||
}
|
||||
bool operator!=(const ErrorPayload& rhs) const {
|
||||
return raw != rhs.raw;
|
||||
}
|
||||
};
|
||||
|
||||
struct ErrorEvent {
|
||||
ErrorCategory error_category;
|
||||
ErrorSubcategory error_subcategory;
|
||||
ErrorPayload payload;
|
||||
|
||||
bool operator==(const ErrorEvent& rhs) const {
|
||||
return error_category == rhs.error_category && error_subcategory.raw == rhs.error_subcategory.raw &&
|
||||
payload.raw == rhs.payload.raw;
|
||||
}
|
||||
bool operator!=(const ErrorEvent& rhs) const {
|
||||
return !(*this == rhs);
|
||||
}
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& os, const ErrorEvent& errorEvent) {
|
||||
os << "Category: " << static_cast<std::uint16_t>(errorEvent.error_category)
|
||||
<< "; Subcategory: " << static_cast<std::uint16_t>(errorEvent.error_subcategory.charging_power_unit)
|
||||
<< "; Flags: " << errorEvent.payload.error_flags << std::endl;
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
std::string to_everest_subtype() const {
|
||||
std::stringstream oss;
|
||||
|
||||
switch (error_category) {
|
||||
case ErrorCategory::PowerUnit: {
|
||||
oss << "PowerUnit"
|
||||
<< "/";
|
||||
ErrorSubcategoryPowerUnit subcategory = error_subcategory.power_unit;
|
||||
oss << subcategory;
|
||||
return oss.str();
|
||||
}
|
||||
case ErrorCategory::ChargingPowerUnit: {
|
||||
oss << "ChargingPowerUnit"
|
||||
<< "/";
|
||||
auto subcategory = error_subcategory.charging_power_unit;
|
||||
oss << subcategory;
|
||||
return oss.str();
|
||||
}
|
||||
case ErrorCategory::AcBranch: {
|
||||
oss << "AcBranch"
|
||||
<< "/";
|
||||
auto subcategory = error_subcategory.ac_branch;
|
||||
oss << subcategory;
|
||||
return oss.str();
|
||||
}
|
||||
case ErrorCategory::AcDcRectifier: {
|
||||
oss << "AcDcRectifier"
|
||||
<< "/";
|
||||
auto subcategory = error_subcategory.ac_dc_rectifier;
|
||||
oss << subcategory;
|
||||
return oss.str();
|
||||
}
|
||||
case ErrorCategory::DcDcChargingModule: {
|
||||
oss << "DcDcChargingModule"
|
||||
<< "/";
|
||||
auto subcategory = error_subcategory.dc_dc_charging_module;
|
||||
oss << subcategory;
|
||||
return oss.str();
|
||||
}
|
||||
case ErrorCategory::CoolingSection: {
|
||||
oss << "CoolingSection"
|
||||
<< "/";
|
||||
auto subcategory = error_subcategory.cooling_section;
|
||||
oss << subcategory;
|
||||
return oss.str();
|
||||
}
|
||||
case ErrorCategory::ErrorSubcategoryPowerDistributionModule: {
|
||||
oss << "PowerDistributionModule"
|
||||
<< "/";
|
||||
auto subcategory = error_subcategory.power_distribution_module;
|
||||
oss << subcategory;
|
||||
return oss.str();
|
||||
}
|
||||
}
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string to_error_log_string() const {
|
||||
std::stringstream oss;
|
||||
|
||||
oss << "Category: " << error_category << "; ";
|
||||
|
||||
switch (error_category) {
|
||||
case ErrorCategory::PowerUnit: {
|
||||
ErrorSubcategoryPowerUnit subcategory = error_subcategory.power_unit;
|
||||
oss << "Subcategory: " << subcategory << "; ";
|
||||
oss << "AlarmState: " << payload.alarm;
|
||||
break;
|
||||
}
|
||||
case ErrorCategory::ChargingPowerUnit: {
|
||||
auto subcategory = error_subcategory.charging_power_unit;
|
||||
|
||||
switch (subcategory) {
|
||||
case ErrorSubcategoryChargingPowerUnit::UnknownSystemType:
|
||||
case ErrorSubcategoryChargingPowerUnit::PowerDetectionException:
|
||||
case ErrorSubcategoryChargingPowerUnit::SyncrhonizationCableStatusFaultOfEnergyRoutingBoard:
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartFault:
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartModuleCommunicationFailure:
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartModuleOverloaded:
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartModuleFault:
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartModuleOvertemperature:
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartModuleUndertemperature:
|
||||
case ErrorSubcategoryChargingPowerUnit::SoftStartModuleDisconnectionFailure:
|
||||
case ErrorSubcategoryChargingPowerUnit::PhaseSequenceAbornmalAlarm:
|
||||
case ErrorSubcategoryChargingPowerUnit::PowerDistributionModuleCommunicationFailure:
|
||||
case ErrorSubcategoryChargingPowerUnit::FaultOfInsulationResistanceToGround:
|
||||
oss << "Subcategory: " << subcategory << "; ";
|
||||
oss << "AlarmState: " << payload.alarm;
|
||||
|
||||
break;
|
||||
case ErrorSubcategoryChargingPowerUnit::ModbusTcpCertificate:
|
||||
oss << "Subcategory: " << subcategory << "; ";
|
||||
oss << "Flags: 0x";
|
||||
oss << std::hex << std::setw(4) << std::setfill('0') << payload.error_flags;
|
||||
break;
|
||||
}
|
||||
|
||||
} break;
|
||||
case ErrorCategory::AcBranch: {
|
||||
auto subcategory = error_subcategory.ac_branch;
|
||||
oss << "Subcategory: " << subcategory << "; ";
|
||||
oss << "Flags: 0x";
|
||||
oss << std::hex << std::setw(8) << std::setfill('0') << payload.error_flags;
|
||||
break;
|
||||
}
|
||||
case ErrorCategory::AcDcRectifier: {
|
||||
auto subcategory = error_subcategory.ac_dc_rectifier;
|
||||
oss << "Subcategory: " << subcategory << "; ";
|
||||
oss << "Flags: 0x";
|
||||
oss << std::hex << std::setw(8) << std::setfill('0') << payload.error_flags;
|
||||
break;
|
||||
}
|
||||
case ErrorCategory::DcDcChargingModule: {
|
||||
auto subcategory = error_subcategory.dc_dc_charging_module;
|
||||
oss << "Subcategory: " << subcategory << "; ";
|
||||
oss << "Flags: 0x";
|
||||
oss << std::hex << std::setw(8) << std::setfill('0') << payload.error_flags;
|
||||
break;
|
||||
}
|
||||
case ErrorCategory::CoolingSection: {
|
||||
auto subcategory = error_subcategory.cooling_section;
|
||||
oss << "Subcategory: " << subcategory << "; ";
|
||||
oss << "Flags: 0x";
|
||||
oss << std::hex << std::setw(8) << std::setfill('0') << payload.error_flags;
|
||||
break;
|
||||
}
|
||||
case ErrorCategory::ErrorSubcategoryPowerDistributionModule: {
|
||||
auto subcategory = error_subcategory.power_distribution_module;
|
||||
oss << "Subcategory: " << subcategory << "; ";
|
||||
oss << "Flags: 0x";
|
||||
oss << std::hex << std::setw(8) << std::setfill('0') << payload.error_flags;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
bool operator<(const ErrorEvent& rhs) const {
|
||||
if (error_category != rhs.error_category) {
|
||||
return error_category < rhs.error_category;
|
||||
}
|
||||
|
||||
return error_subcategory.raw < rhs.error_subcategory.raw;
|
||||
}
|
||||
};
|
||||
|
||||
struct ErrorEventComparator {
|
||||
bool operator()(const ErrorEvent& a, const ErrorEvent& b) const {
|
||||
return a < b;
|
||||
}
|
||||
};
|
||||
|
||||
class ErrorRegisters {
|
||||
public:
|
||||
ErrorRegisters() {
|
||||
}
|
||||
|
||||
void add_to_registry(modbus::registers::registry::ComplexRegisterRegistry& registry) {
|
||||
raw_registers::AlarmPowerUnitRegisters::DataProviders alarm_power_unit_providers{
|
||||
power_unit.high_voltage_door_status_sensor,
|
||||
power_unit.door_status_sensor,
|
||||
power_unit.water,
|
||||
power_unit.smoke,
|
||||
power_unit.epo,
|
||||
};
|
||||
registry.add(std::make_unique<raw_registers::AlarmPowerUnitRegisters>(alarm_power_unit_providers));
|
||||
|
||||
raw_registers::AlarmChargingPowerUnitRegisters::DataProviders alarm_charging_power_unit_providers{
|
||||
charging_power_unit.unknown_system_type,
|
||||
charging_power_unit.power_detection_exception,
|
||||
charging_power_unit.syncrhonization_cable_status_fault_of_energy_routing_board,
|
||||
charging_power_unit.soft_start_fault,
|
||||
charging_power_unit.soft_start_module_communication_failure,
|
||||
charging_power_unit.soft_start_module_overloaded,
|
||||
charging_power_unit.soft_start_module_fault,
|
||||
charging_power_unit.soft_start_module_overtemperature,
|
||||
charging_power_unit.soft_start_module_undertemperature,
|
||||
charging_power_unit.soft_start_module_disconnection_failure,
|
||||
charging_power_unit.phase_sequence_abornmal_alarm,
|
||||
charging_power_unit.power_distribution_module_communication_failure,
|
||||
charging_power_unit.fault_of_insulation_resistance_to_ground,
|
||||
charging_power_unit.modbus_tcp_certificate};
|
||||
|
||||
registry.add(
|
||||
std::make_unique<raw_registers::AlarmChargingPowerUnitRegisters>(alarm_charging_power_unit_providers));
|
||||
|
||||
raw_registers::AlarmAcBranchRegisters::DataProviders alarm_ac_branch_providers{ac_branch.ac_branch_1,
|
||||
ac_branch.ac_branch_2};
|
||||
registry.add(std::make_unique<raw_registers::AlarmAcBranchRegisters>(alarm_ac_branch_providers));
|
||||
|
||||
raw_registers::AlarmAcDcRectifierRegisters::DataProviders ac_dc_rectifier_providers{
|
||||
ac_dc_rectifier.rectifier_1, ac_dc_rectifier.rectifier_2, ac_dc_rectifier.rectifier_3,
|
||||
ac_dc_rectifier.rectifier_4, ac_dc_rectifier.rectifier_5, ac_dc_rectifier.rectifier_6};
|
||||
registry.add(std::make_unique<raw_registers::AlarmAcDcRectifierRegisters>(ac_dc_rectifier_providers));
|
||||
|
||||
raw_registers::DcDcChargingModuleRegisters::DataProviders dc_dc_charging_module_providers{
|
||||
dc_dc_charging_module.dc_dc_module_1, dc_dc_charging_module.dc_dc_module_2,
|
||||
dc_dc_charging_module.dc_dc_module_3, dc_dc_charging_module.dc_dc_module_4,
|
||||
dc_dc_charging_module.dc_dc_module_5, dc_dc_charging_module.dc_dc_module_6,
|
||||
dc_dc_charging_module.dc_dc_module_7, dc_dc_charging_module.dc_dc_module_8,
|
||||
dc_dc_charging_module.dc_dc_module_9, dc_dc_charging_module.dc_dc_module_10,
|
||||
dc_dc_charging_module.dc_dc_module_11, dc_dc_charging_module.dc_dc_module_12};
|
||||
registry.add(std::make_unique<raw_registers::DcDcChargingModuleRegisters>(dc_dc_charging_module_providers));
|
||||
|
||||
raw_registers::CoolingSectionRegisters::DataProviders cooling_section_providers{cooling_section.cooling_unit_1};
|
||||
registry.add(std::make_unique<raw_registers::CoolingSectionRegisters>(cooling_section_providers));
|
||||
|
||||
raw_registers::PowerDistributionModuleRegisters::DataProviders power_distribution_module_providers{
|
||||
power_distribution_module.power_distribution_module_1,
|
||||
power_distribution_module.power_distribution_module_2,
|
||||
power_distribution_module.power_distribution_module_3,
|
||||
power_distribution_module.power_distribution_module_4,
|
||||
power_distribution_module.power_distribution_module_5};
|
||||
registry.add(
|
||||
std::make_unique<raw_registers::PowerDistributionModuleRegisters>(power_distribution_module_providers));
|
||||
}
|
||||
|
||||
void add_callback(std::function<void(ErrorEvent)> callback) {
|
||||
add_callback_to_alarm_power_unit_registers(callback);
|
||||
add_callback_to_alarm_charging_power_unit_registers(callback);
|
||||
add_callback_to_ac_branch_registers(callback);
|
||||
add_callback_to_ac_dc_rectifier_registers(callback);
|
||||
add_callback_to_dc_dc_charging_module_registers(callback);
|
||||
add_callback_to_cooling_section_registers(callback);
|
||||
add_callback_to_power_distribution_module_registers(callback);
|
||||
}
|
||||
|
||||
private:
|
||||
struct {
|
||||
DataProviderHolding<AlarmStatus> high_voltage_door_status_sensor =
|
||||
DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> door_status_sensor = DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> water = DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> smoke = DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> epo = DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
} power_unit;
|
||||
|
||||
struct {
|
||||
DataProviderHolding<AlarmStatus> unknown_system_type = DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> power_detection_exception =
|
||||
DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> syncrhonization_cable_status_fault_of_energy_routing_board =
|
||||
DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> soft_start_fault = DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> soft_start_module_communication_failure =
|
||||
DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> soft_start_module_overloaded =
|
||||
DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> soft_start_module_fault =
|
||||
DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> soft_start_module_overtemperature =
|
||||
DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> soft_start_module_undertemperature =
|
||||
DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> soft_start_module_disconnection_failure =
|
||||
DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> phase_sequence_abornmal_alarm =
|
||||
DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> power_distribution_module_communication_failure =
|
||||
DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<AlarmStatus> fault_of_insulation_resistance_to_ground =
|
||||
DataProviderHolding<AlarmStatus>(AlarmStatus::NORMAL);
|
||||
DataProviderHolding<std::uint16_t> modbus_tcp_certificate = DataProviderHolding<std::uint16_t>(0);
|
||||
} charging_power_unit;
|
||||
|
||||
struct {
|
||||
DataProviderHolding<std::uint32_t> ac_branch_1 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> ac_branch_2 = DataProviderHolding<std::uint32_t>(0);
|
||||
} ac_branch;
|
||||
|
||||
struct {
|
||||
DataProviderHolding<std::uint32_t> rectifier_1 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> rectifier_2 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> rectifier_3 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> rectifier_4 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> rectifier_5 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> rectifier_6 = DataProviderHolding<std::uint32_t>(0);
|
||||
} ac_dc_rectifier;
|
||||
|
||||
struct {
|
||||
DataProviderHolding<std::uint32_t> dc_dc_module_1 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> dc_dc_module_2 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> dc_dc_module_3 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> dc_dc_module_4 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> dc_dc_module_5 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> dc_dc_module_6 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> dc_dc_module_7 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> dc_dc_module_8 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> dc_dc_module_9 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> dc_dc_module_10 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> dc_dc_module_11 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> dc_dc_module_12 = DataProviderHolding<std::uint32_t>(0);
|
||||
} dc_dc_charging_module;
|
||||
|
||||
struct {
|
||||
DataProviderHolding<std::uint32_t> cooling_unit_1 = DataProviderHolding<std::uint32_t>(0);
|
||||
} cooling_section;
|
||||
|
||||
struct {
|
||||
DataProviderHolding<std::uint32_t> power_distribution_module_1 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> power_distribution_module_2 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> power_distribution_module_3 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> power_distribution_module_4 = DataProviderHolding<std::uint32_t>(0);
|
||||
DataProviderHolding<std::uint32_t> power_distribution_module_5 = DataProviderHolding<std::uint32_t>(0);
|
||||
} power_distribution_module;
|
||||
|
||||
#define ERROR_ALARM_CALLBACK(CATEGORY, SUBCATEGORY) \
|
||||
[callback](AlarmStatus register_value) { \
|
||||
struct ErrorEvent event; \
|
||||
event.error_category = CATEGORY; \
|
||||
event.error_subcategory = SUBCATEGORY; \
|
||||
event.payload.alarm = register_value; \
|
||||
callback(event); \
|
||||
}
|
||||
|
||||
#define ERROR_BITFLAGS_CALLBACK(CATEGORY, SUBCATEGORY) \
|
||||
[callback](std::uint32_t register_value) { \
|
||||
struct ErrorEvent event; \
|
||||
event.error_category = CATEGORY; \
|
||||
event.error_subcategory = SUBCATEGORY; \
|
||||
event.payload.error_flags = register_value; \
|
||||
callback(event); \
|
||||
}
|
||||
|
||||
#define POWER_UNIT_ALARM_CALLBACK(SUBCATEGORY) \
|
||||
ERROR_ALARM_CALLBACK(ErrorCategory::PowerUnit, ErrorSubcategory{.power_unit = SUBCATEGORY})
|
||||
|
||||
#define CHARGING_POWER_UNIT_ALARM_CALLBACK(SUBCATEGORY) \
|
||||
ERROR_ALARM_CALLBACK(ErrorCategory::ChargingPowerUnit, ErrorSubcategory{.charging_power_unit = SUBCATEGORY})
|
||||
|
||||
#define AC_BRANCH_BITFLAGS_CALLBACK(SUBCATEGORY) \
|
||||
ERROR_BITFLAGS_CALLBACK(ErrorCategory::AcBranch, ErrorSubcategory{.ac_branch = SUBCATEGORY})
|
||||
|
||||
#define AC_DC_RECTIFIER_BITFLAGS_CALLBACK(SUBCATEGORY) \
|
||||
ERROR_BITFLAGS_CALLBACK(ErrorCategory::AcDcRectifier, ErrorSubcategory{.ac_dc_rectifier = SUBCATEGORY})
|
||||
|
||||
#define DC_DC_CHARGING_BITFLAGS_CALLBACK(SUBCATEGORY) \
|
||||
ERROR_BITFLAGS_CALLBACK(ErrorCategory::DcDcChargingModule, ErrorSubcategory{.dc_dc_charging_module = SUBCATEGORY})
|
||||
|
||||
#define COOLING_SECTION_BITFLAGS_CALLBACK(SUBCATEGORY) \
|
||||
ERROR_BITFLAGS_CALLBACK(ErrorCategory::CoolingSection, ErrorSubcategory{.cooling_section = SUBCATEGORY})
|
||||
|
||||
#define POWER_DISTRIBUTION_BITFLAGS_CALLBACK(SUBCATEGORY) \
|
||||
ERROR_BITFLAGS_CALLBACK(ErrorCategory::ErrorSubcategoryPowerDistributionModule, \
|
||||
ErrorSubcategory{.power_distribution_module = SUBCATEGORY})
|
||||
|
||||
void add_callback_to_alarm_power_unit_registers(std::function<void(ErrorEvent)> callback) {
|
||||
power_unit.high_voltage_door_status_sensor.add_write_callback(
|
||||
POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryPowerUnit::HighVoltageDoorStatusSensor));
|
||||
|
||||
power_unit.door_status_sensor.add_write_callback(
|
||||
POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryPowerUnit::DoorStatusSensor));
|
||||
|
||||
power_unit.water.add_write_callback(POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryPowerUnit::Water));
|
||||
|
||||
power_unit.smoke.add_write_callback(POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryPowerUnit::Smoke));
|
||||
|
||||
power_unit.epo.add_write_callback(POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryPowerUnit::Epo));
|
||||
}
|
||||
|
||||
void add_callback_to_alarm_charging_power_unit_registers(std::function<void(ErrorEvent)> callback) {
|
||||
charging_power_unit.unknown_system_type.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryChargingPowerUnit::UnknownSystemType));
|
||||
|
||||
charging_power_unit.power_detection_exception.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryChargingPowerUnit::PowerDetectionException));
|
||||
|
||||
charging_power_unit.syncrhonization_cable_status_fault_of_energy_routing_board.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(
|
||||
ErrorSubcategoryChargingPowerUnit::SyncrhonizationCableStatusFaultOfEnergyRoutingBoard));
|
||||
|
||||
charging_power_unit.soft_start_fault.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryChargingPowerUnit::SoftStartFault));
|
||||
|
||||
charging_power_unit.soft_start_module_communication_failure.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryChargingPowerUnit::SoftStartModuleCommunicationFailure));
|
||||
|
||||
charging_power_unit.soft_start_module_overloaded.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryChargingPowerUnit::SoftStartModuleOverloaded));
|
||||
|
||||
charging_power_unit.soft_start_module_fault.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryChargingPowerUnit::SoftStartModuleFault));
|
||||
|
||||
charging_power_unit.soft_start_module_overtemperature.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryChargingPowerUnit::SoftStartModuleOvertemperature));
|
||||
|
||||
charging_power_unit.soft_start_module_undertemperature.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryChargingPowerUnit::SoftStartModuleUndertemperature));
|
||||
|
||||
charging_power_unit.soft_start_module_disconnection_failure.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryChargingPowerUnit::SoftStartModuleDisconnectionFailure));
|
||||
|
||||
charging_power_unit.phase_sequence_abornmal_alarm.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryChargingPowerUnit::PhaseSequenceAbornmalAlarm));
|
||||
|
||||
charging_power_unit.power_distribution_module_communication_failure.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(
|
||||
ErrorSubcategoryChargingPowerUnit::PowerDistributionModuleCommunicationFailure));
|
||||
|
||||
charging_power_unit.fault_of_insulation_resistance_to_ground.add_write_callback(
|
||||
CHARGING_POWER_UNIT_ALARM_CALLBACK(ErrorSubcategoryChargingPowerUnit::FaultOfInsulationResistanceToGround));
|
||||
|
||||
// This one is special, because it is the only std::uint16_t flags register
|
||||
charging_power_unit.modbus_tcp_certificate.add_write_callback([callback](std::uint16_t register_value) {
|
||||
struct ErrorEvent event;
|
||||
event.error_category = ErrorCategory::ChargingPowerUnit;
|
||||
event.error_subcategory.charging_power_unit = ErrorSubcategoryChargingPowerUnit::ModbusTcpCertificate;
|
||||
event.payload.error_flags = register_value;
|
||||
callback(event);
|
||||
});
|
||||
}
|
||||
|
||||
void add_callback_to_ac_branch_registers(std::function<void(ErrorEvent)> callback) {
|
||||
ac_branch.ac_branch_1.add_write_callback(AC_BRANCH_BITFLAGS_CALLBACK(ErrorSubcategoryAcBranch::AcBranch1));
|
||||
|
||||
ac_branch.ac_branch_2.add_write_callback(AC_BRANCH_BITFLAGS_CALLBACK(ErrorSubcategoryAcBranch::AcBranch2));
|
||||
}
|
||||
|
||||
void add_callback_to_ac_dc_rectifier_registers(std::function<void(ErrorEvent)> callback) {
|
||||
ac_dc_rectifier.rectifier_1.add_write_callback(
|
||||
AC_DC_RECTIFIER_BITFLAGS_CALLBACK(ErrorSubcategoryAcDcRectifier::rectifier_1));
|
||||
|
||||
ac_dc_rectifier.rectifier_2.add_write_callback(
|
||||
AC_DC_RECTIFIER_BITFLAGS_CALLBACK(ErrorSubcategoryAcDcRectifier::rectifier_2));
|
||||
|
||||
ac_dc_rectifier.rectifier_3.add_write_callback(
|
||||
AC_DC_RECTIFIER_BITFLAGS_CALLBACK(ErrorSubcategoryAcDcRectifier::rectifier_3));
|
||||
|
||||
ac_dc_rectifier.rectifier_4.add_write_callback(
|
||||
AC_DC_RECTIFIER_BITFLAGS_CALLBACK(ErrorSubcategoryAcDcRectifier::rectifier_4));
|
||||
|
||||
ac_dc_rectifier.rectifier_5.add_write_callback(
|
||||
AC_DC_RECTIFIER_BITFLAGS_CALLBACK(ErrorSubcategoryAcDcRectifier::rectifier_5));
|
||||
|
||||
ac_dc_rectifier.rectifier_6.add_write_callback(
|
||||
AC_DC_RECTIFIER_BITFLAGS_CALLBACK(ErrorSubcategoryAcDcRectifier::rectifier_6));
|
||||
}
|
||||
|
||||
void add_callback_to_dc_dc_charging_module_registers(std::function<void(ErrorEvent)> callback) {
|
||||
dc_dc_charging_module.dc_dc_module_1.add_write_callback(
|
||||
DC_DC_CHARGING_BITFLAGS_CALLBACK(ErrorSubcategoryDcDcChargingModule::DcDcModule1));
|
||||
|
||||
dc_dc_charging_module.dc_dc_module_2.add_write_callback(
|
||||
DC_DC_CHARGING_BITFLAGS_CALLBACK(ErrorSubcategoryDcDcChargingModule::DcDcModule2));
|
||||
|
||||
dc_dc_charging_module.dc_dc_module_3.add_write_callback(
|
||||
DC_DC_CHARGING_BITFLAGS_CALLBACK(ErrorSubcategoryDcDcChargingModule::DcDcModule3));
|
||||
|
||||
dc_dc_charging_module.dc_dc_module_4.add_write_callback(
|
||||
DC_DC_CHARGING_BITFLAGS_CALLBACK(ErrorSubcategoryDcDcChargingModule::DcDcModule4));
|
||||
|
||||
dc_dc_charging_module.dc_dc_module_5.add_write_callback(
|
||||
DC_DC_CHARGING_BITFLAGS_CALLBACK(ErrorSubcategoryDcDcChargingModule::DcDcModule5));
|
||||
|
||||
dc_dc_charging_module.dc_dc_module_6.add_write_callback(
|
||||
DC_DC_CHARGING_BITFLAGS_CALLBACK(ErrorSubcategoryDcDcChargingModule::DcDcModule6));
|
||||
|
||||
dc_dc_charging_module.dc_dc_module_7.add_write_callback(
|
||||
DC_DC_CHARGING_BITFLAGS_CALLBACK(ErrorSubcategoryDcDcChargingModule::DcDcModule7));
|
||||
|
||||
dc_dc_charging_module.dc_dc_module_8.add_write_callback(
|
||||
DC_DC_CHARGING_BITFLAGS_CALLBACK(ErrorSubcategoryDcDcChargingModule::DcDcModule8));
|
||||
|
||||
dc_dc_charging_module.dc_dc_module_9.add_write_callback(
|
||||
DC_DC_CHARGING_BITFLAGS_CALLBACK(ErrorSubcategoryDcDcChargingModule::DcDcModule9));
|
||||
|
||||
dc_dc_charging_module.dc_dc_module_10.add_write_callback(
|
||||
DC_DC_CHARGING_BITFLAGS_CALLBACK(ErrorSubcategoryDcDcChargingModule::DcDcModule10));
|
||||
|
||||
dc_dc_charging_module.dc_dc_module_11.add_write_callback(
|
||||
DC_DC_CHARGING_BITFLAGS_CALLBACK(ErrorSubcategoryDcDcChargingModule::DcDcModule11));
|
||||
|
||||
dc_dc_charging_module.dc_dc_module_12.add_write_callback(
|
||||
DC_DC_CHARGING_BITFLAGS_CALLBACK(ErrorSubcategoryDcDcChargingModule::DcDcModule12));
|
||||
}
|
||||
|
||||
void add_callback_to_cooling_section_registers(std::function<void(ErrorEvent)> callback) {
|
||||
cooling_section.cooling_unit_1.add_write_callback(
|
||||
COOLING_SECTION_BITFLAGS_CALLBACK(ErrorSubcategoryCoolingSection::CoolingUnit1));
|
||||
}
|
||||
|
||||
void add_callback_to_power_distribution_module_registers(std::function<void(ErrorEvent)> callback) {
|
||||
power_distribution_module.power_distribution_module_1.add_write_callback(
|
||||
POWER_DISTRIBUTION_BITFLAGS_CALLBACK(ErrorSubcategoryPowerDistributionModule::PowerDistributionModule1));
|
||||
|
||||
power_distribution_module.power_distribution_module_2.add_write_callback(
|
||||
POWER_DISTRIBUTION_BITFLAGS_CALLBACK(ErrorSubcategoryPowerDistributionModule::PowerDistributionModule2));
|
||||
|
||||
power_distribution_module.power_distribution_module_3.add_write_callback(
|
||||
POWER_DISTRIBUTION_BITFLAGS_CALLBACK(ErrorSubcategoryPowerDistributionModule::PowerDistributionModule3));
|
||||
|
||||
power_distribution_module.power_distribution_module_4.add_write_callback(
|
||||
POWER_DISTRIBUTION_BITFLAGS_CALLBACK(ErrorSubcategoryPowerDistributionModule::PowerDistributionModule4));
|
||||
|
||||
power_distribution_module.power_distribution_module_5.add_write_callback(
|
||||
POWER_DISTRIBUTION_BITFLAGS_CALLBACK(ErrorSubcategoryPowerDistributionModule::PowerDistributionModule5));
|
||||
}
|
||||
|
||||
#undef ERROR_ALARM_CALLBACK
|
||||
#undef ERROR_BITFLAGS_CALLBACK
|
||||
#undef POWER_UNIT_ALARM_CALLBACK
|
||||
#undef CHARGING_POWER_UNIT_ALARM_CALLBACK
|
||||
#undef AC_BRANCH_BITFLAGS_CALLBACK
|
||||
#undef AC_DC_RECTIFIER_BITFLAGS_CALLBACK
|
||||
#undef DC_DC_CHARGING_BITFLAGS_CALLBACK
|
||||
#undef COOLING_SECTION_BITFLAGS_CALLBACK
|
||||
#undef POWER_DISTRIBUTION_BITFLAGS_CALLBACK
|
||||
};
|
||||
|
||||
} // namespace fusion_charger::modbus_driver
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user